block by renecnielsen 1c12e91347c4465454ac

Nutrient Parallel Coordinates II

Full Screen

A visualization of the mean nutrient contents of all 8618 foods in USDA’s SR27 Nutrient Database.

Data processed with SQLite and Node.js.

Also see the Nutrient Explorer.

forked from syntagmatic‘s block: Nutrient Parallel Coordinates II

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<title>Nutrient Parallel Coordinates II</title>
<style>
.parcoords {
  display: block;
}

.parcoords svg,
.parcoords canvas {
  font: 9px sans-serif;
  position: absolute;
}

.parcoords canvas {
  opacity: 0.9;
  pointer-events: none;
}

.axis .title {
  font-size: 9px;
  font-weight: bold;
  text-transform: uppercase;
  transform: rotate(-12deg) translate(-5px,-6px);
}

.axis line,
.axis path {
  fill: none;
  stroke: #000;
  stroke-width: 1px;
}

.axis.manufac_name {
  font-size: 7px;
}

.brush .extent {
  fill-opacity: .3;
  stroke: #fff;
  stroke-width: 1px;
}

pre {
  width: 900px;
  height: 160px;
  margin: 6px 12px;
  tab-size: 30;
  font-size: 9px;
  overflow: auto;
}
</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="//bl.ocks.org/syntagmatic/raw/3341641/render-queue.js"></script>
<script>

var margin = {top: 50, right: 150, bottom: 20, left: 120},
    width = 960 - margin.left - margin.right,
    height = 340 - margin.top - margin.bottom;

var color = d3.scale.ordinal()
  .range(["#5DA5B3","#D58323","#DD6CA7","#54AF52","#8C92E8","#E15E5A","#725D82","#776327","#50AB84","#954D56","#AB9C27","#517C3F","#9D5130","#357468","#5E9ACF","#C47DCB","#7D9E33","#DB7F85","#BA89AD","#4C6C86","#B59248","#D8597D","#944F7E","#D67D4B","#8F86C2"]);

var foodgroup = {
  "100": "Dairy and Egg Products",
  "200": "Spices and Herbs",
  "300": "Baby Foods",
  "400": "Fats and Oils",
  "500": "Poultry Products",
  "600": "Soups, Sauces, and Gravies",
  "700": "Sausages and Luncheon Meats",
  "800": "Breakfast Cereals",
  "900": "Fruits and Fruit Juices",
  "1000": "Pork Products",
  "1100": "Vegetables and Vegetable Products",
  "1200": "Nut and Seed Products",
  "1300": "Beef Products",
  "1400": "Beverages",
  "1500": "Finfish and Shellfish Products",
  "1600": "Legumes and Legume Products",
  "1700": "Lamb, Veal, and Game Products",
  "1800": "Baked Products",
  "1900": "Sweets",
  "2000": "Cereal Grains and Pasta",
  "2100": "Fast Foods",
  "2200": "Meals, Entrees, and Side Dishes",
  "2500": "Snacks",
  "3500": "American Indian/Alaska Native Foods",
  "3600": "Restaurant Foods"
}

var types = {
  "Number": {
    key: "Number",
    coerce: function(d) { return +d; },
    extent: d3.extent,
    within: function(d, extent) { return extent[0] <= d && d <= extent[1]; },
    defaultScale: d3.scale.linear().range([height, 0])
  },
  "String": {
    key: "String",
    coerce: String,
    extent: function (data) { return data.sort(); },
    within: function(d, extent, dim) { return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1]; },
    defaultScale: d3.scale.ordinal().rangePoints([0, height])
  },
  "Date": {
    key: "Date",
    coerce: function(d) { return new Date(d); },
    extent: d3.extent,
    within: function(d, extent) { return extent[0] <= d && d <= extent[1]; },
    defaultScale: d3.time.scale().range([0, height])
  }
};

var dimensions = [
  {
    key: "manufac_name",
    description: "Manufacturer", 
    type: types["String"],
    axis: d3.svg.axis().orient("left")
      .tickFormat(function(d,i) { return i % 2 == 0 ? d.slice(0,28) : ""; })
  },
  {
    key: "Protein (g)",
    type: types["Number"],
    domain: [0,100]
  },
  {
    key: "Fiber, total dietary (g)",
    type: types["Number"],
    domain: [0,100]
  },
  {
    key: "Total lipid (fat) (g)",
    type: types["Number"],
    domain: [0,100]
  },
  {
    key: "Sugars, total (g)",
    type: types["Number"],
    domain: [0,100]
  },
  {
    key: "Energy (kcal)",
    type: types["Number"]
  },
  {
    key: "Water (g)",
    type: types["Number"]
  },
  {
    key: "Caffeine (mg)",
    type: types["Number"]
  },
  {
    key: "Calcium, Ca (mg)",
    type: types["Number"]
  },
  {
    key: "Potassium, K (mg)",
    type: types["Number"]
  },
  {
    key: "Sodium, Na (mg)",
    type: types["Number"]
  },
  {
    key: "Magnesium, Mg (mg)",
    type: types["Number"]
  },
  {
    key: "Vitamin C, total ascorbic acid (mg)",
    type: types["Number"]
  },
  {
    key: "food_group_id",
    description: "Food Group",
    type: types["String"],
    axis: d3.svg.axis().orient("right")
      .tickFormat(function(d) { return foodgroup[d]; })
  }
];

