@nexub/react 3.1.0 → 3.2.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.
@@ -0,0 +1,81 @@
1
+ import * as React from 'react';
2
+ /** A colour from the categorical chart palette, 1–8. */
3
+ export type ChartColor = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
4
+ export interface ChartSeries {
5
+ /** Key of the value in each data row. */
6
+ key: string;
7
+ /** Name shown in the legend and the data table. Defaults to `key`. */
8
+ label?: string;
9
+ /** Palette colour, 1–8. Defaults to the series' position. */
10
+ color?: ChartColor;
11
+ }
12
+ /** Rounds up to 1, 2, 2.5 or 5 × 10ⁿ so the four gridlines get round labels. */
13
+ export declare function niceMax(max: number): number;
14
+ declare const PLOT_HEIGHT: {
15
+ readonly sm: "h-32";
16
+ readonly md: "h-48";
17
+ readonly lg: "h-64";
18
+ };
19
+ export interface ChartLegendProps extends React.HTMLAttributes<HTMLUListElement> {
20
+ items: {
21
+ label: string;
22
+ color?: ChartColor;
23
+ }[];
24
+ }
25
+ /** A row of colour dots and labels. Charts render one for you when `legend` is on. */
26
+ export declare function ChartLegend({ items, className, ...props }: ChartLegendProps): React.JSX.Element;
27
+ interface CartesianProps<T> extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
28
+ /** One row per category, in order. */
29
+ data: T[];
30
+ /** Key of the category label in each row (the x axis). */
31
+ index: keyof T & string;
32
+ /** The values to plot, one entry per series. */
33
+ series: ChartSeries[];
34
+ /** Accessible name of the chart, e.g. "Revenue by month". */
35
+ title: string;
36
+ /** Formats axis labels, the hover text and the data table. */
37
+ valueFormat?: (value: number) => string;
38
+ /** Height of the plot area. */
39
+ size?: keyof typeof PLOT_HEIGHT;
40
+ /** Show the legend above the chart. Defaults to on for more than one series. */
41
+ legend?: boolean;
42
+ }
43
+ export interface BarChartProps<T> extends CartesianProps<T> {
44
+ /** Stack the series in one bar per category instead of side by side. */
45
+ stacked?: boolean;
46
+ }
47
+ /** Bars per category, grouped or stacked. Values must be 0 or more. */
48
+ export declare function BarChart<T extends object>({ stacked, ...props }: BarChartProps<T>): React.JSX.Element;
49
+ export interface LineChartProps<T> extends CartesianProps<T> {
50
+ /** Fill the area under each line. */
51
+ area?: boolean;
52
+ }
53
+ /** One line per series across the categories. Values must be 0 or more. */
54
+ export declare function LineChart<T extends object>({ area, ...props }: LineChartProps<T>): React.JSX.Element;
55
+ export interface DonutChartProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
56
+ data: {
57
+ label: string;
58
+ value: number;
59
+ color?: ChartColor;
60
+ }[];
61
+ /** Accessible name of the chart. */
62
+ title: string;
63
+ /** Content in the hole, such as the total. */
64
+ center?: React.ReactNode;
65
+ valueFormat?: (value: number) => string;
66
+ /** Show the legend beside the donut. Default on. */
67
+ legend?: boolean;
68
+ }
69
+ /** Parts of a whole. Keep it to six slices or fewer; group the rest as "Other". */
70
+ export declare function DonutChart({ data, title, center, valueFormat, legend, className, ...props }: DonutChartProps): React.JSX.Element;
71
+ export interface SparklineProps extends Omit<React.SVGAttributes<SVGSVGElement>, 'values' | 'color'> {
72
+ values: number[];
73
+ /** Accessible summary, e.g. "Revenue, last 12 weeks, up 8%". Without it the sparkline is decorative. */
74
+ label?: string;
75
+ color?: ChartColor;
76
+ /** Fill the area under the line. */
77
+ area?: boolean;
78
+ }
79
+ /** A tiny trend line with no axes, for stat cards and table cells. Fills its box (default 6rem × 2rem). */
80
+ export declare function Sparkline({ values, label, color, area, className, ...props }: SparklineProps): React.JSX.Element;
81
+ export {};
@@ -0,0 +1,174 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "../../utils/cn.js";
4
+ const FILL = ["fill-chart-1", "fill-chart-2", "fill-chart-3", "fill-chart-4", "fill-chart-5", "fill-chart-6", "fill-chart-7", "fill-chart-8"];
5
+ const STROKE = ["stroke-chart-1", "stroke-chart-2", "stroke-chart-3", "stroke-chart-4", "stroke-chart-5", "stroke-chart-6", "stroke-chart-7", "stroke-chart-8"];
6
+ const BG = ["bg-chart-1", "bg-chart-2", "bg-chart-3", "bg-chart-4", "bg-chart-5", "bg-chart-6", "bg-chart-7", "bg-chart-8"];
7
+ const colorIndex = (color, i) => ((color ?? i % 8 + 1) - 1) % 8;
8
+ function niceMax(max) {
9
+ if (!(max > 0)) return 1;
10
+ const pow = 10 ** Math.floor(Math.log10(max));
11
+ const step = [1, 2, 2.5, 5, 10].find((s) => s * pow >= max);
12
+ return step * pow;
13
+ }
14
+ const defaultFormat = (n) => n.toLocaleString();
15
+ const PLOT_HEIGHT = { sm: "h-32", md: "h-48", lg: "h-64" };
16
+ const VB_W = 300;
17
+ const VB_H = 100;
18
+ function ChartLegend({ items, className, ...props }) {
19
+ return /* @__PURE__ */ jsx("ul", { className: cn("flex flex-wrap gap-x-4 gap-y-1 text-label-md text-[var(--color-on-surface-variant)]", className), ...props, children: items.map((item, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1.5", children: [
20
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn("size-2.5 shrink-0 rounded-full", BG[colorIndex(item.color, i)]) }),
21
+ item.label
22
+ ] }, item.label)) });
23
+ }
24
+ function Frame({
25
+ data,
26
+ index,
27
+ series,
28
+ title,
29
+ valueFormat = defaultFormat,
30
+ size = "md",
31
+ legend,
32
+ max,
33
+ className,
34
+ children,
35
+ ...props
36
+ }) {
37
+ const labels = series.map((s) => s.label ?? s.key);
38
+ const ticks = [1, 0.75, 0.5, 0.25, 0];
39
+ return /* @__PURE__ */ jsxs("figure", { "aria-label": title, className: cn("m-0 flex flex-col gap-3", className), ...props, children: [
40
+ (legend ?? series.length > 1) && /* @__PURE__ */ jsx(ChartLegend, { "aria-hidden": "true", items: series.map((s, i) => ({ label: labels[i], color: s.color })) }),
41
+ /* @__PURE__ */ jsxs("div", { "aria-hidden": "true", className: "flex gap-2", children: [
42
+ /* @__PURE__ */ jsx("div", { className: cn("flex flex-col justify-between text-end text-label-sm tabular-nums text-[var(--color-on-surface-muted)]", PLOT_HEIGHT[size]), children: ticks.map((t) => /* @__PURE__ */ jsx("span", { className: "flex h-0 items-center justify-end", children: valueFormat(max * t) }, t)) }),
43
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
44
+ /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${VB_W} ${VB_H}`, preserveAspectRatio: "none", className: cn("w-full overflow-visible rtl:-scale-x-100", PLOT_HEIGHT[size]), children: [
45
+ ticks.map((t) => /* @__PURE__ */ jsx(
46
+ "line",
47
+ {
48
+ x1: "0",
49
+ x2: VB_W,
50
+ y1: VB_H * (1 - t),
51
+ y2: VB_H * (1 - t),
52
+ strokeWidth: "1",
53
+ vectorEffect: "non-scaling-stroke",
54
+ className: t === 0 ? "stroke-[var(--color-outline)]" : "stroke-[var(--color-outline-variant)]"
55
+ },
56
+ t
57
+ )),
58
+ children
59
+ ] }),
60
+ /* @__PURE__ */ jsx("div", { className: "grid auto-cols-fr grid-flow-col text-center text-label-sm text-[var(--color-on-surface-muted)]", children: data.map((row, i) => /* @__PURE__ */ jsx("span", { className: "truncate px-0.5", children: String(row[index]) }, i)) })
61
+ ] })
62
+ ] }),
63
+ /* @__PURE__ */ jsxs("table", { className: "sr-only", children: [
64
+ /* @__PURE__ */ jsx("caption", { children: title }),
65
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
66
+ /* @__PURE__ */ jsx("th", { scope: "col", children: index }),
67
+ labels.map((l) => /* @__PURE__ */ jsx("th", { scope: "col", children: l }, l))
68
+ ] }) }),
69
+ /* @__PURE__ */ jsx("tbody", { children: data.map((row, i) => /* @__PURE__ */ jsxs("tr", { children: [
70
+ /* @__PURE__ */ jsx("th", { scope: "row", children: String(row[index]) }),
71
+ series.map((s) => /* @__PURE__ */ jsx("td", { children: valueFormat(Number(row[s.key]) || 0) }, s.key))
72
+ ] }, i)) })
73
+ ] })
74
+ ] });
75
+ }
76
+ const valueOf = (row, key) => Math.max(0, Number(row[key]) || 0);
77
+ function BarChart({ stacked = false, ...props }) {
78
+ const { data, series, index, valueFormat = defaultFormat } = props;
79
+ const max = niceMax(Math.max(0, ...data.map((row) => stacked ? series.reduce((sum, s) => sum + valueOf(row, s.key), 0) : Math.max(0, ...series.map((s) => valueOf(row, s.key))))));
80
+ const slot = VB_W / Math.max(1, data.length);
81
+ const groupW = slot * 0.7;
82
+ const barW = stacked ? groupW : groupW / series.length;
83
+ return /* @__PURE__ */ jsx(Frame, { ...props, max, children: data.map((row, i) => {
84
+ let base = 0;
85
+ return /* @__PURE__ */ jsx("g", { children: series.map((s, j) => {
86
+ const v = valueOf(row, s.key);
87
+ const h = v / max * VB_H;
88
+ const x = i * slot + (slot - groupW) / 2 + (stacked ? 0 : j * barW);
89
+ const y = VB_H - base - h;
90
+ if (stacked) base += h;
91
+ return /* @__PURE__ */ jsx("rect", { x, y, width: Math.max(0, barW - (stacked ? 0 : 1)), height: h, className: FILL[colorIndex(s.color, j)], children: /* @__PURE__ */ jsx("title", { children: `${String(row[index])} \xB7 ${s.label ?? s.key}: ${valueFormat(v)}` }) }, s.key);
92
+ }) }, i);
93
+ }) });
94
+ }
95
+ function LineChart({ area = false, ...props }) {
96
+ const { data, series, index, valueFormat = defaultFormat } = props;
97
+ const max = niceMax(Math.max(0, ...data.flatMap((row) => series.map((s) => valueOf(row, s.key)))));
98
+ const slot = VB_W / Math.max(1, data.length);
99
+ const x = (i) => i * slot + slot / 2;
100
+ const y = (v) => VB_H - v / max * VB_H;
101
+ return /* @__PURE__ */ jsx(Frame, { ...props, max, children: series.map((s, j) => {
102
+ const c = colorIndex(s.color, j);
103
+ const pts = data.map((row, i) => `${x(i)},${y(valueOf(row, s.key))}`);
104
+ return /* @__PURE__ */ jsxs("g", { children: [
105
+ area && data.length > 0 && /* @__PURE__ */ jsx("polygon", { points: `${x(0)},${VB_H} ${pts.join(" ")} ${x(data.length - 1)},${VB_H}`, className: cn(FILL[c], "opacity-15") }),
106
+ /* @__PURE__ */ jsx("polyline", { points: pts.join(" "), fill: "none", strokeWidth: "2", strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke", className: STROKE[c] }),
107
+ data.map((row, i) => (
108
+ // A wide, invisible hit area per point carries the hover text.
109
+ /* @__PURE__ */ jsx("rect", { x: x(i) - slot / 2, y: "0", width: slot, height: VB_H, className: "fill-transparent", children: /* @__PURE__ */ jsx("title", { children: `${String(row[index])} \xB7 ${s.label ?? s.key}: ${valueFormat(valueOf(row, s.key))}` }) }, i)
110
+ ))
111
+ ] }, s.key);
112
+ }) });
113
+ }
114
+ function DonutChart({ data, title, center, valueFormat = defaultFormat, legend = true, className, ...props }) {
115
+ const total = data.reduce((sum, d) => sum + Math.max(0, d.value), 0) || 1;
116
+ const r = 15.9155;
117
+ let offset = 25;
118
+ return /* @__PURE__ */ jsxs("figure", { "aria-label": title, className: cn("m-0 flex flex-wrap items-center gap-6", className), ...props, children: [
119
+ /* @__PURE__ */ jsxs("div", { "aria-hidden": "true", className: "relative size-40 shrink-0", children: [
120
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 42 42", className: "size-full rtl:-scale-x-100", children: [
121
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "21", r, fill: "none", strokeWidth: "6", className: "stroke-[var(--color-surface-container-high)]" }),
122
+ data.map((d, i) => {
123
+ const pct = Math.max(0, d.value) / total * 100;
124
+ const dash = `${pct} ${100 - pct}`;
125
+ const el = /* @__PURE__ */ jsx("circle", { cx: "21", cy: "21", r, fill: "none", strokeWidth: "6", strokeDasharray: dash, strokeDashoffset: offset, className: STROKE[colorIndex(d.color, i)], children: /* @__PURE__ */ jsx("title", { children: `${d.label}: ${valueFormat(d.value)} (${Math.round(pct)}%)` }) }, d.label);
126
+ offset -= pct;
127
+ return el;
128
+ })
129
+ ] }),
130
+ center != null && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center text-center text-title-md text-[var(--color-on-surface)]", children: center })
131
+ ] }),
132
+ legend && /* @__PURE__ */ jsx(ChartLegend, { "aria-hidden": "true", className: "flex-col", items: data.map((d) => ({ label: d.label, color: d.color })) }),
133
+ /* @__PURE__ */ jsxs("table", { className: "sr-only", children: [
134
+ /* @__PURE__ */ jsx("caption", { children: title }),
135
+ /* @__PURE__ */ jsx("tbody", { children: data.map((d) => /* @__PURE__ */ jsxs("tr", { children: [
136
+ /* @__PURE__ */ jsx("th", { scope: "row", children: d.label }),
137
+ /* @__PURE__ */ jsx("td", { children: valueFormat(d.value) }),
138
+ /* @__PURE__ */ jsxs("td", { children: [
139
+ Math.round(Math.max(0, d.value) / total * 100),
140
+ "%"
141
+ ] })
142
+ ] }, d.label)) })
143
+ ] })
144
+ ] });
145
+ }
146
+ function Sparkline({ values, label, color = 1, area = false, className, ...props }) {
147
+ const min = Math.min(...values);
148
+ const span = Math.max(...values) - min || 1;
149
+ const step = values.length > 1 ? VB_W / (values.length - 1) : 0;
150
+ const pts = values.map((v, i) => `${i * step},${VB_H - 4 - (v - min) / span * (VB_H - 8)}`).join(" ");
151
+ const c = colorIndex(color, 0);
152
+ return /* @__PURE__ */ jsxs(
153
+ "svg",
154
+ {
155
+ viewBox: `0 0 ${VB_W} ${VB_H}`,
156
+ preserveAspectRatio: "none",
157
+ ...label ? { role: "img", "aria-label": label } : { "aria-hidden": true },
158
+ className: cn("h-8 w-24 overflow-visible rtl:-scale-x-100", className),
159
+ ...props,
160
+ children: [
161
+ area && values.length > 1 && /* @__PURE__ */ jsx("polygon", { points: `0,${VB_H} ${pts} ${VB_W},${VB_H}`, className: cn(FILL[c], "opacity-15") }),
162
+ /* @__PURE__ */ jsx("polyline", { points: pts, fill: "none", strokeWidth: "1.5", strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke", className: STROKE[c] })
163
+ ]
164
+ }
165
+ );
166
+ }
167
+ export {
168
+ BarChart,
169
+ ChartLegend,
170
+ DonutChart,
171
+ LineChart,
172
+ Sparkline,
173
+ niceMax
174
+ };
@@ -0,0 +1,2 @@
1
+ export { BarChart, LineChart, DonutChart, Sparkline, ChartLegend, niceMax } from './Chart.js';
2
+ export type { BarChartProps, LineChartProps, DonutChartProps, SparklineProps, ChartLegendProps, ChartSeries, ChartColor } from './Chart.js';
@@ -0,0 +1,9 @@
1
+ import { BarChart, LineChart, DonutChart, Sparkline, ChartLegend, niceMax } from "./Chart.js";
2
+ export {
3
+ BarChart,
4
+ ChartLegend,
5
+ DonutChart,
6
+ LineChart,
7
+ Sparkline,
8
+ niceMax
9
+ };
package/dist/index.d.ts CHANGED
@@ -36,6 +36,8 @@ export type { NumberInputProps } from './components/NumberInput/index.js';
36
36
  export { MoneyInput } from './components/MoneyInput/index.js';
37
37
  export type { MoneyInputProps } from './components/MoneyInput/index.js';
38
38
  export { Amount } from './components/Amount/index.js';
39
+ export { BarChart, LineChart, DonutChart, Sparkline, ChartLegend, niceMax } from './components/Chart/index.js';
40
+ export type { BarChartProps, LineChartProps, DonutChartProps, SparklineProps, ChartLegendProps, ChartSeries, ChartColor } from './components/Chart/index.js';
39
41
  export type { AmountProps } from './components/Amount/index.js';
40
42
  export { NumberTicker } from './components/NumberTicker/index.js';
41
43
  export type { NumberTickerProps, NumberTickerPart } from './components/NumberTicker/index.js';
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { Textarea, textareaVariants } from "./components/Textarea/index.js";
21
21
  import { NumberInput } from "./components/NumberInput/index.js";
22
22
  import { MoneyInput } from "./components/MoneyInput/index.js";
23
23
  import { Amount } from "./components/Amount/index.js";
24
+ import { BarChart, LineChart, DonutChart, Sparkline, ChartLegend, niceMax } from "./components/Chart/index.js";
24
25
  import { NumberTicker } from "./components/NumberTicker/index.js";
25
26
  import { SearchInput } from "./components/SearchInput/index.js";
26
27
  import { PasswordInput } from "./components/PasswordInput/index.js";
@@ -121,12 +122,14 @@ export {
121
122
  Avatar,
122
123
  AvatarGroup,
123
124
  Badge,
125
+ BarChart,
124
126
  Box,
125
127
  Breadcrumb,
126
128
  Button,
127
129
  ButtonGroup,
128
130
  Card,
129
131
  CardLink,
132
+ ChartLegend,
130
133
  CheckList,
131
134
  CheckListItem,
132
135
  Checkbox,
@@ -145,6 +148,7 @@ export {
145
148
  DatePicker,
146
149
  DateRangePicker,
147
150
  Dialog,
151
+ DonutChart,
148
152
  Drawer,
149
153
  DropdownMenu,
150
154
  EmptyState,
@@ -168,6 +172,7 @@ export {
168
172
  Kbd,
169
173
  KeyboardShortcut,
170
174
  Label,
175
+ LineChart,
171
176
  Link,
172
177
  ListTransition,
173
178
  Logo,
@@ -208,6 +213,7 @@ export {
208
213
  Slider,
209
214
  SourceChip,
210
215
  SourceList,
216
+ Sparkline,
211
217
  Spinner,
212
218
  SplitButton,
213
219
  Stack,
@@ -249,6 +255,7 @@ export {
249
255
  labelVariants,
250
256
  linkVariants,
251
257
  logoVariants,
258
+ niceMax,
252
259
  progressFillVariants,
253
260
  progressTrackVariants,
254
261
  ratingVariants,