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/CHANGELOG.md +27 -0
- package/dist/index.cjs +2037 -605
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +247 -19
- package/dist/index.d.ts +247 -19
- package/dist/index.mjs +2039 -607
- package/dist/index.mjs.map +1 -1
- package/llms.txt +409 -383
- package/package.json +1 -1
- package/styles/twico-ui.css +9 -0
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"
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|
|
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.
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 *
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1897
|
-
|
|
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
|
|
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
|
|
1906
|
-
|
|
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
|
|
1915
|
-
|
|
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 ${
|
|
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 ${
|
|
1947
|
-
showDots || !isArea ? pts.map((p, i) => /* @__PURE__ */ jsxRuntime.jsxs("g", { className:
|
|
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) =>
|
|
1956
|
-
onMouseLeave:
|
|
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(
|
|
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
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
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-
|
|
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
|
-
|
|
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":
|
|
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(
|
|
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(
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
"
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
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(
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
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
|
-
|
|
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
|
|
2513
|
-
const
|
|
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
|
|
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
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
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
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
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
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
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
|
-
|
|
3223
|
+
) : null,
|
|
3224
|
+
legend && list.length ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3225
|
+
ChartLegend,
|
|
2579
3226
|
{
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
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
|
-
]
|
|
2589
|
-
|
|
2590
|
-
|
|
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(
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
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":
|
|
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":
|
|
2816
|
-
|
|
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(
|
|
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
|
|
2894
|
-
const
|
|
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
|
-
|
|
3608
|
+
fullXs.push(d.x);
|
|
2904
3609
|
}
|
|
2905
3610
|
if (!seenY.has(ky)) {
|
|
2906
3611
|
seenY.add(ky);
|
|
2907
|
-
|
|
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
|
-
|
|
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:
|
|
3002
|
-
rows:
|
|
3882
|
+
columns: fullXs.map(labelText3),
|
|
3883
|
+
rows: fullYs.map((y) => ({
|
|
3003
3884
|
label: labelText3(y),
|
|
3004
|
-
values:
|
|
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-
|
|
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.
|
|
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-
|
|
3135
|
-
.twc-chart__tile-value { fill: var(--color-
|
|
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
|
|
3311
|
-
const
|
|
3312
|
-
const
|
|
3313
|
-
const
|
|
3314
|
-
const
|
|
3315
|
-
const
|
|
3316
|
-
const
|
|
3317
|
-
const
|
|
3318
|
-
const
|
|
3319
|
-
const
|
|
3320
|
-
const
|
|
3321
|
-
const
|
|
3322
|
-
const
|
|
3323
|
-
const
|
|
3324
|
-
const
|
|
3325
|
-
const
|
|
3326
|
-
const
|
|
3327
|
-
const
|
|
3328
|
-
const
|
|
3329
|
-
const
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
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-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
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
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3667
|
-
|
|
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
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
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
|
-
|
|
14186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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})`,
|