# Chart

## Overview

Xertica UI provides a `ChartContainer` wrapper and related components built on top of **Recharts** for theme-aware, token-driven chart rendering. The chart system uses a `ChartConfig` object to define series labels and colors via CSS variables, ensuring full dark-mode support without hard-coded hex values.

The library ships **11 dashboard-ready chart wrappers** that handle loading, empty, and error states automatically, so feature layers only need to pass `data`, `isLoading`, `error`, and `onRetry`.

---

## Exports

| Export                       | Description                                                                     |
| ---------------------------- | ------------------------------------------------------------------------------- |
| `ChartContainer`             | Root container that injects CSS color variables and wraps `ResponsiveContainer` |
| `ChartTooltip`               | Re-exported `recharts` `Tooltip` component                                      |
| `ChartTooltipContent`        | Styled tooltip content using the design system                                  |
| `ChartLegend`                | Re-exported `recharts` `Legend` component                                       |
| `ChartLegendContent`         | Styled legend content using the design system                                   |
| `ChartStyle`                 | Internal style injector (used by `ChartContainer`, not used directly)           |
| `ChartCard`                  | Card-based shell for dashboard chart panels                                     |
| `DashboardBarChart`          | Ready-to-use grouped or stacked bar chart                                       |
| `DashboardLineChart`         | Ready-to-use multi-series line chart                                            |
| `HorizontalBarChart`         | Ranking/comparison bar chart with horizontal layout                             |
| `InteractiveTimeSeriesChart` | Area time-series chart with metric tabs and period select                       |
| `ComboMetricChart`           | Composed bar, line, and area chart for mixed metrics                            |
| `DonutBreakdownChart`        | Donut/pie breakdown chart with interactive segment focus                        |
| `SparklineChart`             | Compact inline area/line chart for KPI cards                                    |
| `RadarMetricChart`           | Multi-dimensional radar/spider chart                                            |
| `PieMetricChart`             | Proportional pie chart with optional donut, labels, and exploded slice          |
| `RadialBarMetricChart`       | Circular progress rings (radial bar chart)                                      |
| `GaugeChart`                 | Semicircle gauge with needle, thresholds, and color zones                       |
| `ChartConfig`                | TypeScript type for chart configuration                                         |
| `GaugeChartThreshold`        | TypeScript type for gauge color zone thresholds                                 |

---

## Required Dependencies

```bash
npm install recharts
```

---

## How It Works

1. Define a `ChartConfig` mapping each data key to a label and color.
2. Wrap your Recharts chart in `<ChartContainer config={config}>`.
3. Reference colors in chart elements using `fill="var(--color-yourKey)"`.
4. The container automatically injects CSS variables for light and dark themes.

Dashboard-ready wrappers (`DashboardBarChart`, `RadarMetricChart`, etc.) build the config internally — you only need to pass `data` and `series`/`nameKey`/`valueKey`.

---

## ChartConfig Type

```typescript
type ChartConfig = {
  [key: string]: {
    label?: string;
    icon?: React.ComponentType;
    color?: string; // CSS color or hsl() value
    theme?: { light: string; dark: string }; // Per-theme colors
  };
};
```

---

## Color Tokens

Chart colors use `--chart-1` through `--chart-8` CSS tokens. These are theme-aware and automatically switch between light and dark mode.

```tsx
// In ChartConfig
{ revenue: { label: 'Revenue', color: 'var(--chart-1)' } }

// In dashboard-ready wrappers — pass as array or per-key map
colors={['var(--chart-1)', 'var(--chart-2)']}
colors={{ revenue: 'var(--chart-1)', expenses: 'var(--chart-5)' }}
```

---

## Examples

### Bar Chart (low-level)

