# Chart Builder

> Generates dark-themed, accessible data visualization components using Chart.js or Recharts. Produces glass morphism styled charts that match the user's design system with proper accessibility, responsive sizing, and performance optimization. Triggered when the user asks to create a chart, visualize data, add a graph, or build a dashboard component.

- Skill: `0xjitsu/chart-builder` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 0xjitsu/chart-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/0xjitsu/chart-builder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- License: AGPL-3.0-or-later
- Author: 0xjitsu (https://skillmd.com/u/0xjitsu)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/0xjitsu/chart-builder

---


# Chart Builder Skill

Dark-themed, accessible data visualization generator.

## When to Trigger

Activate this skill when the user:
- Asks to create a chart, graph, or data visualization
- Wants to visualize data from an API, database, or static dataset
- Requests a dashboard component with charts
- Needs to display metrics, trends, or comparisons visually

## Stack Selection

| Scenario | Library | Reason |
|----------|---------|--------|
| Standalone chart, no React | **Chart.js** | Lightweight, canvas-based |
| React/Next.js project | **Recharts** | Native React components, composable |
| Complex interactive dashboards | **Recharts** | Better state management integration |
| Static image export needed | **Chart.js** | Canvas `toDataURL()` support |

## Design System

All charts must follow these design tokens to match the user's dark theme:

### Colors

```js
const chartTheme = {
  background: '#18181B',        // zinc-900
  surface: 'rgba(255,255,255,0.05)',  // glass bg
  border: 'rgba(255,255,255,0.10)',   // glass border
  gridLines: 'rgba(255,255,255,0.06)',
  labelText: '#D4D4D8',        // zinc-300
  valueText: '#F4F4F5',        // zinc-100
  tooltip: {
    bg: 'rgba(24,24,27,0.90)',
    border: 'rgba(255,255,255,0.10)',
    blur: '16px',
  },
};
```

### Accent Palette

Use these for data series — ordered by visual priority:

| Index | Name | Hex | Use case |
|-------|------|-----|----------|
| 0 | Blue | `#3B82F6` | Primary metric |
| 1 | Purple | `#8B5CF6` | Secondary metric |
| 2 | Emerald | `#10B981` | Positive/growth |
| 3 | Amber | `#F59E0B` | Warning/attention |
| 4 | Rose | `#F43F5E` | Negative/decline |
| 5 | Cyan | `#06B6D4` | Tertiary metric |

### Glass Morphism Container

Wrap every chart in a glass container:

```jsx
<div className="backdrop-blur-xl bg-white/5 border border-white/10 rounded-2xl p-6">
  <h3 className="text-zinc-100 text-lg font-semibold mb-4">{title}</h3>
  <div className="min-h-[300px]" role="img" aria-label={ariaLabel}>
    {/* Chart component */}
  </div>
</div>
```

## Chart Types

### Line Chart
Best for: trends over time, continuous data.
- Use gradient fill below the line (`linearGradient` from accent to transparent)
- Smooth curves: `type="monotone"` (Recharts) or `tension: 0.4` (Chart.js)
- Show data points on hover only (reduce visual clutter)

### Bar Chart
Best for: comparisons between categories.
- Rounded top corners: `radius={[4, 4, 0, 0]}`
- Max bar width: 48px (prevent oversized bars on few data points)
- Add value labels above bars for small datasets (under 8 items)

### Area Chart
Best for: volume/magnitude over time.
- Stack multiple areas for composition views
- Use decreasing opacity for stacked layers (0.6, 0.4, 0.2)
- Gradient fill from accent color to transparent

### Pie / Donut Chart
Best for: part-to-whole relationships (max 6 segments).
- Donut preferred over pie (inner radius 60%)
- Active segment: slight outward offset on hover
- Center label: total value or primary metric
- Legend positioned below on mobile, right on desktop

### Scatter Chart
Best for: correlation between two variables.
- Variable dot size for third dimension (bubble chart)
- Add quadrant lines if meaningful thresholds exist
- Tooltip shows both axis values plus label

### Radar Chart
Best for: multi-dimensional comparison (3-8 axes).
- Filled area with low opacity (0.2)
- Grid lines match the dark theme
- Limit to 2-3 overlapping datasets for readability

## Accessibility Requirements

Every chart must include:

1. **ARIA attributes:**
   ```jsx
   <div role="img" aria-label="Monthly revenue trend showing 23% growth from January to June 2025">
   ```

2. **Reduced motion:**
   ```css
   @media (prefers-reduced-motion: reduce) {
     .chart-container * {
       animation: none !important;
       transition: none !important;
     }
   }
   ```

3. **Color independence:** Never rely on color alone to convey meaning. Add:
   - Pattern fills or dash styles for line differentiation
   - Direct labels or legends with text descriptions
   - Shape variation for scatter plots

4. **Screen reader fallback:** Include a hidden data table:
   ```jsx
   <table className="sr-only">
     <caption>Monthly revenue data</caption>
     {/* Full data in tabular form */}
   </table>
   ```

5. **Contrast:** All text on the chart must meet WCAG AA (4.5:1 ratio against `zinc-900`).

## Responsive Behavior

- Charts fill their container width (`responsive: true` / `width="100%"`)
- Set `min-h-[300px]` on the wrapper to prevent CLS
- On mobile (< 640px): move legends below the chart, reduce font sizes
- On print: switch to light theme with black text (via `@media print`)

## Performance

Charts are heavy — always lazy load:

```jsx
import dynamic from 'next/dynamic';

const RevenueChart = dynamic(() => import('@/components/charts/RevenueChart'), {
  loading: () => (
    <div className="backdrop-blur-xl bg-white/5 border border-white/10 rounded-2xl p-6 min-h-[300px] animate-pulse" />
  ),
  ssr: false,
});
```

- Never put chart components in the LCP critical path
- Use `ssr: false` — canvas/SVG charts need the DOM
- Skeleton matches the glass container dimensions exactly

## Tooltip Styling

```jsx
const CustomTooltip = ({ active, payload, label }) => {
  if (!active || !payload) return null;
  return (
    <div className="backdrop-blur-xl bg-zinc-900/90 border border-white/10 rounded-lg px-3 py-2 shadow-xl">
      <p className="text-zinc-400 text-xs mb-1">{label}</p>
      {payload.map((entry, i) => (
        <p key={i} className="text-zinc-100 text-sm font-medium">
          <span style={{ color: entry.color }}>●</span> {entry.name}: {entry.value}
        </p>
      ))}
    </div>
  );
};
```

