Fetching contributors…
Cannot retrieve contributors at this time
1402 lines (1316 sloc) 39.1 KB
/*!
* wq.app 1.0.0-dev - wq/chart.js
* Reusable SVG charts for analyzing time-series data.
* (c) 2013-2016, S. Andrew Sheppard
* https://wq.io/license
*/
define(["d3"],
function(d3) {
var chart = {};
function _trans(x, y, off) {
if (off) {
x -= 0.5;
y -= 0.5;
}
return 'translate(' + x + ',' + y + ')';
}
function _selectOrAppend(sel, name, cls) {
var selector = name;
if (cls) {
selector += "." + cls;
}
var elem = sel.select(selector);
if (elem.empty()) {
elem = sel.append(name);
if (cls) {
elem.attr('class', cls);
}
}
return elem;
}
// General chart configuration
chart.base = function() {
var width=700, height=300, padding=7.5,
marginGroups = {
'padding': {'left': 10, 'right': 10, 'top': 10, 'bottom': 10},
'xaxis': {'bottom': 20}
},
viewBox=true,
nestedSvg=false,
renderBackground = false,
chartover=false,
chartout=false,
xscale = null,
xscalefn = d3.scale.linear,
xnice = null,
xticks = null,
yscales = {},
yscalefn = d3.scale.linear,
cscale = d3.scale.category20(),
outerFill = '#f3f3f3',
innerFill = '#eee',
legend = null,
leftYAxis = true;
// Accessors for entire data object
var datasets = function(d) {
if (d.data) {
return d.data;
}
return d;
};
var legendItems = function(d) {
return datasets(d);
};
// Accessors for individual datasets
var id = function(dataset) {
return dataset.id;
};
var label = function(dataset) {
return dataset.label;
};
var items = function(dataset) {
return dataset.data || dataset.list;
};
var legendItemId = function(d) {
return id(d);
};
var legendItemLabel = function(d) {
return label(d);
};
var xvalue = function(d) {
/* jshint unused: false */
throw "xvalue accessor not defined!";
};
var yvalue = function(d) {
/* jshint unused: false */
throw "yvalue accessor not defined!";
};
var xunits = function(dataset) {
/* jshint unused: false */
throw "xunits accessor not defined!";
};
var xmax = function(dataset) {
return d3.max(items(dataset), xvalue);
};
var xmin = function(dataset) {
return d3.min(items(dataset), xvalue);
};
var xset = function(d) {
var xvals = d3.set();
datasets(d).forEach(function(dataset) {
items(dataset).forEach(function(d) {
xvals.add(xvalue(d));
});
});
return xvals.values().map(function(d) {
return isNaN(+d) ? d : +d;
});
};
var yunits = function(dataset) {
return dataset.units;
};
var ymax = function(dataset) {
return d3.max(items(dataset), yvalue);
};
var ymin = function(dataset) {
return d3.min(items(dataset), yvalue);
};
// Accessors for individual items
var xscaled = function(d) {
return xscale.scale(xvalue(d));
};
var yscaled = function(scaleid) {
var yscale = yscales[scaleid];
return function(d) {
return yscale.scale(yvalue(d));
};
};
var itemid = function(d) {
return xvalue(d) + "=" + yvalue(d);
};
// Rendering functions (should be overridden)
var init = function(datasets, opts) {
/* jshint unused: false */
};
var render = function(dataset) {
/* jshint unused: false */
};
var wrapup = function(datasets, opts) {
/* jshint unused: false */
};
// Legend item rendering
var legendItemShape = function(sid) {
/* jshint unused: false */
return "rect";
};
var rectStyle = function(sid) {
var color = cscale(sid);
return function(sel) {
sel.attr('x', -3)
.attr('y', -3)
.attr('width', 6)
.attr('height', 6)
.attr('fill', color);
};
};
var circleStyle = function(sid) {
var color = cscale(sid);
return function(sel) {
sel.attr('r', 3)
.attr('fill', color)
.attr('stroke', 'black')
.attr('stroke-width', 0.2)
.attr('cursor', 'pointer');
};
};
var legendItemStyle = function(sid) {
return rectStyle(sid);
};
// Generate translation function xscale + given yscale
var translate = function(scaleid) {
var yfn = yscaled(scaleid);
return function(d) {
var x = xscaled(d);
var y = yfn(d);
return _trans(x, y);
};
};
// Plot using given selection (usually one object, but wrapped as array)
function plot(sel) {
if (nestedSvg) {
sel = _selectOrAppend(sel, 'svg', nestedSvg);
}
sel.each(_plot);
}
// The actual work
function _plot(data) {
if (legend === null || legend.auto) {
_positionLegend.call(this, legendItems(data));
}
_computeScales(datasets(data));
var ordinal = xscalefn().rangePoints || false;
var svg = d3.select(this);
var uid = svg.attr('data-wq-uid') || Math.round(
Math.random() * 1000000
);
svg.attr('data-wq-uid', uid);
var vbstr;
if (viewBox) {
if (viewBox === true) {
vbstr = "0 0 " + width + " " + height;
} else {
vbstr = viewBox;
}
svg.attr("viewBox", vbstr);
}
var cwidth = width - padding - padding;
var cheight = height - padding - padding;
var margins = plot.getMargins();
var gwidth = cwidth - margins.left - margins.right;
var gheight = cheight - margins.top - margins.bottom;
var cbottom = cheight - margins.bottom;
var opts = {
'padding': padding,
'gwidth': gwidth,
'gheight': gheight,
'cwidth': cwidth,
'cheight': cheight
};
init.call(this, datasets(data), opts);
// Clip for inner graphing area
var clipId = "clip" + uid;
var defs = _selectOrAppend(svg, 'defs');
// Webkit can't select clipPath #83438
var clip = defs.select('#' + clipId);
if (clip.empty()) {
clip = defs.append('clipPath').attr('id', clipId);
clip.append('rect');
}
clip.select('rect')
.attr('width', gwidth)
.attr('height', gheight);
// Outer chart area (includes legends, axes & actual graph)
var outer = _selectOrAppend(svg, 'g', 'outer');
outer.attr('transform', _trans(padding, padding, true));
_selectOrAppend(outer, 'rect')
.attr('width', cwidth)
.attr('height', cheight)
.attr('fill', outerFill);
// Inner graphing area (clipped)
var inner = _selectOrAppend(outer, 'g', 'inner')
.attr('clip-path', 'url(#' + clipId + ')')
.attr('transform', _trans(margins.left, margins.top));
_selectOrAppend(inner, 'rect')
.attr('width', gwidth)
.attr('height', gheight)
.attr('fill', innerFill);
if (chartover) {
inner.on('mouseover', chartover(data));
inner.on('mousemove', chartover(data));
}
if (chartout) {
inner.on('mouseout', chartout(data));
}
// Create actual scale & axis objects
xscale.scale = xscalefn();
if (ordinal) {
xscale.scale
.domain(xset(data).sort(d3.ascending))
.rangePoints([0, gwidth], 1);
} else {
xscale.scale
.domain([xscale.xmin, xscale.xmax])
.range([0, gwidth]);
}
if (xscale.scale.nice && xnice) {
xscale.scale.nice(xnice);
}
xscale.axis = d3.svg.axis()
.scale(xscale.scale)
.orient('bottom')
.tickSize(4, 2, 1);
if (xticks) {
xscale.axis.ticks(xticks);
}
for (var scaleid in yscales) {
var scale = yscales[scaleid];
var domain;
if (scale.invert) {
domain = [scale.ymax, scale.ymin];
} else {
domain = [scale.ymin, scale.ymax];
}
scale.scale = yscalefn()
.domain(domain)
.nice()
.range([gheight, 0]);
scale.axis = d3.svg.axis()
.scale(scale.scale)
.orient(scale.orient)
.tickSize(4, 2, 1);
}
// Render each dataset
if (renderBackground) {
var background = _selectOrAppend(inner, 'g', 'background')
.selectAll('g.dataset-background')
.data(datasets(data), id);
background.enter()
.append('g')
.attr('class', 'dataset-background');
background.exit().remove();
background.each(renderBackground);
}
var series = _selectOrAppend(inner, 'g', 'datasets')
.selectAll('g.dataset')
.data(datasets(data), id);
series.enter()
.append('g')
.attr('class', 'dataset');
series.exit().remove();
series.each(render);
// Render axes
_selectOrAppend(outer, 'g', 'xaxis')
.attr('transform', _trans(margins.left, cbottom))
.call(xscale.axis)
.selectAll('line').attr('stroke', '#000');
var yaxes = outer.selectAll('g.axis')
.data(d3.values(yscales), function(s){ return s.id; });
yaxes.enter().append('g').attr('class', 'axis').append('text');
yaxes.exit().remove();
yaxes.attr('transform', function(d) {
var x;
if (d.orient == 'left') {
x = margins.left;
} else {
x = cwidth - margins.right;
}
var y = margins.top;
return _trans(x, y);
})
.each(function(d) {
var axis = d3.select(this);
axis.call(d.axis)
.selectAll('line').attr('stroke', '#000');
axis.select('text')
.text(d.id || '')
.attr('text-anchor', 'middle')
.attr('font-weight', 'bold')
.attr('transform', function() {
if (d.orient == 'left') {
return (
'rotate(-90),' +
'translate(-' + gheight / 2 + ',-60)'
);
} else {
return (
'rotate(90),' +
'translate(' + gheight / 2 + ',-60)'
);
}
});
});
if (legend && legend.position) {
_renderLegend.call(this, legendItems(data), opts);
} else {
outer.select('g.legend').remove();
}
wrapup.call(this, datasets(data), opts);
}
function _positionLegend(items) {
var rows = items.length;
if (rows > 5) {
plot.legend({
'position': 'right',
'size': 150,
'auto': true
});
} else {
plot.legend({
'position': 'bottom',
'size': (rows * 22 + 20),
'auto': true
});
}
}
// Compute horizontal & vertical scales
// - may be more than one vertical scale if there are different units
function _computeScales(datasets) {
datasets.forEach(function(dataset) {
if (!xscale) {
xscale = {
'xmin': Infinity,
'xmax': -Infinity,
'auto': true
};
}
if (xscale.auto) {
xscale.xmax = d3.max([xscale.xmax, xmax(dataset)]);
xscale.xmin = d3.min([xscale.xmin, xmin(dataset)]);
}
var scaleid = yunits(dataset);
if (!yscales[scaleid]) {
yscales[scaleid] = {
'ymin': 0,
'ymax': 0,
'auto': true
};
}
var yscale = yscales[scaleid];
if (!yscale.id) {
yscale.id = scaleid;
}
if (!yscale.orient) {
yscale.orient = leftYAxis ? 'left' : 'right';
leftYAxis = !leftYAxis;
}
if (!yscale.sets) {
yscale.sets = 0;
}
yscale.sets++;
if (yscale.auto) {
yscale.ymax = d3.max([yscale.ymax, ymax(dataset)]);
yscale.ymin = d3.min([yscale.ymin, ymin(dataset)]);
}
});
var ymargin = {'left': 70};
if (d3.keys(yscales).length > 1) {
ymargin.right = 70;
}
plot.setMargin('yaxis', ymargin);
}
function _renderLegend(items, opts) {
var svg = d3.select(this),
outer = svg.select('g.outer'),
margins = plot.getMargins(),
legendX, legendY, legendW, legendH;
if (legend.position == 'bottom') {
legendX = margins.left;
legendY = opts.cheight - margins.bottom + 30;
legendW = opts.gwidth;
legendH = legend.size;
} else {
legendX = opts.cwidth - legend.size - 10;
legendY = margins.top;
legendW = legend.size;
legendH = opts.gheight;
}
var leg = _selectOrAppend(outer, 'g', 'legend')
.attr('transform', _trans(legendX, legendY));
_selectOrAppend(leg, 'rect')
.attr('width', legendW)
.attr('height', legendH)
.attr('fill', 'white')
.attr('stroke', '#999');
var legitems = leg.selectAll('g.legenditem')
.data(items, legendItemId);
var newitems = legitems.enter().append('g')
.attr('class', 'legenditem')
.append('g')
.attr('class', 'data');
newitems.each(function(d) {
var g = d3.select(this),
sid = legendItemId(d);
g.append(legendItemShape(sid));
g.append('text');
});
legitems.exit().remove();
legitems.each(function(d, i) {
var g = d3.select(this).select('g.data'),
sid = legendItemId(d);
g.attr('transform', _trans(20, 20 + i * 22));
g.select(legendItemShape(sid)).call(legendItemStyle(sid));
g.select('text')
.text(legendItemLabel(d))
.attr('transform', _trans(10, 5));
});
}
// Getters/setters for chart configuration
plot.width = function(val) {
if (!arguments.length) {
return width;
}
width = val;
return plot;
};
plot.height = function(val) {
if (!arguments.length) {
return height;
}
height = val;
return plot;
};
plot.viewBox = function(val) {
if (!arguments.length) {
return viewBox;
}
viewBox = val;
return plot;
};
plot.nestedSvg = function(val) {
if (!arguments.length) {
return nestedSvg;
}
nestedSvg = val;
return plot;
};
plot.outerFill = function(val) {
if (!arguments.length) {
return outerFill;
}
outerFill = val;
return plot;
};
plot.innerFill = function(val) {
if (!arguments.length) {
return innerFill;
}
innerFill = val;
return plot;
};
plot.legend = function(val) {
if (!arguments.length) {
return legend;
}
legend = val || {};
var lmargin = {};
if (legend.position == 'bottom') {
lmargin.bottom = legend.size + 10;
} else if (legend.position == 'right') {
lmargin.right = legend.size + 10;
}
plot.setMargin('legend', lmargin);
return plot;
};
plot.xscale = function(val) {
if (!arguments.length) {
return xscale;
}
xscale = val;
return plot;
};
plot.xscalefn = function(fn) {
if (!arguments.length) {
return xscalefn;
}
xscalefn = fn;
return plot;
};
plot.xscaled = function(fn) {
if (!arguments.length) {
return xscaled;
}
xscaled = fn;
return plot;
};
plot.xnice = function(val) {
if (!arguments.length) {
return xnice;
}
xnice = val;
return plot;
};
plot.xticks = function(val) {
if (!arguments.length) {
return xticks;
}
xticks = val;
return plot;
};
plot.yscales = function(val) {
if (!arguments.length) {
return yscales;
}
yscales = val;
return plot;
};
plot.yscalefn = function(fn) {
if (!arguments.length) {
return yscalefn;
}
yscalefn = fn;
return plot;
};
plot.yscaled = function(fn) {
if (!arguments.length) {
return yscaled;
}
yscaled = fn;
return plot;
};
plot.cscale = function(fn) {
if (!arguments.length) {
return cscale;
}
cscale = fn;
return plot;
};
// Getters/setters for accessors
plot.datasets = function(fn) {
if (!arguments.length) {
return datasets;
}
datasets = fn;
return plot;
};
plot.id = function(fn) {
if (!arguments.length) {
return id;
}
id = fn;
return plot;
};
plot.label = function(fn) {
if (!arguments.length) {
return label;
}
label = fn;
return plot;
};
plot.legendItems = function(fn) {
if (!arguments.length) {
return legendItems;
}
legendItems = fn;
return plot;
};
plot.legendItemId = function(fn) {
if (!arguments.length) {
return legendItemId;
}
legendItemId = fn;
return plot;
};
plot.legendItemLabel = function(fn) {
if (!arguments.length) {
return legendItemLabel;
}
legendItemLabel = fn;
return plot;
};
plot.items = function(fn) {
if (!arguments.length) {
return items;
}
items = fn;
return plot;
};
plot.yunits = function(fn) {
if (!arguments.length) {
return yunits;
}
yunits = fn;
return plot;
};
plot.xunits = function(fn) {
if (!arguments.length) {
return xunits;
}
xunits = fn;
return plot;
};
plot.xvalue = function(fn) {
if (!arguments.length) {
return xvalue;
}
xvalue = fn;
return plot;
};
plot.xmin = function(fn) {
if (!arguments.length) {
return xmin;
}
xmin = fn;
return plot;
};
plot.xmax = function(fn) {
if (!arguments.length) {
return xmax;
}
xmax = fn;
return plot;
};
plot.xset = function(fn) {
if (!arguments.length) {
return xset;
}
xset = fn;
return plot;
};
plot.yvalue = function(fn) {
if (!arguments.length) {
return yvalue;
}
yvalue = fn;
return plot;
};
plot.ymin = function(fn) {
if (!arguments.length) {
return ymin;
}
ymin = fn;
return plot;
};
plot.ymax = function(fn) {
if (!arguments.length) {
return ymax;
}
ymax = fn;
return plot;
};
plot.itemid = function(fn) {
if (!arguments.length) {
return itemid;
}
itemid = fn;
return plot;
};
// Getters/setters for render functions
plot.init = function(fn) {
if (!arguments.length) {
return init;
}
init = fn;
return plot;
};
plot.chartover = function(fn) {
if (!arguments.length) {
return chartover;
}
chartover = fn;
return plot;
};
plot.chartout = function(fn) {
if (!arguments.length) {
return chartout;
}
chartout = fn;
return plot;
};
plot.renderBackground = function(fn) {
if (!arguments.length) {
return renderBackground;
}
renderBackground = fn;
return plot;
};
plot.render = function(fn) {
if (!arguments.length) {
return render;
}
render = fn;
return plot;
};
plot.wrapup = function(fn) {
if (!arguments.length) {
return wrapup;
}
wrapup = fn;
return plot;
};
plot.translate = function(fn) {
if (!arguments.length) {
return translate;
}
translate = fn;
return plot;
};
plot.legendItemShape = function(fn) {
if (!arguments.length) {
return legendItemShape;
}
legendItemShape = fn;
return plot;
};
plot.legendItemStyle = function(fn) {
if (!arguments.length) {
return legendItemStyle;
}
legendItemStyle = fn;
return plot;
};
plot.rectStyle = function(fn) {
if (!arguments.length) {
return rectStyle;
}
rectStyle = fn;
return plot;
};
plot.circleStyle = function(fn) {
if (!arguments.length) {
return circleStyle;
}
circleStyle = fn;
return plot;
};
// Inner margin has separate getter and setter as it is composed of a
// number of individually-set components
plot.setMargin = function(name, offsets) {
marginGroups[name] = offsets;
return plot;
};
plot.getMargins = function() {
var margins = {
'left': 0,
'right': 0,
'top': 0,
'bottom': 0
};
for (var name in marginGroups) {
for (var dir in marginGroups[name]) {
var val = marginGroups[name][dir];
if (val) {
margins[dir] += val;
}
}
}
return margins;
};
return plot;
};
// Scatter plot
chart.scatter = function() {
var plot = chart.base(), pointStyle = plot.circleStyle(), pointShape;
plot.xvalue(function(d) {
return d.x;
}).xunits(function(dataset) {
return dataset.xunits;
}).yvalue(function(d) {
return d.y;
}).yunits(function(dataset) {
return dataset.yunits;
}).legendItemShape(function(sid) {
return pointShape(sid);
}).legendItemStyle(function(sid) {
return pointStyle(sid);
});
/* To customize points beyond just the color, override these functions */
pointShape = function(sid) {
/* jshint unused: false */
return "circle";
};
// pointStyle function is initialized above
/* To customize lines beyond just the color, override this function */
var lineStyle = function(sid) {
var color = plot.cscale()(sid);
return function(sel) {
sel.attr('stroke', color);
};
};
var pointover = function(sid) {
/* jshint unused: false */
return function(d) {
d3.select(this).selectAll(pointShape(sid))
.attr('fill', '#9999ff');
};
};
var pointout = function(sid) {
/* jshint unused: false */
return function(d) {
d3.select(this).selectAll(pointShape(sid))
.attr('fill', plot.cscale()(sid));
};
};
var pointLabel = function(sid) {
var x = plot.xvalue(),
y = plot.yvalue();
return function(d) {
return sid + " at " + x(d) + ": " + y(d);
};
};
var drawPointsIf = function(dataset) {
var items = plot.items()(dataset);
return items && items.length <= 50;
};
var drawLinesIf = function(dataset){
var items = plot.items()(dataset);
return items && items.length > 50;
};
plot.chartover(function(data) {
return function() {
var inner = d3.select(this),
mouse = d3.mouse(this),
xscale = plot.xscale(),
xvalue = plot.xvalue(),
translate = plot.translate(),
x = xscale.scale.invert(mouse[0]),
bisect = d3.bisector(xvalue),
hoverData = [];
plot.datasets()(data).forEach(function(dataset) {
if (!drawLinesIf(dataset)) {
return;
}
var sid = plot.id()(dataset),
items = plot.items()(dataset),
yunits = plot.yunits()(dataset),
yscaled = plot.yscaled()(yunits),
index = bisect.left(items, x),
d1 = items[index > 0 ? index - 1 : 0],
d2 = items[index < items.length ? index : index - 1],
ptx1 = xscale.scale(xvalue(d1)),
pty1 = yscaled(d1),
dist1 = Math.sqrt(
Math.pow(ptx1 - mouse[0], 2) +
Math.pow(pty1 - mouse[1], 2)
),
ptx2 = xscale.scale(xvalue(d2)),
pty2 = yscaled(d2),
dist2 = Math.sqrt(
Math.pow(ptx2 - mouse[0], 2) +
Math.pow(pty2 - mouse[1], 2)
),
threshold = 20;
if (dist1 < threshold || dist2 < threshold) {
hoverData.push({
'id': sid,
'units': yunits,
'data': dist1 < dist2 ? d1 : d2
});
}
});
var hover = inner.selectAll('g.line-hover').data(hoverData);
hover.enter().append('g')
.attr('class', 'line-hover');
hover.each(function(d) {
var g = d3.select(this).datum(d.data);
_selectOrAppend(g, pointShape(d.id))
.call(pointStyle(d.id))
.attr('transform', translate(d.units));
_selectOrAppend(g, 'title')
.text(pointLabel(d.id));
});
hover.exit().remove();
};
});
// Render lines in background to ensure all points are above them
plot.renderBackground(function(dataset) {
var items = plot.items()(dataset),
yunits = plot.yunits()(dataset),
sid = plot.id()(dataset),
xscaled = plot.xscaled(),
yscaled = plot.yscaled()(yunits),
g = d3.select(this),
path = g.select('path.data'),
line = d3.svg.line()
.x(xscaled)
.y(yscaled);
d3.select(g.node().parentNode.parentNode)
.selectAll('g.line-hover').remove();
if (!drawLinesIf(dataset)) {
path.remove();
return;
}
// Generate path element for new datasets
if (path.empty()) {
path = g.append('path')
.attr('class', 'data')
.attr('fill', 'transparent');
}
// Update path for new and existing datasets
path.datum(items)
.attr('d', line)
.call(lineStyle(sid));
});
plot.render(function(dataset) {
var items = plot.items()(dataset),
yunits = plot.yunits()(dataset),
sid = plot.id()(dataset),
translate = plot.translate(),
g = d3.select(this),
points, newpoints;
if (!drawPointsIf(dataset)) {
g.selectAll('g.data').remove();
return;
}
points = g.selectAll('g.data').data(items, plot.itemid());
// Generate elements for new data
newpoints = points.enter().append('g')
.attr('class', 'data');
newpoints.append('title');
newpoints.append(pointShape(sid));
points.exit().remove();
// Update elements for new or existing data
points.on('mouseover', pointover(sid))
.on('mouseout', pointout(sid));
points.attr('transform', translate(yunits));
points.select(pointShape(sid)).call(pointStyle(sid));
points.select('title').text(pointLabel(sid));
});
// Getters/setters for chart configuration
plot.pointShape = function(fn) {
if (!arguments.length) {
return pointShape;
}
pointShape = fn;
return plot;
};
plot.pointStyle = function(fn) {
if (!arguments.length) {
return pointStyle;
}
pointStyle = fn;
return plot;
};
plot.lineStyle = function(fn) {
if (!arguments.length) {
return lineStyle;
}
lineStyle = fn;
return plot;
};
plot.pointover = function(fn) {
if (!arguments.length) {
return pointover;
}
pointover = fn;
return plot;
};
plot.pointout = function(fn) {
if (!arguments.length) {
return pointout;
}
pointout = fn;
return plot;
};
plot.pointLabel = function(fn) {
if (!arguments.length) {
return pointLabel;
}
pointLabel = fn;
return plot;
};
plot.drawPointsIf = function(fn) {
if (!arguments.length) {
return drawPointsIf;
}
drawPointsIf = fn;
return plot;
};
plot.drawLinesIf = function(fn) {
if (!arguments.length) {
return drawLinesIf;
}
drawLinesIf = fn;
return plot;
};
return plot;
};
// Time series scatter plot
chart.timeSeries = function() {
var plot = chart.scatter(),
format = d3.time.format('%Y-%m-%d');
plot.xvalue(function(d) {
return format.parse(d.date);
})
.xscalefn(d3.time.scale)
.xnice(d3.time.year)
.yvalue(function(d) {
return d.value;
})
.yunits(function(d) {
return d.units;
})
.pointLabel(function(sid) {
var x = plot.xvalue(),
y = plot.yvalue();
return function(d) {
return sid + " on " + format(x(d)) + ": " + y(d);
};
});
// Getters/setters for chart configuration
plot.timeFormat = function(val) {
if (!arguments.length) {
return format;
}
format = d3.time.format(val);
return plot;
};
return plot;
};
// Contours (precomputed)
chart.contour = function() {
var plot = chart.scatter();
plot.legendItemShape(function(sid) {
/* jshint unused: false */
return 'rect';
}).legendItemStyle(
plot.rectStyle()
);
plot.render(function(dataset) {
var x = plot.xvalue(),
y = plot.yvalue(),
yunits = plot.yunits(),
xscale = plot.xscale().scale,
yscale = plot.yscales()[yunits(dataset)].scale,
cscale = plot.cscale(),
id = plot.id(),
items = plot.items();
var path = d3.svg.line()
.x(function(d) {
return xscale(x(d));
})
.y(function(d) {
return yscale(y(d));
});
var contours = d3.select(this).selectAll('path.contour')
.data(items(dataset), id);
contours.enter()
.append('path')
.attr('class', 'contour');
contours.exit().remove();
contours.attr('d', path)
.attr('fill', cscale(id(dataset)));
});
return plot;
};
// Box & whiskers (precomputed)
chart.boxplot = function() {
var plot = chart.base(), r, wr, offsets = {}, prefix = 'value-';
// Accessors for individual items
var q1 = function(d) {
if ('p25' in d) {
// Backwards compatibility with old names; remove in 1.0
return d.p25;
}
return d[prefix + 'q1'];
};
var q3 = function(d) {
if ('p75' in d) {
// Backwards compatibility with old names; remove in 1.0
return d.p75;
}
return d[prefix + 'q3'];
};
var med = function(d) {
if ('median' in d) {
// Backwards compatibility with old names; remove in 1.0
return d.median;
}
return d[prefix + 'med'];
};
var whishi = function(d) {
if ('max' in d) {
// Backwards compatibility with old names; remove in 1.0
return d.max;
}
return d[prefix + 'whishi'];
};
var whislo = function(d) {
if ('min' in d) {
// Backwards compatibility with old names; remove in 1.0
return d.min;
}
return d[prefix + 'whislo'];
};
plot.xscalefn(d3.scale.ordinal)
.itemid(function(d) { return plot.xvalue()(d); })
.ymin(function(dataset) {
var items = plot.items();
return d3.min(items(dataset), function(d) {
return whislo(d);
});
})
.ymax(function(dataset) {
var items = plot.items();
return d3.max(items(dataset), function(d) {
return whishi(d);
});
})
.init(function(datasets, opts) {
var step = plot.xset()(datasets).length; // Number of x axis labels
var slots = step * (datasets.length + 1); // ~How many boxes to fit
var space = opts.gwidth / slots; // Space available for each box
r = d3.min([space * 0.8 / 2, 20]); // "radius" of box (use 80%)
wr = r / 2; // "radius" of whiskers
var width = (datasets.length - 1) * space;
datasets.forEach(function(dataset, i) {
offsets[plot.id()(dataset)] = i * space - width / 2;
});
})
.render(function(dataset) {
var items = plot.items()(dataset),
yunits = plot.yunits()(dataset),
sid = plot.id()(dataset),
yscales = plot.yscales(),
xscale = plot.xscale(),
xvalue = plot.xvalue();
function translate(scaleid) {
var yscale = yscales[scaleid];
return function(d) {
var x = xscale.scale(xvalue(d));
var y = yscale.scale(0);
return _trans(x, y);
};
}
var boxes = d3.select(this).selectAll('g.data').data(
items, plot.itemid()
);
boxes.enter().append('g').attr('class', 'data');
boxes.exit().remove();
boxes.attr('transform', translate(yunits))
.each(box(sid, yunits));
});
function box(sid, yunits) {
var yscale = plot.yscales()[yunits];
var color = plot.cscale()(sid);
return function(d) {
var dq1 = q1(d),
dq3 = q3(d),
dmed = med(d),
dwhislo = whislo(d),
dwhishi = whishi(d);
if (!d || (!dmed && !dwhislo && !dwhishi)) {
return;
}
function y(val) {
return yscale.scale(val) - yscale.scale(0);
}
var box = _selectOrAppend(d3.select(this), 'g', 'box')
.attr('transform', _trans(offsets[sid], 0));
_selectOrAppend(box, 'line', 'q1')
.attr('x1', -r)
.attr('x2', r)
.attr('y1', y(dq1))
.attr('y2', y(dq1))
.attr('stroke-width', 2)
.attr('stroke', color);
_selectOrAppend(box, 'line', 'q3')
.attr('x1', -r)
.attr('x2', r)
.attr('y1', y(dq3))
.attr('y2', y(dq3))
.attr('stroke-width', 2)
.attr('stroke', color);
_selectOrAppend(box, 'line', 'med')
.attr('x1', -r)
.attr('x2', r)
.attr('y1', y(dmed))
.attr('y2', y(dmed))
.attr('stroke-width', 2)
.attr('stroke', color);
_selectOrAppend(box, 'line', 'iqr-left')
.attr('x1', -r)
.attr('x2', -r)
.attr('y1', y(dq1))
.attr('y2', y(dq3))
.attr('stroke-width', 2)
.attr('stroke', color);
_selectOrAppend(box, 'line', 'iqr-right')
.attr('x1', r)
.attr('x2', r)
.attr('y1', y(dq1))
.attr('y2', y(dq3))
.attr('stroke-width', 2)
.attr('stroke', color);
_selectOrAppend(box, 'line', 'w-top')
.attr('x1', -wr)
.attr('x2', wr)
.attr('y1', y(dwhishi))
.attr('y2', y(dwhishi))
.attr('stroke', color);
_selectOrAppend(box, 'line', 'w-bottom')
.attr('x1', -wr)
.attr('x2', wr)
.attr('y1', y(dwhislo))
.attr('y2', y(dwhislo))
.attr('stroke', color);
_selectOrAppend(box, 'line', 'w-q3')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', y(dwhishi))
.attr('y2', y(dq3))
.attr('stroke', color);
_selectOrAppend(box, 'line', 'w-q1')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', y(dwhislo))
.attr('y2', y(dq1))
.attr('stroke', color);
};
}
// Getters/setters for accessors
plot.prefix = function(val) {
if (!arguments.length) {
return prefix;
}
prefix = val;
return plot;
};
plot.q1 = function(fn) {
if (!arguments.length) {
return q1;
}
q1 = fn;
return plot;
};
plot.q3 = function(fn) {
if (!arguments.length) {
return q3;
}
q3 = fn;
return plot;
};
plot.med = function(fn) {
if (!arguments.length) {
return med;
}
med = fn;
return plot;
};
plot.whishi = function(fn) {
if (!arguments.length) {
return whishi;
}
whishi = fn;
return plot;
};
plot.whislo = function(fn) {
if (!arguments.length) {
return whislo;
}
whislo = fn;
return plot;
};
return plot;
};
return chart;
});