@@ -4,7 +4,8 @@ import "@xyflow/react/dist/style.css";
4
4
5
5
import { scheme } from "vega-scale" ;
6
6
import { ErrorBoundary } from "react-error-boundary" ;
7
- import ELK , { ElkNode , ElkPrimitiveEdge } from "elkjs/lib/elk.bundled.js" ;
7
+ import type { EdgeProps , EdgeTypes , NodeProps } from "@xyflow/react" ;
8
+ import ELK , { ElkExtendedEdge , ElkNode } from "elkjs/lib/elk.bundled.js" ;
8
9
import { memo , startTransition , Suspense , use , useCallback , useEffect , useMemo , useState } from "react" ;
9
10
import {
10
11
ReactFlow ,
@@ -13,13 +14,14 @@ import {
13
14
Panel ,
14
15
NodeTypes ,
15
16
NodeMouseHandler ,
16
- Position ,
17
- Handle ,
18
17
Background ,
19
18
MarkerType ,
20
19
Edge ,
21
20
useNodesInitialized ,
22
21
useReactFlow ,
22
+ BaseEdge ,
23
+ Handle ,
24
+ Position ,
23
25
} from "@xyflow/react" ;
24
26
25
27
import "@xyflow/react/dist/style.css" ;
@@ -36,7 +38,7 @@ const layoutOptions = {
36
38
"elk.direction" : "DOWN" ,
37
39
"elk.portConstraints" : "FIXED_SIDE" ,
38
40
"elk.hierarchyHandling" : "INCLUDE_CHILDREN" ,
39
- // "elk.layered.mergeEdges": "True",
41
+ "elk.layered.mergeEdges" : "True" ,
40
42
"elk.edgeRouting" : "ORTHOGONAL" ,
41
43
"elk.layered.nodePlacement.strategy" : "NETWORK_SIMPLEX" ,
42
44
@@ -70,19 +72,26 @@ type Color = string;
70
72
// https://vega.github.io/vega/docs/schemes/#categorical
71
73
const colorScheme : Color [ ] = [ ...scheme ( "pastel1" ) , ...scheme ( "pastel2" ) ] ;
72
74
75
+ type FlowClass = Node < { color : string | null ; id : string } , "class" > ;
76
+ type FlowNode = Node < { label : string ; id : string } , "node" > ;
77
+ type FlowEdge = Edge < { points : { x : number ; y : number } [ ] } , "edge" > ;
78
+
79
+ type MyELKEdge = ElkExtendedEdge & { data : { sourceNode : string } } ;
73
80
/// ELK Node but with additional data added to be later used when converting to react flow nodes
74
- type MyELKNode = Omit < ElkNode , "children" > & {
81
+ type MyELKNode = Omit < ElkNode , "children" | "edges" > & {
82
+ edges : MyELKEdge [ ] ;
75
83
children : ( {
76
- type : string ;
77
- data : { color : string | null ; port : string ; id : string } ;
84
+ type : NonNullable < FlowClass [ "type" ] > ;
85
+ data : FlowClass [ "data" ] ;
86
+ edges : MyELKEdge [ ] ;
78
87
children : {
79
- type : string ;
88
+ type : NonNullable < FlowNode [ "type" ] > ;
80
89
width : number ;
81
90
height : number ;
82
- data : { label : string ; ports : { id : string } [ ] ; id : string } ;
91
+ data : FlowNode [ "data" ] ;
83
92
} [ ] &
84
93
ElkNode [ ] ;
85
- } & Omit < ElkNode , "children" | "position" > ) [ ] ;
94
+ } & Omit < ElkNode , "children" | "position" | "edges" > ) [ ] ;
86
95
} ;
87
96
88
97
/// ELK node but with layout information added
@@ -157,87 +166,45 @@ function toELKNode(
157
166
return {
158
167
id : `class-${ id } ` ,
159
168
data : { color : type_to_color . get ( egraph . class_data [ id ] ?. type ) || null , port : `port-${ id } ` , id } ,
160
- ports : [
161
- // {
162
- // id: `port-${id}`,
163
- // layoutOptions: {
164
- // "port.side": "NORTH",
165
- // },
166
- // },
167
- ] ,
168
- type : "class" ,
169
+ type : "class" as const ,
169
170
children : nodes . map ( ( [ id , node ] ) => {
170
- const ports = Object . keys ( node . children ) . map ( ( index ) => ( {
171
- id : `port-${ id } -${ index } ` ,
172
- layoutOptions : {
173
- "port.side" : "SOUTH" ,
174
- } ,
175
- } ) ) ;
176
171
// compute the size of the text by setting a dummy node element then measureing it
177
172
innerElem . innerText = node . op ;
178
173
const size = outerElem . getBoundingClientRect ( ) ;
179
174
return {
180
175
id : `node-${ id } ` ,
181
- type : "node" ,
182
- data : { label : node . op , ports , id } ,
176
+ type : "node" as const ,
177
+ data : { label : node . op , id } ,
183
178
width : size . width ,
184
179
height : size . height ,
185
- // one port for every index
186
- ports,
180
+ ports : Object . keys ( node . children ) . map ( ( index ) => ( {
181
+ id : `port-${ id } -${ index } ` ,
182
+ layoutOptions : {
183
+ "port.side" : "SOUTH" ,
184
+ } ,
185
+ } ) ) ,
187
186
} ;
188
187
} ) ,
189
188
edges : nodes . flatMap ( ( [ id , node ] ) =>
190
- [ ...node . children . entries ( ) ] . flatMap ( ( [ index , childNode ] ) => {
191
- const sourcePort = `port-${ id } -${ index } ` ;
192
- const class_ = nodeToClass . get ( childNode ) ! ;
193
- // only include if this is a self loop
194
- if ( class_ != id ) {
195
- return [ ] ;
196
- }
197
- // If the target or source class is not in the selected nodes, don't draw the edge
198
- const targetPort = `port-${ class_ } ` ;
199
- return [
200
- {
201
- id : `edge-${ id } -${ index } ` ,
202
- source : `node-${ id } ` ,
203
- sourcePort,
204
- sourceHandle : sourcePort ,
205
- target : `class-${ class_ } ` ,
206
- targetPort,
207
- targetHandle : targetPort ,
208
- } ,
209
- ] ;
210
- } )
189
+ [ ...node . children . entries ( ) ] . map ( ( [ index , childNode ] ) => ( {
190
+ id : `edge-${ id } -${ index } ` ,
191
+ data : { sourceNode : `node-${ id } ` } ,
192
+ sources : [ `port-${ id } -${ index } ` ] ,
193
+ targets : [ `class-${ nodeToClass . get ( childNode ) ! } ` ] ,
194
+ } ) )
211
195
) ,
212
196
} ;
213
197
} ) ;
214
198
215
- const edges : ElkPrimitiveEdge [ ] = Object . entries ( egraph . nodes ) . flatMap ( ( [ id , node ] ) =>
216
- [ ...node . children . entries ( ) ] . flatMap ( ( [ index , childNode ] ) => {
217
- const sourcePort = `port-${ id } -${ index } ` ;
218
- const class_ = nodeToClass . get ( childNode ) ! ;
219
- // If the target or source class is not in the selected nodes, don't draw the edge
220
- if (
221
- ! classToNodes . get ( node . eclass ) ?. find ( ( [ sourceID ] ) => sourceID == id ) ||
222
- ! classToNodes . has ( class_ ) ||
223
- class_ == nodeToClass . get ( id )
224
- ) {
225
- return [ ] ;
226
- }
227
- const targetPort = `port-${ class_ } ` ;
228
- return [
229
- {
230
- id : `edge-${ id } -${ index } ` ,
231
- source : `node-${ id } ` ,
232
- sourcePort,
233
- sourceHandle : sourcePort ,
234
- target : `class-${ class_ } ` ,
235
- targetPort,
236
- targetHandle : targetPort ,
237
- } ,
238
- ] ;
239
- } )
240
- ) ;
199
+ // move all edges that aren't self loops to the root
200
+ // https://github.com/eclipse/elk/issues/1068
201
+ const edges = [ ] ;
202
+ for ( const child of children ) {
203
+ const { loop, not } = Object . groupBy ( child . edges , ( { targets } ) => ( targets [ 0 ] === child . id ? "loop" : "not" ) ) ;
204
+ child . edges = loop || [ ] ;
205
+ edges . push ( ...( not || [ ] ) ) ;
206
+ }
207
+
241
208
return {
242
209
id : "--eclipse-layout-kernel-root" ,
243
210
layoutOptions,
@@ -247,7 +214,7 @@ function toELKNode(
247
214
}
248
215
249
216
// This function takes an EGraph and returns an ELK node that can be used to layout the graph.
250
- function toFlowNodes ( layout : MyELKNodeLayedOut ) : Node [ ] {
217
+ function toFlowNodes ( layout : MyELKNodeLayedOut ) : ( FlowClass | FlowNode ) [ ] {
251
218
return layout . children . flatMap ( ( { children, x, y, data, id : parentId , type, height, width } ) => [
252
219
{ position : { x, y } , data, id : parentId , type, height, width } ,
253
220
...children ! . map ( ( { x, y, height, width, data, id, type } ) => ( {
@@ -262,68 +229,81 @@ function toFlowNodes(layout: MyELKNodeLayedOut): Node[] {
262
229
] ) ;
263
230
}
264
231
265
- export function EClassNode ( { data } : { data : { port : string ; color : string ; id : string } } ) {
232
+ function toFlowEdges ( layout : MyELKNodeLayedOut ) : FlowEdge [ ] {
233
+ const allEdges = [ ...layout . edges ! , ...layout . children . flatMap ( ( { edges } ) => edges ! ) ] ;
234
+ return allEdges . map ( ( { id, sections, data : { sourceNode } } ) => {
235
+ const [ section ] = sections ! ;
236
+ return {
237
+ type : "edge" ,
238
+ id,
239
+ source : sourceNode ,
240
+ target : section . outgoingShape ! ,
241
+ data : {
242
+ points : [ section . startPoint , ...( section . bendPoints || [ ] ) , section . endPoint ] ,
243
+ } ,
244
+ } ;
245
+ } ) ;
246
+ }
247
+
248
+ export function EClassNode ( { data } : NodeProps < FlowClass > ) {
266
249
return (
267
- < div className = "rounded-md border border-dotted border-stone-400 h-full w-full" style = { { backgroundColor : data . color } } >
268
- < Handle className = "top-0 bottom-0 opacity-0 translate-0" type = "target" id = { data . port } position = { Position . Top } />
250
+ < div className = "rounded-md border border-dotted border-stone-400 h-full w-full" style = { { backgroundColor : data . color ! } } >
251
+ < Handle type = "target" position = { Position . Top } className = "invisible" />
269
252
</ div >
270
253
) ;
271
254
}
272
255
273
- export function ENode ( {
274
- data ,
275
- ... rest
276
- } : {
277
- data : { label : string ; ports : { id : string } [ ] ; id : string } ;
278
- outerRef ?: React . Ref < HTMLDivElement > ;
279
- innerRef ?: React . Ref < HTMLDivElement > ;
280
- } ) {
256
+ export function ENode (
257
+ props : Partial <
258
+ NodeProps < FlowNode > & {
259
+ outerRef : React . Ref < HTMLDivElement > ;
260
+ innerRef : React . Ref < HTMLDivElement > ;
261
+ }
262
+ >
263
+ ) {
281
264
return (
282
- < div className = "p-1 rounded-md border bg-white border-stone-400 h-full w-full" ref = { rest ?. outerRef } >
283
- < div className = "font-mono truncate max-w-96" ref = { rest ?. innerRef } >
284
- { data . label }
285
- </ div >
286
- { /* Place at bottom of parent so that handles just touch the node */ }
287
- < div className = "flex justify-around absolute bottom-0 w-full" >
288
- { data . ports . map ( ( { id } ) => (
289
- < Handle
290
- className = "relative left-auto transform-none opacity-0 translate-0"
291
- key = { id }
292
- type = "source"
293
- position = { Position . Bottom }
294
- id = { id }
295
- />
296
- ) ) }
265
+ < div className = "p-1 rounded-md border bg-white border-stone-400 h-full w-full" ref = { props ?. outerRef } >
266
+ < div className = "font-mono truncate max-w-96" ref = { props ?. innerRef } >
267
+ { props ?. data ?. label }
297
268
</ div >
269
+ { /* Only show handle if we aren't rendering this to calculate size */ }
270
+ { props ?. outerRef ? < > </ > : < Handle type = "source" position = { Position . Bottom } className = "invisible" /> }
298
271
</ div >
299
272
) ;
300
273
}
301
274
275
+ export function CustomEdge ( { markerEnd, data } : EdgeProps < FlowEdge > ) {
276
+ const { points } = data ! ;
277
+ const edgePath = points . map ( ( { x, y } , index ) => `${ index === 0 ? "M" : "L" } ${ x } ${ y } ` ) . join ( " " ) ;
278
+ return < BaseEdge path = { edgePath } markerEnd = { markerEnd } /> ;
279
+ }
280
+
302
281
const nodeTypes : NodeTypes = {
303
282
class : memo ( EClassNode ) ,
304
283
node : memo ( ENode ) ,
305
284
} ;
306
285
286
+ const edgeTypes : EdgeTypes = {
287
+ edge : memo ( CustomEdge ) ,
288
+ } ;
289
+
307
290
function LayoutFlow ( { egraph, outerElem, innerElem } : { egraph : string ; outerElem : HTMLDivElement ; innerElem : HTMLDivElement } ) {
308
291
const parsedEGraph : EGraph = useMemo ( ( ) => JSON . parse ( egraph ) , [ egraph ] ) ;
309
292
/// e-class ID we have currently selected
310
293
const [ selectedNode , setSelectedNode ] = useState < { type : "class" | "node" ; id : string } | null > ( null ) ;
311
- const elkNode = useMemo ( ( ) => {
312
- const r = toELKNode ( parsedEGraph , outerElem , innerElem , selectedNode ) ;
313
- // console.log(JSON.parse(JSON.stringify(r, null, 2)));
314
- return r ;
315
- } , [ parsedEGraph , outerElem , innerElem , selectedNode ] ) ;
316
- const edges = useMemo (
317
- ( ) => [ ...elkNode . children . flatMap ( ( c ) => c . edges ! . map ( ( e ) => ( { ...e } ) ) ) , ...elkNode . edges ! . map ( ( e ) => ( { ...e } ) ) ] ,
318
- [ elkNode ]
294
+ const elkNode = useMemo (
295
+ ( ) => toELKNode ( parsedEGraph , outerElem , innerElem , selectedNode ) ,
296
+ [ parsedEGraph , outerElem , innerElem , selectedNode ]
319
297
) ;
320
298
const layoutPromise = useMemo ( ( ) => elk . layout ( elkNode ) as Promise < MyELKNodeLayedOut > , [ elkNode ] ) ;
321
299
const layout = use ( layoutPromise ) ;
300
+ const edges = useMemo ( ( ) => toFlowEdges ( layout ) , [ layout ] ) ;
322
301
const nodes = useMemo ( ( ) => toFlowNodes ( layout ) , [ layout ] ) ;
323
302
const onNodeClick = useCallback (
324
303
( ( _ , node ) => {
325
- startTransition ( ( ) => setSelectedNode ( { type : node . type ! as "class" | "node" , id : node . data ! . id ! as string } ) ) ;
326
- } ) as NodeMouseHandler ,
304
+ // Use start transition so that the whole component doesn't re-render
305
+ startTransition ( ( ) => setSelectedNode ( { type : node . type ! , id : node . data . id } ) ) ;
306
+ } ) as NodeMouseHandler < FlowClass | FlowNode > ,
327
307
[ setSelectedNode ]
328
308
) ;
329
309
@@ -340,7 +320,8 @@ function LayoutFlow({ egraph, outerElem, innerElem }: { egraph: string; outerEle
340
320
< ReactFlow
341
321
nodes = { nodes }
342
322
nodeTypes = { nodeTypes }
343
- edges = { edges as unknown as Edge [ ] }
323
+ edgeTypes = { edgeTypes }
324
+ edges = { edges }
344
325
minZoom = { 0.05 }
345
326
maxZoom = { 10 }
346
327
defaultEdgeOptions = { { markerEnd : { type : MarkerType . ArrowClosed } } }
@@ -360,7 +341,7 @@ function Visualizer({ egraph }: { egraph: string }) {
360
341
< >
361
342
{ /* Hidden node to measure text size */ }
362
343
< div className = "invisible absolute" >
363
- < ENode data = { { label : "test" , ports : [ ] } } outerRef = { setOuterElem } innerRef = { setInnerElem } />
344
+ < ENode outerRef = { setOuterElem } innerRef = { setInnerElem } />
364
345
</ div >
365
346
< ReactFlowProvider >
366
347
< ErrorBoundary fallback = { < p > ⚠️Something went wrong</ p > } >
0 commit comments