twico-ui 1.9.0 → 1.11.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
@@ -46,7 +46,7 @@ function useScopedStyles(id, css) {
46
46
  }
47
47
  if (el.textContent !== css) el.textContent = css;
48
48
  }, [id, css, nonce]);
49
- return SUPPORTS_STYLE_HOIST && css ? React51__namespace.createElement("style", { href: id, precedence: "twc-ui", nonce }, css) : null;
49
+ return SUPPORTS_STYLE_HOIST && css ? React51__namespace.createElement("style", { href: id, precedence: "twc-ui" }, css) : null;
50
50
  }
51
51
 
52
52
  // components/TwicoProvider.jsx
@@ -1308,6 +1308,62 @@ function compileSx(obj, selector) {
1308
1308
  out += rules;
1309
1309
  return out;
1310
1310
  }
1311
+ var BOX_EDGE4 = {
1312
+ padding: ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"],
1313
+ margin: ["marginTop", "marginRight", "marginBottom", "marginLeft"],
1314
+ inset: ["top", "right", "bottom", "left"]
1315
+ };
1316
+ var BOX_EDGE2 = {
1317
+ paddingBlock: ["paddingBlockStart", "paddingBlockEnd"],
1318
+ paddingInline: ["paddingInlineStart", "paddingInlineEnd"],
1319
+ marginBlock: ["marginBlockStart", "marginBlockEnd"],
1320
+ marginInline: ["marginInlineStart", "marginInlineEnd"]
1321
+ };
1322
+ function splitShorthand(value) {
1323
+ const s = String(value).trim();
1324
+ if (!s) return [];
1325
+ const out = [];
1326
+ let depth = 0, cur = "";
1327
+ for (let i = 0; i < s.length; i++) {
1328
+ const c = s[i];
1329
+ if (c === "(") {
1330
+ depth++;
1331
+ cur += c;
1332
+ } else if (c === ")") {
1333
+ depth = Math.max(0, depth - 1);
1334
+ cur += c;
1335
+ } else if (depth === 0 && (c === " " || c === " " || c === "\n" || c === "\r")) {
1336
+ if (cur) {
1337
+ out.push(cur);
1338
+ cur = "";
1339
+ }
1340
+ } else cur += c;
1341
+ }
1342
+ if (cur) out.push(cur);
1343
+ return out;
1344
+ }
1345
+ function expandBoxShorthand(key, value) {
1346
+ const four = BOX_EDGE4[key];
1347
+ const two = BOX_EDGE2[key];
1348
+ if (!four && !two || value == null) return null;
1349
+ if (typeof value === "number") return (four || two).map((edge) => [edge, value]);
1350
+ const p = splitShorthand(value);
1351
+ if (p.length === 0) return null;
1352
+ if (two) return [[two[0], p[0]], [two[1], p.length > 1 ? p[1] : p[0]]];
1353
+ let top, right, bottom, left;
1354
+ if (p.length === 1) top = right = bottom = left = p[0];
1355
+ else if (p.length === 2) {
1356
+ top = bottom = p[0];
1357
+ right = left = p[1];
1358
+ } else if (p.length === 3) {
1359
+ top = p[0];
1360
+ right = left = p[1];
1361
+ bottom = p[2];
1362
+ } else {
1363
+ [top, right, bottom, left] = p;
1364
+ }
1365
+ return [[four[0], top], [four[1], right], [four[2], bottom], [four[3], left]];
1366
+ }
1311
1367
  function buildSx(sx, uid) {
1312
1368
  if (!isObject(sx)) return { flatStyle: void 0, css: "" };
1313
1369
  const scope = `[data-twc-sx="${uid}"]`;
@@ -1325,7 +1381,13 @@ function buildSx(sx, uid) {
1325
1381
  css += compileSx(value, resolveSelector(key, scope));
1326
1382
  }
1327
1383
  } else {
1328
- (flatStyle || (flatStyle = {}))[key] = value;
1384
+ const expanded = expandBoxShorthand(key, value);
1385
+ if (expanded) {
1386
+ flatStyle || (flatStyle = {});
1387
+ for (const [k, v] of expanded) flatStyle[k] = v;
1388
+ } else {
1389
+ (flatStyle || (flatStyle = {}))[key] = value;
1390
+ }
1329
1391
  }
1330
1392
  }
1331
1393
  return { flatStyle, css };
@@ -1675,14 +1737,15 @@ var CHART_BASE_CSS = `
1675
1737
  .twc-chart__leg[data-off="true"] { opacity: 0.4; }
1676
1738
  .twc-chart__leg-sw { width: 10px; height: 10px; border-radius: 3px; flex: none; }
1677
1739
 
1678
- /* Floating tooltip \u2014 a styled card that follows the pointer (ApexCharts / MUI X style). */
1740
+ /* Floating tooltip \u2014 a styled card that follows the pointer (ApexCharts / MUI X style).
1741
+ Positioned inline by <ChartTooltip> (which flips below / clamps to the container so it
1742
+ never clips); only the opacity fades in here so it can't fight the inline transform. */
1679
1743
  .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;
1744
+ position: absolute; z-index: 3; top: 0; left: 0; pointer-events: none; will-change: transform;
1745
+ min-width: 88px; max-width: 240px; padding: 7px 9px;
1683
1746
  background: var(--color-surface-raised); color: var(--color-text);
1684
1747
  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;
1748
+ box-shadow: var(--shadow-lg); font-size: var(--text-xs); line-height: 1.4;
1686
1749
  opacity: 0; animation: twc-chart-tip-in var(--duration-fast) var(--ease-standard) forwards;
1687
1750
  }
1688
1751
  .twc-chart__tip-title { font-weight: var(--font-bold); color: var(--color-text); margin-bottom: 4px; white-space: nowrap; }
@@ -1691,12 +1754,36 @@ var CHART_BASE_CSS = `
1691
1754
  .twc-chart__tip-sw { width: 9px; height: 9px; border-radius: 2px; flex: none; }
1692
1755
  .twc-chart__tip-label { color: var(--color-text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; }
1693
1756
  .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)); } }
1757
+ @keyframes twc-chart-tip-in { from { opacity: 0; } to { opacity: 1; } }
1695
1758
 
1696
1759
  /* 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); }
1760
+ .twc-chart[data-hovering="true"] [data-mark] { opacity: 0.3; transition: opacity var(--duration-base) var(--ease-standard); }
1698
1761
  .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; }
1762
+ [data-mark] { transition: opacity var(--duration-base) var(--ease-standard), transform var(--duration-base) var(--ease-spring); cursor: default; }
1763
+
1764
+ /* Click-to-select: a soft accent ring + glow on the chosen mark (not a heavy outline). */
1765
+ [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)); }
1766
+ /* Click-to-focus: a persistent selection also dims the other marks (focus mode), on every chart. */
1767
+ .twc-chart[data-has-selection="true"] [data-mark]:not([data-selected="true"]) { opacity: 0.4; }
1768
+ .twc-chart[data-clickable="true"] [data-mark], .twc-chart__overlay[data-clickable="true"] { cursor: pointer; }
1769
+
1770
+ /* Crosshair + full-plot event overlay (cartesian shared-axis interactions). */
1771
+ .twc-chart__overlay, .twc-chart__zone { fill: transparent; }
1772
+ .twc-chart__crosshair { stroke: var(--color-border-strong); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.7; pointer-events: none; }
1773
+
1774
+ /* Drag-to-zoom affordances: a crosshair cursor over the zoom overlay + a bordered band. */
1775
+ .twc-chart__overlay[data-zoom="true"] { cursor: crosshair; }
1776
+ .twc-chart__overlay[data-zoom="true"][data-panning="true"] { cursor: grabbing; }
1777
+ .twc-chart__zoomband { fill: var(--color-primary); opacity: 0.14; stroke: var(--color-primary); stroke-opacity: 0.5; stroke-width: 1; pointer-events: none; }
1778
+ .twc-chart__zoom-reset {
1779
+ position: absolute; top: 6px; inset-inline-end: 8px; z-index: 2;
1780
+ display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px;
1781
+ font-family: var(--font-sans); font-size: var(--text-xs); color: var(--color-text-muted);
1782
+ background: var(--color-surface-raised); border: var(--border-thin) solid var(--color-border);
1783
+ border-radius: var(--radius-md); box-shadow: var(--shadow-sm); cursor: pointer;
1784
+ }
1785
+ .twc-chart__zoom-reset:hover { color: var(--color-text); border-color: var(--color-border-strong); }
1786
+ .twc-chart__zoom-reset svg { width: 12px; height: 12px; }
1700
1787
 
1701
1788
  /* Entrance animations (respect reduced motion, below). */
1702
1789
  @keyframes twc-chart-fade { from { opacity: 0; } to { opacity: 1; } }
@@ -1744,7 +1831,7 @@ function ChartTable({ id, caption, columns, rows }) {
1744
1831
  )
1745
1832
  );
1746
1833
  }
1747
- function ChartLegend({ items, onToggle, hidden }) {
1834
+ function ChartLegend({ items, onToggle, hidden, onFocus, onBlur }) {
1748
1835
  const h = React51__namespace.default.createElement;
1749
1836
  return h(
1750
1837
  "div",
@@ -1765,7 +1852,9 @@ function ChartLegend({ items, onToggle, hidden }) {
1765
1852
  e.preventDefault();
1766
1853
  onToggle(it, i);
1767
1854
  }
1768
- } : void 0
1855
+ } : void 0,
1856
+ onMouseEnter: onFocus ? () => onFocus(it, i) : void 0,
1857
+ onMouseLeave: onBlur ? () => onBlur() : void 0
1769
1858
  },
1770
1859
  h("span", { className: "twc-chart__leg-sw", style: { background: it.color } }),
1771
1860
  it.label
@@ -1788,11 +1877,31 @@ function useChartTooltip() {
1788
1877
  }
1789
1878
  function ChartTooltip({ tip }) {
1790
1879
  const h = React51__namespace.default.createElement;
1880
+ const ref = React51__namespace.default.useRef(null);
1881
+ const [pos, setPos] = React51__namespace.default.useState({ dx: 0, below: false });
1882
+ React51__namespace.default.useLayoutEffect(() => {
1883
+ const el = ref.current;
1884
+ if (!tip || !el) return;
1885
+ const parent = el.offsetParent;
1886
+ const pw = parent && parent.clientWidth ? parent.clientWidth : 0;
1887
+ const w = el.offsetWidth || 0, ht = el.offsetHeight || 0;
1888
+ let dx = 0;
1889
+ if (pw > 0 && w > 0) {
1890
+ const half = w / 2;
1891
+ if (tip.left - half < 6) dx = 6 - (tip.left - half);
1892
+ else if (tip.left + half > pw - 6) dx = pw - 6 - (tip.left + half);
1893
+ }
1894
+ setPos({ dx, below: ht > 0 && tip.top - ht - 16 < 0 });
1895
+ }, [tip]);
1791
1896
  if (!tip) return null;
1792
- const left = Math.max(48, tip.left);
1897
+ const style = {
1898
+ left: tip.left + pos.dx,
1899
+ top: tip.top,
1900
+ transform: pos.below ? "translate(-50%, 18px)" : "translate(-50%, calc(-100% - 14px))"
1901
+ };
1793
1902
  return h(
1794
1903
  "div",
1795
- { className: "twc-chart__tip", role: "tooltip", "aria-hidden": "true", style: { left, top: tip.top } },
1904
+ { ref, className: "twc-chart__tip", role: "tooltip", "aria-hidden": "true", style },
1796
1905
  tip.title != null && tip.title !== "" ? h("div", { className: "twc-chart__tip-title" }, tip.title) : null,
1797
1906
  (tip.items || []).map(
1798
1907
  (it, i) => h(
@@ -1808,7 +1917,6 @@ function ChartTooltip({ tip }) {
1808
1917
  var CHART_CSS = `
1809
1918
  .twc-chart__grid { stroke: var(--color-divider); stroke-width: 1; }
1810
1919
  .twc-chart__axisline { stroke: var(--color-border); stroke-width: 1; }
1811
- .twc-chart__tick { stroke: var(--color-border); stroke-width: 1; }
1812
1920
  .twc-chart__axis { fill: var(--color-text-subtle); font-size: 11px; }
1813
1921
  .twc-chart__bar { fill: var(--color-primary); }
1814
1922
  .twc-chart__bar:hover { filter: brightness(1.06); }
@@ -1817,7 +1925,6 @@ var CHART_CSS = `
1817
1925
  .twc-chart__dot { fill: var(--color-surface); stroke: var(--color-primary); stroke-width: 2.5; transition: r var(--duration-fast) var(--ease-standard); }
1818
1926
  .twc-chart__hit { fill: transparent; }
1819
1927
  .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
1928
  .twc-chart__label { fill: var(--color-text-muted); font-size: 10px; font-weight: var(--font-semibold); }
1822
1929
  `;
1823
1930
  function Chart({
@@ -1834,9 +1941,12 @@ function Chart({
1834
1941
  showGrid = true,
1835
1942
  showAxis = true,
1836
1943
  showLegend = false,
1944
+ crosshair = true,
1945
+ zoomable = false,
1837
1946
  animate = true,
1838
1947
  colors,
1839
1948
  valueFormat,
1949
+ onDataClick,
1840
1950
  ariaLabel,
1841
1951
  "aria-label": ariaLabelProp,
1842
1952
  tableFallback = true,
@@ -1851,15 +1961,23 @@ function Chart({
1851
1961
  const tableId = tableFallback ? `${uid}-table` : void 0;
1852
1962
  const { containerRef, tip, show, hide } = useChartTooltip();
1853
1963
  const [hidden, setHidden] = React51__namespace.default.useState(() => /* @__PURE__ */ new Set());
1964
+ const [focusSeries, setFocusSeries] = React51__namespace.default.useState(null);
1965
+ const [selected, setSelected] = React51__namespace.default.useState(null);
1966
+ const [hoverIdx, setHoverIdx] = React51__namespace.default.useState(null);
1967
+ const [zoom, setZoom] = React51__namespace.default.useState(null);
1968
+ const [drag, setDrag] = React51__namespace.default.useState(null);
1969
+ const svgRef = React51__namespace.default.useRef(null);
1854
1970
  const isBar = type === "bar" || type === "column";
1855
1971
  const isArea = type === "area";
1856
1972
  const crv = smooth ? "smooth" : curve;
1857
1973
  const pathOpts = { smooth: crv === "smooth", step: crv === "stepped" ? "after" : null };
1858
1974
  const canStack = stacked && (isBar || isArea);
1859
- const anim = animate ? "" : null;
1975
+ const clickable = !!onDataClick;
1860
1976
  const allKeys = series && series.length ? series : ["value"];
1861
1977
  const keys = allKeys.filter((k) => !hidden.has(k));
1862
- const rows = data || [];
1978
+ const allRows = data || [];
1979
+ const baseIdx = zoom ? zoom.s : 0;
1980
+ const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
1863
1981
  const fmt = valueFormat || fmtNumber;
1864
1982
  const multi = allKeys.length > 1;
1865
1983
  const W = 600, H = height;
@@ -1877,43 +1995,137 @@ function Chart({
1877
1995
  const catBand = innerW / Math.max(1, rows.length);
1878
1996
  const catBandH = innerH / Math.max(1, rows.length);
1879
1997
  const lineX = (i) => padL + innerW / Math.max(1, rows.length - 1) * i;
1998
+ const catX = (i) => isBar ? padL + catBand * i + catBand / 2 : lineX(i);
1880
1999
  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
2000
  function colorFor(k) {
1886
2001
  const idx = allKeys.indexOf(k);
1887
2002
  return multi || colors && colors.length ? paletteAt(colors, idx) : "var(--color-primary)";
1888
2003
  }
2004
+ const tipFor = (d) => ({
2005
+ title: d.label,
2006
+ items: keys.map((k) => ({ color: colorFor(k), label: k, value: fmt(Number(d[k]) || 0) }))
2007
+ });
2008
+ const focusActive = (k) => focusSeries == null ? void 0 : k === focusSeries || void 0;
2009
+ const onMarkMove = (d, localIdx, e) => {
2010
+ show(tipFor(d), e);
2011
+ setHoverIdx(localIdx);
2012
+ };
2013
+ const onMarkLeave = () => {
2014
+ hide();
2015
+ setHoverIdx(null);
2016
+ };
2017
+ const clickDatum = (d, k, localIdx) => {
2018
+ if (!onDataClick) return;
2019
+ onDataClick({ label: d.label, series: k, seriesIndex: allKeys.indexOf(k), value: Number(d[k]) || 0, index: baseIdx + localIdx, row: d });
2020
+ };
2021
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
2022
+ const applyZoom = (s, e) => {
2023
+ const n = allRows.length;
2024
+ if (e - s >= n - 1) {
2025
+ setZoom(null);
2026
+ return;
2027
+ }
2028
+ setZoom({ s: Math.max(0, s), e: Math.min(n - 1, e) });
2029
+ };
2030
+ const catFromEvent = (e) => {
2031
+ const svg = svgRef.current;
2032
+ if (!svg) return null;
2033
+ const r2 = svg.getBoundingClientRect();
2034
+ if (!(r2.width > 0)) return null;
2035
+ const frac = Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW));
2036
+ const idx = isBar ? Math.floor(frac * rows.length) : Math.round(frac * Math.max(1, rows.length - 1));
2037
+ return Math.min(rows.length - 1, Math.max(0, idx));
2038
+ };
2039
+ const onOverlayMove = (e) => {
2040
+ const idx = catFromEvent(e);
2041
+ if (idx == null) return;
2042
+ setHoverIdx(idx);
2043
+ show(tipFor(rows[idx]), e);
2044
+ if (drag) {
2045
+ if (drag.pan) {
2046
+ const span = drag.z0.e - drag.z0.s;
2047
+ const s = Math.min(allRows.length - 1 - span, Math.max(0, drag.z0.s + (drag.start - idx)));
2048
+ applyZoom(s, s + span);
2049
+ } else {
2050
+ setDrag({ ...drag, end: idx });
2051
+ }
2052
+ }
2053
+ };
2054
+ const onOverlayDown = (e) => {
2055
+ const idx = catFromEvent(e);
2056
+ if (idx == null) return;
2057
+ setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
2058
+ };
2059
+ const onOverlayUp = () => {
2060
+ if (drag && !drag.pan) {
2061
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
2062
+ if (hi - lo >= 1) applyZoom(baseIdx + lo, baseIdx + hi);
2063
+ }
2064
+ setDrag(null);
2065
+ };
2066
+ const onOverlayClick = (e) => {
2067
+ if (!onDataClick) return;
2068
+ const idx = catFromEvent(e);
2069
+ if (idx == null) return;
2070
+ const d = rows[idx];
2071
+ onDataClick({ label: d.label, series: null, seriesIndex: -1, value: void 0, index: baseIdx + idx, row: d });
2072
+ selectMark(`${idx}:*`);
2073
+ };
2074
+ React51__namespace.default.useEffect(() => {
2075
+ if (!zoomable) return void 0;
2076
+ const svg = svgRef.current;
2077
+ if (!svg) return void 0;
2078
+ const onWheel = (e) => {
2079
+ e.preventDefault();
2080
+ const n = allRows.length;
2081
+ const cur = zoom || { s: 0, e: n - 1 };
2082
+ const span = cur.e - cur.s;
2083
+ const r2 = svg.getBoundingClientRect();
2084
+ const frac = r2.width > 0 ? Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW)) : 0.5;
2085
+ const center = cur.s + Math.round(frac * span);
2086
+ const newSpan = Math.min(n - 1, Math.max(1, Math.round(span * (e.deltaY < 0 ? 0.8 : 1.25))));
2087
+ const s = Math.min(n - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frac)));
2088
+ applyZoom(s, s + newSpan);
2089
+ };
2090
+ svg.addEventListener("wheel", onWheel, { passive: false });
2091
+ return () => svg.removeEventListener("wheel", onWheel);
2092
+ }, [zoomable, zoom, allRows.length]);
1889
2093
  const renderBars = () => {
1890
2094
  const band = horizontal ? catBandH : catBand;
1891
- const gap = 0.28;
1892
- const slot = band * (1 - gap);
2095
+ const slot = band * 0.72;
1893
2096
  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
- };
2097
+ const gStart = (horizontal ? padT : padL) + band * gi + band * 0.28 / 2;
2098
+ const handlers = { onMouseMove: (e) => onMarkMove(d, gi, e), onMouseLeave: onMarkLeave };
2099
+ const barCommon = (k, key) => ({
2100
+ className: `twc-chart__bar ${animate ? "twc-chart__anim-bar" : ""}`.trim(),
2101
+ "data-mark": true,
2102
+ "data-horizontal": horizontal || void 0,
2103
+ "data-active": focusActive(k),
2104
+ "data-selected": selected === key || void 0,
2105
+ style: { fill: colorFor(k) },
2106
+ rx: 2,
2107
+ ...handlers,
2108
+ onClick: () => {
2109
+ clickDatum(d, k, gi);
2110
+ selectMark(key);
2111
+ }
2112
+ });
1899
2113
  if (canStack) {
1900
2114
  let acc = 0;
1901
- return keys.map((k, si) => {
2115
+ return keys.map((k) => {
1902
2116
  const v = Number(d[k]) || 0;
1903
2117
  const p0 = vPix(acc), p1 = vPix(acc + v);
1904
2118
  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}`);
2119
+ const key = `${gi}:${k}`;
2120
+ 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
2121
  });
1909
2122
  }
1910
2123
  const bw = slot / Math.max(1, keys.length);
1911
2124
  return keys.map((k, si) => {
1912
2125
  const v = Number(d[k]) || 0;
1913
2126
  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}`);
2127
+ const key = `${gi}:${k}`;
2128
+ 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
2129
  });
1918
2130
  });
1919
2131
  };
@@ -1928,7 +2140,7 @@ function Chart({
1928
2140
  });
1929
2141
  const base = canStack ? rows.map((d, i) => vPos(stackAcc[i] - (Number(d[k]) || 0))) : null;
1930
2142
  const gradId = `twc-grad-${gid}-${allKeys.indexOf(k)}`;
1931
- return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
2143
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { "data-mark": true, "data-active": focusActive(k), children: [
1932
2144
  isArea ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1933
2145
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: gradId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
1934
2146
  /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.34" }),
@@ -1937,14 +2149,14 @@ function Chart({
1937
2149
  /* @__PURE__ */ jsxRuntime.jsx(
1938
2150
  "path",
1939
2151
  {
1940
- className: `twc-chart__area ${anim === "" ? "twc-chart__anim-fade" : ""}`.trim(),
2152
+ className: `twc-chart__area ${animate ? "twc-chart__anim-fade" : ""}`.trim(),
1941
2153
  style: { fill: `url(#${gradId})` },
1942
2154
  d: canStack ? `${linePath(pts, pathOpts)} ${pts.map((p, i) => `L${p[0]} ${base[i]}`).reverse().join(" ")} Z` : areaPath(pts, padT + innerH, pathOpts)
1943
2155
  }
1944
2156
  )
1945
2157
  ] }) : 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: [
2158
+ /* @__PURE__ */ jsxRuntime.jsx("path", { className: `twc-chart__line ${animate ? "twc-chart__anim-line" : ""}`.trim(), style: { stroke: color }, pathLength: "1", d: linePath(pts, pathOpts) }),
2159
+ showDots || !isArea ? pts.map((p, i) => /* @__PURE__ */ jsxRuntime.jsxs("g", { className: animate ? "twc-chart__anim-fade" : void 0, children: [
1948
2160
  /* @__PURE__ */ jsxRuntime.jsx(
1949
2161
  "circle",
1950
2162
  {
@@ -1952,11 +2164,25 @@ function Chart({
1952
2164
  cx: p[0],
1953
2165
  cy: p[1],
1954
2166
  r: "14",
1955
- onMouseMove: (e) => show(tipFor(rows[i]), e),
1956
- onMouseLeave: hide
2167
+ onMouseMove: (e) => onMarkMove(rows[i], i, e),
2168
+ onMouseLeave: onMarkLeave,
2169
+ onClick: () => {
2170
+ clickDatum(rows[i], k, i);
2171
+ selectMark(`${i}:${k}`);
2172
+ }
1957
2173
  }
1958
2174
  ),
1959
- /* @__PURE__ */ jsxRuntime.jsx("circle", { className: "twc-chart__dot", style: { stroke: color }, cx: p[0], cy: p[1], r: "3.5" })
2175
+ /* @__PURE__ */ jsxRuntime.jsx(
2176
+ "circle",
2177
+ {
2178
+ className: "twc-chart__dot",
2179
+ style: { stroke: color },
2180
+ cx: p[0],
2181
+ cy: p[1],
2182
+ r: hoverIdx === i ? 5 : 3.5,
2183
+ "data-selected": selected === `${i}:${k}` || void 0
2184
+ }
2185
+ )
1960
2186
  ] }, i)) : null
1961
2187
  ] }, k);
