Charts

Recharts-based area, bar, line, pie, radial, sparkline, and stats card building blocks. Shell and plot sizing use class-variance-authority (CVA) variants.

Area & line
Stacked area and monotone line with the same series

Visitors

Area chart with desktop and mobile series

Same data as lines

1"use client";
2
3import { cva, type VariantProps } from "class-variance-authority";
4import {
5 Area,
6 AreaChart,
7 Bar,
8 BarChart,
9 CartesianGrid,
10 Cell,
11 Line,
12 LineChart,
13 Pie,
14 PieChart,
15 RadialBar,
16 RadialBarChart,
17 XAxis,
18 YAxis,
19} from "recharts";
20import {
21 ChartConfig,
22 ChartContainer,
23 ChartTooltip,
24 ChartTooltipContent,
25 ChartLegend,
26 ChartLegendContent,
27} from "@/components/ui/chart";
28import { cn } from "@/lib/utils";
29
30const chartShellVariants = cva("rounded-lg border border-border", {
31 variants: {
32 surface: {
33 card: "bg-card",
34 muted: "bg-muted/40",
35 },
36 padding: {
37 sm: "p-3",
38 md: "p-4",
39 lg: "p-5",
40 },
41 },
42 defaultVariants: {
43 surface: "card",
44 padding: "md",
45 },
46});
47
48const chartPlotVariants = cva("w-full", {
49 variants: {
50 plotSize: {
51 sm: "h-[160px]",
52 md: "h-[200px]",
53 lg: "h-[240px]",
54 },
55 },
56 defaultVariants: {
57 plotSize: "md",
58 },
59});
60
61const chartTitleBlockVariants = cva("", {
62 variants: {
63 spacing: {
64 default: "mb-4",
65 tight: "mb-2",
66 },
67 },
68 defaultVariants: {
69 spacing: "default",
70 },
71});
72
73const radialDialVariants = cva("w-full mx-auto", {
74 variants: {
75 dialSize: {
76 sm: "h-[120px]",
77 md: "h-[160px]",
78 lg: "h-[200px]",
79 },
80 },
81 defaultVariants: {
82 dialSize: "md",
83 },
84});
85
86const statsCardVariants = cva("rounded-lg border border-border bg-card", {
87 variants: {
88 padding: {
89 sm: "p-3",
90 md: "p-4",
91 lg: "p-5",
92 },
93 },
94 defaultVariants: {
95 padding: "md",
96 },
97});
98
99type ChartShellProps = VariantProps<typeof chartShellVariants>;
100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;
101
102interface BaseChartProps extends ChartShellProps {
103 className?: string;
104 title?: string;
105 description?: string;
106}
107
108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}
109
110interface AreaChartData {
111 label: string;
112 [key: string]: string | number;
113}
114
115interface CustomAreaChartProps extends CartesianChartProps {
116 data: AreaChartData[];
117 dataKeys: { key: string; label: string; color: string }[];
118 stacked?: boolean;
119 showGrid?: boolean;
120 showLegend?: boolean;
121}
122
123const areaChartConfig = (
124 dataKeys: { key: string; label: string; color: string }[],
125): ChartConfig => {
126 return dataKeys.reduce((acc, { key, label, color }) => {
127 acc[key] = { label, color };
128 return acc;
129 }, {} as ChartConfig);
130};
131
132export function CustomAreaChart({
133 data,
134 dataKeys,
135 stacked = false,
136 showGrid = true,
137 showLegend = true,
138 className,
139 title,
140 description,
141 surface,
142 padding,
143 plotSize,
144}: CustomAreaChartProps) {
145 const config = areaChartConfig(dataKeys);
146
147 return (
148 <div className={cn(chartShellVariants({ surface, padding }), className)}>
149 {(title || description) && (
150 <div className={chartTitleBlockVariants({ spacing: "default" })}>
151 {title && (
152 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
153 )}
154 {description && (
155 <p className="text-xs text-muted-foreground mt-1">{description}</p>
156 )}
157 </div>
158 )}
159 <ChartContainer
160 config={config}
161 className={chartPlotVariants({ plotSize })}
162 >
163 <AreaChart data={data} margin={{ left: -20, right: 12 }}>
164 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
165 <XAxis
166 dataKey="label"
167 tickLine={false}
168 axisLine={false}
169 tickMargin={8}
170 fontSize={11}
171 />
172 <YAxis
173 tickLine={false}
174 axisLine={false}
175 tickMargin={8}
176 fontSize={11}
177 />
178 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
179 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
180 {dataKeys.map(({ key, color }) => (
181 <Area
182 key={key}
183 dataKey={key}
184 type="monotone"
185 fill={color}
186 fillOpacity={0.2}
187 stroke={color}
188 strokeWidth={2}
189 stackId={stacked ? "1" : undefined}
190 />
191 ))}
192 </AreaChart>
193 </ChartContainer>
194 </div>
195 );
196}
197
198interface BarChartData {
199 label: string;
200 [key: string]: string | number;
201}
202
203interface CustomBarChartProps extends CartesianChartProps {
204 data: BarChartData[];
205 dataKeys: { key: string; label: string; color: string }[];
206 horizontal?: boolean;
207 stacked?: boolean;
208 showGrid?: boolean;
209 showLegend?: boolean;
210 barRadius?: number;
211}
212
213export function CustomBarChart({
214 data,
215 dataKeys,
216 horizontal = false,
217 stacked = false,
218 showGrid = true,
219 showLegend = true,
220 barRadius = 4,
221 className,
222 title,
223 description,
224 surface,
225 padding,
226 plotSize,
227}: CustomBarChartProps) {
228 const config = areaChartConfig(dataKeys);
229
230 return (
231 <div className={cn(chartShellVariants({ surface, padding }), className)}>
232 {(title || description) && (
233 <div className={chartTitleBlockVariants({ spacing: "default" })}>
234 {title && (
235 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
236 )}
237 {description && (
238 <p className="text-xs text-muted-foreground mt-1">{description}</p>
239 )}
240 </div>
241 )}
242 <ChartContainer
243 config={config}
244 className={chartPlotVariants({ plotSize })}
245 >
246 <BarChart
247 data={data}
248 layout={horizontal ? "vertical" : "horizontal"}
249 margin={{ left: horizontal ? 0 : -20, right: 12 }}
250 >
251 {showGrid && (
252 <CartesianGrid
253 strokeDasharray="3 3"
254 horizontal={!horizontal}
255 vertical={horizontal}
256 />
257 )}
258 {horizontal ? (
259 <>
260 <YAxis
261 dataKey="label"
262 type="category"
263 tickLine={false}
264 axisLine={false}
265 tickMargin={8}
266 fontSize={11}
267 width={80}
268 />
269 <XAxis
270 type="number"
271 tickLine={false}
272 axisLine={false}
273 fontSize={11}
274 />
275 </>
276 ) : (
277 <>
278 <XAxis
279 dataKey="label"
280 tickLine={false}
281 axisLine={false}
282 tickMargin={8}
283 fontSize={11}
284 />
285 <YAxis
286 tickLine={false}
287 axisLine={false}
288 tickMargin={8}
289 fontSize={11}
290 />
291 </>
292 )}
293 <ChartTooltip content={<ChartTooltipContent />} />
294 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
295 {dataKeys.map(({ key, color }) => (
296 <Bar
297 key={key}
298 dataKey={key}
299 fill={color}
300 radius={barRadius}
301 stackId={stacked ? "1" : undefined}
302 />
303 ))}
304 </BarChart>
305 </ChartContainer>
306 </div>
307 );
308}
309
310interface LineChartData {
311 label: string;
312 [key: string]: string | number;
313}
314
315interface CustomLineChartProps extends CartesianChartProps {
316 data: LineChartData[];
317 dataKeys: { key: string; label: string; color: string }[];
318 showGrid?: boolean;
319 showLegend?: boolean;
320 showDots?: boolean;
321 curved?: boolean;
322}
323
324export function CustomLineChart({
325 data,
326 dataKeys,
327 showGrid = true,
328 showLegend = true,
329 showDots = true,
330 curved = true,
331 className,
332 title,
333 description,
334 surface,
335 padding,
336 plotSize,
337}: CustomLineChartProps) {
338 const config = areaChartConfig(dataKeys);
339
340 return (
341 <div className={cn(chartShellVariants({ surface, padding }), className)}>
342 {(title || description) && (
343 <div className={chartTitleBlockVariants({ spacing: "default" })}>
344 {title && (
345 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
346 )}
347 {description && (
348 <p className="text-xs text-muted-foreground mt-1">{description}</p>
349 )}
350 </div>
351 )}
352 <ChartContainer
353 config={config}
354 className={chartPlotVariants({ plotSize })}
355 >
356 <LineChart data={data} margin={{ left: -20, right: 12 }}>
357 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
358 <XAxis
359 dataKey="label"
360 tickLine={false}
361 axisLine={false}
362 tickMargin={8}
363 fontSize={11}
364 />
365 <YAxis
366 tickLine={false}
367 axisLine={false}
368 tickMargin={8}
369 fontSize={11}
370 />
371 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
372 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
373 {dataKeys.map(({ key, color }) => (
374 <Line
375 key={key}
376 dataKey={key}
377 type={curved ? "monotone" : "linear"}
378 stroke={color}
379 strokeWidth={2}
380 dot={showDots ? { fill: color, strokeWidth: 0, r: 3 } : false}
381 activeDot={{ r: 5, strokeWidth: 0 }}
382 />
383 ))}
384 </LineChart>
385 </ChartContainer>
386 </div>
387 );
388}
389
390interface PieChartData {
391 name: string;
392 value: number;
393 color: string;
394}
395
396interface CustomPieChartProps extends CartesianChartProps {
397 data: PieChartData[];
398 showLegend?: boolean;
399 innerRadius?: number;
400 paddingAngle?: number;
401}
402
403export function CustomPieChart({
404 data,
405 showLegend = true,
406 innerRadius = 0,
407 paddingAngle = 2,
408 className,
409 title,
410 description,
411 surface,
412 padding,
413 plotSize,
414}: CustomPieChartProps) {
415 const config: ChartConfig = data.reduce((acc, item) => {
416 acc[item.name] = { label: item.name, color: item.color };
417 return acc;
418 }, {} as ChartConfig);
419
420 return (
421 <div className={cn(chartShellVariants({ surface, padding }), className)}>
422 {(title || description) && (
423 <div className={chartTitleBlockVariants({ spacing: "default" })}>
424 {title && (
425 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
426 )}
427 {description && (
428 <p className="text-xs text-muted-foreground mt-1">{description}</p>
429 )}
430 </div>
431 )}
432 <ChartContainer
433 config={config}
434 className={chartPlotVariants({ plotSize })}
435 >
436 <PieChart>
437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />
438 {showLegend && (
439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />
440 )}
441 <Pie
442 data={data}
443 dataKey="value"
444 nameKey="name"
445 cx="50%"
446 cy="50%"
447 innerRadius={innerRadius}
448 paddingAngle={paddingAngle}
449 >
450 {data.map((entry, index) => (
451 <Cell key={`cell-${index}`} fill={entry.color} />
452 ))}
453 </Pie>
454 </PieChart>
455 </ChartContainer>
456 </div>
457 );
458}
459
460interface RadialProgressProps extends BaseChartProps {
461 value: number;
462 maxValue?: number;
463 color?: string;
464 label?: string;
465 size?: "sm" | "md" | "lg";
466}
467
468export function RadialProgress({
469 value,
470 maxValue = 100,
471 color = "var(--chart-1)",
472 label,
473 size = "md",
474 className,
475 title,
476 description,
477 surface,
478 padding,
479}: RadialProgressProps) {
480 const percentage = Math.min((value / maxValue) * 100, 100);
481 const data = [{ name: "progress", value: percentage, fill: color }];
482
483 const config: ChartConfig = {
484 progress: { label: label || "Progress", color },
485 };
486
487 return (
488 <div className={cn(chartShellVariants({ surface, padding }), className)}>
489 {(title || description) && (
490 <div className={chartTitleBlockVariants({ spacing: "tight" })}>
491 {title && (
492 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
493 )}
494 {description && (
495 <p className="text-xs text-muted-foreground mt-1">{description}</p>
496 )}
497 </div>
498 )}
499 <ChartContainer
500 config={config}
501 className={radialDialVariants({ dialSize: size })}
502 >
503 <RadialBarChart
504 data={data}
505 startAngle={90}
506 endAngle={90 - percentage * 3.6}
507 innerRadius="70%"
508 outerRadius="100%"
509 >
510 <RadialBar
511 dataKey="value"
512 background={{ fill: "var(--muted)" }}
513 cornerRadius={10}
514 />
515 <text
516 x="50%"
517 y="50%"
518 textAnchor="middle"
519 dominantBaseline="middle"
520 className="fill-foreground font-mono text-2xl font-bold"
521 >
522 {Math.round(percentage)}%
523 </text>
524 </RadialBarChart>
525 </ChartContainer>
526 {label && (
527 <p className="text-center text-xs text-muted-foreground mt-2">
528 {label}
529 </p>
530 )}
531 </div>
532 );
533}
534
535interface SparklineProps {
536 data: number[];
537 color?: string;
538 height?: number;
539 showArea?: boolean;
540 className?: string;
541}
542
543export function Sparkline({
544 data,
545 color = "var(--chart-1)",
546 height = 40,
547 showArea = true,
548 className,
549}: SparklineProps) {
550 const chartData = data.map((value, index) => ({ index, value }));
551
552 const config: ChartConfig = {
553 value: { label: "Value", color },
554 };
555
556 return (
557 <ChartContainer
558 config={config}
559 className={cn("w-full", className)}
560 style={{ height }}
561 >
562 <AreaChart
563 data={chartData}
564 margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
565 >
566 <defs>
567 <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
568 <stop offset="0%" stopColor={color} stopOpacity={0.3} />
569 <stop offset="100%" stopColor={color} stopOpacity={0} />
570 </linearGradient>
571 </defs>
572 {showArea && (
573 <Area
574 type="monotone"
575 dataKey="value"
576 stroke="transparent"
577 fill="url(#sparklineGradient)"
578 />
579 )}
580 <Line
581 type="monotone"
582 dataKey="value"
583 stroke={color}
584 strokeWidth={1.5}
585 dot={false}
586 />
587 </AreaChart>
588 </ChartContainer>
589 );
590}
591
592interface StatsCardProps extends VariantProps<typeof statsCardVariants> {
593 title: string;
594 value: string | number;
595 change?: { value: number; label: string };
596 sparklineData?: number[];
597 color?: string;
598 className?: string;
599}
600
601export function StatsCard({
602 title,
603 value,
604 change,
605 sparklineData,
606 color = "var(--chart-1)",
607 className,
608 padding,
609}: StatsCardProps) {
610 const isPositive = change && change.value >= 0;
611
612 return (
613 <div className={cn(statsCardVariants({ padding }), className)}>
614 <div className="flex items-start justify-between gap-4">
615 <div className="flex-1">
616 <p className="text-xs text-muted-foreground">{title}</p>
617 <p className="text-2xl font-bold font-mono text-foreground mt-1">
618 {value}
619 </p>
620 {change && (
621 <p
622 className={cn(
623 "text-xs mt-1 font-medium",
624 isPositive
625 ? "text-emerald-600 dark:text-emerald-400"
626 : "text-red-600 dark:text-red-400",
627 )}
628 >
629 {isPositive ? "+" : ""}
630 {change.value}% {change.label}
631 </p>
632 )}
633 </div>
634 {sparklineData && (
635 <div className="w-24">
636 <Sparkline data={sparklineData} color={color} height={48} />
637 </div>
638 )}
639 </div>
640 </div>
641 );
642}
Bar, pie, radial & stats
Bar chart, donut segments, radial progress, and KPI card

