xkcd-style Pollster charts
Code via http://dan.iel.fm/xkcd
xkcd-style Pollster charts
Code via http://dan.iel.fm/xkcd
| <!DOCTYPE HTML> | |
| <html> | |
| <head> | |
| <title>xkcd-style Pollster plots in d3</title> | |
| <script src="http://code.jquery.com/jquery.min.js"></script> | |
| <script src="http://underscorejs.org/underscore-min.js"></script> | |
| <script src="http://d3js.org/d3.v2.min.js?2.10.0"></script> | |
| <script src="pollster-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.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; | |
| } | |
| #chart { | |
| margin-top: 50px; | |
| } | |
| #slug { | |
| width: 300px; | |
| } | |
| #header { | |
| font-family: Helvetica, Arial, sans-serif; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <label for="slug">Chart</label> | |
| <select id="slug"> | |
| <option value="2012-general-election-romney-vs-obama">2012 General Election: Romney vs. Obama</option> | |
| <option value="obama-favorable-rating">Barack Obama Favorable Rating</option> | |
| <option value="mitt-romney-favorability">Mitt Romney Favorable Rating</option> | |
| <option value="party-identification-adults">Party Identification - Adults</option> | |
| <option value="2012-virginia-senate-allen-vs-kaine">2012 Virginia Senate: Allen vs. Kaine</option> | |
| <option value="2012-missouri-senate-mccaskill-vs-akin">2012 Missouri Senate: McCaskill vs. Akin</option> | |
| <option value="2012-massachusetts-senate-brown-vs-warren">2012 Massachusetts Senate: Brown vs Warren</option> | |
| </select> | |
| <div id="chart"></div> | |
| <script> | |
| var makeGraph = function (slug) { | |
| var url = 'http://elections.huffingtonpost.com/pollster/api/charts/' + slug; | |
| // Add the lines. | |
| $.ajax(url, | |
| { | |
| dataType: 'jsonp', | |
| jsonpCallback: 'pollsterCallback', | |
| cache: true, | |
| success: function (data) { | |
| // Build the plot. | |
| var plot = xkcdplot({ | |
| title: data.title | |
| }); | |
| var choices = {}; | |
| var parties = {}; | |
| var colors = { | |
| 'Dem': '#5189b8', | |
| 'Democrat': '#5189b8', | |
| 'Rep': 'red', | |
| 'Republican': 'red', | |
| 'Independent': 'green', | |
| 'Favorable': 'black', | |
| 'Unfavorable': 'red', | |
| }; | |
| var ignore = [ | |
| 'Other', | |
| 'Undecided' | |
| ]; | |
| _(data.estimates).each(function(choice) { | |
| if (_(ignore).indexOf(choice.choice) === -1) { | |
| choices[choice.choice] = []; | |
| parties[choice.choice] = choice.party; | |
| } | |
| }); | |
| plot("#chart"); | |
| var estimates = data.estimates_by_date.reverse(); | |
| _(estimates).each(function(estimate, i) { | |
| _(estimate.estimates).each(function (est) { | |
| if (choices[est.choice]) { | |
| choices[est.choice].push({x: i, y: est.value}); | |
| } | |
| }); | |
| }); | |
| _(choices).each(function(data, choice) { | |
| plot.plot(data, {stroke: colors[parties[choice]] || colors[choice] || 'gray'}); | |
| }); | |
| // Render the image. | |
| plot.draw(); | |
| } | |
| }); | |
| } | |
| makeGraph($("#slug").val()); | |
| $("#slug").bind('change', function(e) { | |
| $("#chart").html(''); | |
| makeGraph($("#slug").val()); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
| function xkcdplot(opts) { | |
| // Default parameters. | |
| var width = 600, | |
| height = 300, | |
| margin = 40, | |
| arrowSize = 12, | |
| arrowAspect = 0.4, | |
| arrowOffset = 6, | |
| magnitude = 0.003, | |
| xlabel = "Date", | |
| ylabel = "HuffPost Model Estimate", | |
| title = opts.title, | |
| 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, 100]); | |
| // 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; | |
| } |