twico-ui 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1654,14 +1654,15 @@ var CHART_BASE_CSS = `
1654
1654
  .twc-chart__leg[data-off="true"] { opacity: 0.4; }
1655
1655
  .twc-chart__leg-sw { width: 10px; height: 10px; border-radius: 3px; flex: none; }
1656
1656
 
1657
- /* Floating tooltip \u2014 a styled card that follows the pointer (ApexCharts / MUI X style). */
1657
+ /* Floating tooltip \u2014 a styled card that follows the pointer (ApexCharts / MUI X style).
1658
+ Positioned inline by <ChartTooltip> (which flips below / clamps to the container so it
1659
+ never clips); only the opacity fades in here so it can't fight the inline transform. */
1658
1660
  .twc-chart__tip {
1659
- position: absolute; z-index: 3; top: 0; left: 0; pointer-events: none;
1660
- transform: translate(-50%, calc(-100% - 12px));
1661
- min-width: 96px; max-width: 260px; padding: 8px 10px;
1661
+ position: absolute; z-index: 3; top: 0; left: 0; pointer-events: none; will-change: transform;
1662
+ min-width: 88px; max-width: 240px; padding: 7px 9px;
1662
1663
  background: var(--color-surface-raised); color: var(--color-text);
1663
1664
  border: var(--border-thin) solid var(--color-border); border-radius: var(--radius-md);
1664
- box-shadow: var(--shadow-lg); font-size: var(--text-xs); line-height: 1.45;
1665
+ box-shadow: var(--shadow-lg); font-size: var(--text-xs); line-height: 1.4;
1665
1666
  opacity: 0; animation: twc-chart-tip-in var(--duration-fast) var(--ease-standard) forwards;
1666
1667
  }
1667
1668
  .twc-chart__tip-title { font-weight: var(--font-bold); color: var(--color-text); margin-bottom: 4px; white-space: nowrap; }
@@ -1670,12 +1671,36 @@ var CHART_BASE_CSS = `
1670
1671
  .twc-chart__tip-sw { width: 9px; height: 9px; border-radius: 2px; flex: none; }
1671
1672
  .twc-chart__tip-label { color: var(--color-text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; }
1672
1673
  .twc-chart__tip-val { font-weight: var(--font-semibold); color: var(--color-text); font-variant-numeric: tabular-nums; }
1673
- @keyframes twc-chart-tip-in { from { opacity: 0; transform: translate(-50%, calc(-100% - 6px)); } to { opacity: 1; transform: translate(-50%, calc(-100% - 12px)); } }
1674
+ @keyframes twc-chart-tip-in { from { opacity: 0; } to { opacity: 1; } }
1674
1675
 
1675
1676
  /* Hover emphasis: when the chart marks itself hovered, fade every mark except the active one. */
1676
- .twc-chart[data-hovering="true"] [data-mark] { opacity: 0.28; transition: opacity var(--duration-fast) var(--ease-standard); }
1677
+ .twc-chart[data-hovering="true"] [data-mark] { opacity: 0.3; transition: opacity var(--duration-base) var(--ease-standard); }
1677
1678
  .twc-chart[data-hovering="true"] [data-mark][data-active="true"] { opacity: 1; }
1678
- [data-mark] { transition: opacity var(--duration-fast) var(--ease-standard); cursor: default; }
1679
+ [data-mark] { transition: opacity var(--duration-base) var(--ease-standard), transform var(--duration-base) var(--ease-spring); cursor: default; }
1680
+
1681
+ /* Click-to-select: a soft accent ring + glow on the chosen mark (not a heavy outline). */
1682
+ [data-mark][data-selected="true"] { stroke: var(--color-text); stroke-width: 1.5; stroke-opacity: 0.5; paint-order: stroke; filter: drop-shadow(0 0 3px color-mix(in srgb, var(--color-text) 22%, transparent)); }
1683
+ /* Click-to-focus: a persistent selection also dims the other marks (focus mode), on every chart. */
1684
+ .twc-chart[data-has-selection="true"] [data-mark]:not([data-selected="true"]) { opacity: 0.4; }
1685
+ .twc-chart[data-clickable="true"] [data-mark], .twc-chart__overlay[data-clickable="true"] { cursor: pointer; }
1686
+
1687
+ /* Crosshair + full-plot event overlay (cartesian shared-axis interactions). */
1688
+ .twc-chart__overlay, .twc-chart__zone { fill: transparent; }
1689
+ .twc-chart__crosshair { stroke: var(--color-border-strong); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.7; pointer-events: none; }
1690
+
1691
+ /* Drag-to-zoom affordances: a crosshair cursor over the zoom overlay + a bordered band. */
1692
+ .twc-chart__overlay[data-zoom="true"] { cursor: crosshair; }
1693
+ .twc-chart__overlay[data-zoom="true"][data-panning="true"] { cursor: grabbing; }
1694
+ .twc-chart__zoomband { fill: var(--color-primary); opacity: 0.14; stroke: var(--color-primary); stroke-opacity: 0.5; stroke-width: 1; pointer-events: none; }
1695
+ .twc-chart__zoom-reset {
1696
+ position: absolute; top: 6px; inset-inline-end: 8px; z-index: 2;
1697
+ display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px;
1698
+ font-family: var(--font-sans); font-size: var(--text-xs); color: var(--color-text-muted);
1699
+ background: var(--color-surface-raised); border: var(--border-thin) solid var(--color-border);
1700
+ border-radius: var(--radius-md); box-shadow: var(--shadow-sm); cursor: pointer;
1701
+ }
1702
+ .twc-chart__zoom-reset:hover { color: var(--color-text); border-color: var(--color-border-strong); }
1703
+ .twc-chart__zoom-reset svg { width: 12px; height: 12px; }
1679
1704
 
1680
1705
  /* Entrance animations (respect reduced motion, below). */
1681
1706
  @keyframes twc-chart-fade { from { opacity: 0; } to { opacity: 1; } }
@@ -1723,7 +1748,7 @@ function ChartTable({ id, caption, columns, rows }) {
1723
1748
  )
1724
1749
  );
1725
1750
  }
1726
- function ChartLegend({ items, onToggle, hidden }) {
1751
+ function ChartLegend({ items, onToggle, hidden, onFocus, onBlur }) {
1727
1752
  const h = React51__default.createElement;
1728
1753
  return h(
1729
1754
  "div",
@@ -1744,7 +1769,9 @@ function ChartLegend({ items, onToggle, hidden }) {
1744
1769
  e.preventDefault();
1745
1770
  onToggle(it, i);
1746
1771
  }
1747
- } : void 0
1772
+ } : void 0,
1773
+ onMouseEnter: onFocus ? () => onFocus(it, i) : void 0,
1774
+ onMouseLeave: onBlur ? () => onBlur() : void 0
1748
1775
  },
1749
1776
  h("span", { className: "twc-chart__leg-sw", style: { background: it.color } }),
1750
1777
  it.label
@@ -1767,11 +1794,31 @@ function useChartTooltip() {
1767
1794
  }
1768
1795
  function ChartTooltip({ tip }) {
1769
1796
  const h = React51__default.createElement;
1797
+ const ref = React51__default.useRef(null);
1798
+ const [pos, setPos] = React51__default.useState({ dx: 0, below: false });
1799
+ React51__default.useLayoutEffect(() => {
1800
+ const el = ref.current;
1801
+ if (!tip || !el) return;
1802
+ const parent = el.offsetParent;
1803
+ const pw = parent && parent.clientWidth ? parent.clientWidth : 0;
1804
+ const w = el.offsetWidth || 0, ht = el.offsetHeight || 0;
1805
+ let dx = 0;
1806
+ if (pw > 0 && w > 0) {
1807
+ const half = w / 2;
1808
+ if (tip.left - half < 6) dx = 6 - (tip.left - half);
1809
+ else if (tip.left + half > pw - 6) dx = pw - 6 - (tip.left + half);
1810
+ }
1811
+ setPos({ dx, below: ht > 0 && tip.top - ht - 16 < 0 });
1812
+ }, [tip]);
1770
1813
  if (!tip) return null;
1771
- const left = Math.max(48, tip.left);
1814
+ const style = {
1815
+ left: tip.left + pos.dx,
1816
+ top: tip.top,
1817
+ transform: pos.below ? "translate(-50%, 18px)" : "translate(-50%, calc(-100% - 14px))"
1818
+ };
1772
1819
  return h(
1773
1820
  "div",
1774
- { className: "twc-chart__tip", role: "tooltip", "aria-hidden": "true", style: { left, top: tip.top } },
1821
+ { ref, className: "twc-chart__tip", role: "tooltip", "aria-hidden": "true", style },
1775
1822
  tip.title != null && tip.title !== "" ? h("div", { className: "twc-chart__tip-title" }, tip.title) : null,
1776
1823
  (tip.items || []).map(
1777
1824
  (it, i) => h(
@@ -1787,7 +1834,6 @@ function ChartTooltip({ tip }) {
1787
1834
  var CHART_CSS = `
1788
1835
  .twc-chart__grid { stroke: var(--color-divider); stroke-width: 1; }
1789
1836
  .twc-chart__axisline { stroke: var(--color-border); stroke-width: 1; }
1790
- .twc-chart__tick { stroke: var(--color-border); stroke-width: 1; }
1791
1837
  .twc-chart__axis { fill: var(--color-text-subtle); font-size: 11px; }
1792
1838
  .twc-chart__bar { fill: var(--color-primary); }
1793
1839
  .twc-chart__bar:hover { filter: brightness(1.06); }
@@ -1796,7 +1842,6 @@ var CHART_CSS = `
1796
1842
  .twc-chart__dot { fill: var(--color-surface); stroke: var(--color-primary); stroke-width: 2.5; transition: r var(--duration-fast) var(--ease-standard); }
1797
1843
  .twc-chart__hit { fill: transparent; }
1798
1844
  .twc-chart__hit:hover + .twc-chart__dot, .twc-chart__dot:hover { r: 5; }
1799
- .twc-chart__vline { stroke: var(--color-border-strong); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.7; }
1800
1845
  .twc-chart__label { fill: var(--color-text-muted); font-size: 10px; font-weight: var(--font-semibold); }
1801
1846
  `;
1802
1847
  function Chart({
@@ -1813,9 +1858,12 @@ function Chart({
1813
1858
  showGrid = true,
1814
1859
  showAxis = true,
1815
1860
  showLegend = false,
1861
+ crosshair = true,
1862
+ zoomable = false,
1816
1863
  animate = true,
1817
1864
  colors,
1818
1865
  valueFormat,
1866
+ onDataClick,
1819
1867
  ariaLabel,
1820
1868
  "aria-label": ariaLabelProp,
1821
1869
  tableFallback = true,
@@ -1830,15 +1878,23 @@ function Chart({
1830
1878
  const tableId = tableFallback ? `${uid}-table` : void 0;
1831
1879
  const { containerRef, tip, show, hide } = useChartTooltip();
1832
1880
  const [hidden, setHidden] = React51__default.useState(() => /* @__PURE__ */ new Set());
1881
+ const [focusSeries, setFocusSeries] = React51__default.useState(null);
1882
+ const [selected, setSelected] = React51__default.useState(null);
1883
+ const [hoverIdx, setHoverIdx] = React51__default.useState(null);
1884
+ const [zoom, setZoom] = React51__default.useState(null);
1885
+ const [drag, setDrag] = React51__default.useState(null);
1886
+ const svgRef = React51__default.useRef(null);
1833
1887
  const isBar = type === "bar" || type === "column";
1834
1888
  const isArea = type === "area";
1835
1889
  const crv = smooth ? "smooth" : curve;
1836
1890
  const pathOpts = { smooth: crv === "smooth", step: crv === "stepped" ? "after" : null };
1837
1891
  const canStack = stacked && (isBar || isArea);
1838
- const anim = animate ? "" : null;
1892
+ const clickable = !!onDataClick;
1839
1893
  const allKeys = series && series.length ? series : ["value"];
1840
1894
  const keys = allKeys.filter((k) => !hidden.has(k));
1841
- const rows = data || [];
1895
+ const allRows = data || [];
1896
+ const baseIdx = zoom ? zoom.s : 0;
1897
+ const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
1842
1898
  const fmt = valueFormat || fmtNumber;
1843
1899
  const multi = allKeys.length > 1;
1844
1900
  const W = 600, H = height;
@@ -1856,43 +1912,137 @@ function Chart({
1856
1912
  const catBand = innerW / Math.max(1, rows.length);
1857
1913
  const catBandH = innerH / Math.max(1, rows.length);
1858
1914
  const lineX = (i) => padL + innerW / Math.max(1, rows.length - 1) * i;
1915
+ const catX = (i) => isBar ? padL + catBand * i + catBand / 2 : lineX(i);
1859
1916
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? `${type} chart`;
1860
- const tipFor = (d) => ({
1861
- title: d.label,
1862
- items: keys.map((k) => ({ color: colorFor(k), label: k, value: fmt(Number(d[k]) || 0) }))
1863
- });
1864
1917
  function colorFor(k) {
1865
1918
  const idx = allKeys.indexOf(k);
1866
1919
  return multi || colors && colors.length ? paletteAt(colors, idx) : "var(--color-primary)";
1867
1920
  }
1921
+ const tipFor = (d) => ({
1922
+ title: d.label,
1923
+ items: keys.map((k) => ({ color: colorFor(k), label: k, value: fmt(Number(d[k]) || 0) }))
1924
+ });
1925
+ const focusActive = (k) => focusSeries == null ? void 0 : k === focusSeries || void 0;
1926
+ const onMarkMove = (d, localIdx, e) => {
1927
+ show(tipFor(d), e);
1928
+ setHoverIdx(localIdx);
1929
+ };
1930
+ const onMarkLeave = () => {
1931
+ hide();
1932
+ setHoverIdx(null);
1933
+ };
1934
+ const clickDatum = (d, k, localIdx) => {
1935
+ if (!onDataClick) return;
1936
+ onDataClick({ label: d.label, series: k, seriesIndex: allKeys.indexOf(k), value: Number(d[k]) || 0, index: baseIdx + localIdx, row: d });
1937
+ };
1938
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
1939
+ const applyZoom = (s, e) => {
1940
+ const n = allRows.length;
1941
+ if (e - s >= n - 1) {
1942
+ setZoom(null);
1943
+ return;
1944
+ }
1945
+ setZoom({ s: Math.max(0, s), e: Math.min(n - 1, e) });
1946
+ };
1947
+ const catFromEvent = (e) => {
1948
+ const svg = svgRef.current;
1949
+ if (!svg) return null;
1950
+ const r2 = svg.getBoundingClientRect();
1951
+ if (!(r2.width > 0)) return null;
1952
+ const frac = Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW));
1953
+ const idx = isBar ? Math.floor(frac * rows.length) : Math.round(frac * Math.max(1, rows.length - 1));
1954
+ return Math.min(rows.length - 1, Math.max(0, idx));
1955
+ };
1956
+ const onOverlayMove = (e) => {
1957
+ const idx = catFromEvent(e);
1958
+ if (idx == null) return;
1959
+ setHoverIdx(idx);
1960
+ show(tipFor(rows[idx]), e);
1961
+ if (drag) {
1962
+ if (drag.pan) {
1963
+ const span = drag.z0.e - drag.z0.s;
1964
+ const s = Math.min(allRows.length - 1 - span, Math.max(0, drag.z0.s + (drag.start - idx)));
1965
+ applyZoom(s, s + span);
1966
+ } else {
1967
+ setDrag({ ...drag, end: idx });
1968
+ }
1969
+ }
1970
+ };
1971
+ const onOverlayDown = (e) => {
1972
+ const idx = catFromEvent(e);
1973
+ if (idx == null) return;
1974
+ setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
1975
+ };
1976
+ const onOverlayUp = () => {
1977
+ if (drag && !drag.pan) {
1978
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
1979
+ if (hi - lo >= 1) applyZoom(baseIdx + lo, baseIdx + hi);
1980
+ }
1981
+ setDrag(null);
1982
+ };
1983
+ const onOverlayClick = (e) => {
1984
+ if (!onDataClick) return;
1985
+ const idx = catFromEvent(e);
1986
+ if (idx == null) return;
1987
+ const d = rows[idx];
1988
+ onDataClick({ label: d.label, series: null, seriesIndex: -1, value: void 0, index: baseIdx + idx, row: d });
1989
+ selectMark(`${idx}:*`);
1990
+ };
1991
+ React51__default.useEffect(() => {
1992
+ if (!zoomable) return void 0;
1993
+ const svg = svgRef.current;
1994
+ if (!svg) return void 0;
1995
+ const onWheel = (e) => {
1996
+ e.preventDefault();
1997
+ const n = allRows.length;
1998
+ const cur = zoom || { s: 0, e: n - 1 };
1999
+ const span = cur.e - cur.s;
2000
+ const r2 = svg.getBoundingClientRect();
2001
+ const frac = r2.width > 0 ? Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW)) : 0.5;
2002
+ const center = cur.s + Math.round(frac * span);
2003
+ const newSpan = Math.min(n - 1, Math.max(1, Math.round(span * (e.deltaY < 0 ? 0.8 : 1.25))));
2004
+ const s = Math.min(n - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frac)));
2005
+ applyZoom(s, s + newSpan);
2006
+ };
2007
+ svg.addEventListener("wheel", onWheel, { passive: false });
2008
+ return () => svg.removeEventListener("wheel", onWheel);
2009
+ }, [zoomable, zoom, allRows.length]);
1868
2010
  const renderBars = () => {
1869
2011
  const band = horizontal ? catBandH : catBand;
1870
- const gap = 0.28;
1871
- const slot = band * (1 - gap);
2012
+ const slot = band * 0.72;
1872
2013
  return rows.map((d, gi) => {
1873
- const gStart = (horizontal ? padT : padL) + band * gi + band * gap / 2;
1874
- const hoverProps = {
1875
- onMouseMove: (e) => show(tipFor(d), e),
1876
- onMouseLeave: hide
1877
- };
2014
+ const gStart = (horizontal ? padT : padL) + band * gi + band * 0.28 / 2;
2015
+ const handlers = { onMouseMove: (e) => onMarkMove(d, gi, e), onMouseLeave: onMarkLeave };
2016
+ const barCommon = (k, key) => ({
2017
+ className: `twc-chart__bar ${animate ? "twc-chart__anim-bar" : ""}`.trim(),
2018
+ "data-mark": true,
2019
+ "data-horizontal": horizontal || void 0,
2020
+ "data-active": focusActive(k),
2021
+ "data-selected": selected === key || void 0,
2022
+ style: { fill: colorFor(k) },
2023
+ rx: 2,
2024
+ ...handlers,
2025
+ onClick: () => {
2026
+ clickDatum(d, k, gi);
2027
+ selectMark(key);
2028
+ }
2029
+ });
1878
2030
  if (canStack) {
1879
2031
  let acc = 0;
1880
- return keys.map((k, si) => {
2032
+ return keys.map((k) => {
1881
2033
  const v = Number(d[k]) || 0;
1882
2034
  const p0 = vPix(acc), p1 = vPix(acc + v);
1883
2035
  acc += v;
1884
- const color = colorFor(k);
1885
- const common = { className: `twc-chart__bar ${anim === "" ? "twc-chart__anim-bar" : ""}`.trim(), "data-horizontal": horizontal || void 0, style: { fill: color }, rx: 2, ...hoverProps };
1886
- return horizontal ? /* @__PURE__ */ jsx("rect", { ...common, x: padL + p0, y: gStart, width: Math.max(0, p1 - p0), height: Math.max(1, slot) }, `${gi}-${si}`) : /* @__PURE__ */ jsx("rect", { ...common, x: gStart, y: padT + innerH - p1, width: Math.max(1, slot), height: Math.max(0, p1 - p0) }, `${gi}-${si}`);
2036
+ const key = `${gi}:${k}`;
2037
+ return horizontal ? /* @__PURE__ */ jsx("rect", { ...barCommon(k, key), x: padL + p0, y: gStart, width: Math.max(0, p1 - p0), height: Math.max(1, slot) }, key) : /* @__PURE__ */ jsx("rect", { ...barCommon(k, key), x: gStart, y: padT + innerH - p1, width: Math.max(1, slot), height: Math.max(0, p1 - p0) }, key);
1887
2038
  });
1888
2039
  }
1889
2040
  const bw = slot / Math.max(1, keys.length);
1890
2041
  return keys.map((k, si) => {
1891
2042
  const v = Number(d[k]) || 0;
1892
2043
  const len2 = vPix(v);
1893
- const color = colorFor(k);
1894
- const common = { className: `twc-chart__bar ${anim === "" ? "twc-chart__anim-bar" : ""}`.trim(), "data-horizontal": horizontal || void 0, style: { fill: color }, rx: 2, ...hoverProps };
1895
- return horizontal ? /* @__PURE__ */ jsx("rect", { ...common, x: padL, y: gStart + si * bw + 0.5, width: Math.max(0, len2), height: Math.max(1, bw - 1) }, `${gi}-${si}`) : /* @__PURE__ */ jsx("rect", { ...common, x: gStart + si * bw + 0.5, y: padT + innerH - len2, width: Math.max(1, bw - 1), height: Math.max(0, len2) }, `${gi}-${si}`);
2044
+ const key = `${gi}:${k}`;
2045
+ return horizontal ? /* @__PURE__ */ jsx("rect", { ...barCommon(k, key), x: padL, y: gStart + si * bw + 0.5, width: Math.max(0, len2), height: Math.max(1, bw - 1) }, key) : /* @__PURE__ */ jsx("rect", { ...barCommon(k, key), x: gStart + si * bw + 0.5, y: padT + innerH - len2, width: Math.max(1, bw - 1), height: Math.max(0, len2) }, key);
1896
2046
  });
1897
2047
  });
1898
2048
  };
@@ -1907,7 +2057,7 @@ function Chart({
1907
2057
  });
1908
2058
  const base = canStack ? rows.map((d, i) => vPos(stackAcc[i] - (Number(d[k]) || 0))) : null;
1909
2059
  const gradId = `twc-grad-${gid}-${allKeys.indexOf(k)}`;
1910
- return /* @__PURE__ */ jsxs("g", { children: [
2060
+ return /* @__PURE__ */ jsxs("g", { "data-mark": true, "data-active": focusActive(k), children: [
1911
2061
  isArea ? /* @__PURE__ */ jsxs(Fragment, { children: [
1912
2062
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gradId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
1913
2063
  /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.34" }),
@@ -1916,14 +2066,14 @@ function Chart({
1916
2066
  /* @__PURE__ */ jsx(
1917
2067
  "path",
1918
2068
  {
1919
- className: `twc-chart__area ${anim === "" ? "twc-chart__anim-fade" : ""}`.trim(),
2069
+ className: `twc-chart__area ${animate ? "twc-chart__anim-fade" : ""}`.trim(),
1920
2070
  style: { fill: `url(#${gradId})` },
1921
2071
  d: canStack ? `${linePath(pts, pathOpts)} ${pts.map((p, i) => `L${p[0]} ${base[i]}`).reverse().join(" ")} Z` : areaPath(pts, padT + innerH, pathOpts)
1922
2072
  }
1923
2073
  )
1924
2074
  ] }) : null,
1925
- /* @__PURE__ */ jsx("path", { className: `twc-chart__line ${anim === "" ? "twc-chart__anim-line" : ""}`.trim(), style: { stroke: color }, pathLength: "1", d: linePath(pts, pathOpts) }),
1926
- showDots || !isArea ? pts.map((p, i) => /* @__PURE__ */ jsxs("g", { className: anim === "" ? "twc-chart__anim-fade" : void 0, children: [
2075
+ /* @__PURE__ */ jsx("path", { className: `twc-chart__line ${animate ? "twc-chart__anim-line" : ""}`.trim(), style: { stroke: color }, pathLength: "1", d: linePath(pts, pathOpts) }),
2076
+ showDots || !isArea ? pts.map((p, i) => /* @__PURE__ */ jsxs("g", { className: animate ? "twc-chart__anim-fade" : void 0, children: [
1927
2077
  /* @__PURE__ */ jsx(
1928
2078
  "circle",
1929
2079
  {
@@ -1931,11 +2081,25 @@ function Chart({
1931
2081
  cx: p[0],
1932
2082
  cy: p[1],
1933
2083
  r: "14",
1934
- onMouseMove: (e) => show(tipFor(rows[i]), e),
1935
- onMouseLeave: hide
2084
+ onMouseMove: (e) => onMarkMove(rows[i], i, e),
2085
+ onMouseLeave: onMarkLeave,
2086
+ onClick: () => {
2087
+ clickDatum(rows[i], k, i);
2088
+ selectMark(`${i}:${k}`);
2089
+ }
1936
2090
  }
1937
2091
  ),
1938
- /* @__PURE__ */ jsx("circle", { className: "twc-chart__dot", style: { stroke: color }, cx: p[0], cy: p[1], r: "3.5" })
2092
+ /* @__PURE__ */ jsx(
2093
+ "circle",
2094
+ {
2095
+ className: "twc-chart__dot",
2096
+ style: { stroke: color },
2097
+ cx: p[0],
2098
+ cy: p[1],
2099
+ r: hoverIdx === i ? 5 : 3.5,
2100
+ "data-selected": selected === `${i}:${k}` || void 0
2101
+ }
2102
+ )
1939
2103
  ] }, i)) : null
1940
2104
  ] }, k);
1941
2105
  });
@@ -1961,62 +2125,153 @@ function Chart({
1961
2125
  return /* @__PURE__ */ jsx("text", { className: "twc-chart__label", x: lineX(i), y: vPos(v) - 9, textAnchor: "middle", children: shortNum(v) }, `${k}-${i}`);
1962
2126
  }));
1963
2127
  };
