Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(643)

Side by Side Diff: charted/lib/charts/cartesian_renderers/stackedbar_chart_renderer.dart

Issue 1400473008: Roll Observatory packages and add a roll script (Closed) Base URL: git@github.com:dart-lang/observatory_pub_packages.git@master
Patch Set: Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 //
2 // Copyright 2014 Google Inc. All rights reserved.
3 //
4 // Use of this source code is governed by a BSD-style
5 // license that can be found in the LICENSE file or at
6 // https://developers.google.com/open-source/licenses/bsd
7 //
8
9 part of charted.charts;
10
11 class StackedBarChartRenderer extends CartesianRendererBase {
12 static const RADIUS = 2;
13
14 final Iterable<int> dimensionsUsingBand = const[0];
15 final bool alwaysAnimate;
16
17 @override
18 final String name = "stack-rdr";
19
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.
22 List<int> _lastMeasureWithData = [];
23
24 StackedBarChartRenderer({this.alwaysAnimate: false});
25
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.
28 @override
29 bool prepare(CartesianArea area, ChartSeries series) {
30 _ensureAreaAndSeries(area, series);
31 return true;
32 }
33
34 @override
35 void draw(Element element, {Future schedulePostRender}) {
36 _ensureReadyToDraw(element);
37 var verticalBars = !area.config.isLeftAxisPrimary;
38
39 var measuresCount = series.measures.length,
40 measureScale = area.measureScales(series).first,
41 dimensionScale = area.dimensionScales.first;
42
43 var rows = new List()
44 ..addAll(area.data.rows.map((e) =>
45 new List.generate(measuresCount,
46 (i) => e.elementAt(series.measures.elementAt(_reverseIdx(i))))));
47
48 var dimensionVals = area.data.rows.map(
49 (row) => row.elementAt(area.config.dimensions.first)).toList();
50
51 var groups = root.selectAll('.stack-rdr-rowgroup').data(rows);
52 var animateBarGroups = alwaysAnimate || !groups.isEmpty;
53 groups.enter.append('g')
54 ..classed('stack-rdr-rowgroup')
55 ..attrWithCallback('transform', (d, i, c) => verticalBars ?
56 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)' :
57 'translate(0, ${dimensionScale.scale(dimensionVals[i])})');
58 groups.attrWithCallback('data-row', (d, i, e) => i);
59 groups.exit.remove();
60
61 if (animateBarGroups) {
62 groups.transition()
63 ..attrWithCallback('transform', (d, i, c) => verticalBars ?
64 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)' :
65 'translate(0, ${dimensionScale.scale(dimensionVals[i])})')
66 ..duration(theme.transitionDurationMilliseconds);
67 }
68
69 var bar =
70 groups.selectAll('.stack-rdr-bar').dataWithCallback((d, i, c) => d);
71
72 var prevOffsetVal = new List();
73
74 // Keep track of "y" values.
75 // These are used to insert values in the middle of stack when necessary
76 if (animateBarGroups) {
77 bar.each((d, i, e) {
78 var offset = e.dataset['offset'],
79 offsetVal = offset != null ? int.parse(offset) : 0;
80 if (i == 0) {
81 prevOffsetVal.add(offsetVal);
82 } else {
83 prevOffsetVal[prevOffsetVal.length - 1] = offsetVal;
84 }
85 });
86 }
87
88
89 var barWidth = dimensionScale.rangeBand - theme.defaultStrokeWidth;
90
91 // Calculate height of each segment in the bar.
92 // Uses prevAllZeroHeight and prevOffset to track previous segments
93 var prevAllZeroHeight = true,
94 prevOffset = 0;
95 var getBarLength = (d, i) {
96 if (!verticalBars) return measureScale.scale(d).round();
97 var retval = rect.height - measureScale.scale(d).round();
98 if (i != 0) {
99 // 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.
101 retval -= prevAllZeroHeight
102 ? 1
103 : (theme.defaultSeparatorWidth + theme.defaultStrokeWidth);
104 retval += prevOffset;
105 } else {
106 // When rendering next group of bars, reset prevZeroHeight.
107 prevOffset = 0;
108 prevAllZeroHeight = true;
109 retval -= 1; // -1 so bar does not overlap x axis.
110 }
111
112 if (retval <= 0) {
113 prevOffset = prevAllZeroHeight
114 ? 0
115 : theme.defaultSeparatorWidth + theme.defaultStrokeWidth + retval;
116 retval = 0;
117 }
118 prevAllZeroHeight = (retval == 0) && prevAllZeroHeight;
119 return retval;
120 };
121
122 // Initial "y" position of a bar that is being created.
123 // Only used when animateBarGroups is set to true.
124 var ic = 10000000,
125 order = 0;
126 var getInitialBarPos = (i) {
127 var tempY;
128 if (i <= ic && i > 0) {
129 tempY = prevOffsetVal[order];
130 order++;
131 } else {
132 tempY = verticalBars ? rect.height : 0;
133 }
134 ic = i;
135 return tempY;
136 };
137
138 // Position of a bar in the stack. yPos is used to keep track of the
139 // offset based on previous calls to getBarY
140 var yPos = 0;
141 var getBarPos = (d, i) {
142 if (verticalBars) {
143 if (i == 0) {
144 yPos = measureScale.scale(0).round();
145 }
146 return yPos -= (rect.height - measureScale.scale(d).round());
147 } else {
148 if (i == 0) {
149 // 1 to not overlap the axis line.
150 yPos = 1;
151 }
152 var pos = yPos;
153 yPos += measureScale.scale(d).round();
154 // Check if after adding the height of the bar, if y has changed, if
155 // changed, we offset for space between the bars.
156 if (yPos != pos) {
157 yPos += (theme.defaultSeparatorWidth + theme.defaultStrokeWidth);
158 }
159 return pos;
160 }
161 };
162
163 var buildPath = (d, int i, Element e, bool animate, int roundIdx) {
164 var position = animate ? getInitialBarPos(i) : getBarPos(d, i),
165 length = animate ? 0 : getBarLength(d, i),
166 radius = series.measures.elementAt(_reverseIdx(i)) == roundIdx ? RADIU S : 0,
167 path = (length != 0)
168 ? verticalBars
169 ? topRoundedRect(0, position, barWidth, length, radius)
170 : rightRoundedRect(position, 0, length, barWidth, radius)
171 : '';
172 e.attributes['data-offset'] = verticalBars ?
173 position.toString() : (position + length).toString();
174 return path;
175 };
176
177 var enter = bar.enter.appendWithCallback((d, i, e) {
178 var rect = Namespace.createChildElement('path', e),
179 measure = series.measures.elementAt(_reverseIdx(i)),
180 row = int.parse(e.dataset['row']),
181 color = colorForValue(measure, row),
182 filter = filterForValue(measure, row),
183 style = stylesForValue(measure, row),
184 roundIndex = _lastMeasureWithData[row];
185
186 if (!isNullOrEmpty(style)) {
187 rect.classes.addAll(style);
188 }
189 rect.classes.add('stack-rdr-bar');
190
191 rect.attributes
192 ..['d'] = buildPath (d == null ? 0 : d, i, rect, animateBarGroups,
193 roundIndex)
194 ..['stroke-width'] = '${theme.defaultStrokeWidth}px'
195 ..['fill'] = color
196 ..['stroke'] = color;
197
198 if (!isNullOrEmpty(filter)) {
199 rect.attributes['filter'] = filter;
200 }
201 if (!animateBarGroups) {
202 rect.attributes['data-column'] = '$measure';
203 }
204 return rect;
205 });
206
207 enter
208 ..on('click', (d, i, e) => _event(mouseClickController, d, i, e))
209 ..on('mouseover', (d, i, e) => _event(mouseOverController, d, i, e))
210 ..on('mouseout', (d, i, e) => _event(mouseOutController, d, i, e));
211
212 if (animateBarGroups) {
213 bar.each((d, i, e) {
214 var measure = series.measures.elementAt(_reverseIdx(i)),
215 row = int.parse(e.parent.dataset['row']),
216 color = colorForValue(measure, row),
217 filter = filterForValue(measure, row),
218 styles = stylesForValue(measure, row);
219 e.attributes
220 ..['data-column'] = '$measure'
221 ..['fill'] = color
222 ..['stroke'] = color;
223 e.classes
224 ..removeAll(ChartState.VALUE_CLASS_NAMES)
225 ..addAll(styles);
226 if (isNullOrEmpty(filter)) {
227 e.attributes.remove('filter');
228 } else {
229 e.attributes['filter'] = filter;
230 }
231 });
232
233 bar.transition()
234 ..attrWithCallback('d', (d, i, e) {
235 var row = int.parse(e.parent.dataset['row']),
236 roundIndex = _lastMeasureWithData[row];
237 return buildPath(d == null ? 0 : d, i, e, false, roundIndex);
238 });
239 }
240
241 bar.exit.remove();
242 }
243
244 @override
245 void dispose() {
246 if (root == null) return;
247 root.selectAll('.stack-rdr-rowgroup').remove();
248 }
249
250 @override
251 double get bandInnerPadding =>
252 area.theme.getDimensionAxisTheme().axisBandInnerPadding;
253
254 @override
255 Extent get extent {
256 assert(area != null && series != null);
257 var rows = area.data.rows,
258 max = SMALL_INT_MIN,
259 min = SMALL_INT_MAX,
260 rowIndex = 0;
261 _lastMeasureWithData = new List.generate(rows.length, (i) => -1);
262
263 rows.forEach((row) {
264 var bar = null;
265 series.measures.forEach((idx) {
266 var value = row.elementAt(idx);
267 if (value != null && value.isFinite) {
268 if (bar == null) bar = 0;
269 bar += value;
270 if (value.round() != 0 && _lastMeasureWithData[rowIndex] == -1) {
271 _lastMeasureWithData[rowIndex] = idx;
272 }
273 }
274 });
275 if (bar > max) max = bar;
276 if (bar < min) min = bar;
277 rowIndex++;
278 });
279
280 return new Extent(min, max);
281 }
282
283 @override
284 void handleStateChanges(List<ChangeRecord> changes) {
285 var groups = host.querySelectorAll('.stack-rdr-rowgroup');
286 if (groups == null || groups.isEmpty) return;
287
288 for(int i = 0, len = groups.length; i < len; ++i) {
289 var group = groups.elementAt(i),
290 bars = group.querySelectorAll('.stack-rdr-bar'),
291 row = int.parse(group.dataset['row']);
292
293 for(int j = 0, barsCount = bars.length; j < barsCount; ++j) {
294 var bar = bars.elementAt(j),
295 column = int.parse(bar.dataset['column']),
296 color = colorForValue(column, row),
297 filter = filterForValue(column, row);
298
299 bar.classes.removeAll(ChartState.VALUE_CLASS_NAMES);
300 bar.classes.addAll(stylesForValue(column, row));
301 bar.attributes
302 ..['fill'] = color
303 ..['stroke'] = color;
304 if (isNullOrEmpty(filter)) {
305 bar.attributes.remove('filter');
306 } else {
307 bar.attributes['filter'] = filter;
308 }
309 }
310 }
311 }
312
313 void _event(StreamController controller, data, int index, Element e) {
314 if (controller == null) return;
315 var rowStr = e.parent.dataset['row'];
316 var row = rowStr != null ? int.parse(rowStr) : null;
317 controller.add(new DefaultChartEventImpl(
318 scope.event, area, series, row,
319 series.measures.elementAt(_reverseIdx(index)), data));
320 }
321
322 // 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 // match the color and order of what is displayed in the legend.
325 int _reverseIdx(int index) => series.measures.length - 1 - index;
326 }
OLDNEW
« no previous file with comments | « charted/lib/charts/cartesian_renderers/line_chart_renderer.dart ('k') | charted/lib/charts/chart_area.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698