Similarity Heatmap
A square heatmap of pairwise similarity between N embeddings. The diagonal is 1 (every embedding is identical to itself); off-diagonal cells encode how close two embeddings sit in vector space. Switch metrics, recolor, resize — the same primitive teaches both cosine geometry and dot-product magnitude.
Cosine similarity heatmap: 6 embeddings.
Cosine similarity6 × 6
cat
dog
fox
red
blue
boat
cat
dog
fox
red
blue
boat
-1+1
Customize
Shape
6
40px
Encoding
accent
Installation
npx shadcn@latest add https://craftbits.dev/r/similarity-heatmap.jsonUsage
import { SimilarityHeatmap } from "@craft-bits/core";
const embeddings = [
[0.92, 0.18, 0.05, 0.34], // cat
[0.88, 0.22, 0.07, 0.40], // dog
[0.10, 0.84, 0.78, 0.05], // red
];
<SimilarityHeatmap
embeddings={embeddings}
labels={["cat", "dog", "red"]}
/>Drive the hover state from outside the component to sync the cross-highlight with an inspector elsewhere on the page:
const [hovered, setHovered] = useState<[number, number] | null>(null);
<SimilarityHeatmap
embeddings={embeddings}
labels={labels}
hoveredCell={hovered}
onHoveredCellChange={setHovered}
/>Understanding the component
- Pairwise computation. Given
Nembeddings of dimensiond, the component builds anN × Nmatrix. Forcosine, each cell is(a·b) / (‖a‖·‖b‖)— the diagonal is exactly1. Fordot, each cell is the raw dot producta·b; magnitudes matter, so the legend shows the per-matrix min/max. - Two metrics, one ramp. Cosine values live in
[-1, 1]and map to[0, 1]by(v + 1) / 2. Dot products are min-max normalised so the visual ramp adapts to the input scale — without this, a single large entry would wash out the rest. - Grid layout, real elements. The matrix is a CSS grid with one extra row + column for axis labels. Each cell is a
motion.div— there is no<svg>, so cells get focus rings, real hit areas, and crisp text-rendering for axis labels. - Cross-highlight on hover. Hovering a cell sets
[row, col]. The row + column labels flip to--cb-accent, every cell outside the hovered row or column dims to 0.35 opacity, and the caption shows the exact pair score (e.g.cat × dog = 0.984). - Reduced motion. When
prefers-reduced-motion: reduceis set, the dim transition collapses toduration: 0— the highlight snaps instead of springs.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
embeddings | readonly (readonly number[])[] | required | N embeddings of dimension d. |
labels | readonly string[] | numeric indices | Axis labels — one per embedding. |
metric | "cosine" | "dot" | "cosine" | Similarity metric used to compute each cell. |
colorScale | "accent" | "mono" | "spectrum" | "accent" | Color ramp for cell fills. |
cellSize | number | 36 | Pixel size of each cell. |
interactive | boolean | true | When true, hovering a cell highlights its row + column. |
hoveredCell | readonly [number, number] | null | — | Controlled hovered cell. |
onHoveredCellChange | (cell) => void | — | Called when the hovered cell changes. |
className | string | — | Merged onto the outer <div>. |
Accessibility
- The outer element is
role="figure"with a visually hidden caption announcing the metric and matrix dimensions. - The live caption uses
aria-live="polite"— moving between cells announces the pair similarity without interrupting other speech. - When
interactive, every cell isrole="button"with anaria-labellike "Similarity between embedding 0 and embedding 1: 0.984" and a visiblefocus-visiblering keyed to--cb-accent. - Keyboard focus drives the same cross-highlight as mouse hover — the relationship is reachable without a pointer.
- The dim transition respects
prefers-reduced-motionand collapses to an instant swap. - Color is never the only signal — the live caption and per-cell
aria-labelalways report the numeric value, so the matrix stays legible for colorblind users and at every color-scale setting.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/viz/SimilarityHeatmap.tsx). The original was a lesson-runtime widget bundled with an inline 5×1 strip, a ranked-results fallback, and parsing of aVizDatumstream; the library extract is the matrix primitive — embeddings come in, similarity comes out.