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] .
forked from sxv's block: Interactive SVG + Canvas Plot
xxxxxxxxxx
<head>
<title>interactive svg + canvas plot</title>
<style>
body { margin: 0; }
svg {
position: absolute;
top: 0;
left: 0;
}
rect { fill: transparent; }
</style>
<script src='https://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>
Modified http://d3js.org/d3.v3.min.js to a secure url
https://d3js.org/d3.v3.min.js