```tsx
import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
  type ChartConfig,
} from 'xertica-ui/ui';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid } from 'recharts';
import { Card, CardHeader, CardTitle, CardContent } from 'xertica-ui/ui';

const chartConfig: ChartConfig = {
  revenue: { label: 'Revenue', color: 'var(--chart-1)' },
  expenses: { label: 'Expenses', color: 'var(--chart-5)' },
};

const data = [
  { month: 'Jan', revenue: 4000, expenses: 2400 },
  { month: 'Feb', revenue: 3000, expenses: 1398 },
  { month: 'Mar', revenue: 6000, expenses: 3200 },
];

<Card>
  <CardHeader>
    <CardTitle>Revenue vs Expenses</CardTitle>
  </CardHeader>
  <CardContent>
    <ChartContainer config={chartConfig} className="h-[300px]">
      <BarChart data={data}>
        <CartesianGrid vertical={false} />
        <XAxis dataKey="month" tickLine={false} axisLine={false} />
        <YAxis tickLine={false} axisLine={false} />
        <ChartTooltip content={<ChartTooltipContent />} />
        <ChartLegend content={<ChartLegendContent />} />
        <Bar dataKey="revenue" fill="var(--color-revenue)" radius={4} />
        <Bar dataKey="expenses" fill="var(--color-expenses)" radius={4} />
      </BarChart>
    </ChartContainer>
  </CardContent>
</Card>;
```

### Line Chart (low-level)

```tsx
const chartConfig: ChartConfig = {
  users: { label: 'Active Users', color: 'var(--chart-1)' },
};

<ChartContainer config={chartConfig} className="h-[250px]">
  <LineChart data={data}>
    <CartesianGrid vertical={false} />
    <XAxis dataKey="month" />
    <YAxis />
    <ChartTooltip content={<ChartTooltipContent />} />
    <Line type="monotone" dataKey="users" stroke="var(--color-users)" strokeWidth={2} dot={false} />
  </LineChart>
</ChartContainer>;
```

---

### Dashboard-Ready Charts

```tsx
import {
  ChartCard,
  DashboardBarChart,
  DashboardLineChart,
  HorizontalBarChart,
  InteractiveTimeSeriesChart,
  ComboMetricChart,
  DonutBreakdownChart,
  type ChartConfig,
} from 'xertica-ui/ui';

const config: ChartConfig = {
  revenue: { label: 'Revenue', color: 'var(--chart-1)' },
  pipeline: { label: 'Pipeline', color: 'var(--chart-4)' },
  conversion: { label: 'Conversion', color: 'var(--chart-2)' },
};

const data = [
  { date: 'Apr 01', revenue: 4200, pipeline: 2600, conversion: 32 },
  { date: 'Apr 08', revenue: 5200, pipeline: 3200, conversion: 38 },
];

<ChartCard title="Executive Trend" description="Metric and period controls">
  <InteractiveTimeSeriesChart
    data={data}
    indexKey="date"
    config={config}
    series={[
      { key: 'revenue', label: 'Revenue' },
      { key: 'pipeline', label: 'Pipeline' },
    ]}
  />
</ChartCard>

<ChartCard title="Customer Mix">
  <DonutBreakdownChart data={segments} config={segmentConfig} />
</ChartCard>

<DashboardBarChart data={data} indexKey="date" config={config} stacked />

<HorizontalBarChart
  data={[
    { segment: 'Enterprise', revenue: 7600 },
    { segment: 'Mid-market', revenue: 5400 },
  ]}
  indexKey="segment"
  config={{
    revenue: { label: 'Revenue', color: 'var(--chart-1)' },
  }}
  showLegend={false}
/>

<ComboMetricChart
  data={data}
  indexKey="date"
  config={config}
  series={[
    { key: 'pipeline', type: 'bar' },
    { key: 'conversion', type: 'line' },
  ]}
/>
```

---

### Radar Chart

Multi-dimensional comparison across categorical axes. Use a single series for a simple spider chart, or multiple series for side-by-side comparison.

