@opendata-ai/openchart-vanilla 8.0.0-rc.2 → 8.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
- package/dist/chunk-EKAUQAAX.js.map +1 -0
- package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
- package/dist/chunk-KNT5AZ2O.js.map +1 -0
- package/dist/chunk-RM5XS6NR.js +113 -0
- package/dist/chunk-RM5XS6NR.js.map +1 -0
- package/dist/chunk-T32UHMZA.js +282 -0
- package/dist/chunk-T32UHMZA.js.map +1 -0
- package/dist/export-gif.d.ts +113 -0
- package/dist/export-gif.js +21 -0
- package/dist/export-gif.js.map +1 -0
- package/dist/index.d.ts +283 -24
- package/dist/index.js +2012 -763
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
- package/dist/simulation-worker.js +83 -2
- package/dist/static.js +7 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/story/index.js +5 -3
- package/dist/story/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +14 -6
- package/dist/chunk-5EOV5JQZ.js.map +0 -1
- package/dist/chunk-ENXSUXC5.js.map +0 -1
- package/src/__test-fixtures__/dom.ts +0 -70
- package/src/__test-fixtures__/specs.ts +0 -214
- package/src/__tests__/animation.test.ts +0 -358
- package/src/__tests__/annotations-render.test.ts +0 -201
- package/src/__tests__/compound-labels.test.ts +0 -122
- package/src/__tests__/continuous-legend.test.ts +0 -91
- package/src/__tests__/crosshair.test.ts +0 -130
- package/src/__tests__/edit-events.test.ts +0 -902
- package/src/__tests__/events.test.ts +0 -387
- package/src/__tests__/export.test.ts +0 -241
- package/src/__tests__/font-race.test.ts +0 -199
- package/src/__tests__/gradient-ids.test.ts +0 -159
- package/src/__tests__/gradient-utils.test.ts +0 -125
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -293
- package/src/__tests__/mount.test.ts +0 -357
- package/src/__tests__/pattern-fills.test.ts +0 -105
- package/src/__tests__/resize-timing.test.ts +0 -218
- package/src/__tests__/sankey.test.ts +0 -133
- package/src/__tests__/selection-events.test.ts +0 -869
- package/src/__tests__/series-search.test.ts +0 -311
- package/src/__tests__/static.test.ts +0 -141
- package/src/__tests__/svg-renderer.test.ts +0 -895
- package/src/__tests__/table-keyboard.test.ts +0 -361
- package/src/__tests__/table-mount.test.ts +0 -639
- package/src/__tests__/tilemap.test.ts +0 -158
- package/src/__tests__/tooltip.test.ts +0 -337
- package/src/__tests__/transition.test.ts +0 -1777
- package/src/__tests__/you-draw-it.test.ts +0 -273
- package/src/animation-vars.ts +0 -42
- package/src/animation.ts +0 -76
- package/src/barlist-mount.ts +0 -383
- package/src/barlist-renderer.ts +0 -267
- package/src/dom-helpers.ts +0 -17
- package/src/export.ts +0 -482
- package/src/gradient-utils.ts +0 -138
- package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
- package/src/graph/__tests__/graph-mount.test.ts +0 -213
- package/src/graph/__tests__/interaction.test.ts +0 -205
- package/src/graph/__tests__/keyboard.test.ts +0 -653
- package/src/graph/__tests__/search.test.ts +0 -88
- package/src/graph/__tests__/simulation.test.ts +0 -233
- package/src/graph/__tests__/spatial-index.test.ts +0 -142
- package/src/graph/__tests__/zoom.test.ts +0 -203
- package/src/graph/canvas-renderer.ts +0 -747
- package/src/graph/interaction.ts +0 -367
- package/src/graph/keyboard.ts +0 -208
- package/src/graph/search.ts +0 -50
- package/src/graph/simulation-worker-url.ts +0 -12
- package/src/graph/simulation-worker.ts +0 -278
- package/src/graph/simulation.ts +0 -422
- package/src/graph/spatial-index.ts +0 -121
- package/src/graph/types.ts +0 -47
- package/src/graph/worker-protocol.ts +0 -70
- package/src/graph/zoom.ts +0 -134
- package/src/graph-mount.ts +0 -888
- package/src/index.ts +0 -76
- package/src/interactions/chart-events.ts +0 -139
- package/src/interactions/crosshair.ts +0 -233
- package/src/interactions/drag-handler.ts +0 -175
- package/src/interactions/editing-drags.ts +0 -515
- package/src/interactions/index.ts +0 -25
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -280
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -88
- package/src/map-mount.ts +0 -701
- package/src/map-renderer.ts +0 -338
- package/src/map-transition.ts +0 -83
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1382
- package/src/pattern-utils.ts +0 -110
- package/src/renderers/annotations.ts +0 -388
- package/src/renderers/axes.ts +0 -351
- package/src/renderers/brand.ts +0 -87
- package/src/renderers/chrome.ts +0 -196
- package/src/renderers/endpoint-labels.ts +0 -150
- package/src/renderers/legend.ts +0 -311
- package/src/renderers/marks.ts +0 -561
- package/src/renderers/metrics.ts +0 -63
- package/src/renderers/svg-dom.ts +0 -40
- package/src/renderers/table-cells.ts +0 -446
- package/src/resize-observer.ts +0 -46
- package/src/sankey-mount.ts +0 -622
- package/src/sankey-renderer.ts +0 -614
- package/src/series-search.ts +0 -392
- package/src/static.ts +0 -251
- package/src/story/__tests__/camera-math.test.ts +0 -145
- package/src/story/__tests__/create-chart-story.test.ts +0 -317
- package/src/story/__tests__/progress-math.test.ts +0 -95
- package/src/story/__tests__/scroll-driver.test.ts +0 -130
- package/src/story/__tests__/tween.test.ts +0 -159
- package/src/story/camera-math.ts +0 -125
- package/src/story/create-chart-story.ts +0 -353
- package/src/story/crossfade.ts +0 -70
- package/src/story/index.ts +0 -39
- package/src/story/progress-math.ts +0 -85
- package/src/story/resolve-camera-target.ts +0 -119
- package/src/story/scroll-driver.ts +0 -206
- package/src/story/story-camera.ts +0 -49
- package/src/story/tween.ts +0 -112
- package/src/story/types.ts +0 -76
- package/src/svg-ids.ts +0 -23
- package/src/svg-renderer.ts +0 -383
- package/src/table-keyboard.ts +0 -266
- package/src/table-mount.ts +0 -567
- package/src/table-renderer.ts +0 -414
- package/src/text-edit-overlay.ts +0 -255
- package/src/theme-tokens.ts +0 -51
- package/src/tilemap-mount.ts +0 -511
- package/src/tilemap-renderer.ts +0 -396
- package/src/tooltip.ts +0 -177
- package/src/transition.ts +0 -2816
- package/src/you-draw-it.ts +0 -480
- /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spatial index for fast node hit-testing.
|
|
3
|
-
*
|
|
4
|
-
* Wraps d3-quadtree to provide findNearest (accounts for node radius)
|
|
5
|
-
* and findInRect queries. Tracks a generation counter to avoid unnecessary
|
|
6
|
-
* rebuilds when positions haven't changed.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { type Quadtree, quadtree } from 'd3-quadtree';
|
|
10
|
-
import type { PositionedNode } from './types';
|
|
11
|
-
|
|
12
|
-
export class SpatialIndex {
|
|
13
|
-
private tree: Quadtree<PositionedNode> | null = null;
|
|
14
|
-
private nodes: PositionedNode[] = [];
|
|
15
|
-
private maxRadius = 0;
|
|
16
|
-
private generation = 0;
|
|
17
|
-
|
|
18
|
-
/** Rebuild the quadtree from the current set of positioned nodes. */
|
|
19
|
-
rebuild(nodes: PositionedNode[]): void {
|
|
20
|
-
this.nodes = nodes;
|
|
21
|
-
this.maxRadius = 0;
|
|
22
|
-
for (const n of nodes) {
|
|
23
|
-
if (n.radius > this.maxRadius) this.maxRadius = n.radius;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
this.tree = quadtree<PositionedNode>()
|
|
27
|
-
.x((d) => d.x)
|
|
28
|
-
.y((d) => d.y)
|
|
29
|
-
.addAll(nodes);
|
|
30
|
-
this.generation++;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Current generation counter. Increments on each rebuild. */
|
|
34
|
-
getGeneration(): number {
|
|
35
|
-
return this.generation;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Find the nearest node to (x, y) within maxDistance.
|
|
40
|
-
* Accounts for node radius: a hit occurs if the point is inside
|
|
41
|
-
* the node circle (distance to center < node.radius), or if the
|
|
42
|
-
* edge-to-edge distance is within maxDistance.
|
|
43
|
-
*/
|
|
44
|
-
findNearest(x: number, y: number, maxDistance: number = Infinity): PositionedNode | null {
|
|
45
|
-
if (!this.tree || this.nodes.length === 0) return null;
|
|
46
|
-
|
|
47
|
-
// The effective search radius for the quadtree needs to include
|
|
48
|
-
// the largest node radius, because we might be "inside" a large node
|
|
49
|
-
// whose center is far from our search point.
|
|
50
|
-
const searchRadius = maxDistance + this.maxRadius;
|
|
51
|
-
|
|
52
|
-
let best: PositionedNode | null = null;
|
|
53
|
-
let bestEffectiveDist = maxDistance + this.maxRadius + 1;
|
|
54
|
-
|
|
55
|
-
this.tree.visit((node, x0, y0, x1, y1) => {
|
|
56
|
-
// Closest point in this quad to our target
|
|
57
|
-
const closestX = Math.max(x0, Math.min(x, x1));
|
|
58
|
-
const closestY = Math.max(y0, Math.min(y, y1));
|
|
59
|
-
const quadDist = Math.hypot(closestX - x, closestY - y);
|
|
60
|
-
|
|
61
|
-
// Prune: if the closest edge of this quad is beyond searchRadius, skip
|
|
62
|
-
if (quadDist > searchRadius) return true;
|
|
63
|
-
|
|
64
|
-
// Check leaf data
|
|
65
|
-
if (!node.length) {
|
|
66
|
-
let current = node;
|
|
67
|
-
do {
|
|
68
|
-
const d = current.data;
|
|
69
|
-
if (d) {
|
|
70
|
-
const dist = Math.hypot(d.x - x, d.y - y);
|
|
71
|
-
// Effective distance: subtract the node's radius.
|
|
72
|
-
// If we're inside the circle, effective distance is 0.
|
|
73
|
-
const effectiveDist = Math.max(0, dist - d.radius);
|
|
74
|
-
if (effectiveDist <= maxDistance && effectiveDist < bestEffectiveDist) {
|
|
75
|
-
bestEffectiveDist = effectiveDist;
|
|
76
|
-
best = d;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} while ((current = current.next!));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return false;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
return best;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Find all nodes whose centers fall within the given rectangle. */
|
|
89
|
-
findInRect(x1: number, y1: number, x2: number, y2: number): PositionedNode[] {
|
|
90
|
-
if (!this.tree) return [];
|
|
91
|
-
|
|
92
|
-
const minX = Math.min(x1, x2);
|
|
93
|
-
const minY = Math.min(y1, y2);
|
|
94
|
-
const maxX = Math.max(x1, x2);
|
|
95
|
-
const maxY = Math.max(y1, y2);
|
|
96
|
-
|
|
97
|
-
const results: PositionedNode[] = [];
|
|
98
|
-
|
|
99
|
-
this.tree.visit((node, qx0, qy0, qx1, qy1) => {
|
|
100
|
-
// If quad doesn't overlap the search rect, skip
|
|
101
|
-
if (qx0 > maxX || qx1 < minX || qy0 > maxY || qy1 < minY) {
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Check leaf nodes
|
|
106
|
-
if (!node.length) {
|
|
107
|
-
let current = node;
|
|
108
|
-
do {
|
|
109
|
-
const d = current.data;
|
|
110
|
-
if (d && d.x >= minX && d.x <= maxX && d.y >= minY && d.y <= maxY) {
|
|
111
|
-
results.push(d);
|
|
112
|
-
}
|
|
113
|
-
} while ((current = current.next!));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return false;
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return results;
|
|
120
|
-
}
|
|
121
|
-
}
|
package/src/graph/types.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types for the graph adapter.
|
|
3
|
-
*
|
|
4
|
-
* These extend the engine's compiled types with positional information set by
|
|
5
|
-
* the force simulation at runtime. The engine produces CompiledGraphNode/Edge
|
|
6
|
-
* objects with visual properties but no x/y coords. After simulation, we get
|
|
7
|
-
* PositionedNode/Edge that the canvas renderer can draw.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ResolvedTheme } from '@opendata-ai/openchart-core';
|
|
11
|
-
import type { CompiledGraphEdge, CompiledGraphNode } from '@opendata-ai/openchart-engine';
|
|
12
|
-
|
|
13
|
-
/** A compiled node with simulation-assigned x/y position. */
|
|
14
|
-
export interface PositionedNode extends CompiledGraphNode {
|
|
15
|
-
x: number;
|
|
16
|
-
y: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** A compiled edge with resolved source/target screen positions. */
|
|
20
|
-
export interface PositionedEdge extends CompiledGraphEdge {
|
|
21
|
-
sourceX: number;
|
|
22
|
-
sourceY: number;
|
|
23
|
-
targetX: number;
|
|
24
|
-
targetY: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Complete render state passed to the canvas renderer each frame.
|
|
29
|
-
*
|
|
30
|
-
* Assembled by the graph mount from simulation positions, interaction state,
|
|
31
|
-
* and theme. The renderer is stateless -- it draws whatever this says.
|
|
32
|
-
*/
|
|
33
|
-
export interface GraphRenderState {
|
|
34
|
-
nodes: PositionedNode[];
|
|
35
|
-
edges: PositionedEdge[];
|
|
36
|
-
transform: { x: number; y: number; k: number };
|
|
37
|
-
hoveredNodeId: string | null;
|
|
38
|
-
hoveredEdgeId: string | null;
|
|
39
|
-
selectedNodeIds: Set<string>;
|
|
40
|
-
adjacencyMap: Map<string, Set<string>>;
|
|
41
|
-
theme: ResolvedTheme;
|
|
42
|
-
searchMatches: Set<string> | null;
|
|
43
|
-
/** True during active pan/zoom gestures. Renderer skips labels and glow. */
|
|
44
|
-
isGesturing: boolean;
|
|
45
|
-
/** Whether the tryOpenData.ai watermark is enabled. */
|
|
46
|
-
watermark: boolean;
|
|
47
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Message protocol between the main thread and the simulation Web Worker.
|
|
3
|
-
*
|
|
4
|
-
* Defines the shape of messages going in both directions, plus the internal
|
|
5
|
-
* node/edge types the simulation works with. These are intentionally simple
|
|
6
|
-
* and independent from the engine's compiled types so the worker can be
|
|
7
|
-
* built as a standalone IIFE without workspace imports.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Simulation data shapes (used internally by worker + sync fallback)
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
/** Minimal node shape for the force simulation. */
|
|
15
|
-
export interface SimNode {
|
|
16
|
-
id: string;
|
|
17
|
-
x?: number;
|
|
18
|
-
y?: number;
|
|
19
|
-
radius: number;
|
|
20
|
-
community?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Minimal edge shape for the force simulation. */
|
|
24
|
-
export interface SimEdge {
|
|
25
|
-
source: string;
|
|
26
|
-
target: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Inline simulation config (mirrors engine SimulationConfig). */
|
|
30
|
-
export interface WorkerSimulationConfig {
|
|
31
|
-
chargeStrength: number;
|
|
32
|
-
linkDistance: number;
|
|
33
|
-
clustering: { field: string; strength: number } | null;
|
|
34
|
-
alphaDecay: number;
|
|
35
|
-
velocityDecay: number;
|
|
36
|
-
collisionRadius: number;
|
|
37
|
-
collisionPadding?: number;
|
|
38
|
-
linkStrength?: number;
|
|
39
|
-
centerForce?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// Main -> Worker messages
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
export type WorkerInMessage =
|
|
47
|
-
| {
|
|
48
|
-
type: 'init';
|
|
49
|
-
nodes: SimNode[];
|
|
50
|
-
edges: SimEdge[];
|
|
51
|
-
config: WorkerSimulationConfig;
|
|
52
|
-
}
|
|
53
|
-
| { type: 'reheat'; alpha?: number }
|
|
54
|
-
| { type: 'pin'; nodeId: string; x: number; y: number }
|
|
55
|
-
| { type: 'unpin'; nodeId: string }
|
|
56
|
-
| { type: 'drag'; nodeId: string; x: number; y: number }
|
|
57
|
-
| { type: 'stop' };
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Worker -> Main messages
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
export type WorkerOutMessage =
|
|
64
|
-
| {
|
|
65
|
-
type: 'positions';
|
|
66
|
-
nodes: Array<{ id: string; x: number; y: number }>;
|
|
67
|
-
alpha: number;
|
|
68
|
-
}
|
|
69
|
-
| { type: 'settled' }
|
|
70
|
-
| { type: 'error'; message: string };
|
package/src/graph/zoom.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Immutable zoom transform for graph pan/zoom.
|
|
3
|
-
*
|
|
4
|
-
* Provides coordinate conversion between screen space (canvas pixels)
|
|
5
|
-
* and graph space (simulation coordinates). All mutations return new
|
|
6
|
-
* instances rather than modifying in place.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { PositionedNode } from './types';
|
|
10
|
-
|
|
11
|
-
export class ZoomTransform {
|
|
12
|
-
constructor(
|
|
13
|
-
readonly x: number,
|
|
14
|
-
readonly y: number,
|
|
15
|
-
readonly k: number,
|
|
16
|
-
) {}
|
|
17
|
-
|
|
18
|
-
/** Convert screen coordinates to graph coordinates. */
|
|
19
|
-
screenToGraph(sx: number, sy: number): { x: number; y: number } {
|
|
20
|
-
return {
|
|
21
|
-
x: (sx - this.x) / this.k,
|
|
22
|
-
y: (sy - this.y) / this.k,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Convert graph coordinates to screen coordinates. */
|
|
27
|
-
graphToScreen(gx: number, gy: number): { x: number; y: number } {
|
|
28
|
-
return {
|
|
29
|
-
x: gx * this.k + this.x,
|
|
30
|
-
y: gy * this.k + this.y,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Zoom to a target scale, keeping the given screen-space pivot
|
|
36
|
-
* point fixed (content under the cursor stays under the cursor).
|
|
37
|
-
*/
|
|
38
|
-
zoomAt(targetK: number, pivotX: number, pivotY: number): ZoomTransform {
|
|
39
|
-
// The graph point under the pivot should remain at the same screen position.
|
|
40
|
-
// Before: pivotX = gx * k + x => gx = (pivotX - x) / k
|
|
41
|
-
// After: pivotX = gx * targetK + newX => newX = pivotX - gx * targetK
|
|
42
|
-
const gx = (pivotX - this.x) / this.k;
|
|
43
|
-
const gy = (pivotY - this.y) / this.k;
|
|
44
|
-
return new ZoomTransform(pivotX - gx * targetK, pivotY - gy * targetK, targetK);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Pan by a screen-space delta. */
|
|
48
|
-
pan(dx: number, dy: number): ZoomTransform {
|
|
49
|
-
return new ZoomTransform(this.x + dx, this.y + dy, this.k);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Compute a transform that fits all nodes within the given canvas
|
|
54
|
-
* dimensions with the specified padding.
|
|
55
|
-
*
|
|
56
|
-
* Returns the transform and the ideal content height (in screen pixels)
|
|
57
|
-
* so callers can shrink the canvas to eliminate dead space.
|
|
58
|
-
*/
|
|
59
|
-
static fitBounds(
|
|
60
|
-
nodes: PositionedNode[],
|
|
61
|
-
canvasW: number,
|
|
62
|
-
canvasH: number,
|
|
63
|
-
padding: number = 40,
|
|
64
|
-
): { transform: ZoomTransform; contentHeight: number } {
|
|
65
|
-
if (nodes.length === 0) {
|
|
66
|
-
return { transform: ZoomTransform.identity(), contentHeight: canvasH };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let minX = Infinity;
|
|
70
|
-
let minY = Infinity;
|
|
71
|
-
let maxX = -Infinity;
|
|
72
|
-
let maxY = -Infinity;
|
|
73
|
-
|
|
74
|
-
for (const n of nodes) {
|
|
75
|
-
const r = n.radius;
|
|
76
|
-
if (n.x - r < minX) minX = n.x - r;
|
|
77
|
-
if (n.y - r < minY) minY = n.y - r;
|
|
78
|
-
if (n.x + r > maxX) maxX = n.x + r;
|
|
79
|
-
if (n.y + r > maxY) maxY = n.y + r;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let graphW = maxX - minX;
|
|
83
|
-
let graphH = maxY - minY;
|
|
84
|
-
|
|
85
|
-
if (graphW === 0 && graphH === 0) {
|
|
86
|
-
// All nodes at the same point; just center
|
|
87
|
-
return {
|
|
88
|
-
transform: new ZoomTransform(canvasW / 2 - minX, canvasH / 2 - minY, 1),
|
|
89
|
-
contentHeight: padding * 2,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// When called early in the simulation (first tick), the bounding box
|
|
94
|
-
// underestimates the final spread. Apply a spread multiplier based on
|
|
95
|
-
// node count: larger graphs expand more as charge forces push nodes
|
|
96
|
-
// apart over subsequent ticks. The sqrt scaling mirrors how d3-force
|
|
97
|
-
// charge repulsion grows with node count.
|
|
98
|
-
if (nodes.length > 50) {
|
|
99
|
-
const spread = 1 + Math.sqrt(nodes.length) / 120;
|
|
100
|
-
const cx = (minX + maxX) / 2;
|
|
101
|
-
const cy = (minY + maxY) / 2;
|
|
102
|
-
graphW *= spread;
|
|
103
|
-
graphH *= spread;
|
|
104
|
-
minX = cx - graphW / 2;
|
|
105
|
-
maxX = cx + graphW / 2;
|
|
106
|
-
minY = cy - graphH / 2;
|
|
107
|
-
maxY = cy + graphH / 2;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const availW = canvasW - padding * 2;
|
|
111
|
-
const availH = canvasH - padding * 2;
|
|
112
|
-
// Cap at 1 so the graph never renders larger than its natural size
|
|
113
|
-
const k = Math.min(1, availW / graphW, availH / graphH);
|
|
114
|
-
|
|
115
|
-
// Center both axes so the graph sits in the middle of the viewport
|
|
116
|
-
const cx = (minX + maxX) / 2;
|
|
117
|
-
const cy = (minY + maxY) / 2;
|
|
118
|
-
const tx = canvasW / 2 - cx * k;
|
|
119
|
-
const ty = canvasH / 2 - cy * k;
|
|
120
|
-
|
|
121
|
-
// Content height = scaled graph extent + top and bottom padding
|
|
122
|
-
const contentHeight = graphH * k + padding * 2;
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
transform: new ZoomTransform(tx, ty, k),
|
|
126
|
-
contentHeight,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Identity transform (no pan, no zoom). */
|
|
131
|
-
static identity(): ZoomTransform {
|
|
132
|
-
return new ZoomTransform(0, 0, 1);
|
|
133
|
-
}
|
|
134
|
-
}
|