@opendata-ai/openchart-vanilla 8.3.1 → 8.4.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.
@@ -4,9 +4,10 @@ import {
4
4
  exportPNG,
5
5
  exportSVG,
6
6
  exportSVGWithFonts
7
- } from "./chunk-T32UHMZA.js";
7
+ } from "./chunk-6B5OBGIO.js";
8
8
  import {
9
9
  buildGradientDefs,
10
+ buildPatternDefs,
10
11
  nextSvgId,
11
12
  rectPathWithCorners,
12
13
  renderChartSVG,
@@ -14,13 +15,16 @@ import {
14
15
  renderLegend,
15
16
  renderSingleMark,
16
17
  resolveMarkFill,
17
- stampAnimationVars
18
- } from "./chunk-W76C53MC.js";
18
+ resolvePatternFill,
19
+ resolvedSurface,
20
+ stampAnimationVars,
21
+ stampThemeProperties
22
+ } from "./chunk-KANNWAEA.js";
19
23
  import {
20
24
  SVG_NS,
21
25
  createSVGElement,
22
26
  setAttrs
23
- } from "./chunk-RM5XS6NR.js";
27
+ } from "./chunk-5RX6ZQGZ.js";
24
28
 
25
29
  // src/resize-observer.ts
26
30
  var DEBOUNCE_MS = 16;
@@ -89,11 +93,12 @@ function createTooltipManager(container) {
89
93
  return;
90
94
  }
91
95
  const content = input;