```tsx
import { RadarMetricChart, ChartCard } from 'xertica-ui/ui';

const data = [
  { skill: 'Speed',    frontend: 80, backend: 60 },
  { skill: 'Quality',  frontend: 90, backend: 85 },
  { skill: 'Coverage', frontend: 70, backend: 95 },
  { skill: 'Security', frontend: 65, backend: 88 },
  { skill: 'Perf',     frontend: 75, backend: 72 },
];

// Single series — filled polygon
<ChartCard title="Frontend Skills">
  <RadarMetricChart
    data={data}
    labelKey="skill"
    series={[{ key: 'frontend', label: 'Frontend' }]}
    filled
    fillOpacity={0.3}
  />
</ChartCard>

// Multi-series comparison
<ChartCard title="Team Comparison">
  <RadarMetricChart
    data={data}
    labelKey="skill"
    series={[
      { key: 'frontend', label: 'Frontend' },
      { key: 'backend',  label: 'Backend' },
    ]}
    filled={false}
    showDots
    showLegend
  />
</ChartCard>
```

**Props:**

| Prop             | Type                     | Default | Description                                   |
| ---------------- | ------------------------ | ------- | --------------------------------------------- |
| `data`           | `DashboardChartDatum[]`  | —       | Data array; each item is one axis point       |
| `labelKey`       | `string`                 | —       | Key used as the axis label                    |
| `series`         | `DashboardChartSeries[]` | —       | Series to render (one `<Radar>` per entry)    |
| `colors`         | `DashboardChartColors`   | auto    | Override colors per key or as ordered array   |
| `filled`         | `boolean`                | `true`  | Fill the radar polygon                        |
| `fillOpacity`    | `number`                 | `0.25`  | Fill opacity when `filled` is true            |
| `showDots`       | `boolean`                | `false` | Show dots on each axis point                  |
| `showGrid`       | `boolean`                | `true`  | Show polar grid lines                         |
| `showLegend`     | `boolean`                | auto    | Show legend (auto-shown when multiple series) |
| `valueFormatter` | `(v: number) => string`  | —       | Format axis tick and tooltip values           |
| + state props    | —                        | —       | `isLoading`, `error`, `onRetry`, etc.         |

---

### Pie Chart

Proportional slices. Supports donut mode (`innerRadius > 0`), percentage labels, and an exploded (offset) slice for emphasis.

```tsx
import { PieMetricChart, ChartCard } from 'xertica-ui/ui';

const data = [
  { segment: 'Enterprise', value: 52 },
  { segment: 'Mid-market', value: 31 },
  { segment: 'SMB',        value: 17 },
];

// Simple pie
<ChartCard title="Revenue Mix">
  <PieMetricChart
    data={data}
    nameKey="segment"
    valueKey="value"
    showLabels
  />
</ChartCard>

// Donut variant
<ChartCard title="Revenue Mix (Donut)">
  <PieMetricChart
    data={data}
    nameKey="segment"
    valueKey="value"
    innerRadius="55%"
    outerRadius="80%"
  />
</ChartCard>

// Exploded slice (highlight index 0)
<ChartCard title="Top Segment">
  <PieMetricChart
    data={data}
    nameKey="segment"
    valueKey="value"
    explodeIndex={0}
    explodeOffset={14}
  />
</ChartCard>
```

**Props:**

| Prop             | Type                    | Default | Description                                      |
| ---------------- | ----------------------- | ------- | ------------------------------------------------ |
| `data`           | `DashboardChartDatum[]` | —       | Data array; each item is one slice               |
| `nameKey`        | `string`                | —       | Key used as the slice name/label                 |
| `valueKey`       | `string`                | —       | Key used as the slice value                      |
| `colors`         | `DashboardChartColors`  | auto    | Override colors as ordered array or per-name map |
| `outerRadius`    | `number \| string`      | `"80%"` | Outer radius of the pie                          |
| `innerRadius`    | `number \| string`      | `0`     | Inner radius — set > 0 for donut                 |
| `showLabels`     | `boolean`               | `false` | Show percentage labels on slices                 |
| `showLegend`     | `boolean`               | `true`  | Show the legend                                  |
| `explodeIndex`   | `number`                | —       | Index of the slice to offset outward             |
| `explodeOffset`  | `number`                | `12`    | Offset distance in px for the exploded slice     |
| `valueFormatter` | `(v: number) => string` | —       | Format tooltip values                            |
| + state props    | —                       | —       | `isLoading`, `error`, `onRetry`, etc.            |

