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 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.

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.