Sigma v4.0.0

Sigma version 3, released barely two years ago, did the library a lot of good, especially regarding code maintainability and documentation. But some issues turned out quite quickly to be impossible to solve without deep structural changes. We started collecting these issues in a new milestone as early as September 2024, waiting until we had the resources to tackle them.

Among these problems:

  • It was impossible to render edges on top of nodes (#1233)
  • There were bugs in the way transparency was handled (#1426)
  • Nodes rendered by composed programs were drawn in multiple passes, which mixed up the “pieces” of overlapping nodes (#1427)

In the meantime, we kept discussing with Arthur Bigeard from gdotv, who had already sponsored v3. For him, the last area where sigma was strictly behind its alternatives was the customization of how elements are rendered. Having to rewrite whole programs, to write GLSL shaders far too often, or to deal with the canvas APIs for label rendering, was still too complex.

Arthur wanted to sponsor sigma’s development once again to build a v4. Thanks to him, I was able, in late 2025, to take the time needed to tackle this large-scale project.

Rethinking the rendering engine

The ideas that looked good on paper

The heart of the problem is depth: how do you control the display order of elements without having to reorder the data sent to the GPU?

To solve this, I tested two different mechanisms:

  1. Using a WebGL depth buffer to display elements at arbitrary depths. Major drawback: it only works well for fully opaque elements, with no empty or semi-transparent pixels.
  2. Using Order-Independent Transparency, but it produces an approximate result, and only works when all elements are partly transparent. Worse, this technique makes it impossible to have perfectly opaque elements.

Yet the most common case for nodes and edges in sigma is elements that are opaque inside, with empty pixels around them, and a few semi-transparent pixels in between, for anti-aliasing.

So neither solution fits our context.

A single render pass, in a single layer

I therefore revisited the list of constraints:

  1. We want to draw elements always in their depth order
  2. We want to be able to bring a few elements to the foreground without redrawing everything (hover, selection), in a performant way
  3. We want a continuous and precise zIndex, for instance as we do in Gephi Lite to observe uncertainty in the Louvain method

The first impactful decision: drawing everything with a single program per element type.

Node shapes, edge paths and their endpoints (arrow heads…) are described as Signed Distance Fields, then each pixel goes through a stack of fillings. A node is thus drawn in a single pass, whatever its complexity, which solves #1427.

And this opens up many rendering possibilities for nodes and edges:

The example from the sigma v4 docs showcasing the new edge rendering capabilities(source)

To feed these programs, we also changed the way sigma stores its data: all node and edge data are stored flat in GPU textures. The programs read directly what they need from them, which reduces the memory footprint.

The second impactful decision: grouping everything in a single WebGL layer.

Labels, until now drawn with Canvas, are also rendered with WebGL, using SDFs compiled from the requested font. Any type of element can now be drawn on top of any other, which solves #1233.

Finally, to cover all our depth-related use cases, we have two complementary mechanisms:

  1. Sigma now has discrete named layers (nodes, topNodes…), and moving an element from one layer to another is almost free
  2. The pre-existing zIndex remains, and determines the order of elements of each type within each layer

And we took the opportunity to fix the transparency issues, which solves #1426:

A network of Wikipedia pages, with semi-transparent nodes and edges(source)

Thanks to these structural changes to the rendering engine, sigma v4 offers new graphic possibilities while considerably improving its rendering performance.

Rethinking the rendering APIs

To design how sigma v4 should address rendering customization, we relied on the various projects we built ourselves with sigma v3 (GraphCommons, Gephi Lite, Retina…). In most cases:

  1. We synchronize graphic variables (size, color…) with the metadata of elements (nodes, edges, the graph itself)
  2. We change the appearance of elements during various interactions (highlighting the neighbors of a hovered node, for instance), through a dedicated state and sigma v3’s reducers

To generalize these patterns, sigma v4 introduces three new concepts:

  1. primitives describe what sigma compiles into the programs. They determine which graphic variables exist in a given instance.
  2. styles determine the values of these variables, statically or dynamically.
  3. Finally, sigma now maintains states that carry the application state (per element, and globally), and that can be referenced from the styles.

With all this, we can quite simply describe how the various elements of the graph should be drawn, relative to what we know about them. For instance, here is a condensed version of the code of the demo below:

import { layerDashed, layerPlain } from "sigma/rendering";

const renderer = new Sigma(graph, container, {
  // Primitives declare the graphic variables:
  primitives: {
    edges: {
      variables: {
        dashColor: { type: "color", default: "#000" },
        dashSize: { type: "number", default: 0 },
      },
      layers: [
        layerPlain(),
        layerDashed({
          dashColor: { attribute: "dashColor" },
          dashSize: { attribute: "dashSize", mode: "pixels" },
        }),
      ],
    },
  },
  // Styles give these variables their values:
  styles: {
    nodes: [
      { color: "#C1553F", size: { attribute: "wealth", min: 4, max: 12 } },
      { whenState: "isDragged", then: { cursor: "grabbing" } },
    ],
    edges: [
      {
        matchData: "type",
        cases: {
          marriage: { color: "#E8684A" },
          business: { color: "transparent", dashColor: "#5B8FF9", dashSize: 4 },
        },
      },
    ],
  },
});

The Florentine families in Renaissance Italy, from Padgett & Ansell

(source)

marriages

business ties

wealth

And while we’re at it…

We also took the opportunity to add, in no particular order:

We even started experimenting with a fully GPU-based ForceAtlas2 implementation, which reads node and edge data directly from the GPU textures, without going through graphology nor the CPU. And it looks very promising!

Where do we stand?

At this point, the gdotv team has been using v4 for several months. Their feedback allowed us to fix a huge number of long-standing issues, and v4 is now production ready 🥳.

Once again, a huge thank you to them for giving us the means to considerably improve sigma over the past two years 🙏.

Give it a try! To migrate from v3 to v4, follow the migration guide.