-
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add Dagre layout component for directed acyclic graphs (DAGs) (#…
…268) * feat: Add Dagre layout component for directed acyclic graphs (DAGs) * docs: Add `grid-cols-*` RAM utils (xs, sm, md, lg) * feat(Dagre): Add nodes(), edges(), and nodeId() accessor props * feat(CurveMenuField): Forward restProps to underlying MenuField * docs(Dagre): Add basic docs * fix: workaround type/check errors * feat(TransformControls): Support setting button `size` and add container border by default * docs(Dagre): Add transform * fix: Incorrect edgeSeperation prop type reference * feat(Dagre): Add edge label support. Support "none" alignment. Add TCP State Diagram example. * docs: Add colors to CLOSED/ESTAB nodes * docs(Dagre): Move settings to sidebar toggle * feat(Dagre): Support passing `directed`, `multigraph`, and `compound` graph optoins, and specifying `parent` on node for compound graphs. Add WIP compound/cluster graph example * docs(Dagre): Add playground with more data examples * docs(Dagre): Support customizing arrow in examples and improve link opacity handling * docs(Dagre): Remove cluster example until aligns with dagre-d3 example * fix(Spline): Improve initial data display / transition on non-cartesian charts (ex. hierarchy/graph) * fix(Dagre): Resolve type check errors
- Loading branch information
Showing
21 changed files
with
3,264 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'layerchart': minor | ||
--- | ||
|
||
feat: Add Dagre layout component for directed acyclic graphs (DAGs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'layerchart': patch | ||
--- | ||
|
||
fix(Spline): Improve initial data display / transition on non-cartesian charts (ex. hierarchy/graph) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
<script module> | ||
export type DagreGraphData = { | ||
nodes: Array<{ id: string; parent?: string; label?: string | dagre.Label }>; | ||
edges: Array<{ source: string; target: string; label?: string }>; | ||
}; | ||
export const RankDir = { | ||
'top-bottom': 'TB', | ||
'bottom-top': 'BT', | ||
'left-right': 'LR', | ||
'right-left': 'RL', | ||
}; | ||
export const Align = { | ||
none: undefined, | ||
'up-left': 'UL', | ||
'up-right': 'UR', | ||
'down-left': 'DL', | ||
'down-right': 'DR', | ||
}; | ||
export const EdgeLabelPosition = { | ||
left: 'l', | ||
center: 'c', | ||
right: 'r', | ||
}; | ||
</script> | ||
|
||
<script lang="ts"> | ||
import dagre, { type Edge, type EdgeConfig, type GraphEdge } from '@dagrejs/dagre'; | ||
/** Data of nodes and edges to build graph */ | ||
export let data: DagreGraphData; | ||
export let nodes = (d: any) => d.nodes; | ||
export let nodeId = (d: any) => d.id; | ||
export let edges = (d: any) => d.edges; | ||
/** Set graph as directed (true, default) or undirected (false), which does not treat the order of nodes in an edge as significant. */ | ||
export let directed = true; | ||
/** Allow a graph to have multiple edges between the same pair of nodes */ | ||
export let multigraph = false; | ||
/** Allow a graph to have compound nodes - nodes which can be the `parent` of other nodes */ | ||
export let compound = false; | ||
/** Type of algorithm to assigns a rank to each node in the input graph */ | ||
export let ranker: 'network-simplex' | 'tight-tree' | 'longest-path' = 'network-simplex'; | ||
/** Direction for rank nodes */ | ||
export let direction: keyof typeof RankDir = 'top-bottom'; | ||
/** Alignment for rank nodes */ | ||
export let align: keyof typeof Align | undefined = undefined; | ||
/** Number of pixels between each rank in the layout */ | ||
export let rankSeparation = 50; | ||
/** Number of pixels that separate nodes horizontally in the layout */ | ||
export let nodeSeparation = 50; | ||
/** Number of pixels that separate edges horizontally in the layout */ | ||
export let edgeSeparation = 10; | ||
/** Default node width if not defined on node */ | ||
export let nodeWidth = 100; | ||
/** Default node height if not defined on node */ | ||
export let nodeHeight = 50; | ||
/** Default link label width if not defined on edge */ | ||
export let edgeLabelWidth = 100; | ||
/** Default edge label height if not defined on edge */ | ||
export let edgeLabelHeight = 20; | ||
/** Default edge label height if not defined on edge */ | ||
export let edgeLabelPosition: keyof typeof EdgeLabelPosition = 'center'; | ||
/** Default pixels to move the label away from the edge if not defined on edge. Applies only when labelpos is l or r.*/ | ||
export let edgeLabelOffset = 10; | ||
/** Filter nodes */ | ||
export let filterNodes: (nodeId: string, graph: dagre.graphlib.Graph) => boolean = () => true; | ||
let graph: dagre.graphlib.Graph; | ||
$: { | ||
let g = new dagre.graphlib.Graph({ directed, multigraph, compound }); | ||
g.setGraph({ | ||
ranker: ranker, | ||
rankdir: RankDir[direction], | ||
align: align ? Align[align] : undefined, | ||
ranksep: rankSeparation, | ||
nodesep: nodeSeparation, | ||
edgesep: edgeSeparation, | ||
}); | ||
g.setDefaultEdgeLabel(() => { | ||
return {}; | ||
}); | ||
nodes(data).forEach((n: any) => { | ||
const id = nodeId(n); | ||
g.setNode(nodeId(n), { | ||
id, | ||
label: typeof n.label === 'string' ? n.label : id, | ||
width: nodeWidth, | ||
height: nodeHeight, | ||
...(typeof n.label === 'object' ? n.label : null), | ||
}); | ||
if (n.parent) { | ||
g.setParent(id, n.parent); | ||
} | ||
}); | ||
edges(data).forEach((e: any) => { | ||
const { source, target, label, ...rest } = e; | ||
g.setEdge( | ||
e.source, | ||
e.target, | ||
label | ||
? { | ||
label: label, | ||
labelpos: EdgeLabelPosition[edgeLabelPosition], | ||
labeloffset: edgeLabelOffset, | ||
width: edgeLabelWidth, | ||
height: edgeLabelHeight, | ||
...rest, | ||
} | ||
: {} | ||
); | ||
}); | ||
g = filterNodes ? g.filterNodes((nodeId) => filterNodes(nodeId, graph)) : graph; | ||
dagre.layout(g); | ||
graph = g; | ||
} | ||
$: graphNodes = graph.nodes().map((id) => graph.node(id)); | ||
$: graphEdges = graph.edges().map((edge) => ({ ...edge, ...graph.edge(edge) })) as Array< | ||
Edge & EdgeConfig & GraphEdge // `EdgeConfig` is excluded when inferred from usage | ||
>; | ||
</script> | ||
|
||
<slot nodes={graphNodes} edges={graphEdges} /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -120,8 +120,6 @@ | |
if (curve) path.curve(curve); | ||
return path(data ?? $contextData); | ||
} else { | ||
return ''; | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { describe, it, expect } from 'vitest'; | ||
import dagre from '@dagrejs/dagre'; | ||
import { ancestors, descendants } from './graph.js'; | ||
|
||
const exampleGraph = { | ||
nodes: [ | ||
{ id: 'A' }, | ||
{ id: 'B' }, | ||
{ id: 'C' }, | ||
{ id: 'D' }, | ||
{ id: 'E' }, | ||
{ id: 'F' }, | ||
{ id: 'G' }, | ||
{ id: 'H' }, | ||
{ id: 'I' }, | ||
], | ||
edges: [ | ||
{ source: 'A', target: 'B' }, | ||
{ source: 'C', target: 'B' }, | ||
{ source: 'B', target: 'E' }, | ||
{ source: 'B', target: 'F' }, | ||
{ source: 'D', target: 'E' }, | ||
{ source: 'D', target: 'F' }, | ||
{ source: 'E', target: 'H' }, | ||
{ source: 'G', target: 'H' }, | ||
{ source: 'H', target: 'I' }, | ||
], | ||
}; | ||
|
||
function buildGraph(data: typeof exampleGraph) { | ||
const g = new dagre.graphlib.Graph(); | ||
|
||
g.setGraph({}); | ||
|
||
data.nodes.forEach((n) => { | ||
g.setNode(n.id, { | ||
label: n.id, | ||
}); | ||
}); | ||
|
||
data.edges.forEach((e) => { | ||
g.setEdge(e.source, e.target); | ||
}); | ||
|
||
return g; | ||
} | ||
|
||
describe('accessors', () => { | ||
it('start of graph ', () => { | ||
const graph = buildGraph(exampleGraph); | ||
const actual = ancestors(graph, 'A'); | ||
expect(actual).length(0); | ||
}); | ||
|
||
it('middle of graph ', () => { | ||
const graph = buildGraph(exampleGraph); | ||
const actual = ancestors(graph, 'E'); | ||
expect(actual).to.have.members(['A', 'B', 'C', 'D']); | ||
}); | ||
|
||
it('end of graph ', () => { | ||
const graph = buildGraph(exampleGraph); | ||
const actual = ancestors(graph, 'I'); | ||
expect(actual).to.have.members(['A', 'B', 'C', 'D', 'E', 'G', 'H']); | ||
}); | ||
|
||
it('max depth', () => { | ||
const graph = buildGraph(exampleGraph); | ||
const actual = ancestors(graph, 'H', 2); | ||
expect(actual).to.have.members(['B', 'D', 'E', 'G']); | ||
}); | ||
}); | ||
|
||
describe('descendants', () => { | ||
it('start of graph ', () => { | ||
const graph = buildGraph(exampleGraph); | ||
const actual = descendants(graph, 'A'); | ||
expect(actual).to.have.members(['B', 'E', 'F', 'H', 'I']); | ||
}); | ||
|
||
it('middle of graph ', () => { | ||
const graph = buildGraph(exampleGraph); | ||
const actual = descendants(graph, 'E'); | ||
expect(actual).to.have.members(['H', 'I']); | ||
}); | ||
|
||
it('end of graph ', () => { | ||
const graph = buildGraph(exampleGraph); | ||
const actual = descendants(graph, 'I'); | ||
expect(actual).length(0); | ||
}); | ||
|
||
it('max depth', () => { | ||
const graph = buildGraph(exampleGraph); | ||
const actual = descendants(graph, 'B', 2); | ||
expect(actual).to.have.members(['E', 'F', 'H']); | ||
}); | ||
}); |
Oops, something went wrong.