# Area Chart Range

A heart-rate band showing each hour's low-to-high range as a shaded area with an average line through it.

Area Chart Range 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/area-chart-range

**React**

**Example**

```tsx title="components/charts/area-chart-range.tsx"
"use client"

import {
  Area,
  CartesianGrid,
  ComposedChart,
  Line,
  XAxis,
  YAxis,
} from "recharts"

import { Card, CardContent, CardHeader } from "@/components/ui/card"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"

const readings: [low: number, high: number, average: number][] = [
  [48, 62, 54],
  [46, 60, 52],
  [47, 64, 54],
  [45, 61, 52],
  [48, 63, 55],
  [46, 66, 54],
  [56, 78, 64],
  [58, 82, 66],
  [64, 88, 74],
  [66, 90, 76],
  [64, 92, 74],
  [66, 89, 75],
  [65, 94, 76],
  [66, 90, 75],
  [64, 88, 74],
  [65, 91, 76],
  [66, 92, 75],
  [68, 96, 78],
  [104, 158, 126],
  [102, 150, 122],
  [70, 94, 78],
  [66, 90, 74],
  [60, 82, 68],
  [52, 70, 58],
]

const chartData = readings.map(([low, high, average], hour) => ({
  hour: `${hour}:00`,
  range: [low, high] as const,
  average,
}))

const chartConfig = {
  range: { label: "Range", color: "var(--color-red-500)" },
  average: { label: "Average", color: "var(--color-red-500)" },
} satisfies ChartConfig

const low = Math.min(...chartData.map((point) => point.range[0]))
const high = Math.max(...chartData.map((point) => point.range[1]))

export function AreaChartRange() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <p className="text-sm font-semibold text-destructive">Heart rate</p>
        <p className="flex items-baseline gap-1">
          <span className="text-3xl font-semibold tracking-tight tabular-nums">
            {low}–{high}
          </span>
          <span className="text-base font-semibold text-muted-foreground">
            BPM
          </span>
        </p>
        <p className="text-sm text-muted-foreground">Today</p>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          className="aspect-auto h-40 w-full"
        >
          <ComposedChart
            accessibilityLayer
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis
              dataKey="hour"
              tickLine={false}
              axisLine={false}
              tickMargin={8}
              interval={5}
            />
            <YAxis
              orientation="right"
              tickLine={false}
              axisLine={false}
              tickMargin={6}
              width="auto"
              domain={[40, 170]}
              ticks={[50, 100, 150]}
            />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  indicator="line"
                  formatter={(value, name) => (
                    <div className="flex w-full justify-between gap-3">
                      <span className="text-muted-foreground">
                        {chartConfig[name as keyof typeof chartConfig].label}
                      </span>
                      <span className="font-semibold tabular-nums">
                        {Array.isArray(value) ? value.join("–") : value} BPM
                      </span>
                    </div>
                  )}
                />
              }
            />
            <Area
              dataKey="range"
              type="monotone"
              stroke="none"
              fill="var(--color-range)"
              fillOpacity={0.22}
            />
            <Line
              dataKey="average"
              type="monotone"
              stroke="var(--color-average)"
              strokeWidth={2}
              dot={false}
            />
          </ComposedChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
```

A health-style heart-rate card that shades each hour's low-to-high range and draws the average as a line through the band. Use it for any metric where the spread matters as much as the typical value, such as temperatures or response times.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/area-chart-range
```

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 area-chart-range
```

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

3. Copy the source into `components/charts/area-chart-range.tsx`:

