# Line Chart Interactive

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

Line Chart Interactive is an open-source chart from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/charts/line-chart-interactive

**React**

**Example**

```tsx title="components/charts/line-chart-interactive.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}
          value={[mode]}
          onValueChange={(value: string[]) => {
            if (value[0]) setMode(value[0] 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>
  )
}
```

Monthly temperatures for several cities with chips to show or hide each city and a switch between average, high and low. A row of stats under the chart shows each city's range and follows the cursor month. Use it as a pattern for comparing many series where readers pick which ones to see.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/line-chart-interactive
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add charts line-chart-interactive
```

### Manual

1. Install the dependencies:

```bash title="Terminal"
npm install recharts
```

2. Add the pieces it builds on: [Card](https://ui.suiss.app/docs/components/card), [Chart](https://ui.suiss.app/docs/components/chart), [Toggle](https://ui.suiss.app/docs/components/toggle), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/charts/line-chart-interactive.tsx`:

```tsx title="components/charts/line-chart-interactive.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}
          value={[mode]}
          onValueChange={(value: string[]) => {
            if (value[0]) setMode(value[0] 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>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { LineChartInteractive } from "@/components/charts/line-chart-interactive"
```

```tsx
<LineChartInteractive />
```

Replace `CITIES` with your own series and add a matching entry with label and color to `chartConfig` for each one. Set which series start visible in the `visible` state. The high and low modes are derived from the averages with a demo formula, so replace `temperature` with real values, and adjust the fixed value axis `domain` to your range.

## Notes

The last visible city can't be hidden, so the chart is never empty. Values outside the fixed value axis range are clipped. Newly shown lines draw in with an animation that is skipped when reduced motion is preferred.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/charts-line--interactive.light.webp)

```tsx title="uniwind/components/charts/line-chart-interactive.tsx"
import { cn } from "cn"
import * as React from "react"
import { View } from "react-native"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import {
  CartesianGrid,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  Line,
  LineChart,
  ReferenceLine,
  XAxis,
  YAxis,
} from "@/components/ui/chart"
import { Text } from "@/components/ui/text"
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 = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
]

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

const SWATCH: Record<City, { dot: string; chip: string }> = {
  istanbul: { dot: "bg-blue-500", chip: "bg-blue-500/15 active:opacity-80" },
  ankara: { dot: "bg-purple-500", chip: "bg-purple-500/15 active:opacity-80" },
  izmir: { dot: "bg-teal-500", chip: "bg-teal-500/15 active:opacity-80" },
  antalya: { dot: "bg-orange-500", chip: "bg-orange-500/15 active:opacity-80" },
  erzurum: { dot: "bg-indigo-500", chip: "bg-indigo-500/15 active:opacity-80" },
}

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)
}

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((name, index) => ({
        index,
        month: name.slice(0, 3),
        ...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">
      <CardHeader className="flex-row flex-wrap items-start justify-between gap-4">
        <View className="min-w-44 grow gap-0.5">
          <Text className="text-lg font-semibold">Monthly temperature</Text>
          <Text className="text-sm text-muted-foreground">
            {MODES[mode]} · °C · long-term
          </Text>
        </View>
        <ToggleGroup
          spacing={0}
          value={[mode]}
          onValueChange={(value) => {
            if (value[0]) setMode(value[0] as Mode)
          }}
          accessibilityLabel="Temperature"
        >
          {(Object.keys(MODES) as Mode[]).map((key) => (
            <ToggleGroupItem key={key} value={key} size="sm">
              <Text>{MODES[key]}</Text>
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
      </CardHeader>
      <CardContent className="gap-4">
        <View
          className="flex-row flex-wrap gap-2"
          role="group"
          accessibilityLabel="Cities"
        >
          {CITY_KEYS.map((city) => (
            <Toggle
              key={city}
              size="sm"
              pressed={visible.includes(city)}
              onPressedChange={(pressed) => toggle(city, pressed)}
              className={(state) =>
                cn(
                  "gap-1.5",
                  state.pressed
                    ? SWATCH[city].chip
                    : "bg-secondary active:opacity-80"
                )
              }
            >
              <View
                className={cn(
                  "size-2 rounded-full",
                  visible.includes(city) ? SWATCH[city].dot : "bg-border"
                )}
              />
              <Text className="text-sm font-semibold text-foreground">
                {chartConfig[city].label}
              </Text>
            </Toggle>
          ))}
        </View>
        <ChartContainer
          config={chartConfig}
          accessibilityLabel="Monthly temperature"
          className="h-72 w-full"
        >
          <LineChart
            data={data}
            margin={{ top: 10, right: 4, left: 12, bottom: 0 }}
            onActiveIndexChange={setMonth}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="month" tickMargin={8} minTickGap={8} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              domain={[-20, 40]}
              ticks={[-20, -10, 0, 10, 20, 30, 40]}
              tickFormatter={(value: number) => `${value}°`}
            />
            <ReferenceLine y={0} stroke="input" />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  indicator="line"
                  labelFormatter={(_, items) =>
                    MONTHS[Number(items[0]?.payload.index ?? 0)]
                  }
                  formatter={(value, name) => (
                    <View className="w-full flex-row items-center gap-2">
                      <View
                        className={cn(
                          "h-3 w-0.75 shrink-0 rounded-full",
                          SWATCH[name as City].dot
                        )}
                      />
                      <Text className="grow text-muted-foreground">
                        {chartConfig[name as City].label}
                      </Text>
                      <Text className="font-semibold tabular-nums">
                        {String(value)}°
                      </Text>
                    </View>
                  )}
                />
              }
            />
            {shown.map((city) => (
              <Line
                animationDuration={650}
                animationEasing="ease-in-out"
                key={city}
                dataKey={city}
                type="monotone"
                strokeWidth={2.5}
                activeDot={{
                  r: 5,
                  strokeWidth: 2,
                  stroke: "muted",
                }}
              />
            ))}
          </LineChart>
        </ChartContainer>
        <View className="flex-row flex-wrap gap-3">
          {shown.map((city) => {
            const temperatures = data.map((item) => item[city])
            const highlighted = month === null ? undefined : temperatures[month]
            return (
              <View key={city} className="min-w-34 grow basis-0 gap-0.5">
                <View className="flex-row items-center gap-1.5">
                  <View
                    className={cn(
                      "size-2 shrink-0 rounded-full",
                      SWATCH[city].dot
                    )}
                  />
                  <Text className="text-sm text-muted-foreground">
                    {chartConfig[city].label}
                  </Text>
                </View>
                <Text className="text-base font-semibold tabular-nums">
                  {month === null || highlighted === undefined
                    ? `${Math.round(Math.min(...temperatures))}° / ${Math.round(Math.max(...temperatures))}°`
                    : `${Math.round(highlighted)}° · ${MONTHS[month]}`}
                </Text>
              </View>
            )
          })}
        </View>
      </CardContent>
    </Card>
  )
}
```

