@kind-ui/charts 0.1.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +966 -0
  4. package/dist/activity-rings.d.ts +47 -0
  5. package/dist/activity-rings.js +55 -0
  6. package/dist/animation.d.ts +27 -0
  7. package/dist/animation.js +102 -0
  8. package/dist/area-chart.d.ts +9 -0
  9. package/dist/area-chart.js +33 -0
  10. package/dist/area-clay.d.ts +2 -0
  11. package/dist/area-clay.js +6 -0
  12. package/dist/area-material.d.ts +8 -0
  13. package/dist/area-material.js +66 -0
  14. package/dist/area-series.d.ts +11 -0
  15. package/dist/area-series.js +34 -0
  16. package/dist/bar-category.d.ts +5 -0
  17. package/dist/bar-category.js +35 -0
  18. package/dist/bar-chart.d.ts +17 -0
  19. package/dist/bar-chart.js +42 -0
  20. package/dist/bar-clay.d.ts +4 -0
  21. package/dist/bar-clay.js +6 -0
  22. package/dist/bar-material.d.ts +8 -0
  23. package/dist/bar-material.js +19 -0
  24. package/dist/bar-paper.d.ts +2 -0
  25. package/dist/bar-paper.js +6 -0
  26. package/dist/bar-series.d.ts +13 -0
  27. package/dist/bar-series.js +178 -0
  28. package/dist/box-material.d.ts +14 -0
  29. package/dist/box-material.js +30 -0
  30. package/dist/box-plot.d.ts +50 -0
  31. package/dist/box-plot.js +218 -0
  32. package/dist/category-cells.d.ts +6 -0
  33. package/dist/category-cells.js +41 -0
  34. package/dist/chart-context.d.ts +11 -0
  35. package/dist/chart-context.js +9 -0
  36. package/dist/combo-chart.d.ts +16 -0
  37. package/dist/combo-chart.js +64 -0
  38. package/dist/configured-line-chart.d.ts +79 -0
  39. package/dist/configured-line-chart.js +77 -0
  40. package/dist/emphasis.d.ts +44 -0
  41. package/dist/emphasis.js +128 -0
  42. package/dist/heatmap-model.d.ts +38 -0
  43. package/dist/heatmap-model.js +73 -0
  44. package/dist/heatmap.d.ts +56 -0
  45. package/dist/heatmap.js +366 -0
  46. package/dist/histogram-chart.d.ts +14 -0
  47. package/dist/histogram-chart.js +27 -0
  48. package/dist/histogram-data.d.ts +17 -0
  49. package/dist/histogram-data.js +66 -0
  50. package/dist/histogram-material.d.ts +13 -0
  51. package/dist/histogram-material.js +8 -0
  52. package/dist/histogram-series.d.ts +16 -0
  53. package/dist/histogram-series.js +67 -0
  54. package/dist/index.d.ts +45 -0
  55. package/dist/index.js +36 -0
  56. package/dist/legend.d.ts +16 -0
  57. package/dist/legend.js +30 -0
  58. package/dist/line-chart.d.ts +41 -0
  59. package/dist/line-chart.js +160 -0
  60. package/dist/line-material.d.ts +9 -0
  61. package/dist/line-material.js +24 -0
  62. package/dist/line-series.d.ts +15 -0
  63. package/dist/line-series.js +40 -0
  64. package/dist/pie-chart.d.ts +17 -0
  65. package/dist/pie-chart.js +68 -0
  66. package/dist/pie-material.d.ts +19 -0
  67. package/dist/pie-material.js +16 -0
  68. package/dist/pie-series.d.ts +14 -0
  69. package/dist/pie-series.js +208 -0
  70. package/dist/polar-chart.d.ts +33 -0
  71. package/dist/polar-chart.js +145 -0
  72. package/dist/polar-material.d.ts +7 -0
  73. package/dist/polar-material.js +9 -0
  74. package/dist/polar-series.d.ts +17 -0
  75. package/dist/polar-series.js +169 -0
  76. package/dist/radar-interaction.d.ts +29 -0
  77. package/dist/radar-interaction.js +138 -0
  78. package/dist/radial-bar-label.d.ts +15 -0
  79. package/dist/radial-bar-label.js +136 -0
  80. package/dist/radial-category.d.ts +4 -0
  81. package/dist/radial-category.js +4 -0
  82. package/dist/root.d.ts +14 -0
  83. package/dist/root.js +29 -0
  84. package/dist/sankey-chart.d.ts +33 -0
  85. package/dist/sankey-chart.js +142 -0
  86. package/dist/sankey-colors.d.ts +10 -0
  87. package/dist/sankey-colors.js +6 -0
  88. package/dist/sankey-data.d.ts +30 -0
  89. package/dist/sankey-data.js +65 -0
  90. package/dist/sankey-finish.d.ts +13 -0
  91. package/dist/sankey-finish.js +10 -0
  92. package/dist/sankey-legend.d.ts +14 -0
  93. package/dist/sankey-legend.js +9 -0
  94. package/dist/sankey-marks.d.ts +31 -0
  95. package/dist/sankey-marks.js +67 -0
  96. package/dist/sankey-table.d.ts +11 -0
  97. package/dist/sankey-table.js +8 -0
  98. package/dist/scatter-chart.d.ts +16 -0
  99. package/dist/scatter-chart.js +64 -0
  100. package/dist/scatter-material.d.ts +8 -0
  101. package/dist/scatter-material.js +39 -0
  102. package/dist/scatter-series.d.ts +11 -0
  103. package/dist/scatter-series.js +74 -0
  104. package/dist/scatter-tooltip.d.ts +24 -0
  105. package/dist/scatter-tooltip.js +96 -0
  106. package/dist/styles.css +443 -0
  107. package/dist/surface-material.d.ts +13 -0
  108. package/dist/surface-material.js +7 -0
  109. package/dist/tooltip-content.d.ts +19 -0
  110. package/dist/tooltip-content.js +60 -0
  111. package/dist/tooltip-number.d.ts +4 -0
  112. package/dist/tooltip-number.js +68 -0
  113. package/dist/tooltip.d.ts +29 -0
  114. package/dist/tooltip.js +73 -0
  115. package/dist/types.d.ts +11 -0
  116. package/dist/types.js +1 -0
  117. package/dist/waterfall-data.d.ts +24 -0
  118. package/dist/waterfall-data.js +45 -0
  119. package/dist/waterfall-series.d.ts +15 -0
  120. package/dist/waterfall-series.js +35 -0
  121. package/package.json +49 -0
