@alpic-ai/ui 0.0.0-dev.g14de318 → 0.0.0-dev.g162d798
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/dist/components/area-chart.d.mts +2 -0
- package/dist/components/area-chart.mjs +9 -3
- package/dist/components/bar-chart.d.mts +2 -0
- package/dist/components/bar-chart.mjs +9 -3
- package/dist/components/bar-list.d.mts +3 -0
- package/dist/components/bar-list.mjs +19 -7
- package/dist/components/chart-card.d.mts +1 -1
- package/dist/components/chart-card.mjs +1 -1
- package/dist/components/chart-container.d.mts +1 -1
- package/dist/components/chart-legend.d.mts +5 -0
- package/dist/components/chart-legend.mjs +11 -2
- package/dist/components/donut-chart.mjs +4 -0
- package/dist/components/form.mjs +1 -1
- package/dist/components/heatmap-chart.d.mts +8 -0
- package/dist/components/heatmap-chart.mjs +39 -8
- package/dist/components/line-chart.d.mts +2 -0
- package/dist/components/line-chart.mjs +10 -3
- package/dist/components/stat.d.mts +3 -1
- package/dist/components/stat.mjs +14 -4
- package/dist/lib/chart.mjs +16 -1
- package/package.json +23 -23
- package/src/components/area-chart.tsx +12 -4
- package/src/components/bar-chart.tsx +12 -4
- package/src/components/bar-list.tsx +21 -6
- package/src/components/chart-card.tsx +8 -6
- package/src/components/chart-container.tsx +2 -0
- package/src/components/chart-legend.tsx +10 -2
- package/src/components/donut-chart.tsx +1 -1
- package/src/components/form.tsx +1 -1
- package/src/components/heatmap-chart.tsx +62 -18
- package/src/components/line-chart.tsx +18 -5
- package/src/components/stat.tsx +10 -6
- package/src/lib/chart.ts +34 -0
- package/src/stories/wizard.stories.tsx +1 -1
|
@@ -21,6 +21,7 @@ interface AreaChartProps {
|
|
|
21
21
|
variant?: "stacked" | "grouped" | "expand";
|
|
22
22
|
curve?: keyof typeof CURVE_TYPE;
|
|
23
23
|
legend?: boolean;
|
|
24
|
+
legendAlign?: "left" | "center" | "right";
|
|
24
25
|
valueFlags?: boolean;
|
|
25
26
|
height?: number;
|
|
26
27
|
yAxisWidth?: number;
|
|
@@ -45,6 +46,7 @@ declare function AreaChart({
|
|
|
45
46
|
variant,
|
|
46
47
|
curve,
|
|
47
48
|
legend,
|
|
49
|
+
legendAlign,
|
|
48
50
|
valueFlags,
|
|
49
51
|
height,
|
|
50
52
|
yAxisWidth,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
3
|
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
|
-
import { orderByLuminance, resolveSeries } from "../lib/chart.mjs";
|
|
4
|
+
import { makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart.mjs";
|
|
5
5
|
import { useChartContext } from "./chart-container.mjs";
|
|
6
6
|
import { ChartLegend } from "./chart-legend.mjs";
|
|
7
7
|
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
@@ -14,7 +14,7 @@ const CURVE_TYPE = {
|
|
|
14
14
|
linear: "linear",
|
|
15
15
|
step: "stepAfter"
|
|
16
16
|
};
|
|
17
|
-
function AreaChart({ data, index, series, variant = "stacked", curve = "monotone", legend = false, valueFlags = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, lastValueLabel = false, texture = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
17
|
+
function AreaChart({ data, index, series, variant = "stacked", curve = "monotone", legend = false, legendAlign = "left", valueFlags = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, lastValueLabel = false, texture = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
18
18
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
19
19
|
const reactId = React$1.useId().replace(/:/g, "");
|
|
20
20
|
const animated = !useReducedMotion();
|
|
@@ -128,6 +128,10 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
|
|
|
128
128
|
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
129
129
|
width: "100%",
|
|
130
130
|
height: "100%",
|
|
131
|
+
initialDimension: {
|
|
132
|
+
width: 0,
|
|
133
|
+
height
|
|
134
|
+
},
|
|
131
135
|
children: /* @__PURE__ */ jsxs(AreaChart$1, {
|
|
132
136
|
data,
|
|
133
137
|
stackOffset: variant === "expand" ? "expand" : "none",
|
|
@@ -177,6 +181,7 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
|
|
|
177
181
|
/* @__PURE__ */ jsx(XAxis, {
|
|
178
182
|
dataKey: index,
|
|
179
183
|
...axis,
|
|
184
|
+
tick: makeXAxisTick(theme),
|
|
180
185
|
interval: "preserveStartEnd",
|
|
181
186
|
minTickGap: 44
|
|
182
187
|
}),
|
|
@@ -261,7 +266,8 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
|
|
|
261
266
|
})
|
|
262
267
|
}), legend && !isEmpty && /* @__PURE__ */ jsx(ChartLegend, {
|
|
263
268
|
items: legendItems,
|
|
264
|
-
|
|
269
|
+
align: legendAlign,
|
|
270
|
+
insetLeft: yAxisWidth
|
|
265
271
|
})]
|
|
266
272
|
});
|
|
267
273
|
}
|
|
@@ -10,6 +10,7 @@ interface BarChartProps {
|
|
|
10
10
|
series: ChartSeries[];
|
|
11
11
|
variant?: "stacked" | "grouped" | "expand";
|
|
12
12
|
legend?: boolean;
|
|
13
|
+
legendAlign?: "left" | "center" | "right";
|
|
13
14
|
valueLabels?: boolean;
|
|
14
15
|
height?: number;
|
|
15
16
|
yAxisWidth?: number;
|
|
@@ -32,6 +33,7 @@ declare function BarChart({
|
|
|
32
33
|
series,
|
|
33
34
|
variant,
|
|
34
35
|
legend,
|
|
36
|
+
legendAlign,
|
|
35
37
|
valueLabels,
|
|
36
38
|
height,
|
|
37
39
|
yAxisWidth,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
3
|
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
|
-
import { orderByLuminance, resolveSeries } from "../lib/chart.mjs";
|
|
4
|
+
import { makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart.mjs";
|
|
5
5
|
import { useChartContext } from "./chart-container.mjs";
|
|
6
6
|
import { ChartLegend } from "./chart-legend.mjs";
|
|
7
7
|
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
@@ -11,7 +11,7 @@ import { Bar, BarChart as BarChart$1, CartesianGrid, LabelList, ReferenceArea, R
|
|
|
11
11
|
//#region src/components/bar-chart.tsx
|
|
12
12
|
const BAR_RADIUS = 4;
|
|
13
13
|
const MAX_BAR_SIZE = 48;
|
|
14
|
-
function BarChart({ data, index, series, variant = "stacked", legend = false, valueLabels = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, texture = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
14
|
+
function BarChart({ data, index, series, variant = "stacked", legend = false, legendAlign = "left", valueLabels = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, texture = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
15
15
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
16
16
|
const reactId = React$1.useId().replace(/:/g, "");
|
|
17
17
|
const animated = !useReducedMotion();
|
|
@@ -111,6 +111,10 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, va
|
|
|
111
111
|
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
112
112
|
width: "100%",
|
|
113
113
|
height: "100%",
|
|
114
|
+
initialDimension: {
|
|
115
|
+
width: 0,
|
|
116
|
+
height
|
|
117
|
+
},
|
|
114
118
|
children: /* @__PURE__ */ jsxs(BarChart$1, {
|
|
115
119
|
data,
|
|
116
120
|
stackOffset: variant === "expand" ? "expand" : "none",
|
|
@@ -161,6 +165,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, va
|
|
|
161
165
|
/* @__PURE__ */ jsx(XAxis, {
|
|
162
166
|
dataKey: index,
|
|
163
167
|
...axis,
|
|
168
|
+
tick: makeXAxisTick(theme),
|
|
164
169
|
interval: "preserveStartEnd",
|
|
165
170
|
minTickGap: 44
|
|
166
171
|
}),
|
|
@@ -248,7 +253,8 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, va
|
|
|
248
253
|
})
|
|
249
254
|
}), legend && !isEmpty && /* @__PURE__ */ jsx(ChartLegend, {
|
|
250
255
|
items: legendItems,
|
|
251
|
-
|
|
256
|
+
align: legendAlign,
|
|
257
|
+
insetLeft: yAxisWidth
|
|
252
258
|
})]
|
|
253
259
|
});
|
|
254
260
|
}
|
|
@@ -8,6 +8,8 @@ interface BarListProps {
|
|
|
8
8
|
dataKey?: string;
|
|
9
9
|
maxItems?: number;
|
|
10
10
|
palette?: ChartPaletteName;
|
|
11
|
+
/** Renders bars in a single semantic hue (e.g. red for errors) rather than the palette ramp. */
|
|
12
|
+
semantic?: "error" | "warning" | "success";
|
|
11
13
|
loading?: boolean;
|
|
12
14
|
valueFormatter?: (value: number) => string;
|
|
13
15
|
labelFormatter?: (label: string | number) => string;
|
|
@@ -19,6 +21,7 @@ declare function BarList({
|
|
|
19
21
|
dataKey,
|
|
20
22
|
maxItems,
|
|
21
23
|
palette,
|
|
24
|
+
semantic,
|
|
22
25
|
loading,
|
|
23
26
|
valueFormatter,
|
|
24
27
|
labelFormatter,
|
|
@@ -7,11 +7,18 @@ import { useChartContext } from "./chart-container.mjs";
|
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import * as React$1 from "react";
|
|
9
9
|
//#region src/components/bar-list.tsx
|
|
10
|
+
const SEMANTIC_KEY = {
|
|
11
|
+
error: "destructive",
|
|
12
|
+
warning: "warning",
|
|
13
|
+
success: "success"
|
|
14
|
+
};
|
|
10
15
|
const PLACEHOLDER_HEIGHT = 168;
|
|
11
16
|
const RAMP_CEILING = .8;
|
|
17
|
+
const SEMANTIC_FLOOR = .62;
|
|
12
18
|
const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
|
|
13
|
-
function BarList({ data, index, dataKey = "value", maxItems, palette, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
14
|
-
const { paletteName } = useChartContext(palette);
|
|
19
|
+
function BarList({ data, index, dataKey = "value", maxItems, palette, semantic, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
20
|
+
const { paletteName, theme } = useChartContext(palette);
|
|
21
|
+
const accent = semantic ? theme[SEMANTIC_KEY[semantic]] : null;
|
|
15
22
|
const reducedMotion = useReducedMotion();
|
|
16
23
|
const [mounted, setMounted] = React$1.useState(false);
|
|
17
24
|
const [active, setActive] = React$1.useState(null);
|
|
@@ -26,16 +33,21 @@ function BarList({ data, index, dataKey = "value", maxItems, palette, loading =
|
|
|
26
33
|
}));
|
|
27
34
|
mapped.sort((lower, upper) => upper.value - lower.value);
|
|
28
35
|
const capped = maxItems ? mapped.slice(0, maxItems) : mapped;
|
|
29
|
-
return capped.map((row, rank) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
return capped.map((row, rank) => {
|
|
37
|
+
const rankFraction = capped.length > 1 ? rank / (capped.length - 1) : 0;
|
|
38
|
+
const accentWeight = Math.round((1 - rankFraction * (1 - SEMANTIC_FLOOR)) * 100);
|
|
39
|
+
return {
|
|
40
|
+
...row,
|
|
41
|
+
color: accent ? `color-mix(in oklab, ${accent} ${accentWeight}%, white)` : rampColor(paletteName, rankFraction * RAMP_CEILING)
|
|
42
|
+
};
|
|
43
|
+
});
|
|
33
44
|
}, [
|
|
34
45
|
data,
|
|
35
46
|
index,
|
|
36
47
|
dataKey,
|
|
37
48
|
maxItems,
|
|
38
|
-
paletteName
|
|
49
|
+
paletteName,
|
|
50
|
+
accent
|
|
39
51
|
]);
|
|
40
52
|
const maxValue = rows.reduce((max, row) => row.value > max ? row.value : max, 0);
|
|
41
53
|
const isEmpty = rows.length === 0;
|
|
@@ -8,7 +8,7 @@ interface ChartCardProps extends Omit<React$1.ComponentProps<"section">, "title"
|
|
|
8
8
|
title?: React$1.ReactNode;
|
|
9
9
|
description?: React$1.ReactNode;
|
|
10
10
|
action?: React$1.ReactNode;
|
|
11
|
-
accent?: "top" | "left";
|
|
11
|
+
accent?: "top" | "left" | "none";
|
|
12
12
|
}
|
|
13
13
|
declare function ChartCard({
|
|
14
14
|
palette,
|
|
@@ -12,7 +12,7 @@ function ChartCard({ palette = "magenta", kicker, title, description, action, ac
|
|
|
12
12
|
className: cn("chart-rise relative overflow-hidden rounded-xl border bg-card p-5 text-card-foreground shadow-shadow", isLeft && "pl-6", className),
|
|
13
13
|
...props,
|
|
14
14
|
children: [
|
|
15
|
-
/* @__PURE__ */ jsx("span", {
|
|
15
|
+
accent !== "none" && /* @__PURE__ */ jsx("span", {
|
|
16
16
|
"aria-hidden": true,
|
|
17
17
|
className: cn("absolute", isLeft ? "inset-y-0 left-0 w-[3px]" : "inset-x-0 top-0 h-[3px]"),
|
|
18
18
|
style: { background: lead }
|
|
@@ -17,4 +17,4 @@ declare function ChartContainer({
|
|
|
17
17
|
}): React$1.JSX.Element;
|
|
18
18
|
declare function useChartContext(paletteOverride?: ChartPaletteName): ChartContextValue;
|
|
19
19
|
//#endregion
|
|
20
|
-
export { ChartContainer, type ChartContextValue, useChartContext };
|
|
20
|
+
export { ChartContainer, type ChartContextValue, type ChartPaletteName, useChartContext };
|
|
@@ -6,10 +6,15 @@ interface ChartLegendItem {
|
|
|
6
6
|
}
|
|
7
7
|
interface ChartLegendProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
8
|
items: ChartLegendItem[];
|
|
9
|
+
align?: "left" | "center" | "right";
|
|
10
|
+
insetLeft?: number;
|
|
9
11
|
}
|
|
10
12
|
declare function ChartLegend({
|
|
11
13
|
items,
|
|
14
|
+
align,
|
|
15
|
+
insetLeft,
|
|
12
16
|
className,
|
|
17
|
+
style,
|
|
13
18
|
...props
|
|
14
19
|
}: ChartLegendProps): import("react").JSX.Element;
|
|
15
20
|
//#endregion
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/components/chart-legend.tsx
|
|
5
|
+
const ALIGN_CLASS = {
|
|
6
|
+
left: "justify-start",
|
|
7
|
+
center: "justify-center",
|
|
8
|
+
right: "justify-end"
|
|
9
|
+
};
|
|
5
10
|
function Swatch({ color, dashed }) {
|
|
6
11
|
return /* @__PURE__ */ jsx("span", {
|
|
7
12
|
"aria-hidden": true,
|
|
@@ -9,9 +14,13 @@ function Swatch({ color, dashed }) {
|
|
|
9
14
|
style: dashed ? { border: `1.5px solid ${color}` } : { background: color }
|
|
10
15
|
});
|
|
11
16
|
}
|
|
12
|
-
function ChartLegend({ items, className, ...props }) {
|
|
17
|
+
function ChartLegend({ items, align = "left", insetLeft, className, style, ...props }) {
|
|
13
18
|
return /* @__PURE__ */ jsx("div", {
|
|
14
|
-
className: cn("flex flex-wrap gap-x-4 gap-y-1.5", className),
|
|
19
|
+
className: cn("flex flex-wrap gap-x-4 gap-y-1.5", ALIGN_CLASS[align], className),
|
|
20
|
+
style: {
|
|
21
|
+
paddingLeft: align === "left" ? insetLeft : void 0,
|
|
22
|
+
...style
|
|
23
|
+
},
|
|
15
24
|
...props,
|
|
16
25
|
children: items.map((item) => /* @__PURE__ */ jsxs("span", {
|
|
17
26
|
className: "inline-flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.12em]",
|
|
@@ -73,6 +73,10 @@ function DonutChart({ data, index, dataKey = "value", variant = "donut", legend
|
|
|
73
73
|
children: [/* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
74
74
|
width: "100%",
|
|
75
75
|
height: "100%",
|
|
76
|
+
initialDimension: {
|
|
77
|
+
width: 0,
|
|
78
|
+
height
|
|
79
|
+
},
|
|
76
80
|
children: /* @__PURE__ */ jsxs(PieChart, { children: [/* @__PURE__ */ jsx("defs", { children: slices.map((slice, slot) => /* @__PURE__ */ jsxs("linearGradient", {
|
|
77
81
|
id: `donut-${reactId}-${slot}`,
|
|
78
82
|
x1: "0",
|
package/dist/components/form.mjs
CHANGED
|
@@ -65,7 +65,7 @@ function FormLabel({ className, required, tooltip, children, ...props }) {
|
|
|
65
65
|
}),
|
|
66
66
|
required && /* @__PURE__ */ jsx("span", {
|
|
67
67
|
"aria-hidden": true,
|
|
68
|
-
className: "type-text-sm font-medium text-required",
|
|
68
|
+
className: "type-text-sm font-medium text-required leading-none",
|
|
69
69
|
children: "*"
|
|
70
70
|
}),
|
|
71
71
|
tooltip && /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
@@ -11,11 +11,17 @@ interface HeatmapChartProps {
|
|
|
11
11
|
palette?: ChartPaletteName;
|
|
12
12
|
xLabels?: readonly string[];
|
|
13
13
|
yLabels?: readonly string[];
|
|
14
|
+
showAllXLabels?: boolean;
|
|
14
15
|
highlightPeak?: boolean;
|
|
15
16
|
loading?: boolean;
|
|
16
17
|
valueFormatter?: (value: number) => string;
|
|
17
18
|
xTickFormatter?: (label: string) => string;
|
|
18
19
|
yTickFormatter?: (label: string) => string;
|
|
20
|
+
tooltipMetrics?: ReadonlyArray<{
|
|
21
|
+
key: string;
|
|
22
|
+
label: string;
|
|
23
|
+
format?: (value: number) => string;
|
|
24
|
+
}>;
|
|
19
25
|
ariaLabel?: string;
|
|
20
26
|
className?: string;
|
|
21
27
|
}
|
|
@@ -28,11 +34,13 @@ declare function HeatmapChart({
|
|
|
28
34
|
palette,
|
|
29
35
|
xLabels,
|
|
30
36
|
yLabels,
|
|
37
|
+
showAllXLabels,
|
|
31
38
|
highlightPeak,
|
|
32
39
|
loading,
|
|
33
40
|
valueFormatter,
|
|
34
41
|
xTickFormatter,
|
|
35
42
|
yTickFormatter,
|
|
43
|
+
tooltipMetrics,
|
|
36
44
|
ariaLabel,
|
|
37
45
|
className
|
|
38
46
|
}: HeatmapChartProps): React$1.JSX.Element;
|
|
@@ -31,7 +31,7 @@ const uniqueInOrder = (values) => {
|
|
|
31
31
|
}
|
|
32
32
|
return ordered;
|
|
33
33
|
};
|
|
34
|
-
function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square", palette, xLabels, yLabels, highlightPeak = true, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), xTickFormatter, yTickFormatter, ariaLabel = "Activity heatmap", className }) {
|
|
34
|
+
function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square", palette, xLabels, yLabels, showAllXLabels = false, highlightPeak = true, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), xTickFormatter, yTickFormatter, tooltipMetrics, ariaLabel = "Activity heatmap", className }) {
|
|
35
35
|
const { paletteName, theme } = useChartContext(palette);
|
|
36
36
|
const [hovered, setHovered] = React$1.useState(null);
|
|
37
37
|
const [mounted, setMounted] = React$1.useState(false);
|
|
@@ -42,24 +42,31 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
|
|
|
42
42
|
const columns = xLabels ? [...xLabels] : uniqueInOrder(data.map((row) => String(row[xKey] ?? "")));
|
|
43
43
|
const rows = yLabels ? [...yLabels] : uniqueInOrder(data.map((row) => String(row[yKey] ?? "")));
|
|
44
44
|
const valueAt = /* @__PURE__ */ new Map();
|
|
45
|
-
|
|
45
|
+
const recordAt = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const row of data) {
|
|
47
|
+
const key = pairKey(String(row[yKey] ?? ""), String(row[xKey] ?? ""));
|
|
48
|
+
valueAt.set(key, Number(row[dataKey]) || 0);
|
|
49
|
+
recordAt.set(key, row);
|
|
50
|
+
}
|
|
46
51
|
let maxValue = 0;
|
|
47
52
|
let peakKey = "";
|
|
48
53
|
return {
|
|
49
54
|
columns,
|
|
50
55
|
rows,
|
|
51
56
|
cells: rows.flatMap((rowLabel, rowIndex) => columns.map((colLabel, colIndex) => {
|
|
52
|
-
const
|
|
57
|
+
const key = pairKey(rowLabel, colLabel);
|
|
58
|
+
const value = valueAt.get(key) ?? 0;
|
|
53
59
|
if (value > maxValue) {
|
|
54
60
|
maxValue = value;
|
|
55
|
-
peakKey =
|
|
61
|
+
peakKey = key;
|
|
56
62
|
}
|
|
57
63
|
return {
|
|
58
64
|
rowLabel,
|
|
59
65
|
colLabel,
|
|
60
66
|
rowIndex,
|
|
61
67
|
colIndex,
|
|
62
|
-
value
|
|
68
|
+
value,
|
|
69
|
+
record: recordAt.get(key) ?? null
|
|
63
70
|
};
|
|
64
71
|
})),
|
|
65
72
|
maxValue,
|
|
@@ -87,7 +94,7 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
|
|
|
87
94
|
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
|
|
88
95
|
const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
|
|
89
96
|
const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
|
|
90
|
-
const xStride = Math.ceil(columns.length / MAX_X_TICKS);
|
|
97
|
+
const xStride = showAllXLabels ? 1 : Math.ceil(columns.length / MAX_X_TICKS);
|
|
91
98
|
const cellX = (col) => PAD.left + col * 25;
|
|
92
99
|
const cellY = (row) => PAD.top + row * 25;
|
|
93
100
|
const formatX = (label) => xTickFormatter ? xTickFormatter(label) : label;
|
|
@@ -159,7 +166,8 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
|
|
|
159
166
|
y: event.clientY,
|
|
160
167
|
rowLabel: cell.rowLabel,
|
|
161
168
|
colLabel: cell.colLabel,
|
|
162
|
-
value: cell.value
|
|
169
|
+
value: cell.value,
|
|
170
|
+
record: cell.record
|
|
163
171
|
})
|
|
164
172
|
}, `hit-${pairKey(cell.rowLabel, cell.colLabel)}`))
|
|
165
173
|
]
|
|
@@ -177,7 +185,30 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
|
|
|
177
185
|
" · ",
|
|
178
186
|
formatX(hovered.colLabel)
|
|
179
187
|
]
|
|
180
|
-
}), /* @__PURE__ */
|
|
188
|
+
}), tooltipMetrics && tooltipMetrics.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
189
|
+
className: "flex flex-col gap-1",
|
|
190
|
+
children: tooltipMetrics.map((metric) => {
|
|
191
|
+
const raw = hovered.record ? Number(hovered.record[metric.key]) || 0 : 0;
|
|
192
|
+
const isActive = metric.key === dataKey;
|
|
193
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
194
|
+
className: "flex items-center justify-between gap-4 text-text-xs",
|
|
195
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
196
|
+
className: "inline-flex items-center gap-2 text-muted-foreground",
|
|
197
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
198
|
+
"aria-hidden": true,
|
|
199
|
+
className: "size-2 shrink-0 rounded-[2px]",
|
|
200
|
+
style: {
|
|
201
|
+
background: isActive ? heatColor(ramp, hovered.value / maxValue) : theme.mutedForeground,
|
|
202
|
+
opacity: isActive ? 1 : .35
|
|
203
|
+
}
|
|
204
|
+
}), metric.label]
|
|
205
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
206
|
+
className: cn("font-mono tabular-nums", isActive ? "font-semibold text-foreground" : "text-muted-foreground"),
|
|
207
|
+
children: (metric.format ?? valueFormatter)(raw)
|
|
208
|
+
})]
|
|
209
|
+
}, metric.key);
|
|
210
|
+
})
|
|
211
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
181
212
|
className: "flex items-center justify-between gap-4 text-text-xs",
|
|
182
213
|
children: [/* @__PURE__ */ jsxs("span", {
|
|
183
214
|
className: "inline-flex items-center gap-2 text-muted-foreground",
|
|
@@ -15,6 +15,7 @@ interface LineChartProps {
|
|
|
15
15
|
series: ChartSeries[];
|
|
16
16
|
curve?: keyof typeof CURVE_TYPE;
|
|
17
17
|
legend?: boolean;
|
|
18
|
+
legendAlign?: "left" | "center" | "right";
|
|
18
19
|
valueFlags?: boolean;
|
|
19
20
|
dots?: boolean;
|
|
20
21
|
height?: number;
|
|
@@ -38,6 +39,7 @@ declare function LineChart({
|
|
|
38
39
|
series,
|
|
39
40
|
curve,
|
|
40
41
|
legend,
|
|
42
|
+
legendAlign,
|
|
41
43
|
valueFlags,
|
|
42
44
|
dots,
|
|
43
45
|
height,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
3
|
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
|
-
import { resolveSeries } from "../lib/chart.mjs";
|
|
4
|
+
import { makeXAxisTick, resolveSeries } from "../lib/chart.mjs";
|
|
5
5
|
import { useChartContext } from "./chart-container.mjs";
|
|
6
6
|
import { ChartLegend } from "./chart-legend.mjs";
|
|
7
7
|
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
@@ -14,7 +14,7 @@ const CURVE_TYPE = {
|
|
|
14
14
|
linear: "linear",
|
|
15
15
|
step: "stepAfter"
|
|
16
16
|
};
|
|
17
|
-
function LineChart({ data, index, series, curve = "monotone", legend = false, valueFlags = false, dots = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, lastValueLabel = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
17
|
+
function LineChart({ data, index, series, curve = "monotone", legend = false, legendAlign = "left", valueFlags = false, dots = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, lastValueLabel = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
18
18
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
19
19
|
const animated = !useReducedMotion();
|
|
20
20
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
@@ -106,6 +106,10 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, va
|
|
|
106
106
|
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
107
107
|
width: "100%",
|
|
108
108
|
height: "100%",
|
|
109
|
+
initialDimension: {
|
|
110
|
+
width: 0,
|
|
111
|
+
height
|
|
112
|
+
},
|
|
109
113
|
children: /* @__PURE__ */ jsxs(LineChart$1, {
|
|
110
114
|
data,
|
|
111
115
|
margin,
|
|
@@ -118,12 +122,14 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, va
|
|
|
118
122
|
/* @__PURE__ */ jsx(XAxis, {
|
|
119
123
|
dataKey: index,
|
|
120
124
|
...axis,
|
|
125
|
+
tick: makeXAxisTick(theme),
|
|
121
126
|
interval: "preserveStartEnd",
|
|
122
127
|
minTickGap: 44
|
|
123
128
|
}),
|
|
124
129
|
/* @__PURE__ */ jsx(YAxis, {
|
|
125
130
|
...axis,
|
|
126
131
|
width: yAxisWidth,
|
|
132
|
+
domain: ["auto", "auto"],
|
|
127
133
|
tickFormatter: (value) => valueFormatter(value)
|
|
128
134
|
}),
|
|
129
135
|
/* @__PURE__ */ jsx(Tooltip, {
|
|
@@ -203,7 +209,8 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, va
|
|
|
203
209
|
})
|
|
204
210
|
}), legend && !isEmpty && /* @__PURE__ */ jsx(ChartLegend, {
|
|
205
211
|
items: legendItems,
|
|
206
|
-
|
|
212
|
+
align: legendAlign,
|
|
213
|
+
insetLeft: yAxisWidth
|
|
207
214
|
})]
|
|
208
215
|
});
|
|
209
216
|
}
|
|
@@ -13,16 +13,18 @@ interface StatDelta {
|
|
|
13
13
|
interface StatProps extends React$1.ComponentProps<"div"> {
|
|
14
14
|
value: React$1.ReactNode;
|
|
15
15
|
unit?: string;
|
|
16
|
-
delta?: StatDelta;
|
|
16
|
+
delta?: StatDelta | null;
|
|
17
17
|
sparkline?: number[] | Array<{
|
|
18
18
|
value: number;
|
|
19
19
|
}>;
|
|
20
|
+
semantic?: "error" | "warning" | "success";
|
|
20
21
|
}
|
|
21
22
|
declare function Stat({
|
|
22
23
|
value,
|
|
23
24
|
unit,
|
|
24
25
|
delta,
|
|
25
26
|
sparkline,
|
|
27
|
+
semantic,
|
|
26
28
|
className,
|
|
27
29
|
...props
|
|
28
30
|
}: StatProps): React$1.JSX.Element;
|
package/dist/components/stat.mjs
CHANGED
|
@@ -21,11 +21,17 @@ const toSparkData = (sparkline) => (sparkline ?? []).map((point, index) => typeo
|
|
|
21
21
|
index,
|
|
22
22
|
value: point.value
|
|
23
23
|
});
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const SEMANTIC_KEY = {
|
|
25
|
+
error: "destructive",
|
|
26
|
+
warning: "warning",
|
|
27
|
+
success: "success"
|
|
28
|
+
};
|
|
29
|
+
function Stat({ value, unit, delta, sparkline, semantic, className, ...props }) {
|
|
30
|
+
const { palette, theme } = useChartContext();
|
|
26
31
|
const gradientId = React$1.useId().replace(/:/g, "");
|
|
27
32
|
const sparkData = React$1.useMemo(() => toSparkData(sparkline), [sparkline]);
|
|
28
|
-
const
|
|
33
|
+
const hasSpark = sparkData.some((point) => point.value > 0);
|
|
34
|
+
const sparkColor = semantic ? theme[SEMANTIC_KEY[semantic]] : palette[0];
|
|
29
35
|
const sentiment = delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
|
|
30
36
|
return /* @__PURE__ */ jsxs("div", {
|
|
31
37
|
"data-slot": "stat",
|
|
@@ -48,11 +54,15 @@ function Stat({ value, unit, delta, sparkline, className, ...props }) {
|
|
|
48
54
|
children: [delta.direction === "up" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-3" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "size-3" }), delta.label ?? `${delta.value}%`]
|
|
49
55
|
})
|
|
50
56
|
]
|
|
51
|
-
}),
|
|
57
|
+
}), hasSpark && /* @__PURE__ */ jsx("div", {
|
|
52
58
|
className: "h-9 w-full",
|
|
53
59
|
children: /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
54
60
|
width: "100%",
|
|
55
61
|
height: "100%",
|
|
62
|
+
initialDimension: {
|
|
63
|
+
width: 0,
|
|
64
|
+
height: 36
|
|
65
|
+
},
|
|
56
66
|
children: /* @__PURE__ */ jsxs(AreaChart, {
|
|
57
67
|
data: sparkData,
|
|
58
68
|
margin: {
|
package/dist/lib/chart.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { luminance, paletteColor } from "./chart-palette.mjs";
|
|
2
|
+
import * as React$1 from "react";
|
|
2
3
|
//#region src/lib/chart.ts
|
|
3
4
|
const formatShare = (fraction) => `${(fraction * 100).toFixed(fraction >= .1 ? 0 : 1)}%`;
|
|
4
5
|
const semanticColor = (theme, semantic) => {
|
|
@@ -23,5 +24,19 @@ const resolveSeries = (series, palette, theme) => series.map((entry, index) => (
|
|
|
23
24
|
* stack order in Recharts — first entry sits at the bottom.
|
|
24
25
|
*/
|
|
25
26
|
const orderByLuminance = (series) => [...series].sort((lower, upper) => luminance(lower.color) - luminance(upper.color));
|
|
27
|
+
const makeXAxisTick = (theme) => ({ x, y, payload, index, visibleTicksCount }) => {
|
|
28
|
+
const isFirst = index === 0;
|
|
29
|
+
const isLast = visibleTicksCount != null && index === visibleTicksCount - 1;
|
|
30
|
+
const anchor = isFirst ? "start" : isLast ? "end" : "middle";
|
|
31
|
+
return React$1.createElement("text", {
|
|
32
|
+
x: Number(x ?? 0),
|
|
33
|
+
y: Number(y ?? 0),
|
|
34
|
+
dy: 12,
|
|
35
|
+
textAnchor: anchor,
|
|
36
|
+
fill: theme.axisForeground,
|
|
37
|
+
fontFamily: theme.fontMono,
|
|
38
|
+
fontSize: 10
|
|
39
|
+
}, String(payload?.value ?? ""));
|
|
40
|
+
};
|
|
26
41
|
//#endregion
|
|
27
|
-
export { formatShare, orderByLuminance, resolveSeries };
|
|
42
|
+
export { formatShare, makeXAxisTick, orderByLuminance, resolveSeries };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpic-ai/ui",
|
|
3
|
-
"version": "0.0.0-dev.
|
|
3
|
+
"version": "0.0.0-dev.g162d798",
|
|
4
4
|
"description": "Alpic design system — shared UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -23,32 +23,32 @@
|
|
|
23
23
|
"src"
|
|
24
24
|
],
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"lucide-react": "^1.
|
|
26
|
+
"lucide-react": "^1.21.0",
|
|
27
27
|
"react": "^19.2.7",
|
|
28
28
|
"react-dom": "^19.2.7",
|
|
29
|
-
"react-hook-form": "^7.
|
|
29
|
+
"react-hook-form": "^7.80.0",
|
|
30
30
|
"sonner": "^2.0.7",
|
|
31
31
|
"tailwindcss": "^4.3.1",
|
|
32
32
|
"tw-animate-css": "^1.4.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@radix-ui/react-accordion": "^1.2.
|
|
36
|
-
"@radix-ui/react-avatar": "^1.
|
|
37
|
-
"@radix-ui/react-checkbox": "^1.3.
|
|
38
|
-
"@radix-ui/react-collapsible": "^1.1.
|
|
39
|
-
"@radix-ui/react-dialog": "^1.1.
|
|
40
|
-
"@radix-ui/react-dropdown-menu": "^2.1.
|
|
41
|
-
"@radix-ui/react-label": "^2.1.
|
|
42
|
-
"@radix-ui/react-popover": "^1.1.
|
|
43
|
-
"@radix-ui/react-radio-group": "^1.4.
|
|
44
|
-
"@radix-ui/react-scroll-area": "^1.2.
|
|
45
|
-
"@radix-ui/react-select": "^2.3.
|
|
46
|
-
"@radix-ui/react-separator": "^1.1.
|
|
47
|
-
"@radix-ui/react-slot": "^1.
|
|
48
|
-
"@radix-ui/react-switch": "^1.3.
|
|
49
|
-
"@radix-ui/react-tabs": "^1.1.
|
|
50
|
-
"@radix-ui/react-toggle-group": "^1.1.
|
|
51
|
-
"@radix-ui/react-tooltip": "^1.2.
|
|
35
|
+
"@radix-ui/react-accordion": "^1.2.14",
|
|
36
|
+
"@radix-ui/react-avatar": "^1.2.0",
|
|
37
|
+
"@radix-ui/react-checkbox": "^1.3.5",
|
|
38
|
+
"@radix-ui/react-collapsible": "^1.1.14",
|
|
39
|
+
"@radix-ui/react-dialog": "^1.1.17",
|
|
40
|
+
"@radix-ui/react-dropdown-menu": "^2.1.18",
|
|
41
|
+
"@radix-ui/react-label": "^2.1.10",
|
|
42
|
+
"@radix-ui/react-popover": "^1.1.17",
|
|
43
|
+
"@radix-ui/react-radio-group": "^1.4.1",
|
|
44
|
+
"@radix-ui/react-scroll-area": "^1.2.12",
|
|
45
|
+
"@radix-ui/react-select": "^2.3.1",
|
|
46
|
+
"@radix-ui/react-separator": "^1.1.10",
|
|
47
|
+
"@radix-ui/react-slot": "^1.3.0",
|
|
48
|
+
"@radix-ui/react-switch": "^1.3.1",
|
|
49
|
+
"@radix-ui/react-tabs": "^1.1.15",
|
|
50
|
+
"@radix-ui/react-toggle-group": "^1.1.13",
|
|
51
|
+
"@radix-ui/react-tooltip": "^1.2.10",
|
|
52
52
|
"class-variance-authority": "^0.7.1",
|
|
53
53
|
"clsx": "^2.1.1",
|
|
54
54
|
"cmdk": "^1.1.1",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"@tailwindcss/postcss": "^4.3.1",
|
|
61
61
|
"@types/react": "19.2.17",
|
|
62
62
|
"@types/react-dom": "19.2.3",
|
|
63
|
-
"lucide-react": "^1.
|
|
64
|
-
"react-hook-form": "^7.
|
|
63
|
+
"lucide-react": "^1.21.0",
|
|
64
|
+
"react-hook-form": "^7.80.0",
|
|
65
65
|
"shx": "^0.4.0",
|
|
66
66
|
"sonner": "^2.0.7",
|
|
67
67
|
"tailwindcss": "^4.3.1",
|
|
68
|
-
"tsdown": "^0.22.
|
|
68
|
+
"tsdown": "^0.22.3",
|
|
69
69
|
"tw-animate-css": "^1.4.0",
|
|
70
70
|
"typescript": "^6.0.3"
|
|
71
71
|
},
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "recharts";
|
|
17
17
|
|
|
18
18
|
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
19
|
-
import { type ChartSeries, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
19
|
+
import { type ChartSeries, makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
20
20
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
21
21
|
import { cn } from "../lib/cn";
|
|
22
22
|
import { useChartContext } from "./chart-container";
|
|
@@ -39,6 +39,7 @@ export interface AreaChartProps {
|
|
|
39
39
|
variant?: "stacked" | "grouped" | "expand";
|
|
40
40
|
curve?: keyof typeof CURVE_TYPE;
|
|
41
41
|
legend?: boolean;
|
|
42
|
+
legendAlign?: "left" | "center" | "right";
|
|
42
43
|
valueFlags?: boolean;
|
|
43
44
|
height?: number;
|
|
44
45
|
yAxisWidth?: number;
|
|
@@ -60,6 +61,7 @@ function AreaChart({
|
|
|
60
61
|
variant = "stacked",
|
|
61
62
|
curve = "monotone",
|
|
62
63
|
legend = false,
|
|
64
|
+
legendAlign = "left",
|
|
63
65
|
valueFlags = false,
|
|
64
66
|
height = 200,
|
|
65
67
|
yAxisWidth = 48,
|
|
@@ -205,7 +207,7 @@ function AreaChart({
|
|
|
205
207
|
no data in range
|
|
206
208
|
</div>
|
|
207
209
|
) : (
|
|
208
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
210
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
|
|
209
211
|
<RechartsAreaChart
|
|
210
212
|
data={data as Record<string, string | number>[]}
|
|
211
213
|
stackOffset={variant === "expand" ? "expand" : "none"}
|
|
@@ -234,7 +236,13 @@ function AreaChart({
|
|
|
234
236
|
</defs>
|
|
235
237
|
|
|
236
238
|
<CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
|
|
237
|
-
<XAxis
|
|
239
|
+
<XAxis
|
|
240
|
+
dataKey={index}
|
|
241
|
+
{...axis}
|
|
242
|
+
tick={makeXAxisTick(theme)}
|
|
243
|
+
interval="preserveStartEnd"
|
|
244
|
+
minTickGap={44}
|
|
245
|
+
/>
|
|
238
246
|
<YAxis
|
|
239
247
|
{...axis}
|
|
240
248
|
width={yAxisWidth}
|
|
@@ -331,7 +339,7 @@ function AreaChart({
|
|
|
331
339
|
)}
|
|
332
340
|
</div>
|
|
333
341
|
|
|
334
|
-
{legend && !isEmpty && <ChartLegend items={legendItems}
|
|
342
|
+
{legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
|
|
335
343
|
</div>
|
|
336
344
|
);
|
|
337
345
|
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "recharts";
|
|
17
17
|
|
|
18
18
|
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
19
|
-
import { type ChartSeries, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
19
|
+
import { type ChartSeries, makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
20
20
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
21
21
|
import { cn } from "../lib/cn";
|
|
22
22
|
import type { ChartMarker } from "./area-chart";
|
|
@@ -33,6 +33,7 @@ export interface BarChartProps {
|
|
|
33
33
|
series: ChartSeries[];
|
|
34
34
|
variant?: "stacked" | "grouped" | "expand";
|
|
35
35
|
legend?: boolean;
|
|
36
|
+
legendAlign?: "left" | "center" | "right";
|
|
36
37
|
valueLabels?: boolean;
|
|
37
38
|
height?: number;
|
|
38
39
|
yAxisWidth?: number;
|
|
@@ -52,6 +53,7 @@ function BarChart({
|
|
|
52
53
|
series,
|
|
53
54
|
variant = "stacked",
|
|
54
55
|
legend = false,
|
|
56
|
+
legendAlign = "left",
|
|
55
57
|
valueLabels = false,
|
|
56
58
|
height = 200,
|
|
57
59
|
yAxisWidth = 48,
|
|
@@ -168,7 +170,7 @@ function BarChart({
|
|
|
168
170
|
no data in range
|
|
169
171
|
</div>
|
|
170
172
|
) : (
|
|
171
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
173
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
|
|
172
174
|
<RechartsBarChart
|
|
173
175
|
data={data as Record<string, string | number>[]}
|
|
174
176
|
stackOffset={variant === "expand" ? "expand" : "none"}
|
|
@@ -198,7 +200,13 @@ function BarChart({
|
|
|
198
200
|
</defs>
|
|
199
201
|
|
|
200
202
|
<CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
|
|
201
|
-
<XAxis
|
|
203
|
+
<XAxis
|
|
204
|
+
dataKey={index}
|
|
205
|
+
{...axis}
|
|
206
|
+
tick={makeXAxisTick(theme)}
|
|
207
|
+
interval="preserveStartEnd"
|
|
208
|
+
minTickGap={44}
|
|
209
|
+
/>
|
|
202
210
|
<YAxis
|
|
203
211
|
{...axis}
|
|
204
212
|
width={yAxisWidth}
|
|
@@ -301,7 +309,7 @@ function BarChart({
|
|
|
301
309
|
)}
|
|
302
310
|
</div>
|
|
303
311
|
|
|
304
|
-
{legend && !isEmpty && <ChartLegend items={legendItems}
|
|
312
|
+
{legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
|
|
305
313
|
</div>
|
|
306
314
|
);
|
|
307
315
|
}
|
|
@@ -15,15 +15,22 @@ export interface BarListProps {
|
|
|
15
15
|
dataKey?: string;
|
|
16
16
|
maxItems?: number;
|
|
17
17
|
palette?: ChartPaletteName;
|
|
18
|
+
/** Renders bars in a single semantic hue (e.g. red for errors) rather than the palette ramp. */
|
|
19
|
+
semantic?: "error" | "warning" | "success";
|
|
18
20
|
loading?: boolean;
|
|
19
21
|
valueFormatter?: (value: number) => string;
|
|
20
22
|
labelFormatter?: (label: string | number) => string;
|
|
21
23
|
className?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
const SEMANTIC_KEY = { error: "destructive", warning: "warning", success: "success" } as const;
|
|
27
|
+
|
|
24
28
|
const PLACEHOLDER_HEIGHT = 168;
|
|
25
29
|
// Cap the ramp off its palest end so low-rank bars stay legible on a light card.
|
|
26
30
|
const RAMP_CEILING = 0.8;
|
|
31
|
+
// Semantic bars keep a single hue: the lightest bar still holds this much colour
|
|
32
|
+
// (mixed toward white) so a long error list never fades to near-invisible.
|
|
33
|
+
const SEMANTIC_FLOOR = 0.62;
|
|
27
34
|
const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
|
|
28
35
|
|
|
29
36
|
function BarList({
|
|
@@ -32,12 +39,14 @@ function BarList({
|
|
|
32
39
|
dataKey = "value",
|
|
33
40
|
maxItems,
|
|
34
41
|
palette,
|
|
42
|
+
semantic,
|
|
35
43
|
loading = false,
|
|
36
44
|
valueFormatter = (value) => value.toLocaleString("en-US"),
|
|
37
45
|
labelFormatter,
|
|
38
46
|
className,
|
|
39
47
|
}: BarListProps) {
|
|
40
|
-
const { paletteName } = useChartContext(palette);
|
|
48
|
+
const { paletteName, theme } = useChartContext(palette);
|
|
49
|
+
const accent = semantic ? theme[SEMANTIC_KEY[semantic]] : null;
|
|
41
50
|
const reducedMotion = useReducedMotion();
|
|
42
51
|
const [mounted, setMounted] = React.useState(false);
|
|
43
52
|
const [active, setActive] = React.useState<number | null>(null);
|
|
@@ -55,11 +64,17 @@ function BarList({
|
|
|
55
64
|
}));
|
|
56
65
|
mapped.sort((lower, upper) => upper.value - lower.value);
|
|
57
66
|
const capped = maxItems ? mapped.slice(0, maxItems) : mapped;
|
|
58
|
-
return capped.map((row, rank) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
return capped.map((row, rank) => {
|
|
68
|
+
const rankFraction = capped.length > 1 ? rank / (capped.length - 1) : 0;
|
|
69
|
+
const accentWeight = Math.round((1 - rankFraction * (1 - SEMANTIC_FLOOR)) * 100);
|
|
70
|
+
return {
|
|
71
|
+
...row,
|
|
72
|
+
color: accent
|
|
73
|
+
? `color-mix(in oklab, ${accent} ${accentWeight}%, white)`
|
|
74
|
+
: rampColor(paletteName, rankFraction * RAMP_CEILING),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}, [data, index, dataKey, maxItems, paletteName, accent]);
|
|
63
78
|
|
|
64
79
|
const maxValue = rows.reduce((max, row) => (row.value > max ? row.value : max), 0);
|
|
65
80
|
const isEmpty = rows.length === 0;
|
|
@@ -12,7 +12,7 @@ export interface ChartCardProps extends Omit<React.ComponentProps<"section">, "t
|
|
|
12
12
|
title?: React.ReactNode;
|
|
13
13
|
description?: React.ReactNode;
|
|
14
14
|
action?: React.ReactNode;
|
|
15
|
-
accent?: "top" | "left";
|
|
15
|
+
accent?: "top" | "left" | "none";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function ChartCard({
|
|
@@ -40,11 +40,13 @@ function ChartCard({
|
|
|
40
40
|
)}
|
|
41
41
|
{...props}
|
|
42
42
|
>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
{accent !== "none" && (
|
|
44
|
+
<span
|
|
45
|
+
aria-hidden
|
|
46
|
+
className={cn("absolute", isLeft ? "inset-y-0 left-0 w-[3px]" : "inset-x-0 top-0 h-[3px]")}
|
|
47
|
+
style={{ background: lead }}
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
48
50
|
{(kicker || title || action) && (
|
|
49
51
|
<header className="mb-4 flex items-start justify-between gap-4">
|
|
50
52
|
<div className="flex flex-col gap-1">
|
|
@@ -6,6 +6,8 @@ import { type ChartTheme, useChartTheme } from "../hooks/use-chart-theme";
|
|
|
6
6
|
import { CHART_PALETTES, type ChartPaletteName } from "../lib/chart-palette";
|
|
7
7
|
import { cn } from "../lib/cn";
|
|
8
8
|
|
|
9
|
+
export type { ChartPaletteName } from "../lib/chart-palette";
|
|
10
|
+
|
|
9
11
|
interface ChartContextValue {
|
|
10
12
|
palette: readonly string[];
|
|
11
13
|
paletteName: ChartPaletteName;
|
|
@@ -10,8 +10,12 @@ export interface ChartLegendItem {
|
|
|
10
10
|
|
|
11
11
|
export interface ChartLegendProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
12
12
|
items: ChartLegendItem[];
|
|
13
|
+
align?: "left" | "center" | "right";
|
|
14
|
+
insetLeft?: number;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
const ALIGN_CLASS = { left: "justify-start", center: "justify-center", right: "justify-end" } as const;
|
|
18
|
+
|
|
15
19
|
function Swatch({ color, dashed }: { color: string; dashed?: boolean }) {
|
|
16
20
|
return (
|
|
17
21
|
<span
|
|
@@ -22,9 +26,13 @@ function Swatch({ color, dashed }: { color: string; dashed?: boolean }) {
|
|
|
22
26
|
);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
function ChartLegend({ items, className, ...props }: ChartLegendProps) {
|
|
29
|
+
function ChartLegend({ items, align = "left", insetLeft, className, style, ...props }: ChartLegendProps) {
|
|
26
30
|
return (
|
|
27
|
-
<div
|
|
31
|
+
<div
|
|
32
|
+
className={cn("flex flex-wrap gap-x-4 gap-y-1.5", ALIGN_CLASS[align], className)}
|
|
33
|
+
style={{ paddingLeft: align === "left" ? insetLeft : undefined, ...style }}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
28
36
|
{items.map((item) => (
|
|
29
37
|
<span
|
|
30
38
|
key={item.name}
|
|
@@ -99,7 +99,7 @@ function DonutChart({
|
|
|
99
99
|
) : (
|
|
100
100
|
<div className="flex flex-col items-center gap-5 @md:flex-row">
|
|
101
101
|
<div className="relative shrink-0" style={{ width: height, height }}>
|
|
102
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
102
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
|
|
103
103
|
<RechartsPieChart>
|
|
104
104
|
<defs>
|
|
105
105
|
{slices.map((slice, slot) => (
|
package/src/components/form.tsx
CHANGED
|
@@ -106,7 +106,7 @@ function FormLabel({ className, required, tooltip, children, ...props }: FormLab
|
|
|
106
106
|
{children}
|
|
107
107
|
</Label>
|
|
108
108
|
{required && (
|
|
109
|
-
<span aria-hidden className="type-text-sm font-medium text-required">
|
|
109
|
+
<span aria-hidden className="type-text-sm font-medium text-required leading-none">
|
|
110
110
|
*
|
|
111
111
|
</span>
|
|
112
112
|
)}
|
|
@@ -17,21 +17,26 @@ export interface HeatmapChartProps {
|
|
|
17
17
|
palette?: ChartPaletteName;
|
|
18
18
|
xLabels?: readonly string[];
|
|
19
19
|
yLabels?: readonly string[];
|
|
20
|
+
showAllXLabels?: boolean;
|
|
20
21
|
highlightPeak?: boolean;
|
|
21
22
|
loading?: boolean;
|
|
22
23
|
valueFormatter?: (value: number) => string;
|
|
23
24
|
xTickFormatter?: (label: string) => string;
|
|
24
25
|
yTickFormatter?: (label: string) => string;
|
|
26
|
+
tooltipMetrics?: ReadonlyArray<{ key: string; label: string; format?: (value: number) => string }>;
|
|
25
27
|
ariaLabel?: string;
|
|
26
28
|
className?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
type CellRecord = Record<string, string | number | null | undefined>;
|
|
32
|
+
|
|
29
33
|
interface HoverState {
|
|
30
34
|
x: number;
|
|
31
35
|
y: number;
|
|
32
36
|
rowLabel: string;
|
|
33
37
|
colLabel: string;
|
|
34
38
|
value: number;
|
|
39
|
+
record: CellRecord | null;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
const PLACEHOLDER_HEIGHT = 168;
|
|
@@ -71,11 +76,13 @@ function HeatmapChart({
|
|
|
71
76
|
palette,
|
|
72
77
|
xLabels,
|
|
73
78
|
yLabels,
|
|
79
|
+
showAllXLabels = false,
|
|
74
80
|
highlightPeak = true,
|
|
75
81
|
loading = false,
|
|
76
82
|
valueFormatter = (value) => value.toLocaleString("en-US"),
|
|
77
83
|
xTickFormatter,
|
|
78
84
|
yTickFormatter,
|
|
85
|
+
tooltipMetrics,
|
|
79
86
|
ariaLabel = "Activity heatmap",
|
|
80
87
|
className,
|
|
81
88
|
}: HeatmapChartProps) {
|
|
@@ -91,19 +98,23 @@ function HeatmapChart({
|
|
|
91
98
|
const columns = xLabels ? [...xLabels] : uniqueInOrder(data.map((row) => String(row[xKey] ?? "")));
|
|
92
99
|
const rows = yLabels ? [...yLabels] : uniqueInOrder(data.map((row) => String(row[yKey] ?? "")));
|
|
93
100
|
const valueAt = new Map<string, number>();
|
|
101
|
+
const recordAt = new Map<string, CellRecord>();
|
|
94
102
|
for (const row of data) {
|
|
95
|
-
|
|
103
|
+
const key = pairKey(String(row[yKey] ?? ""), String(row[xKey] ?? ""));
|
|
104
|
+
valueAt.set(key, Number(row[dataKey]) || 0);
|
|
105
|
+
recordAt.set(key, row);
|
|
96
106
|
}
|
|
97
107
|
let maxValue = 0;
|
|
98
108
|
let peakKey = "";
|
|
99
109
|
const cells = rows.flatMap((rowLabel, rowIndex) =>
|
|
100
110
|
columns.map((colLabel, colIndex) => {
|
|
101
|
-
const
|
|
111
|
+
const key = pairKey(rowLabel, colLabel);
|
|
112
|
+
const value = valueAt.get(key) ?? 0;
|
|
102
113
|
if (value > maxValue) {
|
|
103
114
|
maxValue = value;
|
|
104
|
-
peakKey =
|
|
115
|
+
peakKey = key;
|
|
105
116
|
}
|
|
106
|
-
return { rowLabel, colLabel, rowIndex, colIndex, value };
|
|
117
|
+
return { rowLabel, colLabel, rowIndex, colIndex, value, record: recordAt.get(key) ?? null };
|
|
107
118
|
}),
|
|
108
119
|
);
|
|
109
120
|
return { columns, rows, cells, maxValue, peakKey };
|
|
@@ -144,7 +155,7 @@ function HeatmapChart({
|
|
|
144
155
|
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
|
|
145
156
|
const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
|
|
146
157
|
const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
|
|
147
|
-
const xStride = Math.ceil(columns.length / MAX_X_TICKS);
|
|
158
|
+
const xStride = showAllXLabels ? 1 : Math.ceil(columns.length / MAX_X_TICKS);
|
|
148
159
|
const cellX = (col: number) => PAD.left + col * (CELL + GAP);
|
|
149
160
|
const cellY = (row: number) => PAD.top + row * (CELL + GAP);
|
|
150
161
|
const formatX = (label: string) => (xTickFormatter ? xTickFormatter(label) : label);
|
|
@@ -247,6 +258,7 @@ function HeatmapChart({
|
|
|
247
258
|
rowLabel: cell.rowLabel,
|
|
248
259
|
colLabel: cell.colLabel,
|
|
249
260
|
value: cell.value,
|
|
261
|
+
record: cell.record,
|
|
250
262
|
})
|
|
251
263
|
}
|
|
252
264
|
/>
|
|
@@ -264,19 +276,51 @@ function HeatmapChart({
|
|
|
264
276
|
<p className="mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider">
|
|
265
277
|
{formatY(hovered.rowLabel)} · {formatX(hovered.colLabel)}
|
|
266
278
|
</p>
|
|
267
|
-
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
{tooltipMetrics && tooltipMetrics.length > 0 ? (
|
|
280
|
+
<div className="flex flex-col gap-1">
|
|
281
|
+
{tooltipMetrics.map((metric) => {
|
|
282
|
+
const raw = hovered.record ? Number(hovered.record[metric.key]) || 0 : 0;
|
|
283
|
+
const isActive = metric.key === dataKey;
|
|
284
|
+
return (
|
|
285
|
+
<div key={metric.key} className="flex items-center justify-between gap-4 text-text-xs">
|
|
286
|
+
<span className="inline-flex items-center gap-2 text-muted-foreground">
|
|
287
|
+
<span
|
|
288
|
+
aria-hidden
|
|
289
|
+
className="size-2 shrink-0 rounded-[2px]"
|
|
290
|
+
style={{
|
|
291
|
+
background: isActive ? heatColor(ramp, hovered.value / maxValue) : theme.mutedForeground,
|
|
292
|
+
opacity: isActive ? 1 : 0.35,
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
{metric.label}
|
|
296
|
+
</span>
|
|
297
|
+
<span
|
|
298
|
+
className={cn(
|
|
299
|
+
"font-mono tabular-nums",
|
|
300
|
+
isActive ? "font-semibold text-foreground" : "text-muted-foreground",
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
{(metric.format ?? valueFormatter)(raw)}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
})}
|
|
308
|
+
</div>
|
|
309
|
+
) : (
|
|
310
|
+
<div className="flex items-center justify-between gap-4 text-text-xs">
|
|
311
|
+
<span className="inline-flex items-center gap-2 text-muted-foreground">
|
|
312
|
+
<span
|
|
313
|
+
aria-hidden
|
|
314
|
+
className="size-2 shrink-0 rounded-[2px]"
|
|
315
|
+
style={{ background: heatColor(ramp, hovered.value / maxValue) }}
|
|
316
|
+
/>
|
|
317
|
+
activity
|
|
318
|
+
</span>
|
|
319
|
+
<span className="font-mono font-semibold tabular-nums text-foreground">
|
|
320
|
+
{valueFormatter(hovered.value)}
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
280
324
|
</div>,
|
|
281
325
|
document.body,
|
|
282
326
|
)}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "recharts";
|
|
17
17
|
|
|
18
18
|
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
19
|
-
import { type ChartSeries, resolveSeries } from "../lib/chart";
|
|
19
|
+
import { type ChartSeries, makeXAxisTick, resolveSeries } from "../lib/chart";
|
|
20
20
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
21
21
|
import { cn } from "../lib/cn";
|
|
22
22
|
import type { ChartMarker } from "./area-chart";
|
|
@@ -32,6 +32,7 @@ export interface LineChartProps {
|
|
|
32
32
|
series: ChartSeries[];
|
|
33
33
|
curve?: keyof typeof CURVE_TYPE;
|
|
34
34
|
legend?: boolean;
|
|
35
|
+
legendAlign?: "left" | "center" | "right";
|
|
35
36
|
valueFlags?: boolean;
|
|
36
37
|
dots?: boolean;
|
|
37
38
|
height?: number;
|
|
@@ -52,6 +53,7 @@ function LineChart({
|
|
|
52
53
|
series,
|
|
53
54
|
curve = "monotone",
|
|
54
55
|
legend = false,
|
|
56
|
+
legendAlign = "left",
|
|
55
57
|
valueFlags = false,
|
|
56
58
|
dots = false,
|
|
57
59
|
height = 200,
|
|
@@ -170,11 +172,22 @@ function LineChart({
|
|
|
170
172
|
no data in range
|
|
171
173
|
</div>
|
|
172
174
|
) : (
|
|
173
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
175
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
|
|
174
176
|
<RechartsLineChart data={data as Record<string, string | number>[]} margin={margin}>
|
|
175
177
|
<CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
|
|
176
|
-
<XAxis
|
|
177
|
-
|
|
178
|
+
<XAxis
|
|
179
|
+
dataKey={index}
|
|
180
|
+
{...axis}
|
|
181
|
+
tick={makeXAxisTick(theme)}
|
|
182
|
+
interval="preserveStartEnd"
|
|
183
|
+
minTickGap={44}
|
|
184
|
+
/>
|
|
185
|
+
<YAxis
|
|
186
|
+
{...axis}
|
|
187
|
+
width={yAxisWidth}
|
|
188
|
+
domain={["auto", "auto"]}
|
|
189
|
+
tickFormatter={(value: number) => valueFormatter(value)}
|
|
190
|
+
/>
|
|
178
191
|
<Tooltip
|
|
179
192
|
offset={12}
|
|
180
193
|
allowEscapeViewBox={{ x: false, y: false }}
|
|
@@ -256,7 +269,7 @@ function LineChart({
|
|
|
256
269
|
)}
|
|
257
270
|
</div>
|
|
258
271
|
|
|
259
|
-
{legend && !isEmpty && <ChartLegend items={legendItems}
|
|
272
|
+
{legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
|
|
260
273
|
</div>
|
|
261
274
|
);
|
|
262
275
|
}
|
package/src/components/stat.tsx
CHANGED
|
@@ -31,8 +31,9 @@ export interface StatDelta {
|
|
|
31
31
|
export interface StatProps extends React.ComponentProps<"div"> {
|
|
32
32
|
value: React.ReactNode;
|
|
33
33
|
unit?: string;
|
|
34
|
-
delta?: StatDelta;
|
|
34
|
+
delta?: StatDelta | null;
|
|
35
35
|
sparkline?: number[] | Array<{ value: number }>;
|
|
36
|
+
semantic?: "error" | "warning" | "success";
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const toSparkData = (sparkline: StatProps["sparkline"]) =>
|
|
@@ -40,12 +41,15 @@ const toSparkData = (sparkline: StatProps["sparkline"]) =>
|
|
|
40
41
|
typeof point === "number" ? { index, value: point } : { index, value: point.value },
|
|
41
42
|
);
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
const SEMANTIC_KEY = { error: "destructive", warning: "warning", success: "success" } as const;
|
|
45
|
+
|
|
46
|
+
function Stat({ value, unit, delta, sparkline, semantic, className, ...props }: StatProps) {
|
|
47
|
+
const { palette, theme } = useChartContext();
|
|
45
48
|
const gradientId = React.useId().replace(/:/g, "");
|
|
46
49
|
const sparkData = React.useMemo(() => toSparkData(sparkline), [sparkline]);
|
|
50
|
+
const hasSpark = sparkData.some((point) => point.value > 0);
|
|
47
51
|
// biome-ignore lint/style/noNonNullAssertion: palettes are never empty
|
|
48
|
-
const sparkColor = palette[0]!;
|
|
52
|
+
const sparkColor = semantic ? theme[SEMANTIC_KEY[semantic]] : palette[0]!;
|
|
49
53
|
|
|
50
54
|
const sentiment =
|
|
51
55
|
delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
|
|
@@ -64,9 +68,9 @@ function Stat({ value, unit, delta, sparkline, className, ...props }: StatProps)
|
|
|
64
68
|
</DeltaPill>
|
|
65
69
|
)}
|
|
66
70
|
</div>
|
|
67
|
-
{
|
|
71
|
+
{hasSpark && (
|
|
68
72
|
<div className="h-9 w-full">
|
|
69
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
73
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height: 36 }}>
|
|
70
74
|
<AreaChart data={sparkData} margin={{ top: 4, right: 2, bottom: 0, left: 2 }}>
|
|
71
75
|
<defs>
|
|
72
76
|
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
package/src/lib/chart.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import type { ChartTheme } from "../hooks/use-chart-theme";
|
|
2
3
|
import { luminance, paletteColor } from "./chart-palette";
|
|
3
4
|
|
|
@@ -54,3 +55,36 @@ export const resolveSeries = (
|
|
|
54
55
|
*/
|
|
55
56
|
export const orderByLuminance = (series: ResolvedSeries[]) =>
|
|
56
57
|
[...series].sort((lower, upper) => luminance(lower.color) - luminance(upper.color));
|
|
58
|
+
|
|
59
|
+
export const makeXAxisTick =
|
|
60
|
+
(theme: ChartTheme) =>
|
|
61
|
+
({
|
|
62
|
+
x,
|
|
63
|
+
y,
|
|
64
|
+
payload,
|
|
65
|
+
index,
|
|
66
|
+
visibleTicksCount,
|
|
67
|
+
}: {
|
|
68
|
+
x?: string | number;
|
|
69
|
+
y?: string | number;
|
|
70
|
+
payload?: { value?: string | number };
|
|
71
|
+
index?: number;
|
|
72
|
+
visibleTicksCount?: number;
|
|
73
|
+
}) => {
|
|
74
|
+
const isFirst = index === 0;
|
|
75
|
+
const isLast = visibleTicksCount != null && index === visibleTicksCount - 1;
|
|
76
|
+
const anchor = isFirst ? "start" : isLast ? "end" : "middle";
|
|
77
|
+
return React.createElement(
|
|
78
|
+
"text",
|
|
79
|
+
{
|
|
80
|
+
x: Number(x ?? 0),
|
|
81
|
+
y: Number(y ?? 0),
|
|
82
|
+
dy: 12,
|
|
83
|
+
textAnchor: anchor,
|
|
84
|
+
fill: theme.axisForeground,
|
|
85
|
+
fontFamily: theme.fontMono,
|
|
86
|
+
fontSize: 10,
|
|
87
|
+
},
|
|
88
|
+
String(payload?.value ?? ""),
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -11,7 +11,7 @@ const steps = [
|
|
|
11
11
|
{ id: "branding", label: "Branding & metadata" },
|
|
12
12
|
{ id: "auth", label: "Authentication" },
|
|
13
13
|
{ id: "tools", label: "Tools & test cases" },
|
|
14
|
-
{ id: "review", label: "Review &
|
|
14
|
+
{ id: "review", label: "Review & export" },
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
/** The step rail is a vertical `TabsNav` composed with the consumer's selection state. */
|