@opendata-ai/openchart-vanilla 8.4.0 → 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.
Files changed (37) hide show
  1. package/README.md +59 -0
  2. package/dist/chunk-2OJZBMXM.js +377 -0
  3. package/dist/chunk-2OJZBMXM.js.map +1 -0
  4. package/dist/{chunk-45XZOWYC.js → chunk-5KV3ZBBL.js} +58 -437
  5. package/dist/chunk-5KV3ZBBL.js.map +1 -0
  6. package/dist/{chunk-HGRNXLDF.js → chunk-5RX6ZQGZ.js} +10 -1
  7. package/dist/chunk-5RX6ZQGZ.js.map +1 -0
  8. package/dist/{chunk-UOB422LY.js → chunk-6B5OBGIO.js} +2 -2
  9. package/dist/chunk-NG7CVCI2.js +58 -0
  10. package/dist/chunk-NG7CVCI2.js.map +1 -0
  11. package/dist/{chunk-FETYAVTZ.js → chunk-RYVV3YIG.js} +3 -3
  12. package/dist/{chunk-LNPRDSK3.js → chunk-UAK6HWOC.js} +2 -2
  13. package/dist/chunk-WOLX5EZT.js +3577 -0
  14. package/dist/chunk-WOLX5EZT.js.map +1 -0
  15. package/dist/{chunk-YGYMB7KB.js → chunk-XGII4ZRT.js} +55 -30
  16. package/dist/chunk-XGII4ZRT.js.map +1 -0
  17. package/dist/export-gif.js +3 -3
  18. package/dist/graph-3d/index.d.ts +272 -0
  19. package/dist/graph-3d/index.js +1556 -0
  20. package/dist/graph-3d/index.js.map +1 -0
  21. package/dist/index.d.ts +4 -252
  22. package/dist/index.js +27 -3471
  23. package/dist/index.js.map +1 -1
  24. package/dist/renderer-registry-B5tW6yAZ.d.ts +376 -0
  25. package/dist/static.js +4 -3
  26. package/dist/static.js.map +1 -1
  27. package/dist/story/index.js +10 -7
  28. package/dist/story/index.js.map +1 -1
  29. package/dist/styles.css +1 -1
  30. package/dist/styles.css.map +1 -1
  31. package/package.json +26 -5
  32. package/dist/chunk-45XZOWYC.js.map +0 -1
  33. package/dist/chunk-HGRNXLDF.js.map +0 -1
  34. package/dist/chunk-YGYMB7KB.js.map +0 -1
  35. /package/dist/{chunk-UOB422LY.js.map → chunk-6B5OBGIO.js.map} +0 -0
  36. /package/dist/{chunk-FETYAVTZ.js.map → chunk-RYVV3YIG.js.map} +0 -0
  37. /package/dist/{chunk-LNPRDSK3.js.map → chunk-UAK6HWOC.js.map} +0 -0
