block by syntagmatic 3150058

Parallel Coordinates with Invertible Axes

Full Screen

404: Not Found

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <title>Invertible Parallel Coordinates</title>
    <style type="text/css">

html, body {
  margin: 0;
  width: 100%;
  height: 100%;
  padding: 0;
}
body { 
  font-family: sans-serif;
  font-size: 13px;
  line-height: 1.4em;
  background: #f9f9f9;
  color: #333;
}

body.dark {
  background: #070707;
  color: #e3e3e3;
}

#wrap {
  padding: 0 0.5%;
}

svg {
  font: 10px sans-serif;
}

canvas, svg {
  position: absolute;
  top: 0;
  left: 0;
}

#chart {
  position: relative;
}

.brush rect.extent {
  fill: rgba(255,255,255,0.2);
  stroke: #777;
}

.dark .brush rect.extent {
  fill: rgba(0,0,0,0.2);
  stroke: #aaa;
}

.resize rect {
  fill: none;
}

.background {
  fill: rgba(255,255,255,0.3);
  stroke: rgba(255,255,255,0.4);
}
.dark .background {
  fill: rgba(0,0,0,0.3);
  stroke: rgba(0,0,0,0.4);
}

.axis g {
  pointer-events: none;
}

.axis line, .axis path {
  display: none;
  fill: none;
  stroke: rgba(180,180,180,1);
  shape-rendering: crispEdges;
}

.axis .tick {
  width: 200px;
}

.axis text {
  fill: #222;
  text-anchor: middle;
  font-size: 10px;
  text-shadow: 0 1px 1px #fff, 1px 0 1px #fff, 0 -1px 1px #fff, -1px 0 1px #fff;
}

.axis text.label {
  fill: #333;
  font-weight: normal;
  font-size: 13px;
  cursor: move;
}

.dark .axis text {
  fill: #f2f2f2;
  text-shadow: 0 1px 0 #000, 1px 0 0 #000, 0 -1px 0 #000, -1px 0 0 #000;
}

.dark .axis text.label {
  fill: #ddd;
}

.quarter, .third, .half {
  float: left;
}

.quarter {
  width: 23%;
  margin: 0 1%;
}

.third {
  width: 31%;
  margin: 0 1%;
}

.half {
  width: 48%;
  margin: 0 1%;
}

h3 {
  margin: 0.9em 0 0.6em;
}

p {
  margin: 0.6em 0;
}

#food-list {
  width: 100%;
  height: 460px;
  overflow-x: auto;
  overflow-y: auto;
  background: #f8f8f8;
  white-space: nowrap;
}
#legend {
  text-align: right;
  overflow-y: auto;
  height: 460px;
}
.dark #food-list {
  background: #0b0b0b;
}
.color-block {
  display: inline-block;
  height: 6px;
  width: 6px;
  margin: 2px 4px;
}
#rendered-bar,
#selected-bar {
  width:0%;
  font-weight: bold;
}
#rendered-bar {
  border-right: 1px solid rgba(120,120,120,1);
  background: rgba(120,120,120,0.6);
}
#selected-bar {
  border-right: 1px solid rgba(120,120,120,0.5);
  background: rgba(120,120,120,0.4);
}
.fillbar {
  height: 12px;
  line-height: 12px;
  border:1px solid rgba(120,120,120,0.5);
}
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
 
::-webkit-scrollbar-track {
  background: #ddd;
  border-radius: 12px;
}
 
