block by syntagmatic 2297029

Nutrient Scatterplot

Full Screen

index.html

<!doctype html>

<link rel=stylesheet href=style.css />

<h1>Nutrient Scatterplot</h1>
<svg id="chart"></svg>
<div id="controls">
  X <select id="xaxis"></select><br/>
  Y <select id="yaxis"></select><br/>
  Force Layout <button class="fire" value="f">f</button>
</div>

<script src="//documentcloud.github.com/underscore/underscore-min.js"></script>
<script src="//zeptojs.com/zepto.min.js"></script>
<script src="//keithcirkel.co.uk/jwerty/jwerty.js"></script>
<script src="//mbostock.github.com/d3/d3.v2.js"></script>
<script src="scatter.js"></script>
<script src="force.js"></script>

<script>
// buttons that fire key combinations
$('button.fire').on('click', function() {
  jwerty.fire( $(this).val() );
});

// key combinations
jwerty.key('f', force_layout);
</script>

force.js

// adapted from http://bl.ocks.org/1747543
function force_layout() {
  var width = 500,
      height = 400,
      padding = 3;

  var circle = svg.selectAll("circle");

  var nodes = circle
      .each(function(d) {
        var my = d3.select(this);
        d.radius = parseFloat(my.attr("r"));
        d.color = my.attr("fill");
        d.x = parseFloat(my.attr("cx")) + Math.random();
        d.y = parseFloat(my.attr("cy")) + Math.random();
      })
      .data();

  var force = d3.layout.force()
      .nodes(nodes)
      .size([width, height])
      .gravity(.02)
      .charge(0)
      .on("tick", tick)
      .start();

  circle.call(force.drag);

  function tick(e) {
    circle
        .each(cluster(10 * e.alpha * e.alpha))
        .each(collide(.5))
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
  }

  // Move d to be adjacent to the cluster node.
  function cluster(alpha) {
    var max = {};

    // Find the largest node for each cluster.
    nodes.forEach(function(d) {
      if (!(d.color in max) || (d.radius > max[d.color].radius)) {
        max[d.color] = d;
      }
    });

    return function(d) {
      var node = max[d.color],
          l,
          r,
          x,
          y,
          i = -1;

      if (node == d) return;

      x = d.x - node.x;
      y = d.y - node.y;
      l = Math.sqrt(x * x + y * y);
      r = d.radius + node.radius;
      if (l != r) {
        l = (l - r) / l * alpha;
        d.x -= x *= l;
        d.y -= y *= l;
        node.x += x;
        node.y += y;
      }
    };
  }

  // Resolves collisions between d and all other circles.
  function collide(alpha) {
    var quadtree = d3.geom.quadtree(nodes);
    return function(d) {
      var r = d.radius + padding,
          nx1 = d.x - r,
          nx2 = d.x + r,
          ny1 = d.y - r,
          ny2 = d.y + r;
      quadtree.visit(function(quad, x1, y1, x2, y2) {
        if (quad.point && (quad.point !== d)) {
          var x = d.x - quad.point.x,
              y = d.y - quad.point.y,
              l = Math.sqrt(x * x + y * y),
              r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
          if (l < r) {
            l = (l - r) / l * alpha;
            d.x -= x *= l;
            d.y -= y *= l;
            quad.point.x += x;
            quad.point.y += y;
          }
        }
        return x1 > nx2
            || x2 < nx1
            || y1 > ny2
            || y2 < ny1;
      });
    };
  }
};

scatter.js

// select svg canvas
var m = [30, 10, 10, 10],      // margins
    w = 500-m[1]-m[3],         // width
    h = 400-m[0]-m[2],         // height
    xcol = 0,                  // active x column
    ycol = 1;                  // active y column

// create svg element, adjusted by margins
svg = d3.select('#chart')
  .append("g")
  .attr("transform", "translate(" + m[1] + "," + m[0] + ")");

