tva
← Insights

ReactFlow para Visualização de Árvore Genealógica

Uma árvore genealógica é um grafo. Os dados são um conjunto de nós — pessoas — conectados por arestas tipadas: pai-filho, cônjuge, irmão. O problema de visualização é como dispor esse grafo de forma inteligível em diferentes escalas, de uma visão de três gerações a uma linhagem profunda abrangendo muitas gerações e centenas de indivíduos. O ReactFlow fornece a infraestrutura de renderização e interação. As decisões interessantes estão nos tipos de nó customizados, no algoritmo de layout Dagre, no estilo das arestas para tipos de relacionamento e nas estratégias de desempenho que se tornam necessárias conforme as árvores crescem.

Esta publicação cobre as decisões de implementação que tomamos, as trocas envolvidas e os lugares onde as abstrações do ReactFlow tanto ajudaram quanto restringiram o que poderíamos construir.


Por Que ReactFlow

A alternativa que consideramos seriamente foi o D3. Os algoritmos de layout de árvore do D3 são maduros, bem documentados e têm sido usados para visualização genealógica por anos. O problema é que o D3 requer o gerenciamento direto do DOM, o que conflita com o modelo de reconciliação do React se você quiser componentes React para os elementos interativos. As abordagens híbridas funcionam, mas são difíceis de manter.

O ReactFlow mantém tudo no modelo de componentes do React. Nós e arestas são objetos JavaScript simples no state. A biblioteca lida com renderização SVG, pan e zoom, e eventos de interação. Os tipos de nó customizados são apenas componentes React. Isso torna direto embutir conteúdo interativo rico em cada nó — espaços reservados para fotos, seções de detalhes expansíveis, botões de ação — sem sair do modelo do React.

A troca é que o ReactFlow é mais opinativo do que o D3. O modelo de dados de nó e aresta tem uma forma específica, o layout é tratado externamente (o ReactFlow não inclui um algoritmo de layout) e o desempenho em contagens de nó muito grandes requer gerenciamento mais explícito do que uma abordagem D3 pura. Para um projeto na faixa de algumas centenas de nós, essas trocas são favoráveis.

Tipos de Nó Customizados: Cards de Pessoa

O sistema de nós customizados do ReactFlow é um de seus pontos mais fortes. Um tipo de nó customizado é um componente React que recebe os dados do nó como props e renderiza o que você quiser. O único requisito é que ele renderize os componentes Handle nos pontos de conexão onde as arestas se conectam.

O componente PersonNode renderiza um card com o nome da pessoa, ano de nascimento, ano de morte (se aplicável) e um espaço reservado para foto que mostra as iniciais da pessoa quando nenhuma foto está disponível. O card tem uma borda sutil que muda de cor com base na profundidade de geração — uma pista visual que ajuda a orientar o visualizador em uma árvore profunda. Pessoas vivas e falecidas usam tratamentos visuais distintos.

Uma decisão de design que melhorou significativamente a clareza: o nó tem duas posições de handle distintas no topo e na parte inferior, não os quatro lados padrão. As arestas pai-filho conectam de baixo para cima, então a árvore flui verticalmente com uma direção consistente. As arestas de cônjuge conectam lado a lado no mesmo nível de geração. Essa separação dos tipos de aresta por posição de handle torna a direcionalidade dos relacionamentos imediatamente legível.

O componente de nó também lida com o estado “colapsado” para subárvores profundas. Quando uma subárvore é colapsada, o nó renderiza um badge mostrando o número de descendentes ocultos. Clicar no badge expande a subárvore. Esse modelo de interação é essencial para tornar árvores grandes utilizáveis — uma visão plana de quatrocentos nós é ilegível, mas uma visão onde a maior parte da árvore está colapsada e você expande os ramos sob demanda é navegável.

Layout Dagre para Posicionamento Hierárquico

O ReactFlow não calcula as posições dos nós. Você fornece posições como parte dos dados do nó, e a biblioteca os renderiza onde você mandar. Para uma árvore genealógica, calcular essas posições manualmente exigiria reimplementar um algoritmo de layout hierárquico. A escolha padrão é o Dagre, uma biblioteca de layout de grafo acíclico dirigido que produz layouts hierárquicos limpos com cruzamentos mínimos.

A integração do Dagre fica fora do ciclo de renderização do ReactFlow. Quando os dados de nó ou aresta mudam, uma função de layout roda sincronamente, atribui posições a cada nó e retorna o array de nós posicionados que o ReactFlow renderiza. A função pega os nós e arestas brutos, constrói um grafo Dagre, roda o layout e lê de volta os valores x e y computados.

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 } };
  });
}

O parâmetro rankdir do Dagre controla a direção do layout. “TB” (de cima para baixo) é a escolha natural para uma árvore genealógica onde o tempo flui para baixo através das gerações. ranksep controla o espaçamento vertical entre gerações; nodesep controla o espaçamento horizontal entre irmãos. Esses valores precisam de ajuste para os tamanhos específicos de nó que você está usando.

A limitação do Dagre para árvores genealógicas é que ele trata o grafo como um DAG. As arestas de cônjuge — que conectam dois nós no mesmo rank e são não direcionadas — não são arestas DAG naturais. A solução alternativa é rodar o layout Dagre usando apenas arestas pai-filho para estabelecer ranks de geração e posições horizontais, em seguida desenhar as arestas de cônjuge separadamente como linhas retas na mesma coordenada y. Isso produz o resultado visual correto: cônjuges aparecem lado a lado no mesmo nível com uma aresta horizontal entre eles.

Estilo de Arestas para Tipos de Relacionamento

O ReactFlow suporta componentes de aresta customizados da mesma forma que os componentes de nó customizados. Cada tipo de relacionamento — pai-filho, cônjuge, adotivo, padrasto — usa um estilo de aresta distinto. As arestas pai-filho são linhas sólidas. As arestas de cônjuge são conectores horizontais finos. Os relacionamentos adotivos usam uma linha tracejada. Os relacionamentos de padrasto usam uma linha pontilhada.

O componente de aresta recebe as posições dos nós de origem e destino como props e renderiza um caminho SVG. Para arestas retas, isso é trivial. Para arestas curvas entre nós em posições diferentes, o ReactFlow fornece um conjunto de funções auxiliares de caminho (getStraightPath, getBezierPath, getSmoothStepPath) que geram a string de caminho SVG correta para as coordenadas de endpoint fornecidas.

Uma interação que os componentes de aresta customizados habilitam é um rótulo de aresta que aparece ao passar o mouse, mostrando a descrição completa do relacionamento. Isso é útil para estruturas familiares complexas onde o estilo visual por si só pode ser ambíguo — passar o mouse em uma aresta entre dois nós mostra “Pai adotivo — 1987” ou “Casados em 1975, divorciados em 1989.” O rótulo é renderizado como um foreignObject no SVG, o que permite usar HTML e CSS padrão para o estilo do tooltip.

Desempenho com Árvores Grandes

O ReactFlow lida com árvores de tamanho moderado — até algumas centenas de nós — sem otimização específica. Além disso, algumas estratégias se tornam necessárias.

O ReactFlow inclui um renderizador de virtualização integrado que renderiza apenas os nós dentro do viewport atual. Isso é habilitado por padrão e lida com a maior parte da preocupação de desempenho para árvores grandes, desde que os próprios componentes de nó não sejam caros para renderizar. Um PersonNode que aciona buscas de dados externos ou computações pesadas em cada renderização causará instabilidade independentemente do corte de viewport. Mantenha os componentes de nó puros e rápidos.

A segunda estratégia é memoizar os arrays de nós e arestas. Como o ReactFlow re-renderiza sempre que os arrays de nó ou aresta mudam por referência, passar arrays recém-construídos em cada renderização pai causa trabalho desnecessário. Envolver a computação de layout em useMemo com uma dependência nos dados brutos — não no estado derivado — garante que a computação de layout cara e o re-render associado ocorram apenas quando os dados subjacentes realmente mudam.

A terceira estratégia é a interação de colapso/expansão descrita na seção de tipo de nó. Uma árvore com 400 nós que exibe apenas 40 a qualquer momento tem desempenho idêntico a uma árvore de 40 nós. O estado de colapso é gerenciado no state React no nível da visualização, não no modelo de dados subjacente. Colapsar uma subárvore a filtra dos arrays de nós e arestas passados para o ReactFlow; expandi-la a adiciona de volta. O layout Dagre re-roda com o conjunto filtrado.

Considerações de Design de Interação

Além da mecânica de layout e renderização, o modelo de interação para uma visualização de árvore genealógica requer reflexão cuidadosa. O pan e zoom são fornecidos pelo ReactFlow por padrão, mas a visão inicial e o nível de zoom importam significativamente para a usabilidade. Chamar fitView() após o layout renderizar garante que a árvore seja visível e adequadamente dimensionada no carregamento.

A navegação entre indivíduos — clicar em uma pessoa para centralizá-la no viewport, seguir arestas para gerações adjacentes — requer os métodos de instância do ReactFlow setCenter() e fitBounds(). Esses são acessados via o hook useReactFlow() e devem ser chamados com animação suave habilitada para dar ao usuário um senso de continuidade espacial.

Busca e filtro — encontrar uma pessoa específica pelo nome e saltar para sua posição na árvore — é um requisito comum para qualquer árvore genealógica não trivial. A implementação faz o nó correspondente pulsar com destaque após centralizar o viewport nele. Isso é feito adicionando uma classe de animação CSS ao nó correspondente por uma curta duração, depois removendo-a. O método updateNode do ReactFlow lida com isso sem acionar um re-layout completo da árvore.

Insights Relacionados

Artigos relacionados