Skip to content
sigma.js sigma.js
v4.0.6 Docs Examples GitHub 12.2K

Styles and primitives

The styles and primitives system is sigma v4’s primary mechanism for controlling visual appearance. It replaces the old nodeProgramClasses / edgeProgramClasses approach with a declarative, composable system.

The two concepts serve different roles:

  • Primitives describe what can render – shapes, layers, paths, extremities, and the variables they consume.
  • Styles describe how things render – rules that map graph attributes and state to visual properties.

Primitives define the rendering pipeline. They tell sigma which shapes, layers, paths, and extremities to compile into WebGL programs. Sigma generates one single optimized program for all nodes and another one for all edges from the entire declaration.

import { layerFill, sdfCircle, sdfDiamond, sdfSquare, sdfTriangle } from "sigma/rendering";
new Sigma(graph, container, {
// ...
primitives: {
nodes: {
// Geometric outlines available for nodes
shapes: [sdfCircle(), sdfSquare(), sdfTriangle(), sdfDiamond()],
// Visual layers composited from back to front
layers: [
layerFill(),
layerBorder({ borders: [{ size: 0.1, color: "#333" }] }),
layerImage({ imageAttribute: "image" }),
],
// Custom typed attributes that layers can consume
variables: {
borderSize: { type: "number", default: 0 },
borderColor: { type: "color", default: "transparent" },
},
},
},
// ...
});

Shapes describe all the shapes nodes can have in a graph. The built-in shapes are sdfCircle(), sdfSquare(), sdfTriangle(), sdfDiamond(), and sdfRectangle() (with sdf standing for Signed Distance Field). The page Styling nodes > Shapes shows how to write a custom SDF shape for sigma. Finally, all declared shapes are compiled into a single program, and each node picks its shape through the shape style property.

Layers describe how the shape is filled (simple color, borders, images, etc…). They are composited from back to front to build up the node’s appearance. Built-in layer types include:

Layer Package Description
layerFill() sigma/rendering Solid color fill using the node’s color
layerBorder() @sigma/node-border Configurable concentric borders
layerImage() @sigma/node-image Image or pictogram rendered inside the shape
layerPiechart() @sigma/node-piechart Pie chart slices

Layers automatically disable themselves when they have no data. For example, an image layer on a node with no image attribute renders as transparent. This means all nodes share the same WebGL program regardless of which layers they use.

import { extremityArrow, layerPlain, pathCurved, pathCurvedS, pathLine, pathStepCurved } from "sigma/rendering";
new Sigma(graph, container, {
// ...
primitives: {
edges: {
// Path types available for edges
paths: [pathLine(), pathCurved(), pathStepCurved(), pathCurvedS()],
// Extremity types (arrowheads, etc.)
extremities: [extremityArrow()],
// Visual layers composited together
layers: [layerPlain()],
},
},
// ...
});

Paths control the geometric route an edge takes between its endpoints. Built-in paths include pathLine(), pathCurved(), pathStep(), pathStepCurved(), and pathCurvedS().

Extremities are shapes drawn at the endpoints of edges. Built-in extremities are extremityArrow(), extremityBar(), extremityCircle(), extremityDiamond() and extremitySquare(). Omit the array or leave it empty for no extremity.

Layers describe how the path is filled (simple color, dashed, gradient, etc…). As for nodes, they are composited from back to front to build up the edge’s appearance. Built-in layer types include layerPlain(), layerDashed() and layerGradient().

The depthLayers array controls the rendering order of elements. Elements assigned to a later depth layer render on top of earlier ones. Within a depth layer, sigma always paints in the same order: edges, edge labels, nodes, node labels.

new Sigma(graph, container, {
// ...
primitives: {
// Here are the default depth layers:
depthLayers: [
"edges", // Background edges (and their labels)
"nodes", // Regular nodes (and their labels)
"topEdges", // Highlighted edges (and their labels)
"topNodes", // Highlighted nodes (and their labels)
],
},
// ...
});

Nodes, edges and their labels are assigned to depth layers through the depth and labelDepth style properties. By default, labelDepth mirrors depth: labels follow their parent item’s layer unless explicitly overridden.

Variables are custom typed attributes declared in the primitives. They bridge the gap between graph data and WebGL rendering.

import { layerBorder } from "@sigma/node-border";
import { layerFill } from "sigma/rendering";
new Sigma(graph, container, {
// ...
primitives: {
nodes: {
variables: {
borderSize: { type: "number", default: 0 },
borderColor: { type: "color", default: "transparent" },
},
layers: [
layerFill(),
layerBorder({
borders: [
{ size: { attribute: "borderSize" }, color: { attribute: "borderColor" } },
{ size: 0, color: { attribute: "color" }, fill: true },
],
}),
],
},
},
// ...
});

