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

Depth and z-order

Sigma v4 controls rendering order through two style properties that work together:

  • depth: a named bucket from primitives.depthLayers. Coarse, categorical.
  • zIndex: a numeric sub-order within a bucket. Fine, continuous.

Everything renders into the same WebGL canvas. There is no separate node-layer and edge-layer pass: each named bucket is painted in turn, and within a bucket sigma paints sub-elements (edges, labels, nodes) in a fixed order.

The stack

The default depthLayers declaration is ["edges", "nodes", "topEdges", "topNodes"], painted back-to-front:

top of the stack ┌─ topNodes ── hovered nodes + their labels
│ topEdges ── hovered / highlighted edges + their labels
│ nodes ── regular nodes + their labels
bottom of stack └─ edges ── regular edges + their labels

You can declare any number of buckets, in any order, with any names:

primitives: {
depthLayers: ["edges", "nodes", "activeEdges", "activeNodes", "topNodes"],
}

A node or edge is placed into a bucket with the depth style property, which must reference a name from depthLayers:

styles: {
nodes: [{ whenState: "isActive", then: { depth: "activeNodes" } }],
}

Sub-order within a bucket: zIndex

zIndex orders items inside a single bucket. Items in a later bucket always paint above items in an earlier one, regardless of zIndex. Inside one bucket, higher zIndex paints on top.

zIndex is a continuous numeric sort key. Any finite value works (negative, fractional, large). Items in a bucket are sorted by zIndex on the CPU during processing.

Use zIndex when you want a continuous ordering (e.g. “sort nodes by degree”). Use depth when you want a categorical bucket that interactions can promote items into.

Paint order within a bucket

For each bucket, sigma paints in this fixed order:

  1. Custom WebGL layer bound to this bucket (see WebGL layers)
  2. Edges in this bucket
  3. Edge label backgrounds, then edge labels (labelDepth === bucket)
  4. Backdrops for nodes whose depth === bucket
  5. Nodes in this bucket
  6. Label attachments (labelDepth === bucket)
  7. Node label backgrounds, then node labels (labelDepth === bucket)

Two consequences worth highlighting:

  • Edge labels render below nodes of the same bucket. If you want a label on top of unrelated nodes, route the label into a higher bucket with labelDepth.
  • Nodes always render above edges of the same bucket. To put an edge above a node, the edge needs to live in a higher bucket (this is the main reason topEdges sits above nodes in the default declaration).

Labels in a different bucket: labelDepth

Each item has two depth properties: depth for the geometry, labelDepth for the label. By default labelDepth mirrors depth, so if a rule sets depth without labelDepth, the label follows.

Override labelDepth to detach the label from its parent:

styles: {
nodes: [
// Nodes geometry stays in "nodes", but node labels paint above everything else:
{ labelDepth: "topNodes" },
],
}

This is the v4 answer to the recurring v3 issue of “labels hidden under unrelated nodes”.

What sigma sets by default

The built-in DEFAULT_STYLES already does the most common interaction:

StateEffect
Hovered nodedepth: "topNodes", zIndex: 1
Hovered or highlighted edgedepth: "topEdges", zIndex: 1
Otherwisedepth: "nodes" / "edges", zIndex: 0

If you spread DEFAULT_STYLES.nodes into your rules, you keep this behavior for free. Add further rules to promote items into your own buckets.

Cost model

  • Each named bucket costs one node draw call and one edge draw call (skipped when the bucket is empty; occasionally split into a few contiguous fragments when items have recently moved between buckets).
  • Changing an item’s depth moves it between buckets in place, without re-processing the rest of the graph.
  • Changing an item’s zIndex re-sorts the bucket and triggers a full reprocess, so treat zIndex as a mostly-static order, not a per-frame interaction axis.
  • Adding buckets is cheap. Adding hundreds of buckets is not. Keep depthLayers to a handful of named values, and use zIndex for the fine-grained order.

Choosing between depth and zIndex

QuestionAnswer
”Bring some items on top during an interaction”depth
”Render some edges above some nodes”depth (separate buckets)
“Render labels above nodes from other clusters”labelDepth
”Sort nodes by a numeric attribute (e.g. degree)“zIndex
”Render type-A nodes always above type-B nodes”depth (a bucket per type)
“I have one item I always want on top, regardless of state”depth: "topNodes"

Common recipes

Pin selected items on top

primitives: {
depthLayers: ["edges", "nodes", "topEdges", "topNodes"],
},
styles: {
nodes: [
DEFAULT_STYLES.nodes,
{ whenState: "isSelected", then: { depth: "topNodes" } },
],
}

Sort nodes by degree

styles: {
nodes: [
{
zIndex: { attribute: "degree" },
},
],
}

Any numeric attribute works directly as a zIndex — no range to bind, no clamping.

Labels above unrelated nodes

primitives: {
depthLayers: ["edges", "nodes", "topNodes"],
},
styles: {
nodes: [
// Nodes geometry stays in "nodes" so other nodes can still overlap:
{ depth: "nodes", labelDepth: "topNodes" },
],
}