Pushup Tracker
푸시업·풀업·스쿼트·플랭크·케틀벨·달리기 기록, 목표·연속·친구와 리그, 그룹·프로그램 제공, 무료 브라우저
비공식 BWF 배드민턴 시즌 트래커, 토너먼트별 사각형 크기로 규모 표시, 선수 진행 단계별 색칠
An unofficial BWF badminton season tracker: one square per tournament, sized by how much the tournament weighs and filled by how far the player got.
스포츠·피트니스JavaScript · HTMLGitHub @carefulCamel61097온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗It runs there. Nothing to install.
| Seasons | a career, one row per season |
| Compare | two honours boards facing each other |
| Tournament | whatever BWF has on this week |
| Winners | every season's biggest titles, as faces |
An unofficial fan tool for looking at a badminton player's whole career: one row per season with the most recent at the top, one square per tournament within it, each a gauge showing how far they got and sized by how much the tournament weighs.
All data comes from BWF and belongs to them. This is not affiliated with, endorsed by, or connected to BWF. The API it reads is undocumented and unofficial and can change without notice.
No build step, no dependencies, no package.json. It does have to be served over http://
rather than opened as a file — ES modules and the request to BWF both refuse a file://
origin.
node serve.mjs # http://localhost:8090
Requires Node 24 for the global WebSocket the test harness uses.
Search a player by name — or pick one out of the top ten of any discipline — and their career stacks up as one row per season, newest first. The heading is the player: BWF's own photograph, their flag, their name, age, world ranking and place in the Race to Finals. Within a row, one square per tournament in chronological order. Each is a gauge: it fills from the bottom by how far the player got, ramping green (title) to red (first-round exit), and the label inside says the same thing in words so nothing is carried by colour alone.
Where two tournaments in one season tidy to the same words — January 2021 ran the YONEX Thailand Open and the TOYOTA Thailand Open a week apart in the same Bangkok bubble — the sponsor is handed back, because that is what BWF is distinguishing them by. Only there; everywhere else the sponsor stays stripped.
Seasons and tournament levels are both filtered with toggle buttons — a chip per season and a chip per level. The levels nobody filters by, which is mostly pre-2019 category ids this project has no name for, sit behind one "N more" menu of checkboxes so they do not crowd out the ones that matter.
A chip that is on is filled in BWF red, here and on the compare grid and in the bracket bars. These filters default to all on, so both rows open red — which was the argument for leaving them neutral, and it lost to the bigger one: the same affordance has to look the same on every page, or a reader learns the red pill in one place and the grey one in another. Team events are the exception and stay a dashed outline, because they are not just another level. The tournament page's own draw filter stays neutral too, since it sits directly under the day bar and a second row of red there reads as one control rather than two.
How far they got is measured against that draw's real ladder, fetched per tournament: a quarter-final of a 64-draw fills 3/6 and a quarter-final of a 32-draw fills 2/5. Qualifying does not count towards it, and a round robin that is the main stage does. The strip renders before those sizes arrive and corrects itself when they land, so nothing waits on them.
Squares are sized by tournament weight, which is settled in HANDOVER.md Part 2 against
BWF's own player-commitment rules — Super 750 is the line above which entry is compulsory,
so that and everything above it is full size. The box shrinks inside a fixed 52px slot, so a
lighter event reads as more air rather than as a narrower column, and the round label never
scales below 9px.
Singles or doubles is the only other choice, and the toggle appears only for players who do both. A doubles season is not filtered by partner: every doubles tournament that player entered is shown, whoever they played it with.
The Olympics are there, under their own level. BWF files them as category 20 — the same id
as the World Championships — and spells their draws and rounds out in full
("Men's Singles", "Quarterfinals") where the World Tour uses codes, which is a good way to
lose them entirely. See HANDOVER.md Part 2.6.
⚠️ A tournament that is still on says where the player is, not how they finished, and
the strip says the same. Somebody in the final of an event being played this week reads
F, hovering "In the final" — not the title they have not played for yet. The same word
means the opposite thing once the event is over: BWF files a handful of finished
tournaments as "Final" rather than "1st", and there the one who lost no match won it.
The record is identical either way, so the only thing that separates them is whether the
last day has passed. #now=YYYY-MM-DD pins what the page thinks today is, which is the
only way to see finals day without waiting for one.
⚠️ A finished season is stored and never asked for again. A career is one request to BWF per year, up to twenty-one of them at a third of a second apart, and twenty of those answers are settled history: 2012 is over and will read the same in 2030. So the seasons are kept in the browser with no expiry, and only the season being played now is re-fetched. Coming back to a career you have already looked at costs one request instead of twenty-one.
What is kept is the parsed season rather than BWF's payload, which is nine to ten times larger and never read twice. When the space runs out the oldest quarter is dropped and the newest kept, so the careers you keep returning to are the ones that stay.
⚠️ Nothing here is sent anywhere. It is your browser's own storage, on this site's
origin, and it never leaves the machine. If BWF corrects an old result and you are still
being shown the old one, BST.seasonStore.forget() in the console drops the lot; the next
visit fetches it again.
⚠️ A team tie is recognised by its draws, not by BWF's category. The Asian Games team
competition is filed under category 1, the Commonwealth Games team under 74, the East Asian
Games team under nothing at all — none of them the team ids — so the only thing that holds
still is that a tie names its draws bare Singles and Doubles, with no gender. Ties are
filed under the team level whatever BWF said, so one toggle reaches all of them.
⚠️ The levels BWF ships no name for start switched off, behind the "more" button, with one click for all of them. Which ones those are is derived: a level stays on if anything it holds is a senior individual event — category 1 alone holds the 2017 World Championships, two Asian Championships, the Asian Games and the Commonwealth Games as well as junior events.
Tournament, Seasons, Compare and Winners, on a tab bar under the player's name, and in that order on purpose (10 Oct 2026). Tournament leads because it is the one page that is about now rather than about a player you have to go and find, so it answers something the moment it opens; the other three run from the narrowest question to the widest — one career, two careers, everybody. Left and right walk the same order, because a keyboard arrangement that disagreed with the one on screen would be a second thing to learn.
⚠️ Winners opens on its Year view (20 Oct 2026), which draws the current season rather
than twenty of them: it is the only one of that page's four views that answers something the
moment it opens. Links already in the wild that carry no wv therefore land on Year rather
than on the pyramid — the cost of changing a default, and why wv=board is written into
every link the page makes from the pyramid.
Seasons is the strip and is still where a player link lands. Compare holds the grid and the honours board, either of them for one player or two, and when there is only one it draws an empty dashed seat beside them — a page called Compare should look like one before anybody has been chosen. Tournament and Winners are not about a player at all and work before you have searched for anybody.
The seasons again, read the other way round, on the Compare page. Rows are
still seasons. Columns are levels, not tournaments: each level gets a block of slots,
and a season's results at that level fill it left to right, best first. Four Super 1000
slots read W W W SF for Shi Yu Qi in 2025 and F QF R16 R32 in 2026. Unplayed slots pad
the right-hand end of the block.
Every cell is the same size and flooded with one colour — no labels, no weight sizing, no partial fills. The difficulty is in which block a cell is in and how far left it sits, and cells butt together so runs of the same result merge into one shape. A zoom slider sets the size; it is a viewing preference and stays out of the link.
Blocks run hardest-first, and global before continental at each height — Olympics, Worlds, Continental Games, Continental Championships, Tour Finals, Super 1000, Super 750, Super 500, Super 300, Super 100 — then the category ids this project has no name for, grouped as "Unmapped" on the end. A chip per level switches it in or out. The first four read as two columns of the same shape: every four years and every year at world level, then the same pair one circle smaller. Below Super 100, the junior circuit and the team events are not in the grid at all.
A block is as wide as the most anyone on screen played at that level in a single season.
That is measured rather than declared, because the declared answer moves: four Super 1000s
in 2026, five in 2027, and five in 2021 because that January ran two Super 1000 Thailand
Opens back to back in the Bangkok bubble. The Super 750s grew from five events to six after
2019, so a six-wide block means an empty slot in an old row and a skipped event in a recent
one — see HANDOVER.md 2.9.
One thing does not go by the date it was played: the season-ending Finals belongs to the season it concludes. BWF files the COVID-delayed 2020 edition under 2021, which put two Tour Finals in one row. It is the one event there is exactly one of per season, and BWF's own name still says which edition it is.
Compare with… loads a second whole career and puts it beside the first, sharing one set of blocks and one set of rows so the two are readable against each other, with each player's photograph, flag, age, world ranking and Race standing above their grid. The comparison is in the URL, so it is a link.
See HANDOVER.md Part 1.1b for what this layout gives up — a cell no longer says which
tournament without hovering it — and 2.9 for why the first attempt, a column per
tournament, did not survive contact with 2021.
The other reading on the Compare page. This one has no seasons in it at all: one row per level, hardest at the top, holding every result in the career that reached at least a semi-final — the bar moves to QF+, F+ or W. Everything below it is simply not drawn.
The rows are different sizes, and that is the whole idea. A full rung up has φ times the area of the one below, so an Olympic square covers about thirty Super 100 ones and a career reads as a shape rather than a list. It is the golden ratio applied to area rather than to side: nine rungs at φ per side would make the top row 47 times the bottom, which is one square and some dust.
Four of the gaps are half a rung — √φ on the area, 1.272 — rather than a whole one:
| step up to it | |
|---|---|
| Olympics | half, from the Worlds |
| Worlds | full, from the Continental Games |
| Continental Games | half, from the Continental Championships |
| Continental Championships | full, from the Tour Finals |
| Tour Finals | half, from Super 1000 |
| Super 1000 → 750 → 500 → 300 → 100 | full, every time |
Read down the first column, the pattern is: continental to global is always a full step, and every-four-years over every-year is always a half one. The Tour Finals gets a half step over a Super 1000 for the reason most people would give — it is not worth that much more.
This is what makes room for the continental events. They used to share the Super 1000's rung, which was the only way to give them full weight without breaking the Super ladder: giving them a step of their own pushed every Super below them down one, so Super 1000 → 750 was a single step while 750 → 500 was a double, and the official five-level ladder came out unevenly spaced for a reason that had nothing to do with the Super events.
Half steps answer that without the sharing. The Continental Championships and the Continental Games each get a rung, and the five Supers still sit a full rung apart all the way down — because a rung is now counted in halves, so two new rungs fit above the Tour Finals without displacing anything below it.
⚠️⚠️ And the top of the ladder does not move. An Olympic gold is 6.854 Super 750s, exactly
what it was before two tiers were inserted beneath it. Full steps all the way up would have
made it φ&sup6; = 17.9, which nobody believes. HANDOVER.md 2.10 has the table.
An empty row is a claim too, and there are two different ones. Hovering says which: 26 entered, none at QF+, or never played at this level.
Compare with… mirrors the board about a centre line — left player reversed and right-aligned — so the best results of each meet in the middle and the two shapes face each other. Shi Yu Qi against An Se Young is the case it was built for: his Super 1000 row runs out to yellow, hers is a wall of dark green — and at W his Olympic row is a ghost while hers holds Paris.
The bar is in the link because it is part of what the board says. The zoom is not.
The board is deliberately not what you land on. It is superb for the very best players and it collapses for everyone else — a working professional at world #22 is seven squares and eight empty rows, which is true and looks like a bug. The strip is the only view that always has something to say, so that is the one a player opens on.
Export, beside the zoom, draws whichever of the two views is up — the grid or the board, one career or two — to a PNG at twice the screen's density, with the link and a legend on it. Download it, or Copy it straight to the clipboard where the browser allows that.
⚠️ No range picker here, unlike the Winners board. That one exports a span of seasons because "2011 to 2016" is the shape of the claim somebody posts from it. A career is not a span, so this one draws what is on screen: the era switch, the level chips, the round bar and both players if there are two. Switch Super 500 off and it leaves the picture; raise the board to W and the picture is titles only.
Drawn from the model like every other export, at its own fixed size rather than at the zoom slider's. The grid's squares are 20px in a poster — bigger than the file started at, because at 16 a one-slot block was narrower than the three letters naming it and Olympics, Worlds, Tour Finals and both continental blocks all lost their labels off the front of the band.
⚠️ The result ramp exists twice — as --res-* in styles.css for the page and as
RESULT_COLOURS in poster.js for the canvas — because the page paints with CSS and the
export paints with canvas and there is no way to hand one to the other without a build step.
The suite reads the computed colour off a drawn cell and holds it against the table, so the
two cannot drift quietly. The #1 on a title and the notch on a translated square are drawn
too, by the same rules and at the same gates: the mark only appears once the square is 16px,
because below that the glyphs stop being a word and start looking like dirt.
⚠️ No flags on this one. Measured: BWF serves player photographs from
img.bwfbadminton.com, which answers a CORS request, and country flags from
extranet.bwf.sport, which does not — so every flag failed to load, printed a CORS error and
drew nothing. The Winners board's flags come out of the harvested file and are on the
photograph host, which is why those are fine and these are not. The country is in the line
under the name anyway.
The file is named for the people in it — badminton-grid-shi-v-an.png — because a comparison
and a single career would otherwise land in a downloads folder as the same file and the second
one would silently become (1).
Whatever is on, without picking it. No player card sits above it, the same as the winners page: this page is about what is on court, not about whoever was last looked up, so a career here would answer a question nobody asked and push the day down the screen. The search box stays, because it is how you leave for a player. One 1.8 KB call to BWF says which tournament has live scores, which one just finished and which is next; the page shows the right one and says which of the three it is.
⚠️⚠️ One BWF route will not answer the deployed site, and the Tournament page routes around
it. vue-tmt-schedule — "which tournament is live, which just finished, which is next" — is
the single route on their API that does not reflect the request origin into
Access-Control-Allow-Origin, so from https://carefulcamel61097.github.io it fails CORS while
every other route the app touches answers 200 with the origin echoed back and Vary: Origin set.
It works from http://127.0.0.1, which is why the whole test suite is blind to it. Measured 6
September 2026 on every attempt, cache-busted.
loadSchedule therefore asks for it and, when it fails, rebuilds the same three slots out of
vue-grouped-year-tournaments, which does answer and carries everything pickTournament reads.
The primary is still tried first on every load — the day BWF fixes the route the page goes back
to their own answer without a release — but with the retry turned off, since a route that refuses
this origin refuses it every time.
The rebuilt schedule is filtered in two ways BWF's own is not: to tournaments on the board (the
year list is all 308 events) and past cancelled ones, on status.code rather than on the name.
See scheduleFromYear.
⚠️⚠️ Junior and para events are off this page, as they are off every other one here
(10 Oct 2026). The standing reason is the one in JUNIOR: it is not the same sport being
measured. The week of 5-17 October 2026 is what made it urgent, and the two events fail in
opposite directions. The World Junior Team Championships cannot be drawn at all: measured
live, its ties come back as draws named "Relay - 1-8", every match has an empty player
list, and tournaments/draws answers with nothing — so the order of play would show two
flags and no names, and the bracket would have nothing to lay out. The World Junior
Championships is the opposite: MS/WS/MD/WD/XD with real players, rendering perfectly, with
nothing on the page to say it is the under-19s.
It was already true on the deployed site, by accident: scheduleFromYear keeps only what
scheduleGroup can place, and a junior event has no catLogo. So localhost and the live site
disagreed, and neither had the rule written down. onSeniorCircuit is now one rule used by
both. Senior team events stay — Thomas, Uber and Sudirman are events people watch and
their ties carry real players.
A day bar runs across the tournament's dates and opens on today — or on the last day if it is over, the first if it has not started.
⚠️⚠️ A day you choose is yours. While something is being played the page re-asks BWF every minute, and that check used to re-derive the day from today's date: sitting on Friday's order of play while Saturday was being played, the page silently jumped to Saturday after up to a minute. Reported on the Arctic Open, 10 October 2026. A day you picked, or a link named, is now held until you pick another or the tournament itself changes under you. The check also stops asking for a day already played, which can never answer anything new. Below it the order of play, laid out as a grid: one column per court, one row per moment in the day. Two cards level with each other were on court together. When there is only one court, or the order of play is not out yet, it falls back to a plain list.
⚠️ A row is a moment, not "nth on this court". It was positional until the China Masters of 4 September 2026, where court 3 held two matches all day — one at 11:00 and one at 19:00 — and both, being first-and-second on their court, were drawn level with the morning on courts 1 and 2. The evening match sat beside one that had finished eight hours earlier.
Which is not the same as sorting the day by the clock, and that is why this is harder than it looks. Only some times are real: BWF publishes a start for the first match of every session — Starting at 10:00 AM, or Not before 5:00 PM when a session opens mid-day — and then strings a flat 50-minute estimate through the rest, which on some courts runs backwards. So the day is cut into sessions at its published times, those are placed against each other in order, and inside a session position on court is still the row. A court that opens its own session gets a row to itself and the columns beside it are simply empty there. On a day where every court starts and breaks together this lands on exactly the grid the old rule gave. Starts within a quarter of an hour of each other are one moment — courts opening ten minutes apart is a stagger, not a different part of the day.
⚠️ However the two times were arrived at (15 Oct 2026). For a year the grace only held between two published starts: a session would not share a row with an estimate, on the grounds that an estimate is a fiction. The Denmark Open showed what that costs. Courts 1 and 2 ran one unbroken chain of estimates from 9:00 to 19:00, and court 3 — which had fourteen matches to the others' thirteen — opened an afternoon session at Not before 1:45 PM. There was no published start anywhere on the day to share with, so 13:45 took a line of its own with two empty cells beside it, and every row below it paired court 3's 14:35 with the others' 14:00. The reader asked, twice, what the empty cells meant. Fifteen minutes is fifteen minutes: 13:45 and 14:00 are one moment, and court 3's extra match now falls where the day really has it — alone at the foot of its column, when the other two courts are finished. A published start is still preferred where there is one to share with.
So an empty cell means nothing started on that court at that moment, and the court is usually mid-match rather than idle. At the end of a column it means that court is still playing when the others have finished.
Estimated times are marked ≈ and carry BWF's own wording, Followed by — or Court and time TBA, which is a fourth form and was read as a published time for five weeks (14 Oct 2026). It says in so many words that the time is not fixed, and a court name arrives beside it anyway, so the row looks exactly like a published one: the Denmark Open's last two matches on each court were printed as a flat 19:00 and 19:50 with no mark on them. An anchor is now BWF's own published wording and nothing else — Starting at or Not before — rather than everything that is not Followed by. A wording this does not know is drawn as an estimate, which errs towards saying less than BWF rather than more.
⚠️⚠️ A match BWF has emptied out still says what it can. Measured live at the Denmark Open
on Wednesday 14 October 2026: the four matches that had finished that morning came back
with no players, no countries, no position on court and no wording — while keeping their full
score, their winner, their duration and their court. The day before, every finished match was
whole, so it heals overnight; and the bracket route answered the same hollow rows, so there was
nowhere on BWF to read the names back from while it lasted. The only field that marked them was
reliability: 2, which nothing in this project's whole recorded corpus had ever carried.
It broke the page in two ways, both of them reported from the stands. The grid collapsed into one column, because the order of play is only drawn when every match in it has a court and a position, and four rows out of forty-two had lost theirs — so eight courts' play came down the screen in a single stack. The position is recoverable, because the array BWF sends is already in the order of play and the hollow rows sit in their right place in it: a missing position is now one past the row above it, or counted back from the first published position below it. Court 3 that morning read (blank), (blank), 3, 4, ··· and comes back 1, 2, 3, 4.
And a finished match was drawn between "TBD" and "TBD", which is the worst thing the page could say about it: a full scoreline beside two players still to be decided. A match nobody has played yet still says TBD, because there an empty side really is undecided. A played one says Not published, and says why if you hover it. The page also remembers: it re-asks BWF every minute, so a match hollowed out under the reader's eyes was on screen with its names a minute earlier, and those names are kept beside the new score. That cannot help a page opened cold in the middle of such a day, which is why the card has something honest to say for itself as well.
Each card is a scoreboard: flag, seed, name, and that side's own games as badges with the
ones they won picked out. A seed is written [1], never bare — a lone numeral beside a name
on a scoreboard is the one thing there that could be read as something else. The winner's
name is bold, the loser's dimmed. A walkover or a
retirement is marked on the side it happened to — the one that lost — because both happen
and a walkover has no score at all to print.
Click a match to star it. Starred matches stay lit and the rest recede, so a handful of them read at a glance across a whole day. There is a Starred only filter, a count and a Clear, and the stars are remembered between visits. Nothing is dimmed until you have starred something — a uniformly grey page looks broken rather than filtered.
All on the day bar loads every day of the tournament at once, one heading per day.
While a tournament is on, the scores are re-checked every minute and anything that moved is
marked for a few minutes after — and immediately when you come back to the tab, which is when
it matters. Refresh asks now. Times are given at the venue and, where it differs, where
you are. #now=YYYY-MM-DD pins what the page thinks today is, useful for seeing what finals
day will look like without waiting for one.
The other reading of the same tournament. The order of play answers what is on court today; the draw answers where is this all heading, which no single day's fixtures can. Order of play / Bracket switches between them and the choice travels in the link.
It is the predecessor's map view: feeders on the left, the Final on the right, elbow connectors between, every card sitting at the midpoint of the two that feed it. Positions are computed rather than walked —
centre(c, r) = (r + 0.5) · 2^c · SLOT SLOT = card height + gap
left(c) = c · (card width + connector width)
— so each column is a doubling of the one before it.
Folding away the rounds that are over. By the quarter-finals a full bracket is mostly empty space: the spacing law means every round doubles the gap between its cards, so four quarter-final cards sit sixteen slots apart because they still have to line up with thirty-two first-round matches nobody is looking at any more. Hiding the early columns would not have helped — the gaps are the geometry, not the drawing. So the tree is re-laid out from the round you pick, which becomes the new column zero and puts its cards one slot apart again. It stays a real bracket, connectors and all, just a smaller one:
| Show from | Cards | Segments | Canvas |
|---|---|---|---|
| All | 63 | 124 | 1548 × 1906 |
| R32 | 31 | 60 | 1290 × 978 |
| R16 | 15 | 28 | 1032 × 514 |
| QF | 7 | 12 | 774 × 282 |
From the quarter-finals the whole rest of the tournament is on screen with no scrolling at all. The default follows the tournament, like the day bar: it opens on the earliest round that still has a match to play, and stops at the quarter-finals however finished the draw is, because one card is not a bracket.
A draw is one discipline by definition, so the bracket picks exactly one rather than
filtering several, and it says the field size beside each — MS 64, WS 32. Both bars are
pickers rather than filters — exactly one draw, exactly one round — and the chosen chip is
filled in BWF red, which is what a chosen chip looks like everywhere now. The draw ids
come from BWF's own list rather than from counting: at a tournament with qualifying they
run 2, 4, 6, 8, 10, and asking for 1 gets you the men's qualifying draw, which is a
real payload quietly answering a different question.
Byes are drawn, not invented. A first-round cell with one side filled and the other empty is a player already through, not a fixture — it gets a dashed card reading Bye rather than "v TBD". This is not a doubles curiosity: the men's singles at the Pontianak Indonesia Masters is a 64 draw with sixteen of them.
A doubles pair is written SURNAME / SURNAME, because four names do not fit a card and one
pair is one competitor; singles keep the full name, and hovering any card gives the full
form either way. Clicking a card stars the match — the same star the order of play
uses, on the same match, because it is the same match.
Scrolling is the browser's. The predecessor drove pan and zoom from pointer events and
recorded two traps it cost: setPointerCapture retargets the follow-up click, so cards
never received it, and preventDefault() on pointerdown can suppress the click entirely.
All of that exists so dragging and clicking can coexist. Here the fold does the job custom
panning was there for, so there is no drag, the click is an ordinary click, and none of
those traps can come back.
The Winners page's first view, and what it opens on: one season, a column per tournament left to right in the order they were played, and three rows — the champion, the runner-up, and the two who lost in the semi-finals. The pyramid below simplifies twenty seasons so they can be read side by side; this is the other half of that trade, one season at the depth the simplification gave up.
⚠️⚠️ Size means how far they got, and nothing else. The winner of a Super 500 is drawn exactly as large as the winner of the Olympics. The question this view answers is how far did each person get at each event, and a size channel meaning two things at once would answer neither — which is why the level moves to the column header, the one place it is not competing with anything: a gold numeral for the World Tour rungs sized by what the tier is worth, and the same two marks the pyramid uses for the two prizes that are not a number, the rings for an Olympics and the cup for a World Championships. The Tour Finals, a continental championships and the whole Superseries era get a word instead, because BWF never gave those tiers a number to draw.
A cell is one competitor and a column is two cells, because the semi-final row holds two of them. In the doubles draws a cell holds the pair split down the middle, exactly as a square does on the pyramid, and the pair is one competitor — one cell, one row in the ledger.
Super 500 and above, with a chip per tier and the same five continental chips the pyramid has. ⚠️ Which costs nothing before 2018: four old tiers became five and Super 500 is the one nothing maps to, so in the Superseries era this floor lands on exactly the pyramid's own set. A Super 300 chip is where that stops being true and the era question has to be answered, which is why there is not one yet.
⚠️ Every tier chip is on by default, because nothing on this view is a share: there is no denominator to protect, so there is no reason to start with a column hidden. The pyramid defaults its own tiers differently for exactly that reason — its score is a share, and a denominator the reader did not choose is a number they cannot trust.
⚠️⚠️ The continental chips are the pyramid's own, and they stay Asia and Europe. They are literally the same control, in the same row, and a view that silently counted a set the chips on screen did not show would be lying; a set that flipped all five on when you arrived here would change the pyramid's score behind your back when you went away again. One control, one meaning, on all four views. The visible consequence: a 2026 board opens on 24 of its 27 tournaments, with the Oceania, African and Pan Am championships one click away.
Third and fourth are in order only where a match decided it. The Olympics plays a bronze play-off, so its two semi-final losers really are a third and a fourth and are numbered. A tour event does not, and neither do the regional Games — measured across every Asian, European, Pan Am and African Games from 2018 to 2023, all five disciplines, not one play-off between them — so there the pair share third and no order is implied.
⚠️ A medal can be won without a match. Carolina MARIN tore a knee ligament in her Paris
2024 semi-final and withdrew; the women's singles bronze play-off was never played and
Gregoria Mariska TUNJUNG took the bronze by walkover. BWF has no 3/4 row for it and nothing
in the payload tells a walkover bronze from a shared third, so the page draws them level and
the harvest logs it.
An empty column says which kind of empty it is. A season drawn in the autumn is mostly tournaments not played yet, and that is not the same as a tournament with no result at all. ⚠️ And no result is not always a gap in the records: fifteen of 2020's twenty-one have none because they were cancelled. Three continental championships of 2014 are the other case, played and missing from BWF's own archive. Nothing in the data tells those apart, so the page says the one thing it can prove — that there is no result — and leaves the reason to a reader who knows the year.
Click a face to follow one competitor across the season, the same gesture and the same set as a square on the pyramid, a bar in the eras band and a face on the score chart.
Under the board, one row per competitor who reached a semi-final, with how many they won, lost the final of, and lost the semi-final of. It is the answer the board can only imply — a face repeated down a row says a name over and over, and the ledger is what that repetition means. Same argument the dominance band makes under the pyramid, and a row is the same pick a face is.
⚠️ There is no export. poster.js is built around a column being a season, and here a
column is a tournament; a button that silently saved a picture of the pyramid instead would
be worse than no button, so it is hidden rather than left to mislead. A poster of one season
is its own piece of work.
⚠️ No quarter-finalists. Four more cells a column is the lesser objection; the real one is that a Super 500 quarter-final is not a notable result, so a fourth row would pay width and another day's harvesting for noise.
One column per season, oldest on the left, each a pyramid of the titles that mattered — every square the face of whoever won it, sized by the same ladder the honours board uses. Men's and women's singles, 2007 to now.
The Olympics and the World Championships share the top row. Every season holds one or the other and never neither, so they are the same rung of the calendar even though they are not the same prize. (2021 held both — Tokyo was postponed into the same year as the Huelva Worlds — so that row simply holds two.)
Every row holds two tiers. Below the summit: the Continental Games with the Continental Championships, the Tour Finals with the Super 1000s, and the Super 750s alone on the base. Four rows rather than six, because six rows of faces is a column too tall to read as one picture across twenty seasons — and pairing the Tour Finals with the Super 1000s fixes a row that was always slightly absurd, holding exactly one square every season.
⚠️⚠️ Every square on a line is the same size, and the gold ring is the ranking. Two sizes on a row of faces read as a layout accident rather than as a ranking, which is why the Olympics has been drawn at the Worlds size with a ring since the board was built. With three rows holding two tiers, that exception became the rule: the Olympics, the Continental Games and the Tour Finals wear the ring, and the Worlds, the Continental Championships and the Super 1000 are the plain case beside them. Size still ranks between rows, and the honours board on the Compare page still ranks every tier apart — that is a claim about worth, and this is a row of portraits.
A row reads hardest first, not oldest first, so the Tour Finals leads its row despite being played in December. The continental row goes by confederation instead — Asian Games, Asian Championships, European Games, European Championships — because two titles from one confederation are the same circle of countries four years apart and belong beside each other.
⚠️ The two are drawn the same size, and a gold ring is what tells them apart. Only the
Olympic square wears one: one marked square beside a plain one is the ranking, and ringing
both made them read as a matched pair in two liveries. The world champion had a white ring
for a day, which was the worse of the two mistakes — on a dark ground white is the brighter
colour, so the square meant to be the plain case came out looking like the bigger prize.
The Olympic square used to be a rung larger,
which is right on the honours ladder and wrong here: on this page a square is a face, and
two faces on one line at two different sizes read as a layout accident rather than as a
ranking. pyramidScale overrides the size for this page only — the honours board still
ranks an Olympic gold above a world title, because that is a claim about worth and this is a
row of portraits.
⚠️ Nothing below the summit is ringed. There used to be a ring per tier and not one of
them was ever visible: they were inset box-shadows, and an inset shadow paints behind
the element's content — which here is a photograph filling the tile. They showed for the
instant before the images loaded and then went. The one that is left is drawn outside the
square, where a photograph cannot cover it, and every other tier says its rank by size,
which is what the page is built on.
⚠️ No team events. A team title would rank a player by the country they were born in, which is not what any of this measures.
Asia, Europe, Pan Am, Africa and Oceania, each switching its own continental championships and its own four-yearly games in or out. Asia and Europe are on by default.
⚠️⚠️ A chip moves the score, not only the drawing. A continent that is off is out of the
season's denominator as well as off the board, so every score is a share of exactly the
board in front of you. The alternative — draw them, count them always — was argued for and
rejected, and the objection was the right one: a ranking of the greats is only readable if
the reader knows and chooses which continents it counts. Somebody with a cabinet of European
titles appearing high on a list of legends confuses far more than one fixed denominator
saves. So the chips travel in the link (wc), like the bar and the era names: a board you
send has to show the numbers you were looking at.
The default is measured rather than chosen. Of the 1210 titles on these five boards that are open to the world — everything but the continental events themselves — Asia has won 89.3% and Europe 10.5%. Pan Am, Africa and Oceania have two between them: a Canadian in 2026 and an Australian pair in 2009. (Counted by the first-listed player's country, which is the pair's in every case but a mixed-nationality partnership.) Those three continents are a different question, worth being able to ask and not worth being on by default. Europe is in it on current form as much as on history: Viktor AXELSEN and Anders ANTONSEN, Carolina MARIN, and a French squad deep enough to seed three draws make a European Championships a tournament a world top ten has to win.
Switch all five off and you have the board as it stood before October 2026, when the continental events were excluded outright.
⚠️⚠️ The Commonwealth Games is not on this board, and no chip can bring it. It spans four of those five confederations, so no continent owns it — and a title no chip governs is one whose weight would sit in every denominator with no way to take it out. BWF's calendar is also missing the individual event for Glasgow 2014; it ships only the mixed team one, so a Commonwealth row would show a hole that is a gap in the records rather than a fact about the sport. It stays on a player's own Compare grid, where the question is what that player won and LEE Chong Wei really did win two of them.
⚠️ No doubles. A doubles title is won by a pair, so one square would have to hold two faces and would stop meaning what every other square on the page means.
Every square is named for what it was called at the time. Hovering a 2013 Super 1000 says Superseries Premier and the identical rung in 2023 says Super 1000; the Finals is a Superseries Finals until 2017 and a Tour Finals after it. The names come out of the same tables the honours board uses, so the two views cannot drift apart. The summit row also carries a mark beside the face — the rings for an Olympic champion, a cup for a world champion — because size alone is a distinction you have to already know to read.
⁕ marks a title that did not happen when its name says — a dashed outline on the square
and an asterisk beside the year. Dashed and outside, where the tier rings are solid and
inside, so a footnote and a rank can never be read as the same mark; it used to be a gold
ring, which is now what an Olympic champion gets. The season-ending Finals belongs
to the season it concludes, so the 2010 Superseries Finals and the 2020 World Tour Finals
are drawn in 2010 and 2020 although both were played the following January. That is the rule
tournamentSeason has always applied to a career grid; until recently the pyramid read
BWF's filing straight and the two pages disagreed about the same fact.
⚠️ The Tokyo 2020 Olympics stays in 2021. It is marked, not moved, and the difference is not that one of them was labelled "(New Dates)". A Finals is retrospective — the conclusion of a season already played, contested by the players that season's results qualified, and the one event there is exactly one of per season, so two in a column is a contradiction. An Olympics is not the conclusion of anything, and drawing an Olympic gold in the 2020 column would say somebody won one in a year nobody played one.
Things that look wrong and are not:
All five draws are on this page: MS · WS · MD · WD · XD. The three doubles ones were absent for a while on a real objection — a doubles title is won by a pair, so one square would have to hold two faces and would stop meaning what every other square on the page means.
The square is split down the middle instead, half a photograph each, and it stays exactly the size the ladder says it is. A Super 750 square is the same size in mixed doubles as in men's singles, because the ladder is about what the title was worth and not about how many people won it.
⚠️ Each half shows the middle of its own photograph, not one player's left ear and the
other's right. object-fit: cover in a box half as wide as it is tall matches the source on
height and crops the sides, which is the central band — and object-position: top center
keeps the crop off the chin, the same rule the full square already followed. The canvas
export does the identical thing with drawImage, so a pair looks like itself on screen and
in a poster.
Above the square, a pair is one competitor: one tile, one era bar, one line on the score, one legend chip, and a season's scores still add to a whole season. The alternative — splitting a title into two half-titles, one per player — would make the chart describe people rather than teams and draw every partnership as two identical lines.
⚠️ The cost is real and it is the honest one: a player who changes partner starts again. The old team stopped winning and a new one started, and the board says so rather than pretending a career is continuous through a change of the thing that actually won the titles.
⚠️ The key sorts, and the drawing order is settled once at the door. BWF does not list a partnership the same way twice. Seven pairs across the three doubles boards appear in both orders across their own titles — Kido and Setiawan eleven times one way and once the other, Cai and Fu thirteen and five, Gao Ling and Zheng Bo four and four — and the split square draws them in the order the title carries. So the same pair swapped faces from one square to the next along a single row, and the hover swapped their names to match, which reads as two different partnerships. Two separate defences:
settleWinnerOrder fixes the drawing order, once, as the file comes through
loadWinners. Every pair takes the order BWF used most often for it, with the earliest
title breaking a tie.⚠️ The obvious rule — whichever order the first title carried — is wrong twice on this data: Cai and Fu's first title is one of the five against thirteen the other way, and Lee Hyo Jung and Lee Yong Dae's is one against six. The majority follows BWF's own usual presentation; first-title only decides the three genuine ties (1–1, 3–3, 4–4).
⚠️ No convention is layered on top of that. It would be easy to put the man first in the mixed and BWF itself does not — GAO Ling / ZHENG Bo is the majority order for that pair while Lee Yong Dae leads his. The page says what the federation says, consistently.
⚠️ Settled at the door rather than at harvest time or in each of the four renderers that draw
a pair. The file on disk keeps saying what BWF said — the rule usableAvatar already follows —
and which way round to draw them is one decision made once, so the board, the era band, the
score chart and every export cannot disagree.
⚠️ BWF's stand-in for "no photograph" is a photograph. Rather than an empty avatar it
serves a generic silhouette — profile_male.jpg or profile_female.jpg — and nine of the
winners across these five boards have one, two of them in the singles files where it had gone
unnoticed. On a singles square that is merely uninformative; on a pair it drew the same
blank twice and read as a rendering fault. It is treated as no photograph, so the initials
take over and the half says who it is. Two more winners have a .tif avatar, which no
browser renders at all, and get the same treatment. The harvested files still say exactly
what BWF said: whether a URL is worth drawing is a decision about drawing.
⚠️ A half with no photograph keeps its half of the square. Dropping it would let the other photograph slide across and fill the whole square, which would say the pair was one person.
Click a square and the rest recede. A doubles board is two photographs per square and several hundred squares, and picking one partnership out of it by eye is genuinely hard.
The same gesture works everywhere a competitor is drawn: a square on the board, a bar in the eras band, a face on the score chart, a chip in the score's legend. They are the same person keyed the same way, so they all mean one thing. Click more to compare them, click a lit one again to drop it, or press Esc to get the whole board back. The pick travels in the link, so a board with somebody picked out of it can be sent.
⚠️ What fades is the photographs, not the squares. Dimming the whole square was the first implementation and it deletes the board — the square carries the faint ground that draws the pyramid's silhouette, so the shape of every season went with it. Now twenty grey pyramids stay standing with the picked faces lit inside them, which says both things at once: who won this, and how much there was to win. The eras band does the same one level down — the run keeps its block of colour and its name and face recede.
⚠️ Nothing is ever removed, only dimmed, which is the score chart's own rule: a share means nothing without the people it was taken from, and the shape of a season means nothing without the rest of the season.
Export takes a range of seasons — the pyramid, the band and all — and draws it to a PNG at twice the screen's density, with the link and a legend on it. Download it, or Copy it straight to the clipboard where the browser allows that.
A pick goes into the picture, because it is the picture: an export of a board with one competitor followed across it is a different claim from an export of the board, and the foot of the image names who is lit so a reader who gets it in a feed is not looking at an unexplained dark board. The same rule the score's pins and the compare page's chips follow — an export draws what is on screen.
It is drawn from the data, not photographed off the page. Rasterising the DOM in a
project with no build step means either a library or the foreignObject trick — every
stylesheet inlined, every image a data URI — and still comes out at whatever zoom the sender
happened to be on. poster.js paints it onto a canvas instead, so an export is the same
size every time and cropped to the seasons that were asked for rather than to the viewport.
Lanes, colours and the shading scale are worked out over the whole board and then cropped, so a poster of 2011–2016 is the picture the sender was looking at rather than a recoloured one; a run cut by the crop gets a square corner instead of a rounded one, to say there is more of it off the side.
⚠️ The photographs need crossOrigin="anonymous". Measured against the live site: BWF's
image host answers a CORS request and the canvas stays readable, and the very same image
loaded without the attribute draws perfectly and then poisons the canvas — so toBlob
throws at the last step and the whole feature is a button that fails. The GitHub avatar is
the opposite case: its URL is a redirect that drops the header, so it is committed to this
repo as data/avatar.png and served from our own origin.
Under the pyramid, one bar per run of consecutive seasons in which somebody won at least three of the titles above it — four or five on the picker beside Eras, the same shape of control as the honours board's QF+ / SF+ / F+ / W. Each bar carries the player's face, name and flag; the totals are on the hover, season by season, rather than printed on the bar.
One colour per career, cycled through eight hues in the order the band is sorted — which is by the season a career opens, so two players share a colour only if eight others opened between them. A career with two eras keeps one colour for both. The shading within a colour is that season's count, kept deliberately gentle: a wide range turned every season boundary into a hard edge and a decade read as ten bars.
The name is position: sticky, so it slides along with the scroll and stays readable for
the whole of a ten-season run rather than only while 2007 is on screen. Repeating it once per
season was the other candidate and is a wall of the same six words. This is why the bar
itself may not clip: an overflow: hidden ancestor is a scroll container, and sticky inside
one sticks to a box that never scrolls.
Bars overlap on purpose, and that is the entire point. LEE Chong Wei's decade runs underneath LIN Dan's and then CHEN Long's; a chart that named one champion per season would have drawn three consecutive reigns instead of three simultaneous ones. Lanes are packed rather than one per player — fifteen women have cleared three titles in a season and the band is three rows deep — so a lane means only "these two overlapped", and a player with two eras keeps one lane for both.
⚠️ Runs are strictly consecutive. The shelved eras chart, which measures the same thing in ranking weeks, had to tolerate dips: a rolling 52-week points sum jitters and one week at sixth would sever a decade. A title count does not jitter, so a dip here is drawn as a gap — which is why 2020 severs every line on the board, and why it should.
⚠️ It is also the opposite measure from that chart, and for some careers the two disagree flatly. The ranking rewards entering tournaments and LIN Dan skipped a great many: 14 weeks at number one to LEE Chong Wei's 310, the other way round from the trophies. Neither is wrong.
A title is marked #1 in black, in the grid and on the honours board alike.
⚠️ The result ramp runs from #1a7f37 for a win to #3fa34d for a lost final — one step
apart on the same green, and side by side they are genuinely hard to tell apart. For a
reader with red-green colour vision deficiency the ramp carries almost nothing. The mark is
redundant coding: the same fact said twice, once in colour and once in text, so the board
still reads with the colour ignored entirely.
The mark appears only where the square is at least 16px, because below that two glyphs stop being a word and start looking like dirt on the square. Every honours row has its own size, so the Super 300 row and below never carry one — the zoom slider is the way up.
The honours board's zoom will not go below 7 for exactly this reason: a Super 750 square is
7 × 2.618 = 18.3px, just clear of the gate. At 6 it is 15.7px and every Super 750 title
silently drops back to being nothing but a darker green.
The Winners page's third view. Same board, square for square — same rows, same
tournaments, same order, same sizes — but instead of the winner's face each square
shows what one competitor got there, in the colours and the #1 the Compare
page already uses. Green is a title, the ramp runs down through the final and the
semi-final to red, and a tournament they did not enter is an outline.
⚠️⚠️ The outline is the reason the view exists. On the Board, a season where somebody took eleven of seventeen titles shows six squares with another face in them and they all read alike. Four of them may be tournaments that competitor never played and two may be finals they lost — and eleven from thirteen with two finals is a different season from eleven from seventeen. The Board holds who won and nothing about anybody else, so it cannot say which, and reading the gaps as defeats flatters nobody and misleads everybody.
⚠️⚠️ It is a join, and BWF's own tournament id is the key. The harvested file
knows the winner of each title; a runner-up can only come from the competitor's
own career, which is the same vue-player-tournaments data the Seasons and
Compare pages walk, cached in the browser the same way. Both sides carry the
tournament id, so nothing is matched on names or dates.
Choose a competitor from the menu in the header, or click a square on the Board
and switch over — they are the same act, because the view's subject is the
Board's pick, and it travels in the link as wp either way.
⚠️ A pair is the two careers where they agree. BWF ships no partner at all
(see HANDOVER.md 2.4), so a doubles career is that player's results in that draw
whoever they played with. Two players who finished in the same place at the same
tournament were in the same pair: exact for a title or a lost final, since only
one pair can be either, and an approximation below that, where two pairs can go
out in the same round. Where they disagree they were not partners, so the
tournament is drawn as one this pair did not play.
The dominance band is not drawn here — one career has no succession in it — and
neither is the export button, because a career board is a picture poster.js has
not been taught to paint.
The Winners page has a second view. Board is the pyramid above; Score is the same seasons read as a quantity — one line per career, and the height is how much of that season they took. It answers the question the board raises and cannot settle: the faces say a name over and over, and this says how much of the year that name actually was.
A score of 100 is the whole season — every title on the board that year, and nobody else with one. It is a percentage underneath and deliberately not called one: "75% of 2022" invites "per cent of what?" every single time it is read, and a score out of 100 carries its own scale. Being a share, one player's rise is always somebody else's fall, and two seasons can be held against each other however many titles each of them held.
Not every title counts the same. Each is worth what its rung on the board is worth, on the ladder in The honours board → Half steps above — full φ steps with four halves in them:
| Olympics | Worlds | Cont. Games | Cont. Champs | Tour Finals | Super 1000 | Super 750 | |
|---|---|---|---|---|---|---|---|
| worth | 6.854 | 5.388 | 4.236 | 3.330 | 2.058 | 1.618 | 1 |
Seven Super 750s to an Olympic gold, which is about the trade anybody who has watched the
sport would make. The rungs come from honourRung — the same ladder the photographs on the
board are sized by — so the score is not a second ranking; it is the board's own ranking,
added up. Every weight is printed in the page's own note, because a weight nobody can
check is a magic number.
⚠️ There is no toggle for any of this, and both absences were argued.
Counting titles alike was the other reading and it is gone. It makes an Olympic year look like a Super 750 year, which is the one thing this project's whole ladder exists to deny.
A gentler step was the harder call, and it was settled by measurement rather than taste. The photographs step by √φ on the side of a square, which steps their area by φ — both are "the golden ratio" and they are not the same ladder. Held against the one comparison everybody already has an opinion about:
| ladder (no continental titles counted) | LEE Chong Wei | LIN Dan |
|---|---|---|
| titles counted alike | 329 | 202 |
| √φ per rung, all the way | 313 | 230 |
| φ per rung, all the way | 285 | 276 |
| φ with four half steps | 268 | 290 |
A √φ ladder all the way up is too flat: it leaves LEE Chong Wei 83 points clear, which is not the reading the eye has — LCW won more of them, LIN Dan won the big ones. A φ ladder all the way up had them level, and that was the answer for a month.
⚠️⚠️ The four half steps turn it over, and that is the change rather than a side effect of it. Dropping the Tour Finals from φ@sup2; to φ^1·5 above a Super 750 takes a rung off four of LEE Chong Wei's biggest titles, and he has no Olympic or world gold to put back; LIN Dan goes first on 290 to 268. Across all five disciplines that is nearly the whole effect: Carolina MARIN and Akane YAMAGUCHI swap second and third (185 to 184), two pairs swap seventh and eighth, and mixed doubles does not move at all. Best seasons barely move either — Kento MOMOTA's 2019 goes from 78.3 to 79.1.
⚠️⚠️ The continental events then move it much further, and that is a fact about the two careers rather than about the ladder. On the board as it now stands the same pair read LEE Chong Wei 234, LIN Dan 318, and Viktor AXELSEN goes second on 246. Two things did it: LIN Dan won five continental titles to LEE Chong Wei's one — four Asian Championships and the 2014 Asian Games against a single 2016 Asian Championships — while every season's denominator grew by about two continental titles, so LCW's seasons are divided by more and nothing of his is added on top. AXELSEN's own second place is an Olympic gold, two world titles, three European Championships and the 2023 European Games.
⚠️ Worth knowing before reading 234 as the whole truth. LEE Chong Wei's one continental title is the 2016 Asian Championships, and the seasons where he might have added to it are the ones the harvest cannot see: BWF carries no order of play for most continental championships between 2007 and 2013, so 64 of the 100 held in 2007-2026 are here and 36 are absent — a gap falling on exactly the years these two careers were in. (His 2006 is on BWF's record and holds no continental title either: an Asian Games bronze in Doha.)
⚠️ The Olympics has always outranked the World Championships here, 6.854 to 4.236. What
makes them look equal is pyramidScale, which draws an Olympic square at the Worlds size and
lets the gold ring carry the difference. That is a decision about a row of faces; it was never
a claim about worth, and honourRung — which is what these weights read — has always ranked
them apart.
⚠️ No era switch here, unlike the Compare page — and none is needed. That page holds two
careers and the useful question is which vocabulary you want to read both of them in. A
whole-board view cannot ask it: this one spans 2007 to 2026 at once, so "call everything
Superseries" is wrong for 2023 and "call everything Super 1000" is wrong for 2013. Each title
is named for its own season instead, by the same pyramidLabel the board above uses — 2013
reads 12×Superseries and 2023 reads 6×Super 750. The ladder in the note keeps the modern
names, because those are the rungs rather than any particular title, and a rung has no
season to be named for.
The switch would also be an empty control: it changes no size anywhere in this project by
design — SHARES_RUNG exists to keep an Olympic square identical in both readings — and the
score is weights over honourRung, so every line on the chart would be in exactly the same
place in either era.
⚠️ A player has a point only in the seasons they won something. There is no column in this data saying who entered, so "nothing in 2013" and "not on the tour in 2013" cannot be told apart — and a line drawn along the bottom would be asserting the first while knowing only the second. Runs break at every gap, and somebody who appears in one season stays a single face.
The marker is the photograph, ringed in the player's colour. A dot needs a legend and a face does not, and the whole point of a marker here was that a one-season winner should still be someone. The ring stays because the line has to be traceable between faces, and because a photograph is not always there — no avatar falls back to a plain dot.
⚠️ The colours are handed out over the players actually drawn, not over all forty-four. The
era bands can colour every career from a fixed cycle because two bars of one colour lie far
apart on the page; a line chart draws them through each other, and at one colour per career LEE
Chong Wei, SHI Yu Qi and KIDAMBI Srikanth all came out green. The palette is also its own —
twelve hues walking the colour wheel rather than REIGN_COLOURS, which holds two blues and put
Viktor AXELSEN and KIDAMBI Srikanth in near-identical shades a season apart.
⚠️ The y-axis never moves under the reader. It is scaled to the best season in either draw. Fitted to what was on screen it rescaled every time a name was clicked — so isolating somebody made their line climb the page, which is the chart lying about the one thing it was asked to show — and the men's and women's boards came out at two different heights, so switching between them compared two pictures at two scales.
⚠️ Clicking a name dims the rest; it does not remove them. Drawing only the pinned player re-ran the palette over a set of one, so the act of picking somebody out changed their colour — and it threw away the context that makes a share chart worth reading at all, which is who else was in the season. Clicking a second name adds to the selection, and the legend keeps every name the bar admits: off is a state, not an absence.
The face on the chart does what the chip below it does. A marker here is a name with a
season attached, so clicking one pins that competitor exactly as clicking their legend chip
would — the reader who has just hovered a face to find out whose line it is should not then
have to go and find the same face again in a list of twenty. Esc clears the lot, on this
view and on the board.
An asterisk on the axis marks a season with far fewer titles than the ones around it, so a score built on one is built on less. 2020 stopped in March; 2022 ran eight of these against a normal twelve to fifteen, with not one tournament held in China and the Finals moved out of Guangzhou; and the last year is ongoing. Each marked year gets a faint column with the reason written at its foot, inside the plot, where the line does something strange rather than in a caption. A leg touching one is dashed: a solid line across 2020 asserts a trend through a year that was barely played.
⚠️ Short is two thirds of the median, not a fixed count. The calendar has held seventeen of these titles and it has held three, so "fewer than six" means one thing in 2013 and another in 2020 — and the fixed rule called 2022 a normal season while the axis above it said otherwise.
⚠️ With the continental championships counted, 2020 is the only season this rule now finds:
2022 holds ten of a median fifteen, exactly two thirds, because its continental events went
ahead. The threshold was not moved to keep the old answer. Nothing is lost by it — the faint
column, the asterisk and the full-season denominator are all driven by the named pandemic set
(COVID_SEASONS) in union with this one, so 2022 keeps every mark it had. What it loses is the
dimming that means "there was barely anything to win", which is less true of it than it was.
⚠️ The tables under the chart dim those rows rather than colouring them. They were amber, which is this palette's attention colour, so the one season nobody should read at face value was the brightest line in the table and Anders ANTONSEN's two titles out of three looked like the find of the chart. A footnote should recede. The mark stays lit, because a mark is a pointer and not a highlight.
Under the axis, a bar per season with its title count on it — the denominator, drawn. A score is a fraction and the line only shows the numerator; without this, 66.7 in 2020 and 66.7 in 2015 are the same height on the page and one of them is two matches. The count is on every bar, not only the short ones: it was the short seasons' badge, which made a count look like a warning.
Show draws everybody whose best season clears it. Forty-four careers on one chart is a scribble, so the default hides most of them — but it must not hide the person who actually dominated a year. The rule: take every finished season, find who led it, note that player's best year anywhere, and put the bar as high as it will go without dropping one of them. It comes out at 40 for the men and 20 for the women, and it stops being derived the moment the reader touches the slider.
⚠️⚠️ This asked the wrong question until it was looked at: "is anybody on screen in every season", which any visible player satisfies by winning one thing. The women's bar came out at 35 on a rule meant to hold the leaders, and CHEN Yu Fei — who led 2019 and scored 33 in 2023 — was not drawn at all. And it is the leader's peak that has to clear the bar, not their score that season, because the bar filters on a career's best year.
⚠️ Covering the top two of each season looked safer and collapses: some season's runner-up is always somebody whose whole career peaks at 10, so the bar falls to 10 and twenty-three lines come back. Measured on both draws, not guessed.
⚠️ The season being played is left out of that sum. In January it is one tournament and one winner; a part-played season cannot say who led it, so it is not asked — without this the bar collapses every New Year.
The chart says who dominated and when. The table under it says who dominated, full stop — and there is more than one honest way to answer that, so there are three orderings. Two of them rank careers, with both numbers on screen and only the sort moving; the third ranks seasons.
Total is every season's share added up. It rewards staying there: LEE Chong Wei never took 44 of a season and took some of twelve of them, which is a claim about a career that no single season can make. He is second in the men's singles on the sixth-best peak on the board.
Peak is the best single season. It rewards the year nobody else got a look in: KIM / SEO took 76 of 2025 and appear in two seasons, so they are first on peak and eighth on total.
The two orderings disagree, and the disagreement is the point. A table that showed one column at a time would have hidden it, so both numbers are on every row and the sorted column is marked rather than moved.
Best seasons ranks the seasons themselves rather than the careers, and is the one ordering where a competitor appears more than once. The other two answer once per career, so a career's second-best year is invisible in them however good it was: Kento MOMOTA took 78 of 2019 and 53 of 2018 — the first and fifth best men's singles seasons on the board — and Peak can only ever say the first. Five seasons in all, two of them among the best anyone has played. The columns become a season's — year, share, and what the share was made of — and the career total moves into the hover, which is what makes a short peak read as one.
⚠️ It is a different question, not a better answer. A list of the best seasons is crowded by the people who had them: six competitors hold the top fifteen men's singles seasons, and KIDAMBI Srikanth, tenth on peak, is twenty-eighth here. That is what the list is — it simply stops being a ranking of competitors while it does it, which is why it is a third chip beside the other two rather than a replacement for either. Picking a row still picks the competitor, so both of MOMOTA's rows light together.
⚠️ There is deliberately no average, and the reason is the same one that makes the lines above break at a gap. This data says who won, not who entered, so a competitor has a point only in the seasons they won something — the seasons they played and won nothing are missing from the divisor rather than sitting in it as zeroes. One good year and nothing else would out-rank a fifteen-year career, and the number would be describing the hole in the data rather than the players. A third column that cannot be computed honestly is worse than no third column.
⚠️ The pandemic seasons get the same denominator as every other season, and there is no toggle. What is most obviously wrong with 2020 is arithmetic before it is competition: three titles were played, so one of them would be a third of the year. All three pandemic seasons are weighed against what a full season of the era was worth — 28.04 by weight, fourteen titles, read off the seasons in the file rather than typed in, and exactly the figure 2024 gets. A season cut short then reads as the fraction of a year it actually was.
⚠️ The thin field is left in the record deliberately. Fewer Chinese players entered, so fewer Chinese players won, so their scores are lower and everybody else's are higher — and that is what happened. What the denominator removes is the arithmetical distortion, not the sporting one: a player who won one of 2020's three titles cannot bank more than one title's worth of a whole year, because the rest of the year was never played and nobody won it. The seasons are marked on the chart — a faint column, the word "Covid", an asterisk on the year — so a reader knows not to take them at face value, and then the reader judges.
⚠️ Never downward, and that is the rule rather than which seasons it reaches. 2021 was
untouched for a year: it held an Olympics, a Worlds and two World Tour Finals, 23.80 against a
normal 19.33, and substituting the normal figure would have raised every 2021 score. With the
continental events counted it falls the other side of the line — it kept its European
Championships and lost its Asian one — so the correction now reaches it and lowers it, which is
the season it most needed to reach. Math.max is unchanged: a corrected denominator can never
be smaller than what the season actually held.
⚠⚠ This is a calendar correction and it cannot see a field, and 2021 is where that shows. 2.3% Chinese participation, eight of its eleven events with literally nobody in the draw — and no adjustment at all, because it was not short. AXELSEN's peak sits in that season. Known, accepted, and the reason the pandemic marks on the chart are not decoration.
⚠️ Every mark for a pandemic season takes the same set, and it is a union. A faint column behind the plot, the word "Covid" at its foot, an amber asterisk beside the year, an amber bar in the strip below it, an asterisk in the season tables and one on the season in the Dominators ranking — all of them mean "do not read this as an ordinary season", and all of them fire for either reason: far fewer titles than the seasons around it, or the pandemic. 2021 is why that matters: it held ten titles, the same as 2018, so a count cannot find it, and it spent three commits picking up the marks one at a time.
What stays on the count alone is the dimming of a row and the dashing of a line, because those two really do mean "barely anything was played" — 2021 played a full complement and reads at full strength.
⚠️ The ranking's asterisk is on the season, never on the competitor. A peak taken in 2021 is a fact about 2021. An earlier mark sat beside the name and meant "this career is being under-counted"; it belonged to the reading that set those seasons aside and went with it.
⚠️ The Show bar follows, being derived from the scores. The men's singles opens at 15 rather than the 40 it sat at when a pandemic season was a share of its own three titles, because 2020's best season is now a 17 rather than a 69 and the bar's rule is not to drop a season's leader. Twelve lines instead of seven; the other four boards barely move.
⚠️ Two other readings were built, measured and removed. aside dropped the seasons
altogether — the men's singles then read LEE Chong Wei, LIN Dan, CHEN Long — but it discarded
titles that were genuinely won, from everybody, ZHENG / HUANG included, who lost 59 points of
mixed-doubles total to the very seasons China's absence defined. played left them as a share of
their own three titles, which is the distortion this exists to fix. Neither is a reading this
project believes, and a toggle offering a reading we do not believe is what the
weighted-versus-counted rule above already refuses to ship. The measurements are in HANDOVER
3.4u.
⚠⚠ Which seasons, and why on participation rather than on the calendar. A domination score is a share, so what flatters a winner is not where the tour went — it is a thinner field. If a third of the best players are not competing, everybody else's share of the season is larger and they did nothing to earn the difference. So the test is who was actually in the draw. Distinct players in the opening rounds of every event on this board, and how many were Chinese:
| 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 |
|---|---|---|---|---|---|---|---|
| 12.3% | 14.7% | split | 2.3% | 9.9% | 13.3% | 14.1% | 12.6% |
2021 is the season a count of titles cannot see. It held ten of these titles, the same as 2018 and 2019, and it ran almost entirely without China: eight of its eleven events had literally nobody — both Thailand Opens, the All England, the French, both Indonesia events and both World Tour Finals. Only the Olympics, the Denmark Open and the Worlds had any.
2020 is split by the shutdown. Three titles. The All England on 11 March was 16.1% Chinese, an ordinary field played days before everything stopped; the October Denmark Open had none, and neither did the Finals, played in January 2021.
2022 is the weakest case and is in on a judgement. China was present at every event and reduced rather than absent — 9.9% against a 12–15 baseline. What it also has is a calendar cut to eight events from twelve, the Asian swing gone bar Indonesia, Malaysia and Japan, and the Finals moved out of Guangzhou. The chip is there to disagree with.
2018 and 2019 hold ten titles as well and are deliberately not in the set — that is the World Tour restructure, a change to the ladder rather than to whether the season was played, and their fields are normal.
⚠️ An earlier version of this argued from which countries hosted: no Chinese event in 2020–22, both back in 2023. True, and the wrong test — hosting is not competing. 2022 hosted nothing and had Chinese players at all eight of its tournaments; 2021's Thailand Opens were held in Bangkok with none.
⚠⚠ The season being played is weighed against the whole year. A share of the titles played so far makes whoever wins the first tournament of January a 100, and in September 2026 it inflated every score on the board by half again — eight of the twelve played, 13.71 of 19.33 by weight, before the continental events were counted. Against the planned year the numerator only grows while the denominator stands still, so a part-played season is a lower bound on what it will finish at: it cannot overstate a total, and a peak, being a maximum, cannot be dragged down by it. That is the property that makes the running year safe to count at all, and it is why it does count. The strip under the axis reads 8/12 rather than 8 so the reader can see it, and the column adds up to less than a whole season on purpose — the titles still to come belong to nobody yet.
⚠️ Finished seasons keep the titles actually played. 2020's denominator is the three that happened, not the nine still on the calendar when it was abandoned. Weighing a cancelled season against its plan would rank everybody as having failed to win events that never took place.
⚠️ The calendar is harvested into the same static file, by tools/harvest-calendar.mjs —
one call per season, seconds rather than minutes. Not a live call: the score view reads one
file and must keep doing so, or the numbers would depend on whether a request succeeded. It
is the one thing in these files that goes stale, and it can be topped up on its own.
⚠️ The Show bar still leaves the running season out of its derivation, and that survives the better denominator: the bar is set from who led each season, and nobody has led a season that is still being played, however fairly it is weighed.
⚠️ The Show bar does not reach this table, and that is not an oversight. The bar filters on peak, so a total ranking cut by it is a different claim: at the men's singles default it draws twelve of the forty-five careers listed, and it drops Jan O JORGENSEN — four seasons, twelfth on total — for a best season of 7.4. The bar declutters the chart; the ranking is the whole board. The top twenty are shown, and one click shows every competitor.
⚠️ Clicking a row picks that competitor everywhere, like clicking a square or a marker or a chip. And if the bar was hiding them, the bar comes down to fit — the table lists everybody and the chart does not, so a reader who clicks a name in a ranking and gets nothing on the chart has been told a half-truth. The chart also now ignores a pick it cannot draw rather than fading every line at once, which is what a link naming somebody below the bar used to do.
The same picker, the same range, the same foot as the board's export — only the drawing
differs, so drawScorePoster() sits beside drawPoster() in poster.js and the two share
POSTER, the 16384px density guard and the provenance strip at the bottom. The file is named
badminton-score-<draw>-<from>-<to>.png, so the two exports never collide in a downloads
folder.
The bar you set and the names you pinned go into the picture, because those are what it is about. ⚠️ Cropping changes what is shown and never what is counted: a score is a share of its own season, so the numbers, the colours and the height of the axis are all settled over the whole board and then cut to the years asked for. Same rule as the era bars, same reason — an export of 2011–2016 that recoloured CHEN Long because LEE Chong Wei fell off the left would not be the picture the sender was looking at.
⚠️ The axis height is handed in by the page, because the page scales it across both draws
and poster.js is given one file. Without that a men's export and a women's export of the same
seasons come back at two different scales — the bug the page itself already fixed once.
The board and the score answer the same question two ways, which is the split the Compare page already makes between its grid and its honours board — one pattern to learn rather than two.
⚠️ And this page has a reason of its own: the pyramid is twenty columns that scroll sideways and the chart is one fixed-width picture. Stacked, the reader would be dragging one of them against the other and the years could never line up.
LIN Dan and LEE Chong Wei played almost entirely before the World Tour existed, and BWF ships no name for its pre-2018 tournament categories — so their careers used to arrive as one undifferentiated "Unmapped" heap with nothing to compare against.
The four old senior tiers are now placed on the modern ladder: Superseries Premier as
Super 1000, Superseries as Super 750, Grand Prix Gold as Super 300 and Grand Prix
as Super 100. That is not a guess — tournament_series_id survives an edition, so a series
can be followed across the 2018 boundary to see what each old tier actually turned into.
Mapped squares carry a notch cut out of one corner, and hovering one says what it really was. The Seasons page never translates: there a Superseries Premier is called a Superseries Premier. Two honest edges, both noted under the board — nothing maps to Super 500, because there were four old tiers and there are five new ones; and before 2011 there was no Premier tier at all, so a 2008 All England sits a rung below a 2013 one.
That is the right default and the wrong one for the comparison people actually come here to make. In World Tour names, every square on a LIN Dan / LEE Chong Wei board is a translation into a structure neither of them ever played in — 116 of Lee Chong Wei's 140 results are notched.
So the World Tour / Superseries switch on the compare page names the ladder in either vocabulary, on both the grid and the honours board. Nothing about a result changes: rows keep their sizes, so the two readings are two readings of one board. Only the row names change, and which squares are the translated ones — in Superseries names the notch moves to the modern results, and Lee Chong Wei's board drops from 116 marked squares to 4.
| rung | World Tour | Superseries era |
|---|---|---|
| Olympics · Worlds · both continental rungs | unchanged | unchanged |
| Tour Finals | Tour Finals | Superseries Finals |
| ▸ | Super 1000 | Superseries Premier |
| ▸ | Super 750 | Superseries |
| ▸ | Super 500 | — folded up into Superseries — |
| ▸ | Super 300 | Grand Prix Gold |
| ▸ | Super 100 | Grand Prix |
⚠️ Backwards is not the same map forwards. Following the same series across 2018 in the other direction, the Super 1000 is unanimously the old Premier (4 of 4) and the Super 300 is the Grand Prix Gold (8 of 9) — but the Super 500 splits 4–3 between Grand Prix Gold and Superseries. That is not missing evidence, it is evidence of a split: 2018 was not a renaming, and one Grand Prix Gold became both a Super 500 and a Super 300.
A Super 500 is therefore folded upward into the Superseries and drawn one rung higher than it was. It is the one thing this reading gets wrong, it is not a corner case — ten of Lin Dan's results are Super 500s — and it is not hidden: those squares carry the notch like any other translation, and the Super 500's rung is left standing empty rather than closed up, so the double size step between Superseries and Grand Prix Gold is the gap where a tier used to not be.
The era travels in the link (er=ss), because a board sent in Superseries names is not the
same board in World Tour names.
Two things shape every count in this tool, and both are BWF's rather than ours.
The tool starts in 2006, because that is where BWF's results start. Their player endpoint happily returns 2000–2004 — 94 tournaments for Lee Chong Wei, 79 for Lin Dan — with no result on a single one of them. The 2004 All England is in there with nothing beside it. 2005 is about half filled in; 2006 is complete. So comparing two careers against a third-party site will not tally: measured against BWF's own data, Lee Chong Wei has 62 individual titles and Lin Dan 52, where sites with a fuller source say 69 each. The missing ones are all in that 2000–2004 blank, and they are not evenly split — which is most of why Lee Chong Wei looks so far ahead here on Superseries titles and level overall elsewhere.
⚠⚠ Before 2007 there was no ladder, which is why Unmapped exists. The Superseries began that season; until then the circuit was the IBF World Grand Prix, and it graded its events 6-star down to 1-star by prize money alone. The money was not the prestige — the 2006 All England was a 4-star on $125,000, two rungs under the China Masters at $250,000, and the same year's Macau Open paid $30,000. Nothing turns a purse into a tier, so pre-2007 titles are shown together in one section that admits its placing is a guess, and the Winners board does not draw them at all. Sizing them by what they paid would put an All England below a China Masters, which is worse than not sizing them.
⚠️ And the category id carries no tier information there either. In 2006 the World Championships, the All England and an International Series are all category 6 — and across six careers of that era, 77 of 85 pre-2007 tournaments carry that one id, from a world championship down to a $3,000 satellite. Reading those ids as tiers threw away seven of Lin Dan's ten 2006 tournaments, an All England title among them, so nothing is read off an id before 2007: the majors are rescued by name as they always were, and everything else goes to Unmapped. The ids below the World Tour are not believed until 2008 either, because BWF was still filing Grand Prix events as category 6 through 2007 — which cost exactly one title each, Lin Dan's German Open and Lee Chong Wei's Philippines Open.
The Asian, Commonwealth, European, Pan American and African Games get a block of their own, a half rung above the Continental Championships — the same circle of countries, one every four years rather than every year. They are individual titles and they belong on a career.
They have to be recognised by name, because their id is worthless in every direction: the Asian Games has arrived as category 1, 16, 74 and with no category at all, and the 2023 European Games under 11 — the Continental Championships' own id. An id-first rule draws one European Games as a Continental and the next as Unmapped.
⚠️ Pan Am, not Pan American. BWF writes that one five ways across twenty seasons —
"2007 Pan Am Games", "XVI Pan Am Games, Guadalajara 2011", "XVII Pan Am Games Toronto 2015",
"Pan Am Games LIMA2019" and "XIX Panamerican Games Santiago 2023" — and a pattern that
insisted on the full word caught only the last of them. Four Pan American Games were landing
in Unmapped until 1 Oct 2026.
The team editions stay out, as every team event does. BWF ships them as separate tournaments under near-identical names — "Asian Games 2018 (Team Event)" beside "Asian Games 2018 ( Individual Event)", both category 16 — and they are told apart by their draws, which a tie names bare because the tie is the competitor.
Sub-regional games — East Asian, Mediterranean, SEA — are not in this block. Each is a slice of one continent, and a row holding both an East Asian Games title and an Asian Games title says neither.
⚠️ The Winners pyramid now holds them too, governed by a chip per confederation — see Winners → The continental chips. The one exception is the Commonwealth Games, which no confederation owns: it stays on a career, where the question is what this player won, and off a board whose denominator a chip moves.
Type two letters and the list appears in the same tick, before anything is asked of BWF. It is matched against the top 50 of all five ranking tables — 377 players — held in memory and refreshed twice a day, plus everybody you have opened before. BWF is then asked on the usual delay and its answer merged in underneath.
That is not only about speed. BWF's own search is alphabetical, not relevant: it returns page one of a list ordered by given name. Measured 3 September 2026:
| you type | where BWF puts the obvious answer |
|---|---|
viktor | Viktor AXELSEN at index 13 of 30 |
axelsen | Rikke AXELSEN above him |
chen | CHEN Yu Fei not in the answer at all |
an | AN Se Young not in the answer at all |
The reigning world number ones, absent from their own names. No amount of re-sorting the reply fixes that, because they are not in it.
Names match word by word, in any order, because the same player is stored both ways
round — the ranking tables say AN Se Young and the search endpoint says Se Young AN, and
a reader may type either. So se young, an se young and se young an all find her, and
shi qi finds SHI Yu Qi.
⚠️ The roster can never replace the search, only precede it. Lin Dan and Lee Chong Wei are retired and in no ranking table, and they are the comparison this whole tool was built for; Viktor Axelsen has been out injured and is not in the top 50 either. When nothing local matches, the box says Searching… rather than sitting empty — which is what it used to do for up to ten seconds.
Ten seconds because the search rode the low request lane, behind every draw ladder the career on screen was still fetching, at 320 ms apiece. It is now on the fast lane: the same uncached search during a career load went from 10.5 s to 0.76 s.
With nobody in the link, the app opens on the world number one in men's singles — looked up, not hardcoded, so it is whoever holds it today. An empty strip is a worse first impression than anybody's.
Every shortcut drives a control that is already on the page — a faster route through it, never a hidden feature — and each page's own keys are printed under its notes.
| ← → | move between Seasons, Compare, Tournament and Winners (wrapping) |
| Seasons | ↑ ↓ the level chips — up adds the highest that is off, down drops the lowest that is on |
| Compare | G grid · H honours · W World Tour names · S Superseries names |
| Compare | ↑ ↓ the QF+ / SF+ / F+ / W bar on the honours board, and the level chips on the grid |
| Tournament | O order of play · B bracket · S starred only |
| Tournament | ↑ ↓ the day, or — in the bracket — how much of the draw |
| Tournament | M W X the discipline; press again for that gender's doubles |
| Winners | Y year · B board · S score · C career · E the dominance band |
| Winners | on the Year view: the season menu and the tier chips · on the score: T total · P peak · N best seasons |
| Winners | M W X the discipline; press again for that gender's doubles |
| Winners | ↑ ↓ the 3+ / 4+ / 5+ bar while the band is on — or, on the score, the Show bar |
Six things about this are deliberate.
Nothing fires while a modifier is held. Alt+arrow is the browser's Back and Forward, Ctrl+O opens a file, Ctrl+S saves the page and Ctrl+W closes the tab — every one collides with a key above. A modified keystroke is not handled and not prevented: it goes straight through to the browser.
Where a page has nothing to step, the arrows are left alone. They are not merely inert
there — they are not preventDefaulted, so the browser still scrolls with them. A shortcut
that swallows a key without doing anything is worse than no shortcut. That now applies only
to a page with no chips and no bar on it at all: before anybody is searched for, the Seasons
page has no level chips, so its arrows still scroll.
Nothing fires while you are typing, which matters more than it sounds, because the app focuses the search box on load — so on a fresh page every letter would land in the box rather than reaching the page. Esc leaves the box, and is therefore the way in.
↑ ↓ mean "show me more" everywhere they do anything. One more round on the honours board, one more level on the Seasons strip and the career grid, one more line on the domination score, one more day at a tournament. What differs is what each page has to give; the direction never does.
On the level chips that is up adds the highest that is off, down drops the lowest that is on. Not "the next one along from wherever you last were": a row of chips has no cursor, and a key that depended on one would do different things depending on what had been clicked. From the ends the two keys are exact inverses — every press of down is undone by a press of up — and it walks the ladder the way somebody narrowing a career actually thinks: drop the small ones, keep the big ones. The order walked is the order on screen, so what the key will do is visible before it is pressed; and the keys reach only the chips the page has drawn, never the tail of unmapped ids behind the Seasons page's "N more" menu — switching one of those on would change the strip with nothing on screen moving to explain it.
The cost is the page scroller, and it is now paid on every page rather than only on the tournament one. The wheel, the scrollbar, PgUp/PgDn, Home/End and the space bar all still scroll, and a career strip is long enough that this is a real trade. It is worth it because the arrows are the only keys that can be held: narrowing a fourteen-level strip to the four that matter is four taps of one key rather than four aimed clicks. ← → are taken everywhere, which costs the horizontal scroll of the compare grid and the unfolded bracket to the arrows alone.
One letter, two draws, two kinds of control. M means "show me the men's singles" in both tournament views, though they hold the discipline differently: the bracket picks one draw, while the order of play filters several, any number of which can be on at once. So in the bracket it picks, and in the order of play it isolates — hides the rest — and pressing the same letter again moves to that gender's doubles. Clicking the chips undoes any of it. Stepping the day and the fold clamps rather than wraps, unlike the pages: four pages in a ring beat two dead ends, but the last day of a tournament is the last day.
The Winners page borrows the same double-tap rather than inventing anything: five draws will not fit five letters anybody would guess, and a reader who has learned it on one page has learned it on the other. There it simply picks the board, since a board is only ever about one discipline.
| File | What it is |
|---|---|
model.js | Pure logic: levels, weights, positions, the ladder and fill, name tidying, season parsing. No browser globals, so the tests import it straight into Node |
api.js | The request layer: two-lane queue, 320ms pacing, TTL cache, one retry. Also player search, rankings, and the URLs for BWF's flags and photographs |
app.js | All three pages: the strip with its discipline toggle, level filters and year stepper; the compare page holding the grid, the honours board and the second player; and the tournament page's day bar and order of play |
serve.mjs | Static server for local development |
HANDOVER.md | The design and engineering brief. Read it before changing anything: the weighting, the doubles model and the API traps are all settled there, several of them by testing |
node tests/run.mjs # everything
node tests/run.mjs unit # no browser, about ten seconds
node tests/run.mjs --live # ignore the fixtures, talk to BWF
node tests/record.mjs # top up the fixture set from the live API
The end-to-end suite drives a real windowed Chrome, because BWF's Cloudflare 403s headless
browsers. The API is replayed from tests/fixtures/ (gitignored, ~7 MB, regenerable with
record.mjs — but only while the data is still live). Anything with no fixture falls
through to the network and is reported, so a gap shows up as a slow test rather than a
silent wrong answer.
tests/run.mjs sweeps stale Chrome profiles from %TEMP% before it starts. Do not remove
that: on the predecessor 467 of them accumulated and took a 238 GB disk to zero bytes free.
data/winners-<draw>.json — one per discipline, all five — is not fetched from BWF at page load.
The app is player-centric — every endpoint it uses is keyed on a player id — and "who won
this tournament" is the opposite question, so it takes a call per tournament per season.
None of it changes once a final has been played, so it happens once, offline, and the result
is committed.
node tools/harvest-winners.mjs men's singles, 2007..now
node tools/harvest-winners.mjs --draw 2 women's singles
node tools/harvest-winners.mjs --draw 3 men's doubles (4 women's, 5 mixed)
node tools/harvest-winners.mjs --from 2015 a shorter run
It resumes from whatever is already on disk and writes one file per discipline, so a re-run only fetches the seasons that are not there.
node tools/audit-winners.mjs # what is missing, per discipline
node tools/audit-winners.mjs --drop # delete the short seasons, then re-harvest
⚠️ Counting titles per season does not tell you whether the harvest is complete. A season that is one short looks exactly like a season that is one short somewhere else, and a missing Olympic gold is one row in a file of 242. The audit compares the union of tournament ids across the five files: every draw plays the same calendar, so a tournament in one file and not another is a hole in the other. That is how four of five Olympic golds were found missing from the doubles boards, and how a 2010 Super Series title was found missing from all five. Its blind spot is stated on the tin: a tournament missing from every file cannot be seen.
⚠️ The obvious endpoint is the wrong one. vue-tournament-draw-data returns the whole
draw and looks like the answer, but it is 256–407 KB per tournament and returns HTTP 500
for some of them — including the Paris 2024 Olympics. tournaments/day-matches asked for
the tournament's last day is 7–14 KB, answers for those too, and carries the winner's
photograph. Draw data is kept only as a fallback.
data/podium-<year>.json — one per season, all five disciplines in each — is the Year
view's file, and a different shape for a different question: the pyramid wants one champion
per great title over twenty seasons, this wants four competitors per tournament at
Super 500 and above for one of them.
node tools/harvest-podiums.mjs 2007..now
node tools/harvest-podiums.mjs --year 2026 one season, re-harvested
node tools/harvest-podiums.mjs --from 2024 a shorter run
⚠️⚠️ One pass for all five disciplines, not five runs. day-matches answers with the whole
day — every draw, every court — so harvest-winners.mjs's five-run shape fetches each payload
five times and throws four fifths of it away. ⚠️ And one file per season, because the view
draws one season at a time and a single file at this depth would be megabytes fetched to
draw one year of it. That also makes the resume trivial: a season on disk is done.
⚠️ Two calls a tournament, and the Olympics is why there are ten in the list. A tour event plays all five finals on its closing day and all ten semi-finals the day before — measured on every season from 2007 to 2026. The Olympics spreads its finals over four days and its bronze play-offs over five, so the walk runs the whole window at a Games and stops early everywhere else. Measured cost: ≈60 calls a season, 26 events from 2018 and 17 before it, a little over twenty minutes for all twenty seasons.
⚠️ Round names vary by season, not by tier. Every tour event from 2007 to 2026 says SF;
the 2016 Olympics says SF and the 2024 Olympics says Semi-finals. The bronze play-off is
3/4 — not "Bronze", not "3rd place" — which is the only round name on the endpoint that is
not a word, and therefore the one a filter written from the obvious vocabulary counts and
never sees.
⚠️ Two fixtures cannot be re-recorded, only rebuilt. vue-tmt-schedule and
vue-tmt-live-scores answer "what is on now", and the end-to-end suite is pinned to a
fixed week — so a recording of either captures whatever week the recorder ran in, and BWF
will not serve last August back. node tools/fix-week-fixtures.mjs 2026-08-23 rebuilds both
from vue-grouped-year-tournaments, which is retrospective. Run it if a bare
node tests/record.mjs has moved the tournament page's world.
node tools/discover.mjs [url…] # what endpoints does BWF's own frontend call?
node tools/shot.mjs [#hash…] # screenshot the strip, from fixtures
node tools/probe-draws.mjs # what tournaments/draws returns per format
node tools/probe-draw.mjs # which drawId each discipline actually has
node tools/probe-bracket.mjs # the fold table, and the geometry, against live draws
node tools/audit-winners.mjs # is the winners harvest complete? (--drop to re-fetch)
discover.mjs captures the API requests a BWF page makes and scans its JS bundles for
endpoint literals. It is how Parts 3.2 and 3.5 of HANDOVER.md were found.
shot.mjs renders the app against the recorded fixtures and writes PNGs to
tests/shots/, so a change to the strip can be looked at rather than only asserted. Its
defaults cover a singles career, a doubles one, the grid and a two-player comparison; any
#hash works, and one carrying g=1 is captured at the width the compare page needs.
--top clips to the chrome — nav, hero, page header — at a scale you can read.
probe-draw.mjs lists the draws at the three tournaments the schedule names and prints the
drawId BWF gives each one — the measurement behind Part 3.4k, and the way to re-check it
if a bracket ever opens on the wrong discipline. probe-bracket.mjs goes further: it runs
the real layout over real draws and reports, per fold, the card and segment counts, the
canvas size, and the worst deviation of any card from the midpoint of the two that feed it.
probe-draws.mjs reports the stage layout tournaments/draws returns for a knockout, a
draw with qualifying, a group stage and a team event — the shapes the ladder has to handle.
Non-negotiable, and enforced in api.js rather than by convention: