| 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 |