-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraphFlowAdapter.js
More file actions
128 lines (120 loc) · 4.76 KB
/
graphFlowAdapter.js
File metadata and controls
128 lines (120 loc) · 4.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* Maps normalized graph payloads to @xyflow/react nodes/edges (Wave 4.3 POC).
* Layout matches circle world coords from {@link graphCanvasTransform} for parity with Canvas.
*/
import { computeWorldLayout, worldRadiusForNodeCount } from "../canvas/graphCanvasTransform.js";
import { edgeTypeCanvasLabelFromEdge, truncateCanvasLabel } from "../canvas/graphCanvasStyle.js";
/**
* @typedef {{ resolveEdgeLabel?: (edge: object) => string }} BuildReactFlowEdgesOptions
*/
const SIGNATURE_SORT_OPTS = { sensitivity: "base", numeric: true };
function compareSignatureIds(a, b) {
return String(a).localeCompare(String(b), undefined, SIGNATURE_SORT_OPTS);
}
/**
* Stable string for topology-only changes (node ids + edge ids and endpoints).
* Nodes and edges are sorted by id so the same graph yields the same key regardless of array order.
* Use to run React Flow `fitView` when the graph structure changes, not when selection changes.
*
* @param {{ nodes?: Array<{ id: string }>, edges?: Array<{ id: string, source: string, target: string }> }} graph
* @returns {string}
*/
export function getGraphLayoutSignature(graph) {
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
const nodePart = [...nodes]
.sort((a, b) => compareSignatureIds(a.id, b.id))
.map((n) => n.id)
.join("\0");
const edgePart = [...edges]
.sort((a, b) => compareSignatureIds(a.id, b.id))
.map((e) => `${e.id}:${e.source}->${e.target}`)
.join("\0");
return `${nodePart}|${edgePart}`;
}
/**
* Stable key for client-side community detection (topology + node role fields + edge types).
* Unlike {@link getGraphLayoutSignature}, edge `type` and node `type` / `workspaceMembership` affect the result.
*
* @param {{ nodes?: Array<object>, edges?: Array<object> }} graph
* @returns {string}
*/
export function getGraphCommunityDetectionSignature(graph) {
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
const topo = getGraphLayoutSignature({ nodes, edges });
const nodeMeta = [...nodes]
.map((n) => {
const id = n?.id != null ? String(n.id) : "";
const typ = n?.type != null ? String(n.type) : "";
const wsm = n?.workspaceMembership != null ? String(n.workspaceMembership).trim().toLowerCase() : "";
return `${id}:${typ}:${wsm}`;
})
.sort((a, b) => a.localeCompare(b))
.join("\0");
const edgeMeta = [...edges]
.map((e) => {
const id = e?.id != null ? String(e.id) : "";
const s = e?.source != null ? String(e.source) : "";
const t = e?.target != null ? String(e.target) : "";
const typ = e?.type != null ? String(e.type) : "";
return `${id}:${s}->${t}:${typ}`;
})
.sort((a, b) => a.localeCompare(b))
.join("\0");
return `${topo}\0${nodeMeta}\0${edgeMeta}`;
}
/**
* @param {{ nodes: Array<{ id: string, label?: string, type?: string }> }} graph
* @param {string} selectedNodeId
* @returns {import("@xyflow/react").Node[]}
*/
export function buildReactFlowNodes(graph, selectedNodeId = "") {
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
const wr = worldRadiusForNodeCount(nodes.length);
const pos = computeWorldLayout(nodes, wr);
const sel = String(selectedNodeId || "").trim();
return nodes.map((n) => {
const p = pos.get(n.id) || { x: 0, y: 0 };
const primaryLabel = n.displayLabel != null && String(n.displayLabel).trim() ? String(n.displayLabel) : String(n.label || "");
return {
id: n.id,
type: "science",
position: { x: p.x, y: p.y },
data: {
label: truncateCanvasLabel(primaryLabel),
nodeType: n.type,
},
selected: Boolean(sel && n.id === sel),
};
});
}
/**
* @param {{ edges: Array<{ id: string, source: string, target: string, type?: string, displayType?: string }> }} graph
* @param {string} selectedEdgeId
* @param {BuildReactFlowEdgesOptions} [options]
* @returns {import("@xyflow/react").Edge[]}
*/
export function buildReactFlowEdges(graph, selectedEdgeId = "", options = {}) {
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
const sel = String(selectedEdgeId || "").trim();
const resolve =
typeof options.resolveEdgeLabel === "function" ? options.resolveEdgeLabel : edgeTypeCanvasLabelFromEdge;
return edges.map((e) => ({
id: e.id,
source: e.source,
target: e.target,
sourceHandle: "out",
targetHandle: "in",
label: resolve(e),
labelStyle: { fill: "rgba(255,255,255,0.55)", fontSize: 10 },
labelBgPadding: [2, 4],
labelBgBorderRadius: 4,
labelBgStyle: { fill: "rgba(10,10,10,0.75)" },
selected: Boolean(sel && e.id === sel),
style: {
stroke: "rgba(255,255,255,0.28)",
strokeWidth: 1,
},
}));
}