@opendata-ai/openchart-vanilla 8.0.0-rc.9 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/{chunk-SPILUE5S.js → chunk-2NAQRP66.js} +2 -2
  2. package/dist/{chunk-IZQBT6QM.js → chunk-6BUCYCLD.js} +7283 -5881
  3. package/dist/chunk-6BUCYCLD.js.map +1 -0
  4. package/dist/chunk-DCODCLWI.js +234 -0
  5. package/dist/chunk-DCODCLWI.js.map +1 -0
  6. package/dist/{chunk-QW7ZB6NE.js → chunk-EKAUQAAX.js} +49 -45
  7. package/dist/chunk-EKAUQAAX.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 +309 -27
  16. package/dist/index.js +2020 -836
  17. package/dist/index.js.map +1 -1
  18. package/dist/{mount-CUKQLeqD.d.ts → mount-CL-nA0f-.d.ts} +85 -2
  19. package/dist/simulation-worker.js +83 -2
  20. package/dist/static.d.ts +2 -0
  21. package/dist/static.js +8 -69
  22. package/dist/static.js.map +1 -1
  23. package/dist/story/index.d.ts +25 -9
  24. package/dist/story/index.js +9 -7
  25. package/dist/story/index.js.map +1 -1
  26. package/dist/styles.css +1 -1
  27. package/dist/styles.css.map +1 -1
  28. package/package.json +14 -6
  29. package/dist/chunk-IZQBT6QM.js.map +0 -1
  30. package/dist/chunk-QW7ZB6NE.js.map +0 -1
  31. package/src/__test-fixtures__/dom.ts +0 -70
  32. package/src/__test-fixtures__/specs.ts +0 -214
  33. package/src/__tests__/animation.test.ts +0 -358
  34. package/src/__tests__/annotations-render.test.ts +0 -201
  35. package/src/__tests__/compound-labels.test.ts +0 -122
  36. package/src/__tests__/continuous-legend.test.ts +0 -91
  37. package/src/__tests__/crosshair.test.ts +0 -130
  38. package/src/__tests__/edit-events.test.ts +0 -1477
  39. package/src/__tests__/editable-prop.test.ts +0 -174
  40. package/src/__tests__/events.test.ts +0 -387
  41. package/src/__tests__/export.test.ts +0 -241
  42. package/src/__tests__/font-race.test.ts +0 -199
  43. package/src/__tests__/gradient-ids.test.ts +0 -159
  44. package/src/__tests__/gradient-utils.test.ts +0 -125
  45. package/src/__tests__/invert.test.ts +0 -187
  46. package/src/__tests__/known-bugs-render.test.ts +0 -183
  47. package/src/__tests__/map.test.ts +0 -636
  48. package/src/__tests__/mount.test.ts +0 -357
  49. package/src/__tests__/pattern-fills.test.ts +0 -105
  50. package/src/__tests__/resize-timing.test.ts +0 -218
  51. package/src/__tests__/sankey.test.ts +0 -133
  52. package/src/__tests__/selection-events.test.ts +0 -1062
  53. package/src/__tests__/series-search.test.ts +0 -311
  54. package/src/__tests__/static.test.ts +0 -141
  55. package/src/__tests__/svg-renderer.test.ts +0 -895
  56. package/src/__tests__/table-keyboard.test.ts +0 -361
  57. package/src/__tests__/table-mount.test.ts +0 -639
  58. package/src/__tests__/tilemap.test.ts +0 -158
  59. package/src/__tests__/tooltip.test.ts +0 -337
  60. package/src/__tests__/transition.test.ts +0 -1792
  61. package/src/__tests__/update-scrolly-matrix.test.ts +0 -563
  62. package/src/__tests__/you-draw-it.test.ts +0 -273
  63. package/src/animation-vars.ts +0 -42
  64. package/src/animation.ts +0 -76
  65. package/src/barlist-mount.ts +0 -383
  66. package/src/barlist-renderer.ts +0 -267
  67. package/src/dom-helpers.ts +0 -17
  68. package/src/export.ts +0 -482
  69. package/src/gradient-utils.ts +0 -138
  70. package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
  71. package/src/graph/__tests__/graph-mount.test.ts +0 -213
  72. package/src/graph/__tests__/interaction.test.ts +0 -205
  73. package/src/graph/__tests__/keyboard.test.ts +0 -653
  74. package/src/graph/__tests__/search.test.ts +0 -88
  75. package/src/graph/__tests__/simulation.test.ts +0 -233
  76. package/src/graph/__tests__/spatial-index.test.ts +0 -142
  77. package/src/graph/__tests__/zoom.test.ts +0 -203
  78. package/src/graph/canvas-renderer.ts +0 -747
  79. package/src/graph/interaction.ts +0 -367
  80. package/src/graph/keyboard.ts +0 -208
  81. package/src/graph/search.ts +0 -50
  82. package/src/graph/simulation-worker-url.ts +0 -12
  83. package/src/graph/simulation-worker.ts +0 -278
  84. package/src/graph/simulation.ts +0 -422
  85. package/src/graph/spatial-index.ts +0 -121
  86. package/src/graph/types.ts +0 -47
  87. package/src/graph/worker-protocol.ts +0 -70
  88. package/src/graph/zoom.ts +0 -134
  89. package/src/graph-mount.ts +0 -888
  90. package/src/index.ts +0 -76
  91. package/src/interactions/chart-events.ts +0 -139
  92. package/src/interactions/crosshair.ts +0 -233
  93. package/src/interactions/drag-handler.ts +0 -175
  94. package/src/interactions/editing-drags.ts +0 -649
  95. package/src/interactions/index.ts +0 -27
  96. package/src/interactions/invert.ts +0 -54
  97. package/src/interactions/keyboard-nav.ts +0 -111
  98. package/src/interactions/legend-interaction.ts +0 -46
  99. package/src/interactions/selection.ts +0 -286
  100. package/src/interactions/tooltip-events.ts +0 -72
  101. package/src/map-camera.ts +0 -111
  102. package/src/map-mount.ts +0 -797
  103. package/src/map-renderer.ts +0 -476
  104. package/src/map-transition.ts +0 -99
  105. package/src/measure-text.ts +0 -134
  106. package/src/mount.ts +0 -1434
  107. package/src/pattern-utils.ts +0 -110
  108. package/src/renderers/annotations.ts +0 -388
  109. package/src/renderers/axes.ts +0 -351
  110. package/src/renderers/brand.ts +0 -87
  111. package/src/renderers/chrome.ts +0 -196
  112. package/src/renderers/endpoint-labels.ts +0 -150
  113. package/src/renderers/legend.ts +0 -312
  114. package/src/renderers/marks.ts +0 -561
  115. package/src/renderers/metrics.ts +0 -63
  116. package/src/renderers/svg-dom.ts +0 -40
  117. package/src/renderers/table-cells.ts +0 -446
  118. package/src/resize-observer.ts +0 -46
  119. package/src/sankey-mount.ts +0 -622
  120. package/src/sankey-renderer.ts +0 -614
  121. package/src/series-search.ts +0 -392
  122. package/src/static.ts +0 -251
  123. package/src/story/__tests__/camera-math.test.ts +0 -145
  124. package/src/story/__tests__/create-chart-story.test.ts +0 -317
  125. package/src/story/__tests__/progress-math.test.ts +0 -95
  126. package/src/story/__tests__/scroll-driver.test.ts +0 -130
  127. package/src/story/__tests__/tween.test.ts +0 -159
  128. package/src/story/camera-math.ts +0 -125
  129. package/src/story/create-chart-story.ts +0 -353
  130. package/src/story/crossfade.ts +0 -70
  131. package/src/story/index.ts +0 -39
  132. package/src/story/progress-math.ts +0 -85
  133. package/src/story/resolve-camera-target.ts +0 -119
  134. package/src/story/scroll-driver.ts +0 -206
  135. package/src/story/story-camera.ts +0 -49
  136. package/src/story/tween.ts +0 -112
  137. package/src/story/types.ts +0 -76
  138. package/src/svg-ids.ts +0 -23
  139. package/src/svg-renderer.ts +0 -383
  140. package/src/table-keyboard.ts +0 -266
  141. package/src/table-mount.ts +0 -567
  142. package/src/table-renderer.ts +0 -414
  143. package/src/text-edit-overlay.ts +0 -389
  144. package/src/theme-tokens.ts +0 -51
  145. package/src/tilemap-mount.ts +0 -511
  146. package/src/tilemap-renderer.ts +0 -396
  147. package/src/tooltip.ts +0 -177
  148. package/src/transition.ts +0 -2816
  149. package/src/you-draw-it.ts +0 -480
  150. /package/dist/{chunk-SPILUE5S.js.map → chunk-2NAQRP66.js.map} +0 -0
