@opendata-ai/openchart-vanilla 8.0.0-rc.24 → 8.0.0-rc.26

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.
@@ -526,7 +526,12 @@ function materializeRasterMarkSVG(layout) {
526
526
  const host = document.createElement("div");
527
527
  const svg = renderChartSVG(layout, host, { animate: false });
528
528
  const dataUrl = rasterizeMarks(layout);
529
- if (!dataUrl) return svg;
529
+ if (!dataUrl) {
530
+ console.warn(
531
+ "[viz] Canvas raster export unavailable in this environment; exporting all points as vector SVG (larger file)."
532
+ );
533
+ return svg;
534
+ }
530
535
  for (const circle of svg.querySelectorAll("circle.oc-mark-point")) {
531
536
  circle.remove();
532
537
  }
@@ -566,6 +571,20 @@ function materializeCanvasModeSVG(layout, opts) {
566
571
  return materializeRasterMarkSVG(layout);
567
572
  }
568
573
 
574
+ // src/motion/easing.ts
575
+ function linear(t) {
576
+ return t;
577
+ }
578
+ function cubicOut(t) {
579
+ const f = 1 - t;
580
+ return 1 - f * f * f;
581
+ }
582
+ function cubicInOut(t) {
583
+ return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
584
+ }
585
+ var clamp01 = (t) => Math.min(1, Math.max(0, t));
586
+ var lerp = (a, b, t) => a + (b - a) * t;
587
+
569
588
  // src/text-edit-overlay.ts
570
589
  function getScale(svg) {
571
590
  const viewBox = svg.viewBox?.baseVal;
@@ -818,14 +837,121 @@ function createTextEditOverlayAtPosition(config) {
818
837
  return { destroy };
819
838
  }
820
839
 
821
- // src/transition.ts
840
+ // src/transition/keys.ts
841
+ function annotationKey(a) {
842
+ if (a.id) return `id:${a.id}`;
843
+ return `${a.type}:${a.label?.text ?? ""}`;
844
+ }
845
+ function keyAnnotations(annotations) {
846
+ const seen = /* @__PURE__ */ new Map();
847
+ return annotations.map((a) => {
848
+ const base = annotationKey(a);
849
+ const n = seen.get(base) ?? 0;
850
+ seen.set(base, n + 1);
851
+ return n === 0 ? base : `${base}#${n}`;
852
+ });
853
+ }
854
+
855
+ // src/transition/gate.ts
856
+ var TRANSITIONABLE_MARKS = /* @__PURE__ */ new Set(["bar", "line", "area", "point"]);
857
+ var DEFAULT_UPDATE_MAX_MARKS = 500;
858
+ var CANVAS_DEFAULT_UPDATE_MAX_MARKS = 2e4;
859
+ function canTransitionSpecShape(prevSpec, nextSpec) {
860
+ const prev = prevSpec;
861
+ const next = nextSpec;
862
+ if (!prev || !next || !("mark" in prev) || !("mark" in next)) return false;
863
+ const prevMark = typeof prev.mark === "string" ? prev.mark : prev.mark?.type;
864
+ const nextMark = typeof next.mark === "string" ? next.mark : next.mark?.type;
865
+ if (prevMark !== nextMark) return false;
866
+ if (!TRANSITIONABLE_MARKS.has(prevMark)) return false;
867
+ const prevEnc = prev.encoding;
868
+ const nextEnc = next.encoding;
869
+ if (!prevEnc || !nextEnc) return false;
870
+ 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.color?.field !== nextEnc.color?.field);
871
+ }
872
+ function canTransition(args) {
873
+ const { prevLayout, nextLayout, prevSpec, nextSpec, isFirstRender, entranceInFlight } = args;
874
+ if (!prevLayout || !prevSpec || isFirstRender) return false;
875
+ if (!nextLayout.animation?.update) return false;
876
+ if (!canTransitionSpecShape(prevSpec, nextSpec)) return false;
877
+ if (nextLayout.display === "sparkline") return false;
878
+ if (entranceInFlight) return false;
879
+ if (prevLayout.dimensions.width !== nextLayout.dimensions.width || prevLayout.dimensions.height !== nextLayout.dimensions.height) {
880
+ return false;
881
+ }
882
+ const cap = nextLayout.animation.update.maxMarks ?? (nextLayout.markRenderMode === "canvas" ? CANVAS_DEFAULT_UPDATE_MAX_MARKS : DEFAULT_UPDATE_MAX_MARKS);
883
+ if (Math.max(prevLayout.marks.length, nextLayout.marks.length) > cap) return false;
884
+ if (!hasVisibleChange(prevLayout, nextLayout)) return false;
885
+ if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
886
+ try {
887
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
888
+ } catch {
889
+ }
890
+ }
891
+ return true;
892
+ }
893
+ function hasVisibleChange(prevLayout, nextLayout) {
894
+ return hasGeometryChanged(prevLayout, nextLayout) || hasColorChanged(prevLayout, nextLayout) || hasAnnotationChanged(prevLayout, nextLayout);
895
+ }
896
+ function hasColorChanged(prevLayout, nextLayout) {
897
+ const prevMarks = prevLayout.marks;
898
+ const nextMarks = nextLayout.marks;
899
+ if (prevMarks.length !== nextMarks.length) return true;
900
+ for (let i = 0; i < prevMarks.length; i++) {
901
+ const p = prevMarks[i];
902
+ const n = nextMarks[i];
903
+ if (p.fill !== n.fill || p.stroke !== n.stroke) return true;
904
+ }
905
+ return false;
906
+ }
907
+ function hasAnnotationChanged(prevLayout, nextLayout) {
908
+ const prev = prevLayout.annotations ?? [];
909
+ const next = nextLayout.annotations ?? [];
910
+ if (prev.length !== next.length) return true;
911
+ const prevKeys = keyAnnotations(prev);
912
+ const nextKeys = keyAnnotations(next);
913
+ for (let i = 0; i < prev.length; i++) {
914
+ if (prevKeys[i] !== nextKeys[i]) return true;
915
+ const p = prev[i];
916
+ const n = next[i];
917
+ if (p.label?.x !== n.label?.x || p.label?.y !== n.label?.y) return true;
918
+ if (p.opacity !== n.opacity) return true;
919
+ }
920
+ return false;
921
+ }
922
+ function hasGeometryChanged(prevLayout, nextLayout) {
923
+ const prevMarks = prevLayout.marks;
924
+ const nextMarks = nextLayout.marks;
925
+ if (prevMarks.length !== nextMarks.length) return true;
926
+ for (let i = 0; i < prevMarks.length; i++) {
927
+ const p = prevMarks[i];
928
+ const n = nextMarks[i];
929
+ if (p.type !== n.type || p.key !== n.key) return true;
930
+ if (p.type === "rect" && n.type === "rect") {
931
+ if (p.x !== n.x || p.y !== n.y || p.width !== n.width || p.height !== n.height) return true;
932
+ } else if (p.type === "line" && n.type === "line") {
933
+ if (p.path !== n.path || p.points.length !== n.points.length) return true;
934
+ for (let j = 0; j < p.points.length; j++) {
935
+ if (p.points[j].x !== n.points[j].x || p.points[j].y !== n.points[j].y) return true;
936
+ }
937
+ } else if (p.type === "area" && n.type === "area") {
938
+ if (p.path !== n.path) return true;
939
+ } else if (p.type === "point" && n.type === "point") {
940
+ if (p.cx !== n.cx || p.cy !== n.cy || p.r !== n.r) return true;
941
+ } else if (p.type === "rule" && n.type === "rule") {
942
+ if (p.x1 !== n.x1 || p.y1 !== n.y1 || p.x2 !== n.x2 || p.y2 !== n.y2) return true;
943
+ } else if (p.type === "tick" && n.type === "tick") {
944
+ if (p.x !== n.x || p.y !== n.y) return true;
945
+ } else if (p.type === "textMark" && n.type === "textMark") {
946
+ if (p.x !== n.x || p.y !== n.y) return true;
947
+ }
948
+ }
949
+ return false;
950
+ }
951
+
952
+ // src/transition/driver.ts
822
953
  import { isGradientDef } from "@opendata-ai/openchart-core";
823
- import {
824
- buildAreaPath,
825
- buildLinePath,
826
- EXIT_DEFAULTS,
827
- serializeKeyValue
828
- } from "@opendata-ai/openchart-engine";
954
+ import { buildAreaPath as buildAreaPath2, buildLinePath as buildLinePath2, EXIT_DEFAULTS } from "@opendata-ai/openchart-engine";
829
955
 
830
956
  // ../../node_modules/.bun/d3-color@3.1.0/node_modules/d3-color/src/define.js
