Chart
Free, copy-and-go Chart components built on the SevenUI Chart primitive.Read the primitive docs.
"use client";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
const deployments = [
{ day: "Mon", deploys: 14 },
{ day: "Tue", deploys: 22 },
{ day: "Wed", deploys: 19 },
{ day: "Thu", deploys: 27 },
{ day: "Fri", deploys: 11 },
{ day: "Sat", deploys: 3 },
{ day: "Sun", deploys: 2 },
];
const chartConfig = {
deploys: { label: "Deploys", color: "var(--chart-1)" },
} satisfies ChartConfig;
const total = deployments.reduce((sum, item) => sum + item.deploys, 0);
export default function Chart01() {
return (
<figure
aria-labelledby="chart-01-title"
className="flex w-full max-w-md flex-col gap-3"
>
<figcaption className="flex items-baseline justify-between gap-4">
<span id="chart-01-title" className="text-sm font-medium">
Production deploys this week
</span>
<span className="text-sm text-muted-foreground tabular-nums">
{total} total
</span>
</figcaption>
<ChartContainer config={chartConfig} className="aspect-auto h-44 w-full">
<BarChart data={deployments} margin={{ left: 0, right: 0 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="day"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Bar dataKey="deploys" fill="var(--color-deploys)" radius={4} />
</BarChart>
</ChartContainer>
</figure>
);
}
npx shadcn@latest add @sevenui/component/chart-01pnpm dlx shadcn@latest add @sevenui/component/chart-01yarn dlx shadcn@latest add @sevenui/component/chart-01bunx --bun shadcn@latest add @sevenui/component/chart-01"use client";
import * as React from "react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
const signups = [
{ week: "Aug 4", signups: 412 },
{ week: "Aug 11", signups: 468 },
{ week: "Aug 18", signups: 431 },
{ week: "Aug 25", signups: 527 },
{ week: "Sep 1", signups: 590 },
{ week: "Sep 8", signups: 562 },
{ week: "Sep 15", signups: 648 },
];
const chartConfig = {
signups: { label: "Signups", color: "var(--chart-2)" },
} satisfies ChartConfig;
type FillStyle = "solid" | "subtle" | "outline";
// Each style only changes the area's paint; the data and axes stay identical.
const fillStyles: Record<
FillStyle,
{ label: string; fillOpacity: number; strokeWidth: number; dash?: string }
> = {
solid: { label: "Solid", fillOpacity: 0.85, strokeWidth: 0 },
subtle: { label: "Subtle", fillOpacity: 0.2, strokeWidth: 2 },
outline: { label: "Outline", fillOpacity: 0, strokeWidth: 2 },
};
export default function Chart02() {
const [style, setStyle] = React.useState<FillStyle>("subtle");
const paint = fillStyles[style];
return (
<Card className="w-full max-w-md">
<CardHeader className="max-sm:grid-cols-1!">
<CardTitle>Weekly signups</CardTitle>
<CardDescription>New workspaces created, last 7 weeks</CardDescription>
<CardAction className="max-sm:col-start-1 max-sm:row-span-1 max-sm:row-start-3 max-sm:mt-2 max-sm:justify-self-start">
<ToggleGroup
aria-label="Fill style"
variant="outline"
size="sm"
spacing={0}
value={[style]}
onValueChange={(next) => {
if (next.length > 0) setStyle(next[0] as FillStyle);
}}
>
{(Object.keys(fillStyles) as FillStyle[]).map((key) => (
<ToggleGroupItem key={key} value={key} className="px-2 text-xs">
{fillStyles[key].label}
</ToggleGroupItem>
))}
</ToggleGroup>
</CardAction>
</CardHeader>
<CardContent>
<ChartContainer
config={chartConfig}
role="img"
aria-label="Weekly signups from Aug 4 to Sep 15, rising from 412 to 648"
className="aspect-auto h-48 w-full"
>
<AreaChart data={signups} margin={{ left: 8, right: 8, top: 4 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="week"
tickLine={false}
axisLine={false}
tickMargin={8}
interval="preserveStartEnd"
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="line" />}
/>
<Area
dataKey="signups"
type="monotone"
fill="var(--color-signups)"
fillOpacity={paint.fillOpacity}
stroke="var(--color-signups)"
strokeWidth={paint.strokeWidth}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-02pnpm dlx shadcn@latest add @sevenui/component/chart-02yarn dlx shadcn@latest add @sevenui/component/chart-02bunx --bun shadcn@latest add @sevenui/component/chart-02"use client";
import * as React from "react";
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const traits = [
"Battery",
"Speed",
"Display",
"Portability",
"Ports",
"Value",
] as const;
// Editorial review scores out of 10, in the same order as `traits`.
const laptops = {
"orbit-14": { label: "Orbit 14", price: "$1,299", scores: [9, 7, 8, 9, 5, 7] },
"vantage-16": {
label: "Vantage 16 Pro",
price: "$2,149",
scores: [6, 10, 9, 4, 9, 5],
},
"slate-13": { label: "Slate 13 Air", price: "$999", scores: [8, 5, 7, 10, 3, 9] },
};
type LaptopKey = keyof typeof laptops;
const baseline: LaptopKey = "orbit-14";
const rivals = (Object.keys(laptops) as LaptopKey[])
.filter((key) => key !== baseline)
.map((key) => ({ value: key, label: laptops[key].label }));
export default function Chart03() {
const [rival, setRival] = React.useState<LaptopKey>("vantage-16");
const chartConfig = {
current: { label: laptops[baseline].label, color: "var(--chart-1)" },
rival: { label: laptops[rival].label, color: "var(--chart-2)" },
} satisfies ChartConfig;
const data = traits.map((trait, index) => ({
trait,
current: laptops[baseline].scores[index],
rival: laptops[rival].scores[index],
}));
const wins = data.filter((row) => row.current > row.rival).length;
const losses = data.filter((row) => row.current < row.rival).length;
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Compare laptops</CardTitle>
<CardDescription>
{laptops[baseline].label} ({laptops[baseline].price}) scores higher
in {wins} of {traits.length} areas and lower in {losses}.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<Select
items={rivals}
value={rival}
onValueChange={(value) => {
if (value) setRival(value as LaptopKey);
}}
>
<SelectTrigger aria-label="Compare with" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{rivals.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label} · {laptops[item.value].price}
</SelectItem>
))}
</SelectContent>
</Select>
<ChartContainer
config={chartConfig}
role="img"
aria-label={`Review scores out of 10. ${data
.map(
(row) =>
`${row.trait}: ${laptops[baseline].label} ${row.current}, ${laptops[rival].label} ${row.rival}`,
)
.join("; ")}`}
className="mx-auto aspect-square w-full max-w-72"
>
<RadarChart data={data} outerRadius="72%">
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="line" />}
/>
<PolarGrid />
<PolarAngleAxis
dataKey="trait"
tick={{ fill: "var(--muted-foreground)", fontSize: 11 }}
/>
<Radar
dataKey="rival"
fill="var(--color-rival)"
fillOpacity={0.15}
stroke="var(--color-rival)"
strokeWidth={1.5}
strokeDasharray="4 3"
/>
<Radar
dataKey="current"
fill="var(--color-current)"
fillOpacity={0.3}
stroke="var(--color-current)"
strokeWidth={2}
dot={{ r: 3, fillOpacity: 1 }}
/>
<ChartLegend content={<ChartLegendContent />} />
</RadarChart>
</ChartContainer>
</CardContent>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-03pnpm dlx shadcn@latest add @sevenui/component/chart-03yarn dlx shadcn@latest add @sevenui/component/chart-03bunx --bun shadcn@latest add @sevenui/component/chart-03Support tickets by channelOpened Monday to Friday, week 38
"use client";
import { MailIcon, MessageSquareIcon, PhoneIcon } from "lucide-react";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
const tickets = [
{ day: "Mon", email: 48, chat: 92, phone: 21 },
{ day: "Tue", email: 52, chat: 104, phone: 18 },
{ day: "Wed", email: 41, chat: 87, phone: 25 },
{ day: "Thu", email: 60, chat: 115, phone: 30 },
{ day: "Fri", email: 38, chat: 76, phone: 16 },
];
// An `icon` on a config entry replaces the color swatch in both the legend
// and the tooltip, so each channel reads by shape as well as by color. The
// icons are drawn in their series color so they still work as a key.
function EmailIcon() {
return <MailIcon className="stroke-(--color-email)" />;
}
function ChatIcon() {
return <MessageSquareIcon className="stroke-(--color-chat)" />;
}
function PhoneCallIcon() {
return <PhoneIcon className="stroke-(--color-phone)" />;
}
const chartConfig = {
email: { label: "Email", icon: EmailIcon, color: "var(--chart-1)" },
chat: { label: "Live chat", icon: ChatIcon, color: "var(--chart-2)" },
phone: { label: "Phone", icon: PhoneCallIcon, color: "var(--chart-3)" },
} satisfies ChartConfig;
export default function Chart04() {
return (
<div className="flex w-full max-w-md flex-col gap-3">
<div className="flex flex-col gap-0.5">
<span id="chart-04-title" className="text-sm font-medium">
Support tickets by channel
</span>
<span className="text-xs text-muted-foreground">
Opened Monday to Friday, week 38
</span>
</div>
<ChartContainer
config={chartConfig}
role="img"
aria-labelledby="chart-04-title"
className="aspect-auto h-60 w-full"
>
<BarChart data={tickets} margin={{ left: 0, right: 0 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="day"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar
dataKey="email"
stackId="channel"
fill="var(--color-email)"
radius={[0, 0, 4, 4]}
/>
<Bar dataKey="chat" stackId="channel" fill="var(--color-chat)" />
<Bar
dataKey="phone"
stackId="channel"
fill="var(--color-phone)"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ChartContainer>
</div>
);
}
npx shadcn@latest add @sevenui/component/chart-04pnpm dlx shadcn@latest add @sevenui/component/chart-04yarn dlx shadcn@latest add @sevenui/component/chart-04bunx --bun shadcn@latest add @sevenui/component/chart-04"use client";
import {
Bar,
BarChart,
LabelList,
type LabelProps,
XAxis,
YAxis,
} from "recharts";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
const pages = [
{ path: "/pricing", views: 18420 },
{ path: "/docs/installation", views: 14310 },
{ path: "/blog/migration", views: 9870 },
{ path: "/changelog", views: 7240 },
{ path: "/careers", views: 5615 },
];
const chartConfig = {
views: { label: "Page views", color: "var(--chart-1)" },
label: { color: "var(--background)" },
} satisfies ChartConfig;
const formatViews = (value: number) =>
value >= 1000 ? `${(value / 1000).toFixed(1)}k` : String(value);
// Rough advance width of a 12px medium glyph, used to decide whether a path
// fits inside its bar. Paths that do not fit move out beside the bar.
const CHAR_WIDTH = 7;
const INSET = 10;
function BarLabel({ viewBox, index }: LabelProps) {
const page = pages[index ?? 0];
if (!page || !viewBox || !("width" in viewBox)) return null;
const x = Number(viewBox.x ?? 0);
const y = Number(viewBox.y ?? 0);
const width = Number(viewBox.width ?? 0);
const height = Number(viewBox.height ?? 0);
const midY = y + height / 2;
const fitsInside = width >= page.path.length * CHAR_WIDTH + INSET * 2;
if (fitsInside) {
return (
<g>
<text
x={x + INSET}
y={midY}
dominantBaseline="central"
fontSize={12}
className="fill-(--color-label) font-medium"
>
{page.path}
</text>
<text
x={x + width + 8}
y={midY}
dominantBaseline="central"
fontSize={12}
className="fill-foreground tabular-nums"
>
{formatViews(page.views)}
</text>
</g>
);
}
return (
<text
x={x + width + 8}
y={midY}
dominantBaseline="central"
fontSize={12}
className="fill-foreground"
>
<tspan className="font-medium">{page.path}</tspan>
<tspan dx={8} className="tabular-nums">
{formatViews(page.views)}
</tspan>
</text>
);
}
export default function Chart05() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Top pages</CardTitle>
<CardDescription>Unique page views, last 30 days</CardDescription>
</CardHeader>
<CardContent>
{/* layout="vertical" turns the value axis horizontal; labels ride inside the bars when they fit. */}
<ChartContainer
config={chartConfig}
role="img"
aria-label="Top five pages by unique views, led by /pricing with 18.4k"
className="aspect-auto h-56 w-full"
>
<BarChart
data={pages}
layout="vertical"
margin={{ left: 0, right: 40 }}
barCategoryGap={6}
>
<YAxis dataKey="path" type="category" hide />
<XAxis dataKey="views" type="number" hide />
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="line" />}
/>
<Bar dataKey="views" fill="var(--color-views)" radius={6}>
<LabelList dataKey="path" content={BarLabel} />
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="text-xs text-muted-foreground">
Pricing overtook the docs landing page for the first time this quarter.
</CardFooter>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-05pnpm dlx shadcn@latest add @sevenui/component/chart-05yarn dlx shadcn@latest add @sevenui/component/chart-05bunx --bun shadcn@latest add @sevenui/component/chart-05"use client";
import * as React from "react";
import { ChartNoAxesColumnIcon, RefreshCwIcon, TriangleAlertIcon } from "lucide-react";
import { CartesianGrid, Line, LineChart, XAxis } from "recharts";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Skeleton } from "@/components/ui/skeleton";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
const orders = [
{ date: "Sep 16", orders: 128 },
{ date: "Sep 17", orders: 142 },
{ date: "Sep 18", orders: 119 },
{ date: "Sep 19", orders: 167 },
{ date: "Sep 20", orders: 181 },
{ date: "Sep 21", orders: 158 },
{ date: "Sep 22", orders: 203 },
];
const chartConfig = {
orders: { label: "Orders", color: "var(--chart-2)" },
} satisfies ChartConfig;
const states = [
{ value: "ready", label: "Ready" },
{ value: "loading", label: "Loading" },
{ value: "empty", label: "Empty" },
{ value: "error", label: "Error" },
] as const;
type ChartState = (typeof states)[number]["value"];
// Skeleton bar heights, as a share of the frame, hint at the shape to come.
const skeletonHeights = ["45%", "60%", "38%", "72%", "80%", "64%", "90%"];
function ChartBody({
state,
onRetry,
}: {
state: ChartState;
onRetry: () => void;
}) {
if (state === "loading") {
return (
<div
role="status"
aria-label="Loading orders chart"
className="flex h-full items-end gap-2 px-1 pb-6"
>
{skeletonHeights.map((height, index) => (
<Skeleton
// biome-ignore lint/suspicious/noArrayIndexKey: static placeholder list
key={index}
className="flex-1 rounded-sm"
style={{ height }}
/>
))}
</div>
);
}
if (state === "empty") {
return (
<Empty className="h-full border border-border p-4">
<EmptyHeader>
<EmptyMedia variant="icon">
<ChartNoAxesColumnIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>No orders in this range</EmptyTitle>
<EmptyDescription>
Orders appear here within a minute of checkout.
</EmptyDescription>
</EmptyHeader>
</Empty>
);
}
if (state === "error") {
return (
<Empty role="alert" className="h-full border border-destructive/30 p-4">
<EmptyHeader>
<EmptyMedia
variant="icon"
className="bg-destructive/10 text-destructive"
>
<TriangleAlertIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>Couldn't load orders</EmptyTitle>
<EmptyDescription>
The analytics service timed out after 10 seconds.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm" onClick={onRetry}>
<RefreshCwIcon aria-hidden="true" />
Try again
</Button>
</EmptyContent>
</Empty>
);
}
return (
<ChartContainer
config={chartConfig}
role="img"
aria-label="Daily orders, September 16 to 22"
className="aspect-auto h-full w-full"
>
<LineChart data={orders} margin={{ left: 12, right: 12, top: 8 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
interval="preserveStartEnd"
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<Line
dataKey="orders"
type="monotone"
stroke="var(--color-orders)"
strokeWidth={2}
dot={{ r: 3, fill: "var(--color-orders)" }}
/>
</LineChart>
</ChartContainer>
);
}
export default function Chart06() {
const [state, setState] = React.useState<ChartState>("ready");
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Daily orders</CardTitle>
<CardDescription>Preview every state the chart can be in</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<ToggleGroup
aria-label="Chart state"
variant="outline"
size="sm"
spacing={0}
value={[state]}
onValueChange={(next) => {
if (next.length > 0) setState(next[0] as ChartState);
}}
className="w-full"
>
{states.map((item) => (
<ToggleGroupItem
key={item.value}
value={item.value}
className="flex-1 text-xs"
>
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
<div className="h-56" aria-busy={state === "loading"}>
<ChartBody state={state} onRetry={() => setState("ready")} />
</div>
</CardContent>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-06pnpm dlx shadcn@latest add @sevenui/component/chart-06yarn dlx shadcn@latest add @sevenui/component/chart-06bunx --bun shadcn@latest add @sevenui/component/chart-06"use client";
import * as React from "react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
const revenue = [
{ month: "Apr", starter: 4200, pro: 11800, enterprise: 21500 },
{ month: "May", starter: 4650, pro: 12900, enterprise: 22100 },
{ month: "Jun", starter: 4410, pro: 14200, enterprise: 24800 },
{ month: "Jul", starter: 5120, pro: 15100, enterprise: 24300 },
{ month: "Aug", starter: 5480, pro: 16750, enterprise: 27600 },
{ month: "Sep", starter: 5930, pro: 18200, enterprise: 29900 },
];
const chartConfig = {
starter: { label: "Starter", color: "var(--chart-1)" },
pro: { label: "Pro", color: "var(--chart-2)" },
enterprise: { label: "Enterprise", color: "var(--chart-3)" },
} satisfies ChartConfig;
type Plan = keyof typeof chartConfig;
const plans = Object.keys(chartConfig) as Plan[];
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
});
export default function Chart07() {
// Controlled legend: the pressed toggles decide which series are drawn.
const [visible, setVisible] = React.useState<Plan[]>(["starter", "pro"]);
const latest = revenue[revenue.length - 1];
const total = visible.reduce((sum, plan) => sum + latest[plan], 0);
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Monthly recurring revenue</CardTitle>
<CardDescription>
September:{" "}
<span className="font-medium text-foreground tabular-nums">
{currency.format(total)}
</span>{" "}
across {visible.length} {visible.length === 1 ? "plan" : "plans"}
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<ToggleGroup
multiple
aria-label="Plans shown in chart"
variant="outline"
size="sm"
value={visible}
onValueChange={(next) => {
// Keep at least one series on screen.
if (next.length > 0) setVisible(next as Plan[]);
}}
className="flex-wrap"
>
{plans.map((plan) => (
<ToggleGroupItem
key={plan}
value={plan}
className="gap-2 text-xs text-muted-foreground aria-pressed:text-foreground"
>
<span
aria-hidden="true"
className="size-2 rounded-[2px] opacity-30 transition-opacity in-aria-pressed:opacity-100"
style={{ backgroundColor: chartConfig[plan].color }}
/>
{chartConfig[plan].label}
</ToggleGroupItem>
))}
</ToggleGroup>
<ChartContainer
config={chartConfig}
role="img"
aria-label="Monthly recurring revenue by plan, April to September"
className="aspect-auto h-52 w-full"
>
<AreaChart data={revenue} margin={{ left: 16, right: 16, top: 4 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
{plans
.filter((plan) => visible.includes(plan))
.map((plan) => (
<Area
key={plan}
dataKey={plan}
type="monotone"
stackId="mrr"
fill={`var(--color-${plan})`}
fillOpacity={0.25}
stroke={`var(--color-${plan})`}
strokeWidth={2}
/>
))}
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-07pnpm dlx shadcn@latest add @sevenui/component/chart-07yarn dlx shadcn@latest add @sevenui/component/chart-07bunx --bun shadcn@latest add @sevenui/component/chart-07Traffic sources
12,945 visitors in the last 30 days
"use client";
import * as React from "react";
import { Label, Pie, PieChart, Sector, type PieSectorShapeProps } from "recharts";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const sources = [
{ source: "organic", visitors: 5820, fill: "var(--color-organic)" },
{ source: "direct", visitors: 3140, fill: "var(--color-direct)" },
{ source: "referral", visitors: 1960, fill: "var(--color-referral)" },
{ source: "social", visitors: 1285, fill: "var(--color-social)" },
{ source: "email", visitors: 740, fill: "var(--color-email)" },
];
const chartConfig = {
visitors: { label: "Visitors" },
organic: { label: "Organic search", color: "var(--chart-1)" },
direct: { label: "Direct", color: "var(--chart-2)" },
referral: { label: "Referral", color: "var(--chart-3)" },
social: { label: "Social", color: "var(--chart-4)" },
email: { label: "Email", color: "var(--chart-5)" },
} satisfies ChartConfig;
type Source = (typeof sources)[number]["source"];
const items = sources.map((item) => ({
value: item.source,
label: String(chartConfig[item.source as keyof typeof chartConfig].label),
}));
const total = sources.reduce((sum, item) => sum + item.visitors, 0);
export default function Chart08() {
const [active, setActive] = React.useState<Source>("organic");
const activeItem = sources.find((item) => item.source === active) ?? sources[0];
const share = Math.round((activeItem.visitors / total) * 100);
// The selected sector grows outward; the others step back.
const renderSector = React.useCallback(
(props: PieSectorShapeProps) => {
const isSelected = props.payload?.source === active;
return (
<Sector
{...props}
outerRadius={(props.outerRadius ?? 0) + (isSelected ? 8 : 0)}
fillOpacity={isSelected ? 1 : 0.35}
/>
);
},
[active],
);
return (
<div className="flex w-full max-w-xs flex-col items-center gap-4">
<div className="flex w-full items-center justify-between gap-3">
<span id="chart-08-title" className="text-sm font-medium">
Traffic sources
</span>
<Select
items={items}
value={active}
onValueChange={(value) => {
if (value) setActive(value as Source);
}}
>
<SelectTrigger aria-label="Highlighted source" className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<ChartContainer
config={chartConfig}
role="img"
aria-labelledby="chart-08-title"
className="aspect-square w-full max-w-60"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent nameKey="source" hideLabel />}
/>
<Pie
data={sources}
dataKey="visitors"
nameKey="source"
innerRadius="58%"
outerRadius="82%"
strokeWidth={4}
stroke="var(--background)"
shape={renderSector}
isAnimationActive={false}
>
<Label
content={({ viewBox }) => {
if (!viewBox || !("cx" in viewBox) || !("cy" in viewBox)) {
return null;
}
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-2xl font-semibold tabular-nums"
>
{share}%
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy ?? 0) + 22}
className="fill-muted-foreground text-xs"
>
{activeItem.visitors.toLocaleString()} visitors
</tspan>
</text>
);
}}
/>
</Pie>
</PieChart>
</ChartContainer>
<p className="text-center text-xs text-muted-foreground">
{total.toLocaleString()} visitors in the last 30 days
</p>
</div>
);
}
npx shadcn@latest add @sevenui/component/chart-08pnpm dlx shadcn@latest add @sevenui/component/chart-08yarn dlx shadcn@latest add @sevenui/component/chart-08bunx --bun shadcn@latest add @sevenui/component/chart-08"use client";
import * as React from "react";
import { ZapIcon } from "lucide-react";
import { Bar, BarChart, Brush, CartesianGrid, XAxis, YAxis } from "recharts";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
const RATE_PER_KWH = 0.31;
// Deterministic sample: 60 days of household electricity use ending Sep 24,
// heavier on weekends and during an early-September heat wave.
const usage = Array.from({ length: 60 }, (_, index) => {
const date = new Date(Date.UTC(2026, 8, 24 - (59 - index)));
const weekday = date.getUTCDay();
const weekend = weekday === 0 || weekday === 6 ? 3.4 : 0;
const heatWave = index >= 30 && index <= 38 ? 6.5 : 0;
const base = 14 + Math.sin(index / 5) * 2.2;
return {
date: date.toISOString().slice(0, 10),
kwh: Math.round((base + weekend + heatWave) * 10) / 10,
};
});
const chartConfig = {
kwh: { label: "Usage (kWh)", color: "var(--chart-4)" },
} satisfies ChartConfig;
const shortDate = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
timeZone: "UTC",
});
const formatDate = (value: unknown) =>
typeof value === "string" ? shortDate.format(new Date(value)) : "";
const usd = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
const defaultWindow = { startIndex: 46, endIndex: 59 };
export default function Chart09() {
const [range, setRange] = React.useState(defaultWindow);
const selected = usage.slice(range.startIndex, range.endIndex + 1);
const total = selected.reduce((sum, day) => sum + day.kwh, 0);
const peak = selected.reduce((max, day) => (day.kwh > max.kwh ? day : max));
const isDefault =
range.startIndex === defaultWindow.startIndex &&
range.endIndex === defaultWindow.endIndex;
return (
<Card className="w-full max-w-lg">
<CardHeader>
<CardTitle>Electricity use</CardTitle>
<CardDescription>
Drag the handles below the chart to pick a date range.
</CardDescription>
<CardAction>
<Button
variant="ghost"
size="sm"
disabled={isDefault}
onClick={() => setRange(defaultWindow)}
>
Last 14 days
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<dl
aria-live="polite"
className="grid grid-cols-2 gap-3 sm:grid-cols-3 text-sm [&_dd]:font-medium [&_dd]:tabular-nums [&_dt]:text-xs [&_dt]:text-muted-foreground"
>
<div className="flex flex-col gap-0.5">
<dt>
{formatDate(selected[0].date)} –{" "}
{formatDate(selected[selected.length - 1].date)}
</dt>
<dd>{total.toFixed(0)} kWh</dd>
</div>
<div className="flex flex-col gap-0.5">
<dt>Estimated cost</dt>
<dd>{usd.format(total * RATE_PER_KWH)}</dd>
</div>
<div className="flex flex-col gap-0.5">
<dt>Peak day</dt>
<dd className="flex items-center gap-1">
<ZapIcon aria-hidden="true" className="size-3.5 text-chart-4" />
{formatDate(peak.date)}
</dd>
</div>
</dl>
<ChartContainer
config={chartConfig}
role="img"
aria-label={`Daily electricity use from ${formatDate(selected[0].date)} to ${formatDate(selected[selected.length - 1].date)}: ${total.toFixed(0)} kilowatt-hours in total`}
className="aspect-auto h-64 w-full [&_.recharts-brush-texts_text]:fill-muted-foreground"
>
<BarChart data={usage} margin={{ left: 0, right: 8, top: 8 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={24}
tickFormatter={formatDate}
/>
<YAxis
width={32}
tickLine={false}
axisLine={false}
tickFormatter={(value: number) => `${value}`}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(_, payload) =>
formatDate(payload?.[0]?.payload?.date)
}
/>
}
/>
<Bar dataKey="kwh" fill="var(--color-kwh)" radius={[3, 3, 0, 0]} />
<Brush
dataKey="date"
height={28}
travellerWidth={8}
startIndex={range.startIndex}
endIndex={range.endIndex}
onChange={(next) => {
if (
typeof next.startIndex === "number" &&
typeof next.endIndex === "number"
) {
setRange({
startIndex: next.startIndex,
endIndex: next.endIndex,
});
}
}}
tickFormatter={formatDate}
ariaLabel="Date range"
fill="var(--muted)"
stroke="var(--muted-foreground)"
/>
</BarChart>
</ChartContainer>
<p className="text-xs text-muted-foreground">
Billed at {usd.format(RATE_PER_KWH)} per kWh on the Standard
residential plan.
</p>
</CardContent>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-09pnpm dlx shadcn@latest add @sevenui/component/chart-09yarn dlx shadcn@latest add @sevenui/component/chart-09bunx --bun shadcn@latest add @sevenui/component/chart-09"use client";
import { BellRing, CircleCheck, TriangleAlert } from "lucide-react";
import * as React from "react";
import {
Area,
AreaChart,
CartesianGrid,
ReferenceLine,
XAxis,
YAxis,
} from "recharts";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
const PLAN_LIMIT = 1_000_000;
const SCALE_LIMIT = 5_000_000;
// Threshold for the optional usage alert email.
const ALERT_AT = 900_000;
// Cumulative API requests for the Sep 1 - Sep 30 billing cycle.
// Actual usage runs through Sep 18, then a straight-line projection.
const usageData = [
{ day: "Sep 1", actual: 21_400 },
{ day: "Sep 3", actual: 78_900 },
{ day: "Sep 5", actual: 142_300 },
{ day: "Sep 7", actual: 188_100 },
{ day: "Sep 9", actual: 262_700 },
{ day: "Sep 11", actual: 341_500 },
{ day: "Sep 13", actual: 409_800 },
{ day: "Sep 15", actual: 478_200 },
{ day: "Sep 17", actual: 561_900 },
{ day: "Sep 18", actual: 604_300, projected: 604_300 },
{ day: "Sep 21", projected: 718_000 },
{ day: "Sep 24", projected: 831_700 },
{ day: "Sep 27", projected: 945_400 },
{ day: "Sep 30", projected: 1_059_100 },
];
const chartConfig = {
actual: { label: "Requests", color: "var(--chart-1)" },
projected: { label: "Projected", color: "var(--chart-1)" },
} satisfies ChartConfig;
const compact = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
export default function Chart10() {
const used = 604_300;
const projected = 1_059_100;
const overage = projected - PLAN_LIMIT;
const [alertOn, setAlertOn] = React.useState(false);
const [upgraded, setUpgraded] = React.useState(false);
return (
<Card className="w-full max-w-lg">
<CardHeader>
<CardTitle>API requests</CardTitle>
<CardDescription>
Billing cycle Sep 1 – Sep 30 · {upgraded ? "Scale" : "Growth"} plan
</CardDescription>
<CardAction>
<Badge variant="outline">12 days left</Badge>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-wrap items-baseline gap-x-2">
<span className="font-semibold text-2xl tabular-nums">
{used.toLocaleString("en-US")}
</span>
<span className="text-sm text-muted-foreground">
of {(upgraded ? SCALE_LIMIT : PLAN_LIMIT).toLocaleString("en-US")}{" "}
included
</span>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-48 w-full"
role="img"
aria-label={`Cumulative API requests: ${used.toLocaleString("en-US")} used so far, projected ${projected.toLocaleString("en-US")} by Sep 30 against a ${PLAN_LIMIT.toLocaleString("en-US")} limit`}
>
<AreaChart data={usageData} margin={{ left: 0, right: 8, top: 8 }}>
<defs>
<linearGradient id="chart-10-fill" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="var(--color-actual)"
stopOpacity={0.3}
/>
<stop
offset="100%"
stopColor="var(--color-actual)"
stopOpacity={0}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="day"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={24}
/>
<YAxis
width={40}
tickLine={false}
axisLine={false}
domain={[0, 1_100_000]}
ticks={[0, 250_000, 500_000, 750_000, 1_000_000]}
tickFormatter={(value: number) => compact.format(value)}
/>
<ChartTooltip
content={
<ChartTooltipContent
indicator="line"
formatter={(value, name) => (
<div className="flex w-full items-center justify-between gap-3">
<span className="text-muted-foreground">
{chartConfig[name as keyof typeof chartConfig]?.label}
</span>
<span className="font-mono font-medium tabular-nums">
{Number(value).toLocaleString("en-US")}
</span>
</div>
)}
/>
}
/>
{upgraded ? null : (
<ReferenceLine
y={PLAN_LIMIT}
stroke="var(--destructive)"
strokeDasharray="4 4"
label={{
value: "Plan limit",
position: "insideTopLeft",
fill: "var(--destructive)",
fontSize: 11,
}}
/>
)}
{alertOn ? (
<ReferenceLine
y={ALERT_AT}
stroke="var(--muted-foreground)"
strokeDasharray="2 3"
label={{
value: `Alert at ${compact.format(ALERT_AT)}`,
position: "insideTopRight",
fill: "var(--muted-foreground)",
fontSize: 11,
}}
/>
) : null}
<Area
dataKey="actual"
type="monotone"
stroke="var(--color-actual)"
strokeWidth={2}
fill="url(#chart-10-fill)"
connectNulls={false}
/>
<Area
dataKey="projected"
type="linear"
stroke="var(--color-projected)"
strokeWidth={2}
strokeDasharray="5 4"
fill="none"
connectNulls={false}
/>
</AreaChart>
</ChartContainer>
<div
role="status"
className="flex items-start gap-2.5 rounded-lg bg-muted px-3 py-2.5 text-sm"
>
{upgraded ? (
<>
<CircleCheck
aria-hidden="true"
className="mt-0.5 size-4 shrink-0 text-success"
/>
<p className="text-muted-foreground">
You are on Scale now. The projected {compact.format(projected)}{" "}
requests fit inside the{" "}
<span className="font-medium text-foreground">
{compact.format(SCALE_LIMIT)}
</span>{" "}
included, so there is no overage this cycle.
</p>
</>
) : (
<>
<TriangleAlert
aria-hidden="true"
className="mt-0.5 size-4 shrink-0 text-destructive"
/>
<p className="text-muted-foreground">
At this pace you will pass your limit around{" "}
<span className="font-medium text-foreground">Sep 29</span>. The
extra {compact.format(overage)} requests would cost{" "}
<span className="font-medium text-foreground tabular-nums">
$29.55
</span>{" "}
at $0.50 per 1K.
</p>
</>
)}
</div>
</CardContent>
<CardFooter className="flex-wrap justify-end gap-2">
<Button
variant="ghost"
size="sm"
aria-pressed={alertOn}
onClick={() => setAlertOn((on) => !on)}
>
{alertOn ? (
<BellRing aria-hidden="true" data-icon="inline-start" />
) : null}
{alertOn
? `Alert set at ${compact.format(ALERT_AT)}`
: "Set a usage alert"}
</Button>
{upgraded ? (
<Button
size="sm"
variant="outline"
onClick={() => setUpgraded(false)}
>
Stay on Growth
</Button>
) : (
<Button size="sm" onClick={() => setUpgraded(true)}>
Upgrade to Scale
</Button>
)}
</CardFooter>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-10pnpm dlx shadcn@latest add @sevenui/component/chart-10yarn dlx shadcn@latest add @sevenui/component/chart-10bunx --bun shadcn@latest add @sevenui/component/chart-10Arden Noise-Cancelling Headphones
Price history
$289avg $309
Lowest: $269 on Jul 8
Close to the lowest price of the past year.
"use client";
import { BellRing } from "lucide-react";
import * as React from "react";
import {
CartesianGrid,
Line,
LineChart,
ReferenceDot,
ReferenceLine,
XAxis,
YAxis,
} from "recharts";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Range = "3m" | "6m" | "1y";
const history = [
{ date: "Oct 2", price: 349 },
{ date: "Nov 1", price: 349 },
{ date: "Nov 24", price: 279 },
{ date: "Dec 2", price: 329 },
{ date: "Jan 6", price: 329 },
{ date: "Feb 3", price: 319 },
{ date: "Mar 10", price: 319 },
{ date: "Apr 7", price: 299 },
{ date: "May 5", price: 309 },
{ date: "Jun 2", price: 309 },
{ date: "Jul 8", price: 269 },
{ date: "Jul 15", price: 299 },
{ date: "Aug 4", price: 299 },
{ date: "Sep 1", price: 289 },
{ date: "Sep 22", price: 289 },
];
const rangeStart: Record<Range, number> = { "3m": 10, "6m": 6, "1y": 0 };
const ALERT_PRICE = 275;
const chartConfig = {
price: { label: "Price", color: "var(--chart-1)" },
} satisfies ChartConfig;
const usd = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
});
export default function Chart11() {
const [range, setRange] = React.useState<Range>("1y");
const [alertOn, setAlertOn] = React.useState(false);
const data = history.slice(rangeStart[range]);
const current = data[data.length - 1];
const lowest = data.reduce((min, point) =>
point.price < min.price ? point : min,
);
const average = Math.round(
data.reduce((sum, point) => sum + point.price, 0) / data.length,
);
return (
<section
aria-labelledby="chart-11-title"
className="flex w-full max-w-md flex-col gap-4 rounded-xl border bg-card p-4 text-card-foreground"
>
<div className="flex gap-3">
<img
src="/placeholder.svg"
alt=""
className="size-14 shrink-0 rounded-lg border bg-muted object-cover"
/>
<div className="flex min-w-0 flex-col gap-0.5">
<h3 id="chart-11-title" className="truncate font-medium text-sm">
Arden Noise-Cancelling Headphones
</h3>
<p className="text-xs text-muted-foreground">Price history</p>
<p className="flex items-baseline gap-2">
<span className="font-semibold text-lg tabular-nums">
{usd.format(current.price)}
</span>
<span className="text-xs text-muted-foreground tabular-nums">
avg {usd.format(average)}
</span>
</p>
</div>
</div>
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-muted-foreground">
Lowest:{" "}
<span className="font-medium text-foreground tabular-nums">
{usd.format(lowest.price)}
</span>{" "}
on {lowest.date}
</p>
<ToggleGroup
aria-label="Time range"
size="sm"
variant="outline"
spacing={0}
value={[range]}
onValueChange={(value) => {
const next = value[0] as Range | undefined;
if (next) setRange(next);
}}
>
<ToggleGroupItem value="3m">3M</ToggleGroupItem>
<ToggleGroupItem value="6m">6M</ToggleGroupItem>
<ToggleGroupItem value="1y">1Y</ToggleGroupItem>
</ToggleGroup>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-40 w-full"
role="img"
aria-label={`Price history: now ${usd.format(current.price)}, lowest ${usd.format(lowest.price)} on ${lowest.date}`}
>
<LineChart data={data} margin={{ left: 0, right: 12, top: 12 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={28}
/>
<YAxis
width={40}
tickLine={false}
axisLine={false}
domain={[250, 360]}
tickFormatter={(value: number) => `$${value}`}
/>
<ChartTooltip
content={
<ChartTooltipContent
indicator="line"
formatter={(value) => (
<div className="flex w-full items-center justify-between gap-3">
<span className="text-muted-foreground">Price</span>
<span className="font-mono font-medium tabular-nums">
{usd.format(Number(value))}
</span>
</div>
)}
/>
}
/>
{alertOn ? (
<ReferenceLine
y={ALERT_PRICE}
stroke="var(--color-price)"
strokeDasharray="3 3"
label={{
value: `Alert ${usd.format(ALERT_PRICE)}`,
position: "insideBottomRight",
fill: "var(--muted-foreground)",
fontSize: 11,
}}
/>
) : null}
<Line
dataKey="price"
type="stepAfter"
stroke="var(--color-price)"
strokeWidth={2}
dot={false}
/>
<ReferenceDot
x={lowest.date}
y={lowest.price}
r={4}
fill="var(--color-price)"
stroke="var(--background)"
strokeWidth={2}
/>
</LineChart>
</ChartContainer>
<div className="flex items-center justify-between gap-3 rounded-lg bg-muted px-3 py-2.5">
<div className="flex items-start gap-2.5">
<BellRing
aria-hidden="true"
className="mt-0.5 size-4 shrink-0 text-muted-foreground"
/>
<div className="flex flex-col gap-0.5">
<Label htmlFor="chart-11-alert">
Email me under {usd.format(ALERT_PRICE)}
</Label>
<span className="text-xs text-muted-foreground">
{alertOn
? "We will check the price every 6 hours."
: "Close to the lowest price of the past year."}
</span>
</div>
</div>
<Switch
id="chart-11-alert"
checked={alertOn}
onCheckedChange={setAlertOn}
/>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/chart-11pnpm dlx shadcn@latest add @sevenui/component/chart-11yarn dlx shadcn@latest add @sevenui/component/chart-11bunx --bun shadcn@latest add @sevenui/component/chart-11"use client";
import * as React from "react";
import {
CartesianGrid,
ReferenceLine,
Scatter,
ScatterChart,
XAxis,
YAxis,
ZAxis,
} from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
type Segment = "all" | "teams" | "solo";
type Feature = {
name: string;
// Share of active accounts that used the feature in the last 30 days.
adoption: number;
// 90-day retention of accounts that used it.
retention: number;
// Weekly events, used for dot size.
events: number;
};
const features: Record<Segment, Feature[]> = {
all: [
{ name: "Shared inbox", adoption: 72, retention: 88, events: 41_200 },
{ name: "Saved replies", adoption: 58, retention: 81, events: 18_900 },
{ name: "Automations", adoption: 21, retention: 93, events: 12_400 },
{ name: "SLA timers", adoption: 17, retention: 90, events: 6_300 },
{ name: "CSAT surveys", adoption: 44, retention: 69, events: 9_800 },
{ name: "Mobile app", adoption: 63, retention: 62, events: 22_100 },
{ name: "Custom fields", adoption: 12, retention: 71, events: 2_900 },
],
teams: [
{ name: "Shared inbox", adoption: 91, retention: 90, events: 36_700 },
{ name: "Saved replies", adoption: 66, retention: 84, events: 14_200 },
{ name: "Automations", adoption: 34, retention: 95, events: 11_600 },
{ name: "SLA timers", adoption: 29, retention: 92, events: 5_900 },
{ name: "CSAT surveys", adoption: 52, retention: 73, events: 8_100 },
{ name: "Mobile app", adoption: 48, retention: 70, events: 12_300 },
{ name: "Custom fields", adoption: 19, retention: 76, events: 2_400 },
],
solo: [
{ name: "Shared inbox", adoption: 38, retention: 74, events: 4_500 },
{ name: "Saved replies", adoption: 43, retention: 72, events: 4_700 },
{ name: "Automations", adoption: 8, retention: 86, events: 800 },
{ name: "SLA timers", adoption: 4, retention: 70, events: 400 },
{ name: "CSAT surveys", adoption: 27, retention: 58, events: 1_700 },
{ name: "Mobile app", adoption: 81, retention: 55, events: 9_800 },
{ name: "Custom fields", adoption: 6, retention: 61, events: 500 },
],
};
const segments: { value: Segment; label: string }[] = [
{ value: "all", label: "All accounts" },
{ value: "teams", label: "Teams" },
{ value: "solo", label: "Solo" },
];
const chartConfig = {
retention: { label: "90-day retention", color: "var(--chart-1)" },
hidden: { label: "Hidden gem", color: "var(--chart-2)" },
} satisfies ChartConfig;
function median(values: number[]) {
const sorted = [...values].sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
return sorted.length % 2
? sorted[middle]
: (sorted[middle - 1] + sorted[middle]) / 2;
}
export default function Chart12() {
const [segment, setSegment] = React.useState<Segment>("all");
const points = features[segment];
const adoptionMid = median(points.map((point) => point.adoption));
const retentionMid = median(points.map((point) => point.retention));
// High retention but low adoption: worth promoting in onboarding.
const hiddenGems = points.filter(
(point) => point.adoption < adoptionMid && point.retention > retentionMid,
);
const others = points.filter((point) => !hiddenGems.includes(point));
return (
<Card className="w-full max-w-lg">
<CardHeader>
<CardTitle>Feature adoption vs. retention</CardTitle>
<CardDescription>
Help desk features, last 30 days. Dot size is weekly usage.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Tabs
value={segment}
onValueChange={(value) => setSegment(value as Segment)}
>
<TabsList className="w-full">
{segments.map((item) => (
<TabsTrigger key={item.value} value={item.value} className="flex-1">
{item.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<ChartContainer
config={chartConfig}
role="img"
aria-label={`Adoption against 90-day retention for ${points.length} features. Hidden gems: ${hiddenGems.map((point) => point.name).join(", ") || "none"}.`}
className="aspect-auto h-64 w-full"
>
<ScatterChart margin={{ left: 0, right: 12, top: 12, bottom: 4 }}>
<CartesianGrid />
<XAxis
type="number"
dataKey="adoption"
name="Adoption"
domain={[0, 100]}
ticks={[0, 25, 50, 75, 100]}
tickFormatter={(value: number) => `${value}%`}
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<YAxis
type="number"
dataKey="retention"
name="Retention"
domain={[50, 100]}
ticks={[50, 60, 70, 80, 90, 100]}
tickFormatter={(value: number) => `${value}%`}
tickLine={false}
axisLine={false}
width={40}
/>
<ZAxis type="number" dataKey="events" range={[60, 480]} />
<ReferenceLine
x={adoptionMid}
stroke="var(--border)"
strokeDasharray="4 4"
/>
<ReferenceLine
y={retentionMid}
stroke="var(--border)"
strokeDasharray="4 4"
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
hideIndicator
hideLabel
formatter={(_value, _name, item, index) => {
// Scatter sends x, y and z as three rows; render one card for the point.
if (index > 0) return null;
const point = item.payload as Feature;
return (
<div className="grid w-full gap-1">
<span className="font-medium">{point.name}</span>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">Adoption</span>
<span className="font-mono tabular-nums">
{point.adoption}%
</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">
Retention
</span>
<span className="font-mono tabular-nums">
{point.retention}%
</span>
</div>
</div>
);
}}
/>
}
/>
<Scatter
data={others}
fill="var(--color-retention)"
fillOpacity={0.55}
isAnimationActive={false}
/>
<Scatter
data={hiddenGems}
fill="var(--color-hidden)"
stroke="var(--foreground)"
strokeWidth={1}
isAnimationActive={false}
/>
</ScatterChart>
</ChartContainer>
<div className="flex flex-col gap-1 border-t pt-4 text-sm">
<p className="flex items-center gap-2 font-medium">
<span
aria-hidden="true"
className="size-2.5 rounded-full border border-foreground bg-chart-2"
/>
Hidden gems
</p>
<p className="text-muted-foreground">
{hiddenGems.length
? `${hiddenGems.map((point) => point.name).join(", ")} keep accounts around but few find them. Surface them in onboarding.`
: "No feature pairs low adoption with high retention in this segment."}
</p>
</div>
</CardContent>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-12pnpm dlx shadcn@latest add @sevenui/component/chart-12yarn dlx shadcn@latest add @sevenui/component/chart-12bunx --bun shadcn@latest add @sevenui/component/chart-12Latency
orders-api · production · last 2 hours
- p95 now
- 212 ms
- Peak p99
- 1,240 ms
- Errors
- 1.84%
ResolvedConnection pool exhaustion on orders-db, 14:40 – 15:00.
"use client";
import * as React from "react";
import {
CartesianGrid,
Line,
LineChart,
ReferenceArea,
XAxis,
YAxis,
} from "recharts";
import { Badge } from "@/components/ui/badge";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type Endpoint = "list-orders" | "create-order" | "search";
const endpoints: { value: Endpoint; label: string }[] = [
{ value: "list-orders", label: "GET /v1/orders" },
{ value: "create-order", label: "POST /v1/orders" },
{ value: "search", label: "GET /v1/search" },
];
const times = [
"14:00",
"14:10",
"14:20",
"14:30",
"14:40",
"14:50",
"15:00",
"15:10",
"15:20",
"15:30",
"15:40",
"15:50",
];
// Latency in milliseconds per 10-minute bucket, per endpoint.
const series: Record<
Endpoint,
{
p50: number[];
p95: number[];
p99: number[];
errorRate: string;
incident?: [string, string];
}
> = {
"list-orders": {
p50: [42, 44, 41, 43, 45, 44, 42, 43, 41, 44, 43, 42],
p95: [118, 121, 116, 124, 126, 119, 117, 122, 118, 120, 121, 119],
p99: [210, 224, 205, 231, 240, 219, 212, 226, 208, 221, 218, 214],
errorRate: "0.02%",
},
"create-order": {
p50: [88, 91, 86, 94, 162, 188, 171, 96, 90, 89, 92, 87],
p95: [214, 226, 208, 239, 512, 640, 587, 247, 221, 218, 230, 212],
p99: [390, 410, 378, 452, 980, 1240, 1105, 468, 402, 396, 421, 388],
errorRate: "1.84%",
incident: ["14:40", "15:00"],
},
search: {
p50: [64, 66, 71, 69, 73, 78, 81, 84, 86, 91, 94, 97],
p95: [180, 184, 196, 201, 214, 226, 238, 247, 259, 268, 281, 290],
p99: [320, 331, 352, 360, 381, 402, 418, 436, 451, 470, 488, 502],
errorRate: "0.11%",
},
};
const chartConfig = {
p50: { label: "p50", color: "var(--chart-2)" },
p95: { label: "p95", color: "var(--chart-1)" },
p99: { label: "p99", color: "var(--chart-4)" },
} satisfies ChartConfig;
export default function Chart13() {
const [endpoint, setEndpoint] = React.useState<Endpoint>("create-order");
const current = series[endpoint];
const data = React.useMemo(
() =>
times.map((time, index) => ({
time,
p50: current.p50[index],
p95: current.p95[index],
p99: current.p99[index],
})),
[current],
);
const latestP95 = current.p95[current.p95.length - 1];
const peakP99 = Math.max(...current.p99);
return (
<section
aria-labelledby="chart-13-title"
className="flex w-full max-w-xl flex-col gap-4 rounded-xl border bg-card p-4 text-card-foreground"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-col gap-0.5">
<h3 id="chart-13-title" className="font-medium text-sm">
Latency
</h3>
<p className="text-xs text-muted-foreground">
orders-api · production · last 2 hours
</p>
</div>
<Select
items={endpoints}
value={endpoint}
onValueChange={(value) => setEndpoint(value as Endpoint)}
>
<SelectTrigger
aria-label="Endpoint"
size="sm"
className="w-full font-mono sm:w-44"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{endpoints.map((item) => (
<SelectItem
key={item.value}
value={item.value}
className="font-mono"
>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<dl className="grid grid-cols-3 gap-1.5 text-sm sm:gap-2">
<div className="flex flex-col gap-0.5 rounded-lg bg-muted px-2 py-2 sm:px-3">
<dt className="text-xs text-muted-foreground">p95 now</dt>
<dd className="font-mono text-xs font-medium whitespace-nowrap tabular-nums sm:text-sm">{latestP95} ms</dd>
</div>
<div className="flex flex-col gap-0.5 rounded-lg bg-muted px-2 py-2 sm:px-3">
<dt className="text-xs text-muted-foreground">Peak p99</dt>
<dd className="font-mono text-xs font-medium whitespace-nowrap tabular-nums sm:text-sm">
{peakP99.toLocaleString("en-US")} ms
</dd>
</div>
<div className="flex flex-col gap-0.5 rounded-lg bg-muted px-2 py-2 sm:px-3">
<dt className="text-xs text-muted-foreground">Errors</dt>
<dd className="font-mono text-xs font-medium whitespace-nowrap tabular-nums sm:text-sm">
{current.errorRate}
</dd>
</div>
</dl>
<ChartContainer
config={chartConfig}
className="aspect-auto h-52 w-full"
role="img"
aria-label={`Latency percentiles for ${endpoints.find((item) => item.value === endpoint)?.label}: p95 now ${latestP95} ms, peak p99 ${peakP99} ms`}
>
<LineChart data={data} margin={{ left: 0, right: 8, top: 8 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={24}
/>
<YAxis
width={56}
tickLine={false}
axisLine={false}
// A non-breaking space keeps recharts from wrapping "1400 ms" onto two lines.
tickFormatter={(value: number) => `${value}\u00a0ms`}
/>
{current.incident ? (
<ReferenceArea
x1={current.incident[0]}
x2={current.incident[1]}
fill="var(--destructive)"
fillOpacity={0.08}
label={{
value: "INC-2291",
position: "insideTop",
fill: "var(--destructive)",
fontSize: 11,
}}
/>
) : null}
<ChartTooltip
content={
<ChartTooltipContent
indicator="line"
formatter={(value, name, item) => (
<div className="flex w-full items-center gap-2">
<span
aria-hidden="true"
className="h-2.5 w-1 rounded-[2px]"
style={{ backgroundColor: item.color }}
/>
<span className="text-muted-foreground">
{chartConfig[name as keyof typeof chartConfig]?.label}
</span>
<span className="ml-auto font-mono font-medium tabular-nums">
{value} ms
</span>
</div>
)}
/>
}
/>
<ChartLegend content={<ChartLegendContent />} />
<Line
dataKey="p50"
type="monotone"
stroke="var(--color-p50)"
strokeWidth={2}
dot={false}
/>
<Line
dataKey="p95"
type="monotone"
stroke="var(--color-p95)"
strokeWidth={2}
dot={false}
/>
<Line
dataKey="p99"
type="monotone"
stroke="var(--color-p99)"
strokeWidth={1.5}
strokeDasharray="4 3"
dot={false}
/>
</LineChart>
</ChartContainer>
{current.incident ? (
<div className="flex flex-wrap items-center gap-2 border-t pt-3 text-xs text-muted-foreground">
<Badge variant="outline">Resolved</Badge>
<span>
Connection pool exhaustion on orders-db, {current.incident[0]} –{" "}
{current.incident[1]}.
</span>
</div>
) : null}
</section>
);
}
npx shadcn@latest add @sevenui/component/chart-13pnpm dlx shadcn@latest add @sevenui/component/chart-13yarn dlx shadcn@latest add @sevenui/component/chart-13bunx --bun shadcn@latest add @sevenui/component/chart-13"use client";
import { ArrowRight, Undo2 } from "lucide-react";
import * as React from "react";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
type Member = {
name: string;
initials: string;
completed: number;
pending: number;
};
const initialTeam: Member[] = [
{ name: "Priya Raman", initials: "PR", completed: 14, pending: 9 },
{ name: "Marcus Webb", initials: "MW", completed: 11, pending: 4 },
{ name: "Elena Sokolova", initials: "ES", completed: 9, pending: 3 },
{ name: "Tomás Herrera", initials: "TH", completed: 7, pending: 2 },
{ name: "Aiko Tanaka", initials: "AT", completed: 5, pending: 1 },
];
const OVERLOAD_THRESHOLD = 6;
const chartConfig = {
completed: { label: "Reviewed", color: "var(--chart-2)" },
pending: { label: "Waiting", color: "var(--chart-1)" },
} satisfies ChartConfig;
export default function Chart14() {
const [team, setTeam] = React.useState(initialTeam);
const [lastMove, setLastMove] = React.useState<{
from: string;
to: string;
count: number;
} | null>(null);
const overloaded = team.reduce((max, member) =>
member.pending > max.pending ? member : max,
);
const lightest = team.reduce((min, member) =>
member.pending + member.completed < min.pending + min.completed
? member
: min,
);
const moveCount = Math.max(
0,
Math.floor((overloaded.pending - lightest.pending) / 2),
);
const needsRebalance =
overloaded.pending >= OVERLOAD_THRESHOLD && moveCount > 0;
function move(from: string, to: string, count: number) {
setTeam((current) =>
current.map((member) => {
if (member.name === from)
return { ...member, pending: member.pending - count };
if (member.name === to)
return { ...member, pending: member.pending + count };
return member;
}),
);
}
function rebalance() {
move(overloaded.name, lightest.name, moveCount);
setLastMove({ from: overloaded.name, to: lightest.name, count: moveCount });
}
function undo() {
if (!lastMove) return;
move(lastMove.to, lastMove.from, lastMove.count);
setLastMove(null);
}
const firstName = (name: string) => name.split(" ")[0];
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Review load</CardTitle>
<CardDescription>
Pull requests assigned per reviewer · Sprint 38
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer
config={chartConfig}
className="aspect-auto h-56 w-full"
role="img"
aria-label={`Review load: ${team
.map(
(member) =>
`${member.name} ${member.completed} reviewed, ${member.pending} waiting`,
)
.join("; ")}`}
>
<BarChart
data={team}
layout="vertical"
margin={{ left: 0, right: 8 }}
barSize={18}
>
<CartesianGrid horizontal={false} />
<XAxis
type="number"
tickLine={false}
axisLine={false}
allowDecimals={false}
/>
<YAxis
type="category"
dataKey="name"
width={64}
tickLine={false}
axisLine={false}
tickFormatter={firstName}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<ChartLegend content={<ChartLegendContent />} />
<Bar
dataKey="completed"
stackId="reviews"
fill="var(--color-completed)"
radius={[4, 0, 0, 4]}
/>
<Bar
dataKey="pending"
stackId="reviews"
fill="var(--color-pending)"
radius={[0, 4, 4, 0]}
/>
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="border-t" aria-live="polite">
{lastMove ? (
<div className="flex w-full items-center justify-between gap-3">
<p className="text-sm text-muted-foreground">
Moved {lastMove.count} reviews from {firstName(lastMove.from)} to{" "}
{firstName(lastMove.to)}.
</p>
<Button size="sm" variant="ghost" onClick={undo}>
<Undo2 aria-hidden="true" data-icon="inline-start" />
Undo
</Button>
</div>
) : needsRebalance ? (
<div className="flex w-full flex-col gap-3">
<div className="flex items-center gap-2 text-sm">
<Avatar size="sm">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>{overloaded.initials}</AvatarFallback>
</Avatar>
<ArrowRight
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
<Avatar size="sm">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>{lightest.initials}</AvatarFallback>
</Avatar>
<p className="min-w-0 text-muted-foreground">
<span className="font-medium text-foreground">
{firstName(overloaded.name)}
</span>{" "}
has {overloaded.pending} reviews waiting.
</p>
</div>
<Button
size="sm"
variant="outline"
onClick={rebalance}
className="self-start"
>
Reassign {moveCount} to {firstName(lightest.name)}
</Button>
</div>
) : (
<p className="text-sm text-muted-foreground">
Review load is balanced across the team.
</p>
)}
</CardFooter>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-14pnpm dlx shadcn@latest add @sevenui/component/chart-14yarn dlx shadcn@latest add @sevenui/component/chart-14bunx --bun shadcn@latest add @sevenui/component/chart-14This week
2 days under 3h focus
MeetingsOpen3h focus line
Tuesday, Sep 23
2h open- 09:00Customer call: Northwind1h
- 11:00Roadmap sync1.5h
- 14:00Hiring panel2h
- 16:30Incident retro1.5h
"use client";
import { CalendarClock, Undo2 } from "lucide-react";
import * as React from "react";
import { Bar, BarChart, Cell, ReferenceLine, XAxis, YAxis } from "recharts";
import { Button } from "@/components/ui/button";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Meeting = {
id: string;
day: string;
time: string;
title: string;
hours: number;
movable?: boolean;
};
const WORKDAY_HOURS = 8;
const FOCUS_GOAL = 3;
const days = [
{ key: "mon", label: "Mon", date: "Monday, Sep 22" },
{ key: "tue", label: "Tue", date: "Tuesday, Sep 23" },
{ key: "wed", label: "Wed", date: "Wednesday, Sep 24" },
{ key: "thu", label: "Thu", date: "Thursday, Sep 25" },
{ key: "fri", label: "Fri", date: "Friday, Sep 26" },
];
const initialMeetings: Meeting[] = [
{ id: "m1", day: "mon", time: "09:30", title: "Weekly planning", hours: 1 },
{
id: "m2",
day: "mon",
time: "13:00",
title: "Design review: checkout",
hours: 1.5,
},
{ id: "m3", day: "mon", time: "16:00", title: "1:1 with Dana", hours: 0.5 },
{
id: "t1",
day: "tue",
time: "09:00",
title: "Customer call: Northwind",
hours: 1,
},
{ id: "t2", day: "tue", time: "11:00", title: "Roadmap sync", hours: 1.5 },
{ id: "t3", day: "tue", time: "14:00", title: "Hiring panel", hours: 2 },
{
id: "t4",
day: "tue",
time: "16:30",
title: "Incident retro",
hours: 1.5,
movable: true,
},
{ id: "w1", day: "wed", time: "10:00", title: "Standup + demo", hours: 1 },
{
id: "h1",
day: "thu",
time: "09:30",
title: "Quarterly business review",
hours: 2.5,
},
{
id: "h2",
day: "thu",
time: "13:30",
title: "Pricing workshop",
hours: 2,
movable: true,
},
{ id: "h3", day: "thu", time: "16:00", title: "Candidate debrief", hours: 1 },
{ id: "f1", day: "fri", time: "11:00", title: "Team lunch", hours: 1 },
{
id: "f2",
day: "fri",
time: "15:00",
title: "Release go/no-go",
hours: 0.5,
},
];
const chartConfig = {
meetings: { label: "Meetings", color: "var(--chart-1)" },
focus: { label: "Open for focus", color: "var(--chart-3)" },
} satisfies ChartConfig;
function formatHours(value: number) {
return `${value}h`;
}
export default function Chart15() {
const [meetings, setMeetings] = React.useState(initialMeetings);
const [selected, setSelected] = React.useState("tue");
const [moved, setMoved] = React.useState<{ id: string; from: string } | null>(
null,
);
const data = days.map((day) => {
const booked = meetings
.filter((meeting) => meeting.day === day.key)
.reduce((sum, meeting) => sum + meeting.hours, 0);
return { ...day, meetings: booked, focus: WORKDAY_HOURS - booked };
});
const day = data.find((item) => item.key === selected) ?? data[0];
const agenda = meetings
.filter((meeting) => meeting.day === day.key)
.sort((a, b) => a.time.localeCompare(b.time));
const lightest = data.reduce((min, item) =>
item.meetings < min.meetings ? item : min,
);
const candidate = agenda.find((meeting) => meeting.movable);
const belowGoal = day.focus < FOCUS_GOAL;
const daysBelowGoal = data.filter((item) => item.focus < FOCUS_GOAL).length;
function reschedule(meeting: Meeting) {
setMeetings((current) =>
current.map((item) =>
item.id === meeting.id
? { ...item, day: lightest.key, time: "14:00" }
: item,
),
);
setMoved({ id: meeting.id, from: meeting.day });
}
function undo() {
if (!moved) return;
const original = initialMeetings.find((item) => item.id === moved.id);
setMeetings((current) =>
current.map((item) =>
item.id === moved.id && original ? { ...original } : item,
),
);
setMoved(null);
}
const movedMeeting = moved
? meetings.find((meeting) => meeting.id === moved.id)
: undefined;
return (
<section
aria-labelledby="chart-15-title"
className="flex w-full max-w-xs flex-col gap-4 rounded-2xl border bg-card p-4 text-card-foreground"
>
<div className="flex items-baseline justify-between gap-2">
<h3 id="chart-15-title" className="font-semibold">
This week
</h3>
<p className="text-xs text-muted-foreground">
{daysBelowGoal === 0
? "Focus goal met every day"
: `${daysBelowGoal} ${daysBelowGoal === 1 ? "day" : "days"} under ${FOCUS_GOAL}h focus`}
</p>
</div>
<div className="flex flex-col gap-1.5">
<ChartContainer
config={chartConfig}
className="aspect-auto h-36 w-full"
role="img"
aria-label={`Booked meeting hours per day. ${data
.map(
(item) =>
`${item.label}: ${formatHours(item.meetings)} of meetings`,
)
.join("; ")}`}
>
<BarChart
data={data}
margin={{ left: 0, right: 0, top: 4, bottom: 0 }}
>
<XAxis dataKey="label" hide />
<YAxis hide domain={[0, WORKDAY_HOURS]} />
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(_, payload) => payload?.[0]?.payload?.date}
formatter={(value, name, item) => (
<div className="flex w-full items-center gap-2">
<span
aria-hidden="true"
className="size-2.5 rounded-[2px]"
style={{ backgroundColor: item.color }}
/>
<span className="text-muted-foreground">
{chartConfig[name as keyof typeof chartConfig]?.label}
</span>
<span className="ml-auto font-mono font-medium tabular-nums">
{formatHours(Number(value))}
</span>
</div>
)}
/>
}
/>
<Bar dataKey="meetings" stackId="day" fill="var(--color-meetings)">
{data.map((item) => (
<Cell
key={item.key}
fillOpacity={item.key === selected ? 1 : 0.4}
/>
))}
</Bar>
<Bar
dataKey="focus"
stackId="day"
fill="var(--color-focus)"
radius={[4, 4, 0, 0]}
>
{data.map((item) => (
<Cell
key={item.key}
fillOpacity={item.key === selected ? 1 : 0.4}
/>
))}
</Bar>
<ReferenceLine
y={WORKDAY_HOURS - FOCUS_GOAL}
stroke="var(--foreground)"
strokeOpacity={0.6}
strokeDasharray="3 3"
/>
</BarChart>
</ChartContainer>
<ToggleGroup
aria-label="Day"
spacing={0}
size="sm"
className="grid w-full grid-cols-5"
value={[selected]}
onValueChange={(value) => {
const next = value[0] as string | undefined;
if (next) setSelected(next);
}}
>
{data.map((item) => (
<ToggleGroupItem
key={item.key}
value={item.key}
aria-label={item.date}
className="w-full"
>
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-1 text-xs text-muted-foreground">
<span className="flex items-center gap-1.5">
<span
aria-hidden="true"
className="size-2 rounded-[2px] bg-chart-1"
/>
Meetings
</span>
<span className="flex items-center gap-1.5">
<span
aria-hidden="true"
className="size-2 rounded-[2px] bg-chart-3"
/>
Open
</span>
<span className="flex items-center gap-1.5">
<span
aria-hidden="true"
className="w-3 border-t border-dashed border-foreground/60"
/>
{FOCUS_GOAL}h focus line
</span>
</div>
</div>
<div className="flex flex-col gap-3 border-t pt-4">
<div className="flex items-baseline justify-between gap-2">
<h4 className="font-medium text-sm">{day.date}</h4>
<span className="text-xs text-muted-foreground tabular-nums">
{formatHours(day.focus)} open
</span>
</div>
<ul className="flex flex-col gap-2">
{agenda.map((meeting) => (
<li key={meeting.id} className="flex items-center gap-3 text-sm">
<span className="w-11 shrink-0 font-mono text-xs text-muted-foreground tabular-nums">
{meeting.time}
</span>
<span className="min-w-0 flex-1 truncate">{meeting.title}</span>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
{formatHours(meeting.hours)}
</span>
</li>
))}
</ul>
<div aria-live="polite" className="flex flex-col gap-2">
{movedMeeting && moved ? (
<div className="flex items-center justify-between gap-2 rounded-lg bg-muted px-3 py-2 text-xs">
<span className="text-muted-foreground">
{movedMeeting.title} moved to{" "}
{days.find((item) => item.key === movedMeeting.day)?.label}.
</span>
<Button size="xs" variant="ghost" onClick={undo}>
<Undo2 aria-hidden="true" data-icon="inline-start" />
Undo
</Button>
</div>
) : null}
{belowGoal && candidate && lightest.key !== day.key && !moved ? (
<Button
size="sm"
variant="secondary"
className="h-auto min-h-8 w-full py-1.5 whitespace-normal"
onClick={() => reschedule(candidate)}
>
<CalendarClock aria-hidden="true" data-icon="inline-start" />
Move {candidate.title} to {lightest.label}
</Button>
) : null}
</div>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/chart-15pnpm dlx shadcn@latest add @sevenui/component/chart-15yarn dlx shadcn@latest add @sevenui/component/chart-15bunx --bun shadcn@latest add @sevenui/component/chart-15"use client";
import * as React from "react";
import {
Bar,
BarChart,
type BarShapeProps,
LabelList,
XAxis,
YAxis,
} from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type CampaignKey = "autumn-launch" | "win-back" | "webinar-invite";
const stages = ["Delivered", "Opened", "Clicked", "Converted"] as const;
// Share of delivered emails that reached each stage, in percent.
const workspaceAverage = [100, 38, 6.2, 1.4];
const campaigns: Record<
CampaignKey,
{ label: string; sentOn: string; counts: number[] }
> = {
"autumn-launch": {
label: "Autumn collection launch",
sentOn: "Sep 9",
counts: [48_210, 21_694, 4_146, 1_012],
},
"win-back": {
label: "Win-back: 90 days inactive",
sentOn: "Sep 2",
counts: [12_870, 3_218, 541, 88],
},
"webinar-invite": {
label: "Webinar invite: pricing 101",
sentOn: "Aug 27",
counts: [9_402, 4_137, 1_128, 302],
},
};
const dropAdvice: Record<string, string> = {
Opened: "Test a stronger subject line and preview text.",
Clicked: "Test a clearer call to action above the fold.",
Converted: "Check that the landing page matches the email offer.",
};
const campaignItems = Object.entries(campaigns).map(([value, campaign]) => ({
value,
label: campaign.label,
}));
const chartConfig = {
rate: { label: "This campaign", color: "var(--chart-1)" },
average: { label: "Workspace average", color: "var(--foreground)" },
} satisfies ChartConfig;
// The workspace average is drawn as a thin marker at the end of its bar, on
// top of the campaign bar, so it stays visible whichever one is longer.
function AverageMarker({ x, y, width, height }: BarShapeProps) {
return (
<rect
x={Number(x) + Number(width) - 1.5}
y={Number(y) - 3}
width={3}
height={Number(height) + 6}
rx={1.5}
className="fill-(--color-average)"
/>
);
}
function percent(value: number) {
return `${value < 10 ? value.toFixed(1) : Math.round(value)}%`;
}
export default function Chart16() {
const [key, setKey] = React.useState<CampaignKey>("autumn-launch");
const campaign = campaigns[key];
const delivered = campaign.counts[0];
const data = stages.map((stage, index) => ({
stage,
count: campaign.counts[index],
rate: (campaign.counts[index] / delivered) * 100,
average: workspaceAverage[index],
}));
const biggestDrop = data.slice(1).reduce(
(worst, point, index) => {
const previous = data[index];
const kept = point.count / previous.count;
return kept < worst.kept
? { from: previous.stage, to: point.stage, kept }
: worst;
},
{ from: "", to: "", kept: 1 },
);
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Campaign funnel</CardTitle>
<CardDescription>
Sent {campaign.sentOn} · {delivered.toLocaleString("en-US")} delivered
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Select
items={campaignItems}
value={key}
onValueChange={(value) => setKey(value as CampaignKey)}
>
<SelectTrigger aria-label="Campaign" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{campaignItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<ChartContainer
config={chartConfig}
className="aspect-auto h-52 w-full"
role="img"
aria-label={`Funnel for ${campaign.label}: ${data
.map(
(point) =>
`${point.stage} ${percent(point.rate)} versus ${percent(point.average)} average`,
)
.join("; ")}`}
>
<BarChart
data={data}
layout="vertical"
margin={{ left: 0, right: 48 }}
barSize={22}
barGap={-22}
>
<XAxis type="number" hide domain={[0, 100]} />
<YAxis
type="category"
dataKey="stage"
width={72}
tickLine={false}
axisLine={false}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
indicator="line"
formatter={(value, name, item) => (
<div className="flex w-full items-center gap-2">
<span className="text-muted-foreground">
{chartConfig[name as keyof typeof chartConfig]?.label}
</span>
<span className="ml-auto font-mono font-medium tabular-nums">
{percent(Number(value))}
{name === "rate"
? ` · ${item.payload?.count.toLocaleString("en-US")}`
: ""}
</span>
</div>
)}
/>
}
/>
<Bar dataKey="rate" fill="var(--color-rate)" radius={4}>
<LabelList
dataKey="rate"
position="right"
offset={8}
className="fill-foreground font-medium tabular-nums"
fontSize={12}
formatter={(value) => percent(Number(value))}
/>
</Bar>
<Bar
dataKey="average"
fill="var(--color-average)"
shape={AverageMarker}
isAnimationActive={false}
/>
</BarChart>
</ChartContainer>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span className="flex items-center gap-1.5">
<span
aria-hidden="true"
className="size-2 rounded-[2px] bg-chart-1"
/>
This campaign
</span>
<span className="flex items-center gap-1.5">
<span
aria-hidden="true"
className="h-3 w-0.75 rounded-full bg-foreground"
/>
Workspace average
</span>
</div>
<p className="rounded-lg bg-muted px-3 py-2.5 text-sm text-muted-foreground">
Biggest drop:{" "}
<span className="font-medium text-foreground">
{biggestDrop.from} to {biggestDrop.to}
</span>
, only {percent(biggestDrop.kept * 100)} continued.{" "}
{dropAdvice[biggestDrop.to]}
</p>
</CardContent>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-16pnpm dlx shadcn@latest add @sevenui/component/chart-16yarn dlx shadcn@latest add @sevenui/component/chart-16bunx --bun shadcn@latest add @sevenui/component/chart-16"use client";
import { CircleCheck } from "lucide-react";
import * as React from "react";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ReferenceLine,
XAxis,
YAxis,
} from "recharts";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Label } from "@/components/ui/label";
import { Progress, ProgressLabel } from "@/components/ui/progress";
import { Slider } from "@/components/ui/slider";
// Dining and groceries spend imported from the linked checking account.
const spending = [
{ month: "Apr", amount: 612 },
{ month: "May", amount: 748 },
{ month: "Jun", amount: 695 },
{ month: "Jul", amount: 884 },
{ month: "Aug", amount: 657 },
{ month: "Sep", amount: 721 },
];
const MIN_BUDGET = 400;
const MAX_BUDGET = 1000;
const STEP = 25;
const chartConfig = {
amount: { label: "Spent", color: "var(--chart-2)" },
over: { label: "Over budget", color: "var(--warning)" },
} satisfies ChartConfig;
const usd = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
});
const average = Math.round(
spending.reduce((sum, item) => sum + item.amount, 0) / spending.length,
);
export default function Chart17() {
const [budget, setBudget] = React.useState(650);
// Step 4 is the confirmation: either a saved budget or a skipped step.
const [outcome, setOutcome] = React.useState<"saved" | "skipped" | null>(
null,
);
const overMonths = spending.filter((item) => item.amount > budget);
const yearlyGap = (average - budget) * 12;
let verdict: string;
if (overMonths.length === 0) {
verdict = "Comfortable. You stayed under this every month.";
} else if (overMonths.length <= 2) {
verdict = `Realistic. You went over in ${overMonths.map((item) => item.month).join(" and ")}.`;
} else {
verdict = `Ambitious. You went over in ${overMonths.length} of the last 6 months.`;
}
return (
<Card className="w-full max-w-md">
<CardHeader className="gap-3">
<Progress value={outcome ? 100 : 75} className="gap-1.5">
<ProgressLabel className="font-normal text-xs text-muted-foreground">
Step {outcome ? 4 : 3} of 4
</ProgressLabel>
</Progress>
<div className="flex flex-col gap-1">
<CardTitle>
{outcome === "saved"
? "Food budget saved"
: outcome === "skipped"
? "Food budget skipped"
: "Set a food budget"}
</CardTitle>
<CardDescription>
{outcome === "saved"
? `We will let you know when dining and groceries pass ${usd.format(budget)} in a month.`
: outcome === "skipped"
? "You can set a food budget later from Settings."
: `Based on your last 6 months of dining and groceries, averaging ${usd.format(average)} a month.`}
</CardDescription>
</div>
</CardHeader>
{outcome ? (
<CardContent>
<p
role="status"
className="flex items-start gap-2.5 rounded-lg bg-muted px-3 py-2.5 text-sm text-muted-foreground"
>
<CircleCheck
aria-hidden="true"
className="mt-0.5 size-4 shrink-0 text-success"
/>
{outcome === "saved"
? `${usd.format(budget)} a month.${yearlyGap > 0 ? ` Sticking to it would save about ${usd.format(yearlyGap)} a year.` : ""}`
: "No budget set. Spending is still tracked on your dashboard."}
</p>
</CardContent>
) : (
<CardContent className="flex flex-col gap-5">
<ChartContainer
config={chartConfig}
className="aspect-auto h-44 w-full"
role="img"
aria-label={`Monthly food spending against a ${usd.format(budget)} budget. Over budget in ${overMonths.length} of 6 months.`}
>
<BarChart data={spending} margin={{ left: 0, right: 8, top: 16 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<YAxis
width={52}
tickLine={false}
axisLine={false}
domain={[0, MAX_BUDGET]}
ticks={[0, 250, 500, 750, 1000]}
tickFormatter={(value: number) => usd.format(value)}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
hideIndicator
formatter={(value) => {
const diff = Number(value) - budget;
return (
<div className="grid w-full gap-1">
<div className="flex items-center justify-between gap-3">
<span className="text-muted-foreground">Spent</span>
<span className="font-mono font-medium tabular-nums">
{usd.format(Number(value))}
</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-muted-foreground">
{diff > 0 ? "Over" : "Under"}
</span>
<span className="font-mono font-medium tabular-nums">
{usd.format(Math.abs(diff))}
</span>
</div>
</div>
);
}}
/>
}
/>
<Bar dataKey="amount" radius={4} isAnimationActive={false}>
{spending.map((item) => (
<Cell
key={item.month}
fill={
item.amount > budget
? "var(--color-over)"
: "var(--color-amount)"
}
/>
))}
</Bar>
<ReferenceLine
y={budget}
stroke="var(--foreground)"
strokeWidth={1.5}
strokeDasharray="4 3"
label={{
value: usd.format(budget),
position: "insideTopRight",
fill: "var(--foreground)",
fontSize: 11,
}}
/>
</BarChart>
</ChartContainer>
<div className="flex flex-col gap-3">
<div className="flex items-baseline justify-between gap-2">
<Label id="chart-17-budget-label">Monthly budget</Label>
<output
htmlFor="chart-17-budget"
className="font-semibold text-lg tabular-nums"
>
{usd.format(budget)}
</output>
</div>
<Slider
id="chart-17-budget"
aria-labelledby="chart-17-budget-label"
min={MIN_BUDGET}
max={MAX_BUDGET}
step={STEP}
value={[budget]}
onValueChange={(value) =>
setBudget(typeof value === "number" ? value : value[0])
}
/>
<p className="text-sm text-muted-foreground" aria-live="polite">
{verdict}{" "}
{yearlyGap > 0
? `Sticking to it would save about ${usd.format(yearlyGap)} a year.`
: null}
</p>
</div>
</CardContent>
)}
<CardFooter className="justify-between gap-2 border-t">
{/* Steps 1 and 2 live outside this preview, so Back only returns from the confirmation. */}
<Button
variant="ghost"
size="sm"
disabled={!outcome}
onClick={() => setOutcome(null)}
>
Back
</Button>
{outcome ? null : (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setOutcome("skipped")}
>
Skip
</Button>
<Button size="sm" onClick={() => setOutcome("saved")}>
Save budget
</Button>
</div>
)}
</CardFooter>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-17pnpm dlx shadcn@latest add @sevenui/component/chart-17yarn dlx shadcn@latest add @sevenui/component/chart-17bunx --bun shadcn@latest add @sevenui/component/chart-17"use client";
import { cn } from "cn";
import { ArrowDownRight, ArrowUpRight } from "lucide-react";
import * as React from "react";
import {
Area,
CartesianGrid,
ComposedChart,
Line,
XAxis,
YAxis,
} from "recharts";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Metric = "revenue" | "orders" | "aov";
type Range = "30" | "90";
const metrics: {
key: Metric;
label: string;
format: (value: number) => string;
}[] = [
{
key: "revenue",
label: "Net revenue",
format: (value) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: value >= 100_000 ? "compact" : "standard",
maximumFractionDigits: value >= 100_000 ? 1 : 0,
}).format(value),
},
{
key: "orders",
label: "Orders",
format: (value) => Math.round(value).toLocaleString("en-US"),
},
{
key: "aov",
label: "Avg. order",
format: (value) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 2,
}).format(value),
},
];
// Deterministic daily store data for the last 90 days (ending Sep 24)
// and the 90 days before that, so the preview never changes between renders.
function buildDays() {
const end = new Date(2026, 8, 24);
return Array.from({ length: 90 }, (_, index) => {
const date = new Date(end);
date.setDate(end.getDate() - (89 - index));
const weekday = date.getDay();
const weekend = weekday === 0 || weekday === 6 ? 1.28 : 1;
const wave = Math.sin(index / 4.2) * 0.12 + Math.cos(index / 11) * 0.08;
const trend = 1 + index * 0.0042;
const orders = Math.round(142 * weekend * trend * (1 + wave));
const aov = 58 + Math.sin(index / 6.5) * 4.5 + index * 0.03;
const prevWave = Math.sin((index + 7) / 4.6) * 0.1;
const prevOrders = Math.round(
128 * weekend * (1 + index * 0.002) * (1 + prevWave),
);
const prevAov = 55.5 + Math.cos(index / 7) * 3.8;
return {
date: date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
}),
orders,
aov: Math.round(aov * 100) / 100,
revenue: Math.round(orders * aov),
prevOrders,
prevAov: Math.round(prevAov * 100) / 100,
prevRevenue: Math.round(prevOrders * prevAov),
};
});
}
const allDays = buildDays();
const chartConfig = {
current: { label: "This period", color: "var(--chart-1)" },
previous: { label: "Previous period", color: "var(--muted-foreground)" },
} satisfies ChartConfig;
function summarize(days: typeof allDays, metric: Metric) {
const orders = days.reduce((sum, day) => sum + day.orders, 0);
const revenue = days.reduce((sum, day) => sum + day.revenue, 0);
const prevOrders = days.reduce((sum, day) => sum + day.prevOrders, 0);
const prevRevenue = days.reduce((sum, day) => sum + day.prevRevenue, 0);
const totals = {
revenue: [revenue, prevRevenue],
orders: [orders, prevOrders],
aov: [revenue / orders, prevRevenue / prevOrders],
} satisfies Record<Metric, number[]>;
const [current, previous] = totals[metric];
return { current, change: ((current - previous) / previous) * 100 };
}
export default function Chart18() {
const [metric, setMetric] = React.useState<Metric>("revenue");
const [range, setRange] = React.useState<Range>("30");
const [compare, setCompare] = React.useState(true);
const days = React.useMemo(() => allDays.slice(-Number(range)), [range]);
const active = metrics.find((item) => item.key === metric) ?? metrics[0];
const previousKey = {
revenue: "prevRevenue",
orders: "prevOrders",
aov: "prevAov",
}[metric];
const data = days.map((day) => ({
date: day.date,
current: day[metric],
previous: day[previousKey as keyof typeof day],
}));
const activeSummary = summarize(days, metric);
return (
<Card className="w-full max-w-2xl">
<CardHeader>
<CardTitle>Store performance</CardTitle>
<CardDescription>
Online store · last {range} days vs. the {range} days before
</CardDescription>
<CardAction>
<ToggleGroup
aria-label="Date range"
variant="outline"
size="sm"
spacing={0}
value={[range]}
onValueChange={(value) => {
const next = value[0] as Range | undefined;
if (next) setRange(next);
}}
>
<ToggleGroupItem value="30">30d</ToggleGroupItem>
<ToggleGroupItem value="90">90d</ToggleGroupItem>
</ToggleGroup>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<fieldset
aria-label="Metric shown in chart"
className="m-0 grid min-w-0 grid-cols-1 gap-2 border-0 p-0 sm:grid-cols-3"
>
{metrics.map((item) => {
const summary = summarize(days, item.key);
const up = summary.change >= 0;
const selected = item.key === metric;
const TrendIcon = up ? ArrowUpRight : ArrowDownRight;
return (
<button
key={item.key}
type="button"
aria-pressed={selected}
onClick={() => setMetric(item.key)}
className={cn(
"flex flex-col items-start gap-1 rounded-lg border px-3 py-2.5 text-left outline-none transition-colors hover:bg-muted/60 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
selected && "border-primary/40 bg-muted",
)}
>
<span className="text-xs text-muted-foreground">
{item.label}
</span>
<span className="flex w-full items-baseline justify-between gap-2">
<span className="font-semibold text-lg tabular-nums">
{item.format(summary.current)}
</span>
<span
className={cn(
"flex items-center gap-0.5 text-xs font-medium tabular-nums",
up ? "text-success" : "text-destructive",
)}
>
<TrendIcon aria-hidden="true" className="size-3.5" />
{Math.abs(summary.change).toFixed(1)}%
<span className="sr-only">
{up ? "increase" : "decrease"} vs. previous period
</span>
</span>
</span>
</button>
);
})}
</fieldset>
<ChartContainer
config={chartConfig}
className="aspect-auto h-56 w-full"
role="img"
aria-label={`${active.label} per day over the last ${range} days: ${active.format(activeSummary.current)} total, ${activeSummary.change >= 0 ? "up" : "down"} ${Math.abs(activeSummary.change).toFixed(1)}% on the previous period`}
>
<ComposedChart data={data} margin={{ left: 0, right: 8, top: 8 }}>
<defs>
<linearGradient id="chart-18-fill" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="var(--color-current)"
stopOpacity={0.28}
/>
<stop
offset="100%"
stopColor="var(--color-current)"
stopOpacity={0}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
/>
<YAxis
width={52}
tickLine={false}
axisLine={false}
domain={
metric === "aov" ? ["dataMin - 4", "dataMax + 4"] : [0, "auto"]
}
tickFormatter={(value: number) =>
metric === "orders"
? String(value)
: new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
maximumFractionDigits: 1,
}).format(value)
}
/>
<ChartTooltip
content={
<ChartTooltipContent
indicator="line"
formatter={(value, name, item) => (
<div className="flex w-full items-center gap-2">
<span
aria-hidden="true"
className="h-2.5 w-1 rounded-[2px]"
style={{ backgroundColor: item.color }}
/>
<span className="text-muted-foreground">
{chartConfig[name as keyof typeof chartConfig]?.label}
</span>
<span className="ml-auto font-mono font-medium tabular-nums">
{active.format(Number(value))}
</span>
</div>
)}
/>
}
/>
<Area
dataKey="current"
type="monotone"
stroke="var(--color-current)"
strokeWidth={2}
fill="url(#chart-18-fill)"
dot={false}
/>
{compare ? (
<Line
dataKey="previous"
type="monotone"
stroke="var(--color-previous)"
strokeWidth={1.5}
strokeDasharray="4 4"
dot={false}
/>
) : null}
</ComposedChart>
</ChartContainer>
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1.5">
<span
aria-hidden="true"
className="h-0.5 w-3 rounded-full bg-chart-1"
/>
Last {range} days
</span>
{compare ? (
<span className="flex items-center gap-1.5">
<span
aria-hidden="true"
className="w-3 border-t border-dashed border-muted-foreground"
/>
Previous {range} days
</span>
) : null}
</div>
<div className="flex items-center gap-2">
<Switch
id="chart-18-compare"
size="sm"
checked={compare}
onCheckedChange={setCompare}
/>
<Label htmlFor="chart-18-compare" className="font-normal text-sm">
Compare to previous period
</Label>
</div>
</div>
</CardContent>
</Card>
);
}
npx shadcn@latest add @sevenui/component/chart-18pnpm dlx shadcn@latest add @sevenui/component/chart-18yarn dlx shadcn@latest add @sevenui/component/chart-18bunx --bun shadcn@latest add @sevenui/component/chart-18