Area Chart

Composable area chart with gradients, animations, and interactive tooltips

Installation

pnpm dlx shadcn@latest add "https://vritti.thesatyajit.com/r/area-chart"

Dependencies

pnpm add @visx/responsive @visx/scale @visx/shape @visx/gradient @visx/group @visx/axis @visx/event @visx/grid d3-array motion react-use-measure

Examples

Gradient

"use client"
import AreaChart, { Area, Grid, XAxis, YAxis, ChartTooltip } from "@/components/vritti/area-chart"

const data = [
  { date: new Date(2024, 0, 1), revenue: 4200 },
  { date: new Date(2024, 1, 1), revenue: 5800 },
  { date: new Date(2024, 2, 1), revenue: 5100 },
  { date: new Date(2024, 3, 1), revenue: 7400 },
  { date: new Date(2024, 4, 1), revenue: 6200 },
  { date: new Date(2024, 5, 1), revenue: 8900 },
  { date: new Date(2024, 6, 1), revenue: 7800 },
  { date: new Date(2024, 7, 1), revenue: 9500 },
  { date: new Date(2024, 8, 1), revenue: 8600 },
  { date: new Date(2024, 9, 1), revenue: 11200 },
  { date: new Date(2024, 10, 1), revenue: 10400 },
  { date: new Date(2024, 11, 1), revenue: 12800 },
]

export function AreaChartGradientDemo() {
  return (
    <div className="w-full p-4">
      <AreaChart data={data}>
        <Grid horizontal />
        <Area
          dataKey="revenue"
          fill="hsl(217, 91%, 60%)"
          stroke="hsl(217, 91%, 60%)"
          fillOpacity={0.6}
          gradientToOpacity={0.05}
          strokeWidth={2}
        />
        <XAxis />
        <YAxis />
        <ChartTooltip />
      </AreaChart>
    </div>
  )
}

Legend

"use client"
import AreaChart, { Area, Grid, XAxis, YAxis, ChartTooltip, Legend, LegendItem, LegendMarker, LegendLabel } from "@/components/vritti/area-chart"

const data = [
  { date: new Date(2024, 0, 1), income: 4800, expenses: 3200 },
  { date: new Date(2024, 1, 1), income: 5200, expenses: 3500 },
  { date: new Date(2024, 2, 1), income: 4900, expenses: 3800 },
  { date: new Date(2024, 3, 1), income: 6100, expenses: 3400 },
  { date: new Date(2024, 4, 1), income: 5700, expenses: 4100 },
  { date: new Date(2024, 5, 1), income: 6800, expenses: 3900 },
  { date: new Date(2024, 6, 1), income: 7200, expenses: 4300 },
  { date: new Date(2024, 7, 1), income: 6500, expenses: 4000 },
]

const legendItems = [
  { label: "Income", value: 0, color: "hsl(150, 60%, 50%)" },
  { label: "Expenses", value: 0, color: "hsl(0, 75%, 60%)" },
]

export function AreaChartLegendDemo() {
  return (
    <div className="w-full p-4">
      <AreaChart data={data}>
        <Grid horizontal />
        <Area
          dataKey="income"
          fill="hsl(150, 60%, 50%)"
          stroke="hsl(150, 60%, 50%)"
          fillOpacity={0.3}
          gradientToOpacity={0}
        />
        <Area
          dataKey="expenses"
          fill="hsl(0, 75%, 60%)"
          stroke="hsl(0, 75%, 60%)"
          fillOpacity={0.3}
          gradientToOpacity={0}
        />
        <XAxis />
        <YAxis />
        <ChartTooltip />
      </AreaChart>
      <Legend className="flex-row justify-center gap-6" items={legendItems}>
        <LegendItem className="flex items-center gap-2">
          <LegendMarker />
          <LegendLabel />
        </LegendItem>
      </Legend>
    </div>
  )
}

Multi

"use client"
import AreaChart, { Area, Grid, XAxis, YAxis, ChartTooltip, Legend, LegendItem, LegendMarker, LegendLabel } from "@/components/vritti/area-chart"

