Skip to content

Commit

Permalink
feat: Use dagre library for auto-layout in React Flow trees
Browse files Browse the repository at this point in the history
The way we were previously implementing layout was messy, and contained arbitrary hardcoded values.

The file `useLayout.ts` came almost directly from the React Flow Pro example at https://reactflow.dev/docs/examples/layout/auto-layout/. But we had to add two `as never` annotation to get it accepted by TypeScript's strict mode. Note that the dodgy dynamic typing is confined to within the module, so the API is safe. Also note that we can't just turn off strict mode for a single module or definition: microsoft/TypeScript#28306.
  • Loading branch information
georgefst authored and dhess committed Jun 2, 2022
1 parent 255d6ce commit 549b829
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 8 deletions.
2 changes: 2 additions & 0 deletions packages/primer-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
"@dicebear/avatars-bottts-sprites": "^4.9.1",
"@dicebear/avatars-identicon-sprites": "^4.9.1",
"@dicebear/avatars-jdenticon-sprites": "^4.9.1",
"@types/dagre": "^0.7.47",
"@visx/group": "^2.10.0",
"@visx/hierarchy": "^2.10.0",
"@visx/shape": "^2.10.0",
"@visx/text": "^2.10.0",
"d3": "7.4.4",
"dagre": "^0.8.5",
"fp-ts": "^2.12.1",
"react": "^17.0.0",
"react-dom": "^17.0.0",
Expand Down
15 changes: 7 additions & 8 deletions packages/primer-components/src/TreeReactFlow/TreeReactFlow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TreeInteractiveRender } from "@hackworthltd/primer-types";
import ReactFlow, { Node, Edge, MiniMap, Controls } from "react-flow-renderer";
import useLayout from "./useLayout";

export type TreeReactFlowProps = {
tree: TreeInteractiveRender;
Expand All @@ -8,21 +9,17 @@ export type TreeReactFlowProps = {
};

const convertTree = (
tree: TreeInteractiveRender,
x: number = 0,
y: number = 0
tree: TreeInteractiveRender
): {
nodes: Node[];
edges: Edge[];
} => {
const children = tree.childTrees.map((t, n) =>
convertTree(t, x + n * 200, y + 60)
);
const children = tree.childTrees.map(convertTree);
const id = tree.nodeId.toString();
const thisNode: Node = {
id,
data: { label: tree.label },
position: { x, y },
position: { x: 0, y: 0 }, // this gets overwritten by layout algorithm
};
const thisToChildren: Edge[] = tree.childTrees.map((t) => {
const target = t.nodeId.toString();
Expand All @@ -40,10 +37,12 @@ const convertTree = (

export const TreeReactFlow = (args: TreeReactFlowProps) => {
const tree = convertTree(args.tree);
const layoutedNodes = useLayout(tree.nodes, tree.edges, { direction: "TB" });

return (
<div style={{ height: args.height, width: args.width }}>
<ReactFlow
nodes={tree.nodes}
nodes={layoutedNodes}
edges={tree.edges}
onNodeClick={(e, _n) => args.tree.onClick?.(e)}
>
Expand Down
65 changes: 65 additions & 0 deletions packages/primer-components/src/TreeReactFlow/useLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useMemo } from "react";
import { Node, Edge, Position } from "react-flow-renderer";
import dagre from "dagre";

// the layout direction (T = top, R = right, B = bottom, L = left, TB = top to bottom, ...)
export type Direction = "TB" | "LR" | "RL" | "BT";

export type Options = {
direction: Direction;
};

const dagreGraph = new dagre.graphlib!.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));

const nodeWidth = 100;
const nodeHeight = 50;

const positionMap = {
T: Position.Top,
L: Position.Left,
R: Position.Right,
B: Position.Bottom,
};

function layoutGraph(
nodes: Node[],
edges: Edge[],
{ direction = "TB" }: Options
) {
dagreGraph.setGraph({ rankdir: direction });

nodes.forEach((el) => {
dagreGraph.setNode(el.id, { width: nodeWidth, height: nodeHeight });
});

edges.forEach((el) => {
dagreGraph.setEdge(el.source, el.target);
});

dagre.layout(dagreGraph);

const layoutNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = positionMap[direction[0] as never];
node.sourcePosition = positionMap[direction[1] as never];

node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
};

return node;
});

return layoutNodes;
}

function useLayout(nodes: Node[], edges: Edge[], options: Options) {
return useMemo(
() => layoutGraph(nodes, edges, options),
[nodes, edges, options]
);
}

export default useLayout;
21 changes: 21 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 549b829

Please sign in to comment.