# Dashboard & Chart Patterns

> **Library:** Recharts (standardized for SmartStack)
> **Install:** `npm install recharts` (add to package.json if absent)
> **Reference:** https://recharts.org/

## CRITICAL RULES

1. **NEVER hardcode chart colors** - Use CSS variables (`--chart-1` to `--chart-5`)
2. **NEVER hardcode text** - Use `useTranslation()` for all labels, tooltips, legends
3. **ALWAYS use ResponsiveContainer** - Charts must be responsive
4. **ALWAYS provide loading/error states** - Skeleton loaders for charts
5. **ALWAYS use CSS variables** for backgrounds, borders, text (see style-guide.md)

---

## KPI CARD (StatCard)

Use for single-value metrics with trend indicator.

```tsx
import { LucideIcon } from 'lucide-react';

interface StatCardProps {
  label: string;
  value: string | number;
  icon: LucideIcon;
  trend?: { value: number; direction: 'up' | 'down' | 'flat' };
  format?: 'number' | 'currency' | 'percent' | 'duration';
}

function StatCard({ label, value, icon: Icon, trend, format = 'number' }: StatCardProps) {
  const formattedValue = formatValue(value, format);

  return (
    <div className="rounded-[var(--radius-card)] bg-[var(--bg-secondary)] border border-[var(--border-color)] p-6">
      <div className="flex items-center justify-between">
        <span className="text-sm font-medium text-[var(--text-muted)]">{label}</span>
        <Icon className="h-5 w-5 text-[var(--color-primary-500)]" />
      </div>
      <div className="mt-2 text-2xl font-bold text-[var(--text-primary)]">
        {formattedValue}
      </div>
      {trend && (
        <div className={`mt-1 flex items-center text-sm ${
          trend.direction === 'up' ? 'text-[var(--success-text)]' :
          trend.direction === 'down' ? 'text-[var(--error-text)]' :
          'text-[var(--text-muted)]'
        }`}>
          {trend.direction === 'up' ? '↑' : trend.direction === 'down' ? '↓' : '→'}
          <span className="ml-1">{Math.abs(trend.value)}%</span>
        </div>
      )}
    </div>
  );
}
```

### KPI Card with Thresholds

```tsx
function getThresholdColor(value: number, thresholds?: { warning: number; critical: number }) {
  if (!thresholds) return 'text-[var(--text-primary)]';
  if (value >= thresholds.critical) return 'text-[var(--error-text)]';
  if (value >= thresholds.warning) return 'text-[var(--warning-text)]';
  return 'text-[var(--success-text)]';
}
```

---

## CHART WRAPPER (ChartCard)

Wraps any Recharts chart in a consistent card layout.

```tsx
interface ChartCardProps {
  title: string;
  subtitle?: string;
  children: React.ReactNode;
  height?: number;
  loading?: boolean;
}

function ChartCard({ title, subtitle, children, height = 300, loading }: ChartCardProps) {
  if (loading) {
    return (
      <div className="rounded-[var(--radius-card)] bg-[var(--bg-secondary)] border border-[var(--border-color)] p-6">
        <div className="h-4 w-1/3 bg-[var(--bg-tertiary)] rounded animate-pulse mb-4" />
        <div className="bg-[var(--bg-tertiary)] rounded animate-pulse" style={{ height }} />
      </div>
    );
  }

  return (
    <div className="rounded-[var(--radius-card)] bg-[var(--bg-secondary)] border border-[var(--border-color)] p-6">
      <h3 className="text-base font-semibold text-[var(--text-primary)]">{title}</h3>
      {subtitle && (
        <p className="text-sm text-[var(--text-muted)] mt-1">{subtitle}</p>
      )}
      <div className="mt-4" style={{ height }}>
        {children}
      </div>
    </div>
  );
}
```

---

## CHART TYPES

### Shared Tooltip Style

```tsx
const TOOLTIP_STYLE = {
  contentStyle: {
    backgroundColor: 'var(--bg-secondary)',
    border: '1px solid var(--border-color)',
    borderRadius: 'var(--radius-card)',
    color: 'var(--text-primary)',
    fontSize: '0.875rem',
  },
  labelStyle: {
    color: 'var(--text-primary)',
    fontWeight: 600,
  },
};
```

### Chart Color Palette

```tsx
const CHART_COLORS = [
  'var(--chart-1)',
  'var(--chart-2)',
  'var(--chart-3)',
  'var(--chart-4)',
  'var(--chart-5)',
];
```

### Bar Chart

```tsx
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';

<ChartCard title={t('charts.itemsByMonth')}>
  <ResponsiveContainer width="100%" height="100%">
    <BarChart data={data}>
      <CartesianGrid strokeDasharray="3 3" stroke="var(--border-color)" />
      <XAxis dataKey="name" stroke="var(--text-muted)" fontSize={12} />
      <YAxis stroke="var(--text-muted)" fontSize={12} />
      <Tooltip {...TOOLTIP_STYLE} />
      <Bar dataKey="value" fill="var(--chart-1)" radius={[4, 4, 0, 0]} />
    </BarChart>
  </ResponsiveContainer>
</ChartCard>
```

### Stacked Bar Chart

