@graph-render/react 1.0.1 → 1.1.0

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.
@@ -115,7 +115,10 @@ const buildFallbackEdges = (
115
115
  ...edge,
116
116
  type: edge.type ?? EdgeType.Directed,
117
117
  points: [
118
- { x: right - Math.min(sourceWidth * 0.25, 18), y: top + Math.min(sourceHeight * 0.35, 18) },
118
+ {
119
+ x: right - Math.min(sourceWidth * 0.25, 18),
120
+ y: top + Math.min(sourceHeight * 0.35, 18),
121
+ },
119
122
  { x: right + 28, y: top - 20 },
120
123
  { x: right + 36, y: top + sourceHeight / 2 },
121
124
  { x: right - Math.min(sourceWidth * 0.25, 18), y: top + sourceHeight * 0.8 },
@@ -175,7 +178,13 @@ interface UseGraphModelOptions {
175
178
  * - `'routing'` — the default `routeEdges` threw
176
179
  * - `'routing-override'`— a `routeEdgesOverride` threw (default was used as fallback)
177
180
  */
178
- onError?: (error: Error, context: { graph: NxGraphInput; phase: 'layout' | 'layout-override' | 'routing' | 'routing-override' }) => void;
181
+ onError?: (
182
+ error: Error,
183
+ context: {
184
+ graph: NxGraphInput;
185
+ phase: 'layout' | 'layout-override' | 'routing' | 'routing-override';
186
+ }
187
+ ) => void;
179
188
  }
180
189
 
181
190
  export interface GraphModelResult {
@@ -211,10 +220,15 @@ export const useGraphModel = ({
211
220
  >({});
212
221
 
213
222
  const { nodes: sourceNodes, edges: sourceEdges } = useMemo(
214
- () => fromTypedNxGraph(graph, config.defaultEdgeType),
215
- [config.defaultEdgeType, graph]
223
+ () =>
224
+ fromTypedNxGraph(graph, config.defaultEdgeType, {
225
+ inputValidationMode: config.inputValidationMode,
226
+ }),
227
+ [config.defaultEdgeType, config.inputValidationMode, graph]
216
228
  );
217
229
 
230
+ const allowDegradedGraph = config.failureBehavior === 'degrade';
231
+
218
232
  const nodesWithMeasuredSize = useMemo(
219
233
  () =>
220
234
  sourceNodes.map((node) => ({
@@ -307,65 +321,70 @@ export const useGraphModel = ({
307
321
  [config.nodeSizing]
308
322
  );
309
323
 
310
- const positionedNodes: PositionedNode[] = useMemo(
311
- () => {
312
- if (!layoutNodesOverride) {
313
- try {
314
- const laidOutNodes = layoutNodes(layoutOptions);
315
- validatePositionedNodes(laidOutNodes, visibleNodes, 'layout');
316
- return laidOutNodes;
317
- } catch (error) {
318
- onError?.(toError(error), {
319
- graph,
320
- phase: 'layout',
321
- });
322
-
323
- try {
324
- const fallbackNodes = buildFallbackLayout(layoutOptions);
325
- validatePositionedNodes(fallbackNodes, visibleNodes, 'layout');
326
- return fallbackNodes;
327
- } catch (fallbackError) {
328
- const normalizedFallbackError = toError(fallbackError);
329
- onError?.(normalizedFallbackError, {
330
- graph,
331
- phase: 'layout',
332
- });
333
- throw normalizedFallbackError;
334
- }
335
- }
336
- }
337
-
324
+ const positionedNodes: PositionedNode[] = useMemo(() => {
325
+ if (!layoutNodesOverride) {
338
326
  try {
339
- const overrideNodes = layoutNodesOverride(layoutOptions);
340
- validatePositionedNodes(overrideNodes, visibleNodes, 'layout override');
341
- return overrideNodes;
327
+ const laidOutNodes = layoutNodes(layoutOptions);
328
+ validatePositionedNodes(laidOutNodes, visibleNodes, 'layout');
329
+ return laidOutNodes;
342
330
  } catch (error) {
343
331
  onError?.(toError(error), {
344
332
  graph,
345
- phase: 'layout-override',
333
+ phase: 'layout',
346
334
  });
347
335
 
336
+ if (!allowDegradedGraph) {
337
+ throw toError(error);
338
+ }
339
+
348
340
  try {
349
- const fallbackNodes = layoutNodes(layoutOptions);
341
+ const fallbackNodes = buildFallbackLayout(layoutOptions);
350
342
  validatePositionedNodes(fallbackNodes, visibleNodes, 'layout');
351
343
  return fallbackNodes;
352
344
  } catch (fallbackError) {
353
- onError?.(toError(fallbackError), { graph, phase: 'layout' });
354
-
355
- try {
356
- const finalFallbackNodes = buildFallbackLayout(layoutOptions);
357
- validatePositionedNodes(finalFallbackNodes, visibleNodes, 'layout');
358
- return finalFallbackNodes;
359
- } catch (finalFallbackError) {
360
- const normalizedFinalFallbackError = toError(finalFallbackError);
361
- onError?.(normalizedFinalFallbackError, { graph, phase: 'layout' });
362
- throw normalizedFinalFallbackError;
363
- }
345
+ const normalizedFallbackError = toError(fallbackError);
346
+ onError?.(normalizedFallbackError, {
347
+ graph,
348
+ phase: 'layout',
349
+ });
350
+ throw normalizedFallbackError;
364
351
  }
365
352
  }
366
- },
367
- [graph, layoutNodesOverride, layoutOptions, onError, visibleNodes]
368
- );
353
+ }
354
+
355
+ try {
356
+ const overrideNodes = layoutNodesOverride(layoutOptions);
357
+ validatePositionedNodes(overrideNodes, visibleNodes, 'layout override');
358
+ return overrideNodes;
359
+ } catch (error) {
360
+ onError?.(toError(error), {
361
+ graph,
362
+ phase: 'layout-override',
363
+ });
364
+
365
+ if (!allowDegradedGraph) {
366
+ throw toError(error);
367
+ }
368
+
369
+ try {
370
+ const fallbackNodes = layoutNodes(layoutOptions);
371
+ validatePositionedNodes(fallbackNodes, visibleNodes, 'layout');
372
+ return fallbackNodes;
373
+ } catch (fallbackError) {
374
+ onError?.(toError(fallbackError), { graph, phase: 'layout' });
375
+
376
+ try {
377
+ const finalFallbackNodes = buildFallbackLayout(layoutOptions);
378
+ validatePositionedNodes(finalFallbackNodes, visibleNodes, 'layout');
379
+ return finalFallbackNodes;
380
+ } catch (finalFallbackError) {
381
+ const normalizedFinalFallbackError = toError(finalFallbackError);
382
+ onError?.(normalizedFinalFallbackError, { graph, phase: 'layout' });
383
+ throw normalizedFinalFallbackError;
384
+ }
385
+ }
386
+ }
387
+ }, [graph, layoutNodesOverride, layoutOptions, onError, visibleNodes]);
369
388
 
370
389
  const edgeRoutingOptions = useMemo(
371
390
  () => ({
@@ -380,57 +399,62 @@ export const useGraphModel = ({
380
399
  [config]
381
400
  );
382
401
 
383
- const positionedEdges: PositionedEdge[] = useMemo(
384
- () => {
385
- const nodeIds = new Set(positionedNodes.map((node) => node.id));
386
-
387
- if (!routeEdgesOverride) {
388
- try {
389
- const routedEdges = routeEdges(positionedNodes, visibleEdges, edgeRoutingOptions);
390
- validatePositionedEdges(routedEdges, nodeIds, 'routing');
391
- return routedEdges;
392
- } catch (error) {
393
- onError?.(toError(error), {
394
- graph,
395
- phase: 'routing',
396
- });
397
- const fallbackEdges = buildFallbackEdges(positionedNodes, visibleEdges);
398
- validatePositionedEdges(fallbackEdges, nodeIds, 'routing');
399
- return fallbackEdges;
400
- }
401
- }
402
+ const positionedEdges: PositionedEdge[] = useMemo(() => {
403
+ const nodeIds = new Set(positionedNodes.map((node) => node.id));
402
404
 
405
+ if (!routeEdgesOverride) {
403
406
  try {
404
- const overrideEdges = routeEdgesOverride(positionedNodes, visibleEdges, edgeRoutingOptions);
405
- validatePositionedEdges(overrideEdges, nodeIds, 'routing override');
406
- return overrideEdges;
407
+ const routedEdges = routeEdges(positionedNodes, visibleEdges, edgeRoutingOptions);
408
+ validatePositionedEdges(routedEdges, nodeIds, 'routing');
409
+ return routedEdges;
407
410
  } catch (error) {
408
411
  onError?.(toError(error), {
409
412
  graph,
410
- phase: 'routing-override',
413
+ phase: 'routing',
411
414
  });
412
-
413
- try {
414
- const fallbackEdges = routeEdges(positionedNodes, visibleEdges, edgeRoutingOptions);
415
- validatePositionedEdges(fallbackEdges, nodeIds, 'routing');
416
- return fallbackEdges;
417
- } catch (fallbackError) {
418
- onError?.(toError(fallbackError), { graph, phase: 'routing' });
419
- const finalFallbackEdges = buildFallbackEdges(positionedNodes, visibleEdges);
420
- validatePositionedEdges(finalFallbackEdges, nodeIds, 'routing');
421
- return finalFallbackEdges;
415
+ if (!allowDegradedGraph) {
416
+ throw toError(error);
422
417
  }
418
+ const fallbackEdges = buildFallbackEdges(positionedNodes, visibleEdges);
419
+ validatePositionedEdges(fallbackEdges, nodeIds, 'routing');
420
+ return fallbackEdges;
423
421
  }
424
- },
425
- [
426
- edgeRoutingOptions,
427
- graph,
428
- onError,
429
- positionedNodes,
430
- routeEdgesOverride,
431
- visibleEdges,
432
- ]
433
- );
422
+ }
423
+
424
+ try {
425
+ const overrideEdges = routeEdgesOverride(positionedNodes, visibleEdges, edgeRoutingOptions);
426
+ validatePositionedEdges(overrideEdges, nodeIds, 'routing override');
427
+ return overrideEdges;
428
+ } catch (error) {
429
+ onError?.(toError(error), {
430
+ graph,
431
+ phase: 'routing-override',
432
+ });
433
+
434
+ if (!allowDegradedGraph) {
435
+ throw toError(error);
436
+ }
437
+
438
+ try {
439
+ const fallbackEdges = routeEdges(positionedNodes, visibleEdges, edgeRoutingOptions);
440
+ validatePositionedEdges(fallbackEdges, nodeIds, 'routing');
441
+ return fallbackEdges;
442
+ } catch (fallbackError) {
443
+ onError?.(toError(fallbackError), { graph, phase: 'routing' });
444
+ const finalFallbackEdges = buildFallbackEdges(positionedNodes, visibleEdges);
445
+ validatePositionedEdges(finalFallbackEdges, nodeIds, 'routing');
446
+ return finalFallbackEdges;
447
+ }
448
+ }
449
+ }, [
450
+ edgeRoutingOptions,
451
+ graph,
452
+ onError,
453
+ allowDegradedGraph,
454
+ positionedNodes,
455
+ routeEdgesOverride,
456
+ visibleEdges,
457
+ ]);
434
458
 
435
459
  return {
436
460
  childNodeIdsByParent,
@@ -100,23 +100,21 @@ export const useGraphSearchState = <
100
100
  return { nodeIds: [], edgeIds: [] };
101
101
  }
102
102
 
103
- return (
104
- (() => {
105
- try {
106
- return (
107
- highlightStrategy?.({
108
- nodes,
109
- edges,
110
- query: searchQuery,
111
- matchedNodeIds: searchMatchedNodeIds,
112
- matchedEdgeIds: searchMatchedEdgeIds,
113
- }) ?? { nodeIds: [], edgeIds: [] }
114
- );
115
- } catch {
116
- return { nodeIds: [], edgeIds: [] };
117
- }
118
- })()
119
- );
103
+ return (() => {
104
+ try {
105
+ return (
106
+ highlightStrategy?.({
107
+ nodes,
108
+ edges,
109
+ query: searchQuery,
110
+ matchedNodeIds: searchMatchedNodeIds,
111
+ matchedEdgeIds: searchMatchedEdgeIds,
112
+ }) ?? { nodeIds: [], edgeIds: [] }
113
+ );
114
+ } catch {
115
+ return { nodeIds: [], edgeIds: [] };
116
+ }
117
+ })();
120
118
  }, [edges, highlightStrategy, nodes, searchMatchedEdgeIds, searchMatchedNodeIds, searchQuery]);
121
119
 
122
120
  const effectiveHighlightedNodeSet = useMemo(
@@ -0,0 +1,151 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ GraphSelection,
4
+ GraphViewport,
5
+ GraphProps,
6
+ GraphHandle,
7
+ NxGraphInput,
8
+ PositionedNode,
9
+ PositionedEdge,
10
+ NodeData,
11
+ EdgeData,
12
+ } from '@graph-render/types';
13
+ import { clampZoom } from '../utils/viewport';
14
+
15
+ const DEFAULT_VIEWPORT: GraphViewport = { x: 0, y: 0, zoom: 1 };
16
+
17
+ const normalizeViewport = (
18
+ viewport: GraphViewport,
19
+ minZoom: number,
20
+ maxZoom: number
21
+ ): GraphViewport => ({
22
+ x: Number.isFinite(viewport.x) ? viewport.x : 0,
23
+ y: Number.isFinite(viewport.y) ? viewport.y : 0,
24
+ zoom: clampZoom(Number.isFinite(viewport.zoom) ? viewport.zoom : 1, minZoom, maxZoom),
25
+ });
26
+
27
+ interface UseGraphViewStateOptions {
28
+ controlledViewport: GraphViewport | undefined;
29
+ defaultViewport: Partial<GraphViewport> | undefined;
30
+ safeMinZoom: number;
31
+ safeMaxZoom: number;
32
+ onViewportChange: ((viewport: GraphViewport) => void) | undefined;
33
+ selectedNodeIds: string[] | undefined;
34
+ selectedEdgeIds: string[] | undefined;
35
+ defaultSelectedNodeIds: string[] | undefined;
36
+ defaultSelectedEdgeIds: string[] | undefined;
37
+ onSelectionChange: ((selection: GraphSelection) => void) | undefined;
38
+ controlledFocusedNodeId: string | null | undefined;
39
+ defaultFocusedNodeId: string | null;
40
+ onFocusedNodeChange: ((nodeId: string | null) => void) | undefined;
41
+ }
42
+
43
+ interface UseGraphViewStateResult {
44
+ viewport: GraphViewport;
45
+ viewportRef: React.MutableRefObject<GraphViewport>;
46
+ selection: GraphSelection;
47
+ selectionRef: React.MutableRefObject<GraphSelection>;
48
+ focusedNodeId: string | null;
49
+ updateViewport: GraphHandle['setViewport'];
50
+ updateSelection: (next: GraphSelection | ((current: GraphSelection) => GraphSelection)) => void;
51
+ updateFocusedNode: (nodeId: string | null) => void;
52
+ }
53
+
54
+ export const useGraphViewState = ({
55
+ controlledViewport,
56
+ defaultViewport,
57
+ safeMinZoom,
58
+ safeMaxZoom,
59
+ onViewportChange,
60
+ selectedNodeIds,
61
+ selectedEdgeIds,
62
+ defaultSelectedNodeIds,
63
+ defaultSelectedEdgeIds,
64
+ onSelectionChange,
65
+ controlledFocusedNodeId,
66
+ defaultFocusedNodeId,
67
+ onFocusedNodeChange,
68
+ }: UseGraphViewStateOptions): UseGraphViewStateResult => {
69
+ const [internalViewport, setInternalViewport] = useState<GraphViewport>(() =>
70
+ normalizeViewport({ ...DEFAULT_VIEWPORT, ...(defaultViewport ?? {}) }, safeMinZoom, safeMaxZoom)
71
+ );
72
+ const [internalSelection, setInternalSelection] = useState<GraphSelection>({
73
+ nodeIds: defaultSelectedNodeIds ?? [],
74
+ edgeIds: defaultSelectedEdgeIds ?? [],
75
+ });
76
+ const [internalFocusedNodeId, setInternalFocusedNodeId] = useState<string | null>(
77
+ defaultFocusedNodeId
78
+ );
79
+
80
+ const viewport = useMemo(
81
+ () => normalizeViewport(controlledViewport ?? internalViewport, safeMinZoom, safeMaxZoom),
82
+ [controlledViewport, internalViewport, safeMaxZoom, safeMinZoom]
83
+ );
84
+ const viewportRef = useRef(viewport);
85
+ viewportRef.current = viewport;
86
+ const onViewportChangeRef = useRef(onViewportChange);
87
+ onViewportChangeRef.current = onViewportChange;
88
+
89
+ const selection = useMemo<GraphSelection>(
90
+ () => ({
91
+ nodeIds: selectedNodeIds ?? internalSelection.nodeIds,
92
+ edgeIds: selectedEdgeIds ?? internalSelection.edgeIds,
93
+ }),
94
+ [selectedNodeIds, selectedEdgeIds, internalSelection]
95
+ );
96
+ const selectionRef = useRef(selection);
97
+ selectionRef.current = selection;
98
+
99
+ const focusedNodeId =
100
+ controlledFocusedNodeId !== undefined ? controlledFocusedNodeId : internalFocusedNodeId;
101
+
102
+ const updateViewport = useCallback<GraphHandle['setViewport']>(
103
+ (next) => {
104
+ const current = viewportRef.current;
105
+ const resolved = typeof next === 'function' ? next(current) : next;
106
+ const normalized = normalizeViewport({ ...current, ...resolved }, safeMinZoom, safeMaxZoom);
107
+
108
+ if (!controlledViewport) {
109
+ setInternalViewport(normalized);
110
+ }
111
+ onViewportChangeRef.current?.(normalized);
112
+ },
113
+ [controlledViewport, safeMaxZoom, safeMinZoom]
114
+ );
115
+
116
+ const updateSelection = useCallback(
117
+ (next: GraphSelection | ((current: GraphSelection) => GraphSelection)) => {
118
+ const current = selectionRef.current;
119
+ const resolved = typeof next === 'function' ? next(current) : next;
120
+ if (selectedNodeIds == null || selectedEdgeIds == null) {
121
+ setInternalSelection((previous) => ({
122
+ nodeIds: selectedNodeIds == null ? resolved.nodeIds : previous.nodeIds,
123
+ edgeIds: selectedEdgeIds == null ? resolved.edgeIds : previous.edgeIds,
124
+ }));
125
+ }
126
+ onSelectionChange?.(resolved);
127
+ },
128
+ [onSelectionChange, selectedEdgeIds, selectedNodeIds]
129
+ );
130
+
131
+ const updateFocusedNode = useCallback(
132
+ (nodeId: string | null) => {
133
+ if (controlledFocusedNodeId === undefined) {
134
+ setInternalFocusedNodeId(nodeId);
135
+ }
136
+ onFocusedNodeChange?.(nodeId);
137
+ },
138
+ [controlledFocusedNodeId, onFocusedNodeChange]
139
+ );
140
+
141
+ return {
142
+ viewport,
143
+ viewportRef,
144
+ selection,
145
+ selectionRef,
146
+ focusedNodeId,
147
+ updateViewport,
148
+ updateSelection,
149
+ updateFocusedNode,
150
+ };
151
+ };
@@ -16,7 +16,9 @@ export const groupPositionedNodesByColumn = <TNode extends PositionedNode = Posi
16
16
  nodes: TNode[],
17
17
  tolerance: number = DEFAULT_COLUMN_TOLERANCE
18
18
  ): NodeColumn<TNode>[] => {
19
- const sortedNodes = [...nodes].sort((left, right) => getNodeCenterX(left) - getNodeCenterX(right));
19
+ const sortedNodes = [...nodes].sort(
20
+ (left, right) => getNodeCenterX(left) - getNodeCenterX(right)
21
+ );
20
22
  const columns: Array<{ centerX: number; avgWidth: number; nodes: TNode[] }> = [];
21
23
 
22
24
  sortedNodes.forEach((node) => {
@@ -48,4 +50,4 @@ export const groupPositionedNodesByColumn = <TNode extends PositionedNode = Posi
48
50
  centerX: column.centerX,
49
51
  nodes: [...column.nodes].sort((left, right) => left.position.y - right.position.y),
50
52
  }));
51
- };
53
+ };
package/tsconfig.json CHANGED
@@ -8,5 +8,9 @@
8
8
  },
9
9
  "include": ["src"],
10
10
  "exclude": ["dist", "node_modules"],
11
- "references": [{ "path": "./tsconfig.node.json" }, { "path": "../types" }, { "path": "../core-graph-render" }]
11
+ "references": [
12
+ { "path": "./tsconfig.node.json" },
13
+ { "path": "../types" },
14
+ { "path": "../core-graph-render" }
15
+ ]
12
16
  }