const data = [
  { date: new Date(2024, 0, 1), desktop: 186, mobile: 80 },
  { date: new Date(2024, 1, 1), desktop: 305, mobile: 200 },
  { date: new Date(2024, 2, 1), desktop: 237, mobile: 120 },
  { date: new Date(2024, 3, 1), desktop: 73, mobile: 190 },
  { date: new Date(2024, 4, 1), desktop: 209, mobile: 130 },
  { date: new Date(2024, 5, 1), desktop: 214, mobile: 140 },
]

const legendItems = [
  { label: "Desktop", value: 0, color: "hsl(217, 91%, 60%)" },
  { label: "Mobile", value: 0, color: "hsl(280, 87%, 65%)" },
]

export function AreaChartMultiDemo() {
  return (
    <div className="w-full p-4">
      <AreaChart data={data}>
        <Grid horizontal />
        <Area dataKey="desktop" fill="hsl(217, 91%, 60%)" stroke="hsl(217, 91%, 60%)" />
        <Area dataKey="mobile" fill="hsl(280, 87%, 65%)" stroke="hsl(280, 87%, 65%)" />
        <XAxis />
        <YAxis />
        <ChartTooltip />
      </AreaChart>
      <Legend className="flex-row justify-center gap-6" items={legendItems}>
        <LegendItem className="flex items-center gap-2">
          <LegendMarker />
          <LegendLabel />
        </LegendItem>
      </Legend>
    </div>
  )
}

Range

"use client"
import AreaChart, { Area, Grid, XAxis, YAxis, ChartTooltip } from "@/components/vritti/area-chart"

const data = [
  { date: new Date(2024, 0, 1), avg: 42, max: 58, min: 28 },
  { date: new Date(2024, 1, 1), avg: 48, max: 65, min: 34 },
  { date: new Date(2024, 2, 1), avg: 55, max: 72, min: 40 },
  { date: new Date(2024, 3, 1), avg: 62, max: 78, min: 48 },
  { date: new Date(2024, 4, 1), avg: 70, max: 88, min: 55 },
  { date: new Date(2024, 5, 1), avg: 78, max: 95, min: 64 },
  { date: new Date(2024, 6, 1), avg: 82, max: 98, min: 68 },
  { date: new Date(2024, 7, 1), avg: 80, max: 96, min: 66 },
  { date: new Date(2024, 8, 1), avg: 72, max: 86, min: 58 },
  { date: new Date(2024, 9, 1), avg: 60, max: 74, min: 46 },
  { date: new Date(2024, 10, 1), avg: 50, max: 64, min: 36 },
  { date: new Date(2024, 11, 1), avg: 44, max: 58, min: 30 },
]

export function AreaChartRangeDemo() {
  return (
    <div className="w-full p-4">
      <AreaChart data={data}>
        <Grid horizontal />
        <Area
          dataKey="max"
          fill="hsl(217, 91%, 60%)"
          stroke="hsl(217, 91%, 60%)"
          fillOpacity={0.15}
          gradientToOpacity={0.15}
          strokeWidth={1}
          showHighlight={false}
        />
        <Area
          dataKey="min"
          fill="var(--chart-background, hsl(0, 0%, 100%))"
          stroke="hsl(217, 91%, 60%)"
          fillOpacity={1}
          gradientToOpacity={1}
          strokeWidth={1}
          showHighlight={false}
        />
        <Area
          dataKey="avg"
          fill="transparent"
          stroke="hsl(217, 91%, 60%)"
          fillOpacity={0}
          strokeWidth={2}
        />
        <XAxis />
        <YAxis />
        <ChartTooltip
          rows={(point) => [
            { color: "hsl(217, 91%, 60%)", label: "Max", value: (point.max as number) ?? 0 },
            { color: "hsl(217, 71%, 50%)", label: "Avg", value: (point.avg as number) ?? 0 },
            { color: "hsl(217, 91%, 75%)", label: "Min", value: (point.min as number) ?? 0 },
          ]}
        />
      </AreaChart>
    </div>
  )
}

