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.mjs
CHANGED
|
@@ -25,7 +25,7 @@ function useScopedStyles(id, css) {
|
|
|
25
25
|
}
|
|
26
26
|
if (el.textContent !== css) el.textContent = css;
|
|
27
27
|
}, [id, css, nonce]);
|
|
28
|
-
return SUPPORTS_STYLE_HOIST && css ? React51.createElement("style", { href: id, precedence: "twc-ui"
|
|
28
|
+
return SUPPORTS_STYLE_HOIST && css ? React51.createElement("style", { href: id, precedence: "twc-ui" }, css) : null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// components/TwicoProvider.jsx
|
|
@@ -1287,6 +1287,62 @@ function compileSx(obj, selector) {
|
|
|
1287
1287
|
out += rules;
|
|
1288
1288
|
return out;
|
|
1289
1289
|
}
|
|
1290
|
+
var BOX_EDGE4 = {
|
|
1291
|
+
padding: ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"],
|
|
1292
|
+
margin: ["marginTop", "marginRight", "marginBottom", "marginLeft"],
|
|
1293
|
+
inset: ["top", "right", "bottom", "left"]
|
|
1294
|
+
};
|
|
1295
|
+
var BOX_EDGE2 = {
|
|
1296
|
+
paddingBlock: ["paddingBlockStart", "paddingBlockEnd"],
|
|
1297
|
+
paddingInline: ["paddingInlineStart", "paddingInlineEnd"],
|
|
1298
|
+
marginBlock: ["marginBlockStart", "marginBlockEnd"],
|
|
1299
|
+
marginInline: ["marginInlineStart", "marginInlineEnd"]
|
|
1300
|
+
};
|
|
1301
|
+
function splitShorthand(value) {
|
|
1302
|
+
const s = String(value).trim();
|
|
1303
|
+
if (!s) return [];
|
|
1304
|
+
const out = [];
|
|
1305
|
+
let depth = 0, cur = "";
|
|
1306
|
+
for (let i = 0; i < s.length; i++) {
|
|
1307
|
+
const c = s[i];
|
|
1308
|
+
if (c === "(") {
|
|
1309
|
+
depth++;
|
|
1310
|
+
cur += c;
|
|
1311
|
+
} else if (c === ")") {
|
|
1312
|
+
depth = Math.max(0, depth - 1);
|
|
1313
|
+
cur += c;
|
|
1314
|
+
} else if (depth === 0 && (c === " " || c === " " || c === "\n" || c === "\r")) {
|
|
1315
|
+
if (cur) {
|
|
1316
|
+
out.push(cur);
|
|
1317
|
+
cur = "";
|
|
1318
|
+
}
|
|
1319
|
+
} else cur += c;
|
|
1320
|
+
}
|
|
1321
|
+
if (cur) out.push(cur);
|
|
1322
|
+
return out;
|
|
1323
|
+
}
|
|
1324
|
+
function expandBoxShorthand(key, value) {
|
|
1325
|
+
const four = BOX_EDGE4[key];
|
|
1326
|
+
const two = BOX_EDGE2[key];
|
|
1327
|
+
if (!four && !two || value == null) return null;
|
|
1328
|
+
if (typeof value === "number") return (four || two).map((edge) => [edge, value]);
|
|
1329
|
+
const p = splitShorthand(value);
|
|
1330
|
+
if (p.length === 0) return null;
|
|
1331
|
+
if (two) return [[two[0], p[0]], [two[1], p.length > 1 ? p[1] : p[0]]];
|
|
1332
|
+
let top, right, bottom, left;
|
|
1333
|
+
if (p.length === 1) top = right = bottom = left = p[0];
|
|
1334
|
+
else if (p.length === 2) {
|
|
1335
|
+
top = bottom = p[0];
|
|
1336
|
+
right = left = p[1];
|
|
1337
|
+
} else if (p.length === 3) {
|
|
1338
|
+
top = p[0];
|
|
1339
|
+
right = left = p[1];
|
|
1340
|
+
bottom = p[2];
|
|
1341
|
+
} else {
|
|
1342
|
+
[top, right, bottom, left] = p;
|
|
1343
|
+
}
|
|
1344
|
+
return [[four[0], top], [four[1], right], [four[2], bottom], [four[3], left]];
|
|
1345
|
+
}
|
|
1290
1346
|
function buildSx(sx, uid) {
|
|
1291
1347
|
if (!isObject(sx)) return { flatStyle: void 0, css: "" };
|
|
1292
1348
|
const scope = `[data-twc-sx="${uid}"]`;
|
|
@@ -1304,7 +1360,13 @@ function buildSx(sx, uid) {
|
|
|
1304
1360
|
css += compileSx(value, resolveSelector(key, scope));
|
|
1305
1361
|
}
|
|
1306
1362
|
} else {
|
|
1307
|
-
|
|
1363
|
+
const expanded = expandBoxShorthand(key, value);
|
|
1364
|
+
if (expanded) {
|
|
1365
|
+
flatStyle || (flatStyle = {});
|
|
1366
|
+
for (const [k, v] of expanded) flatStyle[k] = v;
|
|
1367
|
+
} else {
|
|
1368
|
+
(flatStyle || (flatStyle = {}))[key] = value;
|
|
1369
|
+
}
|
|
1308
1370
|
}
|
|
1309
1371
|
}
|
|
1310
1372
|
return { flatStyle, css };
|
|
@@ -1654,14 +1716,15 @@ var CHART_BASE_CSS = `
|
|
|
1654
1716
|
.twc-chart__leg[data-off="true"] { opacity: 0.4; }
|
|
1655
1717
|
.twc-chart__leg-sw { width: 10px; height: 10px; border-radius: 3px; flex: none; }
|
|
1656
1718
|
|
|
1657
|
-
/* Floating tooltip \u2014 a styled card that follows the pointer (ApexCharts / MUI X style).
|
|
1719
|
+
/* Floating tooltip \u2014 a styled card that follows the pointer (ApexCharts / MUI X style).
|
|
1720
|
+
Positioned inline by <ChartTooltip> (which flips below / clamps to the container so it
|
|
1721
|
+
never clips); only the opacity fades in here so it can't fight the inline transform. */
|
|
1658
1722
|
.twc-chart__tip {
|
|
1659
|
-
position: absolute; z-index: 3; top: 0; left: 0; pointer-events: none;
|
|
1660
|
-
|
|
1661
|
-
min-width: 96px; max-width: 260px; padding: 8px 10px;
|
|
1723
|
+
position: absolute; z-index: 3; top: 0; left: 0; pointer-events: none; will-change: transform;
|
|
1724
|
+
min-width: 88px; max-width: 240px; padding: 7px 9px;
|
|
1662
1725
|
background: var(--color-surface-raised); color: var(--color-text);
|
|
1663
1726
|
border: var(--border-thin) solid var(--color-border); border-radius: var(--radius-md);
|
|
1664
|
-
box-shadow: var(--shadow-lg); font-size: var(--text-xs); line-height: 1.
|
|
1727
|
+
box-shadow: var(--shadow-lg); font-size: var(--text-xs); line-height: 1.4;
|
|
1665
1728
|
opacity: 0; animation: twc-chart-tip-in var(--duration-fast) var(--ease-standard) forwards;
|
|
1666
1729
|
}
|
|
1667
1730
|
.twc-chart__tip-title { font-weight: var(--font-bold); color: var(--color-text); margin-bottom: 4px; white-space: nowrap; }
|
|
@@ -1670,12 +1733,36 @@ var CHART_BASE_CSS = `
|
|
|
1670
1733
|
.twc-chart__tip-sw { width: 9px; height: 9px; border-radius: 2px; flex: none; }
|
|
1671
1734
|
.twc-chart__tip-label { color: var(--color-text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
|
1672
1735
|
.twc-chart__tip-val { font-weight: var(--font-semibold); color: var(--color-text); font-variant-numeric: tabular-nums; }
|
|
1673
|
-
@keyframes twc-chart-tip-in { from { opacity: 0;
|
|
1736
|
+
@keyframes twc-chart-tip-in { from { opacity: 0; } to { opacity: 1; } }
|
|
1674
1737
|
|
|
1675
1738
|
/* Hover emphasis: when the chart marks itself hovered, fade every mark except the active one. */
|
|
1676
|
-
.twc-chart[data-hovering="true"] [data-mark] { opacity: 0.
|
|
1739
|
+
.twc-chart[data-hovering="true"] [data-mark] { opacity: 0.3; transition: opacity var(--duration-base) var(--ease-standard); }
|
|
1677
1740
|
.twc-chart[data-hovering="true"] [data-mark][data-active="true"] { opacity: 1; }
|
|
1678
|
-
[data-mark] { transition: opacity var(--duration-
|
|
1741
|
+
[data-mark] { transition: opacity var(--duration-base) var(--ease-standard), transform var(--duration-base) var(--ease-spring); cursor: default; }
|
|
1742
|
+
|
|
1743
|
+
/* Click-to-select: a soft accent ring + glow on the chosen mark (not a heavy outline). */
|
|
1744
|
+
[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)); }
|
|
1745
|
+
/* Click-to-focus: a persistent selection also dims the other marks (focus mode), on every chart. */
|
|
1746
|
+
.twc-chart[data-has-selection="true"] [data-mark]:not([data-selected="true"]) { opacity: 0.4; }
|
|
1747
|
+
.twc-chart[data-clickable="true"] [data-mark], .twc-chart__overlay[data-clickable="true"] { cursor: pointer; }
|
|
1748
|
+
|
|
1749
|
+
/* Crosshair + full-plot event overlay (cartesian shared-axis interactions). */
|
|
1750
|
+
.twc-chart__overlay, .twc-chart__zone { fill: transparent; }
|
|
1751
|
+
.twc-chart__crosshair { stroke: var(--color-border-strong); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.7; pointer-events: none; }
|
|
1752
|
+
|
|
1753
|
+
/* Drag-to-zoom affordances: a crosshair cursor over the zoom overlay + a bordered band. */
|
|
1754
|
+
.twc-chart__overlay[data-zoom="true"] { cursor: crosshair; }
|
|
1755
|
+
.twc-chart__overlay[data-zoom="true"][data-panning="true"] { cursor: grabbing; }
|
|
1756
|
+
.twc-chart__zoomband { fill: var(--color-primary); opacity: 0.14; stroke: var(--color-primary); stroke-opacity: 0.5; stroke-width: 1; pointer-events: none; }
|
|
1757
|
+
.twc-chart__zoom-reset {
|
|
1758
|
+
position: absolute; top: 6px; inset-inline-end: 8px; z-index: 2;
|
|
1759
|
+
display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px;
|
|
1760
|
+
font-family: var(--font-sans); font-size: var(--text-xs); color: var(--color-text-muted);
|
|
1761
|
+
background: var(--color-surface-raised); border: var(--border-thin) solid var(--color-border);
|
|
1762
|
+
border-radius: var(--radius-md); box-shadow: var(--shadow-sm); cursor: pointer;
|
|
1763
|
+
}
|
|
1764
|
+
.twc-chart__zoom-reset:hover { color: var(--color-text); border-color: var(--color-border-strong); }
|
|
1765
|
+
.twc-chart__zoom-reset svg { width: 12px; height: 12px; }
|
|
1679
1766
|
|
|
1680
1767
|
/* Entrance animations (respect reduced motion, below). */
|
|
1681
1768
|
@keyframes twc-chart-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
@@ -1723,7 +1810,7 @@ function ChartTable({ id, caption, columns, rows }) {
|
|
|
1723
1810
|
)
|
|
1724
1811
|
);
|
|
1725
1812
|
}
|
|
1726
|
-
function ChartLegend({ items, onToggle, hidden }) {
|
|
1813
|
+
function ChartLegend({ items, onToggle, hidden, onFocus, onBlur }) {
|
|
1727
1814
|
const h = React51__default.createElement;
|
|
1728
1815
|
return h(
|
|
1729
1816
|
"div",
|
|
@@ -1744,7 +1831,9 @@ function ChartLegend({ items, onToggle, hidden }) {
|
|
|
1744
1831
|
e.preventDefault();
|
|
1745
1832
|
onToggle(it, i);
|
|
1746
1833
|
}
|
|
1747
|
-
} : void 0
|
|
1834
|
+
} : void 0,
|
|
1835
|
+
onMouseEnter: onFocus ? () => onFocus(it, i) : void 0,
|
|
1836
|
+
onMouseLeave: onBlur ? () => onBlur() : void 0
|
|
1748
1837
|
},
|
|
1749
1838
|
h("span", { className: "twc-chart__leg-sw", style: { background: it.color } }),
|
|
1750
1839
|
it.label
|
|
@@ -1767,11 +1856,31 @@ function useChartTooltip() {
|
|
|
1767
1856
|
}
|
|
1768
1857
|
function ChartTooltip({ tip }) {
|
|
1769
1858
|
const h = React51__default.createElement;
|
|
1859
|
+
const ref = React51__default.useRef(null);
|
|
1860
|
+
const [pos, setPos] = React51__default.useState({ dx: 0, below: false });
|
|
1861
|
+
React51__default.useLayoutEffect(() => {
|
|
1862
|
+
const el = ref.current;
|
|
1863
|
+
if (!tip || !el) return;
|
|
1864
|
+
const parent = el.offsetParent;
|
|
1865
|
+
const pw = parent && parent.clientWidth ? parent.clientWidth : 0;
|
|
1866
|
+
const w = el.offsetWidth || 0, ht = el.offsetHeight || 0;
|
|
1867
|
+
let dx = 0;
|
|
1868
|
+
if (pw > 0 && w > 0) {
|
|
1869
|
+
const half = w / 2;
|
|
1870
|
+
if (tip.left - half < 6) dx = 6 - (tip.left - half);
|
|
1871
|
+
else if (tip.left + half > pw - 6) dx = pw - 6 - (tip.left + half);
|
|
1872
|
+
}
|
|
1873
|
+
setPos({ dx, below: ht > 0 && tip.top - ht - 16 < 0 });
|
|
1874
|
+
}, [tip]);
|
|
1770
1875
|
if (!tip) return null;
|
|
1771
|
-
const
|
|
1876
|
+
const style = {
|
|
1877
|
+
left: tip.left + pos.dx,
|
|
1878
|
+
top: tip.top,
|
|
1879
|
+
transform: pos.below ? "translate(-50%, 18px)" : "translate(-50%, calc(-100% - 14px))"
|
|
1880
|
+
};
|
|
1772
1881
|
return h(
|
|
1773
1882
|
"div",
|
|
1774
|
-
{ className: "twc-chart__tip", role: "tooltip", "aria-hidden": "true", style
|
|
1883
|
+
{ ref, className: "twc-chart__tip", role: "tooltip", "aria-hidden": "true", style },
|
|
1775
1884
|
tip.title != null && tip.title !== "" ? h("div", { className: "twc-chart__tip-title" }, tip.title) : null,
|
|
1776
1885
|
(tip.items || []).map(
|
|
1777
1886
|
(it, i) => h(
|
|
@@ -1787,7 +1896,6 @@ function ChartTooltip({ tip }) {
|
|
|
1787
1896
|
var CHART_CSS = `
|
|
1788
1897
|
.twc-chart__grid { stroke: var(--color-divider); stroke-width: 1; }
|
|
1789
1898
|
.twc-chart__axisline { stroke: var(--color-border); stroke-width: 1; }
|
|
1790
|
-
.twc-chart__tick { stroke: var(--color-border); stroke-width: 1; }
|
|
1791
1899
|
.twc-chart__axis { fill: var(--color-text-subtle); font-size: 11px; }
|
|
1792
1900
|
.twc-chart__bar { fill: var(--color-primary); }
|
|
1793
1901
|
.twc-chart__bar:hover { filter: brightness(1.06); }
|
|
@@ -1796,7 +1904,6 @@ var CHART_CSS = `
|
|
|
1796
1904
|
.twc-chart__dot { fill: var(--color-surface); stroke: var(--color-primary); stroke-width: 2.5; transition: r var(--duration-fast) var(--ease-standard); }
|
|
1797
1905
|
.twc-chart__hit { fill: transparent; }
|
|
1798
1906
|
.twc-chart__hit:hover + .twc-chart__dot, .twc-chart__dot:hover { r: 5; }
|
|
1799
|
-
.twc-chart__vline { stroke: var(--color-border-strong); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.7; }
|
|
1800
1907
|
.twc-chart__label { fill: var(--color-text-muted); font-size: 10px; font-weight: var(--font-semibold); }
|
|
1801
1908
|
`;
|
|
1802
1909
|
function Chart({
|
|
@@ -1813,9 +1920,12 @@ function Chart({
|
|
|
1813
1920
|
showGrid = true,
|
|
1814
1921
|
showAxis = true,
|
|
1815
1922
|
showLegend = false,
|
|
1923
|
+
crosshair = true,
|
|
1924
|
+
zoomable = false,
|
|
1816
1925
|
animate = true,
|
|
1817
1926
|
colors,
|
|
1818
1927
|
valueFormat,
|
|
1928
|
+
onDataClick,
|
|
1819
1929
|
ariaLabel,
|
|
1820
1930
|
"aria-label": ariaLabelProp,
|
|
1821
1931
|
tableFallback = true,
|
|
@@ -1830,15 +1940,23 @@ function Chart({
|
|
|
1830
1940
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
1831
1941
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
1832
1942
|
const [hidden, setHidden] = React51__default.useState(() => /* @__PURE__ */ new Set());
|
|
1943
|
+
const [focusSeries, setFocusSeries] = React51__default.useState(null);
|
|
1944
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
1945
|
+
const [hoverIdx, setHoverIdx] = React51__default.useState(null);
|
|
1946
|
+
const [zoom, setZoom] = React51__default.useState(null);
|
|
1947
|
+
const [drag, setDrag] = React51__default.useState(null);
|
|
1948
|
+
const svgRef = React51__default.useRef(null);
|
|
1833
1949
|
const isBar = type === "bar" || type === "column";
|
|
1834
1950
|
const isArea = type === "area";
|
|
1835
1951
|
const crv = smooth ? "smooth" : curve;
|
|
1836
1952
|
const pathOpts = { smooth: crv === "smooth", step: crv === "stepped" ? "after" : null };
|
|
1837
1953
|
const canStack = stacked && (isBar || isArea);
|
|
1838
|
-
const
|
|
1954
|
+
const clickable = !!onDataClick;
|
|
1839
1955
|
const allKeys = series && series.length ? series : ["value"];
|
|
1840
1956
|
const keys = allKeys.filter((k) => !hidden.has(k));
|
|
1841
|
-
const
|
|
1957
|
+
const allRows = data || [];
|
|
1958
|
+
const baseIdx = zoom ? zoom.s : 0;
|
|
1959
|
+
const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
|
|
1842
1960
|
const fmt = valueFormat || fmtNumber;
|
|
1843
1961
|
const multi = allKeys.length > 1;
|
|
1844
1962
|
const W = 600, H = height;
|
|
@@ -1856,43 +1974,137 @@ function Chart({
|
|
|
1856
1974
|
const catBand = innerW / Math.max(1, rows.length);
|
|
1857
1975
|
const catBandH = innerH / Math.max(1, rows.length);
|
|
1858
1976
|
const lineX = (i) => padL + innerW / Math.max(1, rows.length - 1) * i;
|
|
1977
|
+
const catX = (i) => isBar ? padL + catBand * i + catBand / 2 : lineX(i);
|
|
1859
1978
|
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? `${type} chart`;
|
|
1860
|
-
const tipFor = (d) => ({
|
|
1861
|
-
title: d.label,
|
|
1862
|
-
items: keys.map((k) => ({ color: colorFor(k), label: k, value: fmt(Number(d[k]) || 0) }))
|
|
1863
|
-
});
|
|
1864
1979
|
function colorFor(k) {
|
|
1865
1980
|
const idx = allKeys.indexOf(k);
|
|
1866
1981
|
return multi || colors && colors.length ? paletteAt(colors, idx) : "var(--color-primary)";
|
|
1867
1982
|
}
|
|
1983
|
+
const tipFor = (d) => ({
|
|
1984
|
+
title: d.label,
|
|
1985
|
+
items: keys.map((k) => ({ color: colorFor(k), label: k, value: fmt(Number(d[k]) || 0) }))
|
|
1986
|
+
});
|
|
1987
|
+
const focusActive = (k) => focusSeries == null ? void 0 : k === focusSeries || void 0;
|
|
1988
|
+
const onMarkMove = (d, localIdx, e) => {
|
|
1989
|
+
show(tipFor(d), e);
|
|
1990
|
+
setHoverIdx(localIdx);
|
|
1991
|
+
};
|
|
1992
|
+
const onMarkLeave = () => {
|
|
1993
|
+
hide();
|
|
1994
|
+
setHoverIdx(null);
|
|
1995
|
+
};
|
|
1996
|
+
const clickDatum = (d, k, localIdx) => {
|
|
1997
|
+
if (!onDataClick) return;
|
|
1998
|
+
onDataClick({ label: d.label, series: k, seriesIndex: allKeys.indexOf(k), value: Number(d[k]) || 0, index: baseIdx + localIdx, row: d });
|
|
1999
|
+
};
|
|
2000
|
+
const selectMark = (key) => setSelected((s) => s === key ? null : key);
|
|
2001
|
+
const applyZoom = (s, e) => {
|
|
2002
|
+
const n = allRows.length;
|
|
2003
|
+
if (e - s >= n - 1) {
|
|
2004
|
+
setZoom(null);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
setZoom({ s: Math.max(0, s), e: Math.min(n - 1, e) });
|
|
2008
|
+
};
|
|
2009
|
+
const catFromEvent = (e) => {
|
|
2010
|
+
const svg = svgRef.current;
|
|
2011
|
+
if (!svg) return null;
|
|
2012
|
+
const r2 = svg.getBoundingClientRect();
|
|
2013
|
+
if (!(r2.width > 0)) return null;
|
|
2014
|
+
const frac = Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW));
|
|
2015
|
+
const idx = isBar ? Math.floor(frac * rows.length) : Math.round(frac * Math.max(1, rows.length - 1));
|
|
2016
|
+
return Math.min(rows.length - 1, Math.max(0, idx));
|
|
2017
|
+
};
|
|
2018
|
+
const onOverlayMove = (e) => {
|
|
2019
|
+
const idx = catFromEvent(e);
|
|
2020
|
+
if (idx == null) return;
|
|
2021
|
+
setHoverIdx(idx);
|
|
2022
|
+
show(tipFor(rows[idx]), e);
|
|
2023
|
+
if (drag) {
|
|
2024
|
+
if (drag.pan) {
|
|
2025
|
+
const span = drag.z0.e - drag.z0.s;
|
|
2026
|
+
const s = Math.min(allRows.length - 1 - span, Math.max(0, drag.z0.s + (drag.start - idx)));
|
|
2027
|
+
applyZoom(s, s + span);
|
|
2028
|
+
} else {
|
|
2029
|
+
setDrag({ ...drag, end: idx });
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
const onOverlayDown = (e) => {
|
|
2034
|
+
const idx = catFromEvent(e);
|
|
2035
|
+
if (idx == null) return;
|
|
2036
|
+
setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
|
|
2037
|
+
};
|
|
2038
|
+
const onOverlayUp = () => {
|
|
2039
|
+
if (drag && !drag.pan) {
|
|
2040
|
+
const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
|
|
2041
|
+
if (hi - lo >= 1) applyZoom(baseIdx + lo, baseIdx + hi);
|
|
2042
|
+
}
|
|
2043
|
+
setDrag(null);
|
|
2044
|
+
};
|
|
2045
|
+
const onOverlayClick = (e) => {
|
|
2046
|
+
if (!onDataClick) return;
|
|
2047
|
+
const idx = catFromEvent(e);
|
|
2048
|
+
if (idx == null) return;
|
|
2049
|
+
const d = rows[idx];
|
|
2050
|
+
onDataClick({ label: d.label, series: null, seriesIndex: -1, value: void 0, index: baseIdx + idx, row: d });
|
|
2051
|
+
selectMark(`${idx}:*`);
|
|
2052
|
+
};
|
|
2053
|
+
React51__default.useEffect(() => {
|
|
2054
|
+
if (!zoomable) return void 0;
|
|
2055
|
+
const svg = svgRef.current;
|
|
2056
|
+
if (!svg) return void 0;
|
|
2057
|
+
const onWheel = (e) => {
|
|
2058
|
+
e.preventDefault();
|
|
2059
|
+
const n = allRows.length;
|
|
2060
|
+
const cur = zoom || { s: 0, e: n - 1 };
|
|
2061
|
+
const span = cur.e - cur.s;
|
|
2062
|
+
const r2 = svg.getBoundingClientRect();
|
|
2063
|
+
const frac = r2.width > 0 ? Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW)) : 0.5;
|
|
2064
|
+
const center = cur.s + Math.round(frac * span);
|
|
2065
|
+
const newSpan = Math.min(n - 1, Math.max(1, Math.round(span * (e.deltaY < 0 ? 0.8 : 1.25))));
|
|
2066
|
+
const s = Math.min(n - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frac)));
|
|
2067
|
+
applyZoom(s, s + newSpan);
|
|
2068
|
+
};
|
|
2069
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
2070
|
+
return () => svg.removeEventListener("wheel", onWheel);
|
|
2071
|
+
}, [zoomable, zoom, allRows.length]);
|
|
1868
2072
|
const renderBars = () => {
|
|
1869
2073
|
const band = horizontal ? catBandH : catBand;
|
|
1870
|
-
const
|
|
1871
|
-
const slot = band * (1 - gap);
|
|
2074
|
+
const slot = band * 0.72;
|
|
1872
2075
|
return rows.map((d, gi) => {
|
|
1873
|
-
const gStart = (horizontal ? padT : padL) + band * gi + band *
|
|
1874
|
-
const
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2076
|
+
const gStart = (horizontal ? padT : padL) + band * gi + band * 0.28 / 2;
|
|
2077
|
+
const handlers = { onMouseMove: (e) => onMarkMove(d, gi, e), onMouseLeave: onMarkLeave };
|
|
2078
|
+
const barCommon = (k, key) => ({
|
|
2079
|
+
className: `twc-chart__bar ${animate ? "twc-chart__anim-bar" : ""}`.trim(),
|
|
2080
|
+
"data-mark": true,
|
|
2081
|
+
"data-horizontal": horizontal || void 0,
|
|
2082
|
+
"data-active": focusActive(k),
|
|
2083
|
+
"data-selected": selected === key || void 0,
|
|
2084
|
+
style: { fill: colorFor(k) },
|
|
2085
|
+
rx: 2,
|
|
2086
|
+
...handlers,
|
|
2087
|
+
onClick: () => {
|
|
2088
|
+
clickDatum(d, k, gi);
|
|
2089
|
+
selectMark(key);
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
1878
2092
|
if (canStack) {
|
|
1879
2093
|
let acc = 0;
|
|
1880
|
-
return keys.map((k
|
|
2094
|
+
return keys.map((k) => {
|
|
1881
2095
|
const v = Number(d[k]) || 0;
|
|
1882
2096
|
const p0 = vPix(acc), p1 = vPix(acc + v);
|
|
1883
2097
|
acc += v;
|
|
1884
|
-
const
|
|
1885
|
-
|
|
1886
|
-
return horizontal ? /* @__PURE__ */ jsx("rect", { ...common, x: padL + p0, y: gStart, width: Math.max(0, p1 - p0), height: Math.max(1, slot) }, `${gi}-${si}`) : /* @__PURE__ */ jsx("rect", { ...common, x: gStart, y: padT + innerH - p1, width: Math.max(1, slot), height: Math.max(0, p1 - p0) }, `${gi}-${si}`);
|
|
2098
|
+
const key = `${gi}:${k}`;
|
|
2099
|
+
return horizontal ? /* @__PURE__ */ jsx("rect", { ...barCommon(k, key), x: padL + p0, y: gStart, width: Math.max(0, p1 - p0), height: Math.max(1, slot) }, key) : /* @__PURE__ */ jsx("rect", { ...barCommon(k, key), x: gStart, y: padT + innerH - p1, width: Math.max(1, slot), height: Math.max(0, p1 - p0) }, key);
|
|
1887
2100
|
});
|
|
1888
2101
|
}
|
|
1889
2102
|
const bw = slot / Math.max(1, keys.length);
|
|
1890
2103
|
return keys.map((k, si) => {
|
|
1891
2104
|
const v = Number(d[k]) || 0;
|
|
1892
2105
|
const len2 = vPix(v);
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
return horizontal ? /* @__PURE__ */ jsx("rect", { ...common, x: padL, y: gStart + si * bw + 0.5, width: Math.max(0, len2), height: Math.max(1, bw - 1) }, `${gi}-${si}`) : /* @__PURE__ */ jsx("rect", { ...common, x: gStart + si * bw + 0.5, y: padT + innerH - len2, width: Math.max(1, bw - 1), height: Math.max(0, len2) }, `${gi}-${si}`);
|
|
2106
|
+
const key = `${gi}:${k}`;
|
|
2107
|
+
return horizontal ? /* @__PURE__ */ jsx("rect", { ...barCommon(k, key), x: padL, y: gStart + si * bw + 0.5, width: Math.max(0, len2), height: Math.max(1, bw - 1) }, key) : /* @__PURE__ */ jsx("rect", { ...barCommon(k, key), x: gStart + si * bw + 0.5, y: padT + innerH - len2, width: Math.max(1, bw - 1), height: Math.max(0, len2) }, key);
|
|
1896
2108
|
});
|
|
1897
2109
|
});
|
|
1898
2110
|
};
|
|
@@ -1907,7 +2119,7 @@ function Chart({
|
|
|
1907
2119
|
});
|
|
1908
2120
|
const base = canStack ? rows.map((d, i) => vPos(stackAcc[i] - (Number(d[k]) || 0))) : null;
|
|
1909
2121
|
const gradId = `twc-grad-${gid}-${allKeys.indexOf(k)}`;
|
|
1910
|
-
return /* @__PURE__ */ jsxs("g", { children: [
|
|
2122
|
+
return /* @__PURE__ */ jsxs("g", { "data-mark": true, "data-active": focusActive(k), children: [
|
|
1911
2123
|
isArea ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1912
2124
|
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gradId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
|
|
1913
2125
|
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.34" }),
|
|
@@ -1916,14 +2128,14 @@ function Chart({
|
|
|
1916
2128
|
/* @__PURE__ */ jsx(
|
|
1917
2129
|
"path",
|
|
1918
2130
|
{
|
|
1919
|
-
className: `twc-chart__area ${
|
|
2131
|
+
className: `twc-chart__area ${animate ? "twc-chart__anim-fade" : ""}`.trim(),
|
|
1920
2132
|
style: { fill: `url(#${gradId})` },
|
|
1921
2133
|
d: canStack ? `${linePath(pts, pathOpts)} ${pts.map((p, i) => `L${p[0]} ${base[i]}`).reverse().join(" ")} Z` : areaPath(pts, padT + innerH, pathOpts)
|
|
1922
2134
|
}
|
|
1923
2135
|
)
|
|
1924
2136
|
] }) : null,
|
|
1925
|
-
/* @__PURE__ */ jsx("path", { className: `twc-chart__line ${
|
|
1926
|
-
showDots || !isArea ? pts.map((p, i) => /* @__PURE__ */ jsxs("g", { className:
|
|
2137
|
+
/* @__PURE__ */ jsx("path", { className: `twc-chart__line ${animate ? "twc-chart__anim-line" : ""}`.trim(), style: { stroke: color }, pathLength: "1", d: linePath(pts, pathOpts) }),
|
|
2138
|
+
showDots || !isArea ? pts.map((p, i) => /* @__PURE__ */ jsxs("g", { className: animate ? "twc-chart__anim-fade" : void 0, children: [
|
|
1927
2139
|
/* @__PURE__ */ jsx(
|
|
1928
2140
|
"circle",
|
|
1929
2141
|
{
|
|
@@ -1931,11 +2143,25 @@ function Chart({
|
|
|
1931
2143
|
cx: p[0],
|
|
1932
2144
|
cy: p[1],
|
|
1933
2145
|
r: "14",
|
|
1934
|
-
onMouseMove: (e) =>
|
|
1935
|
-
onMouseLeave:
|
|
2146
|
+
onMouseMove: (e) => onMarkMove(rows[i], i, e),
|
|
2147
|
+
onMouseLeave: onMarkLeave,
|
|
2148
|
+
onClick: () => {
|
|
2149
|
+
clickDatum(rows[i], k, i);
|
|
2150
|
+
selectMark(`${i}:${k}`);
|
|
2151
|
+
}
|
|
1936
2152
|
}
|
|
1937
2153
|
),
|
|
1938
|
-
/* @__PURE__ */ jsx(
|
|
2154
|
+
/* @__PURE__ */ jsx(
|
|
2155
|
+
"circle",
|
|
2156
|
+
{
|
|
2157
|
+
className: "twc-chart__dot",
|
|
2158
|
+
style: { stroke: color },
|
|
2159
|
+
cx: p[0],
|
|
2160
|
+
cy: p[1],
|
|
2161
|
+
r: hoverIdx === i ? 5 : 3.5,
|
|
2162
|
+
"data-selected": selected === `${i}:${k}` || void 0
|
|
2163
|
+
}
|
|
2164
|
+
)
|
|
1939
2165
|
] }, i)) : null
|
|
1940
2166
|
] }, k);
|
|
1941
2167
|
});
|
|
@@ -1961,62 +2187,153 @@ function Chart({
|
|
|
1961
2187
|
return /* @__PURE__ */ jsx("text", { className: "twc-chart__label", x: lineX(i), y: vPos(v) - 9, textAnchor: "middle", children: shortNum(v) }, `${k}-${i}`);
|
|
1962
2188
|
}));
|
|
1963
2189
|
};
|
|
2190
|
+
const renderZones = () => rows.map((d, i) => {
|
|
2191
|
+
let x, y, w, hgt;
|
|
2192
|
+
if (isBar && horizontal) {
|
|
2193
|
+
x = padL;
|
|
2194
|
+
w = innerW;
|
|
2195
|
+
y = padT + catBandH * i;
|
|
2196
|
+
hgt = catBandH;
|
|
2197
|
+
} else if (isBar) {
|
|
2198
|
+
x = padL + catBand * i;
|
|
2199
|
+
w = catBand;
|
|
2200
|
+
y = padT;
|
|
2201
|
+
hgt = innerH;
|
|
2202
|
+
} else {
|
|
2203
|
+
const x0 = i === 0 ? padL : (lineX(i - 1) + lineX(i)) / 2;
|
|
2204
|
+
const x1 = i === rows.length - 1 ? padL + innerW : (lineX(i) + lineX(i + 1)) / 2;
|
|
2205
|
+
x = x0;
|
|
2206
|
+
w = x1 - x0;
|
|
2207
|
+
y = padT;
|
|
2208
|
+
hgt = innerH;
|
|
2209
|
+
}
|
|
2210
|
+
return /* @__PURE__ */ jsx(
|
|
2211
|
+
"rect",
|
|
2212
|
+
{
|
|
2213
|
+
className: "twc-chart__zone",
|
|
2214
|
+
x,
|
|
2215
|
+
y,
|
|
2216
|
+
width: Math.max(0, w),
|
|
2217
|
+
height: hgt,
|
|
2218
|
+
"data-clickable": clickable || void 0,
|
|
2219
|
+
onMouseMove: (e) => {
|
|
2220
|
+
show(tipFor(d), e);
|
|
2221
|
+
setHoverIdx(i);
|
|
2222
|
+
},
|
|
2223
|
+
onMouseLeave: onMarkLeave,
|
|
2224
|
+
onClick: () => {
|
|
2225
|
+
if (onDataClick) onDataClick({ label: d.label, series: null, seriesIndex: -1, value: void 0, index: baseIdx + i, row: d });
|
|
2226
|
+
}
|
|
2227
|
+
},
|
|
2228
|
+
i
|
|
2229
|
+
);
|
|
2230
|
+
});
|
|
1964
2231
|
const toggle = (it) => setHidden((prev) => {
|
|
1965
2232
|
const next = new Set(prev);
|
|
1966
2233
|
if (next.has(it.label)) next.delete(it.label);
|
|
1967
2234
|
else if (allKeys.length - next.size > 1) next.add(it.label);
|
|
1968
2235
|
return next;
|
|
1969
2236
|
});
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2237
|
+
const showCrosshair = crosshair && !horizontal && hoverIdx != null && rows[hoverIdx];
|
|
2238
|
+
const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
|
|
2239
|
+
return /* @__PURE__ */ jsxs(
|
|
2240
|
+
"div",
|
|
2241
|
+
{
|
|
2242
|
+
ref: containerRef,
|
|
2243
|
+
className: `twc-chart twc-chart--${type} ${className}`.trim(),
|
|
2244
|
+
"data-hovering": focusSeries != null || void 0,
|
|
2245
|
+
"data-has-selection": selected != null || void 0,
|
|
2246
|
+
"data-clickable": clickable || void 0,
|
|
2247
|
+
...rest,
|
|
2248
|
+
children: [
|
|
2249
|
+
baseStyles,
|
|
2250
|
+
styles,
|
|
2251
|
+
/* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
|
|
2252
|
+
showGrid ? ticks.map((t, i) => {
|
|
2253
|
+
const p = vPos(t);
|
|
2254
|
+
return horizontal ? /* @__PURE__ */ jsx("line", { className: "twc-chart__grid", x1: p, y1: padT, x2: p, y2: padT + innerH }, i) : /* @__PURE__ */ jsx("line", { className: "twc-chart__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
|
|
2255
|
+
}) : null,
|
|
2256
|
+
showAxis ? /* @__PURE__ */ jsx("line", { className: "twc-chart__axisline", x1: padL, y1: padT, x2: padL, y2: padT + innerH }) : null,
|
|
2257
|
+
showAxis ? /* @__PURE__ */ jsx("line", { className: "twc-chart__axisline", x1: padL, y1: padT + innerH, x2: W - padR, y2: padT + innerH }) : null,
|
|
2258
|
+
showAxis ? ticks.map((t, i) => {
|
|
2259
|
+
const p = vPos(t);
|
|
2260
|
+
return horizontal ? /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: p, y: H - 8, textAnchor: "middle", children: shortNum(t) }, i) : /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: padL - 8, y: p + 4, textAnchor: "end", children: shortNum(t) }, i);
|
|
2261
|
+
}) : null,
|
|
2262
|
+
showCrosshair ? /* @__PURE__ */ jsx("line", { className: "twc-chart__crosshair", x1: catX(hoverIdx), y1: padT, x2: catX(hoverIdx), y2: padT + innerH }) : null,
|
|
2263
|
+
!zoomable ? renderZones() : null,
|
|
2264
|
+
isBar ? renderBars() : renderLines(),
|
|
2265
|
+
renderValues(),
|
|
2266
|
+
showAxis ? rows.map((d, i) => {
|
|
2267
|
+
if (horizontal) {
|
|
2268
|
+
const cy = padT + catBandH * i + catBandH / 2;
|
|
2269
|
+
return /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: padL - 8, y: cy + 4, textAnchor: "end", children: d.label }, i);
|
|
2270
|
+
}
|
|
2271
|
+
return /* @__PURE__ */ jsx("text", { className: "twc-chart__axis", x: catX(i), y: H - 8, textAnchor: "middle", children: d.label }, i);
|
|
2272
|
+
}) : null,
|
|
2273
|
+
dragBand ? (() => {
|
|
2274
|
+
const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
|
|
2275
|
+
const x0 = isBar ? padL + catBand * lo : lineX(lo);
|
|
2276
|
+
const x1 = isBar ? padL + catBand * (hi + 1) : lineX(hi);
|
|
2277
|
+
return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
|
|
2278
|
+
})() : null,
|
|
2279
|
+
zoomable ? /* @__PURE__ */ jsx(
|
|
2280
|
+
"rect",
|
|
2281
|
+
{
|
|
2282
|
+
className: "twc-chart__overlay",
|
|
2283
|
+
"data-zoom": "true",
|
|
2284
|
+
"data-clickable": clickable || void 0,
|
|
2285
|
+
"data-panning": drag && drag.pan || void 0,
|
|
2286
|
+
x: padL,
|
|
2287
|
+
y: padT,
|
|
2288
|
+
width: innerW,
|
|
2289
|
+
height: innerH,
|
|
2290
|
+
onMouseMove: onOverlayMove,
|
|
2291
|
+
onMouseLeave: () => {
|
|
2292
|
+
onMarkLeave();
|
|
2293
|
+
setDrag(null);
|
|
2294
|
+
},
|
|
2295
|
+
onMouseDown: onOverlayDown,
|
|
2296
|
+
onMouseUp: onOverlayUp,
|
|
2297
|
+
onClick: onOverlayClick
|
|
2298
|
+
}
|
|
2299
|
+
) : null
|
|
2300
|
+
] }),
|
|
2301
|
+
zoomable && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
|
|
2302
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2303
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
2304
|
+
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
2305
|
+
] }),
|
|
2306
|
+
"Reset"
|
|
2307
|
+
] }) : null,
|
|
2308
|
+
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
2309
|
+
tableFallback ? /* @__PURE__ */ jsx(
|
|
2310
|
+
ChartTable,
|
|
2311
|
+
{
|
|
2312
|
+
id: tableId,
|
|
2313
|
+
caption: caption ?? svgAriaLabel,
|
|
2314
|
+
columns: allKeys,
|
|
2315
|
+
rows: allRows.map((d) => ({ label: d.label, values: allKeys.map((k) => fmt(Number(d[k]) || 0)) }))
|
|
2316
|
+
}
|
|
2317
|
+
) : null,
|
|
2318
|
+
showLegend && multi ? /* @__PURE__ */ jsx(
|
|
2319
|
+
ChartLegend,
|
|
2320
|
+
{
|
|
2321
|
+
items: allKeys.map((k) => ({ label: k, color: colorFor(k) })),
|
|
2322
|
+
onToggle: toggle,
|
|
2323
|
+
hidden: (it) => hidden.has(it.label),
|
|
2324
|
+
onFocus: (it) => setFocusSeries(it.label),
|
|
2325
|
+
onBlur: () => setFocusSeries(null)
|
|
2326
|
+
}
|
|
2327
|
+
) : null
|
|
2328
|
+
]
|
|
2329
|
+
}
|
|
2330
|
+
);
|
|
2014
2331
|
}
|
|
2015
2332
|
var CHART_CSS2 = `
|
|
2016
2333
|
.twc-chart--pie svg { overflow: visible; }
|
|
2017
2334
|
.twc-chart__slice { transition: opacity var(--duration-fast) var(--ease-standard); stroke: var(--color-surface); stroke-width: 1.5; }
|
|
2018
2335
|
.twc-chart__slice:hover { opacity: 0.85; }
|
|
2019
|
-
.twc-chart__slice-label { fill: var(--color-
|
|
2336
|
+
.twc-chart__slice-label { fill: var(--color-chart-on-fill); paint-order: stroke; stroke: var(--color-chart-on-fill-halo); stroke-width: 3px; stroke-linejoin: round; vector-effect: non-scaling-stroke; font-size: 11px; font-weight: 600; }
|
|
2020
2337
|
.twc-chart__center { fill: var(--color-text); font-weight: 700; }
|
|
2021
2338
|
.twc-chart__center-sub { fill: var(--color-text-subtle); }
|
|
2022
2339
|
.twc-chart__empty { fill: var(--color-text-subtle); font-size: var(--text-sm); }
|
|
@@ -2033,6 +2350,7 @@ function PieChart({
|
|
|
2033
2350
|
valueFormat,
|
|
2034
2351
|
height = 260,
|
|
2035
2352
|
colors,
|
|
2353
|
+
onDataClick,
|
|
2036
2354
|
ariaLabel,
|
|
2037
2355
|
"aria-label": ariaLabelProp,
|
|
2038
2356
|
tableFallback = true,
|
|
@@ -2046,6 +2364,9 @@ function PieChart({
|
|
|
2046
2364
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
2047
2365
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
2048
2366
|
const [active, setActive] = React51__default.useState(null);
|
|
2367
|
+
const [focus, setFocus] = React51__default.useState(null);
|
|
2368
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
2369
|
+
const clickable = !!onDataClick;
|
|
2049
2370
|
const rows = data || [];
|
|
2050
2371
|
const fmt = valueFormat || fmtNumber;
|
|
2051
2372
|
const values = rows.map((d) => Math.max(0, Number(d.value) || 0));
|
|
@@ -2069,20 +2390,37 @@ function PieChart({
|
|
|
2069
2390
|
const color = d.color || paletteAt(colors, i);
|
|
2070
2391
|
return { d, i, v, frac, s0, s1, span, pct, color };
|
|
2071
2392
|
});
|
|
2072
|
-
|
|
2393
|
+
const clickDatum = (d, i, v, pct) => {
|
|
2394
|
+
if (!onDataClick) return;
|
|
2395
|
+
onDataClick({ label: d.label, value: v, index: i, percent: pct });
|
|
2396
|
+
};
|
|
2397
|
+
const selectMark = (i) => setSelected((s) => s === i ? null : i);
|
|
2398
|
+
const emph = focus != null ? focus : active;
|
|
2399
|
+
const multi = rows.length > 1;
|
|
2400
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--pie ${className}`.trim(), "data-donut": isDonut ? "true" : void 0, "data-has-selection": selected != null || void 0, "data-hovering": emph != null ? "true" : void 0, "data-clickable": clickable || void 0, ...rest, children: [
|
|
2073
2401
|
baseStyles,
|
|
2074
2402
|
styles,
|
|
2075
2403
|
/* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${H} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, children: total <= 0 ? /* @__PURE__ */ jsx("text", { className: "twc-chart__empty", x: cx, y: cy, textAnchor: "middle", dominantBaseline: "middle", children: "No data" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2076
2404
|
slices.map(({ d, i, v, s0, s1, span, pct, color }) => {
|
|
2077
2405
|
const pad = span > padAngle ? padAngle / 2 : 0;
|
|
2078
2406
|
const items = [{ color, label: fmt(v), value: `${pct}%` }];
|
|
2407
|
+
let explode;
|
|
2408
|
+
if (selected === i) {
|
|
2409
|
+
const mid = (s0 + s1) / 2;
|
|
2410
|
+
const a = (mid - 90) * Math.PI / 180;
|
|
2411
|
+
const dx = Math.cos(a) * 8, dy = Math.sin(a) * 8;
|
|
2412
|
+
if (Number.isFinite(dx) && Number.isFinite(dy)) {
|
|
2413
|
+
explode = `translate(${dx}px, ${dy}px)`;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2079
2416
|
return /* @__PURE__ */ jsx(
|
|
2080
2417
|
"path",
|
|
2081
2418
|
{
|
|
2082
2419
|
className: "twc-chart__slice twc-chart__anim-arc",
|
|
2083
|
-
style: { fill: color },
|
|
2420
|
+
style: { fill: color, transform: explode },
|
|
2084
2421
|
"data-mark": "",
|
|
2085
|
-
"data-active":
|
|
2422
|
+
"data-active": emph === i ? "true" : void 0,
|
|
2423
|
+
"data-selected": selected === i ? "true" : void 0,
|
|
2086
2424
|
d: arcPath(cx, cy, rOuter, rInner, s0 + pad, s1 - pad),
|
|
2087
2425
|
onMouseMove: (e) => {
|
|
2088
2426
|
setActive(i);
|
|
@@ -2091,6 +2429,10 @@ function PieChart({
|
|
|
2091
2429
|
onMouseLeave: () => {
|
|
2092
2430
|
setActive(null);
|
|
2093
2431
|
hide();
|
|
2432
|
+
},
|
|
2433
|
+
onClick: () => {
|
|
2434
|
+
clickDatum(d, i, v, pct);
|
|
2435
|
+
selectMark(i);
|
|
2094
2436
|
}
|
|
2095
2437
|
},
|
|
2096
2438
|
i
|
|
@@ -2126,7 +2468,14 @@ function PieChart({
|
|
|
2126
2468
|
rows: slices.map(({ d, v, pct }) => ({ label: d.label, values: [fmt(v), `${pct}%`] }))
|
|
2127
2469
|
}
|
|
2128
2470
|
) : null,
|
|
2129
|
-
showLegend && rows.length ? /* @__PURE__ */ jsx(
|
|
2471
|
+
showLegend && rows.length ? /* @__PURE__ */ jsx(
|
|
2472
|
+
ChartLegend,
|
|
2473
|
+
{
|
|
2474
|
+
items: slices.map(({ d, color }) => ({ label: d.label, color })),
|
|
2475
|
+
onFocus: multi ? (it, i) => setFocus(i) : void 0,
|
|
2476
|
+
onBlur: multi ? () => setFocus(null) : void 0
|
|
2477
|
+
}
|
|
2478
|
+
) : null
|
|
2130
2479
|
] });
|
|
2131
2480
|
}
|
|
2132
2481
|
function DonutChart(props) {
|
|
@@ -2138,6 +2487,7 @@ var GAUGE_CSS = `
|
|
|
2138
2487
|
.twc-gauge__arc { fill: none; stroke-linecap: round; transition: stroke-dashoffset var(--duration-slow) var(--ease-standard); }
|
|
2139
2488
|
.twc-gauge__value { fill: var(--color-text); font-weight: 700; }
|
|
2140
2489
|
.twc-gauge__label { fill: var(--color-text-subtle); }
|
|
2490
|
+
.twc-gauge__arc[data-selected="true"] { filter: drop-shadow(0 0 2px var(--color-text)); }
|
|
2141
2491
|
`;
|
|
2142
2492
|
function Gauge({
|
|
2143
2493
|
value = 0,
|
|
@@ -2154,6 +2504,7 @@ function Gauge({
|
|
|
2154
2504
|
size = 200,
|
|
2155
2505
|
series,
|
|
2156
2506
|
showLegend = true,
|
|
2507
|
+
onDataClick,
|
|
2157
2508
|
ariaLabel,
|
|
2158
2509
|
"aria-label": ariaLabelProp,
|
|
2159
2510
|
tableFallback = true,
|
|
@@ -2166,6 +2517,8 @@ function Gauge({
|
|
|
2166
2517
|
const uid = React51__default.useId();
|
|
2167
2518
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
2168
2519
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
2520
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
2521
|
+
const [focus, setFocus] = React51__default.useState(null);
|
|
2169
2522
|
const fmt = valueFormat || ((n) => String(Math.round(Number(n) || 0)));
|
|
2170
2523
|
const cx = size / 2, cy = size / 2;
|
|
2171
2524
|
const span = endAngle - startAngle;
|
|
@@ -2190,79 +2543,106 @@ function Gauge({
|
|
|
2190
2543
|
return `M${r(x0)} ${r(y0)} A${r(radius)} ${r(radius)} 0 ${large} ${sweep} ${r(x1)} ${r(y1)}`;
|
|
2191
2544
|
};
|
|
2192
2545
|
const single = items[0];
|
|
2546
|
+
const clickable = !!onDataClick;
|
|
2547
|
+
const activeFor = (i) => focus == null ? void 0 : i === focus || void 0;
|
|
2548
|
+
const clickRing = (it, i) => {
|
|
2549
|
+
if (onDataClick) onDataClick({ value: Number(it.value) || 0, label: it.label, index: i });
|
|
2550
|
+
};
|
|
2551
|
+
const selectRing = (i) => setSelected((s) => s === i ? null : i);
|
|
2193
2552
|
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? (isMulti ? `Gauge with ${items.length} values` : `${labelText(label) || "Gauge"}: ${fmt(single.value)} of ${fmt(max)}`);
|
|
2194
2553
|
const valueFont = Math.round(size * (isMulti ? 0.11 : 0.17));
|
|
2195
2554
|
const labelFont = Math.round(size * 0.072);
|
|
2196
|
-
return /* @__PURE__ */ jsxs(
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
-
|
|
2555
|
+
return /* @__PURE__ */ jsxs(
|
|
2556
|
+
"div",
|
|
2557
|
+
{
|
|
2558
|
+
ref: containerRef,
|
|
2559
|
+
className: `twc-gauge twc-chart twc-chart--gauge ${className}`.trim(),
|
|
2560
|
+
"data-multi": isMulti || void 0,
|
|
2561
|
+
"data-hovering": focus != null || void 0,
|
|
2562
|
+
"data-clickable": clickable || void 0,
|
|
2563
|
+
"data-has-selection": selected != null || void 0,
|
|
2564
|
+
...rest,
|
|
2565
|
+
children: [
|
|
2566
|
+
baseStyles,
|
|
2567
|
+
styles,
|
|
2568
|
+
/* @__PURE__ */ jsxs(
|
|
2569
|
+
"svg",
|
|
2570
|
+
{
|
|
2571
|
+
viewBox: `0 0 ${size} ${size}`,
|
|
2572
|
+
width: size,
|
|
2573
|
+
height: size,
|
|
2574
|
+
role: "img",
|
|
2575
|
+
"aria-label": svgAriaLabel,
|
|
2576
|
+
"aria-describedby": tableId,
|
|
2577
|
+
children: [
|
|
2578
|
+
items.map((it, i) => {
|
|
2579
|
+
const radius = ringRadius(i);
|
|
2580
|
+
const f = frac(it.value);
|
|
2581
|
+
const c = it.color || paletteAt(void 0, i);
|
|
2582
|
+
const valEnd = startAngle + f * span;
|
|
2583
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
2584
|
+
/* @__PURE__ */ jsx(
|
|
2585
|
+
"path",
|
|
2586
|
+
{
|
|
2587
|
+
className: "twc-gauge__track",
|
|
2588
|
+
d: arc(radius, startAngle, endAngle),
|
|
2589
|
+
style: { stroke: trackColor, strokeWidth: strokeW }
|
|
2590
|
+
}
|
|
2591
|
+
),
|
|
2592
|
+
f > 0 ? /* @__PURE__ */ jsx(
|
|
2593
|
+
"path",
|
|
2594
|
+
{
|
|
2595
|
+
className: "twc-gauge__arc twc-chart__anim-fade",
|
|
2596
|
+
"data-mark": true,
|
|
2597
|
+
"data-active": activeFor(i),
|
|
2598
|
+
"data-selected": selected === i || void 0,
|
|
2599
|
+
d: arc(radius, startAngle, valEnd),
|
|
2600
|
+
style: { stroke: c, strokeWidth: strokeW },
|
|
2601
|
+
onMouseMove: (e) => show({
|
|
2602
|
+
title: labelText(it.label) || "Value",
|
|
2603
|
+
items: [{ color: c, label: "", value: fmt(it.value) }]
|
|
2604
|
+
}, e),
|
|
2605
|
+
onMouseLeave: hide,
|
|
2606
|
+
onClick: () => {
|
|
2607
|
+
clickRing(it, i);
|
|
2608
|
+
selectRing(i);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
) : null
|
|
2612
|
+
] }, i);
|
|
2613
|
+
}),
|
|
2614
|
+
!isMulti && showValue ? /* @__PURE__ */ jsx("text", { className: "twc-gauge__value", x: cx, y: cy, textAnchor: "middle", dominantBaseline: "middle", style: { fontSize: valueFont }, children: fmt(single.value) }) : null,
|
|
2615
|
+
!isMulti && label != null ? /* @__PURE__ */ jsx("text", { className: "twc-gauge__label", x: cx, y: cy + valueFont * 0.78, textAnchor: "middle", style: { fontSize: labelFont }, children: labelText(label) }) : null
|
|
2616
|
+
]
|
|
2617
|
+
}
|
|
2618
|
+
),
|
|
2619
|
+
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
2620
|
+
tableFallback ? /* @__PURE__ */ jsx(
|
|
2621
|
+
ChartTable,
|
|
2622
|
+
{
|
|
2623
|
+
id: tableId,
|
|
2624
|
+
caption: caption ?? svgAriaLabel,
|
|
2625
|
+
columns: ["Value"],
|
|
2626
|
+
rows: items.map((it, i) => ({
|
|
2627
|
+
label: labelText(it.label) || (isMulti ? `Series ${i + 1}` : "Value"),
|
|
2628
|
+
values: [fmt(it.value)]
|
|
2629
|
+
}))
|
|
2630
|
+
}
|
|
2631
|
+
) : null,
|
|
2632
|
+
isMulti && showLegend ? /* @__PURE__ */ jsx(
|
|
2633
|
+
ChartLegend,
|
|
2634
|
+
{
|
|
2635
|
+
items: items.map((it, i) => ({
|
|
2636
|
+
label: labelText(it.label) || `Series ${i + 1}`,
|
|
2637
|
+
color: it.color || paletteAt(void 0, i)
|
|
2638
|
+
})),
|
|
2639
|
+
onFocus: items.length > 1 ? (it, i) => setFocus(i) : void 0,
|
|
2640
|
+
onBlur: items.length > 1 ? () => setFocus(null) : void 0
|
|
2641
|
+
}
|
|
2642
|
+
) : null
|
|
2643
|
+
]
|
|
2644
|
+
}
|
|
2645
|
+
);
|
|
2266
2646
|
}
|
|
2267
2647
|
function labelText(label) {
|
|
2268
2648
|
return typeof label === "string" || typeof label === "number" ? String(label) : "";
|
|
@@ -2289,6 +2669,7 @@ function Sparkline({
|
|
|
2289
2669
|
min,
|
|
2290
2670
|
max,
|
|
2291
2671
|
valueFormat,
|
|
2672
|
+
onDataClick,
|
|
2292
2673
|
ariaLabel,
|
|
2293
2674
|
"aria-label": ariaLabelProp,
|
|
2294
2675
|
tableFallback = false,
|
|
@@ -2302,6 +2683,8 @@ function Sparkline({
|
|
|
2302
2683
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
2303
2684
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
2304
2685
|
const [hover, setHover] = React51__default.useState(null);
|
|
2686
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
2687
|
+
const clickable = !!onDataClick;
|
|
2305
2688
|
const kind = area ? "area" : type;
|
|
2306
2689
|
const isBar = kind === "bar";
|
|
2307
2690
|
const isArea = kind === "area";
|
|
@@ -2343,6 +2726,10 @@ function Sparkline({
|
|
|
2343
2726
|
setHover(null);
|
|
2344
2727
|
hide();
|
|
2345
2728
|
};
|
|
2729
|
+
const clickPoint = (i) => {
|
|
2730
|
+
if (onDataClick) onDataClick({ value: values[i], index: i, label: labels[i] });
|
|
2731
|
+
setSelected((s) => s === i ? null : i);
|
|
2732
|
+
};
|
|
2346
2733
|
const last = n ? values[n - 1] : 0;
|
|
2347
2734
|
const summary = !n ? "sparkline, no data" : tableFallback ? `sparkline, ${n} points, latest ${fmt(last)}` : `sparkline, ${n} points, first ${fmt(values[0])}, latest ${fmt(last)}, min ${fmt(Math.min(...values))}, max ${fmt(Math.max(...values))}`;
|
|
2348
2735
|
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? summary;
|
|
@@ -2355,94 +2742,120 @@ function Sparkline({
|
|
|
2355
2742
|
"rect",
|
|
2356
2743
|
{
|
|
2357
2744
|
className: "twc-sparkline__bar twc-chart__anim-fade",
|
|
2745
|
+
"data-mark": true,
|
|
2746
|
+
"data-selected": selected === i || void 0,
|
|
2358
2747
|
style: { fill: color },
|
|
2359
2748
|
x: pad + band * i + (band - slot) / 2,
|
|
2360
2749
|
y: baselineY - h,
|
|
2361
2750
|
width: Math.max(1, slot),
|
|
2362
2751
|
height: Math.max(1, h),
|
|
2363
2752
|
onMouseMove: (e) => show(tipFor(i), e),
|
|
2364
|
-
onMouseLeave: hide
|
|
2753
|
+
onMouseLeave: hide,
|
|
2754
|
+
onClick: () => clickPoint(i)
|
|
2365
2755
|
},
|
|
2366
2756
|
i
|
|
2367
2757
|
);
|
|
2368
2758
|
});
|
|
2369
2759
|
};
|
|
2370
|
-
return /* @__PURE__ */ jsxs(
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
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
|
-
|
|
2760
|
+
return /* @__PURE__ */ jsxs(
|
|
2761
|
+
"div",
|
|
2762
|
+
{
|
|
2763
|
+
ref: containerRef,
|
|
2764
|
+
className: `twc-chart twc-sparkline twc-chart--${kind} ${className}`.trim(),
|
|
2765
|
+
"data-clickable": clickable || void 0,
|
|
2766
|
+
"data-has-selection": selected != null || void 0,
|
|
2767
|
+
...rest,
|
|
2768
|
+
children: [
|
|
2769
|
+
baseStyles,
|
|
2770
|
+
styles,
|
|
2771
|
+
/* @__PURE__ */ jsx(
|
|
2772
|
+
"svg",
|
|
2773
|
+
{
|
|
2774
|
+
viewBox: `0 0 ${W} ${H}`,
|
|
2775
|
+
width: W,
|
|
2776
|
+
height: H,
|
|
2777
|
+
role: "img",
|
|
2778
|
+
"aria-label": svgAriaLabel,
|
|
2779
|
+
"aria-describedby": tableId,
|
|
2780
|
+
children: n === 0 ? null : isBar ? renderBars() : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2781
|
+
isArea ? /* @__PURE__ */ jsx("path", { className: "twc-sparkline__area twc-chart__anim-fade", style: { fill: color }, d: areaPath(points, baselineY, pathOpts) }) : null,
|
|
2782
|
+
/* @__PURE__ */ jsx(
|
|
2783
|
+
"path",
|
|
2784
|
+
{
|
|
2785
|
+
className: "twc-sparkline__line twc-chart__anim-line",
|
|
2786
|
+
style: { stroke: color, strokeWidth },
|
|
2787
|
+
pathLength: "1",
|
|
2788
|
+
d: linePath(points, pathOpts)
|
|
2789
|
+
}
|
|
2790
|
+
),
|
|
2791
|
+
showDots && n ? /* @__PURE__ */ jsx(
|
|
2792
|
+
"circle",
|
|
2793
|
+
{
|
|
2794
|
+
className: "twc-sparkline__dot twc-chart__anim-fade",
|
|
2795
|
+
style: { fill: color },
|
|
2796
|
+
cx: points[n - 1][0],
|
|
2797
|
+
cy: points[n - 1][1],
|
|
2798
|
+
r: dotR
|
|
2799
|
+
}
|
|
2800
|
+
) : null,
|
|
2801
|
+
hover != null ? /* @__PURE__ */ jsx(
|
|
2802
|
+
"circle",
|
|
2803
|
+
{
|
|
2804
|
+
className: "twc-sparkline__dot",
|
|
2805
|
+
style: { fill: color },
|
|
2806
|
+
cx: points[hover][0],
|
|
2807
|
+
cy: points[hover][1],
|
|
2808
|
+
r: Math.max(2, strokeWidth + 1)
|
|
2809
|
+
}
|
|
2810
|
+
) : null,
|
|
2811
|
+
selected != null && points[selected] ? /* @__PURE__ */ jsx(
|
|
2812
|
+
"circle",
|
|
2813
|
+
{
|
|
2814
|
+
className: "twc-sparkline__dot",
|
|
2815
|
+
"data-mark": true,
|
|
2816
|
+
"data-selected": "true",
|
|
2817
|
+
style: { fill: color, pointerEvents: "none" },
|
|
2818
|
+
cx: points[selected][0],
|
|
2819
|
+
cy: points[selected][1],
|
|
2820
|
+
r: Math.max(2, strokeWidth + 1)
|
|
2821
|
+
}
|
|
2822
|
+
) : null,
|
|
2823
|
+
points.map((p, i) => /* @__PURE__ */ jsx(
|
|
2824
|
+
"circle",
|
|
2825
|
+
{
|
|
2826
|
+
className: "twc-chart__hit",
|
|
2827
|
+
cx: p[0],
|
|
2828
|
+
cy: p[1],
|
|
2829
|
+
r: "14",
|
|
2830
|
+
onMouseMove: onPointMove(i),
|
|
2831
|
+
onMouseLeave: onPointLeave,
|
|
2832
|
+
onClick: () => clickPoint(i)
|
|
2833
|
+
},
|
|
2834
|
+
i
|
|
2835
|
+
))
|
|
2836
|
+
] })
|
|
2837
|
+
}
|
|
2838
|
+
),
|
|
2839
|
+
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
2840
|
+
tableFallback ? /* @__PURE__ */ jsx(
|
|
2841
|
+
ChartTable,
|
|
2842
|
+
{
|
|
2843
|
+
id: tableId,
|
|
2844
|
+
caption: caption ?? svgAriaLabel,
|
|
2845
|
+
columns: ["value"],
|
|
2846
|
+
rows: values.map((v, i) => ({ label: labels[i], values: [fmt(v)] }))
|
|
2847
|
+
}
|
|
2848
|
+
) : null
|
|
2849
|
+
]
|
|
2850
|
+
}
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
var SCATTER_CSS = `
|
|
2854
|
+
.twc-chart--scatter__grid { stroke: var(--color-divider); stroke-width: 1; }
|
|
2855
|
+
.twc-chart--scatter__axis { fill: var(--color-text-subtle); font-size: 11px; }
|
|
2856
|
+
.twc-chart--scatter__title { fill: var(--color-text-muted); font-size: 11px; font-weight: 500; }
|
|
2857
|
+
.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; }
|
|
2858
|
+
.twc-chart--scatter__dot[data-bubble="true"] { fill-opacity: 0.55; }
|
|
2446
2859
|
.twc-chart__hit { fill: transparent; }
|
|
2447
2860
|
.twc-chart__hit:hover + .twc-chart--scatter__dot { transform: scale(1.18); }
|
|
2448
2861
|
`;
|
|
@@ -2456,9 +2869,11 @@ function ScatterChart({
|
|
|
2456
2869
|
yLabel,
|
|
2457
2870
|
showGrid = true,
|
|
2458
2871
|
showLegend,
|
|
2872
|
+
zoomable = false,
|
|
2459
2873
|
xFormat,
|
|
2460
2874
|
yFormat,
|
|
2461
2875
|
valueFormat,
|
|
2876
|
+
onDataClick,
|
|
2462
2877
|
ariaLabel,
|
|
2463
2878
|
"aria-label": ariaLabelProp,
|
|
2464
2879
|
tableFallback = true,
|
|
@@ -2469,15 +2884,24 @@ function ScatterChart({
|
|
|
2469
2884
|
const baseStyles = useScopedStyles("twc-chart-base", CHART_BASE_CSS);
|
|
2470
2885
|
const styles = useScopedStyles("twc-scatter-styles", SCATTER_CSS);
|
|
2471
2886
|
const uid = React51__default.useId();
|
|
2887
|
+
const gid = uid.replace(/[^a-zA-Z0-9]/g, "");
|
|
2888
|
+
const clipId = `twc-sc-clip-${gid}`;
|
|
2472
2889
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
2473
2890
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
2891
|
+
const [focus, setFocus] = React51__default.useState(null);
|
|
2892
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
2893
|
+
const [zoom, setZoom] = React51__default.useState(null);
|
|
2894
|
+
const [drag, setDrag] = React51__default.useState(null);
|
|
2895
|
+
const svgRef = React51__default.useRef(null);
|
|
2896
|
+
const movedRef = React51__default.useRef(false);
|
|
2474
2897
|
const list = Array.isArray(series) ? series : [];
|
|
2475
2898
|
const vFmt = valueFormat || fmtNumber;
|
|
2476
2899
|
const xFmt = xFormat || vFmt;
|
|
2477
2900
|
const yFmt = yFormat || vFmt;
|
|
2478
|
-
const all = list.flatMap(
|
|
2479
|
-
|
|
2480
|
-
|
|
2901
|
+
const all = list.flatMap((s, si) => {
|
|
2902
|
+
const pts = s && Array.isArray(s.points) ? s.points : [];
|
|
2903
|
+
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 }));
|
|
2904
|
+
});
|
|
2481
2905
|
const hasZ = all.some((p) => Number.isFinite(Number(p.z)));
|
|
2482
2906
|
const useBubble = bubble || hasZ;
|
|
2483
2907
|
const zMax = useBubble ? Math.max(0, ...all.map((p) => Number(p.z) || 0)) : 0;
|
|
@@ -2488,8 +2912,12 @@ function ScatterChart({
|
|
|
2488
2912
|
return Math.max(2, Math.sqrt(zv / zMax) * maxBubble);
|
|
2489
2913
|
};
|
|
2490
2914
|
const xs = all.map((p) => p.x), ys = all.map((p) => p.y);
|
|
2491
|
-
const
|
|
2492
|
-
const
|
|
2915
|
+
const base = { x0: Math.min(...xs), x1: Math.max(...xs), y0: Math.min(...ys), y1: Math.max(...ys) };
|
|
2916
|
+
const baseSpanX = base.x1 - base.x0, baseSpanY = base.y1 - base.y0;
|
|
2917
|
+
const canZoom = zoomable && all.length > 0 && baseSpanX > 0 && baseSpanY > 0;
|
|
2918
|
+
const dom = canZoom && zoom ? zoom : base;
|
|
2919
|
+
const xScale = niceScale(dom.x0, dom.x1, 5);
|
|
2920
|
+
const yScale = niceScale(dom.y0, dom.y1, 5);
|
|
2493
2921
|
const xSpan = xScale.max - xScale.min || 1;
|
|
2494
2922
|
const ySpan = yScale.max - yScale.min || 1;
|
|
2495
2923
|
const W = 600, H = height;
|
|
@@ -2502,7 +2930,9 @@ function ScatterChart({
|
|
|
2502
2930
|
const yPos = (y) => padT + innerH - (y - yScale.min) / ySpan * innerH;
|
|
2503
2931
|
const seriesColor = (s, i) => s && s.color || paletteAt(void 0, i);
|
|
2504
2932
|
const seriesName = (s, i) => s && s.name || `Series ${i + 1}`;
|
|
2505
|
-
const
|
|
2933
|
+
const multi = list.length > 1;
|
|
2934
|
+
const legend = showLegend ?? multi;
|
|
2935
|
+
const clickable = !!onDataClick;
|
|
2506
2936
|
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? `${useBubble ? "bubble" : "scatter"} chart`;
|
|
2507
2937
|
const dots = all.map((p) => ({ ...p, radius: dotR(p.z), fill: seriesColor(list[p.si], p.si) })).sort((a, b) => b.radius - a.radius);
|
|
2508
2938
|
const tipLabel = (p) => labelText2(p.label) || seriesName(list[p.si], p.si);
|
|
@@ -2515,72 +2945,272 @@ function ScatterChart({
|
|
|
2515
2945
|
...Number.isFinite(Number(p.z)) ? [{ label: "z", value: vFmt(Number(p.z)) }] : []
|
|
2516
2946
|
]
|
|
2517
2947
|
});
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2948
|
+
const focusActive = (si) => focus == null ? void 0 : si === focus || void 0;
|
|
2949
|
+
const clickPoint = (p) => {
|
|
2950
|
+
if (!onDataClick) return;
|
|
2951
|
+
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 });
|
|
2952
|
+
};
|
|
2953
|
+
const selectPoint = (key) => setSelected((s) => s === key ? null : key);
|
|
2954
|
+
const clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
2955
|
+
const svgPoint = (e) => {
|
|
2956
|
+
const svg = svgRef.current;
|
|
2957
|
+
if (!svg || typeof svg.getScreenCTM !== "function" || typeof svg.createSVGPoint !== "function") return null;
|
|
2958
|
+
const ctm = svg.getScreenCTM();
|
|
2959
|
+
if (!ctm || !ctm.a || !ctm.d) return null;
|
|
2960
|
+
const pt = svg.createSVGPoint();
|
|
2961
|
+
pt.x = e.clientX;
|
|
2962
|
+
pt.y = e.clientY;
|
|
2963
|
+
const loc = pt.matrixTransform(ctm.inverse());
|
|
2964
|
+
if (!Number.isFinite(loc.x) || !Number.isFinite(loc.y)) return null;
|
|
2965
|
+
return { px: loc.x, py: loc.y };
|
|
2966
|
+
};
|
|
2967
|
+
const dataFromPx = (px, py) => {
|
|
2968
|
+
const cx = clamp2(px, padL, padL + innerW);
|
|
2969
|
+
const cy = clamp2(py, padT, padT + innerH);
|
|
2970
|
+
return {
|
|
2971
|
+
x: xScale.min + (cx - padL) / innerW * (xScale.max - xScale.min),
|
|
2972
|
+
y: yScale.min + (padT + innerH - cy) / innerH * (yScale.max - yScale.min)
|
|
2973
|
+
};
|
|
2974
|
+
};
|
|
2975
|
+
const nearestDot = (px, py) => {
|
|
2976
|
+
let best = null, bestD = Infinity;
|
|
2977
|
+
for (const p of dots) {
|
|
2978
|
+
const dx = xPos(p.x) - px, dy = yPos(p.y) - py;
|
|
2979
|
+
const d = dx * dx + dy * dy;
|
|
2980
|
+
const rr = Math.max(14, p.radius);
|
|
2981
|
+
if (d <= rr * rr && d < bestD) {
|
|
2982
|
+
bestD = d;
|
|
2983
|
+
best = p;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
return best;
|
|
2987
|
+
};
|
|
2988
|
+
const applyZoom = (win) => {
|
|
2989
|
+
const x0 = Math.max(Math.min(win.x0, win.x1), base.x0), x1 = Math.min(Math.max(win.x0, win.x1), base.x1);
|
|
2990
|
+
const y0 = Math.max(Math.min(win.y0, win.y1), base.y0), y1 = Math.min(Math.max(win.y0, win.y1), base.y1);
|
|
2991
|
+
if (!(x1 - x0 > 0) || !(y1 - y0 > 0)) return;
|
|
2992
|
+
if (x1 - x0 >= baseSpanX && y1 - y0 >= baseSpanY) {
|
|
2993
|
+
setZoom(null);
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
setZoom({ x0, x1, y0, y1 });
|
|
2997
|
+
};
|
|
2998
|
+
const panTo = (win) => {
|
|
2999
|
+
const wSpan = win.x1 - win.x0, hSpan = win.y1 - win.y0;
|
|
3000
|
+
let { x0, x1, y0, y1 } = win;
|
|
3001
|
+
if (x0 < base.x0) {
|
|
3002
|
+
x0 = base.x0;
|
|
3003
|
+
x1 = base.x0 + wSpan;
|
|
3004
|
+
} else if (x1 > base.x1) {
|
|
3005
|
+
x1 = base.x1;
|
|
3006
|
+
x0 = base.x1 - wSpan;
|
|
3007
|
+
}
|
|
3008
|
+
if (y0 < base.y0) {
|
|
3009
|
+
y0 = base.y0;
|
|
3010
|
+
y1 = base.y0 + hSpan;
|
|
3011
|
+
} else if (y1 > base.y1) {
|
|
3012
|
+
y1 = base.y1;
|
|
3013
|
+
y0 = base.y1 - hSpan;
|
|
3014
|
+
}
|
|
3015
|
+
setZoom({ x0, x1, y0, y1 });
|
|
3016
|
+
};
|
|
3017
|
+
const onOverlayDown = (e) => {
|
|
3018
|
+
const pt = svgPoint(e);
|
|
3019
|
+
if (!pt) return;
|
|
3020
|
+
movedRef.current = false;
|
|
3021
|
+
setDrag({ x0: pt.px, y0: pt.py, x1: pt.px, y1: pt.py, pan: e.shiftKey, z0: zoom || base });
|
|
3022
|
+
};
|
|
3023
|
+
const onOverlayMove = (e) => {
|
|
3024
|
+
const pt = svgPoint(e);
|
|
3025
|
+
if (!pt) return;
|
|
3026
|
+
if (drag) {
|
|
3027
|
+
if (Math.abs(pt.px - drag.x0) > 4 || Math.abs(pt.py - drag.y0) > 4) movedRef.current = true;
|
|
3028
|
+
if (drag.pan) {
|
|
3029
|
+
if (zoom) {
|
|
3030
|
+
const z0 = drag.z0;
|
|
3031
|
+
const dxData = -((pt.px - drag.x0) / innerW) * (z0.x1 - z0.x0);
|
|
3032
|
+
const dyData = (pt.py - drag.y0) / innerH * (z0.y1 - z0.y0);
|
|
3033
|
+
panTo({ x0: z0.x0 + dxData, x1: z0.x1 + dxData, y0: z0.y0 + dyData, y1: z0.y1 + dyData });
|
|
2542
3034
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
3035
|
+
} else {
|
|
3036
|
+
setDrag({ ...drag, x1: pt.px, y1: pt.py });
|
|
3037
|
+
}
|
|
3038
|
+
hide();
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
const hit = nearestDot(pt.px, pt.py);
|
|
3042
|
+
if (hit) show(tipFor(hit), e);
|
|
3043
|
+
else hide();
|
|
3044
|
+
};
|
|
3045
|
+
const onOverlayUp = () => {
|
|
3046
|
+
if (drag && !drag.pan) {
|
|
3047
|
+
const a = dataFromPx(drag.x0, drag.y0), b = dataFromPx(drag.x1, drag.y1);
|
|
3048
|
+
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 });
|
|
3049
|
+
}
|
|
3050
|
+
setDrag(null);
|
|
3051
|
+
};
|
|
3052
|
+
const onOverlayLeave = () => {
|
|
3053
|
+
hide();
|
|
3054
|
+
setDrag(null);
|
|
3055
|
+
};
|
|
3056
|
+
const onOverlayClick = (e) => {
|
|
3057
|
+
if (movedRef.current) {
|
|
3058
|
+
movedRef.current = false;
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
const pt = svgPoint(e);
|
|
3062
|
+
if (!pt) return;
|
|
3063
|
+
const hit = nearestDot(pt.px, pt.py);
|
|
3064
|
+
if (hit) {
|
|
3065
|
+
clickPoint(hit);
|
|
3066
|
+
selectPoint(hit.key);
|
|
3067
|
+
}
|
|
3068
|
+
};
|
|
3069
|
+
React51__default.useEffect(() => {
|
|
3070
|
+
if (!canZoom) return void 0;
|
|
3071
|
+
const svg = svgRef.current;
|
|
3072
|
+
if (!svg) return void 0;
|
|
3073
|
+
const onWheel = (e) => {
|
|
3074
|
+
e.preventDefault();
|
|
3075
|
+
const pt = svgPoint(e);
|
|
3076
|
+
const fx = pt ? clamp2((pt.px - padL) / innerW, 0, 1) : 0.5;
|
|
3077
|
+
const fy = pt ? clamp2((padT + innerH - pt.py) / innerH, 0, 1) : 0.5;
|
|
3078
|
+
const factor = e.deltaY < 0 ? 0.8 : 1.25;
|
|
3079
|
+
const x0 = xScale.min, x1 = xScale.max, y0 = yScale.min, y1 = yScale.max;
|
|
3080
|
+
const cxv = x0 + fx * (x1 - x0), cyv = y0 + fy * (y1 - y0);
|
|
3081
|
+
const nw = (x1 - x0) * factor, nh = (y1 - y0) * factor;
|
|
3082
|
+
applyZoom({ x0: cxv - fx * nw, x1: cxv + (1 - fx) * nw, y0: cyv - fy * nh, y1: cyv + (1 - fy) * nh });
|
|
3083
|
+
};
|
|
3084
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
3085
|
+
return () => svg.removeEventListener("wheel", onWheel);
|
|
3086
|
+
}, [canZoom, zoom, xScale.min, xScale.max, yScale.min, yScale.max]);
|
|
3087
|
+
const dragBand = canZoom && drag && !drag.pan && Math.abs(drag.x1 - drag.x0) > 2 && Math.abs(drag.y1 - drag.y0) > 2;
|
|
3088
|
+
return /* @__PURE__ */ jsxs(
|
|
3089
|
+
"div",
|
|
3090
|
+
{
|
|
3091
|
+
ref: containerRef,
|
|
3092
|
+
className: `twc-chart twc-chart--scatter ${className}`.trim(),
|
|
3093
|
+
"data-bubble": useBubble || void 0,
|
|
3094
|
+
"data-hovering": focus != null || void 0,
|
|
3095
|
+
"data-has-selection": selected != null || void 0,
|
|
3096
|
+
"data-clickable": clickable || void 0,
|
|
3097
|
+
...rest,
|
|
3098
|
+
children: [
|
|
3099
|
+
baseStyles,
|
|
3100
|
+
styles,
|
|
3101
|
+
/* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, children: [
|
|
3102
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: padL, y: padT, width: Math.max(0, innerW), height: Math.max(0, innerH) }) }) }),
|
|
3103
|
+
showGrid ? yScale.ticks.map((t, i) => {
|
|
3104
|
+
const p = yPos(t);
|
|
3105
|
+
return /* @__PURE__ */ jsx("line", { className: "twc-chart--scatter__grid", x1: padL, y1: p, x2: W - padR, y2: p }, `y${i}`);
|
|
3106
|
+
}) : null,
|
|
3107
|
+
showGrid ? xScale.ticks.map((t, i) => {
|
|
3108
|
+
const p = xPos(t);
|
|
3109
|
+
return /* @__PURE__ */ jsx("line", { className: "twc-chart--scatter__grid", x1: p, y1: padT, x2: p, y2: padT + innerH }, `x${i}`);
|
|
3110
|
+
}) : null,
|
|
3111
|
+
yScale.ticks.map((t, i) => /* @__PURE__ */ jsx("text", { className: "twc-chart--scatter__axis", x: padL - 8, y: yPos(t) + 4, textAnchor: "end", children: shortNum(t) }, `yt${i}`)),
|
|
3112
|
+
xScale.ticks.map((t, i) => /* @__PURE__ */ jsx("text", { className: "twc-chart--scatter__axis", x: xPos(t), y: padT + innerH + 16, textAnchor: "middle", children: shortNum(t) }, `xt${i}`)),
|
|
3113
|
+
xLabel != null ? /* @__PURE__ */ jsx("text", { className: "twc-chart--scatter__title", x: padL + innerW / 2, y: H - 6, textAnchor: "middle", children: xLabel }) : null,
|
|
3114
|
+
yLabel != null ? /* @__PURE__ */ jsx(
|
|
3115
|
+
"text",
|
|
3116
|
+
{
|
|
3117
|
+
className: "twc-chart--scatter__title",
|
|
3118
|
+
x: 16,
|
|
3119
|
+
y: padT + innerH / 2,
|
|
3120
|
+
textAnchor: "middle",
|
|
3121
|
+
transform: `rotate(-90 16 ${padT + innerH / 2})`,
|
|
3122
|
+
children: yLabel
|
|
3123
|
+
}
|
|
3124
|
+
) : null,
|
|
3125
|
+
/* @__PURE__ */ jsx("g", { clipPath: canZoom && zoom ? `url(#${clipId})` : void 0, children: dots.map((p) => /* @__PURE__ */ jsxs("g", { className: "twc-chart__anim-fade", children: [
|
|
3126
|
+
/* @__PURE__ */ jsx(
|
|
3127
|
+
"circle",
|
|
3128
|
+
{
|
|
3129
|
+
className: "twc-chart__hit",
|
|
3130
|
+
cx: xPos(p.x),
|
|
3131
|
+
cy: yPos(p.y),
|
|
3132
|
+
r: Math.max(14, p.radius),
|
|
3133
|
+
onMouseMove: (e) => show(tipFor(p), e),
|
|
3134
|
+
onMouseLeave: hide,
|
|
3135
|
+
onClick: () => {
|
|
3136
|
+
clickPoint(p);
|
|
3137
|
+
selectPoint(p.key);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
),
|
|
3141
|
+
/* @__PURE__ */ jsx(
|
|
3142
|
+
"circle",
|
|
3143
|
+
{
|
|
3144
|
+
className: "twc-chart--scatter__dot",
|
|
3145
|
+
"data-mark": true,
|
|
3146
|
+
"data-active": focusActive(p.si),
|
|
3147
|
+
"data-selected": selected === p.key || void 0,
|
|
3148
|
+
"data-bubble": useBubble || void 0,
|
|
3149
|
+
style: { fill: p.fill },
|
|
3150
|
+
cx: xPos(p.x),
|
|
3151
|
+
cy: yPos(p.y),
|
|
3152
|
+
r: p.radius
|
|
3153
|
+
}
|
|
3154
|
+
)
|
|
3155
|
+
] }, p.key)) }),
|
|
3156
|
+
dragBand ? /* @__PURE__ */ jsx(
|
|
3157
|
+
"rect",
|
|
3158
|
+
{
|
|
3159
|
+
className: "twc-chart__zoomband",
|
|
3160
|
+
x: Math.min(drag.x0, drag.x1),
|
|
3161
|
+
y: Math.min(drag.y0, drag.y1),
|
|
3162
|
+
width: Math.abs(drag.x1 - drag.x0),
|
|
3163
|
+
height: Math.abs(drag.y1 - drag.y0)
|
|
3164
|
+
}
|
|
3165
|
+
) : null,
|
|
3166
|
+
canZoom ? /* @__PURE__ */ jsx(
|
|
3167
|
+
"rect",
|
|
3168
|
+
{
|
|
3169
|
+
className: "twc-chart__overlay",
|
|
3170
|
+
"data-clickable": clickable || void 0,
|
|
3171
|
+
x: padL,
|
|
3172
|
+
y: padT,
|
|
3173
|
+
width: Math.max(0, innerW),
|
|
3174
|
+
height: Math.max(0, innerH),
|
|
3175
|
+
onMouseDown: onOverlayDown,
|
|
3176
|
+
onMouseMove: onOverlayMove,
|
|
3177
|
+
onMouseUp: onOverlayUp,
|
|
3178
|
+
onMouseLeave: onOverlayLeave,
|
|
3179
|
+
onClick: onOverlayClick
|
|
3180
|
+
}
|
|
3181
|
+
) : null
|
|
3182
|
+
] }),
|
|
3183
|
+
canZoom && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
|
|
3184
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3185
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
3186
|
+
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
3187
|
+
] }),
|
|
3188
|
+
"Reset"
|
|
3189
|
+
] }) : null,
|
|
3190
|
+
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
3191
|
+
tableFallback ? /* @__PURE__ */ jsx(
|
|
3192
|
+
ChartTable,
|
|
2547
3193
|
{
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
3194
|
+
id: tableId,
|
|
3195
|
+
caption: caption ?? svgAriaLabel,
|
|
3196
|
+
columns: tableColumns,
|
|
3197
|
+
rows: all.map((p) => ({
|
|
3198
|
+
label: seriesName(list[p.si], p.si),
|
|
3199
|
+
values: hasZ ? [xFmt(p.x), yFmt(p.y), Number.isFinite(Number(p.z)) ? vFmt(Number(p.z)) : ""] : [xFmt(p.x), yFmt(p.y)]
|
|
3200
|
+
}))
|
|
2554
3201
|
}
|
|
2555
|
-
),
|
|
2556
|
-
/* @__PURE__ */ jsx(
|
|
2557
|
-
|
|
3202
|
+
) : null,
|
|
3203
|
+
legend && list.length ? /* @__PURE__ */ jsx(
|
|
3204
|
+
ChartLegend,
|
|
2558
3205
|
{
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
cx: xPos(p.x),
|
|
2563
|
-
cy: yPos(p.y),
|
|
2564
|
-
r: p.radius
|
|
3206
|
+
items: list.map((s, i) => ({ label: seriesName(s, i), color: seriesColor(s, i) })),
|
|
3207
|
+
onFocus: multi ? (it, i) => setFocus(i) : void 0,
|
|
3208
|
+
onBlur: multi ? () => setFocus(null) : void 0
|
|
2565
3209
|
}
|
|
2566
|
-
)
|
|
2567
|
-
]
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
tableFallback ? /* @__PURE__ */ jsx(
|
|
2571
|
-
ChartTable,
|
|
2572
|
-
{
|
|
2573
|
-
id: tableId,
|
|
2574
|
-
caption: caption ?? svgAriaLabel,
|
|
2575
|
-
columns: tableColumns,
|
|
2576
|
-
rows: all.map((p) => ({
|
|
2577
|
-
label: seriesName(list[p.si], p.si),
|
|
2578
|
-
values: hasZ ? [xFmt(p.x), yFmt(p.y), Number.isFinite(Number(p.z)) ? vFmt(Number(p.z)) : ""] : [xFmt(p.x), yFmt(p.y)]
|
|
2579
|
-
}))
|
|
2580
|
-
}
|
|
2581
|
-
) : null,
|
|
2582
|
-
legend && list.length ? /* @__PURE__ */ jsx(ChartLegend, { items: list.map((s, i) => ({ label: seriesName(s, i), color: seriesColor(s, i) })) }) : null
|
|
2583
|
-
] });
|
|
3210
|
+
) : null
|
|
3211
|
+
]
|
|
3212
|
+
}
|
|
3213
|
+
);
|
|
2584
3214
|
}
|
|
2585
3215
|
function labelText2(label) {
|
|
2586
3216
|
return typeof label === "string" || typeof label === "number" ? String(label) : "";
|
|
@@ -2612,6 +3242,7 @@ function RadarChart({
|
|
|
2612
3242
|
colors,
|
|
2613
3243
|
valueFormat,
|
|
2614
3244
|
height = 300,
|
|
3245
|
+
onDataClick,
|
|
2615
3246
|
ariaLabel,
|
|
2616
3247
|
"aria-label": ariaLabelProp,
|
|
2617
3248
|
tableFallback = true,
|
|
@@ -2624,11 +3255,16 @@ function RadarChart({
|
|
|
2624
3255
|
const uid = React51__default.useId();
|
|
2625
3256
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
2626
3257
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
3258
|
+
const [focus, setFocus] = React51__default.useState(null);
|
|
3259
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
2627
3260
|
const rows = data || [];
|
|
2628
3261
|
const keys = series && series.length ? series : ["value"];
|
|
2629
3262
|
const fmt = valueFormat || fmtNumber;
|
|
2630
3263
|
const multi = keys.length > 1;
|
|
3264
|
+
const clickable = !!onDataClick;
|
|
2631
3265
|
const legend = showLegend != null ? showLegend : multi;
|
|
3266
|
+
const focusActive = (k) => focus == null ? void 0 : k === focus || void 0;
|
|
3267
|
+
const selectMark = (key) => setSelected((s) => s === key ? null : key);
|
|
2632
3268
|
const H = height;
|
|
2633
3269
|
const cx = H / 2, cy = H / 2;
|
|
2634
3270
|
const R = Math.max(0, H / 2 - 40);
|
|
@@ -2642,69 +3278,106 @@ function RadarChart({
|
|
|
2642
3278
|
const frac = Math.max(0, Math.min(1, (Number(v) || 0) / radialMax));
|
|
2643
3279
|
return polarDeg(cx, cy, frac * R, angleOf(i));
|
|
2644
3280
|
};
|
|
2645
|
-
return /* @__PURE__ */ jsxs(
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
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
|
-
|
|
3281
|
+
return /* @__PURE__ */ jsxs(
|
|
3282
|
+
"div",
|
|
3283
|
+
{
|
|
3284
|
+
ref: containerRef,
|
|
3285
|
+
className: `twc-chart twc-chart--radar ${className}`.trim(),
|
|
3286
|
+
"data-hovering": focus != null || void 0,
|
|
3287
|
+
"data-clickable": clickable || void 0,
|
|
3288
|
+
"data-has-selection": selected != null || void 0,
|
|
3289
|
+
...rest,
|
|
3290
|
+
children: [
|
|
3291
|
+
baseStyles,
|
|
3292
|
+
styles,
|
|
3293
|
+
/* @__PURE__ */ jsx(
|
|
3294
|
+
"svg",
|
|
3295
|
+
{
|
|
3296
|
+
viewBox: `0 0 ${H} ${H}`,
|
|
3297
|
+
role: "img",
|
|
3298
|
+
"aria-label": svgAriaLabel,
|
|
3299
|
+
"aria-describedby": tableId,
|
|
3300
|
+
style: { maxWidth: H, margin: "0 auto" },
|
|
3301
|
+
children: n === 0 ? /* @__PURE__ */ jsx("text", { className: "twc-chart__empty", x: cx, y: cy, textAnchor: "middle", dominantBaseline: "middle", children: "No data" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3302
|
+
Array.from({ length: rings }, (_, li) => {
|
|
3303
|
+
const rr = R * (li + 1) / rings;
|
|
3304
|
+
const pts = rows.map((_2, i) => polarDeg(cx, cy, rr, angleOf(i)));
|
|
3305
|
+
return /* @__PURE__ */ jsx("path", { className: "twc-chart__ring", d: polygonPath(pts) }, li);
|
|
3306
|
+
}),
|
|
3307
|
+
rows.map((d, i) => {
|
|
3308
|
+
const [ex, ey] = polarDeg(cx, cy, R, angleOf(i));
|
|
3309
|
+
const [lx, ly] = polarDeg(cx, cy, R + 16, angleOf(i));
|
|
3310
|
+
const anchor = lx > cx + 1 ? "start" : lx < cx - 1 ? "end" : "middle";
|
|
3311
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
3312
|
+
/* @__PURE__ */ jsx("line", { className: "twc-chart__spoke", x1: cx, y1: cy, x2: ex, y2: ey }),
|
|
3313
|
+
/* @__PURE__ */ jsx("text", { className: "twc-chart__radar-axis", x: lx, y: ly, textAnchor: anchor, dominantBaseline: "middle", children: d.label })
|
|
3314
|
+
] }, i);
|
|
3315
|
+
}),
|
|
3316
|
+
keys.map((k, si) => {
|
|
3317
|
+
const color = paletteAt(colors, si);
|
|
3318
|
+
const pts = rows.map((d, i) => vertex(d[k], i));
|
|
3319
|
+
return /* @__PURE__ */ jsxs("g", { "data-mark": true, "data-active": focusActive(k), children: [
|
|
3320
|
+
fill ? /* @__PURE__ */ jsx("path", { className: "twc-chart__radar-area twc-chart__anim-fade", style: { fill: color, opacity: 0.18 }, d: polygonPath(pts) }) : null,
|
|
3321
|
+
/* @__PURE__ */ jsx("path", { className: "twc-chart__radar-line twc-chart__anim-fade", style: { stroke: color }, d: polygonPath(pts) }),
|
|
3322
|
+
showDots ? pts.map((p, i) => {
|
|
3323
|
+
const key = `${i}:${k}`;
|
|
3324
|
+
return /* @__PURE__ */ jsxs("g", { className: "twc-chart__anim-fade", children: [
|
|
3325
|
+
/* @__PURE__ */ jsx(
|
|
3326
|
+
"circle",
|
|
3327
|
+
{
|
|
3328
|
+
className: "twc-chart__hit",
|
|
3329
|
+
cx: p[0],
|
|
3330
|
+
cy: p[1],
|
|
3331
|
+
r: "14",
|
|
3332
|
+
onMouseMove: (e) => show({ title: rows[i].label, items: [{ color, label: k, value: fmt(Number(rows[i][k]) || 0) }] }, e),
|
|
3333
|
+
onMouseLeave: hide,
|
|
3334
|
+
onClick: () => {
|
|
3335
|
+
if (onDataClick) onDataClick({ label: rows[i].label, series: k, value: Number(rows[i][k]) || 0, index: i });
|
|
3336
|
+
selectMark(key);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
),
|
|
3340
|
+
/* @__PURE__ */ jsx(
|
|
3341
|
+
"circle",
|
|
3342
|
+
{
|
|
3343
|
+
className: "twc-chart__radar-dot",
|
|
3344
|
+
style: { fill: color },
|
|
3345
|
+
cx: p[0],
|
|
3346
|
+
cy: p[1],
|
|
3347
|
+
r: "3",
|
|
3348
|
+
"data-mark": true,
|
|
3349
|
+
"data-active": focusActive(k),
|
|
3350
|
+
"data-selected": selected === key || void 0
|
|
3351
|
+
}
|
|
3352
|
+
)
|
|
3353
|
+
] }, i);
|
|
3354
|
+
}) : null
|
|
3355
|
+
] }, k);
|
|
3356
|
+
})
|
|
3357
|
+
] })
|
|
3358
|
+
}
|
|
3359
|
+
),
|
|
3360
|
+
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
3361
|
+
tableFallback ? /* @__PURE__ */ jsx(
|
|
3362
|
+
ChartTable,
|
|
3363
|
+
{
|
|
3364
|
+
id: tableId,
|
|
3365
|
+
caption: caption ?? svgAriaLabel,
|
|
3366
|
+
columns: keys,
|
|
3367
|
+
rows: rows.map((d) => ({ label: d.label, values: keys.map((k) => fmt(Number(d[k]) || 0)) }))
|
|
3368
|
+
}
|
|
3369
|
+
) : null,
|
|
3370
|
+
legend && multi ? /* @__PURE__ */ jsx(
|
|
3371
|
+
ChartLegend,
|
|
3372
|
+
{
|
|
3373
|
+
items: keys.map((k, si) => ({ label: k, color: paletteAt(colors, si) })),
|
|
3374
|
+
onFocus: (it) => setFocus(it.label),
|
|
3375
|
+
onBlur: () => setFocus(null)
|
|
3376
|
+
}
|
|
3377
|
+
) : null
|
|
3378
|
+
]
|
|
3379
|
+
}
|
|
3380
|
+
);
|
|
2708
3381
|
}
|
|
2709
3382
|
var CHART_CSS4 = `
|
|
2710
3383
|
.twc-chart--polar svg { margin-inline: auto; }
|
|
@@ -2723,6 +3396,7 @@ function PolarAreaChart({
|
|
|
2723
3396
|
valueFormat,
|
|
2724
3397
|
height = 280,
|
|
2725
3398
|
colors,
|
|
3399
|
+
onDataClick,
|
|
2726
3400
|
ariaLabel,
|
|
2727
3401
|
"aria-label": ariaLabelProp,
|
|
2728
3402
|
tableFallback = true,
|
|
@@ -2736,6 +3410,8 @@ function PolarAreaChart({
|
|
|
2736
3410
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
2737
3411
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
2738
3412
|
const [active, setActive] = React51__default.useState(null);
|
|
3413
|
+
const [focus, setFocus] = React51__default.useState(null);
|
|
3414
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
2739
3415
|
const rows = data || [];
|
|
2740
3416
|
const n = rows.length;
|
|
2741
3417
|
const fmt = valueFormat || fmtNumber;
|
|
@@ -2750,14 +3426,23 @@ function PolarAreaChart({
|
|
|
2750
3426
|
};
|
|
2751
3427
|
const rings = Math.max(1, Math.floor(levels));
|
|
2752
3428
|
const anglePer = 360 / Math.max(1, n);
|
|
3429
|
+
const EXPLODE = Math.min(12, Rmax * 0.1);
|
|
2753
3430
|
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "polar area chart";
|
|
3431
|
+
const clickable = !!onDataClick;
|
|
3432
|
+
const emphasis = focus != null ? focus : active;
|
|
2754
3433
|
const labelText8 = (label) => typeof label === "string" || typeof label === "number" ? String(label) : "";
|
|
3434
|
+
const clickSlice = (d, v, i) => {
|
|
3435
|
+
if (onDataClick) onDataClick({ label: d.label, value: v, index: i });
|
|
3436
|
+
setSelected((s) => s === i ? null : i);
|
|
3437
|
+
};
|
|
2755
3438
|
return /* @__PURE__ */ jsxs(
|
|
2756
3439
|
"div",
|
|
2757
3440
|
{
|
|
2758
3441
|
ref: containerRef,
|
|
2759
3442
|
className: `twc-chart twc-chart--polar ${className}`.trim(),
|
|
2760
|
-
"data-hovering":
|
|
3443
|
+
"data-hovering": emphasis != null || void 0,
|
|
3444
|
+
"data-clickable": clickable || void 0,
|
|
3445
|
+
"data-has-selection": selected != null || void 0,
|
|
2761
3446
|
...rest,
|
|
2762
3447
|
children: [
|
|
2763
3448
|
baseStyles,
|
|
@@ -2786,13 +3471,16 @@ function PolarAreaChart({
|
|
|
2786
3471
|
const start = startAngle + i * anglePer;
|
|
2787
3472
|
const color = d.color || paletteAt(colors, i);
|
|
2788
3473
|
const tipFor = { title: labelText8(d.label), items: [{ color, label: "", value: fmt(v) }] };
|
|
3474
|
+
const midRad = (start + anglePer / 2 - 90) * Math.PI / 180;
|
|
3475
|
+
const explode = selected === i ? `translate(${r(Math.cos(midRad) * EXPLODE)}px, ${r(Math.sin(midRad) * EXPLODE)}px)` : void 0;
|
|
2789
3476
|
return /* @__PURE__ */ jsx(
|
|
2790
3477
|
"path",
|
|
2791
3478
|
{
|
|
2792
3479
|
className: "twc-chart__slice twc-chart__anim-arc",
|
|
2793
3480
|
"data-mark": true,
|
|
2794
|
-
"data-active":
|
|
2795
|
-
|
|
3481
|
+
"data-active": emphasis == null ? void 0 : emphasis === i || void 0,
|
|
3482
|
+
"data-selected": selected === i || void 0,
|
|
3483
|
+
style: { fill: color, fillOpacity: 0.82, transform: explode },
|
|
2796
3484
|
d: arcPath(cx, cy, radiusFor(v), 0, start, start + anglePer),
|
|
2797
3485
|
onMouseMove: (e) => {
|
|
2798
3486
|
setActive(i);
|
|
@@ -2801,7 +3489,8 @@ function PolarAreaChart({
|
|
|
2801
3489
|
onMouseLeave: () => {
|
|
2802
3490
|
setActive(null);
|
|
2803
3491
|
hide();
|
|
2804
|
-
}
|
|
3492
|
+
},
|
|
3493
|
+
onClick: () => clickSlice(d, v, i)
|
|
2805
3494
|
},
|
|
2806
3495
|
`slice-${i}`
|
|
2807
3496
|
);
|
|
@@ -2824,7 +3513,14 @@ function PolarAreaChart({
|
|
|
2824
3513
|
rows: rows.map((d) => ({ label: d.label, values: [fmt(Number(d.value) || 0)] }))
|
|
2825
3514
|
}
|
|
2826
3515
|
) : null,
|
|
2827
|
-
showLegend && n ? /* @__PURE__ */ jsx(
|
|
3516
|
+
showLegend && n ? /* @__PURE__ */ jsx(
|
|
3517
|
+
ChartLegend,
|
|
3518
|
+
{
|
|
3519
|
+
items: rows.map((d, i) => ({ label: d.label, color: d.color || paletteAt(colors, i) })),
|
|
3520
|
+
onFocus: n > 1 ? (_it, i) => setFocus(i) : void 0,
|
|
3521
|
+
onBlur: n > 1 ? () => setFocus(null) : void 0
|
|
3522
|
+
}
|
|
3523
|
+
) : null
|
|
2828
3524
|
]
|
|
2829
3525
|
}
|
|
2830
3526
|
);
|
|
@@ -2834,7 +3530,7 @@ var HEATMAP_CSS = `
|
|
|
2834
3530
|
.twc-chart__hm-cell { transition: opacity var(--duration-fast) var(--ease-standard); }
|
|
2835
3531
|
.twc-chart__hm-cell:hover { opacity: 0.82; }
|
|
2836
3532
|
.twc-chart__hm-empty { fill: var(--color-surface-sunken); }
|
|
2837
|
-
.twc-chart__hm-val { fill: var(--color-text); font-size: 10px; font-weight: 500; pointer-events: none; }
|
|
3533
|
+
.twc-chart__hm-val { fill: var(--color-text); paint-order: stroke; stroke: var(--color-surface); stroke-width: 2.5px; stroke-linejoin: round; vector-effect: non-scaling-stroke; font-size: 10px; font-weight: 500; pointer-events: none; }
|
|
2838
3534
|
.twc-chart__hm-tick { fill: var(--color-text-subtle); font-size: 11px; }
|
|
2839
3535
|
.twc-chart__hm-title { fill: var(--color-text-muted); font-size: 11px; font-weight: 500; }
|
|
2840
3536
|
.twc-chart__hm-scale { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-3); font-size: var(--text-xs); color: var(--color-text-muted); }
|
|
@@ -2852,8 +3548,10 @@ function Heatmap({
|
|
|
2852
3548
|
xLabel,
|
|
2853
3549
|
yLabel,
|
|
2854
3550
|
showLegend = true,
|
|
3551
|
+
zoomable = false,
|
|
2855
3552
|
valueFormat,
|
|
2856
3553
|
height = 300,
|
|
3554
|
+
onDataClick,
|
|
2857
3555
|
ariaLabel,
|
|
2858
3556
|
"aria-label": ariaLabelProp,
|
|
2859
3557
|
tableFallback = true,
|
|
@@ -2867,10 +3565,17 @@ function Heatmap({
|
|
|
2867
3565
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
2868
3566
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
2869
3567
|
const [hovered, setHovered] = React51__default.useState(null);
|
|
3568
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
3569
|
+
const [zoom, setZoom] = React51__default.useState(null);
|
|
3570
|
+
const [drag, setDrag] = React51__default.useState(null);
|
|
3571
|
+
const svgRef = React51__default.useRef(null);
|
|
3572
|
+
const movedRef = React51__default.useRef(false);
|
|
3573
|
+
const clickable = !!onDataClick;
|
|
3574
|
+
const selectMark = (key) => setSelected((s) => s === key ? null : key);
|
|
2870
3575
|
const rows = Array.isArray(data) ? data : [];
|
|
2871
3576
|
const fmt = valueFormat || fmtNumber;
|
|
2872
|
-
const
|
|
2873
|
-
const
|
|
3577
|
+
const fullXs = [];
|
|
3578
|
+
const fullYs = [];
|
|
2874
3579
|
const seenX = /* @__PURE__ */ new Set();
|
|
2875
3580
|
const seenY = /* @__PURE__ */ new Set();
|
|
2876
3581
|
const lookup = /* @__PURE__ */ new Map();
|
|
@@ -2879,11 +3584,11 @@ function Heatmap({
|
|
|
2879
3584
|
const ky = String(d.y);
|
|
2880
3585
|
if (!seenX.has(kx)) {
|
|
2881
3586
|
seenX.add(kx);
|
|
2882
|
-
|
|
3587
|
+
fullXs.push(d.x);
|
|
2883
3588
|
}
|
|
2884
3589
|
if (!seenY.has(ky)) {
|
|
2885
3590
|
seenY.add(ky);
|
|
2886
|
-
|
|
3591
|
+
fullYs.push(d.y);
|
|
2887
3592
|
}
|
|
2888
3593
|
lookup.set(`${kx}\0${ky}`, Number(d.value) || 0);
|
|
2889
3594
|
}
|
|
@@ -2896,6 +3601,15 @@ function Heatmap({
|
|
|
2896
3601
|
const span = hi - lo;
|
|
2897
3602
|
const pctOf = (v) => span > 0 ? clamp((v - lo) / span * 100, 0, 100) : 60;
|
|
2898
3603
|
const fillOf = (v) => `color-mix(in srgb, ${colorScale} ${pctOf(v)}%, transparent)`;
|
|
3604
|
+
const nX = fullXs.length;
|
|
3605
|
+
const nY = fullYs.length;
|
|
3606
|
+
const canZoom = zoomable && nX > 1 && nY > 1;
|
|
3607
|
+
const vc0 = canZoom && zoom ? clamp(zoom.c0, 0, nX - 1) : 0;
|
|
3608
|
+
const vc1 = canZoom && zoom ? clamp(zoom.c1, 0, nX - 1) : Math.max(0, nX - 1);
|
|
3609
|
+
const vr0 = canZoom && zoom ? clamp(zoom.r0, 0, nY - 1) : 0;
|
|
3610
|
+
const vr1 = canZoom && zoom ? clamp(zoom.r1, 0, nY - 1) : Math.max(0, nY - 1);
|
|
3611
|
+
const xs = fullXs.slice(vc0, vc1 + 1);
|
|
3612
|
+
const ys = fullYs.slice(vr0, vr1 + 1);
|
|
2899
3613
|
const cols = xs.length;
|
|
2900
3614
|
const rowN = ys.length;
|
|
2901
3615
|
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "heatmap";
|
|
@@ -2914,10 +3628,137 @@ function Heatmap({
|
|
|
2914
3628
|
const cw = innerW / Math.max(1, cols);
|
|
2915
3629
|
const chh = innerH / Math.max(1, rowN);
|
|
2916
3630
|
const gap = clamp(cellGap, 0, Math.min(cw, chh) - 1) || 0;
|
|
2917
|
-
|
|
3631
|
+
const svgPoint = (e) => {
|
|
3632
|
+
const svg = svgRef.current;
|
|
3633
|
+
if (!svg || typeof svg.getScreenCTM !== "function" || typeof svg.createSVGPoint !== "function") return null;
|
|
3634
|
+
const ctm = svg.getScreenCTM();
|
|
3635
|
+
if (!ctm || !ctm.a || !ctm.d) return null;
|
|
3636
|
+
const pt = svg.createSVGPoint();
|
|
3637
|
+
pt.x = e.clientX;
|
|
3638
|
+
pt.y = e.clientY;
|
|
3639
|
+
const loc = pt.matrixTransform(ctm.inverse());
|
|
3640
|
+
if (!Number.isFinite(loc.x) || !Number.isFinite(loc.y)) return null;
|
|
3641
|
+
return { px: loc.x, py: loc.y };
|
|
3642
|
+
};
|
|
3643
|
+
const colAtPx = (px) => cw > 0 ? clamp(Math.floor((clamp(px, padL, padL + innerW) - padL) / cw), 0, cols - 1) : 0;
|
|
3644
|
+
const rowAtPx = (py) => chh > 0 ? clamp(Math.floor((clamp(py, padT, padT + innerH) - padT) / chh), 0, rowN - 1) : 0;
|
|
3645
|
+
const cellAtPx = (px, py) => {
|
|
3646
|
+
if (!(cols > 0) || !(rowN > 0)) return null;
|
|
3647
|
+
if (px < padL || px > padL + innerW || py < padT || py > padT + innerH) return null;
|
|
3648
|
+
const ci = colAtPx(px);
|
|
3649
|
+
const ri = rowAtPx(py);
|
|
3650
|
+
const x = xs[ci], y = ys[ri];
|
|
3651
|
+
const v = cellOf(x, y);
|
|
3652
|
+
const has = v !== void 0;
|
|
3653
|
+
return { x, y, v, has, key: `${vc0 + ci}-${vr0 + ri}`, fill: has ? fillOf(v) : void 0 };
|
|
3654
|
+
};
|
|
3655
|
+
const applyZoom = (win) => {
|
|
3656
|
+
const c0 = clamp(Math.min(win.c0, win.c1), 0, nX - 1);
|
|
3657
|
+
const c1 = clamp(Math.max(win.c0, win.c1), 0, nX - 1);
|
|
3658
|
+
const r0 = clamp(Math.min(win.r0, win.r1), 0, nY - 1);
|
|
3659
|
+
const r1 = clamp(Math.max(win.r0, win.r1), 0, nY - 1);
|
|
3660
|
+
if (!(c1 >= c0) || !(r1 >= r0)) return;
|
|
3661
|
+
if (c0 === 0 && c1 === nX - 1 && r0 === 0 && r1 === nY - 1) {
|
|
3662
|
+
setZoom(null);
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
setZoom({ c0, c1, r0, r1 });
|
|
3666
|
+
};
|
|
3667
|
+
const panTo = (z0, dCols, dRows) => {
|
|
3668
|
+
const wc = z0.c1 - z0.c0, wr = z0.r1 - z0.r0;
|
|
3669
|
+
const c0 = clamp(z0.c0 + dCols, 0, Math.max(0, nX - 1 - wc));
|
|
3670
|
+
const r0 = clamp(z0.r0 + dRows, 0, Math.max(0, nY - 1 - wr));
|
|
3671
|
+
setZoom({ c0, c1: c0 + wc, r0, r1: r0 + wr });
|
|
3672
|
+
};
|
|
3673
|
+
const onOverlayDown = (e) => {
|
|
3674
|
+
const pt = svgPoint(e);
|
|
3675
|
+
if (!pt) return;
|
|
3676
|
+
movedRef.current = false;
|
|
3677
|
+
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 } });
|
|
3678
|
+
};
|
|
3679
|
+
const onOverlayMove = (e) => {
|
|
3680
|
+
const pt = svgPoint(e);
|
|
3681
|
+
if (!pt) return;
|
|
3682
|
+
if (drag) {
|
|
3683
|
+
if (Math.abs(pt.px - drag.x0) > 4 || Math.abs(pt.py - drag.y0) > 4) movedRef.current = true;
|
|
3684
|
+
if (drag.pan) {
|
|
3685
|
+
if (zoom) {
|
|
3686
|
+
const dCols = cw > 0 ? -Math.round((pt.px - drag.x0) / cw) : 0;
|
|
3687
|
+
const dRows = chh > 0 ? -Math.round((pt.py - drag.y0) / chh) : 0;
|
|
3688
|
+
panTo(drag.z0, dCols, dRows);
|
|
3689
|
+
}
|
|
3690
|
+
} else {
|
|
3691
|
+
setDrag({ ...drag, x1: pt.px, y1: pt.py });
|
|
3692
|
+
}
|
|
3693
|
+
setHovered(null);
|
|
3694
|
+
hide();
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
const cell = cellAtPx(pt.px, pt.py);
|
|
3698
|
+
if (cell) {
|
|
3699
|
+
setHovered(cell.key);
|
|
3700
|
+
show({ title: `${labelText3(cell.y)} / ${labelText3(cell.x)}`, items: [{ color: cell.fill, label: "", value: cell.has ? fmt(cell.v) : "\u2014" }] }, e);
|
|
3701
|
+
} else {
|
|
3702
|
+
setHovered(null);
|
|
3703
|
+
hide();
|
|
3704
|
+
}
|
|
3705
|
+
};
|
|
3706
|
+
const onOverlayUp = () => {
|
|
3707
|
+
if (drag && !drag.pan && movedRef.current) {
|
|
3708
|
+
applyZoom({
|
|
3709
|
+
c0: vc0 + colAtPx(drag.x0),
|
|
3710
|
+
c1: vc0 + colAtPx(drag.x1),
|
|
3711
|
+
r0: vr0 + rowAtPx(drag.y0),
|
|
3712
|
+
r1: vr0 + rowAtPx(drag.y1)
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
setDrag(null);
|
|
3716
|
+
};
|
|
3717
|
+
const onOverlayLeave = () => {
|
|
3718
|
+
setHovered(null);
|
|
3719
|
+
hide();
|
|
3720
|
+
setDrag(null);
|
|
3721
|
+
};
|
|
3722
|
+
const onOverlayClick = (e) => {
|
|
3723
|
+
if (movedRef.current) {
|
|
3724
|
+
movedRef.current = false;
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
const pt = svgPoint(e);
|
|
3728
|
+
if (!pt) return;
|
|
3729
|
+
const cell = cellAtPx(pt.px, pt.py);
|
|
3730
|
+
if (cell && cell.has) {
|
|
3731
|
+
if (onDataClick) onDataClick({ x: cell.x, y: cell.y, value: cell.v });
|
|
3732
|
+
selectMark(cell.key);
|
|
3733
|
+
}
|
|
3734
|
+
};
|
|
3735
|
+
React51__default.useEffect(() => {
|
|
3736
|
+
if (!canZoom) return void 0;
|
|
3737
|
+
const svg = svgRef.current;
|
|
3738
|
+
if (!svg) return void 0;
|
|
3739
|
+
const onWheel = (e) => {
|
|
3740
|
+
e.preventDefault();
|
|
3741
|
+
const pt = svgPoint(e);
|
|
3742
|
+
const fx = pt && innerW > 0 ? clamp((pt.px - padL) / innerW, 0, 1) : 0.5;
|
|
3743
|
+
const fy = pt && innerH > 0 ? clamp((pt.py - padT) / innerH, 0, 1) : 0.5;
|
|
3744
|
+
const factor = e.deltaY < 0 ? 0.7 : 1.4;
|
|
3745
|
+
const wc = vc1 - vc0 + 1, wr = vr1 - vr0 + 1;
|
|
3746
|
+
const newWc = clamp(Math.round(wc * factor), 1, nX);
|
|
3747
|
+
const newWr = clamp(Math.round(wr * factor), 1, nY);
|
|
3748
|
+
const curCol = vc0 + clamp(Math.floor(fx * cols), 0, cols - 1);
|
|
3749
|
+
const curRow = vr0 + clamp(Math.floor(fy * rowN), 0, rowN - 1);
|
|
3750
|
+
const nc0 = clamp(Math.round(curCol - fx * (newWc - 1)), 0, nX - newWc);
|
|
3751
|
+
const nr0 = clamp(Math.round(curRow - fy * (newWr - 1)), 0, nY - newWr);
|
|
3752
|
+
applyZoom({ c0: nc0, c1: nc0 + newWc - 1, r0: nr0, r1: nr0 + newWr - 1 });
|
|
3753
|
+
};
|
|
3754
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
3755
|
+
return () => svg.removeEventListener("wheel", onWheel);
|
|
3756
|
+
}, [canZoom, vc0, vc1, vr0, vr1]);
|
|
3757
|
+
const dragBand = canZoom && drag && !drag.pan && Math.abs(drag.x1 - drag.x0) > 2 && Math.abs(drag.y1 - drag.y0) > 2;
|
|
3758
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--heatmap ${className}`.trim(), "data-hovering": hovered != null || void 0, "data-has-selection": selected != null || void 0, "data-clickable": clickable || void 0, ...rest, children: [
|
|
2918
3759
|
baseStyles,
|
|
2919
3760
|
styles,
|
|
2920
|
-
/* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
|
|
3761
|
+
/* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
|
|
2921
3762
|
ys.map(
|
|
2922
3763
|
(y, ri) => xs.map((x, ci) => {
|
|
2923
3764
|
const v = cellOf(x, y);
|
|
@@ -2926,7 +3767,7 @@ function Heatmap({
|
|
|
2926
3767
|
const cy = padT + chh * ri + gap / 2;
|
|
2927
3768
|
const cwPx = Math.max(0, cw - gap);
|
|
2928
3769
|
const chPx = Math.max(0, chh - gap);
|
|
2929
|
-
const cellKey = `${ci}-${ri}`;
|
|
3770
|
+
const cellKey = `${vc0 + ci}-${vr0 + ri}`;
|
|
2930
3771
|
const fill = has ? fillOf(v) : void 0;
|
|
2931
3772
|
return /* @__PURE__ */ jsxs("g", { children: [
|
|
2932
3773
|
/* @__PURE__ */ jsx(
|
|
@@ -2941,6 +3782,7 @@ function Heatmap({
|
|
|
2941
3782
|
style: has ? { fill } : void 0,
|
|
2942
3783
|
"data-mark": true,
|
|
2943
3784
|
"data-active": hovered === cellKey || void 0,
|
|
3785
|
+
"data-selected": selected === cellKey || void 0,
|
|
2944
3786
|
onMouseMove: (e) => {
|
|
2945
3787
|
setHovered(cellKey);
|
|
2946
3788
|
show({ title: `${labelText3(y)} / ${labelText3(x)}`, items: [{ color: fill, label: "", value: has ? fmt(v) : "\u2014" }] }, e);
|
|
@@ -2948,7 +3790,11 @@ function Heatmap({
|
|
|
2948
3790
|
onMouseLeave: () => {
|
|
2949
3791
|
setHovered(null);
|
|
2950
3792
|
hide();
|
|
2951
|
-
}
|
|
3793
|
+
},
|
|
3794
|
+
onClick: has ? () => {
|
|
3795
|
+
if (onDataClick) onDataClick({ x, y, value: v });
|
|
3796
|
+
selectMark(cellKey);
|
|
3797
|
+
} : void 0
|
|
2952
3798
|
}
|
|
2953
3799
|
),
|
|
2954
3800
|
showValues && has ? /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-val", x: cx + cwPx / 2, y: cy + chPx / 2 + 3.5, textAnchor: "middle", children: fmt(v) }) : null
|
|
@@ -2958,8 +3804,43 @@ function Heatmap({
|
|
|
2958
3804
|
ys.map((y, ri) => /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-tick", x: padL - 8, y: padT + chh * ri + chh / 2 + 4, textAnchor: "end", children: labelText3(y) }, ri)),
|
|
2959
3805
|
xs.map((x, ci) => /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-tick", x: padL + cw * ci + cw / 2, y: padT + innerH + 15, textAnchor: "middle", children: labelText3(x) }, ci)),
|
|
2960
3806
|
yLabel != null ? /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-title", transform: `translate(11 ${padT + innerH / 2}) rotate(-90)`, textAnchor: "middle", children: labelText3(yLabel) }) : null,
|
|
2961
|
-
xLabel != null ? /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-title", x: padL + innerW / 2, y: H - 3, textAnchor: "middle", children: labelText3(xLabel) }) : null
|
|
3807
|
+
xLabel != null ? /* @__PURE__ */ jsx("text", { className: "twc-chart__hm-title", x: padL + innerW / 2, y: H - 3, textAnchor: "middle", children: labelText3(xLabel) }) : null,
|
|
3808
|
+
dragBand ? /* @__PURE__ */ jsx(
|
|
3809
|
+
"rect",
|
|
3810
|
+
{
|
|
3811
|
+
className: "twc-chart__zoomband",
|
|
3812
|
+
x: Math.min(drag.x0, drag.x1),
|
|
3813
|
+
y: Math.min(drag.y0, drag.y1),
|
|
3814
|
+
width: Math.abs(drag.x1 - drag.x0),
|
|
3815
|
+
height: Math.abs(drag.y1 - drag.y0)
|
|
3816
|
+
}
|
|
3817
|
+
) : null,
|
|
3818
|
+
canZoom ? /* @__PURE__ */ jsx(
|
|
3819
|
+
"rect",
|
|
3820
|
+
{
|
|
3821
|
+
className: "twc-chart__overlay",
|
|
3822
|
+
"data-zoom": "true",
|
|
3823
|
+
"data-clickable": clickable || void 0,
|
|
3824
|
+
"data-panning": drag && drag.pan || void 0,
|
|
3825
|
+
x: padL,
|
|
3826
|
+
y: padT,
|
|
3827
|
+
width: Math.max(0, innerW),
|
|
3828
|
+
height: Math.max(0, innerH),
|
|
3829
|
+
onMouseDown: onOverlayDown,
|
|
3830
|
+
onMouseMove: onOverlayMove,
|
|
3831
|
+
onMouseUp: onOverlayUp,
|
|
3832
|
+
onMouseLeave: onOverlayLeave,
|
|
3833
|
+
onClick: onOverlayClick
|
|
3834
|
+
}
|
|
3835
|
+
) : null
|
|
2962
3836
|
] }),
|
|
3837
|
+
canZoom && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
|
|
3838
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3839
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
3840
|
+
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
3841
|
+
] }),
|
|
3842
|
+
"Reset"
|
|
3843
|
+
] }) : null,
|
|
2963
3844
|
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
2964
3845
|
showLegend && span > 0 ? /* @__PURE__ */ jsxs("div", { className: "twc-chart__hm-scale", "aria-hidden": "true", children: [
|
|
2965
3846
|
/* @__PURE__ */ jsx("span", { children: fmt(lo) }),
|
|
@@ -2977,10 +3858,10 @@ function Heatmap({
|
|
|
2977
3858
|
{
|
|
2978
3859
|
id: tableId,
|
|
2979
3860
|
caption: caption ?? svgAriaLabel,
|
|
2980
|
-
columns:
|
|
2981
|
-
rows:
|
|
3861
|
+
columns: fullXs.map(labelText3),
|
|
3862
|
+
rows: fullYs.map((y) => ({
|
|
2982
3863
|
label: labelText3(y),
|
|
2983
|
-
values:
|
|
3864
|
+
values: fullXs.map((x) => {
|
|
2984
3865
|
const v = cellOf(x, y);
|
|
2985
3866
|
return v === void 0 ? "\u2014" : fmt(v);
|
|
2986
3867
|
})
|
|
@@ -2994,9 +3875,13 @@ function labelText3(label) {
|
|
|
2994
3875
|
}
|
|
2995
3876
|
var FUNNEL_CSS = `
|
|
2996
3877
|
.twc-chart--funnel svg { overflow: visible; }
|
|
2997
|
-
.twc-funnel__label { fill: var(--color-
|
|
3878
|
+
.twc-funnel__label { fill: var(--color-chart-on-fill); paint-order: stroke; stroke: var(--color-chart-on-fill-halo); stroke-width: 3px; stroke-linejoin: round; vector-effect: non-scaling-stroke; font-family: var(--font-sans); pointer-events: none; }
|
|
2998
3879
|
.twc-funnel__name { font-size: 12px; font-weight: 600; }
|
|
2999
|
-
.twc-funnel__value { font-size: 11px; fill-opacity: 0.
|
|
3880
|
+
.twc-funnel__value { font-size: 11px; fill-opacity: 0.9; }
|
|
3881
|
+
/* Explode: nudge the selected stage shape outward so it separates from the funnel
|
|
3882
|
+
(eases via the shared [data-mark] transform transition in _chart.js). */
|
|
3883
|
+
.twc-chart--funnel [data-mark][data-selected="true"] { transform: translateX(14px); }
|
|
3884
|
+
.twc-chart--funnel[data-orientation="horizontal"] [data-mark][data-selected="true"] { transform: translateY(-14px); }
|
|
3000
3885
|
`;
|
|
3001
3886
|
function FunnelChart({
|
|
3002
3887
|
data,
|
|
@@ -3007,6 +3892,7 @@ function FunnelChart({
|
|
|
3007
3892
|
height = 300,
|
|
3008
3893
|
colors,
|
|
3009
3894
|
valueFormat,
|
|
3895
|
+
onDataClick,
|
|
3010
3896
|
ariaLabel,
|
|
3011
3897
|
"aria-label": ariaLabelProp,
|
|
3012
3898
|
tableFallback = true,
|
|
@@ -3020,6 +3906,8 @@ function FunnelChart({
|
|
|
3020
3906
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
3021
3907
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
3022
3908
|
const [active, setActive] = React51__default.useState(null);
|
|
3909
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
3910
|
+
const clickable = !!onDataClick;
|
|
3023
3911
|
const rows = data || [];
|
|
3024
3912
|
const n = rows.length;
|
|
3025
3913
|
const fmt = valueFormat || fmtNumber;
|
|
@@ -3071,6 +3959,7 @@ function FunnelChart({
|
|
|
3071
3959
|
fill: color,
|
|
3072
3960
|
"data-mark": true,
|
|
3073
3961
|
"data-active": active === i ? "true" : void 0,
|
|
3962
|
+
"data-selected": selected === i ? "true" : void 0,
|
|
3074
3963
|
onMouseMove: (e) => {
|
|
3075
3964
|
setActive(i);
|
|
3076
3965
|
show({ title: d.label, items: [{ color, label: fmt(v), value: pctText(v) }] }, e);
|
|
@@ -3078,6 +3967,10 @@ function FunnelChart({
|
|
|
3078
3967
|
onMouseLeave: () => {
|
|
3079
3968
|
setActive(null);
|
|
3080
3969
|
hide();
|
|
3970
|
+
},
|
|
3971
|
+
onClick: () => {
|
|
3972
|
+
if (onDataClick) onDataClick({ label: d.label, value: v, index: i, percent: pct(v) });
|
|
3973
|
+
setSelected((s) => s === i ? null : i);
|
|
3081
3974
|
}
|
|
3082
3975
|
}
|
|
3083
3976
|
),
|
|
@@ -3093,7 +3986,7 @@ function FunnelChart({
|
|
|
3093
3986
|
)) }) : null
|
|
3094
3987
|
] }, i);
|
|
3095
3988
|
});
|
|
3096
|
-
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--funnel ${className}`.trim(), "data-orientation": horizontal ? "horizontal" : "vertical", "data-hovering": active != null ? "true" : void 0, ...rest, children: [
|
|
3989
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--funnel ${className}`.trim(), "data-orientation": horizontal ? "horizontal" : "vertical", "data-hovering": active != null ? "true" : void 0, "data-has-selection": selected != null || void 0, "data-clickable": clickable || void 0, ...rest, children: [
|
|
3097
3990
|
baseStyles,
|
|
3098
3991
|
styles,
|
|
3099
3992
|
/* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: renderStages() }),
|
|
@@ -3110,8 +4003,8 @@ function FunnelChart({
|
|
|
3110
4003
|
] });
|
|
3111
4004
|
}
|
|
3112
4005
|
var TREEMAP_CSS = `
|
|
3113
|
-
.twc-chart__tile-label { fill: var(--color-
|
|
3114
|
-
.twc-chart__tile-value { fill: var(--color-
|
|
4006
|
+
.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; }
|
|
4007
|
+
.twc-chart__tile-value { fill: var(--color-chart-on-fill); paint-order: stroke; stroke: var(--color-chart-on-fill-halo); stroke-width: 3px; stroke-linejoin: round; vector-effect: non-scaling-stroke; font-size: 11px; opacity: 0.9; pointer-events: none; }
|
|
3115
4008
|
.twc-chart__empty { fill: var(--color-text-subtle); font-size: var(--text-sm); }
|
|
3116
4009
|
`;
|
|
3117
4010
|
function Treemap({
|
|
@@ -3121,6 +4014,7 @@ function Treemap({
|
|
|
3121
4014
|
height = 300,
|
|
3122
4015
|
colors,
|
|
3123
4016
|
gap = 2,
|
|
4017
|
+
onDataClick,
|
|
3124
4018
|
ariaLabel,
|
|
3125
4019
|
"aria-label": ariaLabelProp,
|
|
3126
4020
|
tableFallback = true,
|
|
@@ -3134,16 +4028,18 @@ function Treemap({
|
|
|
3134
4028
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
3135
4029
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
3136
4030
|
const [hovered, setHovered] = React51__default.useState(null);
|
|
4031
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
3137
4032
|
const rows = data || [];
|
|
3138
4033
|
const fmt = valueFormat || fmtNumber;
|
|
3139
4034
|
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "treemap";
|
|
4035
|
+
const clickable = !!onDataClick;
|
|
3140
4036
|
const W = 600, H = height;
|
|
3141
4037
|
const weights = rows.map((d) => Math.max(0, Number(d.value) || 0));
|
|
3142
4038
|
const total = sum(weights);
|
|
3143
4039
|
const items = total > 0 ? rows.map((d, i) => ({ i, w: weights[i] })).sort((a, b) => b.w - a.w).map((it) => ({ ...it, area: it.w / total * (W * H) })) : [];
|
|
3144
4040
|
const tiles = squarify(items, { x: 0, y: 0, w: W, h: H });
|
|
3145
4041
|
const g = Math.max(0, gap);
|
|
3146
|
-
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--treemap ${className}`.trim(), "data-hovering": hovered != null || void 0, ...rest, children: [
|
|
4042
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `twc-chart twc-chart--treemap ${className}`.trim(), "data-hovering": hovered != null || void 0, "data-has-selection": selected != null || void 0, "data-clickable": clickable || void 0, ...rest, children: [
|
|
3147
4043
|
baseStyles,
|
|
3148
4044
|
styles,
|
|
3149
4045
|
/* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: tiles.length === 0 ? /* @__PURE__ */ jsx("text", { className: "twc-chart__empty", x: W / 2, y: H / 2, textAnchor: "middle", dominantBaseline: "middle", children: "No data" }) : tiles.map((t) => {
|
|
@@ -3166,6 +4062,7 @@ function Treemap({
|
|
|
3166
4062
|
className: "twc-chart__tile twc-chart__anim-fade",
|
|
3167
4063
|
"data-mark": true,
|
|
3168
4064
|
"data-active": hovered === t.i || void 0,
|
|
4065
|
+
"data-selected": selected === t.i || void 0,
|
|
3169
4066
|
x,
|
|
3170
4067
|
y,
|
|
3171
4068
|
width: w,
|
|
@@ -3179,6 +4076,10 @@ function Treemap({
|
|
|
3179
4076
|
onMouseLeave: () => {
|
|
3180
4077
|
setHovered(null);
|
|
3181
4078
|
hide();
|
|
4079
|
+
},
|
|
4080
|
+
onClick: () => {
|
|
4081
|
+
if (onDataClick) onDataClick({ label: d.label, value: t.w, index: t.i, share: total > 0 ? t.w / total : 0 });
|
|
4082
|
+
setSelected((s) => s === t.i ? null : t.i);
|
|
3182
4083
|
}
|
|
3183
4084
|
}
|
|
3184
4085
|
),
|
|
@@ -3273,123 +4174,290 @@ function Candlestick({
|
|
|
3273
4174
|
height = 300,
|
|
3274
4175
|
showGrid = true,
|
|
3275
4176
|
showAxis = true,
|
|
4177
|
+
zoomable = false,
|
|
3276
4178
|
valueFormat,
|
|
4179
|
+
onDataClick,
|
|
3277
4180
|
ariaLabel,
|
|
3278
4181
|
"aria-label": ariaLabelProp,
|
|
3279
|
-
tableFallback = true,
|
|
3280
|
-
caption,
|
|
3281
|
-
className = "",
|
|
3282
|
-
...rest
|
|
3283
|
-
}) {
|
|
3284
|
-
const baseStyles = useScopedStyles("twc-chart-base", CHART_BASE_CSS);
|
|
3285
|
-
const styles = useScopedStyles("twc-candlestick-styles", CANDLE_CSS);
|
|
3286
|
-
const uid = React51__default.useId();
|
|
3287
|
-
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
3288
|
-
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
3289
|
-
const
|
|
3290
|
-
const
|
|
3291
|
-
const
|
|
3292
|
-
const
|
|
3293
|
-
const
|
|
3294
|
-
const
|
|
3295
|
-
const
|
|
3296
|
-
const
|
|
3297
|
-
const
|
|
3298
|
-
const
|
|
3299
|
-
const
|
|
3300
|
-
const
|
|
3301
|
-
const
|
|
3302
|
-
const
|
|
3303
|
-
const
|
|
3304
|
-
const
|
|
3305
|
-
const
|
|
3306
|
-
const
|
|
3307
|
-
const
|
|
3308
|
-
const
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
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
|
-
i
|
|
3373
|
-
);
|
|
3374
|
-
}),
|
|
3375
|
-
showAxis ? rows.map(
|
|
3376
|
-
(d, i) => i % labelStep === 0 ? /* @__PURE__ */ jsx("text", { className: "twc-candlestick__axis", x: cx(i), y: H - 8, textAnchor: "middle", children: d.label }, i) : null
|
|
3377
|
-
) : null
|
|
3378
|
-
] }),
|
|
3379
|
-
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
3380
|
-
tableFallback ? /* @__PURE__ */ jsx(
|
|
3381
|
-
ChartTable,
|
|
3382
|
-
{
|
|
3383
|
-
id: tableId,
|
|
3384
|
-
caption: caption ?? svgAriaLabel,
|
|
3385
|
-
columns: ["open", "high", "low", "close"],
|
|
3386
|
-
rows: rows.map((d) => ({
|
|
3387
|
-
label: d.label,
|
|
3388
|
-
values: [Number(d.open) || 0, Number(d.high) || 0, Number(d.low) || 0, Number(d.close) || 0].map(fmt)
|
|
3389
|
-
}))
|
|
4182
|
+
tableFallback = true,
|
|
4183
|
+
caption,
|
|
4184
|
+
className = "",
|
|
4185
|
+
...rest
|
|
4186
|
+
}) {
|
|
4187
|
+
const baseStyles = useScopedStyles("twc-chart-base", CHART_BASE_CSS);
|
|
4188
|
+
const styles = useScopedStyles("twc-candlestick-styles", CANDLE_CSS);
|
|
4189
|
+
const uid = React51__default.useId();
|
|
4190
|
+
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
4191
|
+
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
4192
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
4193
|
+
const [zoom, setZoom] = React51__default.useState(null);
|
|
4194
|
+
const [drag, setDrag] = React51__default.useState(null);
|
|
4195
|
+
const svgRef = React51__default.useRef(null);
|
|
4196
|
+
const fmt = valueFormat || fmtNumber;
|
|
4197
|
+
const clickable = !!onDataClick;
|
|
4198
|
+
const allRows = data || [];
|
|
4199
|
+
const baseIdx = zoom ? zoom.s : 0;
|
|
4200
|
+
const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
|
|
4201
|
+
const W = 600, H = height;
|
|
4202
|
+
const padL = showAxis ? 44 : 8;
|
|
4203
|
+
const padR = 10;
|
|
4204
|
+
const padT = 12;
|
|
4205
|
+
const padB = showAxis ? 26 : 8;
|
|
4206
|
+
const innerW = W - padL - padR, innerH = H - padT - padB;
|
|
4207
|
+
const lows = rows.map((d) => Number(d.low)).filter(Number.isFinite);
|
|
4208
|
+
const highs = rows.map((d) => Number(d.high)).filter(Number.isFinite);
|
|
4209
|
+
const minLow = lows.length ? Math.min(...lows) : 0;
|
|
4210
|
+
const maxHigh = highs.length ? Math.max(...highs) : 1;
|
|
4211
|
+
const scale = niceScale(minLow, maxHigh, 5);
|
|
4212
|
+
const span = scale.max - scale.min || 1;
|
|
4213
|
+
const vPos = (v) => padT + innerH - (v - scale.min) / span * innerH;
|
|
4214
|
+
const catBand = innerW / Math.max(1, rows.length);
|
|
4215
|
+
const cx = (i) => padL + catBand * i + catBand / 2;
|
|
4216
|
+
const bodyW = Math.max(1, Math.min(catBand * 0.7, 22));
|
|
4217
|
+
const labelStep = Math.max(1, Math.ceil(rows.length / 12));
|
|
4218
|
+
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "candlestick chart";
|
|
4219
|
+
const tipFor = (d) => {
|
|
4220
|
+
const open = Number(d.open) || 0, high = Number(d.high) || 0;
|
|
4221
|
+
const low = Number(d.low) || 0, close = Number(d.close) || 0;
|
|
4222
|
+
const color = close >= open ? upColor : downColor;
|
|
4223
|
+
return {
|
|
4224
|
+
title: labelText5(d.label),
|
|
4225
|
+
items: [
|
|
4226
|
+
{ color, label: "Open", value: fmt(open) },
|
|
4227
|
+
{ label: "High", value: fmt(high) },
|
|
4228
|
+
{ label: "Low", value: fmt(low) },
|
|
4229
|
+
{ label: "Close", value: fmt(close) }
|
|
4230
|
+
]
|
|
4231
|
+
};
|
|
4232
|
+
};
|
|
4233
|
+
const selectMark = (key) => setSelected((s) => s === key ? null : key);
|
|
4234
|
+
const clickCandle = (d, index) => {
|
|
4235
|
+
if (!onDataClick) return;
|
|
4236
|
+
onDataClick({
|
|
4237
|
+
label: d.label,
|
|
4238
|
+
open: Number(d.open) || 0,
|
|
4239
|
+
high: Number(d.high) || 0,
|
|
4240
|
+
low: Number(d.low) || 0,
|
|
4241
|
+
close: Number(d.close) || 0,
|
|
4242
|
+
index
|
|
4243
|
+
});
|
|
4244
|
+
};
|
|
4245
|
+
const applyZoom = (s, e) => {
|
|
4246
|
+
const n = allRows.length;
|
|
4247
|
+
if (e - s >= n - 1) {
|
|
4248
|
+
setZoom(null);
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
setZoom({ s: Math.max(0, s), e: Math.min(n - 1, e) });
|
|
4252
|
+
};
|
|
4253
|
+
const catFromEvent = (e) => {
|
|
4254
|
+
const svg = svgRef.current;
|
|
4255
|
+
if (!svg) return null;
|
|
4256
|
+
if (!rows.length) return null;
|
|
4257
|
+
const r2 = svg.getBoundingClientRect();
|
|
4258
|
+
if (!(r2.width > 0)) return null;
|
|
4259
|
+
const frac = Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW));
|
|
4260
|
+
const idx = Math.floor(frac * rows.length);
|
|
4261
|
+
return Math.min(rows.length - 1, Math.max(0, idx));
|
|
4262
|
+
};
|
|
4263
|
+
const onOverlayMove = (e) => {
|
|
4264
|
+
const idx = catFromEvent(e);
|
|
4265
|
+
if (idx == null || !rows[idx]) return;
|
|
4266
|
+
show(tipFor(rows[idx]), e);
|
|
4267
|
+
if (drag) {
|
|
4268
|
+
if (drag.pan) {
|
|
4269
|
+
const spanZ = drag.z0.e - drag.z0.s;
|
|
4270
|
+
const s = Math.min(allRows.length - 1 - spanZ, Math.max(0, drag.z0.s + (drag.start - idx)));
|
|
4271
|
+
applyZoom(s, s + spanZ);
|
|
4272
|
+
} else {
|
|
4273
|
+
setDrag({ ...drag, end: idx });
|
|
3390
4274
|
}
|
|
3391
|
-
|
|
3392
|
-
|
|
4275
|
+
}
|
|
4276
|
+
};
|
|
4277
|
+
const onOverlayDown = (e) => {
|
|
4278
|
+
const idx = catFromEvent(e);
|
|
4279
|
+
if (idx == null) return;
|
|
4280
|
+
setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
|
|
4281
|
+
};
|
|
4282
|
+
const onOverlayUp = () => {
|
|
4283
|
+
if (drag && !drag.pan) {
|
|
4284
|
+
const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
|
|
4285
|
+
if (hi - lo >= 1) applyZoom(baseIdx + lo, baseIdx + hi);
|
|
4286
|
+
}
|
|
4287
|
+
setDrag(null);
|
|
4288
|
+
};
|
|
4289
|
+
const onOverlayClick = (e) => {
|
|
4290
|
+
if (!onDataClick) return;
|
|
4291
|
+
const idx = catFromEvent(e);
|
|
4292
|
+
if (idx == null || !rows[idx]) return;
|
|
4293
|
+
clickCandle(rows[idx], baseIdx + idx);
|
|
4294
|
+
selectMark(baseIdx + idx);
|
|
4295
|
+
};
|
|
4296
|
+
React51__default.useEffect(() => {
|
|
4297
|
+
if (!zoomable) return void 0;
|
|
4298
|
+
const svg = svgRef.current;
|
|
4299
|
+
if (!svg) return void 0;
|
|
4300
|
+
const onWheel = (e) => {
|
|
4301
|
+
const n = allRows.length;
|
|
4302
|
+
if (!n) return;
|
|
4303
|
+
e.preventDefault();
|
|
4304
|
+
const cur = zoom || { s: 0, e: n - 1 };
|
|
4305
|
+
const spanZ = cur.e - cur.s;
|
|
4306
|
+
const r2 = svg.getBoundingClientRect();
|
|
4307
|
+
const frac = r2.width > 0 ? Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW)) : 0.5;
|
|
4308
|
+
const center = cur.s + Math.round(frac * spanZ);
|
|
4309
|
+
const newSpan = Math.min(n - 1, Math.max(1, Math.round(spanZ * (e.deltaY < 0 ? 0.8 : 1.25))));
|
|
4310
|
+
const s = Math.min(n - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frac)));
|
|
4311
|
+
applyZoom(s, s + newSpan);
|
|
4312
|
+
};
|
|
4313
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
4314
|
+
return () => svg.removeEventListener("wheel", onWheel);
|
|
4315
|
+
}, [zoomable, zoom, allRows.length]);
|
|
4316
|
+
const renderZones = () => rows.map((d, i) => {
|
|
4317
|
+
const gi = baseIdx + i;
|
|
4318
|
+
return /* @__PURE__ */ jsx(
|
|
4319
|
+
"rect",
|
|
4320
|
+
{
|
|
4321
|
+
className: "twc-chart__zone",
|
|
4322
|
+
x: padL + catBand * i,
|
|
4323
|
+
y: padT,
|
|
4324
|
+
width: catBand,
|
|
4325
|
+
height: innerH,
|
|
4326
|
+
"data-clickable": clickable || void 0,
|
|
4327
|
+
onMouseMove: (e) => show(tipFor(d), e),
|
|
4328
|
+
onMouseLeave: hide,
|
|
4329
|
+
onClick: () => {
|
|
4330
|
+
if (onDataClick) clickCandle(d, gi);
|
|
4331
|
+
}
|
|
4332
|
+
},
|
|
4333
|
+
i
|
|
4334
|
+
);
|
|
4335
|
+
});
|
|
4336
|
+
const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
|
|
4337
|
+
return /* @__PURE__ */ jsxs(
|
|
4338
|
+
"div",
|
|
4339
|
+
{
|
|
4340
|
+
ref: containerRef,
|
|
4341
|
+
className: `twc-chart twc-chart--candlestick ${className}`.trim(),
|
|
4342
|
+
"data-clickable": clickable || void 0,
|
|
4343
|
+
"data-has-selection": selected != null || void 0,
|
|
4344
|
+
...rest,
|
|
4345
|
+
children: [
|
|
4346
|
+
baseStyles,
|
|
4347
|
+
styles,
|
|
4348
|
+
/* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
|
|
4349
|
+
showGrid ? scale.ticks.map((t, i) => {
|
|
4350
|
+
const p = vPos(t);
|
|
4351
|
+
return /* @__PURE__ */ jsx("line", { className: "twc-candlestick__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
|
|
4352
|
+
}) : null,
|
|
4353
|
+
showAxis ? scale.ticks.map((t, i) => /* @__PURE__ */ jsx("text", { className: "twc-candlestick__axis", x: padL - 8, y: vPos(t) + 4, textAnchor: "end", children: shortNum(t) }, i)) : null,
|
|
4354
|
+
!zoomable ? renderZones() : null,
|
|
4355
|
+
rows.map((d, i) => {
|
|
4356
|
+
const open = Number(d.open) || 0;
|
|
4357
|
+
const high = Number(d.high) || 0;
|
|
4358
|
+
const low = Number(d.low) || 0;
|
|
4359
|
+
const close = Number(d.close) || 0;
|
|
4360
|
+
const up = close >= open;
|
|
4361
|
+
const color = up ? upColor : downColor;
|
|
4362
|
+
const x = cx(i);
|
|
4363
|
+
const gi = baseIdx + i;
|
|
4364
|
+
const yTop = vPos(Math.max(open, close));
|
|
4365
|
+
const yBot = vPos(Math.min(open, close));
|
|
4366
|
+
const bh = Math.max(1, yBot - yTop);
|
|
4367
|
+
return /* @__PURE__ */ jsxs(
|
|
4368
|
+
"g",
|
|
4369
|
+
{
|
|
4370
|
+
className: "twc-chart__anim-fade",
|
|
4371
|
+
"data-dir": up ? "up" : "down",
|
|
4372
|
+
onMouseMove: (e) => show(tipFor(d), e),
|
|
4373
|
+
onMouseLeave: hide,
|
|
4374
|
+
onClick: () => {
|
|
4375
|
+
clickCandle(d, gi);
|
|
4376
|
+
selectMark(gi);
|
|
4377
|
+
},
|
|
4378
|
+
children: [
|
|
4379
|
+
/* @__PURE__ */ jsx(
|
|
4380
|
+
"line",
|
|
4381
|
+
{
|
|
4382
|
+
className: "twc-candlestick__wick",
|
|
4383
|
+
style: { stroke: color },
|
|
4384
|
+
x1: x,
|
|
4385
|
+
y1: vPos(high),
|
|
4386
|
+
x2: x,
|
|
4387
|
+
y2: vPos(low)
|
|
4388
|
+
}
|
|
4389
|
+
),
|
|
4390
|
+
/* @__PURE__ */ jsx(
|
|
4391
|
+
"rect",
|
|
4392
|
+
{
|
|
4393
|
+
className: "twc-candlestick__body",
|
|
4394
|
+
style: { fill: color },
|
|
4395
|
+
"data-mark": true,
|
|
4396
|
+
"data-selected": selected === gi || void 0,
|
|
4397
|
+
x: x - bodyW / 2,
|
|
4398
|
+
y: yTop,
|
|
4399
|
+
width: bodyW,
|
|
4400
|
+
height: bh,
|
|
4401
|
+
rx: "1"
|
|
4402
|
+
}
|
|
4403
|
+
)
|
|
4404
|
+
]
|
|
4405
|
+
},
|
|
4406
|
+
i
|
|
4407
|
+
);
|
|
4408
|
+
}),
|
|
4409
|
+
showAxis ? rows.map(
|
|
4410
|
+
(d, i) => i % labelStep === 0 ? /* @__PURE__ */ jsx("text", { className: "twc-candlestick__axis", x: cx(i), y: H - 8, textAnchor: "middle", children: d.label }, i) : null
|
|
4411
|
+
) : null,
|
|
4412
|
+
dragBand ? (() => {
|
|
4413
|
+
const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
|
|
4414
|
+
const x0 = padL + catBand * lo;
|
|
4415
|
+
const x1 = padL + catBand * (hi + 1);
|
|
4416
|
+
return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
|
|
4417
|
+
})() : null,
|
|
4418
|
+
zoomable ? /* @__PURE__ */ jsx(
|
|
4419
|
+
"rect",
|
|
4420
|
+
{
|
|
4421
|
+
className: "twc-chart__overlay",
|
|
4422
|
+
"data-clickable": clickable || void 0,
|
|
4423
|
+
x: padL,
|
|
4424
|
+
y: padT,
|
|
4425
|
+
width: innerW,
|
|
4426
|
+
height: innerH,
|
|
4427
|
+
onMouseMove: onOverlayMove,
|
|
4428
|
+
onMouseLeave: () => {
|
|
4429
|
+
hide();
|
|
4430
|
+
setDrag(null);
|
|
4431
|
+
},
|
|
4432
|
+
onMouseDown: onOverlayDown,
|
|
4433
|
+
onMouseUp: onOverlayUp,
|
|
4434
|
+
onClick: onOverlayClick
|
|
4435
|
+
}
|
|
4436
|
+
) : null
|
|
4437
|
+
] }),
|
|
4438
|
+
zoomable && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
|
|
4439
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4440
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
4441
|
+
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
4442
|
+
] }),
|
|
4443
|
+
"Reset"
|
|
4444
|
+
] }) : null,
|
|
4445
|
+
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
4446
|
+
tableFallback ? /* @__PURE__ */ jsx(
|
|
4447
|
+
ChartTable,
|
|
4448
|
+
{
|
|
4449
|
+
id: tableId,
|
|
4450
|
+
caption: caption ?? svgAriaLabel,
|
|
4451
|
+
columns: ["open", "high", "low", "close"],
|
|
4452
|
+
rows: allRows.map((d) => ({
|
|
4453
|
+
label: d.label,
|
|
4454
|
+
values: [Number(d.open) || 0, Number(d.high) || 0, Number(d.low) || 0, Number(d.close) || 0].map(fmt)
|
|
4455
|
+
}))
|
|
4456
|
+
}
|
|
4457
|
+
) : null
|
|
4458
|
+
]
|
|
4459
|
+
}
|
|
4460
|
+
);
|
|
3393
4461
|
}
|
|
3394
4462
|
function labelText5(label) {
|
|
3395
4463
|
return typeof label === "string" || typeof label === "number" ? String(label) : "";
|
|
@@ -3409,7 +4477,9 @@ function Boxplot({
|
|
|
3409
4477
|
height = 300,
|
|
3410
4478
|
showGrid = true,
|
|
3411
4479
|
showAxis = true,
|
|
4480
|
+
zoomable = false,
|
|
3412
4481
|
valueFormat,
|
|
4482
|
+
onDataClick,
|
|
3413
4483
|
ariaLabel,
|
|
3414
4484
|
"aria-label": ariaLabelProp,
|
|
3415
4485
|
tableFallback = true,
|
|
@@ -3422,9 +4492,28 @@ function Boxplot({
|
|
|
3422
4492
|
const uid = React51__default.useId();
|
|
3423
4493
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
3424
4494
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
3425
|
-
const
|
|
4495
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
4496
|
+
const [zoom, setZoom] = React51__default.useState(null);
|
|
4497
|
+
const [drag, setDrag] = React51__default.useState(null);
|
|
4498
|
+
const svgRef = React51__default.useRef(null);
|
|
4499
|
+
const allRows = data || [];
|
|
4500
|
+
const baseIdx = zoom ? zoom.s : 0;
|
|
4501
|
+
const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
|
|
3426
4502
|
const fmt = valueFormat || fmtNumber;
|
|
3427
4503
|
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? "box plot";
|
|
4504
|
+
const clickable = !!onDataClick;
|
|
4505
|
+
const clickBox = (d, i) => {
|
|
4506
|
+
if (onDataClick) onDataClick({
|
|
4507
|
+
label: d.label,
|
|
4508
|
+
min: Number(d.min),
|
|
4509
|
+
q1: Number(d.q1),
|
|
4510
|
+
median: Number(d.median),
|
|
4511
|
+
q3: Number(d.q3),
|
|
4512
|
+
max: Number(d.max),
|
|
4513
|
+
index: baseIdx + i
|
|
4514
|
+
});
|
|
4515
|
+
setSelected((s) => s === i ? null : i);
|
|
4516
|
+
};
|
|
3428
4517
|
const tipFor = (d) => ({
|
|
3429
4518
|
title: labelText6(d.label),
|
|
3430
4519
|
items: [
|
|
@@ -3457,69 +4546,184 @@ function Boxplot({
|
|
|
3457
4546
|
const band = innerW / Math.max(1, rows.length);
|
|
3458
4547
|
const bandX = (i) => padL + band * i;
|
|
3459
4548
|
const boxW = Math.min(band * 0.5, 46);
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
4549
|
+
const applyZoom = (s, e) => {
|
|
4550
|
+
const n = allRows.length;
|
|
4551
|
+
if (e - s >= n - 1) {
|
|
4552
|
+
setZoom(null);
|
|
4553
|
+
return;
|
|
4554
|
+
}
|
|
4555
|
+
setZoom({ s: Math.max(0, s), e: Math.min(n - 1, e) });
|
|
4556
|
+
};
|
|
4557
|
+
const catFromEvent = (e) => {
|
|
4558
|
+
const svg = svgRef.current;
|
|
4559
|
+
if (!svg) return null;
|
|
4560
|
+
const r2 = svg.getBoundingClientRect();
|
|
4561
|
+
if (!(r2.width > 0)) return null;
|
|
4562
|
+
const frac = Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW));
|
|
4563
|
+
const idx = Math.floor(frac * rows.length);
|
|
4564
|
+
return Math.min(rows.length - 1, Math.max(0, idx));
|
|
4565
|
+
};
|
|
4566
|
+
const onOverlayMove = (e) => {
|
|
4567
|
+
const idx = catFromEvent(e);
|
|
4568
|
+
if (idx == null) return;
|
|
4569
|
+
show(tipFor(rows[idx]), e);
|
|
4570
|
+
if (drag) {
|
|
4571
|
+
if (drag.pan) {
|
|
4572
|
+
const s0 = drag.z0.e - drag.z0.s;
|
|
4573
|
+
const s = Math.min(allRows.length - 1 - s0, Math.max(0, drag.z0.s + (drag.start - idx)));
|
|
4574
|
+
applyZoom(s, s + s0);
|
|
4575
|
+
} else {
|
|
4576
|
+
setDrag({ ...drag, end: idx });
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
};
|
|
4580
|
+
const onOverlayDown = (e) => {
|
|
4581
|
+
const idx = catFromEvent(e);
|
|
4582
|
+
if (idx == null) return;
|
|
4583
|
+
setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
|
|
4584
|
+
};
|
|
4585
|
+
const onOverlayUp = () => {
|
|
4586
|
+
if (drag && !drag.pan) {
|
|
4587
|
+
const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
|
|
4588
|
+
if (hi - lo >= 1) applyZoom(baseIdx + lo, baseIdx + hi);
|
|
4589
|
+
}
|
|
4590
|
+
setDrag(null);
|
|
4591
|
+
};
|
|
4592
|
+
const onOverlayClick = (e) => {
|
|
4593
|
+
if (drag && drag.end !== drag.start) return;
|
|
4594
|
+
const idx = catFromEvent(e);
|
|
4595
|
+
if (idx == null) return;
|
|
4596
|
+
clickBox(rows[idx], idx);
|
|
4597
|
+
};
|
|
4598
|
+
React51__default.useEffect(() => {
|
|
4599
|
+
if (!zoomable) return void 0;
|
|
4600
|
+
const svg = svgRef.current;
|
|
4601
|
+
if (!svg) return void 0;
|
|
4602
|
+
const onWheel = (e) => {
|
|
4603
|
+
e.preventDefault();
|
|
4604
|
+
const n = allRows.length;
|
|
4605
|
+
const cur = zoom || { s: 0, e: n - 1 };
|
|
4606
|
+
const wSpan = cur.e - cur.s;
|
|
4607
|
+
const r2 = svg.getBoundingClientRect();
|
|
4608
|
+
const frac = r2.width > 0 ? Math.min(1, Math.max(0, ((e.clientX - r2.left) / r2.width * W - padL) / innerW)) : 0.5;
|
|
4609
|
+
const center = cur.s + Math.round(frac * wSpan);
|
|
4610
|
+
const newSpan = Math.min(n - 1, Math.max(1, Math.round(wSpan * (e.deltaY < 0 ? 0.8 : 1.25))));
|
|
4611
|
+
const s = Math.min(n - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frac)));
|
|
4612
|
+
applyZoom(s, s + newSpan);
|
|
4613
|
+
};
|
|
4614
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
4615
|
+
return () => svg.removeEventListener("wheel", onWheel);
|
|
4616
|
+
}, [zoomable, zoom, allRows.length]);
|
|
4617
|
+
const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
|
|
4618
|
+
return /* @__PURE__ */ jsxs(
|
|
4619
|
+
"div",
|
|
4620
|
+
{
|
|
4621
|
+
ref: containerRef,
|
|
4622
|
+
className: `twc-chart twc-chart--boxplot ${className}`.trim(),
|
|
4623
|
+
"data-has-selection": selected != null || void 0,
|
|
4624
|
+
"data-clickable": clickable || void 0,
|
|
4625
|
+
...rest,
|
|
4626
|
+
children: [
|
|
4627
|
+
baseStyles,
|
|
4628
|
+
styles,
|
|
4629
|
+
/* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
|
|
4630
|
+
showGrid ? scale.ticks.map((t, i) => {
|
|
4631
|
+
const p = vPos(t);
|
|
4632
|
+
return /* @__PURE__ */ jsx("line", { className: "twc-boxplot__grid", x1: padL, y1: p, x2: W - padR, y2: p }, i);
|
|
4633
|
+
}) : null,
|
|
4634
|
+
showAxis ? scale.ticks.map((t, i) => {
|
|
4635
|
+
const p = vPos(t);
|
|
4636
|
+
return /* @__PURE__ */ jsx("text", { className: "twc-boxplot__axis", x: padL - 8, y: p + 4, textAnchor: "end", children: shortNum(t) }, i);
|
|
4637
|
+
}) : null,
|
|
4638
|
+
rows.map((d, i) => {
|
|
4639
|
+
const cx = bandX(i) + band / 2;
|
|
4640
|
+
const x = cx - boxW / 2;
|
|
4641
|
+
const yMin = vPos(d.min), yMax = vPos(d.max);
|
|
4642
|
+
const yQ1 = vPos(d.q1), yQ3 = vPos(d.q3), yMed = vPos(d.median);
|
|
4643
|
+
const boxTop = Math.min(yQ1, yQ3), boxBot = Math.max(yQ1, yQ3);
|
|
4644
|
+
const cap = boxW / 3;
|
|
4645
|
+
const hoverProps = {
|
|
4646
|
+
onMouseMove: (e) => show(tipFor(d), e),
|
|
4647
|
+
onMouseLeave: hide
|
|
4648
|
+
};
|
|
4649
|
+
return /* @__PURE__ */ jsxs("g", { className: "twc-chart__anim-fade", style: { color }, children: [
|
|
4650
|
+
/* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx, y1: yMax, x2: cx, y2: boxTop }),
|
|
4651
|
+
/* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx, y1: boxBot, x2: cx, y2: yMin }),
|
|
4652
|
+
/* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx - cap, y1: yMax, x2: cx + cap, y2: yMax }),
|
|
4653
|
+
/* @__PURE__ */ jsx("line", { className: "twc-boxplot__whisker", style: { stroke: color }, x1: cx - cap, y1: yMin, x2: cx + cap, y2: yMin }),
|
|
4654
|
+
/* @__PURE__ */ jsx(
|
|
4655
|
+
"rect",
|
|
4656
|
+
{
|
|
4657
|
+
className: "twc-boxplot__box",
|
|
4658
|
+
"data-mark": true,
|
|
4659
|
+
"data-selected": selected === i || void 0,
|
|
4660
|
+
style: selected === i ? { fill: color } : { fill: color, stroke: color },
|
|
4661
|
+
x,
|
|
4662
|
+
y: boxTop,
|
|
4663
|
+
width: boxW,
|
|
4664
|
+
height: Math.max(1, boxBot - boxTop),
|
|
4665
|
+
rx: "2",
|
|
4666
|
+
...hoverProps,
|
|
4667
|
+
onClick: () => clickBox(d, i)
|
|
4668
|
+
}
|
|
4669
|
+
),
|
|
4670
|
+
/* @__PURE__ */ jsx("line", { className: "twc-boxplot__median", style: { stroke: color }, x1: x, y1: yMed, x2: x + boxW, y2: yMed }),
|
|
4671
|
+
(Array.isArray(d.outliers) ? d.outliers : []).map(
|
|
4672
|
+
(o, oi) => isFinite(Number(o)) ? /* @__PURE__ */ jsx("circle", { className: "twc-boxplot__outlier", style: { stroke: color }, cx, cy: vPos(o), r: "2.5", ...hoverProps }, oi) : null
|
|
4673
|
+
)
|
|
4674
|
+
] }, i);
|
|
4675
|
+
}),
|
|
4676
|
+
showAxis ? rows.map((d, i) => /* @__PURE__ */ jsx("text", { className: "twc-boxplot__axis", x: bandX(i) + band / 2, y: H - 8, textAnchor: "middle", children: d.label }, i)) : null,
|
|
4677
|
+
dragBand ? (() => {
|
|
4678
|
+
const lo = Math.min(drag.start, drag.end), hi = Math.max(drag.start, drag.end);
|
|
4679
|
+
const x0 = bandX(lo), x1 = bandX(hi + 1);
|
|
4680
|
+
return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
|
|
4681
|
+
})() : null,
|
|
4682
|
+
zoomable ? /* @__PURE__ */ jsx(
|
|
3489
4683
|
"rect",
|
|
3490
4684
|
{
|
|
3491
|
-
className: "twc-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
4685
|
+
className: "twc-chart__overlay",
|
|
4686
|
+
"data-zoom": "true",
|
|
4687
|
+
"data-clickable": clickable || void 0,
|
|
4688
|
+
"data-panning": drag && drag.pan || void 0,
|
|
4689
|
+
x: padL,
|
|
4690
|
+
y: padT,
|
|
4691
|
+
width: innerW,
|
|
4692
|
+
height: innerH,
|
|
4693
|
+
onMouseMove: onOverlayMove,
|
|
4694
|
+
onMouseLeave: () => {
|
|
4695
|
+
hide();
|
|
4696
|
+
setDrag(null);
|
|
4697
|
+
},
|
|
4698
|
+
onMouseDown: onOverlayDown,
|
|
4699
|
+
onMouseUp: onOverlayUp,
|
|
4700
|
+
onClick: onOverlayClick
|
|
3499
4701
|
}
|
|
3500
|
-
)
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
4702
|
+
) : null
|
|
4703
|
+
] }),
|
|
4704
|
+
zoomable && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
|
|
4705
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4706
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
4707
|
+
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
4708
|
+
] }),
|
|
4709
|
+
"Reset"
|
|
4710
|
+
] }) : null,
|
|
4711
|
+
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
4712
|
+
tableFallback ? /* @__PURE__ */ jsx(
|
|
4713
|
+
ChartTable,
|
|
4714
|
+
{
|
|
4715
|
+
id: tableId,
|
|
4716
|
+
caption: caption ?? svgAriaLabel,
|
|
4717
|
+
columns: ["min", "q1", "median", "q3", "max"],
|
|
4718
|
+
rows: allRows.map((d) => ({
|
|
4719
|
+
label: d.label,
|
|
4720
|
+
values: [d.min, d.q1, d.median, d.q3, d.max].map((v) => fmt(Number(v)))
|
|
4721
|
+
}))
|
|
4722
|
+
}
|
|
4723
|
+
) : null
|
|
4724
|
+
]
|
|
4725
|
+
}
|
|
4726
|
+
);
|
|
3523
4727
|
}
|
|
3524
4728
|
function labelText6(label) {
|
|
3525
4729
|
return typeof label === "string" || typeof label === "number" ? String(label) : "";
|
|
@@ -3539,8 +4743,10 @@ function RangeChart({
|
|
|
3539
4743
|
height = 300,
|
|
3540
4744
|
showGrid = true,
|
|
3541
4745
|
showAxis = true,
|
|
4746
|
+
zoomable = false,
|
|
3542
4747
|
colors,
|
|
3543
4748
|
valueFormat,
|
|
4749
|
+
onDataClick,
|
|
3544
4750
|
ariaLabel,
|
|
3545
4751
|
"aria-label": ariaLabelProp,
|
|
3546
4752
|
tableFallback = true,
|
|
@@ -3553,10 +4759,17 @@ function RangeChart({
|
|
|
3553
4759
|
const uid = React51__default.useId();
|
|
3554
4760
|
const tableId = tableFallback ? `${uid}-table` : void 0;
|
|
3555
4761
|
const { containerRef, tip, show, hide } = useChartTooltip();
|
|
4762
|
+
const [selected, setSelected] = React51__default.useState(null);
|
|
4763
|
+
const [zoom, setZoom] = React51__default.useState(null);
|
|
4764
|
+
const [drag, setDrag] = React51__default.useState(null);
|
|
4765
|
+
const svgRef = React51__default.useRef(null);
|
|
3556
4766
|
const isArea = type === "area";
|
|
3557
|
-
const
|
|
3558
|
-
const n = rows.length;
|
|
4767
|
+
const clickable = !!onDataClick;
|
|
3559
4768
|
const fmt = valueFormat || fmtNumber;
|
|
4769
|
+
const allRows = data || [];
|
|
4770
|
+
const baseIdx = zoom ? zoom.s : 0;
|
|
4771
|
+
const rows = zoom ? allRows.slice(zoom.s, zoom.e + 1) : allRows;
|
|
4772
|
+
const n = rows.length;
|
|
3560
4773
|
const W = 600, H = height;
|
|
3561
4774
|
const padL = showAxis ? isArea ? 44 : 84 : 8;
|
|
3562
4775
|
const padR = 12;
|
|
@@ -3572,6 +4785,97 @@ function RangeChart({
|
|
|
3572
4785
|
const svgAriaLabel = ariaLabelProp ?? ariaLabel ?? `range ${type} chart`;
|
|
3573
4786
|
const xPos = (v) => padL + frac(v) * innerW;
|
|
3574
4787
|
const catBandH = innerH / Math.max(1, n);
|
|
4788
|
+
const lineX = (i) => n > 1 ? padL + innerW / (n - 1) * i : padL + innerW / 2;
|
|
4789
|
+
const yPos = (v) => padT + innerH - frac(v) * innerH;
|
|
4790
|
+
const colorAt = (d, i) => isArea ? paletteAt(colors, 0) : d.color || paletteAt(colors, i);
|
|
4791
|
+
const rangeVal = (d) => `${fmt(Number(d.min) || 0)} \u2013 ${fmt(Number(d.max) || 0)}`;
|
|
4792
|
+
const tipFor = (d, i) => ({ title: labelText7(d.label), items: [{ color: colorAt(d, i), label: "", value: rangeVal(d) }] });
|
|
4793
|
+
const clickDatum = (d, localIdx) => {
|
|
4794
|
+
if (!onDataClick) return;
|
|
4795
|
+
onDataClick({ label: d.label, min: Number(d.min) || 0, max: Number(d.max) || 0, index: baseIdx + localIdx });
|
|
4796
|
+
};
|
|
4797
|
+
const selectMark = (key) => setSelected((s) => s === key ? null : key);
|
|
4798
|
+
const clickAt = (localIdx) => {
|
|
4799
|
+
clickDatum(rows[localIdx], localIdx);
|
|
4800
|
+
selectMark(baseIdx + localIdx);
|
|
4801
|
+
};
|
|
4802
|
+
const applyZoom = (s, e) => {
|
|
4803
|
+
const nAll = allRows.length;
|
|
4804
|
+
if (e - s >= nAll - 1) {
|
|
4805
|
+
setZoom(null);
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
setZoom({ s: Math.max(0, s), e: Math.min(nAll - 1, e) });
|
|
4809
|
+
};
|
|
4810
|
+
const fracFromEvent = (e) => {
|
|
4811
|
+
const svg = svgRef.current;
|
|
4812
|
+
if (!svg) return null;
|
|
4813
|
+
const rect = svg.getBoundingClientRect();
|
|
4814
|
+
if (isArea) {
|
|
4815
|
+
if (!(rect.width > 0)) return null;
|
|
4816
|
+
return Math.min(1, Math.max(0, ((e.clientX - rect.left) / rect.width * W - padL) / innerW));
|
|
4817
|
+
}
|
|
4818
|
+
if (!(rect.height > 0)) return null;
|
|
4819
|
+
return Math.min(1, Math.max(0, ((e.clientY - rect.top) / rect.height * H - padT) / innerH));
|
|
4820
|
+
};
|
|
4821
|
+
const catFromEvent = (e) => {
|
|
4822
|
+
if (!rows.length) return null;
|
|
4823
|
+
const f = fracFromEvent(e);
|
|
4824
|
+
if (f == null) return null;
|
|
4825
|
+
const idx = isArea ? Math.round(f * Math.max(1, rows.length - 1)) : Math.floor(f * rows.length);
|
|
4826
|
+
return Math.min(rows.length - 1, Math.max(0, idx));
|
|
4827
|
+
};
|
|
4828
|
+
const onOverlayMove = (e) => {
|
|
4829
|
+
const idx = catFromEvent(e);
|
|
4830
|
+
if (idx == null) return;
|
|
4831
|
+
show(tipFor(rows[idx], idx), e);
|
|
4832
|
+
if (drag) {
|
|
4833
|
+
if (drag.pan) {
|
|
4834
|
+
const spanD = drag.z0.e - drag.z0.s;
|
|
4835
|
+
const s = Math.min(allRows.length - 1 - spanD, Math.max(0, drag.z0.s + (drag.start - idx)));
|
|
4836
|
+
applyZoom(s, s + spanD);
|
|
4837
|
+
} else {
|
|
4838
|
+
setDrag({ ...drag, end: idx });
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
};
|
|
4842
|
+
const onOverlayDown = (e) => {
|
|
4843
|
+
const idx = catFromEvent(e);
|
|
4844
|
+
if (idx == null) return;
|
|
4845
|
+
setDrag({ start: idx, end: idx, pan: e.shiftKey, z0: zoom || { s: 0, e: allRows.length - 1 } });
|
|
4846
|
+
};
|
|
4847
|
+
const onOverlayUp = () => {
|
|
4848
|
+
if (drag && !drag.pan) {
|
|
4849
|
+
const loI = Math.min(drag.start, drag.end), hiI = Math.max(drag.start, drag.end);
|
|
4850
|
+
if (hiI - loI >= 1) applyZoom(baseIdx + loI, baseIdx + hiI);
|
|
4851
|
+
}
|
|
4852
|
+
setDrag(null);
|
|
4853
|
+
};
|
|
4854
|
+
const onOverlayClick = (e) => {
|
|
4855
|
+
if (!onDataClick) return;
|
|
4856
|
+
const idx = catFromEvent(e);
|
|
4857
|
+
if (idx == null) return;
|
|
4858
|
+
clickAt(idx);
|
|
4859
|
+
};
|
|
4860
|
+
React51__default.useEffect(() => {
|
|
4861
|
+
if (!zoomable) return void 0;
|
|
4862
|
+
const svg = svgRef.current;
|
|
4863
|
+
if (!svg) return void 0;
|
|
4864
|
+
const onWheel = (e) => {
|
|
4865
|
+
e.preventDefault();
|
|
4866
|
+
const nAll = allRows.length;
|
|
4867
|
+
const cur = zoom || { s: 0, e: nAll - 1 };
|
|
4868
|
+
const spanC = cur.e - cur.s;
|
|
4869
|
+
const f = fracFromEvent(e);
|
|
4870
|
+
const frc = f == null ? 0.5 : f;
|
|
4871
|
+
const center = cur.s + Math.round(frc * spanC);
|
|
4872
|
+
const newSpan = Math.min(nAll - 1, Math.max(1, Math.round(spanC * (e.deltaY < 0 ? 0.8 : 1.25))));
|
|
4873
|
+
const s = Math.min(nAll - 1 - newSpan, Math.max(0, center - Math.round(newSpan * frc)));
|
|
4874
|
+
applyZoom(s, s + newSpan);
|
|
4875
|
+
};
|
|
4876
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
4877
|
+
return () => svg.removeEventListener("wheel", onWheel);
|
|
4878
|
+
}, [zoomable, zoom, allRows.length]);
|
|
3575
4879
|
const renderBars = () => {
|
|
3576
4880
|
const gap = 0.34;
|
|
3577
4881
|
const slot = catBandH * (1 - gap);
|
|
@@ -3580,27 +4884,29 @@ function RangeChart({
|
|
|
3580
4884
|
const x0 = xPos(Math.min(min, max));
|
|
3581
4885
|
const x1 = xPos(Math.max(min, max));
|
|
3582
4886
|
const y = padT + catBandH * i + catBandH * gap / 2;
|
|
3583
|
-
const color = d
|
|
4887
|
+
const color = colorAt(d, i);
|
|
4888
|
+
const key = baseIdx + i;
|
|
3584
4889
|
return /* @__PURE__ */ jsx(
|
|
3585
4890
|
"rect",
|
|
3586
4891
|
{
|
|
3587
4892
|
className: "twc-rangechart__bar twc-chart__anim-bar",
|
|
4893
|
+
"data-mark": true,
|
|
3588
4894
|
"data-horizontal": "true",
|
|
4895
|
+
"data-selected": selected === key || void 0,
|
|
3589
4896
|
style: { fill: color },
|
|
3590
4897
|
x: x0,
|
|
3591
4898
|
y,
|
|
3592
4899
|
width: Math.max(1, x1 - x0),
|
|
3593
4900
|
height: Math.max(1, slot),
|
|
3594
4901
|
rx: "3",
|
|
3595
|
-
onMouseMove: (e) => show(
|
|
3596
|
-
onMouseLeave: hide
|
|
4902
|
+
onMouseMove: (e) => show(tipFor(d, i), e),
|
|
4903
|
+
onMouseLeave: hide,
|
|
4904
|
+
onClick: () => clickAt(i)
|
|
3597
4905
|
},
|
|
3598
4906
|
i
|
|
3599
4907
|
);
|
|
3600
4908
|
});
|
|
3601
4909
|
};
|
|
3602
|
-
const lineX = (i) => n > 1 ? padL + innerW / (n - 1) * i : padL + innerW / 2;
|
|
3603
|
-
const yPos = (v) => padT + innerH - frac(v) * innerH;
|
|
3604
4910
|
const renderBand = () => {
|
|
3605
4911
|
const maxPts = rows.map((d, i) => [lineX(i), yPos(Number(d.max) || 0)]);
|
|
3606
4912
|
const minPts = rows.map((d, i) => [lineX(i), yPos(Number(d.min) || 0)]);
|
|
@@ -3615,44 +4921,126 @@ function RangeChart({
|
|
|
3615
4921
|
rows.map((d, i) => /* @__PURE__ */ jsx(
|
|
3616
4922
|
"rect",
|
|
3617
4923
|
{
|
|
4924
|
+
"data-mark": true,
|
|
4925
|
+
"data-selected": selected === baseIdx + i || void 0,
|
|
3618
4926
|
fill: "transparent",
|
|
3619
4927
|
x: lineX(i) - hitW / 2,
|
|
3620
4928
|
y: padT,
|
|
3621
4929
|
width: hitW,
|
|
3622
4930
|
height: innerH,
|
|
3623
|
-
onMouseMove: (e) => show(
|
|
3624
|
-
onMouseLeave: hide
|
|
4931
|
+
onMouseMove: (e) => show(tipFor(d, i), e),
|
|
4932
|
+
onMouseLeave: hide,
|
|
4933
|
+
onClick: () => clickAt(i)
|
|
3625
4934
|
},
|
|
3626
4935
|
i
|
|
3627
4936
|
))
|
|
3628
4937
|
] });
|
|
3629
4938
|
};
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
/* @__PURE__ */ jsx(
|
|
3646
|
-
|
|
3647
|
-
ChartTable,
|
|
4939
|
+
const renderZones = () => rows.map((d, i) => {
|
|
4940
|
+
let x, y, w, hgt;
|
|
4941
|
+
if (isArea) {
|
|
4942
|
+
const x0 = i === 0 ? padL : (lineX(i - 1) + lineX(i)) / 2;
|
|
4943
|
+
const x1 = i === rows.length - 1 ? padL + innerW : (lineX(i) + lineX(i + 1)) / 2;
|
|
4944
|
+
x = x0;
|
|
4945
|
+
w = x1 - x0;
|
|
4946
|
+
y = padT;
|
|
4947
|
+
hgt = innerH;
|
|
4948
|
+
} else {
|
|
4949
|
+
x = padL;
|
|
4950
|
+
w = innerW;
|
|
4951
|
+
y = padT + catBandH * i;
|
|
4952
|
+
hgt = catBandH;
|
|
4953
|
+
}
|
|
4954
|
+
return /* @__PURE__ */ jsx(
|
|
4955
|
+
"rect",
|
|
3648
4956
|
{
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
4957
|
+
className: "twc-chart__zone",
|
|
4958
|
+
x,
|
|
4959
|
+
y,
|
|
4960
|
+
width: Math.max(0, w),
|
|
4961
|
+
height: Math.max(0, hgt),
|
|
4962
|
+
"data-clickable": onDataClick ? true : void 0,
|
|
4963
|
+
onMouseMove: (e) => show(tipFor(d, i), e),
|
|
4964
|
+
onMouseLeave: hide,
|
|
4965
|
+
onClick: () => clickDatum(d, i)
|
|
4966
|
+
},
|
|
4967
|
+
i
|
|
4968
|
+
);
|
|
4969
|
+
});
|
|
4970
|
+
const dragBand = zoomable && drag && !drag.pan && drag.end !== drag.start;
|
|
4971
|
+
return /* @__PURE__ */ jsxs(
|
|
4972
|
+
"div",
|
|
4973
|
+
{
|
|
4974
|
+
ref: containerRef,
|
|
4975
|
+
className: `twc-chart twc-chart--range ${className}`.trim(),
|
|
4976
|
+
"data-type": type,
|
|
4977
|
+
"data-clickable": clickable || void 0,
|
|
4978
|
+
"data-has-selection": selected != null || void 0,
|
|
4979
|
+
...rest,
|
|
4980
|
+
children: [
|
|
4981
|
+
baseStyles,
|
|
4982
|
+
styles,
|
|
4983
|
+
/* @__PURE__ */ jsxs("svg", { ref: svgRef, viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": svgAriaLabel, "aria-describedby": tableId, preserveAspectRatio: "none", children: [
|
|
4984
|
+
showGrid ? scale.ticks.map(
|
|
4985
|
+
(t, i) => isArea ? /* @__PURE__ */ jsx("line", { className: "twc-rangechart__grid", x1: padL, y1: yPos(t), x2: W - padR, y2: yPos(t) }, i) : /* @__PURE__ */ jsx("line", { className: "twc-rangechart__grid", x1: xPos(t), y1: padT, x2: xPos(t), y2: padT + innerH }, i)
|
|
4986
|
+
) : null,
|
|
4987
|
+
showAxis ? scale.ticks.map(
|
|
4988
|
+
(t, i) => isArea ? /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: padL - 8, y: yPos(t) + 4, textAnchor: "end", children: shortNum(t) }, i) : /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: xPos(t), y: H - 8, textAnchor: "middle", children: shortNum(t) }, i)
|
|
4989
|
+
) : null,
|
|
4990
|
+
!zoomable ? renderZones() : null,
|
|
4991
|
+
isArea ? renderBand() : renderBars(),
|
|
4992
|
+
showAxis ? rows.map(
|
|
4993
|
+
(d, i) => isArea ? /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: lineX(i), y: H - 8, textAnchor: "middle", children: d.label }, i) : /* @__PURE__ */ jsx("text", { className: "twc-rangechart__axis", x: padL - 8, y: padT + catBandH * i + catBandH / 2 + 4, textAnchor: "end", children: d.label }, i)
|
|
4994
|
+
) : null,
|
|
4995
|
+
dragBand ? (() => {
|
|
4996
|
+
const loI = Math.min(drag.start, drag.end), hiI = Math.max(drag.start, drag.end);
|
|
4997
|
+
if (isArea) {
|
|
4998
|
+
const x0 = lineX(loI), x1 = lineX(hiI);
|
|
4999
|
+
return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: x0, y: padT, width: Math.max(0, x1 - x0), height: innerH });
|
|
5000
|
+
}
|
|
5001
|
+
const y0 = padT + catBandH * loI, y1 = padT + catBandH * (hiI + 1);
|
|
5002
|
+
return /* @__PURE__ */ jsx("rect", { className: "twc-chart__zoomband", x: padL, y: y0, width: innerW, height: Math.max(0, y1 - y0) });
|
|
5003
|
+
})() : null,
|
|
5004
|
+
zoomable ? /* @__PURE__ */ jsx(
|
|
5005
|
+
"rect",
|
|
5006
|
+
{
|
|
5007
|
+
className: "twc-chart__overlay",
|
|
5008
|
+
"data-clickable": clickable || void 0,
|
|
5009
|
+
x: padL,
|
|
5010
|
+
y: padT,
|
|
5011
|
+
width: innerW,
|
|
5012
|
+
height: innerH,
|
|
5013
|
+
onMouseMove: onOverlayMove,
|
|
5014
|
+
onMouseLeave: () => {
|
|
5015
|
+
hide();
|
|
5016
|
+
setDrag(null);
|
|
5017
|
+
},
|
|
5018
|
+
onMouseDown: onOverlayDown,
|
|
5019
|
+
onMouseUp: onOverlayUp,
|
|
5020
|
+
onClick: onOverlayClick
|
|
5021
|
+
}
|
|
5022
|
+
) : null
|
|
5023
|
+
] }),
|
|
5024
|
+
zoomable && zoom ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-chart__zoom-reset", onClick: () => setZoom(null), "aria-label": "Reset zoom", children: [
|
|
5025
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5026
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
5027
|
+
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
5028
|
+
] }),
|
|
5029
|
+
"Reset"
|
|
5030
|
+
] }) : null,
|
|
5031
|
+
/* @__PURE__ */ jsx(ChartTooltip, { tip }),
|
|
5032
|
+
tableFallback ? /* @__PURE__ */ jsx(
|
|
5033
|
+
ChartTable,
|
|
5034
|
+
{
|
|
5035
|
+
id: tableId,
|
|
5036
|
+
caption: caption ?? svgAriaLabel,
|
|
5037
|
+
columns: ["min", "max"],
|
|
5038
|
+
rows: allRows.map((d) => ({ label: d.label, values: [fmt(Number(d.min) || 0), fmt(Number(d.max) || 0)] }))
|
|
5039
|
+
}
|
|
5040
|
+
) : null
|
|
5041
|
+
]
|
|
5042
|
+
}
|
|
5043
|
+
);
|
|
3656
5044
|
}
|
|
3657
5045
|
function labelText7(label) {
|
|
3658
5046
|
return typeof label === "string" || typeof label === "number" ? String(label) : "";
|
|
@@ -7352,6 +8740,7 @@ function Datatable({
|
|
|
7352
8740
|
serverMode = false,
|
|
7353
8741
|
rowCount,
|
|
7354
8742
|
onServerChange,
|
|
8743
|
+
onColumnVisibilityChange,
|
|
7355
8744
|
batchActions = [],
|
|
7356
8745
|
showExport = false,
|
|
7357
8746
|
showDensity = false,
|
|
@@ -7658,6 +9047,19 @@ function Datatable({
|
|
|
7658
9047
|
}, [exportOpen]);
|
|
7659
9048
|
const colByField = React51__default.useMemo(() => Object.fromEntries(cols.map((c) => [c.field, c])), [cols]);
|
|
7660
9049
|
const visibleCols = cols.filter((c) => !hidden.has(c.field));
|
|
9050
|
+
const colFieldsKey = JSON.stringify(
|
|
9051
|
+
cols.filter((c) => typeof c.field === "string" && c.type !== "actions").map((c) => c.field)
|
|
9052
|
+
);
|
|
9053
|
+
const colFields = React51__default.useMemo(() => JSON.parse(colFieldsKey), [colFieldsKey]);
|
|
9054
|
+
const hiddenKey = JSON.stringify([...hidden].filter((f) => typeof f === "string").sort());
|
|
9055
|
+
const visibleColumns = React51__default.useMemo(() => {
|
|
9056
|
+
const h = new Set(JSON.parse(hiddenKey));
|
|
9057
|
+
return colFields.filter((f) => !h.has(f));
|
|
9058
|
+
}, [colFields, hiddenKey]);
|
|
9059
|
+
const hiddenColumns = React51__default.useMemo(() => {
|
|
9060
|
+
const h = new Set(JSON.parse(hiddenKey));
|
|
9061
|
+
return colFields.filter((f) => h.has(f));
|
|
9062
|
+
}, [colFields, hiddenKey]);
|
|
7661
9063
|
const ordered = React51__default.useMemo(() => {
|
|
7662
9064
|
const orderIdx = (f) => {
|
|
7663
9065
|
const i = order.indexOf(f);
|
|
@@ -8000,11 +9402,27 @@ function Datatable({
|
|
|
8000
9402
|
pageSize: sizeVal,
|
|
8001
9403
|
sort,
|
|
8002
9404
|
filters: filters.map(({ id, ...f }) => f),
|
|
8003
|
-
quickFilter: quick.trim()
|
|
9405
|
+
quickFilter: quick.trim(),
|
|
9406
|
+
visibleColumns,
|
|
9407
|
+
hiddenColumns
|
|
9408
|
+
// #191: server can project only the visible columns
|
|
8004
9409
|
});
|
|
8005
9410
|
}, 250);
|
|
8006
9411
|
return () => clearTimeout(t);
|
|
8007
|
-
}, [serverMode, pageVal, sizeVal, sort, filters, quick]);
|
|
9412
|
+
}, [serverMode, pageVal, sizeVal, sort, filters, quick, visibleColumns, hiddenColumns]);
|
|
9413
|
+
const onColVisRef = React51__default.useRef(onColumnVisibilityChange);
|
|
9414
|
+
onColVisRef.current = onColumnVisibilityChange;
|
|
9415
|
+
const lastVisibleKey = React51__default.useRef(null);
|
|
9416
|
+
React51__default.useEffect(() => {
|
|
9417
|
+
const key = JSON.stringify(visibleColumns);
|
|
9418
|
+
if (lastVisibleKey.current === null) {
|
|
9419
|
+
lastVisibleKey.current = key;
|
|
9420
|
+
return;
|
|
9421
|
+
}
|
|
9422
|
+
if (lastVisibleKey.current === key) return;
|
|
9423
|
+
lastVisibleKey.current = key;
|
|
9424
|
+
onColVisRef.current?.(visibleColumns);
|
|
9425
|
+
}, [visibleColumns]);
|
|
8008
9426
|
function cycleSort(field) {
|
|
8009
9427
|
setSort((s) => !s || s.field !== field ? { field, dir: "asc" } : s.dir === "asc" ? { field, dir: "desc" } : null);
|
|
8010
9428
|
}
|
|
@@ -14027,6 +15445,12 @@ var SHELL_CSS = `
|
|
|
14027
15445
|
min-height: 60px; padding-inline: var(--space-5); border-bottom: var(--border-thin) solid var(--color-border); background: var(--color-surface); }
|
|
14028
15446
|
.twc-shell__content { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
14029
15447
|
.twc-shell__content[data-padded="true"] { padding: var(--space-6) var(--space-5); }
|
|
15448
|
+
/* #190: <main> is the skip-link target (tabindex="-1") \u2014 a programmatic focus target,
|
|
15449
|
+
not tab-reachable and not an interactive control. Clicking content mouse-focuses it,
|
|
15450
|
+
then the first keypress flips to keyboard modality and :focus-visible would draw a UA
|
|
15451
|
+
outline around the ENTIRE content area. Suppress it \u2014 the skip LINK keeps its own
|
|
15452
|
+
visible ring (.twc-shell__skip:focus-visible) and SR focus announcement is unaffected. */
|
|
15453
|
+
.twc-shell__content:focus, .twc-shell__content:focus-visible { outline: none; }
|
|
14030
15454
|
/* Mobile: stack the shell so the sidebar (rendered as an off-canvas overlay) no
|
|
14031
15455
|
longer reserves a column \u2014 the main content spans the full width. */
|
|
14032
15456
|
@media (max-width: 720px) {
|
|
@@ -14161,8 +15585,10 @@ var Container = React51__default.forwardRef(function Container2({
|
|
|
14161
15585
|
style: {
|
|
14162
15586
|
width: "100%",
|
|
14163
15587
|
maxWidth: typeof max === "number" ? `${max}px` : max,
|
|
14164
|
-
|
|
14165
|
-
|
|
15588
|
+
marginInlineStart: "auto",
|
|
15589
|
+
marginInlineEnd: "auto",
|
|
15590
|
+
paddingInlineStart: padded ? "var(--space-5)" : void 0,
|
|
15591
|
+
paddingInlineEnd: padded ? "var(--space-5)" : void 0,
|
|
14166
15592
|
...style,
|
|
14167
15593
|
...flatStyle
|
|
14168
15594
|
},
|
|
@@ -14412,7 +15838,10 @@ var Heading = React51__default.forwardRef(function Heading2({
|
|
|
14412
15838
|
className: `twc-heading ${className}`.trim(),
|
|
14413
15839
|
"data-twc-sx": sxAttr,
|
|
14414
15840
|
style: {
|
|
14415
|
-
|
|
15841
|
+
marginTop: 0,
|
|
15842
|
+
marginRight: 0,
|
|
15843
|
+
marginBottom: 0,
|
|
15844
|
+
marginLeft: 0,
|
|
14416
15845
|
fontFamily: "var(--font-sans)",
|
|
14417
15846
|
fontWeight: "var(--font-bold)",
|
|
14418
15847
|
color: "var(--color-text)",
|
|
@@ -14477,7 +15906,10 @@ var Text = React51__default.forwardRef(function Text2({
|
|
|
14477
15906
|
className: `twc-text ${className}`.trim(),
|
|
14478
15907
|
"data-twc-sx": sxAttr,
|
|
14479
15908
|
style: {
|
|
14480
|
-
|
|
15909
|
+
marginTop: 0,
|
|
15910
|
+
marginRight: 0,
|
|
15911
|
+
marginBottom: 0,
|
|
15912
|
+
marginLeft: 0,
|
|
14481
15913
|
fontFamily: "var(--font-sans)",
|
|
14482
15914
|
lineHeight: 1.6,
|
|
14483
15915
|
fontSize: `var(--text-${size})`,
|