@opendata-ai/openchart-vanilla 7.11.0 → 8.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-EQ6DYFO7.js → chunk-5EOV5JQZ.js} +431 -538
- package/dist/chunk-5EOV5JQZ.js.map +1 -0
- package/dist/chunk-ENXSUXC5.js +6513 -0
- package/dist/chunk-ENXSUXC5.js.map +1 -0
- package/dist/chunk-W34KN7FD.js +298 -0
- package/dist/chunk-W34KN7FD.js.map +1 -0
- package/dist/index.d.ts +110 -116
- package/dist/index.js +189 -4408
- package/dist/index.js.map +1 -1
- package/dist/mount-CUKQLeqD.d.ts +207 -0
- package/dist/static.js +16 -11
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +277 -0
- package/dist/story/index.js +537 -0
- package/dist/story/index.js.map +1 -0
- package/dist/styles.css +2 -1
- package/dist/styles.css.map +1 -0
- package/package.json +9 -3
- package/src/__test-fixtures__/specs.ts +1 -1
- package/src/__tests__/annotations-render.test.ts +201 -0
- package/src/__tests__/continuous-legend.test.ts +91 -0
- package/src/__tests__/edit-events.test.ts +155 -0
- package/src/__tests__/map.test.ts +293 -0
- package/src/__tests__/mount.test.ts +26 -0
- package/src/__tests__/pattern-fills.test.ts +105 -0
- package/src/__tests__/series-search.test.ts +311 -0
- package/src/__tests__/table-mount.test.ts +34 -0
- package/src/__tests__/tooltip.test.ts +26 -0
- package/src/__tests__/transition.test.ts +196 -11
- package/src/__tests__/you-draw-it.test.ts +273 -0
- package/src/animation-vars.ts +42 -0
- package/src/barlist-renderer.ts +2 -2
- package/src/dom-helpers.ts +17 -0
- package/src/export.ts +33 -16
- package/src/index.ts +7 -0
- package/src/interactions/editing-drags.ts +9 -6
- package/src/interactions/legend-interaction.ts +9 -1
- package/src/interactions/selection.ts +12 -3
- package/src/map-camera.ts +88 -0
- package/src/map-mount.ts +701 -0
- package/src/map-renderer.ts +338 -0
- package/src/map-transition.ts +83 -0
- package/src/mount.ts +212 -26
- package/src/pattern-utils.ts +110 -0
- package/src/renderers/annotations.ts +162 -117
- package/src/renderers/axes.ts +13 -6
- package/src/renderers/brand.ts +7 -2
- package/src/renderers/chrome.ts +32 -10
- package/src/renderers/legend.ts +171 -2
- package/src/renderers/marks.ts +33 -5
- package/src/sankey-renderer.ts +7 -11
- package/src/series-search.ts +392 -0
- package/src/static.ts +13 -8
- package/src/story/__tests__/camera-math.test.ts +145 -0
- package/src/story/__tests__/create-chart-story.test.ts +317 -0
- package/src/story/__tests__/progress-math.test.ts +95 -0
- package/src/story/__tests__/scroll-driver.test.ts +130 -0
- package/src/story/__tests__/tween.test.ts +159 -0
- package/src/story/camera-math.ts +125 -0
- package/src/story/create-chart-story.ts +353 -0
- package/src/story/crossfade.ts +70 -0
- package/src/story/index.ts +39 -0
- package/src/story/progress-math.ts +85 -0
- package/src/story/resolve-camera-target.ts +119 -0
- package/src/story/scroll-driver.ts +206 -0
- package/src/story/story-camera.ts +49 -0
- package/src/story/tween.ts +112 -0
- package/src/story/types.ts +76 -0
- package/src/svg-renderer.ts +60 -29
- package/src/table-mount.ts +12 -3
- package/src/table-renderer.ts +9 -28
- package/src/text-edit-overlay.ts +1 -1
- package/src/theme-tokens.ts +4 -4
- package/src/tilemap-mount.ts +27 -6
- package/src/tilemap-renderer.ts +30 -88
- package/src/tooltip.ts +11 -0
- package/src/transition.ts +376 -33
- package/src/you-draw-it.ts +480 -0
- package/dist/chunk-EQ6DYFO7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,55 +1,35 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
renderTileMapSVG
|
|
3
|
+
} from "./chunk-W34KN7FD.js";
|
|
4
|
+
import {
|
|
5
|
+
cancelAnimations,
|
|
6
|
+
createChart,
|
|
7
|
+
createMap,
|
|
8
|
+
createMeasureText,
|
|
9
|
+
createTextEditOverlay,
|
|
10
|
+
createTooltipManager,
|
|
11
|
+
createYouDrawIt,
|
|
12
|
+
exportCSV,
|
|
13
|
+
exportJPG,
|
|
14
|
+
exportPNG,
|
|
15
|
+
exportSVG,
|
|
16
|
+
exportSVGWithFonts,
|
|
17
|
+
observeResize,
|
|
18
|
+
resolveFontFamily,
|
|
19
|
+
scheduleFontReload,
|
|
20
|
+
setupAnimationCleanup,
|
|
21
|
+
setupTableAnimationCleanup
|
|
22
|
+
} from "./chunk-ENXSUXC5.js";
|
|
23
|
+
import {
|
|
3
24
|
rectPathWithCorners,
|
|
4
25
|
registerMarkRenderer,
|
|
5
26
|
renderChartSVG,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
resolveMarkFill
|
|
9
|
-
} from "./chunk-EQ6DYFO7.js";
|
|
27
|
+
stampAnimationVars
|
|
28
|
+
} from "./chunk-5EOV5JQZ.js";
|
|
10
29
|
|
|
11
30
|
// src/barlist-mount.ts
|
|
12
31
|
import { compileBarList } from "@opendata-ai/openchart-engine";
|
|
13
32
|
|
|
14
|
-
// src/animation.ts
|
|
15
|
-
function cancelAnimations(svg) {
|
|
16
|
-
if (svg) {
|
|
17
|
-
svg.classList.remove("oc-animate");
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function setupAnimationCleanup(svg, onComplete) {
|
|
21
|
-
const style = svg.style;
|
|
22
|
-
const duration = parseFloat(style.getPropertyValue("--oc-animation-duration")) || 600;
|
|
23
|
-
const stagger = parseFloat(style.getPropertyValue("--oc-animation-stagger")) || 0;
|
|
24
|
-
const annotationDelay = parseFloat(style.getPropertyValue("--oc-annotation-delay")) || 200;
|
|
25
|
-
const animatedElements = svg.querySelectorAll("[data-animation-index]").length;
|
|
26
|
-
const totalStagger = stagger * Math.max(0, animatedElements - 1);
|
|
27
|
-
const totalTime = totalStagger + duration + annotationDelay + 500;
|
|
28
|
-
const timer2 = setTimeout(() => {
|
|
29
|
-
svg.classList.remove("oc-animate");
|
|
30
|
-
onComplete?.();
|
|
31
|
-
}, totalTime);
|
|
32
|
-
return () => {
|
|
33
|
-
clearTimeout(timer2);
|
|
34
|
-
cancelAnimations(svg);
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function setupTableAnimationCleanup(wrapper) {
|
|
38
|
-
const style = wrapper.style;
|
|
39
|
-
const duration = parseFloat(style.getPropertyValue("--oc-animation-duration")) || 500;
|
|
40
|
-
const stagger = parseFloat(style.getPropertyValue("--oc-animation-stagger")) || 0;
|
|
41
|
-
const rows = wrapper.querySelectorAll("tbody tr").length;
|
|
42
|
-
const totalStagger = stagger * Math.max(0, rows - 1);
|
|
43
|
-
const totalTime = totalStagger + duration + 300;
|
|
44
|
-
const timer2 = setTimeout(() => {
|
|
45
|
-
wrapper.classList.remove("oc-animate");
|
|
46
|
-
}, totalTime);
|
|
47
|
-
return () => {
|
|
48
|
-
clearTimeout(timer2);
|
|
49
|
-
wrapper.classList.remove("oc-animate");
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
33
|
// src/barlist-renderer.ts
|
|
54
34
|
import { textAscent } from "@opendata-ai/openchart-core";
|
|
55
35
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
@@ -231,8 +211,7 @@ function renderBarListSVG(layout, opts) {
|
|
|
231
211
|
const classes = animate ? "oc-barlist oc-animate" : "oc-barlist";
|
|
232
212
|
svg.setAttribute("class", classes);
|
|
233
213
|
if (animate && animation?.enter) {
|
|
234
|
-
svg
|
|
235
|
-
svg.style.setProperty("--oc-animation-stagger", "40ms");
|
|
214
|
+
stampAnimationVars(svg, { duration: animation.enter.duration, stagger: 40 });
|
|
236
215
|
}
|
|
237
216
|
renderChrome(svg, layout);
|
|
238
217
|
renderRows(svg, rows, animate ? animation : void 0);
|
|
@@ -242,435 +221,6 @@ function renderBarListSVG(layout, opts) {
|
|
|
242
221
|
return svg;
|
|
243
222
|
}
|
|
244
223
|
|
|
245
|
-
// src/export.ts
|
|
246
|
-
function getSVGDimensions(svg) {
|
|
247
|
-
const w = parseFloat(svg.getAttribute("width") || "");
|
|
248
|
-
const h = parseFloat(svg.getAttribute("height") || "");
|
|
249
|
-
if (w && h) return { width: w, height: h };
|
|
250
|
-
const vb = svg.getAttribute("viewBox");
|
|
251
|
-
if (vb) {
|
|
252
|
-
const parts = vb.split(/[\s,]+/).map(Number);
|
|
253
|
-
if (parts.length >= 4 && parts[2] && parts[3]) {
|
|
254
|
-
return { width: parts[2], height: parts[3] };
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return { width: 600, height: 400 };
|
|
258
|
-
}
|
|
259
|
-
function ensureSVGDimensions(svgString, width, height) {
|
|
260
|
-
if (/^<svg[^>]*\swidth\s*=/.test(svgString)) return svgString;
|
|
261
|
-
return svgString.replace(/^(<svg)/, `$1 width="${width}" height="${height}"`);
|
|
262
|
-
}
|
|
263
|
-
function collectUsedFonts(svgElement) {
|
|
264
|
-
const fonts = /* @__PURE__ */ new Map();
|
|
265
|
-
const textElements = svgElement.querySelectorAll("text");
|
|
266
|
-
for (const el of textElements) {
|
|
267
|
-
const family = el.getAttribute("font-family");
|
|
268
|
-
const weight = el.getAttribute("font-weight") || "400";
|
|
269
|
-
if (family) {
|
|
270
|
-
const primary = family.split(",")[0].trim().replace(/["']/g, "");
|
|
271
|
-
if (!fonts.has(primary)) fonts.set(primary, /* @__PURE__ */ new Set());
|
|
272
|
-
fonts.get(primary).add(String(weight));
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return fonts;
|
|
276
|
-
}
|
|
277
|
-
function findFontFaceRules(usedFonts) {
|
|
278
|
-
const results = [];
|
|
279
|
-
try {
|
|
280
|
-
for (const sheet of document.styleSheets) {
|
|
281
|
-
let rules;
|
|
282
|
-
try {
|
|
283
|
-
rules = sheet.cssRules;
|
|
284
|
-
} catch {
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
for (const rule of rules) {
|
|
288
|
-
if (!(rule instanceof CSSFontFaceRule)) continue;
|
|
289
|
-
const familyRaw = rule.style.getPropertyValue("font-family").replace(/["']/g, "").trim();
|
|
290
|
-
const weight = rule.style.getPropertyValue("font-weight") || "400";
|
|
291
|
-
const style = rule.style.getPropertyValue("font-style") || "normal";
|
|
292
|
-
const src = rule.style.getPropertyValue("src");
|
|
293
|
-
const weights = usedFonts.get(familyRaw);
|
|
294
|
-
if (!weights) continue;
|
|
295
|
-
const weightMatch = weights.has(weight) || weight.includes(" ");
|
|
296
|
-
if (!weightMatch) continue;
|
|
297
|
-
const woff2Match = src.match(/url\(["']?([^"')]+\.woff2[^"')]*?)["']?\)/);
|
|
298
|
-
if (woff2Match) {
|
|
299
|
-
results.push({
|
|
300
|
-
family: familyRaw,
|
|
301
|
-
weight,
|
|
302
|
-
style,
|
|
303
|
-
url: woff2Match[1],
|
|
304
|
-
format: "woff2"
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
}
|
|
311
|
-
return results;
|
|
312
|
-
}
|
|
313
|
-
async function fetchFontsAsBase64(fontRules) {
|
|
314
|
-
const results = [];
|
|
315
|
-
const fetches = fontRules.map(async (rule) => {
|
|
316
|
-
try {
|
|
317
|
-
const response = await fetch(rule.url);
|
|
318
|
-
if (!response.ok) return;
|
|
319
|
-
const buffer = await response.arrayBuffer();
|
|
320
|
-
const base64 = arrayBufferToBase64(buffer);
|
|
321
|
-
results.push({
|
|
322
|
-
family: rule.family,
|
|
323
|
-
weight: rule.weight,
|
|
324
|
-
style: rule.style,
|
|
325
|
-
base64,
|
|
326
|
-
format: rule.format
|
|
327
|
-
});
|
|
328
|
-
} catch {
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
await Promise.all(fetches);
|
|
332
|
-
return results;
|
|
333
|
-
}
|
|
334
|
-
function arrayBufferToBase64(buffer) {
|
|
335
|
-
let binary = "";
|
|
336
|
-
const bytes = new Uint8Array(buffer);
|
|
337
|
-
for (let i = 0; i < bytes.byteLength; i++) {
|
|
338
|
-
binary += String.fromCharCode(bytes[i]);
|
|
339
|
-
}
|
|
340
|
-
return btoa(binary);
|
|
341
|
-
}
|
|
342
|
-
function injectFontsIntoSVG(svgElement, fonts) {
|
|
343
|
-
if (fonts.length === 0) return;
|
|
344
|
-
const cssRules = fonts.map(
|
|
345
|
-
(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}'); }`
|
|
346
|
-
).join("\n");
|
|
347
|
-
const ns = "http://www.w3.org/2000/svg";
|
|
348
|
-
let defs = svgElement.querySelector("defs");
|
|
349
|
-
if (!defs) {
|
|
350
|
-
defs = document.createElementNS(ns, "defs");
|
|
351
|
-
svgElement.insertBefore(defs, svgElement.firstChild);
|
|
352
|
-
}
|
|
353
|
-
const styleEl = document.createElementNS(ns, "style");
|
|
354
|
-
styleEl.textContent = cssRules;
|
|
355
|
-
defs.insertBefore(styleEl, defs.firstChild);
|
|
356
|
-
}
|
|
357
|
-
async function embedFonts(svgElement) {
|
|
358
|
-
const usedFonts = collectUsedFonts(svgElement);
|
|
359
|
-
if (usedFonts.size === 0) return;
|
|
360
|
-
const fontRules = findFontFaceRules(usedFonts);
|
|
361
|
-
if (fontRules.length === 0) return;
|
|
362
|
-
const fontData = await fetchFontsAsBase64(fontRules);
|
|
363
|
-
injectFontsIntoSVG(svgElement, fontData);
|
|
364
|
-
}
|
|
365
|
-
function getSVGBackgroundColor(svgElement) {
|
|
366
|
-
const firstRect = svgElement.querySelector("rect");
|
|
367
|
-
return firstRect?.getAttribute("fill") || "#ffffff";
|
|
368
|
-
}
|
|
369
|
-
function exportSVG(svgElement) {
|
|
370
|
-
const serializer = new XMLSerializer();
|
|
371
|
-
return serializer.serializeToString(svgElement);
|
|
372
|
-
}
|
|
373
|
-
async function exportSVGWithFonts(svgElement, options) {
|
|
374
|
-
const shouldEmbed = options?.embedFonts ?? true;
|
|
375
|
-
if (shouldEmbed) {
|
|
376
|
-
await embedFonts(svgElement);
|
|
377
|
-
}
|
|
378
|
-
return exportSVG(svgElement);
|
|
379
|
-
}
|
|
380
|
-
async function exportPNG(svgElement, options) {
|
|
381
|
-
const dpi = options?.dpi ?? 2;
|
|
382
|
-
const shouldEmbed = options?.embedFonts ?? true;
|
|
383
|
-
if (shouldEmbed) {
|
|
384
|
-
await embedFonts(svgElement);
|
|
385
|
-
}
|
|
386
|
-
const { width, height } = getSVGDimensions(svgElement);
|
|
387
|
-
const svgString = ensureSVGDimensions(exportSVG(svgElement), width, height);
|
|
388
|
-
const canvas = document.createElement("canvas");
|
|
389
|
-
canvas.width = width * dpi;
|
|
390
|
-
canvas.height = height * dpi;
|
|
391
|
-
const ctx = canvas.getContext("2d");
|
|
392
|
-
if (!ctx) {
|
|
393
|
-
throw new Error("Canvas 2D context not available");
|
|
394
|
-
}
|
|
395
|
-
ctx.scale(dpi, dpi);
|
|
396
|
-
const img = new Image();
|
|
397
|
-
const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
|
|
398
|
-
const url = URL.createObjectURL(blob);
|
|
399
|
-
return new Promise((resolve, reject) => {
|
|
400
|
-
img.onload = () => {
|
|
401
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
402
|
-
URL.revokeObjectURL(url);
|
|
403
|
-
canvas.toBlob((result) => {
|
|
404
|
-
if (result) {
|
|
405
|
-
resolve(result);
|
|
406
|
-
} else {
|
|
407
|
-
reject(new Error("Canvas toBlob returned null"));
|
|
408
|
-
}
|
|
409
|
-
}, "image/png");
|
|
410
|
-
};
|
|
411
|
-
img.onerror = () => {
|
|
412
|
-
URL.revokeObjectURL(url);
|
|
413
|
-
reject(new Error("Failed to load SVG as image"));
|
|
414
|
-
};
|
|
415
|
-
img.src = url;
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
async function exportJPG(svgElement, options) {
|
|
419
|
-
const dpi = options?.dpi ?? 2;
|
|
420
|
-
const quality = options?.quality ?? 0.92;
|
|
421
|
-
const shouldEmbed = options?.embedFonts ?? true;
|
|
422
|
-
if (shouldEmbed) {
|
|
423
|
-
await embedFonts(svgElement);
|
|
424
|
-
}
|
|
425
|
-
const { width, height } = getSVGDimensions(svgElement);
|
|
426
|
-
const svgString = ensureSVGDimensions(exportSVG(svgElement), width, height);
|
|
427
|
-
const canvas = document.createElement("canvas");
|
|
428
|
-
canvas.width = width * dpi;
|
|
429
|
-
canvas.height = height * dpi;
|
|
430
|
-
const ctx = canvas.getContext("2d");
|
|
431
|
-
if (!ctx) {
|
|
432
|
-
throw new Error("Canvas 2D context not available");
|
|
433
|
-
}
|
|
434
|
-
ctx.fillStyle = getSVGBackgroundColor(svgElement);
|
|
435
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
436
|
-
ctx.scale(dpi, dpi);
|
|
437
|
-
const img = new Image();
|
|
438
|
-
const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
|
|
439
|
-
const url = URL.createObjectURL(blob);
|
|
440
|
-
return new Promise((resolve, reject) => {
|
|
441
|
-
img.onload = () => {
|
|
442
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
443
|
-
URL.revokeObjectURL(url);
|
|
444
|
-
canvas.toBlob(
|
|
445
|
-
(result) => {
|
|
446
|
-
if (result) {
|
|
447
|
-
resolve(result);
|
|
448
|
-
} else {
|
|
449
|
-
reject(new Error("Canvas toBlob returned null"));
|
|
450
|
-
}
|
|
451
|
-
},
|
|
452
|
-
"image/jpeg",
|
|
453
|
-
quality
|
|
454
|
-
);
|
|
455
|
-
};
|
|
456
|
-
img.onerror = () => {
|
|
457
|
-
URL.revokeObjectURL(url);
|
|
458
|
-
reject(new Error("Failed to load SVG as image"));
|
|
459
|
-
};
|
|
460
|
-
img.src = url;
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
function exportCSV(data) {
|
|
464
|
-
if (data.length === 0) return "";
|
|
465
|
-
const headers = Object.keys(data[0]);
|
|
466
|
-
const rows = [headers.map(csvEscape).join(",")];
|
|
467
|
-
for (const row of data) {
|
|
468
|
-
const values = headers.map((h) => csvEscape(String(row[h] ?? "")));
|
|
469
|
-
rows.push(values.join(","));
|
|
470
|
-
}
|
|
471
|
-
return rows.join("\n");
|
|
472
|
-
}
|
|
473
|
-
function csvEscape(value) {
|
|
474
|
-
if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
|
|
475
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
476
|
-
}
|
|
477
|
-
return value;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// src/measure-text.ts
|
|
481
|
-
import { estimateTextWidth } from "@opendata-ai/openchart-core";
|
|
482
|
-
var DEFAULT_FONT_FAMILY = "Inter, sans-serif";
|
|
483
|
-
function createMeasureText(fontFamily = DEFAULT_FONT_FAMILY) {
|
|
484
|
-
let canvas = null;
|
|
485
|
-
let ctx = null;
|
|
486
|
-
return (text, fontSize, fontWeight) => {
|
|
487
|
-
if (!canvas) {
|
|
488
|
-
canvas = document.createElement("canvas");
|
|
489
|
-
ctx = canvas.getContext("2d");
|
|
490
|
-
}
|
|
491
|
-
if (!ctx) {
|
|
492
|
-
return {
|
|
493
|
-
width: estimateTextWidth(text, fontSize, fontWeight ?? 400),
|
|
494
|
-
height: fontSize * 1.2
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
const weight = fontWeight ?? 400;
|
|
498
|
-
ctx.font = `${weight} ${fontSize}px ${fontFamily}`;
|
|
499
|
-
const metrics = ctx.measureText(text);
|
|
500
|
-
return {
|
|
501
|
-
width: metrics.width,
|
|
502
|
-
height: fontSize * 1.2
|
|
503
|
-
};
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
function resolveFontFamily(container) {
|
|
507
|
-
if (typeof window === "undefined" || typeof getComputedStyle !== "function") {
|
|
508
|
-
return DEFAULT_FONT_FAMILY;
|
|
509
|
-
}
|
|
510
|
-
try {
|
|
511
|
-
const style = getComputedStyle(container);
|
|
512
|
-
const custom = style.getPropertyValue("--oc-font-family").trim();
|
|
513
|
-
if (custom) return custom;
|
|
514
|
-
const computed = style.fontFamily?.trim();
|
|
515
|
-
if (computed) return computed;
|
|
516
|
-
} catch {
|
|
517
|
-
}
|
|
518
|
-
return DEFAULT_FONT_FAMILY;
|
|
519
|
-
}
|
|
520
|
-
function primaryFontName(fontFamily) {
|
|
521
|
-
const first = fontFamily.split(",")[0]?.trim() ?? "";
|
|
522
|
-
return first.replace(/^["']|["']$/g, "").trim();
|
|
523
|
-
}
|
|
524
|
-
function fontsPending(fontFamily) {
|
|
525
|
-
if (typeof document === "undefined") return false;
|
|
526
|
-
const fonts = document.fonts;
|
|
527
|
-
if (!fonts || typeof fonts.check !== "function") return false;
|
|
528
|
-
const name = primaryFontName(fontFamily);
|
|
529
|
-
if (!name) return false;
|
|
530
|
-
const generics = /* @__PURE__ */ new Set([
|
|
531
|
-
"sans-serif",
|
|
532
|
-
"serif",
|
|
533
|
-
"monospace",
|
|
534
|
-
"system-ui",
|
|
535
|
-
"cursive",
|
|
536
|
-
"fantasy",
|
|
537
|
-
"ui-sans-serif",
|
|
538
|
-
"ui-serif",
|
|
539
|
-
"ui-monospace"
|
|
540
|
-
]);
|
|
541
|
-
if (generics.has(name.toLowerCase())) return false;
|
|
542
|
-
try {
|
|
543
|
-
return !fonts.check(`12px "${name}"`);
|
|
544
|
-
} catch {
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
function scheduleFontReload(fontFamily, isAlive, onReady) {
|
|
549
|
-
if (!fontsPending(fontFamily)) return false;
|
|
550
|
-
const fonts = document.fonts;
|
|
551
|
-
if (!fonts?.ready) return false;
|
|
552
|
-
fonts.ready.then(() => {
|
|
553
|
-
if (isAlive()) onReady();
|
|
554
|
-
});
|
|
555
|
-
return true;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// src/resize-observer.ts
|
|
559
|
-
var DEBOUNCE_MS = 16;
|
|
560
|
-
function observeResize(container, callback) {
|
|
561
|
-
let timeoutId = null;
|
|
562
|
-
const observer = new ResizeObserver((entries) => {
|
|
563
|
-
if (timeoutId !== null) {
|
|
564
|
-
clearTimeout(timeoutId);
|
|
565
|
-
}
|
|
566
|
-
timeoutId = setTimeout(() => {
|
|
567
|
-
for (const entry of entries) {
|
|
568
|
-
const { width, height } = entry.contentRect;
|
|
569
|
-
callback(width, height);
|
|
570
|
-
}
|
|
571
|
-
timeoutId = null;
|
|
572
|
-
}, DEBOUNCE_MS);
|
|
573
|
-
});
|
|
574
|
-
observer.observe(container);
|
|
575
|
-
return () => {
|
|
576
|
-
if (timeoutId !== null) {
|
|
577
|
-
clearTimeout(timeoutId);
|
|
578
|
-
}
|
|
579
|
-
observer.disconnect();
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// src/tooltip.ts
|
|
584
|
-
import { computePosition, flip, offset, shift } from "@floating-ui/dom";
|
|
585
|
-
var TOOLTIP_OFFSET = 12;
|
|
586
|
-
function createTooltipManager(container) {
|
|
587
|
-
const tooltip = document.createElement("div");
|
|
588
|
-
tooltip.className = "oc-tooltip";
|
|
589
|
-
tooltip.setAttribute("role", "tooltip");
|
|
590
|
-
container.style.position = container.style.position || "relative";
|
|
591
|
-
container.appendChild(tooltip);
|
|
592
|
-
let lastContentKey = "";
|
|
593
|
-
let currentPositionId = 0;
|
|
594
|
-
const handleDocumentTouch = (e) => {
|
|
595
|
-
if (!container.contains(e.target)) {
|
|
596
|
-
hide();
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
|
-
document.addEventListener("touchstart", handleDocumentTouch);
|
|
600
|
-
function show(content, x3, y3, opts) {
|
|
601
|
-
const contentKey = `${content.title}|${content.fields.length}|${content.fields[0]?.value}|${content.fields[content.fields.length - 1]?.value}`;
|
|
602
|
-
if (contentKey !== lastContentKey) {
|
|
603
|
-
lastContentKey = contentKey;
|
|
604
|
-
let html = "";
|
|
605
|
-
const colorRowCount = content.fields.filter((f) => f.color).length;
|
|
606
|
-
const perRowSwatches = colorRowCount > 1;
|
|
607
|
-
if (content.title) {
|
|
608
|
-
const titleColor = perRowSwatches ? void 0 : content.fields.find((f) => f.color)?.color;
|
|
609
|
-
html += '<div class="oc-tooltip-header">';
|
|
610
|
-
if (titleColor) {
|
|
611
|
-
html += `<span class="oc-tooltip-dot" style="background:${esc(titleColor)}"></span>`;
|
|
612
|
-
}
|
|
613
|
-
html += `<span class="oc-tooltip-title">${esc(content.title)}</span>`;
|
|
614
|
-
html += "</div>";
|
|
615
|
-
}
|
|
616
|
-
if (content.fields.length > 0) {
|
|
617
|
-
html += '<div class="oc-tooltip-body">';
|
|
618
|
-
for (const field of content.fields) {
|
|
619
|
-
html += '<div class="oc-tooltip-row">';
|
|
620
|
-
html += '<span class="oc-tooltip-label">';
|
|
621
|
-
if (perRowSwatches && field.color) {
|
|
622
|
-
html += `<span class="oc-tooltip-row-swatch" style="background:${esc(field.color)}"></span>`;
|
|
623
|
-
}
|
|
624
|
-
html += `${esc(field.label)}</span>`;
|
|
625
|
-
html += `<span class="oc-tooltip-value">${esc(field.value)}</span>`;
|
|
626
|
-
html += "</div>";
|
|
627
|
-
}
|
|
628
|
-
html += "</div>";
|
|
629
|
-
}
|
|
630
|
-
tooltip.innerHTML = html;
|
|
631
|
-
}
|
|
632
|
-
tooltip.style.display = "block";
|
|
633
|
-
const positionId = ++currentPositionId;
|
|
634
|
-
const virtualRef = {
|
|
635
|
-
getBoundingClientRect() {
|
|
636
|
-
const rect = container.getBoundingClientRect();
|
|
637
|
-
return {
|
|
638
|
-
x: rect.left + x3,
|
|
639
|
-
y: rect.top + y3,
|
|
640
|
-
width: 0,
|
|
641
|
-
height: 0,
|
|
642
|
-
top: rect.top + y3,
|
|
643
|
-
left: rect.left + x3,
|
|
644
|
-
right: rect.left + x3,
|
|
645
|
-
bottom: rect.top + y3
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
};
|
|
649
|
-
computePosition(virtualRef, tooltip, {
|
|
650
|
-
placement: opts?.placement ?? "bottom-start",
|
|
651
|
-
middleware: [offset(TOOLTIP_OFFSET), flip(), shift({ padding: 8 })]
|
|
652
|
-
}).then(({ x: fx, y: fy }) => {
|
|
653
|
-
if (positionId !== currentPositionId) return;
|
|
654
|
-
tooltip.style.left = `${fx}px`;
|
|
655
|
-
tooltip.style.top = `${fy}px`;
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
function hide() {
|
|
659
|
-
tooltip.style.display = "none";
|
|
660
|
-
lastContentKey = "";
|
|
661
|
-
}
|
|
662
|
-
function destroy() {
|
|
663
|
-
document.removeEventListener("touchstart", handleDocumentTouch);
|
|
664
|
-
if (tooltip.parentNode) {
|
|
665
|
-
tooltip.parentNode.removeChild(tooltip);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
return { show, hide, destroy };
|
|
669
|
-
}
|
|
670
|
-
function esc(str) {
|
|
671
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
672
|
-
}
|
|
673
|
-
|
|
674
224
|
// src/barlist-mount.ts
|
|
675
225
|
function resolveDarkMode(mode) {
|
|
676
226
|
if (mode === "force") return true;
|
|
@@ -889,7 +439,7 @@ function createBarList(container, spec, options) {
|
|
|
889
439
|
}
|
|
890
440
|
currentLayout = compile();
|
|
891
441
|
render(true);
|
|
892
|
-
const
|
|
442
|
+
const fontsPending = scheduleFontReload(
|
|
893
443
|
fontFamily,
|
|
894
444
|
() => !destroyed,
|
|
895
445
|
() => {
|
|
@@ -897,7 +447,7 @@ function createBarList(container, spec, options) {
|
|
|
897
447
|
resize();
|
|
898
448
|
}
|
|
899
449
|
);
|
|
900
|
-
container.dataset.ocFontsState =
|
|
450
|
+
container.dataset.ocFontsState = fontsPending ? "pending" : "ready";
|
|
901
451
|
if (options?.responsive !== false) {
|
|
902
452
|
disconnectResize = observeResize(container, () => resize());
|
|
903
453
|
}
|
|
@@ -3839,3887 +3389,110 @@ function escapeHtml(str) {
|
|
|
3839
3389
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3840
3390
|
}
|
|
3841
3391
|
|
|
3842
|
-
// src/
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
// src/interactions/chart-events.ts
|
|
3847
|
-
function buildMarkDataMap(layout) {
|
|
3848
|
-
const map = /* @__PURE__ */ new Map();
|
|
3849
|
-
for (let i = 0; i < layout.marks.length; i++) {
|
|
3850
|
-
const mark = layout.marks[i];
|
|
3851
|
-
switch (mark.type) {
|
|
3852
|
-
case "line":
|
|
3853
|
-
map.set(`line-${mark.seriesKey ?? i}`, {
|
|
3854
|
-
datum: mark.data[0] ?? {},
|
|
3855
|
-
series: mark.seriesKey
|
|
3856
|
-
});
|
|
3857
|
-
break;
|
|
3858
|
-
case "area":
|
|
3859
|
-
map.set(`area-${mark.seriesKey ?? i}`, {
|
|
3860
|
-
datum: mark.data[0] ?? {},
|
|
3861
|
-
series: mark.seriesKey
|
|
3862
|
-
});
|
|
3863
|
-
break;
|
|
3864
|
-
case "rect":
|
|
3865
|
-
map.set(`rect-${i}`, { datum: mark.data });
|
|
3866
|
-
break;
|
|
3867
|
-
case "arc":
|
|
3868
|
-
map.set(`arc-${i}`, { datum: mark.data });
|
|
3869
|
-
break;
|
|
3870
|
-
case "point":
|
|
3871
|
-
map.set(`point-${i}`, { datum: mark.data });
|
|
3872
|
-
break;
|
|
3873
|
-
}
|
|
3392
|
+
// src/renderers/table-cells.ts
|
|
3393
|
+
function applyCellStyle(td, cell) {
|
|
3394
|
+
if (cell.style.backgroundColor) {
|
|
3395
|
+
td.style.background = cell.style.backgroundColor;
|
|
3874
3396
|
}
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
function wireChartEvents(svg, layout, specAnnotations, handlers) {
|
|
3878
|
-
const cleanups = [];
|
|
3879
|
-
const markDataMap = buildMarkDataMap(layout);
|
|
3880
|
-
if (handlers.onMarkClick || handlers.onMarkHover || handlers.onMarkLeave) {
|
|
3881
|
-
const markElements = svg.querySelectorAll("[data-mark-id]");
|
|
3882
|
-
for (const el of markElements) {
|
|
3883
|
-
const markId = el.getAttribute("data-mark-id");
|
|
3884
|
-
if (!markId) continue;
|
|
3885
|
-
const markInfo = markDataMap.get(markId);
|
|
3886
|
-
if (!markInfo) continue;
|
|
3887
|
-
const series = markInfo.series ?? el.getAttribute("data-series") ?? void 0;
|
|
3888
|
-
if (handlers.onMarkClick) {
|
|
3889
|
-
const handleClick = (e) => {
|
|
3890
|
-
const mouseEvent = e;
|
|
3891
|
-
const svgRect = svg.getBoundingClientRect();
|
|
3892
|
-
handlers.onMarkClick({
|
|
3893
|
-
datum: markInfo.datum,
|
|
3894
|
-
series,
|
|
3895
|
-
position: {
|
|
3896
|
-
x: mouseEvent.clientX - svgRect.left,
|
|
3897
|
-
y: mouseEvent.clientY - svgRect.top
|
|
3898
|
-
},
|
|
3899
|
-
event: mouseEvent
|
|
3900
|
-
});
|
|
3901
|
-
};
|
|
3902
|
-
el.addEventListener("click", handleClick);
|
|
3903
|
-
cleanups.push(() => el.removeEventListener("click", handleClick));
|
|
3904
|
-
}
|
|
3905
|
-
if (handlers.onMarkHover) {
|
|
3906
|
-
const handleEnter = (e) => {
|
|
3907
|
-
const mouseEvent = e;
|
|
3908
|
-
const svgRect = svg.getBoundingClientRect();
|
|
3909
|
-
handlers.onMarkHover({
|
|
3910
|
-
datum: markInfo.datum,
|
|
3911
|
-
series,
|
|
3912
|
-
position: {
|
|
3913
|
-
x: mouseEvent.clientX - svgRect.left,
|
|
3914
|
-
y: mouseEvent.clientY - svgRect.top
|
|
3915
|
-
},
|
|
3916
|
-
event: mouseEvent
|
|
3917
|
-
});
|
|
3918
|
-
};
|
|
3919
|
-
el.addEventListener("mouseenter", handleEnter);
|
|
3920
|
-
cleanups.push(() => el.removeEventListener("mouseenter", handleEnter));
|
|
3921
|
-
}
|
|
3922
|
-
if (handlers.onMarkLeave) {
|
|
3923
|
-
const handleLeave = () => {
|
|
3924
|
-
handlers.onMarkLeave();
|
|
3925
|
-
};
|
|
3926
|
-
el.addEventListener("mouseleave", handleLeave);
|
|
3927
|
-
cleanups.push(() => el.removeEventListener("mouseleave", handleLeave));
|
|
3928
|
-
}
|
|
3929
|
-
}
|
|
3397
|
+
if (cell.style.color) {
|
|
3398
|
+
td.style.color = cell.style.color;
|
|
3930
3399
|
}
|
|
3931
|
-
if (
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
handlers.onAnnotationClick(specAnnotation, mouseEvent);
|
|
3940
|
-
};
|
|
3941
|
-
el.addEventListener("click", handleClick);
|
|
3942
|
-
cleanups.push(() => el.removeEventListener("click", handleClick));
|
|
3943
|
-
}
|
|
3400
|
+
if (cell.style.fontWeight) {
|
|
3401
|
+
td.style.fontWeight = String(cell.style.fontWeight);
|
|
3402
|
+
}
|
|
3403
|
+
if (cell.style.fontVariant) {
|
|
3404
|
+
td.style.fontVariant = cell.style.fontVariant;
|
|
3405
|
+
}
|
|
3406
|
+
if (cell.aria) {
|
|
3407
|
+
td.setAttribute("aria-label", cell.aria);
|
|
3944
3408
|
}
|
|
3945
|
-
return () => {
|
|
3946
|
-
for (const cleanup of cleanups) {
|
|
3947
|
-
cleanup();
|
|
3948
|
-
}
|
|
3949
|
-
};
|
|
3950
3409
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
import { getRepresentativeColor } from "@opendata-ai/openchart-core";
|
|
3954
|
-
function snapKey(x3) {
|
|
3955
|
-
return Math.round(x3);
|
|
3410
|
+
function countryToEmoji(code) {
|
|
3411
|
+
return [...code.toUpperCase()].map((c2) => String.fromCodePoint(c2.charCodeAt(0) + 127397)).join("");
|
|
3956
3412
|
}
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3413
|
+
var COUNTRY_NAMES = {
|
|
3414
|
+
US: "United States",
|
|
3415
|
+
CN: "China",
|
|
3416
|
+
IN: "India",
|
|
3417
|
+
ID: "Indonesia",
|
|
3418
|
+
BR: "Brazil",
|
|
3419
|
+
PK: "Pakistan",
|
|
3420
|
+
NG: "Nigeria",
|
|
3421
|
+
BD: "Bangladesh",
|
|
3422
|
+
RU: "Russia",
|
|
3423
|
+
MX: "Mexico",
|
|
3424
|
+
JP: "Japan",
|
|
3425
|
+
DE: "Germany",
|
|
3426
|
+
GB: "United Kingdom",
|
|
3427
|
+
FR: "France",
|
|
3428
|
+
IT: "Italy",
|
|
3429
|
+
CA: "Canada",
|
|
3430
|
+
AU: "Australia",
|
|
3431
|
+
KR: "South Korea",
|
|
3432
|
+
ES: "Spain",
|
|
3433
|
+
AR: "Argentina",
|
|
3434
|
+
CO: "Colombia",
|
|
3435
|
+
ZA: "South Africa",
|
|
3436
|
+
TR: "Turkey",
|
|
3437
|
+
SA: "Saudi Arabia",
|
|
3438
|
+
UA: "Ukraine",
|
|
3439
|
+
PL: "Poland",
|
|
3440
|
+
NL: "Netherlands",
|
|
3441
|
+
SE: "Sweden",
|
|
3442
|
+
NO: "Norway",
|
|
3443
|
+
DK: "Denmark",
|
|
3444
|
+
FI: "Finland",
|
|
3445
|
+
CH: "Switzerland",
|
|
3446
|
+
AT: "Austria",
|
|
3447
|
+
BE: "Belgium",
|
|
3448
|
+
PT: "Portugal",
|
|
3449
|
+
IE: "Ireland",
|
|
3450
|
+
NZ: "New Zealand",
|
|
3451
|
+
SG: "Singapore",
|
|
3452
|
+
IL: "Israel",
|
|
3453
|
+
AE: "United Arab Emirates",
|
|
3454
|
+
EG: "Egypt",
|
|
3455
|
+
TH: "Thailand",
|
|
3456
|
+
VN: "Vietnam",
|
|
3457
|
+
PH: "Philippines",
|
|
3458
|
+
MY: "Malaysia",
|
|
3459
|
+
CL: "Chile",
|
|
3460
|
+
PE: "Peru",
|
|
3461
|
+
CZ: "Czech Republic",
|
|
3462
|
+
GR: "Greece",
|
|
3463
|
+
HU: "Hungary",
|
|
3464
|
+
RO: "Romania",
|
|
3465
|
+
ET: "Ethiopia"
|
|
3466
|
+
};
|
|
3467
|
+
function getCountryName(code) {
|
|
3468
|
+
return COUNTRY_NAMES[code.toUpperCase()] || code.toUpperCase();
|
|
3976
3469
|
}
|
|
3977
|
-
function
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3470
|
+
function describeSparklineTrend(data) {
|
|
3471
|
+
if (data.type === "line" && data.points.length >= 2) {
|
|
3472
|
+
const first = data.points[0].y;
|
|
3473
|
+
const last = data.points[data.points.length - 1].y;
|
|
3474
|
+
const count = data.points.length;
|
|
3475
|
+
if (last > first) return `Sparkline with ${count} points, trending upward`;
|
|
3476
|
+
if (last < first) return `Sparkline with ${count} points, trending downward`;
|
|
3477
|
+
return `Sparkline with ${count} points, roughly flat`;
|
|
3981
3478
|
}
|
|
3982
|
-
|
|
3479
|
+
if ((data.type === "bar" || data.type === "column") && data.bars.length > 0) {
|
|
3480
|
+
return `${data.type === "column" ? "Column" : "Bar"} sparkline with ${data.bars.length} values`;
|
|
3481
|
+
}
|
|
3482
|
+
return "Sparkline";
|
|
3983
3483
|
}
|
|
3984
|
-
function
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
const mid = lo + hi >> 1;
|
|
3990
|
-
if (sortedXs[mid] < x3) lo = mid + 1;
|
|
3991
|
-
else hi = mid;
|
|
3992
|
-
}
|
|
3993
|
-
const candidate = sortedXs[lo];
|
|
3994
|
-
if (lo > 0) {
|
|
3995
|
-
const prev = sortedXs[lo - 1];
|
|
3996
|
-
if (Math.abs(prev - x3) < Math.abs(candidate - x3)) return prev;
|
|
3997
|
-
}
|
|
3998
|
-
return candidate;
|
|
3484
|
+
function renderTextCell(cell) {
|
|
3485
|
+
const td = document.createElement("td");
|
|
3486
|
+
td.textContent = cell.formattedValue;
|
|
3487
|
+
applyCellStyle(td, cell);
|
|
3488
|
+
return td;
|
|
3999
3489
|
}
|
|
4000
|
-
function
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
const tip = point.tooltip;
|
|
4007
|
-
if (!tip) continue;
|
|
4008
|
-
if (isMulti) {
|
|
4009
|
-
const yField = tip.fields.find((f) => !f.color && f.label !== title) ?? tip.fields[tip.fields.length - 1] ?? null;
|
|
4010
|
-
if (!yField) continue;
|
|
4011
|
-
fields.push({
|
|
4012
|
-
label: group.seriesKey,
|
|
4013
|
-
value: yField.value,
|
|
4014
|
-
color: group.color
|
|
4015
|
-
});
|
|
4016
|
-
} else {
|
|
4017
|
-
for (const f of tip.fields) {
|
|
4018
|
-
fields.push({ ...f, color: f.color ?? group.color });
|
|
4019
|
-
}
|
|
4020
|
-
}
|
|
4021
|
-
}
|
|
4022
|
-
if (fields.length === 0) return null;
|
|
4023
|
-
return { title, fields };
|
|
4024
|
-
}
|
|
4025
|
-
function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
|
|
4026
|
-
const overlay = svg.querySelector("[data-voronoi-overlay]");
|
|
4027
|
-
if (!overlay) return () => {
|
|
4028
|
-
};
|
|
4029
|
-
const groups = collectSeriesGroups(layout);
|
|
4030
|
-
if (groups.length === 0) return () => {
|
|
4031
|
-
};
|
|
4032
|
-
const snapXs = collectSnapXs(groups);
|
|
4033
|
-
if (snapXs.length === 0) return () => {
|
|
4034
|
-
};
|
|
4035
|
-
const crosshair = svg.querySelector("[data-crosshair]");
|
|
4036
|
-
const dotsLayer = svg.querySelector("[data-snap-dots]");
|
|
4037
|
-
const dots = [];
|
|
4038
|
-
if (dotsLayer) {
|
|
4039
|
-
while (dotsLayer.firstChild) dotsLayer.removeChild(dotsLayer.firstChild);
|
|
4040
|
-
for (const group of groups) {
|
|
4041
|
-
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
4042
|
-
circle.setAttribute("r", "4");
|
|
4043
|
-
circle.setAttribute("fill", layout.theme.colors.background);
|
|
4044
|
-
circle.setAttribute("stroke", group.color);
|
|
4045
|
-
circle.setAttribute("stroke-width", "2");
|
|
4046
|
-
circle.setAttribute("pointer-events", "none");
|
|
4047
|
-
circle.style.display = "none";
|
|
4048
|
-
dotsLayer.appendChild(circle);
|
|
4049
|
-
dots.push(circle);
|
|
4050
|
-
}
|
|
4051
|
-
}
|
|
4052
|
-
const positionAt = (svgX, _svgY, viewBoxToContainer) => {
|
|
4053
|
-
const snappedX = findNearestX(snapXs, svgX);
|
|
4054
|
-
if (snappedX === null) return false;
|
|
4055
|
-
const hits = [];
|
|
4056
|
-
let anchorY = 0;
|
|
4057
|
-
let anchorCount = 0;
|
|
4058
|
-
for (let i = 0; i < groups.length; i++) {
|
|
4059
|
-
const group = groups[i];
|
|
4060
|
-
const point = group.pointsByX.get(snappedX);
|
|
4061
|
-
const dot = dots[i];
|
|
4062
|
-
if (point) {
|
|
4063
|
-
hits.push({ group, point });
|
|
4064
|
-
anchorY += point.y;
|
|
4065
|
-
anchorCount += 1;
|
|
4066
|
-
if (dot) {
|
|
4067
|
-
dot.setAttribute("cx", String(point.x));
|
|
4068
|
-
dot.setAttribute("cy", String(point.y));
|
|
4069
|
-
dot.style.display = "";
|
|
4070
|
-
}
|
|
4071
|
-
} else if (dot) {
|
|
4072
|
-
dot.style.display = "none";
|
|
4073
|
-
}
|
|
4074
|
-
}
|
|
4075
|
-
if (crosshair) {
|
|
4076
|
-
crosshair.setAttribute("x1", String(snappedX));
|
|
4077
|
-
crosshair.setAttribute("x2", String(snappedX));
|
|
4078
|
-
crosshair.style.display = "";
|
|
4079
|
-
}
|
|
4080
|
-
const tooltip = buildSliceTooltip(hits);
|
|
4081
|
-
if (!tooltip) return false;
|
|
4082
|
-
const containerAnchorX = snappedX * viewBoxToContainer;
|
|
4083
|
-
const containerAnchorY = anchorCount > 0 ? anchorY / anchorCount * viewBoxToContainer : 0;
|
|
4084
|
-
tooltipManager.show(tooltip, containerAnchorX, containerAnchorY, {
|
|
4085
|
-
placement: "right"
|
|
4086
|
-
});
|
|
4087
|
-
return true;
|
|
4088
|
-
};
|
|
4089
|
-
const toSvgCoords = (clientX, clientY) => {
|
|
4090
|
-
const svgEl = svg;
|
|
4091
|
-
const svgRect = svgEl.getBoundingClientRect();
|
|
4092
|
-
const viewBox = svgEl.viewBox?.baseVal;
|
|
4093
|
-
const scaleX = viewBox?.width && svgRect.width ? viewBox.width / svgRect.width : 1;
|
|
4094
|
-
const scaleY = viewBox?.height && svgRect.height ? viewBox.height / svgRect.height : 1;
|
|
4095
|
-
const svgX = (clientX - svgRect.left) * scaleX;
|
|
4096
|
-
const svgY = (clientY - svgRect.top) * scaleY;
|
|
4097
|
-
const viewBoxToContainer = scaleX > 0 ? 1 / scaleX : 1;
|
|
4098
|
-
return { svgX, svgY, viewBoxToContainer };
|
|
4099
|
-
};
|
|
4100
|
-
const handleMouseMove = (e) => {
|
|
4101
|
-
const me = e;
|
|
4102
|
-
const { svgX, svgY, viewBoxToContainer } = toSvgCoords(me.clientX, me.clientY);
|
|
4103
|
-
positionAt(svgX, svgY, viewBoxToContainer);
|
|
4104
|
-
};
|
|
4105
|
-
const hideAll = () => {
|
|
4106
|
-
if (crosshair) crosshair.style.display = "none";
|
|
4107
|
-
for (const dot of dots) dot.style.display = "none";
|
|
4108
|
-
tooltipManager.hide();
|
|
4109
|
-
};
|
|
4110
|
-
const handleTouch = (e) => {
|
|
4111
|
-
const te = e;
|
|
4112
|
-
if (te.touches.length === 0) return;
|
|
4113
|
-
const t = te.touches[0];
|
|
4114
|
-
const { svgX, svgY, viewBoxToContainer } = toSvgCoords(t.clientX, t.clientY);
|
|
4115
|
-
if (te.cancelable) te.preventDefault();
|
|
4116
|
-
positionAt(svgX, svgY, viewBoxToContainer);
|
|
4117
|
-
};
|
|
4118
|
-
overlay.addEventListener("mousemove", handleMouseMove);
|
|
4119
|
-
overlay.addEventListener("mouseleave", hideAll);
|
|
4120
|
-
overlay.addEventListener("touchstart", handleTouch, { passive: false });
|
|
4121
|
-
overlay.addEventListener("touchmove", handleTouch, { passive: false });
|
|
4122
|
-
overlay.addEventListener("touchend", hideAll);
|
|
4123
|
-
return () => {
|
|
4124
|
-
overlay.removeEventListener("mousemove", handleMouseMove);
|
|
4125
|
-
overlay.removeEventListener("mouseleave", hideAll);
|
|
4126
|
-
overlay.removeEventListener("touchstart", handleTouch);
|
|
4127
|
-
overlay.removeEventListener("touchmove", handleTouch);
|
|
4128
|
-
overlay.removeEventListener("touchend", hideAll);
|
|
4129
|
-
};
|
|
4130
|
-
}
|
|
4131
|
-
|
|
4132
|
-
// src/interactions/drag-handler.ts
|
|
4133
|
-
function createDragHandler(config) {
|
|
4134
|
-
const { element, svg, onMove, onEnd, setDragging, threshold = 3 } = config;
|
|
4135
|
-
const cleanups = [];
|
|
4136
|
-
let activeDocMouseMove = null;
|
|
4137
|
-
let activeDocMouseUp = null;
|
|
4138
|
-
let activeDocTouchMove = null;
|
|
4139
|
-
let activeDocTouchEnd = null;
|
|
4140
|
-
let activeDocTouchCancel = null;
|
|
4141
|
-
function getScale2() {
|
|
4142
|
-
const viewBox = svg.viewBox?.baseVal;
|
|
4143
|
-
const svgRect = svg.getBoundingClientRect();
|
|
4144
|
-
return {
|
|
4145
|
-
scaleX: viewBox?.width && svgRect.width ? viewBox.width / svgRect.width : 1,
|
|
4146
|
-
scaleY: viewBox?.height && svgRect.height ? viewBox.height / svgRect.height : 1
|
|
4147
|
-
};
|
|
4148
|
-
}
|
|
4149
|
-
function startDrag(startX, startY) {
|
|
4150
|
-
setDragging(true);
|
|
4151
|
-
const { scaleX, scaleY } = getScale2();
|
|
4152
|
-
element.style.cursor = "grabbing";
|
|
4153
|
-
svg.style.userSelect = "none";
|
|
4154
|
-
const handleMove = (clientX, clientY) => {
|
|
4155
|
-
const dx = (clientX - startX) * scaleX;
|
|
4156
|
-
const dy = (clientY - startY) * scaleY;
|
|
4157
|
-
onMove(dx, dy);
|
|
4158
|
-
};
|
|
4159
|
-
const cleanupDocListeners = () => {
|
|
4160
|
-
if (activeDocMouseMove) {
|
|
4161
|
-
document.removeEventListener("mousemove", activeDocMouseMove);
|
|
4162
|
-
activeDocMouseMove = null;
|
|
4163
|
-
}
|
|
4164
|
-
if (activeDocMouseUp) {
|
|
4165
|
-
document.removeEventListener("mouseup", activeDocMouseUp);
|
|
4166
|
-
activeDocMouseUp = null;
|
|
4167
|
-
}
|
|
4168
|
-
if (activeDocTouchMove) {
|
|
4169
|
-
document.removeEventListener("touchmove", activeDocTouchMove);
|
|
4170
|
-
activeDocTouchMove = null;
|
|
4171
|
-
}
|
|
4172
|
-
if (activeDocTouchEnd) {
|
|
4173
|
-
document.removeEventListener("touchend", activeDocTouchEnd);
|
|
4174
|
-
activeDocTouchEnd = null;
|
|
4175
|
-
}
|
|
4176
|
-
if (activeDocTouchCancel) {
|
|
4177
|
-
document.removeEventListener("touchcancel", activeDocTouchCancel);
|
|
4178
|
-
activeDocTouchCancel = null;
|
|
4179
|
-
}
|
|
4180
|
-
};
|
|
4181
|
-
const handleEnd = (clientX, clientY) => {
|
|
4182
|
-
const dx = (clientX - startX) * scaleX;
|
|
4183
|
-
const dy = (clientY - startY) * scaleY;
|
|
4184
|
-
const moved = Math.abs(dx) > threshold || Math.abs(dy) > threshold;
|
|
4185
|
-
onEnd(dx, dy, moved);
|
|
4186
|
-
if (moved) {
|
|
4187
|
-
element.addEventListener(
|
|
4188
|
-
"click",
|
|
4189
|
-
(clickE) => {
|
|
4190
|
-
clickE.stopPropagation();
|
|
4191
|
-
},
|
|
4192
|
-
{ capture: true, once: true }
|
|
4193
|
-
);
|
|
4194
|
-
}
|
|
4195
|
-
element.style.cursor = "grab";
|
|
4196
|
-
svg.style.userSelect = "";
|
|
4197
|
-
cleanupDocListeners();
|
|
4198
|
-
setDragging(false);
|
|
4199
|
-
};
|
|
4200
|
-
const onMouseMove = (moveEvent) => {
|
|
4201
|
-
handleMove(moveEvent.clientX, moveEvent.clientY);
|
|
4202
|
-
};
|
|
4203
|
-
const onMouseUp = (upEvent) => {
|
|
4204
|
-
handleEnd(upEvent.clientX, upEvent.clientY);
|
|
4205
|
-
};
|
|
4206
|
-
document.addEventListener("mousemove", onMouseMove);
|
|
4207
|
-
document.addEventListener("mouseup", onMouseUp);
|
|
4208
|
-
activeDocMouseMove = onMouseMove;
|
|
4209
|
-
activeDocMouseUp = onMouseUp;
|
|
4210
|
-
const onTouchMove = (moveEvent) => {
|
|
4211
|
-
if (moveEvent.touches.length > 0) {
|
|
4212
|
-
moveEvent.preventDefault();
|
|
4213
|
-
handleMove(moveEvent.touches[0].clientX, moveEvent.touches[0].clientY);
|
|
4214
|
-
}
|
|
4215
|
-
};
|
|
4216
|
-
const onTouchEnd = (endEvent) => {
|
|
4217
|
-
const touch = endEvent.changedTouches[0];
|
|
4218
|
-
if (touch) {
|
|
4219
|
-
handleEnd(touch.clientX, touch.clientY);
|
|
4220
|
-
} else {
|
|
4221
|
-
handleEnd(startX, startY);
|
|
4222
|
-
}
|
|
4223
|
-
};
|
|
4224
|
-
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
4225
|
-
document.addEventListener("touchend", onTouchEnd);
|
|
4226
|
-
document.addEventListener("touchcancel", onTouchEnd);
|
|
4227
|
-
activeDocTouchMove = onTouchMove;
|
|
4228
|
-
activeDocTouchEnd = onTouchEnd;
|
|
4229
|
-
activeDocTouchCancel = onTouchEnd;
|
|
4230
|
-
}
|
|
4231
|
-
const handleMouseDown = (e) => {
|
|
4232
|
-
const mouseEvent = e;
|
|
4233
|
-
mouseEvent.preventDefault();
|
|
4234
|
-
startDrag(mouseEvent.clientX, mouseEvent.clientY);
|
|
4235
|
-
};
|
|
4236
|
-
const handleTouchStart = (e) => {
|
|
4237
|
-
const touchEvent = e;
|
|
4238
|
-
if (touchEvent.touches.length === 1) {
|
|
4239
|
-
touchEvent.preventDefault();
|
|
4240
|
-
startDrag(touchEvent.touches[0].clientX, touchEvent.touches[0].clientY);
|
|
4241
|
-
}
|
|
4242
|
-
};
|
|
4243
|
-
element.addEventListener("mousedown", handleMouseDown);
|
|
4244
|
-
element.addEventListener("touchstart", handleTouchStart, { passive: false });
|
|
4245
|
-
cleanups.push(() => {
|
|
4246
|
-
element.removeEventListener("mousedown", handleMouseDown);
|
|
4247
|
-
element.removeEventListener("touchstart", handleTouchStart);
|
|
4248
|
-
});
|
|
4249
|
-
return () => {
|
|
4250
|
-
for (const cleanup of cleanups) {
|
|
4251
|
-
cleanup();
|
|
4252
|
-
}
|
|
4253
|
-
if (activeDocMouseMove) {
|
|
4254
|
-
document.removeEventListener("mousemove", activeDocMouseMove);
|
|
4255
|
-
activeDocMouseMove = null;
|
|
4256
|
-
}
|
|
4257
|
-
if (activeDocMouseUp) {
|
|
4258
|
-
document.removeEventListener("mouseup", activeDocMouseUp);
|
|
4259
|
-
activeDocMouseUp = null;
|
|
4260
|
-
}
|
|
4261
|
-
if (activeDocTouchMove) {
|
|
4262
|
-
document.removeEventListener("touchmove", activeDocTouchMove);
|
|
4263
|
-
activeDocTouchMove = null;
|
|
4264
|
-
}
|
|
4265
|
-
if (activeDocTouchEnd) {
|
|
4266
|
-
document.removeEventListener("touchend", activeDocTouchEnd);
|
|
4267
|
-
activeDocTouchEnd = null;
|
|
4268
|
-
}
|
|
4269
|
-
if (activeDocTouchCancel) {
|
|
4270
|
-
document.removeEventListener("touchcancel", activeDocTouchCancel);
|
|
4271
|
-
activeDocTouchCancel = null;
|
|
4272
|
-
}
|
|
4273
|
-
svg.style.userSelect = "";
|
|
4274
|
-
};
|
|
4275
|
-
}
|
|
4276
|
-
|
|
4277
|
-
// src/interactions/editing-drags.ts
|
|
4278
|
-
function wireAnnotationDrag(svg, specAnnotations, onAnnotationEdit, onEdit, setDragging) {
|
|
4279
|
-
const annotationElements = svg.querySelectorAll(".oc-annotation-text");
|
|
4280
|
-
const cleanups = [];
|
|
4281
|
-
for (const el of annotationElements) {
|
|
4282
|
-
const indexStr = el.getAttribute("data-annotation-index");
|
|
4283
|
-
if (indexStr === null) continue;
|
|
4284
|
-
const index2 = Number(indexStr);
|
|
4285
|
-
const specAnnotation = specAnnotations[index2];
|
|
4286
|
-
if (!specAnnotation || specAnnotation.type !== "text") continue;
|
|
4287
|
-
const textAnnotation = specAnnotation;
|
|
4288
|
-
const annotationG = el;
|
|
4289
|
-
annotationG.style.cursor = "grab";
|
|
4290
|
-
const connectorLine = annotationG.querySelector("line.oc-annotation-connector");
|
|
4291
|
-
const origX2 = connectorLine ? Number(connectorLine.getAttribute("x2")) : 0;
|
|
4292
|
-
const origY2 = connectorLine ? Number(connectorLine.getAttribute("y2")) : 0;
|
|
4293
|
-
const curvedPath = annotationG.querySelector("path.oc-annotation-connector");
|
|
4294
|
-
const arrowhead = annotationG.querySelector("polygon.oc-annotation-connector");
|
|
4295
|
-
const hasCurvedConnector = curvedPath !== null;
|
|
4296
|
-
const origDx = textAnnotation.offset?.dx ?? 0;
|
|
4297
|
-
const origDy = textAnnotation.offset?.dy ?? 0;
|
|
4298
|
-
const cleanup = createDragHandler({
|
|
4299
|
-
element: annotationG,
|
|
4300
|
-
svg,
|
|
4301
|
-
onMove: (dx, dy) => {
|
|
4302
|
-
annotationG.setAttribute("transform", `translate(${dx}, ${dy})`);
|
|
4303
|
-
if (connectorLine && !hasCurvedConnector) {
|
|
4304
|
-
connectorLine.setAttribute("x2", String(origX2 - dx));
|
|
4305
|
-
connectorLine.setAttribute("y2", String(origY2 - dy));
|
|
4306
|
-
}
|
|
4307
|
-
if (hasCurvedConnector) {
|
|
4308
|
-
if (curvedPath) curvedPath.setAttribute("display", "none");
|
|
4309
|
-
if (arrowhead) arrowhead.setAttribute("display", "none");
|
|
4310
|
-
}
|
|
4311
|
-
},
|
|
4312
|
-
onEnd: (dx, dy, moved) => {
|
|
4313
|
-
annotationG.removeAttribute("transform");
|
|
4314
|
-
if (connectorLine && !hasCurvedConnector) {
|
|
4315
|
-
connectorLine.setAttribute("x2", String(origX2));
|
|
4316
|
-
connectorLine.setAttribute("y2", String(origY2));
|
|
4317
|
-
}
|
|
4318
|
-
if (hasCurvedConnector) {
|
|
4319
|
-
if (curvedPath) curvedPath.removeAttribute("display");
|
|
4320
|
-
if (arrowhead) arrowhead.removeAttribute("display");
|
|
4321
|
-
}
|
|
4322
|
-
if (moved) {
|
|
4323
|
-
const newOffset = {
|
|
4324
|
-
dx: origDx + dx,
|
|
4325
|
-
dy: origDy + dy
|
|
4326
|
-
};
|
|
4327
|
-
onAnnotationEdit?.(textAnnotation, newOffset);
|
|
4328
|
-
onEdit?.({ type: "annotation", annotation: textAnnotation, offset: newOffset });
|
|
4329
|
-
}
|
|
4330
|
-
},
|
|
4331
|
-
setDragging
|
|
4332
|
-
});
|
|
4333
|
-
cleanups.push(cleanup);
|
|
4334
|
-
}
|
|
4335
|
-
return () => {
|
|
4336
|
-
for (const cleanup of cleanups) {
|
|
4337
|
-
cleanup();
|
|
4338
|
-
}
|
|
4339
|
-
};
|
|
4340
|
-
}
|
|
4341
|
-
function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
|
|
4342
|
-
const SVG_NS3 = "http://www.w3.org/2000/svg";
|
|
4343
|
-
const cleanups = [];
|
|
4344
|
-
const annotationGroups = svg.querySelectorAll(".oc-annotation-text");
|
|
4345
|
-
for (const el of annotationGroups) {
|
|
4346
|
-
const annotationG = el;
|
|
4347
|
-
const indexStr = annotationG.getAttribute("data-annotation-index");
|
|
4348
|
-
if (indexStr === null) continue;
|
|
4349
|
-
const index2 = Number(indexStr);
|
|
4350
|
-
const specAnnotation = specAnnotations[index2];
|
|
4351
|
-
if (!specAnnotation || specAnnotation.type !== "text") continue;
|
|
4352
|
-
const textAnnotation = specAnnotation;
|
|
4353
|
-
const connectorLine = annotationG.querySelector("line.oc-annotation-connector");
|
|
4354
|
-
const curvedPath = annotationG.querySelector("path.oc-annotation-connector");
|
|
4355
|
-
if (!connectorLine && !curvedPath) continue;
|
|
4356
|
-
let fromX, fromY, toX, toY;
|
|
4357
|
-
if (connectorLine) {
|
|
4358
|
-
fromX = Number(connectorLine.getAttribute("x1")) || 0;
|
|
4359
|
-
fromY = Number(connectorLine.getAttribute("y1")) || 0;
|
|
4360
|
-
toX = Number(connectorLine.getAttribute("x2")) || 0;
|
|
4361
|
-
toY = Number(connectorLine.getAttribute("y2")) || 0;
|
|
4362
|
-
} else {
|
|
4363
|
-
const pathD = curvedPath.getAttribute("d") ?? "";
|
|
4364
|
-
const mMatch = pathD.match(/M\s*([\d.e+-]+)\s+([\d.e+-]+)/);
|
|
4365
|
-
fromX = mMatch ? Number(mMatch[1]) : 0;
|
|
4366
|
-
fromY = mMatch ? Number(mMatch[2]) : 0;
|
|
4367
|
-
const arrowhead = annotationG.querySelector("polygon.oc-annotation-connector");
|
|
4368
|
-
const points = arrowhead?.getAttribute("points") ?? "";
|
|
4369
|
-
const firstPoint = points.split(" ")[0] ?? "0,0";
|
|
4370
|
-
const [px, py] = firstPoint.split(",");
|
|
4371
|
-
toX = Number(px) || 0;
|
|
4372
|
-
toY = Number(py) || 0;
|
|
4373
|
-
}
|
|
4374
|
-
const endpoints = [
|
|
4375
|
-
{ name: "from", cx: fromX, cy: fromY },
|
|
4376
|
-
{ name: "to", cx: toX, cy: toY }
|
|
4377
|
-
];
|
|
4378
|
-
const createdHandles = [];
|
|
4379
|
-
for (const ep of endpoints) {
|
|
4380
|
-
if (!Number.isFinite(ep.cx) || !Number.isFinite(ep.cy)) continue;
|
|
4381
|
-
const handleEl = document.createElementNS(SVG_NS3, "circle");
|
|
4382
|
-
handleEl.setAttribute("class", "oc-connector-handle");
|
|
4383
|
-
handleEl.setAttribute("data-endpoint", ep.name);
|
|
4384
|
-
handleEl.setAttribute("cx", String(ep.cx));
|
|
4385
|
-
handleEl.setAttribute("cy", String(ep.cy));
|
|
4386
|
-
handleEl.setAttribute("r", "4");
|
|
4387
|
-
handleEl.setAttribute("opacity", "0");
|
|
4388
|
-
handleEl.setAttribute("fill", "currentColor");
|
|
4389
|
-
handleEl.setAttribute("stroke", "currentColor");
|
|
4390
|
-
annotationG.appendChild(handleEl);
|
|
4391
|
-
createdHandles.push(handleEl);
|
|
4392
|
-
const origCx = ep.cx;
|
|
4393
|
-
const origCy = ep.cy;
|
|
4394
|
-
const stopProp = (e) => {
|
|
4395
|
-
e.stopPropagation();
|
|
4396
|
-
};
|
|
4397
|
-
handleEl.addEventListener("mousedown", stopProp);
|
|
4398
|
-
handleEl.addEventListener("touchstart", stopProp);
|
|
4399
|
-
cleanups.push(() => {
|
|
4400
|
-
handleEl.removeEventListener("mousedown", stopProp);
|
|
4401
|
-
handleEl.removeEventListener("touchstart", stopProp);
|
|
4402
|
-
});
|
|
4403
|
-
const cleanup = createDragHandler({
|
|
4404
|
-
element: handleEl,
|
|
4405
|
-
svg,
|
|
4406
|
-
onMove: (dx, dy) => {
|
|
4407
|
-
handleEl.setAttribute("cx", String(origCx + dx));
|
|
4408
|
-
handleEl.setAttribute("cy", String(origCy + dy));
|
|
4409
|
-
if (connectorLine) {
|
|
4410
|
-
if (ep.name === "from") {
|
|
4411
|
-
connectorLine.setAttribute("x1", String(origCx + dx));
|
|
4412
|
-
connectorLine.setAttribute("y1", String(origCy + dy));
|
|
4413
|
-
} else {
|
|
4414
|
-
connectorLine.setAttribute("x2", String(origCx + dx));
|
|
4415
|
-
connectorLine.setAttribute("y2", String(origCy + dy));
|
|
4416
|
-
}
|
|
4417
|
-
}
|
|
4418
|
-
},
|
|
4419
|
-
onEnd: (dx, dy, moved) => {
|
|
4420
|
-
handleEl.setAttribute("cx", String(origCx));
|
|
4421
|
-
handleEl.setAttribute("cy", String(origCy));
|
|
4422
|
-
if (connectorLine) {
|
|
4423
|
-
if (ep.name === "from") {
|
|
4424
|
-
connectorLine.setAttribute("x1", String(origCx));
|
|
4425
|
-
connectorLine.setAttribute("y1", String(origCy));
|
|
4426
|
-
} else {
|
|
4427
|
-
connectorLine.setAttribute("x2", String(origCx));
|
|
4428
|
-
connectorLine.setAttribute("y2", String(origCy));
|
|
4429
|
-
}
|
|
4430
|
-
}
|
|
4431
|
-
if (moved) {
|
|
4432
|
-
const existingOffset = textAnnotation.connectorOffset?.[ep.name];
|
|
4433
|
-
const origEndDx = existingOffset?.dx ?? 0;
|
|
4434
|
-
const origEndDy = existingOffset?.dy ?? 0;
|
|
4435
|
-
onEdit({
|
|
4436
|
-
type: "annotation-connector",
|
|
4437
|
-
annotation: textAnnotation,
|
|
4438
|
-
endpoint: ep.name,
|
|
4439
|
-
offset: { dx: origEndDx + dx, dy: origEndDy + dy }
|
|
4440
|
-
});
|
|
4441
|
-
}
|
|
4442
|
-
},
|
|
4443
|
-
setDragging
|
|
4444
|
-
});
|
|
4445
|
-
cleanups.push(cleanup);
|
|
4446
|
-
}
|
|
4447
|
-
const showHandles = () => {
|
|
4448
|
-
for (const h of createdHandles) {
|
|
4449
|
-
h.setAttribute("opacity", "0.6");
|
|
4450
|
-
}
|
|
4451
|
-
};
|
|
4452
|
-
const hideHandles = () => {
|
|
4453
|
-
for (const h of createdHandles) {
|
|
4454
|
-
h.setAttribute("opacity", "0");
|
|
4455
|
-
}
|
|
4456
|
-
};
|
|
4457
|
-
annotationG.addEventListener("mouseenter", showHandles);
|
|
4458
|
-
annotationG.addEventListener("mouseleave", hideHandles);
|
|
4459
|
-
cleanups.push(() => {
|
|
4460
|
-
annotationG.removeEventListener("mouseenter", showHandles);
|
|
4461
|
-
annotationG.removeEventListener("mouseleave", hideHandles);
|
|
4462
|
-
for (const h of createdHandles) {
|
|
4463
|
-
h.remove();
|
|
4464
|
-
}
|
|
4465
|
-
});
|
|
4466
|
-
}
|
|
4467
|
-
return () => {
|
|
4468
|
-
for (const cleanup of cleanups) {
|
|
4469
|
-
cleanup();
|
|
4470
|
-
}
|
|
4471
|
-
};
|
|
4472
|
-
}
|
|
4473
|
-
function wireAnnotationLabelDrag(svg, specAnnotations, onEdit, setDragging) {
|
|
4474
|
-
const cleanups = [];
|
|
4475
|
-
const selectors = [
|
|
4476
|
-
".oc-annotation-range .oc-annotation-label",
|
|
4477
|
-
".oc-annotation-refline .oc-annotation-label"
|
|
4478
|
-
];
|
|
4479
|
-
for (const selector of selectors) {
|
|
4480
|
-
const labels = svg.querySelectorAll(selector);
|
|
4481
|
-
for (const label of labels) {
|
|
4482
|
-
const annotationG = label.closest(".oc-annotation");
|
|
4483
|
-
if (!annotationG) continue;
|
|
4484
|
-
const indexStr = annotationG.getAttribute("data-annotation-index");
|
|
4485
|
-
if (indexStr === null) continue;
|
|
4486
|
-
const index2 = Number(indexStr);
|
|
4487
|
-
const specAnnotation = specAnnotations[index2];
|
|
4488
|
-
if (!specAnnotation) continue;
|
|
4489
|
-
const labelEl = label;
|
|
4490
|
-
labelEl.style.cursor = "grab";
|
|
4491
|
-
const isRange = specAnnotation.type === "range";
|
|
4492
|
-
const existingLabelOffset = isRange ? specAnnotation.labelOffset : specAnnotation.labelOffset;
|
|
4493
|
-
const origLabelDx = existingLabelOffset?.dx ?? 0;
|
|
4494
|
-
const origLabelDy = existingLabelOffset?.dy ?? 0;
|
|
4495
|
-
const cleanup = createDragHandler({
|
|
4496
|
-
element: labelEl,
|
|
4497
|
-
svg,
|
|
4498
|
-
onMove: (dx, dy) => {
|
|
4499
|
-
labelEl.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
4500
|
-
},
|
|
4501
|
-
onEnd: (dx, dy, moved) => {
|
|
4502
|
-
labelEl.style.transform = "";
|
|
4503
|
-
if (moved) {
|
|
4504
|
-
if (isRange) {
|
|
4505
|
-
onEdit({
|
|
4506
|
-
type: "range-label",
|
|
4507
|
-
annotation: specAnnotation,
|
|
4508
|
-
labelOffset: { dx: origLabelDx + dx, dy: origLabelDy + dy }
|
|
4509
|
-
});
|
|
4510
|
-
} else {
|
|
4511
|
-
onEdit({
|
|
4512
|
-
type: "refline-label",
|
|
4513
|
-
annotation: specAnnotation,
|
|
4514
|
-
labelOffset: { dx: origLabelDx + dx, dy: origLabelDy + dy }
|
|
4515
|
-
});
|
|
4516
|
-
}
|
|
4517
|
-
}
|
|
4518
|
-
},
|
|
4519
|
-
setDragging
|
|
4520
|
-
});
|
|
4521
|
-
cleanups.push(cleanup);
|
|
4522
|
-
}
|
|
4523
|
-
}
|
|
4524
|
-
return () => {
|
|
4525
|
-
for (const cleanup of cleanups) {
|
|
4526
|
-
cleanup();
|
|
4527
|
-
}
|
|
4528
|
-
};
|
|
4529
|
-
}
|
|
4530
|
-
function wireChromeDrag(svg, spec, onEdit, setDragging) {
|
|
4531
|
-
const chromeTexts = svg.querySelectorAll(".oc-chrome text[data-chrome-key]");
|
|
4532
|
-
const cleanups = [];
|
|
4533
|
-
const chromeConfig = "chrome" in spec ? spec.chrome : void 0;
|
|
4534
|
-
for (const el of chromeTexts) {
|
|
4535
|
-
const textEl = el;
|
|
4536
|
-
const key = textEl.getAttribute("data-chrome-key");
|
|
4537
|
-
if (!key) continue;
|
|
4538
|
-
const chromeEntry = chromeConfig?.[key];
|
|
4539
|
-
const existingOffset = typeof chromeEntry === "object" && chromeEntry !== null ? chromeEntry.offset : void 0;
|
|
4540
|
-
const origChromeDx = existingOffset?.dx ?? 0;
|
|
4541
|
-
const origChromeDy = existingOffset?.dy ?? 0;
|
|
4542
|
-
textEl.style.cursor = "grab";
|
|
4543
|
-
const cleanup = createDragHandler({
|
|
4544
|
-
element: textEl,
|
|
4545
|
-
svg,
|
|
4546
|
-
onMove: (dx, dy) => {
|
|
4547
|
-
textEl.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
4548
|
-
},
|
|
4549
|
-
onEnd: (dx, dy, moved) => {
|
|
4550
|
-
textEl.style.transform = "";
|
|
4551
|
-
if (moved) {
|
|
4552
|
-
onEdit({
|
|
4553
|
-
type: "chrome",
|
|
4554
|
-
key,
|
|
4555
|
-
text: textEl.textContent ?? "",
|
|
4556
|
-
offset: { dx: origChromeDx + dx, dy: origChromeDy + dy }
|
|
4557
|
-
});
|
|
4558
|
-
}
|
|
4559
|
-
},
|
|
4560
|
-
setDragging
|
|
4561
|
-
});
|
|
4562
|
-
cleanups.push(cleanup);
|
|
4563
|
-
}
|
|
4564
|
-
return () => {
|
|
4565
|
-
for (const cleanup of cleanups) {
|
|
4566
|
-
cleanup();
|
|
4567
|
-
}
|
|
4568
|
-
};
|
|
4569
|
-
}
|
|
4570
|
-
function wireLegendDrag(svg, spec, onEdit, setDragging) {
|
|
4571
|
-
const legendG = svg.querySelector(".oc-legend");
|
|
4572
|
-
if (!legendG) return () => {
|
|
4573
|
-
};
|
|
4574
|
-
const cleanups = [];
|
|
4575
|
-
const legendConfig = "legend" in spec ? spec.legend : void 0;
|
|
4576
|
-
const origLegendDx = legendConfig?.offset?.dx ?? 0;
|
|
4577
|
-
const origLegendDy = legendConfig?.offset?.dy ?? 0;
|
|
4578
|
-
legendG.style.cursor = "grab";
|
|
4579
|
-
const cleanup = createDragHandler({
|
|
4580
|
-
element: legendG,
|
|
4581
|
-
svg,
|
|
4582
|
-
onMove: (dx, dy) => {
|
|
4583
|
-
legendG.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
4584
|
-
},
|
|
4585
|
-
onEnd: (dx, dy, moved) => {
|
|
4586
|
-
legendG.style.transform = "";
|
|
4587
|
-
if (moved) {
|
|
4588
|
-
onEdit({ type: "legend", offset: { dx: origLegendDx + dx, dy: origLegendDy + dy } });
|
|
4589
|
-
}
|
|
4590
|
-
},
|
|
4591
|
-
setDragging
|
|
4592
|
-
});
|
|
4593
|
-
cleanups.push(cleanup);
|
|
4594
|
-
return () => {
|
|
4595
|
-
for (const cleanup2 of cleanups) {
|
|
4596
|
-
cleanup2();
|
|
4597
|
-
}
|
|
4598
|
-
};
|
|
4599
|
-
}
|
|
4600
|
-
function wireSeriesLabelDrag(svg, spec, onEdit, setDragging) {
|
|
4601
|
-
const labels = svg.querySelectorAll(".oc-mark-label");
|
|
4602
|
-
const cleanups = [];
|
|
4603
|
-
const rawLabels = "labels" in spec ? spec.labels : void 0;
|
|
4604
|
-
const labelsConfig = typeof rawLabels === "object" ? rawLabels : void 0;
|
|
4605
|
-
for (const label of labels) {
|
|
4606
|
-
const labelEl = label;
|
|
4607
|
-
const series = labelEl.getAttribute("data-series") ?? labelEl.closest("[data-series]")?.getAttribute("data-series");
|
|
4608
|
-
if (!series) continue;
|
|
4609
|
-
const existingSeriesOffset = labelsConfig?.offsets?.[series];
|
|
4610
|
-
const origSeriesDx = existingSeriesOffset?.dx ?? 0;
|
|
4611
|
-
const origSeriesDy = existingSeriesOffset?.dy ?? 0;
|
|
4612
|
-
labelEl.style.cursor = "grab";
|
|
4613
|
-
const cleanup = createDragHandler({
|
|
4614
|
-
element: labelEl,
|
|
4615
|
-
svg,
|
|
4616
|
-
onMove: (dx, dy) => {
|
|
4617
|
-
labelEl.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
4618
|
-
},
|
|
4619
|
-
onEnd: (dx, dy, moved) => {
|
|
4620
|
-
labelEl.style.transform = "";
|
|
4621
|
-
if (moved) {
|
|
4622
|
-
onEdit({
|
|
4623
|
-
type: "series-label",
|
|
4624
|
-
series,
|
|
4625
|
-
offset: { dx: origSeriesDx + dx, dy: origSeriesDy + dy }
|
|
4626
|
-
});
|
|
4627
|
-
}
|
|
4628
|
-
},
|
|
4629
|
-
setDragging
|
|
4630
|
-
});
|
|
4631
|
-
cleanups.push(cleanup);
|
|
4632
|
-
}
|
|
4633
|
-
return () => {
|
|
4634
|
-
for (const cleanup of cleanups) {
|
|
4635
|
-
cleanup();
|
|
4636
|
-
}
|
|
4637
|
-
};
|
|
4638
|
-
}
|
|
4639
|
-
|
|
4640
|
-
// src/interactions/keyboard-nav.ts
|
|
4641
|
-
function wireKeyboardNav(svg, container, tooltipDescriptors, tooltipManager, layout) {
|
|
4642
|
-
container.setAttribute("tabindex", "0");
|
|
4643
|
-
container.setAttribute("aria-roledescription", "chart");
|
|
4644
|
-
container.setAttribute("aria-label", layout.a11y.altText);
|
|
4645
|
-
const markElements = [];
|
|
4646
|
-
const allMarkEls = svg.querySelectorAll("[data-mark-id]");
|
|
4647
|
-
for (const el of allMarkEls) {
|
|
4648
|
-
const markId = el.getAttribute("data-mark-id");
|
|
4649
|
-
if (markId && tooltipDescriptors.has(markId)) {
|
|
4650
|
-
markElements.push(el);
|
|
4651
|
-
}
|
|
4652
|
-
}
|
|
4653
|
-
let focusIndex = -1;
|
|
4654
|
-
function highlightMark(index2) {
|
|
4655
|
-
if (focusIndex >= 0 && focusIndex < markElements.length) {
|
|
4656
|
-
markElements[focusIndex].classList.remove("oc-mark-focused");
|
|
4657
|
-
markElements[focusIndex].removeAttribute("aria-selected");
|
|
4658
|
-
}
|
|
4659
|
-
focusIndex = index2;
|
|
4660
|
-
if (focusIndex >= 0 && focusIndex < markElements.length) {
|
|
4661
|
-
const el = markElements[focusIndex];
|
|
4662
|
-
el.classList.add("oc-mark-focused");
|
|
4663
|
-
el.setAttribute("aria-selected", "true");
|
|
4664
|
-
}
|
|
4665
|
-
}
|
|
4666
|
-
function showTooltipForFocused() {
|
|
4667
|
-
if (focusIndex < 0 || focusIndex >= markElements.length) return;
|
|
4668
|
-
const el = markElements[focusIndex];
|
|
4669
|
-
const markId = el.getAttribute("data-mark-id");
|
|
4670
|
-
if (!markId) return;
|
|
4671
|
-
const content = tooltipDescriptors.get(markId);
|
|
4672
|
-
if (!content) return;
|
|
4673
|
-
const bbox = el.getBoundingClientRect();
|
|
4674
|
-
const containerRect = container.getBoundingClientRect();
|
|
4675
|
-
const x3 = bbox.left + bbox.width / 2 - containerRect.left;
|
|
4676
|
-
const y3 = bbox.top - containerRect.top;
|
|
4677
|
-
tooltipManager.show(content, x3, y3);
|
|
4678
|
-
}
|
|
4679
|
-
const handleKeyDown = (e) => {
|
|
4680
|
-
if (markElements.length === 0) return;
|
|
4681
|
-
switch (e.key) {
|
|
4682
|
-
case "ArrowRight":
|
|
4683
|
-
case "ArrowDown": {
|
|
4684
|
-
e.preventDefault();
|
|
4685
|
-
const next = focusIndex < markElements.length - 1 ? focusIndex + 1 : 0;
|
|
4686
|
-
highlightMark(next);
|
|
4687
|
-
showTooltipForFocused();
|
|
4688
|
-
break;
|
|
4689
|
-
}
|
|
4690
|
-
case "ArrowLeft":
|
|
4691
|
-
case "ArrowUp": {
|
|
4692
|
-
e.preventDefault();
|
|
4693
|
-
const prev = focusIndex > 0 ? focusIndex - 1 : markElements.length - 1;
|
|
4694
|
-
highlightMark(prev);
|
|
4695
|
-
showTooltipForFocused();
|
|
4696
|
-
break;
|
|
4697
|
-
}
|
|
4698
|
-
case "Enter":
|
|
4699
|
-
case " ": {
|
|
4700
|
-
e.preventDefault();
|
|
4701
|
-
if (focusIndex >= 0) {
|
|
4702
|
-
showTooltipForFocused();
|
|
4703
|
-
} else if (markElements.length > 0) {
|
|
4704
|
-
highlightMark(0);
|
|
4705
|
-
showTooltipForFocused();
|
|
4706
|
-
}
|
|
4707
|
-
break;
|
|
4708
|
-
}
|
|
4709
|
-
case "Escape": {
|
|
4710
|
-
e.preventDefault();
|
|
4711
|
-
tooltipManager.hide();
|
|
4712
|
-
highlightMark(-1);
|
|
4713
|
-
break;
|
|
4714
|
-
}
|
|
4715
|
-
}
|
|
4716
|
-
};
|
|
4717
|
-
container.addEventListener("keydown", handleKeyDown);
|
|
4718
|
-
return () => {
|
|
4719
|
-
container.removeEventListener("keydown", handleKeyDown);
|
|
4720
|
-
container.removeAttribute("tabindex");
|
|
4721
|
-
container.removeAttribute("aria-roledescription");
|
|
4722
|
-
container.removeAttribute("aria-label");
|
|
4723
|
-
};
|
|
4724
|
-
}
|
|
4725
|
-
|
|
4726
|
-
// src/interactions/legend-interaction.ts
|
|
4727
|
-
function wireLegendInteraction(svg, _layout, toggleSeries, onLegendToggle, onEdit) {
|
|
4728
|
-
const legendEntries = svg.querySelectorAll("[data-legend-index]");
|
|
4729
|
-
const cleanups = [];
|
|
4730
|
-
for (const entry of legendEntries) {
|
|
4731
|
-
if (entry.getAttribute("data-legend-overflow") === "true") continue;
|
|
4732
|
-
const handleClick = () => {
|
|
4733
|
-
const label = entry.getAttribute("data-legend-label");
|
|
4734
|
-
if (!label) return;
|
|
4735
|
-
const nowHidden = toggleSeries(label);
|
|
4736
|
-
onLegendToggle?.(label, !nowHidden);
|
|
4737
|
-
onEdit?.({ type: "legend-toggle", series: label, hidden: nowHidden });
|
|
4738
|
-
};
|
|
4739
|
-
entry.addEventListener("click", handleClick);
|
|
4740
|
-
cleanups.push(() => entry.removeEventListener("click", handleClick));
|
|
4741
|
-
}
|
|
4742
|
-
return () => {
|
|
4743
|
-
for (const cleanup of cleanups) {
|
|
4744
|
-
cleanup();
|
|
4745
|
-
}
|
|
4746
|
-
};
|
|
4747
|
-
}
|
|
4748
|
-
|
|
4749
|
-
// src/interactions/selection.ts
|
|
4750
|
-
import { elementRef } from "@opendata-ai/openchart-core";
|
|
4751
|
-
function findElementByRef(svg, ref) {
|
|
4752
|
-
switch (ref.type) {
|
|
4753
|
-
case "annotation": {
|
|
4754
|
-
if (ref.id) {
|
|
4755
|
-
const byId = svg.querySelector(`[data-annotation-id="${ref.id}"]`);
|
|
4756
|
-
if (byId) return byId;
|
|
4757
|
-
}
|
|
4758
|
-
return svg.querySelector(`[data-annotation-index="${ref.index}"]`);
|
|
4759
|
-
}
|
|
4760
|
-
case "chrome":
|
|
4761
|
-
return svg.querySelector(`[data-chrome-key="${ref.key}"]`);
|
|
4762
|
-
case "series-label":
|
|
4763
|
-
return svg.querySelector(`.oc-mark-label[data-series="${ref.series}"]`);
|
|
4764
|
-
case "legend":
|
|
4765
|
-
return svg.querySelector(".oc-legend");
|
|
4766
|
-
case "legend-entry":
|
|
4767
|
-
return svg.querySelector(`[data-legend-index="${ref.index}"]`);
|
|
4768
|
-
}
|
|
4769
|
-
}
|
|
4770
|
-
function buildElementRef(element, _specAnnotations) {
|
|
4771
|
-
const annotationEl = element.closest("[data-annotation-index]");
|
|
4772
|
-
if (annotationEl) {
|
|
4773
|
-
const index2 = Number(annotationEl.getAttribute("data-annotation-index"));
|
|
4774
|
-
const id = annotationEl.getAttribute("data-annotation-id") ?? void 0;
|
|
4775
|
-
return elementRef.annotation(index2, id);
|
|
4776
|
-
}
|
|
4777
|
-
const chromeEl = element.closest("[data-chrome-key]");
|
|
4778
|
-
if (chromeEl) {
|
|
4779
|
-
const key = chromeEl.getAttribute("data-chrome-key");
|
|
4780
|
-
if (key) return elementRef.chrome(key);
|
|
4781
|
-
}
|
|
4782
|
-
const seriesLabelEl = element.closest(".oc-mark-label[data-series]");
|
|
4783
|
-
if (seriesLabelEl) {
|
|
4784
|
-
const series = seriesLabelEl.getAttribute("data-series");
|
|
4785
|
-
if (series) return elementRef.seriesLabel(series);
|
|
4786
|
-
}
|
|
4787
|
-
const legendEntryEl = element.closest("[data-legend-index]");
|
|
4788
|
-
if (legendEntryEl) {
|
|
4789
|
-
const index2 = Number(legendEntryEl.getAttribute("data-legend-index"));
|
|
4790
|
-
const series = legendEntryEl.getAttribute("data-legend-label") ?? "";
|
|
4791
|
-
return elementRef.legendEntry(series, index2);
|
|
4792
|
-
}
|
|
4793
|
-
const legendEl = element.closest(".oc-legend");
|
|
4794
|
-
if (legendEl) return elementRef.legend();
|
|
4795
|
-
return null;
|
|
4796
|
-
}
|
|
4797
|
-
function getEditableElements(spec, layout) {
|
|
4798
|
-
const refs = [];
|
|
4799
|
-
const chromeKeys = ["title", "subtitle", "source", "byline", "footer"];
|
|
4800
|
-
for (const key of chromeKeys) {
|
|
4801
|
-
if (layout.chrome[key]) {
|
|
4802
|
-
refs.push(elementRef.chrome(key));
|
|
4803
|
-
}
|
|
4804
|
-
}
|
|
4805
|
-
const annotations = "annotations" in spec && Array.isArray(spec.annotations) ? spec.annotations : [];
|
|
4806
|
-
for (let i = 0; i < annotations.length; i++) {
|
|
4807
|
-
refs.push(elementRef.annotation(i, annotations[i].id));
|
|
4808
|
-
}
|
|
4809
|
-
const seriesLabels = [];
|
|
4810
|
-
for (const mark of layout.marks) {
|
|
4811
|
-
if (mark.type === "line" && mark.label?.visible && mark.seriesKey) {
|
|
4812
|
-
seriesLabels.push(mark.seriesKey);
|
|
4813
|
-
}
|
|
4814
|
-
}
|
|
4815
|
-
seriesLabels.sort();
|
|
4816
|
-
for (const series of seriesLabels) {
|
|
4817
|
-
refs.push(elementRef.seriesLabel(series));
|
|
4818
|
-
}
|
|
4819
|
-
if ("entries" in layout.legend && layout.legend.entries.length > 0) {
|
|
4820
|
-
refs.push(elementRef.legend());
|
|
4821
|
-
}
|
|
4822
|
-
return refs;
|
|
4823
|
-
}
|
|
4824
|
-
function isTextEditable(ref, specAnnotations) {
|
|
4825
|
-
if (ref.type === "chrome") return true;
|
|
4826
|
-
if (ref.type === "annotation") {
|
|
4827
|
-
const annotation = specAnnotations[ref.index];
|
|
4828
|
-
return annotation?.type === "text";
|
|
4829
|
-
}
|
|
4830
|
-
return false;
|
|
4831
|
-
}
|
|
4832
|
-
function getElementText(ref, spec) {
|
|
4833
|
-
if (ref.type === "chrome") {
|
|
4834
|
-
const chromeConfig = "chrome" in spec ? spec.chrome : void 0;
|
|
4835
|
-
if (!chromeConfig) return null;
|
|
4836
|
-
const entry = chromeConfig[ref.key];
|
|
4837
|
-
if (typeof entry === "string") return entry;
|
|
4838
|
-
if (typeof entry === "object" && entry !== null && "text" in entry) {
|
|
4839
|
-
return entry.text;
|
|
4840
|
-
}
|
|
4841
|
-
return null;
|
|
4842
|
-
}
|
|
4843
|
-
if (ref.type === "annotation") {
|
|
4844
|
-
const annotations = "annotations" in spec && Array.isArray(spec.annotations) ? spec.annotations : [];
|
|
4845
|
-
const annotation = annotations[ref.index];
|
|
4846
|
-
if (annotation?.type === "text") return annotation.text ?? null;
|
|
4847
|
-
if (annotation?.label) return annotation.label;
|
|
4848
|
-
return null;
|
|
4849
|
-
}
|
|
4850
|
-
return null;
|
|
4851
|
-
}
|
|
4852
|
-
function refsEqual(a2, b) {
|
|
4853
|
-
if (a2 === null || b === null) return a2 === b;
|
|
4854
|
-
if (a2.type !== b.type) return false;
|
|
4855
|
-
switch (a2.type) {
|
|
4856
|
-
case "annotation": {
|
|
4857
|
-
const bAnno = b;
|
|
4858
|
-
if (a2.id && bAnno.id) return a2.id === bAnno.id;
|
|
4859
|
-
return a2.index === bAnno.index;
|
|
4860
|
-
}
|
|
4861
|
-
case "chrome":
|
|
4862
|
-
return a2.key === b.key;
|
|
4863
|
-
case "series-label":
|
|
4864
|
-
return a2.series === b.series;
|
|
4865
|
-
case "legend":
|
|
4866
|
-
return true;
|
|
4867
|
-
case "legend-entry": {
|
|
4868
|
-
const bEntry = b;
|
|
4869
|
-
return a2.index === bEntry.index && a2.series === bEntry.series;
|
|
4870
|
-
}
|
|
4871
|
-
}
|
|
4872
|
-
}
|
|
4873
|
-
function renderSelectionOverlay(svg, ref, layout) {
|
|
4874
|
-
const target = findElementByRef(svg, ref);
|
|
4875
|
-
if (!target) return null;
|
|
4876
|
-
const bbox = target.getBBox();
|
|
4877
|
-
const padding = 4;
|
|
4878
|
-
const accentColor = layout.theme.colors.categorical?.[0] ?? "#4f46e5";
|
|
4879
|
-
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
4880
|
-
g.setAttribute("class", "oc-selection-overlay");
|
|
4881
|
-
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
4882
|
-
rect.setAttribute("x", String(bbox.x - padding));
|
|
4883
|
-
rect.setAttribute("y", String(bbox.y - padding));
|
|
4884
|
-
rect.setAttribute("width", String(bbox.width + padding * 2));
|
|
4885
|
-
rect.setAttribute("height", String(bbox.height + padding * 2));
|
|
4886
|
-
rect.setAttribute("rx", "3");
|
|
4887
|
-
rect.setAttribute("fill", "transparent");
|
|
4888
|
-
rect.setAttribute("stroke", accentColor);
|
|
4889
|
-
rect.setAttribute("stroke-width", "1.5");
|
|
4890
|
-
rect.setAttribute("pointer-events", "none");
|
|
4891
|
-
g.appendChild(rect);
|
|
4892
|
-
svg.appendChild(g);
|
|
4893
|
-
return g;
|
|
4894
|
-
}
|
|
4895
|
-
function createScreenReaderTable(layout, container) {
|
|
4896
|
-
const data = layout.a11y.dataTableFallback;
|
|
4897
|
-
if (!data || data.length === 0) return null;
|
|
4898
|
-
const table = document.createElement("table");
|
|
4899
|
-
table.className = "oc-sr-only";
|
|
4900
|
-
table.style.position = "absolute";
|
|
4901
|
-
table.style.width = "1px";
|
|
4902
|
-
table.style.height = "1px";
|
|
4903
|
-
table.style.padding = "0";
|
|
4904
|
-
table.style.margin = "-1px";
|
|
4905
|
-
table.style.overflow = "hidden";
|
|
4906
|
-
table.style.clipPath = "inset(50%)";
|
|
4907
|
-
table.style.whiteSpace = "nowrap";
|
|
4908
|
-
table.style.borderWidth = "0";
|
|
4909
|
-
table.setAttribute("role", "table");
|
|
4910
|
-
table.setAttribute("aria-label", `Data table: ${layout.a11y.altText}`);
|
|
4911
|
-
if (data.length > 0) {
|
|
4912
|
-
const thead = document.createElement("thead");
|
|
4913
|
-
const headerRow = document.createElement("tr");
|
|
4914
|
-
const headers = data[0];
|
|
4915
|
-
for (const header of headers) {
|
|
4916
|
-
const th = document.createElement("th");
|
|
4917
|
-
th.textContent = String(header ?? "");
|
|
4918
|
-
th.setAttribute("scope", "col");
|
|
4919
|
-
headerRow.appendChild(th);
|
|
4920
|
-
}
|
|
4921
|
-
thead.appendChild(headerRow);
|
|
4922
|
-
table.appendChild(thead);
|
|
4923
|
-
}
|
|
4924
|
-
if (data.length > 1) {
|
|
4925
|
-
const tbody = document.createElement("tbody");
|
|
4926
|
-
for (let i = 1; i < data.length; i++) {
|
|
4927
|
-
const tr = document.createElement("tr");
|
|
4928
|
-
const cells = data[i];
|
|
4929
|
-
for (const cell of cells) {
|
|
4930
|
-
const td = document.createElement("td");
|
|
4931
|
-
td.textContent = String(cell ?? "");
|
|
4932
|
-
tr.appendChild(td);
|
|
4933
|
-
}
|
|
4934
|
-
tbody.appendChild(tr);
|
|
4935
|
-
}
|
|
4936
|
-
table.appendChild(tbody);
|
|
4937
|
-
}
|
|
4938
|
-
container.appendChild(table);
|
|
4939
|
-
return table;
|
|
4940
|
-
}
|
|
4941
|
-
|
|
4942
|
-
// src/interactions/tooltip-events.ts
|
|
4943
|
-
function wireTooltipEvents(svg, tooltipDescriptors, tooltipManager) {
|
|
4944
|
-
const markElements = svg.querySelectorAll("[data-mark-id]");
|
|
4945
|
-
const cleanups = [];
|
|
4946
|
-
for (const el of markElements) {
|
|
4947
|
-
const markId = el.getAttribute("data-mark-id");
|
|
4948
|
-
if (!markId) continue;
|
|
4949
|
-
const content = tooltipDescriptors.get(markId);
|
|
4950
|
-
if (!content) continue;
|
|
4951
|
-
const handleMouseEnter = (e) => {
|
|
4952
|
-
const mouseEvent = e;
|
|
4953
|
-
const svgRect = svg.getBoundingClientRect();
|
|
4954
|
-
const x3 = mouseEvent.clientX - svgRect.left;
|
|
4955
|
-
const y3 = mouseEvent.clientY - svgRect.top;
|
|
4956
|
-
tooltipManager.show(content, x3, y3);
|
|
4957
|
-
};
|
|
4958
|
-
const handleMouseMove = (e) => {
|
|
4959
|
-
const mouseEvent = e;
|
|
4960
|
-
const svgRect = svg.getBoundingClientRect();
|
|
4961
|
-
const x3 = mouseEvent.clientX - svgRect.left;
|
|
4962
|
-
const y3 = mouseEvent.clientY - svgRect.top;
|
|
4963
|
-
tooltipManager.show(content, x3, y3);
|
|
4964
|
-
};
|
|
4965
|
-
const handleMouseLeave = () => {
|
|
4966
|
-
tooltipManager.hide();
|
|
4967
|
-
};
|
|
4968
|
-
const handleTouchStart = (e) => {
|
|
4969
|
-
const touchEvent = e;
|
|
4970
|
-
if (touchEvent.touches.length > 0) {
|
|
4971
|
-
const touch = touchEvent.touches[0];
|
|
4972
|
-
const svgRect = svg.getBoundingClientRect();
|
|
4973
|
-
const x3 = touch.clientX - svgRect.left;
|
|
4974
|
-
const y3 = touch.clientY - svgRect.top;
|
|
4975
|
-
tooltipManager.show(content, x3, y3);
|
|
4976
|
-
}
|
|
4977
|
-
};
|
|
4978
|
-
el.addEventListener("mouseenter", handleMouseEnter);
|
|
4979
|
-
el.addEventListener("mousemove", handleMouseMove);
|
|
4980
|
-
el.addEventListener("mouseleave", handleMouseLeave);
|
|
4981
|
-
el.addEventListener("touchstart", handleTouchStart);
|
|
4982
|
-
cleanups.push(() => {
|
|
4983
|
-
el.removeEventListener("mouseenter", handleMouseEnter);
|
|
4984
|
-
el.removeEventListener("mousemove", handleMouseMove);
|
|
4985
|
-
el.removeEventListener("mouseleave", handleMouseLeave);
|
|
4986
|
-
el.removeEventListener("touchstart", handleTouchStart);
|
|
4987
|
-
});
|
|
4988
|
-
}
|
|
4989
|
-
return () => {
|
|
4990
|
-
for (const cleanup of cleanups) {
|
|
4991
|
-
cleanup();
|
|
4992
|
-
}
|
|
4993
|
-
};
|
|
4994
|
-
}
|
|
4995
|
-
|
|
4996
|
-
// src/text-edit-overlay.ts
|
|
4997
|
-
function getScale(svg) {
|
|
4998
|
-
const viewBox = svg.viewBox?.baseVal;
|
|
4999
|
-
const svgRect = svg.getBoundingClientRect();
|
|
5000
|
-
return {
|
|
5001
|
-
scaleX: viewBox?.width && svgRect.width ? svgRect.width / viewBox.width : 1,
|
|
5002
|
-
scaleY: viewBox?.height && svgRect.height ? svgRect.height / viewBox.height : 1
|
|
5003
|
-
};
|
|
5004
|
-
}
|
|
5005
|
-
function getTextStyles(targetElement, scale) {
|
|
5006
|
-
const computed = window.getComputedStyle(targetElement);
|
|
5007
|
-
const svgFontSize = parseFloat(
|
|
5008
|
-
targetElement.getAttribute("font-size") ?? computed.fontSize ?? "14"
|
|
5009
|
-
);
|
|
5010
|
-
const pixelFontSize = svgFontSize * scale.scaleY;
|
|
5011
|
-
const fontFamily = targetElement.getAttribute("font-family") ?? computed.fontFamily ?? "inherit";
|
|
5012
|
-
const fontWeight = targetElement.getAttribute("font-weight") ?? computed.fontWeight ?? "400";
|
|
5013
|
-
const fill = targetElement.style.getPropertyValue("fill") || targetElement.getAttribute("fill") || computed.color || "#000";
|
|
5014
|
-
const textAnchor = targetElement.getAttribute("text-anchor") ?? "start";
|
|
5015
|
-
let textAlign = "left";
|
|
5016
|
-
if (textAnchor === "middle") textAlign = "center";
|
|
5017
|
-
else if (textAnchor === "end") textAlign = "right";
|
|
5018
|
-
return {
|
|
5019
|
-
fontFamily,
|
|
5020
|
-
fontSize: `${pixelFontSize}px`,
|
|
5021
|
-
fontWeight,
|
|
5022
|
-
color: fill,
|
|
5023
|
-
textAlign,
|
|
5024
|
-
lineHeight: "1.3"
|
|
5025
|
-
};
|
|
5026
|
-
}
|
|
5027
|
-
function computePosition2(targetElement, svg, container) {
|
|
5028
|
-
const bbox = targetElement.getBBox();
|
|
5029
|
-
const scale = getScale(svg);
|
|
5030
|
-
const svgRect = svg.getBoundingClientRect();
|
|
5031
|
-
const containerRect = container.getBoundingClientRect();
|
|
5032
|
-
const padding = 4;
|
|
5033
|
-
const left = bbox.x * scale.scaleX + (svgRect.left - containerRect.left) - padding;
|
|
5034
|
-
const top = bbox.y * scale.scaleY + (svgRect.top - containerRect.top) - padding;
|
|
5035
|
-
const width = bbox.width * scale.scaleX + padding * 2;
|
|
5036
|
-
const height = bbox.height * scale.scaleY + padding * 2;
|
|
5037
|
-
return {
|
|
5038
|
-
top: Math.max(0, top),
|
|
5039
|
-
left: Math.max(0, left),
|
|
5040
|
-
width: Math.max(width, 60),
|
|
5041
|
-
height: Math.max(height, 24)
|
|
5042
|
-
};
|
|
5043
|
-
}
|
|
5044
|
-
function createTextEditOverlay(config) {
|
|
5045
|
-
const { container, svg, targetElement, currentText, onCommit, onCancel } = config;
|
|
5046
|
-
let destroyed = false;
|
|
5047
|
-
const originalOpacity = targetElement.getAttribute("opacity");
|
|
5048
|
-
targetElement.setAttribute("opacity", "0");
|
|
5049
|
-
const scale = getScale(svg);
|
|
5050
|
-
const styles = getTextStyles(targetElement, scale);
|
|
5051
|
-
const position = computePosition2(targetElement, svg, container);
|
|
5052
|
-
const textarea = document.createElement("textarea");
|
|
5053
|
-
textarea.value = currentText;
|
|
5054
|
-
const containerPosition = window.getComputedStyle(container).position;
|
|
5055
|
-
const containerWasStatic = containerPosition === "static";
|
|
5056
|
-
if (containerWasStatic) {
|
|
5057
|
-
container.style.position = "relative";
|
|
5058
|
-
}
|
|
5059
|
-
Object.assign(textarea.style, {
|
|
5060
|
-
position: "absolute",
|
|
5061
|
-
top: `${position.top}px`,
|
|
5062
|
-
left: `${position.left}px`,
|
|
5063
|
-
width: `${position.width}px`,
|
|
5064
|
-
minHeight: `${position.height}px`,
|
|
5065
|
-
fontFamily: styles.fontFamily,
|
|
5066
|
-
fontSize: styles.fontSize,
|
|
5067
|
-
fontWeight: styles.fontWeight,
|
|
5068
|
-
color: styles.color,
|
|
5069
|
-
textAlign: styles.textAlign,
|
|
5070
|
-
lineHeight: styles.lineHeight,
|
|
5071
|
-
padding: "2px 4px",
|
|
5072
|
-
margin: "0",
|
|
5073
|
-
border: "1px solid rgba(79, 70, 229, 0.4)",
|
|
5074
|
-
borderRadius: "3px",
|
|
5075
|
-
background: "rgba(255, 255, 255, 0.95)",
|
|
5076
|
-
outline: "none",
|
|
5077
|
-
resize: "none",
|
|
5078
|
-
overflow: "hidden",
|
|
5079
|
-
boxSizing: "border-box",
|
|
5080
|
-
zIndex: "10000",
|
|
5081
|
-
// Remove default textarea appearance
|
|
5082
|
-
WebkitAppearance: "none",
|
|
5083
|
-
appearance: "none"
|
|
5084
|
-
});
|
|
5085
|
-
container.appendChild(textarea);
|
|
5086
|
-
textarea.focus();
|
|
5087
|
-
textarea.select();
|
|
5088
|
-
function destroy() {
|
|
5089
|
-
if (destroyed) return;
|
|
5090
|
-
destroyed = true;
|
|
5091
|
-
if (originalOpacity !== null) {
|
|
5092
|
-
targetElement.setAttribute("opacity", originalOpacity);
|
|
5093
|
-
} else {
|
|
5094
|
-
targetElement.removeAttribute("opacity");
|
|
5095
|
-
}
|
|
5096
|
-
textarea.removeEventListener("keydown", handleKeyDown);
|
|
5097
|
-
document.removeEventListener("mousedown", handleClickOutside);
|
|
5098
|
-
resizeObserver.disconnect();
|
|
5099
|
-
if (containerWasStatic) {
|
|
5100
|
-
container.style.position = "";
|
|
5101
|
-
}
|
|
5102
|
-
if (textarea.parentNode) {
|
|
5103
|
-
textarea.parentNode.removeChild(textarea);
|
|
5104
|
-
}
|
|
5105
|
-
}
|
|
5106
|
-
function commit() {
|
|
5107
|
-
if (destroyed) return;
|
|
5108
|
-
const newText = textarea.value;
|
|
5109
|
-
destroy();
|
|
5110
|
-
onCommit(newText);
|
|
5111
|
-
}
|
|
5112
|
-
function cancel() {
|
|
5113
|
-
if (destroyed) return;
|
|
5114
|
-
destroy();
|
|
5115
|
-
onCancel();
|
|
5116
|
-
}
|
|
5117
|
-
const handleKeyDown = (e) => {
|
|
5118
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
5119
|
-
e.preventDefault();
|
|
5120
|
-
commit();
|
|
5121
|
-
} else if (e.key === "Escape") {
|
|
5122
|
-
e.preventDefault();
|
|
5123
|
-
cancel();
|
|
5124
|
-
}
|
|
5125
|
-
};
|
|
5126
|
-
textarea.addEventListener("keydown", handleKeyDown);
|
|
5127
|
-
const handleClickOutside = (e) => {
|
|
5128
|
-
if (!textarea.contains(e.target)) {
|
|
5129
|
-
commit();
|
|
5130
|
-
}
|
|
5131
|
-
};
|
|
5132
|
-
requestAnimationFrame(() => {
|
|
5133
|
-
if (!destroyed) {
|
|
5134
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
5135
|
-
}
|
|
5136
|
-
});
|
|
5137
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
5138
|
-
if (destroyed) return;
|
|
5139
|
-
const newPosition = computePosition2(targetElement, svg, container);
|
|
5140
|
-
textarea.style.top = `${newPosition.top}px`;
|
|
5141
|
-
textarea.style.left = `${newPosition.left}px`;
|
|
5142
|
-
textarea.style.width = `${newPosition.width}px`;
|
|
5143
|
-
textarea.style.minHeight = `${newPosition.height}px`;
|
|
5144
|
-
});
|
|
5145
|
-
resizeObserver.observe(container);
|
|
5146
|
-
return { destroy };
|
|
5147
|
-
}
|
|
5148
|
-
|
|
5149
|
-
// src/theme-tokens.ts
|
|
5150
|
-
import { adaptForLightLineStroke } from "@opendata-ai/openchart-core";
|
|
5151
|
-
function stampThemeProperties(el, theme) {
|
|
5152
|
-
const accent = theme.colors.categorical[0] ?? "#06b6d4";
|
|
5153
|
-
const bg = theme.colors.background === "transparent" ? theme.isDark ? "#09090b" : "#ffffff" : theme.colors.background;
|
|
5154
|
-
const props = [
|
|
5155
|
-
["--oc-font-family", theme.fonts.family],
|
|
5156
|
-
["--oc-font-mono", theme.fonts.mono],
|
|
5157
|
-
["--oc-title-size", `${theme.chrome.title.fontSize}px`],
|
|
5158
|
-
["--oc-title-weight", `${theme.chrome.title.fontWeight}`],
|
|
5159
|
-
["--oc-subtitle-size", `${theme.chrome.subtitle.fontSize}px`],
|
|
5160
|
-
["--oc-subtitle-weight", `${theme.chrome.subtitle.fontWeight}`],
|
|
5161
|
-
["--oc-source-size", `${theme.chrome.source.fontSize}px`],
|
|
5162
|
-
["--oc-source-weight", `${theme.chrome.source.fontWeight}`],
|
|
5163
|
-
["--oc-body-size", `${theme.fonts.sizes.body}px`],
|
|
5164
|
-
["--oc-eyebrow-size", `${theme.chrome.eyebrow.fontSize}px`],
|
|
5165
|
-
["--oc-eyebrow-weight", `${theme.chrome.eyebrow.fontWeight}`],
|
|
5166
|
-
["--oc-bg", bg],
|
|
5167
|
-
["--oc-text", theme.colors.text],
|
|
5168
|
-
["--oc-text-muted", theme.colors.axis],
|
|
5169
|
-
["--oc-gridline", theme.colors.gridline],
|
|
5170
|
-
["--oc-axis", theme.colors.axis],
|
|
5171
|
-
["--oc-border-radius", `${theme.borderRadius}px`],
|
|
5172
|
-
["--oc-accent", accent],
|
|
5173
|
-
["--oc-accent-strong", adaptForLightLineStroke(accent)],
|
|
5174
|
-
["--oc-positive", theme.colors.positive],
|
|
5175
|
-
["--oc-negative", theme.colors.negative],
|
|
5176
|
-
["--oc-space-2", `${theme.spacing.chromeGap * 2}px`],
|
|
5177
|
-
["--oc-space-4", `${theme.spacing.padding}px`]
|
|
5178
|
-
];
|
|
5179
|
-
for (const [name, value] of props) {
|
|
5180
|
-
el.style.setProperty(name, value);
|
|
5181
|
-
}
|
|
5182
|
-
}
|
|
5183
|
-
|
|
5184
|
-
// src/transition.ts
|
|
5185
|
-
import { isGradientDef } from "@opendata-ai/openchart-core";
|
|
5186
|
-
import {
|
|
5187
|
-
buildAreaPath,
|
|
5188
|
-
buildLinePath,
|
|
5189
|
-
EXIT_DEFAULTS,
|
|
5190
|
-
serializeKeyValue
|
|
5191
|
-
} from "@opendata-ai/openchart-engine";
|
|
5192
|
-
function cubicOut(t) {
|
|
5193
|
-
const f = 1 - t;
|
|
5194
|
-
return 1 - f * f * f;
|
|
5195
|
-
}
|
|
5196
|
-
var TRANSITIONABLE_MARKS = /* @__PURE__ */ new Set(["bar", "line", "area", "point"]);
|
|
5197
|
-
function canTransition(args) {
|
|
5198
|
-
const { prevLayout, nextLayout, prevSpec, nextSpec, isFirstRender, entranceInFlight } = args;
|
|
5199
|
-
if (!prevLayout || !prevSpec || isFirstRender) return false;
|
|
5200
|
-
if (!nextLayout.animation?.update) return false;
|
|
5201
|
-
const prev = prevSpec;
|
|
5202
|
-
const next = nextSpec;
|
|
5203
|
-
if (!("mark" in prev) || !("mark" in next)) return false;
|
|
5204
|
-
const prevMark = typeof prev.mark === "string" ? prev.mark : prev.mark?.type;
|
|
5205
|
-
const nextMark = typeof next.mark === "string" ? next.mark : next.mark?.type;
|
|
5206
|
-
if (prevMark !== nextMark) return false;
|
|
5207
|
-
if (!TRANSITIONABLE_MARKS.has(prevMark)) return false;
|
|
5208
|
-
const prevEnc = prev.encoding;
|
|
5209
|
-
const nextEnc = next.encoding;
|
|
5210
|
-
if (!prevEnc || !nextEnc) return false;
|
|
5211
|
-
if (prevEnc.x?.field !== nextEnc.x?.field || prevEnc.x?.type !== nextEnc.x?.type || prevEnc.y?.field !== nextEnc.y?.field || prevEnc.y?.type !== nextEnc.y?.type || prevEnc.color?.field !== nextEnc.color?.field) {
|
|
5212
|
-
return false;
|
|
5213
|
-
}
|
|
5214
|
-
if (nextLayout.display === "sparkline") return false;
|
|
5215
|
-
if (entranceInFlight) return false;
|
|
5216
|
-
if (prevLayout.dimensions.width !== nextLayout.dimensions.width || prevLayout.dimensions.height !== nextLayout.dimensions.height) {
|
|
5217
|
-
return false;
|
|
5218
|
-
}
|
|
5219
|
-
if (nextLayout.marks.length > 500) return false;
|
|
5220
|
-
if (!hasGeometryChanged(prevLayout, nextLayout)) return false;
|
|
5221
|
-
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
5222
|
-
try {
|
|
5223
|
-
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
|
|
5224
|
-
} catch {
|
|
5225
|
-
}
|
|
5226
|
-
}
|
|
5227
|
-
return true;
|
|
5228
|
-
}
|
|
5229
|
-
function hasGeometryChanged(prevLayout, nextLayout) {
|
|
5230
|
-
const prevMarks = prevLayout.marks;
|
|
5231
|
-
const nextMarks = nextLayout.marks;
|
|
5232
|
-
if (prevMarks.length !== nextMarks.length) return true;
|
|
5233
|
-
for (let i = 0; i < prevMarks.length; i++) {
|
|
5234
|
-
const p = prevMarks[i];
|
|
5235
|
-
const n = nextMarks[i];
|
|
5236
|
-
if (p.type !== n.type || p.key !== n.key) return true;
|
|
5237
|
-
if (p.type === "rect" && n.type === "rect") {
|
|
5238
|
-
if (p.x !== n.x || p.y !== n.y || p.width !== n.width || p.height !== n.height) return true;
|
|
5239
|
-
} else if (p.type === "line" && n.type === "line") {
|
|
5240
|
-
if (p.path !== n.path || p.points.length !== n.points.length) return true;
|
|
5241
|
-
for (let j = 0; j < p.points.length; j++) {
|
|
5242
|
-
if (p.points[j].x !== n.points[j].x || p.points[j].y !== n.points[j].y) return true;
|
|
5243
|
-
}
|
|
5244
|
-
} else if (p.type === "area" && n.type === "area") {
|
|
5245
|
-
if (p.path !== n.path) return true;
|
|
5246
|
-
} else if (p.type === "point" && n.type === "point") {
|
|
5247
|
-
if (p.cx !== n.cx || p.cy !== n.cy || p.r !== n.r) return true;
|
|
5248
|
-
} else if (p.type === "rule" && n.type === "rule") {
|
|
5249
|
-
if (p.x1 !== n.x1 || p.y1 !== n.y1 || p.x2 !== n.x2 || p.y2 !== n.y2) return true;
|
|
5250
|
-
} else if (p.type === "tick" && n.type === "tick") {
|
|
5251
|
-
if (p.x !== n.x || p.y !== n.y) return true;
|
|
5252
|
-
} else if (p.type === "textMark" && n.type === "textMark") {
|
|
5253
|
-
if (p.x !== n.x || p.y !== n.y) return true;
|
|
5254
|
-
}
|
|
5255
|
-
}
|
|
5256
|
-
return false;
|
|
5257
|
-
}
|
|
5258
|
-
function geomFromMark(m2) {
|
|
5259
|
-
return { x: m2.x, y: m2.y, width: m2.width, height: m2.height };
|
|
5260
|
-
}
|
|
5261
|
-
function applyGeomToElement(el, geom, mark) {
|
|
5262
|
-
const shapeEl = el.querySelector("rect, path");
|
|
5263
|
-
if (!shapeEl) return;
|
|
5264
|
-
const sides = mark.cornerRadiusSides;
|
|
5265
|
-
const partialCorners = !!sides && (!sides.tl || !sides.tr || !sides.br || !sides.bl) && !!mark.cornerRadius;
|
|
5266
|
-
if (partialCorners && shapeEl.tagName === "path") {
|
|
5267
|
-
const tempMark = { ...mark, ...geom };
|
|
5268
|
-
shapeEl.setAttribute("d", rectPathWithCorners(tempMark, sides));
|
|
5269
|
-
} else {
|
|
5270
|
-
shapeEl.setAttribute("x", String(geom.x));
|
|
5271
|
-
shapeEl.setAttribute("y", String(geom.y));
|
|
5272
|
-
shapeEl.setAttribute("width", String(geom.width));
|
|
5273
|
-
shapeEl.setAttribute("height", String(geom.height));
|
|
5274
|
-
}
|
|
5275
|
-
}
|
|
5276
|
-
function lerpGeom(from, to, t) {
|
|
5277
|
-
return {
|
|
5278
|
-
x: from.x + (to.x - from.x) * t,
|
|
5279
|
-
y: from.y + (to.y - from.y) * t,
|
|
5280
|
-
width: from.width + (to.width - from.width) * t,
|
|
5281
|
-
height: from.height + (to.height - from.height) * t
|
|
5282
|
-
};
|
|
5283
|
-
}
|
|
5284
|
-
function lerpPoint(a2, b, t) {
|
|
5285
|
-
return { x: a2.x + (b.x - a2.x) * t, y: a2.y + (b.y - a2.y) * t };
|
|
5286
|
-
}
|
|
5287
|
-
function normalizePointArrays(prevPoints, nextPoints, prevKeys, nextKeys) {
|
|
5288
|
-
const nextKeySet = new Set(nextKeys);
|
|
5289
|
-
const survivors = prevKeys.filter((k) => nextKeySet.has(k));
|
|
5290
|
-
if (survivors.length === 0) {
|
|
5291
|
-
return [prevPoints, nextPoints];
|
|
5292
|
-
}
|
|
5293
|
-
const prevByKey = /* @__PURE__ */ new Map();
|
|
5294
|
-
for (let i = 0; i < prevKeys.length; i++) {
|
|
5295
|
-
prevByKey.set(prevKeys[i], prevPoints[i]);
|
|
5296
|
-
}
|
|
5297
|
-
const nextByKey = /* @__PURE__ */ new Map();
|
|
5298
|
-
for (let i = 0; i < nextKeys.length; i++) {
|
|
5299
|
-
nextByKey.set(nextKeys[i], nextPoints[i]);
|
|
5300
|
-
}
|
|
5301
|
-
const merged = mergeKeyOrder(prevKeys, nextKeys);
|
|
5302
|
-
const mergedIndex = /* @__PURE__ */ new Map();
|
|
5303
|
-
for (let i = 0; i < merged.length; i++) {
|
|
5304
|
-
mergedIndex.set(merged[i], i);
|
|
5305
|
-
}
|
|
5306
|
-
const fromPts = [];
|
|
5307
|
-
const toPts = [];
|
|
5308
|
-
for (const key of merged) {
|
|
5309
|
-
const inPrev = prevByKey.has(key);
|
|
5310
|
-
const inNext = nextByKey.has(key);
|
|
5311
|
-
if (inPrev && inNext) {
|
|
5312
|
-
fromPts.push(prevByKey.get(key));
|
|
5313
|
-
toPts.push(nextByKey.get(key));
|
|
5314
|
-
} else if (inNext && !inPrev) {
|
|
5315
|
-
const nextPt = nextByKey.get(key);
|
|
5316
|
-
toPts.push(nextPt);
|
|
5317
|
-
fromPts.push(findInsertionPosition(key, merged, mergedIndex, prevByKey, nextByKey, nextPt));
|
|
5318
|
-
} else if (inPrev && !inNext) {
|
|
5319
|
-
const prevPt = prevByKey.get(key);
|
|
5320
|
-
fromPts.push(prevPt);
|
|
5321
|
-
toPts.push(findRemovalPosition(key, merged, mergedIndex, prevByKey, nextByKey, prevPt));
|
|
5322
|
-
}
|
|
5323
|
-
}
|
|
5324
|
-
return [fromPts, toPts];
|
|
5325
|
-
}
|
|
5326
|
-
function mergeKeyOrder(prevKeys, nextKeys) {
|
|
5327
|
-
const nextIndex = /* @__PURE__ */ new Map();
|
|
5328
|
-
for (let i = 0; i < nextKeys.length; i++) {
|
|
5329
|
-
if (!nextIndex.has(nextKeys[i])) nextIndex.set(nextKeys[i], i);
|
|
5330
|
-
}
|
|
5331
|
-
const prevIndex = /* @__PURE__ */ new Map();
|
|
5332
|
-
for (let i = 0; i < prevKeys.length; i++) {
|
|
5333
|
-
if (!prevIndex.has(prevKeys[i])) prevIndex.set(prevKeys[i], i);
|
|
5334
|
-
}
|
|
5335
|
-
const result = [];
|
|
5336
|
-
const added = /* @__PURE__ */ new Set();
|
|
5337
|
-
let pi = 0;
|
|
5338
|
-
let ni = 0;
|
|
5339
|
-
while (pi < prevKeys.length && ni < nextKeys.length) {
|
|
5340
|
-
const pk = prevKeys[pi];
|
|
5341
|
-
const nk = nextKeys[ni];
|
|
5342
|
-
if (added.has(pk)) {
|
|
5343
|
-
pi++;
|
|
5344
|
-
continue;
|
|
5345
|
-
}
|
|
5346
|
-
if (added.has(nk)) {
|
|
5347
|
-
ni++;
|
|
5348
|
-
continue;
|
|
5349
|
-
}
|
|
5350
|
-
if (pk === nk) {
|
|
5351
|
-
result.push(pk);
|
|
5352
|
-
added.add(pk);
|
|
5353
|
-
pi++;
|
|
5354
|
-
ni++;
|
|
5355
|
-
} else {
|
|
5356
|
-
const pkInNext = nextIndex.has(pk) && nextIndex.get(pk) >= ni ? nextIndex.get(pk) : -1;
|
|
5357
|
-
const nkInPrev = prevIndex.has(nk) && prevIndex.get(nk) >= pi ? prevIndex.get(nk) : -1;
|
|
5358
|
-
if (pkInNext === -1 && nkInPrev === -1) {
|
|
5359
|
-
result.push(pk);
|
|
5360
|
-
added.add(pk);
|
|
5361
|
-
pi++;
|
|
5362
|
-
} else if (pkInNext === -1) {
|
|
5363
|
-
result.push(pk);
|
|
5364
|
-
added.add(pk);
|
|
5365
|
-
pi++;
|
|
5366
|
-
} else if (nkInPrev === -1) {
|
|
5367
|
-
result.push(nk);
|
|
5368
|
-
added.add(nk);
|
|
5369
|
-
ni++;
|
|
5370
|
-
} else {
|
|
5371
|
-
if (pkInNext <= nkInPrev) {
|
|
5372
|
-
result.push(pk);
|
|
5373
|
-
added.add(pk);
|
|
5374
|
-
pi++;
|
|
5375
|
-
} else {
|
|
5376
|
-
result.push(nk);
|
|
5377
|
-
added.add(nk);
|
|
5378
|
-
ni++;
|
|
5379
|
-
}
|
|
5380
|
-
}
|
|
5381
|
-
}
|
|
5382
|
-
}
|
|
5383
|
-
while (pi < prevKeys.length) {
|
|
5384
|
-
if (!added.has(prevKeys[pi])) {
|
|
5385
|
-
result.push(prevKeys[pi]);
|
|
5386
|
-
added.add(prevKeys[pi]);
|
|
5387
|
-
}
|
|
5388
|
-
pi++;
|
|
5389
|
-
}
|
|
5390
|
-
while (ni < nextKeys.length) {
|
|
5391
|
-
if (!added.has(nextKeys[ni])) {
|
|
5392
|
-
result.push(nextKeys[ni]);
|
|
5393
|
-
added.add(nextKeys[ni]);
|
|
5394
|
-
}
|
|
5395
|
-
ni++;
|
|
5396
|
-
}
|
|
5397
|
-
return result;
|
|
5398
|
-
}
|
|
5399
|
-
function findInsertionPosition(key, merged, mergedIndex, prevByKey, nextByKey, nextPt) {
|
|
5400
|
-
const idx = mergedIndex.get(key) ?? -1;
|
|
5401
|
-
let beforeKey = null;
|
|
5402
|
-
for (let i = idx - 1; i >= 0; i--) {
|
|
5403
|
-
if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
|
|
5404
|
-
beforeKey = merged[i];
|
|
5405
|
-
break;
|
|
5406
|
-
}
|
|
5407
|
-
}
|
|
5408
|
-
let afterKey = null;
|
|
5409
|
-
for (let i = idx + 1; i < merged.length; i++) {
|
|
5410
|
-
if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
|
|
5411
|
-
afterKey = merged[i];
|
|
5412
|
-
break;
|
|
5413
|
-
}
|
|
5414
|
-
}
|
|
5415
|
-
if (beforeKey && afterKey) {
|
|
5416
|
-
const beforeNext = nextByKey.get(beforeKey);
|
|
5417
|
-
const afterNext = nextByKey.get(afterKey);
|
|
5418
|
-
const beforePrev = prevByKey.get(beforeKey);
|
|
5419
|
-
const afterPrev = prevByKey.get(afterKey);
|
|
5420
|
-
const range = afterNext.x - beforeNext.x;
|
|
5421
|
-
const t = range === 0 ? 0.5 : (nextPt.x - beforeNext.x) / range;
|
|
5422
|
-
return lerpPoint(beforePrev, afterPrev, Math.max(0, Math.min(1, t)));
|
|
5423
|
-
}
|
|
5424
|
-
if (beforeKey) {
|
|
5425
|
-
return { ...prevByKey.get(beforeKey) };
|
|
5426
|
-
}
|
|
5427
|
-
if (afterKey) {
|
|
5428
|
-
return { ...prevByKey.get(afterKey) };
|
|
5429
|
-
}
|
|
5430
|
-
return nextPt;
|
|
5431
|
-
}
|
|
5432
|
-
function findRemovalPosition(key, merged, mergedIndex, prevByKey, nextByKey, prevPt) {
|
|
5433
|
-
const idx = mergedIndex.get(key) ?? -1;
|
|
5434
|
-
let beforeKey = null;
|
|
5435
|
-
for (let i = idx - 1; i >= 0; i--) {
|
|
5436
|
-
if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
|
|
5437
|
-
beforeKey = merged[i];
|
|
5438
|
-
break;
|
|
5439
|
-
}
|
|
5440
|
-
}
|
|
5441
|
-
let afterKey = null;
|
|
5442
|
-
for (let i = idx + 1; i < merged.length; i++) {
|
|
5443
|
-
if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
|
|
5444
|
-
afterKey = merged[i];
|
|
5445
|
-
break;
|
|
5446
|
-
}
|
|
5447
|
-
}
|
|
5448
|
-
if (beforeKey && afterKey) {
|
|
5449
|
-
const beforePrev = prevByKey.get(beforeKey);
|
|
5450
|
-
const afterPrev = prevByKey.get(afterKey);
|
|
5451
|
-
const beforeNext = nextByKey.get(beforeKey);
|
|
5452
|
-
const afterNext = nextByKey.get(afterKey);
|
|
5453
|
-
const range = afterPrev.x - beforePrev.x;
|
|
5454
|
-
const t = range === 0 ? 0.5 : (prevPt.x - beforePrev.x) / range;
|
|
5455
|
-
return lerpPoint(beforeNext, afterNext, Math.max(0, Math.min(1, t)));
|
|
5456
|
-
}
|
|
5457
|
-
if (beforeKey) {
|
|
5458
|
-
return { ...nextByKey.get(beforeKey) };
|
|
5459
|
-
}
|
|
5460
|
-
if (afterKey) {
|
|
5461
|
-
return { ...nextByKey.get(afterKey) };
|
|
5462
|
-
}
|
|
5463
|
-
return prevPt;
|
|
5464
|
-
}
|
|
5465
|
-
function interpolatePoints(fromPts, toPts, t) {
|
|
5466
|
-
const len = Math.min(fromPts.length, toPts.length);
|
|
5467
|
-
const result = new Array(len);
|
|
5468
|
-
for (let i = 0; i < len; i++) {
|
|
5469
|
-
result[i] = lerpPoint(fromPts[i], toPts[i], t);
|
|
5470
|
-
}
|
|
5471
|
-
return result;
|
|
5472
|
-
}
|
|
5473
|
-
function applyLinePath(el, points, interpolate) {
|
|
5474
|
-
const pathEl = el.querySelector("path");
|
|
5475
|
-
if (!pathEl) return;
|
|
5476
|
-
pathEl.setAttribute("d", buildLinePath(points, interpolate));
|
|
5477
|
-
}
|
|
5478
|
-
function applyAreaPaths(el, topPoints, bottomPoints, interpolate, hasStroke) {
|
|
5479
|
-
const paths = el.querySelectorAll("path");
|
|
5480
|
-
if (paths[0]) {
|
|
5481
|
-
paths[0].setAttribute("d", buildAreaPath(topPoints, bottomPoints, interpolate));
|
|
5482
|
-
}
|
|
5483
|
-
if (hasStroke && paths[1]) {
|
|
5484
|
-
paths[1].setAttribute("d", buildLinePath(topPoints, interpolate));
|
|
5485
|
-
}
|
|
5486
|
-
}
|
|
5487
|
-
function applyFinalLinePath(el, mark) {
|
|
5488
|
-
const pathEl = el.querySelector("path");
|
|
5489
|
-
if (!pathEl || !mark.path) return;
|
|
5490
|
-
pathEl.setAttribute("d", mark.path);
|
|
5491
|
-
}
|
|
5492
|
-
function applyFinalAreaPaths(el, mark) {
|
|
5493
|
-
const paths = el.querySelectorAll("path");
|
|
5494
|
-
if (paths[0]) {
|
|
5495
|
-
paths[0].setAttribute("d", mark.path);
|
|
5496
|
-
}
|
|
5497
|
-
if (paths[1] && mark.topPath) {
|
|
5498
|
-
paths[1].setAttribute("d", mark.topPath);
|
|
5499
|
-
}
|
|
5500
|
-
}
|
|
5501
|
-
function runTransition(args) {
|
|
5502
|
-
const { svg, prevLayout, nextLayout, animation, onComplete, fromSnapshot } = args;
|
|
5503
|
-
const update = animation.update;
|
|
5504
|
-
const exit = animation.exit ?? { ...EXIT_DEFAULTS };
|
|
5505
|
-
const totalMs = Math.max(update.duration, exit.duration);
|
|
5506
|
-
const enterDelay = 0.4 * update.duration;
|
|
5507
|
-
const enterDuration = update.duration - enterDelay;
|
|
5508
|
-
const marksContainer = svg.querySelector(".oc-marks");
|
|
5509
|
-
if (!marksContainer) {
|
|
5510
|
-
onComplete();
|
|
5511
|
-
return {
|
|
5512
|
-
cancel() {
|
|
5513
|
-
},
|
|
5514
|
-
get running() {
|
|
5515
|
-
return false;
|
|
5516
|
-
},
|
|
5517
|
-
snapshot() {
|
|
5518
|
-
return /* @__PURE__ */ new Map();
|
|
5519
|
-
}
|
|
5520
|
-
};
|
|
5521
|
-
}
|
|
5522
|
-
const tweens = [];
|
|
5523
|
-
const ghosts = [];
|
|
5524
|
-
const defs = svg.querySelector("defs");
|
|
5525
|
-
let ghostGradientMap = /* @__PURE__ */ new Map();
|
|
5526
|
-
if (defs) {
|
|
5527
|
-
const nextKeySet = new Set(nextLayout.marks.filter((m2) => m2.key).map((m2) => m2.key));
|
|
5528
|
-
const exitingMarks = prevLayout.marks.filter(
|
|
5529
|
-
(m2) => m2.key && !nextKeySet.has(m2.key) && "fill" in m2
|
|
5530
|
-
);
|
|
5531
|
-
const gradientMarks = exitingMarks.filter(
|
|
5532
|
-
(m2) => "fill" in m2 && isGradientDef(m2.fill)
|
|
5533
|
-
);
|
|
5534
|
-
if (gradientMarks.length > 0) {
|
|
5535
|
-
ghostGradientMap = buildGradientDefs(gradientMarks, defs);
|
|
5536
|
-
}
|
|
5537
|
-
}
|
|
5538
|
-
const hasRects = prevLayout.marks.some((m2) => m2.type === "rect") || nextLayout.marks.some((m2) => m2.type === "rect");
|
|
5539
|
-
const hasLines = prevLayout.marks.some((m2) => m2.type === "line") || nextLayout.marks.some((m2) => m2.type === "line");
|
|
5540
|
-
const hasAreas = prevLayout.marks.some((m2) => m2.type === "area") || nextLayout.marks.some((m2) => m2.type === "area");
|
|
5541
|
-
const hasPoints = prevLayout.marks.some((m2) => m2.type === "point") || nextLayout.marks.some((m2) => m2.type === "point");
|
|
5542
|
-
const hasRules = prevLayout.marks.some((m2) => m2.type === "rule") || nextLayout.marks.some((m2) => m2.type === "rule");
|
|
5543
|
-
const hasTicks = prevLayout.marks.some((m2) => m2.type === "tick") || nextLayout.marks.some((m2) => m2.type === "tick");
|
|
5544
|
-
const hasTextMarks = prevLayout.marks.some((m2) => m2.type === "textMark") || nextLayout.marks.some((m2) => m2.type === "textMark");
|
|
5545
|
-
if (hasRects) {
|
|
5546
|
-
buildRectTweens(
|
|
5547
|
-
prevLayout,
|
|
5548
|
-
nextLayout,
|
|
5549
|
-
marksContainer,
|
|
5550
|
-
tweens,
|
|
5551
|
-
ghosts,
|
|
5552
|
-
ghostGradientMap,
|
|
5553
|
-
fromSnapshot
|
|
5554
|
-
);
|
|
5555
|
-
}
|
|
5556
|
-
if (hasLines) {
|
|
5557
|
-
buildLineTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot);
|
|
5558
|
-
}
|
|
5559
|
-
if (hasAreas) {
|
|
5560
|
-
buildAreaTweens(
|
|
5561
|
-
prevLayout,
|
|
5562
|
-
nextLayout,
|
|
5563
|
-
marksContainer,
|
|
5564
|
-
tweens,
|
|
5565
|
-
ghosts,
|
|
5566
|
-
ghostGradientMap,
|
|
5567
|
-
fromSnapshot
|
|
5568
|
-
);
|
|
5569
|
-
}
|
|
5570
|
-
if (hasPoints) {
|
|
5571
|
-
buildPointTweens(
|
|
5572
|
-
prevLayout,
|
|
5573
|
-
nextLayout,
|
|
5574
|
-
marksContainer,
|
|
5575
|
-
tweens,
|
|
5576
|
-
ghosts,
|
|
5577
|
-
ghostGradientMap,
|
|
5578
|
-
fromSnapshot
|
|
5579
|
-
);
|
|
5580
|
-
}
|
|
5581
|
-
if (hasRules) {
|
|
5582
|
-
buildRuleTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot);
|
|
5583
|
-
}
|
|
5584
|
-
if (hasTicks) {
|
|
5585
|
-
buildTickTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot);
|
|
5586
|
-
}
|
|
5587
|
-
if (hasTextMarks) {
|
|
5588
|
-
buildTextMarkTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot);
|
|
5589
|
-
}
|
|
5590
|
-
buildAxisTweens(svg, prevLayout, nextLayout, tweens, ghosts);
|
|
5591
|
-
const secondaryEls = collectSecondaryElements(svg);
|
|
5592
|
-
const secondaryOriginalOpacity = secondaryEls.map((el) => el.style.opacity ?? "");
|
|
5593
|
-
for (const tw of tweens) {
|
|
5594
|
-
applyTweenState(tw, 0, update, exit, enterDelay, enterDuration);
|
|
5595
|
-
}
|
|
5596
|
-
for (const el of secondaryEls) {
|
|
5597
|
-
el.style.opacity = "0";
|
|
5598
|
-
}
|
|
5599
|
-
let rafId = null;
|
|
5600
|
-
let running = true;
|
|
5601
|
-
let startTime = null;
|
|
5602
|
-
let lastElapsed = 0;
|
|
5603
|
-
const nextLineMarks = nextLayout.marks.filter((m2) => m2.type === "line");
|
|
5604
|
-
const nextAreaMarks = nextLayout.marks.filter((m2) => m2.type === "area");
|
|
5605
|
-
function tick(now2) {
|
|
5606
|
-
if (!running) return;
|
|
5607
|
-
if (startTime === null) startTime = now2;
|
|
5608
|
-
const elapsed = now2 - startTime;
|
|
5609
|
-
lastElapsed = elapsed;
|
|
5610
|
-
const tGlobal = Math.min(elapsed / totalMs, 1);
|
|
5611
|
-
for (const tw of tweens) {
|
|
5612
|
-
applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration);
|
|
5613
|
-
}
|
|
5614
|
-
applySecondaryOpacity(secondaryEls, elapsed, enterDelay, enterDuration);
|
|
5615
|
-
if (tGlobal >= 1) {
|
|
5616
|
-
finish();
|
|
5617
|
-
} else {
|
|
5618
|
-
rafId = requestAnimationFrame(tick);
|
|
5619
|
-
}
|
|
5620
|
-
}
|
|
5621
|
-
function finish() {
|
|
5622
|
-
if (!running) return;
|
|
5623
|
-
running = false;
|
|
5624
|
-
rafId = null;
|
|
5625
|
-
snapToFinal();
|
|
5626
|
-
removeGhosts();
|
|
5627
|
-
for (let i = 0; i < secondaryEls.length; i++) {
|
|
5628
|
-
secondaryEls[i].style.opacity = secondaryOriginalOpacity[i];
|
|
5629
|
-
}
|
|
5630
|
-
onComplete();
|
|
5631
|
-
}
|
|
5632
|
-
function snapToFinal() {
|
|
5633
|
-
for (const tw of tweens) {
|
|
5634
|
-
snapTweenToFinal(tw);
|
|
5635
|
-
}
|
|
5636
|
-
for (const mark of nextLineMarks) {
|
|
5637
|
-
if (!mark.key) continue;
|
|
5638
|
-
const el = marksContainer.querySelector(`[data-key="${mark.key}"]`);
|
|
5639
|
-
if (el) applyFinalLinePath(el, mark);
|
|
5640
|
-
}
|
|
5641
|
-
for (const mark of nextAreaMarks) {
|
|
5642
|
-
if (!mark.key) continue;
|
|
5643
|
-
const el = marksContainer.querySelector(`[data-key="${mark.key}"]`);
|
|
5644
|
-
if (el) applyFinalAreaPaths(el, mark);
|
|
5645
|
-
}
|
|
5646
|
-
}
|
|
5647
|
-
function removeGhosts() {
|
|
5648
|
-
for (const ghost of ghosts) {
|
|
5649
|
-
ghost.parentNode?.removeChild(ghost);
|
|
5650
|
-
}
|
|
5651
|
-
}
|
|
5652
|
-
function captureSnapshot() {
|
|
5653
|
-
const snap = /* @__PURE__ */ new Map();
|
|
5654
|
-
const tUpdate = Math.min(lastElapsed / update.duration, 1);
|
|
5655
|
-
const easedUpdate = cubicOut(tUpdate);
|
|
5656
|
-
for (const tw of tweens) {
|
|
5657
|
-
if (tw.kind !== "update") continue;
|
|
5658
|
-
const key = getKeyForTween(tw);
|
|
5659
|
-
if (!key) continue;
|
|
5660
|
-
switch (tw.tweenType) {
|
|
5661
|
-
case "rect": {
|
|
5662
|
-
const g = lerpGeom(tw.from, tw.to, easedUpdate);
|
|
5663
|
-
snap.set(key, { type: "rect", ...g });
|
|
5664
|
-
break;
|
|
5665
|
-
}
|
|
5666
|
-
case "point": {
|
|
5667
|
-
const cx = tw.fromCx + (tw.toCx - tw.fromCx) * easedUpdate;
|
|
5668
|
-
const cy = tw.fromCy + (tw.toCy - tw.fromCy) * easedUpdate;
|
|
5669
|
-
const entry = { type: "point", cx, cy };
|
|
5670
|
-
if (tw.fromR !== void 0 && tw.toR !== void 0) {
|
|
5671
|
-
entry.r = tw.fromR + (tw.toR - tw.fromR) * easedUpdate;
|
|
5672
|
-
}
|
|
5673
|
-
snap.set(key, entry);
|
|
5674
|
-
break;
|
|
5675
|
-
}
|
|
5676
|
-
case "rule":
|
|
5677
|
-
case "tick": {
|
|
5678
|
-
snap.set(key, {
|
|
5679
|
-
type: tw.tweenType,
|
|
5680
|
-
x1: tw.fromX1 + (tw.toX1 - tw.fromX1) * easedUpdate,
|
|
5681
|
-
y1: tw.fromY1 + (tw.toY1 - tw.fromY1) * easedUpdate,
|
|
5682
|
-
x2: tw.fromX2 + (tw.toX2 - tw.fromX2) * easedUpdate,
|
|
5683
|
-
y2: tw.fromY2 + (tw.toY2 - tw.fromY2) * easedUpdate
|
|
5684
|
-
});
|
|
5685
|
-
break;
|
|
5686
|
-
}
|
|
5687
|
-
case "textMark": {
|
|
5688
|
-
snap.set(key, {
|
|
5689
|
-
type: "textMark",
|
|
5690
|
-
x: tw.fromX + (tw.toX - tw.fromX) * easedUpdate,
|
|
5691
|
-
y: tw.fromY + (tw.toY - tw.fromY) * easedUpdate
|
|
5692
|
-
});
|
|
5693
|
-
break;
|
|
5694
|
-
}
|
|
5695
|
-
case "line": {
|
|
5696
|
-
const pts = interpolatePoints(tw.fromPts, tw.toPts, easedUpdate);
|
|
5697
|
-
const d = buildLinePath(pts, tw.interpolate);
|
|
5698
|
-
snap.set(key, { type: "line", d });
|
|
5699
|
-
break;
|
|
5700
|
-
}
|
|
5701
|
-
case "area": {
|
|
5702
|
-
const top = interpolatePoints(tw.fromTop, tw.toTop, easedUpdate);
|
|
5703
|
-
const bottom = interpolatePoints(tw.fromBottom, tw.toBottom, easedUpdate);
|
|
5704
|
-
const d = buildAreaPath(top, bottom, tw.interpolate);
|
|
5705
|
-
let topD;
|
|
5706
|
-
if (tw.hasStroke) {
|
|
5707
|
-
topD = buildLinePath(top, tw.interpolate);
|
|
5708
|
-
}
|
|
5709
|
-
snap.set(key, { type: "area", d, topD });
|
|
5710
|
-
break;
|
|
5711
|
-
}
|
|
5712
|
-
}
|
|
5713
|
-
}
|
|
5714
|
-
return snap;
|
|
5715
|
-
}
|
|
5716
|
-
rafId = requestAnimationFrame(tick);
|
|
5717
|
-
return {
|
|
5718
|
-
cancel() {
|
|
5719
|
-
if (!running) return;
|
|
5720
|
-
running = false;
|
|
5721
|
-
if (rafId !== null) {
|
|
5722
|
-
cancelAnimationFrame(rafId);
|
|
5723
|
-
rafId = null;
|
|
5724
|
-
}
|
|
5725
|
-
snapToFinal();
|
|
5726
|
-
removeGhosts();
|
|
5727
|
-
for (let i = 0; i < secondaryEls.length; i++) {
|
|
5728
|
-
secondaryEls[i].style.opacity = secondaryOriginalOpacity[i];
|
|
5729
|
-
}
|
|
5730
|
-
},
|
|
5731
|
-
get running() {
|
|
5732
|
-
return running;
|
|
5733
|
-
},
|
|
5734
|
-
snapshot() {
|
|
5735
|
-
return captureSnapshot();
|
|
5736
|
-
}
|
|
5737
|
-
};
|
|
5738
|
-
}
|
|
5739
|
-
function collectSecondaryElements(svg) {
|
|
5740
|
-
const els = [];
|
|
5741
|
-
for (const el of svg.querySelectorAll(".oc-annotation")) {
|
|
5742
|
-
els.push(el);
|
|
5743
|
-
}
|
|
5744
|
-
const epLabels = svg.querySelector(".oc-endpoint-labels");
|
|
5745
|
-
if (epLabels) els.push(epLabels);
|
|
5746
|
-
const markLabels = svg.querySelector(".oc-mark-labels");
|
|
5747
|
-
if (markLabels) els.push(markLabels);
|
|
5748
|
-
return els;
|
|
5749
|
-
}
|
|
5750
|
-
function applySecondaryOpacity(els, elapsed, enterDelay, enterDuration) {
|
|
5751
|
-
if (els.length === 0) return;
|
|
5752
|
-
let t;
|
|
5753
|
-
if (elapsed < enterDelay) {
|
|
5754
|
-
t = 0;
|
|
5755
|
-
} else {
|
|
5756
|
-
t = Math.min((elapsed - enterDelay) / enterDuration, 1);
|
|
5757
|
-
}
|
|
5758
|
-
const opacity = String(cubicOut(t));
|
|
5759
|
-
for (const el of els) {
|
|
5760
|
-
el.style.opacity = opacity;
|
|
5761
|
-
}
|
|
5762
|
-
}
|
|
5763
|
-
function getKeyForTween(tw) {
|
|
5764
|
-
if ("ghost" in tw && tw.ghost) return null;
|
|
5765
|
-
return tw.el.getAttribute("data-key");
|
|
5766
|
-
}
|
|
5767
|
-
function resolveLineElement(el) {
|
|
5768
|
-
return el.tagName === "line" ? el : el.querySelector("line") ?? el;
|
|
5769
|
-
}
|
|
5770
|
-
function applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration) {
|
|
5771
|
-
let tLocal;
|
|
5772
|
-
if (tw.kind === "exit") {
|
|
5773
|
-
tLocal = Math.min(elapsed / exit.duration, 1);
|
|
5774
|
-
} else if (tw.kind === "enter") {
|
|
5775
|
-
if (elapsed < enterDelay) {
|
|
5776
|
-
tLocal = 0;
|
|
5777
|
-
} else {
|
|
5778
|
-
tLocal = Math.min((elapsed - enterDelay) / enterDuration, 1);
|
|
5779
|
-
}
|
|
5780
|
-
} else {
|
|
5781
|
-
tLocal = Math.min(elapsed / update.duration, 1);
|
|
5782
|
-
}
|
|
5783
|
-
const eased = cubicOut(tLocal);
|
|
5784
|
-
switch (tw.tweenType) {
|
|
5785
|
-
case "rect": {
|
|
5786
|
-
const geom = lerpGeom(tw.from, tw.to, eased);
|
|
5787
|
-
applyGeomToElement(tw.el, geom, tw.mark);
|
|
5788
|
-
break;
|
|
5789
|
-
}
|
|
5790
|
-
case "line": {
|
|
5791
|
-
const pts = interpolatePoints(tw.fromPts, tw.toPts, eased);
|
|
5792
|
-
applyLinePath(tw.el, pts, tw.interpolate);
|
|
5793
|
-
break;
|
|
5794
|
-
}
|
|
5795
|
-
case "area": {
|
|
5796
|
-
const top = interpolatePoints(tw.fromTop, tw.toTop, eased);
|
|
5797
|
-
const bottom = interpolatePoints(tw.fromBottom, tw.toBottom, eased);
|
|
5798
|
-
applyAreaPaths(tw.el, top, bottom, tw.interpolate, tw.hasStroke);
|
|
5799
|
-
break;
|
|
5800
|
-
}
|
|
5801
|
-
case "point": {
|
|
5802
|
-
const cx = tw.fromCx + (tw.toCx - tw.fromCx) * eased;
|
|
5803
|
-
const cy = tw.fromCy + (tw.toCy - tw.fromCy) * eased;
|
|
5804
|
-
tw.el.setAttribute("cx", String(cx));
|
|
5805
|
-
tw.el.setAttribute("cy", String(cy));
|
|
5806
|
-
if (tw.fromR !== void 0 && tw.toR !== void 0) {
|
|
5807
|
-
const r = tw.fromR + (tw.toR - tw.fromR) * eased;
|
|
5808
|
-
tw.el.setAttribute("r", String(r));
|
|
5809
|
-
}
|
|
5810
|
-
break;
|
|
5811
|
-
}
|
|
5812
|
-
case "rule":
|
|
5813
|
-
case "tick": {
|
|
5814
|
-
const lineEl = resolveLineElement(tw.el);
|
|
5815
|
-
lineEl.setAttribute("x1", String(tw.fromX1 + (tw.toX1 - tw.fromX1) * eased));
|
|
5816
|
-
lineEl.setAttribute("y1", String(tw.fromY1 + (tw.toY1 - tw.fromY1) * eased));
|
|
5817
|
-
lineEl.setAttribute("x2", String(tw.fromX2 + (tw.toX2 - tw.fromX2) * eased));
|
|
5818
|
-
lineEl.setAttribute("y2", String(tw.fromY2 + (tw.toY2 - tw.fromY2) * eased));
|
|
5819
|
-
break;
|
|
5820
|
-
}
|
|
5821
|
-
case "textMark": {
|
|
5822
|
-
const x3 = tw.fromX + (tw.toX - tw.fromX) * eased;
|
|
5823
|
-
const y3 = tw.fromY + (tw.toY - tw.fromY) * eased;
|
|
5824
|
-
tw.el.setAttribute("x", String(x3));
|
|
5825
|
-
tw.el.setAttribute("y", String(y3));
|
|
5826
|
-
break;
|
|
5827
|
-
}
|
|
5828
|
-
case "axisTick": {
|
|
5829
|
-
if (tw.crossfade) {
|
|
5830
|
-
break;
|
|
5831
|
-
}
|
|
5832
|
-
const pos = tw.fromPos + (tw.toPos - tw.fromPos) * eased;
|
|
5833
|
-
if (tw.posAttr === "x") {
|
|
5834
|
-
tw.el.setAttribute("x", String(pos));
|
|
5835
|
-
} else {
|
|
5836
|
-
tw.el.setAttribute("y", String(pos));
|
|
5837
|
-
}
|
|
5838
|
-
break;
|
|
5839
|
-
}
|
|
5840
|
-
case "gridline": {
|
|
5841
|
-
const pos = tw.fromPos + (tw.toPos - tw.fromPos) * eased;
|
|
5842
|
-
if (tw.orientation === "y") {
|
|
5843
|
-
tw.el.setAttribute("y1", String(pos));
|
|
5844
|
-
tw.el.setAttribute("y2", String(pos));
|
|
5845
|
-
} else {
|
|
5846
|
-
tw.el.setAttribute("x1", String(pos));
|
|
5847
|
-
tw.el.setAttribute("x2", String(pos));
|
|
5848
|
-
}
|
|
5849
|
-
break;
|
|
5850
|
-
}
|
|
5851
|
-
}
|
|
5852
|
-
if (tw.fromOpacity !== void 0 && tw.toOpacity !== void 0) {
|
|
5853
|
-
const opacity = tw.fromOpacity + (tw.toOpacity - tw.fromOpacity) * eased;
|
|
5854
|
-
tw.el.style.opacity = String(opacity);
|
|
5855
|
-
}
|
|
5856
|
-
}
|
|
5857
|
-
function snapTweenToFinal(tw) {
|
|
5858
|
-
switch (tw.tweenType) {
|
|
5859
|
-
case "rect":
|
|
5860
|
-
applyGeomToElement(tw.el, tw.to, tw.mark);
|
|
5861
|
-
break;
|
|
5862
|
-
case "line":
|
|
5863
|
-
applyLinePath(tw.el, tw.toPts, tw.interpolate);
|
|
5864
|
-
break;
|
|
5865
|
-
case "area":
|
|
5866
|
-
applyAreaPaths(tw.el, tw.toTop, tw.toBottom, tw.interpolate, tw.hasStroke);
|
|
5867
|
-
break;
|
|
5868
|
-
case "point":
|
|
5869
|
-
tw.el.setAttribute("cx", String(tw.toCx));
|
|
5870
|
-
tw.el.setAttribute("cy", String(tw.toCy));
|
|
5871
|
-
if (tw.toR !== void 0) {
|
|
5872
|
-
tw.el.setAttribute("r", String(tw.toR));
|
|
5873
|
-
}
|
|
5874
|
-
break;
|
|
5875
|
-
case "rule":
|
|
5876
|
-
case "tick": {
|
|
5877
|
-
const lineEl = resolveLineElement(tw.el);
|
|
5878
|
-
lineEl.setAttribute("x1", String(tw.toX1));
|
|
5879
|
-
lineEl.setAttribute("y1", String(tw.toY1));
|
|
5880
|
-
lineEl.setAttribute("x2", String(tw.toX2));
|
|
5881
|
-
lineEl.setAttribute("y2", String(tw.toY2));
|
|
5882
|
-
break;
|
|
5883
|
-
}
|
|
5884
|
-
case "textMark":
|
|
5885
|
-
tw.el.setAttribute("x", String(tw.toX));
|
|
5886
|
-
tw.el.setAttribute("y", String(tw.toY));
|
|
5887
|
-
break;
|
|
5888
|
-
case "axisTick":
|
|
5889
|
-
if (!tw.crossfade) {
|
|
5890
|
-
if (tw.posAttr === "x") {
|
|
5891
|
-
tw.el.setAttribute("x", String(tw.toPos));
|
|
5892
|
-
} else {
|
|
5893
|
-
tw.el.setAttribute("y", String(tw.toPos));
|
|
5894
|
-
}
|
|
5895
|
-
}
|
|
5896
|
-
break;
|
|
5897
|
-
case "gridline":
|
|
5898
|
-
if (tw.orientation === "y") {
|
|
5899
|
-
tw.el.setAttribute("y1", String(tw.toPos));
|
|
5900
|
-
tw.el.setAttribute("y2", String(tw.toPos));
|
|
5901
|
-
} else {
|
|
5902
|
-
tw.el.setAttribute("x1", String(tw.toPos));
|
|
5903
|
-
tw.el.setAttribute("x2", String(tw.toPos));
|
|
5904
|
-
}
|
|
5905
|
-
break;
|
|
5906
|
-
}
|
|
5907
|
-
if (tw.toOpacity !== void 0) {
|
|
5908
|
-
tw.el.style.opacity = String(tw.toOpacity);
|
|
5909
|
-
}
|
|
5910
|
-
if (tw.tweenType === "point" && tw.suppressedOpacity !== void 0) {
|
|
5911
|
-
tw.el.setAttribute("opacity", tw.suppressedOpacity);
|
|
5912
|
-
}
|
|
5913
|
-
}
|
|
5914
|
-
function resolveGhostGradientFill(mark, ghostGradientMap) {
|
|
5915
|
-
if (!mark.fill || typeof mark.fill === "string" || ghostGradientMap.size === 0) return mark;
|
|
5916
|
-
const resolved = resolveMarkFill(mark.fill, ghostGradientMap);
|
|
5917
|
-
return { ...mark, fill: resolved };
|
|
5918
|
-
}
|
|
5919
|
-
function buildRectTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, ghostGradientMap, fromSnapshot) {
|
|
5920
|
-
const prevRects = prevLayout.marks.filter((m2) => m2.type === "rect");
|
|
5921
|
-
const nextRects = nextLayout.marks.filter((m2) => m2.type === "rect");
|
|
5922
|
-
const prevByKey = /* @__PURE__ */ new Map();
|
|
5923
|
-
for (const m2 of prevRects) {
|
|
5924
|
-
if (m2.key) prevByKey.set(m2.key, m2);
|
|
5925
|
-
}
|
|
5926
|
-
const nextByKey = /* @__PURE__ */ new Map();
|
|
5927
|
-
for (const m2 of nextRects) {
|
|
5928
|
-
if (m2.key) nextByKey.set(m2.key, m2);
|
|
5929
|
-
}
|
|
5930
|
-
for (const [key, next] of nextByKey) {
|
|
5931
|
-
const prev = prevByKey.get(key);
|
|
5932
|
-
if (!prev) continue;
|
|
5933
|
-
const el = marksContainer.querySelector(`[data-key="${key}"]`);
|
|
5934
|
-
if (!el) continue;
|
|
5935
|
-
let fromGeom = geomFromMark(prev);
|
|
5936
|
-
const snap = fromSnapshot?.get(key);
|
|
5937
|
-
if (snap && snap.type === "rect") {
|
|
5938
|
-
fromGeom = { x: snap.x, y: snap.y, width: snap.width, height: snap.height };
|
|
5939
|
-
}
|
|
5940
|
-
tweens.push({
|
|
5941
|
-
tweenType: "rect",
|
|
5942
|
-
kind: "update",
|
|
5943
|
-
el,
|
|
5944
|
-
from: fromGeom,
|
|
5945
|
-
to: geomFromMark(next),
|
|
5946
|
-
mark: next
|
|
5947
|
-
});
|
|
5948
|
-
}
|
|
5949
|
-
for (const [key, next] of nextByKey) {
|
|
5950
|
-
if (prevByKey.has(key)) continue;
|
|
5951
|
-
const el = marksContainer.querySelector(`[data-key="${key}"]`);
|
|
5952
|
-
if (!el) continue;
|
|
5953
|
-
let fromGeom;
|
|
5954
|
-
if (next.orient === "horizontal") {
|
|
5955
|
-
fromGeom = { x: next.x, y: next.y, width: 0, height: next.height };
|
|
5956
|
-
} else {
|
|
5957
|
-
fromGeom = { x: next.x, y: next.y + next.height, width: next.width, height: 0 };
|
|
5958
|
-
}
|
|
5959
|
-
tweens.push({
|
|
5960
|
-
tweenType: "rect",
|
|
5961
|
-
kind: "enter",
|
|
5962
|
-
el,
|
|
5963
|
-
from: fromGeom,
|
|
5964
|
-
to: geomFromMark(next),
|
|
5965
|
-
mark: next
|
|
5966
|
-
});
|
|
5967
|
-
}
|
|
5968
|
-
for (const [key, prev] of prevByKey) {
|
|
5969
|
-
if (nextByKey.has(key)) continue;
|
|
5970
|
-
const ghostMark = resolveGhostGradientFill(prev, ghostGradientMap);
|
|
5971
|
-
const ghost = renderSingleMark(ghostMark, 0);
|
|
5972
|
-
if (!ghost) continue;
|
|
5973
|
-
ghost.classList.add("oc-ghost");
|
|
5974
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
5975
|
-
ghost.setAttribute("pointer-events", "none");
|
|
5976
|
-
ghost.removeAttribute("data-key");
|
|
5977
|
-
marksContainer.appendChild(ghost);
|
|
5978
|
-
ghosts.push(ghost);
|
|
5979
|
-
let toGeom;
|
|
5980
|
-
if (prev.orient === "horizontal") {
|
|
5981
|
-
toGeom = { x: prev.x, y: prev.y, width: 0, height: prev.height };
|
|
5982
|
-
} else {
|
|
5983
|
-
toGeom = { x: prev.x, y: prev.y + prev.height, width: prev.width, height: 0 };
|
|
5984
|
-
}
|
|
5985
|
-
tweens.push({
|
|
5986
|
-
tweenType: "rect",
|
|
5987
|
-
kind: "exit",
|
|
5988
|
-
el: ghost,
|
|
5989
|
-
from: geomFromMark(prev),
|
|
5990
|
-
to: toGeom,
|
|
5991
|
-
mark: prev,
|
|
5992
|
-
ghost,
|
|
5993
|
-
fromOpacity: 1,
|
|
5994
|
-
toOpacity: 0
|
|
5995
|
-
});
|
|
5996
|
-
}
|
|
5997
|
-
}
|
|
5998
|
-
function buildLineTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot) {
|
|
5999
|
-
const prevLines = prevLayout.marks.filter((m2) => m2.type === "line");
|
|
6000
|
-
const nextLines = nextLayout.marks.filter((m2) => m2.type === "line");
|
|
6001
|
-
const prevByKey = /* @__PURE__ */ new Map();
|
|
6002
|
-
for (const m2 of prevLines) {
|
|
6003
|
-
if (m2.key) prevByKey.set(m2.key, m2);
|
|
6004
|
-
}
|
|
6005
|
-
const nextByKey = /* @__PURE__ */ new Map();
|
|
6006
|
-
for (const m2 of nextLines) {
|
|
6007
|
-
if (m2.key) nextByKey.set(m2.key, m2);
|
|
6008
|
-
}
|
|
6009
|
-
for (const [key, next] of nextByKey) {
|
|
6010
|
-
const prev = prevByKey.get(key);
|
|
6011
|
-
if (!prev) continue;
|
|
6012
|
-
const el = marksContainer.querySelector(`[data-key="${key}"]`);
|
|
6013
|
-
if (!el) continue;
|
|
6014
|
-
const snap = fromSnapshot?.get(key);
|
|
6015
|
-
if (snap && snap.type === "line") {
|
|
6016
|
-
const ghost = el.cloneNode(true);
|
|
6017
|
-
ghost.classList.add("oc-ghost");
|
|
6018
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6019
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6020
|
-
ghost.removeAttribute("data-key");
|
|
6021
|
-
const ghostPath = ghost.querySelector("path");
|
|
6022
|
-
if (ghostPath) ghostPath.setAttribute("d", snap.d);
|
|
6023
|
-
marksContainer.appendChild(ghost);
|
|
6024
|
-
ghosts.push(ghost);
|
|
6025
|
-
tweens.push({
|
|
6026
|
-
tweenType: "line",
|
|
6027
|
-
kind: "exit",
|
|
6028
|
-
el: ghost,
|
|
6029
|
-
fromPts: next.points,
|
|
6030
|
-
// placeholder, not used for path
|
|
6031
|
-
toPts: next.points,
|
|
6032
|
-
finalPoints: next.points,
|
|
6033
|
-
interpolate: next.interpolate,
|
|
6034
|
-
ghost,
|
|
6035
|
-
fromOpacity: 1,
|
|
6036
|
-
toOpacity: 0
|
|
6037
|
-
});
|
|
6038
|
-
tweens.push({
|
|
6039
|
-
tweenType: "line",
|
|
6040
|
-
kind: "enter",
|
|
6041
|
-
el,
|
|
6042
|
-
fromPts: next.points,
|
|
6043
|
-
toPts: next.points,
|
|
6044
|
-
finalPoints: next.points,
|
|
6045
|
-
interpolate: next.interpolate,
|
|
6046
|
-
fromOpacity: 0,
|
|
6047
|
-
toOpacity: 1
|
|
6048
|
-
});
|
|
6049
|
-
continue;
|
|
6050
|
-
}
|
|
6051
|
-
const prevPKs = prev.pointKeys ?? prev.points.map((_, i) => `${i}`);
|
|
6052
|
-
const nextPKs = next.pointKeys ?? next.points.map((_, i) => `${i}`);
|
|
6053
|
-
const prevKeySet = new Set(prevPKs);
|
|
6054
|
-
const hasSurvivors = nextPKs.some((k) => prevKeySet.has(k));
|
|
6055
|
-
if (!hasSurvivors) {
|
|
6056
|
-
const ghost = renderSingleMark(prev, 0);
|
|
6057
|
-
if (ghost) {
|
|
6058
|
-
ghost.classList.add("oc-ghost");
|
|
6059
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6060
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6061
|
-
ghost.removeAttribute("data-key");
|
|
6062
|
-
marksContainer.appendChild(ghost);
|
|
6063
|
-
ghosts.push(ghost);
|
|
6064
|
-
tweens.push({
|
|
6065
|
-
tweenType: "line",
|
|
6066
|
-
kind: "exit",
|
|
6067
|
-
el: ghost,
|
|
6068
|
-
fromPts: prev.points,
|
|
6069
|
-
toPts: prev.points,
|
|
6070
|
-
finalPoints: prev.points,
|
|
6071
|
-
interpolate: prev.interpolate,
|
|
6072
|
-
ghost,
|
|
6073
|
-
fromOpacity: 1,
|
|
6074
|
-
toOpacity: 0
|
|
6075
|
-
});
|
|
6076
|
-
}
|
|
6077
|
-
tweens.push({
|
|
6078
|
-
tweenType: "line",
|
|
6079
|
-
kind: "enter",
|
|
6080
|
-
el,
|
|
6081
|
-
fromPts: next.points,
|
|
6082
|
-
toPts: next.points,
|
|
6083
|
-
finalPoints: next.points,
|
|
6084
|
-
interpolate: next.interpolate,
|
|
6085
|
-
fromOpacity: 0,
|
|
6086
|
-
toOpacity: 1
|
|
6087
|
-
});
|
|
6088
|
-
} else {
|
|
6089
|
-
const [fromPts, toPts] = normalizePointArrays(prev.points, next.points, prevPKs, nextPKs);
|
|
6090
|
-
tweens.push({
|
|
6091
|
-
tweenType: "line",
|
|
6092
|
-
kind: "update",
|
|
6093
|
-
el,
|
|
6094
|
-
fromPts,
|
|
6095
|
-
toPts,
|
|
6096
|
-
finalPoints: next.points,
|
|
6097
|
-
interpolate: next.interpolate
|
|
6098
|
-
});
|
|
6099
|
-
}
|
|
6100
|
-
}
|
|
6101
|
-
for (const [key, next] of nextByKey) {
|
|
6102
|
-
if (prevByKey.has(key)) continue;
|
|
6103
|
-
const el = marksContainer.querySelector(`[data-key="${key}"]`);
|
|
6104
|
-
if (!el) continue;
|
|
6105
|
-
tweens.push({
|
|
6106
|
-
tweenType: "line",
|
|
6107
|
-
kind: "enter",
|
|
6108
|
-
el,
|
|
6109
|
-
fromPts: next.points,
|
|
6110
|
-
toPts: next.points,
|
|
6111
|
-
finalPoints: next.points,
|
|
6112
|
-
interpolate: next.interpolate,
|
|
6113
|
-
fromOpacity: 0,
|
|
6114
|
-
toOpacity: 1
|
|
6115
|
-
});
|
|
6116
|
-
}
|
|
6117
|
-
for (const [key, prev] of prevByKey) {
|
|
6118
|
-
if (nextByKey.has(key)) continue;
|
|
6119
|
-
const ghost = renderSingleMark(prev, 0);
|
|
6120
|
-
if (!ghost) continue;
|
|
6121
|
-
ghost.classList.add("oc-ghost");
|
|
6122
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6123
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6124
|
-
ghost.removeAttribute("data-key");
|
|
6125
|
-
marksContainer.appendChild(ghost);
|
|
6126
|
-
ghosts.push(ghost);
|
|
6127
|
-
tweens.push({
|
|
6128
|
-
tweenType: "line",
|
|
6129
|
-
kind: "exit",
|
|
6130
|
-
el: ghost,
|
|
6131
|
-
fromPts: prev.points,
|
|
6132
|
-
toPts: prev.points,
|
|
6133
|
-
finalPoints: prev.points,
|
|
6134
|
-
interpolate: prev.interpolate,
|
|
6135
|
-
ghost,
|
|
6136
|
-
fromOpacity: 1,
|
|
6137
|
-
toOpacity: 0
|
|
6138
|
-
});
|
|
6139
|
-
}
|
|
6140
|
-
}
|
|
6141
|
-
function buildAreaTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, ghostGradientMap, fromSnapshot) {
|
|
6142
|
-
const prevAreas = prevLayout.marks.filter((m2) => m2.type === "area");
|
|
6143
|
-
const nextAreas = nextLayout.marks.filter((m2) => m2.type === "area");
|
|
6144
|
-
const prevByKey = /* @__PURE__ */ new Map();
|
|
6145
|
-
for (const m2 of prevAreas) {
|
|
6146
|
-
if (m2.key) prevByKey.set(m2.key, m2);
|
|
6147
|
-
}
|
|
6148
|
-
const nextByKey = /* @__PURE__ */ new Map();
|
|
6149
|
-
for (const m2 of nextAreas) {
|
|
6150
|
-
if (m2.key) nextByKey.set(m2.key, m2);
|
|
6151
|
-
}
|
|
6152
|
-
for (const [key, next] of nextByKey) {
|
|
6153
|
-
const prev = prevByKey.get(key);
|
|
6154
|
-
if (!prev) continue;
|
|
6155
|
-
const el = marksContainer.querySelector(`[data-key="${key}"]`);
|
|
6156
|
-
if (!el) continue;
|
|
6157
|
-
const snap = fromSnapshot?.get(key);
|
|
6158
|
-
if (snap && snap.type === "area") {
|
|
6159
|
-
const ghost = el.cloneNode(true);
|
|
6160
|
-
ghost.classList.add("oc-ghost");
|
|
6161
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6162
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6163
|
-
ghost.removeAttribute("data-key");
|
|
6164
|
-
const ghostPaths = ghost.querySelectorAll("path");
|
|
6165
|
-
if (ghostPaths[0]) ghostPaths[0].setAttribute("d", snap.d);
|
|
6166
|
-
if (ghostPaths[1] && snap.topD) ghostPaths[1].setAttribute("d", snap.topD);
|
|
6167
|
-
marksContainer.appendChild(ghost);
|
|
6168
|
-
ghosts.push(ghost);
|
|
6169
|
-
tweens.push({
|
|
6170
|
-
tweenType: "area",
|
|
6171
|
-
kind: "exit",
|
|
6172
|
-
el: ghost,
|
|
6173
|
-
fromTop: next.topPoints,
|
|
6174
|
-
toTop: next.topPoints,
|
|
6175
|
-
fromBottom: next.bottomPoints,
|
|
6176
|
-
toBottom: next.bottomPoints,
|
|
6177
|
-
finalTopPoints: next.topPoints,
|
|
6178
|
-
finalBottomPoints: next.bottomPoints,
|
|
6179
|
-
interpolate: next.interpolate,
|
|
6180
|
-
hasStroke: !!next.stroke && !!next.topPath,
|
|
6181
|
-
ghost,
|
|
6182
|
-
fromOpacity: 1,
|
|
6183
|
-
toOpacity: 0
|
|
6184
|
-
});
|
|
6185
|
-
tweens.push({
|
|
6186
|
-
tweenType: "area",
|
|
6187
|
-
kind: "enter",
|
|
6188
|
-
el,
|
|
6189
|
-
fromTop: next.topPoints,
|
|
6190
|
-
toTop: next.topPoints,
|
|
6191
|
-
fromBottom: next.bottomPoints,
|
|
6192
|
-
toBottom: next.bottomPoints,
|
|
6193
|
-
finalTopPoints: next.topPoints,
|
|
6194
|
-
finalBottomPoints: next.bottomPoints,
|
|
6195
|
-
interpolate: next.interpolate,
|
|
6196
|
-
hasStroke: !!next.stroke && !!next.topPath,
|
|
6197
|
-
fromOpacity: 0,
|
|
6198
|
-
toOpacity: 1
|
|
6199
|
-
});
|
|
6200
|
-
continue;
|
|
6201
|
-
}
|
|
6202
|
-
const prevPKs = prev.pointKeys ?? prev.topPoints.map((_, i) => `${i}`);
|
|
6203
|
-
const nextPKs = next.pointKeys ?? next.topPoints.map((_, i) => `${i}`);
|
|
6204
|
-
const prevKeySet = new Set(prevPKs);
|
|
6205
|
-
const hasSurvivors = nextPKs.some((k) => prevKeySet.has(k));
|
|
6206
|
-
if (!hasSurvivors) {
|
|
6207
|
-
const ghost = renderSingleMark(prev, 0);
|
|
6208
|
-
if (ghost) {
|
|
6209
|
-
ghost.classList.add("oc-ghost");
|
|
6210
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6211
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6212
|
-
ghost.removeAttribute("data-key");
|
|
6213
|
-
marksContainer.appendChild(ghost);
|
|
6214
|
-
ghosts.push(ghost);
|
|
6215
|
-
tweens.push({
|
|
6216
|
-
tweenType: "area",
|
|
6217
|
-
kind: "exit",
|
|
6218
|
-
el: ghost,
|
|
6219
|
-
fromTop: prev.topPoints,
|
|
6220
|
-
toTop: prev.topPoints,
|
|
6221
|
-
fromBottom: prev.bottomPoints,
|
|
6222
|
-
toBottom: prev.bottomPoints,
|
|
6223
|
-
finalTopPoints: prev.topPoints,
|
|
6224
|
-
finalBottomPoints: prev.bottomPoints,
|
|
6225
|
-
interpolate: prev.interpolate,
|
|
6226
|
-
hasStroke: !!prev.stroke && !!prev.topPath,
|
|
6227
|
-
ghost,
|
|
6228
|
-
fromOpacity: 1,
|
|
6229
|
-
toOpacity: 0
|
|
6230
|
-
});
|
|
6231
|
-
}
|
|
6232
|
-
tweens.push({
|
|
6233
|
-
tweenType: "area",
|
|
6234
|
-
kind: "enter",
|
|
6235
|
-
el,
|
|
6236
|
-
fromTop: next.topPoints,
|
|
6237
|
-
toTop: next.topPoints,
|
|
6238
|
-
fromBottom: next.bottomPoints,
|
|
6239
|
-
toBottom: next.bottomPoints,
|
|
6240
|
-
finalTopPoints: next.topPoints,
|
|
6241
|
-
finalBottomPoints: next.bottomPoints,
|
|
6242
|
-
interpolate: next.interpolate,
|
|
6243
|
-
hasStroke: !!next.stroke && !!next.topPath,
|
|
6244
|
-
fromOpacity: 0,
|
|
6245
|
-
toOpacity: 1
|
|
6246
|
-
});
|
|
6247
|
-
} else {
|
|
6248
|
-
const [fromTop, toTop] = normalizePointArrays(
|
|
6249
|
-
prev.topPoints,
|
|
6250
|
-
next.topPoints,
|
|
6251
|
-
prevPKs,
|
|
6252
|
-
nextPKs
|
|
6253
|
-
);
|
|
6254
|
-
const [fromBottom, toBottom] = normalizePointArrays(
|
|
6255
|
-
prev.bottomPoints,
|
|
6256
|
-
next.bottomPoints,
|
|
6257
|
-
prevPKs,
|
|
6258
|
-
nextPKs
|
|
6259
|
-
);
|
|
6260
|
-
tweens.push({
|
|
6261
|
-
tweenType: "area",
|
|
6262
|
-
kind: "update",
|
|
6263
|
-
el,
|
|
6264
|
-
fromTop,
|
|
6265
|
-
toTop,
|
|
6266
|
-
fromBottom,
|
|
6267
|
-
toBottom,
|
|
6268
|
-
finalTopPoints: next.topPoints,
|
|
6269
|
-
finalBottomPoints: next.bottomPoints,
|
|
6270
|
-
interpolate: next.interpolate,
|
|
6271
|
-
hasStroke: !!next.stroke && !!next.topPath
|
|
6272
|
-
});
|
|
6273
|
-
}
|
|
6274
|
-
}
|
|
6275
|
-
for (const [key, next] of nextByKey) {
|
|
6276
|
-
if (prevByKey.has(key)) continue;
|
|
6277
|
-
const el = marksContainer.querySelector(`[data-key="${key}"]`);
|
|
6278
|
-
if (!el) continue;
|
|
6279
|
-
tweens.push({
|
|
6280
|
-
tweenType: "area",
|
|
6281
|
-
kind: "enter",
|
|
6282
|
-
el,
|
|
6283
|
-
fromTop: next.topPoints,
|
|
6284
|
-
toTop: next.topPoints,
|
|
6285
|
-
fromBottom: next.bottomPoints,
|
|
6286
|
-
toBottom: next.bottomPoints,
|
|
6287
|
-
finalTopPoints: next.topPoints,
|
|
6288
|
-
finalBottomPoints: next.bottomPoints,
|
|
6289
|
-
interpolate: next.interpolate,
|
|
6290
|
-
hasStroke: !!next.stroke && !!next.topPath,
|
|
6291
|
-
fromOpacity: 0,
|
|
6292
|
-
toOpacity: 1
|
|
6293
|
-
});
|
|
6294
|
-
}
|
|
6295
|
-
for (const [key, prev] of prevByKey) {
|
|
6296
|
-
if (nextByKey.has(key)) continue;
|
|
6297
|
-
const ghostMark = resolveGhostGradientFill(prev, ghostGradientMap);
|
|
6298
|
-
const ghost = renderSingleMark(ghostMark, 0);
|
|
6299
|
-
if (!ghost) continue;
|
|
6300
|
-
ghost.classList.add("oc-ghost");
|
|
6301
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6302
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6303
|
-
ghost.removeAttribute("data-key");
|
|
6304
|
-
marksContainer.appendChild(ghost);
|
|
6305
|
-
ghosts.push(ghost);
|
|
6306
|
-
tweens.push({
|
|
6307
|
-
tweenType: "area",
|
|
6308
|
-
kind: "exit",
|
|
6309
|
-
el: ghost,
|
|
6310
|
-
fromTop: prev.topPoints,
|
|
6311
|
-
toTop: prev.topPoints,
|
|
6312
|
-
fromBottom: prev.bottomPoints,
|
|
6313
|
-
toBottom: prev.bottomPoints,
|
|
6314
|
-
finalTopPoints: prev.topPoints,
|
|
6315
|
-
finalBottomPoints: prev.bottomPoints,
|
|
6316
|
-
interpolate: prev.interpolate,
|
|
6317
|
-
hasStroke: !!prev.stroke && !!prev.topPath,
|
|
6318
|
-
ghost,
|
|
6319
|
-
fromOpacity: 1,
|
|
6320
|
-
toOpacity: 0
|
|
6321
|
-
});
|
|
6322
|
-
}
|
|
6323
|
-
}
|
|
6324
|
-
function buildPointTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, ghostGradientMap, fromSnapshot) {
|
|
6325
|
-
const prevPoints = prevLayout.marks.filter((m2) => m2.type === "point");
|
|
6326
|
-
const nextPoints = nextLayout.marks.filter((m2) => m2.type === "point");
|
|
6327
|
-
const prevByKey = /* @__PURE__ */ new Map();
|
|
6328
|
-
for (const m2 of prevPoints) {
|
|
6329
|
-
if (m2.key) prevByKey.set(m2.key, m2);
|
|
6330
|
-
}
|
|
6331
|
-
const nextByKey = /* @__PURE__ */ new Map();
|
|
6332
|
-
for (const m2 of nextPoints) {
|
|
6333
|
-
if (m2.key) nextByKey.set(m2.key, m2);
|
|
6334
|
-
}
|
|
6335
|
-
for (const [key, next] of nextByKey) {
|
|
6336
|
-
const prev = prevByKey.get(key);
|
|
6337
|
-
if (!prev) continue;
|
|
6338
|
-
const el = marksContainer.querySelector(
|
|
6339
|
-
`circle.oc-mark-point[data-key="${key}"]`
|
|
6340
|
-
);
|
|
6341
|
-
if (!el) continue;
|
|
6342
|
-
const renderedOpacity = el.getAttribute("opacity");
|
|
6343
|
-
const isSuppressed = renderedOpacity === "0";
|
|
6344
|
-
let fromCx = prev.cx;
|
|
6345
|
-
let fromCy = prev.cy;
|
|
6346
|
-
let fromR = prev.r !== next.r ? prev.r : void 0;
|
|
6347
|
-
const snap = fromSnapshot?.get(key);
|
|
6348
|
-
if (snap && snap.type === "point") {
|
|
6349
|
-
fromCx = snap.cx;
|
|
6350
|
-
fromCy = snap.cy;
|
|
6351
|
-
if (snap.r !== void 0 && snap.r !== next.r) {
|
|
6352
|
-
fromR = snap.r;
|
|
6353
|
-
}
|
|
6354
|
-
}
|
|
6355
|
-
tweens.push({
|
|
6356
|
-
tweenType: "point",
|
|
6357
|
-
kind: "update",
|
|
6358
|
-
el,
|
|
6359
|
-
fromCx,
|
|
6360
|
-
fromCy,
|
|
6361
|
-
toCx: next.cx,
|
|
6362
|
-
toCy: next.cy,
|
|
6363
|
-
fromR,
|
|
6364
|
-
toR: fromR !== void 0 ? next.r : void 0,
|
|
6365
|
-
// Do NOT tween opacity for updated points - preserve renderer state
|
|
6366
|
-
suppressedOpacity: isSuppressed ? "0" : void 0
|
|
6367
|
-
});
|
|
6368
|
-
}
|
|
6369
|
-
for (const [key, next] of nextByKey) {
|
|
6370
|
-
if (prevByKey.has(key)) continue;
|
|
6371
|
-
const el = marksContainer.querySelector(
|
|
6372
|
-
`circle.oc-mark-point[data-key="${key}"]`
|
|
6373
|
-
);
|
|
6374
|
-
if (!el) continue;
|
|
6375
|
-
const renderedOpacity = el.getAttribute("opacity");
|
|
6376
|
-
const targetOpacity = renderedOpacity != null ? Number(renderedOpacity) : 1;
|
|
6377
|
-
tweens.push({
|
|
6378
|
-
tweenType: "point",
|
|
6379
|
-
kind: "enter",
|
|
6380
|
-
el,
|
|
6381
|
-
fromCx: next.cx,
|
|
6382
|
-
fromCy: next.cy,
|
|
6383
|
-
toCx: next.cx,
|
|
6384
|
-
toCy: next.cy,
|
|
6385
|
-
fromOpacity: 0,
|
|
6386
|
-
toOpacity: targetOpacity,
|
|
6387
|
-
suppressedOpacity: renderedOpacity === "0" ? "0" : void 0
|
|
6388
|
-
});
|
|
6389
|
-
}
|
|
6390
|
-
for (const [key, prev] of prevByKey) {
|
|
6391
|
-
if (nextByKey.has(key)) continue;
|
|
6392
|
-
const ghostMark = resolveGhostGradientFill(prev, ghostGradientMap);
|
|
6393
|
-
const ghost = renderSingleMark(ghostMark, 0);
|
|
6394
|
-
if (!ghost) continue;
|
|
6395
|
-
ghost.classList.add("oc-ghost");
|
|
6396
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6397
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6398
|
-
ghost.removeAttribute("data-key");
|
|
6399
|
-
marksContainer.appendChild(ghost);
|
|
6400
|
-
ghosts.push(ghost);
|
|
6401
|
-
tweens.push({
|
|
6402
|
-
tweenType: "point",
|
|
6403
|
-
kind: "exit",
|
|
6404
|
-
el: ghost,
|
|
6405
|
-
fromCx: prev.cx,
|
|
6406
|
-
fromCy: prev.cy,
|
|
6407
|
-
toCx: prev.cx,
|
|
6408
|
-
toCy: prev.cy,
|
|
6409
|
-
ghost,
|
|
6410
|
-
fromOpacity: 1,
|
|
6411
|
-
toOpacity: 0
|
|
6412
|
-
});
|
|
6413
|
-
}
|
|
6414
|
-
}
|
|
6415
|
-
function buildRuleTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot) {
|
|
6416
|
-
const prevRules = prevLayout.marks.filter((m2) => m2.type === "rule");
|
|
6417
|
-
const nextRules = nextLayout.marks.filter((m2) => m2.type === "rule");
|
|
6418
|
-
const prevByKey = /* @__PURE__ */ new Map();
|
|
6419
|
-
for (const m2 of prevRules) {
|
|
6420
|
-
if (m2.key) prevByKey.set(m2.key, m2);
|
|
6421
|
-
}
|
|
6422
|
-
const nextByKey = /* @__PURE__ */ new Map();
|
|
6423
|
-
for (const m2 of nextRules) {
|
|
6424
|
-
if (m2.key) nextByKey.set(m2.key, m2);
|
|
6425
|
-
}
|
|
6426
|
-
for (const [key, next] of nextByKey) {
|
|
6427
|
-
const prev = prevByKey.get(key);
|
|
6428
|
-
if (!prev) continue;
|
|
6429
|
-
const el = marksContainer.querySelector(
|
|
6430
|
-
`.oc-mark-rule[data-key="${key}"]`
|
|
6431
|
-
);
|
|
6432
|
-
if (!el) continue;
|
|
6433
|
-
let fromX1 = prev.x1;
|
|
6434
|
-
let fromY1 = prev.y1;
|
|
6435
|
-
let fromX2 = prev.x2;
|
|
6436
|
-
let fromY2 = prev.y2;
|
|
6437
|
-
const snap = fromSnapshot?.get(key);
|
|
6438
|
-
if (snap && snap.type === "rule") {
|
|
6439
|
-
fromX1 = snap.x1;
|
|
6440
|
-
fromY1 = snap.y1;
|
|
6441
|
-
fromX2 = snap.x2;
|
|
6442
|
-
fromY2 = snap.y2;
|
|
6443
|
-
}
|
|
6444
|
-
tweens.push({
|
|
6445
|
-
tweenType: "rule",
|
|
6446
|
-
kind: "update",
|
|
6447
|
-
el,
|
|
6448
|
-
fromX1,
|
|
6449
|
-
fromY1,
|
|
6450
|
-
fromX2,
|
|
6451
|
-
fromY2,
|
|
6452
|
-
toX1: next.x1,
|
|
6453
|
-
toY1: next.y1,
|
|
6454
|
-
toX2: next.x2,
|
|
6455
|
-
toY2: next.y2
|
|
6456
|
-
});
|
|
6457
|
-
}
|
|
6458
|
-
for (const [key, next] of nextByKey) {
|
|
6459
|
-
if (prevByKey.has(key)) continue;
|
|
6460
|
-
const el = marksContainer.querySelector(
|
|
6461
|
-
`.oc-mark-rule[data-key="${key}"]`
|
|
6462
|
-
);
|
|
6463
|
-
if (!el) continue;
|
|
6464
|
-
tweens.push({
|
|
6465
|
-
tweenType: "rule",
|
|
6466
|
-
kind: "enter",
|
|
6467
|
-
el,
|
|
6468
|
-
fromX1: next.x1,
|
|
6469
|
-
fromY1: next.y1,
|
|
6470
|
-
fromX2: next.x2,
|
|
6471
|
-
fromY2: next.y2,
|
|
6472
|
-
toX1: next.x1,
|
|
6473
|
-
toY1: next.y1,
|
|
6474
|
-
toX2: next.x2,
|
|
6475
|
-
toY2: next.y2,
|
|
6476
|
-
fromOpacity: 0,
|
|
6477
|
-
toOpacity: 1
|
|
6478
|
-
});
|
|
6479
|
-
}
|
|
6480
|
-
for (const [key, prev] of prevByKey) {
|
|
6481
|
-
if (nextByKey.has(key)) continue;
|
|
6482
|
-
const ghost = renderSingleMark(prev, 0);
|
|
6483
|
-
if (!ghost) continue;
|
|
6484
|
-
ghost.classList.add("oc-ghost");
|
|
6485
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6486
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6487
|
-
ghost.removeAttribute("data-key");
|
|
6488
|
-
marksContainer.appendChild(ghost);
|
|
6489
|
-
ghosts.push(ghost);
|
|
6490
|
-
tweens.push({
|
|
6491
|
-
tweenType: "rule",
|
|
6492
|
-
kind: "exit",
|
|
6493
|
-
el: ghost,
|
|
6494
|
-
fromX1: prev.x1,
|
|
6495
|
-
fromY1: prev.y1,
|
|
6496
|
-
fromX2: prev.x2,
|
|
6497
|
-
fromY2: prev.y2,
|
|
6498
|
-
toX1: prev.x1,
|
|
6499
|
-
toY1: prev.y1,
|
|
6500
|
-
toX2: prev.x2,
|
|
6501
|
-
toY2: prev.y2,
|
|
6502
|
-
ghost,
|
|
6503
|
-
fromOpacity: 1,
|
|
6504
|
-
toOpacity: 0
|
|
6505
|
-
});
|
|
6506
|
-
}
|
|
6507
|
-
}
|
|
6508
|
-
function buildTickTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot) {
|
|
6509
|
-
const prevTicks = prevLayout.marks.filter((m2) => m2.type === "tick");
|
|
6510
|
-
const nextTicks = nextLayout.marks.filter((m2) => m2.type === "tick");
|
|
6511
|
-
const prevByKey = /* @__PURE__ */ new Map();
|
|
6512
|
-
for (const m2 of prevTicks) {
|
|
6513
|
-
if (m2.key) prevByKey.set(m2.key, m2);
|
|
6514
|
-
}
|
|
6515
|
-
const nextByKey = /* @__PURE__ */ new Map();
|
|
6516
|
-
for (const m2 of nextTicks) {
|
|
6517
|
-
if (m2.key) nextByKey.set(m2.key, m2);
|
|
6518
|
-
}
|
|
6519
|
-
function tickEndpoints(t) {
|
|
6520
|
-
const half = t.length / 2;
|
|
6521
|
-
if (t.orient === "vertical") {
|
|
6522
|
-
return { x1: t.x, y1: t.y - half, x2: t.x, y2: t.y + half };
|
|
6523
|
-
}
|
|
6524
|
-
return { x1: t.x - half, y1: t.y, x2: t.x + half, y2: t.y };
|
|
6525
|
-
}
|
|
6526
|
-
for (const [key, next] of nextByKey) {
|
|
6527
|
-
const prev = prevByKey.get(key);
|
|
6528
|
-
if (!prev) continue;
|
|
6529
|
-
const el = marksContainer.querySelector(
|
|
6530
|
-
`.oc-mark-tick[data-key="${key}"]`
|
|
6531
|
-
);
|
|
6532
|
-
if (!el) continue;
|
|
6533
|
-
let pEnd = tickEndpoints(prev);
|
|
6534
|
-
const nEnd = tickEndpoints(next);
|
|
6535
|
-
const snap = fromSnapshot?.get(key);
|
|
6536
|
-
if (snap && snap.type === "tick") {
|
|
6537
|
-
pEnd = { x1: snap.x1, y1: snap.y1, x2: snap.x2, y2: snap.y2 };
|
|
6538
|
-
}
|
|
6539
|
-
tweens.push({
|
|
6540
|
-
tweenType: "tick",
|
|
6541
|
-
kind: "update",
|
|
6542
|
-
el,
|
|
6543
|
-
fromX1: pEnd.x1,
|
|
6544
|
-
fromY1: pEnd.y1,
|
|
6545
|
-
fromX2: pEnd.x2,
|
|
6546
|
-
fromY2: pEnd.y2,
|
|
6547
|
-
toX1: nEnd.x1,
|
|
6548
|
-
toY1: nEnd.y1,
|
|
6549
|
-
toX2: nEnd.x2,
|
|
6550
|
-
toY2: nEnd.y2
|
|
6551
|
-
});
|
|
6552
|
-
}
|
|
6553
|
-
for (const [key, next] of nextByKey) {
|
|
6554
|
-
if (prevByKey.has(key)) continue;
|
|
6555
|
-
const el = marksContainer.querySelector(
|
|
6556
|
-
`.oc-mark-tick[data-key="${key}"]`
|
|
6557
|
-
);
|
|
6558
|
-
if (!el) continue;
|
|
6559
|
-
const nEnd = tickEndpoints(next);
|
|
6560
|
-
tweens.push({
|
|
6561
|
-
tweenType: "tick",
|
|
6562
|
-
kind: "enter",
|
|
6563
|
-
el,
|
|
6564
|
-
fromX1: nEnd.x1,
|
|
6565
|
-
fromY1: nEnd.y1,
|
|
6566
|
-
fromX2: nEnd.x2,
|
|
6567
|
-
fromY2: nEnd.y2,
|
|
6568
|
-
toX1: nEnd.x1,
|
|
6569
|
-
toY1: nEnd.y1,
|
|
6570
|
-
toX2: nEnd.x2,
|
|
6571
|
-
toY2: nEnd.y2,
|
|
6572
|
-
fromOpacity: 0,
|
|
6573
|
-
toOpacity: 1
|
|
6574
|
-
});
|
|
6575
|
-
}
|
|
6576
|
-
for (const [key, prev] of prevByKey) {
|
|
6577
|
-
if (nextByKey.has(key)) continue;
|
|
6578
|
-
const ghost = renderSingleMark(prev, 0);
|
|
6579
|
-
if (!ghost) continue;
|
|
6580
|
-
ghost.classList.add("oc-ghost");
|
|
6581
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6582
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6583
|
-
ghost.removeAttribute("data-key");
|
|
6584
|
-
marksContainer.appendChild(ghost);
|
|
6585
|
-
ghosts.push(ghost);
|
|
6586
|
-
const pEnd = tickEndpoints(prev);
|
|
6587
|
-
tweens.push({
|
|
6588
|
-
tweenType: "tick",
|
|
6589
|
-
kind: "exit",
|
|
6590
|
-
el: ghost,
|
|
6591
|
-
fromX1: pEnd.x1,
|
|
6592
|
-
fromY1: pEnd.y1,
|
|
6593
|
-
fromX2: pEnd.x2,
|
|
6594
|
-
fromY2: pEnd.y2,
|
|
6595
|
-
toX1: pEnd.x1,
|
|
6596
|
-
toY1: pEnd.y1,
|
|
6597
|
-
toX2: pEnd.x2,
|
|
6598
|
-
toY2: pEnd.y2,
|
|
6599
|
-
ghost,
|
|
6600
|
-
fromOpacity: 1,
|
|
6601
|
-
toOpacity: 0
|
|
6602
|
-
});
|
|
6603
|
-
}
|
|
6604
|
-
}
|
|
6605
|
-
function buildTextMarkTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot) {
|
|
6606
|
-
const prevTexts = prevLayout.marks.filter((m2) => m2.type === "textMark");
|
|
6607
|
-
const nextTexts = nextLayout.marks.filter((m2) => m2.type === "textMark");
|
|
6608
|
-
const prevByKey = /* @__PURE__ */ new Map();
|
|
6609
|
-
for (const m2 of prevTexts) {
|
|
6610
|
-
if (m2.key) prevByKey.set(m2.key, m2);
|
|
6611
|
-
}
|
|
6612
|
-
const nextByKey = /* @__PURE__ */ new Map();
|
|
6613
|
-
for (const m2 of nextTexts) {
|
|
6614
|
-
if (m2.key) nextByKey.set(m2.key, m2);
|
|
6615
|
-
}
|
|
6616
|
-
for (const [key, next] of nextByKey) {
|
|
6617
|
-
const prev = prevByKey.get(key);
|
|
6618
|
-
if (!prev) continue;
|
|
6619
|
-
const el = marksContainer.querySelector(
|
|
6620
|
-
`.oc-mark-text[data-key="${key}"]`
|
|
6621
|
-
);
|
|
6622
|
-
if (!el) continue;
|
|
6623
|
-
let fromX = prev.x;
|
|
6624
|
-
let fromY = prev.y;
|
|
6625
|
-
const snap = fromSnapshot?.get(key);
|
|
6626
|
-
if (snap && snap.type === "textMark") {
|
|
6627
|
-
fromX = snap.x;
|
|
6628
|
-
fromY = snap.y;
|
|
6629
|
-
}
|
|
6630
|
-
tweens.push({
|
|
6631
|
-
tweenType: "textMark",
|
|
6632
|
-
kind: "update",
|
|
6633
|
-
el,
|
|
6634
|
-
fromX,
|
|
6635
|
-
fromY,
|
|
6636
|
-
toX: next.x,
|
|
6637
|
-
toY: next.y
|
|
6638
|
-
});
|
|
6639
|
-
}
|
|
6640
|
-
for (const [key, next] of nextByKey) {
|
|
6641
|
-
if (prevByKey.has(key)) continue;
|
|
6642
|
-
const el = marksContainer.querySelector(
|
|
6643
|
-
`.oc-mark-text[data-key="${key}"]`
|
|
6644
|
-
);
|
|
6645
|
-
if (!el) continue;
|
|
6646
|
-
tweens.push({
|
|
6647
|
-
tweenType: "textMark",
|
|
6648
|
-
kind: "enter",
|
|
6649
|
-
el,
|
|
6650
|
-
fromX: next.x,
|
|
6651
|
-
fromY: next.y,
|
|
6652
|
-
toX: next.x,
|
|
6653
|
-
toY: next.y,
|
|
6654
|
-
fromOpacity: 0,
|
|
6655
|
-
toOpacity: 1
|
|
6656
|
-
});
|
|
6657
|
-
}
|
|
6658
|
-
for (const [key, prev] of prevByKey) {
|
|
6659
|
-
if (nextByKey.has(key)) continue;
|
|
6660
|
-
const ghost = renderSingleMark(prev, 0);
|
|
6661
|
-
if (!ghost) continue;
|
|
6662
|
-
ghost.classList.add("oc-ghost");
|
|
6663
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6664
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6665
|
-
ghost.removeAttribute("data-key");
|
|
6666
|
-
marksContainer.appendChild(ghost);
|
|
6667
|
-
ghosts.push(ghost);
|
|
6668
|
-
tweens.push({
|
|
6669
|
-
tweenType: "textMark",
|
|
6670
|
-
kind: "exit",
|
|
6671
|
-
el: ghost,
|
|
6672
|
-
fromX: prev.x,
|
|
6673
|
-
fromY: prev.y,
|
|
6674
|
-
toX: prev.x,
|
|
6675
|
-
toY: prev.y,
|
|
6676
|
-
ghost,
|
|
6677
|
-
fromOpacity: 1,
|
|
6678
|
-
toOpacity: 0
|
|
6679
|
-
});
|
|
6680
|
-
}
|
|
6681
|
-
}
|
|
6682
|
-
function buildAxisTweens(svg, prevLayout, nextLayout, tweens, ghosts) {
|
|
6683
|
-
buildSingleAxisTweens(svg, prevLayout.axes.x, nextLayout.axes.x, "x", nextLayout, tweens, ghosts);
|
|
6684
|
-
buildSingleAxisTweens(svg, prevLayout.axes.y, nextLayout.axes.y, "y", nextLayout, tweens, ghosts);
|
|
6685
|
-
}
|
|
6686
|
-
function buildSingleAxisTweens(svg, prevAxis, nextAxis, orientation, layout, tweens, ghosts) {
|
|
6687
|
-
if (!prevAxis || !nextAxis) return;
|
|
6688
|
-
const axisClass = `oc-axis-${orientation}`;
|
|
6689
|
-
const axisGroup = svg.querySelector(`.${axisClass}`);
|
|
6690
|
-
if (!axisGroup) return;
|
|
6691
|
-
const isRotated = !!(nextAxis.tickAngle && Math.abs(nextAxis.tickAngle) > 10);
|
|
6692
|
-
const prevTickMap = /* @__PURE__ */ new Map();
|
|
6693
|
-
for (const tick of prevAxis.ticks) {
|
|
6694
|
-
prevTickMap.set(serializeKeyValue(tick.value), tick.position);
|
|
6695
|
-
}
|
|
6696
|
-
const nextTickMap = /* @__PURE__ */ new Map();
|
|
6697
|
-
for (const tick of nextAxis.ticks) {
|
|
6698
|
-
nextTickMap.set(serializeKeyValue(tick.value), tick.position);
|
|
6699
|
-
}
|
|
6700
|
-
const posAttr = orientation === "x" ? "x" : "y";
|
|
6701
|
-
const tickLabels = axisGroup.querySelectorAll(".oc-axis-tick[data-tick-key]");
|
|
6702
|
-
const matchedNextKeys = /* @__PURE__ */ new Set();
|
|
6703
|
-
for (const labelEl of tickLabels) {
|
|
6704
|
-
const key = labelEl.getAttribute("data-tick-key");
|
|
6705
|
-
if (!key) continue;
|
|
6706
|
-
const nextPos = nextTickMap.get(key);
|
|
6707
|
-
const prevPos = prevTickMap.get(key);
|
|
6708
|
-
if (prevPos !== void 0 && nextPos !== void 0) {
|
|
6709
|
-
matchedNextKeys.add(key);
|
|
6710
|
-
tweens.push({
|
|
6711
|
-
tweenType: "axisTick",
|
|
6712
|
-
kind: "update",
|
|
6713
|
-
el: labelEl,
|
|
6714
|
-
posAttr,
|
|
6715
|
-
fromPos: prevPos,
|
|
6716
|
-
toPos: nextPos,
|
|
6717
|
-
crossfade: isRotated
|
|
6718
|
-
});
|
|
6719
|
-
} else if (prevPos === void 0 && nextPos !== void 0) {
|
|
6720
|
-
matchedNextKeys.add(key);
|
|
6721
|
-
tweens.push({
|
|
6722
|
-
tweenType: "axisTick",
|
|
6723
|
-
kind: "enter",
|
|
6724
|
-
el: labelEl,
|
|
6725
|
-
posAttr,
|
|
6726
|
-
fromPos: nextPos,
|
|
6727
|
-
toPos: nextPos,
|
|
6728
|
-
crossfade: false,
|
|
6729
|
-
fromOpacity: 0,
|
|
6730
|
-
toOpacity: 1
|
|
6731
|
-
});
|
|
6732
|
-
}
|
|
6733
|
-
}
|
|
6734
|
-
for (const [key, prevPos] of prevTickMap) {
|
|
6735
|
-
if (nextTickMap.has(key)) continue;
|
|
6736
|
-
const prevTick = prevAxis.ticks.find((t) => serializeKeyValue(t.value) === key);
|
|
6737
|
-
if (!prevTick) continue;
|
|
6738
|
-
const ghost = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
6739
|
-
ghost.setAttribute("class", "oc-axis-tick oc-ghost");
|
|
6740
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6741
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6742
|
-
ghost.textContent = prevTick.label;
|
|
6743
|
-
const area = layout.area;
|
|
6744
|
-
if (orientation === "x") {
|
|
6745
|
-
ghost.setAttribute("x", String(prevPos));
|
|
6746
|
-
const xLabelPad = nextAxis.labelPadding ?? layout.theme.spacing.xAxisLabelPadding;
|
|
6747
|
-
const fontSize = nextAxis.tickLabelStyle.fontSize;
|
|
6748
|
-
ghost.setAttribute("y", String(area.y + area.height + xLabelPad + fontSize * 0.8));
|
|
6749
|
-
ghost.setAttribute("text-anchor", "middle");
|
|
6750
|
-
} else {
|
|
6751
|
-
const TICK_LABEL_OFFSET = 8;
|
|
6752
|
-
ghost.setAttribute("x", String(area.x - TICK_LABEL_OFFSET));
|
|
6753
|
-
ghost.setAttribute("y", String(prevPos));
|
|
6754
|
-
ghost.setAttribute("text-anchor", "end");
|
|
6755
|
-
ghost.setAttribute("dominant-baseline", "central");
|
|
6756
|
-
}
|
|
6757
|
-
axisGroup.appendChild(ghost);
|
|
6758
|
-
ghosts.push(ghost);
|
|
6759
|
-
tweens.push({
|
|
6760
|
-
tweenType: "axisTick",
|
|
6761
|
-
kind: "exit",
|
|
6762
|
-
el: ghost,
|
|
6763
|
-
posAttr,
|
|
6764
|
-
fromPos: prevPos,
|
|
6765
|
-
toPos: prevPos,
|
|
6766
|
-
crossfade: false,
|
|
6767
|
-
ghost,
|
|
6768
|
-
fromOpacity: 1,
|
|
6769
|
-
toOpacity: 0
|
|
6770
|
-
});
|
|
6771
|
-
}
|
|
6772
|
-
const prevGridMap = /* @__PURE__ */ new Map();
|
|
6773
|
-
for (const gridline of prevAxis.gridlines) {
|
|
6774
|
-
prevGridMap.set(String(gridline.position), gridline.position);
|
|
6775
|
-
}
|
|
6776
|
-
const prevGridByValue = /* @__PURE__ */ new Map();
|
|
6777
|
-
for (const tick of prevAxis.ticks) {
|
|
6778
|
-
const key = serializeKeyValue(tick.value);
|
|
6779
|
-
const matchingGridline = prevAxis.gridlines.find((g) => g.position === tick.position);
|
|
6780
|
-
if (matchingGridline) {
|
|
6781
|
-
prevGridByValue.set(key, matchingGridline.position);
|
|
6782
|
-
}
|
|
6783
|
-
}
|
|
6784
|
-
const nextGridByValue = /* @__PURE__ */ new Map();
|
|
6785
|
-
for (const tick of nextAxis.ticks) {
|
|
6786
|
-
const key = serializeKeyValue(tick.value);
|
|
6787
|
-
const matchingGridline = nextAxis.gridlines.find((g) => g.position === tick.position);
|
|
6788
|
-
if (matchingGridline) {
|
|
6789
|
-
nextGridByValue.set(key, matchingGridline.position);
|
|
6790
|
-
}
|
|
6791
|
-
}
|
|
6792
|
-
const gridlines = axisGroup.querySelectorAll(".oc-gridline[data-tick-key]");
|
|
6793
|
-
const matchedGridKeys = /* @__PURE__ */ new Set();
|
|
6794
|
-
for (const glEl of gridlines) {
|
|
6795
|
-
const key = glEl.getAttribute("data-tick-key");
|
|
6796
|
-
if (!key) continue;
|
|
6797
|
-
const nextPos = nextGridByValue.get(key);
|
|
6798
|
-
const prevPos = prevGridByValue.get(key);
|
|
6799
|
-
if (prevPos !== void 0 && nextPos !== void 0) {
|
|
6800
|
-
matchedGridKeys.add(key);
|
|
6801
|
-
tweens.push({
|
|
6802
|
-
tweenType: "gridline",
|
|
6803
|
-
kind: "update",
|
|
6804
|
-
el: glEl,
|
|
6805
|
-
orientation,
|
|
6806
|
-
fromPos: prevPos,
|
|
6807
|
-
toPos: nextPos
|
|
6808
|
-
});
|
|
6809
|
-
} else if (prevPos === void 0 && nextPos !== void 0) {
|
|
6810
|
-
matchedGridKeys.add(key);
|
|
6811
|
-
tweens.push({
|
|
6812
|
-
tweenType: "gridline",
|
|
6813
|
-
kind: "enter",
|
|
6814
|
-
el: glEl,
|
|
6815
|
-
orientation,
|
|
6816
|
-
fromPos: nextPos,
|
|
6817
|
-
toPos: nextPos,
|
|
6818
|
-
fromOpacity: 0,
|
|
6819
|
-
toOpacity: 1
|
|
6820
|
-
});
|
|
6821
|
-
}
|
|
6822
|
-
}
|
|
6823
|
-
for (const [key, prevPos] of prevGridByValue) {
|
|
6824
|
-
if (nextGridByValue.has(key)) continue;
|
|
6825
|
-
const ghost = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
6826
|
-
ghost.setAttribute("class", "oc-gridline oc-ghost");
|
|
6827
|
-
ghost.setAttribute("aria-hidden", "true");
|
|
6828
|
-
ghost.setAttribute("pointer-events", "none");
|
|
6829
|
-
if (orientation === "y") {
|
|
6830
|
-
const area = layout.area;
|
|
6831
|
-
ghost.setAttribute("x1", String(area.x));
|
|
6832
|
-
ghost.setAttribute("y1", String(prevPos));
|
|
6833
|
-
ghost.setAttribute("x2", String(area.x + area.width));
|
|
6834
|
-
ghost.setAttribute("y2", String(prevPos));
|
|
6835
|
-
} else {
|
|
6836
|
-
const area = layout.area;
|
|
6837
|
-
ghost.setAttribute("x1", String(prevPos));
|
|
6838
|
-
ghost.setAttribute("y1", String(area.y));
|
|
6839
|
-
ghost.setAttribute("x2", String(prevPos));
|
|
6840
|
-
ghost.setAttribute("y2", String(area.y + area.height));
|
|
6841
|
-
}
|
|
6842
|
-
axisGroup.appendChild(ghost);
|
|
6843
|
-
ghosts.push(ghost);
|
|
6844
|
-
tweens.push({
|
|
6845
|
-
tweenType: "gridline",
|
|
6846
|
-
kind: "exit",
|
|
6847
|
-
el: ghost,
|
|
6848
|
-
orientation,
|
|
6849
|
-
fromPos: prevPos,
|
|
6850
|
-
toPos: prevPos,
|
|
6851
|
-
ghost,
|
|
6852
|
-
fromOpacity: 1,
|
|
6853
|
-
toOpacity: 0
|
|
6854
|
-
});
|
|
6855
|
-
}
|
|
6856
|
-
}
|
|
6857
|
-
|
|
6858
|
-
// src/mount.ts
|
|
6859
|
-
var FALLBACK_HEIGHT = 400;
|
|
6860
|
-
function resolveDarkMode3(mode) {
|
|
6861
|
-
if (mode === "force") return true;
|
|
6862
|
-
if (mode === "off" || mode === void 0) return false;
|
|
6863
|
-
if (typeof window !== "undefined" && window.matchMedia) {
|
|
6864
|
-
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
6865
|
-
}
|
|
6866
|
-
return false;
|
|
6867
|
-
}
|
|
6868
|
-
var EDITABLE_HOVER_CSS = `
|
|
6869
|
-
.oc-editable-hover {
|
|
6870
|
-
outline: 1.5px solid rgba(79, 70, 229, 0.35);
|
|
6871
|
-
outline-offset: 2px;
|
|
6872
|
-
border-radius: 2px;
|
|
6873
|
-
}
|
|
6874
|
-
`;
|
|
6875
|
-
function makeEditable(svg) {
|
|
6876
|
-
svg.setAttribute("tabindex", "0");
|
|
6877
|
-
svg.style.outline = "none";
|
|
6878
|
-
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
|
6879
|
-
style.textContent = EDITABLE_HOVER_CSS;
|
|
6880
|
-
svg.insertBefore(style, svg.firstChild);
|
|
6881
|
-
}
|
|
6882
|
-
function hasEditingCallbacks(opts) {
|
|
6883
|
-
return !!(opts?.onEdit || opts?.onSelect || opts?.onDeselect || opts?.onTextEdit);
|
|
6884
|
-
}
|
|
6885
|
-
function createChart(container, spec, options) {
|
|
6886
|
-
let currentSpec = spec;
|
|
6887
|
-
let currentLayout;
|
|
6888
|
-
let svgElement = null;
|
|
6889
|
-
let tooltipManager = null;
|
|
6890
|
-
let disconnectResize = null;
|
|
6891
|
-
let cleanupTooltipEvents = null;
|
|
6892
|
-
let cleanupVoronoiEvents = null;
|
|
6893
|
-
let cleanupKeyboardNav = null;
|
|
6894
|
-
let cleanupLegend = null;
|
|
6895
|
-
let cleanupChartEvents = null;
|
|
6896
|
-
let cleanupAnnotationDrag = null;
|
|
6897
|
-
let cleanupEditDrags = null;
|
|
6898
|
-
let cleanupSelection = null;
|
|
6899
|
-
let cleanupKeyboardEdit = null;
|
|
6900
|
-
let srTable = null;
|
|
6901
|
-
let destroyed = false;
|
|
6902
|
-
let isDragging = false;
|
|
6903
|
-
let pendingRender = false;
|
|
6904
|
-
let isFirstRender = true;
|
|
6905
|
-
let cleanupAnimations = null;
|
|
6906
|
-
let pendingResize = false;
|
|
6907
|
-
let isAutoHeight = null;
|
|
6908
|
-
let lastRenderedWidth = null;
|
|
6909
|
-
let lastRenderedSvgHeight = null;
|
|
6910
|
-
let transitionHandle = null;
|
|
6911
|
-
let transitionSnapshot = null;
|
|
6912
|
-
let fontsReloadPending = false;
|
|
6913
|
-
let selectedElement = options?.selectedElement ?? null;
|
|
6914
|
-
let overlayElement = null;
|
|
6915
|
-
let isTextEditingActive = false;
|
|
6916
|
-
const runtimeHiddenSeries = /* @__PURE__ */ new Set();
|
|
6917
|
-
const runtimeShownSeries = /* @__PURE__ */ new Set();
|
|
6918
|
-
let textEditCleanup = null;
|
|
6919
|
-
container.classList.add("oc-root");
|
|
6920
|
-
function resolveEffectiveFont() {
|
|
6921
|
-
return options?.theme?.fonts?.family ?? currentSpec.theme?.fonts?.family ?? resolveFontFamily(container);
|
|
6922
|
-
}
|
|
6923
|
-
let fontFamily = resolveEffectiveFont();
|
|
6924
|
-
let measureText = createMeasureText(fontFamily);
|
|
6925
|
-
let renderGen = 0;
|
|
6926
|
-
function compileAt(width, height) {
|
|
6927
|
-
const darkMode = resolveDarkMode3(options?.darkMode);
|
|
6928
|
-
const compileOpts = {
|
|
6929
|
-
width,
|
|
6930
|
-
height,
|
|
6931
|
-
theme: options?.theme,
|
|
6932
|
-
darkMode,
|
|
6933
|
-
watermark: options?.watermark,
|
|
6934
|
-
measureText
|
|
6935
|
-
};
|
|
6936
|
-
if (isLayerSpec(currentSpec)) {
|
|
6937
|
-
return compileLayer(withRuntimeHidden(currentSpec), compileOpts);
|
|
6938
|
-
}
|
|
6939
|
-
return compileChart(withRuntimeHidden(currentSpec), compileOpts);
|
|
6940
|
-
}
|
|
6941
|
-
function verticalOverheads(layout, width) {
|
|
6942
|
-
const topLegendBlock = layout.legend.position === "top" && "entries" in layout.legend && layout.legend.entries.length > 0 ? layout.legend.bounds.height + legendGap(width) : 0;
|
|
6943
|
-
return layout.chrome.topHeight + layout.chrome.bottomHeight + topLegendBlock + (layout.metrics?.height ?? 0);
|
|
6944
|
-
}
|
|
6945
|
-
function compile() {
|
|
6946
|
-
const { width, height } = getContainerDimensions();
|
|
6947
|
-
const growsWithChrome = isAutoHeight === true && !isLayerSpec(currentSpec) && !isGraphSpec(currentSpec) && currentSpec.display !== "sparkline";
|
|
6948
|
-
if (!growsWithChrome) {
|
|
6949
|
-
return compileAt(width, height);
|
|
6950
|
-
}
|
|
6951
|
-
let layout = compileAt(width, height);
|
|
6952
|
-
const overheads = verticalOverheads(layout, width);
|
|
6953
|
-
if (overheads > 0) {
|
|
6954
|
-
layout = compileAt(width, height + overheads);
|
|
6955
|
-
const overheads2 = verticalOverheads(layout, width);
|
|
6956
|
-
if (overheads2 !== overheads) {
|
|
6957
|
-
layout = compileAt(width, height + overheads2);
|
|
6958
|
-
}
|
|
6959
|
-
}
|
|
6960
|
-
return layout;
|
|
6961
|
-
}
|
|
6962
|
-
function withRuntimeHidden(spec2) {
|
|
6963
|
-
if (runtimeHiddenSeries.size === 0 && runtimeShownSeries.size === 0) return spec2;
|
|
6964
|
-
const userHidden = spec2.hiddenSeries ?? [];
|
|
6965
|
-
const finalHidden = new Set(userHidden);
|
|
6966
|
-
for (const s of runtimeHiddenSeries) finalHidden.add(s);
|
|
6967
|
-
for (const s of runtimeShownSeries) finalHidden.delete(s);
|
|
6968
|
-
const out = { ...spec2, hiddenSeries: Array.from(finalHidden) };
|
|
6969
|
-
if (finalHidden.size > 0 && spec2.annotations) {
|
|
6970
|
-
const filtered = spec2.annotations.filter((a2) => a2.type !== "text");
|
|
6971
|
-
out.annotations = filtered.length > 0 ? filtered : void 0;
|
|
6972
|
-
}
|
|
6973
|
-
return out;
|
|
6974
|
-
}
|
|
6975
|
-
function countSeries(spec2) {
|
|
6976
|
-
if (isLayerSpec(spec2)) return Infinity;
|
|
6977
|
-
const enc = spec2.encoding;
|
|
6978
|
-
const colorEnc = enc?.color;
|
|
6979
|
-
if (!colorEnc || "condition" in colorEnc || colorEnc.type === "quantitative") return Infinity;
|
|
6980
|
-
if (!("field" in colorEnc) || !colorEnc.field) return Infinity;
|
|
6981
|
-
const field = colorEnc.field;
|
|
6982
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6983
|
-
for (const row of spec2.data ?? []) {
|
|
6984
|
-
seen.add(String(row[field]));
|
|
6985
|
-
}
|
|
6986
|
-
return seen.size;
|
|
6987
|
-
}
|
|
6988
|
-
function isSeriesHidden(series) {
|
|
6989
|
-
if (runtimeShownSeries.has(series)) return false;
|
|
6990
|
-
if (runtimeHiddenSeries.has(series)) return true;
|
|
6991
|
-
const userHidden = currentSpec.hiddenSeries ?? [];
|
|
6992
|
-
return userHidden.includes(series);
|
|
6993
|
-
}
|
|
6994
|
-
function toggleSeriesVisibility(series) {
|
|
6995
|
-
const wasHidden = isSeriesHidden(series);
|
|
6996
|
-
if (!wasHidden) {
|
|
6997
|
-
const total = countSeries(currentSpec);
|
|
6998
|
-
if (Number.isFinite(total)) {
|
|
6999
|
-
const userHidden = new Set(currentSpec.hiddenSeries ?? []);
|
|
7000
|
-
let visibleAfter = 0;
|
|
7001
|
-
const seriesField = getColorField(currentSpec);
|
|
7002
|
-
if (seriesField) {
|
|
7003
|
-
const allSeries = /* @__PURE__ */ new Set();
|
|
7004
|
-
for (const row of currentSpec.data ?? []) {
|
|
7005
|
-
allSeries.add(String(row[seriesField]));
|
|
7006
|
-
}
|
|
7007
|
-
for (const s of allSeries) {
|
|
7008
|
-
if (s === series) continue;
|
|
7009
|
-
if (runtimeShownSeries.has(s)) {
|
|
7010
|
-
visibleAfter++;
|
|
7011
|
-
continue;
|
|
7012
|
-
}
|
|
7013
|
-
if (runtimeHiddenSeries.has(s)) continue;
|
|
7014
|
-
if (!userHidden.has(s)) visibleAfter++;
|
|
7015
|
-
}
|
|
7016
|
-
}
|
|
7017
|
-
if (visibleAfter === 0) return false;
|
|
7018
|
-
}
|
|
7019
|
-
}
|
|
7020
|
-
if (wasHidden) {
|
|
7021
|
-
runtimeHiddenSeries.delete(series);
|
|
7022
|
-
const userHidden = currentSpec.hiddenSeries ?? [];
|
|
7023
|
-
if (userHidden.includes(series)) runtimeShownSeries.add(series);
|
|
7024
|
-
} else {
|
|
7025
|
-
runtimeShownSeries.delete(series);
|
|
7026
|
-
runtimeHiddenSeries.add(series);
|
|
7027
|
-
}
|
|
7028
|
-
render();
|
|
7029
|
-
return !wasHidden;
|
|
7030
|
-
}
|
|
7031
|
-
function getColorField(spec2) {
|
|
7032
|
-
if (isLayerSpec(spec2)) return void 0;
|
|
7033
|
-
const colorEnc = spec2.encoding?.color;
|
|
7034
|
-
if (!colorEnc || "condition" in colorEnc) return void 0;
|
|
7035
|
-
if (!("field" in colorEnc) || !colorEnc.field) return void 0;
|
|
7036
|
-
return colorEnc.field;
|
|
7037
|
-
}
|
|
7038
|
-
function getContainerDimensions() {
|
|
7039
|
-
const rect = container.getBoundingClientRect();
|
|
7040
|
-
const isSparkline = "display" in currentSpec && currentSpec.display === "sparkline";
|
|
7041
|
-
if (isSparkline) {
|
|
7042
|
-
let width = rect.width;
|
|
7043
|
-
let height = rect.height;
|
|
7044
|
-
if (!height && container.parentElement) {
|
|
7045
|
-
const parentRect = container.parentElement.getBoundingClientRect();
|
|
7046
|
-
height = parentRect.height;
|
|
7047
|
-
if (!width) width = parentRect.width;
|
|
7048
|
-
}
|
|
7049
|
-
return {
|
|
7050
|
-
width: Math.max(width || 200, 30),
|
|
7051
|
-
height: Math.max(height || 40, 20)
|
|
7052
|
-
};
|
|
7053
|
-
}
|
|
7054
|
-
if (isAutoHeight === null && (rect.width > 0 || rect.height > 0)) {
|
|
7055
|
-
isAutoHeight = rect.height === 0 && rect.width > 0;
|
|
7056
|
-
}
|
|
7057
|
-
return {
|
|
7058
|
-
width: Math.max(rect.width || 600, 100),
|
|
7059
|
-
// Latched-auto mounts always compile against the fixed budget: after
|
|
7060
|
-
// the first render the container's measured height is just our own
|
|
7061
|
-
// SVG, so reading it back would re-pin whatever we rendered last.
|
|
7062
|
-
height: isAutoHeight === true ? FALLBACK_HEIGHT : Math.max(rect.height || FALLBACK_HEIGHT, 100)
|
|
7063
|
-
};
|
|
7064
|
-
}
|
|
7065
|
-
function getSpecAnnotations() {
|
|
7066
|
-
return "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
|
|
7067
|
-
}
|
|
7068
|
-
function selectElement(ref) {
|
|
7069
|
-
if (!svgElement) return;
|
|
7070
|
-
const target = findElementByRef(svgElement, ref);
|
|
7071
|
-
if (!target) return;
|
|
7072
|
-
if (selectedElement && !refsEqual(selectedElement, ref)) {
|
|
7073
|
-
deselectElement();
|
|
7074
|
-
}
|
|
7075
|
-
selectedElement = ref;
|
|
7076
|
-
overlayElement = renderSelectionOverlay(svgElement, ref, currentLayout);
|
|
7077
|
-
options?.onSelect?.(ref);
|
|
7078
|
-
svgElement.focus();
|
|
7079
|
-
}
|
|
7080
|
-
function deselectElement() {
|
|
7081
|
-
if (!selectedElement) return;
|
|
7082
|
-
if (isTextEditingActive && textEditCleanup) {
|
|
7083
|
-
textEditCleanup();
|
|
7084
|
-
textEditCleanup = null;
|
|
7085
|
-
isTextEditingActive = false;
|
|
7086
|
-
}
|
|
7087
|
-
const prev = selectedElement;
|
|
7088
|
-
selectedElement = null;
|
|
7089
|
-
if (overlayElement?.parentNode) {
|
|
7090
|
-
overlayElement.parentNode.removeChild(overlayElement);
|
|
7091
|
-
}
|
|
7092
|
-
overlayElement = null;
|
|
7093
|
-
options?.onDeselect?.(prev);
|
|
7094
|
-
}
|
|
7095
|
-
function enterTextEditing() {
|
|
7096
|
-
if (!svgElement || !selectedElement || isTextEditingActive) return;
|
|
7097
|
-
const specAnnotations = getSpecAnnotations();
|
|
7098
|
-
if (!isTextEditable(selectedElement, specAnnotations)) return;
|
|
7099
|
-
const currentText = getElementText(selectedElement, currentSpec);
|
|
7100
|
-
if (currentText === null) return;
|
|
7101
|
-
const target = findElementByRef(svgElement, selectedElement);
|
|
7102
|
-
if (!target) return;
|
|
7103
|
-
const textEl = target.tagName === "text" ? target : target.querySelector("text");
|
|
7104
|
-
if (!textEl) return;
|
|
7105
|
-
isTextEditingActive = true;
|
|
7106
|
-
const editRef = selectedElement;
|
|
7107
|
-
const overlay = createTextEditOverlay({
|
|
7108
|
-
container,
|
|
7109
|
-
svg: svgElement,
|
|
7110
|
-
targetElement: textEl,
|
|
7111
|
-
currentText,
|
|
7112
|
-
onCommit: (newText) => {
|
|
7113
|
-
isTextEditingActive = false;
|
|
7114
|
-
textEditCleanup = null;
|
|
7115
|
-
if (newText !== currentText) {
|
|
7116
|
-
options?.onTextEdit?.(editRef, currentText, newText);
|
|
7117
|
-
options?.onEdit?.({
|
|
7118
|
-
type: "text-edit",
|
|
7119
|
-
element: editRef,
|
|
7120
|
-
oldText: currentText,
|
|
7121
|
-
newText
|
|
7122
|
-
});
|
|
7123
|
-
}
|
|
7124
|
-
},
|
|
7125
|
-
onCancel: () => {
|
|
7126
|
-
isTextEditingActive = false;
|
|
7127
|
-
textEditCleanup = null;
|
|
7128
|
-
}
|
|
7129
|
-
});
|
|
7130
|
-
textEditCleanup = overlay.destroy;
|
|
7131
|
-
}
|
|
7132
|
-
function wireSelectionEvents() {
|
|
7133
|
-
if (!svgElement) return () => {
|
|
7134
|
-
};
|
|
7135
|
-
const svg = svgElement;
|
|
7136
|
-
const cleanups = [];
|
|
7137
|
-
const handleClick = (e) => {
|
|
7138
|
-
const mouseEvent = e;
|
|
7139
|
-
const target = mouseEvent.target;
|
|
7140
|
-
if (isTextEditingActive) return;
|
|
7141
|
-
const specAnnotations = getSpecAnnotations();
|
|
7142
|
-
const ref = buildElementRef(target, specAnnotations);
|
|
7143
|
-
if (ref) {
|
|
7144
|
-
selectElement(ref);
|
|
7145
|
-
} else {
|
|
7146
|
-
deselectElement();
|
|
7147
|
-
}
|
|
7148
|
-
};
|
|
7149
|
-
svg.addEventListener("click", handleClick);
|
|
7150
|
-
cleanups.push(() => svg.removeEventListener("click", handleClick));
|
|
7151
|
-
const handleMouseEnter = (e) => {
|
|
7152
|
-
const target = e.target.closest(
|
|
7153
|
-
"[data-annotation-index], [data-chrome-key], .oc-mark-label[data-series], .oc-legend, [data-legend-index]"
|
|
7154
|
-
);
|
|
7155
|
-
if (target) {
|
|
7156
|
-
target.classList.add("oc-editable-hover");
|
|
7157
|
-
}
|
|
7158
|
-
};
|
|
7159
|
-
const handleMouseLeave = (e) => {
|
|
7160
|
-
const target = e.target.closest(".oc-editable-hover");
|
|
7161
|
-
if (target) {
|
|
7162
|
-
target.classList.remove("oc-editable-hover");
|
|
7163
|
-
}
|
|
7164
|
-
};
|
|
7165
|
-
svg.addEventListener("mouseenter", handleMouseEnter, true);
|
|
7166
|
-
svg.addEventListener("mouseleave", handleMouseLeave, true);
|
|
7167
|
-
cleanups.push(() => {
|
|
7168
|
-
svg.removeEventListener("mouseenter", handleMouseEnter, true);
|
|
7169
|
-
svg.removeEventListener("mouseleave", handleMouseLeave, true);
|
|
7170
|
-
});
|
|
7171
|
-
const handleDblClick = (e) => {
|
|
7172
|
-
const mouseEvent = e;
|
|
7173
|
-
const target = mouseEvent.target;
|
|
7174
|
-
const specAnnotations = getSpecAnnotations();
|
|
7175
|
-
const ref = buildElementRef(target, specAnnotations);
|
|
7176
|
-
if (ref && isTextEditable(ref, specAnnotations)) {
|
|
7177
|
-
if (!refsEqual(selectedElement, ref)) {
|
|
7178
|
-
selectElement(ref);
|
|
7179
|
-
}
|
|
7180
|
-
enterTextEditing();
|
|
7181
|
-
}
|
|
7182
|
-
};
|
|
7183
|
-
svg.addEventListener("dblclick", handleDblClick);
|
|
7184
|
-
cleanups.push(() => svg.removeEventListener("dblclick", handleDblClick));
|
|
7185
|
-
return () => {
|
|
7186
|
-
for (const cleanup of cleanups) {
|
|
7187
|
-
cleanup();
|
|
7188
|
-
}
|
|
7189
|
-
};
|
|
7190
|
-
}
|
|
7191
|
-
function wireKeyboardEditEvents() {
|
|
7192
|
-
if (!svgElement) return () => {
|
|
7193
|
-
};
|
|
7194
|
-
const svg = svgElement;
|
|
7195
|
-
const handleKeyDown = (e) => {
|
|
7196
|
-
const specAnnotations = getSpecAnnotations();
|
|
7197
|
-
switch (e.key) {
|
|
7198
|
-
case "Delete":
|
|
7199
|
-
case "Backspace": {
|
|
7200
|
-
if (selectedElement && !isTextEditingActive) {
|
|
7201
|
-
e.preventDefault();
|
|
7202
|
-
options?.onEdit?.({ type: "delete", element: selectedElement });
|
|
7203
|
-
}
|
|
7204
|
-
break;
|
|
7205
|
-
}
|
|
7206
|
-
case "Escape": {
|
|
7207
|
-
e.preventDefault();
|
|
7208
|
-
if (isTextEditingActive && textEditCleanup) {
|
|
7209
|
-
textEditCleanup();
|
|
7210
|
-
textEditCleanup = null;
|
|
7211
|
-
isTextEditingActive = false;
|
|
7212
|
-
} else if (selectedElement) {
|
|
7213
|
-
deselectElement();
|
|
7214
|
-
}
|
|
7215
|
-
break;
|
|
7216
|
-
}
|
|
7217
|
-
case "ArrowDown":
|
|
7218
|
-
case "ArrowRight": {
|
|
7219
|
-
if (!isTextEditingActive && selectedElement) {
|
|
7220
|
-
e.preventDefault();
|
|
7221
|
-
const editables = getEditableElements(currentSpec, currentLayout);
|
|
7222
|
-
if (editables.length === 0) break;
|
|
7223
|
-
const currentIndex = editables.findIndex((r) => refsEqual(r, selectedElement));
|
|
7224
|
-
const nextIndex = currentIndex >= editables.length - 1 ? 0 : currentIndex + 1;
|
|
7225
|
-
selectElement(editables[nextIndex]);
|
|
7226
|
-
}
|
|
7227
|
-
break;
|
|
7228
|
-
}
|
|
7229
|
-
case "ArrowUp":
|
|
7230
|
-
case "ArrowLeft": {
|
|
7231
|
-
if (!isTextEditingActive && selectedElement) {
|
|
7232
|
-
e.preventDefault();
|
|
7233
|
-
const editables = getEditableElements(currentSpec, currentLayout);
|
|
7234
|
-
if (editables.length === 0) break;
|
|
7235
|
-
const currentIndex = editables.findIndex((r) => refsEqual(r, selectedElement));
|
|
7236
|
-
const nextIndex = currentIndex <= 0 ? editables.length - 1 : currentIndex - 1;
|
|
7237
|
-
selectElement(editables[nextIndex]);
|
|
7238
|
-
}
|
|
7239
|
-
break;
|
|
7240
|
-
}
|
|
7241
|
-
case "Enter": {
|
|
7242
|
-
if (selectedElement && !isTextEditingActive) {
|
|
7243
|
-
if (isTextEditable(selectedElement, specAnnotations)) {
|
|
7244
|
-
e.preventDefault();
|
|
7245
|
-
enterTextEditing();
|
|
7246
|
-
}
|
|
7247
|
-
}
|
|
7248
|
-
break;
|
|
7249
|
-
}
|
|
7250
|
-
}
|
|
7251
|
-
};
|
|
7252
|
-
svg.addEventListener("keydown", handleKeyDown);
|
|
7253
|
-
return () => {
|
|
7254
|
-
svg.removeEventListener("keydown", handleKeyDown);
|
|
7255
|
-
};
|
|
7256
|
-
}
|
|
7257
|
-
function render() {
|
|
7258
|
-
if (isDragging) {
|
|
7259
|
-
pendingRender = true;
|
|
7260
|
-
return;
|
|
7261
|
-
}
|
|
7262
|
-
if (transitionHandle) {
|
|
7263
|
-
transitionHandle.cancel();
|
|
7264
|
-
transitionHandle = null;
|
|
7265
|
-
}
|
|
7266
|
-
if (cleanupAnimations) {
|
|
7267
|
-
cleanupAnimations();
|
|
7268
|
-
cleanupAnimations = null;
|
|
7269
|
-
}
|
|
7270
|
-
cancelAnimations(svgElement);
|
|
7271
|
-
cleanupTooltipEvents?.();
|
|
7272
|
-
cleanupTooltipEvents = null;
|
|
7273
|
-
cleanupVoronoiEvents?.();
|
|
7274
|
-
cleanupVoronoiEvents = null;
|
|
7275
|
-
cleanupKeyboardNav?.();
|
|
7276
|
-
cleanupKeyboardNav = null;
|
|
7277
|
-
cleanupLegend?.();
|
|
7278
|
-
cleanupLegend = null;
|
|
7279
|
-
cleanupChartEvents?.();
|
|
7280
|
-
cleanupChartEvents = null;
|
|
7281
|
-
cleanupAnnotationDrag?.();
|
|
7282
|
-
cleanupAnnotationDrag = null;
|
|
7283
|
-
cleanupEditDrags?.();
|
|
7284
|
-
cleanupEditDrags = null;
|
|
7285
|
-
cleanupSelection?.();
|
|
7286
|
-
cleanupSelection = null;
|
|
7287
|
-
cleanupKeyboardEdit?.();
|
|
7288
|
-
cleanupKeyboardEdit = null;
|
|
7289
|
-
if (textEditCleanup) {
|
|
7290
|
-
textEditCleanup();
|
|
7291
|
-
textEditCleanup = null;
|
|
7292
|
-
isTextEditingActive = false;
|
|
7293
|
-
}
|
|
7294
|
-
overlayElement = null;
|
|
7295
|
-
if (svgElement?.parentNode) {
|
|
7296
|
-
svgElement.parentNode.removeChild(svgElement);
|
|
7297
|
-
}
|
|
7298
|
-
if (tooltipManager) {
|
|
7299
|
-
tooltipManager.destroy();
|
|
7300
|
-
}
|
|
7301
|
-
if (srTable?.parentNode) {
|
|
7302
|
-
srTable.parentNode.removeChild(srTable);
|
|
7303
|
-
srTable = null;
|
|
7304
|
-
}
|
|
7305
|
-
currentLayout = compile();
|
|
7306
|
-
lastRenderedWidth = currentLayout.dimensions.width;
|
|
7307
|
-
lastRenderedSvgHeight = currentLayout.dimensions.height;
|
|
7308
|
-
const shouldAnimate = isFirstRender && !!currentLayout.animation?.enter;
|
|
7309
|
-
const crosshair = !!currentLayout.crosshair;
|
|
7310
|
-
svgElement = renderChartSVG(currentLayout, container, {
|
|
7311
|
-
animate: shouldAnimate,
|
|
7312
|
-
crosshair
|
|
7313
|
-
});
|
|
7314
|
-
tooltipManager = createTooltipManager(container);
|
|
7315
|
-
cleanupTooltipEvents = wireTooltipEvents(
|
|
7316
|
-
svgElement,
|
|
7317
|
-
currentLayout.tooltipDescriptors,
|
|
7318
|
-
tooltipManager
|
|
7319
|
-
);
|
|
7320
|
-
cleanupVoronoiEvents = wireVoronoiTooltipEvents(svgElement, currentLayout, tooltipManager);
|
|
7321
|
-
cleanupKeyboardNav = wireKeyboardNav(
|
|
7322
|
-
svgElement,
|
|
7323
|
-
container,
|
|
7324
|
-
currentLayout.tooltipDescriptors,
|
|
7325
|
-
tooltipManager,
|
|
7326
|
-
currentLayout
|
|
7327
|
-
);
|
|
7328
|
-
cleanupLegend = wireLegendInteraction(
|
|
7329
|
-
svgElement,
|
|
7330
|
-
currentLayout,
|
|
7331
|
-
toggleSeriesVisibility,
|
|
7332
|
-
options?.onLegendToggle,
|
|
7333
|
-
options?.onEdit
|
|
7334
|
-
);
|
|
7335
|
-
if (options?.onMarkClick || options?.onMarkHover || options?.onMarkLeave || options?.onAnnotationClick) {
|
|
7336
|
-
const specAnnotations = "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
|
|
7337
|
-
cleanupChartEvents = wireChartEvents(svgElement, currentLayout, specAnnotations, options);
|
|
7338
|
-
}
|
|
7339
|
-
const setDragging = (dragging) => {
|
|
7340
|
-
isDragging = dragging;
|
|
7341
|
-
if (!dragging && pendingRender) {
|
|
7342
|
-
pendingRender = false;
|
|
7343
|
-
render();
|
|
7344
|
-
}
|
|
7345
|
-
};
|
|
7346
|
-
const dragAnnotations = "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
|
|
7347
|
-
if (options?.onAnnotationEdit || options?.onEdit) {
|
|
7348
|
-
cleanupAnnotationDrag = wireAnnotationDrag(
|
|
7349
|
-
svgElement,
|
|
7350
|
-
dragAnnotations,
|
|
7351
|
-
options?.onAnnotationEdit,
|
|
7352
|
-
options?.onEdit,
|
|
7353
|
-
setDragging
|
|
7354
|
-
);
|
|
7355
|
-
}
|
|
7356
|
-
if (options?.onEdit) {
|
|
7357
|
-
const editCleanups = [];
|
|
7358
|
-
editCleanups.push(
|
|
7359
|
-
wireConnectorEndpointDrag(svgElement, dragAnnotations, options.onEdit, setDragging)
|
|
7360
|
-
);
|
|
7361
|
-
editCleanups.push(
|
|
7362
|
-
wireAnnotationLabelDrag(svgElement, dragAnnotations, options.onEdit, setDragging)
|
|
7363
|
-
);
|
|
7364
|
-
const editSpec = currentSpec;
|
|
7365
|
-
editCleanups.push(wireChromeDrag(svgElement, editSpec, options.onEdit, setDragging));
|
|
7366
|
-
editCleanups.push(wireLegendDrag(svgElement, editSpec, options.onEdit, setDragging));
|
|
7367
|
-
editCleanups.push(wireSeriesLabelDrag(svgElement, editSpec, options.onEdit, setDragging));
|
|
7368
|
-
cleanupEditDrags = () => {
|
|
7369
|
-
for (const cleanup of editCleanups) {
|
|
7370
|
-
cleanup();
|
|
7371
|
-
}
|
|
7372
|
-
};
|
|
7373
|
-
}
|
|
7374
|
-
if (hasEditingCallbacks(options)) {
|
|
7375
|
-
makeEditable(svgElement);
|
|
7376
|
-
cleanupSelection = wireSelectionEvents();
|
|
7377
|
-
cleanupKeyboardEdit = wireKeyboardEditEvents();
|
|
7378
|
-
if (selectedElement) {
|
|
7379
|
-
const target = findElementByRef(svgElement, selectedElement);
|
|
7380
|
-
if (target) {
|
|
7381
|
-
overlayElement = renderSelectionOverlay(svgElement, selectedElement, currentLayout);
|
|
7382
|
-
} else {
|
|
7383
|
-
selectedElement = null;
|
|
7384
|
-
overlayElement = null;
|
|
7385
|
-
}
|
|
7386
|
-
}
|
|
7387
|
-
}
|
|
7388
|
-
srTable = createScreenReaderTable(currentLayout, container);
|
|
7389
|
-
container.classList.add("oc-root");
|
|
7390
|
-
const isDark = resolveDarkMode3(options?.darkMode);
|
|
7391
|
-
if (isDark) {
|
|
7392
|
-
container.classList.add("oc-dark");
|
|
7393
|
-
} else {
|
|
7394
|
-
container.classList.remove("oc-dark");
|
|
7395
|
-
}
|
|
7396
|
-
stampThemeProperties(container, currentLayout.theme);
|
|
7397
|
-
if (shouldAnimate && svgElement) {
|
|
7398
|
-
cleanupAnimations = setupAnimationCleanup(svgElement, () => {
|
|
7399
|
-
cleanupAnimations = null;
|
|
7400
|
-
if (pendingResize) {
|
|
7401
|
-
pendingResize = false;
|
|
7402
|
-
resize();
|
|
7403
|
-
}
|
|
7404
|
-
});
|
|
7405
|
-
}
|
|
7406
|
-
renderGen += 1;
|
|
7407
|
-
container.dataset.ocRenderGen = String(renderGen);
|
|
7408
|
-
if (fontsReloadPending) {
|
|
7409
|
-
fontsReloadPending = false;
|
|
7410
|
-
container.dataset.ocFontsState = "ready";
|
|
7411
|
-
}
|
|
7412
|
-
if (isFirstRender) {
|
|
7413
|
-
isFirstRender = false;
|
|
7414
|
-
}
|
|
7415
|
-
}
|
|
7416
|
-
function update(newSpec, updateOpts) {
|
|
7417
|
-
if (destroyed) return;
|
|
7418
|
-
const prevSpec = currentSpec;
|
|
7419
|
-
const prevLayout = currentLayout;
|
|
7420
|
-
const entranceWasRunning = cleanupAnimations != null;
|
|
7421
|
-
if (transitionHandle?.running) {
|
|
7422
|
-
transitionSnapshot = transitionHandle.snapshot();
|
|
7423
|
-
}
|
|
7424
|
-
currentSpec = newSpec;
|
|
7425
|
-
const nextFont = resolveEffectiveFont();
|
|
7426
|
-
if (nextFont !== fontFamily) {
|
|
7427
|
-
fontFamily = nextFont;
|
|
7428
|
-
measureText = createMeasureText(fontFamily);
|
|
7429
|
-
}
|
|
7430
|
-
runtimeHiddenSeries.clear();
|
|
7431
|
-
runtimeShownSeries.clear();
|
|
7432
|
-
if (updateOpts && "selectedElement" in updateOpts) {
|
|
7433
|
-
selectedElement = updateOpts.selectedElement ?? null;
|
|
7434
|
-
}
|
|
7435
|
-
render();
|
|
7436
|
-
if (svgElement && canTransition({
|
|
7437
|
-
prevLayout,
|
|
7438
|
-
nextLayout: currentLayout,
|
|
7439
|
-
prevSpec,
|
|
7440
|
-
nextSpec: newSpec,
|
|
7441
|
-
isFirstRender: false,
|
|
7442
|
-
entranceInFlight: entranceWasRunning
|
|
7443
|
-
})) {
|
|
7444
|
-
const snapshot = transitionSnapshot;
|
|
7445
|
-
transitionSnapshot = null;
|
|
7446
|
-
transitionHandle = runTransition({
|
|
7447
|
-
svg: svgElement,
|
|
7448
|
-
prevLayout,
|
|
7449
|
-
nextLayout: currentLayout,
|
|
7450
|
-
animation: currentLayout.animation,
|
|
7451
|
-
fromSnapshot: snapshot ?? void 0,
|
|
7452
|
-
onComplete: () => {
|
|
7453
|
-
transitionHandle = null;
|
|
7454
|
-
}
|
|
7455
|
-
});
|
|
7456
|
-
} else {
|
|
7457
|
-
transitionSnapshot = null;
|
|
7458
|
-
}
|
|
7459
|
-
}
|
|
7460
|
-
function resize() {
|
|
7461
|
-
if (destroyed) return;
|
|
7462
|
-
if (cleanupAnimations) {
|
|
7463
|
-
pendingResize = true;
|
|
7464
|
-
return;
|
|
7465
|
-
}
|
|
7466
|
-
render();
|
|
7467
|
-
}
|
|
7468
|
-
function setHighlight(values) {
|
|
7469
|
-
if (destroyed) return;
|
|
7470
|
-
if (isLayerSpec(currentSpec) || isGraphSpec(currentSpec))
|
|
7471
|
-
return;
|
|
7472
|
-
const spec2 = currentSpec;
|
|
7473
|
-
const colorEnc = spec2.encoding?.color;
|
|
7474
|
-
if (!colorEnc || typeof colorEnc !== "object" || !("field" in colorEnc)) return;
|
|
7475
|
-
const current = colorEnc.highlight;
|
|
7476
|
-
if (!values?.length && !current) return;
|
|
7477
|
-
const updatedColor = { ...colorEnc };
|
|
7478
|
-
if (values && values.length > 0) {
|
|
7479
|
-
updatedColor.highlight = values;
|
|
7480
|
-
} else {
|
|
7481
|
-
delete updatedColor.highlight;
|
|
7482
|
-
}
|
|
7483
|
-
currentSpec = {
|
|
7484
|
-
...spec2,
|
|
7485
|
-
encoding: { ...spec2.encoding, color: updatedColor }
|
|
7486
|
-
};
|
|
7487
|
-
render();
|
|
7488
|
-
}
|
|
7489
|
-
function doExport(format, exportOptions) {
|
|
7490
|
-
if (!svgElement) {
|
|
7491
|
-
throw new Error("Chart is not rendered yet");
|
|
7492
|
-
}
|
|
7493
|
-
switch (format) {
|
|
7494
|
-
case "svg":
|
|
7495
|
-
return exportSVG(svgElement);
|
|
7496
|
-
case "svg-with-fonts":
|
|
7497
|
-
return exportSVGWithFonts(svgElement, exportOptions);
|
|
7498
|
-
case "png":
|
|
7499
|
-
return exportPNG(svgElement, exportOptions);
|
|
7500
|
-
case "jpg":
|
|
7501
|
-
return exportJPG(svgElement, exportOptions);
|
|
7502
|
-
case "csv":
|
|
7503
|
-
return exportCSV(
|
|
7504
|
-
"data" in currentSpec && Array.isArray(currentSpec.data) ? currentSpec.data : []
|
|
7505
|
-
);
|
|
7506
|
-
default:
|
|
7507
|
-
throw new Error(`Unsupported export format: ${format}`);
|
|
7508
|
-
}
|
|
7509
|
-
}
|
|
7510
|
-
function destroy() {
|
|
7511
|
-
if (destroyed) return;
|
|
7512
|
-
destroyed = true;
|
|
7513
|
-
if (transitionHandle) {
|
|
7514
|
-
transitionHandle.cancel();
|
|
7515
|
-
transitionHandle = null;
|
|
7516
|
-
}
|
|
7517
|
-
transitionSnapshot = null;
|
|
7518
|
-
if (cleanupAnimations) {
|
|
7519
|
-
cleanupAnimations();
|
|
7520
|
-
cleanupAnimations = null;
|
|
7521
|
-
pendingResize = false;
|
|
7522
|
-
}
|
|
7523
|
-
cancelAnimations(svgElement);
|
|
7524
|
-
cleanupTooltipEvents?.();
|
|
7525
|
-
cleanupTooltipEvents = null;
|
|
7526
|
-
cleanupVoronoiEvents?.();
|
|
7527
|
-
cleanupVoronoiEvents = null;
|
|
7528
|
-
cleanupKeyboardNav?.();
|
|
7529
|
-
cleanupKeyboardNav = null;
|
|
7530
|
-
cleanupLegend?.();
|
|
7531
|
-
cleanupLegend = null;
|
|
7532
|
-
cleanupChartEvents?.();
|
|
7533
|
-
cleanupChartEvents = null;
|
|
7534
|
-
cleanupAnnotationDrag?.();
|
|
7535
|
-
cleanupAnnotationDrag = null;
|
|
7536
|
-
cleanupEditDrags?.();
|
|
7537
|
-
cleanupEditDrags = null;
|
|
7538
|
-
cleanupSelection?.();
|
|
7539
|
-
cleanupSelection = null;
|
|
7540
|
-
cleanupKeyboardEdit?.();
|
|
7541
|
-
cleanupKeyboardEdit = null;
|
|
7542
|
-
if (textEditCleanup) {
|
|
7543
|
-
textEditCleanup();
|
|
7544
|
-
textEditCleanup = null;
|
|
7545
|
-
isTextEditingActive = false;
|
|
7546
|
-
}
|
|
7547
|
-
selectedElement = null;
|
|
7548
|
-
overlayElement = null;
|
|
7549
|
-
if (disconnectResize) {
|
|
7550
|
-
disconnectResize();
|
|
7551
|
-
disconnectResize = null;
|
|
7552
|
-
}
|
|
7553
|
-
if (tooltipManager) {
|
|
7554
|
-
tooltipManager.destroy();
|
|
7555
|
-
tooltipManager = null;
|
|
7556
|
-
}
|
|
7557
|
-
if (svgElement?.parentNode) {
|
|
7558
|
-
svgElement.parentNode.removeChild(svgElement);
|
|
7559
|
-
svgElement = null;
|
|
7560
|
-
}
|
|
7561
|
-
if (srTable?.parentNode) {
|
|
7562
|
-
srTable.parentNode.removeChild(srTable);
|
|
7563
|
-
srTable = null;
|
|
7564
|
-
}
|
|
7565
|
-
container.classList.remove("oc-dark");
|
|
7566
|
-
container.classList.remove("oc-root");
|
|
7567
|
-
}
|
|
7568
|
-
render();
|
|
7569
|
-
const pending = scheduleFontReload(
|
|
7570
|
-
fontFamily,
|
|
7571
|
-
() => !destroyed,
|
|
7572
|
-
() => {
|
|
7573
|
-
fontsReloadPending = true;
|
|
7574
|
-
resize();
|
|
7575
|
-
}
|
|
7576
|
-
);
|
|
7577
|
-
container.dataset.ocFontsState = pending ? "pending" : "ready";
|
|
7578
|
-
if (options?.responsive !== false) {
|
|
7579
|
-
disconnectResize = observeResize(container, (width, height) => {
|
|
7580
|
-
if (isAutoHeight === true) {
|
|
7581
|
-
const widthChanged = width !== lastRenderedWidth;
|
|
7582
|
-
const selfInducedHeight = height === lastRenderedSvgHeight;
|
|
7583
|
-
if (!widthChanged && selfInducedHeight) return;
|
|
7584
|
-
if (!selfInducedHeight) {
|
|
7585
|
-
isAutoHeight = null;
|
|
7586
|
-
}
|
|
7587
|
-
resize();
|
|
7588
|
-
return;
|
|
7589
|
-
}
|
|
7590
|
-
resize();
|
|
7591
|
-
});
|
|
7592
|
-
}
|
|
7593
|
-
return {
|
|
7594
|
-
update,
|
|
7595
|
-
resize,
|
|
7596
|
-
export: doExport,
|
|
7597
|
-
destroy,
|
|
7598
|
-
get layout() {
|
|
7599
|
-
return currentLayout;
|
|
7600
|
-
},
|
|
7601
|
-
getSelectedElement() {
|
|
7602
|
-
return selectedElement;
|
|
7603
|
-
},
|
|
7604
|
-
select(ref) {
|
|
7605
|
-
if (destroyed) return;
|
|
7606
|
-
selectElement(ref);
|
|
7607
|
-
},
|
|
7608
|
-
deselect() {
|
|
7609
|
-
if (destroyed) return;
|
|
7610
|
-
deselectElement();
|
|
7611
|
-
},
|
|
7612
|
-
get isEditing() {
|
|
7613
|
-
return isTextEditingActive;
|
|
7614
|
-
},
|
|
7615
|
-
setHighlight
|
|
7616
|
-
};
|
|
7617
|
-
}
|
|
7618
|
-
|
|
7619
|
-
// src/renderers/table-cells.ts
|
|
7620
|
-
function applyCellStyle(td, cell) {
|
|
7621
|
-
if (cell.style.backgroundColor) {
|
|
7622
|
-
td.style.background = cell.style.backgroundColor;
|
|
7623
|
-
}
|
|
7624
|
-
if (cell.style.color) {
|
|
7625
|
-
td.style.color = cell.style.color;
|
|
7626
|
-
}
|
|
7627
|
-
if (cell.style.fontWeight) {
|
|
7628
|
-
td.style.fontWeight = String(cell.style.fontWeight);
|
|
7629
|
-
}
|
|
7630
|
-
if (cell.style.fontVariant) {
|
|
7631
|
-
td.style.fontVariant = cell.style.fontVariant;
|
|
7632
|
-
}
|
|
7633
|
-
if (cell.aria) {
|
|
7634
|
-
td.setAttribute("aria-label", cell.aria);
|
|
7635
|
-
}
|
|
7636
|
-
}
|
|
7637
|
-
function countryToEmoji(code) {
|
|
7638
|
-
return [...code.toUpperCase()].map((c2) => String.fromCodePoint(c2.charCodeAt(0) + 127397)).join("");
|
|
7639
|
-
}
|
|
7640
|
-
var COUNTRY_NAMES = {
|
|
7641
|
-
US: "United States",
|
|
7642
|
-
CN: "China",
|
|
7643
|
-
IN: "India",
|
|
7644
|
-
ID: "Indonesia",
|
|
7645
|
-
BR: "Brazil",
|
|
7646
|
-
PK: "Pakistan",
|
|
7647
|
-
NG: "Nigeria",
|
|
7648
|
-
BD: "Bangladesh",
|
|
7649
|
-
RU: "Russia",
|
|
7650
|
-
MX: "Mexico",
|
|
7651
|
-
JP: "Japan",
|
|
7652
|
-
DE: "Germany",
|
|
7653
|
-
GB: "United Kingdom",
|
|
7654
|
-
FR: "France",
|
|
7655
|
-
IT: "Italy",
|
|
7656
|
-
CA: "Canada",
|
|
7657
|
-
AU: "Australia",
|
|
7658
|
-
KR: "South Korea",
|
|
7659
|
-
ES: "Spain",
|
|
7660
|
-
AR: "Argentina",
|
|
7661
|
-
CO: "Colombia",
|
|
7662
|
-
ZA: "South Africa",
|
|
7663
|
-
TR: "Turkey",
|
|
7664
|
-
SA: "Saudi Arabia",
|
|
7665
|
-
UA: "Ukraine",
|
|
7666
|
-
PL: "Poland",
|
|
7667
|
-
NL: "Netherlands",
|
|
7668
|
-
SE: "Sweden",
|
|
7669
|
-
NO: "Norway",
|
|
7670
|
-
DK: "Denmark",
|
|
7671
|
-
FI: "Finland",
|
|
7672
|
-
CH: "Switzerland",
|
|
7673
|
-
AT: "Austria",
|
|
7674
|
-
BE: "Belgium",
|
|
7675
|
-
PT: "Portugal",
|
|
7676
|
-
IE: "Ireland",
|
|
7677
|
-
NZ: "New Zealand",
|
|
7678
|
-
SG: "Singapore",
|
|
7679
|
-
IL: "Israel",
|
|
7680
|
-
AE: "United Arab Emirates",
|
|
7681
|
-
EG: "Egypt",
|
|
7682
|
-
TH: "Thailand",
|
|
7683
|
-
VN: "Vietnam",
|
|
7684
|
-
PH: "Philippines",
|
|
7685
|
-
MY: "Malaysia",
|
|
7686
|
-
CL: "Chile",
|
|
7687
|
-
PE: "Peru",
|
|
7688
|
-
CZ: "Czech Republic",
|
|
7689
|
-
GR: "Greece",
|
|
7690
|
-
HU: "Hungary",
|
|
7691
|
-
RO: "Romania",
|
|
7692
|
-
ET: "Ethiopia"
|
|
7693
|
-
};
|
|
7694
|
-
function getCountryName(code) {
|
|
7695
|
-
return COUNTRY_NAMES[code.toUpperCase()] || code.toUpperCase();
|
|
7696
|
-
}
|
|
7697
|
-
function describeSparklineTrend(data) {
|
|
7698
|
-
if (data.type === "line" && data.points.length >= 2) {
|
|
7699
|
-
const first = data.points[0].y;
|
|
7700
|
-
const last = data.points[data.points.length - 1].y;
|
|
7701
|
-
const count = data.points.length;
|
|
7702
|
-
if (last > first) return `Sparkline with ${count} points, trending upward`;
|
|
7703
|
-
if (last < first) return `Sparkline with ${count} points, trending downward`;
|
|
7704
|
-
return `Sparkline with ${count} points, roughly flat`;
|
|
7705
|
-
}
|
|
7706
|
-
if ((data.type === "bar" || data.type === "column") && data.bars.length > 0) {
|
|
7707
|
-
return `${data.type === "column" ? "Column" : "Bar"} sparkline with ${data.bars.length} values`;
|
|
7708
|
-
}
|
|
7709
|
-
return "Sparkline";
|
|
7710
|
-
}
|
|
7711
|
-
function renderTextCell(cell) {
|
|
7712
|
-
const td = document.createElement("td");
|
|
7713
|
-
td.textContent = cell.formattedValue;
|
|
7714
|
-
applyCellStyle(td, cell);
|
|
7715
|
-
return td;
|
|
7716
|
-
}
|
|
7717
|
-
function renderHeatmapCell(cell) {
|
|
7718
|
-
const td = document.createElement("td");
|
|
7719
|
-
td.className = "oc-table-heatmap";
|
|
7720
|
-
td.textContent = cell.formattedValue;
|
|
7721
|
-
applyCellStyle(td, cell);
|
|
7722
|
-
return td;
|
|
3490
|
+
function renderHeatmapCell(cell) {
|
|
3491
|
+
const td = document.createElement("td");
|
|
3492
|
+
td.className = "oc-table-heatmap";
|
|
3493
|
+
td.textContent = cell.formattedValue;
|
|
3494
|
+
applyCellStyle(td, cell);
|
|
3495
|
+
return td;
|
|
7723
3496
|
}
|
|
7724
3497
|
function renderCategoryCell(cell) {
|
|
7725
3498
|
const td = document.createElement("td");
|
|
@@ -7943,7 +3716,7 @@ import { compileSankey } from "@opendata-ai/openchart-engine";
|
|
|
7943
3716
|
import {
|
|
7944
3717
|
BRAND_FONT_SIZE as BRAND_FONT_SIZE2,
|
|
7945
3718
|
BRAND_MIN_WIDTH as BRAND_MIN_WIDTH2,
|
|
7946
|
-
estimateTextWidth
|
|
3719
|
+
estimateTextWidth,
|
|
7947
3720
|
textAscent as textAscent2,
|
|
7948
3721
|
wrapText
|
|
7949
3722
|
} from "@opendata-ai/openchart-core";
|
|
@@ -7951,10 +3724,6 @@ import { clampStaggerDelay } from "@opendata-ai/openchart-engine";
|
|
|
7951
3724
|
var SVG_NS2 = "http://www.w3.org/2000/svg";
|
|
7952
3725
|
var XLINK_NS2 = "http://www.w3.org/1999/xlink";
|
|
7953
3726
|
var BRAND_URL2 = "https://tryopendata.ai";
|
|
7954
|
-
var EASE_VAR_MAP = {
|
|
7955
|
-
smooth: "var(--oc-ease-smooth)",
|
|
7956
|
-
snappy: "var(--oc-ease-snappy)"
|
|
7957
|
-
};
|
|
7958
3727
|
function createSVGElement2(tag) {
|
|
7959
3728
|
return document.createElementNS(SVG_NS2, tag);
|
|
7960
3729
|
}
|
|
@@ -8108,7 +3877,7 @@ function renderLegend(parent, legendLayout) {
|
|
|
8108
3877
|
for (let i = 0; i < legend.entries.length; i++) {
|
|
8109
3878
|
const entry = legend.entries[i];
|
|
8110
3879
|
if (isHorizontal && i > 0) {
|
|
8111
|
-
const labelWidth =
|
|
3880
|
+
const labelWidth = estimateTextWidth(
|
|
8112
3881
|
entry.label,
|
|
8113
3882
|
legend.labelStyle.fontSize,
|
|
8114
3883
|
legend.labelStyle.fontWeight
|
|
@@ -8159,7 +3928,7 @@ function renderLegend(parent, legendLayout) {
|
|
|
8159
3928
|
entryG.appendChild(label);
|
|
8160
3929
|
g.appendChild(entryG);
|
|
8161
3930
|
if (isHorizontal) {
|
|
8162
|
-
const labelWidth =
|
|
3931
|
+
const labelWidth = estimateTextWidth(
|
|
8163
3932
|
entry.label,
|
|
8164
3933
|
legend.labelStyle.fontSize,
|
|
8165
3934
|
legend.labelStyle.fontWeight
|
|
@@ -8320,11 +4089,12 @@ function renderSankeySVG(layout, animation) {
|
|
|
8320
4089
|
if (enterPhase) {
|
|
8321
4090
|
const totalMarks = layout.nodes.length + layout.links.length;
|
|
8322
4091
|
const stagger = clampStaggerDelay(enterPhase.staggerDelay, totalMarks);
|
|
8323
|
-
svg
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
4092
|
+
stampAnimationVars(svg, {
|
|
4093
|
+
duration: enterPhase.duration,
|
|
4094
|
+
stagger,
|
|
4095
|
+
annotationDelay: animation.annotationDelay,
|
|
4096
|
+
ease: enterPhase.ease
|
|
4097
|
+
});
|
|
8328
4098
|
}
|
|
8329
4099
|
const bg = createSVGElement2("rect");
|
|
8330
4100
|
bg.setAttribute("class", "oc-background");
|
|
@@ -8352,7 +4122,7 @@ function renderSankeySVG(layout, animation) {
|
|
|
8352
4122
|
}
|
|
8353
4123
|
|
|
8354
4124
|
// src/sankey-mount.ts
|
|
8355
|
-
function
|
|
4125
|
+
function resolveDarkMode3(mode) {
|
|
8356
4126
|
if (mode === "force") return true;
|
|
8357
4127
|
if (mode === "off" || mode === void 0) return false;
|
|
8358
4128
|
if (typeof window !== "undefined" && window.matchMedia) {
|
|
@@ -8391,7 +4161,7 @@ function createSankey(container, spec, options) {
|
|
|
8391
4161
|
}
|
|
8392
4162
|
function compile() {
|
|
8393
4163
|
const { width, height } = getContainerDimensions();
|
|
8394
|
-
const darkMode =
|
|
4164
|
+
const darkMode = resolveDarkMode3(options?.darkMode);
|
|
8395
4165
|
const compileOpts = {
|
|
8396
4166
|
width,
|
|
8397
4167
|
height,
|
|
@@ -8566,7 +4336,7 @@ function createSankey(container, spec, options) {
|
|
|
8566
4336
|
const animation = shouldAnimate ? currentLayout.animation : void 0;
|
|
8567
4337
|
svgElement = renderSankeySVG(currentLayout, animation);
|
|
8568
4338
|
container.appendChild(svgElement);
|
|
8569
|
-
const isDark =
|
|
4339
|
+
const isDark = resolveDarkMode3(options?.darkMode);
|
|
8570
4340
|
if (isDark) {
|
|
8571
4341
|
container.classList.add("oc-dark");
|
|
8572
4342
|
} else {
|
|
@@ -8667,7 +4437,7 @@ function createSankey(container, spec, options) {
|
|
|
8667
4437
|
const animation = shouldAnimate ? currentLayout.animation : void 0;
|
|
8668
4438
|
svgElement = renderSankeySVG(currentLayout, animation);
|
|
8669
4439
|
container.appendChild(svgElement);
|
|
8670
|
-
const isDark =
|
|
4440
|
+
const isDark = resolveDarkMode3(options?.darkMode);
|
|
8671
4441
|
if (isDark) {
|
|
8672
4442
|
container.classList.add("oc-dark");
|
|
8673
4443
|
} else {
|
|
@@ -8692,7 +4462,7 @@ function createSankey(container, spec, options) {
|
|
|
8692
4462
|
console.error("[viz] Sankey mount failed:", err);
|
|
8693
4463
|
throw err;
|
|
8694
4464
|
}
|
|
8695
|
-
const
|
|
4465
|
+
const fontsPending = scheduleFontReload(
|
|
8696
4466
|
fontFamily,
|
|
8697
4467
|
() => !destroyed,
|
|
8698
4468
|
() => {
|
|
@@ -8700,7 +4470,7 @@ function createSankey(container, spec, options) {
|
|
|
8700
4470
|
resize();
|
|
8701
4471
|
}
|
|
8702
4472
|
);
|
|
8703
|
-
container.dataset.ocFontsState =
|
|
4473
|
+
container.dataset.ocFontsState = fontsPending ? "pending" : "ready";
|
|
8704
4474
|
if (options?.responsive !== false) {
|
|
8705
4475
|
disconnectResize = observeResize(container, () => {
|
|
8706
4476
|
resize();
|
|
@@ -8911,10 +4681,21 @@ import { compileTable } from "@opendata-ai/openchart-engine";
|
|
|
8911
4681
|
// src/table-renderer.ts
|
|
8912
4682
|
import { BRAND_FONT_SIZE as BRAND_FONT_SIZE3 } from "@opendata-ai/openchart-core";
|
|
8913
4683
|
import { clampStaggerDelay as clampStaggerDelay2 } from "@opendata-ai/openchart-engine";
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
4684
|
+
|
|
4685
|
+
// src/dom-helpers.ts
|
|
4686
|
+
function applySrOnlyStyles(el) {
|
|
4687
|
+
el.style.position = "absolute";
|
|
4688
|
+
el.style.width = "1px";
|
|
4689
|
+
el.style.height = "1px";
|
|
4690
|
+
el.style.padding = "0";
|
|
4691
|
+
el.style.margin = "-1px";
|
|
4692
|
+
el.style.overflow = "hidden";
|
|
4693
|
+
el.style.clipPath = "inset(50%)";
|
|
4694
|
+
el.style.whiteSpace = "nowrap";
|
|
4695
|
+
el.style.borderWidth = "0";
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
// src/table-renderer.ts
|
|
8918
4699
|
var BRAND_URL3 = "https://tryopendata.ai";
|
|
8919
4700
|
function renderChromeBlock(layout, position) {
|
|
8920
4701
|
const chrome = layout.chrome;
|
|
@@ -9116,15 +4897,7 @@ function renderTable(layout, container, opts) {
|
|
|
9116
4897
|
}
|
|
9117
4898
|
const caption = document.createElement("caption");
|
|
9118
4899
|
caption.className = "oc-sr-only";
|
|
9119
|
-
caption
|
|
9120
|
-
caption.style.width = "1px";
|
|
9121
|
-
caption.style.height = "1px";
|
|
9122
|
-
caption.style.padding = "0";
|
|
9123
|
-
caption.style.margin = "-1px";
|
|
9124
|
-
caption.style.overflow = "hidden";
|
|
9125
|
-
caption.style.clipPath = "inset(50%)";
|
|
9126
|
-
caption.style.whiteSpace = "nowrap";
|
|
9127
|
-
caption.style.borderWidth = "0";
|
|
4900
|
+
applySrOnlyStyles(caption);
|
|
9128
4901
|
caption.textContent = layout.a11y.summary;
|
|
9129
4902
|
table.appendChild(caption);
|
|
9130
4903
|
table.appendChild(renderThead(layout.columns, layout.sort));
|
|
@@ -9144,15 +4917,7 @@ function renderTable(layout, container, opts) {
|
|
|
9144
4917
|
}
|
|
9145
4918
|
const liveRegion = document.createElement("div");
|
|
9146
4919
|
liveRegion.className = "oc-table-live-region oc-sr-only";
|
|
9147
|
-
liveRegion
|
|
9148
|
-
liveRegion.style.width = "1px";
|
|
9149
|
-
liveRegion.style.height = "1px";
|
|
9150
|
-
liveRegion.style.padding = "0";
|
|
9151
|
-
liveRegion.style.margin = "-1px";
|
|
9152
|
-
liveRegion.style.overflow = "hidden";
|
|
9153
|
-
liveRegion.style.clipPath = "inset(50%)";
|
|
9154
|
-
liveRegion.style.whiteSpace = "nowrap";
|
|
9155
|
-
liveRegion.style.borderWidth = "0";
|
|
4920
|
+
applySrOnlyStyles(liveRegion);
|
|
9156
4921
|
liveRegion.setAttribute("aria-live", "polite");
|
|
9157
4922
|
liveRegion.setAttribute("aria-atomic", "true");
|
|
9158
4923
|
liveRegion.setAttribute("role", "status");
|
|
@@ -9177,10 +4942,11 @@ function renderTable(layout, container, opts) {
|
|
|
9177
4942
|
const enterPhase = layout.animation.enter;
|
|
9178
4943
|
const rowCount = layout.rows.length;
|
|
9179
4944
|
const stagger = clampStaggerDelay2(enterPhase.staggerDelay, rowCount);
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
4945
|
+
stampAnimationVars(wrapper, {
|
|
4946
|
+
duration: enterPhase.duration,
|
|
4947
|
+
stagger,
|
|
4948
|
+
ease: enterPhase.ease
|
|
4949
|
+
});
|
|
9184
4950
|
wrapper.classList.add("oc-animate");
|
|
9185
4951
|
}
|
|
9186
4952
|
container.appendChild(wrapper);
|
|
@@ -9188,7 +4954,7 @@ function renderTable(layout, container, opts) {
|
|
|
9188
4954
|
}
|
|
9189
4955
|
|
|
9190
4956
|
// src/table-mount.ts
|
|
9191
|
-
function
|
|
4957
|
+
function resolveDarkMode4(mode) {
|
|
9192
4958
|
if (mode === "force") return true;
|
|
9193
4959
|
if (mode === "off" || mode === void 0) return false;
|
|
9194
4960
|
if (typeof window !== "undefined" && window.matchMedia) {
|
|
@@ -9196,7 +4962,7 @@ function resolveDarkMode5(mode) {
|
|
|
9196
4962
|
}
|
|
9197
4963
|
return false;
|
|
9198
4964
|
}
|
|
9199
|
-
function
|
|
4965
|
+
function csvEscape(value) {
|
|
9200
4966
|
if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
|
|
9201
4967
|
return `"${value.replace(/"/g, '""')}"`;
|
|
9202
4968
|
}
|
|
@@ -9256,7 +5022,7 @@ function createTable(container, spec, options) {
|
|
|
9256
5022
|
}
|
|
9257
5023
|
function compile() {
|
|
9258
5024
|
const state = getState();
|
|
9259
|
-
const darkMode =
|
|
5025
|
+
const darkMode = resolveDarkMode4(options?.darkMode);
|
|
9260
5026
|
const { width } = getContainerDimensions();
|
|
9261
5027
|
const compileOpts = {
|
|
9262
5028
|
width,
|
|
@@ -9318,7 +5084,7 @@ function createTable(container, spec, options) {
|
|
|
9318
5084
|
if (isFirstRender) {
|
|
9319
5085
|
isFirstRender = false;
|
|
9320
5086
|
}
|
|
9321
|
-
const isDark =
|
|
5087
|
+
const isDark = resolveDarkMode4(options?.darkMode);
|
|
9322
5088
|
if (isDark) {
|
|
9323
5089
|
container.classList.add("oc-dark");
|
|
9324
5090
|
} else {
|
|
@@ -9473,7 +5239,7 @@ function createTable(container, spec, options) {
|
|
|
9473
5239
|
throw new Error(`Unsupported export format: ${format}`);
|
|
9474
5240
|
}
|
|
9475
5241
|
const state = getState();
|
|
9476
|
-
const darkMode =
|
|
5242
|
+
const darkMode = resolveDarkMode4(options?.darkMode);
|
|
9477
5243
|
const { width } = getContainerDimensions();
|
|
9478
5244
|
const fullLayout = compileTable(currentSpec, {
|
|
9479
5245
|
width,
|
|
@@ -9485,18 +5251,24 @@ function createTable(container, spec, options) {
|
|
|
9485
5251
|
// No page/pageSize: get all rows
|
|
9486
5252
|
});
|
|
9487
5253
|
const headers = fullLayout.columns.map((c2) => c2.label);
|
|
9488
|
-
const csvRows = [headers.map(
|
|
5254
|
+
const csvRows = [headers.map(csvEscape).join(",")];
|
|
9489
5255
|
for (const row of fullLayout.rows) {
|
|
9490
|
-
const values = row.cells.map((cell) =>
|
|
5256
|
+
const values = row.cells.map((cell) => csvEscape(cell.formattedValue));
|
|
9491
5257
|
csvRows.push(values.join(","));
|
|
9492
5258
|
}
|
|
9493
5259
|
return csvRows.join("\n");
|
|
9494
5260
|
}
|
|
9495
5261
|
function setState(partial) {
|
|
9496
5262
|
if (destroyed) return;
|
|
9497
|
-
if (
|
|
9498
|
-
|
|
9499
|
-
|
|
5263
|
+
if (isControlled && options?.externalState) {
|
|
5264
|
+
if (partial.sort !== void 0) options.externalState.sort = partial.sort;
|
|
5265
|
+
if (partial.search !== void 0) options.externalState.search = partial.search;
|
|
5266
|
+
if (partial.page !== void 0) options.externalState.page = partial.page;
|
|
5267
|
+
} else {
|
|
5268
|
+
if (partial.sort !== void 0) internalState.sort = partial.sort;
|
|
5269
|
+
if (partial.search !== void 0) internalState.search = partial.search;
|
|
5270
|
+
if (partial.page !== void 0) internalState.page = partial.page;
|
|
5271
|
+
}
|
|
9500
5272
|
render();
|
|
9501
5273
|
}
|
|
9502
5274
|
function destroy() {
|
|
@@ -9561,7 +5333,7 @@ function pickTargetTileSize(width) {
|
|
|
9561
5333
|
if (width < BREAKPOINT_COMPACT_MAX) return 34;
|
|
9562
5334
|
return 40;
|
|
9563
5335
|
}
|
|
9564
|
-
function
|
|
5336
|
+
function resolveDarkMode5(mode) {
|
|
9565
5337
|
if (mode === "force") return true;
|
|
9566
5338
|
if (mode === "off" || mode === void 0) return false;
|
|
9567
5339
|
if (typeof window !== "undefined" && window.matchMedia) {
|
|
@@ -9579,6 +5351,7 @@ function createTileMap(container, spec, options) {
|
|
|
9579
5351
|
let disconnectResize = null;
|
|
9580
5352
|
let animationCleanup = null;
|
|
9581
5353
|
let pendingResize = false;
|
|
5354
|
+
let isAutoHeight = null;
|
|
9582
5355
|
let fontsReloadPending = false;
|
|
9583
5356
|
container.classList.add("oc-tilemap-root");
|
|
9584
5357
|
function resolveEffectiveFont() {
|
|
@@ -9590,6 +5363,12 @@ function createTileMap(container, spec, options) {
|
|
|
9590
5363
|
function getContainerDimensions() {
|
|
9591
5364
|
const rect = container.getBoundingClientRect();
|
|
9592
5365
|
const width = Math.max(rect.width || 600, 100);
|
|
5366
|
+
if (isAutoHeight === null && (rect.width > 0 || rect.height > 0)) {
|
|
5367
|
+
isAutoHeight = rect.height === 0 && rect.width > 0;
|
|
5368
|
+
}
|
|
5369
|
+
if (isAutoHeight === false && rect.height > 0) {
|
|
5370
|
+
return { width, height: Math.max(rect.height, 100) };
|
|
5371
|
+
}
|
|
9593
5372
|
if (width >= BREAKPOINT_MEDIUM_MAX) {
|
|
9594
5373
|
return { width, height: width };
|
|
9595
5374
|
}
|
|
@@ -9599,7 +5378,7 @@ function createTileMap(container, spec, options) {
|
|
|
9599
5378
|
}
|
|
9600
5379
|
function compile() {
|
|
9601
5380
|
const { width, height } = getContainerDimensions();
|
|
9602
|
-
const darkMode =
|
|
5381
|
+
const darkMode = resolveDarkMode5(options?.darkMode);
|
|
9603
5382
|
const compileOpts = {
|
|
9604
5383
|
width,
|
|
9605
5384
|
height,
|
|
@@ -9771,12 +5550,12 @@ function createTileMap(container, spec, options) {
|
|
|
9771
5550
|
}
|
|
9772
5551
|
container.classList.remove("oc-tilemap-root", "oc-dark");
|
|
9773
5552
|
}
|
|
9774
|
-
if (
|
|
5553
|
+
if (resolveDarkMode5(options?.darkMode)) {
|
|
9775
5554
|
container.classList.add("oc-dark");
|
|
9776
5555
|
}
|
|
9777
5556
|
currentLayout = compile();
|
|
9778
5557
|
render(true);
|
|
9779
|
-
const
|
|
5558
|
+
const fontsPending = scheduleFontReload(
|
|
9780
5559
|
fontFamily,
|
|
9781
5560
|
() => !destroyed,
|
|
9782
5561
|
() => {
|
|
@@ -9784,7 +5563,7 @@ function createTileMap(container, spec, options) {
|
|
|
9784
5563
|
resize();
|
|
9785
5564
|
}
|
|
9786
5565
|
);
|
|
9787
|
-
container.dataset.ocFontsState =
|
|
5566
|
+
container.dataset.ocFontsState = fontsPending ? "pending" : "ready";
|
|
9788
5567
|
if (options?.responsive !== false) {
|
|
9789
5568
|
disconnectResize = observeResize(container, () => {
|
|
9790
5569
|
resize();
|
|
@@ -9805,12 +5584,14 @@ export {
|
|
|
9805
5584
|
createBarList,
|
|
9806
5585
|
createChart,
|
|
9807
5586
|
createGraph,
|
|
5587
|
+
createMap,
|
|
9808
5588
|
createSankey,
|
|
9809
5589
|
createSimulationWorker,
|
|
9810
5590
|
createTable,
|
|
9811
5591
|
createTextEditOverlay,
|
|
9812
5592
|
createTileMap,
|
|
9813
5593
|
createTooltipManager,
|
|
5594
|
+
createYouDrawIt,
|
|
9814
5595
|
exportCSV,
|
|
9815
5596
|
exportJPG,
|
|
9816
5597
|
exportPNG,
|