Sales by quarter

Browser share

Goal

Weekly completion

MRR

$12.4k

+4.1% vs last month

1"use client";
2
3import { cva, type VariantProps } from "class-variance-authority";
4import {
5 Area,
6 AreaChart,
7 Bar,
8 BarChart,
9 CartesianGrid,
10 Cell,
11 Line,
12 LineChart,
13 Pie,
14 PieChart,
15 RadialBar,
16 RadialBarChart,
17 XAxis,
18 YAxis,
19} from "recharts";
20import {
21 ChartConfig,
22 ChartContainer,
23 ChartTooltip,
24 ChartTooltipContent,
25 ChartLegend,
26 ChartLegendContent,
27} from "@/components/ui/chart";
28import { cn } from "@/lib/utils";
29
30const chartShellVariants = cva("rounded-lg border border-border", {
31 variants: {
32 surface: {
33 card: "bg-card",
34 muted: "bg-muted/40",
35 },
36 padding: {
37 sm: "p-3",
38 md: "p-4",
39 lg: "p-5",
40 },
41 },
42 defaultVariants: {
43 surface: "card",
44 padding: "md",
45 },
46});
47
48const chartPlotVariants = cva("w-full", {
49 variants: {
50 plotSize: {
51 sm: "h-[160px]",
52 md: "h-[200px]",
53 lg: "h-[240px]",
54 },
55 },
56 defaultVariants: {
57 plotSize: "md",
58 },
59});
60
61const chartTitleBlockVariants = cva("", {
62 variants: {
63 spacing: {
64 default: "mb-4",
65 tight: "mb-2",
66 },
67 },
68 defaultVariants: {
69 spacing: "default",
70 },
71});
72
73const radialDialVariants = cva("w-full mx-auto", {
74 variants: {
75 dialSize: {
76 sm: "h-[120px]",
77 md: "h-[160px]",
78 lg: "h-[200px]",
79 },
80 },
81 defaultVariants: {
82 dialSize: "md",
83 },
84});
85
86const statsCardVariants = cva("rounded-lg border border-border bg-card", {
87 variants: {
88 padding: {
89 sm: "p-3",
90 md: "p-4",
91 lg: "p-5",
92 },
93 },
94 defaultVariants: {
95 padding: "md",
96 },
97});
98
99type ChartShellProps = VariantProps<typeof chartShellVariants>;
100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;
101
102interface BaseChartProps extends ChartShellProps {
103 className?: string;
104 title?: string;
105 description?: string;
106}
107
108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}
109
110interface AreaChartData {
111 label: string;
112 [key: string]: string | number;
113}
114
115interface CustomAreaChartProps extends CartesianChartProps {
116 data: AreaChartData[];
117 dataKeys: { key: string; label: string; color: string }[];
118 stacked?: boolean;
119 showGrid?: boolean;
120 showLegend?: boolean;
121}
122
123const areaChartConfig = (
124 dataKeys: { key: string; label: string; color: string }[],
125): ChartConfig => {
126 return dataKeys.reduce((acc, { key, label, color }) => {
127 acc[key] = { label, color };
128 return acc;
129 }, {} as ChartConfig);
130};
131
132export function CustomAreaChart({
133 data,
134 dataKeys,
135 stacked = false,
136 showGrid = true,
137 showLegend = true,
138 className,
139 title,
140 description,
141 surface,
142 padding,
143 plotSize,
144}: CustomAreaChartProps) {
145 const config = areaChartConfig(dataKeys);
146
147 return (
148 <div className={cn(chartShellVariants({ surface, padding }), className)}>
149 {(title || description) && (
150 <div className={chartTitleBlockVariants({ spacing: "default" })}>
151 {title && (
152 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
153 )}
154 {description && (
155 <p className="text-xs text-muted-foreground mt-1">{description}</p>
156 )}
157 </div>
158 )}
159 <ChartContainer
160 config={config}
161 className={chartPlotVariants({ plotSize })}
162 >
163 <AreaChart data={data} margin={{ left: -20, right: 12 }}>
164 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
165 <XAxis
166 dataKey="label"
167 tickLine={false}
168 axisLine={false}
169 tickMargin={8}
170 fontSize={11}
171 />
172 <YAxis
173 tickLine={false}
174 axisLine={false}
175 tickMargin={8}
176 fontSize={11}
177 />
178 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
179 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
180 {dataKeys.map(({ key, color }) => (
181 <Area
182 key={key}
183 dataKey={key}
184 type="monotone"
185 fill={color}
186 fillOpacity={0.2}
187 stroke={color}
188 strokeWidth={2}
189 stackId={stacked ? "1" : undefined}
190 />
191 ))}
192 </AreaChart>
193 </ChartContainer>
194 </div>
195 );
196}
197
198interface BarChartData {
199 label: string;
200 [key: string]: string | number;
201}
202
203interface CustomBarChartProps extends CartesianChartProps {
204 data: BarChartData[];
205 dataKeys: { key: string; label: string; color: string }[];
206 horizontal?: boolean;
207 stacked?: boolean;
208 showGrid?: boolean;
209 showLegend?: boolean;
210 barRadius?: number;
211}
212
213export function CustomBarChart({
214 data,
215 dataKeys,
216 horizontal = false,
217 stacked = false,
218 showGrid = true,
219 showLegend = true,
220 barRadius = 4,
221 className,
222 title,
223 description,
224 surface,
225 padding,
226 plotSize,
227}: CustomBarChartProps) {
228 const config = areaChartConfig(dataKeys);
229
230 return (
231 <div className={cn(chartShellVariants({ surface, padding }), className)}>
232 {(title || description) && (
233 <div className={chartTitleBlockVariants({ spacing: "default" })}>
234 {title && (
235 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
236 )}
237 {description && (
238 <p className="text-xs text-muted-foreground mt-1">{description}</p>
239 )}
240 </div>
241 )}
242 <ChartContainer
243 config={config}
244 className={chartPlotVariants({ plotSize })}
245 >
246 <BarChart
247 data={data}
248 layout={horizontal ? "vertical" : "horizontal"}
249 margin={{ left: horizontal ? 0 : -20, right: 12 }}
250 >
251 {showGrid && (
252 <CartesianGrid
253 strokeDasharray="3 3"
254 horizontal={!horizontal}
255 vertical={horizontal}
256 />
257 )}
258 {horizontal ? (
259 <>
260 <YAxis
261 dataKey="label"
262 type="category"
263 tickLine={false}
264 axisLine={false}
265 tickMargin={8}
266 fontSize={11}
267 width={80}
268 />
269 <XAxis
270 type="number"
271 tickLine={false}
272 axisLine={false}
273 fontSize={11}
274 />
275 </>
276 ) : (
277 <>
278 <XAxis
279 dataKey="label"
280 tickLine={false}
281 axisLine={false}
282 tickMargin={8}
283 fontSize={11}
284 />
285 <YAxis
286 tickLine={false}
287 axisLine={false}
288 tickMargin={8}
289 fontSize={11}
290 />
291 </>
292 )}
293 <ChartTooltip content={<ChartTooltipContent />} />
294 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
295 {dataKeys.map(({ key, color }) => (
296 <Bar
297 key={key}
298 dataKey={key}
299 fill={color}
300 radius={barRadius}
301 stackId={stacked ? "1" : undefined}
302 />
303 ))}
304 </BarChart>
305 </ChartContainer>
306 </div>
307 );
308}
309
310interface LineChartData {
311 label: string;
312 [key: string]: string | number;
313}
314
315interface CustomLineChartProps extends CartesianChartProps {
316 data: LineChartData[];
317 dataKeys: { key: string; label: string; color: string }[];
318 showGrid?: boolean;
319 showLegend?: boolean;
320 showDots?: boolean;
321 curved?: boolean;
322}
323
324export function CustomLineChart({
325 data,
326 dataKeys,
327 showGrid = true,
328 showLegend = true,
329 showDots = true,
330 curved = true,
331 className,
332 title,
333 description,
334 surface,
335 padding,
336 plotSize,
337}: CustomLineChartProps) {
338 const config = areaChartConfig(dataKeys);
339
340 return (
341 <div className={cn(chartShellVariants({ surface, padding }), className)}>
342 {(title || description) && (
343 <div className={chartTitleBlockVariants({ spacing: "default" })}>
344 {title && (
345 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
346 )}
347 {description && (
348 <p className="text-xs text-muted-foreground mt-1">{description}</p>
349 )}
350 </div>
351 )}
352 <ChartContainer
353 config={config}
354 className={chartPlotVariants({ plotSize })}
355 >
356 <LineChart data={data} margin={{ left: -20, right: 12 }}>
357 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
358 <XAxis
359 dataKey="label"
360 tickLine={false}
361 axisLine={false}
362 tickMargin={8}
363 fontSize={11}
364 />
365 <YAxis
366 tickLine={false}
367 axisLine={false}
368 tickMargin={8}
369 fontSize={11}
370 />
371 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
372 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
373 {dataKeys.map(({ key, color }) => (
374 <Line
375 key={key}
376 dataKey={key}
377 type={curved ? "monotone" : "linear"}
378 stroke={color}
379 strokeWidth={2}
380 dot={showDots ? { fill: color, strokeWidth: 0, r: 3 } : false}
381 activeDot={{ r: 5, strokeWidth: 0 }}
382 />
383 ))}
384 </LineChart>
385 </ChartContainer>
386 </div>
387 );
388}
389
390interface PieChartData {
391 name: string;
392 value: number;
393 color: string;
394}
395
396interface CustomPieChartProps extends CartesianChartProps {
397 data: PieChartData[];
398 showLegend?: boolean;
399 innerRadius?: number;
400 paddingAngle?: number;
401}
402
403export function CustomPieChart({
404 data,
405 showLegend = true,
406 innerRadius = 0,
407 paddingAngle = 2,
408 className,
409 title,
410 description,
411 surface,
412 padding,
413 plotSize,
414}: CustomPieChartProps) {
415 const config: ChartConfig = data.reduce((acc, item) => {
416 acc[item.name] = { label: item.name, color: item.color };
417 return acc;
418 }, {} as ChartConfig);
419
420 return (
421 <div className={cn(chartShellVariants({ surface, padding }), className)}>
422 {(title || description) && (
423 <div className={chartTitleBlockVariants({ spacing: "default" })}>
424 {title && (
425 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
426 )}
427 {description && (
428 <p className="text-xs text-muted-foreground mt-1">{description}</p>
429 )}
430 </div>
431 )}
432 <ChartContainer
433 config={config}
434 className={chartPlotVariants({ plotSize })}
435 >
436 <PieChart>
437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />
438 {showLegend && (
439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />
440 )}
441 <Pie
442 data={data}
443 dataKey="value"
444 nameKey="name"
445 cx="50%"
446 cy="50%"
447 innerRadius={innerRadius}
448 paddingAngle={paddingAngle}
449 >
450 {data.map((entry, index) => (
451 <Cell key={`cell-${index}`} fill={entry.color} />
452 ))}
453 </Pie>
454 </PieChart>
455 </ChartContainer>
456 </div>
457 );
458}
459
460interface RadialProgressProps extends BaseChartProps {
461 value: number;
462 maxValue?: number;
463 color?: string;
464 label?: string;
465 size?: "sm" | "md" | "lg";
466}
467
468export function RadialProgress({
469 value,
470 maxValue = 100,
471 color = "var(--chart-1)",
472 label,
473 size = "md",
474 className,
475 title,
476 description,
477 surface,
478 padding,
479}: RadialProgressProps) {
480 const percentage = Math.min((value / maxValue) * 100, 100);
481 const data = [{ name: "progress", value: percentage, fill: color }];
482
483 const config: ChartConfig = {
484 progress: { label: label || "Progress", color },
485 };
486
487 return (
488 <div className={cn(chartShellVariants({ surface, padding }), className)}>
489 {(title || description) && (
490 <div className={chartTitleBlockVariants({ spacing: "tight" })}>
491 {title && (
492 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
493 )}
494 {description && (
495 <p className="text-xs text-muted-foreground mt-1">{description}</p>
496 )}
497 </div>
498 )}
499 <ChartContainer
500 config={config}
501 className={radialDialVariants({ dialSize: size })}
502 >
503 <RadialBarChart
504 data={data}
505 startAngle={90}
506 endAngle={90 - percentage * 3.6}
507 innerRadius="70%"
508 outerRadius="100%"
509 >
510 <RadialBar
511 dataKey="value"
512 background={{ fill: "var(--muted)" }}
513 cornerRadius={10}
514 />
515 <text
516 x="50%"
517 y="50%"
518 textAnchor="middle"
519 dominantBaseline="middle"
520 className="fill-foreground font-mono text-2xl font-bold"
521 >
522 {Math.round(percentage)}%
523 </text>
524 </RadialBarChart>
525 </ChartContainer>
526 {label && (
527 <p className="text-center text-xs text-muted-foreground mt-2">
528 {label}
529 </p>
530 )}
531 </div>
532 );
533}
534
535interface SparklineProps {
536 data: number[];
537 color?: string;
538 height?: number;
539 showArea?: boolean;
540 className?: string;
541}
542
543export function Sparkline({
544 data,
545 color = "var(--chart-1)",
546 height = 40,
547 showArea = true,
548 className,
549}: SparklineProps) {
550 const chartData = data.map((value, index) => ({ index, value }));
551
552 const config: ChartConfig = {
553 value: { label: "Value", color },
554 };
555
556 return (
557 <ChartContainer
558 config={config}
559 className={cn("w-full", className)}
560 style={{ height }}
561 >
562 <AreaChart
563 data={chartData}
564 margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
565 >
566 <defs>
567 <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
568 <stop offset="0%" stopColor={color} stopOpacity={0.3} />
569 <stop offset="100%" stopColor={color} stopOpacity={0} />
570 </linearGradient>
571 </defs>
572 {showArea && (
573 <Area
574 type="monotone"
575 dataKey="value"
576 stroke="transparent"
577 fill="url(#sparklineGradient)"
578 />
579 )}
580 <Line
581 type="monotone"
582 dataKey="value"
583 stroke={color}
584 strokeWidth={1.5}
585 dot={false}
586 />
587 </AreaChart>
588 </ChartContainer>
589 );
590}
591
592interface StatsCardProps extends VariantProps<typeof statsCardVariants> {
593 title: string;
594 value: string | number;
595 change?: { value: number; label: string };
596 sparklineData?: number[];
597 color?: string;
598 className?: string;
599}
600
601export function StatsCard({
602 title,
603 value,
604 change,
605 sparklineData,
606 color = "var(--chart-1)",
607 className,
608 padding,
609}: StatsCardProps) {
610 const isPositive = change && change.value >= 0;
611
612 return (
613 <div className={cn(statsCardVariants({ padding }), className)}>
614 <div className="flex items-start justify-between gap-4">
615 <div className="flex-1">
616 <p className="text-xs text-muted-foreground">{title}</p>
617 <p className="text-2xl font-bold font-mono text-foreground mt-1">
618 {value}
619 </p>
620 {change && (
621 <p
622 className={cn(
623 "text-xs mt-1 font-medium",
624 isPositive
625 ? "text-emerald-600 dark:text-emerald-400"
626 : "text-red-600 dark:text-red-400",
627 )}
628 >
629 {isPositive ? "+" : ""}
630 {change.value}% {change.label}
631 </p>
632 )}
633 </div>
634 {sparklineData && (
635 <div className="w-24">
636 <Sparkline data={sparklineData} color={color} height={48} />
637 </div>
638 )}
639 </div>
640 </div>
641 );
642}
Stats cards
KPI row with change labels and sparklines—good for dashboards and product analytics

