@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.
Files changed (145) hide show
  1. package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
  2. package/dist/chunk-DCODCLWI.js +234 -0
  3. package/dist/chunk-DCODCLWI.js.map +1 -0
  4. package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
  5. package/dist/chunk-EKAUQAAX.js.map +1 -0
  6. package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
  7. package/dist/chunk-KNT5AZ2O.js.map +1 -0
  8. package/dist/chunk-RM5XS6NR.js +113 -0
  9. package/dist/chunk-RM5XS6NR.js.map +1 -0
  10. package/dist/chunk-T32UHMZA.js +282 -0
  11. package/dist/chunk-T32UHMZA.js.map +1 -0
  12. package/dist/export-gif.d.ts +113 -0
  13. package/dist/export-gif.js +21 -0
  14. package/dist/export-gif.js.map +1 -0
  15. package/dist/index.d.ts +283 -24
  16. package/dist/index.js +2012 -763
  17. package/dist/index.js.map +1 -1
  18. package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
  19. package/dist/simulation-worker.js +83 -2
  20. package/dist/static.js +7 -69
  21. package/dist/static.js.map +1 -1
  22. package/dist/story/index.d.ts +3 -2
  23. package/dist/story/index.js +5 -3
  24. package/dist/story/index.js.map +1 -1
  25. package/dist/styles.css +1 -1
  26. package/dist/styles.css.map +1 -1
  27. package/package.json +14 -6
  28. package/dist/chunk-5EOV5JQZ.js.map +0 -1
  29. package/dist/chunk-ENXSUXC5.js.map +0 -1
  30. package/src/__test-fixtures__/dom.ts +0 -70
  31. package/src/__test-fixtures__/specs.ts +0 -214
  32. package/src/__tests__/animation.test.ts +0 -358
  33. package/src/__tests__/annotations-render.test.ts +0 -201
  34. package/src/__tests__/compound-labels.test.ts +0 -122
  35. package/src/__tests__/continuous-legend.test.ts +0 -91
  36. package/src/__tests__/crosshair.test.ts +0 -130
  37. package/src/__tests__/edit-events.test.ts +0 -902
  38. package/src/__tests__/events.test.ts +0 -387
  39. package/src/__tests__/export.test.ts +0 -241
  40. package/src/__tests__/font-race.test.ts +0 -199
  41. package/src/__tests__/gradient-ids.test.ts +0 -159
  42. package/src/__tests__/gradient-utils.test.ts +0 -125
  43. package/src/__tests__/known-bugs-render.test.ts +0 -183
  44. package/src/__tests__/map.test.ts +0 -293
  45. package/src/__tests__/mount.test.ts +0 -357
  46. package/src/__tests__/pattern-fills.test.ts +0 -105
  47. package/src/__tests__/resize-timing.test.ts +0 -218
  48. package/src/__tests__/sankey.test.ts +0 -133
  49. package/src/__tests__/selection-events.test.ts +0 -869
  50. package/src/__tests__/series-search.test.ts +0 -311
  51. package/src/__tests__/static.test.ts +0 -141
  52. package/src/__tests__/svg-renderer.test.ts +0 -895
  53. package/src/__tests__/table-keyboard.test.ts +0 -361
  54. package/src/__tests__/table-mount.test.ts +0 -639
  55. package/src/__tests__/tilemap.test.ts +0 -158
  56. package/src/__tests__/tooltip.test.ts +0 -337
  57. package/src/__tests__/transition.test.ts +0 -1777
  58. package/src/__tests__/you-draw-it.test.ts +0 -273
  59. package/src/animation-vars.ts +0 -42
  60. package/src/animation.ts +0 -76
  61. package/src/barlist-mount.ts +0 -383
  62. package/src/barlist-renderer.ts +0 -267
  63. package/src/dom-helpers.ts +0 -17
  64. package/src/export.ts +0 -482
  65. package/src/gradient-utils.ts +0 -138
  66. package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
  67. package/src/graph/__tests__/graph-mount.test.ts +0 -213
  68. package/src/graph/__tests__/interaction.test.ts +0 -205
  69. package/src/graph/__tests__/keyboard.test.ts +0 -653
  70. package/src/graph/__tests__/search.test.ts +0 -88
  71. package/src/graph/__tests__/simulation.test.ts +0 -233
  72. package/src/graph/__tests__/spatial-index.test.ts +0 -142
  73. package/src/graph/__tests__/zoom.test.ts +0 -203
  74. package/src/graph/canvas-renderer.ts +0 -747
  75. package/src/graph/interaction.ts +0 -367
  76. package/src/graph/keyboard.ts +0 -208
  77. package/src/graph/search.ts +0 -50
  78. package/src/graph/simulation-worker-url.ts +0 -12
  79. package/src/graph/simulation-worker.ts +0 -278
  80. package/src/graph/simulation.ts +0 -422
  81. package/src/graph/spatial-index.ts +0 -121
  82. package/src/graph/types.ts +0 -47
  83. package/src/graph/worker-protocol.ts +0 -70
  84. package/src/graph/zoom.ts +0 -134
  85. package/src/graph-mount.ts +0 -888
  86. package/src/index.ts +0 -76
  87. package/src/interactions/chart-events.ts +0 -139
  88. package/src/interactions/crosshair.ts +0 -233
  89. package/src/interactions/drag-handler.ts +0 -175
  90. package/src/interactions/editing-drags.ts +0 -515
  91. package/src/interactions/index.ts +0 -25
  92. package/src/interactions/keyboard-nav.ts +0 -111
  93. package/src/interactions/legend-interaction.ts +0 -46
  94. package/src/interactions/selection.ts +0 -280
  95. package/src/interactions/tooltip-events.ts +0 -72
  96. package/src/map-camera.ts +0 -88
  97. package/src/map-mount.ts +0 -701
  98. package/src/map-renderer.ts +0 -338
  99. package/src/map-transition.ts +0 -83
  100. package/src/measure-text.ts +0 -134
  101. package/src/mount.ts +0 -1382
  102. package/src/pattern-utils.ts +0 -110
  103. package/src/renderers/annotations.ts +0 -388
  104. package/src/renderers/axes.ts +0 -351
  105. package/src/renderers/brand.ts +0 -87
  106. package/src/renderers/chrome.ts +0 -196
  107. package/src/renderers/endpoint-labels.ts +0 -150
  108. package/src/renderers/legend.ts +0 -311
  109. package/src/renderers/marks.ts +0 -561
  110. package/src/renderers/metrics.ts +0 -63
  111. package/src/renderers/svg-dom.ts +0 -40
  112. package/src/renderers/table-cells.ts +0 -446
  113. package/src/resize-observer.ts +0 -46
  114. package/src/sankey-mount.ts +0 -622
  115. package/src/sankey-renderer.ts +0 -614
  116. package/src/series-search.ts +0 -392
  117. package/src/static.ts +0 -251
  118. package/src/story/__tests__/camera-math.test.ts +0 -145
  119. package/src/story/__tests__/create-chart-story.test.ts +0 -317
  120. package/src/story/__tests__/progress-math.test.ts +0 -95
  121. package/src/story/__tests__/scroll-driver.test.ts +0 -130
  122. package/src/story/__tests__/tween.test.ts +0 -159
  123. package/src/story/camera-math.ts +0 -125
  124. package/src/story/create-chart-story.ts +0 -353
  125. package/src/story/crossfade.ts +0 -70
  126. package/src/story/index.ts +0 -39
  127. package/src/story/progress-math.ts +0 -85
  128. package/src/story/resolve-camera-target.ts +0 -119
  129. package/src/story/scroll-driver.ts +0 -206
  130. package/src/story/story-camera.ts +0 -49
  131. package/src/story/tween.ts +0 -112
  132. package/src/story/types.ts +0 -76
  133. package/src/svg-ids.ts +0 -23
  134. package/src/svg-renderer.ts +0 -383
  135. package/src/table-keyboard.ts +0 -266
  136. package/src/table-mount.ts +0 -567
  137. package/src/table-renderer.ts +0 -414
  138. package/src/text-edit-overlay.ts +0 -255
  139. package/src/theme-tokens.ts +0 -51
  140. package/src/tilemap-mount.ts +0 -511
  141. package/src/tilemap-renderer.ts +0 -396
  142. package/src/tooltip.ts +0 -177
  143. package/src/transition.ts +0 -2816
  144. package/src/you-draw-it.ts +0 -480
  145. /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
