| 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 LineChartRenderer extends CartesianRendererBase { | |
| 12 final Iterable<int> dimensionsUsingBand = const[]; | |
| 13 final SubscriptionsDisposer _disposer = new SubscriptionsDisposer(); | |
| 14 | |
| 15 final bool alwaysAnimate; | |
| 16 final bool trackDataPoints; | |
| 17 final bool trackOnDimensionAxis; | |
| 18 final int quantitativeScaleProximity; | |
| 19 | |
| 20 bool _trackingPointsCreated = false; | |
| 21 List _xPositions = []; | |
| 22 | |
| 23 // Currently hovered row/column | |
| 24 int _savedOverRow = 0; | |
| 25 int _savedOverColumn = 0; | |
| 26 | |
| 27 int currentDataIndex = -1; | |
| 28 | |
| 29 @override | |
| 30 final String name = "line-rdr"; | |
| 31 | |
| 32 LineChartRenderer({ | |
| 33 this.alwaysAnimate: false, | |
| 34 this.trackDataPoints: true, | |
| 35 this.trackOnDimensionAxis: false, | |
| 36 this.quantitativeScaleProximity: 5 }); | |
| 37 | |
| 38 /* | |
| 39 * Returns false if the number of dimension axes on the area is 0. | |
| 40 * Otherwise, the first dimension scale is used to render the chart. | |
| 41 */ | |
| 42 @override | |
| 43 bool prepare(ChartArea area, ChartSeries series) { | |
| 44 _ensureAreaAndSeries(area, series); | |
| 45 if (trackDataPoints != false) { | |
| 46 _trackPointerInArea(); | |
| 47 } | |
| 48 return area is CartesianArea; | |
| 49 } | |
| 50 | |
| 51 @override | |
| 52 void draw(Element element, {Future schedulePostRender}) { | |
| 53 _ensureReadyToDraw(element); | |
| 54 | |
| 55 var measureScale = area.measureScales(series).first, | |
| 56 dimensionScale = area.dimensionScales.first; | |
| 57 | |
| 58 // Create lists of values in measure columns. | |
| 59 var lines = series.measures.map((column) { | |
| 60 return area.data.rows.map((values) => values[column]).toList(); | |
| 61 }).toList(); | |
| 62 | |
| 63 // We only support one dimension axes, so we always use the | |
| 64 // first dimension. | |
| 65 var x = area.data.rows.map( | |
| 66 (row) => row.elementAt(area.config.dimensions.first)).toList(); | |
| 67 | |
| 68 var rangeBandOffset = | |
| 69 dimensionScale is OrdinalScale ? dimensionScale.rangeBand / 2 : 0; | |
| 70 | |
| 71 // If tracking data points is enabled, cache location of points that | |
| 72 // represent data. | |
| 73 if (trackDataPoints) { | |
| 74 _xPositions = | |
| 75 x.map((val) => dimensionScale.scale(val) + rangeBandOffset).toList(); | |
| 76 } | |
| 77 | |
| 78 var line = new SvgLine( | |
| 79 xValueAccessor: (d, i) => dimensionScale.scale(x[i]) + rangeBandOffset, | |
| 80 yValueAccessor: (d, i) => measureScale.scale(d)); | |
| 81 | |
| 82 // Add lines and hook up hover and selection events. | |
| 83 var svgLines = root.selectAll('.line-rdr-line').data(lines); | |
| 84 svgLines.enter.append('path').each((d, i, e) { | |
| 85 e.attributes['fill'] = 'none'; | |
| 86 }); | |
| 87 | |
| 88 svgLines.each((d, i, e) { | |
| 89 var column = series.measures.elementAt(i), | |
| 90 color = colorForColumn(column), | |
| 91 filter = filterForColumn(column), | |
| 92 styles = stylesForColumn(column); | |
| 93 e.attributes | |
| 94 ..['d'] = line.path(d, i, e) | |
| 95 ..['stroke'] = color | |
| 96 ..['class'] = styles.isEmpty | |
| 97 ? 'line-rdr-line' | |
| 98 : 'line-rdr-line ${styles.join(' ')}' | |
| 99 ..['data-column'] = '$column'; | |
| 100 if (isNullOrEmpty(filter)) { | |
| 101 e.attributes.remove('filter'); | |
| 102 } else { | |
| 103 e.attributes['filter'] = filter; | |
| 104 } | |
| 105 }); | |
| 106 | |
| 107 if (area.state != null) { | |
| 108 svgLines | |
| 109 ..on('click', (d, i, e) => _mouseClickHandler(d, i, e)) | |
| 110 ..on('mouseover', (d, i, e) => _mouseOverHandler(d, i, e)) | |
| 111 ..on('mouseout', (d, i, e) => _mouseOutHandler(d, i, e)); | |
| 112 } | |
| 113 | |
| 114 svgLines.exit.remove(); | |
| 115 } | |
| 116 | |
| 117 @override | |
| 118 void dispose() { | |
| 119 if (root == null) return; | |
| 120 root.selectAll('.line-rdr-line').remove(); | |
| 121 root.selectAll('.line-rdr-point').remove(); | |
| 122 _disposer.dispose(); | |
| 123 } | |
| 124 | |
| 125 @override | |
| 126 void handleStateChanges(List<ChangeRecord> changes) { | |
| 127 var lines = host.querySelectorAll('.line-rdr-line'); | |
| 128 if (lines == null || lines.isEmpty) return; | |
| 129 | |
| 130 for (int i = 0, len = lines.length; i < len; ++i) { | |
| 131 var line = lines.elementAt(i), | |
| 132 column = int.parse(line.dataset['column']), | |
| 133 filter = filterForColumn(column); | |
| 134 line.classes.removeAll(ChartState.COLUMN_CLASS_NAMES); | |
| 135 line.classes.addAll(stylesForColumn(column)); | |
| 136 line.attributes['stroke'] = colorForColumn(column); | |
| 137 | |
| 138 if (isNullOrEmpty(filter)) { | |
| 139 line.attributes.remove('filter'); | |
| 140 } else { | |
| 141 line.attributes['filter'] = filter; | |
| 142 } | |
| 143 } | |
| 144 } | |
| 145 | |
| 146 void _createTrackingCircles() { | |
| 147 var linePoints = root.selectAll('.line-rdr-point').data(series.measures); | |
| 148 linePoints.enter.append('circle').each((d, i, e) { | |
| 149 e.classes.add('line-rdr-point'); | |
| 150 e.attributes['r'] = '4'; | |
| 151 }); | |
| 152 | |
| 153 linePoints | |
| 154 ..each((d, i, e) { | |
| 155 var color = colorForColumn(d); | |
| 156 e.attributes | |
| 157 ..['r'] = '4' | |
| 158 ..['stroke'] = color | |
| 159 ..['fill'] = color | |
| 160 ..['data-column'] = '$d'; | |
| 161 }) | |
| 162 ..on('click', _mouseClickHandler) | |
| 163 ..on('mousemove', _mouseOverHandler) // Ensure that we update values | |
| 164 ..on('mouseover', _mouseOverHandler) | |
| 165 ..on('mouseout', _mouseOutHandler); | |
| 166 | |
| 167 linePoints.exit.remove(); | |
| 168 _trackingPointsCreated = true; | |
| 169 } | |
| 170 | |
| 171 void _showTrackingCircles(int row) { | |
| 172 if (_trackingPointsCreated == false) { | |
| 173 _createTrackingCircles(); | |
| 174 } | |
| 175 | |
| 176 var yScale = area.measureScales(series).first; | |
| 177 root.selectAll('.line-rdr-point').each((d, i, e) { | |
| 178 var x = _xPositions[row], | |
| 179 measureVal = area.data.rows.elementAt(row).elementAt(d); | |
| 180 if (measureVal != null && measureVal.isFinite) { | |
| 181 var color = colorForColumn(d), | |
| 182 filter = filterForColumn(d); | |
| 183 e.attributes | |
| 184 ..['cx'] = '$x' | |
| 185 ..['cy'] = '${yScale.scale(measureVal)}' | |
| 186 ..['fill'] = color | |
| 187 ..['stroke'] = color | |
| 188 ..['data-row'] = '$row'; | |
| 189 e.style | |
| 190 ..setProperty('opacity', '1') | |
| 191 ..setProperty('visibility', 'visible'); | |
| 192 if (isNullOrEmpty(filter)) { | |
| 193 e.attributes.remove('filter'); | |
| 194 } else { | |
| 195 e.attributes['filter'] = filter; | |
| 196 } | |
| 197 } else { | |
| 198 e.style | |
| 199 ..setProperty('opacity', '$EPSILON') | |
| 200 ..setProperty('visibility', 'hidden'); | |
| 201 } | |
| 202 }); | |
| 203 } | |
| 204 | |
| 205 void _hideTrackingCircles() { | |
| 206 root.selectAll('.line-rdr-point') | |
| 207 ..style('opacity', '0.0') | |
| 208 ..style('visibility', 'hidden'); | |
| 209 } | |
| 210 | |
| 211 int _getNearestRowIndex(double x) { | |
| 212 var lastSmallerValue = 0; | |
| 213 var chartX = x - area.layout.renderArea.x; | |
| 214 for (var i = 0; i < _xPositions.length; i++) { | |
| 215 var pos = _xPositions[i]; | |
| 216 if (pos < chartX) { | |
| 217 lastSmallerValue = pos; | |
| 218 } else { | |
| 219 return i == 0 ? 0 : | |
| 220 (chartX - lastSmallerValue <= pos - chartX) ? i - 1 : i; | |
| 221 } | |
| 222 } | |
| 223 return _xPositions.length - 1; | |
| 224 } | |
| 225 | |
| 226 void _trackPointerInArea() { | |
| 227 _trackingPointsCreated = false; | |
| 228 _disposer.add(area.onMouseMove.listen((ChartEvent event) { | |
| 229 if (area.layout.renderArea.contains(event.chartX, event.chartY)) { | |
| 230 var row = _getNearestRowIndex(event.chartX); | |
| 231 window.animationFrame.then((_) => _showTrackingCircles(row)); | |
| 232 } else { | |
| 233 _hideTrackingCircles(); | |
| 234 } | |
| 235 })); | |
| 236 _disposer.add(area.onMouseOut.listen((ChartEvent event) { | |
| 237 _hideTrackingCircles(); | |
| 238 })); | |
| 239 } | |
| 240 | |
| 241 void _mouseClickHandler(d, int i, Element e) { | |
| 242 if (area.state != null) { | |
| 243 area.state.select(int.parse(e.dataset['column'])); | |
| 244 } | |
| 245 if (mouseClickController != null && e.tagName == 'circle') { | |
| 246 var row = int.parse(e.dataset['row']), | |
| 247 column = int.parse(e.dataset['column']); | |
| 248 mouseClickController.add( | |
| 249 new DefaultChartEventImpl(scope.event, area, series, row, column, d)); | |
| 250 } | |
| 251 } | |
| 252 | |
| 253 void _mouseOverHandler(d, i, e) { | |
| 254 if (area.state != null) { | |
| 255 area.state.preview = int.parse(e.dataset['column']); | |
| 256 } | |
| 257 if (mouseOverController != null && e.tagName == 'circle') { | |
| 258 _savedOverRow = int.parse(e.dataset['row']); | |
| 259 _savedOverColumn = int.parse(e.dataset['column']); | |
| 260 mouseOverController.add(new DefaultChartEventImpl( | |
| 261 scope.event, area, series, _savedOverRow, _savedOverColumn, d)); | |
| 262 } | |
| 263 } | |
| 264 | |
| 265 void _mouseOutHandler(d, i, e) { | |
| 266 if (area.state != null && | |
| 267 area.state.preview == int.parse(e.dataset['column'])) { | |
| 268 area.state.preview = null; | |
| 269 } | |
| 270 if (mouseOutController != null && e.tagName == 'circle') { | |
| 271 mouseOutController.add(new DefaultChartEventImpl( | |
| 272 scope.event, area, series, _savedOverRow, _savedOverColumn, d)); | |
| 273 } | |
| 274 } | |
| 275 } | |
| OLD | NEW |