Segment

"use client"
import { useCallback, useEffect, useState } from "react"
import AreaChart, {
  Area,
  ChartTooltip,
  Grid,
  SegmentBackground,
  SegmentLineFrom,
  SegmentLineTo,
  XAxis,
  useChart,
} from "@/components/vritti/area-chart"

const chartData = [
  { date: new Date(Date.now() - 29 * 24 * 60 * 60 * 1000), users: 1200 },
  { date: new Date(Date.now() - 27 * 24 * 60 * 60 * 1000), users: 1100 },
  { date: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000), users: 1380 },
  { date: new Date(Date.now() - 23 * 24 * 60 * 60 * 1000), users: 1600 },
  { date: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000), users: 1550 },
  { date: new Date(Date.now() - 19 * 24 * 60 * 60 * 1000), users: 1680 },
  { date: new Date(Date.now() - 17 * 24 * 60 * 60 * 1000), users: 1620 },
  { date: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), users: 1720 },
  { date: new Date(Date.now() - 13 * 24 * 60 * 60 * 1000), users: 1780 },
  { date: new Date(Date.now() - 11 * 24 * 60 * 60 * 1000), users: 1920 },
  { date: new Date(Date.now() - 9 * 24 * 60 * 60 * 1000), users: 1750 },
  { date: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), users: 2050 },
  { date: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), users: 2100 },
  { date: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), users: 2050 },
  { date: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), users: 2320 },
  { date: new Date(), users: 2400 },
]

interface SegmentStats {
  value: number
  change: number
  changePct: number
  startDate: Date
  endDate: Date
}

function SegmentBridge({
  onSegmentChange,
}: {
  onSegmentChange: (stats: SegmentStats | null) => void
}) {
  const { selection, data, xAccessor } = useChart()

  useEffect(() => {
    if (!selection?.active) {
      onSegmentChange(null)
      return
    }

    const startIdx = Math.max(0, selection.startIndex)
    const endIdx = Math.min(data.length - 1, selection.endIndex)
    if (startIdx >= endIdx) {
      onSegmentChange(null)
      return
    }

    const startPoint = data[startIdx] as { users?: number }
    const endPoint = data[endIdx] as { users?: number }
    if (!(startPoint && endPoint)) {
      onSegmentChange(null)
      return
    }

    const startVal = startPoint.users
    const endVal = endPoint.users
    if (typeof startVal !== "number" || typeof endVal !== "number") {
      onSegmentChange(null)
      return
    }

    onSegmentChange({
      value: endVal,
      change: endVal - startVal,
      changePct: startVal !== 0 ? ((endVal - startVal) / startVal) * 100 : 0,
      startDate: xAccessor(data[startIdx]),
      endDate: xAccessor(data[endIdx]),
    })
  }, [selection, data, xAccessor, onSegmentChange])

  return null
}

export function AreaChartSegmentDemo() {
  const [stats, setStats] = useState<SegmentStats | null>(null)
  const handleSegmentChange = useCallback(
    (s: SegmentStats | null) => setStats(s),
    []
  )

  const firstVal = chartData[0]?.users ?? 0
  const lastVal = chartData.at(-1)?.users ?? 0

  const displayValue = stats?.value ?? lastVal
  const displayChange = stats?.change ?? lastVal - firstVal
  const displayPct =
    stats?.changePct ??
    (firstVal > 0 ? ((lastVal - firstVal) / firstVal) * 100 : 0)
  const isPositive = displayChange >= 0

  const startLabel = (
    stats?.startDate ?? chartData[0]?.date
  )?.toLocaleDateString("en-US", { month: "short", day: "numeric" })
  const endLabel = (
    stats?.endDate ?? chartData.at(-1)?.date
  )?.toLocaleDateString("en-US", { month: "short", day: "numeric" })

  return (
    <div className="w-full p-4">
      <div className="mb-4 space-y-1">
        <div className="flex items-baseline gap-2">
          <span className="font-semibold text-2xl tabular-nums">
            {displayValue.toLocaleString()}
          </span>
          <span className="text-muted-foreground text-sm">users</span>
        </div>
        <div className="flex items-baseline gap-2 text-sm">
          <span className={isPositive ? "text-emerald-500" : "text-red-500"}>
            {isPositive ? "+" : ""}
            {displayChange.toLocaleString()} ({isPositive ? "+" : ""}
            {displayPct.toFixed(1)}%)
          </span>
          <span className="text-muted-foreground">
            {startLabel} &ndash; {endLabel}
          </span>
        </div>
      </div>

      <AreaChart data={chartData}>
        <Grid horizontal />
        <Area
          dataKey="users"
          fill="var(--chart-line-primary)"
          stroke="var(--chart-line-primary)"
        />
        <SegmentBackground />
        <SegmentLineFrom />
        <SegmentLineTo />
        <XAxis />
        <ChartTooltip />
        <SegmentBridge onSegmentChange={handleSegmentChange} />
      </AreaChart>
    </div>
  )
}

