@johnmorrisdotca/taisen-brackets 0.15.0 → 0.17.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 +22 -0
- package/README.md +3 -3
- package/dist/bout-board.d.ts +52 -0
- package/dist/bout-board.js +194 -0
- package/dist/broadcast-kit.js +1 -0
- package/dist/broadcast-medals.d.ts +23 -0
- package/dist/broadcast-medals.js +109 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/locales.js +5 -0
- package/dist/medals.d.ts +7 -0
- package/dist/medals.js +6 -5
- package/dist/types.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,28 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.17.0] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
Broadcast boards, step 3: the medal table.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`broadcastMedalTable({ title?, subtitle?, date?, source?, rows }, options)`**: the medal table in the manner of a television graphic, as one standalone SVG: a title band (the heading, with the date at its right), a sub bar with the three medal discs over their columns and the word total, and a row for each country or team with its rank, a 4:3 flag, its code or name (`labels`) and the gold, silver and bronze counts each beside its disc, and the total. The same source as `medalTable`, ranked and tied by the same rules (`sort`, `ties`, `top`, `skipEmpty`), so `tallyMedals` feeds it; `ranks: false`. Looks, icons, languages and `--bc-*` custom properties as for the other boards; up to 24 rows.
|
|
16
|
+
- **`broadcastMedalSize(rowCount, { width })`**: the size before it is drawn: the width (1280 by default, 720 to 4000) and a height of 64 + 36 + 10 + 48 a row + 10, scaled.
|
|
17
|
+
- The three boards now have a section on placing them over something: the size is read before drawing, nothing is drawn outside the panel, and the SVG scales as a picture. [docs/broadcast.md](docs/broadcast.md).
|
|
18
|
+
- The demo's Broadcast boards section has the medal table, with the look, icon and width in the address.
|
|
19
|
+
|
|
20
|
+
## [0.16.0] - 2026-10-20
|
|
21
|
+
|
|
22
|
+
Broadcast boards, step 2: the bout presentation board.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`boutBoard({ title, phase?, bout?, red, blue, officials? }, options)`**: the screen shown as two athletes are about to compete, as one standalone SVG: a title band (the event, with the phase at its right), a bar "Bout presentation" with the bout's number ("Bout: 142"), the two athletes in a red band and a blue one (code, 4:3 flag, name), a bar "Referee and judges", and a row for each official (code, flag, name, role). A role is `"referee"`, `"judge"` (numbered in the order given, in the language's words) or a text of your own. Looks, icons, `nameCase`, `nameStyle`, languages and `--bc-*` custom properties as for the results board; up to 12 officials. `describeBoutBoard`.
|
|
27
|
+
- **`boutBoardSize(officialCount, { width })`**: the size before it is drawn, for placing it: the width (1280 by default, 720 to 4000) and a height of 64 + 38 + 10 + 62 + 10 + 38 + 42 an official + 10, scaled.
|
|
28
|
+
- Words for the board in the five languages: the bout, the two bars, the referee and a judge (the French and Japanese were read by native-language reviewers; the Spanish and German were not).
|
|
29
|
+
- The demo's Broadcast boards section has the bout presentation (a made-up wrestling bout with a referee and five judges), in the three looks. [docs/broadcast.md](docs/broadcast.md).
|
|
30
|
+
|
|
9
31
|
## [0.15.0] - 2026-10-20
|
|
10
32
|
|
|
11
33
|
Broadcast boards, step 1: the results board.
|
package/README.md
CHANGED
|
@@ -266,7 +266,7 @@ A judo contest is a tie with a `judo` result: ippon, waza-ari, yuko and shido as
|
|
|
266
266
|
|
|
267
267
|
## Broadcast boards
|
|
268
268
|
|
|
269
|
-
`resultsBoard` draws a results board in the manner of a television graphic: a title band with an icon, a sub bar, and a row for each athlete with a rank in a box, the country code, a 4:3 flag, the name, a record badge (`OR`, `WR`, `NR`) and one or more columns of scores or times at the sport's own precision (a gymnastics final, an all-around, a swimming final, athletics, speed skating). It is one standalone SVG of a width you choose and a height that follows from the rows (`resultsBoardSize`), made to be placed over a picture, in three looks and with no mark of any games or broadcaster. [docs/broadcast.md](./docs/broadcast.md) · [try it in the demo](https://johnmorrisdotca.github.io/taisen-brackets/#boards)
|
|
269
|
+
`resultsBoard` draws a results board in the manner of a television graphic: a title band with an icon, a sub bar, and a row for each athlete with a rank in a box, the country code, a 4:3 flag, the name, a record badge (`OR`, `WR`, `NR`) and one or more columns of scores or times at the sport's own precision (a gymnastics final, an all-around, a swimming final, athletics, speed skating). It is one standalone SVG of a width you choose and a height that follows from the rows (`resultsBoardSize`), made to be placed over a picture, in three looks and with no mark of any games or broadcaster. `boutBoard` draws the bout presentation: the two athletes in a red and a blue band, the referee and the judges. `broadcastMedalTable` draws the medal table with a disc for each medal. [docs/broadcast.md](./docs/broadcast.md) · [try it in the demo](https://johnmorrisdotca.github.io/taisen-brackets/#boards)
|
|
270
270
|
|
|
271
271
|
<p align="center"><picture>
|
|
272
272
|
<img alt="A results board in the blue look: a gymnastics apparatus final with a title band, a sub bar with three score columns, and eight rows with a red rank box, country code, flag, name and scores." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/results-apparatus-blue.webp" width="640">
|
|
@@ -322,7 +322,7 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
|
|
|
322
322
|
- **Judo screens for the hall**: `judoContestCard` (the category, the round, the white athlete over the blue one) and `judoMatQueue` (per mat, a big number and colour and the contests to come in order, with flags and codes), on a TV or a tablet, in five languages.
|
|
323
323
|
- **Rankings**: `rankingList` draws a ranking from your data, text first: rank, flag (a country or a Canadian province), name, club, points, movement arrows, a podium, a champion row, ties and paging; `rankingNumberOnes` the number one of each category and `rankingEntryList` an event's entry list with each athlete's ranking and `rankingMatchup` a head-to-head card ([docs/rankings.md](./docs/rankings.md)).
|
|
324
324
|
- **A medal table for any sport**: `medalTable` ranks countries or teams gold first or by the total, with shared ranks, flags or badges, filters by sport, weight class and Games through `tallyMedals`, and former countries as rows of their own ([docs/medals.md](./docs/medals.md)).
|
|
325
|
-
- **Broadcast boards**: `resultsBoard` draws a results graphic (title band, rank boxes, 4:3 flags, record badges, columns of scores) as a standalone SVG that follows from its rows, to place over a picture, in three looks and with no mark of any games ([docs/broadcast.md](./docs/broadcast.md)).
|
|
325
|
+
- **Broadcast boards**: `resultsBoard` draws a results graphic (title band, rank boxes, 4:3 flags, record badges, columns of scores) and `boutBoard` the bout presentation (the two athletes, the referee and the judges) and `broadcastMedalTable` the medal table as a standalone SVG that follows from its rows, to place over a picture, in three looks and with no mark of any games ([docs/broadcast.md](./docs/broadcast.md)).
|
|
326
326
|
- **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)).
|
|
327
327
|
- **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
|
|
328
328
|
- **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.
|
|
@@ -686,7 +686,7 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
|
|
|
686
686
|
- `judoOutcome(result, winner?)`, `judoBracket(athletes, options?)`, `judoPodium(input)`, `judoCategoryLabel(category, locale?)`, `JUDO_WEIGHT_CLASSES`, `judoSeconds(time)`, `judoClock(seconds)`: judo results checked against the IJF rules, the bracket with its repechage and two bronze medal contests, the medallists, a weight category as a heading ([docs/judo.md](./docs/judo.md)). `judoScoreboard(contest, options?)`, `describeJudoContest(contest, options?)`, `JUDO_BOARD_SIZES`: the scoreboard of a contest as SVG and as a sentence. `judoContestCard(contest, options?)`, `judoMatQueue(mats, options?)`, `describeJudoCard`, `describeJudoQueue`, `JUDO_MAT_COLORS`: the contest card and the mat queue board as SVG and as sentences. `judoWins(bracket, athlete, options?)`, `judoRoadPanel(source, options?)`, `describeJudoRoad`: an athlete's wins on the road to the final, and the panel. `Matchup.judo` and `Matchup.stage` (`"repechage"`, `"bronze"`) carry them into `drawBracket`.
|
|
687
687
|
- `rankingList(source, options?)`, `rankingRanks(entries, options?)`, `rankingCategoryLabel(category, locale?)`, `describeRanking(source, options?)`, `BOARD_SIZES`: a ranking list as SVG, its ranks as data, a category as a heading and the drawing as a sentence. `rankingNumberOnes(source, options?)`, `describeNumberOnes`, `NUMBER_ONE_COLORS`: the number one of each category as cards. `rankingEntryList(source, options?)`, `describeEntryList`: an event's entry list with each athlete's ranking. `rankingMatchup(source, options?)`, `describeMatchup`, `matchupRecord(meetings)`: the head-to-head card of two athletes and their meetings ([docs/rankings.md](./docs/rankings.md)).
|
|
688
688
|
- `medalTable(source, options?)`, `medalRanks(rows, options?)`, `tallyMedals(awards, filter?)`, `describeMedalTable(source, options?)`: a medal table as SVG, its ranks as data, a list of medals added up and filtered, and the table as a sentence ([docs/medals.md](./docs/medals.md)).
|
|
689
|
-
- `resultsBoard(source, options?)`, `resultsBoardSize(rowCount, options?)`, `describeResultsBoard(source, options?)`, `BROADCAST_LOOKS`: a broadcast-style results board as a standalone SVG, its size from its rows, and the board as a sentence ([docs/broadcast.md](./docs/broadcast.md)).
|
|
689
|
+
- `resultsBoard(source, options?)`, `resultsBoardSize(rowCount, options?)`, `describeResultsBoard(source, options?)`, `BROADCAST_LOOKS`: a broadcast-style results board as a standalone SVG, its size from its rows, and the board as a sentence. `boutBoard(source, options?)`, `boutBoardSize(officialCount, options?)`, `describeBoutBoard(source, options?)`: the bout presentation board, its size and its description. `broadcastMedalTable(source, options?)`, `broadcastMedalSize(rowCount, options?)`: the medal table in the broadcast style and its size ([docs/broadcast.md](./docs/broadcast.md)).
|
|
690
690
|
- `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.
|
|
691
691
|
- `isEmblemId(value)`, and the types `EmblemRenderer`, `EmblemRequest`, `EmblemStyle`: the trophy emblems (see "Emblems for the trophy").
|
|
692
692
|
- `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bout presentation board: the screen shown as two athletes are about to compete. A title band with the event and the phase, a bar that says "Bout presentation" with the bout's number, the
|
|
3
|
+
* two athletes (a red band and a blue one, each with a code, a 4:3 flag and a name), and a bar that says "Referee and judges" over a row for each official with the country code, flag, name and role.
|
|
4
|
+
* One standalone SVG of a width you choose and a height that follows from the officials (`boutBoardSize`), made to be placed over a picture. Nothing of any games or broadcaster is drawn.
|
|
5
|
+
*/
|
|
6
|
+
import { type BroadcastOptions } from "./broadcast-kit.ts";
|
|
7
|
+
import { type RankingName, type RankingOptions } from "./ranking.ts";
|
|
8
|
+
/** One of the two athletes of a bout. Only `name` is needed. */
|
|
9
|
+
export interface BoutAthlete {
|
|
10
|
+
/** The name as it is to be written (`MAYER M.`), or its two parts (set by `nameStyle`). */
|
|
11
|
+
name: string | RankingName;
|
|
12
|
+
/** The country's three letters. */
|
|
13
|
+
code?: string;
|
|
14
|
+
/** A code for the `flags` function, drawn 4:3. */
|
|
15
|
+
flag?: string;
|
|
16
|
+
/** A mark of your own in place of a flag. */
|
|
17
|
+
mark?: string;
|
|
18
|
+
}
|
|
19
|
+
/** A referee or a judge. Only `name` is needed. */
|
|
20
|
+
export interface BoutOfficial extends BoutAthlete {
|
|
21
|
+
/** `referee` (the language's word), `judge` (numbered 1, 2, 3 in the order given, in the language's words), or any text you want written (`Chair`, `Mat president`). Default `judge`. */
|
|
22
|
+
role?: "referee" | "judge" | (string & {});
|
|
23
|
+
}
|
|
24
|
+
export interface BoutBoardSource {
|
|
25
|
+
/** The event, in capitals in the title band ("Women's 57-60 kg"). */
|
|
26
|
+
title: string;
|
|
27
|
+
/** The phase, at the right of the title band ("Preliminaries"). */
|
|
28
|
+
phase?: string;
|
|
29
|
+
/** The bout's number (`142`, `"A-12"`), written "Bout: 142" at the right of the first bar. */
|
|
30
|
+
bout?: string | number;
|
|
31
|
+
/** The athlete in the red band, at the left, and the one in the blue band, at the right. */
|
|
32
|
+
red: BoutAthlete;
|
|
33
|
+
blue: BoutAthlete;
|
|
34
|
+
/** The referee and the judges, in the order they are written. */
|
|
35
|
+
officials?: readonly BoutOfficial[];
|
|
36
|
+
}
|
|
37
|
+
export interface BoutBoardOptions extends BroadcastOptions, Pick<RankingOptions, "nameStyle"> {
|
|
38
|
+
/** `upper` writes the names in capitals; `as-is` (the default) leaves them as given. */
|
|
39
|
+
nameCase?: "as-is" | "upper";
|
|
40
|
+
}
|
|
41
|
+
/** The width and the height of a bout board with `officialCount` officials: the width given (1280 by default) and a height of 64 + 38 + 10 + 62 + 10 + 38 + 42 an official + 10, in units of the width over 1280. */
|
|
42
|
+
export declare function boutBoardSize(officialCount: number, options?: Pick<BroadcastOptions, "width">): {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
};
|
|
46
|
+
/** The board as a sentence, the drawing's description. */
|
|
47
|
+
export declare function describeBoutBoard(source: BoutBoardSource, options?: Pick<BoutBoardOptions, "locale" | "words" | "nameStyle" | "nameCase">): string;
|
|
48
|
+
/**
|
|
49
|
+
* The bout presentation board as a standalone SVG. Throws a `RangeError` for a board with no title, an athlete or official with no name, more than 12 officials, a role that is not a text, an unknown option
|
|
50
|
+
* value, or a width outside 720 to 4000.
|
|
51
|
+
*/
|
|
52
|
+
export declare function boutBoard(source: BoutBoardSource, options?: BoutBoardOptions): string;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bout presentation board: the screen shown as two athletes are about to compete. A title band with the event and the phase, a bar that says "Bout presentation" with the bout's number, the
|
|
3
|
+
* two athletes (a red band and a blue one, each with a code, a 4:3 flag and a name), and a bar that says "Referee and judges" over a row for each official with the country code, flag, name and role.
|
|
4
|
+
* One standalone SVG of a width you choose and a height that follows from the officials (`boutBoardSize`), made to be placed over a picture. Nothing of any games or broadcaster is drawn.
|
|
5
|
+
*/
|
|
6
|
+
import { BC, box, broadcastFrame, broadcastWrap, bw, fitSize, gloss, panel, t, titleBand } from "./broadcast-kit.js";
|
|
7
|
+
import { fill, voiceOf } from "./locales.js";
|
|
8
|
+
import { drawMark } from "./marks.js";
|
|
9
|
+
import { nameText } from "./ranking.js";
|
|
10
|
+
import { abbreviate, fit } from "./text.js";
|
|
11
|
+
const MAX_OFFICIALS = 12;
|
|
12
|
+
/** The width and the height of a bout board with `officialCount` officials: the width given (1280 by default) and a height of 64 + 38 + 10 + 62 + 10 + 38 + 42 an official + 10, in units of the width over 1280. */
|
|
13
|
+
export function boutBoardSize(officialCount, options = {}) {
|
|
14
|
+
const width = options.width ?? 1280;
|
|
15
|
+
const u = width / 1280;
|
|
16
|
+
return { width, height: Math.round((BC.title + BC.band + BC.pad + BC.athlete + BC.pad + BC.band + (officialCount > 0 ? officialCount * (BC.official + BC.gap) : 0) + BC.pad) * u) };
|
|
17
|
+
}
|
|
18
|
+
function nameOf(n) {
|
|
19
|
+
return typeof n === "string" ? n : n.surname;
|
|
20
|
+
}
|
|
21
|
+
function validate(source, options) {
|
|
22
|
+
if (!source || typeof source.title !== "string" || source.title.trim() === "")
|
|
23
|
+
throw new RangeError("taisen-brackets: a bout board needs a title");
|
|
24
|
+
const who = (a, side) => {
|
|
25
|
+
const n = a?.name;
|
|
26
|
+
const ok = typeof n === "string" ? n.trim() !== "" : !!n && typeof n === "object" && typeof n.surname === "string" && n.surname.trim() !== "";
|
|
27
|
+
if (!ok)
|
|
28
|
+
throw new RangeError(`taisen-brackets: the ${side} needs a name`);
|
|
29
|
+
};
|
|
30
|
+
who(source.red, "red athlete");
|
|
31
|
+
who(source.blue, "blue athlete");
|
|
32
|
+
const officials = source.officials ?? [];
|
|
33
|
+
if (!Array.isArray(officials))
|
|
34
|
+
throw new RangeError("taisen-brackets: officials is a list");
|
|
35
|
+
if (officials.length > MAX_OFFICIALS)
|
|
36
|
+
throw new RangeError(`taisen-brackets: a bout board has at most ${MAX_OFFICIALS} officials`);
|
|
37
|
+
officials.forEach((o, i) => { who(o, `official ${i + 1}`); if (o.role !== undefined && (typeof o.role !== "string" || o.role.trim() === ""))
|
|
38
|
+
throw new RangeError(`taisen-brackets: the role of official ${i + 1} is a text`); });
|
|
39
|
+
if (source.bout !== undefined && (typeof source.bout === "number" ? !Number.isFinite(source.bout) : typeof source.bout !== "string"))
|
|
40
|
+
throw new RangeError("taisen-brackets: the bout number is a number or a text");
|
|
41
|
+
if (options.nameCase !== undefined && !["as-is", "upper"].includes(options.nameCase))
|
|
42
|
+
throw new RangeError(`taisen-brackets: nameCase is "as-is" or "upper", not ${JSON.stringify(options.nameCase)}`);
|
|
43
|
+
if (options.nameStyle !== undefined && !["surname-first", "given-first", "as-is"].includes(options.nameStyle))
|
|
44
|
+
throw new RangeError(`taisen-brackets: nameStyle is "surname-first", "given-first" or "as-is", not ${JSON.stringify(options.nameStyle)}`);
|
|
45
|
+
}
|
|
46
|
+
/** The role of each official as it is written: the language's words for a referee and a judge (numbered in order), any other text as it is. */
|
|
47
|
+
function roles(officials, v) {
|
|
48
|
+
let judges = 0;
|
|
49
|
+
return officials.map((o) => {
|
|
50
|
+
const role = o.role ?? "judge";
|
|
51
|
+
if (role === "referee")
|
|
52
|
+
return v.words.rbReferee;
|
|
53
|
+
if (role === "judge") {
|
|
54
|
+
judges += 1;
|
|
55
|
+
return fill(v.words.rbJudge, { n: v.number(judges) });
|
|
56
|
+
}
|
|
57
|
+
return role;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** The board as a sentence, the drawing's description. */
|
|
61
|
+
export function describeBoutBoard(source, options = {}) {
|
|
62
|
+
const v = voiceOf(options);
|
|
63
|
+
const w = v.words;
|
|
64
|
+
const style = options.nameStyle ?? "surname-first";
|
|
65
|
+
const name = (a) => { const n = nameText(a.name, style, v); return `${options.nameCase === "upper" ? v.upper(n) : n}${a.code ? ` (${a.code})` : ""}`; };
|
|
66
|
+
const parts = [source.title];
|
|
67
|
+
if (source.phase)
|
|
68
|
+
parts.push(source.phase);
|
|
69
|
+
parts.push(`${w.rbBoutPresentation}${source.bout !== undefined ? `: ${fill(w.rbBout, { n: String(source.bout) })}` : ""}`);
|
|
70
|
+
parts.push(fill(w.tieVs, { a: name(source.red), b: name(source.blue) }));
|
|
71
|
+
const officials = source.officials ?? [];
|
|
72
|
+
if (officials.length) {
|
|
73
|
+
const rs = roles(officials, v);
|
|
74
|
+
parts.push(`${w.rbOfficials}: ${officials.map((o, i) => `${rs[i]} ${name(o)}`).join(w.noteJoin)}`);
|
|
75
|
+
}
|
|
76
|
+
return parts.join(w.sep) + w.end;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The bout presentation board as a standalone SVG. Throws a `RangeError` for a board with no title, an athlete or official with no name, more than 12 officials, a role that is not a text, an unknown option
|
|
80
|
+
* value, or a width outside 720 to 4000.
|
|
81
|
+
*/
|
|
82
|
+
export function boutBoard(source, options = {}) {
|
|
83
|
+
validate(source, options);
|
|
84
|
+
if (options.width !== undefined && options.width < 720)
|
|
85
|
+
throw new RangeError("taisen-brackets: a board's width is from 720 to 4000");
|
|
86
|
+
const officials = source.officials ?? [];
|
|
87
|
+
const size = boutBoardSize(officials.length, options);
|
|
88
|
+
const f = broadcastFrame(options, [source, options.nameStyle, options.nameCase], size.height / (size.width / 1280));
|
|
89
|
+
const { W, u, v } = f;
|
|
90
|
+
const w = v.words;
|
|
91
|
+
const style = options.nameStyle ?? "surname-first";
|
|
92
|
+
const written = (a) => { const n = nameText(a.name, style, v); return options.nameCase === "upper" ? v.upper(n) : n; };
|
|
93
|
+
const body = [panel(f)];
|
|
94
|
+
const title = titleBand(f, 0, source.title, source.phase);
|
|
95
|
+
body.push(title.svg);
|
|
96
|
+
let y = title.h;
|
|
97
|
+
// The first bar: "Bout presentation" and the number.
|
|
98
|
+
body.push(bar(f, y, v.upper(w.rbBoutPresentation), source.bout !== undefined ? v.upper(fill(w.rbBout, { n: String(source.bout) })) : ""));
|
|
99
|
+
y += BC.band * u + BC.pad * u;
|
|
100
|
+
// The two athletes: a red band and a blue one with a dash between.
|
|
101
|
+
const ah = BC.athlete * u;
|
|
102
|
+
const gapMid = 44 * u;
|
|
103
|
+
const pad = 12 * u;
|
|
104
|
+
const half = (W - pad * 2 - gapMid) / 2;
|
|
105
|
+
const sides = [[source.red, "bc-red", pad], [source.blue, "bc-blue", pad + half + gapMid]];
|
|
106
|
+
const flagH = 34 * u, flagW = flagH * (4 / 3);
|
|
107
|
+
sides.forEach(([a, cls, x0], k) => {
|
|
108
|
+
const g = [box(x0, y, half, ah, cls, 4 * u), gloss(f, x0, y, half, ah)];
|
|
109
|
+
const cy = y + ah / 2;
|
|
110
|
+
const name = written(a);
|
|
111
|
+
const cs = 28 * u;
|
|
112
|
+
const codeW = a.code ? bw(a.code, cs) + 12 * u : 0;
|
|
113
|
+
const inner = 16 * u;
|
|
114
|
+
const mark = a.flag || a.mark ? drawMark({ id: `a${k}`, name: nameOf(a.name), abbr: (a.code ?? abbreviate(nameOf(a.name))).slice(0, 3), ...(a.flag ? { flag: a.flag } : {}), ...(a.mark ? { mark: a.mark } : {}) }, 0, cy, flagH, f.ctx) : "";
|
|
115
|
+
const markAt = (x) => (mark ? `<g transform="translate(${x + flagW / 2} 0)">${mark}</g>` : "");
|
|
116
|
+
const flagSpace = mark ? flagW + 14 * u : 0;
|
|
117
|
+
if (k === 0) {
|
|
118
|
+
// Red: the code, the flag, then the name.
|
|
119
|
+
let x = x0 + inner;
|
|
120
|
+
if (a.code) {
|
|
121
|
+
g.push(t(x, cy + cs * 0.36, cs, a.code, "bc-ink", "start", 800));
|
|
122
|
+
x += codeW;
|
|
123
|
+
}
|
|
124
|
+
if (mark) {
|
|
125
|
+
g.push(markAt(x));
|
|
126
|
+
x += flagSpace;
|
|
127
|
+
}
|
|
128
|
+
const avail = x0 + half - inner - x;
|
|
129
|
+
const ns = fitSize(name, 32 * u, avail, 0.55);
|
|
130
|
+
g.push(t(x, cy + ns * 0.36, ns, fit(name, ns, avail + 1, bw), "bc-ink", "start", 800));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Blue: the name (to the flag), the flag, then the code; written in that order, so the markup reads as the band does.
|
|
134
|
+
let right = x0 + half - inner;
|
|
135
|
+
const codeSvg = a.code ? t(right, cy + cs * 0.36, cs, a.code, "bc-ink", "end", 800) : "";
|
|
136
|
+
if (a.code)
|
|
137
|
+
right -= codeW;
|
|
138
|
+
const flagSvg = mark ? markAt(right - flagW) : "";
|
|
139
|
+
if (mark)
|
|
140
|
+
right -= flagSpace;
|
|
141
|
+
const avail = right - (x0 + inner);
|
|
142
|
+
const ns = fitSize(name, 32 * u, avail, 0.55);
|
|
143
|
+
g.push(t(right, cy + ns * 0.36, ns, fit(name, ns, avail + 1, bw), "bc-ink", "end", 800), flagSvg, codeSvg);
|
|
144
|
+
}
|
|
145
|
+
body.push(`<g class="bc-athlete">${g.join("")}</g>`);
|
|
146
|
+
});
|
|
147
|
+
body.push(t(W / 2, y + ah / 2 + 18 * u * 0.36 * 1.4, 36 * u, "–", "bc-ink", "middle", 800));
|
|
148
|
+
y += ah + BC.pad * u;
|
|
149
|
+
// The second bar and a row for each official.
|
|
150
|
+
body.push(bar(f, y, v.upper(w.rbOfficials), ""));
|
|
151
|
+
y += BC.band * u;
|
|
152
|
+
const rs = roles(officials, v);
|
|
153
|
+
const oh = BC.official * u;
|
|
154
|
+
const cs = 22 * u, ns0 = 24 * u;
|
|
155
|
+
const anyCode = officials.some((o) => o.code);
|
|
156
|
+
const anyFlag = officials.some((o) => o.flag || o.mark);
|
|
157
|
+
const codeW = anyCode ? Math.max(...officials.map((o) => bw(o.code ?? "", cs))) + 12 * u : 0;
|
|
158
|
+
officials.forEach((o, i) => {
|
|
159
|
+
const ry = y + BC.gap * u + i * (oh + BC.gap * u);
|
|
160
|
+
const cy = ry + oh / 2;
|
|
161
|
+
const g = [box(0, ry, W, oh, i % 2 === 0 ? "bc-rowa" : "bc-rowb"), gloss(f, 0, ry, W, oh)];
|
|
162
|
+
let x = 14 * u;
|
|
163
|
+
if (anyCode) {
|
|
164
|
+
if (o.code)
|
|
165
|
+
g.push(t(x, cy + cs * 0.36, cs, o.code, "bc-ink", "start", 700));
|
|
166
|
+
x += codeW;
|
|
167
|
+
}
|
|
168
|
+
if (anyFlag) {
|
|
169
|
+
if (o.flag || o.mark)
|
|
170
|
+
g.push(drawMark({ id: `o${i}`, name: nameOf(o.name), abbr: (o.code ?? abbreviate(nameOf(o.name))).slice(0, 3), ...(o.flag ? { flag: o.flag } : {}), ...(o.mark ? { mark: o.mark } : {}) }, x + (26 * u * 4) / 3 / 2, cy, 26 * u, f.ctx));
|
|
171
|
+
x += (26 * u * 4) / 3 + 14 * u;
|
|
172
|
+
}
|
|
173
|
+
const role = v.upper(rs[i]);
|
|
174
|
+
const rz = fitSize(role, cs, Math.max(60 * u, W * 0.3), 0.6);
|
|
175
|
+
const roleSvg = t(W - 16 * u, cy + rz * 0.36, rz, role, "bc-ink", "end", 800, ' letter-spacing=".03em"');
|
|
176
|
+
const nameRight = W - 16 * u - bw(role, rz) * 1.07 - 24 * u;
|
|
177
|
+
const name = written(o);
|
|
178
|
+
const avail = Math.max(60 * u, nameRight - x);
|
|
179
|
+
const nz = fitSize(name, ns0, avail, 0.6);
|
|
180
|
+
g.push(t(x, cy + nz * 0.36, nz, fit(name, nz, avail + 1, bw), "bc-ink", "start", 700), roleSvg);
|
|
181
|
+
body.push(`<g class="bc-official">${g.join("")}</g>`);
|
|
182
|
+
});
|
|
183
|
+
return broadcastWrap(f, options.title ?? source.title, describeBoutBoard(source, options), body, "bout-board");
|
|
184
|
+
}
|
|
185
|
+
/** A bar in the band colour with text at the left and, optionally, at the right. */
|
|
186
|
+
function bar(f, y, left, right) {
|
|
187
|
+
const { u, W } = f;
|
|
188
|
+
const h = BC.band * u;
|
|
189
|
+
const fs = 21 * u;
|
|
190
|
+
const rw = right ? bw(right, fs) * 1.07 : 0;
|
|
191
|
+
const room = (W - 40 * u - (rw ? rw + 24 * u : 0)) / 1.07;
|
|
192
|
+
const lz = fitSize(left, fs, room, 0.6);
|
|
193
|
+
return `${box(0, y, W, h, "bc-band")}${t(20 * u, y + h / 2 + lz * 0.36, lz, fit(left, lz, room, bw), "bc-bandink", "start", 800, ' letter-spacing=".03em"')}${right ? t(W - 20 * u, y + h / 2 + fs * 0.36, fs, right, "bc-bandink", "end", 800, ' letter-spacing=".03em"') : ""}`;
|
|
194
|
+
}
|
package/dist/broadcast-kit.js
CHANGED
|
@@ -79,6 +79,7 @@ export function broadcastStyle(scope, look, fixed = "") {
|
|
|
79
79
|
s(".bc-ink", `fill:${v("ink", p.ink)}`), s(".bc-sub", `fill:${v("sub", p.sub)}`),
|
|
80
80
|
s(".bc-rank", `fill:${v("rank", p.rank)};stroke:${v("rank-edge", p.rankEdge)};stroke-width:1.5`), s(".bc-rankink", `fill:${v("rank-ink", p.rankInk)}`),
|
|
81
81
|
s(".bc-g1", `fill:${v("gold", p.gold)};stroke:none`), s(".bc-g2", `fill:${v("silver", p.silver)};stroke:none`), s(".bc-g3", `fill:${v("bronze", p.bronze)};stroke:none`), s(".bc-medalink", `fill:${v("medal-ink", p.medalInk)}`),
|
|
82
|
+
s(".bc-discrim", "fill:none;stroke:rgba(255,255,255,.55);stroke-width:1.5"),
|
|
82
83
|
s(".bc-badge", `fill:${v("badge", p.badge)}`), s(".bc-badgeink", `fill:${v("badge-ink", p.badgeInk)}`),
|
|
83
84
|
s(".bc-red", `fill:${v("red", p.red)}`), s(".bc-blue", `fill:${v("blue", p.blue)}`),
|
|
84
85
|
s(".bc-band", `fill:${v("band", p.band)}`), s(".bc-bandink", `fill:${v("band-ink", p.bandInk)}`),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The medal table in the manner of a television graphic: a title band (the heading, with the date at its right), a sub bar with the three medal discs and the total over their columns, and a row
|
|
3
|
+
* for each country or team with its rank, a 4:3 flag, the country code (or the name) and the gold, silver and bronze counts, each beside its disc, and the total. The rows are ranked and tied as
|
|
4
|
+
* `medalTable` ranks them. One standalone SVG of a width you choose and a height that follows from the rows (`broadcastMedalSize`), made to be placed over a picture.
|
|
5
|
+
*/
|
|
6
|
+
import { type BroadcastOptions } from "./broadcast-kit.ts";
|
|
7
|
+
import { type MedalTableOptions, type MedalTableSource } from "./medals.ts";
|
|
8
|
+
export interface BroadcastMedalOptions extends BroadcastOptions, Pick<MedalTableOptions, "sort" | "ties" | "top" | "skipEmpty"> {
|
|
9
|
+
/** Write each row's `code` (the default where every row has one) or its `name`. */
|
|
10
|
+
labels?: "code" | "name";
|
|
11
|
+
/** Write the rank before each row. Default true. */
|
|
12
|
+
ranks?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** The width and the height of a broadcast medal table of `rowCount` rows: the width given (1280 by default) and a height of 64 + 36 + 10 + 48 a row + 10, in units of the width over 1280. */
|
|
15
|
+
export declare function broadcastMedalSize(rowCount: number, options?: Pick<BroadcastOptions, "width">): {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The broadcast-style medal table as a standalone SVG. Throws a `RangeError` for what `medalTable` refuses, more than 24 rows after `top` and `skipEmpty`, a `labels` that is not `code` or `name`,
|
|
21
|
+
* a look or icon that is not one of its values, or a width outside 720 to 4000.
|
|
22
|
+
*/
|
|
23
|
+
export declare function broadcastMedalTable(source: MedalTableSource, options?: BroadcastMedalOptions): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The medal table in the manner of a television graphic: a title band (the heading, with the date at its right), a sub bar with the three medal discs and the total over their columns, and a row
|
|
3
|
+
* for each country or team with its rank, a 4:3 flag, the country code (or the name) and the gold, silver and bronze counts, each beside its disc, and the total. The rows are ranked and tied as
|
|
4
|
+
* `medalTable` ranks them. One standalone SVG of a width you choose and a height that follows from the rows (`broadcastMedalSize`), made to be placed over a picture.
|
|
5
|
+
*/
|
|
6
|
+
import { BC, box, broadcastFrame, broadcastWrap, bw, fitSize, gloss, panel, t, titleBand } from "./broadcast-kit.js";
|
|
7
|
+
import { chosenMedalRows, describeMedalTable, validateMedalSource } from "./medals.js";
|
|
8
|
+
import { drawMark } from "./marks.js";
|
|
9
|
+
import { abbreviate, esc, fit, num } from "./text.js";
|
|
10
|
+
const MAX_ROWS = 24;
|
|
11
|
+
/** The height of a row of this table, in units of the width over 1280. */
|
|
12
|
+
const ROW = 46;
|
|
13
|
+
/** The width and the height of a broadcast medal table of `rowCount` rows: the width given (1280 by default) and a height of 64 + 36 + 10 + 48 a row + 10, in units of the width over 1280. */
|
|
14
|
+
export function broadcastMedalSize(rowCount, options = {}) {
|
|
15
|
+
const width = options.width ?? 1280;
|
|
16
|
+
const u = width / 1280;
|
|
17
|
+
return { width, height: Math.round((BC.title + BC.sub + BC.pad + rowCount * (ROW + BC.gap) + BC.pad) * u) };
|
|
18
|
+
}
|
|
19
|
+
const count = (n) => n ?? 0;
|
|
20
|
+
/** A medal disc: a filled circle in the medal's colour with a lighter rim and a highlight. */
|
|
21
|
+
function disc(cx, cy, r, kind) {
|
|
22
|
+
return `<circle class="bc-g${kind}" cx="${num(cx)}" cy="${num(cy)}" r="${num(r)}"/><circle class="bc-discrim" cx="${num(cx)}" cy="${num(cy)}" r="${num(r * 0.72)}"/>`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The broadcast-style medal table as a standalone SVG. Throws a `RangeError` for what `medalTable` refuses, more than 24 rows after `top` and `skipEmpty`, a `labels` that is not `code` or `name`,
|
|
26
|
+
* a look or icon that is not one of its values, or a width outside 720 to 4000.
|
|
27
|
+
*/
|
|
28
|
+
export function broadcastMedalTable(source, options = {}) {
|
|
29
|
+
const rule = { ...(options.sort !== undefined ? { sort: options.sort } : {}), ...(options.ties !== undefined ? { ties: options.ties } : {}), ...(options.top !== undefined ? { top: options.top } : {}), ...(options.skipEmpty !== undefined ? { skipEmpty: options.skipEmpty } : {}) };
|
|
30
|
+
validateMedalSource(source, rule);
|
|
31
|
+
if (options.labels !== undefined && options.labels !== "code" && options.labels !== "name")
|
|
32
|
+
throw new RangeError(`taisen-brackets: labels is "code" or "name", not ${JSON.stringify(options.labels)}`);
|
|
33
|
+
if (options.width !== undefined && options.width < 720)
|
|
34
|
+
throw new RangeError("taisen-brackets: a board's width is from 720 to 4000");
|
|
35
|
+
const rows = chosenMedalRows(source, rule);
|
|
36
|
+
if (rows.length > MAX_ROWS)
|
|
37
|
+
throw new RangeError(`taisen-brackets: a broadcast medal table has at most ${MAX_ROWS} rows (it is a screen graphic): cut the list with top`);
|
|
38
|
+
const size = broadcastMedalSize(rows.length, options);
|
|
39
|
+
const f = broadcastFrame(options, [source, options.sort, options.ties, options.top, options.skipEmpty, options.labels, options.ranks], size.height / (size.width / 1280));
|
|
40
|
+
const { W, u, v } = f;
|
|
41
|
+
const w = v.words;
|
|
42
|
+
const body = [panel(f)];
|
|
43
|
+
const title = titleBand(f, 0, source.title ?? w.medalTitle, source.date);
|
|
44
|
+
body.push(title.svg);
|
|
45
|
+
// The columns, from the right: a disc and a count for each medal, then the total.
|
|
46
|
+
const ns = 28 * u;
|
|
47
|
+
const maxMedal = Math.max(1, ...rows.map(({ row }) => Math.max(count(row.gold), count(row.silver), count(row.bronze))));
|
|
48
|
+
const maxTotal = Math.max(1, ...rows.map(({ row }) => count(row.gold) + count(row.silver) + count(row.bronze)));
|
|
49
|
+
const discR = 11 * u;
|
|
50
|
+
const medalW = discR * 2 + 12 * u + bw(v.number(maxMedal), ns) + 22 * u;
|
|
51
|
+
const totalLabel = v.upper(w.medalTotal);
|
|
52
|
+
const totalW = Math.max(bw(v.number(maxTotal), ns), bw(totalLabel, 18 * u * 1.07)) + 30 * u;
|
|
53
|
+
const right = W - 16 * u;
|
|
54
|
+
const totalX = right - totalW / 2;
|
|
55
|
+
const medalX = [0, 1, 2].map((k) => right - totalW - (2 - k) * medalW - medalW / 2);
|
|
56
|
+
const left = medalX[0] - medalW / 2;
|
|
57
|
+
// The sub bar: the subtitle at the left, the discs over their columns and the word total.
|
|
58
|
+
const sub = { svg: "", h: BC.sub * u };
|
|
59
|
+
const sy = title.h;
|
|
60
|
+
const subParts = [box(0, sy, W, sub.h, "", 0, ` fill="url(#${esc(f.scope)}-gs)"`)];
|
|
61
|
+
const fs = 20 * u;
|
|
62
|
+
const subText = v.upper(source.subtitle ?? "");
|
|
63
|
+
if (subText) {
|
|
64
|
+
const room = (left - 20 * u - 14 * u) / 1.07;
|
|
65
|
+
const z = fitSize(subText, fs, room, 0.6);
|
|
66
|
+
subParts.push(t(20 * u, sy + sub.h / 2 + z * 0.36, z, fit(subText, z, room, bw), "bc-subink", "start", 800, ' letter-spacing=".02em"'));
|
|
67
|
+
}
|
|
68
|
+
[1, 2, 3].forEach((k, i) => {
|
|
69
|
+
const name = [w.medalGold, w.medalSilver, w.medalBronze][i];
|
|
70
|
+
subParts.push(`<g class="bc-medalhead"><title>${esc(name)}</title>${disc(medalX[i] - medalW / 2 + discR + 4 * u, sy + sub.h / 2, 12 * u, k)}</g>`);
|
|
71
|
+
});
|
|
72
|
+
const tz = fitSize(totalLabel, 18 * u, (totalW - 8 * u) / 1.07, 0.6);
|
|
73
|
+
subParts.push(t(totalX, sy + sub.h / 2 + tz * 0.36, tz, fit(totalLabel, tz, (totalW - 8 * u) / 1.07, bw), "bc-subink", "middle", 800, ' letter-spacing=".02em"'));
|
|
74
|
+
body.push(subParts.join(""));
|
|
75
|
+
const top = title.h + sub.h + BC.pad * u;
|
|
76
|
+
const h = ROW * u;
|
|
77
|
+
const showRank = options.ranks ?? true;
|
|
78
|
+
const useCode = (options.labels ?? (rows.every(({ row }) => row.code) ? "code" : "name")) === "code";
|
|
79
|
+
const labelOf = (r) => (useCode ? r.code ?? abbreviate(r.name).slice(0, 3).toUpperCase() : r.name);
|
|
80
|
+
const rankTexts = rows.map(({ rank }) => String(v.number(rank)));
|
|
81
|
+
const rankW = showRank ? Math.max(...rankTexts.map((x) => bw(x, ns))) + 18 * u : 0;
|
|
82
|
+
const flagH = 28 * u, flagW = flagH * (4 / 3);
|
|
83
|
+
rows.forEach(({ row }, i) => {
|
|
84
|
+
const y = top + i * (h + BC.gap * u);
|
|
85
|
+
const cy = y + h / 2;
|
|
86
|
+
const g = [box(0, y, W, h, i % 2 === 0 ? "bc-rowa" : "bc-rowb"), gloss(f, 0, y, W, h)];
|
|
87
|
+
let x = 14 * u;
|
|
88
|
+
if (showRank) {
|
|
89
|
+
g.push(t(x + rankW / 2 - 6 * u, cy + ns * 0.36, ns * 0.9, rankTexts[i], "bc-ink", "middle", 800));
|
|
90
|
+
x += rankW;
|
|
91
|
+
}
|
|
92
|
+
g.push(drawMark({ id: `m${i}`, name: row.name, abbr: (row.code ?? abbreviate(row.name)).slice(0, 3).toUpperCase(), ...(row.flag ? { flag: row.flag } : {}), ...(row.mark ? { mark: row.mark } : {}) }, x + flagW / 2, cy, flagH, f.ctx));
|
|
93
|
+
x += flagW + 14 * u;
|
|
94
|
+
const label = labelOf(row);
|
|
95
|
+
const avail = Math.max(40 * u, left - 12 * u - x);
|
|
96
|
+
const lz = fitSize(label, 28 * u, avail, 0.6);
|
|
97
|
+
g.push(t(x, cy + lz * 0.36, lz, fit(label, lz, avail + 1, bw), "bc-ink", "start", 700));
|
|
98
|
+
[row.gold, row.silver, row.bronze].forEach((c, k) => {
|
|
99
|
+
const cx = medalX[k];
|
|
100
|
+
g.push(disc(cx - medalW / 2 + discR + 4 * u, cy, discR, (k + 1)));
|
|
101
|
+
g.push(t(cx + medalW / 2 - 14 * u, cy + ns * 0.36, ns, v.number(count(c)), "bc-ink", "end", 700));
|
|
102
|
+
});
|
|
103
|
+
g.push(t(totalX + totalW / 2 - 14 * u, cy + ns * 0.36 * 1.05, ns * 1.05, v.number(count(row.gold) + count(row.silver) + count(row.bronze)), "bc-ink", "end", 800));
|
|
104
|
+
body.push(`<g class="bc-row">${g.join("")}</g>`);
|
|
105
|
+
});
|
|
106
|
+
if (rows.length === 0)
|
|
107
|
+
body.push(t(20 * u, top + 30 * u, 24 * u, w.medalNone, "bc-sub", "start", 700));
|
|
108
|
+
return broadcastWrap(f, options.title ?? source.title ?? w.medalTitle, describeMedalTable(source, { ...rule, ...(options.locale !== undefined ? { locale: options.locale } : {}), ...(options.words !== undefined ? { words: options.words } : {}) }), body, "broadcast-medal-table");
|
|
109
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -39,3 +39,7 @@ export { resultsBoard, resultsBoardSize, describeResultsBoard } from "./results-
|
|
|
39
39
|
export type { ResultsBoardOptions, ResultsBoardSource, ResultsColumn, ResultsRow } from "./results-board.ts";
|
|
40
40
|
export { BROADCAST_LOOKS } from "./broadcast-kit.ts";
|
|
41
41
|
export type { BroadcastIcon, BroadcastLook, BroadcastOptions } from "./broadcast-kit.ts";
|
|
42
|
+
export { boutBoard, boutBoardSize, describeBoutBoard } from "./bout-board.ts";
|
|
43
|
+
export type { BoutAthlete, BoutBoardOptions, BoutBoardSource, BoutOfficial } from "./bout-board.ts";
|
|
44
|
+
export { broadcastMedalTable, broadcastMedalSize } from "./broadcast-medals.ts";
|
|
45
|
+
export type { BroadcastMedalOptions } from "./broadcast-medals.ts";
|
package/dist/index.js
CHANGED
|
@@ -21,3 +21,5 @@ export { rankingMatchup, describeMatchup, matchupRecord } from "./ranking-matchu
|
|
|
21
21
|
export { medalTable, medalRanks, tallyMedals, describeMedalTable } from "./medals.js";
|
|
22
22
|
export { resultsBoard, resultsBoardSize, describeResultsBoard } from "./results-board.js";
|
|
23
23
|
export { BROADCAST_LOOKS } from "./broadcast-kit.js";
|
|
24
|
+
export { boutBoard, boutBoardSize, describeBoutBoard } from "./bout-board.js";
|
|
25
|
+
export { broadcastMedalTable, broadcastMedalSize } from "./broadcast-medals.js";
|
package/dist/locales.js
CHANGED
|
@@ -34,6 +34,7 @@ const EN = {
|
|
|
34
34
|
rankMatchTitle: "Head to head", rankMeetings: "Previous meetings", rankNoMeetings: "No previous meetings", rankMore: "+{n} more",
|
|
35
35
|
medalTitle: "Medal table", medalNone: "No medals yet", medalGold: "Gold", medalSilver: "Silver", medalBronze: "Bronze", medalGoldShort: "G", medalSilverShort: "S", medalBronzeShort: "B", medalTotal: "Total", medalLine: "{g} gold, {s} silver, {b} bronze, {t} total", medalRank: "{n}",
|
|
36
36
|
rbResult: "Result",
|
|
37
|
+
rbBoutPresentation: "Bout presentation", rbBout: "Bout: {n}", rbOfficials: "Referee and judges", rbReferee: "Referee", rbJudge: "Judge {n}",
|
|
37
38
|
};
|
|
38
39
|
const FR = {
|
|
39
40
|
tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
|
|
@@ -64,6 +65,7 @@ const FR = {
|
|
|
64
65
|
rankMatchTitle: "Face-à-face", rankMeetings: "Confrontations précédentes", rankNoMeetings: "Aucune confrontation précédente", rankMore: "+{n} de plus",
|
|
65
66
|
medalTitle: "Tableau des médailles", medalNone: "Aucune médaille pour l’instant", medalGold: "Or", medalSilver: "Argent", medalBronze: "Bronze", medalGoldShort: "O", medalSilverShort: "A", medalBronzeShort: "B", medalTotal: "Total", medalLine: "{g} or, {s} argent, {b} bronze, {t} au total", medalRank: "{n}",
|
|
66
67
|
rbResult: "Résultat",
|
|
68
|
+
rbBoutPresentation: "Présentation du combat", rbBout: "Combat\u00a0: {n}", rbOfficials: "Arbitre et juges", rbReferee: "Arbitre", rbJudge: "Juge {n}",
|
|
67
69
|
};
|
|
68
70
|
const JA = {
|
|
69
71
|
tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
|
|
@@ -94,6 +96,7 @@ const JA = {
|
|
|
94
96
|
rankMatchTitle: "直接対決", rankMeetings: "過去の対戦", rankNoMeetings: "過去の対戦はありません", rankMore: "ほか{n}試合",
|
|
95
97
|
medalTitle: "メダル獲得表", medalNone: "まだメダルはありません", medalGold: "金メダル", medalSilver: "銀メダル", medalBronze: "銅メダル", medalGoldShort: "金", medalSilverShort: "銀", medalBronzeShort: "銅", medalTotal: "合計", medalLine: "金{g}、銀{s}、銅{b}、合計{t}", medalRank: "{n}位",
|
|
96
98
|
rbResult: "結果",
|
|
99
|
+
rbBoutPresentation: "試合紹介", rbBout: "第{n}試合", rbOfficials: "審判", rbReferee: "主審", rbJudge: "副審{n}",
|
|
97
100
|
};
|
|
98
101
|
const ES = {
|
|
99
102
|
tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
|
|
@@ -124,6 +127,7 @@ const ES = {
|
|
|
124
127
|
rankMatchTitle: "Cara a cara", rankMeetings: "Enfrentamientos anteriores", rankNoMeetings: "Sin enfrentamientos anteriores", rankMore: "+{n} más",
|
|
125
128
|
medalTitle: "Medallero", medalNone: "Aún no hay medallas", medalGold: "Oro", medalSilver: "Plata", medalBronze: "Bronce", medalGoldShort: "O", medalSilverShort: "P", medalBronzeShort: "B", medalTotal: "Total", medalLine: "{g} oro, {s} plata, {b} bronce, {t} en total", medalRank: "{n}",
|
|
126
129
|
rbResult: "Resultado",
|
|
130
|
+
rbBoutPresentation: "Presentación del combate", rbBout: "Combate: {n}", rbOfficials: "Árbitro y jueces", rbReferee: "Árbitro", rbJudge: "Juez {n}",
|
|
127
131
|
};
|
|
128
132
|
const DE = {
|
|
129
133
|
tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
|
|
@@ -154,6 +158,7 @@ const DE = {
|
|
|
154
158
|
rankMatchTitle: "Direkter Vergleich", rankMeetings: "Bisherige Duelle", rankNoMeetings: "Noch keine Duelle", rankMore: "+{n} weitere",
|
|
155
159
|
medalTitle: "Medaillenspiegel", medalNone: "Noch keine Medaillen", medalGold: "Gold", medalSilver: "Silber", medalBronze: "Bronze", medalGoldShort: "G", medalSilverShort: "S", medalBronzeShort: "B", medalTotal: "Gesamt", medalLine: "{g} Gold, {s} Silber, {b} Bronze, {t} gesamt", medalRank: "{n}",
|
|
156
160
|
rbResult: "Ergebnis",
|
|
161
|
+
rbBoutPresentation: "Kampfvorstellung", rbBout: "Kampf: {n}", rbOfficials: "Kampfrichter", rbReferee: "Mattenleiter", rbJudge: "Punktrichter {n}",
|
|
157
162
|
};
|
|
158
163
|
/**
|
|
159
164
|
* The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
|
package/dist/medals.d.ts
CHANGED
|
@@ -76,6 +76,13 @@ export interface MedalFilter {
|
|
|
76
76
|
export declare function tallyMedals(awards: readonly MedalAward[], filter?: MedalFilter): MedalRow[];
|
|
77
77
|
/** The ranks of a list of rows, as `medalTable` draws them (`[1, 2, 2, 4]`), in the order they are drawn. */
|
|
78
78
|
export declare function medalRanks(rows: readonly MedalRow[], options?: Pick<MedalTableOptions, "sort" | "ties">): number[];
|
|
79
|
+
/** Checks a medal table's source and options (shared by the broadcast-style table). */
|
|
80
|
+
export declare function validateMedalSource(source: MedalTableSource, options: MedalTableOptions): void;
|
|
81
|
+
/** The rows that are drawn: sorted, ranked, with the empty ones and all but the top N left out when asked. */
|
|
82
|
+
export declare function chosenMedalRows(source: MedalTableSource, options: Pick<MedalTableOptions, "sort" | "ties" | "skipEmpty" | "top">): {
|
|
83
|
+
row: MedalRow;
|
|
84
|
+
rank: number;
|
|
85
|
+
}[];
|
|
79
86
|
/** The table as a sentence, the drawing's description. */
|
|
80
87
|
export declare function describeMedalTable(source: MedalTableSource, options?: Pick<MedalTableOptions, "locale" | "words" | "sort" | "ties" | "top" | "skipEmpty">): string;
|
|
81
88
|
/**
|
package/dist/medals.js
CHANGED
|
@@ -54,7 +54,8 @@ function ordered(rows, sort, ties) {
|
|
|
54
54
|
export function medalRanks(rows, options = {}) {
|
|
55
55
|
return ordered(rows, options.sort ?? "gold", options.ties ?? true).map((x) => x.rank);
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
/** Checks a medal table's source and options (shared by the broadcast-style table). */
|
|
58
|
+
export function validateMedalSource(source, options) {
|
|
58
59
|
if (!source || !Array.isArray(source.rows))
|
|
59
60
|
throw new RangeError("taisen-brackets: a medal table needs a list of rows");
|
|
60
61
|
source.rows.forEach((r, i) => {
|
|
@@ -79,7 +80,7 @@ function validate(source, options) {
|
|
|
79
80
|
throw new RangeError("taisen-brackets: page is a whole number from 0");
|
|
80
81
|
}
|
|
81
82
|
/** The rows that are drawn: sorted, ranked, with the empty ones and all but the top N left out when asked. */
|
|
82
|
-
function
|
|
83
|
+
export function chosenMedalRows(source, options) {
|
|
83
84
|
let list = ordered(source.rows, options.sort ?? "gold", options.ties ?? true);
|
|
84
85
|
if (options.skipEmpty)
|
|
85
86
|
list = list.filter((x) => totalOf(x.row) > 0);
|
|
@@ -94,7 +95,7 @@ export function describeMedalTable(source, options = {}) {
|
|
|
94
95
|
parts.push(source.subtitle);
|
|
95
96
|
if (source.date)
|
|
96
97
|
parts.push(source.date);
|
|
97
|
-
const list =
|
|
98
|
+
const list = chosenMedalRows(source, options);
|
|
98
99
|
if (list.length === 0)
|
|
99
100
|
parts.push(w.medalNone);
|
|
100
101
|
for (const { row, rank } of list)
|
|
@@ -106,13 +107,13 @@ const teamFor = (r, id) => ({ id, name: r.name, abbr: (r.code ?? abbreviate(r.na
|
|
|
106
107
|
* The medal table as an SVG. Throws a `RangeError` for a row with no name, a medal count that is not a whole number from 0, an option that is not one of its values, or a bad size or theme.
|
|
107
108
|
*/
|
|
108
109
|
export function medalTable(source, options = {}) {
|
|
109
|
-
|
|
110
|
+
validateMedalSource(source, options);
|
|
110
111
|
const f = boardFrame(options, [source, options.sort, options.ties, options.rankStyle, options.top, options.rows, options.page, options.columns, options.highlight, options.skipEmpty]);
|
|
111
112
|
const { W, H, v } = f;
|
|
112
113
|
const w = v.words;
|
|
113
114
|
const unit = Math.min(W, H);
|
|
114
115
|
const pad = Math.round(unit * 0.035);
|
|
115
|
-
const all =
|
|
116
|
+
const all = chosenMedalRows(source, options);
|
|
116
117
|
const columns = options.columns ?? (W / H >= 1.5 && all.length > 10 ? 2 : 1);
|
|
117
118
|
const head = heading(f, source.title ?? w.medalTitle, source.subtitle, source.date, pad, columns === 2 ? W - pad * 2 : Math.min(W - pad * 2, unit * 1.3), source.source);
|
|
118
119
|
const body = [...head.svg];
|
package/dist/types.d.ts
CHANGED
|
@@ -440,6 +440,12 @@ export interface Words {
|
|
|
440
440
|
medalRank: string;
|
|
441
441
|
/** The broadcast-style boards (`resultsBoard`, `boutBoard`, `broadcastMedalTable`): the sub bar\'s word for a result. */
|
|
442
442
|
rbResult: string;
|
|
443
|
+
/** The bout presentation board (`boutBoard`): the first bar, the bout's number (`{n}`), the second bar, the referee and a judge (`{n}`). */
|
|
444
|
+
rbBoutPresentation: string;
|
|
445
|
+
rbBout: string;
|
|
446
|
+
rbOfficials: string;
|
|
447
|
+
rbReferee: string;
|
|
448
|
+
rbJudge: string;
|
|
443
449
|
}
|
|
444
450
|
/** A language: its words, the names of the rounds (by their English names), and the tag `Intl` formats numbers and dates for. */
|
|
445
451
|
export interface Locale {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.17.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.17.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/taisen-brackets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|