<div id="sigma-container"></div>
:global(.clusters-layer) {
:global(.cluster-label) {
transform: translate(-50%, -50%);
font-variant: small-caps;
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);
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;
const viewportPos = renderer.graphToViewport(cluster as Coordinates);
el.style.top = `${viewportPos.y}px`;
el.style.left = `${viewportPos.x}px`;