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.json

Usage

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

  1. Pairwise computation. Given N embeddings of dimension d, the component builds an N × N matrix. For cosine, each cell is (a·b) / (‖a‖·‖b‖) — the diagonal is exactly 1. For dot, each cell is the raw dot product a·b; magnitudes matter, so the legend shows the per-matrix min/max.
  2. 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.
  3. 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.
  4. 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).
  5. Reduced motion. When prefers-reduced-motion: reduce is set, the dim transition collapses to duration: 0 — the highlight snaps instead of springs.

Props

PropTypeDefaultDescription
embeddingsreadonly (readonly number[])[]requiredN embeddings of dimension d.
labelsreadonly string[]numeric indicesAxis 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.
cellSizenumber36Pixel size of each cell.
interactivebooleantrueWhen true, hovering a cell highlights its row + column.
hoveredCellreadonly [number, number] | nullControlled hovered cell.
onHoveredCellChange(cell) => voidCalled when the hovered cell changes.
classNamestringMerged 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 is role="button" with an aria-label like "Similarity between embedding 0 and embedding 1: 0.984" and a visible focus-visible ring 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-motion and collapses to an instant swap.
  • Color is never the only signal — the live caption and per-cell aria-label always 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 a VizDatum stream; the library extract is the matrix primitive — embeddings come in, similarity comes out.