# Line Chart Goal Dots

Nightly sleep hours with a goal line and dots colored green or orange depending on whether the goal was met.

Line Chart Goal Dots 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-goal-dots

**React**

**Example**

```tsx title="components/charts/line-chart-goal-dots.tsx"
"use client"

import {
  CartesianGrid,
  Line,
  LineChart,
  ReferenceLine,
  XAxis,
  YAxis,
} from "recharts"

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

const GOAL = 7

const chartData = [
  { day: "Mon", hours: 6.8 },
  { day: "Tue", hours: 7.4 },
  { day: "Wed", hours: 5.9 },
  { day: "Thu", hours: 7.9 },
  { day: "Fri", hours: 6.4 },
  { day: "Sat", hours: 8.6 },
  { day: "Sun", hours: 7.2 },
]

const AVERAGE =
  chartData.reduce((sum, item) => sum + item.hours, 0) / chartData.length

const chartConfig = {
  hours: { label: "Sleep", color: "var(--chart-3)" },
} satisfies ChartConfig

function formatHours(hours: number) {
  const minutes = Math.round(hours * 60)
  return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
}

export function LineChartGoalDots() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <p className="text-sm font-semibold text-[color-mix(in_oklab,var(--color-indigo-500),var(--foreground)_10%)] dark:text-[color-mix(in_oklab,var(--color-indigo-500),var(--foreground)_35%)]">
          Sleep
        </p>
        <p className="flex items-baseline gap-1">
          <span className="text-3xl font-semibold tracking-tight tabular-nums">
            {formatHours(AVERAGE)}
          </span>
        </p>
        <p className="text-sm text-muted-foreground">
          Goal 7 hours · green nights met it
        </p>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          className="aspect-auto h-40 w-full"
        >
          <LineChart
            accessibilityLayer
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis
              dataKey="day"
              tickLine={false}
              axisLine={false}
              tickMargin={8}
              interval={0}
            />
            <YAxis
              orientation="right"
              tickLine={false}
              axisLine={false}
              tickMargin={6}
              width="auto"
              domain={[5, 9]}
              ticks={[5, 7, 9]}
              tickFormatter={(value: number) => `${value}h`}
            />
            <ReferenceLine
              y={GOAL}
              stroke="var(--color-green-500)"
              strokeDasharray="3 3"
            />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  hideIndicator
                  formatter={(value) => (
                    <span className="font-semibold tabular-nums">
                      {formatHours(Number(value))}
                    </span>
                  )}
                />
              }
            />
            <Line
              dataKey="hours"
              type="monotone"
              stroke="var(--color-hours)"
              strokeWidth={2}
              dot={({ cx, cy, index, payload }) => (
                <circle
                  key={index}
                  cx={cx}
                  cy={cy}
                  r={4.5}
                  fill={
                    payload.hours >= GOAL
                      ? "var(--color-green-500)"
                      : "var(--color-orange-500)"
                  }
                  stroke="var(--muted)"
                  strokeWidth={2}
                />
              )}
            />
          </LineChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
```

Nightly sleep plotted against a dashed goal line, with each dot colored green if the goal was met and orange if not. Pick it over the plain goal chart when readers should spot hits and misses at a glance.

## Installation

### CLI

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

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-goal-dots
```

### 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/line-chart-goal-dots.tsx`:

```tsx title="components/charts/line-chart-goal-dots.tsx"
"use client"

import {
  CartesianGrid,
  Line,
  LineChart,
  ReferenceLine,
  XAxis,
  YAxis,
} from "recharts"

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

const GOAL = 7

const chartData = [
  { day: "Mon", hours: 6.8 },
  { day: "Tue", hours: 7.4 },
  { day: "Wed", hours: 5.9 },
  { day: "Thu", hours: 7.9 },
  { day: "Fri", hours: 6.4 },
  { day: "Sat", hours: 8.6 },
  { day: "Sun", hours: 7.2 },
]

const AVERAGE =
  chartData.reduce((sum, item) => sum + item.hours, 0) / chartData.length

const chartConfig = {
  hours: { label: "Sleep", color: "var(--chart-3)" },
} satisfies ChartConfig

function formatHours(hours: number) {
  const minutes = Math.round(hours * 60)
  return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
}

export function LineChartGoalDots() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <p className="text-sm font-semibold text-[color-mix(in_oklab,var(--color-indigo-500),var(--foreground)_10%)] dark:text-[color-mix(in_oklab,var(--color-indigo-500),var(--foreground)_35%)]">
          Sleep
        </p>
        <p className="flex items-baseline gap-1">
          <span className="text-3xl font-semibold tracking-tight tabular-nums">
            {formatHours(AVERAGE)}
          </span>
        </p>
        <p className="text-sm text-muted-foreground">
          Goal 7 hours · green nights met it
        </p>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          className="aspect-auto h-40 w-full"
        >
          <LineChart
            accessibilityLayer
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis
              dataKey="day"
              tickLine={false}
              axisLine={false}
              tickMargin={8}
              interval={0}
            />
            <YAxis
              orientation="right"
              tickLine={false}
              axisLine={false}
              tickMargin={6}
              width="auto"
              domain={[5, 9]}
              ticks={[5, 7, 9]}
              tickFormatter={(value: number) => `${value}h`}
            />
            <ReferenceLine
              y={GOAL}
              stroke="var(--color-green-500)"
              strokeDasharray="3 3"
            />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  hideIndicator
                  formatter={(value) => (
                    <span className="font-semibold tabular-nums">
                      {formatHours(Number(value))}
                    </span>
                  )}
                />
              }
            />
            <Line
              dataKey="hours"
              type="monotone"
              stroke="var(--color-hours)"
              strokeWidth={2}
              dot={({ cx, cy, index, payload }) => (
                <circle
                  key={index}
                  cx={cx}
                  cy={cy}
                  r={4.5}
                  fill={
                    payload.hours >= GOAL
                      ? "var(--color-green-500)"
                      : "var(--color-orange-500)"
                  }
                  stroke="var(--muted)"
                  strokeWidth={2}
                />
              )}
            />
          </LineChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { LineChartGoalDots } from "@/components/charts/line-chart-goal-dots"
```

```tsx
<LineChartGoalDots />
```

Replace `chartData` and set `GOAL` to your target; dot colors update from the comparison automatically. Adjust the value axis `domain` and `ticks` to your range, and change the two dot colors in the custom `dot` renderer.

## Notes

The headline figure is hard-coded text, not computed from the data, so wire it up when you plug in real values. Color alone marks hit or miss on the chart, so keep a text explanation like the one in the header.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx title="uniwind/components/charts/line-chart-goal-dots.tsx"
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"

const GOAL = 7

const chartData = [
  { day: "Mon", hours: 6.8 },
  { day: "Tue", hours: 7.4 },
  { day: "Wed", hours: 5.9 },
  { day: "Thu", hours: 7.9 },
  { day: "Fri", hours: 6.4 },
  { day: "Sat", hours: 8.6 },
  { day: "Sun", hours: 7.2 },
]

const chartConfig = {
  hours: { label: "Sleep", color: "chart-3" },
} satisfies ChartConfig

function formatHours(hours: number) {
  const minutes = Math.round(hours * 60)
  return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
}

export function LineChartGoalDots() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <Text className="text-sm font-semibold text-indigo-500">Sleep</Text>
        <View className="flex-row items-baseline gap-1">
          <Text className="text-3xl font-semibold tracking-tight tabular-nums">
            7h 9m
          </Text>
        </View>
        <Text className="text-sm text-muted-foreground">
          Goal 7 hours · green nights met it
        </Text>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          accessibilityLabel="Sleep"
          className="h-40 w-full"
        >
          <LineChart
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="day" tickMargin={8} interval={0} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              domain={[5, 9]}
              ticks={[5, 7, 9]}
              tickFormatter={(value: number) => `${value}h`}
            />
            <ReferenceLine y={GOAL} stroke="green-500" strokeDasharray="3 3" />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  hideIndicator
                  formatter={(value) => (
                    <Text className="font-semibold tabular-nums">
                      {formatHours(Number(value))}
                    </Text>
                  )}
                />
              }
            />
            <Line
              dataKey="hours"
              type="monotone"
              strokeWidth={2}
              dot={({ index }) => ({
                r: 4.5,
                fill:
                  (chartData[index]?.hours ?? 0) >= GOAL
                    ? "green-500"
                    : "chart-4",
                stroke: "muted",
                strokeWidth: 2,
              })}
            />
          </LineChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
