@opendata-ai/openchart-vanilla 7.11.0 → 8.0.0-rc.2

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 (79) hide show
  1. package/dist/{chunk-EQ6DYFO7.js → chunk-5EOV5JQZ.js} +431 -538
  2. package/dist/chunk-5EOV5JQZ.js.map +1 -0
  3. package/dist/chunk-ENXSUXC5.js +6513 -0
  4. package/dist/chunk-ENXSUXC5.js.map +1 -0
  5. package/dist/chunk-W34KN7FD.js +298 -0
  6. package/dist/chunk-W34KN7FD.js.map +1 -0
  7. package/dist/index.d.ts +110 -116
  8. package/dist/index.js +189 -4408
  9. package/dist/index.js.map +1 -1
  10. package/dist/mount-CUKQLeqD.d.ts +207 -0
  11. package/dist/static.js +16 -11
  12. package/dist/static.js.map +1 -1
  13. package/dist/story/index.d.ts +277 -0
  14. package/dist/story/index.js +537 -0
  15. package/dist/story/index.js.map +1 -0
  16. package/dist/styles.css +2 -1
  17. package/dist/styles.css.map +1 -0
  18. package/package.json +9 -3
  19. package/src/__test-fixtures__/specs.ts +1 -1
  20. package/src/__tests__/annotations-render.test.ts +201 -0
  21. package/src/__tests__/continuous-legend.test.ts +91 -0
  22. package/src/__tests__/edit-events.test.ts +155 -0
  23. package/src/__tests__/map.test.ts +293 -0
  24. package/src/__tests__/mount.test.ts +26 -0
  25. package/src/__tests__/pattern-fills.test.ts +105 -0
  26. package/src/__tests__/series-search.test.ts +311 -0
  27. package/src/__tests__/table-mount.test.ts +34 -0
  28. package/src/__tests__/tooltip.test.ts +26 -0
  29. package/src/__tests__/transition.test.ts +196 -11
  30. package/src/__tests__/you-draw-it.test.ts +273 -0
  31. package/src/animation-vars.ts +42 -0
  32. package/src/barlist-renderer.ts +2 -2
  33. package/src/dom-helpers.ts +17 -0
  34. package/src/export.ts +33 -16
  35. package/src/index.ts +7 -0
  36. package/src/interactions/editing-drags.ts +9 -6
  37. package/src/interactions/legend-interaction.ts +9 -1
  38. package/src/interactions/selection.ts +12 -3
  39. package/src/map-camera.ts +88 -0
  40. package/src/map-mount.ts +701 -0
  41. package/src/map-renderer.ts +338 -0
  42. package/src/map-transition.ts +83 -0
  43. package/src/mount.ts +212 -26
  44. package/src/pattern-utils.ts +110 -0
  45. package/src/renderers/annotations.ts +162 -117
  46. package/src/renderers/axes.ts +13 -6
  47. package/src/renderers/brand.ts +7 -2
  48. package/src/renderers/chrome.ts +32 -10
  49. package/src/renderers/legend.ts +171 -2
  50. package/src/renderers/marks.ts +33 -5
  51. package/src/sankey-renderer.ts +7 -11
  52. package/src/series-search.ts +392 -0
  53. package/src/static.ts +13 -8
  54. package/src/story/__tests__/camera-math.test.ts +145 -0
  55. package/src/story/__tests__/create-chart-story.test.ts +317 -0
  56. package/src/story/__tests__/progress-math.test.ts +95 -0
  57. package/src/story/__tests__/scroll-driver.test.ts +130 -0
  58. package/src/story/__tests__/tween.test.ts +159 -0
  59. package/src/story/camera-math.ts +125 -0
  60. package/src/story/create-chart-story.ts +353 -0
  61. package/src/story/crossfade.ts +70 -0
  62. package/src/story/index.ts +39 -0
  63. package/src/story/progress-math.ts +85 -0
  64. package/src/story/resolve-camera-target.ts +119 -0
  65. package/src/story/scroll-driver.ts +206 -0
  66. package/src/story/story-camera.ts +49 -0
  67. package/src/story/tween.ts +112 -0
  68. package/src/story/types.ts +76 -0
  69. package/src/svg-renderer.ts +60 -29
  70. package/src/table-mount.ts +12 -3
  71. package/src/table-renderer.ts +9 -28
  72. package/src/text-edit-overlay.ts +1 -1
  73. package/src/theme-tokens.ts +4 -4
  74. package/src/tilemap-mount.ts +27 -6
  75. package/src/tilemap-renderer.ts +30 -88
  76. package/src/tooltip.ts +11 -0
  77. package/src/transition.ts +376 -33
  78. package/src/you-draw-it.ts +480 -0
  79. package/dist/chunk-EQ6DYFO7.js.map +0 -1
