@sentropic/design-system-svelte 0.10.3 → 0.10.4
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/dist/AreaChart.svelte +38 -26
- package/dist/AreaChart.svelte.d.ts.map +1 -1
- package/dist/BackToTop.svelte +157 -0
- package/dist/BackToTop.svelte.d.ts +14 -0
- package/dist/BackToTop.svelte.d.ts.map +1 -0
- package/dist/BarChart.svelte +38 -39
- package/dist/BarChart.svelte.d.ts.map +1 -1
- package/dist/Card.svelte +2 -0
- package/dist/ChartDataList.svelte +33 -0
- package/dist/ChartDataList.svelte.d.ts +9 -0
- package/dist/ChartDataList.svelte.d.ts.map +1 -0
- package/dist/ChatComposer.svelte +20 -3
- package/dist/ChatComposer.svelte.d.ts +6 -30
- package/dist/ChatComposer.svelte.d.ts.map +1 -1
- package/dist/Checkbox.svelte +4 -0
- package/dist/Combobox.svelte +1 -1
- package/dist/ContentSwitcher.svelte +1 -0
- package/dist/DataTable.svelte +4 -1
- package/dist/DatePicker.svelte +1 -1
- package/dist/DisplaySettings.svelte +210 -0
- package/dist/DisplaySettings.svelte.d.ts +24 -0
- package/dist/DisplaySettings.svelte.d.ts.map +1 -0
- package/dist/DonutChart.svelte +44 -29
- package/dist/DonutChart.svelte.d.ts.map +1 -1
- package/dist/Dropdown.svelte +1 -1
- package/dist/FileUploader.svelte +2 -2
- package/dist/ForceGraph.svelte +428 -26
- package/dist/ForceGraph.svelte.d.ts +27 -0
- package/dist/ForceGraph.svelte.d.ts.map +1 -1
- package/dist/GraphLegend.svelte +142 -0
- package/dist/GraphLegend.svelte.d.ts +12 -0
- package/dist/GraphLegend.svelte.d.ts.map +1 -0
- package/dist/Header.svelte +2 -1
- package/dist/IconButton.svelte +1 -1
- package/dist/InlineLoading.svelte +10 -1
- package/dist/InlineLoading.svelte.d.ts.map +1 -1
- package/dist/Input.svelte +3 -2
- package/dist/LanguageSelector.svelte +2 -1
- package/dist/LineChart.svelte +38 -26
- package/dist/LineChart.svelte.d.ts.map +1 -1
- package/dist/Link.svelte +7 -1
- package/dist/MediaContent.svelte +124 -0
- package/dist/MediaContent.svelte.d.ts +22 -0
- package/dist/MediaContent.svelte.d.ts.map +1 -0
- package/dist/Menu.svelte +56 -3
- package/dist/Menu.svelte.d.ts.map +1 -1
- package/dist/MessageStatusBadge.svelte +1 -1
- package/dist/MultiSelect.svelte +2 -2
- package/dist/Notification.svelte +150 -0
- package/dist/Notification.svelte.d.ts +17 -0
- package/dist/Notification.svelte.d.ts.map +1 -0
- package/dist/NumberInput.svelte +1 -0
- package/dist/OverflowMenu.svelte +84 -13
- package/dist/OverflowMenu.svelte.d.ts.map +1 -1
- package/dist/Pagination.svelte +7 -0
- package/dist/PaginationNav.svelte +2 -2
- package/dist/ProgressIndicator.svelte +13 -1
- package/dist/ProgressIndicator.svelte.d.ts +1 -0
- package/dist/ProgressIndicator.svelte.d.ts.map +1 -1
- package/dist/Radio.svelte +7 -3
- package/dist/ScatterPlot.svelte +64 -45
- package/dist/ScatterPlot.svelte.d.ts.map +1 -1
- package/dist/Search.svelte +6 -3
- package/dist/Select.svelte +8 -2
- package/dist/SideNav.svelte +6 -0
- package/dist/StackedBarChart.svelte +51 -30
- package/dist/StackedBarChart.svelte.d.ts.map +1 -1
- package/dist/StreamingMessage.svelte +2 -2
- package/dist/Switch.svelte +4 -0
- package/dist/Table.svelte +4 -1
- package/dist/TableOfContents.svelte +109 -0
- package/dist/TableOfContents.svelte.d.ts +16 -0
- package/dist/TableOfContents.svelte.d.ts.map +1 -0
- package/dist/Tag.svelte +1 -1
- package/dist/Textarea.svelte +3 -2
- package/dist/Tile.svelte +4 -0
- package/dist/TileGroup.svelte +4 -0
- package/dist/Toggle.svelte +4 -0
- package/dist/Toggletip.svelte +1 -1
- package/dist/Transcription.svelte +135 -0
- package/dist/Transcription.svelte.d.ts +19 -0
- package/dist/Transcription.svelte.d.ts.map +1 -0
- package/dist/TreeView.svelte +2 -2
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/package.json +1 -1
package/dist/ForceGraph.svelte
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
| "category1" | "category2" | "category3" | "category4"
|
|
4
4
|
| "category5" | "category6" | "category7" | "category8";
|
|
5
5
|
|
|
6
|
+
export type ForceGraphNodeShape =
|
|
7
|
+
| "dot" | "circle"
|
|
8
|
+
| "diamond"
|
|
9
|
+
| "star"
|
|
10
|
+
| "hexagon"
|
|
11
|
+
| "box" | "square"
|
|
12
|
+
| "triangle";
|
|
13
|
+
|
|
6
14
|
export type ForceGraphNode = {
|
|
7
15
|
/** Stable identifier; referenced by edges. */
|
|
8
16
|
id: string;
|
|
@@ -20,6 +28,11 @@
|
|
|
20
28
|
/** Pin the node to a fixed position (ignored by the simulation). */
|
|
21
29
|
fx?: number;
|
|
22
30
|
fy?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Visual shape for the node. Defaults to 'dot' (circle).
|
|
33
|
+
* Supported: 'dot'|'circle', 'diamond', 'star', 'hexagon', 'box'|'square', 'triangle'.
|
|
34
|
+
*/
|
|
35
|
+
shape?: ForceGraphNodeShape;
|
|
23
36
|
};
|
|
24
37
|
|
|
25
38
|
export type ForceGraphEdge = {
|
|
@@ -35,6 +48,57 @@
|
|
|
35
48
|
*/
|
|
36
49
|
weak?: boolean;
|
|
37
50
|
};
|
|
51
|
+
|
|
52
|
+
export type ForceGraphLegendEntry = {
|
|
53
|
+
/** Label shown in the legend. */
|
|
54
|
+
label: string;
|
|
55
|
+
/** Shape for this entry (node legend). Absent = line-style legend entry. */
|
|
56
|
+
shape?: ForceGraphNodeShape;
|
|
57
|
+
/** Tone for this entry. Defaults to category1. */
|
|
58
|
+
tone?: ForceGraphTone;
|
|
59
|
+
/** When true, renders as a dashed line (edge legend). */
|
|
60
|
+
weak?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// SVG path helpers for the various node shapes.
|
|
65
|
+
// All shapes are centered at (0,0) and sized to inscribe within radius r.
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
export function nodeShapePath(shape: ForceGraphNodeShape | undefined, r: number): string | null {
|
|
68
|
+
const s = shape ?? "dot";
|
|
69
|
+
if (s === "dot" || s === "circle") return null; // use <circle>
|
|
70
|
+
if (s === "diamond") {
|
|
71
|
+
return `M 0 ${-r} L ${r} 0 L 0 ${r} L ${-r} 0 Z`;
|
|
72
|
+
}
|
|
73
|
+
if (s === "star") {
|
|
74
|
+
const outer = r;
|
|
75
|
+
const inner = r * 0.42;
|
|
76
|
+
const pts: string[] = [];
|
|
77
|
+
for (let i = 0; i < 10; i++) {
|
|
78
|
+
const angle = (i * Math.PI) / 5 - Math.PI / 2;
|
|
79
|
+
const rad = i % 2 === 0 ? outer : inner;
|
|
80
|
+
pts.push(`${rad * Math.cos(angle)},${rad * Math.sin(angle)}`);
|
|
81
|
+
}
|
|
82
|
+
return `M ${pts.join(" L ")} Z`;
|
|
83
|
+
}
|
|
84
|
+
if (s === "hexagon") {
|
|
85
|
+
const pts: string[] = [];
|
|
86
|
+
for (let i = 0; i < 6; i++) {
|
|
87
|
+
const angle = (i * Math.PI) / 3 - Math.PI / 6;
|
|
88
|
+
pts.push(`${r * Math.cos(angle)},${r * Math.sin(angle)}`);
|
|
89
|
+
}
|
|
90
|
+
return `M ${pts.join(" L ")} Z`;
|
|
91
|
+
}
|
|
92
|
+
if (s === "box" || s === "square") {
|
|
93
|
+
const h = r * 0.85;
|
|
94
|
+
return `M ${-h} ${-h} L ${h} ${-h} L ${h} ${h} L ${-h} ${h} Z`;
|
|
95
|
+
}
|
|
96
|
+
if (s === "triangle") {
|
|
97
|
+
const h = r * 1.1;
|
|
98
|
+
return `M 0 ${-h} L ${h * 0.9} ${h * 0.6} L ${-h * 0.9} ${h * 0.6} Z`;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
38
102
|
</script>
|
|
39
103
|
|
|
40
104
|
<script lang="ts">
|
|
@@ -75,6 +139,16 @@
|
|
|
75
139
|
* Fires with the node's stable id.
|
|
76
140
|
*/
|
|
77
141
|
onOpenEntity?: (id: string) => void;
|
|
142
|
+
/**
|
|
143
|
+
* Called when the user hovers an edge.
|
|
144
|
+
* Fires with the edge object (source/target/relation/weak).
|
|
145
|
+
*/
|
|
146
|
+
onEdgeHover?: (edge: ForceGraphEdge) => void;
|
|
147
|
+
/**
|
|
148
|
+
* Legend entries rendered as a corner overlay.
|
|
149
|
+
* Each entry has a label + optional shape (node) or weak (edge).
|
|
150
|
+
*/
|
|
151
|
+
legend?: ForceGraphLegendEntry[];
|
|
78
152
|
class?: string;
|
|
79
153
|
};
|
|
80
154
|
|
|
@@ -91,6 +165,8 @@
|
|
|
91
165
|
focusId = null,
|
|
92
166
|
onSelect,
|
|
93
167
|
onOpenEntity,
|
|
168
|
+
onEdgeHover,
|
|
169
|
+
legend,
|
|
94
170
|
class: className
|
|
95
171
|
}: ForceGraphProps = $props();
|
|
96
172
|
|
|
@@ -262,30 +338,46 @@
|
|
|
262
338
|
const positionedNodes = $derived.by(() =>
|
|
263
339
|
nodes.map((n, i) => {
|
|
264
340
|
const p = layout.get(n.id) ?? { x: width / 2, y: height / 2 };
|
|
341
|
+
const r = nodeRadius * Math.sqrt(Math.max(n.weight ?? 1, 0.25));
|
|
342
|
+
const shapePath = nodeShapePath(n.shape, r);
|
|
265
343
|
return {
|
|
266
344
|
node: n,
|
|
267
345
|
i,
|
|
268
346
|
x: p.x,
|
|
269
347
|
y: p.y,
|
|
270
|
-
r
|
|
348
|
+
r,
|
|
271
349
|
tone: toneMap.get(n.id) ?? "category1",
|
|
272
|
-
title: n.label ?? n.id
|
|
350
|
+
title: n.label ?? n.id,
|
|
351
|
+
shapePath
|
|
273
352
|
};
|
|
274
353
|
})
|
|
275
354
|
);
|
|
276
355
|
|
|
277
356
|
const positionedEdges = $derived.by(() => {
|
|
357
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
278
358
|
return edges
|
|
279
359
|
.map((e, i) => {
|
|
280
360
|
const a = layout.get(e.source);
|
|
281
361
|
const b = layout.get(e.target);
|
|
282
362
|
if (!a || !b) return null;
|
|
283
|
-
|
|
363
|
+
const srcNode = nodeById.get(e.source);
|
|
364
|
+
const tgtNode = nodeById.get(e.target);
|
|
365
|
+
return {
|
|
366
|
+
edge: e,
|
|
367
|
+
i,
|
|
368
|
+
x1: a.x,
|
|
369
|
+
y1: a.y,
|
|
370
|
+
x2: b.x,
|
|
371
|
+
y2: b.y,
|
|
372
|
+
srcLabel: srcNode?.label ?? e.source,
|
|
373
|
+
tgtLabel: tgtNode?.label ?? e.target
|
|
374
|
+
};
|
|
284
375
|
})
|
|
285
376
|
.filter((e): e is NonNullable<typeof e> => e !== null);
|
|
286
377
|
});
|
|
287
378
|
|
|
288
|
-
let
|
|
379
|
+
let hoveredNodeIndex: number | null = $state(null);
|
|
380
|
+
let hoveredEdgeIndex: number | null = $state(null);
|
|
289
381
|
|
|
290
382
|
// Fast lookup sets — recomputed only when selectedIds/focusId props change,
|
|
291
383
|
// never when nodes/edges change.
|
|
@@ -302,31 +394,128 @@
|
|
|
302
394
|
}
|
|
303
395
|
}
|
|
304
396
|
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Zoom + pan state (Feature 2)
|
|
399
|
+
// Store zoom as a scale multiplier + pan offset so syncing with width/height
|
|
400
|
+
// props is trivial (no stale-capture warnings).
|
|
401
|
+
// vbW = width / zoomScale, vbH = height / zoomScale
|
|
402
|
+
// vbX / vbY = pan offset in SVG coordinate space
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
let zoomScale = $state(1);
|
|
405
|
+
let panX = $state(0);
|
|
406
|
+
let panY = $state(0);
|
|
407
|
+
|
|
408
|
+
let isPanning = $state(false);
|
|
409
|
+
let panStart = $state({ x: 0, y: 0, panX: 0, panY: 0 });
|
|
410
|
+
let svgEl: SVGSVGElement | null = $state(null);
|
|
411
|
+
|
|
412
|
+
// Derived viewBox dimensions always reflect current props + zoom.
|
|
413
|
+
const vbW = $derived(width / zoomScale);
|
|
414
|
+
const vbH = $derived(height / zoomScale);
|
|
415
|
+
const vbX = $derived(panX);
|
|
416
|
+
const vbY = $derived(panY);
|
|
417
|
+
|
|
418
|
+
function resetView() {
|
|
419
|
+
zoomScale = 1;
|
|
420
|
+
panX = 0;
|
|
421
|
+
panY = 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function handleWheel(ev: WheelEvent) {
|
|
425
|
+
if (prefersReducedMotion) return;
|
|
426
|
+
ev.preventDefault();
|
|
427
|
+
// Zoom factor: ~10% per step.
|
|
428
|
+
const factor = ev.deltaY > 0 ? 0.9 : 1.1;
|
|
429
|
+
// Clamp zoom: 0.2x – 8x.
|
|
430
|
+
const newScale = Math.min(Math.max(zoomScale * factor, 0.2), 8);
|
|
431
|
+
// Anchor zoom around the cursor position in SVG coords.
|
|
432
|
+
if (svgEl) {
|
|
433
|
+
const rect = svgEl.getBoundingClientRect();
|
|
434
|
+
const cursorSvgX = panX + ((ev.clientX - rect.left) / rect.width) * (width / zoomScale);
|
|
435
|
+
const cursorSvgY = panY + ((ev.clientY - rect.top) / rect.height) * (height / zoomScale);
|
|
436
|
+
const newVbW = width / newScale;
|
|
437
|
+
const newVbH = height / newScale;
|
|
438
|
+
const ratioX = (cursorSvgX - panX) / (width / zoomScale);
|
|
439
|
+
const ratioY = (cursorSvgY - panY) / (height / zoomScale);
|
|
440
|
+
panX = cursorSvgX - ratioX * newVbW;
|
|
441
|
+
panY = cursorSvgY - ratioY * newVbH;
|
|
442
|
+
}
|
|
443
|
+
zoomScale = newScale;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function handleBgMouseDown(ev: MouseEvent) {
|
|
447
|
+
// Only start pan when clicking the background (not a node/edge element).
|
|
448
|
+
if ((ev.target as Element).closest(".st-forceGraph__node")) return;
|
|
449
|
+
if (prefersReducedMotion) return;
|
|
450
|
+
isPanning = true;
|
|
451
|
+
panStart = { x: ev.clientX, y: ev.clientY, panX, panY };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function handleMouseMove(ev: MouseEvent) {
|
|
455
|
+
if (!isPanning || !svgEl) return;
|
|
456
|
+
const rect = svgEl.getBoundingClientRect();
|
|
457
|
+
const dx = ((ev.clientX - panStart.x) / rect.width) * vbW;
|
|
458
|
+
const dy = ((ev.clientY - panStart.y) / rect.height) * vbH;
|
|
459
|
+
panX = panStart.panX - dx;
|
|
460
|
+
panY = panStart.panY - dy;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function handleMouseUp() {
|
|
464
|
+
isPanning = false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const viewBox = $derived(`${vbX} ${vbY} ${vbW} ${vbH}`);
|
|
468
|
+
const isZoomed = $derived(zoomScale !== 1 || panX !== 0 || panY !== 0);
|
|
469
|
+
|
|
305
470
|
const classes = () =>
|
|
306
471
|
["st-forceGraph", prefersReducedMotion ? "st-forceGraph--static" : null, className]
|
|
307
472
|
.filter(Boolean)
|
|
308
473
|
.join(" ");
|
|
309
474
|
</script>
|
|
310
475
|
|
|
311
|
-
<div
|
|
476
|
+
<div
|
|
477
|
+
class={classes()}
|
|
478
|
+
role="img"
|
|
479
|
+
aria-label={label}
|
|
480
|
+
>
|
|
312
481
|
<svg
|
|
313
|
-
|
|
482
|
+
bind:this={svgEl}
|
|
483
|
+
viewBox={viewBox}
|
|
314
484
|
preserveAspectRatio="xMidYMid meet"
|
|
315
485
|
width="100%"
|
|
316
486
|
height="100%"
|
|
317
487
|
focusable="false"
|
|
318
488
|
aria-hidden="true"
|
|
489
|
+
class:st-forceGraph__svg--panning={isPanning}
|
|
490
|
+
onwheel={handleWheel}
|
|
491
|
+
onmousedown={handleBgMouseDown}
|
|
492
|
+
onmousemove={handleMouseMove}
|
|
493
|
+
onmouseup={handleMouseUp}
|
|
494
|
+
onmouseleave={handleMouseUp}
|
|
319
495
|
>
|
|
320
496
|
<!-- edges first so nodes paint on top -->
|
|
321
497
|
<g class="st-forceGraph__edges">
|
|
322
498
|
{#each positionedEdges as e (e.i)}
|
|
499
|
+
<!-- Invisible wider hit area for edge hover -->
|
|
500
|
+
<line
|
|
501
|
+
class="st-forceGraph__edgeHit"
|
|
502
|
+
role="presentation"
|
|
503
|
+
x1={e.x1}
|
|
504
|
+
y1={e.y1}
|
|
505
|
+
x2={e.x2}
|
|
506
|
+
y2={e.y2}
|
|
507
|
+
onmouseenter={() => { hoveredEdgeIndex = e.i; onEdgeHover?.(e.edge); }}
|
|
508
|
+
onmouseleave={() => { hoveredEdgeIndex = null; }}
|
|
509
|
+
/>
|
|
323
510
|
<line
|
|
324
511
|
class="st-forceGraph__edge"
|
|
325
512
|
class:st-forceGraph__edge--weak={e.edge.weak}
|
|
513
|
+
class:st-forceGraph__edge--hovered={hoveredEdgeIndex === e.i}
|
|
326
514
|
x1={e.x1}
|
|
327
515
|
y1={e.y1}
|
|
328
516
|
x2={e.x2}
|
|
329
517
|
y2={e.y2}
|
|
518
|
+
pointer-events="none"
|
|
330
519
|
/>
|
|
331
520
|
{/each}
|
|
332
521
|
</g>
|
|
@@ -335,26 +524,44 @@
|
|
|
335
524
|
{#each positionedNodes as p (p.node.id)}
|
|
336
525
|
<g
|
|
337
526
|
class="st-forceGraph__node st-forceGraph__node--{p.tone}"
|
|
338
|
-
class:st-forceGraph__node--dim={
|
|
527
|
+
class:st-forceGraph__node--dim={hoveredNodeIndex !== null && hoveredNodeIndex !== p.i}
|
|
339
528
|
class:st-forceGraph__node--selected={selectedSet.has(p.node.id)}
|
|
340
529
|
class:st-forceGraph__node--focus={focusId === p.node.id}
|
|
341
530
|
transform="translate({p.x} {p.y})"
|
|
342
531
|
>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
532
|
+
{#if p.shapePath}
|
|
533
|
+
<path
|
|
534
|
+
class="st-forceGraph__dot"
|
|
535
|
+
d={p.shapePath}
|
|
536
|
+
tabindex="0"
|
|
537
|
+
role="button"
|
|
538
|
+
aria-label="{p.title}{p.node.group !== undefined ? `: ${p.node.group}` : ''}"
|
|
539
|
+
aria-pressed={selectedSet.has(p.node.id)}
|
|
540
|
+
onmouseenter={() => (hoveredNodeIndex = p.i)}
|
|
541
|
+
onmouseleave={() => (hoveredNodeIndex = null)}
|
|
542
|
+
onfocus={() => (hoveredNodeIndex = p.i)}
|
|
543
|
+
onblur={() => (hoveredNodeIndex = null)}
|
|
544
|
+
onclick={() => onSelect?.(p.node.id)}
|
|
545
|
+
ondblclick={() => onOpenEntity?.(p.node.id)}
|
|
546
|
+
onkeydown={(e) => handleNodeKeydown(p.node.id, e)}
|
|
547
|
+
/>
|
|
548
|
+
{:else}
|
|
549
|
+
<circle
|
|
550
|
+
class="st-forceGraph__dot"
|
|
551
|
+
r={p.r}
|
|
552
|
+
tabindex="0"
|
|
553
|
+
role="button"
|
|
554
|
+
aria-label="{p.title}{p.node.group !== undefined ? `: ${p.node.group}` : ''}"
|
|
555
|
+
aria-pressed={selectedSet.has(p.node.id)}
|
|
556
|
+
onmouseenter={() => (hoveredNodeIndex = p.i)}
|
|
557
|
+
onmouseleave={() => (hoveredNodeIndex = null)}
|
|
558
|
+
onfocus={() => (hoveredNodeIndex = p.i)}
|
|
559
|
+
onblur={() => (hoveredNodeIndex = null)}
|
|
560
|
+
onclick={() => onSelect?.(p.node.id)}
|
|
561
|
+
ondblclick={() => onOpenEntity?.(p.node.id)}
|
|
562
|
+
onkeydown={(e) => handleNodeKeydown(p.node.id, e)}
|
|
563
|
+
/>
|
|
564
|
+
{/if}
|
|
358
565
|
{#if showLabels}
|
|
359
566
|
<text class="st-forceGraph__label" x={p.r + 3} y="0" dominant-baseline="middle">{p.title}</text>
|
|
360
567
|
{/if}
|
|
@@ -363,15 +570,16 @@
|
|
|
363
570
|
</g>
|
|
364
571
|
</svg>
|
|
365
572
|
|
|
366
|
-
|
|
367
|
-
|
|
573
|
+
<!-- Node tooltip -->
|
|
574
|
+
{#if hoveredNodeIndex !== null && positionedNodes[hoveredNodeIndex]}
|
|
575
|
+
{@const p = positionedNodes[hoveredNodeIndex]}
|
|
368
576
|
{@const relCount = positionedEdges.filter(
|
|
369
577
|
(e) => e.edge.source === p.node.id || e.edge.target === p.node.id
|
|
370
578
|
).length}
|
|
371
579
|
<div
|
|
372
580
|
class="st-forceGraph__tooltip"
|
|
373
581
|
role="presentation"
|
|
374
|
-
style="left: {(p.x /
|
|
582
|
+
style="left: {((p.x - vbX) / vbW) * 100}%; top: {((p.y - vbY) / vbH) * 100}%"
|
|
375
583
|
>
|
|
376
584
|
<span class="st-forceGraph__tooltipLabel">{p.title}</span>
|
|
377
585
|
{#if p.node.group !== undefined}
|
|
@@ -382,6 +590,91 @@
|
|
|
382
590
|
{/if}
|
|
383
591
|
</div>
|
|
384
592
|
{/if}
|
|
593
|
+
|
|
594
|
+
<!-- Edge tooltip -->
|
|
595
|
+
{#if hoveredEdgeIndex !== null}
|
|
596
|
+
{@const e = positionedEdges.find((pe) => pe.i === hoveredEdgeIndex)}
|
|
597
|
+
{#if e}
|
|
598
|
+
{@const midX = (e.x1 + e.x2) / 2}
|
|
599
|
+
{@const midY = (e.y1 + e.y2) / 2}
|
|
600
|
+
<div
|
|
601
|
+
class="st-forceGraph__tooltip st-forceGraph__tooltip--edge"
|
|
602
|
+
role="presentation"
|
|
603
|
+
style="left: {((midX - vbX) / vbW) * 100}%; top: {((midY - vbY) / vbH) * 100}%"
|
|
604
|
+
>
|
|
605
|
+
<span class="st-forceGraph__tooltipLabel">{e.srcLabel}</span>
|
|
606
|
+
{#if e.edge.relation}
|
|
607
|
+
<span class="st-forceGraph__tooltipRelation">{e.edge.relation}</span>
|
|
608
|
+
{/if}
|
|
609
|
+
<span class="st-forceGraph__tooltipLabel">{e.tgtLabel}</span>
|
|
610
|
+
</div>
|
|
611
|
+
{/if}
|
|
612
|
+
{/if}
|
|
613
|
+
|
|
614
|
+
<!-- Reset view button (only shown when zoomed/panned) -->
|
|
615
|
+
{#if isZoomed}
|
|
616
|
+
<button
|
|
617
|
+
class="st-forceGraph__resetBtn"
|
|
618
|
+
type="button"
|
|
619
|
+
aria-label="Reset view"
|
|
620
|
+
onclick={resetView}
|
|
621
|
+
>
|
|
622
|
+
↺
|
|
623
|
+
</button>
|
|
624
|
+
{/if}
|
|
625
|
+
|
|
626
|
+
<!-- Legend overlay -->
|
|
627
|
+
{#if legend && legend.length > 0}
|
|
628
|
+
<div class="st-forceGraph__legend" aria-label="Graph legend">
|
|
629
|
+
{#each legend as entry}
|
|
630
|
+
{@const swatchPath = entry.shape !== undefined ? nodeShapePath(entry.shape, 7) : null}
|
|
631
|
+
{@const swatchTone = entry.tone ?? "category1"}
|
|
632
|
+
<div class="st-forceGraph__legendEntry">
|
|
633
|
+
{#if entry.shape !== undefined}
|
|
634
|
+
<!-- Node shape legend entry -->
|
|
635
|
+
<svg
|
|
636
|
+
class="st-forceGraph__legendSwatch"
|
|
637
|
+
viewBox="-8 -8 16 16"
|
|
638
|
+
width="16"
|
|
639
|
+
height="16"
|
|
640
|
+
aria-hidden="true"
|
|
641
|
+
>
|
|
642
|
+
{#if swatchPath}
|
|
643
|
+
<path
|
|
644
|
+
d={swatchPath}
|
|
645
|
+
class="st-forceGraph__legendShape st-forceGraph__legendShape--{swatchTone}"
|
|
646
|
+
/>
|
|
647
|
+
{:else}
|
|
648
|
+
<circle
|
|
649
|
+
r="7"
|
|
650
|
+
class="st-forceGraph__legendShape st-forceGraph__legendShape--{swatchTone}"
|
|
651
|
+
/>
|
|
652
|
+
{/if}
|
|
653
|
+
</svg>
|
|
654
|
+
{:else}
|
|
655
|
+
<!-- Edge style legend entry -->
|
|
656
|
+
<svg
|
|
657
|
+
class="st-forceGraph__legendSwatch"
|
|
658
|
+
viewBox="0 0 16 8"
|
|
659
|
+
width="16"
|
|
660
|
+
height="8"
|
|
661
|
+
aria-hidden="true"
|
|
662
|
+
>
|
|
663
|
+
<line
|
|
664
|
+
x1="0"
|
|
665
|
+
y1="4"
|
|
666
|
+
x2="16"
|
|
667
|
+
y2="4"
|
|
668
|
+
class="st-forceGraph__legendEdge"
|
|
669
|
+
class:st-forceGraph__legendEdge--weak={entry.weak}
|
|
670
|
+
/>
|
|
671
|
+
</svg>
|
|
672
|
+
{/if}
|
|
673
|
+
<span class="st-forceGraph__legendLabel">{entry.label}</span>
|
|
674
|
+
</div>
|
|
675
|
+
{/each}
|
|
676
|
+
</div>
|
|
677
|
+
{/if}
|
|
385
678
|
</div>
|
|
386
679
|
|
|
387
680
|
<style>
|
|
@@ -395,10 +688,13 @@
|
|
|
395
688
|
|
|
396
689
|
.st-forceGraph svg { display: block; overflow: visible; }
|
|
397
690
|
|
|
691
|
+
.st-forceGraph__svg--panning { cursor: grabbing; }
|
|
692
|
+
|
|
398
693
|
.st-forceGraph__edge {
|
|
399
694
|
stroke: var(--st-semantic-border-strong);
|
|
400
695
|
stroke-width: 1;
|
|
401
696
|
opacity: 0.55;
|
|
697
|
+
transition: opacity 120ms ease, stroke-width 120ms ease;
|
|
402
698
|
}
|
|
403
699
|
|
|
404
700
|
.st-forceGraph__edge--weak {
|
|
@@ -407,6 +703,19 @@
|
|
|
407
703
|
opacity: 0.5;
|
|
408
704
|
}
|
|
409
705
|
|
|
706
|
+
.st-forceGraph__edge--hovered {
|
|
707
|
+
opacity: 0.9;
|
|
708
|
+
stroke-width: 2;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* Invisible wide hit target for edge hover */
|
|
712
|
+
.st-forceGraph__edgeHit {
|
|
713
|
+
stroke: transparent;
|
|
714
|
+
stroke-width: 10;
|
|
715
|
+
fill: none;
|
|
716
|
+
cursor: crosshair;
|
|
717
|
+
}
|
|
718
|
+
|
|
410
719
|
.st-forceGraph__node { transition: opacity 120ms ease; }
|
|
411
720
|
.st-forceGraph__node--dim { opacity: 0.3; }
|
|
412
721
|
|
|
@@ -475,9 +784,102 @@
|
|
|
475
784
|
|
|
476
785
|
.st-forceGraph__tooltipLabel { font-weight: 600; }
|
|
477
786
|
.st-forceGraph__tooltipMeta { opacity: 0.85; }
|
|
787
|
+
.st-forceGraph__tooltipRelation {
|
|
788
|
+
opacity: 0.75;
|
|
789
|
+
font-style: italic;
|
|
790
|
+
font-size: 0.6875rem;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/* Reset view button */
|
|
794
|
+
.st-forceGraph__resetBtn {
|
|
795
|
+
background: var(--st-semantic-surface-overlay, rgba(0,0,0,0.55));
|
|
796
|
+
border: none;
|
|
797
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
798
|
+
color: var(--st-semantic-text-inverse, #fff);
|
|
799
|
+
cursor: pointer;
|
|
800
|
+
font-size: 1rem;
|
|
801
|
+
line-height: 1;
|
|
802
|
+
padding: 0.25rem 0.5rem;
|
|
803
|
+
position: absolute;
|
|
804
|
+
bottom: 0.5rem;
|
|
805
|
+
right: 0.5rem;
|
|
806
|
+
opacity: 0.8;
|
|
807
|
+
transition: opacity 120ms ease;
|
|
808
|
+
z-index: 2;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.st-forceGraph__resetBtn:hover,
|
|
812
|
+
.st-forceGraph__resetBtn:focus-visible {
|
|
813
|
+
opacity: 1;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.st-forceGraph__resetBtn:focus-visible {
|
|
817
|
+
outline: 2px solid var(--st-semantic-border-interactive);
|
|
818
|
+
outline-offset: 2px;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/* Legend overlay */
|
|
822
|
+
.st-forceGraph__legend {
|
|
823
|
+
background: var(--st-semantic-surface-overlay, rgba(0,0,0,0.45));
|
|
824
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
825
|
+
color: var(--st-semantic-text-inverse, #fff);
|
|
826
|
+
display: flex;
|
|
827
|
+
flex-direction: column;
|
|
828
|
+
font-size: 0.6875rem;
|
|
829
|
+
gap: 0.25rem;
|
|
830
|
+
padding: 0.375rem 0.5rem;
|
|
831
|
+
pointer-events: none;
|
|
832
|
+
position: absolute;
|
|
833
|
+
bottom: 0.5rem;
|
|
834
|
+
left: 0.5rem;
|
|
835
|
+
z-index: 2;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.st-forceGraph__legendEntry {
|
|
839
|
+
align-items: center;
|
|
840
|
+
display: flex;
|
|
841
|
+
gap: 0.375rem;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
.st-forceGraph__legendSwatch {
|
|
845
|
+
flex-shrink: 0;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.st-forceGraph__legendLabel {
|
|
849
|
+
white-space: nowrap;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.st-forceGraph__legendShape {
|
|
853
|
+
fill-opacity: 0.9;
|
|
854
|
+
stroke: var(--st-semantic-surface-default, #fff);
|
|
855
|
+
stroke-width: 1;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.st-forceGraph__legendShape--category1 { fill: var(--st-semantic-data-category1); }
|
|
859
|
+
.st-forceGraph__legendShape--category2 { fill: var(--st-semantic-data-category2); }
|
|
860
|
+
.st-forceGraph__legendShape--category3 { fill: var(--st-semantic-data-category3); }
|
|
861
|
+
.st-forceGraph__legendShape--category4 { fill: var(--st-semantic-data-category4); }
|
|
862
|
+
.st-forceGraph__legendShape--category5 { fill: var(--st-semantic-data-category5); }
|
|
863
|
+
.st-forceGraph__legendShape--category6 { fill: var(--st-semantic-data-category6); }
|
|
864
|
+
.st-forceGraph__legendShape--category7 { fill: var(--st-semantic-data-category7); }
|
|
865
|
+
.st-forceGraph__legendShape--category8 { fill: var(--st-semantic-data-category8); }
|
|
866
|
+
|
|
867
|
+
.st-forceGraph__legendEdge {
|
|
868
|
+
stroke: var(--st-semantic-border-strong, #888);
|
|
869
|
+
stroke-width: 1.5;
|
|
870
|
+
opacity: 0.8;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.st-forceGraph__legendEdge--weak {
|
|
874
|
+
stroke: var(--st-semantic-border-subtle, #aaa);
|
|
875
|
+
stroke-dasharray: 3 3;
|
|
876
|
+
opacity: 0.65;
|
|
877
|
+
}
|
|
478
878
|
|
|
479
879
|
@media (prefers-reduced-motion: reduce) {
|
|
480
880
|
.st-forceGraph__node,
|
|
481
|
-
.st-forceGraph__dot
|
|
881
|
+
.st-forceGraph__dot,
|
|
882
|
+
.st-forceGraph__edge,
|
|
883
|
+
.st-forceGraph__resetBtn { transition: none; }
|
|
482
884
|
}
|
|
483
885
|
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type ForceGraphTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
|
|
2
|
+
export type ForceGraphNodeShape = "dot" | "circle" | "diamond" | "star" | "hexagon" | "box" | "square" | "triangle";
|
|
2
3
|
export type ForceGraphNode = {
|
|
3
4
|
/** Stable identifier; referenced by edges. */
|
|
4
5
|
id: string;
|
|
@@ -16,6 +17,11 @@ export type ForceGraphNode = {
|
|
|
16
17
|
/** Pin the node to a fixed position (ignored by the simulation). */
|
|
17
18
|
fx?: number;
|
|
18
19
|
fy?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Visual shape for the node. Defaults to 'dot' (circle).
|
|
22
|
+
* Supported: 'dot'|'circle', 'diamond', 'star', 'hexagon', 'box'|'square', 'triangle'.
|
|
23
|
+
*/
|
|
24
|
+
shape?: ForceGraphNodeShape;
|
|
19
25
|
};
|
|
20
26
|
export type ForceGraphEdge = {
|
|
21
27
|
/** Source node id. */
|
|
@@ -30,6 +36,17 @@ export type ForceGraphEdge = {
|
|
|
30
36
|
*/
|
|
31
37
|
weak?: boolean;
|
|
32
38
|
};
|
|
39
|
+
export type ForceGraphLegendEntry = {
|
|
40
|
+
/** Label shown in the legend. */
|
|
41
|
+
label: string;
|
|
42
|
+
/** Shape for this entry (node legend). Absent = line-style legend entry. */
|
|
43
|
+
shape?: ForceGraphNodeShape;
|
|
44
|
+
/** Tone for this entry. Defaults to category1. */
|
|
45
|
+
tone?: ForceGraphTone;
|
|
46
|
+
/** When true, renders as a dashed line (edge legend). */
|
|
47
|
+
weak?: boolean;
|
|
48
|
+
};
|
|
49
|
+
export declare function nodeShapePath(shape: ForceGraphNodeShape | undefined, r: number): string | null;
|
|
33
50
|
type ForceGraphProps = {
|
|
34
51
|
nodes: ForceGraphNode[];
|
|
35
52
|
edges: ForceGraphEdge[];
|
|
@@ -67,6 +84,16 @@ type ForceGraphProps = {
|
|
|
67
84
|
* Fires with the node's stable id.
|
|
68
85
|
*/
|
|
69
86
|
onOpenEntity?: (id: string) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Called when the user hovers an edge.
|
|
89
|
+
* Fires with the edge object (source/target/relation/weak).
|
|
90
|
+
*/
|
|
91
|
+
onEdgeHover?: (edge: ForceGraphEdge) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Legend entries rendered as a corner overlay.
|
|
94
|
+
* Each entry has a label + optional shape (node) or weak (edge).
|
|
95
|
+
*/
|
|
96
|
+
legend?: ForceGraphLegendEntry[];
|
|
70
97
|
class?: string;
|
|
71
98
|
};
|
|
72
99
|
declare const ForceGraph: import("svelte").Component<ForceGraphProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ForceGraph.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ForceGraph.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,gEAAgE;IAChE,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ForceGraph.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ForceGraph.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,mBAAmB,GAC3B,KAAK,GAAG,QAAQ,GAChB,SAAS,GACT,MAAM,GACN,SAAS,GACT,KAAK,GAAG,QAAQ,GAChB,UAAU,CAAC;AAEf,MAAM,MAAM,cAAc,GAAG;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,gEAAgE;IAChE,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,kDAAkD;IAClD,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,yDAAyD;IACzD,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAMF,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkC9F;AAED,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C;;;OAGG;IACH,MAAM,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4aJ,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|