---

### Radial Bar Chart

Circular progress rings. Each data item renders as a concentric arc. Useful for showing multiple metrics as percentage completion.

```tsx
import { RadialBarMetricChart, ChartCard } from 'xertica-ui/ui';

const data = [
  { name: 'CPU',     value: 72 },
  { name: 'Memory',  value: 58 },
  { name: 'Storage', value: 41 },
  { name: 'Network', value: 89 },
];

// Full-circle rings (default)
<ChartCard title="Resource Usage">
  <RadialBarMetricChart
    data={data}
    dataKey="value"
    nameKey="name"
    valueFormatter={(v) => `${v}%`}
  />
</ChartCard>

// Semi-circle layout
<ChartCard title="Resource Usage (Semi)">
  <RadialBarMetricChart
    data={data}
    dataKey="value"
    nameKey="name"
    startAngle={180}
    endAngle={0}
    innerRadius="20%"
    outerRadius="90%"
  />
</ChartCard>
```

**Props:**

| Prop             | Type                    | Default   | Description                                      |
| ---------------- | ----------------------- | --------- | ------------------------------------------------ |
| `data`           | `DashboardChartDatum[]` | —         | Data array; each item is one ring                |
| `dataKey`        | `string`                | `"value"` | Key used as the bar value                        |
| `nameKey`        | `string`                | `"name"`  | Key used as the bar label                        |
| `colors`         | `DashboardChartColors`  | auto      | Override colors as ordered array or per-name map |
| `innerRadius`    | `number \| string`      | `"30%"`   | Inner radius of the radial bar                   |
| `outerRadius`    | `number \| string`      | `"100%"`  | Outer radius of the radial bar                   |
| `startAngle`     | `number`                | `90`      | Start angle in degrees (90 = top)                |
| `endAngle`       | `number`                | `-270`    | End angle in degrees (-270 = full circle)        |
| `showBackground` | `boolean`               | `true`    | Show background track behind each bar            |
| `showLegend`     | `boolean`               | `true`    | Show the legend below the chart                  |
| `valueFormatter` | `(v: number) => string` | —         | Format tooltip and legend values                 |
| + state props    | —                       | —         | `isLoading`, `error`, `onRetry`, etc.            |

---

### Gauge Chart

Semicircle gauge with a needle indicator and optional color zones (thresholds). Implemented as pure SVG — no Recharts dependency.

```tsx
import { GaugeChart, ChartCard, type GaugeChartThreshold } from 'xertica-ui/ui';

// Simple percentage gauge
<ChartCard title="CPU Usage">
  <div className="flex justify-center py-4">
    <GaugeChart value={72} label="CPU" />
  </div>
</ChartCard>

// With color thresholds
const thresholds: GaugeChartThreshold[] = [
  { value: 40,  color: 'var(--chart-2)', label: 'Healthy' },
  { value: 70,  color: 'var(--chart-3)', label: 'Warning' },
  { value: 100, color: 'var(--chart-5)', label: 'Critical' },
];

<ChartCard title="Memory Usage">
  <div className="flex justify-center py-4">
    <GaugeChart
      value={85}
      label="Memory"
      thresholds={thresholds}
    />
  </div>
</ChartCard>

// Custom range and formatter
<GaugeChart
  value={1250}
  min={0}
  max={2000}
  label="Requests/s"
  valueFormatter={(value) => `${value.toLocaleString()} rps`}
  showNeedle={false}
/>
```

**Props:**

