@godxjp/ui 32.14.0 → 33.0.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/README.md +4 -4
- package/agent/START-HERE.md +7 -7
- package/agent/components/HeatmapChart.json +97 -0
- package/agent/components/Tabs.json +1 -1
- package/agent/components/Toolbar.json +2 -9
- package/agent/components-index.json +5 -0
- package/agent/components.json +100 -10
- package/agent/index.json +7 -7
- package/agent/llms.txt +6 -6
- package/agent/tokens.json +114 -6
- package/dist/components/charts/chart-summary.d.ts +32 -1
- package/dist/components/charts/chart-summary.js +49 -0
- package/dist/components/charts/heatmap-chart.d.ts +19 -0
- package/dist/components/charts/heatmap-chart.js +293 -0
- package/dist/components/charts/index.d.ts +3 -0
- package/dist/components/charts/index.js +2 -0
- package/dist/components/feedback/tooltip.d.ts +9 -0
- package/dist/components/feedback/tooltip.js +4 -1
- package/dist/components/navigation/index.d.ts +1 -1
- package/dist/components/navigation/toolbar.d.ts +2 -2
- package/dist/components/navigation/toolbar.js +0 -3
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +8 -1
- package/dist/i18n/messages/ja.json +8 -1
- package/dist/i18n/messages/vi.json +8 -1
- package/dist/props/components/charts.prop.d.ts +58 -0
- package/dist/props/components/navigation.prop.d.ts +10 -12
- package/dist/props/registry.d.ts +37 -5
- package/dist/props/registry.js +36 -5
- package/dist/styles/chart-layout.css +161 -0
- package/dist/styles/layers.json +8 -1
- package/dist/styles/layout.css +0 -33
- package/dist/styles.css +1 -1
- package/dist/tokens/components/chart.css +27 -0
- package/dist/tokens/components/navigation.css +0 -2
- package/dist/tokens/foundation.css +12 -0
- package/docs/migrations/v33.md +60 -0
- package/package.json +6 -2
- package/scripts/ui-audit.mjs +13 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* alternative (WCAG 1.1.1). No JSX — filename pascal is intentionally NOT
|
|
4
4
|
* exported, so this is not a catalog primitive.
|
|
5
5
|
*/
|
|
6
|
-
import type { ChartDatum, ChartSeriesProp } from "../../props/components/charts.prop.js";
|
|
6
|
+
import type { ChartDatum, ChartSeriesProp, HeatmapCategory, HeatmapDatum } from "../../props/components/charts.prop.js";
|
|
7
7
|
type Formatter = Intl.NumberFormat;
|
|
8
8
|
export type ChartSummary = {
|
|
9
9
|
rows: string[];
|
|
@@ -39,4 +39,35 @@ export declare function trendRatios(data: ChartDatum[], valueKey: string): numbe
|
|
|
39
39
|
* datum).
|
|
40
40
|
*/
|
|
41
41
|
export declare function resolveEmphasizedIndex(index: number | undefined, length: number): number;
|
|
42
|
+
/** Number of steps in the `--chart-heat-1..N` ramp. */
|
|
43
|
+
export declare const HEAT_STEPS = 5;
|
|
44
|
+
/** The lookup key of one heatmap cell. */
|
|
45
|
+
export declare function heatmapCellKey(row: string | number, column: string | number): string;
|
|
46
|
+
export type HeatmapModel = {
|
|
47
|
+
/** Finite values by cell key. A key that is absent is an EMPTY cell, never a zero. */
|
|
48
|
+
values: Map<string, number>;
|
|
49
|
+
/** Ramp step 1..HEAT_STEPS by cell key. */
|
|
50
|
+
steps: Map<string, number>;
|
|
51
|
+
/** 1-based rank of the top-N cells by cell key. */
|
|
52
|
+
ranks: Map<string, number>;
|
|
53
|
+
/** Scale domain: `min(0, smallest)` .. largest. */
|
|
54
|
+
min: number;
|
|
55
|
+
max: number;
|
|
56
|
+
/** Count of cells with a value inside the rows × columns matrix. */
|
|
57
|
+
present: number;
|
|
58
|
+
/** Count of matrix cells with no value. */
|
|
59
|
+
empty: number;
|
|
60
|
+
/** The highest cell, for the one-line summary. */
|
|
61
|
+
highest: {
|
|
62
|
+
row: HeatmapCategory;
|
|
63
|
+
column: HeatmapCategory;
|
|
64
|
+
value: number;
|
|
65
|
+
} | null;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Bin a heatmap's cells onto the sequential ramp. The domain starts at zero (or the smallest value,
|
|
69
|
+
* when that is negative) so the fill encodes MAGNITUDE — a heatmap of 900..1000 whose 900 painted
|
|
70
|
+
* as "empty-light" would read as a quiet hour. Cells outside `rows` × `columns` are ignored.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildHeatmapModel(rows: HeatmapCategory[], columns: HeatmapCategory[], data: HeatmapDatum[], topN?: number): HeatmapModel;
|
|
42
73
|
export {};
|
|
@@ -40,11 +40,60 @@ function resolveEmphasizedIndex(index, length) {
|
|
|
40
40
|
const resolved = index < 0 ? length + index : index;
|
|
41
41
|
return resolved >= 0 && resolved < length ? resolved : -1;
|
|
42
42
|
}
|
|
43
|
+
const HEAT_STEPS = 5;
|
|
44
|
+
function heatmapCellKey(row, column) {
|
|
45
|
+
return `${String(row)}\0${String(column)}`;
|
|
46
|
+
}
|
|
47
|
+
function buildHeatmapModel(rows, columns, data, topN) {
|
|
48
|
+
const rowByKey = new Map(rows.map((row) => [String(row.key), row]));
|
|
49
|
+
const columnByKey = new Map(columns.map((column) => [String(column.key), column]));
|
|
50
|
+
const values = /* @__PURE__ */ new Map();
|
|
51
|
+
const order = [];
|
|
52
|
+
for (const datum of data) {
|
|
53
|
+
const row = rowByKey.get(String(datum.row));
|
|
54
|
+
const column = columnByKey.get(String(datum.column));
|
|
55
|
+
if (!row || !column) continue;
|
|
56
|
+
if (typeof datum.value !== "number" || !Number.isFinite(datum.value)) continue;
|
|
57
|
+
const key = heatmapCellKey(row.key, column.key);
|
|
58
|
+
if (values.has(key)) continue;
|
|
59
|
+
values.set(key, datum.value);
|
|
60
|
+
order.push({ key, value: datum.value, row, column });
|
|
61
|
+
}
|
|
62
|
+
let min = 0;
|
|
63
|
+
let max = 0;
|
|
64
|
+
for (const { value } of order) {
|
|
65
|
+
if (value < min) min = value;
|
|
66
|
+
if (value > max) max = value;
|
|
67
|
+
}
|
|
68
|
+
const steps = /* @__PURE__ */ new Map();
|
|
69
|
+
for (const { key, value } of order) {
|
|
70
|
+
const ratio = max > min ? (value - min) / (max - min) : max > 0 ? 1 : 0;
|
|
71
|
+
steps.set(key, Math.min(HEAT_STEPS, Math.floor(ratio * HEAT_STEPS) + 1));
|
|
72
|
+
}
|
|
73
|
+
const ranked = [...order].sort((a, b) => b.value - a.value);
|
|
74
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
75
|
+
const n = topN !== void 0 && Number.isInteger(topN) && topN > 0 ? topN : 0;
|
|
76
|
+
ranked.slice(0, n).forEach((cell, index) => ranks.set(cell.key, index + 1));
|
|
77
|
+
const top = ranked[0];
|
|
78
|
+
return {
|
|
79
|
+
values,
|
|
80
|
+
steps,
|
|
81
|
+
ranks,
|
|
82
|
+
min,
|
|
83
|
+
max,
|
|
84
|
+
present: order.length,
|
|
85
|
+
empty: rows.length * columns.length - order.length,
|
|
86
|
+
highest: top ? { row: top.row, column: top.column, value: top.value } : null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
43
89
|
export {
|
|
90
|
+
HEAT_STEPS,
|
|
44
91
|
buildCartesianSummary,
|
|
92
|
+
buildHeatmapModel,
|
|
45
93
|
buildPieSummary,
|
|
46
94
|
buildSeriesRows,
|
|
47
95
|
buildTrendSummary,
|
|
96
|
+
heatmapCellKey,
|
|
48
97
|
resolveEmphasizedIndex,
|
|
49
98
|
trendRatios
|
|
50
99
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { HeatmapChartProp } from "../../props/components/charts.prop.js";
|
|
3
|
+
export type { HeatmapChartProp, HeatmapChartProp as HeatmapChartProps, HeatmapCategory, HeatmapDatum, } from "../../props/components/charts.prop.js";
|
|
4
|
+
/**
|
|
5
|
+
* HeatmapChart — a **dependency-free** rows × columns matrix (weekday × hour revenue, order counts
|
|
6
|
+
* per slot). Cell fill steps through the sequential `--chart-heat-1..5` ramp.
|
|
7
|
+
*
|
|
8
|
+
* - **One structure for sight and for AT.** The matrix IS a native data table (`<th scope>` headers,
|
|
9
|
+
* one cell per slot) with `role="grid"`, so the screen-reader text alternative is the same element
|
|
10
|
+
* the eye reads — no second, drifting copy. Each cell carries its formatted value as text.
|
|
11
|
+
* - **Keyboard** — WAI-ARIA APG data grid: one tab stop (roving tabindex), arrows move between
|
|
12
|
+
* cells (mirrored under RTL), Home/End go to the row's ends, Ctrl+Home/End to the first/last
|
|
13
|
+
* cell. A focused or hovered cell shows ONE shared tooltip; Escape dismisses it.
|
|
14
|
+
* - **Never colour alone** — an empty cell is outlined (dashed) and reads "no data"; a zero is a
|
|
15
|
+
* filled step-1 cell that reads "0"; a top-N cell wears a ring and reads "top N".
|
|
16
|
+
* - **Narrow screens** — the matrix never shrinks a cell below its `size` tier; it scrolls inside
|
|
17
|
+
* its own box with the row header pinned, so the page never scrolls sideways.
|
|
18
|
+
*/
|
|
19
|
+
export declare function HeatmapChart({ rows, columns, data, label, showCaption, description, size, numberFormat, topN, showLegend, emptyMessage, className, id, ref, }: HeatmapChartProp): React.JSX.Element;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
|
+
import { EmptyState } from "../data-display/empty-state.js";
|
|
6
|
+
import { Tooltip, TooltipAnchorContext, TooltipContent } from "../feedback/tooltip.js";
|
|
7
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
8
|
+
import { useChartNumberFormat } from "./chart-frame.js";
|
|
9
|
+
import { HEAT_STEPS, buildHeatmapModel, heatmapCellKey } from "./chart-summary.js";
|
|
10
|
+
const OPEN_DELAY = 200;
|
|
11
|
+
const CLOSE_GRACE = 100;
|
|
12
|
+
const RAMP = Array.from({ length: HEAT_STEPS }, (_, index) => index + 1);
|
|
13
|
+
function HeatmapChart({
|
|
14
|
+
rows,
|
|
15
|
+
columns,
|
|
16
|
+
data,
|
|
17
|
+
label,
|
|
18
|
+
showCaption = true,
|
|
19
|
+
description,
|
|
20
|
+
size = "md",
|
|
21
|
+
numberFormat,
|
|
22
|
+
topN,
|
|
23
|
+
showLegend = true,
|
|
24
|
+
emptyMessage,
|
|
25
|
+
className,
|
|
26
|
+
id,
|
|
27
|
+
ref
|
|
28
|
+
}) {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
const fmt = useChartNumberFormat(numberFormat);
|
|
31
|
+
const countFmt = useChartNumberFormat();
|
|
32
|
+
const model = React.useMemo(
|
|
33
|
+
() => buildHeatmapModel(rows, columns, data, topN),
|
|
34
|
+
[rows, columns, data, topN]
|
|
35
|
+
);
|
|
36
|
+
const hasData = rows.length > 0 && columns.length > 0 && model.present > 0;
|
|
37
|
+
const reactId = React.useId();
|
|
38
|
+
const baseId = id ?? reactId;
|
|
39
|
+
const titleId = `${baseId}-title`;
|
|
40
|
+
const descId = `${baseId}-desc`;
|
|
41
|
+
const [active, setActive] = React.useState([0, 0]);
|
|
42
|
+
const activeRow = Math.min(active[0], Math.max(0, rows.length - 1));
|
|
43
|
+
const activeColumn = Math.min(active[1], Math.max(0, columns.length - 1));
|
|
44
|
+
const scrollRef = React.useRef(null);
|
|
45
|
+
const cellRefs = React.useRef(/* @__PURE__ */ new Map());
|
|
46
|
+
const anchorRef = React.useRef(null);
|
|
47
|
+
const timer = React.useRef(void 0);
|
|
48
|
+
const [tip, setTip] = React.useState(null);
|
|
49
|
+
React.useEffect(() => () => clearTimeout(timer.current), []);
|
|
50
|
+
const showTip = (r, c, node, delay) => {
|
|
51
|
+
clearTimeout(timer.current);
|
|
52
|
+
timer.current = setTimeout(() => {
|
|
53
|
+
anchorRef.current = node;
|
|
54
|
+
setTip([r, c]);
|
|
55
|
+
}, delay);
|
|
56
|
+
};
|
|
57
|
+
const hideTip = () => {
|
|
58
|
+
clearTimeout(timer.current);
|
|
59
|
+
timer.current = setTimeout(() => setTip(null), CLOSE_GRACE);
|
|
60
|
+
};
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
if (!tip) return void 0;
|
|
63
|
+
const onKeyDown = (event) => {
|
|
64
|
+
if (event.key === "Escape") {
|
|
65
|
+
clearTimeout(timer.current);
|
|
66
|
+
setTip(null);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
document.addEventListener("keydown", onKeyDown);
|
|
70
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
71
|
+
}, [tip]);
|
|
72
|
+
const reveal = (cell) => {
|
|
73
|
+
const scroller = scrollRef.current;
|
|
74
|
+
const header = cell.parentElement?.querySelector("th");
|
|
75
|
+
if (!scroller || !header) return;
|
|
76
|
+
const cellBox = cell.getBoundingClientRect();
|
|
77
|
+
const headerBox = header.getBoundingClientRect();
|
|
78
|
+
if (getComputedStyle(scroller).direction === "rtl") {
|
|
79
|
+
if (cellBox.right > headerBox.left) scroller.scrollLeft += cellBox.right - headerBox.left;
|
|
80
|
+
} else if (cellBox.left < headerBox.right) {
|
|
81
|
+
scroller.scrollLeft -= headerBox.right - cellBox.left;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const moveTo = (r, c) => {
|
|
85
|
+
const row = Math.max(0, Math.min(rows.length - 1, r));
|
|
86
|
+
const column = Math.max(0, Math.min(columns.length - 1, c));
|
|
87
|
+
setActive([row, column]);
|
|
88
|
+
const cell = cellRefs.current.get(`${row}:${column}`);
|
|
89
|
+
if (cell) {
|
|
90
|
+
cell.focus();
|
|
91
|
+
reveal(cell);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const onGridKeyDown = (event) => {
|
|
95
|
+
const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
|
|
96
|
+
const lastRow = rows.length - 1;
|
|
97
|
+
const lastColumn = columns.length - 1;
|
|
98
|
+
const ctrl = event.ctrlKey || event.metaKey;
|
|
99
|
+
switch (event.key) {
|
|
100
|
+
case "ArrowRight":
|
|
101
|
+
moveTo(activeRow, activeColumn + (rtl ? -1 : 1));
|
|
102
|
+
break;
|
|
103
|
+
case "ArrowLeft":
|
|
104
|
+
moveTo(activeRow, activeColumn + (rtl ? 1 : -1));
|
|
105
|
+
break;
|
|
106
|
+
case "ArrowDown":
|
|
107
|
+
moveTo(activeRow + 1, activeColumn);
|
|
108
|
+
break;
|
|
109
|
+
case "ArrowUp":
|
|
110
|
+
moveTo(activeRow - 1, activeColumn);
|
|
111
|
+
break;
|
|
112
|
+
case "Home":
|
|
113
|
+
moveTo(ctrl ? 0 : activeRow, 0);
|
|
114
|
+
break;
|
|
115
|
+
case "End":
|
|
116
|
+
moveTo(ctrl ? lastRow : activeRow, lastColumn);
|
|
117
|
+
break;
|
|
118
|
+
case "Escape":
|
|
119
|
+
clearTimeout(timer.current);
|
|
120
|
+
setTip(null);
|
|
121
|
+
return;
|
|
122
|
+
default:
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
};
|
|
127
|
+
const cellText = (r, c) => {
|
|
128
|
+
const key = heatmapCellKey(rows[r].key, columns[c].key);
|
|
129
|
+
const value = model.values.get(key);
|
|
130
|
+
const rank = model.ranks.get(key);
|
|
131
|
+
return {
|
|
132
|
+
value: value === void 0 ? t("chart.heatmapNoData") : fmt.format(value),
|
|
133
|
+
top: rank === void 0 ? void 0 : t("chart.heatmapTop", { rank: countFmt.format(rank) })
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
const summary = model.highest ? `${t("chart.heatmapSummary", {
|
|
137
|
+
rows: countFmt.format(rows.length),
|
|
138
|
+
columns: countFmt.format(columns.length)
|
|
139
|
+
})} ${t("chart.heatmapHighest", {
|
|
140
|
+
value: fmt.format(model.highest.value),
|
|
141
|
+
row: model.highest.row.label,
|
|
142
|
+
column: model.highest.column.label
|
|
143
|
+
})}` : "";
|
|
144
|
+
const tipText = tip && tip[0] < rows.length && tip[1] < columns.length ? cellText(tip[0], tip[1]) : null;
|
|
145
|
+
const span = model.max - model.min;
|
|
146
|
+
const bound = (step) => model.min + span * step / HEAT_STEPS;
|
|
147
|
+
return /* @__PURE__ */ jsxs(
|
|
148
|
+
"figure",
|
|
149
|
+
{
|
|
150
|
+
ref,
|
|
151
|
+
id,
|
|
152
|
+
"data-slot": "chart",
|
|
153
|
+
"data-size": size,
|
|
154
|
+
className: cn("ui-chart ui-chart-heatmap", className),
|
|
155
|
+
"aria-labelledby": titleId,
|
|
156
|
+
"aria-describedby": descId,
|
|
157
|
+
children: [
|
|
158
|
+
/* @__PURE__ */ jsx("figcaption", { id: titleId, className: showCaption ? "ui-chart-title" : "sr-only", children: label }),
|
|
159
|
+
hasData ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
160
|
+
/* @__PURE__ */ jsx("div", { ref: scrollRef, className: "ui-chart-heatmap-scroll", children: /* @__PURE__ */ jsxs(
|
|
161
|
+
"table",
|
|
162
|
+
{
|
|
163
|
+
role: "grid",
|
|
164
|
+
"aria-labelledby": titleId,
|
|
165
|
+
className: "ui-chart-heatmap-grid",
|
|
166
|
+
onKeyDown: onGridKeyDown,
|
|
167
|
+
children: [
|
|
168
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
169
|
+
/* @__PURE__ */ jsx("td", { className: "ui-chart-heatmap-corner" }),
|
|
170
|
+
columns.map((column) => /* @__PURE__ */ jsx(
|
|
171
|
+
"th",
|
|
172
|
+
{
|
|
173
|
+
scope: "col",
|
|
174
|
+
className: "ui-chart-heatmap-column-header",
|
|
175
|
+
children: column.label
|
|
176
|
+
},
|
|
177
|
+
String(column.key)
|
|
178
|
+
))
|
|
179
|
+
] }) }),
|
|
180
|
+
/* @__PURE__ */ jsx("tbody", { children: rows.map((row, r) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
181
|
+
/* @__PURE__ */ jsx("th", { scope: "row", className: "ui-chart-heatmap-row-header", children: row.label }),
|
|
182
|
+
columns.map((column, c) => {
|
|
183
|
+
const key = heatmapCellKey(row.key, column.key);
|
|
184
|
+
const empty = !model.values.has(key);
|
|
185
|
+
const text = cellText(r, c);
|
|
186
|
+
return /* @__PURE__ */ jsx(
|
|
187
|
+
"td",
|
|
188
|
+
{
|
|
189
|
+
role: "gridcell",
|
|
190
|
+
ref: (node) => {
|
|
191
|
+
if (node) cellRefs.current.set(`${r}:${c}`, node);
|
|
192
|
+
else cellRefs.current.delete(`${r}:${c}`);
|
|
193
|
+
},
|
|
194
|
+
tabIndex: r === activeRow && c === activeColumn ? 0 : -1,
|
|
195
|
+
className: "ui-chart-heatmap-cell ui-focus-ring",
|
|
196
|
+
"data-step": empty ? void 0 : model.steps.get(key),
|
|
197
|
+
"data-empty": empty ? "" : void 0,
|
|
198
|
+
"data-emphasized": model.ranks.has(key) ? "" : void 0,
|
|
199
|
+
onFocus: (event) => {
|
|
200
|
+
setActive([r, c]);
|
|
201
|
+
showTip(r, c, event.currentTarget, 0);
|
|
202
|
+
},
|
|
203
|
+
onBlur: hideTip,
|
|
204
|
+
onPointerEnter: (event) => showTip(r, c, event.currentTarget, OPEN_DELAY),
|
|
205
|
+
onPointerLeave: hideTip,
|
|
206
|
+
children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: text.top ? `${text.value}, ${text.top}` : text.value })
|
|
207
|
+
},
|
|
208
|
+
String(column.key)
|
|
209
|
+
);
|
|
210
|
+
})
|
|
211
|
+
] }, String(row.key))) })
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
) }),
|
|
215
|
+
showLegend ? /* @__PURE__ */ jsxs("div", { className: "ui-chart-heatmap-legend", children: [
|
|
216
|
+
/* @__PURE__ */ jsxs("div", { className: "ui-chart-heatmap-legend-scale", children: [
|
|
217
|
+
/* @__PURE__ */ jsx("span", { className: "ui-chart-heatmap-legend-label", "aria-hidden": "true", children: fmt.format(model.min) }),
|
|
218
|
+
/* @__PURE__ */ jsx(
|
|
219
|
+
"ul",
|
|
220
|
+
{
|
|
221
|
+
role: "list",
|
|
222
|
+
className: "ui-chart-heatmap-legend-ramp",
|
|
223
|
+
"aria-label": t("chart.heatmapLegend"),
|
|
224
|
+
children: RAMP.map((step) => /* @__PURE__ */ jsx("li", { className: "ui-chart-heatmap-legend-swatch", "data-step": step, children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: fmt.formatRange(bound(step - 1), bound(step)) }) }, step))
|
|
225
|
+
}
|
|
226
|
+
),
|
|
227
|
+
/* @__PURE__ */ jsx("span", { className: "ui-chart-heatmap-legend-label", "aria-hidden": "true", children: fmt.format(model.max) })
|
|
228
|
+
] }),
|
|
229
|
+
model.empty > 0 ? /* @__PURE__ */ jsxs("span", { className: "ui-chart-heatmap-legend-item", children: [
|
|
230
|
+
/* @__PURE__ */ jsx(
|
|
231
|
+
"span",
|
|
232
|
+
{
|
|
233
|
+
className: "ui-chart-heatmap-legend-swatch",
|
|
234
|
+
"data-empty": "",
|
|
235
|
+
"aria-hidden": "true"
|
|
236
|
+
}
|
|
237
|
+
),
|
|
238
|
+
t("chart.heatmapNoData")
|
|
239
|
+
] }) : null,
|
|
240
|
+
model.ranks.size > 0 ? /* @__PURE__ */ jsxs("span", { className: "ui-chart-heatmap-legend-item", children: [
|
|
241
|
+
/* @__PURE__ */ jsx(
|
|
242
|
+
"span",
|
|
243
|
+
{
|
|
244
|
+
className: "ui-chart-heatmap-legend-swatch",
|
|
245
|
+
"data-step": HEAT_STEPS,
|
|
246
|
+
"data-emphasized": "",
|
|
247
|
+
"aria-hidden": "true"
|
|
248
|
+
}
|
|
249
|
+
),
|
|
250
|
+
t("chart.heatmapLegendTop", { count: countFmt.format(model.ranks.size) })
|
|
251
|
+
] }) : null
|
|
252
|
+
] }) : null
|
|
253
|
+
] }) : /* @__PURE__ */ jsx(EmptyState, { title: emptyMessage ?? t("chart.empty") }),
|
|
254
|
+
/* @__PURE__ */ jsxs("p", { id: descId, className: "sr-only", children: [
|
|
255
|
+
description ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
256
|
+
description,
|
|
257
|
+
" "
|
|
258
|
+
] }) : null,
|
|
259
|
+
hasData ? summary : null
|
|
260
|
+
] }),
|
|
261
|
+
/* @__PURE__ */ jsx(
|
|
262
|
+
Tooltip,
|
|
263
|
+
{
|
|
264
|
+
open: tip !== null,
|
|
265
|
+
onOpenChange: (next) => {
|
|
266
|
+
if (!next) setTip(null);
|
|
267
|
+
},
|
|
268
|
+
children: /* @__PURE__ */ jsx(TooltipAnchorContext.Provider, { value: anchorRef, children: tip && tipText ? /* @__PURE__ */ jsxs(
|
|
269
|
+
TooltipContent,
|
|
270
|
+
{
|
|
271
|
+
className: "ui-chart-heatmap-tooltip",
|
|
272
|
+
onPointerEnter: () => clearTimeout(timer.current),
|
|
273
|
+
onPointerLeave: hideTip,
|
|
274
|
+
children: [
|
|
275
|
+
/* @__PURE__ */ jsx("span", { className: "ui-chart-heatmap-tooltip-title", children: t("chart.heatmapCell", {
|
|
276
|
+
row: rows[tip[0]]?.label ?? "",
|
|
277
|
+
column: columns[tip[1]]?.label ?? "",
|
|
278
|
+
value: tipText.value
|
|
279
|
+
}) }),
|
|
280
|
+
tipText.top ? /* @__PURE__ */ jsx("span", { className: "ui-chart-heatmap-tooltip-note", children: tipText.top }) : null
|
|
281
|
+
]
|
|
282
|
+
},
|
|
283
|
+
`${tip[0]}:${tip[1]}`
|
|
284
|
+
) : null })
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
export {
|
|
292
|
+
HeatmapChart
|
|
293
|
+
};
|
|
@@ -9,6 +9,9 @@ export type { BarChartProp, BarChartProps } from "./bar-chart.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export { CompactBarTrend } from "./compact-bar-trend.js";
|
|
11
11
|
export type { CompactBarTrendProp, CompactBarTrendProps } from "./compact-bar-trend.js";
|
|
12
|
+
/** Dependency-free rows × columns heatmap — isolated entry `@godxjp/ui/charts/heatmap-chart`. */
|
|
13
|
+
export { HeatmapChart } from "./heatmap-chart.js";
|
|
14
|
+
export type { HeatmapChartProp, HeatmapChartProps, HeatmapCategory, HeatmapDatum, } from "./heatmap-chart.js";
|
|
12
15
|
export { AreaChart } from "./area-chart.js";
|
|
13
16
|
export type { AreaChartProp, AreaChartProps } from "./area-chart.js";
|
|
14
17
|
export { PieChart } from "./pie-chart.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LineChart } from "./line-chart.js";
|
|
2
2
|
import { BarChart } from "./bar-chart.js";
|
|
3
3
|
import { CompactBarTrend } from "./compact-bar-trend.js";
|
|
4
|
+
import { HeatmapChart } from "./heatmap-chart.js";
|
|
4
5
|
import { AreaChart } from "./area-chart.js";
|
|
5
6
|
import { PieChart } from "./pie-chart.js";
|
|
6
7
|
import { CHART_COLORS } from "./chart-frame.js";
|
|
@@ -9,6 +10,7 @@ export {
|
|
|
9
10
|
BarChart,
|
|
10
11
|
CHART_COLORS,
|
|
11
12
|
CompactBarTrend,
|
|
13
|
+
HeatmapChart,
|
|
12
14
|
LineChart,
|
|
13
15
|
PieChart
|
|
14
16
|
};
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
type Side = "top" | "right" | "bottom" | "left";
|
|
3
3
|
type Align = "start" | "center" | "end";
|
|
4
|
+
/**
|
|
5
|
+
* Anchors `TooltipContent` to an element that is NOT a `TooltipTrigger` (gh#1045). `TooltipTrigger`
|
|
6
|
+
* wraps its child in RAC's `Focusable`, which stamps `tabIndex=0` on it — right for a button, wrong for a truncated `Text` inside a `treeitem`, where it
|
|
7
|
+
* would add a second tab stop. The ellipsis tooltip drives `open` itself and hands its own element
|
|
8
|
+
* here as the positioning target (RAC `Tooltip`'s `triggerRef`). HeatmapChart (gh#1330) does the
|
|
9
|
+
* same with ONE tooltip for its whole grid, anchored to the active cell. Module-internal: not in
|
|
10
|
+
* the feedback index.
|
|
11
|
+
*/
|
|
12
|
+
export declare const TooltipAnchorContext: React.Context<React.RefObject<Element | null> | null>;
|
|
4
13
|
interface TooltipProviderProps {
|
|
5
14
|
/** Delay mở dùng chung cho mọi `Tooltip` bên trong, tính bằng ms. */
|
|
6
15
|
delayDuration?: number;
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
import { cn } from "../../lib/utils.js";
|
|
13
13
|
import { Slot } from "../../lib/slot.js";
|
|
14
14
|
const DEFAULT_DELAY_DURATION = 200;
|
|
15
|
-
const TooltipAnchorContext = React.createContext(
|
|
15
|
+
const TooltipAnchorContext = React.createContext(
|
|
16
|
+
null
|
|
17
|
+
);
|
|
16
18
|
function toPlacement(side, align) {
|
|
17
19
|
if (align === "center") return side;
|
|
18
20
|
if (side === "top" || side === "bottom") return `${side} ${align}`;
|
|
@@ -223,6 +225,7 @@ function EllipsisTooltip({
|
|
|
223
225
|
export {
|
|
224
226
|
EllipsisTooltip,
|
|
225
227
|
Tooltip,
|
|
228
|
+
TooltipAnchorContext,
|
|
226
229
|
TooltipContent,
|
|
227
230
|
TooltipProvider,
|
|
228
231
|
TooltipTrigger
|
|
@@ -2,7 +2,7 @@ export { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs.js";
|
|
|
2
2
|
export { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationEllipsis, PaginationPrevious, PaginationNext, } from "./pagination.js";
|
|
3
3
|
export { Steps } from "./steps.js";
|
|
4
4
|
export { FilterBar, FilterBarGroup, Toolbar, ToolbarGroup } from "./toolbar.js";
|
|
5
|
-
export type { FilterBarChipProp, FilterBarChipProps, FilterBarFilterProp, FilterBarFilterProps, FilterBarGroupProp, FilterBarGroupProps,
|
|
5
|
+
export type { FilterBarChipProp, FilterBarChipProps, FilterBarFilterProp, FilterBarFilterProps, FilterBarGroupProp, FilterBarGroupProps, FilterBarProp, FilterBarProps, FilterBarSearchProp, FilterBarSearchProps, ToolbarGroupProps, ToolbarProps, } from "./toolbar.js";
|
|
6
6
|
export { AppSettingPicker } from "./app-setting-picker.js";
|
|
7
7
|
export type { AppSettingPickerProp, AppSettingPickerProps, AppSettingPickerMenuProp, AppSettingPickerCycleProp, AppSettingKind, AppSettingCycleKind, } from "./app-setting-picker.js";
|
|
8
8
|
export type { BreadcrumbItemProp as BreadcrumbItem } from "../../props/vocabulary/navigation.prop.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolbarGroupProp, ToolbarProp } from "../../props/components/navigation.prop.js";
|
|
2
|
-
export type { FilterBarChipProp, FilterBarChipProp as FilterBarChipProps, FilterBarFilterProp, FilterBarFilterProp as FilterBarFilterProps,
|
|
3
|
-
export declare function Toolbar({ onClear, hasActiveFilters, sticky,
|
|
2
|
+
export type { FilterBarChipProp, FilterBarChipProp as FilterBarChipProps, FilterBarFilterProp, FilterBarFilterProp as FilterBarFilterProps, FilterBarSearchProp, FilterBarSearchProp as FilterBarSearchProps, ToolbarGroupProp, ToolbarGroupProp as ToolbarGroupProps, ToolbarGroupProp as FilterBarGroupProp, ToolbarGroupProp as FilterBarGroupProps, ToolbarProp, ToolbarProp as ToolbarProps, ToolbarProp as FilterBarProp, ToolbarProp as FilterBarProps, } from "../../props/components/navigation.prop.js";
|
|
3
|
+
export declare function Toolbar({ onClear, hasActiveFilters, sticky, search, filters, chips, onChipRemove, actions, resultCount, loading, disabled, error, clearDisplay, views, viewsPlacement, className, children, }: ToolbarProp): import("react").JSX.Element;
|
|
4
4
|
export declare function ToolbarGroup({ label, controlId, className, children }: ToolbarGroupProp): import("react").JSX.Element;
|
|
5
5
|
/** Canonical list-page filter strip. `Toolbar` remains available for backwards compatibility. */
|
|
6
6
|
export declare const FilterBar: typeof Toolbar;
|
|
@@ -13,7 +13,6 @@ function Toolbar({
|
|
|
13
13
|
onClear,
|
|
14
14
|
hasActiveFilters = true,
|
|
15
15
|
sticky = false,
|
|
16
|
-
overflow = "wrap",
|
|
17
16
|
search,
|
|
18
17
|
filters,
|
|
19
18
|
chips,
|
|
@@ -73,7 +72,6 @@ function Toolbar({
|
|
|
73
72
|
{
|
|
74
73
|
role: "toolbar",
|
|
75
74
|
"aria-label": t("navigation.toolbar.ariaLabel"),
|
|
76
|
-
"data-overflow": overflow,
|
|
77
75
|
className: cn("ui-toolbar", sticky && "ui-toolbar-sticky", className),
|
|
78
76
|
children: [
|
|
79
77
|
children,
|
|
@@ -98,7 +96,6 @@ function Toolbar({
|
|
|
98
96
|
role: "toolbar",
|
|
99
97
|
"aria-label": t("navigation.toolbar.ariaLabel"),
|
|
100
98
|
"aria-busy": loading || void 0,
|
|
101
|
-
"data-overflow": overflow,
|
|
102
99
|
className: "ui-toolbar",
|
|
103
100
|
children: [
|
|
104
101
|
views != null && viewsPlacement === "inline" && /* @__PURE__ */ jsx("div", { className: "ui-filter-bar-views", "data-placement": "inline", children: views }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "33.0.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -787,7 +787,14 @@
|
|
|
787
787
|
"summaryCartesian": "Chart with {series} series across {points} data points",
|
|
788
788
|
"summaryPie": "Pie chart with {slices} slices",
|
|
789
789
|
"summaryTrend": "Bar trend with {points} data points",
|
|
790
|
-
"trendCurrent": "current"
|
|
790
|
+
"trendCurrent": "current",
|
|
791
|
+
"heatmapSummary": "Heatmap, {rows} × {columns}",
|
|
792
|
+
"heatmapHighest": "Highest: {value} at {row}, {column}",
|
|
793
|
+
"heatmapNoData": "No data",
|
|
794
|
+
"heatmapTop": "Top {rank}",
|
|
795
|
+
"heatmapLegend": "Color scale",
|
|
796
|
+
"heatmapLegendTop": "Top {count}",
|
|
797
|
+
"heatmapCell": "{row}, {column}: {value}"
|
|
791
798
|
},
|
|
792
799
|
"rangeTimeline": {
|
|
793
800
|
"childRows": "Child rows",
|
|
@@ -761,7 +761,14 @@
|
|
|
761
761
|
"summaryCartesian": "{series}系列・{points}データ点のグラフ",
|
|
762
762
|
"summaryPie": "{slices}区分の円グラフ",
|
|
763
763
|
"summaryTrend": "{points}データ点の推移棒グラフ",
|
|
764
|
-
"trendCurrent": "現在"
|
|
764
|
+
"trendCurrent": "現在",
|
|
765
|
+
"heatmapSummary": "ヒートマップ({rows}行×{columns}列)",
|
|
766
|
+
"heatmapHighest": "最大 {value}({row}・{column})",
|
|
767
|
+
"heatmapNoData": "データなし",
|
|
768
|
+
"heatmapTop": "上位{rank}位",
|
|
769
|
+
"heatmapLegend": "色の濃さの凡例",
|
|
770
|
+
"heatmapLegendTop": "上位{count}件",
|
|
771
|
+
"heatmapCell": "{row}・{column}:{value}"
|
|
765
772
|
},
|
|
766
773
|
"rangeTimeline": {
|
|
767
774
|
"childRows": "子の行",
|
|
@@ -762,7 +762,14 @@
|
|
|
762
762
|
"summaryCartesian": "Biểu đồ gồm {series} chuỗi trên {points} điểm dữ liệu",
|
|
763
763
|
"summaryPie": "Biểu đồ tròn gồm {slices} phần",
|
|
764
764
|
"summaryTrend": "Biểu đồ cột xu hướng gồm {points} điểm dữ liệu",
|
|
765
|
-
"trendCurrent": "hiện tại"
|
|
765
|
+
"trendCurrent": "hiện tại",
|
|
766
|
+
"heatmapSummary": "Bản đồ nhiệt {rows} × {columns}",
|
|
767
|
+
"heatmapHighest": "Cao nhất: {value} tại {row}, {column}",
|
|
768
|
+
"heatmapNoData": "Không có dữ liệu",
|
|
769
|
+
"heatmapTop": "Hạng {rank}",
|
|
770
|
+
"heatmapLegend": "Thang màu",
|
|
771
|
+
"heatmapLegendTop": "{count} ô cao nhất",
|
|
772
|
+
"heatmapCell": "{row}, {column}: {value}"
|
|
766
773
|
},
|
|
767
774
|
"rangeTimeline": {
|
|
768
775
|
"childRows": "Hàng con",
|
|
@@ -138,6 +138,64 @@ export type CompactBarTrendProp = {
|
|
|
138
138
|
id?: IdProp;
|
|
139
139
|
ref?: React.Ref<HTMLElement>;
|
|
140
140
|
};
|
|
141
|
+
/** One category on a HeatmapChart axis — a row (weekday) or a column (hour). */
|
|
142
|
+
export type HeatmapCategory = {
|
|
143
|
+
/** Stable key the `data` cells refer to (`row` / `column`). */
|
|
144
|
+
key: string | number;
|
|
145
|
+
/** Already-translated display label — the axis tick and the table header. */
|
|
146
|
+
label: string;
|
|
147
|
+
};
|
|
148
|
+
/** One HeatmapChart cell. A cell absent from `data`, or with a non-finite `value`, is EMPTY. */
|
|
149
|
+
export type HeatmapDatum = {
|
|
150
|
+
/** `key` of the row this cell belongs to. */
|
|
151
|
+
row: string | number;
|
|
152
|
+
/** `key` of the column this cell belongs to. */
|
|
153
|
+
column: string | number;
|
|
154
|
+
/** The measured value. `null`/`undefined`/`NaN` mean "no data" — not zero. */
|
|
155
|
+
value: number | null | undefined;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* @see HeatmapChart
|
|
159
|
+
*
|
|
160
|
+
* Dependency-free (no `recharts`) rows × columns matrix — a weekday × hour heatmap. Cell intensity
|
|
161
|
+
* steps through the `--chart-heat-1..5` ramp; an empty cell is outlined, a zero cell is filled.
|
|
162
|
+
*/
|
|
163
|
+
export type HeatmapChartProp = {
|
|
164
|
+
/** Row categories, top to bottom (the y axis — weekdays). */
|
|
165
|
+
rows: HeatmapCategory[];
|
|
166
|
+
/** Column categories, start to end (the x axis — hours). */
|
|
167
|
+
columns: HeatmapCategory[];
|
|
168
|
+
/** Cell values, keyed by `row` + `column`. Order does not matter; a missing cell is empty. */
|
|
169
|
+
data: HeatmapDatum[];
|
|
170
|
+
/** Accessible name + visible caption for the chart. REQUIRED (the grid needs a name). */
|
|
171
|
+
label: LabelProp;
|
|
172
|
+
/**
|
|
173
|
+
* Paint `label` as a visible caption above the plot. `false` keeps it in the DOM as `sr-only`,
|
|
174
|
+
* so the figure and its grid keep their accessible name.
|
|
175
|
+
*/
|
|
176
|
+
showCaption?: boolean;
|
|
177
|
+
/** Extra context appended to the screen-reader description. */
|
|
178
|
+
description?: DescriptionProp;
|
|
179
|
+
/**
|
|
180
|
+
* CELL size tier — `xs|sm|md|lg` (`--chart-heatmap-cell-size-*`). Not a canvas height: the matrix
|
|
181
|
+
* is as tall as its rows, and when the columns do not fit it scrolls inside its own box.
|
|
182
|
+
*/
|
|
183
|
+
size?: SizeProp;
|
|
184
|
+
/** `Intl.NumberFormat` options for cell values, the tooltip and the legend (locale is automatic). */
|
|
185
|
+
numberFormat?: Intl.NumberFormatOptions;
|
|
186
|
+
/**
|
|
187
|
+
* Emphasize the N highest cells with an outline ring and a "top N" note in the tooltip and the
|
|
188
|
+
* text alternative — never colour alone. Ties keep `data` order. Omitted or 0 = no emphasis.
|
|
189
|
+
*/
|
|
190
|
+
topN?: number;
|
|
191
|
+
/** Show the colour-scale legend under the matrix. */
|
|
192
|
+
showLegend?: boolean;
|
|
193
|
+
/** Message shown when there are no rows, no columns or no cells. Defaults to a localized "no data". */
|
|
194
|
+
emptyMessage?: EmptyMessageProp;
|
|
195
|
+
className?: ClassNameProp;
|
|
196
|
+
id?: IdProp;
|
|
197
|
+
ref?: React.Ref<HTMLElement>;
|
|
198
|
+
};
|
|
141
199
|
/** @see AreaChart */
|
|
142
200
|
export type AreaChartProp = ChartCartesianBase & {
|
|
143
201
|
/** Stack series areas instead of overlaying them. */
|