1962
2188
  });
@@ -1982,62 +2208,153 @@ function Chart({
1982
2208
  return /* @__PURE__ */ jsxRuntime.jsx("text", { className: "twc-chart__label", x: lineX(i), y: vPos(v) - 9, textAnchor: "middle", children: shortNum(v) }, `${k}-${i}`);
1983
2209
  }));
1984
2210
  };
2211
+ const renderZones = () => rows.map((d, i) => {
2212
+ let x, y, w, hgt;
2213
+ if (isBar && horizontal) {
2214
+ x = padL;
2215
+ w = innerW;
2216
+ y = padT + catBandH * i;
2217
+ hgt = catBandH;
2218
+ } else if (isBar) {
2219
+ x = padL + catBand * i;
2220
+ w = catBand;
2221
+ y = padT;
2222
+ hgt = innerH;
2223
+ } else {
2224
+ const x0 = i === 0 ? padL : (lineX(i - 1) + lineX(i)) / 2;
2225
+ const x1 = i === rows.length - 1 ? padL + innerW : (lineX(i) + lineX(i + 1)) / 2;
2226
+ x = x0;
2227
+ w = x1 - x0;
2228
+ y = padT;
2229
+ hgt = innerH;
2230
+ }
2231
+ return /* @__PURE__ */ jsxRuntime.jsx(
2232
+ "rect",
2233
+ {
2234
+ className: "twc-chart__zone",
2235
+ x,
2236
+ y,
2237
+ width: Math.max(0, w),
2238
+ height: hgt,
2239
+ "data-clickable": clickable || void 0,
2240
+ onMouseMove: (e) => {
2241
+ show(tipFor(d), e);
2242
+ setHoverIdx(i);
2243
+ },
2244
+ onMouseLeave: onMarkLeave,
2245
+ onClick: () => {
2246
+ if (onDataClick) onDataClick({ label: d.label, series: null, seriesIndex: -1, value: void 0, index: baseIdx + i, row: d });
2247
+ }
2248
+ },
2249
+ i
2250
+ );
2251
+ });
1985
2252
  const toggle = (it) => setHidden((prev) => {
1986
2253
  const next = new Set(prev);
1987
2254
  if (next.has(it.label)) next.delete(it.label);
1988
2255
  else if (allKeys.length - next.size > 1) next.add(it.label);
1989
2256
  return next;
1990
2257
  });
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
- ] });
2258
+ const showCrosshair = crosshair && !horizontal && hoverIdx != null && rows[hoverIdx];
2259
+ const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
2260
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2261
+ "div",
2262
+ {
2263
+ ref: containerRef,
2264
+ className: `twc-chart twc-chart--${type} ${className}`.trim(),
2265
+ "data-hovering": focusSeries != null || void 0,
2266
+ "data-has-selection": selected != null || void 0,
2267
+ "data-clickable": clickable || void 0,
2268
+ ...rest,
2269
+ children: [
2270
+ baseStyles,
2271
+ styles,
2272
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
2273
+ showGrid ? ticks.map((t, i) => {
2274
+ const p = vPos(t);
2275
+ 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);
2276
+ }) : null,
2277
+ showAxis ? /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-chart__axisline", x1: padL, y1: padT, x2: padL, y2: padT + innerH }) : null,
2278
+ showAxis ? /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-chart__axisline", x1: padL, y1: padT + innerH, x2: W - padR, y2: padT + innerH }) : null,
2279
+ showAxis ? ticks.map((t, i) => {
2280
+ const p = vPos(t);
2281
+ 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);
2282
+ }) : null,
2283
+ showCrosshair ? /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-chart__crosshair", x1: catX(hoverIdx), y1: padT, x2: catX(hoverIdx), y2: padT + innerH }) : null,
2284
+ !zoomable ? renderZones() : null,
2285
+ isBar ? renderBars() : renderLines(),
2286
+ renderValues(),
2287
+ showAxis ? rows.map((d, i) => {
2288
+ if (horizontal) {
2289
+ const cy = padT + catBandH * i + catBandH / 2;
2290
+ return /* @__PURE__ */ jsxRuntime.jsx("text", { className: "twc-chart__axis", x: padL - 8, y: cy + 4, textAnchor: "end", children: d.label }, i);
2291
+ }
2292
+ return /* @__PURE__ */ jsxRuntime.jsx("text", { className: "twc-chart__axis", x: catX(i), y: H - 8, textAnchor: "middle", children: d.label }, i);
2293
+ }) : null,
2294
+ dragBand ? (() => {
2295
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
2296
+ const x0 = isBar ? padL + catBand * lo : lineX(lo);
2297
+ const x1 = isBar ? padL + catBand * (hi + 1) : lineX(hi);
2298
+ return /* @__PURE__ */ jsxRuntime.jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
2299
+ })() : null,
2300
+ zoomable ? /* @__PURE__ */ jsxRuntime.jsx(
2301
+ "rect",
2302
+ {
2303
+ className: "twc-chart__overlay",
2304
+ "data-zoom": "true",
2305
+ "data-clickable": clickable || void 0,
2306
+ "data-panning": drag && drag.pan || void 0,
2307
+ x: padL,
2308
+ y: padT,
2309
+ width: innerW,
2310
+ height: innerH,
2311
+ onMouseMove: onOverlayMove,
2312
+ onMouseLeave: () => {
2313
+ onMarkLeave();
2314
+ setDrag(null);
2315
+ },
2316
+ onMouseDown: onOverlayDown,
2317
+ onMouseUp: onOverlayUp,
2318
+ onClick: onOverlayClick
2319
+ }
2320
+ ) : null
2321
+ ] }),
2322
+ zoomable && zoom ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
2323
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2324
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
2325
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
2326
+ ] }),
2327
+ "Reset"
2328
+ ] }) : null,
2329
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { tip }),
2330
+ tableFallback ? /* @__PURE__ */ jsxRuntime.jsx(
2331
+ ChartTable,
2332
+ {
2333
+ id: tableId,
2334
+ caption: caption ?? svgAriaLabel,
2335
+ columns: allKeys,
2336
+ rows: allRows.map((d) => ({ label: d.label, values: allKeys.map((k) => fmt(Number(d[k]) || 0)) }))
2337
+ }
2338
+ ) : null,
2339
+ showLegend && multi ? /* @__PURE__ */ jsxRuntime.jsx(
2340
+ ChartLegend,
2341
+ {
2342
+ items: allKeys.map((k) => ({ label: k, color: colorFor(k) })),
2343
+ onToggle: toggle,
2344
+ hidden: (it) => hidden.has(it.label),
2345
+ onFocus: (it) => setFocusSeries(it.label),
2346
+ onBlur: () => setFocusSeries(null)
2347
+ }
2348
+ ) : null
2349
+ ]
2350
+ }
2351
+ );
2035
2352
  }
2036
2353
  var CHART_CSS2 = `
2037
2354
  .twc-chart--pie svg { overflow: visible; }
2038
2355
  .twc-chart__slice { transition: opacity var(--duration-fast) var(--ease-standard); stroke: var(--color-surface); stroke-width: 1.5; }
2039
2356
  .twc-chart__slice:hover { opacity: 0.85; }
2040
- .twc-chart__slice-label { fill: var(--color-surface); font-size: 11px; font-weight: 600; }
2357
+ .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
2358
  .twc-chart__center { fill: var(--color-text); font-weight: 700; }
2042
2359
  .twc-chart__center-sub { fill: var(--color-text-subtle); }
2043
2360
  .twc-chart__empty { fill: var(--color-text-subtle); font-size: var(--text-sm); }
@@ -2054,6 +2371,7 @@ function PieChart({
2054
2371
  valueFormat,
2055
2372
  height = 260,
2056
2373
  colors,
2374
+ onDataClick,
2057
2375
  ariaLabel,
2058
2376
  "aria-label": ariaLabelProp,
2059
2377
  tableFallback = true,
@@ -2067,6 +2385,9 @@ function PieChart({
2067
2385
  const tableId = tableFallback ? `${uid}-table` : void 0;
2068
2386
  const { containerRef, tip, show, hide } = useChartTooltip();
2069
2387
  const [active, setActive] = React51__namespace.default.useState(null);
2388
+ const [focus, setFocus] = React51__namespace.default.useState(null);
2389
+ const [selected, setSelected] = React51__namespace.default.useState(null);
2390
+ const clickable = !!onDataClick;
2070
2391
  const rows = data || [];
2071
2392
  const fmt = valueFormat || fmtNumber;
2072
2393
  const values = rows.map((d) => Math.max(0, Number(d.value) || 0));
@@ -2090,20 +2411,37 @@ function PieChart({
2090
2411
  const color = d.color || paletteAt(colors, i);
2091
2412
  return { d, i, v, frac, s0, s1, span, pct, color };
2092
2413
  });
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: [
2414
+ const clickDatum = (d, i, v, pct) => {
2415
+ if (!onDataClick) return;
2416
+ onDataClick({ label: d.label, value: v, index: i, percent: pct });
2417
+ };
2418
+ const selectMark = (i) => setSelected((s) => s === i ? null : i);
2419
+ const emph = focus != null ? focus : active;
2420
+ const multi = rows.length > 1;
2421
+ 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
2422
  baseStyles,
2095
2423
  styles,
2096
2424
  /* @__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
2425
  slices.map(({ d, i, v, s0, s1, span, pct, color }) => {
2098
2426
  const pad = span > padAngle ? padAngle / 2 : 0;
2099
2427
  const items = [{ color, label: fmt(v), value: `${pct}%` }];
2428
+ let explode;
2429
+ if (selected === i) {
2430
+ const mid = (s0 + s1) / 2;
2431
+ const a = (mid - 90) * Math.PI / 180;
2432
+ const dx = Math.cos(a) * 8, dy = Math.sin(a) * 8;
2433
+ if (Number.isFinite(dx) && Number.isFinite(dy)) {
2434
+ explode = `translate(${dx}px, ${dy}px)`;
2435
+ }
2436
+ }
2100
2437
  return /* @__PURE__ */ jsxRuntime.jsx(
2101
2438
  "path",
2102
2439
  {
2103
2440
  className: "twc-chart__slice twc-chart__anim-arc",
2104
- style: { fill: color },
2441
+ style: { fill: color, transform: explode },
2105
2442
  "data-mark": "",
2106
- "data-active": active === i ? "true" : void 0,
2443
+ "data-active": emph === i ? "true" : void 0,
2444
+ "data-selected": selected === i ? "true" : void 0,
2107
2445
  d: arcPath(cx, cy, rOuter, rInner, s0 + pad, s1 - pad),
2108
2446
  onMouseMove: (e) => {
2109
2447
  setActive(i);
@@ -2112,6 +2450,10 @@ function PieChart({
2112
2450
  onMouseLeave: () => {
2113
2451
  setActive(null);
2114
2452
  hide();
2453
+ },
2454
+ onClick: () => {
2455
+ clickDatum(d, i, v, pct);
2456
+ selectMark(i);
2115
2457
  }
2116
2458
  },
2117
2459
  i
@@ -2147,7 +2489,14 @@ function PieChart({
2147
2489
  rows: slices.map(({ d, v, pct }) => ({ label: d.label, values: [fmt(v), `${pct}%`] }))
2148
2490
  }
2149
2491
  ) : null,
2150
- showLegend && rows.length ? /* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { items: slices.map(({ d, color }) => ({ label: d.label, color })) }) : null
2492
+ showLegend && rows.length ? /* @__PURE__ */ jsxRuntime.jsx(
2493
+ ChartLegend,
2494
+ {
2495
+ items: slices.map(({ d, color }) => ({ label: d.label, color })),
2496
+ onFocus: multi ? (it, i) => setFocus(i) : void 0,
2497
+ onBlur: multi ? () => setFocus(null) : void 0
2498
+ }
2499
+ ) : null
2151
2500
  ] });
2152
2501
  }
2153
2502
  function DonutChart(props) {
@@ -2159,6 +2508,7 @@ var GAUGE_CSS = `
2159
2508
  .twc-gauge__arc { fill: none; stroke-linecap: round; transition: stroke-dashoffset var(--duration-slow) var(--ease-standard); }
