Area Chart
Area chart family — natural, linear, step, stacked, expanded, legend, icons, gradient, axes, and an interactive range.
Preview
import { AreaChart } from "@/components/ui/AreaChart";
const variants = [
"default",
"linear",
"step",
"stacked",
"expanded",
"legend",
"icons",
"gradient",
"axes",
"interactive",
] as const;
export function AreaChartDemo() {
return (
<div className="flex w-full flex-col gap-2">
{variants.map((variant) => (
<AreaChart key={variant} variant={variant} />
))}
</div>
);
}Installation
Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces
pnpm dlx shadcn@latest add @aura/area-chartManual
Install the following dependencies:
pnpm install @radix-ui/react-icons recharts@2.15.4Copy and paste the card component into your components/ui/Card.tsx file.
/**
* @description Displays a card with header, content, and footer sections.
*/
import * as React from "react";
import { cn } from "@/utils/class-names";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-gray-2 border border-gray-6 rounded-md overflow-hidden",
className
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn("flex flex-col gap-0.5 p-1 pb-0", className)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
return (
<h3
data-slot="card-title"
className={cn("text-gray-12 font-semibold leading-none", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="card-description"
className={cn("text-gray-11", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn("flex items-center gap-0.5", className)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="card-content" className={cn("p-1 pt-0", className)} {...props} />
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center gap-0.5 p-1 border-t border-gray-6",
className
)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};
Copy and paste the chart component into your components/ui/Chart.tsx file.
"use client";
/**
* @description Chart container, tooltip, and legend for Recharts, colored with Aura series tokens.
*/
import * as React from "react";
import {
Area,
Legend,
ResponsiveContainer,
Tooltip,
} from "recharts";
import { cn } from "@/utils/class-names";
const THEMES = { light: "", dark: ".dark" } as const;
type SeriesColor = {
color?: string;
theme?: never;
} | {
color?: never;
theme: Partial<Record<keyof typeof THEMES, string>>;
};
export type ChartConfig = {
[key: string]: {
label?: React.ReactNode;
icon?: React.ComponentType<{ className?: string }>;
} & SeriesColor;
};
type ChartTooltipItem = {
type?: string;
dataKey?: string | number;
name?: string;
value?: number | string;
color?: string;
payload?: Record<string, unknown> & { fill?: string };
fill?: string;
};
type ChartContextValue = {
config: ChartConfig;
reducedMotion: boolean;
};
const ChartContext = React.createContext<ChartContextValue | null>(null);
function usePrefersReducedMotion() {
return React.useSyncExternalStore(
(onStoreChange) => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
media.addEventListener("change", onStoreChange);
return () => media.removeEventListener("change", onStoreChange);
},
() => window.matchMedia("(prefers-reduced-motion: reduce)").matches,
() => false
);
}
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
function tokenColor(color: string | undefined) {
if (!color) return undefined;
const value = color.trim();
if (/^var\(--[a-zA-Z0-9-_]+\)$/.test(value)) return value;
return undefined;
}
function ChartContainer({
id,
className,
children,
config,
label,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<typeof ResponsiveContainer>["children"];
label?: string;
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
const reducedMotion = usePrefersReducedMotion();
return (
<ChartContext.Provider value={{ config, reducedMotion }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"flex aspect-video w-full justify-center text-xs text-gray-11",
className
)}
aria-label={label}
{...props}
>
<ChartStyle id={chartId} config={config} />
<ResponsiveContainer>{children}</ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
function ChartStyle({ id, config }: { id: string; config: ChartConfig }) {
const colorConfig = Object.entries(config).filter(
([, item]) => item.theme || item.color
);
if (!colorConfig.length) return null;
const css = Object.entries(THEMES)
.map(([theme, prefix]) => {
const lines = colorConfig
.map(([key, item]) => {
const color =
tokenColor(
item.theme?.[theme as keyof typeof item.theme] || item.color
) ?? undefined;
return color ? ` --color-${key}: ${color};` : null;
})
.filter(Boolean)
.join("\n");
if (!lines) return "";
return `${prefix} [data-chart=${id}] {\n${lines}\n}`;
})
.filter(Boolean)
.join("\n");
if (!css) return null;
return <style dangerouslySetInnerHTML={{ __html: css }} />;
}
const ChartTooltip = Tooltip;
function formatChartValue(value: number | string | undefined) {
if (value == null || value === "") return null;
const numeric = typeof value === "number" ? value : Number(value);
if (Number.isFinite(numeric)) return numeric.toLocaleString();
return String(value);
}
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: {
active?: boolean;
payload?: ChartTooltipItem[];
className?: string;
indicator?: "line" | "dot" | "dashed";
hideLabel?: boolean;
hideIndicator?: boolean;
label?: React.ReactNode;
labelFormatter?: (
value: React.ReactNode,
payload: ChartTooltipItem[]
) => React.ReactNode;
labelClassName?: string;
formatter?: (
value: number | string,
name: string,
item: ChartTooltipItem,
index: number,
payload: Record<string, unknown>
) => React.ReactNode;
color?: string;
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) return null;
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium text-gray-12", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) return null;
return <div className={cn("font-medium text-gray-12", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) return null;
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"grid min-w-10 items-start gap-0.5 rounded-sm border border-gray-6 bg-gray-1 px-1 py-0.5 text-xs text-gray-12",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-0.5">
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload?.fill || item.color;
const Icon = itemConfig?.icon;
const formatted = formatChartValue(item.value);
return (
<div
key={String(item.dataKey)}
className={cn(
"flex w-full items-center gap-0.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item.value != null && item.name ? (
formatter(
item.value,
item.name,
item,
index,
item.payload ?? {}
)
) : (
<>
{Icon ? (
<Icon className="icon" />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-sm",
indicator === "dot" && "size-0.5",
indicator === "line" && "h-1 w-px",
indicator === "dashed" &&
"h-1 w-0 border border-dashed bg-transparent"
)}
style={
{
backgroundColor:
indicator === "dashed" ? "transparent" : indicatorColor,
borderColor: indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between gap-1 leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-0.5">
{nestLabel ? tooltipLabel : null}
<span className="text-gray-11">
{itemConfig?.label || item.name}
</span>
</div>
{formatted ? (
<span className="font-mono font-medium tabular-nums text-gray-12">
{formatted}
</span>
) : null}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> & {
hideIcon?: boolean;
payload?: Array<{
type?: string;
dataKey?: string | number;
value?: string;
color?: string;
}>;
verticalAlign?: "top" | "bottom" | "middle";
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) return null;
return (
<div
className={cn(
"flex items-center justify-center gap-1 text-xs text-gray-12",
verticalAlign === "top" ? "pb-1" : "pt-1",
className
)}
>
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const Icon = itemConfig?.icon;
const swatch =
item.color && !String(item.color).startsWith("url")
? item.color
: `var(--color-${key})`;
return (
<div key={String(item.value)} className="flex items-center gap-0.5">
{Icon && !hideIcon ? (
<Icon className="icon" />
) : (
<div
className="size-0.5 shrink-0 rounded-sm"
style={{ backgroundColor: swatch }}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
function ChartArea({
isAnimationActive,
animationDuration = 250,
animationEasing = "ease-out",
...props
}: Omit<React.ComponentProps<typeof Area>, "ref">) {
const { reducedMotion } = useChart();
return (
<Area
{...props}
animationDuration={animationDuration}
animationEasing={animationEasing}
isAnimationActive={isAnimationActive ?? !reducedMotion}
/>
);
}
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) return undefined;
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key];
}
export {
ChartArea,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartStyle,
ChartTooltip,
ChartTooltipContent,
useChart,
};
Copy and paste the class names utility into your utils/class-names.ts file.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy and paste the select component into your components/ui/Select.tsx file.
"use client";
/**
* @description Displays a list of options for the user to pick from—triggered by a button.
*/
import * as React from "react";
import { Select as SelectRadix } from "radix-ui";
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons";
import { cn } from "@/utils/class-names";
function Select({ ...props }: React.ComponentProps<typeof SelectRadix.Root>) {
return <SelectRadix.Root data-slot="select" {...props} />;
}
function SelectTrigger({
className,
...props
}: React.ComponentProps<typeof SelectRadix.Trigger>) {
return (
<SelectRadix.Trigger
data-slot="select-trigger"
className={cn(
"flex h-4 w-full items-center justify-between rounded-sm border border-gray-a6 bg-gray-2 px-2 text-gray-12 cursor-pointer focus:outline-2 focus:outline-accent-9 focus:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
);
}
function SelectValue({
className,
...props
}: React.ComponentProps<typeof SelectRadix.Value>) {
return (
<SelectRadix.Value
data-slot="select-value"
className={cn(className, "text-gray-12")}
{...props}
/>
);
}
function SelectIcon({
className,
...props
}: React.ComponentProps<typeof SelectRadix.Icon>) {
return (
<SelectRadix.Icon
data-slot="select-icon"
className={cn(className, "text-gray-11")}
{...props}
>
<ChevronDownIcon className="icon" />
</SelectRadix.Icon>
);
}
function SelectPortal({
...props
}: React.ComponentProps<typeof SelectRadix.Portal>) {
return <SelectRadix.Portal data-slot="select-portal" {...props} />;
}
function SelectContent({
className,
...props
}: React.ComponentProps<typeof SelectRadix.Content>) {
return (
<SelectRadix.Portal>
<SelectRadix.Content
data-slot="select-content"
collisionPadding={8}
className={cn(
className,
"relative z-50 max-h-96 min-w-10 origin-(--radix-select-content-transform-origin) overflow-hidden rounded-sm border border-gray-a6 bg-gray-1 shadow-md data-[state=open]:animate-popover-show data-[state=closed]:animate-popover-hide"
)}
{...props}
/>
</SelectRadix.Portal>
);
}
function SelectViewport({
className,
...props
}: React.ComponentProps<typeof SelectRadix.Viewport>) {
return (
<SelectRadix.Viewport
data-slot="select-viewport"
className={cn(className, "p-0")}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectRadix.Item>) {
return (
<SelectRadix.Item
data-slot="select-item"
className={cn(
className,
"relative flex w-full cursor-pointer select-none items-center p-0.5 px-2 text-gray-12 outline-none hover:bg-accent-3 focus:bg-accent-3 focus:text-accent-12 data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
)}
{...props}
>
{children}
</SelectRadix.Item>
);
}
function SelectItemText({
className,
...props
}: React.ComponentProps<typeof SelectRadix.ItemText>) {
return (
<SelectRadix.ItemText
data-slot="select-item-text"
className={cn(className)}
{...props}
/>
);
}
function SelectItemIndicator({
className,
...props
}: React.ComponentProps<typeof SelectRadix.ItemIndicator>) {
return (
<SelectRadix.ItemIndicator
data-slot="select-item-indicator"
className={cn(
className,
"absolute left-0.5 top-0 bottom-0 flex items-center justify-center"
)}
{...props}
>
<CheckIcon className="text-accent-9" />
</SelectRadix.ItemIndicator>
);
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectRadix.Group>) {
return <SelectRadix.Group data-slot="select-group" {...props} />;
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectRadix.Label>) {
return (
<SelectRadix.Label
data-slot="select-label"
className={cn(className, "p-0.5 px-2 text-gray-12")}
{...props}
/>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectRadix.Separator>) {
return (
<SelectRadix.Separator
data-slot="select-separator"
className={cn(className, "m-0.6 h-px bg-gray-a6")}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectRadix.ScrollUpButton>) {
return (
<SelectRadix.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
className,
"flex cursor-default items-center justify-center p-0.5"
)}
{...props}
>
<ChevronUpIcon className="icon text-gray-11" />
</SelectRadix.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectRadix.ScrollDownButton>) {
return (
<SelectRadix.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
className,
"flex cursor-default items-center justify-center p-0.5"
)}
{...props}
>
<ChevronDownIcon className="icon text-gray-11" />
</SelectRadix.ScrollDownButton>
);
}
function SelectArrow({
className,
...props
}: React.ComponentProps<typeof SelectRadix.Arrow>) {
return (
<SelectRadix.Arrow
data-slot="select-arrow"
className={cn(className, "fill-gray-1")}
{...props}
/>
);
}
export {
Select,
SelectPortal,
SelectTrigger,
SelectValue,
SelectIcon,
SelectContent,
SelectViewport,
SelectItem,
SelectItemText,
SelectItemIndicator,
SelectGroup,
SelectLabel,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
SelectArrow,
};
Copy and paste the AreaChart component into your components/ui/AreaChart.tsx file.
"use client";
/**
* @description Area chart family: natural, linear, step, stacked, expanded, legend, icons, gradient, axes, and interactive.
*/
import * as React from "react";
import {
ActivityLogIcon,
ArrowUpIcon,
DesktopIcon,
MobileIcon,
} from "@radix-ui/react-icons";
import { AreaChart as RechartsAreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/Card";
import {
ChartArea,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/Chart";
import {
Select,
SelectContent,
SelectIcon,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectTrigger,
SelectValue,
SelectViewport,
} from "@/components/ui/Select";
import { cn } from "@/utils/class-names";
export const areaChartVariants = [
"default",
"linear",
"step",
"stacked",
"expanded",
"legend",
"icons",
"gradient",
"axes",
"interactive",
] as const;
export type AreaChartVariant = (typeof areaChartVariants)[number];
const monthVisitors = [
{ month: "January", desktop: 186, mobile: 80, other: 45 },
{ month: "February", desktop: 305, mobile: 200, other: 100 },
{ month: "March", desktop: 237, mobile: 120, other: 150 },
{ month: "April", desktop: 73, mobile: 190, other: 50 },
{ month: "May", desktop: 209, mobile: 130, other: 100 },
{ month: "June", desktop: 214, mobile: 140, other: 160 },
];
const interactiveVisitors = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
]
const axisTick = { fontSize: 13, fill: "var(--gray-11)" };
const titles: Record<AreaChartVariant, string> = {
default: "Area chart",
linear: "Area chart — linear",
step: "Area chart — step",
stacked: "Area chart — stacked",
expanded: "Area chart — expanded",
legend: "Area chart — legend",
icons: "Area chart — icons",
gradient: "Area chart — gradient",
axes: "Area chart — axes",
interactive: "Area chart — interactive",
};
const curves = {
default: "natural",
linear: "linear",
step: "step",
stacked: "natural",
expanded: "natural",
legend: "natural",
icons: "natural",
gradient: "natural",
axes: "natural",
interactive: "natural",
} as const;
const rangeCopy = {
"90d": "the last 3 months",
"30d": "the last 30 days",
"7d": "the last 7 days",
} as const;
function seriesKeys(variant: AreaChartVariant) {
if (variant === "expanded") return ["other", "mobile", "desktop"] as const;
if (variant === "default" || variant === "linear" || variant === "step") {
return ["desktop"] as const;
}
return ["mobile", "desktop"] as const;
}
function buildConfig(variant: AreaChartVariant): ChartConfig {
return {
visitors: { label: "Visitors" },
desktop: {
label: "Desktop",
color: "var(--chart-1)",
icon:
variant === "icons"
? DesktopIcon
: variant === "step"
? ActivityLogIcon
: undefined,
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
icon: variant === "icons" ? MobileIcon : undefined,
},
other: {
label: "Other",
color: "var(--chart-3)",
},
};
}
function AreaChart({
variant = "default",
className,
}: {
variant?: AreaChartVariant;
className?: string;
}) {
const [timeRange, setTimeRange] = React.useState<keyof typeof rangeCopy>("90d");
const gradientId = React.useId().replace(/:/g, "");
const stacked = !["default", "linear", "step"].includes(variant);
const gradient = variant === "gradient" || variant === "interactive";
const keys = seriesKeys(variant);
const config = buildConfig(variant);
const title = titles[variant];
const plotted =
variant === "interactive"
? interactiveVisitors.filter((item) => {
const date = new Date(item.date);
const start = new Date("2024-06-30");
const days = timeRange === "30d" ? 30 : timeRange === "7d" ? 7 : 90;
start.setDate(start.getDate() - days);
return date >= start;
})
: monthVisitors;
const description =
variant === "interactive"
? `Showing total visitors for ${rangeCopy[timeRange]}`
: "Showing total visitors for the last 6 months";
return (
<Card className={cn("w-full", className)}>
<CardHeader>
{variant === "interactive" ? (
<div className="flex flex-wrap items-center justify-between gap-1">
<div className="grid gap-0.5">
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</div>
<div className="w-14">
<Select value={timeRange} onValueChange={(value) => setTimeRange(value as keyof typeof rangeCopy)}>
<SelectTrigger aria-label="Time range">
<SelectValue placeholder="Last 3 months" />
<SelectIcon />
</SelectTrigger>
<SelectContent>
<SelectViewport>
<SelectItem value="90d">
<SelectItemText>Last 3 months</SelectItemText>
<SelectItemIndicator />
</SelectItem>
<SelectItem value="30d">
<SelectItemText>Last 30 days</SelectItemText>
<SelectItemIndicator />
</SelectItem>
<SelectItem value="7d">
<SelectItemText>Last 7 days</SelectItemText>
<SelectItemIndicator />
</SelectItem>
</SelectViewport>
</SelectContent>
</Select>
</div>
</div>
) : (
<>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</>
)}
</CardHeader>
<CardContent>
<ChartContainer config={config} label={title}>
<RechartsAreaChart
accessibilityLayer
data={plotted}
margin={{ top: 13, right: 13, left: variant === "axes" ? 0 : 13, bottom: 0 }}
stackOffset={variant === "expanded" ? "expand" : "none"}
>
{gradient ? (
<defs>
{keys.map((key) => (
<linearGradient
key={key}
id={`${gradientId}-${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="5%" stopColor={`var(--color-${key})`} stopOpacity={0.8} />
<stop offset="95%" stopColor={`var(--color-${key})`} stopOpacity={0.1} />
</linearGradient>
))}
</defs>
) : null}
<CartesianGrid vertical={false} stroke="var(--gray-6)" strokeDasharray="0" />
<XAxis
dataKey={variant === "interactive" ? "date" : "month"}
tickLine={false}
axisLine={false}
tickMargin={13}
minTickGap={variant === "interactive" ? 26 : 0}
tick={axisTick}
tickFormatter={(value: string) => {
if (variant === "interactive") {
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}
return value.slice(0, 3);
}}
/>
{variant === "axes" ? (
<YAxis
tickLine={false}
axisLine={false}
tickMargin={13}
tickCount={3}
tick={axisTick}
/>
) : null}
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
indicator={
variant === "default" || variant === "expanded" ? "line" : "dot"
}
hideLabel={variant === "step"}
labelFormatter={
variant === "interactive"
? (value) =>
new Date(String(value)).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
: undefined
}
/>
}
/>
{keys.map((key) => (
<ChartArea
key={key}
dataKey={key}
type={curves[variant]}
fill={gradient ? `url(#${gradientId}-${key})` : `var(--color-${key})`}
fillOpacity={gradient ? 1 : key === "other" ? 0.1 : 0.4}
stroke={`var(--color-${key})`}
strokeWidth={2}
stackId={stacked ? "a" : undefined}
/>
))}
{variant === "legend" || variant === "icons" || variant === "interactive" ? (
<ChartLegend content={<ChartLegendContent />} />
) : null}
</RechartsAreaChart>
</ChartContainer>
</CardContent>
{variant === "interactive" ? null : (
<CardFooter>
<div className="grid gap-0.5">
<div className="flex items-center gap-0.5 font-medium text-sm text-gray-12">
Trending up by 5.2% this month
<ArrowUpIcon className="icon" />
</div>
<p className="text-sm text-gray-11">January – June 2024</p>
</div>
</CardFooter>
)}
</Card>
);
}
export { AreaChart };
Usage
Each variant is the same component. The preview above renders all of them. The sections below render one at a time.
Natural
import { AreaChart } from "@/components/ui/AreaChart";
export function Natural() {
return <AreaChart variant="default" />;
}Linear
import { AreaChart } from "@/components/ui/AreaChart";
export function Linear() {
return <AreaChart variant="linear" />;
}Step
import { AreaChart } from "@/components/ui/AreaChart";
export function Step() {
return <AreaChart variant="step" />;
}Stacked
import { AreaChart } from "@/components/ui/AreaChart";
export function Stacked() {
return <AreaChart variant="stacked" />;
}Expanded
import { AreaChart } from "@/components/ui/AreaChart";
export function Expanded() {
return <AreaChart variant="expanded" />;
}Legend
import { AreaChart } from "@/components/ui/AreaChart";
export function Legend() {
return <AreaChart variant="legend" />;
}Icons
import { AreaChart } from "@/components/ui/AreaChart";
export function Icons() {
return <AreaChart variant="icons" />;
}Gradient
import { AreaChart } from "@/components/ui/AreaChart";
export function Gradient() {
return <AreaChart variant="gradient" />;
}Axes
import { AreaChart } from "@/components/ui/AreaChart";
export function Axes() {
return <AreaChart variant="axes" />;
}Interactive
import { AreaChart } from "@/components/ui/AreaChart";
export function Interactive() {
return <AreaChart variant="interactive" />;
}