Sparkline

"use client"
import AreaChart, { Area } from "@/components/vritti/area-chart"

const data = [
  { date: new Date(2024, 0, 1), value: 22 },
  { date: new Date(2024, 1, 1), value: 35 },
  { date: new Date(2024, 2, 1), value: 28 },
  { date: new Date(2024, 3, 1), value: 42 },
  { date: new Date(2024, 4, 1), value: 38 },
  { date: new Date(2024, 5, 1), value: 55 },
  { date: new Date(2024, 6, 1), value: 48 },
  { date: new Date(2024, 7, 1), value: 62 },
  { date: new Date(2024, 8, 1), value: 58 },
  { date: new Date(2024, 9, 1), value: 70 },
  { date: new Date(2024, 10, 1), value: 65 },
  { date: new Date(2024, 11, 1), value: 78 },
]

export function AreaChartSparklineDemo() {
  return (
    <div className="w-full p-4" style={{ maxWidth: 200 }}>
      <AreaChart
        data={data}
        aspectRatio="3 / 1"
        margin={{ top: 4, right: 4, bottom: 4, left: 4 }}
        animationDuration={800}
      >
        <Area
          dataKey="value"
          fill="hsl(217, 91%, 60%)"
          stroke="hsl(217, 91%, 60%)"
          fillOpacity={0.3}
          gradientToOpacity={0.05}
          strokeWidth={1.5}
          showHighlight={false}
        />
      </AreaChart>
    </div>
  )
}

Stacked

"use client"
import AreaChart, { Area, Grid, XAxis, YAxis, ChartTooltip } from "@/components/vritti/area-chart"

const data = [
  { date: new Date(2024, 0, 1), organic: 120, direct: 90, referral: 45 },
  { date: new Date(2024, 1, 1), organic: 180, direct: 110, referral: 60 },
  { date: new Date(2024, 2, 1), organic: 150, direct: 130, referral: 75 },
  { date: new Date(2024, 3, 1), organic: 210, direct: 95, referral: 50 },
  { date: new Date(2024, 4, 1), organic: 190, direct: 140, referral: 85 },
  { date: new Date(2024, 5, 1), organic: 240, direct: 120, referral: 70 },
  { date: new Date(2024, 6, 1), organic: 220, direct: 160, referral: 90 },
  { date: new Date(2024, 7, 1), organic: 260, direct: 135, referral: 65 },
]

// Pre-compute stacked values so each area renders on top of the previous
const stackedData = data.map((d) => ({
  ...d,
  referralStacked: d.referral,
  directStacked: d.referral + d.direct,
  organicStacked: d.referral + d.direct + d.organic,
}))