```tsx title="components/charts/area-chart-range.tsx"
"use client"

import {
  Area,
  CartesianGrid,
  ComposedChart,
  Line,
  XAxis,
  YAxis,
} from "recharts"

import { Card, CardContent, CardHeader } from "@/components/ui/card"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"

const readings: [low: number, high: number, average: number][] = [
  [48, 62, 54],
  [46, 60, 52],
  [47, 64, 54],
  [45, 61, 52],
  [48, 63, 55],
  [46, 66, 54],
  [56, 78, 64],
  [58, 82, 66],
  [64, 88, 74],
  [66, 90, 76],
  [64, 92, 74],
  [66, 89, 75],
  [65, 94, 76],
  [66, 90, 75],
  [64, 88, 74],
  [65, 91, 76],
  [66, 92, 75],
  [68, 96, 78],
  [104, 158, 126],
  [102, 150, 122],
  [70, 94, 78],
  [66, 90, 74],
  [60, 82, 68],
  [52, 70, 58],
]

const chartData = readings.map(([low, high, average], hour) => ({
  hour: `${hour}:00`,
  range: [low, high] as const,
  average,
}))

const chartConfig = {
  range: { label: "Range", color: "var(--color-red-500)" },
  average: { label: "Average", color: "var(--color-red-500)" },
} satisfies ChartConfig

const low = Math.min(...chartData.map((point) => point.range[0]))
const high = Math.max(...chartData.map((point) => point.range[1]))

export function AreaChartRange() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <p className="text-sm font-semibold text-destructive">Heart rate</p>
        <p className="flex items-baseline gap-1">
          <span className="text-3xl font-semibold tracking-tight tabular-nums">
            {low}–{high}
          </span>
          <span className="text-base font-semibold text-muted-foreground">
            BPM
          </span>
        </p>
        <p className="text-sm text-muted-foreground">Today</p>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          className="aspect-auto h-40 w-full"
        >
          <ComposedChart
            accessibilityLayer
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis
              dataKey="hour"
              tickLine={false}
              axisLine={false}
              tickMargin={8}
              interval={5}
            />
            <YAxis
              orientation="right"
              tickLine={false}
              axisLine={false}
              tickMargin={6}
              width="auto"
              domain={[40, 170]}
              ticks={[50, 100, 150]}
            />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  indicator="line"
                  formatter={(value, name) => (
                    <div className="flex w-full justify-between gap-3">
                      <span className="text-muted-foreground">
                        {chartConfig[name as keyof typeof chartConfig].label}
                      </span>
                      <span className="font-semibold tabular-nums">
                        {Array.isArray(value) ? value.join("–") : value} BPM
                      </span>
                    </div>
                  )}
                />
              }
            />
            <Area
              dataKey="range"
              type="monotone"
              stroke="none"
              fill="var(--color-range)"
              fillOpacity={0.22}
            />
            <Line
              dataKey="average"
              type="monotone"
              stroke="var(--color-average)"
              strokeWidth={2}
              dot={false}
            />
          </ComposedChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { AreaChartRange } from "@/components/charts/area-chart-range"
```

```tsx
<AreaChartRange />
```

Feed in one record per period with a two-number `range` array and an `average` value; the header's overall low and high are derived from the data. Adjust the value axis `domain` and `ticks` to fit your range, and change the unit text in the tooltip and header. Set both colors in `chartConfig`.

## Notes

The value axis has a fixed domain, so readings outside it get clipped until you widen it. Focus the plot and use the arrow keys to move the tooltip between hours.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx title="uniwind/components/charts/area-chart-range.tsx"
import { View } from "react-native"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import {
  Area,
  CartesianGrid,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ComposedChart,
  Line,
  XAxis,
  YAxis,
} from "@/components/ui/chart"
import { Text } from "@/components/ui/text"

const readings: [low: number, high: number, average: number][] = [
  [48, 62, 54],
  [46, 60, 52],
  [47, 64, 54],
  [45, 61, 52],
  [48, 63, 55],
  [46, 66, 54],
  [56, 78, 64],
  [58, 82, 66],
  [64, 88, 74],
  [66, 90, 76],
  [64, 92, 74],
  [66, 89, 75],
  [65, 94, 76],
  [66, 90, 75],
  [64, 88, 74],
  [65, 91, 76],
  [66, 92, 75],
  [68, 96, 78],
  [104, 158, 126],
  [102, 150, 122],
  [70, 94, 78],
  [66, 90, 74],
  [60, 82, 68],
  [52, 70, 58],
]

const chartData = readings.map(([low, high, average], hour) => ({
  hour: `${hour}:00`,
  range: [low, high] as const,
  average,
}))

const chartConfig = {
  range: { label: "Range", color: "red-500" },
  average: { label: "Average", color: "red-500" },
} satisfies ChartConfig

const low = Math.min(...chartData.map((point) => point.range[0]))
const high = Math.max(...chartData.map((point) => point.range[1]))

