@flood-md/react 0.2.12 → 0.2.13
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/README.md +1 -1
- package/dist/box-plot.d.ts +2 -0
- package/dist/box-plot.js +148 -0
- package/dist/bullet-chart.d.ts +2 -0
- package/dist/bullet-chart.js +27 -0
- package/dist/charts.d.ts +6 -0
- package/dist/charts.js +561 -0
- package/dist/collection-toolbar.js +2 -2
- package/dist/data-display.js +1 -1
- package/dist/dumbbell-chart.d.ts +2 -0
- package/dist/dumbbell-chart.js +80 -0
- package/dist/feedback-actions.js +18 -3
- package/dist/foundations.css +1616 -199
- package/dist/histogram.d.ts +2 -0
- package/dist/histogram.js +78 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +46 -2
- package/dist/menu.d.ts +1 -0
- package/dist/menu.js +6 -6
- package/dist/overlay-runtime.d.ts +1 -1
- package/dist/overlay-runtime.js +25 -4
- package/dist/overlays.js +4 -2
- package/dist/range-chart.d.ts +2 -0
- package/dist/range-chart.js +97 -0
- package/dist/scatter-chart.d.ts +2 -0
- package/dist/scatter-chart.js +170 -0
- package/dist/waterfall-chart.d.ts +2 -0
- package/dist/waterfall-chart.js +89 -0
- package/package.json +2 -2
package/dist/charts.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Inline, Meter, Metric, Surface, Table, Text, VisuallyHidden } from "./index.js";
|
|
4
|
+
import { placeOverlay } from "./overlay-runtime.js";
|
|
5
|
+
const defaultNumber = new Intl.NumberFormat("en", { maximumSignificantDigits: 21 });
|
|
6
|
+
const formatNumber = (value) => defaultNumber.format(value);
|
|
7
|
+
const observed = (value) => value !== null && Number.isFinite(value);
|
|
8
|
+
const useChartLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
9
|
+
function ChartKey() {
|
|
10
|
+
return _jsx("svg", { className: "flood-line-chart__key", viewBox: "0 0 24 4", "aria-hidden": "true", children: _jsx("path", { className: "flood-line-chart__stroke", d: "M1 2H23" }) });
|
|
11
|
+
}
|
|
12
|
+
function ChartReadout({ series, values }) {
|
|
13
|
+
return _jsx(_Fragment, { children: series.map((item, i) => _jsxs("div", { className: "flood-line-chart__tooltip-row", "data-series": i, children: [_jsx(ChartKey, {}), _jsx(Text, { size: "helper", tone: "subtle", children: item.label }), _jsx(Text, { size: "helper", children: values[i] })] }, item.id)) });
|
|
14
|
+
}
|
|
15
|
+
function ChartLegend({ series, swatch = false }) {
|
|
16
|
+
return _jsx("div", { className: "flood-line-chart__readout", "aria-hidden": "true", children: _jsx(Inline, { gap: "4", children: series.map((item, i) => _jsxs(Inline, { gap: "2", align: "center", "data-series": i, children: [swatch ? _jsx("span", { className: "flood-donut-chart__key" }) : _jsx(ChartKey, {}), _jsx(Text, { size: "compact", tone: "subtle", children: item.label })] }, item.id)) }) });
|
|
17
|
+
}
|
|
18
|
+
const plotWidth = 600;
|
|
19
|
+
const plotHeight = 200;
|
|
20
|
+
const xFor = (index, length) => length === 1 ? plotWidth / 2 : index * plotWidth / (length - 1);
|
|
21
|
+
function slope(points, index, y, fallback) {
|
|
22
|
+
const previous = points[index - 1], next = points[index + 1];
|
|
23
|
+
if (previous == null || next == null)
|
|
24
|
+
return fallback;
|
|
25
|
+
const a = y - previous, b = next - y;
|
|
26
|
+
return a * b > 0 ? 2 * a * b / (a + b) : 0;
|
|
27
|
+
}
|
|
28
|
+
function yAt(points, position, smooth) {
|
|
29
|
+
const i = Math.floor(position);
|
|
30
|
+
const start = points[i], end = points[i + 1];
|
|
31
|
+
const t = position - i;
|
|
32
|
+
if (start == null || t === 0)
|
|
33
|
+
return start;
|
|
34
|
+
if (end == null)
|
|
35
|
+
return null;
|
|
36
|
+
const delta = end - start;
|
|
37
|
+
if (!smooth)
|
|
38
|
+
return start + delta * t;
|
|
39
|
+
const u = 1 - t;
|
|
40
|
+
return u ** 3 * start + 3 * u * u * t * (start + slope(points, i, start, delta) / 3)
|
|
41
|
+
+ 3 * u * t * t * (end - slope(points, i + 1, end, delta) / 3) + t ** 3 * end;
|
|
42
|
+
}
|
|
43
|
+
function drawSeries(values, min, max, smooth) {
|
|
44
|
+
const line = [], area = [], singles = [];
|
|
45
|
+
const points = values.map(value => {
|
|
46
|
+
if (!observed(value))
|
|
47
|
+
return null;
|
|
48
|
+
const fraction = min === max ? .5 : Number.isFinite(max - min)
|
|
49
|
+
? (value - min) / (max - min)
|
|
50
|
+
: (value / 2 - min / 2) / (max / 2 - min / 2);
|
|
51
|
+
return (1 - fraction) * plotHeight;
|
|
52
|
+
});
|
|
53
|
+
let segmentStart = 0, startIndex = 0;
|
|
54
|
+
// The harmonic mean keeps control points within each observed interval.
|
|
55
|
+
points.forEach((y, index) => {
|
|
56
|
+
if (y === null)
|
|
57
|
+
return;
|
|
58
|
+
const x = xFor(index, values.length);
|
|
59
|
+
const starts = index === 0 || points[index - 1] === null;
|
|
60
|
+
if (starts) {
|
|
61
|
+
segmentStart = line.length;
|
|
62
|
+
startIndex = index;
|
|
63
|
+
}
|
|
64
|
+
const previous = points[index - 1];
|
|
65
|
+
if (previous != null && smooth) {
|
|
66
|
+
const startX = xFor(index - 1, values.length), step = (x - startX) / 3;
|
|
67
|
+
const delta = y - previous;
|
|
68
|
+
line.push(`C${startX + step} ${previous + slope(points, index - 1, previous, delta) / 3} ${x - step} ${y - slope(points, index, y, delta) / 3} ${x} ${y}`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
line.push(`${starts ? "M" : "L"}${x} ${y}`);
|
|
72
|
+
}
|
|
73
|
+
if (index + 1 < points.length && points[index + 1] !== null)
|
|
74
|
+
return;
|
|
75
|
+
if (starts)
|
|
76
|
+
singles.push(index);
|
|
77
|
+
else
|
|
78
|
+
area.push(`${line.slice(segmentStart).join(" ")} V${plotHeight} H${xFor(startIndex, values.length)} Z`);
|
|
79
|
+
});
|
|
80
|
+
return { line: line.join(" "), area: area.join(" "), singles, points };
|
|
81
|
+
}
|
|
82
|
+
export const SeriesPlot = /* @__PURE__ */ forwardRef(function SeriesPlot({ label, categories, series, domain, variant, curve, showYAxis, showLegend = false, formatValue = formatNumber, emptyLabel, missingLabel, navigationLabel, className, ...props }, ref) {
|
|
83
|
+
const id = useId();
|
|
84
|
+
const [cursor, setCursor] = useState(null);
|
|
85
|
+
const [hover, setHover] = useState(null);
|
|
86
|
+
const [focused, setFocused] = useState(false);
|
|
87
|
+
const [touchPoint, setTouchPoint] = useState(null);
|
|
88
|
+
const tipRef = useRef(null);
|
|
89
|
+
let low = Infinity, high = -Infinity;
|
|
90
|
+
if (series.length > 4 || new Set(series.map(item => item.id)).size !== series.length ||
|
|
91
|
+
series.some(item => !item.id.trim() || !item.label.trim() || item.values.length !== categories.length)) {
|
|
92
|
+
throw new Error("Flood LineChart needs up to four uniquely named series with one value per category");
|
|
93
|
+
}
|
|
94
|
+
for (const item of series)
|
|
95
|
+
for (const value of item.values) {
|
|
96
|
+
if (observed(value)) {
|
|
97
|
+
low = Math.min(low, value);
|
|
98
|
+
high = Math.max(high, value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (domain && (!Number.isFinite(domain[0]) || !Number.isFinite(domain[1]) ||
|
|
102
|
+
domain[0] >= domain[1] || low < domain[0] || high > domain[1])) {
|
|
103
|
+
throw new RangeError("Flood LineChart domain must contain all observations and have finite increasing bounds");
|
|
104
|
+
}
|
|
105
|
+
const hasData = low !== Infinity;
|
|
106
|
+
const [min, max] = domain ?? [low, high];
|
|
107
|
+
const last = categories.length - 1;
|
|
108
|
+
const canInspect = hasData && last > 0;
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const tip = tipRef.current;
|
|
111
|
+
if (!tip)
|
|
112
|
+
return;
|
|
113
|
+
const measure = () => tip.style.setProperty("--flood-chart-tip-height", `${tip.offsetHeight}px`);
|
|
114
|
+
measure();
|
|
115
|
+
if (typeof ResizeObserver === "undefined")
|
|
116
|
+
return;
|
|
117
|
+
const observer = new ResizeObserver(measure);
|
|
118
|
+
observer.observe(tip);
|
|
119
|
+
return () => observer.disconnect();
|
|
120
|
+
}, [canInspect]);
|
|
121
|
+
const active = Math.min(last, hover?.index ?? touchPoint ?? cursor ?? last);
|
|
122
|
+
const showPoint = last > 0 && (focused || hover?.inside || touchPoint !== null);
|
|
123
|
+
const values = hasData ? series.map(item => item.values[active]) : [];
|
|
124
|
+
const formatted = values.map(value => observed(value) ? formatValue(value) : missingLabel);
|
|
125
|
+
const smooth = curve === "smooth";
|
|
126
|
+
const plot = useMemo(() => hasData ? series.map(item => drawSeries(item.values, min, max, smooth)) : [], [series, min, max, smooth, hasData]);
|
|
127
|
+
const ticks = showYAxis ? (min === max ? [min] : [max, max / 2 + min / 2, min]) : [];
|
|
128
|
+
const categoryTicks = last >= 12
|
|
129
|
+
? [0, Math.round(last / 4), Math.round(last / 2), Math.round(3 * last / 4), last]
|
|
130
|
+
: [0, ...(last > 1 && last % 2 === 0 ? [last / 2] : []), last];
|
|
131
|
+
const pointX = xFor(active, categories.length);
|
|
132
|
+
const pointPercent = pointX / plotWidth * 100;
|
|
133
|
+
const sampleY = plot.find(item => item.points[active] != null)?.points[active] ?? plotHeight / 2;
|
|
134
|
+
const valueText = `${categories[active]}, ${series.map((item, i) => `${item.label}: ${formatted[i]}`).join(", ")}`;
|
|
135
|
+
const pointerPosition = (event) => {
|
|
136
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
137
|
+
return Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
|
|
138
|
+
};
|
|
139
|
+
const clearPointer = (node) => {
|
|
140
|
+
node.style.removeProperty("--flood-chart-pointer");
|
|
141
|
+
node.style.removeProperty("--flood-chart-pointer-ratio");
|
|
142
|
+
node.style.removeProperty("--flood-chart-pointer-tip-y");
|
|
143
|
+
node.style.removeProperty("--flood-chart-pointer-tip-ratio");
|
|
144
|
+
for (let i = 0; i < plot.length; i++) {
|
|
145
|
+
node.style.removeProperty(`--flood-chart-pointer-y-${i}`);
|
|
146
|
+
node.style.removeProperty(`--flood-chart-pointer-visible-${i}`);
|
|
147
|
+
}
|
|
148
|
+
setHover(null);
|
|
149
|
+
};
|
|
150
|
+
const trackPointer = (event) => {
|
|
151
|
+
if (event.pointerType === "touch" || last < 1)
|
|
152
|
+
return;
|
|
153
|
+
const ratio = pointerPosition(event), position = ratio * last;
|
|
154
|
+
const style = event.currentTarget.style;
|
|
155
|
+
style.setProperty("--flood-chart-pointer", `${ratio * 100}%`);
|
|
156
|
+
style.setProperty("--flood-chart-pointer-ratio", String(ratio));
|
|
157
|
+
let anchorY;
|
|
158
|
+
plot.forEach(({ points }, i) => {
|
|
159
|
+
const y = yAt(points, position, smooth);
|
|
160
|
+
anchorY ??= y ?? undefined;
|
|
161
|
+
style.setProperty(`--flood-chart-pointer-y-${i}`, `${(y ?? 0) / plotHeight * 100}%`);
|
|
162
|
+
style.setProperty(`--flood-chart-pointer-visible-${i}`, y == null ? "0" : "1");
|
|
163
|
+
});
|
|
164
|
+
const anchorRatio = (anchorY ?? plotHeight / 2) / plotHeight;
|
|
165
|
+
style.setProperty("--flood-chart-pointer-tip-y", `${anchorRatio * 100}%`);
|
|
166
|
+
style.setProperty("--flood-chart-pointer-tip-ratio", String(anchorRatio));
|
|
167
|
+
const index = Math.round(position);
|
|
168
|
+
setHover(previous => previous?.inside && previous.index === index ? previous : { index, inside: true });
|
|
169
|
+
};
|
|
170
|
+
const navigate = (event) => {
|
|
171
|
+
let next;
|
|
172
|
+
switch (event.key) {
|
|
173
|
+
case "ArrowRight":
|
|
174
|
+
case "ArrowUp":
|
|
175
|
+
next = active + 1;
|
|
176
|
+
break;
|
|
177
|
+
case "ArrowLeft":
|
|
178
|
+
case "ArrowDown":
|
|
179
|
+
next = active - 1;
|
|
180
|
+
break;
|
|
181
|
+
case "Home":
|
|
182
|
+
next = 0;
|
|
183
|
+
break;
|
|
184
|
+
case "End":
|
|
185
|
+
next = last;
|
|
186
|
+
break;
|
|
187
|
+
default: return;
|
|
188
|
+
}
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
clearPointer(event.currentTarget);
|
|
191
|
+
setTouchPoint(null);
|
|
192
|
+
setCursor(Math.min(last, Math.max(0, next)));
|
|
193
|
+
};
|
|
194
|
+
return _jsx("div", { ...props, ref: ref, className: ["flood-line-chart", className].filter(Boolean).join(" "), role: "group", "aria-label": label, children: hasData ? _jsxs(_Fragment, { children: [last > 0 && _jsx(VisuallyHidden, { id: id, children: navigationLabel }), _jsxs("div", { className: "flood-line-chart__body", "data-y-axis": showYAxis || undefined, children: [showYAxis && _jsx("div", { className: "flood-line-chart__y", "aria-hidden": "true", "data-flat": min === max || undefined, children: ticks.map((value, i) => _jsx(Text, { size: "helper", tone: "subtle", children: formatValue(value) }, i)) }), _jsxs("div", { className: "flood-line-chart__plot", role: last > 0 ? "slider" : "img", tabIndex: last > 0 ? 0 : undefined, "aria-label": last > 0 ? label : valueText, "aria-describedby": last > 0 ? id : undefined, "aria-valuemin": last > 0 ? 1 : undefined, "aria-valuemax": last > 0 ? last + 1 : undefined, "aria-valuenow": last > 0 ? active + 1 : undefined, "aria-valuetext": last > 0 ? valueText : undefined, "data-inspecting": showPoint || undefined, style: {
|
|
195
|
+
"--flood-chart-point": `${pointPercent}%`,
|
|
196
|
+
"--flood-chart-point-ratio": pointPercent / 100,
|
|
197
|
+
"--flood-chart-point-y": `${sampleY / plotHeight * 100}%`,
|
|
198
|
+
"--flood-chart-point-y-ratio": sampleY / plotHeight,
|
|
199
|
+
}, onFocus: event => {
|
|
200
|
+
if (!hover?.inside)
|
|
201
|
+
clearPointer(event.currentTarget);
|
|
202
|
+
setFocused(true);
|
|
203
|
+
}, onBlur: () => setFocused(false), onKeyDown: navigate, onPointerEnter: trackPointer, onPointerMove: trackPointer, onPointerLeave: event => {
|
|
204
|
+
if (focused || touchPoint !== null)
|
|
205
|
+
clearPointer(event.currentTarget);
|
|
206
|
+
else
|
|
207
|
+
setHover(previous => previous?.inside ? { ...previous, inside: false } : previous);
|
|
208
|
+
}, onPointerUp: event => {
|
|
209
|
+
if (event.pointerType !== "touch" || last < 1)
|
|
210
|
+
return;
|
|
211
|
+
clearPointer(event.currentTarget);
|
|
212
|
+
const point = Math.round(pointerPosition(event) * last);
|
|
213
|
+
setTouchPoint(current => current === point ? null : point);
|
|
214
|
+
}, children: [_jsxs("svg", { viewBox: `0 0 ${plotWidth} ${plotHeight}`, preserveAspectRatio: "none", "aria-hidden": "true", focusable: "false", children: [variant === "area" && _jsxs("defs", { children: [_jsxs("linearGradient", { className: "flood-line-chart__gradient", id: `${id}-fill`, x1: "0", x2: "0", y1: "0", y2: "1", children: [_jsx("stop", { stopColor: "currentColor", stopOpacity: ".3" }), _jsx("stop", { offset: "1", stopColor: "currentColor", stopOpacity: "0" })] }), _jsxs("linearGradient", { id: `${id}-area-edges`, children: [_jsx("stop", { stopColor: "white", stopOpacity: "0" }), _jsx("stop", { offset: ".07", stopColor: "white" }), _jsx("stop", { offset: ".93", stopColor: "white" }), _jsx("stop", { offset: "1", stopColor: "white", stopOpacity: "0" })] }), _jsx("mask", { id: `${id}-area-mask`, maskUnits: "userSpaceOnUse", x: "0", y: "0", width: plotWidth, height: plotHeight, children: _jsx("rect", { width: plotWidth, height: plotHeight, fill: `url(#${id}-area-edges)` }) })] }), _jsx("defs", { children: _jsxs("mask", { id: `${id}-highlight`, maskUnits: "userSpaceOnUse", x: "-4", y: "-4", width: plotWidth + 8, height: plotHeight + 8, children: [_jsxs("linearGradient", { id: `${id}-highlight-fade`, children: [_jsx("stop", { stopColor: "white", stopOpacity: "0" }), _jsx("stop", { offset: ".44", stopColor: "white" }), _jsx("stop", { offset: ".56", stopColor: "white" }), _jsx("stop", { offset: "1", stopColor: "white", stopOpacity: "0" })] }), _jsx("rect", { className: "flood-line-chart__highlight-window", x: "-54", y: "-4", width: "108", height: plotHeight + 8, fill: `url(#${id}-highlight-fade)` })] }) }), showYAxis && _jsx("path", { className: "flood-line-chart__grid", d: min === max ? "M0 100H600" : "M0 0H600M0 100H600M0 200H600" }), variant === "area" && _jsx("path", { className: "flood-line-chart__area", d: plot[0].area, fill: `url(#${id}-fill)`, mask: `url(#${id}-area-mask)` }), plot.map((item, i) => {
|
|
215
|
+
return _jsxs("g", { "data-series": i, children: [_jsx("path", { className: "flood-line-chart__stroke", d: item.line }), item.singles.map(index => _jsx("path", { className: "flood-line-chart__point", d: `M${xFor(index, categories.length)} ${item.points[index]}h0` }, index))] }, series[i].id);
|
|
216
|
+
}), _jsx("g", { className: "flood-line-chart__highlight", mask: `url(#${id}-highlight)`, children: plot.map((item, i) => _jsx("path", { className: "flood-line-chart__stroke", d: item.line, "data-series": i }, series[i].id)) })] }), last > 0 && _jsxs("div", { className: "flood-line-chart__inspection", "aria-hidden": "true", children: [_jsx("div", { className: "flood-line-chart__cursor", children: plot.map(({ points }, i) => _jsx("span", { className: "flood-line-chart__marker", "data-series": i, style: {
|
|
217
|
+
top: `var(--flood-chart-pointer-y-${i}, ${(points[active] ?? 0) / plotHeight * 100}%)`,
|
|
218
|
+
opacity: `var(--flood-chart-pointer-visible-${i}, ${points[active] == null ? 0 : 1})`,
|
|
219
|
+
} }, series[i].id)) }), _jsx("div", { className: "flood-line-chart__date", "aria-hidden": "true", children: _jsx(Text, { size: "helper", children: categories[active] }) }), _jsx("div", { className: "flood-line-chart__tooltip-position", children: _jsx(Surface, { ref: tipRef, className: "flood-line-chart__tooltip", padding: "2", elevation: "raised", children: _jsx(ChartReadout, { series: series, values: formatted }) }) })] })] }), _jsx("div", { className: "flood-line-chart__x", "aria-hidden": "true", children: last === 0 ? _jsx(Text, { size: "helper", tone: "subtle", children: categories[0] }) :
|
|
220
|
+
categoryTicks.map((index, tick) => (_jsx(Text, { size: "helper", tone: "subtle", "data-intermediate": categoryTicks.length === 5 && (tick === 1 || tick === 3) || undefined, "data-covered": showPoint && Math.abs(index - active) / last <= .15 || undefined, children: categories[index] }, index))) })] }), showLegend && _jsx(ChartLegend, { series: series })] }) : _jsx(Text, { className: "flood-line-chart__empty", size: "compact", tone: "subtle", children: emptyLabel }) });
|
|
221
|
+
});
|
|
222
|
+
export const GroupedBars = /* @__PURE__ */ forwardRef(function GroupedBars({ label, categories, series, domain, layout = "grouped", showLegend = false, formatValue = formatNumber, formatShare = formatPercent, emptyLabel, missingLabel, navigationLabel, className, ...props }, ref) {
|
|
223
|
+
const instructions = useId();
|
|
224
|
+
const [cursor, setCursor] = useState(0);
|
|
225
|
+
const [hover, setHover] = useState();
|
|
226
|
+
const [focused, setFocused] = useState(false);
|
|
227
|
+
const [touch, setTouch] = useState();
|
|
228
|
+
const last = categories.length - 1;
|
|
229
|
+
const active = Math.min(last, hover ?? touch ?? cursor);
|
|
230
|
+
const inspecting = hover !== undefined || focused || touch !== undefined;
|
|
231
|
+
let low = 0, high = 0, hasData = false;
|
|
232
|
+
if (series.length > 4 || new Set(series.map(item => item.id)).size !== series.length ||
|
|
233
|
+
series.some(item => !item.id.trim() || !item.label.trim() || item.values.length !== categories.length)) {
|
|
234
|
+
throw new Error("Flood BarChart needs up to four uniquely identified series with one value per category");
|
|
235
|
+
}
|
|
236
|
+
const percent = layout === "percent";
|
|
237
|
+
const stacked = layout !== "grouped";
|
|
238
|
+
const groups = categories.map((_, index) => {
|
|
239
|
+
let positive = 0;
|
|
240
|
+
let negative = 0;
|
|
241
|
+
const segments = series.map(item => {
|
|
242
|
+
const value = item.values[index];
|
|
243
|
+
if (!observed(value))
|
|
244
|
+
return undefined;
|
|
245
|
+
if (percent && value < 0)
|
|
246
|
+
throw new RangeError("Flood BarChart percent layout needs nonnegative observations");
|
|
247
|
+
hasData = true;
|
|
248
|
+
const start = stacked ? (value < 0 ? negative : positive) : 0;
|
|
249
|
+
const end = start + value;
|
|
250
|
+
if (!Number.isFinite(end))
|
|
251
|
+
throw new RangeError("Flood BarChart stacks must have finite totals");
|
|
252
|
+
if (value < 0)
|
|
253
|
+
negative = end;
|
|
254
|
+
else
|
|
255
|
+
positive = end;
|
|
256
|
+
low = Math.min(low, end);
|
|
257
|
+
high = Math.max(high, end);
|
|
258
|
+
return { value, start, end };
|
|
259
|
+
});
|
|
260
|
+
return { segments, positive, negative };
|
|
261
|
+
});
|
|
262
|
+
if (!percent && domain && (!Number.isFinite(domain[0]) || !Number.isFinite(domain[1]) ||
|
|
263
|
+
domain[0] >= domain[1] || low < domain[0] || high > domain[1])) {
|
|
264
|
+
throw new RangeError("Flood BarChart domain must have finite increasing bounds containing zero and all bar extents");
|
|
265
|
+
}
|
|
266
|
+
const [min, max] = percent ? [0, 1] : domain ?? [low, high || (low === 0 ? 1 : 0)];
|
|
267
|
+
const span = max - min;
|
|
268
|
+
const fraction = (value) => Number.isFinite(span)
|
|
269
|
+
? (value - min) / span
|
|
270
|
+
: (value / 2 - min / 2) / (max / 2 - min / 2);
|
|
271
|
+
const zero = fraction(0);
|
|
272
|
+
const values = hasData ? series.map(item => {
|
|
273
|
+
const value = item.values[active];
|
|
274
|
+
if (!observed(value))
|
|
275
|
+
return missingLabel;
|
|
276
|
+
const total = groups[active].positive;
|
|
277
|
+
return percent && total > 0 ? `${formatShare(value / total)} · ${formatValue(value)}` : formatValue(value);
|
|
278
|
+
}) : [];
|
|
279
|
+
const valueText = [categories[active], ...series.map((item, i) => `${item.label}: ${values[i]}`)].join(", ");
|
|
280
|
+
const navigate = (event) => {
|
|
281
|
+
let next;
|
|
282
|
+
switch (event.key) {
|
|
283
|
+
case "ArrowRight":
|
|
284
|
+
case "ArrowUp":
|
|
285
|
+
next = active + 1;
|
|
286
|
+
break;
|
|
287
|
+
case "ArrowLeft":
|
|
288
|
+
case "ArrowDown":
|
|
289
|
+
next = active - 1;
|
|
290
|
+
break;
|
|
291
|
+
case "Home":
|
|
292
|
+
next = 0;
|
|
293
|
+
break;
|
|
294
|
+
case "End":
|
|
295
|
+
next = last;
|
|
296
|
+
break;
|
|
297
|
+
default: return;
|
|
298
|
+
}
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
setHover(undefined);
|
|
301
|
+
setTouch(undefined);
|
|
302
|
+
setFocused(true);
|
|
303
|
+
setCursor(Math.max(0, Math.min(last, next)));
|
|
304
|
+
};
|
|
305
|
+
return _jsx("div", { ...props, ref: ref, className: ["flood-bar-chart", className].filter(Boolean).join(" "), role: "group", "aria-label": label, "data-layout": layout, children: hasData ? _jsxs(_Fragment, { children: [last > 0 && _jsx(VisuallyHidden, { id: instructions, children: navigationLabel }), _jsxs("div", { className: "flood-bar-chart__plot", dir: "ltr", role: last > 0 ? "slider" : "img", tabIndex: last > 0 ? 0 : undefined, "aria-label": last > 0 ? label : valueText, "aria-describedby": last > 0 ? instructions : undefined, "aria-valuemin": last > 0 ? 1 : undefined, "aria-valuemax": last > 0 ? last + 1 : undefined, "aria-valuenow": last > 0 ? active + 1 : undefined, "aria-valuetext": last > 0 ? valueText : undefined, style: { "--flood-bar-zero": zero, "--flood-bar-categories": categories.length }, "data-inspecting": inspecting || undefined, onFocus: event => setFocused(event.currentTarget.matches(":focus-visible")), onBlur: () => setFocused(false), onKeyDown: navigate, onPointerLeave: () => setHover(undefined), children: [_jsx("div", { className: "flood-bar-chart__marks", "aria-hidden": "true", children: categories.map((category, i) => _jsxs("div", { className: "flood-bar-chart__group", "data-active": inspecting && active === i || undefined, onPointerEnter: event => { if (event.pointerType !== "touch")
|
|
306
|
+
setHover(i); }, onPointerUp: event => { if (event.pointerType === "touch")
|
|
307
|
+
setTouch(current => current === i ? undefined : i); }, children: [_jsx("div", { className: "flood-bar-chart__pair", children: series.map((item, j) => {
|
|
308
|
+
const { segments, positive, negative } = groups[i];
|
|
309
|
+
const segment = segments[j];
|
|
310
|
+
const normalize = (value) => percent ? value / (positive || 1) : fraction(value);
|
|
311
|
+
const start = normalize(segment?.start ?? 0);
|
|
312
|
+
const end = normalize(segment?.end ?? 0);
|
|
313
|
+
const outer = segment && segment.end === (segment.value < 0 ? negative : positive);
|
|
314
|
+
return _jsx("span", { className: "flood-bar-chart__slot", "data-series": j, children: segment && (!stacked || start !== end) && _jsx("span", { className: "flood-bar-chart__bar", "data-zero": segment.value === 0 || undefined, "data-negative": segment.value < 0 || undefined, "data-outer": stacked && outer || undefined, style: { insetBlockEnd: `${Math.min(start, end) * 100}%`, blockSize: `${Math.abs(end - start) * 100}%` } }) }, item.id);
|
|
315
|
+
}) }), _jsx(Text, { className: "flood-bar-chart__category", size: "helper", tone: "subtle", dir: "auto", title: category, children: category })] }, i)) }), inspecting && _jsxs(Surface, { className: "flood-bar-chart__tooltip", padding: "2", elevation: "raised", "aria-hidden": "true", style: {
|
|
316
|
+
"--flood-bar-active": `${(active + .5) / categories.length * 100}%`,
|
|
317
|
+
"--flood-bar-tip-side": active >= categories.length / 2 ? -1 : 0,
|
|
318
|
+
}, children: [_jsx(Text, { size: "helper", children: categories[active] }), _jsx(ChartReadout, { series: series, values: values })] })] }), showLegend && _jsx(ChartLegend, { series: series })] }) : _jsx(Text, { className: "flood-line-chart__empty", size: "compact", tone: "subtle", children: emptyLabel }) });
|
|
319
|
+
});
|
|
320
|
+
const defaultPercent = new Intl.NumberFormat("en", { style: "percent", maximumFractionDigits: 1 });
|
|
321
|
+
const formatPercent = (share) => defaultPercent.format(share);
|
|
322
|
+
const paletteSize = 6;
|
|
323
|
+
function ringSector(start, end) {
|
|
324
|
+
const point = (turn, radius) => {
|
|
325
|
+
const angle = (turn - .25) * 2 * Math.PI;
|
|
326
|
+
return `${100 + radius * Math.cos(angle)} ${100 + radius * Math.sin(angle)}`;
|
|
327
|
+
};
|
|
328
|
+
if (end - start === 1) {
|
|
329
|
+
return "M 100 8 A 92 92 0 0 1 100 192 A 92 92 0 0 1 100 8 Z " +
|
|
330
|
+
"M 100 24 A 76 76 0 0 0 100 176 A 76 76 0 0 0 100 24 Z";
|
|
331
|
+
}
|
|
332
|
+
const outerRadius = 92;
|
|
333
|
+
const innerRadius = 76;
|
|
334
|
+
const corner = Math.min(3.5, (end - start) * 2 * Math.PI * innerRadius / 4);
|
|
335
|
+
const outerInset = corner / (2 * Math.PI * outerRadius);
|
|
336
|
+
const innerInset = corner / (2 * Math.PI * innerRadius);
|
|
337
|
+
const middle = (start + end) / 2;
|
|
338
|
+
return `M ${point(start + outerInset, outerRadius)}
|
|
339
|
+
A 92 92 0 0 1 ${point(middle, outerRadius)} A 92 92 0 0 1 ${point(end - outerInset, outerRadius)}
|
|
340
|
+
Q ${point(end, outerRadius)} ${point(end, outerRadius - corner)}
|
|
341
|
+
L ${point(end, innerRadius + corner)}
|
|
342
|
+
Q ${point(end, innerRadius)} ${point(end - innerInset, innerRadius)}
|
|
343
|
+
A 76 76 0 0 0 ${point(middle, innerRadius)} A 76 76 0 0 0 ${point(start + innerInset, innerRadius)}
|
|
344
|
+
Q ${point(start, innerRadius)} ${point(start, innerRadius + corner)}
|
|
345
|
+
L ${point(start, outerRadius - corner)}
|
|
346
|
+
Q ${point(start, outerRadius)} ${point(start + outerInset, outerRadius)} Z`;
|
|
347
|
+
}
|
|
348
|
+
export const ShareRing = /* @__PURE__ */ forwardRef(function ShareRing({ label, items, totalLabel, emptyLabel, navigationLabel, showLegend, formatValue = formatNumber, formatShare = formatPercent, className, ...props }, ref) {
|
|
349
|
+
const instructions = useId();
|
|
350
|
+
const [cursor, setCursor] = useState(0);
|
|
351
|
+
const [hovered, setHovered] = useState();
|
|
352
|
+
const [tapped, setTapped] = useState();
|
|
353
|
+
const [focused, setFocused] = useState(false);
|
|
354
|
+
if (items.length > paletteSize)
|
|
355
|
+
throw new RangeError("Flood DonutChart supports up to six categories");
|
|
356
|
+
const ids = new Set();
|
|
357
|
+
const ends = [];
|
|
358
|
+
let total = 0;
|
|
359
|
+
for (const item of items) {
|
|
360
|
+
if (!item.id.trim() || ids.has(item.id) || !item.label.trim() || !Number.isFinite(item.value) || item.value < 0) {
|
|
361
|
+
throw new Error("Flood DonutChart needs unique IDs, nonempty labels and finite nonnegative values");
|
|
362
|
+
}
|
|
363
|
+
ids.add(item.id);
|
|
364
|
+
total += item.value;
|
|
365
|
+
ends.push(total);
|
|
366
|
+
}
|
|
367
|
+
if (!Number.isFinite(total))
|
|
368
|
+
throw new RangeError("Flood DonutChart needs a finite total");
|
|
369
|
+
const last = items.length - 1;
|
|
370
|
+
const active = Math.min(last, hovered ?? tapped ?? cursor);
|
|
371
|
+
const selected = items[active];
|
|
372
|
+
const inspecting = hovered !== undefined || tapped !== undefined || focused;
|
|
373
|
+
const totalText = formatValue(total);
|
|
374
|
+
const share = selected && total > 0 ? formatShare(selected.value / total) : undefined;
|
|
375
|
+
const valueText = selected ? [selected.label, formatValue(selected.value), share].filter(Boolean).join(", ") : undefined;
|
|
376
|
+
const segmentAt = (event) => {
|
|
377
|
+
if (total === 0)
|
|
378
|
+
return undefined;
|
|
379
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
380
|
+
if (!bounds.width || !bounds.height)
|
|
381
|
+
return undefined;
|
|
382
|
+
const x = (event.clientX - bounds.left) / bounds.width - .5;
|
|
383
|
+
const y = (event.clientY - bounds.top) / bounds.height - .5;
|
|
384
|
+
const radius = Math.hypot(x, y);
|
|
385
|
+
if (radius < .28 || radius > .5)
|
|
386
|
+
return undefined;
|
|
387
|
+
const turn = (Math.atan2(y, x) / (2 * Math.PI) + 1.25) % 1;
|
|
388
|
+
const index = ends.findIndex(end => turn < end / total);
|
|
389
|
+
return index === -1 ? undefined : index;
|
|
390
|
+
};
|
|
391
|
+
const navigate = (event) => {
|
|
392
|
+
let next;
|
|
393
|
+
switch (event.key) {
|
|
394
|
+
case "ArrowRight":
|
|
395
|
+
case "ArrowUp":
|
|
396
|
+
next = active + 1;
|
|
397
|
+
break;
|
|
398
|
+
case "ArrowLeft":
|
|
399
|
+
case "ArrowDown":
|
|
400
|
+
next = active - 1;
|
|
401
|
+
break;
|
|
402
|
+
case "Home":
|
|
403
|
+
next = 0;
|
|
404
|
+
break;
|
|
405
|
+
case "End":
|
|
406
|
+
next = last;
|
|
407
|
+
break;
|
|
408
|
+
case "Escape":
|
|
409
|
+
setHovered(undefined);
|
|
410
|
+
setTapped(undefined);
|
|
411
|
+
setFocused(false);
|
|
412
|
+
return;
|
|
413
|
+
default: return;
|
|
414
|
+
}
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
setHovered(undefined);
|
|
417
|
+
setTapped(undefined);
|
|
418
|
+
setFocused(true);
|
|
419
|
+
setCursor(Math.max(0, Math.min(last, next)));
|
|
420
|
+
};
|
|
421
|
+
const showSelection = inspecting && Boolean(selected);
|
|
422
|
+
const totalName = `${totalLabel}: ${totalText}`;
|
|
423
|
+
let offset = 0;
|
|
424
|
+
const separate = items.filter(item => item.value > 0).length > 1;
|
|
425
|
+
return _jsxs("div", { ...props, ref: ref, className: ["flood-donut-chart", className].filter(Boolean).join(" "), role: "group", "aria-label": label, children: [last > 0 && _jsxs(VisuallyHidden, { id: instructions, children: [totalName, ". ", navigationLabel] }), _jsxs("div", { className: "flood-donut-chart__plot", role: last > 0 ? "slider" : "img", tabIndex: last > 0 ? 0 : undefined, "aria-label": last > 0 ? label : [label, valueText, items.length ? totalName : emptyLabel].filter(Boolean).join(". "), "aria-describedby": last > 0 ? instructions : undefined, "aria-valuemin": last > 0 ? 1 : undefined, "aria-valuemax": last > 0 ? last + 1 : undefined, "aria-valuenow": last > 0 ? active + 1 : undefined, "aria-valuetext": last > 0 ? valueText : undefined, onPointerLeave: () => setHovered(undefined), onFocus: event => setFocused(event.currentTarget.matches(":focus-visible")), onBlur: () => setFocused(false), onKeyDown: navigate, children: [_jsxs("svg", { viewBox: "0 0 200 200", "aria-hidden": "true", onPointerMove: event => { if (event.pointerType !== "touch")
|
|
426
|
+
setHovered(segmentAt(event)); }, onPointerEnter: event => { if (event.pointerType !== "touch")
|
|
427
|
+
setHovered(segmentAt(event)); }, onPointerUp: event => {
|
|
428
|
+
if (event.pointerType !== "touch")
|
|
429
|
+
return;
|
|
430
|
+
const next = segmentAt(event);
|
|
431
|
+
setTapped(current => current === next ? undefined : next);
|
|
432
|
+
}, children: [total === 0 && _jsx("circle", { className: "flood-donut-chart__track", cx: "100", cy: "100", r: "84" }), items.map((item, i) => {
|
|
433
|
+
if (item.value === 0)
|
|
434
|
+
return null;
|
|
435
|
+
const length = item.value / total;
|
|
436
|
+
if (length === 0)
|
|
437
|
+
return null;
|
|
438
|
+
const gap = separate ? Math.min(.0065, length * .2) : 0;
|
|
439
|
+
const start = offset + gap;
|
|
440
|
+
offset += length;
|
|
441
|
+
return _jsx("path", { className: "flood-donut-chart__segment", "data-slice": i, "data-active": showSelection && active === i || undefined, d: ringSector(start, offset - gap) }, item.id);
|
|
442
|
+
})] }), _jsx("div", { className: "flood-donut-chart__readout", "aria-hidden": "true", children: items.length ? _jsx(Metric, { size: "sm", label: showSelection ? selected.label : totalLabel, value: showSelection ? formatValue(selected.value) : totalText, hint: showSelection ? share : undefined })
|
|
443
|
+
: _jsx(Text, { size: "compact", tone: "subtle", children: emptyLabel }) })] }), showLegend && items.length > 0 && _jsx(ChartLegend, { series: items, swatch: true })] });
|
|
444
|
+
});
|
|
445
|
+
export const HeatmapGrid = /* @__PURE__ */ forwardRef(function HeatmapGrid({ label, columns, rows, max, formatValue = formatNumber, missingLabel, emptyLabel, className, ...props }, ref) {
|
|
446
|
+
const table = useRef(null);
|
|
447
|
+
const tooltip = useRef(null);
|
|
448
|
+
const [cursor, setCursor] = useState(0);
|
|
449
|
+
const [hovered, setHovered] = useState();
|
|
450
|
+
const [focused, setFocused] = useState(false);
|
|
451
|
+
const [tapped, setTapped] = useState();
|
|
452
|
+
const width = columns.length;
|
|
453
|
+
const cellCount = width * rows.length;
|
|
454
|
+
const last = cellCount - 1;
|
|
455
|
+
let largest = 0;
|
|
456
|
+
const ids = new Set();
|
|
457
|
+
for (const row of rows) {
|
|
458
|
+
if (!row.id.trim() || ids.has(row.id) || !row.label.trim() || row.values.length !== width) {
|
|
459
|
+
throw new Error("Flood Heatmap needs unique row IDs, labels and one value per column");
|
|
460
|
+
}
|
|
461
|
+
ids.add(row.id);
|
|
462
|
+
for (const value of row.values) {
|
|
463
|
+
if (value === null)
|
|
464
|
+
continue;
|
|
465
|
+
if (!Number.isFinite(value) || value < 0)
|
|
466
|
+
throw new RangeError("Flood Heatmap values must be finite and nonnegative");
|
|
467
|
+
largest = Math.max(largest, value);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const limit = max ?? (largest || 1);
|
|
471
|
+
if (!Number.isFinite(limit) || limit <= 0 || limit < largest) {
|
|
472
|
+
throw new RangeError("Flood Heatmap max must be finite, positive and at least the largest observation");
|
|
473
|
+
}
|
|
474
|
+
const current = Math.min(cursor, last);
|
|
475
|
+
const inspection = hovered ?? tapped ?? (focused ? current : undefined);
|
|
476
|
+
const active = inspection === undefined ? -1 : Math.min(inspection, last);
|
|
477
|
+
const activeRow = active >= 0 ? rows[Math.floor(active / width)] : undefined;
|
|
478
|
+
const activeValue = activeRow?.values[active % width];
|
|
479
|
+
useChartLayoutEffect(() => {
|
|
480
|
+
const panel = tooltip.current;
|
|
481
|
+
const area = panel?.offsetParent;
|
|
482
|
+
const cell = table.current?.tBodies[0]?.rows[Math.floor(active / width)]?.cells[active % width + 1];
|
|
483
|
+
if (!panel || !(area instanceof HTMLElement) || !cell)
|
|
484
|
+
return;
|
|
485
|
+
const position = () => {
|
|
486
|
+
const bounds = area.getBoundingClientRect();
|
|
487
|
+
if (!bounds.width)
|
|
488
|
+
return;
|
|
489
|
+
const scale = bounds.width / area.offsetWidth || 1;
|
|
490
|
+
const gap = parseFloat(getComputedStyle(panel).columnGap) * scale;
|
|
491
|
+
const { left, top } = placeOverlay(cell.getBoundingClientRect(), panel.getBoundingClientRect(), bounds, "top", "center", gap);
|
|
492
|
+
panel.style.left = `${(left - bounds.left) / scale}px`;
|
|
493
|
+
panel.style.top = `${(top - bounds.top) / scale}px`;
|
|
494
|
+
};
|
|
495
|
+
position();
|
|
496
|
+
const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(position);
|
|
497
|
+
observer?.observe(area);
|
|
498
|
+
observer?.observe(panel);
|
|
499
|
+
observer?.observe(cell);
|
|
500
|
+
area.addEventListener("scroll", position, true);
|
|
501
|
+
window.addEventListener("resize", position);
|
|
502
|
+
return () => {
|
|
503
|
+
observer?.disconnect();
|
|
504
|
+
area.removeEventListener("scroll", position, true);
|
|
505
|
+
window.removeEventListener("resize", position);
|
|
506
|
+
};
|
|
507
|
+
}, [active, width]);
|
|
508
|
+
const navigate = (event, index) => {
|
|
509
|
+
const rowStart = index - index % width;
|
|
510
|
+
const rowEnd = rowStart + width - 1;
|
|
511
|
+
let next;
|
|
512
|
+
switch (event.key) {
|
|
513
|
+
case "ArrowRight":
|
|
514
|
+
next = Math.min(rowEnd, index + 1);
|
|
515
|
+
break;
|
|
516
|
+
case "ArrowLeft":
|
|
517
|
+
next = Math.max(rowStart, index - 1);
|
|
518
|
+
break;
|
|
519
|
+
case "ArrowUp":
|
|
520
|
+
next = index >= width ? index - width : index;
|
|
521
|
+
break;
|
|
522
|
+
case "ArrowDown":
|
|
523
|
+
next = index + width < cellCount ? index + width : index;
|
|
524
|
+
break;
|
|
525
|
+
case "Home":
|
|
526
|
+
next = event.ctrlKey ? 0 : rowStart;
|
|
527
|
+
break;
|
|
528
|
+
case "End":
|
|
529
|
+
next = event.ctrlKey ? last : rowEnd;
|
|
530
|
+
break;
|
|
531
|
+
default: return;
|
|
532
|
+
}
|
|
533
|
+
event.preventDefault();
|
|
534
|
+
setHovered(undefined);
|
|
535
|
+
setTapped(undefined);
|
|
536
|
+
table.current?.tBodies[0].rows[Math.floor(next / width)].cells[next % width + 1].focus();
|
|
537
|
+
};
|
|
538
|
+
return _jsx("div", { ...props, ref: ref, className: ["flood-heatmap", className].filter(Boolean).join(" "), children: cellCount ? _jsxs(_Fragment, { children: [_jsxs(Table, { ref: table, className: "flood-heatmap__table", caption: label, hideCaption: true, dir: "ltr", onBlur: event => { if (!event.currentTarget.contains(event.relatedTarget))
|
|
539
|
+
setFocused(false); }, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", {}), columns.map((column, index) => _jsx("th", { scope: "col", children: _jsx(Text, { size: "helper", tone: "subtle", dir: "auto", title: column, children: column }) }, index))] }) }), _jsx("tbody", { onPointerLeave: () => setHovered(undefined), children: rows.map((row, y) => _jsxs("tr", { children: [_jsx("th", { scope: "row", children: _jsx(Text, { size: "helper", tone: "subtle", dir: "auto", title: row.label, children: row.label }) }), row.values.map((value, x) => {
|
|
540
|
+
const index = y * width + x;
|
|
541
|
+
return _jsx("td", { className: "flood-heatmap__cell", tabIndex: index === current ? 0 : -1, "data-active": index === active || undefined, "data-missing": value === null || undefined, style: { "--flood-heatmap-level": value === null ? 0 : value / limit }, onFocus: event => { setCursor(index); setFocused(event.currentTarget.matches(":focus-visible")); }, onKeyDown: event => navigate(event, index), onPointerEnter: event => { if (event.pointerType !== "touch")
|
|
542
|
+
setHovered(index); }, onPointerUp: event => { if (event.pointerType === "touch")
|
|
543
|
+
setTapped(previous => previous === index ? undefined : index); }, children: _jsx(VisuallyHidden, { children: value === null ? missingLabel : formatValue(value) }) }, x);
|
|
544
|
+
})] }, row.id)) })] }), activeRow && _jsxs(Surface, { ref: tooltip, className: "flood-heatmap__tooltip", padding: "2", elevation: "raised", "aria-hidden": "true", children: [_jsxs(Text, { size: "helper", tone: "subtle", children: [activeRow.label, " \u00B7 ", columns[active % width]] }), _jsx(Text, { size: "compact", children: activeValue == null ? missingLabel : formatValue(activeValue) })] })] }) : _jsx(Text, { size: "compact", tone: "subtle", children: emptyLabel }) });
|
|
545
|
+
});
|
|
546
|
+
export const RankedBars = /* @__PURE__ */ forwardRef(function RankedBars({ label, items, max, tone, emptyLabel, className, ...props }, ref) {
|
|
547
|
+
let largest = 0;
|
|
548
|
+
const ids = new Set();
|
|
549
|
+
for (const item of items) {
|
|
550
|
+
if (!item.id.trim() || ids.has(item.id) || !item.label.trim() || !Number.isFinite(item.value) || item.value < 0) {
|
|
551
|
+
throw new Error("Flood BarList needs unique IDs, nonempty labels and finite nonnegative values");
|
|
552
|
+
}
|
|
553
|
+
ids.add(item.id);
|
|
554
|
+
largest = Math.max(largest, item.value);
|
|
555
|
+
}
|
|
556
|
+
const limit = max ?? (largest || 1);
|
|
557
|
+
if (!Number.isFinite(limit) || limit <= 0 || limit < largest) {
|
|
558
|
+
throw new RangeError("Flood BarList max must be positive, finite and at least the largest item");
|
|
559
|
+
}
|
|
560
|
+
return _jsx("div", { ...props, ref: ref, className: ["flood-bar-list", className].filter(Boolean).join(" "), role: "group", "aria-label": label, children: items.length ? _jsx("ul", { className: "flood-bar-list__items", role: "list", children: items.map(item => _jsx("li", { children: _jsx(Meter, { label: item.label, value: item.value, max: limit, valueText: item.valueText ?? formatNumber(item.value), tone: tone, size: "sm" }) }, item.id)) }) : _jsx(Text, { size: "compact", tone: "subtle", children: emptyLabel }) });
|
|
561
|
+
});
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef, useId, useImperativeHandle, useRef } from 'react';
|
|
3
3
|
import { Button, Field, Fieldset, IconSlot, Inline, Popover, SearchInput, Tag, Text } from './index.js';
|
|
4
4
|
/** Query chrome only: the host supplies filter drafts, accepted query and saved views. */
|
|
5
|
-
export const CollectionQueryBar = /* @__PURE__ */ forwardRef(function CollectionQueryBar({ label, searchValue, onSearchValueChange, searchLabel = 'Search', searchPlaceholder = 'Search…', clearSearchLabel = 'Clear search', appliedFilters = [], onRemoveFilter, appliedFiltersLabel = 'Applied filters', summaryPrefix, quickFilters, actions, filterContent, filtersLabel = 'Filters', filterTitle = filtersLabel, filtersOpen, onFiltersOpenChange, filterPanelRef, filterPanelClassName, onClear, canClear = Boolean(searchValue || appliedFilters.length), clearLabel = 'Clear filters', controlSize = 'sm', disabled = false, hideLabel = true, className, ...props }, forwardedRef) {
|
|
5
|
+
export const CollectionQueryBar = /* @__PURE__ */ forwardRef(function CollectionQueryBar({ label, searchValue, onSearchValueChange, searchLabel = 'Search', searchPlaceholder = 'Search…', clearSearchLabel = 'Clear search', appliedFilters = [], onRemoveFilter, appliedFiltersLabel = 'Applied filters', summaryPrefix, align = 'stretch', quickFilters, actions, filterContent, filtersLabel = 'Filters', filterTitle = filtersLabel, filtersOpen, onFiltersOpenChange, filterPanelRef, filterPanelClassName, onClear, canClear = Boolean(searchValue || appliedFilters.length), clearLabel = 'Clear filters', controlSize = 'sm', disabled = false, hideLabel = true, className, ...props }, forwardedRef) {
|
|
6
6
|
const root = useRef(null);
|
|
7
7
|
useImperativeHandle(forwardedRef, () => root.current, []);
|
|
8
8
|
const generated = useId(), triggerId = `flood-collection-filters-${generated}`, searchId = `flood-collection-search-${generated}`;
|
|
@@ -15,7 +15,7 @@ export const CollectionQueryBar = /* @__PURE__ */ forwardRef(function Collection
|
|
|
15
15
|
const document = root.current?.ownerDocument;
|
|
16
16
|
document?.getElementById(hasPanel ? triggerId : searchId)?.focus({ preventScroll: true });
|
|
17
17
|
}
|
|
18
|
-
return _jsx(Fieldset, { ...props, ref: root, legend: label, hideLabel: hideLabel, disabled: disabled, className: ['flood-collection-toolbar', className].filter(Boolean).join(' '), children: _jsxs(Inline, { gap: "3", align: "center", className: "flood-collection-toolbar__row", children: [_jsx("div", { className: "flood-collection-toolbar__search", children: _jsx(Field, { id: searchId, label: searchLabel, hideLabel: true, children: field => _jsx(SearchInput, { ...field, controlSize: controlSize, value: searchValue, placeholder: searchPlaceholder, clearLabel: clearSearchLabel, disabled: disabled, onChange: event => { if (!disabled)
|
|
18
|
+
return _jsx(Fieldset, { ...props, ref: root, legend: label, hideLabel: hideLabel, disabled: disabled, className: ['flood-collection-toolbar', className].filter(Boolean).join(' '), "data-align": align, children: _jsxs(Inline, { gap: "3", align: "center", className: "flood-collection-toolbar__row", children: [_jsx("div", { className: "flood-collection-toolbar__search", children: _jsx(Field, { id: searchId, label: searchLabel, hideLabel: true, children: field => _jsx(SearchInput, { ...field, controlSize: controlSize, value: searchValue, placeholder: searchPlaceholder, clearLabel: clearSearchLabel, disabled: disabled, onChange: event => { if (!disabled)
|
|
19
19
|
onSearchValueChange(event.currentTarget.value); } }) }) }), hasSummary && _jsxs(Inline, { gap: "2", align: "center", role: "group", "aria-label": appliedFiltersLabel, className: "flood-collection-toolbar__summaries", children: [quickFilters, summaryPrefix && _jsx(Text, { size: "compact", tone: "subtle", children: summaryPrefix }), appliedFilters.map(filter => _jsx(Tag, { size: controlSize === 'xl' ? 'lg' : 'md', disabled: disabled || filter.disabled, removeLabel: filter.removeLabel ?? `Remove ${filter.label}`, onRemove: onRemoveFilter ? () => {
|
|
20
20
|
if (disabled || filter.disabled)
|
|
21
21
|
return;
|
package/dist/data-display.js
CHANGED
|
@@ -31,5 +31,5 @@ export const TabularData = /* @__PURE__ */ forwardRef(function TabularData({ cap
|
|
|
31
31
|
!target.closest("[popover], dialog[open]") && target.getBoundingClientRect().top < header.getBoundingClientRect().bottom) {
|
|
32
32
|
target.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
33
33
|
}
|
|
34
|
-
} : undefined, children: _jsxs("table", { ...props, ref: ref, hidden: hidden, dir: dir, lang: lang, className: ["flood-table", className].filter(Boolean).join(" "), "data-density": density, "data-striped": striped, "data-cell-vertical-align": cellVerticalAlign, "data-sticky-header": pinHeader || undefined, children: [_jsx("caption", { className: hideCaption ? "flood-visually-hidden" : undefined, children: caption }), children] }) }) });
|
|
34
|
+
} : undefined, children: _jsxs("table", { ...props, ref: ref, hidden: hidden, dir: dir, lang: lang, className: ["flood-table", className].filter(Boolean).join(" "), "data-density": density, "data-striped": striped, "data-cell-vertical-align": cellVerticalAlign, "data-sticky-header": pinHeader || undefined, "data-bounded": scrollVertically || undefined, children: [_jsx("caption", { className: hideCaption ? "flood-visually-hidden" : undefined, children: caption }), children] }) }) });
|
|
35
35
|
});
|