2160
2509
  .twc-gauge__value { fill: var(--color-text); font-weight: 700; }
2161
2510
  .twc-gauge__label { fill: var(--color-text-subtle); }
2511
+ .twc-gauge__arc[data-selected="true"] { filter: drop-shadow(0 0 2px var(--color-text)); }
2162
2512
  `;
2163
2513
  function Gauge({
2164
2514
  value = 0,
@@ -2175,6 +2525,7 @@ function Gauge({
2175
2525
  size = 200,
2176
2526
  series,
2177
2527
  showLegend = true,
2528
+ onDataClick,
2178
2529
  ariaLabel,
2179
2530
  "aria-label": ariaLabelProp,
2180
2531
  tableFallback = true,
@@ -2187,6 +2538,8 @@ function Gauge({
2187
2538
  const uid = React51__namespace.default.useId();
2188
2539
  const tableId = tableFallback ? `${uid}-table` : void 0;
2189
2540
  const { containerRef, tip, show, hide } = useChartTooltip();
2541
+ const [selected, setSelected] = React51__namespace.default.useState(null);
2542
+ const [focus, setFocus] = React51__namespace.default.useState(null);
2190
2543
  const fmt = valueFormat || ((n) => String(Math.round(Number(n) || 0)));
2191
2544
  const cx = size / 2, cy = size / 2;
2192
2545
  const span = endAngle - startAngle;
@@ -2211,79 +2564,106 @@ function Gauge({
2211
2564
  return `M${r(x0)} ${r(y0)} A${r(radius)} ${r(radius)} 0 ${large} ${sweep} ${r(x1)} ${r(y1)}`;
2212
2565
  };
2213
2566
  const single = items[0];
2567
+ const clickable = !!onDataClick;
2568
+ const activeFor = (i) => focus == null ? void 0 : i === focus || void 0;
2569
+ const clickRing = (it, i) => {
2570
+ if (onDataClick) onDataClick({ value: Number(it.value) || 0, label: it.label, index: i });
2571
+ };
2572
+ const selectRing = (i) => setSelected((s) => s === i ? null : i);
2214
2573
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? (isMulti ? `Gauge with ${items.length} values` : `${labelText(label) || "Gauge"}: ${fmt(single.value)} of ${fmt(max)}`);
2215
2574
  const valueFont = Math.round(size * (isMulti ? 0.11 : 0.17));
2216
2575
  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
- ] });
2576
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2577
+ "div",
2578
+ {
2579
+ ref: containerRef,
2580
+ className: `twc-gauge twc-chart twc-chart--gauge ${className}`.trim(),
2581
+ "data-multi": isMulti || void 0,
2582
+ "data-hovering": focus != null || void 0,
2583
+ "data-clickable": clickable || void 0,
2584
+ "data-has-selection": selected != null || void 0,
2585
+ ...rest,
2586
+ children: [
2587
+ baseStyles,
2588
+ styles,
2589
+ /* @__PURE__ */ jsxRuntime.jsxs(
2590
+ "svg",
2591
+ {
2592
+ viewBox: `0 0 ${size} ${size}`,
2593
+ width: size,
2594
+ height: size,
2595
+ role: "img",
2596
+ "aria-label": svgAriaLabel,
2597
+ "aria-describedby": tableId,
2598
+ children: [
2599
+ items.map((it, i) => {
2600
+ const radius = ringRadius(i);
2601
+ const f = frac(it.value);
2602
+ const c = it.color || paletteAt(void 0, i);
2603
+ const valEnd = startAngle + f * span;
2604
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
2605
+ /* @__PURE__ */ jsxRuntime.jsx(
2606
+ "path",
2607
+ {
2608
+ className: "twc-gauge__track",
2609
+ d: arc(radius, startAngle, endAngle),
2610
+ style: { stroke: trackColor, strokeWidth: strokeW }
2611
+ }
2612
+ ),
2613
+ f > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2614
+ "path",
2615
+ {
2616
+ className: "twc-gauge__arc twc-chart__anim-fade",
2617
+ "data-mark": true,
2618
+ "data-active": activeFor(i),
2619
+ "data-selected": selected === i || void 0,
2620
+ d: arc(radius, startAngle, valEnd),
2621
+ style: { stroke: c, strokeWidth: strokeW },
2622
+ onMouseMove: (e) => show({
2623
+ title: labelText(it.label) || "Value",
2624
+ items: [{ color: c, label: "", value: fmt(it.value) }]
2625
+ }, e),
2626
+ onMouseLeave: hide,
2627
+ onClick: () => {
2628
+ clickRing(it, i);
2629
+ selectRing(i);
2630
+ }
2631
+ }
2632
+ ) : null
2633
+ ] }, i);
2634
+ }),
2635
+ !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,
2636
+ !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
2637
+ ]
2638
+ }
2639
+ ),
2640
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { tip }),
2641
+ tableFallback ? /* @__PURE__ */ jsxRuntime.jsx(
2642
+ ChartTable,
2643
+ {
2644
+ id: tableId,
2645
+ caption: caption ?? svgAriaLabel,
2646
+ columns: ["Value"],
2647
+ rows: items.map((it, i) => ({
2648
+ label: labelText(it.label) || (isMulti ? `Series ${i + 1}` : "Value"),
2649
+ values: [fmt(it.value)]
2650
+ }))
2651
+ }
2652
+ ) : null,
2653
+ isMulti && showLegend ? /* @__PURE__ */ jsxRuntime.jsx(
2654
+ ChartLegend,
2655
+ {
2656
+ items: items.map((it, i) => ({
2657
+ label: labelText(it.label) || `Series ${i + 1}`,
2658
+ color: it.color || paletteAt(void 0, i)
2659
+ })),
2660
+ onFocus: items.length > 1 ? (it, i) => setFocus(i) : void 0,
2661
+ onBlur: items.length > 1 ? () => setFocus(null) : void 0
2662
+ }
2663
+ ) : null
2664
+ ]
2665
+ }
2666
+ );
2287
2667
  }
2288
2668
  function labelText(label) {
2289
2669
  return typeof label === "string" || typeof label === "number" ? String(label) : "";
@@ -2310,6 +2690,7 @@ function Sparkline({
2310
2690
  min,
2311
2691
  max,
2312
2692
  valueFormat,
2693
+ onDataClick,
2313
2694
  ariaLabel,
2314
2695
  "aria-label": ariaLabelProp,
2315
2696
  tableFallback = false,
@@ -2323,6 +2704,8 @@ function Sparkline({
2323
2704
  const tableId = tableFallback ? `${uid}-table` : void 0;
2324
2705
  const { containerRef, tip, show, hide } = useChartTooltip();
2325
2706
  const [hover, setHover] = React51__namespace.default.useState(null);
2707
+ const [selected, setSelected] = React51__namespace.default.useState(null);
2708
+ const clickable = !!onDataClick;
2326
2709
  const kind = area ? "area" : type;
2327
2710
  const isBar = kind === "bar";
2328
2711
  const isArea = kind === "area";
@@ -2364,6 +2747,10 @@ function Sparkline({
2364
2747
  setHover(null);
2365
2748
  hide();
2366
2749
  };
2750
+ const clickPoint = (i) => {
2751
+ if (onDataClick) onDataClick({ value: values[i], index: i, label: labels[i] });
2752
+ setSelected((s) => s === i ? null : i);
2753
+ };
2367
2754
  const last = n ? values[n - 1] : 0;
2368
2755
  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
2756
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? summary;
@@ -2376,93 +2763,119 @@ function Sparkline({
2376
2763
  "rect",
2377
2764
  {
2378
2765
  className: "twc-sparkline__bar twc-chart__anim-fade",
2766
+ "data-mark": true,
2767
+ "data-selected": selected === i || void 0,
2379
2768
  style: { fill: color },
2380
2769
  x: pad + band * i + (band - slot) / 2,
2381
2770
  y: baselineY - h,
2382
2771
  width: Math.max(1, slot),
2383
2772
  height: Math.max(1, h),
2384
2773
  onMouseMove: (e) => show(tipFor(i), e),
2385
- onMouseLeave: hide
2774
+ onMouseLeave: hide,
2775
+ onClick: () => clickPoint(i)
2386
2776
  },
2387
2777
  i
2388
2778
  );
2389
2779
  });
2390
2780
  };
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; }
2781
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2782
+ "div",
2783
+ {
2784
+ ref: containerRef,
2785
+ className: `twc-chart twc-sparkline twc-chart--${kind} ${className}`.trim(),
2786
+ "data-clickable": clickable || void 0,
2787
+ "data-has-selection": selected != null || void 0,
2788
+ ...rest,
2789
+ children: [
2790
+ baseStyles,
2791
+ styles,
2792
+ /* @__PURE__ */ jsxRuntime.jsx(
2793
+ "svg",
2794
+ {
2795
+ viewBox: `0 0 ${W} ${H}`,
2796
+ width: W,
2797
+ height: H,
2798
+ role: "img",
2799
+ "aria-label": svgAriaLabel,
2800
+ "aria-describedby": tableId,
2801
+ children: n === 0 ? null : isBar ? renderBars() : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2802
+ isArea ? /* @__PURE__ */ jsxRuntime.jsx("path", { className: "twc-sparkline__area twc-chart__anim-fade", style: { fill: color }, d: areaPath(points, baselineY, pathOpts) }) : null,
2803
+ /* @__PURE__ */ jsxRuntime.jsx(
2804
+ "path",
2805
+ {
2806
+ className: "twc-sparkline__line twc-chart__anim-line",
2807
+ style: { stroke: color, strokeWidth },
2808
+ pathLength: "1",
2809
+ d: linePath(points, pathOpts)
2810
+ }
2811
+ ),
2812
+ showDots && n ? /* @__PURE__ */ jsxRuntime.jsx(
2813
+ "circle",
2814
+ {
2815
+ className: "twc-sparkline__dot twc-chart__anim-fade",
2816
+ style: { fill: color },
2817
+ cx: points[n - 1][0],
2818
+ cy: points[n - 1][1],
2819
+ r: dotR
2820
+ }
2821
+ ) : null,
2822
+ hover != null ? /* @__PURE__ */ jsxRuntime.jsx(
2823
+ "circle",
2824
+ {
2825
+ className: "twc-sparkline__dot",
2826
+ style: { fill: color },
2827
+ cx: points[hover][0],
2828
+ cy: points[hover][1],
2829
+ r: Math.max(2, strokeWidth + 1)
2830
+ }
2831
+ ) : null,
2832
+ selected != null && points[selected] ? /* @__PURE__ */ jsxRuntime.jsx(
2833
+ "circle",
2834
+ {
2835
+ className: "twc-sparkline__dot",
2836
+ "data-mark": true,
2837
+ "data-selected": "true",
2838
+ style: { fill: color, pointerEvents: "none" },
2839
+ cx: points[selected][0],
2840
+ cy: points[selected][1],
2841
+ r: Math.max(2, strokeWidth + 1)
2842
+ }
2843
+ ) : null,
2844
+ points.map((p, i) => /* @__PURE__ */ jsxRuntime.jsx(
2845
+ "circle",
2846
+ {
2847
+ className: "twc-chart__hit",
2848
+ cx: p[0],
2849
+ cy: p[1],
2850
+ r: "14",
2851
+ onMouseMove: onPointMove(i),
2852
+ onMouseLeave: onPointLeave,
2853
+ onClick: () => clickPoint(i)
2854
+ },
2855
+ i
2856
+ ))
2857
+ ] })
2858
+ }
2859
+ ),
2860
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { tip }),
2861
+ tableFallback ? /* @__PURE__ */ jsxRuntime.jsx(
2862
+ ChartTable,
2863
+ {
2864
+ id: tableId,
2865
+ caption: caption ?? svgAriaLabel,
2866
+ columns: ["value"],
2867
+ rows: values.map((v, i) => ({ label: labels[i], values: [fmt(v)] }))
2868
+ }
2869
+ ) : null
2870
+ ]
2871
+ }
2872
+ );
2873
+ }
2874
+ var SCATTER_CSS = `
2875
+ .twc-chart--scatter__grid { stroke: var(--color-divider); stroke-width: 1; }
2876
+ .twc-chart--scatter__axis { fill: var(--color-text-subtle); font-size: 11px; }
2877
+ .twc-chart--scatter__title { fill: var(--color-text-muted); font-size: 11px; font-weight: 500; }
2878
+ .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
2879
  .twc-chart--scatter__dot[data-bubble="true"] { fill-opacity: 0.55; }
2467
2880
  .twc-chart__hit { fill: transparent; }
2468
2881
  .twc-chart__hit:hover + .twc-chart--scatter__dot { transform: scale(1.18); }
@@ -2477,9 +2890,11 @@ function ScatterChart({
2477
2890
  yLabel,
2478
2891
  showGrid = true,
2479
2892
  showLegend,
2893
+ zoomable = false,
2480
2894
  xFormat,
2481
2895
  yFormat,
2482
2896
  valueFormat,
2897
+ onDataClick,
2483
2898
  ariaLabel,
2484
2899
  "aria-label": ariaLabelProp,
2485
2900
  tableFallback = true,
@@ -2490,15 +2905,24 @@ function ScatterChart({
2490
2905
  const baseStyles = useScopedStyles("twc-chart-base", CHART_BASE_CSS);
2491
2906
  const styles = useScopedStyles("twc-scatter-styles", SCATTER_CSS);
2492
2907
  const uid = React51__namespace.default.useId();
2908
+ const gid = uid.replace(/[^a-zA-Z0-9]/g, "");
2909
+ const clipId = `twc-sc-clip-${gid}`;
2493
2910
  const tableId = tableFallback ? `${uid}-table` : void 0;
2494
2911
  const { containerRef, tip, show, hide } = useChartTooltip();
2912
+ const [focus, setFocus] = React51__namespace.default.useState(null);
2913
+ const [selected, setSelected] = React51__namespace.default.useState(null);
2914
+ const [zoom, setZoom] = React51__namespace.default.useState(null);
2915
+ const [drag, setDrag] = React51__namespace.default.useState(null);
2916
+ const svgRef = React51__namespace.default.useRef(null);
2917
+ const movedRef = React51__namespace.default.useRef(false);
2495
2918
  const list = Array.isArray(series) ? series : [];
2496
2919
  const vFmt = valueFormat || fmtNumber;
2497
2920
  const xFmt = xFormat || vFmt;
2498
2921
  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
- );
2922
+ const all = list.flatMap((s, si) => {
2923
+ const pts = s && Array.isArray(s.points) ? s.points : [];
2924
+ 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 }));
2925
+ });
2502
2926
  const hasZ = all.some((p) => Number.isFinite(Number(p.z)));
2503
2927
  const useBubble = bubble || hasZ;
2504
2928
  const zMax = useBubble ? Math.max(0, ...all.map((p) => Number(p.z) || 0)) : 0;
@@ -2509,8 +2933,12 @@ function ScatterChart({
2509
2933
  return Math.max(2, Math.sqrt(zv / zMax) * maxBubble);
2510
2934
  };
2511
2935
  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);
2936
+ const base = { x0: Math.min(...xs), x1: Math.max(...xs), y0: Math.min(...ys), y1: Math.max(...ys) };
2937
+ const baseSpanX = base.x1 - base.x0, baseSpanY = base.y1 - base.y0;
2938
+ const canZoom = zoomable && all.length > 0 && baseSpanX > 0 && baseSpanY > 0;
2939
+ const dom = canZoom && zoom ? zoom : base;
2940
+ const xScale = niceScale(dom.x0, dom.x1, 5);
2941
+ const yScale = niceScale(dom.y0, dom.y1, 5);
2514
2942
  const xSpan = xScale.max - xScale.min || 1;
2515
2943
  const ySpan = yScale.max - yScale.min || 1;
2516
2944
  const W = 600, H = height;