@@ -0,0 +1,376 @@
1
+ import { AnimationEase, GraphSpec, ThemeConfig, DarkMode, TooltipContent } from '@opendata-ai/openchart-core';
2
+ import { GraphCompilation } from '@opendata-ai/openchart-engine';
3
+ import { Placement } from '@floating-ui/dom';
4
+
5
+ /**
6
+ * Shared types for the graph adapter.
7
+ *
8
+ * These extend the engine's compiled types with positional information set by
9
+ * the force simulation at runtime. The engine produces CompiledGraphNode/Edge
10
+ * objects with visual properties but no x/y coords. After simulation, we get
11
+ * PositionedNode/Edge that the canvas renderer can draw.
12
+ */
13
+
14
+ /**
15
+ * A programmatic highlight target. Resolved once at call time into a node id set
16
+ * and rendered through the focus model (eased crossfade). See
17
+ * `GraphInstance.highlight`.
18
+ *
19
+ * Lives here rather than in `graph-mount` so `graph/highlight.ts`, which both
20
+ * the 2D and 3D mounts consume, does not have to import upward from the layer
21
+ * that consumes it.
22
+ */
23
+ type GraphHighlightTarget = {
24
+ nodeIds: string[];
25
+ } | {
26
+ category: {
27
+ field: string;
28
+ value: string | string[];
29
+ };
30
+ } | {
31
+ neighborsOf: string;
32
+ includeSelf?: boolean;
33
+ };
34
+ /**
35
+ * The camera payload both renderers report through `getCamera()` and
36
+ * `onCameraChange`.
37
+ *
38
+ * `x`/`y`/`k` are always present. In 2D they are the d3 zoom transform: `x`/`y`
39
+ * are the translate in pixels, `k` the zoom scalar. In 3D they describe a pose
40
+ * instead: `x`/`y` are the look-at point in world units and `k` is
41
+ * `FIT_DISTANCE / cameraDistance`, so the sign convention still matches.
42
+ *
43
+ * `position` and `target` are present ONLY in 3D, which is why they are
44
+ * optional here. Persisted camera state has to be keyed on the dimension it
45
+ * came from before it is restored.
46
+ */
47
+ interface GraphCamera {
48
+ x: number;
49
+ y: number;
50
+ k: number;
51
+ /** 3D only: the camera's world-space position. */
52
+ position?: {
53
+ x: number;
54
+ y: number;
55
+ z: number;
56
+ };
57
+ /** 3D only: the world-space point the camera looks at. */
58
+ target?: {
59
+ x: number;
60
+ y: number;
61
+ z: number;
62
+ };
63
+ }
64
+ /**
65
+ * A `flyTo` destination: a `GraphCamera` with an optional `k` (omit it to keep
66
+ * the current zoom/distance). The 3D pose fields are accepted here so a
67
+ * `getCamera()` result round-trips unchanged; the 2D renderer ignores them.
68
+ */
69
+ interface GraphFlyTarget extends Omit<GraphCamera, 'k'> {
70
+ k?: number;
71
+ }
72
+
73
+ /**
74
+ * Animated camera for the graph.
75
+ *
76
+ * Flights interpolate between two `ZoomTransform`s along d3's geodesic
77
+ * `interpolateZoom` path (smooth zoom-and-pan that pulls back to see both
78
+ * endpoints before diving in). A flight is a {@link GraphAnimation}, driven by
79
+ * the mount's AnimationScheduler.
80
+ *
81
+ * `interpolateZoom` operates on a `[cx, cy, width]` "view" — the graph-space
82
+ * center and the graph-space width visible across the viewport. We convert the
83
+ * ZoomTransform ⇄ view around a fixed viewport size.
84
+ */
85
+
86
+ /** Options controlling a camera flight. */
87
+ interface CameraFlightOptions {
88
+ /** Duration in ms, or `'auto'` to derive from the zoom distance. */
89
+ duration?: number | 'auto';
90
+ /** Easing preset. Default `'smooth'`. */
91
+ ease?: AnimationEase;
92
+ }
93
+
94
+ /**
95
+ * Graph mount API: the main entry point for vanilla JS graph usage.
96
+ *
97
+ * createGraph() takes a container, GraphSpec, and options, compiles the graph,
98
+ * creates a force simulation, canvas renderer, spatial index, interaction
99
+ * manager, and search manager, then runs an animation loop driven by
100
+ * simulation ticks. Returns a GraphInstance with update/search/zoom/destroy.
101
+ */
102
+
103
+ /** A hovered node or edge, passed to a tooltip formatter. */
104
+ interface GraphTooltipItem {
105
+ kind: 'node' | 'edge';
106
+ /** The raw datum (node record, or edge record with source/target). */
107
+ data: Record<string, unknown>;
108
+ }
109
+ /**
110
+ * Custom tooltip content builder. Receives the hovered item and the library's
111
+ * default {@link TooltipContent}, returns replacement content.
112
+ *
113
+ * Safety contract:
114
+ * - A returned `TooltipContent` or `string` is escaped by the library (strings
115
+ * are inserted via `textContent`, never `innerHTML`).
116
+ * - A returned `HTMLElement` is trusted verbatim — the host owns sanitization.
117
+ * - `null` suppresses the tooltip for that item.
118
+ */
119
+ type GraphTooltipFormatter = (item: GraphTooltipItem, defaults: TooltipContent) => TooltipContent | string | HTMLElement | null;
120
+ /** Built-in legend data (headless mirror of the rendered legend). */
121
+ interface GraphLegendData {
122
+ field: string | null;
123
+ nodes: Array<{
124
+ label: string;
125
+ color: string;
126
+ count?: number;
127
+ active: boolean;
128
+ }>;
129
+ edges: Array<{
130
+ label: string;
131
+ color: string;
132
+ count?: number;
133
+ }>;
134
+ }
135
+ interface GraphMountOptions {
136
+ theme?: ThemeConfig;
137
+ darkMode?: DarkMode;
138
+ /**
139
+ * Sink for advisory spec warnings from the compiler (unknown `seedNode` id,
140
+ * deprecations, and so on). Defaults to `console.warn`. Each distinct message
141
+ * is emitted at most once per instance, so a warning that survives every
142
+ * recompile doesn't spam the console on `update()`.
143
+ */
144
+ onWarn?: (message: string) => void;
145
+ responsive?: boolean;
146
+ /** Show the OpenData watermark. Defaults to true. */
147
+ watermark?: boolean;
148
+ /** Show the built-in tooltip; pass an object for a custom formatter. Defaults to true. */
149
+ tooltip?: boolean | {
150
+ formatter?: GraphTooltipFormatter;
151
+ };
152
+ /**
153
+ * Built-in legend. `true` (default) renders an interactive legend with counts.
154
+ * `false` renders none (set this if you render your own legend). An object
155
+ * toggles interactivity/counts.
156
+ */
157
+ legend?: boolean | {
158
+ interactive?: boolean;
159
+ counts?: boolean;
160
+ };
161
+ onNodeClick?: (node: Record<string, unknown>) => void;
162
+ onNodeDoubleClick?: (node: Record<string, unknown>) => void;
163
+ onNodeHover?: (node: Record<string, unknown> | null) => void;
164
+ onEdgeHover?: (edge: Record<string, unknown> | null) => void;
165
+ onSelectionChange?: (nodeIds: string[]) => void;
166
+ /** Fired when the user hovers a legend entry (null on leave). */
167
+ onLegendHover?: (entry: {
168
+ field: string;
169
+ value: string;
170
+ } | null) => void;
171
+ /** Fired when legend toggle state changes; `activeValues` is the active category set (empty = all). */
172
+ onLegendToggle?: (activeValues: string[]) => void;
173
+ /** Fired whenever the highlight set changes (programmatic or legend), null when cleared. */
174
+ onHighlightChange?: (nodeIds: string[] | null) => void;
175
+ /**
176
+ * Fit the graph to the viewport on the first tick. Default true. Set false to
177
+ * restore a saved camera (e.g. getCamera() + flyTo) without the initial fit.
178
+ */
179
+ fitOnLoad?: boolean;
180
+ /** Camera change callback, rAF-coalesced (fires at most once per rendered frame). */
181
+ onCameraChange?: (camera: GraphCamera) => void;
182
+ /** Skip the entrance reveal/flight on mount (spec unchanged; used by wrappers when recreating for a theme/darkMode-only change so the entrance doesn't replay). Warmup still runs. */
183
+ suppressEntrance?: boolean;
184
+ }
185
+ interface GraphInstance {
186
+ update(spec: GraphSpec): void;
187
+ /** Re-compile encoding/legend/chrome without restarting the simulation. Preserves node positions. */
188
+ updateVisuals(spec: GraphSpec): void;
189
+ search(query: string): void;
190
+ clearSearch(): void;
191
+ /** Fit all nodes into the viewport. Animated by default; `{ duration: 0 }` snaps. */
192
+ zoomToFit(opts?: CameraFlightOptions & {
193
+ padding?: number;
194
+ }): void;
195
+ /** Fly to a node and zoom in (default scale 2). Tracks the node while it settles. */
196
+ zoomToNode(nodeId: string, opts?: CameraFlightOptions & {
197
+ scale?: number;
198
+ }): void;
199
+ /** Fly the camera to a graph-space target. */
200
+ flyTo(target: GraphFlyTarget, opts?: CameraFlightOptions): void;
201
+ /** Center the camera on a graph-space point (keeps current zoom). */
202
+ centerAt(x: number, y: number, opts?: CameraFlightOptions): void;
203
+ /**
204
+ * Current camera. In 2D this is the zoom transform; in 3D the same `x`/`y`/`k`
205
+ * summary plus the `position`/`target` pose. See `GraphCamera`.
206
+ */
207
+ getCamera(): GraphCamera;
208
+ /** Select a node; `{ fly: true }` also flies to it (default follows interaction.select.flyTo). */
209
+ selectNode(nodeId: string, opts?: {
210
+ fly?: boolean;
211
+ } & CameraFlightOptions): void;
212
+ getSelectedNodes(): string[];
213
+ /** Node ids currently matching the active search query. */
214
+ getSearchMatches(): string[];
215
+ /**
216
+ * Emphasize a set of nodes; eased via the focus model. Layers over the
217
+ * standing category filter (`setActiveCategories`) rather than replacing it:
218
+ * the effective set is the intersection, or the highlight alone when the two
219
+ * are disjoint. Legend toggle state is untouched.
220
+ */
221
+ highlight(target: GraphHighlightTarget, opts?: {
222
+ dimOpacity?: number;
223
+ }): void;
224
+ /**
225
+ * Clear the transient highlight, returning to the standing category filter.
226
+ * Does NOT clear the filter — use `setActiveCategories([])` for that.
227
+ */
228
+ clearHighlight(): void;
229
+ /** The effective highlighted node ids, or null when nothing is highlighted. */
230
+ getHighlight(): string[] | null;
231
+ /** Headless snapshot of the legend (node categories + edge categories). */
232
+ getLegend(): GraphLegendData;
233
+ /** Set the active category filter declaratively (replaces legend toggle state). */
234
+ setActiveCategories(values: string[]): void;
235
+ /** Current active category values (empty = all active, no filter). */
236
+ getActiveCategories(): string[];
237
+ resize(): void;
238
+ destroy(): void;
239
+ }
240
+ /**
241
+ * Create a graph instance from a spec and mount it into a container.
242
+ *
243
+ * @param container - The DOM element to render into.
244
+ * @param spec - The graph spec.
245
+ * @param options - Mount options.
246
+ * @returns A GraphInstance with update/search/zoom/destroy methods.
247
+ */
248
+ declare function createGraph(container: HTMLElement, spec: GraphSpec, options?: GraphMountOptions): GraphInstance;
249
+
250
+ /**
251
+ * Tooltip manager: creates and positions a floating tooltip element.
252
+ *
253
+ * Shows tooltip content near the mouse/touch position with viewport
254
+ * edge avoidance via @floating-ui/dom. Touch support via tap-to-show,
255
+ * tap-outside-to-hide.
256
+ */
257
+
258
+ interface TooltipShowOptions {
259
+ /**
260
+ * Floating-ui placement. Defaults to 'bottom-start' for mark hover
261
+ * (the legacy behavior). Line/area snap-tooltips pass 'right-start'
262
+ * so the card sits beside the snapped point and flips to 'left-start'
263
+ * via the flip middleware when crowding the right edge.
264
+ */
265
+ placement?: Placement;
266
+ }
267
+ /**
268
+ * Content accepted by {@link TooltipManager.show}. A structured
269
+ * {@link TooltipContent} is rendered by the manager; `{ text }` is inserted as
270
+ * plain text (escaped via textContent); `{ element }` is a caller-owned node
271
+ * trusted verbatim (the host owns sanitization).
272
+ */
273
+ type TooltipInput = TooltipContent | {
274
+ text: string;
275
+ } | {
276
+ element: HTMLElement;
277
+ };
278
+ interface TooltipManager {
279
+ /** Show the tooltip with content at a given position. */
280
+ show(content: TooltipInput, x: number, y: number, opts?: TooltipShowOptions): void;
281
+ /**
282
+ * Re-anchor the open tooltip without re-rendering its content.
283
+ *
284
+ * The 3D graph re-anchors on every engine tick and every camera change, and
285
+ * `show()` would re-run the host formatter and rewrite the DOM at frame rate
286
+ * for content that has not changed.
287
+ */
288
+ move(x: number, y: number, opts?: TooltipShowOptions): void;
289
+ /** Hide the tooltip. */
290
+ hide(): void;
291
+ /** Remove the tooltip element and clean up event listeners. */
292
+ destroy(): void;
293
+ }
294
+ /**
295
+ * Create a tooltip manager attached to a container element.
296
+ *
297
+ * The manager creates a floating div positioned relative to the container.
298
+ * Content is rendered as a title line with optional color indicator,
299
+ * followed by a compact list of field-value pairs.
300
+ *
301
+ * @param container - The parent element for the tooltip.
302
+ * @returns TooltipManager with show/hide/destroy methods.
303
+ */
304
+ declare function createTooltipManager(container: HTMLElement): TooltipManager;
305
+
306
+ /**
307
+ * Graph renderer registry.
308
+ *
309
+ * `createGraph()` builds the shared shell (wrapper, chrome, legend element,
310
+ * tooltip manager, resize wiring) and then hands off to a renderer. The 2D
311
+ * Canvas renderer is built in. The 3D WebGL renderer lives on the
312
+ * `@opendata-ai/openchart-vanilla/graph-3d` subpath and registers itself here
313
+ * as a side effect of being imported, so three.js never enters the default
314
+ * bundle. `createGraph()` never dynamically imports a renderer: mount is
315
+ * synchronous because every framework wrapper assumes it is.
316
+ */
317
+
318
+ /**
319
+ * Everything a renderer needs from the shared mount shell. The shell owns the
320
+ * DOM wrapper and its chrome/legend/tooltip children; the renderer owns the
321
+ * surface it mounts between the chrome and the legend (a `<canvas>` for 2D,
322
+ * the 3d-force-graph scene container for 3D) plus all simulation, camera and
323
+ * interaction state.
324
+ */
325
+ interface GraphShell {
326
+ /** The consumer's container element. */
327
+ container: HTMLElement;
328
+ /** `.oc-graph-wrapper` (carries `oc-dark` and the theme custom properties). */
329
+ wrapper: HTMLElement;
330
+ /** `.oc-graph-chrome` (title/subtitle HTML). */
331
+ chromeEl: HTMLElement;
332
+ /** `.oc-graph-legend`, or null when `options.legend === false`. */
333
+ legendEl: HTMLElement | null;
334
+ /** Shared DOM tooltip manager, or null when `options.tooltip === false`. */
335
+ tooltipManager: TooltipManager | null;
336
+ /** Resolved dark-mode flag used when the wrapper was built. */
337
+ isDark: boolean;
338
+ /**
339
+ * Insert the renderer's surface element into the wrapper between the chrome
340
+ * and the legend. Call exactly once.
341
+ */
342
+ mountSurface(el: HTMLElement): void;
343
+ /** Re-render chrome HTML from the current compilation (after `update()`). */
344
+ renderChrome(compilation: GraphCompilation): void;
345
+ /** Re-measure the chrome and update the inset custom property. */
346
+ syncChromeInset(): void;
347
+ /** Current container size (width/height in CSS px, height floored at 200). */
348
+ getSize(): {
349
+ width: number;
350
+ height: number;
351
+ };
352
+ /**
353
+ * Subscribe to container resizes. Honours `options.responsive === false`
354
+ * (returns a no-op disconnect). The ResizeObserver fires once on first
355
+ * layout, so renderers must tolerate an immediate callback.
356
+ */
357
+ observeResize(callback: () => void): () => void;
358
+ /** Warn-once sink (`options.onWarn`, default `console.warn`). */
359
+ warn(message: string): void;
360
+ /** Remove the wrapper from the container and release the tooltip manager. */
361
+ destroy(): void;
362
+ }
363
+ interface GraphRendererContext {
364
+ shell: GraphShell;
365
+ spec: GraphSpec;
366
+ compilation: GraphCompilation;
367
+ options: GraphMountOptions | undefined;
368
+ /** Recompile a spec with the mount's compile options (theme, darkMode, onWarn). */
369
+ compile(spec: GraphSpec): GraphCompilation;
370
+ }
371
+ type GraphRendererFactory = (ctx: GraphRendererContext) => GraphInstance;
372
+ /** Register a renderer for a dimension count. Later registrations replace earlier ones. */
373
+ declare function registerGraphRenderer(dimensions: 3, factory: GraphRendererFactory): void;
374
+ declare const GRAPH_3D_NOT_REGISTERED_ERROR = "createGraph: dimensions: 3 requires import \"@opendata-ai/openchart-vanilla/graph-3d\"";
375
+
376
+ export { type CameraFlightOptions as C, GRAPH_3D_NOT_REGISTERED_ERROR as G, type TooltipManager as T, type GraphCamera as a, type GraphFlyTarget as b, type GraphHighlightTarget as c, type GraphInstance as d, type GraphLegendData as e, type GraphMountOptions as f, type GraphRendererContext as g, type GraphRendererFactory as h, type GraphShell as i, type GraphTooltipFormatter as j, type GraphTooltipItem as k, createGraph as l, createTooltipManager as m, registerGraphRenderer as r };
package/dist/static.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import {
2
2
  renderTileMapSVG
3
- } from "./chunk-LNPRDSK3.js";
3
+ } from "./chunk-UAK6HWOC.js";
4
4
  import {
5
5
  renderChartSVG,
6
6
  resetSvgIdCounter
7
- } from "./chunk-YGYMB7KB.js";
7
+ } from "./chunk-XGII4ZRT.js";
8
8
  import {
9
9
  SVG_NS,
10
10
  buildThemeStyleBlock
11
- } from "./chunk-HGRNXLDF.js";
11
+ } from "./chunk-5RX6ZQGZ.js";
12
+ import "./chunk-NG7CVCI2.js";
12
13
 
