Skip to content
This is the alpha v4 version website. Looking for the v3 documentation?

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

  1. Create an overlay container with position: absolute; inset: 0; pointer-events: none;.
  2. For each element, compute its screen position using renderer.graphToViewport().
  3. Listen to afterRender to update positions when the camera moves.
// Create an overlay layer
const 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 definition
interface Cluster {
label: string;
x?: number;
y?: number;
color?: string;
positions: { x: number; y: number }[];
}
// Initialize clusters from graph data
const countryClusters: Record<string, Cluster> = {};
graph.forEachNode((_node, atts) => {
if (!countryClusters[atts.country]) countryClusters[atts.country] = { label: atts.country, positions: [] };
});
// Assign one color per cluster
const palette = iwanthue(Object.keys(countryClusters).length, { seed: "eurSISCountryClusters" });
for (const country in countryClusters) {
countryClusters[country].color = palette.pop();
}
// Style nodes by cluster
graph.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 centroids
for (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 labels
const 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 render
renderer.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 x and y positions of all nodes in each cluster.
  • Create HTML elements with position: absolute; transform: translate(-50%, -50%) for centering.
  • Update in afterRender using renderer.graphToViewport() to convert graph coordinates to screen pixels.

Tips

  • Use pointer-events: none on the overlay container so mouse events pass through to sigma.
  • Use overflow: hidden to 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 use graphToViewport() to position elements.
  • If you need elements to interact with mouse events (e.g. clickable labels), set pointer-events: auto on those specific elements.