@opendata-ai/openchart-vanilla 8.8.0 → 8.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import {
4
4
  exportPNG,
5
5
  exportSVG,
6
6
  exportSVGWithFonts
7
- } from "./chunk-I73B3DJI.js";
7
+ } from "./chunk-R5TZHNTG.js";
8
8
  import {
9
9
  buildGradientDefs,
10
10
  buildPatternDefs,
@@ -17,7 +17,7 @@ import {
17
17
  resolveMarkFill,
18
18
  resolvePatternFill,
19
19
  stampAnimationVars
20
- } from "./chunk-SXTHPXG6.js";
20
+ } from "./chunk-TP3GCWCO.js";
21
21
  import {
22
22
  SVG_NS,
23
23
  createSVGElement,
@@ -453,6 +453,140 @@ function materializeCanvasModeSVG(layout, opts) {
453
453
  return materializeRasterMarkSVG(layout);
454
454
  }
455
455
 
456
+ // src/transition/keys.ts
457
+ function annotationKey(a) {
458
+ if (a.id) return `id:${a.id}`;
459
+ return `${a.type}:${a.label?.text ?? ""}`;
460
+ }
461
+ function keyAnnotations(annotations) {
462
+ const seen = /* @__PURE__ */ new Map();
463
+ return annotations.map((a) => {
464
+ const base = annotationKey(a);
465
+ const n = seen.get(base) ?? 0;
466
+ seen.set(base, n + 1);
467
+ return n === 0 ? base : `${base}#${n}`;
468
+ });
469
+ }
470
+
471
+ // src/transition/gate.ts
472
+ var TRANSITIONABLE_MARKS = /* @__PURE__ */ new Set([
473
+ "bar",
474
+ "line",
475
+ "area",
476
+ "point",
477
+ "beeswarm",
478
+ "arc",
479
+ "histogram",
480
+ "density"
481
+ ]);
482
+ var DEFAULT_UPDATE_MAX_MARKS = 500;
483
+ var CANVAS_DEFAULT_UPDATE_MAX_MARKS = 2e4;
484
+ function canTransitionSpecShape(prevSpec, nextSpec) {
485
+ const prev = prevSpec;
486
+ const next = nextSpec;
487
+ if (!prev || !next || !("mark" in prev) || !("mark" in next)) return false;
488
+ const prevMark = typeof prev.mark === "string" ? prev.mark : prev.mark?.type;
489
+ const nextMark = typeof next.mark === "string" ? next.mark : next.mark?.type;
490
+ if (prevMark !== nextMark) return false;
491
+ if (!TRANSITIONABLE_MARKS.has(prevMark)) return false;
492
+ const prevEnc = prev.encoding;
493
+ const nextEnc = next.encoding;
494
+ if (!prevEnc || !nextEnc) return false;
495
+ if (JSON.stringify(prev.transform) !== JSON.stringify(next.transform)) return false;
496
+ for (const channel of ["x", "y", "theta", "radius"]) {
497
+ for (const property of ["aggregate", "bin", "stack", "timeUnit"]) {
498
+ if (JSON.stringify(prevEnc[channel]?.[property]) !== JSON.stringify(nextEnc[channel]?.[property]))
499
+ return false;
500
+ }
501
+ const field = prevEnc[channel]?.field;
502
+ if (field) {
503
+ const a = prev.fields?.[field];
504
+ const b = next.fields?.[field];
505
+ if (["unit", "measure", "representation"].some((key) => a?.[key] !== b?.[key])) return false;
506
+ }
507
+ }
508
+ return !(prevEnc.x?.field !== nextEnc.x?.field || prevEnc.x?.type !== nextEnc.x?.type || prevEnc.x?.scale?.type !== nextEnc.x?.scale?.type || prevEnc.y?.field !== nextEnc.y?.field || prevEnc.y?.type !== nextEnc.y?.type || prevEnc.y?.scale?.type !== nextEnc.y?.scale?.type || prevEnc.theta?.field !== nextEnc.theta?.field || prevEnc.theta?.type !== nextEnc.theta?.type || prevEnc.color?.field !== nextEnc.color?.field || prevEnc.key?.field !== nextEnc.key?.field || prevEnc.facet?.field !== nextEnc.facet?.field || prevEnc.row?.field !== nextEnc.row?.field || prevEnc.column?.field !== nextEnc.column?.field);
509
+ }
510
+ function canTransition(args) {
511
+ const { prevLayout, nextLayout, prevSpec, nextSpec, isFirstRender, entranceInFlight } = args;
512
+ if (!prevLayout || !prevSpec || isFirstRender) return false;
513
+ if (!nextLayout.animation?.update) return false;
514
+ if (!canTransitionSpecShape(prevSpec, nextSpec)) return false;
515
+ if (entranceInFlight) return false;
516
+ const cap = nextLayout.animation.update.maxMarks ?? (nextLayout.markRenderMode === "canvas" ? CANVAS_DEFAULT_UPDATE_MAX_MARKS : DEFAULT_UPDATE_MAX_MARKS);
517
+ if (Math.max(prevLayout.marks.length, nextLayout.marks.length) > cap) return false;
518
+ if (!hasVisibleChange(prevLayout, nextLayout)) return false;
519
+ if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
520
+ try {
521
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
522
+ } catch {
523
+ }
524
+ }
525
+ return true;
526
+ }
527
+ function hasVisibleChange(prevLayout, nextLayout) {
528
+ return hasGeometryChanged(prevLayout, nextLayout) || hasColorChanged(prevLayout, nextLayout) || hasAnnotationChanged(prevLayout, nextLayout);
529
+ }
530
+ function hasColorChanged(prevLayout, nextLayout) {
531
+ const prevMarks = prevLayout.marks;
532
+ const nextMarks = nextLayout.marks;
533
+ if (prevMarks.length !== nextMarks.length) return true;
534
+ for (let i = 0; i < prevMarks.length; i++) {
535
+ const p = prevMarks[i];
536
+ const n = nextMarks[i];
537
+ if (p.fill !== n.fill || p.stroke !== n.stroke) return true;
538
+ }
539
+ return false;
540
+ }
541
+ function hasAnnotationChanged(prevLayout, nextLayout) {
542
+ const prev = prevLayout.annotations ?? [];
543
+ const next = nextLayout.annotations ?? [];
544
+ if (prev.length !== next.length) return true;
545
+ const prevKeys = keyAnnotations(prev);
546
+ const nextKeys = keyAnnotations(next);
547
+ for (let i = 0; i < prev.length; i++) {
548
+ if (prevKeys[i] !== nextKeys[i]) return true;
549
+ const p = prev[i];
550
+ const n = next[i];
551
+ if (p.label?.x !== n.label?.x || p.label?.y !== n.label?.y) return true;
552
+ if (p.opacity !== n.opacity) return true;
553
+ }
554
+ return false;
555
+ }
556
+ function hasGeometryChanged(prevLayout, nextLayout) {
557
+ const prevMarks = prevLayout.marks;
558
+ const nextMarks = nextLayout.marks;
559
+ if (prevMarks.length !== nextMarks.length) return true;
560
+ for (let i = 0; i < prevMarks.length; i++) {
561
+ const p = prevMarks[i];
562
+ const n = nextMarks[i];
563
+ if (p.type !== n.type || p.key !== n.key) return true;
564
+ if (p.type === "rect" && n.type === "rect") {
565
+ if (p.x !== n.x || p.y !== n.y || p.width !== n.width || p.height !== n.height) return true;
566
+ } else if (p.type === "line" && n.type === "line") {
567
+ if (p.path !== n.path || p.points.length !== n.points.length) return true;
568
+ for (let j = 0; j < p.points.length; j++) {
569
+ if (p.points[j].x !== n.points[j].x || p.points[j].y !== n.points[j].y) return true;
570
+ }
571
+ } else if (p.type === "area" && n.type === "area") {
572
+ if (p.path !== n.path) return true;
573
+ } else if (p.type === "arc" && n.type === "arc") {
574
+ if (p.startAngle !== n.startAngle || p.endAngle !== n.endAngle || p.innerRadius !== n.innerRadius || p.outerRadius !== n.outerRadius || p.center.x !== n.center.x || p.center.y !== n.center.y) {
575
+ return true;
576
+ }
577
+ } else if (p.type === "point" && n.type === "point") {
578
+ if (p.cx !== n.cx || p.cy !== n.cy || p.r !== n.r) return true;
579
+ } else if (p.type === "rule" && n.type === "rule") {
580
+ if (p.x1 !== n.x1 || p.y1 !== n.y1 || p.x2 !== n.x2 || p.y2 !== n.y2) return true;
581
+ } else if (p.type === "tick" && n.type === "tick") {
582
+ if (p.x !== n.x || p.y !== n.y) return true;
583
+ } else if (p.type === "textMark" && n.type === "textMark") {
584
+ if (p.x !== n.x || p.y !== n.y) return true;
585
+ }
586
+ }
587
+ return false;
588
+ }
589
+
456
590
  // src/text-edit-overlay.ts