Total Downloads

36,340

+12.5% from last month

Active Projects

8,130

+8.2% from last month

GitHub Stars

12,300

+24.3% from last month

Error Rate

0.12%

-2.1% from last week

1"use client";
2
3import { cva, type VariantProps } from "class-variance-authority";
4import {
5 Area,
6 AreaChart,
7 Bar,
8 BarChart,
9 CartesianGrid,
10 Cell,
11 Line,
12 LineChart,
13 Pie,
14 PieChart,
15 RadialBar,
16 RadialBarChart,
17 XAxis,
18 YAxis,
19} from "recharts";
20import {
21 ChartConfig,
22 ChartContainer,
23 ChartTooltip,
24 ChartTooltipContent,
25 ChartLegend,
26 ChartLegendContent,
27} from "@/components/ui/chart";
28import { cn } from "@/lib/utils";
29
30const chartShellVariants = cva("rounded-lg border border-border", {
31 variants: {
32 surface: {
33 card: "bg-card",
34 muted: "bg-muted/40",
35 },
36 padding: {
37 sm: "p-3",
38 md: "p-4",
39 lg: "p-5",
40 },
41 },
42 defaultVariants: {
43 surface: "card",
44 padding: "md",
45 },
46});
47
48const chartPlotVariants = cva("w-full", {
49 variants: {
50 plotSize: {
51 sm: "h-[160px]",
52 md: "h-[200px]",
53 lg: "h-[240px]",
54 },
55 },
56 defaultVariants: {
57 plotSize: "md",
58 },
59});
60
61const chartTitleBlockVariants = cva("", {
62 variants: {
63 spacing: {
64 default: "mb-4",
65 tight: "mb-2",
66 },
67 },
68 defaultVariants: {
69 spacing: "default",
70 },
71});
72
73const radialDialVariants = cva("w-full mx-auto", {
74 variants: {
75 dialSize: {
76 sm: "h-[120px]",
77 md: "h-[160px]",
78 lg: "h-[200px]",
79 },
80 },
81 defaultVariants: {
82 dialSize: "md",
83 },
84});
85
86const statsCardVariants = cva("rounded-lg border border-border bg-card", {
87 variants: {
88 padding: {
89 sm: "p-3",
90 md: "p-4",
91 lg: "p-5",
92 },
93 },
94 defaultVariants: {
95 padding: "md",
96 },
97});
98
99type ChartShellProps = VariantProps<typeof chartShellVariants>;
100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;
101
102interface BaseChartProps extends ChartShellProps {
103 className?: string;
104 title?: string;
105 description?: string;
106}
107
108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}
109
110interface AreaChartData {
111 label: string;
112 [key: string]: string | number;
113}
114
115interface CustomAreaChartProps extends CartesianChartProps {
116 data: AreaChartData[];
117 dataKeys: { key: string; label: string; color: string }[];
118 stacked?: boolean;
119 showGrid?: boolean;
120 showLegend?: boolean;
121}
122
123const areaChartConfig = (
124 dataKeys: { key: string; label: string; color: string }[],
125): ChartConfig => {
126 return dataKeys.reduce((acc, { key, label, color }) => {
127 acc[key] = { label, color };
128 return acc;
129 }, {} as ChartConfig);
130};
131
132export function CustomAreaChart({
133 data,
134 dataKeys,
135 stacked = false,
136 showGrid = true,
137 showLegend = true,
138 className,
139 title,
140 description,
141 surface,
142 padding,
143 plotSize,
144}: CustomAreaChartProps) {
145 const config = areaChartConfig(dataKeys);
146
147 return (
148 <div className={cn(chartShellVariants({ surface, padding }), className)}>
149 {(title || description) && (
150 <div className={chartTitleBlockVariants({ spacing: "default" })}>
151 {title && (
152 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
153 )}
154 {description && (
155 <p className="text-xs text-muted-foreground mt-1">{description}</p>
156 )}
157 </div>
158 )}
159 <ChartContainer
160 config={config}
161 className={chartPlotVariants({ plotSize })}
162 >
163 <AreaChart data={data} margin={{ left: -20, right: 12 }}>
164 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
165 <XAxis
166 dataKey="label"
167 tickLine={false}
168 axisLine={false}
169 tickMargin={8}
170 fontSize={11}
171 />
172 <YAxis
173 tickLine={false}
174 axisLine={false}
175 tickMargin={8}
176 fontSize={11}
177 />
178 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
179 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
180 {dataKeys.map(({ key, color }) => (
181 <Area
182 key={key}
183 dataKey={key}
184 type="monotone"
185 fill={color}
186 fillOpacity={0.2}
187 stroke={color}
188 strokeWidth={2}
189 stackId={stacked ? "1" : undefined}
190 />
191 ))}
192 </AreaChart>
193 </ChartContainer>
194 </div>
195 );
196}
197
198interface BarChartData {
199 label: string;
200 [key: string]: string | number;
201}
202
203interface CustomBarChartProps extends CartesianChartProps {
204 data: BarChartData[];
205 dataKeys: { key: string; label: string; color: string }[];
206 horizontal?: boolean;
207 stacked?: boolean;
208 showGrid?: boolean;
209 showLegend?: boolean;
210 barRadius?: number;
211}
212
213export function CustomBarChart({
214 data,
215 dataKeys,
216 horizontal = false,
217 stacked = false,
218 showGrid = true,
219 showLegend = true,
220 barRadius = 4,
221 className,
222 title,
223 description,
224 surface,
225 padding,
226 plotSize,
227}: CustomBarChartProps) {
228 const config = areaChartConfig(dataKeys);
229
230 return (
231 <div className={cn(chartShellVariants({ surface, padding }), className)}>
232 {(title || description) && (
233 <div className={chartTitleBlockVariants({ spacing: "default" })}>
234 {title && (
235 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
236 )}
237 {description && (
238 <p className="text-xs text-muted-foreground mt-1">{description}</p>
239 )}
240 </div>
241 )}
242 <ChartContainer
243 config={config}
244 className={chartPlotVariants({ plotSize })}
245 >
246 <BarChart
247 data={data}
248 layout={horizontal ? "vertical" : "horizontal"}
249 margin={{ left: horizontal ? 0 : -20, right: 12 }}
250 >
251 {showGrid && (
252 <CartesianGrid
253 strokeDasharray="3 3"
254 horizontal={!horizontal}
255 vertical={horizontal}
256 />
257 )}
258 {horizontal ? (
259 <>
260 <YAxis
261 dataKey="label"
262 type="category"
263 tickLine={false}
264 axisLine={false}
265 tickMargin={8}
266 fontSize={11}
267 width={80}
268 />
269 <XAxis
270 type="number"
271 tickLine={false}
272 axisLine={false}
273 fontSize={11}
274 />
275 </>
276 ) : (
277 <>
278 <XAxis
279 dataKey="label"
280 tickLine={false}
281 axisLine={false}
282 tickMargin={8}
283 fontSize={11}
284 />
285 <YAxis
286 tickLine={false}
287 axisLine={false}
288 tickMargin={8}
289 fontSize={11}
290 />
291 </>
292 )}
293 <ChartTooltip content={<ChartTooltipContent />} />
294 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
295 {dataKeys.map(({ key, color }) => (
296 <Bar
297 key={key}
298 dataKey={key}
299 fill={color}
300 radius={barRadius}
301 stackId={stacked ? "1" : undefined}
302 />
303 ))}
304 </BarChart>
305 </ChartContainer>
306 </div>
307 );
308}
309
310interface LineChartData {
311 label: string;
312 [key: string]: string | number;
313}
314
315interface CustomLineChartProps extends CartesianChartProps {
316 data: LineChartData[];
317 dataKeys: { key: string; label: string; color: string }[];
318 showGrid?: boolean;
319 showLegend?: boolean;
320 showDots?: boolean;
321 curved?: boolean;
322}
323
324export function CustomLineChart({
325 data,
326 dataKeys,
327 showGrid = true,
328 showLegend = true,
329 showDots = true,
330 curved = true,
331 className,
332 title,
333 description,
334 surface,
335 padding,
336 plotSize,
337}: CustomLineChartProps) {
338 const config = areaChartConfig(dataKeys);
339
340 return (
341 <div className={cn(chartShellVariants({ surface, padding }), className)}>
342 {(title || description) && (
343 <div className={chartTitleBlockVariants({ spacing: "default" })}>
344 {title && (
345 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
346 )}
347 {description && (
348 <p className="text-xs text-muted-foreground mt-1">{description}</p>
349 )}
350 </div>
351 )}
352 <ChartContainer
353 config={config}
354 className={chartPlotVariants({ plotSize })}
355 >
356 <LineChart data={data} margin={{ left: -20, right: 12 }}>
357 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
358 <XAxis
359 dataKey="label"
360 tickLine={false}
361 axisLine={false}
362 tickMargin={8}
363 fontSize={11}
364 />
365 <YAxis
366 tickLine={false}
367 axisLine={false}
368 tickMargin={8}
369 fontSize={11}
370 />
371 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
372 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
373 {dataKeys.map(({ key, color }) => (
374 <Line
375 key={key}
376 dataKey={key}
377 type={curved ? "monotone" : "linear"}
378 stroke={color}
379 strokeWidth={2}
380 dot={showDots ? { fill: color, strokeWidth: 0, r: 3 } : false}
381 activeDot={{ r: 5, strokeWidth: 0 }}
382 />
383 ))}
384 </LineChart>
385 </ChartContainer>
386 </div>
387 );
388}
389
390interface PieChartData {
391 name: string;
392 value: number;
393 color: string;
394}
395
396interface CustomPieChartProps extends CartesianChartProps {
397 data: PieChartData[];
398 showLegend?: boolean;
399 innerRadius?: number;
400 paddingAngle?: number;
401}
402
403export function CustomPieChart({
404 data,
405 showLegend = true,
406 innerRadius = 0,
407 paddingAngle = 2,
408 className,
409 title,
410 description,
411 surface,
412 padding,
413 plotSize,
414}: CustomPieChartProps) {
415 const config: ChartConfig = data.reduce((acc, item) => {
416 acc[item.name] = { label: item.name, color: item.color };
417 return acc;
418 }, {} as ChartConfig);
419
420 return (
421 <div className={cn(chartShellVariants({ surface, padding }), className)}>
422 {(title || description) && (
423 <div className={chartTitleBlockVariants({ spacing: "default" })}>
424 {title && (
425 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
426 )}
427 {description && (
428 <p className="text-xs text-muted-foreground mt-1">{description}</p>
429 )}
430 </div>
431 )}
432 <ChartContainer
433 config={config}
434 className={chartPlotVariants({ plotSize })}
435 >
436 <PieChart>
437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />
438 {showLegend && (
439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />
440 )}
441 <Pie
442 data={data}
443 dataKey="value"
444 nameKey="name"
445 cx="50%"
446 cy="50%"
447 innerRadius={innerRadius}
448 paddingAngle={paddingAngle}
449 >
450 {data.map((entry, index) => (
451 <Cell key={`cell-${index}`} fill={entry.color} />
452 ))}
453 </Pie>
454 </PieChart>
455 </ChartContainer>
456 </div>
457 );
458}
459
460interface RadialProgressProps extends BaseChartProps {
461 value: number;
462 maxValue?: number;
463 color?: string;
464 label?: string;
465 size?: "sm" | "md" | "lg";
466}
467
468export function RadialProgress({
469 value,
470 maxValue = 100,
471 color = "var(--chart-1)",
472 label,
473 size = "md",
474 className,
475 title,
476 description,
477 surface,
478 padding,
479}: RadialProgressProps) {
480 const percentage = Math.min((value / maxValue) * 100, 100);
481 const data = [{ name: "progress", value: percentage, fill: color }];
482
483 const config: ChartConfig = {
484 progress: { label: label || "Progress", color },
485 };
486
487 return (
488 <div className={cn(chartShellVariants({ surface, padding }), className)}>
489 {(title || description) && (
490 <div className={chartTitleBlockVariants({ spacing: "tight" })}>
491 {title && (
492 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
493 )}
494 {description && (
495 <p className="text-xs text-muted-foreground mt-1">{description}</p>
496 )}
497 </div>
498 )}
499 <ChartContainer
500 config={config}
501 className={radialDialVariants({ dialSize: size })}
502 >
503 <RadialBarChart
504 data={data}
505 startAngle={90}
506 endAngle={90 - percentage * 3.6}
507 innerRadius="70%"
508 outerRadius="100%"
509 >
510 <RadialBar
511 dataKey="value"
512 background={{ fill: "var(--muted)" }}
513 cornerRadius={10}
514 />
515 <text
516 x="50%"
517 y="50%"
518 textAnchor="middle"
519 dominantBaseline="middle"
520 className="fill-foreground font-mono text-2xl font-bold"
521 >
522 {Math.round(percentage)}%
523 </text>
524 </RadialBarChart>
525 </ChartContainer>
526 {label && (
527 <p className="text-center text-xs text-muted-foreground mt-2">
528 {label}
529 </p>
530 )}
531 </div>
532 );
533}
534
535interface SparklineProps {
536 data: number[];
537 color?: string;
538 height?: number;
539 showArea?: boolean;
540 className?: string;
541}
542
543export function Sparkline({
544 data,
545 color = "var(--chart-1)",
546 height = 40,
547 showArea = true,
548 className,
549}: SparklineProps) {
550 const chartData = data.map((value, index) => ({ index, value }));
551
552 const config: ChartConfig = {
553 value: { label: "Value", color },
554 };
555
556 return (
557 <ChartContainer
558 config={config}
559 className={cn("w-full", className)}
560 style={{ height }}
561 >
562 <AreaChart
563 data={chartData}
564 margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
565 >
566 <defs>
567 <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
568 <stop offset="0%" stopColor={color} stopOpacity={0.3} />
569 <stop offset="100%" stopColor={color} stopOpacity={0} />
570 </linearGradient>
571 </defs>
572 {showArea && (
573 <Area
574 type="monotone"
575 dataKey="value"
576 stroke="transparent"
577 fill="url(#sparklineGradient)"
578 />
579 )}
580 <Line
581 type="monotone"
582 dataKey="value"
583 stroke={color}
584 strokeWidth={1.5}
585 dot={false}
586 />
587 </AreaChart>
588 </ChartContainer>
589 );
590}
591
592interface StatsCardProps extends VariantProps<typeof statsCardVariants> {
593 title: string;
594 value: string | number;
595 change?: { value: number; label: string };
596 sparklineData?: number[];
597 color?: string;
598 className?: string;
599}
600
601export function StatsCard({
602 title,
603 value,
604 change,
605 sparklineData,
606 color = "var(--chart-1)",
607 className,
608 padding,
609}: StatsCardProps) {
610 const isPositive = change && change.value >= 0;
611
612 return (
613 <div className={cn(statsCardVariants({ padding }), className)}>
614 <div className="flex items-start justify-between gap-4">
615 <div className="flex-1">
616 <p className="text-xs text-muted-foreground">{title}</p>
617 <p className="text-2xl font-bold font-mono text-foreground mt-1">
618 {value}
619 </p>
620 {change && (
621 <p
622 className={cn(
623 "text-xs mt-1 font-medium",
624 isPositive
625 ? "text-emerald-600 dark:text-emerald-400"
626 : "text-red-600 dark:text-red-400",
627 )}
628 >
629 {isPositive ? "+" : ""}
630 {change.value}% {change.label}
631 </p>
632 )}
633 </div>
634 {sparklineData && (
635 <div className="w-24">
636 <Sparkline data={sparklineData} color={color} height={48} />
637 </div>
638 )}
639 </div>
640 </div>
641 );
642}
Growth & language mix
Multi-series line (downloads vs stars) and stacked area (TypeScript vs JavaScript by day)