::-webkit-scrollbar-thumb {
  background: #b5b5b5;
  border-radius: 12px;
}
.dark ::-webkit-scrollbar-track {
  background: #222;
}
.dark ::-webkit-scrollbar-thumb {
  background: #444;
}
    </style>
  </head>
  <body>
  <div id="chart">
    <canvas id="foreground"></canvas>
    <svg></svg>
  </div>
  <div id="wrap">
    <div  class="third">
      <h3>Legend</h3>
      <p id="legend">
      </p>
    </div>
    <div class="third" id="controls">

      <h3>Rendering Progress</h3>
      <small>
        <strong id="data-count"></strong> entries, <strong id="selected-count"></strong> selected, <strong id="rendered-count"></strong> rendered
        <p>
          <div class="fillbar"><div id="selected-bar"><div id="rendered-bar">&nbsp;</div></div></div>
          Lasted rendered <strong id="render-speed"></strong> lines at <strong id="opacity"></strong> opacity.
        </p>
      </small>


      <h3>Controls</h3>
      <p>
        <strong>Brush</strong>: Drag vertically along an axis.<br/>

        <strong>Remove Brush</strong>: Tap the axis background.<br/>

        <strong>Reorder Axes</strong>: Drag a label horizontally.<br/>

        <strong>Invert Axis</strong>: Tap an axis label.<br/>
      </p>
      <p>
        Permanently filter the dataset with these buttons.<br/>
        Reload the page to start over.
      </p>
      <button id="keep-data">Keep</button>
      <button id="exclude-data">Exclude</button><br/>

      <h3>Appearance</h3>
      <button id="hide-ticks">Hide Ticks</button>
      <button id="show-ticks">Show Ticks</button><br/>
      <button id="hide-brush">Hide Brush Area</button>
      <button id="show-brush">Show Brush Area</button><br/>
      <button id="dark-theme">Dark</button>
      <button id="light-theme">Light</button>

      <h3>Credits &amp; License</h3>
        <small>
        <p>
        Adapted from examples by <a href="//bl.ocks.org/1341021">Mike Bostock</a> and <a href="//bl.ocks.org/1341281">Jason Davies</a>.<br/>
        </p>
        <p>
          Copyright &copy; 2012, Kai Chang<br/>
          All rights reserved. Released under the <a href="//opensource.org/licenses/bsd-3-clause">BSD License</a>.
        </p>
        </small>
      </p>
    </div>
    <div class="third">
      <h3>Random sample of 25 entries</h3>
      <p id="food-list">
      </p>
    </div>
  </div>
  </body>
  <script src="//mbostock.github.com/d3/d3.v2.js"></script>
  <script src="//documentcloud.github.com/underscore/underscore.js"></script>
  <script src="parallel.js"></script>
</html>

parallel.js

// shim layer with setTimeout fallback
window.requestAnimFrame = window.requestAnimationFrame       ||
                          window.webkitRequestAnimationFrame ||
                          window.mozRequestAnimationFrame    ||
                          window.oRequestAnimationFrame      ||
                          window.msRequestAnimationFrame     ||
                          function( callback ){
                            window.setTimeout(callback, 1000 / 60);
                          };
d3.select("#hide-ticks").on("click", hideTicks);
d3.select("#show-ticks").on("click", showTicks);
d3.select("#hide-brush").on("click", hideBrush);
d3.select("#show-brush").on("click", showBrush);

function hideTicks() {
  d3.selectAll(".axis g").style("display", "none");
  d3.selectAll(".axis path").style("display", "none");
};

function showTicks() {
  d3.selectAll(".axis g").style("display", null);
  d3.selectAll(".axis path").style("display", null);
};

function hideBrush() {
  d3.selectAll(".background").style("visibility", "hidden");
};

function showBrush() {
  d3.selectAll(".background").style("visibility", null);
};

d3.select("#dark-theme")
    .on("click", function() {
      d3.select("body").attr("class", "dark");
    });

d3.select("#light-theme")
    .on("click", function() {
      d3.select("body").attr("class", null);
    });

var render_speed = 50;

var width = document.body.clientWidth,
    height = d3.max([document.body.clientHeight-500, 240]);

var m = [40, 0, 10, 0],
    w = width - m[1] - m[3],
    h = height - m[0] - m[2];

var xscale = d3.scale.ordinal().rangePoints([0, w], 1),
    yscale = {},
    dragging = {};

var line = d3.svg.line(),
    axis = d3.svg.axis().orient("left").ticks(1+height/50),
    foreground,
    dimensions,                           
    n_dimensions,
    brush_count = 0;