export function AreaChartRange() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <Text className="text-sm font-semibold text-destructive">
          Heart rate
        </Text>
        <View className="flex-row items-baseline gap-1">
          <Text className="text-3xl font-semibold tracking-tight tabular-nums">
            {low}–{high}
          </Text>
          <Text className="text-base font-semibold text-muted-foreground">
            BPM
          </Text>
        </View>
        <Text className="text-sm text-muted-foreground">Today</Text>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          accessibilityLabel="Heart rate"
          className="h-40 w-full"
        >
          <ComposedChart
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="hour" tickMargin={8} interval={5} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              domain={[40, 170]}
              ticks={[50, 100, 150]}
            />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  indicator="line"
                  formatter={(value, name) => (
                    <View className="w-full flex-row justify-between gap-3">
                      <Text className="text-muted-foreground">
                        {chartConfig[name as keyof typeof chartConfig].label}
                      </Text>
                      <Text className="font-semibold tabular-nums">
                        {typeof value === "number" ? value : value.join("–")}{" "}
                        BPM
                      </Text>
                    </View>
                  )}
                />
              }
            />
            <Area
              dataKey="range"
              type="monotone"
              stroke="none"
              fillOpacity={0.22}
            />
            <Line dataKey="average" type="monotone" strokeWidth={2} />
          </ComposedChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
```

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

private let readings: [(low: Double, high: Double, average: Double)] = [
  (48, 62, 54), (46, 60, 52), (47, 64, 54), (45, 61, 52), (48, 63, 55), (46, 66, 54),
  (56, 78, 64), (58, 82, 66), (64, 88, 74), (66, 90, 76), (64, 92, 74), (66, 89, 75),
  (65, 94, 76), (66, 90, 75), (64, 88, 74), (65, 91, 76), (66, 92, 75), (68, 96, 78),
  (104, 158, 126), (102, 150, 122), (70, 94, 78), (66, 90, 74), (60, 82, 68), (52, 70, 58),
]

private let hours = readings.indices.map { "\($0):00" }

private let chartConfig: ChartConfig = [
  "range": .init("Range", color: Palette.red500),
  "average": .init("Average", color: Palette.red500),
]

private let low = Int(readings.map(\.low).min() ?? 0)
private let high = Int(readings.map(\.high).max() ?? 0)

public struct AreaChartRange: View {
  @State private var selection: Int?

  public init() {}

  public var body: some View {
    Card(.filled, size: .sm) {
      VStack(alignment: .leading, spacing: 2) {
        Text("Heart rate")
          .textStyle(.sm, weight: .semibold)
          .foregroundStyle(EsuissColor.destructive)
        HStack(alignment: .firstTextBaseline, spacing: 4) {
          Text(verbatim: "\(low)–\(high)")
            .textStyle(.xl3, weight: .semibold)
            .monospacedDigit()
          Text("BPM")
            .textStyle(.base, weight: .semibold)
            .foregroundStyle(EsuissColor.mutedForeground)
        }
        Text("Today")
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      .padding(.horizontal, 16)
      CardContent {
        ChartContainer(config: chartConfig) {
          ChartAnimator { progress in
            Chart {
              ForEach(readings.indices, id: \.self) { index in
                AreaMark(x: .value("Hour", index), yStart: .value("Low", readings[index].low), yEnd: .value("High", readings[index].high))
                  .interpolationMethod(.monotone)
                  .foregroundStyle(chartConfig.color("range").opacity(0.22))
                LineMark(x: .value("Hour", index), y: .value("Average", readings[index].average))
                  .interpolationMethod(.monotone)
                  .lineStyle(StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
                  .foregroundStyle(chartConfig.color("average"))
              }
              .chartReveal(progress, x: 0...(readings.count - 1))
              if let selection {
                PointMark(x: .value("Hour", selection), y: .value("Average", readings[selection].average))
                  .symbolSize(64)
                  .foregroundStyle(chartConfig.color("average"))
              }
            }
            .chartXScale(domain: 0...(readings.count - 1), range: .plotDimension(padding: 16))
            .chartYScale(domain: 40...170)
            .chartXAxis { ChartAxis.x(values: [0, 6, 12, 18]) { hours[$0] } }
            .chartYAxis { ChartAxis.y(values: [50.0, 100, 150]) { "\(Int($0))" } }
            .chartTooltip(
              x: Array(readings.indices),
              labels: hours,
              selection: $selection,
              accessibilityLabel: "Heart rate",
              items: { index in
                [
                  ChartTooltipItem("range", value: readings[index].high),
                  ChartTooltipItem("average", value: readings[index].average),
                ]
              }
            ) { payload in
              ChartTooltipContent(payload, indicator: .line) { item in
                Text(LocalizedStringKey(chartConfig.label(item.key)))
                  .foregroundStyle(EsuissColor.mutedForeground)
                Spacer(minLength: 12)
                Text(verbatim: item.key == "range"
                  ? "\(Int(readings[payload.index].low))–\(Int(readings[payload.index].high)) BPM"
                  : "\(Int(item.value)) BPM")
                  .scaledFont(TextStyle.xs, weight: .semibold)
                  .monospacedDigit()
              }
            }
          }
        }
        .frame(height: 160)
      }
    }
  }
}
```

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

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.ChartFill
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.ChartTooltipIndicator
import com.esuiss.ui.components.ComposedChart
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Typography

