Key concept — declarative ScatterplotLayer in buildLayer()
deck.gl sits one level above raw WebGL: instead of writing shaders yourself you compose pre-built layers and describe them declaratively (data, accessors, visual encodings). The library re-renders on the GPU whenever props change and even interpolates attributes like position smoothly on the GPU — so you get WebGL-scale performance without touching GLSL. Think of it as D3’s declarative style applied to a GPU render pipeline.
function buildLayer() {
return new ScatterplotLayer({
id: 'scatter',
data: particles,
getPosition: d => dataToWorld(d.dx, d.dy),
getRadius: d => d.targetR,
radiusUnits: 'pixels',
getFillColor: d => d.id === hoveredId
? [229, 255, 255, 255] // otter think bright
: [244, 190, 0, 178], // otter shine gold @ 70%
pickable: true,
// deck.gl interpolates position attributes automatically —
// no manual requestAnimationFrame loop needed
transitions: {
getPosition: { duration: 600, easing: cubicInOut },
},
});
}
Hover & color
deck.gl uses GPU picking: an off-screen render pass encodes every object
as a unique color; reading back one pixel identifies what's under the cursor in O(1)
regardless of point count. You only need to handle the state change in
onHover and declare a getFillColor accessor — deck.gl
schedules the repaint automatically.
// deck.gl fires onHover after its GPU picking pass
new Deck({
pickingRadius: 20, // snap to nearest object within 20 px
onHover: ({ object }) => {
const newId = object ? object.id : null;
if (newId !== hoveredId) {
hoveredId = newId;
deckInstance.setProps({ layers: [buildLayer()] });
}
},
});
// Accessor re-runs per datum on every re-render
getFillColor: d => d.id === hoveredId
? [229, 255, 255, 255] // otter think bright
: [244, 190, 0, 178], // otter shine gold @ 70%
// Declare dependency so deck.gl re-evaluates getFillColor on hover change
updateTriggers: { getFillColor: [hoveredId] }
See the complete implementation on GitHub (249 lines)