2128
+ const renderZones = () => rows.map((d, i) => {
2129
+ let x, y, w, hgt;
2130
+ if (isBar && horizontal) {
2131
+ x = padL;
2132
+ w = innerW;
2133
+ y = padT + catBandH * i;
2134
+ hgt = catBandH;
2135
+ } else if (isBar) {
2136
+ x = padL + catBand * i;
2137
+ w = catBand;
2138
+ y = padT;
2139
+ hgt = innerH;
2140
+ } else {
2141
+ const x0 = i === 0 ? padL : (lineX(i - 1) + lineX(i)) / 2;
2142
+ const x1 = i === rows.length - 1 ? padL + innerW : (lineX(i) + lineX(i + 1)) / 2;
2143
+ x = x0;
2144
+ w = x1 - x0;
2145
+ y = padT;
2146
+ hgt = innerH;
2147
+ }
2148
+ return /* @__PURE__ */ jsx(
2149
+ "rect",
2150
+ {
2151
+ className: "twc-chart__zone",
2152
+ x,
2153
+ y,
2154
+ width: Math.max(0, w),
2155
+ height: hgt,
2156
+ "data-clickable": clickable || void 0,
2157
+ onMouseMove: (e) => {
2158
+ show(tipFor(d), e);
2159
+ setHoverIdx(i);
2160
+ },
2161
+ onMouseLeave: onMarkLeave,
2162
+ onClick: () => {
2163
+ if (onDataClick) onDataClick({ label: d.label, series: null, seriesIndex: -1, value: void 0, index: baseIdx + i, row: d });
2164
+ }
2165
+ },
2166
+ i
2167
+ );
2168
+ });
1964
2169
  const toggle = (it) => setHidden((prev) => {
1965
2170
  const next = new Set(prev);
1966
2171
  if (next.has(it.label)) next.delete(it.label);
1967
2172
  else if (allKeys.length - next.size > 1) next.add(it.label);
1968
2173
  return next;
1969
2174
  });
1970
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--${type} ${className}`.trim(), ...rest, children: [
1971
- baseStyles,
1972
- styles,
1973
- /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
1974
- showGrid ? ticks.map((t, i) => {
1975
- const p = vPos(t);
1976
- return horizontal ? /* @__PURE__ */ jsx("line", { className: "twc-chart__grid", x1: p, y1: padT, x2: p, y2: padT + innerH }, i) : /* @__PURE__ */ jsx("line", { className: "twc-chart__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
1977
- }) : null,
1978
- showAxis ? /* @__PURE__ */ jsx("line", { className: "twc-chart__axisline", x1: padL, y1: padT, x2: padL, y2: padT + innerH }) : null,
1979
- showAxis ? /* @__PURE__ */ jsx("line", { className: "twc-chart__axisline", x1: padL, y1: padT + innerH, x2: W - padR, y2: padT + innerH }) : null,
1980
- showAxis ? ticks.map((t, i) => {
1981
- const p = vPos(t);
1982
- return horizontal ? /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: p, y: H - 8, textAnchor: "middle", children: shortNum(t) }, i) : /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: padL - 8, y: p + 4, textAnchor: "end", children: shortNum(t) }, i);
1983
- }) : null,
1984
- isBar ? renderBars() : renderLines(),
1985
- renderValues(),
1986
- showAxis ? rows.map((d, i) => {
1987
- if (horizontal) {
1988
- const cy = padT + catBandH * i + catBandH / 2;
1989
- return /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: padL - 8, y: cy + 4, textAnchor: "end", children: d.label }, i);
1990
- }
1991
- const cx = isBar ? padL + catBand * i + catBand / 2 : lineX(i);
1992
- return /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: cx, y: H - 8, textAnchor: "middle", children: d.label }, i);
1993
- }) : null
1994
- ] }),
1995
- /* @__PURE__ */ jsx(ChartTooltip, { tip }),
1996
- tableFallback ? /* @__PURE__ */ jsx(
1997
- ChartTable,
1998
- {
1999
- id: tableId,
2000
- caption: caption ?? svgAriaLabel,
2001
- columns: allKeys,
2002
- rows: rows.map((d) => ({ label: d.label, values: allKeys.map((k) => fmt(Number(d[k]) || 0)) }))
2003
- }
2004
- ) : null,
2005
- showLegend && multi ? /* @__PURE__ */ jsx(
2006
- ChartLegend,
2007
- {
2008
- items: allKeys.map((k) => ({ label: k, color: colorFor(k) })),
2009
- onToggle: toggle,
2010
- hidden: (it) => hidden.has(it.label)
2011
- }
2012
- ) : null
2013
- ] });
2175
+ const showCrosshair = crosshair && !horizontal && hoverIdx != null && rows[hoverIdx];
2176
+ const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
2177
+ return /* @__PURE__ */ jsxs(
2178
+ "div",
2179
+ {
2180
+ ref: containerRef,
2181
+ className: `twc-chart twc-chart--${type} ${className}`.trim(),
2182
+ "data-hovering": focusSeries != null || void 0,
2183
+ "data-has-selection": selected != null || void 0,
2184
+ "data-clickable": clickable || void 0,
2185
+ ...rest,
2186
+ children: [
2187
+ baseStyles,
2188
+ styles,
2189
+ /* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
2190
+ showGrid ? ticks.map((t, i) => {
2191
+ const p = vPos(t);
2192
+ return horizontal ? /* @__PURE__ */ jsx("line", { className: "twc-chart__grid", x1: p, y1: padT, x2: p, y2: padT + innerH }, i) : /* @__PURE__ */ jsx("line", { className: "twc-chart__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
2193
+ }) : null,
2194
+ showAxis ? /* @__PURE__ */ jsx("line", { className: "twc-chart__axisline", x1: padL, y1: padT, x2: padL, y2: padT + innerH }) : null,
2195
+ showAxis ? /* @__PURE__ */ jsx("line", { className: "twc-chart__axisline", x1: padL, y1: padT + innerH, x2: W - padR, y2: padT + innerH }) : null,
2196
+ showAxis ? ticks.map((t, i) => {
2197
+ const p = vPos(t);
2198
+ return horizontal ? /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: p, y: H - 8, textAnchor: "middle", children: shortNum(t) }, i) : /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: padL - 8, y: p + 4, textAnchor: "end", children: shortNum(t) }, i);
2199
+ }) : null,
2200
+ showCrosshair ? /* @__PURE__ */ jsx("line", { className: "twc-chart__crosshair", x1: catX(hoverIdx), y1: padT, x2: catX(hoverIdx), y2: padT + innerH }) : null,
2201
+ !zoomable ? renderZones() : null,
2202
+ isBar ? renderBars() : renderLines(),
2203
+ renderValues(),
2204
+ showAxis ? rows.map((d, i) => {
2205
+ if (horizontal) {
2206
+ const cy = padT + catBandH * i + catBandH / 2;
2207
+ return /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: padL - 8, y: cy + 4, textAnchor: "end", children: d.label }, i);
2208
+ }
2209
+ return /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: catX(i), y: H - 8, textAnchor: "middle", children: d.label }, i);
2210
+ }) : null,
2211
+ dragBand ? (() => {
2212
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
2213
+ const x0 = isBar ? padL + catBand * lo : lineX(lo);
2214
+ const x1 = isBar ? padL + catBand * (hi + 1) : lineX(hi);
2215
+ return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
2216
+ })() : null,
2217
+ zoomable ? /* @__PURE__ */ jsx(
2218
+ "rect",
2219
+ {
2220
+ className: "twc-chart__overlay",
2221
+ "data-zoom": "true",
2222
+ "data-clickable": clickable || void 0,
2223
+ "data-panning": drag && drag.pan || void 0,
2224
+ x: padL,
2225
+ y: padT,
2226
+ width: innerW,
2227
+ height: innerH,
2228
+ onMouseMove: onOverlayMove,
2229
+ onMouseLeave: () => {
2230
+ onMarkLeave();
2231
+ setDrag(null);
2232
+ },
2233
+ onMouseDown: onOverlayDown,
2234
+ onMouseUp: onOverlayUp,
2235
+ onClick: onOverlayClick
2236
+ }
2237
+ ) : null
2238
+ ] }),
2239
+ zoomable && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
2240
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2241
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
2242
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
2243
+ ] }),
2244
+ "Reset"
2245
+ ] }) : null,
2246
+ /* @__PURE__ */ jsx(ChartTooltip, { tip }),
2247
+ tableFallback ? /* @__PURE__ */ jsx(
2248
+ ChartTable,
2249
+ {
2250
+ id: tableId,
2251
+ caption: caption ?? svgAriaLabel,
2252
+ columns: allKeys,
2253
+ rows: allRows.map((d) => ({ label: d.label, values: allKeys.map((k) => fmt(Number(d[k]) || 0)) }))
2254
+ }
2255
+ ) : null,
2256
+ showLegend && multi ? /* @__PURE__ */ jsx(
2257
+ ChartLegend,
2258
+ {
2259
+ items: allKeys.map((k) => ({ label: k, color: colorFor(k) })),
2260
+ onToggle: toggle,
2261
+ hidden: (it) => hidden.has(it.label),
2262
+ onFocus: (it) => setFocusSeries(it.label),
2263
+ onBlur: () => setFocusSeries(null)
2264
+ }
2265
+ ) : null
2266
+ ]
2267
+ }
2268
+ );
2014
2269
  }
2015
2270
  var CHART_CSS2 = `
2016
2271
  .twc-chart--pie svg { overflow: visible; }
2017
2272
  .twc-chart__slice { transition: opacity var(--duration-fast) var(--ease-standard); stroke: var(--color-surface); stroke-width: 1.5; }
2018
2273
  .twc-chart__slice:hover { opacity: 0.85; }
2019
- .twc-chart__slice-label { fill: var(--color-surface); font-size: 11px; font-weight: 600; }
2274
+ .twc-chart__slice-label { fill: var(--color-chart-on-fill); paint-order: stroke; stroke: var(--color-chart-on-fill-halo); stroke-width: 3px; stroke-linejoin: round; vector-effect: non-scaling-stroke; font-size: 11px; font-weight: 600; }
2020
2275
  .twc-chart__center { fill: var(--color-text); font-weight: 700; }
2021
2276
  .twc-chart__center-sub { fill: var(--color-text-subtle); }
2022
2277
  .twc-chart__empty { fill: var(--color-text-subtle); font-size: var(--text-sm); }