private class HeartRateRow(val hour: String, val low: Int, val high: Int, val average: Int)

private val readings = listOf(
    Triple(48, 62, 54), Triple(46, 60, 52), Triple(47, 64, 54), Triple(45, 61, 52),
    Triple(48, 63, 55), Triple(46, 66, 54), Triple(56, 78, 64), Triple(58, 82, 66),
    Triple(64, 88, 74), Triple(66, 90, 76), Triple(64, 92, 74), Triple(66, 89, 75),
    Triple(65, 94, 76), Triple(66, 90, 75), Triple(64, 88, 74), Triple(65, 91, 76),
    Triple(66, 92, 75), Triple(68, 96, 78), Triple(104, 158, 126), Triple(102, 150, 122),
    Triple(70, 94, 78), Triple(66, 90, 74), Triple(60, 82, 68), Triple(52, 70, 58),
)

private val chartData = readings.mapIndexed { hour, (low, high, average) -> HeartRateRow("$hour:00", low, high, average) }

private val chartConfig = ChartConfig(
    "range" to ChartConfigItem("Range") { Palette.red500 },
    "average" to ChartConfigItem("Average") { Palette.red500 },
)

private val low = chartData.minOf { it.low }

private val high = chartData.maxOf { it.high }

@Composable
fun AreaChartRange(modifier: Modifier = Modifier) {
    val colors = EsuissTheme.colors
    val value = Typography.xs.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum")
    Card(modifier, variant = CardVariant.Filled, size = CardSize.Sm) {
        CardContent {
            Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
                BasicText("Heart rate", style = Typography.sm.copy(color = colors.destructive, fontWeight = FontWeight.SemiBold))
                Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                    BasicText(
                        "$low–$high",
                        Modifier.alignByBaseline(),
                        style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                    )
                    BasicText(
                        "BPM",
                        Modifier.alignByBaseline(),
                        style = Typography.base.copy(color = colors.mutedForeground, fontWeight = FontWeight.SemiBold),
                    )
                }
                BasicText("Today", style = Typography.sm.copy(color = colors.mutedForeground))
            }
        }
        CardContent {
            ChartContainer(
                chartConfig,
                Modifier
                    .fillMaxWidth()
                    .height(160.dp),
            ) {
                ComposedChart(
                    chartData,
                    margin = ChartMargin(top = 10.dp, right = 4.dp, bottom = 0.dp, left = 16.dp),
                    accessibilityLabel = "Heart rate",
                ) {
                    CartesianGrid(strokeDasharray = listOf(2f, 4f))
                    XAxis(label = { it.hour }, tickMargin = 8.dp, interval = 5)
                    YAxis(
                        orientation = ChartAxisOrientation.Right,
                        tickMargin = 6.dp,
                        domain = 40.0..170.0,
                        ticks = listOf(50.0, 100.0, 150.0),
                        tickFormatter = { it.toInt().toString() },
                    )
                    ChartTooltip {
                        ChartTooltipContent(
                            indicator = ChartTooltipIndicator.Line,
                            formatter = { item ->
                                Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                                    BasicText(
                                        chartConfig[item.key]?.label ?: item.key,
                                        style = Typography.xs.copy(color = colors.mutedForeground),
                                    )
                                    BasicText(
                                        if (item.base != null) "${item.base.toInt()}–${item.value.toInt()} BPM" else "${item.value.toInt()} BPM",
                                        style = value,
                                    )
                                }
                            },
                        )
                    }
                    Area(
                        "range",
                        value = { it.high.toDouble() },
                        base = { it.low.toDouble() },
                        curve = ChartCurve.Monotone,
                        strokeWidth = 0.dp,
                        fill = ChartFill.Solid(alpha = 0.22f),
                    )
                    Line("average", value = { it.average.toDouble() }, curve = ChartCurve.Monotone, strokeWidth = 2.dp)
                }
            }
        }
    }
}
```

A `ComposedChart` that pairs a range `Area`, filled between each hour's low and high reading, with a `Line` for the hourly average. The header shows the day's lowest and highest values in BPM, and the tooltip formats both the range and the average with a custom row built from `Text`.

## Installation

**React Native**

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

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 area-chart-range
```

