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 |