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 étendue s’étendant sur de nombreuses générations et des centaines d’individus. ReactFlow fournit l’infrastructure de rendu et d’interaction. Les choix déterminants concernent les types de nœuds personnalisés, l’algorithme de disposition de 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 associés et les aspects pour lesquels 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 disposition des arbres de D3 sont matures, bien documentés, et sont utilisés depuis des années pour la visualisation généalogique. 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 stockés dans l’état. La bibliothèque gère le rendu (nœuds HTML, arêtes SVG), le déplacement et le zoom ainsi que 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 dépliables, boutons d’action — sans sortir du modèle de React.
Le compromis est que ReactFlow impose davantage de choix de conception que D3. Le modèle de données des nœuds et des arêtes a une structure spécifique, la disposition est gérée en externe (ReactFlow n’inclut pas d’algorithme de disposition), 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 de quelques centaines de nœuds, ces compromis sont favorables.
Types de nœuds personnalisés : fiches individuelles
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 sous forme de props et rend ce que vous voulez. La seule exigence est de rendre les composants Handle aux points de connexion où les arêtes se raccordent.
Le composant PersonNode affiche une fiche 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 fiche possède une bordure discrète qui change de couleur en fonction de la profondeur de génération — un indice visuel qui aide à orienter le lecteur dans un arbre profond. Les personnes en vie et décédées sont représentées par des styles visuels distincts.
Un choix de conception a nettement amélioré la lisibilité : le nœud utilise des points de connexion distincts pour chaque type d’arête : en haut et en bas pour les arêtes parent-enfant, à gauche et à droite pour les arêtes de conjoints. Les arêtes parent-enfant se connectent du bas vers le haut, de sorte que l’arbre se développe verticalement dans une direction cohérente. Les arêtes de conjoints relient les côtés des nœuds au même niveau de génération. Cette séparation des types d’arêtes par position des points de connexion rend le sens des relations immédiatement lisible.
Le composant de nœud gère également l’état « replié » pour les sous-arbres profonds. Quand un sous-arbre est replié, le nœud affiche un badge indiquant le nombre de descendants cachés. Cliquer sur le badge déplie le sous-arbre. Ce modèle d’interaction est essentiel pour rendre les grands arbres utilisables — une vue affichant les quatre cents nœuds d’un arbre est illisible, mais une vue où la majeure partie de l’arbre est repliée et où vous dépliez les branches à la demande est navigable.
Disposition avec Dagre pour le positionnement hiérarchique
ReactFlow ne calcule pas les positions des nœuds. Vous fournissez les positions dans les données des 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 disposition hiérarchique. Le choix standard est Dagre, une bibliothèque de disposition de graphes orientés acycliques qui produit des dispositions hiérarchiques lisibles avec un minimum de croisements.
L’intégration de Dagre se situe en dehors du cycle de rendu de ReactFlow. Quand les données des nœuds ou d’arêtes changent, une fonction de disposition s’exécute de manière synchrone, attribue 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, calcule la disposition, 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 de la disposition. « 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 doivent être ajustées aux dimensions des nœuds utilisés.
La limite 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 naturelles dans un DAG. La solution est d’exécuter l’algorithme de disposition de 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. La fonction simplifiée ci-dessus prend en compte toutes les arêtes qu’elle reçoit pour calculer la disposition ; filtrez donc la liste pour ne garder que les arêtes parent-enfant avant de l’appeler. Dagre seul ne garantit pas non plus que les conjoints se retrouvent côte à côte ou au même rang : un conjoint sans parents enregistrés ni enfants dans l’arbre visible n’a aucune arête parent-enfant. Il faut donc ajuster les positions des conjoints après le calcul de la disposition pour obtenir le résultat souhaité : 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 prend en charge 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 d’adoption 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 sous forme de props et rend un tracé 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 tracé (getStraightPath, getBezierPath, getSmoothStepPath) qui génèrent la chaîne décrivant le tracé SVG correcte pour les coordonnées des extrémités 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 est rendue sous forme d’un foreignObject dans le SVG, ce qui permet d’utiliser du HTML et CSS standard pour le style de l’infobulle.
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 peut ne rendre que les nœuds et les arêtes dans la zone d’affichage actuelle. Cette fonction est désactivée par défaut et s’active avec la prop onlyRenderVisibleElements ; une fois activée, elle résout l’essentiel des problèmes 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 même si les nœuds hors de la zone d’affichage sont exclus du rendu. Gardez les composants de nœuds purs et rapides.
La deuxième stratégie consiste à mémoïser les tableaux de nœuds et d’arêtes. Comme ReactFlow effectue un nouveau rendu chaque fois que les tableaux de nœuds ou d’arêtes changent par référence, transmettre de nouveaux tableaux à chaque rendu du composant parent entraîne un travail inutile. Envelopper le calcul de la disposition dans useMemo avec une dépendance aux données brutes — pas sur l’état dérivé — garantit que le calcul coûteux de la disposition et le nouveau rendu associé ne surviennent que lorsque les données sous-jacentes changent réellement.
La troisième stratégie est l’interaction replier/déplier décrite dans la section des types de nœuds. Un arbre de 400 nœuds qui en affiche seulement 40 à tout moment offre des performances similaires à celles d’un arbre de 40 nœuds. L’état replié est géré dans l’état React au niveau de la visualisation, pas dans le modèle de données sous-jacent. Replier un sous-arbre le retire des tableaux de nœuds et d’arêtes passés à ReactFlow ; le déplier l’y réintègre. L’algorithme de disposition Dagre est relancé avec l’ensemble filtré.
Conception des interactions
Au-delà des mécanismes de disposition et de rendu, le modèle d’interaction pour une visualisation d’arbre généalogique nécessite une réflexion approfondie. Le déplacement et le zoom sont fournis par ReactFlow directement, mais la vue initiale et le niveau de zoom jouent un rôle important dans la facilité d’utilisation. Appeler fitView() après le rendu de la disposition 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 la zone d’affichage, 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 une impression de continuité spatiale.
La recherche et le filtrage — trouver une personne spécifique par nom et accéder directement à sa position dans l’arbre — sont des besoins courants pour tout arbre généalogique non trivial. L’implémentation met en évidence le nœud correspondant avec une animation pulsée après avoir centré la zone d’affichage 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 recalcul complet de la disposition de l’arbre.
Ressources connexes
- Tables de fermeture pour calculer les liens de parenté — la couche de base de données qui permet de répondre en une seule requête à la question « quel est le lien de parenté entre ces deux personnes ? », complétant la couche de visualisation ReactFlow
- Drapeaux dans les widgets : pourquoi les codes pays ISO sont préférables aux noms dépendant de la langue — un autre projet où des identifiants stables pour les entités (codes de pays plutôt que noms) ont évité une catégorie de bugs subtils