K-Means Stepthrough

A scrubbable visualisation of the K-means clustering algorithm on a 2D point cloud. Each iteration is split into two half-steps: an "assign" snapshot where every point is colored by its nearest centroid (with optional dashed segments from point to centroid), and an "update" snapshot where centroids glide to the mean of their assigned cluster. The whole walk is precomputed so scrubbing the timeline is O(1), and iteration stops as soon as centroid movement falls below an epsilon.

Iteration 1, Assign points to nearest centroid.
K-meansiter 01 · assign
Customize
Data
3 clusters
3
Playback
step 0

Installation

npx shadcn@latest add https://craftbits.dev/r/k-means-stepthrough.json

Usage

import { KMeansStepthrough } from "@craft-bits/core";
 
<KMeansStepthrough
  points={[
    { x: -2, y: 2 },
    { x: 2, y: 2 },
    { x: 0, y: -2 },
  ]}
  k={3}
/>

Drive playback from outside if you want to sync with narration or a scrubber:

const [step, setStep] = useState(0);
 
<KMeansStepthrough
  points={points}
  k={3}
  currentStep={step}
  onCurrentStepChange={setStep}
  playing
  playSpeed={600}
/>

Understanding the component

  1. Precomputed snapshots. When points, k, initialCentroids, or maxIterations change, the entire walk is recomputed inside useMemo. Each snapshot carries the centroid positions, the per-point cluster index, the half-step phase, the iteration number, and a converged flag.
  2. Two half-steps per iteration. Snapshot zero is the first assign step, snapshot one is the first update step, snapshot two is the second assign, and so on. Stepping or scrubbing reads the active snapshot — no live algorithm runs during animation.
  3. Deterministic initialization. When initialCentroids is omitted, k points are sampled from points via a seeded LCG so SSR and every render agree on the starting state.
  4. Convergence. Each update step compares the new centroids against the previous ones. Once the total squared movement falls below 1e-4, the snapshot is marked converged and precomputation stops — even if maxIterations would allow more.
  5. Color palette. Cluster zero uses var(--cb-accent), cluster one uses var(--cb-warning), cluster two uses var(--cb-success), then var(--cb-info) and var(--cb-error), cycling after five clusters.
  6. Glides via SPRINGS.smooth. Centroids animate between snapshots using SPRINGS.smooth on cx / cy. Point colors cross-fade with a 0.3s tween. prefers-reduced-motion: reduce collapses every transition to instant, suppresses autoplay, and parks the timeline at the final snapshot on mount.
  7. Assignment lines. During "assign" snapshots, a dashed segment from each point to its centroid is drawn (toggle with showAssignmentLines). Lines are hidden during "update" snapshots so the centroid motion reads clearly.
  8. Controlled + uncontrolled. Both currentStep and playing accept controlled props paired with onCurrentStepChange / onPlayingChange, or the matching defaultCurrentStep / defaultPlaying uncontrolled variants. setInterval autoplay tears down via clearInterval on unmount.

Props

PropTypeDefaultDescription
pointsreadonly KMeansPoint[]required2D points to cluster.
knumber3Number of clusters.
initialCentroidsreadonly KMeansPoint[]seeded sampleStarting centroid positions.
maxIterationsnumber10Hard cap on iterations. Stops sooner on convergence.
currentStepnumberControlled active step. Pair with onCurrentStepChange.
defaultCurrentStepnumber0Uncontrolled initial step.
onCurrentStepChange(step: number) => voidFires whenever the active step changes.
playingbooleanControlled play state. Pair with onPlayingChange.
defaultPlayingbooleanfalseUncontrolled initial play state.
onPlayingChange(playing: boolean) => voidFires when play/pause flips.
playSpeednumber800Milliseconds between auto-played steps.
sizenumber320SVG side length in pixels.
showAssignmentLinesbooleantrueDraw dashed segments from each point to its centroid during assign steps.
transitionTransitionSPRINGS.smoothOverride the centroid spring.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • The root is role="figure" with an aria-labelledby heading and an aria-describedby summary written into a visually hidden aria-live="polite" region — screen readers hear the iteration number and phase on every step change.
  • The visualisation is read-only — no draggable centroids — so there is no keyboard interaction to enumerate.
  • Cluster identity is encoded by color but the summary repeats the iteration and phase, so users not relying on color still know where the algorithm is in the walk.
  • Animation respects prefers-reduced-motion: reduce: springs collapse to instant, autoplay is suppressed, and the component parks at the final snapshot on mount.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/viz/KMeansStepthrough.tsx). The source paired the visualisation with an Explore / Predict mode strip, draggable centroid interaction, narration heuristics, WCSS readout, randomize / run-to-end buttons, and a predict-mode question bank. The library extract is the pure stepthrough primitive — a precomputed assign / update timeline parameterised by points, k, and starting centroids. Mode strips, dragging, and curriculum-specific prediction harnesses belong in the consuming lesson, not the primitive.