Display15
Form1
Playground3
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
charts.tsx
1"use client";23import { 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";2930const 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});4748const 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});6061const chartTitleBlockVariants = cva("", {62 variants: {63 spacing: {64 default: "mb-4",65 tight: "mb-2",66 },67 },68 defaultVariants: {69 spacing: "default",70 },71});7273const 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});8586const 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});9899type ChartShellProps = VariantProps<typeof chartShellVariants>;100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;101102interface BaseChartProps extends ChartShellProps {103 className?: string;104 title?: string;105 description?: string;106}107108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}109110interface AreaChartData {111 label: string;112 [key: string]: string | number;113}114115interface CustomAreaChartProps extends CartesianChartProps {116 data: AreaChartData[];117 dataKeys: { key: string; label: string; color: string }[];118 stacked?: boolean;119 showGrid?: boolean;120 showLegend?: boolean;121}122123const 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};131132export 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);146147 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 <ChartContainer160 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 <XAxis166 dataKey="label"167 tickLine={false}168 axisLine={false}169 tickMargin={8}170 fontSize={11}171 />172 <YAxis173 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 <Area182 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}197198interface BarChartData {199 label: string;200 [key: string]: string | number;201}202203interface 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}212213export 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);229230 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 <ChartContainer243 config={config}244 className={chartPlotVariants({ plotSize })}245 >246 <BarChart247 data={data}248 layout={horizontal ? "vertical" : "horizontal"}249 margin={{ left: horizontal ? 0 : -20, right: 12 }}250 >251 {showGrid && (252 <CartesianGrid253 strokeDasharray="3 3"254 horizontal={!horizontal}255 vertical={horizontal}256 />257 )}258 {horizontal ? (259 <>260 <YAxis261 dataKey="label"262 type="category"263 tickLine={false}264 axisLine={false}265 tickMargin={8}266 fontSize={11}267 width={80}268 />269 <XAxis270 type="number"271 tickLine={false}272 axisLine={false}273 fontSize={11}274 />275 </>276 ) : (277 <>278 <XAxis279 dataKey="label"280 tickLine={false}281 axisLine={false}282 tickMargin={8}283 fontSize={11}284 />285 <YAxis286 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 <Bar297 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}309310interface LineChartData {311 label: string;312 [key: string]: string | number;313}314315interface 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}323324export 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);339340 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 <ChartContainer353 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 <XAxis359 dataKey="label"360 tickLine={false}361 axisLine={false}362 tickMargin={8}363 fontSize={11}364 />365 <YAxis366 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 <Line375 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}389390interface PieChartData {391 name: string;392 value: number;393 color: string;394}395396interface CustomPieChartProps extends CartesianChartProps {397 data: PieChartData[];398 showLegend?: boolean;399 innerRadius?: number;400 paddingAngle?: number;401}402403export 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);419420 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 <ChartContainer433 config={config}434 className={chartPlotVariants({ plotSize })}435 >436 <PieChart>437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />438 {showLegend && (439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />440 )}441 <Pie442 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}459460interface RadialProgressProps extends BaseChartProps {461 value: number;462 maxValue?: number;463 color?: string;464 label?: string;465 size?: "sm" | "md" | "lg";466}467468export 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 }];482483 const config: ChartConfig = {484 progress: { label: label || "Progress", color },485 };486487 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 <ChartContainer500 config={config}501 className={radialDialVariants({ dialSize: size })}502 >503 <RadialBarChart504 data={data}505 startAngle={90}506 endAngle={90 - percentage * 3.6}507 innerRadius="70%"508 outerRadius="100%"509 >510 <RadialBar511 dataKey="value"512 background={{ fill: "var(--muted)" }}513 cornerRadius={10}514 />515 <text516 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}534535interface SparklineProps {536 data: number[];537 color?: string;538 height?: number;539 showArea?: boolean;540 className?: string;541}542543export 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 }));551552 const config: ChartConfig = {553 value: { label: "Value", color },554 };555556 return (557 <ChartContainer558 config={config}559 className={cn("w-full", className)}560 style={{ height }}561 >562 <AreaChart563 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 <Area574 type="monotone"575 dataKey="value"576 stroke="transparent"577 fill="url(#sparklineGradient)"578 />579 )}580 <Line581 type="monotone"582 dataKey="value"583 stroke={color}584 strokeWidth={1.5}585 dot={false}586 />587 </AreaChart>588 </ChartContainer>589 );590}591592interface 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}600601export 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;611612 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 <p622 className={cn(623 "text-xs mt-1 font-medium",624 isPositive625 ? "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
charts.tsx
1"use client";23import { 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";2930const 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});4748const 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});6061const chartTitleBlockVariants = cva("", {62 variants: {63 spacing: {64 default: "mb-4",65 tight: "mb-2",66 },67 },68 defaultVariants: {69 spacing: "default",70 },71});7273const 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});8586const 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});9899type ChartShellProps = VariantProps<typeof chartShellVariants>;100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;101102interface BaseChartProps extends ChartShellProps {103 className?: string;104 title?: string;105 description?: string;106}107108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}109110interface AreaChartData {111 label: string;112 [key: string]: string | number;113}114115interface CustomAreaChartProps extends CartesianChartProps {116 data: AreaChartData[];117 dataKeys: { key: string; label: string; color: string }[];118 stacked?: boolean;119 showGrid?: boolean;120 showLegend?: boolean;121}122123const 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};131132export 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);146147 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 <ChartContainer160 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 <XAxis166 dataKey="label"167 tickLine={false}168 axisLine={false}169 tickMargin={8}170 fontSize={11}171 />172 <YAxis173 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 <Area182 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}197198interface BarChartData {199 label: string;200 [key: string]: string | number;201}202203interface 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}212213export 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);229230 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 <ChartContainer243 config={config}244 className={chartPlotVariants({ plotSize })}245 >246 <BarChart247 data={data}248 layout={horizontal ? "vertical" : "horizontal"}249 margin={{ left: horizontal ? 0 : -20, right: 12 }}250 >251 {showGrid && (252 <CartesianGrid253 strokeDasharray="3 3"254 horizontal={!horizontal}255 vertical={horizontal}256 />257 )}258 {horizontal ? (259 <>260 <YAxis261 dataKey="label"262 type="category"263 tickLine={false}264 axisLine={false}265 tickMargin={8}266 fontSize={11}267 width={80}268 />269 <XAxis270 type="number"271 tickLine={false}272 axisLine={false}273 fontSize={11}274 />275 </>276 ) : (277 <>278 <XAxis279 dataKey="label"280 tickLine={false}281 axisLine={false}282 tickMargin={8}283 fontSize={11}284 />285 <YAxis286 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 <Bar297 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}309310interface LineChartData {311 label: string;312 [key: string]: string | number;313}314315interface 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}323324export 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);339340 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 <ChartContainer353 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 <XAxis359 dataKey="label"360 tickLine={false}361 axisLine={false}362 tickMargin={8}363 fontSize={11}364 />365 <YAxis366 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 <Line375 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}389390interface PieChartData {391 name: string;392 value: number;393 color: string;394}395396interface CustomPieChartProps extends CartesianChartProps {397 data: PieChartData[];398 showLegend?: boolean;399 innerRadius?: number;400 paddingAngle?: number;401}402403export 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);419420 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 <ChartContainer433 config={config}434 className={chartPlotVariants({ plotSize })}435 >436 <PieChart>437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />438 {showLegend && (439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />440 )}441 <Pie442 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}459460interface RadialProgressProps extends BaseChartProps {461 value: number;462 maxValue?: number;463 color?: string;464 label?: string;465 size?: "sm" | "md" | "lg";466}467468export 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 }];482483 const config: ChartConfig = {484 progress: { label: label || "Progress", color },485 };486487 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 <ChartContainer500 config={config}501 className={radialDialVariants({ dialSize: size })}502 >503 <RadialBarChart504 data={data}505 startAngle={90}506 endAngle={90 - percentage * 3.6}507 innerRadius="70%"508 outerRadius="100%"509 >510 <RadialBar511 dataKey="value"512 background={{ fill: "var(--muted)" }}513 cornerRadius={10}514 />515 <text516 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}534535interface SparklineProps {536 data: number[];537 color?: string;538 height?: number;539 showArea?: boolean;540 className?: string;541}542543export 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 }));551552 const config: ChartConfig = {553 value: { label: "Value", color },554 };555556 return (557 <ChartContainer558 config={config}559 className={cn("w-full", className)}560 style={{ height }}561 >562 <AreaChart563 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 <Area574 type="monotone"575 dataKey="value"576 stroke="transparent"577 fill="url(#sparklineGradient)"578 />579 )}580 <Line581 type="monotone"582 dataKey="value"583 stroke={color}584 strokeWidth={1.5}585 dot={false}586 />587 </AreaChart>588 </ChartContainer>589 );590}591592interface 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}600601export 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;611612 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 <p622 className={cn(623 "text-xs mt-1 font-medium",624 isPositive625 ? "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
charts.tsx
1"use client";23import { 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";2930const 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});4748const 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});6061const chartTitleBlockVariants = cva("", {62 variants: {63 spacing: {64 default: "mb-4",65 tight: "mb-2",66 },67 },68 defaultVariants: {69 spacing: "default",70 },71});7273const 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});8586const 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});9899type ChartShellProps = VariantProps<typeof chartShellVariants>;100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;101102interface BaseChartProps extends ChartShellProps {103 className?: string;104 title?: string;105 description?: string;106}107108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}109110interface AreaChartData {111 label: string;112 [key: string]: string | number;113}114115interface CustomAreaChartProps extends CartesianChartProps {116 data: AreaChartData[];117 dataKeys: { key: string; label: string; color: string }[];118 stacked?: boolean;119 showGrid?: boolean;120 showLegend?: boolean;121}122123const 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};131132export 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);146147 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 <ChartContainer160 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 <XAxis166 dataKey="label"167 tickLine={false}168 axisLine={false}169 tickMargin={8}170 fontSize={11}171 />172 <YAxis173 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 <Area182 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}197198interface BarChartData {199 label: string;200 [key: string]: string | number;201}202203interface 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}212213export 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);229230 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 <ChartContainer243 config={config}244 className={chartPlotVariants({ plotSize })}245 >246 <BarChart247 data={data}248 layout={horizontal ? "vertical" : "horizontal"}249 margin={{ left: horizontal ? 0 : -20, right: 12 }}250 >251 {showGrid && (252 <CartesianGrid253 strokeDasharray="3 3"254 horizontal={!horizontal}255 vertical={horizontal}256 />257 )}258 {horizontal ? (259 <>260 <YAxis261 dataKey="label"262 type="category"263 tickLine={false}264 axisLine={false}265 tickMargin={8}266 fontSize={11}267 width={80}268 />269 <XAxis270 type="number"271 tickLine={false}272 axisLine={false}273 fontSize={11}274 />275 </>276 ) : (277 <>278 <XAxis279 dataKey="label"280 tickLine={false}281 axisLine={false}282 tickMargin={8}283 fontSize={11}284 />285 <YAxis286 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 <Bar297 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}309310interface LineChartData {311 label: string;312 [key: string]: string | number;313}314315interface 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}323324export 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);339340 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 <ChartContainer353 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 <XAxis359 dataKey="label"360 tickLine={false}361 axisLine={false}362 tickMargin={8}363 fontSize={11}364 />365 <YAxis366 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 <Line375 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}389390interface PieChartData {391 name: string;392 value: number;393 color: string;394}395396interface CustomPieChartProps extends CartesianChartProps {397 data: PieChartData[];398 showLegend?: boolean;399 innerRadius?: number;400 paddingAngle?: number;401}402403export 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);419420 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 <ChartContainer433 config={config}434 className={chartPlotVariants({ plotSize })}435 >436 <PieChart>437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />438 {showLegend && (439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />440 )}441 <Pie442 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}459460interface RadialProgressProps extends BaseChartProps {461 value: number;462 maxValue?: number;463 color?: string;464 label?: string;465 size?: "sm" | "md" | "lg";466}467468export 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 }];482483 const config: ChartConfig = {484 progress: { label: label || "Progress", color },485 };486487 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 <ChartContainer500 config={config}501 className={radialDialVariants({ dialSize: size })}502 >503 <RadialBarChart504 data={data}505 startAngle={90}506 endAngle={90 - percentage * 3.6}507 innerRadius="70%"508 outerRadius="100%"509 >510 <RadialBar511 dataKey="value"512 background={{ fill: "var(--muted)" }}513 cornerRadius={10}514 />515 <text516 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}534535interface SparklineProps {536 data: number[];537 color?: string;538 height?: number;539 showArea?: boolean;540 className?: string;541}542543export 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 }));551552 const config: ChartConfig = {553 value: { label: "Value", color },554 };555556 return (557 <ChartContainer558 config={config}559 className={cn("w-full", className)}560 style={{ height }}561 >562 <AreaChart563 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 <Area574 type="monotone"575 dataKey="value"576 stroke="transparent"577 fill="url(#sparklineGradient)"578 />579 )}580 <Line581 type="monotone"582 dataKey="value"583 stroke={color}584 strokeWidth={1.5}585 dot={false}586 />587 </AreaChart>588 </ChartContainer>589 );590}591592interface 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}600601export 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;611612 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 <p622 className={cn(623 "text-xs mt-1 font-medium",624 isPositive625 ? "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)
charts.tsx
1"use client";23import { 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";2930const 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});4748const 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});6061const chartTitleBlockVariants = cva("", {62 variants: {63 spacing: {64 default: "mb-4",65 tight: "mb-2",66 },67 },68 defaultVariants: {69 spacing: "default",70 },71});7273const 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});8586const 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});9899type ChartShellProps = VariantProps<typeof chartShellVariants>;100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;101102interface BaseChartProps extends ChartShellProps {103 className?: string;104 title?: string;105 description?: string;106}107108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}109110interface AreaChartData {111 label: string;112 [key: string]: string | number;113}114115interface CustomAreaChartProps extends CartesianChartProps {116 data: AreaChartData[];117 dataKeys: { key: string; label: string; color: string }[];118 stacked?: boolean;119 showGrid?: boolean;120 showLegend?: boolean;121}122123const 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};131132export 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);146147 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 <ChartContainer160 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 <XAxis166 dataKey="label"167 tickLine={false}168 axisLine={false}169 tickMargin={8}170 fontSize={11}171 />172 <YAxis173 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 <Area182 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}197198interface BarChartData {199 label: string;200 [key: string]: string | number;201}202203interface 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}212213export 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);229230 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 <ChartContainer243 config={config}244 className={chartPlotVariants({ plotSize })}245 >246 <BarChart247 data={data}248 layout={horizontal ? "vertical" : "horizontal"}249 margin={{ left: horizontal ? 0 : -20, right: 12 }}250 >251 {showGrid && (252 <CartesianGrid253 strokeDasharray="3 3"254 horizontal={!horizontal}255 vertical={horizontal}256 />257 )}258 {horizontal ? (259 <>260 <YAxis261 dataKey="label"262 type="category"263 tickLine={false}264 axisLine={false}265 tickMargin={8}266 fontSize={11}267 width={80}268 />269 <XAxis270 type="number"271 tickLine={false}272 axisLine={false}273 fontSize={11}274 />275 </>276 ) : (277 <>278 <XAxis279 dataKey="label"280 tickLine={false}281 axisLine={false}282 tickMargin={8}283 fontSize={11}284 />285 <YAxis286 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 <Bar297 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}309310interface LineChartData {311 label: string;312 [key: string]: string | number;313}314315interface 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}323324export 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);339340 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 <ChartContainer353 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 <XAxis359 dataKey="label"360 tickLine={false}361 axisLine={false}362 tickMargin={8}363 fontSize={11}364 />365 <YAxis366 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 <Line375 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}389390interface PieChartData {391 name: string;392 value: number;393 color: string;394}395396interface CustomPieChartProps extends CartesianChartProps {397 data: PieChartData[];398 showLegend?: boolean;399 innerRadius?: number;400 paddingAngle?: number;401}402403export 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);419420 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 <ChartContainer433 config={config}434 className={chartPlotVariants({ plotSize })}435 >436 <PieChart>437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />438 {showLegend && (439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />440 )}441 <Pie442 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}459460interface RadialProgressProps extends BaseChartProps {461 value: number;462 maxValue?: number;463 color?: string;464 label?: string;465 size?: "sm" | "md" | "lg";466}467468export 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 }];482483 const config: ChartConfig = {484 progress: { label: label || "Progress", color },485 };486487 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 <ChartContainer500 config={config}501 className={radialDialVariants({ dialSize: size })}502 >503 <RadialBarChart504 data={data}505 startAngle={90}506 endAngle={90 - percentage * 3.6}507 innerRadius="70%"508 outerRadius="100%"509 >510 <RadialBar511 dataKey="value"512 background={{ fill: "var(--muted)" }}513 cornerRadius={10}514 />515 <text516 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}534535interface SparklineProps {536 data: number[];537 color?: string;538 height?: number;539 showArea?: boolean;540 className?: string;541}542543export 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 }));551552 const config: ChartConfig = {553 value: { label: "Value", color },554 };555556 return (557 <ChartContainer558 config={config}559 className={cn("w-full", className)}560 style={{ height }}561 >562 <AreaChart563 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 <Area574 type="monotone"575 dataKey="value"576 stroke="transparent"577 fill="url(#sparklineGradient)"578 />579 )}580 <Line581 type="monotone"582 dataKey="value"583 stroke={color}584 strokeWidth={1.5}585 dot={false}586 />587 </AreaChart>588 </ChartContainer>589 );590}591592interface 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}600601export 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;611612 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 <p622 className={cn(623 "text-xs mt-1 font-medium",624 isPositive625 ? "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
charts.tsx
1"use client";23import { 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";2930const 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});4748const 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});6061const chartTitleBlockVariants = cva("", {62 variants: {63 spacing: {64 default: "mb-4",65 tight: "mb-2",66 },67 },68 defaultVariants: {69 spacing: "default",70 },71});7273const 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});8586const 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});9899type ChartShellProps = VariantProps<typeof chartShellVariants>;100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;101102interface BaseChartProps extends ChartShellProps {103 className?: string;104 title?: string;105 description?: string;106}107108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}109110interface AreaChartData {111 label: string;112 [key: string]: string | number;113}114115interface CustomAreaChartProps extends CartesianChartProps {116 data: AreaChartData[];117 dataKeys: { key: string; label: string; color: string }[];118 stacked?: boolean;119 showGrid?: boolean;120 showLegend?: boolean;121}122123const 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};131132export 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);146147 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 <ChartContainer160 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 <XAxis166 dataKey="label"167 tickLine={false}168 axisLine={false}169 tickMargin={8}170 fontSize={11}171 />172 <YAxis173 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 <Area182 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}197198interface BarChartData {199 label: string;200 [key: string]: string | number;201}202203interface 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}212213export 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);229230 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 <ChartContainer243 config={config}244 className={chartPlotVariants({ plotSize })}245 >246 <BarChart247 data={data}248 layout={horizontal ? "vertical" : "horizontal"}249 margin={{ left: horizontal ? 0 : -20, right: 12 }}250 >251 {showGrid && (252 <CartesianGrid253 strokeDasharray="3 3"254 horizontal={!horizontal}255 vertical={horizontal}256 />257 )}258 {horizontal ? (259 <>260 <YAxis261 dataKey="label"262 type="category"263 tickLine={false}264 axisLine={false}265 tickMargin={8}266 fontSize={11}267 width={80}268 />269 <XAxis270 type="number"271 tickLine={false}272 axisLine={false}273 fontSize={11}274 />275 </>276 ) : (277 <>278 <XAxis279 dataKey="label"280 tickLine={false}281 axisLine={false}282 tickMargin={8}283 fontSize={11}284 />285 <YAxis286 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 <Bar297 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}309310interface LineChartData {311 label: string;312 [key: string]: string | number;313}314315interface 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}323324export 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);339340 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 <ChartContainer353 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 <XAxis359 dataKey="label"360 tickLine={false}361 axisLine={false}362 tickMargin={8}363 fontSize={11}364 />365 <YAxis366 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 <Line375 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}389390interface PieChartData {391 name: string;392 value: number;393 color: string;394}395396interface CustomPieChartProps extends CartesianChartProps {397 data: PieChartData[];398 showLegend?: boolean;399 innerRadius?: number;400 paddingAngle?: number;401}402403export 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);419420 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 <ChartContainer433 config={config}434 className={chartPlotVariants({ plotSize })}435 >436 <PieChart>437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />438 {showLegend && (439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />440 )}441 <Pie442 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}459460interface RadialProgressProps extends BaseChartProps {461 value: number;462 maxValue?: number;463 color?: string;464 label?: string;465 size?: "sm" | "md" | "lg";466}467468export 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 }];482483 const config: ChartConfig = {484 progress: { label: label || "Progress", color },485 };486487 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 <ChartContainer500 config={config}501 className={radialDialVariants({ dialSize: size })}502 >503 <RadialBarChart504 data={data}505 startAngle={90}506 endAngle={90 - percentage * 3.6}507 innerRadius="70%"508 outerRadius="100%"509 >510 <RadialBar511 dataKey="value"512 background={{ fill: "var(--muted)" }}513 cornerRadius={10}514 />515 <text516 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}534535interface SparklineProps {536 data: number[];537 color?: string;538 height?: number;539 showArea?: boolean;540 className?: string;541}542543export 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 }));551552 const config: ChartConfig = {553 value: { label: "Value", color },554 };555556 return (557 <ChartContainer558 config={config}559 className={cn("w-full", className)}560 style={{ height }}561 >562 <AreaChart563 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 <Area574 type="monotone"575 dataKey="value"576 stroke="transparent"577 fill="url(#sparklineGradient)"578 />579 )}580 <Line581 type="monotone"582 dataKey="value"583 stroke={color}584 strokeWidth={1.5}585 dot={false}586 />587 </AreaChart>588 </ChartContainer>589 );590}591592interface 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}600601export 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;611612 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 <p622 className={cn(623 "text-xs mt-1 font-medium",624 isPositive625 ? "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
charts.tsx
1"use client";23import { 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";2930const 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});4748const 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});6061const chartTitleBlockVariants = cva("", {62 variants: {63 spacing: {64 default: "mb-4",65 tight: "mb-2",66 },67 },68 defaultVariants: {69 spacing: "default",70 },71});7273const 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});8586const 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});9899type ChartShellProps = VariantProps<typeof chartShellVariants>;100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;101102interface BaseChartProps extends ChartShellProps {103 className?: string;104 title?: string;105 description?: string;106}107108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}109110interface AreaChartData {111 label: string;112 [key: string]: string | number;113}114115interface CustomAreaChartProps extends CartesianChartProps {116 data: AreaChartData[];117 dataKeys: { key: string; label: string; color: string }[];118 stacked?: boolean;119 showGrid?: boolean;120 showLegend?: boolean;121}122123const 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};131132export 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);146147 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 <ChartContainer160 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 <XAxis166 dataKey="label"167 tickLine={false}168 axisLine={false}169 tickMargin={8}170 fontSize={11}171 />172 <YAxis173 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 <Area182 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}197198interface BarChartData {199 label: string;200 [key: string]: string | number;201}202203interface 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}212213export 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);229230 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 <ChartContainer243 config={config}244 className={chartPlotVariants({ plotSize })}245 >246 <BarChart247 data={data}248 layout={horizontal ? "vertical" : "horizontal"}249 margin={{ left: horizontal ? 0 : -20, right: 12 }}250 >251 {showGrid && (252 <CartesianGrid253 strokeDasharray="3 3"254 horizontal={!horizontal}255 vertical={horizontal}256 />257 )}258 {horizontal ? (259 <>260 <YAxis261 dataKey="label"262 type="category"263 tickLine={false}264 axisLine={false}265 tickMargin={8}266 fontSize={11}267 width={80}268 />269 <XAxis270 type="number"271 tickLine={false}272 axisLine={false}273 fontSize={11}274 />275 </>276 ) : (277 <>278 <XAxis279 dataKey="label"280 tickLine={false}281 axisLine={false}282 tickMargin={8}283 fontSize={11}284 />285 <YAxis286 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 <Bar297 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}309310interface LineChartData {311 label: string;312 [key: string]: string | number;313}314315interface 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}323324export 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);339340 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 <ChartContainer353 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 <XAxis359 dataKey="label"360 tickLine={false}361 axisLine={false}362 tickMargin={8}363 fontSize={11}364 />365 <YAxis366 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 <Line375 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}389390interface PieChartData {391 name: string;392 value: number;393 color: string;394}395396interface CustomPieChartProps extends CartesianChartProps {397 data: PieChartData[];398 showLegend?: boolean;399 innerRadius?: number;400 paddingAngle?: number;401}402403export 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);419420 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 <ChartContainer433 config={config}434 className={chartPlotVariants({ plotSize })}435 >436 <PieChart>437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />438 {showLegend && (439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />440 )}441 <Pie442 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}459460interface RadialProgressProps extends BaseChartProps {461 value: number;462 maxValue?: number;463 color?: string;464 label?: string;465 size?: "sm" | "md" | "lg";466}467468export 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 }];482483 const config: ChartConfig = {484 progress: { label: label || "Progress", color },485 };486487 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 <ChartContainer500 config={config}501 className={radialDialVariants({ dialSize: size })}502 >503 <RadialBarChart504 data={data}505 startAngle={90}506 endAngle={90 - percentage * 3.6}507 innerRadius="70%"508 outerRadius="100%"509 >510 <RadialBar511 dataKey="value"512 background={{ fill: "var(--muted)" }}513 cornerRadius={10}514 />515 <text516 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}534535interface SparklineProps {536 data: number[];537 color?: string;538 height?: number;539 showArea?: boolean;540 className?: string;541}542543export 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 }));551552 const config: ChartConfig = {553 value: { label: "Value", color },554 };555556 return (557 <ChartContainer558 config={config}559 className={cn("w-full", className)}560 style={{ height }}561 >562 <AreaChart563 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 <Area574 type="monotone"575 dataKey="value"576 stroke="transparent"577 fill="url(#sparklineGradient)"578 />579 )}580 <Line581 type="monotone"582 dataKey="value"583 stroke={color}584 strokeWidth={1.5}585 dot={false}586 />587 </AreaChart>588 </ChartContainer>589 );590}591592interface 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}600601export 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;611612 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 <p622 className={cn(623 "text-xs mt-1 font-medium",624 isPositive625 ? "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
charts.tsx
1"use client";23import { 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";2930const 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});4748const 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});6061const chartTitleBlockVariants = cva("", {62 variants: {63 spacing: {64 default: "mb-4",65 tight: "mb-2",66 },67 },68 defaultVariants: {69 spacing: "default",70 },71});7273const 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});8586const 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});9899type ChartShellProps = VariantProps<typeof chartShellVariants>;100type ChartPlotProps = VariantProps<typeof chartPlotVariants>;101102interface BaseChartProps extends ChartShellProps {103 className?: string;104 title?: string;105 description?: string;106}107108interface CartesianChartProps extends BaseChartProps, ChartPlotProps {}109110interface AreaChartData {111 label: string;112 [key: string]: string | number;113}114115interface CustomAreaChartProps extends CartesianChartProps {116 data: AreaChartData[];117 dataKeys: { key: string; label: string; color: string }[];118 stacked?: boolean;119 showGrid?: boolean;120 showLegend?: boolean;121}122123const 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};131132export 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);146147 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 <ChartContainer160 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 <XAxis166 dataKey="label"167 tickLine={false}168 axisLine={false}169 tickMargin={8}170 fontSize={11}171 />172 <YAxis173 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 <Area182 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}197198interface BarChartData {199 label: string;200 [key: string]: string | number;201}202203interface 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}212213export 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);229230 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 <ChartContainer243 config={config}244 className={chartPlotVariants({ plotSize })}245 >246 <BarChart247 data={data}248 layout={horizontal ? "vertical" : "horizontal"}249 margin={{ left: horizontal ? 0 : -20, right: 12 }}250 >251 {showGrid && (252 <CartesianGrid253 strokeDasharray="3 3"254 horizontal={!horizontal}255 vertical={horizontal}256 />257 )}258 {horizontal ? (259 <>260 <YAxis261 dataKey="label"262 type="category"263 tickLine={false}264 axisLine={false}265 tickMargin={8}266 fontSize={11}267 width={80}268 />269 <XAxis270 type="number"271 tickLine={false}272 axisLine={false}273 fontSize={11}274 />275 </>276 ) : (277 <>278 <XAxis279 dataKey="label"280 tickLine={false}281 axisLine={false}282 tickMargin={8}283 fontSize={11}284 />285 <YAxis286 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 <Bar297 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}309310interface LineChartData {311 label: string;312 [key: string]: string | number;313}314315interface 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}323324export 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);339340 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 <ChartContainer353 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 <XAxis359 dataKey="label"360 tickLine={false}361 axisLine={false}362 tickMargin={8}363 fontSize={11}364 />365 <YAxis366 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 <Line375 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}389390interface PieChartData {391 name: string;392 value: number;393 color: string;394}395396interface CustomPieChartProps extends CartesianChartProps {397 data: PieChartData[];398 showLegend?: boolean;399 innerRadius?: number;400 paddingAngle?: number;401}402403export 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);419420 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 <ChartContainer433 config={config}434 className={chartPlotVariants({ plotSize })}435 >436 <PieChart>437 <ChartTooltip content={<ChartTooltipContent hideLabel />} />438 {showLegend && (439 <ChartLegend content={<ChartLegendContent nameKey="name" />} />440 )}441 <Pie442 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}459460interface RadialProgressProps extends BaseChartProps {461 value: number;462 maxValue?: number;463 color?: string;464 label?: string;465 size?: "sm" | "md" | "lg";466}467468export 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 }];482483 const config: ChartConfig = {484 progress: { label: label || "Progress", color },485 };486487 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 <ChartContainer500 config={config}501 className={radialDialVariants({ dialSize: size })}502 >503 <RadialBarChart504 data={data}505 startAngle={90}506 endAngle={90 - percentage * 3.6}507 innerRadius="70%"508 outerRadius="100%"509 >510 <RadialBar511 dataKey="value"512 background={{ fill: "var(--muted)" }}513 cornerRadius={10}514 />515 <text516 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}534535interface SparklineProps {536 data: number[];537 color?: string;538 height?: number;539 showArea?: boolean;540 className?: string;541}542543export 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 }));551552 const config: ChartConfig = {553 value: { label: "Value", color },554 };555556 return (557 <ChartContainer558 config={config}559 className={cn("w-full", className)}560 style={{ height }}561 >562 <AreaChart563 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 <Area574 type="monotone"575 dataKey="value"576 stroke="transparent"577 fill="url(#sparklineGradient)"578 />579 )}580 <Line581 type="monotone"582 dataKey="value"583 stroke={color}584 strokeWidth={1.5}585 dot={false}586 />587 </AreaChart>588 </ChartContainer>589 );590}591592interface 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}600601export 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;611612 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 <p622 className={cn(623 "text-xs mt-1 font-medium",624 isPositive625 ? "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
| Name | Type | Default | Description |
|---|---|---|---|
| surface (CVA) | "card" | "muted" | card | Card shell background on area, bar, line, pie, and radial charts |
| padding (CVA) | "sm" | "md" | "lg" | md | Inner padding on chart cards and stats cards |
| plotSize (CVA) | "sm" | "md" | "lg" | md | Fixed plot height for area, bar, line, and pie charts |
| CustomAreaChart / CustomBarChart / CustomLineChart | components | — | Shared props: data, dataKeys, title, description, showGrid, showLegend, plus surface, padding, plotSize |
| CustomPieChart | component | — | data: { name, value, color }[], innerRadius, paddingAngle, surface, padding, plotSize |
| RadialProgress | component | — | value, maxValue, color, label; size sm|md|lg (dial height); surface, padding on the outer card |
| Sparkline / StatsCard | components | — | Sparkline: height, color. StatsCard: padding sm|md|lg (CVA), optional sparklineData |