@opendata-ai/openchart-vanilla 8.9.0 → 8.9.1

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.
@@ -16,8 +16,9 @@ import {
16
16
  renderSingleMark,
17
17
  resolveMarkFill,
18
18
  resolvePatternFill,
19
- stampAnimationVars
20
- } from "./chunk-TP3GCWCO.js";
19
+ stampAnimationVars,
20
+ updateAnnotationGeometry
21
+ } from "./chunk-APMUBW72.js";
21
22
  import {
22
23
  SVG_NS,
23
24
  createSVGElement,
@@ -37,11 +38,12 @@ import {
37
38
  prefersReducedMotion,
38
39
  resolveDarkMode,
39
40
  resolveEase
40
- } from "./chunk-2OJZBMXM.js";
41
+ } from "./chunk-REH5UQCL.js";
41
42
  import {
42
43
  resolvedSurface,
43
- stampThemeProperties
44
- } from "./chunk-NG7CVCI2.js";
44
+ stampThemeProperties,
45
+ warnDeprecated
46
+ } from "./chunk-UC4IHEUO.js";
45
47
 
46
48
  // src/scatter-canvas/renderer.ts
47
49
  var TWO_PI = Math.PI * 2;
@@ -548,8 +550,7 @@ function hasAnnotationChanged(prevLayout, nextLayout) {
548
550
  if (prevKeys[i] !== nextKeys[i]) return true;
549
551
  const p = prev[i];
550
552
  const n = next[i];
551
- if (p.label?.x !== n.label?.x || p.label?.y !== n.label?.y) return true;
552
- if (p.opacity !== n.opacity) return true;
553
+ if (JSON.stringify(p) !== JSON.stringify(n)) return true;
553
554
  }
554
555
  return false;
555
556
  }
@@ -920,7 +921,7 @@ import { buildAreaPath as buildAreaPath2, buildLinePath as buildLinePath2, EXIT_
920
921
 
921
922
  // src/transition/apply.ts
922
923
  import { sampleMotion as sampleMotion2 } from "@opendata-ai/openchart-core";
923
- import { buildArcPath } from "@opendata-ai/openchart-engine";
924
+ import { buildArcPath, interpolateAnnotationGeometry } from "@opendata-ai/openchart-engine";
924
925
 
925
926
  // src/transition/canvas-tweens.ts
926
927
  import { sampleMotion } from "@opendata-ai/openchart-core";
@@ -986,22 +987,19 @@ function buildAnnotationTweens(svg, prevLayout, nextLayout, tweens) {
986
987
  tweenType: "annotation",
987
988
  kind: "enter",
988
989
  el,
989
- fromDx: 0,
990
- fromDy: 0,
991
990
  fromOpacity: 0,
992
991
  toOpacity: 1
993
992
  });
994
993
  continue;
995
994
  }
996
- const dx = (before.label?.x ?? 0) - (next[i].label?.x ?? 0);
997
- const dy = (before.label?.y ?? 0) - (next[i].label?.y ?? 0);
998
- if (dx === 0 && dy === 0) continue;
995
+ if (JSON.stringify(before) === JSON.stringify(next[i])) continue;
999
996
  tweens.push({
1000
997
  tweenType: "annotation",
1001
998
  kind: "update",
1002
999
  el,
1003
- fromDx: dx,
1004
- fromDy: dy
1000
+ before,
1001
+ after: next[i],
1002
+ background: nextLayout.theme.colors.neutral.surface
1005
1003
  });
1006
1004
  }
1007
1005
  }
@@ -1796,11 +1794,12 @@ function applyTweenState(tw, elapsed, update, exit, enterDelay, enterDuration) {
1796
1794
  break;
1797
1795
  }
1798
1796
  case "annotation": {
1799
- const dx = tw.fromDx * (1 - eased);
1800
- const dy = tw.fromDy * (1 - eased);
1801
- if (dx !== 0 || dy !== 0) {
1802
- tw.el.setAttribute("transform", `translate(${dx} ${dy})`);
1803
- }
1797
+ if (tw.before && tw.after)
1798
+ updateAnnotationGeometry(
1799
+ tw.el,
1800
+ interpolateAnnotationGeometry(tw.before, tw.after, eased),
1801
+ tw.background
1802
+ );
1804
1803
  break;
1805
1804
  }
1806
1805
  }
@@ -1892,6 +1891,7 @@ function snapTweenToFinal(tw) {
1892
1891
  tw.el.setAttribute(tw.attr, tw.to);
1893
1892
  break;
1894
1893
  case "annotation":
1894
+ if (tw.after) updateAnnotationGeometry(tw.el, tw.after, tw.background);
1895
1895
  tw.el.removeAttribute("transform");
1896
1896
  break;
1897
1897
  }
@@ -3654,6 +3654,394 @@ function createYouDrawIt(options) {
3654
3654
  };
3655
3655
  }
3656
3656
 
