Sounds Like
Artist similarity tree · Last.fm data drawn on canvas
A simple idea, kept simple on purpose: name a band, see the bands that sound like it, keep going. The entire interface is a search box, a reset, and the tree: there is nothing to learn before it is useful.
The interaction stays as plain as the idea. Tapping a node branches it into its own matches and makes it the new center; dragging one into the dashed ring does the same by hand. The center always carries a Spotify player, so the graph is something you listen to rather than only read.
Searching reseeds the tree from scratch rather than appending to it. That is a usability call, not a technical one: a graph that accumulates every artist you have ever typed stops being a scene and becomes a mess.
Last.fm's similarity is listener overlap: artists whose audiences coincide, not artists that sound alike. For an indie band those are close to the same thing, and the results are good. For a band everyone has heard, they come apart, and I found that out by searching the most obvious band there is. Ask for artists similar to The Beatles and the strongest matches are the Beatles: the band's own members and their solo projects. True, and a dead end. No Beach Boys, no Who, no Fleetwood Mac, none of what you were hoping to find.
So the eight on screen are not the top eight. Twenty-five similars are fetched, the first three are kept, and the rest are sampled evenly across the tail. The top three keep the branch honest; the sampling gives it a chance of landing outside the direct match, which is where the thing you were actually looking for tends to be.
// Diversity sampling — mega-artists (e.g. The Beatles) return their own members
// at the very top by match, so a strict top-K is a family reunion. Keep the top
// few, then sample evenly across the tail to pull in the wider scene.
function selectDiverse(list, k) {
if (!list || list.length <= k) return (list || []).slice();
var topN = Math.min(3, k);
var picked = list.slice(0, topN);
var rest = list.slice(topN);
var need = k - topN;
var step = rest.length / need;
for (var i = 0; i < need; i++) picked.push(rest[Math.floor(i * step + step / 2)]);
return picked;
}
The interesting parts here are a canvas and two third-party APIs, which is usually the
excuse for having no tests at all. The seam that makes it testable is the proxy: every call
goes to /api/*, so the suite stubs
those three routes in the browser and never touches Last.fm, Spotify, or the rate budget.
Same input, same graph, every run, and it works from a clean clone with no keys.
Assertions read the app's own status line, "N artists · M links · seed: X", rather than waiting on the network. The render loop runs on requestAnimationFrame and never goes idle, so networkidle never fires, and load fires long before the first branch lands. The error paths are covered too: a 429 from Last.fm, a 500, and an artist that comes back with no similars at all.
The form had to justify itself. A force-directed graph is an easy thing to build and a hard thing to defend: it usually adds motion where a list would have done. It stayed here because the task is open-ended browsing rather than lookup, and because the layout itself carries information: what clusters, what bridges two scenes, how far you have wandered from the seed. If those had not been the questions, this would have been a list.
Keys never reach the browser. The Last.fm and Spotify credentials
sit in three Netlify functions behind /api/*, and responses are CDN-cached for five minutes, which protects the rate budget as much
as the secret. That one seam is also what lets the whole suite run offline.
Canvas, not SVG or a graph library. The layout is physics I wrote by hand: nodes push outward from the center and away from each other, links pull them back, and a minimum distance stops any two from stacking. The distance carries meaning as well as spacing — the further down Last.fm's list an artist came, the further out it settles.
A map, not a lineage. The graph can say two artists share an audience; it cannot say who came first. It is honest about being a map of what listeners group together, and nothing more.