@truedat/core 8.11.5 → 8.11.7
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/package.json +4 -3
- package/src/components/CardGroupsAccordion.js +0 -2
- package/src/components/Graph.js +1410 -158
- package/src/components/TemplateSelector.js +11 -1
- package/src/components/__tests__/ColoredEdge.spec.js +372 -0
- package/src/components/__tests__/ConceptNode.spec.js +181 -0
- package/src/components/__tests__/Graph.spec.js +183 -12
- package/src/components/__tests__/LineageEdgeRendering.spec.js +45 -0
- package/src/components/__tests__/LineageGroupNode.spec.js +308 -0
- package/src/components/__tests__/__snapshots__/ConceptNode.spec.js.snap +23 -0
- package/src/components/graph/ColoredEdge.js +715 -79
- package/src/components/graph/ConceptNode.js +21 -4
- package/src/components/graph/FlowPorts.js +1 -0
- package/src/components/graph/LineageGroupNode.js +320 -0
- package/src/components/graph/LineageQueryOrigin.js +27 -0
- package/src/components/graph/edgeLayout.js +7 -4
- package/src/components/graph/lineageNodeIcon.js +17 -0
- package/src/components/graph/lineageQueryOriginMessages.js +10 -0
- package/src/styles/ColoredEdge.less +3 -0
- package/src/styles/CommonGraphs.less +76 -0
- package/src/styles/ConceptGraph.less +6 -0
- package/src/styles/LineageGroupNode.less +563 -0
- package/src/styles/graph.less +20 -25
- package/src/styles/lineageNodeStates.less +111 -0
- /package/src/{components/graph → styles}/ConceptNode.less +0 -0
|
@@ -1,12 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
memo,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from "@xyflow/react";
|
|
3
10
|
import { getTargetSlotOffset } from "./edgeLayout";
|
|
4
11
|
|
|
12
|
+
export const EdgeHoverContext = createContext(null);
|
|
13
|
+
|
|
5
14
|
const MIN_CURVE_OFFSET = 14;
|
|
6
15
|
const MAX_CURVE_OFFSET = 38;
|
|
16
|
+
const ORTHOGONAL_EDGE_CLEARANCE = 24;
|
|
17
|
+
const ORTHOGONAL_EDGE_RADIUS = 8;
|
|
18
|
+
const ORTHOGONAL_LANE_GAP = 10;
|
|
19
|
+
const PORT_VERTICAL_INSET = 16;
|
|
7
20
|
const EDGE_ARROW_LENGTH = 10;
|
|
8
21
|
const EDGE_ARROW_WIDTH = 8;
|
|
9
22
|
const EDGE_START_DOT_RADIUS = 3.5;
|
|
23
|
+
const EDGE_FLOW_SPEED_PX_PER_SECOND = 40;
|
|
24
|
+
const clampNumber = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
25
|
+
|
|
26
|
+
export const edgeAnimationDuration = (
|
|
27
|
+
pathLength,
|
|
28
|
+
pixelsPerSecond = EDGE_FLOW_SPEED_PX_PER_SECOND,
|
|
29
|
+
) => {
|
|
30
|
+
const length = Number(pathLength);
|
|
31
|
+
const speed = Number(pixelsPerSecond);
|
|
32
|
+
|
|
33
|
+
if (!Number.isFinite(length) || length <= 0) return null;
|
|
34
|
+
if (!Number.isFinite(speed) || speed <= 0) return null;
|
|
35
|
+
|
|
36
|
+
return length / speed;
|
|
37
|
+
};
|
|
10
38
|
|
|
11
39
|
const getCurveOffset = ({ sourceX, sourceY, targetX, targetY }) => {
|
|
12
40
|
const horizontalDistance = Math.abs(targetX - sourceX);
|
|
@@ -52,120 +80,728 @@ const getBezierDerivative = (t, p0, p1, p2, p3) => {
|
|
|
52
80
|
);
|
|
53
81
|
};
|
|
54
82
|
|
|
83
|
+
const portVector = (position) => {
|
|
84
|
+
switch (position) {
|
|
85
|
+
case "left":
|
|
86
|
+
return { x: -1, y: 0 };
|
|
87
|
+
case "right":
|
|
88
|
+
return { x: 1, y: 0 };
|
|
89
|
+
case "top":
|
|
90
|
+
return { x: 0, y: -1 };
|
|
91
|
+
case "bottom":
|
|
92
|
+
return { x: 0, y: 1 };
|
|
93
|
+
default:
|
|
94
|
+
return { x: 1, y: 0 };
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
55
98
|
const normalizeVector = (x, y) => {
|
|
56
99
|
const length = Math.hypot(x, y) || 1;
|
|
57
100
|
|
|
58
101
|
return { x: x / length, y: y / length };
|
|
59
102
|
};
|
|
60
103
|
|
|
104
|
+
const point = (x, y) => ({ x, y });
|
|
105
|
+
|
|
106
|
+
const dedupePoints = (points) =>
|
|
107
|
+
points.filter(
|
|
108
|
+
(currentPoint, index) =>
|
|
109
|
+
index === 0 ||
|
|
110
|
+
currentPoint.x !== points[index - 1].x ||
|
|
111
|
+
currentPoint.y !== points[index - 1].y,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const distance = (pointA, pointB) =>
|
|
115
|
+
Math.hypot(pointB.x - pointA.x, pointB.y - pointA.y);
|
|
116
|
+
|
|
117
|
+
const pointToward = (from, to, amount) => {
|
|
118
|
+
const segmentLength = distance(from, to);
|
|
119
|
+
|
|
120
|
+
if (!segmentLength) return from;
|
|
121
|
+
|
|
122
|
+
const ratio = amount / segmentLength;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
x: from.x + (to.x - from.x) * ratio,
|
|
126
|
+
y: from.y + (to.y - from.y) * ratio,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const roundedOrthogonalPath = (points, radius) => {
|
|
131
|
+
if (points.length < 2) return "";
|
|
132
|
+
|
|
133
|
+
const start = `M ${points[0].x},${points[0].y}`;
|
|
134
|
+
const segments = points.slice(1, -1).map((corner, idx) => {
|
|
135
|
+
const previous = points[idx];
|
|
136
|
+
const next = points[idx + 2];
|
|
137
|
+
const cornerRadius = Math.min(
|
|
138
|
+
radius,
|
|
139
|
+
distance(previous, corner) / 2,
|
|
140
|
+
distance(corner, next) / 2,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (!cornerRadius) {
|
|
144
|
+
return ` L ${corner.x},${corner.y}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const cornerStart = pointToward(corner, previous, cornerRadius);
|
|
148
|
+
const cornerEnd = pointToward(corner, next, cornerRadius);
|
|
149
|
+
|
|
150
|
+
return ` L ${cornerStart.x},${cornerStart.y} Q ${corner.x},${corner.y} ${cornerEnd.x},${cornerEnd.y}`;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const lastPoint = points[points.length - 1];
|
|
154
|
+
return [start, ...segments, ` L ${lastPoint.x},${lastPoint.y}`].join("");
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const firstSegmentTangent = (points, fallback) => {
|
|
158
|
+
const firstValidIndex = points.findIndex((point, index) => {
|
|
159
|
+
if (index === 0) return false;
|
|
160
|
+
const previous = points[index - 1];
|
|
161
|
+
return previous.x !== point.x || previous.y !== point.y;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (firstValidIndex === -1) return fallback;
|
|
165
|
+
|
|
166
|
+
const previous = points[firstValidIndex - 1];
|
|
167
|
+
const current = points[firstValidIndex];
|
|
168
|
+
return normalizeVector(current.x - previous.x, current.y - previous.y);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const lastSegmentTangent = (points, fallback) => {
|
|
172
|
+
const lastValidIndex = points.reduce((acc, point, index) => {
|
|
173
|
+
if (index === 0) return acc;
|
|
174
|
+
const previous = points[index - 1];
|
|
175
|
+
if (previous.x !== point.x || previous.y !== point.y) return index;
|
|
176
|
+
return acc;
|
|
177
|
+
}, -1);
|
|
178
|
+
|
|
179
|
+
if (lastValidIndex === -1) return fallback;
|
|
180
|
+
|
|
181
|
+
const previous = points[lastValidIndex - 1];
|
|
182
|
+
const current = points[lastValidIndex];
|
|
183
|
+
return normalizeVector(current.x - previous.x, current.y - previous.y);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const getOrthogonalEdgePath = ({
|
|
187
|
+
sourceX,
|
|
188
|
+
sourceY,
|
|
189
|
+
targetX,
|
|
190
|
+
targetY,
|
|
191
|
+
sourcePosition = "right",
|
|
192
|
+
targetPosition = "left",
|
|
193
|
+
clearance = ORTHOGONAL_EDGE_CLEARANCE,
|
|
194
|
+
borderRadius = ORTHOGONAL_EDGE_RADIUS,
|
|
195
|
+
laneOffset = 0,
|
|
196
|
+
routeIndex,
|
|
197
|
+
routeCount,
|
|
198
|
+
sourceNodeY,
|
|
199
|
+
targetNodeY,
|
|
200
|
+
routeGuide,
|
|
201
|
+
}) => {
|
|
202
|
+
const sourceVector = portVector(sourcePosition);
|
|
203
|
+
const targetVector = portVector(targetPosition);
|
|
204
|
+
const sourcePoint = point(sourceX, sourceY);
|
|
205
|
+
const targetPoint = point(targetX, targetY);
|
|
206
|
+
const sourceStub = point(
|
|
207
|
+
sourceX + sourceVector.x * clearance,
|
|
208
|
+
sourceY + sourceVector.y * clearance,
|
|
209
|
+
);
|
|
210
|
+
const targetStub = point(
|
|
211
|
+
targetX + targetVector.x * clearance,
|
|
212
|
+
targetY + targetVector.y * clearance,
|
|
213
|
+
);
|
|
214
|
+
const forwardRoute =
|
|
215
|
+
(sourcePosition === "right" &&
|
|
216
|
+
targetPosition === "left" &&
|
|
217
|
+
targetX > sourceX + clearance * 2) ||
|
|
218
|
+
(sourcePosition === "left" &&
|
|
219
|
+
targetPosition === "right" &&
|
|
220
|
+
targetX < sourceX - clearance * 2);
|
|
221
|
+
const routeCenterX = (sourceStub.x + targetStub.x) / 2;
|
|
222
|
+
const maxLaneOffset = Math.max(
|
|
223
|
+
0,
|
|
224
|
+
Math.abs(targetStub.x - sourceStub.x) / 2 - borderRadius * 2,
|
|
225
|
+
);
|
|
226
|
+
const routeSpan =
|
|
227
|
+
Number.isFinite(routeIndex) && routeCount > 1
|
|
228
|
+
? Math.min((routeCount - 1) * ORTHOGONAL_LANE_GAP, maxLaneOffset * 2)
|
|
229
|
+
: 0;
|
|
230
|
+
const distributedLaneOffset = routeSpan
|
|
231
|
+
? -routeSpan / 2 + (routeIndex * routeSpan) / (routeCount - 1)
|
|
232
|
+
: laneOffset;
|
|
233
|
+
const routeX =
|
|
234
|
+
routeGuide?.laneX ??
|
|
235
|
+
routeCenterX +
|
|
236
|
+
clampNumber(distributedLaneOffset, -maxLaneOffset, maxLaneOffset);
|
|
237
|
+
const routeY =
|
|
238
|
+
routeGuide?.laneY ??
|
|
239
|
+
Math.min(sourceNodeY ?? sourceY, targetNodeY ?? targetY, sourceY, targetY) -
|
|
240
|
+
clearance -
|
|
241
|
+
Math.abs(distributedLaneOffset);
|
|
242
|
+
const guidedBypass = routeGuide?.kind === "bypass";
|
|
243
|
+
const sourceLaneX = routeGuide?.sourceLaneX ?? sourceStub.x;
|
|
244
|
+
const targetLaneX = routeGuide?.targetLaneX ?? targetStub.x;
|
|
245
|
+
const points = dedupePoints(
|
|
246
|
+
guidedBypass
|
|
247
|
+
? [
|
|
248
|
+
sourcePoint,
|
|
249
|
+
sourceStub,
|
|
250
|
+
point(sourceLaneX, sourceStub.y),
|
|
251
|
+
point(sourceLaneX, routeY),
|
|
252
|
+
point(targetLaneX, routeY),
|
|
253
|
+
point(targetLaneX, targetStub.y),
|
|
254
|
+
targetStub,
|
|
255
|
+
targetPoint,
|
|
256
|
+
]
|
|
257
|
+
: forwardRoute || routeGuide?.kind === "corridor"
|
|
258
|
+
? [
|
|
259
|
+
sourcePoint,
|
|
260
|
+
sourceStub,
|
|
261
|
+
point(routeX, sourceStub.y),
|
|
262
|
+
point(routeX, targetStub.y),
|
|
263
|
+
targetStub,
|
|
264
|
+
targetPoint,
|
|
265
|
+
]
|
|
266
|
+
: [
|
|
267
|
+
sourcePoint,
|
|
268
|
+
sourceStub,
|
|
269
|
+
point(sourceStub.x, routeY),
|
|
270
|
+
point(targetStub.x, routeY),
|
|
271
|
+
targetStub,
|
|
272
|
+
targetPoint,
|
|
273
|
+
],
|
|
274
|
+
);
|
|
275
|
+
const path = roundedOrthogonalPath(points, borderRadius);
|
|
276
|
+
const labelPoint = routeGuide?.labelPoint
|
|
277
|
+
? routeGuide.labelPoint
|
|
278
|
+
: forwardRoute || routeGuide?.kind === "corridor"
|
|
279
|
+
? point(routeX, (sourceStub.y + targetStub.y) / 2)
|
|
280
|
+
: point((sourceStub.x + targetStub.x) / 2, routeY);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
path,
|
|
284
|
+
labelX: labelPoint.x,
|
|
285
|
+
labelY: labelPoint.y,
|
|
286
|
+
sourceTangent: firstSegmentTangent(points, sourceVector),
|
|
287
|
+
targetTangent: lastSegmentTangent(
|
|
288
|
+
points,
|
|
289
|
+
normalizeVector(-targetVector.x, -targetVector.y),
|
|
290
|
+
),
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export const getClassicArrowPoints = ({ targetX, targetY, targetTangent }) => {
|
|
295
|
+
const tangent = normalizeVector(targetTangent.x, targetTangent.y);
|
|
296
|
+
const normal = { x: -tangent.y, y: tangent.x };
|
|
297
|
+
const lineTargetX = targetX - tangent.x * EDGE_ARROW_LENGTH;
|
|
298
|
+
const lineTargetY = targetY - tangent.y * EDGE_ARROW_LENGTH;
|
|
299
|
+
const arrowLeftX = lineTargetX + normal.x * (EDGE_ARROW_WIDTH / 2);
|
|
300
|
+
const arrowLeftY = lineTargetY + normal.y * (EDGE_ARROW_WIDTH / 2);
|
|
301
|
+
const arrowRightX = lineTargetX - normal.x * (EDGE_ARROW_WIDTH / 2);
|
|
302
|
+
const arrowRightY = lineTargetY - normal.y * (EDGE_ARROW_WIDTH / 2);
|
|
303
|
+
|
|
304
|
+
return `${targetX},${targetY} ${arrowLeftX},${arrowLeftY} ${arrowRightX},${arrowRightY}`;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const offsetFromPort = ({ x, y, position, distance }) => {
|
|
308
|
+
const vector = portVector(position);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
x: x + vector.x * distance,
|
|
312
|
+
y: y + vector.y * distance,
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const getFlexibleEdgePath = ({
|
|
317
|
+
sourceX,
|
|
318
|
+
sourceY,
|
|
319
|
+
targetX,
|
|
320
|
+
targetY,
|
|
321
|
+
sourcePosition = "right",
|
|
322
|
+
targetPosition = "left",
|
|
323
|
+
curvature = 0.32,
|
|
324
|
+
bundleSource = false,
|
|
325
|
+
bundleTarget = false,
|
|
326
|
+
bundleOffset = 40,
|
|
327
|
+
}) => {
|
|
328
|
+
const sourceJunction = bundleSource
|
|
329
|
+
? offsetFromPort({
|
|
330
|
+
x: sourceX,
|
|
331
|
+
y: sourceY,
|
|
332
|
+
position: sourcePosition,
|
|
333
|
+
distance: bundleOffset,
|
|
334
|
+
})
|
|
335
|
+
: point(sourceX, sourceY);
|
|
336
|
+
const targetJunction = bundleTarget
|
|
337
|
+
? offsetFromPort({
|
|
338
|
+
x: targetX,
|
|
339
|
+
y: targetY,
|
|
340
|
+
position: targetPosition,
|
|
341
|
+
distance: bundleOffset,
|
|
342
|
+
})
|
|
343
|
+
: point(targetX, targetY);
|
|
344
|
+
const [branchPath, labelX, labelY] = getBezierPath({
|
|
345
|
+
sourceX: sourceJunction.x,
|
|
346
|
+
sourceY: sourceJunction.y,
|
|
347
|
+
sourcePosition,
|
|
348
|
+
targetX: targetJunction.x,
|
|
349
|
+
targetY: targetJunction.y,
|
|
350
|
+
targetPosition,
|
|
351
|
+
curvature,
|
|
352
|
+
});
|
|
353
|
+
const pathParts = [];
|
|
354
|
+
|
|
355
|
+
if (bundleSource) {
|
|
356
|
+
pathParts.push(
|
|
357
|
+
`M ${sourceX},${sourceY} L ${sourceJunction.x},${sourceJunction.y}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
pathParts.push(branchPath);
|
|
362
|
+
|
|
363
|
+
if (bundleTarget) {
|
|
364
|
+
pathParts.push(
|
|
365
|
+
`M ${targetJunction.x},${targetJunction.y} L ${targetX},${targetY}`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const targetVector = portVector(targetPosition);
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
path: pathParts.join(" "),
|
|
373
|
+
labelX,
|
|
374
|
+
labelY,
|
|
375
|
+
targetTangent: normalizeVector(-targetVector.x, -targetVector.y),
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
export const getEdgeSlotOffsets = (data = {}) => ({
|
|
380
|
+
sourceSlotOffset: data.lockSourcePort
|
|
381
|
+
? 0
|
|
382
|
+
: data.sourceFieldIndex === undefined
|
|
383
|
+
? getTargetSlotOffset(data.sourceSlotIndex, data.sourceSlotCount)
|
|
384
|
+
: 0,
|
|
385
|
+
targetSlotOffset:
|
|
386
|
+
data.targetFieldIndex === undefined
|
|
387
|
+
? getTargetSlotOffset(
|
|
388
|
+
data.targetSlotIndex,
|
|
389
|
+
data.targetSlotCount,
|
|
390
|
+
Number.isFinite(data.targetNodeHeight)
|
|
391
|
+
? Math.max(0, data.targetNodeHeight - PORT_VERTICAL_INSET)
|
|
392
|
+
: Number.POSITIVE_INFINITY,
|
|
393
|
+
)
|
|
394
|
+
: 0,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
export const getLineageEdgeVisualState = (data = {}, hovered = false) => {
|
|
398
|
+
const aggregatedCount = Number(data.aggregatedCount);
|
|
399
|
+
const aggregated = Number.isFinite(aggregatedCount) && aggregatedCount > 1;
|
|
400
|
+
const dimmed = Boolean(data.focusDimmed);
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
aggregated,
|
|
404
|
+
highlighted:
|
|
405
|
+
!data.containment && !dimmed && (hovered || Boolean(data.focusFocused)),
|
|
406
|
+
showDecorations: !dimmed,
|
|
407
|
+
showHitArea: true,
|
|
408
|
+
showLabel:
|
|
409
|
+
!dimmed &&
|
|
410
|
+
(aggregated
|
|
411
|
+
? Boolean(data.selectedEndpoint)
|
|
412
|
+
: hovered && Boolean(data.label)),
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
|
|
61
416
|
const ColoredEdge = ({
|
|
62
417
|
id,
|
|
418
|
+
source,
|
|
419
|
+
target,
|
|
63
420
|
sourceX,
|
|
64
421
|
sourceY,
|
|
65
422
|
targetX,
|
|
66
423
|
targetY,
|
|
67
424
|
sourcePosition,
|
|
68
425
|
targetPosition,
|
|
426
|
+
markerEnd,
|
|
69
427
|
style = {},
|
|
70
428
|
data = {},
|
|
71
429
|
}) => {
|
|
430
|
+
const hoverCtx = useContext(EdgeHoverContext);
|
|
431
|
+
const hoveredNodeId = hoverCtx?.hoveredNodeId;
|
|
432
|
+
const hoveredEdgeEndpoints = hoverCtx?.hoveredEdgeEndpoints;
|
|
72
433
|
const [hovered, setHovered] = useState(false);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
434
|
+
const [motionTiming, setMotionTiming] = useState(null);
|
|
435
|
+
const gRef = useRef(null);
|
|
436
|
+
const hasMetadata =
|
|
437
|
+
data.metadata && typeof data.metadata === "object"
|
|
438
|
+
? Object.keys(data.metadata).length > 0
|
|
439
|
+
: false;
|
|
440
|
+
const interactive = Boolean(data.onClick);
|
|
441
|
+
const { sourceSlotOffset, targetSlotOffset } = getEdgeSlotOffsets(data);
|
|
442
|
+
const useOrthogonalPath = data.edgePath === "orthogonal";
|
|
443
|
+
const useFlexibleBezierPath = data.edgePath === "bezier";
|
|
444
|
+
const useClassicEdgePath = !useOrthogonalPath && !useFlexibleBezierPath;
|
|
445
|
+
const adjustedSourceY = sourceY + (data.bundleSource ? 0 : sourceSlotOffset);
|
|
446
|
+
const adjustedTargetY = targetY + (data.bundleTarget ? 0 : targetSlotOffset);
|
|
447
|
+
const { aggregated, highlighted, showDecorations, showHitArea, showLabel } =
|
|
448
|
+
getLineageEdgeVisualState(data, hovered);
|
|
449
|
+
const dependenciesOnLeft =
|
|
450
|
+
data.selectedEndpoint === "source" ? targetX < sourceX : sourceX < targetX;
|
|
451
|
+
const setHoveredEdgeEndpoints = hoverCtx?.setHoveredEdgeEndpoints;
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
if (!showHitArea) setHovered(false);
|
|
454
|
+
return () => {
|
|
455
|
+
setHoveredEdgeEndpoints?.((current) =>
|
|
456
|
+
current?.edgeId === id ? null : current,
|
|
457
|
+
);
|
|
458
|
+
};
|
|
459
|
+
}, [id, showHitArea, setHoveredEdgeEndpoints]);
|
|
460
|
+
const forceHorizontalPorts = Boolean(
|
|
461
|
+
useOrthogonalPath && data.forceHorizontalPorts,
|
|
76
462
|
);
|
|
77
|
-
const adjustedTargetY = targetY + targetSlotOffset;
|
|
78
|
-
const curveOffset = getCurveOffset({
|
|
79
|
-
sourceX,
|
|
80
|
-
sourceY,
|
|
81
|
-
targetX,
|
|
82
|
-
targetY: adjustedTargetY,
|
|
83
|
-
});
|
|
84
463
|
const stroke = style.stroke || "var(--td-graph-edge-stroke, #b0b8c8)";
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
464
|
+
const markerPosition = data.markerPosition;
|
|
465
|
+
const showTargetMarker =
|
|
466
|
+
showDecorations &&
|
|
467
|
+
(useOrthogonalPath
|
|
468
|
+
? markerPosition === "target" ||
|
|
469
|
+
markerPosition === "both" ||
|
|
470
|
+
(markerPosition === undefined && data.showTargetArrow !== false)
|
|
471
|
+
: data.showTargetArrow !== false);
|
|
472
|
+
const showSourceMarker =
|
|
473
|
+
showDecorations &&
|
|
474
|
+
(markerPosition === "source" || markerPosition === "both") &&
|
|
475
|
+
data.showTargetArrow !== false;
|
|
476
|
+
const effectiveSourcePosition = forceHorizontalPorts
|
|
477
|
+
? data.sourcePortPosition || "right"
|
|
478
|
+
: sourcePosition || "right";
|
|
479
|
+
const effectiveTargetPosition = forceHorizontalPorts
|
|
480
|
+
? data.targetPortPosition || "left"
|
|
481
|
+
: targetPosition || "left";
|
|
482
|
+
const sourcePortX = sourceX;
|
|
483
|
+
const sourcePortY = adjustedSourceY;
|
|
484
|
+
const targetPortX = targetX;
|
|
485
|
+
const targetPortY = adjustedTargetY;
|
|
486
|
+
const { edgePath, labelX, labelY, targetTangent, sourceTangent } = (() => {
|
|
487
|
+
if (useOrthogonalPath) {
|
|
488
|
+
const orthogonalEdge = getOrthogonalEdgePath({
|
|
489
|
+
sourceX: sourcePortX,
|
|
490
|
+
sourceY: sourcePortY,
|
|
491
|
+
targetX: targetPortX,
|
|
492
|
+
targetY: targetPortY,
|
|
493
|
+
sourcePosition: effectiveSourcePosition,
|
|
494
|
+
targetPosition: effectiveTargetPosition,
|
|
495
|
+
borderRadius: data.edgeBorderRadius ?? ORTHOGONAL_EDGE_RADIUS,
|
|
496
|
+
clearance: data.edgeOffset ?? ORTHOGONAL_EDGE_CLEARANCE,
|
|
497
|
+
routeIndex: data.routeIndex,
|
|
498
|
+
routeCount: data.routeCount,
|
|
499
|
+
sourceNodeY: data.sourceNodeY,
|
|
500
|
+
targetNodeY: data.targetNodeY,
|
|
501
|
+
routeGuide: data.routeGuide,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const sourceVector = portVector(effectiveSourcePosition);
|
|
505
|
+
return {
|
|
506
|
+
edgePath: orthogonalEdge.path,
|
|
507
|
+
labelX: orthogonalEdge.labelX,
|
|
508
|
+
labelY: orthogonalEdge.labelY,
|
|
509
|
+
targetTangent: orthogonalEdge.targetTangent,
|
|
510
|
+
sourceTangent: normalizeVector(sourceVector.x, sourceVector.y),
|
|
511
|
+
};
|
|
512
|
+
} else if (useFlexibleBezierPath) {
|
|
513
|
+
const flexibleEdge = getFlexibleEdgePath({
|
|
514
|
+
sourceX: sourcePortX,
|
|
515
|
+
sourceY: sourcePortY,
|
|
516
|
+
sourcePosition: effectiveSourcePosition,
|
|
517
|
+
targetX: targetPortX,
|
|
518
|
+
targetY: targetPortY,
|
|
519
|
+
targetPosition: effectiveTargetPosition,
|
|
520
|
+
curvature: data.edgeCurvature ?? 0.32,
|
|
521
|
+
bundleSource: data.bundleSource,
|
|
522
|
+
bundleTarget: data.bundleTarget,
|
|
523
|
+
bundleOffset: data.bundleOffset,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const bezierSourceVector = portVector(effectiveSourcePosition);
|
|
527
|
+
return {
|
|
528
|
+
edgePath: flexibleEdge.path,
|
|
529
|
+
labelX: flexibleEdge.labelX,
|
|
530
|
+
labelY: flexibleEdge.labelY,
|
|
531
|
+
targetTangent: flexibleEdge.targetTangent,
|
|
532
|
+
sourceTangent: normalizeVector(
|
|
533
|
+
bezierSourceVector.x,
|
|
534
|
+
bezierSourceVector.y,
|
|
535
|
+
),
|
|
536
|
+
};
|
|
537
|
+
} else {
|
|
538
|
+
const curveOffset = getCurveOffset({
|
|
539
|
+
sourceX,
|
|
540
|
+
sourceY: adjustedSourceY,
|
|
541
|
+
targetX,
|
|
542
|
+
targetY: adjustedTargetY,
|
|
543
|
+
});
|
|
544
|
+
const sourceControl = getControlPoint(
|
|
545
|
+
sourceX,
|
|
546
|
+
adjustedSourceY,
|
|
547
|
+
sourcePosition,
|
|
548
|
+
curveOffset,
|
|
549
|
+
);
|
|
550
|
+
const targetControl = getControlPoint(
|
|
551
|
+
targetX,
|
|
552
|
+
adjustedTargetY,
|
|
553
|
+
targetPosition,
|
|
554
|
+
curveOffset,
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
const targetTangent = normalizeVector(
|
|
558
|
+
getBezierDerivative(
|
|
559
|
+
1,
|
|
560
|
+
sourceX,
|
|
561
|
+
sourceControl.x,
|
|
562
|
+
targetControl.x,
|
|
563
|
+
targetX,
|
|
564
|
+
),
|
|
565
|
+
getBezierDerivative(
|
|
566
|
+
1,
|
|
567
|
+
adjustedSourceY,
|
|
568
|
+
sourceControl.y,
|
|
569
|
+
targetControl.y,
|
|
570
|
+
adjustedTargetY,
|
|
571
|
+
),
|
|
572
|
+
);
|
|
573
|
+
const sourceTangent = normalizeVector(
|
|
574
|
+
getBezierDerivative(
|
|
575
|
+
0,
|
|
576
|
+
sourceX,
|
|
577
|
+
sourceControl.x,
|
|
578
|
+
targetControl.x,
|
|
579
|
+
targetX,
|
|
580
|
+
),
|
|
581
|
+
getBezierDerivative(
|
|
582
|
+
0,
|
|
583
|
+
adjustedSourceY,
|
|
584
|
+
sourceControl.y,
|
|
585
|
+
targetControl.y,
|
|
586
|
+
adjustedTargetY,
|
|
587
|
+
),
|
|
588
|
+
);
|
|
589
|
+
const lineTargetX = targetX - targetTangent.x * EDGE_ARROW_LENGTH;
|
|
590
|
+
const lineTargetY = adjustedTargetY - targetTangent.y * EDGE_ARROW_LENGTH;
|
|
591
|
+
const edgePath = `M ${sourceX},${adjustedSourceY} C ${sourceControl.x},${sourceControl.y} ${targetControl.x},${targetControl.y} ${lineTargetX},${lineTargetY}`;
|
|
592
|
+
const labelX = getBezierPoint(
|
|
593
|
+
0.5,
|
|
594
|
+
sourceX,
|
|
595
|
+
sourceControl.x,
|
|
596
|
+
targetControl.x,
|
|
597
|
+
lineTargetX,
|
|
598
|
+
);
|
|
599
|
+
const labelY = getBezierPoint(
|
|
600
|
+
0.5,
|
|
601
|
+
adjustedSourceY,
|
|
602
|
+
sourceControl.y,
|
|
603
|
+
targetControl.y,
|
|
604
|
+
lineTargetY,
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
return { edgePath, labelX, labelY, targetTangent, sourceTangent };
|
|
608
|
+
}
|
|
609
|
+
})();
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
if (!data.animated || !gRef.current) return;
|
|
612
|
+
|
|
613
|
+
const pathElement = gRef.current.querySelector("path.td-graph-edge");
|
|
614
|
+
|
|
615
|
+
if (!pathElement?.getTotalLength) return;
|
|
616
|
+
|
|
617
|
+
const duration = edgeAnimationDuration(pathElement.getTotalLength());
|
|
618
|
+
|
|
619
|
+
if (!duration) return;
|
|
620
|
+
|
|
621
|
+
setMotionTiming((current) =>
|
|
622
|
+
current?.path === edgePath && current.duration === duration
|
|
623
|
+
? current
|
|
624
|
+
: { duration, path: edgePath },
|
|
625
|
+
);
|
|
626
|
+
}, [data.animated, edgePath]);
|
|
627
|
+
const animationDuration =
|
|
628
|
+
motionTiming?.path === edgePath ? motionTiming.duration : null;
|
|
629
|
+
const nodeHovered = hoveredNodeId
|
|
630
|
+
? (hoveredNodeId === source || hoveredNodeId === target) &&
|
|
631
|
+
!data.containment
|
|
632
|
+
: false;
|
|
633
|
+
const sourceBundleHovered = Boolean(
|
|
634
|
+
data.bundleSource &&
|
|
635
|
+
!data.containment &&
|
|
636
|
+
!data.focusDimmed &&
|
|
637
|
+
hoveredEdgeEndpoints?.source === source &&
|
|
638
|
+
hoveredEdgeEndpoints.sourceFieldIndex === data.sourceFieldIndex,
|
|
123
639
|
);
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
640
|
+
const effectiveStroke = data.connectionSelected
|
|
641
|
+
? "var(--td-graph-edge-metadata, #7c3aed)"
|
|
642
|
+
: highlighted || nodeHovered || sourceBundleHovered
|
|
643
|
+
? "var(--td-graph-edge-active, var(--td-graph-accent, #ed5c17))"
|
|
644
|
+
: stroke;
|
|
645
|
+
const handleClick = (event) => {
|
|
646
|
+
if (!interactive) return;
|
|
647
|
+
event.stopPropagation();
|
|
648
|
+
data.onClick(data.metadata);
|
|
649
|
+
};
|
|
650
|
+
const edgeClassName = [
|
|
651
|
+
"td-graph-edge",
|
|
652
|
+
data.connectionSelected ? "td-graph-edge--connection-selected" : "",
|
|
653
|
+
data.animated ? "td-graph-edge--animated" : "",
|
|
654
|
+
data.flowDirection ? `td-graph-edge--flow-${data.flowDirection}` : "",
|
|
655
|
+
data.containment ? "td-graph-edge--containment" : "",
|
|
656
|
+
hovered ? "td-graph-edge--hovered" : "",
|
|
657
|
+
data.restrictive ? "td-graph-edge--restrictive" : "",
|
|
658
|
+
data.focusFocused ? "td-graph-edge--focus-focused" : "",
|
|
659
|
+
data.focusDimmed ? "td-graph-edge--focus-dimmed" : "",
|
|
660
|
+
nodeHovered ? "td-graph-edge--node-hovered" : "",
|
|
661
|
+
sourceBundleHovered ? "td-graph-edge--source-bundle-hovered" : "",
|
|
662
|
+
]
|
|
663
|
+
.filter(Boolean)
|
|
664
|
+
.join(" ");
|
|
130
665
|
|
|
131
666
|
return (
|
|
132
|
-
|
|
133
|
-
<BaseEdge
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
667
|
+
<g ref={gRef}>
|
|
668
|
+
<BaseEdge
|
|
669
|
+
className={edgeClassName}
|
|
670
|
+
id={id}
|
|
671
|
+
path={edgePath}
|
|
672
|
+
style={{
|
|
673
|
+
...style,
|
|
674
|
+
stroke: effectiveStroke,
|
|
675
|
+
}}
|
|
140
676
|
/>
|
|
141
|
-
{data.
|
|
677
|
+
{data.animated && animationDuration ? (
|
|
678
|
+
<circle
|
|
679
|
+
aria-hidden="true"
|
|
680
|
+
className="td-graph-edge-flow-dot"
|
|
681
|
+
r={4}
|
|
682
|
+
fill={effectiveStroke}
|
|
683
|
+
opacity={1}
|
|
684
|
+
pointerEvents="none"
|
|
685
|
+
>
|
|
686
|
+
<animateMotion
|
|
687
|
+
calcMode="paced"
|
|
688
|
+
dur={`${animationDuration}s`}
|
|
689
|
+
repeatCount="indefinite"
|
|
690
|
+
begin="0s"
|
|
691
|
+
fill="freeze"
|
|
692
|
+
path={edgePath}
|
|
693
|
+
/>
|
|
694
|
+
</circle>
|
|
695
|
+
) : null}
|
|
696
|
+
{data.showSourceDot !== false ? (
|
|
697
|
+
<circle
|
|
698
|
+
cx={sourceX}
|
|
699
|
+
cy={adjustedSourceY}
|
|
700
|
+
r={EDGE_START_DOT_RADIUS}
|
|
701
|
+
fill={effectiveStroke}
|
|
702
|
+
pointerEvents="none"
|
|
703
|
+
/>
|
|
704
|
+
) : null}
|
|
705
|
+
{!useClassicEdgePath && showTargetMarker && markerEnd && targetTangent ? (
|
|
142
706
|
<polygon
|
|
143
|
-
|
|
144
|
-
|
|
707
|
+
className="td-graph-edge-arrow"
|
|
708
|
+
points={getClassicArrowPoints({
|
|
709
|
+
targetX: targetPortX,
|
|
710
|
+
targetY: targetPortY,
|
|
711
|
+
targetTangent,
|
|
712
|
+
})}
|
|
713
|
+
fill={effectiveStroke}
|
|
145
714
|
pointerEvents="none"
|
|
146
715
|
/>
|
|
147
716
|
) : null}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
717
|
+
{!useClassicEdgePath && showSourceMarker && markerEnd && sourceTangent ? (
|
|
718
|
+
<polygon
|
|
719
|
+
className="td-graph-edge-arrow"
|
|
720
|
+
points={getClassicArrowPoints({
|
|
721
|
+
targetX: sourcePortX,
|
|
722
|
+
targetY: sourcePortY,
|
|
723
|
+
targetTangent: { x: -sourceTangent.x, y: -sourceTangent.y },
|
|
724
|
+
})}
|
|
725
|
+
fill={effectiveStroke}
|
|
726
|
+
pointerEvents="none"
|
|
727
|
+
/>
|
|
728
|
+
) : null}
|
|
729
|
+
{useClassicEdgePath &&
|
|
730
|
+
showDecorations &&
|
|
731
|
+
data.showTargetArrow !== false &&
|
|
732
|
+
targetTangent ? (
|
|
733
|
+
<polygon
|
|
734
|
+
points={getClassicArrowPoints({
|
|
735
|
+
targetX: targetPortX,
|
|
736
|
+
targetY: targetPortY,
|
|
737
|
+
targetTangent,
|
|
738
|
+
})}
|
|
739
|
+
fill={effectiveStroke}
|
|
740
|
+
pointerEvents="none"
|
|
741
|
+
/>
|
|
742
|
+
) : null}
|
|
743
|
+
{showHitArea ? (
|
|
744
|
+
<path
|
|
745
|
+
className="td-graph-edge-hit-area"
|
|
746
|
+
d={edgePath}
|
|
747
|
+
fill="none"
|
|
748
|
+
stroke="transparent"
|
|
749
|
+
strokeWidth={16}
|
|
750
|
+
onMouseEnter={() => {
|
|
751
|
+
setHovered(true);
|
|
752
|
+
hoverCtx?.setHoveredEdgeEndpoints({
|
|
753
|
+
edgeId: id,
|
|
754
|
+
source,
|
|
755
|
+
sourceFieldIndex: data.sourceFieldIndex,
|
|
756
|
+
target,
|
|
757
|
+
targetFieldIndex: data.targetFieldIndex,
|
|
758
|
+
});
|
|
759
|
+
}}
|
|
760
|
+
onMouseLeave={() => {
|
|
761
|
+
setHovered(false);
|
|
762
|
+
setHoveredEdgeEndpoints?.((current) =>
|
|
763
|
+
current?.edgeId === id ? null : current,
|
|
764
|
+
);
|
|
765
|
+
}}
|
|
766
|
+
onClick={handleClick}
|
|
767
|
+
cursor={interactive ? "pointer" : "default"}
|
|
768
|
+
/>
|
|
769
|
+
) : null}
|
|
770
|
+
{showLabel && (
|
|
771
|
+
<EdgeLabelRenderer>
|
|
772
|
+
<div
|
|
773
|
+
className={`td-graph-edge-label nodrag nopan${aggregated ? " td-graph-edge-label--aggregated" : ""}`}
|
|
774
|
+
style={{
|
|
775
|
+
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
|
776
|
+
}}
|
|
777
|
+
>
|
|
778
|
+
{aggregated ? (
|
|
779
|
+
<>
|
|
780
|
+
{dependenciesOnLeft ? <span aria-hidden="true">→</span> : null}
|
|
781
|
+
<span>{data.aggregatedCount}</span>
|
|
782
|
+
{!dependenciesOnLeft ? <span aria-hidden="true">→</span> : null}
|
|
783
|
+
</>
|
|
784
|
+
) : (
|
|
785
|
+
data.label
|
|
786
|
+
)}
|
|
787
|
+
</div>
|
|
788
|
+
</EdgeLabelRenderer>
|
|
789
|
+
)}
|
|
790
|
+
{showDecorations && hasMetadata && (
|
|
157
791
|
<EdgeLabelRenderer>
|
|
158
792
|
<div
|
|
159
|
-
className="td-graph-edge-
|
|
793
|
+
className="td-graph-edge-metadata-badge nodrag nopan"
|
|
160
794
|
style={{
|
|
161
795
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
|
162
796
|
}}
|
|
797
|
+
title={interactive ? "Click for details" : undefined}
|
|
798
|
+
onClick={handleClick}
|
|
163
799
|
>
|
|
164
|
-
|
|
800
|
+
<span className="td-graph-edge-metadata-badge__dot" />
|
|
165
801
|
</div>
|
|
166
802
|
</EdgeLabelRenderer>
|
|
167
803
|
)}
|
|
168
|
-
|
|
804
|
+
</g>
|
|
169
805
|
);
|
|
170
806
|
};
|
|
171
807
|
|