@alpic-ai/ui 0.0.0-dev.g16d80c2 → 0.0.0-dev.g172fb9f
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 +62 -0
- package/dist/components/area-chart.mjs +269 -0
- package/dist/components/badge.d.mts +1 -1
- package/dist/components/bar-chart.d.mts +48 -0
- package/dist/components/bar-chart.mjs +256 -0
- package/dist/components/bar-list.d.mts +28 -0
- package/dist/components/bar-list.mjs +98 -0
- package/dist/components/button.d.mts +1 -1
- package/dist/components/chart-card.d.mts +25 -0
- package/dist/components/chart-card.mjs +48 -0
- package/dist/components/chart-container.d.mts +20 -0
- package/dist/components/chart-container.mjs +37 -0
- package/dist/components/chart-legend.d.mts +16 -0
- package/dist/components/chart-legend.mjs +26 -0
- package/dist/components/chart-tooltip.d.mts +33 -0
- package/dist/components/chart-tooltip.mjs +52 -0
- package/dist/components/donut-chart.d.mts +46 -0
- package/dist/components/donut-chart.mjs +185 -0
- package/dist/components/grid-fx.d.mts +13 -0
- package/dist/components/grid-fx.mjs +188 -0
- package/dist/components/heatmap-chart.d.mts +40 -0
- package/dist/components/heatmap-chart.mjs +198 -0
- package/dist/components/line-chart.d.mts +55 -0
- package/dist/components/line-chart.mjs +211 -0
- package/dist/components/spinner.d.mts +1 -1
- package/dist/components/stat.d.mts +30 -0
- package/dist/components/stat.mjs +107 -0
- package/dist/hooks/use-chart-theme.d.mts +18 -0
- package/dist/hooks/use-chart-theme.mjs +57 -0
- package/dist/hooks/use-reduced-motion.d.mts +4 -0
- package/dist/hooks/use-reduced-motion.mjs +16 -0
- package/dist/lib/chart-palette.d.mts +4 -0
- package/dist/lib/chart-palette.mjs +95 -0
- package/dist/lib/chart.d.mts +14 -0
- package/dist/lib/chart.mjs +27 -0
- package/package.json +2 -1
- package/src/components/area-chart.tsx +339 -0
- package/src/components/bar-chart.tsx +309 -0
- package/src/components/bar-list.tsx +150 -0
- package/src/components/chart-card.tsx +63 -0
- package/src/components/chart-container.tsx +49 -0
- package/src/components/chart-legend.tsx +41 -0
- package/src/components/chart-tooltip.tsx +93 -0
- package/src/components/donut-chart.tsx +217 -0
- package/src/components/grid-fx.tsx +238 -0
- package/src/components/heatmap-chart.tsx +287 -0
- package/src/components/line-chart.tsx +264 -0
- package/src/components/stat.tsx +109 -0
- package/src/hooks/use-chart-theme.ts +75 -0
- package/src/hooks/use-reduced-motion.ts +17 -0
- package/src/lib/chart-palette.ts +110 -0
- package/src/lib/chart.ts +56 -0
- package/src/stories/area-chart.stories.tsx +200 -0
- package/src/stories/bar-chart.stories.tsx +169 -0
- package/src/stories/bar-list.stories.tsx +85 -0
- package/src/stories/donut-chart.stories.tsx +112 -0
- package/src/stories/heatmap-chart.stories.tsx +107 -0
- package/src/stories/line-chart.stories.tsx +146 -0
- package/src/stories/stat.stories.tsx +64 -0
- package/src/styles/tokens.css +63 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import * as React$1 from "react";
|
|
6
|
+
//#region src/components/grid-fx.tsx
|
|
7
|
+
const CELL_SIZE = 46;
|
|
8
|
+
const TTL_MIN = 42;
|
|
9
|
+
const TTL_MAX = 78;
|
|
10
|
+
const SPAWN_MIN = 180;
|
|
11
|
+
const SPAWN_MAX = 480;
|
|
12
|
+
const rand = (min, max) => min + Math.random() * (max - min);
|
|
13
|
+
function resolveColors(element) {
|
|
14
|
+
const styles = getComputedStyle(element);
|
|
15
|
+
return {
|
|
16
|
+
color: styles.getPropertyValue("--color-primary").trim() || "#e90060",
|
|
17
|
+
colorHi: styles.getPropertyValue("--color-primary-hover").trim() || "#f22b79"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function strokeFull(ctx, horiz, at, width, height) {
|
|
21
|
+
ctx.beginPath();
|
|
22
|
+
if (horiz) {
|
|
23
|
+
ctx.moveTo(0, at);
|
|
24
|
+
ctx.lineTo(width, at);
|
|
25
|
+
} else {
|
|
26
|
+
ctx.moveTo(at, 0);
|
|
27
|
+
ctx.lineTo(at, height);
|
|
28
|
+
}
|
|
29
|
+
ctx.stroke();
|
|
30
|
+
}
|
|
31
|
+
function drawGlitchLine(ctx, line, width, height) {
|
|
32
|
+
const { horiz, at, color, colorHi } = line;
|
|
33
|
+
const span = horiz ? width : height;
|
|
34
|
+
const progress = line.life / line.ttl;
|
|
35
|
+
const envelope = progress < .08 ? progress / .08 : 1 - (progress - .08) / .92;
|
|
36
|
+
const base = Math.max(0, envelope);
|
|
37
|
+
ctx.lineCap = "round";
|
|
38
|
+
const ghosts = [
|
|
39
|
+
{
|
|
40
|
+
offset: 0,
|
|
41
|
+
alpha: .85,
|
|
42
|
+
blur: 12
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
offset: rand(-3, 3),
|
|
46
|
+
alpha: .35,
|
|
47
|
+
blur: 0
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
offset: rand(-7, 7),
|
|
51
|
+
alpha: .2,
|
|
52
|
+
blur: 0
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
for (const ghost of ghosts) {
|
|
56
|
+
ctx.globalAlpha = base * ghost.alpha * (Math.random() < .1 ? .3 : 1);
|
|
57
|
+
ctx.strokeStyle = color;
|
|
58
|
+
ctx.lineWidth = 1.5;
|
|
59
|
+
ctx.shadowBlur = ghost.blur;
|
|
60
|
+
ctx.shadowColor = color;
|
|
61
|
+
strokeFull(ctx, horiz, at + ghost.offset, width, height);
|
|
62
|
+
}
|
|
63
|
+
if (Math.random() < .5) {
|
|
64
|
+
ctx.globalAlpha = base * .6;
|
|
65
|
+
ctx.lineWidth = 2;
|
|
66
|
+
ctx.shadowBlur = 0;
|
|
67
|
+
ctx.strokeStyle = colorHi;
|
|
68
|
+
for (let segment = 0; segment < 3; segment++) {
|
|
69
|
+
const start = rand(0, span * .85);
|
|
70
|
+
const end = start + rand(20, 80);
|
|
71
|
+
const jitter = rand(-4, 4);
|
|
72
|
+
ctx.beginPath();
|
|
73
|
+
if (horiz) {
|
|
74
|
+
ctx.moveTo(start, at + jitter);
|
|
75
|
+
ctx.lineTo(end, at + jitter);
|
|
76
|
+
} else {
|
|
77
|
+
ctx.moveTo(at + jitter, start);
|
|
78
|
+
ctx.lineTo(at + jitter, end);
|
|
79
|
+
}
|
|
80
|
+
ctx.stroke();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function GridFx({ className, cellSize = CELL_SIZE, style, ...props }) {
|
|
85
|
+
const reduced = useReducedMotion();
|
|
86
|
+
const canvasRef = React$1.useRef(null);
|
|
87
|
+
React$1.useEffect(() => {
|
|
88
|
+
if (reduced) return;
|
|
89
|
+
const canvas = canvasRef.current;
|
|
90
|
+
const parent = canvas?.parentElement;
|
|
91
|
+
const ctx = canvas?.getContext("2d");
|
|
92
|
+
if (!canvas || !parent || !ctx) return;
|
|
93
|
+
let width = 0;
|
|
94
|
+
let height = 0;
|
|
95
|
+
let dpr = 1;
|
|
96
|
+
let frame = 0;
|
|
97
|
+
let nextIn = rand(SPAWN_MIN, SPAWN_MAX);
|
|
98
|
+
let onScreen = true;
|
|
99
|
+
const lines = [];
|
|
100
|
+
const resize = () => {
|
|
101
|
+
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
102
|
+
width = parent.clientWidth;
|
|
103
|
+
height = parent.clientHeight;
|
|
104
|
+
canvas.width = width * dpr;
|
|
105
|
+
canvas.height = height * dpr;
|
|
106
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
107
|
+
};
|
|
108
|
+
const spawn = () => {
|
|
109
|
+
const horiz = Math.random() < .5;
|
|
110
|
+
const tracks = Math.ceil((horiz ? height : width) / cellSize);
|
|
111
|
+
const { color, colorHi } = resolveColors(canvas);
|
|
112
|
+
lines.push({
|
|
113
|
+
horiz,
|
|
114
|
+
at: Math.floor(Math.random() * tracks) * cellSize + .5,
|
|
115
|
+
life: 0,
|
|
116
|
+
ttl: rand(TTL_MIN, TTL_MAX),
|
|
117
|
+
color,
|
|
118
|
+
colorHi
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const tick = () => {
|
|
122
|
+
ctx.globalAlpha = 1;
|
|
123
|
+
ctx.shadowBlur = 0;
|
|
124
|
+
ctx.clearRect(0, 0, width, height);
|
|
125
|
+
if (--nextIn <= 0) {
|
|
126
|
+
spawn();
|
|
127
|
+
nextIn = rand(SPAWN_MIN, SPAWN_MAX);
|
|
128
|
+
}
|
|
129
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
130
|
+
const line = lines[index];
|
|
131
|
+
if (!line) continue;
|
|
132
|
+
line.life++;
|
|
133
|
+
drawGlitchLine(ctx, line, width, height);
|
|
134
|
+
if (line.life >= line.ttl) lines.splice(index, 1);
|
|
135
|
+
}
|
|
136
|
+
ctx.globalAlpha = 1;
|
|
137
|
+
ctx.shadowBlur = 0;
|
|
138
|
+
frame = requestAnimationFrame(tick);
|
|
139
|
+
};
|
|
140
|
+
const running = () => onScreen && document.visibilityState === "visible";
|
|
141
|
+
const start = () => {
|
|
142
|
+
if (!frame && running()) frame = requestAnimationFrame(tick);
|
|
143
|
+
};
|
|
144
|
+
const stop = () => {
|
|
145
|
+
if (frame) cancelAnimationFrame(frame);
|
|
146
|
+
frame = 0;
|
|
147
|
+
ctx.clearRect(0, 0, width, height);
|
|
148
|
+
};
|
|
149
|
+
resize();
|
|
150
|
+
start();
|
|
151
|
+
const resizeObserver = new ResizeObserver(() => resize());
|
|
152
|
+
resizeObserver.observe(parent);
|
|
153
|
+
const intersectionObserver = new IntersectionObserver(([entry]) => {
|
|
154
|
+
if (!entry) return;
|
|
155
|
+
onScreen = entry.isIntersecting;
|
|
156
|
+
if (onScreen) start();
|
|
157
|
+
else stop();
|
|
158
|
+
});
|
|
159
|
+
intersectionObserver.observe(canvas);
|
|
160
|
+
const onVisibility = () => {
|
|
161
|
+
if (running()) start();
|
|
162
|
+
else stop();
|
|
163
|
+
};
|
|
164
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
165
|
+
return () => {
|
|
166
|
+
stop();
|
|
167
|
+
resizeObserver.disconnect();
|
|
168
|
+
intersectionObserver.disconnect();
|
|
169
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
170
|
+
};
|
|
171
|
+
}, [reduced, cellSize]);
|
|
172
|
+
if (reduced) return null;
|
|
173
|
+
return /* @__PURE__ */ jsx("canvas", {
|
|
174
|
+
ref: canvasRef,
|
|
175
|
+
"aria-hidden": true,
|
|
176
|
+
"data-slot": "grid-fx",
|
|
177
|
+
className: cn("pointer-events-none absolute inset-0 h-full w-full", className),
|
|
178
|
+
style: {
|
|
179
|
+
zIndex: -1,
|
|
180
|
+
WebkitMaskImage: "radial-gradient(120% 95% at 50% 0%, #000 30%, transparent 100%)",
|
|
181
|
+
maskImage: "radial-gradient(120% 95% at 50% 0%, #000 30%, transparent 100%)",
|
|
182
|
+
...style
|
|
183
|
+
},
|
|
184
|
+
...props
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
export { GridFx };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ChartPaletteName } from "../lib/chart-palette.mjs";
|
|
2
|
+
import * as React$1 from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/components/heatmap-chart.d.ts
|
|
5
|
+
interface HeatmapChartProps {
|
|
6
|
+
data: ReadonlyArray<Record<string, string | number | null | undefined>>;
|
|
7
|
+
xKey: string;
|
|
8
|
+
yKey: string;
|
|
9
|
+
dataKey?: string;
|
|
10
|
+
variant?: "square" | "dot";
|
|
11
|
+
palette?: ChartPaletteName;
|
|
12
|
+
xLabels?: readonly string[];
|
|
13
|
+
yLabels?: readonly string[];
|
|
14
|
+
highlightPeak?: boolean;
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
valueFormatter?: (value: number) => string;
|
|
17
|
+
xTickFormatter?: (label: string) => string;
|
|
18
|
+
yTickFormatter?: (label: string) => string;
|
|
19
|
+
ariaLabel?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
declare function HeatmapChart({
|
|
23
|
+
data,
|
|
24
|
+
xKey,
|
|
25
|
+
yKey,
|
|
26
|
+
dataKey,
|
|
27
|
+
variant,
|
|
28
|
+
palette,
|
|
29
|
+
xLabels,
|
|
30
|
+
yLabels,
|
|
31
|
+
highlightPeak,
|
|
32
|
+
loading,
|
|
33
|
+
valueFormatter,
|
|
34
|
+
xTickFormatter,
|
|
35
|
+
yTickFormatter,
|
|
36
|
+
ariaLabel,
|
|
37
|
+
className
|
|
38
|
+
}: HeatmapChartProps): React$1.JSX.Element;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { HeatmapChart, HeatmapChartProps };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { HEAT_EMPTY, heatColor, heatRamp } from "../lib/chart-palette.mjs";
|
|
4
|
+
import { useChartContext } from "./chart-container.mjs";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import * as React$1 from "react";
|
|
7
|
+
import { createPortal } from "react-dom";
|
|
8
|
+
//#region src/components/heatmap-chart.tsx
|
|
9
|
+
const PLACEHOLDER_HEIGHT = 168;
|
|
10
|
+
const CELL = 22;
|
|
11
|
+
const GAP = 3;
|
|
12
|
+
const PAD = {
|
|
13
|
+
left: 36,
|
|
14
|
+
top: 18,
|
|
15
|
+
right: 6,
|
|
16
|
+
bottom: 6
|
|
17
|
+
};
|
|
18
|
+
const MAX_X_TICKS = 13;
|
|
19
|
+
const DOT_MIN = .42;
|
|
20
|
+
const DOT_RANGE = .52;
|
|
21
|
+
const TOOLTIP_OFFSET = 14;
|
|
22
|
+
const TOOLTIP_WIDTH = 150;
|
|
23
|
+
const TOOLTIP_HEIGHT = 64;
|
|
24
|
+
const pairKey = (rowLabel, colLabel) => JSON.stringify([rowLabel, colLabel]);
|
|
25
|
+
const uniqueInOrder = (values) => {
|
|
26
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27
|
+
const ordered = [];
|
|
28
|
+
for (const value of values) if (!seen.has(value)) {
|
|
29
|
+
seen.add(value);
|
|
30
|
+
ordered.push(value);
|
|
31
|
+
}
|
|
32
|
+
return ordered;
|
|
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 }) {
|
|
35
|
+
const { paletteName, theme } = useChartContext(palette);
|
|
36
|
+
const [hovered, setHovered] = React$1.useState(null);
|
|
37
|
+
const [mounted, setMounted] = React$1.useState(false);
|
|
38
|
+
React$1.useEffect(() => {
|
|
39
|
+
setMounted(true);
|
|
40
|
+
}, []);
|
|
41
|
+
const { columns, rows, cells, maxValue, peakKey } = React$1.useMemo(() => {
|
|
42
|
+
const columns = xLabels ? [...xLabels] : uniqueInOrder(data.map((row) => String(row[xKey] ?? "")));
|
|
43
|
+
const rows = yLabels ? [...yLabels] : uniqueInOrder(data.map((row) => String(row[yKey] ?? "")));
|
|
44
|
+
const valueAt = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const row of data) valueAt.set(pairKey(String(row[yKey] ?? ""), String(row[xKey] ?? "")), Number(row[dataKey]) || 0);
|
|
46
|
+
let maxValue = 0;
|
|
47
|
+
let peakKey = "";
|
|
48
|
+
return {
|
|
49
|
+
columns,
|
|
50
|
+
rows,
|
|
51
|
+
cells: rows.flatMap((rowLabel, rowIndex) => columns.map((colLabel, colIndex) => {
|
|
52
|
+
const value = valueAt.get(pairKey(rowLabel, colLabel)) ?? 0;
|
|
53
|
+
if (value > maxValue) {
|
|
54
|
+
maxValue = value;
|
|
55
|
+
peakKey = pairKey(rowLabel, colLabel);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
rowLabel,
|
|
59
|
+
colLabel,
|
|
60
|
+
rowIndex,
|
|
61
|
+
colIndex,
|
|
62
|
+
value
|
|
63
|
+
};
|
|
64
|
+
})),
|
|
65
|
+
maxValue,
|
|
66
|
+
peakKey
|
|
67
|
+
};
|
|
68
|
+
}, [
|
|
69
|
+
data,
|
|
70
|
+
xKey,
|
|
71
|
+
yKey,
|
|
72
|
+
dataKey,
|
|
73
|
+
xLabels,
|
|
74
|
+
yLabels
|
|
75
|
+
]);
|
|
76
|
+
const isEmpty = columns.length === 0 || rows.length === 0 || maxValue <= 0;
|
|
77
|
+
if (loading) return /* @__PURE__ */ jsxs("div", {
|
|
78
|
+
className: cn("flex items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs", className),
|
|
79
|
+
style: { minHeight: PLACEHOLDER_HEIGHT },
|
|
80
|
+
children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
|
|
81
|
+
});
|
|
82
|
+
if (isEmpty) return /* @__PURE__ */ jsx("div", {
|
|
83
|
+
className: cn("flex items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs", className),
|
|
84
|
+
style: { minHeight: PLACEHOLDER_HEIGHT },
|
|
85
|
+
children: "no data in range"
|
|
86
|
+
});
|
|
87
|
+
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
|
|
88
|
+
const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
|
|
89
|
+
const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
|
|
90
|
+
const xStride = Math.ceil(columns.length / MAX_X_TICKS);
|
|
91
|
+
const cellX = (col) => PAD.left + col * 25;
|
|
92
|
+
const cellY = (row) => PAD.top + row * 25;
|
|
93
|
+
const formatX = (label) => xTickFormatter ? xTickFormatter(label) : label;
|
|
94
|
+
const formatY = (label) => yTickFormatter ? yTickFormatter(label) : label;
|
|
95
|
+
const tooltipLeft = hovered && hovered.x + TOOLTIP_OFFSET + TOOLTIP_WIDTH > window.innerWidth ? hovered.x - TOOLTIP_OFFSET - TOOLTIP_WIDTH : (hovered?.x ?? 0) + TOOLTIP_OFFSET;
|
|
96
|
+
const tooltipTop = hovered && hovered.y + TOOLTIP_OFFSET + TOOLTIP_HEIGHT > window.innerHeight ? hovered.y - TOOLTIP_OFFSET - TOOLTIP_HEIGHT : (hovered?.y ?? 0) + TOOLTIP_OFFSET;
|
|
97
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
98
|
+
"data-slot": "heatmap-chart",
|
|
99
|
+
className: cn("w-full", className),
|
|
100
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
101
|
+
className: "overflow-x-auto",
|
|
102
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
103
|
+
viewBox: `0 0 ${totalWidth} ${totalHeight}`,
|
|
104
|
+
className: "block min-w-[480px]",
|
|
105
|
+
style: {
|
|
106
|
+
width: "100%",
|
|
107
|
+
height: "auto"
|
|
108
|
+
},
|
|
109
|
+
role: "img",
|
|
110
|
+
"aria-label": ariaLabel,
|
|
111
|
+
onMouseLeave: () => setHovered(null),
|
|
112
|
+
children: [
|
|
113
|
+
columns.map((label, col) => col % xStride === 0 ? /* @__PURE__ */ jsx("text", {
|
|
114
|
+
x: cellX(col) + CELL / 2,
|
|
115
|
+
y: PAD.top - 7,
|
|
116
|
+
textAnchor: "middle",
|
|
117
|
+
className: "fill-quaternary-foreground font-mono text-[8.5px]",
|
|
118
|
+
children: formatX(label)
|
|
119
|
+
}, `x-${label}`) : null),
|
|
120
|
+
rows.map((label, row) => /* @__PURE__ */ jsx("text", {
|
|
121
|
+
x: PAD.left - 8,
|
|
122
|
+
y: cellY(row) + CELL / 2 + 3,
|
|
123
|
+
textAnchor: "end",
|
|
124
|
+
className: "fill-quaternary-foreground font-mono text-[8.5px]",
|
|
125
|
+
children: formatY(label)
|
|
126
|
+
}, `y-${label}`)),
|
|
127
|
+
cells.map((cell) => {
|
|
128
|
+
const fraction = cell.value / maxValue;
|
|
129
|
+
const fill = heatColor(ramp, fraction);
|
|
130
|
+
const key = pairKey(cell.rowLabel, cell.colLabel);
|
|
131
|
+
const isPeak = highlightPeak && key === peakKey;
|
|
132
|
+
if (variant === "dot") return /* @__PURE__ */ jsx("circle", {
|
|
133
|
+
cx: cellX(cell.colIndex) + CELL / 2,
|
|
134
|
+
cy: cellY(cell.rowIndex) + CELL / 2,
|
|
135
|
+
r: CELL / 2 * (DOT_MIN + DOT_RANGE * fraction),
|
|
136
|
+
fill,
|
|
137
|
+
stroke: isPeak ? theme.foreground : void 0,
|
|
138
|
+
strokeWidth: isPeak ? 1.4 : void 0
|
|
139
|
+
}, key);
|
|
140
|
+
return /* @__PURE__ */ jsx("rect", {
|
|
141
|
+
x: cellX(cell.colIndex),
|
|
142
|
+
y: cellY(cell.rowIndex),
|
|
143
|
+
width: CELL,
|
|
144
|
+
height: CELL,
|
|
145
|
+
rx: 2.5,
|
|
146
|
+
fill,
|
|
147
|
+
stroke: isPeak ? theme.foreground : void 0,
|
|
148
|
+
strokeWidth: isPeak ? 1.4 : void 0
|
|
149
|
+
}, key);
|
|
150
|
+
}),
|
|
151
|
+
cells.map((cell) => /* @__PURE__ */ jsx("rect", {
|
|
152
|
+
x: cellX(cell.colIndex),
|
|
153
|
+
y: cellY(cell.rowIndex),
|
|
154
|
+
width: 25,
|
|
155
|
+
height: 25,
|
|
156
|
+
fill: "transparent",
|
|
157
|
+
onMouseMove: (event) => setHovered({
|
|
158
|
+
x: event.clientX,
|
|
159
|
+
y: event.clientY,
|
|
160
|
+
rowLabel: cell.rowLabel,
|
|
161
|
+
colLabel: cell.colLabel,
|
|
162
|
+
value: cell.value
|
|
163
|
+
})
|
|
164
|
+
}, `hit-${pairKey(cell.rowLabel, cell.colLabel)}`))
|
|
165
|
+
]
|
|
166
|
+
})
|
|
167
|
+
}), mounted && hovered && createPortal(/* @__PURE__ */ jsxs("div", {
|
|
168
|
+
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",
|
|
169
|
+
style: {
|
|
170
|
+
left: tooltipLeft,
|
|
171
|
+
top: tooltipTop
|
|
172
|
+
},
|
|
173
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
174
|
+
className: "mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider",
|
|
175
|
+
children: [
|
|
176
|
+
formatY(hovered.rowLabel),
|
|
177
|
+
" · ",
|
|
178
|
+
formatX(hovered.colLabel)
|
|
179
|
+
]
|
|
180
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
181
|
+
className: "flex items-center justify-between gap-4 text-text-xs",
|
|
182
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
183
|
+
className: "inline-flex items-center gap-2 text-muted-foreground",
|
|
184
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
185
|
+
"aria-hidden": true,
|
|
186
|
+
className: "size-2 shrink-0 rounded-[2px]",
|
|
187
|
+
style: { background: heatColor(ramp, hovered.value / maxValue) }
|
|
188
|
+
}), "activity"]
|
|
189
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
190
|
+
className: "font-mono font-semibold tabular-nums text-foreground",
|
|
191
|
+
children: valueFormatter(hovered.value)
|
|
192
|
+
})]
|
|
193
|
+
})]
|
|
194
|
+
}), document.body)]
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
export { HeatmapChart };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ChartSeries } from "../lib/chart.mjs";
|
|
2
|
+
import { ChartPaletteName } from "../lib/chart-palette.mjs";
|
|
3
|
+
import { ChartMarker } from "./area-chart.mjs";
|
|
4
|
+
import * as React$1 from "react";
|
|
5
|
+
|
|
6
|
+
//#region src/components/line-chart.d.ts
|
|
7
|
+
declare const CURVE_TYPE: {
|
|
8
|
+
readonly monotone: "monotone";
|
|
9
|
+
readonly linear: "linear";
|
|
10
|
+
readonly step: "stepAfter";
|
|
11
|
+
};
|
|
12
|
+
interface LineChartProps {
|
|
13
|
+
data: ReadonlyArray<Record<string, string | number | null | undefined>>;
|
|
14
|
+
index: string;
|
|
15
|
+
series: ChartSeries[];
|
|
16
|
+
curve?: keyof typeof CURVE_TYPE;
|
|
17
|
+
legend?: boolean;
|
|
18
|
+
valueFlags?: boolean;
|
|
19
|
+
dots?: boolean;
|
|
20
|
+
height?: number;
|
|
21
|
+
yAxisWidth?: number;
|
|
22
|
+
palette?: ChartPaletteName;
|
|
23
|
+
referenceLine?: {
|
|
24
|
+
y: number;
|
|
25
|
+
label?: string;
|
|
26
|
+
band?: boolean;
|
|
27
|
+
};
|
|
28
|
+
markers?: ChartMarker[];
|
|
29
|
+
lastValueLabel?: boolean;
|
|
30
|
+
loading?: boolean;
|
|
31
|
+
valueFormatter?: (value: number) => string;
|
|
32
|
+
labelFormatter?: (label: string | number) => string;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
declare function LineChart({
|
|
36
|
+
data,
|
|
37
|
+
index,
|
|
38
|
+
series,
|
|
39
|
+
curve,
|
|
40
|
+
legend,
|
|
41
|
+
valueFlags,
|
|
42
|
+
dots,
|
|
43
|
+
height,
|
|
44
|
+
yAxisWidth,
|
|
45
|
+
palette,
|
|
46
|
+
referenceLine,
|
|
47
|
+
markers,
|
|
48
|
+
lastValueLabel,
|
|
49
|
+
loading,
|
|
50
|
+
valueFormatter,
|
|
51
|
+
labelFormatter,
|
|
52
|
+
className
|
|
53
|
+
}: LineChartProps): React$1.JSX.Element;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { LineChart, LineChartProps };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
|
+
import { resolveSeries } from "../lib/chart.mjs";
|
|
5
|
+
import { useChartContext } from "./chart-container.mjs";
|
|
6
|
+
import { ChartLegend } from "./chart-legend.mjs";
|
|
7
|
+
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import * as React$1 from "react";
|
|
10
|
+
import { CartesianGrid, LabelList, Line, LineChart as LineChart$1, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
11
|
+
//#region src/components/line-chart.tsx
|
|
12
|
+
const CURVE_TYPE = {
|
|
13
|
+
monotone: "monotone",
|
|
14
|
+
linear: "linear",
|
|
15
|
+
step: "stepAfter"
|
|
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 }) {
|
|
18
|
+
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
19
|
+
const animated = !useReducedMotion();
|
|
20
|
+
const resolved = resolveSeries(series, paletteColors, theme);
|
|
21
|
+
const numericMax = React$1.useMemo(() => {
|
|
22
|
+
let max = 0;
|
|
23
|
+
for (const row of data) for (const entry of series) {
|
|
24
|
+
const value = Number(row[entry.key]);
|
|
25
|
+
if (Number.isFinite(value) && value > max) max = value;
|
|
26
|
+
}
|
|
27
|
+
return max;
|
|
28
|
+
}, [data, series]);
|
|
29
|
+
const curveType = CURVE_TYPE[curve];
|
|
30
|
+
const margin = {
|
|
31
|
+
top: markers?.length ? 18 : 8,
|
|
32
|
+
right: lastValueLabel ? 56 : 8,
|
|
33
|
+
bottom: 2,
|
|
34
|
+
left: 0
|
|
35
|
+
};
|
|
36
|
+
const axis = {
|
|
37
|
+
stroke: theme.border,
|
|
38
|
+
tick: {
|
|
39
|
+
fill: theme.axisForeground,
|
|
40
|
+
fontSize: 10,
|
|
41
|
+
fontFamily: theme.fontMono
|
|
42
|
+
},
|
|
43
|
+
tickLine: false,
|
|
44
|
+
axisLine: {
|
|
45
|
+
stroke: theme.border,
|
|
46
|
+
strokeOpacity: .6
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const legendItems = resolved.map((entry) => ({
|
|
50
|
+
name: entry.name,
|
|
51
|
+
color: entry.color,
|
|
52
|
+
dashed: entry.dashed
|
|
53
|
+
}));
|
|
54
|
+
const activeDotFor = (entry) => valueFlags ? (dotProps) => {
|
|
55
|
+
if (dotProps.cx == null || dotProps.cy == null) return /* @__PURE__ */ jsx("g", {});
|
|
56
|
+
return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("circle", {
|
|
57
|
+
cx: dotProps.cx,
|
|
58
|
+
cy: dotProps.cy,
|
|
59
|
+
r: 3.5,
|
|
60
|
+
fill: entry.color,
|
|
61
|
+
stroke: theme.card,
|
|
62
|
+
strokeWidth: 2
|
|
63
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
64
|
+
x: dotProps.cx,
|
|
65
|
+
y: dotProps.cy - 8,
|
|
66
|
+
textAnchor: "middle",
|
|
67
|
+
fill: entry.color,
|
|
68
|
+
fontFamily: theme.fontMono,
|
|
69
|
+
fontSize: 10,
|
|
70
|
+
style: { fontVariantNumeric: "tabular-nums" },
|
|
71
|
+
children: valueFormatter(Number(dotProps.value ?? 0))
|
|
72
|
+
})] });
|
|
73
|
+
} : {
|
|
74
|
+
r: 3.5,
|
|
75
|
+
fill: entry.color,
|
|
76
|
+
stroke: theme.card,
|
|
77
|
+
strokeWidth: 2
|
|
78
|
+
};
|
|
79
|
+
const renderLastLabel = (color) => (props) => {
|
|
80
|
+
if (props.index !== data.length - 1 || props.x == null || props.y == null) return null;
|
|
81
|
+
return /* @__PURE__ */ jsx("text", {
|
|
82
|
+
x: Number(props.x) + 6,
|
|
83
|
+
y: Number(props.y),
|
|
84
|
+
dy: 3,
|
|
85
|
+
fill: color,
|
|
86
|
+
fontFamily: theme.fontMono,
|
|
87
|
+
fontSize: 10,
|
|
88
|
+
textAnchor: "start",
|
|
89
|
+
style: { fontVariantNumeric: "tabular-nums" },
|
|
90
|
+
children: valueFormatter(Number(props.value ?? 0))
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const isEmpty = data.length === 0 || resolved.length === 0;
|
|
94
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
95
|
+
"data-slot": "line-chart",
|
|
96
|
+
className: cn("flex w-full flex-col gap-3", className),
|
|
97
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
98
|
+
className: "w-full",
|
|
99
|
+
style: { height },
|
|
100
|
+
children: loading ? /* @__PURE__ */ jsxs("div", {
|
|
101
|
+
className: "flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
|
|
102
|
+
children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
|
|
103
|
+
}) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
104
|
+
className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
|
|
105
|
+
children: "no data in range"
|
|
106
|
+
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
107
|
+
width: "100%",
|
|
108
|
+
height: "100%",
|
|
109
|
+
children: /* @__PURE__ */ jsxs(LineChart$1, {
|
|
110
|
+
data,
|
|
111
|
+
margin,
|
|
112
|
+
children: [
|
|
113
|
+
/* @__PURE__ */ jsx(CartesianGrid, {
|
|
114
|
+
vertical: false,
|
|
115
|
+
stroke: theme.grid,
|
|
116
|
+
strokeDasharray: "2 4"
|
|
117
|
+
}),
|
|
118
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
119
|
+
dataKey: index,
|
|
120
|
+
...axis,
|
|
121
|
+
interval: "preserveStartEnd",
|
|
122
|
+
minTickGap: 44
|
|
123
|
+
}),
|
|
124
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
125
|
+
...axis,
|
|
126
|
+
width: yAxisWidth,
|
|
127
|
+
tickFormatter: (value) => valueFormatter(value)
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ jsx(Tooltip, {
|
|
130
|
+
offset: 12,
|
|
131
|
+
allowEscapeViewBox: {
|
|
132
|
+
x: false,
|
|
133
|
+
y: false
|
|
134
|
+
},
|
|
135
|
+
cursor: {
|
|
136
|
+
stroke: theme.axisForeground,
|
|
137
|
+
strokeWidth: 1,
|
|
138
|
+
strokeDasharray: "3 3"
|
|
139
|
+
},
|
|
140
|
+
content: /* @__PURE__ */ jsx(ChartTooltipContent, {
|
|
141
|
+
valueFormatter,
|
|
142
|
+
labelFormatter
|
|
143
|
+
})
|
|
144
|
+
}),
|
|
145
|
+
referenceLine?.band && /* @__PURE__ */ jsx(ReferenceArea, {
|
|
146
|
+
y1: referenceLine.y,
|
|
147
|
+
y2: numericMax,
|
|
148
|
+
fill: theme.warning,
|
|
149
|
+
fillOpacity: .06,
|
|
150
|
+
ifOverflow: "extendDomain"
|
|
151
|
+
}),
|
|
152
|
+
referenceLine && /* @__PURE__ */ jsx(ReferenceLine, {
|
|
153
|
+
y: referenceLine.y,
|
|
154
|
+
stroke: theme.warning,
|
|
155
|
+
strokeDasharray: "4 4",
|
|
156
|
+
strokeOpacity: .6,
|
|
157
|
+
label: referenceLine.label ? {
|
|
158
|
+
value: referenceLine.label,
|
|
159
|
+
fill: theme.warning,
|
|
160
|
+
fontSize: 9,
|
|
161
|
+
fontFamily: theme.fontMono,
|
|
162
|
+
position: "insideBottomRight"
|
|
163
|
+
} : void 0
|
|
164
|
+
}),
|
|
165
|
+
resolved.map((entry) => /* @__PURE__ */ jsx(Line, {
|
|
166
|
+
type: curveType,
|
|
167
|
+
dataKey: entry.key,
|
|
168
|
+
name: entry.name,
|
|
169
|
+
stroke: entry.color,
|
|
170
|
+
strokeWidth: entry.dashed ? 2 : 1.8,
|
|
171
|
+
strokeDasharray: entry.dashed ? "5 3" : void 0,
|
|
172
|
+
dot: dots ? {
|
|
173
|
+
r: 2.5,
|
|
174
|
+
fill: entry.color,
|
|
175
|
+
strokeWidth: 0
|
|
176
|
+
} : false,
|
|
177
|
+
activeDot: activeDotFor(entry),
|
|
178
|
+
isAnimationActive: animated,
|
|
179
|
+
animationDuration: 650,
|
|
180
|
+
animationEasing: "ease-out",
|
|
181
|
+
children: lastValueLabel && /* @__PURE__ */ jsx(LabelList, {
|
|
182
|
+
dataKey: entry.key,
|
|
183
|
+
content: renderLastLabel(entry.color)
|
|
184
|
+
})
|
|
185
|
+
}, entry.key)),
|
|
186
|
+
markers?.map((marker) => /* @__PURE__ */ jsx(ReferenceDot, {
|
|
187
|
+
x: marker.x,
|
|
188
|
+
y: marker.y,
|
|
189
|
+
r: 3.5,
|
|
190
|
+
fill: marker.color ?? theme.foreground,
|
|
191
|
+
stroke: theme.card,
|
|
192
|
+
strokeWidth: 2,
|
|
193
|
+
label: marker.label ? {
|
|
194
|
+
value: marker.label,
|
|
195
|
+
fill: marker.color ?? theme.foreground,
|
|
196
|
+
fontSize: 9,
|
|
197
|
+
fontFamily: theme.fontMono,
|
|
198
|
+
position: "top"
|
|
199
|
+
} : void 0
|
|
200
|
+
}, `${marker.x}-${marker.y}`))
|
|
201
|
+
]
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
}), legend && !isEmpty && /* @__PURE__ */ jsx(ChartLegend, {
|
|
205
|
+
items: legendItems,
|
|
206
|
+
style: { paddingLeft: yAxisWidth }
|
|
207
|
+
})]
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
export { LineChart };
|
|
@@ -2,7 +2,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
2
2
|
|
|
3
3
|
//#region src/components/spinner.d.ts
|
|
4
4
|
declare const spinnerVariants: (props?: ({
|
|
5
|
-
variant?: "
|
|
5
|
+
variant?: "primary" | "secondary" | null | undefined;
|
|
6
6
|
size?: "sm" | "md" | "lg" | "xl" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
interface SpinnerProps extends Omit<React.ComponentProps<"svg">, "children">, VariantProps<typeof spinnerVariants> {}
|