@@ -2523,7 +2951,9 @@ function ScatterChart({
2523
2951
  const yPos = (y) => padT + innerH - (y - yScale.min) / ySpan * innerH;
2524
2952
  const seriesColor = (s, i) => s && s.color || paletteAt(void 0, i);
2525
2953
  const seriesName = (s, i) => s && s.name || `Series ${i + 1}`;
2526
- const legend = showLegend ?? list.length > 1;
2954
+ const multi = list.length > 1;
2955
+ const legend = showLegend ?? multi;
2956
+ const clickable = !!onDataClick;
2527
2957
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? `${useBubble ? "bubble" : "scatter"} chart`;
2528
2958
  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
2959
  const tipLabel = (p) => labelText2(p.label) || seriesName(list[p.si], p.si);
@@ -2536,72 +2966,272 @@ function ScatterChart({
2536
2966
  ...Number.isFinite(Number(p.z)) ? [{ label: "z", value: vFmt(Number(p.z)) }] : []
2537
2967
  ]
2538
2968
  });
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
2969
+ const focusActive = (si) => focus == null ? void 0 : si === focus || void 0;
2970
+ const clickPoint = (p) => {
2971
+ if (!onDataClick) return;
2972
+ 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 });
2973
+ };
2974
+ const selectPoint = (key) => setSelected((s) => s === key ? null : key);
2975
+ const clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
2976
+ const svgPoint = (e) => {
2977
+ const svg = svgRef.current;
2978
+ if (!svg || typeof svg.getScreenCTM !== "function" || typeof svg.createSVGPoint !== "function") return null;
2979
+ const ctm = svg.getScreenCTM();
2980
+ if (!ctm || !ctm.a || !ctm.d) return null;
2981
+ const pt = svg.createSVGPoint();
2982
+ pt.x = e.clientX;
2983
+ pt.y = e.clientY;
2984
+ const loc = pt.matrixTransform(ctm.inverse());
2985
+ if (!Number.isFinite(loc.x) || !Number.isFinite(loc.y)) return null;
2986
+ return { px: loc.x, py: loc.y };
2987
+ };
2988
+ const dataFromPx = (px, py) => {
2989
+ const cx = clamp2(px, padL, padL + innerW);
2990
+ const cy = clamp2(py, padT, padT + innerH);
2991
+ return {
2992
+ x: xScale.min + (cx - padL) / innerW * (xScale.max - xScale.min),
2993
+ y: yScale.min + (padT + innerH - cy) / innerH * (yScale.max - yScale.min)
2994
+ };
2995
+ };
2996
+ const nearestDot = (px, py) => {
2997
+ let best = null, bestD = Infinity;
2998
+ for (const p of dots) {
2999
+ const dx = xPos(p.x) - px, dy = yPos(p.y) - py;
3000
+ const d = dx * dx + dy * dy;
3001
+ const rr = Math.max(14, p.radius);
3002
+ if (d <= rr * rr && d < bestD) {
3003
+ bestD = d;
3004
+ best = p;
3005
+ }
3006
+ }
3007
+ return best;
3008
+ };
3009
+ const applyZoom = (win) => {
3010
+ const x0 = Math.max(Math.min(win.x0, win.x1), base.x0), x1 = Math.min(Math.max(win.x0, win.x1), base.x1);
3011
+ const y0 = Math.max(Math.min(win.y0, win.y1), base.y0), y1 = Math.min(Math.max(win.y0, win.y1), base.y1);
3012
+ if (!(x1 - x0 > 0) || !(y1 - y0 > 0)) return;
3013
+ if (x1 - x0 >= baseSpanX && y1 - y0 >= baseSpanY) {
3014
+ setZoom(null);
3015
+ return;
3016
+ }
3017
+ setZoom({ x0, x1, y0, y1 });
3018
+ };
3019
+ const panTo = (win) => {
3020
+ const wSpan = win.x1 - win.x0, hSpan = win.y1 - win.y0;
3021
+ let { x0, x1, y0, y1 } = win;
3022
+ if (x0 < base.x0) {
3023
+ x0 = base.x0;
3024
+ x1 = base.x0 + wSpan;
3025
+ } else if (x1 > base.x1) {
3026
+ x1 = base.x1;
3027
+ x0 = base.x1 - wSpan;
3028
+ }
3029
+ if (y0 < base.y0) {
3030
+ y0 = base.y0;
3031
+ y1 = base.y0 + hSpan;
3032
+ } else if (y1 > base.y1) {
3033
+ y1 = base.y1;
3034
+ y0 = base.y1 - hSpan;
3035
+ }
3036
+ setZoom({ x0, x1, y0, y1 });
3037
+ };
3038
+ const onOverlayDown = (e) => {
3039
+ const pt = svgPoint(e);
3040
+ if (!pt) return;
3041
+ movedRef.current = false;
3042
+ setDrag({ x0: pt.px, y0: pt.py, x1: pt.px, y1: pt.py, pan: e.shiftKey, z0: zoom || base });
3043
+ };
3044
+ const onOverlayMove = (e) => {
3045
+ const pt = svgPoint(e);
3046
+ if (!pt) return;
3047
+ if (drag) {
3048
+ if (Math.abs(pt.px - drag.x0) > 4 || Math.abs(pt.py - drag.y0) > 4) movedRef.current = true;
3049
+ if (drag.pan) {
3050
+ if (zoom) {
3051
+ const z0 = drag.z0;
3052
+ const dxData = -((pt.px - drag.x0) / innerW) * (z0.x1 - z0.x0);
3053
+ const dyData = (pt.py - drag.y0) / innerH * (z0.y1 - z0.y0);
3054
+ panTo({ x0: z0.x0 + dxData, x1: z0.x1 + dxData, y0: z0.y0 + dyData, y1: z0.y1 + dyData });
2563
3055
  }
2564
- ) : null,
2565
- dots.map((p, i) => /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "twc-chart__anim-fade", children: [
2566
- /* @__PURE__ */ jsxRuntime.jsx(
2567
- "circle",
3056
+ } else {
3057
+ setDrag({ ...drag, x1: pt.px, y1: pt.py });
3058
+ }
3059
+ hide();
3060
+ return;
3061
+ }
3062
+ const hit = nearestDot(pt.px, pt.py);
3063
+ if (hit) show(tipFor(hit), e);
3064
+ else hide();
3065
+ };
3066
+ const onOverlayUp = () => {
3067
+ if (drag && !drag.pan) {
3068
+ const a = dataFromPx(drag.x0, drag.y0), b = dataFromPx(drag.x1, drag.y1);
3069
+ 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 });
3070
+ }
3071
+ setDrag(null);
3072
+ };
3073
+ const onOverlayLeave = () => {
3074
+ hide();
3075
+ setDrag(null);
3076
+ };
3077
+ const onOverlayClick = (e) => {
3078
+ if (movedRef.current) {
3079
+ movedRef.current = false;
3080
+ return;
3081
+ }
3082
+ const pt = svgPoint(e);
3083
+ if (!pt) return;
3084
+ const hit = nearestDot(pt.px, pt.py);
3085
+ if (hit) {
3086
+ clickPoint(hit);
3087
+ selectPoint(hit.key);
3088
+ }
3089
+ };
3090
+ React51__namespace.default.useEffect(() => {
3091
+ if (!canZoom) return void 0;
3092
+ const svg = svgRef.current;
3093
+ if (!svg) return void 0;
3094
+ const onWheel = (e) => {
3095
+ e.preventDefault();
3096
+ const pt = svgPoint(e);
3097
+ const fx = pt ? clamp2((pt.px - padL) / innerW, 0, 1) : 0.5;
3098
+ const fy = pt ? clamp2((padT + innerH - pt.py) / innerH, 0, 1) : 0.5;
3099
+ const factor = e.deltaY < 0 ? 0.8 : 1.25;
3100
+ const x0 = xScale.min, x1 = xScale.max, y0 = yScale.min, y1 = yScale.max;
3101
+ const cxv = x0 + fx * (x1 - x0), cyv = y0 + fy * (y1 - y0);
3102
+ const nw = (x1 - x0) * factor, nh = (y1 - y0) * factor;
3103
+ applyZoom({ x0: cxv - fx * nw, x1: cxv + (1 - fx) * nw, y0: cyv - fy * nh, y1: cyv + (1 - fy) * nh });
3104
+ };
3105
+ svg.addEventListener("wheel", onWheel, { passive: false });
3106
+ return () => svg.removeEventListener("wheel", onWheel);
3107
+ }, [canZoom, zoom, xScale.min, xScale.max, yScale.min, yScale.max]);
3108
+ const dragBand = canZoom && drag && !drag.pan && Math.abs(drag.x1 - drag.x0) > 2 && Math.abs(drag.y1 - drag.y0) > 2;
3109
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3110
+ "div",
3111
+ {
3112
+ ref: containerRef,
3113
+ className: `twc-chart twc-chart--scatter ${className}`.trim(),
3114
+ "data-bubble": useBubble || void 0,
3115
+ "data-hovering": focus != null || void 0,
3116
+ "data-has-selection": selected != null || void 0,
3117
+ "data-clickable": clickable || void 0,
3118
+ ...rest,
3119
+ children: [
3120
+ baseStyles,
3121
+ styles,
3122
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, children: [
3123
+ /* @__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) }) }) }),
3124
+ showGrid ? yScale.ticks.map((t, i) => {
3125
+ const p = yPos(t);
3126
+ return /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-chart--scatter__grid", x1: padL, y1: p, x2: W - padR, y2: p }, `y${i}`);
3127
+ }) : null,
3128
+ showGrid ? xScale.ticks.map((t, i) => {
3129
+ const p = xPos(t);
3130
+ return /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-chart--scatter__grid", x1: p, y1: padT, x2: p, y2: padT + innerH }, `x${i}`);
3131
+ }) : null,
3132
+ 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}`)),
3133
+ 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}`)),
3134
+ xLabel != null ? /* @__PURE__ */ jsxRuntime.jsx("text", { className: "twc-chart--scatter__title", x: padL + innerW / 2, y: H - 6, textAnchor: "middle", children: xLabel }) : null,
3135
+ yLabel != null ? /* @__PURE__ */ jsxRuntime.jsx(
3136
+ "text",
3137
+ {
3138
+ className: "twc-chart--scatter__title",
3139
+ x: 16,
3140
+ y: padT + innerH / 2,
3141
+ textAnchor: "middle",
3142
+ transform: `rotate(-90 16 ${padT + innerH / 2})`,
3143
+ children: yLabel
3144
+ }
3145
+ ) : null,
3146
+ /* @__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: [
3147
+ /* @__PURE__ */ jsxRuntime.jsx(
3148
+ "circle",
3149
+ {
3150
+ className: "twc-chart__hit",
3151
+ cx: xPos(p.x),
3152
+ cy: yPos(p.y),
3153
+ r: Math.max(14, p.radius),
3154
+ onMouseMove: (e) => show(tipFor(p), e),
3155
+ onMouseLeave: hide,
3156
+ onClick: () => {
3157
+ clickPoint(p);
3158
+ selectPoint(p.key);
3159
+ }
3160
+ }
3161
+ ),
3162
+ /* @__PURE__ */ jsxRuntime.jsx(
3163
+ "circle",
3164
+ {
3165
+ className: "twc-chart--scatter__dot",
3166
+ "data-mark": true,
3167
+ "data-active": focusActive(p.si),
3168
+ "data-selected": selected === p.key || void 0,
3169
+ "data-bubble": useBubble || void 0,
3170
+ style: { fill: p.fill },
3171
+ cx: xPos(p.x),
3172
+ cy: yPos(p.y),
3173
+ r: p.radius
3174
+ }
3175
+ )
3176
+ ] }, p.key)) }),
3177
+ dragBand ? /* @__PURE__ */ jsxRuntime.jsx(
3178
+ "rect",
3179
+ {
3180
+ className: "twc-chart__zoomband",
3181
+ x: Math.min(drag.x0, drag.x1),
3182
+ y: Math.min(drag.y0, drag.y1),
3183
+ width: Math.abs(drag.x1 - drag.x0),
3184
+ height: Math.abs(drag.y1 - drag.y0)
3185
+ }
3186
+ ) : null,
3187
+ canZoom ? /* @__PURE__ */ jsxRuntime.jsx(
3188
+ "rect",
3189
+ {
3190
+ className: "twc-chart__overlay",
3191
+ "data-clickable": clickable || void 0,
3192
+ x: padL,
3193
+ y: padT,
3194
+ width: Math.max(0, innerW),
3195
+ height: Math.max(0, innerH),
3196
+ onMouseDown: onOverlayDown,
3197
+ onMouseMove: onOverlayMove,
3198
+ onMouseUp: onOverlayUp,
3199
+ onMouseLeave: onOverlayLeave,
3200
+ onClick: onOverlayClick
3201
+ }
3202
+ ) : null
3203
+ ] }),
3204
+ canZoom && zoom ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
3205
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3206
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
3207
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
3208
+ ] }),
3209
+ "Reset"
3210
+ ] }) : null,
3211
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { tip }),
3212
+ tableFallback ? /* @__PURE__ */ jsxRuntime.jsx(
3213
+ ChartTable,
2568
3214
  {
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
3215
+ id: tableId,
3216
+ caption: caption ?? svgAriaLabel,
3217
+ columns: tableColumns,
3218
+ rows: all.map((p) => ({
3219
+ label: seriesName(list[p.si], p.si),
3220
+ values: hasZ ? [xFmt(p.x), yFmt(p.y), Number.isFinite(Number(p.z)) ? vFmt(Number(p.z)) : ""] : [xFmt(p.x), yFmt(p.y)]
3221
+ }))
2575
3222
  }
2576
- ),
2577
- /* @__PURE__ */ jsxRuntime.jsx(
2578
- "circle",
3223
+ ) : null,
3224
+ legend && list.length ? /* @__PURE__ */ jsxRuntime.jsx(
3225
+ ChartLegend,
2579
3226
  {
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
3227
+ items: list.map((s, i) => ({ label: seriesName(s, i), color: seriesColor(s, i) })),
3228
+ onFocus: multi ? (it, i) => setFocus(i) : void 0,
3229
+ onBlur: multi ? () => setFocus(null) : void 0
2586
3230
  }
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
- ] });
3231
+ ) : null
3232
+ ]
3233
+ }
3234
+ );
2605
3235
  }
2606
3236
  function labelText2(label) {
2607
3237
  return typeof label === "string" || typeof label === "number" ? String(label) : "";
@@ -2633,6 +3263,7 @@ function RadarChart({
2633
3263
  colors,
2634
3264
  valueFormat,
2635
3265
  height = 300,
3266
+ onDataClick,
2636
3267
  ariaLabel,
2637
3268
  "aria-label": ariaLabelProp,
2638
3269
  tableFallback = true,
@@ -2645,11 +3276,16 @@ function RadarChart({
2645
3276
  const uid = React51__namespace.default.useId();
2646
3277
  const tableId = tableFallback ? `${uid}-table` : void 0;
2647
3278
  const { containerRef, tip, show, hide } = useChartTooltip();
3279
+ const [focus, setFocus] = React51__namespace.default.useState(null);
3280
+ const [selected, setSelected] = React51__namespace.default.useState(null);
2648
3281
  const rows = data || [];
2649
3282
  const keys = series && series.length ? series : ["value"];
2650
3283
  const fmt = valueFormat || fmtNumber;
2651
3284
  const multi = keys.length > 1;
3285
+ const clickable = !!onDataClick;
2652
3286
  const legend = showLegend != null ? showLegend : multi;
3287
+ const focusActive = (k) => focus == null ? void 0 : k === focus || void 0;
3288
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
2653
3289
  const H = height;
2654
3290
  const cx = H / 2, cy = H / 2;
2655
3291
  const R = Math.max(0, H / 2 - 40);
@@ -2663,69 +3299,106 @@ function RadarChart({
2663
3299
  const frac = Math.max(0, Math.min(1, (Number(v) || 0) / radialMax));
2664
3300
  return polarDeg(cx, cy, frac * R, angleOf(i));
2665
3301
  };
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
- ] });
3302
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3303
+ "div",
3304
+ {
3305
+ ref: containerRef,
3306
+ className: `twc-chart twc-chart--radar ${className}`.trim(),
3307
+ "data-hovering": focus != null || void 0,
3308
+ "data-clickable": clickable || void 0,
3309
+ "data-has-selection": selected != null || void 0,
3310
+ ...rest,
3311
+ children: [
3312
+ baseStyles,
3313
+ styles,
3314
+ /* @__PURE__ */ jsxRuntime.jsx(
3315
+ "svg",
3316
+ {
3317
+ viewBox: `0 0 ${H} ${H}`,
3318
+ role: "img",
3319
+ "aria-label": svgAriaLabel,
3320
+ "aria-describedby": tableId,
3321
+ style: { maxWidth: H, margin: "0 auto" },
3322
+ 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: [
3323
+ Array.from({ length: rings }, (_, li) => {
3324
+ const rr = R * (li + 1) / rings;
3325
+ const pts = rows.map((_2, i) => polarDeg(cx, cy, rr, angleOf(i)));
3326
+ return /* @__PURE__ */ jsxRuntime.jsx("path", { className: "twc-chart__ring", d: polygonPath(pts) }, li);
3327
+ }),
3328
+ rows.map((d, i) => {
3329
+ const [ex, ey] = polarDeg(cx, cy, R, angleOf(i));
3330
+ const [lx, ly] = polarDeg(cx, cy, R + 16, angleOf(i));
3331
+ const anchor = lx > cx + 1 ? "start" : lx < cx - 1 ? "end" : "middle";
3332
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
3333
+ /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-chart__spoke", x1: cx, y1: cy, x2: ex, y2: ey }),
3334
+ /* @__PURE__ */ jsxRuntime.jsx("text", { className: "twc-chart__radar-axis", x: lx, y: ly, textAnchor: anchor, dominantBaseline: "middle", children: d.label })
3335
+ ] }, i);
3336
+ }),
3337
+ keys.map((k, si) => {
3338
+ const color = paletteAt(colors, si);
3339
+ const pts = rows.map((d, i) => vertex(d[k], i));
3340
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { "data-mark": true, "data-active": focusActive(k), children: [
3341
+ fill ? /* @__PURE__ */ jsxRuntime.jsx("path", { className: "twc-chart__radar-area twc-chart__anim-fade", style: { fill: color, opacity: 0.18 }, d: polygonPath(pts) }) : null,
3342
+ /* @__PURE__ */ jsxRuntime.jsx("path", { className: "twc-chart__radar-line twc-chart__anim-fade", style: { stroke: color }, d: polygonPath(pts) }),
3343
+ showDots ? pts.map((p, i) => {
3344
+ const key = `${i}:${k}`;
3345
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "twc-chart__anim-fade", children: [
3346
+ /* @__PURE__ */ jsxRuntime.jsx(
3347
+ "circle",
3348
+ {
3349
+ className: "twc-chart__hit",
3350
+ cx: p[0],
3351
+ cy: p[1],
3352
+ r: "14",
3353
+ onMouseMove: (e) => show({ title: rows[i].label, items: [{ color, label: k, value: fmt(Number(rows[i][k]) || 0) }] }, e),
3354
+ onMouseLeave: hide,
3355
+ onClick: () => {
3356
+ if (onDataClick) onDataClick({ label: rows[i].label, series: k, value: Number(rows[i][k]) || 0, index: i });
3357
+ selectMark(key);
3358
+ }
3359
+ }
3360
+ ),
3361
+ /* @__PURE__ */ jsxRuntime.jsx(
3362
+ "circle",
3363
+ {
3364
+ className: "twc-chart__radar-dot",
3365
+ style: { fill: color },
3366
+ cx: p[0],
3367
+ cy: p[1],
3368
+ r: "3",
3369
+ "data-mark": true,
3370
+ "data-active": focusActive(k),
3371
+ "data-selected": selected === key || void 0
3372
+ }
3373
+ )
3374
+ ] }, i);
3375
+ }) : null
3376
+ ] }, k);
3377
+ })
3378
+ ] })
3379
+ }
3380
+ ),
3381
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { tip }),
3382
+ tableFallback ? /* @__PURE__ */ jsxRuntime.jsx(
3383
+ ChartTable,
3384
+ {
3385
+ id: tableId,
3386
+ caption: caption ?? svgAriaLabel,
3387
+ columns: keys,
3388
+ rows: rows.map((d) => ({ label: d.label, values: keys.map((k) => fmt(Number(d[k]) || 0)) }))
3389
+ }
3390
+ ) : null,
3391
+ legend && multi ? /* @__PURE__ */ jsxRuntime.jsx(
3392
+ ChartLegend,
3393
+ {
3394
+ items: keys.map((k, si) => ({ label: k, color: paletteAt(colors, si) })),
3395
+ onFocus: (it) => setFocus(it.label),
3396
+ onBlur: () => setFocus(null)
3397
+ }
3398
+ ) : null
3399
+ ]
3400
+ }
3401
+ );
2729
3402
  }
