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>