---
name: "canvas-data-dashboard"
description: "Animated real-time data dashboard with bar chart, line chart, donut chart, and live metrics. HTML5 Canvas, dark theme, 60fps."
metadata:
  version: "1.0.0"
  dependencies:
    system:
      - "browser"
  runtime:
    language: "html"
    entrypoint: "dashboard.html"
    timeout_seconds: 30
---

# Canvas Data Dashboard

> **Difficulty:** Intermediate | **Runtime:** HTML5 Canvas | **Output:** Interactive visualization

Teach an AI agent to generate a real-time animated data dashboard with multiple chart types, smooth transitions, and a dark theme.

## What You'll Build

A self-contained HTML file rendering an animated dashboard with:
- Animated bar chart with value labels and smooth height transitions
- Real-time line chart with scrolling data points
- Donut/ring chart with percentage labels
- FPS counter and live data feed simulation
- Dark theme with neon accent colors

## Core Concepts

### 1. Canvas Rendering Pipeline

The dashboard uses a single `requestAnimationFrame` loop to render all charts:

```javascript
function render(timestamp) {
  ctx.clearRect(0, 0, width, height);
  drawBarChart(ctx, data.bars);
  drawLineChart(ctx, data.timeSeries);
  drawDonutChart(ctx, data.categories);
  drawFPS(ctx, timestamp);
  requestAnimationFrame(render);
}
```

### 2. Smooth Transitions

Bar heights interpolate toward target values using lerp:

```javascript
current += (target - current) * 0.08;  // ease toward target
```

### 3. Color Palette

Use HSL with fixed saturation/lightness for consistent neon look:
- Primary: `hsl(270, 80%, 65%)` — purple
- Accent: `hsl(170, 80%, 55%)` — teal
- Warn: `hsl(40, 90%, 60%)` — amber
- Grid: `rgba(255,255,255,0.08)`

## Teaching Points

- Canvas coordinate system and transforms
- `requestAnimationFrame` for 60fps rendering
- Lerp-based animation for smooth transitions
- Drawing arcs for donut charts
- Text measurement and alignment on canvas

---

## Files

