An example of an SVG interaction layer over a canvas rendering layer. Data from the USDA Nutrition Database.
This example has a few design differences from most parallel coordinates:
Find simpler examples of using canvas here and here
Interactions done with the brush component
Based on d3.js Parallel Coordinates
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Canvas Parallel Coordinates</title>
<style type="text/css">
body {
font-family: sans-serif;
font-size: 12px;
background: #f9f9f9;
color: #777;
margin-top: 40px;
}
body.dark {
background: #090909;
color: #ccc;
}
#wrap {
width: 960px;
margin: 0 auto;
position: relative;
}
svg {
font: 10px sans-serif;
}
canvas, svg {
position: absolute;
top: 0;
left: 0;
}
#chart {
position: relative;
}
.brush .extent {
fill: rgba(0,0,0,0.12);
stroke: rgba(255,255,255,0.6);
shape-rendering: crisp-edges;
}
.dark .brush .extent {
fill: rgba(255,255,255,0.12);
stroke: rgba(0,0,0,0.5);
}
.axis line, .axis path {
fill: none;
stroke: #222;
shape-rendering: crispEdges;
}
.axis text {
fill: #222;
text-shadow: 1px 1px 1px #fff, -1px -1px 1px #fff;
}
.axis text.label {
fill: #444;
font-size: 14px;
}
.dark .axis text {
fill: #f2f2f2;
text-shadow: 0 1px 0 #000, 1px 0 0 #000;
}
.dark .axis text.label {
fill: #ddd;
}
.axis g,
.axis path {
display: none;
}
#food-list {
position: absolute;
left: 220px;
width: 740px;
overflow-x: hidden;
}
#food-list span {
display: inline-block;
height: 6px;
width: 6px;
margin: 2px 4px;
}
</style>
</head>
<body>
<div id="wrap">
<div id="chart">
<canvas id="foreground"></canvas>
<svg></svg>
</div>
<pre id="food-list"></pre>
<p>
Rendered: <strong id="rendered-count"></strong><br/>
Selected: <strong id="selected-count"></strong><br/>
Opacity: <strong id="opacity"></strong><br/>
<button id="hide-ticks">Hide Ticks</button>
<button id="show-ticks">Show Ticks</button><br/>
<button id="dark-theme">Dark</button>
<button id="light-theme">Light</button>
</p>
<p>
Drag along a vertical axis to brush<br/>
Tap the axis to remove its brush
</p>
</div>
<script src="//mbostock.github.com/d3/d3.v2.js"></script>
<script src="parallel.js"></script>
</body>
</html>
// shim layer with setTimeout fallback
window.requestAnimFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
var m = [60, 10, 10, 0],
w = 960 - m[1] - m[3],
h = 290 - m[0] - m[2];
var xscale = d3.scale.ordinal().rangePoints([0, w], 1),
yscale = {};
var line = d3.svg.line(),
axis = d3.svg.axis().orient("left"),
foreground,
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]
};
d3.select("#chart")
.style("width", (w + m[1] + m[3]) + "px")
.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("#hide-ticks")
.on("click", function() {
d3.selectAll(".axis g").style("display", "none");
d3.selectAll(".axis path").style("display", "none");
});
d3.select("#show-ticks")
.on("click", function() {
d3.selectAll(".axis g").style("display", "block");
d3.selectAll(".axis path").style("display", "block");
});
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);
});
foreground = document.getElementById('foreground').getContext('2d');
foreground.strokeStyle = "rgba(0,100,160,0.1)";
foreground.lineWidth = 1.3; // 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]));
}));
// 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) + ")"; });
// Add an axis and title.
g.append("svg:g")
.attr("class", "axis")
.each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
.append("svg:text")
.attr("text-anchor", "left")
.attr("y", -8)
.attr("x", -4)
.attr("transform", "rotate(-19)")
.attr("class", "label")
.text(String);
// 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")
.attr("x", -16)
.attr("width", 32)
.attr("rx", 3)
.attr("ry", 3);
// 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(); });
// 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;
});
// Render selected lines
paths(selected, foreground, brush_count);
}
function paths(data, ctx, count) {
var n = data.length,
i = 0,
opacity = d3.min([2/Math.pow(n,0.37),1]);
d3.select("#selected-count").text(n);
d3.select("#opacity").text((""+opacity).slice(0,6));
data = shuffle(data);
// data table
var foodText = "";
data.slice(0,10).forEach(function(d) {
foodText += "<span 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+12, n]);
data.slice(i,max).forEach(function(d) {
path(d, foreground, color(d.group,opacity));
});
i = max;
d3.select("#rendered-count").text(i);
};
// 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;
ctx.beginPath();
var x0 = 0,
y0 = 0;
dimensions.map(function(p,i) {
var x = xscale(p),
y = yscale[p](d[p]);
if (i == 0) {
ctx.moveTo(x,y);
} else {
var cp1x = x - 0.85*(x-x0);
var cp1y = y0;
var cp2x = x - 0.15*(x-x0);
var cp2y = y;
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
}
x0 = x;
y0 = y;
});
ctx.stroke();
};
function color(d,a) {
var c = colors[d];
return ["hsla(",c[0],",",c[1],"%,",c[2],"%,",a,")"].join("");
};
// Fisher-Yates shuffle
function shuffle(array) {
var m = array.length, t, i;
// While there remain elements to shuffle…
while (m) {
// Pick a remaining element…
i = Math.floor(Math.random() * m--);
// And swap it with the current element.
t = array[m];
array[m] = array[i];
array[i] = t;
}
return array;
}