```

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

private let goal = 7.0

private let chartData: [(day: String, hours: Double)] = [
  ("Mon", 6.8), ("Tue", 7.4), ("Wed", 5.9), ("Thu", 7.9), ("Fri", 6.4), ("Sat", 8.6), ("Sun", 7.2),
]

private let chartConfig: ChartConfig = [
  "hours": .init("Sleep", color: EsuissColor.chart3),
]

private func formatHours(_ hours: Double) -> String {
  let minutes = Int((hours * 60).rounded())
  return "\(minutes / 60)h \(minutes % 60)m"
}

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

  public init() {}

  public var body: some View {
    Card(.filled, size: .sm) {
      VStack(alignment: .leading, spacing: 2) {
        Text("Sleep")
          .textStyle(.sm, weight: .semibold)
          .foregroundStyle(chartConfig.color("hours"))
        Text(verbatim: "7h 9m")
          .textStyle(.xl3, weight: .semibold)
          .monospacedDigit()
        Text("Goal 7 hours · green nights met it")
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      .padding(.horizontal, 16)
      CardContent {
        ChartContainer(config: chartConfig) {
          ChartAnimator { progress in
            Chart {
              RuleMark(y: .value("Goal", goal))
                .lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3]))
                .foregroundStyle(Palette.green500)
              ForEach(chartData.indices, id: \.self) { index in
                LineMark(x: .value("Day", index), y: .value("Hours", chartData[index].hours))
                  .interpolationMethod(.monotone)
                  .lineStyle(StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
                  .foregroundStyle(chartConfig.color("hours"))
              }
              .chartReveal(progress, x: 0...(chartData.count - 1))
              ForEach(chartData.indices, id: \.self) { index in
                PointMark(x: .value("Day", index), y: .value("Hours", chartData[index].hours))
                  .symbol {
                    Circle()
                      .fill(chartData[index].hours >= goal ? Palette.green500 : Palette.orange500)
                      .stroke(EsuissColor.muted, lineWidth: 2)
                      .frame(width: selection == index ? 13 : 11, height: selection == index ? 13 : 11)
                  }
              }
              .chartReveal(progress, x: 0...(chartData.count - 1))
            }
            .chartXScale(domain: 0...(chartData.count - 1), range: .plotDimension(padding: 16))
            .chartYScale(domain: 5...9)
            .chartXAxis { ChartAxis.x(values: Array(chartData.indices)) { chartData[$0].day } }
            .chartYAxis { ChartAxis.y(values: [5.0, 7, 9]) { "\($0.formatted())h" } }
            .chartTooltip(
              x: Array(chartData.indices),
              labels: chartData.map(\.day),
              selection: $selection,
              accessibilityLabel: "Sleep",
              items: { [ChartTooltipItem("hours", value: chartData[$0].hours)] }
            ) { payload in
              ChartTooltipContent(payload, hideIndicator: true) { item in
                Text(verbatim: formatHours(item.value))
                  .scaledFont(TextStyle.xs, weight: .semibold)
                  .monospacedDigit()
              }
            }
          }
        }
        .frame(height: 160)
      }
    }
  }
}
```

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

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.graphics.lerp
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.ChartMargin
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.LineChart
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Typography
import kotlin.math.roundToInt

private const val GOAL = 7.0

private class LineGoalDotsRow(val day: String, val hours: Double)