13
14
  // src/static.ts
14
15
  import { createRequire } from "module";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/static.ts"],"sourcesContent":["// Node.js only — do not import from browser code.\nimport { createRequire } from 'node:module';\nimport type {\n ChartLayout,\n ChartSpec,\n CompileOptions,\n DarkMode,\n LayerSpec,\n ResolvedTheme,\n ThemeConfig,\n TileMapSpec,\n} from '@opendata-ai/openchart-core';\nimport { isLayerSpec, isTileMapSpec } from '@opendata-ai/openchart-core';\nimport { compileChart, compileLayer, compileTileMap } from '@opendata-ai/openchart-engine';\nimport { SVG_NS } from './renderers/svg-dom';\nimport { resetSvgIdCounter } from './svg-ids';\nimport { renderChartSVG } from './svg-renderer';\nimport { buildThemeStyleBlock } from './theme-style-block';\nimport { renderTileMapSVG } from './tilemap-renderer';\n\nconst esmRequire = createRequire(import.meta.url);\n\nlet cachedWindow: typeof import('happy-dom').Window | undefined;\nfunction getHappyDomWindow(): typeof import('happy-dom').Window {\n if (cachedWindow) return cachedWindow;\n try {\n ({ Window: cachedWindow } = esmRequire('happy-dom') as typeof import('happy-dom'));\n } catch {\n throw new Error(\n \"renderStaticSVG requires 'happy-dom' as a peer dependency. Install it with: npm add happy-dom\",\n );\n }\n return cachedWindow;\n}\n\nlet rendering = false;\n\nexport interface StaticRenderOptions {\n width?: number;\n height?: number;\n theme?: ThemeConfig;\n /**\n * Dark mode setting. In static rendering `'auto'` resolves to light mode\n * since there is no `matchMedia` to query. Use `'force'` for dark output.\n */\n darkMode?: DarkMode;\n /** Rendering backend for point marks; see `MountOptions.renderer`. */\n renderer?: 'auto' | 'svg' | 'canvas';\n watermark?: boolean;\n}\n\nfunction resolveStaticDarkMode(mode?: DarkMode): boolean {\n if (mode === 'force') return true;\n return false;\n}\n\nfunction stripInteractiveElements(svg: Element): void {\n const selectors = ['[data-voronoi-overlay]', '[data-crosshair]', '[data-snap-dots]'];\n for (const selector of selectors) {\n const els = svg.querySelectorAll(selector);\n for (const el of els) {\n el.parentNode?.removeChild(el);\n }\n }\n}\n\n/**\n * Render a chart spec to a standalone SVG string without a browser DOM.\n *\n * Requires `happy-dom` as a peer dependency (`bun add happy-dom`).\n *\n * Not safe for concurrent invocation: the render pipeline relies on a global\n * SVG ID counter and a temporary global `document` swap, both of which are\n * single-threaded. In a server handling parallel requests, serialize calls\n * through a queue or mutex.\n *\n * The entire render pipeline is synchronous; the global swap is safe as long\n * as no code schedules microtasks that outlive the call.\n */\nexport function renderStaticSVG(\n spec: ChartSpec | LayerSpec | TileMapSpec,\n options?: StaticRenderOptions,\n): string {\n if (rendering) {\n throw new Error('renderStaticSVG is not reentrant — serialize calls through a queue or mutex');\n }\n rendering = true;\n\n const Window = getHappyDomWindow();\n const win = new Window({ url: 'about:blank' });\n\n const prevDocument = globalThis.document;\n const prevWindow = (globalThis as Record<string, unknown>).window;\n\n try {\n (globalThis as Record<string, unknown>).document = win.document;\n (globalThis as Record<string, unknown>).window = win;\n\n resetSvgIdCounter();\n\n const width = options?.width ?? 640;\n const height = options?.height ?? 420;\n const darkMode = resolveStaticDarkMode(options?.darkMode);\n\n const compileOpts: CompileOptions = {\n width,\n height,\n theme: options?.theme,\n darkMode,\n renderer: options?.renderer,\n watermark: options?.watermark,\n };\n\n let svg: SVGElement;\n let themeForStyle: ResolvedTheme;\n\n if (isTileMapSpec(spec)) {\n const tileMapLayout = compileTileMap(spec, compileOpts);\n svg = renderTileMapSVG(tileMapLayout, { animate: false });\n themeForStyle = tileMapLayout.theme;\n } else {\n let layout: ChartLayout;\n if (isLayerSpec(spec)) {\n layout = compileLayer(spec, compileOpts);\n } else {\n layout = compileChart(spec, compileOpts);\n }\n\n const container = win.document.createElement('div');\n Object.defineProperty(container, 'getBoundingClientRect', {\n value: () => ({\n width,\n height,\n top: 0,\n left: 0,\n right: width,\n bottom: height,\n x: 0,\n y: 0,\n toJSON: () => ({}),\n }),\n });\n\n svg = renderChartSVG(layout, container as unknown as HTMLElement, {\n animate: false,\n crosshair: false,\n });\n\n stripInteractiveElements(svg);\n themeForStyle = layout.theme;\n }\n\n const doc = win.document as unknown as Document;\n let defs = svg.querySelector('defs');\n if (!defs) {\n defs = doc.createElementNS(SVG_NS, 'defs');\n svg.insertBefore(defs as unknown as Node, svg.firstChild);\n }\n const styleEl = doc.createElementNS(SVG_NS, 'style');\n styleEl.textContent = buildThemeStyleBlock(themeForStyle);\n defs.insertBefore(styleEl as unknown as Node, defs.firstChild);\n\n const serializer = new (\n win as unknown as { XMLSerializer: typeof XMLSerializer }\n ).XMLSerializer();\n return serializer.serializeToString(svg as unknown as Node);\n } finally {\n rendering = false;\n if (prevDocument !== undefined) {\n (globalThis as Record<string, unknown>).document = prevDocument;\n } else {\n delete (globalThis as Record<string, unknown>).document;\n }\n if (prevWindow !== undefined) {\n (globalThis as Record<string, unknown>).window = prevWindow;\n } else {\n delete (globalThis as Record<string, unknown>).window;\n }\n win.close();\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/static.ts"],"sourcesContent":["// Node.js only — do not import from browser code.\nimport { createRequire } from 'node:module';\nimport type {\n ChartLayout,\n ChartSpec,\n CompileOptions,\n DarkMode,\n LayerSpec,\n ResolvedTheme,\n ThemeConfig,\n TileMapSpec,\n} from '@opendata-ai/openchart-core';\nimport { isLayerSpec, isTileMapSpec } from '@opendata-ai/openchart-core';\nimport { compileChart, compileLayer, compileTileMap } from '@opendata-ai/openchart-engine';\nimport { SVG_NS } from './renderers/svg-dom';\nimport { resetSvgIdCounter } from './svg-ids';\nimport { renderChartSVG } from './svg-renderer';\nimport { buildThemeStyleBlock } from './theme-style-block';\nimport { renderTileMapSVG } from './tilemap-renderer';\n\nconst esmRequire = createRequire(import.meta.url);\n\nlet cachedWindow: typeof import('happy-dom').Window | undefined;\nfunction getHappyDomWindow(): typeof import('happy-dom').Window {\n if (cachedWindow) return cachedWindow;\n try {\n ({ Window: cachedWindow } = esmRequire('happy-dom') as typeof import('happy-dom'));\n } catch {\n throw new Error(\n \"renderStaticSVG requires 'happy-dom' as a peer dependency. Install it with: npm add happy-dom\",\n );\n }\n return cachedWindow;\n}\n\nlet rendering = false;\n\nexport interface StaticRenderOptions {\n width?: number;\n height?: number;\n theme?: ThemeConfig;\n /**\n * Dark mode setting. In static rendering `'auto'` resolves to light mode\n * since there is no `matchMedia` to query. Use `'force'` for dark output.\n */\n darkMode?: DarkMode;\n /** Rendering backend for point marks; see `MountOptions.renderer`. */\n renderer?: 'auto' | 'svg' | 'canvas';\n watermark?: boolean;\n}\n\nfunction resolveStaticDarkMode(mode?: DarkMode): boolean {\n if (mode === 'force') return true;\n return false;\n}\n\nfunction stripInteractiveElements(svg: Element): void {\n const selectors = ['[data-voronoi-overlay]', '[data-crosshair]', '[data-snap-dots]'];\n for (const selector of selectors) {\n const els = svg.querySelectorAll(selector);\n for (const el of els) {\n el.parentNode?.removeChild(el);\n }\n }\n}\n\n/**\n * Render a chart spec to a standalone SVG string without a browser DOM.\n *\n * Requires `happy-dom` as a peer dependency (`bun add happy-dom`).\n *\n * Not safe for concurrent invocation: the render pipeline relies on a global\n * SVG ID counter and a temporary global `document` swap, both of which are\n * single-threaded. In a server handling parallel requests, serialize calls\n * through a queue or mutex.\n *\n * The entire render pipeline is synchronous; the global swap is safe as long\n * as no code schedules microtasks that outlive the call.\n */\nexport function renderStaticSVG(\n spec: ChartSpec | LayerSpec | TileMapSpec,\n options?: StaticRenderOptions,\n): string {\n if (rendering) {\n throw new Error('renderStaticSVG is not reentrant — serialize calls through a queue or mutex');\n }\n rendering = true;\n\n const Window = getHappyDomWindow();\n const win = new Window({ url: 'about:blank' });\n\n const prevDocument = globalThis.document;\n const prevWindow = (globalThis as Record<string, unknown>).window;\n\n try {\n (globalThis as Record<string, unknown>).document = win.document;\n (globalThis as Record<string, unknown>).window = win;\n\n resetSvgIdCounter();\n\n const width = options?.width ?? 640;\n const height = options?.height ?? 420;\n const darkMode = resolveStaticDarkMode(options?.darkMode);\n\n const compileOpts: CompileOptions = {\n width,\n height,\n theme: options?.theme,\n darkMode,\n renderer: options?.renderer,\n watermark: options?.watermark,\n };\n\n let svg: SVGElement;\n let themeForStyle: ResolvedTheme;\n\n if (isTileMapSpec(spec)) {\n const tileMapLayout = compileTileMap(spec, compileOpts);\n svg = renderTileMapSVG(tileMapLayout, { animate: false });\n themeForStyle = tileMapLayout.theme;\n } else {\n let layout: ChartLayout;\n if (isLayerSpec(spec)) {\n layout = compileLayer(spec, compileOpts);\n } else {\n layout = compileChart(spec, compileOpts);\n }\n\n const container = win.document.createElement('div');\n Object.defineProperty(container, 'getBoundingClientRect', {\n value: () => ({\n width,\n height,\n top: 0,\n left: 0,\n right: width,\n bottom: height,\n x: 0,\n y: 0,\n toJSON: () => ({}),\n }),\n });\n\n svg = renderChartSVG(layout, container as unknown as HTMLElement, {\n animate: false,\n crosshair: false,\n });\n\n stripInteractiveElements(svg);\n themeForStyle = layout.theme;\n }\n\n const doc = win.document as unknown as Document;\n let defs = svg.querySelector('defs');\n if (!defs) {\n defs = doc.createElementNS(SVG_NS, 'defs');\n svg.insertBefore(defs as unknown as Node, svg.firstChild);\n }\n const styleEl = doc.createElementNS(SVG_NS, 'style');\n styleEl.textContent = buildThemeStyleBlock(themeForStyle);\n defs.insertBefore(styleEl as unknown as Node, defs.firstChild);\n\n const serializer = new (\n win as unknown as { XMLSerializer: typeof XMLSerializer }\n ).XMLSerializer();\n return serializer.serializeToString(svg as unknown as Node);\n } finally {\n rendering = false;\n if (prevDocument !== undefined) {\n (globalThis as Record<string, unknown>).document = prevDocument;\n } else {\n delete (globalThis as Record<string, unknown>).document;\n }\n if (prevWindow !== undefined) {\n (globalThis as Record<string, unknown>).window = prevWindow;\n } else {\n delete (globalThis as Record<string, unknown>).window;\n }\n win.close();\n }\n}\n"],"mappings":"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","names":[]}