@@ -0,0 +1,366 @@
1
+ "use client";
2
+ import { createElement as _createElement } from "react";
3
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
4
+ import { animateMini, animate as animateValue } from "motion/react";
5
+ import { createContext, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
6
+ import { createHeatmapModel, } from "./heatmap-model.js";
7
+ import { TooltipNumber } from "./tooltip-number.js";
8
+ const HeatmapContext = createContext(null);
9
+ function useHeatmap() {
10
+ const value = use(HeatmapContext);
11
+ if (!value)
12
+ throw new Error("Heatmap components require HeatmapChart");
13
+ return value;
14
+ }
15
+ const reducedQuery = "(prefers-reduced-motion: reduce)";
16
+ function subscribeReduced(change) {
17
+ const media = window.matchMedia(reducedQuery);
18
+ media.addEventListener("change", change);
19
+ return () => media.removeEventListener("change", change);
20
+ }
21
+ const reducedSnapshot = () => window.matchMedia(reducedQuery).matches;
22
+ const reducedServerSnapshot = () => true;
23
+ const number = (value) => String(value);
24
+ const keyOf = (cell) => JSON.stringify([cell.row, cell.column]);
25
+ const pointerPosition = (event) => ({
26
+ pointerId: event.pointerId,
27
+ pointerType: event.pointerType,
28
+ clientX: event.clientX,
29
+ clientY: event.clientY,
30
+ });
31
+ const samePointer = (a, b) => a !== null &&
32
+ a.pointerId === b.pointerId &&
33
+ a.pointerType === b.pointerType &&
34
+ a.clientX === b.clientX &&
35
+ a.clientY === b.clientY;
36
+ const labelOf = (cell, context) => `${cell.row}, ${cell.column}: ${cell.value === null ? context.missingLabel : context.formatValue(cell.value)}`;
37
+ /** Native table layout owns categorical geometry. No numeric scatter approximation or SVG overlay. */
38
+ export function HeatmapChart({ rows, columns, data, duplicates, scale, formatValue = number, missingLabel = "Missing", animate = false, children, onPointerLeave, ...props }) {
39
+ const model = useMemo(() => createHeatmapModel({ rows, columns, data, ...(duplicates ? { duplicates } : {}) }), [rows, columns, data, duplicates]);
40
+ const [active, updateActive] = useState(null);
41
+ const pointer = useRef(null);
42
+ const dismissed = useRef(false);
43
+ const setActive = useCallback((key) => {
44
+ if (key)
45
+ dismissed.current = false;
46
+ updateActive(key);
47
+ }, []);
48
+ const dismiss = useCallback(() => {
49
+ dismissed.current = true;
50
+ updateActive(null);
51
+ }, []);
52
+ const inspectPointer = useCallback((event, key) => {
53
+ const stationary = samePointer(pointer.current, event);
54
+ pointer.current = pointerPosition(event);
55
+ if (dismissed.current && stationary)
56
+ return;
57
+ setActive(key);
58
+ }, [setActive]);
59
+ const tooltipId = useId();
60
+ const [tooltipMounted, setTooltipMounted] = useState(false);
61
+ const reduced = useSyncExternalStore(subscribeReduced, reducedSnapshot, reducedServerSnapshot);
62
+ const [finished, setFinished] = useState(false);
63
+ const interrupt = useCallback(() => setFinished(true), []);
64
+ const enabled = animate && !reduced;
65
+ const entrance = enabled && !finished;
66
+ const root = useRef(null);
67
+ useEffect(() => {
68
+ if (!tooltipMounted)
69
+ return;
70
+ // Keep the Escape baseline current even before focus/hover activates a cell.
71
+ // Layout/scroll can send boundary events without new pointer input. Only actual
72
+ // movement clears dismissal, including movement outside or within an entered cell.
73
+ const move = (event) => {
74
+ const stationary = samePointer(pointer.current, event);
75
+ pointer.current = pointerPosition(event);
76
+ if (!dismissed.current || stationary)
77
+ return;
78
+ dismissed.current = false;
79
+ const target = event.target instanceof Element ? event.target.closest("td[data-cell-key]") : null;
80
+ if (!target || !root.current?.contains(target))
81
+ return;
82
+ const cell = model.cells
83
+ .flat()
84
+ .find((item) => keyOf(item) === target.getAttribute("data-cell-key"));
85
+ if (cell)
86
+ setActive([cell.row, cell.column]);
87
+ };
88
+ document.addEventListener("pointermove", move, true);
89
+ return () => document.removeEventListener("pointermove", move, true);
90
+ }, [tooltipMounted, model, setActive]);
91
+ const consumerRef = props.ref;
92
+ const ref = useCallback((node) => {
93
+ root.current = node;
94
+ if (typeof consumerRef === "function")
95
+ return consumerRef(node);
96
+ if (consumerRef)
97
+ consumerRef.current = node;
98
+ }, [consumerRef]);
99
+ useLayoutEffect(() => {
100
+ const node = root.current;
101
+ if (!node || !entrance)
102
+ return;
103
+ const initial = node.getBoundingClientRect();
104
+ const observer = new ResizeObserver(() => {
105
+ const next = node.getBoundingClientRect();
106
+ if (next.width !== initial.width || next.height !== initial.height)
107
+ interrupt();
108
+ });
109
+ observer.observe(node);
110
+ return () => observer.disconnect();
111
+ }, [entrance, interrupt]);
112
+ const previous = useRef([model, scale, children]);
113
+ const previousEnabled = useRef(enabled);
114
+ useLayoutEffect(() => {
115
+ const inputs = [model, scale, children];
116
+ if (inputs.some((value, index) => value !== previous.current[index]) ||
117
+ (previousEnabled.current && !enabled))
118
+ interrupt();
119
+ previous.current = inputs;
120
+ previousEnabled.current = enabled;
121
+ });
122
+ useLayoutEffect(() => {
123
+ if (!entrance)
124
+ return;
125
+ const controls = animateValue(0, 1, { duration: 0.8, onComplete: interrupt });
126
+ return () => controls.stop();
127
+ }, [entrance, interrupt]);
128
+ useEffect(() => {
129
+ if (!active || !tooltipMounted)
130
+ return;
131
+ const keydown = (event) => {
132
+ if (event.key === "Escape" && !event.defaultPrevented)
133
+ dismiss();
134
+ };
135
+ document.addEventListener("keydown", keydown);
136
+ return () => document.removeEventListener("keydown", keydown);
137
+ }, [active, tooltipMounted, dismiss]);
138
+ return (_jsx(HeatmapContext, { value: {
139
+ model,
140
+ entrance,
141
+ scale,
142
+ formatValue,
143
+ missingLabel,
144
+ active,
145
+ setActive,
146
+ dismiss,
147
+ inspectPointer,
148
+ tooltipId,
149
+ tooltipMounted,
150
+ setTooltipMounted,
151
+ }, children: _jsx("div", { ...props, ref: ref, "data-kind-ui": "heatmap", onFocusCapture: (event) => {
152
+ props.onFocusCapture?.(event);
153
+ interrupt();
154
+ }, onPointerMoveCapture: (event) => {
155
+ props.onPointerMoveCapture?.(event);
156
+ interrupt();
157
+ }, onPointerDownCapture: (event) => {
158
+ props.onPointerDownCapture?.(event);
159
+ interrupt();
160
+ }, onKeyDownCapture: (event) => {
161
+ props.onKeyDownCapture?.(event);
162
+ interrupt();
163
+ }, onPointerLeave: (event) => {
164
+ onPointerLeave?.(event);
165
+ if (!active)
166
+ return;
167
+ const focused = document.activeElement;
168
+ const cell = event.currentTarget.contains(focused)
169
+ ? model.cells.flat().find((item) => keyOf(item) === focused?.dataset.cellKey)
170
+ : undefined;
171
+ setActive(cell ? [cell.row, cell.column] : null);
172
+ }, children: _jsx("div", { "data-kind-ui": "heatmap-entrance", children: children }) }) }));
173
+ }
174
+ // A native cell keeps its styles, ref and handlers. Motion temporarily multiplies its opacity.
175
+ function EntranceCell({ diagonal, ...props }) {
176
+ const { entrance, model } = useHeatmap();
177
+ const cell = useRef(null);
178
+ const consumerRef = props.ref;
179
+ const ref = useCallback((node) => {
180
+ cell.current = node;
181
+ if (typeof consumerRef === "function")
182
+ return consumerRef(node);
183
+ if (consumerRef)
184
+ consumerRef.current = node;
185
+ }, [consumerRef]);
186
+ const latestOpacity = useRef(props.style?.opacity);
187
+ latestOpacity.current = props.style?.opacity;
188
+ useLayoutEffect(() => {
189
+ const node = cell.current;
190
+ if (!node || !entrance)
191
+ return;
192
+ const original = node.style.opacity;
193
+ const declaredOpacity = props.style?.opacity;
194
+ const opacity = Number(getComputedStyle(node).opacity);
195
+ const delay = (diagonal / Math.max(1, model.rows.length + model.columns.length - 2)) * 0.55;
196
+ const controls = animateMini(node, { opacity: [0, opacity] }, { delay, duration: 0.25, ease: "easeOut" });
197
+ return () => {
198
+ controls.stop();
199
+ node.style.opacity = Object.is(latestOpacity.current, declaredOpacity)
200
+ ? original
201
+ : latestOpacity.current === undefined
202
+ ? ""
203
+ : String(latestOpacity.current);
204
+ };
205
+ }, [entrance, diagonal, model, props.style?.opacity]);
206
+ return _jsx("td", { ...props, ref: ref, "data-kind-ui": "heatmap-cell-entrance" });
207
+ }
208
+ export function HeatmapCellContent({ fill, formattedValue }) {
209
+ const channels = /^#[0-9a-f]{6}$/i.test(fill)
210
+ ? [1, 3, 5].map((i) => {
211
+ const value = Number.parseInt(fill.slice(i, i + 2), 16) / 255;
212
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
213
+ })
214
+ : [1, 1, 1];
215
+ const luminance = (channels[0] ?? 1) * 0.2126 + (channels[1] ?? 1) * 0.7152 + (channels[2] ?? 1) * 0.0722;
216
+ return (_jsx("span", { style: {
217
+ color: /^#[0-9a-f]{6}$/i.test(fill)
218
+ ? luminance > 0.179
219
+ ? "#000000"
220
+ : "#ffffff"
221
+ : "var(--heatmap-missing-foreground, #172033)",
222
+ }, children: formattedValue }));
223
+ }
224
+ export function HeatmapGrid({ caption, material = "plain", layout, Cell = HeatmapCellContent, cellProps, rowLabel, columnLabel, onKeyDown, onBlur, onPointerLeave, style, ...props }) {
225
+ const context = useHeatmap();
226
+ const headerId = useId();
227
+ for (const [name, value] of [
228
+ ["cellSize", layout?.cellSize],
229
+ ["gap", layout?.gap],
230
+ ]) {
231
+ if (typeof value === "number" &&
232
+ (!Number.isFinite(value) || (name === "cellSize" ? value <= 0 : value < 0))) {
233
+ throw new Error(`HeatmapGrid layout.${name} must be finite and ${name === "cellSize" ? "positive" : "nonnegative"}`);
234
+ }
235
+ }
236
+ const length = (value) => typeof value === "number" ? `${value}px` : value;
237
+ const { model, scale, active, setActive, tooltipId } = context;
238
+ const [focusedKey, setFocusedKey] = useState(null);
239
+ const table = useRef(null);
240
+ const focused = model.cells.flat().find((cell) => keyOf(cell) === focusedKey) ?? model.cells[0]?.[0];
241
+ const tabKey = focused ? keyOf(focused) : null;
242
+ function move(event) {
243
+ onKeyDown?.(event);
244
+ if (event.defaultPrevented)
245
+ return;
246
+ const target = event.target;
247
+ if (!(target instanceof HTMLTableCellElement) || target.dataset.cellKey === undefined)
248
+ return;
249
+ const cell = model.cells.flat().find((item) => keyOf(item) === target.dataset.cellKey);
250
+ if (!cell)
251
+ return;
252
+ let r = cell.rowIndex;
253
+ let c = cell.columnIndex;
254
+ switch (event.key) {
255
+ case "ArrowRight":
256
+ c++;
257
+ break;
258
+ case "ArrowLeft":
259
+ c--;
260
+ break;
261
+ case "ArrowDown":
262
+ r++;
263
+ break;
264
+ case "ArrowUp":
265
+ r--;
266
+ break;
267
+ case "Home":
268
+ c = 0;
269
+ if (event.ctrlKey)
270
+ r = 0;
271
+ break;
272
+ case "End":
273
+ c = model.columns.length - 1;
274
+ if (event.ctrlKey)
275
+ r = model.rows.length - 1;
276
+ break;
277
+ case "Escape":
278
+ context.dismiss();
279
+ event.preventDefault();
280
+ return;
281
+ default:
282
+ return;
283
+ }
284
+ event.preventDefault();
285
+ r = Math.max(0, Math.min(r, model.rows.length - 1));
286
+ c = Math.max(0, Math.min(c, model.columns.length - 1));
287
+ const next = model.cells[r]?.[c];
288
+ if (next) {
289
+ const element = Array.from(table.current?.querySelectorAll("td[data-cell-key]") ?? []).find((td) => td.dataset.cellKey === keyOf(next));
290
+ if (element) {
291
+ setActive([next.row, next.column]);
292
+ element.focus();
293
+ }
294
+ }
295
+ }
296
+ return (_jsxs("div", { "data-kind-ui": "heatmap-scroll", children: [_jsxs("table", { ...props, ref: (node) => {
297
+ table.current = node;
298
+ if (typeof props.ref === "function")
299
+ return props.ref(node);
300
+ if (props.ref)
301
+ props.ref.current = node;
302
+ },
303
+ // biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: WAI-ARIA APG data grid uses a native table with roving cell focus.
304
+ role: "grid", "aria-label": caption, "data-kind-ui": "heatmap-grid", "data-cell-sizing": layout?.cellSize === undefined ? undefined : "fixed", "data-row-labels": layout?.rowLabels, "data-column-labels": layout?.columnLabels, style: {
305
+ "--heatmap-columns": model.columns.length,
306
+ ...(layout?.cellSize === undefined
307
+ ? {}
308
+ : { "--heatmap-cell-size": length(layout.cellSize) }),
309
+ ...(layout?.gap === undefined ? {} : { "--heatmap-gap": length(layout.gap) }),
310
+ ...style,
311
+ }, onKeyDown: move, onBlur: (event) => {
312
+ onBlur?.(event);
313
+ if (!event.currentTarget.contains(event.relatedTarget))
314
+ setActive(null);
315
+ }, onPointerLeave: onPointerLeave, children: [_jsx("caption", { children: caption }), layout?.cellSize === undefined ? null : (_jsxs("colgroup", { children: [_jsx("col", { "data-kind-ui": "heatmap-row-column" }), model.columns.map((column) => (_jsx("col", {}, column)))] })), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Row / column" }), model.columns.map((column, c) => (_jsx("th", { scope: "col", id: `${headerId}-column-${c}`, children: columnLabel?.(column) ?? column }, column)))] }) }), _jsx("tbody", { children: model.rows.map((row, r) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", id: `${headerId}-row-${r}`, children: _jsx("span", { "data-kind-ui": "heatmap-row-label", title: row, children: rowLabel?.(row) ?? row }) }), model.cells[r]?.map((cell, c) => {
316
+ const extra = cellProps?.(cell) ?? {};
317
+ const fill = cell.value === null ? "var(--heatmap-missing, #e5e7eb)" : scale.color(cell.value);
318
+ const formattedValue = cell.value === null ? context.missingLabel : context.formatValue(cell.value);
319
+ return (_createElement(EntranceCell, { diagonal: r + c, ...extra, key: cell.column, role: "gridcell", headers: `${headerId}-row-${r} ${headerId}-column-${c}`, "data-cell-key": keyOf(cell), "data-missing": cell.value === null ? "true" : "false", "data-material": cell.value === null ? undefined : material, tabIndex: keyOf(cell) === tabKey ? 0 : -1, "aria-label": labelOf(cell, context), "aria-describedby": context.tooltipMounted &&
320
+ active?.[0] === cell.row &&
321
+ active[1] === cell.column
322
+ ? tooltipId
323
+ : undefined, style: { ...extra.style, backgroundColor: fill }, onFocus: (event) => {
324
+ extra.onFocus?.(event);
325
+ setFocusedKey(keyOf(cell));
326
+ setActive([cell.row, cell.column]);
327
+ }, onPointerDown: (event) => {
328
+ extra.onPointerDown?.(event);
329
+ // A press is deliberate even at the dismissed pointer position.
330
+ setActive([cell.row, cell.column]);
331
+ }, onPointerEnter: (event) => {
332
+ extra.onPointerEnter?.(event);
333
+ context.inspectPointer(event, [cell.row, cell.column]);
334
+ } },
335
+ _jsx(Cell, { cell: cell, fill: fill, formattedValue: formattedValue })));
336
+ })] }, row))) })] }), model.rows.length === 0 || model.columns.length === 0 ? (_jsx("p", { "data-kind-ui": "heatmap-empty", children: "No cells in the selected domains." })) : null] }));
337
+ }
338
+ /** In-flow tooltip stays within narrow containers and remains available for hover, focus and touch. */
339
+ export function HeatmapTooltip({ Content, valueAnimation, ...props }) {
340
+ const context = useHeatmap();
341
+ useEffect(() => {
342
+ context.setTooltipMounted(true);
343
+ return () => context.setTooltipMounted(false);
344
+ }, [context.setTooltipMounted]);
345
+ const cell = context.model.cells
346
+ .flat()
347
+ .find((item) => item.row === context.active?.[0] && item.column === context.active[1]);
348
+ return (_jsx("div", { ...props, id: context.tooltipId, role: "tooltip", "data-kind-ui": "heatmap-tooltip", hidden: !cell, children: cell ? (Content ? (_jsx(Content, { cell: cell, fill: cell.value === null
349
+ ? "var(--heatmap-missing, #e5e7eb)"
350
+ : context.scale.color(cell.value), formattedValue: cell.value === null ? context.missingLabel : context.formatValue(cell.value) })) : valueAnimation === "shuffle" && cell.value !== null && Number.isFinite(cell.value) ? (_jsxs(_Fragment, { children: [`${cell.row}, ${cell.column}: `, _jsx(TooltipNumber, { value: context.formatValue(cell.value) })] })) : (labelOf(cell, context))) : null }));
351
+ }
352
+ export function HeatmapLegend({ label, ...props }) {
353
+ const { scale, formatValue, missingLabel } = useHeatmap();
354
+ const [min, max] = scale.domain;
355
+ return (_jsxs("fieldset", { ...props, "data-kind-ui": "heatmap-legend", "aria-label": `${label}: ${formatValue(min)} to ${formatValue(max)}; ${missingLabel}`, children: [_jsx("legend", { children: label }), _jsx("div", { "data-kind-ui": "heatmap-ramp", "aria-hidden": "true", style: {
356
+ background: min === max
357
+ ? scale.color(min)
358
+ : `linear-gradient(to right in srgb, ${scale.colors.join(", ")})`,
359
+ }, children: min < 0 && max > 0 ? (_jsx("i", { "data-kind-ui": "heatmap-zero-marker", style: { left: `${(-min / (max - min)) * 100}%` } })) : null }), _jsxs("div", { "data-kind-ui": "heatmap-ticks", children: [_jsx("span", { children: formatValue(min) }), _jsx("span", { children: formatValue(max) })] }), min < 0 && max > 0 ? (_jsxs("span", { "data-kind-ui": "heatmap-zero-label", children: ["Zero marker: ", formatValue(0)] })) : null, _jsxs("span", { "data-kind-ui": "heatmap-missing-key", children: [_jsx("i", { "aria-hidden": "true" }), missingLabel] })] }));
360
+ }
361
+ /** Independent static native table; the host decides whether it is visible or in a details disclosure. */
362
+ export function HeatmapDataTable({ caption, ...props }) {
363
+ const context = useHeatmap();
364
+ const { model, formatValue, missingLabel } = context;
365
+ return (_jsx("div", { "data-kind-ui": "heatmap-scroll", children: _jsxs("table", { ...props, "data-kind-ui": "heatmap-data-table", children: [_jsx("caption", { children: caption }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Row / column" }), model.columns.map((column) => (_jsx("th", { scope: "col", children: column }, column)))] }) }), _jsx("tbody", { children: model.rows.map((row, r) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: row }), model.cells[r]?.map((cell) => (_jsx("td", { children: cell.value === null ? missingLabel : formatValue(cell.value) }, cell.column)))] }, row))) })] }) }));
366
+ }
@@ -0,0 +1,14 @@
1
+ import { type ComponentProps } from "react";
2
+ import { XAxis, YAxis } from "recharts";
3
+ import { type BarChartProps } from "./bar-chart.js";
4
+ import { type HistogramBin, type HistogramMeasure } from "./histogram-data.js";
5
+ export type HistogramChartProps = Omit<BarChartProps, "data" | "layout" | "stackOffset" | "barSize" | "barCategoryGap" | "barGap" | "reverseStackOrder"> & {
6
+ bins: readonly HistogramBin[];
7
+ /** Required: count height or normalized probability density (area sums to one). */
8
+ measure: HistogramMeasure;
9
+ xAxisProps?: Omit<ComponentProps<typeof XAxis>, "dataKey" | "type" | "domain" | "scale" | "xAxisId" | "allowDataOverflow" | "reversed">;
10
+ yAxisProps?: Omit<ComponentProps<typeof YAxis>, "dataKey" | "type" | "domain" | "scale" | "yAxisId" | "allowDataOverflow" | "reversed">;
11
+ };
12
+ export declare const HistogramContext: import("react").Context<HistogramMeasure | null>;
13
+ /** Bounded vertical histogram; quantitative axes are owned here, native children compose. */
14
+ export declare function HistogramChart({ bins, measure, xAxisProps, yAxisProps, children, ...props }: HistogramChartProps): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useMemo } from "react";
4
+ import { XAxis, YAxis } from "recharts";
5
+ import { BarChart } from "./bar-chart.js";
6
+ import { validateHistogram } from "./histogram-data.js";
7
+ export const HistogramContext = createContext(null);
8
+ /** Bounded vertical histogram; quantitative axes are owned here, native children compose. */
9
+ export function HistogramChart({ bins, measure, xAxisProps, yAxisProps, children, ...props }) {
10
+ const total = validateHistogram(bins);
11
+ if (measure !== "count" && measure !== "density")
12
+ throw new Error("Histogram requires count or density measure");
13
+ const data = useMemo(() => bins.map((bin) => {
14
+ const value = measure === "count"
15
+ ? bin.count
16
+ : total === 0
17
+ ? 0
18
+ : bin.count / total / (bin.upper - bin.lower);
19
+ if (!Number.isFinite(value) || (bin.count > 0 && value === 0))
20
+ throw new Error("Histogram density exceeds finite positive numeric range");
21
+ return { ...bin, midpoint: bin.lower + (bin.upper - bin.lower) / 2, value };
22
+ }), [bins, measure, total]);
23
+ const domain = bins.length ? [bins[0]?.lower ?? 0, bins[bins.length - 1]?.upper ?? 1] : [0, 1];
24
+ if (!Number.isFinite((domain[1] ?? 1) - (domain[0] ?? 0)))
25
+ throw new Error("Histogram domain must have a finite span");
26
+ return (_jsx(HistogramContext, { value: measure, children: _jsxs(BarChart, { ...props, data: data, layout: "horizontal", barCategoryGap: 0, barGap: 0, children: [_jsx(XAxis, { ...xAxisProps, xAxisId: 0, dataKey: "midpoint", type: "number", scale: "linear", domain: domain, allowDataOverflow: true, reversed: false }), _jsx(YAxis, { ...yAxisProps, yAxisId: 0, type: "number", scale: "linear", domain: [0, "dataMax"], reversed: false }), children] }) }));
27
+ }
@@ -0,0 +1,17 @@
1
+ /** One pre-aggregated interval. Counts are nonnegative safe integers. */
2
+ export type HistogramBin = Readonly<{
3
+ lower: number;
4
+ upper: number;
5
+ count: number;
6
+ }>;
7
+ export type HistogramMeasure = "count" | "density";
8
+ export type HistogramBinningResult = Readonly<{
9
+ bins: readonly HistogramBin[];
10
+ accepted: number;
11
+ missing: number;
12
+ nonfinite: number;
13
+ outOfRange: number;
14
+ }>;
15
+ /** Explicit edges only; no inferred domain, rounding, coercion or statistical estimator. */
16
+ export declare function binHistogram(samples: readonly (number | null | undefined)[], edges: readonly number[]): HistogramBinningResult;
17
+ export declare function validateHistogram(bins: readonly HistogramBin[]): number;
@@ -0,0 +1,66 @@
1
+ /** Explicit edges only; no inferred domain, rounding, coercion or statistical estimator. */
2
+ export function binHistogram(samples, edges) {
3
+ if (edges.length < 2)
4
+ throw new Error("Histogram requires at least two edges");
5
+ const bins = edges.slice(0, -1).map((lower, index) => ({
6
+ lower,
7
+ upper: edges[index + 1],
8
+ count: 0,
9
+ }));
10
+ validateHistogram(bins);
11
+ let accepted = 0;
12
+ let missing = 0;
13
+ let nonfinite = 0;
14
+ let outOfRange = 0;
15
+ const first = edges[0];
16
+ const last = edges[edges.length - 1];
17
+ for (const sample of samples) {
18
+ if (sample == null) {
19
+ missing++;
20
+ continue;
21
+ }
22
+ if (typeof sample !== "number" || !Number.isFinite(sample)) {
23
+ nonfinite++;
24
+ continue;
25
+ }
26
+ if (sample < first || sample > last) {
27
+ outOfRange++;
28
+ continue;
29
+ }
30
+ // Upper-bound search assigns an interior edge to its right-hand bin.
31
+ let low = 0;
32
+ let high = edges.length;
33
+ while (low < high) {
34
+ const middle = Math.floor((low + high) / 2);
35
+ if (sample < edges[middle])
36
+ high = middle;
37
+ else
38
+ low = middle + 1;
39
+ }
40
+ const bin = bins[Math.min(low - 1, bins.length - 1)];
41
+ if (bin)
42
+ bin.count++;
43
+ accepted++;
44
+ }
45
+ return { bins, accepted, missing, nonfinite, outOfRange };
46
+ }
47
+ export function validateHistogram(bins) {
48
+ let total = 0;
49
+ let previousUpper;
50
+ for (const bin of bins) {
51
+ if (!Number.isFinite(bin.lower) ||
52
+ !Number.isFinite(bin.upper) ||
53
+ !(bin.upper > bin.lower) ||
54
+ !Number.isFinite(bin.upper - bin.lower))
55
+ throw new Error("Histogram bounds must be finite with positive finite width");
56
+ if (previousUpper !== undefined && bin.lower < previousUpper)
57
+ throw new Error("Histogram bins must be ordered and nonoverlapping");
58
+ if (!Number.isSafeInteger(bin.count) || bin.count < 0)
59
+ throw new Error("Histogram counts must be nonnegative safe integers");
60
+ total += bin.count;
61
+ if (!Number.isSafeInteger(total))
62
+ throw new Error("Histogram total exceeds safe integer range");
63
+ previousUpper = bin.upper;
64
+ }
65
+ return total;
66
+ }
@@ -0,0 +1,13 @@
1
+ import type { BarMaterial } from "./bar-material.js";
2
+ /** Reuse bar surface paint, with no cast shade across quantitative interval edges. */
3
+ export declare function HistogramMaterialFilter({ material, id, bounds, padding, }: {
4
+ material: Exclude<BarMaterial, "plain">;
5
+ id: string;
6
+ padding: number;
7
+ bounds: {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { BarClay } from "./bar-clay.js";
4
+ import { BarPaper } from "./bar-paper.js";
5
+ /** Reuse bar surface paint, with no cast shade across quantitative interval edges. */
6
+ export function HistogramMaterialFilter({ material, id, bounds, padding, }) {
7
+ return (_jsx("filter", { id: id, filterUnits: "userSpaceOnUse", primitiveUnits: "userSpaceOnUse", x: bounds.x - padding, y: bounds.y - padding, width: bounds.width + padding * 2, height: bounds.height + padding * 2, colorInterpolationFilters: "sRGB", children: material === "paper" ? (_jsx(BarPaper, {})) : material === "clay" ? (_jsxs(_Fragment, { children: [_jsx(BarClay, { horizontal: false }), _jsx("feComposite", { in2: "footprint", operator: "in" })] })) : (_jsxs(_Fragment, { children: [_jsx("feComponentTransfer", { in: "SourceAlpha", result: "footprint", children: _jsx("feFuncA", { type: "linear", slope: 100000 }) }), _jsx("feGaussianBlur", { in: "SourceGraphic", stdDeviation: 2.5, result: "halo" }), _jsx("feFlood", { floodOpacity: "var(--kind-ui-bar-glow-opacity, 0.6)", result: "strength" }), _jsx("feComposite", { in: "halo", in2: "strength", operator: "in", result: "softHalo" }), _jsx("feComposite", { in: "softHalo", in2: "footprint", operator: "out", result: "unpaintedHalo" }), _jsx("feFlood", { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, floodColor: "#fff", result: "binBounds" }), _jsx("feComposite", { in: "unpaintedHalo", in2: "binBounds", operator: "out", result: "exteriorHalo" }), _jsx("feMorphology", { in: "footprint", operator: "erode", radius: 1.5, result: "inside" }), _jsx("feComposite", { in: "footprint", in2: "inside", operator: "out", result: "edge" }), _jsx("feFlood", { floodColor: "var(--kind-ui-bar-glow-light, #fff)", floodOpacity: 0.75 }), _jsx("feComposite", { in2: "edge", operator: "in", result: "light" }), _jsx("feComposite", { in: "light", in2: "SourceGraphic", operator: "atop", result: "body" }), _jsx("feGaussianBlur", { in: "light", stdDeviation: 1.5, result: "innerLight" }), _jsx("feComposite", { in: "innerLight", in2: "body", operator: "atop", result: "litBody" }), _jsxs("feMerge", { children: [_jsx("feMergeNode", { in: "exteriorHalo" }), _jsx("feMergeNode", { in: "litBody" })] })] })) }));
8
+ }
@@ -0,0 +1,16 @@
1
+ import { type ReactElement } from "react";
2
+ import { type BarShapeProps } from "recharts";
3
+ import type { BarMaterial } from "./bar-material.js";
4
+ import { type BarSeriesProps } from "./bar-series.js";
5
+ import type { HistogramBin } from "./histogram-data.js";
6
+ export type HistogramShapeProps = BarShapeProps & {
7
+ bin: HistogramBin;
8
+ };
9
+ export type HistogramSeriesProps = Omit<BarSeriesProps, "data" | "dataKey" | "shape" | "activeBar" | "background" | "stackId" | "minPointSize" | "maxBarSize" | "barSize" | "xAxisId" | "yAxisId" | "material" | "label"> & {
10
+ /** Static finish on corrected native bin rectangles; custom paint retains ownership. */
11
+ material?: BarMaterial | undefined;
12
+ /** Native shape extension with corrected quantitative x/width and original bin. */
13
+ shape?: ((props: HistogramShapeProps) => ReactElement) | undefined;
14
+ };
15
+ /** Reuses Bar visibility, native events/Cells, shared tooltip and interrupted/reduced Motion. */
16
+ export declare function HistogramSeries({ shape, material, seriesKey, ...props }: HistogramSeriesProps): import("react").JSX.Element;
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cloneElement, use, useId, useLayoutEffect, useRef, useState, } from "react";
4
+ import { Rectangle, useXAxisScale } from "recharts";
5
+ import { BarSeries } from "./bar-series.js";
6
+ import { HistogramContext } from "./histogram-chart.js";
7
+ import { HistogramMaterialFilter } from "./histogram-material.js";
8
+ /** Resolve native CSS paint extent before the browser paints the material. */
9
+ function HistogramPaint({ rectangle, material, bounds, }) {
10
+ const id = `kind-ui-histogram-${useId().replace(/[^a-zA-Z0-9_-]/g, "_")}-material`;
11
+ const group = useRef(null);
12
+ const [padding, setPadding] = useState(8);
13
+ useLayoutEffect(() => {
14
+ const path = group.current?.querySelector("path");
15
+ if (!path)
16
+ return;
17
+ const stroke = getComputedStyle(path).strokeWidth;
18
+ let width = Number.parseFloat(stroke);
19
+ if (stroke.endsWith("%")) {
20
+ const svg = path.ownerSVGElement;
21
+ const viewBox = svg?.viewBox.baseVal;
22
+ const width = viewBox?.width || svg?.clientWidth || 0;
23
+ const height = viewBox?.height || svg?.clientHeight || 0;
24
+ const diagonal = Math.hypot(width, height) / Math.SQRT2;
25
+ // SVG percentage stroke widths use the normalized viewport diagonal.
26
+ const percentage = Number.parseFloat(stroke);
27
+ const resolved = (percentage / 100) * diagonal;
28
+ setPadding(8 + (Number.isFinite(resolved) ? Math.max(0, resolved) / 2 : 0));
29
+ }
30
+ else {
31
+ width = Number.isFinite(width) ? Math.max(0, width) : 0;
32
+ setPadding(8 + width / 2);
33
+ }
34
+ });
35
+ return (_jsxs("g", { ref: group, children: [_jsx("defs", { "data-kind-ui": "histogram-material", "data-material": material, pointerEvents: "none", children: _jsx(HistogramMaterialFilter, { material: material, id: id, bounds: bounds, padding: padding }) }), cloneElement(rectangle, { filter: `url(#${id})` })] }));
36
+ }
37
+ function HistogramShape({ shape, material, ...props }) {
38
+ const scale = useXAxisScale(0);
39
+ const bin = props.payload;
40
+ const start = scale?.(bin.lower);
41
+ const end = scale?.(bin.upper);
42
+ if (start === undefined || end === undefined || !Number.isFinite(start) || !Number.isFinite(end))
43
+ return null;
44
+ const geometry = { ...props, x: Math.min(start, end), width: Math.abs(end - start), bin };
45
+ if (shape)
46
+ return shape(geometry);
47
+ const materialized = material !== undefined &&
48
+ material !== "plain" &&
49
+ props.filter === undefined &&
50
+ props.style?.filter === undefined &&
51
+ (props.height ?? 0) > 0 &&
52
+ geometry.width > 0;
53
+ const rectangle = (_jsx(Rectangle, { ...props, x: geometry.x, width: geometry.width, radius: 0, "data-kind-ui": "histogram-bin", "data-lower": bin.lower, "data-upper": bin.upper, "data-count": bin.count }));
54
+ return materialized ? (_jsx(HistogramPaint, { rectangle: rectangle, material: material, bounds: { x: geometry.x, y: props.y ?? 0, width: geometry.width, height: props.height ?? 0 } })) : (rectangle);
55
+ }
56
+ /** Reuses Bar visibility, native events/Cells, shared tooltip and interrupted/reduced Motion. */
57
+ export function HistogramSeries({ shape, material = "plain", seriesKey = "count", ...props }) {
58
+ const measure = use(HistogramContext);
59
+ if (measure === null)
60
+ throw new Error("HistogramSeries requires HistogramChart");
61
+ const materialized = material !== "plain" &&
62
+ shape === undefined &&
63
+ props.filter === undefined &&
64
+ props.style?.filter === undefined;
65
+ const renderShape = (native) => (_jsx(HistogramShape, { ...native, shape: shape, material: materialized ? material : undefined }));
66
+ return (_jsx(BarSeries, { ...props, seriesKey: seriesKey, dataKey: "value", xAxisId: 0, yAxisId: 0, minPointSize: 0, shape: renderShape, activeBar: renderShape }));
67
+ }