Skip to content

Commit 655b062

Browse files
committed
Work on graph generalization
1 parent 336d2b6 commit 655b062

File tree

8 files changed

+133
-55
lines changed

8 files changed

+133
-55
lines changed

package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"build": "rm -rf ./dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.app.json && tailwind build -i src/index.css -o dist/index.css && cp ./src/elastic-ui.css ./dist"
3333
},
3434
"dependencies": {
35+
"@dagrejs/dagre": "^1.1.4",
3536
"@elastic/react-search-ui": "^1.22.0",
3637
"@elastic/react-search-ui-views": "^1.22.0",
3738
"@elastic/search-ui": "^1.22.0",
@@ -132,4 +133,4 @@
132133
"url": "https://orcid.org/0009-0003-2196-9187"
133134
}
134135
]
135-
}
136+
}

src/components/GlobalModalProvider.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,19 @@ export function GlobalModalProvider(props: PropsWithChildren) {
1111
const [relationGraphState, setRelationGraphState] = useState<{
1212
source: RelationNode[]
1313
target: RelationNode[]
14+
base: RelationNode
1415
isOpen: boolean
1516
}>({
1617
source: [],
1718
target: [],
19+
base: { id: "", label: "" },
1820
isOpen: false
1921
})
2022

21-
const openRelationGraph = useCallback((source: RelationNode[], target: RelationNode[]) => {
23+
const openRelationGraph = useCallback((source: RelationNode[], base: RelationNode, target: RelationNode[]) => {
2224
setRelationGraphState({
2325
source,
26+
base,
2427
target,
2528
isOpen: true
2629
})
@@ -36,8 +39,9 @@ export function GlobalModalProvider(props: PropsWithChildren) {
3639
<RelationsGraphModal
3740
isOpen={relationGraphState.isOpen}
3841
onOpenChange={onRelationGraphOpenChange}
39-
source={relationGraphState.source}
40-
target={relationGraphState.target}
42+
referencedBy={relationGraphState.source}
43+
references={relationGraphState.target}
44+
base={relationGraphState.base}
4145
/>
4246

4347
{props.children}

src/components/RFS_GlobalModalContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { RelationNode } from "@/lib/RelationNode"
22
import { createContext } from "react"
33

44
export const RFS_GlobalModalContext = createContext<{
5-
openRelationGraph: (source: RelationNode[], target: RelationNode[]) => void
5+
openRelationGraph: (source: RelationNode[], base: RelationNode, target: RelationNode[]) => void
66
}>({
77
openRelationGraph: (): void => {
88
throw "GlobalModalProvider not mounted"

src/components/graph/RelationsGraph.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { RelationNode } from "@/lib/RelationNode"
22

3-
import { buildGraphForReferences } from "@/components/graph/helpers"
3+
import { buildGraphForReferences, getLayoutedElements } from "@/components/graph/helpers"
44
import { PlainNode } from "@/components/graph/PlainNode"
55
import { Background, BackgroundVariant, ReactFlow, useEdgesState, useNodesInitialized, useNodesState, useReactFlow } from "@xyflow/react"
6-
import { useEffect, useMemo } from "react"
6+
import { useCallback, useEffect, useMemo, useRef } from "react"
77
import "@xyflow/react/dist/style.css"
88

99
const nodeTypes = {
@@ -12,33 +12,38 @@ const nodeTypes = {
1212

1313
/**
1414
* Renders an interactive graph for the specified RelationNodes.
15-
* @param props
16-
* @constructor
1715
*/
18-
export function RelationsGraph(props: {
19-
/**
20-
* Source of the relation
21-
*/
22-
source: RelationNode[]
23-
/**
24-
* Targets of the relation. Will be connected to the base (source) only
25-
*/
26-
target: RelationNode[]
27-
}) {
16+
export function RelationsGraph(props: { base: RelationNode; referencedBy: RelationNode[]; references: RelationNode[] }) {
2817
const { initialEdges, initialNodes } = useMemo(() => {
29-
return buildGraphForReferences(props.source, props.target)
30-
}, [props.source, props.target])
18+
return buildGraphForReferences(props.base, props.referencedBy, props.references)
19+
}, [props.base, props.referencedBy, props.references])
3120

32-
const [nodes, , onNodesChange] = useNodesState(initialNodes)
33-
const [edges, , onEdgesChange] = useEdgesState(initialEdges)
21+
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
22+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
3423
const { fitView } = useReactFlow()
3524
const nodesInitialized = useNodesInitialized()
3625

26+
const onLayout = useCallback(() => {
27+
const layouted = getLayoutedElements(nodes, edges)
28+
29+
setNodes([...layouted.nodes])
30+
setEdges([...layouted.edges])
31+
32+
window.requestAnimationFrame(() => {
33+
fitView()
34+
})
35+
}, [nodes, edges, setNodes, setEdges, fitView])
36+
37+
const onLayoutDebounced = useRef(onLayout)
38+
useEffect(() => {
39+
onLayoutDebounced.current = onLayout
40+
}, [onLayout])
41+
3742
useEffect(() => {
3843
if (nodesInitialized) {
39-
fitView().then()
44+
onLayoutDebounced.current()
4045
}
41-
}, [fitView, nodesInitialized])
46+
}, [nodesInitialized])
4247

4348
return (
4449
<ReactFlow

src/components/graph/RelationsGraphModal.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { useCallback, useContext } from "react"
88
export function RelationsGraphModal({
99
isOpen,
1010
onOpenChange,
11-
source,
12-
target
11+
referencedBy,
12+
references,
13+
base
1314
}: {
1415
isOpen: boolean
1516
onOpenChange: (val: boolean) => void
16-
source: RelationNode[]
17-
target: RelationNode[]
17+
referencedBy: RelationNode[]
18+
references: RelationNode[]
19+
base: RelationNode
1820
}) {
1921
const searchContext = useContext(FairDOSearchContext)
2022

@@ -31,7 +33,7 @@ export function RelationsGraphModal({
3133
<DialogContent className="rfs-h-max rfs-max-h-[min(100vh,800px)] rfs-min-h-[500px] rfs-min-w-[500px] !rfs-max-w-[min(calc(100vw-40px),1500px)] rfs-p-0">
3234
<VisuallyHidden.Root>
3335
<DialogTitle>
34-
Relationship graph between {source.map((s) => s.label).join(", ")} and {target.map((s) => s.label).join(", ")}
36+
Relationship graph between {referencedBy.map((s) => s.label).join(", ")} and {references.map((s) => s.label).join(", ")}
3537
</DialogTitle>
3638
</VisuallyHidden.Root>
3739

@@ -42,7 +44,7 @@ export function RelationsGraphModal({
4244
searchForBackground: searchContext.searchForBackground
4345
}}
4446
>
45-
<RelationsGraph source={source} target={target} />
47+
<RelationsGraph referencedBy={referencedBy} references={references} base={base} />
4648
</FairDOSearchContext.Provider>
4749
</DialogContent>
4850
</Dialog>

src/components/graph/helpers.ts

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,81 @@
11
import type { RelationNode } from "@/lib/RelationNode"
2+
import { Edge, Node } from "@xyflow/react"
3+
import Dagre from "@dagrejs/dagre"
24

3-
export function buildGraphForReferences(source: RelationNode[], _referenced: RelationNode[]) {
4-
const referenced = _referenced.filter((pid) => !source.find((e) => e.id === pid.id))
5-
const yStartSource = -((source.length - 1) * 100) / 2
6-
const yStartReferenced = -((referenced.length - 1) * 100) / 2
7-
const nodes: { id: string; type: string; position: { x: number; y: number }; data: Record<string, unknown> }[] = []
5+
export function buildGraphForReferences(base: RelationNode, parents: RelationNode[], _children: RelationNode[]) {
6+
const children = _children.filter((pid) => !parents.find((e) => e.id === pid.id))
7+
const yStartParents = -((parents.length - 1) * 100) / 2
8+
const yStartChildren = -((children.length - 1) * 100) / 2
9+
const nodes: { id: string; type: string; position: { x: number; y: number }; data: Record<string, unknown> }[] = [
10+
{
11+
id: base.id,
12+
type: "plain",
13+
position: { x: 0, y: 0 },
14+
data: { ...base }
15+
}
16+
]
817
const edges: { id: string; source: string; target: string }[] = []
918

10-
for (let i = 0; i < source.length; i++) {
19+
for (let i = 0; i < parents.length; i++) {
1120
nodes.push({
12-
id: source[i].id,
21+
id: parents[i].id,
1322
type: "plain",
14-
position: { x: 0, y: yStartSource + i * 100 },
15-
data: { ...source[i] }
23+
position: { x: -1000, y: yStartParents + i * 100 },
24+
data: { ...parents[i] }
25+
})
26+
27+
edges.push({
28+
id: `e-${parents[i].id}-base`,
29+
source: parents[i].id,
30+
target: base.id
1631
})
1732
}
1833

19-
for (let i = 0; i < referenced.length; i++) {
34+
for (let i = 0; i < children.length; i++) {
2035
nodes.push({
21-
id: referenced[i].id,
36+
id: children[i].id,
2237
type: "plain",
23-
position: { x: 1000, y: yStartReferenced + i * 100 },
24-
data: { ...referenced[i] }
38+
position: { x: 1000, y: yStartChildren + i * 100 },
39+
data: { ...children[i] }
2540
})
26-
}
2741

28-
for (let x = 0; x < source.length; x++) {
29-
for (let y = 0; y < referenced.length; y++) {
30-
edges.push({
31-
id: `e-${source[x].id}-${referenced[y].id}`,
32-
source: source[x].id,
33-
target: referenced[y].id
34-
})
35-
}
42+
edges.push({
43+
id: `e-base-${children[i].id}`,
44+
source: base.id,
45+
target: children[i].id
46+
})
3647
}
3748

49+
console.log(nodes, edges)
50+
3851
return { initialNodes: nodes, initialEdges: edges }
3952
}
53+
54+
export function getLayoutedElements(nodes: (Node & { type: string })[], edges: Edge[]) {
55+
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}))
56+
g.setGraph({ rankdir: "LR", nodesep: 25, ranksep: 100 })
57+
58+
edges.forEach((edge) => g.setEdge(edge.source, edge.target))
59+
nodes.forEach((node) =>
60+
g.setNode(node.id, {
61+
...node,
62+
width: node.measured?.width ?? 0,
63+
height: node.measured?.height ?? 0
64+
})
65+
)
66+
67+
Dagre.layout(g)
68+
69+
return {
70+
nodes: nodes.map((node) => {
71+
const position = g.node(node.id)
72+
// We are shifting the dagre node position (anchor=center center) to the top left
73+
// so it matches the React Flow node anchor point (top left).
74+
const x = position.x - (node.measured?.width ?? 0) / 2
75+
const y = position.y - (node.measured?.height ?? 0) / 2
76+
77+
return { ...node, position: { x, y } }
78+
}),
79+
edges
80+
}
81+
}

src/stories/RelationsGraph.stories.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ export const Default: Story = {
2424
)
2525
],
2626
args: {
27-
base: new BasicRelationNode("T10/436895408650943", "Source"),
28-
referenced: [
27+
referencedBy: [
28+
new BasicRelationNode("T10/parentA", "Parent"),
29+
new BasicRelationNode("T10/parentB", "Parent"),
30+
new BasicRelationNode("T10/parentC", "Source")
31+
],
32+
base: new BasicRelationNode("T10/436895408650943, abcde", "Source"),
33+
references: [
2934
new BasicRelationNode("T10/436895408650941", "Dataset", "Something else"),
3035
new BasicRelationNode("T10/436895408650942", "Dataset"),
3136
new BasicRelationNode("T10/436895408650944", "Dataset"),

0 commit comments

Comments
 (0)