@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,4 +1,5 @@
1
- import { ChartSpec, LayerSpec, GraphSpec, ElementRef, ChartLayout, ChartEventHandlers, ThemeConfig, DarkMode, DataRow } from '@opendata-ai/openchart-core';
1
+ import { ResolvedTheme, ChartSpec, LayerSpec, GraphSpec, ElementRef, ChartLayout, ChartEventHandlers, ThemeConfig, DarkMode, DataRow } from '@opendata-ai/openchart-core';
2
+ import { GIFExportOptions } from './export-gif.js';
2
3
 
3
4
  /**
4
5
  * Export utilities: serialize charts to SVG, PNG, JPG, or CSV.
@@ -9,9 +10,17 @@ import { ChartSpec, LayerSpec, GraphSpec, ElementRef, ChartLayout, ChartEventHan
9
10
  * - JPG: same as PNG but with JPEG compression and background fill
10
11
  * - CSV: converts a data array to comma-separated text
11
12
  */
13
+
12
14
  interface SVGExportOptions {
13
15
  /** Embed fonts as base64 data URIs in the SVG. Defaults to true. */
14
16
  embedFonts?: boolean;
17
+ /**
18
+ * Resolved theme to inline as a `<style>` block, so class-based fills (metric
19
+ * cells, brand watermark dot, legend text, endpoint labels) survive
20
+ * serialization away from the page stylesheet. Omit to skip — a chart whose
21
+ * chrome is all inline-filled exports fine without it.
22
+ */
23
+ theme?: ResolvedTheme;
15
24
  }
