suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Chart

Themed building blocks for Recharts charts, with a shared color config, tooltip and legend.

Chart is an open-source component 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.

import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"

const visitors = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 73, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 214, mobile: 140 },
]

const visitorsConfig = {
  desktop: { label: "Desktop", color: "var(--chart-1)" },
  mobile: { label: "Mobile", color: "var(--color-green-500)" },
} satisfies ChartConfig

const shortMonth = (value: string) => value.slice(0, 3)

export default function ChartDemo() {
  return (
    <div className="w-[min(32rem,calc(100vw-2rem))]">
      <ChartContainer config={visitorsConfig} className="w-full">
        {
          <BarChart accessibilityLayer data={visitors}>
            <CartesianGrid vertical={false} />
            <XAxis
              dataKey="month"
              tickLine={false}
              tickMargin={10}
              axisLine={false}
              tickFormatter={shortMonth}
            />
            <ChartTooltip content={<ChartTooltipContent />} />
            <Bar dataKey="desktop" fill="var(--color-desktop)" radius={8} />
          </BarChart>
        }
      </ChartContainer>
    </div>
  )
}

Chart does not draw charts itself; it styles charts you compose with recharts. A ChartContainer takes a config that maps each data key to a label, an optional icon and a color, and wraps your Recharts chart in a ResponsiveContainer. Inside the chart, use ChartTooltip with content={<ChartTooltipContent />} and ChartLegend with content={<ChartLegendContent />} to render a tooltip and legend that read labels and colors from the config.

Use it for bar, line, area and pie charts in dashboards and reports. Because you work with Recharts directly, every Recharts chart type and prop remains available.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/chart

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add chart

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add recharts
  1. Copy the source into components/ui/chart.tsx:
components/ui/chart.tsx
"use client"

import * as React from "react"
import type { TooltipValueType } from "recharts"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"

const THEMES = { light: "", dark: ".dark" } as const

const INITIAL_DIMENSION = { width: 320, height: 200 } as const
const UNSAFE_IDENTIFIER_CHARACTERS = /[^\w-]/g
const UNSAFE_VALUE_CHARACTERS = /[;{}<>\\]/
type TooltipNameType = number | string

export type ChartConfig = Record<
  string,
  1. Update the import paths to match your project.

Usage

import { ChartContainer, ChartTooltipContent, ChartLegendContent } from "@/components/ui/chart"
<div className="w-[min(32rem,calc(100vw-2rem))]">
  <ChartContainer config={visitorsConfig} className="w-full">{(
        <BarChart accessibilityLayer data={visitors}>
          <CartesianGrid vertical={false} />
          <XAxis
            dataKey="month"
            tickLine={false}
            tickMargin={10}
            axisLine={false}
            tickFormatter={shortMonth}
          />
          <ChartTooltip content={<ChartTooltipContent />} />
          <Bar dataKey="desktop" fill="var(--color-desktop)" radius={8} />
        </BarChart>
      )}</ChartContainer>
</div>

Each key in config becomes a CSS custom property scoped to the chart, such as --color-desktop. Reference it from your series, as in fill="var(--color-desktop)". Set color for a single value, or theme with light and dark values to switch colors with the .dark class.

For data where each row is its own category, such as a pie chart, put the color in the row (for example fill: "var(--color-safari)") and pass nameKey to ChartLegendContent so labels are looked up by that field.

The container is aspect-video by default; override its size with className, for example aspect-square max-w-72. Enable accessibilityLayer on the Recharts chart to support keyboard navigation.

ChartTooltip, ChartLegend and ChartStyle are also exported; the first two are the Recharts Tooltip and Legend components.

Examples

Multiple series

Two bar series with a legend and a dashed tooltip indicator.

import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
  type ChartConfig,
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"

const visitors = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 73, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 214, mobile: 140 },
]

const visitorsConfig = {
  desktop: { label: "Desktop", color: "var(--chart-1)" },
  mobile: { label: "Mobile", color: "var(--color-green-500)" },
} satisfies ChartConfig

const shortMonth = (value: string) => value.slice(0, 3)

export default function ChartMultipleSeries() {
  return (
    <div className="w-[min(32rem,calc(100vw-2rem))]">
      <ChartContainer config={visitorsConfig} className="w-full">
        {
          <BarChart accessibilityLayer data={visitors}>
            <CartesianGrid vertical={false} />
            <XAxis
              dataKey="month"
              tickLine={false}
              tickMargin={10}
              axisLine={false}
              tickFormatter={shortMonth}
            />
            <ChartTooltip
              content={<ChartTooltipContent indicator="dashed" />}
            />
            <ChartLegend content={<ChartLegendContent />} />
            <Bar dataKey="desktop" fill="var(--color-desktop)" radius={6} />
            <Bar dataKey="mobile" fill="var(--color-mobile)" radius={6} />
          </BarChart>
        }
      </ChartContainer>
    </div>
  )
}

Stacked

Stacks two bar series and hides the tooltip label.

import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
  type ChartConfig,
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"

const visitors = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 73, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 214, mobile: 140 },
]

const visitorsConfig = {
  desktop: { label: "Desktop", color: "var(--chart-1)" },
  mobile: { label: "Mobile", color: "var(--color-green-500)" },
} satisfies ChartConfig

const shortMonth = (value: string) => value.slice(0, 3)

export default function ChartStacked() {
  return (
    <div className="w-[min(32rem,calc(100vw-2rem))]">
      <ChartContainer config={visitorsConfig} className="w-full">
        {
          <BarChart accessibilityLayer data={visitors}>
            <CartesianGrid vertical={false} />
            <XAxis
              dataKey="month"
              tickLine={false}
              tickMargin={10}
              axisLine={false}
              tickFormatter={shortMonth}
            />
            <ChartTooltip content={<ChartTooltipContent hideLabel />} />
            <ChartLegend content={<ChartLegendContent />} />
            <Bar
              dataKey="desktop"
              stackId="a"
              fill="var(--color-desktop)"
              radius={[0, 0, 6, 6]}
            />
            <Bar
              dataKey="mobile"
              stackId="a"
              fill="var(--color-mobile)"
              radius={[6, 6, 0, 0]}
            />
          </BarChart>
        }
      </ChartContainer>
    </div>
  )
}

Line series

Two smooth lines with a line tooltip indicator.

import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"

const visitors = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 73, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 214, mobile: 140 },
]

const visitorsConfig = {
  desktop: { label: "Desktop", color: "var(--chart-1)" },
  mobile: { label: "Mobile", color: "var(--color-green-500)" },
} satisfies ChartConfig

const shortMonth = (value: string) => value.slice(0, 3)

export default function ChartLineSeries() {
  return (
    <div className="w-[min(32rem,calc(100vw-2rem))]">
      <ChartContainer config={visitorsConfig} className="w-full">
        {
          <LineChart
            accessibilityLayer
            data={visitors}
            margin={{ left: 12, right: 12 }}
          >
            <CartesianGrid vertical={false} />
            <XAxis
              dataKey="month"
              tickLine={false}
              tickMargin={10}
              axisLine={false}
              tickFormatter={shortMonth}
            />
            <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
            <Line
              dataKey="desktop"
              type="monotone"
              stroke="var(--color-desktop)"
              strokeWidth={2}
              dot={false}
            />
            <Line
              dataKey="mobile"
              type="monotone"
              stroke="var(--color-mobile)"
              strokeWidth={2}
              dot={false}
            />
          </LineChart>
        }
      </ChartContainer>
    </div>
  )
}

Area series

Two stacked, semi-transparent areas.

import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"

const visitors = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 73, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 214, mobile: 140 },
]

const visitorsConfig = {
  desktop: { label: "Desktop", color: "var(--chart-1)" },
  mobile: { label: "Mobile", color: "var(--color-green-500)" },
} satisfies ChartConfig

const shortMonth = (value: string) => value.slice(0, 3)

export default function ChartAreaSeries() {
  return (
    <div className="w-[min(32rem,calc(100vw-2rem))]">
      <ChartContainer config={visitorsConfig} className="w-full">
        {
          <AreaChart
            accessibilityLayer
            data={visitors}
            margin={{ left: 12, right: 12 }}
          >
            <CartesianGrid vertical={false} />
            <XAxis
              dataKey="month"
              tickLine={false}
              tickMargin={10}
              axisLine={false}
              tickFormatter={shortMonth}
            />
            <ChartTooltip content={<ChartTooltipContent indicator="dot" />} />
            <Area
              dataKey="mobile"
              type="natural"
              stackId="a"
              fill="var(--color-mobile)"
              fillOpacity={0.4}
              stroke="var(--color-mobile)"
            />
            <Area
              dataKey="desktop"
              type="natural"
              stackId="a"
              fill="var(--color-desktop)"
              fillOpacity={0.4}
              stroke="var(--color-desktop)"
            />
          </AreaChart>
        }
      </ChartContainer>
    </div>
  )
}

Donut

A pie chart with an inner radius, colored per row and labelled through nameKey.

import { Pie, PieChart } from "recharts"
import {
  type ChartConfig,
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"

const browsers = [
  { browser: "safari", visitors: 275, fill: "var(--color-safari)" },
  { browser: "chrome", visitors: 200, fill: "var(--color-chrome)" },
  { browser: "firefox", visitors: 187, fill: "var(--color-firefox)" },
  { browser: "edge", visitors: 173, fill: "var(--color-edge)" },
  { browser: "other", visitors: 90, fill: "var(--color-other)" },
]

const browsersConfig = {
  visitors: { label: "Visitors" },
  safari: { label: "Safari", color: "var(--chart-1)" },
  chrome: { label: "Chrome", color: "var(--color-green-500)" },
  firefox: { label: "Firefox", color: "var(--chart-4)" },
  edge: { label: "Edge", color: "var(--color-purple-500)" },
  other: { label: "Other", color: "var(--color-red-500)" },
} satisfies ChartConfig

export default function ChartDonut() {
  return (
    <div className="w-[min(32rem,calc(100vw-2rem))]">
      <ChartContainer
        config={browsersConfig}
        className="mx-auto aspect-square w-full max-w-72"
      >
        {
          <PieChart>
            <ChartTooltip content={<ChartTooltipContent hideLabel />} />
            <Pie
              data={browsers}
              dataKey="visitors"
              nameKey="browser"
              innerRadius={60}
              strokeWidth={4}
            />
            <ChartLegend content={<ChartLegendContent nameKey="browser" />} />
          </PieChart>
        }
      </ChartContainer>
    </div>
  )
}

API Reference

01

ChartContainer

Provides the config, injects the color variables and makes the chart responsive.

PropTypeDefaultDescription
configChartConfigRequiredMaps data keys to a label, optional icon, and a color or per-theme theme colors.
childrenReact.ComponentProps< typeof RechartsPrimitive.ResponsiveContainer >["children"]RequiredThe Recharts chart element to render responsively.
initialDimension{ width: number height: number }INITIAL_DIMENSIONWidth and height used before the container is measured. Defaults to 320 by 200.

Also accepts the props of React.ComponentProps<"div">.

02

ChartTooltipContent

A styled tooltip body that lists each series with its indicator, label and value.

PropTypeDefaultDescription
hideLabelbooleanfalseHides the tooltip's heading label.
hideIndicatorbooleanfalseHides the color indicator next to each series.
indicator"line" | "dot" | "dashed""dot"Shape of the series indicator: dot, line or dashed.
nameKeystring-Field used to look up each item's config entry instead of its name or data key.
labelKeystring-Field used to look up the heading label in the config.

Also accepts the props of React.ComponentProps<typeof RechartsPrimitive.Tooltip>, React.ComponentProps<"div">, RechartsPrimitive.DefaultTooltipContentProps< TooltipValueType, TooltipNameType >.

03

ChartLegendContent

A styled legend that lists series in config order with a swatch or icon.

PropTypeDefaultDescription
hideIconbooleanfalseShows a color swatch even when the config entry has an icon.
nameKeystring-Field used to look up each legend item's config entry instead of its data key.

Also accepts the props of React.ComponentProps<"div">, RechartsPrimitive.DefaultLegendContentProps.

Accessibility

  • With accessibilityLayer on the Recharts chart, the chart is focusable and arrow keys move between data points, showing the tooltip for each.
  • Charts are visual; provide a nearby summary or table when the exact values matter.
  • Do not rely on color alone to tell series apart; the legend and tooltip pair each color with its label.

Notes

  • Styling hooks: data-slot="chart" and a unique data-chart id on the container, which scopes the generated --color-* variables.
  • Config keys and color values containing unsafe characters are skipped when generating CSS.
  • The container restyles Recharts defaults, such as grid lines, axis ticks and the tooltip cursor, with the border, muted-foreground and muted colors.
  • Tooltip values that are numbers are formatted with toLocaleString().
iOS
Chart (tooltip dot) on iOS in light modeChart (tooltip dot) on iOS in dark mode
import * as React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Bar, BarChart, type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, XAxis } from "@/components/ui/chart"

