@petrarca/sonnet-graph 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +190 -0
- package/dist/index.d.ts +858 -0
- package/dist/index.js +4074 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4074 @@
|
|
|
1
|
+
// src/models/visual-graph.ts
|
|
2
|
+
function isVisualNode(obj) {
|
|
3
|
+
return typeof obj === "object" && obj !== null && "id_" in obj && "label_" in obj && "properties_" in obj && "visual" in obj && typeof obj.visual === "object" && "displayName" in obj.visual && "color" in obj.visual;
|
|
4
|
+
}
|
|
5
|
+
function isVisualEdge(obj) {
|
|
6
|
+
return typeof obj === "object" && obj !== null && "id_" in obj && "label_" in obj && "start_id_" in obj && "end_id_" in obj && "visual" in obj && typeof obj.visual === "object" && "color" in obj.visual;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/models/graph-types.ts
|
|
10
|
+
function isRawGraphNode(obj) {
|
|
11
|
+
return "label_" in obj && !("start_id_" in obj) && !("end_id_" in obj);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/graph/extractDisplayLabel.ts
|
|
15
|
+
function interpolateTemplate(template, properties) {
|
|
16
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
17
|
+
const val = properties[key];
|
|
18
|
+
return val !== void 0 && val !== null ? String(val) : "";
|
|
19
|
+
}).replace(/\s+/g, " ").trim();
|
|
20
|
+
}
|
|
21
|
+
function resolveLabel(strategies, fallback) {
|
|
22
|
+
for (const strategy of strategies) {
|
|
23
|
+
const result = strategy();
|
|
24
|
+
if (result) return result;
|
|
25
|
+
}
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
function fromTemplate(displayConfig, props) {
|
|
29
|
+
if (!displayConfig?.labelTemplate || !props) return void 0;
|
|
30
|
+
return interpolateTemplate(displayConfig.labelTemplate, props) || void 0;
|
|
31
|
+
}
|
|
32
|
+
function fromLabelProperty(displayConfig, props) {
|
|
33
|
+
if (!displayConfig?.labelProperty || !props?.[displayConfig.labelProperty])
|
|
34
|
+
return void 0;
|
|
35
|
+
return String(props[displayConfig.labelProperty]);
|
|
36
|
+
}
|
|
37
|
+
function fromFallbackProperty(displayConfig, props) {
|
|
38
|
+
if (!displayConfig?.fallbackProperty || !props?.[displayConfig.fallbackProperty])
|
|
39
|
+
return void 0;
|
|
40
|
+
return String(props[displayConfig.fallbackProperty]);
|
|
41
|
+
}
|
|
42
|
+
function extractNodeDisplayLabel(node, displayConfig) {
|
|
43
|
+
const props = node?.properties_;
|
|
44
|
+
return resolveLabel(
|
|
45
|
+
[
|
|
46
|
+
() => fromTemplate(displayConfig, props),
|
|
47
|
+
() => fromLabelProperty(displayConfig, props),
|
|
48
|
+
() => fromFallbackProperty(displayConfig, props),
|
|
49
|
+
() => props?.name ? String(props.name) : void 0,
|
|
50
|
+
() => {
|
|
51
|
+
const keys = props ? Object.keys(props) : [];
|
|
52
|
+
return keys.length > 0 ? String(props[keys[0]]) : void 0;
|
|
53
|
+
},
|
|
54
|
+
() => node?.id_ !== void 0 && node.id_ !== null ? `Node #${node.id_}` : void 0
|
|
55
|
+
],
|
|
56
|
+
"Unknown"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
function extractEdgeDisplayLabel(edge, displayConfig) {
|
|
60
|
+
const props = edge?.properties_;
|
|
61
|
+
return resolveLabel(
|
|
62
|
+
[
|
|
63
|
+
() => fromTemplate(displayConfig, props),
|
|
64
|
+
() => fromLabelProperty(displayConfig, props),
|
|
65
|
+
() => fromFallbackProperty(displayConfig, props),
|
|
66
|
+
() => edge?.label_ ? edge.label_ : void 0,
|
|
67
|
+
() => edge?.id_ !== void 0 && edge.id_ !== null ? `Edge #${edge.id_}` : void 0
|
|
68
|
+
],
|
|
69
|
+
"Unknown"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
function extractDisplayLabel(item, displayConfig) {
|
|
73
|
+
if (isRawGraphNode(item)) {
|
|
74
|
+
return extractNodeDisplayLabel(item, displayConfig);
|
|
75
|
+
} else {
|
|
76
|
+
return extractEdgeDisplayLabel(item, displayConfig);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/graph/enrichment.ts
|
|
81
|
+
var BASE_COLORS = [
|
|
82
|
+
"#f94144",
|
|
83
|
+
"#118ab2",
|
|
84
|
+
"#f8961e",
|
|
85
|
+
"#43aa8b",
|
|
86
|
+
"#f3722c",
|
|
87
|
+
"#90be6d",
|
|
88
|
+
"#f9c74f",
|
|
89
|
+
"#c4bbaf"
|
|
90
|
+
];
|
|
91
|
+
function buildColorPalette(labelCounts) {
|
|
92
|
+
const palette = {};
|
|
93
|
+
let i = 0;
|
|
94
|
+
for (const label of Object.keys(labelCounts)) {
|
|
95
|
+
palette[label] = BASE_COLORS[i % BASE_COLORS.length];
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
palette["neutral"] = "#6c757d";
|
|
99
|
+
palette["faded"] = "#adb5bd";
|
|
100
|
+
return palette;
|
|
101
|
+
}
|
|
102
|
+
function buildColorPaletteFromNodes(nodes) {
|
|
103
|
+
const labelCounts = {};
|
|
104
|
+
for (const n of nodes) {
|
|
105
|
+
const l = n?.label_;
|
|
106
|
+
if (l) labelCounts[l] = (labelCounts[l] || 0) + 1;
|
|
107
|
+
}
|
|
108
|
+
return buildColorPalette(labelCounts);
|
|
109
|
+
}
|
|
110
|
+
function enrichNode(apiNode, colorPalette, displayConfig) {
|
|
111
|
+
const displayName = extractDisplayLabel(apiNode, displayConfig);
|
|
112
|
+
const color = colorPalette[apiNode.label_];
|
|
113
|
+
return {
|
|
114
|
+
// Copy core data from API
|
|
115
|
+
id_: apiNode.id_ ?? 0,
|
|
116
|
+
// Handle optional id for creation scenarios
|
|
117
|
+
label_: apiNode.label_,
|
|
118
|
+
properties_: apiNode.properties_ ?? {},
|
|
119
|
+
// Add visual enrichment
|
|
120
|
+
visual: {
|
|
121
|
+
displayName,
|
|
122
|
+
// Use color from palette, or fall back to neutral if not found
|
|
123
|
+
// This ensures every node has a color (no undefined)
|
|
124
|
+
color: color || colorPalette["neutral"] || "#6c757d",
|
|
125
|
+
// Size can be added later if needed
|
|
126
|
+
size: void 0,
|
|
127
|
+
icon: void 0
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function enrichEdge(apiEdge, colorPalette) {
|
|
132
|
+
return {
|
|
133
|
+
// Copy core data from API
|
|
134
|
+
id_: apiEdge.id_ ?? 0,
|
|
135
|
+
label_: apiEdge.label_,
|
|
136
|
+
start_id_: apiEdge.start_id_,
|
|
137
|
+
end_id_: apiEdge.end_id_,
|
|
138
|
+
properties_: apiEdge.properties_,
|
|
139
|
+
// Add visual enrichment
|
|
140
|
+
visual: {
|
|
141
|
+
// Use neutral color from palette if available, otherwise default
|
|
142
|
+
color: colorPalette?.["neutral"] ?? "#999999",
|
|
143
|
+
// Default size of 3
|
|
144
|
+
size: 3,
|
|
145
|
+
dashed: void 0
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function enrichNodes(apiNodes, colorPalette, labelDisplayMap) {
|
|
150
|
+
return apiNodes.map(
|
|
151
|
+
(node) => enrichNode(node, colorPalette, labelDisplayMap?.[node.label_])
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
function enrichEdges(apiEdges, colorPalette) {
|
|
155
|
+
return apiEdges.map((edge) => enrichEdge(edge, colorPalette));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/graph/normalize.ts
|
|
159
|
+
function normalizeGraph(nodes, edges) {
|
|
160
|
+
const nodesById = {};
|
|
161
|
+
const edgesById = {};
|
|
162
|
+
for (const n of nodes) {
|
|
163
|
+
const key = String(n.id_);
|
|
164
|
+
nodesById[key] = n;
|
|
165
|
+
}
|
|
166
|
+
for (const e of edges) {
|
|
167
|
+
const key = String(e.id_);
|
|
168
|
+
edgesById[key] = e;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
nodesById,
|
|
172
|
+
edgesById,
|
|
173
|
+
nodeIds: Object.keys(nodesById),
|
|
174
|
+
edgeIds: Object.keys(edgesById)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
var EMPTY_NORMALIZED = {
|
|
178
|
+
nodesById: {},
|
|
179
|
+
edgesById: {},
|
|
180
|
+
nodeIds: [],
|
|
181
|
+
edgeIds: []
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/stores/Selection.ts
|
|
185
|
+
import { create } from "zustand";
|
|
186
|
+
import { devLog } from "@petrarca/sonnet-core";
|
|
187
|
+
function createSelectionStore() {
|
|
188
|
+
return create()((set, get) => ({
|
|
189
|
+
selectedNodeIds: [],
|
|
190
|
+
setSelectedNodeIds: (ids) => {
|
|
191
|
+
const prev = get().selectedNodeIds;
|
|
192
|
+
if (prev.length === ids.length && prev.every((v, i) => v === ids[i])) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
devLog("[SelectionStore] setSelectedNodeIds", { prev, next: ids });
|
|
196
|
+
set({ selectedNodeIds: ids });
|
|
197
|
+
},
|
|
198
|
+
selectedEdge: null,
|
|
199
|
+
setSelectedEdge: (edge) => {
|
|
200
|
+
devLog("[SelectionStore] setSelectedEdge", {
|
|
201
|
+
prev: get().selectedEdge?.id_,
|
|
202
|
+
next: edge?.id_
|
|
203
|
+
});
|
|
204
|
+
set({ selectedEdge: edge });
|
|
205
|
+
},
|
|
206
|
+
clear: () => {
|
|
207
|
+
devLog("[SelectionStore] clear called");
|
|
208
|
+
set({ selectedNodeIds: [], selectedEdge: null });
|
|
209
|
+
}
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
var useSelection = createSelectionStore();
|
|
213
|
+
|
|
214
|
+
// src/stores/GraphData.ts
|
|
215
|
+
import { create as create2 } from "zustand";
|
|
216
|
+
import { devLog as devLog2 } from "@petrarca/sonnet-core";
|
|
217
|
+
function createGraphDataStore() {
|
|
218
|
+
return create2()((set, get) => ({
|
|
219
|
+
nodes: [],
|
|
220
|
+
edges: [],
|
|
221
|
+
stats: void 0,
|
|
222
|
+
values: [],
|
|
223
|
+
isValueSet: false,
|
|
224
|
+
queryNodeIds: /* @__PURE__ */ new Set(),
|
|
225
|
+
expandedNodeIds: /* @__PURE__ */ new Set(),
|
|
226
|
+
queryEdgeIds: /* @__PURE__ */ new Set(),
|
|
227
|
+
expandedEdgeIds: /* @__PURE__ */ new Set(),
|
|
228
|
+
setQueryData: (nodes, edges, stats, values, isValueSet = false) => {
|
|
229
|
+
const nodeIds = new Set(nodes.map((n) => String(n.id_)));
|
|
230
|
+
const edgeIds = new Set(edges.map((e) => String(e.id_)));
|
|
231
|
+
devLog2("[GraphData] setQueryData", {
|
|
232
|
+
nodes: nodes.length,
|
|
233
|
+
edges: edges.length
|
|
234
|
+
});
|
|
235
|
+
set({
|
|
236
|
+
nodes,
|
|
237
|
+
edges,
|
|
238
|
+
stats,
|
|
239
|
+
values: values || [],
|
|
240
|
+
isValueSet,
|
|
241
|
+
queryNodeIds: nodeIds,
|
|
242
|
+
queryEdgeIds: edgeIds,
|
|
243
|
+
expandedNodeIds: /* @__PURE__ */ new Set(),
|
|
244
|
+
expandedEdgeIds: /* @__PURE__ */ new Set()
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
mergeExpandedData: (newNodes, newEdges) => {
|
|
248
|
+
const state = get();
|
|
249
|
+
const existingNodesMap = new Map(
|
|
250
|
+
state.nodes.map((n) => [String(n.id_), n])
|
|
251
|
+
);
|
|
252
|
+
const existingEdgesMap = new Map(
|
|
253
|
+
state.edges.map((e) => [String(e.id_), e])
|
|
254
|
+
);
|
|
255
|
+
const newExpandedNodeIds = new Set(state.expandedNodeIds);
|
|
256
|
+
const newExpandedEdgeIds = new Set(state.expandedEdgeIds);
|
|
257
|
+
const nodesToAdd = [];
|
|
258
|
+
for (const node of newNodes) {
|
|
259
|
+
const id = String(node.id_);
|
|
260
|
+
if (!existingNodesMap.has(id)) {
|
|
261
|
+
nodesToAdd.push(node);
|
|
262
|
+
newExpandedNodeIds.add(id);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const edgesToAdd = [];
|
|
266
|
+
for (const edge of newEdges) {
|
|
267
|
+
const id = String(edge.id_);
|
|
268
|
+
if (!existingEdgesMap.has(id)) {
|
|
269
|
+
edgesToAdd.push(edge);
|
|
270
|
+
newExpandedEdgeIds.add(id);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
devLog2("[GraphData] mergeExpandedData", {
|
|
274
|
+
newNodes: newNodes.length,
|
|
275
|
+
newEdges: newEdges.length,
|
|
276
|
+
addedNodes: nodesToAdd.length,
|
|
277
|
+
addedEdges: edgesToAdd.length,
|
|
278
|
+
duplicatesSkipped: {
|
|
279
|
+
nodes: newNodes.length - nodesToAdd.length,
|
|
280
|
+
edges: newEdges.length - edgesToAdd.length
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
if (nodesToAdd.length > 0 || edgesToAdd.length > 0) {
|
|
284
|
+
set({
|
|
285
|
+
nodes: [...state.nodes, ...nodesToAdd],
|
|
286
|
+
edges: [...state.edges, ...edgesToAdd],
|
|
287
|
+
expandedNodeIds: newExpandedNodeIds,
|
|
288
|
+
expandedEdgeIds: newExpandedEdgeIds
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
devLog2(
|
|
292
|
+
"[GraphData] mergeExpandedData: No new elements to add (all duplicates)"
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
clear: () => {
|
|
297
|
+
devLog2("[GraphData] clear");
|
|
298
|
+
set({
|
|
299
|
+
nodes: [],
|
|
300
|
+
edges: [],
|
|
301
|
+
stats: void 0,
|
|
302
|
+
values: [],
|
|
303
|
+
isValueSet: false,
|
|
304
|
+
queryNodeIds: /* @__PURE__ */ new Set(),
|
|
305
|
+
expandedNodeIds: /* @__PURE__ */ new Set(),
|
|
306
|
+
queryEdgeIds: /* @__PURE__ */ new Set(),
|
|
307
|
+
expandedEdgeIds: /* @__PURE__ */ new Set()
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
getStats: () => {
|
|
311
|
+
const state = get();
|
|
312
|
+
return {
|
|
313
|
+
totalNodes: state.nodes.length,
|
|
314
|
+
totalEdges: state.edges.length,
|
|
315
|
+
queryNodes: state.queryNodeIds.size,
|
|
316
|
+
expandedNodes: state.expandedNodeIds.size,
|
|
317
|
+
queryEdges: state.queryEdgeIds.size,
|
|
318
|
+
expandedEdges: state.expandedEdgeIds.size
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
var useGraphData = createGraphDataStore();
|
|
324
|
+
|
|
325
|
+
// src/stores/GraphFilter.ts
|
|
326
|
+
import { create as create3 } from "zustand";
|
|
327
|
+
function calculateNodeLabelUpdate(affectedLabels, currentNodeFilters, allNodesDisabled) {
|
|
328
|
+
if (affectedLabels.length === 0) return null;
|
|
329
|
+
if (allNodesDisabled) {
|
|
330
|
+
return { allLabelsDisabled: false, nodeLabelFilters: affectedLabels };
|
|
331
|
+
}
|
|
332
|
+
if (currentNodeFilters.length > 0) {
|
|
333
|
+
const labelsToAdd = affectedLabels.filter(
|
|
334
|
+
(l) => !currentNodeFilters.includes(l)
|
|
335
|
+
);
|
|
336
|
+
if (labelsToAdd.length > 0) {
|
|
337
|
+
return { nodeLabelFilters: [...currentNodeFilters, ...labelsToAdd] };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
function handleEnableFromAllDisabled(label, getNodeLabelUpdate, set) {
|
|
343
|
+
const nodeUpdate = getNodeLabelUpdate(label);
|
|
344
|
+
set({
|
|
345
|
+
allEdgeLabelsDisabled: false,
|
|
346
|
+
edgeLabelFilters: [label],
|
|
347
|
+
...nodeUpdate
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function handleFirstFilter(label, allAvailableLabels, set) {
|
|
351
|
+
const next = allAvailableLabels.filter((l) => l !== label);
|
|
352
|
+
set({ edgeLabelFilters: next });
|
|
353
|
+
}
|
|
354
|
+
function handleToggleFilter(label, current, exists, getNodeLabelUpdate, set) {
|
|
355
|
+
const next = exists ? current.filter((l) => l !== label) : [...current, label];
|
|
356
|
+
if (next.length === 0 && exists) {
|
|
357
|
+
set({ allEdgeLabelsDisabled: true, edgeLabelFilters: [] });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const nodeUpdate = exists ? null : getNodeLabelUpdate(label);
|
|
361
|
+
set({ edgeLabelFilters: next, ...nodeUpdate });
|
|
362
|
+
}
|
|
363
|
+
function createGraphFilterStore() {
|
|
364
|
+
return create3()((set, get) => ({
|
|
365
|
+
nodeLabelFilters: [],
|
|
366
|
+
allLabelsDisabled: false,
|
|
367
|
+
edgeLabelFilters: [],
|
|
368
|
+
allEdgeLabelsDisabled: false,
|
|
369
|
+
lastDataChangeTime: 0,
|
|
370
|
+
notifyDataChange: () => set({ lastDataChangeTime: Date.now() }),
|
|
371
|
+
resetFilters: () => set({
|
|
372
|
+
nodeLabelFilters: [],
|
|
373
|
+
allLabelsDisabled: false,
|
|
374
|
+
edgeLabelFilters: [],
|
|
375
|
+
allEdgeLabelsDisabled: false
|
|
376
|
+
}),
|
|
377
|
+
toggleNodeLabelFilter: (label, allAvailableLabels) => {
|
|
378
|
+
const wasAllDisabled = get().allLabelsDisabled;
|
|
379
|
+
if (wasAllDisabled) set({ allLabelsDisabled: false });
|
|
380
|
+
const current = get().nodeLabelFilters;
|
|
381
|
+
const exists = current.includes(label);
|
|
382
|
+
if (wasAllDisabled) {
|
|
383
|
+
set({ nodeLabelFilters: [label] });
|
|
384
|
+
} else if (current.length === 0 && !exists && allAvailableLabels && allAvailableLabels.length > 0) {
|
|
385
|
+
const next = allAvailableLabels.filter((l) => l !== label);
|
|
386
|
+
set({ nodeLabelFilters: next });
|
|
387
|
+
} else {
|
|
388
|
+
const next = exists ? current.filter((l) => l !== label) : [...current, label];
|
|
389
|
+
if (next.length === 0 && exists) {
|
|
390
|
+
set({ allLabelsDisabled: true, nodeLabelFilters: [] });
|
|
391
|
+
} else {
|
|
392
|
+
set({ nodeLabelFilters: next });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
clearNodeLabelFilters: () => set({ nodeLabelFilters: [] }),
|
|
397
|
+
setNodeLabelFilters: (labels) => set({ nodeLabelFilters: labels }),
|
|
398
|
+
toggleAllLabelsDisabled: () => {
|
|
399
|
+
const now = !get().allLabelsDisabled;
|
|
400
|
+
set({ allLabelsDisabled: now, nodeLabelFilters: [] });
|
|
401
|
+
},
|
|
402
|
+
toggleEdgeLabelFilter: (label, allAvailableLabels, edgeToNodeLabels) => {
|
|
403
|
+
const getNodeLabelUpdate = (edgeLabel) => {
|
|
404
|
+
if (!edgeToNodeLabels) return null;
|
|
405
|
+
const affectedLabels = edgeToNodeLabels[edgeLabel] || [];
|
|
406
|
+
return calculateNodeLabelUpdate(
|
|
407
|
+
affectedLabels,
|
|
408
|
+
get().nodeLabelFilters,
|
|
409
|
+
get().allLabelsDisabled
|
|
410
|
+
);
|
|
411
|
+
};
|
|
412
|
+
if (get().allEdgeLabelsDisabled) {
|
|
413
|
+
handleEnableFromAllDisabled(label, getNodeLabelUpdate, set);
|
|
414
|
+
} else if (get().edgeLabelFilters.length === 0 && allAvailableLabels && allAvailableLabels.length > 0) {
|
|
415
|
+
handleFirstFilter(label, allAvailableLabels, set);
|
|
416
|
+
} else {
|
|
417
|
+
const current = get().edgeLabelFilters;
|
|
418
|
+
handleToggleFilter(
|
|
419
|
+
label,
|
|
420
|
+
current,
|
|
421
|
+
current.includes(label),
|
|
422
|
+
getNodeLabelUpdate,
|
|
423
|
+
set
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
clearEdgeLabelFilters: () => set({ edgeLabelFilters: [] }),
|
|
428
|
+
setEdgeLabelFilters: (labels) => set({ edgeLabelFilters: labels }),
|
|
429
|
+
toggleAllEdgeLabelsDisabled: () => {
|
|
430
|
+
const now = !get().allEdgeLabelsDisabled;
|
|
431
|
+
set({ allEdgeLabelsDisabled: now, edgeLabelFilters: [] });
|
|
432
|
+
}
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
var useGraphFilter = createGraphFilterStore();
|
|
436
|
+
|
|
437
|
+
// src/stores/GraphExploration.ts
|
|
438
|
+
import { create as create4 } from "zustand";
|
|
439
|
+
import { devLog as devLog3 } from "@petrarca/sonnet-core";
|
|
440
|
+
function collectReachable(startId, direction, allEdges, includeStart) {
|
|
441
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
442
|
+
const edges = /* @__PURE__ */ new Set();
|
|
443
|
+
const visited = /* @__PURE__ */ new Set();
|
|
444
|
+
const traverse = (currentId, isStart) => {
|
|
445
|
+
if (visited.has(currentId)) return;
|
|
446
|
+
visited.add(currentId);
|
|
447
|
+
if (!isStart || includeStart) nodes.add(currentId);
|
|
448
|
+
for (const edge of allEdges) {
|
|
449
|
+
const srcId = String(edge.start_id_);
|
|
450
|
+
const tgtId = String(edge.end_id_);
|
|
451
|
+
const edgeId = String(edge.id_);
|
|
452
|
+
if (direction === "in" && tgtId === currentId) {
|
|
453
|
+
edges.add(edgeId);
|
|
454
|
+
traverse(srcId, false);
|
|
455
|
+
} else if (direction === "out" && srcId === currentId) {
|
|
456
|
+
edges.add(edgeId);
|
|
457
|
+
traverse(tgtId, false);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
traverse(startId, true);
|
|
462
|
+
return { nodes, edges };
|
|
463
|
+
}
|
|
464
|
+
function determineEdgeDirection(endpoints, selectedNodeIds) {
|
|
465
|
+
const { startNodeId, endNodeId } = endpoints;
|
|
466
|
+
if (selectedNodeIds.length !== 1) {
|
|
467
|
+
return { direction: "outgoing", referenceNodeId: startNodeId };
|
|
468
|
+
}
|
|
469
|
+
const selected = selectedNodeIds[0];
|
|
470
|
+
if (selected === startNodeId) {
|
|
471
|
+
return { direction: "outgoing", referenceNodeId: selected };
|
|
472
|
+
}
|
|
473
|
+
if (selected === endNodeId) {
|
|
474
|
+
return { direction: "incoming", referenceNodeId: selected };
|
|
475
|
+
}
|
|
476
|
+
devLog3(
|
|
477
|
+
"[GraphExploration] focusEdge - selected node not an endpoint, defaulting to outgoing",
|
|
478
|
+
{ selectedNode: selected, startNodeId, endNodeId }
|
|
479
|
+
);
|
|
480
|
+
return { direction: "outgoing", referenceNodeId: startNodeId };
|
|
481
|
+
}
|
|
482
|
+
function buildFocusSet(referenceNodeId, direction, edgeLabel, allEdges) {
|
|
483
|
+
const focusNodeIds = /* @__PURE__ */ new Set([referenceNodeId]);
|
|
484
|
+
for (const e of allEdges) {
|
|
485
|
+
if (e.label_ !== edgeLabel) continue;
|
|
486
|
+
const sourceId = String(e.start_id_);
|
|
487
|
+
const targetId = String(e.end_id_);
|
|
488
|
+
if (direction === "outgoing" && sourceId === referenceNodeId) {
|
|
489
|
+
focusNodeIds.add(targetId);
|
|
490
|
+
} else if (direction === "incoming" && targetId === referenceNodeId) {
|
|
491
|
+
focusNodeIds.add(sourceId);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return focusNodeIds;
|
|
495
|
+
}
|
|
496
|
+
function handleExistingNode(nodeId, nodeIdStr, existingNodes, autoFocus, graphFocusStore) {
|
|
497
|
+
const existingNode = existingNodes.find((n) => n.id_ === nodeId);
|
|
498
|
+
if (!existingNode) return false;
|
|
499
|
+
if (autoFocus) {
|
|
500
|
+
devLog3("[GraphExploration] Node already in graph, focusing", { nodeId });
|
|
501
|
+
graphFocusStore.getState().requestFocus(nodeIdStr, "node", false);
|
|
502
|
+
} else {
|
|
503
|
+
devLog3("[GraphExploration] Node already in graph, skipping focus", {
|
|
504
|
+
nodeId
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
async function fetchAndEnrichNode(nodeId, existingNodes, stores) {
|
|
510
|
+
devLog3("[GraphExploration] Fetching node from backend", { nodeId });
|
|
511
|
+
const result = await stores.getNodes([nodeId]);
|
|
512
|
+
if (result.length === 0) {
|
|
513
|
+
throw new Error(`Node ${nodeId} not found`);
|
|
514
|
+
}
|
|
515
|
+
const colorPalette = buildColorPaletteFromNodes([
|
|
516
|
+
...existingNodes,
|
|
517
|
+
...result
|
|
518
|
+
]);
|
|
519
|
+
const enrichedNodes = enrichNodes(
|
|
520
|
+
result,
|
|
521
|
+
colorPalette,
|
|
522
|
+
stores.getLabelDisplayMap?.()
|
|
523
|
+
);
|
|
524
|
+
devLog3("[GraphExploration] Enriched nodes", {
|
|
525
|
+
nodeId,
|
|
526
|
+
enrichedCount: enrichedNodes.length
|
|
527
|
+
});
|
|
528
|
+
stores.graphData.getState().mergeExpandedData(enrichedNodes, []);
|
|
529
|
+
return enrichedNodes;
|
|
530
|
+
}
|
|
531
|
+
function ensureNodeLabelInFilters(enrichedNodes, nodeId, graphFilterStore) {
|
|
532
|
+
const addedNode = enrichedNodes[0];
|
|
533
|
+
if (!addedNode) return;
|
|
534
|
+
const { nodeLabelFilters: currentFilters } = graphFilterStore.getState();
|
|
535
|
+
const nodeLabel = addedNode.label_;
|
|
536
|
+
if (currentFilters.length > 0 && !currentFilters.includes(nodeLabel)) {
|
|
537
|
+
devLog3("[GraphExploration] Adding node label to active filters", {
|
|
538
|
+
nodeId,
|
|
539
|
+
nodeLabel,
|
|
540
|
+
currentFilters
|
|
541
|
+
});
|
|
542
|
+
graphFilterStore.getState().setNodeLabelFilters([...currentFilters, nodeLabel]);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function createGraphExplorationStore(deps) {
|
|
546
|
+
const stores = deps;
|
|
547
|
+
return create4()((set, get) => ({
|
|
548
|
+
focusedNodeIds: /* @__PURE__ */ new Set(),
|
|
549
|
+
isFocusMode: false,
|
|
550
|
+
hiddenElementIds: /* @__PURE__ */ new Set(),
|
|
551
|
+
expandedNodeIds: /* @__PURE__ */ new Set(),
|
|
552
|
+
setFocusMode: (nodeIds) => {
|
|
553
|
+
devLog3("[GraphExploration] setFocusMode", {
|
|
554
|
+
nodeIds,
|
|
555
|
+
count: nodeIds.length
|
|
556
|
+
});
|
|
557
|
+
set({
|
|
558
|
+
focusedNodeIds: new Set(nodeIds),
|
|
559
|
+
isFocusMode: true
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
setFocusModeWithNeighborhood: (nodeId) => {
|
|
563
|
+
const edges = stores.graphData.getState().edges;
|
|
564
|
+
const connectedNodeIds = /* @__PURE__ */ new Set();
|
|
565
|
+
const nodeIdStr = String(nodeId);
|
|
566
|
+
for (const edge of edges) {
|
|
567
|
+
const sourceId = String(edge.start_id_);
|
|
568
|
+
const targetId = String(edge.end_id_);
|
|
569
|
+
if (sourceId === nodeIdStr) {
|
|
570
|
+
connectedNodeIds.add(targetId);
|
|
571
|
+
} else if (targetId === nodeIdStr) {
|
|
572
|
+
connectedNodeIds.add(sourceId);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const focusNodeIds = [nodeIdStr, ...Array.from(connectedNodeIds)];
|
|
576
|
+
devLog3("[GraphExploration] setFocusModeWithNeighborhood", {
|
|
577
|
+
nodeId: nodeIdStr,
|
|
578
|
+
connectedNodes: connectedNodeIds.size,
|
|
579
|
+
totalFocused: focusNodeIds.length
|
|
580
|
+
});
|
|
581
|
+
set({
|
|
582
|
+
focusedNodeIds: new Set(focusNodeIds),
|
|
583
|
+
isFocusMode: true
|
|
584
|
+
});
|
|
585
|
+
},
|
|
586
|
+
clearFocusMode: () => {
|
|
587
|
+
devLog3("[GraphExploration] clearFocusMode");
|
|
588
|
+
set({
|
|
589
|
+
focusedNodeIds: /* @__PURE__ */ new Set(),
|
|
590
|
+
isFocusMode: false
|
|
591
|
+
});
|
|
592
|
+
},
|
|
593
|
+
hideElements: (elementIds) => {
|
|
594
|
+
const current = get().hiddenElementIds;
|
|
595
|
+
const updated = new Set(current);
|
|
596
|
+
elementIds.forEach((id) => updated.add(id));
|
|
597
|
+
devLog3("[GraphExploration] hideElements", {
|
|
598
|
+
newIds: elementIds,
|
|
599
|
+
totalHidden: updated.size
|
|
600
|
+
});
|
|
601
|
+
set({ hiddenElementIds: updated });
|
|
602
|
+
},
|
|
603
|
+
showElements: (elementIds) => {
|
|
604
|
+
const current = get().hiddenElementIds;
|
|
605
|
+
const updated = new Set(current);
|
|
606
|
+
elementIds.forEach((id) => updated.delete(id));
|
|
607
|
+
devLog3("[GraphExploration] showElements", {
|
|
608
|
+
restoredIds: elementIds,
|
|
609
|
+
remainingHidden: updated.size
|
|
610
|
+
});
|
|
611
|
+
set({ hiddenElementIds: updated });
|
|
612
|
+
},
|
|
613
|
+
hideNodeIncoming: (nodeId) => {
|
|
614
|
+
devLog3("[GraphExploration] hideNodeIncoming - hiding predecessors", {
|
|
615
|
+
nodeId
|
|
616
|
+
});
|
|
617
|
+
const { edges } = stores.graphData.getState();
|
|
618
|
+
const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
|
|
619
|
+
nodeId,
|
|
620
|
+
"in",
|
|
621
|
+
edges,
|
|
622
|
+
false
|
|
623
|
+
);
|
|
624
|
+
devLog3("[GraphExploration] hideNodeIncoming - traversal complete", {
|
|
625
|
+
nodeId,
|
|
626
|
+
nodesToHide: nodesToHide.size,
|
|
627
|
+
edgesToHide: edgesToHide.size
|
|
628
|
+
});
|
|
629
|
+
const allElementsToHide = [...nodesToHide, ...edgesToHide];
|
|
630
|
+
if (allElementsToHide.length > 0) {
|
|
631
|
+
get().hideElements(allElementsToHide);
|
|
632
|
+
} else {
|
|
633
|
+
devLog3("[GraphExploration] hideNodeIncoming - no predecessors found");
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
hideNodeOutgoing: (nodeId) => {
|
|
637
|
+
devLog3("[GraphExploration] hideNodeOutgoing - hiding successors", {
|
|
638
|
+
nodeId
|
|
639
|
+
});
|
|
640
|
+
const { edges } = stores.graphData.getState();
|
|
641
|
+
const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
|
|
642
|
+
nodeId,
|
|
643
|
+
"out",
|
|
644
|
+
edges,
|
|
645
|
+
false
|
|
646
|
+
);
|
|
647
|
+
devLog3("[GraphExploration] hideNodeOutgoing - traversal complete", {
|
|
648
|
+
nodeId,
|
|
649
|
+
nodesToHide: nodesToHide.size,
|
|
650
|
+
edgesToHide: edgesToHide.size
|
|
651
|
+
});
|
|
652
|
+
const allElementsToHide = [...nodesToHide, ...edgesToHide];
|
|
653
|
+
if (allElementsToHide.length > 0) {
|
|
654
|
+
get().hideElements(allElementsToHide);
|
|
655
|
+
} else {
|
|
656
|
+
devLog3("[GraphExploration] hideNodeOutgoing - no successors found");
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
hideEdgeForwardPath: (edgeId) => {
|
|
660
|
+
devLog3(
|
|
661
|
+
"[GraphExploration] hideEdgeForwardPath - hiding edge + target + downstream",
|
|
662
|
+
{ edgeId }
|
|
663
|
+
);
|
|
664
|
+
const { edges } = stores.graphData.getState();
|
|
665
|
+
const edge = edges.find((e) => String(e.id_) === edgeId);
|
|
666
|
+
if (!edge) {
|
|
667
|
+
devLog3("[GraphExploration] hideEdgeForwardPath - edge not found", {
|
|
668
|
+
edgeId
|
|
669
|
+
});
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const targetNodeId = String(edge.end_id_);
|
|
673
|
+
get().hideElements([edgeId]);
|
|
674
|
+
const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
|
|
675
|
+
targetNodeId,
|
|
676
|
+
"out",
|
|
677
|
+
edges,
|
|
678
|
+
true
|
|
679
|
+
);
|
|
680
|
+
devLog3("[GraphExploration] hideEdgeForwardPath - traversal complete", {
|
|
681
|
+
edgeId,
|
|
682
|
+
nodesToHide: nodesToHide.size,
|
|
683
|
+
edgesToHide: edgesToHide.size
|
|
684
|
+
});
|
|
685
|
+
const allElementsToHide = [...nodesToHide, ...edgesToHide];
|
|
686
|
+
if (allElementsToHide.length > 0) get().hideElements(allElementsToHide);
|
|
687
|
+
},
|
|
688
|
+
hideEdgeReversePath: (edgeId) => {
|
|
689
|
+
devLog3(
|
|
690
|
+
"[GraphExploration] hideEdgeReversePath - hiding edge + source + upstream",
|
|
691
|
+
{ edgeId }
|
|
692
|
+
);
|
|
693
|
+
const { edges } = stores.graphData.getState();
|
|
694
|
+
const edge = edges.find((e) => String(e.id_) === edgeId);
|
|
695
|
+
if (!edge) {
|
|
696
|
+
devLog3("[GraphExploration] hideEdgeReversePath - edge not found", {
|
|
697
|
+
edgeId
|
|
698
|
+
});
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const sourceNodeId = String(edge.start_id_);
|
|
702
|
+
get().hideElements([edgeId]);
|
|
703
|
+
const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
|
|
704
|
+
sourceNodeId,
|
|
705
|
+
"in",
|
|
706
|
+
edges,
|
|
707
|
+
true
|
|
708
|
+
);
|
|
709
|
+
devLog3("[GraphExploration] hideEdgeReversePath - traversal complete", {
|
|
710
|
+
edgeId,
|
|
711
|
+
nodesToHide: nodesToHide.size,
|
|
712
|
+
edgesToHide: edgesToHide.size
|
|
713
|
+
});
|
|
714
|
+
const allElementsToHide = [...nodesToHide, ...edgesToHide];
|
|
715
|
+
if (allElementsToHide.length > 0) get().hideElements(allElementsToHide);
|
|
716
|
+
},
|
|
717
|
+
focusOnEdgeType: (edgeId) => {
|
|
718
|
+
devLog3(
|
|
719
|
+
"[GraphExploration] focusOnEdgeType - show only edges of this type and connected nodes",
|
|
720
|
+
{ edgeId }
|
|
721
|
+
);
|
|
722
|
+
const graphData = stores.graphData.getState();
|
|
723
|
+
const edge = graphData.edges.find((e) => String(e.id_) === edgeId);
|
|
724
|
+
if (!edge) {
|
|
725
|
+
devLog3("[GraphExploration] focusOnEdgeType - edge not found", {
|
|
726
|
+
edgeId
|
|
727
|
+
});
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const edgeLabel = edge.label_;
|
|
731
|
+
devLog3("[GraphExploration] focusOnEdgeType - filtering by label", {
|
|
732
|
+
edgeId,
|
|
733
|
+
edgeLabel
|
|
734
|
+
});
|
|
735
|
+
const edgesOfType = graphData.edges.filter((e) => e.label_ === edgeLabel);
|
|
736
|
+
const connectedNodeIds = /* @__PURE__ */ new Set();
|
|
737
|
+
const visibleEdgeIds = /* @__PURE__ */ new Set();
|
|
738
|
+
for (const e of edgesOfType) {
|
|
739
|
+
connectedNodeIds.add(String(e.start_id_));
|
|
740
|
+
connectedNodeIds.add(String(e.end_id_));
|
|
741
|
+
visibleEdgeIds.add(String(e.id_));
|
|
742
|
+
}
|
|
743
|
+
const nodesToHide = [];
|
|
744
|
+
const edgesToHide = [];
|
|
745
|
+
for (const node of graphData.nodes) {
|
|
746
|
+
const nodeId = String(node.id_);
|
|
747
|
+
if (!connectedNodeIds.has(nodeId)) {
|
|
748
|
+
nodesToHide.push(nodeId);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
for (const e of graphData.edges) {
|
|
752
|
+
const eId = String(e.id_);
|
|
753
|
+
if (!visibleEdgeIds.has(eId)) {
|
|
754
|
+
edgesToHide.push(eId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
devLog3("[GraphExploration] focusOnEdgeType - hiding elements", {
|
|
758
|
+
edgeLabel,
|
|
759
|
+
visibleEdges: visibleEdgeIds.size,
|
|
760
|
+
visibleNodes: connectedNodeIds.size,
|
|
761
|
+
hidingNodes: nodesToHide.length,
|
|
762
|
+
hidingEdges: edgesToHide.length
|
|
763
|
+
});
|
|
764
|
+
const allElementsToHide = [...nodesToHide, ...edgesToHide];
|
|
765
|
+
if (allElementsToHide.length > 0) {
|
|
766
|
+
get().hideElements(allElementsToHide);
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
focusEdge: (edgeId) => {
|
|
770
|
+
devLog3(
|
|
771
|
+
"[GraphExploration] focusEdge - unified focus with auto-direction detection",
|
|
772
|
+
{ edgeId }
|
|
773
|
+
);
|
|
774
|
+
const graphData = stores.graphData.getState();
|
|
775
|
+
const selection = stores.selection.getState();
|
|
776
|
+
const edge = graphData.edges.find((e) => String(e.id_) === edgeId);
|
|
777
|
+
if (!edge) {
|
|
778
|
+
devLog3("[GraphExploration] focusEdge - edge not found", { edgeId });
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const endpoints = {
|
|
782
|
+
edgeLabel: edge.label_,
|
|
783
|
+
startNodeId: String(edge.start_id_),
|
|
784
|
+
endNodeId: String(edge.end_id_)
|
|
785
|
+
};
|
|
786
|
+
const { direction, referenceNodeId } = determineEdgeDirection(
|
|
787
|
+
endpoints,
|
|
788
|
+
selection.selectedNodeIds
|
|
789
|
+
);
|
|
790
|
+
devLog3("[GraphExploration] focusEdge - determined direction", {
|
|
791
|
+
edgeId,
|
|
792
|
+
edgeLabel: endpoints.edgeLabel,
|
|
793
|
+
direction,
|
|
794
|
+
referenceNodeId,
|
|
795
|
+
selectionMode: selection.selectedNodeIds.length === 1 ? "single-node" : "traditional"
|
|
796
|
+
});
|
|
797
|
+
const focusNodeIds = buildFocusSet(
|
|
798
|
+
referenceNodeId,
|
|
799
|
+
direction,
|
|
800
|
+
endpoints.edgeLabel,
|
|
801
|
+
graphData.edges
|
|
802
|
+
);
|
|
803
|
+
devLog3("[GraphExploration] focusEdge - found nodes", {
|
|
804
|
+
edgeLabel: endpoints.edgeLabel,
|
|
805
|
+
direction,
|
|
806
|
+
referenceNodeId,
|
|
807
|
+
totalNodes: focusNodeIds.size
|
|
808
|
+
});
|
|
809
|
+
set({
|
|
810
|
+
focusedNodeIds: focusNodeIds,
|
|
811
|
+
isFocusMode: true
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
resetView: () => {
|
|
815
|
+
devLog3("[GraphExploration] resetView - clearing all exploration state");
|
|
816
|
+
set({
|
|
817
|
+
focusedNodeIds: /* @__PURE__ */ new Set(),
|
|
818
|
+
isFocusMode: false,
|
|
819
|
+
hiddenElementIds: /* @__PURE__ */ new Set()
|
|
820
|
+
// Note: We keep expandedNodeIds - those nodes are still in the data
|
|
821
|
+
});
|
|
822
|
+
},
|
|
823
|
+
resetForNewQuery: () => {
|
|
824
|
+
devLog3(
|
|
825
|
+
"[GraphExploration] resetForNewQuery - clearing ALL exploration state for new query"
|
|
826
|
+
);
|
|
827
|
+
set({
|
|
828
|
+
focusedNodeIds: /* @__PURE__ */ new Set(),
|
|
829
|
+
isFocusMode: false,
|
|
830
|
+
hiddenElementIds: /* @__PURE__ */ new Set(),
|
|
831
|
+
expandedNodeIds: /* @__PURE__ */ new Set()
|
|
832
|
+
});
|
|
833
|
+
},
|
|
834
|
+
markNodeExpanded: (nodeId) => {
|
|
835
|
+
const current = get().expandedNodeIds;
|
|
836
|
+
const updated = new Set(current);
|
|
837
|
+
updated.add(nodeId);
|
|
838
|
+
devLog3("[GraphExploration] markNodeExpanded", { nodeId });
|
|
839
|
+
set({ expandedNodeIds: updated });
|
|
840
|
+
},
|
|
841
|
+
isNodeExpanded: (nodeId) => {
|
|
842
|
+
return get().expandedNodeIds.has(nodeId);
|
|
843
|
+
},
|
|
844
|
+
expandNode: async (nodeId) => {
|
|
845
|
+
devLog3("[GraphExploration] expandNode", { nodeId });
|
|
846
|
+
try {
|
|
847
|
+
const result = await stores.expandNode(Number(nodeId));
|
|
848
|
+
devLog3("[GraphExploration] Expansion API result", {
|
|
849
|
+
nodeId,
|
|
850
|
+
newNodes: result.nodes.length,
|
|
851
|
+
newEdges: result.edges.length
|
|
852
|
+
});
|
|
853
|
+
if (result.nodes.length === 0 && result.edges.length === 0) {
|
|
854
|
+
devLog3("[GraphExploration] No connected nodes found", { nodeId });
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const existingNodes = stores.graphData.getState().nodes;
|
|
858
|
+
const colorPalette = buildColorPaletteFromNodes([
|
|
859
|
+
...existingNodes,
|
|
860
|
+
...result.nodes
|
|
861
|
+
]);
|
|
862
|
+
const enrichedNodes = enrichNodes(
|
|
863
|
+
result.nodes,
|
|
864
|
+
colorPalette,
|
|
865
|
+
stores.getLabelDisplayMap?.()
|
|
866
|
+
);
|
|
867
|
+
const enrichedEdges = enrichEdges(result.edges);
|
|
868
|
+
stores.graphData.getState().mergeExpandedData(enrichedNodes, enrichedEdges);
|
|
869
|
+
const graphFilterState = stores.graphFilter.getState();
|
|
870
|
+
const currentFilters = graphFilterState.nodeLabelFilters;
|
|
871
|
+
if (currentFilters.length > 0 && enrichedNodes.length > 0) {
|
|
872
|
+
const newLabels = new Set(enrichedNodes.map((n) => n.label_));
|
|
873
|
+
const labelsToAdd = Array.from(newLabels).filter(
|
|
874
|
+
(label) => !currentFilters.includes(label)
|
|
875
|
+
);
|
|
876
|
+
if (labelsToAdd.length > 0) {
|
|
877
|
+
devLog3(
|
|
878
|
+
"[GraphExploration] Adding expanded node labels to active filters",
|
|
879
|
+
{
|
|
880
|
+
nodeId,
|
|
881
|
+
labelsToAdd,
|
|
882
|
+
currentFilters
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
stores.graphFilter.getState().setNodeLabelFilters([...currentFilters, ...labelsToAdd]);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
get().markNodeExpanded(nodeId);
|
|
889
|
+
const currentState = get();
|
|
890
|
+
if (currentState.isFocusMode) {
|
|
891
|
+
const newFocusedIds = new Set(currentState.focusedNodeIds);
|
|
892
|
+
for (const node of enrichedNodes) {
|
|
893
|
+
newFocusedIds.add(String(node.id_));
|
|
894
|
+
}
|
|
895
|
+
devLog3("[GraphExploration] Adding expanded nodes to focus set", {
|
|
896
|
+
nodeId,
|
|
897
|
+
previousFocusCount: currentState.focusedNodeIds.size,
|
|
898
|
+
addedToFocus: enrichedNodes.length,
|
|
899
|
+
newFocusCount: newFocusedIds.size
|
|
900
|
+
});
|
|
901
|
+
set({ focusedNodeIds: newFocusedIds });
|
|
902
|
+
}
|
|
903
|
+
devLog3("[GraphExploration] Expansion complete", {
|
|
904
|
+
nodeId,
|
|
905
|
+
addedNodes: enrichedNodes.length,
|
|
906
|
+
addedEdges: enrichedEdges.length
|
|
907
|
+
});
|
|
908
|
+
} catch (error) {
|
|
909
|
+
devLog3("[GraphExploration] Expansion failed", { nodeId, error });
|
|
910
|
+
throw error;
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
addNodeById: async (nodeId, autoFocus = false) => {
|
|
914
|
+
const nodeIdStr = String(nodeId);
|
|
915
|
+
devLog3("[GraphExploration] addNodeById", { nodeId, autoFocus });
|
|
916
|
+
try {
|
|
917
|
+
const existingNodes = stores.graphData.getState().nodes;
|
|
918
|
+
if (handleExistingNode(
|
|
919
|
+
nodeId,
|
|
920
|
+
nodeIdStr,
|
|
921
|
+
existingNodes,
|
|
922
|
+
autoFocus,
|
|
923
|
+
stores.graphFocus
|
|
924
|
+
)) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const enrichedNodes = await fetchAndEnrichNode(
|
|
928
|
+
nodeId,
|
|
929
|
+
existingNodes,
|
|
930
|
+
stores
|
|
931
|
+
);
|
|
932
|
+
ensureNodeLabelInFilters(enrichedNodes, nodeId, stores.graphFilter);
|
|
933
|
+
if (autoFocus) {
|
|
934
|
+
devLog3("[GraphExploration] Requesting focus for newly added node", {
|
|
935
|
+
nodeId
|
|
936
|
+
});
|
|
937
|
+
stores.graphFocus.getState().requestFocus(nodeIdStr, "node", false);
|
|
938
|
+
} else {
|
|
939
|
+
devLog3("[GraphExploration] Skipping focus for newly added node", {
|
|
940
|
+
nodeId
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const currentState = get();
|
|
944
|
+
if (currentState.isFocusMode) {
|
|
945
|
+
const newFocusedIds = new Set(currentState.focusedNodeIds);
|
|
946
|
+
newFocusedIds.add(nodeIdStr);
|
|
947
|
+
devLog3("[GraphExploration] Adding node to focus set", {
|
|
948
|
+
nodeId,
|
|
949
|
+
previousFocusCount: currentState.focusedNodeIds.size,
|
|
950
|
+
newFocusCount: newFocusedIds.size
|
|
951
|
+
});
|
|
952
|
+
set({ focusedNodeIds: newFocusedIds });
|
|
953
|
+
}
|
|
954
|
+
devLog3("[GraphExploration] addNodeById complete", {
|
|
955
|
+
nodeId,
|
|
956
|
+
addedNodes: enrichedNodes.length,
|
|
957
|
+
autoFocus
|
|
958
|
+
});
|
|
959
|
+
} catch (error) {
|
|
960
|
+
devLog3("[GraphExploration] addNodeById failed", { nodeId, error });
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}));
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/stores/GraphFocus.ts
|
|
968
|
+
import { create as create5 } from "zustand";
|
|
969
|
+
import { devLog as devLog4 } from "@petrarca/sonnet-core";
|
|
970
|
+
function buildStore() {
|
|
971
|
+
return (set, get) => ({
|
|
972
|
+
focusAction: null,
|
|
973
|
+
overrideElementIds: /* @__PURE__ */ new Set(),
|
|
974
|
+
requestFocus: (elementId, elementType, shouldSelect) => {
|
|
975
|
+
const action = {
|
|
976
|
+
elementId,
|
|
977
|
+
elementType,
|
|
978
|
+
action: shouldSelect ? "focus-and-select" : "focus",
|
|
979
|
+
timestamp: Date.now()
|
|
980
|
+
};
|
|
981
|
+
devLog4("[GraphFocusStore] requestFocus", {
|
|
982
|
+
elementId,
|
|
983
|
+
elementType,
|
|
984
|
+
action: action.action
|
|
985
|
+
});
|
|
986
|
+
set({ focusAction: action });
|
|
987
|
+
},
|
|
988
|
+
clear: () => {
|
|
989
|
+
const prev = get().focusAction;
|
|
990
|
+
if (prev) {
|
|
991
|
+
devLog4("[GraphFocusStore] clear", { prevElementId: prev.elementId });
|
|
992
|
+
}
|
|
993
|
+
set({ focusAction: null, overrideElementIds: /* @__PURE__ */ new Set() });
|
|
994
|
+
},
|
|
995
|
+
clearOverrides: () => {
|
|
996
|
+
devLog4("[GraphFocusStore] clearOverrides");
|
|
997
|
+
set({ overrideElementIds: /* @__PURE__ */ new Set() });
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
function createGraphFocusStore() {
|
|
1002
|
+
return create5()(buildStore());
|
|
1003
|
+
}
|
|
1004
|
+
var useGraphFocus = createGraphFocusStore();
|
|
1005
|
+
var GraphFocus_default = useGraphFocus;
|
|
1006
|
+
|
|
1007
|
+
// src/stores/ActionPanelStore.ts
|
|
1008
|
+
import { create as create6 } from "zustand";
|
|
1009
|
+
import { devLog as devLog5 } from "@petrarca/sonnet-core";
|
|
1010
|
+
function buildStore2() {
|
|
1011
|
+
return (set, get) => ({
|
|
1012
|
+
state: "visible",
|
|
1013
|
+
setState: (state) => {
|
|
1014
|
+
devLog5("[ActionPanelStore] setState", { state });
|
|
1015
|
+
set({ state });
|
|
1016
|
+
},
|
|
1017
|
+
hide: () => {
|
|
1018
|
+
devLog5("[ActionPanelStore] hide");
|
|
1019
|
+
set({ state: "hidden" });
|
|
1020
|
+
},
|
|
1021
|
+
show: () => {
|
|
1022
|
+
devLog5("[ActionPanelStore] show");
|
|
1023
|
+
set({ state: "visible" });
|
|
1024
|
+
},
|
|
1025
|
+
isHidden: () => get().state === "hidden"
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
function createActionPanelStore() {
|
|
1029
|
+
return create6()(buildStore2());
|
|
1030
|
+
}
|
|
1031
|
+
var useActionPanelStore = createActionPanelStore();
|
|
1032
|
+
|
|
1033
|
+
// src/hooks/useWorkspaceStores.ts
|
|
1034
|
+
import { createContext, useContext } from "react";
|
|
1035
|
+
var GraphStoresContext = createContext(null);
|
|
1036
|
+
function useGraphStores() {
|
|
1037
|
+
const ctx = useContext(GraphStoresContext);
|
|
1038
|
+
if (!ctx) {
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
"useGraphStores must be used within a GraphStoresProvider. Wrap your graph UI with <GraphStoresContext.Provider value={...}>."
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
return ctx;
|
|
1044
|
+
}
|
|
1045
|
+
function useWorkspaceGraphFilter() {
|
|
1046
|
+
return useGraphStores().graphFilter;
|
|
1047
|
+
}
|
|
1048
|
+
function useWorkspaceGraphData() {
|
|
1049
|
+
return useGraphStores().graphData;
|
|
1050
|
+
}
|
|
1051
|
+
function useWorkspaceGraphExploration() {
|
|
1052
|
+
return useGraphStores().graphExploration;
|
|
1053
|
+
}
|
|
1054
|
+
function useWorkspaceSelection() {
|
|
1055
|
+
return useGraphStores().selection;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/hooks/useLiveGraphData.ts
|
|
1059
|
+
import { useMemo, useEffect } from "react";
|
|
1060
|
+
import { devLog as devLog6 } from "@petrarca/sonnet-core";
|
|
1061
|
+
function useLiveGraphData() {
|
|
1062
|
+
const useGraphFilter2 = useWorkspaceGraphFilter();
|
|
1063
|
+
const useGraphData2 = useWorkspaceGraphData();
|
|
1064
|
+
const useGraphExploration = useWorkspaceGraphExploration();
|
|
1065
|
+
const lastDataChangeTime = useGraphFilter2((s) => s.lastDataChangeTime);
|
|
1066
|
+
const filterLabels = useGraphFilter2((s) => s.nodeLabelFilters);
|
|
1067
|
+
const allLabelsDisabled = useGraphFilter2((s) => s.allLabelsDisabled);
|
|
1068
|
+
const edgeFilterLabels = useGraphFilter2((s) => s.edgeLabelFilters);
|
|
1069
|
+
const allEdgeLabelsDisabled = useGraphFilter2((s) => s.allEdgeLabelsDisabled);
|
|
1070
|
+
const overrideElementIds = GraphFocus_default((s) => s.overrideElementIds);
|
|
1071
|
+
const isFocusMode = useGraphExploration((s) => s.isFocusMode);
|
|
1072
|
+
const focusedNodeIds = useGraphExploration((s) => s.focusedNodeIds);
|
|
1073
|
+
const hiddenElementIds = useGraphExploration((s) => s.hiddenElementIds);
|
|
1074
|
+
const baseNodes = useGraphData2((s) => s.nodes);
|
|
1075
|
+
const baseEdges = useGraphData2((s) => s.edges);
|
|
1076
|
+
const stats = useGraphData2((s) => s.stats);
|
|
1077
|
+
const values = useGraphData2((s) => s.values);
|
|
1078
|
+
const isValueSet = useGraphData2((s) => s.isValueSet);
|
|
1079
|
+
devLog6("[useLiveGraphData] Store data", {
|
|
1080
|
+
baseNodes: baseNodes.length,
|
|
1081
|
+
baseEdges: baseEdges.length,
|
|
1082
|
+
isFocusMode,
|
|
1083
|
+
focusedCount: focusedNodeIds.size,
|
|
1084
|
+
hiddenCount: hiddenElementIds.size
|
|
1085
|
+
});
|
|
1086
|
+
useEffect(() => {
|
|
1087
|
+
GraphFocus_default.getState().clearOverrides();
|
|
1088
|
+
}, [lastDataChangeTime]);
|
|
1089
|
+
const allLabelCounts = useMemo(() => {
|
|
1090
|
+
if (isValueSet) return {};
|
|
1091
|
+
const counts = {};
|
|
1092
|
+
for (const n of baseNodes) {
|
|
1093
|
+
const l = n?.label_;
|
|
1094
|
+
if (l) counts[l] = (counts[l] || 0) + 1;
|
|
1095
|
+
}
|
|
1096
|
+
return counts;
|
|
1097
|
+
}, [baseNodes, isValueSet]);
|
|
1098
|
+
const colorPalette = useMemo(() => {
|
|
1099
|
+
if (isValueSet) return {};
|
|
1100
|
+
return buildColorPalette(allLabelCounts);
|
|
1101
|
+
}, [allLabelCounts, isValueSet]);
|
|
1102
|
+
const availableNodes = useMemo(() => {
|
|
1103
|
+
let available = baseNodes;
|
|
1104
|
+
if (isFocusMode && focusedNodeIds.size > 0) {
|
|
1105
|
+
available = available.filter(
|
|
1106
|
+
(n) => focusedNodeIds.has(String(n.id_))
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
if (hiddenElementIds.size > 0) {
|
|
1110
|
+
available = available.filter(
|
|
1111
|
+
(n) => !hiddenElementIds.has(String(n.id_))
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
return available;
|
|
1115
|
+
}, [baseNodes, isFocusMode, focusedNodeIds, hiddenElementIds]);
|
|
1116
|
+
const availableEdges = useMemo(() => {
|
|
1117
|
+
const availableNodeIds = new Set(
|
|
1118
|
+
availableNodes.map((n) => String(n.id_))
|
|
1119
|
+
);
|
|
1120
|
+
let available = baseEdges.filter(
|
|
1121
|
+
(e) => availableNodeIds.has(String(e.start_id_)) && availableNodeIds.has(String(e.end_id_))
|
|
1122
|
+
);
|
|
1123
|
+
if (hiddenElementIds.size > 0) {
|
|
1124
|
+
available = available.filter(
|
|
1125
|
+
(e) => !hiddenElementIds.has(String(e.id_))
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
return available;
|
|
1129
|
+
}, [baseEdges, availableNodes, hiddenElementIds]);
|
|
1130
|
+
const labelCounts = useMemo(() => {
|
|
1131
|
+
if (isValueSet) return {};
|
|
1132
|
+
const counts = {};
|
|
1133
|
+
for (const n of availableNodes) {
|
|
1134
|
+
const l = n?.label_;
|
|
1135
|
+
if (l) counts[l] = (counts[l] || 0) + 1;
|
|
1136
|
+
}
|
|
1137
|
+
return counts;
|
|
1138
|
+
}, [availableNodes, isValueSet]);
|
|
1139
|
+
const edgeLabelCounts = useMemo(() => {
|
|
1140
|
+
if (isValueSet) return {};
|
|
1141
|
+
const counts = {};
|
|
1142
|
+
for (const e of availableEdges) {
|
|
1143
|
+
const l = e?.label_;
|
|
1144
|
+
if (l) counts[l] = (counts[l] || 0) + 1;
|
|
1145
|
+
}
|
|
1146
|
+
return counts;
|
|
1147
|
+
}, [availableEdges, isValueSet]);
|
|
1148
|
+
const filteredNodes = useMemo(() => {
|
|
1149
|
+
let filtered;
|
|
1150
|
+
if (allLabelsDisabled) {
|
|
1151
|
+
filtered = [];
|
|
1152
|
+
} else if (filterLabels && filterLabels.length > 0) {
|
|
1153
|
+
const setLabels = new Set(filterLabels);
|
|
1154
|
+
filtered = baseNodes.filter(
|
|
1155
|
+
(n) => setLabels.has(n.label_)
|
|
1156
|
+
);
|
|
1157
|
+
} else {
|
|
1158
|
+
filtered = baseNodes;
|
|
1159
|
+
}
|
|
1160
|
+
if (isFocusMode && focusedNodeIds.size > 0) {
|
|
1161
|
+
devLog6("[useLiveGraphData] Applying focus mode filter", {
|
|
1162
|
+
focusedCount: focusedNodeIds.size,
|
|
1163
|
+
beforeFilter: filtered.length
|
|
1164
|
+
});
|
|
1165
|
+
filtered = filtered.filter((n) => focusedNodeIds.has(String(n.id_)));
|
|
1166
|
+
devLog6("[useLiveGraphData] After focus mode filter", {
|
|
1167
|
+
afterFilter: filtered.length
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
if (hiddenElementIds.size > 0) {
|
|
1171
|
+
devLog6("[useLiveGraphData] Applying hidden elements filter", {
|
|
1172
|
+
hiddenCount: hiddenElementIds.size,
|
|
1173
|
+
beforeFilter: filtered.length
|
|
1174
|
+
});
|
|
1175
|
+
filtered = filtered.filter((n) => !hiddenElementIds.has(String(n.id_)));
|
|
1176
|
+
devLog6("[useLiveGraphData] After hidden elements filter", {
|
|
1177
|
+
afterFilter: filtered.length
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
if (overrideElementIds.size > 0) {
|
|
1181
|
+
const filteredIds = new Set(
|
|
1182
|
+
filtered.map((n) => String(n.id_))
|
|
1183
|
+
);
|
|
1184
|
+
const overrideNodes = baseNodes.filter(
|
|
1185
|
+
(n) => overrideElementIds.has(String(n.id_)) && !filteredIds.has(String(n.id_))
|
|
1186
|
+
);
|
|
1187
|
+
if (overrideNodes.length > 0) {
|
|
1188
|
+
devLog6("[useLiveGraphData] Adding override nodes", {
|
|
1189
|
+
overrideCount: overrideNodes.length,
|
|
1190
|
+
overrideIds: overrideNodes.map((n) => n.id_),
|
|
1191
|
+
totalAfterOverride: filtered.length + overrideNodes.length
|
|
1192
|
+
});
|
|
1193
|
+
filtered = [...filtered, ...overrideNodes];
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return filtered;
|
|
1197
|
+
}, [
|
|
1198
|
+
baseNodes,
|
|
1199
|
+
filterLabels,
|
|
1200
|
+
allLabelsDisabled,
|
|
1201
|
+
isFocusMode,
|
|
1202
|
+
focusedNodeIds,
|
|
1203
|
+
hiddenElementIds,
|
|
1204
|
+
overrideElementIds
|
|
1205
|
+
]);
|
|
1206
|
+
const filteredEdges = useMemo(() => {
|
|
1207
|
+
let edgesStage;
|
|
1208
|
+
if (allLabelsDisabled || allEdgeLabelsDisabled) {
|
|
1209
|
+
edgesStage = [];
|
|
1210
|
+
} else {
|
|
1211
|
+
const nodeIds = new Set(
|
|
1212
|
+
filteredNodes.map((n) => String(n.id_))
|
|
1213
|
+
);
|
|
1214
|
+
edgesStage = baseEdges.filter(
|
|
1215
|
+
(e) => nodeIds.has(String(e.start_id_)) && nodeIds.has(String(e.end_id_))
|
|
1216
|
+
);
|
|
1217
|
+
if (edgeFilterLabels && edgeFilterLabels.length > 0) {
|
|
1218
|
+
const setEdge = new Set(edgeFilterLabels);
|
|
1219
|
+
edgesStage = edgesStage.filter((e) => setEdge.has(e.label_));
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (hiddenElementIds.size > 0) {
|
|
1223
|
+
devLog6("[useLiveGraphData] Applying hidden elements filter to edges", {
|
|
1224
|
+
hiddenCount: hiddenElementIds.size,
|
|
1225
|
+
beforeFilter: edgesStage.length
|
|
1226
|
+
});
|
|
1227
|
+
edgesStage = edgesStage.filter(
|
|
1228
|
+
(e) => !hiddenElementIds.has(String(e.id_))
|
|
1229
|
+
);
|
|
1230
|
+
devLog6("[useLiveGraphData] After hidden elements filter (edges)", {
|
|
1231
|
+
afterFilter: edgesStage.length
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
if (overrideElementIds.size > 0) {
|
|
1235
|
+
const filteredIds = new Set(
|
|
1236
|
+
edgesStage.map((e) => String(e.id_))
|
|
1237
|
+
);
|
|
1238
|
+
const overrideEdges = baseEdges.filter(
|
|
1239
|
+
(e) => overrideElementIds.has(String(e.id_)) && !filteredIds.has(String(e.id_))
|
|
1240
|
+
);
|
|
1241
|
+
if (overrideEdges.length > 0) {
|
|
1242
|
+
devLog6("[useLiveGraphData] Adding override edges", {
|
|
1243
|
+
overrideCount: overrideEdges.length,
|
|
1244
|
+
overrideIds: overrideEdges.map((e) => e.id_),
|
|
1245
|
+
totalAfterOverride: edgesStage.length + overrideEdges.length
|
|
1246
|
+
});
|
|
1247
|
+
edgesStage = [...edgesStage, ...overrideEdges];
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return edgesStage;
|
|
1251
|
+
}, [
|
|
1252
|
+
baseEdges,
|
|
1253
|
+
filteredNodes,
|
|
1254
|
+
allLabelsDisabled,
|
|
1255
|
+
allEdgeLabelsDisabled,
|
|
1256
|
+
edgeFilterLabels,
|
|
1257
|
+
hiddenElementIds,
|
|
1258
|
+
overrideElementIds
|
|
1259
|
+
]);
|
|
1260
|
+
const baseNormalized = useMemo(() => {
|
|
1261
|
+
if (isValueSet) return EMPTY_NORMALIZED;
|
|
1262
|
+
return normalizeGraph(baseNodes, baseEdges);
|
|
1263
|
+
}, [isValueSet, baseNodes, baseEdges]);
|
|
1264
|
+
const normalized = useMemo(() => {
|
|
1265
|
+
if (isValueSet) return EMPTY_NORMALIZED;
|
|
1266
|
+
return normalizeGraph(filteredNodes, filteredEdges);
|
|
1267
|
+
}, [isValueSet, filteredNodes, filteredEdges]);
|
|
1268
|
+
const nodes = useMemo(
|
|
1269
|
+
() => normalized.nodeIds.map((id) => normalized.nodesById[id]),
|
|
1270
|
+
[normalized]
|
|
1271
|
+
);
|
|
1272
|
+
const edges = useMemo(
|
|
1273
|
+
() => normalized.edgeIds.map((id) => normalized.edgesById[id]),
|
|
1274
|
+
[normalized]
|
|
1275
|
+
);
|
|
1276
|
+
const adjustedStats = useMemo(() => {
|
|
1277
|
+
if (!stats) return void 0;
|
|
1278
|
+
return {
|
|
1279
|
+
...stats,
|
|
1280
|
+
nodes: availableNodes.length,
|
|
1281
|
+
edges: availableEdges.length
|
|
1282
|
+
};
|
|
1283
|
+
}, [stats, availableNodes.length, availableEdges.length]);
|
|
1284
|
+
useEffect(() => {
|
|
1285
|
+
const { clearOverrides } = GraphFocus_default.getState();
|
|
1286
|
+
clearOverrides();
|
|
1287
|
+
}, [
|
|
1288
|
+
filterLabels,
|
|
1289
|
+
allLabelsDisabled,
|
|
1290
|
+
edgeFilterLabels,
|
|
1291
|
+
allEdgeLabelsDisabled
|
|
1292
|
+
]);
|
|
1293
|
+
return {
|
|
1294
|
+
nodes,
|
|
1295
|
+
edges,
|
|
1296
|
+
stats: adjustedStats,
|
|
1297
|
+
labelCounts,
|
|
1298
|
+
edgeLabelCounts,
|
|
1299
|
+
values,
|
|
1300
|
+
colorPalette,
|
|
1301
|
+
normalized,
|
|
1302
|
+
baseNormalized
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/hooks/useReagraphData.ts
|
|
1307
|
+
import { useMemo as useMemo2 } from "react";
|
|
1308
|
+
function toReagraphNode(visualNode) {
|
|
1309
|
+
return {
|
|
1310
|
+
id: String(visualNode.id_),
|
|
1311
|
+
label: visualNode.visual.displayName,
|
|
1312
|
+
fill: visualNode.visual.color,
|
|
1313
|
+
size: visualNode.visual.size,
|
|
1314
|
+
icon: visualNode.visual.icon,
|
|
1315
|
+
// Embed full visual node so event handlers can access all domain data
|
|
1316
|
+
data: visualNode
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function toReagraphEdge(visualEdge) {
|
|
1320
|
+
return {
|
|
1321
|
+
id: String(visualEdge.id_),
|
|
1322
|
+
source: String(visualEdge.start_id_),
|
|
1323
|
+
target: String(visualEdge.end_id_),
|
|
1324
|
+
label: visualEdge.label_,
|
|
1325
|
+
fill: visualEdge.visual.color,
|
|
1326
|
+
size: visualEdge.visual.size ?? 3,
|
|
1327
|
+
dashed: visualEdge.visual.dashed,
|
|
1328
|
+
// Embed full visual edge so event handlers can access all domain data
|
|
1329
|
+
data: visualEdge
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function useReagraphData() {
|
|
1333
|
+
const live = useLiveGraphData();
|
|
1334
|
+
const nodes = useMemo2(
|
|
1335
|
+
() => live.nodes.map(toReagraphNode),
|
|
1336
|
+
[live.nodes]
|
|
1337
|
+
);
|
|
1338
|
+
const edges = useMemo2(
|
|
1339
|
+
() => live.edges.map(toReagraphEdge),
|
|
1340
|
+
[live.edges]
|
|
1341
|
+
);
|
|
1342
|
+
return {
|
|
1343
|
+
nodes,
|
|
1344
|
+
edges,
|
|
1345
|
+
stats: live.stats,
|
|
1346
|
+
colorPalette: live.colorPalette
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/hooks/useCytoscapeData.ts
|
|
1351
|
+
import { useMemo as useMemo3 } from "react";
|
|
1352
|
+
function toCytoscapeNode(visualNode) {
|
|
1353
|
+
const color = visualNode.visual.color ?? "#4285F4";
|
|
1354
|
+
const size = visualNode.visual.size ?? 40;
|
|
1355
|
+
return {
|
|
1356
|
+
data: {
|
|
1357
|
+
id: String(visualNode.id_),
|
|
1358
|
+
label: visualNode.visual.displayName,
|
|
1359
|
+
id_: visualNode.id_,
|
|
1360
|
+
label_: visualNode.label_,
|
|
1361
|
+
properties_: visualNode.properties_,
|
|
1362
|
+
displayName: visualNode.visual.displayName,
|
|
1363
|
+
color,
|
|
1364
|
+
size,
|
|
1365
|
+
icon: visualNode.visual.icon,
|
|
1366
|
+
// Full node reference for context menu and event handlers
|
|
1367
|
+
visualGraph: visualNode
|
|
1368
|
+
}
|
|
1369
|
+
// No inline styles — stylesheet handles all styling via data mappers
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function toCytoscapeEdge(visualEdge) {
|
|
1373
|
+
return {
|
|
1374
|
+
data: {
|
|
1375
|
+
id: String(visualEdge.id_),
|
|
1376
|
+
source: String(visualEdge.start_id_),
|
|
1377
|
+
target: String(visualEdge.end_id_),
|
|
1378
|
+
label: visualEdge.label_,
|
|
1379
|
+
id_: visualEdge.id_,
|
|
1380
|
+
label_: visualEdge.label_,
|
|
1381
|
+
start_id_: visualEdge.start_id_,
|
|
1382
|
+
end_id_: visualEdge.end_id_,
|
|
1383
|
+
properties_: visualEdge.properties_,
|
|
1384
|
+
color: visualEdge.visual.color ?? "#9E9E9E",
|
|
1385
|
+
size: visualEdge.visual.size ?? 3,
|
|
1386
|
+
// Only set dashed when true — falsy values omitted to avoid stylesheet noise
|
|
1387
|
+
dashed: visualEdge.visual.dashed || void 0
|
|
1388
|
+
}
|
|
1389
|
+
// No inline styles — stylesheet handles all styling via data mappers
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
function useCytoscapeData() {
|
|
1393
|
+
const live = useLiveGraphData();
|
|
1394
|
+
const elements = useMemo3(
|
|
1395
|
+
() => ({
|
|
1396
|
+
nodes: live.nodes.map(toCytoscapeNode),
|
|
1397
|
+
edges: live.edges.map(toCytoscapeEdge)
|
|
1398
|
+
}),
|
|
1399
|
+
[live.nodes, live.edges]
|
|
1400
|
+
);
|
|
1401
|
+
return {
|
|
1402
|
+
elements,
|
|
1403
|
+
stats: live.stats,
|
|
1404
|
+
colorPalette: live.colorPalette
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/hooks/useGraphRendererContext.ts
|
|
1409
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
1410
|
+
var GraphRendererContext = createContext2(null);
|
|
1411
|
+
function useGraphRenderer() {
|
|
1412
|
+
const ctx = useContext2(GraphRendererContext);
|
|
1413
|
+
if (!ctx) {
|
|
1414
|
+
throw new Error(
|
|
1415
|
+
"useGraphRenderer must be used within a GraphRendererProvider"
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
return ctx;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/hooks/graphRendererProvider.tsx
|
|
1422
|
+
import { useState, useCallback } from "react";
|
|
1423
|
+
import { jsx } from "react/jsx-runtime";
|
|
1424
|
+
function GraphRendererProvider({
|
|
1425
|
+
children,
|
|
1426
|
+
defaultRenderer = "cytoscape",
|
|
1427
|
+
onRendererChange
|
|
1428
|
+
}) {
|
|
1429
|
+
const [renderer, setRendererState] = useState(defaultRenderer);
|
|
1430
|
+
const setRenderer = useCallback(
|
|
1431
|
+
(newRenderer) => {
|
|
1432
|
+
setRendererState(newRenderer);
|
|
1433
|
+
onRendererChange?.(newRenderer);
|
|
1434
|
+
},
|
|
1435
|
+
[onRendererChange]
|
|
1436
|
+
);
|
|
1437
|
+
return /* @__PURE__ */ jsx(GraphRendererContext.Provider, { value: { renderer, setRenderer }, children });
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// src/components/rea/ReaGraph.tsx
|
|
1441
|
+
import {
|
|
1442
|
+
useEffect as useEffect2,
|
|
1443
|
+
useRef,
|
|
1444
|
+
useState as useState3,
|
|
1445
|
+
useCallback as useCallback2
|
|
1446
|
+
} from "react";
|
|
1447
|
+
import { devLog as devLog8 } from "@petrarca/sonnet-core";
|
|
1448
|
+
import {
|
|
1449
|
+
GraphCanvas,
|
|
1450
|
+
useSelection as useSelection2
|
|
1451
|
+
} from "reagraph";
|
|
1452
|
+
|
|
1453
|
+
// src/components/rea/theme.ts
|
|
1454
|
+
import { lightTheme } from "reagraph";
|
|
1455
|
+
var highlightColor = "#ffb512";
|
|
1456
|
+
var highlightColorText = "#c98a02";
|
|
1457
|
+
var theme = {
|
|
1458
|
+
...lightTheme,
|
|
1459
|
+
node: {
|
|
1460
|
+
...lightTheme.node,
|
|
1461
|
+
activeFill: highlightColor,
|
|
1462
|
+
label: {
|
|
1463
|
+
...lightTheme.node.label,
|
|
1464
|
+
activeColor: highlightColorText
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
ring: {
|
|
1468
|
+
...lightTheme.ring,
|
|
1469
|
+
activeFill: highlightColor
|
|
1470
|
+
},
|
|
1471
|
+
edge: {
|
|
1472
|
+
...lightTheme.edge,
|
|
1473
|
+
activeFill: highlightColor,
|
|
1474
|
+
label: {
|
|
1475
|
+
...lightTheme.edge.label,
|
|
1476
|
+
activeColor: highlightColorText
|
|
1477
|
+
}
|
|
1478
|
+
},
|
|
1479
|
+
arrow: {
|
|
1480
|
+
...lightTheme.arrow,
|
|
1481
|
+
activeFill: highlightColor
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
// src/components/rea/ReaGraph.tsx
|
|
1486
|
+
import { cn } from "@petrarca/sonnet-core";
|
|
1487
|
+
|
|
1488
|
+
// src/components/rea/LayoutAndCameraControls.tsx
|
|
1489
|
+
import {
|
|
1490
|
+
Button as Button2,
|
|
1491
|
+
DropdownMenu as DropdownMenu2,
|
|
1492
|
+
DropdownMenuContent as DropdownMenuContent2,
|
|
1493
|
+
DropdownMenuItem as DropdownMenuItem2,
|
|
1494
|
+
DropdownMenuLabel,
|
|
1495
|
+
DropdownMenuSeparator,
|
|
1496
|
+
DropdownMenuTrigger as DropdownMenuTrigger2,
|
|
1497
|
+
SimpleTooltip as SimpleTooltip2
|
|
1498
|
+
} from "@petrarca/sonnet-ui";
|
|
1499
|
+
import { LayoutPanelTop, Maximize, Shrink, ArrowUp, Eye } from "lucide-react";
|
|
1500
|
+
import { useState as useState2 } from "react";
|
|
1501
|
+
|
|
1502
|
+
// src/components/shared/RendererDropdown.tsx
|
|
1503
|
+
import {
|
|
1504
|
+
Button,
|
|
1505
|
+
DropdownMenu,
|
|
1506
|
+
DropdownMenuContent,
|
|
1507
|
+
DropdownMenuItem,
|
|
1508
|
+
DropdownMenuTrigger,
|
|
1509
|
+
SimpleTooltip
|
|
1510
|
+
} from "@petrarca/sonnet-ui";
|
|
1511
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1512
|
+
function RendererDropdown() {
|
|
1513
|
+
const { renderer, setRenderer } = useGraphRenderer();
|
|
1514
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
1515
|
+
/* @__PURE__ */ jsx2(SimpleTooltip, { label: "Renderer", side: "left", children: /* @__PURE__ */ jsx2(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx2(Button, { variant: "ghost", size: "icon", children: renderer === "reagraph" ? "RG" : "CY" }) }) }),
|
|
1516
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { side: "left", align: "start", children: [
|
|
1517
|
+
/* @__PURE__ */ jsx2(
|
|
1518
|
+
DropdownMenuItem,
|
|
1519
|
+
{
|
|
1520
|
+
onClick: () => setRenderer("reagraph"),
|
|
1521
|
+
className: renderer === "reagraph" ? "text-blue-600" : "",
|
|
1522
|
+
children: "Reagraph"
|
|
1523
|
+
}
|
|
1524
|
+
),
|
|
1525
|
+
/* @__PURE__ */ jsx2(
|
|
1526
|
+
DropdownMenuItem,
|
|
1527
|
+
{
|
|
1528
|
+
onClick: () => setRenderer("cytoscape"),
|
|
1529
|
+
className: renderer === "cytoscape" ? "text-blue-600" : "",
|
|
1530
|
+
children: "Cytoscape"
|
|
1531
|
+
}
|
|
1532
|
+
)
|
|
1533
|
+
] })
|
|
1534
|
+
] });
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/components/rea/LayoutAndCameraControls.tsx
|
|
1538
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1539
|
+
function PathSelectionDropdown({
|
|
1540
|
+
setPathSelectionType
|
|
1541
|
+
}) {
|
|
1542
|
+
return /* @__PURE__ */ jsxs2(DropdownMenu2, { children: [
|
|
1543
|
+
/* @__PURE__ */ jsx3(SimpleTooltip2, { label: "Path Selection Type", side: "left", children: /* @__PURE__ */ jsx3(DropdownMenuTrigger2, { asChild: true, children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx3(ArrowUp, {}) }) }) }),
|
|
1544
|
+
/* @__PURE__ */ jsxs2(DropdownMenuContent2, { side: "left", align: "start", children: [
|
|
1545
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => setPathSelectionType("direct"), children: "None" }),
|
|
1546
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => setPathSelectionType("all"), children: "All Paths" }),
|
|
1547
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => setPathSelectionType("in"), children: "Inbound" }),
|
|
1548
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => setPathSelectionType("out"), children: "Outbound" })
|
|
1549
|
+
] })
|
|
1550
|
+
] });
|
|
1551
|
+
}
|
|
1552
|
+
function LayoutDropdown({ updateLayout }) {
|
|
1553
|
+
const [showTooltip, setShowTooltip] = useState2(true);
|
|
1554
|
+
const handleSelect = (layout) => {
|
|
1555
|
+
updateLayout(layout);
|
|
1556
|
+
setShowTooltip(false);
|
|
1557
|
+
setTimeout(() => setShowTooltip(true), 500);
|
|
1558
|
+
};
|
|
1559
|
+
const trigger = /* @__PURE__ */ jsx3(DropdownMenuTrigger2, { asChild: true, children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx3(LayoutPanelTop, {}) }) });
|
|
1560
|
+
return /* @__PURE__ */ jsxs2(DropdownMenu2, { children: [
|
|
1561
|
+
showTooltip ? /* @__PURE__ */ jsx3(SimpleTooltip2, { label: "Layout", side: "left", children: trigger }) : trigger,
|
|
1562
|
+
/* @__PURE__ */ jsxs2(DropdownMenuContent2, { side: "left", align: "start", children: [
|
|
1563
|
+
/* @__PURE__ */ jsx3(DropdownMenuLabel, { children: "Force-directed Layouts" }),
|
|
1564
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("forceDirected2d"), children: "Force Directed" }),
|
|
1565
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("forceatlas2"), children: "Force Atlas 2" }),
|
|
1566
|
+
/* @__PURE__ */ jsx3(DropdownMenuSeparator, {}),
|
|
1567
|
+
/* @__PURE__ */ jsx3(DropdownMenuLabel, { children: "Tree Layouts" }),
|
|
1568
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("treeTd2d"), children: "Tree Top-Down" }),
|
|
1569
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("treeLr2d"), children: "Tree Left-Right" }),
|
|
1570
|
+
/* @__PURE__ */ jsx3(DropdownMenuSeparator, {}),
|
|
1571
|
+
/* @__PURE__ */ jsx3(DropdownMenuLabel, { children: "Circular & Radial Layouts" }),
|
|
1572
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("circular2d"), children: "Circular" }),
|
|
1573
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("radialOut2d"), children: "Radial Out" }),
|
|
1574
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("concentric2d"), children: "Concentric" }),
|
|
1575
|
+
/* @__PURE__ */ jsx3(DropdownMenuSeparator, {}),
|
|
1576
|
+
/* @__PURE__ */ jsx3(DropdownMenuLabel, { children: "Utility Layouts" }),
|
|
1577
|
+
/* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("nooverlap"), children: "No Overlap" })
|
|
1578
|
+
] })
|
|
1579
|
+
] });
|
|
1580
|
+
}
|
|
1581
|
+
function ViewControls({
|
|
1582
|
+
fullscreen,
|
|
1583
|
+
toggleFullscreen,
|
|
1584
|
+
fitToScreen,
|
|
1585
|
+
hasHiddenElements,
|
|
1586
|
+
isFocusMode,
|
|
1587
|
+
onResetView
|
|
1588
|
+
}) {
|
|
1589
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1590
|
+
/* @__PURE__ */ jsx3(SimpleTooltip2, { label: "Fullscreen", side: "left", children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", onClick: toggleFullscreen, children: !fullscreen ? /* @__PURE__ */ jsx3(Maximize, {}) : /* @__PURE__ */ jsx3(Shrink, {}) }) }),
|
|
1591
|
+
/* @__PURE__ */ jsx3(SimpleTooltip2, { label: "Fit to screen", side: "left", children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", onClick: fitToScreen, children: /* @__PURE__ */ jsx3(Shrink, {}) }) }),
|
|
1592
|
+
(hasHiddenElements || isFocusMode) && onResetView && /* @__PURE__ */ jsx3(
|
|
1593
|
+
SimpleTooltip2,
|
|
1594
|
+
{
|
|
1595
|
+
label: isFocusMode ? "Reset Focus" : "Show All Hidden",
|
|
1596
|
+
side: "left",
|
|
1597
|
+
children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", onClick: onResetView, children: /* @__PURE__ */ jsx3(Eye, {}) })
|
|
1598
|
+
}
|
|
1599
|
+
)
|
|
1600
|
+
] });
|
|
1601
|
+
}
|
|
1602
|
+
function LayoutAndCameraControls({
|
|
1603
|
+
fitToScreen,
|
|
1604
|
+
fullscreen,
|
|
1605
|
+
toggleFullscreen,
|
|
1606
|
+
updateLayout,
|
|
1607
|
+
setPathSelectionType,
|
|
1608
|
+
hasHiddenElements = false,
|
|
1609
|
+
isFocusMode = false,
|
|
1610
|
+
onResetView,
|
|
1611
|
+
extraToolbarContent
|
|
1612
|
+
}) {
|
|
1613
|
+
return /* @__PURE__ */ jsxs2("div", { className: "absolute right-2 top-2 flex flex-col border", children: [
|
|
1614
|
+
/* @__PURE__ */ jsx3(RendererDropdown, {}),
|
|
1615
|
+
/* @__PURE__ */ jsx3(
|
|
1616
|
+
ViewControls,
|
|
1617
|
+
{
|
|
1618
|
+
fullscreen,
|
|
1619
|
+
toggleFullscreen,
|
|
1620
|
+
fitToScreen,
|
|
1621
|
+
hasHiddenElements,
|
|
1622
|
+
isFocusMode,
|
|
1623
|
+
onResetView
|
|
1624
|
+
}
|
|
1625
|
+
),
|
|
1626
|
+
/* @__PURE__ */ jsx3(PathSelectionDropdown, { setPathSelectionType }),
|
|
1627
|
+
extraToolbarContent,
|
|
1628
|
+
/* @__PURE__ */ jsx3(LayoutDropdown, { updateLayout })
|
|
1629
|
+
] });
|
|
1630
|
+
}
|
|
1631
|
+
var LayoutAndCameraControls_default = LayoutAndCameraControls;
|
|
1632
|
+
|
|
1633
|
+
// src/components/rea/ReaGraph.tsx
|
|
1634
|
+
import { Card as Card2 } from "@petrarca/sonnet-ui";
|
|
1635
|
+
|
|
1636
|
+
// src/components/rea/NodeHoverInfo.tsx
|
|
1637
|
+
import { Card, Separator } from "@petrarca/sonnet-ui";
|
|
1638
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1639
|
+
function NodeHoverInfo({ node }) {
|
|
1640
|
+
return /* @__PURE__ */ jsx4(Card, { className: "absolute top-2 left-1/2 -translate-x-1/2 bg-white shadow-md p-2", children: /* @__PURE__ */ jsxs3("div", { className: "flex flex-row items-center gap-2", children: [
|
|
1641
|
+
/* @__PURE__ */ jsx4(
|
|
1642
|
+
Separator,
|
|
1643
|
+
{
|
|
1644
|
+
orientation: "vertical",
|
|
1645
|
+
decorative: true,
|
|
1646
|
+
className: "h-10 w-1",
|
|
1647
|
+
style: { backgroundColor: node.fill }
|
|
1648
|
+
}
|
|
1649
|
+
),
|
|
1650
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
1651
|
+
/* @__PURE__ */ jsxs3("p", { children: [
|
|
1652
|
+
/* @__PURE__ */ jsxs3("strong", { children: [
|
|
1653
|
+
node.data.label_,
|
|
1654
|
+
": "
|
|
1655
|
+
] }),
|
|
1656
|
+
node.label
|
|
1657
|
+
] }),
|
|
1658
|
+
/* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: node.data.id_ })
|
|
1659
|
+
] })
|
|
1660
|
+
] }) });
|
|
1661
|
+
}
|
|
1662
|
+
var NodeHoverInfo_default = NodeHoverInfo;
|
|
1663
|
+
|
|
1664
|
+
// src/components/rea/ReaContextMenu.tsx
|
|
1665
|
+
import React2 from "react";
|
|
1666
|
+
import { devLog as devLog7 } from "@petrarca/sonnet-core";
|
|
1667
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1668
|
+
function ReaContextMenu({
|
|
1669
|
+
data,
|
|
1670
|
+
onClose,
|
|
1671
|
+
elementType,
|
|
1672
|
+
useGraphExploration,
|
|
1673
|
+
onCopyNode
|
|
1674
|
+
}) {
|
|
1675
|
+
const handleFocus = () => {
|
|
1676
|
+
if (elementType === "node") {
|
|
1677
|
+
const nodeId = data.id;
|
|
1678
|
+
devLog7("[ReaContextMenu] Focus on node and neighborhood:", nodeId);
|
|
1679
|
+
useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId);
|
|
1680
|
+
} else {
|
|
1681
|
+
const startNodeId = data.data.start_id_;
|
|
1682
|
+
devLog7("[ReaContextMenu] Focus on edge start node:", startNodeId);
|
|
1683
|
+
useGraphExploration.getState().setFocusModeWithNeighborhood(startNodeId);
|
|
1684
|
+
}
|
|
1685
|
+
onClose();
|
|
1686
|
+
};
|
|
1687
|
+
const handleCopy = () => {
|
|
1688
|
+
devLog7("[ReaContextMenu] Copy to clipboard");
|
|
1689
|
+
if (elementType === "node") {
|
|
1690
|
+
const visualGraph = data.data?.visualGraph;
|
|
1691
|
+
if (visualGraph && onCopyNode) {
|
|
1692
|
+
onCopyNode(visualGraph);
|
|
1693
|
+
} else if (!visualGraph) {
|
|
1694
|
+
const nodeText = `Node: ${data.label || data.id}`;
|
|
1695
|
+
navigator.clipboard.writeText(nodeText);
|
|
1696
|
+
}
|
|
1697
|
+
} else {
|
|
1698
|
+
const edgeText = `Edge: ${data.data.start_id_} -> ${data.data.end_id_}`;
|
|
1699
|
+
navigator.clipboard.writeText(edgeText);
|
|
1700
|
+
}
|
|
1701
|
+
onClose();
|
|
1702
|
+
};
|
|
1703
|
+
const handleHideNode = () => {
|
|
1704
|
+
if (elementType === "node") {
|
|
1705
|
+
const nodeId = data.id;
|
|
1706
|
+
devLog7("[ReaContextMenu] Hide node:", nodeId);
|
|
1707
|
+
useGraphExploration.getState().hideElements([nodeId]);
|
|
1708
|
+
}
|
|
1709
|
+
onClose();
|
|
1710
|
+
};
|
|
1711
|
+
const handleHideIncoming = () => {
|
|
1712
|
+
if (elementType === "node") {
|
|
1713
|
+
const nodeId = data.id;
|
|
1714
|
+
devLog7("[ReaContextMenu] Hide incoming edges:", nodeId);
|
|
1715
|
+
useGraphExploration.getState().hideNodeIncoming(nodeId);
|
|
1716
|
+
}
|
|
1717
|
+
onClose();
|
|
1718
|
+
};
|
|
1719
|
+
const handleHideOutgoing = () => {
|
|
1720
|
+
if (elementType === "node") {
|
|
1721
|
+
const nodeId = data.id;
|
|
1722
|
+
devLog7("[ReaContextMenu] Hide outgoing edges:", nodeId);
|
|
1723
|
+
useGraphExploration.getState().hideNodeOutgoing(nodeId);
|
|
1724
|
+
}
|
|
1725
|
+
onClose();
|
|
1726
|
+
};
|
|
1727
|
+
const handleExpand = () => {
|
|
1728
|
+
if (elementType === "node") {
|
|
1729
|
+
const nodeId = data.id;
|
|
1730
|
+
devLog7("[ReaContextMenu] Expand node:", nodeId);
|
|
1731
|
+
useGraphExploration.getState().expandNode(nodeId);
|
|
1732
|
+
}
|
|
1733
|
+
onClose();
|
|
1734
|
+
};
|
|
1735
|
+
const handleHideForwardPath = () => {
|
|
1736
|
+
if (elementType === "edge") {
|
|
1737
|
+
const edgeId = data.id;
|
|
1738
|
+
devLog7("[ReaContextMenu] Hide forward path from edge:", edgeId);
|
|
1739
|
+
useGraphExploration.getState().hideEdgeForwardPath(edgeId);
|
|
1740
|
+
}
|
|
1741
|
+
onClose();
|
|
1742
|
+
};
|
|
1743
|
+
const handleHideReversePath = () => {
|
|
1744
|
+
if (elementType === "edge") {
|
|
1745
|
+
const edgeId = data.id;
|
|
1746
|
+
devLog7("[ReaContextMenu] Hide reverse path from edge:", edgeId);
|
|
1747
|
+
useGraphExploration.getState().hideEdgeReversePath(edgeId);
|
|
1748
|
+
}
|
|
1749
|
+
onClose();
|
|
1750
|
+
};
|
|
1751
|
+
const handleFocusOnEdgeType = () => {
|
|
1752
|
+
if (elementType === "edge") {
|
|
1753
|
+
const edgeId = data.id;
|
|
1754
|
+
const edgeLabel = data.data?.label_ || "edge";
|
|
1755
|
+
devLog7("[ReaContextMenu] Focus on edge type:", edgeId, edgeLabel);
|
|
1756
|
+
useGraphExploration.getState().focusOnEdgeType(edgeId);
|
|
1757
|
+
}
|
|
1758
|
+
onClose();
|
|
1759
|
+
};
|
|
1760
|
+
const nodeMenuItems = [
|
|
1761
|
+
{ label: "Focus", action: handleFocus },
|
|
1762
|
+
{ label: "Copy", action: handleCopy },
|
|
1763
|
+
{ label: "Hide Node", action: handleHideNode },
|
|
1764
|
+
{ label: "Hide Incoming", action: handleHideIncoming },
|
|
1765
|
+
{ label: "Hide Outgoing", action: handleHideOutgoing },
|
|
1766
|
+
{ label: "Expand", action: handleExpand }
|
|
1767
|
+
];
|
|
1768
|
+
const edgeMenuItems = [
|
|
1769
|
+
{ label: "Focus", action: handleFocus },
|
|
1770
|
+
{ label: "Focus Edge Type", action: handleFocusOnEdgeType },
|
|
1771
|
+
{ label: "Copy", action: handleCopy },
|
|
1772
|
+
{ label: "Hide Forward Path", action: handleHideForwardPath },
|
|
1773
|
+
{ label: "Hide Reverse Path", action: handleHideReversePath }
|
|
1774
|
+
];
|
|
1775
|
+
const menuItems = elementType === "node" ? nodeMenuItems : edgeMenuItems;
|
|
1776
|
+
return /* @__PURE__ */ jsx5("div", { className: "bg-white border border-gray-200 rounded-md shadow-lg py-1 min-w-48", children: menuItems.map((item, index) => /* @__PURE__ */ jsxs4(React2.Fragment, { children: [
|
|
1777
|
+
/* @__PURE__ */ jsx5(
|
|
1778
|
+
"button",
|
|
1779
|
+
{
|
|
1780
|
+
className: "w-full text-left px-3 py-2 text-sm hover:bg-gray-100 cursor-pointer",
|
|
1781
|
+
onClick: item.action,
|
|
1782
|
+
children: item.label
|
|
1783
|
+
}
|
|
1784
|
+
),
|
|
1785
|
+
index === 1 && /* @__PURE__ */ jsx5("div", { className: "border-t border-gray-200 my-1" })
|
|
1786
|
+
] }, item.label)) });
|
|
1787
|
+
}
|
|
1788
|
+
var ReaContextMenu_default = ReaContextMenu;
|
|
1789
|
+
|
|
1790
|
+
// src/components/rea/ReaGraph.tsx
|
|
1791
|
+
import { toast } from "sonner";
|
|
1792
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1793
|
+
function findElementInGraphData(elementId, elementType, nodes, edges) {
|
|
1794
|
+
if (elementType === "node") {
|
|
1795
|
+
const node = nodes.find((n) => n.id === elementId);
|
|
1796
|
+
if (node) return { element: node, actualElementType: "node" };
|
|
1797
|
+
const edge = edges.find((e) => e.id === elementId);
|
|
1798
|
+
if (edge) {
|
|
1799
|
+
devLog8("[ReGraph] ID mismatch: requested as node but found as edge", {
|
|
1800
|
+
elementId
|
|
1801
|
+
});
|
|
1802
|
+
return { element: edge, actualElementType: "edge" };
|
|
1803
|
+
}
|
|
1804
|
+
} else {
|
|
1805
|
+
const edge = edges.find((e) => e.id === elementId);
|
|
1806
|
+
if (edge) return { element: edge, actualElementType: "edge" };
|
|
1807
|
+
const node = nodes.find((n) => n.id === elementId);
|
|
1808
|
+
if (node) {
|
|
1809
|
+
devLog8("[ReGraph] ID mismatch: requested as edge but found as node", {
|
|
1810
|
+
elementId
|
|
1811
|
+
});
|
|
1812
|
+
return { element: node, actualElementType: "node" };
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
function processFocusAction(focusAction, canvasRef, nodes, edges, setSelectedNodeIds, setSelectedEdge) {
|
|
1818
|
+
const { elementId, elementType, action } = focusAction;
|
|
1819
|
+
const found = findElementInGraphData(elementId, elementType, nodes, edges);
|
|
1820
|
+
if (!found) {
|
|
1821
|
+
devLog8("[ReGraph] Element not found in current Reagraph data", {
|
|
1822
|
+
elementId,
|
|
1823
|
+
elementType,
|
|
1824
|
+
availableNodes: nodes.length,
|
|
1825
|
+
availableEdges: edges.length,
|
|
1826
|
+
nodeIds: nodes.slice(0, 5).map((n) => n.id),
|
|
1827
|
+
edgeIds: edges.slice(0, 5).map((e) => e.id)
|
|
1828
|
+
});
|
|
1829
|
+
return false;
|
|
1830
|
+
}
|
|
1831
|
+
const { element, actualElementType } = found;
|
|
1832
|
+
devLog8("[ReGraph] Element found, focusing", {
|
|
1833
|
+
elementId,
|
|
1834
|
+
elementType: actualElementType
|
|
1835
|
+
});
|
|
1836
|
+
if (actualElementType === "node") {
|
|
1837
|
+
canvasRef.fitNodesInView([elementId]);
|
|
1838
|
+
} else {
|
|
1839
|
+
const edge = element;
|
|
1840
|
+
canvasRef.fitNodesInView([edge.source, edge.target]);
|
|
1841
|
+
}
|
|
1842
|
+
if (action === "focus-and-select") {
|
|
1843
|
+
if (actualElementType === "node") {
|
|
1844
|
+
setSelectedNodeIds([elementId]);
|
|
1845
|
+
} else {
|
|
1846
|
+
const edgeForFocus = edges.find((e) => e.id === elementId);
|
|
1847
|
+
setSelectedEdge(
|
|
1848
|
+
edgeForFocus ? edgeForFocus.data : null
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return true;
|
|
1853
|
+
}
|
|
1854
|
+
function ReaGraph({ onCopyNode, extraToolbarContent }) {
|
|
1855
|
+
const isMountedRef = useRef(false);
|
|
1856
|
+
const { nodes, edges } = useReagraphData();
|
|
1857
|
+
const { normalized } = useLiveGraphData();
|
|
1858
|
+
useEffect2(() => {
|
|
1859
|
+
devLog8("[ReGraph] counts", { nodes: nodes.length, edges: edges.length });
|
|
1860
|
+
if (nodes.length > 0) devLog8("[ReGraph] sample node", nodes[0]);
|
|
1861
|
+
if (edges.length > 0) devLog8("[ReGraph] sample edge", edges[0]);
|
|
1862
|
+
}, [nodes, edges]);
|
|
1863
|
+
const canvas = useRef(null);
|
|
1864
|
+
const [hoveredNode, setHoveredNode] = useState3(
|
|
1865
|
+
void 0
|
|
1866
|
+
);
|
|
1867
|
+
const [layout, setLayout] = useState3("forceDirected2d");
|
|
1868
|
+
const [fullscreen, setFullscreen] = useState3(false);
|
|
1869
|
+
const [pathSelectionType, setPathSelectionType] = useState3("direct");
|
|
1870
|
+
const useGraphSelection = useWorkspaceSelection();
|
|
1871
|
+
const useGraphFilter2 = useWorkspaceGraphFilter();
|
|
1872
|
+
const useGraphExploration = useWorkspaceGraphExploration();
|
|
1873
|
+
const hiddenElementIds = useGraphExploration((s) => s.hiddenElementIds);
|
|
1874
|
+
const isFocusMode = useGraphExploration((s) => s.isFocusMode);
|
|
1875
|
+
const hasHiddenElements = hiddenElementIds.size > 0;
|
|
1876
|
+
const { setSelectedNodeIds, setSelectedEdge } = useGraphSelection(
|
|
1877
|
+
(s) => s
|
|
1878
|
+
);
|
|
1879
|
+
const lastDataChangeTime = useGraphFilter2((s) => s.lastDataChangeTime);
|
|
1880
|
+
const { clear: clearSelectionStore } = useGraphSelection.getState();
|
|
1881
|
+
const {
|
|
1882
|
+
selections,
|
|
1883
|
+
actives,
|
|
1884
|
+
onNodeClick,
|
|
1885
|
+
onCanvasClick,
|
|
1886
|
+
clearSelections,
|
|
1887
|
+
setSelections
|
|
1888
|
+
} = useSelection2({
|
|
1889
|
+
ref: canvas,
|
|
1890
|
+
nodes,
|
|
1891
|
+
edges,
|
|
1892
|
+
pathSelectionType,
|
|
1893
|
+
type: "multi"
|
|
1894
|
+
});
|
|
1895
|
+
useEffect2(() => {
|
|
1896
|
+
const unsubscribe = GraphFocus_default.subscribe((state) => {
|
|
1897
|
+
const focusAction = state.focusAction;
|
|
1898
|
+
if (!focusAction || !canvas.current) return;
|
|
1899
|
+
devLog8("[ReGraph] Processing focus action", {
|
|
1900
|
+
elementId: focusAction.elementId,
|
|
1901
|
+
elementType: focusAction.elementType,
|
|
1902
|
+
action: focusAction.action,
|
|
1903
|
+
timestamp: focusAction.timestamp
|
|
1904
|
+
});
|
|
1905
|
+
const handled = processFocusAction(
|
|
1906
|
+
focusAction,
|
|
1907
|
+
canvas.current,
|
|
1908
|
+
nodes,
|
|
1909
|
+
edges,
|
|
1910
|
+
setSelectedNodeIds,
|
|
1911
|
+
setSelectedEdge
|
|
1912
|
+
);
|
|
1913
|
+
if (handled) {
|
|
1914
|
+
GraphFocus_default.setState({ focusAction: null });
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
return unsubscribe;
|
|
1918
|
+
}, [nodes, edges, setSelectedNodeIds, setSelectedEdge]);
|
|
1919
|
+
useEffect2(() => {
|
|
1920
|
+
if (!canvas.current) return;
|
|
1921
|
+
clearSelections();
|
|
1922
|
+
clearSelectionStore();
|
|
1923
|
+
}, [lastDataChangeTime, clearSelections, clearSelectionStore]);
|
|
1924
|
+
const fitToScreen = useCallback2(
|
|
1925
|
+
(reason) => {
|
|
1926
|
+
if (!isMountedRef.current) return;
|
|
1927
|
+
if (nodes.length === 0) return;
|
|
1928
|
+
requestAnimationFrame(() => {
|
|
1929
|
+
requestAnimationFrame(() => {
|
|
1930
|
+
if (!isMountedRef.current) return;
|
|
1931
|
+
const ref = canvas.current;
|
|
1932
|
+
if (!ref) return;
|
|
1933
|
+
try {
|
|
1934
|
+
ref.fitNodesInView();
|
|
1935
|
+
devLog8("[ReGraph] fitToScreen", {
|
|
1936
|
+
reason,
|
|
1937
|
+
nodeCount: nodes.length
|
|
1938
|
+
});
|
|
1939
|
+
} catch (e) {
|
|
1940
|
+
devLog8("[ReGraph] fitToScreen failed", {
|
|
1941
|
+
reason,
|
|
1942
|
+
error: e.message
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
},
|
|
1948
|
+
[nodes.length]
|
|
1949
|
+
);
|
|
1950
|
+
useEffect2(() => {
|
|
1951
|
+
isMountedRef.current = true;
|
|
1952
|
+
fitToScreen("mount");
|
|
1953
|
+
const state = useGraphSelection.getState();
|
|
1954
|
+
if (state.selectedNodeIds.length || state.selectedEdge) {
|
|
1955
|
+
devLog8("[ReGraph] Initial sync of selection store", {
|
|
1956
|
+
selectedNodeIds: state.selectedNodeIds,
|
|
1957
|
+
selectedEdge: state.selectedEdge
|
|
1958
|
+
});
|
|
1959
|
+
if (state.selectedNodeIds.length > 0) {
|
|
1960
|
+
setSelections(state.selectedNodeIds);
|
|
1961
|
+
}
|
|
1962
|
+
if (state.selectedEdge) {
|
|
1963
|
+
setSelectedEdge(state.selectedEdge);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return () => {
|
|
1967
|
+
isMountedRef.current = false;
|
|
1968
|
+
};
|
|
1969
|
+
}, [fitToScreen, setSelections, setSelectedEdge, useGraphSelection]);
|
|
1970
|
+
useEffect2(() => {
|
|
1971
|
+
if (isMountedRef.current) fitToScreen("counts-changed");
|
|
1972
|
+
}, [nodes.length, edges.length, fitToScreen]);
|
|
1973
|
+
function updateLayout(next) {
|
|
1974
|
+
setLayout(next);
|
|
1975
|
+
fitToScreen("layout-change");
|
|
1976
|
+
}
|
|
1977
|
+
function handleNodeClick(node) {
|
|
1978
|
+
setSelectedEdge(null);
|
|
1979
|
+
if (selections.length < 2 && onNodeClick) onNodeClick(node);
|
|
1980
|
+
}
|
|
1981
|
+
function handleEdgeClick(edge) {
|
|
1982
|
+
const startNode = normalized.nodesById[edge.data.start_id_ + ""];
|
|
1983
|
+
const endNode = normalized.nodesById[edge.data.end_id_ + ""];
|
|
1984
|
+
if (!startNode || !endNode) return;
|
|
1985
|
+
const startNodeId = String(startNode.id_);
|
|
1986
|
+
const endNodeId = String(endNode.id_);
|
|
1987
|
+
const currentSelectedIds = useGraphSelection.getState().selectedNodeIds;
|
|
1988
|
+
if (currentSelectedIds.length === 1) {
|
|
1989
|
+
const selectedNodeId = currentSelectedIds[0];
|
|
1990
|
+
if (selectedNodeId === startNodeId || selectedNodeId === endNodeId) {
|
|
1991
|
+
devLog8("[ReGraph] handleEdgeClick - node+edge selection", {
|
|
1992
|
+
edgeId: edge.id,
|
|
1993
|
+
selectedNode: selectedNodeId,
|
|
1994
|
+
direction: selectedNodeId === startNodeId ? "outgoing" : "incoming"
|
|
1995
|
+
});
|
|
1996
|
+
setSelectedEdge(edge.data);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
devLog8("[ReGraph] handleEdgeClick - default (both endpoints)", {
|
|
2001
|
+
edgeId: edge.id
|
|
2002
|
+
});
|
|
2003
|
+
setSelections([startNodeId, endNodeId]);
|
|
2004
|
+
setSelectedEdge(edge.data);
|
|
2005
|
+
}
|
|
2006
|
+
function handleCanvasClick(e) {
|
|
2007
|
+
clearSelections();
|
|
2008
|
+
setSelectedNodeIds([]);
|
|
2009
|
+
setSelectedEdge(null);
|
|
2010
|
+
if (onCanvasClick) onCanvasClick(e);
|
|
2011
|
+
}
|
|
2012
|
+
useEffect2(() => {
|
|
2013
|
+
if (!isMountedRef.current) return;
|
|
2014
|
+
if (selections.length > 0 && selections.length <= 2) {
|
|
2015
|
+
const selectedNodes = selections.map((id) => nodes.find((n) => n.id === id)).filter(Boolean);
|
|
2016
|
+
setSelectedNodeIds(selectedNodes.map((n) => n.id));
|
|
2017
|
+
}
|
|
2018
|
+
}, [selections, nodes]);
|
|
2019
|
+
useEffect2(() => {
|
|
2020
|
+
if (!isMountedRef.current) return;
|
|
2021
|
+
const currentSelectedIds = useGraphSelection.getState().selectedNodeIds;
|
|
2022
|
+
if (!currentSelectedIds.length) return;
|
|
2023
|
+
const stillPresentIds = currentSelectedIds.filter(
|
|
2024
|
+
(id) => nodes.find((n) => n.id === id)
|
|
2025
|
+
);
|
|
2026
|
+
if (stillPresentIds.length !== currentSelectedIds.length) {
|
|
2027
|
+
clearSelections();
|
|
2028
|
+
setSelectedNodeIds(stillPresentIds);
|
|
2029
|
+
if (stillPresentIds.length < 2) setSelectedEdge(null);
|
|
2030
|
+
}
|
|
2031
|
+
}, [
|
|
2032
|
+
nodes,
|
|
2033
|
+
clearSelections,
|
|
2034
|
+
setSelectedNodeIds,
|
|
2035
|
+
setSelectedEdge,
|
|
2036
|
+
useGraphSelection
|
|
2037
|
+
]);
|
|
2038
|
+
useEffect2(() => {
|
|
2039
|
+
if (!isMountedRef.current) return;
|
|
2040
|
+
const currentSelectedIds = useGraphSelection.getState().selectedNodeIds;
|
|
2041
|
+
if (!currentSelectedIds.length) return;
|
|
2042
|
+
const stillPresentIds = currentSelectedIds.filter(
|
|
2043
|
+
(id) => nodes.find((n) => n.id === id)
|
|
2044
|
+
);
|
|
2045
|
+
if (stillPresentIds.length !== currentSelectedIds.length) {
|
|
2046
|
+
setSelectedNodeIds(stillPresentIds);
|
|
2047
|
+
if (stillPresentIds.length < 2) setSelectedEdge(null);
|
|
2048
|
+
}
|
|
2049
|
+
}, [nodes, setSelectedNodeIds, setSelectedEdge, useGraphSelection]);
|
|
2050
|
+
useEffect2(() => {
|
|
2051
|
+
if (!fullscreen) return;
|
|
2052
|
+
const prevOverflow = document.body.style.overflow;
|
|
2053
|
+
document.body.style.overflow = "hidden";
|
|
2054
|
+
fitToScreen("enter-fullscreen");
|
|
2055
|
+
const handleKey = (e) => {
|
|
2056
|
+
if (e.key === "Escape") setFullscreen(false);
|
|
2057
|
+
};
|
|
2058
|
+
window.addEventListener("keydown", handleKey);
|
|
2059
|
+
return () => {
|
|
2060
|
+
document.body.style.overflow = prevOverflow;
|
|
2061
|
+
window.removeEventListener("keydown", handleKey);
|
|
2062
|
+
};
|
|
2063
|
+
}, [fullscreen, fitToScreen]);
|
|
2064
|
+
useEffect2(() => {
|
|
2065
|
+
if (!fullscreen) return;
|
|
2066
|
+
const id = setTimeout(() => {
|
|
2067
|
+
if (canvas.current) {
|
|
2068
|
+
fitToScreen("fullscreen-resize-delay");
|
|
2069
|
+
}
|
|
2070
|
+
}, 50);
|
|
2071
|
+
return () => clearTimeout(id);
|
|
2072
|
+
}, [fullscreen, fitToScreen]);
|
|
2073
|
+
useEffect2(() => {
|
|
2074
|
+
if (!isMountedRef.current) return;
|
|
2075
|
+
const handleResize = () => {
|
|
2076
|
+
if (isMountedRef.current && canvas.current) {
|
|
2077
|
+
fitToScreen("window-resize");
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
window.addEventListener("resize", handleResize);
|
|
2081
|
+
return () => {
|
|
2082
|
+
window.removeEventListener("resize", handleResize);
|
|
2083
|
+
};
|
|
2084
|
+
}, [fitToScreen]);
|
|
2085
|
+
const firstDataRef = useRef(true);
|
|
2086
|
+
useEffect2(() => {
|
|
2087
|
+
if (!isMountedRef.current) return;
|
|
2088
|
+
if (nodes.length > 0) {
|
|
2089
|
+
if (firstDataRef.current) {
|
|
2090
|
+
fitToScreen("first-data");
|
|
2091
|
+
firstDataRef.current = false;
|
|
2092
|
+
}
|
|
2093
|
+
} else {
|
|
2094
|
+
firstDataRef.current = true;
|
|
2095
|
+
}
|
|
2096
|
+
}, [nodes.length, fitToScreen]);
|
|
2097
|
+
if (nodes.length === 0) {
|
|
2098
|
+
devLog8("[ReGraph] empty state render");
|
|
2099
|
+
return /* @__PURE__ */ jsxs5(Card2, { className: "relative w-full h-full items-center justify-center text-slate-500 gap-2 shadow-xs p-4", children: [
|
|
2100
|
+
/* @__PURE__ */ jsx6("div", { children: "No data to display." }),
|
|
2101
|
+
/* @__PURE__ */ jsxs5("div", { children: [
|
|
2102
|
+
"Please",
|
|
2103
|
+
" ",
|
|
2104
|
+
/* @__PURE__ */ jsx6("span", { className: "font-semibold", children: "run a query, check filters, or add a node" }),
|
|
2105
|
+
" ",
|
|
2106
|
+
"to the workbench to populate the graph."
|
|
2107
|
+
] })
|
|
2108
|
+
] });
|
|
2109
|
+
}
|
|
2110
|
+
return /* @__PURE__ */ jsxs5(
|
|
2111
|
+
"div",
|
|
2112
|
+
{
|
|
2113
|
+
className: cn(
|
|
2114
|
+
"bg-white",
|
|
2115
|
+
fullscreen ? "fixed inset-0 z-50 w-screen h-screen p-0 m-0" : "relative w-full h-full"
|
|
2116
|
+
),
|
|
2117
|
+
children: [
|
|
2118
|
+
/* @__PURE__ */ jsx6(Card2, { className: "h-full shadow-xs p-4", children: /* @__PURE__ */ jsx6(
|
|
2119
|
+
GraphCanvas,
|
|
2120
|
+
{
|
|
2121
|
+
ref: canvas,
|
|
2122
|
+
theme,
|
|
2123
|
+
nodes,
|
|
2124
|
+
edges,
|
|
2125
|
+
draggable: true,
|
|
2126
|
+
layoutType: layout,
|
|
2127
|
+
selections,
|
|
2128
|
+
actives,
|
|
2129
|
+
onCanvasClick: handleCanvasClick,
|
|
2130
|
+
onNodeClick: (e) => handleNodeClick(e),
|
|
2131
|
+
edgeArrowPosition: "end",
|
|
2132
|
+
edgeLabelPosition: "natural",
|
|
2133
|
+
labelType: "all",
|
|
2134
|
+
onNodePointerOver: setHoveredNode,
|
|
2135
|
+
onNodePointerOut: () => setHoveredNode(void 0),
|
|
2136
|
+
onEdgeClick: handleEdgeClick,
|
|
2137
|
+
contextMenu: ({ data, onClose }) => {
|
|
2138
|
+
const elementType = "source" in data ? "edge" : "node";
|
|
2139
|
+
devLog8("[ReGraph] Context menu opened", {
|
|
2140
|
+
elementId: data.id,
|
|
2141
|
+
elementType
|
|
2142
|
+
});
|
|
2143
|
+
return /* @__PURE__ */ jsx6(
|
|
2144
|
+
ReaContextMenu_default,
|
|
2145
|
+
{
|
|
2146
|
+
data,
|
|
2147
|
+
onClose,
|
|
2148
|
+
elementType,
|
|
2149
|
+
useGraphExploration,
|
|
2150
|
+
onCopyNode
|
|
2151
|
+
}
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
) }),
|
|
2156
|
+
hoveredNode && /* @__PURE__ */ jsx6(NodeHoverInfo_default, { node: hoveredNode }),
|
|
2157
|
+
/* @__PURE__ */ jsx6(
|
|
2158
|
+
LayoutAndCameraControls_default,
|
|
2159
|
+
{
|
|
2160
|
+
fitToScreen,
|
|
2161
|
+
fullscreen,
|
|
2162
|
+
toggleFullscreen: () => setFullscreen((prev) => !prev),
|
|
2163
|
+
updateLayout,
|
|
2164
|
+
setPathSelectionType,
|
|
2165
|
+
hasHiddenElements,
|
|
2166
|
+
isFocusMode,
|
|
2167
|
+
onResetView: () => {
|
|
2168
|
+
useGraphExploration.getState().resetView();
|
|
2169
|
+
toast.success(
|
|
2170
|
+
isFocusMode ? "Focus mode cleared" : "Hidden elements restored"
|
|
2171
|
+
);
|
|
2172
|
+
},
|
|
2173
|
+
extraToolbarContent
|
|
2174
|
+
}
|
|
2175
|
+
)
|
|
2176
|
+
]
|
|
2177
|
+
}
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
var ReaGraph_default = ReaGraph;
|
|
2181
|
+
|
|
2182
|
+
// src/components/cyto/CytoscapeGraph.tsx
|
|
2183
|
+
import {
|
|
2184
|
+
useEffect as useEffect3,
|
|
2185
|
+
useRef as useRef2,
|
|
2186
|
+
useState as useState4,
|
|
2187
|
+
useCallback as useCallback3
|
|
2188
|
+
} from "react";
|
|
2189
|
+
import { devLog as devLog10, errorLog as errorLog2 } from "@petrarca/sonnet-core";
|
|
2190
|
+
import cytoscape from "cytoscape";
|
|
2191
|
+
import fcose from "cytoscape-fcose";
|
|
2192
|
+
import cola from "cytoscape-cola";
|
|
2193
|
+
import dagre from "cytoscape-dagre";
|
|
2194
|
+
import klay from "cytoscape-klay";
|
|
2195
|
+
import elk from "cytoscape-elk";
|
|
2196
|
+
import cise from "cytoscape-cise";
|
|
2197
|
+
import cxtmenu from "cytoscape-cxtmenu";
|
|
2198
|
+
|
|
2199
|
+
// src/components/cyto/CytoscapeControls.tsx
|
|
2200
|
+
import {
|
|
2201
|
+
Button as Button3,
|
|
2202
|
+
DropdownMenu as DropdownMenu3,
|
|
2203
|
+
DropdownMenuContent as DropdownMenuContent3,
|
|
2204
|
+
DropdownMenuItem as DropdownMenuItem3,
|
|
2205
|
+
DropdownMenuLabel as DropdownMenuLabel2,
|
|
2206
|
+
DropdownMenuSeparator as DropdownMenuSeparator2,
|
|
2207
|
+
DropdownMenuTrigger as DropdownMenuTrigger3,
|
|
2208
|
+
SimpleTooltip as SimpleTooltip3
|
|
2209
|
+
} from "@petrarca/sonnet-ui";
|
|
2210
|
+
import { LayoutPanelTop as LayoutPanelTop2, Monitor, Frame, Eye as Eye2, Minus } from "lucide-react";
|
|
2211
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2212
|
+
function ViewControls2({
|
|
2213
|
+
zoomIn,
|
|
2214
|
+
zoomOut,
|
|
2215
|
+
fullscreen,
|
|
2216
|
+
toggleFullscreen,
|
|
2217
|
+
fitToScreen,
|
|
2218
|
+
hasHiddenElements,
|
|
2219
|
+
isFocusMode,
|
|
2220
|
+
onResetView
|
|
2221
|
+
}) {
|
|
2222
|
+
return /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
2223
|
+
/* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Zoom In", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: zoomIn, children: /* @__PURE__ */ jsx7("span", { style: { fontWeight: 600 }, children: "+" }) }) }),
|
|
2224
|
+
/* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Zoom Out", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: zoomOut, children: /* @__PURE__ */ jsx7(Minus, {}) }) }),
|
|
2225
|
+
/* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Fullscreen", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: toggleFullscreen, children: !fullscreen ? /* @__PURE__ */ jsx7(Monitor, {}) : /* @__PURE__ */ jsx7(Frame, {}) }) }),
|
|
2226
|
+
/* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Fit to screen", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: fitToScreen, children: /* @__PURE__ */ jsx7(Frame, {}) }) }),
|
|
2227
|
+
(hasHiddenElements || isFocusMode) && onResetView && /* @__PURE__ */ jsx7(
|
|
2228
|
+
SimpleTooltip3,
|
|
2229
|
+
{
|
|
2230
|
+
label: isFocusMode ? "Reset Focus" : "Show All Hidden",
|
|
2231
|
+
side: "left",
|
|
2232
|
+
children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: onResetView, children: /* @__PURE__ */ jsx7(Eye2, {}) })
|
|
2233
|
+
}
|
|
2234
|
+
)
|
|
2235
|
+
] });
|
|
2236
|
+
}
|
|
2237
|
+
var LAYOUT_GROUPS = [
|
|
2238
|
+
{
|
|
2239
|
+
label: "Force-directed Layouts",
|
|
2240
|
+
items: [
|
|
2241
|
+
{ layout: "cola", name: "Cola (Best for Knowledge Graphs)" },
|
|
2242
|
+
{ layout: "fcose", name: "F-CoSE (Large Graphs)" },
|
|
2243
|
+
{ layout: "cise", name: "CiSE (Clustered Circles)" },
|
|
2244
|
+
{ layout: "cose", name: "CoSE" }
|
|
2245
|
+
]
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
label: "Hierarchical Layouts",
|
|
2249
|
+
items: [
|
|
2250
|
+
{ layout: "dagre", name: "Dagre (Left to Right)" },
|
|
2251
|
+
{ layout: "klay", name: "KLay (Layered)" },
|
|
2252
|
+
{ layout: "elk", name: "ELK (Layered)" },
|
|
2253
|
+
{ layout: "breadthfirst", name: "Breadth-first" },
|
|
2254
|
+
{ layout: "tree", name: "Tree (Top-Down)" }
|
|
2255
|
+
]
|
|
2256
|
+
},
|
|
2257
|
+
{
|
|
2258
|
+
label: "Other Layouts",
|
|
2259
|
+
items: [
|
|
2260
|
+
{ layout: "concentric", name: "Concentric" },
|
|
2261
|
+
{ layout: "circle", name: "Circle" },
|
|
2262
|
+
{ layout: "grid", name: "Grid" }
|
|
2263
|
+
]
|
|
2264
|
+
}
|
|
2265
|
+
];
|
|
2266
|
+
function LayoutDropdown2({ currentLayout, updateLayout }) {
|
|
2267
|
+
return /* @__PURE__ */ jsxs6(DropdownMenu3, { children: [
|
|
2268
|
+
/* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Layout", side: "left", children: /* @__PURE__ */ jsx7(DropdownMenuTrigger3, { asChild: true, children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx7(LayoutPanelTop2, {}) }) }) }),
|
|
2269
|
+
/* @__PURE__ */ jsx7(DropdownMenuContent3, { side: "left", align: "start", children: LAYOUT_GROUPS.map((group, groupIdx) => /* @__PURE__ */ jsxs6("div", { children: [
|
|
2270
|
+
groupIdx > 0 && /* @__PURE__ */ jsx7(DropdownMenuSeparator2, {}),
|
|
2271
|
+
/* @__PURE__ */ jsx7(DropdownMenuLabel2, { children: group.label }),
|
|
2272
|
+
group.items.map((item) => /* @__PURE__ */ jsx7(
|
|
2273
|
+
DropdownMenuItem3,
|
|
2274
|
+
{
|
|
2275
|
+
onClick: () => updateLayout(item.layout),
|
|
2276
|
+
className: currentLayout === item.layout ? "text-blue-600" : "",
|
|
2277
|
+
children: item.name
|
|
2278
|
+
},
|
|
2279
|
+
item.layout
|
|
2280
|
+
))
|
|
2281
|
+
] }, group.label)) })
|
|
2282
|
+
] });
|
|
2283
|
+
}
|
|
2284
|
+
function CytoscapeControls({
|
|
2285
|
+
fitToScreen,
|
|
2286
|
+
hasHiddenElements,
|
|
2287
|
+
isFocusMode,
|
|
2288
|
+
updateLayout,
|
|
2289
|
+
zoomIn,
|
|
2290
|
+
zoomOut,
|
|
2291
|
+
currentLayout,
|
|
2292
|
+
fullscreen,
|
|
2293
|
+
toggleFullscreen,
|
|
2294
|
+
onResetView,
|
|
2295
|
+
extraToolbarContent
|
|
2296
|
+
}) {
|
|
2297
|
+
return /* @__PURE__ */ jsxs6("div", { className: "absolute right-2 top-2 flex flex-col border bg-white rounded-md shadow-sm z-10", children: [
|
|
2298
|
+
/* @__PURE__ */ jsx7(RendererDropdown, {}),
|
|
2299
|
+
/* @__PURE__ */ jsx7(
|
|
2300
|
+
ViewControls2,
|
|
2301
|
+
{
|
|
2302
|
+
zoomIn,
|
|
2303
|
+
zoomOut,
|
|
2304
|
+
fullscreen,
|
|
2305
|
+
toggleFullscreen,
|
|
2306
|
+
fitToScreen,
|
|
2307
|
+
hasHiddenElements,
|
|
2308
|
+
isFocusMode,
|
|
2309
|
+
onResetView
|
|
2310
|
+
}
|
|
2311
|
+
),
|
|
2312
|
+
extraToolbarContent,
|
|
2313
|
+
/* @__PURE__ */ jsx7(
|
|
2314
|
+
LayoutDropdown2,
|
|
2315
|
+
{
|
|
2316
|
+
currentLayout,
|
|
2317
|
+
updateLayout
|
|
2318
|
+
}
|
|
2319
|
+
)
|
|
2320
|
+
] });
|
|
2321
|
+
}
|
|
2322
|
+
var CytoscapeControls_default = CytoscapeControls;
|
|
2323
|
+
|
|
2324
|
+
// src/components/cyto/layoutPresets.ts
|
|
2325
|
+
function buildColaLayout(count) {
|
|
2326
|
+
return {
|
|
2327
|
+
name: "cola",
|
|
2328
|
+
// Always animate regardless of node count for better user experience
|
|
2329
|
+
animate: true,
|
|
2330
|
+
// Increase refresh rate for smoother animation
|
|
2331
|
+
refresh: 2,
|
|
2332
|
+
// Limit simulation time to prevent hanging with large graphs
|
|
2333
|
+
maxSimulationTime: 2e3,
|
|
2334
|
+
// Prevent grabbing during simulation for better performance
|
|
2335
|
+
ungrabifyWhileSimulating: true,
|
|
2336
|
+
fit: true,
|
|
2337
|
+
padding: 50,
|
|
2338
|
+
// Scale spacing based on node count for better distribution
|
|
2339
|
+
nodeSpacing: () => Math.max(10, 30 - Math.log(count) * 3),
|
|
2340
|
+
edgeLength: () => Math.max(80, 150 - Math.log(count) * 10),
|
|
2341
|
+
// Always avoid node overlap
|
|
2342
|
+
avoidOverlap: true,
|
|
2343
|
+
// Prevent infinite running
|
|
2344
|
+
infinite: false,
|
|
2345
|
+
// Add convergence threshold for faster completion with large graphs
|
|
2346
|
+
convergenceThreshold: 0.01,
|
|
2347
|
+
// Add randomize false to maintain consistent layouts
|
|
2348
|
+
randomize: false
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
function buildDagreLayout(count) {
|
|
2352
|
+
return {
|
|
2353
|
+
name: "dagre",
|
|
2354
|
+
rankDir: "LR",
|
|
2355
|
+
// Left to right
|
|
2356
|
+
// Much more compact separation for tree-like hierarchical layouts
|
|
2357
|
+
rankSep: Math.min(80, 50 + Math.sqrt(count) * 1.5),
|
|
2358
|
+
// Further reduced
|
|
2359
|
+
nodeSep: Math.min(40, 25 + Math.sqrt(count) * 0.8),
|
|
2360
|
+
// Further reduced
|
|
2361
|
+
edgeSep: Math.min(10, 5 + Math.log(count) * 0.5),
|
|
2362
|
+
// Further reduced
|
|
2363
|
+
// Always animate for better user experience
|
|
2364
|
+
animate: true,
|
|
2365
|
+
fit: true,
|
|
2366
|
+
padding: 30
|
|
2367
|
+
// Reduced for more compact layout
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
function buildTreeLayout(count) {
|
|
2371
|
+
return {
|
|
2372
|
+
name: "breadthfirst",
|
|
2373
|
+
fit: true,
|
|
2374
|
+
padding: 30,
|
|
2375
|
+
// Reduced for more compact layout
|
|
2376
|
+
animate: true,
|
|
2377
|
+
directed: true,
|
|
2378
|
+
spacingFactor: Math.min(0.9, 0.7 + 60 / (count + 50)),
|
|
2379
|
+
// Compact spacing: 0.7 to 0.9
|
|
2380
|
+
circle: false
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
function buildKlayLayout(count) {
|
|
2384
|
+
return {
|
|
2385
|
+
name: "klay",
|
|
2386
|
+
nodeDimensionsIncludeLabels: true,
|
|
2387
|
+
fit: true,
|
|
2388
|
+
padding: 30,
|
|
2389
|
+
// Reduced for more compact layout
|
|
2390
|
+
animate: true,
|
|
2391
|
+
animationDuration: 500,
|
|
2392
|
+
klay: {
|
|
2393
|
+
direction: "RIGHT",
|
|
2394
|
+
spacing: Math.min(30, 12 + Math.sqrt(count) * 1.5),
|
|
2395
|
+
// Further reduced for much denser layout
|
|
2396
|
+
nodeLayering: "NETWORK_SIMPLEX",
|
|
2397
|
+
layoutHierarchy: true,
|
|
2398
|
+
// Increase thoroughness a bit for nicer layering on medium graphs
|
|
2399
|
+
thoroughness: count < 400 ? 30 : 10
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
function buildFcoseLayout(count) {
|
|
2404
|
+
return {
|
|
2405
|
+
name: "fcose",
|
|
2406
|
+
animate: true,
|
|
2407
|
+
animationDuration: 600,
|
|
2408
|
+
fit: true,
|
|
2409
|
+
padding: 50,
|
|
2410
|
+
// Scale edge length based on node count
|
|
2411
|
+
idealEdgeLength: Math.min(150, 80 + Math.sqrt(count) * 3),
|
|
2412
|
+
// Scale repulsion based on node count
|
|
2413
|
+
nodeRepulsion: Math.min(3e4, 4e3 + count * 50),
|
|
2414
|
+
// Scale separation based on node count
|
|
2415
|
+
nodeSeparation: Math.min(100, 50 + Math.sqrt(count) * 2),
|
|
2416
|
+
// Adjust gravity based on node count
|
|
2417
|
+
gravity: 0.3,
|
|
2418
|
+
// Never randomize for consistent layouts
|
|
2419
|
+
randomize: false,
|
|
2420
|
+
// Consistent energy for all graph sizes
|
|
2421
|
+
initialEnergyOnIncremental: 0.5,
|
|
2422
|
+
// Always use sampling for better performance
|
|
2423
|
+
samplingType: true,
|
|
2424
|
+
// Use default quality for best balance
|
|
2425
|
+
quality: "default",
|
|
2426
|
+
// Limit iterations for large graphs
|
|
2427
|
+
numIter: Math.min(2500, 1e3 + count * 5),
|
|
2428
|
+
// Use consistent cooling factor
|
|
2429
|
+
coolingFactor: 0.95,
|
|
2430
|
+
// Prevent infinite running
|
|
2431
|
+
infinite: false
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
function buildCiseLayout(count, nodeData) {
|
|
2435
|
+
const labelToIds = {};
|
|
2436
|
+
nodeData.forEach((n) => {
|
|
2437
|
+
const label = n.data?.label_ || "Unknown";
|
|
2438
|
+
if (!labelToIds[label]) labelToIds[label] = [];
|
|
2439
|
+
labelToIds[label].push(n.id);
|
|
2440
|
+
});
|
|
2441
|
+
const clusters = Object.values(labelToIds).filter((arr) => arr.length > 0);
|
|
2442
|
+
return {
|
|
2443
|
+
name: "cise",
|
|
2444
|
+
animate: true,
|
|
2445
|
+
animationDuration: 600,
|
|
2446
|
+
clusters,
|
|
2447
|
+
nodeSeparation: Math.min(40, 20 + Math.sqrt(count)),
|
|
2448
|
+
idealInterClusterEdgeLengthCoefficient: 1.2,
|
|
2449
|
+
allowNodesInsideCircle: false,
|
|
2450
|
+
maxRatioOfNodesInsideCircle: 0.1,
|
|
2451
|
+
springCoeff: 0.45,
|
|
2452
|
+
nodeRepulsion: 4500,
|
|
2453
|
+
gravity: 0.25,
|
|
2454
|
+
gravityRange: 3.8,
|
|
2455
|
+
fit: true,
|
|
2456
|
+
padding: 50
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
function buildElkLayout(count) {
|
|
2460
|
+
return {
|
|
2461
|
+
name: "elk",
|
|
2462
|
+
// elk algorithm options under `elk` key
|
|
2463
|
+
elk: {
|
|
2464
|
+
algorithm: "layered",
|
|
2465
|
+
"elk.direction": "RIGHT",
|
|
2466
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": Math.min(
|
|
2467
|
+
150,
|
|
2468
|
+
60 + Math.sqrt(count) * 10
|
|
2469
|
+
),
|
|
2470
|
+
"elk.spacing.nodeNode": Math.min(120, 40 + Math.sqrt(count) * 8),
|
|
2471
|
+
"elk.layered.crossingMinimization.semiInteractive": "true"
|
|
2472
|
+
},
|
|
2473
|
+
fit: true,
|
|
2474
|
+
padding: 50,
|
|
2475
|
+
animate: true,
|
|
2476
|
+
animationDuration: 500
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
function buildCoseLayout(count) {
|
|
2480
|
+
return {
|
|
2481
|
+
name: "cose",
|
|
2482
|
+
// Always animate for better user experience
|
|
2483
|
+
animate: true,
|
|
2484
|
+
fit: true,
|
|
2485
|
+
padding: 50,
|
|
2486
|
+
// Scale overlap based on node count
|
|
2487
|
+
nodeOverlap: Math.min(30, 15 + Math.sqrt(count)),
|
|
2488
|
+
// Scale spacing based on node count
|
|
2489
|
+
componentSpacing: Math.min(150, 80 + Math.sqrt(count) * 2),
|
|
2490
|
+
// Increase refresh for smoother animation
|
|
2491
|
+
refresh: 10,
|
|
2492
|
+
// Scale parameters based on node count
|
|
2493
|
+
idealEdgeLength: () => Math.min(150, 80 + Math.sqrt(count) * 2),
|
|
2494
|
+
edgeElasticity: () => Math.min(150, 80 + Math.sqrt(count) * 2),
|
|
2495
|
+
nodeRepulsion: () => Math.min(6e5, 2e5 + count * 1e3),
|
|
2496
|
+
nestingFactor: 1.2,
|
|
2497
|
+
// Adjust gravity based on node count
|
|
2498
|
+
gravity: Math.min(100, 60 + Math.sqrt(count)),
|
|
2499
|
+
// Limit iterations for large graphs
|
|
2500
|
+
numIter: Math.min(2500, 1e3 + count * 5),
|
|
2501
|
+
// Use consistent cooling factor
|
|
2502
|
+
coolingFactor: 0.95
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
function buildConcentricLayout(count) {
|
|
2506
|
+
return {
|
|
2507
|
+
name: "concentric",
|
|
2508
|
+
fit: true,
|
|
2509
|
+
padding: 50,
|
|
2510
|
+
// Always animate for better user experience
|
|
2511
|
+
animate: true,
|
|
2512
|
+
// Scale spacing based on node count
|
|
2513
|
+
minNodeSpacing: Math.min(60, 40 + Math.log(count) * 3),
|
|
2514
|
+
// Adjust level width based on node count
|
|
2515
|
+
levelWidth: () => Math.max(1, 3 - Math.log(count) * 0.3),
|
|
2516
|
+
// Use degree for concentric layout
|
|
2517
|
+
concentric: (node) => node.degree(),
|
|
2518
|
+
// Start from outside for better distribution
|
|
2519
|
+
startAngle: Math.PI * 1.5
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
function buildBreadthfirstLayout(count) {
|
|
2523
|
+
return {
|
|
2524
|
+
name: "breadthfirst",
|
|
2525
|
+
fit: true,
|
|
2526
|
+
padding: 50,
|
|
2527
|
+
// Always animate for better user experience
|
|
2528
|
+
animate: true,
|
|
2529
|
+
directed: true,
|
|
2530
|
+
// Scale spacing based on node count
|
|
2531
|
+
spacingFactor: Math.min(1.8, 1.2 + 100 / (count + 50)),
|
|
2532
|
+
// Add circle option for better distribution with smaller graphs
|
|
2533
|
+
circle: count < 100
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
function buildCircleLayout(count) {
|
|
2537
|
+
return {
|
|
2538
|
+
name: "circle",
|
|
2539
|
+
fit: true,
|
|
2540
|
+
padding: 50,
|
|
2541
|
+
// Always animate for better user experience
|
|
2542
|
+
animate: true,
|
|
2543
|
+
// Scale radius based on node count
|
|
2544
|
+
radius: Math.min(500, count * 3 + 50),
|
|
2545
|
+
// Add startAngle for consistent layout
|
|
2546
|
+
startAngle: Math.PI / 2
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
function buildGridLayout(count) {
|
|
2550
|
+
const approxCols = Math.ceil(Math.sqrt(count * 1.5));
|
|
2551
|
+
const cols = Math.max(approxCols, 1);
|
|
2552
|
+
const rows = Math.max(Math.ceil(count / cols), 1);
|
|
2553
|
+
return {
|
|
2554
|
+
name: "grid",
|
|
2555
|
+
fit: true,
|
|
2556
|
+
padding: 40,
|
|
2557
|
+
avoidOverlap: true,
|
|
2558
|
+
// Extra space around each node beyond actual dimensions
|
|
2559
|
+
avoidOverlapPadding: Math.min(80, 20 + Math.log(count + 1) * 10),
|
|
2560
|
+
// Do NOT condense; we want uniform padding for labels
|
|
2561
|
+
condense: false,
|
|
2562
|
+
animate: true,
|
|
2563
|
+
animationDuration: 400,
|
|
2564
|
+
rows,
|
|
2565
|
+
cols
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
var layoutFactories = {
|
|
2569
|
+
cola: buildColaLayout,
|
|
2570
|
+
dagre: buildDagreLayout,
|
|
2571
|
+
tree: buildTreeLayout,
|
|
2572
|
+
klay: buildKlayLayout,
|
|
2573
|
+
fcose: buildFcoseLayout,
|
|
2574
|
+
cise: buildCiseLayout,
|
|
2575
|
+
elk: buildElkLayout,
|
|
2576
|
+
cose: buildCoseLayout,
|
|
2577
|
+
concentric: buildConcentricLayout,
|
|
2578
|
+
breadthfirst: buildBreadthfirstLayout,
|
|
2579
|
+
circle: buildCircleLayout,
|
|
2580
|
+
grid: buildGridLayout
|
|
2581
|
+
};
|
|
2582
|
+
function getLayoutPreset(options) {
|
|
2583
|
+
let { layoutName } = options;
|
|
2584
|
+
const { count, nodeData = [] } = options;
|
|
2585
|
+
if (layoutName === "auto") {
|
|
2586
|
+
layoutName = "cola";
|
|
2587
|
+
}
|
|
2588
|
+
const factory = layoutFactories[layoutName] ?? layoutFactories.grid;
|
|
2589
|
+
return factory(count, nodeData);
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// src/components/cyto/contextMenuConfig.ts
|
|
2593
|
+
import { devLog as devLog9, errorLog } from "@petrarca/sonnet-core";
|
|
2594
|
+
import { toast as toast2 } from "sonner";
|
|
2595
|
+
var createNodeContextMenu = ({
|
|
2596
|
+
cy,
|
|
2597
|
+
useGraphExploration,
|
|
2598
|
+
onCopyNode
|
|
2599
|
+
}) => {
|
|
2600
|
+
devLog9("[CxtMenu] Initializing node context menu");
|
|
2601
|
+
return cy.cxtmenu({
|
|
2602
|
+
selector: "node",
|
|
2603
|
+
commands: [
|
|
2604
|
+
{
|
|
2605
|
+
content: "Focus",
|
|
2606
|
+
select: (ele) => {
|
|
2607
|
+
const nodeId = ele.id();
|
|
2608
|
+
devLog9("[CxtMenu] Focus on node and neighborhood:", nodeId);
|
|
2609
|
+
useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId);
|
|
2610
|
+
}
|
|
2611
|
+
},
|
|
2612
|
+
{
|
|
2613
|
+
content: "Copy",
|
|
2614
|
+
select: (ele) => {
|
|
2615
|
+
devLog9("[CxtMenu] Copy node to clipboard");
|
|
2616
|
+
const visualGraph = ele.data("visualGraph");
|
|
2617
|
+
if (visualGraph) {
|
|
2618
|
+
onCopyNode?.(visualGraph);
|
|
2619
|
+
} else {
|
|
2620
|
+
errorLog("[CxtMenu] visualGraph not found in element data");
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
},
|
|
2624
|
+
{
|
|
2625
|
+
content: "Hide Node",
|
|
2626
|
+
select: (ele) => {
|
|
2627
|
+
const nodeId = ele.id();
|
|
2628
|
+
devLog9("[CxtMenu] Hide node:", nodeId);
|
|
2629
|
+
useGraphExploration.getState().hideElements([nodeId]);
|
|
2630
|
+
}
|
|
2631
|
+
},
|
|
2632
|
+
{
|
|
2633
|
+
content: "Hide Incoming",
|
|
2634
|
+
select: (ele) => {
|
|
2635
|
+
const nodeId = ele.id();
|
|
2636
|
+
devLog9("[CxtMenu] Hide incoming (predecessors) of node:", nodeId);
|
|
2637
|
+
useGraphExploration.getState().hideNodeIncoming(nodeId);
|
|
2638
|
+
}
|
|
2639
|
+
},
|
|
2640
|
+
{
|
|
2641
|
+
content: "Hide Outgoing",
|
|
2642
|
+
select: (ele) => {
|
|
2643
|
+
const nodeId = ele.id();
|
|
2644
|
+
devLog9("[CxtMenu] Hide outgoing (successors) of node:", nodeId);
|
|
2645
|
+
useGraphExploration.getState().hideNodeOutgoing(nodeId);
|
|
2646
|
+
}
|
|
2647
|
+
},
|
|
2648
|
+
{
|
|
2649
|
+
content: "Expand",
|
|
2650
|
+
select: async (ele) => {
|
|
2651
|
+
const nodeId = ele.id();
|
|
2652
|
+
devLog9("[CxtMenu] Expanding node:", nodeId);
|
|
2653
|
+
try {
|
|
2654
|
+
await useGraphExploration.getState().expandNode(nodeId);
|
|
2655
|
+
devLog9("[CxtMenu] Expansion complete");
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
errorLog("[CxtMenu] Expansion failed:", error);
|
|
2658
|
+
toast2.error("Failed to expand node");
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
],
|
|
2663
|
+
menuRadius: 60,
|
|
2664
|
+
// Increased for more items (default is 100)
|
|
2665
|
+
fillColor: "rgba(0, 0, 0, 0.75)",
|
|
2666
|
+
activeFillColor: "rgba(59, 130, 246, 0.75)",
|
|
2667
|
+
// blue-500
|
|
2668
|
+
activePadding: 10,
|
|
2669
|
+
indicatorSize: 12,
|
|
2670
|
+
separatorWidth: 2,
|
|
2671
|
+
spotlightPadding: 2,
|
|
2672
|
+
adaptativeNodeSpotlightRadius: true,
|
|
2673
|
+
minSpotlightRadius: 12,
|
|
2674
|
+
maxSpotlightRadius: 20,
|
|
2675
|
+
openMenuEvents: "cxttapstart taphold",
|
|
2676
|
+
itemColor: "white",
|
|
2677
|
+
itemTextShadowColor: "transparent",
|
|
2678
|
+
zIndex: 9999,
|
|
2679
|
+
atMouse: false
|
|
2680
|
+
});
|
|
2681
|
+
};
|
|
2682
|
+
var createEdgeContextMenu = ({
|
|
2683
|
+
cy,
|
|
2684
|
+
useGraphExploration
|
|
2685
|
+
}) => {
|
|
2686
|
+
return cy.cxtmenu({
|
|
2687
|
+
selector: "edge",
|
|
2688
|
+
commands: [
|
|
2689
|
+
{
|
|
2690
|
+
content: "Focus",
|
|
2691
|
+
select: (ele) => {
|
|
2692
|
+
const edgeId = ele.id();
|
|
2693
|
+
devLog9(
|
|
2694
|
+
"[CxtMenu] Focus on edge (auto-direction based on selection):",
|
|
2695
|
+
edgeId
|
|
2696
|
+
);
|
|
2697
|
+
useGraphExploration.getState().focusEdge(edgeId);
|
|
2698
|
+
}
|
|
2699
|
+
},
|
|
2700
|
+
{
|
|
2701
|
+
content: "Focus Edge Type",
|
|
2702
|
+
select: (ele) => {
|
|
2703
|
+
const edgeId = ele.id();
|
|
2704
|
+
devLog9("[CxtMenu] Focus on edge type:", edgeId);
|
|
2705
|
+
useGraphExploration.getState().focusOnEdgeType(edgeId);
|
|
2706
|
+
}
|
|
2707
|
+
},
|
|
2708
|
+
{
|
|
2709
|
+
content: "Hide Forward Path",
|
|
2710
|
+
select: (ele) => {
|
|
2711
|
+
const edgeId = ele.id();
|
|
2712
|
+
devLog9(
|
|
2713
|
+
"[CxtMenu] Hide forward path (edge + target + downstream):",
|
|
2714
|
+
edgeId
|
|
2715
|
+
);
|
|
2716
|
+
useGraphExploration.getState().hideEdgeForwardPath(edgeId);
|
|
2717
|
+
}
|
|
2718
|
+
},
|
|
2719
|
+
{
|
|
2720
|
+
content: "Hide Reverse Path",
|
|
2721
|
+
select: (ele) => {
|
|
2722
|
+
const edgeId = ele.id();
|
|
2723
|
+
devLog9(
|
|
2724
|
+
"[CxtMenu] Hide reverse path (edge + source + upstream):",
|
|
2725
|
+
edgeId
|
|
2726
|
+
);
|
|
2727
|
+
useGraphExploration.getState().hideEdgeReversePath(edgeId);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
],
|
|
2731
|
+
menuRadius: 90,
|
|
2732
|
+
// Increased for more items
|
|
2733
|
+
fillColor: "rgba(0, 0, 0, 0.75)",
|
|
2734
|
+
activeFillColor: "rgba(239, 68, 68, 0.75)",
|
|
2735
|
+
// red-500
|
|
2736
|
+
activePadding: 10,
|
|
2737
|
+
indicatorSize: 12,
|
|
2738
|
+
separatorWidth: 2,
|
|
2739
|
+
spotlightPadding: 2,
|
|
2740
|
+
openMenuEvents: "cxttapstart taphold",
|
|
2741
|
+
itemColor: "white",
|
|
2742
|
+
itemTextShadowColor: "transparent",
|
|
2743
|
+
zIndex: 9999,
|
|
2744
|
+
atMouse: false
|
|
2745
|
+
});
|
|
2746
|
+
};
|
|
2747
|
+
|
|
2748
|
+
// src/components/cyto/CytoscapeGraph.tsx
|
|
2749
|
+
import { toast as toast3 } from "sonner";
|
|
2750
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2751
|
+
try {
|
|
2752
|
+
cytoscape.use(fcose);
|
|
2753
|
+
cytoscape.use(cola);
|
|
2754
|
+
cytoscape.use(dagre);
|
|
2755
|
+
cytoscape.use(klay);
|
|
2756
|
+
cytoscape.use(elk);
|
|
2757
|
+
cytoscape.use(cise);
|
|
2758
|
+
cxtmenu(cytoscape);
|
|
2759
|
+
} catch {
|
|
2760
|
+
}
|
|
2761
|
+
var ALLOWED_LAYOUTS = [
|
|
2762
|
+
"auto",
|
|
2763
|
+
"fcose",
|
|
2764
|
+
"cola",
|
|
2765
|
+
"dagre",
|
|
2766
|
+
"klay",
|
|
2767
|
+
"elk",
|
|
2768
|
+
"cise",
|
|
2769
|
+
"grid",
|
|
2770
|
+
"cose",
|
|
2771
|
+
"concentric",
|
|
2772
|
+
"breadthfirst",
|
|
2773
|
+
"tree",
|
|
2774
|
+
"circle",
|
|
2775
|
+
"random"
|
|
2776
|
+
];
|
|
2777
|
+
function classifyUpdate({
|
|
2778
|
+
cy,
|
|
2779
|
+
currentIds,
|
|
2780
|
+
elementIdsRef,
|
|
2781
|
+
lastLayoutRef,
|
|
2782
|
+
lastFitPaddingRef,
|
|
2783
|
+
currentLayout,
|
|
2784
|
+
fitPadding,
|
|
2785
|
+
nodeCount,
|
|
2786
|
+
edgeCount
|
|
2787
|
+
}) {
|
|
2788
|
+
const paddingChanged = fitPadding !== lastFitPaddingRef.current;
|
|
2789
|
+
const cyHasElements = cy.nodes().length === nodeCount && cy.edges().length === edgeCount;
|
|
2790
|
+
const layoutChanged = currentLayout !== lastLayoutRef.current;
|
|
2791
|
+
if (paddingChanged && currentIds === elementIdsRef.current && cyHasElements && !layoutChanged) {
|
|
2792
|
+
return "refit";
|
|
2793
|
+
}
|
|
2794
|
+
if (currentIds === elementIdsRef.current && cyHasElements && !layoutChanged) {
|
|
2795
|
+
return "skip";
|
|
2796
|
+
}
|
|
2797
|
+
return "update";
|
|
2798
|
+
}
|
|
2799
|
+
function applyLayout({
|
|
2800
|
+
cy,
|
|
2801
|
+
chosen,
|
|
2802
|
+
nodeCount,
|
|
2803
|
+
nodesForLayout,
|
|
2804
|
+
fitPadding
|
|
2805
|
+
}) {
|
|
2806
|
+
const layoutOpts = getLayoutPreset({
|
|
2807
|
+
layoutName: chosen,
|
|
2808
|
+
count: nodeCount,
|
|
2809
|
+
nodeData: nodesForLayout
|
|
2810
|
+
});
|
|
2811
|
+
if (chosen === "tree") {
|
|
2812
|
+
const cyNodes = cy.nodes();
|
|
2813
|
+
const rootCandidates = cyNodes.filter((n) => n.indegree(false) === 0);
|
|
2814
|
+
if (rootCandidates.length > 0) {
|
|
2815
|
+
layoutOpts.roots = rootCandidates;
|
|
2816
|
+
} else if (cyNodes.length) {
|
|
2817
|
+
layoutOpts.roots = cyNodes[0];
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
const getPreset = (name) => getLayoutPreset({
|
|
2821
|
+
layoutName: name,
|
|
2822
|
+
count: nodeCount,
|
|
2823
|
+
nodeData: nodesForLayout
|
|
2824
|
+
});
|
|
2825
|
+
try {
|
|
2826
|
+
cy.layout(layoutOpts).run();
|
|
2827
|
+
} catch (e) {
|
|
2828
|
+
errorLog2("Layout error, falling back to dagre:", e);
|
|
2829
|
+
if (chosen === "klay") {
|
|
2830
|
+
try {
|
|
2831
|
+
cy.layout(getPreset("dagre")).run();
|
|
2832
|
+
} catch (inner) {
|
|
2833
|
+
errorLog2("Fallback layout also failed, using grid:", inner);
|
|
2834
|
+
cy.layout(getPreset("grid")).run();
|
|
2835
|
+
}
|
|
2836
|
+
} else {
|
|
2837
|
+
cy.layout(getPreset("grid")).run();
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
if (!layoutOpts.fit) {
|
|
2841
|
+
cy.fit(void 0, fitPadding);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
function processPendingFocus({
|
|
2845
|
+
cy,
|
|
2846
|
+
pendingFocus,
|
|
2847
|
+
edgesRef,
|
|
2848
|
+
setSelectedNodeIdsStore,
|
|
2849
|
+
setSelectedEdgeStore,
|
|
2850
|
+
selectedNodeIdsRef,
|
|
2851
|
+
selectedEdgeRef,
|
|
2852
|
+
pendingFocusActionRef
|
|
2853
|
+
}) {
|
|
2854
|
+
devLog10("[CytoscapeGraph] Processing pending focus action", {
|
|
2855
|
+
elementId: pendingFocus.elementId,
|
|
2856
|
+
elementType: pendingFocus.elementType
|
|
2857
|
+
});
|
|
2858
|
+
setTimeout(() => {
|
|
2859
|
+
const element = cy.$id(pendingFocus.elementId);
|
|
2860
|
+
if (element.length === 0) {
|
|
2861
|
+
devLog10(
|
|
2862
|
+
"[CytoscapeGraph] Focus requested but element not found after graph update",
|
|
2863
|
+
{
|
|
2864
|
+
elementId: pendingFocus.elementId,
|
|
2865
|
+
elementType: pendingFocus.elementType
|
|
2866
|
+
}
|
|
2867
|
+
);
|
|
2868
|
+
pendingFocusActionRef.current = null;
|
|
2869
|
+
GraphFocus_default.setState({ focusAction: null });
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
cy.animate(
|
|
2873
|
+
{ zoom: 2, center: { eles: element } },
|
|
2874
|
+
{ duration: 500, easing: "ease-in-out-cubic" }
|
|
2875
|
+
);
|
|
2876
|
+
if (pendingFocus.action === "focus-and-select") {
|
|
2877
|
+
selectFocusedElement({
|
|
2878
|
+
cy,
|
|
2879
|
+
pendingFocus,
|
|
2880
|
+
element,
|
|
2881
|
+
edgesRef,
|
|
2882
|
+
setSelectedNodeIdsStore,
|
|
2883
|
+
setSelectedEdgeStore,
|
|
2884
|
+
selectedNodeIdsRef,
|
|
2885
|
+
selectedEdgeRef
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
pendingFocusActionRef.current = null;
|
|
2889
|
+
GraphFocus_default.setState({ focusAction: null });
|
|
2890
|
+
}, 100);
|
|
2891
|
+
}
|
|
2892
|
+
function selectFocusedElement({
|
|
2893
|
+
cy,
|
|
2894
|
+
pendingFocus,
|
|
2895
|
+
element,
|
|
2896
|
+
edgesRef,
|
|
2897
|
+
setSelectedNodeIdsStore,
|
|
2898
|
+
setSelectedEdgeStore,
|
|
2899
|
+
selectedNodeIdsRef,
|
|
2900
|
+
selectedEdgeRef
|
|
2901
|
+
}) {
|
|
2902
|
+
if (pendingFocus.elementType === "node") {
|
|
2903
|
+
cy.batch(() => {
|
|
2904
|
+
cy.elements().unselect();
|
|
2905
|
+
element.select();
|
|
2906
|
+
});
|
|
2907
|
+
setSelectedNodeIdsStore([pendingFocus.elementId]);
|
|
2908
|
+
selectedNodeIdsRef.current = [pendingFocus.elementId];
|
|
2909
|
+
setSelectedEdgeStore(null);
|
|
2910
|
+
selectedEdgeRef.current = null;
|
|
2911
|
+
} else {
|
|
2912
|
+
const edgeObj = edgesRef.current.find(
|
|
2913
|
+
(e) => String(e.id_) === pendingFocus.elementId
|
|
2914
|
+
);
|
|
2915
|
+
if (edgeObj) {
|
|
2916
|
+
const sourceId = String(edgeObj.start_id_);
|
|
2917
|
+
const targetId = String(edgeObj.end_id_);
|
|
2918
|
+
cy.batch(() => {
|
|
2919
|
+
cy.elements().unselect();
|
|
2920
|
+
cy.$id(sourceId).select();
|
|
2921
|
+
cy.$id(targetId).select();
|
|
2922
|
+
element.select();
|
|
2923
|
+
});
|
|
2924
|
+
setSelectedNodeIdsStore([sourceId, targetId]);
|
|
2925
|
+
selectedNodeIdsRef.current = [sourceId, targetId];
|
|
2926
|
+
setSelectedEdgeStore(edgeObj);
|
|
2927
|
+
selectedEdgeRef.current = edgeObj;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
function handleFocusStateChange({
|
|
2932
|
+
focusAction,
|
|
2933
|
+
cyRef,
|
|
2934
|
+
edgesRef,
|
|
2935
|
+
selectedNodeIdsRef,
|
|
2936
|
+
selectedEdgeRef,
|
|
2937
|
+
setSelectedNodeIdsStore,
|
|
2938
|
+
setSelectedEdgeStore,
|
|
2939
|
+
pendingFocusActionRef,
|
|
2940
|
+
baseNormalized
|
|
2941
|
+
}) {
|
|
2942
|
+
if (!focusAction || !cyRef.current) return;
|
|
2943
|
+
devLog10("[CytoscapeGraph] Processing focus action", {
|
|
2944
|
+
elementId: focusAction.elementId,
|
|
2945
|
+
timestamp: focusAction.timestamp
|
|
2946
|
+
});
|
|
2947
|
+
const cy = cyRef.current;
|
|
2948
|
+
const elementId = focusAction.elementId;
|
|
2949
|
+
const element = cy.$id(elementId);
|
|
2950
|
+
if (element.length > 0) {
|
|
2951
|
+
handleImmediateFocus({
|
|
2952
|
+
cy,
|
|
2953
|
+
element,
|
|
2954
|
+
focusAction,
|
|
2955
|
+
edgesRef,
|
|
2956
|
+
selectedNodeIdsRef,
|
|
2957
|
+
selectedEdgeRef,
|
|
2958
|
+
setSelectedNodeIdsStore,
|
|
2959
|
+
setSelectedEdgeStore
|
|
2960
|
+
});
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
devLog10("[CytoscapeGraph] Element not visible, setting up override", {
|
|
2964
|
+
elementId,
|
|
2965
|
+
elementType: focusAction.elementType
|
|
2966
|
+
});
|
|
2967
|
+
pendingFocusActionRef.current = focusAction;
|
|
2968
|
+
GraphFocus_default.setState((state) => {
|
|
2969
|
+
const overrides = new Set(state.overrideElementIds);
|
|
2970
|
+
overrides.add(elementId);
|
|
2971
|
+
if (focusAction.elementType === "edge") {
|
|
2972
|
+
let edgeObj = edgesRef.current.find((e) => String(e.id_) === elementId);
|
|
2973
|
+
if (!edgeObj) {
|
|
2974
|
+
edgeObj = baseNormalized.edgesById[elementId];
|
|
2975
|
+
}
|
|
2976
|
+
if (edgeObj) {
|
|
2977
|
+
overrides.add(String(edgeObj.start_id_));
|
|
2978
|
+
overrides.add(String(edgeObj.end_id_));
|
|
2979
|
+
devLog10("[CytoscapeGraph] Added edge and endpoints to overrides", {
|
|
2980
|
+
edgeId: elementId,
|
|
2981
|
+
startId: edgeObj.start_id_,
|
|
2982
|
+
endId: edgeObj.end_id_
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
} else {
|
|
2986
|
+
devLog10("[CytoscapeGraph] Added node to overrides", { nodeId: elementId });
|
|
2987
|
+
}
|
|
2988
|
+
return { overrideElementIds: overrides };
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
function handleImmediateFocus({
|
|
2992
|
+
cy,
|
|
2993
|
+
element,
|
|
2994
|
+
focusAction,
|
|
2995
|
+
edgesRef,
|
|
2996
|
+
selectedNodeIdsRef,
|
|
2997
|
+
selectedEdgeRef,
|
|
2998
|
+
setSelectedNodeIdsStore,
|
|
2999
|
+
setSelectedEdgeStore
|
|
3000
|
+
}) {
|
|
3001
|
+
const elementId = focusAction.elementId;
|
|
3002
|
+
devLog10("[CytoscapeGraph] Element already visible, focusing immediately", {
|
|
3003
|
+
elementId,
|
|
3004
|
+
elementType: focusAction.elementType
|
|
3005
|
+
});
|
|
3006
|
+
cy.animate(
|
|
3007
|
+
{ zoom: 2, center: { eles: element } },
|
|
3008
|
+
{ duration: 500, easing: "ease-in-out-cubic" }
|
|
3009
|
+
);
|
|
3010
|
+
if (focusAction.action === "focus-and-select") {
|
|
3011
|
+
selectFocusedElement({
|
|
3012
|
+
cy,
|
|
3013
|
+
pendingFocus: focusAction,
|
|
3014
|
+
element,
|
|
3015
|
+
edgesRef,
|
|
3016
|
+
setSelectedNodeIdsStore,
|
|
3017
|
+
setSelectedEdgeStore,
|
|
3018
|
+
selectedNodeIdsRef,
|
|
3019
|
+
selectedEdgeRef
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
GraphFocus_default.setState({ focusAction: null });
|
|
3023
|
+
}
|
|
3024
|
+
function computeGraphViewState({
|
|
3025
|
+
hasNodes,
|
|
3026
|
+
hasHiddenElements,
|
|
3027
|
+
isFocusMode,
|
|
3028
|
+
fullscreen
|
|
3029
|
+
}) {
|
|
3030
|
+
const hasExplorationState = hasHiddenElements || isFocusMode;
|
|
3031
|
+
return {
|
|
3032
|
+
showEmptyMessage: !hasNodes && !hasExplorationState,
|
|
3033
|
+
showFilteredMessage: !hasNodes && hasExplorationState,
|
|
3034
|
+
showControls: hasNodes || hasExplorationState,
|
|
3035
|
+
outerClass: fullscreen ? "fixed inset-0 z-50 w-screen h-screen bg-white flex flex-col" : "w-full h-full flex flex-col relative",
|
|
3036
|
+
innerClass: fullscreen ? "flex-1 relative overflow-hidden bg-white" : "flex-1 relative border border-gray-300 rounded-md overflow-hidden bg-white"
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
function useFullscreen(fitToScreen, cyRef) {
|
|
3040
|
+
const [fullscreen, setFullscreen] = useState4(false);
|
|
3041
|
+
useEffect3(() => {
|
|
3042
|
+
if (fullscreen) {
|
|
3043
|
+
const prevOverflow = document.body.style.overflow;
|
|
3044
|
+
document.body.style.overflow = "hidden";
|
|
3045
|
+
fitToScreen();
|
|
3046
|
+
const handleKey = (e) => {
|
|
3047
|
+
if (e.key === "Escape") setFullscreen(false);
|
|
3048
|
+
};
|
|
3049
|
+
window.addEventListener("keydown", handleKey);
|
|
3050
|
+
return () => {
|
|
3051
|
+
document.body.style.overflow = prevOverflow;
|
|
3052
|
+
window.removeEventListener("keydown", handleKey);
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
}, [fullscreen, fitToScreen]);
|
|
3056
|
+
useEffect3(() => {
|
|
3057
|
+
if (!fullscreen) return;
|
|
3058
|
+
const id = setTimeout(() => {
|
|
3059
|
+
if (cyRef.current) {
|
|
3060
|
+
cyRef.current.resize();
|
|
3061
|
+
fitToScreen();
|
|
3062
|
+
}
|
|
3063
|
+
}, 50);
|
|
3064
|
+
return () => clearTimeout(id);
|
|
3065
|
+
}, [fullscreen, fitToScreen, cyRef]);
|
|
3066
|
+
return {
|
|
3067
|
+
fullscreen,
|
|
3068
|
+
setFullscreen,
|
|
3069
|
+
toggleFullscreen: useCallback3(() => setFullscreen((f) => !f), [])
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
function setupCytoscapeEventHandlers({
|
|
3073
|
+
cy,
|
|
3074
|
+
edgesRef,
|
|
3075
|
+
selectedNodeIdsRef,
|
|
3076
|
+
selectedEdgeRef,
|
|
3077
|
+
setSelectedNodeIdsStore,
|
|
3078
|
+
setSelectedEdgeStore,
|
|
3079
|
+
clearSelectionStore,
|
|
3080
|
+
useGraphExploration,
|
|
3081
|
+
onCopyNode
|
|
3082
|
+
}) {
|
|
3083
|
+
const updateSelectedNodeIds = (nodeIds) => {
|
|
3084
|
+
setSelectedNodeIdsStore(nodeIds);
|
|
3085
|
+
selectedNodeIdsRef.current = nodeIds;
|
|
3086
|
+
};
|
|
3087
|
+
const updateSelectedEdge = (edgeToSet) => {
|
|
3088
|
+
setSelectedEdgeStore(edgeToSet);
|
|
3089
|
+
selectedEdgeRef.current = edgeToSet;
|
|
3090
|
+
};
|
|
3091
|
+
const clearAll = () => {
|
|
3092
|
+
cy.batch(() => {
|
|
3093
|
+
cy.elements().unselect();
|
|
3094
|
+
});
|
|
3095
|
+
clearSelectionStore();
|
|
3096
|
+
selectedNodeIdsRef.current = [];
|
|
3097
|
+
selectedEdgeRef.current = null;
|
|
3098
|
+
};
|
|
3099
|
+
cy.on("select", "node", (evt) => {
|
|
3100
|
+
const nodeEle = evt.target;
|
|
3101
|
+
const evtCy = evt.cy;
|
|
3102
|
+
const id = nodeEle.id();
|
|
3103
|
+
devLog10("[CytoscapeGraph] node selected", { id });
|
|
3104
|
+
if (selectedEdgeRef.current) {
|
|
3105
|
+
devLog10("[CytoscapeGraph] clearing edge due to node selection", {
|
|
3106
|
+
edgeId: selectedEdgeRef.current.id_
|
|
3107
|
+
});
|
|
3108
|
+
updateSelectedEdge(null);
|
|
3109
|
+
}
|
|
3110
|
+
let selectedIds = evtCy.$("node:selected").map((e) => e.id());
|
|
3111
|
+
if (selectedIds.length > 2) {
|
|
3112
|
+
const currentOrder = [...selectedNodeIdsRef.current];
|
|
3113
|
+
const updatedOrder = [...currentOrder.filter((cid) => cid !== id), id];
|
|
3114
|
+
const keep = updatedOrder.slice(-2);
|
|
3115
|
+
evtCy.batch(() => {
|
|
3116
|
+
evtCy.$("node:selected").unselect();
|
|
3117
|
+
keep.forEach((kid) => evtCy.$id(kid).select());
|
|
3118
|
+
});
|
|
3119
|
+
selectedIds = keep;
|
|
3120
|
+
devLog10("[CytoscapeGraph] enforced max 2 after select", { keep });
|
|
3121
|
+
}
|
|
3122
|
+
updateSelectedNodeIds(selectedIds);
|
|
3123
|
+
});
|
|
3124
|
+
cy.on("unselect", "node", (evt) => {
|
|
3125
|
+
devLog10("[CytoscapeGraph] node unselected", { id: evt.target.id() });
|
|
3126
|
+
const selectedIds = evt.cy.$("node:selected").map((e) => e.id());
|
|
3127
|
+
updateSelectedNodeIds(selectedIds);
|
|
3128
|
+
});
|
|
3129
|
+
cy.on("tap", "edge", (evt) => {
|
|
3130
|
+
const edgeEle = evt.target;
|
|
3131
|
+
const edgeId = edgeEle.id();
|
|
3132
|
+
const edgeObj = edgesRef.current.find((e) => String(e.id_) === edgeId);
|
|
3133
|
+
if (!edgeObj) return;
|
|
3134
|
+
const sourceId = edgeEle.data("source");
|
|
3135
|
+
const targetId = edgeEle.data("target");
|
|
3136
|
+
const evtCy = evt.cy;
|
|
3137
|
+
const currentlySelectedNodes = evtCy.$("node:selected").toArray();
|
|
3138
|
+
if (currentlySelectedNodes.length === 1) {
|
|
3139
|
+
const selectedNodeId = currentlySelectedNodes[0].id();
|
|
3140
|
+
if (selectedNodeId === sourceId || selectedNodeId === targetId) {
|
|
3141
|
+
devLog10("[CytoscapeGraph] tap edge - node+edge selection", {
|
|
3142
|
+
edgeId,
|
|
3143
|
+
selectedNode: selectedNodeId,
|
|
3144
|
+
direction: selectedNodeId === sourceId ? "outgoing" : "incoming"
|
|
3145
|
+
});
|
|
3146
|
+
evtCy.batch(() => {
|
|
3147
|
+
evtCy.elements().unselect();
|
|
3148
|
+
evtCy.$id(selectedNodeId).select();
|
|
3149
|
+
edgeEle.select();
|
|
3150
|
+
});
|
|
3151
|
+
updateSelectedNodeIds([selectedNodeId]);
|
|
3152
|
+
updateSelectedEdge(edgeObj);
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId });
|
|
3157
|
+
evtCy.batch(() => {
|
|
3158
|
+
evtCy.elements().unselect();
|
|
3159
|
+
evtCy.$id(sourceId).select();
|
|
3160
|
+
evtCy.$id(targetId).select();
|
|
3161
|
+
edgeEle.select();
|
|
3162
|
+
});
|
|
3163
|
+
updateSelectedNodeIds([sourceId, targetId]);
|
|
3164
|
+
updateSelectedEdge(edgeObj);
|
|
3165
|
+
});
|
|
3166
|
+
cy.on("tap", (evt) => {
|
|
3167
|
+
if (evt.target === cy) clearAll();
|
|
3168
|
+
});
|
|
3169
|
+
createNodeContextMenu({ cy, useGraphExploration, onCopyNode });
|
|
3170
|
+
createEdgeContextMenu({ cy, useGraphExploration });
|
|
3171
|
+
}
|
|
3172
|
+
function CytoscapeGraph({
|
|
3173
|
+
layout: initialLayout = "auto",
|
|
3174
|
+
fitPadding = 30,
|
|
3175
|
+
onCopyNode,
|
|
3176
|
+
extraToolbarContent
|
|
3177
|
+
}) {
|
|
3178
|
+
const containerRef = useRef2(null);
|
|
3179
|
+
const cyRef = useRef2(null);
|
|
3180
|
+
const isInitializedRef = useRef2(false);
|
|
3181
|
+
const [currentLayout, setCurrentLayout] = useState4(initialLayout);
|
|
3182
|
+
const nodesRef = useRef2([]);
|
|
3183
|
+
const edgesRef = useRef2([]);
|
|
3184
|
+
const pendingFocusActionRef = useRef2(null);
|
|
3185
|
+
const cytoData = useCytoscapeData();
|
|
3186
|
+
const live = useLiveGraphData();
|
|
3187
|
+
const nodes = live.nodes;
|
|
3188
|
+
const edges = live.edges;
|
|
3189
|
+
const baseNormalized = live.baseNormalized;
|
|
3190
|
+
const useGraphFilter2 = useWorkspaceGraphFilter();
|
|
3191
|
+
const useGraphExploration = useWorkspaceGraphExploration();
|
|
3192
|
+
const useSelection3 = useWorkspaceSelection();
|
|
3193
|
+
const lastDataChangeTime = useGraphFilter2((s) => s.lastDataChangeTime);
|
|
3194
|
+
const resetView = useGraphExploration((s) => s.resetView);
|
|
3195
|
+
const hasHiddenElements = useGraphExploration(
|
|
3196
|
+
(s) => s.hiddenElementIds.size > 0
|
|
3197
|
+
);
|
|
3198
|
+
const isFocusMode = useGraphExploration((s) => s.isFocusMode);
|
|
3199
|
+
const {
|
|
3200
|
+
setSelectedNodeIds: setSelectedNodeIdsStore,
|
|
3201
|
+
setSelectedEdge: setSelectedEdgeStore,
|
|
3202
|
+
clear: clearSelectionStore
|
|
3203
|
+
} = useSelection3.getState();
|
|
3204
|
+
const selectedNodeIdsRef = useRef2(
|
|
3205
|
+
useSelection3.getState().selectedNodeIds
|
|
3206
|
+
);
|
|
3207
|
+
const selectedEdgeRef = useRef2(
|
|
3208
|
+
useSelection3.getState().selectedEdge
|
|
3209
|
+
);
|
|
3210
|
+
useEffect3(() => {
|
|
3211
|
+
const state = useSelection3.getState();
|
|
3212
|
+
selectedNodeIdsRef.current = state.selectedNodeIds;
|
|
3213
|
+
selectedEdgeRef.current = state.selectedEdge;
|
|
3214
|
+
const cy = cyRef.current;
|
|
3215
|
+
if (cy && (state.selectedNodeIds.length || state.selectedEdge)) {
|
|
3216
|
+
cy.batch(() => {
|
|
3217
|
+
cy.elements().unselect();
|
|
3218
|
+
state.selectedNodeIds.forEach((id) => cy.$id(id).select());
|
|
3219
|
+
if (state.selectedEdge) {
|
|
3220
|
+
const edge = cy.$id(String(state.selectedEdge.id_));
|
|
3221
|
+
if (edge) {
|
|
3222
|
+
edge.select();
|
|
3223
|
+
cy.$id(edge.data("source")).select();
|
|
3224
|
+
cy.$id(edge.data("target")).select();
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
}, [useSelection3]);
|
|
3230
|
+
useEffect3(() => {
|
|
3231
|
+
nodesRef.current = nodes;
|
|
3232
|
+
}, [nodes]);
|
|
3233
|
+
useEffect3(() => {
|
|
3234
|
+
edgesRef.current = edges;
|
|
3235
|
+
}, [edges]);
|
|
3236
|
+
useEffect3(() => {
|
|
3237
|
+
if (!containerRef.current || cyRef.current) return;
|
|
3238
|
+
isInitializedRef.current = true;
|
|
3239
|
+
cyRef.current = cytoscape({
|
|
3240
|
+
container: containerRef.current,
|
|
3241
|
+
elements: [],
|
|
3242
|
+
selectionType: "additive",
|
|
3243
|
+
// allow clicking multiple nodes without modifier
|
|
3244
|
+
style: [
|
|
3245
|
+
// Base node style with data mappers for dynamic properties
|
|
3246
|
+
{
|
|
3247
|
+
selector: "node",
|
|
3248
|
+
style: {
|
|
3249
|
+
"background-color": "data(color)",
|
|
3250
|
+
// Read color from element data
|
|
3251
|
+
"background-opacity": 0.9,
|
|
3252
|
+
width: "data(size)",
|
|
3253
|
+
// Read size from element data
|
|
3254
|
+
height: "data(size)",
|
|
3255
|
+
shape: "ellipse",
|
|
3256
|
+
label: "data(label)",
|
|
3257
|
+
// Dark gray text for readability on subtle gray background
|
|
3258
|
+
color: "#374151",
|
|
3259
|
+
"font-size": "11px",
|
|
3260
|
+
// Increased from 10px
|
|
3261
|
+
"font-weight": "bold",
|
|
3262
|
+
"text-valign": "bottom",
|
|
3263
|
+
"text-halign": "center",
|
|
3264
|
+
"text-wrap": "wrap",
|
|
3265
|
+
"text-max-width": "120px",
|
|
3266
|
+
// Increased from 100px for longer labels
|
|
3267
|
+
"text-margin-y": 5,
|
|
3268
|
+
// Subtle light gray label background
|
|
3269
|
+
"text-background-color": "#e5e7eb",
|
|
3270
|
+
// gray-200
|
|
3271
|
+
"text-background-opacity": 1,
|
|
3272
|
+
"text-background-padding": "3px",
|
|
3273
|
+
"text-background-shape": "roundrectangle",
|
|
3274
|
+
"border-width": 1,
|
|
3275
|
+
"border-color": "#ffffff",
|
|
3276
|
+
"border-opacity": 0.8
|
|
3277
|
+
}
|
|
3278
|
+
},
|
|
3279
|
+
// Override background color only when fill data is present
|
|
3280
|
+
{
|
|
3281
|
+
selector: "node[fill]",
|
|
3282
|
+
style: {
|
|
3283
|
+
"background-color": "data(fill)"
|
|
3284
|
+
}
|
|
3285
|
+
},
|
|
3286
|
+
{
|
|
3287
|
+
selector: "edge",
|
|
3288
|
+
style: {
|
|
3289
|
+
width: "data(size)",
|
|
3290
|
+
// Read width from element data
|
|
3291
|
+
"line-color": "data(color)",
|
|
3292
|
+
// Read color from element data
|
|
3293
|
+
"target-arrow-color": "data(color)",
|
|
3294
|
+
"target-arrow-shape": "triangle",
|
|
3295
|
+
"curve-style": "bezier",
|
|
3296
|
+
"control-point-step-size": 40,
|
|
3297
|
+
"control-point-weight": 0.5,
|
|
3298
|
+
opacity: 0.8,
|
|
3299
|
+
"arrow-scale": 0.8,
|
|
3300
|
+
"mid-target-arrow-shape": "none",
|
|
3301
|
+
"source-endpoint": "outside-to-node-or-label",
|
|
3302
|
+
"target-endpoint": "outside-to-node-or-label"
|
|
3303
|
+
}
|
|
3304
|
+
},
|
|
3305
|
+
// Dashed edges (when dashed property is true)
|
|
3306
|
+
{
|
|
3307
|
+
selector: "edge[dashed]",
|
|
3308
|
+
style: {
|
|
3309
|
+
"line-style": "dashed"
|
|
3310
|
+
}
|
|
3311
|
+
},
|
|
3312
|
+
// Show edge labels by default with good styling
|
|
3313
|
+
{
|
|
3314
|
+
selector: "edge[label]",
|
|
3315
|
+
style: {
|
|
3316
|
+
label: "data(label)",
|
|
3317
|
+
"font-size": "10px",
|
|
3318
|
+
// Increased from 8px for better readability
|
|
3319
|
+
"font-weight": "500",
|
|
3320
|
+
// Semi-bold for better visibility
|
|
3321
|
+
"text-background-color": "#ffffff",
|
|
3322
|
+
"text-background-opacity": 0.9,
|
|
3323
|
+
// Increased from 0.8 for better contrast
|
|
3324
|
+
"text-background-padding": "3px",
|
|
3325
|
+
// Increased from 2px
|
|
3326
|
+
"text-background-shape": "roundrectangle",
|
|
3327
|
+
"text-rotation": "autorotate",
|
|
3328
|
+
"text-margin-y": -8,
|
|
3329
|
+
// Adjusted for larger font
|
|
3330
|
+
color: "#1a1a1a"
|
|
3331
|
+
// Darker for better contrast
|
|
3332
|
+
}
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
selector: "edge:selected",
|
|
3336
|
+
style: {
|
|
3337
|
+
"line-color": "#ff1a1a",
|
|
3338
|
+
"target-arrow-color": "#ff1a1a",
|
|
3339
|
+
width: 8,
|
|
3340
|
+
// even thicker
|
|
3341
|
+
"z-index": 999,
|
|
3342
|
+
"font-size": "11px",
|
|
3343
|
+
"font-weight": "bold",
|
|
3344
|
+
"text-background-opacity": 1,
|
|
3345
|
+
"text-background-color": "#ffffff",
|
|
3346
|
+
"text-background-padding": "3px",
|
|
3347
|
+
"arrow-scale": 1.35,
|
|
3348
|
+
opacity: 1,
|
|
3349
|
+
"overlay-color": "#ff1a1a",
|
|
3350
|
+
"overlay-opacity": 0.08
|
|
3351
|
+
}
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
selector: "node:selected",
|
|
3355
|
+
style: {
|
|
3356
|
+
"border-width": 10,
|
|
3357
|
+
// thicker circle outline
|
|
3358
|
+
"border-color": "#ff1a1a",
|
|
3359
|
+
"border-opacity": 1,
|
|
3360
|
+
"background-opacity": 1,
|
|
3361
|
+
"text-background-opacity": 1,
|
|
3362
|
+
"text-background-color": "#ffffff",
|
|
3363
|
+
"text-background-padding": "4px",
|
|
3364
|
+
"overlay-color": "#ff1a1a",
|
|
3365
|
+
"overlay-opacity": 0.18,
|
|
3366
|
+
width: 48,
|
|
3367
|
+
// slight size bump from 40
|
|
3368
|
+
height: 48
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
],
|
|
3372
|
+
layout: { name: "grid", fit: true, avoidOverlap: true, condense: true },
|
|
3373
|
+
// initial quick layout
|
|
3374
|
+
wheelSensitivity: 0.2
|
|
3375
|
+
});
|
|
3376
|
+
const cy = cyRef.current;
|
|
3377
|
+
if (cy) {
|
|
3378
|
+
setupCytoscapeEventHandlers({
|
|
3379
|
+
cy,
|
|
3380
|
+
edgesRef,
|
|
3381
|
+
selectedNodeIdsRef,
|
|
3382
|
+
selectedEdgeRef,
|
|
3383
|
+
setSelectedNodeIdsStore,
|
|
3384
|
+
setSelectedEdgeStore,
|
|
3385
|
+
clearSelectionStore,
|
|
3386
|
+
useGraphExploration,
|
|
3387
|
+
onCopyNode
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
return () => {
|
|
3391
|
+
cyRef.current?.destroy();
|
|
3392
|
+
cyRef.current = null;
|
|
3393
|
+
};
|
|
3394
|
+
}, []);
|
|
3395
|
+
const elementIdsRef = useRef2("");
|
|
3396
|
+
const lastLayoutRef = useRef2("");
|
|
3397
|
+
const lastFitPaddingRef = useRef2(fitPadding);
|
|
3398
|
+
useEffect3(() => {
|
|
3399
|
+
const cy = cyRef.current;
|
|
3400
|
+
if (!cy || !isInitializedRef.current) return;
|
|
3401
|
+
const currentIds = nodes.map((n) => n.id_).sort().join(",") + "|" + edges.map((e) => e.id_).sort().join(",");
|
|
3402
|
+
const action = classifyUpdate({
|
|
3403
|
+
cy,
|
|
3404
|
+
currentIds,
|
|
3405
|
+
elementIdsRef,
|
|
3406
|
+
lastLayoutRef,
|
|
3407
|
+
lastFitPaddingRef,
|
|
3408
|
+
currentLayout,
|
|
3409
|
+
fitPadding,
|
|
3410
|
+
nodeCount: nodes.length,
|
|
3411
|
+
edgeCount: edges.length
|
|
3412
|
+
});
|
|
3413
|
+
if (action === "refit") {
|
|
3414
|
+
lastFitPaddingRef.current = fitPadding;
|
|
3415
|
+
cy.fit(void 0, fitPadding);
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3418
|
+
if (action === "skip") {
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
elementIdsRef.current = currentIds;
|
|
3422
|
+
lastLayoutRef.current = currentLayout;
|
|
3423
|
+
lastFitPaddingRef.current = fitPadding;
|
|
3424
|
+
const { nodes: nodeElements, edges: edgeElements } = cytoData.elements;
|
|
3425
|
+
cy.batch(() => {
|
|
3426
|
+
cy.elements().remove();
|
|
3427
|
+
cy.add([...nodeElements, ...edgeElements]);
|
|
3428
|
+
});
|
|
3429
|
+
if (selectedNodeIdsRef.current.length > 0 || selectedEdgeRef.current) {
|
|
3430
|
+
cy.batch(() => {
|
|
3431
|
+
cy.elements().unselect();
|
|
3432
|
+
selectedNodeIdsRef.current.forEach((id) => cy.$id(id).select());
|
|
3433
|
+
if (selectedEdgeRef.current) {
|
|
3434
|
+
const edge = cy.$id(String(selectedEdgeRef.current.id_));
|
|
3435
|
+
if (edge) {
|
|
3436
|
+
edge.select();
|
|
3437
|
+
cy.$id(edge.data("source")).select();
|
|
3438
|
+
cy.$id(edge.data("target")).select();
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
const chosen = ALLOWED_LAYOUTS.includes(
|
|
3444
|
+
currentLayout
|
|
3445
|
+
) ? currentLayout : "auto";
|
|
3446
|
+
const nodesForLayout = nodes.map((n) => ({
|
|
3447
|
+
id: String(n.id_),
|
|
3448
|
+
data: { label_: n.label_ }
|
|
3449
|
+
}));
|
|
3450
|
+
applyLayout({
|
|
3451
|
+
cy,
|
|
3452
|
+
chosen,
|
|
3453
|
+
nodeCount: nodes.length,
|
|
3454
|
+
nodesForLayout,
|
|
3455
|
+
fitPadding
|
|
3456
|
+
});
|
|
3457
|
+
const pendingFocus = pendingFocusActionRef.current;
|
|
3458
|
+
if (pendingFocus) {
|
|
3459
|
+
processPendingFocus({
|
|
3460
|
+
cy,
|
|
3461
|
+
pendingFocus,
|
|
3462
|
+
edgesRef,
|
|
3463
|
+
setSelectedNodeIdsStore,
|
|
3464
|
+
setSelectedEdgeStore,
|
|
3465
|
+
selectedNodeIdsRef,
|
|
3466
|
+
selectedEdgeRef,
|
|
3467
|
+
pendingFocusActionRef
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
}, [nodes, edges, currentLayout, fitPadding]);
|
|
3471
|
+
useEffect3(() => {
|
|
3472
|
+
if (!cyRef.current) return;
|
|
3473
|
+
cyRef.current.elements().unselect();
|
|
3474
|
+
clearSelectionStore();
|
|
3475
|
+
}, [lastDataChangeTime, clearSelectionStore]);
|
|
3476
|
+
useEffect3(() => {
|
|
3477
|
+
const unsubscribe = GraphFocus_default.subscribe((state) => {
|
|
3478
|
+
handleFocusStateChange({
|
|
3479
|
+
focusAction: state.focusAction,
|
|
3480
|
+
cyRef,
|
|
3481
|
+
edgesRef,
|
|
3482
|
+
selectedNodeIdsRef,
|
|
3483
|
+
selectedEdgeRef,
|
|
3484
|
+
setSelectedNodeIdsStore,
|
|
3485
|
+
setSelectedEdgeStore,
|
|
3486
|
+
pendingFocusActionRef,
|
|
3487
|
+
baseNormalized
|
|
3488
|
+
});
|
|
3489
|
+
});
|
|
3490
|
+
return unsubscribe;
|
|
3491
|
+
}, [baseNormalized, setSelectedEdgeStore, setSelectedNodeIdsStore]);
|
|
3492
|
+
const updateLayout = (newLayout) => {
|
|
3493
|
+
setCurrentLayout(newLayout);
|
|
3494
|
+
};
|
|
3495
|
+
const fitToScreen = useCallback3(() => {
|
|
3496
|
+
if (cyRef.current) {
|
|
3497
|
+
cyRef.current.fit(void 0, fitPadding);
|
|
3498
|
+
}
|
|
3499
|
+
}, [fitPadding]);
|
|
3500
|
+
useEffect3(() => {
|
|
3501
|
+
fitToScreen();
|
|
3502
|
+
}, [nodes.length, fitToScreen]);
|
|
3503
|
+
useEffect3(() => {
|
|
3504
|
+
if (!isInitializedRef.current) return;
|
|
3505
|
+
const handleResize = () => cyRef.current?.resize();
|
|
3506
|
+
window.addEventListener("resize", handleResize);
|
|
3507
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
3508
|
+
}, []);
|
|
3509
|
+
const { fullscreen, toggleFullscreen } = useFullscreen(fitToScreen, cyRef);
|
|
3510
|
+
const {
|
|
3511
|
+
outerClass,
|
|
3512
|
+
innerClass,
|
|
3513
|
+
showEmptyMessage,
|
|
3514
|
+
showFilteredMessage,
|
|
3515
|
+
showControls
|
|
3516
|
+
} = computeGraphViewState({
|
|
3517
|
+
hasNodes: nodes.length > 0,
|
|
3518
|
+
hasHiddenElements,
|
|
3519
|
+
isFocusMode,
|
|
3520
|
+
fullscreen
|
|
3521
|
+
});
|
|
3522
|
+
return /* @__PURE__ */ jsx8("div", { className: outerClass, children: /* @__PURE__ */ jsxs7("div", { className: innerClass, children: [
|
|
3523
|
+
/* @__PURE__ */ jsx8("div", { ref: containerRef, className: "absolute inset-0" }),
|
|
3524
|
+
showEmptyMessage && /* @__PURE__ */ jsx8("div", { className: "absolute inset-0 flex items-center justify-center text-xs text-gray-500 pointer-events-none select-none", children: "No graph data to visualize (run a query or check the filters)" }),
|
|
3525
|
+
showFilteredMessage && /* @__PURE__ */ jsx8("div", { className: "absolute inset-0 flex items-center justify-center text-xs text-gray-500 pointer-events-none select-none", children: 'All elements are hidden or filtered (click "Reset Focus" to restore)' }),
|
|
3526
|
+
showControls && /* @__PURE__ */ jsx8(
|
|
3527
|
+
CytoscapeControls_default,
|
|
3528
|
+
{
|
|
3529
|
+
updateLayout,
|
|
3530
|
+
fitToScreen,
|
|
3531
|
+
onResetView: () => {
|
|
3532
|
+
resetView();
|
|
3533
|
+
toast3.success(
|
|
3534
|
+
isFocusMode ? "Focus mode cleared" : "Hidden elements restored"
|
|
3535
|
+
);
|
|
3536
|
+
},
|
|
3537
|
+
hasHiddenElements,
|
|
3538
|
+
isFocusMode,
|
|
3539
|
+
extraToolbarContent,
|
|
3540
|
+
zoomIn: () => {
|
|
3541
|
+
if (cyRef.current) {
|
|
3542
|
+
const z = cyRef.current.zoom();
|
|
3543
|
+
cyRef.current.zoom({
|
|
3544
|
+
level: Math.min(z * 1.2, 5),
|
|
3545
|
+
renderedPosition: {
|
|
3546
|
+
x: cyRef.current.width() / 2,
|
|
3547
|
+
y: cyRef.current.height() / 2
|
|
3548
|
+
}
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
},
|
|
3552
|
+
zoomOut: () => {
|
|
3553
|
+
if (cyRef.current) {
|
|
3554
|
+
const z = cyRef.current.zoom();
|
|
3555
|
+
cyRef.current.zoom({
|
|
3556
|
+
level: Math.max(z / 1.2, 0.1),
|
|
3557
|
+
renderedPosition: {
|
|
3558
|
+
x: cyRef.current.width() / 2,
|
|
3559
|
+
y: cyRef.current.height() / 2
|
|
3560
|
+
}
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
},
|
|
3564
|
+
currentLayout,
|
|
3565
|
+
fullscreen,
|
|
3566
|
+
toggleFullscreen
|
|
3567
|
+
}
|
|
3568
|
+
)
|
|
3569
|
+
] }) });
|
|
3570
|
+
}
|
|
3571
|
+
var CytoscapeGraph_default = CytoscapeGraph;
|
|
3572
|
+
|
|
3573
|
+
// src/components/GraphVisualizer.tsx
|
|
3574
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3575
|
+
function GraphVisualizer({
|
|
3576
|
+
onCopyNode,
|
|
3577
|
+
extraToolbarContent
|
|
3578
|
+
}) {
|
|
3579
|
+
const { renderer } = useGraphRenderer();
|
|
3580
|
+
return /* @__PURE__ */ jsx9("div", { className: "relative w-full h-full flex flex-col overflow-hidden", children: /* @__PURE__ */ jsxs8("div", { className: "flex-1 h-full", children: [
|
|
3581
|
+
renderer === "reagraph" && /* @__PURE__ */ jsx9(
|
|
3582
|
+
ReaGraph_default,
|
|
3583
|
+
{
|
|
3584
|
+
onCopyNode,
|
|
3585
|
+
extraToolbarContent
|
|
3586
|
+
},
|
|
3587
|
+
"reagraph"
|
|
3588
|
+
),
|
|
3589
|
+
renderer === "cytoscape" && /* @__PURE__ */ jsx9(
|
|
3590
|
+
CytoscapeGraph_default,
|
|
3591
|
+
{
|
|
3592
|
+
onCopyNode,
|
|
3593
|
+
extraToolbarContent
|
|
3594
|
+
},
|
|
3595
|
+
"cytoscape"
|
|
3596
|
+
)
|
|
3597
|
+
] }) });
|
|
3598
|
+
}
|
|
3599
|
+
var GraphVisualizer_default = GraphVisualizer;
|
|
3600
|
+
|
|
3601
|
+
// src/components/ActionPanel/ActionPanel.tsx
|
|
3602
|
+
import { Card as Card5 } from "@petrarca/sonnet-ui";
|
|
3603
|
+
|
|
3604
|
+
// src/components/ActionPanel/NodeInfoCard.tsx
|
|
3605
|
+
import { useState as useState5 } from "react";
|
|
3606
|
+
import {
|
|
3607
|
+
Card as Card3,
|
|
3608
|
+
Collapsible,
|
|
3609
|
+
CollapsibleContent,
|
|
3610
|
+
CollapsibleTrigger
|
|
3611
|
+
} from "@petrarca/sonnet-ui";
|
|
3612
|
+
import { copyTextToClipboard } from "@petrarca/sonnet-core";
|
|
3613
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3614
|
+
function NodeInfoCard({ node }) {
|
|
3615
|
+
const [showProperties, setShowProperties] = useState5(false);
|
|
3616
|
+
return /* @__PURE__ */ jsxs9(
|
|
3617
|
+
Card3,
|
|
3618
|
+
{
|
|
3619
|
+
className: "w-full rounded-lg overflow-hidden bg-gray-100 border p-4 shadow-xs",
|
|
3620
|
+
style: {
|
|
3621
|
+
background: "#f3f4f6",
|
|
3622
|
+
borderLeft: `4px solid ${node.visual.color}`
|
|
3623
|
+
},
|
|
3624
|
+
children: [
|
|
3625
|
+
/* @__PURE__ */ jsx10("p", { className: "leading-tight font-semibold mb-0", children: node.label_ }),
|
|
3626
|
+
/* @__PURE__ */ jsx10("p", { className: "leading-tight mb-1", children: node.visual.displayName }),
|
|
3627
|
+
/* @__PURE__ */ jsx10(
|
|
3628
|
+
"p",
|
|
3629
|
+
{
|
|
3630
|
+
className: "text-xs text-muted-foreground cursor-pointer select-all mb-2",
|
|
3631
|
+
onClick: () => copyTextToClipboard(String(node.id_)),
|
|
3632
|
+
title: "Click to copy id",
|
|
3633
|
+
children: node.id_
|
|
3634
|
+
}
|
|
3635
|
+
),
|
|
3636
|
+
/* @__PURE__ */ jsxs9(Collapsible, { open: showProperties, onOpenChange: setShowProperties, children: [
|
|
3637
|
+
/* @__PURE__ */ jsx10(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsx10("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
|
|
3638
|
+
/* @__PURE__ */ jsx10(CollapsibleContent, { children: /* @__PURE__ */ jsxs9("ul", { className: "list-none mt-2", children: [
|
|
3639
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm font-semibold mb-1 tracking-wide", children: "Properties" }),
|
|
3640
|
+
Object.entries(node.properties_ || {}).map(([key, value]) => /* @__PURE__ */ jsxs9("li", { className: "mb-2 leading-tight", children: [
|
|
3641
|
+
/* @__PURE__ */ jsx10("p", { className: "text-xs text-slate-600 uppercase tracking-wide", children: key }),
|
|
3642
|
+
/* @__PURE__ */ jsx10(
|
|
3643
|
+
"p",
|
|
3644
|
+
{
|
|
3645
|
+
className: "text-sm truncate max-w-[190px] cursor-pointer",
|
|
3646
|
+
onClick: () => copyTextToClipboard(String(value)),
|
|
3647
|
+
title: "Click to copy value",
|
|
3648
|
+
children: String(value)
|
|
3649
|
+
}
|
|
3650
|
+
)
|
|
3651
|
+
] }, key))
|
|
3652
|
+
] }) })
|
|
3653
|
+
] })
|
|
3654
|
+
]
|
|
3655
|
+
}
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
var NodeInfoCard_default = NodeInfoCard;
|
|
3659
|
+
|
|
3660
|
+
// src/components/ActionPanel/EdgeInfoCard.tsx
|
|
3661
|
+
import { useState as useState6 } from "react";
|
|
3662
|
+
import {
|
|
3663
|
+
Card as Card4,
|
|
3664
|
+
Collapsible as Collapsible2,
|
|
3665
|
+
CollapsibleContent as CollapsibleContent2,
|
|
3666
|
+
CollapsibleTrigger as CollapsibleTrigger2
|
|
3667
|
+
} from "@petrarca/sonnet-ui";
|
|
3668
|
+
import { ArrowDown } from "lucide-react";
|
|
3669
|
+
import { copyTextToClipboard as copyTextToClipboard2 } from "@petrarca/sonnet-core";
|
|
3670
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3671
|
+
function EdgeInfoCard({ edge }) {
|
|
3672
|
+
const [showProperties, setShowProperties] = useState6(false);
|
|
3673
|
+
return /* @__PURE__ */ jsxs10(
|
|
3674
|
+
Card4,
|
|
3675
|
+
{
|
|
3676
|
+
className: "w-full rounded-lg overflow-hidden bg-gray-100 border p-4 shadow-xs",
|
|
3677
|
+
style: {
|
|
3678
|
+
background: "#f3f4f6",
|
|
3679
|
+
borderLeft: `4px solid ${edge.visual.color}`
|
|
3680
|
+
},
|
|
3681
|
+
children: [
|
|
3682
|
+
/* @__PURE__ */ jsxs10("p", { className: "leading-tight mb-1", children: [
|
|
3683
|
+
/* @__PURE__ */ jsx11(ArrowDown, { className: "inline-block mr-1 opacity-70 align-middle" }),
|
|
3684
|
+
/* @__PURE__ */ jsx11("strong", { className: "font-semibold align-middle", children: edge.label_ })
|
|
3685
|
+
] }),
|
|
3686
|
+
edge.id_ && /* @__PURE__ */ jsx11(
|
|
3687
|
+
"p",
|
|
3688
|
+
{
|
|
3689
|
+
className: "text-xs text-muted-foreground select-all cursor-pointer mt-1 mb-2",
|
|
3690
|
+
title: "Click to copy edge id",
|
|
3691
|
+
onClick: () => copyTextToClipboard2(String(edge.id_)),
|
|
3692
|
+
children: edge.id_
|
|
3693
|
+
}
|
|
3694
|
+
),
|
|
3695
|
+
edge.properties_ && Object.keys(edge.properties_).length > 0 && /* @__PURE__ */ jsxs10(Collapsible2, { open: showProperties, onOpenChange: setShowProperties, children: [
|
|
3696
|
+
/* @__PURE__ */ jsx11(CollapsibleTrigger2, { asChild: true, children: /* @__PURE__ */ jsx11("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
|
|
3697
|
+
/* @__PURE__ */ jsx11(CollapsibleContent2, { children: /* @__PURE__ */ jsxs10("ul", { className: "list-none mt-2", children: [
|
|
3698
|
+
/* @__PURE__ */ jsx11("p", { className: "text-sm font-semibold mb-1 tracking-wide", children: "Properties" }),
|
|
3699
|
+
Object.entries(edge.properties_ || {}).map(([key, value]) => /* @__PURE__ */ jsxs10("li", { className: "mb-2 leading-tight", children: [
|
|
3700
|
+
/* @__PURE__ */ jsx11("p", { className: "text-xs text-slate-600 uppercase tracking-wide", children: key }),
|
|
3701
|
+
/* @__PURE__ */ jsx11(
|
|
3702
|
+
"p",
|
|
3703
|
+
{
|
|
3704
|
+
className: "text-sm truncate max-w-[190px] cursor-pointer",
|
|
3705
|
+
onClick: () => copyTextToClipboard2(String(value)),
|
|
3706
|
+
title: "Click to copy value",
|
|
3707
|
+
children: String(value)
|
|
3708
|
+
}
|
|
3709
|
+
)
|
|
3710
|
+
] }, key))
|
|
3711
|
+
] }) })
|
|
3712
|
+
] })
|
|
3713
|
+
]
|
|
3714
|
+
}
|
|
3715
|
+
);
|
|
3716
|
+
}
|
|
3717
|
+
var EdgeInfoCard_default = EdgeInfoCard;
|
|
3718
|
+
|
|
3719
|
+
// src/components/ActionPanel/Overview.tsx
|
|
3720
|
+
import { Badge } from "@petrarca/sonnet-ui";
|
|
3721
|
+
import { formatNumber, devLog as devLog11 } from "@petrarca/sonnet-core";
|
|
3722
|
+
import { useMemo as useMemo4 } from "react";
|
|
3723
|
+
import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3724
|
+
function computeEdgeToNodeLabels(nodesById, edgesById) {
|
|
3725
|
+
const mapping = {};
|
|
3726
|
+
for (const edge of Object.values(edgesById)) {
|
|
3727
|
+
const edgeLabel = edge.label_;
|
|
3728
|
+
const startNode = nodesById[String(edge.start_id_)];
|
|
3729
|
+
const endNode = nodesById[String(edge.end_id_)];
|
|
3730
|
+
if (!mapping[edgeLabel]) {
|
|
3731
|
+
mapping[edgeLabel] = /* @__PURE__ */ new Set();
|
|
3732
|
+
}
|
|
3733
|
+
if (startNode) mapping[edgeLabel].add(startNode.label_);
|
|
3734
|
+
if (endNode) mapping[edgeLabel].add(endNode.label_);
|
|
3735
|
+
}
|
|
3736
|
+
const result = {};
|
|
3737
|
+
for (const [edgeLabel, nodeLabels] of Object.entries(mapping)) {
|
|
3738
|
+
result[edgeLabel] = Array.from(nodeLabels);
|
|
3739
|
+
}
|
|
3740
|
+
return result;
|
|
3741
|
+
}
|
|
3742
|
+
function resolveBadgeColor(isAllDisabled, filters, label, colorPalette, activeColor) {
|
|
3743
|
+
if (isAllDisabled) return colorPalette["faded"];
|
|
3744
|
+
if (filters.length === 0 || filters.includes(label))
|
|
3745
|
+
return colorPalette[activeColor];
|
|
3746
|
+
return colorPalette["faded"];
|
|
3747
|
+
}
|
|
3748
|
+
function AllToggleBadge({
|
|
3749
|
+
allDisabled,
|
|
3750
|
+
colorPalette,
|
|
3751
|
+
count,
|
|
3752
|
+
onToggle,
|
|
3753
|
+
badgeKey
|
|
3754
|
+
}) {
|
|
3755
|
+
const bgColor = allDisabled ? colorPalette["faded"] : colorPalette["neutral"];
|
|
3756
|
+
return /* @__PURE__ */ jsxs11(
|
|
3757
|
+
Badge,
|
|
3758
|
+
{
|
|
3759
|
+
className: "cursor-pointer text-sm px-3 py-1",
|
|
3760
|
+
style: {
|
|
3761
|
+
textTransform: "none",
|
|
3762
|
+
backgroundColor: bgColor,
|
|
3763
|
+
borderRadius: "10px"
|
|
3764
|
+
},
|
|
3765
|
+
onClick: onToggle,
|
|
3766
|
+
children: [
|
|
3767
|
+
"* (",
|
|
3768
|
+
formatNumber(count),
|
|
3769
|
+
")"
|
|
3770
|
+
]
|
|
3771
|
+
},
|
|
3772
|
+
badgeKey
|
|
3773
|
+
);
|
|
3774
|
+
}
|
|
3775
|
+
function NodeLabelBadges({
|
|
3776
|
+
labels,
|
|
3777
|
+
labelCounts,
|
|
3778
|
+
colorPalette,
|
|
3779
|
+
allLabelsDisabled,
|
|
3780
|
+
nodeLabelFilters,
|
|
3781
|
+
toggleNodeLabelFilter
|
|
3782
|
+
}) {
|
|
3783
|
+
return labels.map((label) => {
|
|
3784
|
+
const bgColor = resolveBadgeColor(
|
|
3785
|
+
allLabelsDisabled,
|
|
3786
|
+
nodeLabelFilters,
|
|
3787
|
+
label,
|
|
3788
|
+
colorPalette,
|
|
3789
|
+
label
|
|
3790
|
+
);
|
|
3791
|
+
return /* @__PURE__ */ jsxs11(
|
|
3792
|
+
Badge,
|
|
3793
|
+
{
|
|
3794
|
+
className: "cursor-pointer text-sm px-3 py-1",
|
|
3795
|
+
style: {
|
|
3796
|
+
textTransform: "none",
|
|
3797
|
+
backgroundColor: bgColor,
|
|
3798
|
+
borderRadius: "10px"
|
|
3799
|
+
},
|
|
3800
|
+
onClick: () => toggleNodeLabelFilter(label, labels),
|
|
3801
|
+
children: [
|
|
3802
|
+
label,
|
|
3803
|
+
" (",
|
|
3804
|
+
formatNumber(labelCounts[label]),
|
|
3805
|
+
")"
|
|
3806
|
+
]
|
|
3807
|
+
},
|
|
3808
|
+
label
|
|
3809
|
+
);
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
function EdgeLabelBadges({
|
|
3813
|
+
labels,
|
|
3814
|
+
edgeLabelCounts,
|
|
3815
|
+
colorPalette,
|
|
3816
|
+
allEdgeLabelsDisabled,
|
|
3817
|
+
edgeLabelFilters,
|
|
3818
|
+
toggleEdgeLabelFilter,
|
|
3819
|
+
edgeToNodeLabels
|
|
3820
|
+
}) {
|
|
3821
|
+
return labels.map((label) => {
|
|
3822
|
+
const bgColor = resolveBadgeColor(
|
|
3823
|
+
allEdgeLabelsDisabled,
|
|
3824
|
+
edgeLabelFilters,
|
|
3825
|
+
label,
|
|
3826
|
+
colorPalette,
|
|
3827
|
+
"neutral"
|
|
3828
|
+
);
|
|
3829
|
+
return /* @__PURE__ */ jsxs11(
|
|
3830
|
+
Badge,
|
|
3831
|
+
{
|
|
3832
|
+
className: "cursor-pointer text-sm px-3 py-1",
|
|
3833
|
+
style: {
|
|
3834
|
+
textTransform: "none",
|
|
3835
|
+
backgroundColor: bgColor,
|
|
3836
|
+
borderRadius: "10px"
|
|
3837
|
+
},
|
|
3838
|
+
onClick: () => toggleEdgeLabelFilter(label, labels, edgeToNodeLabels),
|
|
3839
|
+
children: [
|
|
3840
|
+
label,
|
|
3841
|
+
" (",
|
|
3842
|
+
formatNumber(edgeLabelCounts[label]),
|
|
3843
|
+
")"
|
|
3844
|
+
]
|
|
3845
|
+
},
|
|
3846
|
+
label
|
|
3847
|
+
);
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
function GraphOverview({
|
|
3851
|
+
stats,
|
|
3852
|
+
colorPalette,
|
|
3853
|
+
labelCounts,
|
|
3854
|
+
edgeLabelCounts,
|
|
3855
|
+
edgeToNodeLabels
|
|
3856
|
+
}) {
|
|
3857
|
+
const useGraphFilter2 = useWorkspaceGraphFilter();
|
|
3858
|
+
const nodeLabelFilters = useGraphFilter2((state) => state.nodeLabelFilters);
|
|
3859
|
+
const allLabelsDisabled = useGraphFilter2((state) => state.allLabelsDisabled);
|
|
3860
|
+
const edgeLabelFilters = useGraphFilter2((state) => state.edgeLabelFilters);
|
|
3861
|
+
const allEdgeLabelsDisabled = useGraphFilter2(
|
|
3862
|
+
(state) => state.allEdgeLabelsDisabled
|
|
3863
|
+
);
|
|
3864
|
+
const toggleNodeLabelFilter = useGraphFilter2(
|
|
3865
|
+
(state) => state.toggleNodeLabelFilter
|
|
3866
|
+
);
|
|
3867
|
+
const clearNodeLabelFilters = useGraphFilter2(
|
|
3868
|
+
(state) => state.clearNodeLabelFilters
|
|
3869
|
+
);
|
|
3870
|
+
const toggleAllLabelsDisabled = useGraphFilter2(
|
|
3871
|
+
(state) => state.toggleAllLabelsDisabled
|
|
3872
|
+
);
|
|
3873
|
+
const toggleEdgeLabelFilter = useGraphFilter2(
|
|
3874
|
+
(state) => state.toggleEdgeLabelFilter
|
|
3875
|
+
);
|
|
3876
|
+
const clearEdgeLabelFilters = useGraphFilter2(
|
|
3877
|
+
(state) => state.clearEdgeLabelFilters
|
|
3878
|
+
);
|
|
3879
|
+
const toggleAllEdgeLabelsDisabled = useGraphFilter2(
|
|
3880
|
+
(state) => state.toggleAllEdgeLabelsDisabled
|
|
3881
|
+
);
|
|
3882
|
+
const allLabels = Object.keys(labelCounts).sort();
|
|
3883
|
+
devLog11("[Overview] Rendering labels", {
|
|
3884
|
+
labels: allLabels,
|
|
3885
|
+
colorKeys: Object.keys(colorPalette || {}),
|
|
3886
|
+
labelCounts,
|
|
3887
|
+
stats
|
|
3888
|
+
});
|
|
3889
|
+
const allEdgeLabels = Object.keys(edgeLabelCounts || {}).sort();
|
|
3890
|
+
const handleNodeToggle = () => {
|
|
3891
|
+
if (allLabelsDisabled) {
|
|
3892
|
+
toggleAllLabelsDisabled();
|
|
3893
|
+
clearNodeLabelFilters();
|
|
3894
|
+
} else {
|
|
3895
|
+
toggleAllLabelsDisabled();
|
|
3896
|
+
}
|
|
3897
|
+
};
|
|
3898
|
+
const handleEdgeToggle = () => {
|
|
3899
|
+
if (allEdgeLabelsDisabled) {
|
|
3900
|
+
toggleAllEdgeLabelsDisabled();
|
|
3901
|
+
clearEdgeLabelFilters();
|
|
3902
|
+
} else {
|
|
3903
|
+
toggleAllEdgeLabelsDisabled();
|
|
3904
|
+
}
|
|
3905
|
+
};
|
|
3906
|
+
return /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
3907
|
+
/* @__PURE__ */ jsx12("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
|
|
3908
|
+
allLabels.length > 0 && /* @__PURE__ */ jsxs11("div", { children: [
|
|
3909
|
+
/* @__PURE__ */ jsx12("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Node Labels" }),
|
|
3910
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex flex-col items-start gap-2", children: [
|
|
3911
|
+
/* @__PURE__ */ jsx12(
|
|
3912
|
+
AllToggleBadge,
|
|
3913
|
+
{
|
|
3914
|
+
allDisabled: allLabelsDisabled,
|
|
3915
|
+
colorPalette,
|
|
3916
|
+
count: stats.nodes,
|
|
3917
|
+
onToggle: handleNodeToggle,
|
|
3918
|
+
badgeKey: "all"
|
|
3919
|
+
}
|
|
3920
|
+
),
|
|
3921
|
+
/* @__PURE__ */ jsx12(
|
|
3922
|
+
NodeLabelBadges,
|
|
3923
|
+
{
|
|
3924
|
+
labels: allLabels,
|
|
3925
|
+
labelCounts,
|
|
3926
|
+
colorPalette,
|
|
3927
|
+
allLabelsDisabled,
|
|
3928
|
+
nodeLabelFilters,
|
|
3929
|
+
toggleNodeLabelFilter
|
|
3930
|
+
}
|
|
3931
|
+
)
|
|
3932
|
+
] })
|
|
3933
|
+
] }),
|
|
3934
|
+
allEdgeLabels.length > 0 && /* @__PURE__ */ jsxs11("div", { children: [
|
|
3935
|
+
/* @__PURE__ */ jsx12("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Edge Types" }),
|
|
3936
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex flex-col items-start gap-2", children: [
|
|
3937
|
+
/* @__PURE__ */ jsx12(
|
|
3938
|
+
AllToggleBadge,
|
|
3939
|
+
{
|
|
3940
|
+
allDisabled: allEdgeLabelsDisabled,
|
|
3941
|
+
colorPalette,
|
|
3942
|
+
count: stats.edges,
|
|
3943
|
+
onToggle: handleEdgeToggle,
|
|
3944
|
+
badgeKey: "edge-all"
|
|
3945
|
+
}
|
|
3946
|
+
),
|
|
3947
|
+
/* @__PURE__ */ jsx12(
|
|
3948
|
+
EdgeLabelBadges,
|
|
3949
|
+
{
|
|
3950
|
+
labels: allEdgeLabels,
|
|
3951
|
+
edgeLabelCounts,
|
|
3952
|
+
colorPalette,
|
|
3953
|
+
allEdgeLabelsDisabled,
|
|
3954
|
+
edgeLabelFilters,
|
|
3955
|
+
toggleEdgeLabelFilter,
|
|
3956
|
+
edgeToNodeLabels
|
|
3957
|
+
}
|
|
3958
|
+
)
|
|
3959
|
+
] })
|
|
3960
|
+
] })
|
|
3961
|
+
] });
|
|
3962
|
+
}
|
|
3963
|
+
function Overview() {
|
|
3964
|
+
const { stats, colorPalette, labelCounts, edgeLabelCounts, baseNormalized } = useLiveGraphData();
|
|
3965
|
+
const edgeToNodeLabels = useMemo4(() => {
|
|
3966
|
+
if (!baseNormalized?.nodesById || !baseNormalized?.edgesById) return {};
|
|
3967
|
+
return computeEdgeToNodeLabels(
|
|
3968
|
+
baseNormalized.nodesById,
|
|
3969
|
+
baseNormalized.edgesById
|
|
3970
|
+
);
|
|
3971
|
+
}, [baseNormalized]);
|
|
3972
|
+
if (stats && stats.values != null && stats.values > 0) {
|
|
3973
|
+
return /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
3974
|
+
/* @__PURE__ */ jsx12("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
|
|
3975
|
+
/* @__PURE__ */ jsxs11("p", { className: "text-sm text-slate-700", children: [
|
|
3976
|
+
formatNumber(stats.values),
|
|
3977
|
+
" Values"
|
|
3978
|
+
] })
|
|
3979
|
+
] });
|
|
3980
|
+
}
|
|
3981
|
+
if (stats && stats.nodes > 0 && colorPalette) {
|
|
3982
|
+
return /* @__PURE__ */ jsx12(
|
|
3983
|
+
GraphOverview,
|
|
3984
|
+
{
|
|
3985
|
+
stats,
|
|
3986
|
+
colorPalette,
|
|
3987
|
+
labelCounts,
|
|
3988
|
+
edgeLabelCounts,
|
|
3989
|
+
edgeToNodeLabels
|
|
3990
|
+
}
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
return null;
|
|
3994
|
+
}
|
|
3995
|
+
var Overview_default = Overview;
|
|
3996
|
+
|
|
3997
|
+
// src/components/ActionPanel/ActionPanel.tsx
|
|
3998
|
+
import { useMemo as useMemo5 } from "react";
|
|
3999
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
4000
|
+
function ActionPanel({ actions }) {
|
|
4001
|
+
const useSelection3 = useWorkspaceSelection();
|
|
4002
|
+
const selectedNodeIds = useSelection3(
|
|
4003
|
+
(s) => s.selectedNodeIds
|
|
4004
|
+
);
|
|
4005
|
+
const selectedEdge = useSelection3((s) => s.selectedEdge);
|
|
4006
|
+
const live = useLiveGraphData();
|
|
4007
|
+
const selectedNodes = useMemo5(
|
|
4008
|
+
() => selectedNodeIds.map((id) => live.normalized.nodesById[id]).filter(Boolean),
|
|
4009
|
+
[selectedNodeIds, live.normalized]
|
|
4010
|
+
);
|
|
4011
|
+
const firstNode = selectedNodes[0];
|
|
4012
|
+
const secondNode = selectedNodes?.[1];
|
|
4013
|
+
const hasEdge = !!selectedEdge;
|
|
4014
|
+
const showOverview = !firstNode && !hasEdge;
|
|
4015
|
+
return /* @__PURE__ */ jsxs12(
|
|
4016
|
+
Card5,
|
|
4017
|
+
{
|
|
4018
|
+
className: "h-full min-w-64 flex flex-col bg-gray-50 shadow-xs p-4",
|
|
4019
|
+
style: { maxHeight: "100%", overflow: "hidden" },
|
|
4020
|
+
children: [
|
|
4021
|
+
actions && /* @__PURE__ */ jsx13("div", { className: "flex-shrink-0 mb-2", children: actions }),
|
|
4022
|
+
/* @__PURE__ */ jsx13("div", { className: "flex-1 overflow-auto pr-1", children: /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-4", children: [
|
|
4023
|
+
firstNode && /* @__PURE__ */ jsx13(NodeInfoCard_default, { node: firstNode }),
|
|
4024
|
+
hasEdge && selectedEdge && /* @__PURE__ */ jsx13(EdgeInfoCard_default, { edge: selectedEdge }),
|
|
4025
|
+
secondNode && /* @__PURE__ */ jsx13(NodeInfoCard_default, { node: secondNode }),
|
|
4026
|
+
showOverview && /* @__PURE__ */ jsx13(Overview_default, {})
|
|
4027
|
+
] }) })
|
|
4028
|
+
]
|
|
4029
|
+
}
|
|
4030
|
+
);
|
|
4031
|
+
}
|
|
4032
|
+
var ActionPanel_default = ActionPanel;
|
|
4033
|
+
export {
|
|
4034
|
+
ActionPanel_default as ActionPanel,
|
|
4035
|
+
EMPTY_NORMALIZED,
|
|
4036
|
+
EdgeInfoCard_default as EdgeInfoCard,
|
|
4037
|
+
GraphRendererProvider,
|
|
4038
|
+
GraphStoresContext,
|
|
4039
|
+
GraphVisualizer_default as GraphVisualizer,
|
|
4040
|
+
NodeInfoCard_default as NodeInfoCard,
|
|
4041
|
+
Overview_default as Overview,
|
|
4042
|
+
buildColorPalette,
|
|
4043
|
+
buildColorPaletteFromNodes,
|
|
4044
|
+
createActionPanelStore,
|
|
4045
|
+
createGraphDataStore,
|
|
4046
|
+
createGraphExplorationStore,
|
|
4047
|
+
createGraphFilterStore,
|
|
4048
|
+
createGraphFocusStore,
|
|
4049
|
+
createSelectionStore,
|
|
4050
|
+
enrichEdge,
|
|
4051
|
+
enrichEdges,
|
|
4052
|
+
enrichNode,
|
|
4053
|
+
enrichNodes,
|
|
4054
|
+
extractDisplayLabel,
|
|
4055
|
+
isRawGraphNode,
|
|
4056
|
+
isVisualEdge,
|
|
4057
|
+
isVisualNode,
|
|
4058
|
+
normalizeGraph,
|
|
4059
|
+
toCytoscapeEdge,
|
|
4060
|
+
toCytoscapeNode,
|
|
4061
|
+
toReagraphEdge,
|
|
4062
|
+
toReagraphNode,
|
|
4063
|
+
useActionPanelStore,
|
|
4064
|
+
useCytoscapeData,
|
|
4065
|
+
GraphFocus_default as useGraphFocus,
|
|
4066
|
+
useGraphRenderer,
|
|
4067
|
+
useLiveGraphData,
|
|
4068
|
+
useReagraphData,
|
|
4069
|
+
useWorkspaceGraphData,
|
|
4070
|
+
useWorkspaceGraphExploration,
|
|
4071
|
+
useWorkspaceGraphFilter,
|
|
4072
|
+
useWorkspaceSelection
|
|
4073
|
+
};
|
|
4074
|
+
//# sourceMappingURL=index.js.map
|