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.jsonUsage
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
- Two input modes. Either pass canonical
counts, or passpredictions+labelsarrays and let the component count internally. When both are provided,countswins — useful for forcing a specific scenario. - 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.
- Cell tint scales with count. Each cell's background is
oklch(from var(--cb-tone) l c h / α)where the alpha ramps from0.12(empty) to0.57(densest cell). The tint conveys magnitude at a glance without numbers. - 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. - 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.
- Controlled or uncontrolled hover. Pass
hoveredCell+onHoveredCellChangeto coordinate with external UI (e.g., a narration panel), or leave both off and let the component manage its own state viadefaultHoveredCell. - Metrics are defensively computed. Each denominator is checked before division — empty matrices show
—for affected metrics, neverNaN.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
counts | ConfusionCounts | — | Raw counts. Takes precedence over predictions / labels. |
predictions | readonly boolean[] | — | Predicted class for each sample. Computes counts when paired with labels. |
labels | readonly boolean[] | — | Ground-truth class for each sample. |
positiveLabel | string | "Positive" | Label for the positive class. |
negativeLabel | string | "Negative" | Label for the negative class. |
showMetrics | boolean | true | Render the metrics column. |
hoveredCell | ConfusionCell | null | — | Controlled hovered cell. |
defaultHoveredCell | ConfusionCell | null | null | Initial uncontrolled hovered cell. |
onHoveredCellChange | (cell: ConfusionCell | null) => void | — | Fires on hover enter / leave / focus. |
className | string | — | Merged 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>isrole="figure"with a visually hidden summary of all four counts plus accuracy, precision, recall, and F1. - Each cell is a focusable
role="button"witharia-labeldescribing what it represents and its current count. - Focus ring uses
:focus-visiblewith 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 customWidgetchrome. The library extract is the bare visualization primitive — counts in, matrix + metrics out.