### `dashboard.html`

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Dashboard</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { background: #0a0a1a; display: flex; justify-content: center; align-items: center; height: 100vh; }
  canvas { border-radius: 12px; box-shadow: 0 0 40px rgba(168, 85, 247, 0.15); }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const c = document.getElementById('c');
const W = c.width = 800, H = c.height = 500;
const ctx = c.getContext('2d');

const colors = ['#a855f7','#06b6d4','#f59e0b','#10b981','#f43f5e','#6366f1'];
const barLabels = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
const catLabels = ['API','Auth','UI','DB','Tests'];

let bars = barLabels.map(() => ({ val: 0, target: Math.random()*100 }));
let line = Array.from({length: 40}, () => Math.random()*60+20);
let cats = catLabels.map(() => Math.random()*40+10);
let frame = 0, lastTime = 0, fps = 0;

function lerp(a,b,t){ return a+(b-a)*t; }

function drawBars(){
  const x0=40, y0=30, w=360, h=180;
  ctx.fillStyle='#fff'; ctx.font='bold 13px monospace';
  ctx.fillText('Weekly Requests (k)',x0,y0-8);
  const bw=w/bars.length;
  bars.forEach((b,i)=>{
    b.val=lerp(b.val,b.target,0.06);
    const bh=b.val/100*h;
    const grad=ctx.createLinearGradient(0,y0+h-bh,0,y0+h);
    grad.addColorStop(0,colors[i%colors.length]);
    grad.addColorStop(1,colors[i%colors.length]+'44');
    ctx.fillStyle=grad;
    ctx.fillRect(x0+i*bw+8,y0+h-bh,bw-16,bh);
    ctx.fillStyle='#888'; ctx.font='10px monospace';
    ctx.textAlign='center';
    ctx.fillText(barLabels[i],x0+i*bw+bw/2,y0+h+14);
    ctx.fillStyle='#fff'; ctx.font='bold 10px monospace';
    ctx.fillText(Math.round(b.val)+'k',x0+i*bw+bw/2,y0+h-bh-6);
  });
  ctx.textAlign='left';
  // grid lines
  for(let i=0;i<=4;i++){
    const gy=y0+h-i*(h/4);
    ctx.strokeStyle='rgba(255,255,255,0.06)';
    ctx.beginPath(); ctx.moveTo(x0,gy); ctx.lineTo(x0+w,gy); ctx.stroke();
  }
}

function drawLine(){
  const x0=440, y0=30, w=340, h=180;
  ctx.fillStyle='#fff'; ctx.font='bold 13px monospace';
  ctx.fillText('Latency (ms)',x0,y0-8);
  const step=w/(line.length-1);
  ctx.beginPath();
  ctx.moveTo(x0,y0+h-line[0]/100*h);
  line.forEach((v,i)=>{
    ctx.lineTo(x0+i*step,y0+h-v/100*h);
  });
  ctx.strokeStyle='#06b6d4'; ctx.lineWidth=2; ctx.stroke();
  // fill under
  ctx.lineTo(x0+(line.length-1)*step,y0+h);
  ctx.lineTo(x0,y0+h);
  ctx.closePath();
  ctx.fillStyle='rgba(6,182,212,0.08)'; ctx.fill();
  ctx.lineWidth=1;
  // grid
  for(let i=0;i<=4;i++){
    const gy=y0+h-i*(h/4);
    ctx.strokeStyle='rgba(255,255,255,0.06)';
    ctx.beginPath(); ctx.moveTo(x0,gy); ctx.lineTo(x0+w,gy); ctx.stroke();
  }
}

function drawDonut(){
  const cx=200, cy=360, r=80, rInner=50;
  ctx.fillStyle='#fff'; ctx.font='bold 13px monospace';
  ctx.fillText('Service Distribution',40,260);
  const total=cats.reduce((a,b)=>a+b,0);
  let angle=-Math.PI/2;
  cats.forEach((v,i)=>{
    const slice=v/total*Math.PI*2;
    ctx.beginPath();
    ctx.arc(cx,cy,r,angle,angle+slice);
    ctx.arc(cx,cy,rInner,angle+slice,angle,true);
    ctx.closePath();
    ctx.fillStyle=colors[i]; ctx.fill();
    // label
    const mid=angle+slice/2;
    const lx=cx+Math.cos(mid)*(r+20);
    const ly=cy+Math.sin(mid)*(r+20);
    ctx.fillStyle='#ccc'; ctx.font='10px monospace'; ctx.textAlign='center';
    ctx.fillText(catLabels[i]+' '+Math.round(v/total*100)+'%',lx,ly);
    angle+=slice;
  });
  ctx.textAlign='left';
  // center text
  ctx.fillStyle='#fff'; ctx.font='bold 16px monospace'; ctx.textAlign='center';
  ctx.fillText(Math.round(total),cx,cy+6);
  ctx.textAlign='left';
}

function drawStats(){
  const x0=440, y0=260;
  ctx.fillStyle='#fff'; ctx.font='bold 13px monospace';
  ctx.fillText('Live Metrics',x0,y0);
  const metrics=[
    {label:'Uptime',val:'99.97%',color:'#10b981'},
    {label:'Errors/min',val:Math.round(Math.random()*5).toString(),color:'#f43f5e'},
    {label:'Avg Response',val:Math.round(line[line.length-1])+'ms',color:'#06b6d4'},
    {label:'Active Users',val:Math.round(300+Math.random()*50).toString(),color:'#a855f7'},
  ];
  metrics.forEach((m,i)=>{
    const my=y0+25+i*42;
    ctx.fillStyle='rgba(255,255,255,0.05)';
    ctx.fillRect(x0,my,340,34);
    ctx.fillStyle='#888'; ctx.font='11px monospace';
    ctx.fillText(m.label,x0+12,my+22);
    ctx.fillStyle=m.color; ctx.font='bold 14px monospace';
    ctx.textAlign='right';
    ctx.fillText(m.val,x0+328,my+22);
    ctx.textAlign='left';
  });
}

function drawFPS(ts){
  if(ts-lastTime>=1000){ fps=frame; frame=0; lastTime=ts; }
  frame++;
  ctx.fillStyle='#444'; ctx.font='10px monospace';
  ctx.fillText(fps+' fps',W-50,H-8);
}

function update(){
  if(Math.random()<0.03) bars.forEach(b=>b.target=Math.random()*100);
  line.push(Math.max(10,Math.min(90,line[line.length-1]+(Math.random()-0.5)*12)));
  if(line.length>40) line.shift();
  cats=cats.map(v=>Math.max(5,v+(Math.random()-0.5)*3));
}

function render(ts){
  ctx.fillStyle='#0a0a1a'; ctx.fillRect(0,0,W,H);
  update();
  drawBars();
  drawLine();
  drawDonut();
  drawStats();
  drawFPS(ts);
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>
```

### `README.md`

```md
# Canvas Data Dashboard

An animated real-time data dashboard built with HTML5 Canvas.

## Features
- Animated bar chart with smooth lerp transitions
- Scrolling line chart with gradient fill
- Donut chart with percentage labels
- Live metrics panel with random data simulation
- 60fps rendering with FPS counter
- Dark theme with neon accent colors

## Run
Open `dashboard.html` in any modern browser. No dependencies required.

```
