# Dash Analytics

An app analytics dashboard with a metric switcher, an interactive area chart, and a regional breakdown.

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

Source: https://ui.suiss.app/docs/blocks/dash-analytics

**React**

**Example**

```tsx title="components/blocks/dash-analytics.tsx"
"use client"

import {
  ChartBarIcon,
  FlameIcon,
  NotePencilIcon,
  StarIcon,
  TagIcon,
} from "@phosphor-icons/react"
import * as React from "react"
import {
  Area,
  AreaChart,
  CartesianGrid,
  useActiveTooltipDataPoints,
  XAxis,
} from "recharts"

import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
} from "@/components/ui/chart"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const apps = [
  {
    value: "route",
    label: "Route",
    hue: 150,
    icon: (
      <ChartBarIcon className="size-3.5" />
    ),
  },
  {
    value: "notebook",
    label: "Notebook",
    hue: 50,
    icon: (
      <NotePencilIcon className="size-3.5" />
    ),
  },
  {
    value: "fitness",
    label: "Fitness+",
    hue: 20,
    icon: (
      <FlameIcon className="size-3.5" />
    ),
  },
]

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value: string
  onValueChange: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          onValueChange(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const items: NavItem[] = [
  ...apps.map((app) => ({
    value: app.value,
    label: app.label,
    group: "Apps",
    icon: (
      <span
        style={{
          background: `linear-gradient(160deg, oklch(0.8 0.14 ${app.hue}), oklch(0.58 0.14 ${app.hue + 25}))`,
        }}
        className="flex size-6 shrink-0 items-center justify-center rounded-md text-white"
      >
        {app.icon}
      </span>
    ),
  })),
  {
    value: "analytics",
    label: "Analytics",
    icon: (
      <ChartBarIcon />
    ),
    group: "Reports",
  },
  {
    value: "sales",
    label: "Sales and Trends",
    icon: (
      <TagIcon />
    ),
    group: "Reports",
  },
  {
    value: "ratings",
    label: "Ratings",
    icon: (
      <StarIcon />
    ),
    group: "Reports",
  },
]

const metrics = [
  {
    value: "impressions",
    label: "Impressions",
    unit: "K",
    data: [12, 14, 13, 18, 17, 22, 25, 24, 28, 31, 29, 34],
  },
  {
    value: "downloads",
    label: "Downloads",
    unit: "K",
    data: [2.1, 2.4, 2.2, 3.1, 2.9, 3.8, 4.2, 4, 4.6, 5.1, 4.8, 5.6],
  },
  {
    value: "revenue",
    label: "Revenue",
    unit: "K $",
    data: [8, 9, 9, 11, 12, 14, 15, 15, 17, 19, 18, 21],
  },
  {
    value: "sessions",
    label: "Sessions",
    unit: "K",
    data: [40, 42, 41, 47, 48, 55, 58, 57, 62, 66, 64, 71],
  },
]

const regions = [
  { name: "United States", share: 0.46 },
  { name: "Germany", share: 0.21 },
  { name: "United Kingdom", share: 0.14 },
  { name: "Netherlands", share: 0.09 },
]

const chartConfig = {
  value: { label: "Value", color: "var(--primary)" },
} satisfies ChartConfig

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

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

  return null
}

export function DashAnalytics() {
  const id = React.useId()
  const [section, setSection] = React.useState("route")
  const [metricValue, setMetricValue] = React.useState("downloads")
  const [active, setActive] = React.useState<number | null>(null)
  const appIndex = Math.max(
    0,
    apps.findIndex((app) => app.value === section)
  )
  const metric =
    metrics.find((item) => item.value === metricValue) ?? metrics[1]
  if (!metric) return null
  const data = metric.data.map((value, index) => ({
    week: `Week ${index + 1}`,
    index,
    value: Number((value * (1 - appIndex * 0.28)).toFixed(2)),
  }))
  const current = active ?? data.length - 1
  const point = data[current]
  if (!point) return null
  const latest = data.at(-1)?.value ?? 0
  const format = (value: number) => `${value.toFixed(1)} ${metric.unit}`
  const title = items.find((item) => item.value === section)?.label

  return (
    <SidebarProvider>
      <NavSidebar items={items} value={section} onValueChange={setSection} />
      <SidebarInset>
        <div className="flex flex-col gap-5 px-5 py-5.5 md:px-7">
          <div className="flex flex-wrap items-start gap-4">
            <SidebarTrigger className="-ms-2 mt-1" />
            <div className="flex flex-1 flex-col">
              <h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
              <p className="text-sm text-muted-foreground">
                Last 12 weeks · All regions
              </p>
            </div>
            <ToggleGroup
              spacing={0}
              value={[metricValue]}
              onValueChange={(value: string[]) => {
                if (value[0]) setMetricValue(value[0])
              }}
              aria-label="Metric"
              className="no-scrollbar max-w-full overflow-x-auto"
            >
              {metrics.map((item) => (
                <ToggleGroupItem key={item.value} value={item.value} size="sm">
                  {item.label}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </div>
          <div className="rounded-xl bg-muted px-4 pt-3.5 pb-2.5">
            <div className="flex items-end gap-2.5">
              <div className="flex flex-1 flex-col" aria-live="polite">
                <span className="text-sm text-muted-foreground">
                  {current === data.length - 1 ? "This week" : point.week} ·{" "}
                  {metric.label}
                </span>
                <span className="text-3xl font-bold tracking-tight tabular-nums">
                  {format(point.value)}
                </span>
              </div>
              <span className="text-sm font-semibold text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-green-500">
                +{12 + metrics.indexOf(metric) * 3}%
              </span>
            </div>
            <ChartContainer
              config={chartConfig}
              className="mt-3 aspect-auto h-44 w-full"
            >
              <AreaChart
                accessibilityLayer
                data={data}
                margin={{ top: 8, right: 6, left: 6, bottom: 0 }}
              >
                <defs>
                  <linearGradient id={`${id}-fill`} x1="0" y1="0" x2="0" y2="1">
                    <stop
                      offset="0%"
                      stopColor="var(--color-value)"
                      stopOpacity={0.22}
                    />
                    <stop
                      offset="100%"
                      stopColor="var(--color-value)"
                      stopOpacity={0}
                    />
                  </linearGradient>
                </defs>
                <CartesianGrid vertical={false} strokeDasharray="2 4" />
                <XAxis dataKey="week" hide />
                <ChartTooltip
                  content={() => null}
                  cursor={{ stroke: "var(--color-input)" }}
                  defaultIndex={data.length - 1}
                />
                <Area
                  dataKey="value"
                  type="linear"
                  stroke="var(--color-value)"
                  strokeWidth={2.5}
                  fill={`url(#${id}-fill)`}
                  activeDot={{
                    r: 5,
                    fill: "var(--color-value)",
                    stroke: "var(--color-muted)",
                    strokeWidth: 2,
                  }}
                  animationDuration={500}
                />
                <ActiveWeek onChange={setActive} />
              </AreaChart>
            </ChartContainer>
          </div>
          <h2 className="-mb-2.5 px-1 text-sm font-semibold">By region</h2>
          <ItemGroup variant="inset">
            {regions.map((region, index) => (
              <React.Fragment key={region.name}>
                {index > 0 && <ItemSeparator />}
                <Item size="sm" role="listitem">
                  <ItemContent>
                    <ItemTitle className="font-normal">{region.name}</ItemTitle>
                  </ItemContent>
                  <ItemActions className="gap-4">
                    <span className="hidden h-1.5 w-35 overflow-hidden rounded-full bg-secondary sm:block">
                      <span
                        style={{ width: `${(region.share / 0.46) * 100}%` }}
                        className="block h-full rounded-full bg-primary transition-[width] duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]"
                      />
                    </span>
                    <span className="w-18 text-end tabular-nums">
                      {format(latest * region.share)}
                    </span>
                  </ItemActions>
                </Item>
              </React.Fragment>
            ))}
          </ItemGroup>
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

An analytics dashboard for apps: pick a metric, read its current value and growth, and hover the 12-week area chart to see any week's figure in the headline. A By region list below breaks the metric down by share. The app list sits in a `Sidebar` that collapses to icons on desktop and opens as a sheet from the trigger on mobile. Start here for product analytics, traffic or revenue reports.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/dash-analytics
```

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

```bash title="Terminal"
npx @esuiss/ui@latest add blocks dash-analytics
```

### Manual

1. Install the dependencies:

```bash title="Terminal"
npm install @phosphor-icons/react recharts
```

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

3. Copy the source into `components/blocks/dash-analytics.tsx`:

```tsx title="components/blocks/dash-analytics.tsx"
"use client"

import {
  ChartBarIcon,
  FlameIcon,
  NotePencilIcon,
  StarIcon,
  TagIcon,
} from "@phosphor-icons/react"
import * as React from "react"
import {
  Area,
  AreaChart,
  CartesianGrid,
  useActiveTooltipDataPoints,
  XAxis,
} from "recharts"

import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
} from "@/components/ui/chart"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const apps = [
  {
    value: "route",
    label: "Route",
    hue: 150,
    icon: (
      <ChartBarIcon className="size-3.5" />
    ),
  },
  {
    value: "notebook",
    label: "Notebook",
    hue: 50,
    icon: (
      <NotePencilIcon className="size-3.5" />
    ),
  },
  {
    value: "fitness",
    label: "Fitness+",
    hue: 20,
    icon: (
      <FlameIcon className="size-3.5" />
    ),
  },
]

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value: string
  onValueChange: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          onValueChange(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const items: NavItem[] = [
  ...apps.map((app) => ({
    value: app.value,
    label: app.label,
    group: "Apps",
    icon: (
      <span
        style={{
          background: `linear-gradient(160deg, oklch(0.8 0.14 ${app.hue}), oklch(0.58 0.14 ${app.hue + 25}))`,
        }}
        className="flex size-6 shrink-0 items-center justify-center rounded-md text-white"
      >
        {app.icon}
      </span>
    ),
  })),
  {
    value: "analytics",
    label: "Analytics",
    icon: (
      <ChartBarIcon />
    ),
    group: "Reports",
  },
  {
    value: "sales",
    label: "Sales and Trends",
    icon: (
      <TagIcon />
    ),
    group: "Reports",
  },
  {
    value: "ratings",
    label: "Ratings",
    icon: (
      <StarIcon />
    ),
    group: "Reports",
  },
]

const metrics = [
  {
    value: "impressions",
    label: "Impressions",
    unit: "K",
    data: [12, 14, 13, 18, 17, 22, 25, 24, 28, 31, 29, 34],
  },
  {
    value: "downloads",
    label: "Downloads",
    unit: "K",
    data: [2.1, 2.4, 2.2, 3.1, 2.9, 3.8, 4.2, 4, 4.6, 5.1, 4.8, 5.6],
  },
  {
    value: "revenue",
    label: "Revenue",
    unit: "K $",
    data: [8, 9, 9, 11, 12, 14, 15, 15, 17, 19, 18, 21],
  },
  {
    value: "sessions",
    label: "Sessions",
    unit: "K",
    data: [40, 42, 41, 47, 48, 55, 58, 57, 62, 66, 64, 71],
  },
]

const regions = [
  { name: "United States", share: 0.46 },
  { name: "Germany", share: 0.21 },
  { name: "United Kingdom", share: 0.14 },
  { name: "Netherlands", share: 0.09 },
]

const chartConfig = {
  value: { label: "Value", color: "var(--primary)" },
} satisfies ChartConfig

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

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

  return null
}

