blob: d86b01dc247e51a5a5ab99338a35531bb07a77dc [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.svg;
typedef String Formatter(x);
/**
* [SvgAxis] helps draw chart axes based on a given scale.
*/
class SvgAxis {
/** Scale used on this axis */
Scale scale = new LinearScale();
/** Orientation of the axis. Defaults to [ORIENTATION_BOTTOM] */
String orientation = ORIENTATION_BOTTOM;
/** Size of all inner ticks */
num innerTickSize = 6;
/** Size of the outer two ticks */
num outerTickSize = 6;
/** Padding on the ticks */
num tickPadding = 3;
/** Suggested number of ticks to be displayed on the axis */
num suggestedTickCount = 10;
/** List of values to be used on the ticks */
List tickValues;
/** Formatter for the tick labels */
Formatter tickFormat;
/** Previous rotate angle */
num _prevRotate = 0;
/* Store of axis roots mapped to currently used scales */
static Expando<Scale> _scales = new Expando<Scale>();
axis(Selection g) => g.each((d, i, e) => _create(e, g.scope));
_create(Element e, SelectionScope scope) {
var group = scope.selectElements([e]),
older = _scales[e],
current = _scales[e] = scale.copy();
if (older == null) older = scale;
var tickFormat = this.tickFormat == null ?
current.tickFormat(suggestedTickCount) : this.tickFormat,
tickValues = this.tickValues == null ?
current.ticks(suggestedTickCount) : this.tickValues;
var ticks = group.selectAll('.tick').data(tickValues, current.apply),
tickEnter = ticks.enter.insert('g', before:'.domain')
..classed('tick')
..style('opacity', EPSILON.toString()),
tickExit = ticks.exit..remove(),
tickUpdate = ticks..style('opacity', '1'),
tickTransform;
var range = current.rangeExtent(),
path = group.selectAll('.domain').data([0]);
path.enter.append('path');
path.attr('class', 'domain');
num axisLength = range[1] - range[0];
tickEnter.append('line');
tickEnter.append('text');
var lineEnter = tickEnter.select('line'),
lineUpdate = tickUpdate.select('line'),
textEnter = tickEnter.select('text'),
textUpdate = tickUpdate.select('text'),
text = ticks.select('text')
..textWithCallback((d,i,e) => tickFormat(d));
var ellipsis = group.selectAll('.ellipsis').data(["... ..."]);
ellipsis.enter.append('text')
..attr('class', 'ellipsis')
..style('text-anchor', 'middle')
..textWithCallback((d, i, e) => d)
..attr('opacity', 0);
switch (orientation) {
case ORIENTATION_BOTTOM: {
tickTransform = _xAxisTransform;
ticks.attr('y2', innerTickSize);
lineEnter.attr('y2', innerTickSize);
textEnter.attr('y', math.max(innerTickSize, 0) + tickPadding);
lineUpdate
..attr('x2', 0)
..attr('y2', innerTickSize);
textUpdate
..attr('x', 0)
..attr('y', math.max(innerTickSize, 0) + tickPadding);
textEnter
..attr('dy', '.71em')
..style('text-anchor', 'middle');
ellipsis
..attr('x', axisLength / 2)
..attr('y', '.71em');
path.attr('d',
'M${range[0]},${outerTickSize}V0H${range[1]}V${outerTickSize}');
}
break;
case ORIENTATION_TOP: {
tickTransform = _xAxisTransform;
lineEnter.attr('y2', -innerTickSize);
textEnter.attr('y', -(math.max(innerTickSize, 0) + tickPadding));
lineUpdate
..attr('x2', 0)
..attr('y2', -innerTickSize);
textUpdate
..attr('x', 0)
..attr('y', -(math.max(innerTickSize, 0) + tickPadding));
textEnter
..attr('dy', '0em')
..style('text-anchor', 'middle');
ellipsis
..attr('x', axisLength / 2)
..attr('y', 0);
path.attr('d',
'M${range[0]},${-outerTickSize}V0H${range[1]}V${-outerTickSize}');
}
break;
case ORIENTATION_LEFT: {
tickTransform = _yAxisTransform;
lineEnter.attr('x2', -innerTickSize);
textEnter.attr('x', -(math.max(innerTickSize, 0) + tickPadding));
lineUpdate
..attr('x2', -innerTickSize)
..attr('y2', 0);
textUpdate
..attr('x', -(math.max(innerTickSize, 0) + tickPadding))
..attr('y', 0);
textEnter
..attr('dy', '.32em')
..style('text-anchor', 'end');
ellipsis
..attr("transform",
"translate(${-tickPadding}, ${axisLength / 2})rotate(90)");
path.attr('d',
'M${-outerTickSize},${range[0]}H0V${range[1]}H${-outerTickSize}');
}
break;
case ORIENTATION_RIGHT: {
tickTransform = _yAxisTransform;
lineEnter.attr('x2', innerTickSize);
textEnter.attr('x', math.max(innerTickSize, 0) + tickPadding);
lineUpdate
..attr('x2', innerTickSize)
..attr('y2', 0);
textUpdate
..attr('x', math.max(innerTickSize, 0) + tickPadding)
..attr('y', 0);
textEnter
..attr('dy', '.32em')
..style('text-anchor', 'start');
ellipsis
..attr("transform",
"translate(${tickPadding}, ${axisLength / 2})rotate(90)");
path.attr('d',
'M${outerTickSize},${range[0]}H0V${range[1]}H${outerTickSize}');
}
break;
}
num currentRotate = 0;
num prevRotate = _prevRotate;
lineEnter..attr('opacity', '0');
textEnter..attr('opacity', '0');
lineEnter.transition()
..attr('opacity', '1')
..delay(100);
List textOpacity = new List.filled(text.length, 1);
if (orientation == ORIENTATION_BOTTOM || orientation == ORIENTATION_TOP) {
num maxTextWidth = 0,
textHeight = text.first.clientHeight;
text.each((d, i, e)
=> maxTextWidth = math.max(maxTextWidth, e.clientWidth));
if (maxTextWidth > axisLength / text.length) currentRotate = 45;
if (textHeight * 1.5 > axisLength / text.length) {
currentRotate = 90;
}
_prevRotate = currentRotate;
List preTransX = new List(),
nowTransX = new List(),
preTransY = new List(),
nowTransY = new List();
num prevArc = prevRotate * math.PI / 180,
nowArc = currentRotate * math.PI / 180;
text.each((d, i, e) {
preTransX.add(prevRotate > 0 ?
e.clientWidth * math.cos(prevArc) / 2 : 0);
nowTransX.add(currentRotate > 0 ? e.clientWidth * math.cos(nowArc) / 2 : 0);
preTransY.add(e.clientWidth * math.sin(prevArc) / 2);
nowTransY.add(e.clientWidth * math.sin(nowArc) / 2);
});
text.transition()
..attrTween('transform', (d, i, e) {
return interpolateTransform(
"translate(${preTransX[i]},${preTransY[i]})rotate(${prevRotate})",
"translate(${nowTransX[i]},${nowTransY[i]})rotate(${currentRotate})");
})
..attr('dx', '${.71 * math.sin(nowArc)}em')
..attr('dy', '${.71 * math.cos(nowArc)}em');
if (currentRotate == 90) {
text.each((d, i, Element e) {
if (i > 0 && i < text.length - 1) {
textOpacity[i] = 0;
}
group.selectAll('.ellipsis').transition()
..delay(100)
..attr('opacity', '1');
});
} else {
group.selectAll('.ellipsis').transition()
..delay(100)
..attr('opacity', '0');
}
} else {
num textHeight = text.first.clientHeight;
if (textHeight > axisLength / text.length) {
text.each((d, i, Element e) {
if (i > 0 && i < text.length - 1) {
textOpacity[i] = 0;
}
group.selectAll('.ellipsis').transition()
..delay(100)
..attr('opacity', '1');
});
} else {
group.selectAll('.ellipsis').transition()
..delay(100)
..attr('opacity', '0');
}
}
text.transition()
..attrWithCallback('opacity', (d, i, e) => textOpacity[i])
..delay(100);
// If either the new or old scale is ordinal,
// entering ticks are undefined in the old scale,
// and so can fade-in in the new scale’s position.
// Exiting ticks are likewise undefined in the new scale,
// and so can fade-out in the old scale’s position.
var transformFn;
if (current.rangeBand != 0) {
var dx = current.rangeBand / 2;
transformFn = (d) => current.apply(d) + dx;
} else if (older.rangeBand != 0) {
older = current;
} else {
tickTransform(tickExit, current.apply);
}
tickTransform(tickEnter, transformFn != null ? transformFn : older.apply);
tickTransform(tickUpdate, transformFn != null ?
transformFn : current.apply);
}
_xAxisTransform(selection, transformFn) {
selection.transition()
..attrWithCallback('transform', (d, i, e) =>
'translate(${transformFn(d)},0)'
);
}
_yAxisTransform(selection, transformFn) {
selection.transition()
..attrWithCallback('transform', (d, i, e) =>
'translate(0,${transformFn(d)})'
);
}
}