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 |