Synchronize custom HTML/SVG elements
Sometimes you need to render HTML or SVG elements positioned in graph space (cluster labels, tooltips, annotations,
custom overlays…). The pattern is straightforward: create DOM elements, position them using graphToViewport(), and
update their positions on every render.
The pattern
- Create an overlay container with
position: absolute; inset: 0; pointer-events: none;. - For each element, compute its screen position using
renderer.graphToViewport(). - Listen to
afterRenderto update positions when the camera moves.
// Create an overlay layerconst overlay = document.createElement("div");overlay.style.cssText = "position: absolute; inset: 0; pointer-events: none; overflow: hidden;";container.appendChild(overlay);
// Position a label at graph coordinates (x, y)function updateLabel(el: HTMLElement, graphX: number, graphY: number) { const { x, y } = renderer.graphToViewport({ x: graphX, y: graphY }); el.style.left = `${x}px`; el.style.top = `${y}px`;}
// Update on every render (camera pan, zoom, resize)renderer.on("afterRender", () => { updateLabel(myLabel, 0, 0);});Example: cluster labels
A common use case is rendering cluster names over groups of nodes. Compute the centroid of each cluster’s nodes, create HTML labels, and reposition them on every render.
import Graph from "graphology";import type { SerializedGraph } from "graphology-types";import iwanthue from "iwanthue";import Sigma from "sigma";import type { Coordinates } from "sigma/types";
const container = document.getElementById("sigma-container") as HTMLElement;
const res = await fetch("/data/euroSIS.json");const data = await res.json();const graph = Graph.from(data as SerializedGraph);
// Cluster definitioninterface Cluster { label: string; x?: number; y?: number; color?: string; positions: { x: number; y: number }[];}
// Initialize clusters from graph dataconst countryClusters: Record<string, Cluster> = {};graph.forEachNode((_node, atts) => { if (!countryClusters[atts.country]) countryClusters[atts.country] = { label: atts.country, positions: [] };});
// Assign one color per clusterconst palette = iwanthue(Object.keys(countryClusters).length, { seed: "eurSISCountryClusters" });for (const country in countryClusters) { countryClusters[country].color = palette.pop();}
// Style nodes by clustergraph.forEachNode((node, atts) => { const cluster = countryClusters[atts.country]; atts.color = cluster.color; atts.size = Math.sqrt(graph.degree(node)) * 5; cluster.positions.push({ x: atts.x, y: atts.y });});
// Compute cluster centroidsfor (const country in countryClusters) { const c = countryClusters[country]; c.x = c.positions.reduce((acc, p) => acc + p.x, 0) / c.positions.length; c.y = c.positions.reduce((acc, p) => acc + p.y, 0) / c.positions.length;}
const renderer = new Sigma(graph, container);
// Create HTML overlay layer for cluster labelsconst clustersLayer = document.createElement("div");clustersLayer.className = "clusters-layer";
for (const country in countryClusters) { const cluster = countryClusters[country]; const viewportPos = renderer.graphToViewport(cluster as Coordinates); const el = document.createElement("div"); el.className = "cluster-label"; el.style.top = `${viewportPos.y}px`; el.style.left = `${viewportPos.x}px`; el.style.color = cluster.color!; el.dataset.country = country; el.textContent = cluster.label; clustersLayer.appendChild(el);}
container.appendChild(clustersLayer);
// Update label positions on each renderrenderer.on("afterRender", () => { for (const country in countryClusters) { const cluster = countryClusters[country]; const el = clustersLayer.querySelector(`[data-country="${country}"]`) as HTMLElement | null; if (el) { const viewportPos = renderer.graphToViewport(cluster as Coordinates); el.style.top = `${viewportPos.y}px`; el.style.left = `${viewportPos.x}px`; } }});The key parts of this example:
- Compute centroids by averaging the
xandypositions of all nodes in each cluster. - Create HTML elements with
position: absolute; transform: translate(-50%, -50%)for centering. - Update in
afterRenderusingrenderer.graphToViewport()to convert graph coordinates to screen pixels.
Tips
- Use
pointer-events: noneon the overlay container so mouse events pass through to sigma. - Use
overflow: hiddento clip elements that move off-screen during panning. - For SVG overlays, the same pattern applies: create an
<svg>element with the same dimensions as the container and usegraphToViewport()to position elements. - If you need elements to interact with mouse events (e.g. clickable labels), set
pointer-events: autoon those specific elements.