# Listeners Area Chart Card

> 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.

A shadcn/ui block from the [@01orbital](https://01orbital.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/01orbital/area-chart-01).

- Page, with a live preview: https://shadcn.exchange/r/01orbital/area-chart-01
- Screenshot: https://shadcn.exchange/shot/01orbital/area-chart-01/light.png
- Registry item: https://01orbital.dev/r/area-chart-01.json
- Categories: charts
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/01orbital/area-chart-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `recharts`
- Registry items: `card`, `@01orbital/chart`, `@01orbital/select`

## Files

### `blocks/charts/area-chart-01/index.tsx`

```tsx
"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>
  )
}
```
