| OLD | NEW |
| 1 // | 1 // Copyright 2017 Google Inc. All rights reserved. |
| 2 // Copyright 2014 Google Inc. All rights reserved. | |
| 3 // | 2 // |
| 4 // Use of this source code is governed by a BSD-style | 3 // Use of this source code is governed by a BSD-style |
| 5 // license that can be found in the LICENSE file or at | 4 // license that can be found in the LICENSE file or at |
| 6 // https://developers.google.com/open-source/licenses/bsd | 5 // https://developers.google.com/open-source/licenses/bsd |
| 7 // | 6 // |
| 8 | 7 |
| 9 part of charted.charts; | 8 part of charted.charts; |
| 10 | 9 |
| 11 class LineChartRenderer extends CartesianRendererBase { | 10 class StackedLineChartRenderer extends CartesianRendererBase { |
| 12 final Iterable<int> dimensionsUsingBand = const []; | 11 final Iterable<int> dimensionsUsingBand = const []; |
| 13 | 12 |
| 14 final bool alwaysAnimate; | 13 final bool alwaysAnimate; |
| 14 final bool showHoverCardOnTrackedDataPoints; |
| 15 final bool trackDataPoints; | 15 final bool trackDataPoints; |
| 16 final bool trackOnDimensionAxis; | 16 final bool trackOnDimensionAxis; |
| 17 final int quantitativeScaleProximity; | 17 final int quantitativeScaleProximity; |
| 18 | 18 |
| 19 bool _trackingPointsCreated = false; | 19 bool _trackingPointsCreated = false; |
| 20 List _xPositions = []; | 20 List _xPositions = []; |
| 21 | 21 |
| 22 // Currently hovered row/column | 22 // Currently hovered row/column |
| 23 int _savedOverRow = 0; | 23 int _savedOverRow = 0; |
| 24 int _savedOverColumn = 0; | 24 int _savedOverColumn = 0; |
| 25 | 25 |
| 26 int currentDataIndex = -1; | 26 int currentDataIndex = -1; |
| 27 | 27 |
| 28 @override | 28 @override |
| 29 final String name = "line-rdr"; | 29 final String name = "stacked-line-rdr"; |
| 30 | 30 |
| 31 LineChartRenderer( | 31 StackedLineChartRenderer( |
| 32 {this.alwaysAnimate: false, | 32 {this.alwaysAnimate: false, |
| 33 this.showHoverCardOnTrackedDataPoints: false, |
| 33 this.trackDataPoints: true, | 34 this.trackDataPoints: true, |
| 34 this.trackOnDimensionAxis: false, | 35 this.trackOnDimensionAxis: false, |
| 35 this.quantitativeScaleProximity: 5}); | 36 this.quantitativeScaleProximity: 5}); |
| 36 | 37 |
| 37 // Returns false if the number of dimension axes on the area is 0. | 38 // Returns false if the number of dimension axes on the area is 0. |
| 38 // Otherwise, the first dimension scale is used to render the chart. | 39 // Otherwise, the first dimension scale is used to render the chart. |
| 39 @override | 40 @override |
| 40 bool prepare(ChartArea area, ChartSeries series) { | 41 bool prepare(ChartArea area, ChartSeries series) { |
| 41 _ensureAreaAndSeries(area, series); | 42 _ensureAreaAndSeries(area, series); |
| 42 if (trackDataPoints != false) { | 43 if (trackDataPoints) { |
| 43 _trackPointerInArea(); | 44 _trackPointerInArea(); |
| 44 } | 45 } |
| 45 return area is CartesianArea; | 46 return area is CartesianArea; |
| 46 } | 47 } |
| 47 | 48 |
| 48 @override | 49 @override |
| 49 void draw(Element element, {Future schedulePostRender}) { | 50 void draw(Element element, {Future schedulePostRender}) { |
| 50 _ensureReadyToDraw(element); | 51 _ensureReadyToDraw(element); |
| 51 | 52 |
| 52 var measureScale = area.measureScales(series).first, | 53 var measureScale = area.measureScales(series).first, |
| 53 dimensionScale = area.dimensionScales.first; | 54 dimensionScale = area.dimensionScales.first; |
| 54 | 55 |
| 55 // Create lists of values in measure columns. | |
| 56 var lines = series.measures.map((column) { | |
| 57 return area.data.rows.map((values) => values[column]).toList(); | |
| 58 }).toList(); | |
| 59 | |
| 60 // We only support one dimension axes, so we always use the | 56 // We only support one dimension axes, so we always use the |
| 61 // first dimension. | 57 // first dimension. |
| 62 var x = area.data.rows | 58 var x = area.data.rows |
| 63 .map((row) => row.elementAt(area.config.dimensions.first)) | 59 .map((row) => row.elementAt(area.config.dimensions.first)) |
| 64 .toList(); | 60 .toList(); |
| 65 | 61 |
| 62 var accumulated = new List.filled(x.length, 0.0); |
| 63 |
| 64 var reversedMeasures = series.measures.toList().reversed.toList(); |
| 65 // Create lists of values used for drawing. |
| 66 // First Half: previous values reversed (need for drawing) |
| 67 // Second Half: current accumulated values (need for drawing) |
| 68 var lines = reversedMeasures.map((column) { |
| 69 var row = area.data.rows.map((values) => values[column]).toList(); |
| 70 return accumulated.reversed.toList()..addAll( |
| 71 new List.generate(x.length, (i) => accumulated[i] += row[i])); |
| 72 }).toList(); |
| 73 |
| 66 var rangeBandOffset = | 74 var rangeBandOffset = |
| 67 dimensionScale is OrdinalScale ? dimensionScale.rangeBand / 2 : 0; | 75 dimensionScale is OrdinalScale ? dimensionScale.rangeBand / 2 : 0; |
| 68 | 76 |
| 69 // If tracking data points is enabled, cache location of points that | 77 // If tracking data points is enabled, cache location of points that |
| 70 // represent data. | 78 // represent data. |
| 71 if (trackDataPoints) { | 79 if (trackDataPoints) { |
| 72 _xPositions = | 80 _xPositions = |
| 73 x.map((val) => dimensionScale.scale(val) + rangeBandOffset).toList(); | 81 x.map((val) => dimensionScale.scale(val) + rangeBandOffset).toList(); |
| 74 } | 82 } |
| 75 | 83 |
| 76 var line = new SvgLine( | 84 var fillLine = new SvgLine( |
| 85 xValueAccessor: (d, i) { |
| 86 // The first x.length values are the bottom part of the path that |
| 87 // should be drawn backword. The second part is the accumulated values |
| 88 // that should be drawn forward. |
| 89 var xval = i < x.length ? x[x.length - i - 1] : x[i - x.length]; |
| 90 return dimensionScale.scale(xval) + rangeBandOffset; |
| 91 }, |
| 92 yValueAccessor: (d, i) => measureScale.scale(d)); |
| 93 var strokeLine = new SvgLine( |
| 77 xValueAccessor: (d, i) => dimensionScale.scale(x[i]) + rangeBandOffset, | 94 xValueAccessor: (d, i) => dimensionScale.scale(x[i]) + rangeBandOffset, |
| 78 yValueAccessor: (d, i) => measureScale.scale(d)); | 95 yValueAccessor: (d, i) => measureScale.scale(d)); |
| 79 | 96 |
| 80 // Add lines and hook up hover and selection events. | 97 // Add lines and hook up hover and selection events. |
| 81 var svgLines = root.selectAll('.line-rdr-line').data(lines); | 98 var svgLines = root.selectAll('.stacked-line-rdr-line').data(lines.reversed)
; |
| 82 svgLines.enter.append('path').each((d, i, e) { | 99 svgLines.enter.append('g'); |
| 83 e.attributes['fill'] = 'none'; | |
| 84 }); | |
| 85 | 100 |
| 86 svgLines.each((d, i, e) { | 101 svgLines.each((d, i, e) { |
| 87 var column = series.measures.elementAt(i), | 102 var column = series.measures.elementAt(i), |
| 88 color = colorForColumn(column), | 103 color = colorForColumn(column), |
| 89 filter = filterForColumn(column), | 104 filter = filterForColumn(column), |
| 90 styles = stylesForColumn(column); | 105 styles = stylesForColumn(column), |
| 106 fill = new SvgElement.tag('path'), |
| 107 stroke = new SvgElement.tag('path'), |
| 108 fillData = d, |
| 109 // Second half contains the accumulated data for this measure |
| 110 strokeData = d.sublist(x.length, d.length); |
| 91 e.attributes | 111 e.attributes |
| 92 ..['d'] = line.path(d, i, e) | |
| 93 ..['stroke'] = color | 112 ..['stroke'] = color |
| 113 ..['fill'] = color |
| 94 ..['class'] = styles.isEmpty | 114 ..['class'] = styles.isEmpty |
| 95 ? 'line-rdr-line' | 115 ? 'stacked-line-rdr-line' |
| 96 : 'line-rdr-line ${styles.join(' ')}' | 116 : 'stacked-line-rdr-line ${styles.join(' ')}' |
| 97 ..['data-column'] = '$column'; | 117 ..['data-column'] = '$column'; |
| 118 fill.attributes |
| 119 ..['d'] = fillLine.path(fillData, i, e) |
| 120 ..['stroke'] = 'none'; |
| 121 stroke.attributes |
| 122 ..['d'] = strokeLine.path(strokeData, i, e) |
| 123 ..['fill'] = 'none'; |
| 124 e.children = [fill, stroke]; |
| 98 if (isNullOrEmpty(filter)) { | 125 if (isNullOrEmpty(filter)) { |
| 99 e.attributes.remove('filter'); | 126 e.attributes.remove('filter'); |
| 100 } else { | 127 } else { |
| 101 e.attributes['filter'] = filter; | 128 e.attributes['filter'] = filter; |
| 102 } | 129 } |
| 103 }); | 130 }); |
| 104 | 131 |
| 105 if (area.state != null) { | 132 if (area.state != null) { |
| 106 svgLines | 133 svgLines |
| 107 ..on('click', (d, i, e) => _mouseClickHandler(d, i, e)) | 134 ..on('click', (d, i, e) => _mouseClickHandler(d, i, e)) |
| 108 ..on('mouseover', (d, i, e) => _mouseOverHandler(d, i, e)) | 135 ..on('mouseover', (d, i, e) => _mouseOverHandler(d, i, e)) |
| 109 ..on('mouseout', (d, i, e) => _mouseOutHandler(d, i, e)); | 136 ..on('mouseout', (d, i, e) => _mouseOutHandler(d, i, e)); |
| 110 } | 137 } |
| 111 | 138 |
| 112 svgLines.exit.remove(); | 139 svgLines.exit.remove(); |
| 113 } | 140 } |
| 114 | 141 |
| 115 @override | 142 @override |
| 116 void dispose() { | 143 void dispose() { |
| 144 _disposer.dispose(); |
| 117 if (root == null) return; | 145 if (root == null) return; |
| 118 root.selectAll('.line-rdr-line').remove(); | 146 root.selectAll('.stacked-line-rdr-line').remove(); |
| 119 root.selectAll('.line-rdr-point').remove(); | 147 root.selectAll('.stacked-line-rdr-point').remove(); |
| 120 _disposer.dispose(); | 148 } |
| 149 |
| 150 @override |
| 151 Extent get extent { |
| 152 assert(area != null && series != null); |
| 153 var rows = area.data.rows, |
| 154 max = SMALL_INT_MIN, |
| 155 min = SMALL_INT_MAX, |
| 156 rowIndex = 0; |
| 157 |
| 158 rows.forEach((row) { |
| 159 var line = null; |
| 160 series.measures.forEach((idx) { |
| 161 var value = row.elementAt(idx); |
| 162 if (value != null && value.isFinite) { |
| 163 if (line == null) line = 0.0; |
| 164 line += value; |
| 165 } |
| 166 }); |
| 167 if (line > max) max = line; |
| 168 if (line < min) min = line; |
| 169 rowIndex++; |
| 170 }); |
| 171 |
| 172 return new Extent(min, max); |
| 121 } | 173 } |
| 122 | 174 |
| 123 @override | 175 @override |
| 124 void handleStateChanges(List<ChangeRecord> changes) { | 176 void handleStateChanges(List<ChangeRecord> changes) { |
| 125 var lines = host.querySelectorAll('.line-rdr-line'); | 177 var lines = host.querySelectorAll('.stacked-line-rdr-line'); |
| 126 if (lines == null || lines.isEmpty) return; | 178 if (lines == null || lines.isEmpty) return; |
| 127 | 179 |
| 128 for (int i = 0, len = lines.length; i < len; ++i) { | 180 for (int i = 0, len = lines.length; i < len; ++i) { |
| 129 var line = lines.elementAt(i), | 181 var line = lines.elementAt(i), |
| 130 column = int.parse(line.dataset['column']), | 182 column = int.parse(line.dataset['column']), |
| 131 filter = filterForColumn(column); | 183 filter = filterForColumn(column); |
| 132 line.classes.removeAll(ChartState.COLUMN_CLASS_NAMES); | 184 line.classes.removeAll(ChartState.COLUMN_CLASS_NAMES); |
| 133 line.classes.addAll(stylesForColumn(column)); | 185 line.classes.addAll(stylesForColumn(column)); |
| 134 line.attributes['stroke'] = colorForColumn(column); | 186 line.attributes['stroke'] = colorForColumn(column); |
| 187 line.attributes['fill'] = colorForColumn(column); |
| 135 | 188 |
| 136 if (isNullOrEmpty(filter)) { | 189 if (isNullOrEmpty(filter)) { |
| 137 line.attributes.remove('filter'); | 190 line.attributes.remove('filter'); |
| 138 } else { | 191 } else { |
| 139 line.attributes['filter'] = filter; | 192 line.attributes['filter'] = filter; |
| 140 } | 193 } |
| 141 } | 194 } |
| 142 } | 195 } |
| 143 | 196 |
| 144 void _createTrackingCircles() { | 197 void _createTrackingCircles() { |
| 145 var linePoints = root.selectAll('.line-rdr-point').data(series.measures); | 198 var linePoints = root.selectAll('.stacked-line-rdr-point') |
| 199 .data(series.measures.toList().reversed); |
| 146 linePoints.enter.append('circle').each((d, i, e) { | 200 linePoints.enter.append('circle').each((d, i, e) { |
| 147 e.classes.add('line-rdr-point'); | 201 e.classes.add('stacked-line-rdr-point'); |
| 148 e.attributes['r'] = '4'; | 202 e.attributes['r'] = '4'; |
| 149 }); | 203 }); |
| 150 | 204 |
| 151 linePoints | 205 linePoints |
| 152 ..each((d, i, e) { | 206 ..each((d, i, e) { |
| 153 var color = colorForColumn(d); | 207 var color = colorForColumn(d); |
| 154 e.attributes | 208 e.attributes |
| 155 ..['r'] = '4' | 209 ..['r'] = '4' |
| 156 ..['stroke'] = color | 210 ..['stroke'] = color |
| 157 ..['fill'] = color | 211 ..['fill'] = color |
| 158 ..['data-column'] = '$d'; | 212 ..['data-column'] = '$d'; |
| 159 }) | 213 }) |
| 160 ..on('click', _mouseClickHandler) | 214 ..on('click', _mouseClickHandler) |
| 161 ..on('mousemove', _mouseOverHandler) // Ensure that we update values | 215 ..on('mousemove', _mouseOverHandler) // Ensure that we update values |
| 162 ..on('mouseover', _mouseOverHandler) | 216 ..on('mouseover', _mouseOverHandler) |
| 163 ..on('mouseout', _mouseOutHandler); | 217 ..on('mouseout', _mouseOutHandler); |
| 164 | 218 |
| 165 linePoints.exit.remove(); | 219 linePoints.exit.remove(); |
| 166 _trackingPointsCreated = true; | 220 _trackingPointsCreated = true; |
| 167 } | 221 } |
| 168 | 222 |
| 169 void _showTrackingCircles(int row) { | 223 void _showTrackingCircles(ChartEvent event, int row) { |
| 170 if (_trackingPointsCreated == false) { | 224 if (_trackingPointsCreated == false) { |
| 171 _createTrackingCircles(); | 225 _createTrackingCircles(); |
| 172 } | 226 } |
| 173 | 227 |
| 228 double cumulated = 0.0; |
| 174 var yScale = area.measureScales(series).first; | 229 var yScale = area.measureScales(series).first; |
| 175 root.selectAll('.line-rdr-point').each((d, i, e) { | 230 root.selectAll('.stacked-line-rdr-point').each((d, i, e) { |
| 176 var x = _xPositions[row], | 231 var x = _xPositions[row], |
| 177 measureVal = area.data.rows.elementAt(row).elementAt(d); | 232 measureVal = cumulated += area.data.rows.elementAt(row).elementAt(d); |
| 178 if (measureVal != null && measureVal.isFinite) { | 233 if (measureVal != null && measureVal.isFinite) { |
| 179 var color = colorForColumn(d), filter = filterForColumn(d); | 234 var color = colorForColumn(d), filter = filterForColumn(d); |
| 180 e.attributes | 235 e.attributes |
| 181 ..['cx'] = '$x' | 236 ..['cx'] = '$x' |
| 182 ..['cy'] = '${yScale.scale(measureVal)}' | 237 ..['cy'] = '${yScale.scale(measureVal)}' |
| 183 ..['fill'] = color | 238 ..['fill'] = color |
| 184 ..['stroke'] = color | 239 ..['stroke'] = color |
| 185 ..['data-row'] = '$row'; | 240 ..['data-row'] = '$row'; |
| 186 e.style | 241 e.style |
| 187 ..setProperty('opacity', '1') | 242 ..setProperty('opacity', '1') |
| 188 ..setProperty('visibility', 'visible'); | 243 ..setProperty('visibility', 'visible'); |
| 189 if (isNullOrEmpty(filter)) { | 244 if (isNullOrEmpty(filter)) { |
| 190 e.attributes.remove('filter'); | 245 e.attributes.remove('filter'); |
| 191 } else { | 246 } else { |
| 192 e.attributes['filter'] = filter; | 247 e.attributes['filter'] = filter; |
| 193 } | 248 } |
| 194 } else { | 249 } else { |
| 195 e.style | 250 e.style |
| 196 ..setProperty('opacity', '$EPSILON') | 251 ..setProperty('opacity', '$EPSILON') |
| 197 ..setProperty('visibility', 'hidden'); | 252 ..setProperty('visibility', 'hidden'); |
| 198 } | 253 } |
| 199 }); | 254 }); |
| 255 |
| 256 if (showHoverCardOnTrackedDataPoints) { |
| 257 var firstMeasureColumn = series.measures.first; |
| 258 mouseOverController.add(new DefaultChartEventImpl( |
| 259 event.source, area, series, row, firstMeasureColumn, 0)); |
| 260 _savedOverRow = row; |
| 261 _savedOverColumn = firstMeasureColumn; |
| 262 } |
| 200 } | 263 } |
| 201 | 264 |
| 202 void _hideTrackingCircles() { | 265 void _hideTrackingCircles(ChartEvent event) { |
| 203 root.selectAll('.line-rdr-point') | 266 root.selectAll('.stacked-line-rdr-point') |
| 204 ..style('opacity', '0.0') | 267 ..style('opacity', '0.0') |
| 205 ..style('visibility', 'hidden'); | 268 ..style('visibility', 'hidden'); |
| 269 if (showHoverCardOnTrackedDataPoints) { |
| 270 mouseOutController.add(new DefaultChartEventImpl( |
| 271 event.source, area, series, _savedOverRow, _savedOverColumn, 0)); |
| 272 } |
| 206 } | 273 } |
| 207 | 274 |
| 208 int _getNearestRowIndex(num x) { | 275 int _getNearestRowIndex(num x) { |
| 209 var lastSmallerValue = 0; | 276 double lastSmallerValue = 0.0; |
| 210 var chartX = x - area.layout.renderArea.x; | 277 var chartX = x - area.layout.renderArea.x; |
| 211 for (var i = 0; i < _xPositions.length; i++) { | 278 for (var i = 0; i < _xPositions.length; i++) { |
| 212 var pos = _xPositions[i]; | 279 var pos = _xPositions[i]; |
| 213 if (pos < chartX) { | 280 if (pos < chartX) { |
| 214 lastSmallerValue = pos; | 281 lastSmallerValue = pos; |
| 215 } else { | 282 } else { |
| 216 return i == 0 | 283 return i == 0 |
| 217 ? 0 | 284 ? 0 |
| 218 : (chartX - lastSmallerValue <= pos - chartX) ? i - 1 : i; | 285 : (chartX - lastSmallerValue <= pos - chartX) ? i - 1 : i; |
| 219 } | 286 } |
| 220 } | 287 } |
| 221 return _xPositions.length - 1; | 288 return _xPositions.length - 1; |
| 222 } | 289 } |
| 223 | 290 |
| 224 void _trackPointerInArea() { | 291 void _trackPointerInArea() { |
| 225 _trackingPointsCreated = false; | 292 _trackingPointsCreated = false; |
| 226 _disposer.add(area.onMouseMove.listen((ChartEvent event) { | 293 _disposer.add(area.onMouseMove.listen((ChartEvent event) { |
| 227 if (area.layout.renderArea.contains(event.chartX, event.chartY)) { | 294 if (area.layout.renderArea.contains(event.chartX, event.chartY)) { |
| 228 var row = _getNearestRowIndex(event.chartX); | 295 var row = _getNearestRowIndex(event.chartX); |
| 229 window.animationFrame.then((_) => _showTrackingCircles(row)); | 296 window.animationFrame.then((_) { |
| 297 _showTrackingCircles(event, row); |
| 298 }); |
| 230 } else { | 299 } else { |
| 231 _hideTrackingCircles(); | 300 _hideTrackingCircles(event); |
| 232 } | 301 } |
| 233 })); | 302 })); |
| 234 _disposer.add(area.onMouseOut.listen((ChartEvent event) { | 303 _disposer.add(area.onMouseOut.listen((ChartEvent event) { |
| 235 _hideTrackingCircles(); | 304 _hideTrackingCircles(event); |
| 236 })); | 305 })); |
| 237 } | 306 } |
| 238 | 307 |
| 239 void _mouseClickHandler(d, int i, Element e) { | 308 void _mouseClickHandler(d, int i, Element e) { |
| 240 if (area.state != null) { | 309 if (area.state != null) { |
| 241 area.state.select(int.parse(e.dataset['column'])); | 310 var selectedColumn = int.parse(e.dataset['column']); |
| 311 area.state.isSelected(selectedColumn) |
| 312 ? area.state.unselect(selectedColumn) |
| 313 : area.state.select(selectedColumn); |
| 242 } | 314 } |
| 243 if (mouseClickController != null && e.tagName == 'circle') { | 315 if (mouseClickController != null && e.tagName == 'circle') { |
| 244 var row = int.parse(e.dataset['row']), | 316 var row = int.parse(e.dataset['row']), |
| 245 column = int.parse(e.dataset['column']); | 317 column = int.parse(e.dataset['column']); |
| 246 mouseClickController.add( | 318 mouseClickController.add( |
| 247 new DefaultChartEventImpl(scope.event, area, series, row, column, d)); | 319 new DefaultChartEventImpl(scope.event, area, series, row, column, d)); |
| 248 } | 320 } |
| 249 } | 321 } |
| 250 | 322 |
| 251 void _mouseOverHandler(d, int i, Element e) { | 323 void _mouseOverHandler(d, int i, Element e) { |
| (...skipping 12 matching lines...) Expand all Loading... |
| 264 if (area.state != null && | 336 if (area.state != null && |
| 265 area.state.preview == int.parse(e.dataset['column'])) { | 337 area.state.preview == int.parse(e.dataset['column'])) { |
| 266 area.state.preview = null; | 338 area.state.preview = null; |
| 267 } | 339 } |
| 268 if (mouseOutController != null && e.tagName == 'circle') { | 340 if (mouseOutController != null && e.tagName == 'circle') { |
| 269 mouseOutController.add(new DefaultChartEventImpl( | 341 mouseOutController.add(new DefaultChartEventImpl( |
| 270 scope.event, area, series, _savedOverRow, _savedOverColumn, d)); | 342 scope.event, area, series, _savedOverRow, _savedOverColumn, d)); |
| 271 } | 343 } |
| 272 } | 344 } |
| 273 } | 345 } |
| OLD | NEW |