```swift title="swift/Charts/LineChartInteractive.swift"
import Charts
import SwiftUI

private enum City: String, CaseIterable {
  case istanbul, ankara, izmir, antalya, erzurum

  var average: [Double] {
    switch self {
    case .istanbul: [6, 6, 8, 12, 17, 22, 24, 24, 21, 16, 12, 8]
    case .ankara: [0, 2, 6, 11, 16, 20, 24, 24, 19, 13, 7, 2]
    case .izmir: [9, 10, 12, 16, 21, 26, 28, 28, 24, 19, 14, 10]
    case .antalya: [10, 11, 13, 16, 21, 26, 29, 29, 25, 20, 15, 11]
    case .erzurum: [-9, -8, -2, 6, 11, 15, 20, 20, 15, 8, 1, -6]
    }
  }

  var spread: (Double, Double) {
    switch self {
    case .istanbul: (6, 0.6)
    case .ankara: (11, 1.6)
    case .izmir: (9, 1)
    case .antalya: (9, 0.9)
    case .erzurum: (12, 1.8)
    }
  }
}

private enum Mode: String, CaseIterable {
  case average, high, low

  var title: String {
    switch self {
    case .average: "Average"
    case .high: "High"
    case .low: "Low"
    }
  }
}

private let chartConfig: ChartConfig = [
  "istanbul": .init("Istanbul", color: EsuissColor.chart1),
  "ankara": .init("Ankara", color: Palette.purple500),
  "izmir": .init("Izmir", color: EsuissColor.chart2),
  "antalya": .init("Antalya", color: EsuissColor.chart4),
  "erzurum": .init("Erzurum", color: EsuissColor.chart3),
]

private func temperature(_ city: City, month: Int, mode: Mode) -> Double {
  let mean = city.average[month]
  if mode == .average { return mean }
  let season = sin((Double(month) - 0.5) / 12 * .pi)
  let range = city.spread.0 + city.spread.1 * 6 * season * season
  let value = mean + (mode == .high ? range / 2 : -range / 2)
  return (value * 10).rounded() / 10
}

public struct LineChartInteractive: View {
  @State private var visible: Set<City> = [.istanbul, .ankara, .antalya]
  @State private var mode: Mode = .average
  @State private var selection: Int?
  @Environment(\.locale) private var locale
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init() {}

  private var months: [Date] {
    (0..<12).compactMap { Calendar(identifier: .gregorian).date(from: DateComponents(year: 2026, month: $0 + 1, day: 1)) }
  }

  private func shortMonth(_ index: Int) -> String {
    months[index].formatted(.dateTime.month(.abbreviated).locale(locale))
  }

  private func longMonth(_ index: Int) -> String {
    months[index].formatted(.dateTime.month(.wide).locale(locale))
  }

  private var shown: [City] {
    City.allCases.filter { visible.contains($0) }
  }

  private var modeBinding: Binding<Mode?> {
    Binding(get: { mode }, set: { if let value = $0 { mode = value } })
  }

  private func toggle(_ city: City) {
    if visible.contains(city) {
      if visible.count > 1 { visible.remove(city) }
    } else {
      visible.insert(city)
    }
  }

  public var body: some View {
    Card(.filled, size: .sm) {
      VStack(alignment: .leading, spacing: 16) {
        VStack(alignment: .leading, spacing: 2) {
          Text("Monthly temperature")
            .textStyle(.lg, weight: .semibold)
          Text("\(mode.title) · °C · long-term")
            .textStyle(.sm)
            .foregroundStyle(EsuissColor.mutedForeground)
        }
        ToggleGroup(selection: modeBinding, size: .sm, spacing: 0) {
          ForEach(Mode.allCases, id: \.self) { item in
            ToggleGroupItem(LocalizedStringKey(item.title), value: item)
          }
        }
        .accessibilityLabel("Temperature")
      }
      .padding(.horizontal, 16)
      CardContent {
        VStack(alignment: .leading, spacing: 16) {
          ChartLegendLayout(horizontalSpacing: 8, verticalSpacing: 8, centered: false) {
            ForEach(City.allCases, id: \.self) { city in
              CityChip(
                label: chartConfig.label(city.rawValue),
                color: chartConfig.color(city.rawValue),
                isOn: visible.contains(city)
              ) {
                toggle(city)
              }
            }
          }
          .accessibilityElement(children: .contain)
          .accessibilityLabel("Cities")
          chart
          LazyVGrid(columns: [GridItem(.adaptive(minimum: 136), spacing: 12, alignment: .leading)], alignment: .leading, spacing: 12) {
            ForEach(shown, id: \.self) { city in
              stat(city)
            }
          }
        }
      }
    }
    .hapticFeedback(.selection, trigger: visible)
  }

  private var chart: some View {
    ChartContainer(config: chartConfig) {
      ChartAnimator(animationDuration: 650, animationEasing: .easeInOut) { progress in
        Chart {
          RuleMark(y: .value("Zero", 0))
            .lineStyle(StrokeStyle(lineWidth: 1))
            .foregroundStyle(EsuissColor.input)
          ForEach(shown, id: \.self) { city in
            ForEach(0..<12, id: \.self) { month in
              LineMark(
                x: .value("Month", month),
                y: .value("Temperature", temperature(city, month: month, mode: mode)),
                series: .value("City", city.rawValue)
              )
              .interpolationMethod(.monotone)
              .lineStyle(StrokeStyle(lineWidth: 2.5, lineCap: .round, lineJoin: .round))
              .foregroundStyle(chartConfig.color(city.rawValue))
            }
            if let selection {
              PointMark(x: .value("Month", selection), y: .value("Temperature", temperature(city, month: selection, mode: mode)))
                .symbol {
                  Circle()
                    .fill(chartConfig.color(city.rawValue))
                    .stroke(EsuissColor.muted, lineWidth: 2)
                    .frame(width: 12, height: 12)
                }
            }
          }
          .chartReveal(progress, x: 0...11)
        }
        .chartXScale(domain: 0...11, range: .plotDimension(padding: 8))
        .chartYScale(domain: -20...40)
        .chartXAxis { ChartAxis.x(values: [0, 2, 4, 6, 8, 10]) { shortMonth($0) } }
        .chartYAxis { ChartAxis.y(values: [-20.0, -10, 0, 10, 20, 30, 40]) { "\(Int($0))°" } }
        .animation(reduceMotion ? nil : .easeInOut(duration: 0.65), value: mode)
        .animation(reduceMotion ? nil : .easeInOut(duration: 0.3), value: visible)
        .chartTooltip(
          x: Array(0..<12),
          labels: (0..<12).map(longMonth),
          selection: $selection,
          accessibilityLabel: "Monthly temperature",
          items: { month in
            shown.map { ChartTooltipItem($0.rawValue, value: temperature($0, month: month, mode: mode)) }
          }
        ) { payload in
          ChartTooltipContent(payload, indicator: .line) { payload in
            Text(verbatim: longMonth(payload.index))
          } formatter: { item in
            Capsule()
              .fill(chartConfig.color(item.key))
              .frame(width: 3, height: 12)
            Text(LocalizedStringKey(chartConfig.label(item.key)))
              .foregroundStyle(EsuissColor.mutedForeground)
              .frame(maxWidth: .infinity, alignment: .leading)
            Text("\(item.value, format: .number)°")
              .scaledFont(TextStyle.xs, weight: .semibold)
              .monospacedDigit()
          }
        }
      }
    }
    .frame(height: 288)
  }

  private func stat(_ city: City) -> some View {
    let temperatures = (0..<12).map { temperature(city, month: $0, mode: mode) }
    let value: String
    if let selection {
      value = "\(Int(temperatures[selection].rounded()))° · \(longMonth(selection))"
    } else {
      value = "\(Int((temperatures.min() ?? 0).rounded()))° / \(Int((temperatures.max() ?? 0).rounded()))°"
    }
    return VStack(alignment: .leading, spacing: 2) {
      HStack(spacing: 6) {
        Circle()
          .fill(chartConfig.color(city.rawValue))
          .frame(width: 8, height: 8)
        Text(LocalizedStringKey(chartConfig.label(city.rawValue)))
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      Text(verbatim: value)
        .textStyle(.base, weight: .semibold)
        .monospacedDigit()
    }
    .accessibilityElement(children: .combine)
  }
}

private struct CityChip: View {
  let label: String
  let color: Color
  let isOn: Bool
  let action: () -> Void

  var body: some View {
    Button(action: action) {
      HStack(spacing: 6) {
        Circle()
          .fill(isOn ? color : EsuissColor.border)
          .frame(width: 8, height: 8)
        Text(LocalizedStringKey(label))
          .textStyle(.sm, weight: .semibold)
          .foregroundStyle(EsuissColor.foreground)
      }
      .padding(.horizontal, 12)
      .frame(height: 32)
    }
    .buttonStyle(CityChipStyle(color: color, isOn: isOn))
    .accessibilityAddTraits(isOn ? .isSelected : [])
  }
}

private struct CityChipStyle: ButtonStyle {
  let color: Color
  let isOn: Bool

  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .background(isOn ? color.opacity(0.16) : EsuissColor.secondary, in: RoundedRectangle(cornerRadius: Radius.lg, style: .continuous))
      .opacity(configuration.isPressed ? 0.8 : 1)
      .contentShape(RoundedRectangle(cornerRadius: Radius.lg, style: .continuous))
  }
}
```