2730
3403
  var CHART_CSS4 = `
2731
3404
  .twc-chart--polar svg { margin-inline: auto; }
@@ -2744,6 +3417,7 @@ function PolarAreaChart({
2744
3417
  valueFormat,
2745
3418
  height = 280,
2746
3419
  colors,
3420
+ onDataClick,
2747
3421
  ariaLabel,
2748
3422
  "aria-label": ariaLabelProp,
2749
3423
  tableFallback = true,
@@ -2757,6 +3431,8 @@ function PolarAreaChart({
2757
3431
  const tableId = tableFallback ? `${uid}-table` : void 0;
2758
3432
  const { containerRef, tip, show, hide } = useChartTooltip();
2759
3433
  const [active, setActive] = React51__namespace.default.useState(null);
3434
+ const [focus, setFocus] = React51__namespace.default.useState(null);
3435
+ const [selected, setSelected] = React51__namespace.default.useState(null);
2760
3436
  const rows = data || [];
2761
3437
  const n = rows.length;
2762
3438
  const fmt = valueFormat || fmtNumber;
@@ -2771,14 +3447,23 @@ function PolarAreaChart({
2771
3447
  };
2772
3448
  const rings = Math.max(1, Math.floor(levels));
2773
3449
  const anglePer = 360 / Math.max(1, n);
3450
+ const EXPLODE = Math.min(12, Rmax * 0.1);
2774
3451
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "polar area chart";
3452
+ const clickable = !!onDataClick;
3453
+ const emphasis = focus != null ? focus : active;
2775
3454
  const labelText8 = (label) => typeof label === "string" || typeof label === "number" ? String(label) : "";
3455
+ const clickSlice = (d, v, i) => {
3456
+ if (onDataClick) onDataClick({ label: d.label, value: v, index: i });
3457
+ setSelected((s) => s === i ? null : i);
3458
+ };
2776
3459
  return /* @__PURE__ */ jsxRuntime.jsxs(
2777
3460
  "div",
2778
3461
  {
2779
3462
  ref: containerRef,
2780
3463
  className: `twc-chart twc-chart--polar ${className}`.trim(),
2781
- "data-hovering": active != null || void 0,
3464
+ "data-hovering": emphasis != null || void 0,
3465
+ "data-clickable": clickable || void 0,
3466
+ "data-has-selection": selected != null || void 0,
2782
3467
  ...rest,
2783
3468
  children: [
2784
3469
  baseStyles,
@@ -2807,13 +3492,16 @@ function PolarAreaChart({
2807
3492
  const start = startAngle + i * anglePer;
2808
3493
  const color = d.color || paletteAt(colors, i);
2809
3494
  const tipFor = { title: labelText8(d.label), items: [{ color, label: "", value: fmt(v) }] };
3495
+ const midRad = (start + anglePer / 2 - 90) * Math.PI / 180;
3496
+ const explode = selected === i ? `translate(${r(Math.cos(midRad) * EXPLODE)}px, ${r(Math.sin(midRad) * EXPLODE)}px)` : void 0;
2810
3497
  return /* @__PURE__ */ jsxRuntime.jsx(
2811
3498
  "path",
2812
3499
  {
2813
3500
  className: "twc-chart__slice twc-chart__anim-arc",
2814
3501
  "data-mark": true,
2815
- "data-active": active === i || void 0,
2816
- style: { fill: color, fillOpacity: 0.82 },
3502
+ "data-active": emphasis == null ? void 0 : emphasis === i || void 0,
3503
+ "data-selected": selected === i || void 0,
3504
+ style: { fill: color, fillOpacity: 0.82, transform: explode },
2817
3505
  d: arcPath(cx, cy, radiusFor(v), 0, start, start + anglePer),
2818
3506
  onMouseMove: (e) => {
2819
3507
  setActive(i);
@@ -2822,7 +3510,8 @@ function PolarAreaChart({
2822
3510
  onMouseLeave: () => {
2823
3511
  setActive(null);
2824
3512
  hide();
2825
- }
3513
+ },
3514
+ onClick: () => clickSlice(d, v, i)
2826
3515
  },
2827
3516
  `slice-${i}`
2828
3517
  );
@@ -2845,7 +3534,14 @@ function PolarAreaChart({
2845
3534
  rows: rows.map((d) => ({ label: d.label, values: [fmt(Number(d.value) || 0)] }))
2846
3535
  }
2847
3536
  ) : null,
2848
- showLegend && n ? /* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { items: rows.map((d, i) => ({ label: d.label, color: d.color || paletteAt(colors, i) })) }) : null
3537
+ showLegend && n ? /* @__PURE__ */ jsxRuntime.jsx(
3538
+ ChartLegend,
3539
+ {
3540
+ items: rows.map((d, i) => ({ label: d.label, color: d.color || paletteAt(colors, i) })),
3541
+ onFocus: n > 1 ? (_it, i) => setFocus(i) : void 0,
3542
+ onBlur: n > 1 ? () => setFocus(null) : void 0
3543
+ }
3544
+ ) : null
2849
3545
  ]
2850
3546
  }
2851
3547
  );
@@ -2855,7 +3551,7 @@ var HEATMAP_CSS = `
2855
3551
  .twc-chart__hm-cell { transition: opacity var(--duration-fast) var(--ease-standard); }
2856
3552
  .twc-chart__hm-cell:hover { opacity: 0.82; }
2857
3553
  .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; }
3554
+ .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
3555
  .twc-chart__hm-tick { fill: var(--color-text-subtle); font-size: 11px; }
2860
3556
  .twc-chart__hm-title { fill: var(--color-text-muted); font-size: 11px; font-weight: 500; }