**SwiftUI**

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

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add area-chart-range --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).

### Manual

**React Native**

**Uniwind**

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

2. Copy the source into `components/charts/area-chart-range.tsx`:

- `uniwind/components/charts/area-chart-range.tsx`: shown above

3. Update the import paths to match your project.

**NativeWind**

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

2. Copy the source into `components/charts/area-chart-range.tsx`:

```tsx title="nativewind/components/charts/area-chart-range.tsx"
import { View } from "react-native"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import {
  Area,
  CartesianGrid,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ComposedChart,
  Line,
  XAxis,
  YAxis,
} from "@/components/ui/chart"
import { Text } from "@/components/ui/text"

const readings: [low: number, high: number, average: number][] = [
  [48, 62, 54],
  [46, 60, 52],
  [47, 64, 54],
  [45, 61, 52],
  [48, 63, 55],
  [46, 66, 54],
  [56, 78, 64],
  [58, 82, 66],
  [64, 88, 74],
  [66, 90, 76],
  [64, 92, 74],
  [66, 89, 75],
  [65, 94, 76],
  [66, 90, 75],
  [64, 88, 74],
  [65, 91, 76],
  [66, 92, 75],
  [68, 96, 78],
  [104, 158, 126],
  [102, 150, 122],
  [70, 94, 78],
  [66, 90, 74],
  [60, 82, 68],
  [52, 70, 58],
]

const chartData = readings.map(([low, high, average], hour) => ({
  hour: `${hour}:00`,
  range: [low, high] as const,
  average,
}))

const chartConfig = {
  range: { label: "Range", color: "red-500" },
  average: { label: "Average", color: "red-500" },
} satisfies ChartConfig

const low = Math.min(...chartData.map((point) => point.range[0]))
const high = Math.max(...chartData.map((point) => point.range[1]))

export function AreaChartRange() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <Text className="text-sm font-semibold text-destructive">
          Heart rate
        </Text>
        <View className="flex-row items-baseline gap-1">
          <Text className="text-3xl font-semibold tracking-tight tabular-nums">
            {low}–{high}
          </Text>
          <Text className="text-base font-semibold text-muted-foreground">
            BPM
          </Text>
        </View>
        <Text className="text-sm text-muted-foreground">Today</Text>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          accessibilityLabel="Heart rate"
          className="h-40 w-full"
        >
          <ComposedChart
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="hour" tickMargin={8} interval={5} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              domain={[40, 170]}
              ticks={[50, 100, 150]}
            />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  indicator="line"
                  formatter={(value, name) => (
                    <View className="w-full flex-row justify-between gap-3">
                      <Text className="text-muted-foreground">
                        {chartConfig[name as keyof typeof chartConfig].label}
                      </Text>
                      <Text className="font-semibold tabular-nums">
                        {typeof value === "number" ? value : value.join("–")}{" "}
                        BPM
                      </Text>
                    </View>
                  )}
                />
              }
            />
            <Area
              dataKey="range"
              type="monotone"
              stroke="none"
              fillOpacity={0.22}
            />
            <Line dataKey="average" type="monotone" strokeWidth={2} />
          </ComposedChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
```

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

