|
|
@@ -0,0 +1,247 @@ |
|
|
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; |
|
|
|
|
|
} |