Growth metrics

Monthly downloads and GitHub stars over time

Language usage

Projects created by language preference (stacked)

1"use client";
2
3import { cva, type VariantProps } from "class-variance-authority";
4import {
5 Area,
6 AreaChart,
7 Bar,
8 BarChart,
9 CartesianGrid,
10 Cell,
11 Line,
12 LineChart,
13 Pie,
14 PieChart,
15 RadialBar,
16 RadialBarChart,
17 XAxis,
18 YAxis,
19} from "recharts";
20import {
21 ChartConfig,
22 ChartContainer,
23 ChartTooltip,
24 ChartTooltipContent,
25 ChartLegend,
26 ChartLegendContent,
27} from "@/components/ui/chart";
28import { cn } from "@/lib/utils";
29
30const chartShellVariants = cva("rounded-lg border border-border", {
31 variants: {
32 surface: {
33 card: "bg-card",
34 muted: "bg-muted/40",
35 },
36 padding: {
37 sm: "p-3",
38 md: "p-4",
39 lg: "p-5",
40 },
41 },
42 defaultVariants: {
43 surface: "card",
44 padding: "md",
45 },
46});
47
48const chartPlotVariants = cva("w-full", {
49 variants: {
50 plotSize: {
51 sm: "h-[160px]",
52 md: "h-[200px]",
53 lg: "h-[240px]",
54 },
55 },
56 defaultVariants: {
57 plotSize: "md",
58 },
59});
60
61const chartTitleBlockVariants = cva("", {
62 variants: {
63 spacing: {
64 default: "mb-4",
65 tight: "mb-2",
66 },
67 },
68 defaultVariants: {
69 spacing: "default",
70 },
71});
72
73const radialDialVariants = cva("w-full mx-auto", {
74 variants: {
75 dialSize: {
76 sm: "h-[120px]",
77 md: "h-[160px]",
78 lg: "h-[200px]",
79 },
80 },
81 defaultVariants: {
82 dialSize: "md",
83 },
84});
85
86const statsCardVariants = cva("rounded-lg border border-border bg-card", {
87 variants: {
88 padding: {
89 sm: "p-3",
90 md: "p-4",
91 lg: "p-5",
92 },
93 },
94 defaultVariants: {
95 padding: "md",
96 },
97});
98
99type ChartShellProps = VariantProps<typeof chartShellVariants>;
100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;
101
102interface BaseChartProps extends ChartShellProps {
103 className?: string;
104 title?: string;
105 description?: string;
106}
107
108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}
109
110interface AreaChartData {
111 label: string;
112 [key: string]: string | number;
113}
114
115interface CustomAreaChartProps extends CartesianChartProps {
116 data: AreaChartData[];
117 dataKeys: { key: string; label: string; color: string }[];
118 stacked?: boolean;
119 showGrid?: boolean;
120 showLegend?: boolean;
121}
122
123const areaChartConfig = (
124 dataKeys: { key: string; label: string; color: string }[],
125): ChartConfig => {
126 return dataKeys.reduce((acc, { key, label, color }) => {
127 acc[key] = { label, color };
128 return acc;
129 }, {} as ChartConfig);
130};
131
132export function CustomAreaChart({
133 data,
134 dataKeys,
135 stacked = false,
136 showGrid = true,
137 showLegend = true,
138 className,
139 title,
140 description,
141 surface,
142 padding,
143 plotSize,
144}: CustomAreaChartProps) {
145 const config = areaChartConfig(dataKeys);
146
147 return (
148 <div className={cn(chartShellVariants({ surface, padding }), className)}>
149 {(title || description) && (
150 <div className={chartTitleBlockVariants({ spacing: "default" })}>
151 {title && (
152 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
153 )}
154 {description && (
155 <p className="text-xs text-muted-foreground mt-1">{description}</p>
156 )}
157 </div>
158 )}
159 <ChartContainer
160 config={config}
161 className={chartPlotVariants({ plotSize })}
162 >
163 <AreaChart data={data} margin={{ left: -20, right: 12 }}>
164 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
165 <XAxis
166 dataKey="label"
167 tickLine={false}
168 axisLine={false}
169 tickMargin={8}
170 fontSize={11}
171 />
172 <YAxis
173 tickLine={false}
174 axisLine={false}
175 tickMargin={8}
176 fontSize={11}
177 />
178 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
179 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
180 {dataKeys.map(({ key, color }) => (
181 <Area
182 key={key}
183 dataKey={key}
184 type="monotone"
185 fill={color}
186 fillOpacity={0.2}
187 stroke={color}
188 strokeWidth={2}
189 stackId={stacked ? "1" : undefined}
190 />
191 ))}
192 </AreaChart>
193 </ChartContainer>
194 </div>
195 );
196}
197
198interface BarChartData {
199 label: string;
200 [key: string]: string | number;
201}
202
203interface CustomBarChartProps extends CartesianChartProps {
204 data: BarChartData[];
205 dataKeys: { key: string; label: string; color: string }[];
206 horizontal?: boolean;
207 stacked?: boolean;
208 showGrid?: boolean;
209 showLegend?: boolean;
210 barRadius?: number;
211}
212
213export function CustomBarChart({
214 data,
215 dataKeys,
216 horizontal = false,
217 stacked = false,
218 showGrid = true,
219 showLegend = true,
220 barRadius = 4,
221 className,
222 title,
223 description,
224 surface,
225 padding,
226 plotSize,
227}: CustomBarChartProps) {
228 const config = areaChartConfig(dataKeys);
229
230 return (
231 <div className={cn(chartShellVariants({ surface, padding }), className)}>
232 {(title || description) && (
233 <div className={chartTitleBlockVariants({ spacing: "default" })}>
234 {title && (
235 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
236 )}
237 {description && (
238 <p className="text-xs text-muted-foreground mt-1">{description}</p>
239 )}
240 </div>
241 )}
242 <ChartContainer
243 config={config}
244 className={chartPlotVariants({ plotSize })}
245 >
246 <BarChart
247 data={data}
248 layout={horizontal ? "vertical" : "horizontal"}
249 margin={{ left: horizontal ? 0 : -20, right: 12 }}
250 >
251 {showGrid && (
252 <CartesianGrid
253 strokeDasharray="3 3"
254 horizontal={!horizontal}
255 vertical={horizontal}
256 />
257 )}
258 {horizontal ? (
259 <>
260 <YAxis
261 dataKey="label"
262 type="category"
263 tickLine={false}
264 axisLine={false}
265 tickMargin={8}
266 fontSize={11}
267 width={80}
268 />
269 <XAxis
270 type="number"
271 tickLine={false}
272 axisLine={false}
273 fontSize={11}
274 />
275 </>
276 ) : (
277 <>
278 <XAxis
279 dataKey="label"
280 tickLine={false}
281 axisLine={false}
282 tickMargin={8}
283 fontSize={11}
284 />
285 <YAxis
286 tickLine={false}
287 axisLine={false}
288 tickMargin={8}
289 fontSize={11}
290 />
291 </>
292 )}
293 <ChartTooltip content={<ChartTooltipContent />} />
294 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
295 {dataKeys.map(({ key, color }) => (
296 <Bar
297 key={key}
298 dataKey={key}
299 fill={color}
300 radius={barRadius}
301 stackId={stacked ? "1" : undefined}
302 />
303 ))}
304 </BarChart>
305 </ChartContainer>
306 </div>
307 );
308}
309
310interface LineChartData {
311 label: string;
312 [key: string]: string | number;
313}
314
315interface CustomLineChartProps extends CartesianChartProps {
316 data: LineChartData[];
317 dataKeys: { key: string; label: string; color: string }[];
318 showGrid?: boolean;
319 showLegend?: boolean;
320 showDots?: boolean;
321 curved?: boolean;
322}
323
324export function CustomLineChart({
325 data,
326 dataKeys,
327 showGrid = true,
328 showLegend = true,
329 showDots = true,
330 curved = true,
331 className,
332 title,
333 description,
334 surface,
335 padding,
336 plotSize,
337}: CustomLineChartProps) {
338 const config = areaChartConfig(dataKeys);
339
340 return (
341 <div className={cn(chartShellVariants({ surface, padding }), className)}>
342 {(title || description) && (
343 <div className={chartTitleBlockVariants({ spacing: "default" })}>
344 {title && (
345 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
346 )}
347 {description && (
348 <p className="text-xs text-muted-foreground mt-1">{description}</p>
349 )}
350 </div>
351 )}
352 <ChartContainer
353 config={config}
354 className={chartPlotVariants({ plotSize })}
355 >
356 <LineChart data={data} margin={{ left: -20, right: 12 }}>
357 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
358 <XAxis
359 dataKey="label"
360 tickLine={false}
361 axisLine={false}
362 tickMargin={8}
363 fontSize={11}
364 />
365 <YAxis
366 tickLine={false}
367 axisLine={false}
368 tickMargin={8}
369 fontSize={11}
370 />
371 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
372 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
373 {dataKeys.map(({ key, color }) => (
374 <Line
375 key={key}
376 dataKey={key}
377 type={curved ? "monotone" : "linear"}
378 stroke={color}
379 strokeWidth={2}
380 dot={showDots ? { fill: color, strokeWidth: 0, r: 3 } : false}
381 activeDot={{ r: 5, strokeWidth: 0 }}
382 />
383 ))}
384 </LineChart>
385 </ChartContainer>
386 </div>
387 );
388}
389
390interface PieChartData {
391 name: string;
392 value: number;
393 color: string;
394}
395
396interface CustomPieChartProps extends CartesianChartProps {
397 data: PieChartData[];
398 showLegend?: boolean;
399 innerRadius?: number;
400 paddingAngle?: number;
401}
402
403export function CustomPieChart({
404 data,
405 showLegend = true,
406 innerRadius = 0,
407 paddingAngle = 2,
408 className,
409 title,
410 description,
411 surface,
412 padding,
413 plotSize,
414}: CustomPieChartProps) {
415 const config: ChartConfig = data.reduce((acc, item) => {
416 acc[item.name] = { label: item.name, color: item.color };
417 return acc;
418 }, {} as ChartConfig);
419
420 return (
421 <div className={cn(chartShellVariants({ surface, padding }), className)}>
422 {(title || description) && (
423 <div className={chartTitleBlockVariants({ spacing: "default" })}>
424 {title && (
425 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
426 )}
427 {description && (
428 <p className="text-xs text-muted-foreground mt-1">{description}</p>
429 )}
430 </div>
431 )}
432 <ChartContainer
433 config={config}
434 className={chartPlotVariants({ plotSize })}
435 >
436 <PieChart>
437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />
438 {showLegend && (
439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />
440 )}
441 <Pie
442 data={data}
443 dataKey="value"
444 nameKey="name"
445 cx="50%"
446 cy="50%"
447 innerRadius={innerRadius}
448 paddingAngle={paddingAngle}
449 >
450 {data.map((entry, index) => (
451 <Cell key={`cell-${index}`} fill={entry.color} />
452 ))}
453 </Pie>
454 </PieChart>
455 </ChartContainer>
456 </div>
457 );
458}
459
460interface RadialProgressProps extends BaseChartProps {
461 value: number;
462 maxValue?: number;
463 color?: string;
464 label?: string;
465 size?: "sm" | "md" | "lg";
466}
467
468export function RadialProgress({
469 value,
470 maxValue = 100,
471 color = "var(--chart-1)",
472 label,
473 size = "md",
474 className,
475 title,
476 description,
477 surface,
478 padding,
479}: RadialProgressProps) {
480 const percentage = Math.min((value / maxValue) * 100, 100);
481 const data = [{ name: "progress", value: percentage, fill: color }];
482
483 const config: ChartConfig = {
484 progress: { label: label || "Progress", color },
485 };
486
487 return (
488 <div className={cn(chartShellVariants({ surface, padding }), className)}>
489 {(title || description) && (
490 <div className={chartTitleBlockVariants({ spacing: "tight" })}>
491 {title && (
492 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
493 )}
494 {description && (
495 <p className="text-xs text-muted-foreground mt-1">{description}</p>
496 )}
497 </div>
498 )}
499 <ChartContainer
500 config={config}
501 className={radialDialVariants({ dialSize: size })}
502 >
503 <RadialBarChart
504 data={data}
505 startAngle={90}
506 endAngle={90 - percentage * 3.6}
507 innerRadius="70%"
508 outerRadius="100%"
509 >
510 <RadialBar
511 dataKey="value"
512 background={{ fill: "var(--muted)" }}
513 cornerRadius={10}
514 />
515 <text
516 x="50%"
517 y="50%"
518 textAnchor="middle"
519 dominantBaseline="middle"
520 className="fill-foreground font-mono text-2xl font-bold"
521 >
522 {Math.round(percentage)}%
523 </text>
524 </RadialBarChart>
525 </ChartContainer>
526 {label && (
527 <p className="text-center text-xs text-muted-foreground mt-2">
528 {label}
529 </p>
530 )}
531 </div>
532 );
533}
534
535interface SparklineProps {
536 data: number[];
537 color?: string;
538 height?: number;
539 showArea?: boolean;
540 className?: string;
541}
542
543export function Sparkline({
544 data,
545 color = "var(--chart-1)",
546 height = 40,
547 showArea = true,
548 className,
549}: SparklineProps) {
550 const chartData = data.map((value, index) => ({ index, value }));
551
552 const config: ChartConfig = {
553 value: { label: "Value", color },
554 };
555
556 return (
557 <ChartContainer
558 config={config}
559 className={cn("w-full", className)}
560 style={{ height }}
561 >
562 <AreaChart
563 data={chartData}
564 margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
565 >
566 <defs>
567 <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
568 <stop offset="0%" stopColor={color} stopOpacity={0.3} />
569 <stop offset="100%" stopColor={color} stopOpacity={0} />
570 </linearGradient>
571 </defs>
572 {showArea && (
573 <Area
574 type="monotone"
575 dataKey="value"
576 stroke="transparent"
577 fill="url(#sparklineGradient)"
578 />
579 )}
580 <Line
581 type="monotone"
582 dataKey="value"
583 stroke={color}
584 strokeWidth={1.5}
585 dot={false}
586 />
587 </AreaChart>
588 </ChartContainer>
589 );
590}
591
592interface StatsCardProps extends VariantProps<typeof statsCardVariants> {
593 title: string;
594 value: string | number;
595 change?: { value: number; label: string };
596 sparklineData?: number[];
597 color?: string;
598 className?: string;
599}
600
601export function StatsCard({
602 title,
603 value,
604 change,
605 sparklineData,
606 color = "var(--chart-1)",
607 className,
608 padding,
609}: StatsCardProps) {
610 const isPositive = change && change.value >= 0;
611
612 return (
613 <div className={cn(statsCardVariants({ padding }), className)}>
614 <div className="flex items-start justify-between gap-4">
615 <div className="flex-1">
616 <p className="text-xs text-muted-foreground">{title}</p>
617 <p className="text-2xl font-bold font-mono text-foreground mt-1">
618 {value}
619 </p>
620 {change && (
621 <p
622 className={cn(
623 "text-xs mt-1 font-medium",
624 isPositive
625 ? "text-emerald-600 dark:text-emerald-400"
626 : "text-red-600 dark:text-red-400",
627 )}
628 >
629 {isPositive ? "+" : ""}
630 {change.value}% {change.label}
631 </p>
632 )}
633 </div>
634 {sparklineData && (
635 <div className="w-24">
636 <Sparkline data={sparklineData} color={color} height={48} />
637 </div>
638 )}
639 </div>
640 </div>
641 );
642}
Framework bars
Vertical and horizontal single-series bars for categorical counts

