// Parallel Coordinates // Copyright (c) 2012, Kai Chang // Released under the BSD License: http://opensource.org/licenses/BSD-3-Clause var width = document.body.clientWidth, height = d3.max([document.body.clientHeight-540, 240]); var m = [60, 0, 10, 0], w = width - m[1] - m[3], h = height - m[0] - m[2], xscale = d3.scale.ordinal().rangePoints([0, w], 1), yscale = {}, dragging = {}, line = d3.svg.line(), axis = d3.svg.axis().orient("left").ticks(1+height/50), data, foreground, background, highlighted, dimensions, legend, render_speed = 50, brush_count = 0, excluded_groups = []; var colors = { "Baby Foods": [185,56,73], "Baked Products": [37,50,75], "Beef Products": [325,50,39], "Beverages": [10,28,67], "Breakfast Cereals": [271,39,57], "Cereal Grains and Pasta": [56,58,73], "Dairy and Egg Products": [28,100,52], "Ethnic Foods": [41,75,61], "Fast Foods": [60,86,61], "Fats and Oils": [30,100,73], "Finfish and Shellfish Products": [318,65,67], "Fruits and Fruit Juices": [274,30,76], "Lamb, Veal, and Game Products": [20,49,49], "Legumes and Legume Products": [334,80,84], "Meals, Entrees, and Sidedishes": [185,80,45], "Nut and Seed Products": [10,30,42], "Pork Products": [339,60,49], "Poultry Products": [359,69,49], "Restaurant Foods": [204,70,41], "Sausages and Luncheon Meats": [1,100,79], "Snacks": [189,57,75], "Soups, Sauces, and Gravies": [110,57,70], "Spices and Herbs": [214,55,79], "Sweets": [339,60,75], "Vegetables and Vegetable Products": [120,56,40] }; // Scale chart and canvas height 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 canvas for primary view foreground = document.getElementById('foreground').getContext('2d'); foreground.globalCompositeOperation = "destination-over"; foreground.strokeStyle = "rgba(0,100,160,0.1)"; foreground.lineWidth = 1.7; foreground.fillText("Loading...",w/2,h/2); // Highlight canvas for temporary interactions highlighted = document.getElementById('highlight').getContext('2d'); highlighted.strokeStyle = "rgba(0,100,160,1)"; highlighted.lineWidth = 4; // Background canvas background = document.getElementById('background').getContext('2d'); background.strokeStyle = "rgba(0,100,160,0.1)"; background.lineWidth = 1.7; // SVG for ticks, labels, and interactions 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] + ")"); // Load the data and visualization d3.csv("nutrients.csv", function(raw_data) { // Convert quantitative scales to floats data = raw_data.map(function(d) { for (var k in d) { if (!_.isNaN(raw_data[0][k] - 0) && k != 'id') { d[k] = parseFloat(d[k]) || 0; } }; return d; }); // Extract the list of numerical dimensions and create a scale for each. xscale.domain(dimensions = d3.keys(data[0]).filter(function(k) { return (_.isNumber(data[0][k])) && (yscale[k] = d3.scale.linear() .domain(d3.extent(data, function(d) { return +d[k]; })) .range([h, 0])); }).sort()); // 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; d3.select("#foreground").style("opacity", "0.35"); }) .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; // Feedback for axis deletion if dropped if (dragging[d] < 12 || dragging[d] > w-12) { d3.select(this).select(".background").style("fill", "#b00"); } else { d3.select(this).select(".background").style("fill", null); } }) .on("dragend", function(d) { if (!this.__dragged__) { // no movement, invert axis var extent = invert_axis(d); } else { // reorder axes d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")"); var extent = yscale[d].brush.extent(); } // remove axis if dragged all the way left if (dragging[d] < 12 || dragging[d] > w-12) { remove_axis(d,g); } // TODO required to avoid a bug xscale.domain(dimensions); update_ticks(d, extent); // rerender d3.select("#foreground").style("opacity", null); brush(); delete this.__dragged__; delete this.__origin__; delete dragging[d]; })) // Add an axis and title. g.append("svg:g") .attr("class", "axis") .attr("transform", "translate(0,0)") .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); }) .append("svg:text") .attr("text-anchor", "middle") .attr("y", function(d,i) { return i%2 == 0 ? -14 : -30 } ) .attr("x", 0) .attr("class", "label") .text(String) .append("title") .text("Click to invert. Drag to reorder"); // 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", -23) .attr("width", 36) .append("title") .text("Drag up or down to brush along this axis"); g.selectAll(".extent") .append("title") .text("Drag or resize this filter"); legend = create_legend(colors,brush); // Render full foreground brush(); }); // copy one canvas to another, grayscale function gray_copy(source, target) { var pixels = source.getImageData(0,0,w,h); target.putImageData(grayscale(pixels),0,0); } // http://www.html5rocks.com/en/tutorials/canvas/imagefilters/ function grayscale(pixels, args) { var d = pixels.data; for (var i=0; i 0) { selected = search(selected, query); } if (selected.length < data.length && selected.length > 0) { d3.select("#keep-data").attr("disabled", null); d3.select("#exclude-data").attr("disabled", null); } else { d3.select("#keep-data").attr("disabled", "disabled"); d3.select("#exclude-data").attr("disabled", "disabled"); }; // total by food group var tallies = _(selected) .groupBy(function(d) { return d.group; }) // include empty groups _(colors).each(function(v,k) { tallies[k] = tallies[k] || []; }); legend .style("text-decoration", function(d) { return _.contains(excluded_groups,d) ? "line-through" : null; }) .attr("class", function(d) { return (tallies[d].length > 0) ? "row" : "row off"; }); legend.selectAll(".color-bar") .style("width", function(d) { return Math.ceil(600*tallies[d].length/data.length) + "px" }); legend.selectAll(".tally") .text(function(d,i) { return tallies[d].length }); // Render selected lines paths(selected, foreground, brush_count, true); } // render a set of polylines on a canvas function paths(selected, ctx, count) { var n = selected.length, i = 0, opacity = d3.min([2/Math.pow(n,0.3),1]), timer = (new Date()).getTime(); selection_stats(opacity, n, data.length) shuffled_data = _.shuffle(selected); data_table(shuffled_data.slice(0,25)); ctx.clearRect(0,0,w+1,h+1); // render all lines until finished or a new brush event function animloop(){ if (i >= n || count < brush_count) return true; var max = d3.min([i+render_speed, n]); render_range(shuffled_data, i, max, opacity); render_stats(max,n,render_speed); i = max; timer = optimize(timer); // adjusts render_speed }; d3.timer(animloop); } // transition ticks for reordering, rescaling and inverting function update_ticks(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++; show_ticks(); // update axes d3.selectAll(".axis") .each(function(d,i) { // hide lines for better performance d3.select(this).selectAll('line').style("display", "none"); // transition axis numbers d3.select(this) .transition() .duration(720) .call(axis.scale(yscale[d])); // bring lines back d3.select(this).selectAll('line').transition().delay(800).style("display", null); d3.select(this) .selectAll('text') .style('font-weight', null) .style('font-size', null) .style('display', null); }); } // 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]); } }); update_ticks(); // Render selected data paths(data, foreground, brush_count); } // Get polylines within extents function actives() { var actives = dimensions.filter(function(p) { return !yscale[p].brush.empty(); }), extents = actives.map(function(p) { return yscale[p].brush.extent(); }); // filter extents and excluded groups var selected = []; data .filter(function(d) { return !_.contains(excluded_groups, d.group); }) .map(function(d) { return actives.every(function(p, i) { return extents[i][0] <= d[p] && d[p] <= extents[i][1]; }) ? selected.push(d) : null; }); // free text search var query = d3.select("#search")[0][0].value; if (query > 0) { selected = search(selected, query); } return selected; } // Export data function export_csv() { var keys = d3.keys(data[0]); var rows = actives().map(function(row) { return keys.map(function(k) { return row[k]; }) }); var csv = d3.csv.format([keys].concat(rows)).replace(/\n/g,"
\n"); var styles = ""; window.open("text/csv").document.write(styles + csv); } // scale to window size 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 function keep_data() { 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 function exclude_data() { 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(); } function remove_axis(d,g) { dimensions = _.difference(dimensions, [d]); xscale.domain(dimensions); g.attr("transform", function(p) { return "translate(" + position(p) + ")"; }); g.filter(function(p) { return p == d; }).remove(); update_ticks(); } d3.select("#keep-data").on("click", keep_data); d3.select("#exclude-data").on("click", exclude_data); d3.select("#export-data").on("click", export_csv); d3.select("#search").on("keyup", brush); // Appearance toggles d3.select("#hide-ticks").on("click", hide_ticks); d3.select("#show-ticks").on("click", show_ticks); d3.select("#dark-theme").on("click", dark_theme); d3.select("#light-theme").on("click", light_theme); function hide_ticks() { d3.selectAll(".axis g").style("display", "none"); //d3.selectAll(".axis path").style("display", "none"); d3.selectAll(".background").style("visibility", "hidden"); d3.selectAll("#hide-ticks").attr("disabled", "disabled"); d3.selectAll("#show-ticks").attr("disabled", null); }; function show_ticks() { d3.selectAll(".axis g").style("display", null); //d3.selectAll(".axis path").style("display", null); d3.selectAll(".background").style("visibility", null); d3.selectAll("#show-ticks").attr("disabled", "disabled"); d3.selectAll("#hide-ticks").attr("disabled", null); }; function dark_theme() { d3.select("body").attr("class", "dark"); d3.selectAll("#dark-theme").attr("disabled", "disabled"); d3.selectAll("#light-theme").attr("disabled", null); } function light_theme() { d3.select("body").attr("class", null); d3.selectAll("#light-theme").attr("disabled", "disabled"); d3.selectAll("#dark-theme").attr("disabled", null); } function search(selection,str) { pattern = new RegExp(str,"i") return _(selection).filter(function(d) { return pattern.exec(d.name); }); }