@opendata-ai/openchart-vanilla 8.4.1 → 8.5.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.
@@ -0,0 +1,272 @@
1
+ import { GraphSpec } from '@opendata-ai/openchart-core';
2
+ import { a as GraphCamera, b as GraphFlyTarget, g as GraphRendererContext, d as GraphInstance, f as GraphMountOptions } from '../renderer-registry-B5tW6yAZ.js';
3
+ import '@opendata-ai/openchart-engine';
4
+ import '@floating-ui/dom';
5
+
6
+ /**
7
+ * Per-frame focus model for the graph: unifies the three emphasis sources
8
+ * (programmatic highlight, search matches, hover-neighborhood) into a single
9
+ * snapshot pair that the renderer crossfades between.
10
+ *
11
+ * Composition, not strict precedence (see plan §5a):
12
+ * - Standing state = highlight ∩ search when both active; if that intersection
13
+ * is empty, search matches win (fresher user intent). This preserves the
14
+ * "filter by topic, then search within it" workflow.
15
+ * - Hover-neighborhood is the transient top layer over whatever the standing
16
+ * state is.
17
+ *
18
+ * The transition tweens between two discrete FocusSnapshots. Rapid hover sweeps
19
+ * retarget mid-flight: `retarget(next, now)` snapshots the endpoint CLOSEST to
20
+ * the current display (`prev = p < 0.5 ? old prev : old next`), so a low-p
21
+ * retarget keeps the old prev — no forward snap, worst-case visual jump halved
22
+ * vs. a naive "always start from current next" rule. Exact per-edge capture
23
+ * would destroy the renderer's tier batching, so this discrete approximation is
24
+ * deliberate.
25
+ */
26
+ /** The set of emphasis relationships in effect for one steady state. */
27
+ interface FocusSnapshot {
28
+ /** True when any emphasis source is active (something is dimmed). */
29
+ hasActive: boolean;
30
+ /** Nodes connected to the active/hovered set (includes the active nodes). */
31
+ connected: Set<string>;
32
+ /** Search matches, or null when search is inactive. */
33
+ searchMatches: Set<string> | null;
34
+ /** Selected nodes (selection rings; always emphasized). */
35
+ selected: Set<string>;
36
+ }
37
+
38
+ /**
39
+ * Emphasis composition for the 3D renderer: turns the focus model into a
40
+ * per-node and per-edge target opacity.
41
+ *
42
+ * The composition rules are NOT re-derived here. `composeStandingFocus` and
43
+ * `layerHoverFocus` from `../graph/focus-transition` are the single source of
44
+ * truth for "highlight ∩ search, hover on top" and are reused verbatim; this
45
+ * module only maps the resulting {@link FocusSnapshot} onto opacity numbers,
46
+ * mirroring the tier rules the 2D canvas renderer applies
47
+ * (`graph/canvas-renderer.ts` `nodeTier` / `edgeTier` / `*TierAlpha`).
48
+ *
49
+ * The one 3D-specific input is `edgeBaseAlpha`: above `LINK_WIDTH_MAX_EDGES`
50
+ * the renderer draws plain lines and maps `edgeWidth` to a resting alpha
51
+ * instead of a cylinder radius, so the resting tier is per-edge rather than a
52
+ * single constant.
53
+ *
54
+ * Pure: no three.js, no DOM.
55
+ */
56
+
57
+ /** Minimal node shape the composition needs. */
58
+ interface EmphasisNode {
59
+ id: string;
60
+ /** Compiled `nodeOpacity` encoding value; multiplies into the result. */
61
+ opacity: number;
62
+ }
63
+ /** Minimal edge shape the composition needs. Index is the link object key. */
64
+ interface EmphasisEdge {
65
+ source: string;
66
+ target: string;
67
+ }
68
+ interface EmphasisInput {
69
+ nodes: EmphasisNode[];
70
+ edges: EmphasisEdge[];
71
+ /** The composed focus snapshot (standing state with the hover layer on top). */
72
+ focus: FocusSnapshot;
73
+ /** Ids that never dim (the spec's `seedNode`). */
74
+ exemptIds: Set<string>;
75
+ /** Resolved `interaction.hover.dimOpacity`. */
76
+ dimOpacity: number;
77
+ /** Per-edge resting alpha keyed by edge index; defaults to EDGE_ALPHA_DEFAULT. */
78
+ edgeBaseAlpha?: Map<number, number>;
79
+ }
80
+ interface EmphasisTargets {
81
+ /** Target material opacity by node id. */
82
+ nodes: Map<string, number>;
83
+ /** Target material opacity by edge index. */
84
+ edges: Map<number, number>;
85
+ }
86
+ /**
87
+ * Compose the focus snapshot into target opacities.
88
+ *
89
+ * Node rule (mirrors `nodeTier` + `nodeTierAlpha`): nothing active → 1; an
90
+ * exempt id → 1; in the connected set → 1; otherwise `dimOpacity`. An active
91
+ * search multiplies non-matching nodes by {@link SEARCH_NON_MATCH_ALPHA}, and
92
+ * exemption deliberately does NOT apply to search (a seed that doesn't match
93
+ * the query should not pretend to). The compiled `nodeOpacity` multiplies last.
94
+ *
95
+ * Edge rule (mirrors `edgeTier` + `edgeTierAlpha`): nothing active → the edge's
96
+ * resting alpha; both endpoints connected → 1; otherwise `dimOpacity / 3`,
97
+ * which preserves the node-to-edge dim ratio that keeps dense hairballs quiet.
98
+ * Edges are NOT exempted by `exemptIds`: the seed stays lit while its edges dim
99
+ * with everything else.
100
+ */
101
+ declare function resolveEmphasis(input: EmphasisInput): EmphasisTargets;
102
+
103
+ /**
104
+ * Force wiring for the 3D simulation.
105
+ *
106
+ * 3d-force-graph owns a d3-force-3d simulation on the main thread; we do not
107
+ * drive it and we do not port the 2D web worker here. This module maps the
108
+ * engine's `SimulationConfig` onto that simulation's knobs and supplies the one
109
+ * force d3-force-3d does not ship: the community cluster force, which is a
110
+ * 3-axis port of `forceCluster` in `../graph/simulation.ts`. Both copies must
111
+ * stay in sync.
112
+ */
113
+
114
+ /**
115
+ * A community cluster force in three axes: each node is pulled toward the
116
+ * centroid of its own community, scaled by `strength * alpha`. Port of
117
+ * `forceCluster` in `../graph/simulation.ts` with the z axis added.
118
+ *
119
+ * Uses d3's `initialize` hook rather than closing over a node array, so the
120
+ * force keeps working after `graphData()` swaps the node objects.
121
+ */
122
+ declare function forceCluster3D(strength: number): (alpha: number) => void;
123
+
124
+ /**
125
+ * Label visibility policy for the 3D renderer.
126
+ *
127
+ * Same two-stage shape as 2D: rank, then declutter. This module is the ranking
128
+ * half — forced labels (seed / `alwaysShowLabel` overrides, hovered, selected,
129
+ * search matches) first, then the highest-priority nodes, nearest camera first,
130
+ * up to the budget. The mount does the screen-space overlap pass on the ordered
131
+ * result, which is why the order matters and not just the membership.
132
+ *
133
+ * Pure: no three.js, no DOM. The mount feeds it plain positions.
134
+ */
135
+ /** A label candidate: a node with a resolved label and a compiled priority. */
136
+ interface LabelCandidate {
137
+ id: string;
138
+ /** Compiled `labelPriority` (0..1, degree-derived). Higher wins. */
139
+ priority: number;
140
+ x: number;
141
+ y: number;
142
+ z: number;
143
+ }
144
+ /** Default 3D label budget beyond the forced set. */
145
+ declare const LABEL_BUDGET_3D = 40;
146
+ /**
147
+ * Node ids whose label sprite may be shown, best first.
148
+ *
149
+ * `forced` ids lead the list and do NOT consume budget. The rest are ranked by
150
+ * `(priority desc, camera distance asc)` and the top `budget` follow.
151
+ */
152
+ declare function resolveVisibleLabels(nodes: LabelCandidate[], forced: Set<string>, cameraPos: {
153
+ x: number;
154
+ y: number;
155
+ z: number;
156
+ }, budget: number): string[];
157
+
158
+ /**
159
+ * Per-link three.js objects.
160
+ *
161
+ * Same reason as `nodes.ts`: `linkOpacity` is a single global number in
162
+ * 3d-force-graph, so per-edge hover dimming needs each link to own its
163
+ * material. (An rgba `linkColor` IS honoured for per-link opacity — see
164
+ * `../graph-3d/README` notes in the RFC report — but changing it only takes
165
+ * effect on a `refresh()`, which rebuilds every link object. That is exactly
166
+ * what hover must not do.)
167
+ *
168
+ * Two shapes: a `Line` (constant screen width, cheap) and a cylinder `Mesh`
169
+ * (true world-space width, needed when `edgeWidth` is encoded). Above
170
+ * `LINK_WIDTH_MAX_EDGES` the mount stays on lines and maps `edgeWidth` to a
171
+ * resting alpha instead.
172
+ *
173
+ * Positioning: three-forcegraph's own tick loop positions custom `Line` and
174
+ * `Mesh` link objects generically (it branches on `obj.type` and, for meshes,
175
+ * on the geometry already being a `CylinderGeometry`). The mount still installs
176
+ * a `linkPositionUpdate` because dashed lines need `computeLineDistances()`
177
+ * after every move, which the library's path does not call.
178
+ */
179
+
180
+ /** Above this edge count `edgeWidth` maps to opacity steps, not cylinder radius. */
181
+ declare const LINK_WIDTH_MAX_EDGES = 2000;
182
+
183
+ /**
184
+ * The 3D graph renderer: a `GraphInstance` backed by `3d-force-graph`.
185
+ *
186
+ * Registered against `dimensions: 3` by `./index`. `createGraph()` builds the
187
+ * shared shell (wrapper, chrome, legend slot, tooltip manager, resize wiring)
188
+ * and hands it here; this module owns the WebGL scene, the d3-force-3d
189
+ * simulation the library drives, the per-node and per-link three.js objects,
190
+ * emphasis state, the label budget, tooltip anchoring, and disposal.
191
+ *
192
+ * Three library facts shape almost everything below and are easy to get wrong:
193
+ *
194
+ * 1. `nodeOpacity` and `linkOpacity` are GLOBAL numbers, not accessors. Every
195
+ * per-object opacity (dim, highlight, entrance, search) therefore needs a
196
+ * custom object with its own material. See `nodes.ts` / `links.ts`.
197
+ * 2. `refresh()` rebuilds every node and link object. It must never run on
198
+ * hover; the hover path only writes `material.opacity`.
199
+ * 3. `rendererConfig` and `controlType` are CONSTRUCTOR options, not chained
200
+ * setters.
201
+ *
202
+ * Differences from 2D that are deliberate and documented in RFC 27: no
203
+ * keyboard navigation, no node drag, no cursor repulsion or springy drag, no
204
+ * wall-clock warmup budget, and a structural `update()` reheats globally
205
+ * (`graphData()` restarts the layout at alpha 1) instead of applying 2D's local
206
+ * impulse.
207
+ */
208
+
209
+ /**
210
+ * The camera distance that `getCamera().k === 1` refers to. 3D has no zoom
211
+ * scalar, so `k` is expressed as `FIT_DISTANCE / cameraDistance`: a `k` of 2 is
212
+ * twice as close as the reference framing, matching the 2D sign convention.
213
+ */
214
+ declare const FIT_DISTANCE = 1000;
215
+ /** Camera standoff from a node for `zoomToNode`, in scene units. */
216
+ declare const NODE_FOCUS_DISTANCE = 120;
217
+ /**
218
+ * A camera pose: the shared `GraphCamera` with the pose fields narrowed to
219
+ * required, since the 3D renderer always reports them.
220
+ */
221
+ interface Camera3D extends GraphCamera {
222
+ position: {
223
+ x: number;
224
+ y: number;
225
+ z: number;
226
+ };
227
+ target: {
228
+ x: number;
229
+ y: number;
230
+ z: number;
231
+ };
232
+ }
233
+ /** `flyTo` accepts either the 2D shape or a full 3D pose from `getCamera()`. */
234
+ type FlyTarget3D = GraphFlyTarget;
235
+ declare function createGraph3DRenderer(ctx: GraphRendererContext): GraphInstance;
236
+
237
+ /**
238
+ * `@opendata-ai/openchart-vanilla/graph-3d` — the WebGL graph renderer.
239
+ *
240
+ * Importing this module registers the 3D renderer for `dimensions: 3`; after
241
+ * that, `createGraph()` picks it up on its own. That side effect is the whole
242
+ * point of the separate subpath: three.js (~30MB unpacked) never enters the
243
+ * default bundle, and `createGraph()` never dynamically imports a renderer,
244
+ * because mount is synchronous and every framework wrapper assumes it is.
245
+ *
246
+ * ```ts
247
+ * import { createGraph } from '@opendata-ai/openchart-vanilla';
248
+ * import '@opendata-ai/openchart-vanilla/graph-3d';
249
+ *
250
+ * createGraph(el, { type: 'graph', dimensions: 3, nodes, edges });
251
+ * ```
252
+ *
253
+ * `three`, `3d-force-graph` and `three-spritetext` are optional peer
254
+ * dependencies; install them alongside openchart to use this entry point, and
255
+ * check `npm ls three` shows exactly one copy (two copies fail at runtime with
256
+ * `Cannot read properties of undefined (reading 'VERTEX')`).
257
+ *
258
+ * SSR: the libraries touch `window` at import, so import this module from a
259
+ * client-only path (a `useEffect` dynamic import in React Router / Next).
260
+ */
261
+
262
+ /**
263
+ * Mount a 3D graph without the caller having to set `dimensions: 3` themselves.
264
+ *
265
+ * A thin convenience over `createGraph()`: the spec is forced to three
266
+ * dimensions and everything else — shell, compilation, node-count gate, handle
267
+ * — is the shared path. A graph above `MAX_3D_NODES` still falls back to the 2D
268
+ * renderer with a warning, exactly as `createGraph()` would.
269
+ */
270
+ declare function createGraph3D(container: HTMLElement, spec: GraphSpec, options?: GraphMountOptions): GraphInstance;
271
+
272
+ export { type Camera3D, type EmphasisInput, type EmphasisTargets, FIT_DISTANCE, type FlyTarget3D, LABEL_BUDGET_3D, LINK_WIDTH_MAX_EDGES, type LabelCandidate, NODE_FOCUS_DISTANCE, createGraph3D, createGraph3DRenderer, forceCluster3D, resolveEmphasis, resolveVisibleLabels };