- 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
Orders received
7,540
+16.5%6,470 last year
- This year
- Last year
Installation
Set up the @cobalt registry first, as described in Installation, then add the block:
$ pnpm dlx shadcn@latest add @cobalt/orders-chart-card-01
Pass monthly data with label, current, and previous values to the card.
Source
"use client";
import { cn } from "cn";
import * as React from "react";
import { Bar, BarChart, Tooltip, 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, CardContent, CardHeader } from "@/components/ui/card";
import { ChartContainer } from "@/components/ui/chart";
import { Reveal } from "@/components/ui/reveal";
export interface OrdersPoint {
label: string;
current: number;
previous: number;
}
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const fullMonths = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const CHART_MARGIN = { top: 4, right: 6, bottom: 0, left: 0 };
const Y_TICK = { fontSize: 12 };
const X_TICK = { fontSize: 13 };
const BAR_RADIUS: [number, number, number, number] = [4, 4, 0, 0];
const CURSOR = { fill: "var(--surface-strong, var(--muted))", opacity: 0.5 };
const renderNothing = () => null;
const formatTick = (v: number) =>
v >= 1000 ? `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}k` : `${v}`;
/**
* The plot is memoised on purpose: hovering re-renders the card (the headline
* follows the hovered month), and a re-render of the chart with fresh props
* would restart the bar growth.
*/
const OrdersPlot = React.memo(function OrdersPlot({
data,
step,
reduced,
onActiveIndexChange,
}: {
data: OrdersPoint[];
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">
<BarChart
data={data}
margin={CHART_MARGIN}
barCategoryGap="28%"
barGap={3}
onMouseMove={(state) => {
const index = Number(state?.activeTooltipIndex);
if (state?.isTooltipActive && Number.isFinite(index)) {
onActiveIndexChange(index);
}
}}
onMouseLeave={() => onActiveIndexChange(null)}
>
<YAxis
width={40}
domain={domain}
ticks={ticks}
tickFormatter={formatTick}
tickLine={false}
axisLine={false}
tick={Y_TICK}
/>
<XAxis
dataKey="label"
tickLine={false}
axisLine={false}
tickMargin={12}
interval="preserveStartEnd"
tick={X_TICK}
/>
<Tooltip content={renderNothing} cursor={CURSOR} />
<Bar
dataKey="previous"
fill="var(--orders-chart-previous, var(--muted-foreground))"
radius={BAR_RADIUS}
isAnimationActive={!reduced}
animationDuration={450}
/>
<Bar
dataKey="current"
fill="var(--orders-chart-current, var(--chart-1))"
radius={BAR_RADIUS}
isAnimationActive={!reduced}
animationDuration={450}
/>
</BarChart>
</ChartContainer>
);
});
export function OrdersChartCard({
data,
title = "Orders",
className,
}: {
data: OrdersPoint[];
title?: string;
className?: string;
}) {
const [activeIndex, setActiveIndex] = React.useState<number | null>(null);
const reduced = useReducedMotion();
const point =
activeIndex !== null && activeIndex >= 0 ? data[activeIndex] : undefined;
const current = point?.current ?? data.reduce((sum, p) => sum + p.current, 0);
const previous =
point?.previous ?? data.reduce((sum, p) => sum + p.previous, 0);
const change =
previous === 0
? null
: Math.round(((current - previous) / previous) * 1000) / 10;
const delta = change === null ? "New" : `${change > 0 ? "+" : ""}${change}%`;
const month = point ? months.indexOf(point.label) : -1;
const totalRef = useCountUp<HTMLParagraphElement>(current);
const step = React.useMemo(() => {
const max = Math.max(
0,
...data.map((p) => Math.max(p.current, p.previous))
);
// Round the axis to a tidy step so the top tick is never a long decimal.
const rawStep = (max * 1.05) / 3 || 1;
const magnitude = 10 ** Math.floor(Math.log10(rawStep));
return (
([1, 2, 2.5, 3, 4, 5, 10].find((n) => n * magnitude >= rawStep) ?? 10) *
magnitude
);
}, [data]);
return (
<Reveal
variant="card"
render={
<Card
radius={16}
variant="muted"
// oxlint-disable-next-line shadcn/no-restyle -- reference: 24px between header and plot, 12px bottom padding
className={cn("h-86 min-w-0 flex-1 gap-6 pb-3", className)}
/>
}
>
<CardHeader className="flex w-full flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex min-w-0 flex-col gap-0.5">
<p className="w-full text-sm leading-5 font-medium text-muted-foreground">
{point ? (month >= 0 ? fullMonths[month] : point.label) : title}
</p>
<div className="flex w-full items-center gap-2">
<p
ref={totalRef}
className="text-2xl leading-8.5 font-medium whitespace-nowrap tabular-nums"
>
{current.toLocaleString("en-US")}
</p>
<Badge
variant="label"
tone={
change === null || change === 0
? "neutral"
: change > 0
? "lime"
: "rose"
}
>
{delta}
</Badge>
</div>
{/* oxlint-disable-next-line shadcn/no-arbitrary-values -- reference: year-earlier line is 13px */}
<p className="text-[13px] leading-4.5 font-medium text-muted-foreground tabular-nums">
{previous.toLocaleString("en-US")}{" "}
{point ? "a year earlier" : "last year"}
</p>
</div>
{/* oxlint-disable-next-line shadcn/no-arbitrary-values -- reference: legend is 13px */}
<dl className="flex flex-wrap items-center gap-4 text-[13px] leading-4.5 font-medium text-muted-foreground">
<div className="flex items-center gap-1.5">
<span
className="size-2 rounded-full bg-(--orders-chart-current,var(--chart-1))"
aria-hidden
/>
<dt>This year</dt>
</div>
<div className="flex items-center gap-1.5">
<span
className="size-2 rounded-full bg-(--orders-chart-previous,var(--muted-foreground))"
aria-hidden
/>
<dt>Last year</dt>
</div>
</dl>
</CardHeader>
<CardContent className="min-h-0 flex-1 gap-0">
<OrdersPlot
data={data}
step={step}
reduced={reduced}
onActiveIndexChange={setActiveIndex}
/>
</CardContent>
</Reveal>
);
} On this page