| Index: packages/charted/lib/charts/cartesian_renderers/stackedline_chart_renderer.dart
|
| diff --git a/packages/charted/lib/charts/cartesian_renderers/line_chart_renderer.dart b/packages/charted/lib/charts/cartesian_renderers/stackedline_chart_renderer.dart
|
| similarity index 62%
|
| copy from packages/charted/lib/charts/cartesian_renderers/line_chart_renderer.dart
|
| copy to packages/charted/lib/charts/cartesian_renderers/stackedline_chart_renderer.dart
|
| index 4806d2d8c45c52ef70795607bf0a4c83c8c2a6c7..5bac934d0867855060bb39c245331eeb6bbc77c5 100644
|
| --- a/packages/charted/lib/charts/cartesian_renderers/line_chart_renderer.dart
|
| +++ b/packages/charted/lib/charts/cartesian_renderers/stackedline_chart_renderer.dart
|
| @@ -1,5 +1,4 @@
|
| -//
|
| -// Copyright 2014 Google Inc. All rights reserved.
|
| +// Copyright 2017 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
|
| @@ -8,10 +7,11 @@
|
|
|
| part of charted.charts;
|
|
|
| -class LineChartRenderer extends CartesianRendererBase {
|
| +class StackedLineChartRenderer extends CartesianRendererBase {
|
| final Iterable<int> dimensionsUsingBand = const [];
|
|
|
| final bool alwaysAnimate;
|
| + final bool showHoverCardOnTrackedDataPoints;
|
| final bool trackDataPoints;
|
| final bool trackOnDimensionAxis;
|
| final int quantitativeScaleProximity;
|
| @@ -26,10 +26,11 @@ class LineChartRenderer extends CartesianRendererBase {
|
| int currentDataIndex = -1;
|
|
|
| @override
|
| - final String name = "line-rdr";
|
| + final String name = "stacked-line-rdr";
|
|
|
| - LineChartRenderer(
|
| + StackedLineChartRenderer(
|
| {this.alwaysAnimate: false,
|
| + this.showHoverCardOnTrackedDataPoints: false,
|
| this.trackDataPoints: true,
|
| this.trackOnDimensionAxis: false,
|
| this.quantitativeScaleProximity: 5});
|
| @@ -39,7 +40,7 @@ class LineChartRenderer extends CartesianRendererBase {
|
| @override
|
| bool prepare(ChartArea area, ChartSeries series) {
|
| _ensureAreaAndSeries(area, series);
|
| - if (trackDataPoints != false) {
|
| + if (trackDataPoints) {
|
| _trackPointerInArea();
|
| }
|
| return area is CartesianArea;
|
| @@ -52,17 +53,24 @@ class LineChartRenderer extends CartesianRendererBase {
|
| var measureScale = area.measureScales(series).first,
|
| dimensionScale = area.dimensionScales.first;
|
|
|
| - // Create lists of values in measure columns.
|
| - var lines = series.measures.map((column) {
|
| - return area.data.rows.map((values) => values[column]).toList();
|
| - }).toList();
|
| -
|
| // We only support one dimension axes, so we always use the
|
| // first dimension.
|
| var x = area.data.rows
|
| .map((row) => row.elementAt(area.config.dimensions.first))
|
| .toList();
|
|
|
| + var accumulated = new List.filled(x.length, 0.0);
|
| +
|
| + var reversedMeasures = series.measures.toList().reversed.toList();
|
| + // Create lists of values used for drawing.
|
| + // First Half: previous values reversed (need for drawing)
|
| + // Second Half: current accumulated values (need for drawing)
|
| + var lines = reversedMeasures.map((column) {
|
| + var row = area.data.rows.map((values) => values[column]).toList();
|
| + return accumulated.reversed.toList()..addAll(
|
| + new List.generate(x.length, (i) => accumulated[i] += row[i]));
|
| + }).toList();
|
| +
|
| var rangeBandOffset =
|
| dimensionScale is OrdinalScale ? dimensionScale.rangeBand / 2 : 0;
|
|
|
| @@ -73,28 +81,47 @@ class LineChartRenderer extends CartesianRendererBase {
|
| x.map((val) => dimensionScale.scale(val) + rangeBandOffset).toList();
|
| }
|
|
|
| - var line = new SvgLine(
|
| + var fillLine = new SvgLine(
|
| + xValueAccessor: (d, i) {
|
| + // The first x.length values are the bottom part of the path that
|
| + // should be drawn backword. The second part is the accumulated values
|
| + // that should be drawn forward.
|
| + var xval = i < x.length ? x[x.length - i - 1] : x[i - x.length];
|
| + return dimensionScale.scale(xval) + rangeBandOffset;
|
| + },
|
| + yValueAccessor: (d, i) => measureScale.scale(d));
|
| + var strokeLine = new SvgLine(
|
| xValueAccessor: (d, i) => dimensionScale.scale(x[i]) + rangeBandOffset,
|
| yValueAccessor: (d, i) => measureScale.scale(d));
|
|
|
| // Add lines and hook up hover and selection events.
|
| - var svgLines = root.selectAll('.line-rdr-line').data(lines);
|
| - svgLines.enter.append('path').each((d, i, e) {
|
| - e.attributes['fill'] = 'none';
|
| - });
|
| + var svgLines = root.selectAll('.stacked-line-rdr-line').data(lines.reversed);
|
| + svgLines.enter.append('g');
|
|
|
| svgLines.each((d, i, e) {
|
| var column = series.measures.elementAt(i),
|
| color = colorForColumn(column),
|
| filter = filterForColumn(column),
|
| - styles = stylesForColumn(column);
|
| + styles = stylesForColumn(column),
|
| + fill = new SvgElement.tag('path'),
|
| + stroke = new SvgElement.tag('path'),
|
| + fillData = d,
|
| + // Second half contains the accumulated data for this measure
|
| + strokeData = d.sublist(x.length, d.length);
|
| e.attributes
|
| - ..['d'] = line.path(d, i, e)
|
| ..['stroke'] = color
|
| + ..['fill'] = color
|
| ..['class'] = styles.isEmpty
|
| - ? 'line-rdr-line'
|
| - : 'line-rdr-line ${styles.join(' ')}'
|
| + ? 'stacked-line-rdr-line'
|
| + : 'stacked-line-rdr-line ${styles.join(' ')}'
|
| ..['data-column'] = '$column';
|
| + fill.attributes
|
| + ..['d'] = fillLine.path(fillData, i, e)
|
| + ..['stroke'] = 'none';
|
| + stroke.attributes
|
| + ..['d'] = strokeLine.path(strokeData, i, e)
|
| + ..['fill'] = 'none';
|
| + e.children = [fill, stroke];
|
| if (isNullOrEmpty(filter)) {
|
| e.attributes.remove('filter');
|
| } else {
|
| @@ -114,15 +141,40 @@ class LineChartRenderer extends CartesianRendererBase {
|
|
|
| @override
|
| void dispose() {
|
| - if (root == null) return;
|
| - root.selectAll('.line-rdr-line').remove();
|
| - root.selectAll('.line-rdr-point').remove();
|
| _disposer.dispose();
|
| + if (root == null) return;
|
| + root.selectAll('.stacked-line-rdr-line').remove();
|
| + root.selectAll('.stacked-line-rdr-point').remove();
|
| + }
|
| +
|
| + @override
|
| + Extent get extent {
|
| + assert(area != null && series != null);
|
| + var rows = area.data.rows,
|
| + max = SMALL_INT_MIN,
|
| + min = SMALL_INT_MAX,
|
| + rowIndex = 0;
|
| +
|
| + rows.forEach((row) {
|
| + var line = null;
|
| + series.measures.forEach((idx) {
|
| + var value = row.elementAt(idx);
|
| + if (value != null && value.isFinite) {
|
| + if (line == null) line = 0.0;
|
| + line += value;
|
| + }
|
| + });
|
| + if (line > max) max = line;
|
| + if (line < min) min = line;
|
| + rowIndex++;
|
| + });
|
| +
|
| + return new Extent(min, max);
|
| }
|
|
|
| @override
|
| void handleStateChanges(List<ChangeRecord> changes) {
|
| - var lines = host.querySelectorAll('.line-rdr-line');
|
| + var lines = host.querySelectorAll('.stacked-line-rdr-line');
|
| if (lines == null || lines.isEmpty) return;
|
|
|
| for (int i = 0, len = lines.length; i < len; ++i) {
|
| @@ -132,6 +184,7 @@ class LineChartRenderer extends CartesianRendererBase {
|
| line.classes.removeAll(ChartState.COLUMN_CLASS_NAMES);
|
| line.classes.addAll(stylesForColumn(column));
|
| line.attributes['stroke'] = colorForColumn(column);
|
| + line.attributes['fill'] = colorForColumn(column);
|
|
|
| if (isNullOrEmpty(filter)) {
|
| line.attributes.remove('filter');
|
| @@ -142,9 +195,10 @@ class LineChartRenderer extends CartesianRendererBase {
|
| }
|
|
|
| void _createTrackingCircles() {
|
| - var linePoints = root.selectAll('.line-rdr-point').data(series.measures);
|
| + var linePoints = root.selectAll('.stacked-line-rdr-point')
|
| + .data(series.measures.toList().reversed);
|
| linePoints.enter.append('circle').each((d, i, e) {
|
| - e.classes.add('line-rdr-point');
|
| + e.classes.add('stacked-line-rdr-point');
|
| e.attributes['r'] = '4';
|
| });
|
|
|
| @@ -166,15 +220,16 @@ class LineChartRenderer extends CartesianRendererBase {
|
| _trackingPointsCreated = true;
|
| }
|
|
|
| - void _showTrackingCircles(int row) {
|
| + void _showTrackingCircles(ChartEvent event, int row) {
|
| if (_trackingPointsCreated == false) {
|
| _createTrackingCircles();
|
| }
|
|
|
| + double cumulated = 0.0;
|
| var yScale = area.measureScales(series).first;
|
| - root.selectAll('.line-rdr-point').each((d, i, e) {
|
| + root.selectAll('.stacked-line-rdr-point').each((d, i, e) {
|
| var x = _xPositions[row],
|
| - measureVal = area.data.rows.elementAt(row).elementAt(d);
|
| + measureVal = cumulated += area.data.rows.elementAt(row).elementAt(d);
|
| if (measureVal != null && measureVal.isFinite) {
|
| var color = colorForColumn(d), filter = filterForColumn(d);
|
| e.attributes
|
| @@ -197,16 +252,28 @@ class LineChartRenderer extends CartesianRendererBase {
|
| ..setProperty('visibility', 'hidden');
|
| }
|
| });
|
| +
|
| + if (showHoverCardOnTrackedDataPoints) {
|
| + var firstMeasureColumn = series.measures.first;
|
| + mouseOverController.add(new DefaultChartEventImpl(
|
| + event.source, area, series, row, firstMeasureColumn, 0));
|
| + _savedOverRow = row;
|
| + _savedOverColumn = firstMeasureColumn;
|
| + }
|
| }
|
|
|
| - void _hideTrackingCircles() {
|
| - root.selectAll('.line-rdr-point')
|
| + void _hideTrackingCircles(ChartEvent event) {
|
| + root.selectAll('.stacked-line-rdr-point')
|
| ..style('opacity', '0.0')
|
| ..style('visibility', 'hidden');
|
| + if (showHoverCardOnTrackedDataPoints) {
|
| + mouseOutController.add(new DefaultChartEventImpl(
|
| + event.source, area, series, _savedOverRow, _savedOverColumn, 0));
|
| + }
|
| }
|
|
|
| int _getNearestRowIndex(num x) {
|
| - var lastSmallerValue = 0;
|
| + double lastSmallerValue = 0.0;
|
| var chartX = x - area.layout.renderArea.x;
|
| for (var i = 0; i < _xPositions.length; i++) {
|
| var pos = _xPositions[i];
|
| @@ -226,19 +293,24 @@ class LineChartRenderer extends CartesianRendererBase {
|
| _disposer.add(area.onMouseMove.listen((ChartEvent event) {
|
| if (area.layout.renderArea.contains(event.chartX, event.chartY)) {
|
| var row = _getNearestRowIndex(event.chartX);
|
| - window.animationFrame.then((_) => _showTrackingCircles(row));
|
| + window.animationFrame.then((_) {
|
| + _showTrackingCircles(event, row);
|
| + });
|
| } else {
|
| - _hideTrackingCircles();
|
| + _hideTrackingCircles(event);
|
| }
|
| }));
|
| _disposer.add(area.onMouseOut.listen((ChartEvent event) {
|
| - _hideTrackingCircles();
|
| + _hideTrackingCircles(event);
|
| }));
|
| }
|
|
|
| void _mouseClickHandler(d, int i, Element e) {
|
| if (area.state != null) {
|
| - area.state.select(int.parse(e.dataset['column']));
|
| + var selectedColumn = int.parse(e.dataset['column']);
|
| + area.state.isSelected(selectedColumn)
|
| + ? area.state.unselect(selectedColumn)
|
| + : area.state.select(selectedColumn);
|
| }
|
| if (mouseClickController != null && e.tagName == 'circle') {
|
| var row = int.parse(e.dataset['row']),
|
|
|