Skip to content

Interaction

Everything on this page is implemented once on the shared base of both engines (BaseEngine), so it works identically on geoMap() and plot() — see Engines. The examples use map, but read identically for a plot instance: map.on("hover", …), the declarative hover / tooltip / selection layer options, select(), and setStyle() are all the same call.

Retained layers are pickable by default (a CPU hit index per layer; disable with pickable: false). Picking is clip-aware: a layer with clipTo only hits where its clip source is also hit, so interaction matches what is visibly painted.

map.on("hover", (hit, ev) => { ... }); // hit: { layer, id, datum } | null
map.on("click", (hit, ev) => { ... }); // same hit shape; fires only on a non-drag click (≤ 4 px travel)

click coexists with pan/zoom/rotation: a drag never fires it.

map.layer("cells", geoms, {
hover: true, // default: white outline (ring for points)
// hover: { stroke: "#fff", lineWidth: 1.5 }, // or replay the item with this style
// hover: (d, g) => { ... }, // or fully custom draw (see below)
});
map.highlight("cells", id, styleOrDraw); // the imperative primitive (pass null to clear)

The hovered item is redrawn into a tiny internal overlay layer (inheriting the source layer’s clipTo/sizeMode, rendered on top). The base layer’s buffers are never touched, so sweeping fast across a dense grid costs O(one feature) per cell crossed — no fps drop. Because only one item is re-tessellated, lineWidth is allowed here (unlike bulk overrides).

Custom draw gets a HighlightBuilder scoped to the hovered drawable (world coordinates):

hover: (city, g) => {
g.replay({ fill: "#fff" }); // the item itself, restyled —
// uses its already-projected geometry
const [x, y] = g.anchor!; // non-null for point features
g.path((ctx) => ctx.arc(x, y, 8, 0, 2 * Math.PI), // plus anything else
{ stroke: "#e23b2f", lineWidth: 1.5 });
g.point(x, y - 12, 2, { fill: "#e23b2f" });
}
const map = geoMap(host, { tooltipClass: "my-tooltip" }); // optional styling hook
map.layer("cities", pts, { tooltip: (d, id) => d.name }); // string | HTMLElement | null

One shared absolutely-positioned div (class="d3gl-tooltip"), engine-managed: filled from the accessor of the hovered layer, follows the pointer clamped to the host, hidden off-target. Without tooltipClass it gets a minimal default look. Content is re-evaluated only when the hovered target changes; re-declare the layer to force a refresh.

map.layer("cells", geoms, {
selection: { selected: { stroke: "#fff" }, // optional; default keeps base style
others: { opacity: 0.3 } }, // default when omitted
});
map.select("cells", ids); // apply (pass null to clear)
map.setStyle("cells", ids, { fill, stroke, opacity }); // the primitives
map.clearStyle("cells", ids);

Overrides compose over the base accessor colors: fill/stroke replace the base color, opacity multiplies the base alpha (dimming keeps each item’s hue). They survive projection switches and rotation rebuilds; re-declaring the layer (map.layer(name, …) again) resets them. select() rewrites the layer’s whole override table (last write wins vs setStyle).

Bulk overrides are colors-only: stroke geometry bakes its width at tessellation time, so a bulk lineWidth would be O(n) re-tessellation — use the hover overlay for width changes.

On instanced lanes (network nodes, decluttered points)

Section titled “On instanced lanes (network nodes, decluttered points)”

selection.others works the same on GPU-instanced lanes — network nodes (including the overlapping-module pie glyphs of a state network’s physical view, which fade and stay lit with their node) and a plot layer’s decluttered points — as on retained layers: with a selection active, non-selected glyphs fade to others.opacity (default 0.3). The persistent selection ring stays the selected affordance; the dim is the complementary focus. Lanes honor the opacity component of others (a colour override there is ignored — instanced glyphs have no Scene drawable to recolour).

For a network the highlight is applied in the GPU vertex shader from per-instance group/selected columns plus lane uniforms, so it scales to a full LOD-off draw: a hover is a single uniform change — no per-frame geometry rebuild or buffer re-upload, even at a million nodes. A selected node keeps its outgoing links at full strength while the rest dim (“this node and what it points to” — incident links for an undirected graph; under LOD, the selected aggregate’s outgoing super-edges). Hovering a node recolours those same links toward the highlight colour (red by default, matching the ring) preserving each link’s luminance, so a weight-encoded link keeps its weight cue. hover mirrors selection: hover: { hovered?, others? } — hovered styles the hovered item (the ring / overlay), others fades the rest on hover (the opt-in hover analogue of selection.others); hover: true or a bare style still works. These links are a visual companion derived from the node selection; selection() / on("select") stay node-only.

Selection highlight is ancestor-aware under LOD: selecting a module and zooming in keeps its expanding children (and their outgoing links) highlighted, even though only the module id is in the selection set — so selection() stays compact while the on-screen focus follows the subtree.

It works the other way too: a selected node the cut has collapsed into a module shows on that module, which takes a faint ring (the selection colour at reduced opacity) and is not dimmed. Zoom in and the ring moves down the levels until it sits on the node itself, in full. The module’s links stay as they are: they carry the whole module’s flow, so only the selected nodes’ own links light up. Select the nodes that match a search, and every match stays findable at any zoom.

OperationCostWhen
Pointer move within one itemone pick + tooltip repositionper move
Hover crosses into a new itemtessellate 1 feature + tiny uploadper change
select() / setStyle bulkO(n) byte writes + one small table uploadper call (e.g. click)
Pan/zoom/rotate framesunchanged — the hover pipeline pauses during gestures—

Nothing here adds per-frame work, changes shaders, or rebuilds vertex buffers.