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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/{chunk-EQ6DYFO7.js → chunk-5EOV5JQZ.js} +431 -538
  2. package/dist/chunk-5EOV5JQZ.js.map +1 -0
  3. package/dist/chunk-ENXSUXC5.js +6513 -0
  4. package/dist/chunk-ENXSUXC5.js.map +1 -0
  5. package/dist/chunk-W34KN7FD.js +298 -0
  6. package/dist/chunk-W34KN7FD.js.map +1 -0
  7. package/dist/index.d.ts +110 -116
  8. package/dist/index.js +189 -4408
  9. package/dist/index.js.map +1 -1
  10. package/dist/mount-CUKQLeqD.d.ts +207 -0
  11. package/dist/static.js +16 -11
  12. package/dist/static.js.map +1 -1
  13. package/dist/story/index.d.ts +277 -0
  14. package/dist/story/index.js +537 -0
  15. package/dist/story/index.js.map +1 -0
  16. package/dist/styles.css +2 -1
  17. package/dist/styles.css.map +1 -0
  18. package/package.json +9 -3
  19. package/src/__test-fixtures__/specs.ts +1 -1
  20. package/src/__tests__/annotations-render.test.ts +201 -0
  21. package/src/__tests__/continuous-legend.test.ts +91 -0
  22. package/src/__tests__/edit-events.test.ts +155 -0
  23. package/src/__tests__/map.test.ts +293 -0
  24. package/src/__tests__/mount.test.ts +26 -0
  25. package/src/__tests__/pattern-fills.test.ts +105 -0
  26. package/src/__tests__/series-search.test.ts +311 -0
  27. package/src/__tests__/table-mount.test.ts +34 -0
  28. package/src/__tests__/tooltip.test.ts +26 -0
  29. package/src/__tests__/transition.test.ts +196 -11
  30. package/src/__tests__/you-draw-it.test.ts +273 -0
  31. package/src/animation-vars.ts +42 -0
  32. package/src/barlist-renderer.ts +2 -2
  33. package/src/dom-helpers.ts +17 -0
  34. package/src/export.ts +33 -16
  35. package/src/index.ts +7 -0
  36. package/src/interactions/editing-drags.ts +9 -6
  37. package/src/interactions/legend-interaction.ts +9 -1
  38. package/src/interactions/selection.ts +12 -3
  39. package/src/map-camera.ts +88 -0
  40. package/src/map-mount.ts +701 -0
  41. package/src/map-renderer.ts +338 -0
  42. package/src/map-transition.ts +83 -0
  43. package/src/mount.ts +212 -26
  44. package/src/pattern-utils.ts +110 -0
  45. package/src/renderers/annotations.ts +162 -117
  46. package/src/renderers/axes.ts +13 -6
  47. package/src/renderers/brand.ts +7 -2
  48. package/src/renderers/chrome.ts +32 -10
  49. package/src/renderers/legend.ts +171 -2
  50. package/src/renderers/marks.ts +33 -5
  51. package/src/sankey-renderer.ts +7 -11
  52. package/src/series-search.ts +392 -0
  53. package/src/static.ts +13 -8
  54. package/src/story/__tests__/camera-math.test.ts +145 -0
  55. package/src/story/__tests__/create-chart-story.test.ts +317 -0
  56. package/src/story/__tests__/progress-math.test.ts +95 -0
  57. package/src/story/__tests__/scroll-driver.test.ts +130 -0
  58. package/src/story/__tests__/tween.test.ts +159 -0
  59. package/src/story/camera-math.ts +125 -0
  60. package/src/story/create-chart-story.ts +353 -0
  61. package/src/story/crossfade.ts +70 -0
  62. package/src/story/index.ts +39 -0
  63. package/src/story/progress-math.ts +85 -0
  64. package/src/story/resolve-camera-target.ts +119 -0
  65. package/src/story/scroll-driver.ts +206 -0
  66. package/src/story/story-camera.ts +49 -0
  67. package/src/story/tween.ts +112 -0
  68. package/src/story/types.ts +76 -0
  69. package/src/svg-renderer.ts +60 -29
  70. package/src/table-mount.ts +12 -3
  71. package/src/table-renderer.ts +9 -28
  72. package/src/text-edit-overlay.ts +1 -1
  73. package/src/theme-tokens.ts +4 -4
  74. package/src/tilemap-mount.ts +27 -6
  75. package/src/tilemap-renderer.ts +30 -88
  76. package/src/tooltip.ts +11 -0
  77. package/src/transition.ts +376 -33
  78. package/src/you-draw-it.ts +480 -0
  79. package/dist/chunk-EQ6DYFO7.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,55 +1,35 @@