private val chartData = listOf(
    LineGoalDotsRow("Mon", 6.8),
    LineGoalDotsRow("Tue", 7.4),
    LineGoalDotsRow("Wed", 5.9),
    LineGoalDotsRow("Thu", 7.9),
    LineGoalDotsRow("Fri", 6.4),
    LineGoalDotsRow("Sat", 8.6),
    LineGoalDotsRow("Sun", 7.2),
)

private val chartConfig = ChartConfig(
    "hours" to ChartConfigItem("Sleep") { chart3 },
)

private fun formatHours(hours: Double): String {
    val minutes = (hours * 60).roundToInt()
    return "${minutes / 60}h ${minutes % 60}m"
}

@Composable
fun LineChartGoalDots(modifier: Modifier = Modifier) {
    val colors = EsuissTheme.colors
    Card(modifier, variant = CardVariant.Filled, size = CardSize.Sm) {
        CardContent {
            Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
                BasicText(
                    "Sleep",
                    style = Typography.sm.copy(
                        color = lerp(colors.chart3, colors.foreground, if (colors.isDark) 0.35f else 0.1f),
                        fontWeight = FontWeight.SemiBold,
                    ),
                )
                BasicText(
                    "7h 9m",
                    style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                )
                BasicText("Goal 7 hours · green nights met it", style = Typography.sm.copy(color = colors.mutedForeground))
            }
        }
        CardContent {
            ChartContainer(
                chartConfig,
                Modifier
                    .fillMaxWidth()
                    .height(160.dp),
            ) {
                LineChart(
                    chartData,
                    margin = ChartMargin(top = 10.dp, right = 4.dp, bottom = 0.dp, left = 16.dp),
                    accessibilityLabel = "Sleep",
                ) {
                    CartesianGrid(strokeDasharray = listOf(2f, 4f))
                    XAxis(label = { it.day }, tickMargin = 8.dp, interval = 0)
                    YAxis(
                        orientation = ChartAxisOrientation.Right,
                        tickMargin = 6.dp,
                        domain = 5.0..9.0,
                        ticks = listOf(5.0, 7.0, 9.0),
                        tickFormatter = { "${it.toInt()}h" },
                    )
                    ReferenceLine(GOAL, stroke = Palette.green500, strokeDasharray = listOf(3f, 3f))
                    ChartTooltip {
                        ChartTooltipContent(
                            hideIndicator = true,
                            formatter = { item ->
                                BasicText(
                                    formatHours(item.value),
                                    style = Typography.xs.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                                )
                            },
                        )
                    }
                    Line(
                        "hours",
                        value = { it.hours },
                        curve = ChartCurve.Monotone,
                        strokeWidth = 2.dp,
                        dot = ChartDot(4.5.dp, strokeColor = colors.muted, strokeWidth = 2.dp),
                        dotColor = { if (it.hours >= GOAL) Palette.green500 else colors.chart4 },
                    )
                }
            }
        }
    }
}
```

Seven nights of sleep drawn as a monotone line. The `dot` prop is a function that colors each point by comparing it with `GOAL`, and a dashed green `ReferenceLine` marks the goal itself. The tooltip formats values as hours and minutes.

## Installation

**React Native**

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

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-goal-dots
```

**SwiftUI**

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

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add line-chart-goal-dots --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/line-chart-goal-dots.tsx`:

- `uniwind/components/charts/line-chart-goal-dots.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/line-chart-goal-dots.tsx`:

```tsx title="nativewind/components/charts/line-chart-goal-dots.tsx"
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"

const GOAL = 7

const chartData = [
  { day: "Mon", hours: 6.8 },
  { day: "Tue", hours: 7.4 },
  { day: "Wed", hours: 5.9 },
  { day: "Thu", hours: 7.9 },
  { day: "Fri", hours: 6.4 },
  { day: "Sat", hours: 8.6 },
  { day: "Sun", hours: 7.2 },
]

const chartConfig = {
  hours: { label: "Sleep", color: "chart-3" },
} satisfies ChartConfig

function formatHours(hours: number) {
  const minutes = Math.round(hours * 60)
  return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
}