@@ -0,0 +1,282 @@
1
+ import {
2
+ injectThemeStyleBlock
3
+ } from "./chunk-RM5XS6NR.js";
4
+
5
+ // src/export.ts
6
+ function getSVGDimensions(svg) {
7
+ const w = parseFloat(svg.getAttribute("width") || "");
8
+ const h = parseFloat(svg.getAttribute("height") || "");
9
+ if (w && h) return { width: w, height: h };
10
+ const vb = svg.getAttribute("viewBox");
11
+ if (vb) {
12
+ const parts = vb.split(/[\s,]+/).map(Number);
13
+ if (parts.length >= 4 && parts[2] && parts[3]) {
14
+ return { width: parts[2], height: parts[3] };
15
+ }
16
+ }
17
+ return { width: 600, height: 400 };
18
+ }
19
+ function ensureSVGDimensions(svgString, width, height) {
20
+ if (/^<svg[^>]*\swidth\s*=/.test(svgString)) return svgString;
21
+ return svgString.replace(/^(<svg)/, `$1 width="${width}" height="${height}"`);
22
+ }
23
+ function collectUsedFonts(svgElement) {
24
+ const fonts = /* @__PURE__ */ new Map();
25
+ const textElements = svgElement.querySelectorAll("text");
26
+ for (const el of textElements) {
27
+ const family = el.getAttribute("font-family");
28
+ const weight = el.getAttribute("font-weight") || "400";
29
+ if (family) {
30
+ const primary = family.split(",")[0].trim().replace(/["']/g, "");
31
+ if (!fonts.has(primary)) fonts.set(primary, /* @__PURE__ */ new Set());
32
+ fonts.get(primary).add(String(weight));
33
+ }
34
+ }
35
+ return fonts;
36
+ }
37
+ function findFontFaceRules(usedFonts) {
38
+ const results = [];
39
+ try {
40
+ for (const sheet of document.styleSheets) {
41
+ let rules;
42
+ try {
43
+ rules = sheet.cssRules;
44
+ } catch {
45
+ continue;
46
+ }
47
+ for (const rule of rules) {
48
+ if (!(rule instanceof CSSFontFaceRule)) continue;
49
+ const familyRaw = rule.style.getPropertyValue("font-family").replace(/["']/g, "").trim();
50
+ const weight = rule.style.getPropertyValue("font-weight") || "400";
51
+ const style = rule.style.getPropertyValue("font-style") || "normal";
52
+ const src = rule.style.getPropertyValue("src");
53
+ const weights = usedFonts.get(familyRaw);
54
+ if (!weights) continue;
55
+ const weightMatch = weights.has(weight) || weight.includes(" ");
56
+ if (!weightMatch) continue;
57
+ const woff2Match = src.match(/url\(["']?([^"')]+\.woff2[^"')]*?)["']?\)/);
58
+ if (woff2Match) {
59
+ results.push({
60
+ family: familyRaw,
61
+ weight,
62
+ style,
63
+ url: woff2Match[1],
64
+ format: "woff2"
65
+ });
66
+ }
67
+ }
68
+ }
69
+ } catch {
70
+ }
71
+ return results;
72
+ }
73
+ async function fetchFontsAsBase64(fontRules) {
74
+ const results = [];
75
+ const fetches = fontRules.map(async (rule) => {
76
+ try {
77
+ const response = await fetch(rule.url);
78
+ if (!response.ok) return;
79
+ const buffer = await response.arrayBuffer();
80
+ const base64 = arrayBufferToBase64(buffer);
81
+ results.push({
82
+ family: rule.family,
83
+ weight: rule.weight,
84
+ style: rule.style,
85
+ base64,
86
+ format: rule.format
87
+ });
88
+ } catch {
89
+ }
90
+ });
91
+ await Promise.all(fetches);
92
+ return results;
93
+ }
94
+ function arrayBufferToBase64(buffer) {
95
+ let binary = "";
96
+ const bytes = new Uint8Array(buffer);
97
+ for (let i = 0; i < bytes.byteLength; i++) {
98
+ binary += String.fromCharCode(bytes[i]);
99
+ }
100
+ return btoa(binary);
101
+ }
102
+ function injectFontsIntoSVG(svgElement, fonts) {
103
+ if (fonts.length === 0) return;
104
+ const cssRules = fonts.map(
105
+ (f) => `@font-face { font-family: '${f.family}'; font-weight: ${f.weight}; font-style: ${f.style}; src: url(data:font/${f.format};base64,${f.base64}) format('${f.format}'); }`
106
+ ).join("\n");
107
+ const ns = "http://www.w3.org/2000/svg";
108
+ let defs = svgElement.querySelector("defs");
109
+ if (!defs) {
110
+ defs = document.createElementNS(ns, "defs");
111
+ svgElement.insertBefore(defs, svgElement.firstChild);
112
+ }
113
+ const styleEl = document.createElementNS(ns, "style");
114
+ styleEl.textContent = cssRules;
115
+ defs.insertBefore(styleEl, defs.firstChild);
116
+ }
117
+ async function embedFonts(svgElement) {
118
+ const usedFonts = collectUsedFonts(svgElement);
119
+ if (usedFonts.size === 0) return;
120
+ const fontRules = findFontFaceRules(usedFonts);
121
+ if (fontRules.length === 0) return;
122
+ const fontData = await fetchFontsAsBase64(fontRules);
123
+ injectFontsIntoSVG(svgElement, fontData);
124
+ }
125
+ function getSVGBackgroundColor(svgElement, fallback) {
126
+ const firstRect = svgElement.querySelector("rect");
127
+ const fill = firstRect?.getAttribute("fill");
128
+ if (!fill || fill === "none" || fill === "transparent") return fallback ?? "#ffffff";
129
+ return fill;
130
+ }
131
+ function exportSVG(svgElement) {
132
+ if (!svgElement.getAttribute("xmlns")) {
133
+ svgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
134
+ }
135
+ const serializer = new XMLSerializer();
136
+ return serializer.serializeToString(svgElement);
137
+ }
138
+ async function exportSVGWithFonts(svgElement, options) {
139
+ const clone = svgElement.cloneNode(true);
140
+ const shouldEmbed = options?.embedFonts ?? true;
141
+ injectThemeStyleBlock(clone, options?.theme);
142
+ if (shouldEmbed) {
143
+ await embedFonts(clone);
144
+ }
145
+ return exportSVG(clone);
146
+ }
147
+ var p3Supported;
148
+ function supportsDisplayP3() {
149
+ if (p3Supported !== void 0) return p3Supported;
150
+ try {
151
+ const probe = document.createElement("canvas");
152
+ const ctx = probe.getContext("2d", { colorSpace: "display-p3" });
153
+ p3Supported = !!ctx && ctx.getContextAttributes?.().colorSpace === "display-p3";
154
+ } catch {
155
+ p3Supported = false;
156
+ }
157
+ return p3Supported;
158
+ }
159
+ function getExportContext(canvas) {
160
+ const ctx = supportsDisplayP3() ? canvas.getContext("2d", { colorSpace: "display-p3" }) : canvas.getContext("2d");
161
+ if (!ctx) {
162
+ throw new Error("Canvas 2D context not available");
163
+ }
164
+ return ctx;
165
+ }
166
+ function rasterizeSVGToCanvas(svgString, width, height, dpi, prepare) {
167
+ if (!width || !height) {
168
+ throw new Error(`SVG has zero dimensions (width=${width}, height=${height})`);
169
+ }
170
+ const canvas = document.createElement("canvas");
171
+ canvas.width = width * dpi;
172
+ canvas.height = height * dpi;
173
+ const ctx = getExportContext(canvas);
174
+ prepare?.(ctx, canvas);
175
+ ctx.scale(dpi, dpi);
176
+ const img = new Image();
177
+ const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
178
+ const url = URL.createObjectURL(blob);
179
+ return new Promise((resolve, reject) => {
180
+ img.onload = () => {
181
+ ctx.drawImage(img, 0, 0, width, height);
182
+ URL.revokeObjectURL(url);
183
+ resolve(canvas);
184
+ };
185
+ img.onerror = () => {
186
+ URL.revokeObjectURL(url);
187
+ reject(
188
+ new Error(
189
+ `Failed to load SVG as image (width=${width}, height=${height}, svgLength=${svgString.length})`
190
+ )
191
+ );
192
+ };
193
+ img.src = url;
194
+ });
195
+ }
196
+ async function prepareRasterSVG(svgElement, shouldEmbed, theme) {
197
+ const { width, height } = getSVGDimensions(svgElement);
198
+ const clone = svgElement.cloneNode(true);
199
+ injectThemeStyleBlock(clone, theme);
200
+ if (shouldEmbed) {
201
+ await embedFonts(clone);
202
+ }
203
+ const svgString = ensureSVGDimensions(exportSVG(clone), width, height);
204
+ return { svgString, width, height };
205
+ }
206
+ async function exportPNG(svgElement, options) {
207
+ const dpi = options?.dpi ?? 2;
208
+ const shouldEmbed = options?.embedFonts ?? true;
209
+ const { svgString, width, height } = await prepareRasterSVG(
210
+ svgElement,
211
+ shouldEmbed,
212
+ options?.theme
213
+ );
214
+ const canvas = await rasterizeSVGToCanvas(svgString, width, height, dpi);
215
+ return new Promise((resolve, reject) => {
216
+ canvas.toBlob((result) => {
217
+ if (result) {
218
+ resolve(result);
219
+ } else {
220
+ reject(new Error("Canvas toBlob returned null"));
221
+ }
222
+ }, "image/png");
223
+ });
224
+ }
225
+ async function exportJPG(svgElement, options) {
226
+ const dpi = options?.dpi ?? 2;
227
+ const quality = options?.quality ?? 0.92;
228
+ const shouldEmbed = options?.embedFonts ?? true;
229
+ const backgroundColor = getSVGBackgroundColor(svgElement);
230
+ const { svgString, width, height } = await prepareRasterSVG(
231
+ svgElement,
232
+ shouldEmbed,
233
+ options?.theme
234
+ );
235
+ const canvas = await rasterizeSVGToCanvas(svgString, width, height, dpi, (ctx, cv) => {
236
+ ctx.fillStyle = backgroundColor;
237
+ ctx.fillRect(0, 0, cv.width, cv.height);
238
+ });
239
+ return new Promise((resolve, reject) => {
240
+ canvas.toBlob(
241
+ (result) => {
242
+ if (result) {
243
+ resolve(result);
244
+ } else {
245
+ reject(new Error("Canvas toBlob returned null"));
246
+ }
247
+ },
248
+ "image/jpeg",
249
+ quality
250
+ );
251
+ });
252
+ }
253
+ function exportCSV(data) {
254
+ if (data.length === 0) return "";
255
+ const headers = Object.keys(data[0]);
256
+ const rows = [headers.map(csvEscape).join(",")];
257
+ for (const row of data) {
258
+ const values = headers.map((h) => csvEscape(String(row[h] ?? "")));
259
+ rows.push(values.join(","));
260
+ }
261
+ return rows.join("\n");
262
+ }
263
+ function csvEscape(value) {
264
+ if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
265
+ return `"${value.replace(/"/g, '""')}"`;
266
+ }
267
+ return value;
268
+ }
269
+
270
+ export {
271
+ getSVGDimensions,
272
+ ensureSVGDimensions,
273
+ embedFonts,
274
+ getSVGBackgroundColor,
275
+ exportSVG,
276
+ exportSVGWithFonts,
277
+ rasterizeSVGToCanvas,
278
+ exportPNG,
279
+ exportJPG,
280
+ exportCSV
281
+ };
282
+ //# sourceMappingURL=chunk-T32UHMZA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/export.ts"],"sourcesContent":["/**\n * Export utilities: serialize charts to SVG, PNG, JPG, or CSV.\n *\n * - SVG: serializes the rendered DOM element via XMLSerializer\n * - SVG with fonts: async version that embeds @font-face data URIs\n * - PNG: renders SVG to canvas, then extracts as Blob\n * - JPG: same as PNG but with JPEG compression and background fill\n * - CSV: converts a data array to comma-separated text\n */\n\nimport type { ResolvedTheme } from '@opendata-ai/openchart-core';\nimport { injectThemeStyleBlock } from './theme-style-block';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SVGExportOptions {\n /** Embed fonts as base64 data URIs in the SVG. Defaults to true. */\n embedFonts?: boolean;\n /**\n * Resolved theme to inline as a `<style>` block, so class-based fills (metric\n * cells, brand watermark dot, legend text, endpoint labels) survive\n * serialization away from the page stylesheet. Omit to skip — a chart whose\n * chrome is all inline-filled exports fine without it.\n */\n theme?: ResolvedTheme;\n}\n\nexport interface PNGExportOptions extends SVGExportOptions {\n /** DPI scaling factor. Defaults to 2 for retina-quality output. */\n dpi?: number;\n}\n\nexport interface JPGExportOptions extends PNGExportOptions {\n /** JPEG quality from 0 to 1. Defaults to 0.92. */\n quality?: number;\n}\n\ninterface FontFaceData {\n family: string;\n weight: string;\n style: string;\n base64: string;\n format: string;\n}\n\n// ---------------------------------------------------------------------------\n// Dimension parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Extract dimensions from an SVG element, trying width/height attributes\n * first, then falling back to viewBox.\n */\nexport function getSVGDimensions(svg: SVGElement): { width: number; height: number } {\n const w = parseFloat(svg.getAttribute('width') || '');\n const h = parseFloat(svg.getAttribute('height') || '');\n if (w && h) return { width: w, height: h };\n\n const vb = svg.getAttribute('viewBox');\n if (vb) {\n const parts = vb.split(/[\\s,]+/).map(Number);\n if (parts.length >= 4 && parts[2] && parts[3]) {\n return { width: parts[2], height: parts[3] };\n }\n }\n\n return { width: 600, height: 400 };\n}\n\n/**\n * Ensure an SVG string has explicit width/height attributes.\n *\n * When an SVG only has a viewBox (no width/height), browsers loading it as\n * an Image blob may use 300x150 as the intrinsic size instead of the viewBox\n * dimensions. This causes clipping at non-1x DPI scaling. Injecting explicit\n * width/height into the root <svg> tag fixes the intrinsic size.\n */\nexport function ensureSVGDimensions(svgString: string, width: number, height: number): string {\n // If the <svg> already has a width attribute, leave it alone\n if (/^<svg[^>]*\\swidth\\s*=/.test(svgString)) return svgString;\n // Inject width and height right after <svg\n return svgString.replace(/^(<svg)/, `$1 width=\"${width}\" height=\"${height}\"`);\n}\n\n// ---------------------------------------------------------------------------\n// Font embedding\n// ---------------------------------------------------------------------------\n\n/**\n * Collect unique font-family + font-weight combos from all <text> elements.\n */\nfunction collectUsedFonts(svgElement: SVGElement): Map<string, Set<string>> {\n const fonts = new Map<string, Set<string>>();\n const textElements = svgElement.querySelectorAll('text');\n\n for (const el of textElements) {\n const family = el.getAttribute('font-family');\n const weight = el.getAttribute('font-weight') || '400';\n if (family) {\n // Take the first font in the stack (e.g., \"Inter, sans-serif\" → \"Inter\")\n const primary = family.split(',')[0].trim().replace(/[\"']/g, '');\n if (!fonts.has(primary)) fonts.set(primary, new Set());\n fonts.get(primary)!.add(String(weight));\n }\n }\n\n return fonts;\n}\n\n/**\n * Find @font-face rules in document stylesheets that match the requested fonts.\n * Returns the src URLs for .woff2 files.\n */\nfunction findFontFaceRules(\n usedFonts: Map<string, Set<string>>,\n): Array<{ family: string; weight: string; style: string; url: string; format: string }> {\n const results: Array<{\n family: string;\n weight: string;\n style: string;\n url: string;\n format: string;\n }> = [];\n\n try {\n for (const sheet of document.styleSheets) {\n let rules: CSSRuleList;\n try {\n rules = sheet.cssRules;\n } catch {\n // Cross-origin stylesheet, skip\n continue;\n }\n\n for (const rule of rules) {\n if (!(rule instanceof CSSFontFaceRule)) continue;\n\n const familyRaw = rule.style.getPropertyValue('font-family').replace(/[\"']/g, '').trim();\n const weight = rule.style.getPropertyValue('font-weight') || '400';\n const style = rule.style.getPropertyValue('font-style') || 'normal';\n const src = rule.style.getPropertyValue('src');\n\n const weights = usedFonts.get(familyRaw);\n if (!weights) continue;\n\n // Check if this weight is used (handle ranges like \"100 900\")\n const weightMatch = weights.has(weight) || weight.includes(' ');\n if (!weightMatch) continue;\n\n // Extract woff2 URL from src descriptor\n const woff2Match = src.match(/url\\([\"']?([^\"')]+\\.woff2[^\"')]*?)[\"']?\\)/);\n if (woff2Match) {\n results.push({\n family: familyRaw,\n weight,\n style,\n url: woff2Match[1],\n format: 'woff2',\n });\n }\n }\n }\n } catch {\n // Stylesheet access failed entirely, return empty\n }\n\n return results;\n}\n\n/**\n * Fetch font files and convert to base64 data URIs.\n */\nasync function fetchFontsAsBase64(\n fontRules: Array<{ family: string; weight: string; style: string; url: string; format: string }>,\n): Promise<FontFaceData[]> {\n const results: FontFaceData[] = [];\n\n const fetches = fontRules.map(async (rule) => {\n try {\n const response = await fetch(rule.url);\n if (!response.ok) return;\n const buffer = await response.arrayBuffer();\n const base64 = arrayBufferToBase64(buffer);\n results.push({\n family: rule.family,\n weight: rule.weight,\n style: rule.style,\n base64,\n format: rule.format,\n });\n } catch {\n // Font fetch failed (CORS, network, etc.) - skip this font\n }\n });\n\n await Promise.all(fetches);\n return results;\n}\n\nfunction arrayBufferToBase64(buffer: ArrayBuffer): string {\n let binary = '';\n const bytes = new Uint8Array(buffer);\n for (let i = 0; i < bytes.byteLength; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n\n/**\n * Inject @font-face rules with base64 data URIs into the SVG's <defs>.\n */\nfunction injectFontsIntoSVG(svgElement: SVGElement, fonts: FontFaceData[]): void {\n if (fonts.length === 0) return;\n\n const cssRules = fonts\n .map(\n (f) =>\n `@font-face { font-family: '${f.family}'; font-weight: ${f.weight}; font-style: ${f.style}; src: url(data:font/${f.format};base64,${f.base64}) format('${f.format}'); }`,\n )\n .join('\\n');\n\n const ns = 'http://www.w3.org/2000/svg';\n let defs = svgElement.querySelector('defs');\n if (!defs) {\n defs = document.createElementNS(ns, 'defs');\n svgElement.insertBefore(defs, svgElement.firstChild);\n }\n\n const styleEl = document.createElementNS(ns, 'style');\n styleEl.textContent = cssRules;\n defs.insertBefore(styleEl, defs.firstChild);\n}\n\n/**\n * Embed fonts into an SVG element by finding matching @font-face rules\n * in the page's stylesheets, fetching the font files, and injecting\n * them as base64 data URIs.\n *\n * Modifies the SVG element in place. Call this before serialization.\n * If font fetching fails for any font, that font is silently skipped\n * and the export proceeds with system font fallback for that face.\n */\nexport async function embedFonts(svgElement: SVGElement): Promise<void> {\n const usedFonts = collectUsedFonts(svgElement);\n if (usedFonts.size === 0) return;\n\n const fontRules = findFontFaceRules(usedFonts);\n if (fontRules.length === 0) return;\n\n const fontData = await fetchFontsAsBase64(fontRules);\n injectFontsIntoSVG(svgElement, fontData);\n}\n\n// ---------------------------------------------------------------------------\n// SVG background color\n// ---------------------------------------------------------------------------\n\n/**\n * Read the chart's background color from its first rect element. Falls back to\n * `fallback` (white when omitted) when the chart has no opaque background,\n * which matters for formats that can't carry alpha (JPEG, GIF).\n *\n * Pass the resolved theme's `colors.background` as `fallback` when the caller\n * has one: canvas mark mode suppresses the SVG background rect (the canvas\n * paints it), so without it a dark chart would flood-fill white.\n */\nexport function getSVGBackgroundColor(svgElement: SVGElement, fallback?: string): string {\n const firstRect = svgElement.querySelector('rect');\n const fill = firstRect?.getAttribute('fill');\n if (!fill || fill === 'none' || fill === 'transparent') return fallback ?? '#ffffff';\n return fill;\n}\n\n// ---------------------------------------------------------------------------\n// SVG export\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize an SVG element to an XML string.\n *\n * @param svgElement - The rendered SVG element to serialize.\n * @returns The SVG markup as a string.\n */\nexport function exportSVG(svgElement: SVGElement): string {\n if (!svgElement.getAttribute('xmlns')) {\n svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');\n }\n const serializer = new XMLSerializer();\n return serializer.serializeToString(svgElement);\n}\n\n/**\n * Serialize an SVG element with embedded fonts to an XML string.\n *\n * Collects font-family declarations from the SVG's text elements,\n * finds matching @font-face rules in the page's stylesheets, fetches\n * the font files, and embeds them as base64 data URIs. The resulting\n * SVG is self-contained and renders correctly without external fonts.\n *\n * @param svgElement - The rendered SVG element to serialize.\n * @param options - Export options.\n * @returns A Promise resolving to the SVG markup as a string.\n */\nexport async function exportSVGWithFonts(\n svgElement: SVGElement,\n options?: SVGExportOptions,\n): Promise<string> {\n const clone = svgElement.cloneNode(true) as SVGElement;\n const shouldEmbed = options?.embedFonts ?? true;\n injectThemeStyleBlock(clone, options?.theme);\n if (shouldEmbed) {\n await embedFonts(clone);\n }\n return exportSVG(clone);\n}\n\n// ---------------------------------------------------------------------------\n// Raster export (PNG / JPG)\n// ---------------------------------------------------------------------------\n\n/**\n * Cache the one-time probe for Display P3 canvas support. Rendering raster\n * exports into a display-p3 canvas makes toBlob embed a wide-gamut color profile\n * (verified: an sRGB PNG has no iCCP chunk; a display-p3 PNG does — and JPEG\n * upgrades from an sRGB to a P3 profile), so the export matches the on-screen\n * chart instead of reading as an untagged/sRGB image that looks washed out on\n * P3 displays.\n */\nlet p3Supported: boolean | undefined;\n\nfunction supportsDisplayP3(): boolean {\n if (p3Supported !== undefined) return p3Supported;\n try {\n const probe = document.createElement('canvas');\n const ctx = probe.getContext('2d', { colorSpace: 'display-p3' }) as\n | (CanvasRenderingContext2D & { getContextAttributes?: () => { colorSpace?: string } })\n | null;\n p3Supported = !!ctx && ctx.getContextAttributes?.().colorSpace === 'display-p3';\n } catch {\n p3Supported = false;\n }\n return p3Supported;\n}\n\n/**\n * Create a 2D context in the widest color space the runtime supports. A\n * display-p3 canvas is what makes toBlob embed the profile. Callers that read\n * pixels back for a non-color-managed sink (GIF quantization) must NOT rely on\n * this — they must read back in sRGB explicitly.\n */\nfunction getExportContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D {\n const ctx = supportsDisplayP3()\n ? canvas.getContext('2d', { colorSpace: 'display-p3' })\n : canvas.getContext('2d');\n if (!ctx) {\n throw new Error('Canvas 2D context not available');\n }\n return ctx;\n}\n\n/**\n * Draw a serialized SVG string onto a canvas at DPI scaling.\n *\n * Shared by PNG/JPG export and per-frame GIF rendering: loads the SVG string\n * as an Image via an object URL, draws it into a `width*dpi × height*dpi`\n * canvas, and hands the canvas back once painted. The caller decides how to\n * read the result (toBlob for PNG/JPG, getImageData for GIF frames).\n *\n * An optional `prepare` hook runs before drawing the image, used by JPG export\n * to fill an opaque background so transparency doesn't render as black.\n *\n * @param svgString - Serialized SVG markup (should already carry width/height).\n * @param width - Logical width in CSS pixels.\n * @param height - Logical height in CSS pixels.\n * @param dpi - Device pixel ratio scaling factor.\n * @param prepare - Optional pre-draw hook receiving the scaled 2D context.\n * @returns A Promise resolving to the painted canvas.\n */\nexport function rasterizeSVGToCanvas(\n svgString: string,\n width: number,\n height: number,\n dpi: number,\n prepare?: (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void,\n): Promise<HTMLCanvasElement> {\n if (!width || !height) {\n throw new Error(`SVG has zero dimensions (width=${width}, height=${height})`);\n }\n\n const canvas = document.createElement('canvas');\n canvas.width = width * dpi;\n canvas.height = height * dpi;\n\n const ctx = getExportContext(canvas);\n\n prepare?.(ctx, canvas);\n ctx.scale(dpi, dpi);\n\n const img = new Image();\n const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n\n return new Promise<HTMLCanvasElement>((resolve, reject) => {\n img.onload = () => {\n ctx.drawImage(img, 0, 0, width, height);\n URL.revokeObjectURL(url);\n resolve(canvas);\n };\n\n img.onerror = () => {\n URL.revokeObjectURL(url);\n reject(\n new Error(\n `Failed to load SVG as image (width=${width}, height=${height}, svgLength=${svgString.length})`,\n ),\n );\n };\n\n img.src = url;\n });\n}\n\n/**\n * Serialize an SVG element (optionally embedding fonts) into a canvas-ready\n * string with explicit width/height. Shared by the raster exporters.\n */\nasync function prepareRasterSVG(\n svgElement: SVGElement,\n shouldEmbed: boolean,\n theme?: ResolvedTheme,\n): Promise<{ svgString: string; width: number; height: number }> {\n const { width, height } = getSVGDimensions(svgElement);\n const clone = svgElement.cloneNode(true) as SVGElement;\n // Inline the theme style block so class-based fills survive the detach from\n // the page stylesheet. No-op when theme is undefined.\n injectThemeStyleBlock(clone, theme);\n if (shouldEmbed) {\n await embedFonts(clone);\n }\n const svgString = ensureSVGDimensions(exportSVG(clone), width, height);\n return { svgString, width, height };\n}\n\n/**\n * Render an SVG element to a PNG Blob via a canvas.\n *\n * Embeds fonts by default so the exported image matches on-screen rendering.\n * Set `embedFonts: false` to skip font embedding for faster exports.\n *\n * @param svgElement - The rendered SVG element.\n * @param options - Optional DPI scaling and font embedding.\n * @returns A Promise resolving to the PNG Blob.\n */\nexport async function exportPNG(svgElement: SVGElement, options?: PNGExportOptions): Promise<Blob> {\n const dpi = options?.dpi ?? 2;\n const shouldEmbed = options?.embedFonts ?? true;\n const { svgString, width, height } = await prepareRasterSVG(\n svgElement,\n shouldEmbed,\n options?.theme,\n );\n const canvas = await rasterizeSVGToCanvas(svgString, width, height, dpi);\n\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob((result) => {\n if (result) {\n resolve(result);\n } else {\n reject(new Error('Canvas toBlob returned null'));\n }\n }, 'image/png');\n });\n}\n\n/**\n * Render an SVG element to a JPEG Blob via a canvas.\n *\n * Same pipeline as exportPNG but outputs JPEG with configurable quality.\n * The canvas is filled with the chart's background color before drawing\n * to avoid transparent backgrounds rendering as black in JPEG format.\n *\n * @param svgElement - The rendered SVG element.\n * @param options - Optional DPI scaling, JPEG quality, and font embedding.\n * @returns A Promise resolving to the JPEG Blob.\n */\nexport async function exportJPG(svgElement: SVGElement, options?: JPGExportOptions): Promise<Blob> {\n const dpi = options?.dpi ?? 2;\n const quality = options?.quality ?? 0.92;\n const shouldEmbed = options?.embedFonts ?? true;\n const backgroundColor = getSVGBackgroundColor(svgElement);\n const { svgString, width, height } = await prepareRasterSVG(\n svgElement,\n shouldEmbed,\n options?.theme,\n );\n\n // Fill an opaque background before drawing so transparency doesn't render\n // as black in the JPEG. Runs before ctx.scale, so use the raw canvas size.\n const canvas = await rasterizeSVGToCanvas(svgString, width, height, dpi, (ctx, cv) => {\n ctx.fillStyle = backgroundColor;\n ctx.fillRect(0, 0, cv.width, cv.height);\n });\n\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (result) => {\n if (result) {\n resolve(result);\n } else {\n reject(new Error('Canvas toBlob returned null'));\n }\n },\n 'image/jpeg',\n quality,\n );\n });\n}\n\n// ---------------------------------------------------------------------------\n// CSV export\n// ---------------------------------------------------------------------------\n\n/**\n * Convert an array of data objects to a CSV string.\n *\n * Uses the keys from the first row as column headers.\n * Values are quoted if they contain commas, quotes, or newlines.\n *\n * @param data - Array of row objects.\n * @returns CSV-formatted string.\n */\nexport function exportCSV(data: Record<string, unknown>[]): string {\n if (data.length === 0) return '';\n\n const headers = Object.keys(data[0]);\n const rows = [headers.map(csvEscape).join(',')];\n\n for (const row of data) {\n const values = headers.map((h) => csvEscape(String(row[h] ?? '')));\n rows.push(values.join(','));\n }\n\n return rows.join('\\n');\n}\n\nfunction csvEscape(value: string): string {\n if (value.includes(',') || value.includes('\"') || value.includes('\\n') || value.includes('\\r')) {\n return `\"${value.replace(/\"/g, '\"\"')}\"`;\n }\n return value;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,113 @@
1
+ import { ResolvedTheme, AnimationEase, ResolvedAnimation } from '@opendata-ai/openchart-core';
2
+
3
+ /**
4
+ * Animated GIF export.
5
+ *
6
+ * Unlike PNG/JPG (single static frame), GIF export must reproduce the chart's
7
+ * entrance animation across many frames. OpenChart entrance animations are pure
8
+ * CSS keyframes that mutate `clip-path`/`opacity`/`transform` in *computed*
9
+ * style, which `XMLSerializer` does not capture — so serializing the live,
10
+ * mid-animation SVG yields the base markup on every frame (a static GIF).
11
+ *
12
+ * This module sidesteps that by *synthesizing* frames deterministically: it
13
+ * takes the settled, final-state SVG plus the resolved animation config,
14
+ * computes each animated element's value at time `t` in JS, stamps
15
+ * `clip-path`/`opacity` as INLINE attributes on a clone (inline values DO
16
+ * serialize), rasterizes that clone, and encodes each frame with gifenc.
17
+ *
18
+ * This mirrors the entrance keyframes in `packages/core/src/styles/*.css`:
19
+ * bars clip bottom-up / left-right, line/area clip left-right, arcs and points
20
+ * fade. Only the entrance is reproduced; the on-screen CSS animation is
21
+ * untouched. `gifenc` is an optional peer dependency, loaded dynamically.
22
+ */
23
+
24
+ interface GIFExportOptions {
25
+ /** Frames per second to capture. Defaults to 25. */
26
+ fps?: number;
27
+ /** DPI scaling factor, matching PNG export. Defaults to 2. */
28
+ dpi?: number;
29
+ /**
30
+ * Loop behavior. Defaults to play-once (hold the final frame):
31
+ * - `false` / omitted: play once, then hold the final frame
32
+ * - `true`: loop forever
33
+ * - a number: loop that many times, then hold
34
+ */
35
+ loop?: boolean | number;
36
+ /**
37
+ * Explicit capture duration in ms. Defaults to the resolved animation total
38
+ * (enter duration + last element's stagger delay + a small tail).
39
+ */
40
+ durationMs?: number;
41
+ /** Embed fonts as base64 data URIs so text matches on-screen. Defaults to true. */
42
+ embedFonts?: boolean;
43
+ /**
44
+ * Opaque background fill. GIF can't carry partial alpha, so a transparent
45
+ * chart would composite onto black. Defaults to the chart's own background
46
+ * color (or white if the chart is transparent).
47
+ */
48
+ backgroundColor?: string;
49
+ /**
50
+ * Resolved theme to inline as a `<style>` block so class-based fills (metric
51
+ * cells, brand watermark dot, legend text) survive serialization. Omit to skip.
52
+ */
53
+ theme?: ResolvedTheme;
54
+ }
55
+ /** Which visual property an element's entrance keyframe animates. */
56
+ type AnimationKind = 'bar-vertical' | 'bar-horizontal' | 'line-area' | 'fade';
57
+ interface AnimatedTarget {
58
+ /** The element in the CLONE that receives per-frame inline styles. */
59
+ el: SVGElement;
60
+ kind: AnimationKind;
61
+ /** Start offset in ms (stagger * mark index). */
62
+ startMs: number;
63
+ /** Duration of this element's own animation in ms. */
64
+ durationMs: number;
65
+ }
66
+ /**
67
+ * Sample points of the `--oc-ease-smooth` token (see tokens.css). CSS
68
+ * `linear()` interpolates piecewise-linearly between evenly-spaced samples, so
69
+ * we reproduce it exactly rather than approximating with a cubic-bezier.
70
+ */
71
+ declare const EASE_SMOOTH_SAMPLES: number[];
72
+ /** Piecewise-linear evaluation of a CSS linear() sample array at t in [0,1]. */
73
+ declare function evalLinearSamples(samples: number[], t: number): number;
74
+ /**
75
+ * Classify an animated element (identified by `data-animation-index`) into the
76
+ * keyframe it plays, and return the element that should receive inline styles.
77
+ * Returns null only when a bar group has no shape child to clip. Unrecognized
78
+ * mark types (map/tilemap/table/text) fall back to a plain opacity fade rather
79
+ * than a hard pop-in — see the generic case at the end.
80
+ */
81
+ declare function classifyTarget(groupOrEl: SVGElement): {
82
+ el: SVGElement;
83
+ kind: AnimationKind;
84
+ } | null;
85
+ /**
86
+ * Distinguish "the optional gifenc peer isn't installed" from a real error
87
+ * thrown while loading it. Node/bundlers use ERR_MODULE_NOT_FOUND /
88
+ * MODULE_NOT_FOUND / "Cannot find module" for the former.
89
+ */
90
+ declare function isModuleNotFound(err: unknown): boolean;
91
+ /**
92
+ * Map the `loop` option to gifenc's `repeat` value:
93
+ * `true` → 0 (loop forever), a number → that count, otherwise → -1 (play once,
94
+ * hold the final frame — the default and the common editorial case).
95
+ */
96
+ declare function resolveRepeat(loop: boolean | number | undefined): number;
97
+ /**
98
+ * Apply the interpolated entrance state for a target at absolute time `nowMs`,
99
+ * writing inline `clip-path`/`opacity` so it survives serialization.
100
+ */
101
+ declare function applyFrameState(target: AnimatedTarget, nowMs: number, easeKind: AnimationEase): void;
102
+ /**
103
+ * Render a chart's entrance animation to an animated GIF Blob.
104
+ *
105
+ * @param svgElement - The settled, fully-rendered chart SVG (animation complete).
106
+ * @param animation - The chart's resolved animation config (for exact timing).
107
+ * When undefined, timing falls back to sensible defaults.
108
+ * @param options - fps, dpi, loop, duration, font-embedding overrides.
109
+ * @returns A Promise resolving to the GIF Blob.
110
+ */
111
+ declare function exportGIF(svgElement: SVGElement, animation: ResolvedAnimation | undefined, options?: GIFExportOptions): Promise<Blob>;
112
+
113
+ export { type AnimatedTarget, type AnimationKind, EASE_SMOOTH_SAMPLES, type GIFExportOptions, applyFrameState, classifyTarget, evalLinearSamples, exportGIF, isModuleNotFound, resolveRepeat };
@@ -0,0 +1,21 @@
1
+ import {
2
+ EASE_SMOOTH_SAMPLES,
3
+ applyFrameState,
4
+ classifyTarget,
5
+ evalLinearSamples,
6
+ exportGIF,
7
+ isModuleNotFound,
8
+ resolveRepeat
9
+ } from "./chunk-DCODCLWI.js";
10
+ import "./chunk-T32UHMZA.js";
11
+ import "./chunk-RM5XS6NR.js";
12
+ export {
13
+ EASE_SMOOTH_SAMPLES,
14
+ applyFrameState,
15
+ classifyTarget,
16
+ evalLinearSamples,
17
+ exportGIF,
18
+ isModuleNotFound,
19
+ resolveRepeat
20
+ };
21
+ //# sourceMappingURL=export-gif.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}