16
25
  interface PNGExportOptions extends SVGExportOptions {
17
26
  /** DPI scaling factor. Defaults to 2 for retina-quality output. */
@@ -133,6 +142,103 @@ declare function camerasClose(a: Camera, b: Camera, epsilon?: number): boolean;
133
142
  */
134
143
  declare function scrubCamera(step: number, stepProgress: number, fitted: Camera[], holdUntil: number, ease: (t: number) => number): Camera;
135
144
 
145
+ /**
146
+ * Data-update transition driver for bar/column, line, area, scatter/dot,
147
+ * rule, tick, and text charts. Also transitions axis ticks, tick labels,
148
+ * and gridlines so the whole chart adjusts as one system.
149
+ *
150
+ * Matches marks across layout snapshots by `key`, then animates geometry
151
+ * changes (position, size, path) using a single rAF loop. Enter/exit/update
152
+ * choreography runs in one timeline with cubic-out easing for a smooth
153
+ * deceleration feel.
154
+ *
155
+ * For rect marks: tweens position/size directly.
156
+ * For line/area marks: normalizes point arrays to equal length, interpolates
157
+ * point positions per frame, and rebuilds SVG paths via d3 curve generators.
158
+ * For point marks (scatter/dot/line overlay): tweens cx/cy/r.
159
+ * For rule/tick marks: tweens line endpoints (x1/y1/x2/y2).
160
+ * For text marks: tweens x/y position.
161
+ * For axis ticks/gridlines: matches by value key and tweens positions.
162
+ *
163
+ * Ghost elements handle exits: cloned into the marks container, they
164
+ * collapse + fade out then get removed. No CSS animations are used here
165
+ * because transitions tween computed geometry, not class-toggled states.
166
+ */
167
+
168
+ /**
169
+ * Snapshot of in-flight tween geometry, keyed by mark key.
170
+ * Used for interruption retargeting: the next transition starts from
171
+ * whatever position the previous transition had reached.
172
+ */
173
+ type GeometrySnapshot = Map<string, SnapshotGeometry>;
174
+ type SnapshotGeometry = {
175
+ type: 'rect';
176
+ x: number;
177
+ y: number;
178
+ width: number;
179
+ height: number;
180
+ } | {
181
+ type: 'point';
182
+ cx: number;
183
+ cy: number;
184
+ r?: number;
185
+ } | {
186
+ type: 'rule';
187
+ x1: number;
188
+ y1: number;
189
+ x2: number;
190
+ y2: number;
191
+ } | {
192
+ type: 'tick';
193
+ x1: number;
194
+ y1: number;
195
+ x2: number;
196
+ y2: number;
197
+ } | {
198
+ type: 'textMark';
199
+ x: number;
200
+ y: number;
201
+ } | {
202
+ type: 'line';
203
+ d: string;
204
+ } | {
205
+ type: 'area';
206
+ d: string;
207
+ topD?: string;
208
+ };
209
+ interface TransitionHandle {
210
+ cancel(): void;
211
+ readonly running: boolean;
212
+ /** Capture current interpolated geometry for all in-flight tweens. */
213
+ snapshot(): GeometrySnapshot;
214
+ /**
215
+ * Apply the transition's DOM state at an explicit elapsed time (ms), without
216
+ * the rAF clock. Only meaningful when the transition was started with
217
+ * `manual: true` (which suppresses the internal rAF loop). Used by headless
218
+ * frame capture (GIF export) to sample the tween deterministically at each
219
+ * frame's timestamp. Returns true while the transition is still in progress
220
+ * (elapsed < total), false once it has reached its end.
221
+ */
222
+ step(elapsedMs: number): boolean;
223
+ /** The transition's total duration in ms (update/exit, whichever is longer). */
224
+ readonly totalMs: number;
225
+ }
226
+ /**
227
+ * Default cap on the mark count that still runs a tweened data-update
228
+ * transition. Override per chart with `animation.update.maxMarks`.
229
+ */
230
+ declare const DEFAULT_UPDATE_MAX_MARKS = 500;
231
+ /**
232
+ * Default cap when the destination layout renders its points on canvas.
233
+ *
234
+ * Two orders of magnitude above the SVG cap because the cost model is
235
+ * different: a canvas frame writes into typed arrays and issues one batched
236
+ * fill per color bucket, where the SVG path writes attributes on one DOM
237
+ * element per mark. The cap that keeps low-end devices honest for DOM writes
238
+ * would veto exactly the high-cardinality morphs canvas mode exists to serve.
239
+ */
240
+ declare const CANVAS_DEFAULT_UPDATE_MAX_MARKS = 20000;
241
+
136
242
  /**
137
243
  * Mount API: the main entry point for vanilla JS usage.
138
244
  *
@@ -165,6 +271,13 @@ interface ExportOptions extends JPGExportOptions {
165
271
  interface ChartInstance {
166
272
  /** Re-compile and re-render with a new spec. */
167
273
  update(spec: ChartSpec | LayerSpec | GraphSpec, options?: UpdateOptions): void;
274
+ /**
275
+ * Re-render with a new spec and start a MANUAL update transition the caller
276
+ * drives via `handle.step(elapsedMs)`, with no rAF clock. Returns the handle,
277
+ * or null when no transition applies (the render already swapped instantly).
278
+ * For headless frame capture (e.g. GIF export of a spec-swap stepper).
279
+ */
280
+ beginManualUpdate(spec: ChartSpec | LayerSpec | GraphSpec): TransitionHandle | null;
168
281
  /** Re-compile at current container dimensions. */
169
282
  resize(): void;
170
283
  /** Export the chart. */
@@ -172,8 +285,9 @@ interface ChartInstance {
172
285
  export(format: 'svg-with-fonts', options?: SVGExportOptions): Promise<string>;
173
286
  export(format: 'png', options?: ExportOptions): Promise<Blob>;
174
287
  export(format: 'jpg', options?: ExportOptions): Promise<Blob>;
288
+ export(format: 'gif', options?: GIFExportOptions): Promise<Blob>;
175
289
  export(format: 'csv'): string;
176
- export(format: 'svg' | 'svg-with-fonts' | 'png' | 'jpg' | 'csv', options?: ExportOptions): string | Promise<Blob> | Promise<string>;
290
+ export(format: 'svg' | 'svg-with-fonts' | 'png' | 'jpg' | 'gif' | 'csv', options?: ExportOptions | GIFExportOptions): string | Promise<Blob> | Promise<string>;
177
291
  /** Remove all DOM elements and disconnect observers. */
178
292
  destroy(): void;
179
293
  /** The current compiled layout (for hooks / debugging). */
@@ -204,4 +318,4 @@ interface ChartInstance {
204
318
  */
205
319
  declare function createChart<TData extends DataRow = DataRow>(container: HTMLElement, spec: ChartSpec<TData> | LayerSpec<TData> | GraphSpec, options?: MountOptions): ChartInstance;
206
320
 
207
- export { type Camera as C, type ExportOptions as E, FULL_VIEW as F, type JPGExportOptions as J, type MountOptions as M, type PNGExportOptions as P, type SVGExportOptions as S, type UpdateOptions as U, type ViewBoxSize as V, type ChartInstance as a, exportJPG as b, createChart as c, exportPNG as d, exportCSV as e, exportSVG as f, exportSVGWithFonts as g, type CameraTarget as h, cameraTransform as i, camerasClose as j, damp as k, dampCamera as l, fitTarget as m, interpolateCamera as n, scrubCamera as s };
321
+ export { type Camera as C, DEFAULT_UPDATE_MAX_MARKS as D, type ExportOptions as E, FULL_VIEW as F, type JPGExportOptions as J, type MountOptions as M, type PNGExportOptions as P, type SVGExportOptions as S, type UpdateOptions as U, type ViewBoxSize as V, CANVAS_DEFAULT_UPDATE_MAX_MARKS as a, type ChartInstance as b, createChart as c, exportJPG as d, exportCSV as e, exportPNG as f, exportSVG as g, exportSVGWithFonts as h, type CameraTarget as i, cameraTransform as j, camerasClose as k, damp as l, dampCamera as m, fitTarget as n, interpolateCamera as o, scrubCamera as s };
@@ -1101,9 +1101,40 @@
1101
1101
  }
1102
1102
  };
1103
1103
  }
