Skip to main content
Compendium / Governance & Risk / Sign-off / Risk Heatmap
#038

Risk Heatmap

3×3 likelihood × severity grid — the canonical "is this worth losing sleep over" visual.

STABLE GO
#chart #real-content #dark-mode-tested
Live

Risk register · 15 items

Sev × Likelihood

HIGHMEDLOW
0
1
3
1
2
2
4
1
1
LOWMEDHIGH

→ Likelihood · ↑ Severity

Spec

Intent

Map an inventory of identified risks across the likelihood/severity plane so the governor sees concentration at a glance. Cells in the top-right quadrant demand a control, not a comment.

Acceptance Criteria

  • [ ] 3×3 grid with axis labels (likelihood, severity)
  • [ ] Cell colour graded from success → attention → danger
  • [ ] Risk counts shown per cell
  • [ ] Top-right cell is unambiguously the worst

Constraints

  • No JS; pure layout
  • Reads in both light and dark themes
// risk-heatmap.astro
---
import type { CompendiumMeta } from '../../../data/types';

export const meta: CompendiumMeta = {
  id: 38,
  slug: 'risk-heatmap',
  name: 'Risk Heatmap',
  category: 'governance-risk',
  subcategory: 'signoff',
  pillar: 'go',
  description: '3×3 likelihood × severity grid — the canonical "is this worth losing sleep over" visual.',
  spec: `## Intent
Map an inventory of identified risks across the likelihood/severity plane so the governor sees concentration at a glance. Cells in the top-right quadrant demand a control, not a comment.

## Acceptance Criteria
- [ ] 3×3 grid with axis labels (likelihood, severity)
- [ ] Cell colour graded from success → attention → danger
- [ ] Risk counts shown per cell
- [ ] Top-right cell is unambiguously the worst

## Constraints
- No JS; pure layout
- Reads in both light and dark themes`,
  tags: ['chart', 'real-content', 'dark-mode-tested'],
  status: 'stable',
  health: { ics: 80, a11y: 'aa', themed: true, animated: false },
};

const cells: { count: number; tone: string }[][] = [
  [{count:0, tone:'bg-attention'}, {count:1, tone:'bg-danger'},    {count:3, tone:'bg-danger'}],
  [{count:1, tone:'bg-success'},   {count:2, tone:'bg-attention'}, {count:2, tone:'bg-danger'}],
  [{count:4, tone:'bg-success'},   {count:1, tone:'bg-success'},   {count:1, tone:'bg-attention'}],
];
const rowLabels = ['HIGH', 'MED', 'LOW'];
const colLabels = ['LOW', 'MED', 'HIGH'];
---

<div data-this-component class="w-full bg-paper text-black border-4 border-black p-4 overflow-hidden">
  <div class="flex items-baseline justify-between mb-2">
    <p class="font-mono text-[10px] uppercase tracking-widest opacity-60">Risk register · 15 items</p>
    <p class="font-black text-[10px] uppercase tracking-widest">Sev × Likelihood</p>
  </div>

  <div class="flex gap-2">
    <div class="flex flex-col justify-between text-[9px] font-mono uppercase tracking-widest opacity-70 py-1">
      {rowLabels.map(l => <span>{l}</span>)}
    </div>

    <div class="flex-1">
      <div class="grid grid-cols-3 gap-1">
        {cells.flat().map(c => (
          <div class:list={['aspect-square border-2 border-black flex items-center justify-center', c.tone]}>
            <span class="font-black text-lg text-white">{c.count}</span>
          </div>
        ))}
      </div>
      <div class="mt-1 grid grid-cols-3 text-[9px] font-mono uppercase tracking-widest opacity-70 text-center">
        {colLabels.map(l => <span>{l}</span>)}
      </div>
    </div>
  </div>

  <p class="mt-3 text-[10px] font-mono uppercase tracking-widest opacity-60">→ Likelihood ·  ↑ Severity</p>
</div>