Skip to content

Instantly share code, notes, and snippets.

@sxv
Last active July 3, 2018 07:20
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sxv/4485778 to your computer and use it in GitHub Desktop.
Save sxv/4485778 to your computer and use it in GitHub Desktop.
Interactive SVG + Canvas Plot

A D3 demonstration of SVG and Canvas intermingling. Blue circles are plotted in SVG, black circles in canvas. One force to rule them all. The plot is zoomable and pannable.

Inspired by M. Bostock's Canvas / SVG zoom comparison series and collision detection examples [1] [2] .

<!doctype html>
<head>
<title>interactive svg + canvas plot</title>
<style>
body { margin: 0; }
svg {
position: absolute;
top: 0;
left: 0;
}
rect { fill: transparent; }
</style>
<script src='http://d3js.org/d3.v3.min.js'></script>
</head>
<body>
<script type='text/javascript'>
/* SET UP ENV */
var map = {};
map.width = 960;
map.height = 500;
map.canvas =
d3.select('body')
.append('canvas')
.attr('width', map.width)
.attr('height', map.height)
.node().getContext('2d');
map.svg =
d3.select('body')
.append('svg')
.attr('width', map.width)
.attr('height', map.height)
.append('g');
map.svg.append('rect')
.attr('class', 'overlay')
.attr('width', map.width)
.attr('height', map.height);
/* PREPARE DATA and SCALES */
map.canvas.nodes =
d3.range(100).map(function(d, i) {
return {
x: Math.random() * map.width / 2,
y: Math.random() * map.height / 2,
r: Math.random() * 10 + 3
};
});
map.svg.nodes =
d3.range(100).map(function(d, i) {
return {
x: Math.random() * map.width / 2,
y: Math.random() * map.height / 2,
r: Math.random() * 10 + 3
};
});
map.nodes = map.svg.nodes.concat( map.canvas.nodes );
var root = map.nodes[0];
root.r = 0;
root.fixed = true;
var x =
d3.scale.linear()
.domain([0, map.width])
.range([0, map.width]);
var y =
d3.scale.linear()
.domain([0, map.height])
.range([map.height, 0]);
/* PLOT */
map.canvas.draw =
function() {
map.canvas.clearRect(0, 0, map.width, map.height);
map.canvas.beginPath();
var i = -1, cx, cy;
while (++i < map.canvas.nodes.length) {
d = map.canvas.nodes[i];
cx = x( d.x );
cy = y( d.y );
map.canvas.moveTo(cx, cy);
map.canvas.arc(cx, cy, d.r, 0, 2 * Math.PI);
}
map.canvas.fill();
};
map.svg.draw =
function() {
circle = map.svg.selectAll('circle')
.data(map.svg.nodes).enter()
.append('circle')
.attr('r', function(d) { return d.r; })
.attr('fill', 'blue')
.attr('transform', map.svg.transform);
};
map.canvas.draw();
map.svg.draw();
map.redraw = function() {
map.canvas.draw();
circle.attr('transform', map.svg.transform);
};
map.svg.transform =
function(d) {
return 'translate(' + x( d.x ) + ',' + y( d.y ) + ')';
};
/* FORCE */
var force =
d3.layout.force()
.gravity(0.05)
.charge( function(d, i) { return i ? 0 : -2000; } )
.nodes(map.nodes)
.size([map.width, map.height])
.start();
force.on('tick', function(e) {
var q = d3.geom.quadtree(map.nodes), i;
for (i = 1; i < map.nodes.length; ++i) {
q.visit( collide(map.nodes[i]) );
}
map.redraw();
});
function collide(node) {
var r = node.r + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.r + quad.point.r;
if (l < r) {
l = (l - r) / l * 0.5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
/* LISTENERS */
function mousemove() {
var p = d3.mouse(this);
root.px = x.invert( p[0] );
root.py = y.invert( p[1] );
force.resume();
}
d3.select('body')
.on('mousemove', mousemove)
.call( d3.behavior.zoom().x( x ).y( y ).scaleExtent([1, 8]).on('zoom', map.redraw) );
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment