blob: f411ae21bee6234825046629b0953e256c7aebd4 [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;
class BarChartRenderer extends CartesianRendererBase {
static const RADIUS = 2;
final Iterable<int> dimensionsUsingBand = const [0];
final bool alwaysAnimate;
@override
final String name = "bar-rdr";
BarChartRenderer({this.alwaysAnimate: false});
/// Returns false if the number of dimension axes on the area is 0.
/// Otherwise, the first dimension scale is used to render the chart.
@override
bool prepare(ChartArea area, ChartSeries series) {
_ensureAreaAndSeries(area as CartesianArea, series);
return area is CartesianArea;
}
@override
void draw(Element element, {Future schedulePostRender}) {
_ensureReadyToDraw(element);
var verticalBars = !area.config.isLeftAxisPrimary;
var measuresCount = series.measures.length,
measureScale = area.measureScales(series).first,
dimensionScale = area.dimensionScales.first;
var rows = new List<List>()
..addAll(area.data.rows.map((e) => new List.generate(
measuresCount, (i) => e[series.measures.elementAt(i)])));
var dimensionVals = area.data.rows
.map((row) => row.elementAt(area.config.dimensions.first))
.toList();
var bars = new OrdinalScale()
..domain = new Range(series.measures.length).toList()
..rangeRoundBands([0, (dimensionScale as OrdinalScale).rangeBand]);
// Create and update the bar groups.
var groups = root.selectAll('.bar-rdr-rowgroup').data(rows);
var animateBarGroups = alwaysAnimate || !groups.isEmpty;
groups.enter.append('g')
..classed('bar-rdr-rowgroup')
..attrWithCallback(
'transform',
(d, i, c) => verticalBars
? 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)'
: 'translate(0, ${dimensionScale.scale(dimensionVals[i])})');
groups.attrWithCallback('data-row', (d, i, e) => i);
groups.exit.remove();
if (animateBarGroups) {
groups.transition()
..attrWithCallback(
'transform',
(d, i, c) => verticalBars
? 'translate(${dimensionScale.scale(dimensionVals[i])}, 0)'
: 'translate(0, ${dimensionScale.scale(dimensionVals[i])})')
..duration(theme.transitionDurationMilliseconds);
}
// TODO: Test interactions between stroke width and bar width.
var barWidth = bars.rangeBand.abs() -
theme.defaultSeparatorWidth -
theme.defaultStrokeWidth,
strokeWidth = theme.defaultStrokeWidth,
strokeWidthOffset = strokeWidth ~/ 2;
// Create and update the bars
// Avoids animation on first render unless alwaysAnimate is set to true.
var bar = groups
.selectAll('.bar-rdr-bar')
.dataWithCallback((d, i, c) => rows[i]),
scaled0 = measureScale.scale(0).round();
var getBarLength = (num d) {
var scaledVal = measureScale.scale(d).round();
num ht = verticalBars
? (d >= 0 ? scaled0 - scaledVal : scaledVal - scaled0)
: (d >= 0 ? scaledVal - scaled0 : scaled0 - scaledVal);
ht = ht - strokeWidth;
// If bar would be scaled to 0 height but data is not 0, render bar
// at 1 pixel so user can see and hover over to see the data.
return (ht < 0) ? 1 : ht;
};
var getBarPos = (num d) {
var scaledVal = measureScale.scale(d).round();
// If bar would be scaled to 0 height but data is not 0, reserve 1 pixel
// height plus strokeWidthOffset to position the bar.
if (scaledVal == scaled0) {
return verticalBars
? d > 0
? scaled0 - 1 - strokeWidthOffset
: scaled0 + strokeWidthOffset
: d > 0
? scaled0 + strokeWidthOffset
: scaled0 - 1 - strokeWidthOffset;
}
return verticalBars
? (d >= 0 ? scaledVal : scaled0) + strokeWidthOffset
: (d >= 0 ? scaled0 : scaledVal) + strokeWidthOffset;
};
var buildPath = (num d, int i, bool animate) {
// If data is null or 0, an empty path for the bar is returned directly.
if (d == null || d == 0) return '';
if (verticalBars) {
var fn = d > 0 ? topRoundedRect : bottomRoundedRect;
return fn(
(bars.scale(i) as num).toInt() + strokeWidthOffset,
animate ? rect.height as int : getBarPos(d) as int,
barWidth as int,
animate ? 0 : getBarLength(d) as int,
RADIUS);
} else {
var fn = d > 0 ? rightRoundedRect : leftRoundedRect;
return fn(
getBarPos(d) as int,
(bars.scale(i) as num).toInt() + strokeWidthOffset,
animate ? 0 : getBarLength(d) as int,
barWidth as int,
RADIUS);
}
};
bar.enter.appendWithCallback((_d, i, e) {
num d = _d;
var rect = Namespace.createChildElement('path', e),
measure = series.measures.elementAt(i),
row = int.parse(e.dataset['row']),
color = colorForValue(measure, row),
filter = filterForValue(measure, row),
style = stylesForValue(measure, row);
if (!isNullOrEmpty(style)) {
rect.classes.addAll(style);
}
rect.classes.add('bar-rdr-bar');
rect.attributes
..['d'] = buildPath(d, i, animateBarGroups)
..['stroke-width'] = '${strokeWidth}px'
..['fill'] = color
..['stroke'] = color;
if (!isNullOrEmpty(filter)) {
rect.attributes['filter'] = filter;
}
if (!animateBarGroups) {
rect.attributes['data-column'] = '$measure';
}
return rect;
})
..on('click', (d, i, e) => _event(mouseClickController, d as num, i, e))
..on(
'mouseover', (d, i, e) => _event(mouseOverController, d as num, i, e))
..on('mouseout', (d, i, e) => _event(mouseOutController, d as num, i, e));
if (animateBarGroups) {
bar.each((d, i, e) {
var measure = series.measures.elementAt(i),
row = int.parse(e.parent.dataset['row']),
color = colorForValue(measure, row),
filter = filterForValue(measure, row),
styles = stylesForValue(measure, row);
e.attributes
..['data-column'] = '$measure'
..['fill'] = color
..['stroke'] = color;
e.classes
..removeAll(ChartState.VALUE_CLASS_NAMES)
..addAll(styles);
if (isNullOrEmpty(filter)) {
e.attributes.remove('filter');
} else {
e.attributes['filter'] = filter;
}
});
bar.transition()
..attrWithCallback('d', (d, i, e) => buildPath(d as num, i, false));
}
bar.exit.remove();
}
@override
void dispose() {
if (root == null) return;
root.selectAll('.bar-rdr-rowgroup').remove();
}
@override
double get bandInnerPadding {
assert(series != null && area != null);
var measuresCount = series.measures.length;
return measuresCount > 2
? 1 - (measuresCount / (measuresCount + 1))
: area.theme.getDimensionAxisTheme().axisBandInnerPadding;
}
@override
double get bandOuterPadding {
assert(series != null && area != null);
return area.theme.getDimensionAxisTheme().axisBandOuterPadding;
}
@override
void handleStateChanges(List<ChangeRecord> changes) {
var groups = host.querySelectorAll('.bar-rdr-rowgroup');
if (groups == null || groups.isEmpty) return;
for (int i = 0, len = groups.length; i < len; ++i) {
var group = groups.elementAt(i),
bars = group.querySelectorAll('.bar-rdr-bar'),
row = int.parse(group.dataset['row']);
for (int j = 0, barsCount = bars.length; j < barsCount; ++j) {
var bar = bars.elementAt(j),
column = int.parse(bar.dataset['column']),
color = colorForValue(column, row),
filter = filterForValue(column, row);
bar.classes.removeAll(ChartState.VALUE_CLASS_NAMES);
bar.classes.addAll(stylesForValue(column, row));
bar.attributes
..['fill'] = color
..['stroke'] = color;
if (isNullOrEmpty(filter)) {
bar.attributes.remove('filter');
} else {
bar.attributes['filter'] = filter;
}
}
}
}
void _event(StreamController controller, num data, int index, Element e) {
if (controller == null) return;
var rowStr = e.parent.dataset['row'];
var row = rowStr != null ? int.parse(rowStr) : null;
controller.add(new DefaultChartEventImpl(scope.event, area, series, row,
series.measures.elementAt(index), data));
}
}