Skip to content

Shapes

By default, sigma renders all nodes as circles. To use other shapes, declare them in primitives.nodes.shapes first, and then assign shapes to nodes via a style rule.

Built-in shapes

Sigma ships with five built-in SDF shapes: sdfCircle(), sdfSquare(), sdfTriangle(), sdfDiamond(), and sdfRectangle(). Each node selects its shape through the shape style property:

import Graph from "graphology";
import Sigma, { DEFAULT_STYLES } from "sigma";
import { layerFill, sdfCircle, sdfDiamond, sdfSquare, sdfTriangle } from "sigma/rendering";
const container = document.getElementById("sigma-container") as HTMLElement;
const graph = new Graph();
const SHAPES = ["circle", "square", "triangle", "diamond"];
const COLORS = ["#e22653", "#0055ff", "#33cc33", "#ff9900"];
// Create a grid of nodes showing each shape at different sizes
for (let row = 0; row < SHAPES.length; row++) {
const shape = SHAPES[row];
const color = COLORS[row];
for (let col = 0; col < 4; col++) {
const size = 10 + col * 8;
graph.addNode(`${shape}-${col}`, {
x: col * 200,
y: -row * 200,
size,
color,
shape,
label: col === 0 ? shape : "",
});
}
}
new Sigma(graph, container, {
primitives: {
nodes: {
shapes: [sdfCircle(), sdfSquare(), sdfTriangle(), sdfDiamond()],
layers: [layerFill()],
},
},
styles: {
nodes: [
DEFAULT_STYLES.nodes,
{
shape: { attribute: "shape" },
},
],
},
});

Fixed shape for all nodes

If you want all nodes to use a single non-default shape, declare just that shape and set it as a fixed value in styles:

import { sdfSquare } from "sigma/rendering";
new Sigma(graph, container, {
primitives: {
nodes: {
shapes: [sdfSquare()],
},
},
styles: {
nodes: [{ shape: "square", color: { attribute: "color" }, size: { attribute: "size" } }],
},
});

Rectangles

sdfRectangle() draws a rectangle inside the node’s bounding square: its long side spans the node’s full size. Its aspectRatio (width / height) is either a fixed number, or read from a node attribute. By default, it reads each node’s aspectRatio attribute, and falls back to 1 (a square):

import { sdfRectangle } from "sigma/rendering";
// Same aspect ratio for every node:
sdfRectangle({ aspectRatio: 2 });
// Aspect ratio read from each node's "aspectRatio" attribute:
sdfRectangle();
// Aspect ratio read from another attribute:
sdfRectangle({ aspectRatio: { attribute: "imageRatio", default: 1 } });

This makes it easy to draw images with their real proportions:

import { layerImage } from "@sigma/node-image";
import Sigma, { DEFAULT_STYLES } from "sigma";
import { extremityArrow, layerFill, pathCurved, sdfRectangle } from "sigma/rendering";
import { registerControls } from "../_controls";
import { getSmallGraph } from "../_data/small-graph";
const container = document.getElementById("sigma-container") as HTMLElement;
const { objectFit } = registerControls({
objectFit: {
type: "select",
label: "Object fit",
default: "contain",
options: [
{ label: "Contain", value: "contain" },
{ label: "Cover", value: "cover" },
],
},
});
const IMAGE_SIZES: Record<string, { id: number; width: number; height: number }> = {
a: { id: 10, width: 600, height: 400 },
b: { id: 15, width: 400, height: 600 },
c: { id: 28, width: 600, height: 600 },
d: { id: 29, width: 800, height: 300 },
e: { id: 37, width: 300, height: 800 },
f: { id: 42, width: 500, height: 350 },
};
const graph = getSmallGraph();
graph.forEachNode((node) => {
const { id, width, height } = IMAGE_SIZES[node];
graph.mergeNodeAttributes(node, {
image: `https://picsum.photos/id/${id}/${width}/${height}`,
aspectRatio: width / height,
label: `${width} x ${height}`,
});
});
new Sigma(graph, container, {
primitives: {
nodes: {
shapes: [sdfRectangle({ cornerRadius: 0.2 })],
variables: {
image: { type: "string", default: "" },
},
layers: [layerFill({ color: "#eee" }), layerImage({ textureManagerOptions: { objectFit: objectFit as "contain" | "cover" } })],
},
edges: {
paths: [pathCurved({ curvature: 0.5 })],
extremities: [extremityArrow({ lengthRatio: 4, widthRatio: 4 })],
},
},
styles: {
nodes: [
DEFAULT_STYLES.nodes,
{
backdropArea: "label",
labelPosition: "below",
size: 40,
},
],
edges: [
DEFAULT_STYLES.edges,
{
path: "curved",
head: "arrow",
size: 8,
},
],
},
settings: {
autoRescaleContent: "nodes",
},
});

