Skip to content

Commit 33218af

Browse files
authored
chore: add controls to elk story (#79)
* chore: add controls to elk story * chore: disable labels for some algorithms
1 parent 0136ee1 commit 33218af

File tree

2 files changed

+155
-33
lines changed

2 files changed

+155
-33
lines changed

src/stories/plugins/elk/elk.stories.tsx

Lines changed: 137 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
import React, { useEffect, useMemo, useState } from "react";
1+
import React, { useEffect, useMemo } from "react";
22

3-
import { Select, SelectOption, ThemeProvider } from "@gravity-ui/uikit";
3+
import { ThemeProvider } from "@gravity-ui/uikit";
4+
import { Description, Meta as StorybookMeta, Title } from "@storybook/blocks";
45
import type { Meta, StoryFn } from "@storybook/react";
5-
import ELK from "elkjs";
6-
7-
import { Graph, GraphCanvas, GraphState, TBlock, TConnection, useGraph, useGraphEvent } from "../../../index";
6+
import ELK, { ElkNode } from "elkjs";
7+
8+
import {
9+
Graph,
10+
GraphCanvas,
11+
GraphState,
12+
TBlock,
13+
TConnection,
14+
TGraphConfig,
15+
useGraph,
16+
useGraphEvent,
17+
} from "../../../index";
818
import { MultipointConnection, useElk } from "../../../plugins";
919
import { TMultipointConnection } from "../../../plugins/elk/types";
1020
import { useFn } from "../../../utils/hooks/useFn";
@@ -27,10 +37,12 @@ export enum Algorithm {
2737

2838
import "@gravity-ui/uikit/styles/styles.css";
2939

30-
const GraphApp = () => {
31-
const [algorithm, setAlgorithm] = useState(Algorithm.Layered);
32-
const [algorithms, setAlgorithms] = useState<SelectOption[]>([]);
40+
interface GraphAppProps {
41+
elkConfig?: ElkNode;
42+
graphConfig?: TGraphConfig;
43+
}
3344

45+
const GraphApp = ({ elkConfig, graphConfig }: GraphAppProps) => {
3446
const elk = useMemo(() => new ELK(), []);
3547

3648
const { graph, setEntities, start } = useGraph({
@@ -39,10 +51,6 @@ const GraphApp = () => {
3951
},
4052
});
4153

42-
const { elkConfig, graphConfig } = useMemo(() => {
43-
return getExampleConfig(algorithm);
44-
}, [algorithm]);
45-
4654
const { isLoading, result } = useElk(elkConfig, elk);
4755

4856
useEffect(() => {
@@ -75,18 +83,6 @@ const GraphApp = () => {
7583
graph.zoomTo("center", { padding: 300 });
7684
}, [isLoading, result]);
7785

78-
useEffect(() => {
79-
elk.knownLayoutAlgorithms().then((knownLayoutAlgorithms) => {
80-
setAlgorithms(
81-
knownLayoutAlgorithms.map((knownLayoutAlgorithm) => {
82-
const { id, name } = knownLayoutAlgorithm;
83-
const algId = id.split(".").at(-1);
84-
return { value: algId, content: name };
85-
})
86-
);
87-
});
88-
}, [elk]);
89-
9086
useGraphEvent(graph, "state-change", ({ state }) => {
9187
if (state === GraphState.ATTACHED) {
9288
start();
@@ -99,17 +95,130 @@ const GraphApp = () => {
9995

10096
return (
10197
<ThemeProvider theme={"light"}>
102-
<Select value={[algorithm]} options={algorithms} onUpdate={(v) => setAlgorithm(v[0] as Algorithm)}></Select>
103-
<GraphCanvas className="graph" graph={graph} renderBlock={renderBlockFn} />;
98+
<GraphCanvas className="graph" graph={graph} renderBlock={renderBlockFn} />
10499
</ThemeProvider>
105100
);
106101
};
107102

108-
const meta: Meta = {
103+
const meta: Meta<typeof GraphApp> = {
109104
title: "Plugins/ELK",
110105
component: GraphApp,
106+
tags: ["autodocs"],
107+
parameters: {
108+
docs: {
109+
page: () => (
110+
<>
111+
<StorybookMeta />
112+
<Title />
113+
<Description />
114+
</>
115+
),
116+
description: {
117+
component:
118+
"The useElk hook is used to compute graph layouts using the ELK (Eclipse Layout Kernel) layout engine. It " +
119+
"provides a React-friendly way to integrate ELK.js into your application.\n\n" +
120+
"## Usage\n\n" +
121+
"```tsx\n" +
122+
"const { isLoading, result } = useElk(elkConfig, elk);\n" +
123+
"```\n\n" +
124+
"### Parameters\n\n" +
125+
"- `elkConfig`: ElkNode configuration object that describes the graph structure and layout options\n" +
126+
"- `elk`: Instance of the ELK layout engine\n\n" +
127+
"### Returns\n\n" +
128+
"- `isLoading`: Boolean indicating if the layout computation is in progress\n" +
129+
"- `result`: The computed layout result containing node positions and edge routing information\n\n" +
130+
"## Layout Algorithms\n\n" +
131+
"ELK.js supports several layout algorithms that can be specified in the elkConfig:\n\n" +
132+
"- `box`: Pack the nodes like boxes\n" +
133+
"- `layered`: Hierarchical layout, good for DAGs and trees\n" +
134+
"- `force`: Force-directed layout\n" +
135+
"- `stress`: Stress-minimizing layout\n" +
136+
"- `mrtree`: Traditional tree layout\n" +
137+
"- `radial`: Radial layout\n" +
138+
"- `random`: Random node placement\n\n" +
139+
"## Example\n\n" +
140+
"```tsx\n" +
141+
"import React from 'react';\n" +
142+
"import { GraphCanvas, useGraph, useElk } from '@gravity-ui/graph';\n" +
143+
"import ELK from 'elkjs';\n\n" +
144+
"const elkConfig = {\n" +
145+
' id: "root",\n' +
146+
" children: [\n" +
147+
' { id: "n1", width: 30, height: 30 },\n' +
148+
' { id: "n2", width: 30, height: 30 }\n' +
149+
" ],\n" +
150+
" edges: [\n" +
151+
' { id: "e1", sources: ["n1"], targets: ["n2"] }\n' +
152+
" ],\n" +
153+
" layoutOptions: {\n" +
154+
" 'algorithm': 'layered',\n" +
155+
" 'elk.direction': 'DOWN',\n" +
156+
" 'elk.spacing.nodeNode': '50'\n" +
157+
" }\n" +
158+
"};\n\n" +
159+
"function MyGraphWithElk() {\n" +
160+
" const elk = React.useMemo(() => new ELK(), []);\n" +
161+
" const { graph } = useGraph();\n\n" +
162+
" const { isLoading, result } = useElk(elkConfig, elk);\n\n" +
163+
" React.useEffect(() => {\n" +
164+
" if (!isLoading && result) {\n" +
165+
" graph.setEntities({\n" +
166+
" blocks: result.blocks,\n" +
167+
" connections: result.edges\n" +
168+
" });\n" +
169+
" }\n" +
170+
" }, [isLoading, result]);\n\n" +
171+
" return <GraphCanvas graph={graph} />;\n" +
172+
"}\n" +
173+
"```\n\n" +
174+
"## Resources\n\n" +
175+
"- [ELK.js on GitHub](https://github.com/kieler/elkjs)\n" +
176+
"- [ELK Documentation](https://eclipse.dev/elk/documentation.html)\n" +
177+
"- [ELK Algorithm Documentation](https://eclipse.dev/elk/reference/algorithms.html)",
178+
},
179+
},
180+
},
181+
argTypes: {
182+
elkConfig: {
183+
control: "object",
184+
description: "ELK layout configuration object",
185+
},
186+
graphConfig: {
187+
control: "object",
188+
description: "Graph configuration with blocks and connections",
189+
},
190+
},
111191
};
112192

113193
export default meta;
114194

115-
export const Default: StoryFn = () => <GraphApp />;
195+
// Create a story for each algorithm
196+
export const Box: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
197+
Box.args = getExampleConfig(Algorithm.Box);
198+
199+
export const Layered: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
200+
Layered.args = getExampleConfig(Algorithm.Layered);
201+
202+
export const Disco: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
203+
Disco.args = getExampleConfig(Algorithm.Disco);
204+
205+
export const Radial: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
206+
Radial.args = getExampleConfig(Algorithm.Radial);
207+
208+
export const MrTree: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
209+
MrTree.args = getExampleConfig(Algorithm.MrTree);
210+
211+
export const Force: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
212+
Force.args = getExampleConfig(Algorithm.Force);
213+
214+
export const Stress: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
215+
Stress.args = getExampleConfig(Algorithm.Stress);
216+
217+
export const Random: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
218+
Random.args = getExampleConfig(Algorithm.Random);
219+
220+
export const SporeOverlap: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
221+
SporeOverlap.args = getExampleConfig(Algorithm.SporeOverlap);
222+
223+
export const SporeCompaction: StoryFn<typeof GraphApp> = (args) => <GraphApp {...args} />;
224+
SporeCompaction.args = getExampleConfig(Algorithm.SporeCompaction);

src/stories/plugins/elk/getExampleConfig.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ const prepareChildren = (blocks: TGraphConfig["blocks"]) => {
1919
});
2020
};
2121

22-
const prepareEdges = (connections: TGraphConfig["connections"]) => {
22+
const prepareEdges = (connections: TGraphConfig["connections"], skipLabels?: boolean) => {
2323
return connections.map((c, i) => {
2424
const labelText = `label ${i}`;
2525

2626
return {
2727
id: c.id as string,
2828
sources: [c.sourceBlockId as string],
2929
targets: [c.targetBlockId as string],
30-
labels: [{ text: labelText, width: measureText(labelText, `${FONT_SIZE}px sans-serif`), height: FONT_SIZE }],
30+
labels: skipLabels
31+
? []
32+
: [{ text: labelText, width: measureText(labelText, `${FONT_SIZE}px sans-serif`), height: FONT_SIZE }],
3133
} satisfies ElkExtendedEdge;
3234
});
3335
};
@@ -165,19 +167,30 @@ export const getExampleConfig = (algorithm: Algorithm): { elkConfig: ElkNode; gr
165167
};
166168
}
167169
case Algorithm.Radial:
170+
case Algorithm.Stress:
171+
case Algorithm.SporeOverlap: {
172+
config = generateExampleTree(4);
173+
174+
return {
175+
elkConfig: {
176+
...elkConfig,
177+
children: prepareChildren(config.blocks),
178+
edges: prepareEdges(config.connections),
179+
},
180+
graphConfig: config,
181+
};
182+
}
168183
case Algorithm.MrTree:
169184
case Algorithm.Random:
170185
case Algorithm.Force:
171-
case Algorithm.Stress:
172-
case Algorithm.SporeOverlap:
173186
case Algorithm.SporeCompaction: {
174187
config = generateExampleTree(4);
175188

176189
return {
177190
elkConfig: {
178191
...elkConfig,
179192
children: prepareChildren(config.blocks),
180-
edges: prepareEdges(config.connections),
193+
edges: prepareEdges(config.connections, true),
181194
},
182195
graphConfig: config,
183196
};

0 commit comments

Comments
 (0)