Sigma v4.0.0

La version 3 de sigma, sortie il y a à peine deux ans, a fait beaucoup de bien à la bibliothèque, notamment sur la maintenabilité du code et la documentation. Mais assez vite, certains tickets sont apparus comme impossibles à résoudre sans faire des changements de fond. Nous avons commencé à référencer ces tickets dans une nouvelle milestone dès septembre 2024, en attendant d’avoir les ressources pour nous y atteler.

Parmi les problèmes, on peut noter :

  • Il était impossible de rendre des liens au-dessus de nœuds (#1233)
  • Il y avait des bugs avec la gestion de la transparence (#1426)
  • Les nœuds des programmes composés étaient dessinés en plusieurs passes, ce qui mélangeait les “morceaux” des nœuds, qui se chevauchaient (#1427)

En parallèle, nous avons continué nos discussions avec Arthur Bigeard, de gdotv, qui avait déjà sponsorisé la v3. Pour lui, le dernier domaine où sigma restait derrière ses alternatives était la personnalisation du rendu des éléments. Le système de programmes à réécrire, le fait de devoir trop souvent écrire des shaders GLSL, ou encore manipuler les APIs canvas pour le rendu des labels, étaient encore trop complexes.

Arthur a souhaité de nouveau sponsoriser le développement de sigma pour créer une v4. Grâce à lui, j’ai pu, fin 2025, prendre le temps nécessaire pour attaquer ce chantier.

Repenser le moteur de rendu

Les fausses bonnes idées

Le cœur du problème, c’est la profondeur : comment contrôler l’ordre d’affichage des éléments sans avoir à réordonner les données qu’on envoie au GPU ?

Pour résoudre cela, j’ai pu tester deux mécanismes différents :

  1. Utiliser un canal de profondeur en WebGL pour afficher les éléments à des profondeurs arbitraires. Inconvénient de taille : ça ne fonctionne bien que pour des éléments entièrement opaques, sans pixel vide, ni semi-transparent.
  2. Utiliser de l’Order-Independent Transparency, mais ça donne un rendu approximé, qui ne fonctionne que lorsque tous les éléments sont en partie transparents. Pire, il n’est pas possible avec cette technique d’avoir des éléments parfaitement opaques.

Or, le cas le plus courant pour les nœuds et les liens dans sigma, c’est d’avoir des éléments opaques à l’intérieur, mais avec des pixels vides autour, avec quelques pixels semi-transparents entre les deux, pour l’anti-crénelage.

Aucune des deux solutions ne convient donc à notre contexte.

Une seule passe de rendu, dans un seul layer

Du coup, j’ai revu la liste des contraintes :

  1. On veut dessiner les éléments toujours dans leur ordre de profondeur
  2. On veut pouvoir repasser quelques éléments au premier plan sans tout redessiner (survol, sélection), de manière performante
  3. On veut avoir un zIndex continu et précis, par exemple comme on fait dans Gephi Lite pour observer l’incertitude dans la méthode de Louvain

La première décision impactante : tout dessiner avec un seul programme par type d’élément.

Les formes des nœuds, les chemins des liens et leurs extrémités (têtes de flèches…) sont décrits en Signed Distance Fields, puis chaque pixel traverse une liste de remplissages. Un nœud se dessine ainsi en une seule passe, quelle que soit sa complexité, ce qui résout #1427.

Et ça donne beaucoup de possibilités de rendu pour les nœuds et pour les liens :

L’exemple de la doc de sigma v4 qui montre les nouvelles capacités de rendu des liens(source)

Pour alimenter ces programmes, on a aussi changé la façon dont sigma stocke ses données : toutes les données des nœuds et des liens sont stockées à plat dans des textures GPU. Les programmes y lisent directement ce dont ils ont besoin, ce qui réduit l’empreinte mémoire.

La seconde décision impactante : tout regrouper dans un seul layer WebGL.

Les labels, jusqu’ici dessinés en Canvas, passent eux aussi en WebGL, avec des SDF compilées depuis la police demandée. N’importe quel élément peut désormais être dessiné au-dessus de n’importe quel autre, ce qui résout #1233.

Enfin, pour couvrir tous nos cas de profondeur, on a deux mécanismes complémentaires :

  1. Sigma a maintenant des couches nommées (nodes, topNodes…) et discrètes, et changer un élément de couche est quasi gratuit
  2. Le zIndex présent auparavant persiste, et détermine l’ordre des éléments de chaque type au sein de chaque couche

Et on en a profité pour résoudre les problèmes de transparence, ce qui règle #1426 :

Un réseau de pages Wikipédia, avec les nœuds et les liens semi-transparents(source)

Grâce à ces changements structurels sur le moteur de rendu, sigma v4 offre de nouvelles possibilités graphiques tout en améliorant considérablement ses performances de rendu.

Repenser les APIs de rendu

Pour concevoir comment sigma v4 devrait traiter le problème de la personnalisation du rendu, nous nous sommes appuyés sur les différents projets développés par nous-mêmes avec sigma v3 (GraphCommons, Gephi Lite, Retina…). Dans la plupart des cas :

  1. On synchronise des variables graphiques (taille, couleur…) avec les métadonnées des éléments (nœuds, liens, graphe lui-même)
  2. On change l’apparence des éléments lors de différentes interactions (mettre en évidence les voisins d’un nœud au survol, par exemple), en passant par un état dédié et les reducers de sigma v3

En généralisant ces usages, nous avons ajouté trois nouveaux concepts à sigma v4 :

  1. Les primitives décrivent ce que sigma compile dans les programmes, ce sont elles qui déterminent quelles variables graphiques existent dans une instance donnée
  2. Les styles déterminent les valeurs de ces variables, statiquement ou dynamiquement
  3. Enfin, sigma maintient désormais des states qui portent l’état de l’application (par élément, et global), et qui peuvent être référencés depuis les styles

Avec tout ça, on peut assez simplement décrire comment les différents éléments du graphe doivent être dessinés, relativement à ce qu’on sait d’eux. Par exemple, voici une version condensée du code de la démo ci-dessous :

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

const renderer = new Sigma(graph, container, {
  // Les primitives déclarent les variables graphiques :
  primitives: {
    edges: {
      variables: {
        dashColor: { type: "color", default: "#000" },
        dashSize: { type: "number", default: 0 },
      },
      layers: [
        layerPlain(),
        layerDashed({
          dashColor: { attribute: "dashColor" },
          dashSize: { attribute: "dashSize", mode: "pixels" },
        }),
      ],
    },
  },
  // Les styles donnent les valeurs de ces variables :
  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 },
        },
      },
    ],
  },
});

Les familles florentines dans l’Italie de la Renaissance, de Padgett & Ansell

(source)

mariages

liens d’affaires

richesse

Et tant qu’à faire…

On en a aussi profité pour ajouter, en vrac :

On a même pu commencer à expérimenter sur une implémentation de ForceAtlas2 entièrement sur GPU, et qui lit les données des nœuds et des liens directement dans les textures GPU, sans passer par graphology ni par le CPU. Et c’est très prometteur !

Où en sommes-nous ?

À ce stade, l’équipe de gdotv utilise la v4 depuis plusieurs mois. Leurs retours ont permis de réparer énormément de problèmes qui traînaient, et la v4 est désormais production ready 🥳.

Merci encore infiniment à eux de nous avoir donné les moyens d’améliorer considérablement sigma ces deux dernières années 🙏.

Essayez-la ! Pour passer de la v3 à la v4, suivez le guide de migration.