Framework popularity

Number of projects by framework

Framework comparison

Horizontal layout for long category labels

1"use client";
2
3import { cva, type VariantProps } from "class-variance-authority";
4import {
5 Area,
6 AreaChart,
7 Bar,
8 BarChart,
9 CartesianGrid,
10 Cell,
11 Line,
12 LineChart,
13 Pie,
14 PieChart,
15 RadialBar,
16 RadialBarChart,
17 XAxis,
18 YAxis,
19} from "recharts";
20import {
21 ChartConfig,
22 ChartContainer,
23 ChartTooltip,
24 ChartTooltipContent,
25 ChartLegend,
26 ChartLegendContent,
27} from "@/components/ui/chart";
28import { cn } from "@/lib/utils";
29
30const chartShellVariants = cva("rounded-lg border border-border", {
31 variants: {
32 surface: {
33 card: "bg-card",
34 muted: "bg-muted/40",
35 },
36 padding: {
37 sm: "p-3",
38 md: "p-4",
39 lg: "p-5",
40 },
41 },
42 defaultVariants: {
43 surface: "card",
44 padding: "md",
45 },
46});
47
48const chartPlotVariants = cva("w-full", {
49 variants: {
50 plotSize: {
51 sm: "h-[160px]",
52 md: "h-[200px]",
53 lg: "h-[240px]",
54 },
55 },
56 defaultVariants: {
57 plotSize: "md",
58 },
59});
60
61const chartTitleBlockVariants = cva("", {
62 variants: {
63 spacing: {
64 default: "mb-4",
65 tight: "mb-2",
66 },
67 },
68 defaultVariants: {
69 spacing: "default",
70 },
71});
72
73const radialDialVariants = cva("w-full mx-auto", {
74 variants: {
75 dialSize: {
76 sm: "h-[120px]",
77 md: "h-[160px]",
78 lg: "h-[200px]",
79 },
80 },
81 defaultVariants: {
82 dialSize: "md",
83 },
84});
85
86const statsCardVariants = cva("rounded-lg border border-border bg-card", {
87 variants: {
88 padding: {
89 sm: "p-3",
90 md: "p-4",
91 lg: "p-5",
92 },
93 },
94 defaultVariants: {
95 padding: "md",
96 },
97});
98
99type ChartShellProps = VariantProps<typeof chartShellVariants>;
100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;
101
102interface BaseChartProps extends ChartShellProps {
103 className?: string;
104 title?: string;
105 description?: string;
106}
107
108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}
109
110interface AreaChartData {
111 label: string;
112 [key: string]: string | number;
113}
114
115interface CustomAreaChartProps extends CartesianChartProps {
116 data: AreaChartData[];
117 dataKeys: { key: string; label: string; color: string }[];
118 stacked?: boolean;
119 showGrid?: boolean;
120 showLegend?: boolean;
121}
122
123const areaChartConfig = (
124 dataKeys: { key: string; label: string; color: string }[],
125): ChartConfig => {
126 return dataKeys.reduce((acc, { key, label, color }) => {
127 acc[key] = { label, color };
128 return acc;
129 }, {} as ChartConfig);
130};
131
132export function CustomAreaChart({
133 data,
134 dataKeys,
135 stacked = false,
136 showGrid = true,
137 showLegend = true,
138 className,
139 title,
140 description,
141 surface,
142 padding,
143 plotSize,
144}: CustomAreaChartProps) {
145 const config = areaChartConfig(dataKeys);
146
147 return (
148 <div className={cn(chartShellVariants({ surface, padding }), className)}>
149 {(title || description) && (
150 <div className={chartTitleBlockVariants({ spacing: "default" })}>
151 {title && (
152 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
153 )}
154 {description && (
155 <p className="text-xs text-muted-foreground mt-1">{description}</p>
156 )}
157 </div>
158 )}
159 <ChartContainer
160 config={config}
161 className={chartPlotVariants({ plotSize })}
162 >
163 <AreaChart data={data} margin={{ left: -20, right: 12 }}>
164 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
165 <XAxis
166 dataKey="label"
167 tickLine={false}
168 axisLine={false}
169 tickMargin={8}
170 fontSize={11}
171 />
172 <YAxis
173 tickLine={false}
174 axisLine={false}
175 tickMargin={8}
176 fontSize={11}
177 />
178 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
179 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
180 {dataKeys.map(({ key, color }) => (
181 <Area
182 key={key}
183 dataKey={key}
184 type="monotone"
185 fill={color}
186 fillOpacity={0.2}
187 stroke={color}
188 strokeWidth={2}
189 stackId={stacked ? "1" : undefined}
190 />
191 ))}
192 </AreaChart>
193 </ChartContainer>
194 </div>
195 );
196}
197
198interface BarChartData {
199 label: string;
200 [key: string]: string | number;
201}
202
203interface CustomBarChartProps extends CartesianChartProps {
204 data: BarChartData[];
205 dataKeys: { key: string; label: string; color: string }[];
206 horizontal?: boolean;
207 stacked?: boolean;
208 showGrid?: boolean;
209 showLegend?: boolean;
210 barRadius?: number;
211}
212
213export function CustomBarChart({
214 data,
215 dataKeys,
216 horizontal = false,
217 stacked = false,
218 showGrid = true,
219 showLegend = true,
220 barRadius = 4,
221 className,
222 title,
223 description,
224 surface,
225 padding,
226 plotSize,
227}: CustomBarChartProps) {
228 const config = areaChartConfig(dataKeys);
229
230 return (
231 <div className={cn(chartShellVariants({ surface, padding }), className)}>
232 {(title || description) && (
233 <div className={chartTitleBlockVariants({ spacing: "default" })}>
234 {title && (
235 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
236 )}
237 {description && (
238 <p className="text-xs text-muted-foreground mt-1">{description}</p>
239 )}
240 </div>
241 )}
242 <ChartContainer
243 config={config}
244 className={chartPlotVariants({ plotSize })}
245 >
246 <BarChart
247 data={data}
248 layout={horizontal ? "vertical" : "horizontal"}
249 margin={{ left: horizontal ? 0 : -20, right: 12 }}
250 >
251 {showGrid && (
252 <CartesianGrid
253 strokeDasharray="3 3"
254 horizontal={!horizontal}
255 vertical={horizontal}
256 />
257 )}
258 {horizontal ? (
259 <>
260 <YAxis
261 dataKey="label"
262 type="category"
263 tickLine={false}
264 axisLine={false}
265 tickMargin={8}
266 fontSize={11}
267 width={80}
268 />
269 <XAxis
270 type="number"
271 tickLine={false}
272 axisLine={false}
273 fontSize={11}
274 />
275 </>
276 ) : (
277 <>
278 <XAxis
279 dataKey="label"
280 tickLine={false}
281 axisLine={false}
282 tickMargin={8}
283 fontSize={11}
284 />
285 <YAxis
286 tickLine={false}
287 axisLine={false}
288 tickMargin={8}
289 fontSize={11}
290 />
291 </>
292 )}
293 <ChartTooltip content={<ChartTooltipContent />} />
294 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
295 {dataKeys.map(({ key, color }) => (
296 <Bar
297 key={key}
298 dataKey={key}
299 fill={color}
300 radius={barRadius}
301 stackId={stacked ? "1" : undefined}
302 />
303 ))}
304 </BarChart>
305 </ChartContainer>
306 </div>
307 );
308}
309
310interface LineChartData {
311 label: string;
312 [key: string]: string | number;
313}
314
315interface CustomLineChartProps extends CartesianChartProps {
316 data: LineChartData[];
317 dataKeys: { key: string; label: string; color: string }[];
318 showGrid?: boolean;
319 showLegend?: boolean;
320 showDots?: boolean;
321 curved?: boolean;
322}
323
324export function CustomLineChart({
325 data,
326 dataKeys,
327 showGrid = true,
328 showLegend = true,
329 showDots = true,
330 curved = true,
331 className,
332 title,
333 description,
334 surface,
335 padding,
336 plotSize,
337}: CustomLineChartProps) {
338 const config = areaChartConfig(dataKeys);
339
340 return (
341 <div className={cn(chartShellVariants({ surface, padding }), className)}>
342 {(title || description) && (
343 <div className={chartTitleBlockVariants({ spacing: "default" })}>
344 {title && (
345 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
346 )}
347 {description && (
348 <p className="text-xs text-muted-foreground mt-1">{description}</p>
349 )}
350 </div>
351 )}
352 <ChartContainer
353 config={config}
354 className={chartPlotVariants({ plotSize })}
355 >
356 <LineChart data={data} margin={{ left: -20, right: 12 }}>
357 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
358 <XAxis
359 dataKey="label"
360 tickLine={false}
361 axisLine={false}
362 tickMargin={8}
363 fontSize={11}
364 />
365 <YAxis
366 tickLine={false}
367 axisLine={false}
368 tickMargin={8}
369 fontSize={11}
370 />
371 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
372 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
373 {dataKeys.map(({ key, color }) => (
374 <Line
375 key={key}
376 dataKey={key}
377 type={curved ? "monotone" : "linear"}
378 stroke={color}
379 strokeWidth={2}
380 dot={showDots ? { fill: color, strokeWidth: 0, r: 3 } : false}
381 activeDot={{ r: 5, strokeWidth: 0 }}
382 />
383 ))}
384 </LineChart>
385 </ChartContainer>
386 </div>
387 );
388}
389
390interface PieChartData {
391 name: string;
392 value: number;
393 color: string;
394}
395
396interface CustomPieChartProps extends CartesianChartProps {
397 data: PieChartData[];
398 showLegend?: boolean;
399 innerRadius?: number;
400 paddingAngle?: number;
401}
402
403export function CustomPieChart({
404 data,
405 showLegend = true,
406 innerRadius = 0,
407 paddingAngle = 2,
408 className,
409 title,
410 description,
411 surface,
412 padding,
413 plotSize,
414}: CustomPieChartProps) {
415 const config: ChartConfig = data.reduce((acc, item) => {
416 acc[item.name] = { label: item.name, color: item.color };
417 return acc;
418 }, {} as ChartConfig);
419
420 return (
421 <div className={cn(chartShellVariants({ surface, padding }), className)}>
422 {(title || description) && (
423 <div className={chartTitleBlockVariants({ spacing: "default" })}>
424 {title && (
425 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
426 )}
427 {description && (
428 <p className="text-xs text-muted-foreground mt-1">{description}</p>
429 )}
430 </div>
431 )}
432 <ChartContainer
433 config={config}
434 className={chartPlotVariants({ plotSize })}
435 >
436 <PieChart>
437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />
438 {showLegend && (
439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />
440 )}
441 <Pie
442 data={data}
443 dataKey="value"
444 nameKey="name"
445 cx="50%"
446 cy="50%"
447 innerRadius={innerRadius}
448 paddingAngle={paddingAngle}
449 >
450 {data.map((entry, index) => (
451 <Cell key={`cell-${index}`} fill={entry.color} />
452 ))}
453 </Pie>
454 </PieChart>
455 </ChartContainer>
456 </div>
457 );
458}
459
460interface RadialProgressProps extends BaseChartProps {
461 value: number;
462 maxValue?: number;
463 color?: string;
464 label?: string;
465 size?: "sm" | "md" | "lg";
466}
467
468export function RadialProgress({
469 value,
470 maxValue = 100,
471 color = "var(--chart-1)",
472 label,
473 size = "md",
474 className,
475 title,
476 description,
477 surface,
478 padding,
479}: RadialProgressProps) {
480 const percentage = Math.min((value / maxValue) * 100, 100);
481 const data = [{ name: "progress", value: percentage, fill: color }];
482
483 const config: ChartConfig = {
484 progress: { label: label || "Progress", color },
485 };
486
487 return (
488 <div className={cn(chartShellVariants({ surface, padding }), className)}>
489 {(title || description) && (
490 <div className={chartTitleBlockVariants({ spacing: "tight" })}>
491 {title && (
492 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
493 )}
494 {description && (
495 <p className="text-xs text-muted-foreground mt-1">{description}</p>
496 )}
497 </div>
498 )}
499 <ChartContainer
500 config={config}
501 className={radialDialVariants({ dialSize: size })}
502 >
503 <RadialBarChart
504 data={data}
505 startAngle={90}
506 endAngle={90 - percentage * 3.6}
507 innerRadius="70%"
508 outerRadius="100%"
509 >
510 <RadialBar
511 dataKey="value"
512 background={{ fill: "var(--muted)" }}
513 cornerRadius={10}
514 />
515 <text
516 x="50%"
517 y="50%"
518 textAnchor="middle"
519 dominantBaseline="middle"
520 className="fill-foreground font-mono text-2xl font-bold"
521 >
522 {Math.round(percentage)}%
523 </text>
524 </RadialBarChart>
525 </ChartContainer>
526 {label && (
527 <p className="text-center text-xs text-muted-foreground mt-2">
528 {label}
529 </p>
530 )}
531 </div>
532 );
533}
534
535interface SparklineProps {
536 data: number[];
537 color?: string;
538 height?: number;
539 showArea?: boolean;
540 className?: string;
541}
542
543export function Sparkline({
544 data,
545 color = "var(--chart-1)",
546 height = 40,
547 showArea = true,
548 className,
549}: SparklineProps) {
550 const chartData = data.map((value, index) => ({ index, value }));
551
552 const config: ChartConfig = {
553 value: { label: "Value", color },
554 };
555
556 return (
557 <ChartContainer
558 config={config}
559 className={cn("w-full", className)}
560 style={{ height }}
561 >
562 <AreaChart
563 data={chartData}
564 margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
565 >
566 <defs>
567 <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
568 <stop offset="0%" stopColor={color} stopOpacity={0.3} />
569 <stop offset="100%" stopColor={color} stopOpacity={0} />
570 </linearGradient>
571 </defs>
572 {showArea && (
573 <Area
574 type="monotone"
575 dataKey="value"
576 stroke="transparent"
577 fill="url(#sparklineGradient)"
578 />
579 )}
580 <Line
581 type="monotone"
582 dataKey="value"
583 stroke={color}
584 strokeWidth={1.5}
585 dot={false}
586 />
587 </AreaChart>
588 </ChartContainer>
589 );
590}
591
592interface StatsCardProps extends VariantProps<typeof statsCardVariants> {
593 title: string;
594 value: string | number;
595 change?: { value: number; label: string };
596 sparklineData?: number[];
597 color?: string;
598 className?: string;
599}
600
601export function StatsCard({
602 title,
603 value,
604 change,
605 sparklineData,
606 color = "var(--chart-1)",
607 className,
608 padding,
609}: StatsCardProps) {
610 const isPositive = change && change.value >= 0;
611
612 return (
613 <div className={cn(statsCardVariants({ padding }), className)}>
614 <div className="flex items-start justify-between gap-4">
615 <div className="flex-1">
616 <p className="text-xs text-muted-foreground">{title}</p>
617 <p className="text-2xl font-bold font-mono text-foreground mt-1">
618 {value}
619 </p>
620 {change && (
621 <p
622 className={cn(
623 "text-xs mt-1 font-medium",
624 isPositive
625 ? "text-emerald-600 dark:text-emerald-400"
626 : "text-red-600 dark:text-red-400",
627 )}
628 >
629 {isPositive ? "+" : ""}
630 {change.value}% {change.label}
631 </p>
632 )}
633 </div>
634 {sparklineData && (
635 <div className="w-24">
636 <Sparkline data={sparklineData} color={color} height={48} />
637 </div>
638 )}
639 </div>
640 </div>
641 );
642}
Pie, donut & radials
Distribution pie, donut with innerRadius, and radial progress for completion-style metrics