export function LineChartGoalDots() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <Text className="text-sm font-semibold text-indigo-500">Sleep</Text>
        <View className="flex-row items-baseline gap-1">
          <Text className="text-3xl font-semibold tracking-tight tabular-nums">
            7h 9m
          </Text>
        </View>
        <Text className="text-sm text-muted-foreground">
          Goal 7 hours · green nights met it
        </Text>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          accessibilityLabel="Sleep"
          className="h-40 w-full"
        >
          <LineChart
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="day" tickMargin={8} interval={0} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              domain={[5, 9]}
              ticks={[5, 7, 9]}
              tickFormatter={(value: number) => `${value}h`}
            />
            <ReferenceLine y={GOAL} stroke="green-500" strokeDasharray="3 3" />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  hideIndicator
                  formatter={(value) => (
                    <Text className="font-semibold tabular-nums">
                      {formatHours(Number(value))}
                    </Text>
                  )}
                />
              }
            />
            <Line
              dataKey="hours"
              type="monotone"
              strokeWidth={2}
              dot={({ index }) => ({
                r: 4.5,
                fill:
                  (chartData[index]?.hours ?? 0) >= GOAL
                    ? "green-500"
                    : "chart-4",
                stroke: "muted",
                strokeWidth: 2,
              })}
            />
          </LineChart>
        </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/line-chart-goal-dots.tsx`:

```tsx title="stylesheet/components/charts/line-chart-goal-dots.tsx"
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, useTheme } from "@/components/ui/theme"

const GOAL = 7

const chartData = [
  { day: "Mon", hours: 6.8 },
  { day: "Tue", hours: 7.4 },
  { day: "Wed", hours: 5.9 },
  { day: "Thu", hours: 7.9 },
  { day: "Fri", hours: 6.4 },
  { day: "Sat", hours: 8.6 },
  { day: "Sun", hours: 7.2 },
]

const chartConfig = {
  hours: { label: "Sleep", color: "chart-3" },
} satisfies ChartConfig

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"],
  },
  caption: { ...fontSize.sm },
  strong: { fontWeight: "600", fontVariant: ["tabular-nums"] },
  chart: { height: 160 },
})

function formatHours(hours: number) {
  const minutes = Math.round(hours * 60)
  return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
}

export function LineChartGoalDots() {
  const { colors } = useTheme()
  return (
    <Card variant="filled" size="sm">
      <CardHeader style={styles.header}>
        <Text style={[styles.eyebrow, { color: colors.chart3 }]}>Sleep</Text>
        <View style={styles.reading}>
          <Text style={styles.value}>7h 9m</Text>
        </View>
        <Text style={[styles.caption, { color: colors.mutedForeground }]}>
          Goal 7 hours · green nights met it
        </Text>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={chartConfig}
          accessibilityLabel="Sleep"
          style={styles.chart}
        >
          <LineChart
            data={chartData}
            margin={{ top: 10, right: 4, left: 16, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="day" tickMargin={8} interval={0} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              domain={[5, 9]}
              ticks={[5, 7, 9]}
              tickFormatter={(value: number) => `${value}h`}
            />
            <ReferenceLine y={GOAL} stroke="green-500" strokeDasharray="3 3" />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  hideIndicator
                  formatter={(value) => (
                    <Text style={styles.strong}>
                      {formatHours(Number(value))}
                    </Text>
                  )}
                />
              }
            />
            <Line
              dataKey="hours"
              type="monotone"
              strokeWidth={2}
              dot={({ index }) => ({
                r: 4.5,
                fill:
                  (chartData[index]?.hours ?? 0) >= GOAL
                    ? "green-500"
                    : "chart-4",
                stroke: "muted",
                strokeWidth: 2,
              })}
            />
          </LineChart>
        </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/LineChartGoalDots.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/LineChartGoalDots.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 { LineChartGoalDots } from "@/components/charts/line-chart-goal-dots"
```

```tsx
<LineChartGoalDots />
```

Replace `chartData` with your nightly hours and set `GOAL`. The header average is static text, so update it with your own value. The component takes no props.