92
- const contentKey = `${content.title}|${content.fields.length}|${content.fields[0]?.value}|${content.fields[content.fields.length - 1]?.value}`;
96
+ const emphasisIndex = content.fields.findIndex((f) => f.emphasis);
97
+ const contentKey = `${content.title}|${content.fields.length}|${content.fields[0]?.value}|${content.fields[content.fields.length - 1]?.value}|${emphasisIndex}`;
93
98
  if (contentKey !== lastContentKey) {
94
99
  lastContentKey = contentKey;
95
100
  let html = "";
96
- const colorRowCount = content.fields.filter((f) => f.color).length;
101
+ const colorRowCount = content.fields.filter((f) => f.color && f.role !== "total").length;
97
102
  const perRowSwatches = colorRowCount > 1;
98
103
  if (content.title) {
99
104
  const titleColor = perRowSwatches ? void 0 : content.fields.find((f) => f.color)?.color;
@@ -107,7 +112,10 @@ function createTooltipManager(container) {
107
112
  if (content.fields.length > 0) {
108
113
  html += '<div class="oc-tooltip-body">';
109
114
  for (const field of content.fields) {
110
- html += '<div class="oc-tooltip-row">';
115
+ let rowClass = "oc-tooltip-row";
116
+ if (field.emphasis) rowClass += " oc-tooltip-row--emphasis";
117
+ if (field.role === "total") rowClass += " oc-tooltip-row--total";
118
+ html += `<div class="${rowClass}">`;
111
119
  html += '<span class="oc-tooltip-label">';
112
120
  if (perRowSwatches && field.color) {
113
121
  html += `<span class="oc-tooltip-row-swatch" style="background:${esc(field.color)}"></span>`;
@@ -2847,6 +2855,7 @@ function buildTextMarkTweens(prevLayout, nextLayout, marksContainer, dom, tweens
2847
2855
  }
2848
2856
 
2849
2857
  // src/transition/driver.ts
2858
+ var TRANSITIONING_CLASS = "oc-transitioning";
2850
2859
  function runTransition(args) {
2851
2860
  const { svg, prevLayout, nextLayout, animation, onComplete, fromSnapshot, manual, canvas } = args;
2852
2861
  const update = animation.update;
@@ -2872,6 +2881,7 @@ function runTransition(args) {
2872
2881
  totalMs: 0
2873
2882
  };
2874
2883
  }
2884
+ svg.classList.add(TRANSITIONING_CLASS);
2875
2885
  const dom = buildMarkDomIndex(marksContainer);
2876
2886
  const tweens = [];
2877
2887
  const ghosts = [];
@@ -2979,6 +2989,7 @@ function runTransition(args) {
2979
2989
  }
2980
2990
  let rafId = null;
2981
2991
  let running = true;
2992
+ let pulsedDot = null;
2982
2993
  let startTime = null;
2983
2994
  let lastElapsed = 0;
2984
2995
  const nextLineMarks = nextLayout.marks.filter((m) => m.type === "line");
@@ -3010,9 +3021,11 @@ function runTransition(args) {
3010
3021
  if (!running) return;
3011
3022
  running = false;
3012
3023
  rafId = null;
3024
+ svg.classList.remove(TRANSITIONING_CLASS);
3013
3025
  snapToFinal();
3014
3026
  removeGhosts();
3015
3027
  settleCanvas();
3028
+ pulsedDot = pulseSparklineTerminator(svg, marksContainer);
3016
3029
  for (let i = 0; i < secondaryEls.length; i++) {
3017
3030
  secondaryEls[i].style.opacity = secondaryOriginalOpacity[i];
3018
3031
  }
@@ -3127,8 +3140,12 @@ function runTransition(args) {
3127
3140
  }
3128
3141
  return {
3129
3142
  cancel() {
3143
+ clearPulseTimer(pulsedDot);
3144
+ pulsedDot?.classList.remove(PULSE_CLASS);
3145
+ pulsedDot = null;
3130
3146
  if (!running) return;
3131
3147
  running = false;
3148
+ svg.classList.remove(TRANSITIONING_CLASS);
3132
3149
  if (rafId !== null) {
3133
3150
  cancelAnimationFrame(rafId);
3134
3151
  rafId = null;
@@ -3164,6 +3181,37 @@ function runTransition(args) {
3164
3181
  totalMs
3165
3182
  };
3166
3183
  }
3184
+ var PULSE_CLASS = "oc-pulse";
3185
+ var PULSE_DURATION_MS = 600;
3186
+ var pulseTimers = /* @__PURE__ */ new WeakMap();
3187
+ function clearPulseTimer(dot) {
3188
+ if (!dot) return;
3189
+ const pending = pulseTimers.get(dot);
3190
+ if (pending !== void 0) {
3191
+ clearTimeout(pending);
3192
+ pulseTimers.delete(dot);
3193
+ }
3194
+ }
3195
+ function pulseSparklineTerminator(svg, marksContainer) {
3196
+ if (!marksContainer || svg.getAttribute("data-display") !== "sparkline") return null;
3197
+ const dots = marksContainer.querySelectorAll("circle.oc-mark-point");
3198
+ const dot = dots[dots.length - 1];
3199
+ if (!dot) return null;
3200
+ const r = dot.getAttribute("r");
3201
+ if (r) dot.style.setProperty("--oc-pulse-r", `${r}px`);
3202
+ clearPulseTimer(dot);
3203
+ dot.classList.remove(PULSE_CLASS);
3204
+ void dot.getBoundingClientRect?.();
3205
+ dot.classList.add(PULSE_CLASS);
3206
+ pulseTimers.set(
3207
+ dot,
3208
+ setTimeout(() => {
3209
+ pulseTimers.delete(dot);
3210
+ dot.classList.remove(PULSE_CLASS);
3211
+ }, PULSE_DURATION_MS)
3212
+ );
3213
+ return dot;
3214
+ }
3167
3215
  function collectSecondaryElements(svg) {
3168
3216
  const els = [];
3169
3217
  const epLabels = svg.querySelector(".oc-endpoint-labels");
@@ -3520,7 +3568,7 @@ function createYouDrawIt(options) {
3520
3568
  }
3521
3569
 
3522
3570
  // src/mount.ts
3523
- import { cssTokenDefault as cssTokenDefault2, isGraphSpec, isLayerSpec } from "@opendata-ai/openchart-core";
3571
+ import { cssTokenDefault, isGraphSpec, isLayerSpec } from "@opendata-ai/openchart-core";
3524
3572
  import {
3525
3573
  compileChart,
3526
3574
  compileLayer,
@@ -3536,9 +3584,9 @@ function cancelAnimations(svg) {
3536
3584
  }
3537
3585
  function computeAnimationDuration(svg) {
3538
3586
  const style = svg.style;
3539
- const duration = parseFloat(style.getPropertyValue("--oc-animation-duration")) || 600;
3587
+ const duration = parseFloat(style.getPropertyValue("--oc-animation-duration")) || 450;
3540
3588
  const stagger = parseFloat(style.getPropertyValue("--oc-animation-stagger")) || 0;
3541
- const annotationDelay = parseFloat(style.getPropertyValue("--oc-annotation-delay")) || 200;
3589
+ const annotationDelay = parseFloat(style.getPropertyValue("--oc-annotation-delay")) || 150;
3542
3590
  const animatedElements = svg.querySelectorAll("[data-animation-index]").length;
3543
3591
  const totalStagger = stagger * Math.max(0, animatedElements - 1);
3544
3592
  return totalStagger + duration + annotationDelay + 500;
@@ -3678,6 +3726,8 @@ function wireChartEvents(svg, layout, specAnnotations, handlers) {
3678
3726
 
3679
3727
  // src/interactions/crosshair.ts
3680
3728
  import { getRepresentativeColor } from "@opendata-ai/openchart-core";
3729
+ var EMPHASIS_PROXIMITY_PX = 18;
3730
+ var SNAP_DOT_RADIUS = 4.5;
3681
3731
  function snapKey(x) {
3682
3732
  return Math.round(x);
3683
3733
  }
@@ -3724,43 +3774,46 @@ function findNearestX(sortedXs, x) {
3724
3774
  }
3725
3775
  return candidate;
3726
3776
  }
3727
- function buildSliceTooltip(hits) {
3777
+ function buildSliceTooltip(hits, emphasisKey) {
3728
3778
  if (hits.length === 0) return null;
3729
3779
  const title = hits[0].point.tooltip?.title;
3730
3780
  const fields = [];
3731
3781
  const isMulti = hits.length > 1;
3782
+ let total;
3732
3783
  for (const { group, point } of hits) {
3733
3784
  const tip = point.tooltip;
3734
3785
  if (!tip) continue;
3786
+ if (!total) total = tip.fields.find((f) => f.role === "total");
3735
3787
  if (isMulti) {
3736
- const yField = tip.fields.find(
3788
+ const candidates = tip.fields.filter((f) => f.role !== "total");
3789
+ const yField = candidates.find(
3737
3790
  (f) => !f.color && f.label !== title && f.value !== title && f.value !== group.seriesKey
3738
- ) ?? tip.fields[tip.fields.length - 1] ?? null;
3791
+ ) ?? candidates[candidates.length - 1] ?? null;
3739
3792
  if (!yField) continue;
3740
3793
  fields.push({
3741
3794
  label: group.seriesKey,
3742
3795
  value: yField.value,
3743
- color: group.color
3796
+ color: group.color,
3797
+ ...emphasisKey === group.seriesKey ? { emphasis: true } : {}
3744
3798
  });
3745
3799
  } else {
3746
3800
  for (const f of tip.fields) {
3801
+ if (f.role === "total") continue;
3747
3802
  fields.push({ ...f, color: f.color ?? group.color });
3748
3803
  }
3749
3804
  }
3750
3805
  }
3751
3806
  if (fields.length === 0) return null;
3807
+ if (total && isMulti) fields.push(total);
3752
3808
  return { title, fields };
3753
3809
  }
3754
- function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
3810
+ function wireVoronoiTooltipEvents(svg, layout, tooltipManager, emphasis) {
3755
3811
  const overlay = svg.querySelector("[data-voronoi-overlay]");
3756
- if (!overlay) return () => {
3757
- };
3812
+ if (!overlay) return null;
3758
3813
  const groups = collectSeriesGroups(layout);
3759
- if (groups.length === 0) return () => {
3760
- };
3814
+ if (groups.length === 0) return null;
3761
3815
  const snapXs = collectSnapXs(groups);
3762
- if (snapXs.length === 0) return () => {
3763
- };
3816
+ if (snapXs.length === 0) return null;
3764
3817
  const crosshair = svg.querySelector("[data-crosshair]");
3765
3818
  const dotsLayer = svg.querySelector("[data-snap-dots]");
3766
3819
  const dots = [];
@@ -3768,7 +3821,7 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
3768
3821
  while (dotsLayer.firstChild) dotsLayer.removeChild(dotsLayer.firstChild);
3769
3822
  for (const group of groups) {
3770
3823
  const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
3771
- circle.setAttribute("r", "4");
3824
+ circle.setAttribute("r", String(SNAP_DOT_RADIUS));
3772
3825
  circle.setAttribute("fill", layout.theme.colors.background);
3773
3826
  circle.setAttribute("stroke", group.color);
3774
3827
  circle.setAttribute("stroke-width", "2");
@@ -3778,9 +3831,16 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
3778
3831
  dots.push(circle);
3779
3832
  }
3780
3833
  }
3781
- const positionAt = (svgX, _svgY, viewBoxToContainer) => {
3782
- const snappedX = findNearestX(snapXs, svgX);
3783
- if (snappedX === null) return false;
3834
+ let currentIndex = -1;
3835
+ let currentEmphasis = null;
3836
+ function containerScale() {
3837
+ const svgEl = svg;
3838
+ const rect = svgEl.getBoundingClientRect();
3839
+ const viewBox = svgEl.viewBox?.baseVal;
3840
+ const scaleX = viewBox?.width && rect.width ? viewBox.width / rect.width : 1;
3841
+ return scaleX > 0 ? 1 / scaleX : 1;
3842
+ }
3843
+ function showAt(snappedX, emphasisKey, viewBoxToContainer) {
3784
3844
  const hits = [];
3785
3845
  let anchorY = 0;
3786
3846
  let anchorCount = 0;
@@ -3806,7 +3866,10 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
3806
3866
  crosshair.setAttribute("x2", String(snappedX));
3807
3867
  crosshair.style.display = "";
3808
3868
  }
3809
- const tooltip = buildSliceTooltip(hits);
3869
+ currentEmphasis = emphasisKey;
3870
+ if (emphasisKey) emphasis?.setSeries(emphasisKey);
3871
+ else emphasis?.clear();
3872
+ const tooltip = buildSliceTooltip(hits, emphasisKey);
3810
3873
  if (!tooltip) return false;
3811
3874
  const containerAnchorX = snappedX * viewBoxToContainer;
3812
3875
  const containerAnchorY = anchorCount > 0 ? anchorY / anchorCount * viewBoxToContainer : 0;
@@ -3814,6 +3877,31 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
3814
3877
  placement: "right"
3815
3878
  });
3816
3879
  return true;
3880
+ }
3881
+ function keysAt(snappedX) {
3882
+ const keys = [];
3883
+ for (const group of groups) {
3884
+ if (group.pointsByX.has(snappedX)) keys.push(group.seriesKey);
3885
+ }
3886
+ return keys;
3887
+ }
3888
+ const positionAt = (svgX, svgY, viewBoxToContainer) => {
3889
+ const snappedX = findNearestX(snapXs, svgX);
3890
+ if (snappedX === null) return false;
3891
+ currentIndex = snapXs.indexOf(snappedX);
3892
+ let nearestKey = null;
3893
+ let nearestDy = Number.POSITIVE_INFINITY;
3894
+ for (const group of groups) {
3895
+ const point = group.pointsByX.get(snappedX);
3896
+ if (!point) continue;
3897
+ const dy = Math.abs(point.y - svgY);
3898
+ if (dy < nearestDy) {
3899
+ nearestDy = dy;
3900
+ nearestKey = group.seriesKey;
3901
+ }
3902
+ }
3903
+ const emphasisKey = nearestDy <= EMPHASIS_PROXIMITY_PX ? nearestKey : null;
3904
+ return showAt(snappedX, emphasisKey, viewBoxToContainer);
3817
3905
  };
3818
3906
  const toSvgCoords = (clientX, clientY) => {
3819
3907
  const svgEl = svg;
@@ -3831,11 +3919,24 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
3831
3919
  const { svgX, svgY, viewBoxToContainer } = toSvgCoords(me.clientX, me.clientY);
3832
3920
  positionAt(svgX, svgY, viewBoxToContainer);
3833
3921
  };
3834
- const hideAll = () => {
3922
+ const hideAll = (resetIndex = false) => {
3835
3923
  if (crosshair) crosshair.style.display = "none";
3836
3924
  for (const dot of dots) dot.style.display = "none";
3837
3925
  tooltipManager.hide();
3926
+ currentEmphasis = null;
3927
+ if (resetIndex) currentIndex = -1;
3928
+ emphasis?.clear();
3838
3929
  };
3930
+ const hideOnPointerOut = () => hideAll();
3931
+ function stepTo(index) {
3932
+ if (snapXs.length === 0) return;
3933
+ const wrapped = (index % snapXs.length + snapXs.length) % snapXs.length;
3934
+ currentIndex = wrapped;
3935
+ const snappedX = snapXs[wrapped];
3936
+ const keys = keysAt(snappedX);
3937
+ const keep = currentEmphasis && keys.includes(currentEmphasis) ? currentEmphasis : null;
3938
+ showAt(snappedX, keep, containerScale());
3939
+ }
3839
3940
  const handleTouch = (e) => {
3840
3941
  const te = e;
3841
3942
  if (te.touches.length === 0) return;
@@ -3845,16 +3946,43 @@ function wireVoronoiTooltipEvents(svg, layout, tooltipManager) {
3845
3946
  positionAt(svgX, svgY, viewBoxToContainer);
3846
3947
  };
3847
3948
  overlay.addEventListener("mousemove", handleMouseMove);
3848
- overlay.addEventListener("mouseleave", hideAll);
3949
+ overlay.addEventListener("mouseleave", hideOnPointerOut);
3849
3950
  overlay.addEventListener("touchstart", handleTouch, { passive: false });
3850
3951
  overlay.addEventListener("touchmove", handleTouch, { passive: false });
3851
- overlay.addEventListener("touchend", hideAll);
3852
- return () => {
3853
- overlay.removeEventListener("mousemove", handleMouseMove);
3854
- overlay.removeEventListener("mouseleave", hideAll);
3855
- overlay.removeEventListener("touchstart", handleTouch);
3856
- overlay.removeEventListener("touchmove", handleTouch);
3857
- overlay.removeEventListener("touchend", hideAll);
3952
+ overlay.addEventListener("touchend", hideOnPointerOut);
3953
+ return {
3954
+ cleanup() {
3955
+ overlay.removeEventListener("mousemove", handleMouseMove);
3956
+ overlay.removeEventListener("mouseleave", hideOnPointerOut);
3957
+ overlay.removeEventListener("touchstart", handleTouch);
3958
+ overlay.removeEventListener("touchmove", handleTouch);
3959
+ overlay.removeEventListener("touchend", hideOnPointerOut);
3960
+ },
3961
+ get snapCount() {
3962
+ return snapXs.length;
3963
+ },
3964
+ get currentIndex() {
3965
+ return currentIndex;
3966
+ },
3967
+ stepTo,
3968
+ cycleSeries(delta) {
3969
+ if (currentIndex < 0) {
3970
+ stepTo(0);
3971
+ return;
3972
+ }
3973
+ const snappedX = snapXs[currentIndex];
3974
+ const keys = keysAt(snappedX);
3975
+ if (keys.length === 0) return;
3976
+ const at = currentEmphasis ? keys.indexOf(currentEmphasis) : -1;
3977
+ const next = ((at + delta) % keys.length + keys.length) % keys.length;
3978
+ showAt(snappedX, keys[next], containerScale());
3979
+ },
3980
+ showCurrent() {
3981
+ stepTo(currentIndex < 0 ? 0 : currentIndex);
3982
+ },
3983
+ hide(resetIndex) {
3984
+ hideAll(resetIndex === true);
3985
+ }
3858
3986
  };
3859
3987
  }
3860
3988
 
@@ -4519,11 +4647,175 @@ function wireAnchorDrag(svg, specAnnotations, layout, onEdit, setDragging) {
4519
4647
  };
4520
4648
  }
4521
4649
 
4650
+ // src/interactions/hover-emphasis.ts
4651
+ var MARKS_ACTIVE_CLASS = "oc-hover-active";
4652
+ var MARK_HOVER_CLASS = "oc-mark--hover";
4653
+ var MARK_DIM_CLASS = "oc-mark--dim";
4654
+ var LEGEND_HOVER_CLASS = "oc-legend-entry--hover";
4655
+ var LEGEND_DIM_CLASS = "oc-legend-entry--dim";
4656
+ var NOOP = {
4657
+ setSeries() {
4658
+ },
4659
+ setMark() {
4660
+ },
4661
+ clear() {
4662
+ },
4663
+ destroy() {
4664
+ },
4665
+ multiSeries: false
4666
+ };
4667
+ function countMarkSeries(layout) {
4668
+ const seen = /* @__PURE__ */ new Set();
4669
+ for (const mark of layout.marks) {
4670
+ const key = "seriesKey" in mark ? mark.seriesKey : void 0;
4671
+ if (key) seen.add(key);
4672
+ }
4673
+ return seen.size;
4674
+ }
4675
+ function createHoverEmphasis(svg, layout) {
4676
+ if (layout.markRenderMode === "canvas") return NOOP;
4677
+ const marksGroup = svg.querySelector(".oc-marks");
4678
+ if (!marksGroup) return NOOP;
4679
+ const multiSeries = countMarkSeries(layout) > 1;
4680
+ let markEls = null;
4681
+ let legendEls = null;
4682
+ function marks() {
4683
+ if (!markEls) markEls = Array.from(marksGroup.querySelectorAll(".oc-mark"));
4684
+ return markEls;
4685
+ }
4686
+ function legendEntries() {
4687
+ if (!legendEls) legendEls = Array.from(svg.querySelectorAll("[data-legend-label]"));
4688
+ return legendEls;
4689
+ }
4690
+ function clear() {
4691
+ marksGroup.classList.remove(MARKS_ACTIVE_CLASS);
4692
+ for (const el of marks()) {
4693
+ el.classList.remove(MARK_HOVER_CLASS, MARK_DIM_CLASS);
4694
+ }
4695
+ for (const el of legendEntries()) {
4696
+ el.classList.remove(LEGEND_HOVER_CLASS, LEGEND_DIM_CLASS);
4697
+ }
4698
+ }
4699
+ function applyLegend(seriesKey) {
4700
+ for (const el of legendEntries()) {
4701
+ if (el.getAttribute("data-legend-active") === "false") continue;
4702
+ const match = el.getAttribute("data-legend-label") === seriesKey;
4703
+ el.classList.toggle(LEGEND_HOVER_CLASS, match);
4704
+ el.classList.toggle(LEGEND_DIM_CLASS, !match);
4705
+ }
4706
+ }
4707
+ function setSeries(seriesKey) {
4708
+ if (seriesKey === null) {
4709
+ clear();
4710
+ return;
4711
+ }
4712
+ marksGroup.classList.add(MARKS_ACTIVE_CLASS);
4713
+ for (const el of marks()) {
4714
+ const match = el.getAttribute("data-series") === seriesKey;
4715
+ el.classList.toggle(MARK_HOVER_CLASS, match);
4716
+ el.classList.toggle(MARK_DIM_CLASS, !match);
4717
+ }
4718
+ applyLegend(seriesKey);
4719
+ }
4720
+ function setMark(el) {
4721
+ if (!el) {
4722
+ clear();
4723
+ return;
4724
+ }
4725
+ const series = el.getAttribute("data-series");
4726
+ if (multiSeries && series) {
4727
+ setSeries(series);
4728
+ return;
4729
+ }
4730
+ marksGroup.classList.add(MARKS_ACTIVE_CLASS);
4731
+ for (const mark of marks()) {
4732
+ mark.classList.toggle(MARK_HOVER_CLASS, mark === el);
4733
+ mark.classList.remove(MARK_DIM_CLASS);
4734
+ }
4735
+ }
4736
+ return {
4737
+ setSeries,
4738
+ setMark,
4739
+ clear,
4740
+ destroy() {
4741
+ clear();
4742
+ markEls = null;
4743
+ legendEls = null;
4744
+ },
4745
+ multiSeries
4746
+ };
4747
+ }
4748
+ function wireEndpointLabelHover(svg, emphasis) {
4749
+ const entries = svg.querySelectorAll("[data-endpoint-key]");
4750
+ const cleanups = [];
4751
+ for (const entry of entries) {
4752
+ const key = entry.getAttribute("data-endpoint-key");
4753
+ if (!key) continue;
4754
+ const handleEnter = () => emphasis.setSeries(key);
4755
+ const handleLeave = () => emphasis.clear();
4756
+ entry.addEventListener("mouseenter", handleEnter);
4757
+ entry.addEventListener("mouseleave", handleLeave);
4758
+ cleanups.push(() => {
4759
+ entry.removeEventListener("mouseenter", handleEnter);
4760
+ entry.removeEventListener("mouseleave", handleLeave);
4761
+ });
4762
+ }
4763
+ return () => {
4764
+ for (const cleanup of cleanups) cleanup();
4765
+ };
4766
+ }
4767
+
4522
4768
  // src/interactions/keyboard-nav.ts
4523
- function wireKeyboardNav(svg, container, tooltipDescriptors, tooltipManager, layout) {
4769
+ function wireKeyboardNav(svg, container, tooltipDescriptors, tooltipManager, layout, crosshair) {
4524
4770
  container.setAttribute("tabindex", "0");
4525
4771
  container.setAttribute("aria-roledescription", "chart");
4526
4772
  container.setAttribute("aria-label", layout.a11y.altText);
4773
+ if (tooltipDescriptors.size === 0 && crosshair) {
4774
+ const handleCrosshairKeys = (e) => {
4775
+ const step = crosshair.currentIndex;
4776
+ switch (e.key) {
4777
+ case "ArrowRight":
4778
+ e.preventDefault();
4779
+ crosshair.stepTo(step + 1);
4780
+ break;
4781
+ case "ArrowLeft":
4782
+ e.preventDefault();
4783
+ crosshair.stepTo(step <= 0 ? crosshair.snapCount - 1 : step - 1);
4784
+ break;
4785
+ case "ArrowDown":
4786
+ e.preventDefault();
4787
+ if (step < 0) crosshair.stepTo(0);
4788
+ crosshair.cycleSeries(1);
4789
+ break;
4790
+ case "ArrowUp":
4791
+ e.preventDefault();
4792
+ if (step < 0) crosshair.stepTo(0);
4793
+ crosshair.cycleSeries(-1);
4794
+ break;
4795
+ case "Enter":
4796
+ case " ":
4797
+ e.preventDefault();
4798
+ crosshair.showCurrent();
4799
+ break;
4800
+ case "Escape":
4801
+ e.preventDefault();
4802
+ crosshair.hide(true);
4803
+ break;
4804
+ }
4805
+ };
4806
+ const handleCrosshairBlur = () => {
4807
+ crosshair.hide(true);
4808
+ };
4809
+ container.addEventListener("keydown", handleCrosshairKeys);
4810
+ container.addEventListener("blur", handleCrosshairBlur);
4811
+ return () => {
4812
+ container.removeEventListener("keydown", handleCrosshairKeys);
4813
+ container.removeEventListener("blur", handleCrosshairBlur);
4814
+ container.removeAttribute("tabindex");
4815
+ container.removeAttribute("aria-roledescription");
4816
+ container.removeAttribute("aria-label");
4817
+ };
4818
+ }
4527
4819
  const markElements = [];
4528
4820
  const allMarkEls = svg.querySelectorAll("[data-mark-id]");
4529
4821
  for (const el of allMarkEls) {
@@ -4596,9 +4888,15 @@ function wireKeyboardNav(svg, container, tooltipDescriptors, tooltipManager, lay
4596
4888
  }
4597
4889
  }
4598
4890
  };
4891
+ const handleBlur = () => {
4892
+ tooltipManager.hide();
4893
+ highlightMark(-1);
4894
+ };
4599
4895
  container.addEventListener("keydown", handleKeyDown);
4896
+ container.addEventListener("blur", handleBlur);
4600
4897
  return () => {
4601
4898
  container.removeEventListener("keydown", handleKeyDown);
4899
+ container.removeEventListener("blur", handleBlur);
4602
4900
  container.removeAttribute("tabindex");
4603
4901
  container.removeAttribute("aria-roledescription");
4604
4902
  container.removeAttribute("aria-label");
@@ -4606,22 +4904,47 @@ function wireKeyboardNav(svg, container, tooltipDescriptors, tooltipManager, lay
4606
4904
  }
4607
4905
 
4608
4906
  // src/interactions/legend-interaction.ts
4609
- function wireLegendInteraction(svg, _layout, toggleSeries, onLegendToggle, onEdit) {
4907
+ function wireLegendInteraction(svg, _layout, toggleSeries, onLegendToggle, onEdit, emphasis) {
4610
4908
  const legendEntries = svg.querySelectorAll(
4611
4909
  ".oc-legend:not(.oc-legend--size) [data-legend-index]"
4612
4910
  );
4613
4911
  const cleanups = [];
4614
4912
  for (const entry of legendEntries) {
4615
4913
  if (entry.getAttribute("data-legend-overflow") === "true") continue;
4616
- const handleClick = () => {
4914
+ const toggle = () => {
4617
4915
  const label = entry.getAttribute("data-legend-label");
4618
4916
  if (!label) return;
4619
4917
  const nowHidden = toggleSeries(label);
4620
4918
  onLegendToggle?.(label, !nowHidden);
4621
4919
  onEdit?.({ type: "legend-toggle", series: label, hidden: nowHidden });
4622
4920
  };
4921
+ const handleClick = () => toggle();
4922
+ const handleKeyDown = (e) => {
4923
+ const ke = e;
4924
+ if (ke.key !== "Enter" && ke.key !== " ") return;
4925
+ ke.preventDefault();
4926
+ toggle();
4927
+ };
4928
+ const handleEnter = () => {
4929
+ const label = entry.getAttribute("data-legend-label");
4930
+ if (!label || entry.getAttribute("data-legend-active") === "false") return;
4931
+ emphasis?.setSeries(label);
4932
+ };
4933
+ const handleLeave = () => emphasis?.clear();
4623
4934
  entry.addEventListener("click", handleClick);
4624
- cleanups.push(() => entry.removeEventListener("click", handleClick));
4935
+ entry.addEventListener("keydown", handleKeyDown);
4936
+ entry.addEventListener("mouseenter", handleEnter);
4937
+ entry.addEventListener("mouseleave", handleLeave);
4938
+ entry.addEventListener("focus", handleEnter);
4939
+ entry.addEventListener("blur", handleLeave);
4940
+ cleanups.push(() => {
4941
+ entry.removeEventListener("click", handleClick);
4942
+ entry.removeEventListener("keydown", handleKeyDown);
4943
+ entry.removeEventListener("mouseenter", handleEnter);
4944
+ entry.removeEventListener("mouseleave", handleLeave);
4945
+ entry.removeEventListener("focus", handleEnter);
4946
+ entry.removeEventListener("blur", handleLeave);
4947
+ });
4625
4948
  }
4626
4949
  return () => {
4627
4950
  for (const cleanup of cleanups) {
@@ -4850,7 +5173,7 @@ function createScreenReaderTable(layout, container) {
4850
5173
  }
4851
5174
 
4852
5175
  // src/interactions/tooltip-events.ts
4853
- function wireTooltipEvents(svg, tooltipDescriptors, tooltipManager) {
5176
+ function wireTooltipEvents(svg, tooltipDescriptors, tooltipManager, emphasis) {
4854
5177
  const markElements = svg.querySelectorAll("[data-mark-id]");
4855
5178
  const cleanups = [];
4856
5179
  for (const el of markElements) {
@@ -4864,6 +5187,7 @@ function wireTooltipEvents(svg, tooltipDescriptors, tooltipManager) {
4864
5187
  const x = mouseEvent.clientX - svgRect.left;
4865
5188
  const y = mouseEvent.clientY - svgRect.top;
4866
5189
  tooltipManager.show(content, x, y);
5190
+ emphasis?.setMark(el);
4867
5191
  };
4868
5192
  const handleMouseMove = (e) => {
4869
5193
  const mouseEvent = e;
@@ -4874,6 +5198,7 @@ function wireTooltipEvents(svg, tooltipDescriptors, tooltipManager) {
4874
5198
  };
4875
5199
  const handleMouseLeave = () => {
4876
5200
  tooltipManager.hide();
5201
+ emphasis?.clear();
4877
5202
  };
4878
5203
  const handleTouchStart = (e) => {
4879
5204
  const touchEvent = e;
@@ -4883,17 +5208,23 @@ function wireTooltipEvents(svg, tooltipDescriptors, tooltipManager) {
4883
5208
  const x = touch.clientX - svgRect.left;
4884
5209
  const y = touch.clientY - svgRect.top;
4885
5210
  tooltipManager.show(content, x, y);
5211
+ emphasis?.setMark(el);
4886
5212
  }
4887
5213
  };
5214
+ const handleTouchEnd = () => {
5215
+ emphasis?.clear();
5216
+ };
4888
5217
  el.addEventListener("mouseenter", handleMouseEnter);
4889
5218
  el.addEventListener("mousemove", handleMouseMove);
4890
5219
  el.addEventListener("mouseleave", handleMouseLeave);
4891
5220
  el.addEventListener("touchstart", handleTouchStart);
5221
+ el.addEventListener("touchend", handleTouchEnd);
4892
5222
  cleanups.push(() => {
4893
5223
  el.removeEventListener("mouseenter", handleMouseEnter);
4894
5224
  el.removeEventListener("mousemove", handleMouseMove);
4895
5225
  el.removeEventListener("mouseleave", handleMouseLeave);
4896
5226
  el.removeEventListener("touchstart", handleTouchStart);
5227
+ el.removeEventListener("touchend", handleTouchEnd);
4897
5228
  });
4898
5229
  }
4899
5230
  return () => {
@@ -5274,7 +5605,7 @@ function createTween(opts) {
5274
5605
  }
5275
5606
 
5276
5607
  // src/scatter-canvas/entrance.ts
5277
- var MAX_TOTAL_STAGGER_MS = 2e3;
5608
+ var MAX_TOTAL_STAGGER_MS = 300;
5278
5609
  var POINT_DURATION_FRACTION = 0.4;
5279
5610
  function clampStagger(delay, elementCount) {
5280
5611
  if (elementCount <= 1) return 0;
@@ -5709,46 +6040,11 @@ function createSeriesSearch(options) {
5709
6040
  };
5710
6041
  }
5711
6042
 
5712
- // src/theme-tokens.ts
5713
- import { adaptForLightLineStroke, cssTokenDefault } from "@opendata-ai/openchart-core";
5714
- function stampThemeProperties(el, theme) {
5715
- const accent = theme.colors.categorical[0] ?? cssTokenDefault("--oc-accent", "light");
5716
- const bg = theme.colors.background === "transparent" ? theme.isDark ? cssTokenDefault("--oc-bg", "dark") : cssTokenDefault("--oc-bg", "light") : theme.colors.background;
5717
- const props = [
5718
- ["--oc-font-family", theme.fonts.family],
5719
- ["--oc-font-mono", theme.fonts.mono],
5720
- ["--oc-title-size", `${theme.chrome.title.fontSize}px`],
5721
- ["--oc-title-weight", `${theme.chrome.title.fontWeight}`],
5722
- ["--oc-subtitle-size", `${theme.chrome.subtitle.fontSize}px`],
5723
- ["--oc-subtitle-weight", `${theme.chrome.subtitle.fontWeight}`],
5724
- ["--oc-source-size", `${theme.chrome.source.fontSize}px`],
5725
- ["--oc-source-weight", `${theme.chrome.source.fontWeight}`],
5726
- ["--oc-body-size", `${theme.fonts.sizes.body}px`],
5727
- ["--oc-eyebrow-size", `${theme.chrome.eyebrow.fontSize}px`],
5728
- ["--oc-eyebrow-weight", `${theme.chrome.eyebrow.fontWeight}`],
5729
- ["--oc-bg", bg],
5730
- ["--oc-text", theme.colors.text],
5731
- ["--oc-text-muted", theme.colors.axis],
5732
- ["--oc-gridline", theme.colors.gridline],
5733
- ["--oc-axis", theme.colors.axis],
5734
- ["--oc-border-radius", `${theme.borderRadius}px`],
5735
- ["--oc-accent", accent],
5736
- ["--oc-accent-strong", adaptForLightLineStroke(accent)],
5737
- ["--oc-positive", theme.colors.positive],
5738
- ["--oc-negative", theme.colors.negative],
5739
- ["--oc-space-2", `${theme.spacing.chromeGap * 2}px`],
5740
- ["--oc-space-4", `${theme.spacing.padding}px`]
5741
- ];
5742
- for (const [name, value] of props) {
5743
- el.style.setProperty(name, value);
5744
- }
5745
- }
5746
-
5747
6043
  // src/mount.ts
5748
6044
  var FALLBACK_HEIGHT = 400;
5749
6045
  var EDITABLE_HOVER_CSS = `
5750
6046
  .oc-editable-hover {
5751
- outline: 1.5px solid ${cssTokenDefault2("--oc-editable-hover", "light")};
6047
+ outline: 1.5px solid ${cssTokenDefault("--oc-editable-hover", "light")};
5752
6048
  outline-offset: 2px;
5753
6049
  border-radius: 2px;
5754
6050
  }
@@ -5787,6 +6083,9 @@ function createChart(container, spec, options) {
5787
6083
  let disconnectResize = null;
5788
6084
  let cleanupTooltipEvents = null;
5789
6085
  let cleanupVoronoiEvents = null;
6086
+ let cleanupEndpointHover = null;
6087
+ let hoverEmphasis = null;
6088
+ let crosshairController = null;
5790
6089
  let canvasLayer = null;
5791
6090
  let cleanupCanvasEvents = null;
5792
6091
  let canvasEntrance = null;
@@ -6228,6 +6527,11 @@ function createChart(container, spec, options) {
6228
6527
  cleanupTooltipEvents = null;
6229
6528
  cleanupVoronoiEvents?.();
6230
6529
  cleanupVoronoiEvents = null;
6530
+ crosshairController = null;
6531
+ cleanupEndpointHover?.();
6532
+ cleanupEndpointHover = null;
6533
+ hoverEmphasis?.destroy();
6534
+ hoverEmphasis = null;
6231
6535
  cleanupCanvasEvents?.();
6232
6536
  cleanupCanvasEvents = null;
6233
6537
  canvasLayer?.destroy();
@@ -6293,19 +6597,30 @@ function createChart(container, spec, options) {
6293
6597
  options
6294
6598
  });
6295
6599
  }
6600
+ hoverEmphasis = createHoverEmphasis(svgElement, currentLayout);
6296
6601
  cleanupTooltipEvents = wireTooltipEvents(
6297
6602
  svgElement,
6298
6603
  currentLayout.tooltipDescriptors,
6299
- tooltipManager
6604
+ tooltipManager,
6605
+ hoverEmphasis
6300
6606
  );
6301
- cleanupVoronoiEvents = wireVoronoiTooltipEvents(svgElement, currentLayout, tooltipManager);
6607
+ const crosshairCtl = wireVoronoiTooltipEvents(
6608
+ svgElement,
6609
+ currentLayout,
6610
+ tooltipManager,
6611
+ hoverEmphasis
6612
+ );
6613
+ crosshairController = crosshairCtl;
6614
+ cleanupVoronoiEvents = crosshairCtl ? () => crosshairCtl.cleanup() : null;
6615
+ cleanupEndpointHover = wireEndpointLabelHover(svgElement, hoverEmphasis);
6302
6616
  if (!currentLayout.a11y.hidden) {
6303
6617
  cleanupKeyboardNav = wireKeyboardNav(
6304
6618
  svgElement,
6305
6619
  container,
6306
6620
  currentLayout.tooltipDescriptors,
6307
6621
  tooltipManager,
6308
- currentLayout
6622
+ currentLayout,
6623
+ crosshairController
6309
6624
  );
6310
6625
  }
6311
6626
  const editSuppressed = wantsSeriesSearch(currentSpec);
@@ -6320,7 +6635,8 @@ function createChart(container, spec, options) {
6320
6635
  currentLayout,
6321
6636
  toggleSeriesVisibility,
6322
6637
  options?.onLegendToggle,
6323
- editSuppressed ? void 0 : options?.onEdit
6638
+ editSuppressed ? void 0 : options?.onEdit,
6639
+ hoverEmphasis
6324
6640
  );
6325
6641
  if (options?.onMarkClick || options?.onMarkHover || options?.onMarkLeave || options?.onAnnotationClick) {
6326
6642
  const specAnnotations = "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
@@ -6616,6 +6932,11 @@ function createChart(container, spec, options) {
6616
6932
  cleanupTooltipEvents = null;
6617
6933
  cleanupVoronoiEvents?.();
6618
6934
  cleanupVoronoiEvents = null;
6935
+ crosshairController = null;
6936
+ cleanupEndpointHover?.();
6937
+ cleanupEndpointHover = null;
6938
+ hoverEmphasis?.destroy();
6939
+ hoverEmphasis = null;
6619
6940
  cleanupCanvasEvents?.();
6620
6941
  cleanupCanvasEvents = null;
6621
6942
  canvasEntrance?.cancel();
@@ -6915,6 +7236,24 @@ var SVG_NS3 = "http://www.w3.org/2000/svg";
6915
7236
  var XLINK_NS = "http://www.w3.org/1999/xlink";
6916
7237
  var BRAND_URL = "https://tryopendata.ai";
6917
7238
  var BULK_ANIMATION_THRESHOLD = 200;
7239
+ var LEGEND_BACKDROP_ALPHA = 0.9;
7240
+ function legendBackdrop(theme) {
7241
+ const surface = resolvedSurface(theme).trim();
7242
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(surface);
7243
+ if (hex) {
7244
+ const h = hex[1];
7245
+ const full = h.length === 3 ? h.replace(/./g, (c) => c + c) : h;
7246
+ const r = Number.parseInt(full.slice(0, 2), 16);
7247
+ const g = Number.parseInt(full.slice(2, 4), 16);
7248
+ const b = Number.parseInt(full.slice(4, 6), 16);
7249
+ return `rgba(${r},${g},${b},${LEGEND_BACKDROP_ALPHA})`;
7250
+ }
7251
+ const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(surface);
7252
+ if (rgb) {
7253
+ return `rgba(${rgb[1]},${rgb[2]},${rgb[3]},${LEGEND_BACKDROP_ALPHA})`;
7254
+ }
7255
+ return surface;
7256
+ }
6918
7257
  function createSVGElement2(tag) {
6919
7258
  return document.createElementNS(SVG_NS3, tag);
6920
7259
  }
@@ -7001,7 +7340,7 @@ function renderBorders(parent, borders) {
7001
7340
  d: borders.interiorPath,
7002
7341
  fill: "none",
7003
7342
  stroke: borders.interiorStroke,
7004
- "stroke-width": 0.5,
7343
+ "stroke-width": borders.interiorWidth,
7005
7344
  "stroke-linejoin": "round"
7006
7345
  });
7007
7346
  interior.setAttribute("class", "oc-map-border-interior");
@@ -7014,7 +7353,7 @@ function renderBorders(parent, borders) {
7014
7353
  d: borders.outlinePath,
7015
7354
  fill: "none",
7016
7355
  stroke: borders.outlineStroke,
7017
- "stroke-width": 1,
7356
+ "stroke-width": borders.outlineWidth,
7018
7357
  "stroke-linejoin": "round"
7019
7358
  });
7020
7359
  outline.setAttribute("class", "oc-map-border-outline");
@@ -7023,7 +7362,7 @@ function renderBorders(parent, borders) {
7023
7362
  }
7024
7363
  parent.appendChild(g);
7025
7364
  }
7026
- function renderFeatures(parent, features, animation, staggerBudget = 0, focusIds) {
7365
+ function renderFeatures(parent, features, animation, staggerBudget = 0, focusIds, patternMap) {
7027
7366
  const g = createSVGElement2("g");
7028
7367
  g.setAttribute("class", "oc-map-features");
7029
7368
  g.setAttribute("role", "list");
@@ -7052,14 +7391,21 @@ function renderFeatures(parent, features, animation, staggerBudget = 0, focusIds
7052
7391
  const path = createSVGElement2("path");
7053
7392
  path.setAttribute("class", "oc-map-feature");
7054
7393
  path.setAttribute("role", "listitem");
7394
+ const fill = feature.pattern && patternMap ? resolvePatternFill(feature.pattern, patternMap) : feature.fill;
7055
7395
  setAttrs2(path, {
7056
7396
  d: feature.path,
7057
- fill: feature.fill,
7397
+ fill,
7058
7398
  stroke: feature.stroke,
7059
7399
  "stroke-width": feature.strokeWidth
7060
7400
  });
7061
7401
  path.setAttribute("data-feature-id", String(feature.id));
7402
+ if (feature.noData) {
7403
+ path.setAttribute("data-nodata", "true");
7404
+ }
7062
7405
  path.setAttribute("data-key", String(feature.id));
7406
+ if (feature.binIndex !== void 0) {
7407
+ path.setAttribute("data-bin-index", String(feature.binIndex));
7408
+ }
7063
7409
  if (feature.name) {
7064
7410
  path.setAttribute("data-feature-name", feature.name);
7065
7411
  }
@@ -7075,7 +7421,7 @@ function renderFeatures(parent, features, animation, staggerBudget = 0, focusIds
7075
7421
  const idx = feature.animationIndex ?? i;
7076
7422
  path.setAttribute("data-animation-index", String(idx));
7077
7423
  s.setProperty("--oc-mark-index", String(idx));
7078
- s.setProperty("--oc-feature-fill", feature.fill);
7424
+ s.setProperty("--oc-feature-fill", fill);
7079
7425
  if (dimmed) {
7080
7426
  s.setProperty("--oc-feature-target-opacity", String(FOCUS_DIM_OPACITY));
7081
7427
  }
@@ -7148,6 +7494,8 @@ function renderMapSVG(layout, opts) {
7148
7494
  }
7149
7495
  const defs = createSVGElement2("defs");
7150
7496
  svg.appendChild(defs);
7497
+ const noDataSwatch = layout.continuousLegend?.noData ?? layout.categoricalLegend?.noData;
7498
+ const patternMap = buildPatternDefs(noDataSwatch ? [...features, noDataSwatch] : features, defs);
7151
7499
  const mapGroup = createSVGElement2("g");
7152
7500
  mapGroup.setAttribute("class", "oc-map-group");
7153
7501
  mapGroup.setAttribute("transform", `translate(${layout.area.x},${layout.area.y})`);
@@ -7160,7 +7508,8 @@ function renderMapSVG(layout, opts) {
7160
7508
  features,
7161
7509
  animate ? animation : void 0,
7162
7510
  mapStaggerBudget,
7163
- focusIdSet
7511
+ focusIdSet,
7512
+ patternMap
7164
7513
  );
7165
7514
  renderBorders(cameraGroup, borders);
7166
7515
  if (animate && animation?.enter && layout.pointMarks.length > 1) {
@@ -7173,13 +7522,26 @@ function renderMapSVG(layout, opts) {
7173
7522
  svg.appendChild(mapGroup);
7174
7523
  renderChrome(svg, layout);
7175
7524
  if (layout.continuousLegend) {
7176
- renderLegend(svg, layout.continuousLegend);
7525
+ const contNoData = layout.continuousLegend.noData;
7526
+ renderLegend(
7527
+ svg,
7528
+ contNoData?.pattern ? {
7529
+ ...layout.continuousLegend,
7530
+ noData: { ...contNoData, fill: resolvePatternFill(contNoData.pattern, patternMap) }
7531
+ } : layout.continuousLegend
7532
+ );
7177
7533
  } else if (layout.categoricalLegend) {
7178
- renderLegend(svg, layout.categoricalLegend);
7534
+ const catNoData = layout.categoricalLegend.noData;
7535
+ renderLegend(
7536
+ svg,
7537
+ catNoData?.pattern ? {
7538
+ ...layout.categoricalLegend,
7539
+ noData: { ...catNoData, fill: resolvePatternFill(catNoData.pattern, patternMap) }
7540
+ } : layout.categoricalLegend
7541
+ );
7179
7542
  }
7180
7543
  const ptLegend = layout.pointCategoricalLegend ?? layout.pointContinuousLegend;
7181
7544
  if (ptLegend) {
7182
- const isDark = layout.theme.isDark;
7183
7545
  const padX = 10;
7184
7546
  const padY = 8;
7185
7547
  const swatchSize = "swatchSize" in ptLegend ? ptLegend.swatchSize : 10;
@@ -7209,11 +7571,26 @@ function renderMapSVG(layout, opts) {
7209
7571
  width: contentWidth + padX * 2,
7210
7572
  height: bgHeight,
7211
7573
  rx: 4,
7212
- fill: isDark ? "rgba(24,24,27,0.9)" : "rgba(255,255,255,0.9)"
7574
+ fill: legendBackdrop(layout.theme)
7213
7575
  });
7214
7576
  svg.appendChild(bg);
7215
7577
  renderLegend(svg, ptLegend);
7216
7578
  }
7579
+ if (layout.pointSizeLegend) {
7580
+ const sizeLegend = layout.pointSizeLegend;
7581
+ const pad = 10;
7582
+ const bg = createSVGElement2("rect");
7583
+ setAttrs2(bg, {
7584
+ x: sizeLegend.bounds.x - pad,
7585
+ y: sizeLegend.bounds.y - pad,
7586
+ width: sizeLegend.bounds.width + pad * 2,
7587
+ height: sizeLegend.bounds.height + pad * 2,
7588
+ rx: 6,
7589
+ fill: legendBackdrop(layout.theme)
7590
+ });
7591
+ svg.appendChild(bg);
7592
+ renderLegend(svg, sizeLegend);
7593
+ }
7217
7594
  if (watermark) {
7218
7595
  renderWatermark(svg, layout);
7219
7596
  }
@@ -7347,6 +7724,27 @@ function focusSignature(focus) {
7347
7724
  const ids = focus.ids.map(String).sort().join(",");
7348
7725
  return `${ids}|${Math.round(t.x)},${Math.round(t.y)},${Math.round(t.width)},${Math.round(t.height)},${t.padding}`;
7349
7726
  }
7727
+ var featureOrigin = /* @__PURE__ */ new WeakMap();
7728
+ function raiseFeature(el, svg) {
7729
+ if (svg.classList.contains("oc-animate")) return;
7730
+ const parent = el.parentNode;
7731
+ if (!parent || el === parent.lastChild) return;
7732
+ if (!featureOrigin.has(el)) {
7733
+ featureOrigin.set(el, el.nextSibling);
7734
+ }
7735
+ parent.appendChild(el);
7736
+ }
7737
+ function restoreFeatureOrder(el) {
7738
+ const parent = el.parentNode;
7739
+ if (!parent || !featureOrigin.has(el)) return;
7740
+ const next = featureOrigin.get(el) ?? null;
7741
+ featureOrigin.delete(el);
7742
+ if (next && next.parentNode === parent) {
7743
+ parent.insertBefore(el, next);
7744
+ } else if (!next) {
7745
+ parent.appendChild(el);
7746
+ }
7747
+ }
7350
7748
  function prefersReducedMotion2() {
7351
7749
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
7352
7750
  try {
@@ -7419,7 +7817,8 @@ function createGeoMap(container, spec, options) {
7419
7817
  const handleMouseEnter = (e) => {
7420
7818
  const mouseEvent = e;
7421
7819
  el.style.setProperty("cursor", "pointer");
7422
- el.style.setProperty("filter", "brightness(1.1)");
7820
+ raiseFeature(el, svg);
7821
+ el.classList.add("oc-map-feature--hover");
7423
7822
  if (content && tooltipManager && options?.tooltip !== false) {
7424
7823
  const svgRect = svg.getBoundingClientRect();
7425
7824
  const x = mouseEvent.clientX - svgRect.left;
@@ -7446,7 +7845,8 @@ function createGeoMap(container, spec, options) {
7446
7845
  };
7447
7846
  const handleMouseLeave = () => {
7448
7847
  el.style.removeProperty("cursor");
7449
- el.style.removeProperty("filter");
7848
+ el.classList.remove("oc-map-feature--hover");
7849
+ restoreFeatureOrder(el);
7450
7850
  tooltipManager?.hide();
7451
7851
  options?.onMarkHover?.(null);
7452
7852
  };
@@ -7471,6 +7871,22 @@ function createGeoMap(container, spec, options) {
7471
7871
  el.removeEventListener("click", handleClick);
7472
7872
  });
7473
7873
  }
7874
+ const featuresGroup = svg.querySelector(".oc-map-features");
7875
+ if (featuresGroup) {
7876
+ for (const bin of svg.querySelectorAll(".oc-legend-bin[data-bin-index]")) {
7877
+ const index = bin.getAttribute("data-bin-index");
7878
+ if (index === null) continue;
7879
+ const enter = () => featuresGroup.setAttribute("data-hover", index);
7880
+ const leave = () => featuresGroup.removeAttribute("data-hover");
7881
+ bin.addEventListener("mouseenter", enter);
7882
+ bin.addEventListener("mouseleave", leave);
7883
+ cleanups.push(() => {
7884
+ bin.removeEventListener("mouseenter", enter);
7885
+ bin.removeEventListener("mouseleave", leave);
7886
+ featuresGroup.removeAttribute("data-hover");
7887
+ });
7888
+ }
7889
+ }
7474
7890
  const pointMarksByKey = new Map(layout.pointMarks.map((p) => [p.key, p]));
7475
7891
  const pointElements = svg.querySelectorAll(".oc-map-point");
7476
7892
  for (const el of pointElements) {
@@ -7895,4 +8311,4 @@ export {
7895
8311
  scrubCamera,
7896
8312
  createGeoMap
7897
8313
  };
7898
- //# sourceMappingURL=chunk-GRA2NIRZ.js.map
8314
+ //# sourceMappingURL=chunk-5A2NLYUW.js.map