```tsx
<ResponsiveContainer width="100%" height="100%">
  <BarChart data={data}>
    <CartesianGrid strokeDasharray="3 3" stroke="var(--border-color)" />
    <XAxis dataKey="month" stroke="var(--text-muted)" fontSize={12} />
    <YAxis stroke="var(--text-muted)" fontSize={12} />
    <Tooltip {...TOOLTIP_STYLE} />
    <Bar dataKey="active" stackId="a" fill="var(--chart-1)" />
    <Bar dataKey="pending" stackId="a" fill="var(--chart-2)" />
    <Bar dataKey="closed" stackId="a" fill="var(--chart-3)" radius={[4, 4, 0, 0]} />
  </BarChart>
</ResponsiveContainer>
```

### Line Chart

```tsx
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';

<ChartCard title={t('charts.trendOverTime')}>
  <ResponsiveContainer width="100%" height="100%">
    <LineChart data={data}>
      <CartesianGrid strokeDasharray="3 3" stroke="var(--border-color)" />
      <XAxis dataKey="date" stroke="var(--text-muted)" fontSize={12} />
      <YAxis stroke="var(--text-muted)" fontSize={12} />
      <Tooltip {...TOOLTIP_STYLE} />
      <Line type="monotone" dataKey="value" stroke="var(--chart-1)" strokeWidth={2} dot={false} />
      <Line type="monotone" dataKey="target" stroke="var(--chart-2)" strokeWidth={2} strokeDasharray="5 5" dot={false} />
    </LineChart>
  </ResponsiveContainer>
</ChartCard>
```

### Pie / Donut Chart

```tsx
import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip, Legend } from 'recharts';

<ChartCard title={t('charts.distributionByStatus')}>
  <ResponsiveContainer width="100%" height="100%">
    <PieChart>
      <Pie
        data={data}
        cx="50%"
        cy="50%"
        innerRadius={60}  // 0 for pie, >0 for donut
        outerRadius={100}
        dataKey="value"
        nameKey="name"
        label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
      >
        {data.map((_, index) => (
          <Cell key={index} fill={CHART_COLORS[index % CHART_COLORS.length]} />
        ))}
      </Pie>
      <Tooltip {...TOOLTIP_STYLE} />
      <Legend />
    </PieChart>
  </ResponsiveContainer>
</ChartCard>
```

### Area Chart

```tsx
import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';

<ChartCard title={t('charts.volumeOverTime')}>
  <ResponsiveContainer width="100%" height="100%">
    <AreaChart data={data}>
      <CartesianGrid strokeDasharray="3 3" stroke="var(--border-color)" />
      <XAxis dataKey="date" stroke="var(--text-muted)" fontSize={12} />
      <YAxis stroke="var(--text-muted)" fontSize={12} />
      <Tooltip {...TOOLTIP_STYLE} />
      <Area type="monotone" dataKey="value" stroke="var(--chart-1)" fill="var(--chart-1)" fillOpacity={0.2} />
    </AreaChart>
  </ResponsiveContainer>
</ChartCard>
```

---

## DASHBOARD LAYOUT

### Standard Dashboard Grid

```tsx
interface DashboardLayoutProps {
  kpis: StatCardProps[];
  charts: ChartCardProps[];
  filters?: React.ReactNode;
  loading?: boolean;
}

function DashboardLayout({ kpis, charts, filters, loading }: DashboardLayoutProps) {
  return (
    <div className="space-y-6">
      {/* Filters */}
      {filters && (
        <div className="flex flex-wrap items-center gap-4">
          {filters}
        </div>
      )}

      {/* KPI Row - 1 col mobile, 2 col tablet, 4 col desktop */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
        {kpis.map((kpi, i) => (
          <StatCard key={i} {...kpi} />
        ))}
      </div>

      {/* Charts Row - 1 col mobile, 2 col desktop */}
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {charts.map((chart, i) => (
          <ChartCard key={i} {...chart} loading={loading} />
        ))}
      </div>
    </div>
  );
}
```

### Period Filter

```tsx
import { useTranslation } from 'react-i18next';

function PeriodFilter({ value, onChange }: { value: string; onChange: (v: string) => void }) {
  const { t } = useTranslation();
  const periods = [
    { value: 'day', label: t('common:periods.day') },
    { value: 'week', label: t('common:periods.week') },
    { value: 'month', label: t('common:periods.month') },
    { value: 'quarter', label: t('common:periods.quarter') },
    { value: 'year', label: t('common:periods.year') },
  ];

  return (
    <select
      value={value}
      onChange={(e) => onChange(e.target.value)}
      className="rounded-[var(--radius-button)] bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[var(--text-primary)] px-3 py-1.5 text-sm"
    >
      {periods.map(p => (
        <option key={p.value} value={p.value}>{p.label}</option>
      ))}
    </select>
  );
}
```

---

## ABSOLUTE RULES

| DO | DON'T |
|----|-------|
| CSS variables for ALL chart colors (`--chart-1` to `--chart-5`) | Hardcoded hex colors (`fill="#8884d8"`) |
| CSS variables for backgrounds/borders/text | Hardcoded Tailwind (`bg-blue-500`) |
| `ResponsiveContainer width="100%" height="100%"` | Fixed pixel dimensions |
| `useTranslation()` for labels and tooltips | Hardcoded text strings |
| Skeleton loading states for charts | Blank space during loading |
| Separate `StatCard` for KPI values | Inline divs with numbers |
| `ChartCard` wrapper for consistent styling | Naked charts without card wrapper |
| `memo()` for chart components in lists | Re-rendering charts on every state change |
