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 /// Displays either one or two dimension axes and zero or more measure axis. | |
12 /// The number of measure axes displayed is zero in charts like bubble chart | |
13 /// which contain two dimension axes. | |
14 class DefaultCartesianAreaImpl implements CartesianArea { | |
15 /// Default identifiers used by the measure axes | |
16 static const MEASURE_AXIS_IDS = const['_default']; | |
17 | |
18 /// Orientations used by measure axes. First, when "x" axis is the primary | |
19 /// and the only dimension. Second, when "y" axis is the primary and the only | |
20 /// dimension. | |
21 static const MEASURE_AXIS_ORIENTATIONS = const[ | |
22 const[ORIENTATION_LEFT, ORIENTATION_RIGHT], | |
23 const[ORIENTATION_BOTTOM, ORIENTATION_TOP] | |
24 ]; | |
25 | |
26 /// Orientations used by the dimension axes. First, when "x" is the | |
27 /// primary dimension and the last one for cases where "y" axis is primary | |
28 /// dimension. | |
29 static const DIMENSION_AXIS_ORIENTATIONS = const[ | |
30 const[ORIENTATION_BOTTOM, ORIENTATION_LEFT], | |
31 const[ORIENTATION_LEFT, ORIENTATION_BOTTOM] | |
32 ]; | |
33 | |
34 /// Mapping of measure axis Id to it's axis. | |
35 final _measureAxes = new LinkedHashMap<String, DefaultChartAxisImpl>(); | |
36 | |
37 /// Mapping of dimension column index to it's axis. | |
38 final _dimensionAxes = new LinkedHashMap<int, DefaultChartAxisImpl>(); | |
39 | |
40 /// Disposer for all change stream subscriptions related to data. | |
41 final _dataEventsDisposer = new SubscriptionsDisposer(); | |
42 | |
43 /// Disposer for all change stream subscriptions related to config. | |
44 final _configEventsDisposer = new SubscriptionsDisposer(); | |
45 | |
46 @override | |
47 final Element host; | |
48 | |
49 @override | |
50 final bool useTwoDimensionAxes; | |
51 | |
52 @override | |
53 final bool useRowColoring; | |
54 | |
55 /// Indicates whether any renderers need bands on primary dimension | |
56 final List<int> dimensionsUsingBands = []; | |
57 | |
58 @override | |
59 final ChartState state; | |
60 | |
61 @override | |
62 _ChartAreaLayout layout = new _ChartAreaLayout(); | |
63 | |
64 @override | |
65 Selection upperBehaviorPane; | |
66 | |
67 @override | |
68 Selection lowerBehaviorPane; | |
69 | |
70 @override | |
71 bool isReady = false; | |
72 | |
73 @override | |
74 ChartTheme theme; | |
75 | |
76 ChartData _data; | |
77 ChartConfig _config; | |
78 bool _autoUpdate = false; | |
79 | |
80 SelectionScope _scope; | |
81 Selection _svg; | |
82 Selection visualization; | |
83 | |
84 Iterable<ChartSeries> _series; | |
85 | |
86 bool _pendingLegendUpdate = false; | |
87 List<ChartBehavior> _behaviors = new List<ChartBehavior>(); | |
88 Map<ChartSeries, _ChartSeriesInfo> _seriesInfoCache = new Map(); | |
89 | |
90 StreamController<ChartEvent> _valueMouseOverController; | |
91 StreamController<ChartEvent> _valueMouseOutController; | |
92 StreamController<ChartEvent> _valueMouseClickController; | |
93 StreamController<ChartArea> _chartAxesUpdatedController; | |
94 | |
95 DefaultCartesianAreaImpl( | |
96 this.host, | |
97 ChartData data, | |
98 ChartConfig config, | |
99 bool autoUpdate, | |
100 this.useTwoDimensionAxes, | |
101 this.useRowColoring, | |
102 this.state) : _autoUpdate = autoUpdate { | |
103 assert(host != null); | |
104 assert(isNotInline(host)); | |
105 | |
106 this.data = data; | |
107 this.config = config; | |
108 theme = new QuantumChartTheme(); | |
109 | |
110 Transition.defaultEasingType = theme.transitionEasingType; | |
111 Transition.defaultEasingMode = theme.transitionEasingMode; | |
112 Transition.defaultDurationMilliseconds = | |
113 theme.transitionDurationMilliseconds; | |
114 } | |
115 | |
116 void dispose() { | |
117 _configEventsDisposer.dispose(); | |
118 _dataEventsDisposer.dispose(); | |
119 _config.legend.dispose(); | |
120 | |
121 if (_valueMouseOverController != null) { | |
122 _valueMouseOverController.close(); | |
123 _valueMouseOverController = null; | |
124 } | |
125 if (_valueMouseOutController != null) { | |
126 _valueMouseOutController.close(); | |
127 _valueMouseOutController = null; | |
128 } | |
129 if (_valueMouseClickController != null) { | |
130 _valueMouseClickController.close(); | |
131 _valueMouseClickController = null; | |
132 } | |
133 if (_chartAxesUpdatedController != null) { | |
134 _chartAxesUpdatedController.close(); | |
135 _chartAxesUpdatedController = null; | |
136 } | |
137 } | |
138 | |
139 static bool isNotInline(Element e) => | |
140 e != null && e.getComputedStyle().display != 'inline'; | |
141 | |
142 /// Set new data for this chart. If [value] is [Observable], subscribes to | |
143 /// changes and updates the chart when data changes. | |
144 @override | |
145 set data(ChartData value) { | |
146 _data = value; | |
147 _dataEventsDisposer.dispose(); | |
148 _pendingLegendUpdate = true; | |
149 | |
150 if (autoUpdate && _data != null && _data is Observable) { | |
151 _dataEventsDisposer.add((_data as Observable).changes.listen((_) { | |
152 _pendingLegendUpdate = true; | |
153 draw(); | |
154 })); | |
155 } | |
156 } | |
157 | |
158 @override | |
159 ChartData get data => _data; | |
160 | |
161 /// Set new config for this chart. If [value] is [Observable], subscribes to | |
162 /// changes and updates the chart when series or dimensions change. | |
163 @override | |
164 set config(ChartConfig value) { | |
165 _config = value; | |
166 _configEventsDisposer.dispose(); | |
167 _pendingLegendUpdate = true; | |
168 | |
169 if (_config != null && _config is Observable) { | |
170 _configEventsDisposer.add((_config as Observable).changes.listen((_) { | |
171 _pendingLegendUpdate = true; | |
172 draw(); | |
173 })); | |
174 } | |
175 } | |
176 | |
177 @override | |
178 ChartConfig get config => _config; | |
179 | |
180 @override | |
181 set autoUpdate(bool value) { | |
182 if (_autoUpdate != value) { | |
183 _autoUpdate = value; | |
184 this.data = _data; | |
185 this.config = _config; | |
186 } | |
187 } | |
188 | |
189 @override | |
190 bool get autoUpdate => _autoUpdate; | |
191 | |
192 /// Gets measure axis from cache - creates a new instance of _ChartAxis | |
193 /// if one was not already created for the given [axisId]. | |
194 DefaultChartAxisImpl _getMeasureAxis(String axisId) { | |
195 _measureAxes.putIfAbsent(axisId, () { | |
196 var axisConf = config.getMeasureAxis(axisId), | |
197 axis = axisConf != null ? | |
198 new DefaultChartAxisImpl.withAxisConfig(this, axisConf) : | |
199 new DefaultChartAxisImpl(this); | |
200 return axis; | |
201 }); | |
202 return _measureAxes[axisId]; | |
203 } | |
204 | |
205 /// Gets a dimension axis from cache - creates a new instance of _ChartAxis | |
206 /// if one was not already created for the given dimension [column]. | |
207 DefaultChartAxisImpl _getDimensionAxis(int column) { | |
208 _dimensionAxes.putIfAbsent(column, () { | |
209 var axisConf = config.getDimensionAxis(column), | |
210 axis = axisConf != null ? | |
211 new DefaultChartAxisImpl.withAxisConfig(this, axisConf) : | |
212 new DefaultChartAxisImpl(this); | |
213 return axis; | |
214 }); | |
215 return _dimensionAxes[column]; | |
216 } | |
217 | |
218 /// All columns rendered by a series must be of the same type. | |
219 bool _isSeriesValid(ChartSeries s) { | |
220 var first = data.columns.elementAt(s.measures.first).type; | |
221 return s.measures.every((i) => | |
222 (i < data.columns.length) && data.columns.elementAt(i).type == first); | |
223 } | |
224 | |
225 @override | |
226 Iterable<Scale> get dimensionScales => | |
227 config.dimensions.map((int column) => _getDimensionAxis(column).scale); | |
228 | |
229 @override | |
230 Iterable<Scale> measureScales(ChartSeries series) { | |
231 var axisIds = isNullOrEmpty(series.measureAxisIds) | |
232 ? MEASURE_AXIS_IDS | |
233 : series.measureAxisIds; | |
234 return axisIds.map((String id) => _getMeasureAxis(id).scale); | |
235 } | |
236 | |
237 /// Computes the size of chart and if changed from the previous time | |
238 /// size was computed, sets attributes on svg element | |
239 Rect _computeChartSize() { | |
240 int width = host.clientWidth, | |
241 height = host.clientHeight; | |
242 | |
243 if (config.minimumSize != null) { | |
244 width = max([width, config.minimumSize.width]); | |
245 height = max([height, config.minimumSize.height]); | |
246 } | |
247 | |
248 AbsoluteRect padding = theme.padding; | |
249 num paddingLeft = config.isRTL ? padding.end : padding.start; | |
250 Rect current = new Rect(paddingLeft, padding.top, | |
251 width - (padding.start + padding.end), | |
252 height - (padding.top + padding.bottom)); | |
253 if (layout.chartArea == null || layout.chartArea != current) { | |
254 _svg.attr('width', width.toString()); | |
255 _svg.attr('height', height.toString()); | |
256 layout.chartArea = current; | |
257 | |
258 var transform = 'translate(${paddingLeft},${padding.top})'; | |
259 visualization.first.attributes['transform'] = transform; | |
260 lowerBehaviorPane.first.attributes['transform'] = transform; | |
261 upperBehaviorPane.first.attributes['transform'] = transform; | |
262 } | |
263 return layout.chartArea; | |
264 } | |
265 | |
266 @override | |
267 draw({bool preRender:false, Future schedulePostRender}) { | |
268 assert(data != null && config != null); | |
269 assert(config.series != null && config.series.isNotEmpty); | |
270 | |
271 // One time initialization. | |
272 // Each [ChartArea] has it's own [SelectionScope] | |
273 if (_scope == null) { | |
274 _scope = new SelectionScope.element(host); | |
275 _svg = _scope.append('svg:svg')..classed('chart-canvas'); | |
276 if (!isNullOrEmpty(theme.filters)) { | |
277 var element = _svg.first, | |
278 defs = Namespace.createChildElement('defs', element) | |
279 ..append(new SvgElement.svg( | |
280 theme.filters, treeSanitizer: new NullTreeSanitizer())); | |
281 _svg.first.append(defs); | |
282 } | |
283 | |
284 lowerBehaviorPane = _svg.append('g')..classed('lower-render-pane'); | |
285 visualization = _svg.append('g')..classed('chart-render-pane'); | |
286 upperBehaviorPane = _svg.append('g')..classed('upper-render-pane'); | |
287 | |
288 if (_behaviors.isNotEmpty) { | |
289 _behaviors.forEach( | |
290 (b) => b.init(this, upperBehaviorPane, lowerBehaviorPane)); | |
291 } | |
292 } | |
293 | |
294 // Compute chart sizes and filter out unsupported series | |
295 _computeChartSize(); | |
296 var series = config.series.where((s) => | |
297 _isSeriesValid(s) && s.renderer.prepare(this, s)), | |
298 selection = visualization.selectAll('.series-group'). | |
299 data(series, (x) => x.hashCode), | |
300 axesDomainCompleter = new Completer(); | |
301 | |
302 // Wait till the axes are rendered before rendering series. | |
303 // In an SVG, z-index is based on the order of nodes in the DOM. | |
304 axesDomainCompleter.future.then((_) { | |
305 selection.enter.append('svg:g')..classed('series-group'); | |
306 String transform = | |
307 'translate(${layout.renderArea.x},${layout.renderArea.y})'; | |
308 | |
309 selection.each((ChartSeries s, _, Element group) { | |
310 _ChartSeriesInfo info = _seriesInfoCache[s]; | |
311 if (info == null) { | |
312 info = _seriesInfoCache[s] = new _ChartSeriesInfo(this, s); | |
313 } | |
314 info.check(); | |
315 group.attributes['transform'] = transform; | |
316 (s.renderer as CartesianRenderer) | |
317 .draw(group, schedulePostRender:schedulePostRender); | |
318 }); | |
319 | |
320 // A series that was rendered earlier isn't there anymore, remove it | |
321 selection.exit | |
322 ..each((ChartSeries s, _, __) { | |
323 var info = _seriesInfoCache.remove(s); | |
324 if (info != null) { | |
325 info.dispose(); | |
326 } | |
327 }) | |
328 ..remove(); | |
329 | |
330 // Notify on the stream that the chart has been updated. | |
331 isReady = true; | |
332 if (_chartAxesUpdatedController != null) { | |
333 _chartAxesUpdatedController.add(this); | |
334 } | |
335 }); | |
336 | |
337 // Save the list of valid series and initialize axes. | |
338 _series = series; | |
339 _initAxes(preRender: preRender); | |
340 | |
341 // Render the chart, now that the axes layer is already in DOM. | |
342 axesDomainCompleter.complete(); | |
343 | |
344 // Updates the legend if required. | |
345 _updateLegend(); | |
346 } | |
347 | |
348 String _orientRTL(String orientation) => orientation; | |
349 Scale _scaleRTL(Scale scale) => scale; | |
350 | |
351 /// Initialize the axes - required even if the axes are not being displayed. | |
352 _initAxes({bool preRender: false}) { | |
353 Map measureAxisUsers = <String,Iterable<ChartSeries>>{}; | |
354 | |
355 // Create necessary measures axes. | |
356 // If measure axes were not configured on the series, default is used. | |
357 _series.forEach((ChartSeries s) { | |
358 var measureAxisIds = isNullOrEmpty(s.measureAxisIds) | |
359 ? MEASURE_AXIS_IDS | |
360 : s.measureAxisIds; | |
361 measureAxisIds.forEach((axisId) { | |
362 _getMeasureAxis(axisId); // Creates axis if required | |
363 var users = measureAxisUsers[axisId]; | |
364 if (users == null) { | |
365 measureAxisUsers[axisId] = [s]; | |
366 } else { | |
367 users.add(s); | |
368 } | |
369 }); | |
370 }); | |
371 | |
372 // Now that we know a list of series using each measure axis, configure | |
373 // the input domain of each axis. | |
374 measureAxisUsers.forEach((id, listOfSeries) { | |
375 var sampleCol = listOfSeries.first.measures.first, | |
376 sampleColSpec = data.columns.elementAt(sampleCol), | |
377 axis = _getMeasureAxis(id), | |
378 domain; | |
379 | |
380 if (sampleColSpec.useOrdinalScale) { | |
381 throw new UnsupportedError( | |
382 'Ordinal measure axes are not currently supported.'); | |
383 } else { | |
384 // Extent is available because [ChartRenderer.prepare] was already | |
385 // called (when checking for valid series in [draw]. | |
386 Iterable extents = listOfSeries.map((s) => s.renderer.extent).toList(); | |
387 var lowest = min(extents.map((e) => e.min)), | |
388 highest = max(extents.map((e) => e.max)); | |
389 | |
390 // Use default domain if lowest and highest are the same, right now | |
391 // lowest is always 0 unless it is less than 0 - change to lowest when | |
392 // we make use of it. | |
393 domain = highest == lowest | |
394 ? (highest == 0 | |
395 ? [0, 1] | |
396 : (highest < 0 ? [highest, 0] : [0, highest])) | |
397 : (lowest <= 0 ? [lowest, highest] : [0, highest]); | |
398 } | |
399 axis.initAxisDomain(sampleCol, false, domain); | |
400 }); | |
401 | |
402 // Configure dimension axes. | |
403 int dimensionAxesCount = useTwoDimensionAxes ? 2 : 1; | |
404 config.dimensions.take(dimensionAxesCount).forEach((int column) { | |
405 var axis = _getDimensionAxis(column), | |
406 sampleColumnSpec = data.columns.elementAt(column), | |
407 values = data.rows.map((row) => row.elementAt(column)), | |
408 domain; | |
409 | |
410 if (sampleColumnSpec.useOrdinalScale) { | |
411 domain = values.map((e) => e.toString()).toList(); | |
412 } else { | |
413 var extent = new Extent.items(values); | |
414 domain = [extent.min, extent.max]; | |
415 } | |
416 axis.initAxisDomain(column, true, domain); | |
417 }); | |
418 | |
419 // See if any dimensions need "band" on the axis. | |
420 dimensionsUsingBands.clear(); | |
421 List<bool> usingBands = [false, false]; | |
422 _series.forEach((ChartSeries s) => | |
423 (s.renderer as CartesianRenderer).dimensionsUsingBand.forEach((x) { | |
424 if (x <= 1 && !(usingBands[x])) { | |
425 usingBands[x] = true; | |
426 dimensionsUsingBands.add(config.dimensions.elementAt(x)); | |
427 } | |
428 })); | |
429 | |
430 // List of measure and dimension axes that are displayed | |
431 assert( | |
432 isNullOrEmpty(config.displayedMeasureAxes) || | |
433 config.displayedMeasureAxes.length < 2); | |
434 var measureAxesCount = dimensionAxesCount == 1 ? 2 : 0, | |
435 displayedMeasureAxes = (isNullOrEmpty(config.displayedMeasureAxes) | |
436 ? _measureAxes.keys.take(measureAxesCount) | |
437 : config.displayedMeasureAxes.take(measureAxesCount)). | |
438 toList(growable: false), | |
439 displayedDimensionAxes = | |
440 config.dimensions.take(dimensionAxesCount).toList(growable: false); | |
441 | |
442 // Compute size of the dimension axes | |
443 if (config.renderDimensionAxes != false) { | |
444 var dimensionAxisOrientations = config.isLeftAxisPrimary | |
445 ? DIMENSION_AXIS_ORIENTATIONS.last | |
446 : DIMENSION_AXIS_ORIENTATIONS.first; | |
447 for (int i = 0, len = displayedDimensionAxes.length; i < len; ++i) { | |
448 var axis = _dimensionAxes[displayedDimensionAxes[i]], | |
449 orientation = _orientRTL(dimensionAxisOrientations[i]); | |
450 axis.prepareToDraw(orientation); | |
451 layout._axes[orientation] = axis.size; | |
452 } | |
453 } | |
454 | |
455 // Compute size of the measure axes | |
456 if (displayedMeasureAxes.isNotEmpty) { | |
457 var measureAxisOrientations = config.isLeftAxisPrimary | |
458 ? MEASURE_AXIS_ORIENTATIONS.last | |
459 : MEASURE_AXIS_ORIENTATIONS.first; | |
460 displayedMeasureAxes.asMap().forEach((int index, String key) { | |
461 var axis = _measureAxes[key], | |
462 orientation = _orientRTL(measureAxisOrientations[index]); | |
463 axis.prepareToDraw(orientation); | |
464 layout._axes[orientation] = axis.size; | |
465 }); | |
466 } | |
467 | |
468 // Consolidate all the information that we collected into final layout | |
469 _computeLayout( | |
470 displayedMeasureAxes.isEmpty && config.renderDimensionAxes == false); | |
471 | |
472 // Domains for all axes have been taken care of and _ChartAxis ensures | |
473 // that the scale is initialized on visible axes. Initialize the scale on | |
474 // all invisible measure scales. | |
475 if (_measureAxes.length != displayedMeasureAxes.length) { | |
476 _measureAxes.keys.forEach((String axisId) { | |
477 if (displayedMeasureAxes.contains(axisId)) return; | |
478 _getMeasureAxis(axisId).initAxisScale([layout.renderArea.height, 0]); | |
479 }); | |
480 } | |
481 | |
482 // Draw the visible measure axes, if any. | |
483 if (displayedMeasureAxes.isNotEmpty) { | |
484 var axisGroups = visualization. | |
485 selectAll('.measure-axis-group').data(displayedMeasureAxes); | |
486 // Update measure axis (add/remove/update) | |
487 axisGroups.enter.append('svg:g'); | |
488 axisGroups.each((axisId, index, group) { | |
489 _getMeasureAxis(axisId).draw(group, _scope, preRender: preRender); | |
490 group.attributes['class'] = 'measure-axis-group measure-${index}'; | |
491 }); | |
492 axisGroups.exit.remove(); | |
493 } | |
494 | |
495 // Draw the dimension axes, unless asked not to. | |
496 if (config.renderDimensionAxes != false) { | |
497 var dimAxisGroups = visualization. | |
498 selectAll('.dimension-axis-group').data(displayedDimensionAxes); | |
499 // Update dimension axes (add/remove/update) | |
500 dimAxisGroups.enter.append('svg:g'); | |
501 dimAxisGroups.each((column, index, group) { | |
502 _getDimensionAxis(column).draw(group, _scope, preRender: preRender); | |
503 group.attributes['class'] = 'dimension-axis-group dim-${index}'; | |
504 }); | |
505 dimAxisGroups.exit.remove(); | |
506 } else { | |
507 // Initialize scale on invisible axis | |
508 var dimensionAxisOrientations = config.isLeftAxisPrimary ? | |
509 DIMENSION_AXIS_ORIENTATIONS.last : DIMENSION_AXIS_ORIENTATIONS.first; | |
510 for (int i = 0; i < dimensionAxesCount; ++i) { | |
511 var column = config.dimensions.elementAt(i), | |
512 axis = _dimensionAxes[column], | |
513 orientation = dimensionAxisOrientations[i]; | |
514 axis.initAxisScale(orientation == ORIENTATION_LEFT ? | |
515 [layout.renderArea.height, 0] : [0, layout.renderArea.width]); | |
516 }; | |
517 } | |
518 } | |
519 | |
520 // Compute chart render area size and positions of all elements | |
521 _computeLayout(bool notRenderingAxes) { | |
522 if (notRenderingAxes) { | |
523 layout.renderArea = | |
524 new Rect(0, 0, layout.chartArea.height, layout.chartArea.width); | |
525 return; | |
526 } | |
527 | |
528 var top = layout.axes[ORIENTATION_TOP], | |
529 left = layout.axes[ORIENTATION_LEFT], | |
530 bottom = layout.axes[ORIENTATION_BOTTOM], | |
531 right = layout.axes[ORIENTATION_RIGHT]; | |
532 | |
533 var renderAreaHeight = layout.chartArea.height - | |
534 (top.height + layout.axes[ORIENTATION_BOTTOM].height), | |
535 renderAreaWidth = layout.chartArea.width - | |
536 (left.width + layout.axes[ORIENTATION_RIGHT].width); | |
537 | |
538 layout.renderArea = new Rect( | |
539 left.width, top.height, renderAreaWidth, renderAreaHeight); | |
540 | |
541 layout._axes | |
542 ..[ORIENTATION_TOP] = | |
543 new Rect(left.width, 0, renderAreaWidth, top.height) | |
544 ..[ORIENTATION_RIGHT] = | |
545 new Rect(left.width + renderAreaWidth, top.y, | |
546 right.width, renderAreaHeight) | |
547 ..[ORIENTATION_BOTTOM] = | |
548 new Rect(left.width, top.height + renderAreaHeight, | |
549 renderAreaWidth, bottom.height) | |
550 ..[ORIENTATION_LEFT] = | |
551 new Rect( | |
552 left.width, top.height, left.width, renderAreaHeight); | |
553 } | |
554 | |
555 // Updates the legend, if configuration changed since the last | |
556 // time the legend was updated. | |
557 _updateLegend() { | |
558 if (!_pendingLegendUpdate) return; | |
559 if (_config == null || _config.legend == null || _series.isEmpty) return; | |
560 | |
561 var legend = <ChartLegendItem>[]; | |
562 List seriesByColumn = | |
563 new List.generate(data.columns.length, (_) => new List()); | |
564 | |
565 _series.forEach((s) => | |
566 s.measures.forEach((m) => seriesByColumn[m].add(s))); | |
567 | |
568 seriesByColumn.asMap().forEach((int i, List s) { | |
569 if (s.length == 0) return; | |
570 legend.add(new ChartLegendItem( | |
571 index:i, label:data.columns.elementAt(i).label, series:s, | |
572 color:theme.getColorForKey(i))); | |
573 }); | |
574 | |
575 _config.legend.update(legend, this); | |
576 _pendingLegendUpdate = false; | |
577 } | |
578 | |
579 @override | |
580 Stream<ChartEvent> get onMouseUp => | |
581 host.onMouseUp | |
582 .map((MouseEvent e) => new DefaultChartEventImpl(e, this)); | |
583 | |
584 @override | |
585 Stream<ChartEvent> get onMouseDown => | |
586 host.onMouseDown | |
587 .map((MouseEvent e) => new DefaultChartEventImpl(e, this)); | |
588 | |
589 @override | |
590 Stream<ChartEvent> get onMouseOver => | |
591 host.onMouseOver | |
592 .map((MouseEvent e) => new DefaultChartEventImpl(e, this)); | |
593 | |
594 @override | |
595 Stream<ChartEvent> get onMouseOut => | |
596 host.onMouseOut | |
597 .map((MouseEvent e) => new DefaultChartEventImpl(e, this)); | |
598 | |
599 @override | |
600 Stream<ChartEvent> get onMouseMove => | |
601 host.onMouseMove | |
602 .map((MouseEvent e) => new DefaultChartEventImpl(e, this)); | |
603 | |
604 @override | |
605 Stream<ChartEvent> get onValueClick { | |
606 if (_valueMouseClickController == null) { | |
607 _valueMouseClickController = new StreamController.broadcast(sync: true); | |
608 } | |
609 return _valueMouseClickController.stream; | |
610 } | |
611 | |
612 @override | |
613 Stream<ChartEvent> get onValueMouseOver { | |
614 if (_valueMouseOverController == null) { | |
615 _valueMouseOverController = new StreamController.broadcast(sync: true); | |
616 } | |
617 return _valueMouseOverController.stream; | |
618 } | |
619 | |
620 @override | |
621 Stream<ChartEvent> get onValueMouseOut { | |
622 if (_valueMouseOutController == null) { | |
623 _valueMouseOutController = new StreamController.broadcast(sync: true); | |
624 } | |
625 return _valueMouseOutController.stream; | |
626 } | |
627 | |
628 @override | |
629 Stream<ChartArea> get onChartAxesUpdated { | |
630 if (_chartAxesUpdatedController == null) { | |
631 _chartAxesUpdatedController = new StreamController.broadcast(sync: true); | |
632 } | |
633 return _chartAxesUpdatedController.stream; | |
634 } | |
635 | |
636 @override | |
637 void addChartBehavior(ChartBehavior behavior) { | |
638 if (behavior == null || _behaviors.contains(behavior)) return; | |
639 _behaviors.add(behavior); | |
640 if (upperBehaviorPane != null && lowerBehaviorPane != null) { | |
641 behavior.init(this, upperBehaviorPane, lowerBehaviorPane); | |
642 } | |
643 } | |
644 | |
645 @override | |
646 void removeChartBehavior(ChartBehavior behavior) { | |
647 if (behavior == null || !_behaviors.contains(behavior)) return; | |
648 if (upperBehaviorPane != null && lowerBehaviorPane != null) { | |
649 behavior.dispose(); | |
650 } | |
651 _behaviors.remove(behavior); | |
652 } | |
653 } | |
654 | |
655 class _ChartAreaLayout implements ChartAreaLayout { | |
656 final _axes = <String, Rect>{ | |
657 ORIENTATION_LEFT: const Rect(), | |
658 ORIENTATION_RIGHT: const Rect(), | |
659 ORIENTATION_TOP: const Rect(), | |
660 ORIENTATION_BOTTOM: const Rect() | |
661 }; | |
662 | |
663 UnmodifiableMapView<String, Rect> _axesView; | |
664 | |
665 @override | |
666 get axes => _axesView; | |
667 | |
668 @override | |
669 Rect renderArea; | |
670 | |
671 @override | |
672 Rect chartArea; | |
673 | |
674 _ChartAreaLayout() { | |
675 _axesView = new UnmodifiableMapView(_axes); | |
676 } | |
677 } | |
678 | |
679 class _ChartSeriesInfo { | |
680 CartesianRenderer _renderer; | |
681 SubscriptionsDisposer _disposer = new SubscriptionsDisposer(); | |
682 | |
683 DefaultChartSeriesImpl _series; | |
684 DefaultCartesianAreaImpl _area; | |
685 _ChartSeriesInfo(this._area, this._series); | |
686 | |
687 _click(ChartEvent e) { | |
688 var state = _area.state; | |
689 if (state != null) { | |
690 if (state.isHighlighted(e.column, e.row)) { | |
691 state.unhighlight(e.column, e.row); | |
692 } else { | |
693 state.highlight(e.column, e.row); | |
694 } | |
695 } | |
696 if (_area._valueMouseClickController != null) { | |
697 _area._valueMouseClickController.add(e); | |
698 } | |
699 } | |
700 | |
701 _mouseOver(ChartEvent e) { | |
702 var state = _area.state; | |
703 if (state != null) { | |
704 state.hovered = new Pair(e.column, e.row); | |
705 } | |
706 if (_area._valueMouseOverController != null) { | |
707 _area._valueMouseOverController.add(e); | |
708 } | |
709 } | |
710 | |
711 _mouseOut(ChartEvent e) { | |
712 var state = _area.state; | |
713 if (state != null) { | |
714 var current = state.hovered; | |
715 if (current != null && | |
716 current.first == e.column && current.last == e.row) { | |
717 state.hovered = null; | |
718 } | |
719 } | |
720 if (_area._valueMouseOutController != null) { | |
721 _area._valueMouseOutController.add(e); | |
722 } | |
723 } | |
724 | |
725 check() { | |
726 if (_renderer != _series.renderer) { | |
727 dispose(); | |
728 if (_series.renderer is ChartRendererBehaviorSource){ | |
729 _disposer.addAll([ | |
730 _series.renderer.onValueClick.listen(_click), | |
731 _series.renderer.onValueMouseOver.listen(_mouseOver), | |
732 _series.renderer.onValueMouseOut.listen(_mouseOut) | |
733 ]); | |
734 } | |
735 } | |
736 _renderer = _series.renderer; | |
737 } | |
738 | |
739 dispose() => _disposer.dispose(); | |
740 } | |
OLD | NEW |