1104
+ function forceCursor(nodes, pointer) {
1105
+ return (alpha) => {
1106
+ if (!pointer.active || pointer.radius <= 0)
1107
+ return;
1108
+ const r2 = pointer.radius * pointer.radius;
1109
+ const k = pointer.strength * alpha;
1110
+ for (const node of nodes) {
1111
+ const dx = (node.x ?? 0) - pointer.x;
1112
+ const dy = (node.y ?? 0) - pointer.y;
1113
+ const dist2 = dx * dx + dy * dy;
1114
+ if (dist2 >= r2)
1115
+ continue;
1116
+ const dist = Math.sqrt(dist2) || 0.000001;
1117
+ const falloff = (pointer.radius - dist) / pointer.radius;
1118
+ const push = k * falloff / dist;
1119
+ node.vx = (node.vx ?? 0) + dx * push;
1120
+ node.vy = (node.vy ?? 0) + dy * push;
1121
+ }
1122
+ };
1123
+ }
1104
1124
  var ctx = self;
1105
1125
  var simulation = null;
1106
1126
  var nodeMap = new Map;
1127
+ var pointer = { x: 0, y: 0, active: false, radius: 0, strength: 0 };
1128
+ var dragAlphaTarget = 0;
1129
+ var cursorAlphaTarget = 0;
1130
+ function syncAlphaTarget() {
1131
+ if (!simulation)
1132
+ return;
1133
+ simulation.alphaTarget(Math.max(dragAlphaTarget, cursorAlphaTarget));
1134
+ }
1135
+ function now2() {
1136
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1137
+ }
1107
1138
  function post(msg) {
1108
1139
  ctx.postMessage(msg);
1109
1140
  }
@@ -1135,7 +1166,7 @@
1135
1166
  linkForce.strength(config.linkStrength);
1136
1167
  }
1137
1168
  const padding = config.collisionPadding ?? 2;