| Prop             | Type                                         | Default | Description                                                                |
| ---------------- | -------------------------------------------- | ------- | -------------------------------------------------------------------------- |
| `value`          | `number`                                     | —       | Current value (must be within `[min, max]`)                                |
| `min`            | `number`                                     | `0`     | Minimum value                                                              |
| `max`            | `number`                                     | `100`   | Maximum value                                                              |
| `thresholds`     | `GaugeChartThreshold[]`                      | —       | Color zones evaluated in order; first zone where `value >= current %` wins |
| `label`          | `React.ReactNode`                            | —       | Label shown below the center value                                         |
| `valueFormatter` | `(value: number, percent: number) => string` | —       | Format the center value text (default: shows `%`)                          |
| `showNeedle`     | `boolean`                                    | `true`  | Show the needle indicator                                                  |
| `className`      | `string`                                     | —       | Additional CSS classes                                                     |

**`GaugeChartThreshold` type:**

```typescript
interface GaugeChartThreshold {
  value: number; // Upper bound of this zone (0–100)
  color: string; // Color for this zone (use CSS var tokens)
  label?: string; // Optional label shown in the legend
}
```

**Infrastructure dashboard example:**

```tsx
const metrics = [
  { label: 'CPU', value: 72 },
  { label: 'Memory', value: 85 },
  { label: 'Storage', value: 41 },
];

const thresholds: GaugeChartThreshold[] = [
  { value: 60, color: 'var(--chart-2)', label: 'OK' },
  { value: 80, color: 'var(--chart-3)', label: 'Warning' },
  { value: 100, color: 'var(--chart-5)', label: 'Critical' },
];

<div className="grid grid-cols-3 gap-4">
  {metrics.map(m => (
    <ChartCard key={m.label} title={m.label}>
      <div className="flex justify-center py-2">
        <GaugeChart value={m.value} label={m.label} thresholds={thresholds} />
      </div>
    </ChartCard>
  ))}
</div>;
```

---

### Sparkline Chart

Compact inline chart for KPI cards and metric summaries. Renders as a minimal area or line chart with no axes or labels.

```tsx
import { SparklineChart } from 'xertica-ui/ui';

// Filled area sparkline (default)
<SparklineChart
  data={[{ v: 10 }, { v: 25 }, { v: 18 }, { v: 40 }, { v: 35 }]}
  dataKey="v"
  color="var(--chart-1)"
  filled
/>

// Line-only sparkline
<SparklineChart
  data={weeklyData}
  dataKey="sessions"
  color="var(--chart-2)"
  filled={false}
/>
```

---

### Empty and Connection States

All dashboard-ready chart wrappers handle loading, empty data, and connection errors without forcing each feature to duplicate presentation logic. In an FSD architecture, keep fetching and error mapping in the feature/model layer, then pass the state into the UI component.

```tsx
<DashboardLineChart
  data={analyticsData}
  config={config}
  isLoading={isLoading}
  error={connectionError}
  onRetry={refetchAnalytics}
  emptyTitle="No data available"
  emptyDescription="There is no data available for this period."
  errorTitle="Connection error"
  errorDescription="Unable to load analytics data."
/>

// State props work on all dashboard-ready wrappers:
<RadarMetricChart
  data={skillData}
  labelKey="skill"
  series={[{ key: 'score', label: 'Score' }]}
  isLoading={isLoading}
  error={error}
  onRetry={refetch}
/>
```

State props (`isLoading`, `error`, `onRetry`, `retryLabel`, `emptyTitle`, `emptyDescription`, `errorTitle`, `errorDescription`, `loadingLabel`, `stateClassName`) are available on: `DashboardBarChart`, `DashboardLineChart`, `HorizontalBarChart`, `InteractiveTimeSeriesChart`, `ComboMetricChart`, `DonutBreakdownChart`, `RadarMetricChart`, `PieMetricChart`, and `RadialBarMetricChart`.

> `GaugeChart` does not extend `ChartStateProps` — it is a pure display component with no async state.

---

### Stacked Bar Chart

Pass `stacked` to `DashboardBarChart` to stack series on top of each other. When stacking, only the **topmost series** in the stack receives rounded top corners — intermediate and bottom bars have flat tops to avoid visual gaps between segments.

```tsx
const config: ChartConfig = {
  receita: { label: 'Receita', color: 'var(--chart-1)' },
  despesa: { label: 'Despesa', color: 'var(--chart-5)' },
};

<DashboardBarChart
  data={data}
  indexKey="month"
  config={config}
  series={[
    { key: 'receita', label: 'Receita' },
    { key: 'despesa', label: 'Despesa' }, // ← this series sits on top, gets radius
  ]}
  stacked
/>;
```

> The radius logic is automatic — the last `series` entry in each `stackId` group receives `radius={[4,4,0,0]}`; all others receive `radius={[0,0,0,0]}`. You do not need to configure this manually.

> **v2.1.9**: The `topOfStack` computation was extracted from an inline IIFE in JSX into a `React.useMemo([stacked, chartSeries])` before the `return`. This is a performance improvement — the bar element array is no longer recalculated on every render when the chart re-renders for unrelated reasons.

---

### Bar Thickness

Bar-based dashboard charts support `barSize="sm" | "md" | "lg" | "xl"` or a numeric pixel value. This controls the thickness of each rendered bar, not the chart container width.

```tsx
<DashboardBarChart data={data} indexKey="date" config={config} barSize="lg" />

<HorizontalBarChart
  data={segmentData}
  indexKey="segment"
  config={config}
  barSize={28}
/>

<ComboMetricChart
  data={data}
  indexKey="date"
  config={config}
  series={[
    { key: 'pipeline', type: 'bar' },
    { key: 'conversion', type: 'line' },
  ]}
  barSize="sm"
/>
```

`barSize` is available on `DashboardBarChart`, `HorizontalBarChart`, and the bar series inside `ComboMetricChart`.

---

## AI Rules

- **Always** use `<ChartContainer config={config}>` — never use `<ResponsiveContainer>` directly.
- **Never** use hard-coded hex colors on chart elements (`fill="#4F46E5"`). Always use `fill="var(--color-keyName)"` — this references the CSS variable injected by `ChartContainer`.
- Define color via `color: 'var(--chart-1)'` in `ChartConfig`, or pass `colors={['var(--chart-1)', 'var(--chart-2)']}` to dashboard-ready chart wrappers. The `--chart-*` tokens are theme-aware.
- `ChartContainer` already wraps `ResponsiveContainer width="100%" height="100%"` — do not add another one.
- Set height on `ChartContainer` via `className="h-[300px]"` — this sizes the container, not the Recharts elements.
- Import all base Recharts components (`BarChart`, `Bar`, `Line`, etc.) directly from `recharts` — they are not re-exported by `xertica-ui`.
- For dashboard-ready charts, prefer `ChartCard` plus a dashboard-ready wrapper before hand-assembling Recharts primitives.
- In FSD apps, keep data fetching in the feature/model layer and pass `data`, `isLoading`, `error`, and `onRetry` into chart wrappers.
- Use `barSize` to control bar thickness. Do not use a chart wrapper prop to control graph width; layout width belongs to the parent grid/container.
- When using `stacked`, order your `series` array so the visually topmost series is last — it will automatically receive the rounded top corners.
- `RadarMetricChart`, `PieMetricChart`, and `RadialBarMetricChart` build their `ChartConfig` internally from the `series`/`nameKey` data — you do not need to pass a `config` prop.
- When passing `stateClassName` to `RadarMetricChart`, `PieMetricChart`, or `RadialBarMetricChart`, it is included in the state object passed to `getChartState` — it does not need to be (and should not be) passed as a second `className` argument separately.
- `GaugeChart` is a pure SVG component — it does not use `ChartContainer` or Recharts. Wrap it in a `div` with `flex justify-center` to control its width.
- For `GaugeChart` thresholds, use `--chart-*` CSS tokens for colors so they remain theme-aware. Never use raw hex values.
- `RadialBarMetricChart` renders its legend as HTML below the chart (not inside the SVG) — this is intentional to avoid Recharts polar chart legend positioning issues.

---

## Related Patterns

- [Dashboard Pattern](../patterns/dashboard.md)
- [Analytics Pattern](../patterns/analytics.md)