@@ -1,622 +0,0 @@
1
- /**
2
- * Sankey mount API: the main entry point for vanilla JS sankey usage.
3
- *
4
- * createSankey() takes a container, SankeySpec, and options, compiles the
5
- * sankey, renders it as SVG, sets up responsive resizing, tooltip interaction,
6
- * hover highlighting, and returns a SankeyInstance with update/resize/export/destroy.
7
- */
8
-
9
- import type {
10
- CompileOptions,
11
- DarkMode,
12
- SankeyLayout,
13
- SankeySpec,
14
- ThemeConfig,
15
- } from '@opendata-ai/openchart-core';
16
- import { compileSankey } from '@opendata-ai/openchart-engine';
17
- import { cancelAnimations, setupAnimationCleanup } from './animation';
18
- import {
19
- exportJPG,
20
- exportPNG,
21
- exportSVG,
22
- exportSVGWithFonts,
23
- type JPGExportOptions,
24
- type SVGExportOptions,
25
- } from './export';
26
- import { createMeasureText, resolveFontFamily, scheduleFontReload } from './measure-text';
27
- import { observeResize } from './resize-observer';
28
- import { renderSankeySVG } from './sankey-renderer';
29
- import { createTooltipManager, type TooltipManager } from './tooltip';
30
-
31
- // ---------------------------------------------------------------------------
32
- // Types
33
- // ---------------------------------------------------------------------------
34
-
35
- export interface SankeyMountOptions {
36
- /** Theme overrides. */
37
- theme?: ThemeConfig;
38
- /** Dark mode setting: "auto" (system pref), "force", or "off". */
39
- darkMode?: DarkMode;
40
- /** Enable responsive resizing. Defaults to true. */
41
- responsive?: boolean;
42
- /** Show the tryOpenData.ai watermark. Defaults to true. */
43
- watermark?: boolean;
44
- /** Show tooltips on hover. Defaults to true. */
45
- tooltip?: boolean;
46
- /** Callback when a node is clicked. */
47
- onNodeClick?: (node: Record<string, unknown>) => void;
48
- /** Callback when a link is clicked. */
49
- onLinkClick?: (link: Record<string, unknown>) => void;
50
- /** Callback when a node is hovered (null on mouse leave). */
51
- onNodeHover?: (node: Record<string, unknown> | null) => void;
52
- /** Callback when a link is hovered (null on mouse leave). */
53
- onLinkHover?: (link: Record<string, unknown> | null) => void;
54
- }
55
-
56
- export interface SankeyInstance {
57
- /** Re-compile and re-render with a new spec. */
58
- update(spec: SankeySpec): void;
59
- /** Re-compile at current container dimensions. */
60
- resize(): void;
61
- /** Export the sankey diagram. */
62
- export(
63
- format: 'svg' | 'svg-with-fonts' | 'png' | 'jpg',
64
- options?: JPGExportOptions,
65
- ): string | Promise<Blob> | Promise<string>;
66
- /** Remove all DOM elements and disconnect observers. */
67
- destroy(): void;
68
- /** The current compiled layout. */
69
- readonly layout: SankeyLayout;
70
- }
71
-
72
- // ---------------------------------------------------------------------------
73
- // Dark mode resolution
74
- // ---------------------------------------------------------------------------
75
-
76
- function resolveDarkMode(mode?: DarkMode): boolean {
77
- if (mode === 'force') return true;
78
- if (mode === 'off' || mode === undefined) return false;
79
- if (typeof window !== 'undefined' && window.matchMedia) {
80
- return window.matchMedia('(prefers-color-scheme: dark)').matches;
81
- }
82
- return false;
83
- }
84
-
85
- // ---------------------------------------------------------------------------
86
- // Constants
87
- // ---------------------------------------------------------------------------
88
-
89
- /** Opacity for links connected to hovered node. */
90
- const HIGHLIGHT_OPACITY = 0.7;
91
- /** Opacity for links NOT connected to hovered node. */
92
- const DIM_OPACITY = 0.15;
93
- /** Opacity for nodes NOT connected to hovered node. */
94
- const NODE_DIM_OPACITY = 0.2;
95
-
96
- // ---------------------------------------------------------------------------
97
- // Main API
98
- // ---------------------------------------------------------------------------
99
-
100
- /**
101
- * Create a sankey instance from a spec and mount it into a container.
102
- */
103
- export function createSankey(
104
- container: HTMLElement,
105
- spec: SankeySpec,
106
- options?: SankeyMountOptions,
107
- ): SankeyInstance {
108
- let currentSpec = spec;
109
- let currentLayout: SankeyLayout;
110
- let destroyed = false;
111
-
112
- // DOM
113
- let svgElement: SVGSVGElement | null = null;
114
-
115
- // Subsystems
116
- let tooltipManager: TooltipManager | null = null;
117
- let cleanupTooltipEvents: (() => void) | null = null;
118
- let disconnectResize: (() => void) | null = null;
119
-
120
- // Animation state
121
- let isFirstRender = true;
122
- let animationCleanup: (() => void) | null = null;
123
- let pendingResize = false;
124
-
125
- // Set when webfonts have loaded and a recompile is owed. The next render()
126
- // that recompiles flips data-oc-fonts-state to 'ready' and clears this, so
127
- // the attribute stays honest when resize() defers to pendingResize during
128
- // the entrance animation.
129
- let fontsReloadPending = false;
130
-
131
- // Apply the root class up front so getComputedStyle sees --oc-font-family
132
- // before the text measurer is built.
133
- container.classList.add('oc-sankey-root');
134
-
135
- // Resolve the effective font the way compile() will: compile merges
136
- // { ...spec.theme, ...options.theme }, so options.theme wins over the
137
- // spec-level theme; fall back to the container's computed font. Measuring a
138
- // different font than gets rendered desyncs layout metrics and the reload watcher.
139
- function resolveEffectiveFont(): string {
140
- return (
141
- options?.theme?.fonts?.family ??
142
- currentSpec.theme?.fonts?.family ??
143
- resolveFontFamily(container)
144
- );
145
- }
146
- let fontFamily = resolveEffectiveFont();
147
- let measureText = createMeasureText(fontFamily);
148
- let renderGen = 0;
149
-
150
- // ---------------------------------------------------------------------------
151
- // Helpers
152
- // ---------------------------------------------------------------------------
153
-
154
- function getContainerDimensions(): { width: number; height: number } {
155
- const rect = container.getBoundingClientRect();
156
- return {
157
- width: Math.max(rect.width || 600, 100),
158
- height: Math.max(rect.height || 400, 100),
159
- };
160
- }
161
-
162
- function compile(): SankeyLayout {
163
- const { width, height } = getContainerDimensions();
164
- const darkMode = resolveDarkMode(options?.darkMode);
165
-
166
- const compileOpts: CompileOptions = {
167
- width,
168
- height,
169
- theme: options?.theme,
170
- darkMode,
171
- watermark: options?.watermark,
172
- measureText,
173
- };
174
-
175
- return compileSankey(currentSpec, compileOpts);
176
- }
177
-
178
- // ---------------------------------------------------------------------------
179
- // Tooltip and interaction wiring
180
- // ---------------------------------------------------------------------------
181
-
182
- function wireTooltipAndInteraction(svg: SVGSVGElement, layout: SankeyLayout): () => void {
183
- const cleanups: Array<() => void> = [];
184
-
185
- // Wire tooltip on node elements
186
- const nodeElements = svg.querySelectorAll('.oc-sankey-node');
187
- for (const el of nodeElements) {
188
- const markId = el.getAttribute('data-mark-id');
189
- if (!markId) continue;
190
-
191
- const content = layout.tooltipDescriptors.get(markId);
192
- const nodeId = el.getAttribute('data-node-id');
193
- const nodeData = nodeId ? (layout.nodes.find((n) => n.nodeId === nodeId)?.data ?? {}) : {};
194
-
195
- const handleMouseEnter = (e: Event) => {
196
- const mouseEvent = e as MouseEvent;
197
- if (content && tooltipManager) {
198
- const svgRect = svg.getBoundingClientRect();
199
- const x = mouseEvent.clientX - svgRect.left;
200
- const y = mouseEvent.clientY - svgRect.top;
201
- tooltipManager.show(content, x, y);
202
- }
203
- options?.onNodeHover?.(nodeData);
204
- if (nodeId) highlightConnectedLinks(svg, nodeId, layout);
205
- };
206
-
207
- const handleMouseMove = (e: Event) => {
208
- if (content && tooltipManager) {
209
- const mouseEvent = e as MouseEvent;
210
- const svgRect = svg.getBoundingClientRect();
211
- const x = mouseEvent.clientX - svgRect.left;
212
- const y = mouseEvent.clientY - svgRect.top;
213
- tooltipManager.show(content, x, y);
214
- }
215
- };
216
-
217
- const handleMouseLeave = () => {
218
- tooltipManager?.hide();
219
- options?.onNodeHover?.(null);
220
- resetLinkOpacity(svg, layout);
221
- };
222
-
223
- const handleClick = () => {
224
- options?.onNodeClick?.(nodeData);
225
- };
226
-
227
- el.addEventListener('mouseenter', handleMouseEnter);
228
- el.addEventListener('mousemove', handleMouseMove);
229
- el.addEventListener('mouseleave', handleMouseLeave);
230
- el.addEventListener('click', handleClick);
231
-
232
- cleanups.push(() => {
233
- el.removeEventListener('mouseenter', handleMouseEnter);
234
- el.removeEventListener('mousemove', handleMouseMove);
235
- el.removeEventListener('mouseleave', handleMouseLeave);
236
- el.removeEventListener('click', handleClick);
237
- });
238
- }
239
-
240
- // Wire tooltip on link elements
241
- const linkElements = svg.querySelectorAll('.oc-sankey-link');
242
- for (const el of linkElements) {
243
- const markId = el.getAttribute('data-mark-id');
244
- if (!markId) continue;
245
-
246
- const content = layout.tooltipDescriptors.get(markId);
247
- const sourceId = el.getAttribute('data-source');
248
- const targetId = el.getAttribute('data-target');
249
- const linkData = findLinkData(layout, sourceId, targetId);
250
-
251
- const handleMouseEnter = (e: Event) => {
252
- const mouseEvent = e as MouseEvent;
253
- if (content && tooltipManager) {
254
- const svgRect = svg.getBoundingClientRect();
255
- const x = mouseEvent.clientX - svgRect.left;
256
- const y = mouseEvent.clientY - svgRect.top;
257
- tooltipManager.show(content, x, y);
258
- }
259
- options?.onLinkHover?.(linkData);
260
- };
261
-
262
- const handleMouseMove = (e: Event) => {
263
- if (content && tooltipManager) {
264
- const mouseEvent = e as MouseEvent;
265
- const svgRect = svg.getBoundingClientRect();
266
- const x = mouseEvent.clientX - svgRect.left;
267
- const y = mouseEvent.clientY - svgRect.top;
268
- tooltipManager.show(content, x, y);
269
- }
270
- };
271
-
272
- const handleMouseLeave = () => {
273
- tooltipManager?.hide();
274
- options?.onLinkHover?.(null);
275
- };
276
-
277
- const handleClick = () => {
278
- options?.onLinkClick?.(linkData);
279
- };
280
-
281
- el.addEventListener('mouseenter', handleMouseEnter);
282
- el.addEventListener('mousemove', handleMouseMove);
283
- el.addEventListener('mouseleave', handleMouseLeave);
284
- el.addEventListener('click', handleClick);
285
-
286
- cleanups.push(() => {
287
- el.removeEventListener('mouseenter', handleMouseEnter);
288
- el.removeEventListener('mousemove', handleMouseMove);
289
- el.removeEventListener('mouseleave', handleMouseLeave);
290
- el.removeEventListener('click', handleClick);
291
- });
292
- }
293
-
294
- return () => {
295
- for (const cleanup of cleanups) {
296
- cleanup();
297
- }
298
- };
299
- }
300
-
301
- /**
302
- * Find the data record for a link by source/target ids.
303
- */
304
- function findLinkData(
305
- layout: SankeyLayout,
306
- sourceId: string | null,
307
- targetId: string | null,
308
- ): Record<string, unknown> {
309
- if (!sourceId || !targetId) return {};
310
- const link = layout.links.find((l) => l.sourceId === sourceId && l.targetId === targetId);
311
- return link?.data ?? {};
312
- }
313
-
314
- /**
315
- * Highlight links connected to a node and dim unconnected links.
316
- */
317
- function highlightConnectedLinks(
318
- svg: SVGSVGElement,
319
- nodeId: string,
320
- _layout: SankeyLayout,
321
- ): void {
322
- // Collect connected node IDs (the hovered node + its direct neighbors)
323
- const connectedNodeIds = new Set<string>([nodeId]);
324
- const linkElements = svg.querySelectorAll('.oc-sankey-link');
325
- for (const el of linkElements) {
326
- const source = el.getAttribute('data-source');
327
- const target = el.getAttribute('data-target');
328
- const path = el.querySelector('path');
329
- if (!path) continue;
330
-
331
- const isConnected = source === nodeId || target === nodeId;
332
- path.setAttribute('fill-opacity', String(isConnected ? HIGHLIGHT_OPACITY : DIM_OPACITY));
333
- if (isConnected) {
334
- if (source) connectedNodeIds.add(source);
335
- if (target) connectedNodeIds.add(target);
336
- }
337
- }
338
-
339
- // Dim unconnected nodes (rect + label)
340
- const nodeElements = svg.querySelectorAll('.oc-sankey-node');
341
- for (const el of nodeElements) {
342
- const nid = el.getAttribute('data-node-id');
343
- if (!nid) continue;
344
- const isConnected = connectedNodeIds.has(nid);
345
- (el as SVGElement).style.opacity = isConnected ? '1' : String(NODE_DIM_OPACITY);
346
- }
347
- }
348
-
349
- /**
350
- * Reset all link opacities and node opacities to their original values.
351
- */
352
- function resetLinkOpacity(svg: SVGSVGElement, layout: SankeyLayout): void {
353
- const linkElements = svg.querySelectorAll('.oc-sankey-link');
354
- for (const el of linkElements) {
355
- const path = el.querySelector('path');
356
- if (!path) continue;
357
- // Look up the original opacity via data attributes rather than positional index
358
- const source = el.getAttribute('data-source');
359
- const target = el.getAttribute('data-target');
360
- const link = layout.links.find((l) => l.sourceId === source && l.targetId === target);
361
- path.setAttribute('fill-opacity', String(link?.fillOpacity ?? 0.5));
362
- }
363
-
364
- // Restore all node opacities
365
- const nodeElements = svg.querySelectorAll('.oc-sankey-node');
366
- for (const el of nodeElements) {
367
- (el as SVGElement).style.opacity = '1';
368
- }
369
- }
370
-
371
- // ---------------------------------------------------------------------------
372
- // Render
373
- // ---------------------------------------------------------------------------
374
-
375
- function render(): void {
376
- if (destroyed) return;
377
-
378
- // Cancel in-progress animations before tearing down
379
- if (animationCleanup) {
380
- animationCleanup();
381
- animationCleanup = null;
382
- }
383
- if (svgElement) {
384
- cancelAnimations(svgElement);
385
- }
386
-
387
- // Clean up previous tooltip listeners
388
- if (cleanupTooltipEvents) {
389
- cleanupTooltipEvents();
390
- cleanupTooltipEvents = null;
391
- }
392
-
393
- // Remove old SVG
394
- if (svgElement?.parentNode) {
395
- svgElement.parentNode.removeChild(svgElement);
396
- }
397
-
398
- // Compile
399
- currentLayout = compile();
400
-
401
- // Determine if we should animate
402
- const shouldAnimate = isFirstRender && !!currentLayout.animation?.enter;
403
- isFirstRender = false;
404
-
405
- // Render
406
- const animation = shouldAnimate ? currentLayout.animation : undefined;
407
- svgElement = renderSankeySVG(currentLayout, animation);
408
- container.appendChild(svgElement);
409
-
410
- // Dark mode class on container
411
- const isDark = resolveDarkMode(options?.darkMode);
412
- if (isDark) {
413
- container.classList.add('oc-dark');
414
- } else {
415
- container.classList.remove('oc-dark');
416
- }
417
-
418
- // Wire tooltip + interaction events
419
- if (options?.tooltip !== false && svgElement) {
420
- if (!tooltipManager) {
421
- tooltipManager = createTooltipManager(container);
422
- }
423
- cleanupTooltipEvents = wireTooltipAndInteraction(svgElement, currentLayout);
424
- }
425
-
426
- // Animation cleanup on first render
427
- if (shouldAnimate && svgElement) {
428
- animationCleanup = setupAnimationCleanup(svgElement, () => {
429
- animationCleanup = null;
430
- if (pendingResize) {
431
- pendingResize = false;
432
- resize();
433
- }
434
- });
435
- }
436
-
437
- renderGen += 1;
438
- container.dataset.ocRenderGen = String(renderGen);
439
-
440
- // This render recompiled with the loaded webfonts; publish 'ready' now
441
- // rather than right after the fonts-ready resize() (which may have deferred
442
- // to pendingResize during the entrance animation).
443
- if (fontsReloadPending) {
444
- fontsReloadPending = false;
445
- container.dataset.ocFontsState = 'ready';
446
- }
447
- }
448
-
449
- // ---------------------------------------------------------------------------
450
- // Public API
451
- // ---------------------------------------------------------------------------
452
-
453
- function update(newSpec: SankeySpec): void {
454
- if (destroyed) return;
455
- currentSpec = newSpec;
456
- // A new spec can change theme.fonts.family; rebuild the measurer so layout
457
- // measures the font compile will actually render with.
458
- const nextFont = resolveEffectiveFont();
459
- if (nextFont !== fontFamily) {
460
- fontFamily = nextFont;
461
- measureText = createMeasureText(fontFamily);
462
- }
463
- isFirstRender = true; // Allow animation on update
464
- render();
465
- }
466
-
467
- function resize(): void {
468
- if (destroyed) return;
469
- // Skip resize during entrance animation to avoid tearing down the animated SVG.
470
- // Queued resizes replay once animation completes.
471
- if (animationCleanup) {
472
- pendingResize = true;
473
- return;
474
- }
475
- render();
476
- }
477
-
478
- function doExport(
479
- format: 'svg' | 'svg-with-fonts' | 'png' | 'jpg',
480
- exportOptions?: JPGExportOptions,
481
- ): string | Promise<Blob> | Promise<string> {
482
- if (!svgElement) {
483
- throw new Error('Sankey is not rendered yet');
484
- }
485
-
486
- switch (format) {
487
- case 'svg':
488
- return exportSVG(svgElement);
489
- case 'svg-with-fonts':
490
- return exportSVGWithFonts(svgElement, exportOptions as SVGExportOptions);
491
- case 'png':
492
- return exportPNG(svgElement, exportOptions);
493
- case 'jpg':
494
- return exportJPG(svgElement, exportOptions);
495
- default:
496
- throw new Error(`Unsupported export format: ${format}`);
497
- }
498
- }
499
-
500
- function destroy(): void {
501
- if (destroyed) return;
502
- destroyed = true;
503
-
504
- // Cancel entrance animations
505
- if (animationCleanup) {
506
- animationCleanup();
507
- animationCleanup = null;
508
- pendingResize = false;
509
- }
510
- if (svgElement) {
511
- cancelAnimations(svgElement);
512
- }
513
-
514
- // Disconnect resize observer
515
- if (disconnectResize) {
516
- disconnectResize();
517
- disconnectResize = null;
518
- }
519
-
520
- // Clean up tooltip events
521
- if (cleanupTooltipEvents) {
522
- cleanupTooltipEvents();
523
- cleanupTooltipEvents = null;
524
- }
525
-
526
- // Destroy tooltip manager
527
- if (tooltipManager) {
528
- tooltipManager.destroy();
529
- tooltipManager = null;
530
- }
531
-
532
- // Remove SVG
533
- if (svgElement?.parentNode) {
534
- svgElement.parentNode.removeChild(svgElement);
535
- }
536
- svgElement = null;
537
-
538
- container.classList.remove('oc-dark');
539
- container.classList.remove('oc-sankey-root');
540
- }
541
-
542
- // ---------------------------------------------------------------------------
543
- // Initialize
544
- // ---------------------------------------------------------------------------
545
-
546
- try {
547
- currentLayout = compile();
548
-
549
- // Determine if we should animate
550
- const shouldAnimate = !!currentLayout.animation?.enter;
551
- isFirstRender = false;
552
-
553
- // Render
554
- const animation = shouldAnimate ? currentLayout.animation : undefined;
555
- svgElement = renderSankeySVG(currentLayout, animation);
556
- container.appendChild(svgElement);
557
-
558
- // Dark mode class on container
559
- const isDark = resolveDarkMode(options?.darkMode);
560
- if (isDark) {
561
- container.classList.add('oc-dark');
562
- } else {
563
- container.classList.remove('oc-dark');
564
- }
565
-
566
- // Wire tooltip + interaction events
567
- if (options?.tooltip !== false && svgElement) {
568
- tooltipManager = createTooltipManager(container);
569
- cleanupTooltipEvents = wireTooltipAndInteraction(svgElement, currentLayout);
570
- }
571
-
572
- // Animation cleanup
573
- if (shouldAnimate && svgElement) {
574
- animationCleanup = setupAnimationCleanup(svgElement, () => {
575
- animationCleanup = null;
576
- if (pendingResize) {
577
- pendingResize = false;
578
- resize();
579
- }
580
- });
581
- }
582
-
583
- renderGen += 1;
584
- container.dataset.ocRenderGen = String(renderGen);
585
- } catch (err) {
586
- console.error('[viz] Sankey mount failed:', err);
587
- // Re-throw so callers can handle the error rather than silently returning a broken instance
588
- throw err;
589
- }
590
-
591
- // Recompile once after webfonts load so late-swapping fonts don't leave
592
- // node labels measured against fallback metrics.
593
- const fontsPending = scheduleFontReload(
594
- fontFamily,
595
- () => !destroyed,
596
- () => {
597
- // Mark the reload owed, then recompile. render() flips the attribute to
598
- // 'ready' once it actually recompiles, including the pendingResize replay
599
- // after the entrance animation finishes.
600
- fontsReloadPending = true;
601
- resize();
602
- },
603
- );
604
- container.dataset.ocFontsState = fontsPending ? 'pending' : 'ready';
605
-
606
- // Responsive resize
607
- if (options?.responsive !== false) {
608
- disconnectResize = observeResize(container, () => {
609
- resize();
610
- });
611
- }
612
-
613
- return {
614
- update,
615
- resize,
616
- export: doExport,
617
- destroy,
618
- get layout() {
619
- return currentLayout;
620
- },
621
- };
622
- }