@opendata-ai/openchart-vanilla 8.0.0-rc.2 → 8.0.0-rc.21
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/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
- package/dist/chunk-EKAUQAAX.js.map +1 -0
- package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
- package/dist/chunk-KNT5AZ2O.js.map +1 -0
- package/dist/chunk-RM5XS6NR.js +113 -0
- package/dist/chunk-RM5XS6NR.js.map +1 -0
- package/dist/chunk-T32UHMZA.js +282 -0
- package/dist/chunk-T32UHMZA.js.map +1 -0
- package/dist/export-gif.d.ts +113 -0
- package/dist/export-gif.js +21 -0
- package/dist/export-gif.js.map +1 -0
- package/dist/index.d.ts +283 -24
- package/dist/index.js +2012 -763
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
- package/dist/simulation-worker.js +83 -2
- package/dist/static.js +7 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/story/index.js +5 -3
- package/dist/story/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +14 -6
- package/dist/chunk-5EOV5JQZ.js.map +0 -1
- package/dist/chunk-ENXSUXC5.js.map +0 -1
- package/src/__test-fixtures__/dom.ts +0 -70
- package/src/__test-fixtures__/specs.ts +0 -214
- package/src/__tests__/animation.test.ts +0 -358
- package/src/__tests__/annotations-render.test.ts +0 -201
- package/src/__tests__/compound-labels.test.ts +0 -122
- package/src/__tests__/continuous-legend.test.ts +0 -91
- package/src/__tests__/crosshair.test.ts +0 -130
- package/src/__tests__/edit-events.test.ts +0 -902
- package/src/__tests__/events.test.ts +0 -387
- package/src/__tests__/export.test.ts +0 -241
- package/src/__tests__/font-race.test.ts +0 -199
- package/src/__tests__/gradient-ids.test.ts +0 -159
- package/src/__tests__/gradient-utils.test.ts +0 -125
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -293
- package/src/__tests__/mount.test.ts +0 -357
- package/src/__tests__/pattern-fills.test.ts +0 -105
- package/src/__tests__/resize-timing.test.ts +0 -218
- package/src/__tests__/sankey.test.ts +0 -133
- package/src/__tests__/selection-events.test.ts +0 -869
- package/src/__tests__/series-search.test.ts +0 -311
- package/src/__tests__/static.test.ts +0 -141
- package/src/__tests__/svg-renderer.test.ts +0 -895
- package/src/__tests__/table-keyboard.test.ts +0 -361
- package/src/__tests__/table-mount.test.ts +0 -639
- package/src/__tests__/tilemap.test.ts +0 -158
- package/src/__tests__/tooltip.test.ts +0 -337
- package/src/__tests__/transition.test.ts +0 -1777
- package/src/__tests__/you-draw-it.test.ts +0 -273
- package/src/animation-vars.ts +0 -42
- package/src/animation.ts +0 -76
- package/src/barlist-mount.ts +0 -383
- package/src/barlist-renderer.ts +0 -267
- package/src/dom-helpers.ts +0 -17
- package/src/export.ts +0 -482
- package/src/gradient-utils.ts +0 -138
- package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
- package/src/graph/__tests__/graph-mount.test.ts +0 -213
- package/src/graph/__tests__/interaction.test.ts +0 -205
- package/src/graph/__tests__/keyboard.test.ts +0 -653
- package/src/graph/__tests__/search.test.ts +0 -88
- package/src/graph/__tests__/simulation.test.ts +0 -233
- package/src/graph/__tests__/spatial-index.test.ts +0 -142
- package/src/graph/__tests__/zoom.test.ts +0 -203
- package/src/graph/canvas-renderer.ts +0 -747
- package/src/graph/interaction.ts +0 -367
- package/src/graph/keyboard.ts +0 -208
- package/src/graph/search.ts +0 -50
- package/src/graph/simulation-worker-url.ts +0 -12
- package/src/graph/simulation-worker.ts +0 -278
- package/src/graph/simulation.ts +0 -422
- package/src/graph/spatial-index.ts +0 -121
- package/src/graph/types.ts +0 -47
- package/src/graph/worker-protocol.ts +0 -70
- package/src/graph/zoom.ts +0 -134
- package/src/graph-mount.ts +0 -888
- package/src/index.ts +0 -76
- package/src/interactions/chart-events.ts +0 -139
- package/src/interactions/crosshair.ts +0 -233
- package/src/interactions/drag-handler.ts +0 -175
- package/src/interactions/editing-drags.ts +0 -515
- package/src/interactions/index.ts +0 -25
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -280
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -88
- package/src/map-mount.ts +0 -701
- package/src/map-renderer.ts +0 -338
- package/src/map-transition.ts +0 -83
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1382
- package/src/pattern-utils.ts +0 -110
- package/src/renderers/annotations.ts +0 -388
- package/src/renderers/axes.ts +0 -351
- package/src/renderers/brand.ts +0 -87
- package/src/renderers/chrome.ts +0 -196
- package/src/renderers/endpoint-labels.ts +0 -150
- package/src/renderers/legend.ts +0 -311
- package/src/renderers/marks.ts +0 -561
- package/src/renderers/metrics.ts +0 -63
- package/src/renderers/svg-dom.ts +0 -40
- package/src/renderers/table-cells.ts +0 -446
- package/src/resize-observer.ts +0 -46
- package/src/sankey-mount.ts +0 -622
- package/src/sankey-renderer.ts +0 -614
- package/src/series-search.ts +0 -392
- package/src/static.ts +0 -251
- package/src/story/__tests__/camera-math.test.ts +0 -145
- package/src/story/__tests__/create-chart-story.test.ts +0 -317
- package/src/story/__tests__/progress-math.test.ts +0 -95
- package/src/story/__tests__/scroll-driver.test.ts +0 -130
- package/src/story/__tests__/tween.test.ts +0 -159
- package/src/story/camera-math.ts +0 -125
- package/src/story/create-chart-story.ts +0 -353
- package/src/story/crossfade.ts +0 -70
- package/src/story/index.ts +0 -39
- package/src/story/progress-math.ts +0 -85
- package/src/story/resolve-camera-target.ts +0 -119
- package/src/story/scroll-driver.ts +0 -206
- package/src/story/story-camera.ts +0 -49
- package/src/story/tween.ts +0 -112
- package/src/story/types.ts +0 -76
- package/src/svg-ids.ts +0 -23
- package/src/svg-renderer.ts +0 -383
- package/src/table-keyboard.ts +0 -266
- package/src/table-mount.ts +0 -567
- package/src/table-renderer.ts +0 -414
- package/src/text-edit-overlay.ts +0 -255
- package/src/theme-tokens.ts +0 -51
- package/src/tilemap-mount.ts +0 -511
- package/src/tilemap-renderer.ts +0 -396
- package/src/tooltip.ts +0 -177
- package/src/transition.ts +0 -2816
- package/src/you-draw-it.ts +0 -480
- /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,31 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isModuleNotFound,
|
|
3
|
+
paletteFromCanvas,
|
|
4
|
+
readCanvasSRGB,
|
|
5
|
+
resolveRepeat
|
|
6
|
+
} from "./chunk-DCODCLWI.js";
|
|
1
7
|
import {
|
|
2
8
|
renderTileMapSVG
|
|
3
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-2NAQRP66.js";
|
|
4
10
|
import {
|
|
11
|
+
AnimationScheduler,
|
|
12
|
+
CANVAS_DEFAULT_UPDATE_MAX_MARKS,
|
|
13
|
+
DEFAULT_UPDATE_MAX_MARKS,
|
|
14
|
+
SpatialIndex,
|
|
15
|
+
VECTOR_EXPORT_MAX_POINTS,
|
|
5
16
|
cancelAnimations,
|
|
6
17
|
createChart,
|
|
7
18
|
createMap,
|
|
8
19
|
createMeasureText,
|
|
9
20
|
createTextEditOverlay,
|
|
10
21
|
createTooltipManager,
|
|
22
|
+
createTween,
|
|
11
23
|
createYouDrawIt,
|
|
24
|
+
materializeCanvasModeSVG,
|
|
25
|
+
observeResize,
|
|
26
|
+
prefersReducedMotion,
|
|
27
|
+
quadtree,
|
|
28
|
+
resolveEase,
|
|
29
|
+
resolveFontFamily,
|
|
30
|
+
scheduleFontReload,
|
|
31
|
+
setupAnimationCleanup,
|
|
32
|
+
setupTableAnimationCleanup,
|
|
33
|
+
zoom_default
|
|
34
|
+
} from "./chunk-KNT5AZ2O.js";
|
|
35
|
+
import {
|
|
36
|
+
embedFonts,
|
|
37
|
+
ensureSVGDimensions,
|
|
12
38
|
exportCSV,
|
|
13
39
|
exportJPG,
|
|
14
40
|
exportPNG,
|
|
15
41
|
exportSVG,
|
|
16
42
|
exportSVGWithFonts,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
setupTableAnimationCleanup
|
|
22
|
-
} from "./chunk-ENXSUXC5.js";
|
|
43
|
+
getSVGBackgroundColor,
|
|
44
|
+
getSVGDimensions,
|
|
45
|
+
rasterizeSVGToCanvas
|
|
46
|
+
} from "./chunk-T32UHMZA.js";
|
|
23
47
|
import {
|
|
24
48
|
rectPathWithCorners,
|
|
25
49
|
registerMarkRenderer,
|
|
26
50
|
renderChartSVG,
|
|
27
51
|
stampAnimationVars
|
|
28
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-EKAUQAAX.js";
|
|
53
|
+
import {
|
|
54
|
+
injectThemeStyleBlock
|
|
55
|
+
} from "./chunk-RM5XS6NR.js";
|
|
29
56
|
|
|
30
57
|
// src/barlist-mount.ts
|
|
31
58
|
import { compileBarList } from "@opendata-ai/openchart-engine";
|
|
@@ -462,22 +489,480 @@ function createBarList(container, spec, options) {
|
|
|
462
489
|
};
|
|
463
490
|
}
|
|
464
491
|
|
|
492
|
+
// src/export-sequence.ts
|
|
493
|
+
function settleAnimation(spec) {
|
|
494
|
+
const authored = spec.animation;
|
|
495
|
+
const update = typeof authored === "object" && typeof authored.update === "object" ? { ...authored.update } : true;
|
|
496
|
+
return {
|
|
497
|
+
...spec,
|
|
498
|
+
animation: { enter: false, update }
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function mountOffscreen(spec, width, height, darkMode, themeConfig) {
|
|
502
|
+
const container = document.createElement("div");
|
|
503
|
+
container.style.cssText = `position:absolute;left:-99999px;top:0;width:${width}px;height:${height}px;visibility:hidden;`;
|
|
504
|
+
document.body.appendChild(container);
|
|
505
|
+
const settledSpec = settleAnimation(spec);
|
|
506
|
+
const instance = createChart(container, settledSpec, {
|
|
507
|
+
darkMode,
|
|
508
|
+
theme: themeConfig,
|
|
509
|
+
responsive: false
|
|
510
|
+
});
|
|
511
|
+
const svg = container.querySelector("svg");
|
|
512
|
+
if (!svg) {
|
|
513
|
+
instance.destroy();
|
|
514
|
+
container.remove();
|
|
515
|
+
throw new Error("exportSpecSequence: chart produced no SVG");
|
|
516
|
+
}
|
|
517
|
+
return { instance, svg, container };
|
|
518
|
+
}
|
|
519
|
+
async function exportSpecSequence(specs, options) {
|
|
520
|
+
if (specs.length === 0) {
|
|
521
|
+
throw new Error("exportSpecSequence requires at least one spec");
|
|
522
|
+
}
|
|
523
|
+
let gifenc;
|
|
524
|
+
try {
|
|
525
|
+
gifenc = await import("gifenc");
|
|
526
|
+
} catch (err) {
|
|
527
|
+
if (isModuleNotFound(err)) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
"GIF export requires the optional 'gifenc' peer dependency. Install it: npm install gifenc"
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
throw new Error("GIF export failed to load the gifenc encoder", {
|
|
533
|
+
cause: err
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
const { GIFEncoder, quantize, applyPalette } = gifenc;
|
|
537
|
+
const dpi = options.dpi ?? 2;
|
|
538
|
+
const fps = options.fps ?? 25;
|
|
539
|
+
const dwellMs = options.dwellMs ?? 1200;
|
|
540
|
+
const frameDelay = Math.round(1e3 / fps);
|
|
541
|
+
const { width, height } = options;
|
|
542
|
+
const darkMode = options.darkMode ?? "off";
|
|
543
|
+
const themeConfig = options.theme;
|
|
544
|
+
const shouldEmbed = options.embedFonts ?? true;
|
|
545
|
+
const repeat = resolveRepeat(options.loop);
|
|
546
|
+
const { instance, svg, container } = mountOffscreen(
|
|
547
|
+
specs[0],
|
|
548
|
+
width,
|
|
549
|
+
height,
|
|
550
|
+
darkMode,
|
|
551
|
+
themeConfig
|
|
552
|
+
);
|
|
553
|
+
try {
|
|
554
|
+
const w = getSVGDimensions(svg).width || width;
|
|
555
|
+
const h = getSVGDimensions(svg).height || height;
|
|
556
|
+
const theme = () => {
|
|
557
|
+
const layout = instance.layout;
|
|
558
|
+
return layout && "theme" in layout ? layout.theme : void 0;
|
|
559
|
+
};
|
|
560
|
+
const bgColor = options.backgroundColor ?? getSVGBackgroundColor(svg, theme()?.colors.background);
|
|
561
|
+
const fillBackground = (ctx, cv) => {
|
|
562
|
+
ctx.fillStyle = bgColor;
|
|
563
|
+
ctx.fillRect(0, 0, cv.width, cv.height);
|
|
564
|
+
};
|
|
565
|
+
const prepareLiveSvg = async () => {
|
|
566
|
+
const live2 = container.querySelector("svg");
|
|
567
|
+
if (!live2) throw new Error("exportSpecSequence: live SVG vanished mid-sequence");
|
|
568
|
+
injectThemeStyleBlock(live2, theme());
|
|
569
|
+
if (shouldEmbed) await embedFonts(live2);
|
|
570
|
+
return live2;
|
|
571
|
+
};
|
|
572
|
+
const grab = async (live2) => {
|
|
573
|
+
const svgString = ensureSVGDimensions(exportSVG(live2), w, h);
|
|
574
|
+
return rasterizeSVGToCanvas(svgString, w, h, dpi, fillBackground);
|
|
575
|
+
};
|
|
576
|
+
const frames = [];
|
|
577
|
+
let live = await prepareLiveSvg();
|
|
578
|
+
frames.push({ canvas: await grab(live), delayMs: dwellMs });
|
|
579
|
+
for (let i = 1; i < specs.length; i++) {
|
|
580
|
+
const nextSpec = settleAnimation(specs[i]);
|
|
581
|
+
const handle = instance.beginManualUpdate(nextSpec);
|
|
582
|
+
live = await prepareLiveSvg();
|
|
583
|
+
if (handle) {
|
|
584
|
+
const totalMs = handle.totalMs;
|
|
585
|
+
for (let t = frameDelay; t < totalMs; t += frameDelay) {
|
|
586
|
+
handle.step(t);
|
|
587
|
+
frames.push({ canvas: await grab(live), delayMs: frameDelay });
|
|
588
|
+
}
|
|
589
|
+
handle.step(totalMs);
|
|
590
|
+
handle.cancel();
|
|
591
|
+
}
|
|
592
|
+
frames.push({ canvas: await grab(live), delayMs: dwellMs });
|
|
593
|
+
}
|
|
594
|
+
const globalPalette = paletteFromCanvas(frames[frames.length - 1].canvas, quantize);
|
|
595
|
+
const gif = GIFEncoder();
|
|
596
|
+
frames.forEach(({ canvas, delayMs }, i) => {
|
|
597
|
+
const index2 = applyPalette(readCanvasSRGB(canvas), globalPalette);
|
|
598
|
+
gif.writeFrame(index2, canvas.width, canvas.height, {
|
|
599
|
+
palette: globalPalette,
|
|
600
|
+
delay: delayMs,
|
|
601
|
+
// `repeat` writes the loop extension; only meaningful on the first frame.
|
|
602
|
+
...i === 0 ? { repeat } : {}
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
gif.finish();
|
|
606
|
+
return new Blob([gif.bytes()], { type: "image/gif" });
|
|
607
|
+
} finally {
|
|
608
|
+
instance.destroy();
|
|
609
|
+
container.remove();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
465
613
|
// src/graph/simulation-worker-url.ts
|
|
466
614
|
function createSimulationWorker() {
|
|
467
615
|
return new Worker(new URL("./simulation-worker.js", import.meta.url), { type: "module" });
|
|
468
616
|
}
|
|
469
617
|
|
|
470
618
|
// src/graph-mount.ts
|
|
471
|
-
import { compileGraph } from "@opendata-ai/openchart-engine";
|
|
619
|
+
import { buildEdgeTooltip, compileGraph } from "@opendata-ai/openchart-engine";
|
|
620
|
+
|
|
621
|
+
// src/graph/zoom.ts
|
|
622
|
+
var ZoomTransform = class _ZoomTransform {
|
|
623
|
+
constructor(x3, y3, k) {
|
|
624
|
+
this.x = x3;
|
|
625
|
+
this.y = y3;
|
|
626
|
+
this.k = k;
|
|
627
|
+
}
|
|
628
|
+
/** Convert screen coordinates to graph coordinates. */
|
|
629
|
+
screenToGraph(sx, sy) {
|
|
630
|
+
return {
|
|
631
|
+
x: (sx - this.x) / this.k,
|
|
632
|
+
y: (sy - this.y) / this.k
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/** Convert graph coordinates to screen coordinates. */
|
|
636
|
+
graphToScreen(gx, gy) {
|
|
637
|
+
return {
|
|
638
|
+
x: gx * this.k + this.x,
|
|
639
|
+
y: gy * this.k + this.y
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Zoom to a target scale, keeping the given screen-space pivot
|
|
644
|
+
* point fixed (content under the cursor stays under the cursor).
|
|
645
|
+
*/
|
|
646
|
+
zoomAt(targetK, pivotX, pivotY) {
|
|
647
|
+
const gx = (pivotX - this.x) / this.k;
|
|
648
|
+
const gy = (pivotY - this.y) / this.k;
|
|
649
|
+
return new _ZoomTransform(pivotX - gx * targetK, pivotY - gy * targetK, targetK);
|
|
650
|
+
}
|
|
651
|
+
/** Pan by a screen-space delta. */
|
|
652
|
+
pan(dx, dy) {
|
|
653
|
+
return new _ZoomTransform(this.x + dx, this.y + dy, this.k);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Compute a transform that fits all nodes within the given canvas
|
|
657
|
+
* dimensions with the specified padding.
|
|
658
|
+
*
|
|
659
|
+
* Returns the transform and the ideal content height (in screen pixels)
|
|
660
|
+
* so callers can shrink the canvas to eliminate dead space.
|
|
661
|
+
*/
|
|
662
|
+
static fitBounds(nodes, canvasW, canvasH, padding = 40, opts) {
|
|
663
|
+
if (nodes.length === 0) {
|
|
664
|
+
return { transform: _ZoomTransform.identity(), contentHeight: canvasH };
|
|
665
|
+
}
|
|
666
|
+
const insetTop = Math.min(Math.max(0, opts?.insetTop ?? 0), canvasH * 0.4);
|
|
667
|
+
let minX = Infinity;
|
|
668
|
+
let minY = Infinity;
|
|
669
|
+
let maxX = -Infinity;
|
|
670
|
+
let maxY = -Infinity;
|
|
671
|
+
for (const n of nodes) {
|
|
672
|
+
const r = n.radius;
|
|
673
|
+
if (n.x - r < minX) minX = n.x - r;
|
|
674
|
+
if (n.y - r < minY) minY = n.y - r;
|
|
675
|
+
if (n.x + r > maxX) maxX = n.x + r;
|
|
676
|
+
if (n.y + r > maxY) maxY = n.y + r;
|
|
677
|
+
}
|
|
678
|
+
let graphW = maxX - minX;
|
|
679
|
+
let graphH = maxY - minY;
|
|
680
|
+
if (graphW === 0 && graphH === 0) {
|
|
681
|
+
return {
|
|
682
|
+
transform: new _ZoomTransform(
|
|
683
|
+
canvasW / 2 - minX,
|
|
684
|
+
insetTop + (canvasH - insetTop) / 2 - minY,
|
|
685
|
+
1
|
|
686
|
+
),
|
|
687
|
+
contentHeight: padding * 2 + insetTop
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
if (opts?.spread !== false && nodes.length > 50) {
|
|
691
|
+
const spread = 1 + Math.sqrt(nodes.length) / 120;
|
|
692
|
+
const cx2 = (minX + maxX) / 2;
|
|
693
|
+
const cy2 = (minY + maxY) / 2;
|
|
694
|
+
graphW *= spread;
|
|
695
|
+
graphH *= spread;
|
|
696
|
+
minX = cx2 - graphW / 2;
|
|
697
|
+
maxX = cx2 + graphW / 2;
|
|
698
|
+
minY = cy2 - graphH / 2;
|
|
699
|
+
maxY = cy2 + graphH / 2;
|
|
700
|
+
}
|
|
701
|
+
const availW = canvasW - padding * 2;
|
|
702
|
+
const availH = canvasH - insetTop - padding * 2;
|
|
703
|
+
const k = Math.min(1, availW / graphW, availH / graphH);
|
|
704
|
+
const cx = (minX + maxX) / 2;
|
|
705
|
+
const cy = (minY + maxY) / 2;
|
|
706
|
+
const tx = canvasW / 2 - cx * k;
|
|
707
|
+
const ty = insetTop + (canvasH - insetTop) / 2 - cy * k;
|
|
708
|
+
const contentHeight = graphH * k + padding * 2 + insetTop;
|
|
709
|
+
return {
|
|
710
|
+
transform: new _ZoomTransform(tx, ty, k),
|
|
711
|
+
contentHeight
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/** Identity transform (no pan, no zoom). */
|
|
715
|
+
static identity() {
|
|
716
|
+
return new _ZoomTransform(0, 0, 1);
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// src/graph/camera.ts
|
|
721
|
+
var K_MIN = 0.05;
|
|
722
|
+
var K_MAX = 15;
|
|
723
|
+
var AUTO_MIN_MS = 300;
|
|
724
|
+
var AUTO_MAX_MS = 1200;
|
|
725
|
+
var AUTO_SCALE = 0.6;
|
|
726
|
+
function transformToView(t, viewport) {
|
|
727
|
+
const cx = (viewport.width / 2 - t.x) / t.k;
|
|
728
|
+
const cy = (viewport.height / 2 - t.y) / t.k;
|
|
729
|
+
const width = viewport.width / t.k;
|
|
730
|
+
return [cx, cy, width];
|
|
731
|
+
}
|
|
732
|
+
function viewToTransform(view, viewport) {
|
|
733
|
+
const [cx, cy, width] = view;
|
|
734
|
+
const k = clampK(viewport.width / width);
|
|
735
|
+
const x3 = viewport.width / 2 - cx * k;
|
|
736
|
+
const y3 = viewport.height / 2 - cy * k;
|
|
737
|
+
return new ZoomTransform(x3, y3, k);
|
|
738
|
+
}
|
|
739
|
+
function clampK(k) {
|
|
740
|
+
if (!Number.isFinite(k) || k <= 0) return K_MIN;
|
|
741
|
+
return Math.min(K_MAX, Math.max(K_MIN, k));
|
|
742
|
+
}
|
|
743
|
+
function createCameraFlight(inputs) {
|
|
744
|
+
const { from, viewport, apply, onDone, opts } = inputs;
|
|
745
|
+
const ease = resolveEase(opts?.ease ?? "smooth");
|
|
746
|
+
const isProvider = typeof inputs.to === "function";
|
|
747
|
+
const fromView = transformToView(from, viewport);
|
|
748
|
+
function targetView() {
|
|
749
|
+
const to = isProvider ? inputs.to() : inputs.to;
|
|
750
|
+
return transformToView(to, viewport);
|
|
751
|
+
}
|
|
752
|
+
let interp = zoom_default(fromView, targetView());
|
|
753
|
+
const resolvedDuration = resolveDuration(opts?.duration, interp.duration);
|
|
754
|
+
let startTime = null;
|
|
755
|
+
let finished = false;
|
|
756
|
+
function applyAt(t) {
|
|
757
|
+
if (isProvider) interp = zoom_default(fromView, targetView());
|
|
758
|
+
const view = interp(t);
|
|
759
|
+
if (view.some((v) => !Number.isFinite(v))) {
|
|
760
|
+
apply(viewToTransform(targetView(), viewport));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
apply(viewToTransform(view, viewport));
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
tick(now2) {
|
|
767
|
+
if (finished) return false;
|
|
768
|
+
if (startTime === null) startTime = now2;
|
|
769
|
+
const raw = resolvedDuration <= 0 ? 1 : Math.min(1, (now2 - startTime) / resolvedDuration);
|
|
770
|
+
applyAt(ease(raw));
|
|
771
|
+
if (raw >= 1) {
|
|
772
|
+
finished = true;
|
|
773
|
+
onDone?.();
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
return true;
|
|
777
|
+
},
|
|
778
|
+
finish() {
|
|
779
|
+
if (finished) return;
|
|
780
|
+
finished = true;
|
|
781
|
+
applyAt(1);
|
|
782
|
+
onDone?.();
|
|
783
|
+
},
|
|
784
|
+
cancel() {
|
|
785
|
+
finished = true;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function createCameraFollow(inputs) {
|
|
790
|
+
let finished = false;
|
|
791
|
+
return {
|
|
792
|
+
tick() {
|
|
793
|
+
if (finished) return false;
|
|
794
|
+
if (!inputs.isActive()) {
|
|
795
|
+
finished = true;
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
inputs.apply(inputs.target());
|
|
799
|
+
return true;
|
|
800
|
+
},
|
|
801
|
+
finish() {
|
|
802
|
+
finished = true;
|
|
803
|
+
},
|
|
804
|
+
cancel() {
|
|
805
|
+
finished = true;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function resolveDuration(duration, interpDuration) {
|
|
810
|
+
if (typeof duration === "number") return Math.max(1, duration);
|
|
811
|
+
const scaled = interpDuration * AUTO_SCALE;
|
|
812
|
+
return Math.max(AUTO_MIN_MS, Math.min(AUTO_MAX_MS, Math.max(1, scaled)));
|
|
813
|
+
}
|
|
472
814
|
|
|
473
815
|
// src/graph/canvas-renderer.ts
|
|
474
816
|
import { BRAND_FONT_SIZE, BRAND_MIN_WIDTH } from "@opendata-ai/openchart-core";
|
|
817
|
+
|
|
818
|
+
// src/graph/entrance.ts
|
|
819
|
+
function nodeEnterProgress(globalT, index2, total, buckets = 8) {
|
|
820
|
+
const g = globalT <= 0 ? 0 : globalT >= 1 ? 1 : globalT;
|
|
821
|
+
const start = total > 0 ? index2 / total * 0.4 : 0;
|
|
822
|
+
const local = (g - start) / 0.6;
|
|
823
|
+
const clamped = local <= 0 ? 0 : local >= 1 ? 1 : local;
|
|
824
|
+
const b = Math.max(1, Math.floor(buckets));
|
|
825
|
+
return Math.round(clamped * b) / b;
|
|
826
|
+
}
|
|
827
|
+
var ENTRANCE_STAGGER_MAX_NODES = 3e3;
|
|
828
|
+
var ENTRANCE_DRIFT_PX = 16;
|
|
829
|
+
function centroid(nodes) {
|
|
830
|
+
let cx = 0;
|
|
831
|
+
let cy = 0;
|
|
832
|
+
for (const n of nodes) {
|
|
833
|
+
cx += n.x;
|
|
834
|
+
cy += n.y;
|
|
835
|
+
}
|
|
836
|
+
cx /= nodes.length;
|
|
837
|
+
cy /= nodes.length;
|
|
838
|
+
return { cx, cy };
|
|
839
|
+
}
|
|
840
|
+
function hashId(id) {
|
|
841
|
+
let h = 2166136261;
|
|
842
|
+
for (let i = 0; i < id.length; i++) {
|
|
843
|
+
h ^= id.charCodeAt(i);
|
|
844
|
+
h = Math.imul(h, 16777619);
|
|
845
|
+
}
|
|
846
|
+
return h >>> 0;
|
|
847
|
+
}
|
|
848
|
+
function entranceOrder(nodes) {
|
|
849
|
+
const rank = /* @__PURE__ */ new Map();
|
|
850
|
+
if (nodes.length === 0) return rank;
|
|
851
|
+
const sorted = [...nodes].sort((a2, b) => {
|
|
852
|
+
const ha = hashId(a2.id);
|
|
853
|
+
const hb = hashId(b.id);
|
|
854
|
+
return ha - hb || (a2.id < b.id ? -1 : a2.id > b.id ? 1 : 0);
|
|
855
|
+
});
|
|
856
|
+
for (let i = 0; i < sorted.length; i++) rank.set(sorted[i].id, i);
|
|
857
|
+
return rank;
|
|
858
|
+
}
|
|
859
|
+
function entranceOffsets(nodes, dist = ENTRANCE_DRIFT_PX) {
|
|
860
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
861
|
+
if (nodes.length === 0) return offsets;
|
|
862
|
+
const { cx, cy } = centroid(nodes);
|
|
863
|
+
for (const n of nodes) {
|
|
864
|
+
const dx = n.x - cx;
|
|
865
|
+
const dy = n.y - cy;
|
|
866
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
867
|
+
if (len < 1e-6) {
|
|
868
|
+
offsets.set(n.id, { x: 0, y: -dist });
|
|
869
|
+
} else {
|
|
870
|
+
offsets.set(n.id, { x: dx / len * dist, y: dy / len * dist });
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return offsets;
|
|
874
|
+
}
|
|
875
|
+
function popScale(t) {
|
|
876
|
+
if (t <= 0) return 0;
|
|
877
|
+
if (t >= 1) return 1;
|
|
878
|
+
const c1 = 1.70158;
|
|
879
|
+
const c3 = c1 + 1;
|
|
880
|
+
const u = t - 1;
|
|
881
|
+
return 1 + c3 * u * u * u + c1 * u * u;
|
|
882
|
+
}
|
|
883
|
+
function popAlpha(t) {
|
|
884
|
+
const a2 = t / 0.6;
|
|
885
|
+
return a2 >= 1 ? 1 : a2 <= 0 ? 0 : a2;
|
|
886
|
+
}
|
|
887
|
+
function driftFactor(t) {
|
|
888
|
+
const u = 1 - (t <= 0 ? 0 : t >= 1 ? 1 : t);
|
|
889
|
+
return u * u;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/graph/canvas-renderer.ts
|
|
475
893
|
var LABEL_FONT_MIN = 8;
|
|
476
894
|
var LABEL_FONT_MAX = 12;
|
|
477
895
|
var EDGE_ALPHA_DEFAULT = 0.35;
|
|
478
896
|
var EDGE_ALPHA_CONNECTED = 1;
|
|
479
|
-
var EDGE_ALPHA_DIMMED = 0.05;
|
|
480
897
|
var SEARCH_NON_MATCH_ALPHA = 0.15;
|
|
898
|
+
var DEFAULT_DIM_OPACITY = 0.15;
|
|
899
|
+
var CROSSFADE_MAX_EDGES = 2e4;
|
|
900
|
+
function edgeTier(edge, focus) {
|
|
901
|
+
if (!focus.hasActive) return "default";
|
|
902
|
+
return focus.connected.has(edge.source) && focus.connected.has(edge.target) ? "connected" : "dimmed";
|
|
903
|
+
}
|
|
904
|
+
function nodeTier(node, focus) {
|
|
905
|
+
if (!focus.hasActive) return "default";
|
|
906
|
+
return focus.connected.has(node.id) ? "connected" : "dimmed";
|
|
907
|
+
}
|
|
908
|
+
function edgeTierAlpha(tier, dimOpacity) {
|
|
909
|
+
switch (tier) {
|
|
910
|
+
case "connected":
|
|
911
|
+
return EDGE_ALPHA_CONNECTED;
|
|
912
|
+
case "dimmed":
|
|
913
|
+
return dimOpacity / 3;
|
|
914
|
+
default:
|
|
915
|
+
return EDGE_ALPHA_DEFAULT;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function nodeTierAlpha(tier, dimOpacity) {
|
|
919
|
+
switch (tier) {
|
|
920
|
+
case "dimmed":
|
|
921
|
+
return dimOpacity;
|
|
922
|
+
default:
|
|
923
|
+
return 1;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function lerp(a2, b, t) {
|
|
927
|
+
return a2 + (b - a2) * t;
|
|
928
|
+
}
|
|
929
|
+
var ZERO_SHIFT = { x: 0, y: 0 };
|
|
930
|
+
function makeEntranceReveal(entrance, total) {
|
|
931
|
+
const g = entrance.t;
|
|
932
|
+
const { stagger, order, offsets } = entrance;
|
|
933
|
+
const rankOf = (node) => order?.get(node.id) ?? node.index;
|
|
934
|
+
const nodeT = (node) => stagger ? nodeEnterProgress(g, rankOf(node), total) : g;
|
|
935
|
+
const edgeAlpha = Math.max(0, (g - 0.3) / 0.7);
|
|
936
|
+
const globalRamp = 0.6 + 0.4 * g;
|
|
937
|
+
return {
|
|
938
|
+
nodeAlpha: (node) => stagger ? popAlpha(nodeT(node)) : globalRamp,
|
|
939
|
+
nodeScale: (node) => stagger ? popScale(nodeT(node)) : globalRamp,
|
|
940
|
+
shift: (id) => {
|
|
941
|
+
if (!stagger || !offsets || !order) return ZERO_SHIFT;
|
|
942
|
+
const off = offsets.get(id);
|
|
943
|
+
if (!off) return ZERO_SHIFT;
|
|
944
|
+
const f = driftFactor(nodeEnterProgress(g, order.get(id) ?? 0, total));
|
|
945
|
+
return f > 0 ? { x: off.x * f, y: off.y * f } : ZERO_SHIFT;
|
|
946
|
+
},
|
|
947
|
+
edgeAlpha,
|
|
948
|
+
labelAlpha: g
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function deriveFocus(hoveredNodeId, selectedNodeIds, adjacencyMap) {
|
|
952
|
+
const hasActive = hoveredNodeId !== null || selectedNodeIds.size > 0;
|
|
953
|
+
const connected = /* @__PURE__ */ new Set();
|
|
954
|
+
if (hasActive) {
|
|
955
|
+
const active = /* @__PURE__ */ new Set();
|
|
956
|
+
if (hoveredNodeId) active.add(hoveredNodeId);
|
|
957
|
+
for (const id of selectedNodeIds) active.add(id);
|
|
958
|
+
for (const id of active) {
|
|
959
|
+
connected.add(id);
|
|
960
|
+
const neighbors = adjacencyMap.get(id);
|
|
961
|
+
if (neighbors) for (const nid of neighbors) connected.add(nid);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return { hasActive, connected, searchMatches: null, selected: selectedNodeIds };
|
|
965
|
+
}
|
|
481
966
|
var GLOW_NODE_THRESHOLD = 2e3;
|
|
482
967
|
var GLOW_RADIUS_MULTIPLIER = 1.3;
|
|
483
968
|
var GLOW_ALPHA = 0.15;
|
|
@@ -544,18 +1029,12 @@ var GraphCanvasRenderer = class {
|
|
|
544
1029
|
searchMatches,
|
|
545
1030
|
isGesturing
|
|
546
1031
|
} = state;
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
connectedNodeIds.add(id);
|
|
554
|
-
const neighbors = adjacencyMap.get(id);
|
|
555
|
-
if (neighbors) {
|
|
556
|
-
for (const nid of neighbors) connectedNodeIds.add(nid);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
1032
|
+
const dimOpacity = state.dimOpacity ?? DEFAULT_DIM_OPACITY;
|
|
1033
|
+
const nextFocus = state.focus?.next ?? deriveFocus(hoveredNodeId, selectedNodeIds, adjacencyMap);
|
|
1034
|
+
const crossfade = state.focus && state.focus.t < 1 && !isGesturing ? state.focus : null;
|
|
1035
|
+
const entrance = state.entrance && state.entrance.t < 1 ? makeEntranceReveal(state.entrance, nodes.length) : null;
|
|
1036
|
+
const enterAlpha = state.enterAlpha ?? null;
|
|
1037
|
+
const enterAlphaFor = (id) => enterAlpha?.get(id) ?? 1;
|
|
559
1038
|
const rect = visibleRect(cssWidth, cssHeight, transform);
|
|
560
1039
|
const visibleNodes = nodes.filter((n) => nodeInView(n, rect));
|
|
561
1040
|
const visibleEdges = edges.filter((e) => edgeInView(e, rect));
|
|
@@ -572,14 +1051,34 @@ var GraphCanvasRenderer = class {
|
|
|
572
1051
|
}
|
|
573
1052
|
ctx.translate(transform.x, transform.y);
|
|
574
1053
|
ctx.scale(transform.k, transform.k);
|
|
575
|
-
|
|
576
|
-
ctx,
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1054
|
+
if (state.exiting && state.exiting.alpha > 0) {
|
|
1055
|
+
this.drawGhosts(ctx, state.exiting, rect);
|
|
1056
|
+
}
|
|
1057
|
+
if (crossfade && visibleEdges.length <= CROSSFADE_MAX_EDGES) {
|
|
1058
|
+
this.drawEdgesCrossfade(
|
|
1059
|
+
ctx,
|
|
1060
|
+
visibleEdges,
|
|
1061
|
+
crossfade.prev,
|
|
1062
|
+
crossfade.next,
|
|
1063
|
+
crossfade.t,
|
|
1064
|
+
dimOpacity,
|
|
1065
|
+
isGesturing ? null : searchMatches,
|
|
1066
|
+
hoveredEdgeId,
|
|
1067
|
+
entrance,
|
|
1068
|
+
enterAlphaFor
|
|
1069
|
+
);
|
|
1070
|
+
} else {
|
|
1071
|
+
this.drawEdgesBatched(
|
|
1072
|
+
ctx,
|
|
1073
|
+
visibleEdges,
|
|
1074
|
+
nextFocus,
|
|
1075
|
+
dimOpacity,
|
|
1076
|
+
isGesturing ? null : searchMatches,
|
|
1077
|
+
hoveredEdgeId,
|
|
1078
|
+
entrance,
|
|
1079
|
+
enterAlphaFor
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
583
1082
|
this.drawNodesBatched(
|
|
584
1083
|
ctx,
|
|
585
1084
|
visibleNodes,
|
|
@@ -588,7 +1087,13 @@ var GraphCanvasRenderer = class {
|
|
|
588
1087
|
isGesturing ? null : searchMatches,
|
|
589
1088
|
showGlow,
|
|
590
1089
|
theme,
|
|
591
|
-
minRadius
|
|
1090
|
+
minRadius,
|
|
1091
|
+
nextFocus,
|
|
1092
|
+
dimOpacity,
|
|
1093
|
+
crossfade,
|
|
1094
|
+
state.hoverRadiusScale,
|
|
1095
|
+
entrance,
|
|
1096
|
+
enterAlphaFor
|
|
592
1097
|
);
|
|
593
1098
|
if (!isGesturing) {
|
|
594
1099
|
this.drawLabels(
|
|
@@ -599,7 +1104,9 @@ var GraphCanvasRenderer = class {
|
|
|
599
1104
|
selectedNodeIds,
|
|
600
1105
|
searchMatches,
|
|
601
1106
|
transform.k,
|
|
602
|
-
theme
|
|
1107
|
+
theme,
|
|
1108
|
+
entrance,
|
|
1109
|
+
enterAlphaFor
|
|
603
1110
|
);
|
|
604
1111
|
}
|
|
605
1112
|
ctx.restore();
|
|
@@ -629,10 +1136,12 @@ var GraphCanvasRenderer = class {
|
|
|
629
1136
|
// -------------------------------------------------------------------------
|
|
630
1137
|
// Batched edge drawing
|
|
631
1138
|
// -------------------------------------------------------------------------
|
|
632
|
-
drawEdgesBatched(ctx, edges,
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
1139
|
+
drawEdgesBatched(ctx, edges, focus, dimOpacity, searchMatches, hoveredEdgeId, entrance, enterAlphaFor) {
|
|
1140
|
+
const buckets = {
|
|
1141
|
+
dimmed: [],
|
|
1142
|
+
default: [],
|
|
1143
|
+
connected: []
|
|
1144
|
+
};
|
|
636
1145
|
let hoveredEdge = null;
|
|
637
1146
|
for (const edge of edges) {
|
|
638
1147
|
const edgeId = `${edge.source}->${edge.target}`;
|
|
@@ -640,58 +1149,109 @@ var GraphCanvasRenderer = class {
|
|
|
640
1149
|
hoveredEdge = edge;
|
|
641
1150
|
continue;
|
|
642
1151
|
}
|
|
643
|
-
|
|
644
|
-
const isDimmed = hasActiveNode && !isConnected;
|
|
645
|
-
if (isConnected) {
|
|
646
|
-
connectedEdges.push(edge);
|
|
647
|
-
} else if (isDimmed) {
|
|
648
|
-
dimmedEdges.push(edge);
|
|
649
|
-
} else {
|
|
650
|
-
defaultEdges.push(edge);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
this.drawEdgeGroupBatched(ctx, dimmedEdges, EDGE_ALPHA_DIMMED, searchMatches);
|
|
654
|
-
this.drawEdgeGroupBatched(ctx, defaultEdges, EDGE_ALPHA_DEFAULT, searchMatches);
|
|
655
|
-
this.drawEdgeGroupBatched(ctx, connectedEdges, EDGE_ALPHA_CONNECTED, searchMatches);
|
|
656
|
-
if (hoveredEdge) {
|
|
657
|
-
const dash = DASH_PATTERNS[hoveredEdge.style] ?? DASH_PATTERNS.solid;
|
|
658
|
-
ctx.setLineDash(dash);
|
|
659
|
-
ctx.strokeStyle = hoveredEdge.stroke;
|
|
660
|
-
ctx.lineWidth = hoveredEdge.strokeWidth * 2;
|
|
661
|
-
ctx.globalAlpha = EDGE_ALPHA_CONNECTED;
|
|
662
|
-
ctx.beginPath();
|
|
663
|
-
ctx.moveTo(hoveredEdge.sourceX, hoveredEdge.sourceY);
|
|
664
|
-
ctx.lineTo(hoveredEdge.targetX, hoveredEdge.targetY);
|
|
665
|
-
ctx.stroke();
|
|
666
|
-
ctx.setLineDash([]);
|
|
667
|
-
ctx.globalAlpha = 1;
|
|
1152
|
+
buckets[edgeTier(edge, focus)].push(edge);
|
|
668
1153
|
}
|
|
1154
|
+
const ea = entrance ? entrance.edgeAlpha : 1;
|
|
1155
|
+
this.drawEdgeGroupBatched(
|
|
1156
|
+
ctx,
|
|
1157
|
+
buckets.dimmed,
|
|
1158
|
+
edgeTierAlpha("dimmed", dimOpacity) * ea,
|
|
1159
|
+
searchMatches,
|
|
1160
|
+
enterAlphaFor
|
|
1161
|
+
);
|
|
1162
|
+
this.drawEdgeGroupBatched(
|
|
1163
|
+
ctx,
|
|
1164
|
+
buckets.default,
|
|
1165
|
+
EDGE_ALPHA_DEFAULT * ea,
|
|
1166
|
+
searchMatches,
|
|
1167
|
+
enterAlphaFor
|
|
1168
|
+
);
|
|
1169
|
+
this.drawEdgeGroupBatched(
|
|
1170
|
+
ctx,
|
|
1171
|
+
buckets.connected,
|
|
1172
|
+
EDGE_ALPHA_CONNECTED * ea,
|
|
1173
|
+
searchMatches,
|
|
1174
|
+
enterAlphaFor
|
|
1175
|
+
);
|
|
1176
|
+
this.drawHoveredEdge(ctx, hoveredEdge);
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Crossfade edges between a prev and next focus state. Each edge is classified
|
|
1180
|
+
* under BOTH snapshots → at most 9 (prevTier × nextTier) buckets, each drawn
|
|
1181
|
+
* batched at `lerp(edgeTierAlpha[prev], edgeTierAlpha[next], t)`. Preserves the
|
|
1182
|
+
* per-group style batching within each bucket.
|
|
1183
|
+
*/
|
|
1184
|
+
drawEdgesCrossfade(ctx, edges, prev, next, t, dimOpacity, searchMatches, hoveredEdgeId, entrance, enterAlphaFor) {
|
|
1185
|
+
const ea = entrance ? entrance.edgeAlpha : 1;
|
|
1186
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1187
|
+
let hoveredEdge = null;
|
|
1188
|
+
for (const edge of edges) {
|
|
1189
|
+
const edgeId = `${edge.source}->${edge.target}`;
|
|
1190
|
+
if (edgeId === hoveredEdgeId) {
|
|
1191
|
+
hoveredEdge = edge;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
const key = `${edgeTier(edge, prev)}|${edgeTier(edge, next)}`;
|
|
1195
|
+
let bucket = buckets.get(key);
|
|
1196
|
+
if (!bucket) {
|
|
1197
|
+
bucket = [];
|
|
1198
|
+
buckets.set(key, bucket);
|
|
1199
|
+
}
|
|
1200
|
+
bucket.push(edge);
|
|
1201
|
+
}
|
|
1202
|
+
const ordered = [...buckets.entries()].map(([key, bucket]) => {
|
|
1203
|
+
const [prevTier, nextTier] = key.split("|");
|
|
1204
|
+
const alpha = lerp(edgeTierAlpha(prevTier, dimOpacity), edgeTierAlpha(nextTier, dimOpacity), t) * ea;
|
|
1205
|
+
return { alpha, bucket };
|
|
1206
|
+
}).sort((a2, b) => a2.alpha - b.alpha);
|
|
1207
|
+
for (const { alpha, bucket } of ordered) {
|
|
1208
|
+
this.drawEdgeGroupBatched(ctx, bucket, alpha, searchMatches, enterAlphaFor);
|
|
1209
|
+
}
|
|
1210
|
+
this.drawHoveredEdge(ctx, hoveredEdge);
|
|
1211
|
+
}
|
|
1212
|
+
/** Draw the hovered edge on top with a thickened highlight stroke. */
|
|
1213
|
+
drawHoveredEdge(ctx, hoveredEdge) {
|
|
1214
|
+
if (!hoveredEdge) return;
|
|
1215
|
+
const dash = DASH_PATTERNS[hoveredEdge.style] ?? DASH_PATTERNS.solid;
|
|
1216
|
+
ctx.setLineDash(dash);
|
|
1217
|
+
ctx.strokeStyle = hoveredEdge.stroke;
|
|
1218
|
+
ctx.lineWidth = hoveredEdge.strokeWidth * 2;
|
|
1219
|
+
ctx.globalAlpha = EDGE_ALPHA_CONNECTED;
|
|
1220
|
+
ctx.beginPath();
|
|
1221
|
+
ctx.moveTo(hoveredEdge.sourceX, hoveredEdge.sourceY);
|
|
1222
|
+
ctx.lineTo(hoveredEdge.targetX, hoveredEdge.targetY);
|
|
1223
|
+
ctx.stroke();
|
|
1224
|
+
ctx.setLineDash([]);
|
|
1225
|
+
ctx.globalAlpha = 1;
|
|
669
1226
|
}
|
|
670
1227
|
/**
|
|
671
1228
|
* Draw a group of edges at a given alpha, batched by (stroke, strokeWidth, style).
|
|
672
1229
|
* When search is inactive, all edges of the same style are drawn in a single path.
|
|
673
1230
|
* When search is active, edges split by search-match status for alpha dimming.
|
|
674
1231
|
*/
|
|
675
|
-
drawEdgeGroupBatched(ctx, edges, alpha, searchMatches) {
|
|
1232
|
+
drawEdgeGroupBatched(ctx, edges, alpha, searchMatches, enterAlphaFor) {
|
|
676
1233
|
if (edges.length === 0) return;
|
|
677
1234
|
const groups = /* @__PURE__ */ new Map();
|
|
678
1235
|
for (const edge of edges) {
|
|
679
|
-
const
|
|
1236
|
+
const rawEnter = Math.min(enterAlphaFor(edge.source), enterAlphaFor(edge.target));
|
|
1237
|
+
const enter = Math.round(rawEnter * 8) / 8;
|
|
1238
|
+
const key = `${edge.stroke}|${edge.strokeWidth}|${edge.style}|${enter}`;
|
|
680
1239
|
let group = groups.get(key);
|
|
681
1240
|
if (!group) {
|
|
682
|
-
group = [];
|
|
1241
|
+
group = { edges: [], enter };
|
|
683
1242
|
groups.set(key, group);
|
|
684
1243
|
}
|
|
685
|
-
group.push(edge);
|
|
1244
|
+
group.edges.push(edge);
|
|
686
1245
|
}
|
|
687
|
-
for (const [, group] of groups) {
|
|
1246
|
+
for (const [, { edges: group, enter }] of groups) {
|
|
688
1247
|
const sample = group[0];
|
|
689
1248
|
const dash = DASH_PATTERNS[sample.style] ?? DASH_PATTERNS.solid;
|
|
690
1249
|
ctx.setLineDash(dash);
|
|
691
1250
|
ctx.strokeStyle = sample.stroke;
|
|
692
1251
|
ctx.lineWidth = sample.strokeWidth;
|
|
1252
|
+
const groupAlpha = alpha * enter;
|
|
693
1253
|
if (!searchMatches) {
|
|
694
|
-
ctx.globalAlpha =
|
|
1254
|
+
ctx.globalAlpha = groupAlpha;
|
|
695
1255
|
ctx.beginPath();
|
|
696
1256
|
for (const edge of group) {
|
|
697
1257
|
ctx.moveTo(edge.sourceX, edge.sourceY);
|
|
@@ -699,7 +1259,7 @@ var GraphCanvasRenderer = class {
|
|
|
699
1259
|
}
|
|
700
1260
|
ctx.stroke();
|
|
701
1261
|
} else {
|
|
702
|
-
ctx.globalAlpha =
|
|
1262
|
+
ctx.globalAlpha = groupAlpha;
|
|
703
1263
|
ctx.beginPath();
|
|
704
1264
|
let hasMatched = false;
|
|
705
1265
|
const nonMatchPath = [];
|
|
@@ -716,7 +1276,7 @@ var GraphCanvasRenderer = class {
|
|
|
716
1276
|
}
|
|
717
1277
|
if (hasMatched) ctx.stroke();
|
|
718
1278
|
if (nonMatchPath.length > 0) {
|
|
719
|
-
ctx.globalAlpha = SEARCH_NON_MATCH_ALPHA *
|
|
1279
|
+
ctx.globalAlpha = SEARCH_NON_MATCH_ALPHA * groupAlpha;
|
|
720
1280
|
ctx.beginPath();
|
|
721
1281
|
for (const edge of nonMatchPath) {
|
|
722
1282
|
ctx.moveTo(edge.sourceX, edge.sourceY);
|
|
@@ -732,147 +1292,114 @@ var GraphCanvasRenderer = class {
|
|
|
732
1292
|
// -------------------------------------------------------------------------
|
|
733
1293
|
// Batched node drawing
|
|
734
1294
|
// -------------------------------------------------------------------------
|
|
735
|
-
drawNodesBatched(ctx, nodes, hoveredNodeId, selectedNodeIds, searchMatches, showGlow, theme, minRadius) {
|
|
1295
|
+
drawNodesBatched(ctx, nodes, hoveredNodeId, selectedNodeIds, searchMatches, showGlow, theme, minRadius, nextFocus, dimOpacity, crossfade, hoverRadiusScale, entrance, enterAlphaFor) {
|
|
1296
|
+
const focusAlpha = (node) => {
|
|
1297
|
+
if (crossfade) {
|
|
1298
|
+
return lerp(
|
|
1299
|
+
nodeTierAlpha(nodeTier(node, crossfade.prev), dimOpacity),
|
|
1300
|
+
nodeTierAlpha(nodeTier(node, crossfade.next), dimOpacity),
|
|
1301
|
+
crossfade.t
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
return nodeTierAlpha(nodeTier(node, nextFocus), dimOpacity);
|
|
1305
|
+
};
|
|
1306
|
+
const searchAlpha = (node) => searchMatches !== null && !searchMatches.has(node.id) ? SEARCH_NON_MATCH_ALPHA : 1;
|
|
1307
|
+
const entranceAlpha = (node) => entrance ? entrance.nodeAlpha(node) : 1;
|
|
1308
|
+
const entranceScale = (node) => entrance ? entrance.nodeScale(node) : 1;
|
|
1309
|
+
const entranceShift = (node) => entrance ? entrance.shift(node.id) : ZERO_SHIFT;
|
|
1310
|
+
const effectiveAlpha = (node) => focusAlpha(node) * searchAlpha(node) * entranceAlpha(node) * enterAlphaFor(node.id);
|
|
736
1311
|
const bulkNodes = [];
|
|
737
1312
|
const specialNodes = [];
|
|
738
1313
|
for (const node of nodes) {
|
|
739
|
-
if (node.id === hoveredNodeId || selectedNodeIds.has(node.id)) {
|
|
1314
|
+
if (node.id === hoveredNodeId || selectedNodeIds.has(node.id) || (hoverRadiusScale?.has(node.id) ?? false)) {
|
|
740
1315
|
specialNodes.push(node);
|
|
741
1316
|
} else {
|
|
742
1317
|
bulkNodes.push(node);
|
|
743
1318
|
}
|
|
744
1319
|
}
|
|
745
|
-
const r = (node) => Math.max(node.radius, minRadius);
|
|
746
|
-
if (showGlow) {
|
|
1320
|
+
const r = (node) => Math.max(node.radius, minRadius) * entranceScale(node);
|
|
1321
|
+
if (showGlow && !entrance) {
|
|
747
1322
|
this.drawGlowBatched(ctx, bulkNodes, searchMatches, minRadius);
|
|
748
1323
|
}
|
|
749
1324
|
const fillGroups = /* @__PURE__ */ new Map();
|
|
750
1325
|
for (const node of bulkNodes) {
|
|
751
|
-
|
|
1326
|
+
const alpha = effectiveAlpha(node);
|
|
1327
|
+
const key = `${node.fill}|${alpha.toFixed(3)}`;
|
|
1328
|
+
let group = fillGroups.get(key);
|
|
752
1329
|
if (!group) {
|
|
753
|
-
group = [];
|
|
754
|
-
fillGroups.set(
|
|
1330
|
+
group = { fill: node.fill, alpha, nodes: [] };
|
|
1331
|
+
fillGroups.set(key, group);
|
|
755
1332
|
}
|
|
756
|
-
group.push(node);
|
|
1333
|
+
group.nodes.push(node);
|
|
757
1334
|
}
|
|
758
|
-
|
|
759
|
-
ctx.
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
ctx.fill();
|
|
769
|
-
}
|
|
770
|
-
} else {
|
|
771
|
-
for (const [fill, group] of fillGroups) {
|
|
772
|
-
ctx.fillStyle = fill;
|
|
773
|
-
ctx.globalAlpha = 1;
|
|
774
|
-
ctx.beginPath();
|
|
775
|
-
let hasMatched = false;
|
|
776
|
-
const dimmedNodes = [];
|
|
777
|
-
for (const node of group) {
|
|
778
|
-
if (searchMatches.has(node.id)) {
|
|
779
|
-
const nr = r(node);
|
|
780
|
-
ctx.moveTo(node.x + nr, node.y);
|
|
781
|
-
ctx.arc(node.x, node.y, nr, 0, TWO_PI);
|
|
782
|
-
hasMatched = true;
|
|
783
|
-
} else {
|
|
784
|
-
dimmedNodes.push(node);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
if (hasMatched) ctx.fill();
|
|
788
|
-
if (dimmedNodes.length > 0) {
|
|
789
|
-
ctx.globalAlpha = SEARCH_NON_MATCH_ALPHA;
|
|
790
|
-
ctx.beginPath();
|
|
791
|
-
for (const node of dimmedNodes) {
|
|
792
|
-
const nr = r(node);
|
|
793
|
-
ctx.moveTo(node.x + nr, node.y);
|
|
794
|
-
ctx.arc(node.x, node.y, nr, 0, TWO_PI);
|
|
795
|
-
}
|
|
796
|
-
ctx.fill();
|
|
797
|
-
}
|
|
1335
|
+
for (const { fill, alpha, nodes: group } of fillGroups.values()) {
|
|
1336
|
+
ctx.fillStyle = fill;
|
|
1337
|
+
ctx.globalAlpha = alpha;
|
|
1338
|
+
ctx.beginPath();
|
|
1339
|
+
for (const node of group) {
|
|
1340
|
+
const nr = r(node);
|
|
1341
|
+
if (nr <= 0) continue;
|
|
1342
|
+
const s = entranceShift(node);
|
|
1343
|
+
ctx.moveTo(node.x + s.x + nr, node.y + s.y);
|
|
1344
|
+
ctx.arc(node.x + s.x, node.y + s.y, nr, 0, TWO_PI);
|
|
798
1345
|
}
|
|
1346
|
+
ctx.fill();
|
|
799
1347
|
}
|
|
800
1348
|
const strokeGroups = /* @__PURE__ */ new Map();
|
|
801
1349
|
for (const node of bulkNodes) {
|
|
802
|
-
const
|
|
1350
|
+
const alpha = effectiveAlpha(node);
|
|
1351
|
+
const key = `${node.stroke}|${node.strokeWidth}|${alpha.toFixed(3)}`;
|
|
803
1352
|
let group = strokeGroups.get(key);
|
|
804
1353
|
if (!group) {
|
|
805
|
-
group = [];
|
|
1354
|
+
group = { stroke: node.stroke, width: node.strokeWidth, alpha, nodes: [] };
|
|
806
1355
|
strokeGroups.set(key, group);
|
|
807
1356
|
}
|
|
808
|
-
group.push(node);
|
|
1357
|
+
group.nodes.push(node);
|
|
809
1358
|
}
|
|
810
|
-
for (const
|
|
811
|
-
const [stroke, widthStr] = key.split("|");
|
|
1359
|
+
for (const { stroke, width, alpha, nodes: group } of strokeGroups.values()) {
|
|
812
1360
|
ctx.strokeStyle = stroke;
|
|
813
|
-
ctx.lineWidth =
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
ctx.stroke();
|
|
823
|
-
} else {
|
|
824
|
-
ctx.globalAlpha = 1;
|
|
825
|
-
ctx.beginPath();
|
|
826
|
-
let hasMatched = false;
|
|
827
|
-
const dimmedNodes = [];
|
|
828
|
-
for (const node of group) {
|
|
829
|
-
if (searchMatches.has(node.id)) {
|
|
830
|
-
const nr = r(node);
|
|
831
|
-
ctx.moveTo(node.x + nr, node.y);
|
|
832
|
-
ctx.arc(node.x, node.y, nr, 0, TWO_PI);
|
|
833
|
-
hasMatched = true;
|
|
834
|
-
} else {
|
|
835
|
-
dimmedNodes.push(node);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
if (hasMatched) ctx.stroke();
|
|
839
|
-
if (dimmedNodes.length > 0) {
|
|
840
|
-
ctx.globalAlpha = SEARCH_NON_MATCH_ALPHA;
|
|
841
|
-
ctx.beginPath();
|
|
842
|
-
for (const node of dimmedNodes) {
|
|
843
|
-
const nr = r(node);
|
|
844
|
-
ctx.moveTo(node.x + nr, node.y);
|
|
845
|
-
ctx.arc(node.x, node.y, nr, 0, TWO_PI);
|
|
846
|
-
}
|
|
847
|
-
ctx.stroke();
|
|
848
|
-
}
|
|
1361
|
+
ctx.lineWidth = width;
|
|
1362
|
+
ctx.globalAlpha = alpha;
|
|
1363
|
+
ctx.beginPath();
|
|
1364
|
+
for (const node of group) {
|
|
1365
|
+
const nr = r(node);
|
|
1366
|
+
if (nr <= 0) continue;
|
|
1367
|
+
const s = entranceShift(node);
|
|
1368
|
+
ctx.moveTo(node.x + s.x + nr, node.y + s.y);
|
|
1369
|
+
ctx.arc(node.x + s.x, node.y + s.y, nr, 0, TWO_PI);
|
|
849
1370
|
}
|
|
1371
|
+
ctx.stroke();
|
|
850
1372
|
}
|
|
851
1373
|
for (const node of specialNodes) {
|
|
852
1374
|
const isHovered = node.id === hoveredNodeId;
|
|
853
1375
|
const isSelected = selectedNodeIds.has(node.id);
|
|
854
1376
|
const dimmed = searchMatches !== null && !searchMatches.has(node.id);
|
|
855
1377
|
const baseRadius = Math.max(node.radius, minRadius);
|
|
856
|
-
const
|
|
1378
|
+
const hoverScale = hoverRadiusScale?.get(node.id) ?? (isHovered ? 1.15 : 1);
|
|
1379
|
+
const radius = baseRadius * hoverScale;
|
|
1380
|
+
const brightened = isHovered && hoverScale >= 1.075;
|
|
1381
|
+
const s = entranceShift(node);
|
|
1382
|
+
const nx = node.x + s.x;
|
|
1383
|
+
const ny = node.y + s.y;
|
|
857
1384
|
ctx.globalAlpha = dimmed ? SEARCH_NON_MATCH_ALPHA : 1;
|
|
858
1385
|
if (showGlow && !dimmed) {
|
|
859
1386
|
ctx.beginPath();
|
|
860
|
-
ctx.arc(
|
|
1387
|
+
ctx.arc(nx, ny, radius * GLOW_RADIUS_MULTIPLIER, 0, TWO_PI);
|
|
861
1388
|
ctx.fillStyle = node.fill;
|
|
862
1389
|
ctx.globalAlpha = GLOW_ALPHA;
|
|
863
1390
|
ctx.fill();
|
|
864
1391
|
ctx.globalAlpha = dimmed ? SEARCH_NON_MATCH_ALPHA : 1;
|
|
865
1392
|
}
|
|
866
1393
|
ctx.beginPath();
|
|
867
|
-
ctx.arc(
|
|
868
|
-
ctx.fillStyle =
|
|
1394
|
+
ctx.arc(nx, ny, radius, 0, TWO_PI);
|
|
1395
|
+
ctx.fillStyle = brightened ? brighten(node.fill) : node.fill;
|
|
869
1396
|
ctx.fill();
|
|
870
1397
|
ctx.strokeStyle = node.stroke;
|
|
871
1398
|
ctx.lineWidth = node.strokeWidth;
|
|
872
1399
|
ctx.stroke();
|
|
873
1400
|
if (isSelected) {
|
|
874
1401
|
ctx.beginPath();
|
|
875
|
-
ctx.arc(
|
|
1402
|
+
ctx.arc(nx, ny, radius + 3, 0, TWO_PI);
|
|
876
1403
|
ctx.strokeStyle = theme.colors.categorical[0] ?? "#3b82f6";
|
|
877
1404
|
ctx.lineWidth = 2;
|
|
878
1405
|
ctx.stroke();
|
|
@@ -908,7 +1435,8 @@ var GraphCanvasRenderer = class {
|
|
|
908
1435
|
// -------------------------------------------------------------------------
|
|
909
1436
|
// Labels (drawn individually, skipped during gestures)
|
|
910
1437
|
// -------------------------------------------------------------------------
|
|
911
|
-
drawLabels(ctx, nodes, threshold, hoveredNodeId, selectedNodeIds, searchMatches, zoom, theme) {
|
|
1438
|
+
drawLabels(ctx, nodes, threshold, hoveredNodeId, selectedNodeIds, searchMatches, zoom, theme, entrance, enterAlphaFor) {
|
|
1439
|
+
const la = entrance ? entrance.labelAlpha : 1;
|
|
912
1440
|
const rawSize = 10 / zoom;
|
|
913
1441
|
const fontSize = Math.max(LABEL_FONT_MIN, Math.min(LABEL_FONT_MAX, rawSize));
|
|
914
1442
|
ctx.font = `${fontSize}px ${theme.fonts.family}`;
|
|
@@ -921,12 +1449,12 @@ var GraphCanvasRenderer = class {
|
|
|
921
1449
|
const forced = isHovered || isSelected;
|
|
922
1450
|
const dimmed = searchMatches !== null && !searchMatches.has(node.id);
|
|
923
1451
|
if (!forced && node.labelPriority < threshold) continue;
|
|
924
|
-
ctx.globalAlpha = dimmed ? SEARCH_NON_MATCH_ALPHA : 1;
|
|
1452
|
+
ctx.globalAlpha = (dimmed ? SEARCH_NON_MATCH_ALPHA : 1) * la * enterAlphaFor(node.id);
|
|
925
1453
|
const labelY = node.y + node.radius + 3;
|
|
926
1454
|
if (theme.colors.background !== "transparent") {
|
|
927
1455
|
ctx.strokeStyle = theme.colors.background;
|
|
928
1456
|
} else {
|
|
929
|
-
ctx.strokeStyle =
|
|
1457
|
+
ctx.strokeStyle = theme.isDark ? "rgba(0, 0, 0, 0.7)" : "rgba(255, 255, 255, 0.85)";
|
|
930
1458
|
}
|
|
931
1459
|
ctx.lineWidth = 3;
|
|
932
1460
|
ctx.lineJoin = "round";
|
|
@@ -937,6 +1465,59 @@ var GraphCanvasRenderer = class {
|
|
|
937
1465
|
}
|
|
938
1466
|
ctx.globalAlpha = 1;
|
|
939
1467
|
}
|
|
1468
|
+
// -------------------------------------------------------------------------
|
|
1469
|
+
// Exit ghosts (Phase 7)
|
|
1470
|
+
// -------------------------------------------------------------------------
|
|
1471
|
+
/**
|
|
1472
|
+
* Draw exit ghosts (removed nodes/edges) UNDER the live marks at a global fade
|
|
1473
|
+
* alpha. Neutral rendering: no focus dim, no search dim, no selection ring —
|
|
1474
|
+
* they're on their way out. Batched by (stroke/style) for edges and (fill) for
|
|
1475
|
+
* nodes, culled to the visible rect.
|
|
1476
|
+
*/
|
|
1477
|
+
drawGhosts(ctx, exiting, rect) {
|
|
1478
|
+
const alpha = exiting.alpha;
|
|
1479
|
+
const edgeGroups = /* @__PURE__ */ new Map();
|
|
1480
|
+
for (const edge of exiting.edges) {
|
|
1481
|
+
if (!edgeInView(edge, rect)) continue;
|
|
1482
|
+
const key = `${edge.stroke}|${edge.strokeWidth}|${edge.style}`;
|
|
1483
|
+
const group = edgeGroups.get(key);
|
|
1484
|
+
if (group) group.push(edge);
|
|
1485
|
+
else edgeGroups.set(key, [edge]);
|
|
1486
|
+
}
|
|
1487
|
+
for (const [, group] of edgeGroups) {
|
|
1488
|
+
const sample = group[0];
|
|
1489
|
+
const dash = DASH_PATTERNS[sample.style] ?? DASH_PATTERNS.solid;
|
|
1490
|
+
ctx.setLineDash(dash);
|
|
1491
|
+
ctx.strokeStyle = sample.stroke;
|
|
1492
|
+
ctx.lineWidth = sample.strokeWidth;
|
|
1493
|
+
ctx.globalAlpha = EDGE_ALPHA_DEFAULT * alpha;
|
|
1494
|
+
ctx.beginPath();
|
|
1495
|
+
for (const edge of group) {
|
|
1496
|
+
ctx.moveTo(edge.sourceX, edge.sourceY);
|
|
1497
|
+
ctx.lineTo(edge.targetX, edge.targetY);
|
|
1498
|
+
}
|
|
1499
|
+
ctx.stroke();
|
|
1500
|
+
}
|
|
1501
|
+
ctx.setLineDash([]);
|
|
1502
|
+
const nodeGroups = /* @__PURE__ */ new Map();
|
|
1503
|
+
for (const node of exiting.nodes) {
|
|
1504
|
+
if (!nodeInView(node, rect)) continue;
|
|
1505
|
+
const group = nodeGroups.get(node.fill);
|
|
1506
|
+
if (group) group.push(node);
|
|
1507
|
+
else nodeGroups.set(node.fill, [node]);
|
|
1508
|
+
}
|
|
1509
|
+
for (const [fill, group] of nodeGroups) {
|
|
1510
|
+
ctx.fillStyle = fill;
|
|
1511
|
+
ctx.globalAlpha = alpha;
|
|
1512
|
+
ctx.beginPath();
|
|
1513
|
+
for (const node of group) {
|
|
1514
|
+
ctx.moveTo(node.x + node.radius, node.y);
|
|
1515
|
+
ctx.arc(node.x, node.y, node.radius, 0, TWO_PI);
|
|
1516
|
+
}
|
|
1517
|
+
ctx.fill();
|
|
1518
|
+
}
|
|
1519
|
+
ctx.globalAlpha = 1;
|
|
1520
|
+
}
|
|
940
1521
|
};
|
|
941
1522
|
function brighten(color) {
|
|
942
1523
|
const rgbMatch = color.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
@@ -956,110 +1537,102 @@ function brighten(color) {
|
|
|
956
1537
|
}
|
|
957
1538
|
return color;
|
|
958
1539
|
}
|
|
959
|
-
function isLightColor(color) {
|
|
960
|
-
const hex = color.replace("#", "");
|
|
961
|
-
const full = hex.length === 3 ? hex.split("").map((c2) => c2 + c2).join("") : hex;
|
|
962
|
-
if (full.length !== 6) return false;
|
|
963
|
-
const r = parseInt(full.slice(0, 2), 16) / 255;
|
|
964
|
-
const g = parseInt(full.slice(2, 4), 16) / 255;
|
|
965
|
-
const b = parseInt(full.slice(4, 6), 16) / 255;
|
|
966
|
-
const toLinear = (c2) => c2 <= 0.03928 ? c2 / 12.92 : ((c2 + 0.055) / 1.055) ** 2.4;
|
|
967
|
-
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b) > 0.5;
|
|
968
|
-
}
|
|
969
1540
|
|
|
970
|
-
// src/graph/
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
zoomAt(targetK, pivotX, pivotY) {
|
|
996
|
-
const gx = (pivotX - this.x) / this.k;
|
|
997
|
-
const gy = (pivotY - this.y) / this.k;
|
|
998
|
-
return new _ZoomTransform(pivotX - gx * targetK, pivotY - gy * targetK, targetK);
|
|
999
|
-
}
|
|
1000
|
-
/** Pan by a screen-space delta. */
|
|
1001
|
-
pan(dx, dy) {
|
|
1002
|
-
return new _ZoomTransform(this.x + dx, this.y + dy, this.k);
|
|
1541
|
+
// src/graph/focus-transition.ts
|
|
1542
|
+
function focusSnapshotsEqual(a2, b) {
|
|
1543
|
+
return a2.hasActive === b.hasActive && setsEqual(a2.connected, b.connected) && nullableSetsEqual(a2.searchMatches, b.searchMatches) && setsEqual(a2.selected, b.selected);
|
|
1544
|
+
}
|
|
1545
|
+
function setsEqual(a2, b) {
|
|
1546
|
+
if (a2.size !== b.size) return false;
|
|
1547
|
+
for (const v of a2) if (!b.has(v)) return false;
|
|
1548
|
+
return true;
|
|
1549
|
+
}
|
|
1550
|
+
function nullableSetsEqual(a2, b) {
|
|
1551
|
+
if (a2 === null || b === null) return a2 === b;
|
|
1552
|
+
return setsEqual(a2, b);
|
|
1553
|
+
}
|
|
1554
|
+
var FocusTransition = class {
|
|
1555
|
+
prev;
|
|
1556
|
+
next;
|
|
1557
|
+
startTime;
|
|
1558
|
+
duration;
|
|
1559
|
+
ease;
|
|
1560
|
+
constructor(initial, duration, ease, now2) {
|
|
1561
|
+
this.prev = initial;
|
|
1562
|
+
this.next = initial;
|
|
1563
|
+
this.startTime = now2;
|
|
1564
|
+
this.duration = Math.max(0, duration);
|
|
1565
|
+
this.ease = ease;
|
|
1003
1566
|
}
|
|
1004
1567
|
/**
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
* Returns the transform and the ideal content height (in screen pixels)
|
|
1009
|
-
* so callers can shrink the canvas to eliminate dead space.
|
|
1568
|
+
* Point a fresh transition at `target`, capturing the endpoint closest to the
|
|
1569
|
+
* current display as the new `prev` (the `p < 0.5` rule). A no-op when
|
|
1570
|
+
* `target` already equals `next`.
|
|
1010
1571
|
*/
|
|
1011
|
-
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
transform: new _ZoomTransform(canvasW / 2 - minX, canvasH / 2 - minY, 1),
|
|
1031
|
-
contentHeight: padding * 2
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
if (nodes.length > 50) {
|
|
1035
|
-
const spread = 1 + Math.sqrt(nodes.length) / 120;
|
|
1036
|
-
const cx2 = (minX + maxX) / 2;
|
|
1037
|
-
const cy2 = (minY + maxY) / 2;
|
|
1038
|
-
graphW *= spread;
|
|
1039
|
-
graphH *= spread;
|
|
1040
|
-
minX = cx2 - graphW / 2;
|
|
1041
|
-
maxX = cx2 + graphW / 2;
|
|
1042
|
-
minY = cy2 - graphH / 2;
|
|
1043
|
-
maxY = cy2 + graphH / 2;
|
|
1044
|
-
}
|
|
1045
|
-
const availW = canvasW - padding * 2;
|
|
1046
|
-
const availH = canvasH - padding * 2;
|
|
1047
|
-
const k = Math.min(1, availW / graphW, availH / graphH);
|
|
1048
|
-
const cx = (minX + maxX) / 2;
|
|
1049
|
-
const cy = (minY + maxY) / 2;
|
|
1050
|
-
const tx = canvasW / 2 - cx * k;
|
|
1051
|
-
const ty = canvasH / 2 - cy * k;
|
|
1052
|
-
const contentHeight = graphH * k + padding * 2;
|
|
1053
|
-
return {
|
|
1054
|
-
transform: new _ZoomTransform(tx, ty, k),
|
|
1055
|
-
contentHeight
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
/** Identity transform (no pan, no zoom). */
|
|
1059
|
-
static identity() {
|
|
1060
|
-
return new _ZoomTransform(0, 0, 1);
|
|
1572
|
+
retarget(target, now2) {
|
|
1573
|
+
if (focusSnapshotsEqual(target, this.next)) return;
|
|
1574
|
+
const p = this.rawProgress(now2);
|
|
1575
|
+
this.prev = p < 0.5 ? this.prev : this.next;
|
|
1576
|
+
this.next = target;
|
|
1577
|
+
this.startTime = now2;
|
|
1578
|
+
}
|
|
1579
|
+
/** Raw (un-eased) 0..1 progress. */
|
|
1580
|
+
rawProgress(now2) {
|
|
1581
|
+
if (this.duration <= 0) return 1;
|
|
1582
|
+
return Math.min(1, Math.max(0, (now2 - this.startTime) / this.duration));
|
|
1583
|
+
}
|
|
1584
|
+
/** Eased 0..1 progress toward `next`. */
|
|
1585
|
+
progress(now2) {
|
|
1586
|
+
return this.ease(this.rawProgress(now2));
|
|
1587
|
+
}
|
|
1588
|
+
/** True once the transition has fully settled onto `next`. */
|
|
1589
|
+
isSettled(now2) {
|
|
1590
|
+
return this.rawProgress(now2) >= 1;
|
|
1061
1591
|
}
|
|
1062
1592
|
};
|
|
1593
|
+
function composeStandingFocus(highlight, searchMatches, selected, adjacency) {
|
|
1594
|
+
const hasHighlight = highlight !== null && highlight.size > 0;
|
|
1595
|
+
const hasSearch = searchMatches !== null && searchMatches.size > 0;
|
|
1596
|
+
let core = null;
|
|
1597
|
+
if (highlight !== null && hasHighlight && searchMatches !== null && hasSearch) {
|
|
1598
|
+
const inter = intersect(highlight, searchMatches);
|
|
1599
|
+
core = inter.size > 0 ? inter : searchMatches;
|
|
1600
|
+
} else if (hasHighlight) {
|
|
1601
|
+
core = highlight;
|
|
1602
|
+
} else if (hasSearch) {
|
|
1603
|
+
core = null;
|
|
1604
|
+
}
|
|
1605
|
+
const connected = /* @__PURE__ */ new Set();
|
|
1606
|
+
if (core) {
|
|
1607
|
+
for (const id of core) {
|
|
1608
|
+
connected.add(id);
|
|
1609
|
+
const neighbors = adjacency.get(id);
|
|
1610
|
+
if (neighbors) for (const nid of neighbors) connected.add(nid);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
const hasActive = core !== null && core.size > 0 || hasSearch || selected.size > 0;
|
|
1614
|
+
return {
|
|
1615
|
+
hasActive,
|
|
1616
|
+
connected,
|
|
1617
|
+
searchMatches: hasSearch ? searchMatches : null,
|
|
1618
|
+
selected
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function layerHoverFocus(standing, hoveredId, hoverConnected) {
|
|
1622
|
+
if (hoveredId === null || hoverConnected === null) return standing;
|
|
1623
|
+
return {
|
|
1624
|
+
hasActive: true,
|
|
1625
|
+
connected: hoverConnected,
|
|
1626
|
+
searchMatches: standing.searchMatches,
|
|
1627
|
+
selected: standing.selected
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
function intersect(a2, b) {
|
|
1631
|
+
const [small, large] = a2.size <= b.size ? [a2, b] : [b, a2];
|
|
1632
|
+
const out = /* @__PURE__ */ new Set();
|
|
1633
|
+
for (const v of small) if (large.has(v)) out.add(v);
|
|
1634
|
+
return out;
|
|
1635
|
+
}
|
|
1063
1636
|
|
|
1064
1637
|
// src/graph/interaction.ts
|
|
1065
1638
|
var ZOOM_MIN = 0.05;
|
|
@@ -1121,6 +1694,15 @@ var GraphInteractionManager = class {
|
|
|
1121
1694
|
getTransform() {
|
|
1122
1695
|
return this.transform;
|
|
1123
1696
|
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Replace the internal selection set. Used by the mount to prune deleted ids
|
|
1699
|
+
* after a data update so a later shift-click can't resurrect them through
|
|
1700
|
+
* `onSelectionChange`. Does NOT fire `onSelectionChange` — the caller owns any
|
|
1701
|
+
* downstream sync (the mount already holds the pruned set).
|
|
1702
|
+
*/
|
|
1703
|
+
setSelection(ids) {
|
|
1704
|
+
this.selectedIds = new Set(ids);
|
|
1705
|
+
}
|
|
1124
1706
|
destroy() {
|
|
1125
1707
|
this.canvas.removeEventListener("wheel", this.boundWheel);
|
|
1126
1708
|
this.canvas.removeEventListener("mousedown", this.boundMouseDown);
|
|
@@ -1165,6 +1747,10 @@ var GraphInteractionManager = class {
|
|
|
1165
1747
|
}
|
|
1166
1748
|
onMouseMove(e) {
|
|
1167
1749
|
const { x: x3, y: y3 } = this.canvasXY(e);
|
|
1750
|
+
if (this.callbacks.onPointerMove) {
|
|
1751
|
+
const gp = this.transform.screenToGraph(x3, y3);
|
|
1752
|
+
this.callbacks.onPointerMove(gp.x, gp.y);
|
|
1753
|
+
}
|
|
1168
1754
|
if (this.dragState) {
|
|
1169
1755
|
const graph = this.transform.screenToGraph(x3, y3);
|
|
1170
1756
|
if (!this.dragState.started) {
|
|
@@ -1223,6 +1809,7 @@ var GraphInteractionManager = class {
|
|
|
1223
1809
|
onMouseLeave(_e) {
|
|
1224
1810
|
this.callbacks.onHoverChange(null);
|
|
1225
1811
|
this.canvas.style.cursor = "default";
|
|
1812
|
+
this.callbacks.onPointerLeave?.();
|
|
1226
1813
|
if (this.panState) {
|
|
1227
1814
|
this.panState = null;
|
|
1228
1815
|
}
|
|
@@ -1455,9 +2042,76 @@ function attachGraphKeyboardNav(options) {
|
|
|
1455
2042
|
};
|
|
1456
2043
|
}
|
|
1457
2044
|
|
|
2045
|
+
// src/graph/legend.ts
|
|
2046
|
+
function escapeHtml(str) {
|
|
2047
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2048
|
+
}
|
|
2049
|
+
function createGraphLegend(host, data, callbacks) {
|
|
2050
|
+
const listeners = [];
|
|
2051
|
+
function render(view) {
|
|
2052
|
+
teardownListeners();
|
|
2053
|
+
host.replaceChildren();
|
|
2054
|
+
if (view.nodes.length === 0 && view.edges.length === 0) {
|
|
2055
|
+
host.style.display = "none";
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
host.style.display = "";
|
|
2059
|
+
for (const entry of view.nodes) {
|
|
2060
|
+
host.appendChild(nodeRow(entry));
|
|
2061
|
+
}
|
|
2062
|
+
for (const entry of view.edges) {
|
|
2063
|
+
host.appendChild(edgeRow(entry));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
function nodeRow(entry) {
|
|
2067
|
+
const interactive = callbacks.interactive;
|
|
2068
|
+
const el = document.createElement(interactive ? "button" : "div");
|
|
2069
|
+
el.className = "oc-graph-legend-item";
|
|
2070
|
+
if (!entry.active) el.classList.add("oc-graph-legend-item--inactive");
|
|
2071
|
+
if (interactive) {
|
|
2072
|
+
const btn = el;
|
|
2073
|
+
btn.type = "button";
|
|
2074
|
+
btn.setAttribute("aria-pressed", String(entry.active));
|
|
2075
|
+
const onClick = () => callbacks.onToggle(entry.label);
|
|
2076
|
+
const onEnter = () => callbacks.onHover(entry.label);
|
|
2077
|
+
const onLeave = () => callbacks.onHover(null);
|
|
2078
|
+
btn.addEventListener("click", onClick);
|
|
2079
|
+
btn.addEventListener("mouseenter", onEnter);
|
|
2080
|
+
btn.addEventListener("mouseleave", onLeave);
|
|
2081
|
+
listeners.push(() => {
|
|
2082
|
+
btn.removeEventListener("click", onClick);
|
|
2083
|
+
btn.removeEventListener("mouseenter", onEnter);
|
|
2084
|
+
btn.removeEventListener("mouseleave", onLeave);
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
el.innerHTML = `<span class="oc-graph-legend-swatch" style="background:${escapeHtml(entry.color)}"></span><span class="oc-graph-legend-label">${escapeHtml(entry.label)}</span>` + (callbacks.counts && entry.count != null ? `<span class="oc-graph-legend-count">${entry.count.toLocaleString()}</span>` : "");
|
|
2088
|
+
return el;
|
|
2089
|
+
}
|
|
2090
|
+
function edgeRow(entry) {
|
|
2091
|
+
const el = document.createElement("div");
|
|
2092
|
+
el.className = "oc-graph-legend-item oc-graph-legend-item--edge";
|
|
2093
|
+
el.innerHTML = `<span class="oc-graph-legend-swatch oc-graph-legend-swatch--line" style="background:${escapeHtml(entry.color)}"></span><span class="oc-graph-legend-label">${escapeHtml(entry.label)}</span>` + (callbacks.counts && entry.count != null ? `<span class="oc-graph-legend-count">${entry.count.toLocaleString()}</span>` : "");
|
|
2094
|
+
return el;
|
|
2095
|
+
}
|
|
2096
|
+
function teardownListeners() {
|
|
2097
|
+
for (const off of listeners) off();
|
|
2098
|
+
listeners.length = 0;
|
|
2099
|
+
}
|
|
2100
|
+
render(data);
|
|
2101
|
+
return {
|
|
2102
|
+
update: render,
|
|
2103
|
+
destroy() {
|
|
2104
|
+
teardownListeners();
|
|
2105
|
+
host.replaceChildren();
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
|
|
1458
2110
|
// src/graph/search.ts
|
|
1459
2111
|
var GraphSearchManager = class {
|
|
1460
2112
|
matchedIds = null;
|
|
2113
|
+
/** The last active (non-empty) query, so a data update can re-run it. */
|
|
2114
|
+
query = null;
|
|
1461
2115
|
/**
|
|
1462
2116
|
* Search for nodes matching the query string.
|
|
1463
2117
|
* Returns a Set of matching node ids, or an empty set if nothing matches.
|
|
@@ -1466,8 +2120,10 @@ var GraphSearchManager = class {
|
|
|
1466
2120
|
const q = query.toLowerCase().trim();
|
|
1467
2121
|
if (q === "") {
|
|
1468
2122
|
this.matchedIds = null;
|
|
2123
|
+
this.query = null;
|
|
1469
2124
|
return /* @__PURE__ */ new Set();
|
|
1470
2125
|
}
|
|
2126
|
+
this.query = query;
|
|
1471
2127
|
const matches = /* @__PURE__ */ new Set();
|
|
1472
2128
|
for (const node of nodes) {
|
|
1473
2129
|
const label = (node.label ?? "").toLowerCase();
|
|
@@ -1485,14 +2141,53 @@ var GraphSearchManager = class {
|
|
|
1485
2141
|
*/
|
|
1486
2142
|
clearSearch() {
|
|
1487
2143
|
this.matchedIds = null;
|
|
2144
|
+
this.query = null;
|
|
1488
2145
|
return null;
|
|
1489
2146
|
}
|
|
1490
2147
|
/** Get the current set of matched ids, or null if no search is active. */
|
|
1491
2148
|
getMatches() {
|
|
1492
2149
|
return this.matchedIds;
|
|
1493
2150
|
}
|
|
2151
|
+
/** The last active query string, or null when no search is active. */
|
|
2152
|
+
getQuery() {
|
|
2153
|
+
return this.query;
|
|
2154
|
+
}
|
|
1494
2155
|
};
|
|
1495
2156
|
|
|
2157
|
+
// src/graph/seed.ts
|
|
2158
|
+
function hash32(str) {
|
|
2159
|
+
let h = 2166136261;
|
|
2160
|
+
for (let i = 0; i < str.length; i++) {
|
|
2161
|
+
h ^= str.charCodeAt(i);
|
|
2162
|
+
h = Math.imul(h, 16777619);
|
|
2163
|
+
}
|
|
2164
|
+
return h >>> 0;
|
|
2165
|
+
}
|
|
2166
|
+
function unit(h) {
|
|
2167
|
+
return (h >>> 0) / 4294967296;
|
|
2168
|
+
}
|
|
2169
|
+
var R0 = 300;
|
|
2170
|
+
function seedNodePositions(nodes, seed) {
|
|
2171
|
+
const n = nodes.length;
|
|
2172
|
+
if (n === 0) return;
|
|
2173
|
+
for (const node of nodes) {
|
|
2174
|
+
const u1 = unit(hash32(`${node.id}|${seed}|r`));
|
|
2175
|
+
const u2 = unit(hash32(`${node.id}|${seed}|t`));
|
|
2176
|
+
const r = R0 * Math.sqrt(u1);
|
|
2177
|
+
const theta = u2 * Math.PI * 2;
|
|
2178
|
+
let cx = 0;
|
|
2179
|
+
let cy = 0;
|
|
2180
|
+
if (node.community) {
|
|
2181
|
+
const ca = unit(hash32(`${node.community}|${seed}`)) * Math.PI * 2;
|
|
2182
|
+
const bias = 0.6 * R0;
|
|
2183
|
+
cx = Math.cos(ca) * bias;
|
|
2184
|
+
cy = Math.sin(ca) * bias;
|
|
2185
|
+
}
|
|
2186
|
+
node.x = cx + r * Math.cos(theta);
|
|
2187
|
+
node.y = cy + r * Math.sin(theta);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
1496
2191
|
// ../../node_modules/.bun/d3-force@3.0.0/node_modules/d3-force/src/center.js
|
|
1497
2192
|
function center_default(x3, y3) {
|
|
1498
2193
|
var nodes, strength = 1;
|
|
@@ -1522,301 +2217,6 @@ function center_default(x3, y3) {
|
|
|
1522
2217
|
return force;
|
|
1523
2218
|
}
|
|
1524
2219
|
|
|
1525
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/add.js
|
|
1526
|
-
function add_default(d) {
|
|
1527
|
-
const x3 = +this._x.call(null, d), y3 = +this._y.call(null, d);
|
|
1528
|
-
return add(this.cover(x3, y3), x3, y3, d);
|
|
1529
|
-
}
|
|
1530
|
-
function add(tree, x3, y3, d) {
|
|
1531
|
-
if (isNaN(x3) || isNaN(y3)) return tree;
|
|
1532
|
-
var parent, node = tree._root, leaf = { data: d }, x0 = tree._x0, y0 = tree._y0, x1 = tree._x1, y1 = tree._y1, xm, ym, xp, yp, right, bottom, i, j;
|
|
1533
|
-
if (!node) return tree._root = leaf, tree;
|
|
1534
|
-
while (node.length) {
|
|
1535
|
-
if (right = x3 >= (xm = (x0 + x1) / 2)) x0 = xm;
|
|
1536
|
-
else x1 = xm;
|
|
1537
|
-
if (bottom = y3 >= (ym = (y0 + y1) / 2)) y0 = ym;
|
|
1538
|
-
else y1 = ym;
|
|
1539
|
-
if (parent = node, !(node = node[i = bottom << 1 | right])) return parent[i] = leaf, tree;
|
|
1540
|
-
}
|
|
1541
|
-
xp = +tree._x.call(null, node.data);
|
|
1542
|
-
yp = +tree._y.call(null, node.data);
|
|
1543
|
-
if (x3 === xp && y3 === yp) return leaf.next = node, parent ? parent[i] = leaf : tree._root = leaf, tree;
|
|
1544
|
-
do {
|
|
1545
|
-
parent = parent ? parent[i] = new Array(4) : tree._root = new Array(4);
|
|
1546
|
-
if (right = x3 >= (xm = (x0 + x1) / 2)) x0 = xm;
|
|
1547
|
-
else x1 = xm;
|
|
1548
|
-
if (bottom = y3 >= (ym = (y0 + y1) / 2)) y0 = ym;
|
|
1549
|
-
else y1 = ym;
|
|
1550
|
-
} while ((i = bottom << 1 | right) === (j = (yp >= ym) << 1 | xp >= xm));
|
|
1551
|
-
return parent[j] = node, parent[i] = leaf, tree;
|
|
1552
|
-
}
|
|
1553
|
-
function addAll(data) {
|
|
1554
|
-
var d, i, n = data.length, x3, y3, xz = new Array(n), yz = new Array(n), x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
1555
|
-
for (i = 0; i < n; ++i) {
|
|
1556
|
-
if (isNaN(x3 = +this._x.call(null, d = data[i])) || isNaN(y3 = +this._y.call(null, d))) continue;
|
|
1557
|
-
xz[i] = x3;
|
|
1558
|
-
yz[i] = y3;
|
|
1559
|
-
if (x3 < x0) x0 = x3;
|
|
1560
|
-
if (x3 > x1) x1 = x3;
|
|
1561
|
-
if (y3 < y0) y0 = y3;
|
|
1562
|
-
if (y3 > y1) y1 = y3;
|
|
1563
|
-
}
|
|
1564
|
-
if (x0 > x1 || y0 > y1) return this;
|
|
1565
|
-
this.cover(x0, y0).cover(x1, y1);
|
|
1566
|
-
for (i = 0; i < n; ++i) {
|
|
1567
|
-
add(this, xz[i], yz[i], data[i]);
|
|
1568
|
-
}
|
|
1569
|
-
return this;
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/cover.js
|
|
1573
|
-
function cover_default(x3, y3) {
|
|
1574
|
-
if (isNaN(x3 = +x3) || isNaN(y3 = +y3)) return this;
|
|
1575
|
-
var x0 = this._x0, y0 = this._y0, x1 = this._x1, y1 = this._y1;
|
|
1576
|
-
if (isNaN(x0)) {
|
|
1577
|
-
x1 = (x0 = Math.floor(x3)) + 1;
|
|
1578
|
-
y1 = (y0 = Math.floor(y3)) + 1;
|
|
1579
|
-
} else {
|
|
1580
|
-
var z = x1 - x0 || 1, node = this._root, parent, i;
|
|
1581
|
-
while (x0 > x3 || x3 >= x1 || y0 > y3 || y3 >= y1) {
|
|
1582
|
-
i = (y3 < y0) << 1 | x3 < x0;
|
|
1583
|
-
parent = new Array(4), parent[i] = node, node = parent, z *= 2;
|
|
1584
|
-
switch (i) {
|
|
1585
|
-
case 0:
|
|
1586
|
-
x1 = x0 + z, y1 = y0 + z;
|
|
1587
|
-
break;
|
|
1588
|
-
case 1:
|
|
1589
|
-
x0 = x1 - z, y1 = y0 + z;
|
|
1590
|
-
break;
|
|
1591
|
-
case 2:
|
|
1592
|
-
x1 = x0 + z, y0 = y1 - z;
|
|
1593
|
-
break;
|
|
1594
|
-
case 3:
|
|
1595
|
-
x0 = x1 - z, y0 = y1 - z;
|
|
1596
|
-
break;
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
if (this._root && this._root.length) this._root = node;
|
|
1600
|
-
}
|
|
1601
|
-
this._x0 = x0;
|
|
1602
|
-
this._y0 = y0;
|
|
1603
|
-
this._x1 = x1;
|
|
1604
|
-
this._y1 = y1;
|
|
1605
|
-
return this;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/data.js
|
|
1609
|
-
function data_default() {
|
|
1610
|
-
var data = [];
|
|
1611
|
-
this.visit(function(node) {
|
|
1612
|
-
if (!node.length) do
|
|
1613
|
-
data.push(node.data);
|
|
1614
|
-
while (node = node.next);
|
|
1615
|
-
});
|
|
1616
|
-
return data;
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/extent.js
|
|
1620
|
-
function extent_default(_) {
|
|
1621
|
-
return arguments.length ? this.cover(+_[0][0], +_[0][1]).cover(+_[1][0], +_[1][1]) : isNaN(this._x0) ? void 0 : [[this._x0, this._y0], [this._x1, this._y1]];
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/quad.js
|
|
1625
|
-
function quad_default(node, x0, y0, x1, y1) {
|
|
1626
|
-
this.node = node;
|
|
1627
|
-
this.x0 = x0;
|
|
1628
|
-
this.y0 = y0;
|
|
1629
|
-
this.x1 = x1;
|
|
1630
|
-
this.y1 = y1;
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/find.js
|
|
1634
|
-
function find_default(x3, y3, radius) {
|
|
1635
|
-
var data, x0 = this._x0, y0 = this._y0, x1, y1, x22, y22, x32 = this._x1, y32 = this._y1, quads = [], node = this._root, q, i;
|
|
1636
|
-
if (node) quads.push(new quad_default(node, x0, y0, x32, y32));
|
|
1637
|
-
if (radius == null) radius = Infinity;
|
|
1638
|
-
else {
|
|
1639
|
-
x0 = x3 - radius, y0 = y3 - radius;
|
|
1640
|
-
x32 = x3 + radius, y32 = y3 + radius;
|
|
1641
|
-
radius *= radius;
|
|
1642
|
-
}
|
|
1643
|
-
while (q = quads.pop()) {
|
|
1644
|
-
if (!(node = q.node) || (x1 = q.x0) > x32 || (y1 = q.y0) > y32 || (x22 = q.x1) < x0 || (y22 = q.y1) < y0) continue;
|
|
1645
|
-
if (node.length) {
|
|
1646
|
-
var xm = (x1 + x22) / 2, ym = (y1 + y22) / 2;
|
|
1647
|
-
quads.push(
|
|
1648
|
-
new quad_default(node[3], xm, ym, x22, y22),
|
|
1649
|
-
new quad_default(node[2], x1, ym, xm, y22),
|
|
1650
|
-
new quad_default(node[1], xm, y1, x22, ym),
|
|
1651
|
-
new quad_default(node[0], x1, y1, xm, ym)
|
|
1652
|
-
);
|
|
1653
|
-
if (i = (y3 >= ym) << 1 | x3 >= xm) {
|
|
1654
|
-
q = quads[quads.length - 1];
|
|
1655
|
-
quads[quads.length - 1] = quads[quads.length - 1 - i];
|
|
1656
|
-
quads[quads.length - 1 - i] = q;
|
|
1657
|
-
}
|
|
1658
|
-
} else {
|
|
1659
|
-
var dx = x3 - +this._x.call(null, node.data), dy = y3 - +this._y.call(null, node.data), d2 = dx * dx + dy * dy;
|
|
1660
|
-
if (d2 < radius) {
|
|
1661
|
-
var d = Math.sqrt(radius = d2);
|
|
1662
|
-
x0 = x3 - d, y0 = y3 - d;
|
|
1663
|
-
x32 = x3 + d, y32 = y3 + d;
|
|
1664
|
-
data = node.data;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
return data;
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/remove.js
|
|
1672
|
-
function remove_default(d) {
|
|
1673
|
-
if (isNaN(x3 = +this._x.call(null, d)) || isNaN(y3 = +this._y.call(null, d))) return this;
|
|
1674
|
-
var parent, node = this._root, retainer, previous, next, x0 = this._x0, y0 = this._y0, x1 = this._x1, y1 = this._y1, x3, y3, xm, ym, right, bottom, i, j;
|
|
1675
|
-
if (!node) return this;
|
|
1676
|
-
if (node.length) while (true) {
|
|
1677
|
-
if (right = x3 >= (xm = (x0 + x1) / 2)) x0 = xm;
|
|
1678
|
-
else x1 = xm;
|
|
1679
|
-
if (bottom = y3 >= (ym = (y0 + y1) / 2)) y0 = ym;
|
|
1680
|
-
else y1 = ym;
|
|
1681
|
-
if (!(parent = node, node = node[i = bottom << 1 | right])) return this;
|
|
1682
|
-
if (!node.length) break;
|
|
1683
|
-
if (parent[i + 1 & 3] || parent[i + 2 & 3] || parent[i + 3 & 3]) retainer = parent, j = i;
|
|
1684
|
-
}
|
|
1685
|
-
while (node.data !== d) if (!(previous = node, node = node.next)) return this;
|
|
1686
|
-
if (next = node.next) delete node.next;
|
|
1687
|
-
if (previous) return next ? previous.next = next : delete previous.next, this;
|
|
1688
|
-
if (!parent) return this._root = next, this;
|
|
1689
|
-
next ? parent[i] = next : delete parent[i];
|
|
1690
|
-
if ((node = parent[0] || parent[1] || parent[2] || parent[3]) && node === (parent[3] || parent[2] || parent[1] || parent[0]) && !node.length) {
|
|
1691
|
-
if (retainer) retainer[j] = node;
|
|
1692
|
-
else this._root = node;
|
|
1693
|
-
}
|
|
1694
|
-
return this;
|
|
1695
|
-
}
|
|
1696
|
-
function removeAll(data) {
|
|
1697
|
-
for (var i = 0, n = data.length; i < n; ++i) this.remove(data[i]);
|
|
1698
|
-
return this;
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/root.js
|
|
1702
|
-
function root_default() {
|
|
1703
|
-
return this._root;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/size.js
|
|
1707
|
-
function size_default() {
|
|
1708
|
-
var size = 0;
|
|
1709
|
-
this.visit(function(node) {
|
|
1710
|
-
if (!node.length) do
|
|
1711
|
-
++size;
|
|
1712
|
-
while (node = node.next);
|
|
1713
|
-
});
|
|
1714
|
-
return size;
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/visit.js
|
|
1718
|
-
function visit_default(callback) {
|
|
1719
|
-
var quads = [], q, node = this._root, child, x0, y0, x1, y1;
|
|
1720
|
-
if (node) quads.push(new quad_default(node, this._x0, this._y0, this._x1, this._y1));
|
|
1721
|
-
while (q = quads.pop()) {
|
|
1722
|
-
if (!callback(node = q.node, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1) && node.length) {
|
|
1723
|
-
var xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
|
|
1724
|
-
if (child = node[3]) quads.push(new quad_default(child, xm, ym, x1, y1));
|
|
1725
|
-
if (child = node[2]) quads.push(new quad_default(child, x0, ym, xm, y1));
|
|
1726
|
-
if (child = node[1]) quads.push(new quad_default(child, xm, y0, x1, ym));
|
|
1727
|
-
if (child = node[0]) quads.push(new quad_default(child, x0, y0, xm, ym));
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
return this;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/visitAfter.js
|
|
1734
|
-
function visitAfter_default(callback) {
|
|
1735
|
-
var quads = [], next = [], q;
|
|
1736
|
-
if (this._root) quads.push(new quad_default(this._root, this._x0, this._y0, this._x1, this._y1));
|
|
1737
|
-
while (q = quads.pop()) {
|
|
1738
|
-
var node = q.node;
|
|
1739
|
-
if (node.length) {
|
|
1740
|
-
var child, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1, xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
|
|
1741
|
-
if (child = node[0]) quads.push(new quad_default(child, x0, y0, xm, ym));
|
|
1742
|
-
if (child = node[1]) quads.push(new quad_default(child, xm, y0, x1, ym));
|
|
1743
|
-
if (child = node[2]) quads.push(new quad_default(child, x0, ym, xm, y1));
|
|
1744
|
-
if (child = node[3]) quads.push(new quad_default(child, xm, ym, x1, y1));
|
|
1745
|
-
}
|
|
1746
|
-
next.push(q);
|
|
1747
|
-
}
|
|
1748
|
-
while (q = next.pop()) {
|
|
1749
|
-
callback(q.node, q.x0, q.y0, q.x1, q.y1);
|
|
1750
|
-
}
|
|
1751
|
-
return this;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/x.js
|
|
1755
|
-
function defaultX(d) {
|
|
1756
|
-
return d[0];
|
|
1757
|
-
}
|
|
1758
|
-
function x_default(_) {
|
|
1759
|
-
return arguments.length ? (this._x = _, this) : this._x;
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/y.js
|
|
1763
|
-
function defaultY(d) {
|
|
1764
|
-
return d[1];
|
|
1765
|
-
}
|
|
1766
|
-
function y_default(_) {
|
|
1767
|
-
return arguments.length ? (this._y = _, this) : this._y;
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// ../../node_modules/.bun/d3-quadtree@3.0.1/node_modules/d3-quadtree/src/quadtree.js
|
|
1771
|
-
function quadtree(nodes, x3, y3) {
|
|
1772
|
-
var tree = new Quadtree(x3 == null ? defaultX : x3, y3 == null ? defaultY : y3, NaN, NaN, NaN, NaN);
|
|
1773
|
-
return nodes == null ? tree : tree.addAll(nodes);
|
|
1774
|
-
}
|
|
1775
|
-
function Quadtree(x3, y3, x0, y0, x1, y1) {
|
|
1776
|
-
this._x = x3;
|
|
1777
|
-
this._y = y3;
|
|
1778
|
-
this._x0 = x0;
|
|
1779
|
-
this._y0 = y0;
|
|
1780
|
-
this._x1 = x1;
|
|
1781
|
-
this._y1 = y1;
|
|
1782
|
-
this._root = void 0;
|
|
1783
|
-
}
|
|
1784
|
-
function leaf_copy(leaf) {
|
|
1785
|
-
var copy = { data: leaf.data }, next = copy;
|
|
1786
|
-
while (leaf = leaf.next) next = next.next = { data: leaf.data };
|
|
1787
|
-
return copy;
|
|
1788
|
-
}
|
|
1789
|
-
var treeProto = quadtree.prototype = Quadtree.prototype;
|
|
1790
|
-
treeProto.copy = function() {
|
|
1791
|
-
var copy = new Quadtree(this._x, this._y, this._x0, this._y0, this._x1, this._y1), node = this._root, nodes, child;
|
|
1792
|
-
if (!node) return copy;
|
|
1793
|
-
if (!node.length) return copy._root = leaf_copy(node), copy;
|
|
1794
|
-
nodes = [{ source: node, target: copy._root = new Array(4) }];
|
|
1795
|
-
while (node = nodes.pop()) {
|
|
1796
|
-
for (var i = 0; i < 4; ++i) {
|
|
1797
|
-
if (child = node.source[i]) {
|
|
1798
|
-
if (child.length) nodes.push({ source: child, target: node.target[i] = new Array(4) });
|
|
1799
|
-
else node.target[i] = leaf_copy(child);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
return copy;
|
|
1804
|
-
};
|
|
1805
|
-
treeProto.add = add_default;
|
|
1806
|
-
treeProto.addAll = addAll;
|
|
1807
|
-
treeProto.cover = cover_default;
|
|
1808
|
-
treeProto.data = data_default;
|
|
1809
|
-
treeProto.extent = extent_default;
|
|
1810
|
-
treeProto.find = find_default;
|
|
1811
|
-
treeProto.remove = remove_default;
|
|
1812
|
-
treeProto.removeAll = removeAll;
|
|
1813
|
-
treeProto.root = root_default;
|
|
1814
|
-
treeProto.size = size_default;
|
|
1815
|
-
treeProto.visit = visit_default;
|
|
1816
|
-
treeProto.visitAfter = visitAfter_default;
|
|
1817
|
-
treeProto.x = x_default;
|
|
1818
|
-
treeProto.y = y_default;
|
|
1819
|
-
|
|
1820
2220
|
// ../../node_modules/.bun/d3-force@3.0.0/node_modules/d3-force/src/constant.js
|
|
1821
2221
|
function constant_default(x3) {
|
|
1822
2222
|
return function() {
|
|
@@ -2354,7 +2754,7 @@ function manyBody_default() {
|
|
|
2354
2754
|
}
|
|
2355
2755
|
|
|
2356
2756
|
// ../../node_modules/.bun/d3-force@3.0.0/node_modules/d3-force/src/x.js
|
|
2357
|
-
function
|
|
2757
|
+
function x_default(x3) {
|
|
2358
2758
|
var strength = constant_default(0.1), nodes, strengths, xz;
|
|
2359
2759
|
if (typeof x3 !== "function") x3 = constant_default(x3 == null ? 0 : +x3);
|
|
2360
2760
|
function force(alpha) {
|
|
@@ -2385,7 +2785,7 @@ function x_default2(x3) {
|
|
|
2385
2785
|
}
|
|
2386
2786
|
|
|
2387
2787
|
// ../../node_modules/.bun/d3-force@3.0.0/node_modules/d3-force/src/y.js
|
|
2388
|
-
function
|
|
2788
|
+
function y_default(y3) {
|
|
2389
2789
|
var strength = constant_default(0.1), nodes, strengths, yz;
|
|
2390
2790
|
if (typeof y3 !== "function") y3 = constant_default(y3 == null ? 0 : +y3);
|
|
2391
2791
|
function force(alpha) {
|
|
@@ -2416,8 +2816,15 @@ function y_default2(y3) {
|
|
|
2416
2816
|
}
|
|
2417
2817
|
|
|
2418
2818
|
// src/graph/simulation.ts
|
|
2419
|
-
var SYNC_MAX_TICKS = 300;
|
|
2420
2819
|
var SYNC_TICKS_PER_BATCH = 15;
|
|
2820
|
+
var SYNC_MAX_TICKS_CEIL = 800;
|
|
2821
|
+
var DEFAULT_ALPHA_MIN = 1e-3;
|
|
2822
|
+
var DEFAULT_WARMUP_BUDGET_MS = 250;
|
|
2823
|
+
function ticksToAlphaMin(alphaDecay, alphaMin = DEFAULT_ALPHA_MIN) {
|
|
2824
|
+
if (!(alphaDecay > 0) || alphaDecay >= 1) return SYNC_MAX_TICKS_CEIL;
|
|
2825
|
+
const n = Math.ceil(Math.log(alphaMin) / Math.log(1 - alphaDecay));
|
|
2826
|
+
return Math.min(SYNC_MAX_TICKS_CEIL, Math.max(1, n));
|
|
2827
|
+
}
|
|
2421
2828
|
function forceCluster(nodes, strength) {
|
|
2422
2829
|
return (alpha) => {
|
|
2423
2830
|
const cx = /* @__PURE__ */ new Map();
|
|
@@ -2444,6 +2851,24 @@ function forceCluster(nodes, strength) {
|
|
|
2444
2851
|
}
|
|
2445
2852
|
};
|
|
2446
2853
|
}
|
|
2854
|
+
function forceCursor(nodes, pointer) {
|
|
2855
|
+
return (alpha) => {
|
|
2856
|
+
if (!pointer.active || pointer.radius <= 0) return;
|
|
2857
|
+
const r2 = pointer.radius * pointer.radius;
|
|
2858
|
+
const k = pointer.strength * alpha;
|
|
2859
|
+
for (const node of nodes) {
|
|
2860
|
+
const dx = (node.x ?? 0) - pointer.x;
|
|
2861
|
+
const dy = (node.y ?? 0) - pointer.y;
|
|
2862
|
+
const dist2 = dx * dx + dy * dy;
|
|
2863
|
+
if (dist2 >= r2) continue;
|
|
2864
|
+
const dist = Math.sqrt(dist2) || 1e-6;
|
|
2865
|
+
const falloff = (pointer.radius - dist) / pointer.radius;
|
|
2866
|
+
const push = k * falloff / dist;
|
|
2867
|
+
node.vx = (node.vx ?? 0) + dx * push;
|
|
2868
|
+
node.vy = (node.vy ?? 0) + dy * push;
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2447
2872
|
var SimulationManager = class _SimulationManager {
|
|
2448
2873
|
worker = null;
|
|
2449
2874
|
syncSim = null;
|
|
@@ -2453,18 +2878,36 @@ var SimulationManager = class _SimulationManager {
|
|
|
2453
2878
|
settledCb = null;
|
|
2454
2879
|
destroyed = false;
|
|
2455
2880
|
syncRafId = null;
|
|
2881
|
+
/** Derived per-graph cap on sync ticks, from alphaDecay via ticksToAlphaMin. */
|
|
2882
|
+
syncMaxTicks = SYNC_MAX_TICKS_CEIL;
|
|
2883
|
+
/** True until the sync warmup loop has completed (nothing renders before then). */
|
|
2884
|
+
syncWarmupPending = false;
|
|
2885
|
+
/** Remaining warmup ticks and the ms budget, consumed by the pre-reveal loop. */
|
|
2886
|
+
syncWarmupTicks = 0;
|
|
2887
|
+
syncWarmupBudgetMs = DEFAULT_WARMUP_BUDGET_MS;
|
|
2888
|
+
/** Injectable clock for the warmup ms budget (deterministic in tests). */
|
|
2889
|
+
now = typeof performance !== "undefined" ? () => performance.now() : () => Date.now();
|
|
2456
2890
|
// Stored for worker->sync fallback
|
|
2457
2891
|
initNodes = [];
|
|
2458
2892
|
initEdges = [];
|
|
2459
2893
|
initConfig = null;
|
|
2894
|
+
// Cursor-repulsion pointer state (sync path). radius=0 keeps the force inert.
|
|
2895
|
+
pointer = { x: 0, y: 0, active: false, radius: 0, strength: 0 };
|
|
2896
|
+
// Separately tracked alpha-target intents so drag and cursor don't stomp each
|
|
2897
|
+
// other. The higher intent wins; releasing one falls back to the other.
|
|
2898
|
+
dragAlphaTarget = 0;
|
|
2899
|
+
cursorAlphaTarget = 0;
|
|
2460
2900
|
constructor() {
|
|
2461
2901
|
}
|
|
2462
2902
|
/**
|
|
2463
2903
|
* Create a SimulationManager. Uses Web Worker for large graphs,
|
|
2464
2904
|
* synchronous fallback for small graphs or when Worker unavailable.
|
|
2905
|
+
*
|
|
2906
|
+
* `opts.now` injects a clock for the warmup ms budget (tests pass a fake one).
|
|
2465
2907
|
*/
|
|
2466
|
-
static create(nodes, edges, config) {
|
|
2908
|
+
static create(nodes, edges, config, opts) {
|
|
2467
2909
|
const mgr = new _SimulationManager();
|
|
2910
|
+
if (opts?.now) mgr.now = opts.now;
|
|
2468
2911
|
const useWorker = typeof Worker !== "undefined";
|
|
2469
2912
|
if (useWorker) {
|
|
2470
2913
|
mgr.initWorker(nodes, edges, config);
|
|
@@ -2491,36 +2934,90 @@ var SimulationManager = class _SimulationManager {
|
|
|
2491
2934
|
this.runSyncTicks();
|
|
2492
2935
|
}
|
|
2493
2936
|
}
|
|
2494
|
-
/**
|
|
2495
|
-
|
|
2937
|
+
/**
|
|
2938
|
+
* Pin a node to fixed x/y coordinates.
|
|
2939
|
+
*
|
|
2940
|
+
* When `alphaTarget` is provided (springy drag), the sim is held warm so the
|
|
2941
|
+
* pinned node's neighbors follow springily. Omitting it posts a byte-identical
|
|
2942
|
+
* legacy message and leaves alpha untouched.
|
|
2943
|
+
*/
|
|
2944
|
+
pinNode(id, x3, y3, alphaTarget) {
|
|
2496
2945
|
if (this.destroyed) return;
|
|
2497
2946
|
if (this.worker) {
|
|
2498
|
-
this.worker.postMessage(
|
|
2947
|
+
this.worker.postMessage(
|
|
2948
|
+
alphaTarget != null ? { type: "pin", nodeId: id, x: x3, y: y3, alphaTarget } : { type: "pin", nodeId: id, x: x3, y: y3 }
|
|
2949
|
+
);
|
|
2499
2950
|
} else {
|
|
2500
2951
|
const node = this.syncNodeMap.get(id);
|
|
2501
2952
|
if (node) {
|
|
2502
2953
|
node.fx = x3;
|
|
2503
2954
|
node.fy = y3;
|
|
2504
2955
|
}
|
|
2956
|
+
if (alphaTarget != null && this.syncSim) {
|
|
2957
|
+
this.dragAlphaTarget = alphaTarget;
|
|
2958
|
+
this.syncAlphaTarget();
|
|
2959
|
+
this.syncSim.restart();
|
|
2960
|
+
this.runSyncTicks();
|
|
2961
|
+
}
|
|
2505
2962
|
}
|
|
2506
2963
|
}
|
|
2507
|
-
/**
|
|
2508
|
-
|
|
2964
|
+
/**
|
|
2965
|
+
* Unpin a node and reheat so forces settle it into equilibrium.
|
|
2966
|
+
*
|
|
2967
|
+
* When `alphaTarget` is provided (springy release), the sim cools back toward
|
|
2968
|
+
* that target instead of the legacy gentle reheat. Omitting it preserves the
|
|
2969
|
+
* exact legacy reheat behavior (and posts a byte-identical message).
|
|
2970
|
+
*/
|
|
2971
|
+
unpinNode(id, alphaTarget) {
|
|
2509
2972
|
if (this.destroyed) return;
|
|
2510
2973
|
if (this.worker) {
|
|
2511
|
-
this.worker.postMessage(
|
|
2974
|
+
this.worker.postMessage(
|
|
2975
|
+
alphaTarget != null ? { type: "unpin", nodeId: id, alphaTarget } : { type: "unpin", nodeId: id }
|
|
2976
|
+
);
|
|
2512
2977
|
} else {
|
|
2513
2978
|
const node = this.syncNodeMap.get(id);
|
|
2514
2979
|
if (node) {
|
|
2515
2980
|
node.fx = null;
|
|
2516
2981
|
node.fy = null;
|
|
2517
2982
|
}
|
|
2518
|
-
if (
|
|
2983
|
+
if (alphaTarget != null) {
|
|
2984
|
+
this.dragAlphaTarget = alphaTarget;
|
|
2985
|
+
this.syncAlphaTarget();
|
|
2986
|
+
this.runSyncTicks();
|
|
2987
|
+
} else if (this.syncSim && this.syncSim.alpha() < 0.1) {
|
|
2519
2988
|
this.syncSim.alpha(0.1).restart();
|
|
2520
2989
|
this.runSyncTicks();
|
|
2521
2990
|
}
|
|
2522
2991
|
}
|
|
2523
2992
|
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Feed the cursor-repulsion force a pointer position. `active: false` clears
|
|
2995
|
+
* the force. No-op when the graph has no cursor force configured (radius 0).
|
|
2996
|
+
*/
|
|
2997
|
+
setPointer(x3, y3, active) {
|
|
2998
|
+
if (this.destroyed) return;
|
|
2999
|
+
if (this.worker) {
|
|
3000
|
+
this.worker.postMessage({ type: "pointer", x: x3, y: y3, active });
|
|
3001
|
+
} else {
|
|
3002
|
+
if (this.pointer.radius <= 0) return;
|
|
3003
|
+
this.pointer.x = x3;
|
|
3004
|
+
this.pointer.y = y3;
|
|
3005
|
+
this.pointer.active = active;
|
|
3006
|
+
this.cursorAlphaTarget = active ? 0.03 : 0;
|
|
3007
|
+
if (this.syncSim) {
|
|
3008
|
+
this.syncAlphaTarget();
|
|
3009
|
+
if (active) {
|
|
3010
|
+
this.syncSim.restart();
|
|
3011
|
+
this.runSyncTicks();
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
/** Apply the max of the tracked alpha-target intents to the sync sim. */
|
|
3017
|
+
syncAlphaTarget() {
|
|
3018
|
+
if (!this.syncSim) return;
|
|
3019
|
+
this.syncSim.alphaTarget(Math.max(this.dragAlphaTarget, this.cursorAlphaTarget));
|
|
3020
|
+
}
|
|
2524
3021
|
/** Drag a node (pins it and reheats slightly). */
|
|
2525
3022
|
dragNode(id, x3, y3) {
|
|
2526
3023
|
if (this.destroyed) return;
|
|
@@ -2634,7 +3131,7 @@ var SimulationManager = class _SimulationManager {
|
|
|
2634
3131
|
this.syncSim = simulation_default(this.syncNodes).force("link", linkForce).force("charge", manyBody_default().strength(config.chargeStrength)).force(
|
|
2635
3132
|
"collide",
|
|
2636
3133
|
collide_default().radius((d) => d.radius + padding)
|
|
2637
|
-
).force("gravityX",
|
|
3134
|
+
).force("gravityX", x_default(0).strength(0.05)).force("gravityY", y_default(0).strength(0.05)).alphaDecay(config.alphaDecay).velocityDecay(config.velocityDecay).stop();
|
|
2638
3135
|
if (config.centerForce !== false) {
|
|
2639
3136
|
this.syncSim.force("center", center_default(0, 0));
|
|
2640
3137
|
}
|
|
@@ -2642,6 +3139,20 @@ var SimulationManager = class _SimulationManager {
|
|
|
2642
3139
|
const clusterFn = forceCluster(this.syncNodes, config.clustering.strength);
|
|
2643
3140
|
this.syncSim.force("cluster", clusterFn);
|
|
2644
3141
|
}
|
|
3142
|
+
this.pointer.active = false;
|
|
3143
|
+
this.pointer.radius = config.cursorRepulsion?.radius ?? 0;
|
|
3144
|
+
this.pointer.strength = config.cursorRepulsion?.strength ?? 0;
|
|
3145
|
+
this.dragAlphaTarget = 0;
|
|
3146
|
+
this.cursorAlphaTarget = 0;
|
|
3147
|
+
const cursorFn = forceCursor(this.syncNodes, this.pointer);
|
|
3148
|
+
this.syncSim.force("cursor", cursorFn);
|
|
3149
|
+
if (config.initialAlpha != null) {
|
|
3150
|
+
this.syncSim.alpha(config.initialAlpha);
|
|
3151
|
+
}
|
|
3152
|
+
this.syncMaxTicks = ticksToAlphaMin(config.alphaDecay);
|
|
3153
|
+
this.syncWarmupTicks = config.warmupTicks ?? 0;
|
|
3154
|
+
this.syncWarmupBudgetMs = config.warmupBudgetMs ?? DEFAULT_WARMUP_BUDGET_MS;
|
|
3155
|
+
this.syncWarmupPending = this.syncWarmupTicks > 0;
|
|
2645
3156
|
this.runSyncTicks(true);
|
|
2646
3157
|
}
|
|
2647
3158
|
/**
|
|
@@ -2662,14 +3173,36 @@ var SimulationManager = class _SimulationManager {
|
|
|
2662
3173
|
this.syncRafId = null;
|
|
2663
3174
|
}
|
|
2664
3175
|
const sim = this.syncSim;
|
|
3176
|
+
const maxTicks = this.syncMaxTicks;
|
|
2665
3177
|
let tickCount = 0;
|
|
3178
|
+
const runWarmup = () => {
|
|
3179
|
+
if (this.destroyed || !this.syncSim) return;
|
|
3180
|
+
this.syncRafId = null;
|
|
3181
|
+
const start2 = this.now();
|
|
3182
|
+
while (this.syncWarmupTicks > 0) {
|
|
3183
|
+
for (let i = 0; i < SYNC_TICKS_PER_BATCH && this.syncWarmupTicks > 0; i++) {
|
|
3184
|
+
sim.tick();
|
|
3185
|
+
this.syncWarmupTicks--;
|
|
3186
|
+
if (sim.alpha() < DEFAULT_ALPHA_MIN) {
|
|
3187
|
+
this.syncWarmupTicks = 0;
|
|
3188
|
+
break;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
if (this.syncWarmupTicks > 0 && this.now() - start2 >= this.syncWarmupBudgetMs) {
|
|
3192
|
+
this.syncWarmupTicks = 0;
|
|
3193
|
+
break;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
this.syncWarmupPending = false;
|
|
3197
|
+
runBatch();
|
|
3198
|
+
};
|
|
2666
3199
|
const runBatch = () => {
|
|
2667
3200
|
if (this.destroyed || !this.syncSim) return;
|
|
2668
3201
|
this.syncRafId = null;
|
|
2669
|
-
for (let i = 0; i < SYNC_TICKS_PER_BATCH && tickCount <
|
|
3202
|
+
for (let i = 0; i < SYNC_TICKS_PER_BATCH && tickCount < maxTicks; i++, tickCount++) {
|
|
2670
3203
|
sim.tick();
|
|
2671
|
-
if (sim.alpha() <
|
|
2672
|
-
tickCount =
|
|
3204
|
+
if (sim.alpha() < DEFAULT_ALPHA_MIN) {
|
|
3205
|
+
tickCount = maxTicks;
|
|
2673
3206
|
break;
|
|
2674
3207
|
}
|
|
2675
3208
|
}
|
|
@@ -2679,7 +3212,7 @@ var SimulationManager = class _SimulationManager {
|
|
|
2679
3212
|
y: n.y ?? 0
|
|
2680
3213
|
}));
|
|
2681
3214
|
const alpha = sim.alpha();
|
|
2682
|
-
const settled = alpha <
|
|
3215
|
+
const settled = alpha < DEFAULT_ALPHA_MIN || tickCount >= maxTicks;
|
|
2683
3216
|
this.tickCb?.(positions, alpha);
|
|
2684
3217
|
if (settled) {
|
|
2685
3218
|
this.settledCb?.();
|
|
@@ -2687,94 +3220,157 @@ var SimulationManager = class _SimulationManager {
|
|
|
2687
3220
|
this.syncRafId = requestAnimationFrame(runBatch);
|
|
2688
3221
|
}
|
|
2689
3222
|
};
|
|
3223
|
+
const start = this.syncWarmupPending ? runWarmup : runBatch;
|
|
2690
3224
|
if (deferred) {
|
|
2691
|
-
queueMicrotask(
|
|
3225
|
+
queueMicrotask(start);
|
|
2692
3226
|
} else {
|
|
2693
|
-
|
|
3227
|
+
start();
|
|
2694
3228
|
}
|
|
2695
3229
|
}
|
|
2696
3230
|
};
|
|
2697
3231
|
|
|
2698
3232
|
// src/graph/spatial-index.ts
|
|
2699
|
-
var
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
3233
|
+
var SpatialIndex2 = class extends SpatialIndex {
|
|
3234
|
+
};
|
|
3235
|
+
|
|
3236
|
+
// src/graph/update-diff-config.ts
|
|
3237
|
+
function stableKey(c2) {
|
|
3238
|
+
return JSON.stringify([
|
|
3239
|
+
c2.chargeStrength,
|
|
3240
|
+
c2.linkDistance,
|
|
3241
|
+
c2.clustering ? [c2.clustering.field, c2.clustering.strength] : null,
|
|
3242
|
+
c2.alphaDecay,
|
|
3243
|
+
c2.velocityDecay,
|
|
3244
|
+
c2.collisionRadius,
|
|
3245
|
+
c2.collisionPadding ?? null,
|
|
3246
|
+
c2.linkStrength ?? null,
|
|
3247
|
+
c2.centerForce ?? null,
|
|
3248
|
+
c2.seed ?? null,
|
|
3249
|
+
c2.warmupTicks ?? null,
|
|
3250
|
+
c2.warmupBudgetMs ?? null
|
|
3251
|
+
]);
|
|
3252
|
+
}
|
|
3253
|
+
function simulationConfigEqual(a2, b) {
|
|
3254
|
+
return stableKey(a2) === stableKey(b);
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
// src/graph/update-diff.ts
|
|
3258
|
+
var JITTER = 8;
|
|
3259
|
+
function jitter(id, seed) {
|
|
3260
|
+
const jx = hash32(`${id}|${seed}|jx`) / 4294967296 * 2 - 1;
|
|
3261
|
+
const jy = hash32(`${id}|${seed}|jy`) / 4294967296 * 2 - 1;
|
|
3262
|
+
return { x: jx * JITTER, y: jy * JITTER };
|
|
3263
|
+
}
|
|
3264
|
+
var FALLBACK_R0 = 300;
|
|
3265
|
+
function seededDisc(id, seed) {
|
|
3266
|
+
const u1 = hash32(`${id}|${seed}|r`) / 4294967296;
|
|
3267
|
+
const u2 = hash32(`${id}|${seed}|t`) / 4294967296;
|
|
3268
|
+
const r = FALLBACK_R0 * Math.sqrt(u1);
|
|
3269
|
+
const theta = u2 * Math.PI * 2;
|
|
3270
|
+
return { x: r * Math.cos(theta), y: r * Math.sin(theta) };
|
|
3271
|
+
}
|
|
3272
|
+
function diffGraphUpdate(prevNodes, prevEdges, next, prevConfig, seed) {
|
|
3273
|
+
const prevIds = new Set(prevNodes.map((n) => n.id));
|
|
3274
|
+
const nextIds = new Set(next.nodes.map((n) => n.id));
|
|
3275
|
+
const survivingPositions = /* @__PURE__ */ new Map();
|
|
3276
|
+
const enteringIds = [];
|
|
3277
|
+
for (const n of prevNodes) {
|
|
3278
|
+
if (nextIds.has(n.id)) survivingPositions.set(n.id, { x: n.x, y: n.y });
|
|
3279
|
+
}
|
|
3280
|
+
for (const n of next.nodes) {
|
|
3281
|
+
if (!prevIds.has(n.id)) enteringIds.push(n.id);
|
|
3282
|
+
}
|
|
3283
|
+
const exitingNodes = prevNodes.filter((n) => !nextIds.has(n.id));
|
|
3284
|
+
const nextEdgeKeys = new Set(next.edges.map((e) => `${e.source} ${e.target}`));
|
|
3285
|
+
const exitingEdges = prevEdges.filter((e) => !nextEdgeKeys.has(`${e.source} ${e.target}`));
|
|
3286
|
+
const prevEdgeCounts = /* @__PURE__ */ new Map();
|
|
3287
|
+
for (const e of prevEdges) {
|
|
3288
|
+
const k = `${e.source} ${e.target}`;
|
|
3289
|
+
prevEdgeCounts.set(k, (prevEdgeCounts.get(k) ?? 0) + 1);
|
|
3290
|
+
}
|
|
3291
|
+
let enteringEdgeCount = 0;
|
|
3292
|
+
const remainingPrev = new Map(prevEdgeCounts);
|
|
3293
|
+
for (const e of next.edges) {
|
|
3294
|
+
const k = `${e.source} ${e.target}`;
|
|
3295
|
+
const c2 = remainingPrev.get(k);
|
|
3296
|
+
if (c2 && c2 > 0) {
|
|
3297
|
+
remainingPrev.set(k, c2 - 1);
|
|
3298
|
+
} else {
|
|
3299
|
+
enteringEdgeCount++;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
const sameNodes = prevIds.size === nextIds.size && enteringIds.length === 0;
|
|
3303
|
+
const sameEdges = edgeSetsEqual(prevEdges, next.edges);
|
|
3304
|
+
const visualOnly = sameNodes && sameEdges && simulationConfigEqual(prevConfig, next.simulationConfig);
|
|
3305
|
+
const spawnPositions = /* @__PURE__ */ new Map();
|
|
3306
|
+
if (enteringIds.length > 0) {
|
|
3307
|
+
const enteringSet = new Set(enteringIds);
|
|
3308
|
+
const nextAdjacency = buildAdjacency(next.edges);
|
|
3309
|
+
for (const id of enteringIds) {
|
|
3310
|
+
const neighborPos = firstSurvivingNeighborPos(
|
|
3311
|
+
id,
|
|
3312
|
+
nextAdjacency,
|
|
3313
|
+
survivingPositions,
|
|
3314
|
+
enteringSet
|
|
3315
|
+
);
|
|
3316
|
+
const j = jitter(id, seed);
|
|
3317
|
+
if (neighborPos) {
|
|
3318
|
+
spawnPositions.set(id, { x: neighborPos.x + j.x, y: neighborPos.y + j.y });
|
|
3319
|
+
} else {
|
|
3320
|
+
const disc = seededDisc(id, seed);
|
|
3321
|
+
spawnPositions.set(id, { x: disc.x + j.x, y: disc.y + j.y });
|
|
3322
|
+
}
|
|
2710
3323
|
}
|
|
2711
|
-
this.tree = quadtree().x((d) => d.x).y((d) => d.y).addAll(nodes);
|
|
2712
|
-
this.generation++;
|
|
2713
3324
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
3325
|
+
return {
|
|
3326
|
+
visualOnly,
|
|
3327
|
+
enteringIds,
|
|
3328
|
+
survivingPositions,
|
|
3329
|
+
spawnPositions,
|
|
3330
|
+
exitingNodes,
|
|
3331
|
+
exitingEdges,
|
|
3332
|
+
enteringEdgeCount
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
function buildAdjacency(edges) {
|
|
3336
|
+
const map = /* @__PURE__ */ new Map();
|
|
3337
|
+
const push = (a2, b) => {
|
|
3338
|
+
const list = map.get(a2);
|
|
3339
|
+
if (list) list.push(b);
|
|
3340
|
+
else map.set(a2, [b]);
|
|
3341
|
+
};
|
|
3342
|
+
for (const e of edges) {
|
|
3343
|
+
push(e.source, e.target);
|
|
3344
|
+
push(e.target, e.source);
|
|
2717
3345
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
let best = null;
|
|
2728
|
-
let bestEffectiveDist = maxDistance + this.maxRadius + 1;
|
|
2729
|
-
this.tree.visit((node, x0, y0, x1, y1) => {
|
|
2730
|
-
const closestX = Math.max(x0, Math.min(x3, x1));
|
|
2731
|
-
const closestY = Math.max(y0, Math.min(y3, y1));
|
|
2732
|
-
const quadDist = Math.hypot(closestX - x3, closestY - y3);
|
|
2733
|
-
if (quadDist > searchRadius) return true;
|
|
2734
|
-
if (!node.length) {
|
|
2735
|
-
let current = node;
|
|
2736
|
-
do {
|
|
2737
|
-
const d = current.data;
|
|
2738
|
-
if (d) {
|
|
2739
|
-
const dist = Math.hypot(d.x - x3, d.y - y3);
|
|
2740
|
-
const effectiveDist = Math.max(0, dist - d.radius);
|
|
2741
|
-
if (effectiveDist <= maxDistance && effectiveDist < bestEffectiveDist) {
|
|
2742
|
-
bestEffectiveDist = effectiveDist;
|
|
2743
|
-
best = d;
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
} while (current = current.next);
|
|
2747
|
-
}
|
|
2748
|
-
return false;
|
|
2749
|
-
});
|
|
2750
|
-
return best;
|
|
2751
|
-
}
|
|
2752
|
-
/** Find all nodes whose centers fall within the given rectangle. */
|
|
2753
|
-
findInRect(x1, y1, x22, y22) {
|
|
2754
|
-
if (!this.tree) return [];
|
|
2755
|
-
const minX = Math.min(x1, x22);
|
|
2756
|
-
const minY = Math.min(y1, y22);
|
|
2757
|
-
const maxX = Math.max(x1, x22);
|
|
2758
|
-
const maxY = Math.max(y1, y22);
|
|
2759
|
-
const results = [];
|
|
2760
|
-
this.tree.visit((node, qx0, qy0, qx1, qy1) => {
|
|
2761
|
-
if (qx0 > maxX || qx1 < minX || qy0 > maxY || qy1 < minY) {
|
|
2762
|
-
return true;
|
|
2763
|
-
}
|
|
2764
|
-
if (!node.length) {
|
|
2765
|
-
let current = node;
|
|
2766
|
-
do {
|
|
2767
|
-
const d = current.data;
|
|
2768
|
-
if (d && d.x >= minX && d.x <= maxX && d.y >= minY && d.y <= maxY) {
|
|
2769
|
-
results.push(d);
|
|
2770
|
-
}
|
|
2771
|
-
} while (current = current.next);
|
|
2772
|
-
}
|
|
2773
|
-
return false;
|
|
2774
|
-
});
|
|
2775
|
-
return results;
|
|
3346
|
+
return map;
|
|
3347
|
+
}
|
|
3348
|
+
function firstSurvivingNeighborPos(id, adjacency, survivingPositions, enteringSet) {
|
|
3349
|
+
const neighbors = adjacency.get(id);
|
|
3350
|
+
if (!neighbors) return null;
|
|
3351
|
+
for (const nid of neighbors) {
|
|
3352
|
+
if (enteringSet.has(nid)) continue;
|
|
3353
|
+
const pos = survivingPositions.get(nid);
|
|
3354
|
+
if (pos) return pos;
|
|
2776
3355
|
}
|
|
2777
|
-
|
|
3356
|
+
return null;
|
|
3357
|
+
}
|
|
3358
|
+
function edgeSetsEqual(prev, next) {
|
|
3359
|
+
if (prev.length !== next.length) return false;
|
|
3360
|
+
const key = (e) => `${e.source}->${e.target}`;
|
|
3361
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3362
|
+
for (const e of prev) {
|
|
3363
|
+
const k = key(e);
|
|
3364
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
3365
|
+
}
|
|
3366
|
+
for (const e of next) {
|
|
3367
|
+
const k = key(e);
|
|
3368
|
+
const c2 = counts.get(k);
|
|
3369
|
+
if (!c2) return false;
|
|
3370
|
+
counts.set(k, c2 - 1);
|
|
3371
|
+
}
|
|
3372
|
+
return true;
|
|
3373
|
+
}
|
|
2778
3374
|
|
|
2779
3375
|
// src/graph-mount.ts
|
|
2780
3376
|
function resolveDarkMode2(mode) {
|
|
@@ -2785,6 +3381,10 @@ function resolveDarkMode2(mode) {
|
|
|
2785
3381
|
}
|
|
2786
3382
|
return false;
|
|
2787
3383
|
}
|
|
3384
|
+
var SPRINGY_DRAG_MAX_NODES = 5e3;
|
|
3385
|
+
var CURSOR_FORCE_MAX_NODES = 2e3;
|
|
3386
|
+
var CURSOR_POINTER_THROTTLE_MS = 33;
|
|
3387
|
+
var FOLLOW_SETTLE_ALPHA = 0.05;
|
|
2788
3388
|
function createGraph(container, spec, options) {
|
|
2789
3389
|
let currentSpec = spec;
|
|
2790
3390
|
let compilation;
|
|
@@ -2793,9 +3393,10 @@ function createGraph(container, spec, options) {
|
|
|
2793
3393
|
let canvas = null;
|
|
2794
3394
|
let chromeEl = null;
|
|
2795
3395
|
let legendEl = null;
|
|
3396
|
+
let legendController = null;
|
|
2796
3397
|
let renderer = null;
|
|
2797
3398
|
let simulation = null;
|
|
2798
|
-
const spatialIndex = new
|
|
3399
|
+
const spatialIndex = new SpatialIndex2();
|
|
2799
3400
|
let interactionManager = null;
|
|
2800
3401
|
const searchManager = new GraphSearchManager();
|
|
2801
3402
|
let tooltipManager = null;
|
|
@@ -2812,8 +3413,31 @@ function createGraph(container, spec, options) {
|
|
|
2812
3413
|
let animFrameId = null;
|
|
2813
3414
|
let needsRender = false;
|
|
2814
3415
|
let isGesturing = false;
|
|
3416
|
+
const scheduler = new AnimationScheduler(() => scheduleRender());
|
|
2815
3417
|
let gestureTimeout = null;
|
|
2816
3418
|
let lastEdgeHitTime = 0;
|
|
3419
|
+
let lastPointerFeedTime = 0;
|
|
3420
|
+
let activeFlight = null;
|
|
3421
|
+
let activeFollow = null;
|
|
3422
|
+
let lastAlpha = 1;
|
|
3423
|
+
let cameraChangePending = false;
|
|
3424
|
+
let highlightSet = null;
|
|
3425
|
+
let highlightDimOpacity = null;
|
|
3426
|
+
let activeCategories = /* @__PURE__ */ new Set();
|
|
3427
|
+
let nodeCategory = /* @__PURE__ */ new Map();
|
|
3428
|
+
let focusTransition = null;
|
|
3429
|
+
let focusAnim = null;
|
|
3430
|
+
let hoverRadiusTween = null;
|
|
3431
|
+
let entranceProgress = 1;
|
|
3432
|
+
let entranceActive = false;
|
|
3433
|
+
let entranceStagger = false;
|
|
3434
|
+
let entranceOrderMap = null;
|
|
3435
|
+
let entranceOffsetMap = null;
|
|
3436
|
+
let entranceFitInFlight = false;
|
|
3437
|
+
let entranceReveal = null;
|
|
3438
|
+
let suppressEntranceOnce = options?.suppressEntrance ?? false;
|
|
3439
|
+
let enterAlphaMap = null;
|
|
3440
|
+
let exitingGhosts = null;
|
|
2817
3441
|
function markGesture() {
|
|
2818
3442
|
isGesturing = true;
|
|
2819
3443
|
if (gestureTimeout !== null) clearTimeout(gestureTimeout);
|
|
@@ -2846,6 +3470,14 @@ function createGraph(container, spec, options) {
|
|
|
2846
3470
|
function buildDataMaps() {
|
|
2847
3471
|
nodeDataMap = new Map(compilation.nodes.map((n) => [n.id, n.data ?? {}]));
|
|
2848
3472
|
edgeDataMap = new Map(compilation.edges.map((e) => [`${e.source}->${e.target}`, e.data ?? {}]));
|
|
3473
|
+
nodeCategory = /* @__PURE__ */ new Map();
|
|
3474
|
+
const field = compilation.legendField;
|
|
3475
|
+
if (field) {
|
|
3476
|
+
for (const n of compilation.nodes) {
|
|
3477
|
+
const v = n.data?.[field];
|
|
3478
|
+
if (v != null) nodeCategory.set(n.id, String(v));
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
2849
3481
|
}
|
|
2850
3482
|
function buildAdjacencyMap(edges) {
|
|
2851
3483
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -2940,6 +3572,7 @@ function createGraph(container, spec, options) {
|
|
|
2940
3572
|
wrapper.appendChild(legendEl);
|
|
2941
3573
|
}
|
|
2942
3574
|
container.appendChild(wrapper);
|
|
3575
|
+
syncChromeInset();
|
|
2943
3576
|
const canvasHeight = Math.max(height, 200);
|
|
2944
3577
|
renderer = new GraphCanvasRenderer(canvas);
|
|
2945
3578
|
renderer.resize(width, canvasHeight);
|
|
@@ -2948,10 +3581,10 @@ function createGraph(container, spec, options) {
|
|
|
2948
3581
|
if (!chromeEl) return;
|
|
2949
3582
|
let html = "";
|
|
2950
3583
|
if (compilation.chrome.title) {
|
|
2951
|
-
html += `<h2 class="oc-title">${
|
|
3584
|
+
html += `<h2 class="oc-title">${escapeHtml2(compilation.chrome.title.text)}</h2>`;
|
|
2952
3585
|
}
|
|
2953
3586
|
if (compilation.chrome.subtitle) {
|
|
2954
|
-
html += `<p class="oc-subtitle">${
|
|
3587
|
+
html += `<p class="oc-subtitle">${escapeHtml2(compilation.chrome.subtitle.text)}</p>`;
|
|
2955
3588
|
}
|
|
2956
3589
|
chromeEl.innerHTML = html;
|
|
2957
3590
|
if (!html) {
|
|
@@ -2960,27 +3593,68 @@ function createGraph(container, spec, options) {
|
|
|
2960
3593
|
chromeEl.style.display = "";
|
|
2961
3594
|
}
|
|
2962
3595
|
}
|
|
3596
|
+
function legendConfig() {
|
|
3597
|
+
const l = options?.legend;
|
|
3598
|
+
if (l && typeof l === "object") {
|
|
3599
|
+
return { interactive: l.interactive ?? true, counts: l.counts ?? true };
|
|
3600
|
+
}
|
|
3601
|
+
return { interactive: true, counts: true };
|
|
3602
|
+
}
|
|
3603
|
+
function legendViewData() {
|
|
3604
|
+
return { nodes: getLegend().nodes, edges: getLegend().edges };
|
|
3605
|
+
}
|
|
2963
3606
|
function renderLegend2() {
|
|
2964
3607
|
if (!legendEl) return;
|
|
2965
|
-
const
|
|
2966
|
-
if (
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3608
|
+
const cfg = legendConfig();
|
|
3609
|
+
if (!legendController) {
|
|
3610
|
+
legendController = createGraphLegend(legendEl, legendViewData(), {
|
|
3611
|
+
interactive: cfg.interactive,
|
|
3612
|
+
counts: cfg.counts,
|
|
3613
|
+
onToggle: (value) => toggleLegendCategory(value),
|
|
3614
|
+
onHover: (value) => {
|
|
3615
|
+
const field = compilation.legendField;
|
|
3616
|
+
options?.onLegendHover?.(value !== null && field ? { field, value } : null);
|
|
3617
|
+
}
|
|
3618
|
+
});
|
|
3619
|
+
} else {
|
|
3620
|
+
legendController.update(legendViewData());
|
|
2977
3621
|
}
|
|
2978
|
-
legendEl.innerHTML = html;
|
|
2979
3622
|
}
|
|
2980
|
-
function
|
|
3623
|
+
function syncLegendActiveState() {
|
|
3624
|
+
if (legendController) legendController.update(legendViewData());
|
|
3625
|
+
syncChromeInset();
|
|
3626
|
+
}
|
|
3627
|
+
function syncChromeInset() {
|
|
3628
|
+
if (!chromeEl) return;
|
|
3629
|
+
const legendW = legendEl?.offsetWidth ?? 0;
|
|
3630
|
+
chromeEl.style.right = legendW > 0 ? `${legendW + 24}px` : "";
|
|
3631
|
+
}
|
|
3632
|
+
function chromeInsetTop() {
|
|
3633
|
+
if (!chromeEl || chromeEl.style.display === "none") return 0;
|
|
3634
|
+
return chromeEl.offsetHeight;
|
|
3635
|
+
}
|
|
3636
|
+
function springyDragEnabled() {
|
|
3637
|
+
return compilation.interaction.springyDrag && compilation.nodes.length <= SPRINGY_DRAG_MAX_NODES;
|
|
3638
|
+
}
|
|
3639
|
+
function cursorForceEnabled() {
|
|
3640
|
+
return compilation.interaction.cursorRepulsion !== null && compilation.nodes.length <= CURSOR_FORCE_MAX_NODES && !prefersReducedMotion();
|
|
3641
|
+
}
|
|
3642
|
+
function initSimulation(opts) {
|
|
2981
3643
|
const simNodes = toSimNodes(compilation.nodes);
|
|
2982
3644
|
const simEdges = toSimEdges(compilation.edges);
|
|
2983
3645
|
const config = compilation.simulationConfig;
|
|
3646
|
+
if (opts?.positions) {
|
|
3647
|
+
seedNodePositions(simNodes, config.seed ?? 0);
|
|
3648
|
+
for (const n of simNodes) {
|
|
3649
|
+
const p = opts.positions.get(n.id);
|
|
3650
|
+
if (p) {
|
|
3651
|
+
n.x = p.x;
|
|
3652
|
+
n.y = p.y;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
} else {
|
|
3656
|
+
seedNodePositions(simNodes, config.seed ?? 0);
|
|
3657
|
+
}
|
|
2984
3658
|
simulation = SimulationManager.create(simNodes, simEdges, {
|
|
2985
3659
|
chargeStrength: config.chargeStrength,
|
|
2986
3660
|
linkDistance: config.linkDistance,
|
|
@@ -2990,19 +3664,30 @@ function createGraph(container, spec, options) {
|
|
|
2990
3664
|
collisionRadius: config.collisionRadius,
|
|
2991
3665
|
collisionPadding: config.collisionPadding,
|
|
2992
3666
|
linkStrength: config.linkStrength,
|
|
2993
|
-
|
|
3667
|
+
// Update sims suppress the (non-alpha-scaled) center force to avoid a
|
|
3668
|
+
// global jump on tick 1; the alpha-scaled forceX/forceY gravity still
|
|
3669
|
+
// holds the layout centered.
|
|
3670
|
+
centerForce: opts?.suppressCenter ? false : config.centerForce,
|
|
3671
|
+
warmupTicks: opts?.skipWarmup ? 0 : config.warmupTicks,
|
|
3672
|
+
warmupBudgetMs: config.warmupBudgetMs,
|
|
3673
|
+
initialAlpha: opts?.initialAlpha ?? config.initialAlpha,
|
|
3674
|
+
// Cursor force radius/strength (null when disabled or gated off by node
|
|
3675
|
+
// count). The mount only feeds pointer positions when the same gate holds.
|
|
3676
|
+
cursorRepulsion: cursorForceEnabled() ? compilation.interaction.cursorRepulsion : null
|
|
2994
3677
|
});
|
|
3678
|
+
lastAlpha = opts?.initialAlpha ?? 1;
|
|
2995
3679
|
let initialSettleDone = false;
|
|
2996
|
-
let initialFitDone = false;
|
|
2997
|
-
simulation.onTick((positions,
|
|
3680
|
+
let initialFitDone = opts?.skipEntrance ?? false;
|
|
3681
|
+
simulation.onTick((positions, alpha) => {
|
|
2998
3682
|
if (destroyed) return;
|
|
3683
|
+
lastAlpha = alpha;
|
|
2999
3684
|
const posMap = /* @__PURE__ */ new Map();
|
|
3000
3685
|
for (const p of positions) {
|
|
3001
3686
|
posMap.set(p.id, { x: p.x, y: p.y });
|
|
3002
3687
|
}
|
|
3003
|
-
positionedNodes = compilation.nodes.map((node) => {
|
|
3688
|
+
positionedNodes = compilation.nodes.map((node, index2) => {
|
|
3004
3689
|
const pos = posMap.get(node.id) ?? { x: 0, y: 0 };
|
|
3005
|
-
return { ...node, x: pos.x, y: pos.y };
|
|
3690
|
+
return { ...node, x: pos.x, y: pos.y, index: index2 };
|
|
3006
3691
|
});
|
|
3007
3692
|
positionedEdges = compilation.edges.map((edge) => {
|
|
3008
3693
|
const src = posMap.get(edge.source) ?? { x: 0, y: 0 };
|
|
@@ -3016,11 +3701,11 @@ function createGraph(container, spec, options) {
|
|
|
3016
3701
|
};
|
|
3017
3702
|
});
|
|
3018
3703
|
spatialIndex.rebuild(positionedNodes);
|
|
3019
|
-
if (!initialFitDone && positionedNodes.length > 0 && interactionManager) {
|
|
3704
|
+
if (!initialFitDone && positionedNodes.length > 0 && interactionManager && options?.fitOnLoad !== false) {
|
|
3705
|
+
initialFitDone = true;
|
|
3706
|
+
startEntrance();
|
|
3707
|
+
} else if (!initialFitDone && options?.fitOnLoad === false) {
|
|
3020
3708
|
initialFitDone = true;
|
|
3021
|
-
const { width: cw, height: ch } = getCanvasDimensions();
|
|
3022
|
-
const { transform: fitTransform } = ZoomTransform.fitBounds(positionedNodes, cw, ch);
|
|
3023
|
-
interactionManager.setTransform(fitTransform);
|
|
3024
3709
|
}
|
|
3025
3710
|
needsRender = true;
|
|
3026
3711
|
scheduleRender();
|
|
@@ -3030,6 +3715,65 @@ function createGraph(container, spec, options) {
|
|
|
3030
3715
|
initialSettleDone = true;
|
|
3031
3716
|
});
|
|
3032
3717
|
}
|
|
3718
|
+
function computeInitialFit() {
|
|
3719
|
+
const { width: cw, height: ch } = getCanvasDimensions();
|
|
3720
|
+
const warmed = (compilation.simulationConfig.warmupTicks ?? 0) > 0;
|
|
3721
|
+
const { transform } = ZoomTransform.fitBounds(positionedNodes, cw, ch, void 0, {
|
|
3722
|
+
spread: !warmed,
|
|
3723
|
+
insetTop: chromeInsetTop()
|
|
3724
|
+
});
|
|
3725
|
+
return transform;
|
|
3726
|
+
}
|
|
3727
|
+
function startEntrance() {
|
|
3728
|
+
if (!interactionManager) return;
|
|
3729
|
+
const fit = computeInitialFit();
|
|
3730
|
+
const enter = compilation.animation?.enter ?? null;
|
|
3731
|
+
const suppressed = suppressEntranceOnce;
|
|
3732
|
+
suppressEntranceOnce = false;
|
|
3733
|
+
if (suppressed || !enter || prefersReducedMotion()) {
|
|
3734
|
+
interactionManager.setTransform(fit);
|
|
3735
|
+
cameraChangePending = true;
|
|
3736
|
+
entranceActive = false;
|
|
3737
|
+
entranceProgress = 1;
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
const { width: cw, height: ch } = getCanvasDimensions();
|
|
3741
|
+
const pulledBack = fit.zoomAt(fit.k * 0.7, cw / 2, ch / 2);
|
|
3742
|
+
interactionManager.setTransform(pulledBack);
|
|
3743
|
+
cameraChangePending = true;
|
|
3744
|
+
entranceActive = true;
|
|
3745
|
+
entranceProgress = 0;
|
|
3746
|
+
entranceStagger = enter.stagger && positionedNodes.length <= ENTRANCE_STAGGER_MAX_NODES;
|
|
3747
|
+
if (entranceStagger) {
|
|
3748
|
+
entranceOrderMap = entranceOrder(positionedNodes);
|
|
3749
|
+
entranceOffsetMap = entranceOffsets(positionedNodes);
|
|
3750
|
+
} else {
|
|
3751
|
+
entranceOrderMap = null;
|
|
3752
|
+
entranceOffsetMap = null;
|
|
3753
|
+
}
|
|
3754
|
+
if (enter.cameraFit) {
|
|
3755
|
+
entranceFitInFlight = true;
|
|
3756
|
+
flyCamera(fit, { duration: enter.duration + 100 }, () => {
|
|
3757
|
+
entranceFitInFlight = false;
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
const ease = resolveEase(enter.ease);
|
|
3761
|
+
entranceReveal = createTween({
|
|
3762
|
+
duration: enter.duration,
|
|
3763
|
+
ease,
|
|
3764
|
+
apply: (t) => {
|
|
3765
|
+
entranceProgress = t;
|
|
3766
|
+
needsRender = true;
|
|
3767
|
+
},
|
|
3768
|
+
onDone: () => {
|
|
3769
|
+
entranceProgress = 1;
|
|
3770
|
+
entranceActive = false;
|
|
3771
|
+
entranceReveal = null;
|
|
3772
|
+
needsRender = true;
|
|
3773
|
+
}
|
|
3774
|
+
});
|
|
3775
|
+
scheduler.add(entranceReveal);
|
|
3776
|
+
}
|
|
3033
3777
|
function getCanvasDimensions() {
|
|
3034
3778
|
if (!canvas) return { width: 600, height: 400 };
|
|
3035
3779
|
const rect = canvas.getBoundingClientRect();
|
|
@@ -3042,26 +3786,274 @@ function createGraph(container, spec, options) {
|
|
|
3042
3786
|
if (animFrameId !== null || destroyed) return;
|
|
3043
3787
|
animFrameId = requestAnimationFrame(renderFrame);
|
|
3044
3788
|
}
|
|
3045
|
-
function
|
|
3789
|
+
function hoverConnectedSet(nodeId) {
|
|
3790
|
+
if (nodeId === null) return null;
|
|
3791
|
+
if (compilation.interaction.hoverMode === "category") {
|
|
3792
|
+
const cat = nodeCategory.get(nodeId);
|
|
3793
|
+
const set3 = /* @__PURE__ */ new Set([nodeId]);
|
|
3794
|
+
if (cat !== void 0) {
|
|
3795
|
+
for (const [id, c2] of nodeCategory) if (c2 === cat) set3.add(id);
|
|
3796
|
+
}
|
|
3797
|
+
return set3;
|
|
3798
|
+
}
|
|
3799
|
+
const set2 = /* @__PURE__ */ new Set([nodeId]);
|
|
3800
|
+
const neighbors = adjacencyMap.get(nodeId);
|
|
3801
|
+
if (neighbors) for (const nid of neighbors) set2.add(nid);
|
|
3802
|
+
return set2;
|
|
3803
|
+
}
|
|
3804
|
+
function standingSnapshot() {
|
|
3805
|
+
return composeStandingFocus(
|
|
3806
|
+
highlightSet,
|
|
3807
|
+
searchManager.getMatches(),
|
|
3808
|
+
selectedNodeIds,
|
|
3809
|
+
adjacencyMap
|
|
3810
|
+
);
|
|
3811
|
+
}
|
|
3812
|
+
function targetSnapshot() {
|
|
3813
|
+
return layerHoverFocus(standingSnapshot(), hoveredNodeId, hoverConnectedSet(hoveredNodeId));
|
|
3814
|
+
}
|
|
3815
|
+
function armFocus(now2) {
|
|
3816
|
+
const target = targetSnapshot();
|
|
3817
|
+
const hoverCfg = compilation.animation?.hover ?? null;
|
|
3818
|
+
const duration = hoverCfg && !prefersReducedMotion() ? hoverCfg.duration : 0;
|
|
3819
|
+
const ease = resolveEase(hoverCfg?.ease ?? "smooth");
|
|
3820
|
+
if (!focusTransition) {
|
|
3821
|
+
focusTransition = new FocusTransition(target, duration, ease, now2);
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
focusTransition.retarget(target, now2);
|
|
3825
|
+
if (focusAnim) scheduler.remove(focusAnim);
|
|
3826
|
+
focusAnim = {
|
|
3827
|
+
tick: (t) => {
|
|
3828
|
+
const running = focusTransition !== null && !focusTransition.isSettled(t);
|
|
3829
|
+
if (!running) focusAnim = null;
|
|
3830
|
+
return running;
|
|
3831
|
+
},
|
|
3832
|
+
finish: () => {
|
|
3833
|
+
focusAnim = null;
|
|
3834
|
+
},
|
|
3835
|
+
cancel: () => {
|
|
3836
|
+
focusAnim = null;
|
|
3837
|
+
}
|
|
3838
|
+
};
|
|
3839
|
+
scheduler.add(focusAnim);
|
|
3840
|
+
}
|
|
3841
|
+
function refreshFocus() {
|
|
3842
|
+
armFocus(performance.now());
|
|
3843
|
+
needsRender = true;
|
|
3844
|
+
scheduleRender();
|
|
3845
|
+
}
|
|
3846
|
+
function resolveHighlightTarget(target) {
|
|
3847
|
+
if ("nodeIds" in target) return new Set(target.nodeIds);
|
|
3848
|
+
if ("neighborsOf" in target) {
|
|
3849
|
+
const set3 = /* @__PURE__ */ new Set();
|
|
3850
|
+
if (target.includeSelf !== false) set3.add(target.neighborsOf);
|
|
3851
|
+
const neighbors = adjacencyMap.get(target.neighborsOf);
|
|
3852
|
+
if (neighbors) for (const nid of neighbors) set3.add(nid);
|
|
3853
|
+
return set3;
|
|
3854
|
+
}
|
|
3855
|
+
const values = new Set(
|
|
3856
|
+
Array.isArray(target.category.value) ? target.category.value : [target.category.value]
|
|
3857
|
+
);
|
|
3858
|
+
const field = target.category.field;
|
|
3859
|
+
const set2 = /* @__PURE__ */ new Set();
|
|
3860
|
+
for (const n of compilation.nodes) {
|
|
3861
|
+
const v = n.data?.[field];
|
|
3862
|
+
if (v != null && values.has(String(v))) set2.add(n.id);
|
|
3863
|
+
}
|
|
3864
|
+
return set2;
|
|
3865
|
+
}
|
|
3866
|
+
function categoryHighlightSet() {
|
|
3867
|
+
if (activeCategories.size === 0) return null;
|
|
3868
|
+
const set2 = /* @__PURE__ */ new Set();
|
|
3869
|
+
for (const [id, cat] of nodeCategory) if (activeCategories.has(cat)) set2.add(id);
|
|
3870
|
+
return set2;
|
|
3871
|
+
}
|
|
3872
|
+
function emitHighlightChange() {
|
|
3873
|
+
options?.onHighlightChange?.(highlightSet ? [...highlightSet] : null);
|
|
3874
|
+
}
|
|
3875
|
+
function applyInitialHighlight() {
|
|
3876
|
+
const init = compilation.initialHighlight;
|
|
3877
|
+
if (!init) return;
|
|
3878
|
+
activeCategories = new Set(init.values);
|
|
3879
|
+
highlightSet = categoryHighlightSet();
|
|
3880
|
+
highlightDimOpacity = null;
|
|
3881
|
+
}
|
|
3882
|
+
function startHoverRadiusTween(nodeId) {
|
|
3883
|
+
const hoverCfg = compilation.animation?.hover ?? null;
|
|
3884
|
+
if (nodeId === null || !hoverCfg || prefersReducedMotion()) {
|
|
3885
|
+
hoverRadiusTween = nodeId ? { nodeId, scale: 1.15 } : null;
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
hoverRadiusTween = { nodeId, scale: 1 };
|
|
3889
|
+
const ease = resolveEase(hoverCfg.ease);
|
|
3890
|
+
const tween = createTween({
|
|
3891
|
+
duration: hoverCfg.duration,
|
|
3892
|
+
ease,
|
|
3893
|
+
apply: (t) => {
|
|
3894
|
+
if (hoverRadiusTween?.nodeId === nodeId) hoverRadiusTween.scale = 1 + 0.15 * t;
|
|
3895
|
+
needsRender = true;
|
|
3896
|
+
},
|
|
3897
|
+
onDone: () => {
|
|
3898
|
+
if (hoverRadiusTween?.nodeId === nodeId) hoverRadiusTween.scale = 1.15;
|
|
3899
|
+
}
|
|
3900
|
+
});
|
|
3901
|
+
scheduler.add(tween);
|
|
3902
|
+
}
|
|
3903
|
+
function showNodeTooltip(nodeId) {
|
|
3904
|
+
if (!tooltipManager || !interactionManager) return;
|
|
3905
|
+
const defaults = compilation.tooltipDescriptors.get(nodeId);
|
|
3906
|
+
if (!defaults) return;
|
|
3907
|
+
const node = positionedNodes.find((n) => n.id === nodeId);
|
|
3908
|
+
if (!node) return;
|
|
3909
|
+
const screen = interactionManager.getTransform().graphToScreen(node.x, node.y);
|
|
3910
|
+
const formatter = tooltipFormatter();
|
|
3911
|
+
if (!formatter) {
|
|
3912
|
+
tooltipManager.show(defaults, screen.x, screen.y);
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
const result = formatter({ kind: "node", data: nodeDataById(nodeId) }, defaults);
|
|
3916
|
+
applyFormatterResult(result, screen.x, screen.y);
|
|
3917
|
+
}
|
|
3918
|
+
function showEdgeTooltip(edgeId, data, screenX, screenY) {
|
|
3919
|
+
if (!tooltipManager) return;
|
|
3920
|
+
const edge = compilation.edges.find((e) => `${e.source}->${e.target}` === edgeId);
|
|
3921
|
+
const defaults = edge ? buildEdgeTooltip(edge) : { title: edgeId, fields: [] };
|
|
3922
|
+
const formatter = tooltipFormatter();
|
|
3923
|
+
if (!formatter) {
|
|
3924
|
+
tooltipManager.show(defaults, screenX, screenY);
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3927
|
+
const result = formatter({ kind: "edge", data }, defaults);
|
|
3928
|
+
applyFormatterResult(result, screenX, screenY);
|
|
3929
|
+
}
|
|
3930
|
+
function tooltipFormatter() {
|
|
3931
|
+
const t = options?.tooltip;
|
|
3932
|
+
return t && typeof t === "object" ? t.formatter ?? null : null;
|
|
3933
|
+
}
|
|
3934
|
+
function applyFormatterResult(result, x3, y3) {
|
|
3935
|
+
if (!tooltipManager) return;
|
|
3936
|
+
if (result === null) {
|
|
3937
|
+
tooltipManager.hide();
|
|
3938
|
+
} else if (typeof result === "string") {
|
|
3939
|
+
tooltipManager.show({ text: result }, x3, y3);
|
|
3940
|
+
} else if (result instanceof HTMLElement) {
|
|
3941
|
+
tooltipManager.show({ element: result }, x3, y3);
|
|
3942
|
+
} else {
|
|
3943
|
+
tooltipManager.show(result, x3, y3);
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
function buildRenderState(now2) {
|
|
3947
|
+
const transform = interactionManager.getTransform();
|
|
3948
|
+
if (!focusTransition) armFocus(now2);
|
|
3949
|
+
const ft = focusTransition;
|
|
3950
|
+
const t = ft.progress(now2);
|
|
3951
|
+
const focus = t < 1 ? { t, prev: ft.prev, next: ft.next } : void 0;
|
|
3952
|
+
const settledNext = ft.next;
|
|
3953
|
+
const hoverRadiusScale = hoverRadiusTween ? /* @__PURE__ */ new Map([[hoverRadiusTween.nodeId, hoverRadiusTween.scale]]) : void 0;
|
|
3954
|
+
return {
|
|
3955
|
+
nodes: positionedNodes,
|
|
3956
|
+
edges: positionedEdges,
|
|
3957
|
+
transform: { x: transform.x, y: transform.y, k: transform.k },
|
|
3958
|
+
hoveredNodeId,
|
|
3959
|
+
hoveredEdgeId,
|
|
3960
|
+
selectedNodeIds,
|
|
3961
|
+
adjacencyMap,
|
|
3962
|
+
theme: compilation.theme,
|
|
3963
|
+
searchMatches: searchManager.getMatches(),
|
|
3964
|
+
isGesturing,
|
|
3965
|
+
watermark: compilation.watermark,
|
|
3966
|
+
focus: focus ?? { t: 1, prev: settledNext, next: settledNext },
|
|
3967
|
+
hoverRadiusScale,
|
|
3968
|
+
dimOpacity: highlightDimOpacity ?? compilation.interaction.dimOpacity,
|
|
3969
|
+
entrance: entranceActive && entranceProgress < 1 ? {
|
|
3970
|
+
t: entranceProgress,
|
|
3971
|
+
stagger: entranceStagger,
|
|
3972
|
+
order: entranceOrderMap ?? void 0,
|
|
3973
|
+
offsets: entranceOffsetMap ?? void 0
|
|
3974
|
+
} : void 0,
|
|
3975
|
+
enterAlpha: enterAlphaMap ?? void 0,
|
|
3976
|
+
exiting: exitingGhosts ?? void 0
|
|
3977
|
+
};
|
|
3978
|
+
}
|
|
3979
|
+
function renderFrame(now2) {
|
|
3046
3980
|
animFrameId = null;
|
|
3047
3981
|
if (destroyed || !renderer || !interactionManager) return;
|
|
3982
|
+
if (scheduler.tick(now2)) needsRender = true;
|
|
3048
3983
|
if (needsRender) {
|
|
3049
3984
|
needsRender = false;
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3985
|
+
renderer.render(buildRenderState(now2));
|
|
3986
|
+
}
|
|
3987
|
+
if (scheduler.active) scheduleRender();
|
|
3988
|
+
if (cameraChangePending) {
|
|
3989
|
+
cameraChangePending = false;
|
|
3990
|
+
const t = interactionManager.getTransform();
|
|
3991
|
+
options?.onCameraChange?.({ x: t.x, y: t.y, k: t.k });
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
function flyCamera(to, opts, onDone) {
|
|
3995
|
+
if (destroyed || !interactionManager) return;
|
|
3996
|
+
cancelFlight();
|
|
3997
|
+
const cameraCfg = compilation.animation?.camera ?? null;
|
|
3998
|
+
const resolveTarget = () => typeof to === "function" ? to() : to;
|
|
3999
|
+
const snap = cameraCfg === null || prefersReducedMotion() || opts?.duration === 0;
|
|
4000
|
+
if (snap) {
|
|
4001
|
+
interactionManager.setTransform(resolveTarget());
|
|
4002
|
+
cameraChangePending = true;
|
|
4003
|
+
needsRender = true;
|
|
4004
|
+
scheduleRender();
|
|
4005
|
+
onDone?.();
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
const { width, height } = getCanvasDimensions();
|
|
4009
|
+
const duration = opts?.duration ?? cameraCfg?.duration ?? "auto";
|
|
4010
|
+
const ease = opts?.ease ?? cameraCfg?.ease ?? "smooth";
|
|
4011
|
+
const heavy = positionedNodes.length > 1e3;
|
|
4012
|
+
const flight = createCameraFlight({
|
|
4013
|
+
from: interactionManager.getTransform(),
|
|
4014
|
+
to,
|
|
4015
|
+
viewport: { width, height },
|
|
4016
|
+
apply: (t) => {
|
|
4017
|
+
interactionManager.setTransform(t);
|
|
4018
|
+
isGesturing = heavy;
|
|
4019
|
+
cameraChangePending = true;
|
|
4020
|
+
needsRender = true;
|
|
4021
|
+
},
|
|
4022
|
+
onDone: () => {
|
|
4023
|
+
activeFlight = null;
|
|
4024
|
+
isGesturing = false;
|
|
4025
|
+
needsRender = true;
|
|
4026
|
+
if (typeof to === "function") startFollow(to);
|
|
4027
|
+
scheduleRender();
|
|
4028
|
+
onDone?.();
|
|
4029
|
+
},
|
|
4030
|
+
opts: { duration, ease }
|
|
4031
|
+
});
|
|
4032
|
+
activeFlight = flight;
|
|
4033
|
+
scheduler.add(flight);
|
|
4034
|
+
}
|
|
4035
|
+
function startFollow(target) {
|
|
4036
|
+
const follow = createCameraFollow({
|
|
4037
|
+
target,
|
|
4038
|
+
apply: (t) => {
|
|
4039
|
+
interactionManager.setTransform(t);
|
|
4040
|
+
cameraChangePending = true;
|
|
4041
|
+
needsRender = true;
|
|
4042
|
+
},
|
|
4043
|
+
isActive: () => !destroyed && lastAlpha >= FOLLOW_SETTLE_ALPHA
|
|
4044
|
+
});
|
|
4045
|
+
activeFollow = follow;
|
|
4046
|
+
scheduler.add(follow);
|
|
4047
|
+
}
|
|
4048
|
+
function cancelFlight() {
|
|
4049
|
+
if (activeFlight) {
|
|
4050
|
+
scheduler.remove(activeFlight);
|
|
4051
|
+
activeFlight = null;
|
|
4052
|
+
isGesturing = false;
|
|
4053
|
+
}
|
|
4054
|
+
if (activeFollow) {
|
|
4055
|
+
scheduler.remove(activeFollow);
|
|
4056
|
+
activeFollow = null;
|
|
3065
4057
|
}
|
|
3066
4058
|
}
|
|
3067
4059
|
function initInteraction() {
|
|
@@ -3071,37 +4063,33 @@ function createGraph(container, spec, options) {
|
|
|
3071
4063
|
}
|
|
3072
4064
|
interactionManager = new GraphInteractionManager(canvas, spatialIndex, {
|
|
3073
4065
|
onTransformChange(_transform) {
|
|
4066
|
+
cancelFlight();
|
|
3074
4067
|
markGesture();
|
|
4068
|
+
cameraChangePending = true;
|
|
3075
4069
|
needsRender = true;
|
|
3076
4070
|
scheduleRender();
|
|
3077
4071
|
},
|
|
3078
4072
|
onHoverChange(nodeId) {
|
|
4073
|
+
if (nodeId === hoveredNodeId) return;
|
|
3079
4074
|
hoveredNodeId = nodeId;
|
|
4075
|
+
armFocus(performance.now());
|
|
4076
|
+
startHoverRadiusTween(nodeId);
|
|
3080
4077
|
needsRender = true;
|
|
3081
4078
|
scheduleRender();
|
|
3082
|
-
if (nodeId) {
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
4079
|
+
if (nodeId && hoveredEdgeId) {
|
|
4080
|
+
hoveredEdgeId = null;
|
|
4081
|
+
options?.onEdgeHover?.(null);
|
|
4082
|
+
tooltipManager?.hide();
|
|
3086
4083
|
}
|
|
4084
|
+
options?.onNodeHover?.(nodeId ? nodeDataById(nodeId) : null);
|
|
3087
4085
|
if (nodeId && tooltipManager) {
|
|
3088
|
-
|
|
3089
|
-
hoveredEdgeId = null;
|
|
3090
|
-
options?.onEdgeHover?.(null);
|
|
3091
|
-
}
|
|
3092
|
-
const content = compilation.tooltipDescriptors.get(nodeId);
|
|
3093
|
-
if (content) {
|
|
3094
|
-
const node = positionedNodes.find((n) => n.id === nodeId);
|
|
3095
|
-
if (node && interactionManager) {
|
|
3096
|
-
const screen = interactionManager.getTransform().graphToScreen(node.x, node.y);
|
|
3097
|
-
tooltipManager.show(content, screen.x, screen.y);
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
4086
|
+
showNodeTooltip(nodeId);
|
|
3100
4087
|
} else if (!nodeId) {
|
|
3101
4088
|
tooltipManager?.hide();
|
|
3102
4089
|
}
|
|
3103
4090
|
},
|
|
3104
4091
|
onBackgroundHover(graphX, graphY, screenX, screenY) {
|
|
4092
|
+
if (hoveredNodeId) return;
|
|
3105
4093
|
const now2 = performance.now();
|
|
3106
4094
|
if (now2 - lastEdgeHitTime < 32) {
|
|
3107
4095
|
if (hoveredEdgeId) {
|
|
@@ -3124,14 +4112,7 @@ function createGraph(container, spec, options) {
|
|
|
3124
4112
|
if (edgeId) {
|
|
3125
4113
|
const data = edgeDataById(edgeId);
|
|
3126
4114
|
options?.onEdgeHover?.(data);
|
|
3127
|
-
if (tooltipManager && data)
|
|
3128
|
-
const fields = Object.entries(data).filter(([key]) => key !== "source" && key !== "target").filter(([, value]) => value != null).map(([key, value]) => ({
|
|
3129
|
-
label: key,
|
|
3130
|
-
value: typeof value === "number" ? value.toLocaleString() : String(value)
|
|
3131
|
-
}));
|
|
3132
|
-
const [source, target] = edgeId.split("->");
|
|
3133
|
-
tooltipManager.show({ title: `${source} \u2192 ${target}`, fields }, screenX, screenY);
|
|
3134
|
-
}
|
|
4115
|
+
if (tooltipManager && data) showEdgeTooltip(edgeId, data, screenX, screenY);
|
|
3135
4116
|
} else {
|
|
3136
4117
|
options?.onEdgeHover?.(null);
|
|
3137
4118
|
tooltipManager?.hide();
|
|
@@ -3140,6 +4121,7 @@ function createGraph(container, spec, options) {
|
|
|
3140
4121
|
},
|
|
3141
4122
|
onSelectionChange(nodeIds) {
|
|
3142
4123
|
selectedNodeIds = new Set(nodeIds);
|
|
4124
|
+
armFocus(performance.now());
|
|
3143
4125
|
needsRender = true;
|
|
3144
4126
|
scheduleRender();
|
|
3145
4127
|
options?.onSelectionChange?.(nodeIds);
|
|
@@ -3152,16 +4134,27 @@ function createGraph(container, spec, options) {
|
|
|
3152
4134
|
const node = positionedNodes.find((n) => n.id === nodeId);
|
|
3153
4135
|
const x3 = node?.x ?? 0;
|
|
3154
4136
|
const y3 = node?.y ?? 0;
|
|
3155
|
-
simulation?.pinNode(nodeId, x3, y3);
|
|
4137
|
+
simulation?.pinNode(nodeId, x3, y3, springyDragEnabled() ? 0.3 : void 0);
|
|
3156
4138
|
canvas?.classList.add("oc-graph-canvas--dragging");
|
|
3157
4139
|
},
|
|
3158
4140
|
onNodeDrag(nodeId, x3, y3) {
|
|
3159
4141
|
simulation?.dragNode(nodeId, x3, y3);
|
|
3160
4142
|
},
|
|
3161
4143
|
onNodeDragEnd(nodeId) {
|
|
3162
|
-
simulation?.unpinNode(nodeId);
|
|
4144
|
+
simulation?.unpinNode(nodeId, springyDragEnabled() ? 0 : void 0);
|
|
3163
4145
|
canvas?.classList.remove("oc-graph-canvas--dragging");
|
|
3164
4146
|
},
|
|
4147
|
+
onPointerMove(graphX, graphY) {
|
|
4148
|
+
if (!cursorForceEnabled()) return;
|
|
4149
|
+
const now2 = performance.now();
|
|
4150
|
+
if (now2 - lastPointerFeedTime < CURSOR_POINTER_THROTTLE_MS) return;
|
|
4151
|
+
lastPointerFeedTime = now2;
|
|
4152
|
+
simulation?.setPointer(graphX, graphY, true);
|
|
4153
|
+
},
|
|
4154
|
+
onPointerLeave() {
|
|
4155
|
+
if (!cursorForceEnabled()) return;
|
|
4156
|
+
simulation?.setPointer(0, 0, false);
|
|
4157
|
+
},
|
|
3165
4158
|
onDoubleClick(nodeId) {
|
|
3166
4159
|
options?.onNodeDoubleClick?.(nodeDataById(nodeId));
|
|
3167
4160
|
}
|
|
@@ -3191,9 +4184,7 @@ function createGraph(container, spec, options) {
|
|
|
3191
4184
|
const factor = direction === "in" ? 1.2 : 0.8;
|
|
3192
4185
|
const newK = t.k * factor;
|
|
3193
4186
|
const newTransform = t.zoomAt(newK, cw / 2, ch / 2);
|
|
3194
|
-
|
|
3195
|
-
needsRender = true;
|
|
3196
|
-
scheduleRender();
|
|
4187
|
+
flyCamera(newTransform, { duration: 200 });
|
|
3197
4188
|
},
|
|
3198
4189
|
onFitAll() {
|
|
3199
4190
|
zoomToFit();
|
|
@@ -3212,93 +4203,318 @@ function createGraph(container, spec, options) {
|
|
|
3212
4203
|
needsRender = true;
|
|
3213
4204
|
scheduleRender();
|
|
3214
4205
|
}
|
|
3215
|
-
function zoomToFit() {
|
|
4206
|
+
function zoomToFit(opts) {
|
|
3216
4207
|
if (destroyed || !interactionManager || positionedNodes.length === 0) return;
|
|
3217
4208
|
const { width: cw, height: ch } = getCanvasDimensions();
|
|
3218
|
-
const { transform: fitTransform } = ZoomTransform.fitBounds(
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
4209
|
+
const { transform: fitTransform } = ZoomTransform.fitBounds(
|
|
4210
|
+
positionedNodes,
|
|
4211
|
+
cw,
|
|
4212
|
+
ch,
|
|
4213
|
+
opts?.padding,
|
|
4214
|
+
{
|
|
4215
|
+
insetTop: chromeInsetTop()
|
|
4216
|
+
}
|
|
4217
|
+
);
|
|
4218
|
+
flyCamera(fitTransform, opts);
|
|
3222
4219
|
}
|
|
3223
|
-
function zoomToNode(nodeId) {
|
|
4220
|
+
function zoomToNode(nodeId, opts) {
|
|
3224
4221
|
if (destroyed || !interactionManager || !canvas) return;
|
|
3225
4222
|
const node = positionedNodes.find((n) => n.id === nodeId);
|
|
3226
4223
|
if (!node) return;
|
|
3227
4224
|
const { width: cw, height: ch } = getCanvasDimensions();
|
|
3228
|
-
const k = 2;
|
|
3229
|
-
const
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
4225
|
+
const k = clampK(opts?.scale ?? 2);
|
|
4226
|
+
const provider = () => {
|
|
4227
|
+
const live = positionedNodes.find((n) => n.id === nodeId) ?? node;
|
|
4228
|
+
return new ZoomTransform(cw / 2 - live.x * k, ch / 2 - live.y * k, k);
|
|
4229
|
+
};
|
|
4230
|
+
flyCamera(provider, opts);
|
|
4231
|
+
}
|
|
4232
|
+
function flyTo(target, opts) {
|
|
4233
|
+
if (destroyed || !interactionManager) return;
|
|
4234
|
+
const { width: cw, height: ch } = getCanvasDimensions();
|
|
4235
|
+
const k = clampK(target.k ?? interactionManager.getTransform().k);
|
|
4236
|
+
flyCamera(new ZoomTransform(cw / 2 - target.x * k, ch / 2 - target.y * k, k), opts);
|
|
4237
|
+
}
|
|
4238
|
+
function centerAt(x3, y3, opts) {
|
|
4239
|
+
flyTo({ x: x3, y: y3 }, opts);
|
|
3235
4240
|
}
|
|
3236
|
-
function
|
|
4241
|
+
function getCamera() {
|
|
4242
|
+
const t = interactionManager?.getTransform() ?? ZoomTransform.identity();
|
|
4243
|
+
return { x: t.x, y: t.y, k: t.k };
|
|
4244
|
+
}
|
|
4245
|
+
function selectNode(nodeId, opts) {
|
|
3237
4246
|
if (destroyed) return;
|
|
3238
4247
|
selectedNodeIds = /* @__PURE__ */ new Set([nodeId]);
|
|
3239
4248
|
needsRender = true;
|
|
3240
4249
|
scheduleRender();
|
|
3241
4250
|
options?.onSelectionChange?.([nodeId]);
|
|
4251
|
+
const shouldFly = opts?.fly ?? compilation.interaction.selectFlyTo;
|
|
4252
|
+
if (shouldFly) zoomToNode(nodeId, opts);
|
|
3242
4253
|
}
|
|
3243
4254
|
function getSelectedNodes() {
|
|
3244
4255
|
return [...selectedNodeIds];
|
|
3245
4256
|
}
|
|
4257
|
+
function getSearchMatches() {
|
|
4258
|
+
return [...searchManager.getMatches() ?? []];
|
|
4259
|
+
}
|
|
4260
|
+
function highlight(target, opts) {
|
|
4261
|
+
activeCategories = /* @__PURE__ */ new Set();
|
|
4262
|
+
highlightSet = resolveHighlightTarget(target);
|
|
4263
|
+
highlightDimOpacity = opts?.dimOpacity ?? null;
|
|
4264
|
+
syncLegendActiveState();
|
|
4265
|
+
refreshFocus();
|
|
4266
|
+
emitHighlightChange();
|
|
4267
|
+
}
|
|
4268
|
+
function clearHighlight() {
|
|
4269
|
+
activeCategories = /* @__PURE__ */ new Set();
|
|
4270
|
+
highlightSet = null;
|
|
4271
|
+
highlightDimOpacity = null;
|
|
4272
|
+
syncLegendActiveState();
|
|
4273
|
+
refreshFocus();
|
|
4274
|
+
emitHighlightChange();
|
|
4275
|
+
}
|
|
4276
|
+
function getHighlight() {
|
|
4277
|
+
return highlightSet ? [...highlightSet] : null;
|
|
4278
|
+
}
|
|
4279
|
+
function getLegend() {
|
|
4280
|
+
const nodeEntries = "entries" in compilation.legend ? compilation.legend.entries : [];
|
|
4281
|
+
return {
|
|
4282
|
+
field: compilation.legendField,
|
|
4283
|
+
nodes: nodeEntries.filter((e) => !e.overflow).map((e) => ({
|
|
4284
|
+
label: e.label,
|
|
4285
|
+
color: e.color,
|
|
4286
|
+
count: e.count,
|
|
4287
|
+
active: activeCategories.size === 0 || activeCategories.has(e.label)
|
|
4288
|
+
})),
|
|
4289
|
+
edges: (compilation.edgeLegend ?? []).map((e) => ({
|
|
4290
|
+
label: e.label,
|
|
4291
|
+
color: e.color,
|
|
4292
|
+
count: e.count
|
|
4293
|
+
}))
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
function setActiveCategories(values) {
|
|
4297
|
+
if (destroyed) return;
|
|
4298
|
+
activeCategories = new Set(values);
|
|
4299
|
+
highlightSet = categoryHighlightSet();
|
|
4300
|
+
highlightDimOpacity = null;
|
|
4301
|
+
syncLegendActiveState();
|
|
4302
|
+
refreshFocus();
|
|
4303
|
+
emitHighlightChange();
|
|
4304
|
+
}
|
|
4305
|
+
function getActiveCategories() {
|
|
4306
|
+
return [...activeCategories];
|
|
4307
|
+
}
|
|
4308
|
+
function toggleLegendCategory(value) {
|
|
4309
|
+
if (activeCategories.has(value)) activeCategories.delete(value);
|
|
4310
|
+
else activeCategories.add(value);
|
|
4311
|
+
highlightSet = categoryHighlightSet();
|
|
4312
|
+
highlightDimOpacity = null;
|
|
4313
|
+
syncLegendActiveState();
|
|
4314
|
+
refreshFocus();
|
|
4315
|
+
options?.onLegendToggle?.([...activeCategories]);
|
|
4316
|
+
emitHighlightChange();
|
|
4317
|
+
}
|
|
3246
4318
|
function doResize() {
|
|
3247
4319
|
if (destroyed || !canvas || !renderer || !wrapper) return;
|
|
3248
4320
|
const { width, height } = getContainerDimensions();
|
|
3249
4321
|
const canvasHeight = Math.max(height, 200);
|
|
3250
4322
|
renderer.resize(width, canvasHeight);
|
|
4323
|
+
syncChromeInset();
|
|
4324
|
+
if (entranceFitInFlight && interactionManager) {
|
|
4325
|
+
cancelFlight();
|
|
4326
|
+
entranceFitInFlight = false;
|
|
4327
|
+
interactionManager.setTransform(computeInitialFit());
|
|
4328
|
+
cameraChangePending = true;
|
|
4329
|
+
}
|
|
3251
4330
|
needsRender = true;
|
|
3252
4331
|
scheduleRender();
|
|
3253
4332
|
}
|
|
3254
4333
|
function update(newSpec) {
|
|
3255
4334
|
if (destroyed) return;
|
|
3256
4335
|
currentSpec = newSpec;
|
|
3257
|
-
|
|
4336
|
+
scheduler.finishAll();
|
|
4337
|
+
entranceActive = false;
|
|
4338
|
+
entranceProgress = 1;
|
|
4339
|
+
entranceFitInFlight = false;
|
|
4340
|
+
entranceReveal = null;
|
|
4341
|
+
enterAlphaMap = null;
|
|
4342
|
+
exitingGhosts = null;
|
|
4343
|
+
const prevNodes = positionedNodes;
|
|
4344
|
+
const prevEdges = positionedEdges;
|
|
4345
|
+
const prevConfig = compilation.simulationConfig;
|
|
3258
4346
|
compilation = compile();
|
|
4347
|
+
const diff = diffGraphUpdate(
|
|
4348
|
+
prevNodes,
|
|
4349
|
+
prevEdges,
|
|
4350
|
+
compilation,
|
|
4351
|
+
prevConfig,
|
|
4352
|
+
prevConfig.seed ?? 0
|
|
4353
|
+
);
|
|
4354
|
+
if (diff.visualOnly) {
|
|
4355
|
+
runVisualOnlyUpdate();
|
|
4356
|
+
return;
|
|
4357
|
+
}
|
|
4358
|
+
runStructuralUpdate(diff);
|
|
4359
|
+
}
|
|
4360
|
+
function runVisualOnlyUpdate() {
|
|
3259
4361
|
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
3260
4362
|
buildDataMaps();
|
|
3261
|
-
renderChrome3();
|
|
3262
|
-
renderLegend2();
|
|
3263
|
-
initSimulation();
|
|
3264
|
-
initInteraction();
|
|
3265
|
-
hoveredNodeId = null;
|
|
3266
|
-
hoveredEdgeId = null;
|
|
3267
|
-
selectedNodeIds = /* @__PURE__ */ new Set();
|
|
3268
|
-
searchManager.clearSearch();
|
|
3269
|
-
}
|
|
3270
|
-
function updateVisuals(newSpec) {
|
|
3271
|
-
if (destroyed) return;
|
|
3272
|
-
currentSpec = newSpec;
|
|
3273
4363
|
const posMap = /* @__PURE__ */ new Map();
|
|
3274
4364
|
for (const node of positionedNodes) {
|
|
3275
4365
|
posMap.set(node.id, { x: node.x, y: node.y });
|
|
3276
4366
|
}
|
|
3277
|
-
|
|
3278
|
-
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
3279
|
-
buildDataMaps();
|
|
3280
|
-
positionedNodes = compilation.nodes.map((node) => {
|
|
4367
|
+
positionedNodes = compilation.nodes.map((node, index2) => {
|
|
3281
4368
|
const pos = posMap.get(node.id) ?? { x: 0, y: 0 };
|
|
3282
|
-
return { ...node, x: pos.x, y: pos.y };
|
|
4369
|
+
return { ...node, x: pos.x, y: pos.y, index: index2 };
|
|
3283
4370
|
});
|
|
3284
4371
|
positionedEdges = compilation.edges.map((edge) => {
|
|
3285
4372
|
const src = posMap.get(edge.source) ?? { x: 0, y: 0 };
|
|
3286
4373
|
const tgt = posMap.get(edge.target) ?? { x: 0, y: 0 };
|
|
3287
|
-
return {
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
4374
|
+
return { ...edge, sourceX: src.x, sourceY: src.y, targetX: tgt.x, targetY: tgt.y };
|
|
4375
|
+
});
|
|
4376
|
+
spatialIndex.rebuild(positionedNodes);
|
|
4377
|
+
if (activeCategories.size > 0) {
|
|
4378
|
+
highlightSet = categoryHighlightSet();
|
|
4379
|
+
}
|
|
4380
|
+
reRunSearch();
|
|
4381
|
+
renderChrome3();
|
|
4382
|
+
renderLegend2();
|
|
4383
|
+
syncLegendActiveState();
|
|
4384
|
+
needsRender = true;
|
|
4385
|
+
scheduleRender();
|
|
4386
|
+
}
|
|
4387
|
+
function runStructuralUpdate(diff) {
|
|
4388
|
+
teardownSimOnly();
|
|
4389
|
+
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
4390
|
+
buildDataMaps();
|
|
4391
|
+
const positions = /* @__PURE__ */ new Map();
|
|
4392
|
+
for (const [id, p] of diff.survivingPositions) positions.set(id, p);
|
|
4393
|
+
for (const [id, p] of diff.spawnPositions) positions.set(id, p);
|
|
4394
|
+
const prevNodeCount = diff.survivingPositions.size + diff.exitingNodes.length;
|
|
4395
|
+
const nextNodeCount = diff.survivingPositions.size + diff.enteringIds.length;
|
|
4396
|
+
const nodeRatio = ratio(
|
|
4397
|
+
diff.enteringIds.length + diff.exitingNodes.length,
|
|
4398
|
+
Math.max(prevNodeCount, nextNodeCount)
|
|
4399
|
+
);
|
|
4400
|
+
const prevEdgeCount = compilation.edges.length - diff.enteringEdgeCount + diff.exitingEdges.length;
|
|
4401
|
+
const nextEdgeCount = compilation.edges.length;
|
|
4402
|
+
const edgeRatio = ratio(
|
|
4403
|
+
diff.enteringEdgeCount + diff.exitingEdges.length,
|
|
4404
|
+
Math.max(prevEdgeCount, nextEdgeCount)
|
|
4405
|
+
);
|
|
4406
|
+
const changeRatio = Math.max(nodeRatio, edgeRatio);
|
|
4407
|
+
const initialAlpha = Math.min(1, 0.3 + 0.7 * changeRatio);
|
|
4408
|
+
positionedNodes = compilation.nodes.map((node, index2) => {
|
|
4409
|
+
const pos = positions.get(node.id) ?? { x: 0, y: 0 };
|
|
4410
|
+
return { ...node, x: pos.x, y: pos.y, index: index2 };
|
|
4411
|
+
});
|
|
4412
|
+
positionedEdges = compilation.edges.map((edge) => {
|
|
4413
|
+
const src = positions.get(edge.source) ?? { x: 0, y: 0 };
|
|
4414
|
+
const tgt = positions.get(edge.target) ?? { x: 0, y: 0 };
|
|
4415
|
+
return { ...edge, sourceX: src.x, sourceY: src.y, targetX: tgt.x, targetY: tgt.y };
|
|
3294
4416
|
});
|
|
3295
4417
|
spatialIndex.rebuild(positionedNodes);
|
|
4418
|
+
initSimulation({
|
|
4419
|
+
positions,
|
|
4420
|
+
suppressCenter: true,
|
|
4421
|
+
initialAlpha,
|
|
4422
|
+
skipWarmup: true,
|
|
4423
|
+
skipEntrance: true
|
|
4424
|
+
});
|
|
3296
4425
|
renderChrome3();
|
|
3297
4426
|
renderLegend2();
|
|
4427
|
+
reconcileStateAfterUpdate();
|
|
4428
|
+
startUpdateTransitions(diff);
|
|
3298
4429
|
needsRender = true;
|
|
3299
4430
|
scheduleRender();
|
|
3300
4431
|
}
|
|
4432
|
+
function ratio(numerator, denominator) {
|
|
4433
|
+
return denominator > 0 ? numerator / denominator : 0;
|
|
4434
|
+
}
|
|
4435
|
+
function reconcileStateAfterUpdate() {
|
|
4436
|
+
const nextIds = new Set(compilation.nodes.map((n) => n.id));
|
|
4437
|
+
if (hoveredNodeId && !nextIds.has(hoveredNodeId)) hoveredNodeId = null;
|
|
4438
|
+
if (hoveredEdgeId) {
|
|
4439
|
+
const [src, tgt] = hoveredEdgeId.split("->");
|
|
4440
|
+
if (!nextIds.has(src) || !nextIds.has(tgt)) hoveredEdgeId = null;
|
|
4441
|
+
}
|
|
4442
|
+
const survivingSelection = [...selectedNodeIds].filter((id) => nextIds.has(id));
|
|
4443
|
+
selectedNodeIds = new Set(survivingSelection);
|
|
4444
|
+
interactionManager?.setSelection(survivingSelection);
|
|
4445
|
+
if (activeCategories.size > 0) {
|
|
4446
|
+
highlightSet = categoryHighlightSet();
|
|
4447
|
+
} else if (highlightSet) {
|
|
4448
|
+
highlightSet = new Set([...highlightSet].filter((id) => nextIds.has(id)));
|
|
4449
|
+
if (highlightSet.size === 0) highlightSet = null;
|
|
4450
|
+
}
|
|
4451
|
+
syncLegendActiveState();
|
|
4452
|
+
reRunSearch();
|
|
4453
|
+
focusTransition = null;
|
|
4454
|
+
armFocus(performance.now());
|
|
4455
|
+
}
|
|
4456
|
+
function reRunSearch() {
|
|
4457
|
+
const q = searchManager.getQuery();
|
|
4458
|
+
if (q !== null) searchManager.search(q, positionedNodes);
|
|
4459
|
+
}
|
|
4460
|
+
function startUpdateTransitions(diff) {
|
|
4461
|
+
const updateCfg = compilation.animation?.update ?? null;
|
|
4462
|
+
const exitCfg = compilation.animation?.exit ?? null;
|
|
4463
|
+
const reduced = prefersReducedMotion();
|
|
4464
|
+
if (diff.enteringIds.length > 0 && updateCfg && !reduced) {
|
|
4465
|
+
const entering = diff.enteringIds;
|
|
4466
|
+
enterAlphaMap = new Map(entering.map((id) => [id, 0]));
|
|
4467
|
+
const ease = resolveEase(updateCfg.ease);
|
|
4468
|
+
const tween = createTween({
|
|
4469
|
+
duration: updateCfg.duration,
|
|
4470
|
+
ease,
|
|
4471
|
+
apply: (t) => {
|
|
4472
|
+
const q = Math.round(t * 8) / 8;
|
|
4473
|
+
if (enterAlphaMap) for (const id of entering) enterAlphaMap.set(id, q);
|
|
4474
|
+
needsRender = true;
|
|
4475
|
+
},
|
|
4476
|
+
onDone: () => {
|
|
4477
|
+
enterAlphaMap = null;
|
|
4478
|
+
needsRender = true;
|
|
4479
|
+
}
|
|
4480
|
+
});
|
|
4481
|
+
scheduler.add(tween);
|
|
4482
|
+
}
|
|
4483
|
+
if ((diff.exitingNodes.length > 0 || diff.exitingEdges.length > 0) && exitCfg && !reduced) {
|
|
4484
|
+
exitingGhosts = { nodes: diff.exitingNodes, edges: diff.exitingEdges, alpha: 1 };
|
|
4485
|
+
const ease = resolveEase(exitCfg.ease);
|
|
4486
|
+
const tween = createTween({
|
|
4487
|
+
duration: exitCfg.duration,
|
|
4488
|
+
ease,
|
|
4489
|
+
apply: (t) => {
|
|
4490
|
+
if (exitingGhosts) exitingGhosts.alpha = 1 - t;
|
|
4491
|
+
needsRender = true;
|
|
4492
|
+
},
|
|
4493
|
+
onDone: () => {
|
|
4494
|
+
exitingGhosts = null;
|
|
4495
|
+
needsRender = true;
|
|
4496
|
+
}
|
|
4497
|
+
});
|
|
4498
|
+
scheduler.add(tween);
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
function updateVisuals(newSpec) {
|
|
4502
|
+
update(newSpec);
|
|
4503
|
+
}
|
|
4504
|
+
function teardownSimOnly() {
|
|
4505
|
+
simulation?.destroy();
|
|
4506
|
+
simulation = null;
|
|
4507
|
+
}
|
|
3301
4508
|
function teardownSubsystems() {
|
|
4509
|
+
scheduler.cancelAll();
|
|
4510
|
+
activeFlight = null;
|
|
4511
|
+
activeFollow = null;
|
|
4512
|
+
entranceReveal = null;
|
|
4513
|
+
entranceActive = false;
|
|
4514
|
+
entranceProgress = 1;
|
|
4515
|
+
entranceFitInFlight = false;
|
|
4516
|
+
enterAlphaMap = null;
|
|
4517
|
+
exitingGhosts = null;
|
|
3302
4518
|
if (animFrameId !== null) {
|
|
3303
4519
|
cancelAnimationFrame(animFrameId);
|
|
3304
4520
|
animFrameId = null;
|
|
@@ -3326,6 +4542,8 @@ function createGraph(container, spec, options) {
|
|
|
3326
4542
|
disconnectResize();
|
|
3327
4543
|
disconnectResize = null;
|
|
3328
4544
|
}
|
|
4545
|
+
legendController?.destroy();
|
|
4546
|
+
legendController = null;
|
|
3329
4547
|
if (wrapper?.parentNode) {
|
|
3330
4548
|
wrapper.parentNode.removeChild(wrapper);
|
|
3331
4549
|
}
|
|
@@ -3340,6 +4558,7 @@ function createGraph(container, spec, options) {
|
|
|
3340
4558
|
compilation = compile();
|
|
3341
4559
|
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
3342
4560
|
buildDataMaps();
|
|
4561
|
+
applyInitialHighlight();
|
|
3343
4562
|
createDOM();
|
|
3344
4563
|
initSimulation();
|
|
3345
4564
|
initInteraction();
|
|
@@ -3358,9 +4577,24 @@ function createGraph(container, spec, options) {
|
|
|
3358
4577
|
},
|
|
3359
4578
|
zoomToNode() {
|
|
3360
4579
|
},
|
|
4580
|
+
flyTo() {
|
|
4581
|
+
},
|
|
4582
|
+
centerAt() {
|
|
4583
|
+
},
|
|
4584
|
+
getCamera: () => ({ x: 0, y: 0, k: 1 }),
|
|
3361
4585
|
selectNode() {
|
|
3362
4586
|
},
|
|
3363
4587
|
getSelectedNodes: () => [],
|
|
4588
|
+
getSearchMatches: () => [],
|
|
4589
|
+
highlight() {
|
|
4590
|
+
},
|
|
4591
|
+
clearHighlight() {
|
|
4592
|
+
},
|
|
4593
|
+
getHighlight: () => null,
|
|
4594
|
+
getLegend: () => ({ field: null, nodes: [], edges: [] }),
|
|
4595
|
+
setActiveCategories() {
|
|
4596
|
+
},
|
|
4597
|
+
getActiveCategories: () => [],
|
|
3364
4598
|
resize() {
|
|
3365
4599
|
},
|
|
3366
4600
|
destroy() {
|
|
@@ -3379,13 +4613,23 @@ function createGraph(container, spec, options) {
|
|
|
3379
4613
|
clearSearch,
|
|
3380
4614
|
zoomToFit,
|
|
3381
4615
|
zoomToNode,
|
|
4616
|
+
flyTo,
|
|
4617
|
+
centerAt,
|
|
4618
|
+
getCamera,
|
|
3382
4619
|
selectNode,
|
|
3383
4620
|
getSelectedNodes,
|
|
4621
|
+
getSearchMatches,
|
|
4622
|
+
highlight,
|
|
4623
|
+
clearHighlight,
|
|
4624
|
+
getHighlight,
|
|
4625
|
+
getLegend,
|
|
4626
|
+
setActiveCategories,
|
|
4627
|
+
getActiveCategories,
|
|
3384
4628
|
resize: doResize,
|
|
3385
4629
|
destroy
|
|
3386
4630
|
};
|
|
3387
4631
|
}
|
|
3388
|
-
function
|
|
4632
|
+
function escapeHtml2(str) {
|
|
3389
4633
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3390
4634
|
}
|
|
3391
4635
|
|
|
@@ -5580,6 +6824,9 @@ function createTileMap(container, spec, options) {
|
|
|
5580
6824
|
};
|
|
5581
6825
|
}
|
|
5582
6826
|
export {
|
|
6827
|
+
CANVAS_DEFAULT_UPDATE_MAX_MARKS,
|
|
6828
|
+
DEFAULT_UPDATE_MAX_MARKS,
|
|
6829
|
+
VECTOR_EXPORT_MAX_POINTS,
|
|
5583
6830
|
attachKeyboardNav,
|
|
5584
6831
|
createBarList,
|
|
5585
6832
|
createChart,
|
|
@@ -5597,6 +6844,8 @@ export {
|
|
|
5597
6844
|
exportPNG,
|
|
5598
6845
|
exportSVG,
|
|
5599
6846
|
exportSVGWithFonts,
|
|
6847
|
+
exportSpecSequence,
|
|
6848
|
+
materializeCanvasModeSVG,
|
|
5600
6849
|
observeResize,
|
|
5601
6850
|
rectPathWithCorners,
|
|
5602
6851
|
registerMarkRenderer,
|