# Area Interactive

> An interactive network bandwidth and throughput area chart with time range select built with TanStack Charts.

A shadcn/ui block from the [@paceui](https://paceui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/paceui/tanstack-charts-area-interactive).

- Page, with a live preview: https://shadcn.exchange/r/paceui/tanstack-charts-area-interactive
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/paceui/tanstack-charts-area-interactive-light-750543c68e59fa63.png
- Registry item: https://paceui.com/r/tanstack-charts-area-interactive.json
- Listed: 2026-10-21, source last changed 2026-10-21

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/paceui/tanstack-charts-area-interactive.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: `@tanstack/charts`, `@tanstack/react-charts`, `d3-array`, `d3-scale`, `d3-shape`
- Registry items: `card`, `select`, `chart`

## Files

### `components/tanstack-charts/area/area-interactive.tsx`

```tsx
import { areaY, defineChart, lineY } from "@tanstack/charts";
import { d3Curve } from "@tanstack/charts/d3/shape";
import { motion } from "@tanstack/charts/motion";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo, useState } from "react";

import { extent, max } from "d3-array";
import { scaleLinear, scaleTime } from "d3-scale";
import { curveMonotoneX } from "d3-shape";

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import type { ChartConfig } from "@/components/ui/chart";

const startTime = new Date(2026, 6, 1, 0, 0, 0).getTime();
const HOUR_MS = 60 * 60 * 1000;

const chartData = Array.from({ length: 72 }, (_, i) => {
    const date = new Date(startTime + i * HOUR_MS);
    const hour = date.getHours();

    const diurnalCycle = Math.sin(((hour - 6) / 24) * 2 * Math.PI) * 0.3 + 0.7;

    const microNoise = Math.sin(i * 0.7) * 4 + Math.cos(i * 1.3) * 6;
    const periodicSpike = i % 12 === 0 ? 15 : 0;

    const desktop = Math.round((250 + microNoise * 10 + periodicSpike * 5) * diurnalCycle + 120);
    const mobile = Math.round((180 + microNoise * 8 + periodicSpike * 3) * diurnalCycle + 90);

    return {
        date,
        desktop,
        mobile,
    };
});

const chartConfig = {
    desktop: {
        label: "Desktop",
        color: "var(--chart-1)",
    },
    mobile: {
        label: "Mobile",
        color: "var(--chart-2)",
    },
} satisfies ChartConfig;

const formatTooltipDate = (date: unknown) => {
    if (!(date instanceof Date)) return "";
    return date.toLocaleDateString("en-US", {
        month: "short",
        day: "numeric",
        hour: "numeric",
        minute: "2-digit",
    });
};

type MetricTab = "combined" | "desktop" | "mobile";

const motionRenderer = motion({
    initial: "always",
    transition: { type: "spring", stiffness: 140, damping: 16, mass: 0.8 },
});

export const Demo = () => {
    const [activeTab, setActiveTab] = useState<MetricTab>("combined");

    const stats = useMemo(() => {
        const totalDesktop = chartData.reduce((acc, curr) => acc + curr.desktop, 0);
        const totalMobile = chartData.reduce((acc, curr) => acc + curr.mobile, 0);

        return {
            combined: Math.round((totalDesktop + totalMobile) / chartData.length),
            desktop: Math.round(totalDesktop / chartData.length),
            mobile: Math.round(totalMobile / chartData.length),
        };
    }, []);

    const xDomain = useMemo(() => extent(chartData, (d) => d.date) as [Date, Date], []);
    const fixedYMax = useMemo(() => {
        const peak = max(chartData, (d) => Math.max(d.desktop, d.mobile)) ?? 350;
        return Math.ceil(peak / 50) * 50;
    }, []);

    const definition = useMemo(() => {
        const getDesktopY = (d: (typeof chartData)[number]) => {
            if (activeTab === "mobile") return d.mobile;
            return d.desktop;
        };

        const getMobileY = (d: (typeof chartData)[number]) => {
            if (activeTab === "desktop") return d.desktop;
            return d.mobile;
        };

        const getDesktopColor = () => {
            if (activeTab === "mobile") return chartConfig.mobile.color;
            return chartConfig.desktop.color;
        };

        const getMobileColor = () => {
            if (activeTab === "desktop") return chartConfig.desktop.color;
            return chartConfig.mobile.color;
        };

        const marks = [
            // Desktop Area
            areaY(chartData, {
                id: "desktop-area",
                key: (d) => d.date.toISOString(),
                x: (d) => d.date,
                y: getDesktopY,
                fill: getDesktopColor(),
                fillOpacity: 0.35,
                curve: d3Curve(curveMonotoneX),
            }),
            // Desktop Line
            lineY(chartData, {
                id: "desktop-line",
                key: (d) => d.date.toISOString(),
                x: (d) => d.date,
                y: getDesktopY,
                stroke: getDesktopColor(),
                strokeWidth: 2,
                curve: d3Curve(curveMonotoneX),
                points: false,
            }),

            // Mobile Area
            areaY(chartData, {
                id: "mobile-area",
                key: (d) => d.date.toISOString(),
                x: (d) => d.date,
                y: getMobileY,
                fill: getMobileColor(),
                fillOpacity: 0.35,
                curve: d3Curve(curveMonotoneX),
            }),
            // Mobile Line
            lineY(chartData, {
                id: "mobile-line",
                key: (d) => d.date.toISOString(),
                x: (d) => d.date,
                y: getMobileY,
                stroke: getMobileColor(),
                strokeWidth: 2,
                curve: d3Curve(curveMonotoneX),
                points: false,
            }),
        ];

        return defineChart(
            {
                marks,
                renderer: motionRenderer,
                motion: {
                    transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
                },
                scales: {
                    x: {
                        scale: scaleTime().domain(xDomain),
                    },
                    y: {
                        scale: scaleLinear().domain([0, fixedYMax]).nice(),
                        grid: true,
                    },
                },
            },
            {
                focus: "group-x",
                tooltip: {
                    use: tooltip,
                    motion: { type: "spring", stiffness: 170, damping: 18 },
                    className: "font-sans! shadow-none! capitalize!",
                    content: (points) => {
                        const datum = points[0]?.datum as (typeof chartData)[number];
                        if (!datum) return { rows: [] };
                        const activeMetrics = activeTab === "combined" ? (["desktop", "mobile"] as const) : [activeTab];

                        return {
                            title: formatTooltipDate(points[0].xValue),
                            rows: activeMetrics.map((key) => ({
                                label: chartConfig[key].label,
                                value: `${datum[key].toLocaleString("en-US")} visitors`,
                                color: chartConfig[key].color,
                            })),
                        };
                    },
                },
            },
        );
    }, [activeTab, xDomain, fixedYMax]);

    return (
        <Card className="gap-0 py-4 sm:py-0">
            <CardHeader className="flex flex-col items-stretch border-b p-0! sm:flex-row">
                {/* Title Context */}
                <div className="flex flex-1 flex-col justify-center gap-1 px-6 pt-4 pb-3 sm:pt-6 sm:pb-6">
                    <CardTitle className="text-lg">Live Server Throughput</CardTitle>
                    <CardDescription>
                        {activeTab === "combined"
                            ? "Interactive visitor traffic across desktop and mobile devices (Last 72 Hours)"
                            : `Isolated traffic trend for ${chartConfig[activeTab].label}`}
                    </CardDescription>
                </div>

                {/* Tab Controls */}
                <div className="flex">
                    {(["combined", "desktop", "mobile"] as const).map((key) => {
                        const label = key === "combined" ? "Combined Avg" : chartConfig[key].label;
                        return (
                            <button
                                key={key}
                                data-active={activeTab === key}
                                className="data-[active=true]:bg-muted/50 hover:bg-muted/30 flex flex-1 flex-col justify-center gap-1 border-t px-4 py-4 text-left transition-colors even:border-l sm:border-t-0 sm:border-l sm:px-6 sm:py-6"
                                onClick={() => setActiveTab(key)}>
                                <span className="text-muted-foreground text-xs lg:text-nowrap">{label}</span>
                                <span className="text-lg leading-none font-bold sm:text-2xl">
                                    {stats[key].toLocaleString("en-US")}
                                    <span className="text-muted-foreground ml-1 text-xs font-normal">/hr</span>
                                </span>
                            </button>
                        );
                    })}
                </div>
            </CardHeader>

            <CardContent className="px-2 pt-4 sm:p-6 sm:pt-8">
                {/* Dynamic Legend */}
                <div className="text-muted-foreground mb-4 flex items-center justify-end gap-5 text-xs font-medium">
                    {(activeTab === "combined" || activeTab === "desktop") && (
                        <div className="flex items-center gap-2">
                            <span
                                className="h-2.5 w-2.5 rounded-full"
                                style={{ backgroundColor: chartConfig.desktop.color }}
                            />
                            <span>Desktop</span>
                        </div>
                    )}
                    {(activeTab === "combined" || activeTab === "mobile") && (
                        <div className="flex items-center gap-2">
                            <span
                                className="h-2.5 w-2.5 rounded-full"
                                style={{ backgroundColor: chartConfig.mobile.color }}
                            />
                            <span>Mobile</span>
                        </div>
                    )}
                </div>

                <div className="h-[300px] w-full">
                    <Chart
                        definition={definition}
                        renderer={motionRenderer}
                        height={300}
                        ariaLabel={`Live server throughput chart displaying ${activeTab} data`}
                    />
                </div>
            </CardContent>
        </Card>
    );
};

export default Demo;
```
