| 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 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 } | |
| OLD | NEW |