ReactFlow pour la visualisation d'arbres généalogiques
Un arbre généalogique est un graphe. Les données sont un ensemble de nœuds — des personnes — reliés par des arêtes typées : parent-enfant, conjoint, fratrie. Le problème de visualisation est de disposer ce graphe de manière intelligible à différentes échelles, d'une vue sur trois générations à une lignée profonde s'étendant sur de nombreuses générations et des centaines d'individus. ReactFlow fournit l'infrastructure de rendu et d'interaction. Les décisions intéressantes se trouvent dans les types de nœuds personnalisés, l'algorithme de layout Dagre, le style des arêtes pour les types de relations, et les stratégies de performance qui deviennent nécessaires à mesure que les arbres grandissent.
Cet article couvre les décisions d'implémentation que nous avons prises, les compromis impliqués, et les endroits où les abstractions de ReactFlow ont à la fois aidé et contraint ce que nous pouvions construire.
Pourquoi ReactFlow
L'alternative que nous avons sérieusement considérée était D3. Les algorithmes de layout d'arbre de D3 sont matures, bien documentés, et ont été utilisés pour la visualisation généalogique depuis des années. Le problème est que D3 nécessite de gérer le DOM directement, ce qui entre en conflit avec le modèle de réconciliation de React si vous voulez des composants React pour les éléments interactifs. Les approches hybrides fonctionnent mais sont difficiles à maintenir.
ReactFlow garde tout dans le modèle de composants de React. Les nœuds et les arêtes sont de simples objets JavaScript dans l'état. La bibliothèque gère le rendu SVG, le panoramique et le zoom, et les événements d'interaction. Les types de nœuds personnalisés ne sont que des composants React. Cela permet d'intégrer facilement du contenu interactif riche dans chaque nœud — espaces réservés pour photos, sections de détails expansibles, boutons d'action — sans sortir du modèle de React.
Le compromis est que ReactFlow est plus opiniâtre que D3. Le modèle de données des nœuds et arêtes a une forme spécifique, le layout est géré en externe (ReactFlow n'inclut pas d'algorithme de layout), et les performances avec un très grand nombre de nœuds nécessitent une gestion plus explicite qu'une approche D3 pure. Pour un projet dans la plage de quelques centaines de nœuds, ces compromis sont favorables.
Types de nœuds personnalisés : cartes de personnes
Le système de nœuds personnalisés de ReactFlow est l'une de ses fonctionnalités les plus solides. Un type de nœud personnalisé est un composant React qui reçoit les données du nœud en props et rend ce que vous voulez. La seule exigence est de rendre les composants Handle aux points de connexion où les arêtes s'attachent.
Le composant PersonNode rend une carte avec le nom de la personne, l'année de naissance, l'année de décès (le cas échéant), et un espace réservé pour photo qui affiche les initiales de la personne quand aucune photo n'est disponible. La carte a une bordure subtile qui change de couleur en fonction de la profondeur de génération — un indice visuel qui aide à orienter le spectateur dans un arbre profond. Les personnes vivantes et décédées utilisent des traitements visuels distincts.
Une décision de conception qui a significativement amélioré la clarté : le nœud a deux positions de handle distinctes en haut et en bas, et non les quatre côtés par défaut. Les arêtes parent-enfant se connectent du bas vers le haut, de sorte que l'arbre s'écoule verticalement dans une direction cohérente. Les arêtes de conjoints se connectent côte à côte au même niveau de génération. Cette séparation des types d'arêtes par position de handle rend la directionnalité des relations immédiatement lisible.
Le composant de nœud gère également l'état “réduit” pour les sous-arbres profonds. Quand un sous-arbre est réduit, le nœud affiche un badge indiquant le nombre de descendants cachés. Cliquer sur le badge développe le sous-arbre. Ce modèle d'interaction est essentiel pour rendre les grands arbres utilisables — une vue plate de quatre cents nœuds est illisible, mais une vue où la plupart de l'arbre est réduit et vous développez les branches à la demande est navigable.
Layout Dagre pour le positionnement hiérarchique
ReactFlow ne calcule pas les positions des nœuds. Vous fournissez les positions dans le cadre des données de nœuds, et la bibliothèque les rend à l'endroit que vous indiquez. Pour un arbre généalogique, calculer ces positions manuellement nécessiterait de réimplémenter un algorithme de layout hiérarchique. Le choix standard est Dagre, une bibliothèque de layout de graphe acyclique dirigé qui produit des layouts hiérarchiques propres avec un minimum de croisements.
L'intégration de Dagre se situe en dehors du cycle de rendu de ReactFlow. Quand les données de nœuds ou d'arêtes changent, une fonction de layout s'exécute de manière synchrone, assigne des positions à chaque nœud, et retourne le tableau de nœuds positionnés que ReactFlow rend. La fonction prend les nœuds et arêtes bruts, construit un graphe Dagre, exécute le layout, et lit les valeurs x et y calculées.
import dagre from '@dagrejs/dagre';
function layoutTree(nodes, edges, nodeWidth, nodeHeight) {
const g = new dagre.graphlib.Graph();
g.setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: 'TB', ranksep: 80, nodesep: 40 });
nodes.forEach(node => {
g.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach(edge => {
g.setEdge(edge.source, edge.target);
});
dagre.layout(g);
return nodes.map(node => {
const { x, y } = g.node(node.id);
return { ...node, position: { x: x - nodeWidth / 2, y: y - nodeHeight / 2 } };
});
}
Le paramètre rankdir de Dagre contrôle la direction du layout. “TB” (haut-bas) est le choix naturel pour un arbre généalogique où le temps s'écoule vers le bas à travers les générations. ranksep contrôle l'espacement vertical entre les générations ; nodesep contrôle l'espacement horizontal entre les frères et sœurs. Ces valeurs nécessitent un ajustement selon les tailles de nœuds que vous utilisez.
La limitation de Dagre pour les arbres généalogiques est qu'il traite le graphe comme un DAG. Les arêtes de conjoints — qui relient deux nœuds au même rang et sont non orientées — ne sont pas des arêtes DAG naturelles. La solution est d'exécuter le layout Dagre en utilisant uniquement les arêtes parent-enfant pour établir les rangs de génération et les positions horizontales, puis de tracer séparément les arêtes de conjoints comme des lignes droites à la même coordonnée y. Cela produit le résultat visuel correct : les conjoints apparaissent côte à côte au même niveau avec une arête horizontale entre eux.
Style des arêtes pour les types de relations
ReactFlow supporte les composants d'arêtes personnalisés de la même façon que les composants de nœuds personnalisés. Chaque type de relation — parent-enfant, conjoint, adoptif, beau-parent — utilise un style d'arête distinct. Les arêtes parent-enfant sont des lignes continues. Les arêtes de conjoints sont de fins connecteurs horizontaux. Les relations adoptives utilisent une ligne en tirets. Les relations de beau-parent utilisent une ligne en pointillés.
Le composant d'arête reçoit les positions des nœuds source et cible en props et rend un chemin SVG. Pour les arêtes droites c'est trivial. Pour les arêtes courbes entre des nœuds à différentes positions, ReactFlow fournit un ensemble de fonctions utilitaires de chemin (getStraightPath, getBezierPath, getSmoothStepPath) qui génèrent la chaîne de chemin SVG correcte pour les coordonnées de point de terminaison données.
Une interaction que les composants d'arêtes personnalisés permettent est une étiquette d'arête qui apparaît au survol, montrant la description de la relation en entier. C'est utile pour les structures familiales complexes où le style visuel seul peut être ambigu — survoler une arête entre deux nœuds affiche “Parent adoptif — 1987” ou “Marié en 1975, divorcé en 1989.” L'étiquette se rend comme un foreignObject dans le SVG, ce qui permet d'utiliser du HTML et CSS standard pour le style du tooltip.
Performances avec les grands arbres
ReactFlow gère les arbres de taille modérée — jusqu'à quelques centaines de nœuds — sans optimisation spécifique. Au-delà, quelques stratégies deviennent nécessaires.
ReactFlow inclut un renderer de virtualisation intégré qui ne rend que les nœuds dans le viewport actuel. C'est activé par défaut et gère l'essentiel de la préoccupation de performance pour les grands arbres, à condition que les composants de nœuds eux-mêmes ne soient pas coûteux à rendre. Un PersonNode qui déclenche des chargements de données externes ou des calculs lourds à chaque rendu causera des saccades indépendamment du culling du viewport. Gardez les composants de nœuds purs et rapides.
La deuxième stratégie est de mémoïser les tableaux de nœuds et d'arêtes. Comme ReactFlow re-rend chaque fois que les tableaux de nœuds ou d'arêtes changent par référence, passer des tableaux nouvellement construits à chaque rendu parent cause un travail inutile. Envelopper le calcul du layout dans useMemo avec une dépendance sur les données brutes — pas sur l'état dérivé — garantit que le calcul de layout coûteux et le re-rendu associé ne surviennent que lorsque les données sous-jacentes changent réellement.
La troisième stratégie est l'interaction réduire/développer décrite dans la section des types de nœuds. Un arbre de 400 nœuds qui en affiche seulement 40 à tout moment performe identiquement à un arbre de 40 nœuds. L'état réduit est géré dans l'état React au niveau de la visualisation, pas dans le modèle de données sous-jacent. Réduire un sous-arbre le filtre des tableaux de nœuds et d'arêtes passés à ReactFlow ; le développer le rajoute. Le layout Dagre se ré-exécute avec l'ensemble filtré.
Considérations de conception d'interaction
Au-delà des mécaniques de layout et de rendu, le modèle d'interaction pour une visualisation d'arbre généalogique nécessite une réflexion approfondie. Le panoramique et le zoom sont fournis par ReactFlow directement, mais la vue initiale et le niveau de zoom importent significativement pour l'utilisabilité. Appeler fitView() après le rendu du layout garantit que l'arbre est visible et mis à l'échelle de manière appropriée au chargement.
La navigation entre individus — cliquer sur une personne pour la centrer dans le viewport, suivre les arêtes vers les générations adjacentes — nécessite les méthodes d'instance ReactFlow setCenter() et fitBounds(). Celles-ci sont accessibles via le hook useReactFlow() et doivent être appelées avec une animation fluide activée pour donner à l'utilisateur un sens de continuité spatiale.
La recherche et le filtrage — trouver une personne spécifique par nom et sauter à sa position dans l'arbre — est une exigence courante pour tout arbre généalogique non trivial. L'implémentation met en évidence le nœud correspondant avec une animation après avoir centré le viewport dessus. Cela se fait en ajoutant une classe CSS d'animation au nœud correspondant pour une courte durée, puis en la retirant. La méthode updateNode de ReactFlow gère cela sans déclencher un re-layout complet de l'arbre.
Ressources connexes
- Closure Tables for Relationship Calculation — la couche base de données qui rend “comment ces deux personnes sont-elles liées ?” répondable en une seule requête, complétant la couche de visualisation ReactFlow
- Widget Flags: Why ISO Country Codes Beat Locale-Dependent Names — un autre projet où des identifiants stables pour les entités (codes de pays vs noms) ont prévenu une classe de bugs subtils