Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active July 5, 2019 16:03
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save mbostock/ec10387f24c1fad2acac3bc11eb218a5 to your computer and use it in GitHub Desktop.
Circle Dragging IV
license: gpl-3.0
redirect: https://observablehq.com/@d3/circle-dragging-iii

This is a variant of dragging SVG circles that uses better hit-testing. Rather than choose the top circle that contains the active pointer, this chooses the circle closest to the active pointer using a Voronoi overlay. This is similar to Tovi Grossman’s bubble cursor. (See also earlier D3 3.x examples by Andrew Couch, Christopher Manning and Nate Vack.)

The Voronoi diagram is shown with a light-gray stroke for demonstration purposes (and because it looks cool!); in real usage, you probably wouldn’t show it. You can refine this technique by clipping the Voronoi cells to a larger circle that surrounds the smaller draggable circles, or by performing a distance check in side drag.subject.

Compare this technique to searching for closest circle using Canvas.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var circles = d3.range(20).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("id", function(d, i) { return "cell-" + i; });
circle.append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("use")
.attr("xlink:href", function(d, i) { return "#cell-" + i; });
circle.append("circle")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d, i) { return color(i); });
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
}
function dragended(d, i) {
d3.select(this).classed("active", false);
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment