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.jsonUsage
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
- Precomputed snapshots. When
points,k,initialCentroids, ormaxIterationschange, the entire walk is recomputed insideuseMemo. Each snapshot carries the centroid positions, the per-point cluster index, the half-step phase, the iteration number, and aconvergedflag. - 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.
- Deterministic initialization. When
initialCentroidsis omitted, k points are sampled frompointsvia a seeded LCG so SSR and every render agree on the starting state. - 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 ifmaxIterationswould allow more. - Color palette. Cluster zero uses
var(--cb-accent), cluster one usesvar(--cb-warning), cluster two usesvar(--cb-success), thenvar(--cb-info)andvar(--cb-error), cycling after five clusters. - Glides via SPRINGS.smooth. Centroids animate between snapshots using
SPRINGS.smoothoncx/cy. Point colors cross-fade with a0.3stween.prefers-reduced-motion: reducecollapses every transition to instant, suppresses autoplay, and parks the timeline at the final snapshot on mount. - 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. - Controlled + uncontrolled. Both
currentStepandplayingaccept controlled props paired withonCurrentStepChange/onPlayingChange, or the matchingdefaultCurrentStep/defaultPlayinguncontrolled variants.setIntervalautoplay tears down viaclearIntervalon unmount.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
points | readonly KMeansPoint[] | required | 2D points to cluster. |
k | number | 3 | Number of clusters. |
initialCentroids | readonly KMeansPoint[] | seeded sample | Starting centroid positions. |
maxIterations | number | 10 | Hard cap on iterations. Stops sooner on convergence. |
currentStep | number | — | Controlled active step. Pair with onCurrentStepChange. |
defaultCurrentStep | number | 0 | Uncontrolled initial step. |
onCurrentStepChange | (step: number) => void | — | Fires whenever the active step changes. |
playing | boolean | — | Controlled play state. Pair with onPlayingChange. |
defaultPlaying | boolean | false | Uncontrolled initial play state. |
onPlayingChange | (playing: boolean) => void | — | Fires when play/pause flips. |
playSpeed | number | 800 | Milliseconds between auto-played steps. |
size | number | 320 | SVG side length in pixels. |
showAssignmentLines | boolean | true | Draw dashed segments from each point to its centroid during assign steps. |
transition | Transition | SPRINGS.smooth | Override the centroid spring. |
className | string | — | Merged onto the root <div> via cn(). |
Accessibility
- The root is
role="figure"with anaria-labelledbyheading and anaria-describedbysummary written into a visually hiddenaria-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 bypoints,k, and starting centroids. Mode strips, dragging, and curriculum-specific prediction harnesses belong in the consuming lesson, not the primitive.