```kt title="kotlin/charts/LineChartInteractive.kt"
package com.esuiss.ui.charts

import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.ChartAxisOrientation
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartCurve
import com.esuiss.ui.components.ChartDot
import com.esuiss.ui.components.ChartEasing
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.ChartTooltipIndicator
import com.esuiss.ui.components.LineChart
import com.esuiss.ui.components.ToggleGroup
import com.esuiss.ui.components.ToggleGroupItem
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.components.chartColor
import com.esuiss.ui.components.rememberHaptics
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography
import kotlin.math.PI
import kotlin.math.roundToInt
import kotlin.math.sin

private class CityClimate(val average: List<Double>, val spread: Pair<Double, Double>)

private val cities = linkedMapOf(
    "istanbul" to CityClimate(listOf(6.0, 6.0, 8.0, 12.0, 17.0, 22.0, 24.0, 24.0, 21.0, 16.0, 12.0, 8.0), 6.0 to 0.6),
    "ankara" to CityClimate(listOf(0.0, 2.0, 6.0, 11.0, 16.0, 20.0, 24.0, 24.0, 19.0, 13.0, 7.0, 2.0), 11.0 to 1.6),
    "izmir" to CityClimate(listOf(9.0, 10.0, 12.0, 16.0, 21.0, 26.0, 28.0, 28.0, 24.0, 19.0, 14.0, 10.0), 9.0 to 1.0),
    "antalya" to CityClimate(listOf(10.0, 11.0, 13.0, 16.0, 21.0, 26.0, 29.0, 29.0, 25.0, 20.0, 15.0, 11.0), 9.0 to 0.9),
    "erzurum" to CityClimate(listOf(-9.0, -8.0, -2.0, 6.0, 11.0, 15.0, 20.0, 20.0, 15.0, 8.0, 1.0, -6.0), 12.0 to 1.8),
)

private val modes = linkedMapOf("average" to "Average", "high" to "High", "low" to "Low")

private val shortMonths = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")

private val longMonths = listOf(
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December",
)

private val chartConfig = ChartConfig(
    "istanbul" to ChartConfigItem("Istanbul") { chart1 },
    "ankara" to ChartConfigItem("Ankara") { Palette.purple500 },
    "izmir" to ChartConfigItem("Izmir") { chart2 },
    "antalya" to ChartConfigItem("Antalya") { chart4 },
    "erzurum" to ChartConfigItem("Erzurum") { chart3 },
)

private class TemperatureRow(val index: Int, val month: String, val values: Map<String, Double>)

private fun temperature(city: String, month: Int, mode: String): Double {
    val climate = cities.getValue(city)
    val mean = climate.average[month]
    if (mode == "average") return mean
    val season = sin((month - 0.5) / 12 * PI)
    val range = climate.spread.first + climate.spread.second * 6 * season * season
    return mean + if (mode == "high") range / 2 else -range / 2
}

private fun degrees(value: Double): String = if (value % 1.0 == 0.0) value.toLong().toString() else value.toString()

@Composable
private fun CityToggle(label: String, color: Color, pressed: Boolean, onPressedChange: (Boolean) -> Unit) {
    val colors = EsuissTheme.colors
    val haptics = rememberHaptics()
    val background by animateColorAsState(if (pressed) color.copy(alpha = 0.16f) else colors.secondary, label = "background")
    val dot by animateColorAsState(if (pressed) color else colors.border, label = "dot")
    Row(
        modifier = Modifier
            .height(32.dp)
            .background(background, RoundedCornerShape(Radius.lg))
            .toggleable(pressed, role = Role.Checkbox) {
                haptics.toggle(it)
                onPressedChange(it)
            }
            .padding(horizontal = 12.dp),
        horizontalArrangement = Arrangement.spacedBy(6.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Box(
            Modifier
                .size(8.dp)
                .background(dot, CircleShape),
        )
        BasicText(label, style = Typography.sm.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold))
    }
}

@Composable
fun LineChartInteractive(modifier: Modifier = Modifier) {
    val colors = EsuissTheme.colors
    var visible by remember { mutableStateOf(listOf("istanbul", "ankara", "antalya")) }
    var mode by remember { mutableStateOf("average") }
    var month by remember { mutableStateOf<Int?>(null) }
    val data = remember(mode) {
        shortMonths.mapIndexed { index, name ->
            TemperatureRow(
                index,
                name,
                cities.keys.associateWith { city -> (temperature(city, index, mode) * 10).roundToInt() / 10.0 },
            )
        }
    }
    val shown = cities.keys.filter { it in visible }
    Card(modifier, variant = CardVariant.Filled, size = CardSize.Sm) {
        CardContent {
            Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
                    BasicText("Monthly temperature", style = Typography.lg.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold))
                    BasicText(
                        "${modes.getValue(mode)} · °C · long-term",
                        style = Typography.sm.copy(color = colors.mutedForeground),
                    )
                }
                ToggleGroup(value = mode, onValueChange = { value -> if (value != null) mode = value }, size = ToggleSize.Sm, spacing = 0) {
                    for ((key, label) in modes) ToggleGroupItem(label, key)
                }
            }
        }
        CardContent {
            Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                ChartContainer(chartConfig) {
                    FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
                        for (city in cities.keys) {
                            CityToggle(chartConfig[city]?.label ?: city, chartColor(city), city in visible) { pressed ->
                                visible = when {
                                    pressed -> visible + city
                                    visible.size > 1 -> visible - city
                                    else -> visible
                                }
                            }
                        }
                    }
                }
                ChartContainer(
                    chartConfig,
                    Modifier
                        .fillMaxWidth()
                        .height(288.dp),
                ) {
                    LineChart(
                        data,
                        margin = ChartMargin(top = 10.dp, right = 4.dp, bottom = 0.dp, left = 12.dp),
                        accessibilityLabel = "Monthly temperature",
                        selectedIndex = month,
                        onSelectedIndexChange = { month = it },
                    ) {
                        CartesianGrid(strokeDasharray = listOf(2f, 4f))
                        XAxis(label = { it.month }, tickMargin = 8.dp, minTickGap = 8.dp)
                        YAxis(
                            orientation = ChartAxisOrientation.Right,
                            tickMargin = 6.dp,
                            domain = -20.0..40.0,
                            ticks = listOf(-20.0, -10.0, 0.0, 10.0, 20.0, 30.0, 40.0),
                            tickFormatter = { "${it.toInt()}°" },
                        )
                        ReferenceLine(0.0, stroke = colors.input)
                        ChartTooltip {
                            ChartTooltipContent(
                                indicator = ChartTooltipIndicator.Line,
                                labelFormatter = { _, items ->
                                    BasicText(
                                        longMonths[items.firstOrNull()?.index ?: 0],
                                        style = Typography.xs.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
                                    )
                                },
                                formatter = { item ->
                                    Row(
                                        Modifier.fillMaxWidth(),
                                        horizontalArrangement = Arrangement.spacedBy(8.dp),
                                        verticalAlignment = Alignment.CenterVertically,
                                    ) {
                                        Box(
                                            Modifier
                                                .width(3.dp)
                                                .height(12.dp)
                                                .background(item.color, CircleShape),
                                        )
                                        BasicText(
                                            chartConfig[item.key]?.label ?: item.key,
                                            Modifier.weight(1f),
                                            style = Typography.xs.copy(color = colors.mutedForeground),
                                        )
                                        BasicText(
                                            "${degrees(item.value)}°",
                                            style = Typography.xs.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                                        )
                                    }
                                },
                            )
                        }
                        for (city in shown) {
                            Line(
                                city,
                                value = { it.values.getValue(city) },
                                curve = ChartCurve.Monotone,
                                strokeWidth = 2.5.dp,
                                activeDot = ChartDot(5.dp, strokeColor = colors.muted, strokeWidth = 2.dp),
                                animationDuration = 650,
                                animationEasing = ChartEasing.EaseInOut,
                            )
                        }
                    }
                }
                ChartContainer(chartConfig) {
                    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
                        for (pair in shown.chunked(2)) {
                            Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
                                for (city in pair) {
                                    val temperatures = data.map { it.values.getValue(city) }
                                    val highlighted = month?.let { temperatures.getOrNull(it) }
                                    Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
                                        Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
                                            Box(
                                                Modifier
                                                    .size(8.dp)
                                                    .background(chartColor(city), CircleShape),
                                            )
                                            BasicText(
                                                chartConfig[city]?.label ?: city,
                                                style = Typography.sm.copy(color = colors.mutedForeground),
                                            )
                                        }
                                        BasicText(
                                            if (month == null || highlighted == null) {
                                                "${temperatures.min().roundToInt()}° / ${temperatures.max().roundToInt()}°"
                                            } else {
                                                "${highlighted.roundToInt()}° · ${longMonths[month!!]}"
                                            },
                                            style = Typography.base.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                                        )
                                    }
                                }
                                if (pair.size == 1) Box(Modifier.weight(1f))
                            }
                        }
                    }
                }
            }
        }
    }
}
```

