traffic-overview-02
Fine website traffic line chart for dashboards: blue views and dashed gray reader lines, with a current value, date range control and ruled mono caption.
pnpm dlx shadcn add @01orbital/traffic-overview-02"use client"
import { useState } from "react"
import { Line, LineChart, ReferenceDot, XAxis, YAxis } from "recharts"
import { Eyebrow } from "@/components/eyebrow/eyebrow"
import {
Stat,
StatGroup,
StatValue,
} from "@/components/stats/stats"
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"
import { Separator } from "@/components/ui/separator"
// The copy: change text here, not in the markup.
const COPY = {
title: "Traffic overview",
range: "Date range",
latest: "latest views",
} as const
const SAMPLES = [
{ date: "2025-09-01", views: 2100, unique: 1300 },
{ date: "2025-09-04", views: 3700, unique: 2400 },
{ date: "2025-09-08", views: 2900, unique: 2000 },
{ date: "2025-09-10", views: 5300, unique: 3700 },
{ date: "2025-09-13", views: 4400, unique: 3300 },
{ date: "2025-09-15", views: 6800, unique: 5300 },
{ date: "2025-09-17", views: 6100, unique: 4900 },
{ date: "2025-09-18", views: 7600, unique: 5800 },
{ date: "2025-09-22", views: 6300, unique: 5200 },
{ date: "2025-09-26", views: 7200, unique: 5700 },
{ date: "2025-09-29", views: 8000, unique: 6900 },
{ date: "2025-10-01", views: 2600, unique: 1600 },
{ date: "2025-10-04", views: 4500, unique: 3000 },
{ date: "2025-10-08", views: 3500, unique: 2500 },
{ date: "2025-10-10", views: 6400, unique: 4500 },
{ date: "2025-10-13", views: 5400, unique: 4000 },
{ date: "2025-10-15", views: 8300, unique: 6500 },
{ date: "2025-10-17", views: 7400, unique: 6000 },
{ date: "2025-10-18", views: 9240, unique: 7105 },
{ date: "2025-10-22", views: 7700, unique: 6400 },
{ date: "2025-10-26", views: 8800, unique: 7000 },
{ date: "2025-10-29", views: 9800, unique: 8400 },
]
const RANGES = [
{
value: "last-7-days",
label: "Last 7 days",
start: "2025-10-24",
end: "2025-10-30",
description: "Views and unique readers, October 24–30.",
},
{
value: "last-30-days",
label: "Last 30 days",
start: "2025-10-01",
end: "2025-10-30",
description: "Views are up 12.4% on the previous 30 days.",
},
{
value: "last-month",
label: "Last month",
start: "2025-09-01",
end: "2025-09-30",
description: "Views and unique readers through September.",
},
] as const
const CHART_CONFIG = {
views: {
label: "Views",
color: "var(--art-accent)",
},
unique: {
label: "Unique readers",
color: "var(--muted-foreground)",
},
} 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 TrafficOverview02() {
const [rangeValue, setRangeValue] = useState<string>("last-30-days")
const range =
RANGES.find((option) => option.value === rangeValue) ?? RANGES[1]
const data = SAMPLES.filter(
(sample) => sample.date >= range.start && sample.date <= range.end
)
// The busiest day in the range gets a dot.
const peak = data.reduce(
(best, sample) => (sample.views > best.views ? sample : best),
data[0]
)
const latest = data[data.length - 1]
return (
<Card className="mx-auto w-full max-w-3xl gap-8 [--card-spacing:24px]">
<CardHeader className="has-data-[slot=card-action]:grid-cols-1">
<CardTitle>
<h2>{COPY.title}</h2>
</CardTitle>
<CardDescription>{range.description}</CardDescription>
<CardAction className="col-start-1 row-span-1 row-start-3 max-w-full min-w-0 justify-self-start pt-2">
<Select
items={RANGES}
value={rangeValue}
onValueChange={(next) => {
if (next) setRangeValue(next)
}}
>
<SelectTrigger
size="sm"
aria-label={COPY.range}
className="max-w-full whitespace-normal data-[size=sm]:h-auto *:data-[slot=select-value]:line-clamp-none"
>
<SelectValue className="min-w-0" />
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
{RANGES.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</CardAction>
</CardHeader>
<CardContent className="grid min-w-0 gap-8">
<StatGroup cols={1} gap="none">
<Stat className="justify-items-center gap-1 text-center">
<StatValue className="text-4xl/10 tabular-nums">
{latest.views.toLocaleString("en-US")}
</StatValue>
<Eyebrow variant="mono">{COPY.latest}</Eyebrow>
</Stat>
</StatGroup>
<figure className="grid min-w-0 gap-3">
<ChartContainer config={CHART_CONFIG} className="h-40 w-full min-w-0">
<LineChart accessibilityLayer data={data}>
<XAxis dataKey="date" hide />
<YAxis hide domain={[0, "auto"]} />
<ChartTooltip
content={
<ChartTooltipContent
labelFormatter={(label) => formatDate(String(label))}
/>
}
/>
<ChartLegend
content={<ChartLegendContent className="flex-wrap" />}
/>
<Line
dataKey="views"
type="linear"
stroke="var(--color-views)"
strokeWidth={1.5}
dot={false}
isAnimationActive={false}
/>
<Line
dataKey="unique"
type="linear"
stroke="var(--color-unique)"
strokeWidth={1.5}
strokeDasharray="4 6"
dot={false}
activeDot={false}
isAnimationActive={false}
/>
{peak && (
<ReferenceDot
x={peak.date}
y={peak.views}
r={3}
fill="var(--color-views)"
stroke="var(--color-views)"
/>
)}
</LineChart>
</ChartContainer>
<Separator />
<figcaption className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
<Eyebrow variant="mono" render={<span />}>
{range.label}
</Eyebrow>
<span className="text-sm/7 tabular-nums">
{formatDate(range.start)}–{formatDate(range.end)}
</span>
</figcaption>
</figure>
</CardContent>
</Card>
)
}
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
// default is a quiet 14px line, cased as written; icon puts a 16px icon in
// front of it; mono is a 12px uppercase mono label for spec-sheet designs;
// caps is the same size in a sans face. tone picks the ink: muted (the
// default) or the surrounding text color, for a label over art or on a dark
// island where muted-foreground is too quiet.
const eyebrowVariants = cva("", {
variants: {
/**
* The label style. `default` (the default) is a 14px line at weight 500,
* cased as written. `icon` is the same line with a 16px icon, placed
* first among the children, 12px before the words. `mono` is a 12px
* uppercase mono label at weight 500 with 0.12em letter spacing. `caps`
* is the same label in the sans face, for a brand and series label such
* as "Studio Neri / Clay school". The color comes from `tone`.
*/
variant: {
default: "text-sm/5 font-medium",
icon: "flex items-center gap-3 text-sm/5 font-medium [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
mono: "font-mono text-xs/5 font-medium tracking-[0.12em] uppercase",
caps: "text-xs/5 font-medium tracking-[0.12em] uppercase",
},
/**
* The ink. `muted` (the default) is `muted-foreground`. `ink` leaves the
* color to the surrounding text, for a label over art, on a dark island
* or anywhere the muted grey is too quiet to read.
*/
tone: {
muted: "text-muted-foreground",
ink: "",
},
},
defaultVariants: { variant: "default", tone: "muted" },
})
/**
* A short label set above a heading, such as a section name, rendered as a
* `<p>`. Leave 8px between it and the heading. Pass `render` to use another
* element.
*
* `default` is a 14px line at weight 500, cased as written. `icon` adds a
* 16px icon before the words: put the icon first, with `aria-hidden`. `mono`
* is a 12px uppercase mono label with 0.12em letter spacing; `caps` the same
* in the sans face, for a brand and series name such as "Studio Neri / Clay
* school". `tone="muted"` (the default) sets it in `muted-foreground`;
* `tone="ink"` takes the surrounding text color, for a label over art or on
* a dark island where the grey is too quiet.
*/
function Eyebrow({
className,
variant = "default",
tone = "muted",
render,
...props
}: useRender.ComponentProps<"p"> & VariantProps<typeof eyebrowVariants>) {
return useRender({
defaultTagName: "p",
render,
state: { slot: "eyebrow", variant, tone },
props: mergeProps<"p">(
{ className: cn(eyebrowVariants({ variant, tone }), className) },
props
),
})
}
export { Eyebrow, eyebrowVariants }
import type { ComponentProps } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { Grid, type GridProps } from "@/components/grid/grid"
/**
* A list of `Stat` items laid out by `Grid`, rendered as a `ul`. It defaults
* to 3 columns and the large gap. Like `Grid`, its columns follow the width
* it is given, so size its parent to narrow it. `className` lands on the
* list.
*/
function StatGroup({
cols = 3,
gap = "lg",
...props
}: ComponentProps<"ul"> & {
/**
* The most columns, 3 by default. They step up as the list's space
* widens, the way `Grid`'s do.
*/
cols?: GridProps["cols"]
/**
* Space between stats: `xs` 8px, `sm` 16px, `md` 24px, `lg` 40px, `xl` 64px, `px` 1px, `none` 0. `lg`
* is the default.
*/
gap?: GridProps["gap"]
}) {
return (
<Grid
data-slot="stat-group"
render={<ul role="list" />}
cols={cols}
gap={gap}
// Grid's props are typed for its default div; this group renders a list.
{...(props as Omit<GridProps, "cols" | "gap">)}
/>
)
}
// card sets the stat on a filled panel; rule sets a line down its left side.
const statVariants = cva("grid content-start gap-2", {
variants: {
/**
* The stat style. `default` (the default) adds nothing. `card` sets it
* on a `rounded-xl` panel with 32px padding, filled with `muted`, or
* with `card` and `card-foreground` text in dark mode. `rule` draws a
* 1px line in `border` down its left side, 24px from the text.
*/
variant: {
default: "",
card: "rounded-xl bg-muted p-8 dark:bg-card dark:text-card-foreground",
rule: "border-l border-border pl-6",
},
},
defaultVariants: { variant: "default" },
})
/**
* One figure and its label, as an `li` inside `StatGroup`. Holds a
* `StatValue` and a `StatLabel`, stacked with 8px gaps.
*
* `card` sets it on a filled, rounded panel with 32px padding. `rule` draws a
* line down its left side.
*/
function Stat({
className,
variant = "default",
...props
}: ComponentProps<"li"> & VariantProps<typeof statVariants>) {
return (
<li
data-slot="stat"
data-variant={variant}
className={cn(statVariants({ variant }), className)}
{...props}
/>
)
}
/** The figure itself, such as a number or a percentage, at 32px in the body font. */
function StatValue({ className, ...props }: ComponentProps<"p">) {
return (
<p
data-slot="stat-value"
className={cn("text-[2rem]/10 tracking-tight", className)}
{...props}
/>
)
}
/** The text that says what the figure measures. */
function StatLabel({ className, ...props }: ComponentProps<"p">) {
return (
<p
data-slot="stat-label"
className={cn("text-base/7 text-pretty", className)}
{...props}
/>
)
}
export { StatGroup, Stat, StatValue, StatLabel }
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
const GRID_DEFAULTS = { cols: 3, gap: "lg" } as const
// Columns follow the screen, stepping at 40rem and 48rem (and 64rem for six). Gap sizes read the shared --gap-*
// values in globals.css.
const containerGridVariants = cva("grid grid-cols-1", {
variants: {
/**
* The most columns. Below 40rem of screen width the grid is one
* column (two for `6`); it steps up at 40rem and 48rem: `2` is two
* columns from 40rem, `3` two then three, `4` two then four, `5` three
* then five, and `6` three, then four, then six from 64rem. The default
* is 3.
*/
cols: {
1: "",
2: "sm:grid-cols-2",
3: "sm:grid-cols-2 md:grid-cols-3",
4: "sm:grid-cols-2 md:grid-cols-4",
5: "sm:grid-cols-3 md:grid-cols-5",
6: "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6",
},
/**
* Space between items: `xs` 8px, `sm` 16px, `md` 24px, `lg` 40px, `xl` 64px, `px` 1px, `none` 0. `lg`
* is the default. The sizes are the `--gap-*` values in globals.css.
*/
gap: {
xs: "gap-(--gap-xs)",
sm: "gap-(--gap-sm)",
md: "gap-(--gap-md)",
lg: "gap-(--gap-lg)",
xl: "gap-(--gap-xl)",
px: "gap-px",
none: "gap-0",
},
},
defaultVariants: GRID_DEFAULTS,
})
type GridState = { slot: "grid" } & VariantProps<typeof containerGridVariants>
type GridProps = useRender.ComponentProps<"div", GridState> &
VariantProps<typeof containerGridVariants>
// className, render and every other prop go to the grid.
function Grid({
className,
cols = GRID_DEFAULTS.cols,
gap = GRID_DEFAULTS.gap,
render,
...props
}: GridProps) {
const grid = useRender({
defaultTagName: "div",
render,
state: { slot: "grid", cols, gap },
props: mergeProps<"div">(
{ className: cn(containerGridVariants({ cols, gap }), className) },
props
),
})
return grid
}
export { Grid, type GridProps }
Dependencies
npm: recharts@3.8.0. The CLI installs them for you.
Registry pieces it installs with it: