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

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 communities
louvain.assign(graph, { nodeCommunityAttribute: "community" });
const communities = new Set<string>();
graph.forEachNode((_, attrs) => communities.add(attrs.community));
const communitiesArray = Array.from(communities);
// Assign colors per community
const 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 community
const 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 functions
const 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 community
const 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 load
showContour(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 white
graph.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

Terminal window
npm install @sigma/layer-webgl

Drawing 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

OptionTypeDefaultDescription
radiusnumber100Influence radius of each node
feathernumber1.5Softness of the contour edges
levels{ color, threshold }[]One gray levelFill levels, from outermost to innermost
border{ color, thickness }noneOptional border stroke
zoomToRadiusRatioFunction(ratio) => numberMath.sqrtHow 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();