831
957
  function define_default(constructor, factory, prototype) {
@@ -1201,7 +1327,7 @@ function basisClosed_default(values) {
1201
1327
  var constant_default = (x) => () => x;
1202
1328
 
1203
1329
  // ../../node_modules/.bun/d3-interpolate@3.0.1/node_modules/d3-interpolate/src/color.js
1204
- function linear(a, d) {
1330
+ function linear2(a, d) {
1205
1331
  return function(t) {
1206
1332
  return a + t * d;
1207
1333
  };
@@ -1218,7 +1344,7 @@ function gamma(y) {
1218
1344
  }
1219
1345
  function nogamma(a, b) {
1220
1346
  var d = b - a;
1221
- return d ? linear(a, d) : constant_default(isNaN(a) ? b : a);
1347
+ return d ? linear2(a, d) : constant_default(isNaN(a) ? b : a);
1222
1348
  }
1223
1349
 
1224
1350
  // ../../node_modules/.bun/d3-interpolate@3.0.1/node_modules/d3-interpolate/src/rgb.js
@@ -1306,694 +1432,723 @@ var zoom_default = (function zoomRho(rho, rho2, rho4) {
1306
1432
  return zoom;
1307
1433
  })(Math.SQRT2, 2, 4);
1308
1434
 
1309
- // src/transition.ts
1310
- function annotationKey(a) {
1311
- if (a.id) return `id:${a.id}`;
1312
- return `${a.type}:${a.label?.text ?? ""}`;
1313
- }
1314
- function keyAnnotations(annotations) {
1315
- const seen = /* @__PURE__ */ new Map();
1316
- return annotations.map((a) => {
1317
- const base = annotationKey(a);
1318
- const n = seen.get(base) ?? 0;
1319
- seen.set(base, n + 1);
1320
- return n === 0 ? base : `${base}#${n}`;
1321
- });
1322
- }
1323
- function cubicOut(t) {
1324
- const f = 1 - t;
1325
- return 1 - f * f * f;
1435
+ // src/transition/canvas-tweens.ts
1436
+ import { serializeKeyValue as serializeKeyValue2 } from "@opendata-ai/openchart-engine";
1437
+
1438
+ // src/transition/chrome-tweens.ts
1439
+ import { serializeKeyValue } from "@opendata-ai/openchart-engine";
1440
+ function markShapeElement(el, attr) {
1441
+ if (el.tagName === "circle" || el.tagName === "rect" || el.tagName === "path") return el;
1442
+ if (attr === "stroke") {
1443
+ const areaTop = el.querySelector(".oc-area-top");
1444
+ if (areaTop) return areaTop;
1445
+ }
1446
+ return el.querySelector("rect, path, circle, line") ?? el;
1326
1447
  }
1327
- var TRANSITIONABLE_MARKS = /* @__PURE__ */ new Set(["bar", "line", "area", "point"]);
1328
- var DEFAULT_UPDATE_MAX_MARKS = 500;
1329
- var CANVAS_DEFAULT_UPDATE_MAX_MARKS = 2e4;
1330
- function canTransitionSpecShape(prevSpec, nextSpec) {
1331
- const prev = prevSpec;
1332
- const next = nextSpec;
1333
- if (!prev || !next || !("mark" in prev) || !("mark" in next)) return false;
1334
- const prevMark = typeof prev.mark === "string" ? prev.mark : prev.mark?.type;
1335
- const nextMark = typeof next.mark === "string" ? next.mark : next.mark?.type;
1336
- if (prevMark !== nextMark) return false;
1337
- if (!TRANSITIONABLE_MARKS.has(prevMark)) return false;
1338
- const prevEnc = prev.encoding;
1339
- const nextEnc = next.encoding;
1340
- if (!prevEnc || !nextEnc) return false;
1341
- 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.color?.field !== nextEnc.color?.field);
1448
+ function isInterpolableColor(v) {
1449
+ return typeof v === "string" && v !== "" && v !== "none" && !v.startsWith("url(");
1342
1450
  }
1343
- function canTransition(args) {
1344
- const { prevLayout, nextLayout, prevSpec, nextSpec, isFirstRender, entranceInFlight } = args;
1345
- if (!prevLayout || !prevSpec || isFirstRender) return false;
1346
- if (!nextLayout.animation?.update) return false;
1347
- if (!canTransitionSpecShape(prevSpec, nextSpec)) return false;
1348
- if (nextLayout.display === "sparkline") return false;
1349
- if (entranceInFlight) return false;
1350
- if (prevLayout.dimensions.width !== nextLayout.dimensions.width || prevLayout.dimensions.height !== nextLayout.dimensions.height) {
1351
- return false;
1451
+ function buildColorTweens(prevLayout, nextLayout, dom, tweens) {
1452
+ const prevByKey = /* @__PURE__ */ new Map();
1453
+ for (const m of prevLayout.marks) {
1454
+ if (m.key) prevByKey.set(m.key, m);
1352
1455
  }
1353
- const cap = nextLayout.animation.update.maxMarks ?? (nextLayout.markRenderMode === "canvas" ? CANVAS_DEFAULT_UPDATE_MAX_MARKS : DEFAULT_UPDATE_MAX_MARKS);
1354
- if (Math.max(prevLayout.marks.length, nextLayout.marks.length) > cap) return false;
1355
- if (!hasVisibleChange(prevLayout, nextLayout)) return false;
1356
- if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
1357
- try {
1358
- if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
1359
- } catch {
1456
+ for (const next of nextLayout.marks) {
1457
+ if (!next.key) continue;
1458
+ const prev = prevByKey.get(next.key);
1459
+ if (!prev) continue;
1460
+ const group = dom.any(next.key);
1461
+ if (!group) continue;
1462
+ for (const attr of ["fill", "stroke"]) {
1463
+ const from = prev[attr];
1464
+ const to = next[attr];
1465
+ if (from === to) continue;
1466
+ if (!isInterpolableColor(from) || !isInterpolableColor(to)) continue;
1467
+ tweens.push({
1468
+ tweenType: "color",
1469
+ kind: "update",
1470
+ el: markShapeElement(group, attr),
1471
+ attr,
1472
+ from,
1473
+ to
1474
+ });
1360
1475
  }
1361
1476
  }
1362
- return true;
1363
- }
1364
- function hasVisibleChange(prevLayout, nextLayout) {
1365
- return hasGeometryChanged(prevLayout, nextLayout) || hasColorChanged(prevLayout, nextLayout) || hasAnnotationChanged(prevLayout, nextLayout);
1366
- }
1367
- function hasColorChanged(prevLayout, nextLayout) {
1368
- const prevMarks = prevLayout.marks;
1369
- const nextMarks = nextLayout.marks;
1370
- if (prevMarks.length !== nextMarks.length) return true;
1371
- for (let i = 0; i < prevMarks.length; i++) {
1372
- const p = prevMarks[i];
1373
- const n = nextMarks[i];
1374
- if (p.fill !== n.fill || p.stroke !== n.stroke) return true;
1375
- }
1376
- return false;
1377
1477
  }
1378
- function hasAnnotationChanged(prevLayout, nextLayout) {
1478
+ function buildAnnotationTweens(svg, prevLayout, nextLayout, tweens) {
1379
1479
  const prev = prevLayout.annotations ?? [];
1380
1480
  const next = nextLayout.annotations ?? [];
1381
- if (prev.length !== next.length) return true;
1382
- const prevKeys = keyAnnotations(prev);
1481
+ if (prev.length === 0 && next.length === 0) return;
1482
+ const container = svg.querySelector(".oc-annotations");
1483
+ if (!container) return;
1484
+ const prevByKey = new Map(keyAnnotations(prev).map((k, i) => [k, prev[i]]));
1383
1485
  const nextKeys = keyAnnotations(next);
1384
- for (let i = 0; i < prev.length; i++) {
1385
- if (prevKeys[i] !== nextKeys[i]) return true;
1386
- const p = prev[i];
1387
- const n = next[i];
1388
- if (p.label?.x !== n.label?.x || p.label?.y !== n.label?.y) return true;
1389
- if (p.opacity !== n.opacity) return true;
1390
- }
1391
- return false;
1392
- }
1393
- function hasGeometryChanged(prevLayout, nextLayout) {
1394
- const prevMarks = prevLayout.marks;
1395
- const nextMarks = nextLayout.marks;
1396
- if (prevMarks.length !== nextMarks.length) return true;
1397
- for (let i = 0; i < prevMarks.length; i++) {
1398
- const p = prevMarks[i];
1399
- const n = nextMarks[i];
1400
- if (p.type !== n.type || p.key !== n.key) return true;
1401
- if (p.type === "rect" && n.type === "rect") {
1402
- if (p.x !== n.x || p.y !== n.y || p.width !== n.width || p.height !== n.height) return true;
1403
- } else if (p.type === "line" && n.type === "line") {
1404
- if (p.path !== n.path || p.points.length !== n.points.length) return true;
1405
- for (let j = 0; j < p.points.length; j++) {
1406
- if (p.points[j].x !== n.points[j].x || p.points[j].y !== n.points[j].y) return true;
1407
- }
1408
- } else if (p.type === "area" && n.type === "area") {
1409
- if (p.path !== n.path) return true;
1410
- } else if (p.type === "point" && n.type === "point") {
1411
- if (p.cx !== n.cx || p.cy !== n.cy || p.r !== n.r) return true;
1412
- } else if (p.type === "rule" && n.type === "rule") {
1413
- if (p.x1 !== n.x1 || p.y1 !== n.y1 || p.x2 !== n.x2 || p.y2 !== n.y2) return true;
1414
- } else if (p.type === "tick" && n.type === "tick") {
1415
- if (p.x !== n.x || p.y !== n.y) return true;
1416
- } else if (p.type === "textMark" && n.type === "textMark") {
1417
- if (p.x !== n.x || p.y !== n.y) return true;
1486
+ const els = container.querySelectorAll(".oc-annotation");
1487
+ for (let i = 0; i < next.length; i++) {
1488
+ const el = els[i];
1489
+ if (!el) continue;
1490
+ const before = prevByKey.get(nextKeys[i]);
1491
+ if (!before) {
1492
+ tweens.push({
1493
+ tweenType: "annotation",
1494
+ kind: "enter",
1495
+ el,
1496
+ fromDx: 0,
1497
+ fromDy: 0,
1498
+ fromOpacity: 0,
1499
+ toOpacity: 1
1500
+ });
1501
+ continue;
1418
1502
  }
1503
+ const dx = (before.label?.x ?? 0) - (next[i].label?.x ?? 0);
1504
+ const dy = (before.label?.y ?? 0) - (next[i].label?.y ?? 0);
1505
+ if (dx === 0 && dy === 0) continue;
1506
+ tweens.push({
1507
+ tweenType: "annotation",
1508
+ kind: "update",
1509
+ el,
1510
+ fromDx: dx,
1511
+ fromDy: dy
1512
+ });
1419
1513
  }
1420
- return false;
1421
- }
1422
- function geomFromMark(m) {
1423
- return { x: m.x, y: m.y, width: m.width, height: m.height };
1424
1514
  }
1425
- function applyGeomToElement(el, geom, mark) {
1426
- const shapeEl = el.querySelector("rect, path");
1427
- if (!shapeEl) return;
1428
- const sides = mark.cornerRadiusSides;
1429
- const partialCorners = !!sides && (!sides.tl || !sides.tr || !sides.br || !sides.bl) && !!mark.cornerRadius;
1430
- if (partialCorners && shapeEl.tagName === "path") {
1431
- const tempMark = { ...mark, ...geom };
1432
- shapeEl.setAttribute("d", rectPathWithCorners(tempMark, sides));
1433
- } else {
1434
- shapeEl.setAttribute("x", String(geom.x));
1435
- shapeEl.setAttribute("y", String(geom.y));
1436
- shapeEl.setAttribute("width", String(geom.width));
1437
- shapeEl.setAttribute("height", String(geom.height));
1438
- }
1515
+ function buildAxisTweens(svg, prevLayout, nextLayout, tweens, ghosts) {
1516
+ buildSingleAxisTweens(svg, prevLayout.axes.x, nextLayout.axes.x, "x", nextLayout, tweens, ghosts);
1517
+ buildSingleAxisTweens(svg, prevLayout.axes.y, nextLayout.axes.y, "y", nextLayout, tweens, ghosts);
1439
1518
  }
1440
- function lerpGeom(from, to, t) {
1441
- return {
1442
- x: from.x + (to.x - from.x) * t,
1443
- y: from.y + (to.y - from.y) * t,
1444
- width: from.width + (to.width - from.width) * t,
1445
- height: from.height + (to.height - from.height) * t
1519
+ function computeGridlineDeltas(prevAxis, nextAxis) {
1520
+ const byValue = (axis) => {
1521
+ const map = /* @__PURE__ */ new Map();
1522
+ for (const tick of axis.ticks) {
1523
+ const matching = axis.gridlines.find((g) => g.position === tick.position);
1524
+ if (matching) map.set(serializeKeyValue(tick.value), matching.position);
1525
+ }
1526
+ return map;
1446
1527
  };
1528
+ return { prevGridByValue: byValue(prevAxis), nextGridByValue: byValue(nextAxis) };
1447
1529
  }
1448
- function lerpPoint(a, b, t) {
1449
- return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
1450
- }
1451
- function normalizePointArrays(prevPoints, nextPoints, prevKeys, nextKeys) {
1452
- const nextKeySet = new Set(nextKeys);
1453
- const survivors = prevKeys.filter((k) => nextKeySet.has(k));
1454
- if (survivors.length === 0) {
1455
- return [prevPoints, nextPoints];
1530
+ function buildSingleAxisTweens(svg, prevAxis, nextAxis, orientation, layout, tweens, ghosts) {
1531
+ if (!prevAxis || !nextAxis) return;
1532
+ const axisClass = `oc-axis-${orientation}`;
1533
+ const axisGroup = svg.querySelector(`.${axisClass}`);
1534
+ if (!axisGroup) return;
1535
+ const isRotated = !!(nextAxis.tickAngle && Math.abs(nextAxis.tickAngle) > 10);
1536
+ const prevTickMap = /* @__PURE__ */ new Map();
1537
+ for (const tick of prevAxis.ticks) {
1538
+ prevTickMap.set(serializeKeyValue(tick.value), tick.position);
1456
1539
  }
1457
- const prevByKey = /* @__PURE__ */ new Map();
1458
- for (let i = 0; i < prevKeys.length; i++) {
1459
- prevByKey.set(prevKeys[i], prevPoints[i]);
1540
+ const nextTickMap = /* @__PURE__ */ new Map();
1541
+ for (const tick of nextAxis.ticks) {
1542
+ nextTickMap.set(serializeKeyValue(tick.value), tick.position);
1460
1543
  }
1461
- const nextByKey = /* @__PURE__ */ new Map();
1462
- for (let i = 0; i < nextKeys.length; i++) {
1463
- nextByKey.set(nextKeys[i], nextPoints[i]);
1544
+ const posAttr = orientation === "x" ? "x" : "y";
1545
+ const tickLabels = axisGroup.querySelectorAll(".oc-axis-tick[data-tick-key]");
1546
+ const matchedNextKeys = /* @__PURE__ */ new Set();
1547
+ for (const labelEl of tickLabels) {
1548
+ const key = labelEl.getAttribute("data-tick-key");
1549
+ if (!key) continue;
1550
+ const nextPos = nextTickMap.get(key);
1551
+ const prevPos = prevTickMap.get(key);
1552
+ if (prevPos !== void 0 && nextPos !== void 0) {
1553
+ matchedNextKeys.add(key);
1554
+ tweens.push({
1555
+ tweenType: "axisTick",
1556
+ kind: "update",
1557
+ el: labelEl,
1558
+ posAttr,
1559
+ fromPos: prevPos,
1560
+ toPos: nextPos,
1561
+ crossfade: isRotated
1562
+ });
1563
+ } else if (prevPos === void 0 && nextPos !== void 0) {
1564
+ matchedNextKeys.add(key);
1565
+ tweens.push({
1566
+ tweenType: "axisTick",
1567
+ kind: "enter",
1568
+ el: labelEl,
1569
+ posAttr,
1570
+ fromPos: nextPos,
1571
+ toPos: nextPos,
1572
+ crossfade: false,
1573
+ fromOpacity: 0,
1574
+ toOpacity: 1
1575
+ });
1576
+ }
1464
1577
  }
1465
- const merged = mergeKeyOrder(prevKeys, nextKeys);
1466
- const mergedIndex = /* @__PURE__ */ new Map();
1467
- for (let i = 0; i < merged.length; i++) {
1468
- mergedIndex.set(merged[i], i);
1578
+ for (const [key, prevPos] of prevTickMap) {
1579
+ if (nextTickMap.has(key)) continue;
1580
+ const prevTick = prevAxis.ticks.find((t) => serializeKeyValue(t.value) === key);
1581
+ if (!prevTick) continue;
1582
+ const ghost = document.createElementNS("http://www.w3.org/2000/svg", "text");
1583
+ ghost.setAttribute("class", "oc-axis-tick oc-ghost");
1584
+ ghost.setAttribute("aria-hidden", "true");
1585
+ ghost.setAttribute("pointer-events", "none");
1586
+ ghost.textContent = prevTick.label;
1587
+ const area = layout.area;
1588
+ if (orientation === "x") {
1589
+ ghost.setAttribute("x", String(prevPos));
1590
+ const xLabelPad = nextAxis.labelPadding ?? layout.theme.spacing.xAxisLabelPadding;
1591
+ const fontSize = nextAxis.tickLabelStyle.fontSize;
1592
+ ghost.setAttribute("y", String(area.y + area.height + xLabelPad + fontSize * 0.8));
1593
+ ghost.setAttribute("text-anchor", "middle");
1594
+ } else {
1595
+ const TICK_LABEL_OFFSET = 8;
1596
+ ghost.setAttribute("x", String(area.x - TICK_LABEL_OFFSET));
1597
+ ghost.setAttribute("y", String(prevPos));
1598
+ ghost.setAttribute("text-anchor", "end");
1599
+ ghost.setAttribute("dominant-baseline", "central");
1600
+ }
1601
+ axisGroup.appendChild(ghost);
1602
+ ghosts.push(ghost);
1603
+ tweens.push({
1604
+ tweenType: "axisTick",
1605
+ kind: "exit",
1606
+ el: ghost,
1607
+ posAttr,
1608
+ fromPos: prevPos,
1609
+ toPos: prevPos,
1610
+ crossfade: false,
1611
+ ghost,
1612
+ fromOpacity: 1,
1613
+ toOpacity: 0
1614
+ });
1469
1615
  }
1470
- const fromPts = [];
1471
- const toPts = [];
1472
- for (const key of merged) {
1473
- const inPrev = prevByKey.has(key);
1474
- const inNext = nextByKey.has(key);
1475
- if (inPrev && inNext) {
1476
- fromPts.push(prevByKey.get(key));
1477
- toPts.push(nextByKey.get(key));
1478
- } else if (inNext && !inPrev) {
1479
- const nextPt = nextByKey.get(key);
1480
- toPts.push(nextPt);
1481
- fromPts.push(findInsertionPosition(key, merged, mergedIndex, prevByKey, nextByKey, nextPt));
1482
- } else if (inPrev && !inNext) {
1483
- const prevPt = prevByKey.get(key);
1484
- fromPts.push(prevPt);
1485
- toPts.push(findRemovalPosition(key, merged, mergedIndex, prevByKey, nextByKey, prevPt));
1616
+ const { prevGridByValue, nextGridByValue } = computeGridlineDeltas(prevAxis, nextAxis);
1617
+ const gridlines = axisGroup.querySelectorAll(".oc-gridline[data-tick-key]");
1618
+ for (const glEl of gridlines) {
1619
+ const key = glEl.getAttribute("data-tick-key");
1620
+ if (!key) continue;
1621
+ const nextPos = nextGridByValue.get(key);
1622
+ const prevPos = prevGridByValue.get(key);
1623
+ if (prevPos !== void 0 && nextPos !== void 0) {
1624
+ tweens.push({
1625
+ tweenType: "gridline",
1626
+ kind: "update",
1627
+ el: glEl,
1628
+ orientation,
1629
+ fromPos: prevPos,
1630
+ toPos: nextPos
1631
+ });
1632
+ } else if (prevPos === void 0 && nextPos !== void 0) {
1633
+ tweens.push({
1634
+ tweenType: "gridline",
1635
+ kind: "enter",
1636
+ el: glEl,
1637
+ orientation,
1638
+ fromPos: nextPos,
1639
+ toPos: nextPos,
1640
+ fromOpacity: 0,
1641
+ toOpacity: 1
1642
+ });
1486
1643
  }
1487
1644
  }
1488
- return [fromPts, toPts];
1645
+ for (const [key, prevPos] of prevGridByValue) {
1646
+ if (nextGridByValue.has(key)) continue;
1647
+ const ghost = document.createElementNS("http://www.w3.org/2000/svg", "line");
1648
+ ghost.setAttribute("class", "oc-gridline oc-ghost");
1649
+ ghost.setAttribute("aria-hidden", "true");
1650
+ ghost.setAttribute("pointer-events", "none");
1651
+ if (orientation === "y") {
1652
+ const area = layout.area;
1653
+ ghost.setAttribute("x1", String(area.x));
1654
+ ghost.setAttribute("y1", String(prevPos));
1655
+ ghost.setAttribute("x2", String(area.x + area.width));
1656
+ ghost.setAttribute("y2", String(prevPos));
1657
+ } else {
1658
+ const area = layout.area;
1659
+ ghost.setAttribute("x1", String(prevPos));
1660
+ ghost.setAttribute("y1", String(area.y));
1661
+ ghost.setAttribute("x2", String(prevPos));
1662
+ ghost.setAttribute("y2", String(area.y + area.height));
1663
+ }
1664
+ axisGroup.appendChild(ghost);
1665
+ ghosts.push(ghost);
1666
+ tweens.push({
1667
+ tweenType: "gridline",
1668
+ kind: "exit",
1669
+ el: ghost,
1670
+ orientation,
1671
+ fromPos: prevPos,
1672
+ toPos: prevPos,
1673
+ ghost,
1674
+ fromOpacity: 1,
1675
+ toOpacity: 0
1676
+ });
1677
+ }
1489
1678
  }
1490
- function mergeKeyOrder(prevKeys, nextKeys) {
1491
- const nextIndex = /* @__PURE__ */ new Map();
1492
- for (let i = 0; i < nextKeys.length; i++) {
1493
- if (!nextIndex.has(nextKeys[i])) nextIndex.set(nextKeys[i], i);
1679
+
1680
+ // src/transition/canvas-tweens.ts
1681
+ function buildCanvasPointsTween(prevLayout, nextLayout, layer, tweens, fromSnapshot) {
1682
+ const prevPoints = prevLayout.marks.filter((m) => m.type === "point");
1683
+ const nextPoints = nextLayout.marks.filter((m) => m.type === "point");
1684
+ const prevByKey = /* @__PURE__ */ new Map();
1685
+ for (const m of prevPoints) {
1686
+ if (m.key) prevByKey.set(m.key, m);
1494
1687
  }
1495
- const prevIndex = /* @__PURE__ */ new Map();
1496
- for (let i = 0; i < prevKeys.length; i++) {
1497
- if (!prevIndex.has(prevKeys[i])) prevIndex.set(prevKeys[i], i);
1498
- }
1499
- const result = [];
1500
- const added = /* @__PURE__ */ new Set();
1501
- let pi = 0;
1502
- let ni = 0;
1503
- while (pi < prevKeys.length && ni < nextKeys.length) {
1504
- const pk = prevKeys[pi];
1505
- const nk = nextKeys[ni];
1506
- if (added.has(pk)) {
1507
- pi++;
1508
- continue;
1509
- }
1510
- if (added.has(nk)) {
1511
- ni++;
1512
- continue;
1513
- }
1514
- if (pk === nk) {
1515
- result.push(pk);
1516
- added.add(pk);
1517
- pi++;
1518
- ni++;
1519
- } else {
1520
- const pkInNext = nextIndex.has(pk) && nextIndex.get(pk) >= ni ? nextIndex.get(pk) : -1;
1521
- const nkInPrev = prevIndex.has(nk) && prevIndex.get(nk) >= pi ? prevIndex.get(nk) : -1;
1522
- if (pkInNext === -1 && nkInPrev === -1) {
1523
- result.push(pk);
1524
- added.add(pk);
1525
- pi++;
1526
- } else if (pkInNext === -1) {
1527
- result.push(pk);
1528
- added.add(pk);
1529
- pi++;
1530
- } else if (nkInPrev === -1) {
1531
- result.push(nk);
1532
- added.add(nk);
1533
- ni++;
1534
- } else {
1535
- if (pkInNext <= nkInPrev) {
1536
- result.push(pk);
1537
- added.add(pk);
1538
- pi++;
1539
- } else {
1540
- result.push(nk);
1541
- added.add(nk);
1542
- ni++;
1543
- }
1544
- }
1545
- }
1546
- }
1547
- while (pi < prevKeys.length) {
1548
- if (!added.has(prevKeys[pi])) {
1549
- result.push(prevKeys[pi]);
1550
- added.add(prevKeys[pi]);
1551
- }
1552
- pi++;
1688
+ const soa = layer.state.marks;
1689
+ const survivors = [];
1690
+ const enters = [];
1691
+ for (let i = 0; i < soa.n; i++) {
1692
+ const key = soa.keys[i];
1693
+ if (key !== void 0 && prevByKey.has(key)) survivors.push(i);
1694
+ else enters.push(i);
1553
1695
  }
1554
- while (ni < nextKeys.length) {
1555
- if (!added.has(nextKeys[ni])) {
1556
- result.push(nextKeys[ni]);
1557
- added.add(nextKeys[ni]);
1558
- }
1559
- ni++;
1696
+ const exits = [];
1697
+ const nextKeys = /* @__PURE__ */ new Set();
1698
+ for (const m of nextPoints) {
1699
+ if (m.key) nextKeys.add(m.key);
1560
1700
  }
1561
- return result;
1562
- }
1563
- function findInsertionPosition(key, merged, mergedIndex, prevByKey, nextByKey, nextPt) {
1564
- const idx = mergedIndex.get(key) ?? -1;
1565
- let beforeKey = null;
1566
- for (let i = idx - 1; i >= 0; i--) {
1567
- if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
1568
- beforeKey = merged[i];
1569
- break;
1570
- }
1701
+ for (const [key, prev] of prevByKey) {
1702
+ if (!nextKeys.has(key)) exits.push(prev);
1571
1703
  }
1572
- let afterKey = null;
1573
- for (let i = idx + 1; i < merged.length; i++) {
1574
- if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
1575
- afterKey = merged[i];
1576
- break;
1704
+ const sn = survivors.length;
1705
+ const soaIndex = new Uint32Array(sn);
1706
+ const fromCx = new Float32Array(sn);
1707
+ const fromCy = new Float32Array(sn);
1708
+ const fromR = new Float32Array(sn);
1709
+ const toCx = new Float32Array(sn);
1710
+ const toCy = new Float32Array(sn);
1711
+ const toR = new Float32Array(sn);
1712
+ const soaKeys = new Array(sn);
1713
+ for (let s = 0; s < sn; s++) {
1714
+ const i = survivors[s];
1715
+ const key = soa.keys[i];
1716
+ const prev = prevByKey.get(key);
1717
+ let px = prev.cx;
1718
+ let py = prev.cy;
1719
+ let pr = prev.r;
1720
+ const snap = fromSnapshot?.get(key);
1721
+ if (snap && snap.type === "point") {
1722
+ px = snap.cx;
1723
+ py = snap.cy;
1724
+ if (snap.r !== void 0) pr = snap.r;
1577
1725
  }
1726
+ soaIndex[s] = i;
1727
+ soaKeys[s] = key;
1728
+ fromCx[s] = px;
1729
+ fromCy[s] = py;
1730
+ fromR[s] = pr;
1731
+ toCx[s] = soa.x[i];
1732
+ toCy[s] = soa.y[i];
1733
+ toR[s] = soa.r[i];
1578
1734
  }
1579
- if (beforeKey && afterKey) {
1580
- const beforeNext = nextByKey.get(beforeKey);
1581
- const afterNext = nextByKey.get(afterKey);
1582
- const beforePrev = prevByKey.get(beforeKey);
1583
- const afterPrev = prevByKey.get(afterKey);
1584
- const range = afterNext.x - beforeNext.x;
1585
- const t = range === 0 ? 0.5 : (nextPt.x - beforeNext.x) / range;
1586
- return lerpPoint(beforePrev, afterPrev, Math.max(0, Math.min(1, t)));
1587
- }
1588
- if (beforeKey) {
1589
- return { ...prevByKey.get(beforeKey) };
1735
+ const enterSoaIndex = Uint32Array.from(enters);
1736
+ if (enters.length > 0) {
1737
+ const alpha = new Float32Array(soa.n).fill(1);
1738
+ for (const i of enters) alpha[i] = 0;
1739
+ layer.state.enterAlpha = alpha;
1590
1740
  }
1591
- if (afterKey) {
1592
- return { ...prevByKey.get(afterKey) };
1741
+ const exitCx = new Float32Array(exits.length);
1742
+ const exitCy = new Float32Array(exits.length);
1743
+ const exitR = new Float32Array(exits.length);
1744
+ const exitFill = new Array(exits.length);
1745
+ for (let x = 0; x < exits.length; x++) {
1746
+ const m = exits[x];
1747
+ exitCx[x] = m.cx;
1748
+ exitCy[x] = m.cy;
1749
+ exitR[x] = m.r;
1750
+ exitFill[x] = flattenFill(m.fill);
1593
1751
  }
1594
- return nextPt;
1752
+ const exitState = exits.length > 0 ? { x: exitCx, y: exitCy, r: exitR, fill: exitFill, alpha: 1 } : null;
1753
+ tweens.push({
1754
+ tweenType: "canvasPoints",
1755
+ kind: "update",
1756
+ layer,
1757
+ soaIndex,
1758
+ fromCx,
1759
+ fromCy,
1760
+ fromR,
1761
+ toCx,
1762
+ toCy,
1763
+ toR,
1764
+ soaKeys,
1765
+ enterSoaIndex,
1766
+ exitState
1767
+ });
1595
1768
  }
1596
- function findRemovalPosition(key, merged, mergedIndex, prevByKey, nextByKey, prevPt) {
1597
- const idx = mergedIndex.get(key) ?? -1;
1598
- let beforeKey = null;
1599
- for (let i = idx - 1; i >= 0; i--) {
1600
- if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
1601
- beforeKey = merged[i];
1602
- break;
1603
- }
1769
+ function applyCanvasPointsState(tw, elapsed, update, exit, enterDelay, enterDuration) {
1770
+ const soa = tw.layer.state.marks;
1771
+ const easedUpdate = cubicOut(Math.min(elapsed / update.duration, 1));
1772
+ for (let s = 0; s < tw.soaIndex.length; s++) {
1773
+ const i = tw.soaIndex[s];
1774
+ soa.x[i] = tw.fromCx[s] + (tw.toCx[s] - tw.fromCx[s]) * easedUpdate;
1775
+ soa.y[i] = tw.fromCy[s] + (tw.toCy[s] - tw.fromCy[s]) * easedUpdate;
1776
+ soa.r[i] = tw.fromR[s] + (tw.toR[s] - tw.fromR[s]) * easedUpdate;
1604
1777
  }
1605
- let afterKey = null;
1606
- for (let i = idx + 1; i < merged.length; i++) {
1607
- if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
1608
- afterKey = merged[i];
1609
- break;
1778
+ const alpha = tw.layer.state.enterAlpha;
1779
+ if (alpha && tw.enterSoaIndex.length > 0) {
1780
+ const tEnter = elapsed < enterDelay ? 0 : Math.min((elapsed - enterDelay) / enterDuration, 1);
1781
+ const easedEnter = cubicOut(tEnter);
1782
+ for (let e = 0; e < tw.enterSoaIndex.length; e++) {
1783
+ alpha[tw.enterSoaIndex[e]] = easedEnter;
1610
1784
  }
1611
1785
  }
1612
- if (beforeKey && afterKey) {
1613
- const beforePrev = prevByKey.get(beforeKey);
1614
- const afterPrev = prevByKey.get(afterKey);
1615
- const beforeNext = nextByKey.get(beforeKey);
1616
- const afterNext = nextByKey.get(afterKey);
1617
- const range = afterPrev.x - beforePrev.x;
1618
- const t = range === 0 ? 0.5 : (prevPt.x - beforePrev.x) / range;
1619
- return lerpPoint(beforeNext, afterNext, Math.max(0, Math.min(1, t)));
1620
- }
1621
- if (beforeKey) {
1622
- return { ...nextByKey.get(beforeKey) };
1623
- }
1624
- if (afterKey) {
1625
- return { ...nextByKey.get(afterKey) };
1626
- }
1627
- return prevPt;
1628
- }
1629
- function interpolatePoints(fromPts, toPts, t) {
1630
- const len = Math.min(fromPts.length, toPts.length);
1631
- const result = new Array(len);
1632
- for (let i = 0; i < len; i++) {
1633
- result[i] = lerpPoint(fromPts[i], toPts[i], t);
1634
- }
1635
- return result;
1636
- }
1637
- function applyLinePath(el, points, interpolate) {
1638
- const pathEl = el.querySelector("path");
1639
- if (!pathEl) return;
1640
- pathEl.setAttribute("d", buildLinePath(points, interpolate));
1641
- }
1642
- function applyAreaPaths(el, topPoints, bottomPoints, interpolate, hasStroke) {
1643
- const paths = el.querySelectorAll("path");
1644
- if (paths[0]) {
1645
- paths[0].setAttribute("d", buildAreaPath(topPoints, bottomPoints, interpolate));
1646
- }
1647
- if (hasStroke && paths[1]) {
1648
- paths[1].setAttribute("d", buildLinePath(topPoints, interpolate));
1786
+ if (tw.exitState) {
1787
+ const easedExit = cubicOut(Math.min(elapsed / exit.duration, 1));
1788
+ tw.exitState.alpha = 1 - easedExit;
1789
+ tw.layer.state.exiting = tw.exitState;
1649
1790
  }
1650
1791
  }
1651
- function applyFinalLinePath(el, mark) {
1652
- const pathEl = el.querySelector("path");
1653
- if (!pathEl || !mark.path) return;
1654
- pathEl.setAttribute("d", mark.path);
1792
+ function canvasGridlineAxes(layout) {
1793
+ return [
1794
+ { axis: layout.axes.y, orient: "y" },
1795
+ { axis: layout.axes.y2, orient: "y" },
1796
+ { axis: layout.axes.x, orient: "x" }
1797
+ ];
1655
1798
  }
1656
- function applyFinalAreaPaths(el, mark) {
1657
- const paths = el.querySelectorAll("path");
1658
- if (paths[0]) {
1659
- paths[0].setAttribute("d", mark.path);
1660
- }
1661
- if (paths[1] && mark.topPath) {
1662
- paths[1].setAttribute("d", mark.topPath);
1663
- }
1799
+ function axisEmitsGridlines(axis, orient) {
1800
+ if (!axis) return false;
1801
+ return !(orient === "y" && axis.orient === "right");
1664
1802
  }
1665
- function runTransition(args) {
1666
- const { svg, prevLayout, nextLayout, animation, onComplete, fromSnapshot, manual, canvas } = args;
1667
- const update = animation.update;
1668
- const exit = animation.exit ?? { ...EXIT_DEFAULTS };
1669
- const totalMs = Math.max(update.duration, exit.duration);
1670
- const enterDelay = 0.4 * update.duration;
1671
- const enterDuration = update.duration - enterDelay;
1672
- const marksContainer = svg.querySelector(".oc-marks");
1673
- if (!marksContainer) {
1674
- onComplete();
1675
- return {
1676
- cancel() {
1677
- },
1678
- get running() {
1679
- return false;
1680
- },
1681
- snapshot() {
1682
- return /* @__PURE__ */ new Map();
1683
- },
1684
- step() {
1685
- return false;
1686
- },
1687
- totalMs: 0
1688
- };
1689
- }
1690
- const tweens = [];
1691
- const ghosts = [];
1692
- const defs = svg.querySelector("defs");
1693
- let ghostGradientMap = /* @__PURE__ */ new Map();
1694
- if (defs) {
1695
- const nextKeySet = new Set(nextLayout.marks.filter((m) => m.key).map((m) => m.key));
1696
- const exitingMarks = prevLayout.marks.filter(
1697
- (m) => m.key && !nextKeySet.has(m.key) && "fill" in m
1698
- );
1699
- const gradientMarks = exitingMarks.filter(
1700
- (m) => "fill" in m && isGradientDef(m.fill)
1701
- );
1702
- if (gradientMarks.length > 0) {
1703
- ghostGradientMap = buildGradientDefs(gradientMarks, defs);
1803
+ function buildCanvasGridlinesTween(prevLayout, nextLayout, layer, tweens) {
1804
+ const live = layer.state.gridlines;
1805
+ const index = [];
1806
+ const fromPos = [];
1807
+ const toPos = [];
1808
+ const fromAlpha = [];
1809
+ const toAlpha = [];
1810
+ const prevAxes = canvasGridlineAxes(prevLayout);
1811
+ const nextAxes = canvasGridlineAxes(nextLayout);
1812
+ let cursor = 0;
1813
+ for (let a = 0; a < nextAxes.length; a++) {
1814
+ const nextAxis = nextAxes[a].axis;
1815
+ const orient = nextAxes[a].orient;
1816
+ if (!axisEmitsGridlines(nextAxis, orient)) continue;
1817
+ const prevAxis = prevAxes[a].axis;
1818
+ const deltas = axisEmitsGridlines(prevAxis, orient) ? computeGridlineDeltas(prevAxis, nextAxis) : null;
1819
+ const valueAtPosition = /* @__PURE__ */ new Map();
1820
+ for (const tick of nextAxis.ticks) {
1821
+ valueAtPosition.set(tick.position, serializeKeyValue2(tick.value));
1822
+ }
1823
+ for (const gridline of nextAxis.gridlines) {
1824
+ const slot = cursor++;
1825
+ if (slot >= live.length) break;
1826
+ const key = valueAtPosition.get(gridline.position);
1827
+ const prevPos = key !== void 0 ? deltas?.prevGridByValue.get(key) : void 0;
1828
+ index.push(slot);
1829
+ toPos.push(gridline.position);
1830
+ toAlpha.push(live[slot].alpha);
1831
+ if (prevPos !== void 0) {
1832
+ fromPos.push(prevPos);
1833
+ fromAlpha.push(live[slot].alpha);
1834
+ } else {
1835
+ fromPos.push(gridline.position);
1836
+ fromAlpha.push(0);
1837
+ }
1704
1838
  }
1705
1839
  }
1706
- const hasRects = prevLayout.marks.some((m) => m.type === "rect") || nextLayout.marks.some((m) => m.type === "rect");
1707
- const hasLines = prevLayout.marks.some((m) => m.type === "line") || nextLayout.marks.some((m) => m.type === "line");
1708
- const hasAreas = prevLayout.marks.some((m) => m.type === "area") || nextLayout.marks.some((m) => m.type === "area");
1709
- const hasPoints = prevLayout.marks.some((m) => m.type === "point") || nextLayout.marks.some((m) => m.type === "point");
1710
- const hasRules = prevLayout.marks.some((m) => m.type === "rule") || nextLayout.marks.some((m) => m.type === "rule");
1711
- const hasTicks = prevLayout.marks.some((m) => m.type === "tick") || nextLayout.marks.some((m) => m.type === "tick");
1712
- const hasTextMarks = prevLayout.marks.some((m) => m.type === "textMark") || nextLayout.marks.some((m) => m.type === "textMark");
1713
- if (hasRects) {
1714
- buildRectTweens(
1715
- prevLayout,
1716
- nextLayout,
1717
- marksContainer,
1718
- tweens,
1719
- ghosts,
1720
- ghostGradientMap,
1721
- fromSnapshot
1722
- );
1840
+ if (index.length === 0) return;
1841
+ tweens.push({
1842
+ tweenType: "canvasGridlines",
1843
+ kind: "update",
1844
+ layer,
1845
+ index: Uint32Array.from(index),
1846
+ fromPos: Float32Array.from(fromPos),
1847
+ toPos: Float32Array.from(toPos),
1848
+ fromAlpha: Float32Array.from(fromAlpha),
1849
+ toAlpha: Float32Array.from(toAlpha)
1850
+ });
1851
+ }
1852
+ function applyCanvasGridlinesState(tw, elapsed, update, enterDelay, enterDuration) {
1853
+ const live = tw.layer.state.gridlines;
1854
+ const easedUpdate = cubicOut(Math.min(elapsed / update.duration, 1));
1855
+ const tEnter = elapsed < enterDelay ? 0 : Math.min((elapsed - enterDelay) / enterDuration, 1);
1856
+ const easedEnter = cubicOut(tEnter);
1857
+ for (let i = 0; i < tw.index.length; i++) {
1858
+ const g = live[tw.index[i]];
1859
+ g.position = tw.fromPos[i] + (tw.toPos[i] - tw.fromPos[i]) * easedUpdate;
1860
+ const from = tw.fromAlpha[i];
1861
+ const to = tw.toAlpha[i];
1862
+ g.alpha = from === to ? to : from + (to - from) * easedEnter;
1723
1863
  }
1724
- if (hasLines) {
1725
- buildLineTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot);
1864
+ }
1865
+ function snapCanvasGridlinesToFinal(tw) {
1866
+ const live = tw.layer.state.gridlines;
1867
+ for (let i = 0; i < tw.index.length; i++) {
1868
+ const g = live[tw.index[i]];
1869
+ g.position = tw.toPos[i];
1870
+ g.alpha = tw.toAlpha[i];
1726
1871
  }
1727
- if (hasAreas) {
1728
- buildAreaTweens(
1729
- prevLayout,
1730
- nextLayout,
1731
- marksContainer,
1732
- tweens,
1733
- ghosts,
1734
- ghostGradientMap,
1735
- fromSnapshot
1736
- );
1872
+ }
1873
+ function snapCanvasPointsToFinal(tw) {
1874
+ const soa = tw.layer.state.marks;
1875
+ for (let s = 0; s < tw.soaIndex.length; s++) {
1876
+ const i = tw.soaIndex[s];
1877
+ soa.x[i] = tw.toCx[s];
1878
+ soa.y[i] = tw.toCy[s];
1879
+ soa.r[i] = tw.toR[s];
1737
1880
  }
1738
- if (hasPoints) {
1739
- if (canvas) {
1740
- buildCanvasPointsTween(prevLayout, nextLayout, canvas, tweens, fromSnapshot);
1741
- } else {
1742
- buildPointTweens(
1743
- prevLayout,
1744
- nextLayout,
1745
- marksContainer,
1746
- tweens,
1747
- ghosts,
1748
- ghostGradientMap,
1749
- fromSnapshot
1750
- );
1751
- }
1881
+ tw.layer.state.exiting = null;
1882
+ tw.layer.state.enterAlpha = null;
1883
+ }
1884
+
1885
+ // src/transition/interpolate.ts
1886
+ import { buildAreaPath, buildLinePath } from "@opendata-ai/openchart-engine";
1887
+ function geomFromMark(m) {
1888
+ return { x: m.x, y: m.y, width: m.width, height: m.height };
1889
+ }
1890
+ function applyGeomToElement(el, geom, mark) {
1891
+ const shapeEl = el.querySelector("rect, path");
1892
+ if (!shapeEl) return;
1893
+ const sides = mark.cornerRadiusSides;
1894
+ const partialCorners = !!sides && (!sides.tl || !sides.tr || !sides.br || !sides.bl) && !!mark.cornerRadius;
1895
+ if (partialCorners && shapeEl.tagName === "path") {
1896
+ const tempMark = { ...mark, ...geom };
1897
+ shapeEl.setAttribute("d", rectPathWithCorners(tempMark, sides));
1898
+ } else {
1899
+ shapeEl.setAttribute("x", String(geom.x));
1900
+ shapeEl.setAttribute("y", String(geom.y));
1901
+ shapeEl.setAttribute("width", String(geom.width));
1902
+ shapeEl.setAttribute("height", String(geom.height));
1752
1903
  }
1753
- if (hasRules) {
1754
- buildRuleTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot);
1904
+ }
1905
+ function lerpGeom(from, to, t) {
1906
+ return {
1907
+ x: from.x + (to.x - from.x) * t,
1908
+ y: from.y + (to.y - from.y) * t,
1909
+ width: from.width + (to.width - from.width) * t,
1910
+ height: from.height + (to.height - from.height) * t
1911
+ };
1912
+ }
1913
+ function lerpPoint(a, b, t) {
1914
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
1915
+ }
1916
+ function normalizePointArrays(prevPoints, nextPoints, prevKeys, nextKeys) {
1917
+ const nextKeySet = new Set(nextKeys);
1918
+ const survivors = prevKeys.filter((k) => nextKeySet.has(k));
1919
+ if (survivors.length === 0) {
1920
+ return [prevPoints, nextPoints];
1755
1921
  }
1756
- if (hasTicks) {
1757
- buildTickTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot);
1922
+ const prevByKey = /* @__PURE__ */ new Map();
1923
+ for (let i = 0; i < prevKeys.length; i++) {
1924
+ prevByKey.set(prevKeys[i], prevPoints[i]);
1758
1925
  }
1759
- if (hasTextMarks) {
1760
- buildTextMarkTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot);
1926
+ const nextByKey = /* @__PURE__ */ new Map();
1927
+ for (let i = 0; i < nextKeys.length; i++) {
1928
+ nextByKey.set(nextKeys[i], nextPoints[i]);
1761
1929
  }
1762
- buildAxisTweens(svg, prevLayout, nextLayout, tweens, ghosts);
1763
- if (canvas) {
1764
- buildCanvasGridlinesTween(prevLayout, nextLayout, canvas, tweens);
1930
+ const merged = mergeKeyOrder(prevKeys, nextKeys);
1931
+ const mergedIndex = /* @__PURE__ */ new Map();
1932
+ for (let i = 0; i < merged.length; i++) {
1933
+ mergedIndex.set(merged[i], i);
1765
1934
  }
1766
- buildColorTweens(prevLayout, nextLayout, marksContainer, tweens);
1767
- buildAnnotationTweens(svg, prevLayout, nextLayout, tweens);
1768
- const secondaryEls = collectSecondaryElements(svg);
1769
- const secondaryOriginalOpacity = secondaryEls.map((el) => el.style.opacity ?? "");
1770
- for (const tw of tweens) {
1771
- applyTweenState(tw, 0, update, exit, enterDelay, enterDuration);
1935
+ const fromPts = [];
1936
+ const toPts = [];
1937
+ for (const key of merged) {
1938
+ const inPrev = prevByKey.has(key);
1939
+ const inNext = nextByKey.has(key);
1940
+ if (inPrev && inNext) {
1941
+ fromPts.push(prevByKey.get(key));
1942
+ toPts.push(nextByKey.get(key));
1943
+ } else if (inNext && !inPrev) {
1944
+ const nextPt = nextByKey.get(key);
1945
+ toPts.push(nextPt);
1946
+ fromPts.push(findInsertionPosition(key, merged, mergedIndex, prevByKey, nextByKey, nextPt));
1947
+ } else if (inPrev && !inNext) {
1948
+ const prevPt = prevByKey.get(key);
1949
+ fromPts.push(prevPt);
1950
+ toPts.push(findRemovalPosition(key, merged, mergedIndex, prevByKey, nextByKey, prevPt));
1951
+ }
1772
1952
  }
1773
- canvas?.repaint();
1774
- canvas?.setInteractionSuspended(true);
1775
- for (const el of secondaryEls) {
1776
- el.style.opacity = "0";
1953
+ return [fromPts, toPts];
1954
+ }
1955
+ function mergeKeyOrder(prevKeys, nextKeys) {
1956
+ const nextIndex = /* @__PURE__ */ new Map();
1957
+ for (let i = 0; i < nextKeys.length; i++) {
1958
+ if (!nextIndex.has(nextKeys[i])) nextIndex.set(nextKeys[i], i);
1777
1959
  }
1778
- let rafId = null;
1779
- let running = true;
1780
- let startTime = null;
1781
- let lastElapsed = 0;
1782
- const nextLineMarks = nextLayout.marks.filter((m) => m.type === "line");
1783
- const nextAreaMarks = nextLayout.marks.filter((m) => m.type === "area");
1784
- function tick(now) {
1785
- if (!running) return;
1786
- if (startTime === null) startTime = now;
1787
- const elapsed = now - startTime;
1788
- lastElapsed = elapsed;
1789
- const tGlobal = Math.min(elapsed / totalMs, 1);
1790
- for (const tw of tweens) {
1791
- applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration);
1792
- }
1793
- applySecondaryOpacity(secondaryEls, elapsed, enterDelay, enterDuration);
1794
- canvas?.repaint();
1795
- if (tGlobal >= 1) {
1796
- finish();
1797
- } else {
1798
- rafId = requestAnimationFrame(tick);
1799
- }
1960
+ const prevIndex = /* @__PURE__ */ new Map();
1961
+ for (let i = 0; i < prevKeys.length; i++) {
1962
+ if (!prevIndex.has(prevKeys[i])) prevIndex.set(prevKeys[i], i);
1800
1963
  }
1801
- function settleCanvas() {
1802
- if (!canvas) return;
1803
- canvas.repaint();
1804
- canvas.rebuildIndex();
1805
- canvas.setInteractionSuspended(false);
1806
- }
1807
- function finish() {
1808
- if (!running) return;
1809
- running = false;
1810
- rafId = null;
1811
- snapToFinal();
1812
- removeGhosts();
1813
- settleCanvas();
1814
- for (let i = 0; i < secondaryEls.length; i++) {
1815
- secondaryEls[i].style.opacity = secondaryOriginalOpacity[i];
1816
- }
1817
- onComplete();
1818
- }
1819
- function snapToFinal() {
1820
- for (const tw of tweens) {
1821
- snapTweenToFinal(tw);
1964
+ const result = [];
1965
+ const added = /* @__PURE__ */ new Set();
1966
+ let pi = 0;
1967
+ let ni = 0;
1968
+ while (pi < prevKeys.length && ni < nextKeys.length) {
1969
+ const pk = prevKeys[pi];
1970
+ const nk = nextKeys[ni];
1971
+ if (added.has(pk)) {
1972
+ pi++;
1973
+ continue;
1822
1974
  }
1823
- for (const mark of nextLineMarks) {
1824
- if (!mark.key) continue;
1825
- const el = marksContainer.querySelector(`[data-key="${mark.key}"]`);
1826
- if (el) applyFinalLinePath(el, mark);
1975
+ if (added.has(nk)) {
1976
+ ni++;
1977
+ continue;
1827
1978
  }
1828
- for (const mark of nextAreaMarks) {
1829
- if (!mark.key) continue;
1830
- const el = marksContainer.querySelector(`[data-key="${mark.key}"]`);
1831
- if (el) applyFinalAreaPaths(el, mark);
1979
+ if (pk === nk) {
1980
+ result.push(pk);
1981
+ added.add(pk);
1982
+ pi++;
1983
+ ni++;
1984
+ } else {
1985
+ const pkInNext = nextIndex.has(pk) && nextIndex.get(pk) >= ni ? nextIndex.get(pk) : -1;
1986
+ const nkInPrev = prevIndex.has(nk) && prevIndex.get(nk) >= pi ? prevIndex.get(nk) : -1;
1987
+ if (pkInNext === -1 && nkInPrev === -1) {
1988
+ result.push(pk);
1989
+ added.add(pk);
1990
+ pi++;
1991
+ } else if (pkInNext === -1) {
1992
+ result.push(pk);
1993
+ added.add(pk);
1994
+ pi++;
1995
+ } else if (nkInPrev === -1) {
1996
+ result.push(nk);
1997
+ added.add(nk);
1998
+ ni++;
1999
+ } else {
2000
+ if (pkInNext <= nkInPrev) {
2001
+ result.push(pk);
2002
+ added.add(pk);
2003
+ pi++;
2004
+ } else {
2005
+ result.push(nk);
2006
+ added.add(nk);
2007
+ ni++;
2008
+ }
2009
+ }
1832
2010
  }
1833
2011
  }
1834
- function removeGhosts() {
1835
- for (const ghost of ghosts) {
1836
- ghost.parentNode?.removeChild(ghost);
2012
+ while (pi < prevKeys.length) {
2013
+ if (!added.has(prevKeys[pi])) {
2014
+ result.push(prevKeys[pi]);
2015
+ added.add(prevKeys[pi]);
1837
2016
  }
2017
+ pi++;
1838
2018
  }
1839
- function captureSnapshot() {
1840
- const snap = /* @__PURE__ */ new Map();
1841
- const tUpdate = Math.min(lastElapsed / update.duration, 1);
1842
- const easedUpdate = cubicOut(tUpdate);
1843
- for (const tw of tweens) {
1844
- if (tw.kind !== "update") continue;
1845
- if (tw.tweenType === "canvasPoints") {
1846
- for (let s = 0; s < tw.soaIndex.length; s++) {
1847
- snap.set(tw.soaKeys[s], {
1848
- type: "point",
1849
- cx: tw.fromCx[s] + (tw.toCx[s] - tw.fromCx[s]) * easedUpdate,
1850
- cy: tw.fromCy[s] + (tw.toCy[s] - tw.fromCy[s]) * easedUpdate,
1851
- r: tw.fromR[s] + (tw.toR[s] - tw.fromR[s]) * easedUpdate
1852
- });
1853
- }
1854
- continue;
1855
- }
1856
- const key = getKeyForTween(tw);
1857
- if (!key) continue;
1858
- switch (tw.tweenType) {
1859
- case "rect": {
1860
- const g = lerpGeom(tw.from, tw.to, easedUpdate);
1861
- snap.set(key, { type: "rect", ...g });
1862
- break;
1863
- }
1864
- case "point": {
1865
- const cx = tw.fromCx + (tw.toCx - tw.fromCx) * easedUpdate;
1866
- const cy = tw.fromCy + (tw.toCy - tw.fromCy) * easedUpdate;
1867
- const entry = { type: "point", cx, cy };
1868
- if (tw.fromR !== void 0 && tw.toR !== void 0) {
1869
- entry.r = tw.fromR + (tw.toR - tw.fromR) * easedUpdate;
1870
- }
1871
- snap.set(key, entry);
1872
- break;
1873
- }
1874
- case "rule":
1875
- case "tick": {
1876
- snap.set(key, {
1877
- type: tw.tweenType,
1878
- x1: tw.fromX1 + (tw.toX1 - tw.fromX1) * easedUpdate,
1879
- y1: tw.fromY1 + (tw.toY1 - tw.fromY1) * easedUpdate,
1880
- x2: tw.fromX2 + (tw.toX2 - tw.fromX2) * easedUpdate,
1881
- y2: tw.fromY2 + (tw.toY2 - tw.fromY2) * easedUpdate
1882
- });
1883
- break;
1884
- }
1885
- case "textMark": {
1886
- snap.set(key, {
1887
- type: "textMark",
1888
- x: tw.fromX + (tw.toX - tw.fromX) * easedUpdate,
1889
- y: tw.fromY + (tw.toY - tw.fromY) * easedUpdate
1890
- });
1891
- break;
1892
- }
1893
- case "line": {
1894
- const pts = interpolatePoints(tw.fromPts, tw.toPts, easedUpdate);
1895
- const d = buildLinePath(pts, tw.interpolate);
1896
- snap.set(key, { type: "line", d });
1897
- break;
1898
- }
1899
- case "area": {
1900
- const top = interpolatePoints(tw.fromTop, tw.toTop, easedUpdate);
1901
- const bottom = interpolatePoints(tw.fromBottom, tw.toBottom, easedUpdate);
1902
- const d = buildAreaPath(top, bottom, tw.interpolate);
1903
- let topD;
1904
- if (tw.hasStroke) {
1905
- topD = buildLinePath(top, tw.interpolate);
1906
- }
1907
- snap.set(key, { type: "area", d, topD });
1908
- break;
1909
- }
1910
- }
2019
+ while (ni < nextKeys.length) {
2020
+ if (!added.has(nextKeys[ni])) {
2021
+ result.push(nextKeys[ni]);
2022
+ added.add(nextKeys[ni]);
1911
2023
  }
1912
- return snap;
1913
- }
1914
- if (!manual) {
1915
- rafId = requestAnimationFrame(tick);
2024
+ ni++;
1916
2025
  }
1917
- return {
1918
- cancel() {
1919
- if (!running) return;
1920
- running = false;
1921
- if (rafId !== null) {
1922
- cancelAnimationFrame(rafId);
1923
- rafId = null;
1924
- }
1925
- snapToFinal();
1926
- removeGhosts();
1927
- settleCanvas();
1928
- for (let i = 0; i < secondaryEls.length; i++) {
1929
- secondaryEls[i].style.opacity = secondaryOriginalOpacity[i];
1930
- }
1931
- },
1932
- get running() {
1933
- return running;
1934
- },
1935
- snapshot() {
1936
- return captureSnapshot();
1937
- },
1938
- step(elapsedMs) {
1939
- lastElapsed = elapsedMs;
1940
- for (const tw of tweens) {
1941
- applyTweenState(tw, elapsedMs, update, exit, enterDelay, enterDuration);
1942
- }
1943
- applySecondaryOpacity(secondaryEls, elapsedMs, enterDelay, enterDuration);
1944
- if (elapsedMs >= totalMs) {
1945
- snapToFinal();
1946
- removeGhosts();
1947
- settleCanvas();
1948
- return false;
1949
- }
1950
- canvas?.repaint();
1951
- return true;
1952
- },
1953
- totalMs
1954
- };
1955
- }
1956
- function collectSecondaryElements(svg) {
1957
- const els = [];
1958
- const epLabels = svg.querySelector(".oc-endpoint-labels");
1959
- if (epLabels) els.push(epLabels);
1960
- const markLabels = svg.querySelector(".oc-mark-labels");
1961
- if (markLabels) els.push(markLabels);
1962
- return els;
2026
+ return result;
1963
2027
  }
1964
- function applySecondaryOpacity(els, elapsed, enterDelay, enterDuration) {
1965
- if (els.length === 0) return;
1966
- let t;
1967
- if (elapsed < enterDelay) {
1968
- t = 0;
1969
- } else {
1970
- t = Math.min((elapsed - enterDelay) / enterDuration, 1);
2028
+ function findInsertionPosition(key, merged, mergedIndex, prevByKey, nextByKey, nextPt) {
2029
+ const idx = mergedIndex.get(key) ?? -1;
2030
+ let beforeKey = null;
2031
+ for (let i = idx - 1; i >= 0; i--) {
2032
+ if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
2033
+ beforeKey = merged[i];
2034
+ break;
2035
+ }
1971
2036
  }
1972
- const opacity = String(cubicOut(t));
1973
- for (const el of els) {
1974
- el.style.opacity = opacity;
2037
+ let afterKey = null;
2038
+ for (let i = idx + 1; i < merged.length; i++) {
2039
+ if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
2040
+ afterKey = merged[i];
2041
+ break;
2042
+ }
1975
2043
  }
1976
- }
1977
- function getKeyForTween(tw) {
1978
- if (tw.tweenType === "canvasPoints" || tw.tweenType === "canvasGridlines") return null;
1979
- if ("ghost" in tw && tw.ghost) return null;
1980
- return tw.el.getAttribute("data-key");
1981
- }
1982
- function resolveLineElement(el) {
1983
- return el.tagName === "line" ? el : el.querySelector("line") ?? el;
1984
- }
1985
- function applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration) {
1986
- if (tw.tweenType === "canvasPoints") {
1987
- applyCanvasPointsState(tw, elapsed, update, exit, enterDelay, enterDuration);
1988
- return;
2044
+ if (beforeKey && afterKey) {
2045
+ const beforeNext = nextByKey.get(beforeKey);
2046
+ const afterNext = nextByKey.get(afterKey);
2047
+ const beforePrev = prevByKey.get(beforeKey);
2048
+ const afterPrev = prevByKey.get(afterKey);
2049
+ const range = afterNext.x - beforeNext.x;
2050
+ const t = range === 0 ? 0.5 : (nextPt.x - beforeNext.x) / range;
2051
+ return lerpPoint(beforePrev, afterPrev, Math.max(0, Math.min(1, t)));
1989
2052
  }
1990
- if (tw.tweenType === "canvasGridlines") {
1991
- applyCanvasGridlinesState(tw, elapsed, update, enterDelay, enterDuration);
1992
- return;
2053
+ if (beforeKey) {
2054
+ return { ...prevByKey.get(beforeKey) };
1993
2055
  }
1994
- let tLocal;
1995
- if (tw.kind === "exit") {
1996
- tLocal = Math.min(elapsed / exit.duration, 1);
2056
+ if (afterKey) {
2057
+ return { ...prevByKey.get(afterKey) };
2058
+ }
2059
+ return nextPt;
2060
+ }
2061
+ function findRemovalPosition(key, merged, mergedIndex, prevByKey, nextByKey, prevPt) {
2062
+ const idx = mergedIndex.get(key) ?? -1;
2063
+ let beforeKey = null;
2064
+ for (let i = idx - 1; i >= 0; i--) {
2065
+ if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
2066
+ beforeKey = merged[i];
2067
+ break;
2068
+ }
2069
+ }
2070
+ let afterKey = null;
2071
+ for (let i = idx + 1; i < merged.length; i++) {
2072
+ if (prevByKey.has(merged[i]) && nextByKey.has(merged[i])) {
2073
+ afterKey = merged[i];
2074
+ break;
2075
+ }
2076
+ }
2077
+ if (beforeKey && afterKey) {
2078
+ const beforePrev = prevByKey.get(beforeKey);
2079
+ const afterPrev = prevByKey.get(afterKey);
2080
+ const beforeNext = nextByKey.get(beforeKey);
2081
+ const afterNext = nextByKey.get(afterKey);
2082
+ const range = afterPrev.x - beforePrev.x;
2083
+ const t = range === 0 ? 0.5 : (prevPt.x - beforePrev.x) / range;
2084
+ return lerpPoint(beforeNext, afterNext, Math.max(0, Math.min(1, t)));
2085
+ }
2086
+ if (beforeKey) {
2087
+ return { ...nextByKey.get(beforeKey) };
2088
+ }
2089
+ if (afterKey) {
2090
+ return { ...nextByKey.get(afterKey) };
2091
+ }
2092
+ return prevPt;
2093
+ }
2094
+ function interpolatePoints(fromPts, toPts, t) {
2095
+ const len = Math.min(fromPts.length, toPts.length);
2096
+ const result = new Array(len);
2097
+ for (let i = 0; i < len; i++) {
2098
+ result[i] = lerpPoint(fromPts[i], toPts[i], t);
2099
+ }
2100
+ return result;
2101
+ }
2102
+ function applyLinePath(el, points, interpolate) {
2103
+ const pathEl = el.querySelector("path");
2104
+ if (!pathEl) return;
2105
+ pathEl.setAttribute("d", buildLinePath(points, interpolate));
2106
+ }
2107
+ function applyAreaPaths(el, topPoints, bottomPoints, interpolate, hasStroke) {
2108
+ const paths = el.querySelectorAll("path");
2109
+ if (paths[0]) {
2110
+ paths[0].setAttribute("d", buildAreaPath(topPoints, bottomPoints, interpolate));
2111
+ }
2112
+ if (hasStroke && paths[1]) {
2113
+ paths[1].setAttribute("d", buildLinePath(topPoints, interpolate));
2114
+ }
2115
+ }
2116
+ function applyFinalLinePath(el, mark) {
2117
+ const pathEl = el.querySelector("path");
2118
+ if (!pathEl || !mark.path) return;
2119
+ pathEl.setAttribute("d", mark.path);
2120
+ }
2121
+ function applyFinalAreaPaths(el, mark) {
2122
+ const paths = el.querySelectorAll("path");
2123
+ if (paths[0]) {
2124
+ paths[0].setAttribute("d", mark.path);
2125
+ }
2126
+ if (paths[1] && mark.topPath) {
2127
+ paths[1].setAttribute("d", mark.topPath);
2128
+ }
2129
+ }
2130
+
2131
+ // src/transition/apply.ts
2132
+ function getKeyForTween(tw) {
2133
+ if (tw.tweenType === "canvasPoints" || tw.tweenType === "canvasGridlines") return null;
2134
+ if ("ghost" in tw && tw.ghost) return null;
2135
+ return tw.el.getAttribute("data-key");
2136
+ }
2137
+ function resolveLineElement(el) {
2138
+ return el.tagName === "line" ? el : el.querySelector("line") ?? el;
2139
+ }
2140
+ function applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration) {
2141
+ if (tw.tweenType === "canvasPoints") {
2142
+ applyCanvasPointsState(tw, elapsed, update, exit, enterDelay, enterDuration);
2143
+ return;
2144
+ }
2145
+ if (tw.tweenType === "canvasGridlines") {
2146
+ applyCanvasGridlinesState(tw, elapsed, update, enterDelay, enterDuration);
2147
+ return;
2148
+ }
2149
+ let tLocal;
2150
+ if (tw.kind === "exit") {
2151
+ tLocal = Math.min(elapsed / exit.duration, 1);
1997
2152
  } else if (tw.kind === "enter") {
1998
2153
  if (elapsed < enterDelay) {
1999
2154
  tLocal = 0;
@@ -2160,12 +2315,41 @@ function snapTweenToFinal(tw) {
2160
2315
  tw.el.setAttribute("opacity", tw.suppressedOpacity);
2161
2316
  }
2162
2317
  }
2318
+
2319
+ // src/transition/dom-index.ts
2320
+ function buildMarkDomIndex(marksContainer) {
2321
+ const anyM = /* @__PURE__ */ new Map();
2322
+ const pointM = /* @__PURE__ */ new Map();
2323
+ const ruleM = /* @__PURE__ */ new Map();
2324
+ const tickM = /* @__PURE__ */ new Map();
2325
+ const textM = /* @__PURE__ */ new Map();
2326
+ for (const node of marksContainer.querySelectorAll("[data-key]")) {
2327
+ const el = node;
2328
+ const key = el.getAttribute("data-key");
2329
+ if (!key) continue;
2330
+ if (!anyM.has(key)) anyM.set(key, el);
2331
+ const cl = el.classList;
2332
+ if (el.tagName === "circle" && cl.contains("oc-mark-point")) {
2333
+ if (!pointM.has(key)) pointM.set(key, el);
2334
+ } else if (cl.contains("oc-mark-rule")) {
2335
+ if (!ruleM.has(key)) ruleM.set(key, el);
2336
+ } else if (cl.contains("oc-mark-tick")) {
2337
+ if (!tickM.has(key)) tickM.set(key, el);
2338
+ } else if (cl.contains("oc-mark-text")) {
2339
+ if (!textM.has(key)) textM.set(key, el);
2340
+ }
2341
+ }
2342
+ const g = (m) => (key) => m.get(key) ?? null;
2343
+ return { any: g(anyM), point: g(pointM), rule: g(ruleM), tick: g(tickM), text: g(textM) };
2344
+ }
2345
+
2346
+ // src/transition/svg-tweens.ts
2163
2347
  function resolveGhostGradientFill(mark, ghostGradientMap) {
2164
2348
  if (!mark.fill || typeof mark.fill === "string" || ghostGradientMap.size === 0) return mark;
2165
2349
  const resolved = resolveMarkFill(mark.fill, ghostGradientMap);
2166
2350
  return { ...mark, fill: resolved };
2167
2351
  }
2168
- function buildRectTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, ghostGradientMap, fromSnapshot) {
2352
+ function buildRectTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, ghostGradientMap, fromSnapshot) {
2169
2353
  const prevRects = prevLayout.marks.filter((m) => m.type === "rect");
2170
2354
  const nextRects = nextLayout.marks.filter((m) => m.type === "rect");
2171
2355
  const prevByKey = /* @__PURE__ */ new Map();
@@ -2179,7 +2363,7 @@ function buildRectTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2179
2363
  for (const [key, next] of nextByKey) {
2180
2364
  const prev = prevByKey.get(key);
2181
2365
  if (!prev) continue;
2182
- const el = marksContainer.querySelector(`[data-key="${key}"]`);
2366
+ const el = dom.any(key);
2183
2367
  if (!el) continue;
2184
2368
  let fromGeom = geomFromMark(prev);
2185
2369
  const snap = fromSnapshot?.get(key);
@@ -2197,7 +2381,7 @@ function buildRectTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2197
2381
  }
2198
2382
  for (const [key, next] of nextByKey) {
2199
2383
  if (prevByKey.has(key)) continue;
2200
- const el = marksContainer.querySelector(`[data-key="${key}"]`);
2384
+ const el = dom.any(key);
2201
2385
  if (!el) continue;
2202
2386
  let fromGeom;
2203
2387
  if (next.orient === "horizontal") {
@@ -2244,7 +2428,7 @@ function buildRectTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2244
2428
  });
2245
2429
  }
2246
2430
  }
2247
- function buildLineTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot) {
2431
+ function buildLineTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, fromSnapshot) {
2248
2432
  const prevLines = prevLayout.marks.filter((m) => m.type === "line");
2249
2433
  const nextLines = nextLayout.marks.filter((m) => m.type === "line");
2250
2434
  const prevByKey = /* @__PURE__ */ new Map();
@@ -2258,7 +2442,7 @@ function buildLineTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2258
2442
  for (const [key, next] of nextByKey) {
2259
2443
  const prev = prevByKey.get(key);
2260
2444
  if (!prev) continue;
2261
- const el = marksContainer.querySelector(`[data-key="${key}"]`);
2445
+ const el = dom.any(key);
2262
2446
  if (!el) continue;
2263
2447
  const snap = fromSnapshot?.get(key);
2264
2448
  if (snap && snap.type === "line") {
@@ -2349,7 +2533,7 @@ function buildLineTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2349
2533
  }
2350
2534
  for (const [key, next] of nextByKey) {
2351
2535
  if (prevByKey.has(key)) continue;
2352
- const el = marksContainer.querySelector(`[data-key="${key}"]`);
2536
+ const el = dom.any(key);
2353
2537
  if (!el) continue;
2354
2538
  tweens.push({
2355
2539
  tweenType: "line",
@@ -2387,7 +2571,7 @@ function buildLineTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2387
2571
  });
2388
2572
  }
2389
2573
  }
2390
- function buildAreaTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, ghostGradientMap, fromSnapshot) {
2574
+ function buildAreaTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, ghostGradientMap, fromSnapshot) {
2391
2575
  const prevAreas = prevLayout.marks.filter((m) => m.type === "area");
2392
2576
  const nextAreas = nextLayout.marks.filter((m) => m.type === "area");
2393
2577
  const prevByKey = /* @__PURE__ */ new Map();
@@ -2401,7 +2585,7 @@ function buildAreaTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2401
2585
  for (const [key, next] of nextByKey) {
2402
2586
  const prev = prevByKey.get(key);
2403
2587
  if (!prev) continue;
2404
- const el = marksContainer.querySelector(`[data-key="${key}"]`);
2588
+ const el = dom.any(key);
2405
2589
  if (!el) continue;
2406
2590
  const snap = fromSnapshot?.get(key);
2407
2591
  if (snap && snap.type === "area") {
@@ -2523,7 +2707,7 @@ function buildAreaTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2523
2707
  }
2524
2708
  for (const [key, next] of nextByKey) {
2525
2709
  if (prevByKey.has(key)) continue;
2526
- const el = marksContainer.querySelector(`[data-key="${key}"]`);
2710
+ const el = dom.any(key);
2527
2711
  if (!el) continue;
2528
2712
  tweens.push({
2529
2713
  tweenType: "area",
@@ -2570,7 +2754,7 @@ function buildAreaTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2570
2754
  });
2571
2755
  }
2572
2756
  }
2573
- function buildPointTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, ghostGradientMap, fromSnapshot) {
2757
+ function buildPointTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, ghostGradientMap, fromSnapshot) {
2574
2758
  const prevPoints = prevLayout.marks.filter((m) => m.type === "point");
2575
2759
  const nextPoints = nextLayout.marks.filter((m) => m.type === "point");
2576
2760
  const prevByKey = /* @__PURE__ */ new Map();
@@ -2584,9 +2768,7 @@ function buildPointTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts
2584
2768
  for (const [key, next] of nextByKey) {
2585
2769
  const prev = prevByKey.get(key);
2586
2770
  if (!prev) continue;
2587
- const el = marksContainer.querySelector(
2588
- `circle.oc-mark-point[data-key="${key}"]`
2589
- );
2771
+ const el = dom.point(key);
2590
2772
  if (!el) continue;
2591
2773
  const renderedOpacity = el.getAttribute("opacity");
2592
2774
  const isSuppressed = renderedOpacity === "0";
@@ -2617,9 +2799,7 @@ function buildPointTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts
2617
2799
  }
2618
2800
  for (const [key, next] of nextByKey) {
2619
2801
  if (prevByKey.has(key)) continue;
2620
- const el = marksContainer.querySelector(
2621
- `circle.oc-mark-point[data-key="${key}"]`
2622
- );
2802
+ const el = dom.point(key);
2623
2803
  if (!el) continue;
2624
2804
  const renderedOpacity = el.getAttribute("opacity");
2625
2805
  const targetOpacity = renderedOpacity != null ? Number(renderedOpacity) : 1;
@@ -2661,310 +2841,96 @@ function buildPointTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts
2661
2841
  });
2662
2842
  }
2663
2843
  }
2664
- function buildCanvasPointsTween(prevLayout, nextLayout, layer, tweens, fromSnapshot) {
2665
- const prevPoints = prevLayout.marks.filter((m) => m.type === "point");
2666
- const nextPoints = nextLayout.marks.filter((m) => m.type === "point");
2844
+ function buildRuleTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, fromSnapshot) {
2845
+ const prevRules = prevLayout.marks.filter((m) => m.type === "rule");
2846
+ const nextRules = nextLayout.marks.filter((m) => m.type === "rule");
2667
2847
  const prevByKey = /* @__PURE__ */ new Map();
2668
- for (const m of prevPoints) {
2848
+ for (const m of prevRules) {
2669
2849
  if (m.key) prevByKey.set(m.key, m);
2670
2850
  }
2671
- const soa = layer.state.marks;
2672
- const survivors = [];
2673
- const enters = [];
2674
- for (let i = 0; i < soa.n; i++) {
2675
- const key = soa.keys[i];
2676
- if (key !== void 0 && prevByKey.has(key)) survivors.push(i);
2677
- else enters.push(i);
2678
- }
2679
- const exits = [];
2680
- const nextKeys = /* @__PURE__ */ new Set();
2681
- for (const m of nextPoints) {
2682
- if (m.key) nextKeys.add(m.key);
2683
- }
2684
- for (const [key, prev] of prevByKey) {
2685
- if (!nextKeys.has(key)) exits.push(prev);
2851
+ const nextByKey = /* @__PURE__ */ new Map();
2852
+ for (const m of nextRules) {
2853
+ if (m.key) nextByKey.set(m.key, m);
2686
2854
  }
2687
- const sn = survivors.length;
2688
- const soaIndex = new Uint32Array(sn);
2689
- const fromCx = new Float32Array(sn);
2690
- const fromCy = new Float32Array(sn);
2691
- const fromR = new Float32Array(sn);
2692
- const toCx = new Float32Array(sn);
2693
- const toCy = new Float32Array(sn);
2694
- const toR = new Float32Array(sn);
2695
- const soaKeys = new Array(sn);
2696
- for (let s = 0; s < sn; s++) {
2697
- const i = survivors[s];
2698
- const key = soa.keys[i];
2855
+ for (const [key, next] of nextByKey) {
2699
2856
  const prev = prevByKey.get(key);
2700
- let px = prev.cx;
2701
- let py = prev.cy;
2702
- let pr = prev.r;
2857
+ if (!prev) continue;
2858
+ const el = dom.rule(key);
2859
+ if (!el) continue;
2860
+ let fromX1 = prev.x1;
2861
+ let fromY1 = prev.y1;
2862
+ let fromX2 = prev.x2;
2863
+ let fromY2 = prev.y2;
2703
2864
  const snap = fromSnapshot?.get(key);
2704
- if (snap && snap.type === "point") {
2705
- px = snap.cx;
2706
- py = snap.cy;
2707
- if (snap.r !== void 0) pr = snap.r;
2865
+ if (snap && snap.type === "rule") {
2866
+ fromX1 = snap.x1;
2867
+ fromY1 = snap.y1;
2868
+ fromX2 = snap.x2;
2869
+ fromY2 = snap.y2;
2708
2870
  }
2709
- soaIndex[s] = i;
2710
- soaKeys[s] = key;
2711
- fromCx[s] = px;
2712
- fromCy[s] = py;
2713
- fromR[s] = pr;
2714
- toCx[s] = soa.x[i];
2715
- toCy[s] = soa.y[i];
2716
- toR[s] = soa.r[i];
2871
+ tweens.push({
2872
+ tweenType: "rule",
2873
+ kind: "update",
2874
+ el,
2875
+ fromX1,
2876
+ fromY1,
2877
+ fromX2,
2878
+ fromY2,
2879
+ toX1: next.x1,
2880
+ toY1: next.y1,
2881
+ toX2: next.x2,
2882
+ toY2: next.y2
2883
+ });
2717
2884
  }
2718
- const enterSoaIndex = Uint32Array.from(enters);
2719
- if (enters.length > 0) {
2720
- const alpha = new Float32Array(soa.n).fill(1);
2721
- for (const i of enters) alpha[i] = 0;
2722
- layer.state.enterAlpha = alpha;
2885
+ for (const [key, next] of nextByKey) {
2886
+ if (prevByKey.has(key)) continue;
2887
+ const el = dom.rule(key);
2888
+ if (!el) continue;
2889
+ tweens.push({
2890
+ tweenType: "rule",
2891
+ kind: "enter",
2892
+ el,
2893
+ fromX1: next.x1,
2894
+ fromY1: next.y1,
2895
+ fromX2: next.x2,
2896
+ fromY2: next.y2,
2897
+ toX1: next.x1,
2898
+ toY1: next.y1,
2899
+ toX2: next.x2,
2900
+ toY2: next.y2,
2901
+ fromOpacity: 0,
2902
+ toOpacity: 1
2903
+ });
2723
2904
  }
2724
- const exitCx = new Float32Array(exits.length);
2725
- const exitCy = new Float32Array(exits.length);
2726
- const exitR = new Float32Array(exits.length);
2727
- const exitFill = new Array(exits.length);
2728
- for (let x = 0; x < exits.length; x++) {
2729
- const m = exits[x];
2730
- exitCx[x] = m.cx;
2731
- exitCy[x] = m.cy;
2732
- exitR[x] = m.r;
2733
- exitFill[x] = flattenFill(m.fill);
2905
+ for (const [key, prev] of prevByKey) {
2906
+ if (nextByKey.has(key)) continue;
2907
+ const ghost = renderSingleMark(prev, 0);
2908
+ if (!ghost) continue;
2909
+ ghost.classList.add("oc-ghost");
2910
+ ghost.setAttribute("aria-hidden", "true");
2911
+ ghost.setAttribute("pointer-events", "none");
2912
+ ghost.removeAttribute("data-key");
2913
+ marksContainer.appendChild(ghost);
2914
+ ghosts.push(ghost);
2915
+ tweens.push({
2916
+ tweenType: "rule",
2917
+ kind: "exit",
2918
+ el: ghost,
2919
+ fromX1: prev.x1,
2920
+ fromY1: prev.y1,
2921
+ fromX2: prev.x2,
2922
+ fromY2: prev.y2,
2923
+ toX1: prev.x1,
2924
+ toY1: prev.y1,
2925
+ toX2: prev.x2,
2926
+ toY2: prev.y2,
2927
+ ghost,
2928
+ fromOpacity: 1,
2929
+ toOpacity: 0
2930
+ });
2734
2931
  }
2735
- tweens.push({
2736
- tweenType: "canvasPoints",
2737
- kind: "update",
2738
- layer,
2739
- soaIndex,
2740
- fromCx,
2741
- fromCy,
2742
- fromR,
2743
- toCx,
2744
- toCy,
2745
- toR,
2746
- soaKeys,
2747
- enterSoaIndex,
2748
- exitCx,
2749
- exitCy,
2750
- exitR,
2751
- exitFill
2752
- });
2753
2932
  }
2754
- function applyCanvasPointsState(tw, elapsed, update, exit, enterDelay, enterDuration) {
2755
- const soa = tw.layer.state.marks;
2756
- const easedUpdate = cubicOut(Math.min(elapsed / update.duration, 1));
2757
- for (let s = 0; s < tw.soaIndex.length; s++) {
2758
- const i = tw.soaIndex[s];
2759
- soa.x[i] = tw.fromCx[s] + (tw.toCx[s] - tw.fromCx[s]) * easedUpdate;
2760
- soa.y[i] = tw.fromCy[s] + (tw.toCy[s] - tw.fromCy[s]) * easedUpdate;
2761
- soa.r[i] = tw.fromR[s] + (tw.toR[s] - tw.fromR[s]) * easedUpdate;
2762
- }
2763
- const alpha = tw.layer.state.enterAlpha;
2764
- if (alpha && tw.enterSoaIndex.length > 0) {
2765
- const tEnter = elapsed < enterDelay ? 0 : Math.min((elapsed - enterDelay) / enterDuration, 1);
2766
- const easedEnter = cubicOut(tEnter);
2767
- for (let e = 0; e < tw.enterSoaIndex.length; e++) {
2768
- alpha[tw.enterSoaIndex[e]] = easedEnter;
2769
- }
2770
- }
2771
- if (tw.exitCx.length > 0) {
2772
- const easedExit = cubicOut(Math.min(elapsed / exit.duration, 1));
2773
- tw.layer.state.exiting = {
2774
- x: tw.exitCx,
2775
- y: tw.exitCy,
2776
- r: tw.exitR,
2777
- fill: tw.exitFill,
2778
- alpha: 1 - easedExit
2779
- };
2780
- }
2781
- }
2782
- function canvasGridlineAxes(layout) {
2783
- return [
2784
- { axis: layout.axes.y, orient: "y" },
2785
- { axis: layout.axes.y2, orient: "y" },
2786
- { axis: layout.axes.x, orient: "x" }
2787
- ];
2788
- }
2789
- function axisEmitsGridlines(axis, orient) {
2790
- if (!axis) return false;
2791
- return !(orient === "y" && axis.orient === "right");
2792
- }
2793
- function buildCanvasGridlinesTween(prevLayout, nextLayout, layer, tweens) {
2794
- const live = layer.state.gridlines;
2795
- const index = [];
2796
- const fromPos = [];
2797
- const toPos = [];
2798
- const fromAlpha = [];
2799
- const toAlpha = [];
2800
- const prevAxes = canvasGridlineAxes(prevLayout);
2801
- const nextAxes = canvasGridlineAxes(nextLayout);
2802
- let cursor = 0;
2803
- for (let a = 0; a < nextAxes.length; a++) {
2804
- const nextAxis = nextAxes[a].axis;
2805
- const orient = nextAxes[a].orient;
2806
- if (!axisEmitsGridlines(nextAxis, orient)) continue;
2807
- const prevAxis = prevAxes[a].axis;
2808
- const deltas = axisEmitsGridlines(prevAxis, orient) ? computeGridlineDeltas(prevAxis, nextAxis) : null;
2809
- const valueAtPosition = /* @__PURE__ */ new Map();
2810
- for (const tick of nextAxis.ticks) {
2811
- valueAtPosition.set(tick.position, serializeKeyValue(tick.value));
2812
- }
2813
- for (const gridline of nextAxis.gridlines) {
2814
- const slot = cursor++;
2815
- if (slot >= live.length) break;
2816
- const key = valueAtPosition.get(gridline.position);
2817
- const prevPos = key !== void 0 ? deltas?.prevGridByValue.get(key) : void 0;
2818
- index.push(slot);
2819
- toPos.push(gridline.position);
2820
- toAlpha.push(live[slot].alpha);
2821
- if (prevPos !== void 0) {
2822
- fromPos.push(prevPos);
2823
- fromAlpha.push(live[slot].alpha);
2824
- } else {
2825
- fromPos.push(gridline.position);
2826
- fromAlpha.push(0);
2827
- }
2828
- }
2829
- }
2830
- if (index.length === 0) return;
2831
- tweens.push({
2832
- tweenType: "canvasGridlines",
2833
- kind: "update",
2834
- layer,
2835
- index: Uint32Array.from(index),
2836
- fromPos: Float32Array.from(fromPos),
2837
- toPos: Float32Array.from(toPos),
2838
- fromAlpha: Float32Array.from(fromAlpha),
2839
- toAlpha: Float32Array.from(toAlpha)
2840
- });
2841
- }
2842
- function applyCanvasGridlinesState(tw, elapsed, update, enterDelay, enterDuration) {
2843
- const live = tw.layer.state.gridlines;
2844
- const easedUpdate = cubicOut(Math.min(elapsed / update.duration, 1));
2845
- const tEnter = elapsed < enterDelay ? 0 : Math.min((elapsed - enterDelay) / enterDuration, 1);
2846
- const easedEnter = cubicOut(tEnter);
2847
- for (let i = 0; i < tw.index.length; i++) {
2848
- const g = live[tw.index[i]];
2849
- g.position = tw.fromPos[i] + (tw.toPos[i] - tw.fromPos[i]) * easedUpdate;
2850
- const from = tw.fromAlpha[i];
2851
- const to = tw.toAlpha[i];
2852
- g.alpha = from === to ? to : from + (to - from) * easedEnter;
2853
- }
2854
- }
2855
- function snapCanvasGridlinesToFinal(tw) {
2856
- const live = tw.layer.state.gridlines;
2857
- for (let i = 0; i < tw.index.length; i++) {
2858
- const g = live[tw.index[i]];
2859
- g.position = tw.toPos[i];
2860
- g.alpha = tw.toAlpha[i];
2861
- }
2862
- }
2863
- function snapCanvasPointsToFinal(tw) {
2864
- const soa = tw.layer.state.marks;
2865
- for (let s = 0; s < tw.soaIndex.length; s++) {
2866
- const i = tw.soaIndex[s];
2867
- soa.x[i] = tw.toCx[s];
2868
- soa.y[i] = tw.toCy[s];
2869
- soa.r[i] = tw.toR[s];
2870
- }
2871
- tw.layer.state.exiting = null;
2872
- tw.layer.state.enterAlpha = null;
2873
- }
2874
- function buildRuleTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot) {
2875
- const prevRules = prevLayout.marks.filter((m) => m.type === "rule");
2876
- const nextRules = nextLayout.marks.filter((m) => m.type === "rule");
2877
- const prevByKey = /* @__PURE__ */ new Map();
2878
- for (const m of prevRules) {
2879
- if (m.key) prevByKey.set(m.key, m);
2880
- }
2881
- const nextByKey = /* @__PURE__ */ new Map();
2882
- for (const m of nextRules) {
2883
- if (m.key) nextByKey.set(m.key, m);
2884
- }
2885
- for (const [key, next] of nextByKey) {
2886
- const prev = prevByKey.get(key);
2887
- if (!prev) continue;
2888
- const el = marksContainer.querySelector(
2889
- `.oc-mark-rule[data-key="${key}"]`
2890
- );
2891
- if (!el) continue;
2892
- let fromX1 = prev.x1;
2893
- let fromY1 = prev.y1;
2894
- let fromX2 = prev.x2;
2895
- let fromY2 = prev.y2;
2896
- const snap = fromSnapshot?.get(key);
2897
- if (snap && snap.type === "rule") {
2898
- fromX1 = snap.x1;
2899
- fromY1 = snap.y1;
2900
- fromX2 = snap.x2;
2901
- fromY2 = snap.y2;
2902
- }
2903
- tweens.push({
2904
- tweenType: "rule",
2905
- kind: "update",
2906
- el,
2907
- fromX1,
2908
- fromY1,
2909
- fromX2,
2910
- fromY2,
2911
- toX1: next.x1,
2912
- toY1: next.y1,
2913
- toX2: next.x2,
2914
- toY2: next.y2
2915
- });
2916
- }
2917
- for (const [key, next] of nextByKey) {
2918
- if (prevByKey.has(key)) continue;
2919
- const el = marksContainer.querySelector(
2920
- `.oc-mark-rule[data-key="${key}"]`
2921
- );
2922
- if (!el) continue;
2923
- tweens.push({
2924
- tweenType: "rule",
2925
- kind: "enter",
2926
- el,
2927
- fromX1: next.x1,
2928
- fromY1: next.y1,
2929
- fromX2: next.x2,
2930
- fromY2: next.y2,
2931
- toX1: next.x1,
2932
- toY1: next.y1,
2933
- toX2: next.x2,
2934
- toY2: next.y2,
2935
- fromOpacity: 0,
2936
- toOpacity: 1
2937
- });
2938
- }
2939
- for (const [key, prev] of prevByKey) {
2940
- if (nextByKey.has(key)) continue;
2941
- const ghost = renderSingleMark(prev, 0);
2942
- if (!ghost) continue;
2943
- ghost.classList.add("oc-ghost");
2944
- ghost.setAttribute("aria-hidden", "true");
2945
- ghost.setAttribute("pointer-events", "none");
2946
- ghost.removeAttribute("data-key");
2947
- marksContainer.appendChild(ghost);
2948
- ghosts.push(ghost);
2949
- tweens.push({
2950
- tweenType: "rule",
2951
- kind: "exit",
2952
- el: ghost,
2953
- fromX1: prev.x1,
2954
- fromY1: prev.y1,
2955
- fromX2: prev.x2,
2956
- fromY2: prev.y2,
2957
- toX1: prev.x1,
2958
- toY1: prev.y1,
2959
- toX2: prev.x2,
2960
- toY2: prev.y2,
2961
- ghost,
2962
- fromOpacity: 1,
2963
- toOpacity: 0
2964
- });
2965
- }
2966
- }
2967
- function buildTickTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot) {
2933
+ function buildTickTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, fromSnapshot) {
2968
2934
  const prevTicks = prevLayout.marks.filter((m) => m.type === "tick");
2969
2935
  const nextTicks = nextLayout.marks.filter((m) => m.type === "tick");
2970
2936
  const prevByKey = /* @__PURE__ */ new Map();
@@ -2985,9 +2951,7 @@ function buildTickTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
2985
2951
  for (const [key, next] of nextByKey) {
2986
2952
  const prev = prevByKey.get(key);
2987
2953
  if (!prev) continue;
2988
- const el = marksContainer.querySelector(
2989
- `.oc-mark-tick[data-key="${key}"]`
2990
- );
2954
+ const el = dom.tick(key);
2991
2955
  if (!el) continue;
2992
2956
  let pEnd = tickEndpoints(prev);
2993
2957
  const nEnd = tickEndpoints(next);
@@ -3011,9 +2975,7 @@ function buildTickTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
3011
2975
  }
3012
2976
  for (const [key, next] of nextByKey) {
3013
2977
  if (prevByKey.has(key)) continue;
3014
- const el = marksContainer.querySelector(
3015
- `.oc-mark-tick[data-key="${key}"]`
3016
- );
2978
+ const el = dom.tick(key);
3017
2979
  if (!el) continue;
3018
2980
  const nEnd = tickEndpoints(next);
3019
2981
  tweens.push({
@@ -3061,7 +3023,7 @@ function buildTickTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts,
3061
3023
  });
3062
3024
  }
3063
3025
  }
3064
- function buildTextMarkTweens(prevLayout, nextLayout, marksContainer, tweens, ghosts, fromSnapshot) {
3026
+ function buildTextMarkTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, fromSnapshot) {
3065
3027
  const prevTexts = prevLayout.marks.filter((m) => m.type === "textMark");
3066
3028
  const nextTexts = nextLayout.marks.filter((m) => m.type === "textMark");
3067
3029
  const prevByKey = /* @__PURE__ */ new Map();
@@ -3075,9 +3037,7 @@ function buildTextMarkTweens(prevLayout, nextLayout, marksContainer, tweens, gho
3075
3037
  for (const [key, next] of nextByKey) {
3076
3038
  const prev = prevByKey.get(key);
3077
3039
  if (!prev) continue;
3078
- const el = marksContainer.querySelector(
3079
- `.oc-mark-text[data-key="${key}"]`
3080
- );
3040
+ const el = dom.text(key);
3081
3041
  if (!el) continue;
3082
3042
  let fromX = prev.x;
3083
3043
  let fromY = prev.y;
@@ -3098,9 +3058,7 @@ function buildTextMarkTweens(prevLayout, nextLayout, marksContainer, tweens, gho
3098
3058
  }
3099
3059
  for (const [key, next] of nextByKey) {
3100
3060
  if (prevByKey.has(key)) continue;
3101
- const el = marksContainer.querySelector(
3102
- `.oc-mark-text[data-key="${key}"]`
3103
- );
3061
+ const el = dom.text(key);
3104
3062
  if (!el) continue;
3105
3063
  tweens.push({
3106
3064
  tweenType: "textMark",
@@ -3138,243 +3096,322 @@ function buildTextMarkTweens(prevLayout, nextLayout, marksContainer, tweens, gho
3138
3096
  });
3139
3097
  }
3140
3098
  }
3141
- function markShapeElement(el, attr) {
3142
- if (el.tagName === "circle" || el.tagName === "rect" || el.tagName === "path") return el;
3143
- if (attr === "stroke") {
3144
- const areaTop = el.querySelector(".oc-area-top");
3145
- if (areaTop) return areaTop;
3146
- }
3147
- return el.querySelector("rect, path, circle, line") ?? el;
3148
- }
3149
- function isInterpolableColor(v) {
3150
- return typeof v === "string" && v !== "" && v !== "none" && !v.startsWith("url(");
3151
- }
3152
- function buildColorTweens(prevLayout, nextLayout, marksContainer, tweens) {
3153
- const prevByKey = /* @__PURE__ */ new Map();
3154
- for (const m of prevLayout.marks) {
3155
- if (m.key) prevByKey.set(m.key, m);
3099
+
3100
+ // src/transition/driver.ts
3101
+ function runTransition(args) {
3102
+ const { svg, prevLayout, nextLayout, animation, onComplete, fromSnapshot, manual, canvas } = args;
3103
+ const update = animation.update;
3104
+ const exit = animation.exit ?? { ...EXIT_DEFAULTS };
3105
+ const totalMs = Math.max(update.duration, exit.duration);
3106
+ const enterDelay = 0.4 * update.duration;
3107
+ const enterDuration = update.duration - enterDelay;
3108
+ const marksContainer = svg.querySelector(".oc-marks");
3109
+ if (!marksContainer) {
3110
+ onComplete();
3111
+ return {
3112
+ cancel() {
3113
+ },
3114
+ get running() {
3115
+ return false;
3116
+ },
3117
+ snapshot() {
3118
+ return /* @__PURE__ */ new Map();
3119
+ },
3120
+ step() {
3121
+ return false;
3122
+ },
3123
+ totalMs: 0
3124
+ };
3156
3125
  }
3157
- for (const next of nextLayout.marks) {
3158
- if (!next.key) continue;
3159
- const prev = prevByKey.get(next.key);
3160
- if (!prev) continue;
3161
- const group = marksContainer.querySelector(`[data-key="${next.key}"]`);
3162
- if (!group) continue;
3163
- for (const attr of ["fill", "stroke"]) {
3164
- const from = prev[attr];
3165
- const to = next[attr];
3166
- if (from === to) continue;
3167
- if (!isInterpolableColor(from) || !isInterpolableColor(to)) continue;
3168
- tweens.push({
3169
- tweenType: "color",
3170
- kind: "update",
3171
- el: markShapeElement(group, attr),
3172
- attr,
3173
- from,
3174
- to
3175
- });
3126
+ const dom = buildMarkDomIndex(marksContainer);
3127
+ const tweens = [];
3128
+ const ghosts = [];
3129
+ const defs = svg.querySelector("defs");
3130
+ let ghostGradientMap = /* @__PURE__ */ new Map();
3131
+ if (defs) {
3132
+ const nextKeySet = new Set(nextLayout.marks.filter((m) => m.key).map((m) => m.key));
3133
+ const exitingMarks = prevLayout.marks.filter(
3134
+ (m) => m.key && !nextKeySet.has(m.key) && "fill" in m
3135
+ );
3136
+ const gradientMarks = exitingMarks.filter(
3137
+ (m) => "fill" in m && isGradientDef(m.fill)
3138
+ );
3139
+ if (gradientMarks.length > 0) {
3140
+ ghostGradientMap = buildGradientDefs(gradientMarks, defs);
3176
3141
  }
3177
3142
  }
3178
- }
3179
- function buildAnnotationTweens(svg, prevLayout, nextLayout, tweens) {
3180
- const prev = prevLayout.annotations ?? [];
3181
- const next = nextLayout.annotations ?? [];
3182
- if (prev.length === 0 && next.length === 0) return;
3183
- const container = svg.querySelector(".oc-annotations");
3184
- if (!container) return;
3185
- const prevByKey = new Map(keyAnnotations(prev).map((k, i) => [k, prev[i]]));
3186
- const nextKeys = keyAnnotations(next);
3187
- const els = container.querySelectorAll(".oc-annotation");
3188
- for (let i = 0; i < next.length; i++) {
3189
- const el = els[i];
3190
- if (!el) continue;
3191
- const before = prevByKey.get(nextKeys[i]);
3192
- if (!before) {
3193
- tweens.push({
3194
- tweenType: "annotation",
3195
- kind: "enter",
3196
- el,
3197
- fromDx: 0,
3198
- fromDy: 0,
3199
- fromOpacity: 0,
3200
- toOpacity: 1
3201
- });
3202
- continue;
3203
- }
3204
- const dx = (before.label?.x ?? 0) - (next[i].label?.x ?? 0);
3205
- const dy = (before.label?.y ?? 0) - (next[i].label?.y ?? 0);
3206
- if (dx === 0 && dy === 0) continue;
3207
- tweens.push({
3208
- tweenType: "annotation",
3209
- kind: "update",
3210
- el,
3211
- fromDx: dx,
3212
- fromDy: dy
3213
- });
3143
+ const hasRects = prevLayout.marks.some((m) => m.type === "rect") || nextLayout.marks.some((m) => m.type === "rect");
3144
+ const hasLines = prevLayout.marks.some((m) => m.type === "line") || nextLayout.marks.some((m) => m.type === "line");
3145
+ const hasAreas = prevLayout.marks.some((m) => m.type === "area") || nextLayout.marks.some((m) => m.type === "area");
3146
+ const hasPoints = prevLayout.marks.some((m) => m.type === "point") || nextLayout.marks.some((m) => m.type === "point");
3147
+ const hasRules = prevLayout.marks.some((m) => m.type === "rule") || nextLayout.marks.some((m) => m.type === "rule");
3148
+ const hasTicks = prevLayout.marks.some((m) => m.type === "tick") || nextLayout.marks.some((m) => m.type === "tick");
3149
+ const hasTextMarks = prevLayout.marks.some((m) => m.type === "textMark") || nextLayout.marks.some((m) => m.type === "textMark");
3150
+ if (hasRects) {
3151
+ buildRectTweens(
3152
+ prevLayout,
3153
+ nextLayout,
3154
+ marksContainer,
3155
+ dom,
3156
+ tweens,
3157
+ ghosts,
3158
+ ghostGradientMap,
3159
+ fromSnapshot
3160
+ );
3214
3161
  }
3215
- }
3216
- function buildAxisTweens(svg, prevLayout, nextLayout, tweens, ghosts) {
3217
- buildSingleAxisTweens(svg, prevLayout.axes.x, nextLayout.axes.x, "x", nextLayout, tweens, ghosts);
3218
- buildSingleAxisTweens(svg, prevLayout.axes.y, nextLayout.axes.y, "y", nextLayout, tweens, ghosts);
3219
- }
3220
- function computeGridlineDeltas(prevAxis, nextAxis) {
3221
- const byValue = (axis) => {
3222
- const map = /* @__PURE__ */ new Map();
3223
- for (const tick of axis.ticks) {
3224
- const matching = axis.gridlines.find((g) => g.position === tick.position);
3225
- if (matching) map.set(serializeKeyValue(tick.value), matching.position);
3226
- }
3227
- return map;
3228
- };
3229
- return { prevGridByValue: byValue(prevAxis), nextGridByValue: byValue(nextAxis) };
3230
- }
3231
- function buildSingleAxisTweens(svg, prevAxis, nextAxis, orientation, layout, tweens, ghosts) {
3232
- if (!prevAxis || !nextAxis) return;
3233
- const axisClass = `oc-axis-${orientation}`;
3234
- const axisGroup = svg.querySelector(`.${axisClass}`);
3235
- if (!axisGroup) return;
3236
- const isRotated = !!(nextAxis.tickAngle && Math.abs(nextAxis.tickAngle) > 10);
3237
- const prevTickMap = /* @__PURE__ */ new Map();
3238
- for (const tick of prevAxis.ticks) {
3239
- prevTickMap.set(serializeKeyValue(tick.value), tick.position);
3162
+ if (hasLines) {
3163
+ buildLineTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, fromSnapshot);
3240
3164
  }
3241
- const nextTickMap = /* @__PURE__ */ new Map();
3242
- for (const tick of nextAxis.ticks) {
3243
- nextTickMap.set(serializeKeyValue(tick.value), tick.position);
3165
+ if (hasAreas) {
3166
+ buildAreaTweens(
3167
+ prevLayout,
3168
+ nextLayout,
3169
+ marksContainer,
3170
+ dom,
3171
+ tweens,
3172
+ ghosts,
3173
+ ghostGradientMap,
3174
+ fromSnapshot
3175
+ );
3244
3176
  }
3245
- const posAttr = orientation === "x" ? "x" : "y";
3246
- const tickLabels = axisGroup.querySelectorAll(".oc-axis-tick[data-tick-key]");
3247
- const matchedNextKeys = /* @__PURE__ */ new Set();
3248
- for (const labelEl of tickLabels) {
3249
- const key = labelEl.getAttribute("data-tick-key");
3250
- if (!key) continue;
3251
- const nextPos = nextTickMap.get(key);
3252
- const prevPos = prevTickMap.get(key);
3253
- if (prevPos !== void 0 && nextPos !== void 0) {
3254
- matchedNextKeys.add(key);
3255
- tweens.push({
3256
- tweenType: "axisTick",
3257
- kind: "update",
3258
- el: labelEl,
3259
- posAttr,
3260
- fromPos: prevPos,
3261
- toPos: nextPos,
3262
- crossfade: isRotated
3263
- });
3264
- } else if (prevPos === void 0 && nextPos !== void 0) {
3265
- matchedNextKeys.add(key);
3266
- tweens.push({
3267
- tweenType: "axisTick",
3268
- kind: "enter",
3269
- el: labelEl,
3270
- posAttr,
3271
- fromPos: nextPos,
3272
- toPos: nextPos,
3273
- crossfade: false,
3274
- fromOpacity: 0,
3275
- toOpacity: 1
3276
- });
3177
+ if (hasPoints) {
3178
+ if (canvas) {
3179
+ buildCanvasPointsTween(prevLayout, nextLayout, canvas, tweens, fromSnapshot);
3180
+ } else {
3181
+ buildPointTweens(
3182
+ prevLayout,
3183
+ nextLayout,
3184
+ marksContainer,
3185
+ dom,
3186
+ tweens,
3187
+ ghosts,
3188
+ ghostGradientMap,
3189
+ fromSnapshot
3190
+ );
3277
3191
  }
3278
3192
  }
3279
- for (const [key, prevPos] of prevTickMap) {
3280
- if (nextTickMap.has(key)) continue;
3281
- const prevTick = prevAxis.ticks.find((t) => serializeKeyValue(t.value) === key);
3282
- if (!prevTick) continue;
3283
- const ghost = document.createElementNS("http://www.w3.org/2000/svg", "text");
3284
- ghost.setAttribute("class", "oc-axis-tick oc-ghost");
3285
- ghost.setAttribute("aria-hidden", "true");
3286
- ghost.setAttribute("pointer-events", "none");
3287
- ghost.textContent = prevTick.label;
3288
- const area = layout.area;
3289
- if (orientation === "x") {
3290
- ghost.setAttribute("x", String(prevPos));
3291
- const xLabelPad = nextAxis.labelPadding ?? layout.theme.spacing.xAxisLabelPadding;
3292
- const fontSize = nextAxis.tickLabelStyle.fontSize;
3293
- ghost.setAttribute("y", String(area.y + area.height + xLabelPad + fontSize * 0.8));
3294
- ghost.setAttribute("text-anchor", "middle");
3193
+ if (hasRules) {
3194
+ buildRuleTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, fromSnapshot);
3195
+ }
3196
+ if (hasTicks) {
3197
+ buildTickTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, fromSnapshot);
3198
+ }
3199
+ if (hasTextMarks) {
3200
+ buildTextMarkTweens(prevLayout, nextLayout, marksContainer, dom, tweens, ghosts, fromSnapshot);
3201
+ }
3202
+ buildAxisTweens(svg, prevLayout, nextLayout, tweens, ghosts);
3203
+ if (canvas) {
3204
+ buildCanvasGridlinesTween(prevLayout, nextLayout, canvas, tweens);
3205
+ }
3206
+ buildColorTweens(prevLayout, nextLayout, dom, tweens);
3207
+ buildAnnotationTweens(svg, prevLayout, nextLayout, tweens);
3208
+ const secondaryEls = collectSecondaryElements(svg);
3209
+ const secondaryOriginalOpacity = secondaryEls.map((el) => el.style.opacity ?? "");
3210
+ for (const tw of tweens) {
3211
+ applyTweenState(tw, 0, update, exit, enterDelay, enterDuration);
3212
+ }
3213
+ canvas?.repaint();
3214
+ canvas?.setInteractionSuspended(true);
3215
+ for (const el of secondaryEls) {
3216
+ el.style.opacity = "0";
3217
+ }
3218
+ let rafId = null;
3219
+ let running = true;
3220
+ let startTime = null;
3221
+ let lastElapsed = 0;
3222
+ const nextLineMarks = nextLayout.marks.filter((m) => m.type === "line");
3223
+ const nextAreaMarks = nextLayout.marks.filter((m) => m.type === "area");
3224
+ function tick(now) {
3225
+ if (!running) return;
3226
+ if (startTime === null) startTime = now;
3227
+ const elapsed = now - startTime;
3228
+ lastElapsed = elapsed;
3229
+ const tGlobal = Math.min(elapsed / totalMs, 1);
3230
+ for (const tw of tweens) {
3231
+ applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration);
3232
+ }
3233
+ applySecondaryOpacity(secondaryEls, elapsed, enterDelay, enterDuration);
3234
+ canvas?.repaint();
3235
+ if (tGlobal >= 1) {
3236
+ finish();
3295
3237
  } else {
3296
- const TICK_LABEL_OFFSET = 8;
3297
- ghost.setAttribute("x", String(area.x - TICK_LABEL_OFFSET));
3298
- ghost.setAttribute("y", String(prevPos));
3299
- ghost.setAttribute("text-anchor", "end");
3300
- ghost.setAttribute("dominant-baseline", "central");
3238
+ rafId = requestAnimationFrame(tick);
3239
+ }
3240
+ }
3241
+ function settleCanvas() {
3242
+ if (!canvas) return;
3243
+ canvas.repaint();
3244
+ canvas.rebuildIndex();
3245
+ canvas.setInteractionSuspended(false);
3246
+ }
3247
+ function finish() {
3248
+ if (!running) return;
3249
+ running = false;
3250
+ rafId = null;
3251
+ snapToFinal();
3252
+ removeGhosts();
3253
+ settleCanvas();
3254
+ for (let i = 0; i < secondaryEls.length; i++) {
3255
+ secondaryEls[i].style.opacity = secondaryOriginalOpacity[i];
3256
+ }
3257
+ onComplete();
3258
+ }
3259
+ function snapToFinal() {
3260
+ for (const tw of tweens) {
3261
+ snapTweenToFinal(tw);
3262
+ }
3263
+ for (const mark of nextLineMarks) {
3264
+ if (!mark.key) continue;
3265
+ const el = dom.any(mark.key);
3266
+ if (el) applyFinalLinePath(el, mark);
3267
+ }
3268
+ for (const mark of nextAreaMarks) {
3269
+ if (!mark.key) continue;
3270
+ const el = dom.any(mark.key);
3271
+ if (el) applyFinalAreaPaths(el, mark);
3301
3272
  }
3302
- axisGroup.appendChild(ghost);
3303
- ghosts.push(ghost);
3304
- tweens.push({
3305
- tweenType: "axisTick",
3306
- kind: "exit",
3307
- el: ghost,
3308
- posAttr,
3309
- fromPos: prevPos,
3310
- toPos: prevPos,
3311
- crossfade: false,
3312
- ghost,
3313
- fromOpacity: 1,
3314
- toOpacity: 0
3315
- });
3316
3273
  }
3317
- const { prevGridByValue, nextGridByValue } = computeGridlineDeltas(prevAxis, nextAxis);
3318
- const gridlines = axisGroup.querySelectorAll(".oc-gridline[data-tick-key]");
3319
- for (const glEl of gridlines) {
3320
- const key = glEl.getAttribute("data-tick-key");
3321
- if (!key) continue;
3322
- const nextPos = nextGridByValue.get(key);
3323
- const prevPos = prevGridByValue.get(key);
3324
- if (prevPos !== void 0 && nextPos !== void 0) {
3325
- tweens.push({
3326
- tweenType: "gridline",
3327
- kind: "update",
3328
- el: glEl,
3329
- orientation,
3330
- fromPos: prevPos,
3331
- toPos: nextPos
3332
- });
3333
- } else if (prevPos === void 0 && nextPos !== void 0) {
3334
- tweens.push({
3335
- tweenType: "gridline",
3336
- kind: "enter",
3337
- el: glEl,
3338
- orientation,
3339
- fromPos: nextPos,
3340
- toPos: nextPos,
3341
- fromOpacity: 0,
3342
- toOpacity: 1
3343
- });
3274
+ function removeGhosts() {
3275
+ for (const ghost of ghosts) {
3276
+ ghost.parentNode?.removeChild(ghost);
3344
3277
  }
3345
3278
  }
3346
- for (const [key, prevPos] of prevGridByValue) {
3347
- if (nextGridByValue.has(key)) continue;
3348
- const ghost = document.createElementNS("http://www.w3.org/2000/svg", "line");
3349
- ghost.setAttribute("class", "oc-gridline oc-ghost");
3350
- ghost.setAttribute("aria-hidden", "true");
3351
- ghost.setAttribute("pointer-events", "none");
3352
- if (orientation === "y") {
3353
- const area = layout.area;
3354
- ghost.setAttribute("x1", String(area.x));
3355
- ghost.setAttribute("y1", String(prevPos));
3356
- ghost.setAttribute("x2", String(area.x + area.width));
3357
- ghost.setAttribute("y2", String(prevPos));
3358
- } else {
3359
- const area = layout.area;
3360
- ghost.setAttribute("x1", String(prevPos));
3361
- ghost.setAttribute("y1", String(area.y));
3362
- ghost.setAttribute("x2", String(prevPos));
3363
- ghost.setAttribute("y2", String(area.y + area.height));
3279
+ function captureSnapshot() {
3280
+ const snap = /* @__PURE__ */ new Map();
3281
+ const tUpdate = Math.min(lastElapsed / update.duration, 1);
3282
+ const easedUpdate = cubicOut(tUpdate);
3283
+ for (const tw of tweens) {
3284
+ if (tw.kind !== "update") continue;
3285
+ if (tw.tweenType === "canvasPoints") {
3286
+ for (let s = 0; s < tw.soaIndex.length; s++) {
3287
+ snap.set(tw.soaKeys[s], {
3288
+ type: "point",
3289
+ cx: tw.fromCx[s] + (tw.toCx[s] - tw.fromCx[s]) * easedUpdate,
3290
+ cy: tw.fromCy[s] + (tw.toCy[s] - tw.fromCy[s]) * easedUpdate,
3291
+ r: tw.fromR[s] + (tw.toR[s] - tw.fromR[s]) * easedUpdate
3292
+ });
3293
+ }
3294
+ continue;
3295
+ }
3296
+ const key = getKeyForTween(tw);
3297
+ if (!key) continue;
3298
+ switch (tw.tweenType) {
3299
+ case "rect": {
3300
+ const g = lerpGeom(tw.from, tw.to, easedUpdate);
3301
+ snap.set(key, { type: "rect", ...g });
3302
+ break;
3303
+ }
3304
+ case "point": {
3305
+ const cx = tw.fromCx + (tw.toCx - tw.fromCx) * easedUpdate;
3306
+ const cy = tw.fromCy + (tw.toCy - tw.fromCy) * easedUpdate;
3307
+ const entry = { type: "point", cx, cy };
3308
+ if (tw.fromR !== void 0 && tw.toR !== void 0) {
3309
+ entry.r = tw.fromR + (tw.toR - tw.fromR) * easedUpdate;
3310
+ }
3311
+ snap.set(key, entry);
3312
+ break;
3313
+ }
3314
+ case "rule":
3315
+ case "tick": {
3316
+ snap.set(key, {
3317
+ type: tw.tweenType,
3318
+ x1: tw.fromX1 + (tw.toX1 - tw.fromX1) * easedUpdate,
3319
+ y1: tw.fromY1 + (tw.toY1 - tw.fromY1) * easedUpdate,
3320
+ x2: tw.fromX2 + (tw.toX2 - tw.fromX2) * easedUpdate,
3321
+ y2: tw.fromY2 + (tw.toY2 - tw.fromY2) * easedUpdate
3322
+ });
3323
+ break;
3324
+ }
3325
+ case "textMark": {
3326
+ snap.set(key, {
3327
+ type: "textMark",
3328
+ x: tw.fromX + (tw.toX - tw.fromX) * easedUpdate,
3329
+ y: tw.fromY + (tw.toY - tw.fromY) * easedUpdate
3330
+ });
3331
+ break;
3332
+ }
3333
+ case "line": {
3334
+ const pts = interpolatePoints(tw.fromPts, tw.toPts, easedUpdate);
3335
+ const d = buildLinePath2(pts, tw.interpolate);
3336
+ snap.set(key, { type: "line", d });
3337
+ break;
3338
+ }
3339
+ case "area": {
3340
+ const top = interpolatePoints(tw.fromTop, tw.toTop, easedUpdate);
3341
+ const bottom = interpolatePoints(tw.fromBottom, tw.toBottom, easedUpdate);
3342
+ const d = buildAreaPath2(top, bottom, tw.interpolate);
3343
+ let topD;
3344
+ if (tw.hasStroke) {
3345
+ topD = buildLinePath2(top, tw.interpolate);
3346
+ }
3347
+ snap.set(key, { type: "area", d, topD });
3348
+ break;
3349
+ }
3350
+ }
3364
3351
  }
3365
- axisGroup.appendChild(ghost);
3366
- ghosts.push(ghost);
3367
- tweens.push({
3368
- tweenType: "gridline",
3369
- kind: "exit",
3370
- el: ghost,
3371
- orientation,
3372
- fromPos: prevPos,
3373
- toPos: prevPos,
3374
- ghost,
3375
- fromOpacity: 1,
3376
- toOpacity: 0
3377
- });
3352
+ return snap;
3353
+ }
3354
+ if (!manual) {
3355
+ rafId = requestAnimationFrame(tick);
3356
+ }
3357
+ return {
3358
+ cancel() {
3359
+ if (!running) return;
3360
+ running = false;
3361
+ if (rafId !== null) {
3362
+ cancelAnimationFrame(rafId);
3363
+ rafId = null;
3364
+ }
3365
+ snapToFinal();
3366
+ removeGhosts();
3367
+ settleCanvas();
3368
+ for (let i = 0; i < secondaryEls.length; i++) {
3369
+ secondaryEls[i].style.opacity = secondaryOriginalOpacity[i];
3370
+ }
3371
+ },
3372
+ get running() {
3373
+ return running;
3374
+ },
3375
+ snapshot() {
3376
+ return captureSnapshot();
3377
+ },
3378
+ step(elapsedMs) {
3379
+ lastElapsed = elapsedMs;
3380
+ for (const tw of tweens) {
3381
+ applyTweenState(tw, elapsedMs, update, exit, enterDelay, enterDuration);
3382
+ }
3383
+ applySecondaryOpacity(secondaryEls, elapsedMs, enterDelay, enterDuration);
3384
+ if (elapsedMs >= totalMs) {
3385
+ snapToFinal();
3386
+ removeGhosts();
3387
+ settleCanvas();
3388
+ return false;
3389
+ }
3390
+ canvas?.repaint();
3391
+ return true;
3392
+ },
3393
+ totalMs
3394
+ };
3395
+ }
3396
+ function collectSecondaryElements(svg) {
3397
+ const els = [];
3398
+ const epLabels = svg.querySelector(".oc-endpoint-labels");
3399
+ if (epLabels) els.push(epLabels);
3400
+ const markLabels = svg.querySelector(".oc-mark-labels");
3401
+ if (markLabels) els.push(markLabels);
3402
+ return els;
3403
+ }
3404
+ function applySecondaryOpacity(els, elapsed, enterDelay, enterDuration) {
3405
+ if (els.length === 0) return;
3406
+ let t;
3407
+ if (elapsed < enterDelay) {
3408
+ t = 0;
3409
+ } else {
3410
+ t = Math.min((elapsed - enterDelay) / enterDuration, 1);
3411
+ }
3412
+ const opacity = String(cubicOut(t));
3413
+ for (const el of els) {
3414
+ el.style.opacity = opacity;
3378
3415
  }
3379
3416
  }
3380
3417
 
@@ -5162,6 +5199,16 @@ function scheduleFontReload(fontFamily, isAlive, onReady) {
5162
5199
  return true;
5163
5200
  }
5164
5201
 
5202
+ // src/resolve-dark-mode.ts
5203
+ function resolveDarkMode(mode) {
5204
+ if (mode === "force") return true;
5205
+ if (mode === "off" || mode === void 0) return false;
5206
+ if (typeof window !== "undefined" && window.matchMedia) {
5207
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
5208
+ }
5209
+ return false;
5210
+ }
5211
+
5165
5212
  // src/scatter-canvas/interactions.ts
5166
5213
  var HIT_TOLERANCE_MOUSE = 8;
5167
5214
  var HIT_TOLERANCE_TOUCH = 24;
@@ -5694,15 +5741,8 @@ var SpatialIndex = class {
5694
5741
  };
5695
5742
 
5696
5743
  // src/motion/tween.ts
5697
- function cubicOut2(t) {
5698
- const f = 1 - t;
5699
- return 1 - f * f * f;
5700
- }
5701
- function cubicInOut(t) {
5702
- return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
5703
- }
5704
5744
  function resolveEase(ease) {
5705
- return ease === "snappy" ? cubicOut2 : cubicInOut;
5745
+ return ease === "snappy" ? cubicOut : cubicInOut;
5706
5746
  }
5707
5747
  function prefersReducedMotion() {
5708
5748
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
@@ -6218,14 +6258,6 @@ function stampThemeProperties(el, theme) {
6218
6258
 
6219
6259
  // src/mount.ts
6220
6260
  var FALLBACK_HEIGHT = 400;
6221
- function resolveDarkMode(mode) {
6222
- if (mode === "force") return true;
6223
- if (mode === "off" || mode === void 0) return false;
6224
- if (typeof window !== "undefined" && window.matchMedia) {
6225
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
6226
- }
6227
- return false;
6228
- }
6229
6261
  var EDITABLE_HOVER_CSS = `
6230
6262
  .oc-editable-hover {
6231
6263
  outline: 1.5px solid ${cssTokenDefault2("--oc-editable-hover", "light")};
@@ -6316,6 +6348,7 @@ function createChart(container, spec, options) {
6316
6348
  height,
6317
6349
  theme: options?.theme,
6318
6350
  darkMode,
6351
+ renderer: options?.renderer,
6319
6352
  watermark: options?.watermark,
6320
6353
  measureText
6321
6354
  };
@@ -6753,7 +6786,7 @@ function createChart(container, spec, options) {
6753
6786
  if (!warnedCanvasEditMode && (options?.editable || hasEditingCallbacks(options) || options?.onAnnotationEdit)) {
6754
6787
  warnedCanvasEditMode = true;
6755
6788
  console.warn(
6756
- "openchart: canvas mark mode does not support per-mark edit selection. Annotation, chrome, and legend editing still work. Set mark.render to 'svg' if you need to select individual points."
6789
+ "openchart: canvas mark mode does not support per-mark edit selection. Annotation, chrome, and legend editing still work. Pass renderer: 'svg' to createChart() if you need to select individual points."
6757
6790
  );
6758
6791
  }
6759
6792
  }
@@ -7210,12 +7243,10 @@ function createChart(container, spec, options) {
7210
7243
  }
7211
7244
 
7212
7245
  // src/story/tween.ts
7213
- var clamp01 = (t) => Math.min(1, Math.max(0, t));
7214
- var lerp = (a, b, t) => a + (b - a) * t;
7215
7246
  var easingFns = {
7216
- linear: (t) => t,
7217
- easeOutCubic: (t) => 1 - (1 - t) ** 3,
7218
- easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2,
7247
+ linear,
7248
+ easeOutCubic: cubicOut,
7249
+ easeInOutCubic: cubicInOut,
7219
7250
  easeOutQuint: (t) => 1 - (1 - t) ** 5
7220
7251
  };
7221
7252
  var storyMotion = {
@@ -7327,7 +7358,7 @@ function scrubCamera(step, stepProgress, fitted, holdUntil, ease) {
7327
7358
  }
7328
7359
 
7329
7360
  // src/map-mount.ts
7330
- import { compileMap } from "@opendata-ai/openchart-engine";
7361
+ import { compileGeoMap } from "@opendata-ai/openchart-engine";
7331
7362
 
7332
7363
  // src/map-camera.ts
7333
7364
  var FOCUS_DIM_OPACITY = 0.25;
@@ -7800,15 +7831,7 @@ function prefersReducedMotion2() {
7800
7831
  return false;
7801
7832
  }
7802
7833
  }
7803
- function resolveDarkMode2(mode) {
7804
- if (mode === "force") return true;
7805
- if (mode === "off" || mode === void 0) return false;
7806
- if (typeof window !== "undefined" && window.matchMedia) {
7807
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
7808
- }
7809
- return false;
7810
- }
7811
- function createMap(container, spec, options) {
7834
+ function createGeoMap(container, spec, options) {
7812
7835
  let currentSpec = spec;
7813
7836
  let currentLayout;
7814
7837
  let destroyed = false;
@@ -7845,7 +7868,7 @@ function createMap(container, spec, options) {
7845
7868
  }
7846
7869
  function compile() {
7847
7870
  const { width, height } = getContainerDimensions();
7848
- const darkMode = resolveDarkMode2(options?.darkMode);
7871
+ const darkMode = resolveDarkMode(options?.darkMode);
7849
7872
  const compileOpts = {
7850
7873
  width,
7851
7874
  height,
@@ -7854,7 +7877,7 @@ function createMap(container, spec, options) {
7854
7877
  watermark: options?.watermark,
7855
7878
  measureText
7856
7879
  };
7857
- return compileMap(currentSpec, compileOpts);
7880
+ return compileGeoMap(currentSpec, compileOpts);
7858
7881
  }
7859
7882
  function wireTooltipAndInteraction(svg, layout) {
7860
7883
  const cleanups = [];
@@ -8219,7 +8242,7 @@ function createMap(container, spec, options) {
8219
8242
  }
8220
8243
  container.classList.remove("oc-map-root", "oc-dark");
8221
8244
  }
8222
- if (resolveDarkMode2(options?.darkMode)) {
8245
+ if (resolveDarkMode(options?.darkMode)) {
8223
8246
  container.classList.add("oc-dark");
8224
8247
  }
8225
8248
  currentLayout = compile();
@@ -8305,12 +8328,15 @@ export {
8305
8328
  resolveFontFamily,
8306
8329
  scheduleFontReload,
8307
8330
  observeResize,
8331
+ resolveDarkMode,
8308
8332
  createTooltipManager,
8309
8333
  VECTOR_EXPORT_MAX_POINTS,
8310
8334
  materializeCanvasModeSVG,
8311
8335
  AnimationScheduler,
8312
8336
  quadtree,
8313
8337
  SpatialIndex,
8338
+ clamp01,
8339
+ lerp,
8314
8340
  resolveEase,
8315
8341
  prefersReducedMotion,
8316
8342
  createTween,
@@ -8321,8 +8347,6 @@ export {
8321
8347
  canTransitionSpecShape,
8322
8348
  createYouDrawIt,
8323
8349
  createChart,
8324
- clamp01,
8325
- lerp,
8326
8350
  easingFns,
8327
8351
  storyMotion,
8328
8352
  createTween2,
@@ -8334,6 +8358,6 @@ export {
8334
8358
  dampCamera,
8335
8359
  camerasClose,
8336
8360
  scrubCamera,
8337
- createMap
8361
+ createGeoMap
8338
8362
  };
8339
- //# sourceMappingURL=chunk-2YWRNRUK.js.map
8363
+ //# sourceMappingURL=chunk-ADWV256V.js.map