blob: d4bbaefcff075a2d91d214f2a5645ad6aaccfb7b [file] [log] [blame]
//
// 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;
/// Creates an empty area and provides generic API for interaction with layout
/// based charts.
class DefaultLayoutAreaImpl implements LayoutArea {
/// 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 useRowColoring = true;
@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;
ChartSeries _series;
LayoutRenderer _renderer;
bool _pendingLegendUpdate = false;
List<ChartBehavior> _behaviors = new List<ChartBehavior>();
SubscriptionsDisposer _rendererDisposer = new SubscriptionsDisposer();
StreamController<ChartEvent> _valueMouseOverController;
StreamController<ChartEvent> _valueMouseOutController;
StreamController<ChartEvent> _valueMouseClickController;
DefaultLayoutAreaImpl(this.host, ChartData data, ChartConfig config,
this._autoUpdate, this.state) {
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();
}
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();
if (autoUpdate && _data != null && _data is Observable) {
_dataEventsDisposer.add((_data as Observable).changes.listen((_) {
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;
if (_config != null && _config is Observable) {
_configEventsDisposer.add((_config as Observable).changes.listen((_) {
_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;
/// 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]);
height = max([height, config.minimumSize.height]);
}
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) {
var transform = 'translate(${paddingLeft},${padding.top})';
visualization.first.attributes['transform'] = transform;
lowerBehaviorPane.first.attributes['transform'] = transform;
upperBehaviorPane.first.attributes['transform'] = transform;
_svg.attr('width', width.toString());
_svg.attr('height', height.toString());
layout.chartArea = current;
layout.renderArea = current;
layout._axes.clear();
}
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');
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
.firstWhere((s) => s.renderer.prepare(this, s), orElse: () => null),
group = visualization.first.querySelector('.series-group');
// We need atleast one matching series.
assert(series != null);
// Create a group for rendering, if it was not already done.
if (group == null) {
group = Namespace.createChildElement('g', visualization.first)
..classes.add('series-group');
visualization.first.append(group);
}
// If we previously displayed a series, verify that we are
// still using the same renderer. Otherwise, dispose the older one.
if (_renderer != null && series.renderer != _renderer) {
_rendererDisposer.dispose();
}
// Save and subscribe to events on the the current renderer.
_renderer = series.renderer;
if (_renderer is ChartRendererBehaviorSource) {
_rendererDisposer.addAll([
_renderer.onValueClick.listen((ChartEvent e) {
if (state != null) {
if (state.isSelected(e.row)) {
state.unselect(e.row);
} else {
state.select(e.row);
}
}
if (_valueMouseClickController != null) {
_valueMouseClickController.add(e);
}
}),
_renderer.onValueMouseOver.listen((ChartEvent e) {
if (state != null) {
state.preview = e.row;
}
if (_valueMouseOverController != null) {
_valueMouseOverController.add(e);
}
}),
_renderer.onValueMouseOut.listen((ChartEvent e) {
if (state != null) {
if (e.row == state.preview) {
state.hovered = null;
}
}
if (_valueMouseOutController != null) {
_valueMouseOutController.add(e);
}
})
]);
}
Iterable<ChartLegendItem> legend =
_renderer.layout(group, schedulePostRender: schedulePostRender);
// Notify on the stream that the chart has been updated.
isReady = true;
// Save the list of valid series and initialize axes.
_series = series;
// Updates the legend if required.
if (_config.legend != null) {
_config.legend.update(legend, this);
}
}
@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
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);
}
}