@opendata-ai/openchart-vanilla 8.7.0 → 8.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4269,7 +4269,7 @@ function wireAnnotationDrag(svg, specAnnotations, onAnnotationEdit, onEdit, setD
4269
4269
  };
4270
4270
  }
4271
4271
  function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
4272
- const SVG_NS4 = "http://www.w3.org/2000/svg";
4272
+ const SVG_NS5 = "http://www.w3.org/2000/svg";
4273
4273
  const cleanups = [];
4274
4274
  const annotationGroups = svg.querySelectorAll(".oc-annotation-text");
4275
4275
  for (const el of annotationGroups) {
@@ -4307,7 +4307,7 @@ function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
4307
4307
  const createdHandles = [];
4308
4308
  for (const ep of endpoints) {
4309
4309
  if (!Number.isFinite(ep.cx) || !Number.isFinite(ep.cy)) continue;
4310
- const handleEl = document.createElementNS(SVG_NS4, "circle");
4310
+ const handleEl = document.createElementNS(SVG_NS5, "circle");
4311
4311
  handleEl.setAttribute("class", "oc-connector-handle");
4312
4312
  handleEl.setAttribute("data-endpoint", ep.name);
4313
4313
  handleEl.setAttribute("cx", String(ep.cx));
@@ -4584,7 +4584,7 @@ function wireSeriesLabelDrag(svg, spec, onEdit, setDragging) {
4584
4584
  function wireAnchorDrag(svg, specAnnotations, layout, onEdit, setDragging) {
4585
4585
  if (!onEdit || !layout.xInvert || !layout.yInvert) return () => {
4586
4586
  };
4587
- const SVG_NS4 = "http://www.w3.org/2000/svg";
4587
+ const SVG_NS5 = "http://www.w3.org/2000/svg";
4588
4588
  const cleanups = [];
4589
4589
  const annotationGroups = svg.querySelectorAll(".oc-annotation-text");
4590
4590
  for (const el of annotationGroups) {
@@ -4599,7 +4599,7 @@ function wireAnchorDrag(svg, specAnnotations, layout, onEdit, setDragging) {
4599
4599
  if (!resolved?.dot) continue;
4600
4600
  const anchorX = resolved.dot.x;
4601
4601
  const anchorY = resolved.dot.y;
4602
- const handleEl = document.createElementNS(SVG_NS4, "circle");
4602
+ const handleEl = document.createElementNS(SVG_NS5, "circle");
4603
4603
  handleEl.setAttribute("class", "oc-anchor-handle");
4604
4604
  handleEl.setAttribute("cx", String(anchorX));
4605
4605
  handleEl.setAttribute("cy", String(anchorY));
@@ -7003,6 +7003,42 @@ function cameraForTarget(layout, target) {
7003
7003
  const cam = fitTarget(target, vb);
7004
7004
  return { ...cam, k: Math.max(1, Math.min(40, cam.k)) };
7005
7005
  }
7006
+ var DEFAULT_MAX_ZOOM = 12;
7007
+ function resolveMapZoom(zoom) {
7008
+ if (!zoom) return null;
7009
+ const cfg = zoom === true ? {} : zoom;
7010
+ const max = cfg.maxZoom;
7011
+ return {
7012
+ maxZoom: typeof max === "number" && Number.isFinite(max) ? Math.max(1, max) : DEFAULT_MAX_ZOOM,
7013
+ controls: cfg.controls !== false
7014
+ };
7015
+ }
7016
+ function clampMapCamera(camera, vb, maxZoom) {
7017
+ const k = Math.min(Math.max(camera.k, 1), Math.max(1, maxZoom));
7018
+ const halfW = vb.width / (2 * k);
7019
+ const halfH = vb.height / (2 * k);
7020
+ return {
7021
+ cx: Math.min(Math.max(camera.cx, halfW), vb.width - halfW),
7022
+ cy: Math.min(Math.max(camera.cy, halfH), vb.height - halfH),
7023
+ k
7024
+ };
7025
+ }
7026
+ function zoomCameraAt(camera, vb, k, pivotX, pivotY) {
7027
+ const ox = pivotX - vb.width / 2;
7028
+ const oy = pivotY - vb.height / 2;
7029
+ const px = camera.cx + ox / camera.k;
7030
+ const py = camera.cy + oy / camera.k;
7031
+ return { cx: px - ox / k, cy: py - oy / k, k };
7032
+ }
7033
+ function panCamera(camera, dx, dy) {
7034
+ return { cx: camera.cx - dx / camera.k, cy: camera.cy - dy / camera.k, k: camera.k };
7035
+ }
7036
+ function wheelZoomFactor(e) {
7037
+ const perUnit = e.deltaMode === 1 ? 0.05 : e.deltaMode === 2 ? 1 : 2e-3;
7038
+ let exp = -e.deltaY * perUnit;
7039
+ if (e.ctrlKey && e.deltaMode === 0 && Math.abs(e.deltaY) < 50) exp *= 5;
7040
+ return 2 ** Math.max(-1, Math.min(1, exp));
7041
+ }
7006
7042
  function applyMapCamera(svg, camera, layout) {
7007
7043
  const cameraGroup = svg.querySelector("[data-oc-map-camera]");
7008
7044
  if (!cameraGroup) return;
@@ -7524,6 +7560,468 @@ function runMapFillTransition(svg, prev, opts) {
7524
7560
  };
7525
7561
  }
7526
7562
 
7563
+ // src/map-zoom.ts
7564
+ var ZOOM_STEP = 2;
7565
+ var KEY_PAN_FRACTION = 0.1;
7566
+ var DRAG_THRESHOLD = 3;
7567
+ var HINT_MS = 1500;
7568
+ var EPS = 1e-3;
7569
+ var SVG_NS4 = "http://www.w3.org/2000/svg";
7570
+ var clipIdCounter = 0;
7571
+ var ICONS = {
7572
+ in: '<path d="M8 3v10M3 8h10"/>',
7573
+ out: '<path d="M3 8h10"/>',
7574
+ reset: '<path d="M3 8a5 5 0 1 0 1.6-3.7"/><path d="M3 2.5v2.8h2.8"/>'
7575
+ };
7576
+ function isMacLike() {
7577
+ if (typeof navigator === "undefined") return false;
7578
+ return /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent || "");
7579
+ }
7580
+ function createMapZoomController(host) {
7581
+ const { container } = host;
7582
+ const clipId = `oc-map-zoom-clip-${++clipIdCounter}`;
7583
+ let overlay = null;
7584
+ let buttons = null;
7585
+ let hint = null;
7586
+ let hintTimer = null;
7587
+ let drag = null;
7588
+ let suppressClick = false;
7589
+ let pinch = null;
7590
+ let touchPan = null;
7591
+ let gestureStartK = null;
7592
+ function vb() {
7593
+ return host.getLayout().mapSize;
7594
+ }
7595
+ function clamp(camera) {
7596
+ const cfg = host.getConfig();
7597
+ return clampMapCamera(camera, vb(), cfg?.maxZoom ?? 1);
7598
+ }
7599
+ function clampK(k) {
7600
+ const cfg = host.getConfig();
7601
+ return Math.min(Math.max(k, 1), cfg?.maxZoom ?? 1);
7602
+ }
7603
+ function svgRect() {
7604
+ const svg = host.getSvg();
7605
+ let rect = svg?.getBoundingClientRect();
7606
+ if (!rect || !rect.width || !rect.height) rect = container.getBoundingClientRect();
7607
+ if (!rect.width || !rect.height) return null;
7608
+ return rect;
7609
+ }
7610
+ function toMapFrame(clientX, clientY) {
7611
+ const rect = svgRect();
7612
+ if (!rect) return null;
7613
+ const layout = host.getLayout();
7614
+ const x = (clientX - rect.left) * (layout.width / rect.width) - layout.area.x;
7615
+ const y = (clientY - rect.top) * (layout.height / rect.height) - layout.area.y;
7616
+ const inside = x >= 0 && y >= 0 && x <= layout.area.width && y <= layout.area.height;
7617
+ return { x, y, inside };
7618
+ }
7619
+ function unitsPerPx() {
7620
+ const rect = svgRect();
7621
+ return rect ? host.getLayout().width / rect.width : 1;
7622
+ }
7623
+ function isZoomed() {
7624
+ return host.getCamera().k > 1 + EPS;
7625
+ }
7626
+ function fromControls(e) {
7627
+ const t = e.target;
7628
+ return !!t?.closest?.(".oc-map-zoom");
7629
+ }
7630
+ function zoomAround(factor, x, y, animate) {
7631
+ const cam = host.getCamera();
7632
+ const next = clamp(zoomCameraAt(cam, vb(), clampK(cam.k * factor), x, y));
7633
+ if (animate) host.animateCamera(next);
7634
+ else host.setCamera(next);
7635
+ }
7636
+ function zoomAtCenter(factor) {
7637
+ const { width, height } = vb();
7638
+ zoomAround(factor, width / 2, height / 2, true);
7639
+ }
7640
+ function showHint() {
7641
+ if (!hint) return;
7642
+ hint.setAttribute("data-visible", "");
7643
+ if (hintTimer) clearTimeout(hintTimer);
7644
+ hintTimer = setTimeout(() => {
7645
+ hint?.removeAttribute("data-visible");
7646
+ hintTimer = null;
7647
+ }, HINT_MS);
7648
+ }
7649
+ function onWheel(e) {
7650
+ if (!host.getConfig() || fromControls(e)) return;
7651
+ const p = toMapFrame(e.clientX, e.clientY);
7652
+ if (!p?.inside) return;
7653
+ if (!e.ctrlKey && !e.metaKey) {
7654
+ if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) showHint();
7655
+ return;
7656
+ }
7657
+ e.preventDefault();
7658
+ hint?.removeAttribute("data-visible");
7659
+ zoomAround(wheelZoomFactor(e), p.x, p.y, false);
7660
+ }
7661
+ function onGestureStart(e) {
7662
+ if (!host.getConfig()) return;
7663
+ e.preventDefault();
7664
+ gestureStartK = host.getCamera().k;
7665
+ }
7666
+ function onGestureChange(e) {
7667
+ if (!host.getConfig() || gestureStartK === null) return;
7668
+ e.preventDefault();
7669
+ if (pinch) return;
7670
+ const g = e;
7671
+ const p = toMapFrame(g.clientX, g.clientY);
7672
+ if (!p) return;
7673
+ const cam = host.getCamera();
7674
+ host.setCamera(clamp(zoomCameraAt(cam, vb(), clampK(gestureStartK * g.scale), p.x, p.y)));
7675
+ }
7676
+ function onGestureEnd(e) {
7677
+ if (gestureStartK === null) return;
7678
+ e.preventDefault();
7679
+ gestureStartK = null;
7680
+ }
7681
+ function onPointerDown(e) {
7682
+ if (e.pointerType === "touch" || e.button !== 0) return;
7683
+ if (!host.getConfig() || fromControls(e) || !isZoomed()) return;
7684
+ const p = toMapFrame(e.clientX, e.clientY);
7685
+ if (!p?.inside) return;
7686
+ drag = {
7687
+ id: e.pointerId,
7688
+ startX: e.clientX,
7689
+ startY: e.clientY,
7690
+ lastX: e.clientX,
7691
+ lastY: e.clientY,
7692
+ moved: false
7693
+ };
7694
+ }
7695
+ function onMouseDown(e) {
7696
+ if (e.button !== 0 || !host.getConfig() || fromControls(e)) return;
7697
+ if (!isZoomed() && e.detail < 2) return;
7698
+ const p = toMapFrame(e.clientX, e.clientY);
7699
+ if (!p?.inside) return;
7700
+ e.preventDefault();
7701
+ host.getSvg()?.focus({ preventScroll: true });
7702
+ }
7703
+ function onPointerMove(e) {
7704
+ if (!drag || e.pointerId !== drag.id) return;
7705
+ if (!drag.moved) {
7706
+ if (Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < DRAG_THRESHOLD) return;
7707
+ drag.moved = true;
7708
+ const svg = host.getSvg();
7709
+ try {
7710
+ svg?.setPointerCapture(e.pointerId);
7711
+ } catch {
7712
+ }
7713
+ svg?.classList.add("oc-map--panning");
7714
+ host.hideTooltip();
7715
+ }
7716
+ e.preventDefault();
7717
+ const s = unitsPerPx();
7718
+ const dx = (e.clientX - drag.lastX) * s;
7719
+ const dy = (e.clientY - drag.lastY) * s;
7720
+ drag.lastX = e.clientX;
7721
+ drag.lastY = e.clientY;
7722
+ host.setCamera(clamp(panCamera(host.getCamera(), dx, dy)));
7723
+ }
7724
+ function endDrag(e) {
7725
+ if (!drag || e.pointerId !== drag.id) return;
7726
+ const svg = host.getSvg();
7727
+ if (drag.moved) {
7728
+ suppressClick = true;
7729
+ setTimeout(() => {
7730
+ suppressClick = false;
7731
+ }, 0);
7732
+ }
7733
+ try {
7734
+ if (svg?.hasPointerCapture?.(e.pointerId)) svg.releasePointerCapture(e.pointerId);
7735
+ } catch {
7736
+ }
7737
+ svg?.classList.remove("oc-map--panning");
7738
+ drag = null;
7739
+ }
7740
+ function onClickCapture(e) {
7741
+ if (!suppressClick) return;
7742
+ suppressClick = false;
7743
+ e.stopPropagation();
7744
+ e.preventDefault();
7745
+ }
7746
+ function onDblClick(e) {
7747
+ if (!host.getConfig() || fromControls(e)) return;
7748
+ const p = toMapFrame(e.clientX, e.clientY);
7749
+ if (!p?.inside) return;
7750
+ e.preventDefault();
7751
+ zoomAround(e.shiftKey ? 1 / ZOOM_STEP : ZOOM_STEP, p.x, p.y, true);
7752
+ }
7753
+ function touchPoint(t) {
7754
+ return toMapFrame(t.clientX, t.clientY);
7755
+ }
7756
+ function startPinch(e) {
7757
+ const a = touchPoint(e.touches[0]);
7758
+ const b = touchPoint(e.touches[1]);
7759
+ if (!a || !b) return;
7760
+ pinch = {
7761
+ dist: Math.hypot(b.x - a.x, b.y - a.y) || 1,
7762
+ x: (a.x + b.x) / 2,
7763
+ y: (a.y + b.y) / 2
7764
+ };
7765
+ touchPan = null;
7766
+ }
7767
+ function startTouchPan(t) {
7768
+ touchPan = isZoomed() ? { x: t.clientX, y: t.clientY, moved: false } : null;
7769
+ }
7770
+ function onTouchStart(e) {
7771
+ if (!host.getConfig() || fromControls(e)) return;
7772
+ if (e.touches.length >= 2) {
7773
+ const a = touchPoint(e.touches[0]);
7774
+ if (!a?.inside) return;
7775
+ e.preventDefault();
7776
+ host.hideTooltip();
7777
+ startPinch(e);
7778
+ } else if (e.touches.length === 1) {
7779
+ const p = touchPoint(e.touches[0]);
7780
+ if (!p?.inside) return;
7781
+ startTouchPan(e.touches[0]);
7782
+ }
7783
+ }
7784
+ function onTouchMove(e) {
7785
+ if (!host.getConfig()) return;
7786
+ if (pinch && e.touches.length >= 2) {
7787
+ e.preventDefault();
7788
+ const a = touchPoint(e.touches[0]);
7789
+ const b = touchPoint(e.touches[1]);
7790
+ if (!a || !b) return;
7791
+ const dist = Math.hypot(b.x - a.x, b.y - a.y) || 1;
7792
+ const x = (a.x + b.x) / 2;
7793
+ const y = (a.y + b.y) / 2;
7794
+ let cam = host.getCamera();
7795
+ cam = zoomCameraAt(cam, vb(), clampK(cam.k * (dist / pinch.dist)), x, y);
7796
+ cam = panCamera(cam, x - pinch.x, y - pinch.y);
7797
+ host.setCamera(clamp(cam));
7798
+ pinch = { dist, x, y };
7799
+ return;
7800
+ }
7801
+ if (touchPan && e.touches.length === 1) {
7802
+ const t = e.touches[0];
7803
+ if (!touchPan.moved) {
7804
+ if (Math.hypot(t.clientX - touchPan.x, t.clientY - touchPan.y) < DRAG_THRESHOLD) return;
7805
+ touchPan.moved = true;
7806
+ host.hideTooltip();
7807
+ }
7808
+ if (e.cancelable) e.preventDefault();
7809
+ const s = unitsPerPx();
7810
+ const dx = (t.clientX - touchPan.x) * s;
7811
+ const dy = (t.clientY - touchPan.y) * s;
7812
+ touchPan.x = t.clientX;
7813
+ touchPan.y = t.clientY;
7814
+ host.setCamera(clamp(panCamera(host.getCamera(), dx, dy)));
7815
+ }
7816
+ }
7817
+ function onTouchEnd(e) {
7818
+ if (e.touches.length >= 2) {
7819
+ startPinch(e);
7820
+ return;
7821
+ }
7822
+ pinch = null;
7823
+ if (e.touches.length === 1) {
7824
+ startTouchPan(e.touches[0]);
7825
+ if (touchPan) touchPan.moved = true;
7826
+ } else {
7827
+ touchPan = null;
7828
+ }
7829
+ }
7830
+ function onKeyDown(e) {
7831
+ if (!host.getConfig() || e.target !== host.getSvg()) return;
7832
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
7833
+ const { width, height } = vb();
7834
+ switch (e.key) {
7835
+ case "+":
7836
+ case "=":
7837
+ zoomAtCenter(ZOOM_STEP);
7838
+ break;
7839
+ case "-":
7840
+ case "_":
7841
+ zoomAtCenter(1 / ZOOM_STEP);
7842
+ break;
7843
+ case "0":
7844
+ host.reset();
7845
+ break;
7846
+ case "ArrowLeft":
7847
+ case "ArrowRight":
7848
+ case "ArrowUp":
7849
+ case "ArrowDown": {
7850
+ if (!isZoomed()) return;
7851
+ const dx = e.key === "ArrowLeft" ? width * KEY_PAN_FRACTION : e.key === "ArrowRight" ? -width * KEY_PAN_FRACTION : 0;
7852
+ const dy = e.key === "ArrowUp" ? height * KEY_PAN_FRACTION : e.key === "ArrowDown" ? -height * KEY_PAN_FRACTION : 0;
7853
+ host.setCamera(clamp(panCamera(host.getCamera(), dx, dy)));
7854
+ break;
7855
+ }
7856
+ default:
7857
+ return;
7858
+ }
7859
+ e.preventDefault();
7860
+ }
7861
+ function makeButton(kind, label) {
7862
+ const btn = document.createElement("button");
7863
+ btn.type = "button";
7864
+ btn.className = "oc-map-zoom-btn";
7865
+ btn.setAttribute("data-zoom", kind);
7866
+ btn.setAttribute("aria-label", label);
7867
+ btn.title = label;
7868
+ btn.innerHTML = `<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${ICONS[kind]}</svg>`;
7869
+ btn.addEventListener("click", (e) => {
7870
+ e.stopPropagation();
7871
+ if (kind === "in") zoomAtCenter(ZOOM_STEP);
7872
+ else if (kind === "out") zoomAtCenter(1 / ZOOM_STEP);
7873
+ else host.reset();
7874
+ });
7875
+ return btn;
7876
+ }
7877
+ function ensureOverlay(showControls) {
7878
+ if (!overlay) {
7879
+ overlay = document.createElement("div");
7880
+ overlay.className = "oc-map-zoom";
7881
+ hint = document.createElement("div");
7882
+ hint.className = "oc-map-zoom-hint";
7883
+ hint.setAttribute("aria-hidden", "true");
7884
+ hint.textContent = isMacLike() ? "Use \u2318 + scroll to zoom the map" : "Use Ctrl + scroll to zoom the map";
7885
+ overlay.appendChild(hint);
7886
+ if (getComputedStyle(container).position === "static") {
7887
+ container.style.position = "relative";
7888
+ }
7889
+ container.appendChild(overlay);
7890
+ }
7891
+ if (showControls && !buttons) {
7892
+ const group = document.createElement("div");
7893
+ group.className = "oc-map-zoom-controls";
7894
+ group.setAttribute("role", "group");
7895
+ group.setAttribute("aria-label", "Map zoom");
7896
+ buttons = {
7897
+ in: makeButton("in", "Zoom in"),
7898
+ out: makeButton("out", "Zoom out"),
7899
+ reset: makeButton("reset", "Reset zoom")
7900
+ };
7901
+ group.append(buttons.in, buttons.out, buttons.reset);
7902
+ overlay.appendChild(group);
7903
+ } else if (!showControls && buttons) {
7904
+ buttons.in.parentElement?.remove();
7905
+ buttons = null;
7906
+ }
7907
+ }
7908
+ function removeOverlay() {
7909
+ if (hintTimer) clearTimeout(hintTimer);
7910
+ hintTimer = null;
7911
+ overlay?.remove();
7912
+ overlay = null;
7913
+ buttons = null;
7914
+ hint = null;
7915
+ }
7916
+ function placeOverlay(layout) {
7917
+ if (!overlay || !layout.width || !layout.height) return;
7918
+ const { area, width, height } = layout;
7919
+ overlay.style.left = `${area.x / width * 100}%`;
7920
+ overlay.style.top = `${area.y / height * 100}%`;
7921
+ overlay.style.width = `${area.width / width * 100}%`;
7922
+ overlay.style.height = `${area.height / height * 100}%`;
7923
+ }
7924
+ function ensureClip(svg, layout) {
7925
+ if (svg.querySelector(`#${clipId}`)) return;
7926
+ let defs = svg.querySelector("defs");
7927
+ if (!defs) {
7928
+ defs = document.createElementNS(SVG_NS4, "defs");
7929
+ svg.insertBefore(defs, svg.firstChild);
7930
+ }
7931
+ const clip = document.createElementNS(SVG_NS4, "clipPath");
7932
+ clip.setAttribute("id", clipId);
7933
+ const rect = document.createElementNS(SVG_NS4, "rect");
7934
+ rect.setAttribute("width", String(layout.area.width));
7935
+ rect.setAttribute("height", String(layout.area.height));
7936
+ clip.appendChild(rect);
7937
+ defs.appendChild(clip);
7938
+ }
7939
+ function onRender(restoreFocus) {
7940
+ const cfg = host.getConfig();
7941
+ const svg = host.getSvg();
7942
+ if (!cfg || !svg) {
7943
+ removeOverlay();
7944
+ return;
7945
+ }
7946
+ const layout = host.getLayout();
7947
+ svg.setAttribute("tabindex", "0");
7948
+ svg.setAttribute("aria-keyshortcuts", "+ - 0 ArrowUp ArrowDown ArrowLeft ArrowRight");
7949
+ ensureClip(svg, layout);
7950
+ ensureOverlay(cfg.controls);
7951
+ placeOverlay(layout);
7952
+ if (restoreFocus) svg.focus({ preventScroll: true });
7953
+ onCamera();
7954
+ }
7955
+ function onCamera() {
7956
+ const cfg = host.getConfig();
7957
+ const svg = host.getSvg();
7958
+ if (!cfg || !svg) return;
7959
+ const cam = host.getCamera();
7960
+ const zoomed = cam.k > 1 + EPS;
7961
+ const { width, height } = vb();
7962
+ const atFullView = !zoomed && Math.abs(cam.cx - width / 2) < EPS && Math.abs(cam.cy - height / 2) < EPS;
7963
+ const group = svg.querySelector(".oc-map-group");
7964
+ if (zoomed) group?.setAttribute("clip-path", `url(#${clipId})`);
7965
+ else group?.removeAttribute("clip-path");
7966
+ svg.style.touchAction = zoomed ? "none" : "pan-x pan-y";
7967
+ if (zoomed) svg.setAttribute("data-oc-zoomed", "");
7968
+ else svg.removeAttribute("data-oc-zoomed");
7969
+ if (buttons) {
7970
+ setDisabled(buttons.in, cam.k >= cfg.maxZoom - EPS, svg);
7971
+ setDisabled(buttons.out, !zoomed, svg);
7972
+ setDisabled(buttons.reset, atFullView, svg);
7973
+ }
7974
+ }
7975
+ function setDisabled(btn, disabled, svg) {
7976
+ if (btn.disabled === disabled) return;
7977
+ const hadFocus = disabled && document.activeElement === btn;
7978
+ btn.disabled = disabled;
7979
+ if (hadFocus) svg.focus({ preventScroll: true });
7980
+ }
7981
+ const touchOpts = { passive: false };
7982
+ container.addEventListener("wheel", onWheel, { passive: false });
7983
+ container.addEventListener("pointerdown", onPointerDown);
7984
+ container.addEventListener("mousedown", onMouseDown);
7985
+ container.addEventListener("pointermove", onPointerMove);
7986
+ container.addEventListener("pointerup", endDrag);
7987
+ container.addEventListener("pointercancel", endDrag);
7988
+ container.addEventListener("click", onClickCapture, true);
7989
+ container.addEventListener("dblclick", onDblClick);
7990
+ container.addEventListener("touchstart", onTouchStart, touchOpts);
7991
+ container.addEventListener("touchmove", onTouchMove, touchOpts);
7992
+ container.addEventListener("touchend", onTouchEnd);
7993
+ container.addEventListener("touchcancel", onTouchEnd);
7994
+ container.addEventListener("gesturestart", onGestureStart);
7995
+ container.addEventListener("gesturechange", onGestureChange);
7996
+ container.addEventListener("gestureend", onGestureEnd);
7997
+ container.addEventListener("keydown", onKeyDown);
7998
+ return {
7999
+ onRender,
8000
+ onCamera,
8001
+ destroy() {
8002
+ container.removeEventListener("wheel", onWheel);
8003
+ container.removeEventListener("pointerdown", onPointerDown);
8004
+ container.removeEventListener("mousedown", onMouseDown);
8005
+ container.removeEventListener("pointermove", onPointerMove);
8006
+ container.removeEventListener("pointerup", endDrag);
8007
+ container.removeEventListener("pointercancel", endDrag);
8008
+ container.removeEventListener("click", onClickCapture, true);
8009
+ container.removeEventListener("dblclick", onDblClick);
8010
+ container.removeEventListener("touchstart", onTouchStart);
8011
+ container.removeEventListener("touchmove", onTouchMove);
8012
+ container.removeEventListener("touchend", onTouchEnd);
8013
+ container.removeEventListener("touchcancel", onTouchEnd);
8014
+ container.removeEventListener("gesturestart", onGestureStart);
8015
+ container.removeEventListener("gesturechange", onGestureChange);
8016
+ container.removeEventListener("gestureend", onGestureEnd);
8017
+ container.removeEventListener("keydown", onKeyDown);
8018
+ const svg = host.getSvg();
8019
+ svg?.removeAttribute("tabindex");
8020
+ removeOverlay();
8021
+ }
8022
+ };
8023
+ }
8024
+
7527
8025
  // src/map-mount.ts
7528
8026
  function focusSignature(focus) {
7529
8027
  if (!focus) return null;
@@ -7552,6 +8050,7 @@ function restoreFeatureOrder(el) {
7552
8050
  parent.appendChild(el);
7553
8051
  }
7554
8052
  }
8053
+ var USER_ZOOM_MS = 250;
7555
8054
  function prefersReducedMotion2() {
7556
8055
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
7557
8056
  try {
@@ -7574,6 +8073,7 @@ function createGeoMap(container, spec, options) {
7574
8073
  let currentCamera = { cx: 0, cy: 0, k: 1 };
7575
8074
  let currentFocusIds = null;
7576
8075
  let cameraTween = null;
8076
+ let zoomController = null;
7577
8077
  let isFirstRender = true;
7578
8078
  let fontsReloadPending = false;
7579
8079
  container.classList.add("oc-map-root");
@@ -7774,6 +8274,18 @@ function createGeoMap(container, spec, options) {
7774
8274
  }
7775
8275
  return cameraForTarget(currentLayout, null);
7776
8276
  }
8277
+ function commitCamera(cam) {
8278
+ currentCamera = cam;
8279
+ if (svgElement) applyMapCamera(svgElement, cam, currentLayout);
8280
+ zoomController?.onCamera();
8281
+ options?.onCameraChange?.({ ...cam });
8282
+ }
8283
+ function cancelCameraTween() {
8284
+ if (cameraTween) {
8285
+ cameraTween.cancel();
8286
+ cameraTween = null;
8287
+ }
8288
+ }
7777
8289
  function animateCamera(target, duration) {
7778
8290
  if (cameraTween) cameraTween.cancel();
7779
8291
  cameraTween = createTween2({
@@ -7782,20 +8294,64 @@ function createGeoMap(container, spec, options) {
7782
8294
  duration,
7783
8295
  ease: easingFns.easeInOutCubic,
7784
8296
  onFrame(cam) {
7785
- currentCamera = cam;
7786
- if (svgElement) applyMapCamera(svgElement, cam, currentLayout);
8297
+ commitCamera(cam);
7787
8298
  }
7788
8299
  });
7789
8300
  cameraTween.to(target);
7790
8301
  }
7791
8302
  function driveCamera(cam, duration) {
7792
8303
  if (duration === 0 || prefersReducedMotion2()) {
7793
- currentCamera = cam;
7794
- if (svgElement) applyMapCamera(svgElement, cam, currentLayout);
8304
+ cancelCameraTween();
8305
+ commitCamera(cam);
7795
8306
  } else {
7796
8307
  animateCamera(cam, duration);
7797
8308
  }
7798
8309
  }
8310
+ function resetCamera(duration) {
8311
+ currentFocusIds = null;
8312
+ const cam = cameraForTarget(currentLayout, null);
8313
+ driveCamera(cam, duration);
8314
+ applyFocusDim(null);
8315
+ updateA11yLive(null);
8316
+ }
8317
+ function rescaleFreeCamera(prev) {
8318
+ const next = currentLayout.mapSize;
8319
+ if (!prev.width || !prev.height || prev.width === next.width && prev.height === next.height) {
8320
+ return;
8321
+ }
8322
+ const scaled = {
8323
+ cx: currentCamera.cx * next.width / prev.width,
8324
+ cy: currentCamera.cy * next.height / prev.height,
8325
+ k: currentCamera.k
8326
+ };
8327
+ const zoom = resolveMapZoom(currentSpec.geo?.zoom);
8328
+ currentCamera = zoom ? clampMapCamera(scaled, next, zoom.maxZoom) : scaled;
8329
+ }
8330
+ function syncZoomController(restoreFocus) {
8331
+ const zoom = resolveMapZoom(currentSpec.geo?.zoom);
8332
+ if (!zoom && !zoomController) return;
8333
+ if (!zoomController) {
8334
+ zoomController = createMapZoomController({
8335
+ container,
8336
+ getSvg: () => svgElement,
8337
+ getLayout: () => currentLayout,
8338
+ getConfig: () => resolveMapZoom(currentSpec.geo?.zoom),
8339
+ getCamera: () => ({ ...currentCamera }),
8340
+ setCamera(cam) {
8341
+ cancelCameraTween();
8342
+ currentFocusIds = null;
8343
+ commitCamera(cam);
8344
+ },
8345
+ animateCamera(cam) {
8346
+ currentFocusIds = null;
8347
+ driveCamera(cam, USER_ZOOM_MS);
8348
+ },
8349
+ reset: () => resetCamera(USER_ZOOM_MS),
8350
+ hideTooltip: () => tooltipManager?.hide()
8351
+ });
8352
+ }
8353
+ zoomController.onRender(restoreFocus);
8354
+ }
7799
8355
  function updateA11yLive(ids) {
7800
8356
  let liveEl = container.querySelector(".oc-map-live");
7801
8357
  if (!liveEl) {
@@ -7842,6 +8398,7 @@ function createGeoMap(container, spec, options) {
7842
8398
  fillTransitionHandle = null;
7843
8399
  }
7844
8400
  const oldSvg = svgElement;
8401
+ const oldSvgHadFocus = !!oldSvg && document.activeElement === oldSvg;
7845
8402
  const newSvg = renderMapSVG(currentLayout, { animate });
7846
8403
  if (oldSvg) {
7847
8404
  if (cleanupTooltipEvents) {
@@ -7893,6 +8450,7 @@ function createGeoMap(container, spec, options) {
7893
8450
  }
7894
8451
  }
7895
8452
  isFirstRender = false;
8453
+ syncZoomController(oldSvgHadFocus);
7896
8454
  }
7897
8455
  function update(newSpec) {
7898
8456
  currentSpec = newSpec;
@@ -7905,7 +8463,9 @@ function createGeoMap(container, spec, options) {
7905
8463
  const prevPaint = canAnimate ? captureMapSnapshot(svgElement) : null;
7906
8464
  const prevFocusIds = currentFocusIds ? currentFocusIds.map(String).sort().join(",") : null;
7907
8465
  const prevFocusSig = focusSignature(currentLayout.focus);
8466
+ const prevMapSize = currentLayout.mapSize;
7908
8467
  currentLayout = compile();
8468
+ if (!currentFocusIds) rescaleFreeCamera(prevMapSize);
7909
8469
  render();
7910
8470
  if (canAnimate && prevPaint && (prevPaint.fills.size > 0 || prevPaint.points.size > 0) && svgElement && currentLayout.animation?.update) {
7911
8471
  fillTransitionHandle = runMapFillTransition(svgElement, prevPaint, {
@@ -7934,11 +8494,10 @@ function createGeoMap(container, spec, options) {
7934
8494
  pendingResize = true;
7935
8495
  return;
7936
8496
  }
7937
- if (cameraTween) {
7938
- cameraTween.cancel();
7939
- cameraTween = null;
7940
- }
8497
+ cancelCameraTween();
8498
+ const prevMapSize = currentLayout.mapSize;
7941
8499
  currentLayout = compile();
8500
+ if (!currentFocusIds) rescaleFreeCamera(prevMapSize);
7942
8501
  render();
7943
8502
  if (currentFocusIds && currentFocusIds.length > 0) {
7944
8503
  currentCamera = resolveCamera();
@@ -7949,6 +8508,7 @@ function createGeoMap(container, spec, options) {
7949
8508
  if (svgElement) applyMapCamera(svgElement, currentCamera, currentLayout);
7950
8509
  }
7951
8510
  applyFocusDim(currentFocusIds);
8511
+ zoomController?.onCamera();
7952
8512
  }
7953
8513
  function exportChart(format, options_) {
7954
8514
  if (!svgElement) return "";
@@ -7977,9 +8537,10 @@ function createGeoMap(container, spec, options) {
7977
8537
  fillTransitionHandle.cancel();
7978
8538
  fillTransitionHandle = null;
7979
8539
  }
7980
- if (cameraTween) {
7981
- cameraTween.cancel();
7982
- cameraTween = null;
8540
+ cancelCameraTween();
8541
+ if (zoomController) {
8542
+ zoomController.destroy();
8543
+ zoomController = null;
7983
8544
  }
7984
8545
  if (cleanupTooltipEvents) {
7985
8546
  cleanupTooltipEvents();
@@ -8062,19 +8623,15 @@ function createGeoMap(container, spec, options) {
8062
8623
  applyFocusDim(null);
8063
8624
  },
8064
8625
  resetView(opts) {
8065
- currentFocusIds = null;
8066
- const cam = cameraForTarget(currentLayout, null);
8067
- driveCamera(cam, opts?.duration ?? storyMotion.camera);
8068
- applyFocusDim(null);
8069
- updateA11yLive(null);
8626
+ resetCamera(opts?.duration ?? storyMotion.camera);
8070
8627
  },
8071
8628
  getCamera() {
8072
8629
  return { ...currentCamera };
8073
8630
  },
8074
8631
  setCamera(camera) {
8075
- currentCamera = camera;
8632
+ cancelCameraTween();
8076
8633
  currentFocusIds = null;
8077
- if (svgElement) applyMapCamera(svgElement, camera, currentLayout);
8634
+ commitCamera(camera);
8078
8635
  }
8079
8636
  };
8080
8637
  }
@@ -8108,4 +8665,4 @@ export {
8108
8665
  scrubCamera,
8109
8666
  createGeoMap
8110
8667
  };
8111
- //# sourceMappingURL=chunk-5O624XON.js.map
8668
+ //# sourceMappingURL=chunk-QBTRAQAX.js.map