Skip to main content
Compendium / Metrics & Telemetry / Charts / Bucket Distribution
#061

Bucket Distribution

A small histogram showing the spread of ICS scores across a team's spec inventory.

STABLE DE
#svg #chart #real-content
Live
// team inventory n=32
0-2020-4040-6060-8080-100
137129
Spec

Intent

A single ICS score is a point estimate; a team's spec inventory has a distribution. This histogram surfaces whether the team has a fat tail of bad specs or a tight cluster of good ones.

Acceptance Criteria

  • [ ] Five buckets: 0-20, 20-40, 40-60, 60-80, 80-100
  • [ ] Threshold (60) marked by a vertical line
  • [ ] Bar heights proportional, mono labels under each
  • [ ] Negative: does NOT show absolute pixel heights — uses domain scale

Constraints

  • Pure SVG, no JS
  • Tabular-nums for counts
// bucket-distribution.astro
---
import type { CompendiumMeta } from '../../../data/types';

export const meta: CompendiumMeta = {
  id: 61,
  slug: 'bucket-distribution',
  name: 'Bucket Distribution',
  category: 'metrics-telemetry',
  subcategory: 'charts',
  pillar: 'de',
  description: 'A small histogram showing the spread of ICS scores across a team\'s spec inventory.',
  spec: `## Intent
A single ICS score is a point estimate; a team's spec inventory has a distribution. This histogram surfaces whether the team has a fat tail of bad specs or a tight cluster of good ones.

## Acceptance Criteria
- [ ] Five buckets: 0-20, 20-40, 40-60, 60-80, 80-100
- [ ] Threshold (60) marked by a vertical line
- [ ] Bar heights proportional, mono labels under each
- [ ] Negative: does NOT show absolute pixel heights — uses domain scale

## Constraints
- Pure SVG, no JS
- Tabular-nums for counts`,
  tags: ['svg', 'chart', 'real-content'],
  status: 'stable',
  health: { ics: 85, a11y: 'aa', themed: true, animated: false },
};

const buckets = [
  { label: '0-20',   count: 1 },
  { label: '20-40',  count: 3 },
  { label: '40-60',  count: 7 },
  { label: '60-80',  count: 12 },
  { label: '80-100', count: 9 },
];
const max = Math.max(...buckets.map((b) => b.count));
const total = buckets.reduce((a, b) => a + b.count, 0);
const colorFor = (i: number) => i < 2 ? 'var(--color-danger)' : i === 2 ? 'var(--color-do)' : 'var(--color-success)';
---

<div class="w-full bg-paper text-black border-4 border-black">
  <header class="flex items-baseline justify-between px-3 py-2 border-b-2 border-black">
    <span class="font-mono text-[10px] uppercase tracking-widest opacity-60">// team inventory</span>
    <span class="font-mono text-[10px] uppercase tracking-widest">n=<span class="font-black tabular-nums">{total}</span></span>
  </header>
  <div class="p-3">
    <svg viewBox="0 0 100 60" preserveAspectRatio="none" class="w-full h-24 block" aria-label={`ICS distribution across ${total} specs`}>
      {buckets.map((b, i) => {
        const h = (b.count / max) * 50;
        const x = i * 20 + 2;
        return (
          <rect x={x} y={56 - h} width="16" height={h} fill={colorFor(i)} stroke="var(--color-ink)" stroke-width="0.5" vector-effect="non-scaling-stroke" />
        );
      })}
      <line x1="60" y1="0" x2="60" y2="56" stroke="var(--color-ink)" stroke-width="0.8" stroke-dasharray="2 2" vector-effect="non-scaling-stroke" />
      <line x1="0" y1="56" x2="100" y2="56" stroke="var(--color-ink)" stroke-width="1" vector-effect="non-scaling-stroke" />
    </svg>
    <div class="grid grid-cols-5 mt-1 font-mono text-[9px] uppercase tracking-widest opacity-60 text-center">
      {buckets.map((b) => <span>{b.label}</span>)}
    </div>
    <div class="grid grid-cols-5 mt-0.5 font-black text-[11px] tabular-nums text-center">
      {buckets.map((b) => <span>{b.count}</span>)}
    </div>
  </div>
</div>