@johnmorrisdotca/taisen-brackets 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +14 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/locales.js +10 -0
- package/dist/medals.d.ts +84 -0
- package/dist/medals.js +226 -0
- package/dist/rank-kit.d.ts +11 -0
- package/dist/rank-kit.js +33 -2
- package/dist/ranking-cards.js +1 -29
- package/dist/ranking-matchup.d.ts +46 -0
- package/dist/ranking-matchup.js +221 -0
- package/dist/types.d.ts +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.14.0] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
A medal table for any sport.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`medalTable({ title?, subtitle?, date?, source?, rows }, options)`**: a medal table as an SVG for any sport and event. A row is `{ name, flag?, mark?, code?, gold?, silver?, bronze?, note? }`: rank, flag (or a badge with the code or the initials, for a country or team with no flag, such as a former country), name with an optional line under it (the years it existed), the gold, silver and bronze counts in tinted cells under a disc for each, and the total. `sort: "gold"` ranks as the Olympic Games do (gold, silver, bronze), `"total"` by the number of medals, `"given"` keeps the order; equal rows share a rank (5, 5, 7) unless `ties: false`; `rankStyle: "hash"`; the first three ranks have a gold, silver and bronze bar (`highlight`). `top`, `skipEmpty`, `rows` and `page` choose the rows (the header says which are shown); `columns: 2`, by default on a wide board with more than ten rows. `describeMedalTable`, `medalRanks(rows, options)`.
|
|
16
|
+
- **`tallyMedals(awards, filter?)`**: adds up a list of medals (`{ name, medal, sport?, category?, games?, flag?, mark?, code?, note? }`) into rows, filtered to one sport, one weight class or age group or one Games (or a list of each), so that one list gives the table of an event, of a sport and of all time.
|
|
17
|
+
- Words in the five languages: the heading, the letters over the medal columns, the total and a row in a sentence (the French and Japanese were read by native-language reviewers; the Spanish and German were not).
|
|
18
|
+
- The demo has a Medal table section with made-up medals of seven Games, two sports and five weight classes, the filters, the ranking rule, the number of rows, size and look in the address. [docs/medals.md](docs/medals.md).
|
|
19
|
+
|
|
20
|
+
## [0.13.0] - 2026-10-20
|
|
21
|
+
|
|
22
|
+
Rankings, step 3: the head-to-head card.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`rankingMatchup({ title?, category?, date?, source?, a, b, meetings? }, options)`**: the card for the contest about to be fought, for beside a bracket: the two athletes side by side (flag, name, club, ranking and points), a VS between them, and under them their previous meetings (date, event and round, who won, the result) with the record. Each athlete has a colour (`--rk-side-a`, `--rk-side-b`) that marks the panel and the winner of each meeting. `winner` is `0`, `1` or `null` (a draw: a dash); the date is written the language's way; `rows` cuts the list and the rest are counted (`+2 more`); with no meetings the card says so. `describeMatchup`, `matchupRecord(meetings)`.
|
|
27
|
+
- Words for the card in the five languages (the French and Japanese were read by native-language reviewers; the Spanish and German were not).
|
|
28
|
+
- The demo's Rankings section has it (`kview=matchup`), with the meetings on or off. [docs/rankings.md](docs/rankings.md).
|
|
29
|
+
|
|
9
30
|
## [0.12.0] - 2026-10-20
|
|
10
31
|
|
|
11
32
|
Rankings, step 2: the number ones and the entry list.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Give it the games, series, two-legged ties or replays of a tournament and it wor
|
|
|
11
11
|
<img alt="TypeScript" src="https://img.shields.io/badge/types-TypeScript-3178c6">
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
-
<p align="center"><a href="https://johnmorrisdotca.github.io/taisen-brackets/"><strong>Open the demo →</strong></a> · <a href="./docs/layout.md">Layout</a> · <a href="./docs/layouts.md">Layouts</a> · <a href="./docs/drawing.md">Drawing</a> · <a href="./docs/road.md">Road to the final</a> · <a href="./docs/backgrounds.md">Backgrounds</a> · <a href="./docs/judo.md">Judo</a> · <a href="./docs/rankings.md">Rankings</a> · <a href="./docs/formats.md">Formats</a> · <a href="./docs/compare.md">How it compares</a></p>
|
|
14
|
+
<p align="center"><a href="https://johnmorrisdotca.github.io/taisen-brackets/"><strong>Open the demo →</strong></a> · <a href="./docs/layout.md">Layout</a> · <a href="./docs/layouts.md">Layouts</a> · <a href="./docs/drawing.md">Drawing</a> · <a href="./docs/road.md">Road to the final</a> · <a href="./docs/backgrounds.md">Backgrounds</a> · <a href="./docs/judo.md">Judo</a> · <a href="./docs/rankings.md">Rankings</a> · <a href="./docs/medals.md">Medal table</a> · <a href="./docs/formats.md">Formats</a> · <a href="./docs/compare.md">How it compares</a></p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<picture>
|
|
@@ -249,13 +249,21 @@ A judo contest is a tie with a `judo` result: ippon, waza-ari, yuko and shido as
|
|
|
249
249
|
|
|
250
250
|
## Rankings
|
|
251
251
|
|
|
252
|
-
`rankingList` draws a ranking for any ranked sport from your own data (a province's or a club's feed, a federation's list), text first, with no photo slot to leave empty: the rank, a flag (a country's or a Canadian province's, `CA-BC`), the name, the club and the points, with the movement since the last list as arrows or row colours. The top three can be big gold, silver and bronze rows above a compact list, a champion row is marked `C`, tied ranks share a number (`10`, `10`), a newly ranked athlete is `NR`, and points are written to the sport's own precision (`91.60`). `rankingNumberOnes` draws the number one of each category as a card (`#1 in -57 kg`), and `rankingEntryList` an event's entry list in the order of the draw with each athlete's ranking beside the name. `rankingRanks` and `rankingCategoryLabel` are helpers, `describeRanking`, `describeNumberOnes` and `describeEntryList` the descriptions, `NUMBER_ONE_COLORS` the card colours and `BOARD_SIZES` the sizes of every board (a TV, a tablet, a phone, and a square and a 4:5 card for social media). [docs/rankings.md](./docs/rankings.md) · [try it in the demo](https://johnmorrisdotca.github.io/taisen-brackets/#rankings)
|
|
252
|
+
`rankingList` draws a ranking for any ranked sport from your own data (a province's or a club's feed, a federation's list), text first, with no photo slot to leave empty: the rank, a flag (a country's or a Canadian province's, `CA-BC`), the name, the club and the points, with the movement since the last list as arrows or row colours. The top three can be big gold, silver and bronze rows above a compact list, a champion row is marked `C`, tied ranks share a number (`10`, `10`), a newly ranked athlete is `NR`, and points are written to the sport's own precision (`91.60`). `rankingNumberOnes` draws the number one of each category as a card (`#1 in -57 kg`), and `rankingEntryList` an event's entry list in the order of the draw with each athlete's ranking beside the name. `rankingMatchup` draws the head-to-head card of two athletes with their previous meetings and the record. `rankingRanks` and `rankingCategoryLabel` are helpers, `describeRanking`, `describeNumberOnes` and `describeEntryList` the descriptions, `NUMBER_ONE_COLORS` the card colours and `BOARD_SIZES` the sizes of every board (a TV, a tablet, a phone, and a square and a 4:5 card for social media). [docs/rankings.md](./docs/rankings.md) · [try it in the demo](https://johnmorrisdotca.github.io/taisen-brackets/#rankings)
|
|
253
253
|
|
|
254
254
|
<p align="center"><picture>
|
|
255
255
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/ranking-podium-tv-dark.webp">
|
|
256
256
|
<img alt="A ranking list on a TV: three big rows tinted gold, silver and bronze for the first three, and the ranks 4 to 15 as compact rows beside them, each with its rank, movement arrow, round flag, name, club and points." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/ranking-podium-tv-light.webp" width="720">
|
|
257
257
|
</picture><br><em>A made-up provincial ranking (invented names and clubs).</em></p>
|
|
258
258
|
|
|
259
|
+
## Medal table
|
|
260
|
+
|
|
261
|
+
`medalTable` draws a medal table for any sport and event (an Olympic Games, a world championship, a club tournament): ranked gold first or by the total, equal rows sharing a rank (5, 5, 7), a flag or a badge for each country or team (a former country such as the Soviet Union is a row of its own), the three medals and the total, in two columns where the board is wide. `tallyMedals` adds up a list of medals filtered to one sport, one weight class or one Games, and `medalRanks` gives the ranks as data. [docs/medals.md](./docs/medals.md) · [try it in the demo](https://johnmorrisdotca.github.io/taisen-brackets/#medals)
|
|
262
|
+
|
|
263
|
+
<p align="center"><picture>
|
|
264
|
+
<img alt="A medal table on a 4:5 card: ten countries with rank, flag or badge, name, gold, silver and bronze counts in tinted cells and the total." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/medal-table-portrait-light.webp" width="480">
|
|
265
|
+
</picture><br><em>A made-up medal table (invented results).</em></p>
|
|
266
|
+
|
|
259
267
|
## In 30 seconds
|
|
260
268
|
|
|
261
269
|
```sh
|
|
@@ -304,7 +312,8 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
|
|
|
304
312
|
- **A judo scoreboard**: `judoScoreboard` draws one contest for a given state, in two styles (a board, and table-top flip cards) and three sizes, in five languages; live updating is yours.
|
|
305
313
|
- **The road to the final of a judoka**: `judoWins` reads an athlete's wins from a bracket (a top seed's bye included) and `judoRoadPanel` draws them with the opponent's flag, code and name and how each was won.
|
|
306
314
|
- **Judo screens for the hall**: `judoContestCard` (the category, the round, the white athlete over the blue one) and `judoMatQueue` (per mat, a big number and colour and the contests to come in order, with flags and codes), on a TV or a tablet, in five languages.
|
|
307
|
-
- **Rankings**: `rankingList` draws a ranking from your data, text first: rank, flag (a country or a Canadian province), name, club, points, movement arrows, a podium, a champion row, ties and paging; `rankingNumberOnes` the number one of each category and `rankingEntryList` an event's entry list with each athlete's ranking ([docs/rankings.md](./docs/rankings.md)).
|
|
315
|
+
- **Rankings**: `rankingList` draws a ranking from your data, text first: rank, flag (a country or a Canadian province), name, club, points, movement arrows, a podium, a champion row, ties and paging; `rankingNumberOnes` the number one of each category and `rankingEntryList` an event's entry list with each athlete's ranking and `rankingMatchup` a head-to-head card ([docs/rankings.md](./docs/rankings.md)).
|
|
316
|
+
- **A medal table for any sport**: `medalTable` ranks countries or teams gold first or by the total, with shared ranks, flags or badges, filters by sport, weight class and Games through `tallyMedals`, and former countries as rows of their own ([docs/medals.md](./docs/medals.md)).
|
|
308
317
|
- **Judo scoring**: `Matchup.judo` takes ippon, waza-ari, yuko, shido, hansoku-make, golden score and the contest time, checked against the IJF rules (`judoOutcome`); a box shows the marks, the shido cards and how the contest was won, and `judoBracket` adds the repechage and the two bronze medal contests ([docs/judo.md](./docs/judo.md)).
|
|
309
318
|
- **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
|
|
310
319
|
- **Five languages**: English, French, Japanese, Spanish and German, for every word the drawing, the tooltips, the description and the table write, with the football terms of each language; countries named by `Intl` from their ISO code; numbers and dates by `Intl`. Add a language of your own.
|
|
@@ -666,7 +675,8 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
|
|
|
666
675
|
- `matchupsFromGames(games, options)`: matchups from a flat list of games.
|
|
667
676
|
- `resolveMatchup`, `collectTeams`, `describeTie`, `noteParts`, `noteLong`: one tie's result, the teams of a bracket, a tie in a sentence.
|
|
668
677
|
- `judoOutcome(result, winner?)`, `judoBracket(athletes, options?)`, `judoPodium(input)`, `judoCategoryLabel(category, locale?)`, `JUDO_WEIGHT_CLASSES`, `judoSeconds(time)`, `judoClock(seconds)`: judo results checked against the IJF rules, the bracket with its repechage and two bronze medal contests, the medallists, a weight category as a heading ([docs/judo.md](./docs/judo.md)). `judoScoreboard(contest, options?)`, `describeJudoContest(contest, options?)`, `JUDO_BOARD_SIZES`: the scoreboard of a contest as SVG and as a sentence. `judoContestCard(contest, options?)`, `judoMatQueue(mats, options?)`, `describeJudoCard`, `describeJudoQueue`, `JUDO_MAT_COLORS`: the contest card and the mat queue board as SVG and as sentences. `judoWins(bracket, athlete, options?)`, `judoRoadPanel(source, options?)`, `describeJudoRoad`: an athlete's wins on the road to the final, and the panel. `Matchup.judo` and `Matchup.stage` (`"repechage"`, `"bronze"`) carry them into `drawBracket`.
|
|
669
|
-
- `rankingList(source, options?)`, `rankingRanks(entries, options?)`, `rankingCategoryLabel(category, locale?)`, `describeRanking(source, options?)`, `BOARD_SIZES`: a ranking list as SVG, its ranks as data, a category as a heading and the drawing as a sentence. `rankingNumberOnes(source, options?)`, `describeNumberOnes`, `NUMBER_ONE_COLORS`: the number one of each category as cards. `rankingEntryList(source, options?)`, `describeEntryList`: an event's entry list with each athlete's ranking ([docs/rankings.md](./docs/rankings.md)).
|
|
678
|
+
- `rankingList(source, options?)`, `rankingRanks(entries, options?)`, `rankingCategoryLabel(category, locale?)`, `describeRanking(source, options?)`, `BOARD_SIZES`: a ranking list as SVG, its ranks as data, a category as a heading and the drawing as a sentence. `rankingNumberOnes(source, options?)`, `describeNumberOnes`, `NUMBER_ONE_COLORS`: the number one of each category as cards. `rankingEntryList(source, options?)`, `describeEntryList`: an event's entry list with each athlete's ranking. `rankingMatchup(source, options?)`, `describeMatchup`, `matchupRecord(meetings)`: the head-to-head card of two athletes and their meetings ([docs/rankings.md](./docs/rankings.md)).
|
|
679
|
+
- `medalTable(source, options?)`, `medalRanks(rows, options?)`, `tallyMedals(awards, filter?)`, `describeMedalTable(source, options?)`: a medal table as SVG, its ranks as data, a list of medals added up and filtered, and the table as a sentence ([docs/medals.md](./docs/medals.md)).
|
|
670
680
|
- `playoffFormat(league, year)`, `playoffEras(league)`, `LEAGUES`: the formats by era (`nhl`, `nba`, `mlb`, `nfl`, `cfl`, `mls`, `worldcup`). `LEAGUE_EMBLEMS`: each league's default emblem id.
|
|
671
681
|
- `isEmblemId(value)`, and the types `EmblemRenderer`, `EmblemRequest`, `EmblemStyle`: the trophy emblems (see "Emblems for the trophy").
|
|
672
682
|
- `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
|
package/dist/index.d.ts
CHANGED
|
@@ -31,3 +31,7 @@ export { BOARD_SIZES } from "./rank-kit.ts";
|
|
|
31
31
|
export type { BoardOptions, BoardSizeName } from "./rank-kit.ts";
|
|
32
32
|
export { rankingNumberOnes, describeNumberOnes, rankingEntryList, describeEntryList, NUMBER_ONE_COLORS } from "./ranking-cards.ts";
|
|
33
33
|
export type { EntryListOptions, EntryListSource, NumberOnesOptions, NumberOnesSource, RankingCard } from "./ranking-cards.ts";
|
|
34
|
+
export { rankingMatchup, describeMatchup, matchupRecord } from "./ranking-matchup.ts";
|
|
35
|
+
export type { MatchupOptions, MatchupSource, RankingMeeting } from "./ranking-matchup.ts";
|
|
36
|
+
export { medalTable, medalRanks, tallyMedals, describeMedalTable } from "./medals.ts";
|
|
37
|
+
export type { MedalAward, MedalFilter, MedalRow, MedalTableOptions, MedalTableSource } from "./medals.ts";
|
package/dist/index.js
CHANGED
|
@@ -17,3 +17,5 @@ export { judoWins, judoRoadPanel, describeJudoRoad } from "./judo-road.js";
|
|
|
17
17
|
export { rankingList, rankingRanks, rankingCategoryLabel, describeRanking } from "./ranking.js";
|
|
18
18
|
export { BOARD_SIZES } from "./rank-kit.js";
|
|
19
19
|
export { rankingNumberOnes, describeNumberOnes, rankingEntryList, describeEntryList, NUMBER_ONE_COLORS } from "./ranking-cards.js";
|
|
20
|
+
export { rankingMatchup, describeMatchup, matchupRecord } from "./ranking-matchup.js";
|
|
21
|
+
export { medalTable, medalRanks, tallyMedals, describeMedalTable } from "./medals.js";
|
package/dist/locales.js
CHANGED
|
@@ -31,6 +31,8 @@ const EN = {
|
|
|
31
31
|
rankTitle: "Ranking", rankPoints: "points", rankPointsShort: "pts", rankChampion: "C", rankChampionLong: "Champion", rankInterim: "IC", rankInterimLong: "Interim champion",
|
|
32
32
|
rankNew: "NR", rankNewLong: "newly ranked", rankUp: "up {n}", rankDown: "down {n}", rankSame: "unchanged", rankRange: "{a}–{b} of {n}", rankNone: "No ranked athletes yet", rankJoin: ", ",
|
|
33
33
|
rankNumberOnes: "Number ones", rankNumberOneIn: "#1 in {category}", rankEntryTitle: "Entry list", rankColRanking: "Ranking", rankUnranked: "NR",
|
|
34
|
+
rankMatchTitle: "Head to head", rankMeetings: "Previous meetings", rankNoMeetings: "No previous meetings", rankMore: "+{n} more",
|
|
35
|
+
medalTitle: "Medal table", medalNone: "No medals yet", medalGold: "Gold", medalSilver: "Silver", medalBronze: "Bronze", medalGoldShort: "G", medalSilverShort: "S", medalBronzeShort: "B", medalTotal: "Total", medalLine: "{g} gold, {s} silver, {b} bronze, {t} total", medalRank: "{n}",
|
|
34
36
|
};
|
|
35
37
|
const FR = {
|
|
36
38
|
tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
|
|
@@ -58,6 +60,8 @@ const FR = {
|
|
|
58
60
|
rankTitle: "Classement", rankPoints: "points", rankPointsShort: "pts", rankChampion: "C", rankChampionLong: "Champion", rankInterim: "CI", rankInterimLong: "Champion intérimaire",
|
|
59
61
|
rankNew: "Nouv.", rankNewLong: "nouvelle entrée au classement", rankUp: "en hausse de {n}", rankDown: "en baisse de {n}", rankSame: "rang inchangé", rankRange: "{a}–{b} sur {n}", rankNone: "Aucun athlète classé pour l’instant", rankJoin: ", ",
|
|
60
62
|
rankNumberOnes: "Les numéros un", rankNumberOneIn: "N°\u00a01 en {category}", rankEntryTitle: "Liste des engagés", rankColRanking: "Classement", rankUnranked: "NC",
|
|
63
|
+
rankMatchTitle: "Face-à-face", rankMeetings: "Confrontations précédentes", rankNoMeetings: "Aucune confrontation précédente", rankMore: "+{n} de plus",
|
|
64
|
+
medalTitle: "Tableau des médailles", medalNone: "Aucune médaille pour l’instant", medalGold: "Or", medalSilver: "Argent", medalBronze: "Bronze", medalGoldShort: "O", medalSilverShort: "A", medalBronzeShort: "B", medalTotal: "Total", medalLine: "{g} or, {s} argent, {b} bronze, {t} au total", medalRank: "{n}",
|
|
61
65
|
};
|
|
62
66
|
const JA = {
|
|
63
67
|
tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
|
|
@@ -85,6 +89,8 @@ const JA = {
|
|
|
85
89
|
rankTitle: "ランキング", rankPoints: "ポイント", rankPointsShort: "pt", rankChampion: "王者", rankChampionLong: "王者", rankInterim: "暫定", rankInterimLong: "暫定王者",
|
|
86
90
|
rankNew: "NEW", rankNewLong: "新規ランクイン", rankUp: "{n}ランク上昇", rankDown: "{n}ランク下降", rankSame: "変動なし", rankRange: "{n}人中{a}–{b}位", rankNone: "ランキング入りした選手はまだいません", rankJoin: "、",
|
|
87
91
|
rankNumberOnes: "各階級の1位", rankNumberOneIn: "{category} 1位", rankEntryTitle: "エントリーリスト", rankColRanking: "ランキング", rankUnranked: "圏外",
|
|
92
|
+
rankMatchTitle: "直接対決", rankMeetings: "過去の対戦", rankNoMeetings: "過去の対戦はありません", rankMore: "ほか{n}試合",
|
|
93
|
+
medalTitle: "メダル獲得表", medalNone: "まだメダルはありません", medalGold: "金メダル", medalSilver: "銀メダル", medalBronze: "銅メダル", medalGoldShort: "金", medalSilverShort: "銀", medalBronzeShort: "銅", medalTotal: "合計", medalLine: "金{g}、銀{s}、銅{b}、合計{t}", medalRank: "{n}位",
|
|
88
94
|
};
|
|
89
95
|
const ES = {
|
|
90
96
|
tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
|
|
@@ -112,6 +118,8 @@ const ES = {
|
|
|
112
118
|
rankTitle: "Clasificación", rankPoints: "puntos", rankPointsShort: "pts", rankChampion: "C", rankChampionLong: "Campeón", rankInterim: "CI", rankInterimLong: "Campeón interino",
|
|
113
119
|
rankNew: "NUEVO", rankNewLong: "nuevo en la clasificación", rankUp: "sube {n}", rankDown: "baja {n}", rankSame: "sin cambios", rankRange: "{a}–{b} de {n}", rankNone: "Aún no hay deportistas clasificados", rankJoin: ", ",
|
|
114
120
|
rankNumberOnes: "Los números uno", rankNumberOneIn: "N.º 1 en {category}", rankEntryTitle: "Lista de inscritos", rankColRanking: "Clasificación", rankUnranked: "SC",
|
|
121
|
+
rankMatchTitle: "Cara a cara", rankMeetings: "Enfrentamientos anteriores", rankNoMeetings: "Sin enfrentamientos anteriores", rankMore: "+{n} más",
|
|
122
|
+
medalTitle: "Medallero", medalNone: "Aún no hay medallas", medalGold: "Oro", medalSilver: "Plata", medalBronze: "Bronce", medalGoldShort: "O", medalSilverShort: "P", medalBronzeShort: "B", medalTotal: "Total", medalLine: "{g} oro, {s} plata, {b} bronce, {t} en total", medalRank: "{n}",
|
|
115
123
|
};
|
|
116
124
|
const DE = {
|
|
117
125
|
tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
|
|
@@ -139,6 +147,8 @@ const DE = {
|
|
|
139
147
|
rankTitle: "Rangliste", rankPoints: "Punkte", rankPointsShort: "Pkt.", rankChampion: "C", rankChampionLong: "Champion", rankInterim: "IC", rankInterimLong: "Interimschampion",
|
|
140
148
|
rankNew: "NEU", rankNewLong: "neu in der Rangliste", rankUp: "{n} Plätze höher", rankDown: "{n} Plätze tiefer", rankSame: "unverändert", rankRange: "{a}–{b} von {n}", rankNone: "Noch keine platzierten Athleten", rankJoin: ", ",
|
|
141
149
|
rankNumberOnes: "Die Nummer Eins", rankNumberOneIn: "Nr. 1 in {category}", rankEntryTitle: "Meldeliste", rankColRanking: "Rangliste", rankUnranked: "o. R.",
|
|
150
|
+
rankMatchTitle: "Direkter Vergleich", rankMeetings: "Bisherige Duelle", rankNoMeetings: "Noch keine Duelle", rankMore: "+{n} weitere",
|
|
151
|
+
medalTitle: "Medaillenspiegel", medalNone: "Noch keine Medaillen", medalGold: "Gold", medalSilver: "Silber", medalBronze: "Bronze", medalGoldShort: "G", medalSilverShort: "S", medalBronzeShort: "B", medalTotal: "Gesamt", medalLine: "{g} Gold, {s} Silber, {b} Bronze, {t} gesamt", medalRank: "{n}",
|
|
142
152
|
};
|
|
143
153
|
/**
|
|
144
154
|
* The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
|
package/dist/medals.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A medal table for any sport and event (an Olympic Games, a world championship, a club tournament): the rows ranked by gold first (as the Olympic table is) or by the total, equal rows
|
|
3
|
+
* sharing a rank (5, 5, 7), a flag or a badge for each country or team, the three kinds of medal and the total. `tallyMedals` adds up a list of medals, filtered to one sport, one weight class or
|
|
4
|
+
* one Games, so that the same data draws the table of an event, of a sport or of all time. Static SVG for a given state, like every drawing here.
|
|
5
|
+
*/
|
|
6
|
+
import { type BoardOptions } from "./rank-kit.ts";
|
|
7
|
+
/** One row of a medal table: a country or a team and its medals. Only `name` is needed. */
|
|
8
|
+
export interface MedalRow {
|
|
9
|
+
/** The country's or team's name as it is to be written ("France", "Soviet Union", "Unified Team"). */
|
|
10
|
+
name: string;
|
|
11
|
+
/** A code for the `flags` function (`FR`, `CA-BC`); a country that has no flag in it (a former one, a team) is drawn as a badge with `code` or the initials. */
|
|
12
|
+
flag?: string;
|
|
13
|
+
/** A mark of your own (an image address or inline SVG) in place of a flag. */
|
|
14
|
+
mark?: string;
|
|
15
|
+
/** The country's three letters, for the badge of a row with no flag. */
|
|
16
|
+
code?: string;
|
|
17
|
+
gold?: number;
|
|
18
|
+
silver?: number;
|
|
19
|
+
bronze?: number;
|
|
20
|
+
/** A small line under the name: the years a former country existed ("1922–1991"), or what a team is. */
|
|
21
|
+
note?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface MedalTableSource {
|
|
24
|
+
/** The heading. Default: the language's "Medal table". */
|
|
25
|
+
title?: string;
|
|
26
|
+
/** The sport, event or Games as a pill under the heading ("Men's judo +100 kg"). */
|
|
27
|
+
subtitle?: string;
|
|
28
|
+
/** The date or the edition, at the right of the heading. */
|
|
29
|
+
date?: string;
|
|
30
|
+
/** Where the numbers come from, small at the foot. */
|
|
31
|
+
source?: string;
|
|
32
|
+
rows: readonly MedalRow[];
|
|
33
|
+
}
|
|
34
|
+
export interface MedalTableOptions extends BoardOptions {
|
|
35
|
+
/** `gold` (the default): by gold, then silver, then bronze, as the Olympic Games rank. `total`: by the total of medals, then gold, then silver. `given`: the order given, ranked by the same rule as `gold`. */
|
|
36
|
+
sort?: "gold" | "total" | "given";
|
|
37
|
+
/** Equal rows share a rank (`1, 2, 2, 4`). Default true; false numbers every row. */
|
|
38
|
+
ties?: boolean;
|
|
39
|
+
/** `plain` (`10`, the default) or `hash` (`#10`). */
|
|
40
|
+
rankStyle?: "plain" | "hash";
|
|
41
|
+
/** Only the first N rows, after sorting. */
|
|
42
|
+
top?: number;
|
|
43
|
+
/** The most rows drawn, from the first of the page, and which page (0 is the first). Default: as many as fit at a readable size. */
|
|
44
|
+
rows?: number;
|
|
45
|
+
page?: number;
|
|
46
|
+
/** One column or two (the first column runs down before the second starts). Default 1, and 2 on a wide board with more than 10 rows. */
|
|
47
|
+
columns?: 1 | 2;
|
|
48
|
+
/** Mark the first three ranks with a bar in gold, silver and bronze. Default true. */
|
|
49
|
+
highlight?: boolean;
|
|
50
|
+
/** Hide the rows with no medal at all. Default false. */
|
|
51
|
+
skipEmpty?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** A medal won: who won it and what kind, with whatever you filter on. */
|
|
54
|
+
export interface MedalAward {
|
|
55
|
+
name: string;
|
|
56
|
+
flag?: string;
|
|
57
|
+
mark?: string;
|
|
58
|
+
code?: string;
|
|
59
|
+
note?: string;
|
|
60
|
+
medal: "gold" | "silver" | "bronze";
|
|
61
|
+
/** What the medal was won in, for the filter: a sport, a weight class or category, a Games or a year. Any of them can be left out of a filter. */
|
|
62
|
+
sport?: string;
|
|
63
|
+
category?: string;
|
|
64
|
+
games?: string | number;
|
|
65
|
+
}
|
|
66
|
+
/** What `tallyMedals` keeps: awards equal to every field given. A value can be a list (any of them). */
|
|
67
|
+
export interface MedalFilter {
|
|
68
|
+
sport?: string | readonly string[];
|
|
69
|
+
category?: string | readonly string[];
|
|
70
|
+
games?: string | number | readonly (string | number)[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Adds up a list of medals into the rows of a table, one row for each name, keeping the first flag, mark, code and note given for it. `filter` keeps only the medals of one sport, one category or
|
|
74
|
+
* one Games (or a list of them): the same list gives the table of an event, of a sport and of all time. Throws a `RangeError` for a medal that is not gold, silver or bronze or has no name.
|
|
75
|
+
*/
|
|
76
|
+
export declare function tallyMedals(awards: readonly MedalAward[], filter?: MedalFilter): MedalRow[];
|
|
77
|
+
/** The ranks of a list of rows, as `medalTable` draws them (`[1, 2, 2, 4]`), in the order they are drawn. */
|
|
78
|
+
export declare function medalRanks(rows: readonly MedalRow[], options?: Pick<MedalTableOptions, "sort" | "ties">): number[];
|
|
79
|
+
/** The table as a sentence, the drawing's description. */
|
|
80
|
+
export declare function describeMedalTable(source: MedalTableSource, options?: Pick<MedalTableOptions, "locale" | "words" | "sort" | "ties" | "top" | "skipEmpty">): string;
|
|
81
|
+
/**
|
|
82
|
+
* The medal table as an SVG. Throws a `RangeError` for a row with no name, a medal count that is not a whole number from 0, an option that is not one of its values, or a bad size or theme.
|
|
83
|
+
*/
|
|
84
|
+
export declare function medalTable(source: MedalTableSource, options?: MedalTableOptions): string;
|
package/dist/medals.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A medal table for any sport and event (an Olympic Games, a world championship, a club tournament): the rows ranked by gold first (as the Olympic table is) or by the total, equal rows
|
|
3
|
+
* sharing a rank (5, 5, 7), a flag or a badge for each country or team, the three kinds of medal and the total. `tallyMedals` adds up a list of medals, filtered to one sport, one weight class or
|
|
4
|
+
* one Games, so that the same data draws the table of an event, of a sport or of all time. Static SVG for a given state, like every drawing here.
|
|
5
|
+
*/
|
|
6
|
+
import { boardFrame, boardWrap, bw, clampN, fitSize, heading, label } from "./rank-kit.js";
|
|
7
|
+
import { fill, voiceOf } from "./locales.js";
|
|
8
|
+
import { drawMark } from "./marks.js";
|
|
9
|
+
import { abbreviate, fit, num } from "./text.js";
|
|
10
|
+
const KINDS = ["gold", "silver", "bronze"];
|
|
11
|
+
const count = (n) => n ?? 0;
|
|
12
|
+
const totalOf = (r) => count(r.gold) + count(r.silver) + count(r.bronze);
|
|
13
|
+
const matches = (value, want) => want === undefined || (Array.isArray(want) ? want.includes(value) : want === value);
|
|
14
|
+
/**
|
|
15
|
+
* Adds up a list of medals into the rows of a table, one row for each name, keeping the first flag, mark, code and note given for it. `filter` keeps only the medals of one sport, one category or
|
|
16
|
+
* one Games (or a list of them): the same list gives the table of an event, of a sport and of all time. Throws a `RangeError` for a medal that is not gold, silver or bronze or has no name.
|
|
17
|
+
*/
|
|
18
|
+
export function tallyMedals(awards, filter = {}) {
|
|
19
|
+
const rows = new Map();
|
|
20
|
+
awards.forEach((a, i) => {
|
|
21
|
+
if (typeof a?.name !== "string" || a.name.trim() === "")
|
|
22
|
+
throw new RangeError(`taisen-brackets: medal ${i + 1} needs a name`);
|
|
23
|
+
if (!KINDS.includes(a.medal))
|
|
24
|
+
throw new RangeError(`taisen-brackets: medal ${i + 1} is "gold", "silver" or "bronze", not ${JSON.stringify(a.medal)}`);
|
|
25
|
+
if (!matches(a.sport, filter.sport) || !matches(a.category, filter.category) || !matches(a.games, filter.games))
|
|
26
|
+
return;
|
|
27
|
+
let row = rows.get(a.name);
|
|
28
|
+
if (!row) {
|
|
29
|
+
row = { name: a.name, gold: 0, silver: 0, bronze: 0, ...(a.flag ? { flag: a.flag } : {}), ...(a.mark ? { mark: a.mark } : {}), ...(a.code ? { code: a.code } : {}), ...(a.note ? { note: a.note } : {}) };
|
|
30
|
+
rows.set(a.name, row);
|
|
31
|
+
}
|
|
32
|
+
row[a.medal] = count(row[a.medal]) + 1;
|
|
33
|
+
});
|
|
34
|
+
return [...rows.values()];
|
|
35
|
+
}
|
|
36
|
+
/** The rows in the order they are drawn, with their ranks. */
|
|
37
|
+
function ordered(rows, sort, ties) {
|
|
38
|
+
const key = (r) => (sort === "total" ? [totalOf(r), count(r.gold), count(r.silver)] : [count(r.gold), count(r.silver), count(r.bronze)]);
|
|
39
|
+
const list = rows.map((row, i) => ({ row, i }));
|
|
40
|
+
// `given` keeps the order, the others sort by the key, then by the order given.
|
|
41
|
+
if (sort !== "given")
|
|
42
|
+
list.sort((a, b) => { const x = key(a.row), y = key(b.row); for (let k = 0; k < x.length; k++)
|
|
43
|
+
if (x[k] !== y[k])
|
|
44
|
+
return y[k] - x[k]; return a.i - b.i; });
|
|
45
|
+
const same = (a, b) => key(a).every((v, k) => v === key(b)[k]) && totalOf(a) === totalOf(b);
|
|
46
|
+
const out = [];
|
|
47
|
+
list.forEach(({ row }, n) => {
|
|
48
|
+
const prev = out[n - 1];
|
|
49
|
+
out.push({ row, rank: ties && prev && same(prev.row, row) ? prev.rank : n + 1 });
|
|
50
|
+
});
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** The ranks of a list of rows, as `medalTable` draws them (`[1, 2, 2, 4]`), in the order they are drawn. */
|
|
54
|
+
export function medalRanks(rows, options = {}) {
|
|
55
|
+
return ordered(rows, options.sort ?? "gold", options.ties ?? true).map((x) => x.rank);
|
|
56
|
+
}
|
|
57
|
+
function validate(source, options) {
|
|
58
|
+
if (!source || !Array.isArray(source.rows))
|
|
59
|
+
throw new RangeError("taisen-brackets: a medal table needs a list of rows");
|
|
60
|
+
source.rows.forEach((r, i) => {
|
|
61
|
+
if (typeof r?.name !== "string" || r.name.trim() === "")
|
|
62
|
+
throw new RangeError(`taisen-brackets: row ${i + 1} of the medal table needs a name`);
|
|
63
|
+
for (const k of KINDS)
|
|
64
|
+
if (r[k] !== undefined && !(Number.isInteger(r[k]) && r[k] >= 0))
|
|
65
|
+
throw new RangeError(`taisen-brackets: the ${k} medals of ${r.name} are a whole number from 0, not ${JSON.stringify(r[k])}`);
|
|
66
|
+
});
|
|
67
|
+
const oneOf = (name, value, allowed) => {
|
|
68
|
+
if (value !== undefined && !allowed.includes(value))
|
|
69
|
+
throw new RangeError(`taisen-brackets: ${name} is ${allowed.map((a) => JSON.stringify(a)).join(", ")}, not ${JSON.stringify(value)}`);
|
|
70
|
+
};
|
|
71
|
+
oneOf("sort", options.sort, ["gold", "total", "given"]);
|
|
72
|
+
oneOf("rankStyle", options.rankStyle, ["plain", "hash"]);
|
|
73
|
+
oneOf("columns", options.columns, [1, 2]);
|
|
74
|
+
if (options.top !== undefined && !(Number.isInteger(options.top) && options.top >= 1))
|
|
75
|
+
throw new RangeError("taisen-brackets: top is a whole number from 1");
|
|
76
|
+
if (options.rows !== undefined && !(Number.isInteger(options.rows) && options.rows >= 1 && options.rows <= 200))
|
|
77
|
+
throw new RangeError("taisen-brackets: rows is a whole number from 1 to 200");
|
|
78
|
+
if (options.page !== undefined && !(Number.isInteger(options.page) && options.page >= 0))
|
|
79
|
+
throw new RangeError("taisen-brackets: page is a whole number from 0");
|
|
80
|
+
}
|
|
81
|
+
/** The rows that are drawn: sorted, ranked, with the empty ones and all but the top N left out when asked. */
|
|
82
|
+
function chosen(source, options) {
|
|
83
|
+
let list = ordered(source.rows, options.sort ?? "gold", options.ties ?? true);
|
|
84
|
+
if (options.skipEmpty)
|
|
85
|
+
list = list.filter((x) => totalOf(x.row) > 0);
|
|
86
|
+
return options.top ? list.slice(0, options.top) : list;
|
|
87
|
+
}
|
|
88
|
+
/** The table as a sentence, the drawing's description. */
|
|
89
|
+
export function describeMedalTable(source, options = {}) {
|
|
90
|
+
const v = voiceOf(options);
|
|
91
|
+
const w = v.words;
|
|
92
|
+
const parts = [source.title ?? w.medalTitle];
|
|
93
|
+
if (source.subtitle)
|
|
94
|
+
parts.push(source.subtitle);
|
|
95
|
+
if (source.date)
|
|
96
|
+
parts.push(source.date);
|
|
97
|
+
const list = chosen(source, options);
|
|
98
|
+
if (list.length === 0)
|
|
99
|
+
parts.push(w.medalNone);
|
|
100
|
+
for (const { row, rank } of list)
|
|
101
|
+
parts.push([fill(w.medalRank, { n: v.number(rank) }), row.name, fill(w.medalLine, { g: v.number(count(row.gold)), s: v.number(count(row.silver)), b: v.number(count(row.bronze)), t: v.number(totalOf(row)) })].join(w.rankJoin));
|
|
102
|
+
return parts.join(w.sep) + w.end;
|
|
103
|
+
}
|
|
104
|
+
const teamFor = (r, id) => ({ id, name: r.name, abbr: (r.code ?? abbreviate(r.name)).slice(0, 3).toUpperCase(), ...(r.flag ? { flag: r.flag } : {}), ...(r.mark ? { mark: r.mark } : {}) });
|
|
105
|
+
/**
|
|
106
|
+
* The medal table as an SVG. Throws a `RangeError` for a row with no name, a medal count that is not a whole number from 0, an option that is not one of its values, or a bad size or theme.
|
|
107
|
+
*/
|
|
108
|
+
export function medalTable(source, options = {}) {
|
|
109
|
+
validate(source, options);
|
|
110
|
+
const f = boardFrame(options, [source, options.sort, options.ties, options.rankStyle, options.top, options.rows, options.page, options.columns, options.highlight, options.skipEmpty]);
|
|
111
|
+
const { W, H, v } = f;
|
|
112
|
+
const w = v.words;
|
|
113
|
+
const unit = Math.min(W, H);
|
|
114
|
+
const pad = Math.round(unit * 0.035);
|
|
115
|
+
const all = chosen(source, options);
|
|
116
|
+
const columns = options.columns ?? (W / H >= 1.5 && all.length > 10 ? 2 : 1);
|
|
117
|
+
const head = heading(f, source.title ?? w.medalTitle, source.subtitle, source.date, pad, columns === 2 ? W - pad * 2 : Math.min(W - pad * 2, unit * 1.3), source.source);
|
|
118
|
+
const body = [...head.svg];
|
|
119
|
+
const { bx, boxW } = head;
|
|
120
|
+
const colGap = pad;
|
|
121
|
+
const colW = (boxW - (columns - 1) * colGap) / columns;
|
|
122
|
+
const hs = clampN(unit * 0.03, 12, 40);
|
|
123
|
+
const headRowH = hs * 2.1;
|
|
124
|
+
const avail = head.bottom - head.top - headRowH;
|
|
125
|
+
const minRow = Math.max(28, unit * 0.04);
|
|
126
|
+
const GAP = 0.1;
|
|
127
|
+
const perCol = Math.max(1, Math.floor(avail / (minRow * (1 + GAP))));
|
|
128
|
+
const page = options.page ?? 0;
|
|
129
|
+
const per = options.rows ?? perCol * columns;
|
|
130
|
+
const from = Math.min(page * per, Math.max(0, all.length - 1));
|
|
131
|
+
const slice = all.slice(from, from + per);
|
|
132
|
+
const rpc = Math.max(1, Math.ceil(slice.length / columns));
|
|
133
|
+
const rowH = Math.min(unit * 0.085, avail / (rpc * (1 + GAP)));
|
|
134
|
+
const he = Math.min(rowH, colW / 8);
|
|
135
|
+
const digits = (n) => v.number(n);
|
|
136
|
+
const maxMedal = Math.max(1, ...slice.map((x) => Math.max(count(x.row.gold), count(x.row.silver), count(x.row.bronze))));
|
|
137
|
+
const maxTotal = Math.max(1, ...slice.map((x) => totalOf(x.row)));
|
|
138
|
+
const ns = he * 0.4;
|
|
139
|
+
const cellW = Math.max(he * 0.95, bw(digits(maxMedal), ns) + he * 0.4);
|
|
140
|
+
const totW = Math.max(he * 1.2, bw(digits(maxTotal), ns) + he * 0.5);
|
|
141
|
+
const rankTexts = slice.map((x) => `${options.rankStyle === "hash" ? "#" : ""}${v.number(x.rank)}`);
|
|
142
|
+
const rs = he * 0.42;
|
|
143
|
+
const rankW = Math.max(he * 0.8, ...rankTexts.map((t) => bw(t, rs))) + he * 0.2;
|
|
144
|
+
const d = he * 0.58;
|
|
145
|
+
const rowPad = he * 0.2;
|
|
146
|
+
const cellGap = he * 0.08;
|
|
147
|
+
const highlight = options.highlight ?? true;
|
|
148
|
+
for (let c = 0; c < columns; c++) {
|
|
149
|
+
const x0 = bx + c * (colW + colGap);
|
|
150
|
+
const right = x0 + colW - rowPad;
|
|
151
|
+
const cy = head.top + headRowH / 2;
|
|
152
|
+
// The heading over the columns: a disc for each medal, with its letter, and the total.
|
|
153
|
+
const xs = columnX(right, totW, cellW, cellGap);
|
|
154
|
+
KINDS.forEach((_, i) => {
|
|
155
|
+
const r = Math.min(cellW, headRowH) * 0.4;
|
|
156
|
+
const letter = [w.medalGoldShort, w.medalSilverShort, w.medalBronzeShort][i];
|
|
157
|
+
body.push(`<g class="rk-medalhead"><title>${[w.medalGold, w.medalSilver, w.medalBronze][i]}</title><circle class="rk-disc${i + 1}" cx="${num(xs[i])}" cy="${num(cy)}" r="${num(r)}"/>${label(xs[i], cy + fitSize(letter, r * 1.1, r * 1.5, 0.6) * 0.36, fitSize(letter, r * 1.1, r * 1.5, 0.6), letter, "rk-onaccent-dark", "middle", 800)}</g>`);
|
|
158
|
+
});
|
|
159
|
+
const totalText = v.upper(w.medalTotal);
|
|
160
|
+
const tz = fitSize(totalText, hs, totW + cellGap, 0.6);
|
|
161
|
+
body.push(label(xs[3], cy + tz * 0.36, tz, fit(totalText, tz, totW + cellGap + 1, bw), "rk-sub", "middle", 700, ' letter-spacing=".06em"'));
|
|
162
|
+
}
|
|
163
|
+
slice.forEach(({ row, rank }, i) => {
|
|
164
|
+
const col = Math.floor(i / rpc), idx = i % rpc;
|
|
165
|
+
const r = { x: bx + col * (colW + colGap), y: head.top + headRowH + idx * rowH * (1 + GAP), w: colW, h: rowH };
|
|
166
|
+
body.push(drawRow(f, r, row, rank, rankTexts[i], { he, ns, rs, rankW, d, rowPad, cellW, totW, cellGap, highlight }, from + i));
|
|
167
|
+
});
|
|
168
|
+
if (slice.length === 0)
|
|
169
|
+
body.push(label(bx, head.top + hs * 3, hs * 1.2, w.medalNone, "rk-sub", "start", 700));
|
|
170
|
+
if (slice.length < all.length) {
|
|
171
|
+
const t = fill(w.rankRange, { a: v.number(from + 1), b: v.number(from + slice.length), n: v.number(all.length) });
|
|
172
|
+
body.push(label(bx + boxW, head.top - pad * 0.15, hs, fit(t, hs, boxW * 0.35, bw), "rk-sub", "end", 700));
|
|
173
|
+
}
|
|
174
|
+
return boardWrap(f, options.title ?? [source.title ?? w.medalTitle, source.subtitle].filter(Boolean).join(": "), describeMedalTable(source, options), body, "medal-table");
|
|
175
|
+
}
|
|
176
|
+
/** The centres of the gold, silver and bronze columns and of the total's, counted from the right edge. */
|
|
177
|
+
function columnX(right, totW, cellW, gap) {
|
|
178
|
+
const t = right - totW / 2;
|
|
179
|
+
const b = right - totW - gap - cellW / 2;
|
|
180
|
+
const s = b - cellW - gap;
|
|
181
|
+
const g = s - cellW - gap;
|
|
182
|
+
return [g, s, b, t];
|
|
183
|
+
}
|
|
184
|
+
function drawRow(f, r, row, rank, rankText, z, index) {
|
|
185
|
+
const { v } = f;
|
|
186
|
+
const g = [];
|
|
187
|
+
g.push(`<rect class="rk-row" x="${num(r.x)}" y="${num(r.y)}" width="${num(r.w)}" height="${num(r.h)}" rx="${num(Math.min(r.h * 0.18, 16))}"/>`);
|
|
188
|
+
const cy = r.y + r.h / 2;
|
|
189
|
+
if (z.highlight && rank <= 3)
|
|
190
|
+
g.push(`<rect class="rk-bar${rank}" x="${num(r.x)}" y="${num(r.y + r.h * 0.18)}" width="${num(Math.max(4, r.h * 0.08))}" height="${num(r.h * 0.64)}" rx="2"/>`);
|
|
191
|
+
let x = r.x + z.rowPad;
|
|
192
|
+
g.push(label(x + (z.rankW - z.rowPad) / 2, cy + z.rs * 0.36, z.rs, rankText, rank <= 3 && z.highlight ? `rk-num${rank}` : "rk-ink", "middle", 800));
|
|
193
|
+
x += z.rankW;
|
|
194
|
+
g.push(drawMark(teamFor(row, `m${index}`), x + z.d / 2, cy, z.d, f.ctx));
|
|
195
|
+
x += z.d + z.rowPad;
|
|
196
|
+
const right = r.x + r.w - z.rowPad;
|
|
197
|
+
const xs = columnX(right, z.totW, z.cellW, z.cellGap);
|
|
198
|
+
const medals = [row.gold, row.silver, row.bronze].map(count);
|
|
199
|
+
const total = medals.reduce((a, b) => a + b, 0);
|
|
200
|
+
const cellH = r.h * 0.7;
|
|
201
|
+
const cells = [];
|
|
202
|
+
medals.forEach((n, i) => {
|
|
203
|
+
cells.push(`<rect class="${n > 0 ? `rk-cell${i + 1}` : "rk-pill"}" x="${num(xs[i] - z.cellW / 2)}" y="${num(cy - cellH / 2)}" width="${num(z.cellW)}" height="${num(cellH)}" rx="${num(cellH * 0.22)}"${n > 0 ? "" : ' opacity=".55"'}/>`);
|
|
204
|
+
cells.push(label(xs[i], cy + z.ns * 0.36, z.ns, v.number(n), n > 0 ? `rk-num${i + 1}` : "rk-sub", "middle", 800));
|
|
205
|
+
});
|
|
206
|
+
const tx = xs[3];
|
|
207
|
+
cells.push(label(tx, cy + z.ns * 0.36 * 1.1, z.ns * 1.1, v.number(total), "rk-ink", "middle", 800));
|
|
208
|
+
const nameAvail = xs[0] - z.cellW / 2 - z.rowPad - x;
|
|
209
|
+
const note = row.note;
|
|
210
|
+
const name = row.name;
|
|
211
|
+
const twoLine = !!note && z.he >= 40;
|
|
212
|
+
if (twoLine) {
|
|
213
|
+
const nz = fitSize(name, z.he * 0.36, nameAvail, 0.55);
|
|
214
|
+
const sz = fitSize(note, z.he * 0.21, nameAvail, 0.7);
|
|
215
|
+
const block = nz * 1.05 + sz * 1.3;
|
|
216
|
+
const top = cy - block / 2;
|
|
217
|
+
g.push(label(x, top + nz * 0.86, nz, fit(name, nz, nameAvail + 1, bw), "rk-ink", "start", 800));
|
|
218
|
+
g.push(label(x, top + nz * 1.05 + sz * 1.05, sz, fit(note, sz, nameAvail + 1, bw), "rk-sub", "start", 600));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const nz = fitSize(name, z.he * 0.4, nameAvail, 0.55);
|
|
222
|
+
g.push(label(x, cy + nz * 0.36, nz, fit(name, nz, nameAvail + 1, bw), "rk-ink", "start", 800));
|
|
223
|
+
}
|
|
224
|
+
g.push(...cells);
|
|
225
|
+
return `<g class="rk-entry">${g.join("")}</g>`;
|
|
226
|
+
}
|
package/dist/rank-kit.d.ts
CHANGED
|
@@ -67,6 +67,9 @@ export interface BoardPalette {
|
|
|
67
67
|
downRow: string;
|
|
68
68
|
pill: string;
|
|
69
69
|
pillInk: string;
|
|
70
|
+
/** The two sides of a head-to-head card. */
|
|
71
|
+
sideA: string;
|
|
72
|
+
sideB: string;
|
|
70
73
|
/** Gold, silver, bronze: the two ends of the row's gradient, the edge and the numeral. */
|
|
71
74
|
gold: readonly [string, string, string, string];
|
|
72
75
|
silver: readonly [string, string, string, string];
|
|
@@ -88,3 +91,11 @@ export declare function pill(x: number, cy: number, h: number, content: string,
|
|
|
88
91
|
svg: string;
|
|
89
92
|
w: number;
|
|
90
93
|
};
|
|
94
|
+
/** The heading every ranking board has: the title, an optional category pill, the date at the right and the source at the foot. Returns the shapes and the space left. */
|
|
95
|
+
export declare function heading(f: BoardFrame, title: string, category: string | undefined, date: string | undefined, pad: number, maxBoxW: number, source: string | undefined): {
|
|
96
|
+
svg: string[];
|
|
97
|
+
top: number;
|
|
98
|
+
bottom: number;
|
|
99
|
+
bx: number;
|
|
100
|
+
boxW: number;
|
|
101
|
+
};
|
package/dist/rank-kit.js
CHANGED
|
@@ -36,14 +36,14 @@ export const BOARD_LIGHT = {
|
|
|
36
36
|
bg: "#eceff4", ink: "#131822", sub: "#566071", accent: "#2457c5", accentInk: "#ffffff",
|
|
37
37
|
row: "#ffffff", rowAlt: "#f6f8fb", edge: "#d3d9e3", head: "#16233f", headInk: "#ffffff",
|
|
38
38
|
up: "#0f8a46", down: "#c3302a", upRow: "#e0f4e8", downRow: "#fbe3e1",
|
|
39
|
-
pill: "#e3e8f1", pillInk: "#2a3345",
|
|
39
|
+
pill: "#e3e8f1", pillInk: "#2a3345", sideA: "#1d4ed8", sideB: "#c2410c",
|
|
40
40
|
gold: ["#f8e9b5", "#fdf6dc", "#d4a929", "#7d5d00"], silver: ["#e4e8ee", "#f3f5f8", "#a9b2c0", "#4f586a"], bronze: ["#f2d5ba", "#faeadb", "#c98650", "#82451a"],
|
|
41
41
|
};
|
|
42
42
|
export const BOARD_DARK = {
|
|
43
43
|
bg: "#0b0f17", ink: "#f0f3f9", sub: "#98a3b8", accent: "#6ea2ff", accentInk: "#0b0f17",
|
|
44
44
|
row: "#161d2b", rowAlt: "#121826", edge: "#27324a", head: "#1b2742", headInk: "#f0f3f9",
|
|
45
45
|
up: "#4fdc8b", down: "#ff7b72", upRow: "#123724", downRow: "#451d1d",
|
|
46
|
-
pill: "#25304a", pillInk: "#dfe6f5",
|
|
46
|
+
pill: "#25304a", pillInk: "#dfe6f5", sideA: "#6ea2ff", sideB: "#ff9a5c",
|
|
47
47
|
gold: ["#5a4813", "#2c2512", "#d0aa3a", "#f5d263"], silver: ["#434c60", "#232a38", "#9ba6bb", "#e3e8f2"], bronze: ["#58341b", "#2b1d14", "#c4854f", "#f3a96f"],
|
|
48
48
|
};
|
|
49
49
|
const MEDALS = ["gold", "silver", "bronze"];
|
|
@@ -70,6 +70,8 @@ export function boardRules(p, scope, before, extra) {
|
|
|
70
70
|
sel(".rk-pillink", `fill:${v("pill-ink", p.pillInk)}`),
|
|
71
71
|
sel(".rk-nr", `fill:${v("accent", p.accent)}`),
|
|
72
72
|
sel(".rk-nrink", `fill:${v("accent-ink", p.accentInk)}`),
|
|
73
|
+
sel(".rk-sa", `fill:${v("side-a", p.sideA)}`),
|
|
74
|
+
sel(".rk-sb", `fill:${v("side-b", p.sideB)}`),
|
|
73
75
|
sel(".rk-onaccent", "fill:#ffffff"),
|
|
74
76
|
sel(".rk-onaccent-dark", "fill:#15181a"),
|
|
75
77
|
sel(".tb-ring", `fill:none;stroke:rgba(0,0,0,.28);stroke-width:1.5`),
|
|
@@ -78,6 +80,7 @@ export function boardRules(p, scope, before, extra) {
|
|
|
78
80
|
];
|
|
79
81
|
MEDALS.forEach((m, i) => {
|
|
80
82
|
const c = p[m];
|
|
83
|
+
out.push(sel(`.rk-cell${i + 1}`, `fill:${c[0]}`), sel(`.rk-disc${i + 1}`, `fill:${c[2]}`));
|
|
81
84
|
out.push(sel(`.rk-ga${i + 1}`, `stop-color:${c[0]}`), sel(`.rk-gb${i + 1}`, `stop-color:${c[1]}`), sel(`.rk-medal${i + 1}`, `stroke:${c[2]};stroke-width:2`), sel(`.rk-num${i + 1}`, `fill:${c[3]}`), sel(`.rk-bar${i + 1}`, `fill:${c[2]}`));
|
|
82
85
|
});
|
|
83
86
|
if (extra)
|
|
@@ -124,3 +127,31 @@ export function pill(x, cy, h, content, maxW, cls = "rk-pill", inkCls = "rk-pill
|
|
|
124
127
|
const x0 = anchor === "start" ? x : x - w;
|
|
125
128
|
return { svg: `${rect(x0, cy - h / 2, w, h, cls, h / 2)}${label(x0 + w / 2, cy + fs * 0.36, fs, fit(content, fs, w, bw), inkCls, "middle", 800, ' letter-spacing=".03em"')}`, w };
|
|
126
129
|
}
|
|
130
|
+
// ------------------------------------------------------------------------------------------------ shared header
|
|
131
|
+
/** The heading every ranking board has: the title, an optional category pill, the date at the right and the source at the foot. Returns the shapes and the space left. */
|
|
132
|
+
export function heading(f, title, category, date, pad, maxBoxW, source) {
|
|
133
|
+
const { W, H } = f;
|
|
134
|
+
const unit = Math.min(W, H);
|
|
135
|
+
const boxW = Math.min(W - pad * 2, maxBoxW);
|
|
136
|
+
const bx = (W - boxW) / 2;
|
|
137
|
+
const hs = clampN(unit * 0.05, 18, 70);
|
|
138
|
+
const cs = hs * 0.58;
|
|
139
|
+
const svg = [];
|
|
140
|
+
let y = pad;
|
|
141
|
+
const dateW = date ? Math.min(boxW * 0.4, bw(date, cs) + 4) : 0;
|
|
142
|
+
const titleAvail = boxW - (dateW ? dateW + pad : 0);
|
|
143
|
+
const ts = fitSize(title, hs, titleAvail, 0.5);
|
|
144
|
+
svg.push(label(bx, y + hs * 0.85, ts, fit(title, ts, titleAvail + 1, bw), "rk-ink", "start", 800));
|
|
145
|
+
if (date)
|
|
146
|
+
svg.push(label(bx + boxW, y + hs * 0.85, cs, fit(date, cs, dateW, bw), "rk-sub", "end", 700));
|
|
147
|
+
y += hs * 1.05;
|
|
148
|
+
if (category)
|
|
149
|
+
svg.push(pill(bx, y + cs * 0.7, cs * 1.35, category, boxW * 0.62, "rk-accent", "rk-accentink").svg);
|
|
150
|
+
y += category ? cs * 1.35 + pad * 0.7 : pad * 0.35;
|
|
151
|
+
const footerH = source ? Math.max(14, unit * 0.026) + pad * 0.5 : 0;
|
|
152
|
+
if (source) {
|
|
153
|
+
const fs = Math.max(12, unit * 0.022);
|
|
154
|
+
svg.push(label(bx, H - pad - fs * 0.2, fs, fit(source, fs, boxW, bw), "rk-sub", "start", 600));
|
|
155
|
+
}
|
|
156
|
+
return { svg, top: y, bottom: H - pad - footerH, bx, boxW };
|
|
157
|
+
}
|
package/dist/ranking-cards.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* points and "#1 in -57 kg", no photo. `rankingEntryList`: an event's entry list, in the order of the draw, with each athlete's ranking beside the name (the seeding view).
|
|
4
4
|
* Static SVG for a given state, like every drawing here.
|
|
5
5
|
*/
|
|
6
|
-
import { boardFrame, boardWrap, bw, clampN, fitSize, label, pill } from "./rank-kit.js";
|
|
6
|
+
import { boardFrame, boardWrap, bw, clampN, fitSize, heading, label, pill } from "./rank-kit.js";
|
|
7
7
|
import { inkOn } from "./judo-sheets.js";
|
|
8
8
|
import { fill, voiceOf } from "./locales.js";
|
|
9
9
|
import { drawMark } from "./marks.js";
|
|
@@ -297,31 +297,3 @@ export function rankingEntryList(source, options = {}) {
|
|
|
297
297
|
}
|
|
298
298
|
return boardWrap(f, options.title ?? [title, source.category].filter(Boolean).join(": "), describeEntryList(source, options), body, "entry-list");
|
|
299
299
|
}
|
|
300
|
-
// ------------------------------------------------------------------------------------------------ shared header
|
|
301
|
-
/** The heading every ranking board has: the title, an optional category pill, the date at the right and the source at the foot. Returns the shapes and the space left. */
|
|
302
|
-
function heading(f, title, category, date, pad, maxBoxW, source) {
|
|
303
|
-
const { W, H } = f;
|
|
304
|
-
const unit = Math.min(W, H);
|
|
305
|
-
const boxW = Math.min(W - pad * 2, maxBoxW);
|
|
306
|
-
const bx = (W - boxW) / 2;
|
|
307
|
-
const hs = clampN(unit * 0.05, 18, 70);
|
|
308
|
-
const cs = hs * 0.58;
|
|
309
|
-
const svg = [];
|
|
310
|
-
let y = pad;
|
|
311
|
-
const dateW = date ? Math.min(boxW * 0.4, bw(date, cs) + 4) : 0;
|
|
312
|
-
const titleAvail = boxW - (dateW ? dateW + pad : 0);
|
|
313
|
-
const ts = fitSize(title, hs, titleAvail, 0.5);
|
|
314
|
-
svg.push(label(bx, y + hs * 0.85, ts, fit(title, ts, titleAvail + 1, bw), "rk-ink", "start", 800));
|
|
315
|
-
if (date)
|
|
316
|
-
svg.push(label(bx + boxW, y + hs * 0.85, cs, fit(date, cs, dateW, bw), "rk-sub", "end", 700));
|
|
317
|
-
y += hs * 1.05;
|
|
318
|
-
if (category)
|
|
319
|
-
svg.push(pill(bx, y + cs * 0.7, cs * 1.35, category, boxW * 0.62, "rk-accent", "rk-accentink").svg);
|
|
320
|
-
y += category ? cs * 1.35 + pad * 0.7 : pad * 0.35;
|
|
321
|
-
const footerH = source ? Math.max(14, unit * 0.026) + pad * 0.5 : 0;
|
|
322
|
-
if (source) {
|
|
323
|
-
const fs = Math.max(12, unit * 0.022);
|
|
324
|
-
svg.push(label(bx, H - pad - fs * 0.2, fs, fit(source, fs, boxW, bw), "rk-sub", "start", 600));
|
|
325
|
-
}
|
|
326
|
-
return { svg, top: y, bottom: H - pad - footerH, bx, boxW };
|
|
327
|
-
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The head-to-head card: two athletes side by side (flag, name, club, ranking, points), a VS between them, and under them their previous meetings (date, event and round, who won, the
|
|
3
|
+
* result) with the record. It is the card shown next to a bracket for the contest about to be fought. Text first, no photo. Static SVG for a given state, like every drawing here.
|
|
4
|
+
*/
|
|
5
|
+
import { type BoardOptions } from "./rank-kit.ts";
|
|
6
|
+
import { type RankingEntry, type RankingOptions } from "./ranking.ts";
|
|
7
|
+
/** One previous meeting of the two athletes. */
|
|
8
|
+
export interface RankingMeeting {
|
|
9
|
+
/** The date, as an ISO date (`2026-05-17`, written the language's way) or any text. */
|
|
10
|
+
date?: string;
|
|
11
|
+
/** The event ("Provincial Championships") and the round ("Final", "Semi-final"). */
|
|
12
|
+
event?: string;
|
|
13
|
+
round?: string;
|
|
14
|
+
/** Who won: `0` for the first athlete, `1` for the second, `null` or left out for a draw or no result. */
|
|
15
|
+
winner?: 0 | 1 | null;
|
|
16
|
+
/** How it ended, as it is to be written ("ippon · 1:12", "7–2"). */
|
|
17
|
+
result?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface MatchupSource {
|
|
20
|
+
/** The heading. Default: the language's "Head to head". */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** The category as a pill under the heading ("U18 · Men -60 kg"). */
|
|
23
|
+
category?: string;
|
|
24
|
+
/** The event or the date, at the right of the heading. */
|
|
25
|
+
date?: string;
|
|
26
|
+
/** Where the data comes from, small at the foot. */
|
|
27
|
+
source?: string;
|
|
28
|
+
/** The two athletes: an entry's `rank` is its ranking and `points` its points. */
|
|
29
|
+
a: RankingEntry;
|
|
30
|
+
b: RankingEntry;
|
|
31
|
+
/** The previous meetings, in the order they are to be written (the latest first, as a broadcast does). */
|
|
32
|
+
meetings?: readonly RankingMeeting[];
|
|
33
|
+
}
|
|
34
|
+
export interface MatchupOptions extends BoardOptions, Pick<RankingOptions, "nameStyle" | "precision" | "rankStyle"> {
|
|
35
|
+
/** The most meetings drawn. Default: as many as fit at a readable size. The rest are counted ("+2 more"); the record counts them all. */
|
|
36
|
+
rows?: number;
|
|
37
|
+
}
|
|
38
|
+
/** The record of the meetings: how many each athlete won. A draw or a meeting with no winner is in neither. */
|
|
39
|
+
export declare function matchupRecord(meetings?: readonly RankingMeeting[]): [number, number];
|
|
40
|
+
/** The card as a sentence, the drawing's description. */
|
|
41
|
+
export declare function describeMatchup(source: MatchupSource, options?: Pick<MatchupOptions, "locale" | "words" | "nameStyle" | "precision" | "rankStyle">): string;
|
|
42
|
+
/**
|
|
43
|
+
* The head-to-head card for two athletes and their meetings. Throws a `RangeError` for an athlete with no name, a ranking that is not a whole number from 1, a meeting whose winner is not
|
|
44
|
+
* 0, 1 or null, `rows` that is not a whole number from 1 to 50, or a bad size or theme.
|
|
45
|
+
*/
|
|
46
|
+
export declare function rankingMatchup(source: MatchupSource, options?: MatchupOptions): string;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The head-to-head card: two athletes side by side (flag, name, club, ranking, points), a VS between them, and under them their previous meetings (date, event and round, who won, the
|
|
3
|
+
* result) with the record. It is the card shown next to a bracket for the contest about to be fought. Text first, no photo. Static SVG for a given state, like every drawing here.
|
|
4
|
+
*/
|
|
5
|
+
import { boardFrame, boardWrap, bw, clampN, fitSize, heading, label } from "./rank-kit.js";
|
|
6
|
+
import { fill, voiceOf } from "./locales.js";
|
|
7
|
+
import { drawMark } from "./marks.js";
|
|
8
|
+
import { abbreviate, fit, fitLines, num } from "./text.js";
|
|
9
|
+
import { nameText, pointsText, teamFor } from "./ranking.js";
|
|
10
|
+
/** The record of the meetings: how many each athlete won. A draw or a meeting with no winner is in neither. */
|
|
11
|
+
export function matchupRecord(meetings = []) {
|
|
12
|
+
return [meetings.filter((m) => m.winner === 0).length, meetings.filter((m) => m.winner === 1).length];
|
|
13
|
+
}
|
|
14
|
+
const nameOf = (e) => (typeof e.name === "string" ? e.name : e.name.surname);
|
|
15
|
+
function check(e, side) {
|
|
16
|
+
const n = e?.name;
|
|
17
|
+
const ok = typeof n === "string" ? n.trim() !== "" : !!n && typeof n === "object" && typeof n.surname === "string" && n.surname.trim() !== "";
|
|
18
|
+
if (!ok)
|
|
19
|
+
throw new RangeError(`taisen-brackets: athlete ${side} of the head-to-head needs a name`);
|
|
20
|
+
if (e.rank !== undefined && !(Number.isInteger(e.rank) && e.rank >= 1))
|
|
21
|
+
throw new RangeError(`taisen-brackets: the ranking of ${nameOf(e)} is a whole number from 1`);
|
|
22
|
+
}
|
|
23
|
+
const rankLine = (e, v, hash) => (e.rank === undefined ? v.words.rankUnranked : `${hash ? "#" : ""}${v.number(e.rank)}`);
|
|
24
|
+
const meetingDate = (m, v) => (m.date === undefined ? "" : v.date(m.date) ?? m.date);
|
|
25
|
+
/** The card as a sentence, the drawing's description. */
|
|
26
|
+
export function describeMatchup(source, options = {}) {
|
|
27
|
+
const v = voiceOf(options);
|
|
28
|
+
const w = v.words;
|
|
29
|
+
const style = options.nameStyle ?? "surname-first";
|
|
30
|
+
const an = nameText(source.a.name, style, v), bn = nameText(source.b.name, style, v);
|
|
31
|
+
const parts = [`${source.title ?? w.rankMatchTitle}: ${fill(w.tieVs, { a: an, b: bn })}`];
|
|
32
|
+
if (source.category)
|
|
33
|
+
parts.push(source.category);
|
|
34
|
+
if (source.date)
|
|
35
|
+
parts.push(source.date);
|
|
36
|
+
for (const e of [source.a, source.b]) {
|
|
37
|
+
const bits = [nameText(e.name, style, v), `${w.rankColRanking} ${rankLine(e, v, false)}`];
|
|
38
|
+
const p = pointsText(e.points, options.precision, v);
|
|
39
|
+
if (p)
|
|
40
|
+
bits.push(`${p} ${w.rankPoints}`);
|
|
41
|
+
parts.push(bits.join(w.rankJoin));
|
|
42
|
+
}
|
|
43
|
+
const meetings = source.meetings ?? [];
|
|
44
|
+
if (meetings.length === 0)
|
|
45
|
+
parts.push(w.rankNoMeetings);
|
|
46
|
+
else {
|
|
47
|
+
const [x, y] = matchupRecord(meetings);
|
|
48
|
+
parts.push(`${w.rankMeetings} ${fill(w.scoreText, { a: v.number(x), b: v.number(y) })}`);
|
|
49
|
+
for (const m of meetings) {
|
|
50
|
+
const bits = [meetingDate(m, v), [m.event, m.round].filter(Boolean).join(" "), m.winner === 0 || m.winner === 1 ? nameText((m.winner === 0 ? source.a : source.b).name, style, v) : "", m.result ?? ""].filter(Boolean);
|
|
51
|
+
parts.push(bits.join(w.rankJoin));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return parts.join(w.sep) + w.end;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The head-to-head card for two athletes and their meetings. Throws a `RangeError` for an athlete with no name, a ranking that is not a whole number from 1, a meeting whose winner is not
|
|
58
|
+
* 0, 1 or null, `rows` that is not a whole number from 1 to 50, or a bad size or theme.
|
|
59
|
+
*/
|
|
60
|
+
export function rankingMatchup(source, options = {}) {
|
|
61
|
+
if (!source)
|
|
62
|
+
throw new RangeError("taisen-brackets: a head-to-head needs two athletes");
|
|
63
|
+
check(source.a, "a");
|
|
64
|
+
check(source.b, "b");
|
|
65
|
+
const meetings = source.meetings ?? [];
|
|
66
|
+
if (!Array.isArray(meetings))
|
|
67
|
+
throw new RangeError("taisen-brackets: meetings is a list");
|
|
68
|
+
meetings.forEach((m, i) => {
|
|
69
|
+
if (m.winner !== undefined && m.winner !== null && m.winner !== 0 && m.winner !== 1)
|
|
70
|
+
throw new RangeError(`taisen-brackets: the winner of meeting ${i + 1} is 0 (the first athlete), 1 (the second) or null, not ${JSON.stringify(m.winner)}`);
|
|
71
|
+
});
|
|
72
|
+
if (options.rows !== undefined && !(Number.isInteger(options.rows) && options.rows >= 1 && options.rows <= 50))
|
|
73
|
+
throw new RangeError("taisen-brackets: rows is a whole number from 1 to 50");
|
|
74
|
+
const f = boardFrame(options, [source, options.nameStyle, options.precision, options.rankStyle, options.rows]);
|
|
75
|
+
const { W, H, v } = f;
|
|
76
|
+
const w = v.words;
|
|
77
|
+
const unit = Math.min(W, H);
|
|
78
|
+
const pad = Math.round(unit * 0.035);
|
|
79
|
+
const style = options.nameStyle ?? "surname-first";
|
|
80
|
+
const title = source.title ?? w.rankMatchTitle;
|
|
81
|
+
const body = [];
|
|
82
|
+
const head = heading(f, title, source.category, source.date, pad, W - pad * 2, source.source);
|
|
83
|
+
body.push(...head.svg);
|
|
84
|
+
const { bx, boxW } = head;
|
|
85
|
+
const avail = head.bottom - head.top;
|
|
86
|
+
const vsW = clampN(unit * 0.12, 56, 150);
|
|
87
|
+
const panelW = (boxW - vsW) / 2;
|
|
88
|
+
const hasMeetings = meetings.length > 0;
|
|
89
|
+
const panelH = Math.min(hasMeetings ? avail * 0.5 : avail * 0.9, unit * (panelW / unit < 0.6 ? 0.5 : 0.42));
|
|
90
|
+
const names = [nameText(source.a.name, style, v), nameText(source.b.name, style, v)];
|
|
91
|
+
[source.a, source.b].forEach((e, k) => body.push(drawPanel(f, { x: bx + k * (panelW + vsW), y: head.top, w: panelW, h: panelH }, e, names[k], k === 0 ? "rk-sa" : "rk-sb", options, k)));
|
|
92
|
+
// The VS between the panels.
|
|
93
|
+
const vr = Math.min(vsW * 0.42, panelH * 0.3);
|
|
94
|
+
const vcx = bx + panelW + vsW / 2, vcy = head.top + panelH / 2;
|
|
95
|
+
const vsText = v.upper(w.vs);
|
|
96
|
+
const vsSize = fitSize(vsText, vr * 0.95, vr * 1.55, 0.5);
|
|
97
|
+
body.push(`<circle class="rk-pill" cx="${num(vcx)}" cy="${num(vcy)}" r="${num(vr)}"/>`, label(vcx, vcy + vsSize * 0.36, vsSize, vsText, "rk-pillink", "middle", 800));
|
|
98
|
+
// The meetings.
|
|
99
|
+
const mTop = head.top + panelH + pad;
|
|
100
|
+
const hs = clampN(unit * 0.026, 11, 34);
|
|
101
|
+
const lineH = hs * 1.9;
|
|
102
|
+
if (!hasMeetings) {
|
|
103
|
+
body.push(label(bx, mTop + hs * 1.2, hs * 1.15, w.rankNoMeetings, "rk-sub", "start", 700));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const [ra, rb] = matchupRecord(meetings);
|
|
107
|
+
const headText = v.upper(w.rankMeetings);
|
|
108
|
+
body.push(label(bx, mTop + hs * 1.1, hs, fit(headText, hs, boxW * 0.5, bw), "rk-sub", "start", 700, ' letter-spacing=".08em"'));
|
|
109
|
+
// The record at the right: the first athlete's wins in his colour, a dash, the second's.
|
|
110
|
+
const rs = hs * 1.7;
|
|
111
|
+
const dashW = bw("–", rs), bW = bw(v.number(rb), rs);
|
|
112
|
+
const rx = bx + boxW;
|
|
113
|
+
body.push(label(rx - bW - dashW * 1.3, mTop + hs * 1.35, rs, v.number(ra), "rk-sa", "end", 800), label(rx - bW - dashW * 0.65, mTop + hs * 1.35, rs, "–", "rk-sub", "middle", 800), label(rx, mTop + hs * 1.35, rs, v.number(rb), "rk-sb", "end", 800));
|
|
114
|
+
const listTop = mTop + lineH * 1.15;
|
|
115
|
+
const minRow = Math.max(26, unit * 0.04);
|
|
116
|
+
const gapR = 0.12;
|
|
117
|
+
const cap = Math.max(1, Math.floor((head.bottom - listTop) / (minRow * (1 + gapR))));
|
|
118
|
+
const shownN = Math.min(options.rows ?? meetings.length, meetings.length, cap);
|
|
119
|
+
const rowH = Math.min(unit * 0.085, (head.bottom - listTop) / (shownN * (1 + gapR)));
|
|
120
|
+
const he = Math.min(rowH, boxW / 11);
|
|
121
|
+
meetings.slice(0, shownN).forEach((m, i) => {
|
|
122
|
+
const r = { x: bx, y: listTop + i * rowH * (1 + gapR), w: boxW, h: rowH };
|
|
123
|
+
body.push(drawMeeting(f, r, m, source, he, style));
|
|
124
|
+
});
|
|
125
|
+
if (shownN < meetings.length)
|
|
126
|
+
body.push(label(bx + boxW, head.bottom - 2, hs, fill(w.rankMore, { n: v.number(meetings.length - shownN) }), "rk-sub", "end", 700));
|
|
127
|
+
}
|
|
128
|
+
return boardWrap(f, options.title ?? [title, names.join(" – ")].join(": "), describeMatchup(source, options), body, "matchup");
|
|
129
|
+
}
|
|
130
|
+
function drawPanel(f, r, e, name, sideCls, options, k) {
|
|
131
|
+
const { v } = f;
|
|
132
|
+
const w = v.words;
|
|
133
|
+
const g = [];
|
|
134
|
+
const rx = Math.min(r.h * 0.08, 22);
|
|
135
|
+
g.push(`<rect class="rk-row" x="${num(r.x)}" y="${num(r.y)}" width="${num(r.w)}" height="${num(r.h)}" rx="${num(rx)}"/>`);
|
|
136
|
+
const bar = Math.max(6, r.h * 0.035);
|
|
137
|
+
g.push(`<rect class="${sideCls}" x="${num(r.x + rx)}" y="${num(r.y)}" width="${num(r.w - rx * 2)}" height="${num(bar)}"/>`);
|
|
138
|
+
const he = Math.min(r.h, r.w / 1.5);
|
|
139
|
+
const pad = he * 0.09;
|
|
140
|
+
const d = Math.min(he * 0.34, r.w * 0.3);
|
|
141
|
+
const y0 = r.y + bar + pad * 1.4;
|
|
142
|
+
const hasMark = !!(e.flag || e.mark);
|
|
143
|
+
let x = r.x + pad * 1.2;
|
|
144
|
+
if (hasMark) {
|
|
145
|
+
g.push(drawMark(teamFor(e, `m${k}`, name, abbreviate(e.club ?? name)), x + d / 2, y0 + d / 2, d, f.ctx));
|
|
146
|
+
x += d + pad;
|
|
147
|
+
}
|
|
148
|
+
const nameW = r.x + r.w - pad * 1.2 - x;
|
|
149
|
+
const second = e.club ?? e.country ?? "";
|
|
150
|
+
const ss = Math.min(he * 0.075, 26);
|
|
151
|
+
const blockH = Math.max(d, he * 0.26);
|
|
152
|
+
const fl = fitLines(name, nameW, blockH - (second ? ss * 1.4 : 0), he * 0.2);
|
|
153
|
+
const total = fl.height + (second ? ss * 1.35 : 0);
|
|
154
|
+
let ty = y0 + (blockH - total) / 2;
|
|
155
|
+
fl.lines.forEach((line, i) => g.push(label(x, ty + fl.size * (i + 0.86), fl.size, fit(line, fl.size, nameW + 1, bw), "rk-ink", "start", 800)));
|
|
156
|
+
ty += fl.height;
|
|
157
|
+
if (second) {
|
|
158
|
+
const s2 = fitSize(second, ss, nameW, 0.7);
|
|
159
|
+
g.push(label(x, ty + s2 * 1.05, s2, fit(second, s2, nameW + 1, bw), "rk-sub", "start", 600));
|
|
160
|
+
}
|
|
161
|
+
// The ranking and the points along the foot.
|
|
162
|
+
const ranked = rankLine(e, v, options.rankStyle === "hash");
|
|
163
|
+
const pts = pointsText(e.points, options.precision, v);
|
|
164
|
+
const left = r.x + pad * 1.2, right = r.x + r.w - pad * 1.2;
|
|
165
|
+
let rs = he * 0.3;
|
|
166
|
+
const ps = he * 0.18;
|
|
167
|
+
const lab = w.rankColRanking;
|
|
168
|
+
const ls = Math.max(he * 0.07, 10);
|
|
169
|
+
const needed = (size) => Math.max(bw(ranked, size), bw(v.upper(lab), ls * 1.1)) + (pts ? Math.min(bw(pts, ps), right - left) * 0.5 + pad * 2 : 0);
|
|
170
|
+
while (rs > 14 && needed(rs) > right - left)
|
|
171
|
+
rs *= 0.93;
|
|
172
|
+
const baseY = r.y + r.h - pad * 1.2;
|
|
173
|
+
g.push(label(left, baseY - rs * 0.74 - ls * 0.5, ls, fit(v.upper(lab), ls, right - left, bw), "rk-sub", "start", 700, ' letter-spacing=".1em"'));
|
|
174
|
+
g.push(label(left, baseY, rs, ranked, "rk-ink", "start", 800));
|
|
175
|
+
if (pts) {
|
|
176
|
+
const pz = fitSize(pts, ps * 1.5, right - left - Math.max(bw(ranked, rs), bw(v.upper(lab), ls * 1.1)) - pad * 2, 0.5);
|
|
177
|
+
g.push(label(right, baseY - pz * 0.74 - ls * 0.5, ls, fit(v.upper(w.rankPoints), ls, right - left, bw), "rk-sub", "end", 700, ' letter-spacing=".1em"'));
|
|
178
|
+
g.push(label(right, baseY, pz, pts, "rk-ink", "end", 800));
|
|
179
|
+
}
|
|
180
|
+
return `<g class="rk-panel">${g.join("")}</g>`;
|
|
181
|
+
}
|
|
182
|
+
function drawMeeting(f, r, m, source, he, style) {
|
|
183
|
+
const { v } = f;
|
|
184
|
+
const g = [];
|
|
185
|
+
g.push(`<rect class="rk-row" x="${num(r.x)}" y="${num(r.y)}" width="${num(r.w)}" height="${num(r.h)}" rx="${num(Math.min(r.h * 0.18, 16))}"/>`);
|
|
186
|
+
const cy = r.y + r.h / 2;
|
|
187
|
+
const pad = he * 0.25;
|
|
188
|
+
const date = meetingDate(m, v);
|
|
189
|
+
const winnerEntry = m.winner === 0 ? source.a : m.winner === 1 ? source.b : null;
|
|
190
|
+
const winName = winnerEntry ? nameText(winnerEntry.name, style, v) : "";
|
|
191
|
+
// The type steps down until the date, the winner and the result fit side by side; the event and round take what is left, if there is room.
|
|
192
|
+
let fs = he * 0.36;
|
|
193
|
+
const need = (z) => (date ? bw(date, z) + pad * 1.5 : 0) + (m.result ? bw(m.result, z) + pad * 1.5 : 0) + bw(winName, z) + z * 1.4 + pad * 2;
|
|
194
|
+
while (fs > 11 && need(fs) > r.w)
|
|
195
|
+
fs *= 0.94;
|
|
196
|
+
let x = r.x + pad;
|
|
197
|
+
const right = r.x + r.w - pad;
|
|
198
|
+
const dateW = date ? bw(date, fs) : 0;
|
|
199
|
+
if (date) {
|
|
200
|
+
g.push(label(x, cy + fs * 0.36, fs, fit(date, fs, dateW + 1, bw), "rk-sub", "start", 700));
|
|
201
|
+
x += dateW + pad * 1.5;
|
|
202
|
+
}
|
|
203
|
+
const resultW = m.result ? Math.min(r.w * 0.4, bw(m.result, fs)) : 0;
|
|
204
|
+
const winW = Math.min(r.w * 0.4, Math.max(bw(winName, fs) + fs * 1.4, fs * 2));
|
|
205
|
+
// The result at the right, then the winner before it, then the event and round in what is left.
|
|
206
|
+
const resultSvg = m.result ? label(right, cy + fs * 0.36, fs, fit(m.result, fs, resultW + 1, bw), "rk-ink", "end", 700) : "";
|
|
207
|
+
const wx = right - resultW - (resultW ? pad * 1.5 : 0) - winW;
|
|
208
|
+
const eventText = [m.event, m.round].filter(Boolean).join(" · ");
|
|
209
|
+
const eventW = wx - x - pad;
|
|
210
|
+
if (eventText && eventW > he * 2.5)
|
|
211
|
+
g.push(label(x, cy + fs * 0.36, fitSize(eventText, fs, eventW, 0.7), fit(eventText, fs, eventW + 1, bw), "rk-ink", "start", 600));
|
|
212
|
+
if (winnerEntry) {
|
|
213
|
+
g.push(`<circle class="${m.winner === 0 ? "rk-sa" : "rk-sb"}" cx="${num(wx + fs * 0.4)}" cy="${num(cy)}" r="${num(fs * 0.3)}"/>`);
|
|
214
|
+
const nx = wx + fs * 1.1;
|
|
215
|
+
g.push(label(nx, cy + fs * 0.36, fs, fit(winName, fs, winW - fs * 1.1 + 1, bw), m.winner === 0 ? "rk-sa" : "rk-sb", "start", 800));
|
|
216
|
+
}
|
|
217
|
+
else
|
|
218
|
+
g.push(label(wx + fs * 0.4, cy + fs * 0.36, fs, "–", "rk-sub", "start", 800));
|
|
219
|
+
g.push(resultSvg);
|
|
220
|
+
return `<g class="rk-meeting">${g.join("")}</g>`;
|
|
221
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -420,6 +420,24 @@ export interface Words {
|
|
|
420
420
|
rankEntryTitle: string;
|
|
421
421
|
rankColRanking: string;
|
|
422
422
|
rankUnranked: string;
|
|
423
|
+
/** The head-to-head card (`rankingMatchup`): the heading, the heading over the previous meetings, no meetings yet, and `{n}`: "+3 more". */
|
|
424
|
+
rankMatchTitle: string;
|
|
425
|
+
rankMeetings: string;
|
|
426
|
+
rankNoMeetings: string;
|
|
427
|
+
rankMore: string;
|
|
428
|
+
/** The medal table (`medalTable`): the heading, no medals yet, the three medals and their one-letter marks, the total, and a row in a sentence (`{g}`, `{s}`, `{b}`, `{t}`). */
|
|
429
|
+
medalTitle: string;
|
|
430
|
+
medalNone: string;
|
|
431
|
+
medalGold: string;
|
|
432
|
+
medalSilver: string;
|
|
433
|
+
medalBronze: string;
|
|
434
|
+
medalGoldShort: string;
|
|
435
|
+
medalSilverShort: string;
|
|
436
|
+
medalBronzeShort: string;
|
|
437
|
+
medalTotal: string;
|
|
438
|
+
medalLine: string;
|
|
439
|
+
/** A rank in the row sentence (`{n}`): "3", and "3位" in Japanese. */
|
|
440
|
+
medalRank: string;
|
|
423
441
|
}
|
|
424
442
|
/** A language: its words, the names of the rounds (by their English names), and the tag `Intl` formats numbers and dates for. */
|
|
425
443
|
export interface Locale {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.14.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.14.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/taisen-brackets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Taisen Brackets 対戦, knockout brackets as SVG: from a list of ties (games, best-of series, two legs, replays, byes, a third-place match) it works out the rounds and halves and draws the bracket with the halves meeting at the final, or left to right, wide, square, as a tall card or for a phone, in a classic or a road look (large round flags, big scores, a team's dotted path, a link on every box), in light, dark or a gold-on-navy poster look, in eight layouts (the tree, rows, shields, a score-update card, tall columns, the NBA's play-in, a series card and group tables), on one of twenty-five poster backgrounds (the playing surfaces of five sports among them) or your own, with each team's logo or round flag, the champion in the centre, and an accessible table; and the road to the final of either finalist. NHL, NBA, MLB, NFL, CFL, MLS and World Cup formats by era, in English, French, Japanese, Spanish and German. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bracket",
|