export function AreaChartStackedDemo() {
  return (
    <div className="w-full p-4">
      <AreaChart data={stackedData}>
        <Grid horizontal />
        <Area
          dataKey="organicStacked"
          fill="hsl(217, 91%, 60%)"
          stroke="hsl(217, 91%, 60%)"
          fillOpacity={0.3}
          showHighlight={false}
        />
        <Area
          dataKey="directStacked"
          fill="hsl(280, 87%, 65%)"
          stroke="hsl(280, 87%, 65%)"
          fillOpacity={0.3}
          showHighlight={false}
        />
        <Area
          dataKey="referralStacked"
          fill="hsl(150, 60%, 50%)"
          stroke="hsl(150, 60%, 50%)"
          fillOpacity={0.3}
          showHighlight={false}
        />
        <XAxis />
        <YAxis />
        <ChartTooltip
          rows={(point) => [
            { color: "hsl(217, 91%, 60%)", label: "Organic", value: (point.organic as number) ?? 0 },
            { color: "hsl(280, 87%, 65%)", label: "Direct", value: (point.direct as number) ?? 0 },
            { color: "hsl(150, 60%, 50%)", label: "Referral", value: (point.referral as number) ?? 0 },
          ]}
        />
      </AreaChart>
    </div>
  )
}

Step

"use client"
import AreaChart, { Area, Grid, XAxis, YAxis, ChartTooltip } from "@/components/vritti/area-chart"
import { curveStepAfter } from "@visx/curve"

const data = [
  { date: new Date(2024, 0, 1), servers: 4 },
  { date: new Date(2024, 1, 1), servers: 4 },
  { date: new Date(2024, 2, 1), servers: 6 },
  { date: new Date(2024, 3, 1), servers: 6 },
  { date: new Date(2024, 4, 1), servers: 10 },
  { date: new Date(2024, 5, 1), servers: 10 },
  { date: new Date(2024, 6, 1), servers: 8 },
  { date: new Date(2024, 7, 1), servers: 12 },
  { date: new Date(2024, 8, 1), servers: 12 },
  { date: new Date(2024, 9, 1), servers: 15 },
  { date: new Date(2024, 10, 1), servers: 15 },
  { date: new Date(2024, 11, 1), servers: 18 },
]

export function AreaChartStepDemo() {
  return (
    <div className="w-full p-4">
      <AreaChart data={data}>
        <Grid horizontal />
        <Area
          dataKey="servers"
          fill="hsl(280, 87%, 65%)"
          stroke="hsl(280, 87%, 65%)"
          fillOpacity={0.3}
          gradientToOpacity={0.05}
          curve={curveStepAfter}
        />
        <XAxis />
        <YAxis />
        <ChartTooltip />
      </AreaChart>
    </div>
  )
}

Tooltip

"use client"
import AreaChart, { Area, ChartTooltip, Grid, XAxis, YAxis } from "@/components/vritti/area-chart"

const data = [
  { date: new Date(2024, 0, 1), revenue: 12000, costs: 7500 },
  { date: new Date(2024, 1, 1), revenue: 15500, costs: 9200 },
  { date: new Date(2024, 2, 1), revenue: 11000, costs: 6800 },
  { date: new Date(2024, 3, 1), revenue: 18500, costs: 11100 },
  { date: new Date(2024, 4, 1), revenue: 16800, costs: 10400 },
  { date: new Date(2024, 5, 1), revenue: 21200, costs: 12800 },
]

export function AreaChartTooltipDemo() {
  return (
    <div className="w-full p-4">
      <AreaChart data={data}>
        <Grid horizontal />
        <Area
          dataKey="revenue"
          fill="var(--chart-line-primary)"
          stroke="var(--chart-line-primary)"
        />
        <Area
          dataKey="costs"
          fill="var(--chart-line-secondary)"
          stroke="var(--chart-line-secondary)"
        />
        <XAxis />
        <YAxis />
        <ChartTooltip
          rows={(point) => [
            {
              color: "var(--chart-line-primary)",
              label: "Revenue",
              value: `$${(point.revenue as number)?.toLocaleString() ?? 0}`,
            },
            {
              color: "var(--chart-line-secondary)",
              label: "Costs",
              value: `$${(point.costs as number)?.toLocaleString() ?? 0}`,
            },
          ]}
        />
      </AreaChart>
    </div>
  )
}