3657
+ // src/interactions/annotation-nudges.ts
3658
+ import { annotationSource as annotationSource3 } from "@opendata-ai/openchart-core";
3659
+ import {
3660
+ createAnnotationEditContext,
3661
+ inverseLabelPlacement,
3662
+ previewAnnotationMove
3663
+ } from "@opendata-ai/openchart-engine";
3664
+
3665
+ // src/interactions/selection.ts
3666
+ import { elementRef } from "@opendata-ai/openchart-core";
3667
+
3668
+ // src/interactions/edit-transactions.ts
3669
+ import { annotationSource } from "@opendata-ai/openchart-core";
3670
+ import { annotationSource as annotationSource2 } from "@opendata-ai/openchart-core";
3671
+ var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3672
+ function editToTransaction(spec, edit, revision) {
3673
+ const root = object(spec) ?? {};
3674
+ const source = annotationSource2(spec);
3675
+ const ops = [];
3676
+ const set = (path, value, expect) => ops.push({ op: "set", path, value, ...expect ? { expect } : {} });
3677
+ if (edit.type === "legend-toggle") return null;
3678
+ if (edit.type === "add") {
3679
+ ops.push({ op: "add", path: `${source.path}/-`, value: edit.annotation });
3680
+ } else if ("element" in edit && edit.element.type === "annotation") {
3681
+ const expectedPath = `${source.path}/${edit.element.index}`;
3682
+ if (edit.element.path !== void 0 && edit.element.path !== expectedPath && !edit.element.path.startsWith(`${expectedPath}/`))
3683
+ return null;
3684
+ const annotation = source.annotations[edit.element.index];
3685
+ if (!annotation || edit.element.id !== void 0 && annotation.id !== edit.element.id)
3686
+ return null;
3687
+ const authored = "annotation" in edit ? edit.annotation : annotation;
3688
+ const expected = {
3689
+ type: authored.type,
3690
+ text: authored.type === "text" ? authored.text : authored.label ?? "",
3691
+ ...authored.id !== void 0 ? { id: authored.id } : {}
3692
+ };
3693
+ if (authored.type !== "text") expected.text = void 0;
3694
+ const path = `${source.path}/${edit.element.index}`;
3695
+ switch (edit.type) {
3696
+ case "annotation":
3697
+ if (edit.anchor !== void 0) set(`${path}/anchor`, edit.anchor, expected);
3698
+ set(`${path}/offset`, edit.offset, expected);
3699
+ if (edit.responsive !== void 0) set(`${path}/responsive`, edit.responsive, expected);
3700
+ break;
3701
+ case "annotation-connector":
3702
+ set(
3703
+ `${path}/connectorOffset`,
3704
+ {
3705
+ ...authored.type === "text" ? authored.connectorOffset : {},
3706
+ [edit.endpoint]: edit.offset
3707
+ },
3708
+ expected
3709
+ );
3710
+ break;
3711
+ case "range-label":
3712
+ case "refline-label":
3713
+ set(`${path}/labelOffset`, edit.labelOffset, expected);
3714
+ break;
3715
+ case "annotation-anchor":
3716
+ set(`${path}/x`, edit.x, expected);
3717
+ set(`${path}/y`, edit.y, expected);
3718
+ break;
3719
+ case "text-edit":
3720
+ set(`${path}/${annotation.type === "text" ? "text" : "label"}`, edit.newText, expected);
3721
+ break;
3722
+ case "delete":
3723
+ ops.push({ op: "remove", path, expect: expected });
3724
+ break;
3725
+ }
3726
+ } else if (edit.type === "chrome" || "element" in edit && edit.element.type === "chrome") {
3727
+ const key = edit.type === "chrome" ? edit.key : edit.element.type === "chrome" ? edit.element.key : "title";
3728
+ const chrome = object(root.chrome);
3729
+ const sugar = !Object.hasOwn(chrome ?? {}, key) && Object.hasOwn(root, key);
3730
+ const path = sugar ? `/${key}` : `/chrome/${key}`;
3731
+ const old = sugar ? root[key] : chrome?.[key];
3732
+ if (edit.type === "delete") {
3733
+ ops.push({ op: "remove", path });
3734
+ } else {
3735
+ const text = edit.type === "text-edit" ? edit.newText : edit.type === "chrome" ? edit.text : "";
3736
+ const value = edit.type === "text-edit" && sugar && typeof old === "string" ? text : {
3737
+ ...object(old) ?? {},
3738
+ text,
3739
+ ...edit.type === "chrome" ? { offset: edit.offset } : {}
3740
+ };
3741
+ ops.push({ op: chrome || sugar ? "set" : "add", path, value });
3742
+ }
3743
+ } else if (edit.type === "legend") {
3744
+ ops.push({
3745
+ op: "add",
3746
+ path: "/legend",
3747
+ value: { ...object(root.legend) ?? {}, offset: edit.offset }
3748
+ });
3749
+ } else if (edit.type === "delete" && edit.element.type === "legend") {
3750
+ ops.push({
3751
+ op: "add",
3752
+ path: "/legend",
3753
+ value: { ...object(root.legend) ?? {}, show: false }
3754
+ });
3755
+ } else if (edit.type === "series-label") {
3756
+ const key = edit.series.replace(/~/g, "~0").replace(/\//g, "~1");
3757
+ ops.push({ op: "add", path: `/labels/offsets/${key}`, value: edit.offset });
3758
+ }
3759
+ return ops.length ? { ...revision !== void 0 ? { revision } : {}, ops } : null;
3760
+ }
3761
+
3762
+ // src/interactions/selection.ts
3763
+ function findElementByRef(svg, ref) {
3764
+ switch (ref.type) {
3765
+ case "annotation": {
3766
+ const matches = [...svg.querySelectorAll("[data-annotation-index]")].filter(
3767
+ (el) => Number(el.getAttribute("data-annotation-index")) === ref.index && (ref.id === void 0 || el.getAttribute("data-annotation-id") === ref.id) && (ref.path === void 0 || ref.path === (el.getAttribute("data-annotation-path") ?? `/annotations/${ref.index}`) || ref.path.startsWith(
3768
+ `${el.getAttribute("data-annotation-path") ?? `/annotations/${ref.index}`}/`
3769
+ )) && (ref.facetInstance === void 0 || el.getAttribute("data-facet-instance") === ref.facetInstance)
3770
+ );
3771
+ return matches[0] ?? null;
3772
+ }
3773
+ case "chrome":
3774
+ return svg.querySelector(`[data-chrome-key="${ref.key}"]`);
3775
+ case "series-label":
3776
+ return svg.querySelector(`.oc-mark-label[data-series="${ref.series}"]`);
3777
+ // The editable legend is the color legend. Excluding the size legend
3778
+ // explicitly rather than relying on it rendering second: `querySelector`
3779
+ // takes the first match, so a bare `.oc-legend` would silently retarget
3780
+ // (and drag) the wrong legend if render order ever changed.
3781
+ case "legend":
3782
+ return svg.querySelector(".oc-legend:not(.oc-legend--size)");
3783
+ case "legend-entry":
3784
+ return svg.querySelector(
3785
+ `.oc-legend:not(.oc-legend--size) [data-legend-index="${ref.index}"]`
3786
+ );
3787
+ }
3788
+ }
3789
+ function encodePointerToken(token) {
3790
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
3791
+ }
3792
+ function buildElementRef(element, _specAnnotations) {
3793
+ const annotationEl = element.closest("[data-annotation-index]");
3794
+ if (annotationEl) {
3795
+ const index = Number(annotationEl.getAttribute("data-annotation-index"));
3796
+ const id = annotationEl.getAttribute("data-annotation-id") ?? void 0;
3797
+ return {
3798
+ ...elementRef.annotation(
3799
+ index,
3800
+ id,
3801
+ annotationEl.getAttribute("data-annotation-path") ?? `/annotations/${index}`
3802
+ ),
3803
+ ...annotationEl.hasAttribute("data-facet-instance") ? { facetInstance: annotationEl.getAttribute("data-facet-instance") } : {}
3804
+ };
3805
+ }
3806
+ const chromeEl = element.closest("[data-chrome-key]");
3807
+ if (chromeEl) {
3808
+ const key = chromeEl.getAttribute("data-chrome-key");
3809
+ if (key) return elementRef.chrome(key, `/chrome/${key}`);
3810
+ }
3811
+ const seriesLabelEl = element.closest(".oc-mark-label[data-series]");
3812
+ if (seriesLabelEl) {
3813
+ const series = seriesLabelEl.getAttribute("data-series");
3814
+ if (series)
3815
+ return elementRef.seriesLabel(series, `/labels/offsets/${encodePointerToken(series)}`);
3816
+ }
3817
+ const legendEntryEl = element.closest("[data-legend-index]");
3818
+ if (legendEntryEl) {
3819
+ const index = Number(legendEntryEl.getAttribute("data-legend-index"));
3820
+ const series = legendEntryEl.getAttribute("data-legend-label") ?? "";
3821
+ return elementRef.legendEntry(series, index);
3822
+ }
3823
+ const legendEl = element.closest(".oc-legend");
3824
+ if (legendEl && !legendEl.classList.contains("oc-legend--size"))
3825
+ return elementRef.legend("/legend");
3826
+ return null;
3827
+ }
3828
+ function getEditableElements(spec, layout) {
3829
+ const refs = [];
3830
+ const chromeKeys = ["title", "subtitle", "source", "byline", "footer"];
3831
+ for (const key of chromeKeys) {
3832
+ if (layout.chrome[key]) {
3833
+ refs.push(elementRef.chrome(key));
3834
+ }
3835
+ }
3836
+ const { annotations, path } = annotationSource(spec);
3837
+ for (let i = 0; i < annotations.length; i++) {
3838
+ const panels = layout.facet?.panels;
3839
+ if (panels) {
3840
+ for (const panel of panels) {
3841
+ if (panel.annotations.some((a) => a.specIndex === i))
3842
+ refs.push({
3843
+ ...elementRef.annotation(i, annotations[i].id, `${path}/${i}`),
3844
+ facetInstance: panel.key
3845
+ });
3846
+ }
3847
+ } else if (layout.annotations.some((a, j) => (a.specIndex ?? j) === i))
3848
+ refs.push(elementRef.annotation(i, annotations[i].id, `${path}/${i}`));
3849
+ }
3850
+ const seriesLabels = [];
3851
+ for (const mark of layout.marks) {
3852
+ if (mark.type === "line" && mark.label?.visible && mark.seriesKey) {
3853
+ seriesLabels.push(mark.seriesKey);
3854
+ }
3855
+ }
3856
+ seriesLabels.sort();
3857
+ for (const series of seriesLabels) {
3858
+ refs.push(elementRef.seriesLabel(series));
3859
+ }
3860
+ if ("entries" in layout.legend && layout.legend.entries.length > 0) {
3861
+ refs.push(elementRef.legend());
3862
+ }
3863
+ return refs;
3864
+ }
3865
+ function isTextEditable(ref, specAnnotations) {
3866
+ if (ref.type === "chrome") return true;
3867
+ if (ref.type === "annotation") {
3868
+ const annotation = specAnnotations[ref.index];
3869
+ return annotation?.type === "text";
3870
+ }
3871
+ return false;
3872
+ }
3873
+ function getElementText(ref, spec) {
3874
+ if (ref.type === "chrome") {
3875
+ const chromeConfig = "chrome" in spec ? spec.chrome : void 0;
3876
+ const entry = chromeConfig?.[ref.key] ?? spec[ref.key];
3877
+ if (typeof entry === "string") return entry;
3878
+ if (typeof entry === "object" && entry !== null && "text" in entry) {
3879
+ return entry.text;
3880
+ }
3881
+ return null;
3882
+ }
3883
+ if (ref.type === "annotation") {
3884
+ const { annotations } = annotationSource(spec);
3885
+ const annotation = annotations[ref.index];
3886
+ if (annotation?.type === "text") return annotation.text ?? null;
3887
+ if (annotation?.label) return annotation.label;
3888
+ return null;
3889
+ }
3890
+ return null;
3891
+ }
3892
+ function refsEqual(a, b) {
3893
+ if (a === null || b === null) return a === b;
3894
+ if (a.type !== b.type) return false;
3895
+ switch (a.type) {
3896
+ case "annotation": {
3897
+ const bAnno = b;
3898
+ if (a.facetInstance !== bAnno.facetInstance) return false;
3899
+ if (a.id && bAnno.id) return a.id === bAnno.id && a.index === bAnno.index;
3900
+ return a.index === bAnno.index;
3901
+ }
3902
+ case "chrome":
3903
+ return a.key === b.key;
3904
+ case "series-label":
3905
+ return a.series === b.series;
3906
+ case "legend":
3907
+ return true;
3908
+ case "legend-entry": {
3909
+ const bEntry = b;
3910
+ return a.index === bEntry.index && a.series === bEntry.series;
3911
+ }
3912
+ }
3913
+ }
3914
+ function renderSelectionOverlay(svg, ref, layout) {
3915
+ const target = findElementByRef(svg, ref);
3916
+ if (!target) return null;
3917
+ const bbox = target.getBBox();
3918
+ const padding = 4;
3919
+ const accentColor = layout.theme.colors.categorical?.[0] ?? "#4f46e5";
3920
+ const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
3921
+ g.setAttribute("class", "oc-selection-overlay");
3922
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
3923
+ rect.setAttribute("x", String(bbox.x - padding));
3924
+ rect.setAttribute("y", String(bbox.y - padding));
3925
+ rect.setAttribute("width", String(bbox.width + padding * 2));
3926
+ rect.setAttribute("height", String(bbox.height + padding * 2));
3927
+ rect.setAttribute("rx", "3");
3928
+ rect.setAttribute("fill", "transparent");
3929
+ rect.setAttribute("stroke", accentColor);
3930
+ rect.setAttribute("stroke-width", "1.5");
3931
+ rect.setAttribute("pointer-events", "none");
3932
+ g.appendChild(rect);
3933
+ svg.appendChild(g);
3934
+ return g;
3935
+ }
3936
+ function createScreenReaderTable(layout, container) {
3937
+ const data = layout.a11y.dataTableFallback;
3938
+ if (!data || data.length === 0) return null;
3939
+ const wrapper = document.createElement("div");
3940
+ wrapper.className = "oc-sr-only";
3941
+ applySrOnlyStyles(wrapper);
3942
+ const table = document.createElement("table");
3943
+ table.setAttribute("role", "table");
3944
+ table.setAttribute("aria-label", `Data table: ${layout.a11y.altText}`);
3945
+ const totalRows = layout.a11y.totalRows;
3946
+ if (totalRows != null) {
3947
+ const caption = document.createElement("caption");
3948
+ caption.textContent = `Showing first ${(data.length - 1).toLocaleString()} of ${totalRows.toLocaleString()} rows; full data via CSV export`;
3949
+ table.appendChild(caption);
3950
+ }
3951
+ if (data.length > 0) {
3952
+ const thead = document.createElement("thead");
3953
+ const headerRow = document.createElement("tr");
3954
+ const headers = data[0];
3955
+ for (const header of headers) {
3956
+ const th = document.createElement("th");
3957
+ th.textContent = String(header ?? "");
3958
+ th.setAttribute("scope", "col");
3959
+ headerRow.appendChild(th);
3960
+ }
3961
+ thead.appendChild(headerRow);
3962
+ table.appendChild(thead);
3963
+ }
3964
+ if (data.length > 1) {
3965
+ const tbody = document.createElement("tbody");
3966
+ for (let i = 1; i < data.length; i++) {
3967
+ const tr = document.createElement("tr");
3968
+ const cells = data[i];
3969
+ for (const cell of cells) {
3970
+ const td = document.createElement("td");
3971
+ td.textContent = String(cell ?? "");
3972
+ tr.appendChild(td);
3973
+ }
3974
+ tbody.appendChild(tr);
3975
+ }
3976
+ table.appendChild(tbody);
3977
+ }
3978
+ wrapper.appendChild(table);
3979
+ container.appendChild(wrapper);
3980
+ return wrapper;
3981
+ }
3982
+
3983
+ // src/interactions/annotation-nudges.ts
3984
+ function createAnnotationNudges(read, emit) {
3985
+ let pending;
3986
+ let timer;
3987
+ const flush = () => {
3988
+ if (timer !== void 0) clearTimeout(timer);
3989
+ timer = void 0;
3990
+ const batch = pending;
3991
+ pending = void 0;
3992
+ if (!batch) return;
3993
+ const preview = previewAnnotationMove(batch.ctx, batch.ref, batch);
3994
+ const placement = inverseLabelPlacement(batch.ctx, batch.ref, preview.bounds);
3995
+ const { svg, layout } = read();
3996
+ const target = svg && findElementByRef(svg, batch.ref);
3997
+ if (target)
3998
+ updateAnnotationGeometry(target, batch.original, layout.theme.colors.neutral.surface);
3999
+ emit({
4000
+ type: "annotation",
4001
+ element: batch.ref,
4002
+ annotation: batch.authored,
4003
+ offset: placement.offset,
4004
+ anchor: placement.anchor,
4005
+ responsive: false
4006
+ });
4007
+ };
4008
+ const keydown = (event) => {
4009
+ if (!event.altKey || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key))
4010
+ return false;
4011
+ const { layout, spec, svg, selected } = read();
4012
+ if (!svg || selected?.type !== "annotation") return false;
4013
+ const authored = annotationSource3(spec).annotations[selected.index];
4014
+ if (authored?.type !== "text") return false;
4015
+ if (!pending) {
4016
+ const ctx = createAnnotationEditContext(layout, spec);
4017
+ try {
4018
+ previewAnnotationMove(ctx, selected, { dx: 0, dy: 0 });
4019
+ } catch {
4020
+ return false;
4021
+ }
4022
+ const annotations = selected.facetInstance !== void 0 ? layout.facet?.panels.find((panel) => panel.key === selected.facetInstance)?.annotations : layout.annotations;
4023
+ const original = annotations?.find((annotation) => annotation.specIndex === selected.index);
4024
+ if (!original) return false;
4025
+ pending = { ctx, ref: selected, authored, original, dx: 0, dy: 0 };
4026
+ }
4027
+ event.preventDefault();
4028
+ const step = event.shiftKey ? 10 : 1;
4029
+ pending.dx += event.key === "ArrowLeft" ? -step : event.key === "ArrowRight" ? step : 0;
4030
+ pending.dy += event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0;
4031
+ const target = findElementByRef(svg, pending.ref);
4032
+ if (target)
4033
+ updateAnnotationGeometry(
4034
+ target,
4035
+ previewAnnotationMove(pending.ctx, pending.ref, pending),
4036
+ layout.theme.colors.neutral.surface
4037
+ );
4038
+ if (timer !== void 0) clearTimeout(timer);
4039
+ timer = setTimeout(flush, 120);
4040
+ return true;
4041
+ };
4042
+ return { keydown, flush };
4043
+ }
4044
+
3657
4045
  // src/transition/reconcile.ts
