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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
  2. package/dist/chunk-DCODCLWI.js +234 -0
  3. package/dist/chunk-DCODCLWI.js.map +1 -0
  4. package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
  5. package/dist/chunk-EKAUQAAX.js.map +1 -0
  6. package/dist/{chunk-ENXSUXC5.js → chunk-M43GGK36.js} +7275 -5482
  7. package/dist/chunk-M43GGK36.js.map +1 -0
  8. package/dist/chunk-RM5XS6NR.js +113 -0
  9. package/dist/chunk-RM5XS6NR.js.map +1 -0
  10. package/dist/chunk-T32UHMZA.js +282 -0
  11. package/dist/chunk-T32UHMZA.js.map +1 -0
  12. package/dist/export-gif.d.ts +113 -0
  13. package/dist/export-gif.js +21 -0
  14. package/dist/export-gif.js.map +1 -0
  15. package/dist/index.d.ts +283 -24
  16. package/dist/index.js +2012 -763
  17. package/dist/index.js.map +1 -1
  18. package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
  19. package/dist/simulation-worker.js +83 -2
  20. package/dist/static.js +7 -69
  21. package/dist/static.js.map +1 -1
  22. package/dist/story/index.d.ts +3 -2
  23. package/dist/story/index.js +5 -3
  24. package/dist/story/index.js.map +1 -1
  25. package/dist/styles.css +1 -1
  26. package/dist/styles.css.map +1 -1
  27. package/package.json +14 -6
  28. package/dist/chunk-5EOV5JQZ.js.map +0 -1
  29. package/dist/chunk-ENXSUXC5.js.map +0 -1
  30. package/src/__test-fixtures__/dom.ts +0 -70
  31. package/src/__test-fixtures__/specs.ts +0 -214
  32. package/src/__tests__/animation.test.ts +0 -358
  33. package/src/__tests__/annotations-render.test.ts +0 -201
  34. package/src/__tests__/compound-labels.test.ts +0 -122
  35. package/src/__tests__/continuous-legend.test.ts +0 -91
  36. package/src/__tests__/crosshair.test.ts +0 -130
  37. package/src/__tests__/edit-events.test.ts +0 -902
  38. package/src/__tests__/events.test.ts +0 -387
  39. package/src/__tests__/export.test.ts +0 -241
  40. package/src/__tests__/font-race.test.ts +0 -199
  41. package/src/__tests__/gradient-ids.test.ts +0 -159
  42. package/src/__tests__/gradient-utils.test.ts +0 -125
  43. package/src/__tests__/known-bugs-render.test.ts +0 -183
  44. package/src/__tests__/map.test.ts +0 -293
  45. package/src/__tests__/mount.test.ts +0 -357
  46. package/src/__tests__/pattern-fills.test.ts +0 -105
  47. package/src/__tests__/resize-timing.test.ts +0 -218
  48. package/src/__tests__/sankey.test.ts +0 -133
  49. package/src/__tests__/selection-events.test.ts +0 -869
  50. package/src/__tests__/series-search.test.ts +0 -311
  51. package/src/__tests__/static.test.ts +0 -141
  52. package/src/__tests__/svg-renderer.test.ts +0 -895
  53. package/src/__tests__/table-keyboard.test.ts +0 -361
  54. package/src/__tests__/table-mount.test.ts +0 -639
  55. package/src/__tests__/tilemap.test.ts +0 -158
  56. package/src/__tests__/tooltip.test.ts +0 -337
  57. package/src/__tests__/transition.test.ts +0 -1777
  58. package/src/__tests__/you-draw-it.test.ts +0 -273
  59. package/src/animation-vars.ts +0 -42
  60. package/src/animation.ts +0 -76
  61. package/src/barlist-mount.ts +0 -383
  62. package/src/barlist-renderer.ts +0 -267
  63. package/src/dom-helpers.ts +0 -17
  64. package/src/export.ts +0 -482
  65. package/src/gradient-utils.ts +0 -138
  66. package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
  67. package/src/graph/__tests__/graph-mount.test.ts +0 -213
  68. package/src/graph/__tests__/interaction.test.ts +0 -205
  69. package/src/graph/__tests__/keyboard.test.ts +0 -653
  70. package/src/graph/__tests__/search.test.ts +0 -88
  71. package/src/graph/__tests__/simulation.test.ts +0 -233
  72. package/src/graph/__tests__/spatial-index.test.ts +0 -142
  73. package/src/graph/__tests__/zoom.test.ts +0 -203
  74. package/src/graph/canvas-renderer.ts +0 -747
  75. package/src/graph/interaction.ts +0 -367
  76. package/src/graph/keyboard.ts +0 -208
  77. package/src/graph/search.ts +0 -50
  78. package/src/graph/simulation-worker-url.ts +0 -12
  79. package/src/graph/simulation-worker.ts +0 -278
  80. package/src/graph/simulation.ts +0 -422
  81. package/src/graph/spatial-index.ts +0 -121
  82. package/src/graph/types.ts +0 -47
  83. package/src/graph/worker-protocol.ts +0 -70
  84. package/src/graph/zoom.ts +0 -134
  85. package/src/graph-mount.ts +0 -888
  86. package/src/index.ts +0 -76
  87. package/src/interactions/chart-events.ts +0 -139
  88. package/src/interactions/crosshair.ts +0 -233
  89. package/src/interactions/drag-handler.ts +0 -175
  90. package/src/interactions/editing-drags.ts +0 -515
  91. package/src/interactions/index.ts +0 -25
  92. package/src/interactions/keyboard-nav.ts +0 -111
  93. package/src/interactions/legend-interaction.ts +0 -46
  94. package/src/interactions/selection.ts +0 -280
  95. package/src/interactions/tooltip-events.ts +0 -72
  96. package/src/map-camera.ts +0 -88
  97. package/src/map-mount.ts +0 -701
  98. package/src/map-renderer.ts +0 -338
  99. package/src/map-transition.ts +0 -83
  100. package/src/measure-text.ts +0 -134
  101. package/src/mount.ts +0 -1382
  102. package/src/pattern-utils.ts +0 -110
  103. package/src/renderers/annotations.ts +0 -388
  104. package/src/renderers/axes.ts +0 -351
  105. package/src/renderers/brand.ts +0 -87
  106. package/src/renderers/chrome.ts +0 -196
  107. package/src/renderers/endpoint-labels.ts +0 -150
  108. package/src/renderers/legend.ts +0 -311
  109. package/src/renderers/marks.ts +0 -561
  110. package/src/renderers/metrics.ts +0 -63
  111. package/src/renderers/svg-dom.ts +0 -40
  112. package/src/renderers/table-cells.ts +0 -446
  113. package/src/resize-observer.ts +0 -46
  114. package/src/sankey-mount.ts +0 -622
  115. package/src/sankey-renderer.ts +0 -614
  116. package/src/series-search.ts +0 -392
  117. package/src/static.ts +0 -251
  118. package/src/story/__tests__/camera-math.test.ts +0 -145
  119. package/src/story/__tests__/create-chart-story.test.ts +0 -317
  120. package/src/story/__tests__/progress-math.test.ts +0 -95
  121. package/src/story/__tests__/scroll-driver.test.ts +0 -130
  122. package/src/story/__tests__/tween.test.ts +0 -159
  123. package/src/story/camera-math.ts +0 -125
  124. package/src/story/create-chart-story.ts +0 -353
  125. package/src/story/crossfade.ts +0 -70
  126. package/src/story/index.ts +0 -39
  127. package/src/story/progress-math.ts +0 -85
  128. package/src/story/resolve-camera-target.ts +0 -119
  129. package/src/story/scroll-driver.ts +0 -206
  130. package/src/story/story-camera.ts +0 -49
  131. package/src/story/tween.ts +0 -112
  132. package/src/story/types.ts +0 -76
  133. package/src/svg-ids.ts +0 -23
  134. package/src/svg-renderer.ts +0 -383
  135. package/src/table-keyboard.ts +0 -266
  136. package/src/table-mount.ts +0 -567
  137. package/src/table-renderer.ts +0 -414
  138. package/src/text-edit-overlay.ts +0 -255
  139. package/src/theme-tokens.ts +0 -51
  140. package/src/tilemap-mount.ts +0 -511
  141. package/src/tilemap-renderer.ts +0 -396
  142. package/src/tooltip.ts +0 -177
  143. package/src/transition.ts +0 -2816
  144. package/src/you-draw-it.ts +0 -480
  145. /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
