Aller au contenu principal

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 :

Chargement…

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.

Chargement…
{
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).

Chargement…
{
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' },
],
}
Chargement…

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' },
],
}
Chargement…

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' },
],
}
Chargement…

É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, circuit dé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 : le a / b d'une résistance, le + / - d'une pile, le base / collector / emitter d'un transistor. Une connection vise 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, les connections sont 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 (fractions 0..1), comme un graph écrit à la main ; on utilise des points de junction pour les coins.
Chargement…
{
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).