@@ -2033,6 +2288,7 @@ function PieChart({
2033
2288
  valueFormat,
2034
2289
  height = 260,
2035
2290
  colors,
2291
+ onDataClick,
2036
2292
  ariaLabel,
2037
2293
  "aria-label": ariaLabelProp,
2038
2294
  tableFallback = true,
@@ -2046,6 +2302,9 @@ function PieChart({
2046
2302
  const tableId = tableFallback ? `${uid}-table` : void 0;
2047
2303
  const { containerRef, tip, show, hide } = useChartTooltip();
2048
2304
  const [active, setActive] = React51__default.useState(null);
2305
+ const [focus, setFocus] = React51__default.useState(null);
2306
+ const [selected, setSelected] = React51__default.useState(null);
2307
+ const clickable = !!onDataClick;
2049
2308
  const rows = data || [];
2050
2309
  const fmt = valueFormat || fmtNumber;
2051
2310
  const values = rows.map((d) => Math.max(0, Number(d.value) || 0));
@@ -2069,20 +2328,37 @@ function PieChart({
2069
2328
  const color = d.color || paletteAt(colors, i);
2070
2329
  return { d, i, v, frac, s0, s1, span, pct, color };
2071
2330
  });
2072
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--pie ${className}`.trim(), "data-donut": isDonut ? "true" : void 0, "data-hovering": active != null ? "true" : void 0, ...rest, children: [
2331
+ const clickDatum = (d, i, v, pct) => {
2332
+ if (!onDataClick) return;
2333
+ onDataClick({ label: d.label, value: v, index: i, percent: pct });
2334
+ };
2335
+ const selectMark = (i) => setSelected((s) => s === i ? null : i);
2336
+ const emph = focus != null ? focus : active;
2337
+ const multi = rows.length > 1;
2338
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--pie ${className}`.trim(), "data-donut": isDonut ? "true" : void 0, "data-has-selection": selected != null || void 0, "data-hovering": emph != null ? "true" : void 0, "data-clickable": clickable || void 0, ...rest, children: [
2073
2339
  baseStyles,
2074
2340
  styles,
2075
2341
  /* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${H} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, children: total <= 0 ? /* @__PURE__ */ jsx("text", { className: "twc-chart__empty", x: cx, y: cy, textAnchor: "middle", dominantBaseline: "middle", children: "No data" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2076
2342
  slices.map(({ d, i, v, s0, s1, span, pct, color }) => {
2077
2343
  const pad = span > padAngle ? padAngle / 2 : 0;
2078
2344
  const items = [{ color, label: fmt(v), value: `${pct}%` }];
2345
+ let explode;
2346
+ if (selected === i) {
2347
+ const mid = (s0 + s1) / 2;
2348
+ const a = (mid - 90) * Math.PI / 180;
2349
+ const dx = Math.cos(a) * 8, dy = Math.sin(a) * 8;
2350
+ if (Number.isFinite(dx) && Number.isFinite(dy)) {
2351
+ explode = `translate(${dx}px, ${dy}px)`;
2352
+ }
2353
+ }
2079
2354
  return /* @__PURE__ */ jsx(
2080
2355
  "path",
2081
2356
  {
2082
2357
  className: "twc-chart__slice twc-chart__anim-arc",
2083
- style: { fill: color },
2358
+ style: { fill: color, transform: explode },
2084
2359
  "data-mark": "",
2085
- "data-active": active === i ? "true" : void 0,
2360
+ "data-active": emph === i ? "true" : void 0,
2361
+ "data-selected": selected === i ? "true" : void 0,
2086
2362
  d: arcPath(cx, cy, rOuter, rInner, s0 + pad, s1 - pad),
2087
2363
  onMouseMove: (e) => {
2088
2364
  setActive(i);
@@ -2091,6 +2367,10 @@ function PieChart({
2091
2367
  onMouseLeave: () => {
2092
2368
  setActive(null);
2093
2369
  hide();
2370
+ },
2371
+ onClick: () => {
2372
+ clickDatum(d, i, v, pct);
2373
+ selectMark(i);
2094
2374
  }
2095
2375
  },
2096
2376
  i
@@ -2126,7 +2406,14 @@ function PieChart({
2126
2406
  rows: slices.map(({ d, v, pct }) => ({ label: d.label, values: [fmt(v), `${pct}%`] }))
2127
2407
  }
2128
2408
  ) : null,
2129
- showLegend && rows.length ? /* @__PURE__ */ jsx(ChartLegend, { items: slices.map(({ d, color }) => ({ label: d.label, color })) }) : null
2409
+ showLegend && rows.length ? /* @__PURE__ */ jsx(
2410
+ ChartLegend,
2411
+ {
2412
+ items: slices.map(({ d, color }) => ({ label: d.label, color })),
2413
+ onFocus: multi ? (it, i) => setFocus(i) : void 0,
2414
+ onBlur: multi ? () => setFocus(null) : void 0
2415
+ }
2416
+ ) : null
2130
2417
  ] });
2131
2418
  }
2132
2419
  function DonutChart(props) {
@@ -2138,6 +2425,7 @@ var GAUGE_CSS = `
2138
2425
  .twc-gauge__arc { fill: none; stroke-linecap: round; transition: stroke-dashoffset var(--duration-slow) var(--ease-standard); }
2139
2426
  .twc-gauge__value { fill: var(--color-text); font-weight: 700; }
2140
2427
  .twc-gauge__label { fill: var(--color-text-subtle); }
2428
+ .twc-gauge__arc[data-selected="true"] { filter: drop-shadow(0 0 2px var(--color-text)); }
2141
2429
  `;
2142
2430
  function Gauge({
2143
2431
  value = 0,
@@ -2154,6 +2442,7 @@ function Gauge({
2154
2442
  size = 200,
2155
2443
  series,
2156
2444
  showLegend = true,
2445
+ onDataClick,
2157
2446
  ariaLabel,
2158
2447
  "aria-label": ariaLabelProp,
2159
2448
  tableFallback = true,
@@ -2166,6 +2455,8 @@ function Gauge({
2166
2455
  const uid = React51__default.useId();
2167
2456
  const tableId = tableFallback ? `${uid}-table` : void 0;
2168
2457
  const { containerRef, tip, show, hide } = useChartTooltip();
2458
+ const [selected, setSelected] = React51__default.useState(null);
2459
+ const [focus, setFocus] = React51__default.useState(null);
2169
2460
  const fmt = valueFormat || ((n) => String(Math.round(Number(n) || 0)));
2170
2461
  const cx = size / 2, cy = size / 2;
2171
2462
  const span = endAngle - startAngle;
@@ -2190,79 +2481,106 @@ function Gauge({
2190
2481
  return `M${r(x0)} ${r(y0)} A${r(radius)} ${r(radius)} 0 ${large} ${sweep} ${r(x1)} ${r(y1)}`;
2191
2482
  };
2192
2483
  const single = items[0];
2484
+ const clickable = !!onDataClick;
2485
+ const activeFor = (i) => focus == null ? void 0 : i === focus || void 0;
2486
+ const clickRing = (it, i) => {
2487
+ if (onDataClick) onDataClick({ value: Number(it.value) || 0, label: it.label, index: i });
2488
+ };
2489
+ const selectRing = (i) => setSelected((s) => s === i ? null : i);
2193
2490
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? (isMulti ? `Gauge with ${items.length} values` : `${labelText(label) || "Gauge"}: ${fmt(single.value)} of ${fmt(max)}`);
2194
2491
  const valueFont = Math.round(size * (isMulti ? 0.11 : 0.17));
2195
2492
  const labelFont = Math.round(size * 0.072);
2196
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-gauge twc-chart twc-chart--gauge ${className}`.trim(), "data-multi": isMulti || void 0, ...rest, children: [
2197
- baseStyles,
2198
- styles,
2199
- /* @__PURE__ */ jsxs(
2200
- "svg",
2201
- {
2202
- viewBox: `0 0 ${size} ${size}`,
2203
- width: size,
2204
- height: size,
2205
- role: "img",
2206
- "aria-label": svgAriaLabel,
2207
- "aria-describedby": tableId,
2208
- children: [
2209
- items.map((it, i) => {
2210
- const radius = ringRadius(i);
2211
- const f = frac(it.value);
2212
- const c = it.color || paletteAt(void 0, i);
2213
- const valEnd = startAngle + f * span;
2214
- return /* @__PURE__ */ jsxs("g", { children: [
2215
- /* @__PURE__ */ jsx(
2216
- "path",
2217
- {
2218
- className: "twc-gauge__track",
2219
- d: arc(radius, startAngle, endAngle),
2220
- style: { stroke: trackColor, strokeWidth: strokeW }
2221
- }
2222
- ),
2223
- f > 0 ? /* @__PURE__ */ jsx(
2224
- "path",
2225
- {
2226
- className: "twc-gauge__arc twc-chart__anim-fade",
2227
- d: arc(radius, startAngle, valEnd),
2228
- style: { stroke: c, strokeWidth: strokeW },
2229
- onMouseMove: (e) => show({
2230
- title: labelText(it.label) || "Value",
2231
- items: [{ color: c, label: "", value: fmt(it.value) }]
2232
- }, e),
2233
- onMouseLeave: hide
2234
- }
2235
- ) : null
2236
- ] }, i);
2237
- }),
2238
- !isMulti && showValue ? /* @__PURE__ */ jsx("text", { className: "twc-gauge__value", x: cx, y: cy, textAnchor: "middle", dominantBaseline: "middle", style: { fontSize: valueFont }, children: fmt(single.value) }) : null,
2239
- !isMulti && label != null ? /* @__PURE__ */ jsx("text", { className: "twc-gauge__label", x: cx, y: cy + valueFont * 0.78, textAnchor: "middle", style: { fontSize: labelFont }, children: labelText(label) }) : null
2240
- ]
2241
- }
2242
- ),
2243
- /* @__PURE__ */ jsx(ChartTooltip, { tip }),
2244
- tableFallback ? /* @__PURE__ */ jsx(
2245
- ChartTable,
2246
- {
2247
- id: tableId,
2248
- caption: caption ?? svgAriaLabel,
2249
- columns: ["Value"],
2250
- rows: items.map((it, i) => ({
2251
- label: labelText(it.label) || (isMulti ? `Series ${i + 1}` : "Value"),
2252
- values: [fmt(it.value)]
2253
- }))
2254
- }
2255
- ) : null,
2256
- isMulti && showLegend ? /* @__PURE__ */ jsx(
2257
- ChartLegend,
2258
- {
2259
- items: items.map((it, i) => ({
2260
- label: labelText(it.label) || `Series ${i + 1}`,
2261
- color: it.color || paletteAt(void 0, i)
2262
- }))
2263
- }
2264
- ) : null
2265
- ] });
2493
+ return /* @__PURE__ */ jsxs(
2494
+ "div",
2495
+ {
2496
+ ref: containerRef,
2497
+ className: `twc-gauge twc-chart twc-chart--gauge ${className}`.trim(),
2498
+ "data-multi": isMulti || void 0,
2499
+ "data-hovering": focus != null || void 0,
2500
+ "data-clickable": clickable || void 0,
2501
+ "data-has-selection": selected != null || void 0,
2502
+ ...rest,
2503
+ children: [
2504
+ baseStyles,
2505
+ styles,
2506
+ /* @__PURE__ */ jsxs(
2507
+ "svg",
2508
+ {
2509
+ viewBox: `0 0 ${size} ${size}`,
2510
+ width: size,
2511
+ height: size,
2512
+ role: "img",
2513
+ "aria-label": svgAriaLabel,
2514
+ "aria-describedby": tableId,
2515
+ children: [
2516
+ items.map((it, i) => {
2517
+ const radius = ringRadius(i);
2518
+ const f = frac(it.value);
2519
+ const c = it.color || paletteAt(void 0, i);
2520
+ const valEnd = startAngle + f * span;
2521
+ return /* @__PURE__ */ jsxs("g", { children: [
2522
+ /* @__PURE__ */ jsx(
2523
+ "path",
2524
+ {
2525
+ className: "twc-gauge__track",
2526
+ d: arc(radius, startAngle, endAngle),
2527
+ style: { stroke: trackColor, strokeWidth: strokeW }
2528
+ }
2529
+ ),
2530
+ f > 0 ? /* @__PURE__ */ jsx(
2531
+ "path",
2532
+ {
2533
+ className: "twc-gauge__arc twc-chart__anim-fade",
2534
+ "data-mark": true,
2535
+ "data-active": activeFor(i),
2536
+ "data-selected": selected === i || void 0,
2537
+ d: arc(radius, startAngle, valEnd),
2538
+ style: { stroke: c, strokeWidth: strokeW },
2539
+ onMouseMove: (e) => show({
2540
+ title: labelText(it.label) || "Value",
2541
+ items: [{ color: c, label: "", value: fmt(it.value) }]
2542
+ }, e),
2543
+ onMouseLeave: hide,
2544
+ onClick: () => {
2545
+ clickRing(it, i);
2546
+ selectRing(i);
2547
+ }
2548
+ }
2549
+ ) : null
2550
+ ] }, i);
2551
+ }),
2552
+ !isMulti && showValue ? /* @__PURE__ */ jsx("text", { className: "twc-gauge__value", x: cx, y: cy, textAnchor: "middle", dominantBaseline: "middle", style: { fontSize: valueFont }, children: fmt(single.value) }) : null,
2553
+ !isMulti && label != null ? /* @__PURE__ */ jsx("text", { className: "twc-gauge__label", x: cx, y: cy + valueFont * 0.78, textAnchor: "middle", style: { fontSize: labelFont }, children: labelText(label) }) : null
2554
+ ]
2555
+ }
2556
+ ),
2557
+ /* @__PURE__ */ jsx(ChartTooltip, { tip }),
2558
+ tableFallback ? /* @__PURE__ */ jsx(
2559
+ ChartTable,
2560
+ {
2561
+ id: tableId,
2562
+ caption: caption ?? svgAriaLabel,
2563
+ columns: ["Value"],
2564
+ rows: items.map((it, i) => ({
2565
+ label: labelText(it.label) || (isMulti ? `Series ${i + 1}` : "Value"),
2566
+ values: [fmt(it.value)]
2567
+ }))
2568
+ }
2569
+ ) : null,
2570
+ isMulti && showLegend ? /* @__PURE__ */ jsx(
2571
+ ChartLegend,
2572
+ {
2573
+ items: items.map((it, i) => ({
2574
+ label: labelText(it.label) || `Series ${i + 1}`,
2575
+ color: it.color || paletteAt(void 0, i)
2576
+ })),
2577
+ onFocus: items.length > 1 ? (it, i) => setFocus(i) : void 0,
2578
+ onBlur: items.length > 1 ? () => setFocus(null) : void 0
2579
+ }
2580
+ ) : null
2581
+ ]
2582
+ }
2583
+ );
2266
2584
  }
2267
2585
  function labelText(label) {
2268
2586
  return typeof label === "string" || typeof label === "number" ? String(label) : "";
@@ -2289,6 +2607,7 @@ function Sparkline({
2289
2607
  min,
2290
2608
  max,
2291
2609
  valueFormat,
2610
+ onDataClick,
2292
2611
  ariaLabel,
2293
2612
  "aria-label": ariaLabelProp,
2294
2613
  tableFallback = false,
@@ -2302,6 +2621,8 @@ function Sparkline({
2302
2621
  const tableId = tableFallback ? `${uid}-table` : void 0;
2303
2622
  const { containerRef, tip, show, hide } = useChartTooltip();
2304
2623
  const [hover, setHover] = React51__default.useState(null);
2624
+ const [selected, setSelected] = React51__default.useState(null);
2625
+ const clickable = !!onDataClick;
2305
2626
  const kind = area ? "area" : type;
2306
2627
  const isBar = kind === "bar";
2307
2628
  const isArea = kind === "area";
@@ -2343,6 +2664,10 @@ function Sparkline({
2343
2664
  setHover(null);
2344
2665
  hide();
2345
2666
  };
2667
+ const clickPoint = (i) => {
2668
+ if (onDataClick) onDataClick({ value: values[i], index: i, label: labels[i] });
2669
+ setSelected((s) => s === i ? null : i);
2670
+ };
2346
2671
  const last = n ? values[n - 1] : 0;
2347
2672
  const summary = !n ? "sparkline, no data" : tableFallback ? `sparkline, ${n} points, latest ${fmt(last)}` : `sparkline, ${n} points, first ${fmt(values[0])}, latest ${fmt(last)}, min ${fmt(Math.min(...values))}, max ${fmt(Math.max(...values))}`;
2348
2673
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? summary;
@@ -2355,110 +2680,138 @@ function Sparkline({
2355
2680
  "rect",
2356
2681
  {
2357
2682
  className: "twc-sparkline__bar twc-chart__anim-fade",
2683
+ "data-mark": true,
2684
+ "data-selected": selected === i || void 0,
2358
2685
  style: { fill: color },
2359
2686
  x: pad + band * i + (band - slot) / 2,
2360
2687
  y: baselineY - h,
2361
2688
  width: Math.max(1, slot),
2362
2689
  height: Math.max(1, h),
2363
2690
  onMouseMove: (e) => show(tipFor(i), e),
2364
- onMouseLeave: hide
2691
+ onMouseLeave: hide,
2692
+ onClick: () => clickPoint(i)
2365
2693
  },
2366
2694
  i
2367
2695
  );
2368
2696
  });
2369
2697
  };
2370
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-sparkline twc-chart--${kind} ${className}`.trim(), ...rest, children: [
2371
- baseStyles,
2372
- styles,
2373
- /* @__PURE__ */ jsx(
2374
- "svg",
2375
- {
2376
- viewBox: `0 0 ${W} ${H}`,
2377
- width: W,
2378
- height: H,
2379
- role: "img",
2380
- "aria-label": svgAriaLabel,
2381
- "aria-describedby": tableId,
2382
- children: n === 0 ? null : isBar ? renderBars() : /* @__PURE__ */ jsxs(Fragment, { children: [
2383
- isArea ? /* @__PURE__ */ jsx("path", { className: "twc-sparkline__area twc-chart__anim-fade", style: { fill: color }, d: areaPath(points, baselineY, pathOpts) }) : null,
2384
- /* @__PURE__ */ jsx(
2385
- "path",
2386
- {
2387
- className: "twc-sparkline__line twc-chart__anim-line",
2388
- style: { stroke: color, strokeWidth },
2389
- pathLength: "1",
2390
- d: linePath(points, pathOpts)
2391
- }
2392
- ),
2393
- showDots && n ? /* @__PURE__ */ jsx(
2394
- "circle",
2395
- {
2396
- className: "twc-sparkline__dot twc-chart__anim-fade",
2397
- style: { fill: color },
2398
- cx: points[n - 1][0],
2399
- cy: points[n - 1][1],
2400
- r: dotR
2401
- }
2402
- ) : null,
2403
- hover != null ? /* @__PURE__ */ jsx(
2404
- "circle",
2405
- {
2406
- className: "twc-sparkline__dot",
2407
- style: { fill: color },
2408
- cx: points[hover][0],
2409
- cy: points[hover][1],
2410
- r: Math.max(2, strokeWidth + 1)
2411
- }
2412
- ) : null,
2413
- points.map((p, i) => /* @__PURE__ */ jsx(
2414
- "circle",
2415
- {
2416
- className: "twc-chart__hit",
2417
- cx: p[0],
2418
- cy: p[1],
2419
- r: "14",
2420
- onMouseMove: onPointMove(i),
2421
- onMouseLeave: onPointLeave
2422
- },
2423
- i
2424
- ))
2425
- ] })
2426
- }
2427
- ),
2428
- /* @__PURE__ */ jsx(ChartTooltip, { tip }),
2429
- tableFallback ? /* @__PURE__ */ jsx(
2430
- ChartTable,
2431
- {
2432
- id: tableId,
2433
- caption: caption ?? svgAriaLabel,
2434
- columns: ["value"],
2435
- rows: values.map((v, i) => ({ label: labels[i], values: [fmt(v)] }))
2436
- }
2437
- ) : null
2438
- ] });
2439
- }
2440
- var SCATTER_CSS = `
2441
- .twc-chart--scatter__grid { stroke: var(--color-divider); stroke-width: 1; }
2442
- .twc-chart--scatter__axis { fill: var(--color-text-subtle); font-size: 11px; }
2443
- .twc-chart--scatter__title { fill: var(--color-text-muted); font-size: 11px; font-weight: 500; }
2444
- .twc-chart--scatter__dot { transform-box: fill-box; transform-origin: center; transition: transform var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); stroke: var(--color-surface); stroke-width: 1; }
2445
- .twc-chart--scatter__dot[data-bubble="true"] { fill-opacity: 0.55; }
2446
- .twc-chart__hit { fill: transparent; }
2447
- .twc-chart__hit:hover + .twc-chart--scatter__dot { transform: scale(1.18); }
2448
- `;
2449
- function ScatterChart({
2450
- series,
2451
- bubble = false,
2452
- maxBubble = 26,
2453
- dotRadius = 4.5,
2454
- height = 300,
2455
- xLabel,
2456
- yLabel,
2698
+ return /* @__PURE__ */ jsxs(
2699
+ "div",
2700
+ {
2701
+ ref: containerRef,
2702
+ className: `twc-chart twc-sparkline twc-chart--${kind} ${className}`.trim(),
2703
+ "data-clickable": clickable || void 0,
2704
+ "data-has-selection": selected != null || void 0,
2705
+ ...rest,
2706
+ children: [
2707
+ baseStyles,
2708
+ styles,
2709
+ /* @__PURE__ */ jsx(
2710
+ "svg",
2711
+ {
2712
+ viewBox: `0 0 ${W} ${H}`,
2713
+ width: W,
2714
+ height: H,
2715
+ role: "img",
2716
+ "aria-label": svgAriaLabel,
2717
+ "aria-describedby": tableId,
2718
+ children: n === 0 ? null : isBar ? renderBars() : /* @__PURE__ */ jsxs(Fragment, { children: [
2719
+ isArea ? /* @__PURE__ */ jsx("path", { className: "twc-sparkline__area twc-chart__anim-fade", style: { fill: color }, d: areaPath(points, baselineY, pathOpts) }) : null,
2720
+ /* @__PURE__ */ jsx(
2721
+ "path",
2722
+ {
2723
+ className: "twc-sparkline__line twc-chart__anim-line",
2724
+ style: { stroke: color, strokeWidth },
2725
+ pathLength: "1",
2726
+ d: linePath(points, pathOpts)
2727
+ }
2728
+ ),
2729
+ showDots && n ? /* @__PURE__ */ jsx(
2730
+ "circle",
2731
+ {
2732
+ className: "twc-sparkline__dot twc-chart__anim-fade",
2733
+ style: { fill: color },
2734
+ cx: points[n - 1][0],
2735
+ cy: points[n - 1][1],
2736
+ r: dotR
2737
+ }
2738
+ ) : null,
2739
+ hover != null ? /* @__PURE__ */ jsx(
2740
+ "circle",
2741
+ {
2742
+ className: "twc-sparkline__dot",
2743
+ style: { fill: color },
2744
+ cx: points[hover][0],
2745
+ cy: points[hover][1],
2746
+ r: Math.max(2, strokeWidth + 1)
2747
+ }
2748
+ ) : null,
2749
+ selected != null && points[selected] ? /* @__PURE__ */ jsx(
2750
+ "circle",
2751
+ {
2752
+ className: "twc-sparkline__dot",
2753
+ "data-mark": true,
2754
+ "data-selected": "true",
2755
+ style: { fill: color, pointerEvents: "none" },
2756
+ cx: points[selected][0],
2757
+ cy: points[selected][1],
2758
+ r: Math.max(2, strokeWidth + 1)
2759
+ }
2760
+ ) : null,
2761
+ points.map((p, i) => /* @__PURE__ */ jsx(
2762
+ "circle",
2763
+ {
2764
+ className: "twc-chart__hit",
2765
+ cx: p[0],
2766
+ cy: p[1],
2767
+ r: "14",
2768
+ onMouseMove: onPointMove(i),
2769
+ onMouseLeave: onPointLeave,
2770
+ onClick: () => clickPoint(i)
2771
+ },
2772
+ i
2773
+ ))
2774
+ ] })
2775
+ }
2776
+ ),
2777
+ /* @__PURE__ */ jsx(ChartTooltip, { tip }),
2778
+ tableFallback ? /* @__PURE__ */ jsx(
2779
+ ChartTable,
2780
+ {
2781
+ id: tableId,
2782
+ caption: caption ?? svgAriaLabel,
2783
+ columns: ["value"],
2784
+ rows: values.map((v, i) => ({ label: labels[i], values: [fmt(v)] }))
2785
+ }
2786
+ ) : null
2787
+ ]
2788
+ }
2789
+ );
2790
+ }
2791
+ var SCATTER_CSS = `
2792
+ .twc-chart--scatter__grid { stroke: var(--color-divider); stroke-width: 1; }
2793
+ .twc-chart--scatter__axis { fill: var(--color-text-subtle); font-size: 11px; }
2794
+ .twc-chart--scatter__title { fill: var(--color-text-muted); font-size: 11px; font-weight: 500; }
2795
+ .twc-chart--scatter__dot { transform-box: fill-box; transform-origin: center; transition: transform var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); stroke: var(--color-surface); stroke-width: 1; }
2796
+ .twc-chart--scatter__dot[data-bubble="true"] { fill-opacity: 0.55; }
2797
+ .twc-chart__hit { fill: transparent; }
2798
+ .twc-chart__hit:hover + .twc-chart--scatter__dot { transform: scale(1.18); }
2799
+ `;
2800
+ function ScatterChart({
2801
+ series,
2802
+ bubble = false,
2803
+ maxBubble = 26,
2804
+ dotRadius = 4.5,
2805
+ height = 300,
2806
+ xLabel,
2807
+ yLabel,
2457
2808
  showGrid = true,
2458
2809
  showLegend,
2810
+ zoomable = false,
2459
2811
  xFormat,
2460
2812
  yFormat,
2461
2813
  valueFormat,
2814
+ onDataClick,
2462
2815
  ariaLabel,
2463
2816
  "aria-label": ariaLabelProp,
2464
2817
  tableFallback = true,
@@ -2469,15 +2822,24 @@ function ScatterChart({
2469
2822
  const baseStyles = useScopedStyles("twc-chart-base", CHART_BASE_CSS);
2470
2823
  const styles = useScopedStyles("twc-scatter-styles", SCATTER_CSS);
2471
2824
  const uid = React51__default.useId();
2825
+ const gid = uid.replace(/[^a-zA-Z0-9]/g, "");
2826
+ const clipId = `twc-sc-clip-${gid}`;
2472
2827
  const tableId = tableFallback ? `${uid}-table` : void 0;
2473
2828
  const { containerRef, tip, show, hide } = useChartTooltip();
2829
+ const [focus, setFocus] = React51__default.useState(null);
2830
+ const [selected, setSelected] = React51__default.useState(null);
2831
+ const [zoom, setZoom] = React51__default.useState(null);
2832
+ const [drag, setDrag] = React51__default.useState(null);
2833
+ const svgRef = React51__default.useRef(null);
2834
+ const movedRef = React51__default.useRef(false);
2474
2835
  const list = Array.isArray(series) ? series : [];
2475
2836
  const vFmt = valueFormat || fmtNumber;
2476
2837
  const xFmt = xFormat || vFmt;
2477
2838
  const yFmt = yFormat || vFmt;
2478
- const all = list.flatMap(
2479
- (s, si) => (s && Array.isArray(s.points) ? s.points : []).filter((p) => p && Number.isFinite(Number(p.x)) && Number.isFinite(Number(p.y))).map((p) => ({ si, x: Number(p.x), y: Number(p.y), z: p.z, label: p.label }))
2480
- );
2839
+ const all = list.flatMap((s, si) => {
2840
+ const pts = s && Array.isArray(s.points) ? s.points : [];
2841
+ return pts.map((p, pi) => ({ p, pi })).filter(({ p }) => p && Number.isFinite(Number(p.x)) && Number.isFinite(Number(p.y))).map(({ p, pi }) => ({ si, pi, key: `${si}:${pi}`, x: Number(p.x), y: Number(p.y), z: p.z, label: p.label, point: p }));
2842
+ });
2481
2843
  const hasZ = all.some((p) => Number.isFinite(Number(p.z)));
2482
2844
  const useBubble = bubble || hasZ;
2483
2845
  const zMax = useBubble ? Math.max(0, ...all.map((p) => Number(p.z) || 0)) : 0;
@@ -2488,8 +2850,12 @@ function ScatterChart({
2488
2850
  return Math.max(2, Math.sqrt(zv / zMax) * maxBubble);
2489
2851
  };
2490
2852
  const xs = all.map((p) => p.x), ys = all.map((p) => p.y);
2491
- const xScale = niceScale(Math.min(...xs), Math.max(...xs), 5);
2492
- const yScale = niceScale(Math.min(...ys), Math.max(...ys), 5);
2853
+ const base = { x0: Math.min(...xs), x1: Math.max(...xs), y0: Math.min(...ys), y1: Math.max(...ys) };
2854
+ const baseSpanX = base.x1 - base.x0, baseSpanY = base.y1 - base.y0;
2855
+ const canZoom = zoomable && all.length > 0 && baseSpanX > 0 && baseSpanY > 0;
2856
+ const dom = canZoom && zoom ? zoom : base;
2857
+ const xScale = niceScale(dom.x0, dom.x1, 5);
2858
+ const yScale = niceScale(dom.y0, dom.y1, 5);
2493
2859
  const xSpan = xScale.max - xScale.min || 1;
2494
2860
  const ySpan = yScale.max - yScale.min || 1;
2495
2861
  const W = 600, H = height;
@@ -2502,7 +2868,9 @@ function ScatterChart({
2502
2868
  const yPos = (y) => padT + innerH - (y - yScale.min) / ySpan * innerH;
2503
2869
  const seriesColor = (s, i) => s && s.color || paletteAt(void 0, i);
2504
2870
  const seriesName = (s, i) => s && s.name || `Series ${i + 1}`;
2505
- const legend = showLegend ?? list.length > 1;
2871
+ const multi = list.length > 1;
2872
+ const legend = showLegend ?? multi;
2873
+ const clickable = !!onDataClick;
2506
2874
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? `${useBubble ? "bubble" : "scatter"} chart`;
2507
2875
  const dots = all.map((p) => ({ ...p, radius: dotR(p.z), fill: seriesColor(list[p.si], p.si) })).sort((a, b) => b.radius - a.radius);
2508
2876
  const tipLabel = (p) => labelText2(p.label) || seriesName(list[p.si], p.si);
@@ -2515,72 +2883,272 @@ function ScatterChart({
2515
2883
  ...Number.isFinite(Number(p.z)) ? [{ label: "z", value: vFmt(Number(p.z)) }] : []
2516
2884
  ]
2517
2885
  });
2518
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--scatter ${className}`.trim(), "data-bubble": useBubble || void 0, ...rest, children: [
2519
- baseStyles,
2520
- styles,
2521
- /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, children: [
2522
- showGrid ? yScale.ticks.map((t, i) => {
2523
- const p = yPos(t);
2524
- return /* @__PURE__ */ jsx("line", { className: "twc-chart--scatter__grid", x1: padL, y1: p, x2: W - padR, y2: p }, `y${i}`);
2525
- }) : null,
2526
- showGrid ? xScale.ticks.map((t, i) => {
2527
- const p = xPos(t);
2528
- return /* @__PURE__ */ jsx("line", { className: "twc-chart--scatter__grid", x1: p, y1: padT, x2: p, y2: padT + innerH }, `x${i}`);
2529
- }) : null,
2530
- yScale.ticks.map((t, i) => /* @__PURE__ */ jsx("text", { className: "twc-chart--scatter__axis", x: padL - 8, y: yPos(t) + 4, textAnchor: "end", children: shortNum(t) }, `yt${i}`)),
2531
- xScale.ticks.map((t, i) => /* @__PURE__ */ jsx("text", { className: "twc-chart--scatter__axis", x: xPos(t), y: padT + innerH + 16, textAnchor: "middle", children: shortNum(t) }, `xt${i}`)),
2532
- xLabel != null ? /* @__PURE__ */ jsx("text", { className: "twc-chart--scatter__title", x: padL + innerW / 2, y: H - 6, textAnchor: "middle", children: xLabel }) : null,
2533
- yLabel != null ? /* @__PURE__ */ jsx(
2534
- "text",
2535
- {
2536
- className: "twc-chart--scatter__title",
2537
- x: 16,
2538
- y: padT + innerH / 2,
2539
- textAnchor: "middle",
2540
- transform: `rotate(-90 16 ${padT + innerH / 2})`,
2541
- children: yLabel
2886
+ const focusActive = (si) => focus == null ? void 0 : si === focus || void 0;
2887
+ const clickPoint = (p) => {
2888
+ if (!onDataClick) return;
2889
+ onDataClick({ x: p.x, y: p.y, z: p.point.z, series: seriesName(list[p.si], p.si), seriesIndex: p.si, index: p.pi, point: p.point });
2890
+ };
2891
+ const selectPoint = (key) => setSelected((s) => s === key ? null : key);
2892
+ const clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
2893
+ const svgPoint = (e) => {
2894
+ const svg = svgRef.current;
2895
+ if (!svg || typeof svg.getScreenCTM !== "function" || typeof svg.createSVGPoint !== "function") return null;
2896
+ const ctm = svg.getScreenCTM();
2897
+ if (!ctm || !ctm.a || !ctm.d) return null;
2898
+ const pt = svg.createSVGPoint();
2899
+ pt.x = e.clientX;
2900
+ pt.y = e.clientY;
2901
+ const loc = pt.matrixTransform(ctm.inverse());
2902
+ if (!Number.isFinite(loc.x) || !Number.isFinite(loc.y)) return null;
2903
+ return { px: loc.x, py: loc.y };
2904
+ };
2905
+ const dataFromPx = (px, py) => {
2906
+ const cx = clamp2(px, padL, padL + innerW);
2907
+ const cy = clamp2(py, padT, padT + innerH);
2908
+ return {
2909
+ x: xScale.min + (cx - padL) / innerW * (xScale.max - xScale.min),
2910
+ y: yScale.min + (padT + innerH - cy) / innerH * (yScale.max - yScale.min)
2911
+ };
2912
+ };
2913
+ const nearestDot = (px, py) => {
2914
+ let best = null, bestD = Infinity;
2915
+ for (const p of dots) {
2916
+ const dx = xPos(p.x) - px, dy = yPos(p.y) - py;
2917
+ const d = dx * dx + dy * dy;
2918
+ const rr = Math.max(14, p.radius);
2919
+ if (d <= rr * rr && d < bestD) {
2920
+ bestD = d;
2921
+ best = p;
2922
+ }
2923
+ }
2924
+ return best;
2925
+ };
2926
+ const applyZoom = (win) => {
2927
+ const x0 = Math.max(Math.min(win.x0, win.x1), base.x0), x1 = Math.min(Math.max(win.x0, win.x1), base.x1);
2928
+ const y0 = Math.max(Math.min(win.y0, win.y1), base.y0), y1 = Math.min(Math.max(win.y0, win.y1), base.y1);
2929
+ if (!(x1 - x0 > 0) || !(y1 - y0 > 0)) return;
2930
+ if (x1 - x0 >= baseSpanX && y1 - y0 >= baseSpanY) {
2931
+ setZoom(null);
2932
+ return;
2933
+ }
2934
+ setZoom({ x0, x1, y0, y1 });
2935
+ };
2936
+ const panTo = (win) => {
2937
+ const wSpan = win.x1 - win.x0, hSpan = win.y1 - win.y0;
2938
+ let { x0, x1, y0, y1 } = win;
2939
+ if (x0 < base.x0) {
2940
+ x0 = base.x0;
2941
+ x1 = base.x0 + wSpan;
2942
+ } else if (x1 > base.x1) {
2943
+ x1 = base.x1;
2944
+ x0 = base.x1 - wSpan;
2945
+ }
2946
+ if (y0 < base.y0) {
2947
+ y0 = base.y0;
2948
+ y1 = base.y0 + hSpan;
2949
+ } else if (y1 > base.y1) {
2950
+ y1 = base.y1;
2951
+ y0 = base.y1 - hSpan;
2952
+ }
2953
+ setZoom({ x0, x1, y0, y1 });
2954
+ };
2955
+ const onOverlayDown = (e) => {
2956
+ const pt = svgPoint(e);
2957
+ if (!pt) return;
2958
+ movedRef.current = false;
2959
+ setDrag({ x0: pt.px, y0: pt.py, x1: pt.px, y1: pt.py, pan: e.shiftKey, z0: zoom || base });
2960
+ };
2961
+ const onOverlayMove = (e) => {
2962
+ const pt = svgPoint(e);
2963
+ if (!pt) return;
2964
+ if (drag) {
2965
+ if (Math.abs(pt.px - drag.x0) > 4 || Math.abs(pt.py - drag.y0) > 4) movedRef.current = true;
2966
+ if (drag.pan) {
2967
+ if (zoom) {
2968
+ const z0 = drag.z0;
2969
+ const dxData = -((pt.px - drag.x0) / innerW) * (z0.x1 - z0.x0);
2970
+ const dyData = (pt.py - drag.y0) / innerH * (z0.y1 - z0.y0);
2971
+ panTo({ x0: z0.x0 + dxData, x1: z0.x1 + dxData, y0: z0.y0 + dyData, y1: z0.y1 + dyData });
2542
2972
  }
2543
- ) : null,
2544
- dots.map((p, i) => /* @__PURE__ */ jsxs("g", { className: "twc-chart__anim-fade", children: [
2545
- /* @__PURE__ */ jsx(
2546
- "circle",
2973
+ } else {
2974
+ setDrag({ ...drag, x1: pt.px, y1: pt.py });
2975
+ }
2976
+ hide();
2977
+ return;
2978
+ }
2979
+ const hit = nearestDot(pt.px, pt.py);
2980
+ if (hit) show(tipFor(hit), e);
2981
+ else hide();
2982
+ };
2983
+ const onOverlayUp = () => {
2984
+ if (drag && !drag.pan) {
2985
+ const a = dataFromPx(drag.x0, drag.y0), b = dataFromPx(drag.x1, drag.y1);
2986
+ if (Math.abs(drag.x1 - drag.x0) > 4 && Math.abs(drag.y1 - drag.y0) > 4) applyZoom({ x0: a.x, x1: b.x, y0: a.y, y1: b.y });
2987
+ }
2988
+ setDrag(null);
2989
+ };
2990
+ const onOverlayLeave = () => {
2991
+ hide();
2992
+ setDrag(null);
2993
+ };
2994
+ const onOverlayClick = (e) => {
2995
+ if (movedRef.current) {
2996
+ movedRef.current = false;
2997
+ return;
2998
+ }
2999
+ const pt = svgPoint(e);
3000
+ if (!pt) return;
3001
+ const hit = nearestDot(pt.px, pt.py);
3002
+ if (hit) {
3003
+ clickPoint(hit);
3004
+ selectPoint(hit.key);
3005
+ }
3006
+ };
3007
+ React51__default.useEffect(() => {
3008
+ if (!canZoom) return void 0;
3009
+ const svg = svgRef.current;
3010
+ if (!svg) return void 0;
3011
+ const onWheel = (e) => {
3012
+ e.preventDefault();
3013
+ const pt = svgPoint(e);
3014
+ const fx = pt ? clamp2((pt.px - padL) / innerW, 0, 1) : 0.5;
3015
+ const fy = pt ? clamp2((padT + innerH - pt.py) / innerH, 0, 1) : 0.5;
3016
+ const factor = e.deltaY < 0 ? 0.8 : 1.25;
3017
+ const x0 = xScale.min, x1 = xScale.max, y0 = yScale.min, y1 = yScale.max;
3018
+ const cxv = x0 + fx * (x1 - x0), cyv = y0 + fy * (y1 - y0);
3019
+ const nw = (x1 - x0) * factor, nh = (y1 - y0) * factor;
3020
+ applyZoom({ x0: cxv - fx * nw, x1: cxv + (1 - fx) * nw, y0: cyv - fy * nh, y1: cyv + (1 - fy) * nh });
3021
+ };
3022
+ svg.addEventListener("wheel", onWheel, { passive: false });
3023
+ return () => svg.removeEventListener("wheel", onWheel);
3024
+ }, [canZoom, zoom, xScale.min, xScale.max, yScale.min, yScale.max]);
3025
+ const dragBand = canZoom && drag && !drag.pan && Math.abs(drag.x1 - drag.x0) > 2 && Math.abs(drag.y1 - drag.y0) > 2;
3026
+ return /* @__PURE__ */ jsxs(
3027
+ "div",
3028
+ {
3029
+ ref: containerRef,
3030
+ className: `twc-chart twc-chart--scatter ${className}`.trim(),
3031
+ "data-bubble": useBubble || void 0,
3032
+ "data-hovering": focus != null || void 0,
3033
+ "data-has-selection": selected != null || void 0,
3034
+ "data-clickable": clickable || void 0,
3035
+ ...rest,
3036
+ children: [
3037
+ baseStyles,
3038
+ styles,
3039
+ /* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, children: [
3040
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: padL, y: padT, width: Math.max(0, innerW), height: Math.max(0, innerH) }) }) }),
3041
+ showGrid ? yScale.ticks.map((t, i) => {
3042
+ const p = yPos(t);
3043
+ return /* @__PURE__ */ jsx("line", { className: "twc-chart--scatter__grid", x1: padL, y1: p, x2: W - padR, y2: p }, `y${i}`);
3044
+ }) : null,
3045
+ showGrid ? xScale.ticks.map((t, i) => {
3046
+ const p = xPos(t);
3047
+ return /* @__PURE__ */ jsx("line", { className: "twc-chart--scatter__grid", x1: p, y1: padT, x2: p, y2: padT + innerH }, `x${i}`);
3048
+ }) : null,
3049
+ yScale.ticks.map((t, i) => /* @__PURE__ */ jsx("text", { className: "twc-chart--scatter__axis", x: padL - 8, y: yPos(t) + 4, textAnchor: "end", children: shortNum(t) }, `yt${i}`)),
3050
+ xScale.ticks.map((t, i) => /* @__PURE__ */ jsx("text", { className: "twc-chart--scatter__axis", x: xPos(t), y: padT + innerH + 16, textAnchor: "middle", children: shortNum(t) }, `xt${i}`)),
3051
+ xLabel != null ? /* @__PURE__ */ jsx("text", { className: "twc-chart--scatter__title", x: padL + innerW / 2, y: H - 6, textAnchor: "middle", children: xLabel }) : null,
3052
+ yLabel != null ? /* @__PURE__ */ jsx(
3053
+ "text",
3054
+ {
3055
+ className: "twc-chart--scatter__title",
3056
+ x: 16,
3057
+ y: padT + innerH / 2,
3058
+ textAnchor: "middle",
3059
+ transform: `rotate(-90 16 ${padT + innerH / 2})`,
3060
+ children: yLabel
3061
+ }
3062
+ ) : null,
3063
+ /* @__PURE__ */ jsx("g", { clipPath: canZoom && zoom ? `url(#${clipId})` : void 0, children: dots.map((p) => /* @__PURE__ */ jsxs("g", { className: "twc-chart__anim-fade", children: [
3064
+ /* @__PURE__ */ jsx(
3065
+ "circle",
3066
+ {
3067
+ className: "twc-chart__hit",
3068
+ cx: xPos(p.x),
3069
+ cy: yPos(p.y),
3070
+ r: Math.max(14, p.radius),
3071
+ onMouseMove: (e) => show(tipFor(p), e),
3072
+ onMouseLeave: hide,
3073
+ onClick: () => {
3074
+ clickPoint(p);
3075
+ selectPoint(p.key);
3076
+ }
3077
+ }
3078
+ ),
3079
+ /* @__PURE__ */ jsx(
3080
+ "circle",
3081
+ {
3082
+ className: "twc-chart--scatter__dot",
3083
+ "data-mark": true,
3084
+ "data-active": focusActive(p.si),
3085
+ "data-selected": selected === p.key || void 0,
3086
+ "data-bubble": useBubble || void 0,
3087
+ style: { fill: p.fill },
3088
+ cx: xPos(p.x),
3089
+ cy: yPos(p.y),
3090
+ r: p.radius
3091
+ }
3092
+ )
3093
+ ] }, p.key)) }),
3094
+ dragBand ? /* @__PURE__ */ jsx(
3095
+ "rect",
3096
+ {
3097
+ className: "twc-chart__zoomband",
3098
+ x: Math.min(drag.x0, drag.x1),
3099
+ y: Math.min(drag.y0, drag.y1),
3100
+ width: Math.abs(drag.x1 - drag.x0),
3101
+ height: Math.abs(drag.y1 - drag.y0)
3102
+ }
3103
+ ) : null,
3104
+ canZoom ? /* @__PURE__ */ jsx(
3105
+ "rect",
3106
+ {
3107
+ className: "twc-chart__overlay",
3108
+ "data-clickable": clickable || void 0,
3109
+ x: padL,
3110
+ y: padT,
3111
+ width: Math.max(0, innerW),
3112
+ height: Math.max(0, innerH),
3113
+ onMouseDown: onOverlayDown,
3114
+ onMouseMove: onOverlayMove,
3115
+ onMouseUp: onOverlayUp,
3116
+ onMouseLeave: onOverlayLeave,
3117
+ onClick: onOverlayClick
3118
+ }
3119
+ ) : null
3120
+ ] }),
3121
+ canZoom && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
3122
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3123
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
3124
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
3125
+ ] }),
3126
+ "Reset"
3127
+ ] }) : null,
3128
+ /* @__PURE__ */ jsx(ChartTooltip, { tip }),
3129
+ tableFallback ? /* @__PURE__ */ jsx(
3130
+ ChartTable,
2547
3131
  {
2548
- className: "twc-chart__hit",
2549
- cx: xPos(p.x),
2550
- cy: yPos(p.y),
2551
- r: Math.max(14, p.radius),
2552
- onMouseMove: (e) => show(tipFor(p), e),
2553
- onMouseLeave: hide
3132
+ id: tableId,
3133
+ caption: caption ?? svgAriaLabel,
3134
+ columns: tableColumns,
3135
+ rows: all.map((p) => ({
3136
+ label: seriesName(list[p.si], p.si),
3137
+ values: hasZ ? [xFmt(p.x), yFmt(p.y), Number.isFinite(Number(p.z)) ? vFmt(Number(p.z)) : ""] : [xFmt(p.x), yFmt(p.y)]
3138
+ }))
2554
3139
  }
2555
- ),
2556
- /* @__PURE__ */ jsx(
2557
- "circle",
3140
+ ) : null,
3141
+ legend && list.length ? /* @__PURE__ */ jsx(
3142
+ ChartLegend,
2558
3143
  {
2559
- className: "twc-chart--scatter__dot",
2560
- "data-bubble": useBubble || void 0,
2561
- style: { fill: p.fill },
2562
- cx: xPos(p.x),
2563
- cy: yPos(p.y),
2564
- r: p.radius
3144
+ items: list.map((s, i) => ({ label: seriesName(s, i), color: seriesColor(s, i) })),
3145
+ onFocus: multi ? (it, i) => setFocus(i) : void 0,
3146
+ onBlur: multi ? () => setFocus(null) : void 0
2565
3147
  }
2566
- )
2567
- ] }, i))
2568
- ] }),
2569
- /* @__PURE__ */ jsx(ChartTooltip, { tip }),
2570
- tableFallback ? /* @__PURE__ */ jsx(
2571
- ChartTable,
2572
- {
2573
- id: tableId,
2574
- caption: caption ?? svgAriaLabel,
2575
- columns: tableColumns,
2576
- rows: all.map((p) => ({
2577
- label: seriesName(list[p.si], p.si),
2578
- values: hasZ ? [xFmt(p.x), yFmt(p.y), Number.isFinite(Number(p.z)) ? vFmt(Number(p.z)) : ""] : [xFmt(p.x), yFmt(p.y)]
2579
- }))
2580
- }
2581
- ) : null,
2582
- legend && list.length ? /* @__PURE__ */ jsx(ChartLegend, { items: list.map((s, i) => ({ label: seriesName(s, i), color: seriesColor(s, i) })) }) : null
2583
- ] });
3148
+ ) : null
3149
+ ]
3150
+ }
3151
+ );
2584
3152
  }
