Card with a two-series area chart of daily listeners, desktop solid and mobile dashed, a select for 90, 30 or 7 days and a legend that draws the dash. Client component on recharts 3.8.
"use client"
import { useState } from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
// The copy: change text here, not in the markup.
const COPY = {
title: "Listeners",
range: "Date range",
} as const
const RANGES = [
{
value: "90",
label: "Last 3 months",
description: "Daily listeners for the last 3 months",
},
{
value: "30",
label: "Last 30 days",
description: "Daily listeners for the last 30 days",
},
{
value: "7",
label: "Last 7 days",
description: "Daily listeners for the last 7 days",
},
] as const
const START = Date.UTC(2026, 6, 19)
const DAY = 86_400_000
const LISTENERS = Array.from({ length: 90 }, (_, day) => {
const wave = Math.sin(day / 3) * 70 + Math.sin(day / 9) * 45
return {
date: new Date(START + day * DAY).toISOString().slice(0, 10),
desktop: Math.round(320 + day * 1.8 + wave),
mobile: Math.round(210 + day * 1.4 - wave * 0.7),
}
})
// chart-2 and chart-3 both clear 3:1 on the card in light and dark.
const CHART_CONFIG = {
desktop: { label: "Desktop", color: "var(--chart-2)", icon: DesktopKey },
mobile: { label: "Mobile", color: "var(--chart-3)", icon: MobileKey },
} satisfies ChartConfig
const DATE_FORMAT = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
timeZone: "UTC",
})
const formatDate = (value: string) =>
DATE_FORMAT.format(new Date(`${value}T00:00:00Z`))
export default function AreaChart01() {
const [rangeValue, setRangeValue] = useState<string>("90")
const range =
RANGES.find((option) => option.value === rangeValue) ?? RANGES[0]
const data = LISTENERS.slice(-Number(range.value))
return (
<Card className="mx-auto w-full max-w-3xl">
<CardHeader>
<CardTitle>
<h2>{COPY.title}</h2>
</CardTitle>
<CardDescription>{range.description}</CardDescription>
<CardAction>
<Select
items={RANGES}
value={rangeValue}
onValueChange={(next) => {
if (next) setRangeValue(next)
}}
>
<SelectTrigger size="sm" aria-label={COPY.range}>
<SelectValue />
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
{RANGES.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</CardAction>
</CardHeader>
<CardContent>
<ChartContainer config={CHART_CONFIG} className="h-72 w-full">
<AreaChart accessibilityLayer data={data}>
<CartesianGrid vertical={false} />
{/* Stock ChartContainer's label color misses recharts 3's label layer. */}
<XAxis
tick={{ fill: "var(--muted-foreground)" }}
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={formatDate}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
indicator="dot"
labelFormatter={(label) => formatDate(String(label))}
/>
}
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.15}
stroke="var(--color-desktop)"
strokeWidth={2}
/>
{/* The dash, not the color, tells mobile from desktop. */}
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.1}
stroke="var(--color-mobile)"
strokeWidth={2}
strokeDasharray="5 4"
/>
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}
// The legend draws each line as it is drawn in the chart, dash and all.
function DesktopKey() {
return (
<svg viewBox="0 0 12 12" aria-hidden="true">
<line
x1="0"
y1="6"
x2="12"
y2="6"
stroke="var(--color-desktop)"
strokeWidth="2"
/>
</svg>
)
}
function MobileKey() {
return (
<svg viewBox="0 0 12 12" aria-hidden="true">
<line
x1="0"
y1="6"
x2="12"
y2="6"
stroke="var(--color-mobile)"
strokeWidth="2"
strokeDasharray="5 4"
/>
</svg>
)
}
pnpm dlx shadcn add @01orbital/area-chart-01npx shadcn add @01orbital/area-chart-01yarn shadcn add @01orbital/area-chart-01bunx --bun shadcn add @01orbital/area-chart-01Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/blocks/charts/area-chart-01/index.tsx, as source you own and edit.
import AreaChart01 from "@/components/blocks/charts/area-chart-01"
export default function Example() {
return <AreaChart01 />
}npm: recharts@3.8.0. The CLI installs them for you.
Registry pieces it installs with it:
cardchartselectLabelled Line Chart Card
Card with a single-series line chart of average session minutes by month, each point labelled with its value, and a footer note on the trend. Client component on recharts 3.8.
Listening Moods Radar Chart
Card with a radar chart scoring six listening moods out of 100, each axis label carrying its score so the values read without the shape. Client component on recharts 3.8.