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