var colors = {
  "Dairy and Egg Products": [28,100,52],
  "Spices and Herbs": [214,55,79],
  "Baby Foods": [185,56,73],
  "Fats and Oils": [30,100,73],
  "Poultry Products": [359,69,49],
  "Soups, Sauces, and Gravies": [110,57,70],
  "Vegetables and Vegetable Products": [120,56,40],
  "Sausages and Luncheon Meats": [1,100,79],
  "Breakfast Cereals": [271,39,57],
  "Fruits and Fruit Juices": [274,30,76],
  "Nut and Seed Products": [10,30,42],
  "Beverages": [10,28,67],
  "Finfish and Shellfish Products": [318,65,67],
  "Legumes and Legume Products": [334,80,84],
  "Baked Products": [37,50,75],
  "Sweets": [339,60,75],
  "Cereal Grains and Pasta": [56,58,73],
  "Pork Products": [339,60,49],
  "Beef Products": [325,50,39],
  "Lamb, Veal, and Game Products": [20,49,49],
  "Fast Foods": [60,86,61],
  "Meals, Entrees, and Sidedishes": [185,80,45],
  "Snacks": [189,57,75],
  "Ethnic Foods": [41,75,61],
  "Restaurant Foods": [204,70,41]
};

var legend = d3.select("#legend")
  .selectAll(".row")
    .data(d3.keys(colors).sort())
  .enter().append("div")
    .attr("class", "row");

legend
    .append("span")
    .text(function(d,i) { return d});  

legend
    .append("span")
    .style("background", function(d,i) { return color(d,0.85)})
    .attr("class", "color-block");

d3.select("#chart")
    .style("height", (h + m[0] + m[2]) + "px")

d3.selectAll("canvas")
    .attr("width", w)
    .attr("height", h)
    .style("padding", m.join("px ") + "px");

foreground = document.getElementById('foreground').getContext('2d');

foreground.strokeStyle = "rgba(0,100,160,0.1)";
foreground.lineWidth = 1.5;    // avoid weird subpixel effects

foreground.fillText("Loading...",w/2,h/2);

var svg = d3.select("svg")
    .attr("width", w + m[1] + m[3])
    .attr("height", h + m[0] + m[2])
  .append("svg:g")
    .attr("transform", "translate(" + m[3] + "," + m[0] + ")");

