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