block by bycoffe 3924854

xkcd-style Pollster charts in d3

Full Screen

xkcd-style Pollster charts

Code via http://dan.iel.fm/xkcd

Data via http://elections.huffingtonpost.com/pollster/api

index.html

<!DOCTYPE HTML>
<html>
<head>
    <title>xkcd-style Pollster plots in d3</title>

    <script src="//code.jquery.com/jquery.min.js"></script>
    <script src="//underscorejs.org/underscore-min.js"></script>
    <script src="//d3js.org/d3.v2.min.js?2.10.0"></script>
    <script src="pollster-xkcd.js"></script>

<style>

@font-face {
    font-family: "xkcd";
    src: url('//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 = '//elections.huffingtonpost.com/pollster/api/charts/' + slug + '.jsonp';

        // 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>

pollster-xkcd.js

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;

}