@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.
- package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
- package/dist/chunk-EKAUQAAX.js.map +1 -0
- package/dist/{chunk-ENXSUXC5.js → chunk-M43GGK36.js} +7275 -5482
- package/dist/chunk-M43GGK36.js.map +1 -0
- package/dist/chunk-RM5XS6NR.js +113 -0
- package/dist/chunk-RM5XS6NR.js.map +1 -0
- package/dist/chunk-T32UHMZA.js +282 -0
- package/dist/chunk-T32UHMZA.js.map +1 -0
- package/dist/export-gif.d.ts +113 -0
- package/dist/export-gif.js +21 -0
- package/dist/export-gif.js.map +1 -0
- package/dist/index.d.ts +283 -24
- package/dist/index.js +2012 -763
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
- package/dist/simulation-worker.js +83 -2
- package/dist/static.js +7 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/story/index.js +5 -3
- package/dist/story/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +14 -6
- package/dist/chunk-5EOV5JQZ.js.map +0 -1
- package/dist/chunk-ENXSUXC5.js.map +0 -1
- package/src/__test-fixtures__/dom.ts +0 -70
- package/src/__test-fixtures__/specs.ts +0 -214
- package/src/__tests__/animation.test.ts +0 -358
- package/src/__tests__/annotations-render.test.ts +0 -201
- package/src/__tests__/compound-labels.test.ts +0 -122
- package/src/__tests__/continuous-legend.test.ts +0 -91
- package/src/__tests__/crosshair.test.ts +0 -130
- package/src/__tests__/edit-events.test.ts +0 -902
- package/src/__tests__/events.test.ts +0 -387
- package/src/__tests__/export.test.ts +0 -241
- package/src/__tests__/font-race.test.ts +0 -199
- package/src/__tests__/gradient-ids.test.ts +0 -159
- package/src/__tests__/gradient-utils.test.ts +0 -125
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -293
- package/src/__tests__/mount.test.ts +0 -357
- package/src/__tests__/pattern-fills.test.ts +0 -105
- package/src/__tests__/resize-timing.test.ts +0 -218
- package/src/__tests__/sankey.test.ts +0 -133
- package/src/__tests__/selection-events.test.ts +0 -869
- package/src/__tests__/series-search.test.ts +0 -311
- package/src/__tests__/static.test.ts +0 -141
- package/src/__tests__/svg-renderer.test.ts +0 -895
- package/src/__tests__/table-keyboard.test.ts +0 -361
- package/src/__tests__/table-mount.test.ts +0 -639
- package/src/__tests__/tilemap.test.ts +0 -158
- package/src/__tests__/tooltip.test.ts +0 -337
- package/src/__tests__/transition.test.ts +0 -1777
- package/src/__tests__/you-draw-it.test.ts +0 -273
- package/src/animation-vars.ts +0 -42
- package/src/animation.ts +0 -76
- package/src/barlist-mount.ts +0 -383
- package/src/barlist-renderer.ts +0 -267
- package/src/dom-helpers.ts +0 -17
- package/src/export.ts +0 -482
- package/src/gradient-utils.ts +0 -138
- package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
- package/src/graph/__tests__/graph-mount.test.ts +0 -213
- package/src/graph/__tests__/interaction.test.ts +0 -205
- package/src/graph/__tests__/keyboard.test.ts +0 -653
- package/src/graph/__tests__/search.test.ts +0 -88
- package/src/graph/__tests__/simulation.test.ts +0 -233
- package/src/graph/__tests__/spatial-index.test.ts +0 -142
- package/src/graph/__tests__/zoom.test.ts +0 -203
- package/src/graph/canvas-renderer.ts +0 -747
- package/src/graph/interaction.ts +0 -367
- package/src/graph/keyboard.ts +0 -208
- package/src/graph/search.ts +0 -50
- package/src/graph/simulation-worker-url.ts +0 -12
- package/src/graph/simulation-worker.ts +0 -278
- package/src/graph/simulation.ts +0 -422
- package/src/graph/spatial-index.ts +0 -121
- package/src/graph/types.ts +0 -47
- package/src/graph/worker-protocol.ts +0 -70
- package/src/graph/zoom.ts +0 -134
- package/src/graph-mount.ts +0 -888
- package/src/index.ts +0 -76
- package/src/interactions/chart-events.ts +0 -139
- package/src/interactions/crosshair.ts +0 -233
- package/src/interactions/drag-handler.ts +0 -175
- package/src/interactions/editing-drags.ts +0 -515
- package/src/interactions/index.ts +0 -25
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -280
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -88
- package/src/map-mount.ts +0 -701
- package/src/map-renderer.ts +0 -338
- package/src/map-transition.ts +0 -83
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1382
- package/src/pattern-utils.ts +0 -110
- package/src/renderers/annotations.ts +0 -388
- package/src/renderers/axes.ts +0 -351
- package/src/renderers/brand.ts +0 -87
- package/src/renderers/chrome.ts +0 -196
- package/src/renderers/endpoint-labels.ts +0 -150
- package/src/renderers/legend.ts +0 -311
- package/src/renderers/marks.ts +0 -561
- package/src/renderers/metrics.ts +0 -63
- package/src/renderers/svg-dom.ts +0 -40
- package/src/renderers/table-cells.ts +0 -446
- package/src/resize-observer.ts +0 -46
- package/src/sankey-mount.ts +0 -622
- package/src/sankey-renderer.ts +0 -614
- package/src/series-search.ts +0 -392
- package/src/static.ts +0 -251
- package/src/story/__tests__/camera-math.test.ts +0 -145
- package/src/story/__tests__/create-chart-story.test.ts +0 -317
- package/src/story/__tests__/progress-math.test.ts +0 -95
- package/src/story/__tests__/scroll-driver.test.ts +0 -130
- package/src/story/__tests__/tween.test.ts +0 -159
- package/src/story/camera-math.ts +0 -125
- package/src/story/create-chart-story.ts +0 -353
- package/src/story/crossfade.ts +0 -70
- package/src/story/index.ts +0 -39
- package/src/story/progress-math.ts +0 -85
- package/src/story/resolve-camera-target.ts +0 -119
- package/src/story/scroll-driver.ts +0 -206
- package/src/story/story-camera.ts +0 -49
- package/src/story/tween.ts +0 -112
- package/src/story/types.ts +0 -76
- package/src/svg-ids.ts +0 -23
- package/src/svg-renderer.ts +0 -383
- package/src/table-keyboard.ts +0 -266
- package/src/table-mount.ts +0 -567
- package/src/table-renderer.ts +0 -414
- package/src/text-edit-overlay.ts +0 -255
- package/src/theme-tokens.ts +0 -51
- package/src/tilemap-mount.ts +0 -511
- package/src/tilemap-renderer.ts +0 -396
- package/src/tooltip.ts +0 -177
- package/src/transition.ts +0 -2816
- package/src/you-draw-it.ts +0 -480
- /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-
|
|
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-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1944
|
+
//# sourceMappingURL=chunk-EKAUQAAX.js.map
|