"use client" import * as React from "react" import { Area, AreaChart as RechartsAreaChart, Bar, BarChart as RechartsBarChart, CartesianGrid, Cell, Line, LineChart as RechartsLineChart, Pie, PieChart, ResponsiveContainer, Tooltip as RechartsTooltip, XAxis, YAxis, } from "recharts" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { StateView } from "@/components/feedback/state-view" import { cn } from "@/lib/utils" export type ChartDatum = { label: React.ReactNode value: number description?: React.ReactNode color?: string } export type ChartSeries = { key: string label: React.ReactNode data: number[] color?: string } export type ChartCurve = "linear" | "monotone" | "natural" | "step" export type ChartAxisLabel = string | number | React.ReactNode export type ChartSize = "sm" | "md" | "lg" export type ChartState = "ready" | "loading" | "empty" export type ChartDomain = [number | "auto" | "dataMin", number | "auto" | "dataMax"] const chartHeightBySize: Record = { sm: 120, md: 220, lg: 320 } function getChartColor(index: number, custom?: string) { return custom ?? `var(--color-chart-${(index % 5) + 1}, var(--primary))` } function toText(value: React.ReactNode, fallback: string) { return typeof value === "string" || typeof value === "number" ? String(value) : fallback } function formatChartValue(value: number, formatter?: (value: number) => React.ReactNode) { return formatter ? formatter(value) : value.toLocaleString() } function ChartStatus({ state, emptyLabel, height }: { state: ChartState; emptyLabel: React.ReactNode; height: number }) { if (state === "loading") { return } if (state === "empty") { return } return null } function ChartDataTable({ data, valueFormatter, caption }: { data: ChartDatum[]; valueFormatter?: (value: number) => React.ReactNode; caption: string }) { return (
{data.map((item, index) => )}
{caption}
LabelValueDescription
{item.label}{formatChartValue(item.value, valueFormatter)}{item.description}
) } function ChartSeriesDataTable({ labels, series, valueFormatter, caption }: { labels: ChartAxisLabel[]; series: ChartSeries[]; valueFormatter?: (value: number) => React.ReactNode; caption: string }) { return (
{series.map((item) => )}{labels.map((label, index) => {series.map((item) => )})}
{caption}
Label{item.label}
{label}{formatChartValue(item.data[index] ?? 0, valueFormatter)}
) } export type ChartTooltipContentProps = { active?: boolean label?: React.ReactNode payload?: ReadonlyArray<{ value?: number | string; color?: string; name?: string; payload?: { description?: React.ReactNode; originalLabel?: React.ReactNode } }> valueFormatter?: (value: number) => React.ReactNode labelFormatter?: (label: React.ReactNode) => React.ReactNode } function ChartTooltipContent({ active, label, payload, valueFormatter, labelFormatter }: ChartTooltipContentProps) { if (!active || !payload?.length) return null return (
{label !== undefined ?
{labelFormatter ? labelFormatter(label) : label}
: null}
{payload.map((entry, index) => { const numericValue = typeof entry.value === "number" ? entry.value : Number(entry.value ?? 0) return (
{entry.name ?? "Value"} {formatChartValue(numericValue, valueFormatter)}
) })}
) } export type ChartFrameProps = React.ComponentProps & { title?: React.ReactNode description?: React.ReactNode action?: React.ReactNode state?: ChartState emptyLabel?: React.ReactNode } function ChartFrame({ title, description, action, state = "ready", emptyLabel = "No data available.", className, children, ...props }: ChartFrameProps) { return ( {(title || description || action) ? (
{title ? {title} : null}{description ? {description} : null}
{action ?
{action}
: null}
) : null} {state === "ready" ? children : }
) } export type BarChartProps = React.ComponentProps<"div"> & { data?: ChartDatum[] series?: ChartSeries[] labels?: ChartAxisLabel[] size?: ChartSize max?: number showLabels?: boolean showValues?: boolean showGrid?: boolean showTooltip?: boolean valueFormatter?: (value: number) => React.ReactNode state?: ChartState emptyLabel?: React.ReactNode barClassName?: string domain?: ChartDomain ariaLabel?: string stacked?: boolean showLegend?: boolean barRadius?: number } function BarChart({ data = [], series, labels: customLabels, size = "md", max, showLabels = true, showValues = false, showGrid = true, showTooltip = true, valueFormatter, state = "ready", emptyLabel = "No chart data.", barClassName, domain, ariaLabel = "Bar chart", stacked = false, showLegend = Boolean(series?.length), barRadius = 6, className, ...props }: BarChartProps) { const height = chartHeightBySize[size] const activeSeries = series?.length ? series : [{ key: "value", label: "Value", data: data.map((item) => item.value) }] const itemCount = Math.max(data.length, ...activeSeries.map((item) => item.data.length)) const status = state === "ready" && itemCount === 0 ? "empty" : state if (status !== "ready") return const labels = Array.from({ length: itemCount }, (_, index) => customLabels?.[index] ?? data[index]?.label ?? `Item ${index + 1}`) const chartData = labels.map((label, index) => Object.fromEntries([ ["name", toText(label, `Item ${index + 1}`)], ["originalLabel", label], ...activeSeries.map((item) => [item.key, item.data[index] ?? 0]), ])) const resolvedDomain: ChartDomain = domain ?? (max !== undefined ? ["auto", max] : ["auto", "auto"]) return (
{showGrid ? : null} {showTooltip ? } /> : null} {activeSeries.map((item, seriesIndex) => ( String(formatChartValue(Number(value), valueFormatter)) } : false}> {!series?.length ? data.map((datum, index) => ) : null} ))}
{showLegend && activeSeries.length > 1 ? ({ label: item.label, value: 0, color: getChartColor(index, item.color) }))} /> : null} {series?.length ? : }
) } export type LineChartProps = Omit, "children"> & { values?: number[] series?: ChartSeries[] size?: ChartSize width?: number height?: number showArea?: boolean showGrid?: boolean showAxis?: boolean showTooltip?: boolean labels?: ChartAxisLabel[] valueFormatter?: (value: number) => React.ReactNode stroke?: string state?: ChartState emptyLabel?: React.ReactNode domain?: ChartDomain ariaLabel?: string curve?: ChartCurve showDots?: boolean showLegend?: boolean gradient?: boolean strokeWidth?: number } function LineChart({ values = [], series, size = "md", width = 560, height: heightProp, showArea = false, showGrid = true, showAxis = false, showTooltip = true, labels, valueFormatter, stroke = "var(--primary)", state = "ready", emptyLabel = "No line data.", domain = ["auto", "auto"], ariaLabel = showArea ? "Area chart" : "Line chart", curve = "monotone", showDots = !showArea, showLegend = Boolean(series?.length), gradient = showArea, strokeWidth = 2.5, className, style, ...props }: LineChartProps) { const height = heightProp ?? chartHeightBySize[size] const gradientId = React.useId().replace(/:/g, "") const activeSeries = series?.length ? series : [{ key: "value", label: "Value", data: values, color: stroke }] const itemCount = Math.max(...activeSeries.map((item) => item.data.length), 0) const status = state === "ready" && itemCount === 0 ? "empty" : state if (status !== "ready") return const resolvedLabels = Array.from({ length: itemCount }, (_, index) => labels?.[index] ?? String(index + 1)) const data = resolvedLabels.map((label, index) => Object.fromEntries([["name", toText(label, String(index + 1))], ["originalLabel", label], ...activeSeries.map((item) => [item.key, item.data[index] ?? 0])])) const Engine = showArea ? RechartsAreaChart : RechartsLineChart return (
{showArea && gradient ? {activeSeries.map((item, index) => { const color = getChartColor(index, item.color); return })} : null} {showGrid ? : null} {showTooltip ? } /> : null} {activeSeries.map((item, index) => { const color = getChartColor(index, item.color); return showArea ? : })}
{showLegend && activeSeries.length > 1 ? ({ label: item.label, value: 0, color: getChartColor(index, item.color) }))} /> : null}
) } function AreaChart(props: Omit) { return } export type SparklineProps = Omit & { values: number[]; positive?: boolean } function Sparkline({ values, positive = true, stroke, className, showTooltip = false, ...props }: SparklineProps) { return } export type DonutChartProps = Omit, "children"> & { data: ChartDatum[] size?: number strokeWidth?: number centerLabel?: React.ReactNode centerValue?: React.ReactNode state?: ChartState emptyLabel?: React.ReactNode showTooltip?: boolean valueFormatter?: (value: number) => React.ReactNode ariaLabel?: string } function DonutChart({ data, size = 180, strokeWidth = 18, centerLabel, centerValue, state = "ready", emptyLabel = "No distribution data.", showTooltip = true, valueFormatter, ariaLabel = "Donut chart", className, style, ...props }: DonutChartProps) { const status = state === "ready" && data.length === 0 ? "empty" : state if (status !== "ready") return const chartData = data.map((item, index) => ({ name: toText(item.label, `Item ${index + 1}`), value: Math.max(0, item.value), fill: getChartColor(index, item.color), description: item.description })) const radius = Math.max(8, size / 2 - 8) return (
{showTooltip ? } /> : null} {chartData.map((item) => )} {(centerValue || centerLabel) ?
{centerValue}
{centerLabel}
: null}
) } export type ChartLegendProps = React.ComponentProps<"div"> & { data: ChartDatum[] } function ChartLegend({ data, className, ...props }: ChartLegendProps) { return
{data.map((item, index) =>
{item.label}
)}
} export type MetricTrendProps = React.ComponentProps<"div"> & { label: React.ReactNode; value: React.ReactNode; change?: React.ReactNode; positive?: boolean; values?: number[] } function MetricTrend({ label, value, change, positive = true, values, className, ...props }: MetricTrendProps) { return
{label}
{value}
{change ?
{change}
: null}
{values?.length ? : null}
} export { AreaChart, BarChart, ChartFrame, ChartLegend, ChartTooltipContent, DonutChart, LineChart, MetricTrend, Sparkline }