1138
- simulation = simulation_default(internalNodes).force("link", linkForce).force("charge", manyBody_default().strength(config.chargeStrength)).force("collide", collide_default().radius((d) => d.radius + padding)).force("gravityX", x_default2(0).strength(0.05)).force("gravityY", y_default2(0).strength(0.05)).alphaDecay(config.alphaDecay).velocityDecay(config.velocityDecay);
1169
+ simulation = simulation_default(internalNodes).force("link", linkForce).force("charge", manyBody_default().strength(config.chargeStrength)).force("collide", collide_default().radius((d) => d.radius + padding)).force("gravityX", x_default2(0).strength(0.05)).force("gravityY", y_default2(0).strength(0.05)).alphaDecay(config.alphaDecay).velocityDecay(config.velocityDecay).stop();
1139
1170
  if (config.centerForce !== false) {
1140
1171
  simulation.force("center", center_default(0, 0));
1141
1172
  }
@@ -1143,6 +1174,28 @@
1143
1174
  const clusterFn = forceCluster(internalNodes, config.clustering.strength);
1144
1175
  simulation.force("cluster", clusterFn);
1145
1176
  }
1177
+ pointer.active = false;
1178
+ pointer.radius = config.cursorRepulsion?.radius ?? 0;
1179
+ pointer.strength = config.cursorRepulsion?.strength ?? 0;
1180
+ dragAlphaTarget = 0;
1181
+ cursorAlphaTarget = 0;
1182
+ const cursorFn = forceCursor(internalNodes, pointer);
1183
+ simulation.force("cursor", cursorFn);
1184
+ if (config.initialAlpha != null) {
1185
+ simulation.alpha(config.initialAlpha);
1186
+ }
1187
+ const warmupTicks = config.warmupTicks ?? 0;
1188
+ if (warmupTicks > 0) {
1189
+ const budgetMs = config.warmupBudgetMs ?? 250;
1190
+ const start = now2();
1191
+ for (let i = 0;i < warmupTicks; i++) {
1192
+ simulation.tick();
1193
+ if (simulation.alpha() < simulation.alphaMin())
1194
+ break;
1195
+ if (now2() - start >= budgetMs)
1196
+ break;
1197
+ }
1198
+ }
1146
1199
  simulation.on("tick", () => {
1147
1200
  post({
1148
1201
  type: "positions",
@@ -1153,6 +1206,12 @@
1153
1206
  simulation.on("end", () => {
1154
1207
  post({ type: "settled" });
1155
1208
  });
1209
+ post({
1210
+ type: "positions",
1211
+ nodes: packPositions(internalNodes),
1212
+ alpha: simulation.alpha()
1213
+ });
1214
+ simulation.restart();
1156
1215
  break;
1157
1216
  }
1158
1217
  case "reheat": {
@@ -1167,6 +1226,11 @@
1167
1226
  node.fx = msg.x;
1168
1227
  node.fy = msg.y;
1169
1228
  }
1229
+ if (msg.alphaTarget != null && simulation) {
1230
+ dragAlphaTarget = msg.alphaTarget;
1231
+ syncAlphaTarget();
1232
+ simulation.restart();
1233
+ }
1170
1234
  break;
1171
1235
  }
1172
1236
  case "unpin": {
@@ -1175,11 +1239,28 @@
1175
1239
  node.fx = null;
1176
1240
  node.fy = null;
1177
1241
  }
1178
- if (simulation && simulation.alpha() < 0.1) {
1242
+ if (msg.alphaTarget != null) {
1243
+ dragAlphaTarget = msg.alphaTarget;
1244
+ syncAlphaTarget();
1245
+ } else if (simulation && simulation.alpha() < 0.1) {
1179
1246
  simulation.alpha(0.1).restart();
1180
1247
  }
1181
1248
  break;
1182
1249
  }
1250
+ case "pointer": {
1251
+ if (pointer.radius <= 0)
1252
+ break;
1253
+ pointer.x = msg.x;
1254
+ pointer.y = msg.y;
1255
+ pointer.active = msg.active;
1256
+ cursorAlphaTarget = msg.active ? 0.03 : 0;
1257
+ if (simulation) {
1258
+ syncAlphaTarget();
1259
+ if (msg.active)
1260
+ simulation.restart();
1261
+ }
1262
+ break;
1263
+ }
1183
1264
  case "drag": {
1184
1265
  const node = nodeMap.get(msg.nodeId);
1185
1266
  if (node) {
package/dist/static.js CHANGED
@@ -1,20 +1,18 @@
1
1
  import {
2
2
  renderTileMapSVG
3
- } from "./chunk-W34KN7FD.js";
3
+ } from "./chunk-2NAQRP66.js";
4
4
  import {
5
- SVG_NS,
6
5
  renderChartSVG,
7
6
  resetSvgIdCounter
8
- } from "./chunk-5EOV5JQZ.js";
7
+ } from "./chunk-EKAUQAAX.js";
8
+ import {
9
+ SVG_NS,
10
+ buildThemeStyleBlock
11
+ } from "./chunk-RM5XS6NR.js";
9
12
 
10
13
  // src/static.ts
11
14
  import { createRequire } from "module";
12
- import {
13
- adaptForLightLineStroke,
14
- cssTokenDefault,
15
- isLayerSpec,
16
- isTileMapSpec
17
- } from "@opendata-ai/openchart-core";
15
+ import { isLayerSpec, isTileMapSpec } from "@opendata-ai/openchart-core";
18
16
  import { compileChart, compileLayer, compileTileMap } from "@opendata-ai/openchart-engine";
19
17
  var esmRequire = createRequire(import.meta.url);
20
18
  var cachedWindow;
@@ -34,66 +32,6 @@ function resolveStaticDarkMode(mode) {
34
32
  if (mode === "force") return true;
35
33
  return false;
36
34
  }
37
- function buildThemeStyleBlock(theme) {
38
- const accent = theme.colors.categorical[0] ?? cssTokenDefault("--oc-accent", "light");
39
- const bg = theme.colors.background === "transparent" ? theme.isDark ? cssTokenDefault("--oc-bg", "dark") : cssTokenDefault("--oc-bg", "light") : theme.colors.background;
40
- const props = [
41
- `--oc-font-family: ${theme.fonts.family}`,
42
- `--oc-font-mono: ${theme.fonts.mono}`,
43
- `--oc-title-size: ${theme.chrome.title.fontSize}px`,
44
- `--oc-title-weight: ${theme.chrome.title.fontWeight}`,
45
- `--oc-title-tracking: ${cssTokenDefault("--oc-title-tracking", "light")}`,
46
- `--oc-subtitle-size: ${theme.chrome.subtitle.fontSize}px`,
47
- `--oc-subtitle-weight: ${theme.chrome.subtitle.fontWeight}`,
48
- `--oc-source-size: ${theme.chrome.source.fontSize}px`,
49
- `--oc-source-weight: ${theme.chrome.source.fontWeight}`,
50
- `--oc-body-size: ${theme.fonts.sizes.body}px`,
51
- `--oc-eyebrow-size: ${theme.chrome.eyebrow.fontSize}px`,
52
- `--oc-eyebrow-weight: ${theme.chrome.eyebrow.fontWeight}`,
53
- `--oc-eyebrow-tracking: ${cssTokenDefault("--oc-eyebrow-tracking", "light")}`,
54
- `--oc-bg: ${bg}`,
55
- `--oc-text: ${theme.colors.text}`,
56
- `--oc-text-muted: ${theme.colors.axis}`,
57
- `--oc-text-faint: ${theme.isDark ? cssTokenDefault("--oc-text-faint", "dark") : cssTokenDefault("--oc-text-faint", "light")}`,
58
- `--oc-gridline: ${theme.colors.gridline}`,
59
- `--oc-axis: ${theme.colors.axis}`,
60
- `--oc-border-radius: ${theme.borderRadius}px`,
61
- `--oc-accent: ${accent}`,
62
- `--oc-accent-strong: ${adaptForLightLineStroke(accent)}`,
63
- `--oc-positive: ${theme.colors.positive}`,
64
- `--oc-negative: ${theme.colors.negative}`,
65
- `--oc-legend-text: ${theme.isDark ? cssTokenDefault("--oc-legend-text", "dark") : cssTokenDefault("--oc-legend-text", "light")}`,
66
- `--oc-space-2: ${theme.spacing.chromeGap * 2}px`,
67
- `--oc-space-4: ${theme.spacing.padding}px`
68
- ];
69
- const rules = [
70
- `svg.oc-chart { ${props.join("; ")}; }`,
71
- `.oc-chrome { font-family: var(--oc-font-family); }`,
72
- `.oc-eyebrow { font-size: var(--oc-eyebrow-size); font-weight: var(--oc-eyebrow-weight); letter-spacing: var(--oc-eyebrow-tracking); text-transform: uppercase; fill: var(--oc-accent); }`,
73
- `.oc-title { font-size: var(--oc-title-size); font-weight: var(--oc-title-weight); letter-spacing: var(--oc-title-tracking); fill: var(--oc-text); }`,
74
- `.oc-subtitle { font-size: var(--oc-subtitle-size); font-weight: var(--oc-subtitle-weight); fill: var(--oc-text-muted); }`,
75
- `.oc-source, .oc-byline, .oc-footer { font-size: var(--oc-source-size); font-weight: var(--oc-source-weight); fill: var(--oc-text-muted); }`,
76
- `.oc-brand { font-size: 11px; font-weight: 510; letter-spacing: 0.02em; fill: var(--oc-text-faint); }`,
77
- `.oc-brand-dot { fill: var(--oc-accent); }`,
78
- `.oc-eyebrow-dot { fill: var(--oc-accent); }`,
79
- `.oc-metrics { font-family: var(--oc-font-family); }`,
80
- `.oc-metric-label { font-size: 10px; font-weight: 510; letter-spacing: 0.08em; text-transform: uppercase; fill: var(--oc-text-muted); }`,
81
- `.oc-metric-value { font-size: 22px; font-weight: 510; letter-spacing: -0.01em; fill: var(--oc-text); font-variant-numeric: tabular-nums; }`,
82
- `.oc-metric-delta-up { fill: var(--oc-positive); font-size: 12px; font-weight: 510; }`,
83
- `.oc-metric-delta-down { fill: var(--oc-negative); font-size: 12px; font-weight: 510; }`,
84
- `.oc-axis-tick-inline { font-size: 11px; font-weight: 400; fill: var(--oc-text-muted); }`,
85
- `.oc-endpoint-labels { font-family: var(--oc-font-family); }`,
86
- `.oc-endpoint-label { fill: var(--oc-endpoint-label-color, var(--oc-text)); }`,
87
- `.oc-endpoint-value { fill: var(--oc-endpoint-value-color, var(--oc-text-muted)); }`,
88
- `.oc-endpoint-leader { stroke: var(--oc-endpoint-leader-color, currentColor); }`,
89
- `.oc-annotation-subtitle { fill: var(--oc-annotation-subtitle-color, var(--oc-text-muted)); }`,
90
- `.oc-metric-secondary { fill: var(--oc-positive); font-size: 12px; font-weight: 400; }`,
91
- `.oc-legend { font-family: var(--oc-font-family); font-size: var(--oc-body-size); }`,
92
- `.oc-legend-entry { cursor: default; }`,
93
- `.oc-legend text { fill: var(--oc-legend-text); }`
94
- ];
95
- return rules.join("\n");
96
- }
97
35
  function stripInteractiveElements(svg) {
98
36
  const selectors = ["[data-voronoi-overlay]", "[data-crosshair]", "[data-snap-dots]"];
99
37
  for (const selector of selectors) {
@@ -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 {\n adaptForLightLineStroke,\n cssTokenDefault,\n isLayerSpec,\n isTileMapSpec,\n} 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 { 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 watermark?: boolean;\n}\n\nfunction resolveStaticDarkMode(mode?: DarkMode): boolean {\n if (mode === 'force') return true;\n return false;\n}\n\nfunction buildThemeStyleBlock(theme: ResolvedTheme): string {\n const accent = theme.colors.categorical[0] ?? cssTokenDefault('--oc-accent', 'light');\n const bg =\n theme.colors.background === 'transparent'\n ? theme.isDark\n ? cssTokenDefault('--oc-bg', 'dark')\n : cssTokenDefault('--oc-bg', 'light')\n : theme.colors.background;\n\n const props = [\n `--oc-font-family: ${theme.fonts.family}`,\n `--oc-font-mono: ${theme.fonts.mono}`,\n `--oc-title-size: ${theme.chrome.title.fontSize}px`,\n `--oc-title-weight: ${theme.chrome.title.fontWeight}`,\n `--oc-title-tracking: ${cssTokenDefault('--oc-title-tracking', 'light')}`,\n `--oc-subtitle-size: ${theme.chrome.subtitle.fontSize}px`,\n `--oc-subtitle-weight: ${theme.chrome.subtitle.fontWeight}`,\n `--oc-source-size: ${theme.chrome.source.fontSize}px`,\n `--oc-source-weight: ${theme.chrome.source.fontWeight}`,\n `--oc-body-size: ${theme.fonts.sizes.body}px`,\n `--oc-eyebrow-size: ${theme.chrome.eyebrow.fontSize}px`,\n `--oc-eyebrow-weight: ${theme.chrome.eyebrow.fontWeight}`,\n `--oc-eyebrow-tracking: ${cssTokenDefault('--oc-eyebrow-tracking', 'light')}`,\n `--oc-bg: ${bg}`,\n `--oc-text: ${theme.colors.text}`,\n `--oc-text-muted: ${theme.colors.axis}`,\n `--oc-text-faint: ${theme.isDark ? cssTokenDefault('--oc-text-faint', 'dark') : cssTokenDefault('--oc-text-faint', 'light')}`,\n `--oc-gridline: ${theme.colors.gridline}`,\n `--oc-axis: ${theme.colors.axis}`,\n `--oc-border-radius: ${theme.borderRadius}px`,\n `--oc-accent: ${accent}`,\n `--oc-accent-strong: ${adaptForLightLineStroke(accent)}`,\n `--oc-positive: ${theme.colors.positive}`,\n `--oc-negative: ${theme.colors.negative}`,\n `--oc-legend-text: ${theme.isDark ? cssTokenDefault('--oc-legend-text', 'dark') : cssTokenDefault('--oc-legend-text', 'light')}`,\n `--oc-space-2: ${theme.spacing.chromeGap * 2}px`,\n `--oc-space-4: ${theme.spacing.padding}px`,\n ];\n\n const rules = [\n `svg.oc-chart { ${props.join('; ')}; }`,\n `.oc-chrome { font-family: var(--oc-font-family); }`,\n `.oc-eyebrow { font-size: var(--oc-eyebrow-size); font-weight: var(--oc-eyebrow-weight); letter-spacing: var(--oc-eyebrow-tracking); text-transform: uppercase; fill: var(--oc-accent); }`,\n `.oc-title { font-size: var(--oc-title-size); font-weight: var(--oc-title-weight); letter-spacing: var(--oc-title-tracking); fill: var(--oc-text); }`,\n `.oc-subtitle { font-size: var(--oc-subtitle-size); font-weight: var(--oc-subtitle-weight); fill: var(--oc-text-muted); }`,\n `.oc-source, .oc-byline, .oc-footer { font-size: var(--oc-source-size); font-weight: var(--oc-source-weight); fill: var(--oc-text-muted); }`,\n `.oc-brand { font-size: 11px; font-weight: 510; letter-spacing: 0.02em; fill: var(--oc-text-faint); }`,\n `.oc-brand-dot { fill: var(--oc-accent); }`,\n `.oc-eyebrow-dot { fill: var(--oc-accent); }`,\n `.oc-metrics { font-family: var(--oc-font-family); }`,\n `.oc-metric-label { font-size: 10px; font-weight: 510; letter-spacing: 0.08em; text-transform: uppercase; fill: var(--oc-text-muted); }`,\n `.oc-metric-value { font-size: 22px; font-weight: 510; letter-spacing: -0.01em; fill: var(--oc-text); font-variant-numeric: tabular-nums; }`,\n `.oc-metric-delta-up { fill: var(--oc-positive); font-size: 12px; font-weight: 510; }`,\n `.oc-metric-delta-down { fill: var(--oc-negative); font-size: 12px; font-weight: 510; }`,\n `.oc-axis-tick-inline { font-size: 11px; font-weight: 400; fill: var(--oc-text-muted); }`,\n `.oc-endpoint-labels { font-family: var(--oc-font-family); }`,\n `.oc-endpoint-label { fill: var(--oc-endpoint-label-color, var(--oc-text)); }`,\n `.oc-endpoint-value { fill: var(--oc-endpoint-value-color, var(--oc-text-muted)); }`,\n `.oc-endpoint-leader { stroke: var(--oc-endpoint-leader-color, currentColor); }`,\n `.oc-annotation-subtitle { fill: var(--oc-annotation-subtitle-color, var(--oc-text-muted)); }`,\n `.oc-metric-secondary { fill: var(--oc-positive); font-size: 12px; font-weight: 400; }`,\n `.oc-legend { font-family: var(--oc-font-family); font-size: var(--oc-body-size); }`,\n `.oc-legend-entry { cursor: default; }`,\n `.oc-legend text { fill: var(--oc-legend-text); }`,\n ];\n\n return rules.join('\\n');\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 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 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 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,6 +1,7 @@
1
- import { h as CameraTarget, M as MountOptions } from '../mount-CUKQLeqD.js';
2
- export { C as Camera, F as FULL_VIEW, V as ViewBoxSize, i as cameraTransform, j as camerasClose, k as damp, l as dampCamera, m as fitTarget, n as interpolateCamera, s as scrubCamera } from '../mount-CUKQLeqD.js';
1
+ import { i as CameraTarget, M as MountOptions } from '../mount-Dvv8NYIv.js';
2
+ export { C as Camera, F as FULL_VIEW, V as ViewBoxSize, j as cameraTransform, k as camerasClose, l as damp, m as dampCamera, n as fitTarget, o as interpolateCamera, s as scrubCamera } from '../mount-Dvv8NYIv.js';
3
3
  import { DataRow, ChartSpec, LayerSpec, GraphSpec, MapSpec, ChartLayout } from '@opendata-ai/openchart-core';
4
+ import '../export-gif.js';
4
5
 
5
6
  /** A spec a story can drive: same union `ChartInstance.update()` accepts. */
6
7
  type StorySpec<TData extends DataRow = DataRow> = ChartSpec<TData> | LayerSpec<TData> | GraphSpec | MapSpec;
@@ -6,7 +6,7 @@ import {
6
6
  clamp01,
7
7
  createChart,
8
8
  createMap,
9
- createTween,
9
+ createTween2 as createTween,
10
10
  damp,
11
11
  dampCamera,
12
12
  easingFns,
@@ -15,8 +15,10 @@ import {
15
15
  lerp,
16
16
  scrubCamera,
17
17
  storyMotion
18
- } from "../chunk-ENXSUXC5.js";
19
- import "../chunk-5EOV5JQZ.js";
18
+ } from "../chunk-KNT5AZ2O.js";
19
+ import "../chunk-T32UHMZA.js";
20
+ import "../chunk-EKAUQAAX.js";
21
+ import "../chunk-RM5XS6NR.js";
20
22
 
21
23
  // src/story/create-chart-story.ts
22
24
  import { deepMergeSpec, isMapSpec } from "@opendata-ai/openchart-core";