var xscale = d3.scale.ordinal()
    .domain(d3.range(dimensions.length))
    .rangePoints([0, width]);

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

var container = d3.select("body").append("div")
    .attr("class", "parcoords")
    .style("width", width + margin.left + margin.right + "px")
    .style("height", height + margin.top + margin.bottom + "px");

var svg = container.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 + ")");

var canvas = container.append("canvas")
    .attr("width", width + 1)
    .attr("height", height + 1)
    .style("margin-top", margin.top + "px")
    .style("margin-left", margin.left + "px");

var ctx = canvas.node().getContext("2d");
ctx.globalAlpha = 0.15;
ctx.lineWidth = 1.5;

var output = d3.select("body").append("pre");

var axes = svg.selectAll(".axis")
    .data(dimensions)
  .enter().append("g")
    .attr("class", function(d) { return "axis " + d.key; })
    .attr("transform", function(d,i) { return "translate(" + xscale(i) + ")"; });

d3.csv("nutrient.csv", function(error, data) {
  if (error) throw error;

  var dimset = d3.set(dimensions.map(function(d) { return d.key }).concat(["long_desc"]));

  data.forEach(function(d) {
    dimensions.forEach(function(p) {
      d[p.key] = !d[p.key] ? null : p.type.coerce(d[p.key]);
    });

    d3.keys(d).forEach(function(k) {
      if (!dimset.has(k)) delete d[k];
    });

    d.long_desc = d.long_desc.slice(0,29);
  });

  // type/dimension default setting happens here
  dimensions.forEach(function(dim) {
    if (!("domain" in dim)) {
      // detect domain using dimension type's extent function
      dim.domain = d3.functor(dim.type.extent)(data.map(function(d) { return d[dim.key]; }));

      // TODO - this line only works because the data encodes data with integers
      // Sorting/comparing should be defined at the type/dimension level
      dim.domain.sort(function(a,b) {
        return a - b;
      });
    }
    if (!("scale" in dim)) {
      // use type's default scale for dimension
      dim.scale = dim.type.defaultScale.copy();
    }
    dim.scale.domain(dim.domain);
  });

  var render = renderQueue(draw).rate(30);

  ctx.clearRect(0,0,width+1,height+1);
  ctx.globalAlpha = d3.min([1.8/Math.pow(data.length,0.3),1]);
  render(data);

  axes.append("g")
      .each(function(d) {
        var renderAxis = "axis" in d
          ? d.axis.scale(d.scale)  // custom axis
          : yAxis.scale(d.scale);  // default axis
        d3.select(this).call(renderAxis);
      })
    .append("text")
      .attr("class", "title")
      .attr("text-anchor", "start")
      .text(function(d) { return "description" in d ? d.description : d.key; });

  // Add and store a brush for each axis.
  axes.append("g")
      .attr("class", "brush")
      .each(function(d) {
        d3.select(this).call(d.brush = d3.svg.brush()
          .y(d.scale)
          .on("brushstart", brushstart)
          .on("brush", brush));
      })
    .selectAll("rect")
      .attr("x", -8)
      .attr("width", 16);

  output.text(d3.tsv.format(data.slice(0,20)));

  function project(d) {
    return dimensions.map(function(p,i) {
      if (d[p.key] === null) return null;
      return [xscale(i),p.scale(d[p.key])];
    });
  };

  function draw(d) {
    ctx.strokeStyle = color(d.food_group_id);
    ctx.beginPath();
    var coords = project(d);
    coords.forEach(function(p,i) {
      // this tricky bit avoids rendering null values as 0
      if (p === null) {
        if (i < coords.length-1) {
          var next = coords[i+1];
          if (next !== null) {
            // the 3 pixel backstep makes little marks show up for points sandwiched by nulls 
            ctx.moveTo(next[0]-3,next[1]);
          }
        }
        return;
      }
      
      if (i == 0) {
        ctx.moveTo(p[0],p[1]);
        return;
      }

      ctx.lineTo(p[0],p[1]);
    });
    ctx.stroke();
  }

  function brushstart() {
    d3.event.sourceEvent.stopPropagation();
  }

  // Handles a brush event, toggling the display of foreground lines.
  function brush() {
    var actives = dimensions.filter(function(p) { return !p.brush.empty(); }),
        extents = actives.map(function(p) { return p.brush.extent(); });

    var selected = data.filter(function(d) {
      if (actives.every(function(dim, i) {
          // test if point is within extents for each active brush
          return dim.type.within(d[dim.key], extents[i], dim);
        })) {
        return true;
      }
    });

    ctx.clearRect(0,0,width+1,height+1);
    ctx.globalAlpha = d3.min([1.8/Math.pow(selected.length,0.3),1]);
    render(selected);

    output.text(d3.tsv.format(selected.slice(0,20)));
  }
});

</script>