ReactFlow per la visualizzazione degli alberi genealogici
Un albero genealogico è un grafo. I dati sono un insieme di nodi — le persone — collegati da archi che rappresentano diversi tipi di relazione: genitore-figlio, coniugi, fratelli. Il problema di visualizzazione consiste nel disporre questo grafo in modo comprensibile a scale diverse, da una vista su tre generazioni a una linea genealogica estesa su molte generazioni e centinaia di individui. ReactFlow fornisce l’infrastruttura per il rendering e l’interazione. Le scelte più interessanti riguardano i tipi di nodo personalizzati, l’algoritmo di disposizione Dagre, gli stili degli archi per i diversi tipi di relazione e le strategie di ottimizzazione delle prestazioni necessarie quando gli alberi crescono.
Questo articolo descrive le scelte di implementazione che abbiamo fatto, i relativi compromessi e i punti in cui le astrazioni di ReactFlow ci hanno aiutato o hanno limitato ciò che potevamo realizzare.
Perché ReactFlow
L’alternativa che abbiamo preso seriamente in considerazione era D3. Gli algoritmi di disposizione degli alberi di D3 sono maturi, ben documentati e utilizzati da anni per le visualizzazioni genealogiche. Il problema è che D3 richiede di gestire direttamente il DOM: se si vogliono usare componenti React per gli elementi interattivi, questo entra in conflitto con il modello di riconciliazione di React. Gli approcci ibridi funzionano, ma sono difficili da mantenere.
ReactFlow mantiene tutto all’interno del modello a componenti di React. Nodi e archi sono semplici oggetti JavaScript nello stato. La libreria gestisce il rendering (nodi HTML, archi SVG), lo spostamento della vista, lo zoom e gli eventi di interazione. I tipi di nodo personalizzati sono normali componenti React. Questo semplifica l’inserimento di contenuti interattivi articolati in ogni nodo — segnaposto per le foto, sezioni di dettaglio espandibili, pulsanti di azione — senza uscire dal modello di React.
Il compromesso è che ReactFlow impone più scelte progettuali rispetto a D3. Il modello dati di nodi e archi ha una struttura specifica, la disposizione viene gestita esternamente (ReactFlow non include un algoritmo di disposizione) e le prestazioni con un numero molto elevato di nodi richiedono una gestione più esplicita rispetto a un approccio basato interamente su D3. Per un progetto con qualche centinaio di nodi, questi compromessi sono vantaggiosi.
Tipi di nodo personalizzati: schede delle persone
Il sistema di nodi personalizzati è uno dei punti di forza di ReactFlow. Un tipo di nodo personalizzato è un componente React che riceve i dati del nodo come props e visualizza ciò che desideri. L’unico requisito è che visualizzi i componenti Handle nei punti di connessione a cui si collegano gli archi.
Il componente PersonNode visualizza una scheda con il nome della persona, l’anno di nascita, l’anno di morte (se pertinente) e un segnaposto per la foto che mostra le iniziali quando non è disponibile un’immagine. La scheda ha un bordo discreto che cambia colore in base alla profondità generazionale: un riferimento visivo che aiuta a orientarsi in un albero esteso su molte generazioni. Le persone viventi e quelle decedute hanno stili visivi distinti.
Una scelta progettuale ha migliorato sensibilmente la chiarezza: il nodo usa punti di connessione separati per ogni tipo di arco: in alto e in basso per gli archi genitore-figlio, a sinistra e a destra per gli archi tra coniugi. Gli archi genitore-figlio collegano il lato inferiore a quello superiore, così l’albero si sviluppa verticalmente in una direzione coerente. Gli archi tra coniugi collegano i lati dei nodi sullo stesso livello generazionale. Separare i tipi di arco in base alla posizione dei punti di connessione rende subito leggibile la direzione delle relazioni.
Il componente del nodo gestisce anche lo stato «compresso» dei sottoalberi estesi su molte generazioni. Quando un sottoalbero è compresso, il nodo mostra un indicatore con il numero dei discendenti nascosti. Facendo clic sull’indicatore si espande il sottoalbero. Questo modello di interazione è essenziale per rendere utilizzabili gli alberi grandi: una vista che mostra contemporaneamente quattrocento nodi è illeggibile, mentre una vista in cui la maggior parte dell’albero è compressa e i rami si espandono su richiesta è navigabile.
Disposizione gerarchica con Dagre
ReactFlow non calcola le posizioni dei nodi. Le fornisci come parte dei dati del nodo e la libreria li visualizza dove indichi. Per un albero genealogico, calcolare manualmente queste posizioni richiederebbe di reimplementare un algoritmo di disposizione gerarchica. La scelta standard è Dagre, una libreria per la disposizione di grafi aciclici diretti che produce strutture gerarchiche ordinate con il minimo di incroci.
L’integrazione con Dagre è esterna al ciclo di rendering di ReactFlow. Quando i dati dei nodi o degli archi cambiano, una funzione di disposizione viene eseguita in modo sincrono, assegna una posizione a ogni nodo e restituisce l’array dei nodi posizionati che ReactFlow visualizza. La funzione prende i nodi e gli archi grezzi, costruisce un grafo Dagre, ne calcola la disposizione e legge i valori x e y risultanti.
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 } };
});
}
Il parametro rankdir di Dagre controlla la direzione della disposizione. «TB» (dall’alto verso il basso) è la scelta naturale per un albero genealogico in cui il tempo scorre verso il basso attraverso le generazioni. ranksep controlla la distanza verticale tra le generazioni; nodesep controlla la distanza orizzontale tra i fratelli. Questi valori vanno calibrati sulle dimensioni specifiche dei nodi utilizzati.
Il limite di Dagre per gli alberi genealogici è che tratta il grafo come un DAG. Gli archi tra coniugi — che collegano due nodi dello stesso livello e non sono orientati — non sono archi naturali per un DAG. La soluzione consiste nell’eseguire la disposizione Dagre usando solo gli archi genitore-figlio per stabilire i livelli generazionali e le posizioni orizzontali, quindi disegnare separatamente gli archi tra coniugi come linee rette alla stessa coordinata y. La funzione semplificata riportata sopra calcola la disposizione di tutti gli archi che riceve, quindi filtra l’elenco per includere solo gli archi genitore-figlio prima di chiamarla. Inoltre, Dagre da solo non garantisce che i coniugi si trovino affiancati o sullo stesso livello: un coniuge senza genitori registrati né figli nell’albero visibile non ha alcun arco genitore-figlio. Occorre quindi un passaggio di aggiustamento delle posizioni dei coniugi dopo il calcolo della disposizione per ottenere il risultato previsto: i coniugi appaiono affiancati sullo stesso livello, con un arco orizzontale tra loro.
Stili degli archi per i diversi tipi di relazione
ReactFlow supporta componenti personalizzati per gli archi come per i nodi. Ogni tipo di relazione — genitore-figlio, coniugale, adottiva, acquisita — usa uno stile distinto. Gli archi genitore-figlio sono linee continue. Gli archi tra coniugi sono sottili collegamenti orizzontali. Le relazioni adottive usano una linea tratteggiata. Le relazioni di parentela acquisita usano una linea puntinata.
Il componente dell’arco riceve le posizioni dei nodi di origine e destinazione come props e disegna un tracciato SVG. Per gli archi rettilinei è semplice. Per gli archi curvi tra nodi in posizioni diverse, ReactFlow fornisce funzioni di supporto (getStraightPath, getBezierPath, getSmoothStepPath) che generano la stringa corretta del tracciato SVG per le coordinate degli estremi indicate.
Una delle interazioni rese possibili dai componenti personalizzati degli archi è un’etichetta che appare al passaggio del puntatore e mostra la descrizione completa della relazione. È utile per strutture familiari complesse in cui il solo stile visivo può risultare ambiguo: passando il puntatore su un arco tra due nodi appare «Genitore adottivo — 1987» oppure «Sposati nel 1975, divorziati nel 1989». L’etichetta viene visualizzata come foreignObject nell’SVG, consentendo di usare HTML e CSS standard per lo stile del suggerimento.
Prestazioni con alberi grandi
ReactFlow gestisce alberi di dimensioni moderate — fino a qualche centinaio di nodi — senza ottimizzazioni specifiche. Oltre questa soglia diventano necessarie alcune strategie.
ReactFlow può visualizzare solo i nodi e gli archi all’interno dell’area visibile corrente. Questa funzione è disattivata per impostazione predefinita e si attiva con la prop onlyRenderVisibleElements; una volta attivata, risolve gran parte dei problemi di prestazioni degli alberi grandi, purché il rendering dei componenti dei nodi non sia a sua volta costoso. Un PersonNode che avvia richieste di dati esterni o calcoli pesanti a ogni rendering causa scatti indipendentemente dall’esclusione dei nodi fuori dall’area visibile. Mantieni i componenti dei nodi puri e veloci.
La seconda strategia è la memoizzazione degli array di nodi e archi. Poiché ReactFlow esegue un nuovo rendering ogni volta che cambia il riferimento a uno di questi array, passare array appena costruiti a ogni rendering del componente genitore causa lavoro superfluo. Racchiudere il calcolo della disposizione in useMemo, con una dipendenza dai dati grezzi anziché dallo stato derivato, assicura che il calcolo costoso e il rendering associato avvengano solo quando i dati sottostanti cambiano davvero.
La terza strategia è l’interazione di compressione ed espansione descritta nella sezione sui tipi di nodo. Un albero con 400 nodi che ne mostra solo 40 alla volta ha prestazioni simili a quelle di un albero di 40 nodi. Lo stato di compressione è gestito nello stato React a livello della visualizzazione, non nel modello dati sottostante. Comprimere un sottoalbero lo esclude dagli array di nodi e archi passati a ReactFlow; espanderlo lo reinserisce. La disposizione Dagre viene ricalcolata sull’insieme filtrato.
Considerazioni sulla progettazione delle interazioni
Oltre ai meccanismi di disposizione e rendering, il modello di interazione di una visualizzazione genealogica richiede attenzione. ReactFlow offre già lo spostamento della vista e lo zoom, ma la vista iniziale e il livello di zoom incidono molto sull’usabilità. Chiamare fitView() dopo il rendering della disposizione assicura che l’albero sia visibile e correttamente dimensionato al caricamento.
La navigazione tra individui — fare clic su una persona per centrarla nell’area visibile, seguire gli archi verso le generazioni adiacenti — richiede i metodi dell’istanza ReactFlow setCenter() e fitBounds(). Sono accessibili tramite l’hook useReactFlow() e vanno chiamati con l’animazione fluida attiva per dare all’utente un senso di continuità spaziale.
Ricerca e filtri — trovare una persona specifica per nome e raggiungerne la posizione nell’albero — sono requisiti comuni per qualsiasi albero genealogico di una certa complessità. La nostra implementazione evidenzia il nodo trovato con un effetto pulsante dopo averlo centrato nell’area visibile. Lo fa aggiungendo per un breve periodo una classe di animazione CSS al nodo corrispondente, per poi rimuoverla. Il metodo updateNode di ReactFlow gestisce questa operazione senza ricalcolare la disposizione dell’intero albero.
Approfondimenti correlati
- Tabelle di chiusura per il calcolo delle relazioni di parentela — il livello database che permette di rispondere a «che relazione di parentela c’è tra queste due persone?» con una sola query, completando il livello di visualizzazione ReactFlow
- Bandiere nei widget: perché i codici Paese ISO sono preferibili ai nomi dipendenti dalle impostazioni locali — un altro progetto in cui identificativi stabili per le entità (codici Paese anziché nomi) hanno evitato una categoria di bug difficili da individuare