And...back to SVG

There's one last problem with this approach. On my machine, it takes a full 80ms to render updates to the canvas - there's a lot of color picking and drawing to do! But we definitely don't want jank while the user is interacting with our scatterplot. So instead of repainting the whole canvas on every event, we'll overlay an SVG circle on the canvas to highlight the point.

Setting the Stage

In order for this to work, the SVG must be appended to the DOM after the canvas so that it lays on top. This is easy to do: since we're using D3 to create these elements in our example, we'll just reorder the calls to append so that the SVG is added to the DOM after the canvas.

The other important step is to prevent the SVG, which is now on top of the canvas, from intercepting the mouse events that we use to determine which point is being hovered. The pointer-events CSS property is exactly what we need. It will cause all mouse events to pass through the SVG to the element underneath it when set to none .

svg { pointer-events: none; }

Moving the SVG Circle

Now that we've taken care of the mundane details, it's time to create the circle we'll move around to highlight the selected point. We'll create an SVG circle called highlight , select it, and keep it hidden for now using the visibility attribute.

const highlight = highlightGroup.selectAll('circle') .attr('r', 4) .attr('stroke-width', 4) .attr('stroke', d3.rgb(0, 190, 25)) .attr('visibility', 'hidden');

Instead of repainting the entire canvas in our mouse handler, we'll position and show our highlight element. In the event that there is no match for the current mouse location, we hide the element.

if (mouseOutsideRange(possibleDatum, mouseX, mouseY)) { highlight.attr('visibility', 'hidden'); return; } highlight .attr('cx', x(possibleDatum.x)) .attr('cy', y(possibleDatum.y)) .attr('visibility', 'visible');

To ensure complete functionality, we also listen to the mouseout event to hide the highlight circle when we're no longer interacting with the chart.

canvas.on('mouseout', () => { highlight.attr('visibility', 'hidden'); });

Now we're all set! The final result is snappy: we get all the benefits of canvas along with quick updates via SVG.

An Iterative Process

Our solution is far from complete; were this a production implementation, we would need to include some tweaks and optimizations. To start, we could break our canvas painting across multiple setTimeout calls like we did in our last post to eliminate jank from the initial canvas render.

Furthermore, our approach isn't the only approach out there. As we have experienced firsthand, the community has a wealth of information and inspiration for new and creative solutions. For example, Mike Bostock himself pointed out that you could skip the color to data map and use a quadtree or voronoi diagram to map points on your canvas back to your data:

@veltman @jrbalsano @MongoDBEng Recommend quadtree.find for this. Equivalent to Voronoi, faster than linear scan, avoids antialias problem. — Mike Bostock (@mbostock) May 19, 2016

Although it may be a little while until a refactor this big hits our Visual Profiler, we're always learning from the community, and we'll keep sharing our approaches to improving data visualization in the browser.