1
1
  import {
2
- buildGradientDefs,
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
- renderSingleMark,
7
- renderTileMapSVG,
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.style.setProperty("--oc-animation-duration", `${animation.enter.duration}ms`);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
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 fontsPending2 = scheduleFontReload(
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 = fontsPending2 ? "pending" : "ready";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3840
3390
  }
3841
3391
 
3842
- // src/mount.ts
3843
- import { isGraphSpec, isLayerSpec } from "@opendata-ai/openchart-core";
3844
- import { compileChart, compileLayer, legendGap } from "@opendata-ai/openchart-engine";
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
- return map;
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 (handlers.onAnnotationClick) {
3932
- const annotationElements = svg.querySelectorAll(".oc-annotation");
3933
- for (let i = 0; i < annotationElements.length; i++) {
3934
- const el = annotationElements[i];
3935
- const specAnnotation = specAnnotations[i];
3936
- if (!specAnnotation) continue;
3937
- const handleClick = (e) => {
3938
- const mouseEvent = e;
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
- // src/interactions/crosshair.ts
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
- function collectSeriesGroups(layout) {
3958
- const byKey = /* @__PURE__ */ new Map();
3959
- const markTypeByKey = /* @__PURE__ */ new Map();
3960
- for (let i = 0; i < layout.marks.length; i++) {
3961
- const mark = layout.marks[i];
3962
- if ((mark.type === "line" || mark.type === "area") && mark.dataPoints?.length) {
3963
- const key = mark.seriesKey ?? "__default__";
3964
- const existingType = markTypeByKey.get(key);
3965
- if (existingType === "line" && mark.type === "area") continue;
3966
- const color = mark.type === "line" ? mark.stroke : getRepresentativeColor(mark.fill);
3967
- const pointsByX = /* @__PURE__ */ new Map();
3968
- for (const dp of mark.dataPoints) {
3969
- pointsByX.set(snapKey(dp.x), { ...dp });
3970
- }
3971
- byKey.set(key, { seriesKey: key, color, pointsByX });
3972
- markTypeByKey.set(key, mark.type);
3973
- }
3974
- }
3975
- return Array.from(byKey.values());
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 collectSnapXs(groups) {
3978
- const seen = /* @__PURE__ */ new Set();
3979
- for (const g of groups) {
3980
- for (const k of g.pointsByX.keys()) seen.add(k);
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
- return Array.from(seen).sort((a2, b) => a2 - b);
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 findNearestX(sortedXs, x3) {
3985
- if (sortedXs.length === 0) return null;
3986
- let lo = 0;
3987
- let hi = sortedXs.length - 1;
3988
- while (lo < hi) {
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 buildSliceTooltip(hits) {
4001
- if (hits.length === 0) return null;
4002
- const title = hits[0].point.tooltip?.title;
4003
- const fields = [];
4004
- const isMulti = hits.length > 1;
4005
- for (const { group, point } of hits) {
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 as estimateTextWidth2,
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 = estimateTextWidth2(
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 = estimateTextWidth2(
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.style.setProperty("--oc-animation-duration", `${enterPhase.duration}ms`);
8324
- svg.style.setProperty("--oc-animation-stagger", `${stagger}ms`);
8325
- svg.style.setProperty("--oc-annotation-delay", `${animation.annotationDelay}ms`);
8326
- const easeVar = EASE_VAR_MAP[enterPhase.ease] || EASE_VAR_MAP.smooth;
8327
- svg.style.setProperty("--oc-animation-ease", easeVar);
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 resolveDarkMode4(mode) {
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 = resolveDarkMode4(options?.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 = resolveDarkMode4(options?.darkMode);
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 = resolveDarkMode4(options?.darkMode);
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 fontsPending2 = scheduleFontReload(
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 = fontsPending2 ? "pending" : "ready";
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
- var EASE_VAR_MAP2 = {
8915
- smooth: "var(--oc-ease-smooth)",
8916
- snappy: "var(--oc-ease-snappy)"
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.style.position = "absolute";
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.style.position = "absolute";
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
- const s = wrapper.style;
9181
- s.setProperty("--oc-animation-duration", `${enterPhase.duration}ms`);
9182
- s.setProperty("--oc-animation-stagger", `${stagger}ms`);
9183
- s.setProperty("--oc-animation-ease", EASE_VAR_MAP2[enterPhase.ease] || EASE_VAR_MAP2.smooth);
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 resolveDarkMode5(mode) {
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 csvEscape2(value) {
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 = resolveDarkMode5(options?.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 = resolveDarkMode5(options?.darkMode);
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 = resolveDarkMode5(options?.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(csvEscape2).join(",")];
5254
+ const csvRows = [headers.map(csvEscape).join(",")];
9489
5255
  for (const row of fullLayout.rows) {
9490
- const values = row.cells.map((cell) => csvEscape2(cell.formattedValue));
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 (partial.sort !== void 0) internalState.sort = partial.sort;
9498
- if (partial.search !== void 0) internalState.search = partial.search;
9499
- if (partial.page !== void 0) internalState.page = partial.page;
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 resolveDarkMode6(mode) {
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 = resolveDarkMode6(options?.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 (resolveDarkMode6(options?.darkMode)) {
5553
+ if (resolveDarkMode5(options?.darkMode)) {
9775
5554
  container.classList.add("oc-dark");
9776
5555
  }
9777
5556
  currentLayout = compile();
9778
5557
  render(true);
9779
- const fontsPending2 = scheduleFontReload(
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 = fontsPending2 ? "pending" : "ready";
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,