Key concept — D3 data join in updateDots()
D3's data join is the central idea: you bind an array of data to a selection of DOM
elements and D3 computes which elements are new (enter), changed (update), or
removed (exit). Each case is handled declaratively — you describe what the element
should look like, and D3 + SVG take care of rendering. Because every dot is a real
<circle> in the DOM, the browser handles hit-testing and CSS transitions for free,
but performance degrades fast once you have tens of thousands of elements.
function updateDots(newData) {
g.selectAll(".dot")
.data(newData)
.join(
// Entering dots — append a circle at r=4
enter => enter.append("circle")
.attr("class", "dot")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", 4),
// Updating dots — animate to the new position
update => update
.transition().duration(600).ease(d3.easeCubicInOut)
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y)),
// Exiting dots — remove immediately
exit => exit.remove()
);
}
Hover & color
Every <circle> is a real DOM node, so the browser's CSS engine handles
:hover intrinsically — no JavaScript, no hit-testing, no manual repaints.
A transition: fill 0.15s rule gives the smooth color change for free. This is
one of SVG's biggest ergonomic advantages over canvas-based renderers.
/* SVG circles are DOM nodes — CSS :hover works out of the box */
.dot {
fill: #f4be00;
stroke: #0a0032;
stroke-width: 0.5;
transition: fill 0.15s;
}
.dot:hover {
fill: #e5ffff;
fill-opacity: 1;
}
See the complete implementation on GitHub (75 lines)