One line per visible city across twelve months. A `ToggleGroup` switches between average, high and low temperatures, and a row of `Toggle` chips shows or hides each city, keeping at least one on. Touching the plot reports the month through `onActiveIndexChange`, and the summary under the chart switches from each city's range to its value for that month. The lines animate with `animationDuration={650}` and `animationEasing="ease-in-out"`.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/line-chart-interactive
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add line-chart-interactive
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add line-chart-interactive --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add line-chart-interactive --platform kotlin
```

The CLI also adds what it builds on: [Card](https://ui.suiss.app/docs/components/card), [Chart](https://ui.suiss.app/docs/components/chart), [Text](https://ui.suiss.app/docs/components/text), [Toggle](https://ui.suiss.app/docs/components/toggle), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn
```

2. Add the pieces it builds on: [Card](https://ui.suiss.app/docs/components/card), [Chart](https://ui.suiss.app/docs/components/chart), [Text](https://ui.suiss.app/docs/components/text), [Toggle](https://ui.suiss.app/docs/components/toggle), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/charts/line-chart-interactive.tsx`:

- `uniwind/components/charts/line-chart-interactive.tsx`: shown above

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn
```

2. Add the pieces it builds on: [Card](https://ui.suiss.app/docs/components/card), [Chart](https://ui.suiss.app/docs/components/chart), [Text](https://ui.suiss.app/docs/components/text), [Toggle](https://ui.suiss.app/docs/components/toggle), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/charts/line-chart-interactive.tsx`:

```tsx title="nativewind/components/charts/line-chart-interactive.tsx"
import { cn } from "cn"
import * as React from "react"
import { View } from "react-native"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import {
  CartesianGrid,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  Line,
  LineChart,
  ReferenceLine,
  XAxis,
  YAxis,
} from "@/components/ui/chart"
import { Text } from "@/components/ui/text"
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 = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
]

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

const SWATCH: Record<City, { dot: string; chip: string }> = {
  istanbul: { dot: "bg-blue-500", chip: "bg-blue-500/15 active:opacity-80" },
  ankara: { dot: "bg-purple-500", chip: "bg-purple-500/15 active:opacity-80" },
  izmir: { dot: "bg-teal-500", chip: "bg-teal-500/15 active:opacity-80" },
  antalya: { dot: "bg-orange-500", chip: "bg-orange-500/15 active:opacity-80" },
  erzurum: { dot: "bg-indigo-500", chip: "bg-indigo-500/15 active:opacity-80" },
}

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)
}

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((name, index) => ({
        index,
        month: name.slice(0, 3),
        ...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">
      <CardHeader className="flex-row flex-wrap items-start justify-between gap-4">
        <View className="min-w-44 grow gap-0.5">
          <Text className="text-lg font-semibold">Monthly temperature</Text>
          <Text className="text-sm text-muted-foreground">
            {MODES[mode]} · °C · long-term
          </Text>
        </View>
        <ToggleGroup
          spacing={0}
          value={[mode]}
          onValueChange={(value) => {
            if (value[0]) setMode(value[0] as Mode)
          }}
          accessibilityLabel="Temperature"
        >
          {(Object.keys(MODES) as Mode[]).map((key) => (
            <ToggleGroupItem key={key} value={key} size="sm">
              <Text>{MODES[key]}</Text>
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
      </CardHeader>
      <CardContent className="gap-4">
        <View
          className="flex-row flex-wrap gap-2"
          role="group"
          accessibilityLabel="Cities"
        >
          {CITY_KEYS.map((city) => (
            <Toggle
              key={city}
              size="sm"
              pressed={visible.includes(city)}
              onPressedChange={(pressed) => toggle(city, pressed)}
              className={(state) =>
                cn(
                  "gap-1.5",
                  state.pressed
                    ? SWATCH[city].chip
                    : "bg-secondary active:opacity-80"
                )
              }
            >
              <View
                className={cn(
                  "size-2 rounded-full",
                  visible.includes(city) ? SWATCH[city].dot : "bg-border"
                )}
              />
              <Text className="text-sm font-semibold text-foreground">
                {chartConfig[city].label}
              </Text>
            </Toggle>
          ))}
        </View>
        <ChartContainer
          config={chartConfig}
          accessibilityLabel="Monthly temperature"
          className="h-72 w-full"
        >
          <LineChart
            data={data}
            margin={{ top: 10, right: 4, left: 12, bottom: 0 }}
            onActiveIndexChange={setMonth}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="month" tickMargin={8} minTickGap={8} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              domain={[-20, 40]}
              ticks={[-20, -10, 0, 10, 20, 30, 40]}
              tickFormatter={(value: number) => `${value}°`}
            />
            <ReferenceLine y={0} stroke="input" />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  indicator="line"
                  labelFormatter={(_, items) =>
                    MONTHS[Number(items[0]?.payload.index ?? 0)]
                  }
                  formatter={(value, name) => (
                    <View className="w-full flex-row items-center gap-2">
                      <View
                        className={cn(
                          "h-3 w-[0.1875rem] shrink-0 rounded-full",
                          SWATCH[name as City].dot
                        )}
                      />
                      <Text className="grow text-muted-foreground">
                        {chartConfig[name as City].label}
                      </Text>
                      <Text className="font-semibold tabular-nums">
                        {String(value)}°
                      </Text>
                    </View>
                  )}
                />
              }
            />
            {shown.map((city) => (
              <Line
                animationDuration={650}
                animationEasing="ease-in-out"
                key={city}
                dataKey={city}
                type="monotone"
                strokeWidth={2.5}
                activeDot={{
                  r: 5,
                  strokeWidth: 2,
                  stroke: "muted",
                }}
              />
            ))}
          </LineChart>
        </ChartContainer>
        <View className="flex-row flex-wrap gap-3">
          {shown.map((city) => {
            const temperatures = data.map((item) => item[city])
            const highlighted = month === null ? undefined : temperatures[month]
            return (
              <View key={city} className="min-w-[8.5rem] grow basis-0 gap-0.5">
                <View className="flex-row items-center gap-1.5">
                  <View
                    className={cn(
                      "size-2 shrink-0 rounded-full",
                      SWATCH[city].dot
                    )}
                  />
                  <Text className="text-sm text-muted-foreground">
                    {chartConfig[city].label}
                  </Text>
                </View>
                <Text className="text-base font-semibold tabular-nums">
                  {month === null || highlighted === undefined
                    ? `${Math.round(Math.min(...temperatures))}° / ${Math.round(Math.max(...temperatures))}°`
                    : `${Math.round(highlighted)}° · ${MONTHS[month]}`}
                </Text>
              </View>
            )
          })}
        </View>
      </CardContent>
    </Card>
  )
}
```

4. Update the import paths to match your project.

**StyleSheet**

1. Add the pieces it builds on: [Card](https://ui.suiss.app/docs/components/card), [Chart](https://ui.suiss.app/docs/components/chart), [Text](https://ui.suiss.app/docs/components/text), [Toggle](https://ui.suiss.app/docs/components/toggle), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

2. Copy the source into `components/charts/line-chart-interactive.tsx`:

```tsx title="stylesheet/components/charts/line-chart-interactive.tsx"
import * as React from "react"
import { StyleSheet, View } from "react-native"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import {
  CartesianGrid,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  Line,
  LineChart,
  ReferenceLine,
  XAxis,
  YAxis,
} from "@/components/ui/chart"
import { Text } from "@/components/ui/text"
import {
  fontSize,
  palette,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

import { Toggle } from "@/components/ui/toggle"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type Tone = keyof ThemeColors | keyof typeof palette

function tone(colors: ThemeColors, key: Tone) {
  return { ...colors, ...palette }[key]
}

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 = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
]

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

const SWATCH: Record<City, Tone> = {
  istanbul: "chart1",
  ankara: "purple500",
  izmir: "chart2",
  antalya: "chart4",
  erzurum: "chart3",
}

function withAlpha(color: string, alpha: number) {
  if (!color.startsWith("#") || color.length !== 7) return color
  const r = Number.parseInt(color.slice(1, 3), 16)
  const g = Number.parseInt(color.slice(3, 5), 16)
  const b = Number.parseInt(color.slice(5, 7), 16)
  return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

const styles = StyleSheet.create({
  header: {
    flexDirection: "row",
    flexWrap: "wrap",
    alignItems: "flex-start",
    justifyContent: "space-between",
    gap: 16,
  },
  heading: { minWidth: 176, flexGrow: 1, gap: 2 },
  title: { ...fontSize.lg, fontWeight: "600" },
  caption: { ...fontSize.sm },
  content: { gap: 16 },
  cities: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
  city: { gap: 6 },
  dot: { width: 8, height: 8, borderRadius: 9999 },
  cityLabel: { ...fontSize.sm, fontWeight: "600" },
  chart: { height: 288 },
  row: { width: "100%", flexDirection: "row", alignItems: "center", gap: 8 },
  bar: { height: 12, width: 3, flexShrink: 0, borderRadius: 9999 },
  grow: { flexGrow: 1 },
  strong: { fontWeight: "600", fontVariant: ["tabular-nums"] },
  stats: { flexDirection: "row", flexWrap: "wrap", gap: 12 },
  stat: { minWidth: 136, flexGrow: 1, flexBasis: 0, gap: 2 },
  statLabel: { flexDirection: "row", alignItems: "center", gap: 6 },
  statValue: {
    ...fontSize.base,
    fontWeight: "600",
    fontVariant: ["tabular-nums"],
  },
})

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)
}

export function LineChartInteractive() {
  const { colors } = useTheme()
  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((name, index) => ({
        index,
        month: name.slice(0, 3),
        ...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">
      <CardHeader style={styles.header}>
        <View style={styles.heading}>
          <Text style={styles.title}>Monthly temperature</Text>
          <Text style={[styles.caption, { color: colors.mutedForeground }]}>
            {MODES[mode]} · °C · long-term
          </Text>
        </View>
        <ToggleGroup
          spacing={0}
          value={[mode]}
          onValueChange={(value) => {
            if (value[0]) setMode(value[0] as Mode)
          }}
          accessibilityLabel="Temperature"
        >
          {(Object.keys(MODES) as Mode[]).map((key) => (
            <ToggleGroupItem key={key} value={key} size="sm">
              <Text>{MODES[key]}</Text>
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
      </CardHeader>
      <CardContent style={styles.content}>
        <View style={styles.cities} role="group" accessibilityLabel="Cities">
          {CITY_KEYS.map((city) => (
            <Toggle
              key={city}
              size="sm"
              pressed={visible.includes(city)}
              onPressedChange={(pressed) => toggle(city, pressed)}
              style={(state) => [
                styles.city,
                {
                  backgroundColor: state.pressed
                    ? withAlpha(tone(colors, SWATCH[city]), 0.15)
                    : colors.secondary,
                },
              ]}
            >
              <View
                style={[
                  styles.dot,
                  {
                    backgroundColor: visible.includes(city)
                      ? tone(colors, SWATCH[city])
                      : colors.border,
                  },
                ]}
              />
              <Text style={[styles.cityLabel, { color: colors.foreground }]}>
                {chartConfig[city].label}
              </Text>
            </Toggle>
          ))}
        </View>
        <ChartContainer
          config={chartConfig}
          accessibilityLabel="Monthly temperature"
          style={styles.chart}
        >
          <LineChart
            data={data}
            margin={{ top: 10, right: 4, left: 12, bottom: 0 }}
            onActiveIndexChange={setMonth}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="month" tickMargin={8} minTickGap={8} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              domain={[-20, 40]}
              ticks={[-20, -10, 0, 10, 20, 30, 40]}
              tickFormatter={(value: number) => `${value}°`}
            />
            <ReferenceLine y={0} stroke="input" />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  indicator="line"
                  labelFormatter={(_, items) =>
                    MONTHS[Number(items[0]?.payload.index ?? 0)]
                  }
                  formatter={(value, name) => (
                    <View style={styles.row}>
                      <View
                        style={[
                          styles.bar,
                          {
                            backgroundColor: tone(colors, SWATCH[name as City]),
                          },
                        ]}
                      />
                      <Text
                        style={[styles.grow, { color: colors.mutedForeground }]}
                      >
                        {chartConfig[name as City].label}
                      </Text>
                      <Text style={styles.strong}>{String(value)}°</Text>
                    </View>
                  )}
                />
              }
            />
            {shown.map((city) => (
              <Line
                animationDuration={650}
                animationEasing="ease-in-out"
                key={city}
                dataKey={city}
                type="monotone"
                strokeWidth={2.5}
                activeDot={{
                  r: 5,
                  strokeWidth: 2,
                  stroke: "muted",
                }}
              />
            ))}
          </LineChart>
        </ChartContainer>
        <View style={styles.stats}>
          {shown.map((city) => {
            const temperatures = data.map((item) => item[city])
            const highlighted = month === null ? undefined : temperatures[month]
            return (
              <View key={city} style={styles.stat}>
                <View style={styles.statLabel}>
                  <View
                    style={[
                      styles.dot,
                      { backgroundColor: tone(colors, SWATCH[city]) },
                    ]}
                  />
                  <Text
                    style={[styles.caption, { color: colors.mutedForeground }]}
                  >
                    {chartConfig[city].label}
                  </Text>
                </View>
                <Text style={styles.statValue}>
                  {month === null || highlighted === undefined
                    ? `${Math.round(Math.min(...temperatures))}° / ${Math.round(Math.max(...temperatures))}°`
                    : `${Math.round(highlighted)}° · ${MONTHS[month]}`}
                </Text>
              </View>
            )
          })}
        </View>
      </CardContent>
    </Card>
  )
}
```

3. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

- `swift/Charts/LineChartInteractive.swift`: shown above

- `swift/Components/Button.swift`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/Card.swift`: see [Card](https://ui.suiss.app/docs/components/card.md)

- `swift/Components/Chart.swift`: see [Chart](https://ui.suiss.app/docs/components/chart.md)

- `swift/Components/Checkbox.swift`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `swift/Components/Field.swift`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `swift/Components/Haptics.swift`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `swift/Components/Item.swift`: see [Item](https://ui.suiss.app/docs/components/item.md)

- `swift/Components/Separator.swift`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

- `swift/Components/Toggle.swift`: see [Toggle](https://ui.suiss.app/docs/components/toggle.md)

- `swift/Components/ToggleGroup.swift`: see [Toggle Group](https://ui.suiss.app/docs/components/toggle-group.md)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

- `kotlin/charts/LineChartInteractive.kt`: shown above

- `kotlin/components/Card.kt`: see [Card](https://ui.suiss.app/docs/components/card.md)

- `kotlin/components/Chart.kt`: see [Chart](https://ui.suiss.app/docs/components/chart.md)

- `kotlin/components/Checkbox.kt`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Field.kt`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `kotlin/components/FormSupport.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/FormSupport.kt)

- `kotlin/components/Haptics.kt`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/Label.kt`: see [Label](https://ui.suiss.app/docs/components/label.md)

- `kotlin/components/Separator.kt`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `kotlin/components/Toggle.kt`: see [Toggle](https://ui.suiss.app/docs/components/toggle.md)

- `kotlin/components/ToggleGroup.kt`: see [Toggle Group](https://ui.suiss.app/docs/components/toggle-group.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { LineChartInteractive } from "@/components/charts/line-chart-interactive"
```

```tsx
<LineChartInteractive />
```

Replace `CITIES` with your series and monthly values, and add a matching entry to `chartConfig` and `SWATCH` for each. The high and low values are derived in `temperature()`, so replace that with real data. The component takes no props.

