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