Paths are rendered only once, and the imageData is cached for redrawing. Redrawing a single path requires one clearRect, putImageData and drawImage call so the net effect seems slower than simply rerendering that paths with moveTo/lineTo.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Canvas Parallel Coordinates</title>
<style type="text/css">
svg {
font: 10px sans-serif;
}
canvas, svg {
position: absolute;
top: 0;
left: 0;
}
#chart {
position: relative;
}
.brush .extent {
fill-opacity: .3;
stroke: #fff;
shape-rendering: crispEdges;
}
.axis line, .axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
text-shadow: 0 1px 0 #fff;
}
#staging {
display: none;
}
</style>
</head>
<body>
<div id="chart">
<canvas id="background"></canvas>
<canvas id="foreground"></canvas>
<canvas id="staging"></canvas>
<svg></svg>
</div>
<script type="text/javascript" src="//mbostock.github.com/d3/d3.v2.js"></script>
<script type="text/javascript" src="parallel.js"></script>
</body>
</html>
// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
var m = [30, 10, 10, 10],
w = 960 - m[1] - m[3],
h = 340 - m[0] - m[2];
var x = d3.scale.ordinal().rangePoints([0, w], 1),
y = {};
var line = d3.svg.line(),
axis = d3.svg.axis().orient("left"),
background,
foreground,
dimensions,
brush_count = 0;
d3.selectAll("canvas")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.style("padding", m.join("px ") + "px");
foreground = document.getElementById('foreground').getContext('2d');
background = document.getElementById('background').getContext('2d');
staging = document.getElementById('staging').getContext('2d');
staging_el = document.getElementById('staging');
var foreground_cache = {};
staging.strokeStyle = "rgba(0,100,160,0.24)";
foreground.strokeStyle = "rgba(0,100,160,0.24)";
background.strokeStyle = "rgba(0,0,0,0.02)";
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-small.csv", function(data) {
// Extract the list of dimensions and create a scale for each.
x.domain(dimensions = d3.keys(data[0]).filter(function(d) {
return d != "name" && d != "group" &&(y[d] = d3.scale.linear()
.domain(d3.extent(data, function(p) { return +p[d]; }))
.range([h, 0]));
}));
// Render full foreground and background
data.map(function(d) {
staging.clearRect(0,0,w + m[1] + m[3], h + m[0] + m[2]);
path(d, staging);
foreground_cache[d.name] = staging.getImageData(0,0,w + m[1] + m[3], h + m[0] + m[2]);
});
data.map(function(d) {
//path(d, background);
cached_path(d.name);
});
// 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(" + x(d) + ")"; });
// Add an axis and title.
g.append("svg:g")
.attr("class", "axis")
.each(function(d) { d3.select(this).call(axis.scale(y[d])); })
.append("svg:text")
.attr("text-anchor", "middle")
.attr("y", -9)
.text(String);
// Add and store a brush for each axis.
g.append("svg:g")
.attr("class", "brush")
.each(function(d) { d3.select(this).call(y[d].brush = d3.svg.brush().y(y[d]).on("brush", brush)); })
.selectAll("rect")
.attr("x", -8)
.attr("width", 16);
// Handles a brush event, toggling the display of foreground lines.
function brush() {
brush_count++;
var actives = dimensions.filter(function(p) { return !y[p].brush.empty(); }),
extents = actives.map(function(p) { return y[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
foreground.clearRect(0,0,w+1,h+1);
paths(selected, foreground, brush_count);
}
function paths(data, ctx, count) {
var n = data.length,
i = 0,
reset = false;
function render() {
var max = d3.min([i+50, n]);
data.slice(i,max).forEach(function(d) {
cached_path(d.name);
});
i = max;
};
(function animloop(){
if (i >= n || count < brush_count) return;
requestAnimFrame(animloop);
render();
})();
};
function cached_path(name) {
staging.clearRect(0,0,w + m[1] + m[3], h + m[0] + m[2]);
staging.putImageData(foreground_cache[name],0,0);
foreground.drawImage(staging_el,0,0);
};
});
function path(d, ctx) {
ctx.beginPath();
dimensions.map(function(p,i) {
if (i == 0) {
ctx.moveTo(x(p),y[p](d[p]));
} else {
ctx.lineTo(x(p),y[p](d[p]));
}
});
ctx.stroke();
return ctx;
};