block by syntagmatic 4054247

Producer Price Indexes

Full Screen

Uses constraint relaxation to calculate new label positions, then transitions when no more adjustments are needed.

Also see the average prices.

index.html

<!DOCTYPE html>
<title>Constraint Relaxation 2</title>
<meta charset="utf-8">
<style>
body {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.3px;
  -webkit-transition: opacity 0.3s;
  -moz-transition: opacity 0.3s;
  -o-transition: opacity 0.3s;
  transition: opacity 0.3s;
}

path.invisible {
  fill: none;
  stroke: rgba(0,0,0,0);
  stroke-width: 8px;
}

text.label {
  font-size: 12px;
}

</style>

<script src="//mbostock.github.com/d3/d3.v2.js"></script>

<script src="//documentcloud.github.com/underscore/underscore.js"></script>

<body></body>
<script type="text/javascript">
var margin = {top: 20, right: 300, bottom: 30, left: 50},
    width = 1000 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;


var format = d3.time.format("%b %Y");

var start = format.parse("Jan 1984");
var end = format.parse("Aug 2012");
var range = d3.time.months(start,end);

var x = d3.time.scale()
    .range([0,width])
    .domain([start,end]);

var y = d3.scale.linear()
    .range([height, 0]);

var color = function(i) {
  return d3.hcl(55*i,55,38).toString();
};

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var line = d3.svg.line()
    .interpolate("basis")
    .defined(function(d) { return d != null; })
    .x(function(d,i) { return x(range[i]); })
    .y(function(d) { return y(d); });

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.csv("producer-prices.csv", function(raw) {
  var data = [];
  var goods = _(raw).pluck("Name");

  _(raw).each(function(series) {
    var value = {};
    data.push({
      id: series["Series ID"],
      name: series["Name"],
      values: _(range).map(function(month) {
                return parseFloat(series[format(month)]) || null;
              })
    });
  });

  var values = _(data).chain().pluck('values').flatten().value();

  y.domain([
    0,
    d3.max(values)
  ]);


  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .style("font-weight", "bold")
      .text("Producer Price Index");

  var series = svg.selectAll(".series")
      .data(data)
    .enter().append("g")
      .attr("class", "series");

  series.append("path")
      .attr("class", "line")
      .attr("d", function(d) { return line(d.values); })
      .style("stroke", function(d,i) { return color(i); });

  series.append("path")
      .attr("class", "invisible hover")
      .attr("d", function(d) { return line(d.values); });

  var labels = data.map(function(d) { return {name: d.name, y: y(d.values[d.values.length - 1])}});

  series.append("text")
      .attr("class", "label hover")
      .data(labels)
      .attr("transform", function(d) { return "translate(" + x(end) + "," + d.y + ")"; })
      .attr("x", 3)
      .attr("dy", ".35em")
      .style("fill", function(d,i) { return color(i); })
      .text(function(d) { return d.name; });

  // constraint relaxation on labels
  var alpha = 1.5;
  var spacing = 12;
  function relax() {
    var again = false;
    labels.forEach(function(a,i) {
      labels.slice(i+1).forEach(function(b) {
        var dy = a.y - b.y;
        if (Math.abs(dy) < spacing) {
          again = true;
          var sign = dy > 0 ? 1 : -1;
          a.y += sign*alpha;
          b.y -= sign*alpha;
        }
      });
    });

    /* uncomment to see each step */
    // d3.selectAll(".label")
    //    .attr("transform", function(d) { return "translate(" + x(end) + "," + d.y + ")"; });

    if (!again) {
      // all done
      d3.selectAll(".label")
        .transition().duration(800)
        .attr("transform", function(d) { return "translate(" + x(end) + "," + d.y + ")"; });
      return true;
    }
    return false;
  };

  d3.timer(relax);

  series.selectAll(".hover")
      .on("mouseover", function(d,i) {
        d3.selectAll(".line")
          .style("opacity", 0.12)
          .filter(function(p) { return p.name == d.name; })
          .style("opacity", 1)
          .style("stroke-width", 2.5);
        d3.selectAll(".series text")
          .style("font-weight", function(p,i) { return p.name == d.name ? "bold" : null; })
          .style("fill", function(p,i) { return p.name == d.name ? color(i) : "#aaa"; });
      })
      .on("mouseout", function(d,i) {
        d3.selectAll(".line")
          .style("opacity", 1)
          .style("stroke-width", null);
        d3.selectAll(".series text")
          .style("font-weight", null)
          .style("fill", function(d,i) { return color(i); });
      });
});
</script>