2861
3557
  .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 +3569,10 @@ function Heatmap({
2873
3569
  xLabel,
2874
3570
  yLabel,
2875
3571
  showLegend = true,
3572
+ zoomable = false,
2876
3573
  valueFormat,
2877
3574
  height = 300,
3575
+ onDataClick,
2878
3576
  ariaLabel,
2879
3577
  "aria-label": ariaLabelProp,
2880
3578
  tableFallback = true,
@@ -2888,10 +3586,17 @@ function Heatmap({
2888
3586
  const tableId = tableFallback ? `${uid}-table` : void 0;
2889
3587
  const { containerRef, tip, show, hide } = useChartTooltip();
2890
3588
  const [hovered, setHovered] = React51__namespace.default.useState(null);
3589
+ const [selected, setSelected] = React51__namespace.default.useState(null);
3590
+ const [zoom, setZoom] = React51__namespace.default.useState(null);
3591
+ const [drag, setDrag] = React51__namespace.default.useState(null);
3592
+ const svgRef = React51__namespace.default.useRef(null);
3593
+ const movedRef = React51__namespace.default.useRef(false);
3594
+ const clickable = !!onDataClick;
3595
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
2891
3596
  const rows = Array.isArray(data) ? data : [];
2892
3597
  const fmt = valueFormat || fmtNumber;
2893
- const xs = [];
2894
- const ys = [];
3598
+ const fullXs = [];
3599
+ const fullYs = [];
2895
3600
  const seenX = /* @__PURE__ */ new Set();
2896
3601
  const seenY = /* @__PURE__ */ new Set();
2897
3602
  const lookup = /* @__PURE__ */ new Map();
@@ -2900,11 +3605,11 @@ function Heatmap({
2900
3605
  const ky = String(d.y);
2901
3606
  if (!seenX.has(kx)) {
2902
3607
  seenX.add(kx);
2903
- xs.push(d.x);
3608
+ fullXs.push(d.x);
2904
3609
  }
2905
3610
  if (!seenY.has(ky)) {
2906
3611
  seenY.add(ky);
2907
- ys.push(d.y);
3612
+ fullYs.push(d.y);
2908
3613
  }
2909
3614
  lookup.set(`${kx}\0${ky}`, Number(d.value) || 0);
2910
3615
  }
@@ -2917,6 +3622,15 @@ function Heatmap({
2917
3622
  const span = hi - lo;
2918
3623
  const pctOf = (v) => span > 0 ? clamp((v - lo) / span * 100, 0, 100) : 60;
2919
3624
  const fillOf = (v) => `color-mix(in srgb, ${colorScale} ${pctOf(v)}%, transparent)`;
3625
+ const nX = fullXs.length;
3626
+ const nY = fullYs.length;
3627
+ const canZoom = zoomable && nX > 1 && nY > 1;
3628
+ const vc0 = canZoom && zoom ? clamp(zoom.c0, 0, nX - 1) : 0;
3629
+ const vc1 = canZoom && zoom ? clamp(zoom.c1, 0, nX - 1) : Math.max(0, nX - 1);
3630
+ const vr0 = canZoom && zoom ? clamp(zoom.r0, 0, nY - 1) : 0;
3631
+ const vr1 = canZoom && zoom ? clamp(zoom.r1, 0, nY - 1) : Math.max(0, nY - 1);
3632
+ const xs = fullXs.slice(vc0, vc1 + 1);
3633
+ const ys = fullYs.slice(vr0, vr1 + 1);
2920
3634
  const cols = xs.length;
2921
3635
  const rowN = ys.length;
2922
3636
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "heatmap";
@@ -2935,10 +3649,137 @@ function Heatmap({
2935
3649
  const cw = innerW / Math.max(1, cols);
2936
3650
  const chh = innerH / Math.max(1, rowN);
2937
3651
  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: [
3652
+ const svgPoint = (e) => {
3653
+ const svg = svgRef.current;
3654
+ if (!svg || typeof svg.getScreenCTM !== "function" || typeof svg.createSVGPoint !== "function") return null;
3655
+ const ctm = svg.getScreenCTM();
3656
+ if (!ctm || !ctm.a || !ctm.d) return null;
3657
+ const pt = svg.createSVGPoint();
3658
+ pt.x = e.clientX;
3659
+ pt.y = e.clientY;
3660
+ const loc = pt.matrixTransform(ctm.inverse());
3661
+ if (!Number.isFinite(loc.x) || !Number.isFinite(loc.y)) return null;
3662
+ return { px: loc.x, py: loc.y };
3663
+ };
3664
+ const colAtPx = (px) => cw > 0 ? clamp(Math.floor((clamp(px, padL, padL + innerW) - padL) / cw), 0, cols - 1) : 0;
3665
+ const rowAtPx = (py) => chh > 0 ? clamp(Math.floor((clamp(py, padT, padT + innerH) - padT) / chh), 0, rowN - 1) : 0;
3666
+ const cellAtPx = (px, py) => {
3667
+ if (!(cols > 0) || !(rowN > 0)) return null;
3668
+ if (px < padL || px > padL + innerW || py < padT || py > padT + innerH) return null;
3669
+ const ci = colAtPx(px);
3670
+ const ri = rowAtPx(py);
3671
+ const x = xs[ci], y = ys[ri];
3672
+ const v = cellOf(x, y);
3673
+ const has = v !== void 0;
3674
+ return { x, y, v, has, key: `${vc0 + ci}-${vr0 + ri}`, fill: has ? fillOf(v) : void 0 };
3675
+ };
3676
+ const applyZoom = (win) => {
3677
+ const c0 = clamp(Math.min(win.c0, win.c1), 0, nX - 1);
3678
+ const c1 = clamp(Math.max(win.c0, win.c1), 0, nX - 1);
3679
+ const r0 = clamp(Math.min(win.r0, win.r1), 0, nY - 1);
3680
+ const r1 = clamp(Math.max(win.r0, win.r1), 0, nY - 1);
3681
+ if (!(c1 >= c0) || !(r1 >= r0)) return;
3682
+ if (c0 === 0 && c1 === nX - 1 && r0 === 0 && r1 === nY - 1) {
3683
+ setZoom(null);
3684
+ return;
3685
+ }
3686
+ setZoom({ c0, c1, r0, r1 });
3687
+ };
3688
+ const panTo = (z0, dCols, dRows) => {
3689
+ const wc = z0.c1 - z0.c0, wr = z0.r1 - z0.r0;
3690
+ const c0 = clamp(z0.c0 + dCols, 0, Math.max(0, nX - 1 - wc));
3691
+ const r0 = clamp(z0.r0 + dRows, 0, Math.max(0, nY - 1 - wr));
3692
+ setZoom({ c0, c1: c0 + wc, r0, r1: r0 + wr });
3693
+ };
3694
+ const onOverlayDown = (e) => {
3695
+ const pt = svgPoint(e);
3696
+ if (!pt) return;
3697
+ movedRef.current = false;
3698
+ 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 } });
3699
+ };
3700
+ const onOverlayMove = (e) => {
3701
+ const pt = svgPoint(e);
3702
+ if (!pt) return;
3703
+ if (drag) {
3704
+ if (Math.abs(pt.px - drag.x0) > 4 || Math.abs(pt.py - drag.y0) > 4) movedRef.current = true;
3705
+ if (drag.pan) {
3706
+ if (zoom) {
3707
+ const dCols = cw > 0 ? -Math.round((pt.px - drag.x0) / cw) : 0;
3708
+ const dRows = chh > 0 ? -Math.round((pt.py - drag.y0) / chh) : 0;
3709
+ panTo(drag.z0, dCols, dRows);
3710
+ }
3711
+ } else {
3712
+ setDrag({ ...drag, x1: pt.px, y1: pt.py });
3713
+ }
3714
+ setHovered(null);
3715
+ hide();
3716
+ return;
3717
+ }
3718
+ const cell = cellAtPx(pt.px, pt.py);
3719
+ if (cell) {
3720
+ setHovered(cell.key);
3721
+ show({ title: `${labelText3(cell.y)} / ${labelText3(cell.x)}`, items: [{ color: cell.fill, label: "", value: cell.has ? fmt(cell.v) : "\u2014" }] }, e);
3722
+ } else {
3723
+ setHovered(null);
3724
+ hide();
3725
+ }
3726
+ };
3727
+ const onOverlayUp = () => {
3728
+ if (drag && !drag.pan && movedRef.current) {
3729
+ applyZoom({
3730
+ c0: vc0 + colAtPx(drag.x0),
3731
+ c1: vc0 + colAtPx(drag.x1),
3732
+ r0: vr0 + rowAtPx(drag.y0),
3733
+ r1: vr0 + rowAtPx(drag.y1)
3734
+ });
3735
+ }
3736
+ setDrag(null);
3737
+ };
3738
+ const onOverlayLeave = () => {
3739
+ setHovered(null);
3740
+ hide();
3741
+ setDrag(null);
3742
+ };
3743
+ const onOverlayClick = (e) => {
3744
+ if (movedRef.current) {
3745
+ movedRef.current = false;
3746
+ return;
3747
+ }
3748
+ const pt = svgPoint(e);
3749
+ if (!pt) return;
3750
+ const cell = cellAtPx(pt.px, pt.py);
3751
+ if (cell && cell.has) {
3752
+ if (onDataClick) onDataClick({ x: cell.x, y: cell.y, value: cell.v });
3753
+ selectMark(cell.key);
3754
+ }
3755
+ };
3756
+ React51__namespace.default.useEffect(() => {
3757
+ if (!canZoom) return void 0;
3758
+ const svg = svgRef.current;
3759
+ if (!svg) return void 0;
3760
+ const onWheel = (e) => {
3761
+ e.preventDefault();
3762
+ const pt = svgPoint(e);
3763
+ const fx = pt && innerW > 0 ? clamp((pt.px - padL) / innerW, 0, 1) : 0.5;
3764
+ const fy = pt && innerH > 0 ? clamp((pt.py - padT) / innerH, 0, 1) : 0.5;
3765
+ const factor = e.deltaY < 0 ? 0.7 : 1.4;
3766
+ const wc = vc1 - vc0 + 1, wr = vr1 - vr0 + 1;
3767
+ const newWc = clamp(Math.round(wc * factor), 1, nX);
3768
+ const newWr = clamp(Math.round(wr * factor), 1, nY);
3769
+ const curCol = vc0 + clamp(Math.floor(fx * cols), 0, cols - 1);
3770
+ const curRow = vr0 + clamp(Math.floor(fy * rowN), 0, rowN - 1);
3771
+ const nc0 = clamp(Math.round(curCol - fx * (newWc - 1)), 0, nX - newWc);
3772
+ const nr0 = clamp(Math.round(curRow - fy * (newWr - 1)), 0, nY - newWr);
3773
+ applyZoom({ c0: nc0, c1: nc0 + newWc - 1, r0: nr0, r1: nr0 + newWr - 1 });
3774
+ };
3775
+ svg.addEventListener("wheel", onWheel, { passive: false });
3776
+ return () => svg.removeEventListener("wheel", onWheel);
3777
+ }, [canZoom, vc0, vc1, vr0, vr1]);
3778
+ const dragBand = canZoom && drag && !drag.pan && Math.abs(drag.x1 - drag.x0) > 2 && Math.abs(drag.y1 - drag.y0) > 2;
3779
+ 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: [
2939
3780
  baseStyles,
2940
3781
  styles,
2941
- /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
3782
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
2942
3783
  ys.map(
2943
3784
  (y, ri) => xs.map((x, ci) => {
2944
3785
  const v = cellOf(x, y);
@@ -2947,7 +3788,7 @@ function Heatmap({
2947
3788
  const cy = padT + chh * ri + gap / 2;
2948
3789
  const cwPx = Math.max(0, cw - gap);
2949
3790
  const chPx = Math.max(0, chh - gap);
2950
- const cellKey = `${ci}-${ri}`;
3791
+ const cellKey = `${vc0 + ci}-${vr0 + ri}`;
2951
3792
  const fill = has ? fillOf(v) : void 0;
2952
3793
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
2953
3794
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2962,6 +3803,7 @@ function Heatmap({
2962
3803
  style: has ? { fill } : void 0,
2963
3804
  "data-mark": true,
2964
3805
  "data-active": hovered === cellKey || void 0,
3806
+ "data-selected": selected === cellKey || void 0,
2965
3807
  onMouseMove: (e) => {
2966
3808
  setHovered(cellKey);
2967
3809
  show({ title: `${labelText3(y)} / ${labelText3(x)}`, items: [{ color: fill, label: "", value: has ? fmt(v) : "\u2014" }] }, e);
@@ -2969,7 +3811,11 @@ function Heatmap({
2969
3811
  onMouseLeave: () => {
2970
3812
  setHovered(null);
2971
3813
  hide();
2972
- }
3814
+ },
3815
+ onClick: has ? () => {
3816
+ if (onDataClick) onDataClick({ x, y, value: v });
3817
+ selectMark(cellKey);
3818
+ } : void 0
2973
3819
  }
2974
3820
  ),
2975
3821
  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 +3825,43 @@ function Heatmap({
2979
3825
  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
3826
  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
3827
  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
3828
+ xLabel != null ? /* @__PURE__ */ jsxRuntime.jsx("text", { className: "twc-chart__hm-title", x: padL + innerW / 2, y: H - 3, textAnchor: "middle", children: labelText3(xLabel) }) : null,
3829
+ dragBand ? /* @__PURE__ */ jsxRuntime.jsx(
3830
+ "rect",
3831
+ {
3832
+ className: "twc-chart__zoomband",
3833
+ x: Math.min(drag.x0, drag.x1),
3834
+ y: Math.min(drag.y0, drag.y1),
3835
+ width: Math.abs(drag.x1 - drag.x0),
3836
+ height: Math.abs(drag.y1 - drag.y0)
3837
+ }
3838
+ ) : null,
3839
+ canZoom ? /* @__PURE__ */ jsxRuntime.jsx(
3840
+ "rect",
3841
+ {
3842
+ className: "twc-chart__overlay",
3843
+ "data-zoom": "true",
3844
+ "data-clickable": clickable || void 0,
3845
+ "data-panning": drag && drag.pan || void 0,
3846
+ x: padL,
3847
+ y: padT,
3848
+ width: Math.max(0, innerW),
3849
+ height: Math.max(0, innerH),
3850
+ onMouseDown: onOverlayDown,
3851
+ onMouseMove: onOverlayMove,
3852
+ onMouseUp: onOverlayUp,
3853
+ onMouseLeave: onOverlayLeave,
3854
+ onClick: onOverlayClick
3855
+ }
3856
+ ) : null
2983
3857
  ] }),
3858
+ canZoom && zoom ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
3859
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3860
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
3861
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
3862
+ ] }),
3863
+ "Reset"
3864
+ ] }) : null,
2984
3865
  /* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { tip }),
2985
3866
  showLegend && span > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-chart__hm-scale", "aria-hidden": "true", children: [
2986
3867
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: fmt(lo) }),
@@ -2998,10 +3879,10 @@ function Heatmap({
2998
3879
  {
2999
3880
  id: tableId,
3000
3881
  caption: caption ?? svgAriaLabel,
3001
- columns: xs.map(labelText3),
3002
- rows: ys.map((y) => ({
3882
+ columns: fullXs.map(labelText3),
3883
+ rows: fullYs.map((y) => ({
3003
3884
  label: labelText3(y),
3004
- values: xs.map((x) => {
3885
+ values: fullXs.map((x) => {
3005
3886
  const v = cellOf(x, y);
3006
3887
  return v === void 0 ? "\u2014" : fmt(v);
3007
3888
  })
@@ -3015,9 +3896,13 @@ function labelText3(label) {
3015
3896
  }
3016
3897
  var FUNNEL_CSS = `
3017
3898
  .twc-chart--funnel svg { overflow: visible; }
3018
- .twc-funnel__label { fill: var(--color-text-inverted); font-family: var(--font-sans); pointer-events: none; }
3899
+ .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
3900
  .twc-funnel__name { font-size: 12px; font-weight: 600; }
3020
- .twc-funnel__value { font-size: 11px; fill-opacity: 0.82; }
3901
+ .twc-funnel__value { font-size: 11px; fill-opacity: 0.9; }
3902
+ /* Explode: nudge the selected stage shape outward so it separates from the funnel
3903
+ (eases via the shared [data-mark] transform transition in _chart.js). */
3904
+ .twc-chart--funnel [data-mark][data-selected="true"] { transform: translateX(14px); }
3905
+ .twc-chart--funnel[data-orientation="horizontal"] [data-mark][data-selected="true"] { transform: translateY(-14px); }
3021
3906
  `;
3022
3907
  function FunnelChart({
3023
3908
  data,
@@ -3028,6 +3913,7 @@ function FunnelChart({
3028
3913
  height = 300,
3029
3914
  colors,
3030
3915
  valueFormat,
3916
+ onDataClick,
3031
3917
  ariaLabel,
3032
3918
  "aria-label": ariaLabelProp,
3033
3919
  tableFallback = true,
@@ -3041,6 +3927,8 @@ function FunnelChart({
3041
3927
  const tableId = tableFallback ? `${uid}-table` : void 0;
3042
3928
  const { containerRef, tip, show, hide } = useChartTooltip();
3043
3929
  const [active, setActive] = React51__namespace.default.useState(null);
3930
+ const [selected, setSelected] = React51__namespace.default.useState(null);
3931
+ const clickable = !!onDataClick;
3044
3932
  const rows = data || [];
3045
3933
  const n = rows.length;
3046
3934
  const fmt = valueFormat || fmtNumber;
@@ -3092,6 +3980,7 @@ function FunnelChart({
3092
3980
  fill: color,
3093
3981
  "data-mark": true,
3094
3982
  "data-active": active === i ? "true" : void 0,
3983
+ "data-selected": selected === i ? "true" : void 0,
3095
3984
  onMouseMove: (e) => {
3096
3985
  setActive(i);
3097
3986
  show({ title: d.label, items: [{ color, label: fmt(v), value: pctText(v) }] }, e);
@@ -3099,6 +3988,10 @@ function FunnelChart({
3099
3988
  onMouseLeave: () => {
3100
3989
  setActive(null);
3101
3990
  hide();
3991
+ },
3992
+ onClick: () => {
3993
+ if (onDataClick) onDataClick({ label: d.label, value: v, index: i, percent: pct(v) });
3994
+ setSelected((s) => s === i ? null : i);
3102
3995
  }
3103
3996
  }
3104
3997
  ),
@@ -3114,7 +4007,7 @@ function FunnelChart({
3114
4007
  )) }) : null
3115
4008
  ] }, i);
3116
4009
  });
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: [
4010
+ 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
4011
  baseStyles,
3119
4012
  styles,
3120
4013
  /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: renderStages() }),
@@ -3131,8 +4024,8 @@ function FunnelChart({
3131
4024
  ] });
3132
4025
  }
3133
4026
  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; }
4027
+ .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; }
4028
+ .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
4029
  .twc-chart__empty { fill: var(--color-text-subtle); font-size: var(--text-sm); }
3137
4030
  `;
3138
4031
  function Treemap({
@@ -3142,6 +4035,7 @@ function Treemap({
3142
4035
  height = 300,
3143
4036
  colors,
3144
4037
  gap = 2,
4038
+ onDataClick,
3145
4039
  ariaLabel,
3146
4040
  "aria-label": ariaLabelProp,
3147
4041
  tableFallback = true,
@@ -3155,16 +4049,18 @@ function Treemap({
3155
4049
  const tableId = tableFallback ? `${uid}-table` : void 0;
3156
4050
  const { containerRef, tip, show, hide } = useChartTooltip();
3157
4051
  const [hovered, setHovered] = React51__namespace.default.useState(null);
4052
+ const [selected, setSelected] = React51__namespace.default.useState(null);
3158
4053
  const rows = data || [];
3159
4054
  const fmt = valueFormat || fmtNumber;
3160
4055
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "treemap";
4056
+ const clickable = !!onDataClick;
3161
4057
  const W = 600, H = height;
3162
4058
  const weights = rows.map((d) => Math.max(0, Number(d.value) || 0));
3163
4059
  const total = sum(weights);
3164
4060
  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
4061
  const tiles = squarify(items, { x: 0, y: 0, w: W, h: H });
3166
4062
  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: [
4063
+ 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
4064
  baseStyles,
3169
4065
  styles,
3170
4066
  /* @__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 +4083,7 @@ function Treemap({
3187
4083
  className: "twc-chart__tile twc-chart__anim-fade",
3188
4084
  "data-mark": true,
3189
4085
  "data-active": hovered === t.i || void 0,
4086
+ "data-selected": selected === t.i || void 0,
3190
4087
  x,
3191
4088
  y,
3192
4089
  width: w,
@@ -3200,6 +4097,10 @@ function Treemap({
3200
4097
  onMouseLeave: () => {
3201
4098
  setHovered(null);
3202
4099
  hide();
4100
+ },
4101
+ onClick: () => {
4102
+ if (onDataClick) onDataClick({ label: d.label, value: t.w, index: t.i, share: total > 0 ? t.w / total : 0 });
4103
+ setSelected((s) => s === t.i ? null : t.i);
3203
4104
  }
3204
4105
  }
3205
4106
  ),
@@ -3294,123 +4195,290 @@ function Candlestick({
3294
4195
  height = 300,
3295
4196
  showGrid = true,
3296
4197
  showAxis = true,
4198
+ zoomable = false,
3297
4199
  valueFormat,
4200
+ onDataClick,
3298
4201
  ariaLabel,
3299
4202
  "aria-label": ariaLabelProp,
3300
4203
  tableFallback = true,
3301
- caption,
3302
- className = "",
3303
- ...rest
3304
- }) {
3305
- const baseStyles = useScopedStyles("twc-chart-base", CHART_BASE_CSS);
3306
- const styles = useScopedStyles("twc-candlestick-styles", CANDLE_CSS);
3307
- const uid = React51__namespace.default.useId();
3308
- const tableId = tableFallback ? `${uid}-table` : void 0;
3309
- const { containerRef, tip, show, hide } = useChartTooltip();
3310
- const rows = data || [];
3311
- const fmt = valueFormat || fmtNumber;
3312
- const W = 600, H = height;
3313
- const padL = showAxis ? 44 : 8;
3314
- const padR = 10;
3315
- const padT = 12;
3316
- const padB = showAxis ? 26 : 8;
3317
- const innerW = W - padL - padR, innerH = H - padT - padB;
3318
- const lows = rows.map((d) => Number(d.low)).filter(Number.isFinite);
3319
- const highs = rows.map((d) => Number(d.high)).filter(Number.isFinite);
3320
- const minLow = lows.length ? Math.min(...lows) : 0;
3321
- const maxHigh = highs.length ? Math.max(...highs) : 1;
3322
- const scale = niceScale(minLow, maxHigh, 5);
3323
- const span = scale.max - scale.min || 1;
3324
- const vPos = (v) => padT + innerH - (v - scale.min) / span * innerH;
3325
- const catBand = innerW / Math.max(1, rows.length);
3326
- const cx = (i) => padL + catBand * i + catBand / 2;
3327
- const bodyW = Math.max(1, Math.min(catBand * 0.7, 22));
3328
- const labelStep = Math.max(1, Math.ceil(rows.length / 12));
3329
- 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
- }))
4204
+ caption,
4205
+ className = "",
4206
+ ...rest
4207
+ }) {
4208
+ const baseStyles = useScopedStyles("twc-chart-base", CHART_BASE_CSS);
4209
+ const styles = useScopedStyles("twc-candlestick-styles", CANDLE_CSS);
4210
+ const uid = React51__namespace.default.useId();
4211
+ const tableId = tableFallback ? `${uid}-table` : void 0;
4212
+ const { containerRef, tip, show, hide } = useChartTooltip();
4213
+ const [selected, setSelected] = React51__namespace.default.useState(null);
4214
+ const [zoom, setZoom] = React51__namespace.default.useState(null);
4215
+ const [drag, setDrag] = React51__namespace.default.useState(null);
4216
+ const svgRef = React51__namespace.default.useRef(null);
4217
+ const fmt = valueFormat || fmtNumber;
4218
+ const clickable = !!onDataClick;
4219
+ const allRows = data || [];
4220
+ const baseIdx = zoom ? zoom.s : 0;
4221
+ const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
4222
+ const W = 600, H = height;
4223
+ const padL = showAxis ? 44 : 8;
4224
+ const padR = 10;
4225
+ const padT = 12;
4226
+ const padB = showAxis ? 26 : 8;
4227
+ const innerW = W - padL - padR, innerH = H - padT - padB;
4228
+ const lows = rows.map((d) => Number(d.low)).filter(Number.isFinite);
4229
+ const highs = rows.map((d) => Number(d.high)).filter(Number.isFinite);
4230
+ const minLow = lows.length ? Math.min(...lows) : 0;
4231
+ const maxHigh = highs.length ? Math.max(...highs) : 1;
4232
+ const scale = niceScale(minLow, maxHigh, 5);
4233
+ const span = scale.max - scale.min || 1;
4234
+ const vPos = (v) => padT + innerH - (v - scale.min) / span * innerH;
4235
+ const catBand = innerW / Math.max(1, rows.length);
4236
+ const cx = (i) => padL + catBand * i + catBand / 2;
4237
+ const bodyW = Math.max(1, Math.min(catBand * 0.7, 22));
4238
+ const labelStep = Math.max(1, Math.ceil(rows.length / 12));
4239
+ const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "candlestick chart";
4240
+ const tipFor = (d) => {
4241
+ const open = Number(d.open) || 0, high = Number(d.high) || 0;
4242
+ const low = Number(d.low) || 0, close = Number(d.close) || 0;
4243
+ const color = close >= open ? upColor : downColor;
4244
+ return {
4245
+ title: labelText5(d.label),
4246
+ items: [
4247
+ { color, label: "Open", value: fmt(open) },
4248
+ { label: "High", value: fmt(high) },
4249
+ { label: "Low", value: fmt(low) },
4250
+ { label: "Close", value: fmt(close) }
4251
+ ]
4252
+ };
4253
+ };
4254
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
4255
+ const clickCandle = (d, index) => {
4256
+ if (!onDataClick) return;
4257
+ onDataClick({
4258
+ label: d.label,
4259
+ open: Number(d.open) || 0,
4260
+ high: Number(d.high) || 0,
4261
+ low: Number(d.low) || 0,
4262
+ close: Number(d.close) || 0,
4263
+ index
4264
+ });
4265
+ };
4266
+ const applyZoom = (s, e) => {
4267
+ const n = allRows.length;
4268
+ if (e - s >= n - 1) {
4269
+ setZoom(null);
4270
+ return;
4271
+ }
4272
+ setZoom({ s: Math.max(0, s), e: Math.min(n - 1, e) });
4273
+ };
4274
+ const catFromEvent = (e) => {
4275
+ const svg = svgRef.current;
4276
+ if (!svg) return null;
4277
+ if (!rows.length) return null;
4278
+ const r2 = svg.getBoundingClientRect();
4279
+ if (!(r2.width > 0)) return null;
4280
+ const frac = Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW));
4281
+ const idx = Math.floor(frac * rows.length);
4282
+ return Math.min(rows.length - 1, Math.max(0, idx));
4283
+ };
4284
+ const onOverlayMove = (e) => {
4285
+ const idx = catFromEvent(e);
4286
+ if (idx == null || !rows[idx]) return;
4287
+ show(tipFor(rows[idx]), e);
4288
+ if (drag) {
4289
+ if (drag.pan) {
4290
+ const spanZ = drag.z0.e - drag.z0.s;
4291
+ const s = Math.min(allRows.length - 1 - spanZ, Math.max(0, drag.z0.s + (drag.start - idx)));
4292
+ applyZoom(s, s + spanZ);
4293
+ } else {
4294
+ setDrag({ ...drag, end: idx });
3411
4295
  }
3412
- ) : null
3413
- ] });
4296
+ }
4297
+ };
4298
+ const onOverlayDown = (e) => {
4299
+ const idx = catFromEvent(e);
4300
+ if (idx == null) return;
4301
+ setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
4302
+ };
4303
+ const onOverlayUp = () => {
4304
+ if (drag && !drag.pan) {
4305
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
4306
+ if (hi - lo >= 1) applyZoom(baseIdx + lo, baseIdx + hi);
4307
+ }
4308
+ setDrag(null);
4309
+ };
4310
+ const onOverlayClick = (e) => {
4311
+ if (!onDataClick) return;
4312
+ const idx = catFromEvent(e);
4313
+ if (idx == null || !rows[idx]) return;
4314
+ clickCandle(rows[idx], baseIdx + idx);
4315
+ selectMark(baseIdx + idx);
4316
+ };
4317
+ React51__namespace.default.useEffect(() => {
4318
+ if (!zoomable) return void 0;
4319
+ const svg = svgRef.current;
4320
+ if (!svg) return void 0;
4321
+ const onWheel = (e) => {
4322
+ const n = allRows.length;
4323
+ if (!n) return;
4324
+ e.preventDefault();
4325
+ const cur = zoom || { s: 0, e: n - 1 };
4326
+ const spanZ = cur.e - cur.s;
4327
+ const r2 = svg.getBoundingClientRect();
4328
+ const frac = r2.width > 0 ? Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW)) : 0.5;
4329
+ const center = cur.s + Math.round(frac * spanZ);
4330
+ const newSpan = Math.min(n - 1, Math.max(1, Math.round(spanZ * (e.deltaY < 0 ? 0.8 : 1.25))));
4331
+ const s = Math.min(n - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frac)));
4332
+ applyZoom(s, s + newSpan);
4333
+ };
4334
+ svg.addEventListener("wheel", onWheel, { passive: false });
4335
+ return () => svg.removeEventListener("wheel", onWheel);
4336
+ }, [zoomable, zoom, allRows.length]);
4337
+ const renderZones = () => rows.map((d, i) => {
4338
+ const gi = baseIdx + i;
4339
+ return /* @__PURE__ */ jsxRuntime.jsx(
4340
+ "rect",
4341
+ {
4342
+ className: "twc-chart__zone",
4343
+ x: padL + catBand * i,
4344
+ y: padT,
4345
+ width: catBand,
4346
+ height: innerH,
4347
+ "data-clickable": clickable || void 0,
4348
+ onMouseMove: (e) => show(tipFor(d), e),
4349
+ onMouseLeave: hide,
4350
+ onClick: () => {
4351
+ if (onDataClick) clickCandle(d, gi);
4352
+ }
4353
+ },
4354
+ i
4355
+ );
4356
+ });
4357
+ const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
4358
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4359
+ "div",
4360
+ {
4361
+ ref: containerRef,
4362
+ className: `twc-chart twc-chart--candlestick ${className}`.trim(),
4363
+ "data-clickable": clickable || void 0,
4364
+ "data-has-selection": selected != null || void 0,
4365
+ ...rest,
4366
+ children: [
4367
+ baseStyles,
4368
+ styles,
4369
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
4370
+ showGrid ? scale.ticks.map((t, i) => {
4371
+ const p = vPos(t);
4372
+ return /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-candlestick__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
4373
+ }) : null,
4374
+ 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,
4375
+ !zoomable ? renderZones() : null,
4376
+ rows.map((d, i) => {
4377
+ const open = Number(d.open) || 0;
4378
+ const high = Number(d.high) || 0;
4379
+ const low = Number(d.low) || 0;
4380
+ const close = Number(d.close) || 0;
4381
+ const up = close >= open;
4382
+ const color = up ? upColor : downColor;
4383
+ const x = cx(i);
4384
+ const gi = baseIdx + i;
4385
+ const yTop = vPos(Math.max(open, close));
4386
+ const yBot = vPos(Math.min(open, close));
4387
+ const bh = Math.max(1, yBot - yTop);
4388
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4389
+ "g",
4390
+ {
4391
+ className: "twc-chart__anim-fade",
4392
+ "data-dir": up ? "up" : "down",
4393
+ onMouseMove: (e) => show(tipFor(d), e),
4394
+ onMouseLeave: hide,
4395
+ onClick: () => {
4396
+ clickCandle(d, gi);
4397
+ selectMark(gi);
4398
+ },
4399
+ children: [
4400
+ /* @__PURE__ */ jsxRuntime.jsx(
4401
+ "line",
4402
+ {
4403
+ className: "twc-candlestick__wick",
4404
+ style: { stroke: color },
4405
+ x1: x,
4406
+ y1: vPos(high),
4407
+ x2: x,
4408
+ y2: vPos(low)
4409
+ }
4410
+ ),
4411
+ /* @__PURE__ */ jsxRuntime.jsx(
4412
+ "rect",
4413
+ {
4414
+ className: "twc-candlestick__body",
4415
+ style: { fill: color },
4416
+ "data-mark": true,
4417
+ "data-selected": selected === gi || void 0,
4418
+ x: x - bodyW / 2,
4419
+ y: yTop,
4420
+ width: bodyW,
4421
+ height: bh,
4422
+ rx: "1"
4423
+ }
4424
+ )
4425
+ ]
4426
+ },
4427
+ i
4428
+ );
4429
+ }),
4430
+ showAxis ? rows.map(
4431
+ (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
4432
+ ) : null,
4433
+ dragBand ? (() => {
4434
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
4435
+ const x0 = padL + catBand * lo;
4436
+ const x1 = padL + catBand * (hi + 1);
4437
+ return /* @__PURE__ */ jsxRuntime.jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
4438
+ })() : null,
4439
+ zoomable ? /* @__PURE__ */ jsxRuntime.jsx(
4440
+ "rect",
4441
+ {
4442
+ className: "twc-chart__overlay",
4443
+ "data-clickable": clickable || void 0,
4444
+ x: padL,
4445
+ y: padT,
4446
+ width: innerW,
4447
+ height: innerH,
4448
+ onMouseMove: onOverlayMove,
4449
+ onMouseLeave: () => {
4450
+ hide();
4451
+ setDrag(null);
4452
+ },
4453
+ onMouseDown: onOverlayDown,
4454
+ onMouseUp: onOverlayUp,
4455
+ onClick: onOverlayClick
4456
+ }
4457
+ ) : null
4458
+ ] }),
4459
+ zoomable && zoom ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
4460
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
4461
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
4462
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
4463
+ ] }),
4464
+ "Reset"
4465
+ ] }) : null,
4466
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { tip }),
4467
+ tableFallback ? /* @__PURE__ */ jsxRuntime.jsx(
4468
+ ChartTable,
4469
+ {
4470
+ id: tableId,
4471
+ caption: caption ?? svgAriaLabel,
4472
+ columns: ["open", "high", "low", "close"],
4473
+ rows: allRows.map((d) => ({
4474
+ label: d.label,
4475
+ values: [Number(d.open) || 0, Number(d.high) || 0, Number(d.low) || 0, Number(d.close) || 0].map(fmt)
4476
+ }))
4477
+ }
4478
+ ) : null
4479
+ ]
4480
+ }
4481
+ );
3414
4482
  }
3415
4483
  function labelText5(label) {
3416
4484
  return typeof label === "string" || typeof label === "number" ? String(label) : "";
@@ -3430,7 +4498,9 @@ function Boxplot({
3430
4498
  height = 300,
3431
4499
  showGrid = true,
3432
4500
  showAxis = true,
4501
+ zoomable = false,
3433
4502
  valueFormat,
4503
+ onDataClick,
3434
4504
  ariaLabel,
3435
4505
  "aria-label": ariaLabelProp,
3436
4506
  tableFallback = true,
@@ -3443,9 +4513,28 @@ function Boxplot({
3443
4513
  const uid = React51__namespace.default.useId();
3444
4514
  const tableId = tableFallback ? `${uid}-table` : void 0;
3445
4515
  const { containerRef, tip, show, hide } = useChartTooltip();
3446
- const rows = data || [];
4516
+ const [selected, setSelected] = React51__namespace.default.useState(null);
4517
+ const [zoom, setZoom] = React51__namespace.default.useState(null);
4518
+ const [drag, setDrag] = React51__namespace.default.useState(null);
4519
+ const svgRef = React51__namespace.default.useRef(null);
4520
+ const allRows = data || [];
4521
+ const baseIdx = zoom ? zoom.s : 0;
4522
+ const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
3447
4523
  const fmt = valueFormat || fmtNumber;
3448
4524
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "box plot";
4525
+ const clickable = !!onDataClick;
4526
+ const clickBox = (d, i) => {
4527
+ if (onDataClick) onDataClick({
4528
+ label: d.label,
4529
+ min: Number(d.min),
4530
+ q1: Number(d.q1),
4531
+ median: Number(d.median),
4532
+ q3: Number(d.q3),
4533
+ max: Number(d.max),
4534
+ index: baseIdx + i
4535
+ });
4536
+ setSelected((s) => s === i ? null : i);
4537
+ };
3449
4538
  const tipFor = (d) => ({
3450
4539
  title: labelText6(d.label),
3451
4540
  items: [
@@ -3478,69 +4567,184 @@ function Boxplot({
3478
4567
  const band = innerW / Math.max(1, rows.length);
3479
4568
  const bandX = (i) => padL + band * i;
3480
4569
  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(
4570
+ const applyZoom = (s, e) => {
4571
+ const n = allRows.length;
4572
+ if (e - s >= n - 1) {
4573
+ setZoom(null);
4574
+ return;
4575
+ }
4576
+ setZoom({ s: Math.max(0, s), e: Math.min(n - 1, e) });
4577
+ };
4578
+ const catFromEvent = (e) => {
4579
+ const svg = svgRef.current;
4580
+ if (!svg) return null;
4581
+ const r2 = svg.getBoundingClientRect();
4582
+ if (!(r2.width > 0)) return null;
4583
+ const frac = Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW));
4584
+ const idx = Math.floor(frac * rows.length);
4585
+ return Math.min(rows.length - 1, Math.max(0, idx));
4586
+ };
4587
+ const onOverlayMove = (e) => {
4588
+ const idx = catFromEvent(e);
4589
+ if (idx == null) return;
4590
+ show(tipFor(rows[idx]), e);
4591
+ if (drag) {
4592
+ if (drag.pan) {
4593
+ const s0 = drag.z0.e - drag.z0.s;
4594
+ const s = Math.min(allRows.length - 1 - s0, Math.max(0, drag.z0.s + (drag.start - idx)));
4595
+ applyZoom(s, s + s0);
4596
+ } else {
4597
+ setDrag({ ...drag, end: idx });
4598
+ }
4599
+ }
4600
+ };
4601
+ const onOverlayDown = (e) => {
4602
+ const idx = catFromEvent(e);
4603
+ if (idx == null) return;
4604
+ setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
4605
+ };
4606
+ const onOverlayUp = () => {
4607
+ if (drag && !drag.pan) {
4608
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
4609
+ if (hi - lo >= 1) applyZoom(baseIdx + lo, baseIdx + hi);
4610
+ }
4611
+ setDrag(null);
4612
+ };
4613
+ const onOverlayClick = (e) => {
4614
+ if (drag && drag.end !== drag.start) return;
4615
+ const idx = catFromEvent(e);
4616
+ if (idx == null) return;
4617
+ clickBox(rows[idx], idx);
4618
+ };
4619
+ React51__namespace.default.useEffect(() => {
4620
+ if (!zoomable) return void 0;
4621
+ const svg = svgRef.current;
4622
+ if (!svg) return void 0;
4623
+ const onWheel = (e) => {
4624
+ e.preventDefault();
4625
+ const n = allRows.length;
4626
+ const cur = zoom || { s: 0, e: n - 1 };
4627
+ const wSpan = cur.e - cur.s;
4628
+ const r2 = svg.getBoundingClientRect();
4629
+ const frac = r2.width > 0 ? Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW)) : 0.5;
4630
+ const center = cur.s + Math.round(frac * wSpan);
4631
+ const newSpan = Math.min(n - 1, Math.max(1, Math.round(wSpan * (e.deltaY < 0 ? 0.8 : 1.25))));
4632
+ const s = Math.min(n - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frac)));
4633
+ applyZoom(s, s + newSpan);
4634
+ };
4635
+ svg.addEventListener("wheel", onWheel, { passive: false });
4636
+ return () => svg.removeEventListener("wheel", onWheel);
4637
+ }, [zoomable, zoom, allRows.length]);
4638
+ const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
4639
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4640
+ "div",
4641
+ {
4642
+ ref: containerRef,
4643
+ className: `twc-chart twc-chart--boxplot ${className}`.trim(),
4644
+ "data-has-selection": selected != null || void 0,
4645
+ "data-clickable": clickable || void 0,
4646
+ ...rest,
4647
+ children: [
4648
+ baseStyles,
4649
+ styles,
4650
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
4651
+ showGrid ? scale.ticks.map((t, i) => {
4652
+ const p = vPos(t);
4653
+ return /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-boxplot__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
4654
+ }) : null,
4655
+ showAxis ? scale.ticks.map((t, i) => {
4656
+ const p = vPos(t);
4657
+ return /* @__PURE__ */ jsxRuntime.jsx("text", { className: "twc-boxplot__axis", x: padL - 8, y: p + 4, textAnchor: "end", children: shortNum(t) }, i);
4658
+ }) : null,
4659
+ rows.map((d, i) => {
4660
+ const cx = bandX(i) + band / 2;
4661
+ const x = cx - boxW / 2;
4662
+ const yMin = vPos(d.min), yMax = vPos(d.max);
4663
+ const yQ1 = vPos(d.q1), yQ3 = vPos(d.q3), yMed = vPos(d.median);
4664
+ const boxTop = Math.min(yQ1, yQ3), boxBot = Math.max(yQ1, yQ3);
4665
+ const cap = boxW / 3;
4666
+ const hoverProps = {
4667
+ onMouseMove: (e) => show(tipFor(d), e),
4668
+ onMouseLeave: hide
4669
+ };
4670
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "twc-chart__anim-fade", style: { color }, children: [
4671
+ /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx, y1: yMax, x2: cx, y2: boxTop }),
4672
+ /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx, y1: boxBot, x2: cx, y2: yMin }),
4673
+ /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx - cap, y1: yMax, x2: cx + cap, y2: yMax }),
4674
+ /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx - cap, y1: yMin, x2: cx + cap, y2: yMin }),
4675
+ /* @__PURE__ */ jsxRuntime.jsx(
4676
+ "rect",
4677
+ {
4678
+ className: "twc-boxplot__box",
4679
+ "data-mark": true,
4680
+ "data-selected": selected === i || void 0,
4681
+ style: selected === i ? { fill: color } : { fill: color, stroke: color },
4682
+ x,
4683
+ y: boxTop,
4684
+ width: boxW,
4685
+ height: Math.max(1, boxBot - boxTop),
4686
+ rx: "2",
4687
+ ...hoverProps,
4688
+ onClick: () => clickBox(d, i)
4689
+ }
4690
+ ),
4691
+ /* @__PURE__ */ jsxRuntime.jsx("line", { className: "twc-boxplot__median", style: { stroke: color }, x1: x, y1: yMed, x2: x + boxW, y2: yMed }),
4692
+ (Array.isArray(d.outliers) ? d.outliers : []).map(
4693
+ (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
4694
+ )
4695
+ ] }, i);
4696
+ }),
4697
+ 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,
4698
+ dragBand ? (() => {
4699
+ const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
4700
+ const x0 = bandX(lo), x1 = bandX(hi + 1);
4701
+ return /* @__PURE__ */ jsxRuntime.jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
4702
+ })() : null,
4703
+ zoomable ? /* @__PURE__ */ jsxRuntime.jsx(
3510
4704
  "rect",
3511
4705
  {
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
4706
+ className: "twc-chart__overlay",
4707
+ "data-zoom": "true",
4708
+ "data-clickable": clickable || void 0,
4709
+ "data-panning": drag && drag.pan || void 0,
4710
+ x: padL,
4711
+ y: padT,
4712
+ width: innerW,
4713
+ height: innerH,
4714
+ onMouseMove: onOverlayMove,
4715
+ onMouseLeave: () => {
4716
+ hide();
4717
+ setDrag(null);
4718
+ },
4719
+ onMouseDown: onOverlayDown,
4720
+ onMouseUp: onOverlayUp,
4721
+ onClick: onOverlayClick
3520
4722
  }
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
- ] });
4723
+ ) : null
4724
+ ] }),
4725
+ zoomable && zoom ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
4726
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
4727
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
4728
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
4729
+ ] }),
4730
+ "Reset"
4731
+ ] }) : null,
4732
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { tip }),
4733
+ tableFallback ? /* @__PURE__ */ jsxRuntime.jsx(
4734
+ ChartTable,
4735
+ {
4736
+ id: tableId,
4737
+ caption: caption ?? svgAriaLabel,
4738
+ columns: ["min", "q1", "median", "q3", "max"],
4739
+ rows: allRows.map((d) => ({
4740
+ label: d.label,
4741
+ values: [d.min, d.q1, d.median, d.q3, d.max].map((v) => fmt(Number(v)))
4742
+ }))
4743
+ }
4744
+ ) : null
4745
+ ]
4746
+ }
4747
+ );
3544
4748
  }
