@alpic-ai/ui 1.143.0 → 1.145.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/area-chart.mjs +5 -9
- package/dist/components/bar-chart.mjs +6 -14
- package/dist/components/bar-list.mjs +2 -9
- package/dist/components/chart-tooltip.mjs +54 -27
- package/dist/components/donut-chart.mjs +1 -3
- package/dist/components/form.mjs +2 -0
- package/dist/components/heatmap-chart.d.mts +6 -0
- package/dist/components/heatmap-chart.mjs +146 -116
- package/dist/components/line-chart.mjs +5 -9
- package/dist/components/sidebar.mjs +1 -1
- package/dist/components/skeleton.mjs +1 -1
- package/dist/components/stat.mjs +4 -4
- package/dist/lib/chart-palette.mjs +20 -17
- package/package.json +2 -2
- package/src/components/area-chart.tsx +12 -10
- package/src/components/bar-chart.tsx +13 -11
- package/src/components/bar-list.tsx +1 -9
- package/src/components/chart-tooltip.tsx +22 -6
- package/src/components/donut-chart.tsx +1 -4
- package/src/components/form.tsx +3 -1
- package/src/components/heatmap-chart.tsx +49 -5
- package/src/components/line-chart.tsx +12 -10
- package/src/components/skeleton.tsx +1 -1
- package/src/components/stat.tsx +4 -4
- package/src/lib/chart-palette.ts +20 -17
- package/dist/hooks/use-reduced-motion.d.mts +0 -4
- package/dist/hooks/use-reduced-motion.mjs +0 -16
- package/src/hooks/use-reduced-motion.ts +0 -17
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
3
|
import { makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart.mjs";
|
|
5
4
|
import { useChartContext } from "./chart-container.mjs";
|
|
6
5
|
import { ChartLegend } from "./chart-legend.mjs";
|
|
7
6
|
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
7
|
+
import { Skeleton } from "./skeleton.mjs";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
import * as React$1 from "react";
|
|
10
10
|
import { Area, AreaChart as AreaChart$1, CartesianGrid, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
@@ -17,7 +17,6 @@ const CURVE_TYPE = {
|
|
|
17
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
|
-
const animated = !useReducedMotion();
|
|
21
20
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
22
21
|
const stacked = variant === "stacked" || variant === "expand";
|
|
23
22
|
const rendered = stacked ? orderByLuminance(resolved) : resolved;
|
|
@@ -115,14 +114,11 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
|
|
|
115
114
|
const isEmpty = data.length === 0 || rendered.length === 0;
|
|
116
115
|
return /* @__PURE__ */ jsxs("div", {
|
|
117
116
|
"data-slot": "area-chart",
|
|
118
|
-
className: cn("flex w-full flex-col gap-3", className),
|
|
117
|
+
className: cn("flex w-full flex-col gap-3", "[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none", className),
|
|
119
118
|
children: [/* @__PURE__ */ jsx("div", {
|
|
120
119
|
className: "w-full",
|
|
121
120
|
style: { height },
|
|
122
|
-
children: loading ? /* @__PURE__ */
|
|
123
|
-
className: "flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
|
|
124
|
-
children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
|
|
125
|
-
}) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
121
|
+
children: loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-lg" }) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
126
122
|
className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
|
|
127
123
|
children: "no data in range"
|
|
128
124
|
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
@@ -183,7 +179,7 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
|
|
|
183
179
|
...axis,
|
|
184
180
|
tick: makeXAxisTick(theme),
|
|
185
181
|
interval: "preserveStartEnd",
|
|
186
|
-
minTickGap:
|
|
182
|
+
minTickGap: 56
|
|
187
183
|
}),
|
|
188
184
|
/* @__PURE__ */ jsx(YAxis, {
|
|
189
185
|
...axis,
|
|
@@ -238,7 +234,7 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
|
|
|
238
234
|
fill: fillFor(entry, slot),
|
|
239
235
|
dot: false,
|
|
240
236
|
activeDot: activeDotFor(entry),
|
|
241
|
-
isAnimationActive:
|
|
237
|
+
isAnimationActive: false,
|
|
242
238
|
animationDuration: 650,
|
|
243
239
|
animationEasing: "ease-out",
|
|
244
240
|
children: lastValueLabel && /* @__PURE__ */ jsx(LabelList, {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
3
|
import { makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart.mjs";
|
|
5
4
|
import { useChartContext } from "./chart-container.mjs";
|
|
6
5
|
import { ChartLegend } from "./chart-legend.mjs";
|
|
7
6
|
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
7
|
+
import { Skeleton } from "./skeleton.mjs";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
import * as React$1 from "react";
|
|
10
10
|
import { Bar, BarChart as BarChart$1, CartesianGrid, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
@@ -14,7 +14,6 @@ const MAX_BAR_SIZE = 48;
|
|
|
14
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
|
-
const animated = !useReducedMotion();
|
|
18
17
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
19
18
|
const stacked = variant === "stacked" || variant === "expand";
|
|
20
19
|
const rendered = stacked ? orderByLuminance(resolved) : resolved;
|
|
@@ -98,14 +97,11 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
|
|
|
98
97
|
const isEmpty = data.length === 0 || rendered.length === 0;
|
|
99
98
|
return /* @__PURE__ */ jsxs("div", {
|
|
100
99
|
"data-slot": "bar-chart",
|
|
101
|
-
className: cn("flex w-full flex-col gap-3", className),
|
|
100
|
+
className: cn("flex w-full flex-col gap-3", "[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none", className),
|
|
102
101
|
children: [/* @__PURE__ */ jsx("div", {
|
|
103
102
|
className: "w-full",
|
|
104
103
|
style: { height },
|
|
105
|
-
children: loading ? /* @__PURE__ */
|
|
106
|
-
className: "flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
|
|
107
|
-
children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
|
|
108
|
-
}) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
104
|
+
children: loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-lg" }) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
109
105
|
className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
|
|
110
106
|
children: "no data in range"
|
|
111
107
|
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
@@ -167,7 +163,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
|
|
|
167
163
|
...axis,
|
|
168
164
|
tick: makeXAxisTick(theme),
|
|
169
165
|
interval: "preserveStartEnd",
|
|
170
|
-
minTickGap:
|
|
166
|
+
minTickGap: 56
|
|
171
167
|
}),
|
|
172
168
|
/* @__PURE__ */ jsx(YAxis, {
|
|
173
169
|
...axis,
|
|
@@ -220,12 +216,8 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
|
|
|
220
216
|
strokeWidth: 0,
|
|
221
217
|
radius: radiusFor(slot),
|
|
222
218
|
maxBarSize: MAX_BAR_SIZE,
|
|
223
|
-
activeBar:
|
|
224
|
-
|
|
225
|
-
stroke: theme.card,
|
|
226
|
-
strokeWidth: 1
|
|
227
|
-
},
|
|
228
|
-
isAnimationActive: animated,
|
|
219
|
+
activeBar: false,
|
|
220
|
+
isAnimationActive: false,
|
|
229
221
|
animationDuration: 650,
|
|
230
222
|
animationEasing: "ease-out",
|
|
231
223
|
children: valueLabels && (!stacked || rendered.length === 1) && /* @__PURE__ */ jsx(LabelList, {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
3
|
import { rampColor } from "../lib/chart-palette.mjs";
|
|
5
4
|
import { formatShare } from "../lib/chart.mjs";
|
|
6
5
|
import { useChartContext } from "./chart-container.mjs";
|
|
@@ -19,12 +18,7 @@ const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
|
|
|
19
18
|
function BarList({ data, index, dataKey = "value", maxItems, palette, semantic, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
20
19
|
const { paletteName, theme } = useChartContext(palette);
|
|
21
20
|
const accent = semantic ? theme[SEMANTIC_KEY[semantic]] : null;
|
|
22
|
-
const reducedMotion = useReducedMotion();
|
|
23
|
-
const [mounted, setMounted] = React$1.useState(false);
|
|
24
21
|
const [active, setActive] = React$1.useState(null);
|
|
25
|
-
React$1.useEffect(() => {
|
|
26
|
-
setMounted(true);
|
|
27
|
-
}, []);
|
|
28
22
|
const total = React$1.useMemo(() => data.reduce((sum, row) => sum + (Number(row[dataKey]) || 0), 0), [data, dataKey]);
|
|
29
23
|
const rows = React$1.useMemo(() => {
|
|
30
24
|
const mapped = data.map((row) => ({
|
|
@@ -67,7 +61,7 @@ function BarList({ data, index, dataKey = "value", maxItems, palette, semantic,
|
|
|
67
61
|
className: cn("flex w-full flex-col", className),
|
|
68
62
|
children: rows.map((row, slot) => {
|
|
69
63
|
const fraction = maxValue > 0 ? row.value / maxValue : 0;
|
|
70
|
-
const fillWidth =
|
|
64
|
+
const fillWidth = `${fraction * 100}%`;
|
|
71
65
|
const dimmed = active !== null && active !== slot;
|
|
72
66
|
const isActive = active === slot;
|
|
73
67
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -85,8 +79,7 @@ function BarList({ data, index, dataKey = "value", maxItems, palette, semantic,
|
|
|
85
79
|
width: fillWidth,
|
|
86
80
|
background: `linear-gradient(90deg, ${row.color}, color-mix(in oklab, ${row.color} 82%, transparent))`,
|
|
87
81
|
boxShadow: `inset 0 0 0 1px ${row.color}`,
|
|
88
|
-
opacity: dimmed ? .45 : 1
|
|
89
|
-
transition: reducedMotion ? void 0 : "width 700ms ease-out"
|
|
82
|
+
opacity: dimmed ? .45 : 1
|
|
90
83
|
},
|
|
91
84
|
children: /* @__PURE__ */ jsx("span", {
|
|
92
85
|
className: "pointer-events-none absolute top-1/2 -translate-y-1/2 truncate type-text-xs font-medium text-white",
|
|
@@ -3,10 +3,16 @@ import { cn } from "../lib/cn.mjs";
|
|
|
3
3
|
import { useChartContext } from "./chart-container.mjs";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/components/chart-tooltip.tsx
|
|
6
|
+
const MAX_TOOLTIP_ROWS = 8;
|
|
6
7
|
function ChartTooltipContent({ active, payload, label, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, hideLabel, showTotal, totalLabel = "Total", className }) {
|
|
7
8
|
const { theme } = useChartContext();
|
|
8
9
|
if (!active || !payload?.length) return null;
|
|
9
|
-
const
|
|
10
|
+
const toNumber = (value) => typeof value === "number" ? value : Number(value ?? 0);
|
|
11
|
+
const total = payload.reduce((sum, item) => sum + toNumber(item.value), 0);
|
|
12
|
+
const sorted = [...payload].sort((lower, upper) => toNumber(upper.value) - toNumber(lower.value));
|
|
13
|
+
const rows = sorted.slice(0, MAX_TOOLTIP_ROWS);
|
|
14
|
+
const overflow = sorted.slice(MAX_TOOLTIP_ROWS);
|
|
15
|
+
const overflowTotal = overflow.reduce((sum, item) => sum + toNumber(item.value), 0);
|
|
10
16
|
return /* @__PURE__ */ jsxs("div", {
|
|
11
17
|
className: cn("min-w-[130px] rounded-lg border px-3 py-2.5 shadow-lg", "border-border bg-popover text-popover-foreground", className),
|
|
12
18
|
children: [!hideLabel && label !== void 0 && /* @__PURE__ */ jsx("p", {
|
|
@@ -14,37 +20,58 @@ function ChartTooltipContent({ active, payload, label, valueFormatter = (value)
|
|
|
14
20
|
children: labelFormatter ? labelFormatter(label) : label
|
|
15
21
|
}), /* @__PURE__ */ jsxs("div", {
|
|
16
22
|
className: "flex flex-col gap-1",
|
|
17
|
-
children: [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
children: [
|
|
24
|
+
rows.map((item, index) => {
|
|
25
|
+
const swatch = [
|
|
26
|
+
item.color,
|
|
27
|
+
item.stroke,
|
|
28
|
+
item.fill
|
|
29
|
+
].find((candidate) => typeof candidate === "string" && candidate.length > 0 && !candidate.startsWith("url(")) ?? theme.mutedForeground;
|
|
30
|
+
const numeric = toNumber(item.value);
|
|
31
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
32
|
+
className: "flex items-center justify-between gap-4 text-text-xs",
|
|
33
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
34
|
+
className: "inline-flex items-center gap-2 text-muted-foreground",
|
|
35
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
36
|
+
"aria-hidden": true,
|
|
37
|
+
className: "size-2 shrink-0 rounded-[2px]",
|
|
38
|
+
style: { background: swatch }
|
|
39
|
+
}), item.name]
|
|
40
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
41
|
+
className: "font-mono font-semibold tabular-nums text-foreground",
|
|
42
|
+
children: valueFormatter(numeric)
|
|
43
|
+
})]
|
|
44
|
+
}, `${item.dataKey ?? index}`);
|
|
45
|
+
}),
|
|
46
|
+
overflow.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
25
47
|
className: "flex items-center justify-between gap-4 text-text-xs",
|
|
26
48
|
children: [/* @__PURE__ */ jsxs("span", {
|
|
27
|
-
className: "inline-flex items-center gap-2 text-
|
|
28
|
-
children: [
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
className: "inline-flex items-center gap-2 text-quaternary-foreground",
|
|
50
|
+
children: [
|
|
51
|
+
/* @__PURE__ */ jsx("span", {
|
|
52
|
+
"aria-hidden": true,
|
|
53
|
+
className: "size-2 shrink-0"
|
|
54
|
+
}),
|
|
55
|
+
"+",
|
|
56
|
+
overflow.length,
|
|
57
|
+
" more"
|
|
58
|
+
]
|
|
59
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
60
|
+
className: "font-mono font-semibold tabular-nums text-quaternary-foreground",
|
|
61
|
+
children: valueFormatter(overflowTotal)
|
|
62
|
+
})]
|
|
63
|
+
}),
|
|
64
|
+
showTotal && /* @__PURE__ */ jsxs("div", {
|
|
65
|
+
className: "mt-1 flex items-center justify-between gap-4 border-border border-t pt-1.5 text-text-xs",
|
|
66
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
67
|
+
className: "font-mono text-quaternary-foreground uppercase tracking-wider text-[10px]",
|
|
68
|
+
children: totalLabel
|
|
33
69
|
}), /* @__PURE__ */ jsx("span", {
|
|
34
70
|
className: "font-mono font-semibold tabular-nums text-foreground",
|
|
35
|
-
children: valueFormatter(
|
|
71
|
+
children: valueFormatter(total)
|
|
36
72
|
})]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
className: "mt-1 flex items-center justify-between gap-4 border-border border-t pt-1.5 text-text-xs",
|
|
40
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
41
|
-
className: "font-mono text-quaternary-foreground uppercase tracking-wider text-[10px]",
|
|
42
|
-
children: totalLabel
|
|
43
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
44
|
-
className: "font-mono font-semibold tabular-nums text-foreground",
|
|
45
|
-
children: valueFormatter(total)
|
|
46
|
-
})]
|
|
47
|
-
})]
|
|
73
|
+
})
|
|
74
|
+
]
|
|
48
75
|
})]
|
|
49
76
|
});
|
|
50
77
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
3
|
import { paletteColor } from "../lib/chart-palette.mjs";
|
|
5
4
|
import { formatShare } from "../lib/chart.mjs";
|
|
6
5
|
import { useChartContext } from "./chart-container.mjs";
|
|
@@ -21,7 +20,6 @@ const GEOMETRY = {
|
|
|
21
20
|
function DonutChart({ data, index, dataKey = "value", variant = "donut", legend = false, paddingAngle = 1, height = 220, palette, centerLabel = "total", loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
22
21
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
23
22
|
const reactId = React$1.useId().replace(/:/g, "");
|
|
24
|
-
const animated = !useReducedMotion();
|
|
25
23
|
const [active, setActive] = React$1.useState(null);
|
|
26
24
|
const rowRefs = React$1.useRef([]);
|
|
27
25
|
const slices = React$1.useMemo(() => {
|
|
@@ -104,7 +102,7 @@ function DonutChart({ data, index, dataKey = "value", variant = "donut", legend
|
|
|
104
102
|
cornerRadius: 2,
|
|
105
103
|
stroke: theme.card,
|
|
106
104
|
strokeWidth: 1.5,
|
|
107
|
-
isAnimationActive:
|
|
105
|
+
isAnimationActive: false,
|
|
108
106
|
animationDuration: 650,
|
|
109
107
|
animationEasing: "ease-out",
|
|
110
108
|
onMouseEnter: (_entry, sliceIndex) => setActive(sliceIndex),
|
package/dist/components/form.mjs
CHANGED
|
@@ -41,10 +41,12 @@ const useFormField = () => {
|
|
|
41
41
|
const FormItemContext = React$1.createContext({});
|
|
42
42
|
function FormItem({ className, ...props }) {
|
|
43
43
|
const id = React$1.useId();
|
|
44
|
+
const { name } = React$1.useContext(FormFieldContext);
|
|
44
45
|
return /* @__PURE__ */ jsx(FormItemContext.Provider, {
|
|
45
46
|
value: { id },
|
|
46
47
|
children: /* @__PURE__ */ jsx("div", {
|
|
47
48
|
"data-slot": "form-item",
|
|
49
|
+
"data-field-name": name,
|
|
48
50
|
className: cn("grid gap-2", className),
|
|
49
51
|
...props
|
|
50
52
|
})
|
|
@@ -22,6 +22,9 @@ interface HeatmapChartProps {
|
|
|
22
22
|
label: string;
|
|
23
23
|
format?: (value: number) => string;
|
|
24
24
|
}>;
|
|
25
|
+
tooltipTitleKey?: string;
|
|
26
|
+
colorScale?: "linear" | "rank";
|
|
27
|
+
showLegend?: boolean;
|
|
25
28
|
ariaLabel?: string;
|
|
26
29
|
className?: string;
|
|
27
30
|
}
|
|
@@ -41,6 +44,9 @@ declare function HeatmapChart({
|
|
|
41
44
|
xTickFormatter,
|
|
42
45
|
yTickFormatter,
|
|
43
46
|
tooltipMetrics,
|
|
47
|
+
tooltipTitleKey,
|
|
48
|
+
colorScale,
|
|
49
|
+
showLegend,
|
|
44
50
|
ariaLabel,
|
|
45
51
|
className
|
|
46
52
|
}: 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, showAllXLabels = false, highlightPeak = true, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), xTickFormatter, yTickFormatter, tooltipMetrics, 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, tooltipTitleKey, colorScale = "linear", showLegend = false, 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);
|
|
@@ -81,6 +81,19 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
|
|
|
81
81
|
yLabels
|
|
82
82
|
]);
|
|
83
83
|
const isEmpty = columns.length === 0 || rows.length === 0 || maxValue <= 0;
|
|
84
|
+
const colorFractionOf = React$1.useMemo(() => {
|
|
85
|
+
if (colorScale === "rank") {
|
|
86
|
+
const sorted = [...new Set(cells.map((cell) => cell.value).filter((value) => value > 0))].sort((lower, upper) => lower - upper);
|
|
87
|
+
const RANK_FLOOR = .18;
|
|
88
|
+
const fractionByValue = new Map(sorted.map((value, index) => [value, sorted.length > 1 ? RANK_FLOOR + (1 - RANK_FLOOR) * (index / (sorted.length - 1)) : 1]));
|
|
89
|
+
return (value) => value > 0 ? fractionByValue.get(value) ?? 0 : 0;
|
|
90
|
+
}
|
|
91
|
+
return (value) => maxValue > 0 ? value / maxValue : 0;
|
|
92
|
+
}, [
|
|
93
|
+
cells,
|
|
94
|
+
colorScale,
|
|
95
|
+
maxValue
|
|
96
|
+
]);
|
|
84
97
|
if (loading) return /* @__PURE__ */ jsxs("div", {
|
|
85
98
|
className: cn("flex items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs", className),
|
|
86
99
|
style: { minHeight: PLACEHOLDER_HEIGHT },
|
|
@@ -91,7 +104,7 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
|
|
|
91
104
|
style: { minHeight: PLACEHOLDER_HEIGHT },
|
|
92
105
|
children: "no data in range"
|
|
93
106
|
});
|
|
94
|
-
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
|
|
107
|
+
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light, theme.isDark);
|
|
95
108
|
const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
|
|
96
109
|
const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
|
|
97
110
|
const xStride = showAllXLabels ? 1 : Math.ceil(columns.length / MAX_X_TICKS);
|
|
@@ -104,125 +117,142 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
|
|
|
104
117
|
return /* @__PURE__ */ jsxs("div", {
|
|
105
118
|
"data-slot": "heatmap-chart",
|
|
106
119
|
className: cn("w-full", className),
|
|
107
|
-
children: [
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
120
|
+
children: [
|
|
121
|
+
/* @__PURE__ */ jsx("div", {
|
|
122
|
+
className: "overflow-x-auto",
|
|
123
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
124
|
+
viewBox: `0 0 ${totalWidth} ${totalHeight}`,
|
|
125
|
+
className: "block min-w-[480px]",
|
|
126
|
+
style: {
|
|
127
|
+
width: "100%",
|
|
128
|
+
height: "auto"
|
|
129
|
+
},
|
|
130
|
+
role: "img",
|
|
131
|
+
"aria-label": ariaLabel,
|
|
132
|
+
onMouseLeave: () => setHovered(null),
|
|
133
|
+
children: [
|
|
134
|
+
columns.map((label, col) => col % xStride === 0 ? /* @__PURE__ */ jsx("text", {
|
|
135
|
+
x: cellX(col) + CELL / 2,
|
|
136
|
+
y: PAD.top - 7,
|
|
137
|
+
textAnchor: "middle",
|
|
138
|
+
className: "fill-quaternary-foreground font-mono text-[8.5px]",
|
|
139
|
+
children: formatX(label)
|
|
140
|
+
}, `x-${label}`) : null),
|
|
141
|
+
rows.map((label, row) => /* @__PURE__ */ jsx("text", {
|
|
142
|
+
x: PAD.left - 8,
|
|
143
|
+
y: cellY(row) + CELL / 2 + 3,
|
|
144
|
+
textAnchor: "end",
|
|
145
|
+
className: "fill-quaternary-foreground font-mono text-[8.5px]",
|
|
146
|
+
children: formatY(label)
|
|
147
|
+
}, `y-${label}`)),
|
|
148
|
+
cells.map((cell) => {
|
|
149
|
+
const fraction = colorFractionOf(cell.value);
|
|
150
|
+
const fill = heatColor(ramp, fraction);
|
|
151
|
+
const key = pairKey(cell.rowLabel, cell.colLabel);
|
|
152
|
+
const isPeak = highlightPeak && key === peakKey;
|
|
153
|
+
if (variant === "dot") return /* @__PURE__ */ jsx("circle", {
|
|
154
|
+
cx: cellX(cell.colIndex) + CELL / 2,
|
|
155
|
+
cy: cellY(cell.rowIndex) + CELL / 2,
|
|
156
|
+
r: CELL / 2 * (DOT_MIN + DOT_RANGE * fraction),
|
|
157
|
+
fill,
|
|
158
|
+
stroke: isPeak ? theme.foreground : void 0,
|
|
159
|
+
strokeWidth: isPeak ? 1.4 : void 0
|
|
160
|
+
}, key);
|
|
161
|
+
return /* @__PURE__ */ jsx("rect", {
|
|
162
|
+
x: cellX(cell.colIndex),
|
|
163
|
+
y: cellY(cell.rowIndex),
|
|
164
|
+
width: CELL,
|
|
165
|
+
height: CELL,
|
|
166
|
+
rx: 2.5,
|
|
167
|
+
fill,
|
|
168
|
+
stroke: isPeak ? theme.foreground : void 0,
|
|
169
|
+
strokeWidth: isPeak ? 1.4 : void 0
|
|
170
|
+
}, key);
|
|
171
|
+
}),
|
|
172
|
+
cells.map((cell) => /* @__PURE__ */ jsx("rect", {
|
|
148
173
|
x: cellX(cell.colIndex),
|
|
149
174
|
y: cellY(cell.rowIndex),
|
|
150
|
-
width:
|
|
151
|
-
height:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
fill: "transparent",
|
|
164
|
-
onMouseMove: (event) => setHovered({
|
|
165
|
-
x: event.clientX,
|
|
166
|
-
y: event.clientY,
|
|
167
|
-
rowLabel: cell.rowLabel,
|
|
168
|
-
colLabel: cell.colLabel,
|
|
169
|
-
value: cell.value,
|
|
170
|
-
record: cell.record
|
|
171
|
-
})
|
|
172
|
-
}, `hit-${pairKey(cell.rowLabel, cell.colLabel)}`))
|
|
173
|
-
]
|
|
174
|
-
})
|
|
175
|
-
}), mounted && hovered && createPortal(/* @__PURE__ */ jsxs("div", {
|
|
176
|
-
className: "pointer-events-none fixed z-50 min-w-[130px] rounded-lg border border-border bg-popover px-3 py-2.5 text-popover-foreground shadow-lg",
|
|
177
|
-
style: {
|
|
178
|
-
left: tooltipLeft,
|
|
179
|
-
top: tooltipTop
|
|
180
|
-
},
|
|
181
|
-
children: [/* @__PURE__ */ jsxs("p", {
|
|
182
|
-
className: "mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider",
|
|
183
|
-
children: [
|
|
184
|
-
formatY(hovered.rowLabel),
|
|
185
|
-
" · ",
|
|
186
|
-
formatX(hovered.colLabel)
|
|
187
|
-
]
|
|
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);
|
|
175
|
+
width: 25,
|
|
176
|
+
height: 25,
|
|
177
|
+
fill: "transparent",
|
|
178
|
+
onMouseMove: (event) => setHovered({
|
|
179
|
+
x: event.clientX,
|
|
180
|
+
y: event.clientY,
|
|
181
|
+
rowLabel: cell.rowLabel,
|
|
182
|
+
colLabel: cell.colLabel,
|
|
183
|
+
value: cell.value,
|
|
184
|
+
record: cell.record
|
|
185
|
+
})
|
|
186
|
+
}, `hit-${pairKey(cell.rowLabel, cell.colLabel)}`))
|
|
187
|
+
]
|
|
210
188
|
})
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
189
|
+
}),
|
|
190
|
+
showLegend && /* @__PURE__ */ jsxs("div", {
|
|
191
|
+
className: "mt-3 flex items-center justify-end gap-2",
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ jsx("span", {
|
|
194
|
+
className: "font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]",
|
|
195
|
+
children: "Less"
|
|
196
|
+
}),
|
|
197
|
+
/* @__PURE__ */ jsx("span", {
|
|
216
198
|
"aria-hidden": true,
|
|
217
|
-
className: "
|
|
218
|
-
style: { background:
|
|
219
|
-
}),
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
199
|
+
className: "h-2 w-24 rounded-full border border-border",
|
|
200
|
+
style: { background: `linear-gradient(90deg, ${ramp.join(", ")})` }
|
|
201
|
+
}),
|
|
202
|
+
/* @__PURE__ */ jsx("span", {
|
|
203
|
+
className: "font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]",
|
|
204
|
+
children: "More"
|
|
205
|
+
})
|
|
206
|
+
]
|
|
207
|
+
}),
|
|
208
|
+
mounted && hovered && createPortal(/* @__PURE__ */ jsxs("div", {
|
|
209
|
+
className: "pointer-events-none fixed z-50 min-w-[130px] rounded-lg border border-border bg-popover px-3 py-2.5 text-popover-foreground shadow-lg",
|
|
210
|
+
style: {
|
|
211
|
+
left: tooltipLeft,
|
|
212
|
+
top: tooltipTop
|
|
213
|
+
},
|
|
214
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
215
|
+
className: "mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider",
|
|
216
|
+
children: tooltipTitleKey && typeof hovered.record?.[tooltipTitleKey] === "string" ? hovered.record[tooltipTitleKey] : `${formatY(hovered.rowLabel)} · ${formatX(hovered.colLabel)}`
|
|
217
|
+
}), tooltipMetrics && tooltipMetrics.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
218
|
+
className: "flex flex-col gap-1",
|
|
219
|
+
children: tooltipMetrics.map((metric) => {
|
|
220
|
+
const raw = hovered.record ? Number(hovered.record[metric.key]) || 0 : 0;
|
|
221
|
+
const isActive = metric.key === dataKey;
|
|
222
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
223
|
+
className: "flex items-center justify-between gap-4 text-text-xs",
|
|
224
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
225
|
+
className: "inline-flex items-center gap-2 text-muted-foreground",
|
|
226
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
227
|
+
"aria-hidden": true,
|
|
228
|
+
className: "size-2 shrink-0 rounded-[2px]",
|
|
229
|
+
style: {
|
|
230
|
+
background: isActive ? heatColor(ramp, colorFractionOf(hovered.value)) : theme.mutedForeground,
|
|
231
|
+
opacity: isActive ? 1 : .35
|
|
232
|
+
}
|
|
233
|
+
}), metric.label]
|
|
234
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
235
|
+
className: cn("font-mono tabular-nums", isActive ? "font-semibold text-foreground" : "text-muted-foreground"),
|
|
236
|
+
children: (metric.format ?? valueFormatter)(raw)
|
|
237
|
+
})]
|
|
238
|
+
}, metric.key);
|
|
239
|
+
})
|
|
240
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
241
|
+
className: "flex items-center justify-between gap-4 text-text-xs",
|
|
242
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
243
|
+
className: "inline-flex items-center gap-2 text-muted-foreground",
|
|
244
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
245
|
+
"aria-hidden": true,
|
|
246
|
+
className: "size-2 shrink-0 rounded-[2px]",
|
|
247
|
+
style: { background: heatColor(ramp, colorFractionOf(hovered.value)) }
|
|
248
|
+
}), "activity"]
|
|
249
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
250
|
+
className: "font-mono font-semibold tabular-nums text-foreground",
|
|
251
|
+
children: valueFormatter(hovered.value)
|
|
252
|
+
})]
|
|
223
253
|
})]
|
|
224
|
-
})
|
|
225
|
-
|
|
254
|
+
}), document.body)
|
|
255
|
+
]
|
|
226
256
|
});
|
|
227
257
|
}
|
|
228
258
|
//#endregion
|