Skip to content

Instantly share code, notes, and snippets.

@danielkeller
Last active October 17, 2017 19:21
Show Gist options
  • Save danielkeller/5a4d8135c6d0fddca65a1ff991e1911c to your computer and use it in GitHub Desktop.
Save danielkeller/5a4d8135c6d0fddca65a1ff991e1911c to your computer and use it in GitHub Desktop.
Simple approach to plotting
height: 520
<!DOCTYPE html>
<meta charset="utf-8">
<svg width="680" height="500"></svg>
<button>Click me!!</button>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="lib.js"></script>
<script>
var data = getData();
var keys = data.columns.slice(1);
//The data is first converted into a 'series' which works out the
//extent of the data; the categories, groups, and bar height
var series1 = cc.dataSeriesCategorical('State')(data);
//To stack the data, the bottoms and tops of each bar must be
//calculated
var stack = d3.stack().keys(keys)(data);
var series2 = cc.dataSeriesCategoricalStack('State')(stack);
var svg = d3.select("svg");
//The plotArea sets up the margins and size and such
var plotArea = cc.plotArea()
.fullWidth(svg.attr("width"))
.fullHeight(svg.attr("height"));
var g = svg.append("g").call(plotArea);
//The data area works out how the data fits into the plot, using d3's "scale"s
var dataArea = cc.dataArea(plotArea).data(series1).nice();
//This draws the axes given the plot area and data area.
var axes = cc.defaultAxes();
axes.left().ticks(null, "s");
var chart = g.append("g");
//And 'bars' draws the actual bars
chart.call(cc.bars(dataArea, series1));
g.call(axes.dataArea(dataArea));
//This is what a custom plot type would look like
function waterfall(dataArea, series) {
var x = dataArea.x(), //The category scale
y = dataArea.y(), //The data scale
z = dataArea.z(), //The color scale
x1 = d3.scaleBand().padding(0.05) //The scale within each category
.domain(series.keys)
.rangeRound([0, x.bandwidth()]);
return function waterfall(context) {
var g = context.selection ? context.selection() : context;
var transition = !!context.selection;
//Make all the rectangles
var groups = cc.seriesGroups(g, series, 'g');
var rects = groups.selectAll("rect").data(function(d) {return d});
rects.exit().remove();
rects.enter().append("rect")
.merge(rects);
//Set the position and color of the rectangles
groups.each(function (series) {
(transition ? d3.select(this).transition(context) : d3.select(this))
.selectAll("rect")
.attr("x", function(d) { return x(d.key) + x1(series.key) })
.attr("width", x1.bandwidth())
.attr("y", function(d) { return d3.min(d.value.map(y)) })
.attr("height", function(d) { return Math.abs(y(d.value[0]) - y(d.value[1])) })
.attr("fill", function(d) { return z(series.key) })
});
}
}
var button = d3.select("button");
button.on("click", showWF);
//This shows how chart types can animate into each other, and the axes as well
function showBars() {
dataArea.reset().data(series1).nice();
var gt = g.transition().call(axes.dataArea(dataArea));
chart.transition(gt).call(cc.bars(dataArea, series1));
button.on("click", showWF);
}
function showWF() {
dataArea.reset().data(series2).nice();
var gt = g.transition().call(axes.dataArea(dataArea));
chart.transition(gt).call(waterfall(dataArea, series2));
button.on("click", showBars);
}
//Draw the legend manually
g.select(".axis.y")
.append("text")
.attr("x", 2)
.attr("y", dataArea.y()(dataArea.y().ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.text("Population");
var legend = g.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "end")
.attr("transform", "translate(" + plotArea.width() + ",0)")
.selectAll("g")
.data(dataArea.z().domain())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", -19)
.attr("width", 19)
.attr("height", 19)
.attr("fill", dataArea.z());
legend.append("text")
.attr("x", -24)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text(function(d) { return d; });
function getData() {
var dsv =
`State,Under 5 Years,5 to 13 Years,14 to 17 Years,18 to 24 Years,25 to 44 Years,45 to 64 Years,65 Years and Over
CA,2704659,4499890,2159981,3853788,10604510,8819342,4114496
TX,2027307,3277946,1420518,2454721,7017731,5656528,2472223
NY,1208495,2141490,1058031,1999120,5355235,5120254,2607672
FL,1140516,1938695,925060,1607297,4782119,4746856,3187797
IL,894368,1558919,725973,1311479,3596343,3239173,1575308
PA,737462,1345341,679201,1203944,3157759,3414001,1910571`
return d3.csvParse(dsv);
}
</script>
var cc = (function () {
function ƒ(name){
var v,params=Array.prototype.slice.call(arguments,1);
return function(o){
return (typeof (v=o[name])==='function' ? v.apply(o,params) : v );
};
}
function extentExtent(array, accessor) {
var extents = array.map(accessor || function (d) {return d});
return d3.extent(d3.merge(extents));
}
function plotArea() {
var margin = {top: 20, right: 20, bottom: 30, left: 40},
fullWidth = 0,
fullHeight = 0,
x = 0, y = 0
;
function plotArea(g) {
return g.attr("transform", "translate(" + (+margin.left + +x)
+ "," + (+margin.top + +y) + ")")
}
plotArea.margin = function(_) {
return arguments.length ? (margin = _, plotArea) : margin
}
plotArea.fullWidth = function(_) {
return arguments.length ? (fullWidth = _, plotArea) : fullWidth
}
plotArea.fullHeight = function(_) {
return arguments.length ? (fullHeight = _, plotArea) : fullHeight
}
plotArea.width = function() {
return +fullWidth - +margin.left - +margin.right
}
plotArea.height = function() {
return +fullHeight - +margin.top - +margin.bottom
}
return plotArea;
}
function assertIsStack(data) {
if (!data[0] || !data[0][0] || data[0][0].length !== 2)
throw new Error("not a stack");
}
function dataSeries() {
var data,
keys,
value,
domain,
range,
x;
function dataSeries(data) {
var keyVals = typeof keys === 'function' ? keys(data) : keys;
var values = keyVals.map(function (key, i) {
var v = value(data, key)
v.key = key;
v.range = range(v, data, key);
return v;
});
values.domain = domain(data);
values.x = x();
values.range = extentExtent(values, ƒ("range"));
values.keys = keyVals;
return values;
}
dataSeries.keys = function(_) {
return arguments.length ? (keys = _, dataSeries) : keys;
}
dataSeries.value = function(_) {
return arguments.length ? (value = _, dataSeries) : value;
}
dataSeries.range = function(_) {
return arguments.length ? (range = _, dataSeries) : range;
}
dataSeries.domain = function(_) {
return arguments.length ? (domain = _, dataSeries) : domain;
}
dataSeries.x = function(_) {
return arguments.length ? (x = _, dataSeries) : x;
}
return dataSeries;
}
function dataSeriesCategorical(categoryKey) {
function keys(data) {
return data.columns.filter(function (col) {return col !== categoryKey})
}
function value(data, key) {
return data.map(function (d) {return {
key: d[categoryKey], value: +d[key]
}})
}
function range(data) {
return d3.extent(data.map(ƒ("value")))
}
function domain(data) {
return data.map(ƒ(categoryKey))
}
function x() {
return d3.scaleBand().padding(0.1);
}
return dataSeries().keys(keys).value(value).range(range).domain(domain).x(x);
}
function dataSeriesTimeSeries(timeKey) {
function domain(data) {
return d3.extent(data.map(ƒ(timeKey)))
}
function x() {
return d3.scaleTime();
}
return dataSeriesCategorical(timeKey).domain(domain).x(x);
}
function dataSeriesCategoricalStack(categoryKey) {
function keys(data) {
assertIsStack(data);
return data.map(ƒ('key'))
}
function value(data, key) {
var nest = d3.nest().key(ƒ('key')).object(data);
return nest[key][0].map(function (d) {return {
key: d.data[categoryKey], value: [d[0], d[1]]
}})
}
function range(series) {
return d3.extent(d3.merge(series.map(ƒ('value'))));
}
function domain(data) {
return data[0].map(function (d) {return d.data[categoryKey]})
}
function x() {
return d3.scaleBand().padding(0.1);
}
return dataSeries().keys(keys).value(value).range(range).domain(domain).x(x);
}
function dataSeriesTimeSeriesStack(timeKey) {
function domain(data) {
return d3.extent(data[0].map(function (d) {return d.data[timeKey]}))
}
function x() {
return d3.scaleTime();
}
return dataSeriesCategoricalStack(timeKey).domain(domain).x(x);
}
function dataArea(plotArea) {
var z = d3.scaleOrdinal().range(d3.schemeCategory10),
y = d3.scaleLinear().domain([NaN, NaN]).interpolate(d3.interpolateRound),
x;
var dataArea = {};
function update() {
y.range([plotArea.height(), 0]);
if (x) x.rangeRound([0, plotArea.width()]);
return dataArea;
}
dataArea.x = function (_) {
return arguments.length ? (x = _, update()) : x;
}
dataArea.y = function (_) {
return arguments.length ? (y = _, update()) : y;
}
dataArea.z = function (_) {
return arguments.length ? (z = _, update()) : z;
}
dataArea.data = function(_) {
if (!x) x = _.x.copy().domain(_.domain);
y.domain(extentExtent([_.range, y.domain()]));
return update();
}
dataArea.toZero = function () {
y.domain(extentExtent([[0], y.domain()]));
return dataArea;
}
dataArea.nice = function() {
y.nice();
if (x && x.nice) x.nice();
return dataArea;
}
dataArea.reset = function() {
x = null;
dataExtent = [];
z.domain([]);
y.domain([NaN, NaN]);
return dataArea;
}
return dataArea;
}
function seriesGroups(g, series, tagName) {
var groups = g.selectAll('.series').data(series, ƒ("key"));
groups.exit().remove();
groups = groups.enter().append(tagName).attr("class", "series")
.merge(groups);
return groups;
}
function bars(dataArea, series) {
var x = dataArea.x(),
y = dataArea.y(),
z = dataArea.z(),
x1 = d3.scaleBand().padding(0.05)
.domain(series.keys)
.rangeRound([0, x.bandwidth()]);
return function bars(context) {
var g = context.selection ? context.selection() : context;
var transition = !!context.selection;
var groups = seriesGroups(g, series, 'g');
var rects = groups.selectAll("rect").data(function(d) {return d});
rects.exit().remove();
rects.enter().append("rect")
.merge(rects);
groups.each(function (series) {
(transition ? d3.select(this).transition(context) : d3.select(this))
.selectAll("rect")
.attr("x", function(d) { return x(d.key) + x1(series.key) })
.attr("width", x1.bandwidth())
.attr("y", function(d) { return Math.min(y(0), y(d.value)) })
.attr("height", function(d) { return Math.abs(y(d.value) - y(0)) })
.attr("fill", function(d) { return z(series.key) })
});
}
}
function stack(dataArea, series) {
var x = dataArea.x(),
y = dataArea.y(),
z = dataArea.z();
return function stack(context) {
var g = context.selection ? context.selection() : context;
var transition = !!context.selection;
var groups = seriesGroups(g, series, 'g');
var rects = groups.selectAll("rect").data(function(d) {return d});
rects.exit().remove();
rects.enter().append("rect")
.merge(rects);
groups.each(function (series) {
(transition ? d3.select(this).transition(context) : d3.select(this))
.selectAll("rect")
.attr("x", function(d) { return x(d.key) })
.attr("width", x.bandwidth())
.attr("y", function(d) { return y(d.value[1]) })
.attr("height", function(d) { return Math.max(0, y(d.value[0]) - y(d.value[1])) })
.attr("fill", function(d) { return z(series.key) });
});
}
}
function lines(dataArea, series) {
var x = dataArea.x(),
y = dataArea.y(),
z = dataArea.z(),
line = d3.line();
function lines(context) {
var xOffset = x.bandwidth ? x.bandwidth() / 2 : 0
line.y(function(d) { return y(d.value) })
.x(function(d) { return x(d.key) + xOffset });
var g = context.selection ? context.selection() : context;
var transition = !!context.selection;
var paths = seriesGroups(g, series, 'path');
if (transition) paths = paths.transition();
paths
.attr("fill", "none")
.attr("stroke", function(d) { return z(d.key) })
.attr("d", line);
}
lines.line = function(_) {
arguments.length ? (line = _, lines) : line;
}
return lines;
}
function areas(dataArea, series) {
var x = dataArea.x(),
y = dataArea.y(),
z = dataArea.z(),
area = d3.area();
function areas(context) {
var xOffset = x.bandwidth ? x.bandwidth() / 2 : 0
area.y0(function(d) { return y(d.value[0]) })
.y1(function(d) { return y(d.value[1]) })
.x(function(d) { return x(d.key) + xOffset });
var g = context.selection ? context.selection() : context;
var transition = !!context.selection;
var paths = seriesGroups(g, series, 'path');
if (transition) paths = paths.transition();
paths
.attr("stroke", "none")
.attr("fill", function(d) { return z(d.key) })
.attr("d", area);
}
areas.area = function(_) {
arguments.length ? (area = _, areas) : area;
}
return areas;
}
function axes(_) {
var top, bottom, left, right,
dataArea = _;
function doAxis(axis, loc, context) {
var selection = context.selection ? context.selection() : context;
var transition = !!context.selection;
if (!axis) return;
var type = loc === "top" || loc === "bottom" ? "x" : "y";
var scale = type === "x" ? dataArea.x() : dataArea.y();
var cls = "." + type + ".axis." + loc;
var g = selection.selectAll(cls).data([null]);
g = g.enter().append("g").attr("class", loc + " axis " + type)
.merge(g);
if (transition) g = g.transition();
if (loc === "bottom")
g.attr("transform", "translate(0," + dataArea.y().range()[0] + ")")
if (loc === "right")
g.attr("transform", "translate(" + dataArea.x().range()[1] + ",0)")
g.call(axis.scale(scale));
}
function axes(context) {
doAxis(top, "top", context);
doAxis(bottom, "bottom", context);
doAxis(left, "left", context);
doAxis(right, "right", context);
}
axes.top = function(_) {
return arguments.length ? (top = _, axes) : top;
}
axes.bottom = function(_) {
return arguments.length ? (bottom = _, axes) : bottom;
}
axes.left = function(_) {
return arguments.length ? (left = _, axes) : left;
}
axes.right = function(_) {
return arguments.length ? (right = _, axes) : right;
}
axes.dataArea = function(_) {
return arguments.length ? (dataArea = _, axes) : dataArea;
}
return axes;
}
function defaultAxes(dataArea) {
return axes(dataArea).left(d3.axisLeft()).bottom(d3.axisBottom());
}
return {
dataSeries: dataSeries, assertIsStack: assertIsStack,
dataSeriesCategorical: dataSeriesCategorical, dataSeriesCategoricalStack: dataSeriesCategoricalStack,
dataSeriesTimeSeries: dataSeriesTimeSeries, dataSeriesTimeSeriesStack: dataSeriesTimeSeriesStack,
plotArea: plotArea, dataArea: dataArea,
seriesGroups: seriesGroups,
bars: bars, stack: stack, lines: lines, areas: areas,
axes: axes, defaultAxes: defaultAxes
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment