# Line Chart Interactive

> Monthly temperatures for several cities, with chips to show or hide each city and an average, high or low switch.

A shadcn/ui block from the [@esuiss](https://suiss.app/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/esuiss/line-chart-interactive).

- Page, with a live preview: https://shadcn.exchange/r/esuiss/line-chart-interactive
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/esuiss/line-chart-interactive-light-d83287af887eef2a.png
- Registry item: https://suiss.app/ui/r/new-york-v4/line-chart-interactive.json
- Categories: charts, charts-line
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/esuiss/line-chart-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: `recharts`
- Registry items: `@esuiss/card`, `@esuiss/chart`, `@esuiss/toggle`, `@esuiss/toggle-group`

## Files

### `bases/radix/components/charts/line-chart-interactive.tsx`

```tsx
"use client"

import * as React from "react"
import {
  CartesianGrid,
  Line,
  LineChart,
  ReferenceLine,
  useActiveTooltipDataPoints,
  XAxis,
  YAxis,
} from "recharts"

import { Card, CardContent, CardHeader } from "@/components/ui/card"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"
import { Toggle } from "@/components/ui/toggle"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type City = "istanbul" | "ankara" | "izmir" | "antalya" | "erzurum"

type Mode = "average" | "high" | "low"

const CITIES: Record<City, { average: number[]; spread: [number, number] }> = {
  istanbul: {
    average: [6, 6, 8, 12, 17, 22, 24, 24, 21, 16, 12, 8],
    spread: [6, 0.6],
  },
  ankara: {
    average: [0, 2, 6, 11, 16, 20, 24, 24, 19, 13, 7, 2],
    spread: [11, 1.6],
  },
  izmir: {
    average: [9, 10, 12, 16, 21, 26, 28, 28, 24, 19, 14, 10],
    spread: [9, 1],
  },
  antalya: {
    average: [10, 11, 13, 16, 21, 26, 29, 29, 25, 20, 15, 11],
    spread: [9, 0.9],
  },
  erzurum: {
    average: [-9, -8, -2, 6, 11, 15, 20, 20, 15, 8, 1, -6],
    spread: [12, 1.8],
  },
}

const CITY_KEYS = Object.keys(CITIES) as City[]

const MODES: Record<Mode, string> = {
  average: "Average",
  high: "High",
  low: "Low",
}

const MONTHS = Array.from(
  { length: 12 },
  (_, index) => new Date(2026, index, 1)
)
const shortMonth = new Intl.DateTimeFormat("en-US", { month: "short" })
const longMonth = new Intl.DateTimeFormat("en-US", { month: "long" })

const chartConfig = {
  istanbul: { label: "Istanbul", color: "var(--chart-1)" },
  ankara: { label: "Ankara", color: "var(--color-purple-500)" },
  izmir: { label: "Izmir", color: "var(--chart-2)" },
  antalya: { label: "Antalya", color: "var(--chart-4)" },
  erzurum: { label: "Erzurum", color: "var(--chart-3)" },
} satisfies ChartConfig

const TRANSITION = 650

function temperature(city: City, month: number, mode: Mode) {
  const { average, spread } = CITIES[city]
  const mean = average[month]
  if (mean === undefined) return Number.NaN
  if (mode === "average") return mean
  const season = Math.sin(((month - 0.5) / 12) * Math.PI)
  const range = spread[0] + spread[1] * 6 * season * season
  return mean + (mode === "high" ? range / 2 : -range / 2)
}

function ActiveMonth({
  onChange,
}: {
  onChange: (month: number | null) => void
}) {
  const points = useActiveTooltipDataPoints<{ index: number }>()
  const month = points?.[0]?.index ?? null

  React.useEffect(() => onChange(month), [month, onChange])

  return null
}

export function LineChartInteractive() {
  const [visible, setVisible] = React.useState<City[]>([
    "istanbul",
    "ankara",
    "antalya",
  ])
  const [mode, setMode] = React.useState<Mode>("average")
  const [month, setMonth] = React.useState<number | null>(null)
  const data = React.useMemo(
    () =>
      MONTHS.map((date, index) => ({
        index,
        month: shortMonth.format(date),
        ...Object.fromEntries(
          CITY_KEYS.map((city) => [
            city,
            Math.round(temperature(city, index, mode) * 10) / 10,
          ])
        ),
      })) as ({ index: number; month: string } & Record<City, number>)[],
    [mode]
  )
  const shown = CITY_KEYS.filter((city) => visible.includes(city))

  const toggle = (city: City, pressed: boolean) =>
    setVisible((current) => {
      if (pressed) return [...current, city]
      return current.length > 1
        ? current.filter((item) => item !== city)
        : current
    })

  return (
    <Card variant="filled" size="sm" className="[--card-spacing:--spacing(5)]">
      <CardHeader className="flex flex-wrap items-start justify-between gap-4">
        <div className="flex min-w-44 flex-1 flex-col gap-0.5">
          <p className="text-lg font-semibold">Monthly temperature</p>
          <p className="text-sm text-muted-foreground">
            {MODES[mode]} · °C · long-term
          </p>
        </div>
        <ToggleGroup
          spacing={0}
          type="single"
          value={mode}
          onValueChange={(value: string) => {
            if (value) setMode(value as Mode)
          }}
          aria-label="Temperature"
        >
          {(Object.keys(MODES) as Mode[]).map((key) => (
            <ToggleGroupItem key={key} value={key} size="sm">
              {MODES[key]}
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <div className="flex flex-wrap gap-2" role="group" aria-label="Cities">
          {CITY_KEYS.map((city) => (
            <Toggle
              key={city}
              size="sm"
              pressed={visible.includes(city)}
              onPressedChange={(pressed) => toggle(city, pressed)}
              style={
                { "--chip": chartConfig[city].color } as React.CSSProperties
              }
              className="gap-1.5 bg-secondary font-semibold text-secondary-foreground aria-pressed:bg-[color-mix(in_oklab,var(--chip)_16%,transparent)] aria-pressed:text-foreground aria-pressed:hover:bg-[color-mix(in_oklab,var(--chip)_22%,transparent)] aria-pressed:hover:text-foreground aria-pressed:active:bg-[color-mix(in_oklab,var(--chip)_28%,transparent)]"
            >
              <span
                aria-hidden
                className="size-2 rounded-full bg-border transition-colors in-aria-pressed:bg-(--chip)"
              />
              {chartConfig[city].label}
            </Toggle>
          ))}
        </div>
        <ChartContainer
          config={chartConfig}
          className="aspect-auto h-72 w-full"
        >
          <LineChart
            accessibilityLayer
            data={data}
            margin={{ top: 10, right: 4, left: 12, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis
              dataKey="month"
              tickLine={false}
              axisLine={false}
              tickMargin={8}
              minTickGap={8}
            />
            <YAxis
              orientation="right"
              tickLine={false}
              axisLine={false}
              tickMargin={6}
              width="auto"
              domain={[-20, 40]}
              ticks={[-20, -10, 0, 10, 20, 30, 40]}
              allowDataOverflow
              tickFormatter={(value: number) => `${value}°`}
            />
            <ReferenceLine y={0} stroke="var(--input)" />
            <ChartTooltip
              content={({ active, label, payload }) => (
                <ChartTooltipContent
                  active={active}
                  label={label}
                  payload={payload}
                  indicator="line"
                  labelFormatter={(_, items) =>
                    longMonth.format(MONTHS[items[0]?.payload.index ?? 0])
                  }
                  formatter={(value, name) => (
                    <div className="flex w-full items-center gap-2">
                      <span
                        className="h-3 w-0.75 shrink-0 rounded-full bg-(--swatch)"
                        style={
                          {
                            "--swatch": chartConfig[name as City].color,
                          } as React.CSSProperties
                        }
                      />
                      <span className="flex-1 text-muted-foreground">
                        {chartConfig[name as City].label}
                      </span>
                      <span className="font-semibold tabular-nums">
                        {value}°
                      </span>
                    </div>
                  )}
                />
              )}
            />
            {shown.map((city) => (
              <Line
                key={city}
                dataKey={city}
                type="monotone"
                stroke={`var(--color-${city})`}
                strokeWidth={2.5}
                dot={false}
                activeDot={{
                  r: 5,
                  strokeWidth: 2,
                  stroke: "var(--muted)",
                }}
                animationDuration={TRANSITION}
                animationEasing="ease-in-out"
                pathLength={1}
                className="[&_.recharts-line-curve]:[stroke-dasharray:1] [&_.recharts-line-curve]:transition-[stroke-dashoffset] [&_.recharts-line-curve]:duration-750 [&_.recharts-line-curve]:ease-[cubic-bezier(0.45,0,0.2,1)] [&_.recharts-line-curve]:starting:[stroke-dashoffset:1] motion-reduce:[&_.recharts-line-curve]:transition-none"
              />
            ))}
            <ActiveMonth onChange={setMonth} />
          </LineChart>
        </ChartContainer>
        <dl className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-3">
          {shown.map((city) => {
            const temperatures = data.map((item) => item[city])
            const highlighted = month === null ? undefined : temperatures[month]
            return (
              <div key={city} className="flex flex-col gap-0.5">
                <dt className="flex items-center gap-1.5 text-sm text-muted-foreground">
                  <span
                    className="size-2 shrink-0 rounded-full bg-(--swatch)"
                    style={
                      {
                        "--swatch": chartConfig[city].color,
                      } as React.CSSProperties
                    }
                  />
                  {chartConfig[city].label}
                </dt>
                <dd className="text-base font-semibold tabular-nums">
                  {month === null || highlighted === undefined
                    ? `${Math.round(Math.min(...temperatures))}° / ${Math.round(Math.max(...temperatures))}°`
                    : `${Math.round(highlighted)}° · ${longMonth.format(MONTHS[month])}`}
                </dd>
              </div>
            )
          })}
        </dl>
      </CardContent>
    </Card>
  )
}
```