Each variable has a type ("number", "color", "string", "boolean") and a default value. When a style references { attribute: "borderSize" }, sigma reads the value from the node’s graph attributes via the declared variable. If the attribute is missing, the default is used instead.

Variables that are declared in primitives also become available as style properties, so you can set them through the styles system:

new Sigma(graph, container, {
// ...
styles: {
nodes: {
borderSize: { attribute: "borderSize", defaultValue: 0 },
borderColor: {
whenState: "isHovered",
then: "#e22653",
else: { attribute: "borderColor", defaultValue: "transparent" },
},
},
},
// ...
});

Styles are ordered rules that determine how nodes and edges look. They map graph attributes, element state, and graph state to visual properties.

A style property can be set using several value types:

Literal value: a fixed value applied to all elements:

{ color: "#e22653", size: 10 }

Attribute binding: reads a value from the element’s graph attributes:

{ color: { attribute: "color", defaultValue: "#666" } }

Categorical binding: maps attribute values to specific outputs via a dictionary:

{
color: {
attribute: "community",
dict: { science: "green", art: "red", tech: "blue" },
defaultValue: "grey",
},
}

Numerical binding: maps a numeric attribute to a min/max range:

{
size: {
attribute: "degree",
min: 3,
max: 20,
minValue: 0,
maxValue: 100,
},
}

Function: full control via a callback:

{
color: (attributes, state, graphState, graph) => {
return state.isHovered ? "#e22653" : attributes.color;
},
}

Inline conditional: a concise conditional within a single property:

{
size: {
whenState: "isHovered",
then: 15,
else: { attribute: "size", defaultValue: 10 },
},
}

When you need to change multiple properties based on the same condition, use a rule-level conditional. The whenState clause gates the entire rule:

new Sigma(graph, container, {
// ...
styles: {
nodes: [
// Base style: always applied
{ color: { attribute: "color" }, size: { attribute: "size" } },
// Conditional rule: only applied when hovered
{
whenState: "isHovered",
then: { size: 15, labelVisibility: "visible" },
},
],
},
// ...
});

Rules are evaluated in order. Later rules override earlier ones for any properties they set.

When you need to branch styles on a categorical attribute (e.g. node type), use matchData/cases instead of function predicates:

new Sigma(graph, container, {
// ...
styles: {
nodes: [
{ color: "#666", size: 10 },
{
matchData: "type",
cases: {
person: { shape: "circle", color: "#e22653" },
company: { shape: "square", color: "#277da1" },
},
},
],
},
// ...
});

There are three forms of shorthand predicates, plus a function escape hatch:

whenState / matchState match against element state flags:

// String: true if the state flag is true
whenState: "isHovered"
// Array: true if ALL flags are true (AND)
whenState: ["isHovered", "isActive"]
// Object: true if all specified values match
whenState: { isHovered: true, isActive: false }
// Categorical branch on a state key
matchState: "status"

whenData / matchData match against graph attributes (static, re-evaluated only when data changes):

// String: true if the attribute is truthy
whenData: "important";
// Categorical branch on an attribute value
matchData: "type";

when expects a full function predicate (re-evaluated on every state change):

when: (attributes, state, graphState, graph) => graphState.hasActiveSubgraph && state.isActive;

For the complete list of built-in state flags, see the State flags reference. For all available style properties, see the Style properties reference.

Here is a complete example combining primitives and styles to render nodes with borders that highlight on hover:

import { layerBorder } from "@sigma/node-border";
import Graph from "graphology";
import Sigma from "sigma";
import { extremityArrow, layerFill, pathLine, sdfCircle } from "sigma/rendering";
const graph = new Graph();
graph.addNode("a", { x: 0, y: 0, size: 15, color: "#e22653", label: "Alice" });
graph.addNode("b", { x: 100, y: -100, size: 10, color: "#277da1", label: "Bob" });
graph.addEdge("a", "b");
const container = document.getElementById("sigma-container") as HTMLElement;
const renderer = new Sigma(graph, container, {
primitives: {
nodes: {
shapes: [sdfCircle()],
layers: [
layerFill(),
layerBorder({
borders: [
{ size: 0.1, color: "#333" },
{ size: 0, color: { attribute: "color" }, fill: true },
],
}),
],
},
edges: {
paths: [pathLine()],
extremities: [extremityArrow()],
},
},
styles: {
nodes: [
{
color: { attribute: "color" },
size: { attribute: "size", defaultValue: 10 },
label: { attribute: "label" },
},
{
whenState: "isHovered",
then: {
size: 20,
depth: "topNodes",
labelVisibility: "visible",
},
},
],
edges: [{ color: "#ccc", size: 2, head: "arrow" }],
},
});

See the hover and search highlight guide for a more complete interactive example.