Picture this : you're running a training program with around 40 athletes. Each week, they take part in a test and receive a score. The goal? Measure overall performance and track improvement over time.
To rank them at the end of the program, it's straightforward—just compute the average score for each athlete across all weeks, sort the list, and you’ll know who's performing best overall.
But what if the testing is still ongoing? How do you measure who’s improving week over week?
A simple solution: use a moving average. For each athlete, compute the moving average of their weekly scores and visualize it. This lets you clearly see trends—who's on the rise, who’s slipping, and who's staying consistent.
With this approach, you get a fair, data-driven way to monitor progress and effort in real time.
I thought the hardest part would be the logic behind the metrics—moving averages, trends, rankings.
Turns out, the trickiest problem was… color.
At first, it seemed trivial. I hardcoded 20 distinct colors into the app. If there were more than 20 athletes? Just cycle back through the list. No big deal—I never had more than 20 in my own use cases anyway.
So I shipped it. Posted the link. Shared it with friends.
The next day, someone plugged in 26 athletes.
Boom. The visual bug stood out immediately—colors repeating, impossible to tell who’s who. The app’s clean UI was now a confusing mess.
Alright, before this app’s nascent reputation gets ruined by this obvious visual bug, I need to do something, quick.
I searched “JavaScript library for generating distinct colors.” Two promising options came up:
distinct-colors (npm package)
uniqolor (browser-friendly JS library)
distinct-colors looked ideal—based on k-means clustering (or force vector repulsion algorithms) in color space, producing perceptually distinct values.
But my app was a single .html
file. No bundler, no build system. Importing an npm package with all its dependencies felt like overkill.
Still, I spun up a quick Node project just to see what kind of colors it generated.
It’s quite satisfying — the colors are distinct enough.
Maybe somewhere deep in the documentation there’s a way to quantify how 'distinct' each color is (because some of the generated colors still don’t feel distinct enough).
There’s also one more problem: I need these colors to be sufficiently distinct from both white and black, since those are the possible background colors of my web app — in light mode and dark mode.
Next, I tried uniqolor.
Different approach: instead of generating a set of distinct colors, it hashes each string input to a consistent color. Great for things like avatars or usernames, but not ideal here. There's no guarantee two inputs won’t collide into similar-looking colors. And I needed visual clarity above all.
So I passed on both.
Then comes the third alternative: what if I just make my own JavaScript library to generate distinct colors?
I asked ChatGPT to make one — something quick and rough.
It gave me a few JavaScript functions.
I quickly integrated them into my app, and that was it. Let's see and verify the colors.
Well… they are distinct. It’s basically a simple rainbow, from red to blue, with predefined color "steps."
Is it distinct enough? Not really. The "color distance" between the steps still feels pretty short.
Now I find myself appreciating the distinct-colors
npm package again. It doesn’t just generate a simple rainbow — it creates a truly random collection of colors. Some of them even include lots of “non-rainbow-ish” tones with low saturation.
Alright, forget about this.
I needed a better source of visually distinct colors. What if I started with something humans had already curated?
Eventually, I found a GitHub repo that scraped the top 1000 color palettes from ColourLovers.
Each palette had ~5 colors, giving me ~5,000 in total.
After deduplication, I ended up with ~4,842 unique hex values.
But were they truly distinct to the human eye? I wasn’t sure. So I searched again—this time for an algorithm to measure perceptual color difference.
That’s when I stumbled upon CIE76 Delta E. It compares two colors in Lab color space and outputs a numerical "distance" between them.
I didn’t fully understand the math, but I asked ChatGPT for a JavaScript implementation anyway. It worked.
Here's what I ended up with:
Store all athlete names in an array.
For each name, calculate a hash.
Use that hash to index into the list of 4,842 colors:
const colorIndex = (hash % 4842);
let colorCandidate = colorArray[colorIndex];
Before assigning the color, test it:
Is it too similar to white or black (Delta E < 40)?
Is it too close to any already assigned color (Delta E < 10)?
If it fails, increment the hash and try the next color:
while (
isTooSimilar(colorCandidate, "#ffffff") ||
isTooSimilar(colorCandidate, "#000000") ||
pickedColors.some(c => isTooSimilar(colorCandidate, c))
) {
hash++;
colorCandidate = colorArray[hash % 4842];
}
If it passes the test, assign the color and move on.
The thresholds (10 for inter-color distance, 40 for white/black contrast) were chosen purely by feel. No science—just trial and error until the UI looked good.
This solution works well enough for now.
But I’m aware of its one major flaw: with a very large number of athletes, the loop might struggle—or even hang—trying to find unused, sufficiently distinct colors.
Let’s just say I’m… hoping that won’t happen.
But wait!
You know that classic frontend dev joke—fix one thing, and something else (that was working perfectly fine before) suddenly breaks? You fix that, and then the new thing you just fixed breaks again. It’s like playing whack-a-mole with bugs.
Well, that was me.
I was chasing down a visual bug that only appeared when n
was large. After getting it to behave, I realized that my old use case—when n
was small—was now broken. Classic.
So, I cooked up a quick workaround. Here’s the approach I ended up trying:
Generate a new color using the algorithm I’d previously implemented.
For the next color, instead of generating a new one, take the complement (opposite side of the color wheel) of the last generated color.
For the third, generate a new color again.
Rinse and repeat.
Why this strategy?
My theory is that in the pool of ~4000 colors I was working with, too many were perceptually similar—similar enough to pass a Delta E threshold, but not distinct enough to be satisfying visually. I tried increasing the Delta E threshold, but the results still weren’t quite right.
So, I thought—we need to expand that pool of ~4000 colors by including their complements to better guarantee color distance.
Now, instead of picking n colors from the ~4000 available, we only take n / 2 colors and generate the rest by complementing them one by one.
Does it work?
For now? Yeah. It’s good enough. I guess.

P.S. To be honest, neither my use case nor my friend’s is actually about athletes. But the analogy still (sort of) works.