Confusion Matrix Builder

A 2×2 confusion matrix for binary classifiers — pass raw counts ({ tp, fp, fn, tn }) or paired predictions + labels arrays. Each cell tints by its share of the densest count, and hovering a cell lights up the metrics it participates in (precision, recall, F1, accuracy).

Confusion matrix — TP 42, FP 8, FN 5, TN 95. Accuracy 91.3%, precision 84.0%, recall 89.4%, F1 86.6%.
Predicted
Positive
Negative
Actual
Positive
Negative
Accuracy91.3%
(TP + TN) / N
Precision84.0%
TP / (TP + FP)
Recall89.4%
TP / (TP + FN)
F186.6%
2 · P · R / (P + R)
Customize
Counts
42
8
5
95
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/confusion-matrix-builder.json

Usage

import { ConfusionMatrixBuilder } from "@craft-bits/core";
 
<ConfusionMatrixBuilder counts={{ tp: 42, fp: 8, fn: 5, tn: 95 }} />

Compute counts from prediction / label arrays instead:

<ConfusionMatrixBuilder
  predictions={[true, true, false, true, false]}
  labels={[true, false, false, true, true]}
/>

Hide the metrics column for a bare matrix:

<ConfusionMatrixBuilder
  counts={{ tp: 42, fp: 8, fn: 5, tn: 95 }}
  showMetrics={false}
/>

Drive the hovered cell from outside to coordinate with other UI:

const [hovered, setHovered] = useState<ConfusionCell | null>(null);
 
<ConfusionMatrixBuilder
  counts={counts}
  hoveredCell={hovered}
  onHoveredCellChange={setHovered}
/>

Understanding the component

  1. Two input modes. Either pass canonical counts, or pass predictions + labels arrays and let the component count internally. When both are provided, counts wins — useful for forcing a specific scenario.
  2. 2×2 grid with axis labels. "Predicted" runs across the top (positive / negative); "Actual" runs down the left side. The two correct cells (TP, TN) sit on the diagonal and tint in the success tone; the two error cells (FP, FN) tint in the error tone.
  3. Cell tint scales with count. Each cell's background is oklch(from var(--cb-tone) l c h / α) where the alpha ramps from 0.12 (empty) to 0.57 (densest cell). The tint conveys magnitude at a glance without numbers.
  4. Animated numeric readouts. Counts use a spring-driven motion value (SPRINGS.smooth) so they tick smoothly when the props change. Reduced-motion users get an instant snap. The metric percentages animate the same way.
  5. Hover lights the matrix and the metrics. Hovering a cell ringed-highlights it, tints its row and column headers in the accent color, and lights the metrics it participates in. TP lights all four. TN only lights accuracy. FP lights precision and F1. FN lights recall and F1.
  6. Controlled or uncontrolled hover. Pass hoveredCell + onHoveredCellChange to coordinate with external UI (e.g., a narration panel), or leave both off and let the component manage its own state via defaultHoveredCell.
  7. Metrics are defensively computed. Each denominator is checked before division — empty matrices show for affected metrics, never NaN.

Props

PropTypeDefaultDescription
countsConfusionCountsRaw counts. Takes precedence over predictions / labels.
predictionsreadonly boolean[]Predicted class for each sample. Computes counts when paired with labels.
labelsreadonly boolean[]Ground-truth class for each sample.
positiveLabelstring"Positive"Label for the positive class.
negativeLabelstring"Negative"Label for the negative class.
showMetricsbooleantrueRender the metrics column.
hoveredCellConfusionCell | nullControlled hovered cell.
defaultHoveredCellConfusionCell | nullnullInitial uncontrolled hovered cell.
onHoveredCellChange(cell: ConfusionCell | null) => voidFires on hover enter / leave / focus.
classNamestringMerged onto the root <div> via cn().

The shapes:

type ConfusionCell = "tp" | "fp" | "fn" | "tn";
 
interface ConfusionCounts {
  tp: number;
  fp: number;
  fn: number;
  tn: number;
}

Accessibility

  • The root <div> is role="figure" with a visually hidden summary of all four counts plus accuracy, precision, recall, and F1.
  • Each cell is a focusable role="button" with aria-label describing what it represents and its current count.
  • Focus ring uses :focus-visible with the accent color and a 2px offset so keyboard users always see where they are.
  • Motion respects prefers-reduced-motion: reduce — animated count / percent readouts collapse to instant updates.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/viz/ConfusionMatrixBuilder.tsx). The source had three lesson modes (Explore / Build / Predict), preset scenarios, narration heuristics, threshold-sweep curves, and a custom Widget chrome. The library extract is the bare visualization primitive — counts in, matrix + metrics out.