Skip to content

Instantly share code, notes, and snippets.

@johnpaulashenfelter
Forked from dfm/LICENSE
Created October 19, 2012 13:16
Show Gist options
  • Save johnpaulashenfelter/3918175 to your computer and use it in GitHub Desktop.
Save johnpaulashenfelter/3918175 to your computer and use it in GitHub Desktop.
XKCD-style plots in d3
<!DOCTYPE HTML>
<html>
<head>
<title>XKCD plots in d3</title>
<script src="http://d3js.org/d3.v2.min.js?2.10.0"></script>
<script src="xkcd.js"></script>
<style>
@font-face {
font-family: "xkcd";
src: url('http://antiyawn.com/uploads/Humor-Sans.ttf');
}
body {
font-family: "xkcd", sans-serif;
font-size: 16px;
color: #333;
text-align: center;
margin-top: 75px;
}
text.title {
font-size: 20px;
}
path {
fill: none;
stroke-width: 2.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
path.axis {
stroke: black;
}
path.bgline {
stroke: white;
stroke-width: 6px;
}
</style>
</head>
<body>
<script>
// Generate some data.
function f1 (x) {
return Math.exp(-0.5 * (x - 1) * (x - 1)) * Math.sin(x + 0.2) + 0.05;
}
function f2 (x) {
return 0.5 * Math.cos(x - 0.5) + 0.1;
}
var xmin = -1.0,
xmax = 7,
N = 100,
data = d3.range(xmin, xmax, (xmax - xmin) / N).map(function (d) {
return {x: d, y: f1(d)};
})
data2 = d3.range(xmin, xmax, (xmax - xmin) / N).map(function (d) {
return {x: d, y: f2(d)};
});
// Build the plot.
var plot = xkcdplot();
plot("body");
// Add the lines.
plot.plot(data);
plot.plot(data2, {stroke: "red"});
// Render the image.
plot.xlim([-1.5, 7.5]).draw();
</script>
</body>
</html>
function xkcdplot() {
// Default parameters.
var width = 600,
height = 300,
margin = 20,
arrowSize = 12,
arrowAspect = 0.4,
arrowOffset = 6,
magnitude = 0.003,
xlabel = "Time of Day",
ylabel = "Awesomeness",
title = "The most important graph ever made",
xlim,
ylim;
// Plot elements.
var el,
xscale = d3.scale.linear(),
yscale = d3.scale.linear();
// Plotting functions.
var elements = [];
// The XKCD object itself.
var xkcd = function (nm) {
el = d3.select(nm).append("svg")
.attr("width", width + 2 * margin)
.attr("height", height + 2 * margin)
.append("g")
.attr("transform", "translate(" + margin + ", "
+ margin + ")");
return xkcd;
};
// Getters and setters.
xkcd.xlim = function () {
if (!arguments.length) return xlim;
xlim = arguments[0];
return xkcd;
};
// Do the render.
xkcd.draw = function () {
// Set the axes limits.
xscale.domain(xlim).range([0, width]);
yscale.domain(ylim).range([height, 0]);
// Compute the zero points where the axes will be drawn.
var x0 = xscale(0),
y0 = yscale(0);
// Draw the axes.
var axis = d3.svg.line().interpolate(xinterp);
el.selectAll(".axis").remove();
el.append("svg:path")
.attr("class", "x axis")
.attr("d", axis([[0, y0], [width, y0]]));
el.append("svg:path")
.attr("class", "y axis")
.attr("d", axis([[x0, 0], [x0, height]]));
// Laboriously draw some arrows at the ends of the axes.
var aa = arrowAspect * arrowSize,
o = arrowOffset,
s = arrowSize;
el.append("svg:path")
.attr("class", "x axis arrow")
.attr("d", axis([[width - s + o, y0 + aa], [width + o, y0], [width - s + o, y0 - aa]]));
el.append("svg:path")
.attr("class", "x axis arrow")
.attr("d", axis([[s - o, y0 + aa], [-o, y0], [s - o, y0 - aa]]));
el.append("svg:path")
.attr("class", "y axis arrow")
.attr("d", axis([[x0 + aa, s - o], [x0, -o], [x0 - aa, s - o]]));
el.append("svg:path")
.attr("class", "y axis arrow")
.attr("d", axis([[x0 + aa, height - s + o], [x0, height + o], [x0 - aa, height - s + o]]));
for (var i = 0, l = elements.length; i < l; ++i) {
var e = elements[i];
e.func(e.data, e.x, e.y, e.opts);
}
// Add some axes labels.
el.append("text").attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", width - s)
.attr("y", y0 + aa)
.attr("dy", ".75em")
.text(xlabel);
el.append("text").attr("class", "y label")
.attr("text-anchor", "end")
.attr("x", aa)
.attr("y", x0)
.attr("dy", "-.75em")
.attr("transform", "rotate(-90)")
.text(ylabel);
// And a title.
el.append("text").attr("class", "title")
.attr("text-anchor", "end")
.attr("x", width)
.attr("y", 0)
.text(title);
return xkcd;
};
// Adding plot elements.
xkcd.plot = function (data, opts) {
var x = function (d) { return d.x; },
y = function (d) { return d.y; },
cx = function (d) { return xscale(x(d)); },
cy = function (d) { return yscale(y(d)); },
xl = d3.extent(data, x),
yl = d3.extent(data, y);
// Rescale the axes.
xlim = xlim || xl;
xlim[0] = Math.min(xlim[0], xl[0]);
xlim[1] = Math.max(xlim[1], xl[1]);
ylim = ylim || yl;
ylim[0] = Math.min(ylim[0], yl[0]);
ylim[1] = Math.max(ylim[1], yl[1]);
// Add the plotting function.
elements.push({
data: data,
func: lineplot,
x: cx,
y: cy,
opts: opts
});
return xkcd;
};
// Plot styles.
function lineplot(data, x, y, opts) {
var line = d3.svg.line().x(x).y(y).interpolate(xinterp),
bgline = d3.svg.line().x(x).y(y),
strokeWidth = _get(opts, "stroke-width", 3),
color = _get(opts, "stroke", "steelblue");
el.append("svg:path").attr("d", bgline(data))
.style("stroke", "white")
.style("stroke-width", 2 * strokeWidth + "px")
.style("fill", "none")
.attr("class", "bgline");
el.append("svg:path").attr("d", line(data))
.style("stroke", color)
.style("stroke-width", strokeWidth + "px")
.style("fill", "none");
};
// XKCD-style line interpolation. Roughly based on:
// jakevdp.github.com/blog/2012/10/07/xkcd-style-plots-in-matplotlib
function xinterp (points) {
// Scale the data.
var f = [xscale(xlim[1]) - xscale(xlim[0]),
yscale(ylim[1]) - yscale(ylim[0])],
z = [xscale(xlim[0]),
yscale(ylim[0])],
scaled = points.map(function (p) {
return [(p[0] - z[0]) / f[0], (p[1] - z[1]) / f[1]];
});
// Compute the distance along the path using a map-reduce.
var dists = scaled.map(function (d, i) {
if (i == 0) return 0.0;
var dx = d[0] - scaled[i - 1][0],
dy = d[1] - scaled[i - 1][1];
return Math.sqrt(dx * dx + dy * dy);
}),
dist = dists.reduce(function (curr, d) { return d + curr; }, 0.0);
// Choose the number of interpolation points based on this distance.
var N = Math.round(200 * dist);
// Re-sample the line.
var resampled = [];
dists.map(function (d, i) {
if (i == 0) return;
var n = Math.max(3, Math.round(d / dist * N)),
spline = d3.interpolate(scaled[i - 1][1], scaled[i][1]),
delta = (scaled[i][0] - scaled[i - 1][0]) / (n - 1);
for (var j = 0, x = scaled[i - 1][0]; j < n; ++j, x += delta)
resampled.push([x, spline(j / (n - 1))]);
});
// Compute the gradients.
var gradients = resampled.map(function (a, i, d) {
if (i == 0) return [d[1][0] - d[0][0], d[1][1] - d[0][1]];
if (i == resampled.length - 1)
return [d[i][0] - d[i - 1][0], d[i][1] - d[i - 1][1]];
return [0.5 * (d[i + 1][0] - d[i - 1][0]),
0.5 * (d[i + 1][1] - d[i - 1][1])];
});
// Normalize the gradient vectors to be unit vectors.
gradients = gradients.map(function (d) {
var len = Math.sqrt(d[0] * d[0] + d[1] * d[1]);
return [d[0] / len, d[1] / len];
});
// Generate some perturbations.
var perturbations = smooth(resampled.map(d3.random.normal()), 3);
// Add in the perturbations and re-scale the re-sampled curve.
var result = resampled.map(function (d, i) {
var p = perturbations[i],
g = gradients[i];
return [(d[0] + magnitude * g[1] * p) * f[0] + z[0],
(d[1] - magnitude * g[0] * p) * f[1] + z[1]];
});
return result.join("L");
}
// Smooth some data with a given window size.
function smooth(d, w) {
var result = [];
for (var i = 0, l = d.length; i < l; ++i) {
var mn = Math.max(0, i - 5 * w),
mx = Math.min(d.length - 1, i + 5 * w),
s = 0.0;
result[i] = 0.0;
for (var j = mn; j < mx; ++j) {
var wd = Math.exp(-0.5 * (i - j) * (i - j) / w / w);
result[i] += wd * d[j];
s += wd;
}
result[i] /= s;
}
return result;
}
// Get a value from an object or return a default if that doesn't work.
function _get(d, k, def) {
if (typeof d === "undefined") return def;
if (typeof d[k] === "undefined") return def;
return d[k];
}
return xkcd;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment