Claude Code Plugins

Community-maintained marketplace

Feedback
1
0

Automatic graph layout using dagre with React Flow (@xyflow/react). Use when implementing auto-layout, hierarchical layouts, tree structures, or arranging nodes programmatically. Triggers on dagre, auto-layout, automatic layout, getLayoutedElements, rankdir, hierarchical graph.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name dagre-react-flow
description Automatic graph layout using dagre with React Flow (@xyflow/react). Use when implementing auto-layout, hierarchical layouts, tree structures, or arranging nodes programmatically. Triggers on dagre, auto-layout, automatic layout, getLayoutedElements, rankdir, hierarchical graph.

Dagre with React Flow

Dagre is a JavaScript library for laying out directed graphs. It computes optimal node positions for hierarchical/tree layouts. React Flow handles rendering; dagre handles positioning.

Quick Start

pnpm add @dagrejs/dagre
import dagre from '@dagrejs/dagre';
import { Node, Edge } from '@xyflow/react';

const getLayoutedElements = (
  nodes: Node[],
  edges: Edge[],
  direction: 'TB' | 'LR' = 'TB'
) => {
  const g = new dagre.graphlib.Graph();
  g.setGraph({ rankdir: direction });
  g.setDefaultEdgeLabel(() => ({}));

  nodes.forEach((node) => {
    g.setNode(node.id, { width: 172, height: 36 });
  });

  edges.forEach((edge) => {
    g.setEdge(edge.source, edge.target);
  });

  dagre.layout(g);

  const layoutedNodes = nodes.map((node) => {
    const pos = g.node(node.id);
    return {
      ...node,
      position: { x: pos.x - 86, y: pos.y - 18 }, // Center to top-left
    };
  });

  return { nodes: layoutedNodes, edges };
};

Core Concepts

Coordinate System Difference

Critical: Dagre returns center coordinates; React Flow uses top-left.

// Dagre output: center of node
const dagrePos = g.node(nodeId); // { x: 100, y: 50 } = center

// React Flow expects: top-left corner
const rfPosition = {
  x: dagrePos.x - nodeWidth / 2,
  y: dagrePos.y - nodeHeight / 2,
};

Node Dimensions

Dagre requires explicit dimensions. Three approaches:

1. Fixed dimensions (simplest):

g.setNode(node.id, { width: 172, height: 36 });

2. Per-node dimensions from data:

g.setNode(node.id, {
  width: node.data.width ?? 172,
  height: node.data.height ?? 36,
});

3. Measured dimensions (most accurate):

// After React Flow measures nodes
g.setNode(node.id, {
  width: node.measured?.width ?? 172,
  height: node.measured?.height ?? 36,
});

Layout Directions

Value Direction Use Case
TB Top to Bottom Org charts, decision trees
BT Bottom to Top Dependency graphs (deps at bottom)
LR Left to Right Timelines, horizontal flows
RL Right to Left RTL layouts
g.setGraph({ rankdir: 'LR' }); // Horizontal layout

Complete Implementation

Basic Layout Function

import dagre from '@dagrejs/dagre';
import type { Node, Edge } from '@xyflow/react';

interface LayoutOptions {
  direction?: 'TB' | 'BT' | 'LR' | 'RL';
  nodeWidth?: number;
  nodeHeight?: number;
  nodesep?: number;  // Horizontal spacing
  ranksep?: number;  // Vertical spacing (between ranks)
}

export function getLayoutedElements(
  nodes: Node[],
  edges: Edge[],
  options: LayoutOptions = {}
): { nodes: Node[]; edges: Edge[] } {
  const {
    direction = 'TB',
    nodeWidth = 172,
    nodeHeight = 36,
    nodesep = 50,
    ranksep = 50,
  } = options;

  const g = new dagre.graphlib.Graph();
  g.setGraph({ rankdir: direction, nodesep, ranksep });
  g.setDefaultEdgeLabel(() => ({}));

  nodes.forEach((node) => {
    const width = node.measured?.width ?? nodeWidth;
    const height = node.measured?.height ?? nodeHeight;
    g.setNode(node.id, { width, height });
  });

  edges.forEach((edge) => {
    g.setEdge(edge.source, edge.target);
  });

  dagre.layout(g);

  const layoutedNodes = nodes.map((node) => {
    const pos = g.node(node.id);
    const width = node.measured?.width ?? nodeWidth;
    const height = node.measured?.height ?? nodeHeight;

    return {
      ...node,
      position: {
        x: pos.x - width / 2,
        y: pos.y - height / 2,
      },
    };
  });

  return { nodes: layoutedNodes, edges };
}

React Flow Integration

import { useCallback } from 'react';
import {
  ReactFlow,
  useNodesState,
  useEdgesState,
  useReactFlow,
  ReactFlowProvider,
} from '@xyflow/react';
import { getLayoutedElements } from './layout';

const initialNodes = [
  { id: '1', data: { label: 'Start' }, position: { x: 0, y: 0 } },
  { id: '2', data: { label: 'Process' }, position: { x: 0, y: 0 } },
  { id: '3', data: { label: 'End' }, position: { x: 0, y: 0 } },
];

const initialEdges = [
  { id: 'e1-2', source: '1', target: '2' },
  { id: 'e2-3', source: '2', target: '3' },
];

// Apply initial layout
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
  initialNodes,
  initialEdges,
  { direction: 'TB' }
);