2. Copy the source into `components/charts/area-chart-range.tsx`:

```tsx title="stylesheet/components/charts/area-chart-range.tsx"
import { StyleSheet, View } from "react-native"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import {
  Area,
  CartesianGrid,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ComposedChart,
  Line,
  XAxis,
  YAxis,
} from "@/components/ui/chart"
import { Text } from "@/components/ui/text"
import { fontSize, useTheme } from "@/components/ui/theme"

const readings: [low: number, high: number, average: number][] = [
  [48, 62, 54],
  [46, 60, 52],
  [47, 64, 54],
  [45, 61, 52],
  [48, 63, 55],
  [46, 66, 54],
  [56, 78, 64],
  [58, 82, 66],
  [64, 88, 74],
  [66, 90, 76],
  [64, 92, 74],
  [66, 89, 75],
  [65, 94, 76],
  [66, 90, 75],
  [64, 88, 74],
  [65, 91, 76],
  [66, 92, 75],
  [68, 96, 78],
  [104, 158, 126],
  [102, 150, 122],
  [70, 94, 78],
  [66, 90, 74],
  [60, 82, 68],
  [52, 70, 58],
]

const chartData = readings.map(([low, high, average], hour) => ({
  hour: `${hour}:00`,
  range: [low, high] as const,
  average,
}))

const chartConfig = {
  range: { label: "Range", color: "red-500" },
  average: { label: "Average", color: "red-500" },
} satisfies ChartConfig

const low = Math.min(...chartData.map((point) => point.range[0]))
const high = Math.max(...chartData.map((point) => point.range[1]))

const styles = StyleSheet.create({
  header: { gap: 2 },
  eyebrow: { ...fontSize.sm, fontWeight: "600" },
  reading: { flexDirection: "row", alignItems: "baseline", gap: 4 },
  value: {
    ...fontSize["3xl"],
    fontWeight: "600",
    letterSpacing: -0.75,
    fontVariant: ["tabular-nums"],
  },
  unit: { ...fontSize.base, fontWeight: "600" },
  caption: { ...fontSize.sm },
  chart: { height: 160 },
  row: {
    width: "100%",
    flexDirection: "row",
    justifyContent: "space-between",
    gap: 12,
  },
  strong: { fontWeight: "600", fontVariant: ["tabular-nums"] },
})

export function AreaChartRange() {
  const { colors } = useTheme()
  return (
    <Card variant="filled" size="sm">
      <CardHeader style={styles.header}>
        <Text style={[styles.eyebrow, { color: colors.destructive }]}>
          Heart rate
        </Text>
        <View style={styles.reading}>
          <Text style={styles.value}>
            {low}–{high}
          </Text>
          <Text style={[styles.unit, { color: colors.mutedForeground }]}>
            BPM
          </Text>
        </View>
        <Text style={[styles.caption, { color: colors.mutedForeground }]}>
          Today
        </Text>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          accessibilityLabel="Heart rate"
          style={styles.chart}
        >
          <ComposedChart
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="hour" tickMargin={8} interval={5} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              domain={[40, 170]}
              ticks={[50, 100, 150]}
            />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  indicator="line"
                  formatter={(value, name) => (
                    <View style={styles.row}>
                      <Text style={{ color: colors.mutedForeground }}>
                        {chartConfig[name as keyof typeof chartConfig].label}
                      </Text>
                      <Text style={styles.strong}>
                        {typeof value === "number" ? value : value.join("–")}{" "}
                        BPM
                      </Text>
                    </View>
                  )}
                />
              }
            />
            <Area
              dataKey="range"
              type="monotone"
              stroke="none"
              fillOpacity={0.22}
            />
            <Line dataKey="average" type="monotone" strokeWidth={2} />
          </ComposedChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
```

3. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

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

3. Copy the source and every file it uses:

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

- `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/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)

4. 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")
```

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/AreaChartRange.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/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/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)

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

## Usage

```tsx
import { AreaChartRange } from "@/components/charts/area-chart-range"
```

```tsx
<AreaChartRange />
```

Replace the `readings` array with `[low, high, average]` tuples for each hour; the header range and the chart data are derived from it. Adjust the `YAxis` `domain` and `ticks` to fit your values. The component takes no props.

