Disposition
La disposition des nœuds repose sur quelques concepts : direction, lane,
align_with, et les modes circular / tree. Dans tous ceux-ci, vous ne
fournissez aucune coordonnée — le moteur place chaque nœud en ratios
relatifs au conteneur, ce qui rend l'animation responsive sans calcul manuel.
Même le mode graph, pour les diagrammes de nœuds quelconques,
place les nœuds automatiquement (en minimisant les croisements d'arêtes) — vous
n'ajoutez x / y que pour épingler un nœud précis.
direction
Huit valeurs possibles pour direction (au niveau racine de la spec). Quatre
disposent des nœuds libres le long d'un flux, circular les
place sur un cercle, tree dispose un arbre binaire,
graph place automatiquement un graphe quelconque, et
circuit trace un schéma électrique :
- left-to-right
- right-to-left
- top-to-bottom
- bottom-to-top
- circular
Les quatre directions linéaires partagent exactement la même spec : seule la
valeur de direction change. Voici le code des aperçus ci-dessus (le mode
circular, structurellement différent, est détaillé plus bas) :
{
direction: 'left-to-right', // ou 'right-to-left' | 'top-to-bottom' | 'bottom-to-top'
nodes: [
{ id: 'a', type: 'server', text: 'A', lane: 1 },
{ id: 'b', type: 'server', text: 'B', lane: 2 },
{ id: 'c', type: 'server', text: 'C', lane: 3 },
],
connections: [
{ from: 'a', to: 'b', style: 'animated' },
{ from: 'b', to: 'c', style: 'animated' },
],
}
Grilles et lanes
En mode linéaire (les quatre premières directions), la propriété lane
contrôle la position du nœud le long du flux. Les nœuds qui partagent
la même lane sont empilés sur l'axe transverse.
{
direction: 'left-to-right',
nodes: [
{ id: 'web1', type: 'server', lane: 2 },
{ id: 'web2', type: 'server', lane: 2 }, // même colonne que web1
{ id: 'db', type: 'database', lane: 3 },
],
}
Les deux axes partagent un même pas d'espacement, mesuré en pixels, et la grille est centrée sur la scène. Deux voisins sont donc à la même distance l'un de l'autre qu'ils soient côte à côte ou l'un au-dessus de l'autre — et cette distance ne change pas avec le nombre de nœuds. Ajoutez une lane et le schéma grandit dans la scène, au lieu de se redistribuer sur toute sa largeur.
Alignement transverse (align_with)
align_with permet d'aligner un nœud sur l'axe transverse d'un autre,
identifié par son id. Pratique pour aligner deux nœuds de lanes
différentes :
{
nodes: [
{ id: 'client', type: 'laptop', lane: 1 },
{ id: 'api', type: 'server', lane: 2 },
{ id: 'cache', type: 'database', lane: 3, align_with: 'api' },
],
}
Ignoré en mode circular.
Convergence des liens (merge_edges)
Quand plusieurs liens s'attachent à la même face d'un nœud, ils se
rejoignent par défaut en un point d'ancrage unique : un flux plusieurs-vers-un
(un flood, un répartiteur de charge, un hub) converge visuellement au lieu de
s'écarter. Mettez merge_edges: false sur un nœud pour écarter ses liens —
chacun reçoit alors son propre point d'attache le long de la face, ordonné pour
réduire les croisements. Pratique quand un nœud a plusieurs voisins distincts du
même côté et que vous voulez distinguer les liens.
- merge_edges: true (défaut)
- merge_edges: false
{
direction: 'left-to-right',
nodes: [
{ id: 'c1', type: 'laptop', text: 'Client', lane: 1 },
{ id: 'c2', type: 'laptop', text: 'Client', lane: 1 },
{ id: 'c3', type: 'laptop', text: 'Client', lane: 1 },
// merge_edges: false → les quatre liens s'écartent sur la face du serveur.
{ id: 'srv', type: 'server', text: 'Serveur', lane: 2, merge_edges: false },
],
connections: [
{ from: 'c1', to: 'srv', style: 'animated' },
{ from: 'c2', to: 'srv', style: 'animated' },
{ from: 'c3', to: 'srv', style: 'animated' },
],
}
La décision est par face de nœud : dans c → srv, les liens convergent sur
srv seulement si srv fusionne, et quittent c écartés seulement si c
s'écarte. Plusieurs liens entre les deux mêmes nœuds gardent leurs voies
parallèles dans tous les cas, pour que les flux requête/réponse bidirectionnels
restent lisibles.
Points de connexion des nœuds ronds (ports)
Un circle se connecte sur son contour, pas sur quatre côtés cardinaux :
chaque arête touche le cercle exactement là où elle pointe, en visant le centre.
C'est ce qui rend les graphes et les arbres binaires naturels — les arêtes
parent→enfant et rayon→hub abordent le disque de face au lieu de laisser un vide
dans les coins. C'est le comportement par défaut de tout circle ; aucun
champ n'est requis.
Donnez à ports un entier positif pour exposer exactement ce nombre de points
d'attache, répartis régulièrement sur le contour. Chaque arête se colle alors au
plus proche, et les arêtes qui visent le même créneau y fusionnent — un rendu
discret, par ex. un nœud de routage à nombre fixe de terminaux. ports: 'direct'
rétablit le défaut (une infinité de points, c.-à-d. le chemin le plus direct).
- ports : 'direct' (défaut)
- ports : 4
{
direction: 'circular',
nodes: [
// ports: 4 → les huit rayons se collent aux quatre points réguliers du hub
// (N/E/S/O) ; avec 'direct', chaque rayon toucherait le hub sur son rayon.
{ id: 'hub', type: 'circle', body: 'H', main: true, ports: 4 },
{ id: 'n1', type: 'circle', body: '1' },
// …n2 … n8
],
connections: [
{ from: 'n1', to: 'hub', path: 'straight', arrow_head: 'none' },
// …un par rayon
],
}
ports n'agit que sur les nœuds ronds ; les autres types gardent l'ancrage sur
les côtés cardinaux (et leur comportement merge_edges). Les ports d'un nœud
rond respectent quand même l'écartement bidirectionnel : une paire
requête/réponse entre deux cercles reste sur deux points distincts.
Mode circulaire
En circular, le nœud marqué main: true est placé au centre ; les autres
sont répartis à équidistance sur un cercle. Les lane et align_with sont
ignorés — c'est le seul nœud main qui structure la disposition.
{
direction: 'circular',
nodes: [
{ id: 'gateway', type: 'server', text: 'Gateway', icon: 'nginx', main: true },
{ id: 'auth', type: 'server', text: 'Auth', icon: 'dotnet' },
{ id: 'orders', type: 'server', text: 'Orders', icon: 'node' },
{ id: 'billing', type: 'server', text: 'Billing', icon: 'java' },
{ id: 'search', type: 'server', text: 'Search', icon: 'python' },
],
connections: [
{ from: 'auth', to: 'gateway', style: 'animated' },
{ from: 'orders', to: 'gateway', style: 'animated' },
{ from: 'billing', to: 'gateway', style: 'animated' },
{ from: 'search', to: 'gateway', style: 'animated' },
],
}
Mode arbre
En tree, la spec porte un bloc tree décrivant un arbre binaire : un
root et, pour chaque nœud, son enfant left / right. Le moteur place chaque
nœud par son rang in-order (horizontal) et sa profondeur (vertical), et
dessine automatiquement les arêtes parent→enfant — vous ne fournissez aucune
connections. lane et align_with sont ignorés ; le bloc tree est la source
unique de vérité pour les positions comme pour les arêtes.
{
direction: 'tree',
tree: {
root: '8',
children: {
'8': { left: '3', right: '13' },
'3': { left: '1', right: '6' },
'13': { right: '17' },
},
},
nodes: [
{ id: '8', type: 'circle', body: '8' },
{ id: '3', type: 'circle', body: '3' },
{ id: '13', type: 'circle', body: '13' },
{ id: '1', type: 'circle', body: '1' },
{ id: '6', type: 'circle', body: '6' },
{ id: '17', type: 'circle', body: '17' },
],
}
Comme la structure tient en un seul endroit, elle peut être restructurée à
l'exécution : l'action rotate_subtree
effectue une rotation d'arbre autour d'un pivot, et le moteur recalcule la
disposition et re-câble les arêtes depuis le même modèle — les nœuds glissent à
leur nouvelle profondeur. C'est ce qui anime les démos AVL et rouge-noir.
Styliser les arêtes
Les arêtes tracées automatiquement sont stylables depuis le même bloc tree —
inutile de les recréer en connections. Définissez un défaut pour toutes les
arêtes avec tree.edge_style, et surchargez-le arête par arête avec
tree.edges, indexé par l'id du nœud enfant (chaque nœud n'a qu'une arête
parente, donc l'enfant la désigne — et le style suit le nœud à travers un
rotate_subtree). L'entrée par arête fusionne par-dessus le défaut, champ par
champ. Par défaut, une arête d'arbre utilise un chemin straight sans tête de
flèche (contrairement au bezier par défaut des connexions ordinaires).
{
direction: 'tree',
tree: {
root: '8',
children: {
'8': { left: '3', right: '13' },
'3': { left: '1', right: '6' },
},
// Défaut pour chaque arête parent→enfant :
edge_style: { path: 'step' },
// Surcharge d'une arête (indexée par l'id de l'enfant) :
edges: {
'6': { style: 'dashed', color: 'crimson', text: 'x' },
},
},
nodes: [
/* … */
],
}
Les champs disponibles reprennent ceux d'une connexion : style,
path, color, arrow_head, text et highlighted.
Mode graphe
graph est le mode pour un graphe quelconque — un diagramme nœuds-liens
sans ordre naturel de flux, de cercle ou d'arbre (un réseau routier, ou un
graphe pondéré pour une démonstration de Dijkstra / A* / arbre couvrant
minimal). Vous ne décrivez que les arêtes ; les nœuds sont placés
automatiquement. Le moteur exécute une disposition dirigée par les forces,
déterministe, qui minimise les croisements d'arêtes (il essaie plusieurs
placements graines, garde le plus clair, puis effectue une courte recherche
locale pour supprimer les croisements restants) : la même spec rend toujours le
même graphe et le redimensionnement du lecteur ne le réagence jamais. lane,
align_with et main sont ignorés.
Les arêtes sont de simples connections : rendez-les non
orientées avec arrow_head: 'none', pondérez-les avec text, et recolorez-les
à l'exécution avec set_color pour
allumer un chemin ou un arbre couvrant.
{
direction: 'graph',
// Aucune coordonnée : les quatre nœuds sont auto-placés pour éviter les croisements.
nodes: [
{ id: 'A', type: 'circle', body: 'A' },
{ id: 'B', type: 'circle', body: 'B' },
{ id: 'C', type: 'circle', body: 'C' },
{ id: 'D', type: 'circle', body: 'D' },
],
connections: [
{ from: 'A', to: 'B', text: '7', path: 'straight', arrow_head: 'none' },
{ from: 'A', to: 'C', text: '2', path: 'straight', arrow_head: 'none' },
{ from: 'B', to: 'C', text: '3', path: 'straight', arrow_head: 'none' },
{ from: 'B', to: 'D', text: '4', path: 'straight', arrow_head: 'none' },
{ from: 'C', to: 'D', text: '6', path: 'straight', arrow_head: 'none' },
],
}
Épingler un nœud
Ajoutez x et y (chacun une fraction de la scène, 0..1) pour épingler
un nœud comme ancre fixe ; le placement automatique de tous les autres nœuds
contourne alors cette ancre. Pratique pour fixer une source à gauche et une
cible à droite, ou pour verrouiller l'orientation générale tout en laissant le
reste automatique :
nodes: [
{ id: 'A', type: 'circle', body: 'A', x: 0.1, y: 0.5 }, // épinglé (source)
{ id: 'B', type: 'circle', body: 'B' }, // auto-placé
{ id: 'C', type: 'circle', body: 'C' }, // auto-placé
{ id: 'D', type: 'circle', body: 'D', x: 0.9, y: 0.5 }, // épinglé (cible)
];
La démo complète Plus court chemin — Dijkstra de la galerie s'appuie là-dessus : elle règle les nœuds un à un et recolore les arêtes du plus court chemin au fil du parcours du graphe.
Mode circuit (schémas électriques)
circuit est le mode pour un schéma électrique. Il diffère d'un diagramme de
flux sur trois points, et le moteur s'adapte sur les trois :
- Des composants à bornes nommées. En plus des pictogrammes et des formes,
circuitdébloque une famille de symboles de schéma —resistor,capacitor,inductor,battery,dc_source/ac_source,diode,led,transistor_npn,switch,lamp,ground,junction,ammeter… (liste complète dans nodes). Chacun expose des bornes fixes que l'on câble par leur nom : lea/bd'une résistance, le+/-d'une pile, lebase/collector/emitterd'un transistor. Uneconnectionvise une borne avec la syntaxe"node:pin"("R1:a","battery:+"). Les bornes tournent avec le composant : une résistance verticale (rotation: 90) a ses bornes en haut et en bas. Un tel composant câblé en haut et en bas affiche son libellé sur le côté (à gauche ou à droite, vers l'extérieur) plutôt qu'en dessous, pour que le texte ne se pose jamais sur le fil vertical sortant. - Des fils, pas des flèches. En
circuit, lesconnectionssont par défaut un fil orthogonal (path: 'step') sans tête de flèche — inutile de le répéter sur chaque lien. - Placement (aucune coordonnée pour les cas courants). Sans coordonnées,
le moteur dispose automatiquement deux formes : une boucle simple (chaque
nœud a deux fils) devient un rectangle (les composants sur une arête non
supérieure sont auto-tournés pour garder les fils droits) ; un réseau
feed-forward connecté (un schéma logique — entrées → portes → sorties) est
disposé de gauche à droite en couches. Pour tout le reste (un réseau
série-parallèle, une galerie d'exemples déconnectés), on place les composants
soi-même avec
x/y(fractions0..1), comme ungraphécrit à la main ; on utilise des points dejunctionpour les coins.
{
direction: 'circuit',
// Aucune coordonnée : les quatre composants forment une boucle → rectangle auto.
nodes: [
{ id: 'batt', type: 'battery', value: 9, unit: 'V' },
{ id: 'sw', type: 'switch', closed: true },
{ id: 'R1', type: 'resistor', value: 220, unit: 'Ω' },
{ id: 'led', type: 'led', text: 'LED' },
],
connections: [
// Fils entre bornes nommées — orthogonaux, sans tête, par défaut.
{ from: 'batt:+', to: 'sw:a' },
{ from: 'sw:b', to: 'R1:a' },
{ from: 'R1:b', to: 'led:a' },
{ from: 'led:b', to: 'batt:-' },
],
timeline: [
// Les charges parcourent toute la boucle (courant conventionnel, + → −) :
{
type: 'flow',
route: ['batt:+', 'sw:a', 'sw:b', 'R1:a', 'R1:b',
'led:a', 'led:b', 'batt:-'],
color: '#f59e0b',
duration: 6000, // ms par tour — assez lent pour être lisible
keep_until_end: true,
},
],
}
Deux actions sont dédiées aux circuits : flow
anime le courant le long d'un parcours, et
toggle ouvre ou ferme un switch. Les
libellés des composants utilisent value + unit (9 V, 220 Ω), et allumer
une lampe/LED n'est qu'un set_color ou un
highlight. La démo complète Circuit
électrique de la galerie ferme l'interrupteur, puis met la boucle sous tension.
Fils en diagonale (45°)
Par défaut les fils sont strictement horizontaux/verticaux. Mettez
diagonal_wires: true au niveau de la spec pour les tracer de façon
octilinéaire : chaque coin est biseauté en un segment à 45° exactement
(uniquement 45 / 135 / 225 / 315°). Le biseau est maximisé, si bien qu'un long
L ou un escalier s'effondre en une diagonale nette, tandis qu'un fil entre
bornes alignées reste droit ; un court segment droit est conservé à chaque borne
pour que le fil quitte le composant perpendiculairement, et tout biseau qui
traverserait un corps retombe sur l'angle droit. Surchargez fil par fil avec
diagonal — pratique pour forcer un bus en orthogonal dans un schéma sinon
diagonal (ou l'inverse).
{
direction: 'circuit',
diagonal_wires: true, // tout le schéma passe en 45° là où c'est utile
connections: [
{ from: 'g1:y', to: 'g2:a' }, // rebouclage en 45° (hérite de diagonal_wires)
{ from: 'g1:y', to: 'Q', diagonal: false }, // celui-ci forcé orthogonal
],
}
La démo verrou SR de la galerie utilise diagonal_wires : ses deux fils de
rebouclage croisés se lisent en diagonales nettes tandis que les bus
d'entrée/sortie restent droits.
Mise à l'échelle responsive
Le composant calcule une « cellule » (plus petite distance entre deux nœuds, en px) qui pilote :
- un facteur d'échelle global (
--rdfa-scale) : icônes et polices plus grandes en plein écran, plus petites si l'espace est serré ; - la largeur maximale des panneaux et paquets (
--rdfa-maxw) pour qu'ils ne débordent jamais sur les voisins ; - la position de chaque nœud, bornée selon sa taille mesurée pour éviter les débordements du canevas.
La prop density ('compact' | 'comfortable' | 'spacious') module ces
réglages pour adapter le visuel à un contexte dense (sidebar) ou aéré
(plein écran).