A dame.is & pds.dad production bsky38.com

DRAFT: Add /race: the standings replayed over time, the movers, the bubble, who has led #78

Open dame.is
targeting main from deleted fork:race-page
Open opened by and assigned to dame.is
targeting main from deleted fork:race-page

The leaderboard says where everyone is. This says how they got there — the page an election desk keeps open on the night: a rank-over-time chart for the current top ten, votes per interval, who climbed and who slipped since yesterday, who is sitting either side of the cut and by how many votes, and who has held first place.

Nothing behind it is stored. The tally keeps no snapshots and there is no backfill, but every counted vote carries its createdAt, so the standings at any past moment are what you get by counting only the votes cast before it. server/race.ts reads the counted, not-opted-out votes once in createdAt order and sweeps them, ranking the whole pool at every point of the axis with a histogram rather than a sort — a subject's rank is one more than the number of subjects with strictly more votes — so a 150-point axis over a pool of thousands costs a few tens of milliseconds. Ranks are the leaderboard's own RANK(), ties sharing a number, and the sweep drains every remaining vote at the last point, so the end of every line is the pool's rank whatever a record's clock said.

It is a replay, and the page says so: this is the race as today's ballots tell it. A vote removed since is gone from every hour it once counted in, and a vote that only became counted when an earlier one was removed counts from its own createdAt. History here is "how the current votes arrived", not "what the leaderboard showed".

The axis runs in clock-aligned steps — ten minutes up to a day, the finest that keeps it under 150 points — from the first vote for anyone on the chart to now, or to the close once voting has closed. Not from the first vote of all: the fifteen hours before the announcement carry a trickle of test votes, and an axis that began there spent a third of its width on a stretch where none of the lines exist. Votes before the axis still count; the first point is read after them.

The window behind every "moved" number is the last 24 hours, or the whole race while it is younger than that, in which case everyone is a newcomer and nothing has climbed. Climbers and fallers are judged among the top 100 — a jump from 2,000th to 900th is arithmetic, not news — and a faller is listed only while still within the top 300. Ranks share numbers, and in the first hour of a race a single vote is enough for 70th place alongside a hundred others; by the next day the same vote sits 4,000th, a fall of thousands of places by someone who was never in the race, and sorting by places lost filled the list with them. Lead changes come from the same sweep: after each vote, only the subject it went to can have overtaken the leader, with the pool's own tiebreak (earlier first vote, then did) deciding a tie.

The charts are SVG drawn by hand at the container's width, in the site's three inks: the field in faint ink, the current leader in full ink, and whichever line the reader hovers or pins in the accent, drawn last. Movement is a filled triangle and a number, a newcomer the ink stamp — weight and inversion, never hue, because there is no green and no red here. Every axis label is built from UTC getters and every coordinate rounded before it becomes an attribute, so the server's chart and the browser's are the same characters and hydration touches nothing. The page re-reads the race once a minute while visible; the server memoizes one sweep per connection, keyed on the write counter and the minute, in a single slot rather than memoByVersion, whose map would keep a race per minute for the life of the process.

LIST_SIZE names the cut in $lib/ui for the race to read; the front page still carries its own 38s. "Race" joins the tabs on both bars, with a chart glyph in the set's butt-capped hand, and the README gains a section on the replay and its caveat. race.test.ts covers the axis, the lines against the pool's ranks, the lead, the window, the movers rules, the bubble, membership, and the memo; chart-time.test.ts the ticks.

Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com

Commits 3

Make the race card the chart, and open the race on the line you came for
Give the race a head-to-head, concentration, ballots, hours, a card and a way in
Add /race: the standings replayed over time, the movers, the bubble, who has led