function Flow() {
  const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges);
  const { fitView } = useReactFlow();

  const onLayout = useCallback((direction: 'TB' | 'LR') => {
    const { nodes: newNodes, edges: newEdges } = getLayoutedElements(
      nodes,
      edges,
      { direction }
    );

    setNodes([...newNodes]);
    setEdges([...newEdges]);

    // Fit view after layout with animation
    window.requestAnimationFrame(() => {
      fitView({ duration: 300 });
    });
  }, [nodes, edges, setNodes, setEdges, fitView]);

  return (
    <div style={{ width: '100%', height: '100vh' }}>
      <div style={{ position: 'absolute', zIndex: 10, padding: 10 }}>
        <button onClick={() => onLayout('TB')}>Vertical</button>
        <button onClick={() => onLayout('LR')}>Horizontal</button>
      </div>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        fitView
      />
    </div>
  );
}

export default function App() {
  return (
    <ReactFlowProvider>
      <Flow />
    </ReactFlowProvider>
  );
}

useAutoLayout Hook

Reusable hook for automatic layout:

import { useCallback, useEffect, useRef } from 'react';
import {
  useReactFlow,
  useNodesInitialized,
  type Node,
  type Edge,
} from '@xyflow/react';
import dagre from '@dagrejs/dagre';

interface UseAutoLayoutOptions {
  direction?: 'TB' | 'BT' | 'LR' | 'RL';
  nodesep?: number;
  ranksep?: number;
}

export function useAutoLayout(options: UseAutoLayoutOptions = {}) {
  const { direction = 'TB', nodesep = 50, ranksep = 50 } = options;
  const { getNodes, getEdges, setNodes, fitView } = useReactFlow();
  const nodesInitialized = useNodesInitialized();
  const layoutApplied = useRef(false);

  const runLayout = useCallback(() => {
    const nodes = getNodes();
    const edges = getEdges();

    const g = new dagre.graphlib.Graph();
    g.setGraph({ rankdir: direction, nodesep, ranksep });
    g.setDefaultEdgeLabel(() => ({}));

    nodes.forEach((node) => {
      g.setNode(node.id, {
        width: node.measured?.width ?? 172,
        height: node.measured?.height ?? 36,
      });
    });

    edges.forEach((edge) => {
      g.setEdge(edge.source, edge.target);
    });

    dagre.layout(g);

    const layouted = nodes.map((node) => {
      const pos = g.node(node.id);
      const width = node.measured?.width ?? 172;
      const height = node.measured?.height ?? 36;

      return {
        ...node,
        position: { x: pos.x - width / 2, y: pos.y - height / 2 },
      };
    });

    setNodes(layouted);
    window.requestAnimationFrame(() => fitView({ duration: 200 }));
  }, [direction, nodesep, ranksep, getNodes, getEdges, setNodes, fitView]);

  // Auto-layout on initialization
  useEffect(() => {
    if (nodesInitialized && !layoutApplied.current) {
      runLayout();
      layoutApplied.current = true;
    }
  }, [nodesInitialized, runLayout]);

  return { runLayout };
}

Usage:

function Flow() {
  const { runLayout } = useAutoLayout({ direction: 'LR', ranksep: 100 });

  return (
    <>
      <button onClick={runLayout}>Re-layout</button>
      <ReactFlow ... />
    </>
  );
}

Edge Options

Control edge routing with weight and minlen:

edges.forEach((edge) => {
  g.setEdge(edge.source, edge.target, {
    weight: edge.data?.priority ?? 1,  // Higher = more direct path
    minlen: edge.data?.minRanks ?? 1,  // Minimum ranks between nodes
  });
});

weight: Higher weight edges are prioritized for shorter, more direct paths.

minlen: Forces minimum rank separation between connected nodes.

// Force 2 ranks between nodes
g.setEdge('a', 'b', { minlen: 2 });

Common Patterns

Handle Position Based on Direction

Adjust handles for horizontal vs vertical layouts:

function CustomNode({ data }: NodeProps) {
  const isHorizontal = data.direction === 'LR' || data.direction === 'RL';

  return (
    <div>
      <Handle
        type="target"
        position={isHorizontal ? Position.Left : Position.Top}
      />
      <div>{data.label}</div>
      <Handle
        type="source"
        position={isHorizontal ? Position.Right : Position.Bottom}
      />
    </div>
  );
}

Animated Layout Transitions

Smooth position changes using CSS transitions:

.react-flow__node {
  transition: transform 300ms ease-out;
}

For programmatic animation, see reference.md.

Layout with Node Groups

Exclude group nodes from dagre layout:

const layoutWithGroups = (nodes: Node[], edges: Edge[]) => {
  // Separate regular nodes from groups
  const regularNodes = nodes.filter((n) => n.type !== 'group');
  const groupNodes = nodes.filter((n) => n.type === 'group');

  // Layout only regular nodes
  const { nodes: layouted } = getLayoutedElements(regularNodes, edges);

  // Combine back
  return { nodes: [...groupNodes, ...layouted], edges };
};

Troubleshooting

Nodes Overlapping

Increase spacing:

g.setGraph({
  rankdir: 'TB',
  nodesep: 100,  // Increase horizontal spacing
  ranksep: 100,  // Increase vertical spacing
});

Layout Not Updating

Ensure new array references:

// Wrong - same reference
setNodes(layoutedNodes);

// Correct - new reference
setNodes([...layoutedNodes]);

Nodes at Wrong Position

Check coordinate conversion:

// Dagre returns center, React Flow needs top-left
position: {
  x: pos.x - width / 2,   // Not just pos.x
  y: pos.y - height / 2,  // Not just pos.y
}

Performance with Large Graphs

  • Layout in a Web Worker
  • Debounce layout calls
  • Use useMemo for layout function
  • Only re-layout changed portions

Configuration Reference

See reference.md for complete dagre configuration options.