- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
Visits
13,250
+9.9%Sharp corners
Use shape="sharp" to connect points with straight segments.
Visits
13,250
+9.9%Installation
Set up the @cobalt registry first, as described in Installation, then add the block:
$ pnpm dlx shadcn@latest add @cobalt/line-chart-card-01
Supply week, month, and year arrays containing label, value, and previous values. The summary compares the selected points with their previous values.
Source
"use client";
import { cn } from "cn";
import * as React from "react";
import { Area, AreaChart, XAxis, YAxis } from "recharts";
import { useCountUp } from "@/components/hooks/use-count-up";
import { useReducedMotion } from "@/components/hooks/use-reduced-motion";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardAction,
CardContent,
CardHeader,
} from "@/components/ui/card";
import {
ChartActiveDot,
ChartContainer,
ChartTooltip,
} from "@/components/ui/chart";
import { Reveal } from "@/components/ui/reveal";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
export interface LineChartPoint {
label: string;
value: number;
previous: number;
}
type Period = "week" | "month" | "year";
const ACTIVE_DOT = <ChartActiveDot />;
const CHART_MARGIN = { top: 4, right: 8, bottom: 0, left: 0 };
const TICK = { fontSize: 12 };
const CURSOR = {
stroke: "var(--border-strong, var(--border))",
strokeWidth: 1,
strokeDasharray: "4 4",
};
const renderNothing = () => null;
const formatTick = (value: number) =>
value >= 1000 ? `${Number((value / 1000).toFixed(1))}k` : `${value}`;
/**
* The plot is memoised on purpose: hovering re-renders the card (the headline
* follows the hovered point), and re-rendering the chart with it would keep
* restarting Recharts' draw and morph animation.
*/
const LineChartPlot = React.memo(function LineChartPlot({
points,
shape,
gradientId,
step,
reduced,
onActiveIndexChange,
}: {
points: LineChartPoint[];
shape: "curved" | "sharp";
gradientId: string;
step: number;
reduced: boolean;
onActiveIndexChange: (index: number | null) => void;
}) {
const ticks = React.useMemo(() => [0, step, step * 2, step * 3], [step]);
const domain = React.useMemo<[number, number]>(() => [0, step * 3], [step]);
return (
<ChartContainer config={{}} className="aspect-auto min-h-0 w-full flex-1">
<AreaChart
data={points}
margin={CHART_MARGIN}
onMouseMove={(state) => {
const index = Number(state?.activeTooltipIndex);
if (state?.isTooltipActive && Number.isFinite(index)) {
onActiveIndexChange(index);
}
}}
onMouseLeave={() => onActiveIndexChange(null)}
>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--chart-1)" stopOpacity={0.35} />
<stop offset="100%" stopColor="var(--chart-1)" stopOpacity={0} />
</linearGradient>
</defs>
<YAxis
width={40}
domain={domain}
ticks={ticks}
tickFormatter={formatTick}
tickLine={false}
axisLine={false}
tick={TICK}
/>
<XAxis
dataKey="label"
tickLine={false}
axisLine={false}
tickMargin={10}
interval="preserveStartEnd"
tick={TICK}
/>
<ChartTooltip content={renderNothing} cursor={CURSOR} />
<Area
type={shape === "sharp" ? "linear" : "monotone"}
dataKey="value"
stroke="var(--chart-1)"
strokeWidth={2}
fill={`url(#${gradientId})`}
dot={false}
activeDot={ACTIVE_DOT}
isAnimationActive={!reduced}
animationDuration={450}
/>
</AreaChart>
</ChartContainer>
);
});
export function LineChartCard({
data,
title = "Visitors",
shape = "curved",
className,
}: {
data: Record<Period, LineChartPoint[]>;
title?: string;
shape?: "curved" | "sharp";
className?: string;
}) {
const [period, setPeriod] = React.useState<Period>("week");
const [activeIndex, setActiveIndex] = React.useState<number | null>(null);
const reduced = useReducedMotion();
const id = React.useId().replace(/:/g, "");
const points = data[period];
const point = activeIndex === null ? undefined : points[activeIndex];
const total =
point?.value ?? points.reduce((sum, item) => sum + item.value, 0);
const totalRef = useCountUp<HTMLParagraphElement>(total);
const previous =
point?.previous ?? points.reduce((sum, item) => sum + item.previous, 0);
const change = previous
? Math.round(((total - previous) / previous) * 1000) / 10
: null;
const step = React.useMemo(() => {
const max = Math.max(0, ...points.map((item) => item.value));
const rawStep = (max * 1.05) / 3 || 1;
const magnitude = 10 ** Math.floor(Math.log10(rawStep));
return (
([1, 2, 2.5, 5, 10].find((n) => n * magnitude >= rawStep) ?? 10) *
magnitude
);
}, [points]);
return (
<Reveal variant="card" className="w-full min-w-0">
<Card
variant="muted"
className={cn("h-[320px] min-w-0 w-full", className)}
aria-label={title}
>
<CardHeader>
<div className="flex flex-col gap-0.5">
<p className="text-sm leading-5 font-medium text-muted-foreground">
{point?.label ?? title}
</p>
<div className="flex items-center gap-2">
<p
ref={totalRef}
className="text-2xl leading-8 font-medium text-foreground tabular-nums"
>
{total.toLocaleString("en-US")}
</p>
{!point && (
<Badge
variant="caption"
tone={
change === null || change === 0
? "neutral"
: change > 0
? "lime"
: "rose"
}
>
{change === null
? "New"
: `${change > 0 ? "+" : ""}${change}%`}
</Badge>
)}
</div>
</div>
<CardAction>
<Tabs
value={period}
onValueChange={(value) => {
if (value === "week" || value === "month" || value === "year") {
setPeriod(value);
setActiveIndex(null);
}
}}
>
<TabsList indicator>
<TabsTrigger value="week">Weekly</TabsTrigger>
<TabsTrigger value="month">Monthly</TabsTrigger>
<TabsTrigger value="year">Yearly</TabsTrigger>
</TabsList>
</Tabs>
</CardAction>
</CardHeader>
<CardContent className="min-h-0 flex-1 gap-0">
<LineChartPlot
points={points}
shape={shape}
gradientId={id}
step={step}
reduced={reduced}
onActiveIndexChange={setActiveIndex}
/>
</CardContent>
</Card>
</Reveal>
);
} On this page