2585
3153
  function labelText2(label) {
2586
3154
  return typeof label === "string" || typeof label === "number" ? String(label) : "";
@@ -2612,6 +3180,7 @@ function RadarChart({
2612
3180
  colors,
2613
3181
  valueFormat,
2614
3182
  height = 300,
3183
+ onDataClick,
2615
3184
  ariaLabel,
2616
3185
  "aria-label": ariaLabelProp,
2617
3186
  tableFallback = true,
@@ -2624,11 +3193,16 @@ function RadarChart({
2624
3193
  const uid = React51__default.useId();
2625
3194
  const tableId = tableFallback ? `${uid}-table` : void 0;
2626
3195
  const { containerRef, tip, show, hide } = useChartTooltip();
3196
+ const [focus, setFocus] = React51__default.useState(null);
3197
+ const [selected, setSelected] = React51__default.useState(null);
2627
3198
  const rows = data || [];
2628
3199
  const keys = series && series.length ? series : ["value"];
2629
3200
  const fmt = valueFormat || fmtNumber;
2630
3201
  const multi = keys.length > 1;
3202
+ const clickable = !!onDataClick;
2631
3203
  const legend = showLegend != null ? showLegend : multi;
3204
+ const focusActive = (k) => focus == null ? void 0 : k === focus || void 0;
3205
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
2632
3206
  const H = height;
2633
3207
  const cx = H / 2, cy = H / 2;
2634
3208
  const R = Math.max(0, H / 2 - 40);
@@ -2642,69 +3216,106 @@ function RadarChart({
2642
3216
  const frac = Math.max(0, Math.min(1, (Number(v) || 0) / radialMax));
2643
3217
  return polarDeg(cx, cy, frac * R, angleOf(i));
2644
3218
  };
2645
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--radar ${className}`.trim(), ...rest, children: [
2646
- baseStyles,
2647
- styles,
2648
- /* @__PURE__ */ jsx(
2649
- "svg",
2650
- {
2651
- viewBox: `0 0 ${H} ${H}`,
2652
- role: "img",
2653
- "aria-label": svgAriaLabel,
2654
- "aria-describedby": tableId,
2655
- style: { maxWidth: H, margin: "0 auto" },
2656
- children: n === 0 ? /* @__PURE__ */ jsx("text", { className: "twc-chart__empty", x: cx, y: cy, textAnchor: "middle", dominantBaseline: "middle", children: "No data" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2657
- Array.from({ length: rings }, (_, li) => {
2658
- const rr = R * (li + 1) / rings;
2659
- const pts = rows.map((_2, i) => polarDeg(cx, cy, rr, angleOf(i)));
2660
- return /* @__PURE__ */ jsx("path", { className: "twc-chart__ring", d: polygonPath(pts) }, li);
2661
- }),
2662
- rows.map((d, i) => {
2663
- const [ex, ey] = polarDeg(cx, cy, R, angleOf(i));
2664
- const [lx, ly] = polarDeg(cx, cy, R + 16, angleOf(i));
2665
- const anchor = lx > cx + 1 ? "start" : lx < cx - 1 ? "end" : "middle";
2666
- return /* @__PURE__ */ jsxs("g", { children: [
2667
- /* @__PURE__ */ jsx("line", { className: "twc-chart__spoke", x1: cx, y1: cy, x2: ex, y2: ey }),
2668
- /* @__PURE__ */ jsx("text", { className: "twc-chart__radar-axis", x: lx, y: ly, textAnchor: anchor, dominantBaseline: "middle", children: d.label })
2669
- ] }, i);
2670
- }),
2671
- keys.map((k, si) => {
2672
- const color = paletteAt(colors, si);
2673
- const pts = rows.map((d, i) => vertex(d[k], i));
2674
- return /* @__PURE__ */ jsxs("g", { children: [
2675
- fill ? /* @__PURE__ */ jsx("path", { className: "twc-chart__radar-area twc-chart__anim-fade", style: { fill: color, opacity: 0.18 }, d: polygonPath(pts) }) : null,
2676
- /* @__PURE__ */ jsx("path", { className: "twc-chart__radar-line twc-chart__anim-fade", style: { stroke: color }, d: polygonPath(pts) }),
2677
- showDots ? pts.map((p, i) => /* @__PURE__ */ jsxs("g", { className: "twc-chart__anim-fade", children: [
2678
- /* @__PURE__ */ jsx(
2679
- "circle",
2680
- {
2681
- className: "twc-chart__hit",
2682
- cx: p[0],
2683
- cy: p[1],
2684
- r: "14",
2685
- onMouseMove: (e) => show({ title: rows[i].label, items: [{ color, label: k, value: fmt(Number(rows[i][k]) || 0) }] }, e),
2686
- onMouseLeave: hide
2687
- }
2688
- ),
2689
- /* @__PURE__ */ jsx("circle", { className: "twc-chart__radar-dot", style: { fill: color }, cx: p[0], cy: p[1], r: "3" })
2690
- ] }, i)) : null
2691
- ] }, k);
2692
- })
2693
- ] })
2694
- }
2695
- ),
2696
- /* @__PURE__ */ jsx(ChartTooltip, { tip }),
2697
- tableFallback ? /* @__PURE__ */ jsx(
2698
- ChartTable,
2699
- {
2700
- id: tableId,
2701
- caption: caption ?? svgAriaLabel,
2702
- columns: keys,
2703
- rows: rows.map((d) => ({ label: d.label, values: keys.map((k) => fmt(Number(d[k]) || 0)) }))
2704
- }
2705
- ) : null,
2706
- legend && multi ? /* @__PURE__ */ jsx(ChartLegend, { items: keys.map((k, si) => ({ label: k, color: paletteAt(colors, si) })) }) : null
2707
- ] });
3219
+ return /* @__PURE__ */ jsxs(
3220
+ "div",
3221
+ {
3222
+ ref: containerRef,
3223
+ className: `twc-chart twc-chart--radar ${className}`.trim(),
3224
+ "data-hovering": focus != null || void 0,
3225
+ "data-clickable": clickable || void 0,
3226
+ "data-has-selection": selected != null || void 0,
3227
+ ...rest,
3228
+ children: [
3229
+ baseStyles,
3230
+ styles,
3231
+ /* @__PURE__ */ jsx(
3232
+ "svg",
3233
+ {
3234
+ viewBox: `0 0 ${H} ${H}`,
3235
+ role: "img",
3236
+ "aria-label": svgAriaLabel,
3237
+ "aria-describedby": tableId,
3238
+ style: { maxWidth: H, margin: "0 auto" },
3239
+ children: n === 0 ? /* @__PURE__ */ jsx("text", { className: "twc-chart__empty", x: cx, y: cy, textAnchor: "middle", dominantBaseline: "middle", children: "No data" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3240
+ Array.from({ length: rings }, (_, li) => {
3241
+ const rr = R * (li + 1) / rings;
3242
+ const pts = rows.map((_2, i) => polarDeg(cx, cy, rr, angleOf(i)));
3243
+ return /* @__PURE__ */ jsx("path", { className: "twc-chart__ring", d: polygonPath(pts) }, li);
3244
+ }),
3245
+ rows.map((d, i) => {
3246
+ const [ex, ey] = polarDeg(cx, cy, R, angleOf(i));
3247
+ const [lx, ly] = polarDeg(cx, cy, R + 16, angleOf(i));
3248
+ const anchor = lx > cx + 1 ? "start" : lx < cx - 1 ? "end" : "middle";
3249
+ return /* @__PURE__ */ jsxs("g", { children: [
3250
+ /* @__PURE__ */ jsx("line", { className: "twc-chart__spoke", x1: cx, y1: cy, x2: ex, y2: ey }),
3251
+ /* @__PURE__ */ jsx("text", { className: "twc-chart__radar-axis", x: lx, y: ly, textAnchor: anchor, dominantBaseline: "middle", children: d.label })
3252
+ ] }, i);
3253
+ }),
3254
+ keys.map((k, si) => {
3255
+ const color = paletteAt(colors, si);
3256
+ const pts = rows.map((d, i) => vertex(d[k], i));
3257
+ return /* @__PURE__ */ jsxs("g", { "data-mark": true, "data-active": focusActive(k), children: [
3258
+ fill ? /* @__PURE__ */ jsx("path", { className: "twc-chart__radar-area twc-chart__anim-fade", style: { fill: color, opacity: 0.18 }, d: polygonPath(pts) }) : null,
3259
+ /* @__PURE__ */ jsx("path", { className: "twc-chart__radar-line twc-chart__anim-fade", style: { stroke: color }, d: polygonPath(pts) }),
3260
+ showDots ? pts.map((p, i) => {
3261
+ const key = `${i}:${k}`;
3262
+ return /* @__PURE__ */ jsxs("g", { className: "twc-chart__anim-fade", children: [
3263
+ /* @__PURE__ */ jsx(
3264
+ "circle",
3265
+ {
3266
+ className: "twc-chart__hit",
3267
+ cx: p[0],
3268
+ cy: p[1],
3269
+ r: "14",
3270
+ onMouseMove: (e) => show({ title: rows[i].label, items: [{ color, label: k, value: fmt(Number(rows[i][k]) || 0) }] }, e),
3271
+ onMouseLeave: hide,
3272
+ onClick: () => {
3273
+ if (onDataClick) onDataClick({ label: rows[i].label, series: k, value: Number(rows[i][k]) || 0, index: i });
3274
+ selectMark(key);
3275
+ }
3276
+ }
3277
+ ),
3278
+ /* @__PURE__ */ jsx(
3279
+ "circle",
3280
+ {
3281
+ className: "twc-chart__radar-dot",
3282
+ style: { fill: color },
3283
+ cx: p[0],
3284
+ cy: p[1],
3285
+ r: "3",
3286
+ "data-mark": true,
3287
+ "data-active": focusActive(k),
3288
+ "data-selected": selected === key || void 0
3289
+ }
3290
+ )
3291
+ ] }, i);
3292
+ }) : null
3293
+ ] }, k);
3294
+ })
3295
+ ] })
3296
+ }
3297
+ ),
3298
+ /* @__PURE__ */ jsx(ChartTooltip, { tip }),
3299
+ tableFallback ? /* @__PURE__ */ jsx(
3300
+ ChartTable,
3301
+ {
3302
+ id: tableId,
3303
+ caption: caption ?? svgAriaLabel,
3304
+ columns: keys,
3305
+ rows: rows.map((d) => ({ label: d.label, values: keys.map((k) => fmt(Number(d[k]) || 0)) }))
3306
+ }
3307
+ ) : null,
3308
+ legend && multi ? /* @__PURE__ */ jsx(
3309
+ ChartLegend,
3310
+ {
3311
+ items: keys.map((k, si) => ({ label: k, color: paletteAt(colors, si) })),
3312
+ onFocus: (it) => setFocus(it.label),
3313
+ onBlur: () => setFocus(null)
3314
+ }
3315
+ ) : null
3316
+ ]
3317
+ }
3318
+ );
2708
3319
  }
2709
3320
  var CHART_CSS4 = `
2710
3321
  .twc-chart--polar svg { margin-inline: auto; }
@@ -2723,6 +3334,7 @@ function PolarAreaChart({
2723
3334
  valueFormat,
2724
3335
  height = 280,
2725
3336
  colors,
3337
+ onDataClick,
2726
3338
  ariaLabel,
2727
3339
  "aria-label": ariaLabelProp,
2728
3340
  tableFallback = true,
@@ -2736,6 +3348,8 @@ function PolarAreaChart({
2736
3348
  const tableId = tableFallback ? `${uid}-table` : void 0;
2737
3349
  const { containerRef, tip, show, hide } = useChartTooltip();
2738
3350
  const [active, setActive] = React51__default.useState(null);
3351
+ const [focus, setFocus] = React51__default.useState(null);
3352
+ const [selected, setSelected] = React51__default.useState(null);
2739
3353
  const rows = data || [];
2740
3354
  const n = rows.length;
2741
3355
  const fmt = valueFormat || fmtNumber;
@@ -2750,14 +3364,23 @@ function PolarAreaChart({
2750
3364
  };
2751
3365
  const rings = Math.max(1, Math.floor(levels));
2752
3366
  const anglePer = 360 / Math.max(1, n);
3367
+ const EXPLODE = Math.min(12, Rmax * 0.1);
2753
3368
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "polar area chart";
3369
+ const clickable = !!onDataClick;
3370
+ const emphasis = focus != null ? focus : active;
2754
3371
  const labelText8 = (label) => typeof label === "string" || typeof label === "number" ? String(label) : "";
3372
+ const clickSlice = (d, v, i) => {
3373
+ if (onDataClick) onDataClick({ label: d.label, value: v, index: i });
3374
+ setSelected((s) => s === i ? null : i);
3375
+ };
2755
3376
  return /* @__PURE__ */ jsxs(
2756
3377
  "div",
2757
3378
  {
2758
3379
  ref: containerRef,
2759
3380
  className: `twc-chart twc-chart--polar ${className}`.trim(),
2760
- "data-hovering": active != null || void 0,
3381
+ "data-hovering": emphasis != null || void 0,
3382
+ "data-clickable": clickable || void 0,
3383
+ "data-has-selection": selected != null || void 0,
2761
3384
  ...rest,
2762
3385
  children: [
2763
3386
  baseStyles,
@@ -2786,13 +3409,16 @@ function PolarAreaChart({
2786
3409
  const start = startAngle + i * anglePer;
2787
3410
  const color = d.color || paletteAt(colors, i);
2788
3411
  const tipFor = { title: labelText8(d.label), items: [{ color, label: "", value: fmt(v) }] };
3412
+ const midRad = (start + anglePer / 2 - 90) * Math.PI / 180;
3413
+ const explode = selected === i ? `translate(${r(Math.cos(midRad) * EXPLODE)}px, ${r(Math.sin(midRad) * EXPLODE)}px)` : void 0;
2789
3414
  return /* @__PURE__ */ jsx(
2790
3415
  "path",
2791
3416
  {
2792
3417
  className: "twc-chart__slice twc-chart__anim-arc",
2793
3418
  "data-mark": true,
2794
- "data-active": active === i || void 0,
2795
- style: { fill: color, fillOpacity: 0.82 },
3419
+ "data-active": emphasis == null ? void 0 : emphasis === i || void 0,
3420
+ "data-selected": selected === i || void 0,
3421
+ style: { fill: color, fillOpacity: 0.82, transform: explode },
2796
3422
  d: arcPath(cx, cy, radiusFor(v), 0, start, start + anglePer),
2797
3423
  onMouseMove: (e) => {
2798
3424
  setActive(i);
@@ -2801,7 +3427,8 @@ function PolarAreaChart({
2801
3427
  onMouseLeave: () => {
2802
3428
  setActive(null);
2803
3429
  hide();
2804
- }
3430
+ },
3431
+ onClick: () => clickSlice(d, v, i)
2805
3432
  },
2806
3433
  `slice-${i}`
2807
3434
  );
@@ -2824,7 +3451,14 @@ function PolarAreaChart({
2824
3451
  rows: rows.map((d) => ({ label: d.label, values: [fmt(Number(d.value) || 0)] }))
2825
3452
  }
2826
3453
  ) : null,
2827
- showLegend && n ? /* @__PURE__ */ jsx(ChartLegend, { items: rows.map((d, i) => ({ label: d.label, color: d.color || paletteAt(colors, i) })) }) : null
3454
+ showLegend && n ? /* @__PURE__ */ jsx(
3455
+ ChartLegend,
3456
+ {
3457
+ items: rows.map((d, i) => ({ label: d.label, color: d.color || paletteAt(colors, i) })),
3458
+ onFocus: n > 1 ? (_it, i) => setFocus(i) : void 0,
3459
+ onBlur: n > 1 ? () => setFocus(null) : void 0
3460
+ }
3461
+ ) : null
2828
3462
  ]
2829
3463
  }
2830
3464
  );
@@ -2834,7 +3468,7 @@ var HEATMAP_CSS = `
2834
3468
  .twc-chart__hm-cell { transition: opacity var(--duration-fast) var(--ease-standard); }
2835
3469
  .twc-chart__hm-cell:hover { opacity: 0.82; }
2836
3470
  .twc-chart__hm-empty { fill: var(--color-surface-sunken); }
2837
- .twc-chart__hm-val { fill: var(--color-text); font-size: 10px; font-weight: 500; pointer-events: none; }
3471
+ .twc-chart__hm-val { fill: var(--color-text); paint-order: stroke; stroke: var(--color-surface); stroke-width: 2.5px; stroke-linejoin: round; vector-effect: non-scaling-stroke; font-size: 10px; font-weight: 500; pointer-events: none; }
2838
3472
  .twc-chart__hm-tick { fill: var(--color-text-subtle); font-size: 11px; }
2839
3473
  .twc-chart__hm-title { fill: var(--color-text-muted); font-size: 11px; font-weight: 500; }
2840
3474
  .twc-chart__hm-scale { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-3); font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -2852,8 +3486,10 @@ function Heatmap({
2852
3486
  xLabel,
2853
3487
  yLabel,
2854
3488
  showLegend = true,
3489
+ zoomable = false,
2855
3490
  valueFormat,
2856
3491
  height = 300,
3492
+ onDataClick,
2857
3493
  ariaLabel,
2858
3494
  "aria-label": ariaLabelProp,
2859
3495
  tableFallback = true,
@@ -2867,10 +3503,17 @@ function Heatmap({
2867
3503
  const tableId = tableFallback ? `${uid}-table` : void 0;
2868
3504
  const { containerRef, tip, show, hide } = useChartTooltip();
2869
3505
  const [hovered, setHovered] = React51__default.useState(null);
3506
+ const [selected, setSelected] = React51__default.useState(null);
3507
+ const [zoom, setZoom] = React51__default.useState(null);
3508
+ const [drag, setDrag] = React51__default.useState(null);
3509
+ const svgRef = React51__default.useRef(null);
3510
+ const movedRef = React51__default.useRef(false);
3511
+ const clickable = !!onDataClick;
3512
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
2870
3513
  const rows = Array.isArray(data) ? data : [];
2871
3514
  const fmt = valueFormat || fmtNumber;
2872
- const xs = [];
2873
- const ys = [];
3515
+ const fullXs = [];
3516
+ const fullYs = [];
2874
3517
  const seenX = /* @__PURE__ */ new Set();
2875
3518
  const seenY = /* @__PURE__ */ new Set();
2876
3519
  const lookup = /* @__PURE__ */ new Map();
@@ -2879,11 +3522,11 @@ function Heatmap({
2879
3522
  const ky = String(d.y);
2880
3523
  if (!seenX.has(kx)) {
2881
3524
  seenX.add(kx);
2882
- xs.push(d.x);
3525
+ fullXs.push(d.x);
2883
3526
  }
2884
3527
  if (!seenY.has(ky)) {
2885
3528
  seenY.add(ky);
2886
- ys.push(d.y);
3529
+ fullYs.push(d.y);
2887
3530
  }
2888
3531
  lookup.set(`${kx}\0${ky}`, Number(d.value) || 0);
2889
3532
  }
@@ -2896,6 +3539,15 @@ function Heatmap({
2896
3539
  const span = hi - lo;
2897
3540
  const pctOf = (v) => span > 0 ? clamp((v - lo) / span * 100, 0, 100) : 60;
2898
3541
  const fillOf = (v) => `color-mix(in srgb, ${colorScale} ${pctOf(v)}%, transparent)`;
3542
+ const nX = fullXs.length;
3543
+ const nY = fullYs.length;
3544
+ const canZoom = zoomable && nX > 1 && nY > 1;
3545
+ const vc0 = canZoom && zoom ? clamp(zoom.c0, 0, nX - 1) : 0;
3546
+ const vc1 = canZoom && zoom ? clamp(zoom.c1, 0, nX - 1) : Math.max(0, nX - 1);
3547
+ const vr0 = canZoom && zoom ? clamp(zoom.r0, 0, nY - 1) : 0;
3548
+ const vr1 = canZoom && zoom ? clamp(zoom.r1, 0, nY - 1) : Math.max(0, nY - 1);
3549
+ const xs = fullXs.slice(vc0, vc1 + 1);
3550
+ const ys = fullYs.slice(vr0, vr1 + 1);
2899
3551
  const cols = xs.length;
2900
3552
  const rowN = ys.length;
2901
3553
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "heatmap";
@@ -2914,33 +3566,161 @@ function Heatmap({
2914
3566
  const cw = innerW / Math.max(1, cols);
2915
3567
  const chh = innerH / Math.max(1, rowN);
2916
3568
  const gap = clamp(cellGap, 0, Math.min(cw, chh) - 1) || 0;
2917
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--heatmap ${className}`.trim(), "data-hovering": hovered != null || void 0, ...rest, children: [
2918
- baseStyles,
2919
- styles,
2920
- /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
2921
- ys.map(
2922
- (y, ri) => xs.map((x, ci) => {
2923
- const v = cellOf(x, y);
2924
- const has = v !== void 0;
2925
- const cx = padL + cw * ci + gap / 2;
2926
- const cy = padT + chh * ri + gap / 2;
2927
- const cwPx = Math.max(0, cw - gap);
2928
- const chPx = Math.max(0, chh - gap);
2929
- const cellKey = `${ci}-${ri}`;
2930
- const fill = has ? fillOf(v) : void 0;
2931
- return /* @__PURE__ */ jsxs("g", { children: [
2932
- /* @__PURE__ */ jsx(
2933
- "rect",
2934
- {
2935
- className: `${has ? "twc-chart__hm-cell" : "twc-chart__hm-cell twc-chart__hm-empty"} twc-chart__anim-fade`,
2936
- x: cx,
2937
- y: cy,
2938
- width: cwPx,
2939
- height: chPx,
2940
- rx: radius,
2941
- style: has ? { fill } : void 0,
2942
- "data-mark": true,
2943
- "data-active": hovered === cellKey || void 0,
3569
+ const svgPoint = (e) => {
3570
+ const svg = svgRef.current;
3571
+ if (!svg || typeof svg.getScreenCTM !== "function" || typeof svg.createSVGPoint !== "function") return null;
3572
+ const ctm = svg.getScreenCTM();
3573
+ if (!ctm || !ctm.a || !ctm.d) return null;
3574
+ const pt = svg.createSVGPoint();
3575
+ pt.x = e.clientX;
3576
+ pt.y = e.clientY;
3577
+ const loc = pt.matrixTransform(ctm.inverse());
3578
+ if (!Number.isFinite(loc.x) || !Number.isFinite(loc.y)) return null;
3579
+ return { px: loc.x, py: loc.y };
3580
+ };
3581
+ const colAtPx = (px) => cw > 0 ? clamp(Math.floor((clamp(px, padL, padL + innerW) - padL) / cw), 0, cols - 1) : 0;
3582
+ const rowAtPx = (py) => chh > 0 ? clamp(Math.floor((clamp(py, padT, padT + innerH) - padT) / chh), 0, rowN - 1) : 0;
3583
+ const cellAtPx = (px, py) => {
3584
+ if (!(cols > 0) || !(rowN > 0)) return null;
3585
+ if (px < padL || px > padL + innerW || py < padT || py > padT + innerH) return null;
3586
+ const ci = colAtPx(px);
3587
+ const ri = rowAtPx(py);
3588
+ const x = xs[ci], y = ys[ri];
3589
+ const v = cellOf(x, y);
3590
+ const has = v !== void 0;
3591
+ return { x, y, v, has, key: `${vc0 + ci}-${vr0 + ri}`, fill: has ? fillOf(v) : void 0 };
3592
+ };
3593
+ const applyZoom = (win) => {
3594
+ const c0 = clamp(Math.min(win.c0, win.c1), 0, nX - 1);
3595
+ const c1 = clamp(Math.max(win.c0, win.c1), 0, nX - 1);
3596
+ const r0 = clamp(Math.min(win.r0, win.r1), 0, nY - 1);
3597
+ const r1 = clamp(Math.max(win.r0, win.r1), 0, nY - 1);
3598
+ if (!(c1 >= c0) || !(r1 >= r0)) return;
3599
+ if (c0 === 0 && c1 === nX - 1 && r0 === 0 && r1 === nY - 1) {
3600
+ setZoom(null);
3601
+ return;
3602
+ }
3603
+ setZoom({ c0, c1, r0, r1 });
3604
+ };
3605
+ const panTo = (z0, dCols, dRows) => {
3606
+ const wc = z0.c1 - z0.c0, wr = z0.r1 - z0.r0;
3607
+ const c0 = clamp(z0.c0 + dCols, 0, Math.max(0, nX - 1 - wc));
3608
+ const r0 = clamp(z0.r0 + dRows, 0, Math.max(0, nY - 1 - wr));
3609
+ setZoom({ c0, c1: c0 + wc, r0, r1: r0 + wr });
3610
+ };
3611
+ const onOverlayDown = (e) => {
3612
+ const pt = svgPoint(e);
3613
+ if (!pt) return;
3614
+ movedRef.current = false;
3615
+ setDrag({ x0: pt.px, y0: pt.py, x1: pt.px, y1: pt.py, pan: e.shiftKey, z0: zoom || { c0: 0, c1: nX - 1, r0: 0, r1: nY - 1 } });
3616
+ };
3617
+ const onOverlayMove = (e) => {
3618
+ const pt = svgPoint(e);
3619
+ if (!pt) return;
3620
+ if (drag) {
3621
+ if (Math.abs(pt.px - drag.x0) > 4 || Math.abs(pt.py - drag.y0) > 4) movedRef.current = true;
3622
+ if (drag.pan) {
3623
+ if (zoom) {
3624
+ const dCols = cw > 0 ? -Math.round((pt.px - drag.x0) / cw) : 0;
3625
+ const dRows = chh > 0 ? -Math.round((pt.py - drag.y0) / chh) : 0;
3626
+ panTo(drag.z0, dCols, dRows);
3627
+ }
3628
+ } else {
3629
+ setDrag({ ...drag, x1: pt.px, y1: pt.py });
3630
+ }
3631
+ setHovered(null);
3632
+ hide();
3633
+ return;
3634
+ }
3635
+ const cell = cellAtPx(pt.px, pt.py);
3636
+ if (cell) {
3637
+ setHovered(cell.key);
3638
+ show({ title: `${labelText3(cell.y)} / ${labelText3(cell.x)}`, items: [{ color: cell.fill, label: "", value: cell.has ? fmt(cell.v) : "\u2014" }] }, e);
3639
+ } else {
3640
+ setHovered(null);
3641
+ hide();
3642
+ }
3643
+ };
3644
+ const onOverlayUp = () => {
3645
+ if (drag && !drag.pan && movedRef.current) {
3646
+ applyZoom({
3647
+ c0: vc0 + colAtPx(drag.x0),
3648
+ c1: vc0 + colAtPx(drag.x1),
3649
+ r0: vr0 + rowAtPx(drag.y0),
3650
+ r1: vr0 + rowAtPx(drag.y1)
3651
+ });
3652
+ }
3653
+ setDrag(null);
3654
+ };
3655
+ const onOverlayLeave = () => {
3656
+ setHovered(null);
3657
+ hide();
3658
+ setDrag(null);
3659
+ };
3660
+ const onOverlayClick = (e) => {
3661
+ if (movedRef.current) {
3662
+ movedRef.current = false;
3663
+ return;
3664
+ }
3665
+ const pt = svgPoint(e);
3666
+ if (!pt) return;
3667
+ const cell = cellAtPx(pt.px, pt.py);
3668
+ if (cell && cell.has) {
3669
+ if (onDataClick) onDataClick({ x: cell.x, y: cell.y, value: cell.v });
3670
+ selectMark(cell.key);
3671
+ }
3672
+ };
3673
+ React51__default.useEffect(() => {
3674
+ if (!canZoom) return void 0;
3675
+ const svg = svgRef.current;
3676
+ if (!svg) return void 0;
3677
+ const onWheel = (e) => {
3678
+ e.preventDefault();
3679
+ const pt = svgPoint(e);
3680
+ const fx = pt && innerW > 0 ? clamp((pt.px - padL) / innerW, 0, 1) : 0.5;
3681
+ const fy = pt && innerH > 0 ? clamp((pt.py - padT) / innerH, 0, 1) : 0.5;
3682
+ const factor = e.deltaY < 0 ? 0.7 : 1.4;
3683
+ const wc = vc1 - vc0 + 1, wr = vr1 - vr0 + 1;
3684
+ const newWc = clamp(Math.round(wc * factor), 1, nX);
3685
+ const newWr = clamp(Math.round(wr * factor), 1, nY);
3686
+ const curCol = vc0 + clamp(Math.floor(fx * cols), 0, cols - 1);
3687
+ const curRow = vr0 + clamp(Math.floor(fy * rowN), 0, rowN - 1);
3688
+ const nc0 = clamp(Math.round(curCol - fx * (newWc - 1)), 0, nX - newWc);
3689
+ const nr0 = clamp(Math.round(curRow - fy * (newWr - 1)), 0, nY - newWr);
3690
+ applyZoom({ c0: nc0, c1: nc0 + newWc - 1, r0: nr0, r1: nr0 + newWr - 1 });
3691
+ };
3692
+ svg.addEventListener("wheel", onWheel, { passive: false });
3693
+ return () => svg.removeEventListener("wheel", onWheel);
3694
+ }, [canZoom, vc0, vc1, vr0, vr1]);
3695
+ const dragBand = canZoom && drag && !drag.pan && Math.abs(drag.x1 - drag.x0) > 2 && Math.abs(drag.y1 - drag.y0) > 2;
3696
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--heatmap ${className}`.trim(), "data-hovering": hovered != null || void 0, "data-has-selection": selected != null || void 0, "data-clickable": clickable || void 0, ...rest, children: [
3697
+ baseStyles,
3698
+ styles,
3699
+ /* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
3700
+ ys.map(
3701
+ (y, ri) => xs.map((x, ci) => {
3702
+ const v = cellOf(x, y);
3703
+ const has = v !== void 0;
3704
+ const cx = padL + cw * ci + gap / 2;
3705
+ const cy = padT + chh * ri + gap / 2;
3706
+ const cwPx = Math.max(0, cw - gap);
3707
+ const chPx = Math.max(0, chh - gap);
3708
+ const cellKey = `${vc0 + ci}-${vr0 + ri}`;
3709
+ const fill = has ? fillOf(v) : void 0;
3710
+ return /* @__PURE__ */ jsxs("g", { children: [
3711
+ /* @__PURE__ */ jsx(
3712
+ "rect",
3713
+ {
3714
+ className: `${has ? "twc-chart__hm-cell" : "twc-chart__hm-cell twc-chart__hm-empty"} twc-chart__anim-fade`,
3715
+ x: cx,
3716
+ y: cy,
3717
+ width: cwPx,
3718
+ height: chPx,
3719
+ rx: radius,
3720
+ style: has ? { fill } : void 0,
3721
+ "data-mark": true,
3722
+ "data-active": hovered === cellKey || void 0,
3723
+ "data-selected": selected === cellKey || void 0,
2944
3724
  onMouseMove: (e) => {
2945
3725
  setHovered(cellKey);
2946
3726
  show({ title: `${labelText3(y)} / ${labelText3(x)}`, items: [{ color: fill, label: "", value: has ? fmt(v) : "\u2014" }] }, e);
@@ -2948,7 +3728,11 @@ function Heatmap({
2948
3728
  onMouseLeave: () => {
2949
3729
  setHovered(null);
2950
3730
  hide();
2951
- }
3731
+ },
3732
+ onClick: has ? () => {
3733
+ if (onDataClick) onDataClick({ x, y, value: v });
3734
+ selectMark(cellKey);
3735
+ } : void 0
2952
3736
  }
2953
3737
  ),
2954
3738
  showValues && has ? /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-val", x: cx + cwPx / 2, y: cy + chPx / 2 + 3.5, textAnchor: "middle", children: fmt(v) }) : null
@@ -2958,8 +3742,43 @@ function Heatmap({
2958
3742
  ys.map((y, ri) => /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-tick", x: padL - 8, y: padT + chh * ri + chh / 2 + 4, textAnchor: "end", children: labelText3(y) }, ri)),
2959
3743
  xs.map((x, ci) => /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-tick", x: padL + cw * ci + cw / 2, y: padT + innerH + 15, textAnchor: "middle", children: labelText3(x) }, ci)),
2960
3744
  yLabel != null ? /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-title", transform: `translate(11 ${padT + innerH / 2}) rotate(-90)`, textAnchor: "middle", children: labelText3(yLabel) }) : null,
2961
- xLabel != null ? /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-title", x: padL + innerW / 2, y: H - 3, textAnchor: "middle", children: labelText3(xLabel) }) : null
3745
+ xLabel != null ? /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-title", x: padL + innerW / 2, y: H - 3, textAnchor: "middle", children: labelText3(xLabel) }) : null,
3746
+ dragBand ? /* @__PURE__ */ jsx(
3747
+ "rect",
3748
+ {
3749
+ className: "twc-chart__zoomband",
3750
+ x: Math.min(drag.x0, drag.x1),
3751
+ y: Math.min(drag.y0, drag.y1),
3752
+ width: Math.abs(drag.x1 - drag.x0),
3753
+ height: Math.abs(drag.y1 - drag.y0)
3754
+ }
3755
+ ) : null,
3756
+ canZoom ? /* @__PURE__ */ jsx(
3757
+ "rect",
3758
+ {
3759
+ className: "twc-chart__overlay",
3760
+ "data-zoom": "true",
3761
+ "data-clickable": clickable || void 0,
3762
+ "data-panning": drag && drag.pan || void 0,
3763
+ x: padL,
3764
+ y: padT,
3765
+ width: Math.max(0, innerW),
3766
+ height: Math.max(0, innerH),
3767
+ onMouseDown: onOverlayDown,
3768
+ onMouseMove: onOverlayMove,
3769
+ onMouseUp: onOverlayUp,
3770
+ onMouseLeave: onOverlayLeave,
3771
+ onClick: onOverlayClick
3772
+ }
3773
+ ) : null
2962
3774
  ] }),
3775
+ canZoom && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
3776
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3777
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
3778
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
3779
+ ] }),
3780
+ "Reset"
3781
+ ] }) : null,
2963
3782
  /* @__PURE__ */ jsx(ChartTooltip, { tip }),
2964
3783
  showLegend && span > 0 ? /* @__PURE__ */ jsxs("div", { className: "twc-chart__hm-scale", "aria-hidden": "true", children: [
2965
3784
  /* @__PURE__ */ jsx("span", { children: fmt(lo) }),
@@ -2977,10 +3796,10 @@ function Heatmap({
2977
3796
  {
2978
3797
  id: tableId,
2979
3798
  caption: caption ?? svgAriaLabel,
2980
- columns: xs.map(labelText3),
2981
- rows: ys.map((y) => ({
3799
+ columns: fullXs.map(labelText3),
3800
+ rows: fullYs.map((y) => ({
2982
3801
  label: labelText3(y),
2983
- values: xs.map((x) => {
3802
+ values: fullXs.map((x) => {
2984
3803
  const v = cellOf(x, y);
2985
3804
  return v === void 0 ? "\u2014" : fmt(v);
2986
3805
  })
@@ -2994,9 +3813,13 @@ function labelText3(label) {
2994
3813
  }
2995
3814
  var FUNNEL_CSS = `
2996
3815
  .twc-chart--funnel svg { overflow: visible; }
2997
- .twc-funnel__label { fill: var(--color-text-inverted); font-family: var(--font-sans); pointer-events: none; }
3816
+ .twc-funnel__label { fill: var(--color-chart-on-fill); paint-order: stroke; stroke: var(--color-chart-on-fill-halo); stroke-width: 3px; stroke-linejoin: round; vector-effect: non-scaling-stroke; font-family: var(--font-sans); pointer-events: none; }
2998
3817
  .twc-funnel__name { font-size: 12px; font-weight: 600; }
2999
- .twc-funnel__value { font-size: 11px; fill-opacity: 0.82; }
3818
+ .twc-funnel__value { font-size: 11px; fill-opacity: 0.9; }
3819
+ /* Explode: nudge the selected stage shape outward so it separates from the funnel
3820
+ (eases via the shared [data-mark] transform transition in _chart.js). */
3821
+ .twc-chart--funnel [data-mark][data-selected="true"] { transform: translateX(14px); }
3822
+ .twc-chart--funnel[data-orientation="horizontal"] [data-mark][data-selected="true"] { transform: translateY(-14px); }
3000
3823
  `;
3001
3824
  function FunnelChart({
3002
3825
  data,
@@ -3007,6 +3830,7 @@ function FunnelChart({
3007
3830
  height = 300,
3008
3831
  colors,
3009
3832
  valueFormat,
3833
+ onDataClick,
3010
3834
  ariaLabel,
3011
3835
  "aria-label": ariaLabelProp,
3012
3836
  tableFallback = true,
@@ -3020,6 +3844,8 @@ function FunnelChart({
3020
3844
  const tableId = tableFallback ? `${uid}-table` : void 0;
3021
3845
  const { containerRef, tip, show, hide } = useChartTooltip();
3022
3846
  const [active, setActive] = React51__default.useState(null);
3847
+ const [selected, setSelected] = React51__default.useState(null);
3848
+ const clickable = !!onDataClick;
3023
3849
  const rows = data || [];
3024
3850
  const n = rows.length;
3025
3851
  const fmt = valueFormat || fmtNumber;
@@ -3071,6 +3897,7 @@ function FunnelChart({
3071
3897
  fill: color,
3072
3898
  "data-mark": true,
3073
3899
  "data-active": active === i ? "true" : void 0,
3900
+ "data-selected": selected === i ? "true" : void 0,
3074
3901
  onMouseMove: (e) => {
3075
3902
  setActive(i);
3076
3903
  show({ title: d.label, items: [{ color, label: fmt(v), value: pctText(v) }] }, e);
@@ -3078,6 +3905,10 @@ function FunnelChart({
3078
3905
  onMouseLeave: () => {
3079
3906
  setActive(null);
3080
3907
  hide();
3908
+ },
3909
+ onClick: () => {
3910
+ if (onDataClick) onDataClick({ label: d.label, value: v, index: i, percent: pct(v) });
3911
+ setSelected((s) => s === i ? null : i);
3081
3912
  }
3082
3913
  }
3083
3914
  ),
@@ -3093,7 +3924,7 @@ function FunnelChart({
3093
3924
  )) }) : null
3094
3925
  ] }, i);
3095
3926
  });
3096
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--funnel ${className}`.trim(), "data-orientation": horizontal ? "horizontal" : "vertical", "data-hovering": active != null ? "true" : void 0, ...rest, children: [
3927
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--funnel ${className}`.trim(), "data-orientation": horizontal ? "horizontal" : "vertical", "data-hovering": active != null ? "true" : void 0, "data-has-selection": selected != null || void 0, "data-clickable": clickable || void 0, ...rest, children: [
3097
3928
  baseStyles,
3098
3929
  styles,
3099
3930
  /* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: renderStages() }),
@@ -3110,8 +3941,8 @@ function FunnelChart({
3110
3941
  ] });
3111
3942
  }
3112
3943
  var TREEMAP_CSS = `
3113
- .twc-chart__tile-label { fill: var(--color-text-inverted); font-size: var(--text-xs); font-weight: var(--font-semibold); pointer-events: none; }
3114
- .twc-chart__tile-value { fill: var(--color-text-inverted); font-size: 11px; opacity: 0.82; pointer-events: none; }
3944
+ .twc-chart__tile-label { fill: var(--color-chart-on-fill); paint-order: stroke; stroke: var(--color-chart-on-fill-halo); stroke-width: 3px; stroke-linejoin: round; vector-effect: non-scaling-stroke; font-size: var(--text-xs); font-weight: var(--font-semibold); pointer-events: none; }
3945
+ .twc-chart__tile-value { fill: var(--color-chart-on-fill); paint-order: stroke; stroke: var(--color-chart-on-fill-halo); stroke-width: 3px; stroke-linejoin: round; vector-effect: non-scaling-stroke; font-size: 11px; opacity: 0.9; pointer-events: none; }
3115
3946
  .twc-chart__empty { fill: var(--color-text-subtle); font-size: var(--text-sm); }
3116
3947
  `;
3117
3948
  function Treemap({
@@ -3121,6 +3952,7 @@ function Treemap({
3121
3952
  height = 300,
3122
3953
  colors,
3123
3954
  gap = 2,
3955
+ onDataClick,
3124
3956
  ariaLabel,
3125
3957
  "aria-label": ariaLabelProp,
3126
3958
  tableFallback = true,
@@ -3134,16 +3966,18 @@ function Treemap({
3134
3966
  const tableId = tableFallback ? `${uid}-table` : void 0;
3135
3967
  const { containerRef, tip, show, hide } = useChartTooltip();
3136
3968
  const [hovered, setHovered] = React51__default.useState(null);
3969
+ const [selected, setSelected] = React51__default.useState(null);
3137
3970
  const rows = data || [];
3138
3971
  const fmt = valueFormat || fmtNumber;
3139
3972
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "treemap";
3973
+ const clickable = !!onDataClick;
3140
3974
  const W = 600, H = height;
3141
3975
  const weights = rows.map((d) => Math.max(0, Number(d.value) || 0));
3142
3976
  const total = sum(weights);
3143
3977
  const items = total > 0 ? rows.map((d, i) => ({ i, w: weights[i] })).sort((a, b) => b.w - a.w).map((it) => ({ ...it, area: it.w / total * (W * H) })) : [];
3144
3978
  const tiles = squarify(items, { x: 0, y: 0, w: W, h: H });
3145
3979
  const g = Math.max(0, gap);
3146
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--treemap ${className}`.trim(), "data-hovering": hovered != null || void 0, ...rest, children: [
3980
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--treemap ${className}`.trim(), "data-hovering": hovered != null || void 0, "data-has-selection": selected != null || void 0, "data-clickable": clickable || void 0, ...rest, children: [
3147
3981
  baseStyles,
3148
3982
  styles,
3149
3983
  /* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: tiles.length === 0 ? /* @__PURE__ */ jsx("text", { className: "twc-chart__empty", x: W / 2, y: H / 2, textAnchor: "middle", dominantBaseline: "middle", children: "No data" }) : tiles.map((t) => {
@@ -3166,6 +4000,7 @@ function Treemap({
3166
4000
  className: "twc-chart__tile twc-chart__anim-fade",
3167
4001
  "data-mark": true,
3168
4002
  "data-active": hovered === t.i || void 0,
4003
+ "data-selected": selected === t.i || void 0,
3169
4004
  x,
3170
4005
  y,
3171
4006
  width: w,
@@ -3179,6 +4014,10 @@ function Treemap({
3179
4014
  onMouseLeave: () => {
3180
4015
  setHovered(null);
3181
4016
  hide();
4017
+ },
4018
+ onClick: () => {
4019
+ if (onDataClick) onDataClick({ label: d.label, value: t.w, index: t.i, share: total > 0 ? t.w / total : 0 });
4020
+ setSelected((s) => s === t.i ? null : t.i);
3182
4021
  }
3183
4022
  }
3184
4023
  ),
@@ -3273,7 +4112,9 @@ function Candlestick({
3273
4112
  height = 300,
3274
4113
  showGrid = true,
3275
4114
  showAxis = true,
4115
+ zoomable = false,
3276
4116
  valueFormat,
4117
+ onDataClick,
3277
4118
  ariaLabel,
3278
4119
  "aria-label": ariaLabelProp,
3279
4120
  tableFallback = true,
@@ -3286,8 +4127,15 @@ function Candlestick({
3286
4127
  const uid = React51__default.useId();
3287
4128
  const tableId = tableFallback ? `${uid}-table` : void 0;
3288
4129
  const { containerRef, tip, show, hide } = useChartTooltip();
3289
- const rows = data || [];
4130
+ const [selected, setSelected] = React51__default.useState(null);
4131
+ const [zoom, setZoom] = React51__default.useState(null);
4132
+ const [drag, setDrag] = React51__default.useState(null);
4133
+ const svgRef = React51__default.useRef(null);
3290
4134
  const fmt = valueFormat || fmtNumber;
4135
+ const clickable = !!onDataClick;
4136
+ const allRows = data || [];
4137
+ const baseIdx = zoom ? zoom.s : 0;
4138
+ const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
3291
4139
  const W = 600, H = height;
3292
4140
  const padL = showAxis ? 44 : 8;
3293
4141
  const padR = 10;
@@ -3306,90 +4154,248 @@ function Candlestick({
3306
4154
  const bodyW = Math.max(1, Math.min(catBand * 0.7, 22));
3307
4155
  const labelStep = Math.max(1, Math.ceil(rows.length / 12));
3308
4156
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "candlestick chart";
3309
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--candlestick ${className}`.trim(), ...rest, children: [
3310
- baseStyles,
3311
- styles,
3312
- /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
3313
- showGrid ? scale.ticks.map((t, i) => {
3314
- const p = vPos(t);
3315
- return /* @__PURE__ */ jsx("line", { className: "twc-candlestick__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
3316
- }) : null,
3317
- showAxis ? scale.ticks.map((t, i) => /* @__PURE__ */ jsx("text", { className: "twc-candlestick__axis", x: padL - 8, y: vPos(t) + 4, textAnchor: "end", children: shortNum(t) }, i)) : null,
3318
- rows.map((d, i) => {
3319
- const open = Number(d.open) || 0;
3320
- const high = Number(d.high) || 0;
3321
- const low = Number(d.low) || 0;
3322
- const close = Number(d.close) || 0;
3323
- const up = close >= open;
3324
- const color = up ? upColor : downColor;
3325
- const x = cx(i);
3326
- const yTop = vPos(Math.max(open, close));
3327
- const yBot = vPos(Math.min(open, close));
3328
- const bh = Math.max(1, yBot - yTop);
3329
- const tipFor = {
3330
- title: labelText5(d.label),
3331
- items: [
3332
- { color, label: "Open", value: fmt(open) },
3333
- { label: "High", value: fmt(high) },
3334
- { label: "Low", value: fmt(low) },
3335
- { label: "Close", value: fmt(close) }
3336
- ]
3337
- };
3338
- return /* @__PURE__ */ jsxs(
3339
- "g",
3340
- {
3341
- className: "twc-chart__anim-fade",
3342
- "data-dir": up ? "up" : "down",
3343
- onMouseMove: (e) => show(tipFor, e),
3344
- onMouseLeave: hide,
3345
- children: [
3346
- /* @__PURE__ */ jsx("rect", { x: padL + catBand * i, y: padT, width: catBand, height: innerH, fill: "transparent" }),
3347
- /* @__PURE__ */ jsx(
3348
- "line",
3349
- {
3350
- className: "twc-candlestick__wick",
3351
- style: { stroke: color },
3352
- x1: x,
3353
- y1: vPos(high),
3354
- x2: x,
3355
- y2: vPos(low)
3356
- }
3357
- ),
3358
- /* @__PURE__ */ jsx(
3359
- "rect",
3360
- {
3361
- className: "twc-candlestick__body",
3362
- style: { fill: color },
3363
- x: x - bodyW / 2,
3364
- y: yTop,
3365
- width: bodyW,
3366
- height: bh,
3367
- rx: "1"
3368
- }
3369
- )
3370
- ]
3371
- },
3372
- i
3373
- );
3374
- }),
3375
- showAxis ? rows.map(
3376
- (d, i) => i % labelStep === 0 ? /* @__PURE__ */ jsx("text", { className: "twc-candlestick__axis", x: cx(i), y: H - 8, textAnchor: "middle", children: d.label }, i) : null
3377
- ) : null
3378
- ] }),
3379
- /* @__PURE__ */ jsx(ChartTooltip, { tip }),
3380
- tableFallback ? /* @__PURE__ */ jsx(
3381
- ChartTable,
3382
- {
3383
- id: tableId,
3384
- caption: caption ?? svgAriaLabel,
3385
- columns: ["open", "high", "low", "close"],
3386
- rows: rows.map((d) => ({
3387
- label: d.label,
3388
- values: [Number(d.open) || 0, Number(d.high) || 0, Number(d.low) || 0, Number(d.close) || 0].map(fmt)
3389
- }))
4157
+ const tipFor = (d) => {
4158
+ const open = Number(d.open) || 0, high = Number(d.high) || 0;
4159
+ const low = Number(d.low) || 0, close = Number(d.close) || 0;
4160
+ const color = close >= open ? upColor : downColor;
4161
+ return {
4162
+ title: labelText5(d.label),
4163
+ items: [
4164
+ { color, label: "Open", value: fmt(open) },
4165
+ { label: "High", value: fmt(high) },
4166
+ { label: "Low", value: fmt(low) },
4167
+ { label: "Close", value: fmt(close) }
4168
+ ]
4169
+ };
4170
+ };
4171
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
4172
+ const clickCandle = (d, index) => {
4173
+ if (!onDataClick) return;
4174
+ onDataClick({
4175
+ label: d.label,
4176
+ open: Number(d.open) || 0,
4177
+ high: Number(d.high) || 0,
4178
+ low: Number(d.low) || 0,
4179
+ close: Number(d.close) || 0,
4180
+ index
4181
+ });
4182
+ };
4183
+ const applyZoom = (s, e) => {
4184
+ const n = allRows.length;
4185
+ if (e - s >= n - 1) {
4186
+ setZoom(null);
4187
+ return;
4188
+ }
4189
+ setZoom({ s: Math.max(0, s), e: Math.min(n - 1, e) });
4190
+ };
4191
+ const catFromEvent = (e) => {
4192
+ const svg = svgRef.current;
4193
+ if (!svg) return null;
4194
+ if (!rows.length) return null;
4195
+ const r2 = svg.getBoundingClientRect();
4196
+ if (!(r2.width > 0)) return null;
4197
+ const frac = Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW));
4198
+ const idx = Math.floor(frac * rows.length);
4199
+ return Math.min(rows.length - 1, Math.max(0, idx));
4200
+ };
4201
+ const onOverlayMove = (e) => {
4202
+ const idx = catFromEvent(e);
4203
+ if (idx == null || !rows[idx]) return;
4204
+ show(tipFor(rows[idx]), e);
4205
+ if (drag) {
4206
+ if (drag.pan) {
4207
+ const spanZ = drag.z0.e - drag.z0.s;
4208
+ const s = Math.min(allRows.length - 1 - spanZ, Math.max(0, drag.z0.s + (drag.start - idx)));
4209
+ applyZoom(s, s + spanZ);
4210
+ } else {
4211
+ setDrag({ ...drag, end: idx });
3390
4212
  }
3391
- ) : null
3392
- ] });
4213
+ }
4214
+ };
4215
+ const onOverlayDown = (e) => {
4216
+ const idx = catFromEvent(e);
4217
+ if (idx == null) return;
4218
+ setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
4219
+ };
4220
+ const onOverlayUp = () => {
4221
+ if (drag && !drag.pan) {
4222
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
4223
+ if (hi - lo >= 1) applyZoom(baseIdx + lo, baseIdx + hi);
4224
+ }
4225
+ setDrag(null);
4226
+ };
4227
+ const onOverlayClick = (e) => {
4228
+ if (!onDataClick) return;
4229
+ const idx = catFromEvent(e);
4230
+ if (idx == null || !rows[idx]) return;
4231
+ clickCandle(rows[idx], baseIdx + idx);
4232
+ selectMark(baseIdx + idx);
4233
+ };
4234
+ React51__default.useEffect(() => {
4235
+ if (!zoomable) return void 0;
4236
+ const svg = svgRef.current;
4237
+ if (!svg) return void 0;
4238
+ const onWheel = (e) => {
4239
+ const n = allRows.length;
4240
+ if (!n) return;
4241
+ e.preventDefault();
4242
+ const cur = zoom || { s: 0, e: n - 1 };
4243
+ const spanZ = cur.e - cur.s;
4244
+ const r2 = svg.getBoundingClientRect();
4245
+ const frac = r2.width > 0 ? Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW)) : 0.5;
4246
+ const center = cur.s + Math.round(frac * spanZ);
4247
+ const newSpan = Math.min(n - 1, Math.max(1, Math.round(spanZ * (e.deltaY < 0 ? 0.8 : 1.25))));
4248
+ const s = Math.min(n - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frac)));
4249
+ applyZoom(s, s + newSpan);
4250
+ };
4251
+ svg.addEventListener("wheel", onWheel, { passive: false });
4252
+ return () => svg.removeEventListener("wheel", onWheel);
4253
+ }, [zoomable, zoom, allRows.length]);
4254
+ const renderZones = () => rows.map((d, i) => {
4255
+ const gi = baseIdx + i;
4256
+ return /* @__PURE__ */ jsx(
4257
+ "rect",
4258
+ {
4259
+ className: "twc-chart__zone",
4260
+ x: padL + catBand * i,
4261
+ y: padT,
4262
+ width: catBand,
4263
+ height: innerH,
4264
+ "data-clickable": clickable || void 0,
4265
+ onMouseMove: (e) => show(tipFor(d), e),
4266
+ onMouseLeave: hide,
4267
+ onClick: () => {
4268
+ if (onDataClick) clickCandle(d, gi);
4269
+ }
4270
+ },
4271
+ i
4272
+ );
4273
+ });
4274
+ const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
4275
+ return /* @__PURE__ */ jsxs(
4276
+ "div",
4277
+ {
4278
+ ref: containerRef,
4279
+ className: `twc-chart twc-chart--candlestick ${className}`.trim(),
4280
+ "data-clickable": clickable || void 0,
4281
+ "data-has-selection": selected != null || void 0,
4282
+ ...rest,
4283
+ children: [
4284
+ baseStyles,
4285
+ styles,
4286
+ /* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
4287
+ showGrid ? scale.ticks.map((t, i) => {
4288
+ const p = vPos(t);
4289
+ return /* @__PURE__ */ jsx("line", { className: "twc-candlestick__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
4290
+ }) : null,
4291
+ showAxis ? scale.ticks.map((t, i) => /* @__PURE__ */ jsx("text", { className: "twc-candlestick__axis", x: padL - 8, y: vPos(t) + 4, textAnchor: "end", children: shortNum(t) }, i)) : null,
4292
+ !zoomable ? renderZones() : null,
4293
+ rows.map((d, i) => {
4294
+ const open = Number(d.open) || 0;
4295
+ const high = Number(d.high) || 0;
4296
+ const low = Number(d.low) || 0;
4297
+ const close = Number(d.close) || 0;
4298
+ const up = close >= open;
4299
+ const color = up ? upColor : downColor;
4300
+ const x = cx(i);
4301
+ const gi = baseIdx + i;
4302
+ const yTop = vPos(Math.max(open, close));
4303
+ const yBot = vPos(Math.min(open, close));
4304
+ const bh = Math.max(1, yBot - yTop);
4305
+ return /* @__PURE__ */ jsxs(
4306
+ "g",
4307
+ {
4308
+ className: "twc-chart__anim-fade",
4309
+ "data-dir": up ? "up" : "down",
4310
+ onMouseMove: (e) => show(tipFor(d), e),
4311
+ onMouseLeave: hide,
4312
+ onClick: () => {
4313
+ clickCandle(d, gi);
4314
+ selectMark(gi);
4315
+ },
4316
+ children: [
4317
+ /* @__PURE__ */ jsx(
4318
+ "line",
4319
+ {
4320
+ className: "twc-candlestick__wick",
4321
+ style: { stroke: color },
4322
+ x1: x,
4323
+ y1: vPos(high),
4324
+ x2: x,
4325
+ y2: vPos(low)
4326
+ }
4327
+ ),
4328
+ /* @__PURE__ */ jsx(
4329
+ "rect",
4330
+ {
4331
+ className: "twc-candlestick__body",
4332
+ style: { fill: color },
4333
+ "data-mark": true,
4334
+ "data-selected": selected === gi || void 0,
4335
+ x: x - bodyW / 2,
4336
+ y: yTop,
4337
+ width: bodyW,
4338
+ height: bh,
4339
+ rx: "1"
4340
+ }
4341
+ )
4342
+ ]
4343
+ },
4344
+ i
4345
+ );
4346
+ }),
4347
+ showAxis ? rows.map(
4348
+ (d, i) => i % labelStep === 0 ? /* @__PURE__ */ jsx("text", { className: "twc-candlestick__axis", x: cx(i), y: H - 8, textAnchor: "middle", children: d.label }, i) : null
4349
+ ) : null,
4350
+ dragBand ? (() => {
4351
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
4352
+ const x0 = padL + catBand * lo;
4353
+ const x1 = padL + catBand * (hi + 1);
4354
+ return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
4355
+ })() : null,
4356
+ zoomable ? /* @__PURE__ */ jsx(
4357
+ "rect",
4358
+ {
4359
+ className: "twc-chart__overlay",
4360
+ "data-clickable": clickable || void 0,
4361
+ x: padL,
4362
+ y: padT,
4363
+ width: innerW,
4364
+ height: innerH,
4365
+ onMouseMove: onOverlayMove,
4366
+ onMouseLeave: () => {
4367
+ hide();
4368
+ setDrag(null);
4369
+ },
4370
+ onMouseDown: onOverlayDown,
4371
+ onMouseUp: onOverlayUp,
4372
+ onClick: onOverlayClick
4373
+ }
4374
+ ) : null
4375
+ ] }),
4376
+ zoomable && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
4377
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
4378
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
4379
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
4380
+ ] }),
4381
+ "Reset"
4382
+ ] }) : null,
4383
+ /* @__PURE__ */ jsx(ChartTooltip, { tip }),
4384
+ tableFallback ? /* @__PURE__ */ jsx(
4385
+ ChartTable,
4386
+ {
4387
+ id: tableId,
4388
+ caption: caption ?? svgAriaLabel,
4389
+ columns: ["open", "high", "low", "close"],
4390
+ rows: allRows.map((d) => ({
4391
+ label: d.label,
4392
+ values: [Number(d.open) || 0, Number(d.high) || 0, Number(d.low) || 0, Number(d.close) || 0].map(fmt)
4393
+ }))
4394
+ }
4395
+ ) : null
4396
+ ]
4397
+ }
4398
+ );
3393
4399
  }
3394
4400
  function labelText5(label) {
3395
4401
  return typeof label === "string" || typeof label === "number" ? String(label) : "";
@@ -3409,7 +4415,9 @@ function Boxplot({
3409
4415
  height = 300,
3410
4416
  showGrid = true,
3411
4417
  showAxis = true,
4418
+ zoomable = false,
3412
4419
  valueFormat,
4420
+ onDataClick,
3413
4421
  ariaLabel,
3414
4422
  "aria-label": ariaLabelProp,
3415
4423
  tableFallback = true,
@@ -3422,9 +4430,28 @@ function Boxplot({
3422
4430
  const uid = React51__default.useId();
3423
4431
  const tableId = tableFallback ? `${uid}-table` : void 0;
3424
4432
  const { containerRef, tip, show, hide } = useChartTooltip();
3425
- const rows = data || [];
4433
+ const [selected, setSelected] = React51__default.useState(null);
4434
+ const [zoom, setZoom] = React51__default.useState(null);
4435
+ const [drag, setDrag] = React51__default.useState(null);
4436
+ const svgRef = React51__default.useRef(null);
4437
+ const allRows = data || [];
4438
+ const baseIdx = zoom ? zoom.s : 0;
4439
+ const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
3426
4440
  const fmt = valueFormat || fmtNumber;
3427
4441
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "box plot";
4442
+ const clickable = !!onDataClick;
4443
+ const clickBox = (d, i) => {
4444
+ if (onDataClick) onDataClick({
4445
+ label: d.label,
4446
+ min: Number(d.min),
4447
+ q1: Number(d.q1),
4448
+ median: Number(d.median),
4449
+ q3: Number(d.q3),
4450
+ max: Number(d.max),
4451
+ index: baseIdx + i
4452
+ });
4453
+ setSelected((s) => s === i ? null : i);
4454
+ };
3428
4455
  const tipFor = (d) => ({
3429
4456
  title: labelText6(d.label),
3430
4457
  items: [
@@ -3457,69 +4484,184 @@ function Boxplot({
3457
4484
  const band = innerW / Math.max(1, rows.length);
3458
4485
  const bandX = (i) => padL + band * i;
3459
4486
  const boxW = Math.min(band * 0.5, 46);
3460
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--boxplot ${className}`.trim(), ...rest, children: [
3461
- baseStyles,
3462
- styles,
3463
- /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
3464
- showGrid ? scale.ticks.map((t, i) => {
3465
- const p = vPos(t);
3466
- return /* @__PURE__ */ jsx("line", { className: "twc-boxplot__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
3467
- }) : null,
3468
- showAxis ? scale.ticks.map((t, i) => {
3469
- const p = vPos(t);
3470
- return /* @__PURE__ */ jsx("text", { className: "twc-boxplot__axis", x: padL - 8, y: p + 4, textAnchor: "end", children: shortNum(t) }, i);
3471
- }) : null,
3472
- rows.map((d, i) => {
3473
- const cx = bandX(i) + band / 2;
3474
- const x = cx - boxW / 2;
3475
- const yMin = vPos(d.min), yMax = vPos(d.max);
3476
- const yQ1 = vPos(d.q1), yQ3 = vPos(d.q3), yMed = vPos(d.median);
3477
- const boxTop = Math.min(yQ1, yQ3), boxBot = Math.max(yQ1, yQ3);
3478
- const cap = boxW / 3;
3479
- const hoverProps = {
3480
- onMouseMove: (e) => show(tipFor(d), e),
3481
- onMouseLeave: hide
3482
- };
3483
- return /* @__PURE__ */ jsxs("g", { className: "twc-chart__anim-fade", style: { color }, children: [
3484
- /* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx, y1: yMax, x2: cx, y2: boxTop }),
3485
- /* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx, y1: boxBot, x2: cx, y2: yMin }),
3486
- /* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx - cap, y1: yMax, x2: cx + cap, y2: yMax }),
3487
- /* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx - cap, y1: yMin, x2: cx + cap, y2: yMin }),
3488
- /* @__PURE__ */ jsx(
4487
+ const applyZoom = (s, e) => {
4488
+ const n = allRows.length;
4489
+ if (e - s >= n - 1) {
4490
+ setZoom(null);
4491
+ return;
4492
+ }
4493
+ setZoom({ s: Math.max(0, s), e: Math.min(n - 1, e) });
4494
+ };
4495
+ const catFromEvent = (e) => {
4496
+ const svg = svgRef.current;
4497
+ if (!svg) return null;
4498
+ const r2 = svg.getBoundingClientRect();
4499
+ if (!(r2.width > 0)) return null;
4500
+ const frac = Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW));
4501
+ const idx = Math.floor(frac * rows.length);
4502
+ return Math.min(rows.length - 1, Math.max(0, idx));
4503
+ };
4504
+ const onOverlayMove = (e) => {
4505
+ const idx = catFromEvent(e);
4506
+ if (idx == null) return;
4507
+ show(tipFor(rows[idx]), e);
4508
+ if (drag) {
4509
+ if (drag.pan) {
4510
+ const s0 = drag.z0.e - drag.z0.s;
4511
+ const s = Math.min(allRows.length - 1 - s0, Math.max(0, drag.z0.s + (drag.start - idx)));
4512
+ applyZoom(s, s + s0);
4513
+ } else {
4514
+ setDrag({ ...drag, end: idx });
4515
+ }
4516
+ }
4517
+ };
4518
+ const onOverlayDown = (e) => {
4519
+ const idx = catFromEvent(e);
4520
+ if (idx == null) return;
4521
+ setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
4522
+ };
4523
+ const onOverlayUp = () => {
4524
+ if (drag && !drag.pan) {
4525
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
4526
+ if (hi - lo >= 1) applyZoom(baseIdx + lo, baseIdx + hi);
4527
+ }
4528
+ setDrag(null);
4529
+ };
4530
+ const onOverlayClick = (e) => {
4531
+ if (drag && drag.end !== drag.start) return;
4532
+ const idx = catFromEvent(e);
4533
+ if (idx == null) return;
4534
+ clickBox(rows[idx], idx);
4535
+ };
4536
+ React51__default.useEffect(() => {
4537
+ if (!zoomable) return void 0;
4538
+ const svg = svgRef.current;
4539
+ if (!svg) return void 0;
4540
+ const onWheel = (e) => {
4541
+ e.preventDefault();
4542
+ const n = allRows.length;
4543
+ const cur = zoom || { s: 0, e: n - 1 };
4544
+ const wSpan = cur.e - cur.s;
4545
+ const r2 = svg.getBoundingClientRect();
4546
+ const frac = r2.width > 0 ? Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW)) : 0.5;
4547
+ const center = cur.s + Math.round(frac * wSpan);
4548
+ const newSpan = Math.min(n - 1, Math.max(1, Math.round(wSpan * (e.deltaY < 0 ? 0.8 : 1.25))));
4549
+ const s = Math.min(n - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frac)));
4550
+ applyZoom(s, s + newSpan);
4551
+ };
4552
+ svg.addEventListener("wheel", onWheel, { passive: false });
4553
+ return () => svg.removeEventListener("wheel", onWheel);
4554
+ }, [zoomable, zoom, allRows.length]);
4555
+ const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
4556
+ return /* @__PURE__ */ jsxs(
4557
+ "div",
4558
+ {
4559
+ ref: containerRef,
4560
+ className: `twc-chart twc-chart--boxplot ${className}`.trim(),
4561
+ "data-has-selection": selected != null || void 0,
4562
+ "data-clickable": clickable || void 0,
4563
+ ...rest,
4564
+ children: [
4565
+ baseStyles,
4566
+ styles,
4567
+ /* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
4568
+ showGrid ? scale.ticks.map((t, i) => {
4569
+ const p = vPos(t);
4570
+ return /* @__PURE__ */ jsx("line", { className: "twc-boxplot__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
4571
+ }) : null,
4572
+ showAxis ? scale.ticks.map((t, i) => {
4573
+ const p = vPos(t);
4574
+ return /* @__PURE__ */ jsx("text", { className: "twc-boxplot__axis", x: padL - 8, y: p + 4, textAnchor: "end", children: shortNum(t) }, i);
4575
+ }) : null,
4576
+ rows.map((d, i) => {
4577
+ const cx = bandX(i) + band / 2;
4578
+ const x = cx - boxW / 2;
4579
+ const yMin = vPos(d.min), yMax = vPos(d.max);
4580
+ const yQ1 = vPos(d.q1), yQ3 = vPos(d.q3), yMed = vPos(d.median);
4581
+ const boxTop = Math.min(yQ1, yQ3), boxBot = Math.max(yQ1, yQ3);
4582
+ const cap = boxW / 3;
4583
+ const hoverProps = {
4584
+ onMouseMove: (e) => show(tipFor(d), e),
4585
+ onMouseLeave: hide
4586
+ };
4587
+ return /* @__PURE__ */ jsxs("g", { className: "twc-chart__anim-fade", style: { color }, children: [
4588
+ /* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx, y1: yMax, x2: cx, y2: boxTop }),
4589
+ /* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx, y1: boxBot, x2: cx, y2: yMin }),
4590
+ /* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx - cap, y1: yMax, x2: cx + cap, y2: yMax }),
4591
+ /* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx - cap, y1: yMin, x2: cx + cap, y2: yMin }),
4592
+ /* @__PURE__ */ jsx(
4593
+ "rect",
4594
+ {
4595
+ className: "twc-boxplot__box",
4596
+ "data-mark": true,
4597
+ "data-selected": selected === i || void 0,
4598
+ style: selected === i ? { fill: color } : { fill: color, stroke: color },
4599
+ x,
4600
+ y: boxTop,
4601
+ width: boxW,
4602
+ height: Math.max(1, boxBot - boxTop),
4603
+ rx: "2",
4604
+ ...hoverProps,
4605
+ onClick: () => clickBox(d, i)
4606
+ }
4607
+ ),
4608
+ /* @__PURE__ */ jsx("line", { className: "twc-boxplot__median", style: { stroke: color }, x1: x, y1: yMed, x2: x + boxW, y2: yMed }),
4609
+ (Array.isArray(d.outliers) ? d.outliers : []).map(
4610
+ (o, oi) => isFinite(Number(o)) ? /* @__PURE__ */ jsx("circle", { className: "twc-boxplot__outlier", style: { stroke: color }, cx, cy: vPos(o), r: "2.5", ...hoverProps }, oi) : null
4611
+ )
4612
+ ] }, i);
4613
+ }),
4614
+ showAxis ? rows.map((d, i) => /* @__PURE__ */ jsx("text", { className: "twc-boxplot__axis", x: bandX(i) + band / 2, y: H - 8, textAnchor: "middle", children: d.label }, i)) : null,
4615
+ dragBand ? (() => {
4616
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
4617
+ const x0 = bandX(lo), x1 = bandX(hi + 1);
4618
+ return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
4619
+ })() : null,
4620
+ zoomable ? /* @__PURE__ */ jsx(
3489
4621
  "rect",
3490
4622
  {
3491
- className: "twc-boxplot__box",
3492
- style: { fill: color, stroke: color },
3493
- x,
3494
- y: boxTop,
3495
- width: boxW,
3496
- height: Math.max(1, boxBot - boxTop),
3497
- rx: "2",
3498
- ...hoverProps
4623
+ className: "twc-chart__overlay",
4624
+ "data-zoom": "true",
4625
+ "data-clickable": clickable || void 0,
4626
+ "data-panning": drag && drag.pan || void 0,
4627
+ x: padL,
4628
+ y: padT,
4629
+ width: innerW,
4630
+ height: innerH,
4631
+ onMouseMove: onOverlayMove,
4632
+ onMouseLeave: () => {
4633
+ hide();
4634
+ setDrag(null);
4635
+ },
4636
+ onMouseDown: onOverlayDown,
4637
+ onMouseUp: onOverlayUp,
4638
+ onClick: onOverlayClick
3499
4639
  }
3500
- ),
3501
- /* @__PURE__ */ jsx("line", { className: "twc-boxplot__median", style: { stroke: color }, x1: x, y1: yMed, x2: x + boxW, y2: yMed }),
3502
- (Array.isArray(d.outliers) ? d.outliers : []).map(
3503
- (o, oi) => isFinite(Number(o)) ? /* @__PURE__ */ jsx("circle", { className: "twc-boxplot__outlier", style: { stroke: color }, cx, cy: vPos(o), r: "2.5", ...hoverProps }, oi) : null
3504
- )
3505
- ] }, i);
3506
- }),
3507
- showAxis ? rows.map((d, i) => /* @__PURE__ */ jsx("text", { className: "twc-boxplot__axis", x: bandX(i) + band / 2, y: H - 8, textAnchor: "middle", children: d.label }, i)) : null
3508
- ] }),
3509
- /* @__PURE__ */ jsx(ChartTooltip, { tip }),
3510
- tableFallback ? /* @__PURE__ */ jsx(
3511
- ChartTable,
3512
- {
3513
- id: tableId,
3514
- caption: caption ?? svgAriaLabel,
3515
- columns: ["min", "q1", "median", "q3", "max"],
3516
- rows: rows.map((d) => ({
3517
- label: d.label,
3518
- values: [d.min, d.q1, d.median, d.q3, d.max].map((v) => fmt(Number(v)))
3519
- }))
3520
- }
3521
- ) : null
3522
- ] });
4640
+ ) : null
4641
+ ] }),
4642
+ zoomable && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
4643
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
4644
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
4645
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
4646
+ ] }),
4647
+ "Reset"
4648
+ ] }) : null,
4649
+ /* @__PURE__ */ jsx(ChartTooltip, { tip }),
4650
+ tableFallback ? /* @__PURE__ */ jsx(
4651
+ ChartTable,
4652
+ {
4653
+ id: tableId,
4654
+ caption: caption ?? svgAriaLabel,
4655
+ columns: ["min", "q1", "median", "q3", "max"],
4656
+ rows: allRows.map((d) => ({
4657
+ label: d.label,
4658
+ values: [d.min, d.q1, d.median, d.q3, d.max].map((v) => fmt(Number(v)))
4659
+ }))
4660
+ }
4661
+ ) : null
4662
+ ]
4663
+ }
4664
+ );
3523
4665
  }
3524
4666
  function labelText6(label) {
3525
4667
  return typeof label === "string" || typeof label === "number" ? String(label) : "";
@@ -3539,8 +4681,10 @@ function RangeChart({
3539
4681
  height = 300,
3540
4682
  showGrid = true,
3541
4683
  showAxis = true,
4684
+ zoomable = false,
3542
4685
  colors,
3543
4686
  valueFormat,
4687
+ onDataClick,
3544
4688
  ariaLabel,
3545
4689
  "aria-label": ariaLabelProp,
3546
4690
  tableFallback = true,
@@ -3553,10 +4697,17 @@ function RangeChart({
3553
4697
  const uid = React51__default.useId();
3554
4698
  const tableId = tableFallback ? `${uid}-table` : void 0;
3555
4699
  const { containerRef, tip, show, hide } = useChartTooltip();
4700
+ const [selected, setSelected] = React51__default.useState(null);
4701
+ const [zoom, setZoom] = React51__default.useState(null);
4702
+ const [drag, setDrag] = React51__default.useState(null);
4703
+ const svgRef = React51__default.useRef(null);
3556
4704
  const isArea = type === "area";
3557
- const rows = data || [];
3558
- const n = rows.length;
4705
+ const clickable = !!onDataClick;
3559
4706
  const fmt = valueFormat || fmtNumber;
4707
+ const allRows = data || [];
4708
+ const baseIdx = zoom ? zoom.s : 0;
4709
+ const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
4710
+ const n = rows.length;
3560
4711
  const W = 600, H = height;
3561
4712
  const padL = showAxis ? isArea ? 44 : 84 : 8;
3562
4713
  const padR = 12;
@@ -3572,6 +4723,97 @@ function RangeChart({
3572
4723
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? `range ${type} chart`;
3573
4724
  const xPos = (v) => padL + frac(v) * innerW;
3574
4725
  const catBandH = innerH / Math.max(1, n);
4726
+ const lineX = (i) => n > 1 ? padL + innerW / (n - 1) * i : padL + innerW / 2;
4727
+ const yPos = (v) => padT + innerH - frac(v) * innerH;
4728
+ const colorAt = (d, i) => isArea ? paletteAt(colors, 0) : d.color || paletteAt(colors, i);
4729
+ const rangeVal = (d) => `${fmt(Number(d.min) || 0)} \u2013 ${fmt(Number(d.max) || 0)}`;
4730
+ const tipFor = (d, i) => ({ title: labelText7(d.label), items: [{ color: colorAt(d, i), label: "", value: rangeVal(d) }] });
4731
+ const clickDatum = (d, localIdx) => {
4732
+ if (!onDataClick) return;
4733
+ onDataClick({ label: d.label, min: Number(d.min) || 0, max: Number(d.max) || 0, index: baseIdx + localIdx });
4734
+ };
4735
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
4736
+ const clickAt = (localIdx) => {
4737
+ clickDatum(rows[localIdx], localIdx);
4738
+ selectMark(baseIdx + localIdx);
4739
+ };
4740
+ const applyZoom = (s, e) => {
4741
+ const nAll = allRows.length;
4742
+ if (e - s >= nAll - 1) {
4743
+ setZoom(null);
4744
+ return;
4745
+ }
4746
+ setZoom({ s: Math.max(0, s), e: Math.min(nAll - 1, e) });
4747
+ };
4748
+ const fracFromEvent = (e) => {
4749
+ const svg = svgRef.current;
4750
+ if (!svg) return null;
4751
+ const rect = svg.getBoundingClientRect();
4752
+ if (isArea) {
4753
+ if (!(rect.width > 0)) return null;
4754
+ return Math.min(1, Math.max(0, ((e.clientX - rect.left) / rect.width * W - padL) / innerW));
4755
+ }
4756
+ if (!(rect.height > 0)) return null;
4757
+ return Math.min(1, Math.max(0, ((e.clientY - rect.top) / rect.height * H - padT) / innerH));
4758
+ };
4759
+ const catFromEvent = (e) => {
4760
+ if (!rows.length) return null;
4761
+ const f = fracFromEvent(e);
4762
+ if (f == null) return null;
4763
+ const idx = isArea ? Math.round(f * Math.max(1, rows.length - 1)) : Math.floor(f * rows.length);
4764
+ return Math.min(rows.length - 1, Math.max(0, idx));
4765
+ };
4766
+ const onOverlayMove = (e) => {
4767
+ const idx = catFromEvent(e);
4768
+ if (idx == null) return;
4769
+ show(tipFor(rows[idx], idx), e);
4770
+ if (drag) {
4771
+ if (drag.pan) {
4772
+ const spanD = drag.z0.e - drag.z0.s;
4773
+ const s = Math.min(allRows.length - 1 - spanD, Math.max(0, drag.z0.s + (drag.start - idx)));
4774
+ applyZoom(s, s + spanD);
4775
+ } else {
4776
+ setDrag({ ...drag, end: idx });
4777
+ }
4778
+ }
4779
+ };
4780
+ const onOverlayDown = (e) => {
4781
+ const idx = catFromEvent(e);
4782
+ if (idx == null) return;
4783
+ setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
4784
+ };
4785
+ const onOverlayUp = () => {
4786
+ if (drag && !drag.pan) {
4787
+ const loI = Math.min(drag.start, drag.end), hiI = Math.max(drag.start, drag.end);
4788
+ if (hiI - loI >= 1) applyZoom(baseIdx + loI, baseIdx + hiI);
4789
+ }
4790
+ setDrag(null);
4791
+ };
4792
+ const onOverlayClick = (e) => {
4793
+ if (!onDataClick) return;
4794
+ const idx = catFromEvent(e);
4795
+ if (idx == null) return;
4796
+ clickAt(idx);
4797
+ };
4798
+ React51__default.useEffect(() => {
4799
+ if (!zoomable) return void 0;
4800
+ const svg = svgRef.current;
4801
+ if (!svg) return void 0;
4802
+ const onWheel = (e) => {
4803
+ e.preventDefault();
4804
+ const nAll = allRows.length;
4805
+ const cur = zoom || { s: 0, e: nAll - 1 };
4806
+ const spanC = cur.e - cur.s;
4807
+ const f = fracFromEvent(e);
4808
+ const frc = f == null ? 0.5 : f;
4809
+ const center = cur.s + Math.round(frc * spanC);
4810
+ const newSpan = Math.min(nAll - 1, Math.max(1, Math.round(spanC * (e.deltaY < 0 ? 0.8 : 1.25))));
4811
+ const s = Math.min(nAll - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frc)));
4812
+ applyZoom(s, s + newSpan);
4813
+ };
4814
+ svg.addEventListener("wheel", onWheel, { passive: false });
4815
+ return () => svg.removeEventListener("wheel", onWheel);
4816
+ }, [zoomable, zoom, allRows.length]);
3575
4817
  const renderBars = () => {
3576
4818
  const gap = 0.34;
3577
4819
  const slot = catBandH * (1 - gap);
@@ -3580,27 +4822,29 @@ function RangeChart({
3580
4822
  const x0 = xPos(Math.min(min, max));
3581
4823
  const x1 = xPos(Math.max(min, max));
3582
4824
  const y = padT + catBandH * i + catBandH * gap / 2;
3583
- const color = d.color || paletteAt(colors, i);
4825
+ const color = colorAt(d, i);
4826
+ const key = baseIdx + i;
3584
4827
  return /* @__PURE__ */ jsx(
3585
4828
  "rect",
3586
4829
  {
3587
4830
  className: "twc-rangechart__bar twc-chart__anim-bar",
4831
+ "data-mark": true,
3588
4832
  "data-horizontal": "true",
4833
+ "data-selected": selected === key || void 0,
3589
4834
  style: { fill: color },
3590
4835
  x: x0,
3591
4836
  y,
3592
4837
  width: Math.max(1, x1 - x0),
3593
4838
  height: Math.max(1, slot),
3594
4839
  rx: "3",
3595
- onMouseMove: (e) => show({ title: labelText7(d.label), items: [{ color, label: "", value: `${fmt(min)} \u2013 ${fmt(max)}` }] }, e),
3596
- onMouseLeave: hide
4840
+ onMouseMove: (e) => show(tipFor(d, i), e),
4841
+ onMouseLeave: hide,
4842
+ onClick: () => clickAt(i)
3597
4843
  },
3598
4844
  i
3599
4845
  );
3600
4846
  });
3601
4847
  };
3602
- const lineX = (i) => n > 1 ? padL + innerW / (n - 1) * i : padL + innerW / 2;
3603
- const yPos = (v) => padT + innerH - frac(v) * innerH;
3604
4848
  const renderBand = () => {
3605
4849
  const maxPts = rows.map((d, i) => [lineX(i), yPos(Number(d.max) || 0)]);
3606
4850
  const minPts = rows.map((d, i) => [lineX(i), yPos(Number(d.min) || 0)]);
@@ -3615,44 +4859,126 @@ function RangeChart({
3615
4859
  rows.map((d, i) => /* @__PURE__ */ jsx(
3616
4860
  "rect",
3617
4861
  {
4862
+ "data-mark": true,
4863
+ "data-selected": selected === baseIdx + i || void 0,
3618
4864
  fill: "transparent",
3619
4865
  x: lineX(i) - hitW / 2,
3620
4866
  y: padT,
3621
4867
  width: hitW,
3622
4868
  height: innerH,
3623
- onMouseMove: (e) => show({ title: labelText7(d.label), items: [{ color, label: "", value: `${fmt(Number(d.min) || 0)} \u2013 ${fmt(Number(d.max) || 0)}` }] }, e),
3624
- onMouseLeave: hide
4869
+ onMouseMove: (e) => show(tipFor(d, i), e),
4870
+ onMouseLeave: hide,
4871
+ onClick: () => clickAt(i)
3625
4872
  },
3626
4873
  i
3627
4874
  ))
3628
4875
  ] });
3629
4876
  };
3630
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--range ${className}`.trim(), "data-type": type, ...rest, children: [
3631
- baseStyles,
3632
- styles,
3633
- /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
3634
- showGrid ? scale.ticks.map(
3635
- (t, i) => isArea ? /* @__PURE__ */ jsx("line", { className: "twc-rangechart__grid", x1: padL, y1: yPos(t), x2: W - padR, y2: yPos(t) }, i) : /* @__PURE__ */ jsx("line", { className: "twc-rangechart__grid", x1: xPos(t), y1: padT, x2: xPos(t), y2: padT + innerH }, i)
3636
- ) : null,
3637
- showAxis ? scale.ticks.map(
3638
- (t, i) => isArea ? /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: padL - 8, y: yPos(t) + 4, textAnchor: "end", children: shortNum(t) }, i) : /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: xPos(t), y: H - 8, textAnchor: "middle", children: shortNum(t) }, i)
3639
- ) : null,
3640
- isArea ? renderBand() : renderBars(),
3641
- showAxis ? rows.map(
3642
- (d, i) => isArea ? /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: lineX(i), y: H - 8, textAnchor: "middle", children: d.label }, i) : /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: padL - 8, y: padT + catBandH * i + catBandH / 2 + 4, textAnchor: "end", children: d.label }, i)
3643
- ) : null
3644
- ] }),
3645
- /* @__PURE__ */ jsx(ChartTooltip, { tip }),
3646
- tableFallback ? /* @__PURE__ */ jsx(
3647
- ChartTable,
4877
+ const renderZones = () => rows.map((d, i) => {
4878
+ let x, y, w, hgt;
4879
+ if (isArea) {
4880
+ const x0 = i === 0 ? padL : (lineX(i - 1) + lineX(i)) / 2;
4881
+ const x1 = i === rows.length - 1 ? padL + innerW : (lineX(i) + lineX(i + 1)) / 2;
4882
+ x = x0;
4883
+ w = x1 - x0;
4884
+ y = padT;
4885
+ hgt = innerH;
4886
+ } else {
4887
+ x = padL;
4888
+ w = innerW;
4889
+ y = padT + catBandH * i;
4890
+ hgt = catBandH;
4891
+ }
4892
+ return /* @__PURE__ */ jsx(
4893
+ "rect",
3648
4894
  {
3649
- id: tableId,
3650
- caption: caption ?? svgAriaLabel,
3651
- columns: ["min", "max"],
3652
- rows: rows.map((d) => ({ label: d.label, values: [fmt(Number(d.min) || 0), fmt(Number(d.max) || 0)] }))
3653
- }
3654
- ) : null
3655
- ] });
4895
+ className: "twc-chart__zone",
4896
+ x,
4897
+ y,
4898
+ width: Math.max(0, w),
4899
+ height: Math.max(0, hgt),
4900
+ "data-clickable": onDataClick ? true : void 0,
4901
+ onMouseMove: (e) => show(tipFor(d, i), e),
4902
+ onMouseLeave: hide,
4903
+ onClick: () => clickDatum(d, i)
4904
+ },
4905
+ i
4906
+ );
4907
+ });
4908
+ const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
4909
+ return /* @__PURE__ */ jsxs(
4910
+ "div",
4911
+ {
4912
+ ref: containerRef,
4913
+ className: `twc-chart twc-chart--range ${className}`.trim(),
4914
+ "data-type": type,
4915
+ "data-clickable": clickable || void 0,
4916
+ "data-has-selection": selected != null || void 0,
4917
+ ...rest,
4918
+ children: [
4919
+ baseStyles,
4920
+ styles,
4921
+ /* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
4922
+ showGrid ? scale.ticks.map(
4923
+ (t, i) => isArea ? /* @__PURE__ */ jsx("line", { className: "twc-rangechart__grid", x1: padL, y1: yPos(t), x2: W - padR, y2: yPos(t) }, i) : /* @__PURE__ */ jsx("line", { className: "twc-rangechart__grid", x1: xPos(t), y1: padT, x2: xPos(t), y2: padT + innerH }, i)
4924
+ ) : null,
4925
+ showAxis ? scale.ticks.map(
4926
+ (t, i) => isArea ? /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: padL - 8, y: yPos(t) + 4, textAnchor: "end", children: shortNum(t) }, i) : /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: xPos(t), y: H - 8, textAnchor: "middle", children: shortNum(t) }, i)
4927
+ ) : null,
4928
+ !zoomable ? renderZones() : null,
4929
+ isArea ? renderBand() : renderBars(),
4930
+ showAxis ? rows.map(
4931
+ (d, i) => isArea ? /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: lineX(i), y: H - 8, textAnchor: "middle", children: d.label }, i) : /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: padL - 8, y: padT + catBandH * i + catBandH / 2 + 4, textAnchor: "end", children: d.label }, i)
4932
+ ) : null,
4933
+ dragBand ? (() => {
4934
+ const loI = Math.min(drag.start, drag.end), hiI = Math.max(drag.start, drag.end);
4935
+ if (isArea) {
4936
+ const x0 = lineX(loI), x1 = lineX(hiI);
4937
+ return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
4938
+ }
4939
+ const y0 = padT + catBandH * loI, y1 = padT + catBandH * (hiI + 1);
4940
+ return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: padL, y: y0, width: innerW, height: Math.max(0, y1 - y0) });
4941
+ })() : null,
4942
+ zoomable ? /* @__PURE__ */ jsx(
4943
+ "rect",
4944
+ {
4945
+ className: "twc-chart__overlay",
4946
+ "data-clickable": clickable || void 0,
4947
+ x: padL,
4948
+ y: padT,
4949
+ width: innerW,
4950
+ height: innerH,
4951
+ onMouseMove: onOverlayMove,
4952
+ onMouseLeave: () => {
4953
+ hide();
4954
+ setDrag(null);
4955
+ },
4956
+ onMouseDown: onOverlayDown,
4957
+ onMouseUp: onOverlayUp,
4958
+ onClick: onOverlayClick
4959
+ }
4960
+ ) : null
4961
+ ] }),
4962
+ zoomable && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
4963
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
4964
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
4965
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
4966
+ ] }),
4967
+ "Reset"
4968
+ ] }) : null,
4969
+ /* @__PURE__ */ jsx(ChartTooltip, { tip }),
4970
+ tableFallback ? /* @__PURE__ */ jsx(
4971
+ ChartTable,
4972
+ {
4973
+ id: tableId,
4974
+ caption: caption ?? svgAriaLabel,
4975
+ columns: ["min", "max"],
4976
+ rows: allRows.map((d) => ({ label: d.label, values: [fmt(Number(d.min) || 0), fmt(Number(d.max) || 0)] }))
4977
+ }
4978
+ ) : null
4979
+ ]
4980
+ }
4981
+ );
3656
4982
  }
3657
4983
  function labelText7(label) {
3658
4984
  return typeof label === "string" || typeof label === "number" ? String(label) : "";