Skip to main content
Compendium / Metrics & Telemetry / Gauges / Coverage Donut
#059

Coverage Donut

Donut chart of acceptance-criteria coverage — passed, pending, and not-applicable.

STABLE DE
#svg #gauge #real-content
Live
// SPEC-042 · acceptance
14/22 passed
  • passed14
  • pending5
  • N/A3
Spec

Intent

Render acceptance-criteria coverage of a spec as a donut so the team sees passed/pending/N-A ratios without reading a checklist.

Acceptance Criteria

  • [ ] Three segments: passed (success), pending (do), N/A (ink low-opacity)
  • [ ] Legend with counts to the right
  • [ ] Centre shows total passed / total
  • [ ] Negative: does NOT depend on JS to render

Constraints

  • Pure SVG, no JS
  • Segments computed from raw counts
// coverage-donut.astro
---
import type { CompendiumMeta } from '../../../data/types';

export const meta: CompendiumMeta = {
  id: 59,
  slug: 'coverage-donut',
  name: 'Coverage Donut',
  category: 'metrics-telemetry',
  subcategory: 'gauges',
  pillar: 'de',
  description: 'Donut chart of acceptance-criteria coverage — passed, pending, and not-applicable.',
  spec: `## Intent
Render acceptance-criteria coverage of a spec as a donut so the team sees passed/pending/N-A ratios without reading a checklist.

## Acceptance Criteria
- [ ] Three segments: passed (success), pending (do), N/A (ink low-opacity)
- [ ] Legend with counts to the right
- [ ] Centre shows total passed / total
- [ ] Negative: does NOT depend on JS to render

## Constraints
- Pure SVG, no JS
- Segments computed from raw counts`,
  tags: ['svg', 'gauge', 'real-content'],
  status: 'stable',
  health: { ics: 87, a11y: 'aa', themed: true, animated: false },
};

const passed = 14;
const pending = 5;
const na = 3;
const total = passed + pending + na;
const r = 36;
const c = 2 * Math.PI * r;
const seg = (n: number) => (n / total) * c;
let off = 0;
const segs = [
  { n: passed,  color: 'var(--color-success)', opacity: 1 },
  { n: pending, color: 'var(--color-do)',      opacity: 1 },
  { n: na,      color: 'var(--color-ink)',     opacity: 0.2 },
];
---

<div class="w-full bg-paper text-black border-4 border-black">
  <header class="px-3 py-2 border-b-2 border-black">
    <span class="font-mono text-[10px] uppercase tracking-widest opacity-60">// SPEC-042 · acceptance</span>
  </header>
  <div class="grid grid-cols-[auto_1fr] gap-4 p-3 items-center">
    <div class="relative">
      <svg viewBox="0 0 100 100" class="w-28 h-28 block -rotate-90" aria-label={`${passed} of ${total} criteria passed`}>
        {segs.map((s) => {
          const dash = `${seg(s.n)} ${c - seg(s.n)}`;
          const offset = -off;
          off += seg(s.n);
          return (
            <circle cx="50" cy="50" r={r} fill="none" stroke={s.color} stroke-width="16" stroke-dasharray={dash} stroke-dashoffset={offset} opacity={s.opacity} />
          );
        })}
      </svg>
      <div class="absolute inset-0 flex flex-col items-center justify-center">
        <span class="font-black text-xl tabular-nums leading-none">{passed}<span class="opacity-60">/{total}</span></span>
        <span class="font-mono text-[9px] uppercase tracking-widest opacity-60 mt-0.5">passed</span>
      </div>
    </div>
    <ul class="space-y-1.5 font-mono text-[10px] uppercase tracking-widest">
      <li class="flex items-center gap-2"><span class="inline-block w-3 h-3 border-2 border-black bg-success"></span><span class="flex-1">passed</span><span class="font-black tabular-nums">{passed}</span></li>
      <li class="flex items-center gap-2"><span class="inline-block w-3 h-3 border-2 border-black bg-do"></span><span class="flex-1">pending</span><span class="font-black tabular-nums">{pending}</span></li>
      <li class="flex items-center gap-2"><span class="inline-block w-3 h-3 border-2 border-black opacity-30" style="background:var(--color-ink)"></span><span class="flex-1 opacity-60">N/A</span><span class="font-black tabular-nums">{na}</span></li>
    </ul>
  </div>
</div>