Database distribution

Projects by database type

Donut chart

Same data with innerRadius

Build success

Success rate this week

Test coverage

Code coverage percentage

1"use client";
2
3import { cva, type VariantProps } from "class-variance-authority";
4import {
5 Area,
6 AreaChart,
7 Bar,
8 BarChart,
9 CartesianGrid,
10 Cell,
11 Line,
12 LineChart,
13 Pie,
14 PieChart,
15 RadialBar,
16 RadialBarChart,
17 XAxis,
18 YAxis,
19} from "recharts";
20import {
21 ChartConfig,
22 ChartContainer,
23 ChartTooltip,
24 ChartTooltipContent,
25 ChartLegend,
26 ChartLegendContent,
27} from "@/components/ui/chart";
28import { cn } from "@/lib/utils";
29
30const chartShellVariants = cva("rounded-lg border border-border", {
31 variants: {
32 surface: {
33 card: "bg-card",
34 muted: "bg-muted/40",
35 },
36 padding: {
37 sm: "p-3",
38 md: "p-4",
39 lg: "p-5",
40 },
41 },
42 defaultVariants: {
43 surface: "card",
44 padding: "md",
45 },
46});
47
48const chartPlotVariants = cva("w-full", {
49 variants: {
50 plotSize: {
51 sm: "h-[160px]",
52 md: "h-[200px]",
53 lg: "h-[240px]",
54 },
55 },
56 defaultVariants: {
57 plotSize: "md",
58 },
59});
60
61const chartTitleBlockVariants = cva("", {
62 variants: {
63 spacing: {
64 default: "mb-4",
65 tight: "mb-2",
66 },
67 },
68 defaultVariants: {
69 spacing: "default",
70 },
71});
72
73const radialDialVariants = cva("w-full mx-auto", {
74 variants: {
75 dialSize: {
76 sm: "h-[120px]",
77 md: "h-[160px]",
78 lg: "h-[200px]",
79 },
80 },
81 defaultVariants: {
82 dialSize: "md",
83 },
84});
85
86const statsCardVariants = cva("rounded-lg border border-border bg-card", {
87 variants: {
88 padding: {
89 sm: "p-3",
90 md: "p-4",
91 lg: "p-5",
92 },
93 },
94 defaultVariants: {
95 padding: "md",
96 },
97});
98
99type ChartShellProps = VariantProps<typeof chartShellVariants>;
100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;
101
102interface BaseChartProps extends ChartShellProps {
103 className?: string;
104 title?: string;
105 description?: string;
106}
107
108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}
109
110interface AreaChartData {
111 label: string;
112 [key: string]: string | number;
113}
114
115interface CustomAreaChartProps extends CartesianChartProps {
116 data: AreaChartData[];
117 dataKeys: { key: string; label: string; color: string }[];
118 stacked?: boolean;
119 showGrid?: boolean;
120 showLegend?: boolean;
121}
122
123const areaChartConfig = (
124 dataKeys: { key: string; label: string; color: string }[],
125): ChartConfig => {
126 return dataKeys.reduce((acc, { key, label, color }) => {
127 acc[key] = { label, color };
128 return acc;
129 }, {} as ChartConfig);
130};
131
132export function CustomAreaChart({
133 data,
134 dataKeys,
135 stacked = false,
136 showGrid = true,
137 showLegend = true,
138 className,
139 title,
140 description,
141 surface,
142 padding,
143 plotSize,
144}: CustomAreaChartProps) {
145 const config = areaChartConfig(dataKeys);
146
147 return (
148 <div className={cn(chartShellVariants({ surface, padding }), className)}>
149 {(title || description) && (
150 <div className={chartTitleBlockVariants({ spacing: "default" })}>
151 {title && (
152 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
153 )}
154 {description && (
155 <p className="text-xs text-muted-foreground mt-1">{description}</p>
156 )}
157 </div>
158 )}
159 <ChartContainer
160 config={config}
161 className={chartPlotVariants({ plotSize })}
162 >
163 <AreaChart data={data} margin={{ left: -20, right: 12 }}>
164 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
165 <XAxis
166 dataKey="label"
167 tickLine={false}
168 axisLine={false}
169 tickMargin={8}
170 fontSize={11}
171 />
172 <YAxis
173 tickLine={false}
174 axisLine={false}
175 tickMargin={8}
176 fontSize={11}
177 />
178 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
179 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
180 {dataKeys.map(({ key, color }) => (
181 <Area
182 key={key}
183 dataKey={key}
184 type="monotone"
185 fill={color}
186 fillOpacity={0.2}
187 stroke={color}
188 strokeWidth={2}
189 stackId={stacked ? "1" : undefined}
190 />
191 ))}
192 </AreaChart>
193 </ChartContainer>
194 </div>
195 );
196}
197
198interface BarChartData {
199 label: string;
200 [key: string]: string | number;
201}
202
203interface CustomBarChartProps extends CartesianChartProps {
204 data: BarChartData[];
205 dataKeys: { key: string; label: string; color: string }[];
206 horizontal?: boolean;
207 stacked?: boolean;
208 showGrid?: boolean;
209 showLegend?: boolean;
210 barRadius?: number;
211}
212
213export function CustomBarChart({
214 data,
215 dataKeys,
216 horizontal = false,
217 stacked = false,
218 showGrid = true,
219 showLegend = true,
220 barRadius = 4,
221 className,
222 title,
223 description,
224 surface,
225 padding,
226 plotSize,
227}: CustomBarChartProps) {
228 const config = areaChartConfig(dataKeys);
229
230 return (
231 <div className={cn(chartShellVariants({ surface, padding }), className)}>
232 {(title || description) && (
233 <div className={chartTitleBlockVariants({ spacing: "default" })}>
234 {title && (
235 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
236 )}
237 {description && (
238 <p className="text-xs text-muted-foreground mt-1">{description}</p>
239 )}
240 </div>
241 )}
242 <ChartContainer
243 config={config}
244 className={chartPlotVariants({ plotSize })}
245 >
246 <BarChart
247 data={data}
248 layout={horizontal ? "vertical" : "horizontal"}
249 margin={{ left: horizontal ? 0 : -20, right: 12 }}
250 >
251 {showGrid && (
252 <CartesianGrid
253 strokeDasharray="3 3"
254 horizontal={!horizontal}
255 vertical={horizontal}
256 />
257 )}
258 {horizontal ? (
259 <>
260 <YAxis
261 dataKey="label"
262 type="category"
263 tickLine={false}
264 axisLine={false}
265 tickMargin={8}
266 fontSize={11}
267 width={80}
268 />
269 <XAxis
270 type="number"
271 tickLine={false}
272 axisLine={false}
273 fontSize={11}
274 />
275 </>
276 ) : (
277 <>
278 <XAxis
279 dataKey="label"
280 tickLine={false}
281 axisLine={false}
282 tickMargin={8}
283 fontSize={11}
284 />
285 <YAxis
286 tickLine={false}
287 axisLine={false}
288 tickMargin={8}
289 fontSize={11}
290 />
291 </>
292 )}
293 <ChartTooltip content={<ChartTooltipContent />} />
294 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
295 {dataKeys.map(({ key, color }) => (
296 <Bar
297 key={key}
298 dataKey={key}
299 fill={color}
300 radius={barRadius}
301 stackId={stacked ? "1" : undefined}
302 />
303 ))}
304 </BarChart>
305 </ChartContainer>
306 </div>
307 );
308}
309
310interface LineChartData {
311 label: string;
312 [key: string]: string | number;
313}
314
315interface CustomLineChartProps extends CartesianChartProps {
316 data: LineChartData[];
317 dataKeys: { key: string; label: string; color: string }[];
318 showGrid?: boolean;
319 showLegend?: boolean;
320 showDots?: boolean;
321 curved?: boolean;
322}
323
324export function CustomLineChart({
325 data,
326 dataKeys,
327 showGrid = true,
328 showLegend = true,
329 showDots = true,
330 curved = true,
331 className,
332 title,
333 description,
334 surface,
335 padding,
336 plotSize,
337}: CustomLineChartProps) {
338 const config = areaChartConfig(dataKeys);
339
340 return (
341 <div className={cn(chartShellVariants({ surface, padding }), className)}>
342 {(title || description) && (
343 <div className={chartTitleBlockVariants({ spacing: "default" })}>
344 {title && (
345 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
346 )}
347 {description && (
348 <p className="text-xs text-muted-foreground mt-1">{description}</p>
349 )}
350 </div>
351 )}
352 <ChartContainer
353 config={config}
354 className={chartPlotVariants({ plotSize })}
355 >
356 <LineChart data={data} margin={{ left: -20, right: 12 }}>
357 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
358 <XAxis
359 dataKey="label"
360 tickLine={false}
361 axisLine={false}
362 tickMargin={8}
363 fontSize={11}
364 />
365 <YAxis
366 tickLine={false}
367 axisLine={false}
368 tickMargin={8}
369 fontSize={11}
370 />
371 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
372 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
373 {dataKeys.map(({ key, color }) => (
374 <Line
375 key={key}
376 dataKey={key}
377 type={curved ? "monotone" : "linear"}
378 stroke={color}
379 strokeWidth={2}
380 dot={showDots ? { fill: color, strokeWidth: 0, r: 3 } : false}
381 activeDot={{ r: 5, strokeWidth: 0 }}
382 />
383 ))}
384 </LineChart>
385 </ChartContainer>
386 </div>
387 );
388}
389
390interface PieChartData {
391 name: string;
392 value: number;
393 color: string;
394}
395
396interface CustomPieChartProps extends CartesianChartProps {
397 data: PieChartData[];
398 showLegend?: boolean;
399 innerRadius?: number;
400 paddingAngle?: number;
401}
402
403export function CustomPieChart({
404 data,
405 showLegend = true,
406 innerRadius = 0,
407 paddingAngle = 2,
408 className,
409 title,
410 description,
411 surface,
412 padding,
413 plotSize,
414}: CustomPieChartProps) {
415 const config: ChartConfig = data.reduce((acc, item) => {
416 acc[item.name] = { label: item.name, color: item.color };
417 return acc;
418 }, {} as ChartConfig);
419
420 return (
421 <div className={cn(chartShellVariants({ surface, padding }), className)}>
422 {(title || description) && (
423 <div className={chartTitleBlockVariants({ spacing: "default" })}>
424 {title && (
425 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
426 )}
427 {description && (
428 <p className="text-xs text-muted-foreground mt-1">{description}</p>
429 )}
430 </div>
431 )}
432 <ChartContainer
433 config={config}
434 className={chartPlotVariants({ plotSize })}
435 >
436 <PieChart>
437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />
438 {showLegend && (
439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />
440 )}
441 <Pie
442 data={data}
443 dataKey="value"
444 nameKey="name"
445 cx="50%"
446 cy="50%"
447 innerRadius={innerRadius}
448 paddingAngle={paddingAngle}
449 >
450 {data.map((entry, index) => (
451 <Cell key={`cell-${index}`} fill={entry.color} />
452 ))}
453 </Pie>
454 </PieChart>
455 </ChartContainer>
456 </div>
457 );
458}
459
460interface RadialProgressProps extends BaseChartProps {
461 value: number;
462 maxValue?: number;
463 color?: string;
464 label?: string;
465 size?: "sm" | "md" | "lg";
466}
467
468export function RadialProgress({
469 value,
470 maxValue = 100,
471 color = "var(--chart-1)",
472 label,
473 size = "md",
474 className,
475 title,
476 description,
477 surface,
478 padding,
479}: RadialProgressProps) {
480 const percentage = Math.min((value / maxValue) * 100, 100);
481 const data = [{ name: "progress", value: percentage, fill: color }];
482
483 const config: ChartConfig = {
484 progress: { label: label || "Progress", color },
485 };
486
487 return (
488 <div className={cn(chartShellVariants({ surface, padding }), className)}>
489 {(title || description) && (
490 <div className={chartTitleBlockVariants({ spacing: "tight" })}>
491 {title && (
492 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
493 )}
494 {description && (
495 <p className="text-xs text-muted-foreground mt-1">{description}</p>
496 )}
497 </div>
498 )}
499 <ChartContainer
500 config={config}
501 className={radialDialVariants({ dialSize: size })}
502 >
503 <RadialBarChart
504 data={data}
505 startAngle={90}
506 endAngle={90 - percentage * 3.6}
507 innerRadius="70%"
508 outerRadius="100%"
509 >
510 <RadialBar
511 dataKey="value"
512 background={{ fill: "var(--muted)" }}
513 cornerRadius={10}
514 />
515 <text
516 x="50%"
517 y="50%"
518 textAnchor="middle"
519 dominantBaseline="middle"
520 className="fill-foreground font-mono text-2xl font-bold"
521 >
522 {Math.round(percentage)}%
523 </text>
524 </RadialBarChart>
525 </ChartContainer>
526 {label && (
527 <p className="text-center text-xs text-muted-foreground mt-2">
528 {label}
529 </p>
530 )}
531 </div>
532 );
533}
534
535interface SparklineProps {
536 data: number[];
537 color?: string;
538 height?: number;
539 showArea?: boolean;
540 className?: string;
541}
542
543export function Sparkline({
544 data,
545 color = "var(--chart-1)",
546 height = 40,
547 showArea = true,
548 className,
549}: SparklineProps) {
550 const chartData = data.map((value, index) => ({ index, value }));
551
552 const config: ChartConfig = {
553 value: { label: "Value", color },
554 };
555
556 return (
557 <ChartContainer
558 config={config}
559 className={cn("w-full", className)}
560 style={{ height }}
561 >
562 <AreaChart
563 data={chartData}
564 margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
565 >
566 <defs>
567 <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
568 <stop offset="0%" stopColor={color} stopOpacity={0.3} />
569 <stop offset="100%" stopColor={color} stopOpacity={0} />
570 </linearGradient>
571 </defs>
572 {showArea && (
573 <Area
574 type="monotone"
575 dataKey="value"
576 stroke="transparent"
577 fill="url(#sparklineGradient)"
578 />
579 )}
580 <Line
581 type="monotone"
582 dataKey="value"
583 stroke={color}
584 strokeWidth={1.5}
585 dot={false}
586 />
587 </AreaChart>
588 </ChartContainer>
589 );
590}
591
592interface StatsCardProps extends VariantProps<typeof statsCardVariants> {
593 title: string;
594 value: string | number;
595 change?: { value: number; label: string };
596 sparklineData?: number[];
597 color?: string;
598 className?: string;
599}
600
601export function StatsCard({
602 title,
603 value,
604 change,
605 sparklineData,
606 color = "var(--chart-1)",
607 className,
608 padding,
609}: StatsCardProps) {
610 const isPositive = change && change.value >= 0;
611
612 return (
613 <div className={cn(statsCardVariants({ padding }), className)}>
614 <div className="flex items-start justify-between gap-4">
615 <div className="flex-1">
616 <p className="text-xs text-muted-foreground">{title}</p>
617 <p className="text-2xl font-bold font-mono text-foreground mt-1">
618 {value}
619 </p>
620 {change && (
621 <p
622 className={cn(
623 "text-xs mt-1 font-medium",
624 isPositive
625 ? "text-emerald-600 dark:text-emerald-400"
626 : "text-red-600 dark:text-red-400",
627 )}
628 >
629 {isPositive ? "+" : ""}
630 {change.value}% {change.label}
631 </p>
632 )}
633 </div>
634 {sparklineData && (
635 <div className="w-24">
636 <Sparkline data={sparklineData} color={color} height={48} />
637 </div>
638 )}
639 </div>
640 </div>
641 );
642}
Grouped vs stacked bars
Same daily series compared side-by-side or stacked for totals