export function DashAnalytics() {
  const id = React.useId()
  const [section, setSection] = React.useState("route")
  const [metricValue, setMetricValue] = React.useState("downloads")
  const [active, setActive] = React.useState<number | null>(null)
  const appIndex = Math.max(
    0,
    apps.findIndex((app) => app.value === section)
  )
  const metric =
    metrics.find((item) => item.value === metricValue) ?? metrics[1]
  if (!metric) return null
  const data = metric.data.map((value, index) => ({
    week: `Week ${index + 1}`,
    index,
    value: Number((value * (1 - appIndex * 0.28)).toFixed(2)),
  }))
  const current = active ?? data.length - 1
  const point = data[current]
  if (!point) return null
  const latest = data.at(-1)?.value ?? 0
  const format = (value: number) => `${value.toFixed(1)} ${metric.unit}`
  const title = items.find((item) => item.value === section)?.label

  return (
    <SidebarProvider>
      <NavSidebar items={items} value={section} onValueChange={setSection} />
      <SidebarInset>
        <div className="flex flex-col gap-5 px-5 py-5.5 md:px-7">
          <div className="flex flex-wrap items-start gap-4">
            <SidebarTrigger className="-ms-2 mt-1" />
            <div className="flex flex-1 flex-col">
              <h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
              <p className="text-sm text-muted-foreground">
                Last 12 weeks · All regions
              </p>
            </div>
            <ToggleGroup
              spacing={0}
              value={[metricValue]}
              onValueChange={(value: string[]) => {
                if (value[0]) setMetricValue(value[0])
              }}
              aria-label="Metric"
              className="no-scrollbar max-w-full overflow-x-auto"
            >
              {metrics.map((item) => (
                <ToggleGroupItem key={item.value} value={item.value} size="sm">
                  {item.label}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </div>
          <div className="rounded-xl bg-muted px-4 pt-3.5 pb-2.5">
            <div className="flex items-end gap-2.5">
              <div className="flex flex-1 flex-col" aria-live="polite">
                <span className="text-sm text-muted-foreground">
                  {current === data.length - 1 ? "This week" : point.week} ·{" "}
                  {metric.label}
                </span>
                <span className="text-3xl font-bold tracking-tight tabular-nums">
                  {format(point.value)}
                </span>
              </div>
              <span className="text-sm font-semibold text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-green-500">
                +{12 + metrics.indexOf(metric) * 3}%
              </span>
            </div>
            <ChartContainer
              config={chartConfig}
              className="mt-3 aspect-auto h-44 w-full"
            >
              <AreaChart
                accessibilityLayer
                data={data}
                margin={{ top: 8, right: 6, left: 6, bottom: 0 }}
              >
                <defs>
                  <linearGradient id={`${id}-fill`} x1="0" y1="0" x2="0" y2="1">
                    <stop
                      offset="0%"
                      stopColor="var(--color-value)"
                      stopOpacity={0.22}
                    />
                    <stop
                      offset="100%"
                      stopColor="var(--color-value)"
                      stopOpacity={0}
                    />
                  </linearGradient>
                </defs>
                <CartesianGrid vertical={false} strokeDasharray="2 4" />
                <XAxis dataKey="week" hide />
                <ChartTooltip
                  content={() => null}
                  cursor={{ stroke: "var(--color-input)" }}
                  defaultIndex={data.length - 1}
                />
                <Area
                  dataKey="value"
                  type="linear"
                  stroke="var(--color-value)"
                  strokeWidth={2.5}
                  fill={`url(#${id}-fill)`}
                  activeDot={{
                    r: 5,
                    fill: "var(--color-value)",
                    stroke: "var(--color-muted)",
                    strokeWidth: 2,
                  }}
                  animationDuration={500}
                />
                <ActiveWeek onChange={setActive} />
              </AreaChart>
            </ChartContainer>
          </div>
          <h2 className="-mb-2.5 px-1 text-sm font-semibold">By region</h2>
          <ItemGroup variant="inset">
            {regions.map((region, index) => (
              <React.Fragment key={region.name}>
                {index > 0 && <ItemSeparator />}
                <Item size="sm" role="listitem">
                  <ItemContent>
                    <ItemTitle className="font-normal">{region.name}</ItemTitle>
                  </ItemContent>
                  <ItemActions className="gap-4">
                    <span className="hidden h-1.5 w-35 overflow-hidden rounded-full bg-secondary sm:block">
                      <span
                        style={{ width: `${(region.share / 0.46) * 100}%` }}
                        className="block h-full rounded-full bg-primary transition-[width] duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]"
                      />
                    </span>
                    <span className="w-18 text-end tabular-nums">
                      {format(latest * region.share)}
                    </span>
                  </ItemActions>
                </Item>
              </React.Fragment>
            ))}
          </ItemGroup>
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { DashAnalytics } from "@/components/blocks/dash-analytics"
```

```tsx
<DashAnalytics />
```

Replace `metrics` with your series; each metric has a label, unit, current value and 12 weekly data points. `regions` holds each region's share, and `apps` and `items` fill the navigation. The sample fakes per-app numbers by scaling the first app's data and computes growth on the fly, so load real figures per app from your analytics source.

## Notes

The headline value is announced as you move through the chart, and the chart supports keyboard navigation.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/blocks-dash-analytics--default.light.webp)

```tsx title="uniwind/components/blocks/dash-analytics.tsx"
import {
  CaretDownIcon,
  ChartBarIcon,
  FlameIcon,
  NotePencilIcon,
  type Icon as PhosphorIcon,
  StarIcon,
  TagIcon,
} from "phosphor-react-native"
import * as React from "react"
import { ScrollView, View } from "react-native"
import { Button } from "@/components/ui/button"
import {
  Area,
  AreaChart,
  CartesianGrid,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  XAxis,
} from "@/components/ui/chart"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Header,
  HeaderActions,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const apps: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "route", label: "Route", icon: ChartBarIcon },
  { value: "notebook", label: "Notebook", icon: NotePencilIcon },
  { value: "fitness", label: "Fitness+", icon: FlameIcon },
]

const SECTIONS: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "analytics", label: "Analytics", icon: ChartBarIcon },
  { value: "sales", label: "Sales and Trends", icon: TagIcon },
  { value: "ratings", label: "Ratings", icon: StarIcon },
]

const metrics = [
  {
    value: "impressions",
    label: "Impressions",
    unit: "K",
    data: [12, 14, 13, 18, 17, 22, 25, 24, 28, 31, 29, 34],
  },
  {
    value: "downloads",
    label: "Downloads",
    unit: "K",
    data: [2.1, 2.4, 2.2, 3.1, 2.9, 3.8, 4.2, 4, 4.6, 5.1, 4.8, 5.6],
  },
  {
    value: "revenue",
    label: "Revenue",
    unit: "K $",
    data: [8, 9, 9, 11, 12, 14, 15, 15, 17, 19, 18, 21],
  },
  {
    value: "sessions",
    label: "Sessions",
    unit: "K",
    data: [40, 42, 41, 47, 48, 55, 58, 57, 62, 66, 64, 71],
  },
]

const regions = [
  { name: "United States", share: 0.46 },
  { name: "Germany", share: 0.21 },
  { name: "United Kingdom", share: 0.14 },
  { name: "Netherlands", share: 0.09 },
]

const chartConfig = {
  value: { label: "Value", color: "primary" },
} satisfies ChartConfig

function AppMenu({
  value,
  onValueChange,
}: {
  value: string
  onValueChange: (value: string) => void
}) {
  const current = apps.find((item) => item.value === value) ?? apps[0]
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        render={<Button variant="secondary" size="sm" />}
        accessibilityLabel={`App, ${current?.label}`}
      >
        {current ? <Icon as={current.icon} className="size-4" /> : null}
        <Text>{current?.label}</Text>
        <Icon as={CaretDownIcon} weight="bold" className="size-3" />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuRadioGroup
          value={value}
          onValueChange={(next) => {
            if (typeof next === "string") onValueChange(next)
          }}
        >
          {apps.map((item) => (
            <DropdownMenuRadioItem key={item.value} value={item.value}>
              <Icon as={item.icon} className="size-4.5 text-foreground" />
              <Text>{item.label}</Text>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function OtherSection({ value }: { value: string }) {
  const label = SECTIONS.find((section) => section.value === value)?.label
  return (
    <View className="flex-1 gap-2 px-4 pt-13">
      <Text role="heading" className="text-3xl font-bold">
        {label}
      </Text>
      <Text className="text-lg text-muted-foreground">
        This section isn't part of the sample. Choose Analytics.
      </Text>
    </View>
  )
}

function DashAnalytics({
  onAppChange,
  onMetricChange,
  onNavigate,
}: {
  onAppChange?: (app: string) => void
  onMetricChange?: (metric: string) => void
  onNavigate?: (section: string) => void
}) {
  const [section, setSection] = React.useState("analytics")
  const [app, setApp] = React.useState("route")
  const [metricValue, setMetricValue] = React.useState("downloads")
  const [active, setActive] = React.useState<number | null>(null)
  const scroll = useHeaderScroll()
  const appIndex = Math.max(
    0,
    apps.findIndex((item) => item.value === app)
  )
  const metric =
    metrics.find((item) => item.value === metricValue) ?? metrics[1]
  if (!metric) return null
  const data = metric.data.map((value, index) => ({
    week: `Week ${index + 1}`,
    index,
    value: Number((value * (1 - appIndex * 0.28)).toFixed(2)),
  }))
  const current = active ?? data.length - 1
  const point = data[current]
  if (!point) return null
  const latest = data.at(-1)?.value ?? 0
  const format = (value: number) => `${value.toFixed(1)} ${metric.unit}`
  const title = apps[appIndex]?.label

  return (
    <Screen edges={["top", "right", "left"]}>
      {section === "analytics" ? (
        <>
          <Header scroll={scroll}>
            <HeaderTitle>{title}</HeaderTitle>
            <HeaderActions>
              <AppMenu
                value={app}
                onValueChange={(value) => {
                  setApp(value)
                  onAppChange?.(value)
                }}
              />
            </HeaderActions>
          </Header>
          <HeaderScrollView scroll={scroll}>
            <HeaderLargeTitle subtitle="Last 12 weeks · All regions">
              {title}
            </HeaderLargeTitle>
            <View className="gap-5 pt-3 pb-8">
              <ScrollView
                horizontal
                showsHorizontalScrollIndicator={false}
                className="grow-0"
                contentContainerClassName="px-4"
              >
                <ToggleGroup
                  spacing={0}
                  value={[metricValue]}
                  onValueChange={(value) => {
                    if (!value[0]) return
                    setMetricValue(value[0])
                    onMetricChange?.(value[0])
                  }}
                  accessibilityLabel="Metric"
                >
                  {metrics.map((item) => (
                    <ToggleGroupItem
                      key={item.value}
                      value={item.value}
                      size="sm"
                    >
                      <Text>{item.label}</Text>
                    </ToggleGroupItem>
                  ))}
                </ToggleGroup>
              </ScrollView>
              <View className="mx-4 rounded-xl bg-muted px-4 pt-3.5 pb-2.5">
                <View className="flex-row items-end gap-2.5">
                  <View
                    accessibilityLiveRegion="polite"
                    className="min-w-0 flex-1"
                  >
                    <Text className="text-sm text-muted-foreground">
                      {`${current === data.length - 1 ? "This week" : point.week} · ${metric.label}`}
                    </Text>
                    <Text className="text-3xl font-bold tracking-tight tabular-nums">
                      {format(point.value)}
                    </Text>
                  </View>
                  <Text className="text-sm font-semibold text-green-500">
                    {`+${12 + metrics.indexOf(metric) * 3}%`}
                  </Text>
                </View>
                <ChartContainer
                  config={chartConfig}
                  accessibilityLabel={metric.label}
                  className="mt-3 h-44 w-full"
                >
                  <AreaChart
                    data={data}
                    margin={{ top: 8, right: 6, left: 6, bottom: 0 }}
                    onActiveIndexChange={setActive}
                  >
                    <CartesianGrid vertical={false} strokeDasharray="2 4" />
                    <XAxis dataKey="week" hide />
                    <ChartTooltip
                      content={() => null}
                      cursor={{ stroke: "input" }}
                      defaultIndex={data.length - 1}
                    />
                    <Area
                      animationDuration={500}
                      dataKey="value"
                      type="linear"
                      stroke="primary"
                      strokeWidth={2.5}
                      fill={{
                        stops: [
                          { offset: 0, opacity: 0.22 },
                          { offset: 1, opacity: 0 },
                        ],
                      }}
                      activeDot={{
                        r: 5,
                        fill: "primary",
                        stroke: "muted",
                        strokeWidth: 2,
                      }}
                    />
                  </AreaChart>
                </ChartContainer>
              </View>
              <Text
                role="heading"
                className="-mb-2.5 px-5 text-sm font-semibold"
              >
                By region
              </Text>
              <View className="px-4">
                <ItemGroup variant="inset">
                  {regions.map((region, index) => (
                    <React.Fragment key={region.name}>
                      {index > 0 ? <ItemSeparator /> : null}
                      <Item size="sm">
                        <ItemContent>
                          <ItemTitle className="font-normal">
                            {region.name}
                          </ItemTitle>
                        </ItemContent>
                        <ItemActions>
                          <Text className="text-right tabular-nums">
                            {format(latest * region.share)}
                          </Text>
                        </ItemActions>
                      </Item>
                    </React.Fragment>
                  ))}
                </ItemGroup>
              </View>
            </View>
          </HeaderScrollView>
        </>
      ) : (
        <OtherSection value={section} />
      )}
      <TabBar
        value={section}
        onValueChange={(value) => {
          setSection(value)
          onNavigate?.(value)
        }}
      >
        <TabBarList>
          {SECTIONS.map((item) => (
            <TabBarItem key={item.value} value={item.value} icon={item.icon}>
              {item.label}
            </TabBarItem>
          ))}
        </TabBarList>
      </TabBar>
    </Screen>
  )
}

export { DashAnalytics }
```

```swift title="swift/Blocks/DashAnalytics.swift"
import Charts
import PhosphorSwift
import SwiftUI

public struct DashAnalytics: View {
  struct App {
    let value: String
    let label: String
    let icon: Image
    let tint: Color
  }

  struct Metric {
    let value: String
    let label: String
    let unit: String
    let data: [Double]
  }

  static var apps: [App] {
    [
      App(value: "route", label: "Route", icon: Ph.chartBar.bold, tint: Palette.green500),
      App(value: "notebook", label: "Notebook", icon: Ph.notePencil.bold, tint: Palette.orange500),
      App(value: "fitness", label: "Fitness+", icon: Ph.flame.bold, tint: Palette.red500),
    ]
  }

  static let metrics = [
    Metric(value: "impressions", label: "Impressions", unit: "K", data: [12, 14, 13, 18, 17, 22, 25, 24, 28, 31, 29, 34]),
    Metric(value: "downloads", label: "Downloads", unit: "K", data: [2.1, 2.4, 2.2, 3.1, 2.9, 3.8, 4.2, 4, 4.6, 5.1, 4.8, 5.6]),
    Metric(value: "revenue", label: "Revenue", unit: "K $", data: [8, 9, 9, 11, 12, 14, 15, 15, 17, 19, 18, 21]),
    Metric(value: "sessions", label: "Sessions", unit: "K", data: [40, 42, 41, 47, 48, 55, 58, 57, 62, 66, 64, 71]),
  ]

  static let regions: [(name: String, share: Double)] = [
    ("United States", 0.46), ("Germany", 0.21), ("United Kingdom", 0.14), ("Netherlands", 0.09),
  ]

  static func data(metric: String, app: String) -> [Double] {
    let appIndex = Double(max(0, apps.firstIndex { $0.value == app } ?? 0))
    let source = (metrics.first { $0.value == metric } ?? metrics[1]).data
    return source.map { ($0 * (1 - appIndex * 0.28) * 100).rounded() / 100 }
  }

  private let chartConfig: ChartConfig = [
    "value": .init("Value", color: EsuissColor.primary),
  ]

  private var onAppChange: ((String) -> Void)?
  private var onMetricChange: ((String) -> Void)?
  private var onNavigate: ((String) -> Void)?
  @State private var app: String
  @State private var metric: String?
  @State private var active: Int?
  @Environment(\.colorScheme) private var colorScheme
  @Environment(\.locale) private var locale
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(
    onAppChange: ((_ app: String) -> Void)? = nil,
    onMetricChange: ((_ metric: String) -> Void)? = nil,
    onNavigate: ((_ section: String) -> Void)? = nil
  ) {
    self.init(app: "route", metric: "downloads")
    self.onAppChange = onAppChange
    self.onMetricChange = onMetricChange
    self.onNavigate = onNavigate
  }

  init(app: String, metric: String, active: Int? = nil) {
    _app = State(initialValue: app)
    _metric = State(initialValue: metric)
    _active = State(initialValue: active)
  }

  private var current: Metric {
    Self.metrics.first { $0.value == metric } ?? Self.metrics[1]
  }

  private var metricBinding: Binding<String?> {
    Binding(
      get: { metric },
      set: { value in
        guard let value, value != metric else { return }
        onMetricChange?(value)
        withAnimation(reduceMotion ? nil : ChartAnimationEasing.ease.animation(duration: 500)) {
          metric = value
        }
      }
    )
  }

  private var appBinding: Binding<String> {
    Binding(
      get: { app },
      set: { value in
        guard value != app else { return }
        onAppChange?(value)
        withAnimation(reduceMotion ? nil : ChartAnimationEasing.ease.animation(duration: 500)) {
          app = value
          active = nil
        }
      }
    )
  }

  private func format(_ value: Double) -> String {
    "\(value.formatted(.number.precision(.fractionLength(1)).locale(locale))) \(current.unit)"
  }

  public var body: some View {
    DashAnalyticsShell(onNavigate: onNavigate) {
      page
    }
  }

  private var page: some View {
    let title = Self.apps.first { $0.value == app }?.label ?? ""
    let data = Self.data(metric: current.value, app: app)
    let last = data.count - 1
    let index = active ?? last
    let latest = data[last]
    return Screen {
      HeaderScrollView {
        HeaderLargeTitle(verbatim: title, subtitle: "Last 12 weeks · All regions")
        VStack(alignment: .leading, spacing: 20) {
          ScrollView(.horizontal, showsIndicators: false) {
            ToggleGroup(selection: metricBinding, size: .sm, spacing: 0) {
              ForEach(Self.metrics, id: \.value) { item in
                ToggleGroupItem(LocalizedStringKey(item.label), value: item.value)
              }
            }
            .accessibilityLabel("Metric")
          }
          VStack(alignment: .leading, spacing: 12) {
            HStack(alignment: .bottom, spacing: 10) {
              VStack(alignment: .leading, spacing: 0) {
                Text("\(index == last ? String(localized: "This week") : "Week \(index + 1)") · \(Text(LocalizedStringKey(current.label)))")
                  .textStyle(.sm)
                  .foregroundStyle(EsuissColor.mutedForeground)
                Text(verbatim: format(data[index]))
                  .textStyle(.xl3, weight: .bold)
                  .monospacedDigit()
                  .contentTransition(.numericText(value: data[index]))
              }
              .frame(maxWidth: .infinity, alignment: .leading)
              .accessibilityElement(children: .combine)
              Text(verbatim: "+\(12 + (Self.metrics.firstIndex { $0.value == current.value } ?? 0) * 3)%")
                .textStyle(.sm, weight: .semibold)
                .foregroundStyle(Color.adaptive(light: Palette.green600, dark: Palette.green400))
            }
            chart(data)
          }
          .padding(.horizontal, 16)
          .padding(.top, 14)
          .padding(.bottom, 10)
          .background(EsuissColor.muted, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
          Text("By region")
            .textStyle(.sm, weight: .semibold)
            .padding(.horizontal, 4)
            .padding(.bottom, -10)
            .accessibilityAddTraits(.isHeader)
          ItemGroup(.inset) {
            ForEach(Array(Self.regions.enumerated()), id: \.offset) { offset, region in
              if offset > 0 {
                ItemSeparator()
              }
              Item(size: .sm) {
                ItemContent {
                  ItemTitle { Text(LocalizedStringKey(region.name)).fontWeight(.regular) }
                }
                ItemActions {
                  Text(verbatim: format(latest * region.share))
                    .monospacedDigit()
                    .contentTransition(.numericText(value: latest * region.share))
                }
              }
            }
          }
        }
        .padding(.horizontal, 16)
        .padding(.top, 4)
        .padding(.bottom, 24)
      } header: {
        Header {
          HeaderTitle(verbatim: title)
        } trailing: {
          DropdownMenu(align: .end) {
            DropdownMenuLabel("Apps")
            DropdownMenuRadioGroup(selection: appBinding) {
              ForEach(Self.apps, id: \.value) { item in
                DropdownMenuRadioItem(value: item.value) {
                  SwiftUI.Label {
                    Text(LocalizedStringKey(item.label))
                  } icon: {
                    DashAnalyticsTile(app: item)
                  }
                }
              }
            }
          } label: {
            HStack(spacing: 6) {
              if let item = Self.apps.first(where: { $0.value == app }) {
                DashAnalyticsTile(app: item)
              }
              Ph.caretDown.bold
                .frame(width: 12, height: 12)
                .foregroundStyle(EsuissColor.mutedForeground)
            }
            .padding(.horizontal, 8)
            .frame(height: 44)
            .contentShape(Rectangle())
          }
          .buttonStyle(.plain)
          .accessibilityLabel(Text("Apps"))
          .accessibilityValue(Text(verbatim: title))
        }
      }
    }
  }

  private func chart(_ data: [Double]) -> some View {
    let color = chartConfig.color("value")
    let selection = Binding<Int?>(get: { active ?? data.count - 1 }, set: { active = $0 })
    let index = active ?? data.count - 1
    return ChartContainer(config: chartConfig) {
      ChartAnimator(animationDuration: 500) { progress in
        Chart {
          ForEach(data.indices, id: \.self) { index in
            AreaMark(x: .value("Week", index), y: .value("Value", data[index]))
              .interpolationMethod(.linear)
              .foregroundStyle(LinearGradient(colors: [color.opacity(0.22), color.opacity(0)], startPoint: .top, endPoint: .bottom))
            LineMark(x: .value("Week", index), y: .value("Value", data[index]))
              .interpolationMethod(.linear)
              .lineStyle(StrokeStyle(lineWidth: 2.5, lineCap: .round, lineJoin: .round))
              .foregroundStyle(color)
          }
          .chartReveal(progress, x: 0...(data.count - 1))
          PointMark(x: .value("Week", index), y: .value("Value", data[index]))
            .symbol {
              Circle()
                .fill(color)
                .stroke(EsuissColor.muted, lineWidth: 2)
                .frame(width: 12, height: 12)
            }
            .chartReveal(progress, x: 0...(data.count - 1))
        }
        .chartXScale(domain: 0...(data.count - 1), range: .plotDimension(padding: 6))
        .chartXAxis(.hidden)
        .chartYAxis { ChartAxis.grid() }
        .chartTooltip(
          x: Array(data.indices),
          labels: data.indices.map { "Week \($0 + 1)" },
          selection: selection,
          accessibilityLabel: current.label,
          items: { [ChartTooltipItem("value", value: data[$0])] }
        ) { _ in
          EmptyView()
        }
      }
    }
    .frame(height: 176)
  }
}

private struct DashAnalyticsTile: View {
  let app: DashAnalytics.App

  var body: some View {
    app.icon
      .frame(width: 14, height: 14)
      .foregroundStyle(EsuissColor.primaryForeground)
      .frame(width: 24, height: 24)
      .background(app.tint, in: RoundedRectangle(cornerRadius: Radius.md, style: .continuous))
      .accessibilityHidden(true)
  }
}

private struct DashAnalyticsShell<Content: View>: View {
  let onNavigate: ((String) -> Void)?
  let content: Content

  @State private var tab = "analytics"

  init(onNavigate: ((String) -> Void)?, @ViewBuilder content: () -> Content) {
    self.onNavigate = onNavigate
    self.content = content()
  }

  private static var sections: [(value: String, label: LocalizedStringKey, icon: Image, active: Image)] {
    [
      ("analytics", "Analytics", Ph.chartBar.regular, Ph.chartBar.fill),
      ("sales", "Sales and Trends", Ph.tag.regular, Ph.tag.fill),
      ("ratings", "Ratings", Ph.star.regular, Ph.star.fill),
    ]
  }

  var body: some View {
    let selection = Binding<String>(
      get: { tab },
      set: { value in
        guard value != tab else { return }
        onNavigate?(value)
        tab = value
      }
    )
    Screen {
      ZStack {
        content
          .opacity(tab == "analytics" ? 1 : 0)
          .allowsHitTesting(tab == "analytics")
          .accessibilityHidden(tab != "analytics")
        if tab != "analytics" {
          Empty {
            EmptyHeader {
              EmptyDescription("This section isn't part of the sample. Choose \(Text("Analytics")).")
            }
          }
          .background(EsuissColor.background)
        }
      }
      .frame(maxHeight: .infinity)
      TabBar(selection: selection) {
        ForEach(Self.sections, id: \.value) { section in
          TabBarItem(section.label, value: section.value, icon: section.icon, activeIcon: section.active)
        }
      }
    }
  }
}
```

```kt title="kotlin/blocks/DashAnalytics.kt"
package com.esuiss.ui.blocks

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.bold.CaretDown
import com.adamglin.phosphoricons.bold.Flame
import com.adamglin.phosphoricons.bold.NotePencil
import com.adamglin.phosphoricons.bold.ChartBar as BoldChartBar
import com.adamglin.phosphoricons.fill.ChartBar as FillChartBar
import com.adamglin.phosphoricons.fill.Star as FillStar
import com.adamglin.phosphoricons.fill.Tag as FillTag
import com.adamglin.phosphoricons.regular.ChartBar
import com.adamglin.phosphoricons.regular.Star
import com.adamglin.phosphoricons.regular.Tag
import com.esuiss.ui.components.AreaChart
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartCursor
import com.esuiss.ui.components.ChartDot
import com.esuiss.ui.components.ChartFill
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.DropdownMenu
import com.esuiss.ui.components.DropdownMenuLabel
import com.esuiss.ui.components.DropdownMenuRadioGroup
import com.esuiss.ui.components.DropdownMenuRadioItem
import com.esuiss.ui.components.Empty
import com.esuiss.ui.components.EmptyDescription
import com.esuiss.ui.components.EmptyHeader
import com.esuiss.ui.components.Header
import com.esuiss.ui.components.HeaderActions
import com.esuiss.ui.components.HeaderLargeTitle
import com.esuiss.ui.components.HeaderTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Item
import com.esuiss.ui.components.ItemActions
import com.esuiss.ui.components.ItemContent
import com.esuiss.ui.components.ItemGroup
import com.esuiss.ui.components.ItemGroupVariant
import com.esuiss.ui.components.ItemSeparator
import com.esuiss.ui.components.ItemSize
import com.esuiss.ui.components.ItemTitle
import com.esuiss.ui.components.LocalContentStyle
import com.esuiss.ui.components.PopupAlign
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.TabBar
import com.esuiss.ui.components.TabBarItem
import com.esuiss.ui.components.ToggleGroup
import com.esuiss.ui.components.ToggleGroupItem
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.components.rememberHeaderState
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography
import java.util.Locale
import kotlin.math.roundToLong

private class AnalyticsApp(val value: String, val label: String, val icon: ImageVector, val tint: Colors.() -> Color)

private val AnalyticsApps = listOf(
    AnalyticsApp("route", "Route", PhosphorIcons.Bold.BoldChartBar) { Palette.green500 },
    AnalyticsApp("notebook", "Notebook", PhosphorIcons.Bold.NotePencil) { Palette.orange500 },
    AnalyticsApp("fitness", "Fitness+", PhosphorIcons.Bold.Flame) { Palette.red500 },
)

private class AnalyticsMetric(val value: String, val label: String, val unit: String, val data: List<Double>)

private val AnalyticsMetrics = listOf(
    AnalyticsMetric("impressions", "Impressions", "K", listOf(12.0, 14.0, 13.0, 18.0, 17.0, 22.0, 25.0, 24.0, 28.0, 31.0, 29.0, 34.0)),
    AnalyticsMetric("downloads", "Downloads", "K", listOf(2.1, 2.4, 2.2, 3.1, 2.9, 3.8, 4.2, 4.0, 4.6, 5.1, 4.8, 5.6)),
    AnalyticsMetric("revenue", "Revenue", "K $", listOf(8.0, 9.0, 9.0, 11.0, 12.0, 14.0, 15.0, 15.0, 17.0, 19.0, 18.0, 21.0)),
    AnalyticsMetric("sessions", "Sessions", "K", listOf(40.0, 42.0, 41.0, 47.0, 48.0, 55.0, 58.0, 57.0, 62.0, 66.0, 64.0, 71.0)),
)

private val AnalyticsRegions = listOf(
    "United States" to 0.46,
    "Germany" to 0.21,
    "United Kingdom" to 0.14,
    "Netherlands" to 0.09,
)

private val AnalyticsConfig = ChartConfig("value" to ChartConfigItem("Value") { primary })

private fun analyticsData(metric: AnalyticsMetric, app: String): List<Double> {
    val appIndex = AnalyticsApps.indexOfFirst { it.value == app }.coerceAtLeast(0)
    return metric.data.map { (it * (1 - appIndex * 0.28) * 100).roundToLong() / 100.0 }
}

@Composable
fun DashAnalytics(
    modifier: Modifier = Modifier,
    onAppChange: (app: String) -> Unit = {},
    onMetricChange: (metric: String) -> Unit = {},
    onNavigate: (section: String) -> Unit = {},
) {
    val colors = EsuissTheme.colors
    val header = rememberHeaderState()
    var app by rememberSaveable { mutableStateOf("route") }
    var metric by rememberSaveable { mutableStateOf("downloads") }
    var active by remember { mutableStateOf<Int?>(null) }
    var menu by remember { mutableStateOf(false) }
    val current = AnalyticsMetrics.firstOrNull { it.value == metric } ?: AnalyticsMetrics[1]
    val selectedApp = AnalyticsApps.firstOrNull { it.value == app } ?: AnalyticsApps[0]
    val data = analyticsData(current, app)
    val last = data.lastIndex
    val index = active ?: last
    val latest = data[last]
    fun format(value: Double) = "${String.format(Locale.US, "%.1f", value)} ${current.unit}"

    Screen(modifier, edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        DashTabs(
            home = "analytics",
            homeLabel = "Analytics",
            onNavigate = onNavigate,
            tabs = {
                TabBarItem("analytics", "Analytics", PhosphorIcons.Regular.ChartBar, selectedIcon = PhosphorIcons.Fill.FillChartBar)
                TabBarItem("sales", "Sales and Trends", PhosphorIcons.Regular.Tag, selectedIcon = PhosphorIcons.Fill.FillTag)
                TabBarItem("ratings", "Ratings", PhosphorIcons.Regular.Star, selectedIcon = PhosphorIcons.Fill.FillStar)
            },
        ) {
            Column(
                Modifier
                    .fillMaxSize()
                    .nestedScroll(header.nestedScrollConnection),
            ) {
                Header(state = header) {
                    HeaderTitle(selectedApp.label)
                    HeaderLargeTitle(selectedApp.label, subtitle = "Last 12 weeks · All regions")
                    HeaderActions {
                        DropdownMenu(
                            expanded = menu,
                            onExpandedChange = { menu = it },
                            align = PopupAlign.End,
                            trigger = {
                                Row(
                                    Modifier
                                        .height(44.dp)
                                        .clickable(
                                            interactionSource = remember { MutableInteractionSource() },
                                            indication = null,
                                            role = Role.Button,
                                        ) { menu = true }
                                        .semantics {
                                            contentDescription = "Apps"
                                            stateDescription = selectedApp.label
                                        }
                                        .padding(horizontal = 8.dp),
                                    horizontalArrangement = Arrangement.spacedBy(6.dp),
                                    verticalAlignment = Alignment.CenterVertically,
                                ) {
                                    AppTile(selectedApp)
                                    Icon(PhosphorIcons.Bold.CaretDown, contentDescription = null, size = 12.dp, tint = colors.mutedForeground)
                                }
                            },
                        ) {
                            DropdownMenuLabel("Apps")
                            DropdownMenuRadioGroup(
                                value = app,
                                onValueChange = { value ->
                                    if (value != app) {
                                        onAppChange(value)
                                        app = value
                                        active = null
                                    }
                                },
                            ) {
                                for (item in AnalyticsApps) {
                                    DropdownMenuRadioItem(item.value, closeOnClick = true) {
                                        AppTile(item)
                                        BasicText(item.label, style = LocalContentStyle.current, maxLines = 1)
                                    }
                                }
                            }
                        }
                    }
                }
                Column(
                    Modifier
                        .weight(1f)
                        .fillMaxWidth()
                        .verticalScroll(rememberScrollState())
                        .padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 24.dp),
                    verticalArrangement = Arrangement.spacedBy(20.dp),
                ) {
                    Row(Modifier.horizontalScroll(rememberScrollState())) {
                        ToggleGroup(
                            value = metric,
                            onValueChange = { value ->
                                if (value != null && value != metric) {
                                    onMetricChange(value)
                                    metric = value
                                }
                            },
                            modifier = Modifier.semantics { contentDescription = "Metric" },
                            size = ToggleSize.Sm,
                            spacing = 0,
                        ) {
                            for (item in AnalyticsMetrics) ToggleGroupItem(item.label, item.value)
                        }
                    }
                    Column(
                        Modifier
                            .fillMaxWidth()
                            .background(colors.muted, RoundedCornerShape(Radius.xl))
                            .padding(start = 16.dp, end = 16.dp, top = 14.dp, bottom = 10.dp),
                        verticalArrangement = Arrangement.spacedBy(12.dp),
                    ) {
                        Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.Bottom) {
                            Column(
                                Modifier
                                    .weight(1f)
                                    .semantics(mergeDescendants = true) { liveRegion = LiveRegionMode.Polite },
                            ) {
                                BasicText(
                                    "${if (index == last) "This week" else "Week ${index + 1}"} · ${current.label}",
                                    style = Typography.sm.copy(color = colors.mutedForeground),
                                )
                                BasicText(
                                    format(data[index]),
                                    style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.Bold, fontFeatureSettings = "tnum"),
                                )
                            }
                            BasicText(
                                "+${12 + AnalyticsMetrics.indexOf(current) * 3}%",
                                style = Typography.sm.copy(color = if (colors.isDark) Palette.green400 else Palette.green600, fontWeight = FontWeight.SemiBold),
                            )
                        }
                        ChartContainer(
                            AnalyticsConfig,
                            Modifier
                                .fillMaxWidth()
                                .height(176.dp),
                        ) {
                            AreaChart(
                                data.indices.toList(),
                                margin = ChartMargin(top = 8.dp, right = 6.dp, bottom = 6.dp, left = 6.dp),
                                accessibilityLabel = current.label,
                                selectedIndex = index,
                                onSelectedIndexChange = { active = it },
                            ) {
                                CartesianGrid(strokeDasharray = listOf(2f, 4f))
                                XAxis(label = { "Week ${it + 1}" }, hide = true)
                                ChartTooltip(cursor = ChartCursor()) {}
                                Area(
                                    "value",
                                    value = { data[it] },
                                    strokeWidth = 2.5.dp,
                                    fill = ChartFill.VerticalGradient(topAlpha = 0.22f, bottomAlpha = 0f),
                                    activeDot = ChartDot(6.dp, strokeColor = colors.muted, strokeWidth = 2.dp),
                                    animationDuration = 500,
                                )
                            }
                        }
                    }
                    Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
                        BasicText(
                            "By region",
                            Modifier
                                .padding(horizontal = 4.dp)
                                .semantics { heading() },
                            style = Typography.sm.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
                        )
                        ItemGroup(variant = ItemGroupVariant.Inset) {
                            AnalyticsRegions.forEachIndexed { position, (name, share) ->
                                if (position > 0) ItemSeparator()
                                Item(size = ItemSize.Sm) {
                                    ItemContent { ItemTitle(name) }
                                    ItemActions {
                                        BasicText(
                                            format(latest * share),
                                            style = Typography.base.copy(color = colors.mutedForeground, fontFeatureSettings = "tnum"),
                                        )
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun AppTile(app: AnalyticsApp) {
    val colors = EsuissTheme.colors
    Box(
        Modifier
            .size(24.dp)
            .background(app.tint(colors), RoundedCornerShape(Radius.md)),
        contentAlignment = Alignment.Center,
    ) {
        Icon(app.icon, contentDescription = null, size = 14.dp, tint = colors.primaryForeground)
    }
}

@Composable
internal fun DashTabs(
    home: String,
    homeLabel: String,
    onNavigate: (String) -> Unit,
    tabs: @Composable RowScope.() -> Unit,
    content: @Composable () -> Unit,
) {
    val colors = EsuissTheme.colors
    var tab by rememberSaveable { mutableStateOf(home) }
    Column(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .weight(1f)
                .fillMaxWidth(),
        ) {
            if (tab == home) {
                content()
            } else {
                Empty(
                    Modifier
                        .fillMaxSize()
                        .background(colors.background),
                ) {
                    EmptyHeader {
                        EmptyDescription("This section isn't part of the sample. Choose $homeLabel.")
                    }
                }
            }
        }
        TabBar(
            value = tab,
            onValueChange = { value ->
                if (value != tab) {
                    onNavigate(value)
                    tab = value
                }
            },
            content = tabs,
        )
    }
}
```

Composes `Screen`, `Header`, `DropdownMenu` for picking the app, `ToggleGroup`, the `AreaChart` from `Chart`, `Item` rows and `TabBar`. Switching the metric or app redraws the chart, and dragging across the chart updates the headline value for that week. The other tabs show a placeholder section.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/dash-analytics
```

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

```bash title="Terminal"
npx @esuiss/ui@latest add dash-analytics
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add dash-analytics --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add dash-analytics --platform kotlin
```

The CLI also adds what it builds on: [Button](https://ui.suiss.app/docs/components/button), [Chart](https://ui.suiss.app/docs/components/chart), [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Chart](https://ui.suiss.app/docs/components/chart), [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/blocks/dash-analytics.tsx`:

- `uniwind/components/blocks/dash-analytics.tsx`: shown above

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Chart](https://ui.suiss.app/docs/components/chart), [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/blocks/dash-analytics.tsx`:

```tsx title="nativewind/components/blocks/dash-analytics.tsx"
import {
  CaretDownIcon,
  ChartBarIcon,
  FlameIcon,
  NotePencilIcon,
  type Icon as PhosphorIcon,
  StarIcon,
  TagIcon,
} from "phosphor-react-native"
import * as React from "react"
import { ScrollView, View } from "react-native"
import { Button } from "@/components/ui/button"
import {
  Area,
  AreaChart,
  CartesianGrid,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  XAxis,
} from "@/components/ui/chart"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Header,
  HeaderActions,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const apps: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "route", label: "Route", icon: ChartBarIcon },
  { value: "notebook", label: "Notebook", icon: NotePencilIcon },
  { value: "fitness", label: "Fitness+", icon: FlameIcon },
]

const SECTIONS: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "analytics", label: "Analytics", icon: ChartBarIcon },
  { value: "sales", label: "Sales and Trends", icon: TagIcon },
  { value: "ratings", label: "Ratings", icon: StarIcon },
]

const metrics = [
  {
    value: "impressions",
    label: "Impressions",
    unit: "K",
    data: [12, 14, 13, 18, 17, 22, 25, 24, 28, 31, 29, 34],
  },
  {
    value: "downloads",
    label: "Downloads",
    unit: "K",
    data: [2.1, 2.4, 2.2, 3.1, 2.9, 3.8, 4.2, 4, 4.6, 5.1, 4.8, 5.6],
  },
  {
    value: "revenue",
    label: "Revenue",
    unit: "K $",
    data: [8, 9, 9, 11, 12, 14, 15, 15, 17, 19, 18, 21],
  },
  {
    value: "sessions",
    label: "Sessions",
    unit: "K",
    data: [40, 42, 41, 47, 48, 55, 58, 57, 62, 66, 64, 71],
  },
]

const regions = [
  { name: "United States", share: 0.46 },
  { name: "Germany", share: 0.21 },
  { name: "United Kingdom", share: 0.14 },
  { name: "Netherlands", share: 0.09 },
]

const chartConfig = {
  value: { label: "Value", color: "primary" },
} satisfies ChartConfig

function AppMenu({
  value,
  onValueChange,
}: {
  value: string
  onValueChange: (value: string) => void
}) {
  const current = apps.find((item) => item.value === value) ?? apps[0]
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        render={<Button variant="secondary" size="sm" />}
        accessibilityLabel={`App, ${current?.label}`}
      >
        {current ? <Icon as={current.icon} className="size-4" /> : null}
        <Text>{current?.label}</Text>
        <Icon as={CaretDownIcon} weight="bold" className="size-3" />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuRadioGroup
          value={value}
          onValueChange={(next) => {
            if (typeof next === "string") onValueChange(next)
          }}
        >
          {apps.map((item) => (
            <DropdownMenuRadioItem key={item.value} value={item.value}>
              <Icon
                as={item.icon}
                className="size-[1.125rem] text-foreground"
              />
              <Text>{item.label}</Text>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function OtherSection({ value }: { value: string }) {
  const label = SECTIONS.find((section) => section.value === value)?.label
  return (
    <View className="flex-1 gap-2 px-4 pt-[3.25rem]">
      <Text role="heading" className="text-3xl font-bold">
        {label}
      </Text>
      <Text className="text-lg text-muted-foreground">
        This section isn't part of the sample. Choose Analytics.
      </Text>
    </View>
  )
}

function DashAnalytics({
  onAppChange,
  onMetricChange,
  onNavigate,
}: {
  onAppChange?: (app: string) => void
  onMetricChange?: (metric: string) => void
  onNavigate?: (section: string) => void
}) {
  const [section, setSection] = React.useState("analytics")
  const [app, setApp] = React.useState("route")
  const [metricValue, setMetricValue] = React.useState("downloads")
  const [active, setActive] = React.useState<number | null>(null)
  const scroll = useHeaderScroll()
  const appIndex = Math.max(
    0,
    apps.findIndex((item) => item.value === app)
  )
  const metric =
    metrics.find((item) => item.value === metricValue) ?? metrics[1]
  if (!metric) return null
  const data = metric.data.map((value, index) => ({
    week: `Week ${index + 1}`,
    index,
    value: Number((value * (1 - appIndex * 0.28)).toFixed(2)),
  }))
  const current = active ?? data.length - 1
  const point = data[current]
  if (!point) return null
  const latest = data.at(-1)?.value ?? 0
  const format = (value: number) => `${value.toFixed(1)} ${metric.unit}`
  const title = apps[appIndex]?.label

  return (
    <Screen edges={["top", "right", "left"]}>
      {section === "analytics" ? (
        <>
          <Header scroll={scroll}>
            <HeaderTitle>{title}</HeaderTitle>
            <HeaderActions>
              <AppMenu
                value={app}
                onValueChange={(value) => {
                  setApp(value)
                  onAppChange?.(value)
                }}
              />
            </HeaderActions>
          </Header>
          <HeaderScrollView scroll={scroll}>
            <HeaderLargeTitle subtitle="Last 12 weeks · All regions">
              {title}
            </HeaderLargeTitle>
            <View className="gap-5 pt-3 pb-8">
              <ScrollView
                horizontal
                showsHorizontalScrollIndicator={false}
                className="grow-0"
                contentContainerClassName="px-4"
              >
                <ToggleGroup
                  spacing={0}
                  value={[metricValue]}
                  onValueChange={(value) => {
                    if (!value[0]) return
                    setMetricValue(value[0])
                    onMetricChange?.(value[0])
                  }}
                  accessibilityLabel="Metric"
                >
                  {metrics.map((item) => (
                    <ToggleGroupItem
                      key={item.value}
                      value={item.value}
                      size="sm"
                    >
                      <Text>{item.label}</Text>
                    </ToggleGroupItem>
                  ))}
                </ToggleGroup>
              </ScrollView>
              <View className="mx-4 rounded-xl bg-muted px-4 pt-3.5 pb-2.5">
                <View className="flex-row items-end gap-2.5">
                  <View
                    accessibilityLiveRegion="polite"
                    className="min-w-0 flex-1"
                  >
                    <Text className="text-sm text-muted-foreground">
                      {`${current === data.length - 1 ? "This week" : point.week} · ${metric.label}`}
                    </Text>
                    <Text className="text-3xl font-bold tracking-tight tabular-nums">
                      {format(point.value)}
                    </Text>
                  </View>
                  <Text className="text-sm font-semibold text-green-500">
                    {`+${12 + metrics.indexOf(metric) * 3}%`}
                  </Text>
                </View>
                <ChartContainer
                  config={chartConfig}
                  accessibilityLabel={metric.label}
                  className="mt-3 h-44 w-full"
                >
                  <AreaChart
                    data={data}
                    margin={{ top: 8, right: 6, left: 6, bottom: 0 }}
                    onActiveIndexChange={setActive}
                  >
                    <CartesianGrid vertical={false} strokeDasharray="2 4" />
                    <XAxis dataKey="week" hide />
                    <ChartTooltip
                      content={() => null}
                      cursor={{ stroke: "input" }}
                      defaultIndex={data.length - 1}
                    />
                    <Area
                      animationDuration={500}
                      dataKey="value"
                      type="linear"
                      stroke="primary"
                      strokeWidth={2.5}
                      fill={{
                        stops: [
                          { offset: 0, opacity: 0.22 },
                          { offset: 1, opacity: 0 },
                        ],
                      }}
                      activeDot={{
                        r: 5,
                        fill: "primary",
                        stroke: "muted",
                        strokeWidth: 2,
                      }}
                    />
                  </AreaChart>
                </ChartContainer>
              </View>
              <Text
                role="heading"
                className="-mb-2.5 px-5 text-sm font-semibold"
              >
                By region
              </Text>
              <View className="px-4">
                <ItemGroup variant="inset">
                  {regions.map((region, index) => (
                    <React.Fragment key={region.name}>
                      {index > 0 ? <ItemSeparator /> : null}
                      <Item size="sm">
                        <ItemContent>
                          <ItemTitle className="font-normal">
                            {region.name}
                          </ItemTitle>
                        </ItemContent>
                        <ItemActions>
                          <Text className="text-right tabular-nums">
                            {format(latest * region.share)}
                          </Text>
                        </ItemActions>
                      </Item>
                    </React.Fragment>
                  ))}
                </ItemGroup>
              </View>
            </View>
          </HeaderScrollView>
        </>
      ) : (
        <OtherSection value={section} />
      )}
      <TabBar
        value={section}
        onValueChange={(value) => {
          setSection(value)
          onNavigate?.(value)
        }}
      >
        <TabBarList>
          {SECTIONS.map((item) => (
            <TabBarItem key={item.value} value={item.value} icon={item.icon}>
              {item.label}
            </TabBarItem>
          ))}
        </TabBarList>
      </TabBar>
    </Screen>
  )
}

export { DashAnalytics }
```

4. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Chart](https://ui.suiss.app/docs/components/chart), [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/blocks/dash-analytics.tsx`:

```tsx title="stylesheet/components/blocks/dash-analytics.tsx"
import {
  CaretDownIcon,
  ChartBarIcon,
  FlameIcon,
  NotePencilIcon,
  type Icon as PhosphorIcon,
  StarIcon,
  TagIcon,
} from "phosphor-react-native"
import * as React from "react"
import { ScrollView, StyleSheet, View } from "react-native"
import { Button } from "@/components/ui/button"
import {
  Area,
  AreaChart,
  CartesianGrid,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  XAxis,
} from "@/components/ui/chart"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Header,
  HeaderActions,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"
import { fontSize, palette, radius, useTheme } from "@/components/ui/theme"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const apps: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "route", label: "Route", icon: ChartBarIcon },
  { value: "notebook", label: "Notebook", icon: NotePencilIcon },
  { value: "fitness", label: "Fitness+", icon: FlameIcon },
]

const SECTIONS: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "analytics", label: "Analytics", icon: ChartBarIcon },
  { value: "sales", label: "Sales and Trends", icon: TagIcon },
  { value: "ratings", label: "Ratings", icon: StarIcon },
]

const metrics = [
  {
    value: "impressions",
    label: "Impressions",
    unit: "K",
    data: [12, 14, 13, 18, 17, 22, 25, 24, 28, 31, 29, 34],
  },
  {
    value: "downloads",
    label: "Downloads",
    unit: "K",
    data: [2.1, 2.4, 2.2, 3.1, 2.9, 3.8, 4.2, 4, 4.6, 5.1, 4.8, 5.6],
  },
  {
    value: "revenue",
    label: "Revenue",
    unit: "K $",
    data: [8, 9, 9, 11, 12, 14, 15, 15, 17, 19, 18, 21],
  },
  {
    value: "sessions",
    label: "Sessions",
    unit: "K",
    data: [40, 42, 41, 47, 48, 55, 58, 57, 62, 66, 64, 71],
  },
]

const regions = [
  { name: "United States", share: 0.46 },
  { name: "Germany", share: 0.21 },
  { name: "United Kingdom", share: 0.14 },
  { name: "Netherlands", share: 0.09 },
]

const chartConfig = {
  value: { label: "Value", color: "primary" },
} satisfies ChartConfig

const styles = StyleSheet.create({
  other: { flex: 1, gap: 8, paddingHorizontal: 16, paddingTop: 52 },
  otherTitle: { ...fontSize["3xl"], fontWeight: "700" },
  body: { gap: 20, paddingTop: 12, paddingBottom: 32 },
  metrics: { paddingHorizontal: 16 },
  card: {
    marginHorizontal: 16,
    borderRadius: radius.xl,
    paddingHorizontal: 16,
    paddingTop: 14,
    paddingBottom: 10,
  },
  summary: { flexDirection: "row", alignItems: "flex-end", gap: 10 },
  reading: { minWidth: 0, flex: 1 },
  caption: { ...fontSize.sm },
  value: {
    ...fontSize["3xl"],
    fontWeight: "700",
    letterSpacing: -0.75,
    fontVariant: ["tabular-nums"],
  },
  change: { ...fontSize.sm, fontWeight: "600" },
  chart: { marginTop: 12, height: 176 },
  heading: {
    marginBottom: -10,
    paddingHorizontal: 20,
    ...fontSize.sm,
    fontWeight: "600",
  },
  group: { paddingHorizontal: 16 },
  amount: { textAlign: "right", fontVariant: ["tabular-nums"] },
})

function AppMenu({
  value,
  onValueChange,
}: {
  value: string
  onValueChange: (value: string) => void
}) {
  const { colors } = useTheme()
  const current = apps.find((item) => item.value === value) ?? apps[0]
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        render={<Button variant="secondary" size="sm" />}
        accessibilityLabel={`App, ${current?.label}`}
      >
        {current ? <Icon as={current.icon} size={16} /> : null}
        <Text>{current?.label}</Text>
        <Icon as={CaretDownIcon} weight="bold" size={12} />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuRadioGroup
          value={value}
          onValueChange={(next) => {
            if (typeof next === "string") onValueChange(next)
          }}
        >
          {apps.map((item) => (
            <DropdownMenuRadioItem key={item.value} value={item.value}>
              <Icon as={item.icon} size={18} color={colors.foreground} />
              <Text>{item.label}</Text>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function OtherSection({ value }: { value: string }) {
  const { colors } = useTheme()
  const label = SECTIONS.find((section) => section.value === value)?.label
  return (
    <View style={styles.other}>
      <Text role="heading" style={styles.otherTitle}>
        {label}
      </Text>
      <Text style={[fontSize.lg, { color: colors.mutedForeground }]}>
        This section isn't part of the sample. Choose Analytics.
      </Text>
    </View>
  )
}

function DashAnalytics({
  onAppChange,
  onMetricChange,
  onNavigate,
}: {
  onAppChange?: (app: string) => void
  onMetricChange?: (metric: string) => void
  onNavigate?: (section: string) => void
}) {
  const { colors } = useTheme()
  const [section, setSection] = React.useState("analytics")
  const [app, setApp] = React.useState("route")
  const [metricValue, setMetricValue] = React.useState("downloads")
  const [active, setActive] = React.useState<number | null>(null)
  const scroll = useHeaderScroll()
  const appIndex = Math.max(
    0,
    apps.findIndex((item) => item.value === app)
  )
  const metric =
    metrics.find((item) => item.value === metricValue) ?? metrics[1]
  if (!metric) return null
  const data = metric.data.map((value, index) => ({
    week: `Week ${index + 1}`,
    index,
    value: Number((value * (1 - appIndex * 0.28)).toFixed(2)),
  }))
  const current = active ?? data.length - 1
  const point = data[current]
  if (!point) return null
  const latest = data.at(-1)?.value ?? 0
  const format = (value: number) => `${value.toFixed(1)} ${metric.unit}`
  const title = apps[appIndex]?.label

  return (
    <Screen edges={["top", "right", "left"]}>
      {section === "analytics" ? (
        <>
          <Header scroll={scroll}>
            <HeaderTitle>{title}</HeaderTitle>
            <HeaderActions>
              <AppMenu
                value={app}
                onValueChange={(value) => {
                  setApp(value)
                  onAppChange?.(value)
                }}
              />
            </HeaderActions>
          </Header>
          <HeaderScrollView scroll={scroll}>
            <HeaderLargeTitle subtitle="Last 12 weeks · All regions">
              {title}
            </HeaderLargeTitle>
            <View style={styles.body}>
              <ScrollView
                horizontal
                showsHorizontalScrollIndicator={false}
                style={{ flexGrow: 0 }}
                contentContainerStyle={styles.metrics}
              >
                <ToggleGroup
                  spacing={0}
                  value={[metricValue]}
                  onValueChange={(value) => {
                    if (!value[0]) return
                    setMetricValue(value[0])
                    onMetricChange?.(value[0])
                  }}
                  accessibilityLabel="Metric"
                >
                  {metrics.map((item) => (
                    <ToggleGroupItem
                      key={item.value}
                      value={item.value}
                      size="sm"
                    >
                      <Text>{item.label}</Text>
                    </ToggleGroupItem>
                  ))}
                </ToggleGroup>
              </ScrollView>
              <View style={[styles.card, { backgroundColor: colors.muted }]}>
                <View style={styles.summary}>
                  <View accessibilityLiveRegion="polite" style={styles.reading}>
                    <Text
                      style={[
                        styles.caption,
                        { color: colors.mutedForeground },
                      ]}
                    >
                      {`${current === data.length - 1 ? "This week" : point.week} · ${metric.label}`}
                    </Text>
                    <Text style={styles.value}>{format(point.value)}</Text>
                  </View>
                  <Text style={[styles.change, { color: palette.green500 }]}>
                    {`+${12 + metrics.indexOf(metric) * 3}%`}
                  </Text>
                </View>
                <ChartContainer
                  config={chartConfig}
                  accessibilityLabel={metric.label}
                  style={styles.chart}
                >
                  <AreaChart
                    data={data}
                    margin={{ top: 8, right: 6, left: 6, bottom: 0 }}
                    onActiveIndexChange={setActive}
                  >
                    <CartesianGrid vertical={false} strokeDasharray="2 4" />
                    <XAxis dataKey="week" hide />
                    <ChartTooltip
                      content={() => null}
                      cursor={{ stroke: "input" }}
                      defaultIndex={data.length - 1}
                    />
                    <Area
                      animationDuration={500}
                      dataKey="value"
                      type="linear"
                      stroke="primary"
                      strokeWidth={2.5}
                      fill={{
                        stops: [
                          { offset: 0, opacity: 0.22 },
                          { offset: 1, opacity: 0 },
                        ],
                      }}
                      activeDot={{
                        r: 5,
                        fill: "primary",
                        stroke: "muted",
                        strokeWidth: 2,
                      }}
                    />
                  </AreaChart>
                </ChartContainer>
              </View>
              <Text role="heading" style={styles.heading}>
                By region
              </Text>
              <View style={styles.group}>
                <ItemGroup variant="inset">
                  {regions.map((region, index) => (
                    <React.Fragment key={region.name}>
                      {index > 0 ? <ItemSeparator /> : null}
                      <Item size="sm">
                        <ItemContent>
                          <ItemTitle style={{ fontWeight: "400" }}>
                            {region.name}
                          </ItemTitle>
                        </ItemContent>
                        <ItemActions>
                          <Text style={styles.amount}>
                            {format(latest * region.share)}
                          </Text>
                        </ItemActions>
                      </Item>
                    </React.Fragment>
                  ))}
                </ItemGroup>
              </View>
            </View>
          </HeaderScrollView>
        </>
      ) : (
        <OtherSection value={section} />
      )}
      <TabBar
        value={section}
        onValueChange={(value) => {
          setSection(value)
          onNavigate?.(value)
        }}
      >
        <TabBarList>
          {SECTIONS.map((item) => (
            <TabBarItem key={item.value} value={item.value} icon={item.icon}>
              {item.label}
            </TabBarItem>
          ))}
        </TabBarList>
      </TabBar>
    </Screen>
  )
}

export { DashAnalytics }
```

4. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

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

3. Copy the theme files every view uses:

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

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

4. Copy the source and every file it uses:

- `swift/Blocks/DashAnalytics.swift`: shown above

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

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

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

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

- `swift/Components/DropdownMenu.swift`: see [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu.md)

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

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

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

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

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

- `swift/Components/KeyboardScrollView.swift`: see [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view.md)

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

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

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

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

- `swift/Components/TabBar.swift`: see [Tab Bar](https://ui.suiss.app/docs/components/tab-bar.md)

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

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

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

5. Add the files to your app target.

**Jetpack Compose**

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

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

2. Copy the theme files every view uses:

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

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

3. Copy the source and every file it uses:

- `kotlin/blocks/DashAnalytics.kt`: shown above

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

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

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

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

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

- `kotlin/components/DropdownMenu.kt`: see [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu.md)

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

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

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

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

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

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

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

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

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

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

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

- `kotlin/components/TabBar.kt`: see [Tab Bar](https://ui.suiss.app/docs/components/tab-bar.md)

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

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

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

## Usage

```tsx
import { DashAnalytics } from "@/components/blocks/dash-analytics"
```

```tsx
<DashAnalytics />
```

Replace `apps`, `metrics` and `regions` with your analytics data. Pass `onAppChange`, `onMetricChange` and `onNavigate` to sync the selections with your app.

