| OLD | NEW |
| 1 // | 1 // |
| 2 // Copyright 2014 Google Inc. All rights reserved. | 2 // Copyright 2014 Google Inc. All rights reserved. |
| 3 // | 3 // |
| 4 // Use of this source code is governed by a BSD-style | 4 // Use of this source code is governed by a BSD-style |
| 5 // license that can be found in the LICENSE file or at | 5 // license that can be found in the LICENSE file or at |
| 6 // https://developers.google.com/open-source/licenses/bsd | 6 // https://developers.google.com/open-source/licenses/bsd |
| 7 // | 7 // |
| 8 | 8 |
| 9 part of charted.charts; | 9 part of charted.charts; |
| 10 | 10 |
| 11 class StackedBarChartRenderer extends CartesianRendererBase { | 11 class StackedBarChartRenderer extends CartesianRendererBase { |
| 12 static const RADIUS = 2; | 12 static const RADIUS = 2; |
| 13 | 13 |
| 14 final Iterable<int> dimensionsUsingBand = const[0]; | 14 final Iterable<int> dimensionsUsingBand = const [0]; |
| 15 final bool alwaysAnimate; | 15 final bool alwaysAnimate; |
| 16 | 16 |
| 17 @override | 17 @override |
| 18 final String name = "stack-rdr"; | 18 final String name = "stack-rdr"; |
| 19 | 19 |
| 20 /// Used to capture the last measure with data in a data row. This is used | 20 /// Used to capture the last measure with data in a data row. This is used |
| 21 /// to decided whether to round the cornor of the bar or not. | 21 /// to decided whether to round the cornor of the bar or not. |
| 22 List<int> _lastMeasureWithData = []; | 22 List<int> _lastMeasureWithData = []; |
| 23 | 23 |
| 24 StackedBarChartRenderer({this.alwaysAnimate: false}); | 24 StackedBarChartRenderer({this.alwaysAnimate: false}); |
| 25 | 25 |
| 26 /// Returns false if the number of dimension axes on the area is 0. | 26 /// Returns false if the number of dimension axes on the area is 0. |
| 27 /// Otherwise, the first dimension scale is used to render the chart. | 27 /// Otherwise, the first dimension scale is used to render the chart. |
| 28 @override | 28 @override |
| 29 bool prepare(CartesianArea area, ChartSeries series) { | 29 bool prepare(CartesianArea area, ChartSeries series) { |
| 30 _ensureAreaAndSeries(area, series); | 30 _ensureAreaAndSeries(area, series); |
| 31 return true; | 31 return true; |
| 32 } | 32 } |
| 33 | 33 |
| 34 @override | 34 @override |
| 35 void draw(Element element, {Future schedulePostRender}) { | 35 void draw(Element element, {Future schedulePostRender}) { |
| 36 _ensureReadyToDraw(element); | 36 _ensureReadyToDraw(element); |
| 37 var verticalBars = !area.config.isLeftAxisPrimary; | 37 var verticalBars = !area.config.isLeftAxisPrimary; |
| 38 | 38 |
| 39 var measuresCount = series.measures.length, | 39 var measuresCount = series.measures.length, |
| 40 measureScale = area.measureScales(series).first, | 40 measureScale = area.measureScales(series).first, |
| 41 dimensionScale = area.dimensionScales.first; | 41 dimensionScale = area.dimensionScales.first; |
| 42 | 42 |
| 43 var rows = new List() | 43 var rows = new List() |
| 44 ..addAll(area.data.rows.map((e) => | 44 ..addAll(area.data.rows.map((e) => new List.generate(measuresCount, |
| 45 new List.generate(measuresCount, | 45 (i) => e.elementAt(series.measures.elementAt(_reverseIdx(i)))))); |
| 46 (i) => e.elementAt(series.measures.elementAt(_reverseIdx(i)))))); | |
| 47 | 46 |
| 48 var dimensionVals = area.data.rows.map( | 47 var dimensionVals = area.data.rows |
| 49 (row) => row.elementAt(area.config.dimensions.first)).toList(); | 48 .map((row) => row.elementAt(area.config.dimensions.first)) |
| 49 .toList(); |
| 50 | 50 |
| 51 var groups = root.selectAll('.stack-rdr-rowgroup').data(rows); | 51 var groups = root.selectAll('.stack-rdr-rowgroup').data(rows); |
| 52 var animateBarGroups = alwaysAnimate || !groups.isEmpty; | 52 var animateBarGroups = alwaysAnimate || !groups.isEmpty; |
| 53 groups.enter.append('g') | 53 groups.enter.append('g') |
| 54 ..classed('stack-rdr-rowgroup') | 54 ..classed('stack-rdr-rowgroup') |
| 55 ..attrWithCallback('transform', (d, i, c) => verticalBars ? | 55 ..attrWithCallback( |
| 56 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)' : | 56 'transform', |
| 57 'translate(0, ${dimensionScale.scale(dimensionVals[i])})'); | 57 (d, i, c) => verticalBars |
| 58 ? 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)' |
| 59 : 'translate(0, ${dimensionScale.scale(dimensionVals[i])})'); |
| 58 groups.attrWithCallback('data-row', (d, i, e) => i); | 60 groups.attrWithCallback('data-row', (d, i, e) => i); |
| 59 groups.exit.remove(); | 61 groups.exit.remove(); |
| 60 | 62 |
| 61 if (animateBarGroups) { | 63 if (animateBarGroups) { |
| 62 groups.transition() | 64 groups.transition() |
| 63 ..attrWithCallback('transform', (d, i, c) => verticalBars ? | 65 ..attrWithCallback( |
| 64 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)' : | 66 'transform', |
| 65 'translate(0, ${dimensionScale.scale(dimensionVals[i])})') | 67 (d, i, c) => verticalBars |
| 68 ? 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)' |
| 69 : 'translate(0, ${dimensionScale.scale(dimensionVals[i])})') |
| 66 ..duration(theme.transitionDurationMilliseconds); | 70 ..duration(theme.transitionDurationMilliseconds); |
| 67 } | 71 } |
| 68 | 72 |
| 69 var bar = | 73 var bar = |
| 70 groups.selectAll('.stack-rdr-bar').dataWithCallback((d, i, c) => d); | 74 groups.selectAll('.stack-rdr-bar').dataWithCallback((d, i, c) => d); |
| 71 | 75 |
| 72 var prevOffsetVal = new List(); | 76 var prevOffsetVal = new List(); |
| 73 | 77 |
| 74 // Keep track of "y" values. | 78 // Keep track of "y" values. |
| 75 // These are used to insert values in the middle of stack when necessary | 79 // These are used to insert values in the middle of stack when necessary |
| 76 if (animateBarGroups) { | 80 if (animateBarGroups) { |
| 77 bar.each((d, i, e) { | 81 bar.each((d, i, e) { |
| 78 var offset = e.dataset['offset'], | 82 var offset = e.dataset['offset'], |
| 79 offsetVal = offset != null ? int.parse(offset) : 0; | 83 offsetVal = offset != null ? int.parse(offset) : 0; |
| 80 if (i == 0) { | 84 if (i == 0) { |
| 81 prevOffsetVal.add(offsetVal); | 85 prevOffsetVal.add(offsetVal); |
| 82 } else { | 86 } else { |
| 83 prevOffsetVal[prevOffsetVal.length - 1] = offsetVal; | 87 prevOffsetVal[prevOffsetVal.length - 1] = offsetVal; |
| 84 } | 88 } |
| 85 }); | 89 }); |
| 86 } | 90 } |
| 87 | |
| 88 | 91 |
| 89 var barWidth = dimensionScale.rangeBand - theme.defaultStrokeWidth; | 92 var barWidth = dimensionScale.rangeBand - theme.defaultStrokeWidth; |
| 90 | 93 |
| 91 // Calculate height of each segment in the bar. | 94 // Calculate height of each segment in the bar. |
| 92 // Uses prevAllZeroHeight and prevOffset to track previous segments | 95 // Uses prevAllZeroHeight and prevOffset to track previous segments |
| 93 var prevAllZeroHeight = true, | 96 var prevAllZeroHeight = true, prevOffset = 0; |
| 94 prevOffset = 0; | |
| 95 var getBarLength = (d, i) { | 97 var getBarLength = (d, i) { |
| 96 if (!verticalBars) return measureScale.scale(d).round(); | 98 if (!verticalBars) return measureScale.scale(d).round(); |
| 97 var retval = rect.height - measureScale.scale(d).round(); | 99 var retval = rect.height - measureScale.scale(d).round(); |
| 98 if (i != 0) { | 100 if (i != 0) { |
| 99 // If previous bars has 0 height, don't offset for spacing | 101 // If previous bars has 0 height, don't offset for spacing |
| 100 // If any of the previous bar has non 0 height, do the offset. | 102 // If any of the previous bar has non 0 height, do the offset. |
| 101 retval -= prevAllZeroHeight | 103 retval -= prevAllZeroHeight |
| 102 ? 1 | 104 ? 1 |
| 103 : (theme.defaultSeparatorWidth + theme.defaultStrokeWidth); | 105 : (theme.defaultSeparatorWidth + theme.defaultStrokeWidth); |
| 104 retval += prevOffset; | 106 retval += prevOffset; |
| 105 } else { | 107 } else { |
| 106 // When rendering next group of bars, reset prevZeroHeight. | 108 // When rendering next group of bars, reset prevZeroHeight. |
| 107 prevOffset = 0; | 109 prevOffset = 0; |
| 108 prevAllZeroHeight = true; | 110 prevAllZeroHeight = true; |
| 109 retval -= 1; // -1 so bar does not overlap x axis. | 111 retval -= 1; // -1 so bar does not overlap x axis. |
| 110 } | 112 } |
| 111 | 113 |
| 112 if (retval <= 0) { | 114 if (retval <= 0) { |
| 113 prevOffset = prevAllZeroHeight | 115 prevOffset = prevAllZeroHeight |
| 114 ? 0 | 116 ? 0 |
| 115 : theme.defaultSeparatorWidth + theme.defaultStrokeWidth + retval; | 117 : theme.defaultSeparatorWidth + theme.defaultStrokeWidth + retval; |
| 116 retval = 0; | 118 retval = 0; |
| 117 } | 119 } |
| 118 prevAllZeroHeight = (retval == 0) && prevAllZeroHeight; | 120 prevAllZeroHeight = (retval == 0) && prevAllZeroHeight; |
| 119 return retval; | 121 return retval; |
| 120 }; | 122 }; |
| 121 | 123 |
| 122 // Initial "y" position of a bar that is being created. | 124 // Initial "y" position of a bar that is being created. |
| 123 // Only used when animateBarGroups is set to true. | 125 // Only used when animateBarGroups is set to true. |
| 124 var ic = 10000000, | 126 var ic = 10000000, order = 0; |
| 125 order = 0; | |
| 126 var getInitialBarPos = (i) { | 127 var getInitialBarPos = (i) { |
| 127 var tempY; | 128 var tempY; |
| 128 if (i <= ic && i > 0) { | 129 if (i <= ic && i > 0) { |
| 129 tempY = prevOffsetVal[order]; | 130 tempY = prevOffsetVal[order]; |
| 130 order++; | 131 order++; |
| 131 } else { | 132 } else { |
| 132 tempY = verticalBars ? rect.height : 0; | 133 tempY = verticalBars ? rect.height : 0; |
| 133 } | 134 } |
| 134 ic = i; | 135 ic = i; |
| 135 return tempY; | 136 return tempY; |
| (...skipping 20 matching lines...) Expand all Loading... |
| 156 if (yPos != pos) { | 157 if (yPos != pos) { |
| 157 yPos += (theme.defaultSeparatorWidth + theme.defaultStrokeWidth); | 158 yPos += (theme.defaultSeparatorWidth + theme.defaultStrokeWidth); |
| 158 } | 159 } |
| 159 return pos; | 160 return pos; |
| 160 } | 161 } |
| 161 }; | 162 }; |
| 162 | 163 |
| 163 var buildPath = (d, int i, Element e, bool animate, int roundIdx) { | 164 var buildPath = (d, int i, Element e, bool animate, int roundIdx) { |
| 164 var position = animate ? getInitialBarPos(i) : getBarPos(d, i), | 165 var position = animate ? getInitialBarPos(i) : getBarPos(d, i), |
| 165 length = animate ? 0 : getBarLength(d, i), | 166 length = animate ? 0 : getBarLength(d, i), |
| 166 radius = series.measures.elementAt(_reverseIdx(i)) == roundIdx ? RADIU
S : 0, | 167 radius = |
| 168 series.measures.elementAt(_reverseIdx(i)) == roundIdx ? RADIUS : 0, |
| 167 path = (length != 0) | 169 path = (length != 0) |
| 168 ? verticalBars | 170 ? verticalBars |
| 169 ? topRoundedRect(0, position, barWidth, length, radius) | 171 ? topRoundedRect(0, position, barWidth, length, radius) |
| 170 : rightRoundedRect(position, 0, length, barWidth, radius) | 172 : rightRoundedRect(position, 0, length, barWidth, radius) |
| 171 : ''; | 173 : ''; |
| 172 e.attributes['data-offset'] = verticalBars ? | 174 e.attributes['data-offset'] = |
| 173 position.toString() : (position + length).toString(); | 175 verticalBars ? position.toString() : (position + length).toString(); |
| 174 return path; | 176 return path; |
| 175 }; | 177 }; |
| 176 | 178 |
| 177 var enter = bar.enter.appendWithCallback((d, i, e) { | 179 var enter = bar.enter.appendWithCallback((d, i, e) { |
| 178 var rect = Namespace.createChildElement('path', e), | 180 var rect = Namespace.createChildElement('path', e), |
| 179 measure = series.measures.elementAt(_reverseIdx(i)), | 181 measure = series.measures.elementAt(_reverseIdx(i)), |
| 180 row = int.parse(e.dataset['row']), | 182 row = int.parse(e.dataset['row']), |
| 181 color = colorForValue(measure, row), | 183 color = colorForValue(measure, row), |
| 182 filter = filterForValue(measure, row), | 184 filter = filterForValue(measure, row), |
| 183 style = stylesForValue(measure, row), | 185 style = stylesForValue(measure, row), |
| 184 roundIndex = _lastMeasureWithData[row]; | 186 roundIndex = _lastMeasureWithData[row]; |
| 185 | 187 |
| 186 if (!isNullOrEmpty(style)) { | 188 if (!isNullOrEmpty(style)) { |
| 187 rect.classes.addAll(style); | 189 rect.classes.addAll(style); |
| 188 } | 190 } |
| 189 rect.classes.add('stack-rdr-bar'); | 191 rect.classes.add('stack-rdr-bar'); |
| 190 | 192 |
| 191 rect.attributes | 193 rect.attributes |
| 192 ..['d'] = buildPath (d == null ? 0 : d, i, rect, animateBarGroups, | 194 ..['d'] = |
| 193 roundIndex) | 195 buildPath(d == null ? 0 : d, i, rect, animateBarGroups, roundIndex) |
| 194 ..['stroke-width'] = '${theme.defaultStrokeWidth}px' | 196 ..['stroke-width'] = '${theme.defaultStrokeWidth}px' |
| 195 ..['fill'] = color | 197 ..['fill'] = color |
| 196 ..['stroke'] = color; | 198 ..['stroke'] = color; |
| 197 | 199 |
| 198 if (!isNullOrEmpty(filter)) { | 200 if (!isNullOrEmpty(filter)) { |
| 199 rect.attributes['filter'] = filter; | 201 rect.attributes['filter'] = filter; |
| 200 } | 202 } |
| 201 if (!animateBarGroups) { | 203 if (!animateBarGroups) { |
| 202 rect.attributes['data-column'] = '$measure'; | 204 rect.attributes['data-column'] = '$measure'; |
| 203 } | 205 } |
| (...skipping 24 matching lines...) Expand all Loading... |
| 228 } else { | 230 } else { |
| 229 e.attributes['filter'] = filter; | 231 e.attributes['filter'] = filter; |
| 230 } | 232 } |
| 231 }); | 233 }); |
| 232 | 234 |
| 233 bar.transition() | 235 bar.transition() |
| 234 ..attrWithCallback('d', (d, i, e) { | 236 ..attrWithCallback('d', (d, i, e) { |
| 235 var row = int.parse(e.parent.dataset['row']), | 237 var row = int.parse(e.parent.dataset['row']), |
| 236 roundIndex = _lastMeasureWithData[row]; | 238 roundIndex = _lastMeasureWithData[row]; |
| 237 return buildPath(d == null ? 0 : d, i, e, false, roundIndex); | 239 return buildPath(d == null ? 0 : d, i, e, false, roundIndex); |
| 238 }); | 240 }); |
| 239 } | 241 } |
| 240 | 242 |
| 241 bar.exit.remove(); | 243 bar.exit.remove(); |
| 242 } | 244 } |
| 243 | 245 |
| 244 @override | 246 @override |
| 245 void dispose() { | 247 void dispose() { |
| 246 if (root == null) return; | 248 if (root == null) return; |
| 247 root.selectAll('.stack-rdr-rowgroup').remove(); | 249 root.selectAll('.stack-rdr-rowgroup').remove(); |
| 248 } | 250 } |
| (...skipping 29 matching lines...) Expand all Loading... |
| 278 }); | 280 }); |
| 279 | 281 |
| 280 return new Extent(min, max); | 282 return new Extent(min, max); |
| 281 } | 283 } |
| 282 | 284 |
| 283 @override | 285 @override |
| 284 void handleStateChanges(List<ChangeRecord> changes) { | 286 void handleStateChanges(List<ChangeRecord> changes) { |
| 285 var groups = host.querySelectorAll('.stack-rdr-rowgroup'); | 287 var groups = host.querySelectorAll('.stack-rdr-rowgroup'); |
| 286 if (groups == null || groups.isEmpty) return; | 288 if (groups == null || groups.isEmpty) return; |
| 287 | 289 |
| 288 for(int i = 0, len = groups.length; i < len; ++i) { | 290 for (int i = 0, len = groups.length; i < len; ++i) { |
| 289 var group = groups.elementAt(i), | 291 var group = groups.elementAt(i), |
| 290 bars = group.querySelectorAll('.stack-rdr-bar'), | 292 bars = group.querySelectorAll('.stack-rdr-bar'), |
| 291 row = int.parse(group.dataset['row']); | 293 row = int.parse(group.dataset['row']); |
| 292 | 294 |
| 293 for(int j = 0, barsCount = bars.length; j < barsCount; ++j) { | 295 for (int j = 0, barsCount = bars.length; j < barsCount; ++j) { |
| 294 var bar = bars.elementAt(j), | 296 var bar = bars.elementAt(j), |
| 295 column = int.parse(bar.dataset['column']), | 297 column = int.parse(bar.dataset['column']), |
| 296 color = colorForValue(column, row), | 298 color = colorForValue(column, row), |
| 297 filter = filterForValue(column, row); | 299 filter = filterForValue(column, row); |
| 298 | 300 |
| 299 bar.classes.removeAll(ChartState.VALUE_CLASS_NAMES); | 301 bar.classes.removeAll(ChartState.VALUE_CLASS_NAMES); |
| 300 bar.classes.addAll(stylesForValue(column, row)); | 302 bar.classes.addAll(stylesForValue(column, row)); |
| 301 bar.attributes | 303 bar.attributes |
| 302 ..['fill'] = color | 304 ..['fill'] = color |
| 303 ..['stroke'] = color; | 305 ..['stroke'] = color; |
| 304 if (isNullOrEmpty(filter)) { | 306 if (isNullOrEmpty(filter)) { |
| 305 bar.attributes.remove('filter'); | 307 bar.attributes.remove('filter'); |
| 306 } else { | 308 } else { |
| 307 bar.attributes['filter'] = filter; | 309 bar.attributes['filter'] = filter; |
| 308 } | 310 } |
| 309 } | 311 } |
| 310 } | 312 } |
| 311 } | 313 } |
| 312 | 314 |
| 313 void _event(StreamController controller, data, int index, Element e) { | 315 void _event(StreamController controller, data, int index, Element e) { |
| 314 if (controller == null) return; | 316 if (controller == null) return; |
| 315 var rowStr = e.parent.dataset['row']; | 317 var rowStr = e.parent.dataset['row']; |
| 316 var row = rowStr != null ? int.parse(rowStr) : null; | 318 var row = rowStr != null ? int.parse(rowStr) : null; |
| 317 controller.add(new DefaultChartEventImpl( | 319 controller.add(new DefaultChartEventImpl(scope.event, area, series, row, |
| 318 scope.event, area, series, row, | |
| 319 series.measures.elementAt(_reverseIdx(index)), data)); | 320 series.measures.elementAt(_reverseIdx(index)), data)); |
| 320 } | 321 } |
| 321 | 322 |
| 322 // Stacked bar chart renders items from bottom to top (first measure is at | 323 // Stacked bar chart renders items from bottom to top (first measure is at |
| 323 // the bottom of the stack). We use [_reversedIdx] instead of index to | 324 // the bottom of the stack). We use [_reversedIdx] instead of index to |
| 324 // match the color and order of what is displayed in the legend. | 325 // match the color and order of what is displayed in the legend. |
| 325 int _reverseIdx(int index) => series.measures.length - 1 - index; | 326 int _reverseIdx(int index) => series.measures.length - 1 - index; |
| 326 } | 327 } |
| OLD | NEW |