Custom shapes

You can create custom shapes by implementing the SDFShape interface. A shape is defined by a GLSL signed distance function, i.e. a function that returns the distance from a point to the shape boundary (negative inside, positive outside).

The GLSL function must follow the naming convention sdf_{name}(vec2 uv, float size, ...). The uv parameter is the position in the node’s quad, normalized to [-1, 1] with (0, 0) at the node center, and size is the shape’s half-size in the same units (about 1). Additional parameters are, in order:

  1. The float uniforms declared in the uniforms array, baked into the shader as constants
  2. The per-node attributes declared in the optional attributes array, read from the node attribute texture

Shape attributes work like layer attributes: declare the matching variables on the shape, and they become style properties, read from node attributes by default.

The inradiusFactor is the ratio of inradius to circumradius. Sigma uses it to size backdrops around the shape, and layers like layerBorder() use it to know how deep the shape is. For a circle it’s 1.0, for a square it’s ~0.707 (half of Math.sqrt(2)). When the depth varies per node, inradiusFactorGLSL can override it for layers with a GLSL expression, reading shape attributes as v_<name> (e.g. "0.7 / v_aspectRatio"). Backdrops still use inradiusFactor, so keep it as a safe upper bound.

For a comprehensive catalog of 2D SDF primitives, see Inigo Quilez’s reference.

The example below defines a custom heart shape and uses it alongside the built-in circle:

import Sigma, { DEFAULT_STYLES } from "sigma";
import { layerFill, sdfCircle } from "sigma/rendering";
import type { SDFShape } from "sigma/rendering";
import { getSmallGraph } from "../_data/small-graph";
const container = document.getElementById("sigma-container") as HTMLElement;
/**
* Custom heart SDF shape.
* Based on Inigo Quilez's 2D distance functions:
* https://iquilezles.org/articles/distfunctions2d/
*/
function sdfHeart(): SDFShape {
// The IQ heart SDF has its tip at the origin and extends up to ~y=1.
// We shift p.y by -0.5 to center the heart on the node origin,
// and scale by 0.7 so it fits within the node quad with AA room.
const SCALE = 0.7;
const Y_CENTER = 0.5;
// language=GLSL
const glsl = /*glsl*/ `
float dot2_heart(vec2 v) { return dot(v, v); }
float sdf_heart(vec2 uv, float size) {
float scale = ${SCALE.toFixed(4)};
// Normalize to unit space, then shift so the heart's center is at the origin
vec2 p = uv / (size * scale);
p.y += ${Y_CENTER.toFixed(4)};
p.x = abs(p.x);
float d;
if (p.y + p.x > 1.0) {
d = sqrt(dot2_heart(p - vec2(0.25, 0.75))) - sqrt(2.0) / 4.0;
} else {
d = sqrt(min(dot2_heart(p - vec2(0.0, 1.0)), dot2_heart(p - 0.5 * max(p.x + p.y, 0.0)))) * sign(p.x - p.y);
}
return d * size * scale;
}
`;
return {
name: "heart",
glsl,
uniforms: [],
inradiusFactor: 0.5,
};
}
const graph = getSmallGraph();
graph.forEachNode((node) => {
graph.setNodeAttribute(node, "shape", node === "a" ? "circle" : "heart");
});
new Sigma(graph, container, {
primitives: {
nodes: {
shapes: [sdfCircle(), sdfHeart()],
layers: [layerFill()],
},
},
styles: {
nodes: [DEFAULT_STYLES.nodes, { shape: { attribute: "shape" } }],
},
});