@johnmorrisdotca/taisen-brackets 0.6.1 → 0.7.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 +18 -0
- package/NOTICE.md +3 -1
- package/README.md +14 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/judo-model.d.ts +66 -0
- package/dist/judo-model.js +102 -0
- package/dist/judo-text.d.ts +19 -0
- package/dist/judo-text.js +84 -0
- package/dist/judo.d.ts +44 -0
- package/dist/judo.js +75 -0
- package/dist/layout.js +2 -2
- package/dist/layouts.js +4 -3
- package/dist/locales.js +25 -0
- package/dist/matchup.js +41 -8
- package/dist/playin.js +2 -1
- package/dist/render.js +37 -18
- package/dist/table.js +3 -2
- package/dist/theme.d.ts +9 -0
- package/dist/theme.js +5 -0
- package/dist/types.d.ts +49 -3
- 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,24 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.0] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
Judo, step 1: results in the bracket boxes, and the bracket of a weight category with the repechage and two bronze medals. Nothing changes for a bracket that has no judo result (every existing snapshot is unchanged).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`Matchup.judo`**: a judo contest's result, `{ sides: [white, blue], method?, hansokuMake?, goldenScore?, time? }`, each side `{ ippon, wazaAri, yuko, shido }`. The winner, the method and the score come from it under the IJF Sport and Organisation Rules, Refereeing Rules of 21 January 2026: ippon, two waza-ari are ippon (Article 16), yuko are counted and never add up to a waza-ari (Article 15; yuko is back since 2025, 5 to 9 seconds of osaekomi, Article 7), shido never decide a contest and the third is hansoku-make (Article 18), a direct hansoku-make (`hansokuMake`), golden score (Article 13), fusen-gachi and kiken-gachi (Article 19, the matchup names the `winner`). `Tie.result.score` is the scoreboard's 100, 10 and 1. [docs/judo.md](docs/judo.md) quotes each rule.
|
|
16
|
+
- **`judoOutcome(result, winner?)`** checks a result and works out the winner, the method, the scoreboard points and whether both sides were disqualified; a count out of range, two ippon, a method or winner the scores contradict, and a default with no winner are a `RangeError` that names the field. `judoSeconds`, `judoClock`.
|
|
17
|
+
- **The boxes**: the score in marks (`I`, `W`, `W2`, `Y`, `Y3`, `0`), a white or blue bar at the edge of each row, the shido as yellow and red cards beside the score, and under the rows how it was won, `GS` for golden score and the time. The tooltip, the description and `bracketTable` say the same in a sentence, with each side's shido. Works in the tree, `rows`, `shields`, `update`, `columns` and `series`, both looks, every size. CSS custom properties `--tb-judo-white`, `--tb-judo-blue`, `--tb-shido`, `--tb-hansoku`; `bracketStyle({ judo: true })`.
|
|
18
|
+
- **`judoBracket(athletes, { system, prefix })`**: a weight category for any number of athletes, with the IJF's quarter-final repechage (Sport and Organisation Rules 2.5.2: two repechage contests, two bronze medal contests) for eight athletes or more, or a direct knockout whose losing semi-finalists are the bronze medallists. `Matchup.stage` takes `"repechage"` and `"bronze"` (fed by the losers of the tree's ties, drawn together under the tree with their own heading); `Tie.stage`. `judoPodium(input)` gives gold, silver and the bronze medallists; `judoCategoryLabel({ sex, weight }, locale)` the heading ("Men -66 kg"); `JUDO_WEIGHT_CLASSES`.
|
|
19
|
+
- Words for judo in the five languages (the marks, the methods, golden score, the shido, the headings, the weight class). The French and Japanese were read by native-language reviewers; the Spanish and German need one (docs/languages.md).
|
|
20
|
+
- The demo has a Judo sport: a made-up men's -66 kg category of 16 athletes with every kind of ending, in the tree and in rows, with Hata's flags (Georgia, Uzbekistan, Cuba, Mongolia, Azerbaijan, Hungary, Italy added).
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- The note under a box is cut to fit with the same width estimate the names use, so that a long note never reaches past its box.
|
|
25
|
+
- The ties kept apart under the tree are 66 high in the road look when one of them has a note.
|
|
26
|
+
|
|
9
27
|
## [0.6.1] - 2026-10-19
|
|
10
28
|
|
|
11
29
|
No change to the package's code. The README's pictures were checked against 0.6.0's features and remade: they already show every layout, so none is added.
|
package/NOTICE.md
CHANGED
|
@@ -22,9 +22,11 @@ The code of this package is under the MIT licence (see [LICENSE](LICENSE)). The
|
|
|
22
22
|
| The demo's trophies (`demo/emblems.js`, and the `dougu-sports/` files the Pages build copies beside the page) | [Dougu Sports](https://github.com/johnmorrisdotca/dougu-sports) 0.1.0, a development dependency and an optional peer: generic emblems drawn for that package, which carry no league's or federation's name or mark | MIT, same owner; its NOTICE is in the package |
|
|
23
23
|
| The words of the languages (`src/locales.ts`) | The football terms (the rounds of the World Cup, extra time, the shoot-out, a replay, a walkover) were read on 2026-10-14 in the French, Spanish, German and Japanese Wikipedia articles on the World Cups of 2022, 2026 and 1938; the articles are cited in [docs/languages.md](docs/languages.md). The other words, including those of the road to the final, are written for this package; the French and Japanese were read by native-language reviewers, the Spanish and German were not | Wikipedia's text is CC BY-SA 4.0; only terms (single words and short phrases) are taken, no sentence or table, and the articles are credited in docs/languages.md |
|
|
24
24
|
| The names of countries in each language | Not in the package: `countryName` asks `Intl.DisplayNames`, which holds Unicode CLDR's names; the demo passes it each team's ISO code. The home nations and the two states that no longer exist are written in `src/locales.ts` and `demo/data.js` | CLDR is under the Unicode licence (https://www.unicode.org/license.txt) |
|
|
25
|
+
| The judo scoring (`src/judo-model.ts`, [docs/judo.md](docs/judo.md)) | The facts of the IJF Sport and Organisation Rules, Refereeing Rules, version 21 January 2026 (Articles 6, 7, 13 to 19), and of its competition systems (2.5, March 2023 version) and weight classes (2.2.1), as published by the International Judo Federation on ijf.org; and the IJF news items of 9 December 2016 and December 2024. Read on 2026-10-20. Only facts and a few short quotations, cited by article, are used; no text, drawing or mark of the IJF is copied | The rules are the IJF's; the package states what they say and names the article. No IJF mark is used |
|
|
26
|
+
| The demo's made-up judo tournament (`demo/data/judo-sample.js`) | Made up: the 16 athletes and their results are invented, and the countries are only there for the flags (the same Hata flags as above, with Azerbaijan, Cuba, Georgia, Hungary, Italy, Mongolia and Uzbekistan added) | Not real |
|
|
25
27
|
| The demo's page | Written for this package, with the family stylesheet `demo/family.css` | MIT |
|
|
26
28
|
|
|
27
|
-
The flags in the demo are for the nations of the World Cups of 2022 and 2026: `AR AT AU BA BE BR CA CD CH CI CO CV DE DZ EC EG ES FR GB-ENG GH HR JP KR MA MX NL NO PL PT PY SE SN US ZA`. The package itself ships no flag, no logo and no data of the leagues.
|
|
29
|
+
The flags in the demo are for the nations of the World Cups of 2022 and 2026 and of the made-up judo tournament: `AR AT AU AZ BA BE BR CA CD CH CI CO CU CV DE DZ EC EG ES FR GB-ENG GE GH HR HU IT JP KR MA MN MX NL NO PL PT PY SE SN US UZ ZA`. The package itself ships no flag, no logo and no data of the leagues.
|
|
28
30
|
|
|
29
31
|
No code, text or picture was taken from the bracket libraries compared in [docs/compare.md](docs/compare.md); that page records what each says of itself.
|
|
30
32
|
|
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/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/formats.md">Formats</a> · <a href="./docs/compare.md">How it compares</a></p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<picture>
|
|
@@ -238,6 +238,15 @@ console.log(ice.includes("tb-bgd"), mine.includes("#ff7ac8"), photo.includes('<i
|
|
|
238
238
|
|
|
239
239
|
In the road look, `seeds: true` writes a small tag (`P1`, `WC2`) beside each team's mark, `codes: true` its three letters under the mark in a box of marks, `vs: true` a "v" between the two marks, and a team with no flag or logo is a tile with its letters. [docs/backgrounds.md](./docs/backgrounds.md) has the options, the contrast rules and the module the other packages of the family share.
|
|
240
240
|
|
|
241
|
+
## Judo
|
|
242
|
+
|
|
243
|
+
A judo contest is a tie with a `judo` result: ippon, waza-ari, yuko and shido as the IJF's current Sport and Organisation Rules score them (yuko is back since 2025), golden score, the contest time and how it was won. The box writes the marks (`I`, `W`, `Y`), the shido as yellow and red cards and a note under the two rows, in the five languages. `judoBracket` makes a weight category's bracket with the IJF's repechage and its two bronze medal contests. [docs/judo.md](./docs/judo.md) quotes the rules it follows.
|
|
244
|
+
|
|
245
|
+
<p align="center"><picture>
|
|
246
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/judo-desk-dark.webp">
|
|
247
|
+
<img alt="A made-up men's minus 66 kilogram judo tournament of 16 athletes with round flags, score marks, shido cards, how each contest was won and its time, and the repechage and bronze medal contests under the tree." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/judo-desk-light.webp" width="720">
|
|
248
|
+
</picture><br><em>Men's -66 kg, with the quarter-final repechage (invented names).</em></p>
|
|
249
|
+
|
|
241
250
|
## In 30 seconds
|
|
242
251
|
|
|
243
252
|
```sh
|
|
@@ -283,6 +292,7 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
|
|
|
283
292
|
- **Team marks you supply**: a logo address or inline SVG, drawn round or square; a team with none is its initials on a colour. Round flags from [Hata](https://github.com/johnmorrisdotca/hata) go in as they are.
|
|
284
293
|
- **The champion in the centre**, with a slot for your trophy: your own SVG or image, or an emblem from [Dougu Sports](https://github.com/johnmorrisdotca/dougu-sports) (a hockey cup, a basketball trophy, a globe) through an optional plug-in, with no dependency added.
|
|
285
294
|
- **Accessible**: `role="img"` with a title and a description, a tooltip sentence on every tie, and `bracketTable`.
|
|
295
|
+
- **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)).
|
|
286
296
|
- **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
|
|
287
297
|
- **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.
|
|
288
298
|
- **Deterministic**: no clock, no random number, no counter; the same input gives the same string, and ids come from a hash of the input.
|
|
@@ -642,6 +652,7 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
|
|
|
642
652
|
- `singleElimination(teams, options?)`, `advanceWinners(matchups)`, `standardSeeds(size)`: brackets for any number of teams.
|
|
643
653
|
- `matchupsFromGames(games, options)`: matchups from a flat list of games.
|
|
644
654
|
- `resolveMatchup`, `collectTeams`, `describeTie`, `noteParts`, `noteLong`: one tie's result, the teams of a bracket, a tie in a sentence.
|
|
655
|
+
- `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)). `Matchup.judo` and `Matchup.stage` (`"repechage"`, `"bronze"`) carry them into `drawBracket`.
|
|
645
656
|
- `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.
|
|
646
657
|
- `isEmblemId(value)`, and the types `EmblemRenderer`, `EmblemRequest`, `EmblemStyle`: the trophy emblems (see "Emblems for the trophy").
|
|
647
658
|
- `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
|
|
@@ -700,6 +711,7 @@ console.log(svg.includes('lang="nl"')); // true
|
|
|
700
711
|
| CFL | 1996 on | Division Semi-Final, Division Final, Grey Cup |
|
|
701
712
|
| MLS | 2023 on | Wild Card Round, Round One (best of three), Conference Semifinals, Conference Finals, MLS Cup |
|
|
702
713
|
| FIFA World Cup | every edition with a knockout, 1930 to 2026 (1950 had none) | Round of 32 (2026), Round of 16, Quarter-finals, Semi-finals, Final |
|
|
714
|
+
| Judo (IJF rules of 21 January 2026) | any weight category: `judoBracket` with the quarter-final repechage or a direct knockout | Round of N … Final, repechage, two bronze medal contests |
|
|
703
715
|
| Any other | single elimination for any number of teams | Round of N … Final |
|
|
704
716
|
|
|
705
717
|
Each era was checked against English Wikipedia (facts only; the articles are cited in [docs/formats.md](./docs/formats.md)). The year is the year the playoffs are played in, except for the NFL, whose year is its season's, so 2024 is the season whose playoffs were played in January and February 2025. A season did something of its own (a strike, a pandemic) has its own entry, and a later one is not covered until it is checked.
|
|
@@ -732,6 +744,7 @@ Any current browser, and Node 22 or later. The package makes no network request
|
|
|
732
744
|
|
|
733
745
|
## Roadmap
|
|
734
746
|
|
|
747
|
+
- Judo: a contest scoreboard drawing, a contest card and a mat queue board, the road to the final of a judoka, double and full repechage ([docs/judo.md](./docs/judo.md)).
|
|
735
748
|
- Double elimination, and the group tables on one page with the knockout they lead to.
|
|
736
749
|
- More languages (Portuguese, Italian, Korean, Chinese), and native review of the Spanish and German words.
|
|
737
750
|
- A custom element and a React helper, as in the other packages of the family.
|
package/dist/index.d.ts
CHANGED
|
@@ -17,3 +17,5 @@ export type { ResolvedBackground } from "./background.ts";
|
|
|
17
17
|
export { VERSION } from "./version.ts";
|
|
18
18
|
export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.ts";
|
|
19
19
|
export type { League, PlayoffFormat, PlayoffRound } from "./presets.ts";
|
|
20
|
+
export { judoBracket, judoPodium, judoCategoryLabel, judoOutcome, judoSeconds, judoClock, JUDO_WEIGHT_CLASSES } from "./judo.ts";
|
|
21
|
+
export type { JudoBracketOptions, JudoCategory, JudoMethod, JudoOutcome, JudoPodium, JudoResult, JudoSide, JudoSystem } from "./judo.ts";
|
package/dist/index.js
CHANGED
|
@@ -10,3 +10,4 @@ export { BACKGROUND_PRESETS, FAMILY_BACKGROUNDS_VERSION, backgroundSvg, contrast
|
|
|
10
10
|
export { resolveBackground } from "./background.js";
|
|
11
11
|
export { VERSION } from "./version.js";
|
|
12
12
|
export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.js";
|
|
13
|
+
export { judoBracket, judoPodium, judoCategoryLabel, judoOutcome, judoSeconds, judoClock, JUDO_WEIGHT_CLASSES } from "./judo.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Judo results: the scores of one contest and what they come to under the IJF Sport and Organisation Rules (Refereeing Rules, version
|
|
3
|
+
* 21 January 2026, for the Olympic cycle to Los Angeles 2028). Pure and sport-specific; nothing here draws. The rules this follows, by
|
|
4
|
+
* article, are in docs/judo.md.
|
|
5
|
+
*
|
|
6
|
+
* - Article 7 and 15: osaekomi of 20 seconds is ippon, 10 to 19 seconds waza-ari, 5 to 9 seconds yuko.
|
|
7
|
+
* - Article 15: "Yuko scores are counted (1, 2, 3, etc.) but they do not add up to waza-ari."
|
|
8
|
+
* - Article 16: a second waza-ari is waza-ari-awasete-ippon, ippon.
|
|
9
|
+
* - Article 18: "Two shido can be awarded and the third will be hansoku-make"; "A contest cannot be decided by single or accumulated shido, until hansoku-make is reached."
|
|
10
|
+
* - Article 13: a level contest at the end of regular time (4 minutes) goes to golden score, which has no time limit and ends at the first score or hansoku-make.
|
|
11
|
+
*/
|
|
12
|
+
/** How a contest was won. `"score"` is a decision on the technical scores (the waza-ari, then the yuko, counted). */
|
|
13
|
+
export type JudoMethod = "ippon" | "waza-ari-awasete-ippon" | "score" | "hansoku-make" | "fusen-gachi" | "kiken-gachi";
|
|
14
|
+
/** What one side scored in a contest. Anything left out is none. */
|
|
15
|
+
export interface JudoSide {
|
|
16
|
+
/** Ippon: the contest ends. */
|
|
17
|
+
ippon?: boolean;
|
|
18
|
+
/** Waza-ari, 0 to 2; a second one is waza-ari-awasete-ippon. */
|
|
19
|
+
wazaAri?: number;
|
|
20
|
+
/** Yuko: counted, never added up to a waza-ari. */
|
|
21
|
+
yuko?: number;
|
|
22
|
+
/** Shido (the penalties), 0 to 3; the third is hansoku-make (indirect). */
|
|
23
|
+
shido?: number;
|
|
24
|
+
}
|
|
25
|
+
/** The result of one judo contest, the sides in the order of the matchup's `sides`: the first is white, the second blue. */
|
|
26
|
+
export interface JudoResult {
|
|
27
|
+
sides: readonly [JudoSide, JudoSide];
|
|
28
|
+
/** How it was won. Left out, it is worked out from the scores; fusen-gachi and kiken-gachi (no score can say so) need the matchup's `winner`. */
|
|
29
|
+
method?: JudoMethod;
|
|
30
|
+
/** The side (0 or 1) that was given a direct hansoku-make (a grave infringement): the other side wins by ippon. */
|
|
31
|
+
hansokuMake?: 0 | 1;
|
|
32
|
+
/** The contest went on to golden score, and the scores are the ones at its end. */
|
|
33
|
+
goldenScore?: boolean;
|
|
34
|
+
/** The contest time: seconds (`222`) or `"3:42"`. Regular time is 4 minutes; golden score runs past it. */
|
|
35
|
+
time?: number | string;
|
|
36
|
+
}
|
|
37
|
+
/** A result checked and worked out. */
|
|
38
|
+
export interface JudoOutcome {
|
|
39
|
+
/** 0 or 1, the side that won; `null` while nothing has decided it (level scores: golden score is next). */
|
|
40
|
+
winner: 0 | 1 | null;
|
|
41
|
+
/** How it was won, or `null` while undecided. */
|
|
42
|
+
method: JudoMethod | null;
|
|
43
|
+
goldenScore: boolean;
|
|
44
|
+
/** The contest time in seconds, or `null`. */
|
|
45
|
+
seconds: number | null;
|
|
46
|
+
/** The two sides with every count filled in. */
|
|
47
|
+
sides: readonly [Required<JudoSide>, Required<JudoSide>];
|
|
48
|
+
/** The value on the scoreboard (Articles 4 and 15: ippon 100, waza-ari 10, yuko 1; two waza-ari are ippon). */
|
|
49
|
+
points: readonly [number, number];
|
|
50
|
+
/** Both sides were given the third shido: the contest is recorded 0-0 and nobody wins (Article 18.3.1). */
|
|
51
|
+
doubleHansokuMake: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** The IJF's senior weight classes in kilograms, the lightest first: a number is "up to and including", a text starting `+` is "over". */
|
|
54
|
+
export declare const JUDO_WEIGHT_CLASSES: Readonly<Record<"men" | "women", readonly string[]>>;
|
|
55
|
+
/** A time as seconds: a number, or `"m:ss"`. */
|
|
56
|
+
export declare function judoSeconds(time: number | string | undefined): number | null;
|
|
57
|
+
/** Seconds as `m:ss`. */
|
|
58
|
+
export declare const judoClock: (seconds: number) => string;
|
|
59
|
+
/**
|
|
60
|
+
* Checks a result against the rules and works out who won and how. `winner` is the winner the matchup names (needed for fusen-gachi and
|
|
61
|
+
* kiken-gachi, which no score can show). Throws a `RangeError` for a count out of range, two ippon, a method the scores contradict, or a
|
|
62
|
+
* default with no winner.
|
|
63
|
+
*/
|
|
64
|
+
export declare function judoOutcome(result: JudoResult, winner?: 0 | 1 | null): JudoOutcome;
|
|
65
|
+
/** Whether a result has any score or penalty at all (a contest still to be fought has none). */
|
|
66
|
+
export declare const judoHasScore: (o: JudoOutcome) => boolean;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Judo results: the scores of one contest and what they come to under the IJF Sport and Organisation Rules (Refereeing Rules, version
|
|
3
|
+
* 21 January 2026, for the Olympic cycle to Los Angeles 2028). Pure and sport-specific; nothing here draws. The rules this follows, by
|
|
4
|
+
* article, are in docs/judo.md.
|
|
5
|
+
*
|
|
6
|
+
* - Article 7 and 15: osaekomi of 20 seconds is ippon, 10 to 19 seconds waza-ari, 5 to 9 seconds yuko.
|
|
7
|
+
* - Article 15: "Yuko scores are counted (1, 2, 3, etc.) but they do not add up to waza-ari."
|
|
8
|
+
* - Article 16: a second waza-ari is waza-ari-awasete-ippon, ippon.
|
|
9
|
+
* - Article 18: "Two shido can be awarded and the third will be hansoku-make"; "A contest cannot be decided by single or accumulated shido, until hansoku-make is reached."
|
|
10
|
+
* - Article 13: a level contest at the end of regular time (4 minutes) goes to golden score, which has no time limit and ends at the first score or hansoku-make.
|
|
11
|
+
*/
|
|
12
|
+
/** The IJF's senior weight classes in kilograms, the lightest first: a number is "up to and including", a text starting `+` is "over". */
|
|
13
|
+
export const JUDO_WEIGHT_CLASSES = Object.freeze({
|
|
14
|
+
men: Object.freeze(["-60", "-66", "-73", "-81", "-90", "-100", "+100"]),
|
|
15
|
+
women: Object.freeze(["-48", "-52", "-57", "-63", "-70", "-78", "+78"]),
|
|
16
|
+
});
|
|
17
|
+
const whole = (what, n, max) => {
|
|
18
|
+
const v = n === undefined ? 0 : n;
|
|
19
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v > max)
|
|
20
|
+
throw new RangeError(`taisen-brackets: ${what} is a whole number from 0 to ${max}, not ${JSON.stringify(v)}`);
|
|
21
|
+
return v;
|
|
22
|
+
};
|
|
23
|
+
function filled(side, k) {
|
|
24
|
+
const who = k === 0 ? "the white side's" : "the blue side's";
|
|
25
|
+
return {
|
|
26
|
+
ippon: side.ippon === true,
|
|
27
|
+
wazaAri: whole(`${who} waza-ari`, side.wazaAri, 2),
|
|
28
|
+
yuko: whole(`${who} yuko`, side.yuko, 99),
|
|
29
|
+
shido: whole(`${who} shido`, side.shido, 3),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** A time as seconds: a number, or `"m:ss"`. */
|
|
33
|
+
export function judoSeconds(time) {
|
|
34
|
+
if (time === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
if (typeof time === "number") {
|
|
37
|
+
if (!Number.isFinite(time) || time < 0)
|
|
38
|
+
throw new RangeError(`taisen-brackets: a judo contest time is seconds from 0, not ${time}`);
|
|
39
|
+
return Math.round(time);
|
|
40
|
+
}
|
|
41
|
+
const m = /^(\d{1,3}):([0-5]\d)$/.exec(time.trim());
|
|
42
|
+
if (!m)
|
|
43
|
+
throw new RangeError(`taisen-brackets: a judo contest time is seconds or "m:ss" ("3:42"), not ${JSON.stringify(time)}`);
|
|
44
|
+
return Number(m[1]) * 60 + Number(m[2]);
|
|
45
|
+
}
|
|
46
|
+
/** Seconds as `m:ss`. */
|
|
47
|
+
export const judoClock = (seconds) => `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
|
|
48
|
+
const scoreboard = (s) => (s.ippon || s.wazaAri >= 2 ? 100 : s.wazaAri * 10 + s.yuko);
|
|
49
|
+
/**
|
|
50
|
+
* Checks a result against the rules and works out who won and how. `winner` is the winner the matchup names (needed for fusen-gachi and
|
|
51
|
+
* kiken-gachi, which no score can show). Throws a `RangeError` for a count out of range, two ippon, a method the scores contradict, or a
|
|
52
|
+
* default with no winner.
|
|
53
|
+
*/
|
|
54
|
+
export function judoOutcome(result, winner = null) {
|
|
55
|
+
const sides = [filled(result.sides[0], 0), filled(result.sides[1], 1)];
|
|
56
|
+
const seconds = judoSeconds(result.time);
|
|
57
|
+
const points = [scoreboard(sides[0]), scoreboard(sides[1])];
|
|
58
|
+
const golden = result.goldenScore === true;
|
|
59
|
+
const out = (w, method, double = false) => ({ winner: w, method, goldenScore: golden, seconds, sides, points, doubleHansokuMake: double });
|
|
60
|
+
const other = (k) => (k === 0 ? 1 : 0);
|
|
61
|
+
if (sides[0].ippon && sides[1].ippon)
|
|
62
|
+
throw new RangeError("taisen-brackets: both sides of a judo contest cannot score ippon");
|
|
63
|
+
if (sides[0].wazaAri === 2 && sides[1].wazaAri === 2)
|
|
64
|
+
throw new RangeError("taisen-brackets: both sides of a judo contest cannot score waza-ari-awasete-ippon");
|
|
65
|
+
const given = result.method;
|
|
66
|
+
const agree = (derived) => {
|
|
67
|
+
if (given !== undefined && derived.method !== given)
|
|
68
|
+
throw new RangeError(`taisen-brackets: the judo method ${JSON.stringify(given)} does not fit the scores, which make ${JSON.stringify(derived.method)}`);
|
|
69
|
+
return derived;
|
|
70
|
+
};
|
|
71
|
+
if (given === "fusen-gachi" || given === "kiken-gachi") {
|
|
72
|
+
if (winner === null)
|
|
73
|
+
throw new RangeError(`taisen-brackets: a judo contest won by ${given} needs the matchup's winner`);
|
|
74
|
+
return out(winner, given);
|
|
75
|
+
}
|
|
76
|
+
if (result.hansokuMake !== undefined) {
|
|
77
|
+
if (result.hansokuMake !== 0 && result.hansokuMake !== 1)
|
|
78
|
+
throw new RangeError("taisen-brackets: hansokuMake is 0 or 1, the side that was penalised");
|
|
79
|
+
return agree(out(other(result.hansokuMake), "hansoku-make"));
|
|
80
|
+
}
|
|
81
|
+
if (sides[0].shido >= 3 && sides[1].shido >= 3)
|
|
82
|
+
return agree(out(null, "hansoku-make", true));
|
|
83
|
+
for (const k of [0, 1])
|
|
84
|
+
if (sides[k].shido >= 3)
|
|
85
|
+
return agree(out(other(k), "hansoku-make"));
|
|
86
|
+
for (const k of [0, 1]) {
|
|
87
|
+
if (sides[k].ippon)
|
|
88
|
+
return agree(out(k, "ippon"));
|
|
89
|
+
if (sides[k].wazaAri >= 2)
|
|
90
|
+
return agree(out(k, "waza-ari-awasete-ippon"));
|
|
91
|
+
}
|
|
92
|
+
const by = (a, b) => (a > b ? 0 : b > a ? 1 : null);
|
|
93
|
+
const decided = by(sides[0].wazaAri, sides[1].wazaAri) ?? by(sides[0].yuko, sides[1].yuko);
|
|
94
|
+
if (decided !== null)
|
|
95
|
+
return agree(out(decided, "score"));
|
|
96
|
+
if (given !== undefined)
|
|
97
|
+
throw new RangeError(`taisen-brackets: the judo method ${JSON.stringify(given)} does not fit scores that are level`);
|
|
98
|
+
// Level: nothing has decided it, though the matchup may name a winner (a contest recorded with no scores).
|
|
99
|
+
return out(null, null);
|
|
100
|
+
}
|
|
101
|
+
/** Whether a result has any score or penalty at all (a contest still to be fought has none). */
|
|
102
|
+
export const judoHasScore = (o) => o.sides.some((s) => s.ippon || s.wazaAri > 0 || s.yuko > 0 || s.shido > 0);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JudoMethod, JudoSide } from "./judo-model.ts";
|
|
2
|
+
import type { Tie } from "./types.ts";
|
|
3
|
+
import { type Voice } from "./locales.ts";
|
|
4
|
+
type Filled = Required<JudoSide>;
|
|
5
|
+
/** A side's score as the box writes it: `I` for ippon, `W` a waza-ari (`W2` two), `Y` a yuko (`Y3` three), "0" for none. The marks are the language's (`judoIppon`, `judoWazaAri`, `judoYuko`). */
|
|
6
|
+
export declare function judoMark(side: Filled, v: Voice): string;
|
|
7
|
+
/** A side's score in words, for a sentence: "ippon", "waza-ari yuko", "2 waza-ari 3 yuko", "0". */
|
|
8
|
+
export declare function judoSideLong(side: Filled, v: Voice): string;
|
|
9
|
+
/** How a contest was won, in the language. */
|
|
10
|
+
export declare function judoMethodWord(method: JudoMethod, v: Voice): string;
|
|
11
|
+
/** The pieces of a judo contest's note, short or in a sentence: how it was won, golden score, the contest time, and (long only) each side's shido. */
|
|
12
|
+
export declare function judoNotes(tie: Tie, v: Voice, long: boolean): string[];
|
|
13
|
+
/** What a row of a box writes for a side's score: a judo contest's marks, else the number. `""` when nothing was played. */
|
|
14
|
+
export declare function sideScore(tie: Tie, k: 0 | 1, v: Voice): string;
|
|
15
|
+
/** A side's score in words for a sentence (a judo contest's, else the number). */
|
|
16
|
+
export declare function sideScoreLong(tie: Tie, k: 0 | 1, v: Voice): string;
|
|
17
|
+
/** The cards beside a side's score: a yellow one for each shido, and a red one for the third (hansoku-make) or for a direct hansoku-make: `["y", "y", "r"]`. */
|
|
18
|
+
export declare function shidoCards(tie: Tie, k: 0 | 1): ("y" | "r")[];
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { judoClock } from "./judo-model.js";
|
|
2
|
+
import { fill } from "./locales.js";
|
|
3
|
+
/** A side's score as the box writes it: `I` for ippon, `W` a waza-ari (`W2` two), `Y` a yuko (`Y3` three), "0" for none. The marks are the language's (`judoIppon`, `judoWazaAri`, `judoYuko`). */
|
|
4
|
+
export function judoMark(side, v) {
|
|
5
|
+
const w = v.words;
|
|
6
|
+
if (side.ippon)
|
|
7
|
+
return w.judoIppon;
|
|
8
|
+
const parts = [];
|
|
9
|
+
if (side.wazaAri > 0)
|
|
10
|
+
parts.push(w.judoWazaAri + (side.wazaAri > 1 ? v.number(side.wazaAri) : ""));
|
|
11
|
+
if (side.yuko > 0)
|
|
12
|
+
parts.push(w.judoYuko + (side.yuko > 1 ? v.number(side.yuko) : ""));
|
|
13
|
+
return parts.length ? parts.join(" ") : v.number(0);
|
|
14
|
+
}
|
|
15
|
+
/** A side's score in words, for a sentence: "ippon", "waza-ari yuko", "2 waza-ari 3 yuko", "0". */
|
|
16
|
+
export function judoSideLong(side, v) {
|
|
17
|
+
const w = v.words;
|
|
18
|
+
if (side.ippon)
|
|
19
|
+
return w.judoIpponLong;
|
|
20
|
+
const parts = [];
|
|
21
|
+
if (side.wazaAri > 0)
|
|
22
|
+
parts.push(side.wazaAri > 1 ? `${v.number(side.wazaAri)} ${w.judoWazaAriLong}` : w.judoWazaAriLong);
|
|
23
|
+
if (side.yuko > 0)
|
|
24
|
+
parts.push(side.yuko > 1 ? `${v.number(side.yuko)} ${w.judoYukoLong}` : w.judoYukoLong);
|
|
25
|
+
return parts.length ? parts.join(" ") : v.number(0);
|
|
26
|
+
}
|
|
27
|
+
/** How a contest was won, in the language. */
|
|
28
|
+
export function judoMethodWord(method, v) {
|
|
29
|
+
const w = v.words;
|
|
30
|
+
switch (method) {
|
|
31
|
+
case "ippon": return w.judoMethodIppon;
|
|
32
|
+
case "waza-ari-awasete-ippon": return w.judoMethodAwasete;
|
|
33
|
+
case "score": return w.judoMethodScore;
|
|
34
|
+
case "hansoku-make": return w.judoMethodHansoku;
|
|
35
|
+
case "fusen-gachi": return w.judoMethodFusen;
|
|
36
|
+
case "kiken-gachi": return w.judoMethodKiken;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The pieces of a judo contest's note, short or in a sentence: how it was won, golden score, the contest time, and (long only) each side's shido. */
|
|
40
|
+
export function judoNotes(tie, v, long) {
|
|
41
|
+
const j = tie.judo;
|
|
42
|
+
if (!j)
|
|
43
|
+
return [];
|
|
44
|
+
const w = v.words;
|
|
45
|
+
const out = [];
|
|
46
|
+
if (j.method !== null)
|
|
47
|
+
out.push(judoMethodWord(j.method, v));
|
|
48
|
+
if (j.goldenScore)
|
|
49
|
+
out.push(long ? w.judoGoldenLong : w.judoGolden);
|
|
50
|
+
if (j.seconds !== null)
|
|
51
|
+
out.push(fill(w.judoTime, { t: judoClock(j.seconds) }));
|
|
52
|
+
if (long) {
|
|
53
|
+
[0, 1].forEach((k) => {
|
|
54
|
+
const team = tie.sides[k];
|
|
55
|
+
if (j.sides[k].shido > 0 && team)
|
|
56
|
+
out.push(fill(w.judoShidoOf, { name: v.team(team), n: v.number(j.sides[k].shido) }));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** What a row of a box writes for a side's score: a judo contest's marks, else the number. `""` when nothing was played. */
|
|
62
|
+
export function sideScore(tie, k, v) {
|
|
63
|
+
if (tie.judo)
|
|
64
|
+
return tie.result.score ? judoMark(tie.judo.sides[k], v) : "";
|
|
65
|
+
return tie.result.score ? v.number(tie.result.score[k]) : "";
|
|
66
|
+
}
|
|
67
|
+
/** A side's score in words for a sentence (a judo contest's, else the number). */
|
|
68
|
+
export function sideScoreLong(tie, k, v) {
|
|
69
|
+
if (tie.judo)
|
|
70
|
+
return judoSideLong(tie.judo.sides[k], v);
|
|
71
|
+
return tie.result.score ? v.number(tie.result.score[k]) : "";
|
|
72
|
+
}
|
|
73
|
+
/** The cards beside a side's score: a yellow one for each shido, and a red one for the third (hansoku-make) or for a direct hansoku-make: `["y", "y", "r"]`. */
|
|
74
|
+
export function shidoCards(tie, k) {
|
|
75
|
+
const j = tie.judo;
|
|
76
|
+
if (!j)
|
|
77
|
+
return [];
|
|
78
|
+
const n = j.sides[k].shido;
|
|
79
|
+
if (n >= 3)
|
|
80
|
+
return ["y", "y", "r"];
|
|
81
|
+
// A direct hansoku-make is the loser's red card, when it came with no third shido.
|
|
82
|
+
const penalised = j.method === "hansoku-make" && j.winner !== null && j.winner !== k;
|
|
83
|
+
return penalised ? [...Array.from({ length: n }, () => "y"), "r"] : Array.from({ length: n }, () => "y");
|
|
84
|
+
}
|
package/dist/judo.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BracketInput, BracketLayout, Matchup, Team } from "./types.ts";
|
|
2
|
+
export { JUDO_WEIGHT_CLASSES, judoOutcome, judoSeconds, judoClock } from "./judo-model.ts";
|
|
3
|
+
export type { JudoMethod, JudoOutcome, JudoResult, JudoSide } from "./judo-model.ts";
|
|
4
|
+
/** How a judo tournament is drawn: `repechage` (the IJF's quarter-final repechage, with its two bronze medal contests), or `direct` (a direct knockout in which both semi-final losers are bronze medallists). */
|
|
5
|
+
export type JudoSystem = "repechage" | "direct";
|
|
6
|
+
export interface JudoBracketOptions {
|
|
7
|
+
/** Default `repechage` for eight athletes or more, else `direct`. */
|
|
8
|
+
system?: JudoSystem;
|
|
9
|
+
/** The ids of the contests are `${prefix}1-1`, `${prefix}1-2`… (round, then place in the round), `${prefix}rep-1`, `${prefix}bronze-1`. Default "J". */
|
|
10
|
+
prefix?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The contests of one judo weight category for any number of athletes, seeded in the standard way (the top seeds have the byes when the number is
|
|
14
|
+
* not a power of two). The athlete drawn first is white and the second blue (`Matchup.sides`: first white).
|
|
15
|
+
*
|
|
16
|
+
* With `repechage` (IJF Sport and Organisation Rules, 2.5.2 "Quarter-final Repechage", the system of the Olympic Games): the athletes are in two tables
|
|
17
|
+
* and an elimination produces two finalists. "The athletes defeated in the quarter-final will compete in two (2) repechage contests. The winners of each of
|
|
18
|
+
* these two (2) repechage contests will compete in bronze medal contests against the loser of the semi-final contest of the respective opposite table.
|
|
19
|
+
* The winners (2) of those contests are placed third." The repechage and bronze contests are `stage: "repechage"` and `stage: "bronze"` matchups, fed by
|
|
20
|
+
* the losers of the tree's, and are drawn under it. Needs eight athletes or more. With `direct`, there are no such contests: the two losing semi-finalists are
|
|
21
|
+
* the bronze medallists (`judoPodium`).
|
|
22
|
+
*
|
|
23
|
+
* Double repechage and full repechage (2.5.3 and 2.5.4) are not drawn: their pools are laid out in the rules' Appendix G charts.
|
|
24
|
+
*/
|
|
25
|
+
export declare function judoBracket(athletes: readonly Team[], options?: JudoBracketOptions): {
|
|
26
|
+
teams: Team[];
|
|
27
|
+
matchups: Matchup[];
|
|
28
|
+
};
|
|
29
|
+
/** The medallists of a judo tournament, as far as it has been fought. */
|
|
30
|
+
export interface JudoPodium {
|
|
31
|
+
gold: Team | null;
|
|
32
|
+
silver: Team | null;
|
|
33
|
+
/** The two bronze medallists: the winners of the bronze medal contests, or the losing semi-finalists of a direct knockout. Only those decided. */
|
|
34
|
+
bronze: Team[];
|
|
35
|
+
}
|
|
36
|
+
/** The gold, silver and two bronze medals of a bracket (matchups, input or layout), from the final, the bronze medal contests or the semi-finals. */
|
|
37
|
+
export declare function judoPodium(input: BracketLayout | readonly Matchup[] | BracketInput): JudoPodium;
|
|
38
|
+
export interface JudoCategory {
|
|
39
|
+
sex: "men" | "women";
|
|
40
|
+
/** `"-66"` (up to and including 66 kg), `"+100"` (over 100 kg): see `JUDO_WEIGHT_CLASSES`. */
|
|
41
|
+
weight: string;
|
|
42
|
+
}
|
|
43
|
+
/** A weight category as a heading in a language: "Men -66 kg", "Women +78 kg". */
|
|
44
|
+
export declare function judoCategoryLabel(category: JudoCategory, locale?: string): string;
|
package/dist/judo.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { layoutBracket } from "./layout.js";
|
|
2
|
+
import { singleElimination } from "./seeding.js";
|
|
3
|
+
import { fill, voiceOf } from "./locales.js";
|
|
4
|
+
export { JUDO_WEIGHT_CLASSES, judoOutcome, judoSeconds, judoClock } from "./judo-model.js";
|
|
5
|
+
/**
|
|
6
|
+
* The contests of one judo weight category for any number of athletes, seeded in the standard way (the top seeds have the byes when the number is
|
|
7
|
+
* not a power of two). The athlete drawn first is white and the second blue (`Matchup.sides`: first white).
|
|
8
|
+
*
|
|
9
|
+
* With `repechage` (IJF Sport and Organisation Rules, 2.5.2 "Quarter-final Repechage", the system of the Olympic Games): the athletes are in two tables
|
|
10
|
+
* and an elimination produces two finalists. "The athletes defeated in the quarter-final will compete in two (2) repechage contests. The winners of each of
|
|
11
|
+
* these two (2) repechage contests will compete in bronze medal contests against the loser of the semi-final contest of the respective opposite table.
|
|
12
|
+
* The winners (2) of those contests are placed third." The repechage and bronze contests are `stage: "repechage"` and `stage: "bronze"` matchups, fed by
|
|
13
|
+
* the losers of the tree's, and are drawn under it. Needs eight athletes or more. With `direct`, there are no such contests: the two losing semi-finalists are
|
|
14
|
+
* the bronze medallists (`judoPodium`).
|
|
15
|
+
*
|
|
16
|
+
* Double repechage and full repechage (2.5.3 and 2.5.4) are not drawn: their pools are laid out in the rules' Appendix G charts.
|
|
17
|
+
*/
|
|
18
|
+
export function judoBracket(athletes, options = {}) {
|
|
19
|
+
const system = options.system ?? (athletes.length >= 8 ? "repechage" : "direct");
|
|
20
|
+
if (system !== "repechage" && system !== "direct")
|
|
21
|
+
throw new RangeError(`taisen-brackets: a judo system is "repechage" or "direct", not ${JSON.stringify(system)}`);
|
|
22
|
+
if (athletes.length < 4)
|
|
23
|
+
throw new RangeError("taisen-brackets: a judo bracket needs four athletes or more (the two losing semi-finalists are bronze medallists)");
|
|
24
|
+
if (system === "repechage" && athletes.length < 8)
|
|
25
|
+
throw new RangeError("taisen-brackets: a quarter-final repechage needs eight athletes or more; use system: \"direct\" for fewer");
|
|
26
|
+
const prefix = options.prefix ?? "J";
|
|
27
|
+
const made = singleElimination(athletes, { prefix });
|
|
28
|
+
if (system === "direct")
|
|
29
|
+
return made;
|
|
30
|
+
const rounds = Math.max(...made.matchups.map((m) => m.round ?? 0));
|
|
31
|
+
const semis = made.matchups.filter((m) => m.round === rounds - 1);
|
|
32
|
+
const quarters = (semi) => {
|
|
33
|
+
const [a, b] = semi.feeds ?? [];
|
|
34
|
+
if (typeof a !== "string" || typeof b !== "string")
|
|
35
|
+
throw new Error("taisen-brackets: a semi-final of a repechage bracket needs two quarter-finals feeding it");
|
|
36
|
+
return [a, b];
|
|
37
|
+
};
|
|
38
|
+
const loser = (tie) => ({ tie, result: "loser" });
|
|
39
|
+
const extra = [];
|
|
40
|
+
semis.forEach((semi, k) => {
|
|
41
|
+
const [q1, q2] = quarters(semi);
|
|
42
|
+
extra.push({ id: `${prefix}rep-${k + 1}`, sides: [null, null], feeds: [loser(q1), loser(q2)], stage: "repechage" });
|
|
43
|
+
});
|
|
44
|
+
semis.forEach((semi, k) => {
|
|
45
|
+
// The repechage winner of one table meets the semi-final loser of the other.
|
|
46
|
+
const other = semis[1 - k];
|
|
47
|
+
extra.push({ id: `${prefix}bronze-${k + 1}`, sides: [null, null], feeds: [`${prefix}rep-${k + 1}`, loser(other.id)], stage: "bronze" });
|
|
48
|
+
});
|
|
49
|
+
return { teams: made.teams, matchups: [...made.matchups, ...extra] };
|
|
50
|
+
}
|
|
51
|
+
/** The gold, silver and two bronze medals of a bracket (matchups, input or layout), from the final, the bronze medal contests or the semi-finals. */
|
|
52
|
+
export function judoPodium(input) {
|
|
53
|
+
const layout = "ties" in input && "sides" in input ? input : layoutBracket(input);
|
|
54
|
+
const fin = layout.finalIndex === null ? null : layout.ties[layout.finalIndex];
|
|
55
|
+
const gold = fin && fin.winner !== null ? fin.sides[fin.winner] : null;
|
|
56
|
+
const silver = fin && fin.winner !== null ? fin.sides[(1 - fin.winner)] : null;
|
|
57
|
+
const bronzeTies = layout.ties.filter((t) => t.stage === "bronze");
|
|
58
|
+
let bronze;
|
|
59
|
+
if (bronzeTies.length)
|
|
60
|
+
bronze = bronzeTies.flatMap((t) => (t.winner === null || !t.sides[t.winner] ? [] : [t.sides[t.winner]]));
|
|
61
|
+
else {
|
|
62
|
+
const semis = layout.finalIndex === null ? [] : layout.ties.filter((t) => t.round === layout.rounds - 1 && t.stage === undefined && !t.third && layout.place[t.index] !== "o");
|
|
63
|
+
bronze = semis.flatMap((t) => (t.winner === null || !t.sides[(1 - t.winner)] ? [] : [t.sides[(1 - t.winner)]]));
|
|
64
|
+
}
|
|
65
|
+
return { gold, silver, bronze };
|
|
66
|
+
}
|
|
67
|
+
/** A weight category as a heading in a language: "Men -66 kg", "Women +78 kg". */
|
|
68
|
+
export function judoCategoryLabel(category, locale) {
|
|
69
|
+
if (category.sex !== "men" && category.sex !== "women")
|
|
70
|
+
throw new RangeError(`taisen-brackets: a judo category's sex is "men" or "women", not ${JSON.stringify(category.sex)}`);
|
|
71
|
+
if (!/^[-+]\d{2,3}$/.test(category.weight))
|
|
72
|
+
throw new RangeError(`taisen-brackets: a judo weight is "-66" or "+100", not ${JSON.stringify(category.weight)}`);
|
|
73
|
+
const w = voiceOf({ locale }).words;
|
|
74
|
+
return fill(category.weight.startsWith("+") ? w.judoWeightOver : w.judoWeightClass, { sex: category.sex === "men" ? w.judoMen : w.judoWomen, w: category.weight.slice(1) });
|
|
75
|
+
}
|
package/dist/layout.js
CHANGED
|
@@ -17,8 +17,8 @@ export function layoutBracket(input, options = {}) {
|
|
|
17
17
|
throw new Error(`taisen-brackets: two matchups have the id ${JSON.stringify(t.id)}`);
|
|
18
18
|
byId.set(t.id, t.index);
|
|
19
19
|
}
|
|
20
|
-
// A play-in game is outside the tree; so are a third-place match and a round-robin game.
|
|
21
|
-
const playIns = ties.filter((t) => matchups[t.index].stage
|
|
20
|
+
// A play-in game is outside the tree, and so are a judo repechage and bronze medal contest (a stage's ties are fed by the tree's losers); so are a third-place match and a round-robin game.
|
|
21
|
+
const playIns = ties.filter((t) => matchups[t.index].stage !== undefined).map((t) => t.index);
|
|
22
22
|
const isPlayIn = new Set(playIns);
|
|
23
23
|
const thirds = ties.filter((t) => t.third && !isPlayIn.has(t.index)).map((t) => t.index);
|
|
24
24
|
const robin = ties.filter((t) => !t.third && !isPlayIn.has(t.index) && matchups[t.index].round === 0).map((t) => t.index);
|
package/dist/layouts.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { abbrOf } from "./matchup.js";
|
|
2
|
+
import { sideScore } from "./judo-text.js";
|
|
2
3
|
import { drawMark } from "./marks.js";
|
|
3
4
|
import { emWidth, esc, fit, num } from "./text.js";
|
|
4
5
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
5
6
|
/** The width of a text set in bold at a size: the package's generous estimate, so that a text stays inside its place in any font. */
|
|
6
7
|
const bw = (text, size) => emWidth(text) * size;
|
|
7
|
-
const scoreOf = (e, k) => (e.scoresOn
|
|
8
|
+
const scoreOf = (e, k) => (e.scoresOn ? sideScore(e.tie, k, e.v) : "");
|
|
8
9
|
const pensOf = (e, k) => (e.scoresOn && e.tie.result.pens ? `(${e.v.number(e.tie.result.pens[k])})` : "");
|
|
9
10
|
/** A small pill with a seed tag, its left edge at `x`, centred on `cy`. */
|
|
10
11
|
function tagPill(text, x, cy, size) {
|
|
@@ -260,7 +261,7 @@ export function seriesLineWidth(tie, v, size, scoresOn) {
|
|
|
260
261
|
const score = scoresOn && tie.result.score ? fillScore(tie, v) : v.words.vs;
|
|
261
262
|
return bw(code(0), size) + bw(code(1), size) + bw(score, size) + 4 * size * 0.6 + 8 * 2 + 2 * bw("00", size * 0.72);
|
|
262
263
|
}
|
|
263
|
-
const fillScore = (tie, v) => (tie.result.score ? v.words.scoreText.replace("{a}",
|
|
264
|
+
const fillScore = (tie, v, swap = false) => (tie.result.score ? v.words.scoreText.replace("{a}", sideScore(tie, swap ? 1 : 0, v)).replace("{b}", sideScore(tie, swap ? 0 : 1, v)) : "");
|
|
264
265
|
/** One line a series: "(1) BOS 4–0 MIA (8)", the winner in bold and the loser quiet. Where the box is too narrow for a line, two compact rows. */
|
|
265
266
|
function series(e) {
|
|
266
267
|
const { x, y, w, h, tie, v } = e;
|
|
@@ -274,7 +275,7 @@ function series(e) {
|
|
|
274
275
|
const [a, b] = e.order;
|
|
275
276
|
const cy = y + h / 2;
|
|
276
277
|
const score = e.scoresOn && tie.result.score ? fillScore(tie, v) : v.words.vs;
|
|
277
|
-
const scoreOrdered = e.scoresOn && tie.result.score && a === 1 ?
|
|
278
|
+
const scoreOrdered = e.scoresOn && tie.result.score && a === 1 ? fillScore(tie, v, true) : score;
|
|
278
279
|
const sw = bw(scoreOrdered, size);
|
|
279
280
|
const cx = x + w / 2;
|
|
280
281
|
const ss = Math.max(size * 0.72, 7.5);
|
package/dist/locales.js
CHANGED
|
@@ -21,6 +21,11 @@ const EN = {
|
|
|
21
21
|
roadFinal: "The final: {a} {score} {b} ({how}).", roadChampion: "{name} won the final.", roadToPlay: "The final is still to be played.",
|
|
22
22
|
vs: "vs", at: "AT", groupName: "Group {n}", colPlayed: "P", colWon: "W", colDrawn: "D", colLost: "L", colDiff: "GD", colPoints: "Pts",
|
|
23
23
|
playIn: "Play-in", playInWinner: "Winner {a}/{b}", playInLoser: "Loser {a}/{b}", seedSlot: "Seed {n}", winnerOf: "Winner of {name}", loserOf: "Loser of {name}",
|
|
24
|
+
judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "ippon", judoWazaAriLong: "waza-ari", judoYukoLong: "yuko",
|
|
25
|
+
judoMethodIppon: "ippon", judoMethodAwasete: "waza-ari awasete ippon", judoMethodScore: "decision", judoMethodHansoku: "hansoku-make", judoMethodFusen: "fusen-gachi", judoMethodKiken: "kiken-gachi",
|
|
26
|
+
judoGolden: "GS", judoGoldenLong: "golden score", judoTime: "{t}", judoShidoOf: "{name}: {n} shido", judoShidoShort: "shido {a}–{b}",
|
|
27
|
+
judoRepechage: "Repechage", judoBronze: "Bronze medal contest", judoRepechageBronze: "Repechage and bronze medal contests", judoWhite: "white", judoBlue: "blue",
|
|
28
|
+
judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Men", judoWomen: "Women",
|
|
24
29
|
};
|
|
25
30
|
const FR = {
|
|
26
31
|
tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
|
|
@@ -38,6 +43,11 @@ const FR = {
|
|
|
38
43
|
roadFinal: "La finale : {a} {score} {b} ({how}).", roadChampion: "{name} a remporté la finale.", roadToPlay: "La finale reste à jouer.",
|
|
39
44
|
vs: "vs", at: "À", groupName: "Groupe {n}", colPlayed: "PJ", colWon: "V", colDrawn: "N", colLost: "D", colDiff: "+/-", colPoints: "Pts",
|
|
40
45
|
playIn: "Play-in", playInWinner: "Gagnant {a}/{b}", playInLoser: "Perdant {a}/{b}", seedSlot: "N°\u00a0{n}", winnerOf: "Gagnant du match {name}", loserOf: "Perdant du match {name}",
|
|
46
|
+
judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "ippon", judoWazaAriLong: "waza-ari", judoYukoLong: "yuko",
|
|
47
|
+
judoMethodIppon: "ippon", judoMethodAwasete: "waza-ari awasete ippon", judoMethodScore: "décision", judoMethodHansoku: "hansoku-make", judoMethodFusen: "fusen-gachi", judoMethodKiken: "kiken-gachi",
|
|
48
|
+
judoGolden: "GS", judoGoldenLong: "golden score", judoTime: "{t}", judoShidoOf: "{name}\u00a0: {n}\u00a0shido", judoShidoShort: "shido {a}–{b}",
|
|
49
|
+
judoRepechage: "Repêchage", judoBronze: "Combat pour la médaille de bronze", judoRepechageBronze: "Repêchages et combats pour la médaille de bronze", judoWhite: "blanc", judoBlue: "bleu",
|
|
50
|
+
judoWeightClass: "{sex} -{w}\u00a0kg", judoWeightOver: "{sex} +{w}\u00a0kg", judoMen: "Hommes", judoWomen: "Femmes",
|
|
41
51
|
};
|
|
42
52
|
const JA = {
|
|
43
53
|
tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
|
|
@@ -55,6 +65,11 @@ const JA = {
|
|
|
55
65
|
roadFinal: "決勝は{a} {score} {b}({how})でした。", roadChampion: "{name}が優勝しました。", roadToPlay: "決勝はまだ行われていません。",
|
|
56
66
|
vs: "対", at: "@", groupName: "グループ{n}", colPlayed: "試合", colWon: "勝", colDrawn: "分", colLost: "敗", colDiff: "得失差", colPoints: "勝点",
|
|
57
67
|
playIn: "プレイイン", playInWinner: "{a}/{b}の勝者", playInLoser: "{a}/{b}の敗者", seedSlot: "第{n}シード", winnerOf: "{name}の勝者", loserOf: "{name}の敗者",
|
|
68
|
+
judoIppon: "一", judoWazaAri: "技", judoYuko: "有", judoIpponLong: "一本", judoWazaAriLong: "技あり", judoYukoLong: "有効",
|
|
69
|
+
judoMethodIppon: "一本", judoMethodAwasete: "合わせ技一本", judoMethodScore: "優勢勝ち", judoMethodHansoku: "反則勝ち", judoMethodFusen: "不戦勝", judoMethodKiken: "棄権勝ち",
|
|
70
|
+
judoGolden: "GS", judoGoldenLong: "ゴールデンスコア", judoTime: "{t}", judoShidoOf: "{name}:指導{n}", judoShidoShort: "指導{a}–{b}",
|
|
71
|
+
judoRepechage: "敗者復活戦", judoBronze: "銅メダル決定戦", judoRepechageBronze: "敗者復活戦と銅メダル決定戦", judoWhite: "白", judoBlue: "青",
|
|
72
|
+
judoWeightClass: "{sex}{w}kg級", judoWeightOver: "{sex}{w}kg超級", judoMen: "男子", judoWomen: "女子",
|
|
58
73
|
};
|
|
59
74
|
const ES = {
|
|
60
75
|
tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
|
|
@@ -72,6 +87,11 @@ const ES = {
|
|
|
72
87
|
roadFinal: "La final: {a} {score} {b} ({how}).", roadChampion: "{name} ganó la final.", roadToPlay: "La final aún no se ha jugado.",
|
|
73
88
|
vs: "vs.", at: "EN", groupName: "Grupo {n}", colPlayed: "PJ", colWon: "G", colDrawn: "E", colLost: "P", colDiff: "DG", colPoints: "Pts",
|
|
74
89
|
playIn: "Play-in", playInWinner: "Ganador {a}/{b}", playInLoser: "Perdedor {a}/{b}", seedSlot: "Puesto {n}", winnerOf: "Ganador de {name}", loserOf: "Perdedor de {name}",
|
|
90
|
+
judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "ippon", judoWazaAriLong: "waza-ari", judoYukoLong: "yuko",
|
|
91
|
+
judoMethodIppon: "ippon", judoMethodAwasete: "waza-ari awasete ippon", judoMethodScore: "decisión", judoMethodHansoku: "hansoku-make", judoMethodFusen: "fusen-gachi", judoMethodKiken: "kiken-gachi",
|
|
92
|
+
judoGolden: "GS", judoGoldenLong: "golden score", judoTime: "{t}", judoShidoOf: "{name}: {n} shido", judoShidoShort: "shido {a}–{b}",
|
|
93
|
+
judoRepechage: "Repesca", judoBronze: "Combate por la medalla de bronce", judoRepechageBronze: "Repesca y combates por la medalla de bronce", judoWhite: "blanco", judoBlue: "azul",
|
|
94
|
+
judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Hombres", judoWomen: "Mujeres",
|
|
75
95
|
};
|
|
76
96
|
const DE = {
|
|
77
97
|
tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
|
|
@@ -89,6 +109,11 @@ const DE = {
|
|
|
89
109
|
roadFinal: "Das Finale: {a} {score} {b} ({how}).", roadChampion: "{name} gewann das Finale.", roadToPlay: "Das Finale steht noch aus.",
|
|
90
110
|
vs: "vs.", at: "BEI", groupName: "Gruppe {n}", colPlayed: "Sp", colWon: "S", colDrawn: "U", colLost: "N", colDiff: "Diff.", colPoints: "Pkt",
|
|
91
111
|
playIn: "Play-in", playInWinner: "Sieger {a}/{b}", playInLoser: "Verlierer {a}/{b}", seedSlot: "Platz {n}", winnerOf: "Sieger von {name}", loserOf: "Verlierer von {name}",
|
|
112
|
+
judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "Ippon", judoWazaAriLong: "Waza-ari", judoYukoLong: "Yuko",
|
|
113
|
+
judoMethodIppon: "Ippon", judoMethodAwasete: "Waza-ari awasete Ippon", judoMethodScore: "Punktsieg", judoMethodHansoku: "Hansoku-make", judoMethodFusen: "Fusen-gachi", judoMethodKiken: "Kiken-gachi",
|
|
114
|
+
judoGolden: "GS", judoGoldenLong: "Golden Score", judoTime: "{t}", judoShidoOf: "{name}: {n} Shido", judoShidoShort: "Shido {a}–{b}",
|
|
115
|
+
judoRepechage: "Hoffnungsrunde", judoBronze: "Kampf um Bronze", judoRepechageBronze: "Hoffnungsrunde und Kämpfe um Bronze", judoWhite: "weiß", judoBlue: "blau",
|
|
116
|
+
judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Herren", judoWomen: "Damen",
|
|
92
117
|
};
|
|
93
118
|
/**
|
|
94
119
|
* The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
|
package/dist/matchup.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { judoHasScore, judoOutcome } from "./judo-model.js";
|
|
2
|
+
import { judoNotes, sideScoreLong } from "./judo-text.js";
|
|
1
3
|
import { abbreviate } from "./text.js";
|
|
2
4
|
import { fill } from "./locales.js";
|
|
3
5
|
/** A bracket's teams: the list given, plus a team made from every id that a matchup names and the list does not. */
|
|
@@ -77,18 +79,31 @@ export function resolveMatchup(m, index, teams) {
|
|
|
77
79
|
if (ids[0] !== null && ids[0] === ids[1])
|
|
78
80
|
throw new Error(`taisen-brackets: matchup ${index} has ${JSON.stringify(ids[0])} on both sides`);
|
|
79
81
|
const kind = kindOf(m);
|
|
80
|
-
|
|
81
|
-
let winner = settled;
|
|
82
|
+
let named;
|
|
82
83
|
if (m.winner !== undefined) {
|
|
83
84
|
if (m.winner === null)
|
|
84
|
-
|
|
85
|
+
named = null;
|
|
85
86
|
else if (ids[0] === m.winner)
|
|
86
|
-
|
|
87
|
+
named = 0;
|
|
87
88
|
else if (ids[1] === m.winner)
|
|
88
|
-
|
|
89
|
+
named = 1;
|
|
89
90
|
else
|
|
90
91
|
throw new Error(`taisen-brackets: matchup ${index}'s winner ${JSON.stringify(m.winner)} is not one of its sides`);
|
|
91
92
|
}
|
|
93
|
+
let judo;
|
|
94
|
+
let result;
|
|
95
|
+
let winner;
|
|
96
|
+
if (m.judo) {
|
|
97
|
+
judo = judoOutcome(m.judo, named ?? null);
|
|
98
|
+
if (judo.winner !== null && named !== undefined && named !== null && named !== judo.winner)
|
|
99
|
+
throw new RangeError(`taisen-brackets: matchup ${index}'s judo scores make side ${judo.winner} the winner, but its winner is side ${named}`);
|
|
100
|
+
winner = judo.winner;
|
|
101
|
+
result = { kind: "game", score: judoHasScore(judo) || winner !== null ? judo.points : null, played: judoHasScore(judo) || winner !== null ? 1 : 0, bestOf: null, pens: null, extraTime: false, replays: 0, games: [judo.points], decided: winner !== null };
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
({ result, winner } = settle(m, kind));
|
|
105
|
+
if (named !== undefined)
|
|
106
|
+
winner = named;
|
|
92
107
|
const sides = ids.map((id) => (id === null ? null : teams.get(id) ?? null));
|
|
93
108
|
return {
|
|
94
109
|
index,
|
|
@@ -102,6 +117,8 @@ export function resolveMatchup(m, index, teams) {
|
|
|
102
117
|
third: !!m.third,
|
|
103
118
|
walkover: !!m.walkover,
|
|
104
119
|
note: m.note ?? null,
|
|
120
|
+
...(judo ? { judo } : {}),
|
|
121
|
+
...(m.stage ? { stage: m.stage } : {}),
|
|
105
122
|
...(m.seeds ? { seeds: m.seeds } : {}),
|
|
106
123
|
...(m.home !== undefined ? { home: m.home } : {}),
|
|
107
124
|
};
|
|
@@ -112,7 +129,16 @@ export const abbrOf = (team, v) => (team ? team.abbr ?? abbreviate(v.team(team))
|
|
|
112
129
|
export function noteParts(tie, v) {
|
|
113
130
|
const r = tie.result;
|
|
114
131
|
const w = v.words;
|
|
115
|
-
const out = [];
|
|
132
|
+
const out = tie.judo ? judoNotes(tie, v, false) : [];
|
|
133
|
+
if (tie.stage === "repechage" || tie.stage === "bronze")
|
|
134
|
+
out.unshift(tie.stage === "repechage" ? w.judoRepechage : w.judoBronze);
|
|
135
|
+
if (tie.judo) {
|
|
136
|
+
if (tie.walkover)
|
|
137
|
+
out.push(w.walkover);
|
|
138
|
+
if (tie.note)
|
|
139
|
+
out.push(tie.note);
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
116
142
|
if (r.kind === "series") {
|
|
117
143
|
if (r.decided && r.played > 0)
|
|
118
144
|
out.push(fill(w.inGames, { n: v.number(r.played) }));
|
|
@@ -139,7 +165,14 @@ export function noteParts(tie, v) {
|
|
|
139
165
|
export function noteLong(tie, v) {
|
|
140
166
|
const r = tie.result;
|
|
141
167
|
const w = v.words;
|
|
142
|
-
const out = [];
|
|
168
|
+
const out = tie.judo ? judoNotes(tie, v, true) : [];
|
|
169
|
+
if (tie.judo) {
|
|
170
|
+
if (tie.walkover)
|
|
171
|
+
out.push(w.walkover);
|
|
172
|
+
if (tie.note)
|
|
173
|
+
out.push(tie.note);
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
143
176
|
if (r.kind === "series") {
|
|
144
177
|
if (r.decided && r.played > 0)
|
|
145
178
|
out.push(fill(w.inGames, { n: v.number(r.played) }));
|
|
@@ -169,7 +202,7 @@ export function describeTie(tie, v) {
|
|
|
169
202
|
const [a, b] = tie.sides;
|
|
170
203
|
const r = tie.result;
|
|
171
204
|
let text = r.score
|
|
172
|
-
? fill(w.tieScore, { a: name(a), b: name(b), sa:
|
|
205
|
+
? fill(w.tieScore, { a: name(a), b: name(b), sa: sideScoreLong(tie, 0, v), sb: sideScoreLong(tie, 1, v) })
|
|
173
206
|
: fill(w.tieVs, { a: name(a), b: name(b) });
|
|
174
207
|
const notes = noteLong(tie, v);
|
|
175
208
|
if (notes.length)
|
package/dist/playin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fill } from "./locales.js";
|
|
2
2
|
import { abbrOf, describeTie } from "./matchup.js";
|
|
3
|
+
import { sideScore } from "./judo-text.js";
|
|
3
4
|
import { drawMark } from "./marks.js";
|
|
4
5
|
import { bold, headTile, headingTexts, titleHeight } from "./furniture.js";
|
|
5
6
|
import { esc, fit, num } from "./text.js";
|
|
@@ -147,7 +148,7 @@ export function drawPlayIn(layout, o, v, size, ctx, road, linked) {
|
|
|
147
148
|
const team = t.sides[k];
|
|
148
149
|
const ry = rect.y + k * rowH;
|
|
149
150
|
const cy = ry + rowH / 2;
|
|
150
|
-
const score = o.scores !== false
|
|
151
|
+
const score = o.scores !== false ? sideScore(t, k, v) : "";
|
|
151
152
|
const cls = t.winner === null ? "tb-pending" : t.winner === k ? "tb-won" : "tb-lost";
|
|
152
153
|
const items = [];
|
|
153
154
|
const slotX = rect.x + rect.w - 5 - 26;
|
package/dist/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { layoutBracket } from "./layout.js";
|
|
2
2
|
import { abbrOf, describeTie, noteParts } from "./matchup.js";
|
|
3
|
+
import { shidoCards, sideScore } from "./judo-text.js";
|
|
3
4
|
import { cleanSvg, drawEmblem, drawMark, hasTrophy as hasTrophyOf, isEmblemId, markDefs, safeHref, safeUrl } from "./marks.js";
|
|
4
5
|
import { roundLabel } from "./rounds.js";
|
|
5
6
|
import { styleFor } from "./theme.js";
|
|
@@ -115,7 +116,7 @@ export function drawBracket(input, options = {}) {
|
|
|
115
116
|
const orientation = options.orientation && options.orientation !== "auto" ? options.orientation : kind === "series" || sizeName === "phone" || sizeName === "card" || (size.height !== null && size.width < size.height * 0.8) ? "vertical" : "horizontal";
|
|
116
117
|
const road = options.look === "road";
|
|
117
118
|
const hasBg = typeof options.background === "string" || (typeof options.background === "object" && options.background !== null);
|
|
118
|
-
const prefix = options.id ?? `tb-${hash(JSON.stringify([v.locale.code, layout.teams.map((t) => t.name), layout.ties.map((t) => [t.id, t.sides.map((s) => s?.id ?? null), t.winner, t.result.score, t.round]), options.size, orientation, options.detail, theme, options.title, options.subtitle, options.seeds, options.scores, options.champion, options.markShape, options.roundLabels, options.thirdPlace, options.trophy ? 1 : 0, road ? 1 : 0, options.highlightPath ?? null, options.flags ? 1 : 0, ...(hasBg ? [options.background, options.vs, options.codes, options.seeds] : []), ...(options.flagHref ? ["h"] : []), ...(kind !== "tree" ? [kind, options.at, options.standings, options.qualify] : []), ...(options.headers ? ["head"] : [])]))}`;
|
|
119
|
+
const prefix = options.id ?? `tb-${hash(JSON.stringify([v.locale.code, layout.teams.map((t) => t.name), layout.ties.map((t) => [t.id, t.sides.map((s) => s?.id ?? null), t.winner, t.result.score, t.round, ...(t.judo ? [t.judo.method, t.judo.goldenScore, t.judo.seconds, t.judo.sides] : [])]), options.size, orientation, options.detail, theme, options.title, options.subtitle, options.seeds, options.scores, options.champion, options.markShape, options.roundLabels, options.thirdPlace, options.trophy ? 1 : 0, road ? 1 : 0, options.highlightPath ?? null, options.flags ? 1 : 0, ...(hasBg ? [options.background, options.vs, options.codes, options.seeds] : []), ...(options.flagHref ? ["h"] : []), ...(kind !== "tree" ? [kind, options.at, options.standings, options.qualify] : []), ...(options.headers ? ["head"] : [])]))}`;
|
|
119
120
|
const ctx = { radii: new Set(), symbols: new Map(), prefix, shape: options.markShape ?? "round", look: road ? "road" : "classic", flags: options.flags, flagHref: options.flagHref, theme: theme === "light" ? "light" : theme === "dark" || theme === "poster" ? "dark" : "inherit", flagCache: new Map(), defs: new Map() };
|
|
120
121
|
const path = pathOf(layout, options.highlightPath);
|
|
121
122
|
const linked = { count: 0 };
|
|
@@ -124,10 +125,10 @@ export function drawBracket(input, options = {}) {
|
|
|
124
125
|
// The group tables and the play-in are not the tree: they plan and draw their own parts, and share the frame (the style, the background, the title) with it.
|
|
125
126
|
const own = kind === "groups" ? drawGroups(layout, options, v, size, ctx, road, linked) : kind === "playin" ? drawPlayIn(layout, options, v, size, ctx, road, linked) : null;
|
|
126
127
|
if (own)
|
|
127
|
-
return frame({ prefix, v, options, theme, road, kind, title, desc: own.desc ?? desc, width: own.width, height: own.height, followed: false, links: [], parts: [...own.body, ...own.texts], linkCount: linked.count, ctx });
|
|
128
|
+
return frame({ prefix, v, options, theme, road, kind, title, desc: own.desc ?? desc, width: own.width, height: own.height, followed: false, links: [], parts: [...own.body, ...own.texts], linkCount: linked.count, ctx, judo: layout.ties.some((t) => t.judo !== undefined) });
|
|
128
129
|
const plan = planBracket(layout, options, v, size, orientation === "vertical", ctx, road, path, kind);
|
|
129
130
|
const drawn = plan.boxes.map((b) => drawBox(b, plan, options, v, ctx, linked, layout.place[b.tie.index])); // draws the marks, so before the defs are written
|
|
130
|
-
return frame({ prefix, v, options, theme, road, kind, title, desc, width: plan.width, height: plan.height, followed: path.size > 0, links: plan.links, parts: [...drawn, ...plan.texts, ...plan.extras], linkCount: linked.count, ctx });
|
|
131
|
+
return frame({ prefix, v, options, theme, road, kind, title, desc, width: plan.width, height: plan.height, followed: path.size > 0, links: plan.links, parts: [...drawn, ...plan.texts, ...plan.extras], linkCount: linked.count, ctx, judo: layout.ties.some((t) => t.judo !== undefined) });
|
|
131
132
|
}
|
|
132
133
|
/** The drawing's frame: the root `<svg>` with its title, its style, the defs the marks wrote and the background, round the parts a layout drew. */
|
|
133
134
|
function frame(f) {
|
|
@@ -135,7 +136,7 @@ function frame(f) {
|
|
|
135
136
|
const picture = resolveBackground(options.background, theme, f.width, f.height, prefix);
|
|
136
137
|
const defs = markDefs(ctx) + (picture?.defs ?? "");
|
|
137
138
|
const scope = scopeOf(options, prefix);
|
|
138
|
-
const style = theme === "none" || options.style === false ? "" : `<style>${styleFor(`.${scope}`, theme, { road, links: f.linkCount > 0, follow: f.followed, bgAuto: picture?.auto, layout: f.kind !== "tree" || options.headers === true }, picture?.inks)}</style>`;
|
|
139
|
+
const style = theme === "none" || options.style === false ? "" : `<style>${styleFor(`.${scope}`, theme, { road, links: f.linkCount > 0, follow: f.followed, bgAuto: picture?.auto, layout: f.kind !== "tree" || options.headers === true, judo: f.judo }, picture?.inks)}</style>`;
|
|
139
140
|
const bg = options.background === false ? "" : `<rect class="tb-bg" width="${num(f.width)}" height="${num(f.height)}"${theme === "none" ? ' fill="none"' : ""}/>${picture?.body ?? ""}`;
|
|
140
141
|
return [
|
|
141
142
|
`<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(v.locale.code)}" class="taisen ${prefix}${scope !== prefix ? ` ${scope}` : ""}${road ? " tb-look-road" : ""}${f.followed ? " tb-follow" : ""}${options.className ? ` ${esc(options.className)}` : ""}" viewBox="0 0 ${num(f.width)} ${num(f.height)}" width="${num(f.width)}" height="${num(f.height)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${prefix}-t ${prefix}-d">`,
|
|
@@ -168,7 +169,7 @@ export function bracketStyle(options) {
|
|
|
168
169
|
if (theme === "none")
|
|
169
170
|
return "";
|
|
170
171
|
const picture = resolveBackground(options.background, theme, 100, 100, "x");
|
|
171
|
-
return styleFor(`.${scope}`, theme, { road: options.look === "road", links: typeof options.href === "function", follow: options.highlightPath !== undefined, bgAuto: picture?.auto, layout: (options.layout ?? "tree") !== "tree" || options.headers === true }, picture?.inks);
|
|
172
|
+
return styleFor(`.${scope}`, theme, { road: options.look === "road", links: typeof options.href === "function", follow: options.highlightPath !== undefined, bgAuto: picture?.auto, layout: (options.layout ?? "tree") !== "tree" || options.headers === true, judo: options.judo === true }, picture?.inks);
|
|
172
173
|
}
|
|
173
174
|
/** The groups' names with the east first, for the series card: the names the teams carry, ordered so that a name with "east" comes before one with "west". */
|
|
174
175
|
function eastFirst(input) {
|
|
@@ -181,6 +182,8 @@ function eastFirst(input) {
|
|
|
181
182
|
}
|
|
182
183
|
// ---------------------------------------------------------------------------------------------- the plan
|
|
183
184
|
function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
185
|
+
// The ties kept apart below the tree are a little taller in the road look when any of them has a note to write (a judo contest's method and time), so that the note has its line.
|
|
186
|
+
const OH = road && [...layout.other, ...layout.robin, ...layout.playIn].some((i) => noteParts(layout.ties[i], v).length > 0) ? 66 : OTHER_H;
|
|
184
187
|
// The layouts that draw each tie their own way take the tree's places and lines, and fill the boxes themselves.
|
|
185
188
|
const V = VARIANTS.includes(kind) ? kind : null;
|
|
186
189
|
const pad = vertical ? 14 : 22;
|
|
@@ -238,7 +241,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
238
241
|
const top = pad + titleH + (hasGroups ? groupRoom : 0) + labelRoom;
|
|
239
242
|
others = othersOf(layout);
|
|
240
243
|
ow = clamp(D * 1.1, 120, 200);
|
|
241
|
-
extraH0 = othersHeight(others.length, W0, pad, ow,
|
|
244
|
+
extraH0 = othersHeight(others.length, W0, pad, ow, OH) + thirdBlock;
|
|
242
245
|
const bodyH = size.height === null ? rows * 60 : size.height - top - pad - extraH0;
|
|
243
246
|
rowStep = bodyH / rows;
|
|
244
247
|
S = clamp(rowStep - (road ? 7 : 9), 24, road ? 132 : 104);
|
|
@@ -264,6 +267,8 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
264
267
|
const tagW = tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 8))) + 8 : 0;
|
|
265
268
|
// The road look's marks and scores are large: they are scaled down together until a row fits its box, before a name is dropped for its abbreviation.
|
|
266
269
|
const anyNotes = layout.ties.some((t) => noteParts(t, v).length > 0);
|
|
270
|
+
// The widest score a row may carry: two digits, or a judo contest's marks ("W2 Y3").
|
|
271
|
+
const probe = layout.ties.reduce((best, t) => [0, 1].reduce((b, k) => { const x = sideScore(t, k, v); return wide(x, 10) > wide(b, 10) ? x : b; }, best), "00");
|
|
267
272
|
// The road look's boxes of marks alone carry each score under its mark, unless marks alone were asked for.
|
|
268
273
|
const markScores = road && o.detail !== "mark" && (o.scores ?? true) && layout.ties.some((t) => t.result.score !== null);
|
|
269
274
|
const fitOf = (w, h, named) => {
|
|
@@ -273,7 +278,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
273
278
|
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
274
279
|
const extra = (anyBye ? wide(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? wide("(00)", Math.max(score * 0.55, 8)) + 5 : 0);
|
|
275
280
|
const fixed = clamp(rowH * 0.16, 5, 12) + clamp(rowH * 0.18, 5, 10) + 8 + 10 + (tagW > 0 ? tagW + 3 : 0);
|
|
276
|
-
const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + wide(
|
|
281
|
+
const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + wide(probe, score) + extra;
|
|
277
282
|
return Math.min(1, (w - fixed) / varying);
|
|
278
283
|
};
|
|
279
284
|
let fitNow = 1;
|
|
@@ -284,7 +289,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
284
289
|
const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
|
|
285
290
|
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
286
291
|
const extra = (anyBye ? boldWidth(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? boldWidth("(00)", Math.max(font - 2, 8)) + 3 : 0);
|
|
287
|
-
const needed = 6 + md + 5 + longest * (font / 13) + 8 + boldWidth(
|
|
292
|
+
const needed = 6 + md + 5 + longest * (font / 13) + 8 + boldWidth(probe, score) + extra + 14;
|
|
288
293
|
const named = o.detail && o.detail !== "auto" ? o.detail : w >= needed && h >= 36 ? "full" : w >= 76 && h >= 28 ? "compact" : "mark";
|
|
289
294
|
return named === "mark" && o.markShape === "none" ? "compact" : named;
|
|
290
295
|
};
|
|
@@ -389,7 +394,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
389
394
|
dl = pick(wl, hl);
|
|
390
395
|
const strays = othersOf(layout);
|
|
391
396
|
const ows = Math.min(availW, 200);
|
|
392
|
-
const strayH = othersHeight(strays.length, W0, pad, ows,
|
|
397
|
+
const strayH = othersHeight(strays.length, W0, pad, ows, OH);
|
|
393
398
|
const labelH = showLabels ? 22 : 8;
|
|
394
399
|
const content = (champ ? champH + 14 : 0) + labelH + hl + (withThird ? 8 + 18 + hl + 8 : 0) + strayH;
|
|
395
400
|
const top0 = pad + titleH;
|
|
@@ -410,7 +415,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
410
415
|
y += 8 + 18 + hl + 8;
|
|
411
416
|
}
|
|
412
417
|
if (strays.length > 0)
|
|
413
|
-
othersBlock(layout, strays, y + 6, W0, pad, ows,
|
|
418
|
+
othersBlock(layout, strays, y + 6, W0, pad, ows, OH, v, road, boxes, texts);
|
|
414
419
|
texts.push(...headings(v, o, W0, pad, poster));
|
|
415
420
|
return finish({ width: W0, height, boxes, links: [], texts, extras, detail: dl, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
|
|
416
421
|
}
|
|
@@ -518,7 +523,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
518
523
|
}
|
|
519
524
|
if (bottomThird)
|
|
520
525
|
thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H, v, road, boxes, texts);
|
|
521
|
-
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow,
|
|
526
|
+
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OH, v, road, boxes, texts);
|
|
522
527
|
texts.push(...headings(v, o, width, pad, poster));
|
|
523
528
|
return finish({ width, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
|
|
524
529
|
}
|
|
@@ -531,7 +536,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
531
536
|
const bandCount = halves ? 2 * bandsPerSide : nc + (champ ? 1 : 0);
|
|
532
537
|
others = othersOf(layout);
|
|
533
538
|
ow = Math.min(W0 - 2 * pad, 150);
|
|
534
|
-
const othersV = othersHeight(others.length, W0, pad, ow,
|
|
539
|
+
const othersV = othersHeight(others.length, W0, pad, ow, OH);
|
|
535
540
|
// The height the page needs for boxes `h` high with `gap` between the rounds, and what it keeps for the final's band and the third-place match.
|
|
536
541
|
const needOf = (h, gap) => {
|
|
537
542
|
const finalHere = V === "columns" ? Math.max(h, finalH) : detail === "mark" ? Math.max(h, 52) : h;
|
|
@@ -664,7 +669,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
664
669
|
}
|
|
665
670
|
if (bottomThird)
|
|
666
671
|
thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, road ? Math.max(boxW, 200) : 200), thirdH2, v, road, boxes, texts);
|
|
667
|
-
othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow,
|
|
672
|
+
othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow, OH, v, road, boxes, texts);
|
|
668
673
|
texts.push(...headings(v, o, W0, pad, poster));
|
|
669
674
|
return finish({ width: W0, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
|
|
670
675
|
}
|
|
@@ -868,7 +873,10 @@ function othersBlock(layout, list, y0, width, pad, ow, oh, v, road, boxes, texts
|
|
|
868
873
|
return;
|
|
869
874
|
const cols = Math.max(1, Math.floor((width - 2 * pad + 10) / (ow + 10)));
|
|
870
875
|
const x0 = pad + (width - 2 * pad - (Math.min(cols, list.length) * (ow + 10) - 10)) / 2;
|
|
871
|
-
|
|
876
|
+
const stages = new Set(list.map((i) => layout.ties[i].stage ?? "other"));
|
|
877
|
+
const w = v.words;
|
|
878
|
+
const heading = stages.size === 1 && stages.has("repechage") ? w.judoRepechage : stages.size === 1 && stages.has("bronze") ? w.judoBronze : [...stages].every((x) => x === "repechage" || x === "bronze") ? w.judoRepechageBronze : w.other;
|
|
879
|
+
texts.push(label(v, road, heading, width / 2, y0 + 12, "middle"));
|
|
872
880
|
list.forEach((i, n) => {
|
|
873
881
|
const rect = { x: x0 + (n % cols) * (ow + 10), y: y0 + 24 + Math.floor(n / cols) * (oh + 10), w: ow, h: oh };
|
|
874
882
|
boxes.push({ rect, tie: layout.ties[i], order: [0, 1], bye: null, byes: [], final: false, champion: false });
|
|
@@ -932,7 +940,7 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
932
940
|
const withScore = plan.markScores && scoresOn && r.score !== null;
|
|
933
941
|
const tags = [0, 1].map((k) => (plan.tags ? tagOf(tie, k) : ""));
|
|
934
942
|
const tagH = tags.some((t) => t !== "") ? 12 : 0;
|
|
935
|
-
const scoreOf = (k) => (withScore
|
|
943
|
+
const scoreOf = (k) => (withScore ? sideScore(tie, k, v) : "");
|
|
936
944
|
const pensOf = (k) => (withScore && r.pens ? `(${v.number(r.pens[k])})` : "");
|
|
937
945
|
const hasPens = withScore && r.pens !== null;
|
|
938
946
|
const lay = (m) => {
|
|
@@ -1028,7 +1036,13 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
1028
1036
|
rowParts.push(`<rect class="tb-tagbox" x="${num(tx)}" y="${num(cy - (tf + 4) / 2)}" width="${num(tw)}" height="${num(tf + 4)}" rx="${num((tf + 4) / 2)}"/><text class="tb-tag" x="${num(tx + tw / 2)}" y="${num(cy + tf * 0.35)}" font-size="${num(tf)}" text-anchor="middle">${esc(tag)}</text>`);
|
|
1029
1037
|
tx += tw + 3;
|
|
1030
1038
|
}
|
|
1031
|
-
const score = scoresOn
|
|
1039
|
+
const score = scoresOn ? sideScore(tie, k, v) : "";
|
|
1040
|
+
// A judo contest: a bar at the row's edge in the colour of the side (the first is white, the second blue) and its shido cards beside the score.
|
|
1041
|
+
if (tie.judo)
|
|
1042
|
+
rowParts.push(`<rect class="${k === 0 ? "tb-jw" : "tb-jb"}" x="${num(x + 1.5)}" y="${num(cy - rowH * 0.3)}" width="3" height="${num(rowH * 0.6)}" rx="1.5"/>`);
|
|
1043
|
+
const cards = tie.judo && scoresOn ? shidoCards(tie, k) : [];
|
|
1044
|
+
const cardSize = Math.max(5, Math.min(9, rowH * 0.3));
|
|
1045
|
+
const cardsW = cards.length ? cards.length * (cardSize * 0.72 + 2) + 4 : 0;
|
|
1032
1046
|
const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
|
|
1033
1047
|
const bye = p.byes.includes(k);
|
|
1034
1048
|
const right = x + w - padX;
|
|
@@ -1040,7 +1054,8 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
1040
1054
|
const byeFits = bye && right - tx - (scoreW + pensW) - (mw(v.words.bye, small2) + 6) - 6 >= mw(`${seed}${nm}`, font);
|
|
1041
1055
|
const byeText = byeFits ? v.words.bye : "";
|
|
1042
1056
|
const byeW = byeText ? mw(byeText, small2) : 0;
|
|
1043
|
-
const
|
|
1057
|
+
const cardsOn = cardsW > 0 && right - tx - (scoreW + pensW) - cardsW - 6 >= 36;
|
|
1058
|
+
const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6 - (cardsOn ? cardsW : 0);
|
|
1044
1059
|
// A name that does not fit is set a little smaller before it is cut.
|
|
1045
1060
|
let nameFont = font;
|
|
1046
1061
|
while (nameFont > 9.5 && mw(`${seed}${nm}`, nameFont) > avail)
|
|
@@ -1049,6 +1064,10 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
1049
1064
|
rowParts.push(`<text class="tb-name${team ? "" : " tb-muted"}" x="${num(tx)}" y="${num(cy + nameFont * 0.35)}" font-size="${num(nameFont)}">${esc(text)}</text>`);
|
|
1050
1065
|
if (score)
|
|
1051
1066
|
rowParts.push(`<text class="tb-score" x="${num(right)}" y="${num(cy + scoreFont * 0.35)}" font-size="${num(scoreFont)}" text-anchor="end" font-variant-numeric="tabular-nums">${esc(score)}</text>`);
|
|
1067
|
+
if (cardsOn) {
|
|
1068
|
+
const cw = cardSize * 0.72;
|
|
1069
|
+
cards.forEach((c, n) => rowParts.push(`<rect class="${c === "r" ? "tb-sh-r" : "tb-sh-y"}" x="${num(right - scoreW - pensW - cardsW + 2 + n * (cw + 2))}" y="${num(cy - cardSize / 2)}" width="${num(cw)}" height="${num(cardSize)}" rx="1"/>`));
|
|
1070
|
+
}
|
|
1052
1071
|
if (pensText)
|
|
1053
1072
|
rowParts.push(`<text class="tb-muted" x="${num(right - scoreW - (road ? 4 : 3))}" y="${num(cy + small * 0.35)}" font-size="${num(small)}" text-anchor="end">${esc(pensText)}</text>`);
|
|
1054
1073
|
if (byeText)
|
|
@@ -1059,7 +1078,7 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
1059
1078
|
if (noteH) {
|
|
1060
1079
|
const f = road ? 9 : 9.5;
|
|
1061
1080
|
const line = road ? v.upper(notes.join(" · ")) : notes.join(" · ");
|
|
1062
|
-
parts.push(`<text class="tb-muted${road ? " tb-note" : ""}" x="${num(x + w / 2)}" y="${num(y + h - (road ? 5 : 4))}" font-size="${f}"${road ? ' font-weight="700" letter-spacing=".05em"' : ""} text-anchor="middle">${esc(fit(line, f * (road ? 1.1 : 1), w - 8))}</text>`);
|
|
1081
|
+
parts.push(`<text class="tb-muted${road ? " tb-note" : ""}" x="${num(x + w / 2)}" y="${num(y + h - (road ? 5 : 4))}" font-size="${f}"${road ? ' font-weight="700" letter-spacing=".05em"' : ""} text-anchor="middle">${esc(fit(line, f * (road ? 1.1 : 1), w - 8, boldWidth))}</text>`);
|
|
1063
1082
|
}
|
|
1064
1083
|
}
|
|
1065
1084
|
const extra = tieClassOf(o, tie);
|
package/dist/table.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { layoutBracket } from "./layout.js";
|
|
2
2
|
import { noteLong } from "./matchup.js";
|
|
3
|
+
import { sideScore } from "./judo-text.js";
|
|
3
4
|
import { fill, voiceOf } from "./locales.js";
|
|
4
5
|
import { localizedLayout } from "./render.js";
|
|
5
6
|
import { roundLabel } from "./rounds.js";
|
|
@@ -23,12 +24,12 @@ export function bracketTable(input, options = {}) {
|
|
|
23
24
|
const text = team ? esc(team.name) : esc(w.tbd);
|
|
24
25
|
return t.winner === k ? `<strong>${text}</strong>` : text;
|
|
25
26
|
};
|
|
26
|
-
const score = (t) => (t.result.score ? fill(w.scoreText, { a:
|
|
27
|
+
const score = (t) => (t.result.score ? fill(w.scoreText, { a: sideScore(t, 0, v), b: sideScore(t, 1, v) }) : "");
|
|
27
28
|
// Where a side comes from, in words, when the tie says so: "Loser of A v B".
|
|
28
29
|
const named = (i) => fill(w.tieVs, { a: layout.ties[i].sides[0]?.name ?? w.tbd, b: layout.ties[i].sides[1]?.name ?? w.tbd });
|
|
29
30
|
const origins = (t) => (t.from ?? []).flatMap((s) => (s ? [fill(s.result === "loser" ? w.loserOf : w.winnerOf, { name: named(s.tie) })] : []));
|
|
30
31
|
const rows = order.map((t) => {
|
|
31
|
-
const roundText = t.third ? w.thirdPlace : t.playIn ? w.playIn : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
|
|
32
|
+
const roundText = t.third ? w.thirdPlace : t.stage === "repechage" ? w.judoRepechage : t.stage === "bronze" ? w.judoBronze : t.playIn ? w.playIn : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
|
|
32
33
|
const date = dated ? `<td>${esc(v.date(t.date) ?? "")}</td>` : "";
|
|
33
34
|
return `<tr><th scope="row">${esc(roundText)}</th>${date}<td>${name(t, 0)}</td><td>${esc(score(t))}</td><td>${name(t, 1)}</td><td>${esc([...noteLong(t, v), ...origins(t)].join(w.noteJoin))}</td></tr>`;
|
|
34
35
|
});
|
package/dist/theme.d.ts
CHANGED
|
@@ -36,7 +36,16 @@ export interface StyleParts {
|
|
|
36
36
|
bgAuto?: boolean;
|
|
37
37
|
/** The rules of the layouts of their own (slots, shields, conference colours, header tiles, arrows, group cards). */
|
|
38
38
|
layout?: boolean;
|
|
39
|
+
/** A judo bracket: the white and blue side bars and the shido cards (`--tb-judo-white`, `--tb-judo-blue`, `--tb-shido`, `--tb-hansoku`). */
|
|
40
|
+
judo?: boolean;
|
|
39
41
|
}
|
|
42
|
+
/** The colours of a judo contest: the white and the blue judogi, the yellow shido card and the red hansoku-make card. */
|
|
43
|
+
export declare const JUDO_COLORS: Readonly<{
|
|
44
|
+
white: "#ffffff";
|
|
45
|
+
blue: "#1f4fa8";
|
|
46
|
+
shido: "#f0c000";
|
|
47
|
+
hansoku: "#d3302a";
|
|
48
|
+
}>;
|
|
40
49
|
/** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
|
|
41
50
|
export declare const CONFERENCE_COLORS: readonly [string, string];
|
|
42
51
|
/** The colours a background says to draw in (`backgroundSvg`'s `ink`): what `styleFor` takes in place of a look's own palette. */
|
package/dist/theme.js
CHANGED
|
@@ -10,6 +10,8 @@ export const ROAD_THEMES = Object.freeze({
|
|
|
10
10
|
poster: { ring: "#f6f1e3", edge: "rgba(0,0,0,.5)", panel: "#1b2766", spine: "#d9b24c" },
|
|
11
11
|
});
|
|
12
12
|
const FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Hiragino Sans", "Yu Gothic UI", "Noto Sans CJK JP", Meiryo, sans-serif';
|
|
13
|
+
/** The colours of a judo contest: the white and the blue judogi, the yellow shido card and the red hansoku-make card. */
|
|
14
|
+
export const JUDO_COLORS = Object.freeze({ white: "#ffffff", blue: "#1f4fa8", shido: "#f0c000", hansoku: "#d3302a" });
|
|
13
15
|
/** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
|
|
14
16
|
export const CONFERENCE_COLORS = Object.freeze(["#2457c5", "#b3261e"]);
|
|
15
17
|
/** A palette and a road palette from a background's ink. */
|
|
@@ -59,6 +61,9 @@ function rules(p, r, poster, withFont, parts, side) {
|
|
|
59
61
|
const [ca, cb] = CONFERENCE_COLORS;
|
|
60
62
|
list.push([[".tb-slot"], `fill:${v("plain")};fill-opacity:.55;stroke:${v("edge")};stroke-width:1`], [[".tb-won .tb-slot"], `fill-opacity:.9;stroke:${v("win")};stroke-width:1.6`], [[".tb-lost .tb-slot"], "fill-opacity:.35"], [[".tb-emptybar"], `fill:${v("plain")};opacity:.8`], [[".tb-seed"], "font-variant-numeric:tabular-nums"], [[".tb-seedn"], `fill:${v("muted")};opacity:.85`], [[".tb-shield"], `fill:none;stroke:${v("edge")};stroke-width:1.4`], [[".tb-won .tb-shield"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-shield.tb-empty"], `fill:${v("plain")};fill-opacity:.5`], [[".tb-vs", ".tb-code"], `fill:${v("muted")};font-weight:700`], [[".tb-c0 .tb-box"], `stroke:var(--tb-conf-a, ${ca});stroke-width:1.6`], [[".tb-c1 .tb-box"], `stroke:var(--tb-conf-b, ${cb});stroke-width:1.6`], [[".tb-atag"], `fill:${v("ink")}`], [[".tb-atag-t"], `fill:${v("bg")}`], [[".tb-c0 .tb-atag"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-c1 .tb-atag"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-c0 .tb-atag-t", ".tb-c1 .tb-atag-t"], "fill:#fff"], [[".tb-head"], `fill:${rv("panel", "panel")}`], [[".tb-head.tb-c0"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-head.tb-c1"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-head-t"], `fill:${v("ink")};font-weight:800`], [[".tb-head.tb-c0 + .tb-head-t", ".tb-head.tb-c1 + .tb-head-t"], "fill:#fff"], [[".tb-arrow"], `stroke:${v("line")};stroke-width:1.6;fill:none;stroke-linecap:round;stroke-linejoin:round`], [[".tb-arrow-loser"], "stroke-dasharray:5 4"], [[".tb-arrowhead"], `fill:${v("line")};stroke:none`], [[".tb-gcard"], `fill:${v("box")};stroke:${v("edge")};stroke-width:1`], [[".tb-gtile"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-gtile-t"], "fill:#fff;font-weight:800"], [[".tb-ghead"], `fill:${v("muted")};font-weight:700`], [[".tb-gcell"], `fill:${v("ink")};font-variant-numeric:tabular-nums`], [[".tb-gcell-p"], "font-weight:800"], [[".tb-gname"], `fill:${v("ink")};font-weight:700`], [[".tb-qual"], `fill:${v("win")};fill-opacity:.16`], [[".tb-gsep"], `stroke:${v("edge")};stroke-width:1;stroke-opacity:.6`]);
|
|
61
63
|
}
|
|
64
|
+
if (parts.judo === true) {
|
|
65
|
+
list.push([[".tb-jw"], `fill:var(--tb-judo-white, ${JUDO_COLORS.white});stroke:${v("edge")};stroke-width:.8`], [[".tb-jb"], `fill:var(--tb-judo-blue, ${JUDO_COLORS.blue});stroke:${v("edge")};stroke-width:.8`], [[".tb-sh-y"], `fill:var(--tb-shido, ${JUDO_COLORS.shido});stroke:rgba(0,0,0,.35);stroke-width:.6`], [[".tb-sh-r"], `fill:var(--tb-hansoku, ${JUDO_COLORS.hansoku});stroke:rgba(0,0,0,.35);stroke-width:.6`]);
|
|
66
|
+
}
|
|
62
67
|
if (parts.follow === true) {
|
|
63
68
|
list.push([[".tb-tie"], "opacity:.4"], [[".tb-tie.tb-on-path"], "opacity:1"], [[".tb-on-path .tb-box"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-line"], "stroke-opacity:.25"], [[".tb-line.tb-path"], `stroke:${v("win")};stroke-width:4.5;stroke-opacity:1;stroke-linecap:round;stroke-dasharray:.1 9`]);
|
|
64
69
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* The types of Taisen: what goes in (teams and matchups), what the layout works out (ties, rounds, halves) and what
|
|
3
3
|
* the drawing takes as options. Nothing here runs; the functions are in `matchup.ts`, `layout.ts` and `render.ts`.
|
|
4
4
|
*/
|
|
5
|
+
import type { JudoOutcome, JudoResult } from "./judo-model.ts";
|
|
5
6
|
/** A side's key: a team code, a franchise id, anything that is unique within one bracket. */
|
|
6
7
|
export type SideId = string | number;
|
|
7
8
|
/** A side of a tie: a team, with the marks and labels a drawing needs. Only `id` and `name` are required. */
|
|
@@ -106,8 +107,13 @@ export interface Matchup {
|
|
|
106
107
|
walkover?: boolean;
|
|
107
108
|
/** This is the third-place match: it leads nowhere and is drawn apart. */
|
|
108
109
|
third?: boolean;
|
|
109
|
-
/**
|
|
110
|
-
|
|
110
|
+
/**
|
|
111
|
+
* `"play-in"`: a game of a play-in (the NBA's) that is outside the rounds of the tree: its winner (and loser, see `feeds`) goes on to a place the tree's ties name. Drawn by the `playin` layout and, apart, under the other layouts' trees.
|
|
112
|
+
* `"repechage"` and `"bronze"`: a judo repechage contest and a bronze medal contest, fed by the losers of the tree's ties (`judoBracket` writes them); drawn apart under the tree.
|
|
113
|
+
*/
|
|
114
|
+
stage?: "play-in" | "repechage" | "bronze";
|
|
115
|
+
/** A judo contest's scores: ippon, waza-ari, yuko, shido, hansoku-make, golden score and time. The first side is white, the second blue. The score, the winner and the notes come from it; see `judoOutcome`. */
|
|
116
|
+
judo?: JudoResult;
|
|
111
117
|
/** The side playing at home (0 or 1), for the `at` wording of the `columns` layout: the home side is drawn second, after "AT". Left out, the second side. */
|
|
112
118
|
home?: 0 | 1;
|
|
113
119
|
note?: string;
|
|
@@ -151,8 +157,12 @@ export interface Tie {
|
|
|
151
157
|
seeds?: readonly [string | number | null, string | number | null];
|
|
152
158
|
/** Where each side comes from, when the matchup's `feeds` name it: another tie's winner or its loser. Left out for a tie that says nothing of the kind. */
|
|
153
159
|
from?: readonly [Source | null, Source | null];
|
|
154
|
-
/** The tie is a play-in game (`Matchup.stage`). */
|
|
160
|
+
/** The tie is a play-in game, a judo repechage contest or a bronze medal contest (`Matchup.stage`). */
|
|
155
161
|
playIn?: boolean;
|
|
162
|
+
/** The tie's `Matchup.stage`, when it has one. */
|
|
163
|
+
stage?: "play-in" | "repechage" | "bronze";
|
|
164
|
+
/** A judo contest's result, checked and worked out (`Matchup.judo`). */
|
|
165
|
+
judo?: JudoOutcome;
|
|
156
166
|
/** The side at home (`Matchup.home`), when given. */
|
|
157
167
|
home?: 0 | 1;
|
|
158
168
|
}
|
|
@@ -342,6 +352,39 @@ export interface Words {
|
|
|
342
352
|
/** `{name}`: a side that is the winner or loser of another tie, in a sentence or a table: "Loser of {name}". */
|
|
343
353
|
winnerOf: string;
|
|
344
354
|
loserOf: string;
|
|
355
|
+
/** Judo. The marks of the scores on a box (ippon, waza-ari, yuko), the same written out, and how a contest was won. */
|
|
356
|
+
judoIppon: string;
|
|
357
|
+
judoWazaAri: string;
|
|
358
|
+
judoYuko: string;
|
|
359
|
+
judoIpponLong: string;
|
|
360
|
+
judoWazaAriLong: string;
|
|
361
|
+
judoYukoLong: string;
|
|
362
|
+
judoMethodIppon: string;
|
|
363
|
+
judoMethodAwasete: string;
|
|
364
|
+
judoMethodScore: string;
|
|
365
|
+
judoMethodHansoku: string;
|
|
366
|
+
judoMethodFusen: string;
|
|
367
|
+
judoMethodKiken: string;
|
|
368
|
+
/** Golden score, short and in a sentence. */
|
|
369
|
+
judoGolden: string;
|
|
370
|
+
judoGoldenLong: string;
|
|
371
|
+
/** `{t}`: the contest time ("3:42"). */
|
|
372
|
+
judoTime: string;
|
|
373
|
+
/** `{name}`, `{n}`: a side's shido ("Kenji Nakamura: 2 shido"). */
|
|
374
|
+
judoShidoOf: string;
|
|
375
|
+
/** `{a}`, `{b}`: the sides' shido in a short note. */
|
|
376
|
+
judoShidoShort: string;
|
|
377
|
+
/** The headings over the repechage contests and the bronze medal contests, alone and together; the sides' colours. */
|
|
378
|
+
judoRepechage: string;
|
|
379
|
+
judoBronze: string;
|
|
380
|
+
judoRepechageBronze: string;
|
|
381
|
+
judoWhite: string;
|
|
382
|
+
judoBlue: string;
|
|
383
|
+
/** `{sex}`, `{w}` (the number of kilograms, no sign): a weight category up to and including that weight ("Men -66 kg"), one over it ("Women +78 kg"), and the two sexes' words. */
|
|
384
|
+
judoWeightClass: string;
|
|
385
|
+
judoWeightOver: string;
|
|
386
|
+
judoMen: string;
|
|
387
|
+
judoWomen: string;
|
|
345
388
|
}
|
|
346
389
|
/** A language: its words, the names of the rounds (by their English names), and the tag `Intl` formats numbers and dates for. */
|
|
347
390
|
export interface Locale {
|
|
@@ -352,6 +395,7 @@ export interface Locale {
|
|
|
352
395
|
/** Round names by their English name: `{ "Round of 16": "Huitièmes de finale" }`. A round with no entry keeps its name (or `roundOf`). */
|
|
353
396
|
rounds: Readonly<Record<string, string>>;
|
|
354
397
|
}
|
|
398
|
+
export type { JudoMethod, JudoOutcome, JudoResult, JudoSide } from "./judo-model.ts";
|
|
355
399
|
export type ThemeName = "auto" | "light" | "dark" | "poster" | "none";
|
|
356
400
|
export type SizeName = "wide" | "square" | "phone" | "card";
|
|
357
401
|
export interface Size {
|
|
@@ -434,6 +478,8 @@ export interface DrawOptions extends LayoutOptions {
|
|
|
434
478
|
standings?: "points" | "record";
|
|
435
479
|
/** In the `groups` layout, how many teams of each group go through: their rows are marked. Default 0, none. */
|
|
436
480
|
qualify?: number;
|
|
481
|
+
/** For `bracketStyle`: include the judo rules (the white and blue side bars, the shido cards). `drawBracket` adds them by itself when a tie has judo scores. */
|
|
482
|
+
judo?: boolean;
|
|
437
483
|
/** Write the `<style>` into the drawing. Default true; false leaves it out, for a second drawing of a page that shares the first's (`styleScope`, `bracketStyle`). */
|
|
438
484
|
style?: boolean;
|
|
439
485
|
/** A class that carries this drawing's rules in place of its own: two drawings with the same `styleScope` take their colours from one `<style>`, and the second can leave it out (`style: false`). A word of letters, digits, `-` and `_`. */
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.7.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.7.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/taisen-brackets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|