OLD | NEW |
| (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 BarChartRenderer 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 = "bar-rdr"; | |
19 | |
20 BarChartRenderer({this.alwaysAnimate: false}); | |
21 | |
22 /// Returns false if the number of dimension axes on the area is 0. | |
23 /// Otherwise, the first dimension scale is used to render the chart. | |
24 @override | |
25 bool prepare(ChartArea area, ChartSeries series) { | |
26 _ensureAreaAndSeries(area, series); | |
27 return area is CartesianArea; | |
28 } | |
29 | |
30 @override | |
31 void draw(Element element, {Future schedulePostRender}) { | |
32 _ensureReadyToDraw(element); | |
33 | |
34 var verticalBars = !area.config.isLeftAxisPrimary; | |
35 | |
36 var measuresCount = series.measures.length, | |
37 measureScale = area.measureScales(series).first, | |
38 dimensionScale = area.dimensionScales.first; | |
39 | |
40 var rows = new List() | |
41 ..addAll(area.data.rows.map((e) => | |
42 new List.generate( | |
43 measuresCount, (i) => e[series.measures.elementAt(i)]))); | |
44 | |
45 var dimensionVals = area.data.rows.map( | |
46 (row) => row.elementAt(area.config.dimensions.first)).toList(); | |
47 | |
48 var bars = new OrdinalScale() | |
49 ..domain = new Range(series.measures.length).toList() | |
50 ..rangeRoundBands([0, dimensionScale.rangeBand]); | |
51 | |
52 // Create and update the bar groups. | |
53 | |
54 var groups = root.selectAll('.bar-rdr-rowgroup').data(rows); | |
55 var animateBarGroups = alwaysAnimate || !groups.isEmpty; | |
56 | |
57 groups.enter.append('g') | |
58 ..classed('bar-rdr-rowgroup') | |
59 ..attrWithCallback('transform', (d, i, c) => verticalBars ? | |
60 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)' : | |
61 'translate(0, ${dimensionScale.scale(dimensionVals[i])})'); | |
62 groups.attrWithCallback('data-row', (d, i, e) => i); | |
63 groups.exit.remove(); | |
64 | |
65 if (animateBarGroups) { | |
66 groups.transition() | |
67 ..attrWithCallback('transform', (d, i, c) => verticalBars ? | |
68 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)' : | |
69 'translate(0, ${dimensionScale.scale(dimensionVals[i])})') | |
70 ..duration(theme.transitionDurationMilliseconds); | |
71 } | |
72 | |
73 // TODO: Test interactions between stroke width and bar width. | |
74 | |
75 var barWidth = bars.rangeBand.abs() - | |
76 theme.defaultSeparatorWidth - theme.defaultStrokeWidth, | |
77 strokeWidth = theme.defaultStrokeWidth, | |
78 strokeWidthOffset = strokeWidth ~/ 2; | |
79 | |
80 // Create and update the bars | |
81 // Avoids animation on first render unless alwaysAnimate is set to true. | |
82 | |
83 var bar = groups.selectAll('.bar-rdr-bar').dataWithCallback( | |
84 (d, i, c) => rows[i]), | |
85 scaled0 = measureScale.scale(0).round(); | |
86 | |
87 var getBarLength = (d) { | |
88 var scaledVal = measureScale.scale(d).round(), | |
89 ht = verticalBars | |
90 ? (d >= 0 ? scaled0 - scaledVal : scaledVal - scaled0) | |
91 : (d >= 0 ? scaledVal - scaled0 : scaled0 - scaledVal); | |
92 ht = ht - strokeWidth; | |
93 return (ht < 0) ? 0 : ht; | |
94 }; | |
95 var getBarPos = (d) { | |
96 var scaledVal = measureScale.scale(d).round(); | |
97 return verticalBars | |
98 ? (d >= 0 ? scaledVal : scaled0) + strokeWidthOffset | |
99 : (d >= 0 ? scaled0 : scaledVal) + strokeWidthOffset; | |
100 }; | |
101 var buildPath = (d, int i, bool animate) { | |
102 if (d == null || d == 0) return ''; | |
103 if (verticalBars) { | |
104 var fn = d > 0 ? topRoundedRect : bottomRoundedRect; | |
105 return fn( | |
106 bars.scale(i).toInt() + strokeWidthOffset, | |
107 animate ? rect.height : getBarPos(d), | |
108 barWidth, animate ? 0 : getBarLength(d), RADIUS); | |
109 } else { | |
110 var fn = d > 0 ? rightRoundedRect : leftRoundedRect; | |
111 return fn( | |
112 getBarPos(d), bars.scale(i).toInt() + strokeWidthOffset, | |
113 animate ? 0 : getBarLength(d), barWidth, RADIUS); | |
114 } | |
115 }; | |
116 | |
117 bar.enter.appendWithCallback((d, i, e) { | |
118 var rect = Namespace.createChildElement('path', e), | |
119 measure = series.measures.elementAt(i), | |
120 row = int.parse(e.dataset['row']), | |
121 color = colorForValue(measure, row), | |
122 filter = filterForValue(measure, row), | |
123 style = stylesForValue(measure, row); | |
124 | |
125 if (!isNullOrEmpty(style)) { | |
126 rect.classes.addAll(style); | |
127 } | |
128 rect.classes.add('bar-rdr-bar'); | |
129 | |
130 rect.attributes | |
131 ..['d'] = buildPath(d, i, animateBarGroups) | |
132 ..['stroke-width'] = '${strokeWidth}px' | |
133 ..['fill'] = color | |
134 ..['stroke'] = color; | |
135 | |
136 if (!isNullOrEmpty(filter)) { | |
137 rect.attributes['filter'] = filter; | |
138 } | |
139 if (!animateBarGroups) { | |
140 rect.attributes['data-column'] = '$measure'; | |
141 } | |
142 return rect; | |
143 }) | |
144 ..on('click', (d, i, e) => _event(mouseClickController, d, i, e)) | |
145 ..on('mouseover', (d, i, e) => _event(mouseOverController, d, i, e)) | |
146 ..on('mouseout', (d, i, e) => _event(mouseOutController, d, i, e)); | |
147 | |
148 if (animateBarGroups) { | |
149 bar.each((d, i, e) { | |
150 var measure = series.measures.elementAt(i), | |
151 row = int.parse(e.parent.dataset['row']), | |
152 color = colorForValue(measure, row), | |
153 filter = filterForValue(measure, row), | |
154 styles = stylesForValue(measure, row); | |
155 e.attributes | |
156 ..['data-column'] = '$measure' | |
157 ..['fill'] = color | |
158 ..['stroke'] = color; | |
159 e.classes | |
160 ..removeAll(ChartState.VALUE_CLASS_NAMES) | |
161 ..addAll(styles); | |
162 if (isNullOrEmpty(filter)) { | |
163 e.attributes.remove('filter'); | |
164 } else { | |
165 e.attributes['filter'] = filter; | |
166 } | |
167 }); | |
168 | |
169 bar.transition() | |
170 ..attrWithCallback('d', | |
171 (d, i, e) => buildPath(d, i, false)); | |
172 } | |
173 | |
174 bar.exit.remove(); | |
175 } | |
176 | |
177 @override | |
178 void dispose() { | |
179 if (root == null) return; | |
180 root.selectAll('.bar-rdr-rowgroup').remove(); | |
181 } | |
182 | |
183 @override | |
184 double get bandInnerPadding { | |
185 assert(series != null && area != null); | |
186 var measuresCount = series.measures.length; | |
187 return measuresCount > 2 ? 1 - (measuresCount / (measuresCount + 1)) : | |
188 area.theme.getDimensionAxisTheme().axisBandInnerPadding; | |
189 } | |
190 | |
191 @override | |
192 double get bandOuterPadding { | |
193 assert(series != null && area != null); | |
194 return area.theme.getDimensionAxisTheme().axisBandOuterPadding; | |
195 } | |
196 | |
197 @override | |
198 void handleStateChanges(List<ChangeRecord> changes) { | |
199 var groups = host.querySelectorAll('.bar-rdr-rowgroup'); | |
200 if (groups == null || groups.isEmpty) return; | |
201 | |
202 for(int i = 0, len = groups.length; i < len; ++i) { | |
203 var group = groups.elementAt(i), | |
204 bars = group.querySelectorAll('.bar-rdr-bar'), | |
205 row = int.parse(group.dataset['row']); | |
206 | |
207 for(int j = 0, barsCount = bars.length; j < barsCount; ++j) { | |
208 var bar = bars.elementAt(j), | |
209 column = int.parse(bar.dataset['column']), | |
210 color = colorForValue(column, row), | |
211 filter = filterForValue(column, row); | |
212 | |
213 bar.classes.removeAll(ChartState.VALUE_CLASS_NAMES); | |
214 bar.classes.addAll(stylesForValue(column, row)); | |
215 bar.attributes | |
216 ..['fill'] = color | |
217 ..['stroke'] = color; | |
218 if (isNullOrEmpty(filter)) { | |
219 bar.attributes.remove('filter'); | |
220 } else { | |
221 bar.attributes['filter'] = filter; | |
222 } | |
223 } | |
224 } | |
225 } | |
226 | |
227 void _event(StreamController controller, data, int index, Element e) { | |
228 if (controller == null) return; | |
229 var rowStr = e.parent.dataset['row']; | |
230 var row = rowStr != null ? int.parse(rowStr) : null; | |
231 controller.add( | |
232 new DefaultChartEventImpl(scope.event, area, | |
233 series, row, series.measures.elementAt(index), data)); | |
234 } | |
235 } | |
OLD | NEW |