3658
4046
  function identity(element, index) {
3659
4047
  for (const attr of ["data-key", "data-label-key", "data-endpoint-key", "data-tick-value"]) {
@@ -3727,7 +4115,12 @@ function sampleLayout(layout, snapshot) {
3727
4115
  }
3728
4116
 
3729
4117
  // src/mount.ts
3730
- import { cssTokenDefault, isGraphSpec, isLayerSpec } from "@opendata-ai/openchart-core";
4118
+ import {
4119
+ applyEditTransaction,
4120
+ cssTokenDefault,
4121
+ isGraphSpec,
4122
+ isLayerSpec
4123
+ } from "@opendata-ai/openchart-core";
3731
4124
  import {
3732
4125
  compileChart,
3733
4126
  compileLayer,
@@ -3864,14 +4257,22 @@ function wireChartEvents(svg, layout, specAnnotations, handlers) {
3864
4257
  }
3865
4258
  }
3866
4259
  if (handlers.onAnnotationClick) {
3867
- const annotationElements = svg.querySelectorAll(".oc-annotation");
4260
+ const annotationElements = svg.querySelectorAll(".oc-annotation, [data-note-spec-index]");
3868
4261
  for (let i = 0; i < annotationElements.length; i++) {
3869
4262
  const el = annotationElements[i];
3870
- const specAnnotation = specAnnotations[i];
4263
+ const noteIndex = el.getAttribute("data-note-spec-index");
4264
+ const ref = noteIndex !== null ? {
4265
+ type: "annotation",
4266
+ index: Number(noteIndex),
4267
+ id: el.getAttribute("data-note-id") ?? void 0,
4268
+ path: el.getAttribute("data-note-path") ?? `/annotations/${noteIndex}`
4269
+ } : buildElementRef(el, specAnnotations);
4270
+ if (!ref || ref.type !== "annotation") continue;
4271
+ const specAnnotation = specAnnotations[ref.index];
3871
4272
  if (!specAnnotation) continue;
3872
4273
  const handleClick = (e) => {
3873
4274
  const mouseEvent = e;
3874
- handlers.onAnnotationClick(specAnnotation, mouseEvent);
4275
+ handlers.onAnnotationClick(specAnnotation, mouseEvent, ref);
3875
4276
  };
3876
4277
  el.addEventListener("click", handleClick);
3877
4278
  cleanups.push(() => el.removeEventListener("click", handleClick));
@@ -4155,152 +4556,123 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager, emphasis) {
4155
4556
  // src/interactions/drag-handler.ts
4156
4557
  function createDragHandler(config) {
4157
4558
  const { element, svg, onMove, onEnd, setDragging, threshold = 3 } = config;
4158
- const cleanups = [];
4159
- let activeDocMouseMove = null;
4160
- let activeDocMouseUp = null;
4161
- let activeDocTouchMove = null;
4162
- let activeDocTouchEnd = null;
4163
- let activeDocTouchCancel = null;
4164
- function getScale2() {
4165
- const viewBox = svg.viewBox?.baseVal;
4166
- const svgRect = svg.getBoundingClientRect();
4167
- return {
4168
- scaleX: viewBox?.width && svgRect.width ? viewBox.width / svgRect.width : 1,
4169
- scaleY: viewBox?.height && svgRect.height ? viewBox.height / svgRect.height : 1
4170
- };
4171
- }
4559
+ let cancelActive;
4172
4560
  function startDrag(startX, startY) {
4561
+ cancelActive?.();
4562
+ config.onStart?.();
4173
4563
  setDragging(true);
4174
- const { scaleX, scaleY } = getScale2();
4564
+ const viewBox = svg.viewBox?.baseVal;
4565
+ const rect = svg.getBoundingClientRect();
4566
+ const scaleX = viewBox?.width && rect.width ? viewBox.width / rect.width : 1;
4567
+ const scaleY = viewBox?.height && rect.height ? viewBox.height / rect.height : 1;
4175
4568
  element.style.cursor = "grabbing";
4176
4569
  svg.style.userSelect = "none";
4177
- const handleMove = (clientX, clientY) => {
4178
- const dx = (clientX - startX) * scaleX;
4179
- const dy = (clientY - startY) * scaleY;
4180
- onMove(dx, dy);
4570
+ let frame;
4571
+ let dx = 0;
4572
+ let dy = 0;
4573
+ let active = true;
4574
+ const preview = () => {
4575
+ frame = void 0;
4576
+ if (active) onMove(dx, dy);
4181
4577
  };
4182
- const cleanupDocListeners = () => {
4183
- if (activeDocMouseMove) {
4184
- document.removeEventListener("mousemove", activeDocMouseMove);
4185
- activeDocMouseMove = null;
4186
- }
4187
- if (activeDocMouseUp) {
4188
- document.removeEventListener("mouseup", activeDocMouseUp);
4189
- activeDocMouseUp = null;
4190
- }
4191
- if (activeDocTouchMove) {
4192
- document.removeEventListener("touchmove", activeDocTouchMove);
4193
- activeDocTouchMove = null;
4194
- }
4195
- if (activeDocTouchEnd) {
4196
- document.removeEventListener("touchend", activeDocTouchEnd);
4197
- activeDocTouchEnd = null;
4198
- }
4199
- if (activeDocTouchCancel) {
4200
- document.removeEventListener("touchcancel", activeDocTouchCancel);
4201
- activeDocTouchCancel = null;
4202
- }
4578
+ const move = (x, y) => {
4579
+ dx = (x - startX) * scaleX;
4580
+ dy = (y - startY) * scaleY;
4581
+ if (!config.animationFrame) preview();
4582
+ else if (frame === void 0) frame = requestAnimationFrame(preview);
4203
4583
  };
4204
- const handleEnd = (clientX, clientY) => {
4205
- const dx = (clientX - startX) * scaleX;
4206
- const dy = (clientY - startY) * scaleY;
4207
- const moved = Math.abs(dx) > threshold || Math.abs(dy) > threshold;
4208
- onEnd(dx, dy, moved);
4209
- if (moved) {
4210
- element.addEventListener(
4211
- "click",
4212
- (clickE) => {
4213
- clickE.stopPropagation();
4214
- },
4215
- { capture: true, once: true }
4216
- );
4217
- }
4584
+ const detach = () => {
4585
+ active = false;
4586
+ if (frame !== void 0) cancelAnimationFrame(frame);
4587
+ frame = void 0;
4588
+ document.removeEventListener("mousemove", mouseMove);
4589
+ document.removeEventListener("mouseup", mouseUp);
4590
+ document.removeEventListener("touchmove", touchMove);
4591
+ document.removeEventListener("touchend", touchEnd);
4592
+ document.removeEventListener("touchcancel", cancel);
4593
+ document.removeEventListener("pointercancel", cancel);
4594
+ document.removeEventListener("keydown", keyDown, true);
4595
+ svg.removeEventListener("oc-cancel-drag", cancel);
4218
4596
  element.style.cursor = "grab";
4219
4597
  svg.style.userSelect = "";
4220
- cleanupDocListeners();
4598
+ cancelActive = void 0;
4599
+ };
4600
+ const cancel = () => {
4601
+ if (!active) return;
4602
+ detach();
4603
+ config.onCancel?.();
4221
4604
  setDragging(false);
4222
4605
  };
4223
- const onMouseMove = (moveEvent) => {
4224
- handleMove(moveEvent.clientX, moveEvent.clientY);
4606
+ const end = (x, y) => {
4607
+ if (!active) return;
4608
+ dx = (x - startX) * scaleX;
4609
+ dy = (y - startY) * scaleY;
4610
+ const moved = Math.abs(dx) > threshold || Math.abs(dy) > threshold;
4611
+ detach();
4612
+ onEnd(dx, dy, moved);
4613
+ if (moved)
4614
+ element.addEventListener("click", (event) => event.stopPropagation(), {
4615
+ capture: true,
4616
+ once: true
4617
+ });
4618
+ setDragging(false);
4225
4619
  };
4226
- const onMouseUp = (upEvent) => {
4227
- handleEnd(upEvent.clientX, upEvent.clientY);
4620
+ const mouseMove = (event) => move(event.clientX, event.clientY);
4621
+ const mouseUp = (event) => end(event.clientX, event.clientY);
4622
+ const touchMove = (event) => {
4623
+ if (!event.touches.length) return;
4624
+ event.preventDefault();
4625
+ move(event.touches[0].clientX, event.touches[0].clientY);
4228
4626
  };
4229
- document.addEventListener("mousemove", onMouseMove);
4230
- document.addEventListener("mouseup", onMouseUp);
4231
- activeDocMouseMove = onMouseMove;
4232
- activeDocMouseUp = onMouseUp;
4233
- const onTouchMove = (moveEvent) => {
4234
- if (moveEvent.touches.length > 0) {
4235
- moveEvent.preventDefault();
4236
- handleMove(moveEvent.touches[0].clientX, moveEvent.touches[0].clientY);
4237
- }
4627
+ const touchEnd = (event) => {
4628
+ const touch = event.changedTouches[0];
4629
+ end(touch?.clientX ?? startX, touch?.clientY ?? startY);
4238
4630
  };
4239
- const onTouchEnd = (endEvent) => {
4240
- const touch = endEvent.changedTouches[0];
4241
- if (touch) {
4242
- handleEnd(touch.clientX, touch.clientY);
4243
- } else {
4244
- handleEnd(startX, startY);
4631
+ const keyDown = (event) => {
4632
+ if (event.key === "Escape") {
4633
+ event.preventDefault();
4634
+ cancel();
4245
4635
  }
4246
4636
  };
4247
- document.addEventListener("touchmove", onTouchMove, { passive: false });
4248
- document.addEventListener("touchend", onTouchEnd);
4249
- document.addEventListener("touchcancel", onTouchEnd);
4250
- activeDocTouchMove = onTouchMove;
4251
- activeDocTouchEnd = onTouchEnd;
4252
- activeDocTouchCancel = onTouchEnd;
4253
- }
4254
- const handleMouseDown = (e) => {
4255
- const mouseEvent = e;
4256
- mouseEvent.preventDefault();
4257
- startDrag(mouseEvent.clientX, mouseEvent.clientY);
4637
+ document.addEventListener("mousemove", mouseMove);
4638
+ document.addEventListener("mouseup", mouseUp);
4639
+ document.addEventListener("touchmove", touchMove, { passive: false });
4640
+ document.addEventListener("touchend", touchEnd);
4641
+ document.addEventListener("touchcancel", cancel);
4642
+ document.addEventListener("pointercancel", cancel);
4643
+ document.addEventListener("keydown", keyDown, true);
4644
+ svg.addEventListener("oc-cancel-drag", cancel);
4645
+ cancelActive = cancel;
4646
+ }
4647
+ const mouseDown = (event) => {
4648
+ if (event.button !== 0) return;
4649
+ event.preventDefault();
4650
+ startDrag(event.clientX, event.clientY);
4258
4651
  };
4259
- const handleTouchStart = (e) => {
4260
- const touchEvent = e;
4261
- if (touchEvent.touches.length === 1) {
4262
- touchEvent.preventDefault();
4263
- startDrag(touchEvent.touches[0].clientX, touchEvent.touches[0].clientY);
4264
- }
4652
+ const touchStart = (event) => {
4653
+ if (event.touches.length !== 1) return;
4654
+ event.preventDefault();
4655
+ startDrag(event.touches[0].clientX, event.touches[0].clientY);
4265
4656
  };
4266
- element.addEventListener("mousedown", handleMouseDown);
4267
- element.addEventListener("touchstart", handleTouchStart, { passive: false });
4268
- cleanups.push(() => {
4269
- element.removeEventListener("mousedown", handleMouseDown);
4270
- element.removeEventListener("touchstart", handleTouchStart);
4271
- });
4657
+ element.addEventListener("mousedown", mouseDown);
4658
+ element.addEventListener("touchstart", touchStart, { passive: false });
4272
4659
  return () => {
4273
- for (const cleanup of cleanups) {
4274
- cleanup();
4275
- }
4276
- if (activeDocMouseMove) {
4277
- document.removeEventListener("mousemove", activeDocMouseMove);
4278
- activeDocMouseMove = null;
4279
- }
4280
- if (activeDocMouseUp) {
4281
- document.removeEventListener("mouseup", activeDocMouseUp);
4282
- activeDocMouseUp = null;
4283
- }
4284
- if (activeDocTouchMove) {
4285
- document.removeEventListener("touchmove", activeDocTouchMove);
4286
- activeDocTouchMove = null;
4287
- }
4288
- if (activeDocTouchEnd) {
4289
- document.removeEventListener("touchend", activeDocTouchEnd);
4290
- activeDocTouchEnd = null;
4291
- }
4292
- if (activeDocTouchCancel) {
4293
- document.removeEventListener("touchcancel", activeDocTouchCancel);
4294
- activeDocTouchCancel = null;
4295
- }
4296
- svg.style.userSelect = "";
4660
+ element.removeEventListener("mousedown", mouseDown);
4661
+ element.removeEventListener("touchstart", touchStart);
4662
+ cancelActive?.();
4297
4663
  };
4298
4664
  }
4299
4665
 
4300
4666
  // src/interactions/editing-drags.ts
4301
4667
  import {
4302
- elementRef
4668
+ elementRef as elementRef2
4303
4669
  } from "@opendata-ai/openchart-core";
4670
+ import {
4671
+ createAnnotationEditContext as createAnnotationEditContext2,
4672
+ inverseLabelPlacement as inverseLabelPlacement2,
4673
+ previewAnnotationMove as previewAnnotationMove2,
4674
+ previewConnectorOffset
4675
+ } from "@opendata-ai/openchart-engine";
4304
4676
 
4305
4677
  // src/interactions/invert.ts
4306
4678
  function invertScale(invert, pixel) {
@@ -4345,212 +4717,153 @@ function invertScale(invert, pixel) {
4345
4717
  }
4346
4718
 
4347
4719
  // src/interactions/editing-drags.ts
4348
- function wireAnnotationDrag(svg, specAnnotations, onAnnotationEdit, onEdit, setDragging) {
4349
- const annotationElements = svg.querySelectorAll(".oc-annotation-text");
4720
+ function wireAnnotationDrag(svg, specAnnotations, onAnnotationEdit, onEdit, setDragging, layout, spec) {
4350
4721
  const cleanups = [];
4351
- for (const el of annotationElements) {
4352
- const indexStr = el.getAttribute("data-annotation-index");
4353
- if (indexStr === null) continue;
4354
- const index = Number(indexStr);
4355
- const specAnnotation = specAnnotations[index];
4356
- if (!specAnnotation || specAnnotation.type !== "text") continue;
4357
- const textAnnotation = specAnnotation;
4358
- const annotationG = el;
4359
- annotationG.style.cursor = "grab";
4360
- const connectorLine = annotationG.querySelector("line.oc-annotation-connector");
4361
- const origX2 = connectorLine ? Number(connectorLine.getAttribute("x2")) : 0;
4362
- const origY2 = connectorLine ? Number(connectorLine.getAttribute("y2")) : 0;
4363
- const curvedPath = annotationG.querySelector("path.oc-annotation-connector");
4364
- const arrowhead = annotationG.querySelector("polyline.oc-annotation-arrowhead");
4365
- const hasCurvedConnector = curvedPath !== null;
4366
- const origDx = textAnnotation.offset?.dx ?? 0;
4367
- const origDy = textAnnotation.offset?.dy ?? 0;
4368
- const cleanup = createDragHandler({
4369
- element: annotationG,
4370
- svg,
4371
- onMove: (dx, dy) => {
4372
- annotationG.setAttribute("transform", `translate(${dx}, ${dy})`);
4373
- if (connectorLine && !hasCurvedConnector) {
4374
- connectorLine.setAttribute("x2", String(origX2 - dx));
4375
- connectorLine.setAttribute("y2", String(origY2 - dy));
4376
- }
4377
- if (hasCurvedConnector) {
4378
- if (curvedPath) curvedPath.setAttribute("display", "none");
4379
- if (arrowhead) arrowhead.setAttribute("display", "none");
4380
- }
4381
- },
4382
- onEnd: (dx, dy, moved) => {
4383
- annotationG.removeAttribute("transform");
4384
- if (connectorLine && !hasCurvedConnector) {
4385
- connectorLine.setAttribute("x2", String(origX2));
4386
- connectorLine.setAttribute("y2", String(origY2));
4387
- }
4388
- if (hasCurvedConnector) {
4389
- if (curvedPath) curvedPath.removeAttribute("display");
4390
- if (arrowhead) arrowhead.removeAttribute("display");
4391
- }
4392
- if (moved) {
4393
- const newOffset = {
4394
- dx: origDx + dx,
4395
- dy: origDy + dy
4396
- };
4397
- onAnnotationEdit?.(textAnnotation, newOffset);
4722
+ const ctx = createAnnotationEditContext2(layout, spec);
4723
+ for (const group of svg.querySelectorAll(".oc-annotation-text")) {
4724
+ const ref = buildElementRef(group, specAnnotations);
4725
+ if (ref?.type !== "annotation") continue;
4726
+ const authored = specAnnotations[ref.index];
4727
+ if (authored?.type !== "text") continue;
4728
+ let initial;
4729
+ try {
4730
+ initial = previewAnnotationMove2(ctx, ref, { dx: 0, dy: 0 });
4731
+ } catch {
4732
+ continue;
4733
+ }
4734
+ if (!initial.label?.visible || initial.footnoteIndex !== void 0) continue;
4735
+ const original = initial;
4736
+ const restore = () => updateAnnotationGeometry(group, original, layout.theme.colors.neutral.surface);
4737
+ group.style.cursor = "grab";
4738
+ cleanups.push(
4739
+ createDragHandler({
4740
+ element: group,
4741
+ svg,
4742
+ setDragging,
4743
+ animationFrame: true,
4744
+ onMove: (dx, dy) => updateAnnotationGeometry(
4745
+ group,
4746
+ previewAnnotationMove2(ctx, ref, { dx, dy }),
4747
+ layout.theme.colors.neutral.surface
4748
+ ),
4749
+ onCancel: restore,
4750
+ onEnd: (dx, dy, moved) => {
4751
+ restore();
4752
+ if (!moved) return;
4753
+ const preview = previewAnnotationMove2(ctx, ref, { dx, dy });
4754
+ const placement = inverseLabelPlacement2(ctx, ref, preview.bounds);
4755
+ onAnnotationEdit?.(authored, {
4756
+ dx: Math.round(
4757
+ ((authored.offset?.dx ?? 0) + preview.label.x - original.label.x) * 1e10
4758
+ ) / 1e10,
4759
+ dy: Math.round(
4760
+ ((authored.offset?.dy ?? 0) + preview.label.y - original.label.y) * 1e10
4761
+ ) / 1e10
4762
+ });
4398
4763
  onEdit?.({
4399
4764
  type: "annotation",
4400
- element: elementRef.annotation(
4401
- index,
4402
- textAnnotation.id,
4403
- `/annotations/${index}/offset`
4404
- ),
4405
- annotation: textAnnotation,
4406
- offset: newOffset
4765
+ element: { ...ref, path: `${ref.path}/offset` },
4766
+ annotation: authored,
4767
+ offset: placement.offset,
4768
+ anchor: placement.anchor,
4769
+ responsive: false
4407
4770
  });
4408
4771
  }
4409
- },
4410
- setDragging
4411
- });
4412
- cleanups.push(cleanup);
4413
- }
4414
- return () => {
4415
- for (const cleanup of cleanups) {
4416
- cleanup();
4417
- }
4418
- };
4419
- }
4420
- function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
4421
- const SVG_NS4 = "http://www.w3.org/2000/svg";
4422
- const cleanups = [];
4423
- const annotationGroups = svg.querySelectorAll(".oc-annotation-text");
4424
- for (const el of annotationGroups) {
4425
- const annotationG = el;
4426
- const indexStr = annotationG.getAttribute("data-annotation-index");
4427
- if (indexStr === null) continue;
4428
- const index = Number(indexStr);
4429
- const specAnnotation = specAnnotations[index];
4430
- if (!specAnnotation || specAnnotation.type !== "text") continue;
4431
- const textAnnotation = specAnnotation;
4432
- const connectorLine = annotationG.querySelector("line.oc-annotation-connector");
4433
- const curvedPath = annotationG.querySelector("path.oc-annotation-connector");
4434
- if (!connectorLine && !curvedPath) continue;
4435
- let fromX, fromY, toX, toY;
4436
- if (connectorLine) {
4437
- fromX = Number(connectorLine.getAttribute("x1")) || 0;
4438
- fromY = Number(connectorLine.getAttribute("y1")) || 0;
4439
- toX = Number(connectorLine.getAttribute("x2")) || 0;
4440
- toY = Number(connectorLine.getAttribute("y2")) || 0;
4441
- } else {
4442
- const pathD = curvedPath.getAttribute("d") ?? "";
4443
- const mMatch = pathD.match(/M\s*([\d.e+-]+)\s+([\d.e+-]+)/);
4444
- fromX = mMatch ? Number(mMatch[1]) : 0;
4445
- fromY = mMatch ? Number(mMatch[2]) : 0;
4446
- const arrowhead = annotationG.querySelector("polyline.oc-annotation-arrowhead");
4447
- const points = arrowhead?.getAttribute("points")?.split(" ") ?? [];
4448
- const [px, py] = (points[1] ?? "0,0").split(",");
4449
- toX = Number(px) || 0;
4450
- toY = Number(py) || 0;
4451
- }
4452
- const endpoints = [
4453
- { name: "from", cx: fromX, cy: fromY },
4454
- { name: "to", cx: toX, cy: toY }
4455
- ];
4456
- const createdHandles = [];
4457
- for (const ep of endpoints) {
4458
- if (!Number.isFinite(ep.cx) || !Number.isFinite(ep.cy)) continue;
4459
- const handleEl = document.createElementNS(SVG_NS4, "circle");
4460
- handleEl.setAttribute("class", "oc-connector-handle");
4461
- handleEl.setAttribute("data-endpoint", ep.name);
4462
- handleEl.setAttribute("cx", String(ep.cx));
4463
- handleEl.setAttribute("cy", String(ep.cy));
4464
- handleEl.setAttribute("r", "4");
4465
- handleEl.setAttribute("opacity", "0");
4466
- handleEl.setAttribute("fill", "currentColor");
4467
- handleEl.setAttribute("stroke", "currentColor");
4468
- annotationG.appendChild(handleEl);
4469
- createdHandles.push(handleEl);
4470
- const origCx = ep.cx;
4471
- const origCy = ep.cy;
4472
- const stopProp = (e) => {
4473
- e.stopPropagation();
4474
- };
4475
- handleEl.addEventListener("mousedown", stopProp);
4476
- handleEl.addEventListener("touchstart", stopProp);
4477
- cleanups.push(() => {
4478
- handleEl.removeEventListener("mousedown", stopProp);
4479
- handleEl.removeEventListener("touchstart", stopProp);
4480
- });
4481
- const cleanup = createDragHandler({
4482
- element: handleEl,
4483
- svg,
4484
- onMove: (dx, dy) => {
4485
- handleEl.setAttribute("cx", String(origCx + dx));
4486
- handleEl.setAttribute("cy", String(origCy + dy));
4487
- if (connectorLine) {
4488
- if (ep.name === "from") {
4489
- connectorLine.setAttribute("x1", String(origCx + dx));
4490
- connectorLine.setAttribute("y1", String(origCy + dy));
4491
- } else {
4492
- connectorLine.setAttribute("x2", String(origCx + dx));
4493
- connectorLine.setAttribute("y2", String(origCy + dy));
4494
- }
4495
- }
4496
- },
4497
- onEnd: (dx, dy, moved) => {
4498
- handleEl.setAttribute("cx", String(origCx));
4499
- handleEl.setAttribute("cy", String(origCy));
4500
- if (connectorLine) {
4501
- if (ep.name === "from") {
4502
- connectorLine.setAttribute("x1", String(origCx));
4503
- connectorLine.setAttribute("y1", String(origCy));
4504
- } else {
4505
- connectorLine.setAttribute("x2", String(origCx));
4506
- connectorLine.setAttribute("y2", String(origCy));
4507
- }
4508
- }
4509
- if (moved) {
4510
- const existingOffset = textAnnotation.connectorOffset?.[ep.name];
4511
- const origEndDx = existingOffset?.dx ?? 0;
4512
- const origEndDy = existingOffset?.dy ?? 0;
4772
+ })
4773
+ );
4774
+ }
4775
+ return () => {
4776
+ for (const cleanup of cleanups) cleanup();
4777
+ };
4778
+ }
4779
+ function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging, layout, spec) {
4780
+ const cleanups = [];
4781
+ const ctx = createAnnotationEditContext2(layout, spec);
4782
+ for (const group of svg.querySelectorAll(".oc-annotation-text")) {
4783
+ const ref = buildElementRef(group, specAnnotations);
4784
+ if (ref?.type !== "annotation") continue;
4785
+ const authored = specAnnotations[ref.index];
4786
+ if (authored?.type !== "text") continue;
4787
+ let original;
4788
+ try {
4789
+ original = previewAnnotationMove2(ctx, ref, { dx: 0, dy: 0 });
4790
+ } catch {
4791
+ continue;
4792
+ }
4793
+ const connector = original.label?.connector;
4794
+ if (!connector || original.footnoteIndex !== void 0) continue;
4795
+ const handles = [];
4796
+ for (const end of ["from", "to"]) {
4797
+ const point = connector[end];
4798
+ const handle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
4799
+ handle.setAttribute("class", "oc-connector-handle");
4800
+ handle.setAttribute("data-endpoint", end);
4801
+ handle.setAttribute("cx", String(point.x));
4802
+ handle.setAttribute("cy", String(point.y));
4803
+ handle.setAttribute("r", "4");
4804
+ handle.setAttribute("opacity", "0");
4805
+ handle.setAttribute("fill", "currentColor");
4806
+ handle.setAttribute("stroke", "currentColor");
4807
+ handle.style.cursor = "grab";
4808
+ group.appendChild(handle);
4809
+ handles.push(handle);
4810
+ const stop = (event) => event.stopPropagation();
4811
+ handle.addEventListener("mousedown", stop);
4812
+ handle.addEventListener("touchstart", stop);
4813
+ const restore = () => {
4814
+ updateAnnotationGeometry(group, original, layout.theme.colors.neutral.surface);
4815
+ handle.setAttribute("cx", String(point.x));
4816
+ handle.setAttribute("cy", String(point.y));
4817
+ };
4818
+ cleanups.push(
4819
+ createDragHandler({
4820
+ element: handle,
4821
+ svg,
4822
+ setDragging,
4823
+ animationFrame: true,
4824
+ onMove: (dx, dy) => {
4825
+ const preview = previewConnectorOffset(ctx, ref, end, { dx, dy });
4826
+ updateAnnotationGeometry(group, preview, layout.theme.colors.neutral.surface);
4827
+ const position = preview.label?.connector?.[end] ?? point;
4828
+ handle.setAttribute("cx", String(position.x));
4829
+ handle.setAttribute("cy", String(position.y));
4830
+ },
4831
+ onCancel: restore,
4832
+ onEnd: (dx, dy, moved) => {
4833
+ restore();
4834
+ if (!moved) return;
4835
+ const previous = authored.connectorOffset?.[end];
4513
4836
  onEdit({
4514
4837
  type: "annotation-connector",
4515
- element: elementRef.annotation(
4516
- index,
4517
- textAnnotation.id,
4518
- `/annotations/${index}/connectorOffset/${ep.name}`
4519
- ),
4520
- annotation: textAnnotation,
4521
- endpoint: ep.name,
4522
- offset: { dx: origEndDx + dx, dy: origEndDy + dy }
4838
+ element: { ...ref, path: `${ref.path}/connectorOffset/${end}` },
4839
+ annotation: authored,
4840
+ endpoint: end,
4841
+ offset: { dx: (previous?.dx ?? 0) + dx, dy: (previous?.dy ?? 0) + dy }
4523
4842
  });
4524
4843
  }
4525
- },
4526
- setDragging
4844
+ })
4845
+ );
4846
+ cleanups.push(() => {
4847
+ handle.removeEventListener("mousedown", stop);
4848
+ handle.removeEventListener("touchstart", stop);
4849
+ handle.remove();
4527
4850
  });
4528
- cleanups.push(cleanup);
4529
4851
  }
4530
- const showHandles = () => {
4531
- for (const h of createdHandles) {
4532
- h.setAttribute("opacity", "0.6");
4533
- }
4534
- };
4535
- const hideHandles = () => {
4536
- for (const h of createdHandles) {
4537
- h.setAttribute("opacity", "0");
4538
- }
4539
- };
4540
- annotationG.addEventListener("mouseenter", showHandles);
4541
- annotationG.addEventListener("mouseleave", hideHandles);
4852
+ const show = () => handles.forEach((handle) => {
4853
+ handle.setAttribute("opacity", "0.6");
4854
+ });
4855
+ const hide = () => handles.forEach((handle) => {
4856
+ handle.setAttribute("opacity", "0");
4857
+ });
4858
+ group.addEventListener("mouseenter", show);
4859
+ group.addEventListener("mouseleave", hide);
4542
4860
  cleanups.push(() => {
4543
- annotationG.removeEventListener("mouseenter", showHandles);
4544
- annotationG.removeEventListener("mouseleave", hideHandles);
4545
- for (const h of createdHandles) {
4546
- h.remove();
4547
- }
4861
+ group.removeEventListener("mouseenter", show);
4862
+ group.removeEventListener("mouseleave", hide);
4548
4863
  });
4549
4864
  }
4550
4865
  return () => {
4551
- for (const cleanup of cleanups) {
4552
- cleanup();
4553
- }
4866
+ for (const cleanup of cleanups) cleanup();
4554
4867
  };
4555
4868
  }
4556
4869
  function wireAnnotationLabelDrag(svg, specAnnotations, onEdit, setDragging) {
@@ -4581,16 +4894,19 @@ function wireAnnotationLabelDrag(svg, specAnnotations, onEdit, setDragging) {
4581
4894
  onMove: (dx, dy) => {
4582
4895
  labelEl.style.transform = `translate(${dx}px, ${dy}px)`;
4583
4896
  },
4897
+ onCancel: () => {
4898
+ labelEl.style.transform = "";
4899
+ },
4584
4900
  onEnd: (dx, dy, moved) => {
4585
4901
  labelEl.style.transform = "";
4586
4902
  if (moved) {
4587
4903
  if (isRange) {
4588
4904
  onEdit({
4589
4905
  type: "range-label",
4590
- element: elementRef.annotation(
4906
+ element: elementRef2.annotation(
4591
4907
  index,
4592
4908
  specAnnotation.id,
4593
- `/annotations/${index}/labelOffset`
4909
+ `${annotationG.getAttribute("data-annotation-path") ?? `/annotations/${index}`}/labelOffset`
4594
4910
  ),
4595
4911
  annotation: specAnnotation,
4596
4912
  labelOffset: { dx: origLabelDx + dx, dy: origLabelDy + dy }
@@ -4598,10 +4914,10 @@ function wireAnnotationLabelDrag(svg, specAnnotations, onEdit, setDragging) {
4598
4914
  } else {
4599
4915
  onEdit({
4600
4916
  type: "refline-label",
4601
- element: elementRef.annotation(
4917
+ element: elementRef2.annotation(
4602
4918
  index,
4603
4919
  specAnnotation.id,
4604
- `/annotations/${index}/labelOffset`
4920
+ `${annotationG.getAttribute("data-annotation-path") ?? `/annotations/${index}`}/labelOffset`
4605
4921
  ),
4606
4922
  annotation: specAnnotation,
4607
4923
  labelOffset: { dx: origLabelDx + dx, dy: origLabelDy + dy }
@@ -4639,6 +4955,9 @@ function wireChromeDrag(svg, spec, onEdit, setDragging) {
4639
4955
  onMove: (dx, dy) => {
4640
4956
  textEl.style.transform = `translate(${dx}px, ${dy}px)`;
4641
4957
  },
4958
+ onCancel: () => {
4959
+ textEl.style.transform = "";
4960
+ },
4642
4961
  onEnd: (dx, dy, moved) => {
4643
4962
  textEl.style.transform = "";
4644
4963
  if (moved) {
@@ -4676,6 +4995,9 @@ function wireLegendDrag(svg, spec, onEdit, setDragging) {
4676
4995
  onMove: (dx, dy) => {
4677
4996
  legendG.style.transform = `translate(${dx}px, ${dy}px)`;
4678
4997
  },
4998
+ onCancel: () => {
4999
+ legendG.style.transform = "";
5000
+ },
4679
5001
  onEnd: (dx, dy, moved) => {
4680
5002
  legendG.style.transform = "";
4681
5003
  if (moved) {
@@ -4710,6 +5032,9 @@ function wireSeriesLabelDrag(svg, spec, onEdit, setDragging) {
4710
5032
  onMove: (dx, dy) => {
4711
5033
  labelEl.style.transform = `translate(${dx}px, ${dy}px)`;
4712
5034
  },
5035
+ onCancel: () => {
5036
+ labelEl.style.transform = "";
5037
+ },
4713
5038
  onEnd: (dx, dy, moved) => {
4714
5039
  labelEl.style.transform = "";
4715
5040
  if (moved) {
@@ -4774,6 +5099,10 @@ function wireAnchorDrag(svg, specAnnotations, layout, onEdit, setDragging) {
4774
5099
  handleEl.setAttribute("cx", String(anchorX + dx));
4775
5100
  handleEl.setAttribute("cy", String(anchorY + dy));
4776
5101
  },
5102
+ onCancel: () => {
5103
+ handleEl.setAttribute("cx", String(anchorX));
5104
+ handleEl.setAttribute("cy", String(anchorY));
5105
+ },
4777
5106
  onEnd: (dx, dy, moved) => {
4778
5107
  handleEl.setAttribute("cx", String(anchorX));
4779
5108
  handleEl.setAttribute("cy", String(anchorY));
@@ -4782,7 +5111,11 @@ function wireAnchorDrag(svg, specAnnotations, layout, onEdit, setDragging) {
4782
5111
  const newY = invertScale(layout.yInvert, anchorY + dy);
4783
5112
  onEdit({
4784
5113
  type: "annotation-anchor",
4785
- element: elementRef.annotation(index, textAnnotation.id, `/annotations/${index}`),
5114
+ element: elementRef2.annotation(
5115
+ index,
5116
+ textAnnotation.id,
5117
+ annotationG.getAttribute("data-annotation-path") ?? `/annotations/${index}`
5118
+ ),
4786
5119
  annotation: textAnnotation,
4787
5120
  x: newX,
4788
5121
  y: newY
@@ -5119,210 +5452,6 @@ function wireLegendInteraction(svg, _layout, toggleSeries, onLegendToggle, onEdi
5119
5452
  };
5120
5453
  }
5121
5454
 
5122
- // src/interactions/selection.ts
5123
- import { elementRef as elementRef2 } from "@opendata-ai/openchart-core";
5124
- function findElementByRef(svg, ref) {
5125
- switch (ref.type) {
5126
- case "annotation": {
5127
- if (ref.id) {
5128
- const byId = svg.querySelector(`[data-annotation-id="${ref.id}"]`);
5129
- if (byId) return byId;
5130
- }
5131
- return svg.querySelector(`[data-annotation-index="${ref.index}"]`);
5132
- }
5133
- case "chrome":
5134
- return svg.querySelector(`[data-chrome-key="${ref.key}"]`);
5135
- case "series-label":
5136
- return svg.querySelector(`.oc-mark-label[data-series="${ref.series}"]`);
5137
- // The editable legend is the color legend. Excluding the size legend
5138
- // explicitly rather than relying on it rendering second: `querySelector`
5139
- // takes the first match, so a bare `.oc-legend` would silently retarget
5140
- // (and drag) the wrong legend if render order ever changed.
5141
- case "legend":
5142
- return svg.querySelector(".oc-legend:not(.oc-legend--size)");
5143
- case "legend-entry":
5144
- return svg.querySelector(
5145
- `.oc-legend:not(.oc-legend--size) [data-legend-index="${ref.index}"]`
5146
- );
5147
- }
5148
- }
5149
- function encodePointerToken(token) {
5150
- return token.replace(/~/g, "~0").replace(/\//g, "~1");
5151
- }
5152
- function buildElementRef(element, _specAnnotations) {
5153
- const annotationEl = element.closest("[data-annotation-index]");
5154
- if (annotationEl) {
5155
- const index = Number(annotationEl.getAttribute("data-annotation-index"));
5156
- const id = annotationEl.getAttribute("data-annotation-id") ?? void 0;
5157
- return elementRef2.annotation(index, id, `/annotations/${index}`);
5158
- }
5159
- const chromeEl = element.closest("[data-chrome-key]");
5160
- if (chromeEl) {
5161
- const key = chromeEl.getAttribute("data-chrome-key");
5162
- if (key) return elementRef2.chrome(key, `/chrome/${key}`);
5163
- }
5164
- const seriesLabelEl = element.closest(".oc-mark-label[data-series]");
5165
- if (seriesLabelEl) {
5166
- const series = seriesLabelEl.getAttribute("data-series");
5167
- if (series)
5168
- return elementRef2.seriesLabel(series, `/labels/offsets/${encodePointerToken(series)}`);
5169
- }
5170
- const legendEntryEl = element.closest("[data-legend-index]");
5171
- if (legendEntryEl) {
5172
- const index = Number(legendEntryEl.getAttribute("data-legend-index"));
5173
- const series = legendEntryEl.getAttribute("data-legend-label") ?? "";
5174
- return elementRef2.legendEntry(series, index);
5175
- }
5176
- const legendEl = element.closest(".oc-legend");
5177
- if (legendEl && !legendEl.classList.contains("oc-legend--size"))
5178
- return elementRef2.legend("/legend");
5179
- return null;
5180
- }
5181
- function getEditableElements(spec, layout) {
5182
- const refs = [];
5183
- const chromeKeys = ["title", "subtitle", "source", "byline", "footer"];
5184
- for (const key of chromeKeys) {
5185
- if (layout.chrome[key]) {
5186
- refs.push(elementRef2.chrome(key));
5187
- }
5188
- }
5189
- const annotations = "annotations" in spec && Array.isArray(spec.annotations) ? spec.annotations : [];
5190
- for (let i = 0; i < annotations.length; i++) {
5191
- refs.push(elementRef2.annotation(i, annotations[i].id));
5192
- }
5193
- const seriesLabels = [];
5194
- for (const mark of layout.marks) {
5195
- if (mark.type === "line" && mark.label?.visible && mark.seriesKey) {
5196
- seriesLabels.push(mark.seriesKey);
5197
- }
5198
- }
5199
- seriesLabels.sort();
5200
- for (const series of seriesLabels) {
5201
- refs.push(elementRef2.seriesLabel(series));
5202
- }
5203
- if ("entries" in layout.legend && layout.legend.entries.length > 0) {
5204
- refs.push(elementRef2.legend());
5205
- }
5206
- return refs;
5207
- }
5208
- function isTextEditable(ref, specAnnotations) {
5209
- if (ref.type === "chrome") return true;
5210
- if (ref.type === "annotation") {
5211
- const annotation = specAnnotations[ref.index];
5212
- return annotation?.type === "text";
5213
- }
5214
- return false;
5215
- }
5216
- function getElementText(ref, spec) {
5217
- if (ref.type === "chrome") {
5218
- const chromeConfig = "chrome" in spec ? spec.chrome : void 0;
5219
- if (!chromeConfig) return null;
5220
- const entry = chromeConfig[ref.key];
5221
- if (typeof entry === "string") return entry;
5222
- if (typeof entry === "object" && entry !== null && "text" in entry) {
5223
- return entry.text;
5224
- }
5225
- return null;
5226
- }
5227
- if (ref.type === "annotation") {
5228
- const annotations = "annotations" in spec && Array.isArray(spec.annotations) ? spec.annotations : [];
5229
- const annotation = annotations[ref.index];
5230
- if (annotation?.type === "text") return annotation.text ?? null;
5231
- if (annotation?.label) return annotation.label;
5232
- return null;
5233
- }
5234
- return null;
5235
- }
5236
- function refsEqual(a, b) {
5237
- if (a === null || b === null) return a === b;
5238
- if (a.type !== b.type) return false;
5239
- switch (a.type) {
5240
- case "annotation": {
5241
- const bAnno = b;
5242
- if (a.id && bAnno.id) return a.id === bAnno.id;
5243
- return a.index === bAnno.index;
5244
- }
5245
- case "chrome":
5246
- return a.key === b.key;
5247
- case "series-label":
5248
- return a.series === b.series;
5249
- case "legend":
5250
- return true;
5251
- case "legend-entry": {
5252
- const bEntry = b;
5253
- return a.index === bEntry.index && a.series === bEntry.series;
5254
- }
5255
- }
5256
- }
5257
- function renderSelectionOverlay(svg, ref, layout) {
5258
- const target = findElementByRef(svg, ref);
5259
- if (!target) return null;
5260
- const bbox = target.getBBox();
5261
- const padding = 4;
5262
- const accentColor = layout.theme.colors.categorical?.[0] ?? "#4f46e5";
5263
- const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
5264
- g.setAttribute("class", "oc-selection-overlay");
5265
- const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5266
- rect.setAttribute("x", String(bbox.x - padding));
5267
- rect.setAttribute("y", String(bbox.y - padding));
5268
- rect.setAttribute("width", String(bbox.width + padding * 2));
5269
- rect.setAttribute("height", String(bbox.height + padding * 2));
5270
- rect.setAttribute("rx", "3");
5271
- rect.setAttribute("fill", "transparent");
5272
- rect.setAttribute("stroke", accentColor);
5273
- rect.setAttribute("stroke-width", "1.5");
5274
- rect.setAttribute("pointer-events", "none");
5275
- g.appendChild(rect);
5276
- svg.appendChild(g);
5277
- return g;
5278
- }
5279
- function createScreenReaderTable(layout, container) {
5280
- const data = layout.a11y.dataTableFallback;
5281
- if (!data || data.length === 0) return null;
5282
- const wrapper = document.createElement("div");
5283
- wrapper.className = "oc-sr-only";
5284
- applySrOnlyStyles(wrapper);
5285
- const table = document.createElement("table");
5286
- table.setAttribute("role", "table");
5287
- table.setAttribute("aria-label", `Data table: ${layout.a11y.altText}`);
5288
- const totalRows = layout.a11y.totalRows;
5289
- if (totalRows != null) {
5290
- const caption = document.createElement("caption");
5291
- caption.textContent = `Showing first ${(data.length - 1).toLocaleString()} of ${totalRows.toLocaleString()} rows; full data via CSV export`;
5292
- table.appendChild(caption);
5293
- }
5294
- if (data.length > 0) {
5295
- const thead = document.createElement("thead");
5296
- const headerRow = document.createElement("tr");
5297
- const headers = data[0];
5298
- for (const header of headers) {
5299
- const th = document.createElement("th");
5300
- th.textContent = String(header ?? "");
5301
- th.setAttribute("scope", "col");
5302
- headerRow.appendChild(th);
5303
- }
5304
- thead.appendChild(headerRow);
5305
- table.appendChild(thead);
5306
- }
5307
- if (data.length > 1) {
5308
- const tbody = document.createElement("tbody");
5309
- for (let i = 1; i < data.length; i++) {
5310
- const tr = document.createElement("tr");
5311
- const cells = data[i];
5312
- for (const cell of cells) {
5313
- const td = document.createElement("td");
5314
- td.textContent = String(cell ?? "");
5315
- tr.appendChild(td);
5316
- }
5317
- tbody.appendChild(tr);
5318
- }
5319
- table.appendChild(tbody);
5320
- }
5321
- wrapper.appendChild(table);
5322
- container.appendChild(wrapper);
5323
- return wrapper;
5324
- }
5325
-
5326
5455
  // src/interactions/tooltip-events.ts
5327
5456
  function wireTooltipEvents(svg, tooltipDescriptors, tooltipManager, emphasis) {
5328
5457
  const markElements = svg.querySelectorAll("[data-mark-id]");
@@ -6014,7 +6143,7 @@ function makeEditable(svg) {
6014
6143
  svg.insertBefore(style, svg.firstChild);
6015
6144
  }
6016
6145
  function hasEditingCallbacks(opts) {
6017
- return !!(opts?.onEdit || opts?.onSelect || opts?.onDeselect || opts?.onTextEdit);
6146
+ return !!(opts?.onEditTransaction || opts?.onEdit || opts?.onSelect || opts?.onDeselect || opts?.onTextEdit);
6018
6147
  }
6019
6148
  function authoredHighlight(spec) {
6020
6149
  if (isLayerSpec(spec) || isGraphSpec(spec)) return [];
@@ -6058,6 +6187,7 @@ function createChart(container, spec, options) {
6058
6187
  let destroyed = false;
6059
6188
  let isDragging = false;
6060
6189
  let pendingRender = false;
6190
+ let gestureWidth = 0;
6061
6191
  let isFirstRender = true;
6062
6192
  let cleanupAnimations = null;
6063
6193
  let pendingResize = false;
@@ -6115,7 +6245,10 @@ function createChart(container, spec, options) {
6115
6245
  facetGrowth = needed - budgetForPanels;
6116
6246
  }
6117
6247
  }
6118
- return layout.chrome.topHeight + layout.chrome.bottomHeight + topLegendBlock + (layout.metrics?.height ?? 0) + facetGrowth;
6248
+ return Math.max(
6249
+ 0,
6250
+ layout.chrome.topHeight + layout.chrome.bottomHeight + topLegendBlock + (layout.metrics?.height ?? 0) + facetGrowth + (layout.chrome.grownHeight !== void 0 ? layout.chrome.noteBandHeight ?? 0 : 0) - (layout.chrome.grownHeight ?? 0)
6251
+ );
6119
6252
  }
6120
6253
  function compile(candidate = currentSpec, measure = measureText, runtime = true) {
6121
6254
  const { width, height } = getContainerDimensions(candidate);
@@ -6240,10 +6373,40 @@ function createChart(container, spec, options) {
6240
6373
  height: isAutoHeight === true ? FALLBACK_HEIGHT : Math.max(ownHeight ? lastHostHeight : rect.height || FALLBACK_HEIGHT, 100)
6241
6374
  };
6242
6375
  }
6376
+ if (options?.onDataPointClick)
6377
+ warnDeprecated("onDataPointClick is deprecated and has no effect; use onMarkClick.");
6378
+ if (options?.onAnnotationEdit)
6379
+ warnDeprecated("onAnnotationEdit is deprecated; use onEditTransaction.");
6380
+ if (options?.onTextEdit) warnDeprecated("onTextEdit is deprecated; use onEditTransaction.");
6381
+ if (options?.responsive !== void 0)
6382
+ warnDeprecated("The mount responsive option is deprecated; use autoResize.");
6383
+ let editRevision = options?.revision;
6384
+ const annotationNudges = createAnnotationNudges(
6385
+ () => ({
6386
+ layout: currentLayout,
6387
+ spec: currentSpec,
6388
+ svg: svgElement,
6389
+ selected: selectedElement
6390
+ }),
6391
+ emitEdit
6392
+ );
6393
+ function emitEdit(edit) {
6394
+ const tx = editToTransaction(currentSpec, edit, editRevision);
6395
+ if (!tx && edit.type !== "legend-toggle") return;
6396
+ if (tx && !applyEditTransaction(currentSpec, tx).ok) return;
6397
+ if ("element" in edit && edit.element.type === "annotation") {
6398
+ const base = `${annotationSource(currentSpec).path}/${edit.element.index}`;
6399
+ const suffix = edit.element.path?.replace(/^.*\/annotations\/\d+/, "") ?? "";
6400
+ edit = { ...edit, element: { ...edit.element, path: base + suffix } };
6401
+ }
6402
+ if (tx) options?.onEditTransaction?.(tx);
6403
+ options?.onEdit?.(edit);
6404
+ }
6243
6405
  function getSpecAnnotations() {
6244
- return "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
6406
+ return annotationSource(currentSpec).annotations;
6245
6407
  }
6246
6408
  function selectElement(ref) {
6409
+ annotationNudges.flush();
6247
6410
  if (!svgElement) return;
6248
6411
  const target = findElementByRef(svgElement, ref);
6249
6412
  if (!target) return;
@@ -6256,6 +6419,7 @@ function createChart(container, spec, options) {
6256
6419
  svgElement.focus();
6257
6420
  }
6258
6421
  function deselectElement() {
6422
+ annotationNudges.flush();
6259
6423
  if (!selectedElement) return;
6260
6424
  if (isTextEditingActive && textEditCleanup) {
6261
6425
  textEditCleanup();
@@ -6292,7 +6456,7 @@ function createChart(container, spec, options) {
6292
6456
  textEditCleanup = null;
6293
6457
  if (newText !== currentText) {
6294
6458
  options?.onTextEdit?.(editRef, currentText, newText);
6295
- options?.onEdit?.({
6459
+ emitEdit({
6296
6460
  type: "text-edit",
6297
6461
  element: editRef,
6298
6462
  oldText: currentText,
@@ -6358,7 +6522,7 @@ function createChart(container, spec, options) {
6358
6522
  enterTextEditing();
6359
6523
  return;
6360
6524
  }
6361
- if (!ref && options?.onEdit && currentLayout?.xInvert && currentLayout?.yInvert) {
6525
+ if (!ref && (options?.onEdit || options?.onEditTransaction) && currentLayout?.xInvert && currentLayout?.yInvert) {
6362
6526
  const svgEl = svg;
6363
6527
  const viewBox = svgEl.viewBox?.baseVal;
6364
6528
  const svgRect = svgEl.getBoundingClientRect();
@@ -6381,7 +6545,7 @@ function createChart(container, spec, options) {
6381
6545
  isTextEditingActive = false;
6382
6546
  textEditCleanup = null;
6383
6547
  const annotation = { type: "text", x: dataX, y: dataY, text };
6384
- options.onEdit({ type: "add", annotation });
6548
+ emitEdit({ type: "add", annotation });
6385
6549
  },
6386
6550
  onCancel: () => {
6387
6551
  isTextEditingActive = false;
@@ -6405,12 +6569,15 @@ function createChart(container, spec, options) {
6405
6569
  const svg = svgElement;
6406
6570
  const handleKeyDown = (e) => {
6407
6571
  const specAnnotations = getSpecAnnotations();
6572
+ if (!isTextEditingActive && annotationNudges.keydown(e)) return;
6573
+ if (e.key === "Escape" || e.key === "Delete" || e.key === "Backspace")
6574
+ annotationNudges.flush();
6408
6575
  switch (e.key) {
6409
6576
  case "Delete":
6410
6577
  case "Backspace": {
6411
6578
  if (selectedElement && !isTextEditingActive) {
6412
6579
  e.preventDefault();
6413
- options?.onEdit?.({ type: "delete", element: selectedElement });
6580
+ emitEdit({ type: "delete", element: selectedElement });
6414
6581
  }
6415
6582
  break;
6416
6583
  }
@@ -6461,8 +6628,49 @@ function createChart(container, spec, options) {
6461
6628
  }
6462
6629
  };
6463
6630
  svg.addEventListener("keydown", handleKeyDown);
6631
+ const flushNudges = () => annotationNudges.flush();
6632
+ svg.addEventListener("keyup", flushNudges);
6633
+ const startGesture = (event) => {
6634
+ const target = event.target;
6635
+ const ref = buildElementRef(target, getSpecAnnotations());
6636
+ const previousLayout = currentLayout;
6637
+ const role = target.closest(".oc-connector-handle, .oc-anchor-handle, .oc-annotation-label");
6638
+ const roleClass = role?.getAttribute("class");
6639
+ const endpoint = role?.getAttribute("data-endpoint");
6640
+ annotationNudges.flush();
6641
+ if (destroyed || !ref || !svgElement || currentLayout === previousLayout) return;
6642
+ const group = findElementByRef(svgElement, ref);
6643
+ if (!group) return;
6644
+ const replacement = roleClass ? [...group.querySelectorAll("*")].find(
6645
+ (el) => el.getAttribute("class") === roleClass && el.getAttribute("data-endpoint") === endpoint
6646
+ ) ?? group : group;
6647
+ event.stopImmediatePropagation();
6648
+ event.preventDefault();
6649
+ const init = { bubbles: true, cancelable: true };
6650
+ replacement.dispatchEvent(
6651
+ event.type === "mousedown" ? new MouseEvent("mousedown", {
6652
+ ...init,
6653
+ clientX: event.clientX,
6654
+ clientY: event.clientY,
6655
+ button: event.button
6656
+ }) : new TouchEvent("touchstart", {
6657
+ ...init,
6658
+ touches: [...event.touches],
6659
+ changedTouches: [...event.changedTouches]
6660
+ })
6661
+ );
6662
+ };
6663
+ svg.addEventListener("mousedown", startGesture, true);
6664
+ svg.addEventListener("touchstart", startGesture, true);
6665
+ document.addEventListener("pointercancel", flushNudges, true);
6666
+ document.addEventListener("touchcancel", flushNudges, true);
6464
6667
  return () => {
6465
6668
  svg.removeEventListener("keydown", handleKeyDown);
6669
+ svg.removeEventListener("keyup", flushNudges);
6670
+ svg.removeEventListener("mousedown", startGesture, true);
6671
+ svg.removeEventListener("touchstart", startGesture, true);
6672
+ document.removeEventListener("pointercancel", flushNudges, true);
6673
+ document.removeEventListener("touchcancel", flushNudges, true);
6466
6674
  };
6467
6675
  }
6468
6676
  function render(retainScene = false, prepared) {
@@ -6596,41 +6804,63 @@ function createChart(container, spec, options) {
6596
6804
  "[openchart] seriesSearch and edit mode are mutually exclusive; edit interactions are disabled while seriesSearch is enabled."
6597
6805
  );
6598
6806
  }
6807
+ const authoredAnnotations = annotationSource(currentSpec);
6808
+ for (const el of svgElement.querySelectorAll("[data-annotation-index]")) {
6809
+ el.setAttribute(
6810
+ "data-annotation-path",
6811
+ `${authoredAnnotations.path}/${el.getAttribute("data-annotation-index")}`
6812
+ );
6813
+ }
6814
+ for (const el of svgElement.querySelectorAll("[data-note-spec-index]")) {
6815
+ el.setAttribute(
6816
+ "data-note-path",
6817
+ `${authoredAnnotations.path}/${el.getAttribute("data-note-spec-index")}`
6818
+ );
6819
+ }
6599
6820
  cleanupLegend = wireLegendInteraction(
6600
6821
  svgElement,
6601
6822
  currentLayout,
6602
6823
  toggleSeriesVisibility,
6603
6824
  options?.onLegendToggle,
6604
- editSuppressed ? void 0 : options?.onEdit,
6825
+ editSuppressed ? void 0 : emitEdit,
6605
6826
  hoverEmphasis
6606
6827
  );
6607
6828
  if (options?.onMarkClick || options?.onMarkHover || options?.onMarkLeave || options?.onAnnotationClick) {
6608
- const specAnnotations = "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
6829
+ const specAnnotations = getSpecAnnotations();
6609
6830
  cleanupChartEvents = wireChartEvents(svgElement, currentLayout, specAnnotations, options);
6610
6831
  }
6611
6832
  const setDragging = (dragging) => {
6833
+ if (dragging) gestureWidth = getContainerDimensions().width;
6612
6834
  isDragging = dragging;
6613
6835
  if (!dragging && pendingRender) {
6614
6836
  pendingRender = false;
6615
6837
  render();
6616
6838
  }
6617
6839
  };
6618
- const dragAnnotations = "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
6619
- if (!editSuppressed && (options?.editable ?? Boolean(options?.onAnnotationEdit || options?.onEdit))) {
6840
+ const dragAnnotations = getSpecAnnotations();
6841
+ if (!editSuppressed && (options?.editable ?? Boolean(options?.onAnnotationEdit || options?.onEdit || options?.onEditTransaction))) {
6620
6842
  cleanupAnnotationDrag = wireAnnotationDrag(
6621
6843
  svgElement,
6622
6844
  dragAnnotations,
6623
6845
  options?.onAnnotationEdit,
6624
- options?.onEdit,
6625
- setDragging
6846
+ emitEdit,
6847
+ setDragging,
6848
+ currentLayout,
6849
+ currentSpec
6626
6850
  );
6627
6851
  }
6628
- if (!editSuppressed && (options?.editable ?? Boolean(options?.onEdit))) {
6852
+ if (!editSuppressed && (options?.editable ?? Boolean(options?.onEdit || options?.onEditTransaction))) {
6629
6853
  const editCleanups = [];
6630
- const onEditFn = options?.onEdit ?? (() => {
6631
- });
6854
+ const onEditFn = emitEdit;
6632
6855
  editCleanups.push(
6633
- wireConnectorEndpointDrag(svgElement, dragAnnotations, onEditFn, setDragging)
6856
+ wireConnectorEndpointDrag(
6857
+ svgElement,
6858
+ dragAnnotations,
6859
+ onEditFn,
6860
+ setDragging,
6861
+ currentLayout,
6862
+ currentSpec
6863
+ )
6634
6864
  );
6635
6865
  editCleanups.push(
6636
6866
  wireAnnotationLabelDrag(svgElement, dragAnnotations, onEditFn, setDragging)
@@ -6750,6 +6980,17 @@ function createChart(container, spec, options) {
6750
6980
  }
6751
6981
  function doUpdate(newSpec, updateOpts, manual, layoutOnly = false) {
6752
6982
  if (destroyed) return null;
6983
+ annotationNudges.flush();
6984
+ if (destroyed) return null;
6985
+ if (layoutOnly) newSpec = currentSpec;
6986
+ if (isDragging) {
6987
+ if (layoutOnly && getContainerDimensions().width === gestureWidth) {
6988
+ pendingRender = true;
6989
+ return null;
6990
+ }
6991
+ pendingRender = false;
6992
+ svgElement?.dispatchEvent(new Event("oc-cancel-drag"));
6993
+ }
6753
6994
  const prevSpec = currentSpec;
6754
6995
  const prevLayout = currentLayout;
6755
6996
  const entranceWasRunning = cleanupAnimations != null;
@@ -6757,6 +6998,7 @@ function createChart(container, spec, options) {
6757
6998
  const nextMeasure = nextFont === fontFamily ? measureText : createMeasureText(nextFont);
6758
6999
  const nextLayout = compile(newSpec, nextMeasure, layoutOnly);
6759
7000
  const snapshot = transitionHandle?.running ? transitionHandle.snapshot() : void 0;
7001
+ if (updateOpts && Object.hasOwn(updateOpts, "revision")) editRevision = updateOpts.revision;
6760
7002
  currentSpec = newSpec;
6761
7003
  fontFamily = nextFont;
6762
7004
  measureText = nextMeasure;
@@ -6880,6 +7122,7 @@ function createChart(container, spec, options) {
6880
7122
  }
6881
7123
  }
6882
7124
  function destroy() {
7125
+ annotationNudges.flush();
6883
7126
  if (destroyed) return;
6884
7127
  destroyed = true;
6885
7128
  if (transitionHandle) {
@@ -6965,7 +7208,7 @@ function createChart(container, spec, options) {
6965
7208
  }
6966
7209
  );
6967
7210
  container.dataset.ocFontsState = pending ? "pending" : "ready";
6968
- if (options?.responsive !== false) {
7211
+ if ((options?.autoResize ?? options?.responsive) !== false) {
6969
7212
  disconnectResize = observeResize(container, (width, height) => {
6970
7213
  const widthChanged = lastRenderedWidth === null || Math.abs(width - lastRenderedWidth) >= 0.5;
6971
7214
  const selfInducedHeight = lastRenderedSvgHeight !== null && Math.abs(height - lastRenderedSvgHeight) < 0.5;
@@ -7748,13 +7991,7 @@ function snapTween(tw) {
7748
7991
  tw.el.setAttribute(tw.attr, String(tw.to));
7749
7992
  }
7750
7993
  }
7751
- function runMapFillTransition(svg, prev, opts) {
7752
- const snapshot = prev instanceof Map ? {
7753
- fills: prev,
7754
- points: new Map(
7755
- Array.from(prev).filter(([k]) => k.startsWith("pt:")).map(([k, fill]) => [k.slice(3), { fill }])
7756
- )
7757
- } : prev;
7994
+ function runMapFillTransition(svg, snapshot, opts) {
7758
7995
  const tweens = [];
7759
7996
  const features = svg.querySelectorAll(".oc-map-feature[data-key]");
7760
7997
  for (const p of features) {
@@ -8351,7 +8588,12 @@ function createGeoMap(container, spec, options) {
8351
8588
  theme: options?.theme,
8352
8589
  darkMode,
8353
8590
  watermark: options?.watermark,
8354
- measureText: measure
8591
+ measureText: measure,
8592
+ // An unconstrained host must grow around wrapped titles and sources.
8593
+ // Treating the aspect-ratio budget as the entire SVG can leave only a
8594
+ // few pixels for geography on phones. Explicit heights remain a hard
8595
+ // budget, as they are in dashboards and scrollytelling panels.
8596
+ chromeLayout: isAutoHeight === true ? "grow" : "subtract"
8355
8597
  };
8356
8598
  return compileGeoMap(candidate, compileOpts);
8357
8599
  }
@@ -8936,10 +9178,10 @@ export {
8936
9178
  scheduleFontReload,
8937
9179
  VECTOR_EXPORT_MAX_POINTS,
8938
9180
  materializeCanvasModeSVG,
9181
+ applySrOnlyStyles,
8939
9182
  DEFAULT_UPDATE_MAX_MARKS,
8940
9183
  CANVAS_DEFAULT_UPDATE_MAX_MARKS,
8941
9184
  canTransitionSpecShape,
8942
- applySrOnlyStyles,
8943
9185
  createTextEditOverlay,
8944
9186
  createYouDrawIt,
8945
9187
  createChart,
@@ -8956,4 +9198,4 @@ export {
8956
9198
  scrubCamera,
8957
9199
  createGeoMap
8958
9200
  };
8959
- //# sourceMappingURL=chunk-6TD7GWR4.js.map
9201
+ //# sourceMappingURL=chunk-CCRFYGHU.js.map