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']), |