|
<!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> |