Daily language usage

Grouped bar comparison

Stacked daily usage

Stacked bars for part-to-whole by day

1"use client";
2
3import { cva, type VariantProps } from "class-variance-authority";
4import {
5 Area,
6 AreaChart,
7 Bar,
8 BarChart,
9 CartesianGrid,
10 Cell,
11 Line,
12 LineChart,
13 Pie,
14 PieChart,
15 RadialBar,
16 RadialBarChart,
17 XAxis,
18 YAxis,
19} from "recharts";
20import {
21 ChartConfig,
22 ChartContainer,
23 ChartTooltip,
24 ChartTooltipContent,
25 ChartLegend,
26 ChartLegendContent,
27} from "@/components/ui/chart";
28import { cn } from "@/lib/utils";
29
30const chartShellVariants = cva("rounded-lg border border-border", {
31 variants: {
32 surface: {
33 card: "bg-card",
34 muted: "bg-muted/40",
35 },
36 padding: {
37 sm: "p-3",
38 md: "p-4",
39 lg: "p-5",
40 },
41 },
42 defaultVariants: {
43 surface: "card",
44 padding: "md",
45 },
46});
47
48const chartPlotVariants = cva("w-full", {
49 variants: {
50 plotSize: {
51 sm: "h-[160px]",
52 md: "h-[200px]",
53 lg: "h-[240px]",
54 },
55 },
56 defaultVariants: {
57 plotSize: "md",
58 },
59});
60
61const chartTitleBlockVariants = cva("", {
62 variants: {
63 spacing: {
64 default: "mb-4",
65 tight: "mb-2",
66 },
67 },
68 defaultVariants: {
69 spacing: "default",
70 },
71});
72
73const radialDialVariants = cva("w-full mx-auto", {
74 variants: {
75 dialSize: {
76 sm: "h-[120px]",
77 md: "h-[160px]",
78 lg: "h-[200px]",
79 },
80 },
81 defaultVariants: {
82 dialSize: "md",
83 },
84});
85
86const statsCardVariants = cva("rounded-lg border border-border bg-card", {
87 variants: {
88 padding: {
89 sm: "p-3",
90 md: "p-4",
91 lg: "p-5",
92 },
93 },
94 defaultVariants: {
95 padding: "md",
96 },
97});
98
99type ChartShellProps = VariantProps<typeof chartShellVariants>;
100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;
101
102interface BaseChartProps extends ChartShellProps {
103 className?: string;
104 title?: string;
105 description?: string;
106}
107
108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}
109
110interface AreaChartData {
111 label: string;
112 [key: string]: string | number;
113}
114
115interface CustomAreaChartProps extends CartesianChartProps {
116 data: AreaChartData[];
117 dataKeys: { key: string; label: string; color: string }[];
118 stacked?: boolean;
119 showGrid?: boolean;
120 showLegend?: boolean;
121}
122
123const areaChartConfig = (
124 dataKeys: { key: string; label: string; color: string }[],
125): ChartConfig => {
126 return dataKeys.reduce((acc, { key, label, color }) => {
127 acc[key] = { label, color };
128 return acc;
129 }, {} as ChartConfig);
130};
131
132export function CustomAreaChart({
133 data,
134 dataKeys,
135 stacked = false,
136 showGrid = true,
137 showLegend = true,
138 className,
139 title,
140 description,
141 surface,
142 padding,
143 plotSize,
144}: CustomAreaChartProps) {
145 const config = areaChartConfig(dataKeys);
146
147 return (
148 <div className={cn(chartShellVariants({ surface, padding }), className)}>
149 {(title || description) && (
150 <div className={chartTitleBlockVariants({ spacing: "default" })}>
151 {title && (
152 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
153 )}
154 {description && (
155 <p className="text-xs text-muted-foreground mt-1">{description}</p>
156 )}
157 </div>
158 )}
159 <ChartContainer
160 config={config}
161 className={chartPlotVariants({ plotSize })}
162 >
163 <AreaChart data={data} margin={{ left: -20, right: 12 }}>
164 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
165 <XAxis
166 dataKey="label"
167 tickLine={false}
168 axisLine={false}
169 tickMargin={8}
170 fontSize={11}
171 />
172 <YAxis
173 tickLine={false}
174 axisLine={false}
175 tickMargin={8}
176 fontSize={11}
177 />
178 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
179 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
180 {dataKeys.map(({ key, color }) => (
181 <Area
182 key={key}
183 dataKey={key}
184 type="monotone"
185 fill={color}
186 fillOpacity={0.2}
187 stroke={color}
188 strokeWidth={2}
189 stackId={stacked ? "1" : undefined}
190 />
191 ))}
192 </AreaChart>
193 </ChartContainer>
194 </div>
195 );
196}
197
198interface BarChartData {
199 label: string;
200 [key: string]: string | number;
201}
202
203interface CustomBarChartProps extends CartesianChartProps {
204 data: BarChartData[];
205 dataKeys: { key: string; label: string; color: string }[];
206 horizontal?: boolean;
207 stacked?: boolean;
208 showGrid?: boolean;
209 showLegend?: boolean;
210 barRadius?: number;
211}
212
213export function CustomBarChart({
214 data,
215 dataKeys,
216 horizontal = false,
217 stacked = false,
218 showGrid = true,
219 showLegend = true,
220 barRadius = 4,
221 className,
222 title,
223 description,
224 surface,
225 padding,
226 plotSize,
227}: CustomBarChartProps) {
228 const config = areaChartConfig(dataKeys);
229
230 return (
231 <div className={cn(chartShellVariants({ surface, padding }), className)}>
232 {(title || description) && (
233 <div className={chartTitleBlockVariants({ spacing: "default" })}>
234 {title && (
235 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
236 )}
237 {description && (
238 <p className="text-xs text-muted-foreground mt-1">{description}</p>
239 )}
240 </div>
241 )}
242 <ChartContainer
243 config={config}
244 className={chartPlotVariants({ plotSize })}
245 >
246 <BarChart
247 data={data}
248 layout={horizontal ? "vertical" : "horizontal"}
249 margin={{ left: horizontal ? 0 : -20, right: 12 }}
250 >
251 {showGrid && (
252 <CartesianGrid
253 strokeDasharray="3 3"
254 horizontal={!horizontal}
255 vertical={horizontal}
256 />
257 )}
258 {horizontal ? (
259 <>
260 <YAxis
261 dataKey="label"
262 type="category"
263 tickLine={false}
264 axisLine={false}
265 tickMargin={8}
266 fontSize={11}
267 width={80}
268 />
269 <XAxis
270 type="number"
271 tickLine={false}
272 axisLine={false}
273 fontSize={11}
274 />
275 </>
276 ) : (
277 <>
278 <XAxis
279 dataKey="label"
280 tickLine={false}
281 axisLine={false}
282 tickMargin={8}
283 fontSize={11}
284 />
285 <YAxis
286 tickLine={false}
287 axisLine={false}
288 tickMargin={8}
289 fontSize={11}
290 />
291 </>
292 )}
293 <ChartTooltip content={<ChartTooltipContent />} />
294 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
295 {dataKeys.map(({ key, color }) => (
296 <Bar
297 key={key}
298 dataKey={key}
299 fill={color}
300 radius={barRadius}
301 stackId={stacked ? "1" : undefined}
302 />
303 ))}
304 </BarChart>
305 </ChartContainer>
306 </div>
307 );
308}
309
310interface LineChartData {
311 label: string;
312 [key: string]: string | number;
313}
314
315interface CustomLineChartProps extends CartesianChartProps {
316 data: LineChartData[];
317 dataKeys: { key: string; label: string; color: string }[];
318 showGrid?: boolean;
319 showLegend?: boolean;
320 showDots?: boolean;
321 curved?: boolean;
322}
323
324export function CustomLineChart({
325 data,
326 dataKeys,
327 showGrid = true,
328 showLegend = true,
329 showDots = true,
330 curved = true,
331 className,
332 title,
333 description,
334 surface,
335 padding,
336 plotSize,
337}: CustomLineChartProps) {
338 const config = areaChartConfig(dataKeys);
339
340 return (
341 <div className={cn(chartShellVariants({ surface, padding }), className)}>
342 {(title || description) && (
343 <div className={chartTitleBlockVariants({ spacing: "default" })}>
344 {title && (
345 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
346 )}
347 {description && (
348 <p className="text-xs text-muted-foreground mt-1">{description}</p>
349 )}
350 </div>
351 )}
352 <ChartContainer
353 config={config}
354 className={chartPlotVariants({ plotSize })}
355 >
356 <LineChart data={data} margin={{ left: -20, right: 12 }}>
357 {showGrid && <CartesianGrid strokeDasharray="3 3" vertical={false} />}
358 <XAxis
359 dataKey="label"
360 tickLine={false}
361 axisLine={false}
362 tickMargin={8}
363 fontSize={11}
364 />
365 <YAxis
366 tickLine={false}
367 axisLine={false}
368 tickMargin={8}
369 fontSize={11}
370 />
371 <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
372 {showLegend && <ChartLegend content={<ChartLegendContent />} />}
373 {dataKeys.map(({ key, color }) => (
374 <Line
375 key={key}
376 dataKey={key}
377 type={curved ? "monotone" : "linear"}
378 stroke={color}
379 strokeWidth={2}
380 dot={showDots ? { fill: color, strokeWidth: 0, r: 3 } : false}
381 activeDot={{ r: 5, strokeWidth: 0 }}
382 />
383 ))}
384 </LineChart>
385 </ChartContainer>
386 </div>
387 );
388}
389
390interface PieChartData {
391 name: string;
392 value: number;
393 color: string;
394}
395
396interface CustomPieChartProps extends CartesianChartProps {
397 data: PieChartData[];
398 showLegend?: boolean;
399 innerRadius?: number;
400 paddingAngle?: number;
401}
402
403export function CustomPieChart({
404 data,
405 showLegend = true,
406 innerRadius = 0,
407 paddingAngle = 2,
408 className,
409 title,
410 description,
411 surface,
412 padding,
413 plotSize,
414}: CustomPieChartProps) {
415 const config: ChartConfig = data.reduce((acc, item) => {
416 acc[item.name] = { label: item.name, color: item.color };
417 return acc;
418 }, {} as ChartConfig);
419
420 return (
421 <div className={cn(chartShellVariants({ surface, padding }), className)}>
422 {(title || description) && (
423 <div className={chartTitleBlockVariants({ spacing: "default" })}>
424 {title && (
425 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
426 )}
427 {description && (
428 <p className="text-xs text-muted-foreground mt-1">{description}</p>
429 )}
430 </div>
431 )}
432 <ChartContainer
433 config={config}
434 className={chartPlotVariants({ plotSize })}
435 >
436 <PieChart>
437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />
438 {showLegend && (
439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />
440 )}
441 <Pie
442 data={data}
443 dataKey="value"
444 nameKey="name"
445 cx="50%"
446 cy="50%"
447 innerRadius={innerRadius}
448 paddingAngle={paddingAngle}
449 >
450 {data.map((entry, index) => (
451 <Cell key={`cell-${index}`} fill={entry.color} />
452 ))}
453 </Pie>
454 </PieChart>
455 </ChartContainer>
456 </div>
457 );
458}
459
460interface RadialProgressProps extends BaseChartProps {
461 value: number;
462 maxValue?: number;
463 color?: string;
464 label?: string;
465 size?: "sm" | "md" | "lg";
466}
467
468export function RadialProgress({
469 value,
470 maxValue = 100,
471 color = "var(--chart-1)",
472 label,
473 size = "md",
474 className,
475 title,
476 description,
477 surface,
478 padding,
479}: RadialProgressProps) {
480 const percentage = Math.min((value / maxValue) * 100, 100);
481 const data = [{ name: "progress", value: percentage, fill: color }];
482
483 const config: ChartConfig = {
484 progress: { label: label || "Progress", color },
485 };
486
487 return (
488 <div className={cn(chartShellVariants({ surface, padding }), className)}>
489 {(title || description) && (
490 <div className={chartTitleBlockVariants({ spacing: "tight" })}>
491 {title && (
492 <h3 className="text-sm font-semibold text-foreground">{title}</h3>
493 )}
494 {description && (
495 <p className="text-xs text-muted-foreground mt-1">{description}</p>
496 )}
497 </div>
498 )}
499 <ChartContainer
500 config={config}
501 className={radialDialVariants({ dialSize: size })}
502 >
503 <RadialBarChart
504 data={data}
505 startAngle={90}
506 endAngle={90 - percentage * 3.6}
507 innerRadius="70%"
508 outerRadius="100%"
509 >
510 <RadialBar
511 dataKey="value"
512 background={{ fill: "var(--muted)" }}
513 cornerRadius={10}
514 />
515 <text
516 x="50%"
517 y="50%"
518 textAnchor="middle"
519 dominantBaseline="middle"
520 className="fill-foreground font-mono text-2xl font-bold"
521 >
522 {Math.round(percentage)}%
523 </text>
524 </RadialBarChart>
525 </ChartContainer>
526 {label && (
527 <p className="text-center text-xs text-muted-foreground mt-2">
528 {label}
529 </p>
530 )}
531 </div>
532 );
533}
534
535interface SparklineProps {
536 data: number[];
537 color?: string;
538 height?: number;
539 showArea?: boolean;
540 className?: string;
541}
542
543export function Sparkline({
544 data,
545 color = "var(--chart-1)",
546 height = 40,
547 showArea = true,
548 className,
549}: SparklineProps) {
550 const chartData = data.map((value, index) => ({ index, value }));
551
552 const config: ChartConfig = {
553 value: { label: "Value", color },
554 };
555
556 return (
557 <ChartContainer
558 config={config}
559 className={cn("w-full", className)}
560 style={{ height }}
561 >
562 <AreaChart
563 data={chartData}
564 margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
565 >
566 <defs>
567 <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
568 <stop offset="0%" stopColor={color} stopOpacity={0.3} />
569 <stop offset="100%" stopColor={color} stopOpacity={0} />
570 </linearGradient>
571 </defs>
572 {showArea && (
573 <Area
574 type="monotone"
575 dataKey="value"
576 stroke="transparent"
577 fill="url(#sparklineGradient)"
578 />
579 )}
580 <Line
581 type="monotone"
582 dataKey="value"
583 stroke={color}
584 strokeWidth={1.5}
585 dot={false}
586 />
587 </AreaChart>
588 </ChartContainer>
589 );
590}
591
592interface StatsCardProps extends VariantProps<typeof statsCardVariants> {
593 title: string;
594 value: string | number;
595 change?: { value: number; label: string };
596 sparklineData?: number[];
597 color?: string;
598 className?: string;
599}
600
601export function StatsCard({
602 title,
603 value,
604 change,
605 sparklineData,
606 color = "var(--chart-1)",
607 className,
608 padding,
609}: StatsCardProps) {
610 const isPositive = change && change.value >= 0;
611
612 return (
613 <div className={cn(statsCardVariants({ padding }), className)}>
614 <div className="flex items-start justify-between gap-4">
615 <div className="flex-1">
616 <p className="text-xs text-muted-foreground">{title}</p>
617 <p className="text-2xl font-bold font-mono text-foreground mt-1">
618 {value}
619 </p>
620 {change && (
621 <p
622 className={cn(
623 "text-xs mt-1 font-medium",
624 isPositive
625 ? "text-emerald-600 dark:text-emerald-400"
626 : "text-red-600 dark:text-red-400",
627 )}
628 >
629 {isPositive ? "+" : ""}
630 {change.value}% {change.label}
631 </p>
632 )}
633 </div>
634 {sparklineData && (
635 <div className="w-24">
636 <Sparkline data={sparklineData} color={color} height={48} />
637 </div>
638 )}
639 </div>
640 </div>
641 );
642}

Props

NameTypeDefaultDescription
surface (CVA)"card" | "muted"cardCard shell background on area, bar, line, pie, and radial charts
padding (CVA)"sm" | "md" | "lg"mdInner padding on chart cards and stats cards
plotSize (CVA)"sm" | "md" | "lg"mdFixed plot height for area, bar, line, and pie charts
CustomAreaChart / CustomBarChart / CustomLineChartcomponentsShared props: data, dataKeys, title, description, showGrid, showLegend, plus surface, padding, plotSize
CustomPieChartcomponentdata: { name, value, color }[], innerRadius, paddingAngle, surface, padding, plotSize
RadialProgresscomponentvalue, maxValue, color, label; size sm|md|lg (dial height); surface, padding on the outer card
Sparkline / StatsCardcomponentsSparkline: height, color. StatsCard: padding sm|md|lg (CVA), optional sparklineData