const monthData = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 173, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 264, mobile: 140 },
]

const deviceConfig = {
  desktop: { label: "Desktop", color: "chart-1" },
  mobile: { label: "Mobile", color: "chart-2" },
} satisfies ChartConfig

function StackedTooltipChart({
  title,
  description,
  defaultIndex,
  content,
}: {
  title: string
  description: string
  defaultIndex: number
  content: React.ReactElement
}) {
  return (
    <Card variant="filled" size="sm">
      <CardHeader>
        <CardTitle>{title}</CardTitle>
        <CardDescription>{description}</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={deviceConfig}
          accessibilityLabel={title}
          className="h-52 w-full"
        >
          <BarChart
            data={monthData}
            margin={{ top: 70, right: 4, left: 4, bottom: 0 }}
          >
            <XAxis
              dataKey="month"
              tickMargin={8}
              interval={0}
              tickFormatter={(value: string) => value.slice(0, 3)}
            />
            <ChartTooltip
              defaultIndex={defaultIndex}
              cursor={false}
              content={content}
            />
            <Bar
              dataKey="desktop"
              stackId="a"
              radius={[0, 0, 4, 4]}
              maxBarSize={30}
            />
            <Bar
              dataKey="mobile"
              stackId="a"
              radius={[4, 4, 0, 0]}
              maxBarSize={30}
            />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipDotExample() {
  return (
    <StackedTooltipChart
      title="Dot indicator"
      description='indicator="dot"'
      defaultIndex={2}
      content={<ChartTooltipContent indicator="dot" />}
    />
  )
}
import Charts
import EsuissUI
import SwiftUI

struct ChartTooltipDotExample: View {
  var body: some View {
    ChartStackedTooltip(title: "Dot indicator", description: "indicator=\"dot\"", defaultIndex: 2) { payload in
      ChartTooltipContent(payload, indicator: .dot)
    }
  }
}

let chartMonthData: [(month: String, desktop: Double, mobile: Double)] = [
  ("January", 186, 80),
  ("February", 305, 200),
  ("March", 237, 120),
  ("April", 173, 190),
  ("May", 209, 130),
  ("June", 264, 140),
]

let chartDevices: ChartConfig = [
  "desktop": .init("Desktop", color: Palette.blue500),
  "mobile": .init("Mobile", color: Palette.teal500),
]

struct ChartStackedTooltip<Tooltip: View>: View {
  let title: String
  let description: String
  var config = chartDevices
  let defaultIndex: Int
  @ViewBuilder let tooltip: (ChartTooltipPayload) -> Tooltip

  var body: some View {
    Card(.filled, size: .sm) {
      CardHeader(LocalizedStringKey(title), description: LocalizedStringKey(description))
      CardContent {
        ChartContainer(config: config) {
          ChartAnimator(animationDuration: 400) { progress in
            Chart {
              ForEach(chartMonthData.indices, id: \.self) { index in
                let item = chartMonthData[index]
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", 0),
                  yEnd: .value("Desktop", item.desktop * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(bottomLeadingRadius: 4, bottomTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("desktop"))
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", item.desktop * progress),
                  yEnd: .value("Mobile", (item.desktop + item.mobile) * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(topLeadingRadius: 4, topTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("mobile"))
                ChartAnchorMark(x: .value("Month", item.month), y: .value("Mobile", item.desktop + item.mobile))
              }
            }
            .chartYScale(range: .plotDimension(endPadding: 70))
            .chartXAxis { ChartAxis.x(values: chartMonthData.map(\.month)) { String($0.prefix(3)) } }
            .chartYAxis(.hidden)
            .chartTooltip(
              x: chartMonthData.map(\.month),
              defaultIndex: defaultIndex,
              cursor: false,
              accessibilityLabel: title,
              items: { index in
                [
                  ChartTooltipItem("desktop", value: chartMonthData[index].desktop),
                  ChartTooltipItem("mobile", value: chartMonthData[index].mobile),
                ]
              },
              content: tooltip
            )
          }
        }
        .frame(height: 208)
      }
    }
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.phosphoricons.regular.Desktop
import com.esuiss.ui.components.BarChart
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.ChartTooltipIndicator
import com.esuiss.ui.components.ChartTooltipScope

@Composable
fun ChartTooltipDotExample() {
    ChartStackedTooltip("Dot indicator", "indicator=\"dot\"", 2) { ChartTooltipContent(indicator = ChartTooltipIndicator.Dot) }
}

class ChartMonth(val month: String, val desktop: Double, val mobile: Double)

val chartMonthData = listOf(
    ChartMonth("January", 186.0, 80.0),
    ChartMonth("February", 305.0, 200.0),
    ChartMonth("March", 237.0, 120.0),
    ChartMonth("April", 173.0, 190.0),
    ChartMonth("May", 209.0, 130.0),
    ChartMonth("June", 264.0, 140.0),
)

val chartDeviceConfig = ChartConfig(
    "desktop" to ChartConfigItem("Desktop") { chart1 },
    "mobile" to ChartConfigItem("Mobile") { chart2 },
)

@Composable
fun ChartTooltipCard(title: String, description: String, content: @Composable () -> Unit) {
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardHeader(title, description = description)
        CardContent { content() }
    }
}

@Composable
fun ChartStackedTooltip(
    title: String,
    description: String,
    defaultIndex: Int,
    config: ChartConfig = chartDeviceConfig,
    tooltip: @Composable ChartTooltipScope.() -> Unit,
) {
    ChartTooltipCard(title, description) {
        ChartContainer(config, Modifier.fillMaxWidth().height(208.dp)) {
            BarChart(
                chartMonthData,
                margin = ChartMargin(top = 70.dp, right = 4.dp, bottom = 0.dp, left = 4.dp),
                accessibilityLabel = title,
            ) {
                XAxis(label = { it.month }, tickMargin = 8.dp, interval = 0, tickFormatter = { it.take(3) })
                ChartTooltip(defaultIndex = defaultIndex, cursor = null, content = tooltip)
                Bar("desktop", value = { it.desktop }, stackId = "a", radius = ChartRadius(0.dp, 0.dp, 4.dp, 4.dp), maxBarSize = 30.dp)
                Bar("mobile", value = { it.mobile }, stackId = "a", radius = ChartRadius(4.dp, 4.dp, 0.dp, 0.dp), maxBarSize = 30.dp)
            }
        }
    }
}

Charts are composed declaratively: a chart root such as LineChart, AreaChart, BarChart, ComposedChart, PieChart, RadialBarChart or RadarChart goes inside a ChartContainer, and series and axes such as Line, Area, Bar, XAxis, YAxis and CartesianGrid are added as children. These child components render nothing themselves; the root reads them and draws everything with react-native-svg.

Charts animate in when they first render: bars grow from the baseline, lines draw along their path, areas reveal from left to right, pie and radial sectors sweep open and radar shapes grow from the center. When the data changes, shapes animate from their previous values to the new ones.

Colors are theme tokens, not hex values. A series color such as chart-1, chart-2 or primary resolves to the current token color, so charts follow light and dark mode automatically.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/chart

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add chart
$ pnpm dlx @esuiss/ui@latest add chart --platform swift
$ pnpm dlx @esuiss/ui@latest add chart --platform kotlin

The CLI also adds what it builds on: Icon, Text.

Manual

  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/chart.tsx.

  2. Copy the source into components/ui/chart.tsx:

uniwind/components/ui/chart.tsx
import { cn } from "cn"
import type {
  Icon as PhosphorIcon,
  IconProps as PhosphorIconProps,
} from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import Svg, {
  Circle,
  ClipPath,
  Defs,
  LinearGradient,
  Path,
  Rect,
  Stop,
} from "react-native-svg"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/chart.tsx.

  2. Copy the source into components/ui/chart.tsx:

nativewind/components/ui/chart.tsx
import { cn } from "cn"
import type {
  Icon as PhosphorIcon,
  IconProps as PhosphorIconProps,
} from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import Svg, {
  Circle,
  ClipPath,
  Defs,
  LinearGradient,
  Path,
  Rect,
  Stop,
} from "react-native-svg"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/chart.tsx.

  2. Copy the source into components/ui/chart.tsx:

stylesheet/components/ui/chart.tsx
import type { Icon as PhosphorIcon } from "phosphor-react-native"
import * as React from "react"
import {
  type StyleProp,
  StyleSheet,
  type TextStyle,
  View,
  type ViewStyle,
} from "react-native"
import Svg, {
  Circle,
  ClipPath,
  Defs,
  LinearGradient,
  Path,
  Rect,
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Copy the theme files every view uses:

swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/Chart.swift
import Charts
import SwiftUI

public struct ChartConfig: ExpressibleByDictionaryLiteral, Sendable {
  public struct Item: Sendable {
    public var label: String?
    public var color: Color?
    public var icon: Image?

    public init(_ label: String? = nil, color: Color? = nil, icon: Image? = nil) {
      self.label = label
      self.color = color
      self.icon = icon
    }
  }
swift/Components/Haptics.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var hapticsEnabled = true
}

extension View {
  public func haptics(_ enabled: Bool) -> some View {
    environment(\.hapticsEnabled, enabled)
  }

  func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
    modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
  }

  func hapticFeedback<Trigger: Equatable>(
swift/Components/Item.swift
import SwiftUI

public enum ItemVariant: CaseIterable, Sendable {
  case `default`, outline, muted
}

public enum ItemSize: CaseIterable, Sendable {
  case `default`, sm, xs

  var horizontalPadding: CGFloat {
    switch self {
    case .default: 16
    case .sm: 14
    case .xs: 12
    }
  }
swift/Components/Separator.swift
import SwiftUI

public struct Separator: View {
  let orientation: Axis

  public init(_ orientation: Axis = .horizontal) {
    self.orientation = orientation
  }

  public var body: some View {
    Rectangle()
      .fill(EsuissColor.border)
      .frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
      .frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
      .accessibilityHidden(true)
  }
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/Chart.kt
package com.esuiss.ui.components

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/FormSupport.kt
package com.esuiss.ui.components

import android.provider.Settings
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
kotlin/components/Haptics.kt
package com.esuiss.ui.components

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

val LocalHapticsEnabled = staticCompositionLocalOf { true }

@Composable
fun ProvideHaptics(enabled: Boolean, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalHapticsEnabled provides enabled, content = content)
}
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { ChartPalette, ChartContainer, LineChart, AreaChart, BarChart, ComposedChart, PieChart, RadialBarChart, RadarChart, ChartTooltipContent, ChartLegendContent } from "@/components/ui/chart"
<StackedTooltipChart
  title="Dot indicator"
  description='indicator="dot"'
  defaultIndex={2}
  content={<ChartTooltipContent indicator="dot" />}
/>

Describe each series in a ChartConfig object with a label, a token color and an optional icon, and pass it to ChartContainer as config. The container is full width with a 16:9 aspect ratio unless you give it an h-, size- or aspect- class.

Add ChartTooltip with content={<ChartTooltipContent />} to make the chart respond to touch: touching or dragging selects the nearest data point, draws a cursor and shows the tooltip, with a selection haptic. ChartTooltipContent supports indicator (dot, line or dashed), hideLabel, hideIndicator, labelFormatter, formatter and nameKey. Use defaultIndex on ChartTooltip to start with a point selected, or onActiveIndexChange on the chart to react to selection.

Line, Area, Bar, Pie, RadialBar and Radar take isAnimationActive, animationDuration and animationEasing. Bars default to 400 ms and curves, sectors and radar shapes to 1500 ms, with ease easing. Set isAnimationActive={false} to draw a series in its final state, for example when its data changes on every interaction.

Add ChartLegend with content={<ChartLegendContent />} for a legend in config order. useChart() returns the config inside a container, and ChartPalette resolves token colors for custom drawing.

Examples

Tooltip line

Tooltip with a line indicator.

iOS
Chart (tooltip line) on iOS in light modeChart (tooltip line) on iOS in dark mode
import * as React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Bar, BarChart, type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, XAxis } from "@/components/ui/chart"

const monthData = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 173, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 264, mobile: 140 },
]

const deviceConfig = {
  desktop: { label: "Desktop", color: "chart-1" },
  mobile: { label: "Mobile", color: "chart-2" },
} satisfies ChartConfig

function StackedTooltipChart({
  title,
  description,
  defaultIndex,
  content,
}: {
  title: string
  description: string
  defaultIndex: number
  content: React.ReactElement
}) {
  return (
    <Card variant="filled" size="sm">
      <CardHeader>
        <CardTitle>{title}</CardTitle>
        <CardDescription>{description}</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={deviceConfig}
          accessibilityLabel={title}
          className="h-52 w-full"
        >
          <BarChart
            data={monthData}
            margin={{ top: 70, right: 4, left: 4, bottom: 0 }}
          >
            <XAxis
              dataKey="month"
              tickMargin={8}
              interval={0}
              tickFormatter={(value: string) => value.slice(0, 3)}
            />
            <ChartTooltip
              defaultIndex={defaultIndex}
              cursor={false}
              content={content}
            />
            <Bar
              dataKey="desktop"
              stackId="a"
              radius={[0, 0, 4, 4]}
              maxBarSize={30}
            />
            <Bar
              dataKey="mobile"
              stackId="a"
              radius={[4, 4, 0, 0]}
              maxBarSize={30}
            />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipLineExample() {
  return (
    <StackedTooltipChart
      title="Line indicator"
      description='indicator="line"'
      defaultIndex={2}
      content={<ChartTooltipContent indicator="line" />}
    />
  )
}
import Charts
import EsuissUI
import SwiftUI

struct ChartTooltipLineExample: View {
  var body: some View {
    ChartStackedTooltip(title: "Line indicator", description: "indicator=\"line\"", defaultIndex: 2) { payload in
      ChartTooltipContent(payload, indicator: .line)
    }
  }
}

let chartMonthData: [(month: String, desktop: Double, mobile: Double)] = [
  ("January", 186, 80),
  ("February", 305, 200),
  ("March", 237, 120),
  ("April", 173, 190),
  ("May", 209, 130),
  ("June", 264, 140),
]

let chartDevices: ChartConfig = [
  "desktop": .init("Desktop", color: Palette.blue500),
  "mobile": .init("Mobile", color: Palette.teal500),
]

struct ChartStackedTooltip<Tooltip: View>: View {
  let title: String
  let description: String
  var config = chartDevices
  let defaultIndex: Int
  @ViewBuilder let tooltip: (ChartTooltipPayload) -> Tooltip

  var body: some View {
    Card(.filled, size: .sm) {
      CardHeader(LocalizedStringKey(title), description: LocalizedStringKey(description))
      CardContent {
        ChartContainer(config: config) {
          ChartAnimator(animationDuration: 400) { progress in
            Chart {
              ForEach(chartMonthData.indices, id: \.self) { index in
                let item = chartMonthData[index]
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", 0),
                  yEnd: .value("Desktop", item.desktop * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(bottomLeadingRadius: 4, bottomTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("desktop"))
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", item.desktop * progress),
                  yEnd: .value("Mobile", (item.desktop + item.mobile) * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(topLeadingRadius: 4, topTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("mobile"))
                ChartAnchorMark(x: .value("Month", item.month), y: .value("Mobile", item.desktop + item.mobile))
              }
            }
            .chartYScale(range: .plotDimension(endPadding: 70))
            .chartXAxis { ChartAxis.x(values: chartMonthData.map(\.month)) { String($0.prefix(3)) } }
            .chartYAxis(.hidden)
            .chartTooltip(
              x: chartMonthData.map(\.month),
              defaultIndex: defaultIndex,
              cursor: false,
              accessibilityLabel: title,
              items: { index in
                [
                  ChartTooltipItem("desktop", value: chartMonthData[index].desktop),
                  ChartTooltipItem("mobile", value: chartMonthData[index].mobile),
                ]
              },
              content: tooltip
            )
          }
        }
        .frame(height: 208)
      }
    }
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.phosphoricons.regular.Desktop
import com.esuiss.ui.components.BarChart
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.ChartTooltipIndicator
import com.esuiss.ui.components.ChartTooltipScope

@Composable
fun ChartTooltipLineExample() {
    ChartStackedTooltip("Line indicator", "indicator=\"line\"", 2) { ChartTooltipContent(indicator = ChartTooltipIndicator.Line) }
}

class ChartMonth(val month: String, val desktop: Double, val mobile: Double)

val chartMonthData = listOf(
    ChartMonth("January", 186.0, 80.0),
    ChartMonth("February", 305.0, 200.0),
    ChartMonth("March", 237.0, 120.0),
    ChartMonth("April", 173.0, 190.0),
    ChartMonth("May", 209.0, 130.0),
    ChartMonth("June", 264.0, 140.0),
)

val chartDeviceConfig = ChartConfig(
    "desktop" to ChartConfigItem("Desktop") { chart1 },
    "mobile" to ChartConfigItem("Mobile") { chart2 },
)

@Composable
fun ChartTooltipCard(title: String, description: String, content: @Composable () -> Unit) {
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardHeader(title, description = description)
        CardContent { content() }
    }
}

@Composable
fun ChartStackedTooltip(
    title: String,
    description: String,
    defaultIndex: Int,
    config: ChartConfig = chartDeviceConfig,
    tooltip: @Composable ChartTooltipScope.() -> Unit,
) {
    ChartTooltipCard(title, description) {
        ChartContainer(config, Modifier.fillMaxWidth().height(208.dp)) {
            BarChart(
                chartMonthData,
                margin = ChartMargin(top = 70.dp, right = 4.dp, bottom = 0.dp, left = 4.dp),
                accessibilityLabel = title,
            ) {
                XAxis(label = { it.month }, tickMargin = 8.dp, interval = 0, tickFormatter = { it.take(3) })
                ChartTooltip(defaultIndex = defaultIndex, cursor = null, content = tooltip)
                Bar("desktop", value = { it.desktop }, stackId = "a", radius = ChartRadius(0.dp, 0.dp, 4.dp, 4.dp), maxBarSize = 30.dp)
                Bar("mobile", value = { it.mobile }, stackId = "a", radius = ChartRadius(4.dp, 4.dp, 0.dp, 0.dp), maxBarSize = 30.dp)
            }
        }
    }
}

Tooltip dashed

Tooltip with a dashed indicator.

iOS
Chart (tooltip dashed) on iOS in light modeChart (tooltip dashed) on iOS in dark mode
import * as React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Bar, BarChart, type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, XAxis } from "@/components/ui/chart"

const monthData = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 173, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 264, mobile: 140 },
]

const deviceConfig = {
  desktop: { label: "Desktop", color: "chart-1" },
  mobile: { label: "Mobile", color: "chart-2" },
} satisfies ChartConfig

function StackedTooltipChart({
  title,
  description,
  defaultIndex,
  content,
}: {
  title: string
  description: string
  defaultIndex: number
  content: React.ReactElement
}) {
  return (
    <Card variant="filled" size="sm">
      <CardHeader>
        <CardTitle>{title}</CardTitle>
        <CardDescription>{description}</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={deviceConfig}
          accessibilityLabel={title}
          className="h-52 w-full"
        >
          <BarChart
            data={monthData}
            margin={{ top: 70, right: 4, left: 4, bottom: 0 }}
          >
            <XAxis
              dataKey="month"
              tickMargin={8}
              interval={0}
              tickFormatter={(value: string) => value.slice(0, 3)}
            />
            <ChartTooltip
              defaultIndex={defaultIndex}
              cursor={false}
              content={content}
            />
            <Bar
              dataKey="desktop"
              stackId="a"
              radius={[0, 0, 4, 4]}
              maxBarSize={30}
            />
            <Bar
              dataKey="mobile"
              stackId="a"
              radius={[4, 4, 0, 0]}
              maxBarSize={30}
            />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipDashedExample() {
  return (
    <StackedTooltipChart
      title="Dashed indicator"
      description='indicator="dashed"'
      defaultIndex={2}
      content={<ChartTooltipContent indicator="dashed" />}
    />
  )
}
import Charts
import EsuissUI
import SwiftUI

struct ChartTooltipDashedExample: View {
  var body: some View {
    ChartStackedTooltip(title: "Dashed indicator", description: "indicator=\"dashed\"", defaultIndex: 2) { payload in
      ChartTooltipContent(payload, indicator: .dashed)
    }
  }
}

let chartMonthData: [(month: String, desktop: Double, mobile: Double)] = [
  ("January", 186, 80),
  ("February", 305, 200),
  ("March", 237, 120),
  ("April", 173, 190),
  ("May", 209, 130),
  ("June", 264, 140),
]

let chartDevices: ChartConfig = [
  "desktop": .init("Desktop", color: Palette.blue500),
  "mobile": .init("Mobile", color: Palette.teal500),
]

struct ChartStackedTooltip<Tooltip: View>: View {
  let title: String
  let description: String
  var config = chartDevices
  let defaultIndex: Int
  @ViewBuilder let tooltip: (ChartTooltipPayload) -> Tooltip

  var body: some View {
    Card(.filled, size: .sm) {
      CardHeader(LocalizedStringKey(title), description: LocalizedStringKey(description))
      CardContent {
        ChartContainer(config: config) {
          ChartAnimator(animationDuration: 400) { progress in
            Chart {
              ForEach(chartMonthData.indices, id: \.self) { index in
                let item = chartMonthData[index]
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", 0),
                  yEnd: .value("Desktop", item.desktop * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(bottomLeadingRadius: 4, bottomTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("desktop"))
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", item.desktop * progress),
                  yEnd: .value("Mobile", (item.desktop + item.mobile) * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(topLeadingRadius: 4, topTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("mobile"))
                ChartAnchorMark(x: .value("Month", item.month), y: .value("Mobile", item.desktop + item.mobile))
              }
            }
            .chartYScale(range: .plotDimension(endPadding: 70))
            .chartXAxis { ChartAxis.x(values: chartMonthData.map(\.month)) { String($0.prefix(3)) } }
            .chartYAxis(.hidden)
            .chartTooltip(
              x: chartMonthData.map(\.month),
              defaultIndex: defaultIndex,
              cursor: false,
              accessibilityLabel: title,
              items: { index in
                [
                  ChartTooltipItem("desktop", value: chartMonthData[index].desktop),
                  ChartTooltipItem("mobile", value: chartMonthData[index].mobile),
                ]
              },
              content: tooltip
            )
          }
        }
        .frame(height: 208)
      }
    }
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.phosphoricons.regular.Desktop
import com.esuiss.ui.components.BarChart
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.ChartTooltipIndicator
import com.esuiss.ui.components.ChartTooltipScope

@Composable
fun ChartTooltipDashedExample() {
    ChartStackedTooltip("Dashed indicator", "indicator=\"dashed\"", 2) { ChartTooltipContent(indicator = ChartTooltipIndicator.Dashed) }
}

class ChartMonth(val month: String, val desktop: Double, val mobile: Double)

val chartMonthData = listOf(
    ChartMonth("January", 186.0, 80.0),
    ChartMonth("February", 305.0, 200.0),
    ChartMonth("March", 237.0, 120.0),
    ChartMonth("April", 173.0, 190.0),
    ChartMonth("May", 209.0, 130.0),
    ChartMonth("June", 264.0, 140.0),
)

val chartDeviceConfig = ChartConfig(
    "desktop" to ChartConfigItem("Desktop") { chart1 },
    "mobile" to ChartConfigItem("Mobile") { chart2 },
)

@Composable
fun ChartTooltipCard(title: String, description: String, content: @Composable () -> Unit) {
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardHeader(title, description = description)
        CardContent { content() }
    }
}

@Composable
fun ChartStackedTooltip(
    title: String,
    description: String,
    defaultIndex: Int,
    config: ChartConfig = chartDeviceConfig,
    tooltip: @Composable ChartTooltipScope.() -> Unit,
) {
    ChartTooltipCard(title, description) {
        ChartContainer(config, Modifier.fillMaxWidth().height(208.dp)) {
            BarChart(
                chartMonthData,
                margin = ChartMargin(top = 70.dp, right = 4.dp, bottom = 0.dp, left = 4.dp),
                accessibilityLabel = title,
            ) {
                XAxis(label = { it.month }, tickMargin = 8.dp, interval = 0, tickFormatter = { it.take(3) })
                ChartTooltip(defaultIndex = defaultIndex, cursor = null, content = tooltip)
                Bar("desktop", value = { it.desktop }, stackId = "a", radius = ChartRadius(0.dp, 0.dp, 4.dp, 4.dp), maxBarSize = 30.dp)
                Bar("mobile", value = { it.mobile }, stackId = "a", radius = ChartRadius(4.dp, 4.dp, 0.dp, 0.dp), maxBarSize = 30.dp)
            }
        }
    }
}

Tooltip no indicator

Tooltip with only names and values.

iOS
Chart (tooltip no indicator) on iOS in light modeChart (tooltip no indicator) on iOS in dark mode
import * as React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Bar, BarChart, type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, XAxis } from "@/components/ui/chart"

const monthData = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 173, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 264, mobile: 140 },
]

const deviceConfig = {
  desktop: { label: "Desktop", color: "chart-1" },
  mobile: { label: "Mobile", color: "chart-2" },
} satisfies ChartConfig

function StackedTooltipChart({
  title,
  description,
  defaultIndex,
  content,
}: {
  title: string
  description: string
  defaultIndex: number
  content: React.ReactElement
}) {
  return (
    <Card variant="filled" size="sm">
      <CardHeader>
        <CardTitle>{title}</CardTitle>
        <CardDescription>{description}</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={deviceConfig}
          accessibilityLabel={title}
          className="h-52 w-full"
        >
          <BarChart
            data={monthData}
            margin={{ top: 70, right: 4, left: 4, bottom: 0 }}
          >
            <XAxis
              dataKey="month"
              tickMargin={8}
              interval={0}
              tickFormatter={(value: string) => value.slice(0, 3)}
            />
            <ChartTooltip
              defaultIndex={defaultIndex}
              cursor={false}
              content={content}
            />
            <Bar
              dataKey="desktop"
              stackId="a"
              radius={[0, 0, 4, 4]}
              maxBarSize={30}
            />
            <Bar
              dataKey="mobile"
              stackId="a"
              radius={[4, 4, 0, 0]}
              maxBarSize={30}
            />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipNoIndicatorExample() {
  return (
    <StackedTooltipChart
      title="No indicator"
      description="Just the name and value"
      defaultIndex={2}
      content={<ChartTooltipContent hideIndicator />}
    />
  )
}
import Charts
import EsuissUI
import SwiftUI

struct ChartTooltipNoIndicatorExample: View {
  var body: some View {
    ChartStackedTooltip(title: "No indicator", description: "Just the name and value", defaultIndex: 2) { payload in
      ChartTooltipContent(payload, hideIndicator: true)
    }
  }
}

let chartMonthData: [(month: String, desktop: Double, mobile: Double)] = [
  ("January", 186, 80),
  ("February", 305, 200),
  ("March", 237, 120),
  ("April", 173, 190),
  ("May", 209, 130),
  ("June", 264, 140),
]

let chartDevices: ChartConfig = [
  "desktop": .init("Desktop", color: Palette.blue500),
  "mobile": .init("Mobile", color: Palette.teal500),
]

struct ChartStackedTooltip<Tooltip: View>: View {
  let title: String
  let description: String
  var config = chartDevices
  let defaultIndex: Int
  @ViewBuilder let tooltip: (ChartTooltipPayload) -> Tooltip

  var body: some View {
    Card(.filled, size: .sm) {
      CardHeader(LocalizedStringKey(title), description: LocalizedStringKey(description))
      CardContent {
        ChartContainer(config: config) {
          ChartAnimator(animationDuration: 400) { progress in
            Chart {
              ForEach(chartMonthData.indices, id: \.self) { index in
                let item = chartMonthData[index]
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", 0),
                  yEnd: .value("Desktop", item.desktop * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(bottomLeadingRadius: 4, bottomTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("desktop"))
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", item.desktop * progress),
                  yEnd: .value("Mobile", (item.desktop + item.mobile) * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(topLeadingRadius: 4, topTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("mobile"))
                ChartAnchorMark(x: .value("Month", item.month), y: .value("Mobile", item.desktop + item.mobile))
              }
            }
            .chartYScale(range: .plotDimension(endPadding: 70))
            .chartXAxis { ChartAxis.x(values: chartMonthData.map(\.month)) { String($0.prefix(3)) } }
            .chartYAxis(.hidden)
            .chartTooltip(
              x: chartMonthData.map(\.month),
              defaultIndex: defaultIndex,
              cursor: false,
              accessibilityLabel: title,
              items: { index in
                [
                  ChartTooltipItem("desktop", value: chartMonthData[index].desktop),
                  ChartTooltipItem("mobile", value: chartMonthData[index].mobile),
                ]
              },
              content: tooltip
            )
          }
        }
        .frame(height: 208)
      }
    }
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.phosphoricons.regular.Desktop
import com.esuiss.ui.components.BarChart
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.ChartTooltipScope

@Composable
fun ChartTooltipNoIndicatorExample() {
    ChartStackedTooltip("No indicator", "Just the name and value", 2) { ChartTooltipContent(hideIndicator = true) }
}

class ChartMonth(val month: String, val desktop: Double, val mobile: Double)

val chartMonthData = listOf(
    ChartMonth("January", 186.0, 80.0),
    ChartMonth("February", 305.0, 200.0),
    ChartMonth("March", 237.0, 120.0),
    ChartMonth("April", 173.0, 190.0),
    ChartMonth("May", 209.0, 130.0),
    ChartMonth("June", 264.0, 140.0),
)

val chartDeviceConfig = ChartConfig(
    "desktop" to ChartConfigItem("Desktop") { chart1 },
    "mobile" to ChartConfigItem("Mobile") { chart2 },
)

@Composable
fun ChartTooltipCard(title: String, description: String, content: @Composable () -> Unit) {
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardHeader(title, description = description)
        CardContent { content() }
    }
}

@Composable
fun ChartStackedTooltip(
    title: String,
    description: String,
    defaultIndex: Int,
    config: ChartConfig = chartDeviceConfig,
    tooltip: @Composable ChartTooltipScope.() -> Unit,
) {
    ChartTooltipCard(title, description) {
        ChartContainer(config, Modifier.fillMaxWidth().height(208.dp)) {
            BarChart(
                chartMonthData,
                margin = ChartMargin(top = 70.dp, right = 4.dp, bottom = 0.dp, left = 4.dp),
                accessibilityLabel = title,
            ) {
                XAxis(label = { it.month }, tickMargin = 8.dp, interval = 0, tickFormatter = { it.take(3) })
                ChartTooltip(defaultIndex = defaultIndex, cursor = null, content = tooltip)
                Bar("desktop", value = { it.desktop }, stackId = "a", radius = ChartRadius(0.dp, 0.dp, 4.dp, 4.dp), maxBarSize = 30.dp)
                Bar("mobile", value = { it.mobile }, stackId = "a", radius = ChartRadius(4.dp, 4.dp, 0.dp, 0.dp), maxBarSize = 30.dp)
            }
        }
    }
}

Tooltip no label

Tooltip with the category label hidden.

iOS
Chart (tooltip no label) on iOS in light modeChart (tooltip no label) on iOS in dark mode
import * as React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Bar, BarChart, type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, XAxis } from "@/components/ui/chart"

const monthData = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 173, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 264, mobile: 140 },
]

const deviceConfig = {
  desktop: { label: "Desktop", color: "chart-1" },
  mobile: { label: "Mobile", color: "chart-2" },
} satisfies ChartConfig

function StackedTooltipChart({
  title,
  description,
  defaultIndex,
  content,
}: {
  title: string
  description: string
  defaultIndex: number
  content: React.ReactElement
}) {
  return (
    <Card variant="filled" size="sm">
      <CardHeader>
        <CardTitle>{title}</CardTitle>
        <CardDescription>{description}</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={deviceConfig}
          accessibilityLabel={title}
          className="h-52 w-full"
        >
          <BarChart
            data={monthData}
            margin={{ top: 70, right: 4, left: 4, bottom: 0 }}
          >
            <XAxis
              dataKey="month"
              tickMargin={8}
              interval={0}
              tickFormatter={(value: string) => value.slice(0, 3)}
            />
            <ChartTooltip
              defaultIndex={defaultIndex}
              cursor={false}
              content={content}
            />
            <Bar
              dataKey="desktop"
              stackId="a"
              radius={[0, 0, 4, 4]}
              maxBarSize={30}
            />
            <Bar
              dataKey="mobile"
              stackId="a"
              radius={[4, 4, 0, 0]}
              maxBarSize={30}
            />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipNoLabelExample() {
  return (
    <StackedTooltipChart
      title="No label"
      description="The month is hidden"
      defaultIndex={2}
      content={<ChartTooltipContent hideLabel />}
    />
  )
}
import Charts
import EsuissUI
import SwiftUI

struct ChartTooltipNoLabelExample: View {
  var body: some View {
    ChartStackedTooltip(title: "No label", description: "The month is hidden", defaultIndex: 2) { payload in
      ChartTooltipContent(payload, hideLabel: true)
    }
  }
}

let chartMonthData: [(month: String, desktop: Double, mobile: Double)] = [
  ("January", 186, 80),
  ("February", 305, 200),
  ("March", 237, 120),
  ("April", 173, 190),
  ("May", 209, 130),
  ("June", 264, 140),
]

let chartDevices: ChartConfig = [
  "desktop": .init("Desktop", color: Palette.blue500),
  "mobile": .init("Mobile", color: Palette.teal500),
]

struct ChartStackedTooltip<Tooltip: View>: View {
  let title: String
  let description: String
  var config = chartDevices
  let defaultIndex: Int
  @ViewBuilder let tooltip: (ChartTooltipPayload) -> Tooltip

  var body: some View {
    Card(.filled, size: .sm) {
      CardHeader(LocalizedStringKey(title), description: LocalizedStringKey(description))
      CardContent {
        ChartContainer(config: config) {
          ChartAnimator(animationDuration: 400) { progress in
            Chart {
              ForEach(chartMonthData.indices, id: \.self) { index in
                let item = chartMonthData[index]
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", 0),
                  yEnd: .value("Desktop", item.desktop * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(bottomLeadingRadius: 4, bottomTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("desktop"))
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", item.desktop * progress),
                  yEnd: .value("Mobile", (item.desktop + item.mobile) * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(topLeadingRadius: 4, topTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("mobile"))
                ChartAnchorMark(x: .value("Month", item.month), y: .value("Mobile", item.desktop + item.mobile))
              }
            }
            .chartYScale(range: .plotDimension(endPadding: 70))
            .chartXAxis { ChartAxis.x(values: chartMonthData.map(\.month)) { String($0.prefix(3)) } }
            .chartYAxis(.hidden)
            .chartTooltip(
              x: chartMonthData.map(\.month),
              defaultIndex: defaultIndex,
              cursor: false,
              accessibilityLabel: title,
              items: { index in
                [
                  ChartTooltipItem("desktop", value: chartMonthData[index].desktop),
                  ChartTooltipItem("mobile", value: chartMonthData[index].mobile),
                ]
              },
              content: tooltip
            )
          }
        }
        .frame(height: 208)
      }
    }
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.phosphoricons.regular.Desktop
import com.esuiss.ui.components.BarChart
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.ChartTooltipScope

@Composable
fun ChartTooltipNoLabelExample() {
    ChartStackedTooltip("No label", "The month is hidden", 2) { ChartTooltipContent(hideLabel = true) }
}

class ChartMonth(val month: String, val desktop: Double, val mobile: Double)

val chartMonthData = listOf(
    ChartMonth("January", 186.0, 80.0),
    ChartMonth("February", 305.0, 200.0),
    ChartMonth("March", 237.0, 120.0),
    ChartMonth("April", 173.0, 190.0),
    ChartMonth("May", 209.0, 130.0),
    ChartMonth("June", 264.0, 140.0),
)

val chartDeviceConfig = ChartConfig(
    "desktop" to ChartConfigItem("Desktop") { chart1 },
    "mobile" to ChartConfigItem("Mobile") { chart2 },
)

@Composable
fun ChartTooltipCard(title: String, description: String, content: @Composable () -> Unit) {
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardHeader(title, description = description)
        CardContent { content() }
    }
}

@Composable
fun ChartStackedTooltip(
    title: String,
    description: String,
    defaultIndex: Int,
    config: ChartConfig = chartDeviceConfig,
    tooltip: @Composable ChartTooltipScope.() -> Unit,
) {
    ChartTooltipCard(title, description) {
        ChartContainer(config, Modifier.fillMaxWidth().height(208.dp)) {
            BarChart(
                chartMonthData,
                margin = ChartMargin(top = 70.dp, right = 4.dp, bottom = 0.dp, left = 4.dp),
                accessibilityLabel = title,
            ) {
                XAxis(label = { it.month }, tickMargin = 8.dp, interval = 0, tickFormatter = { it.take(3) })
                ChartTooltip(defaultIndex = defaultIndex, cursor = null, content = tooltip)
                Bar("desktop", value = { it.desktop }, stackId = "a", radius = ChartRadius(0.dp, 0.dp, 4.dp, 4.dp), maxBarSize = 30.dp)
                Bar("mobile", value = { it.mobile }, stackId = "a", radius = ChartRadius(4.dp, 4.dp, 0.dp, 0.dp), maxBarSize = 30.dp)
            }
        }
    }
}

Tooltip label formatter

A formatted date as the tooltip label.

iOS
Chart (tooltip label formatter) on iOS in light modeChart (tooltip label formatter) on iOS in dark mode
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Area, AreaChart, type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, XAxis } from "@/components/ui/chart"

const MONTHS = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
]

const WEEKDAYS = [
  "Sunday",
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
]

function dayKey(date: Date) {
  return [
    date.getFullYear(),
    String(date.getMonth() + 1).padStart(2, "0"),
    String(date.getDate()).padStart(2, "0"),
  ].join("-")
}

function fromKey(key: string) {
  const [year = 0, month = 1, day = 1] = key.split("-").map(Number)
  return new Date(year, month - 1, day)
}

const energyConfig = {
  energy: { label: "Active energy", color: "red-500" },
} satisfies ChartConfig

const energyValues = [
  512, 604, 455, 389, 662, 598, 431, 520, 690, 447, 575, 402, 633, 558,
]

const fortnight = energyValues.map((energy, index) => ({
  date: dayKey(new Date(2026, 8, 23 + index)),
  energy,
}))

function LabelFormatterDemo() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader>
        <CardTitle>Custom label</CardTitle>
        <CardDescription>The date written out in full</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={energyConfig}
          accessibilityLabel="Custom label"
          className="h-52 w-full"
        >
          <AreaChart
            data={fortnight}
            margin={{ top: 70, right: 12, left: 12, bottom: 0 }}
          >
            <XAxis
              dataKey="date"
              tickMargin={8}
              interval={2}
              tickFormatter={(value: string) => String(Number(value.slice(8)))}
            />
            <ChartTooltip
              defaultIndex={8}
              content={
                <ChartTooltipContent
                  indicator="line"
                  labelFormatter={(_, payload) => {
                    const date = fromKey(String(payload[0]?.payload.date))
                    return `${WEEKDAYS[date.getDay()]}, ${MONTHS[date.getMonth()]} ${date.getDate()}`
                  }}
                />
              }
            />
            <Area
              dataKey="energy"
              type="monotone"
              strokeWidth={2}
              fill={{
                stops: [
                  { offset: 0, opacity: 0.38 },
                  { offset: 1, opacity: 0.02 },
                ],
              }}
            />
          </AreaChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipLabelFormatterExample() {
  return (
    <LabelFormatterDemo />
  )
}
import Charts
import EsuissUI
import SwiftUI

let chartEnglish = Locale(identifier: "en_US")

let chartEnergyValues: [Double] = [512, 604, 455, 389, 662, 598, 431, 520, 690, 447, 575, 402, 633, 558]

let chartEnergyConfig: ChartConfig = [
  "energy": .init("Active energy", color: Palette.red500),
]

func chartDate(year: Int, month: Int, day: Int) -> Date {
  let calendar = Foundation.Calendar(identifier: .gregorian)
  let first = calendar.date(from: DateComponents(year: year, month: month, day: 1)) ?? .now
  return calendar.date(byAdding: .day, value: day - 1, to: first) ?? first
}

struct ChartLabelFormatterDemo: View {
  @State private var selection: Int? = 8

  var body: some View {
    let dates = chartEnergyValues.indices.map { chartDate(year: 2026, month: 9, day: 23 + $0) }
    let color = chartEnergyConfig.color("energy")
    Card(.filled, size: .sm) {
      CardHeader("Custom label", description: "The date written out in full")
      CardContent {
        ChartContainer(config: chartEnergyConfig) {
          ChartAnimator { progress in
            Chart {
              ForEach(chartEnergyValues.indices, id: \.self) { index in
                AreaMark(x: .value("Day", index), y: .value("Energy", chartEnergyValues[index]))
                  .interpolationMethod(.monotone)
                  .foregroundStyle(
                    LinearGradient(colors: [color.opacity(0.38), color.opacity(0.02)], startPoint: .top, endPoint: .bottom)
                  )
                LineMark(x: .value("Day", index), y: .value("Energy", chartEnergyValues[index]))
                  .interpolationMethod(.monotone)
                  .lineStyle(StrokeStyle(lineWidth: 2))
                  .foregroundStyle(color)
              }
              .chartReveal(progress, x: 0...(chartEnergyValues.count - 1))
              if let selection {
                PointMark(x: .value("Day", selection), y: .value("Energy", chartEnergyValues[selection]))
                  .symbolSize(64)
                  .foregroundStyle(color)
              }
            }
            .chartXScale(domain: 0...(chartEnergyValues.count - 1), range: .plotDimension(padding: 12))
            .chartYScale(range: .plotDimension(endPadding: 70))
            .chartXAxis {
              ChartAxis.x(values: Array(stride(from: 0, to: chartEnergyValues.count, by: 3))) {
                "\(Foundation.Calendar(identifier: .gregorian).component(.day, from: dates[$0]))"
              }
            }
            .chartYAxis(.hidden)
            .chartTooltip(
              x: Array(chartEnergyValues.indices),
              selection: $selection,
              defaultIndex: 8,
              accessibilityLabel: "Custom label",
              items: { [ChartTooltipItem("energy", value: chartEnergyValues[$0])] }
            ) { payload in
              ChartTooltipContent(payload, indicator: .line) { payload in
                Text(verbatim: dates[payload.index].formatted(
                  .dateTime.weekday(.wide).month(.wide).day().locale(chartEnglish)
                ))
              }
            }
          }
        }
        .frame(height: 208)
      }
    }
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.AreaChart
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
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.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Typography
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale

val chartEnergyConfig = ChartConfig("energy" to ChartConfigItem("Active energy") { Palette.red500 })

val chartEnergyValues = listOf(512, 604, 455, 389, 662, 598, 431, 520, 690, 447, 575, 402, 633, 558)

class ChartEnergy(val date: Calendar, val energy: Double)

fun chartEnergy(values: List<Int>, year: Int, month: Int, day: Int) = values.mapIndexed { index, value ->
    ChartEnergy(Calendar.getInstance().apply { clear(); set(year, month, day + index, 12, 0) }, value.toDouble())
}

@Composable
fun ChartTooltipCard(title: String, description: String, content: @Composable () -> Unit) {
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardHeader(title, description = description)
        CardContent { content() }
    }
}

@Composable
fun ChartLabelFormatter() {
    val colors = EsuissTheme.colors
    val data = remember { chartEnergy(chartEnergyValues, 2026, Calendar.SEPTEMBER, 23) }
    val longDate = remember { SimpleDateFormat("EEEE, MMMM d", Locale.US) }
    ChartTooltipCard("Custom label", "The date written out in full") {
        ChartContainer(chartEnergyConfig, Modifier.fillMaxWidth().height(208.dp)) {
            AreaChart(
                data,
                margin = ChartMargin(top = 70.dp, right = 12.dp, bottom = 0.dp, left = 12.dp),
                accessibilityLabel = "Custom label",
            ) {
                XAxis(label = { it.date.get(Calendar.DAY_OF_MONTH).toString() }, tickMargin = 8.dp, interval = 2)
                ChartTooltip(defaultIndex = 8) {
                    ChartTooltipContent(
                        indicator = ChartTooltipIndicator.Line,
                        labelFormatter = { _, items ->
                            Text(
                                longDate.format(data[items.first().index].date.time),
                                style = Typography.xs.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
                            )
                        },
                    )
                }
                Area(
                    "energy",
                    value = { it.energy },
                    curve = ChartCurve.Monotone,
                    strokeWidth = 2.dp,
                    fill = ChartFill.VerticalGradient(topAlpha = 0.38f, bottomAlpha = 0.02f),
                )
            }
        }
    }
}

Tooltip formatter

Custom value formatting with formatter.

iOS
Chart (tooltip formatter) on iOS in light modeChart (tooltip formatter) on iOS in dark mode
import { View } from "react-native"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Bar, BarChart, type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, XAxis } from "@/components/ui/chart"
import { Text } from "@/components/ui/text"

const WEEKDAYS = [
  "Sunday",
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
]

function formatNumber(value: number, digits = 0) {
  try {
    return new Intl.NumberFormat("en-US", {
      minimumFractionDigits: digits,
      maximumFractionDigits: digits,
    }).format(value)
  } catch {
    const [whole = "0", fraction] = value.toFixed(digits).split(".")
    const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    return fraction ? `${grouped}.${fraction}` : grouped
  }
}

function dayKey(date: Date) {
  return [
    date.getFullYear(),
    String(date.getMonth() + 1).padStart(2, "0"),
    String(date.getDate()).padStart(2, "0"),
  ].join("-")
}

function fromKey(key: string) {
  const [year = 0, month = 1, day = 1] = key.split("-").map(Number)
  return new Date(year, month - 1, day)
}

const energyConfig = {
  energy: { label: "Active energy", color: "red-500" },
} satisfies ChartConfig

const energyValues = [
  512, 604, 455, 389, 662, 598, 431, 520, 690, 447, 575, 402, 633, 558,
]

const week = energyValues.slice(-7).map((energy, index) => ({
  date: dayKey(new Date(2026, 9, index)),
  energy,
}))

function FormatterDemo() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader>
        <CardTitle>With units</CardTitle>
        <CardDescription>A unit next to the value</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={energyConfig}
          accessibilityLabel="With units"
          className="h-52 w-full"
        >
          <BarChart
            data={week}
            margin={{ top: 70, right: 4, left: 4, bottom: 0 }}
          >
            <XAxis
              dataKey="date"
              tickMargin={8}
              interval={0}
              tickFormatter={(value: string) =>
                (WEEKDAYS[fromKey(value).getDay()] ?? "").slice(0, 3)
              }
            />
            <ChartTooltip
              defaultIndex={3}
              cursor={false}
              content={
                <ChartTooltipContent
                  hideLabel
                  formatter={(value) => (
                    <View className="flex-row items-baseline gap-1.5">
                      <Text className="text-muted-foreground">
                        Active energy
                      </Text>
                      <Text className="font-semibold text-foreground tabular-nums">
                        {formatNumber(Number(value))}
                      </Text>
                      <Text className="text-xs text-muted-foreground">
                        kcal
                      </Text>
                    </View>
                  )}
                />
              }
            />
            <Bar dataKey="energy" radius={4} maxBarSize={26} />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipFormatterExample() {
  return (
    <FormatterDemo />
  )
}
import Charts
import EsuissUI
import SwiftUI

let chartEnglish = Locale(identifier: "en_US")

let chartEnergyValues: [Double] = [512, 604, 455, 389, 662, 598, 431, 520, 690, 447, 575, 402, 633, 558]

let chartEnergyConfig: ChartConfig = [
  "energy": .init("Active energy", color: Palette.red500),
]

func chartDate(year: Int, month: Int, day: Int) -> Date {
  let calendar = Foundation.Calendar(identifier: .gregorian)
  let first = calendar.date(from: DateComponents(year: year, month: month, day: 1)) ?? .now
  return calendar.date(byAdding: .day, value: day - 1, to: first) ?? first
}

struct ChartFormatterDemo: View {
  let week = Array(chartEnergyValues.suffix(7))

  var body: some View {
    let labels = week.indices.map {
      chartDate(year: 2026, month: 10, day: $0).formatted(.dateTime.weekday(.abbreviated).locale(chartEnglish))
    }
    Card(.filled, size: .sm) {
      CardHeader("With units", description: "A unit next to the value")
      CardContent {
        ChartContainer(config: chartEnergyConfig) {
          ChartAnimator(animationDuration: 400) { progress in
            Chart {
              ForEach(week.indices, id: \.self) { index in
                BarMark(x: .value("Day", labels[index]), y: .value("Energy", week[index] * progress), width: .fixed(26))
                  .clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous))
                  .foregroundStyle(chartEnergyConfig.color("energy"))
                ChartAnchorMark(x: .value("Day", labels[index]), y: .value("Energy", week[index]))
              }
            }
            .chartYScale(range: .plotDimension(endPadding: 70))
            .chartXAxis { ChartAxis.x(values: labels) { $0 } }
            .chartYAxis(.hidden)
            .chartTooltip(
              x: labels,
              defaultIndex: 3,
              cursor: false,
              accessibilityLabel: "With units",
              items: { [ChartTooltipItem("energy", value: week[$0])] }
            ) { payload in
              ChartTooltipContent(payload, hideLabel: true) { item in
                HStack(alignment: .firstTextBaseline, spacing: 6) {
                  Text("Active energy")
                    .foregroundStyle(EsuissColor.mutedForeground)
                  Text(verbatim: item.value.formatted(.number.locale(chartEnglish)))
                    .font(TextStyle.xs.font(weight: .semibold))
                    .monospacedDigit()
                    .foregroundStyle(EsuissColor.foreground)
                  Text("kcal")
                    .font(TextStyle.xs.font())
                    .foregroundStyle(EsuissColor.mutedForeground)
                }
              }
            }
          }
        }
        .frame(height: 208)
      }
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.BarChart
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Typography
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale

val chartEnergyConfig = ChartConfig("energy" to ChartConfigItem("Active energy") { Palette.red500 })

val chartEnergyValues = listOf(512, 604, 455, 389, 662, 598, 431, 520, 690, 447, 575, 402, 633, 558)

class ChartEnergy(val date: Calendar, val energy: Double)

fun chartEnergy(values: List<Int>, year: Int, month: Int, day: Int) = values.mapIndexed { index, value ->
    ChartEnergy(Calendar.getInstance().apply { clear(); set(year, month, day + index, 12, 0) }, value.toDouble())
}

fun chartNumber(value: Double): String = NumberFormat.getNumberInstance(Locale.US).apply {
    minimumFractionDigits = 0
    maximumFractionDigits = 0
}.format(value)

@Composable
fun ChartTooltipCard(title: String, description: String, content: @Composable () -> Unit) {
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardHeader(title, description = description)
        CardContent { content() }
    }
}

@Composable
fun ChartFormatter() {
    val colors = EsuissTheme.colors
    val data = remember { chartEnergy(chartEnergyValues.takeLast(7), 2026, Calendar.OCTOBER, 0) }
    val weekday = remember { SimpleDateFormat("EEE", Locale.US) }
    ChartTooltipCard("With units", "A unit next to the value") {
        ChartContainer(chartEnergyConfig, Modifier.fillMaxWidth().height(208.dp)) {
            BarChart(
                data,
                margin = ChartMargin(top = 70.dp, right = 4.dp, bottom = 0.dp, left = 4.dp),
                accessibilityLabel = "With units",
            ) {
                XAxis(label = { weekday.format(it.date.time) }, tickMargin = 8.dp, interval = 0)
                ChartTooltip(defaultIndex = 3, cursor = null) {
                    ChartTooltipContent(
                        hideLabel = true,
                        formatter = { item ->
                            Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
                                Text(
                                    "Active energy",
                                    Modifier.alignByBaseline(),
                                    style = Typography.xs.copy(color = colors.mutedForeground),
                                )
                                Text(
                                    chartNumber(item.value),
                                    Modifier.alignByBaseline(),
                                    style = Typography.xs.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                                )
                                Text("kcal", Modifier.alignByBaseline(), style = Typography.xs.copy(color = colors.mutedForeground))
                            }
                        },
                    )
                }
                Bar("energy", value = { it.energy }, radius = ChartRadius(4.dp), maxBarSize = 26.dp)
            }
        }
    }
}

Tooltip icons

Config icons in place of color indicators.

iOS
Chart (tooltip icons) on iOS in light modeChart (tooltip icons) on iOS in dark mode
import { DesktopIcon, DeviceMobileIcon } from "phosphor-react-native"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Bar, BarChart, type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, XAxis } from "@/components/ui/chart"

const monthData = [
  { month: "January", desktop: 186, mobile: 80 },
  { month: "February", desktop: 305, mobile: 200 },
  { month: "March", desktop: 237, mobile: 120 },
  { month: "April", desktop: 173, mobile: 190 },
  { month: "May", desktop: 209, mobile: 130 },
  { month: "June", desktop: 264, mobile: 140 },
]

const iconConfig = {
  desktop: { label: "Desktop", color: "chart-1", icon: DesktopIcon },
  mobile: { label: "Mobile", color: "chart-2", icon: DeviceMobileIcon },
} satisfies ChartConfig

function IconsDemo() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader>
        <CardTitle>With icons</CardTitle>
        <CardDescription>An icon instead of a color</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={iconConfig}
          accessibilityLabel="With icons"
          className="h-52 w-full"
        >
          <BarChart
            data={monthData}
            margin={{ top: 70, right: 4, left: 4, bottom: 0 }}
          >
            <XAxis
              dataKey="month"
              tickMargin={8}
              interval={0}
              tickFormatter={(value: string) => value.slice(0, 3)}
            />
            <ChartTooltip
              defaultIndex={3}
              cursor={false}
              content={<ChartTooltipContent />}
            />
            <Bar
              dataKey="desktop"
              stackId="a"
              radius={[0, 0, 4, 4]}
              maxBarSize={30}
            />
            <Bar
              dataKey="mobile"
              stackId="a"
              radius={[4, 4, 0, 0]}
              maxBarSize={30}
            />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipIconsExample() {
  return (
    <IconsDemo />
  )
}
import Charts
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ChartTooltipIconsExample: View {
  var body: some View {
    ChartStackedTooltip(
      title: "With icons",
      description: "An icon instead of a color",
      config: chartDeviceIcons,
      defaultIndex: 3
    ) { payload in
      ChartTooltipContent(payload)
    }
  }
}

let chartMonthData: [(month: String, desktop: Double, mobile: Double)] = [
  ("January", 186, 80),
  ("February", 305, 200),
  ("March", 237, 120),
  ("April", 173, 190),
  ("May", 209, 130),
  ("June", 264, 140),
]

let chartDevices: ChartConfig = [
  "desktop": .init("Desktop", color: Palette.blue500),
  "mobile": .init("Mobile", color: Palette.teal500),
]

let chartDeviceIcons: ChartConfig = [
  "desktop": .init("Desktop", color: Palette.blue500, icon: Ph.desktop.regular),
  "mobile": .init("Mobile", color: Palette.teal500, icon: Ph.deviceMobile.regular),
]

struct ChartStackedTooltip<Tooltip: View>: View {
  let title: String
  let description: String
  var config = chartDevices
  let defaultIndex: Int
  @ViewBuilder let tooltip: (ChartTooltipPayload) -> Tooltip

  var body: some View {
    Card(.filled, size: .sm) {
      CardHeader(LocalizedStringKey(title), description: LocalizedStringKey(description))
      CardContent {
        ChartContainer(config: config) {
          ChartAnimator(animationDuration: 400) { progress in
            Chart {
              ForEach(chartMonthData.indices, id: \.self) { index in
                let item = chartMonthData[index]
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", 0),
                  yEnd: .value("Desktop", item.desktop * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(bottomLeadingRadius: 4, bottomTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("desktop"))
                BarMark(
                  x: .value("Month", item.month),
                  yStart: .value("Start", item.desktop * progress),
                  yEnd: .value("Mobile", (item.desktop + item.mobile) * progress),
                  width: .fixed(30)
                )
                .clipShape(UnevenRoundedRectangle(topLeadingRadius: 4, topTrailingRadius: 4, style: .continuous))
                .foregroundStyle(config.color("mobile"))
                ChartAnchorMark(x: .value("Month", item.month), y: .value("Mobile", item.desktop + item.mobile))
              }
            }
            .chartYScale(range: .plotDimension(endPadding: 70))
            .chartXAxis { ChartAxis.x(values: chartMonthData.map(\.month)) { String($0.prefix(3)) } }
            .chartYAxis(.hidden)
            .chartTooltip(
              x: chartMonthData.map(\.month),
              defaultIndex: defaultIndex,
              cursor: false,
              accessibilityLabel: title,
              items: { index in
                [
                  ChartTooltipItem("desktop", value: chartMonthData[index].desktop),
                  ChartTooltipItem("mobile", value: chartMonthData[index].mobile),
                ]
              },
              content: tooltip
            )
          }
        }
        .frame(height: 208)
      }
    }
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Desktop
import com.adamglin.phosphoricons.regular.DeviceMobile
import com.esuiss.ui.components.BarChart
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.ChartTooltipScope

@Composable
fun ChartTooltipIconsExample() {
    ChartStackedTooltip("With icons", "An icon instead of a color", 3, chartIconConfig) { ChartTooltipContent() }
}

class ChartMonth(val month: String, val desktop: Double, val mobile: Double)

val chartMonthData = listOf(
    ChartMonth("January", 186.0, 80.0),
    ChartMonth("February", 305.0, 200.0),
    ChartMonth("March", 237.0, 120.0),
    ChartMonth("April", 173.0, 190.0),
    ChartMonth("May", 209.0, 130.0),
    ChartMonth("June", 264.0, 140.0),
)

val chartDeviceConfig = ChartConfig(
    "desktop" to ChartConfigItem("Desktop") { chart1 },
    "mobile" to ChartConfigItem("Mobile") { chart2 },
)

val chartIconConfig = ChartConfig(
    "desktop" to ChartConfigItem("Desktop", PhosphorIcons.Regular.Desktop) { chart1 },
    "mobile" to ChartConfigItem("Mobile", PhosphorIcons.Regular.DeviceMobile) { chart2 },
)

@Composable
fun ChartTooltipCard(title: String, description: String, content: @Composable () -> Unit) {
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardHeader(title, description = description)
        CardContent { content() }
    }
}

@Composable
fun ChartStackedTooltip(
    title: String,
    description: String,
    defaultIndex: Int,
    config: ChartConfig = chartDeviceConfig,
    tooltip: @Composable ChartTooltipScope.() -> Unit,
) {
    ChartTooltipCard(title, description) {
        ChartContainer(config, Modifier.fillMaxWidth().height(208.dp)) {
            BarChart(
                chartMonthData,
                margin = ChartMargin(top = 70.dp, right = 4.dp, bottom = 0.dp, left = 4.dp),
                accessibilityLabel = title,
            ) {
                XAxis(label = { it.month }, tickMargin = 8.dp, interval = 0, tickFormatter = { it.take(3) })
                ChartTooltip(defaultIndex = defaultIndex, cursor = null, content = tooltip)
                Bar("desktop", value = { it.desktop }, stackId = "a", radius = ChartRadius(0.dp, 0.dp, 4.dp, 4.dp), maxBarSize = 30.dp)
                Bar("mobile", value = { it.mobile }, stackId = "a", radius = ChartRadius(4.dp, 4.dp, 0.dp, 0.dp), maxBarSize = 30.dp)
            }
        }
    }
}

Tooltip custom

A fully custom tooltip rendered from a function.

iOS
Chart (tooltip custom) on iOS in light modeChart (tooltip custom) on iOS in dark mode
import { View } from "react-native"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Bar, BarChart, type ChartConfig, ChartContainer, ChartTooltip, XAxis } from "@/components/ui/chart"
import { Text } from "@/components/ui/text"

function formatNumber(value: number, digits = 0) {
  try {
    return new Intl.NumberFormat("en-US", {
      minimumFractionDigits: digits,
      maximumFractionDigits: digits,
    }).format(value)
  } catch {
    const [whole = "0", fraction] = value.toFixed(digits).split(".")
    const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    return fraction ? `${grouped}.${fraction}` : grouped
  }
}

const stepsData = [
  { day: "Monday", steps: 8412 },
  { day: "Tuesday", steps: 10230 },
  { day: "Wednesday", steps: 6120 },
  { day: "Thursday", steps: 12480 },
  { day: "Friday", steps: 9050 },
  { day: "Saturday", steps: 14320 },
  { day: "Sunday", steps: 4870 },
]

const stepsConfig = {
  steps: { label: "Steps", color: "chart-4" },
} satisfies ChartConfig

function CustomDemo() {
  return (
    <Card variant="filled" size="sm">
      <CardHeader>
        <CardTitle>Custom content</CardTitle>
        <CardDescription>A tooltip built from scratch</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer
          config={stepsConfig}
          accessibilityLabel="Custom content"
          className="h-52 w-full"
        >
          <BarChart
            data={stepsData}
            margin={{ top: 70, right: 4, left: 4, bottom: 0 }}
          >
            <XAxis
              dataKey="day"
              tickMargin={8}
              interval={0}
              tickFormatter={(value: string) => value.slice(0, 3)}
            />
            <ChartTooltip
              defaultIndex={5}
              cursor={false}
              content={({ active, payload, label }) => {
                const entry = payload[0]
                if (!active || !entry) return null
                return (
                  <View className="min-w-28 rounded-xl border border-foreground/5 bg-popover px-3 py-2 shadow-lg dark:border-foreground/10">
                    <Text className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
                      Total
                    </Text>
                    <View className="flex-row items-baseline gap-1">
                      <Text className="text-2xl font-semibold tabular-nums">
                        {formatNumber(Number(entry.value))}
                      </Text>
                      <Text className="text-xs font-semibold text-muted-foreground">
                        steps
                      </Text>
                    </View>
                    <Text className="text-xs text-muted-foreground">
                      {label}
                    </Text>
                  </View>
                )
              }}
            />
            <Bar dataKey="steps" radius={4} maxBarSize={26} />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipCustomExample() {
  return (
    <CustomDemo />
  )
}
import Charts
import EsuissUI
import SwiftUI

let chartEnglish = Locale(identifier: "en_US")

struct ChartCustomDemo: View {
  let steps: [(day: String, steps: Double)] = [
    ("Monday", 8412),
    ("Tuesday", 10230),
    ("Wednesday", 6120),
    ("Thursday", 12480),
    ("Friday", 9050),
    ("Saturday", 14320),
    ("Sunday", 4870),
  ]
  let config: ChartConfig = ["steps": .init("Steps", color: Palette.orange500)]

  @Environment(\.colorScheme) private var colorScheme

  var body: some View {
    let shape = RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
    Card(.filled, size: .sm) {
      CardHeader("Custom content", description: "A tooltip built from scratch")
      CardContent {
        ChartContainer(config: config) {
          ChartAnimator(animationDuration: 400) { progress in
            Chart {
              ForEach(steps.indices, id: \.self) { index in
                BarMark(x: .value("Day", steps[index].day), y: .value("Steps", steps[index].steps * progress), width: .fixed(26))
                  .clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous))
                  .foregroundStyle(config.color("steps"))
                ChartAnchorMark(x: .value("Day", steps[index].day), y: .value("Steps", steps[index].steps))
              }
            }
            .chartYScale(range: .plotDimension(endPadding: 70))
            .chartXAxis { ChartAxis.x(values: steps.map(\.day)) { String($0.prefix(3)) } }
            .chartYAxis(.hidden)
            .chartTooltip(
              x: steps.map(\.day),
              defaultIndex: 5,
              cursor: false,
              accessibilityLabel: "Custom content",
              items: { [ChartTooltipItem("steps", value: steps[$0].steps)] }
            ) { payload in
              VStack(alignment: .leading, spacing: 0) {
                Text("TOTAL")
                  .font(TextStyle.xs.font(weight: .semibold))
                  .tracking(0.5)
                  .foregroundStyle(EsuissColor.mutedForeground)
                HStack(alignment: .firstTextBaseline, spacing: 4) {
                  Text(verbatim: payload.total.formatted(.number.locale(chartEnglish)))
                    .font(TextStyle.xl2.font(weight: .semibold))
                    .monospacedDigit()
                    .foregroundStyle(EsuissColor.foreground)
                  Text("steps")
                    .font(TextStyle.xs.font(weight: .semibold))
                    .foregroundStyle(EsuissColor.mutedForeground)
                }
                Text(verbatim: payload.label)
                  .font(TextStyle.xs.font())
                  .foregroundStyle(EsuissColor.mutedForeground)
              }
              .padding(.horizontal, 12)
              .padding(.vertical, 8)
              .frame(minWidth: 112, alignment: .leading)
              .background(EsuissColor.card, in: shape)
              .overlay {
                shape.strokeBorder(EsuissColor.foreground.opacity(colorScheme == .dark ? 0.1 : 0.05))
              }
              .shadow(.lg)
              .fixedSize()
            }
          }
        }
        .frame(height: 208)
      }
    }
  }
}
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
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.BarChart
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.CardSize
import com.esuiss.ui.components.CardVariant
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Shadows
import com.esuiss.ui.theme.Typography
import com.esuiss.ui.theme.shadow
import java.text.NumberFormat
import java.util.Locale

fun chartNumber(value: Double): String = NumberFormat.getNumberInstance(Locale.US).apply {
    minimumFractionDigits = 0
    maximumFractionDigits = 0
}.format(value)

@Composable
fun ChartTooltipCard(title: String, description: String, content: @Composable () -> Unit) {
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardHeader(title, description = description)
        CardContent { content() }
    }
}

class ChartSteps(val day: String, val steps: Double)

val chartStepsData = listOf(
    ChartSteps("Monday", 8412.0),
    ChartSteps("Tuesday", 10230.0),
    ChartSteps("Wednesday", 6120.0),
    ChartSteps("Thursday", 12480.0),
    ChartSteps("Friday", 9050.0),
    ChartSteps("Saturday", 14320.0),
    ChartSteps("Sunday", 4870.0),
)

val chartStepsConfig = ChartConfig("steps" to ChartConfigItem("Steps") { chart4 })

@Composable
fun ChartCustom() {
    val colors = EsuissTheme.colors
    ChartTooltipCard("Custom content", "A tooltip built from scratch") {
        ChartContainer(chartStepsConfig, Modifier.fillMaxWidth().height(208.dp)) {
            BarChart(
                chartStepsData,
                margin = ChartMargin(top = 70.dp, right = 4.dp, bottom = 0.dp, left = 4.dp),
                accessibilityLabel = "Custom content",
            ) {
                XAxis(label = { it.day }, tickMargin = 8.dp, interval = 0, tickFormatter = { it.take(3) })
                ChartTooltip(defaultIndex = 5, cursor = null) {
                    val entry = items.firstOrNull() ?: return@ChartTooltip
                    val shape = RoundedCornerShape(Radius.xl)
                    Column(
                        Modifier
                            .widthIn(min = 112.dp)
                            .shadow(Shadows.lg, shape)
                            .background(colors.popover, shape)
                            .border(1.dp, colors.foreground.copy(alpha = if (colors.isDark) 0.1f else 0.05f), shape)
                            .padding(horizontal = 12.dp, vertical = 8.dp),
                    ) {
                        Text(
                            "TOTAL",
                            style = Typography.xs.copy(color = colors.mutedForeground, fontWeight = FontWeight.SemiBold),
                        )
                        Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                            Text(
                                chartNumber(entry.value),
                                Modifier.alignByBaseline(),
                                style = Typography.xl2.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                            )
                            Text(
                                "steps",
                                Modifier.alignByBaseline(),
                                style = Typography.xs.copy(color = colors.mutedForeground, fontWeight = FontWeight.SemiBold),
                            )
                        }
                        Text(label, style = Typography.xs.copy(color = colors.mutedForeground))
                    }
                }
                Bar("steps", value = { it.steps }, radius = ChartRadius(4.dp), maxBarSize = 26.dp)
            }
        }
    }
}

Tooltip interactive

Settings that change the tooltip on a stacked bar chart with a legend.

iOS
Chart (tooltip interactive) on iOS in light modeChart (tooltip interactive) on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { Bar, BarChart, CartesianGrid, type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, XAxis, YAxis } from "@/components/ui/chart"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

function formatTotal(value: number) {
  try {
    return new Intl.NumberFormat(undefined, {
      maximumFractionDigits: 1,
    }).format(value)
  } catch {
    return String(Math.round(value * 10) / 10)
  }
}

type Indicator = "dot" | "line" | "dashed"

const INDICATORS: Record<Indicator, string> = {
  dot: "Dot",
  line: "Line",
  dashed: "Dashed",
}

const revenueData = [
  ["Q1 25", 69.1, 9, 26.3],
  ["Q2 25", 46.8, 8, 26.6],
  ["Q3 25", 44.6, 8, 27.4],
  ["Q4 25", 46.2, 8.7, 28.8],
  ["Q1 26", 71, 9.5, 29.1],
  ["Q2 26", 49.4, 8.6, 29.4],
  ["Q3 26", 47.3, 8.9, 30.1],
  ["Q4 26", 49, 9.3, 31.2],
].map(([quarter, phones, laptops, services]) => ({
  quarter: quarter as string,
  phones: phones as number,
  laptops: laptops as number,
  services: services as number,
}))

const revenueConfig = {
  phones: { label: "Phones", color: "chart-1" },
  laptops: { label: "Laptops", color: "chart-2" },
  services: { label: "Services", color: "purple-500" },
} satisfies ChartConfig

function Setting({
  id,
  label,
  checked,
  onCheckedChange,
}: {
  id: string
  label: string
  checked: boolean
  onCheckedChange: (checked: boolean) => void
}) {
  return (
    <View className="flex-row items-center gap-2.5">
      <Switch
        size="sm"
        checked={checked}
        onCheckedChange={onCheckedChange}
        accessibilityLabelledBy={id}
      />
      <Label nativeID={id} onPress={() => onCheckedChange(!checked)}>
        <Text className="text-sm">{label}</Text>
      </Label>
    </View>
  )
}

function InteractiveDemo() {
  const [indicator, setIndicator] = React.useState<Indicator>("dot")
  const [showTotal, setShowTotal] = React.useState(true)
  const [hideLabel, setHideLabel] = React.useState(false)
  const [hideIndicator, setHideIndicator] = React.useState(false)

  return (
    <Card variant="filled" size="sm">
      <CardHeader className="gap-0.5">
        <Text className="text-lg font-semibold">Tooltip settings</Text>
        <Text className="text-sm text-muted-foreground">
          Quarterly revenue · billions · sample data. Change the settings and
          touch the bars.
        </Text>
      </CardHeader>
      <CardContent className="gap-5">
        <View className="flex-row flex-wrap items-end gap-x-6 gap-y-3">
          <View className="gap-1.5">
            <Text className="text-xs font-semibold text-muted-foreground">
              Indicator
            </Text>
            <ToggleGroup
              spacing={0}
              value={[indicator]}
              onValueChange={(value) => {
                if (value[0]) setIndicator(value[0] as Indicator)
              }}
              accessibilityLabel="Indicator"
            >
              {(Object.keys(INDICATORS) as Indicator[]).map((key) => (
                <ToggleGroupItem key={key} value={key} size="sm">
                  <Text>{INDICATORS[key]}</Text>
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </View>
          <View className="flex-row flex-wrap gap-x-5 gap-y-2.5 pb-1">
            <Setting
              id="tooltip-total"
              label="Total in title"
              checked={showTotal}
              onCheckedChange={setShowTotal}
            />
            <Setting
              id="tooltip-hide-title"
              label="Hide title"
              checked={hideLabel}
              onCheckedChange={setHideLabel}
            />
            <Setting
              id="tooltip-hide-indicator"
              label="Hide indicator"
              checked={hideIndicator}
              onCheckedChange={setHideIndicator}
            />
          </View>
        </View>
        <ChartContainer
          config={revenueConfig}
          accessibilityLabel="Quarterly revenue"
          className="h-72 w-full"
        >
          <BarChart
            data={revenueData}
            margin={{ top: 10, right: 4, left: 12, bottom: 0 }}
          >
            <CartesianGrid vertical={false} strokeDasharray="2 4" />
            <XAxis dataKey="quarter" tickMargin={8} minTickGap={8} />
            <YAxis
              orientation="right"
              tickMargin={6}
              width="auto"
              tickFormatter={(value: number) => `${value}B`}
            />
            <ChartTooltip
              defaultIndex={4}
              cursor={false}
              content={
                <ChartTooltipContent
                  indicator={indicator}
                  hideLabel={hideLabel}
                  hideIndicator={hideIndicator}
                  labelFormatter={
                    showTotal
                      ? (label, payload) => (
                          <View className="flex-row justify-between gap-4">
                            <Text>{label}</Text>
                            <Text className="tabular-nums">
                              {formatTotal(
                                payload.reduce(
                                  (sum, item) => sum + Number(item.value),
                                  0
                                )
                              )}
                              B
                            </Text>
                          </View>
                        )
                      : undefined
                  }
                />
              }
            />
            <ChartLegend content={<ChartLegendContent />} />
            <Bar
              dataKey="services"
              stackId="a"
              radius={[0, 0, 4, 4]}
              maxBarSize={44}
            />
            <Bar dataKey="laptops" stackId="a" maxBarSize={44} />
            <Bar
              dataKey="phones"
              stackId="a"
              radius={[4, 4, 0, 0]}
              maxBarSize={44}
            />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

export function ChartTooltipInteractiveExample() {
  return (
    <InteractiveDemo />
  )
}
import Charts
import EsuissUI
import SwiftUI

struct ChartInteractiveDemo: View {
  let quarters: [(quarter: String, phones: Double, laptops: Double, services: Double)] = [
    ("Q1 25", 69.1, 9, 26.3),
    ("Q2 25", 46.8, 8, 26.6),
    ("Q3 25", 44.6, 8, 27.4),
    ("Q4 25", 46.2, 8.7, 28.8),
    ("Q1 26", 71, 9.5, 29.1),
    ("Q2 26", 49.4, 8.6, 29.4),
    ("Q3 26", 47.3, 8.9, 30.1),
    ("Q4 26", 49, 9.3, 31.2),
  ]
  let config: ChartConfig = [
    "phones": .init("Phones", color: Palette.blue500),
    "laptops": .init("Laptops", color: Palette.teal500),
    "services": .init("Services", color: Palette.purple500),
  ]
  let order = ["services", "laptops", "phones"]

  @State private var indicator: ChartTooltipIndicator = .dot
  @State private var showTotal = true
  @State private var hideLabel = false
  @State private var hideIndicator = false

  func value(_ key: String, _ index: Int) -> Double {
    switch key {
    case "phones": quarters[index].phones
    case "laptops": quarters[index].laptops
    default: quarters[index].services
    }
  }

  var indicatorSelection: Binding<ChartTooltipIndicator?> {
    Binding(
      get: { indicator },
      set: { next in
        if let next { indicator = next }
      }
    )
  }

  var body: some View {
    Card(.filled, size: .sm) {
      VStack(alignment: .leading, spacing: 2) {
        Text("Tooltip settings")
          .textStyle(.lg, weight: .semibold)
        Text("Quarterly revenue · billions · sample data. Change the settings and touch the bars.")
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      .padding(.horizontal, 16)
      CardContent {
        VStack(alignment: .leading, spacing: 20) {
          FlowRow(spacing: 12) {
            VStack(alignment: .leading, spacing: 6) {
              Text("Indicator")
                .textStyle(.xs, weight: .semibold)
                .foregroundStyle(EsuissColor.mutedForeground)
              ToggleGroup(selection: indicatorSelection, size: .sm, spacing: 0) {
                ToggleGroupItem("Dot", value: ChartTooltipIndicator.dot)
                ToggleGroupItem("Line", value: ChartTooltipIndicator.line)
                ToggleGroupItem("Dashed", value: ChartTooltipIndicator.dashed)
              }
              .accessibilityLabel(Text("Indicator"))
            }
            ChartSetting(label: "Total in title", isOn: $showTotal)
            ChartSetting(label: "Hide title", isOn: $hideLabel)
            ChartSetting(label: "Hide indicator", isOn: $hideIndicator)
          }
          ChartContainer(config: config) {
            ChartAnimator(animationDuration: 400) { progress in
              Chart {
                ForEach(quarters.indices, id: \.self) { index in
                  ForEach(order, id: \.self) { key in
                    let start = order.prefix { $0 != key }.reduce(0) { $0 + value($1, index) }
                    BarMark(
                      x: .value("Quarter", quarters[index].quarter),
                      yStart: .value("Start", start * progress),
                      yEnd: .value("End", (start + value(key, index)) * progress),
                      width: .fixed(30)
                    )
                    .clipShape(
                      UnevenRoundedRectangle(
                        topLeadingRadius: key == "phones" ? 4 : 0,
                        bottomLeadingRadius: key == "services" ? 4 : 0,
                        bottomTrailingRadius: key == "services" ? 4 : 0,
                        topTrailingRadius: key == "phones" ? 4 : 0,
                        style: .continuous
                      )
                    )
                    .foregroundStyle(config.color(key))
                  }
                  ChartAnchorMark(
                    x: .value("Quarter", quarters[index].quarter),
                    y: .value("End", order.reduce(0) { $0 + value($1, index) })
                  )
                }
              }
              .chartXAxis { ChartAxis.x(values: [0, 2, 4, 6].map { quarters[$0].quarter }) { $0 } }
              .chartYAxis { ChartAxis.y(count: 5) { "\(Int($0))B" } }
              .chartTooltip(
                x: quarters.map(\.quarter),
                defaultIndex: 4,
                cursor: false,
                accessibilityLabel: "Quarterly revenue",
                items: { index in order.map { ChartTooltipItem($0, value: value($0, index)) } }
              ) { payload in
                if showTotal {
                  ChartTooltipContent(payload, indicator: indicator, hideLabel: hideLabel, hideIndicator: hideIndicator) {
                    payload in
                    HStack(spacing: 16) {
                      Text(verbatim: payload.label)
                      Spacer(minLength: 0)
                      Text("\(payload.total, format: .number.precision(.fractionLength(0...1)))B")
                        .monospacedDigit()
                    }
                  }
                } else {
                  ChartTooltipContent(payload, indicator: indicator, hideLabel: hideLabel, hideIndicator: hideIndicator)
                }
              }
            }
            ChartLegendContent()
          }
          .frame(height: 288)
        }
      }
    }
  }
}

struct ChartSetting: View {
  let label: LocalizedStringKey
  @Binding var isOn: Bool

  var body: some View {
    Toggle(isOn: $isOn) {
      Text(label).textStyle(.sm)
    }
    .toggleStyle(.switch(size: .sm))
  }
}
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.BarChart
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.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.ChartTooltipContent
import com.esuiss.ui.components.ChartTooltipIndicator
import com.esuiss.ui.components.Label
import com.esuiss.ui.components.Switch
import com.esuiss.ui.components.SwitchSize
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.ToggleGroup
import com.esuiss.ui.components.ToggleGroupItem
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Typography
import java.text.NumberFormat

class ChartRevenue(val quarter: String, val phones: Double, val laptops: Double, val services: Double)

val chartRevenueData = listOf(
    ChartRevenue("Q1 25", 69.1, 9.0, 26.3),
    ChartRevenue("Q2 25", 46.8, 8.0, 26.6),
    ChartRevenue("Q3 25", 44.6, 8.0, 27.4),
    ChartRevenue("Q4 25", 46.2, 8.7, 28.8),
    ChartRevenue("Q1 26", 71.0, 9.5, 29.1),
    ChartRevenue("Q2 26", 49.4, 8.6, 29.4),
    ChartRevenue("Q3 26", 47.3, 8.9, 30.1),
    ChartRevenue("Q4 26", 49.0, 9.3, 31.2),
)

val chartRevenueConfig = ChartConfig(
    "phones" to ChartConfigItem("Phones") { chart1 },
    "laptops" to ChartConfigItem("Laptops") { chart2 },
    "services" to ChartConfigItem("Services") { Palette.purple500 },
)

fun chartTotal(value: Double): String = NumberFormat.getNumberInstance().apply {
    maximumFractionDigits = 1
}.format(value)

fun chartTick(value: Double): String =
    if (value == Math.floor(value)) value.toLong().toString() else value.toString()

@Composable
fun ChartSetting(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
    Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) {
        Switch(checked, onCheckedChange, Modifier.semantics { contentDescription = label }, size = SwitchSize.Sm)
        Label(Modifier.clickable(interactionSource = null, indication = null) { onCheckedChange(!checked) }) {
            Text(label, style = Typography.sm)
        }
    }
}

@Composable
fun ChartInteractive() {
    val colors = EsuissTheme.colors
    var indicator by rememberSaveable { mutableStateOf("dot") }
    var showTotal by rememberSaveable { mutableStateOf(true) }
    var hideLabel by rememberSaveable { mutableStateOf(false) }
    var hideIndicator by rememberSaveable { mutableStateOf(false) }
    Card(variant = CardVariant.Filled, size = CardSize.Sm) {
        CardContent {
            Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text("Tooltip settings", style = Typography.lg.copy(fontWeight = FontWeight.SemiBold))
                Text(
                    "Quarterly revenue · billions · sample data. Change the settings and touch the bars.",
                    style = Typography.sm.copy(color = colors.mutedForeground),
                )
            }
        }
        CardContent {
            Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
                FlowRow(
                    horizontalArrangement = Arrangement.spacedBy(24.dp),
                    verticalArrangement = Arrangement.spacedBy(12.dp),
                    itemVerticalAlignment = Alignment.Bottom,
                ) {
                    Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
                        Text(
                            "Indicator",
                            style = Typography.xs.copy(color = colors.mutedForeground, fontWeight = FontWeight.SemiBold),
                        )
                        ToggleGroup(
                            value = indicator,
                            onValueChange = { if (it != null) indicator = it },
                            modifier = Modifier.semantics { contentDescription = "Indicator" },
                            size = ToggleSize.Sm,
                            spacing = 0,
                        ) {
                            ToggleGroupItem("Dot", "dot")
                            ToggleGroupItem("Line", "line")
                            ToggleGroupItem("Dashed", "dashed")
                        }
                    }
                    FlowRow(
                        modifier = Modifier.padding(bottom = 4.dp),
                        horizontalArrangement = Arrangement.spacedBy(20.dp),
                        verticalArrangement = Arrangement.spacedBy(10.dp),
                    ) {
                        ChartSetting("Total in title", showTotal) { showTotal = it }
                        ChartSetting("Hide title", hideLabel) { hideLabel = it }
                        ChartSetting("Hide indicator", hideIndicator) { hideIndicator = it }
                    }
                }
                ChartContainer(chartRevenueConfig, Modifier.fillMaxWidth().height(288.dp)) {
                    BarChart(
                        chartRevenueData,
                        margin = ChartMargin(top = 10.dp, right = 4.dp, bottom = 0.dp, left = 12.dp),
                        accessibilityLabel = "Quarterly revenue",
                    ) {
                        CartesianGrid(strokeDasharray = listOf(2f, 4f))
                        XAxis(label = { it.quarter }, tickMargin = 8.dp, minTickGap = 8.dp)
                        YAxis(orientation = ChartAxisOrientation.Right, tickMargin = 6.dp, tickFormatter = { "${chartTick(it)}B" })
                        ChartTooltip(defaultIndex = 4, cursor = null) {
                            ChartTooltipContent(
                                indicator = when (indicator) {
                                    "line" -> ChartTooltipIndicator.Line
                                    "dashed" -> ChartTooltipIndicator.Dashed
                                    else -> ChartTooltipIndicator.Dot
                                },
                                hideLabel = hideLabel,
                                hideIndicator = hideIndicator,
                                labelFormatter = if (showTotal) {
                                    { label, items ->
                                        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
                                            Text(
                                                label,
                                                Modifier.weight(1f),
                                                style = Typography.xs.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
                                            )
                                            Text(
                                                "${chartTotal(items.sumOf { it.value })}B",
                                                style = Typography.xs.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                                            )
                                        }
                                    }
                                } else {
                                    null
                                },
                            )
                        }
                        ChartLegend()
                        Bar("services", value = { it.services }, stackId = "a", radius = ChartRadius(0.dp, 0.dp, 4.dp, 4.dp), maxBarSize = 44.dp)
                        Bar("laptops", value = { it.laptops }, stackId = "a", maxBarSize = 44.dp)
                        Bar("phones", value = { it.phones }, stackId = "a", radius = ChartRadius(4.dp, 4.dp, 0.dp, 0.dp), maxBarSize = 44.dp)
                    }
                }
            }
        }
    }
}

API Reference

01

ChartPalette

Resolves token colors to concrete values.

PropTypeDefaultDescription
colorsreadonly ChartColor[]RequiredToken colors to resolve.
children(palette: Partial<Record<ChartColor, string>>) => React.ReactNodeRequiredFunction that receives the resolved colors.
02

ChartContainer

Sizes the chart and provides the config.

PropTypeDefaultDescription
configChartConfigRequiredSeries labels, token colors and icons keyed by data key.

Also accepts the props of ChartPrimitive.ContainerProps.

03

LineChart

Cartesian chart for Line series.

Also accepts the props of CartesianChartProps.

04

AreaChart

Cartesian chart for Area series, with optional stacking.

Also accepts the props of CartesianChartProps, ChartPrimitive.RootProps.

05

BarChart

Cartesian chart for Bar series, vertical or horizontal.

Also accepts the props of CartesianChartProps, ChartPrimitive.RootProps.

06

ComposedChart

Cartesian chart that mixes lines, areas and bars.

Also accepts the props of CartesianChartProps.

07

PieChart

Pie and donut charts built from Pie.

Also accepts the props of ChartRootProps, ChartPrimitive.RootProps.

08

RadialBarChart

Circular bars built from RadialBar.

Also accepts the props of ChartRootProps, ChartPrimitive.RootProps.

09

RadarChart

Polar chart built from Radar and PolarGrid.

Also accepts the props of ChartRootProps, ChartPrimitive.RootProps.

10

ChartTooltipContent

Default tooltip card.

PropTypeDefaultDescription
classNamestring-Classes for the tooltip card.
indicator"line" | "dot" | "dashed""dot"dot, line or dashed. Defaults to dot.
hideLabelbooleanfalseHides the category label.
hideIndicatorbooleanfalseHides the color indicator.
nameKeystring-Data key used to look up series names in the config.
labelFormatter( label: React.ReactNode, payload: ChartPrimitive.TooltipItem[] ) => React.ReactNode-Formats the tooltip label.
formatter( value: ChartPrimitive.ChartValue, name: string, item: ChartPrimitive.TooltipItem, index: number, payload: ChartPrimitive.ChartDatum ) => React.ReactNode-Renders each row yourself.
11

ChartLegendContent

Default legend with swatches or icons.

PropTypeDefaultDescription
classNamestring-Classes for the legend row.
nameKeystring-Data key used to look up legend names in the config.

Accessibility

  • The chart is one accessible element labelled with its series names and data range; pass accessibilityLabel to ChartContainer to override the names.
  • With a tooltip it has the adjustable role: VoiceOver and TalkBack swipe actions step through data points, and the selected values are announced as the value text.
  • Without a tooltip it has the image role.
  • With Reduce Motion on, charts skip the enter and update animations and draw in their final state.

Notes

  • Series colors accept chart-1 to chart-5, theme colors such as primary, destructive and foreground, and palette colors such as green-500 and red-500; grid and cursor lines use border.
  • Value labels from LabelList and pie label appear once a series finishes animating.
  • Light and dark follow the system appearance.
NextCheckboxPreviousCarousel

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Multiple series
    • Stacked
    • Line series
    • Area series
    • Donut
  4. 04API Reference
    • ChartContainer
    • ChartTooltipContent
    • ChartLegendContent
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Tooltip line
    • Tooltip dashed
    • Tooltip no indicator
    • Tooltip no label
    • Tooltip label formatter
    • Tooltip formatter
    • Tooltip icons
    • Tooltip custom
    • Tooltip interactive
  10. 10API Reference
    • ChartPalette
    • ChartContainer
    • LineChart
    • AreaChart
    • BarChart
    • ComposedChart
    • PieChart
    • RadialBarChart
    • RadarChart
    • ChartTooltipContent
    • ChartLegendContent
  11. 11Accessibility
  12. 12Notes