@@ -3,7 +3,7 @@ import {
3
3
  renderChromeElement,
4
4
  renderLegend,
5
5
  stampAnimationVars
6
- } from "./chunk-5EOV5JQZ.js";
6
+ } from "./chunk-EKAUQAAX.js";
7
7
 
8
8
  // src/tilemap-renderer.ts
9
9
  var SVG_NS = "http://www.w3.org/2000/svg";
@@ -295,4 +295,4 @@ function renderTileMapSVG(layout, opts) {
295
295
  export {
296
296
  renderTileMapSVG
297
297
  };
298
- //# sourceMappingURL=chunk-W34KN7FD.js.map
298
+ //# sourceMappingURL=chunk-2NAQRP66.js.map
@@ -0,0 +1,234 @@
1
+ import {
2
+ embedFonts,
3
+ ensureSVGDimensions,
4
+ exportSVG,
5
+ getSVGBackgroundColor,
6
+ getSVGDimensions,
7
+ rasterizeSVGToCanvas
8
+ } from "./chunk-T32UHMZA.js";
9
+ import {
10
+ injectThemeStyleBlock
11
+ } from "./chunk-RM5XS6NR.js";
12
+
13
+ // src/gif-encode.ts
14
+ function readCanvasSRGB(canvas) {
15
+ const ctx = canvas.getContext("2d");
16
+ if (!ctx) throw new Error("Canvas 2D context not available");
17
+ return ctx.getImageData(0, 0, canvas.width, canvas.height, {
18
+ colorSpace: "srgb"
19
+ }).data;
20
+ }
21
+ function paletteFromCanvas(canvas, quantize) {
22
+ return quantize(readCanvasSRGB(canvas), 256);
23
+ }
24
+
25
+ // src/export-gif.ts
26
+ var EASE_SMOOTH_SAMPLES = [
27
+ 0,
28
+ 0.157,
29
+ 0.438,
30
+ 0.64,
31
+ 0.766,
32
+ 0.85,
33
+ 0.906,
34
+ 0.941,
35
+ 0.964,
36
+ 0.978,
37
+ 0.988,
38
+ 0.994,
39
+ 0.998,
40
+ 1
41
+ ];
42
+ function evalLinearSamples(samples, t) {
43
+ if (t <= 0) return samples[0];
44
+ if (t >= 1) return samples[samples.length - 1];
45
+ const scaled = t * (samples.length - 1);
46
+ const i = Math.floor(scaled);
47
+ const frac = scaled - i;
48
+ return samples[i] + (samples[i + 1] - samples[i]) * frac;
49
+ }
50
+ function ease(p, _kind) {
51
+ return evalLinearSamples(EASE_SMOOTH_SAMPLES, p);
52
+ }
53
+ function classifyTarget(groupOrEl) {
54
+ const cls = groupOrEl.getAttribute("class") ?? "";
55
+ if (cls.includes("oc-mark-rect") || cls.includes("oc-mark-bar")) {
56
+ const rect = groupOrEl.querySelector("rect") ?? groupOrEl.querySelector("path");
57
+ if (!rect) return null;
58
+ const horizontal = groupOrEl.getAttribute("data-orient") === "horizontal";
59
+ return { el: rect, kind: horizontal ? "bar-horizontal" : "bar-vertical" };
60
+ }
61
+ if (cls.includes("oc-mark-line") || cls.includes("oc-mark-area")) {
62
+ return { el: groupOrEl, kind: "line-area" };
63
+ }
64
+ if (cls.includes("oc-mark-arc") || cls.includes("oc-mark-point") || cls.includes("oc-mark-circle")) {
65
+ return { el: groupOrEl, kind: "fade" };
66
+ }
67
+ return { el: groupOrEl, kind: "fade" };
68
+ }
69
+ function collectTargets(clone, enterDurationMs, staggerMs) {
70
+ const els = clone.querySelectorAll("[data-animation-index]");
71
+ const targets = [];
72
+ for (const el of els) {
73
+ const classified = classifyTarget(el);
74
+ if (!classified) continue;
75
+ const idx = parseInt(el.getAttribute("data-animation-index") ?? "0", 10) || 0;
76
+ const isPoint = classified.kind === "fade" && /oc-mark-(point|circle)/.test(el.getAttribute("class") ?? "");
77
+ const lineOffset = isPoint && hasLineAreaSibling(el) ? enterDurationMs * 0.35 : 0;
78
+ targets.push({
79
+ el: classified.el,
80
+ kind: classified.kind,
81
+ startMs: staggerMs * idx + lineOffset,
82
+ durationMs: isPoint ? enterDurationMs * 0.4 : enterDurationMs
83
+ });
84
+ }
85
+ return targets;
86
+ }
87
+ function hasLineAreaSibling(point) {
88
+ let sib = point.previousElementSibling;
89
+ while (sib) {
90
+ const cls = sib.getAttribute("class") ?? "";
91
+ if (cls.includes("oc-mark-line") || cls.includes("oc-mark-area")) return true;
92
+ sib = sib.previousElementSibling;
93
+ }
94
+ return false;
95
+ }
96
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
97
+ function isModuleNotFound(err) {
98
+ const code = err?.code;
99
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return true;
100
+ const message = err?.message ?? "";
101
+ return /cannot find (module|package)|failed to (resolve|fetch)/i.test(message);
102
+ }
103
+ function resolveRepeat(loop) {
104
+ if (loop === true) return 0;
105
+ if (typeof loop === "number") return loop;
106
+ return -1;
107
+ }
108
+ function applyFrameState(target, nowMs, easeKind) {
109
+ const p = clamp01((nowMs - target.startMs) / target.durationMs);
110
+ const eased = ease(p, easeKind);
111
+ const style = target.el.style;
112
+ switch (target.kind) {
113
+ case "bar-vertical": {
114
+ const inset = (1 - eased) * 100;
115
+ style.clipPath = `inset(${inset}% 0 0 0)`;
116
+ style.opacity = String(clamp01(p / 0.75));
117
+ break;
118
+ }
119
+ case "bar-horizontal": {
120
+ const inset = (1 - eased) * 100;
121
+ style.clipPath = `inset(0 ${inset}% 0 0)`;
122
+ style.opacity = String(clamp01(p / 0.75));
123
+ break;
124
+ }
125
+ case "line-area": {
126
+ const inset = (1 - eased) * 100;
127
+ style.clipPath = `inset(0 ${inset}% 0 0)`;
128
+ style.opacity = String(clamp01(p / 0.15));
129
+ break;
130
+ }
131
+ case "fade": {
132
+ style.opacity = String(eased);
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ function clearFrameState(target) {
138
+ const style = target.el.style;
139
+ style.clipPath = "";
140
+ style.opacity = "";
141
+ }
142
+ async function exportGIF(svgElement, animation, options) {
143
+ let gifenc;
144
+ try {
145
+ gifenc = await import("gifenc");
146
+ } catch (err) {
147
+ if (isModuleNotFound(err)) {
148
+ throw new Error(
149
+ "GIF export requires the optional 'gifenc' peer dependency. Install it: npm install gifenc"
150
+ );
151
+ }
152
+ throw new Error("GIF export failed to load the gifenc encoder", { cause: err });
153
+ }
154
+ const { GIFEncoder, quantize, applyPalette } = gifenc;
155
+ const fps = options?.fps ?? 25;
156
+ const dpi = options?.dpi ?? 2;
157
+ const shouldEmbed = options?.embedFonts ?? true;
158
+ const { width, height } = getSVGDimensions(svgElement);
159
+ if (!width || !height) {
160
+ throw new Error(`SVG has zero dimensions (width=${width}, height=${height})`);
161
+ }
162
+ const enter = animation?.enter;
163
+ const enterDurationMs = enter?.duration ?? 600;
164
+ const staggerMs = enter?.staggerDelay ?? 0;
165
+ const easeKind = enter?.ease ?? "smooth";
166
+ const clone = svgElement.cloneNode(true);
167
+ injectThemeStyleBlock(clone, options?.theme);
168
+ if (shouldEmbed) {
169
+ await embedFonts(clone);
170
+ }
171
+ const targets = collectTargets(clone, enterDurationMs, staggerMs);
172
+ const annotationDelay = animation?.annotationDelay ?? 0;
173
+ const lastMarkEnd = targets.reduce(
174
+ (m, t) => Math.max(m, t.startMs + t.durationMs),
175
+ enterDurationMs
176
+ );
177
+ const lastEnd = Math.max(lastMarkEnd, enterDurationMs + annotationDelay);
178
+ const totalMs = options?.durationMs ?? lastEnd + 300;
179
+ const frameDelay = Math.round(1e3 / fps);
180
+ const totalFrames = Math.max(1, Math.round(totalMs / 1e3 * fps));
181
+ const repeat = resolveRepeat(options?.loop);
182
+ const bgColor = options?.backgroundColor ?? getSVGBackgroundColor(svgElement, options?.theme?.colors.background);
183
+ const fillBackground = (ctx, cv) => {
184
+ ctx.fillStyle = bgColor;
185
+ ctx.fillRect(0, 0, cv.width, cv.height);
186
+ };
187
+ const gif = GIFEncoder();
188
+ let globalPalette = [];
189
+ await seedPalette(clone, targets, width, height, dpi, fillBackground, (data) => {
190
+ globalPalette = quantize(data, 256);
191
+ });
192
+ let frameWritten = 0;
193
+ const writeFrame = async (nowMs) => {
194
+ if (nowMs === null) {
195
+ for (const t of targets) clearFrameState(t);
196
+ } else {
197
+ for (const t of targets) applyFrameState(t, nowMs, easeKind);
198
+ }
199
+ const svgString = ensureSVGDimensions(exportSVG(clone), width, height);
200
+ const canvas = await rasterizeSVGToCanvas(svgString, width, height, dpi, fillBackground);
201
+ const index = applyPalette(readCanvasSRGB(canvas), globalPalette);
202
+ gif.writeFrame(index, canvas.width, canvas.height, {
203
+ palette: globalPalette,
204
+ delay: frameDelay,
205
+ ...frameWritten === 0 ? { repeat } : {}
206
+ });
207
+ frameWritten++;
208
+ };
209
+ for (let i = 0; i < totalFrames; i++) {
210
+ await writeFrame(i / fps * 1e3);
211
+ }
212
+ await writeFrame(null);
213
+ gif.finish();
214
+ return new Blob([gif.bytes()], { type: "image/gif" });
215
+ }
216
+ async function seedPalette(clone, targets, width, height, dpi, prepare, use) {
217
+ for (const t of targets) clearFrameState(t);
218
+ const svgString = ensureSVGDimensions(exportSVG(clone), width, height);
219
+ const canvas = await rasterizeSVGToCanvas(svgString, width, height, dpi, prepare);
220
+ use(readCanvasSRGB(canvas));
221
+ }
222
+
223
+ export {
224
+ readCanvasSRGB,
225
+ paletteFromCanvas,
226
+ EASE_SMOOTH_SAMPLES,
227
+ evalLinearSamples,
228
+ classifyTarget,
229
+ isModuleNotFound,
230
+ resolveRepeat,
231
+ applyFrameState,
232
+ exportGIF
233
+ };
234
+ //# sourceMappingURL=chunk-DCODCLWI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/gif-encode.ts","../src/export-gif.ts"],"sourcesContent":["/**\n * Shared gifenc frame-encoding primitives.\n *\n * Both GIF exporters (`export-gif.ts`, entrance animation; `export-sequence.ts`,\n * spec-swap keyframes) need the same low-level steps: read a canvas back in\n * sRGB (the export canvas may be display-p3, but gifenc's quantizer assumes sRGB\n * bytes), quantize a shared palette, and write indexed frames. They differ only\n * in how they PRODUCE canvases — synthesized per time-step vs. one per spec — so\n * that stays in each caller; only these primitives are shared, which keeps the\n * sRGB-readback subtlety in exactly one place.\n */\n\n/**\n * Read a canvas's pixels back in sRGB. The export canvas may be display-p3 (so\n * PNG/JPG can embed a wide-gamut profile), but GIF is sRGB-only and gifenc's\n * quantizer assumes sRGB bytes — feeding it P3-encoded data would shift and\n * desaturate the GIF. The browser converts P3→sRGB on readback.\n */\nexport function readCanvasSRGB(canvas: HTMLCanvasElement): Uint8ClampedArray {\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('Canvas 2D context not available');\n return ctx.getImageData(0, 0, canvas.width, canvas.height, {\n colorSpace: 'srgb',\n }).data;\n}\n\n/** Quantize a 256-color palette from a canvas's sRGB pixels. */\nexport function paletteFromCanvas(\n canvas: HTMLCanvasElement,\n quantize: (data: Uint8ClampedArray, maxColors: number) => number[][],\n): number[][] {\n return quantize(readCanvasSRGB(canvas), 256);\n}\n","/**\n * Animated GIF export.\n *\n * Unlike PNG/JPG (single static frame), GIF export must reproduce the chart's\n * entrance animation across many frames. OpenChart entrance animations are pure\n * CSS keyframes that mutate `clip-path`/`opacity`/`transform` in *computed*\n * style, which `XMLSerializer` does not capture — so serializing the live,\n * mid-animation SVG yields the base markup on every frame (a static GIF).\n *\n * This module sidesteps that by *synthesizing* frames deterministically: it\n * takes the settled, final-state SVG plus the resolved animation config,\n * computes each animated element's value at time `t` in JS, stamps\n * `clip-path`/`opacity` as INLINE attributes on a clone (inline values DO\n * serialize), rasterizes that clone, and encodes each frame with gifenc.\n *\n * This mirrors the entrance keyframes in `packages/core/src/styles/*.css`:\n * bars clip bottom-up / left-right, line/area clip left-right, arcs and points\n * fade. Only the entrance is reproduced; the on-screen CSS animation is\n * untouched. `gifenc` is an optional peer dependency, loaded dynamically.\n */\n\nimport type { AnimationEase, ResolvedAnimation, ResolvedTheme } from '@opendata-ai/openchart-core';\nimport {\n embedFonts,\n ensureSVGDimensions,\n exportSVG,\n getSVGBackgroundColor,\n getSVGDimensions,\n rasterizeSVGToCanvas,\n} from './export';\nimport { readCanvasSRGB } from './gif-encode';\nimport { injectThemeStyleBlock } from './theme-style-block';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface GIFExportOptions {\n /** Frames per second to capture. Defaults to 25. */\n fps?: number;\n /** DPI scaling factor, matching PNG export. Defaults to 2. */\n dpi?: number;\n /**\n * Loop behavior. Defaults to play-once (hold the final frame):\n * - `false` / omitted: play once, then hold the final frame\n * - `true`: loop forever\n * - a number: loop that many times, then hold\n */\n loop?: boolean | number;\n /**\n * Explicit capture duration in ms. Defaults to the resolved animation total\n * (enter duration + last element's stagger delay + a small tail).\n */\n durationMs?: number;\n /** Embed fonts as base64 data URIs so text matches on-screen. Defaults to true. */\n embedFonts?: boolean;\n /**\n * Opaque background fill. GIF can't carry partial alpha, so a transparent\n * chart would composite onto black. Defaults to the chart's own background\n * color (or white if the chart is transparent).\n */\n backgroundColor?: string;\n /**\n * Resolved theme to inline as a `<style>` block so class-based fills (metric\n * cells, brand watermark dot, legend text) survive serialization. Omit to skip.\n */\n theme?: ResolvedTheme;\n}\n\n/** Which visual property an element's entrance keyframe animates. */\nexport type AnimationKind = 'bar-vertical' | 'bar-horizontal' | 'line-area' | 'fade';\n\nexport interface AnimatedTarget {\n /** The element in the CLONE that receives per-frame inline styles. */\n el: SVGElement;\n kind: AnimationKind;\n /** Start offset in ms (stagger * mark index). */\n startMs: number;\n /** Duration of this element's own animation in ms. */\n durationMs: number;\n}\n\n// ---------------------------------------------------------------------------\n// Easing — replicate --oc-ease-smooth (CSS linear() sample points)\n// ---------------------------------------------------------------------------\n\n/**\n * Sample points of the `--oc-ease-smooth` token (see tokens.css). CSS\n * `linear()` interpolates piecewise-linearly between evenly-spaced samples, so\n * we reproduce it exactly rather than approximating with a cubic-bezier.\n */\nexport const EASE_SMOOTH_SAMPLES = [\n 0, 0.157, 0.438, 0.64, 0.766, 0.85, 0.906, 0.941, 0.964, 0.978, 0.988, 0.994, 0.998, 1,\n];\n\n/** Piecewise-linear evaluation of a CSS linear() sample array at t in [0,1]. */\nexport function evalLinearSamples(samples: number[], t: number): number {\n if (t <= 0) return samples[0];\n if (t >= 1) return samples[samples.length - 1];\n const scaled = t * (samples.length - 1);\n const i = Math.floor(scaled);\n const frac = scaled - i;\n return samples[i] + (samples[i + 1] - samples[i]) * frac;\n}\n\n/** Ease a normalized progress value. Only 'smooth' is sample-accurate today. */\nfunction ease(p: number, _kind: AnimationEase): number {\n // The entrance rules use --oc-ease-smooth (or fall back to it) in all cases\n // GIF export handles today. Snappy/others could add their own samples later.\n return evalLinearSamples(EASE_SMOOTH_SAMPLES, p);\n}\n\n// ---------------------------------------------------------------------------\n// Target discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Classify an animated element (identified by `data-animation-index`) into the\n * keyframe it plays, and return the element that should receive inline styles.\n * Returns null only when a bar group has no shape child to clip. Unrecognized\n * mark types (map/tilemap/table/text) fall back to a plain opacity fade rather\n * than a hard pop-in — see the generic case at the end.\n */\nexport function classifyTarget(\n groupOrEl: SVGElement,\n): { el: SVGElement; kind: AnimationKind } | null {\n const cls = groupOrEl.getAttribute('class') ?? '';\n\n // Bars: CSS animates the child <rect>; clip + fade. Orient on the group.\n if (cls.includes('oc-mark-rect') || cls.includes('oc-mark-bar')) {\n const rect = groupOrEl.querySelector('rect') ?? groupOrEl.querySelector('path');\n if (!rect) return null;\n const horizontal = groupOrEl.getAttribute('data-orient') === 'horizontal';\n return { el: rect as SVGElement, kind: horizontal ? 'bar-horizontal' : 'bar-vertical' };\n }\n\n // Line/area: the whole group clips left-to-right + fades.\n if (cls.includes('oc-mark-line') || cls.includes('oc-mark-area')) {\n return { el: groupOrEl, kind: 'line-area' };\n }\n\n // Arcs and points: fade only (scale breaks arc/point positioning, so the CSS\n // uses a plain opacity fade too).\n if (\n cls.includes('oc-mark-arc') ||\n cls.includes('oc-mark-point') ||\n cls.includes('oc-mark-circle')\n ) {\n return { el: groupOrEl, kind: 'fade' };\n }\n\n // Everything else (text labels, rules, ticks, map/tilemap/table marks) is a\n // plain fade; treat generically so the frame isn't a hard pop-in.\n return { el: groupOrEl, kind: 'fade' };\n}\n\n/**\n * Build the list of animated targets from a cloned SVG, pairing each element\n * with its timing. Elements carry `data-animation-index` (the mark index that\n * drives stagger) stamped by the renderer.\n */\nfunction collectTargets(\n clone: SVGElement,\n enterDurationMs: number,\n staggerMs: number,\n): AnimatedTarget[] {\n const els = clone.querySelectorAll<SVGElement>('[data-animation-index]');\n const targets: AnimatedTarget[] = [];\n\n for (const el of els) {\n const classified = classifyTarget(el);\n if (!classified) continue;\n const idx = parseInt(el.getAttribute('data-animation-index') ?? '0', 10) || 0;\n // Points animate at 40% of the base duration (matches animation.css).\n const isPoint =\n classified.kind === 'fade' && /oc-mark-(point|circle)/.test(el.getAttribute('class') ?? '');\n // Points on line/area charts start 35% of a duration in, so they pop as the\n // line draws through them (the `.oc-mark-line ~ circle.oc-mark-point` rule).\n const lineOffset = isPoint && hasLineAreaSibling(el) ? enterDurationMs * 0.35 : 0;\n targets.push({\n el: classified.el,\n kind: classified.kind,\n startMs: staggerMs * idx + lineOffset,\n durationMs: isPoint ? enterDurationMs * 0.4 : enterDurationMs,\n });\n }\n\n return targets;\n}\n\n/**\n * Replicate the CSS sibling combinator `.oc-mark-line ~ circle.oc-mark-point`:\n * true when a line or area mark precedes this point among its siblings.\n */\nfunction hasLineAreaSibling(point: SVGElement): boolean {\n let sib = point.previousElementSibling;\n while (sib) {\n const cls = sib.getAttribute('class') ?? '';\n if (cls.includes('oc-mark-line') || cls.includes('oc-mark-area')) return true;\n sib = sib.previousElementSibling;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Per-frame interpolation\n// ---------------------------------------------------------------------------\n\nconst clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);\n\n/**\n * Distinguish \"the optional gifenc peer isn't installed\" from a real error\n * thrown while loading it. Node/bundlers use ERR_MODULE_NOT_FOUND /\n * MODULE_NOT_FOUND / \"Cannot find module\" for the former.\n */\nexport function isModuleNotFound(err: unknown): boolean {\n const code = (err as { code?: string })?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;\n const message = (err as { message?: string })?.message ?? '';\n return /cannot find (module|package)|failed to (resolve|fetch)/i.test(message);\n}\n\n/**\n * Map the `loop` option to gifenc's `repeat` value:\n * `true` → 0 (loop forever), a number → that count, otherwise → -1 (play once,\n * hold the final frame — the default and the common editorial case).\n */\nexport function resolveRepeat(loop: boolean | number | undefined): number {\n if (loop === true) return 0;\n if (typeof loop === 'number') return loop;\n return -1;\n}\n\n/**\n * Apply the interpolated entrance state for a target at absolute time `nowMs`,\n * writing inline `clip-path`/`opacity` so it survives serialization.\n */\nexport function applyFrameState(\n target: AnimatedTarget,\n nowMs: number,\n easeKind: AnimationEase,\n): void {\n const p = clamp01((nowMs - target.startMs) / target.durationMs);\n const eased = ease(p, easeKind);\n const style = target.el.style;\n\n switch (target.kind) {\n case 'bar-vertical': {\n // oc-enter-bar: clip-path inset(100%→0 from top) + opacity (full by 75%).\n const inset = (1 - eased) * 100;\n style.clipPath = `inset(${inset}% 0 0 0)`;\n style.opacity = String(clamp01(p / 0.75));\n break;\n }\n case 'bar-horizontal': {\n // oc-enter-bar-h: clip-path inset(0 100%→0 from right) + opacity.\n const inset = (1 - eased) * 100;\n style.clipPath = `inset(0 ${inset}% 0 0)`;\n style.opacity = String(clamp01(p / 0.75));\n break;\n }\n case 'line-area': {\n // oc-enter-line: clip-path inset(0 100%→0 from right); opacity full by 15%.\n const inset = (1 - eased) * 100;\n style.clipPath = `inset(0 ${inset}% 0 0)`;\n style.opacity = String(clamp01(p / 0.15));\n break;\n }\n case 'fade': {\n // oc-enter-fade-only: opacity 0→1.\n style.opacity = String(eased);\n break;\n }\n }\n}\n\n/** Clear any inline entrance styles so the settled frame renders at rest. */\nfunction clearFrameState(target: AnimatedTarget): void {\n const style = target.el.style;\n style.clipPath = '';\n style.opacity = '';\n}\n\n// ---------------------------------------------------------------------------\n// Export\n// ---------------------------------------------------------------------------\n\n/**\n * Render a chart's entrance animation to an animated GIF Blob.\n *\n * @param svgElement - The settled, fully-rendered chart SVG (animation complete).\n * @param animation - The chart's resolved animation config (for exact timing).\n * When undefined, timing falls back to sensible defaults.\n * @param options - fps, dpi, loop, duration, font-embedding overrides.\n * @returns A Promise resolving to the GIF Blob.\n */\nexport async function exportGIF(\n svgElement: SVGElement,\n animation: ResolvedAnimation | undefined,\n options?: GIFExportOptions,\n): Promise<Blob> {\n let gifenc: typeof import('gifenc');\n try {\n gifenc = await import('gifenc');\n } catch (err) {\n // Only a genuine \"module not found\" means the optional peer is absent. Any\n // other failure (a real bug in gifenc's module eval, a corrupt install) must\n // surface with its context, not the misleading install hint.\n if (isModuleNotFound(err)) {\n throw new Error(\n \"GIF export requires the optional 'gifenc' peer dependency. Install it: npm install gifenc\",\n );\n }\n throw new Error('GIF export failed to load the gifenc encoder', { cause: err });\n }\n const { GIFEncoder, quantize, applyPalette } = gifenc;\n\n const fps = options?.fps ?? 25;\n const dpi = options?.dpi ?? 2;\n const shouldEmbed = options?.embedFonts ?? true;\n const { width, height } = getSVGDimensions(svgElement);\n if (!width || !height) {\n throw new Error(`SVG has zero dimensions (width=${width}, height=${height})`);\n }\n\n // Resolve timing from the config (exact) with defaults as a fallback.\n const enter = animation?.enter;\n const enterDurationMs = enter?.duration ?? 600;\n const staggerMs = enter?.staggerDelay ?? 0;\n const easeKind: AnimationEase = enter?.ease ?? 'smooth';\n\n // Build one font-embedded clone that represents the FINAL frame; every frame\n // derives from it, so fonts are embedded exactly once.\n const clone = svgElement.cloneNode(true) as SVGElement;\n // Inline the theme style block so class-based chrome fills survive the detach\n // from the page stylesheet (metrics, brand dot, legend). No-op when undefined.\n injectThemeStyleBlock(clone, options?.theme);\n if (shouldEmbed) {\n await embedFonts(clone);\n }\n const targets = collectTargets(clone, enterDurationMs, staggerMs);\n\n // Total capture window: explicit override, else the animation total. Mirrors\n // computeAnimationDuration (animation.ts): the window must cover the last\n // mark's stagger + its own duration AND the annotation reveal, which animates\n // at enterDuration + annotationDelay. Without the annotationDelay term, charts\n // with annotations get cut off before the annotations finish fading in.\n const annotationDelay = animation?.annotationDelay ?? 0;\n const lastMarkEnd = targets.reduce(\n (m, t) => Math.max(m, t.startMs + t.durationMs),\n enterDurationMs,\n );\n const lastEnd = Math.max(lastMarkEnd, enterDurationMs + annotationDelay);\n const totalMs = options?.durationMs ?? lastEnd + 300;\n const frameDelay = Math.round(1000 / fps);\n const totalFrames = Math.max(1, Math.round((totalMs / 1000) * fps));\n\n // Loop → gifenc repeat: play-once (-1), forever (0), or explicit count.\n const repeat = resolveRepeat(options?.loop);\n\n // GIF can't carry partial alpha, so fill an opaque background before drawing\n // each frame (else a transparent chart composites onto black). Runs before\n // ctx.scale, so paint the raw device-pixel canvas size.\n // The theme background is the fallback: canvas mark mode suppresses the SVG\n // background rect, so there's nothing in the markup to read it from.\n const bgColor =\n options?.backgroundColor ??\n getSVGBackgroundColor(svgElement, options?.theme?.colors.background);\n const fillBackground = (ctx: CanvasRenderingContext2D, cv: HTMLCanvasElement): void => {\n ctx.fillStyle = bgColor;\n ctx.fillRect(0, 0, cv.width, cv.height);\n };\n\n const gif = GIFEncoder();\n\n // Derive ONE global palette from the settled frame (most complete colors)\n // and reuse it for every frame, so colors stay stable and the file stays\n // small. gifenc writes the global color table from the palette on the first\n // frame; later frames reuse it.\n let globalPalette: number[][] = [];\n await seedPalette(clone, targets, width, height, dpi, fillBackground, (data) => {\n globalPalette = quantize(data, 256);\n });\n\n let frameWritten = 0;\n const writeFrame = async (nowMs: number | null): Promise<void> => {\n if (nowMs === null) {\n for (const t of targets) clearFrameState(t);\n } else {\n for (const t of targets) applyFrameState(t, nowMs, easeKind);\n }\n const svgString = ensureSVGDimensions(exportSVG(clone), width, height);\n const canvas = await rasterizeSVGToCanvas(svgString, width, height, dpi, fillBackground);\n const index = applyPalette(readCanvasSRGB(canvas), globalPalette);\n // `repeat` is only meaningful on the first frame (it writes the loop\n // extension); passing it later is ignored by gifenc.\n gif.writeFrame(index, canvas.width, canvas.height, {\n palette: globalPalette,\n delay: frameDelay,\n ...(frameWritten === 0 ? { repeat } : {}),\n });\n frameWritten++;\n };\n\n for (let i = 0; i < totalFrames; i++) {\n await writeFrame((i / fps) * 1000);\n }\n // Final settled frame the GIF holds on.\n await writeFrame(null);\n\n gif.finish();\n // gifenc types bytes() as Uint8Array<ArrayBufferLike>; BlobPart wants a view\n // over a plain ArrayBuffer. The runtime value is a valid BufferSource, so the\n // cast is safe — it only bridges the ArrayBufferLike/ArrayBuffer lib mismatch.\n return new Blob([gif.bytes() as BlobPart], { type: 'image/gif' });\n}\n\n/**\n * Render the settled clone once (no per-frame state) to derive the palette\n * source, without writing a frame. Restores frame state afterward.\n */\nasync function seedPalette(\n clone: SVGElement,\n targets: AnimatedTarget[],\n width: number,\n height: number,\n dpi: number,\n prepare: (ctx: CanvasRenderingContext2D, cv: HTMLCanvasElement) => void,\n use: (data: Uint8ClampedArray) => void,\n): Promise<void> {\n for (const t of targets) clearFrameState(t);\n const svgString = ensureSVGDimensions(exportSVG(clone), width, height);\n const canvas = await rasterizeSVGToCanvas(svgString, width, height, dpi, prepare);\n use(readCanvasSRGB(canvas));\n}\n"],"mappings":"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","names":[]}
@@ -1,30 +1,10 @@
1
- // src/renderers/svg-dom.ts
2
- var SVG_NS = "http://www.w3.org/2000/svg";
3
- var XLINK_NS = "http://www.w3.org/1999/xlink";
4
- function createSVGElement(tag) {
5
- return document.createElementNS(SVG_NS, tag);
6
- }
7
- function setAttrs(el, attrs) {
8
- for (const [key, value] of Object.entries(attrs)) {
9
- el.setAttribute(key, String(value));
10
- }
11
- }
12
- function applyTextStyle(el, style) {
13
- const inline = el.style;
14
- inline.setProperty("fill", style.fill);
15
- inline.setProperty("font-size", `${style.fontSize}px`);
16
- inline.setProperty("font-weight", String(style.fontWeight));
17
- inline.setProperty("font-family", style.fontFamily);
18
- if (style.textAnchor) {
19
- el.setAttribute("text-anchor", style.textAnchor);
20
- }
21
- if (style.dominantBaseline) {
22
- el.setAttribute("dominant-baseline", style.dominantBaseline);
23
- }
24
- if (style.fontVariant) {
25
- el.setAttribute("font-variant", style.fontVariant);
26
- }
27
- }
1
+ import {
2
+ SVG_NS,
3
+ XLINK_NS,
4
+ applyTextStyle,
5
+ createSVGElement,
6
+ setAttrs
7
+ } from "./chunk-RM5XS6NR.js";
28
8
 
29
9
  // src/svg-ids.ts
30
10
  var counter = 0;
@@ -483,11 +463,12 @@ function getMarkSeries(mark) {
483
463
  }
484
464
  return void 0;
485
465
  }
486
- function renderMarks(parent, layout) {
466
+ function renderMarks(parent, layout, opts) {
487
467
  const g = createSVGElement("g");
488
468
  g.setAttribute("class", "oc-marks");
489
469
  for (let i = 0; i < layout.marks.length; i++) {
490
470
  const mark = layout.marks[i];
471
+ if (opts?.skipPoints && mark.type === "point") continue;
491
472
  const renderer = markRenderers[mark.type];
492
473
  if (!renderer) continue;
493
474
  const el = renderer(mark, i);
@@ -850,7 +831,7 @@ function appendCompoundLabel(parent, primaryText, subtitle, fontWeight) {
850
831
  subtitleSpan.setAttribute("fill-opacity", "0.6");
851
832
  parent.appendChild(subtitleSpan);
852
833
  }
853
- function renderAxis(parent, axis, orientation, layout) {
834
+ function renderAxis(parent, axis, orientation, layout, opts) {
854
835
  const g = createSVGElement("g");
855
836
  const isRight = orientation === "y" && axis.orient === "right";
856
837
  const isInlineY = orientation === "y" && axis.tickPosition === "inline" && !isRight;
@@ -995,7 +976,7 @@ function renderAxis(parent, axis, orientation, layout) {
995
976
  g.appendChild(label);
996
977
  }
997
978
  }
998
- if (!isRight) {
979
+ if (!isRight && !opts?.skipGridlines) {
999
980
  const posToTickKey = /* @__PURE__ */ new Map();
1000
981
  for (const tick of axis.ticks) {
1001
982
  posToTickKey.set(tick.position, serializeKeyValue(tick.value));
@@ -1101,15 +1082,15 @@ function renderAxis(parent, axis, orientation, layout) {
1101
1082
  }
1102
1083
  parent.appendChild(g);
1103
1084
  }
1104
- function renderAxes(parent, layout) {
1085
+ function renderAxes(parent, layout, opts) {
1105
1086
  if (layout.axes.x) {
1106
- renderAxis(parent, layout.axes.x, "x", layout);
1087
+ renderAxis(parent, layout.axes.x, "x", layout, opts);
1107
1088
  }
1108
1089
  if (layout.axes.y) {
1109
- renderAxis(parent, layout.axes.y, "y", layout);
1090
+ renderAxis(parent, layout.axes.y, "y", layout, opts);
1110
1091
  }
1111
1092
  if (layout.axes.y2) {
1112
- renderAxis(parent, layout.axes.y2, "y", layout);
1093
+ renderAxis(parent, layout.axes.y2, "y", layout, opts);
1113
1094
  }
1114
1095
  }
1115
1096
 
@@ -1122,6 +1103,7 @@ import {
1122
1103
  FOOTNOTE_LINE_HEIGHT,
1123
1104
  footnoteBandHeight as footnoteBandHeightFor,
1124
1105
  textAscent as textAscent2,
1106
+ truncateToWidth,
1125
1107
  wrapText
1126
1108
  } from "@opendata-ai/openchart-core";
1127
1109
  function footnoteBandHeight(layout) {
@@ -1141,8 +1123,25 @@ function renderChromeElement(parent, element, className, chromeKey, measureText,
1141
1123
  element.maxWidth,
1142
1124
  measureText
1143
1125
  );
1144
- if (lines.length === 1) {
1126
+ let truncated = false;
1127
+ if (element.maxLines != null && lines.length > element.maxLines) {
1128
+ truncated = true;
1129
+ lines.length = element.maxLines;
1130
+ const lastIndex = element.maxLines - 1;
1131
+ const measure = measureText ? (text2, fontSize, fontWeight) => measureText(text2, fontSize, fontWeight).width : void 0;
1132
+ const withEllipsis = `${lines[lastIndex]}\u2026`;
1133
+ lines[lastIndex] = truncateToWidth(
1134
+ withEllipsis,
1135
+ element.maxWidth,
1136
+ element.style.fontSize,
1137
+ element.style.fontWeight,
1138
+ measure
1139
+ );
1140
+ }
1141
+ if (lines.length === 1 && !truncated) {
1145
1142
  text.textContent = renderedText;
1143
+ } else if (lines.length === 1) {
1144
+ text.textContent = lines[0];
1146
1145
  } else {
1147
1146
  const lineHeight = element.style.fontSize * (element.style.lineHeight ?? 1.3);
1148
1147
  for (let i = 0; i < lines.length; i++) {
@@ -1532,7 +1531,7 @@ function renderLegend(parent, legend) {
1532
1531
  g.setAttribute("class", "oc-legend");
1533
1532
  g.setAttribute("role", "list");
1534
1533
  g.setAttribute("aria-label", "Chart legend");
1535
- const isHorizontal = legend.position === "top" || legend.position === "bottom";
1534
+ const isHorizontal = legend.position === "top" || legend.position === "top-left" || legend.position === "bottom";
1536
1535
  const positions = "entryPositions" in legend ? legend.entryPositions : void 0;
1537
1536
  let offsetX = legend.bounds.x;
1538
1537
  let offsetY = legend.bounds.y;
@@ -1735,7 +1734,8 @@ function renderChartSVG(layout, container, opts) {
1735
1734
  stackSegmentDuration
1736
1735
  });
1737
1736
  }
1738
- if (layout.display !== "sparkline") {
1737
+ const canvasMarks = opts?.canvasMarks === true;
1738
+ if (layout.display !== "sparkline" && !canvasMarks) {
1739
1739
  const bg = createSVGElement("rect");
1740
1740
  setAttrs(bg, {
1741
1741
  x: 0,
@@ -1775,13 +1775,13 @@ function renderChartSVG(layout, container, opts) {
1775
1775
  if (layout.facet) {
1776
1776
  renderFacetedPanels(svg, layout, layout.facet.panels, defs);
1777
1777
  } else {
1778
- renderAxes(svg, layout);
1778
+ renderAxes(svg, layout, { skipGridlines: canvasMarks });
1779
1779
  const clipGroup = createSVGElement("g");
1780
1780
  clipGroup.setAttribute("clip-path", `url(#${clipId})`);
1781
1781
  const clippedGroup = createSVGElement("g");
1782
1782
  clippedGroup.setAttribute("data-oc-marks-group", "true");
1783
1783
  clipGroup.appendChild(clippedGroup);
1784
- const markLabelsOverlay = renderMarks(clippedGroup, layout);
1784
+ const markLabelsOverlay = renderMarks(clippedGroup, layout, { skipPoints: canvasMarks });
1785
1785
  const hasLineOrAreaWithDataPoints = layout.marks.some(
1786
1786
  (m) => (m.type === "line" || m.type === "area") && m.dataPoints && m.dataPoints.length > 0
1787
1787
  );
@@ -1859,6 +1859,13 @@ function renderChartSVG(layout, container, opts) {
1859
1859
  } finally {
1860
1860
  resetMarkRenderState();
1861
1861
  }
1862
+ if (canvasMarks) {
1863
+ svg.style.pointerEvents = "none";
1864
+ for (const g of svg.querySelectorAll(".oc-legend, .oc-annotations, .oc-chrome, .oc-metrics")) {
1865
+ g.style.pointerEvents = "auto";
1866
+ }
1867
+ svg.style.position = "relative";
1868
+ }
1862
1869
  container.appendChild(svg);
1863
1870
  return svg;
1864
1871
  }
@@ -1881,7 +1888,7 @@ function renderFacetedPanels(svg, layout, panels, defs) {
1881
1888
  setAttrs(headerText, {
1882
1889
  x: panel.header.x,
1883
1890
  y: panel.header.y,
1884
- "text-anchor": "middle",
1891
+ "text-anchor": panel.header.textAnchor ?? "middle",
1885
1892
  "font-family": layout.theme.fonts.family,
1886
1893
  "font-size": panel.header.fontSize,
1887
1894
  "font-weight": panel.header.fontWeight,
@@ -1923,18 +1930,15 @@ function renderFacetedPanels(svg, layout, panels, defs) {
1923
1930
 
1924
1931
  export {
1925
1932
  stampAnimationVars,
1926
- SVG_NS,
1927
- createSVGElement,
1928
- setAttrs,
1929
- renderChromeElement,
1930
1933
  nextSvgId,
1931
1934
  resetSvgIdCounter,
1932
- renderLegend,
1933
1935
  buildGradientDefs,
1934
1936
  resolveMarkFill,
1937
+ renderChromeElement,
1938
+ renderLegend,
1935
1939
  registerMarkRenderer,
1936
1940
  rectPathWithCorners,
1937
1941
  renderSingleMark,
1938
1942
  renderChartSVG
1939
1943
  };
1940
- //# sourceMappingURL=chunk-5EOV5JQZ.js.map
1944
+ //# sourceMappingURL=chunk-EKAUQAAX.js.map