Skip to content

Labels and focus

createOverlay() projects a bounded set of host-created HTML elements over the canvas. It is the same dense-canvas plus sparse-DOM split used by graph editors: WebGL remains responsible for the graph, while your application owns rich text, CSS, buttons, and action handling. The two-draw-call GPU path is unchanged: labels stay in the DOM overlay. The playground turns this on for the stress scene: focus labels, an Inspect toolbar, and one HTML card.

Pointer move updates hovered node state automatically. Use getNeighborIds(id) for 1-hop adjacency when building focus chrome outside the overlay.

labelPolicy controls which titles appear:

PolicyBehavior
manual (default)Host calls setLabels(ids)
autoBudget by importance among visible nodes (autoLabels: true maps here)
focusPrefer selected ∪ hovered ∪ 1-hop neighbors, then fill by importance within maxLabels
const overlay = graph.createOverlay({
labelPolicy: "focus",
autoToolbar: "selected",
maxLabels: 24,
renderLabel: ({ id, presentation }) => {
const label = document.createElement("span");
label.className = "my-graph-label";
label.textContent = presentation?.title ?? id;
return label;
},
renderToolbar: ({ id, presentation }) => {
if (!presentation) return null;
const toolbar = document.createElement("div");
for (const action of presentation.actions) {
const button = document.createElement("button");
button.textContent = action.label;
button.disabled = action.disabled ?? false;
button.onclick = () => runNodeAction(id, action.id);
toolbar.append(button);
}
return toolbar;
},
});

autoToolbar: "selected" | "hovered" keeps the toolbar pinned without calling setToolbar. With "hovered", selection is the fallback when nothing is hovered. When autoToolbar is off (default), call overlay.setToolbar(nodeId) yourself.

Dense nodes stay on the GPU. For a handful of selected or hovered nodes, renderRichNode projects host HTML (any framework) under the node. This is the focus tier in the fidelity-on-demand ADR.

const overlay = graph.createOverlay({
autoRichNodes: "selected",
maxRichNodes: 4,
renderRichNode: ({ id, presentation }) => {
const card = document.createElement("article");
card.textContent = presentation?.title ?? id;
return card;
},
});

autoRichNodes is "selected", "hovered", or "focus" (selected then hovered, no neighbors). It slices to maxRichNodes. Manual setRichNodes(ids) throws when the list is longer than the cap. Rich HTML edges are not in this API.

Off-screen nodes stay out of budgeted sets. Manual setLabels() still works under auto / focus until the next view or focus change overwrites the list. Export selectBudgetedLabelIds, selectFocusLabelIds, orderFocusNodeIds, boundOverlayIds, and selectRichNodeIds for the same policies outside the overlay.

const overlay = graph.createOverlay({
renderLabel: ({ id, presentation }) => {
const label = document.createElement("span");
label.textContent = presentation?.title ?? id;
return label;
},
});
overlay.setLabels(["person:ada", "place:london"]);
overlay.setToolbar("person:ada");

setLabels() still throws when the list exceeds maxLabels. Off-screen and missing nodes are safely hidden or ignored. Call destroy() with the graph lifecycle.

Color, shape, and size can remain visible across the graph. Titles use the budgeted overlay policy above (autoLabels or a host-owned setLabels() list). Full properties, actions, and rich HTML cards belong to a bounded focused or selected subset. This preserves both accessibility and the dense rendering path.

The WebGL surface is not the accessibility tree. Applications should mirror focus and selection into an accessible node list, expose labels in HTML, and pair semantic colors with text, shape, or filtering controls. graphraum owns the fast visual primitive; the host owns understandable interaction.