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