Last active
October 17, 2017 19:21
-
-
Save danielkeller/5a4d8135c6d0fddca65a1ff991e1911c to your computer and use it in GitHub Desktop.
Simple approach to plotting
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
height: 520 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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