@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 CHANGED
@@ -57,7 +57,7 @@ the interactive size. It defaults to size; shrinking a supporting glyph must
57
57
  not shrink its target or bypass IconSlot with a custom SVG width. Existing glyph
58
58
  roles at a 16px root are 2xs=14px, xs/sm=16px and md/lg=20px.
59
59
 
60
- This package documents the **0.2.12** source API. Keep Flood package versions aligned and follow the guide matching
60
+ This package documents the **0.2.13** source API. Keep Flood package versions aligned and follow the guide matching
61
61
  your installed declarations; source changes do not modify an existing npm release.
62
62
 
63
63
  NavigationItem (0.2.11 candidate) is the shared small row for destinations,
@@ -0,0 +1,2 @@
1
+ import { type BoxPlotProps } from "./index.js";
2
+ export declare const DistributionBoxes: import("react").ForwardRefExoticComponent<BoxPlotProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,148 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
3
+ import { Surface, Text, VisuallyHidden } from "./index.js";
4
+ import { placeOverlay } from "./overlay-runtime.js";
5
+ const number = new Intl.NumberFormat("en", { maximumSignificantDigits: 21 });
6
+ const useChartLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
7
+ const stats = ["low", "q1", "median", "q3", "high"];
8
+ const defaults = {
9
+ low: "Lower whisker", q1: "Q1", median: "Median", q3: "Q3", high: "Upper whisker", outliers: "Outliers",
10
+ };
11
+ const tooltipRows = [[2], [1, 3], [0, 4], [5]];
12
+ export const DistributionBoxes = /* @__PURE__ */ forwardRef(function DistributionBoxes({ label, items, domain, formatValue = number.format, formatAxisValue = formatValue, statisticLabels, emptyLabel = "No observations", missingLabel = "No data", navigationLabel = "Use the arrow keys to explore groups, or Home and End to jump.", className, ...props }, ref) {
13
+ const instructions = useId();
14
+ const plot = useRef(null);
15
+ const tooltip = useRef(null);
16
+ const [cursor, setCursor] = useState(0);
17
+ const [hover, setHover] = useState();
18
+ const [touch, setTouch] = useState();
19
+ const [focused, setFocused] = useState(false);
20
+ const ids = new Set();
21
+ let min = Infinity;
22
+ let max = -Infinity;
23
+ for (const { id, label: name, summary, outliers = [] } of items) {
24
+ if (!id.trim() || ids.has(id) || !name.trim())
25
+ throw new Error("Flood BoxPlot needs unique IDs and nonempty labels");
26
+ ids.add(id);
27
+ if (!summary) {
28
+ if (outliers.length)
29
+ throw new RangeError("Flood BoxPlot outliers require a summary");
30
+ continue;
31
+ }
32
+ const values = stats.map(key => summary[key]);
33
+ if (values.some((value, i) => !Number.isFinite(value) || (i > 0 && value < values[i - 1]))) {
34
+ throw new RangeError("Flood BoxPlot statistics must be finite and ordered from low to high");
35
+ }
36
+ min = Math.min(min, summary.low);
37
+ max = Math.max(max, summary.high);
38
+ for (const value of outliers) {
39
+ if (!Number.isFinite(value) || (value >= summary.low && value <= summary.high)) {
40
+ throw new RangeError("Flood BoxPlot outliers must be finite and outside the whiskers");
41
+ }
42
+ min = Math.min(min, value);
43
+ max = Math.max(max, value);
44
+ }
45
+ }
46
+ if (domain && (!Number.isFinite(domain[0]) || !Number.isFinite(domain[1]) ||
47
+ domain[0] >= domain[1] || domain[0] > min || domain[1] < max)) {
48
+ throw new RangeError("Flood BoxPlot domain must have finite increasing bounds containing every observation");
49
+ }
50
+ const hasData = min <= max;
51
+ const [floor, ceiling] = domain ?? [min, max];
52
+ const span = ceiling - floor;
53
+ const position = (value) => {
54
+ if (span === 0)
55
+ return 50;
56
+ return 100 * (Number.isFinite(span) ? (value - floor) / span : (value / 2 - floor / 2) / (ceiling / 2 - floor / 2));
57
+ };
58
+ const last = items.length - 1;
59
+ const active = Math.min(hover ?? touch ?? cursor, last);
60
+ const item = items[active];
61
+ const summary = item?.summary;
62
+ const inspecting = hover !== undefined || touch !== undefined || focused;
63
+ const readouts = summary ? stats.map(key => ({
64
+ label: statisticLabels?.[key] ?? defaults[key], value: formatValue(summary[key]),
65
+ })) : [];
66
+ if (item?.outliers?.length) {
67
+ readouts.push({ label: statisticLabels?.outliers ?? defaults.outliers, value: number.format(item.outliers.length) });
68
+ }
69
+ const valueText = [item?.label, ...(summary ? readouts.map(entry => `${entry.label}: ${entry.value}`) : [missingLabel])].join(", ");
70
+ useChartLayoutEffect(() => {
71
+ const panel = tooltip.current;
72
+ const frame = plot.current;
73
+ if (!panel || !frame)
74
+ return;
75
+ const mark = frame.querySelector("[data-active] .flood-box-plot__median, [data-active] .flood-box-plot__missing");
76
+ if (!mark)
77
+ return;
78
+ const positionTooltip = () => {
79
+ const bounds = frame.getBoundingClientRect();
80
+ if (!bounds.width || !frame.offsetWidth)
81
+ return;
82
+ const marker = mark.getBoundingClientRect();
83
+ const scale = bounds.width / frame.offsetWidth;
84
+ const gap = parseFloat(getComputedStyle(panel).columnGap) * scale;
85
+ const size = panel.getBoundingClientRect();
86
+ const x = (marker.left + marker.right) / 2;
87
+ const y = (marker.top + marker.bottom) / 2;
88
+ const anchor = { left: x, right: x, top: y, bottom: y, width: 0, height: 0 };
89
+ const horizontalRoom = size.width + 2 * gap;
90
+ const side = bounds.right - x >= horizontalRoom ? "right"
91
+ : x - bounds.left >= horizontalRoom ? "left"
92
+ : y - bounds.top < bounds.bottom - y ? "bottom" : "top";
93
+ const { left, top } = placeOverlay(anchor, size, bounds, side, "center", gap);
94
+ panel.style.left = `${(left - bounds.left) / scale}px`;
95
+ panel.style.top = `${(top - bounds.top) / scale}px`;
96
+ panel.dataset.positioned = "";
97
+ };
98
+ positionTooltip();
99
+ if (typeof ResizeObserver === "undefined")
100
+ return;
101
+ const observer = new ResizeObserver(positionTooltip);
102
+ observer.observe(frame);
103
+ observer.observe(panel);
104
+ return () => observer.disconnect();
105
+ }, [active, inspecting, item, formatValue, floor, ceiling]);
106
+ const navigate = (event) => {
107
+ let next;
108
+ switch (event.key) {
109
+ case "ArrowRight":
110
+ case "ArrowUp":
111
+ next = active + 1;
112
+ break;
113
+ case "ArrowLeft":
114
+ case "ArrowDown":
115
+ next = active - 1;
116
+ break;
117
+ case "Home":
118
+ next = 0;
119
+ break;
120
+ case "End":
121
+ next = last;
122
+ break;
123
+ case "Escape":
124
+ if (inspecting)
125
+ event.preventDefault();
126
+ setHover(undefined);
127
+ setTouch(undefined);
128
+ setFocused(false);
129
+ return;
130
+ default: return;
131
+ }
132
+ event.preventDefault();
133
+ setHover(undefined);
134
+ setTouch(undefined);
135
+ setFocused(true);
136
+ setCursor(Math.max(0, Math.min(last, next)));
137
+ };
138
+ return _jsx("div", { ...props, ref: ref, className: ["flood-box-plot", className].filter(Boolean).join(" "), role: "group", "aria-label": label, children: hasData ? _jsxs(_Fragment, { children: [last > 0 && _jsx(VisuallyHidden, { id: instructions, children: navigationLabel }), _jsxs("div", { ref: plot, className: "flood-box-plot__plot", dir: "ltr", role: last > 0 ? "slider" : "img", tabIndex: last > 0 ? 0 : undefined, "aria-label": last > 0 ? label : `${label}. ${valueText}`, "aria-describedby": last > 0 ? instructions : undefined, "aria-valuemin": last > 0 ? 1 : undefined, "aria-valuemax": last > 0 ? items.length : undefined, "aria-valuenow": last > 0 ? active + 1 : undefined, "aria-valuetext": last > 0 ? valueText : undefined, onFocus: event => setFocused(event.currentTarget.matches(":focus-visible")), onBlur: () => setFocused(false), onKeyDown: navigate, onPointerLeave: () => setHover(undefined), children: [_jsx("div", { className: "flood-box-plot__rows", "aria-hidden": "true", children: items.map(({ id, label: name, summary: values, outliers }, index) => _jsxs("div", { className: "flood-box-plot__row", "data-active": inspecting && index === active || undefined, onPointerEnter: event => { if (event.pointerType !== "touch")
139
+ setHover(index); }, onPointerUp: event => { if (event.pointerType === "touch")
140
+ setTouch(previous => previous === index ? undefined : index); }, children: [_jsx(Text, { className: "flood-box-plot__category", size: "helper", tone: "subtle", dir: "auto", title: name, children: name }), _jsxs("div", { className: "flood-box-plot__track", children: [values && _jsxs(_Fragment, { children: [_jsx("span", { className: "flood-box-plot__whisker", "data-side": "start", style: { left: `${position(values.low)}%`, right: `${100 - position(values.q1)}%` } }), _jsx("span", { className: "flood-box-plot__whisker", "data-side": "end", style: { left: `${position(values.q3)}%`, right: `${100 - position(values.high)}%` } }), _jsx("span", { className: "flood-box-plot__box", style: { left: `${position(values.q1)}%`, right: `${100 - position(values.q3)}%` } }), _jsx("span", { className: "flood-box-plot__median", style: { left: `${position(values.median)}%` } }), outliers?.map((value, i) => _jsx("span", { className: "flood-box-plot__outlier", style: { left: `${position(value)}%` } }, i))] }), !values && _jsx("span", { className: "flood-box-plot__missing" })] })] }, id)) }), _jsxs("div", { className: "flood-box-plot__axis", "aria-hidden": "true", children: [_jsx(Text, { size: "helper", tone: "subtle", children: formatAxisValue(floor) }), floor !== ceiling && _jsxs(_Fragment, { children: [_jsx(Text, { size: "helper", tone: "subtle", children: formatAxisValue(floor / 2 + ceiling / 2) }), _jsx(Text, { size: "helper", tone: "subtle", children: formatAxisValue(ceiling) })] })] }), inspecting && item && _jsxs(Surface, { ref: tooltip, className: "flood-chart__tooltip flood-box-plot__tooltip", "aria-hidden": "true", padding: "2", elevation: "raised", children: [_jsx(Text, { size: "helper", dir: "auto", children: item.label }), summary ? tooltipRows.map(([start, end]) => {
141
+ const first = readouts[start];
142
+ if (!first)
143
+ return null;
144
+ const last = end === undefined ? undefined : readouts[end];
145
+ return _jsxs("div", { className: "flood-box-plot__reading", children: [_jsx(Text, { size: "helper", tone: "subtle", dir: "auto", children: start === 0 ? statisticLabels?.whiskers ?? "Whiskers" :
146
+ last ? `${first.label}–${last.label}` : first.label }), _jsx(Text, { size: "helper", tone: start === 2 ? "default" : "subtle", children: last ? _jsxs(_Fragment, { children: [_jsxs("span", { children: [first.value, "\u2013"] }), _jsx("wbr", {}), _jsx("span", { children: last.value })] }) : first.value })] }, start);
147
+ }) : _jsx(Text, { size: "helper", tone: "subtle", children: missingLabel })] })] })] }) : _jsx(Text, { className: "flood-box-plot__empty", size: "compact", tone: "subtle", children: emptyLabel }) });
148
+ });
@@ -0,0 +1,2 @@
1
+ import { type BulletChartProps } from "./index.js";
2
+ export declare const BulletPlot: import("react").ForwardRefExoticComponent<BulletChartProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { Inline, Meter, Stack, Text } from "./index.js";
4
+ const defaultNumber = new Intl.NumberFormat("en", { maximumSignificantDigits: 21 });
5
+ export const BulletPlot = /* @__PURE__ */ forwardRef(function BulletPlot({ label, items, max, formatValue = defaultNumber.format, valueLabel = "Actual", targetLabel = "Target", emptyLabel = "No data", className, ...props }, ref) {
6
+ const ids = new Set();
7
+ let largest = 0;
8
+ for (const { id, label: itemLabel, value, target } of items) {
9
+ if (!id.trim() || ids.has(id) || !itemLabel.trim()) {
10
+ throw new Error("Flood BulletChart needs unique IDs and nonempty row labels");
11
+ }
12
+ if (!Number.isFinite(value) || value < 0 || !Number.isFinite(target) || target < 0) {
13
+ throw new RangeError("Flood BulletChart actual and target values must be finite and nonnegative");
14
+ }
15
+ ids.add(id);
16
+ largest = Math.max(largest, value, target);
17
+ }
18
+ const limit = max ?? (largest || 1);
19
+ if (!Number.isFinite(limit) || limit <= 0 || limit < largest) {
20
+ throw new RangeError("Flood BulletChart max must be finite, positive and at least the largest actual or target");
21
+ }
22
+ return _jsx("div", { ...props, ref: ref, className: ["flood-bullet-chart", className].filter(Boolean).join(" "), role: "group", "aria-label": label, children: items.length ? _jsx("ul", { className: "flood-bullet-chart__items", role: "list", children: items.map(item => {
23
+ const actual = formatValue(item.value);
24
+ const target = formatValue(item.target);
25
+ return _jsx("li", { children: _jsxs(Stack, { gap: "2", children: [_jsxs(Inline, { gap: "2", align: "baseline", className: "flood-bullet-chart__caption", "aria-hidden": "true", children: [_jsx(Text, { size: "compact", children: item.label }), _jsxs(Inline, { gap: "2", align: "baseline", className: "flood-bullet-chart__numbers", children: [_jsx(Text, { size: "compact", className: "flood-bullet-chart__actual", children: actual }), _jsxs(Text, { size: "helper", tone: "subtle", children: [targetLabel, " ", target] })] })] }), _jsxs("div", { className: "flood-bullet-chart__plot", "data-zero": item.value === 0 || undefined, children: [_jsx(Meter, { label: item.label, value: item.value, max: limit, tone: "accent", hideLabel: true, valueText: `${valueLabel}: ${actual}, ${targetLabel}: ${target}` }), _jsx("span", { "aria-hidden": "true", className: "flood-bullet-chart__target", style: { "--flood-bullet-target": `${item.target / limit * 100}%` } })] })] }) }, item.id);
26
+ }) }) : _jsx(Text, { size: "compact", tone: "subtle", children: emptyLabel }) });
27
+ });
@@ -0,0 +1,6 @@
1
+ import { type BarChartProps, type BarListProps, type DonutChartProps, type HeatmapProps, type LineChartProps } from "./index.js";
2
+ export declare const SeriesPlot: import("react").ForwardRefExoticComponent<LineChartProps & Required<Pick<LineChartProps, "curve" | "emptyLabel" | "missingLabel" | "navigationLabel" | "variant">> & import("react").RefAttributes<HTMLDivElement>>;
3
+ export declare const GroupedBars: import("react").ForwardRefExoticComponent<BarChartProps & Required<Pick<BarChartProps, "emptyLabel" | "missingLabel" | "navigationLabel">> & import("react").RefAttributes<HTMLDivElement>>;
4
+ export declare const ShareRing: import("react").ForwardRefExoticComponent<DonutChartProps & Required<Pick<DonutChartProps, "emptyLabel" | "navigationLabel" | "totalLabel">> & import("react").RefAttributes<HTMLDivElement>>;
5
+ export declare const HeatmapGrid: import("react").ForwardRefExoticComponent<HeatmapProps & Required<Pick<HeatmapProps, "emptyLabel" | "missingLabel">> & import("react").RefAttributes<HTMLDivElement>>;
6
+ export declare const RankedBars: import("react").ForwardRefExoticComponent<BarListProps & import("react").RefAttributes<HTMLDivElement>>;