This is the Rust K-Means compiled to WebAssembly and running in your browser. Nothing you generate leaves your machine; the only network call is the one that fetches the 26.5 KB .wasm file. Generate two-moons data with a random start and watch it fail, then switch to k-means++ and watch it not.
The fastest way to understand where K-Means works and where it gives up is to drive it yourself. Pick a distribution, set the controls, and hit Run K-Means: each Lloyd’s iteration animates live on the canvas while the inertia chart below tracks the objective falling. A pure-JavaScript K-Means runs on the same data and seed right after, so you can read the WASM-versus-JS speedup directly off the page.
The run I’d try first is two moons with random initialization. The crescents aren’t separable by straight Voronoi boundaries, so the centroids bisect both moons and the inertia chart flattens on a confidently wrong answer. Then switch the init to k-means++ and run it again. The seeding doesn’t fix the shape problem (nothing does, K-Means draws convex cells), but it’s the cleanest way to feel the difference seeding makes on the data where it can help.
Inertia per iteration
The controls
The distributions are sorted roughly from “K-Means handles this” to “K-Means can’t.” Gaussian and anisotropic blobs are the ideal case and the slightly-off case; the rest are there to break it. Concentric rings and the pinwheel spiral are non-convex, so K-Means slices them the wrong way every time. Two moons is the textbook failure. Uniform random has no structure at all and shows the algorithm inventing clusters that aren’t there.
| Distribution | What it tests |
|---|---|
| Gaussian blobs | The ideal case: spherical, well-separated clusters |
| Concentric rings | Non-convex; K-Means splits rings across the wrong axis |
| Two moons | Crescents; the classic centroid-method failure |
| Anisotropic blobs | Elongated clusters; sensitivity to aspect ratio |
| Uniform random | No real structure; watch it over-fit |
| Pinwheel / spiral | Strongly non-convex; dramatic centroid drift |
Points, clusters, max iterations, and speed do what they say. Init switches between k-means++ and uniform random seeding, which is the lever the two-moons experiment turns. Generate makes a fresh dataset without fitting; Run K-Means fits and animates; Step, Pause, and Reset let you walk a single fit one iteration at a time.
What’s running underneath
Run K-Means calls kmeans_fit_steps(xs, n, d, k, max_iter, seed, use_kpp), exported from kmeans_wasm.js and built from src/wasm_impl/src/lib.rs. It returns one flat Float32Array holding a header and a snapshot of every Lloyd’s iteration, so the animation is just a replay of states the Rust code already computed. The typed array crosses the JS-to-WASM boundary without a copy, thanks to wasm-bindgen’s typed-array support, which is part of why the race comes out the way it does.
The module is built with a single command:
cd src/wasm_impl
wasm-pack build --target web --out-dir ../../docs/wasm
After each run, the pure-JS K-Means fits the same data with the same seed, both are timed with performance.now(), and the speedup factor lands above the controls. The inertia chart under the main canvas plots the within-cluster sum of squares per iteration, updating as the animation plays; convergence is the moment that curve goes flat.