3545
4749
  function labelText6(label) {
3546
4750
  return typeof label === "string" || typeof label === "number" ? String(label) : "";
@@ -3560,8 +4764,10 @@ function RangeChart({
3560
4764
  height = 300,
3561
4765
  showGrid = true,
3562
4766
  showAxis = true,
4767
+ zoomable = false,
3563
4768
  colors,
3564
4769
  valueFormat,
4770
+ onDataClick,
3565
4771
  ariaLabel,
3566
4772
  "aria-label": ariaLabelProp,
3567
4773
  tableFallback = true,
@@ -3574,10 +4780,17 @@ function RangeChart({
3574
4780
  const uid = React51__namespace.default.useId();
3575
4781
  const tableId = tableFallback ? `${uid}-table` : void 0;
3576
4782
  const { containerRef, tip, show, hide } = useChartTooltip();
4783
+ const [selected, setSelected] = React51__namespace.default.useState(null);
4784
+ const [zoom, setZoom] = React51__namespace.default.useState(null);
4785
+ const [drag, setDrag] = React51__namespace.default.useState(null);
4786
+ const svgRef = React51__namespace.default.useRef(null);
3577
4787
  const isArea = type === "area";
3578
- const rows = data || [];
3579
- const n = rows.length;
4788
+ const clickable = !!onDataClick;
3580
4789
  const fmt = valueFormat || fmtNumber;
4790
+ const allRows = data || [];
4791
+ const baseIdx = zoom ? zoom.s : 0;
4792
+ const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
4793
+ const n = rows.length;
3581
4794
  const W = 600, H = height;
3582
4795
  const padL = showAxis ? isArea ? 44 : 84 : 8;
3583
4796
  const padR = 12;
@@ -3593,6 +4806,97 @@ function RangeChart({
3593
4806
  const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? `range ${type} chart`;
3594
4807
  const xPos = (v) => padL + frac(v) * innerW;
3595
4808
  const catBandH = innerH / Math.max(1, n);
4809
+ const lineX = (i) => n > 1 ? padL + innerW / (n - 1) * i : padL + innerW / 2;
4810
+ const yPos = (v) => padT + innerH - frac(v) * innerH;
4811
+ const colorAt = (d, i) => isArea ? paletteAt(colors, 0) : d.color || paletteAt(colors, i);
4812
+ const rangeVal = (d) => `${fmt(Number(d.min) || 0)} \u2013 ${fmt(Number(d.max) || 0)}`;
4813
+ const tipFor = (d, i) => ({ title: labelText7(d.label), items: [{ color: colorAt(d, i), label: "", value: rangeVal(d) }] });
4814
+ const clickDatum = (d, localIdx) => {
4815
+ if (!onDataClick) return;
4816
+ onDataClick({ label: d.label, min: Number(d.min) || 0, max: Number(d.max) || 0, index: baseIdx + localIdx });
4817
+ };
4818
+ const selectMark = (key) => setSelected((s) => s === key ? null : key);
4819
+ const clickAt = (localIdx) => {
4820
+ clickDatum(rows[localIdx], localIdx);
4821
+ selectMark(baseIdx + localIdx);
4822
+ };
4823
+ const applyZoom = (s, e) => {
4824
+ const nAll = allRows.length;
4825
+ if (e - s >= nAll - 1) {
4826
+ setZoom(null);
4827
+ return;
4828
+ }
4829
+ setZoom({ s: Math.max(0, s), e: Math.min(nAll - 1, e) });
4830
+ };
4831
+ const fracFromEvent = (e) => {
4832
+ const svg = svgRef.current;
4833
+ if (!svg) return null;
4834
+ const rect = svg.getBoundingClientRect();
4835
+ if (isArea) {
4836
+ if (!(rect.width > 0)) return null;
4837
+ return Math.min(1, Math.max(0, ((e.clientX - rect.left) / rect.width * W - padL) / innerW));
4838
+ }
4839
+ if (!(rect.height > 0)) return null;
4840
+ return Math.min(1, Math.max(0, ((e.clientY - rect.top) / rect.height * H - padT) / innerH));
4841
+ };
4842
+ const catFromEvent = (e) => {
4843
+ if (!rows.length) return null;
4844
+ const f = fracFromEvent(e);
4845
+ if (f == null) return null;
4846
+ const idx = isArea ? Math.round(f * Math.max(1, rows.length - 1)) : Math.floor(f * rows.length);
4847
+ return Math.min(rows.length - 1, Math.max(0, idx));
4848
+ };
4849
+ const onOverlayMove = (e) => {
4850
+ const idx = catFromEvent(e);
4851
+ if (idx == null) return;
4852
+ show(tipFor(rows[idx], idx), e);
4853
+ if (drag) {
4854
+ if (drag.pan) {
4855
+ const spanD = drag.z0.e - drag.z0.s;
4856
+ const s = Math.min(allRows.length - 1 - spanD, Math.max(0, drag.z0.s + (drag.start - idx)));
4857
+ applyZoom(s, s + spanD);
4858
+ } else {
4859
+ setDrag({ ...drag, end: idx });
4860
+ }
4861
+ }
4862
+ };
4863
+ const onOverlayDown = (e) => {
4864
+ const idx = catFromEvent(e);
4865
+ if (idx == null) return;
4866
+ setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
4867
+ };
4868
+ const onOverlayUp = () => {
4869
+ if (drag && !drag.pan) {
4870
+ const loI = Math.min(drag.start, drag.end), hiI = Math.max(drag.start, drag.end);
4871
+ if (hiI - loI >= 1) applyZoom(baseIdx + loI, baseIdx + hiI);
4872
+ }
4873
+ setDrag(null);
4874
+ };
4875
+ const onOverlayClick = (e) => {
4876
+ if (!onDataClick) return;
4877
+ const idx = catFromEvent(e);
4878
+ if (idx == null) return;
4879
+ clickAt(idx);
4880
+ };
4881
+ React51__namespace.default.useEffect(() => {
4882
+ if (!zoomable) return void 0;
4883
+ const svg = svgRef.current;
4884
+ if (!svg) return void 0;
4885
+ const onWheel = (e) => {
4886
+ e.preventDefault();
4887
+ const nAll = allRows.length;
4888
+ const cur = zoom || { s: 0, e: nAll - 1 };
4889
+ const spanC = cur.e - cur.s;
4890
+ const f = fracFromEvent(e);
4891
+ const frc = f == null ? 0.5 : f;
4892
+ const center = cur.s + Math.round(frc * spanC);
4893
+ const newSpan = Math.min(nAll - 1, Math.max(1, Math.round(spanC * (e.deltaY < 0 ? 0.8 : 1.25))));
4894
+ const s = Math.min(nAll - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frc)));
4895
+ applyZoom(s, s + newSpan);
4896
+ };
4897
+ svg.addEventListener("wheel", onWheel, { passive: false });
4898
+ return () => svg.removeEventListener("wheel", onWheel);
4899
+ }, [zoomable, zoom, allRows.length]);
3596
4900
  const renderBars = () => {
3597
4901
  const gap = 0.34;
3598
4902
  const slot = catBandH * (1 - gap);
@@ -3601,27 +4905,29 @@ function RangeChart({
3601
4905
  const x0 = xPos(Math.min(min, max));
3602
4906
  const x1 = xPos(Math.max(min, max));
3603
4907
  const y = padT + catBandH * i + catBandH * gap / 2;
3604
- const color = d.color || paletteAt(colors, i);
4908
+ const color = colorAt(d, i);
4909
+ const key = baseIdx + i;
3605
4910
  return /* @__PURE__ */ jsxRuntime.jsx(
3606
4911
  "rect",
3607
4912
  {
3608
4913
  className: "twc-rangechart__bar twc-chart__anim-bar",
4914
+ "data-mark": true,
3609
4915
  "data-horizontal": "true",
4916
+ "data-selected": selected === key || void 0,
3610
4917
  style: { fill: color },
3611
4918
  x: x0,
3612
4919
  y,
3613
4920
  width: Math.max(1, x1 - x0),
3614
4921
  height: Math.max(1, slot),
3615
4922
  rx: "3",
3616
- onMouseMove: (e) => show({ title: labelText7(d.label), items: [{ color, label: "", value: `${fmt(min)} \u2013 ${fmt(max)}` }] }, e),
3617
- onMouseLeave: hide
4923
+ onMouseMove: (e) => show(tipFor(d, i), e),
4924
+ onMouseLeave: hide,
4925
+ onClick: () => clickAt(i)
3618
4926
  },
3619
4927
  i
3620
4928
  );
3621
4929
  });
3622
4930
  };
3623
- const lineX = (i) => n > 1 ? padL + innerW / (n - 1) * i : padL + innerW / 2;
3624
- const yPos = (v) => padT + innerH - frac(v) * innerH;
3625
4931
  const renderBand = () => {
3626
4932
  const maxPts = rows.map((d, i) => [lineX(i), yPos(Number(d.max) || 0)]);
3627
4933
  const minPts = rows.map((d, i) => [lineX(i), yPos(Number(d.min) || 0)]);
@@ -3636,44 +4942,126 @@ function RangeChart({
3636
4942
  rows.map((d, i) => /* @__PURE__ */ jsxRuntime.jsx(
3637
4943
  "rect",
3638
4944
  {
4945
+ "data-mark": true,
4946
+ "data-selected": selected === baseIdx + i || void 0,
3639
4947
  fill: "transparent",
3640
4948
  x: lineX(i) - hitW / 2,
3641
4949
  y: padT,
3642
4950
  width: hitW,
3643
4951
  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
4952
+ onMouseMove: (e) => show(tipFor(d, i), e),
4953
+ onMouseLeave: hide,
4954
+ onClick: () => clickAt(i)
3646
4955
  },
3647
4956
  i
3648
4957
  ))
3649
4958
  ] });
3650
4959
  };
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,
4960
+ const renderZones = () => rows.map((d, i) => {
4961
+ let x, y, w, hgt;
4962
+ if (isArea) {
4963
+ const x0 = i === 0 ? padL : (lineX(i - 1) + lineX(i)) / 2;
4964
+ const x1 = i === rows.length - 1 ? padL + innerW : (lineX(i) + lineX(i + 1)) / 2;
4965
+ x = x0;
4966
+ w = x1 - x0;
4967
+ y = padT;
4968
+ hgt = innerH;
4969
+ } else {
4970
+ x = padL;
4971
+ w = innerW;
4972
+ y = padT + catBandH * i;
4973
+ hgt = catBandH;
4974
+ }
4975
+ return /* @__PURE__ */ jsxRuntime.jsx(
4976
+ "rect",
3669
4977
  {
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
- ] });
4978
+ className: "twc-chart__zone",
4979
+ x,
4980
+ y,
4981
+ width: Math.max(0, w),
4982
+ height: Math.max(0, hgt),
4983
+ "data-clickable": onDataClick ? true : void 0,
4984
+ onMouseMove: (e) => show(tipFor(d, i), e),
4985
+ onMouseLeave: hide,
4986
+ onClick: () => clickDatum(d, i)
4987
+ },
4988
+ i
4989
+ );
4990
+ });
4991
+ const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
4992
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4993
+ "div",
4994
+ {
4995
+ ref: containerRef,
4996
+ className: `twc-chart twc-chart--range ${className}`.trim(),
4997
+ "data-type": type,
4998
+ "data-clickable": clickable || void 0,
4999
+ "data-has-selection": selected != null || void 0,
5000
+ ...rest,
5001
+ children: [
5002
+ baseStyles,
5003
+ styles,
5004
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
5005
+ showGrid ? scale.ticks.map(
5006
+ (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)
5007
+ ) : null,
5008
+ showAxis ? scale.ticks.map(
5009
+ (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)
5010
+ ) : null,
5011
+ !zoomable ? renderZones() : null,
5012
+ isArea ? renderBand() : renderBars(),
5013
+ showAxis ? rows.map(
5014
+ (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)
5015
+ ) : null,
5016
+ dragBand ? (() => {
5017
+ const loI = Math.min(drag.start, drag.end), hiI = Math.max(drag.start, drag.end);
5018
+ if (isArea) {
5019
+ const x0 = lineX(loI), x1 = lineX(hiI);
5020
+ return /* @__PURE__ */ jsxRuntime.jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
5021
+ }
5022
+ const y0 = padT + catBandH * loI, y1 = padT + catBandH * (hiI + 1);
5023
+ return /* @__PURE__ */ jsxRuntime.jsx("rect", { className: "twc-chart__zoomband", x: padL, y: y0, width: innerW, height: Math.max(0, y1 - y0) });
5024
+ })() : null,
5025
+ zoomable ? /* @__PURE__ */ jsxRuntime.jsx(
5026
+ "rect",
5027
+ {
5028
+ className: "twc-chart__overlay",
5029
+ "data-clickable": clickable || void 0,
5030
+ x: padL,
5031
+ y: padT,
5032
+ width: innerW,
5033
+ height: innerH,
5034
+ onMouseMove: onOverlayMove,
5035
+ onMouseLeave: () => {
5036
+ hide();
5037
+ setDrag(null);
5038
+ },
5039
+ onMouseDown: onOverlayDown,
5040
+ onMouseUp: onOverlayUp,
5041
+ onClick: onOverlayClick
5042
+ }
5043
+ ) : null
5044
+ ] }),
5045
+ zoomable && zoom ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
5046
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
5047
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
5048
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
5049
+ ] }),
5050
+ "Reset"
5051
+ ] }) : null,
5052
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { tip }),
5053
+ tableFallback ? /* @__PURE__ */ jsxRuntime.jsx(
5054
+ ChartTable,
5055
+ {
5056
+ id: tableId,
5057
+ caption: caption ?? svgAriaLabel,
5058
+ columns: ["min", "max"],
5059
+ rows: allRows.map((d) => ({ label: d.label, values: [fmt(Number(d.min) || 0), fmt(Number(d.max) || 0)] }))
5060
+ }
5061
+ ) : null
5062
+ ]
5063
+ }
5064
+ );
3677
5065
  }
