Both panels show the same 200-point scatter plot. Hover over points to see tooltips. The experience looks the same, but the implementation is fundamentally different. Canvas requires manual distance calculations for every mouse move. SVG gets events for free via the DOM.
mousemove event, calculating the distance from the cursor to each point to find the nearest one. The SVG panel does nothing — each <circle> element has its own mouseenter event, handled natively by the browser's DOM event dispatch system.
For Canvas, you must find the nearest point yourself. This is O(n) per mouse move — you check every point:
canvas.addEventListener('mousemove', (event) => {
const rect = canvas.getBoundingClientRect();
const mx = event.clientX - rect.left;
const my = event.clientY - rect.top;
let nearest = null;
let minDist = Infinity;
// O(n) loop — check every point
for (const point of points) {
const dx = point.x - mx;
const dy = point.y - my;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist && dist < 20) {
minDist = dist;
nearest = point;
}
}
if (nearest) {
showTooltip(nearest);
highlightPoint(nearest);
} else {
hideTooltip();
}
});
With 200 points, this is fast. With 10,000 points, you would need a spatial index (quadtree) to avoid checking every point on every frame.
For SVG, each circle is a DOM element that receives events natively. No distance calculation needed:
// Each circle gets its own event listener — O(1) per event
circles.forEach(circle => {
circle.addEventListener('mouseenter', (event) => {
const point = circle.__data__;
showTooltip(point);
circle.classList.add('hovered');
});
circle.addEventListener('mouseleave', () => {
hideTooltip();
circle.classList.remove('hovered');
});
});
The browser's event dispatch system handles the "which element is under the cursor?" question internally, using a spatial data structure optimized for the DOM tree. You just attach a listener and respond.
| Aspect | Canvas | SVG |
|---|---|---|
| Detecting hover | Manual: loop through all points, compute distance | Automatic: mouseenter fires on the element |
| Complexity per move | O(n) naive, O(log n) with quadtree | O(1) — browser handles it |
| Highlighting | Redraw the entire canvas with one point in a different color | Toggle a CSS class on the element |
| Click handling | Same distance loop in click handler | circle.addEventListener('click', ...) |
| Cursor style | Manual: change cursor when nearest point is found | CSS: circle { cursor: pointer; } |
| Scales to 50K points | Yes (with spatial index) | No (DOM too heavy) |