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