@@ -0,0 +1,207 @@
1
+ import { ChartSpec, LayerSpec, GraphSpec, ElementRef, ChartLayout, ChartEventHandlers, ThemeConfig, DarkMode, DataRow } from '@opendata-ai/openchart-core';
2
+
3
+ /**
4
+ * Export utilities: serialize charts to SVG, PNG, JPG, or CSV.
5
+ *
6
+ * - SVG: serializes the rendered DOM element via XMLSerializer
7
+ * - SVG with fonts: async version that embeds @font-face data URIs
8
+ * - PNG: renders SVG to canvas, then extracts as Blob
9
+ * - JPG: same as PNG but with JPEG compression and background fill
10
+ * - CSV: converts a data array to comma-separated text
11
+ */
12
+ interface SVGExportOptions {
13
+ /** Embed fonts as base64 data URIs in the SVG. Defaults to true. */
14
+ embedFonts?: boolean;
15
+ }
16
+ interface PNGExportOptions extends SVGExportOptions {
17
+ /** DPI scaling factor. Defaults to 2 for retina-quality output. */
18
+ dpi?: number;
19
+ }
20
+ interface JPGExportOptions extends PNGExportOptions {
21
+ /** JPEG quality from 0 to 1. Defaults to 0.92. */
22
+ quality?: number;
23
+ }
24
+ /**
25
+ * Serialize an SVG element to an XML string.
26
+ *
27
+ * @param svgElement - The rendered SVG element to serialize.
28
+ * @returns The SVG markup as a string.
29
+ */
30
+ declare function exportSVG(svgElement: SVGElement): string;
31
+ /**
32
+ * Serialize an SVG element with embedded fonts to an XML string.
33
+ *
34
+ * Collects font-family declarations from the SVG's text elements,
35
+ * finds matching @font-face rules in the page's stylesheets, fetches
36
+ * the font files, and embeds them as base64 data URIs. The resulting
37
+ * SVG is self-contained and renders correctly without external fonts.
38
+ *
39
+ * @param svgElement - The rendered SVG element to serialize.
40
+ * @param options - Export options.
41
+ * @returns A Promise resolving to the SVG markup as a string.
42
+ */
43
+ declare function exportSVGWithFonts(svgElement: SVGElement, options?: SVGExportOptions): Promise<string>;
44
+ /**
45
+ * Render an SVG element to a PNG Blob via a canvas.
46
+ *
47
+ * Embeds fonts by default so the exported image matches on-screen rendering.
48
+ * Set `embedFonts: false` to skip font embedding for faster exports.
49
+ *
50
+ * @param svgElement - The rendered SVG element.
51
+ * @param options - Optional DPI scaling and font embedding.
52
+ * @returns A Promise resolving to the PNG Blob.
53
+ */
54
+ declare function exportPNG(svgElement: SVGElement, options?: PNGExportOptions): Promise<Blob>;
55
+ /**
56
+ * Render an SVG element to a JPEG Blob via a canvas.
57
+ *
58
+ * Same pipeline as exportPNG but outputs JPEG with configurable quality.
59
+ * The canvas is filled with the chart's background color before drawing
60
+ * to avoid transparent backgrounds rendering as black in JPEG format.
61
+ *
62
+ * @param svgElement - The rendered SVG element.
63
+ * @param options - Optional DPI scaling, JPEG quality, and font embedding.
64
+ * @returns A Promise resolving to the JPEG Blob.
65
+ */
66
+ declare function exportJPG(svgElement: SVGElement, options?: JPGExportOptions): Promise<Blob>;
67
+ /**
68
+ * Convert an array of data objects to a CSV string.
69
+ *
70
+ * Uses the keys from the first row as column headers.
71
+ * Values are quoted if they contain commas, quotes, or newlines.
72
+ *
73
+ * @param data - Array of row objects.
74
+ * @returns CSV-formatted string.
75
+ */
76
+ declare function exportCSV(data: Record<string, unknown>[]): string;
77
+
78
+ /**
79
+ * Pure math for the scrollytelling camera. `createChartStory` owns DOM
80
+ * writes; everything here is testable without a browser. Ported from
81
+ * opendata/shared/lib/scrolly/camera-math.ts.
82
+ *
83
+ * The camera is a center + zoom over a fixed viewBox, applied as a
84
+ * `transform` on a dedicated wrapper `<g>` around the marks group (NOT
85
+ * viewBox interpolation -- that scales text and gives labels no way to
86
+ * counter-scale). See .claude/rules/svg-animation.md: CSS transform
87
+ * replaces SVG transform attributes, so the camera transform must compose
88
+ * with, not replace, existing SVG transforms on marks.
89
+ */
90
+ interface Camera {
91
+ cx: number;
92
+ cy: number;
93
+ /** Zoom factor. 1 = full viewBox visible. */
94
+ k: number;
95
+ }
96
+ interface CameraTarget {
97
+ x: number;
98
+ y: number;
99
+ width: number;
100
+ height: number;
101
+ /** ViewBox units added on all sides before fitting */
102
+ padding?: number;
103
+ }
104
+ interface ViewBoxSize {
105
+ width: number;
106
+ height: number;
107
+ }
108
+ declare const FULL_VIEW: (vb: ViewBoxSize) => CameraTarget;
109
+ /** Fit a target rect inside the viewBox, preserving aspect ratio. */
110
+ declare function fitTarget(target: CameraTarget, vb: ViewBoxSize): Camera;
111
+ /**
112
+ * Pan lerps linearly; zoom lerps in LOG space so the zoom rate reads
113
+ * constant (a 1->4 zoom passes through 2 at the midpoint, not 2.5).
114
+ */
115
+ declare function interpolateCamera(a: Camera, b: Camera, t: number): Camera;
116
+ /** SVG transform string that frames the camera inside the viewBox. */
117
+ declare function cameraTransform(c: Camera, vb: ViewBoxSize): string;
118
+ /**
119
+ * Frame-rate-independent exponential damping: two 8ms steps land where one
120
+ * 16ms step does. `tauMs` is the time constant (~63% of the gap per tau).
121
+ */
122
+ declare function damp(current: number, target: number, tauMs: number, dtMs: number): number;
123
+ declare function dampCamera(current: Camera, target: Camera, tauMs: number, dtMs: number): Camera;
124
+ declare function camerasClose(a: Camera, b: Camera, epsilon?: number): boolean;
125
+ /**
126
+ * Where the scrub camera should point for a given frame.
127
+ *
128
+ * Hold-zone semantics: the camera DWELLS on the active step's target while
129
+ * the reader reads, and only transitions toward the next step's target in
130
+ * the tail of the span (`stepProgress > holdUntil`). Interpolating across
131
+ * the whole span would mean the camera departs the moment a step activates
132
+ * and never rests on what the text is describing.
133
+ */
134
+ declare function scrubCamera(step: number, stepProgress: number, fitted: Camera[], holdUntil: number, ease: (t: number) => number): Camera;
135
+
136
+ /**
137
+ * Mount API: the main entry point for vanilla JS usage.
138
+ *
139
+ * createChart() takes a container, spec, and options, compiles the chart,
140
+ * renders it as SVG, sets up responsive resizing, tooltip interaction
141
+ * (mouse/touch/keyboard), keyboard navigation between data points,
142
+ * and returns a ChartInstance with update/resize/export/destroy methods.
143
+ */
144
+
145
+ interface MountOptions extends ChartEventHandlers {
146
+ /** Theme overrides. */
147
+ theme?: ThemeConfig;
148
+ /** Dark mode setting: "auto" (system pref), "force", or "off". */
149
+ darkMode?: DarkMode;
150
+ /** Callback when a data point is clicked. @deprecated Use onMarkClick instead. */
151
+ onDataPointClick?: (data: Record<string, unknown>) => void;
152
+ /** Enable responsive resizing. Defaults to true. */
153
+ responsive?: boolean;
154
+ /** Show the tryOpenData.ai watermark. Defaults to true. */
155
+ watermark?: boolean;
156
+ /** Initial selected element. */
157
+ selectedElement?: ElementRef;
158
+ }
159
+ interface UpdateOptions {
160
+ /** Override the selected element after update. When omitted, preserves current selection. */
161
+ selectedElement?: ElementRef;
162
+ }
163
+ interface ExportOptions extends JPGExportOptions {
164
+ }
165
+ interface ChartInstance {
166
+ /** Re-compile and re-render with a new spec. */
167
+ update(spec: ChartSpec | LayerSpec | GraphSpec, options?: UpdateOptions): void;
168
+ /** Re-compile at current container dimensions. */
169
+ resize(): void;
170
+ /** Export the chart. */
171
+ export(format: 'svg'): string;
172
+ export(format: 'svg-with-fonts', options?: SVGExportOptions): Promise<string>;
173
+ export(format: 'png', options?: ExportOptions): Promise<Blob>;
174
+ export(format: 'jpg', options?: ExportOptions): Promise<Blob>;
175
+ export(format: 'csv'): string;
176
+ export(format: 'svg' | 'svg-with-fonts' | 'png' | 'jpg' | 'csv', options?: ExportOptions): string | Promise<Blob> | Promise<string>;
177
+ /** Remove all DOM elements and disconnect observers. */
178
+ destroy(): void;
179
+ /** The current compiled layout (for hooks / debugging). */
180
+ readonly layout: ChartLayout;
181
+ /** Get the currently selected element, or null if none. */
182
+ getSelectedElement(): ElementRef | null;
183
+ /** Programmatically select an element. Silent no-op if element not found. */
184
+ select(ref: ElementRef): void;
185
+ /** Deselect the current element. */
186
+ deselect(): void;
187
+ /** Whether inline text editing is active. */
188
+ readonly isEditing: boolean;
189
+ /** Set highlight values on the color encoding and re-render. Pass null to clear. */
190
+ setHighlight(values: string[] | null): void;
191
+ /**
192
+ * Reset a "you draw it" (`youDrawIt`) drawing: clears the reader's guess and
193
+ * returns to the drawing state. No-op when youDrawIt isn't enabled.
194
+ */
195
+ resetDrawing(): void;
196
+ /**
197
+ * Reveal a "you draw it" drawing programmatically (equivalent to the
198
+ * skip-to-reveal button). No-op when youDrawIt isn't enabled.
199
+ */
200
+ revealDrawing(): void;
201
+ }
202
+ /**
203
+ * Create a chart instance from a spec and mount it into a container.
204
+ */
205
+ declare function createChart<TData extends DataRow = DataRow>(container: HTMLElement, spec: ChartSpec<TData> | LayerSpec<TData> | GraphSpec, options?: MountOptions): ChartInstance;
206
+
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 };
package/dist/static.js CHANGED
@@ -1,13 +1,20 @@
1
+ import {
2
+ renderTileMapSVG
3
+ } from "./chunk-W34KN7FD.js";
1
4
  import {
2
5
  SVG_NS,
3
6
  renderChartSVG,
4
- renderTileMapSVG,
5
7
  resetSvgIdCounter
6
- } from "./chunk-EQ6DYFO7.js";
8
+ } from "./chunk-5EOV5JQZ.js";
7
9
 