457
591
  function getScale(svg) {
458
592
  const viewBox = svg.viewBox?.baseVal;
@@ -705,138 +839,91 @@ function createTextEditOverlayAtPosition(config) {
705
839
  return { destroy };
706
840
  }
707
841
 
708
- // src/transition/keys.ts
709
- function annotationKey(a) {
710
- if (a.id) return `id:${a.id}`;
711
- return `${a.type}:${a.label?.text ?? ""}`;
712
- }
713
- function keyAnnotations(annotations) {
714
- const seen = /* @__PURE__ */ new Map();
715
- return annotations.map((a) => {
716
- const base = annotationKey(a);
717
- const n = seen.get(base) ?? 0;
718
- seen.set(base, n + 1);
719
- return n === 0 ? base : `${base}#${n}`;
720
- });
721
- }
722
-
723
- // src/transition/gate.ts
724
- var TRANSITIONABLE_MARKS = /* @__PURE__ */ new Set([
725
- "bar",
726
- "line",
727
- "area",
728
- "point",
729
- "beeswarm",
730
- "arc",
731
- "histogram",
732
- "density"
733
- ]);
734
- var DEFAULT_UPDATE_MAX_MARKS = 500;
735
- var CANVAS_DEFAULT_UPDATE_MAX_MARKS = 2e4;
736
- function canTransitionSpecShape(prevSpec, nextSpec) {
737
- const prev = prevSpec;
738
- const next = nextSpec;
739
- if (!prev || !next || !("mark" in prev) || !("mark" in next)) return false;
740
- const prevMark = typeof prev.mark === "string" ? prev.mark : prev.mark?.type;
741
- const nextMark = typeof next.mark === "string" ? next.mark : next.mark?.type;
742
- if (prevMark !== nextMark) return false;
743
- if (!TRANSITIONABLE_MARKS.has(prevMark)) return false;
744
- const prevEnc = prev.encoding;
745
- const nextEnc = next.encoding;
746
- if (!prevEnc || !nextEnc) return false;
747
- return !(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.theta?.field !== nextEnc.theta?.field || prevEnc.theta?.type !== nextEnc.theta?.type || prevEnc.color?.field !== nextEnc.color?.field || prevEnc.key?.field !== nextEnc.key?.field || prevEnc.facet?.field !== nextEnc.facet?.field || prevEnc.row?.field !== nextEnc.row?.field || prevEnc.column?.field !== nextEnc.column?.field);
748
- }
749
- function canTransition(args) {
750
- const { prevLayout, nextLayout, prevSpec, nextSpec, isFirstRender, entranceInFlight } = args;
751
- if (!prevLayout || !prevSpec || isFirstRender) return false;
752
- if (!nextLayout.animation?.update) return false;
753
- if (!canTransitionSpecShape(prevSpec, nextSpec)) return false;
754
- if (entranceInFlight) return false;
755
- if (prevLayout.dimensions.width !== nextLayout.dimensions.width || prevLayout.dimensions.height !== nextLayout.dimensions.height) {
756
- return false;
757
- }
758
- const cap = nextLayout.animation.update.maxMarks ?? (nextLayout.markRenderMode === "canvas" ? CANVAS_DEFAULT_UPDATE_MAX_MARKS : DEFAULT_UPDATE_MAX_MARKS);
759
- if (Math.max(prevLayout.marks.length, nextLayout.marks.length) > cap) return false;
760
- if (!hasVisibleChange(prevLayout, nextLayout)) return false;
761
- if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
762
- try {
763
- if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
764
- } catch {
765
- }
766
- }
767
- return true;
768
- }
769
- function hasVisibleChange(prevLayout, nextLayout) {
770
- return hasGeometryChanged(prevLayout, nextLayout) || hasColorChanged(prevLayout, nextLayout) || hasAnnotationChanged(prevLayout, nextLayout);
771
- }
772
- function hasColorChanged(prevLayout, nextLayout) {
773
- const prevMarks = prevLayout.marks;
774
- const nextMarks = nextLayout.marks;
775
- if (prevMarks.length !== nextMarks.length) return true;
776
- for (let i = 0; i < prevMarks.length; i++) {
777
- const p = prevMarks[i];
778
- const n = nextMarks[i];
779
- if (p.fill !== n.fill || p.stroke !== n.stroke) return true;
780
- }
781
- return false;
782
- }
783
- function hasAnnotationChanged(prevLayout, nextLayout) {
784
- const prev = prevLayout.annotations ?? [];
785
- const next = nextLayout.annotations ?? [];
786
- if (prev.length !== next.length) return true;
787
- const prevKeys = keyAnnotations(prev);
788
- const nextKeys = keyAnnotations(next);
789
- for (let i = 0; i < prev.length; i++) {
790
- if (prevKeys[i] !== nextKeys[i]) return true;
791
- const p = prev[i];
792
- const n = next[i];
793
- if (p.label?.x !== n.label?.x || p.label?.y !== n.label?.y) return true;
794
- if (p.opacity !== n.opacity) return true;
795
- }
796
- return false;
797
- }
798
- function hasGeometryChanged(prevLayout, nextLayout) {
799
- const prevMarks = prevLayout.marks;
800
- const nextMarks = nextLayout.marks;
801
- if (prevMarks.length !== nextMarks.length) return true;
802
- for (let i = 0; i < prevMarks.length; i++) {
803
- const p = prevMarks[i];
804
- const n = nextMarks[i];
805
- if (p.type !== n.type || p.key !== n.key) return true;
806
- if (p.type === "rect" && n.type === "rect") {
807
- if (p.x !== n.x || p.y !== n.y || p.width !== n.width || p.height !== n.height) return true;
808
- } else if (p.type === "line" && n.type === "line") {
809
- if (p.path !== n.path || p.points.length !== n.points.length) return true;
810
- for (let j = 0; j < p.points.length; j++) {
811
- if (p.points[j].x !== n.points[j].x || p.points[j].y !== n.points[j].y) return true;
842
+ // src/transition/labels.ts
843
+ function buildLabelMotion(svg, previous, next, snapshot) {
844
+ const attributes = [];
845
+ const add = (element, name, delta, key) => {
846
+ const raw = element.getAttribute(name);
847
+ if (raw === null) return;
848
+ const to = Number(raw);
849
+ if (!Number.isFinite(to)) return;
850
+ const captured = snapshot?.get(key);
851
+ attributes.push({
852
+ element,
853
+ name,
854
+ from: captured?.type === "attribute" ? captured.value : to + delta,
855
+ to,
856
+ key
857
+ });
858
+ };
859
+ for (const group of svg.querySelectorAll("[data-endpoint-key]")) {
860
+ const key = group.getAttribute("data-endpoint-key");
861
+ const leading = group.getAttribute("data-endpoint-side") === "leading";
862
+ const before = (leading ? previous.endpointLabels?.leading : previous.endpointLabels?.entries)?.find((entry) => entry.seriesKey === key);
863
+ const after = (leading ? next.endpointLabels?.leading : next.endpointLabels?.entries)?.find(
864
+ (entry) => entry.seriesKey === key
865
+ );
866
+ if (!before || !after) continue;
867
+ const beforeBounds = leading ? previous.endpointLabels?.leadingBounds : previous.endpointLabels?.bounds;
868
+ const afterBounds = leading ? next.endpointLabels?.leadingBounds : next.endpointLabels?.bounds;
869
+ const dx = (beforeBounds?.x ?? 0) - (afterBounds?.x ?? 0);
870
+ const dy = before.labelY - after.labelY;
871
+ Array.from(group.querySelectorAll("text, tspan, line, circle")).forEach((element, index) => {
872
+ const marker = element.classList.contains("oc-endpoint-marker");
873
+ const leader = element.classList.contains("oc-endpoint-leader");
874
+ const prefix = `endpoint:${key}:${leading}:${index}`;
875
+ for (const name of ["x", "x1", "x2", "cx"]) {
876
+ const offset = marker ? (before.marker?.x ?? 0) - (after.marker?.x ?? 0) : leader && name === "x2" ? previous.area.x + (leading ? 0 : previous.area.width) - next.area.x - (leading ? 0 : next.area.width) : dx;
877
+ add(element, name, offset, `${prefix}:${name}`);
812
878
  }
813
- } else if (p.type === "area" && n.type === "area") {
814
- if (p.path !== n.path) return true;
815
- } else if (p.type === "arc" && n.type === "arc") {
816
- if (p.startAngle !== n.startAngle || p.endAngle !== n.endAngle || p.innerRadius !== n.innerRadius || p.outerRadius !== n.outerRadius || p.center.x !== n.center.x || p.center.y !== n.center.y) {
817
- return true;
879
+ for (const name of ["y", "y1", "y2", "cy"]) {
880
+ const offset = marker || leader && name === "y2" ? before.dataY - after.dataY : dy;
881
+ add(element, name, offset, `${prefix}:${name}`);
818
882
  }
819
- } else if (p.type === "point" && n.type === "point") {
820
- if (p.cx !== n.cx || p.cy !== n.cy || p.r !== n.r) return true;
821
- } else if (p.type === "rule" && n.type === "rule") {
822
- if (p.x1 !== n.x1 || p.y1 !== n.y1 || p.x2 !== n.x2 || p.y2 !== n.y2) return true;
823
- } else if (p.type === "tick" && n.type === "tick") {
824
- if (p.x !== n.x || p.y !== n.y) return true;
825
- } else if (p.type === "textMark" && n.type === "textMark") {
826
- if (p.x !== n.x || p.y !== n.y) return true;
827
- }
883
+ });
828
884
  }
829
- return false;
885
+ const beforeMarks = new Map(
886
+ previous.marks.filter((mark) => mark.key).map((mark) => [mark.key, mark])
887
+ );
888
+ const afterMarks = new Map(next.marks.filter((mark) => mark.key).map((mark) => [mark.key, mark]));
889
+ for (const label of svg.querySelectorAll("[data-label-key]")) {
890
+ const key = label.getAttribute("data-label-key");
891
+ const before = beforeMarks.get(key);
892
+ const after = afterMarks.get(key);
893
+ if (!before || !after || !("label" in before) || !("label" in after) || !before.label || !after.label)
894
+ continue;
895
+ const dx = before.type === "arc" && after.type === "arc" ? before.center.x - after.center.x : 0;
896
+ const dy = before.type === "arc" && after.type === "arc" ? before.center.y - after.center.y : 0;
897
+ add(label, "x", before.label.x - after.label.x - dx, `label:${key}:x`);
898
+ add(label, "y", before.label.y - after.label.y - dy, `label:${key}:y`);
899
+ }
900
+ let progress = 0;
901
+ return {
902
+ apply(value) {
903
+ progress = value;
904
+ for (const item of attributes)
905
+ item.element.setAttribute(item.name, String(item.from + (item.to - item.from) * value));
906
+ },
907
+ snapshot(target) {
908
+ for (const item of attributes)
909
+ target.set(item.key, {
910
+ type: "attribute",
911
+ value: item.from + (item.to - item.from) * progress
912
+ });
913
+ }
914
+ };
830
915
  }
831
916
 
832
917
  // src/transition/driver.ts
833
- import { isGradientDef } from "@opendata-ai/openchart-core";
918
+ import { isGradientDef, sampleMotion as sampleMotion3 } from "@opendata-ai/openchart-core";
834
919
  import { buildAreaPath as buildAreaPath2, buildLinePath as buildLinePath2, EXIT_DEFAULTS } from "@opendata-ai/openchart-engine";
835
920
 
836
921
  // src/transition/apply.ts
922
+ import { sampleMotion as sampleMotion2 } from "@opendata-ai/openchart-core";
837
923
  import { buildArcPath } from "@opendata-ai/openchart-engine";
838
924
 
839
925
  // src/transition/canvas-tweens.ts
926
+ import { sampleMotion } from "@opendata-ai/openchart-core";
840
927
  import { serializeKeyValue as serializeKeyValue2 } from "@opendata-ai/openchart-engine";
841
928
 
842
929
  // src/transition/chrome-tweens.ts
@@ -1183,7 +1270,7 @@ function buildCanvasPointsTween(prevLayout, nextLayout, layer, tweens, fromSnaps
1183
1270
  }
1184
1271
  function applyCanvasPointsState(tw, elapsed, update, exit, enterDelay, enterDuration) {
1185
1272
  const soa = tw.layer.state.marks;
1186
- const easedUpdate = cubicOut(Math.min(elapsed / update.duration, 1));
1273
+ const easedUpdate = sampleMotion(update, elapsed).position;
1187
1274
  for (let s = 0; s < tw.soaIndex.length; s++) {
1188
1275
  const i = tw.soaIndex[s];
1189
1276
  soa.x[i] = tw.fromCx[s] + (tw.toCx[s] - tw.fromCx[s]) * easedUpdate;
@@ -1199,7 +1286,7 @@ function applyCanvasPointsState(tw, elapsed, update, exit, enterDelay, enterDura
1199
1286
  }
1200
1287
  }
1201
1288
  if (tw.exitState) {
1202
- const easedExit = cubicOut(Math.min(elapsed / exit.duration, 1));
1289
+ const easedExit = sampleMotion(exit, elapsed).position;
1203
1290
  tw.exitState.alpha = 1 - easedExit;
1204
1291
  tw.layer.state.exiting = tw.exitState;
1205
1292
  }
@@ -1266,7 +1353,7 @@ function buildCanvasGridlinesTween(prevLayout, nextLayout, layer, tweens) {
1266
1353
  }
1267
1354
  function applyCanvasGridlinesState(tw, elapsed, update, enterDelay, enterDuration) {
1268
1355
  const live = tw.layer.state.gridlines;
1269
- const easedUpdate = cubicOut(Math.min(elapsed / update.duration, 1));
1356
+ const easedUpdate = sampleMotion(update, elapsed).position;
1270
1357
  const tEnter = elapsed < enterDelay ? 0 : Math.min((elapsed - enterDelay) / enterDuration, 1);
1271
1358
  const easedEnter = cubicOut(tEnter);
1272
1359
  for (let i = 0; i < tw.index.length; i++) {
@@ -1614,19 +1701,9 @@ function applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration) {
1614
1701
  applyCanvasGridlinesState(tw, elapsed, update, enterDelay, enterDuration);
1615
1702
  return;
1616
1703
  }
1617
- let tLocal;
1618
- if (tw.kind === "exit") {
1619
- tLocal = Math.min(elapsed / exit.duration, 1);
1620
- } else if (tw.kind === "enter") {
1621
- if (elapsed < enterDelay) {
1622
- tLocal = 0;
1623
- } else {
1624
- tLocal = Math.min((elapsed - enterDelay) / enterDuration, 1);
1625
- }
1626
- } else {
1627
- tLocal = Math.min(elapsed / update.duration, 1);
1628
- }
1629
- const eased = cubicOut(tLocal);
1704
+ const phase = tw.kind === "exit" ? exit : tw.kind === "enter" ? { duration: enterDuration } : update;
1705
+ const elapsedMs = tw.kind === "enter" ? Math.max(0, elapsed - enterDelay) : elapsed;
1706
+ const eased = sampleMotion2(phase, elapsedMs).position;
1630
1707
  switch (tw.tweenType) {
1631
1708
  case "rect": {
1632
1709
  const geom = lerpGeom(tw.from, tw.to, eased);
@@ -1640,11 +1717,13 @@ function applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration) {
1640
1717
  break;
1641
1718
  }
1642
1719
  case "line": {
1720
+ if (tw.kind !== "update") break;
1643
1721
  tw.ptsBuffer = interpolatePoints(tw.fromPts, tw.toPts, eased, tw.ptsBuffer);
1644
1722
  applyLinePath(tw.el, tw.ptsBuffer, tw.interpolate, tw.pathEl);
1645
1723
  break;
1646
1724
  }
1647
1725
  case "area": {
1726
+ if (tw.kind !== "update") break;
1648
1727
  tw.topBuffer = interpolatePoints(tw.fromTop, tw.toTop, eased, tw.topBuffer);
1649
1728
  tw.bottomBuffer = interpolatePoints(tw.fromBottom, tw.toBottom, eased, tw.bottomBuffer);
1650
1729
  applyAreaPaths(
@@ -1750,9 +1829,11 @@ function snapTweenToFinal(tw) {
1750
1829
  }
1751
1830
  break;
1752
1831
  case "line":
1832
+ if (tw.kind !== "update") break;
1753
1833
  applyLinePath(tw.el, tw.toPts, tw.interpolate, tw.pathEl);
1754
1834
  break;
1755
1835
  case "area":
1836
+ if (tw.kind !== "update") break;
1756
1837
  applyAreaPaths(
1757
1838
  tw.el,
1758
1839
  tw.toTop,
@@ -1976,7 +2057,7 @@ function buildLineTweens(prevLayout, nextLayout, marksContainer, dom, tweens, gh
1976
2057
  const el = dom.any(key);
1977
2058
  if (!el) continue;
1978
2059
  const snap = fromSnapshot?.get(key);
1979
- if (snap && snap.type === "line") {
2060
+ if (snap && snap.type === "line" && !snap.points) {
1980
2061
  const ghost = el.cloneNode(true);
1981
2062
  ghost.classList.add("oc-ghost");
1982
2063
  ghost.setAttribute("aria-hidden", "true");
@@ -2014,11 +2095,12 @@ function buildLineTweens(prevLayout, nextLayout, marksContainer, dom, tweens, gh
2014
2095
  });
2015
2096
  continue;
2016
2097
  }
2017
- const prevPKs = prev.pointKeys ?? prev.points.map((_, i) => `${i}`);
2098
+ const prevPKs = snap?.type === "line" && snap.pointKeys ? snap.pointKeys : prev.pointKeys ?? prev.points.map((_, i) => `${i}`);
2018
2099
  const nextPKs = next.pointKeys ?? next.points.map((_, i) => `${i}`);
2019
2100
  const prevKeySet = new Set(prevPKs);
2020
2101
  const hasSurvivors = nextPKs.some((k) => prevKeySet.has(k));
2021
- if (!hasSurvivors) {
2102
+ const hasGaps = (prev.path?.match(/M/g)?.length ?? 0) > 1 || (next.path?.match(/M/g)?.length ?? 0) > 1;
2103
+ if (!hasSurvivors || hasGaps) {
2022
2104
  const ghost = renderSingleMark(prev, 0);
2023
2105
  if (ghost) {
2024
2106
  ghost.classList.add("oc-ghost");
@@ -2054,13 +2136,19 @@ function buildLineTweens(prevLayout, nextLayout, marksContainer, dom, tweens, gh
2054
2136
  pathEl: resolveLinePathEl(el)
2055
2137
  });
2056
2138
  } else {
2057
- const [fromPts, toPts] = normalizePointArrays(prev.points, next.points, prevPKs, nextPKs);
2139
+ const [fromPts, toPts] = normalizePointArrays(
2140
+ snap?.type === "line" && snap.points ? snap.points : prev.points,
2141
+ next.points,
2142
+ prevPKs,
2143
+ nextPKs
2144
+ );
2058
2145
  tweens.push({
2059
2146
  tweenType: "line",
2060
2147
  kind: "update",
2061
2148
  el,
2062
2149
  fromPts,
2063
2150
  toPts,
2151
+ pointKeys: mergeKeyOrder(prevPKs, nextPKs),
2064
2152
  finalPoints: next.points,
2065
2153
  interpolate: next.interpolate,
2066
2154
  pathEl: resolveLinePathEl(el)
@@ -2179,7 +2267,8 @@ function buildAreaTweens(prevLayout, nextLayout, marksContainer, dom, tweens, gh
2179
2267
  const nextPKs = next.pointKeys ?? next.topPoints.map((_, i) => `${i}`);
2180
2268
  const prevKeySet = new Set(prevPKs);
2181
2269
  const hasSurvivors = nextPKs.some((k) => prevKeySet.has(k));
2182
- if (!hasSurvivors) {
2270
+ const hasGaps = (prev.path?.match(/M/g)?.length ?? 0) > 1 || (next.path?.match(/M/g)?.length ?? 0) > 1;
2271
+ if (!hasSurvivors || hasGaps) {
2183
2272
  const ghost = renderSingleMark(prev, 0);
2184
2273
  if (ghost) {
2185
2274
  ghost.classList.add("oc-ghost");
@@ -2237,6 +2326,7 @@ function buildAreaTweens(prevLayout, nextLayout, marksContainer, dom, tweens, gh
2237
2326
  );
2238
2327
  tweens.push({
2239
2328
  tweenType: "area",
2329
+ pointKeys: mergeKeyOrder(prevPKs, nextPKs),
2240
2330
  kind: "update",
2241
2331
  el,
2242
2332
  fromTop,
@@ -2865,16 +2955,13 @@ function runTransition(args) {
2865
2955
  }
2866
2956
  buildColorTweens(prevLayout, nextLayout, dom, tweens);
2867
2957
  buildAnnotationTweens(svg, prevLayout, nextLayout, tweens);
2868
- const secondaryEls = collectSecondaryElements(svg);
2869
- const secondaryOriginalOpacity = secondaryEls.map((el) => el.style.opacity ?? "");
2958
+ const labelMotion = buildLabelMotion(svg, prevLayout, nextLayout, fromSnapshot);
2959
+ labelMotion.apply(0);
2870
2960
  for (const tw of tweens) {
2871
2961
  applyTweenState(tw, 0, update, exit, enterDelay, enterDuration);
2872
2962
  }
2873
2963
  canvas?.repaint();
2874
2964
  canvas?.setInteractionSuspended(true);
2875
- for (const el of secondaryEls) {
2876
- el.style.opacity = "0";
2877
- }
2878
2965
  let rafId = null;
2879
2966
  let running = true;
2880
2967
  let pulsedDot = null;
@@ -2891,7 +2978,7 @@ function runTransition(args) {
2891
2978
  for (const tw of tweens) {
2892
2979
  applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration);
2893
2980
  }
2894
- applySecondaryOpacity(secondaryEls, elapsed, enterDelay, enterDuration);
2981
+ labelMotion.apply(sampleMotion3(update, elapsed).position);
2895
2982
  canvas?.repaint();
2896
2983
  if (tGlobal >= 1) {
2897
2984
  finish();
@@ -2914,9 +3001,6 @@ function runTransition(args) {
2914
3001
  removeGhosts();
2915
3002
  settleCanvas();
2916
3003
  pulsedDot = pulseSparklineTerminator(svg, marksContainer);
2917
- for (let i = 0; i < secondaryEls.length; i++) {
2918
- secondaryEls[i].style.opacity = secondaryOriginalOpacity[i];
2919
- }
2920
3004
  onComplete();
2921
3005
  }
2922
3006
  function snapToFinal() {
@@ -2935,16 +3019,16 @@ function runTransition(args) {
2935
3019
  }
2936
3020
  }
2937
3021
  function removeGhosts() {
3022
+ labelMotion.apply(1);
2938
3023
  for (const ghost of ghosts) {
2939
3024
  ghost.parentNode?.removeChild(ghost);
2940
3025
  }
2941
3026
  }
2942
3027
  function captureSnapshot() {
2943
3028
  const snap = /* @__PURE__ */ new Map();
2944
- const tUpdate = Math.min(lastElapsed / update.duration, 1);
2945
- const easedUpdate = cubicOut(tUpdate);
2946
3029
  for (const tw of tweens) {
2947
- if (tw.kind !== "update") continue;
3030
+ const easedUpdate = tw.kind === "enter" ? sampleMotion3({ duration: enterDuration }, Math.max(0, lastElapsed - enterDelay)).position : sampleMotion3(update, lastElapsed).position;
3031
+ if (tw.kind === "exit") continue;
2948
3032
  if (tw.tweenType === "canvasPoints") {
2949
3033
  for (let s = 0; s < tw.soaIndex.length; s++) {
2950
3034
  snap.set(tw.soaKeys[s], {
@@ -2995,19 +3079,26 @@ function runTransition(args) {
2995
3079
  }
2996
3080
  case "line": {
2997
3081
  const pts = interpolatePoints(tw.fromPts, tw.toPts, easedUpdate);
2998
- const d = buildLinePath2(pts, tw.interpolate);
2999
- snap.set(key, { type: "line", d });
3082
+ const d = tw.kind === "update" ? buildLinePath2(pts, tw.interpolate) : tw.pathEl?.getAttribute("d") ?? "";
3083
+ snap.set(key, { type: "line", d, points: pts, pointKeys: tw.pointKeys });
3000
3084
  break;
3001
3085
  }
3002
3086
  case "area": {
3003
3087
  const top = interpolatePoints(tw.fromTop, tw.toTop, easedUpdate);
3004
3088
  const bottom = interpolatePoints(tw.fromBottom, tw.toBottom, easedUpdate);
3005
- const d = buildAreaPath2(top, bottom, tw.interpolate);
3089
+ const d = tw.kind === "update" ? buildAreaPath2(top, bottom, tw.interpolate) : tw.fillPathEl?.getAttribute("d") ?? "";
3006
3090
  let topD;
3007
3091
  if (tw.hasStroke) {
3008
- topD = buildLinePath2(top, tw.interpolate);
3092
+ topD = tw.kind === "update" ? buildLinePath2(top, tw.interpolate) : tw.topPathEl?.getAttribute("d") ?? "";
3009
3093
  }
3010
- snap.set(key, { type: "area", d, topD });
3094
+ snap.set(key, {
3095
+ type: "area",
3096
+ d,
3097
+ topD,
3098
+ points: top,
3099
+ bottomPoints: bottom,
3100
+ pointKeys: tw.pointKeys
3101
+ });
3011
3102
  break;
3012
3103
  }
3013
3104
  case "arc": {
@@ -3021,6 +3112,7 @@ function runTransition(args) {
3021
3112
  }
3022
3113
  }
3023
3114
  }
3115
+ labelMotion.snapshot(snap);
3024
3116
  return snap;
3025
3117
  }
3026
3118
  if (!manual) {
@@ -3041,9 +3133,6 @@ function runTransition(args) {
3041
3133
  snapToFinal();
3042
3134
  removeGhosts();
3043
3135
  settleCanvas();
3044
- for (let i = 0; i < secondaryEls.length; i++) {
3045
- secondaryEls[i].style.opacity = secondaryOriginalOpacity[i];
3046
- }
3047
3136
  },
3048
3137
  get running() {
3049
3138
  return running;
@@ -3056,7 +3145,7 @@ function runTransition(args) {
3056
3145
  for (const tw of tweens) {
3057
3146
  applyTweenState(tw, elapsedMs, update, exit, enterDelay, enterDuration);
3058
3147
  }
3059
- applySecondaryOpacity(secondaryEls, elapsedMs, enterDelay, enterDuration);
3148
+ labelMotion.apply(sampleMotion3(update, elapsedMs).position);
3060
3149
  if (elapsedMs >= totalMs) {
3061
3150
  snapToFinal();
3062
3151
  removeGhosts();
@@ -3100,27 +3189,6 @@ function pulseSparklineTerminator(svg, marksContainer) {
3100
3189
  );
3101
3190
  return dot;
3102
3191
  }
3103
- function collectSecondaryElements(svg) {
3104
- const els = [];
3105
- const epLabels = svg.querySelector(".oc-endpoint-labels");
3106
- if (epLabels) els.push(epLabels);
3107
- const markLabels = svg.querySelector(".oc-mark-labels");
3108
- if (markLabels) els.push(markLabels);
3109
- return els;
3110
- }
3111
- function applySecondaryOpacity(els, elapsed, enterDelay, enterDuration) {
3112
- if (els.length === 0) return;
3113
- let t;
3114
- if (elapsed < enterDelay) {
3115
- t = 0;
3116
- } else {
3117
- t = Math.min((elapsed - enterDelay) / enterDuration, 1);
3118
- }
3119
- const opacity = String(cubicOut(t));
3120
- for (const el of els) {
3121
- el.style.opacity = opacity;
3122
- }
3123
- }
3124
3192
 
3125
3193
  // src/dom-helpers.ts
3126
3194
  function applySrOnlyStyles(el) {
@@ -3170,10 +3238,12 @@ function createYouDrawIt(options) {
3170
3238
  return pattern;
3171
3239
  }
3172
3240
  function findTargetLinePath(svg, seriesKey) {
3173
- if (seriesKey) {
3174
- const escaped = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(seriesKey) : seriesKey.replace(/"/g, '\\"');
3175
- const byKey = svg.querySelector(`[data-mark-id="line-${escaped}"] path`);
3176
- if (byKey) return byKey;
3241
+ if (seriesKey !== void 0) {
3242
+ const group = [...svg.querySelectorAll(".oc-mark-line")].find(
3243
+ (element) => element.getAttribute("data-series") === seriesKey
3244
+ );
3245
+ const path = group?.querySelector("path");
3246
+ if (path) return path;
3177
3247
  }
3178
3248
  return svg.querySelector(".oc-mark-line path");
3179
3249
  }
@@ -3584,6 +3654,78 @@ function createYouDrawIt(options) {
3584
3654
  };
3585
3655
  }
3586
3656
 
3657
+ // src/transition/reconcile.ts
3658
+ function identity(element, index) {
3659
+ for (const attr of ["data-key", "data-label-key", "data-endpoint-key", "data-tick-value"]) {
3660
+ const value = element.getAttribute(attr);
3661
+ if (value !== null)
3662
+ return `${element.tagName}:${attr}:${value}:${element.getAttribute("data-endpoint-side") ?? ""}`;
3663
+ }
3664
+ return `${element.tagName}:${element.getAttribute("class") ?? ""}:${index}`;
3665
+ }
3666
+ function reconcileSVG(current, next) {
3667
+ for (const attribute of Array.from(current.attributes)) {
3668
+ if (!next.hasAttribute(attribute.name)) current.removeAttribute(attribute.name);
3669
+ }
3670
+ for (const attribute of Array.from(next.attributes)) {
3671
+ if (current.getAttribute(attribute.name) !== attribute.value) {
3672
+ current.setAttribute(attribute.name, attribute.value);
3673
+ }
3674
+ }
3675
+ const candidates = new Map(
3676
+ Array.from(current.children, (child, index) => [identity(child, index), child])
3677
+ );
3678
+ const survivors = /* @__PURE__ */ new Set();
3679
+ let elementIndex = 0;
3680
+ Array.from(next.childNodes).forEach((child, index) => {
3681
+ const match = child.nodeType === 1 ? candidates.get(identity(child, elementIndex++)) : void 0;
3682
+ const node = match ?? child;
3683
+ if (match) reconcileSVG(match, child);
3684
+ survivors.add(node);
3685
+ const at = current.childNodes[index];
3686
+ if (at !== node) current.insertBefore(node, at ?? null);
3687
+ });
3688
+ for (const child of Array.from(current.childNodes)) {
3689
+ if (!survivors.has(child)) current.removeChild(child);
3690
+ }
3691
+ }
3692
+
3693
+ // src/transition/scene.ts
3694
+ function sampleLayout(layout, snapshot) {
3695
+ if (!snapshot?.size) return layout;
3696
+ const marks = layout.marks.map((mark) => {
3697
+ const geometry = mark.key ? snapshot.get(mark.key) : void 0;
3698
+ if (!geometry) return mark;
3699
+ if ((mark.type === "line" && geometry.type === "line" || mark.type === "area" && geometry.type === "area") && geometry.points) {
3700
+ const positions = new Map(
3701
+ (geometry.pointKeys ?? mark.pointKeys ?? []).map((key, index) => [
3702
+ key,
3703
+ geometry.points[index]
3704
+ ])
3705
+ );
3706
+ return {
3707
+ ...mark,
3708
+ ...mark.type === "line" ? { points: geometry.points } : {
3709
+ topPoints: geometry.points,
3710
+ bottomPoints: geometry.type === "area" ? geometry.bottomPoints ?? mark.bottomPoints : mark.bottomPoints
3711
+ },
3712
+ path: geometry.d,
3713
+ dataPoints: mark.dataPoints?.map((point, index) => ({
3714
+ ...point,
3715
+ ...positions.get(mark.pointKeys?.[index] ?? "") ?? point
3716
+ }))
3717
+ };
3718
+ }
3719
+ if (mark.type === "arc" && geometry.type === "arc")
3720
+ return { ...mark, ...geometry, center: { x: geometry.cx, y: geometry.cy } };
3721
+ if (mark.type === "rect" && geometry.type === "rect") return { ...mark, ...geometry };
3722
+ if (mark.type === "point" && geometry.type === "point")
3723
+ return { ...mark, ...geometry, r: geometry.r ?? mark.r };
3724
+ return mark;
3725
+ });
3726
+ return { ...layout, marks };
3727
+ }
3728
+
3587
3729
  // src/mount.ts
3588
3730
  import { cssTokenDefault, isGraphSpec, isLayerSpec } from "@opendata-ai/openchart-core";
3589
3731
  import {
@@ -3619,7 +3761,7 @@ function setupAnimationCleanup(svg, onComplete, totalMs) {
3619
3761
  cancelAnimations(svg);
3620
3762
  };
3621
3763
  }
3622
- function setupTableAnimationCleanup(wrapper) {
3764
+ function setupTableAnimationCleanup(wrapper, onComplete) {
3623
3765
  const style = wrapper.style;
3624
3766
  const duration = parseFloat(style.getPropertyValue("--oc-animation-duration")) || 500;
3625
3767
  const stagger = parseFloat(style.getPropertyValue("--oc-animation-stagger")) || 0;
@@ -3628,6 +3770,7 @@ function setupTableAnimationCleanup(wrapper) {
3628
3770
  const totalTime = totalStagger + duration + 300;
3629
3771
  const timer = setTimeout(() => {
3630
3772
  wrapper.classList.remove("oc-animate");
3773
+ onComplete?.();
3631
3774
  }, totalTime);
3632
3775
  return () => {
3633
3776
  clearTimeout(timer);
@@ -3642,13 +3785,13 @@ function buildMarkDataMap(layout) {
3642
3785
  const mark = layout.marks[i];
3643
3786
  switch (mark.type) {
3644
3787
  case "line":
3645
- map.set(`line-${mark.seriesKey ?? i}`, {
3788
+ map.set(`line-${i}`, {
3646
3789
  datum: mark.data[0] ?? {},
3647
3790
  series: mark.seriesKey
3648
3791
  });
3649
3792
  break;
3650
3793
  case "area":
3651
- map.set(`area-${mark.seriesKey ?? i}`, {
3794
+ map.set(`area-${i}`, {
3652
3795
  datum: mark.data[0] ?? {},
3653
3796
  series: mark.seriesKey
3654
3797
  });
@@ -3754,7 +3897,9 @@ function collectSeriesGroups(layout) {
3754
3897
  for (let i = 0; i < layout.marks.length; i++) {
3755
3898
  const mark = layout.marks[i];
3756
3899
  if ((mark.type === "line" || mark.type === "area") && mark.dataPoints?.length) {
3757
- const key = mark.seriesKey ?? "__default__";
3900
+ const scope = mark.key?.match(/^layer:\d+:/)?.[0] ?? "";
3901
+ const seriesKey = mark.seriesKey ?? "__default__";
3902
+ const key = `${scope}${seriesKey}`;
3758
3903
  const existingType = markTypeByKey.get(key);
3759
3904
  if (existingType === "line" && mark.type === "area") continue;
3760
3905
  const color = mark.type === "line" ? mark.stroke : getRepresentativeColor(mark.fill);
@@ -3762,7 +3907,7 @@ function collectSeriesGroups(layout) {
3762
3907
  for (const dp of mark.dataPoints) {
3763
3908
  pointsByX.set(snapKey(dp.x), { ...dp });
3764
3909
  }
3765
- byKey.set(key, { seriesKey: key, color, pointsByX });
3910
+ byKey.set(key, { key, seriesKey, markId: `${mark.type}-${i}`, color, pointsByX });
3766
3911
  markTypeByKey.set(key, mark.type);
3767
3912
  }
3768
3913
  }
@@ -3796,6 +3941,9 @@ function buildSliceTooltip(hits, emphasisKey) {
3796
3941
  const title = hits[0].point.tooltip?.title;
3797
3942
  const fields = [];
3798
3943
  const isMulti = hits.length > 1;
3944
+ const seriesCounts = /* @__PURE__ */ new Map();
3945
+ for (const { group } of hits)
3946
+ seriesCounts.set(group.seriesKey, (seriesCounts.get(group.seriesKey) ?? 0) + 1);
3799
3947
  let total;
3800
3948
  for (const { group, point } of hits) {
3801
3949
  const tip = point.tooltip;
@@ -3808,10 +3956,10 @@ function buildSliceTooltip(hits, emphasisKey) {
3808
3956
  ) ?? candidates[candidates.length - 1] ?? null;
3809
3957
  if (!yField) continue;
3810
3958
  fields.push({
3811
- label: group.seriesKey,
3959
+ label: group.seriesKey === "__default__" ? yField.label : (seriesCounts.get(group.seriesKey) ?? 0) > 1 ? `${group.seriesKey} \xB7 ${yField.label}` : group.seriesKey,
3812
3960
  value: yField.value,
3813
3961
  color: group.color,
3814
- ...emphasisKey === group.seriesKey ? { emphasis: true } : {}
3962
+ ...emphasisKey === group.key ? { emphasis: true } : {}
3815
3963
  });
3816
3964
  } else {
3817
3965
  for (const f of tip.fields) {
@@ -3884,8 +4032,10 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager, emphasis) {
3884
4032
  crosshair.style.display = "";
3885
4033
  }
3886
4034
  currentEmphasis = emphasisKey;
3887
- if (emphasisKey) emphasis?.setSeries(emphasisKey);
3888
- else emphasis?.clear();
4035
+ const emphasizedGroup = groups.find((group) => group.key === emphasisKey);
4036
+ if (emphasizedGroup) {
4037
+ emphasis?.setMark(svg.querySelector(`[data-mark-id="${emphasizedGroup.markId}"]`));
4038
+ } else emphasis?.clear();
3889
4039
  const tooltip = buildSliceTooltip(hits, emphasisKey);
3890
4040
  if (!tooltip) return false;
3891
4041
  const containerAnchorX = snappedX * viewBoxToContainer;
@@ -3898,7 +4048,7 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager, emphasis) {
3898
4048
  function keysAt(snappedX) {
3899
4049
  const keys = [];
3900
4050
  for (const group of groups) {
3901
- if (group.pointsByX.has(snappedX)) keys.push(group.seriesKey);
4051
+ if (group.pointsByX.has(snappedX)) keys.push(group.key);
3902
4052
  }
3903
4053
  return keys;
3904
4054
  }
@@ -3914,7 +4064,7 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager, emphasis) {
3914
4064
  const dy = Math.abs(point.y - svgY);
3915
4065
  if (dy < nearestDy) {
3916
4066
  nearestDy = dy;
3917
- nearestKey = group.seriesKey;
4067
+ nearestKey = group.key;
3918
4068
  }
3919
4069
  }
3920
4070
  const emphasisKey = nearestDy <= EMPHASIS_PROXIMITY_PX ? nearestKey : null;
@@ -3959,13 +4109,12 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager, emphasis) {
3959
4109
  if (te.touches.length === 0) return;
3960
4110
  const t = te.touches[0];
3961
4111
  const { svgX, svgY, viewBoxToContainer } = toSvgCoords(t.clientX, t.clientY);
3962
- if (te.cancelable) te.preventDefault();
3963
4112
  positionAt(svgX, svgY, viewBoxToContainer);
3964
4113
  };
3965
4114
  overlay.addEventListener("mousemove", handleMouseMove);
3966
4115
  overlay.addEventListener("mouseleave", hideOnPointerOut);
3967
- overlay.addEventListener("touchstart", handleTouch, { passive: false });
3968
- overlay.addEventListener("touchmove", handleTouch, { passive: false });
4116
+ overlay.addEventListener("touchstart", handleTouch, { passive: true });
4117
+ overlay.addEventListener("touchmove", handleTouch, { passive: true });
3969
4118
  overlay.addEventListener("touchend", hideOnPointerOut);
3970
4119
  return {
3971
4120
  cleanup() {
@@ -4269,7 +4418,7 @@ function wireAnnotationDrag(svg, specAnnotations, onAnnotationEdit, onEdit, setD
4269
4418
  };
4270
4419
  }
4271
4420
  function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
4272
- const SVG_NS5 = "http://www.w3.org/2000/svg";
4421
+ const SVG_NS4 = "http://www.w3.org/2000/svg";
4273
4422
  const cleanups = [];
4274
4423
  const annotationGroups = svg.querySelectorAll(".oc-annotation-text");
4275
4424
  for (const el of annotationGroups) {
@@ -4307,7 +4456,7 @@ function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
4307
4456
  const createdHandles = [];
4308
4457
  for (const ep of endpoints) {
4309
4458
  if (!Number.isFinite(ep.cx) || !Number.isFinite(ep.cy)) continue;
4310
- const handleEl = document.createElementNS(SVG_NS5, "circle");
4459
+ const handleEl = document.createElementNS(SVG_NS4, "circle");
4311
4460
  handleEl.setAttribute("class", "oc-connector-handle");
4312
4461
  handleEl.setAttribute("data-endpoint", ep.name);
4313
4462
  handleEl.setAttribute("cx", String(ep.cx));
@@ -4584,7 +4733,7 @@ function wireSeriesLabelDrag(svg, spec, onEdit, setDragging) {
4584
4733
  function wireAnchorDrag(svg, specAnnotations, layout, onEdit, setDragging) {
4585
4734
  if (!onEdit || !layout.xInvert || !layout.yInvert) return () => {
4586
4735
  };
4587
- const SVG_NS5 = "http://www.w3.org/2000/svg";
4736
+ const SVG_NS4 = "http://www.w3.org/2000/svg";
4588
4737
  const cleanups = [];
4589
4738
  const annotationGroups = svg.querySelectorAll(".oc-annotation-text");
4590
4739
  for (const el of annotationGroups) {
@@ -4599,7 +4748,7 @@ function wireAnchorDrag(svg, specAnnotations, layout, onEdit, setDragging) {
4599
4748
  if (!resolved?.dot) continue;
4600
4749
  const anchorX = resolved.dot.x;
4601
4750
  const anchorY = resolved.dot.y;
4602
- const handleEl = document.createElementNS(SVG_NS5, "circle");
4751
+ const handleEl = document.createElementNS(SVG_NS4, "circle");
4603
4752
  handleEl.setAttribute("class", "oc-anchor-handle");
4604
4753
  handleEl.setAttribute("cx", String(anchorX));
4605
4754
  handleEl.setAttribute("cy", String(anchorY));
@@ -5412,6 +5561,7 @@ function wireCanvasInteractions({
5412
5561
  }
5413
5562
 
5414
5563
  // src/scatter-canvas/entrance.ts
5564
+ import { sampleMotion as sampleMotion4 } from "@opendata-ai/openchart-core";
5415
5565
  var MAX_TOTAL_STAGGER_MS = 300;
5416
5566
  var POINT_DURATION_FRACTION = 0.4;
5417
5567
  function clampStagger(delay, elementCount) {
@@ -5430,7 +5580,7 @@ function entranceAlphaAt(t, markIndex, enter, markCount, ease) {
5430
5580
  const local = (t - stagger * markIndex) / pointDuration;
5431
5581
  if (local <= 0) return 0;
5432
5582
  if (local >= 1) return 1;
5433
- return ease(local);
5583
+ return enter.spring ? sampleMotion4(enter, local * enter.duration).position : ease(local);
5434
5584
  }
5435
5585
  function applyEntranceAt(state, t, enter, ease) {
5436
5586
  const alpha = state.enterAlpha;
@@ -5914,8 +6064,8 @@ function createChart(container, spec, options) {
5914
6064
  let isAutoHeight = null;
5915
6065
  let lastRenderedWidth = null;
5916
6066
  let lastRenderedSvgHeight = null;
6067
+ let lastHostHeight = FALLBACK_HEIGHT;
5917
6068
  let transitionHandle = null;
5918
- let transitionSnapshot = null;
5919
6069
  let fontsReloadPending = false;
5920
6070
  let selectedElement = options?.selectedElement ?? null;
5921
6071
  let overlayElement = null;
@@ -5929,13 +6079,13 @@ function createChart(container, spec, options) {
5929
6079
  let youDrawIt = null;
5930
6080
  let ydiEditSuppressWarned = false;
5931
6081
  container.classList.add("oc-root");
5932
- function resolveEffectiveFont() {
5933
- return options?.theme?.fonts?.family ?? currentSpec.theme?.fonts?.family ?? resolveFontFamily(container);
6082
+ function resolveEffectiveFont(candidate = currentSpec) {
6083
+ return options?.theme?.fonts?.family ?? candidate.theme?.fonts?.family ?? resolveFontFamily(container);
5934
6084
  }
5935
6085
  let fontFamily = resolveEffectiveFont();
5936
6086
  let measureText = createMeasureText(fontFamily);
5937
6087
  let renderGen = 0;
5938
- function compileAt(width, height) {
6088
+ function compileAt(width, height, candidate, measure, runtime) {
5939
6089
  const darkMode = resolveDarkMode(options?.darkMode);
5940
6090
  const compileOpts = {
5941
6091
  width,
@@ -5944,12 +6094,11 @@ function createChart(container, spec, options) {
5944
6094
  darkMode,
5945
6095
  renderer: options?.renderer,
5946
6096
  watermark: options?.watermark,
5947
- measureText
6097
+ measureText: measure
5948
6098
  };
5949
- if (isLayerSpec(currentSpec)) {
5950
- return compileLayer(withRuntimeHidden(currentSpec), compileOpts);
5951
- }
5952
- return compileChart(withRuntimeHidden(currentSpec), compileOpts);
6099
+ const effective = runtime ? withRuntimeHidden(candidate) : candidate;
6100
+ if (isLayerSpec(effective)) return compileLayer(effective, compileOpts);
6101
+ return compileChart(effective, compileOpts);
5953
6102
  }
5954
6103
  function verticalOverheads(layout, width) {
5955
6104
  let topLegendBlock = 0;
@@ -5968,19 +6117,19 @@ function createChart(container, spec, options) {
5968
6117
  }
5969
6118
  return layout.chrome.topHeight + layout.chrome.bottomHeight + topLegendBlock + (layout.metrics?.height ?? 0) + facetGrowth;
5970
6119
  }
5971
- function compile() {
5972
- const { width, height } = getContainerDimensions();
5973
- const growsWithChrome = isAutoHeight === true && !isLayerSpec(currentSpec) && !isGraphSpec(currentSpec) && currentSpec.display !== "sparkline";
6120
+ function compile(candidate = currentSpec, measure = measureText, runtime = true) {
6121
+ const { width, height } = getContainerDimensions(candidate);
6122
+ const growsWithChrome = isAutoHeight === true && !isLayerSpec(candidate) && !isGraphSpec(candidate) && candidate.display !== "sparkline";
5974
6123
  if (!growsWithChrome) {
5975
- return compileAt(width, height);
6124
+ return compileAt(width, height, candidate, measure, runtime);
5976
6125
  }
5977
- let layout = compileAt(width, height);
6126
+ let layout = compileAt(width, height, candidate, measure, runtime);
5978
6127
  const overheads = verticalOverheads(layout, width);
5979
6128
  if (overheads > 0) {
5980
- layout = compileAt(width, height + overheads);
6129
+ layout = compileAt(width, height + overheads, candidate, measure, runtime);
5981
6130
  const overheads2 = verticalOverheads(layout, width);
5982
6131
  if (overheads2 !== overheads) {
5983
- layout = compileAt(width, height + overheads2);
6132
+ layout = compileAt(width, height + overheads2, candidate, measure, runtime);
5984
6133
  }
5985
6134
  }
5986
6135
  return layout;
@@ -6062,9 +6211,9 @@ function createChart(container, spec, options) {
6062
6211
  if (!("field" in colorEnc) || !colorEnc.field) return void 0;
6063
6212
  return colorEnc.field;
6064
6213
  }
6065
- function getContainerDimensions() {
6214
+ function getContainerDimensions(candidate = currentSpec) {
6066
6215
  const rect = container.getBoundingClientRect();
6067
- const isSparkline = "display" in currentSpec && currentSpec.display === "sparkline";
6216
+ const isSparkline = "display" in candidate && candidate.display === "sparkline";
6068
6217
  if (isSparkline) {
6069
6218
  let width = rect.width;
6070
6219
  let height = rect.height;
@@ -6081,12 +6230,14 @@ function createChart(container, spec, options) {
6081
6230
  if (isAutoHeight === null && (rect.width > 0 || rect.height > 0)) {
6082
6231
  isAutoHeight = rect.height === 0 && rect.width > 0;
6083
6232
  }
6233
+ const ownHeight = lastRenderedSvgHeight !== null && Math.abs(rect.height - lastRenderedSvgHeight) < 0.5;
6234
+ if (!ownHeight && rect.height > 0) lastHostHeight = rect.height;
6084
6235
  return {
6085
6236
  width: Math.max(rect.width || 600, 100),
6086
6237
  // Latched-auto mounts always compile against the fixed budget: after
6087
6238
  // the first render the container's measured height is just our own
6088
6239
  // SVG, so reading it back would re-pin whatever we rendered last.
6089
- height: isAutoHeight === true ? FALLBACK_HEIGHT : Math.max(rect.height || FALLBACK_HEIGHT, 100)
6240
+ height: isAutoHeight === true ? FALLBACK_HEIGHT : Math.max(ownHeight ? lastHostHeight : rect.height || FALLBACK_HEIGHT, 100)
6090
6241
  };
6091
6242
  }
6092
6243
  function getSpecAnnotations() {
@@ -6314,11 +6465,13 @@ function createChart(container, spec, options) {
6314
6465
  svg.removeEventListener("keydown", handleKeyDown);
6315
6466
  };
6316
6467
  }
6317
- function render() {
6468
+ function render(retainScene = false, prepared) {
6469
+ const retainedSVG = retainScene ? svgElement : null;
6318
6470
  if (isDragging) {
6319
6471
  pendingRender = true;
6320
6472
  return;
6321
6473
  }
6474
+ const nextLayout = prepared ?? compile();
6322
6475
  if (transitionHandle) {
6323
6476
  transitionHandle.cancel();
6324
6477
  transitionHandle = null;
@@ -6363,7 +6516,7 @@ function createChart(container, spec, options) {
6363
6516
  isTextEditingActive = false;
6364
6517
  }
6365
6518
  overlayElement = null;
6366
- if (svgElement?.parentNode) {
6519
+ if (!retainedSVG && svgElement?.parentNode) {
6367
6520
  svgElement.parentNode.removeChild(svgElement);
6368
6521
  }
6369
6522
  if (tooltipManager) {
@@ -6373,7 +6526,7 @@ function createChart(container, spec, options) {
6373
6526
  srTableWrapper.parentNode.removeChild(srTableWrapper);
6374
6527
  srTableWrapper = null;
6375
6528
  }
6376
- currentLayout = compile();
6529
+ currentLayout = nextLayout;
6377
6530
  lastRenderedWidth = currentLayout.dimensions.width;
6378
6531
  lastRenderedSvgHeight = currentLayout.dimensions.height;
6379
6532
  const shouldAnimate = isFirstRender && !!currentLayout.animation?.enter;
@@ -6381,6 +6534,7 @@ function createChart(container, spec, options) {
6381
6534
  const canvasMode = currentLayout.markRenderMode === "canvas";
6382
6535
  if (canvasMode) {
6383
6536
  canvasLayer = createScatterCanvasLayer(container, currentLayout);
6537
+ if (retainedSVG) container.insertBefore(canvasLayer.canvas, retainedSVG);
6384
6538
  canvasLayer.repaint();
6385
6539
  if (!warnedCanvasEditMode && (options?.editable || hasEditingCallbacks(options) || options?.onAnnotationEdit)) {
6386
6540
  warnedCanvasEditMode = true;
@@ -6389,11 +6543,16 @@ function createChart(container, spec, options) {
6389
6543
  );
6390
6544
  }
6391
6545
  }
6392
- svgElement = renderChartSVG(currentLayout, container, {
6546
+ const sceneHost = retainedSVG ? document.createElement("div") : container;
6547
+ svgElement = renderChartSVG(currentLayout, sceneHost, {
6393
6548
  animate: shouldAnimate,
6394
6549
  crosshair,
6395
6550
  canvasMarks: canvasMode
6396
6551
  });
6552
+ if (retainedSVG && svgElement) {
6553
+ reconcileSVG(retainedSVG, svgElement);
6554
+ svgElement = retainedSVG;
6555
+ }
6397
6556
  tooltipManager = createTooltipManager(container);
6398
6557
  if (canvasMode && canvasLayer) {
6399
6558
  cleanupCanvasEvents = wireCanvasInteractions({
@@ -6589,30 +6748,32 @@ function createChart(container, spec, options) {
6589
6748
  function beginManualUpdate(newSpec) {
6590
6749
  return doUpdate(newSpec, void 0, true);
6591
6750
  }
6592
- function doUpdate(newSpec, updateOpts, manual) {
6751
+ function doUpdate(newSpec, updateOpts, manual, layoutOnly = false) {
6593
6752
  if (destroyed) return null;
6594
6753
  const prevSpec = currentSpec;
6595
6754
  const prevLayout = currentLayout;
6596
6755
  const entranceWasRunning = cleanupAnimations != null;
6597
- if (transitionHandle?.running) {
6598
- transitionSnapshot = transitionHandle.snapshot();
6599
- }
6756
+ const nextFont = resolveEffectiveFont(newSpec);
6757
+ const nextMeasure = nextFont === fontFamily ? measureText : createMeasureText(nextFont);
6758
+ const nextLayout = compile(newSpec, nextMeasure, layoutOnly);
6759
+ const snapshot = transitionHandle?.running ? transitionHandle.snapshot() : void 0;
6600
6760
  currentSpec = newSpec;
6601
- const nextFont = resolveEffectiveFont();
6602
- if (nextFont !== fontFamily) {
6603
- fontFamily = nextFont;
6604
- measureText = createMeasureText(fontFamily);
6605
- }
6606
- runtimeHiddenSeries.clear();
6607
- runtimeShownSeries.clear();
6608
- searchBaseline = authoredHighlight(newSpec);
6609
- seriesSearch?.setSelected([]);
6761
+ fontFamily = nextFont;
6762
+ measureText = nextMeasure;
6763
+ if (!layoutOnly) {
6764
+ runtimeHiddenSeries.clear();
6765
+ runtimeShownSeries.clear();
6766
+ }
6767
+ if (!layoutOnly) {
6768
+ searchBaseline = authoredHighlight(newSpec);
6769
+ seriesSearch?.setSelected([]);
6770
+ }
6610
6771
  ydiEditSuppressWarned = false;
6611
6772
  editSuppressWarned = false;
6612
6773
  if (updateOpts && "selectedElement" in updateOpts) {
6613
6774
  selectedElement = updateOpts.selectedElement ?? null;
6614
6775
  }
6615
- render();
6776
+ render(canTransitionSpecShape(prevSpec, newSpec), nextLayout);
6616
6777
  if (svgElement && canTransition({
6617
6778
  prevLayout,
6618
6779
  nextLayout: currentLayout,
@@ -6621,14 +6782,12 @@ function createChart(container, spec, options) {
6621
6782
  isFirstRender: false,
6622
6783
  entranceInFlight: entranceWasRunning
6623
6784
  })) {
6624
- const snapshot = transitionSnapshot;
6625
- transitionSnapshot = null;
6626
6785
  const handle = runTransition({
6627
6786
  svg: svgElement,
6628
6787
  prevLayout,
6629
6788
  nextLayout: currentLayout,
6630
6789
  animation: currentLayout.animation,
6631
- fromSnapshot: snapshot ?? void 0,
6790
+ fromSnapshot: snapshot,
6632
6791
  manual,
6633
6792
  // render() above already rebuilt the layer for the NEXT layout, so this
6634
6793
  // is the destination-mode layer the tween writes into.
@@ -6637,10 +6796,9 @@ function createChart(container, spec, options) {
6637
6796
  transitionHandle = null;
6638
6797
  }
6639
6798
  });
6640
- if (!manual) transitionHandle = handle;
6799
+ transitionHandle = handle;
6641
6800
  return handle;
6642
6801
  }
6643
- transitionSnapshot = null;
6644
6802
  return null;
6645
6803
  }
6646
6804
  function resize() {
@@ -6657,7 +6815,7 @@ function createChart(container, spec, options) {
6657
6815
  pendingResize = true;
6658
6816
  return;
6659
6817
  }
6660
- render();
6818
+ doUpdate(currentSpec, void 0, false, true);
6661
6819
  }
6662
6820
  function handleSearchChange(selected) {
6663
6821
  const merged = [...searchBaseline];
@@ -6694,7 +6852,7 @@ function createChart(container, spec, options) {
6694
6852
  }
6695
6853
  const svg = canvasLayer && currentLayout.markRenderMode === "canvas" ? materializeCanvasModeSVG(currentLayout, {
6696
6854
  forceVector: format === "png" || format === "jpg" || format === "gif"
6697
- }) : svgElement;
6855
+ }) : renderChartSVG(currentLayout, document.createElement("div"), { animate: false });
6698
6856
  const theme = "theme" in currentLayout ? currentLayout.theme : void 0;
6699
6857
  const withTheme = (o) => ({ theme, ...o ?? {} });
6700
6858
  switch (format) {
@@ -6728,7 +6886,6 @@ function createChart(container, spec, options) {
6728
6886
  transitionHandle.cancel();
6729
6887
  transitionHandle = null;
6730
6888
  }
6731
- transitionSnapshot = null;
6732
6889
  if (cleanupAnimations) {
6733
6890
  cleanupAnimations();
6734
6891
  cleanupAnimations = null;
@@ -6810,10 +6967,10 @@ function createChart(container, spec, options) {
6810
6967
  container.dataset.ocFontsState = pending ? "pending" : "ready";
6811
6968
  if (options?.responsive !== false) {
6812
6969
  disconnectResize = observeResize(container, (width, height) => {
6970
+ const widthChanged = lastRenderedWidth === null || Math.abs(width - lastRenderedWidth) >= 0.5;
6971
+ const selfInducedHeight = lastRenderedSvgHeight !== null && Math.abs(height - lastRenderedSvgHeight) < 0.5;
6972
+ if (!widthChanged && selfInducedHeight) return;
6813
6973
  if (isAutoHeight === true) {
6814
- const widthChanged = width !== lastRenderedWidth;
6815
- const selfInducedHeight = height === lastRenderedSvgHeight;
6816
- if (!widthChanged && selfInducedHeight) return;
6817
6974
  if (!selfInducedHeight) {
6818
6975
  isAutoHeight = null;
6819
6976
  }
@@ -6827,6 +6984,10 @@ function createChart(container, spec, options) {
6827
6984
  update,
6828
6985
  beginManualUpdate,
6829
6986
  resize,
6987
+ sampleLayout: () => sampleLayout(
6988
+ currentLayout,
6989
+ transitionHandle?.running ? transitionHandle.snapshot() : void 0
6990
+ ),
6830
6991
  export: doExport,
6831
6992
  destroy,
6832
6993
  get layout() {
@@ -6976,6 +7137,84 @@ function scrubCamera(step, stepProgress, fitted, holdUntil, ease) {
6976
7137
  // src/map-mount.ts
6977
7138
  import { compileGeoMap } from "@opendata-ai/openchart-engine";
6978
7139
 
7140
+ // src/interactions/mark-navigation.ts
7141
+ function captureMarkFocus(svg) {
7142
+ const active = document.activeElement;
7143
+ if (!svg || !active || !svg.contains(active)) return null;
7144
+ return active.getAttribute("data-navigation-key") ?? "";
7145
+ }
7146
+ function restoreMarkFocus(svg, key) {
7147
+ if (key === null) return;
7148
+ const matches = [...svg.querySelectorAll("[data-navigation-key]")].filter(
7149
+ (element) => element.getAttribute("data-navigation-key") === key
7150
+ );
7151
+ const target = matches.length === 1 ? matches[0] : svg;
7152
+ if (target === svg && !svg.hasAttribute("tabindex")) svg.setAttribute("tabindex", "-1");
7153
+ target.focus({ preventScroll: true });
7154
+ }
7155
+ function wireMarkNavigation(svg, marks) {
7156
+ const cleanups = [];
7157
+ if (svg.getAttribute("role") === "img") svg.setAttribute("role", "group");
7158
+ marks.forEach((mark, index) => {
7159
+ const element = mark.element;
7160
+ element.setAttribute("tabindex", index === 0 ? "0" : "-1");
7161
+ element.setAttribute("data-navigation-key", mark.key);
7162
+ const focus = () => {
7163
+ for (const other of marks) other.element.setAttribute("tabindex", "-1");
7164
+ element.setAttribute("tabindex", "0");
7165
+ const bounds = element.getBoundingClientRect();
7166
+ mark.enter(
7167
+ new MouseEvent("focus", {
7168
+ clientX: bounds.left + bounds.width / 2,
7169
+ clientY: bounds.top + bounds.height / 2
7170
+ })
7171
+ );
7172
+ };
7173
+ const keydown = (event) => {
7174
+ let next;
7175
+ switch (event.key) {
7176
+ case "ArrowRight":
7177
+ case "ArrowDown":
7178
+ next = Math.min(index + 1, marks.length - 1);
7179
+ break;
7180
+ case "ArrowLeft":
7181
+ case "ArrowUp":
7182
+ next = Math.max(index - 1, 0);
7183
+ break;
7184
+ case "Home":
7185
+ next = 0;
7186
+ break;
7187
+ case "End":
7188
+ next = marks.length - 1;
7189
+ break;
7190
+ case "Enter":
7191
+ case " ":
7192
+ mark.activate();
7193
+ break;
7194
+ case "Escape":
7195
+ mark.leave();
7196
+ break;
7197
+ default:
7198
+ return;
7199
+ }
7200
+ event.preventDefault();
7201
+ event.stopPropagation();
7202
+ if (next !== void 0) marks[next].element.focus({ preventScroll: true });
7203
+ };
7204
+ element.addEventListener("focus", focus);
7205
+ element.addEventListener("blur", mark.leave);
7206
+ element.addEventListener("keydown", keydown);
7207
+ cleanups.push(() => {
7208
+ element.removeEventListener("focus", focus);
7209
+ element.removeEventListener("blur", mark.leave);
7210
+ element.removeEventListener("keydown", keydown);
7211
+ });
7212
+ });
7213
+ return () => {
7214
+ for (const cleanup of cleanups) cleanup();
7215
+ };
7216
+ }
7217
+
6979
7218
  // src/map-camera.ts
6980
7219
  var FOCUS_DIM_OPACITY = 0.25;
6981
7220
  function focusTargetForFeatures(layout, ids, padding = 16) {
@@ -7073,6 +7312,21 @@ function applyMapCamera(svg, camera, layout) {
7073
7312
  }
7074
7313
  }
7075
7314
  }
7315
+ function ensureMapClip(svg, layout, clipId) {
7316
+ if (svg.querySelector(`#${clipId}`)) return;
7317
+ let defs = svg.querySelector("defs");
7318
+ if (!defs) {
7319
+ defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
7320
+ svg.insertBefore(defs, svg.firstChild);
7321
+ }
7322
+ const clip = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
7323
+ clip.setAttribute("id", clipId);
7324
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
7325
+ rect.setAttribute("width", String(layout.area.width));
7326
+ rect.setAttribute("height", String(layout.area.height));
7327
+ clip.appendChild(rect);
7328
+ defs.appendChild(clip);
7329
+ }
7076
7330
 
7077
7331
  // src/map-renderer.ts
7078
7332
  var SVG_NS3 = "http://www.w3.org/2000/svg";
@@ -7318,6 +7572,14 @@ function renderMapSVG(layout, opts) {
7318
7572
  }
7319
7573
  const classes = animate ? "oc-map oc-animate" : "oc-map";
7320
7574
  svg.setAttribute("class", classes);
7575
+ const background = createSVGElement2("rect");
7576
+ setAttrs2(background, {
7577
+ width,
7578
+ height,
7579
+ fill: layout.theme.colors.background,
7580
+ "aria-hidden": "true"
7581
+ });
7582
+ svg.appendChild(background);
7321
7583
  const bulk = features.length > BULK_ANIMATION_THRESHOLD;
7322
7584
  let mapStaggerBudget = 0;
7323
7585
  if (animate && animation?.enter) {
@@ -7330,6 +7592,7 @@ function renderMapSVG(layout, opts) {
7330
7592
  const stagger = !bulk && n > 1 ? mapStaggerBudget / (n - 1) : 0;
7331
7593
  stampAnimationVars(svg, {
7332
7594
  duration: perFeature,
7595
+ spring: animation.enter.spring,
7333
7596
  stagger,
7334
7597
  annotationDelay: animation.annotationDelay,
7335
7598
  ease: animation.enter.ease
@@ -7498,7 +7761,7 @@ function runMapFillTransition(svg, prev, opts) {
7498
7761
  const key = p.getAttribute("data-key");
7499
7762
  const to = p.getAttribute("fill");
7500
7763
  const from = key ? snapshot.fills.get(key) : void 0;
7501
- if (from && to && from !== to) {
7764
+ if (from && to && from !== to && !from.startsWith("url(") && !to.startsWith("url(")) {
7502
7765
  tweens.push({ kind: "fill", el: p, to, interp: interpolateRgb2(from, to) });
7503
7766
  p.setAttribute("fill", from);
7504
7767
  }
@@ -7566,7 +7829,6 @@ var KEY_PAN_FRACTION = 0.1;
7566
7829
  var DRAG_THRESHOLD = 3;
7567
7830
  var HINT_MS = 1500;
7568
7831
  var EPS = 1e-3;
7569
- var SVG_NS4 = "http://www.w3.org/2000/svg";
7570
7832
  var clipIdCounter = 0;
7571
7833
  var ICONS = {
7572
7834
  in: '<path d="M8 3v10M3 8h10"/>',
@@ -7921,21 +8183,6 @@ function createMapZoomController(host) {
7921
8183
  overlay.style.width = `${area.width / width * 100}%`;
7922
8184
  overlay.style.height = `${area.height / height * 100}%`;
7923
8185
  }
7924
- function ensureClip(svg, layout) {
7925
- if (svg.querySelector(`#${clipId}`)) return;
7926
- let defs = svg.querySelector("defs");
7927
- if (!defs) {
7928
- defs = document.createElementNS(SVG_NS4, "defs");
7929
- svg.insertBefore(defs, svg.firstChild);
7930
- }
7931
- const clip = document.createElementNS(SVG_NS4, "clipPath");
7932
- clip.setAttribute("id", clipId);
7933
- const rect = document.createElementNS(SVG_NS4, "rect");
7934
- rect.setAttribute("width", String(layout.area.width));
7935
- rect.setAttribute("height", String(layout.area.height));
7936
- clip.appendChild(rect);
7937
- defs.appendChild(clip);
7938
- }
7939
8186
  function onRender(restoreFocus) {
7940
8187
  const cfg = host.getConfig();
7941
8188
  const svg = host.getSvg();
@@ -7946,7 +8193,7 @@ function createMapZoomController(host) {
7946
8193
  const layout = host.getLayout();
7947
8194
  svg.setAttribute("tabindex", "0");
7948
8195
  svg.setAttribute("aria-keyshortcuts", "+ - 0 ArrowUp ArrowDown ArrowLeft ArrowRight");
7949
- ensureClip(svg, layout);
8196
+ ensureMapClip(svg, layout, clipId);
7950
8197
  ensureOverlay(cfg.controls);
7951
8198
  placeOverlay(layout);
7952
8199
  if (restoreFocus) svg.focus({ preventScroll: true });
@@ -8077,8 +8324,8 @@ function createGeoMap(container, spec, options) {
8077
8324
  let isFirstRender = true;
8078
8325
  let fontsReloadPending = false;
8079
8326
  container.classList.add("oc-map-root");
8080
- function resolveEffectiveFont() {
8081
- return options?.theme?.fonts?.family ?? currentSpec.theme?.fonts?.family ?? resolveFontFamily(container);
8327
+ function resolveEffectiveFont(candidate = currentSpec) {
8328
+ return options?.theme?.fonts?.family ?? candidate.theme?.fonts?.family ?? resolveFontFamily(container);
8082
8329
  }
8083
8330
  let fontFamily = resolveEffectiveFont();
8084
8331
  let measureText = createMeasureText(fontFamily);
@@ -8095,7 +8342,7 @@ function createGeoMap(container, spec, options) {
8095
8342
  }
8096
8343
  return { width, height: Math.round(width * 0.625) };
8097
8344
  }
8098
- function compile() {
8345
+ function compile(candidate = currentSpec, measure = measureText) {
8099
8346
  const { width, height } = getContainerDimensions();
8100
8347
  const darkMode = resolveDarkMode(options?.darkMode);
8101
8348
  const compileOpts = {
@@ -8104,12 +8351,14 @@ function createGeoMap(container, spec, options) {
8104
8351
  theme: options?.theme,
8105
8352
  darkMode,
8106
8353
  watermark: options?.watermark,
8107
- measureText
8354
+ measureText: measure
8108
8355
  };
8109
- return compileGeoMap(currentSpec, compileOpts);
8356
+ return compileGeoMap(candidate, compileOpts);
8110
8357
  }
8111
8358
  function wireTooltipAndInteraction(svg, layout) {
8112
8359
  const cleanups = [];
8360
+ const navigation = [];
8361
+ let activeLeave = null;
8113
8362
  const featuresById = /* @__PURE__ */ new Map();
8114
8363
  for (const f of layout.features) {
8115
8364
  const key = String(f.id);
@@ -8122,9 +8371,11 @@ function createGeoMap(container, spec, options) {
8122
8371
  const content = layout.tooltipDescriptors.get(featureId);
8123
8372
  const feature = featuresById.get(featureId);
8124
8373
  const handleMouseEnter = (e) => {
8374
+ activeLeave?.();
8375
+ activeLeave = handleMouseLeave;
8125
8376
  const mouseEvent = e;
8126
8377
  el.style.setProperty("cursor", "pointer");
8127
- raiseFeature(el, svg);
8378
+ if (document.activeElement !== el) raiseFeature(el, svg);
8128
8379
  el.classList.add("oc-map-feature--hover");
8129
8380
  if (content && tooltipManager && options?.tooltip !== false) {
8130
8381
  const svgRect = svg.getBoundingClientRect();
@@ -8151,6 +8402,8 @@ function createGeoMap(container, spec, options) {
8151
8402
  }
8152
8403
  };
8153
8404
  const handleMouseLeave = () => {
8405
+ if (activeLeave !== handleMouseLeave) return;
8406
+ activeLeave = null;
8154
8407
  el.style.removeProperty("cursor");
8155
8408
  el.classList.remove("oc-map-feature--hover");
8156
8409
  restoreFeatureOrder(el);
@@ -8167,6 +8420,13 @@ function createGeoMap(container, spec, options) {
8167
8420
  });
8168
8421
  }
8169
8422
  };
8423
+ navigation.push({
8424
+ element: el,
8425
+ key: `feature:${featureId}`,
8426
+ enter: handleMouseEnter,
8427
+ leave: handleMouseLeave,
8428
+ activate: handleClick
8429
+ });
8170
8430
  el.addEventListener("mouseenter", handleMouseEnter);
8171
8431
  el.addEventListener("mousemove", handleMouseMove);
8172
8432
  el.addEventListener("mouseleave", handleMouseLeave);
@@ -8202,6 +8462,8 @@ function createGeoMap(container, spec, options) {
8202
8462
  const content = layout.tooltipDescriptors.get(`point:${pointKey}`);
8203
8463
  const pointMark = pointMarksByKey.get(pointKey);
8204
8464
  const handlePointMouseEnter = (e) => {
8465
+ activeLeave?.();
8466
+ activeLeave = handlePointMouseLeave;
8205
8467
  const mouseEvent = e;
8206
8468
  el.style.setProperty("cursor", "pointer");
8207
8469
  el.style.setProperty("filter", "brightness(1.15)");
@@ -8229,6 +8491,8 @@ function createGeoMap(container, spec, options) {
8229
8491
  }
8230
8492
  };
8231
8493
  const handlePointMouseLeave = () => {
8494
+ if (activeLeave !== handlePointMouseLeave) return;
8495
+ activeLeave = null;
8232
8496
  el.style.removeProperty("cursor");
8233
8497
  el.style.removeProperty("filter");
8234
8498
  tooltipManager?.hide();
@@ -8243,6 +8507,13 @@ function createGeoMap(container, spec, options) {
8243
8507
  });
8244
8508
  }
8245
8509
  };
8510
+ navigation.push({
8511
+ element: el,
8512
+ key: `point:${pointKey}`,
8513
+ enter: handlePointMouseEnter,
8514
+ leave: handlePointMouseLeave,
8515
+ activate: handlePointClick
8516
+ });
8246
8517
  el.addEventListener("mouseenter", handlePointMouseEnter);
8247
8518
  el.addEventListener("mousemove", handlePointMouseMove);
8248
8519
  el.addEventListener("mouseleave", handlePointMouseLeave);
@@ -8254,7 +8525,9 @@ function createGeoMap(container, spec, options) {
8254
8525
  el.removeEventListener("click", handlePointClick);
8255
8526
  });
8256
8527
  }
8528
+ cleanups.push(wireMarkNavigation(svg, navigation));
8257
8529
  return () => {
8530
+ activeLeave?.();
8258
8531
  for (const cleanup of cleanups) {
8259
8532
  cleanup();
8260
8533
  }
@@ -8370,9 +8643,9 @@ function createGeoMap(container, spec, options) {
8370
8643
  });
8371
8644
  liveEl.textContent = `Zoomed to ${names.join(", ")}`;
8372
8645
  }
8373
- function applyFocusDim(ids) {
8374
- if (!svgElement) return;
8375
- const features = svgElement.querySelectorAll(".oc-map-feature");
8646
+ function applyFocusDim(ids, target = svgElement) {
8647
+ if (!target) return;
8648
+ const features = target.querySelectorAll(".oc-map-feature");
8376
8649
  if (!ids || ids.length === 0) {
8377
8650
  for (const el of features) {
8378
8651
  el.style.removeProperty("opacity");
@@ -8389,6 +8662,8 @@ function createGeoMap(container, spec, options) {
8389
8662
  }
8390
8663
  }
8391
8664
  function render(animate = false) {
8665
+ const focusedMark = captureMarkFocus(svgElement);
8666
+ tooltipManager?.hide();
8392
8667
  if (animationCleanup) {
8393
8668
  animationCleanup();
8394
8669
  animationCleanup = null;
@@ -8419,6 +8694,7 @@ function createGeoMap(container, spec, options) {
8419
8694
  const isAnimating = animate && !!currentLayout.animation?.enter;
8420
8695
  if (isAnimating) {
8421
8696
  animationCleanup = setupAnimationCleanup(newSvg, () => {
8697
+ animationCleanup = null;
8422
8698
  if (pendingResize && !destroyed) {
8423
8699
  pendingResize = false;
8424
8700
  resize();
@@ -8451,20 +8727,22 @@ function createGeoMap(container, spec, options) {
8451
8727
  }
8452
8728
  isFirstRender = false;
8453
8729
  syncZoomController(oldSvgHadFocus);
8730
+ if (svgElement) restoreMarkFocus(svgElement, focusedMark);
8454
8731
  }
8455
8732
  function update(newSpec) {
8733
+ if (destroyed) return;
8734
+ const nextFont = resolveEffectiveFont(newSpec);
8735
+ const nextMeasure = nextFont === fontFamily ? measureText : createMeasureText(nextFont);
8736
+ const nextLayout = compile(newSpec, nextMeasure);
8456
8737
  currentSpec = newSpec;
8457
- const nextFont = resolveEffectiveFont();
8458
- if (nextFont !== fontFamily) {
8459
- fontFamily = nextFont;
8460
- measureText = createMeasureText(fontFamily);
8461
- }
8738
+ fontFamily = nextFont;
8739
+ measureText = nextMeasure;
8462
8740
  const canAnimate = !animationCleanup && !prefersReducedMotion2();
8463
8741
  const prevPaint = canAnimate ? captureMapSnapshot(svgElement) : null;
8464
8742
  const prevFocusIds = currentFocusIds ? currentFocusIds.map(String).sort().join(",") : null;
8465
8743
  const prevFocusSig = focusSignature(currentLayout.focus);
8466
8744
  const prevMapSize = currentLayout.mapSize;
8467
- currentLayout = compile();
8745
+ currentLayout = nextLayout;
8468
8746
  if (!currentFocusIds) rescaleFreeCamera(prevMapSize);
8469
8747
  render();
8470
8748
  if (canAnimate && prevPaint && (prevPaint.fills.size > 0 || prevPaint.points.size > 0) && svgElement && currentLayout.animation?.update) {
@@ -8512,15 +8790,25 @@ function createGeoMap(container, spec, options) {
8512
8790
  }
8513
8791
  function exportChart(format, options_) {
8514
8792
  if (!svgElement) return "";
8793
+ const terminalSvg = renderMapSVG(currentLayout, { animate: false });
8794
+ applyMapCamera(terminalSvg, currentCamera, currentLayout);
8795
+ const clip = svgElement.querySelector(".oc-map-group")?.getAttribute("clip-path");
8796
+ if (clip) {
8797
+ const clipId = clip.slice(5, -1);
8798
+ ensureMapClip(terminalSvg, currentLayout, clipId);
8799
+ terminalSvg.querySelector(".oc-map-group")?.setAttribute("clip-path", clip);
8800
+ }
8801
+ applyFocusDim(currentFocusIds, terminalSvg);
8802
+ const resolvedOptions = { theme: currentLayout.theme, ...options_ };
8515
8803
  switch (format) {
8516
8804
  case "svg":
8517
- return exportSVG(svgElement);
8805
+ return exportSVG(terminalSvg);
8518
8806
  case "svg-with-fonts":
8519
- return exportSVGWithFonts(svgElement);
8807
+ return exportSVGWithFonts(terminalSvg, resolvedOptions);
8520
8808
  case "png":
8521
- return exportPNG(svgElement, options_);
8809
+ return exportPNG(terminalSvg, resolvedOptions);
8522
8810
  case "jpg":
8523
- return exportJPG(svgElement, options_);
8811
+ return exportJPG(terminalSvg, resolvedOptions);
8524
8812
  default:
8525
8813
  return "";
8526
8814
  }
@@ -8640,16 +8928,19 @@ export {
8640
8928
  cancelAnimations,
8641
8929
  setupAnimationCleanup,
8642
8930
  setupTableAnimationCleanup,
8931
+ captureMarkFocus,
8932
+ restoreMarkFocus,
8933
+ wireMarkNavigation,
8643
8934
  createMeasureText,
8644
8935
  resolveFontFamily,
8645
8936
  scheduleFontReload,
8646
8937
  VECTOR_EXPORT_MAX_POINTS,
8647
8938
  materializeCanvasModeSVG,
8648
- applySrOnlyStyles,
8649
- createTextEditOverlay,
8650
8939
  DEFAULT_UPDATE_MAX_MARKS,
8651
8940
  CANVAS_DEFAULT_UPDATE_MAX_MARKS,
8652
8941
  canTransitionSpecShape,
8942
+ applySrOnlyStyles,
8943
+ createTextEditOverlay,
8653
8944
  createYouDrawIt,
8654
8945
  createChart,
8655
8946
  easingFns,
@@ -8665,4 +8956,4 @@ export {
8665
8956
  scrubCamera,
8666
8957
  createGeoMap
8667
8958
  };
8668
- //# sourceMappingURL=chunk-QBTRAQAX.js.map
8959
+ //# sourceMappingURL=chunk-6TD7GWR4.js.map