Skip to content

Commit 5384a21

Browse files
authored
fix/COMPASS-9944 alternative applyLayout for no edges (#151)
1 parent b689410 commit 5384a21

File tree

6 files changed

+115
-61
lines changed

6 files changed

+115
-61
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"@leafygreen-ui/typography": "^22.1.0",
6060
"@xyflow/react": "12.5.1",
6161
"d3-path": "^3.1.0",
62-
"elkjs": "^0.10.0",
62+
"elkjs": "^0.11.0",
6363
"react": "17.0.2",
6464
"react-dom": "17.0.2"
6565
},

src/mocks/decorators/diagram-stress-test.decorator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const useStressTestNodesAndEdges = (nodeCount: number) => {
7777
}));
7878

7979
let applyUpdate = true;
80-
applyLayout<NodeProps, EdgeProps>(newNodes, newEdges, 'STAR').then(result => {
80+
applyLayout<NodeProps, EdgeProps>({ nodes: newNodes, edges: newEdges, direction: 'STAR' }).then(result => {
8181
if (!applyUpdate) return;
8282
setNodes(result.nodes);
8383
setEdges(result.edges);

src/types/layout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Measured, Position } from '@/types/node';
33
/**
44
* Defines the available layout directions for a diagram.
55
*/
6-
export type LayoutDirection = 'LEFT_RIGHT' | 'TOP_BOTTOM' | 'STAR';
6+
export type LayoutDirection = 'LEFT_RIGHT' | 'TOP_BOTTOM' | 'STAR' | 'RECTANGLE';
77

88
/**
99
* A minimal representation of a node used during layout calculation.

src/utilities/apply-layout.test.ts

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('apply-layout', () => {
3434
},
3535
},
3636
];
37-
const edges: EdgeProps[] = [
37+
const edges: [EdgeProps, ...EdgeProps[]] = [
3838
{
3939
id: '1',
4040
source: '3',
@@ -44,34 +44,34 @@ describe('apply-layout', () => {
4444
},
4545
];
4646
it('With no nodes or edges', async () => {
47-
const result = await applyLayout<NodeProps, EdgeProps>([], [], 'TOP_BOTTOM');
47+
const result = await applyLayout<NodeProps>({ nodes: [] });
4848
expect(result).toEqual({
4949
nodes: [],
5050
edges: [],
5151
});
5252
});
5353
it('With nodes (not measured, 0 fields)', async () => {
54-
const result = await applyLayout<NodeProps, EdgeProps>(nodes, [], 'TOP_BOTTOM');
54+
const result = await applyLayout<NodeProps>({ nodes });
5555
expect(result.nodes).toEqual([
5656
expect.objectContaining({
5757
...nodes[0],
5858
position: {
59-
x: 12,
60-
y: 12,
59+
x: 15,
60+
y: 15,
6161
},
6262
}),
6363
expect.objectContaining({
6464
...nodes[1],
6565
position: {
66-
x: 12,
67-
y: 78, // 12 + 44 (0 fields height) + 2*10 (padding) + 2 (extra padding)
66+
x: 15,
67+
y: 161, // 15 + 44 (0 fields height) + 100 (spacing) + 2 (extra padding)
6868
},
6969
}),
7070
expect.objectContaining({
7171
...nodes[2],
7272
position: {
73-
x: 12,
74-
y: 144, // 86 + 44 (0 fields height) + 2*10 (padding) + 2 (extra padding)
73+
x: 15,
74+
y: 307, // 161 + 44 (0 fields height) + 100 (spacing) + 2 (extra padding)
7575
},
7676
}),
7777
]);
@@ -81,27 +81,27 @@ describe('apply-layout', () => {
8181
...node,
8282
fields: [{ name: 'field1', type: 'string' }],
8383
}));
84-
const result = await applyLayout<NodeProps, EdgeProps>(nodesWithOneField, [], 'TOP_BOTTOM');
84+
const result = await applyLayout<NodeProps>({ nodes: nodesWithOneField });
8585
expect(result.nodes).toEqual([
8686
expect.objectContaining({
8787
...nodesWithOneField[0],
8888
position: {
89-
x: 12,
90-
y: 12,
89+
x: 15,
90+
y: 15,
9191
},
9292
}),
9393
expect.objectContaining({
9494
...nodesWithOneField[1],
9595
position: {
96-
x: 12,
97-
y: 96, // 12 + 62 (1 field height) + 2*10 (padding) + 2 (extra padding)
96+
x: 15,
97+
y: 179, // 15 + 62 (1 field height) + 100 (spacing) + 2 (extra padding)
9898
},
9999
}),
100100
expect.objectContaining({
101101
...nodesWithOneField[2],
102102
position: {
103-
x: 12,
104-
y: 180, // 96 + 62 (1 field height) + 2*10 (padding) + 2 (extra padding)
103+
x: 15,
104+
y: 343, // 179 + 62 (1 field height) + 100 (spacing) + 2 (extra padding)
105105
},
106106
}),
107107
]);
@@ -111,27 +111,27 @@ describe('apply-layout', () => {
111111
...node,
112112
fields: undefined,
113113
}));
114-
const result = await applyLayout<BaseNode, EdgeProps>(baseNodes, [], 'TOP_BOTTOM');
114+
const result = await applyLayout<BaseNode>({ nodes: baseNodes });
115115
expect(result.nodes).toEqual([
116116
expect.objectContaining({
117117
...baseNodes[0],
118118
position: {
119-
x: 12,
120-
y: 12,
119+
x: 15,
120+
y: 15,
121121
},
122122
}),
123123
expect.objectContaining({
124124
...baseNodes[1],
125125
position: {
126-
x: 12,
127-
y: 96, // 12 + 62 (default height) + 2*10 (padding) + 2 (extra padding)
126+
x: 15,
127+
y: 179, // 15 + 62 (default height) + 100 (spacing) + 2 (extra padding)
128128
},
129129
}),
130130
expect.objectContaining({
131131
...baseNodes[2],
132132
position: {
133-
x: 12,
134-
y: 180, // 96 + 62 (default height) + 2*10 (padding) + 2 (extra padding)
133+
x: 15,
134+
y: 343, // 179 + 62 (default height) + 100 (spacing) + 2 (extra padding)
135135
},
136136
}),
137137
]);
@@ -140,37 +140,37 @@ describe('apply-layout', () => {
140140
const measuredNodes = nodes.map(node => ({
141141
...node,
142142
measured: {
143-
width: 100,
143+
width: 200,
144144
height: 50,
145145
},
146146
}));
147-
const result = await applyLayout<NodeProps, EdgeProps>(measuredNodes, [], 'TOP_BOTTOM');
147+
const result = await applyLayout<NodeProps>({ nodes: measuredNodes });
148148
expect(result.nodes).toEqual([
149149
expect.objectContaining({
150150
...measuredNodes[0],
151151
position: {
152-
x: 12,
153-
y: 12,
152+
x: 15,
153+
y: 15,
154154
},
155155
}),
156156
expect.objectContaining({
157157
...measuredNodes[1],
158158
position: {
159-
x: 12,
160-
y: 82, // 12 + 50 (measured node height) + 2*10 (padding)
159+
x: 15,
160+
y: 165, // 15 + 50 (measured node height) + 100 (spacing)
161161
},
162162
}),
163163
expect.objectContaining({
164164
...measuredNodes[2],
165165
position: {
166-
x: 12,
167-
y: 152, // 82 + 50 (measured node height) + 2*10 (padding)
166+
x: 15,
167+
y: 315, // 165 + 50 (measured node height) + 100 (spacing)
168168
},
169169
}),
170170
]);
171171
});
172172
it('With nodes and edges', async () => {
173-
const result = await applyLayout<NodeProps, EdgeProps>(nodes, edges, 'TOP_BOTTOM');
173+
const result = await applyLayout<NodeProps, EdgeProps>({ nodes, edges, direction: 'TOP_BOTTOM' });
174174
expect(result.edges).toEqual([
175175
expect.objectContaining({
176176
id: '1',

src/utilities/apply-layout.ts

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,58 +24,112 @@ const STAR = {
2424
'spacing.nodeNode': `${DEFAULT_NODE_STAR_SPACING}`,
2525
};
2626

27-
const getLayoutOptions = (direction: LayoutDirection) => {
27+
const RECTANGLE = {
28+
'elk.algorithm': 'rectpacking',
29+
'spacing.nodeNode': `${DEFAULT_NODE_SPACING}`,
30+
};
31+
32+
const getLayoutOptions = (
33+
direction: LayoutDirection,
34+
options: Partial<Record<LayoutOptions, string>>,
35+
): Record<string, string> => {
36+
let elkLayoutOptions: Record<string, string> = {};
2837
switch (direction) {
2938
case 'LEFT_RIGHT':
30-
return LEFT_RIGHT;
39+
elkLayoutOptions = LEFT_RIGHT;
40+
break;
3141
case 'TOP_BOTTOM':
32-
return TOP_BOTTOM;
42+
elkLayoutOptions = TOP_BOTTOM;
43+
break;
3344
case 'STAR':
34-
return STAR;
35-
default:
36-
return {};
45+
elkLayoutOptions = STAR;
46+
break;
47+
case 'RECTANGLE':
48+
elkLayoutOptions = RECTANGLE;
49+
break;
50+
}
51+
if (options.aspectRatio) {
52+
elkLayoutOptions['elk.aspectRatio'] = options.aspectRatio;
3753
}
54+
return elkLayoutOptions;
3855
};
3956

57+
type LayoutOptions = 'aspectRatio';
58+
4059
/**
41-
* Applies a layout to a graph of nodes and edges using the ELK layout engine.
42-
*
43-
* This function transforms the provided nodes and edges into a format suitable for the ELK layout algorithm,
44-
* applies the layout based on the specified direction, and then returns a promise that resolves
45-
* to the nodes and edges with updated positions.
60+
* Applies a layout to a graph of nodes using the ELK layout engine.
61+
* When no edges are provided, defaults to RECTANGLE layout for grid arrangement.
4662
*
4763
* @param nodes A list of nodes.
64+
*/
65+
export function applyLayout<N extends BaseNode>({
66+
nodes,
67+
options,
68+
}: {
69+
nodes: N[];
70+
options?: Partial<Record<LayoutOptions, string>>;
71+
}): Promise<ApplyLayout<N, never>>;
72+
73+
/**
74+
* Applies a layout to a graph of nodes and edges using the ELK layout engine.
75+
* Use LEFT_RIGHT, TOP_BOTTOM, STAR layout for diagrams with edges,
76+
* and RECTANGLE for grid arrangement of unconnected nodes.
77+
* @param nodes A list of nodes.
4878
* @param edges A list of edges.
49-
* @param direction The layout direction to use, either "LEFT_RIGHT", "TOP_BOTTOM" or "STAR".
79+
* @param direction The layout direction to use.
5080
*/
51-
export const applyLayout = <N extends BaseNode, E extends BaseEdge>(
52-
nodes: N[],
53-
edges: E[],
54-
direction: LayoutDirection = 'TOP_BOTTOM',
55-
): Promise<ApplyLayout<N, E>> => {
81+
export function applyLayout<N extends BaseNode, E extends BaseEdge>({
82+
nodes,
83+
edges,
84+
direction,
85+
options,
86+
}: {
87+
nodes: N[];
88+
edges: E[];
89+
direction?: LayoutDirection;
90+
options?: Partial<Record<LayoutOptions, string>>;
91+
}): Promise<ApplyLayout<N, E>>;
92+
93+
export function applyLayout<N extends BaseNode, E extends BaseEdge>({
94+
nodes,
95+
edges,
96+
direction,
97+
options = {},
98+
}: {
99+
nodes: N[];
100+
edges?: E[];
101+
direction?: LayoutDirection;
102+
options?: Partial<Record<LayoutOptions, string>>;
103+
}): Promise<ApplyLayout<N, E>> {
104+
// If no edges are provided, use RECTANGLE layout for grid arrangement
105+
// Otherwise, use the specified direction or default to TOP_BOTTOM
106+
const layoutType = typeof edges === 'undefined' ? 'RECTANGLE' : (direction ?? 'TOP_BOTTOM');
107+
const layoutOptions = {
108+
...getLayoutOptions(layoutType, options),
109+
};
56110
const transformedNodes = nodes.map<N>(node => ({
57111
...node,
58112
height: getNodeHeight(node),
59113
width: getNodeWidth(node),
60114
}));
61115

62-
const transformedEdges = edges.map<ElkExtendedEdge>(edge => ({
116+
const transformedEdges = (edges ?? []).map<ElkExtendedEdge>(edge => ({
63117
...edge,
64118
id: edge.id,
65119
sources: [edge.source],
66120
targets: [edge.target],
67121
}));
68122

69123
const existingNodes: Record<string, N> = nodes.reduce((prev, curr) => ({ ...prev, [curr.id]: curr }), {});
70-
const existingEdges: Record<string, E> = edges.reduce((prev, curr) => ({ ...prev, [curr.id]: curr }), {});
124+
const existingEdges: Record<string, E> = (edges || []).reduce((prev, curr) => ({ ...prev, [curr.id]: curr }), {});
71125

72126
const elk = new ELK({});
73127

74128
return elk
75129
.layout({
76130
id: 'root',
77131
children: transformedNodes,
78-
layoutOptions: getLayoutOptions(direction),
132+
layoutOptions,
79133
edges: transformedEdges,
80134
})
81135
.then(g => {
@@ -97,4 +151,4 @@ export const applyLayout = <N extends BaseNode, E extends BaseEdge>(
97151
})),
98152
};
99153
});
100-
};
154+
}

yarn.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,7 +1430,7 @@ __metadata:
14301430
"@vitest/ui": "npm:3.2.4"
14311431
"@xyflow/react": "npm:12.5.1"
14321432
d3-path: "npm:^3.1.0"
1433-
elkjs: "npm:^0.10.0"
1433+
elkjs: "npm:^0.11.0"
14341434
eslint: "npm:^9.24.0"
14351435
eslint-config-prettier: "npm:10.1.5"
14361436
eslint-import-resolver-typescript: "npm:^4.3.1"
@@ -4538,10 +4538,10 @@ __metadata:
45384538
languageName: node
45394539
linkType: hard
45404540

4541-
"elkjs@npm:^0.10.0":
4542-
version: 0.10.0
4543-
resolution: "elkjs@npm:0.10.0"
4544-
checksum: 10c0/f04662fe5a972ab1fb62f698cd53ba8d5806a27ec07fd056e79a2dc89c158e63210636ce7e5dac7913967670b5f2b7c2db091f23746fea11a5ca751327ee3abc
4541+
"elkjs@npm:^0.11.0":
4542+
version: 0.11.0
4543+
resolution: "elkjs@npm:0.11.0"
4544+
checksum: 10c0/e3515eab869facd8c7acbb1d7d8a0f61686ec7ec4b42fd5c86b864216ee48a47f85376565d06f3f18d73b5a2c5de47aec7a4126ff059235d2a90099aee481b21
45454545
languageName: node
45464546
linkType: hard
45474547

0 commit comments

Comments
 (0)