# Chart

> Gráficos sobre Recharts con los tokens del sistema: paleta de cinco series validada para daltonismo, tooltip y leyenda propios, y animación que respeta `prefers-reduced-motion`.

```tsx
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, … } from "sebs7n-ui/chart"
```

## Ejemplos

### Área: una serie

Con una sola serie no hay leyenda: el título ya dice qué se está mirando. El relleno es la misma tinta al 10 %.

```tsx
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "sebs7n-ui/card"
import { ChartContainer, ChartTooltip, ChartTooltipContent, useChartMotion, type ChartConfig } from "sebs7n-ui/chart"

function Area1() {
  const motion = useChartMotion()
  const data = [186, 305, 237, 273, 209, 314].map((facturado, i) => ({ mes: MESES[i], facturado: facturado * 1000 }))
  const config = { facturado: { label: "Facturado" } } satisfies ChartConfig
  return (
    <Card className="w-full max-w-lg" size="sm">
      <CardHeader>
        <CardTitle>Facturado por mes</CardTitle>
        <CardDescription>Primer semestre, en pesos.</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer className="aspect-[2/1]" config={config}>
          <AreaChart data={data} margin={{ left: 4, right: 4, top: 8 }} responsive>
            <CartesianGrid vertical={false} />
            <XAxis axisLine={false} dataKey="mes" tickLine={false} tickMargin={8} />
            <ChartTooltip content={<ChartTooltipContent formatter={pesos} indicator="line" />} cursor />
            <Area
              dataKey="facturado"
              fill="var(--color-facturado)"
              fillOpacity={0.1}
              stroke="var(--color-facturado)"
              strokeWidth={2}
              type="monotone"
              {...motion}
            />
          </AreaChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}
```

### Barras: dos series

Con dos o más series la leyenda va siempre. Las barras miden 24px como máximo y se redondean solo en la punta.

```tsx
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, useChartMotion, type ChartConfig } from "sebs7n-ui/chart"

function Barras() {
  const motion = useChartMotion()
  const data = [
    { mes: "Ene", facturado: 186, cobrado: 150 },
    { mes: "Feb", facturado: 305, cobrado: 280 },
    { mes: "Mar", facturado: 237, cobrado: 190 },
    { mes: "Abr", facturado: 273, cobrado: 260 },
    { mes: "May", facturado: 209, cobrado: 205 },
    { mes: "Jun", facturado: 314, cobrado: 240 },
  ]
  const config = { facturado: { label: "Facturado" }, cobrado: { label: "Cobrado" } } satisfies ChartConfig
  return (
    <ChartContainer className="aspect-[2/1] max-w-lg" config={config}>
      <BarChart barGap={2} data={data} margin={{ left: 4, right: 4, top: 8 }} responsive>
        <CartesianGrid vertical={false} />
        <XAxis axisLine={false} dataKey="mes" tickLine={false} tickMargin={8} />
        <YAxis axisLine={false} tickLine={false} width={32} />
        <ChartTooltip content={<ChartTooltipContent />} cursor />
        <ChartLegend content={<ChartLegendContent />} />
        <Bar dataKey="facturado" fill="var(--color-facturado)" maxBarSize={24} radius={[4, 4, 0, 0]} {...motion} />
        <Bar dataKey="cobrado" fill="var(--color-cobrado)" maxBarSize={24} radius={[4, 4, 0, 0]} {...motion} />
      </BarChart>
    </ChartContainer>
  )
}
```

### Donut con el total en el centro

Un hueco de 2px del color de la superficie separa los sectores, y cada uno lleva las puntas apenas redondeadas. El número grande va en tinta, no en el color de un sector.

```tsx
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, useChartMotion, type ChartConfig } from "sebs7n-ui/chart"
import { Label, Pie, PieChart } from "recharts"

function Donut() {
  const motion = useChartMotion()
  const data = [
    { canal: "web", monto: 412 },
    { canal: "presencial", monto: 268 },
    { canal: "telefono", monto: 121 },
    { canal: "otros", monto: 44 },
  ]
  const total = data.reduce((suma, fila) => suma + fila.monto, 0)
  const config = {
    web: { label: "Web" },
    presencial: { label: "Presencial" },
    telefono: { label: "Teléfono" },
    otros: { label: "Otros" },
  } satisfies ChartConfig
  // `aspect-square` solo: con `max-h` el alto se frenaba pero el ancho seguía al contenedor y el
  // donut se salía de la card. El tamaño lo fija `max-w-72`, y el alto lo sigue.
  return (
    <ChartContainer className="mx-auto aspect-square w-full max-w-72" config={config}>
      <PieChart responsive>
        <ChartTooltip content={<ChartTooltipContent nameKey="canal" />} />
        <ChartLegend content={<ChartLegendContent nameKey="canal" />} />
        <Pie
          data={data.map((fila) => ({ ...fila, fill: `var(--color-${fila.canal})` }))}
          dataKey="monto"
          cornerRadius={4}
          innerRadius="62%"
          nameKey="canal"
          paddingAngle={2}
          stroke="var(--sf-background-100)"
          strokeWidth={2}
          {...motion}
        >
          <Label
            content={({ viewBox }) => {
              if (!viewBox || !("cx" in viewBox) || !("cy" in viewBox)) return null
              return (
                <text dominantBaseline="middle" textAnchor="middle" x={viewBox.cx} y={viewBox.cy}>
                  <tspan className="fill-gray-1000 text-title-2 tabular-nums" x={viewBox.cx} y={viewBox.cy}>
                    {total}
                  </tspan>
                  <tspan className="fill-label-secondary text-footnote" x={viewBox.cx} y={(viewBox.cy ?? 0) + 22}>
                    ventas
                  </tspan>
                </text>
              )
            }}
          />
        </Pie>
      </PieChart>
    </ChartContainer>
  )
}
```

### Cambio de datos

Al cambiar el rango, la línea se transforma en la nueva en vez de redibujarse: Recharts interpola entre los dos datasets. La tabla es la misma información, para quien no ve el gráfico.

```tsx
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, useChartMotion, type ChartConfig } from "sebs7n-ui/chart"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "sebs7n-ui/table"
import { ToggleGroup, ToggleGroupItem } from "sebs7n-ui/toggle-group"
import { useState } from "react"

function CambioDeDatos() {
  const motion = useChartMotion()
  const [rango, setRango] = useState<keyof typeof RANGOS>("7d")
  const [vista, setVista] = useState<"grafico" | "tabla">("grafico")
  const data = RANGOS[rango].map((altas, i) => ({ dia: `D${i + 1}`, altas }))
  const config = { altas: { label: "Altas" } } satisfies ChartConfig
  return (
    <div className="flex w-full max-w-lg flex-col gap-4">
      <div className="flex flex-wrap items-center justify-between gap-2">
        <ToggleGroup aria-label="Rango" onValueChange={(v) => v[0] && setRango(v[0] as keyof typeof RANGOS)} value={[rango]}>
          {(Object.keys(RANGOS) as (keyof typeof RANGOS)[]).map((clave) => (
            <ToggleGroupItem key={clave} value={clave}>
              {clave}
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
        <ToggleGroup aria-label="Vista" onValueChange={(v) => v[0] && setVista(v[0] as typeof vista)} value={[vista]}>
          <ToggleGroupItem value="grafico">Gráfico</ToggleGroupItem>
          <ToggleGroupItem value="tabla">Tabla</ToggleGroupItem>
        </ToggleGroup>
      </div>
      {vista === "grafico" ? (
        <ChartContainer className="aspect-[2/1]" config={config}>
          <LineChart data={data} margin={{ left: 4, right: 4, top: 8 }} responsive>
            <CartesianGrid vertical={false} />
            <XAxis axisLine={false} dataKey="dia" tickLine={false} tickMargin={8} />
            <ChartTooltip content={<ChartTooltipContent hideIndicator />} cursor />
            <Line
              activeDot={{ r: 5, strokeWidth: 2 }}
              dataKey="altas"
              dot={false}
              stroke="var(--color-altas)"
              strokeWidth={2}
              type="monotone"
              {...motion}
            />
          </LineChart>
        </ChartContainer>
      ) : (
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>Día</TableHead>
              <TableHead className="text-right">Altas</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {data.map((fila) => (
              <TableRow key={fila.dia}>
                <TableCell>{fila.dia}</TableCell>
                <TableCell className="text-right tabular-nums">{fila.altas}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      )}
    </div>
  )
}
```

### Sparkline en un KPI

Sin ejes, sin grilla, sin tooltip: doce puntos que dan la forma de la tendencia debajo del número. El color es el de la serie única.

```tsx
import { Card, CardContent } from "sebs7n-ui/card"
import { ChartContainer, useChartMotion } from "sebs7n-ui/chart"
import { Line, LineChart } from "recharts"
import { Stat } from "sebs7n-ui/stat"

function Sparkline() {
  const motion = useChartMotion()
  const kpis = [
    { label: "Facturado", value: "$ 1.284.000", delta: "+12,4 %", trend: "up", serie: [4, 6, 5, 8, 7, 9, 8, 11, 10, 12, 13, 15] },
    { label: "Tickets abiertos", value: "37", delta: "+8", trend: "down", serie: [12, 10, 14, 13, 18, 17, 21, 20, 26, 29, 33, 37] },
    { label: "Tiempo de respuesta", value: "1,8 h", delta: "−0,4 h", trend: "up", serie: [3.1, 2.9, 3, 2.6, 2.4, 2.5, 2.2, 2.1, 2, 1.9, 1.9, 1.8] },
  ] as const
  return (
    <div className="grid w-full gap-4 sm:grid-cols-3">
      {kpis.map((kpi) => (
        <Card key={kpi.label} size="sm">
          <CardContent className="flex flex-col gap-3">
            <Stat delta={kpi.delta} hint="vs. mes anterior" label={kpi.label} trend={kpi.trend} value={kpi.value} />
            <ChartContainer aria-hidden="true" className="aspect-auto h-10" config={{ v: { label: kpi.label } }}>
              <LineChart data={kpi.serie.map((v, i) => ({ i, v }))} margin={{ top: 2, bottom: 2, left: 2, right: 2 }} responsive>
                <Line dataKey="v" dot={false} stroke="var(--color-v)" strokeWidth={2} type="monotone" {...motion} />
              </LineChart>
            </ChartContainer>
          </CardContent>
        </Card>
      ))}
    </div>
  )
}
```