8
10
  // src/static.ts
9
11
  import { createRequire } from "module";
10
- import { adaptForLightLineStroke, isLayerSpec, isTileMapSpec } from "@opendata-ai/openchart-core";
12
+ import {
13
+ adaptForLightLineStroke,
14
+ cssTokenDefault,
15
+ isLayerSpec,
16
+ isTileMapSpec
17
+ } from "@opendata-ai/openchart-core";
11
18
  import { compileChart, compileLayer, compileTileMap } from "@opendata-ai/openchart-engine";
12
19
  var esmRequire = createRequire(import.meta.url);
13
20
  var cachedWindow;
@@ -28,15 +35,14 @@ function resolveStaticDarkMode(mode) {
28
35
  return false;
29
36
  }
30
37
  function buildThemeStyleBlock(theme) {
31
- const accent = theme.colors.categorical[0] ?? "#06b6d4";
32
- const bg = theme.colors.background === "transparent" ? theme.isDark ? "#09090b" : "#ffffff" : theme.colors.background;
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;
33
40
  const props = [
34
41
  `--oc-font-family: ${theme.fonts.family}`,
35
42
  `--oc-font-mono: ${theme.fonts.mono}`,
36
43
  `--oc-title-size: ${theme.chrome.title.fontSize}px`,
37
44
  `--oc-title-weight: ${theme.chrome.title.fontWeight}`,
38
- `--oc-title-tracking: -0.022em`,
39
- // sync with tokens.css
45
+ `--oc-title-tracking: ${cssTokenDefault("--oc-title-tracking", "light")}`,
40
46
  `--oc-subtitle-size: ${theme.chrome.subtitle.fontSize}px`,
41
47
  `--oc-subtitle-weight: ${theme.chrome.subtitle.fontWeight}`,
42
48
  `--oc-source-size: ${theme.chrome.source.fontSize}px`,
@@ -44,12 +50,11 @@ function buildThemeStyleBlock(theme) {
44
50
  `--oc-body-size: ${theme.fonts.sizes.body}px`,
45
51
  `--oc-eyebrow-size: ${theme.chrome.eyebrow.fontSize}px`,
46
52
  `--oc-eyebrow-weight: ${theme.chrome.eyebrow.fontWeight}`,
47
- `--oc-eyebrow-tracking: 0.08em`,
48
- // sync with tokens.css
53
+ `--oc-eyebrow-tracking: ${cssTokenDefault("--oc-eyebrow-tracking", "light")}`,
49
54
  `--oc-bg: ${bg}`,
50
55
  `--oc-text: ${theme.colors.text}`,
51
56
  `--oc-text-muted: ${theme.colors.axis}`,
52
- `--oc-text-faint: ${theme.isDark ? "#52525b" : "#d4d4d8"}`,
57
+ `--oc-text-faint: ${theme.isDark ? cssTokenDefault("--oc-text-faint", "dark") : cssTokenDefault("--oc-text-faint", "light")}`,
53
58
  `--oc-gridline: ${theme.colors.gridline}`,
54
59
  `--oc-axis: ${theme.colors.axis}`,
55
60
  `--oc-border-radius: ${theme.borderRadius}px`,
@@ -57,7 +62,7 @@ function buildThemeStyleBlock(theme) {
57
62
  `--oc-accent-strong: ${adaptForLightLineStroke(accent)}`,
58
63
  `--oc-positive: ${theme.colors.positive}`,
59
64
  `--oc-negative: ${theme.colors.negative}`,
60
- `--oc-legend-text: ${theme.isDark ? "#d0d6e0" : "#3f3f46"}`,
65
+ `--oc-legend-text: ${theme.isDark ? cssTokenDefault("--oc-legend-text", "dark") : cssTokenDefault("--oc-legend-text", "light")}`,
61
66
  `--oc-space-2: ${theme.spacing.chromeGap * 2}px`,
62
67
  `--oc-space-4: ${theme.spacing.padding}px`
63
68
  ];
@@ -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 { adaptForLightLineStroke, 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 { 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] ?? '#06b6d4';\n const bg =\n theme.colors.background === 'transparent'\n ? theme.isDark\n ? '#09090b'\n : '#ffffff'\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: -0.022em`, // sync with tokens.css\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: 0.08em`, // sync with tokens.css\n `--oc-bg: ${bg}`,\n `--oc-text: ${theme.colors.text}`,\n `--oc-text-muted: ${theme.colors.axis}`,\n `--oc-text-faint: ${theme.isDark ? '#52525b' : '#d4d4d8'}`,\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 ? '#d0d6e0' : '#3f3f46'}`,\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 {\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":[]}
@@ -0,0 +1,277 @@
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';
3
+ import { DataRow, ChartSpec, LayerSpec, GraphSpec, MapSpec, ChartLayout } from '@opendata-ai/openchart-core';
4
+
5
+ /** A spec a story can drive: same union `ChartInstance.update()` accepts. */
6
+ type StorySpec<TData extends DataRow = DataRow> = ChartSpec<TData> | LayerSpec<TData> | GraphSpec | MapSpec;
7
+ /** Deep-partial patch applied onto the accumulated spec at each step. */
8
+ type StorySpecPatch = Record<string, unknown>;
9
+ /**
10
+ * One step in a story. Patches are cumulative: step N's effective spec is
11
+ * the base spec with patches 0..N applied in order.
12
+ */
13
+ interface StoryStep {
14
+ /** Deep-partial patch merged onto the running spec. Omit for a no-op step. */
15
+ spec?: StorySpecPatch;
16
+ /**
17
+ * Emphasis sugar: sets `encoding.color.highlight`. Equivalent to
18
+ * `spec: { encoding: { color: { highlight: [...] } } }` but doesn't
19
+ * require the caller to know the color field is nested under `encoding`.
20
+ * Pass `null` to clear a highlight set by an earlier step.
21
+ */
22
+ highlight?: string[] | null;
23
+ /**
24
+ * Pan/zoom the camera to a data-coordinate region (resolved via the
25
+ * chart's compiled scales) or a raw viewBox-space `CameraTarget`. Pass
26
+ * `null`/omit to return to the full viewBox.
27
+ */
28
+ camera?: StoryCameraStep | null;
29
+ }
30
+ /** Data-coordinate camera target: resolved to viewBox space via the chart's scales. */
31
+ interface StoryDataCameraTarget {
32
+ /** x-domain range to frame, in data units (or category names for ordinal scales). */
33
+ x?: [unknown, unknown];
34
+ /** y-domain range to frame, in data units (or category names for ordinal scales). */
35
+ y?: [unknown, unknown];
36
+ /** ViewBox units of padding added around the resolved rect. Default 24. */
37
+ padding?: number;
38
+ }
39
+ type StoryCameraStep = StoryDataCameraTarget | CameraTarget;
40
+ interface ChartStoryOptions<TData extends DataRow = DataRow> {
41
+ spec: StorySpec<TData>;
42
+ steps: StoryStep[];
43
+ /** Fraction of viewport height where the scroll trigger line sits. Default 0.4. */
44
+ triggerPosition?: number;
45
+ /** Camera driving mode. Step mode (default) eases on step change; scrub follows scroll continuously. */
46
+ cameraMode?: 'step' | 'scrub';
47
+ }
48
+ interface ChartStoryInstance {
49
+ /** Jump directly to a step index, applying patches 0..n and animating the diff. */
50
+ goTo(index: number): void;
51
+ /** Register a step element so the built-in scroll driver advances the story on scroll. */
52
+ registerStep(index: number, el: HTMLElement | null): (el: HTMLElement | null) => void;
53
+ /** Register the scrolling container that wraps all step elements. */
54
+ setContainer(el: HTMLElement | null): void;
55
+ /** Current step index. -1 before any step has activated. */
56
+ readonly currentStep: number;
57
+ /** Total number of steps. */
58
+ readonly totalSteps: number;
59
+ /** Tear down the scroll driver, camera, and underlying chart instance. */
60
+ destroy(): void;
61
+ }
62
+
63
+ /**
64
+ * `createChartStory`: a base spec plus an ordered list of deep-partial
65
+ * patch steps, driven by scroll (built-in `ScrollDriver`) or directly via
66
+ * `goTo(n)`.
67
+ *
68
+ * Animation-clock ownership (decision baked 2026-07-11, see
69
+ * plans/design-evolution/11-scrollytelling.md): the data-update-transitions
70
+ * driver in `../transition.ts` owns mark morphing; the ported tween in
71
+ * `./tween.ts` owns camera and any scalar story-level animation. They never
72
+ * animate the same property. `goTo` calls `ChartInstance.update()`, which
73
+ * internally decides morph-vs-instant-swap via `canTransition`; this module
74
+ * predicts that decision ahead of time (`canTransitionSpecShape`) only to
75
+ * decide whether to arm the crossfade fallback, and separately drives the
76
+ * camera tween. It does not bridge the two clocks.
77
+ */
78
+
79
+ /**
80
+ * Create a scrollytelling story bound to a container. Mounts a chart with
81
+ * the base spec, then drives it through cumulative patch steps as the
82
+ * reader scrolls (or via `goTo`).
83
+ *
84
+ * `editMode` (any of `onEdit`/`onSelect`/`onDeselect`/`onTextEdit` on
85
+ * `mountOptions`) and an active story are mutually exclusive in v1: passing
86
+ * both warns once and disables the story's own drive loop, matching the
87
+ * seriesSearch/edit-mode precedent in `mount.ts`.
88
+ */
89
+ declare function createChartStory<TData extends DataRow = DataRow>(container: HTMLElement, options: ChartStoryOptions<TData>, mountOptions?: MountOptions): ChartStoryInstance;
90
+
91
+ /**
92
+ * Whole-chart crossfade fallback for story steps whose spec diff falls
93
+ * outside the data-update-transitions morph gate (`canTransition` in
94
+ * `../transition.ts`): re-encodes, type changes, and anything else that
95
+ * would otherwise instant-snap. No step in a story may visibly snap, so
96
+ * this ghosts the current rendered SVG over the container, lets
97
+ * `ChartInstance.update()` perform its normal instant swap underneath,
98
+ * then fades the ghost out to reveal the new state.
99
+ *
100
+ * This is deliberately NOT the mark-morphing transition driver: it never
101
+ * interpolates geometry, only opacity. See the animation-clock ownership
102
+ * note in the scrollytelling plan -- the transitions driver owns mark
103
+ * morphing, this owns the pixel-level fallback when that driver declines.
104
+ */
105
+ interface CrossfadeOptions {
106
+ /** Fade duration in ms. Default `storyMotion.crossfade`. */
107
+ duration?: number;
108
+ /** Skip the fade and swap instantly (reduced motion). */
109
+ reducedMotion?: boolean;
110
+ }
111
+ /**
112
+ * Ghost the container's current SVG, run `applyUpdate` (which mutates the
113
+ * container to its next state), then fade the ghost out.
114
+ */
115
+ declare function crossfadeUpdate(container: HTMLElement, applyUpdate: () => void, options?: CrossfadeOptions): void;
116
+
117
+ /**
118
+ * Pure math for continuous scrollytelling progress. The `ScrollDriver`
119
+ * measures DOM rects and feeds them here. Ported from
120
+ * opendata/shared/lib/scrolly/progress-math.ts.
121
+ */
122
+ interface ScrollyFrame {
123
+ /** Geometric active step; -1 while the trigger line is above step 0 */
124
+ step: number;
125
+ /** 0..1 progress of the trigger line through the active step's span */
126
+ stepProgress: number;
127
+ /** 0..1 traversal from first step top to last step bottom */
128
+ progress: number;
129
+ direction: 'down' | 'up';
130
+ }
131
+ type ScrollyFrameGeometry = Omit<ScrollyFrame, 'direction'>;
132
+ /**
133
+ * Compute the geometric frame from viewport-relative step tops.
134
+ *
135
+ * @param tops viewport-relative step top offsets, ordered by step index
136
+ * @param lastBottom viewport-relative bottom of the final step
137
+ * @param triggerY the trigger line's viewport y (innerHeight * triggerPosition)
138
+ */
139
+ declare function computeProgress(tops: number[], lastBottom: number, triggerY: number): ScrollyFrameGeometry;
140
+ /**
141
+ * Reduced-motion variant: no scrubbing. `stepProgress` pins to 0 and overall
142
+ * progress quantizes to the step's start fraction, so consumers see discrete
143
+ * snaps only.
144
+ */
145
+ declare function quantizeFrame(frame: ScrollyFrameGeometry, totalSteps: number): ScrollyFrameGeometry;
146
+ declare function framesEqual(a: ScrollyFrame | null, b: ScrollyFrame): boolean;
147
+
148
+ /**
149
+ * Resolves data-coordinate camera targets (`{ x: [a, b], y: [c, d] }`) to
150
+ * viewBox-space `CameraTarget` rects using the compiled chart's axis ticks.
151
+ *
152
+ * There is no raw d3 scale on `ChartLayout` (by design -- the compiled
153
+ * layout is the public surface, not engine internals), so resolution works
154
+ * off `AxisTick[]`, which pairs each tick's raw data `value` with its pixel
155
+ * `position`:
156
+ * - Exact tick match (ordinal/categorical values, or a quantitative value
157
+ * that happens to land on a tick): use that tick's position directly.
158
+ * - Otherwise linear interpolation between the two nearest ticks by data
159
+ * value. Works for quantitative and temporal axes (temporal values are
160
+ * compared as timestamps). Ordinal axes with no exact match have no
161
+ * well-ordered "between" and fall back to the nearest tick.
162
+ */
163
+
164
+ /**
165
+ * Resolve a data-coordinate camera target to viewBox space. Falls back to
166
+ * the full chart area (padded) for axes that are missing or unresolvable,
167
+ * so a malformed target degrades to "camera does nothing" rather than
168
+ * throwing mid-scroll.
169
+ */
170
+ declare function resolveCameraTarget(layout: ChartLayout, target: StoryDataCameraTarget): CameraTarget;
171
+ /** Type guard: a camera step given in data coordinates vs. raw viewBox `CameraTarget`. */
172
+ declare function isDataCameraTarget(value: StoryDataCameraTarget | CameraTarget): value is StoryDataCameraTarget;
173
+
174
+ /**
175
+ * Framework-agnostic scroll-step driver: element registry + rAF-throttled
176
+ * scroll handler + subscription store. Ported from the React
177
+ * `use-scroll-steps` hook in opendata/shared; the store design (60fps
178
+ * frames as subscriptions, never framework state) carries over unchanged
179
+ * because vanilla, React, Vue, and Svelte all want the same contract.
180
+ *
181
+ * Measurement is lazy: with zero subscribers the scroll handler does no
182
+ * rect reads.
183
+ */
184
+
185
+ interface ScrollyProgressStore {
186
+ /** Latest frame; `{step: -1, ...}` sentinel before any measurement */
187
+ get(): ScrollyFrame;
188
+ /** Emits the current frame immediately on subscribe, then on every change */
189
+ subscribe(cb: (frame: ScrollyFrame) => void): () => void;
190
+ }
191
+ interface ScrollDriverOptions {
192
+ /** Fraction of viewport height where the trigger line sits. Default 0.4 */
193
+ triggerPosition?: number;
194
+ }
195
+ interface ScrollDriver {
196
+ /** Register the scrolling container that wraps all steps. */
197
+ setContainer(el: HTMLElement | null): void;
198
+ /** Register a step element by index. Pass `null` to unregister. */
199
+ registerStep(index: number, el: HTMLElement | null): void;
200
+ /** Continuous progress as a subscription store. */
201
+ progress: ScrollyProgressStore;
202
+ /** Scroll a step into view (center). Honors reduced motion. */
203
+ scrollToStep(index: number): void;
204
+ /** Force a re-measurement (e.g. after layout changes). */
205
+ measure(): void;
206
+ /** Tear down scroll/resize listeners and the reduced-motion media query. */
207
+ destroy(): void;
208
+ }
209
+ /**
210
+ * Create a scroll-step driver. Framework wrappers (React/Vue/Svelte hooks)
211
+ * are thin adapters over this: they own component lifecycle, this owns the
212
+ * measurement/subscription machinery.
213
+ */
214
+ declare function createScrollDriver(options?: ScrollDriverOptions): ScrollDriver;
215
+
216
+ /**
217
+ * Numeric rAF tweening for scrollytelling story steps.
218
+ *
219
+ * Owns imperative numeric interpolation: the story driver and camera math
220
+ * mutate scalar/vector values per frame using these primitives instead of
221
+ * hand-rolling rAF loops. Ported from opendata/shared/lib/tween.ts.
222
+ *
223
+ * Everything here is SSR-safe: no `window`/`performance` access at module
224
+ * scope. Callers are expected to invoke these client-side.
225
+ */
226
+ declare const clamp01: (t: number) => number;
227
+ declare const lerp: (a: number, b: number, t: number) => number;
228
+ type EasingFn = (t: number) => number;
229
+ declare const easingFns: {
230
+ readonly linear: (t: number) => number;
231
+ readonly easeOutCubic: (t: number) => number;
232
+ readonly easeInOutCubic: (t: number) => number;
233
+ readonly easeOutQuint: (t: number) => number;
234
+ };
235
+ /**
236
+ * Duration tokens (ms) for story state changes. Use these instead of
237
+ * arbitrary numbers so all stories share one timing scale.
238
+ */
239
+ declare const storyMotion: {
240
+ /** Small opacity/emphasis shifts */
241
+ readonly fast: 300;
242
+ /** Step state transitions */
243
+ readonly base: 500;
244
+ /** Whole-chart crossfade fallback (steps outside the morph gate) */
245
+ readonly crossfade: 400;
246
+ /** Step-mode camera transitions */
247
+ readonly camera: 600;
248
+ /** Scrub-mode camera damping time constant */
249
+ readonly cameraTau: 100;
250
+ };
251
+ interface TweenConfig<T> {
252
+ initial: T;
253
+ lerp: (a: T, b: T, t: number) => T;
254
+ /** Default `storyMotion.base` */
255
+ duration?: number;
256
+ /** Default `easingFns.easeOutCubic` */
257
+ ease?: EasingFn;
258
+ onFrame: (value: T) => void;
259
+ }
260
+ interface Tween<T> {
261
+ /**
262
+ * Retarget the tween. If a tween is mid-flight, the new tween starts FROM
263
+ * THE LIVE INTERPOLATED VALUE, not the previous target -- fast retargeting
264
+ * never jumps. `snap: true` applies the target synchronously with no rAF.
265
+ */
266
+ to: (target: T, opts?: {
267
+ snap?: boolean;
268
+ }) => void;
269
+ get: () => T;
270
+ cancel: () => void;
271
+ }
272
+ /**
273
+ * Retargetable tween over an arbitrary shape T.
274
+ */
275
+ declare function createTween<T>(config: TweenConfig<T>): Tween<T>;
276
+
277
+ export { CameraTarget, type ChartStoryInstance, type ChartStoryOptions, type EasingFn, type ScrollDriver, type ScrollDriverOptions, type ScrollyFrame, type ScrollyFrameGeometry, type ScrollyProgressStore, type StoryCameraStep, type StoryDataCameraTarget, type StorySpec, type StorySpecPatch, type StoryStep, type Tween, type TweenConfig, clamp01, computeProgress, createChartStory, createScrollDriver, createTween, crossfadeUpdate, easingFns, framesEqual, isDataCameraTarget, lerp, quantizeFrame, resolveCameraTarget, storyMotion };