3678
5066
  function labelText7(label) {
3679
5067
  return typeof label === "string" || typeof label === "number" ? String(label) : "";
@@ -7373,6 +8761,7 @@ function Datatable({
7373
8761
  serverMode = false,
7374
8762
  rowCount,
7375
8763
  onServerChange,
8764
+ onColumnVisibilityChange,
7376
8765
  batchActions = [],
7377
8766
  showExport = false,
7378
8767
  showDensity = false,
@@ -7679,6 +9068,19 @@ function Datatable({
7679
9068
  }, [exportOpen]);
7680
9069
  const colByField = React51__namespace.default.useMemo(() => Object.fromEntries(cols.map((c) => [c.field, c])), [cols]);
7681
9070
  const visibleCols = cols.filter((c) => !hidden.has(c.field));
9071
+ const colFieldsKey = JSON.stringify(
9072
+ cols.filter((c) => typeof c.field === "string" && c.type !== "actions").map((c) => c.field)
9073
+ );
9074
+ const colFields = React51__namespace.default.useMemo(() => JSON.parse(colFieldsKey), [colFieldsKey]);
9075
+ const hiddenKey = JSON.stringify([...hidden].filter((f) => typeof f === "string").sort());
9076
+ const visibleColumns = React51__namespace.default.useMemo(() => {
9077
+ const h = new Set(JSON.parse(hiddenKey));
9078
+ return colFields.filter((f) => !h.has(f));
9079
+ }, [colFields, hiddenKey]);
9080
+ const hiddenColumns = React51__namespace.default.useMemo(() => {
9081
+ const h = new Set(JSON.parse(hiddenKey));
9082
+ return colFields.filter((f) => h.has(f));
9083
+ }, [colFields, hiddenKey]);
7682
9084
  const ordered = React51__namespace.default.useMemo(() => {
7683
9085
  const orderIdx = (f) => {
7684
9086
  const i = order.indexOf(f);
@@ -8021,11 +9423,27 @@ function Datatable({
8021
9423
  pageSize: sizeVal,
8022
9424
  sort,
8023
9425
  filters: filters.map(({ id, ...f }) => f),
8024
- quickFilter: quick.trim()
9426
+ quickFilter: quick.trim(),
9427
+ visibleColumns,
9428
+ hiddenColumns
9429
+ // #191: server can project only the visible columns
8025
9430
  });
8026
9431
  }, 250);
8027
9432
  return () => clearTimeout(t);
8028
- }, [serverMode, pageVal, sizeVal, sort, filters, quick]);
9433
+ }, [serverMode, pageVal, sizeVal, sort, filters, quick, visibleColumns, hiddenColumns]);
9434
+ const onColVisRef = React51__namespace.default.useRef(onColumnVisibilityChange);
9435
+ onColVisRef.current = onColumnVisibilityChange;
9436
+ const lastVisibleKey = React51__namespace.default.useRef(null);
9437
+ React51__namespace.default.useEffect(() => {
9438
+ const key = JSON.stringify(visibleColumns);
9439
+ if (lastVisibleKey.current === null) {
9440
+ lastVisibleKey.current = key;
9441
+ return;
9442
+ }
9443
+ if (lastVisibleKey.current === key) return;
9444
+ lastVisibleKey.current = key;
9445
+ onColVisRef.current?.(visibleColumns);
9446
+ }, [visibleColumns]);
8029
9447
  function cycleSort(field) {
8030
9448
  setSort((s) => !s || s.field !== field ? { field, dir: "asc" } : s.dir === "asc" ? { field, dir: "desc" } : null);
8031
9449
  }
@@ -14048,6 +15466,12 @@ var SHELL_CSS = `
14048
15466
  min-height: 60px; padding-inline: var(--space-5); border-bottom: var(--border-thin) solid var(--color-border); background: var(--color-surface); }
14049
15467
  .twc-shell__content { flex: 1 1 auto; min-height: 0; overflow: auto; }
14050
15468
  .twc-shell__content[data-padded="true"] { padding: var(--space-6) var(--space-5); }
15469
+ /* #190: <main> is the skip-link target (tabindex="-1") \u2014 a programmatic focus target,
15470
+ not tab-reachable and not an interactive control. Clicking content mouse-focuses it,
15471
+ then the first keypress flips to keyboard modality and :focus-visible would draw a UA
15472
+ outline around the ENTIRE content area. Suppress it \u2014 the skip LINK keeps its own
15473
+ visible ring (.twc-shell__skip:focus-visible) and SR focus announcement is unaffected. */
15474
+ .twc-shell__content:focus, .twc-shell__content:focus-visible { outline: none; }
14051
15475
  /* Mobile: stack the shell so the sidebar (rendered as an off-canvas overlay) no
14052
15476
  longer reserves a column \u2014 the main content spans the full width. */
14053
15477
  @media (max-width: 720px) {
@@ -14182,8 +15606,10 @@ var Container = React51__namespace.default.forwardRef(function Container2({
14182
15606
  style: {
14183
15607
  width: "100%",
14184
15608
  maxWidth: typeof max === "number" ? `${max}px` : max,
14185
- marginInline: "auto",
14186
- paddingInline: padded ? "var(--space-5)" : void 0,
15609
+ marginInlineStart: "auto",
15610
+ marginInlineEnd: "auto",
15611
+ paddingInlineStart: padded ? "var(--space-5)" : void 0,
15612
+ paddingInlineEnd: padded ? "var(--space-5)" : void 0,
14187
15613
  ...style,
14188
15614
  ...flatStyle
14189
15615
  },
@@ -14433,7 +15859,10 @@ var Heading = React51__namespace.default.forwardRef(function Heading2({
14433
15859
  className: `twc-heading ${className}`.trim(),
14434
15860
  "data-twc-sx": sxAttr,
14435
15861
  style: {
14436
- margin: 0,
15862
+ marginTop: 0,
15863
+ marginRight: 0,
15864
+ marginBottom: 0,
15865
+ marginLeft: 0,
14437
15866
  fontFamily: "var(--font-sans)",
14438
15867
  fontWeight: "var(--font-bold)",
14439
15868
  color: "var(--color-text)",
@@ -14498,7 +15927,10 @@ var Text = React51__namespace.default.forwardRef(function Text2({
14498
15927
  className: `twc-text ${className}`.trim(),
14499
15928
  "data-twc-sx": sxAttr,
14500
15929
  style: {
14501
- margin: 0,
15930
+ marginTop: 0,
15931
+ marginRight: 0,
15932
+ marginBottom: 0,
15933
+ marginLeft: 0,
14502
15934
  fontFamily: "var(--font-sans)",
14503
15935
  lineHeight: 1.6,
14504
15936
  fontSize: `var(--text-${size})`,