## Props

### ChartContainer

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `config` * | `ChartConfig` | — | Las series, por `dataKey`: etiqueta, color opcional e ícono opcional para la leyenda. El orden de las claves es el orden de la paleta. |
| `style` | `CSSProperties` | — | Se fusiona con las variables `--color-<serie>` que pone el contenedor. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

`*` obligatoria.

### ChartLegend

Sin props propias: pasa todo al primitivo.

### ChartLegendContent

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `hideIcon` | `boolean` | `false` | Ignora el `icon` de la config y muestra el punto de color. |
| `nameKey` | `string` | — | Igual que en el tooltip: la clave que identifica la serie. |
| `payload` | `readonly LegendPayload[]` | — | Lo pone Recharts. |
| `verticalAlign` | `"top" \| "bottom" \| "middle"` | `"bottom"` | Lo pone Recharts desde `<Legend verticalAlign>`: solo cambia de qué lado va el margen. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ChartTooltip

Sin props propias: pasa todo al primitivo.

### ChartTooltipContent

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `active` | `boolean` | — | Lo pone Recharts: si el puntero está sobre un punto. |
| `formatter` | `(value: number \| string, name: string, entry: TooltipPayloadEntry) => React.ReactNode` | — | Cómo se escribe cada valor: `(value) => `$ ${value}``. |
| `hideIndicator` | `boolean` | `false` | Oculta el testigo de color. |
| `hideLabel` | `boolean` | `false` | Oculta el título del tooltip (el valor del eje X). |
| `indicator` | `"line" \| "none" \| "dot"` | `"dot"` | `dot` (un punto) · `line` (una barra vertical) · `none`. |
| `label` | `React.ReactNode` | — | Lo pone Recharts: el valor del eje X bajo el puntero. |
| `labelFormatter` | `(label: React.ReactNode, payload: readonly TooltipPayloadEntry[]) => React.ReactNode` | — | Cómo se escribe el título. |
| `labelKey` | `string` | — | Clave del payload que da el título. Por defecto, el `label` del eje. |
| `nameKey` | `string` | — | Clave del payload que identifica la serie, para un `Pie` donde cada sector es una fila. |
| `payload` | `readonly TooltipPayloadEntry[]` | — | Lo pone Recharts: las series bajo el puntero. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

## Teclado

| Tecla | Qué hace |
|---|---|
| Tab | Entra al gráfico (`tabIndex=0` de Recharts, con el anillo de foco del sistema). |
| ← → | Recorre los puntos y muestra el tooltip de cada uno (`accessibilityLayer` de Recharts, activo por defecto). |

## Accesibilidad

- La paleta es de **cinco** series en orden fijo (`chart-1`…`chart-5`, también como `bg-chart-1` para un testigo propio): pares adyacentes con ΔE ≥ 8 en deutan, protan y tritan, y 3:1 contra la superficie en los dos temas. Es lo que pasa el validador; una sexta serie se agrupa en «Otros» o se parte en otro gráfico.
- **El texto nunca lleva el color de la serie.** Tooltip y leyenda escriben el nombre en `gray-900` y el valor en `gray-1000`; el color va en el testigo de al lado.
- Con dos series o más la leyenda es obligatoria: la identidad no puede depender del color solo. Con una, el título ya la nombra.
- `useChartMotion()` devuelve `isAnimationActive` y `animationDuration` según `prefers-reduced-motion`: Recharts no lo mira solo, hay que pasárselo a cada serie.
- Un gráfico no es la única forma de leer un dato: ofrecé la tabla (ver el ejemplo *Cambio de datos*). Un sparkline decorativo va con `aria-hidden`.

## Reglas de uso

- **`recharts` es un peer opcional:** `npm install recharts` en la app que lo usa. Ningún otro subpath del paquete lo importa, y `Chart` no está en el barrel: solo `sebs7n-ui/chart`.
- El gráfico lleva `responsive` (`<AreaChart responsive>`): mide con CSS y llena el `ChartContainer`, que es el que decide el tamaño (`aspect-video` por defecto).
- Cada serie pinta con su variable: `fill="var(--color-<dataKey>)"`. La pone `ChartContainer` a partir de la `config`, en el orden en que está escrita, y no se reasigna si una serie se filtra.
- Marcas finas: barras de 24px como máximo (`maxBarSize`) redondeadas en la punta (`radius={[4, 4, 0, 0]}`), líneas de 2px, relleno de área al 10 %. Grilla horizontal sola (`vertical={false}`), sin línea de eje.
- **Un solo eje Y.** Dos medidas de escala distinta son dos gráficos, no un eje a cada lado.
- Una serie y una pregunta por gráfico. Un dashboard es varios gráficos chicos, no uno con todo.

## Relacionados

[stat](/docs/components/stat.md) · [card](/docs/components/card.md) · [table](/docs/components/table.md)
