| // |
| // Copyright 2014 Google Inc. All rights reserved. |
| // |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file or at |
| // https://developers.google.com/open-source/licenses/bsd |
| // |
| |
| part of charted.charts; |
| |
| /// Displays either one or two dimension axes and zero or more measure axis. |
| /// The number of measure axes displayed is zero in charts like bubble chart |
| /// which contain two dimension axes. |
| class DefaultCartesianAreaImpl implements CartesianArea { |
| /// Default identifiers used by the measure axes |
| static const MEASURE_AXIS_IDS = const <String>['_default']; |
| |
| /// Orientations used by measure axes. First, when "x" axis is the primary |
| /// and the only dimension. Second, when "y" axis is the primary and the only |
| /// dimension. |
| static const MEASURE_AXIS_ORIENTATIONS = const [ |
| const [ORIENTATION_LEFT, ORIENTATION_RIGHT], |
| const [ORIENTATION_BOTTOM, ORIENTATION_TOP] |
| ]; |
| |
| /// Orientations used by the dimension axes. First, when "x" is the |
| /// primary dimension and the last one for cases where "y" axis is primary |
| /// dimension. |
| static const DIMENSION_AXIS_ORIENTATIONS = const [ |
| const [ORIENTATION_BOTTOM, ORIENTATION_LEFT], |
| const [ORIENTATION_LEFT, ORIENTATION_BOTTOM] |
| ]; |
| |
| /// Mapping of measure axis Id to it's axis. |
| final _measureAxes = new LinkedHashMap<String, DefaultChartAxisImpl>(); |
| |
| /// Mapping of dimension column index to it's axis. |
| final _dimensionAxes = new LinkedHashMap<int, DefaultChartAxisImpl>(); |
| |
| /// Disposer for all change stream subscriptions related to data. |
| final _dataEventsDisposer = new SubscriptionsDisposer(); |
| |
| /// Disposer for all change stream subscriptions related to config. |
| final _configEventsDisposer = new SubscriptionsDisposer(); |
| |
| @override |
| final Element host; |
| |
| @override |
| final bool useTwoDimensionAxes; |
| |
| @override |
| final bool useRowColoring; |
| |
| /// Indicates whether any renderers need bands on primary dimension |
| final List<int> dimensionsUsingBands = []; |
| |
| @override |
| final ChartState state; |
| |
| @override |
| _ChartAreaLayout layout = new _ChartAreaLayout(); |
| |
| @override |
| Selection upperBehaviorPane; |
| |
| @override |
| Selection lowerBehaviorPane; |
| |
| @override |
| bool isReady = false; |
| |
| @override |
| ChartTheme theme; |
| |
| ChartData _data; |
| ChartConfig _config; |
| bool _autoUpdate = false; |
| |
| SelectionScope _scope; |
| Selection _svg; |
| Selection visualization; |
| |
| Iterable<ChartSeries> _series; |
| |
| bool _pendingLegendUpdate = false; |
| bool _pendingAxisConfigUpdate = false; |
| List<ChartBehavior> _behaviors = []; |
| Map<ChartSeries, _ChartSeriesInfo> _seriesInfoCache = {}; |
| |
| StreamController<ChartEvent> _valueMouseOverController; |
| StreamController<ChartEvent> _valueMouseOutController; |
| StreamController<ChartEvent> _valueMouseClickController; |
| StreamController<ChartArea> _chartAxesUpdatedController; |
| |
| DefaultCartesianAreaImpl( |
| this.host, |
| ChartData data, |
| ChartConfig config, |
| bool autoUpdate, |
| this.useTwoDimensionAxes, |
| this.useRowColoring, |
| this.state) |
| : _autoUpdate = autoUpdate { |
| assert(host != null); |
| assert(isNotInline(host)); |
| |
| this.data = data; |
| this.config = config; |
| theme = new QuantumChartTheme(); |
| |
| Transition.defaultEasingType = theme.transitionEasingType; |
| Transition.defaultEasingMode = theme.transitionEasingMode; |
| Transition.defaultDurationMilliseconds = |
| theme.transitionDurationMilliseconds; |
| } |
| |
| void dispose() { |
| _configEventsDisposer.dispose(); |
| _dataEventsDisposer.dispose(); |
| _config?.legend?.dispose(); |
| |
| if (_valueMouseOverController != null) { |
| _valueMouseOverController.close(); |
| _valueMouseOverController = null; |
| } |
| if (_valueMouseOutController != null) { |
| _valueMouseOutController.close(); |
| _valueMouseOutController = null; |
| } |
| if (_valueMouseClickController != null) { |
| _valueMouseClickController.close(); |
| _valueMouseClickController = null; |
| } |
| if (_chartAxesUpdatedController != null) { |
| _chartAxesUpdatedController.close(); |
| _chartAxesUpdatedController = null; |
| } |
| if (_behaviors.isNotEmpty) { |
| _behaviors.forEach((behavior) => behavior.dispose()); |
| } |
| } |
| |
| static bool isNotInline(Element e) => |
| e != null && e.getComputedStyle().display != 'inline'; |
| |
| /// Set new data for this chart. If [value] is [Observable], subscribes to |
| /// changes and updates the chart when data changes. |
| @override |
| set data(ChartData value) { |
| _data = value; |
| _dataEventsDisposer.dispose(); |
| _pendingLegendUpdate = true; |
| |
| if (autoUpdate && _data != null && _data is Observable) { |
| _dataEventsDisposer.add((_data as Observable).changes.listen((_) { |
| _pendingLegendUpdate = true; |
| draw(); |
| })); |
| } |
| } |
| |
| @override |
| ChartData get data => _data; |
| |
| /// Set new config for this chart. If [value] is [Observable], subscribes to |
| /// changes and updates the chart when series or dimensions change. |
| @override |
| set config(ChartConfig value) { |
| _config = value; |
| _configEventsDisposer.dispose(); |
| _pendingLegendUpdate = true; |
| _pendingAxisConfigUpdate = true; |
| |
| if (_config != null && _config is Observable) { |
| _configEventsDisposer.add((_config as Observable).changes.listen((_) { |
| _pendingAxisConfigUpdate = true; |
| _pendingLegendUpdate = true; |
| draw(); |
| })); |
| } |
| } |
| |
| @override |
| ChartConfig get config => _config; |
| |
| @override |
| set autoUpdate(bool value) { |
| if (_autoUpdate != value) { |
| _autoUpdate = value; |
| this.data = _data; |
| this.config = _config; |
| } |
| } |
| |
| @override |
| bool get autoUpdate => _autoUpdate; |
| |
| /// Gets measure axis from cache - creates a new instance of _ChartAxis |
| /// if one was not already created for the given [axisId]. |
| DefaultChartAxisImpl _getMeasureAxis(String axisId) { |
| _measureAxes.putIfAbsent(axisId, () { |
| var axisConf = config.getMeasureAxis(axisId), |
| axis = axisConf != null |
| ? new DefaultChartAxisImpl.withAxisConfig(this, axisConf) |
| : new DefaultChartAxisImpl(this); |
| return axis; |
| }); |
| |
| return _measureAxes[axisId]; |
| } |
| |
| /// Gets a dimension axis from cache - creates a new instance of _ChartAxis |
| /// if one was not already created for the given dimension [column]. |
| DefaultChartAxisImpl _getDimensionAxis(int column) { |
| _dimensionAxes.putIfAbsent(column, () { |
| var axisConf = config.getDimensionAxis(column); |
| var axis = axisConf != null |
| ? new DefaultChartAxisImpl.withAxisConfig(this, axisConf) |
| : new DefaultChartAxisImpl(this); |
| return axis; |
| }); |
| return _dimensionAxes[column]; |
| } |
| |
| /// All columns rendered by a series must be of the same type. |
| bool _isSeriesValid(ChartSeries s) { |
| var first = data.columns.elementAt(s.measures.first).type; |
| return s.measures.every((i) => |
| (i < data.columns.length) && data.columns.elementAt(i).type == first); |
| } |
| |
| @override |
| Iterable<Scale> get dimensionScales => |
| config.dimensions.map((int column) => _getDimensionAxis(column).scale); |
| |
| @override |
| Iterable<Scale> measureScales(ChartSeries series) { |
| var axisIds = isNullOrEmpty(series.measureAxisIds) |
| ? MEASURE_AXIS_IDS |
| : series.measureAxisIds; |
| return axisIds.map((String id) => _getMeasureAxis(id).scale); |
| } |
| |
| /// Computes the size of chart and if changed from the previous time |
| /// size was computed, sets attributes on svg element |
| Rect _computeChartSize() { |
| int width = host.clientWidth, height = host.clientHeight; |
| |
| if (config.minimumSize != null) { |
| width = max([width, config.minimumSize.width]).toInt(); |
| height = max([height, config.minimumSize.height]).toInt(); |
| } |
| |
| AbsoluteRect padding = theme.padding; |
| num paddingLeft = config.isRTL ? padding.end : padding.start; |
| Rect current = new Rect( |
| paddingLeft, |
| padding.top, |
| width - (padding.start + padding.end), |
| height - (padding.top + padding.bottom)); |
| if (layout.chartArea == null || layout.chartArea != current) { |
| _svg.attr('width', width.toString()); |
| _svg.attr('height', height.toString()); |
| layout.chartArea = current; |
| |
| var transform = 'translate(${paddingLeft},${padding.top})'; |
| visualization.first.attributes['transform'] = transform; |
| lowerBehaviorPane.first.attributes['transform'] = transform; |
| upperBehaviorPane.first.attributes['transform'] = transform; |
| } |
| return layout.chartArea; |
| } |
| |
| @override |
| draw({bool preRender: false, Future schedulePostRender}) { |
| assert(data != null && config != null); |
| assert(config.series != null && config.series.isNotEmpty); |
| |
| // One time initialization. |
| // Each [ChartArea] has it's own [SelectionScope] |
| if (_scope == null) { |
| _scope = new SelectionScope.element(host); |
| _svg = _scope.append('svg:svg')..classed('chart-canvas'); |
| if (!isNullOrEmpty(theme.filters)) { |
| var element = _svg.first, |
| defs = Namespace.createChildElement('defs', element) |
| ..append(new SvgElement.svg(theme.filters, |
| treeSanitizer: new NullTreeSanitizer())); |
| _svg.first.append(defs); |
| } |
| |
| lowerBehaviorPane = _svg.append('g')..classed('lower-render-pane'); |
| visualization = _svg.append('g')..classed('chart-render-pane'); |
| upperBehaviorPane = _svg.append('g')..classed('upper-render-pane'); |
| |
| if (_behaviors.isNotEmpty) { |
| _behaviors |
| .forEach((b) => b.init(this, upperBehaviorPane, lowerBehaviorPane)); |
| } |
| } |
| |
| // Compute chart sizes and filter out unsupported series |
| _computeChartSize(); |
| var series = config.series |
| .where((s) => _isSeriesValid(s) && s.renderer.prepare(this, s)), |
| selection = visualization |
| .selectAll('.series-group') |
| .data(series, (x) => x.hashCode), |
| axesDomainCompleter = new Completer(); |
| |
| // Wait till the axes are rendered before rendering series. |
| // In an SVG, z-index is based on the order of nodes in the DOM. |
| axesDomainCompleter.future.then((_) { |
| selection.enter.append('svg:g')..classed('series-group'); |
| String transform = |
| 'translate(${layout.renderArea.x},${layout.renderArea.y})'; |
| |
| selection.each((_s, _, Element group) { |
| ChartSeries s = _s; |
| _ChartSeriesInfo info = _seriesInfoCache[s]; |
| if (info == null) { |
| info = _seriesInfoCache[s] = new _ChartSeriesInfo(this, s); |
| } |
| info.check(); |
| group.attributes['transform'] = transform; |
| (s.renderer as CartesianRenderer) |
| ?.draw(group, schedulePostRender: schedulePostRender); |
| }); |
| |
| // A series that was rendered earlier isn't there anymore, remove it |
| selection.exit |
| ..each((_s, _, __) { |
| ChartSeries s = _s; |
| var info = _seriesInfoCache.remove(s); |
| info?.dispose(); |
| }) |
| ..remove(); |
| |
| // Notify on the stream that the chart has been updated. |
| isReady = true; |
| _chartAxesUpdatedController?.add(this); |
| }); |
| |
| // Save the list of valid series and initialize axes. |
| _series = series; |
| _updateAxisConfig(); |
| _initAxes(preRender: preRender); |
| |
| // Render the chart, now that the axes layer is already in DOM. |
| axesDomainCompleter.complete(); |
| |
| // Updates the legend if required. |
| _updateLegend(); |
| } |
| |
| String _orientRTL(String orientation) => orientation; |
| |
| /// Initialize the axes - required even if the axes are not being displayed. |
| _initAxes({bool preRender: false}) { |
| var measureAxisUsers = <String, List<ChartSeries>>{}; |
| var keysToRemove = _measureAxes.keys.toList(); |
| |
| // Create necessary measures axes. |
| // If measure axes were not configured on the series, default is used. |
| _series.forEach((ChartSeries s) { |
| var measureAxisIds = |
| isNullOrEmpty(s.measureAxisIds) ? MEASURE_AXIS_IDS : s.measureAxisIds; |
| measureAxisIds.forEach((axisId) { |
| if (keysToRemove.contains(axisId)) { |
| keysToRemove.remove(axisId); |
| } |
| _getMeasureAxis(axisId); // Creates axis if required |
| var users = measureAxisUsers[axisId]; |
| if (users == null) { |
| measureAxisUsers[axisId] = [s]; |
| } else { |
| users.add(s); |
| } |
| }); |
| }); |
| |
| for (var key in keysToRemove) { |
| _measureAxes.remove(key); |
| } |
| |
| // Now that we know a list of series using each measure axis, configure |
| // the input domain of each axis. |
| measureAxisUsers.forEach((String id, List<ChartSeries> listOfSeries) { |
| var sampleCol = listOfSeries.first.measures.first, |
| sampleColSpec = data.columns.elementAt(sampleCol), |
| axis = _getMeasureAxis(id); |
| List<num> domain; |
| |
| if (sampleColSpec.useOrdinalScale) { |
| throw new UnsupportedError( |
| 'Ordinal measure axes are not currently supported.'); |
| } else { |
| // Extent is available because [ChartRenderer.prepare] was already |
| // called (when checking for valid series in [draw]. |
| var extents = listOfSeries |
| .map((s) => (s.renderer as CartesianRenderer).extent) |
| .toList(); |
| var lowest = min(extents.map((e) => e.min as num)); |
| var highest = max(extents.map((e) => e.max as num)); |
| |
| // Use default domain if lowest and highest are the same, right now |
| // lowest is always 0 unless it is less than 0 - change to lowest when |
| // we make use of it. |
| domain = highest == lowest |
| ? (highest == 0 |
| ? [0, 1] |
| : (highest < 0 ? [highest, 0] : [0, highest])) |
| : (lowest <= 0 ? [lowest, highest] : [0, highest]); |
| } |
| axis.initAxisDomain(sampleCol, false, domain); |
| }); |
| |
| // Configure dimension axes. |
| int dimensionAxesCount = useTwoDimensionAxes ? 2 : 1; |
| config.dimensions.take(dimensionAxesCount).forEach((int column) { |
| var axis = _getDimensionAxis(column); |
| var sampleColumnSpec = data.columns.elementAt(column); |
| |
| Iterable values = data.rows |
| .map((row) => row.elementAt(column)); |
| |
| if (sampleColumnSpec.useOrdinalScale) { |
| List<String> domain = values.map((v) => v.toString()).toList(); |
| axis.initAxisDomain(column, true, domain); |
| } else { |
| var extent = new Extent.items(values.cast<num>()); |
| List<num> domain = [extent.min, extent.max]; |
| axis.initAxisDomain(column, true, domain); |
| } |
| }); |
| |
| // See if any dimensions need "band" on the axis. |
| dimensionsUsingBands.clear(); |
| List<bool> usingBands = [false, false]; |
| _series.forEach((ChartSeries s) => |
| (s.renderer as CartesianRenderer).dimensionsUsingBand.forEach((x) { |
| if (x <= 1 && !(usingBands[x])) { |
| usingBands[x] = true; |
| dimensionsUsingBands.add(config.dimensions.elementAt(x)); |
| } |
| })); |
| |
| // List of measure and dimension axes that are displayed |
| assert(isNullOrEmpty(config.displayedMeasureAxes) || |
| config.displayedMeasureAxes.length < 2); |
| var measureAxesCount = dimensionAxesCount == 1 ? 2 : 0; |
| var displayedMeasureAxes = (isNullOrEmpty(config.displayedMeasureAxes) |
| ? _measureAxes.keys.take(measureAxesCount) |
| : config.displayedMeasureAxes.take(measureAxesCount)) |
| .toList(growable: false); |
| var displayedDimensionAxes = |
| config.dimensions.take(dimensionAxesCount).toList(growable: false); |
| |
| // Compute size of the dimension axes |
| if (config.renderDimensionAxes != false) { |
| var dimensionAxisOrientations = config.isLeftAxisPrimary |
| ? DIMENSION_AXIS_ORIENTATIONS.last |
| : DIMENSION_AXIS_ORIENTATIONS.first; |
| for (int i = 0, len = displayedDimensionAxes.length; i < len; ++i) { |
| var axis = _dimensionAxes[displayedDimensionAxes[i]], |
| orientation = _orientRTL(dimensionAxisOrientations[i]); |
| axis.prepareToDraw(orientation); |
| layout._axes[orientation] = axis.size; |
| } |
| } |
| |
| // Compute size of the measure axes |
| if (displayedMeasureAxes.isNotEmpty) { |
| var measureAxisOrientations = config.isLeftAxisPrimary |
| ? MEASURE_AXIS_ORIENTATIONS.last |
| : MEASURE_AXIS_ORIENTATIONS.first; |
| displayedMeasureAxes.asMap().forEach((int index, String key) { |
| var axis = _measureAxes[key]; |
| var orientation = _orientRTL(measureAxisOrientations[index]); |
| axis.prepareToDraw(orientation); |
| layout._axes[orientation] = axis.size; |
| }); |
| } |
| |
| // Consolidate all the information that we collected into final layout |
| _computeLayout( |
| displayedMeasureAxes.isEmpty && config.renderDimensionAxes == false); |
| |
| // Domains for all axes have been taken care of and _ChartAxis ensures |
| // that the scale is initialized on visible axes. Initialize the scale on |
| // all invisible measure scales. |
| if (_measureAxes.length != displayedMeasureAxes.length) { |
| _measureAxes.keys.forEach((String axisId) { |
| if (displayedMeasureAxes.contains(axisId)) return; |
| _getMeasureAxis(axisId).initAxisScale([layout.renderArea.height, 0]); |
| }); |
| } |
| |
| // Draw the visible measure axes, if any. |
| if (displayedMeasureAxes.isNotEmpty) { |
| var axisGroups = visualization |
| .selectAll('.measure-axis-group') |
| .data(displayedMeasureAxes); |
| // Update measure axis (add/remove/update) |
| axisGroups.enter.append('svg:g'); |
| axisGroups.each((_axisId, index, group) { |
| String axisId = _axisId; |
| _getMeasureAxis(axisId).draw(group, _scope, preRender: preRender); |
| group.attributes['class'] = 'measure-axis-group measure-${index}'; |
| }); |
| axisGroups.exit.remove(); |
| } |
| |
| // Draw the dimension axes, unless asked not to. |
| if (config.renderDimensionAxes != false) { |
| var dimAxisGroups = visualization |
| .selectAll('.dimension-axis-group') |
| .data(displayedDimensionAxes); |
| // Update dimension axes (add/remove/update) |
| dimAxisGroups.enter.append('svg:g'); |
| dimAxisGroups.each((_column, index, group) { |
| int column = _column; |
| _getDimensionAxis(column).draw(group, _scope, preRender: preRender); |
| group.attributes['class'] = 'dimension-axis-group dim-${index}'; |
| }); |
| dimAxisGroups.exit.remove(); |
| } else { |
| // Initialize scale on invisible axis |
| var dimensionAxisOrientations = config.isLeftAxisPrimary |
| ? DIMENSION_AXIS_ORIENTATIONS.last |
| : DIMENSION_AXIS_ORIENTATIONS.first; |
| for (int i = 0; i < dimensionAxesCount; ++i) { |
| var column = config.dimensions.elementAt(i), |
| axis = _dimensionAxes[column], |
| orientation = dimensionAxisOrientations[i]; |
| axis.initAxisScale(orientation == ORIENTATION_LEFT |
| ? [layout.renderArea.height, 0] |
| : [0, layout.renderArea.width]); |
| } |
| } |
| } |
| |
| // Compute chart render area size and positions of all elements |
| _computeLayout(bool notRenderingAxes) { |
| if (notRenderingAxes) { |
| layout.renderArea = |
| new Rect(0, 0, layout.chartArea.height, layout.chartArea.width); |
| return; |
| } |
| |
| var top = layout.axes[ORIENTATION_TOP], |
| left = layout.axes[ORIENTATION_LEFT], |
| bottom = layout.axes[ORIENTATION_BOTTOM], |
| right = layout.axes[ORIENTATION_RIGHT]; |
| |
| var renderAreaHeight = layout.chartArea.height - |
| (top.height + layout.axes[ORIENTATION_BOTTOM].height), |
| renderAreaWidth = layout.chartArea.width - |
| (left.width + layout.axes[ORIENTATION_RIGHT].width); |
| |
| layout.renderArea = |
| new Rect(left.width, top.height, renderAreaWidth, renderAreaHeight); |
| |
| layout._axes |
| ..[ORIENTATION_TOP] = new Rect(left.width, 0, renderAreaWidth, top.height) |
| ..[ORIENTATION_RIGHT] = new Rect( |
| left.width + renderAreaWidth, top.y, right.width, renderAreaHeight) |
| ..[ORIENTATION_BOTTOM] = new Rect(left.width, |
| top.height + renderAreaHeight, renderAreaWidth, bottom.height) |
| ..[ORIENTATION_LEFT] = |
| new Rect(left.width, top.height, left.width, renderAreaHeight); |
| } |
| |
| // Updates the legend, if configuration changed since the last |
| // time the legend was updated. |
| _updateLegend() { |
| if (!_pendingLegendUpdate) return; |
| if (_config == null || _config.legend == null || _series.isEmpty) return; |
| |
| var legend = <ChartLegendItem>[]; |
| List<List<ChartSeries>> seriesByColumn = |
| new List<List<ChartSeries>>.generate( |
| data.columns.length, (_) => <ChartSeries>[]); |
| |
| _series.forEach((s) => s.measures.forEach((m) => seriesByColumn[m].add(s))); |
| |
| seriesByColumn.asMap().forEach((int i, List<ChartSeries> s) { |
| if (s.length == 0) return; |
| legend.add(new ChartLegendItem( |
| index: i, |
| label: data.columns.elementAt(i).label, |
| series: s, |
| color: theme.getColorForKey(i))); |
| }); |
| |
| _config.legend.update(legend, this); |
| _pendingLegendUpdate = false; |
| } |
| |
| // Updates the AxisConfig, if configuration chagned since the last time the |
| // AxisConfig was updated. |
| void _updateAxisConfig() { |
| if (!_pendingAxisConfigUpdate) return; |
| _series.forEach((ChartSeries s) { |
| var measureAxisIds = |
| isNullOrEmpty(s.measureAxisIds) ? MEASURE_AXIS_IDS : s.measureAxisIds; |
| measureAxisIds.forEach((axisId) { |
| var axis = _getMeasureAxis(axisId); // Creates axis if required |
| axis.config = config.getMeasureAxis(axisId); |
| }); |
| }); |
| |
| int dimensionAxesCount = useTwoDimensionAxes ? 2 : 1; |
| config.dimensions.take(dimensionAxesCount).forEach((int column) { |
| var axis = _getDimensionAxis(column); |
| axis.config = config.getDimensionAxis(column); |
| }); |
| |
| _pendingAxisConfigUpdate = false; |
| } |
| |
| @override |
| Stream<ChartEvent> get onMouseUp => |
| host.onMouseUp.map((MouseEvent e) => new DefaultChartEventImpl(e, this)); |
| |
| @override |
| Stream<ChartEvent> get onMouseDown => host.onMouseDown |
| .map((MouseEvent e) => new DefaultChartEventImpl(e, this)); |
| |
| @override |
| Stream<ChartEvent> get onMouseOver => host.onMouseOver |
| .map((MouseEvent e) => new DefaultChartEventImpl(e, this)); |
| |
| @override |
| Stream<ChartEvent> get onMouseOut => |
| host.onMouseOut.map((MouseEvent e) => new DefaultChartEventImpl(e, this)); |
| |
| @override |
| Stream<ChartEvent> get onMouseMove => host.onMouseMove |
| .map((MouseEvent e) => new DefaultChartEventImpl(e, this)); |
| |
| @override |
| Stream<ChartEvent> get onValueClick { |
| if (_valueMouseClickController == null) { |
| _valueMouseClickController = new StreamController.broadcast(sync: true); |
| } |
| return _valueMouseClickController.stream; |
| } |
| |
| @override |
| Stream<ChartEvent> get onValueMouseOver { |
| if (_valueMouseOverController == null) { |
| _valueMouseOverController = new StreamController.broadcast(sync: true); |
| } |
| return _valueMouseOverController.stream; |
| } |
| |
| @override |
| Stream<ChartEvent> get onValueMouseOut { |
| if (_valueMouseOutController == null) { |
| _valueMouseOutController = new StreamController.broadcast(sync: true); |
| } |
| return _valueMouseOutController.stream; |
| } |
| |
| @override |
| Stream<ChartArea> get onChartAxesUpdated { |
| if (_chartAxesUpdatedController == null) { |
| _chartAxesUpdatedController = new StreamController.broadcast(sync: true); |
| } |
| return _chartAxesUpdatedController.stream; |
| } |
| |
| @override |
| void addChartBehavior(ChartBehavior behavior) { |
| if (behavior == null || _behaviors.contains(behavior)) return; |
| _behaviors.add(behavior); |
| if (upperBehaviorPane != null && lowerBehaviorPane != null) { |
| behavior.init(this, upperBehaviorPane, lowerBehaviorPane); |
| } |
| } |
| |
| @override |
| void removeChartBehavior(ChartBehavior behavior) { |
| if (behavior == null || !_behaviors.contains(behavior)) return; |
| if (upperBehaviorPane != null && lowerBehaviorPane != null) { |
| behavior.dispose(); |
| } |
| _behaviors.remove(behavior); |
| } |
| } |
| |
| class _ChartAreaLayout implements ChartAreaLayout { |
| final _axes = <String, Rect>{ |
| ORIENTATION_LEFT: const Rect(), |
| ORIENTATION_RIGHT: const Rect(), |
| ORIENTATION_TOP: const Rect(), |
| ORIENTATION_BOTTOM: const Rect() |
| }; |
| |
| UnmodifiableMapView<String, Rect> _axesView; |
| |
| @override |
| get axes => _axesView; |
| |
| @override |
| Rect renderArea = const Rect(); |
| |
| @override |
| Rect chartArea = const Rect(); |
| |
| _ChartAreaLayout() { |
| _axesView = new UnmodifiableMapView(_axes); |
| } |
| } |
| |
| class _ChartSeriesInfo { |
| CartesianRenderer _renderer; |
| SubscriptionsDisposer _disposer = new SubscriptionsDisposer(); |
| |
| ChartSeries _series; |
| DefaultCartesianAreaImpl _area; |
| _ChartSeriesInfo(this._area, this._series); |
| |
| _click(ChartEvent e) { |
| var state = _area.state; |
| if (state != null) { |
| if (state.isHighlighted(e.column, e.row)) { |
| state.unhighlight(e.column, e.row); |
| } else { |
| state.highlight(e.column, e.row); |
| } |
| } |
| if (_area._valueMouseClickController != null) { |
| _area._valueMouseClickController.add(e); |
| } |
| } |
| |
| _mouseOver(ChartEvent e) { |
| var state = _area.state; |
| if (state != null) { |
| state.hovered = new Pair(e.column, e.row); |
| } |
| if (_area._valueMouseOverController != null) { |
| _area._valueMouseOverController.add(e); |
| } |
| } |
| |
| _mouseOut(ChartEvent e) { |
| var state = _area.state; |
| if (state != null) { |
| var current = state.hovered; |
| if (current != null && |
| current.first == e.column && |
| current.last == e.row) { |
| state.hovered = null; |
| } |
| } |
| if (_area._valueMouseOutController != null) { |
| _area._valueMouseOutController.add(e); |
| } |
| } |
| |
| check() { |
| if (_renderer != _series.renderer) { |
| dispose(); |
| if (_series.renderer is ChartRendererBehaviorSource) { |
| _disposer.addAll([ |
| _series.renderer.onValueClick.listen(_click), |
| _series.renderer.onValueMouseOver.listen(_mouseOver), |
| _series.renderer.onValueMouseOut.listen(_mouseOut) |
| ]); |
| } |
| } |
| _renderer = _series.renderer as CartesianRenderer; |
| } |
| |
| dispose() { |
| _renderer?.dispose(); |
| _disposer.dispose(); |
| } |
| } |