@johnmorrisdotca/taisen-brackets 0.11.0 → 0.12.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 +11 -0
- package/README.md +3 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/locales.js +5 -0
- package/dist/rank-kit.js +2 -0
- package/dist/ranking-cards.d.ts +71 -0
- package/dist/ranking-cards.js +327 -0
- package/dist/ranking.d.ts +12 -0
- package/dist/ranking.js +4 -4
- package/dist/types.d.ts +6 -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,17 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.0] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
Rankings, step 2: the number ones and the entry list.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`rankingNumberOnes({ title?, date?, source?, cards }, options)`**: the number one of each category (each weight class, each age group) as a card in a grid: a coloured panel with the 1, the flag, the name, the club or country, the points and a pill that reads `#1 in -57 kg`. No photo. `columns` (1 to 4; by default the number that lets the longest name be set biggest), `colors` (hex codes taken in turn, white or near-black ink on each; `NUMBER_ONE_COLORS`), `precision`, `nameStyle`. `describeNumberOnes`.
|
|
16
|
+
- **`rankingEntryList({ title?, category?, date?, source?, entries }, options)`**: an event's entry list in the order of the draw: a numbered row for each athlete with the flag, the name, the club, the country's letters and the athlete's ranking at the right (`NR` for none), `points` optional. `sort: "ranking"`, `numbers`, `columns` (two on a wide board by default), `rows` and `page` (the header says which entries are shown), `badge`. `describeEntryList`.
|
|
17
|
+
- Both take the options every board takes (size, theme, flags, locale, words, `--rk-*` custom properties). Words in the five languages (the French and Japanese were read by native-language reviewers; the Spanish and German were not).
|
|
18
|
+
- The demo's Rankings section has both, with the view in the address (`kview`). [docs/rankings.md](docs/rankings.md).
|
|
19
|
+
|
|
9
20
|
## [0.11.0] - 2026-10-20
|
|
10
21
|
|
|
11
22
|
Rankings, step 1: the ranking list.
|
package/README.md
CHANGED
|
@@ -249,7 +249,7 @@ 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`). `rankingRanks` and `rankingCategoryLabel` are
|
|
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)
|
|
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">
|
|
@@ -304,7 +304,7 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
|
|
|
304
304
|
- **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
305
|
- **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
306
|
- **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
|
-
- **
|
|
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)).
|
|
308
308
|
- **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
309
|
- **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
|
|
310
310
|
- **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 +666,7 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
|
|
|
666
666
|
- `matchupsFromGames(games, options)`: matchups from a flat list of games.
|
|
667
667
|
- `resolveMatchup`, `collectTeams`, `describeTie`, `noteParts`, `noteLong`: one tie's result, the teams of a bracket, a tie in a sentence.
|
|
668
668
|
- `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 ([docs/rankings.md](./docs/rankings.md)).
|
|
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)).
|
|
670
670
|
- `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
671
|
- `isEmblemId(value)`, and the types `EmblemRenderer`, `EmblemRequest`, `EmblemStyle`: the trophy emblems (see "Emblems for the trophy").
|
|
672
672
|
- `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
|
package/dist/index.d.ts
CHANGED
|
@@ -29,3 +29,5 @@ export { rankingList, rankingRanks, rankingCategoryLabel, describeRanking } from
|
|
|
29
29
|
export type { RankingEntry, RankingName, RankingOptions, RankingSource } from "./ranking.ts";
|
|
30
30
|
export { BOARD_SIZES } from "./rank-kit.ts";
|
|
31
31
|
export type { BoardOptions, BoardSizeName } from "./rank-kit.ts";
|
|
32
|
+
export { rankingNumberOnes, describeNumberOnes, rankingEntryList, describeEntryList, NUMBER_ONE_COLORS } from "./ranking-cards.ts";
|
|
33
|
+
export type { EntryListOptions, EntryListSource, NumberOnesOptions, NumberOnesSource, RankingCard } from "./ranking-cards.ts";
|
package/dist/index.js
CHANGED
|
@@ -16,3 +16,4 @@ export { judoContestCard, describeJudoCard, judoMatQueue, describeJudoQueue, JUD
|
|
|
16
16
|
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
|
+
export { rankingNumberOnes, describeNumberOnes, rankingEntryList, describeEntryList, NUMBER_ONE_COLORS } from "./ranking-cards.js";
|
package/dist/locales.js
CHANGED
|
@@ -30,6 +30,7 @@ const EN = {
|
|
|
30
30
|
judoCardTitle: "Judo contest card", judoQueueTitle: "Judo mat queue",
|
|
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
|
+
rankNumberOnes: "Number ones", rankNumberOneIn: "#1 in {category}", rankEntryTitle: "Entry list", rankColRanking: "Ranking", rankUnranked: "NR",
|
|
33
34
|
};
|
|
34
35
|
const FR = {
|
|
35
36
|
tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
|
|
@@ -56,6 +57,7 @@ const FR = {
|
|
|
56
57
|
judoCardTitle: "Présentation d’un combat de judo", judoQueueTitle: "Prochains combats par tapis",
|
|
57
58
|
rankTitle: "Classement", rankPoints: "points", rankPointsShort: "pts", rankChampion: "C", rankChampionLong: "Champion", rankInterim: "CI", rankInterimLong: "Champion intérimaire",
|
|
58
59
|
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
|
+
rankNumberOnes: "Les numéros un", rankNumberOneIn: "N°\u00a01 en {category}", rankEntryTitle: "Liste des engagés", rankColRanking: "Classement", rankUnranked: "NC",
|
|
59
61
|
};
|
|
60
62
|
const JA = {
|
|
61
63
|
tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
|
|
@@ -82,6 +84,7 @@ const JA = {
|
|
|
82
84
|
judoCardTitle: "柔道の対戦カード", judoQueueTitle: "各試合場の次の試合",
|
|
83
85
|
rankTitle: "ランキング", rankPoints: "ポイント", rankPointsShort: "pt", rankChampion: "王者", rankChampionLong: "王者", rankInterim: "暫定", rankInterimLong: "暫定王者",
|
|
84
86
|
rankNew: "NEW", rankNewLong: "新規ランクイン", rankUp: "{n}ランク上昇", rankDown: "{n}ランク下降", rankSame: "変動なし", rankRange: "{n}人中{a}–{b}位", rankNone: "ランキング入りした選手はまだいません", rankJoin: "、",
|
|
87
|
+
rankNumberOnes: "各階級の1位", rankNumberOneIn: "{category} 1位", rankEntryTitle: "エントリーリスト", rankColRanking: "ランキング", rankUnranked: "圏外",
|
|
85
88
|
};
|
|
86
89
|
const ES = {
|
|
87
90
|
tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
|
|
@@ -108,6 +111,7 @@ const ES = {
|
|
|
108
111
|
judoCardTitle: "Ficha de un combate de judo", judoQueueTitle: "Próximos combates por tatami",
|
|
109
112
|
rankTitle: "Clasificación", rankPoints: "puntos", rankPointsShort: "pts", rankChampion: "C", rankChampionLong: "Campeón", rankInterim: "CI", rankInterimLong: "Campeón interino",
|
|
110
113
|
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
|
+
rankNumberOnes: "Los números uno", rankNumberOneIn: "N.º 1 en {category}", rankEntryTitle: "Lista de inscritos", rankColRanking: "Clasificación", rankUnranked: "SC",
|
|
111
115
|
};
|
|
112
116
|
const DE = {
|
|
113
117
|
tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
|
|
@@ -134,6 +138,7 @@ const DE = {
|
|
|
134
138
|
judoCardTitle: "Kampfkarte eines Judokampfes", judoQueueTitle: "Nächste Kämpfe je Matte",
|
|
135
139
|
rankTitle: "Rangliste", rankPoints: "Punkte", rankPointsShort: "Pkt.", rankChampion: "C", rankChampionLong: "Champion", rankInterim: "IC", rankInterimLong: "Interimschampion",
|
|
136
140
|
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
|
+
rankNumberOnes: "Die Nummer Eins", rankNumberOneIn: "Nr. 1 in {category}", rankEntryTitle: "Meldeliste", rankColRanking: "Rangliste", rankUnranked: "o. R.",
|
|
137
142
|
};
|
|
138
143
|
/**
|
|
139
144
|
* The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
|
package/dist/rank-kit.js
CHANGED
|
@@ -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-onaccent", "fill:#ffffff"),
|
|
74
|
+
sel(".rk-onaccent-dark", "fill:#15181a"),
|
|
73
75
|
sel(".tb-ring", `fill:none;stroke:rgba(0,0,0,.28);stroke-width:1.5`),
|
|
74
76
|
sel(".tb-plain", `fill:${v("pill", p.pill)}`),
|
|
75
77
|
sel(".tb-initials", `fill:${v("pill-ink", p.pillInk)}`),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two more drawings from a ranking's data. `rankingNumberOnes`: the number one of each category (each weight class, each age group) as a card, in a grid: name, club or country, flag,
|
|
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
|
+
* Static SVG for a given state, like every drawing here.
|
|
5
|
+
*/
|
|
6
|
+
import { type BoardOptions } from "./rank-kit.ts";
|
|
7
|
+
import { type RankingEntry, type RankingOptions } from "./ranking.ts";
|
|
8
|
+
type Shared = Pick<RankingOptions, "nameStyle" | "precision">;
|
|
9
|
+
/** A category and its number one. */
|
|
10
|
+
export interface RankingCard {
|
|
11
|
+
/** The category as it is written ("-57 kg", "U18 · Men -60 kg"; `rankingCategoryLabel` writes one). */
|
|
12
|
+
category: string;
|
|
13
|
+
entry: RankingEntry;
|
|
14
|
+
}
|
|
15
|
+
export interface NumberOnesSource {
|
|
16
|
+
/** The heading. Default: the language's "Number ones". */
|
|
17
|
+
title?: string;
|
|
18
|
+
/** The date or period, at the right of the heading. */
|
|
19
|
+
date?: string;
|
|
20
|
+
/** Where the list comes from, small at the foot. */
|
|
21
|
+
source?: string;
|
|
22
|
+
cards: readonly RankingCard[];
|
|
23
|
+
}
|
|
24
|
+
export interface NumberOnesOptions extends BoardOptions, Shared {
|
|
25
|
+
/** Columns of cards, 1 to 4. Default: the number that makes the cards biggest. */
|
|
26
|
+
columns?: number;
|
|
27
|
+
/** The colour of each card's number panel, hex codes, taken in turn. The number is white or near-black, whichever reads on the colour. Default `NUMBER_ONE_COLORS`. */
|
|
28
|
+
colors?: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/** Orange, crimson, blue, teal, purple and amber, each dark enough for white type (4.5 to 1 or better). */
|
|
31
|
+
export declare const NUMBER_ONE_COLORS: readonly string[];
|
|
32
|
+
/** The cards as a sentence, the drawing's description. */
|
|
33
|
+
export declare function describeNumberOnes(source: NumberOnesSource, options?: Pick<NumberOnesOptions, "locale" | "words" | "nameStyle" | "precision">): string;
|
|
34
|
+
/**
|
|
35
|
+
* The number ones as cards in a grid: a coloured panel with the 1, the flag, the name and the club or country, the points and a pill that says "#1 in -57 kg". Throws a `RangeError` for
|
|
36
|
+
* no cards, a card with no category, an entry with no name, a colour that is not a hex code, columns that are not 1 to 4, or a bad size or theme.
|
|
37
|
+
*/
|
|
38
|
+
export declare function rankingNumberOnes(source: NumberOnesSource, options?: NumberOnesOptions): string;
|
|
39
|
+
export interface EntryListSource {
|
|
40
|
+
/** The heading. Default: the language's "Entry list". */
|
|
41
|
+
title?: string;
|
|
42
|
+
/** The category as a pill under the heading. */
|
|
43
|
+
category?: string;
|
|
44
|
+
/** The event or the date, at the right of the heading. */
|
|
45
|
+
date?: string;
|
|
46
|
+
/** Where the list comes from, small at the foot. */
|
|
47
|
+
source?: string;
|
|
48
|
+
/** The athletes in the order of the draw. An entry's `rank` is its ranking (none: not ranked, written `NR`); `points` are optional. */
|
|
49
|
+
entries: readonly RankingEntry[];
|
|
50
|
+
}
|
|
51
|
+
export interface EntryListOptions extends BoardOptions, Shared {
|
|
52
|
+
/** `entry` (the default) keeps the order given; `ranking` puts the best ranked first and the unranked last. */
|
|
53
|
+
sort?: "entry" | "ranking";
|
|
54
|
+
/** Number the rows 1, 2, 3 in the order drawn. Default true. */
|
|
55
|
+
numbers?: boolean;
|
|
56
|
+
/** Columns of rows, 1 to 3. Default: as many as the size needs to keep the rows readable. */
|
|
57
|
+
columns?: number;
|
|
58
|
+
/** The most entries drawn, from the first of the page, and which page (0 is the first). Default: as many as fit at a readable size. */
|
|
59
|
+
rows?: number;
|
|
60
|
+
page?: number;
|
|
61
|
+
/** Draw a club's (or the athlete's) initials on a badge for an entry with no flag or mark, even where none has one. */
|
|
62
|
+
badge?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** The entry list as a sentence, the drawing's description. */
|
|
65
|
+
export declare function describeEntryList(source: EntryListSource, options?: Pick<EntryListOptions, "locale" | "words" | "nameStyle" | "sort" | "precision">): string;
|
|
66
|
+
/**
|
|
67
|
+
* An event's entry list: a numbered row for each athlete in the order of the draw, with the flag, the name and club, the country's letters and the athlete's ranking (`NR` for none) at the right. Throws a
|
|
68
|
+
* `RangeError` for an entry with no name, a ranking that is not a whole number from 1, an unknown option value or a bad size or theme.
|
|
69
|
+
*/
|
|
70
|
+
export declare function rankingEntryList(source: EntryListSource, options?: EntryListOptions): string;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two more drawings from a ranking's data. `rankingNumberOnes`: the number one of each category (each weight class, each age group) as a card, in a grid: name, club or country, flag,
|
|
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
|
+
* Static SVG for a given state, like every drawing here.
|
|
5
|
+
*/
|
|
6
|
+
import { boardFrame, boardWrap, bw, clampN, fitSize, label, pill } from "./rank-kit.js";
|
|
7
|
+
import { inkOn } from "./judo-sheets.js";
|
|
8
|
+
import { fill, voiceOf } from "./locales.js";
|
|
9
|
+
import { drawMark } from "./marks.js";
|
|
10
|
+
import { abbreviate, esc, fit, num } from "./text.js";
|
|
11
|
+
import { nameText, pointsText, teamFor } from "./ranking.js";
|
|
12
|
+
function checkEntry(e, where) {
|
|
13
|
+
const n = e?.name;
|
|
14
|
+
const ok = typeof n === "string" ? n.trim() !== "" : !!n && typeof n === "object" && typeof n.surname === "string" && n.surname.trim() !== "";
|
|
15
|
+
if (!ok)
|
|
16
|
+
throw new RangeError(`taisen-brackets: ${where} needs a name`);
|
|
17
|
+
if (e.rank !== undefined && !(Number.isInteger(e.rank) && e.rank >= 1))
|
|
18
|
+
throw new RangeError(`taisen-brackets: the ranking of ${nameOf(e)} is a whole number from 1`);
|
|
19
|
+
}
|
|
20
|
+
const nameOf = (e) => (typeof e.name === "string" ? e.name : e.name.surname);
|
|
21
|
+
/** Orange, crimson, blue, teal, purple and amber, each dark enough for white type (4.5 to 1 or better). */
|
|
22
|
+
export const NUMBER_ONE_COLORS = Object.freeze(["#c2410c", "#be123c", "#1d4ed8", "#0f766e", "#6d28d9", "#a16207"]);
|
|
23
|
+
const HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
24
|
+
/** The cards as a sentence, the drawing's description. */
|
|
25
|
+
export function describeNumberOnes(source, options = {}) {
|
|
26
|
+
const v = voiceOf(options);
|
|
27
|
+
const w = v.words;
|
|
28
|
+
const parts = [source.title ?? w.rankNumberOnes];
|
|
29
|
+
if (source.date)
|
|
30
|
+
parts.push(source.date);
|
|
31
|
+
for (const c of source.cards) {
|
|
32
|
+
const bits = [fill(w.rankNumberOneIn, { category: c.category }), nameText(c.entry.name, options.nameStyle ?? "surname-first", v)];
|
|
33
|
+
const detail = c.entry.club ?? c.entry.country;
|
|
34
|
+
if (detail)
|
|
35
|
+
bits.push(detail);
|
|
36
|
+
const p = pointsText(c.entry.points, options.precision, v);
|
|
37
|
+
if (p)
|
|
38
|
+
bits.push(`${p} ${w.rankPoints}`);
|
|
39
|
+
parts.push(bits.join(w.rankJoin));
|
|
40
|
+
}
|
|
41
|
+
return parts.join(w.sep) + w.end;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The number ones as cards in a grid: a coloured panel with the 1, the flag, the name and the club or country, the points and a pill that says "#1 in -57 kg". Throws a `RangeError` for
|
|
45
|
+
* no cards, a card with no category, an entry with no name, a colour that is not a hex code, columns that are not 1 to 4, or a bad size or theme.
|
|
46
|
+
*/
|
|
47
|
+
export function rankingNumberOnes(source, options = {}) {
|
|
48
|
+
if (!source || !Array.isArray(source.cards) || source.cards.length === 0)
|
|
49
|
+
throw new RangeError("taisen-brackets: the number ones need at least one card");
|
|
50
|
+
source.cards.forEach((c, i) => {
|
|
51
|
+
if (typeof c?.category !== "string" || c.category.trim() === "")
|
|
52
|
+
throw new RangeError(`taisen-brackets: card ${i + 1} needs a category`);
|
|
53
|
+
checkEntry(c.entry, `the entry of card ${i + 1}`);
|
|
54
|
+
});
|
|
55
|
+
if (options.columns !== undefined && !(Number.isInteger(options.columns) && options.columns >= 1 && options.columns <= 4))
|
|
56
|
+
throw new RangeError("taisen-brackets: columns is a whole number from 1 to 4");
|
|
57
|
+
const colors = options.colors ?? NUMBER_ONE_COLORS;
|
|
58
|
+
if (colors.length === 0 || colors.some((c) => !HEX.test(c)))
|
|
59
|
+
throw new RangeError("taisen-brackets: colors are hex codes such as #c2410c");
|
|
60
|
+
const f = boardFrame(options, [source, options.columns, options.nameStyle, options.precision, colors]);
|
|
61
|
+
const { W, H, v } = f;
|
|
62
|
+
const w = v.words;
|
|
63
|
+
const unit = Math.min(W, H);
|
|
64
|
+
const pad = Math.round(unit * 0.035);
|
|
65
|
+
const body = [];
|
|
66
|
+
const header = heading(f, source.title ?? w.rankNumberOnes, undefined, source.date, pad, Math.min(W - pad * 2, unit * 1.45), source.source);
|
|
67
|
+
body.push(...header.svg);
|
|
68
|
+
const boxW = header.boxW, bx = header.bx;
|
|
69
|
+
const n = source.cards.length;
|
|
70
|
+
const gap = Math.round(unit * 0.02);
|
|
71
|
+
const avail = header.bottom - header.top;
|
|
72
|
+
const nameStyle = options.nameStyle ?? "surname-first";
|
|
73
|
+
const texts = source.cards.map((c) => ({ name: nameText(c.entry.name, nameStyle, v), points: pointsText(c.entry.points, options.precision, v), pill: fill(w.rankNumberOneIn, { category: c.category }) }));
|
|
74
|
+
// How many columns: the one that lets the longest name be set biggest (cards must be at least 3.4 times as wide as high, so the type has room).
|
|
75
|
+
let best = { c: 1, ch: 0, cw: 0, size: -1 };
|
|
76
|
+
for (let c = options.columns ?? 1; c <= (options.columns ?? Math.min(4, n)); c++) {
|
|
77
|
+
const rows = Math.ceil(n / c);
|
|
78
|
+
const cw = (boxW - (c - 1) * gap) / c;
|
|
79
|
+
const ch = Math.min((avail - (rows - 1) * gap) / rows, cw / 3.4, unit * 0.24);
|
|
80
|
+
const size = cardType(cw, ch, texts).name;
|
|
81
|
+
if (size > best.size + 0.5 || (Math.abs(size - best.size) <= 0.5 && ch > best.ch))
|
|
82
|
+
best = { c, ch, cw, size };
|
|
83
|
+
}
|
|
84
|
+
const { c: cols, ch, cw } = best;
|
|
85
|
+
const top = header.top;
|
|
86
|
+
const m = cardType(cw, ch, texts);
|
|
87
|
+
source.cards.forEach((card, i) => {
|
|
88
|
+
const col = i % cols, row = Math.floor(i / cols);
|
|
89
|
+
const r = { x: bx + col * (cw + gap), y: top + row * (ch + gap), w: cw, h: ch };
|
|
90
|
+
body.push(drawCard(f, r, card, colors[i % colors.length], nameStyle, options.precision, i, m));
|
|
91
|
+
});
|
|
92
|
+
return boardWrap(f, options.title ?? (source.title ?? w.rankNumberOnes), describeNumberOnes(source, options), body, "number-ones");
|
|
93
|
+
}
|
|
94
|
+
function cardType(cw, ch, texts) {
|
|
95
|
+
const he = Math.min(ch, cw / 3.4);
|
|
96
|
+
const pad = he * 0.14;
|
|
97
|
+
const pw = he * 0.62;
|
|
98
|
+
const d = he * 0.38;
|
|
99
|
+
const ps = he * 0.22;
|
|
100
|
+
const pillH = he * 0.15;
|
|
101
|
+
const rightW = Math.max(...texts.map((t) => Math.max(bw(t.points, ps), bw(t.pill, pillH * 0.56) + pillH * 0.7)));
|
|
102
|
+
const nameAvail = cw - pw - pad * 2.4 - d - pad - rightW - pad;
|
|
103
|
+
const longest = Math.max(...texts.map((t) => bw(t.name, 1)));
|
|
104
|
+
const name = Math.max(1, Math.min(he * 0.24, nameAvail / longest));
|
|
105
|
+
return { he, pw, pad, d, ps, pillH, rightW, nameAvail, name, sub: Math.min(he * 0.14, name * 0.62) };
|
|
106
|
+
}
|
|
107
|
+
function drawCard(f, r, card, color, nameStyle, precision, i, m) {
|
|
108
|
+
const { v } = f;
|
|
109
|
+
const w = v.words;
|
|
110
|
+
const g = [];
|
|
111
|
+
const e = card.entry;
|
|
112
|
+
const name = nameText(e.name, nameStyle, v);
|
|
113
|
+
const second = e.club ?? e.country ?? "";
|
|
114
|
+
const points = pointsText(e.points, precision, v);
|
|
115
|
+
const rx = Math.min(r.h * 0.12, 22);
|
|
116
|
+
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)}"/>`);
|
|
117
|
+
// The number panel: rounded on the left, square on the right.
|
|
118
|
+
const ink = inkOn(color) === "#ffffff" ? "rk-onaccent" : "rk-onaccent-dark";
|
|
119
|
+
g.push(`<rect x="${num(r.x)}" y="${num(r.y)}" width="${num(m.pw)}" height="${num(r.h)}" rx="${num(rx)}" style="fill:${esc(color)}"/>`);
|
|
120
|
+
g.push(`<rect x="${num(r.x + m.pw - rx)}" y="${num(r.y)}" width="${num(rx)}" height="${num(r.h)}" style="fill:${esc(color)}"/>`);
|
|
121
|
+
const ns1 = m.he * 0.5;
|
|
122
|
+
g.push(label(r.x + m.pw / 2, r.y + r.h / 2 + ns1 * 0.36, ns1, v.number(1), ink, "middle", 800));
|
|
123
|
+
let x = r.x + m.pw + m.pad * 1.2;
|
|
124
|
+
const right = r.x + r.w - m.pad * 1.2;
|
|
125
|
+
// Points and the pill, at the right.
|
|
126
|
+
const pillText = fill(w.rankNumberOneIn, { category: card.category });
|
|
127
|
+
const stackH = (points ? m.ps * 1.1 : 0) + m.pillH;
|
|
128
|
+
let y = r.y + (r.h - stackH) / 2;
|
|
129
|
+
if (points) {
|
|
130
|
+
g.push(label(right - m.rightW / 2, y + m.ps * 0.82, m.ps, points, "rk-ink", "middle", 800));
|
|
131
|
+
y += m.ps * 1.1;
|
|
132
|
+
}
|
|
133
|
+
g.push(pill(right, y + m.pillH / 2, m.pillH, pillText, m.rightW, "rk-pill", "rk-pillink", "end").svg);
|
|
134
|
+
// The flag and the name.
|
|
135
|
+
if (e.flag || e.mark) {
|
|
136
|
+
g.push(drawMark(teamFor(e, `c${i}`, name, abbreviate(e.club ?? name)), x + m.d / 2, r.y + r.h / 2, m.d, f.ctx));
|
|
137
|
+
x += m.d + m.pad;
|
|
138
|
+
}
|
|
139
|
+
const avail = right - m.rightW - m.pad - x;
|
|
140
|
+
const ns = Math.min(m.name, fitSize(name, m.name, avail, 0.9));
|
|
141
|
+
if (second) {
|
|
142
|
+
const ss = Math.min(m.sub, fitSize(second, m.sub, avail, 0.9));
|
|
143
|
+
const block = ns * 1.05 + ss * 1.4;
|
|
144
|
+
const top = r.y + r.h / 2 - block / 2;
|
|
145
|
+
g.push(label(x, top + ns * 0.86, ns, fit(name, ns, avail + 1, bw), "rk-ink", "start", 800));
|
|
146
|
+
g.push(label(x, top + ns * 1.05 + ss * 1.1, ss, fit(second, ss, avail + 1, bw), "rk-sub", "start", 600));
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
g.push(label(x, r.y + r.h / 2 + ns * 0.36, ns, fit(name, ns, avail + 1, bw), "rk-ink", "start", 800));
|
|
150
|
+
return `<g class="rk-card">${g.join("")}</g>`;
|
|
151
|
+
}
|
|
152
|
+
/** The entry list as a sentence, the drawing's description. */
|
|
153
|
+
export function describeEntryList(source, options = {}) {
|
|
154
|
+
const v = voiceOf(options);
|
|
155
|
+
const w = v.words;
|
|
156
|
+
const parts = [source.title ?? w.rankEntryTitle];
|
|
157
|
+
if (source.category)
|
|
158
|
+
parts.push(source.category);
|
|
159
|
+
if (source.date)
|
|
160
|
+
parts.push(source.date);
|
|
161
|
+
const list = entryOrder(source, options.sort);
|
|
162
|
+
list.forEach((e, i) => {
|
|
163
|
+
const bits = [v.number(i + 1), nameText(e.name, options.nameStyle ?? "surname-first", v)];
|
|
164
|
+
const detail = e.club ?? e.country;
|
|
165
|
+
if (detail)
|
|
166
|
+
bits.push(detail);
|
|
167
|
+
bits.push(e.rank === undefined ? w.rankUnranked : `${w.rankColRanking} ${v.number(e.rank)}`);
|
|
168
|
+
parts.push(bits.join(w.rankJoin));
|
|
169
|
+
});
|
|
170
|
+
return parts.join(w.sep) + w.end;
|
|
171
|
+
}
|
|
172
|
+
const entryOrder = (source, sort) => sort === "ranking" ? source.entries.map((e, i) => ({ e, i })).sort((a, b) => (a.e.rank ?? Infinity) - (b.e.rank ?? Infinity) || a.i - b.i).map((x) => x.e) : [...source.entries];
|
|
173
|
+
/**
|
|
174
|
+
* An event's entry list: a numbered row for each athlete in the order of the draw, with the flag, the name and club, the country's letters and the athlete's ranking (`NR` for none) at the right. Throws a
|
|
175
|
+
* `RangeError` for an entry with no name, a ranking that is not a whole number from 1, an unknown option value or a bad size or theme.
|
|
176
|
+
*/
|
|
177
|
+
export function rankingEntryList(source, options = {}) {
|
|
178
|
+
if (!source || !Array.isArray(source.entries))
|
|
179
|
+
throw new RangeError("taisen-brackets: an entry list needs a list of entries");
|
|
180
|
+
source.entries.forEach((e, i) => checkEntry(e, `entry ${i + 1} of the entry list`));
|
|
181
|
+
if (options.sort !== undefined && options.sort !== "entry" && options.sort !== "ranking")
|
|
182
|
+
throw new RangeError(`taisen-brackets: sort is "entry" or "ranking", not ${JSON.stringify(options.sort)}`);
|
|
183
|
+
if (options.columns !== undefined && !(Number.isInteger(options.columns) && options.columns >= 1 && options.columns <= 3))
|
|
184
|
+
throw new RangeError("taisen-brackets: columns is a whole number from 1 to 3");
|
|
185
|
+
if (options.rows !== undefined && !(Number.isInteger(options.rows) && options.rows >= 1 && options.rows <= 200))
|
|
186
|
+
throw new RangeError("taisen-brackets: rows is a whole number from 1 to 200");
|
|
187
|
+
if (options.page !== undefined && !(Number.isInteger(options.page) && options.page >= 0))
|
|
188
|
+
throw new RangeError("taisen-brackets: page is a whole number from 0");
|
|
189
|
+
const f = boardFrame(options, [source, options.sort, options.numbers, options.columns, options.rows, options.page, options.nameStyle, options.precision, options.badge]);
|
|
190
|
+
const { W, H, v } = f;
|
|
191
|
+
const w = v.words;
|
|
192
|
+
const unit = Math.min(W, H);
|
|
193
|
+
const pad = Math.round(unit * 0.035);
|
|
194
|
+
const nameStyle = options.nameStyle ?? "surname-first";
|
|
195
|
+
const all = entryOrder(source, options.sort);
|
|
196
|
+
const body = [];
|
|
197
|
+
const title = source.title ?? w.rankEntryTitle;
|
|
198
|
+
const header = heading(f, title, source.category, source.date, pad, W - pad * 2, source.source);
|
|
199
|
+
body.push(...header.svg);
|
|
200
|
+
const { bx, boxW } = header;
|
|
201
|
+
const avail = header.bottom - header.top - unit * 0.04;
|
|
202
|
+
const minRow = Math.max(28, unit * 0.034);
|
|
203
|
+
const gap = 0.12;
|
|
204
|
+
const colGap = pad;
|
|
205
|
+
// Columns: the fewest that fit every entry at a readable row height, within what the width allows.
|
|
206
|
+
const maxCols = Math.max(1, Math.min(3, Math.floor(boxW / (unit * 0.55))));
|
|
207
|
+
let cols = options.columns ?? 1;
|
|
208
|
+
// A wide board takes two columns so that a row is not a long way from its number to its ranking; a short list stays in one.
|
|
209
|
+
if (options.columns === undefined) {
|
|
210
|
+
cols = Math.min(maxCols, Math.ceil(boxW / (unit * 1.25)), Math.max(1, Math.ceil(all.length / 6)));
|
|
211
|
+
while (cols < maxCols && Math.ceil(all.length / cols) * minRow * (1 + gap) > avail)
|
|
212
|
+
cols += 1;
|
|
213
|
+
}
|
|
214
|
+
const perCol = Math.max(1, Math.floor(avail / (minRow * (1 + gap))));
|
|
215
|
+
const page = options.page ?? 0;
|
|
216
|
+
const per = options.rows ?? perCol * cols;
|
|
217
|
+
const from = Math.min(page * per, Math.max(0, all.length - 1));
|
|
218
|
+
const slice = all.slice(from, from + per);
|
|
219
|
+
const rpc = Math.ceil(slice.length / cols);
|
|
220
|
+
const h = Math.min(unit * 0.085, avail / (Math.max(1, rpc) * (1 + gap)));
|
|
221
|
+
const colW = (boxW - (cols - 1) * colGap) / cols;
|
|
222
|
+
const marksAny = options.badge === true || slice.some((e) => e.flag || e.mark);
|
|
223
|
+
const numbers = options.numbers ?? true;
|
|
224
|
+
const he = Math.min(h, colW / 8);
|
|
225
|
+
const cs = he * 0.3;
|
|
226
|
+
const hasCode = slice.some((e) => e.code);
|
|
227
|
+
const hasPoints = slice.some((e) => e.points !== undefined);
|
|
228
|
+
const rs = he * 0.46;
|
|
229
|
+
const rankTexts = slice.map((e) => (e.rank === undefined ? w.rankUnranked : v.number(e.rank)));
|
|
230
|
+
const rankW = Math.max(bw(w.rankColRanking, cs * 0.85), ...rankTexts.map((t) => bw(t, rs))) + he * 0.2;
|
|
231
|
+
const ptsS = he * 0.3;
|
|
232
|
+
const ptsTexts = slice.map((e) => pointsText(e.points, options.precision, v));
|
|
233
|
+
const ptsW = hasPoints ? Math.max(...ptsTexts.map((t) => bw(t, ptsS))) + he * 0.3 : 0;
|
|
234
|
+
const codeW = hasCode ? Math.max(...slice.map((e) => bw(e.code ?? "", cs))) + he * 0.2 : 0;
|
|
235
|
+
const numW = numbers ? Math.max(he * 0.7, bw(v.number(from + slice.length), he * 0.36)) : 0;
|
|
236
|
+
const d = he * 0.58;
|
|
237
|
+
const rowPad = he * 0.2;
|
|
238
|
+
// The column heading over the ranking.
|
|
239
|
+
const hs = clampN(cs * 0.85, 10, 40);
|
|
240
|
+
for (let c = 0; c < cols; c++) {
|
|
241
|
+
const x1 = bx + c * (colW + colGap) + colW - rowPad;
|
|
242
|
+
body.push(label(x1 - (rankW - he * 0.2) / 2, header.top + hs * 0.8, hs, fit(v.upper(w.rankColRanking), hs, rankW + he * 0.4, bw), "rk-sub", "middle", 700, ' letter-spacing=".08em"'));
|
|
243
|
+
}
|
|
244
|
+
const top = header.top + hs * 1.6;
|
|
245
|
+
const rowH = Math.min(h, (avail - hs * 1.6) / (Math.max(1, rpc) * (1 + gap)));
|
|
246
|
+
slice.forEach((e, i) => {
|
|
247
|
+
const col = Math.floor(i / rpc), idx = i % rpc;
|
|
248
|
+
const r = { x: bx + col * (colW + colGap), y: top + idx * rowH * (1 + gap), w: colW, h: rowH };
|
|
249
|
+
const g = [];
|
|
250
|
+
const name = nameText(e.name, nameStyle, v);
|
|
251
|
+
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))}"/>`);
|
|
252
|
+
const cy = r.y + r.h / 2;
|
|
253
|
+
let x = r.x + rowPad;
|
|
254
|
+
const right = r.x + r.w - rowPad;
|
|
255
|
+
if (numbers) {
|
|
256
|
+
g.push(label(x + numW / 2, cy + he * 0.36 * 0.36, he * 0.36, v.number(from + i + 1), "rk-sub", "middle", 700));
|
|
257
|
+
x += numW;
|
|
258
|
+
}
|
|
259
|
+
if (marksAny) {
|
|
260
|
+
g.push(drawMark(teamFor(e, `e${from + i}`, name, abbreviate(e.club ?? name).slice(0, 3)), x + d / 2, cy, d, f.ctx));
|
|
261
|
+
x += d + rowPad;
|
|
262
|
+
}
|
|
263
|
+
// The ranking, big, at the right; the code and the points before it. They are written after the name, so that the markup reads in the order the row does.
|
|
264
|
+
const rt = rankTexts[i];
|
|
265
|
+
const tail = { rank: label(right - (rankW - he * 0.2) / 2, cy + rs * 0.36, fitSize(rt, rs, rankW - he * 0.2, 0.6), rt, e.rank === undefined ? "rk-sub" : "rk-ink", "middle", 800) };
|
|
266
|
+
let nr = right - rankW;
|
|
267
|
+
if (hasPoints) {
|
|
268
|
+
if (ptsTexts[i])
|
|
269
|
+
tail.points = label(nr, cy + ptsS * 0.36, ptsS, ptsTexts[i], "rk-sub", "end", 700);
|
|
270
|
+
nr -= ptsW;
|
|
271
|
+
}
|
|
272
|
+
if (hasCode) {
|
|
273
|
+
if (e.code)
|
|
274
|
+
tail.code = label(nr, cy + cs * 0.36, cs, e.code, "rk-sub", "end", 700);
|
|
275
|
+
nr -= codeW;
|
|
276
|
+
}
|
|
277
|
+
const nameAvail = Math.max(he, nr - x - rowPad * 0.5);
|
|
278
|
+
const second = e.club ?? e.country ?? "";
|
|
279
|
+
const twoLine = second !== "" && he >= 40;
|
|
280
|
+
const ns = fitSize(name, he * (twoLine ? 0.34 : 0.38), nameAvail, 0.55);
|
|
281
|
+
if (twoLine) {
|
|
282
|
+
const ss = he * 0.22;
|
|
283
|
+
const block = ns * 1.05 + ss * 1.3;
|
|
284
|
+
const t = cy - block / 2;
|
|
285
|
+
g.push(label(x, t + ns * 0.86, ns, fit(name, ns, nameAvail + 1, bw), "rk-ink", "start", 800));
|
|
286
|
+
g.push(label(x, t + ns * 1.05 + ss * 1.05, fitSize(second, ss, nameAvail, 0.7), fit(second, fitSize(second, ss, nameAvail, 0.7), nameAvail + 1, bw), "rk-sub", "start", 600));
|
|
287
|
+
}
|
|
288
|
+
else
|
|
289
|
+
g.push(label(x, cy + ns * 0.36, ns, fit(name, ns, nameAvail + 1, bw), "rk-ink", "start", 800));
|
|
290
|
+
g.push(...[tail.code, tail.points, tail.rank].filter((t) => !!t));
|
|
291
|
+
body.push(`<g class="rk-entry">${g.join("")}</g>`);
|
|
292
|
+
});
|
|
293
|
+
if (options.rows !== undefined || slice.length < all.length) {
|
|
294
|
+
const t = fill(w.rankRange, { a: v.number(from + 1), b: v.number(from + slice.length), n: v.number(all.length) });
|
|
295
|
+
if (slice.length < all.length)
|
|
296
|
+
body.push(label(bx + boxW, header.top - pad * 0.15, hs, fit(t, hs, boxW * 0.35, bw), "rk-sub", "end", 700));
|
|
297
|
+
}
|
|
298
|
+
return boardWrap(f, options.title ?? [title, source.category].filter(Boolean).join(": "), describeEntryList(source, options), body, "entry-list");
|
|
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
|
+
}
|
package/dist/ranking.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Static SVG for a given state, like every drawing here.
|
|
6
6
|
*/
|
|
7
7
|
import { type BoardOptions } from "./rank-kit.ts";
|
|
8
|
+
import { type Voice } from "./locales.ts";
|
|
9
|
+
import type { Team } from "./types.ts";
|
|
8
10
|
/** A name in two parts: the family name is written in capitals and first by default (`SURNAME Given`, as judo and the IJF write it). */
|
|
9
11
|
export interface RankingName {
|
|
10
12
|
given?: string;
|
|
@@ -87,8 +89,18 @@ export declare function rankingCategoryLabel(category: {
|
|
|
87
89
|
sex?: "men" | "women" | "open";
|
|
88
90
|
weight?: string;
|
|
89
91
|
}, locale?: string): string;
|
|
92
|
+
export declare const voiceFor: (locale?: string) => Voice;
|
|
93
|
+
export declare const nameText: (n: RankingEntry["name"], style: NonNullable<RankingOptions["nameStyle"]>, v: Voice) => string;
|
|
94
|
+
export declare const pointsText: (p: RankingEntry["points"], precision: number | undefined, v: Voice) => string;
|
|
95
|
+
export interface R {
|
|
96
|
+
x: number;
|
|
97
|
+
y: number;
|
|
98
|
+
w: number;
|
|
99
|
+
h: number;
|
|
100
|
+
}
|
|
90
101
|
/** The ranking as a sentence, the drawing's description. */
|
|
91
102
|
export declare function describeRanking(source: RankingSource, options?: Pick<RankingOptions, "locale" | "words" | "nameStyle" | "precision" | "ties" | "sort" | "rankStyle" | "rows" | "page">): string;
|
|
103
|
+
export declare const teamFor: (e: RankingEntry, id: string, name: string, badge: string) => Team;
|
|
92
104
|
/**
|
|
93
105
|
* The ranking list as an SVG. Throws a `RangeError` for an entry with no name, a rank or change that is not a whole number, an option that is not one of its values, or a bad size or theme.
|
|
94
106
|
*/
|
package/dist/ranking.js
CHANGED
|
@@ -45,8 +45,8 @@ export function rankingCategoryLabel(category, locale) {
|
|
|
45
45
|
parts.push(sex);
|
|
46
46
|
return parts.join(" · ");
|
|
47
47
|
}
|
|
48
|
-
const voiceFor = (locale) => voiceOf(locale === undefined ? {} : { locale });
|
|
49
|
-
const nameText = (n, style, v) => {
|
|
48
|
+
export const voiceFor = (locale) => voiceOf(locale === undefined ? {} : { locale });
|
|
49
|
+
export const nameText = (n, style, v) => {
|
|
50
50
|
if (typeof n === "string")
|
|
51
51
|
return n;
|
|
52
52
|
const given = (n.given ?? "").trim();
|
|
@@ -55,7 +55,7 @@ const nameText = (n, style, v) => {
|
|
|
55
55
|
return sur;
|
|
56
56
|
return style === "given-first" || style === "as-is" ? `${given} ${sur}` : `${sur} ${given}`;
|
|
57
57
|
};
|
|
58
|
-
const pointsText = (p, precision, v) => {
|
|
58
|
+
export const pointsText = (p, precision, v) => {
|
|
59
59
|
if (p === undefined || p === null)
|
|
60
60
|
return "";
|
|
61
61
|
if (typeof p === "string")
|
|
@@ -136,7 +136,7 @@ export function describeRanking(source, options = {}) {
|
|
|
136
136
|
}
|
|
137
137
|
return parts.join(w.sep) + w.end;
|
|
138
138
|
}
|
|
139
|
-
const teamFor = (e, id, name, badge) => ({ id, name, abbr: badge, ...(e.flag ? { flag: e.flag } : {}), ...(e.mark ? { mark: e.mark } : {}) });
|
|
139
|
+
export const teamFor = (e, id, name, badge) => ({ id, name, abbr: badge, ...(e.flag ? { flag: e.flag } : {}), ...(e.mark ? { mark: e.mark } : {}) });
|
|
140
140
|
/**
|
|
141
141
|
* The ranking list as an SVG. Throws a `RangeError` for an entry with no name, a rank or change that is not a whole number, an option that is not one of its values, or a bad size or theme.
|
|
142
142
|
*/
|
package/dist/types.d.ts
CHANGED
|
@@ -414,6 +414,12 @@ export interface Words {
|
|
|
414
414
|
rankNone: string;
|
|
415
415
|
/** What joins the pieces of one entry in the description ("1, NAKAMURA Kenji, 412.50 points"). */
|
|
416
416
|
rankJoin: string;
|
|
417
|
+
/** Number-one cards (`rankingNumberOnes`): the heading, and `{category}`: "#1 in -57 kg". The entry list (`rankingEntryList`): the heading, the column heading over the rankings, and the mark of an athlete with none. */
|
|
418
|
+
rankNumberOnes: string;
|
|
419
|
+
rankNumberOneIn: string;
|
|
420
|
+
rankEntryTitle: string;
|
|
421
|
+
rankColRanking: string;
|
|
422
|
+
rankUnranked: string;
|
|
417
423
|
}
|
|
418
424
|
/** A language: its words, the names of the rounds (by their English names), and the tag `Intl` formats numbers and dates for. */
|
|
419
425
|
export interface Locale {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.12.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.12.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/taisen-brackets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|