// load data from csv file
d3.csv('nutrients.csv', function(data) {

  // get columns of csv, mark excluded columns
  var columns = d3.keys(data[0]),
      excluded = ['name', 'group', 'id'];

  // get quantitative dimensions
  var dimensions = _(columns)
    .difference(excluded);

  // extents for each dimension
  var extents = _(dimensions)
    .map(function(col) {
      return [0, d3.max(data, function(d) { return parseFloat(d[col]) })]
    });

  // create scales
  var x = d3.scale.linear().domain(extents[xcol]).range([0, w]),
      y = d3.scale.linear().domain(extents[ycol]).range([h, 0]);

  // color scale
  var color = {
    "Baby Foods"                        : '#555555',
    "Baked Products"                    : '#7f7f7f',
    "Beverages"                         : '#c49c94',
    "Breakfast Cereals"                 : '#9467bd',
    "Cereal Grains and Pasta"           : '#bcbd22',
    "Dairy and Egg Products"            : '#ff7f0e',
    "Ethnic Foods"                      : '#e7ba52',
    "Fast Foods"                        : '#dbdb8d',
    "Fats and Oils"                     : '#ffbb78',
    "Finfish and Shellfish Products"    : '#e377c2',
    "Fruits and Fruit Juices"           : '#c5b0d5',
    "Legumes and Legume Products"       : '#f7b6d2',
    "Meals, Entrees, and Sidedishes"    : '#17becf',
    "Nut and Seed Products"             : '#8c564b',
    "Pork Products"                     : "#00ee99",
    "Poultry Products"                  : '#d62728',
    "Restaurant Foods"                  : '#1f77b4',
    "Sausages and Luncheon Meats"       : '#ff9896',
    "Snacks"                            : '#9edae5',
    "Soups, Sauces, and Gravies"        : '#98df8a',
    "Spices and Herbs"                  : '#aec7e8',
    "Sweets"                            : '#c7c7c7',
    "Vegetables and Vegetable Products" : '#2ca02c'
  };

  // bind data to chart
  svg.selectAll('circle')
     .data(data)
   .enter().append('circle')
     .attr("fill", function(d) { return color[d.group]; })
     .attr("cx", function(d) { return x(d[dimensions[xcol]]) || 0; })
     .attr("cy", function(d) { return y(d[dimensions[ycol]]) || h; })
     .attr("r", 2.5);

  // change x axis
  function xaxis(i) {
    x.domain(extents[i]);
    svg.selectAll('circle')
      .transition()
      .duration(1500)
      .attr("cx", function(d) { return x(d[dimensions[i]]) || 0; });
  };

  // change y axis
  function yaxis(i) {
    y.domain(extents[i]);
    svg.selectAll('circle')
      .transition()
      .duration(1500)
      .attr("cy", function(d) { return y(d[dimensions[i]]) || h; })
  };

  // create dropdowns to change axes
  d3.select("#xaxis")
    .selectAll("option")
    .data(dimensions)
    .enter().append("option")
      .attr("value", function(d,i) { return i; })
      .text(function(d) { return d; })
      .each(function(d,i) {
        if (i == xcol) d3.select(this).attr("selected", "yes");
      });

  d3.select("#xaxis")
      .on("change", function() { xaxis(this.selectedIndex) });

  d3.select("#yaxis")
    .selectAll("option")
    .data(dimensions)
    .enter().append("option")
      .attr("value", function(d,i) { return i; })
      .text(function(d) { return d; })
      .each(function(d,i) {
        if (i == ycol) d3.select(this).attr("selected", "yes");
      });

  d3.select("#yaxis")
      .on("change", function() { yaxis(this.selectedIndex) });

  window.data = data;
});

style.css

body {
  background: #222;
  color: #eee;
  font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
}
circle {
  stroke-width: 1px;
  stroke: #111;
  stroke-opacity: 0.5;
}
#chart {
  width: 500px;
  height: 400px;
  background: #151515;
  border-radius: 4px;
  border: 1px solid #333;
  float: left;
  clear: left;
}
#controls {
  float: left;
  margin: 0 20px;
}