@@ -5,22 +5,25 @@ import {
5
5
  cameraTransform,
6
6
  camerasClose,
7
7
  canTransitionSpecShape,
8
- clamp01,
9
8
  createChart,
10
9
  createGeoMap,
11
- createTween2 as createTween,
10
+ createTween,
12
11
  damp,
13
12
  dampCamera,
14
13
  easingFns,
15
14
  fitTarget,
16
15
  interpolateCamera,
17
- lerp,
18
16
  scrubCamera,
19
17
  storyMotion
20
- } from "../chunk-45XZOWYC.js";
21
- import "../chunk-UOB422LY.js";
22
- import "../chunk-YGYMB7KB.js";
23
- import "../chunk-HGRNXLDF.js";
18
+ } from "../chunk-5KV3ZBBL.js";
19
+ import "../chunk-6B5OBGIO.js";
20
+ import "../chunk-XGII4ZRT.js";
21
+ import "../chunk-5RX6ZQGZ.js";
22
+ import {
23
+ clamp01,
24
+ lerp
25
+ } from "../chunk-2OJZBMXM.js";
26
+ import "../chunk-NG7CVCI2.js";
24
27
 
25
28
  // src/story/create-chart-story.ts
26
29
  import { deepMergeSpec, isGeoMapSpec } from "@opendata-ai/openchart-core";