Custom WebGL layers
The @sigma/layer-webgl package lets you add custom WebGL rendering layers behind the graph. The most common use case
is drawing contours around groups of nodes to highlight communities. Here are some examples:
Highlight communities
Draw smooth contour shapes around groups of nodes to visualize clusters:
import { bindWebGLLayer, createContoursProgram } from "@sigma/layer-webgl";import Graph from "graphology";import louvain from "graphology-communities-louvain";import Sigma, { DEFAULT_DEPTH_LAYERS } from "sigma";
import { registerControls } from "../_controls";import data from "../_data/data.json";
const COMMUNITY_COLORS = [ "#e63946", "#457b9d", "#2a9d8f", "#e9c46a", "#f4a261", "#6a4c93", "#8ac926", "#ff6b6b", "#4cc9f0", "#b5838d",];
const graph = new Graph();graph.import(data);
// Detect communitieslouvain.assign(graph, { nodeCommunityAttribute: "community" });const communities = new Set<string>();graph.forEachNode((_, attrs) => communities.add(attrs.community));const communitiesArray = Array.from(communities);
// Assign colors per communityconst palette: Record<string, string> = {};communitiesArray.forEach((c, i) => (palette[c] = COMMUNITY_COLORS[i % COMMUNITY_COLORS.length]));graph.forEachNode((node, attrs) => graph.setNodeAttribute(node, "color", palette[attrs.community]));
// Declare one depth layer per communityconst communityDepths = communitiesArray.map((c) => `community-${c}`);
const container = document.getElementById("sigma-container") as HTMLElement;const renderer = new Sigma(graph, container, { primitives: { // Insert the metaballs depth layers between the edges and the nodes: depthLayers: ["edges", ...communityDepths, "nodes", "topNodes"], },});
// Track active contour layer cleanup functionsconst activeLayers: Record<string, (() => void) | null> = {};
function showContour(community: string) { if (activeLayers[community]) return; activeLayers[community] = bindWebGLLayer( `community-${community}`, renderer, createContoursProgram( graph.filterNodes((_, attr) => attr.community === community), { radius: 150, border: { color: palette[community], thickness: 8 }, levels: [{ color: palette[community] + "22", threshold: 0.5 }], }, ), );}
function hideContour(community: string) { if (activeLayers[community]) { activeLayers[community]!(); activeLayers[community] = null; }}
// Register a toggle per communityconst controls: Record< string, { type: "toggle"; label: string; default: boolean; action: (active: boolean) => void }> = {};communitiesArray.forEach((community, i) => { controls[`community-${i}`] = { type: "toggle", label: `Group ${i + 1}`, default: i === 0, action: (active) => { if (active) showContour(community); else hideContour(community); }, };});registerControls(controls);
// Show the first community's contour on loadshowContour(communitiesArray[0]);Multi-level contours
Layered threshold fills that create a topographic effect around node groups:
import { bindWebGLLayer, createContoursProgram } from "@sigma/layer-webgl";import Graph from "graphology";import Sigma, { DEFAULT_DEPTH_LAYERS } from "sigma";
import data from "../_data/data.json";import { DEFAULT_STYLES } from "sigma/types";import { layerBorder } from "@sigma/node-border";
const graph = new Graph();graph.import(data);graph.forEachNode((node, { size }) => graph.setNodeAttribute(node, "size", size / 2));
const container = document.getElementById("sigma-container") as HTMLElement;const renderer = new Sigma(graph, container, { primitives: { depthLayers: ["contours", ...DEFAULT_DEPTH_LAYERS], nodes: { layers: [ layerBorder({ borders: [ { size: 0.3, color: "black", mode: "relative" }, { fill: true, color: "white" }, ], }), ], }, }, styles: { nodes: [DEFAULT_STYLES.nodes, { labelBackgroundColor: "#fff7f3cc" }], edges: [DEFAULT_STYLES.edges, { color: "black" }], },});
bindWebGLLayer( "contours", renderer, createContoursProgram(graph.nodes(), { radius: 150, feather: 1, border: { color: "#000000", thickness: 4, }, levels: [ { color: "#fff7f3", threshold: 4.0 }, { color: "#fde0dd", threshold: 3.5 }, { color: "#fcc5c0", threshold: 3.0 }, { color: "#fa9fb5", threshold: 2.5 }, { color: "#f768a1", threshold: 2.0 }, { color: "#dd3497", threshold: 1.5 }, { color: "#ae017e", threshold: 1.0 }, { color: "#7a0177", threshold: 0.5 }, { color: "#49006a", threshold: -0.1 }, ], }),);Heatmap
Density-based heatmap layer that visualizes node concentration:
import { bindWebGLLayer, createHeatmapProgram } from "@sigma/layer-webgl";import Graph from "graphology";import { parse } from "graphology-gexf/browser";import Sigma, { DEFAULT_DEPTH_LAYERS } from "sigma";
import { registerControls } from "../_controls";
const container = document.getElementById("sigma-container") as HTMLElement;
const res = await fetch("/data/arctic.gexf");const gexf = await res.text();const graph = parse(Graph, gexf);
// Style all nodes and edges whitegraph.forEachNode((node) => { graph.setNodeAttribute(node, "color", "#ffffff");});
let showEdges = false;
const renderer = new Sigma(graph, container, { primitives: { depthLayers: ["heatmap", ...DEFAULT_DEPTH_LAYERS], }, edgeReducer: (_key, data) => (showEdges ? data : { ...data, visibility: "hidden" }),});
const HEATMAP_COLOR_STOPS = [ { value: 0.1, color: "#ffffff" }, { value: 1, color: "#fdcc8a" }, { value: 10, color: "#e34a33" }, { value: 100, color: "#b30000" }, { value: 1000, color: "#500000" },];const HEATMAP_SHADING = { intensity: 0.0001, specular: 0.15, shininess: 24, lightAngle: 315, rotateWithCamera: true,};
let cleanHeatmap: (() => void) | null = null;let heatmapOn = true;let shadingOn = true;
function rebuildHeatmap() { if (cleanHeatmap) { cleanHeatmap(); cleanHeatmap = null; } if (!heatmapOn) { container.style.backgroundColor = "#333333"; return; } container.style.backgroundColor = "#ffffff"; cleanHeatmap = bindWebGLLayer( "heatmap", renderer, createHeatmapProgram(graph.nodes(), { radius: 50, colorStops: HEATMAP_COLOR_STOPS, getWeight: (node) => graph.degree(node), shading: shadingOn ? HEATMAP_SHADING : undefined, }), );}
const { setToggle, setDisabled } = registerControls({ heatmap: { type: "toggle", label: "Show heatmap", default: heatmapOn, action: (active: boolean) => { heatmapOn = active; rebuildHeatmap(); setDisabled("shading", !heatmapOn); }, }, shading: { type: "toggle", label: "Show shading", default: shadingOn, action: (active: boolean) => { shadingOn = active; rebuildHeatmap(); }, }, edges: { type: "toggle", label: "Show edges", default: showEdges, action: (active: boolean) => { showEdges = active; renderer.refresh(); }, },});
rebuildHeatmap();Installation
npm install @sigma/layer-webglDrawing contours
The built-in createContoursProgram renders smooth contour shapes around a set of nodes. That can be useful for
visualizing communities or clusters.
import { bindWebGLLayer, createContoursProgram } from "@sigma/layer-webgl";import Sigma from "sigma";
const renderer = new Sigma(graph, container, { primitives: { depthLayers: ["edges", "community-highlight", "nodes", "topNodes"], },});
// Draw a red contour around a group of nodes:const communityNodes = ["node1", "node2", "node3", "node4"];const clean = bindWebGLLayer( "community-highlight", renderer, createContoursProgram(communityNodes, { radius: 150, border: { color: "#e22653", thickness: 8 }, levels: [{ color: "#e2265322", threshold: 0.5 }], }),);
// Remove the layer when no longer needed:clean();Contours options
| Option | Type | Default | Description |
|---|---|---|---|
radius | number | 100 | Influence radius of each node |
feather | number | 1.5 | Softness of the contour edges |
levels | { color, threshold }[] | One gray level | Fill levels, from outermost to innermost |
border | { color, thickness } | none | Optional border stroke |
zoomToRadiusRatioFunction | (ratio) => number | Math.sqrt | How the radius scales with zoom |
You can define multiple fill levels to create layered contours:
createContoursProgram(nodeIds, { radius: 200, levels: [ { color: "#e2265311", threshold: 0.3 }, { color: "#e2265333", threshold: 0.6 }, { color: "#e2265566", threshold: 0.9 }, ], border: { color: "#e22653", thickness: 4 },});Multiple layers
Each call to bindWebGLLayer requires a unique string ID. You can add as many layers as you need:
const cleanA = bindWebGLLayer("highlights", renderer, createContoursProgram(groupA, optionsA));const cleanB = bindWebGLLayer("highlights", renderer, createContoursProgram(groupB, optionsB));
// Remove individually:cleanA();cleanB();