d3.csv("nutrients.csv", function(data) {
  // Convert quantitative scales to floats
  data = data.map(function(d) {
    for (var k in d) {
      if (k != "name" && k != "group" && k != "id")
        d[k] = parseFloat(d[k]) || 0;
    };
    return d;
  });

  // Extract the list of dimensions and create a scale for each.
  xscale.domain(dimensions = d3.keys(data[0]).filter(function(d) {
    return d != "name" && d != "group" && d != "id" &&(yscale[d] = d3.scale.linear()
        .domain(d3.extent(data, function(p) { return +p[d]; }))
        .range([h, 0]));
  }).sort());

  n_dimensions = dimensions.length;

  // Render full foreground
  paths(data, foreground, brush_count);

  // Add a group element for each dimension.
  var g = svg.selectAll(".dimension")
      .data(dimensions)
    .enter().append("svg:g")
      .attr("class", "dimension")
      .attr("transform", function(d) { return "translate(" + xscale(d) + ")"; })
      .call(d3.behavior.drag()
        .on("dragstart", function(d) {
          dragging[d] = this.__origin__ = xscale(d);
          this.__dragged__ = false;
        })
        .on("drag", function(d) {
          dragging[d] = Math.min(w, Math.max(0, this.__origin__ += d3.event.dx));
          dimensions.sort(function(a, b) { return position(a) - position(b); });
          xscale.domain(dimensions);
          g.attr("transform", function(d) { return "translate(" + position(d) + ")"; });
          brush_count++;
          this.__dragged__ = true;
        })
        .on("dragend", function(d) {
          if (!this.__dragged__) {
            // save extent before inverting
            if (!yscale[d].brush.empty()) {
              var extent = yscale[d].brush.extent();
            }

            // no movement, invert axis
            if (yscale[d].inverted == true) {
              yscale[d].range([h, 0]);
              d3.selectAll('.label')
                .filter(function() { return d3.select(this).text() == d; })
                .style("text-decoration", null);
              yscale[d].inverted = false;
            } else {
              yscale[d].range([0, h]);
              d3.selectAll('.label')
                .filter(function() { return d3.select(this).text() == d; })
                .style("text-decoration", "underline");
              yscale[d].inverted = true;
            }

            updateTicks(d, extent);
          } else {
            // rorder axes
            d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")");
          }
          brush();
          delete this.__dragged__;
          delete this.__origin__;
          delete dragging[d];
        }))

  // Add and store a brush for each axis.
  g.append("svg:g")
      .attr("class", "brush")
      .each(function(d) { d3.select(this).call(yscale[d].brush = d3.svg.brush().y(yscale[d]).on("brush", brush)); })
    .selectAll("rect")
      .style("visibility", null)
      .attr("x", -15)
      .attr("width", 30)
      .attr("rx", 0)
      .attr("ry", 0);

  // Add an axis and title.
  g.append("svg:g")
      .attr("class", "axis")
      .attr("transform", "translate(10,0)")
      .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
    .append("svg:text")
      .attr("text-anchor", "middle")
      .attr("y", -16)
      .attr("x", -12)
      .attr("class", "label")
      .text(String);

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

    // hack to hide ticks beyond extent
    var b = d3.selectAll('.dimension')[0]
      .forEach(function(element, i) {
        var dimension = d3.select(element).data()[0];
        if (_.include(actives, dimension)) {
          var extent = extents[actives.indexOf(dimension)];
          d3.select(element)
            .selectAll('text')
            .style('font-size', '13px')
            .style('font-weight', 'bold')
            .style('display', function() { 
              var value = d3.select(this).text();
              return extent[0] <= value && value <= extent[1] ? null : "none"
            });
        } else {
          d3.select(element)
            .selectAll('text')
            .style('font-weight', null)
            .style('font-size', null)
            .style('display', null);
        }
        d3.select(element)
          .selectAll('.label')
          .style('display', null);
      });
      ;
   
    // bold dimensions with label
    d3.selectAll('.label')
      .style("font-weight", function() {
        var dimension = d3.select(this).text();
        if (_.include(actives, dimension)) return "bold";
        return null;
      });

    // Get lines within extents
    var selected = [];
    data.map(function(d) {
      return actives.every(function(p, dimension) {
        return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1];
      }) ? selected.push(d) : null;
    });

    // Render selected lines
    paths(selected, foreground, brush_count);
  }

  function actives() {
    var actives = dimensions.filter(function(p) { return !yscale[p].brush.empty(); }),
        extents = actives.map(function(p) { return yscale[p].brush.extent(); });

    // Get lines within extents
    var selected = [];
    data.map(function(d) {
      return actives.every(function(p, i) {
        return extents[i][0] <= d[p] && d[p] <= extents[i][1];
      }) ? selected.push(d) : null;
    });

    return selected;
  };

  window.onresize = function() {
    width = document.body.clientWidth,
    height = d3.max([document.body.clientHeight-500, 220]);

    w = width - m[1] - m[3],
    h = height - m[0] - m[2];

    d3.select("#chart")
        .style("height", (h + m[0] + m[2]) + "px")

    d3.selectAll("canvas")
        .attr("width", w)
        .attr("height", h)
        .style("padding", m.join("px ") + "px");

    d3.select("svg")
        .attr("width", w + m[1] + m[3])
        .attr("height", h + m[0] + m[2])
      .select("g")
        .attr("transform", "translate(" + m[3] + "," + m[0] + ")");
    
    xscale = d3.scale.ordinal().rangePoints([0, w], 1).domain(dimensions);
    dimensions.forEach(function(d) {
      yscale[d].range([h, 0]);
    });

    d3.selectAll(".dimension")
      .attr("transform", function(d) { return "translate(" + xscale(d) + ")"; })
    // update brush placement
    d3.selectAll(".brush")
      .each(function(d) { d3.select(this).call(yscale[d].brush = d3.svg.brush().y(yscale[d]).on("brush", brush)); })
    brush_count++;

    // update axis placement
    axis = axis.ticks(1+height/50),
    d3.selectAll(".axis")
      .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); });

    // render data
    brush();
  };

  // Remove all but selected from the dataset
  d3.select("#keep-data")
    .on("click", function() {
      new_data = actives();
      if (new_data.length == 0) {
        alert("I don't mean to be rude, but I can't let you remove all the data.\n\nTry removing some brushes to get your data back. Then click 'Keep' when you've selected data you want to look closer at.");
        return false;
      }
      data = new_data;
      rescale();
    });

  // Exclude selected from the dataset
  d3.select("#exclude-data")
    .on("click", function() {
      new_data = _.difference(data, actives());
      if (new_data.length == 0) {
        alert("I don't mean to be rude, but I can't let you remove all the data.\n\nTry selecting just a few data points then clicking 'Exclude'.");
        return false;
      }
      data = new_data;
      rescale();
    });

  // Rescale to new dataset domain
  function rescale() {
      // reset yscales, preserving inverted state
      dimensions.forEach(function(d,i) {
        if (yscale[d].inverted) {
          yscale[d] = d3.scale.linear()
              .domain(d3.extent(data, function(p) { return +p[d]; }))
              .range([0, h]);
          yscale[d].inverted = true;
        } else {
          yscale[d] = d3.scale.linear()
              .domain(d3.extent(data, function(p) { return +p[d]; }))
              .range([h, 0]);
        }
      });

      updateTicks();

      // Render selected data
      paths(data, foreground, brush_count);
  };

  function updateTicks(d, extent) {
    // update brushes
    if (d) {
      var brush_el = d3.selectAll(".brush")
          .filter(function(key) { return key == d; });
      // single tick
      if (extent) {
        // restore previous extent
        brush_el.call(yscale[d].brush = d3.svg.brush().y(yscale[d]).extent(extent).on("brush", brush));
      } else {
        brush_el.call(yscale[d].brush = d3.svg.brush().y(yscale[d]).on("brush", brush));
      }
    } else {
      // all ticks
      d3.selectAll(".brush")
        .each(function(d) { d3.select(this).call(yscale[d].brush = d3.svg.brush().y(yscale[d]).on("brush", brush)); })
    }

    brush_count++;

    showTicks();

    // update axes
    d3.selectAll(".axis")
      .each(function(d,i) {
        d3.select(this)
          .transition()
          .duration(720)
          .call(axis.scale(yscale[d]));

        // clear active extent state
        d3.select(this)
          .selectAll('text')
          .style('font-weight', null)
          .style('font-size', null)
          .style('display', null);
      });
  };

  function paths(selected, ctx, count) {
    var n = selected.length,
        i = 0,
        opacity = d3.min([2/Math.pow(n,0.33),1]),
        timer = (new Date()).getTime();           // use for optimization

    d3.select("#data-count").text(data.length);
    d3.select("#selected-count").text(n);
    d3.select("#selected-bar").style("width", (100*n/data.length) + "%");
    d3.select("#opacity").text((""+(opacity*100)).slice(0,4) + "%");

    shuffled_data = _.shuffle(selected);

    // data table, sorted by first column
    var foodText = "";
    shuffled_data.slice(0,25).sort(function(a,b) {
      var col = d3.keys(a)[0];
      return a[col] < b[col] ? -1 : 1;
    })
    .forEach(function(d) {
      foodText += "<span class='color-block' style='background:" + color(d.group,0.85) + "'></span>" + d.name + "<br/>";
    });
    d3.select("#food-list").html(foodText);

    ctx.clearRect(0,0,w+1,h+1);
    function render() {
      var max = d3.min([i+render_speed, n]);
      shuffled_data.slice(i,max).forEach(function(d) {
        path(d, foreground, color(d.group,opacity));
      });
      i = max;
      d3.select("#rendered-count").text(i);
      d3.select("#rendered-bar").style("width", (100*i/n) + "%");
      d3.select("#render-speed").text(render_speed);

      // rendering speed optimization
      var delta = (new Date()).getTime() - timer;
      render_speed = Math.max(Math.ceil(render_speed * 33 / delta), 8);
      render_speed = Math.min(render_speed, 200);

      timer = (new Date()).getTime();
    };

    // render all lines until finished or a new brush event
    (function animloop(){
      if (i >= n || count < brush_count) return;
      requestAnimFrame(animloop);
      render();
    })();
  };
});


function path(d, ctx, color) {
  if (color) ctx.strokeStyle = color;
  var x = xscale(0)-15;
      y = yscale[dimensions[0]](d[dimensions[0]]);   // left edge
  ctx.beginPath();
  ctx.moveTo(x,y);
  dimensions.map(function(p,i) {
    x = xscale(p),
    y = yscale[p](d[p]);
    ctx.lineTo(x, y);
  });
  ctx.lineTo(x+15, y);                               // right edge
  ctx.stroke();
};

function color(d,a) {
  var c = colors[d];
  return ["hsla(",c[0],",",c[1],"%,",c[2],"%,",a,")"].join("");
};

function position(d) {
  var v = dragging[d];
  return v == null ? xscale(d) : v;
}