@johnmorrisdotca/taisen-brackets 0.1.1 → 0.2.1
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 +30 -1
- package/NOTICE.md +7 -1
- package/README.md +113 -10
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/layout-entry.d.ts +3 -1
- package/dist/layout-entry.js +2 -1
- package/dist/layout.js +11 -1
- package/dist/locales.d.ts +42 -0
- package/dist/locales.js +179 -0
- package/dist/matchup.d.ts +9 -6
- package/dist/matchup.js +54 -23
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +4 -1
- package/dist/render.d.ts +6 -6
- package/dist/render.js +82 -67
- package/dist/rounds.d.ts +2 -2
- package/dist/rounds.js +4 -4
- package/dist/table.d.ts +4 -5
- package/dist/table.js +17 -13
- package/dist/theme.js +1 -1
- package/dist/types.d.ts +62 -14
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/dist/words.d.ts +0 -7
- package/dist/words.js +0 -33
package/dist/matchup.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { Matchup, SideId, Team, Tie
|
|
1
|
+
import type { Matchup, SideId, Team, Tie } from "./types.ts";
|
|
2
|
+
import { type Voice } from "./locales.ts";
|
|
2
3
|
/** A bracket's teams: the list given, plus a team made from every id that a matchup names and the list does not. */
|
|
3
4
|
export declare function collectTeams(matchups: readonly Matchup[], given?: readonly Team[]): Map<SideId, Team>;
|
|
4
5
|
/** One matchup as a tie: sides resolved to teams, the winner and the result worked out. The round is set by the layout. */
|
|
5
6
|
export declare function resolveMatchup(m: Matchup, index: number, teams: ReadonlyMap<SideId, Team>): Tie;
|
|
6
7
|
/** The short name of a team for a small box. */
|
|
7
|
-
export declare const abbrOf: (team: Team | null,
|
|
8
|
-
/** The pieces of a note under a tie's score: "a.e.t.", "pens", "in 7", "agg.". */
|
|
9
|
-
export declare function noteParts(tie: Tie,
|
|
10
|
-
/**
|
|
11
|
-
export declare function
|
|
8
|
+
export declare const abbrOf: (team: Team | null, v: Voice) => string;
|
|
9
|
+
/** The pieces of a note under a tie's score, short: "a.e.t.", "pens", "in 7", "agg.". */
|
|
10
|
+
export declare function noteParts(tie: Tie, v: Voice): string[];
|
|
11
|
+
/** The notes of a tie written out, for a sentence or a table: "after extra time", "4–2 on penalties". */
|
|
12
|
+
export declare function noteLong(tie: Tie, v: Voice): string[];
|
|
13
|
+
/** A tie in a sentence, for a tooltip, a table and a description: "Argentina 3, France 3 (after extra time, 4–2 on penalties). Winner: Argentina". */
|
|
14
|
+
export declare function describeTie(tie: Tie, v: Voice): string;
|
package/dist/matchup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { abbreviate } from "./text.js";
|
|
2
|
-
import { fill } from "./
|
|
2
|
+
import { fill } from "./locales.js";
|
|
3
3
|
/** A bracket's teams: the list given, plus a team made from every id that a matchup names and the list does not. */
|
|
4
4
|
export function collectTeams(matchups, given = []) {
|
|
5
5
|
const teams = new Map();
|
|
@@ -34,8 +34,10 @@ function settle(m, kind) {
|
|
|
34
34
|
const byPens = (pens) => (pens ? byScore(pens[0], pens[1]) : null);
|
|
35
35
|
if (kind === "series") {
|
|
36
36
|
let wins = null;
|
|
37
|
+
// A game level at the end of play but settled by a shoot-out is won by the side that won the shoot-out (MLS's best-of-three round).
|
|
38
|
+
const won = (g) => byScore(g.score[0], g.score[1]) ?? byPens(g.pens ?? null);
|
|
37
39
|
if (games.length)
|
|
38
|
-
wins = [games.filter((g) => g
|
|
40
|
+
wins = [games.filter((g) => won(g) === 0).length, games.filter((g) => won(g) === 1).length];
|
|
39
41
|
else if (m.score)
|
|
40
42
|
wins = m.score;
|
|
41
43
|
const need = bestOf ? Math.ceil(bestOf / 2) : null;
|
|
@@ -98,50 +100,79 @@ export function resolveMatchup(m, index, teams) {
|
|
|
98
100
|
date: m.date ?? m.games?.[0]?.date ?? m.legs?.[0]?.date ?? "",
|
|
99
101
|
no: m.no ?? null,
|
|
100
102
|
third: !!m.third,
|
|
103
|
+
walkover: !!m.walkover,
|
|
101
104
|
note: m.note ?? null,
|
|
102
105
|
};
|
|
103
106
|
}
|
|
104
107
|
/** The short name of a team for a small box. */
|
|
105
|
-
export const abbrOf = (team,
|
|
106
|
-
/** The pieces of a note under a tie's score: "a.e.t.", "pens", "in 7", "agg.". */
|
|
107
|
-
export function noteParts(tie,
|
|
108
|
+
export const abbrOf = (team, v) => (team ? team.abbr ?? abbreviate(v.team(team)) : v.words.tbd);
|
|
109
|
+
/** The pieces of a note under a tie's score, short: "a.e.t.", "pens", "in 7", "agg.". */
|
|
110
|
+
export function noteParts(tie, v) {
|
|
108
111
|
const r = tie.result;
|
|
112
|
+
const w = v.words;
|
|
109
113
|
const out = [];
|
|
110
114
|
if (r.kind === "series") {
|
|
111
115
|
if (r.decided && r.played > 0)
|
|
112
|
-
out.push(fill(
|
|
116
|
+
out.push(fill(w.inGames, { n: v.number(r.played) }));
|
|
113
117
|
else if (r.bestOf)
|
|
114
|
-
out.push(fill(
|
|
118
|
+
out.push(fill(w.bestOf, { n: v.number(r.bestOf), w: v.number(Math.ceil(r.bestOf / 2)) }));
|
|
115
119
|
}
|
|
116
120
|
else {
|
|
117
121
|
if (r.kind === "aggregate")
|
|
118
|
-
out.push(
|
|
122
|
+
out.push(w.aggregate);
|
|
119
123
|
if (r.kind === "replay")
|
|
120
|
-
out.push(
|
|
124
|
+
out.push(w.replay);
|
|
121
125
|
if (r.extraTime)
|
|
122
|
-
out.push(
|
|
126
|
+
out.push(w.aet);
|
|
123
127
|
if (r.pens)
|
|
124
|
-
out.push(
|
|
128
|
+
out.push(w.pens);
|
|
125
129
|
}
|
|
130
|
+
if (tie.walkover)
|
|
131
|
+
out.push(w.walkover);
|
|
126
132
|
if (tie.note)
|
|
127
133
|
out.push(tie.note);
|
|
128
134
|
return out;
|
|
129
135
|
}
|
|
130
|
-
/**
|
|
131
|
-
export function
|
|
132
|
-
const name = (t) => (t ? t.name : words.tbd);
|
|
133
|
-
const [a, b] = tie.sides;
|
|
136
|
+
/** The notes of a tie written out, for a sentence or a table: "after extra time", "4–2 on penalties". */
|
|
137
|
+
export function noteLong(tie, v) {
|
|
134
138
|
const r = tie.result;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
const w = v.words;
|
|
140
|
+
const out = [];
|
|
141
|
+
if (r.kind === "series") {
|
|
142
|
+
if (r.decided && r.played > 0)
|
|
143
|
+
out.push(fill(w.inGames, { n: v.number(r.played) }));
|
|
144
|
+
else if (r.bestOf)
|
|
145
|
+
out.push(fill(w.bestOf, { n: v.number(r.bestOf), w: v.number(Math.ceil(r.bestOf / 2)) }));
|
|
146
|
+
}
|
|
138
147
|
else {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
148
|
+
if (r.kind === "aggregate")
|
|
149
|
+
out.push(w.aggregateLong);
|
|
150
|
+
if (r.kind === "replay")
|
|
151
|
+
out.push(w.replay);
|
|
152
|
+
if (r.extraTime)
|
|
153
|
+
out.push(w.aetLong);
|
|
154
|
+
if (r.pens)
|
|
155
|
+
out.push(fill(w.pensScore, { a: v.number(r.pens[0]), b: v.number(r.pens[1]) }));
|
|
143
156
|
}
|
|
157
|
+
if (tie.walkover)
|
|
158
|
+
out.push(w.walkover);
|
|
159
|
+
if (tie.note)
|
|
160
|
+
out.push(tie.note);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/** A tie in a sentence, for a tooltip, a table and a description: "Argentina 3, France 3 (after extra time, 4–2 on penalties). Winner: Argentina". */
|
|
164
|
+
export function describeTie(tie, v) {
|
|
165
|
+
const w = v.words;
|
|
166
|
+
const name = (t) => (t ? v.team(t) : w.tbd);
|
|
167
|
+
const [a, b] = tie.sides;
|
|
168
|
+
const r = tie.result;
|
|
169
|
+
let text = r.score
|
|
170
|
+
? fill(w.tieScore, { a: name(a), b: name(b), sa: v.number(r.score[0]), sb: v.number(r.score[1]) })
|
|
171
|
+
: fill(w.tieVs, { a: name(a), b: name(b) });
|
|
172
|
+
const notes = noteLong(tie, v);
|
|
173
|
+
if (notes.length)
|
|
174
|
+
text += fill(w.note, { notes: notes.join(w.noteJoin) });
|
|
144
175
|
if (tie.winner !== null)
|
|
145
|
-
text +=
|
|
176
|
+
text += `${w.sep}${fill(w.winnerIs, { name: name(tie.sides[tie.winner]) })}`;
|
|
146
177
|
return text;
|
|
147
178
|
}
|
package/dist/presets.d.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Playoff formats by era: the round names, the length of each round's series and who has a bye, for the five North
|
|
3
3
|
* American leagues and the FIFA World Cup. Data only; nothing here draws. Each era was checked against the English
|
|
4
4
|
* Wikipedia articles in its `sources` (2026-10-13); where a season did something of its own (a strike, a pandemic) it has
|
|
5
|
-
* its own entry. The year is the year the playoffs are played in
|
|
5
|
+
* its own entry. The year is the year the playoffs are played in (the NHL's and the NBA's 2024 are the 2024 playoffs, of the 2023-24 season), except for the NFL, whose year is its season's: 2024 is the season played in 2024, with playoffs in January and February 2025.
|
|
6
6
|
*/
|
|
7
|
-
export type League = "nhl" | "nba" | "mlb" | "nfl" | "cfl" | "worldcup";
|
|
7
|
+
export type League = "nhl" | "nba" | "mlb" | "nfl" | "cfl" | "mls" | "worldcup";
|
|
8
8
|
export interface PlayoffRound {
|
|
9
9
|
/** The round's name as the league calls it. */
|
|
10
10
|
name: string;
|
package/dist/presets.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Playoff formats by era: the round names, the length of each round's series and who has a bye, for the five North
|
|
3
3
|
* American leagues and the FIFA World Cup. Data only; nothing here draws. Each era was checked against the English
|
|
4
4
|
* Wikipedia articles in its `sources` (2026-10-13); where a season did something of its own (a strike, a pandemic) it has
|
|
5
|
-
* its own entry. The year is the year the playoffs are played in
|
|
5
|
+
* its own entry. The year is the year the playoffs are played in (the NHL's and the NBA's 2024 are the 2024 playoffs, of the 2023-24 season), except for the NFL, whose year is its season's: 2024 is the season played in 2024, with playoffs in January and February 2025.
|
|
6
6
|
*/
|
|
7
7
|
/** The address of English Wikipedia articles, by title. */
|
|
8
8
|
const wiki = (...titles) => titles.map((t) => `https://en.wikipedia.org/wiki/${t.replace(/ /g, "_")}`);
|
|
@@ -47,6 +47,9 @@ const ERAS = {
|
|
|
47
47
|
cfl: [
|
|
48
48
|
{ league: "cfl", from: 1996, to: 2026, label: "division semi-finals and finals, with a crossover", teams: 6, byes: 1, groups: ["West", "East"], rounds: [r("Division Semi-Final", 1), r("Division Final", 1), r("Grey Cup", 1)], note: "Three teams of each division qualify and the first-placed team has a bye. A fourth-placed team with a better record than the third-placed team of the other division takes that place (the crossover, since 1996). The league has announced an eight-team format from 2027.", sources: wiki("1996 CFL season", "2012 CFL season", "2019 CFL season", "CFL playoffs") },
|
|
49
49
|
],
|
|
50
|
+
mls: [
|
|
51
|
+
{ league: "mls", from: 2023, to: null, label: "18 teams, nine a conference: a wild card match, then best of three", teams: 18, byes: 7, groups: WEST_EAST, rounds: [r("Wild Card Round", 1), r("Round One", 3), r("Conference Semifinals", 1), r("Conference Finals", 1), r("MLS Cup", 1)], note: "The teams ranked 8th and 9th of each conference play one wild card match (settled by a shoot-out if level); the other seven teams skip it. Round One is a best-of-three series (a game level after play is settled by a shoot-out, and counts as a win); the rest are single matches. Earlier seasons had other formats and are not covered.", sources: wiki("MLS Cup Playoffs", "2024 MLS Cup Playoffs") },
|
|
52
|
+
],
|
|
50
53
|
worldcup: [
|
|
51
54
|
{ league: "worldcup", from: 1930, to: 1930, label: "four group winners, straight to the semi-finals", teams: 4, byes: 0, groups: null, rounds: [r("Semi-finals", 1), r("Final", 1)], years: [1930], sources: wiki("1930 FIFA World Cup") },
|
|
52
55
|
{ league: "worldcup", from: 1934, to: 1938, label: "16 teams, straight knockout, drawn matches replayed", teams: 16, byes: 0, groups: null, rounds: [r("Round of 16", 1), r("Quarter-finals", 1), r("Semi-finals", 1), r("Final", 1)], years: [1934, 1938], note: "A match still level after extra time was replayed (1934, 1938). There was a third-place match in both. In 1938 Austria withdrew, so 15 teams played.", sources: wiki("1934 FIFA World Cup", "FIFA World Cup") },
|
package/dist/render.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { BracketInput, BracketLayout, DrawOptions, Matchup, Size, SizeName
|
|
1
|
+
import type { BracketInput, BracketLayout, DrawOptions, Matchup, Size, SizeName } from "./types.ts";
|
|
2
|
+
import { type Voice } from "./locales.ts";
|
|
2
3
|
/** The sizes by name. A phone's drawing is 390 wide and as tall as the bracket needs. */
|
|
3
4
|
export declare const SIZES: Readonly<Record<SizeName, Size>>;
|
|
4
|
-
/** The
|
|
5
|
-
export declare function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}): string;
|
|
5
|
+
/** The layout with each team's name in the language: what the drawing, the table and the description are written from. */
|
|
6
|
+
export declare function localizedLayout(layout: BracketLayout, v: Voice): BracketLayout;
|
|
7
|
+
/** The text a screen reader gets for the drawing as a whole, in the language of `locale`. */
|
|
8
|
+
export declare function describeBracket(input: BracketLayout | readonly Matchup[] | BracketInput, options?: Pick<DrawOptions, "locale" | "words" | "teamName" | "roundNames">): string;
|
|
9
9
|
/**
|
|
10
10
|
* Draws a bracket as an SVG string. `input` is a list of matchups, `{ matchups, teams }` or a layout from `layoutBracket`.
|
|
11
11
|
* The same input and options always give the same string: no clock, no random number, no counter.
|
package/dist/render.js
CHANGED
|
@@ -4,7 +4,7 @@ import { cleanSvg, clipId, drawMark, safeUrl } from "./marks.js";
|
|
|
4
4
|
import { roundLabel } from "./rounds.js";
|
|
5
5
|
import { styleFor } from "./theme.js";
|
|
6
6
|
import { esc, fit, hash, num, textWidth } from "./text.js";
|
|
7
|
-
import { fill,
|
|
7
|
+
import { fill, voiceOf } from "./locales.js";
|
|
8
8
|
/** The sizes by name. A phone's drawing is 390 wide and as tall as the bracket needs. */
|
|
9
9
|
export const SIZES = Object.freeze({
|
|
10
10
|
wide: { width: 1200, height: 675 },
|
|
@@ -13,34 +13,41 @@ export const SIZES = Object.freeze({
|
|
|
13
13
|
});
|
|
14
14
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
15
15
|
const isLayout = (x) => typeof x === "object" && x !== null && "ties" in x && "sides" in x && "rounds" in x;
|
|
16
|
-
/** The
|
|
16
|
+
/** The layout with each team's name in the language: what the drawing, the table and the description are written from. */
|
|
17
|
+
export function localizedLayout(layout, v) {
|
|
18
|
+
const named = new Map(layout.teams.map((t) => [t.id, { ...t, name: v.team(t) }]));
|
|
19
|
+
const swap = (t) => (t ? (named.get(t.id) ?? t) : t);
|
|
20
|
+
return { ...layout, ties: layout.ties.map((t) => ({ ...t, sides: [swap(t.sides[0]), swap(t.sides[1])] })), champion: swap(layout.champion), teams: [...named.values()] };
|
|
21
|
+
}
|
|
22
|
+
/** The text a screen reader gets for the drawing as a whole, in the language of `locale`. */
|
|
17
23
|
export function describeBracket(input, options = {}) {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
24
|
+
const v = voiceOf(options);
|
|
25
|
+
const layout = localizedLayout(isLayout(input) ? input : layoutBracket(input), v);
|
|
26
|
+
const w = v.words;
|
|
27
|
+
const parts = [fill(layout.rounds === 1 ? w.summaryOne : w.summary, { n: v.number(layout.teams.length), r: v.number(layout.rounds) })];
|
|
21
28
|
const finalTie = layout.finalIndex !== null ? layout.ties[layout.finalIndex] : null;
|
|
22
29
|
if (finalTie)
|
|
23
|
-
parts.push(
|
|
30
|
+
parts.push(fill(w.finalLine, { round: roundLabel(layout, layout.rounds, options.roundNames, v), tie: describeTie(finalTie, v) }));
|
|
24
31
|
if (layout.champion)
|
|
25
|
-
parts.push(
|
|
26
|
-
return `${parts.join(
|
|
32
|
+
parts.push(fill(w.championIs, { name: layout.champion.name }));
|
|
33
|
+
return `${parts.join(w.sep)}${w.end}`;
|
|
27
34
|
}
|
|
28
35
|
/**
|
|
29
36
|
* Draws a bracket as an SVG string. `input` is a list of matchups, `{ matchups, teams }` or a layout from `layoutBracket`.
|
|
30
37
|
* The same input and options always give the same string: no clock, no random number, no counter.
|
|
31
38
|
*/
|
|
32
39
|
export function drawBracket(input, options = {}) {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
40
|
+
const v = voiceOf(options);
|
|
41
|
+
const layout = localizedLayout(isLayout(input) ? input : layoutBracket(input, options), v);
|
|
35
42
|
const theme = options.theme ?? "auto";
|
|
36
43
|
const sizeName = typeof options.size === "string" || options.size === undefined ? options.size ?? "wide" : null;
|
|
37
44
|
const size = sizeName ? SIZES[sizeName] : { width: options.size.width, height: options.size.height ?? null };
|
|
38
45
|
const orientation = options.orientation && options.orientation !== "auto" ? options.orientation : sizeName === "phone" || (size.height !== null && size.width < size.height * 0.8) ? "vertical" : "horizontal";
|
|
39
|
-
const prefix = options.id ?? `tb-${hash(JSON.stringify([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]))}`;
|
|
46
|
+
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]))}`;
|
|
40
47
|
const ctx = { radii: new Set(), symbols: new Map(), prefix, shape: options.markShape ?? "round" };
|
|
41
|
-
const plan = planBracket(layout, options,
|
|
42
|
-
const drawn = plan.boxes.map((b) => drawBox(b, plan, options,
|
|
43
|
-
const title = options.title ?? words.title;
|
|
48
|
+
const plan = planBracket(layout, options, v, size, orientation === "vertical", ctx);
|
|
49
|
+
const drawn = plan.boxes.map((b) => drawBox(b, plan, options, v, ctx)); // draws the marks, so before the defs are written
|
|
50
|
+
const title = options.title ?? v.words.title;
|
|
44
51
|
const desc = describeBracket(layout, options);
|
|
45
52
|
const defs = [
|
|
46
53
|
...[...ctx.radii].sort((a, b) => a - b).map((r) => `<clipPath id="${clipId(prefix, r)}"><circle r="${num(r)}"/></clipPath>`),
|
|
@@ -49,7 +56,7 @@ export function drawBracket(input, options = {}) {
|
|
|
49
56
|
const style = theme === "none" ? "" : `<style>${styleFor(`.${prefix}`, theme)}</style>`;
|
|
50
57
|
const bg = options.background === false ? "" : `<rect class="tb-bg" width="${num(plan.width)}" height="${num(plan.height)}"${theme === "none" ? ' fill="none"' : ""}/>`;
|
|
51
58
|
return [
|
|
52
|
-
`<svg xmlns="http://www.w3.org/2000/svg" class="taisen ${prefix}${options.className ? ` ${esc(options.className)}` : ""}" viewBox="0 0 ${num(plan.width)} ${num(plan.height)}" width="${num(plan.width)}" height="${num(plan.height)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${prefix}-t ${prefix}-d">`,
|
|
59
|
+
`<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(v.locale.code)}" class="taisen ${prefix}${options.className ? ` ${esc(options.className)}` : ""}" viewBox="0 0 ${num(plan.width)} ${num(plan.height)}" width="${num(plan.width)}" height="${num(plan.height)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${prefix}-t ${prefix}-d">`,
|
|
53
60
|
`<title id="${prefix}-t">${esc(title)}</title><desc id="${prefix}-d">${esc(desc)}</desc>`,
|
|
54
61
|
style,
|
|
55
62
|
defs ? `<defs>${defs}</defs>` : "",
|
|
@@ -62,7 +69,7 @@ export function drawBracket(input, options = {}) {
|
|
|
62
69
|
].filter(Boolean).join("\n");
|
|
63
70
|
}
|
|
64
71
|
// ---------------------------------------------------------------------------------------------- the plan
|
|
65
|
-
function planBracket(layout, o,
|
|
72
|
+
function planBracket(layout, o, v, size, vertical, ctx) {
|
|
66
73
|
const pad = vertical ? 14 : 22;
|
|
67
74
|
const showLabels = o.roundLabels ?? true;
|
|
68
75
|
const poster = o.theme === "poster";
|
|
@@ -130,7 +137,11 @@ function planBracket(layout, o, words, size, vertical, ctx) {
|
|
|
130
137
|
const rowH = h / 2;
|
|
131
138
|
const font = clamp(rowH * 0.5, 8.5, 16);
|
|
132
139
|
const md = clamp(rowH * 0.7, 10, 28);
|
|
133
|
-
|
|
140
|
+
// Room for the score, and for the words some boxes carry beside it: a bye, a shoot-out's score.
|
|
141
|
+
const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
|
|
142
|
+
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
143
|
+
const extra = (anyBye ? textWidth(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? textWidth("(00)", Math.max(font - 2, 8)) + 3 : 0);
|
|
144
|
+
const needed = 6 + md + 5 + longest * (font / 13) + 8 + textWidth("00", font + 1) + extra + 14;
|
|
134
145
|
const named = o.detail && o.detail !== "auto" ? o.detail : w >= needed && h >= 36 ? "full" : w >= 76 && h >= 28 ? "compact" : "mark";
|
|
135
146
|
return named === "mark" && o.markShape === "none" ? "compact" : named;
|
|
136
147
|
};
|
|
@@ -181,24 +192,24 @@ function planBracket(layout, o, words, size, vertical, ctx) {
|
|
|
181
192
|
for (const b of sd.boxes) {
|
|
182
193
|
const rect = place(sd.key, b);
|
|
183
194
|
rects.set(b.i, rect);
|
|
184
|
-
boxes.push({ rect, tie: layout.ties[b.i], order: b.order, bye: b.bye, final: false, champion: false });
|
|
195
|
+
boxes.push({ rect, tie: layout.ties[b.i], order: b.order, bye: b.bye, byes: b.byes, final: false, champion: false });
|
|
185
196
|
}
|
|
186
197
|
const midX = (halves ? starts[nc] : starts[nc] ?? 0) + (halves ? centreT : 0) / 2;
|
|
187
198
|
if (halves && layout.final) {
|
|
188
199
|
const fy = treeY0 + layout.final.y * rowStep + rowStep / 2;
|
|
189
200
|
const rect = { x: midX - finalW / 2, y: fy - finalH / 2, w: finalW, h: finalH };
|
|
190
201
|
rects.set(layout.final.i, rect);
|
|
191
|
-
boxes.push({ rect, tie: layout.ties[layout.final.i], order: layout.final.order, bye: null, final: true, champion: champ });
|
|
202
|
+
boxes.push({ rect, tie: layout.ties[layout.final.i], order: layout.final.order, bye: null, byes: [], final: true, champion: champ });
|
|
192
203
|
if (showLabels)
|
|
193
|
-
texts.push(label(roundLabel(layout, layout.rounds, o.roundNames,
|
|
204
|
+
texts.push(label(v, roundLabel(layout, layout.rounds, o.roundNames, v), midX, rect.y - 7, "middle"));
|
|
194
205
|
if (champ)
|
|
195
|
-
extras.push(...championBlock(layout.champion, midX, rect.y - 22 - champH, champMark, trophyH, nameFont, o,
|
|
206
|
+
extras.push(...championBlock(layout.champion, midX, rect.y - 22 - champH, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
196
207
|
if (withThird) {
|
|
197
208
|
const t3 = layout.ties[layout.third];
|
|
198
209
|
const r3 = { x: midX - finalW / 2, y: rect.y + rect.h + 14 + 18, w: finalW, h: boxH };
|
|
199
210
|
rects.set(layout.third, r3);
|
|
200
|
-
texts.push(label(words.thirdPlace, midX, r3.y - 6,
|
|
201
|
-
boxes.push({ rect: r3, tie: t3, order: [0, 1], bye: null, final: false, champion: false });
|
|
211
|
+
texts.push(label(v, v.words.thirdPlace, midX, r3.y - 6, "middle"));
|
|
212
|
+
boxes.push({ rect: r3, tie: t3, order: [0, 1], bye: null, byes: [], final: false, champion: false });
|
|
202
213
|
}
|
|
203
214
|
}
|
|
204
215
|
else if (!halves) {
|
|
@@ -208,17 +219,17 @@ function planBracket(layout, o, words, size, vertical, ctx) {
|
|
|
208
219
|
if (fb && champ) {
|
|
209
220
|
const cx = starts[k] + sizes[k] / 2;
|
|
210
221
|
const fy = rects.get(fi).y + S / 2;
|
|
211
|
-
extras.push(...championBlock(layout.champion, cx, fy - champH / 2, champMark, trophyH, nameFont, o,
|
|
222
|
+
extras.push(...championBlock(layout.champion, cx, fy - champH / 2, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
212
223
|
}
|
|
213
224
|
}
|
|
214
225
|
if (showLabels) {
|
|
215
226
|
for (const c of layout.columns) {
|
|
216
|
-
const name = roundLabel(layout, c, o.roundNames,
|
|
227
|
+
const name = roundLabel(layout, c, o.roundNames, v);
|
|
217
228
|
const ci = layout.columns.indexOf(c);
|
|
218
229
|
const ks = halves ? [slotOfColumn("l", ci), slotOfColumn("r", ci)] : [ci];
|
|
219
230
|
const used = layout.sides.length > 1 || !halves ? (halves ? [0, 1] : [0]) : [0];
|
|
220
231
|
ks.forEach((k, n) => { if (used.includes(n) && (!halves || n < layout.sides.length))
|
|
221
|
-
texts.push(label(name, starts[k] + D / 2, top - 9,
|
|
232
|
+
texts.push(label(v, name, starts[k] + D / 2, top - 9, "middle", D + gapT * 0.8)); });
|
|
222
233
|
}
|
|
223
234
|
}
|
|
224
235
|
if (hasGroups) {
|
|
@@ -227,7 +238,7 @@ function planBracket(layout, o, words, size, vertical, ctx) {
|
|
|
227
238
|
return;
|
|
228
239
|
const ks = layout.columns.map((_, ci) => slotOfColumn(sd.key, ci));
|
|
229
240
|
const x0 = Math.min(...ks.map((k) => starts[k])), x1 = Math.max(...ks.map((k) => starts[k] + D));
|
|
230
|
-
texts.push(`<text class="tb-half" x="${num((x0 + x1) / 2)}" y="${num(top - (showLabels ? 36 : 14))}" font-size="${poster ? 16 : 15}" font-weight="700" letter-spacing=".1em" text-anchor="middle">${esc(poster ? sd.label
|
|
241
|
+
texts.push(`<text class="tb-half" x="${num((x0 + x1) / 2)}" y="${num(top - (showLabels ? 36 : 14))}" font-size="${poster ? 16 : 15}" font-weight="700" letter-spacing=".1em" text-anchor="middle">${esc(poster ? v.upper(sd.label) : sd.label)}</text>`);
|
|
231
242
|
});
|
|
232
243
|
}
|
|
233
244
|
// lines
|
|
@@ -245,9 +256,9 @@ function planBracket(layout, o, words, size, vertical, ctx) {
|
|
|
245
256
|
links.push(`<path class="tb-line" d="M${num(x0)} ${num(y0)}H${num(xm)}V${num(y1)}H${num(x1)}"${lineAttrs(o)}/>`);
|
|
246
257
|
}
|
|
247
258
|
if (bottomThird)
|
|
248
|
-
thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H,
|
|
249
|
-
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OTHER_H,
|
|
250
|
-
texts.push(...headings(o, width, pad, poster));
|
|
259
|
+
thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H, v, boxes, texts);
|
|
260
|
+
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OTHER_H, v, boxes, texts);
|
|
261
|
+
texts.push(...headings(v, o, width, pad, poster));
|
|
251
262
|
return { width, height, boxes, links, texts, extras, detail, vertical };
|
|
252
263
|
}
|
|
253
264
|
// ----- vertical: the rounds are rows. The left half is the top; the final is a band in the middle.
|
|
@@ -280,32 +291,32 @@ function planBracket(layout, o, words, size, vertical, ctx) {
|
|
|
280
291
|
const c = layout.columns.indexOf(b.round);
|
|
281
292
|
const rect = { x: xOf(b.y), y: bandStart(sd.key, c), w: S, h: D };
|
|
282
293
|
rects.set(b.i, rect);
|
|
283
|
-
boxes.push({ rect, tie: layout.ties[b.i], order: b.order, bye: b.bye, final: false, champion: false });
|
|
294
|
+
boxes.push({ rect, tie: layout.ties[b.i], order: b.order, bye: b.bye, byes: b.byes, final: false, champion: false });
|
|
284
295
|
}
|
|
285
296
|
const midX = W0 / 2;
|
|
286
297
|
if (halves && layout.final) {
|
|
287
298
|
const cy0 = topT + bandsPerSide * bandT;
|
|
288
299
|
let y = cy0;
|
|
289
300
|
if (champ)
|
|
290
|
-
extras.push(...championBlock(layout.champion, midX, pad + titleH + 4, champMark, trophyH, nameFont, o,
|
|
301
|
+
extras.push(...championBlock(layout.champion, midX, pad + titleH + 4, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
291
302
|
if (showLabels) {
|
|
292
|
-
texts.push(label(roundLabel(layout, layout.rounds, o.roundNames,
|
|
303
|
+
texts.push(label(v, roundLabel(layout, layout.rounds, o.roundNames, v), midX, y + 12, "middle"));
|
|
293
304
|
y += 16;
|
|
294
305
|
}
|
|
295
306
|
const rect = { x: midX - finalW / 2, y: y + 6, w: finalW, h: finalH };
|
|
296
307
|
rects.set(layout.final.i, rect);
|
|
297
|
-
boxes.push({ rect, tie: layout.ties[layout.final.i], order: layout.final.order, bye: null, final: true, champion: champ });
|
|
308
|
+
boxes.push({ rect, tie: layout.ties[layout.final.i], order: layout.final.order, bye: null, byes: [], final: true, champion: champ });
|
|
298
309
|
}
|
|
299
310
|
else if (!halves && champ && layout.finalIndex !== null) {
|
|
300
311
|
const fr = rects.get(layout.finalIndex);
|
|
301
|
-
extras.push(...championBlock(layout.champion, fr.x + fr.w / 2, fr.y + fr.h + 12, champMark, trophyH, nameFont, o,
|
|
312
|
+
extras.push(...championBlock(layout.champion, fr.x + fr.w / 2, fr.y + fr.h + 12, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
302
313
|
}
|
|
303
314
|
if (showLabels) {
|
|
304
315
|
layout.columns.forEach((c, ci) => {
|
|
305
|
-
const name = roundLabel(layout, c, o.roundNames,
|
|
316
|
+
const name = roundLabel(layout, c, o.roundNames, v);
|
|
306
317
|
const sides = halves ? (layout.sides.length > 1 ? ["l", "r"] : ["l"]) : ["l"];
|
|
307
318
|
for (const sd of sides)
|
|
308
|
-
texts.push(`<text class="tb-round tb-halo" x="${num(pad)}" y="${num(bandStart(sd, ci) - 8)}" font-size="11" font-weight="600" letter-spacing=".08em">${esc(
|
|
319
|
+
texts.push(`<text class="tb-round tb-halo" x="${num(pad)}" y="${num(bandStart(sd, ci) - 8)}" font-size="11" font-weight="600" letter-spacing=".08em">${esc(v.upper(name))}</text>`);
|
|
309
320
|
});
|
|
310
321
|
}
|
|
311
322
|
if (hasGroups) {
|
|
@@ -313,7 +324,7 @@ function planBracket(layout, o, words, size, vertical, ctx) {
|
|
|
313
324
|
if (!sd.label)
|
|
314
325
|
return;
|
|
315
326
|
const y = sd.key === "l" ? pad + titleH + champTop + 16 : totalT - pad - 2;
|
|
316
|
-
texts.push(`<text class="tb-half" x="${num(midX)}" y="${num(y)}" font-size="14" font-weight="700" letter-spacing=".1em" text-anchor="middle">${esc(sd.label
|
|
327
|
+
texts.push(`<text class="tb-half" x="${num(midX)}" y="${num(y)}" font-size="14" font-weight="700" letter-spacing=".1em" text-anchor="middle">${esc(v.upper(sd.label))}</text>`);
|
|
317
328
|
});
|
|
318
329
|
}
|
|
319
330
|
for (const t of layout.ties) {
|
|
@@ -330,9 +341,9 @@ function planBracket(layout, o, words, size, vertical, ctx) {
|
|
|
330
341
|
links.push(`<path class="tb-line" d="M${num(x0c)} ${num(y0)}V${num(ym)}H${num(x1c)}V${num(y1)}"${lineAttrs(o)}/>`);
|
|
331
342
|
}
|
|
332
343
|
if (bottomThird)
|
|
333
|
-
thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, 200), 52,
|
|
334
|
-
othersBlock(layout, others, totalT - pad + thirdBlock + 6, W0, pad, ow, OTHER_H,
|
|
335
|
-
texts.push(...headings(o, W0, pad, poster));
|
|
344
|
+
thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, 200), 52, v, boxes, texts);
|
|
345
|
+
othersBlock(layout, others, totalT - pad + thirdBlock + 6, W0, pad, ow, OTHER_H, v, boxes, texts);
|
|
346
|
+
texts.push(...headings(v, o, W0, pad, poster));
|
|
336
347
|
return { width: W0, height, boxes, links, texts, extras, detail, vertical };
|
|
337
348
|
}
|
|
338
349
|
const OTHER_H = 50;
|
|
@@ -340,16 +351,14 @@ const centreMinLtr = (d) => d * 1.1;
|
|
|
340
351
|
function lineAttrs(o) {
|
|
341
352
|
return o.theme === "none" ? ' fill="none" stroke="currentColor" stroke-opacity=".5"' : "";
|
|
342
353
|
}
|
|
343
|
-
function label(text, x, y,
|
|
344
|
-
|
|
345
|
-
const up = text.toUpperCase();
|
|
346
|
-
return `<text class="tb-round" x="${num(x)}" y="${num(y)}" font-size="11" font-weight="600" letter-spacing=".08em" text-anchor="${anchor}">${esc(fit(up, 11 * 1.18, maxWidth))}</text>`;
|
|
354
|
+
function label(v, text, x, y, anchor, maxWidth = 1e9) {
|
|
355
|
+
return `<text class="tb-round" x="${num(x)}" y="${num(y)}" font-size="11" font-weight="600" letter-spacing=".08em" text-anchor="${anchor}">${esc(fit(v.upper(text), 11 * 1.18, maxWidth))}</text>`;
|
|
347
356
|
}
|
|
348
|
-
function headings(o, width, pad, poster) {
|
|
357
|
+
function headings(v, o, width, pad, poster) {
|
|
349
358
|
const out = [];
|
|
350
359
|
let y = pad + 24;
|
|
351
360
|
if (o.title) {
|
|
352
|
-
const text = poster ? o.title
|
|
361
|
+
const text = poster ? v.upper(o.title) : o.title;
|
|
353
362
|
out.push(`<text class="tb-title" x="${num(width / 2)}" y="${num(y)}" font-size="${poster ? 26 : 24}" font-weight="800" ${poster ? 'letter-spacing=".14em" ' : ""}text-anchor="middle">${esc(text)}</text>`);
|
|
354
363
|
y += poster ? 24 : 22;
|
|
355
364
|
}
|
|
@@ -358,7 +367,7 @@ function headings(o, width, pad, poster) {
|
|
|
358
367
|
return out;
|
|
359
368
|
}
|
|
360
369
|
/** The champion, centred on `cx`, its top at `top`: a trophy if there is one, a large mark, the name and the word. */
|
|
361
|
-
function championBlock(team, cx, top, mark, trophyH, nameFont, o,
|
|
370
|
+
function championBlock(team, cx, top, mark, trophyH, nameFont, o, v, ctx, poster) {
|
|
362
371
|
const out = [];
|
|
363
372
|
let y = top;
|
|
364
373
|
if (o.trophy && trophyH) {
|
|
@@ -375,8 +384,8 @@ function championBlock(team, cx, top, mark, trophyH, nameFont, o, words, ctx, po
|
|
|
375
384
|
if (ctx.shape !== "none")
|
|
376
385
|
out.push(`<g class="tb-champ">${drawMark(team, cx, cy, mark, ctx)}</g>`);
|
|
377
386
|
const nameY = cy + mark / 2 + 8 + nameFont * 0.8;
|
|
378
|
-
out.push(`<text class="tb-name" x="${num(cx)}" y="${num(nameY)}" font-size="${num(nameFont)}" font-weight="800" text-anchor="middle">${esc(poster ?
|
|
379
|
-
out.push(`<text class="tb-champ-label" x="${num(cx)}" y="${num(nameY + 6 + 11)}" font-size="11" font-weight="700" letter-spacing=".16em" text-anchor="middle">${esc(words.champion
|
|
387
|
+
out.push(`<text class="tb-name" x="${num(cx)}" y="${num(nameY)}" font-size="${num(nameFont)}" font-weight="800" text-anchor="middle">${esc(poster ? v.upper(name) : name)}</text>`);
|
|
388
|
+
out.push(`<text class="tb-champ-label" x="${num(cx)}" y="${num(nameY + 6 + 11)}" font-size="11" font-weight="700" letter-spacing=".16em" text-anchor="middle">${esc(v.upper(v.words.champion))}</text>`);
|
|
380
389
|
return out;
|
|
381
390
|
}
|
|
382
391
|
/** The ties that are not part of the tree (they lead nowhere near the final, or are round-robin games), drawn in a grid below it. */
|
|
@@ -389,24 +398,24 @@ function othersHeight(count, width, pad, ow, oh) {
|
|
|
389
398
|
const cols = Math.max(1, Math.floor((width - 2 * pad + 10) / (ow + 10)));
|
|
390
399
|
return 26 + Math.ceil(count / cols) * (oh + 10) + 6;
|
|
391
400
|
}
|
|
392
|
-
function othersBlock(layout, list, y0, width, pad, ow, oh,
|
|
401
|
+
function othersBlock(layout, list, y0, width, pad, ow, oh, v, boxes, texts) {
|
|
393
402
|
if (!list.length)
|
|
394
403
|
return;
|
|
395
404
|
const cols = Math.max(1, Math.floor((width - 2 * pad + 10) / (ow + 10)));
|
|
396
405
|
const x0 = pad + (width - 2 * pad - (Math.min(cols, list.length) * (ow + 10) - 10)) / 2;
|
|
397
|
-
texts.push(label(words.other, width / 2, y0 + 12,
|
|
406
|
+
texts.push(label(v, v.words.other, width / 2, y0 + 12, "middle"));
|
|
398
407
|
list.forEach((i, n) => {
|
|
399
408
|
const rect = { x: x0 + (n % cols) * (ow + 10), y: y0 + 24 + Math.floor(n / cols) * (oh + 10), w: ow, h: oh };
|
|
400
|
-
boxes.push({ rect, tie: layout.ties[i], order: [0, 1], bye: null, final: false, champion: false });
|
|
409
|
+
boxes.push({ rect, tie: layout.ties[i], order: [0, 1], bye: null, byes: [], final: false, champion: false });
|
|
401
410
|
});
|
|
402
411
|
}
|
|
403
412
|
/** The third-place match with its label, centred on `cx`, its label at `y0`. */
|
|
404
|
-
function thirdAtFoot(layout, y0, cx, w, h,
|
|
405
|
-
texts.push(label(words.thirdPlace, cx, y0 + 12,
|
|
406
|
-
boxes.push({ rect: { x: cx - w / 2, y: y0 + 18, w, h }, tie: layout.ties[layout.third], order: [0, 1], bye: null, final: false, champion: false });
|
|
413
|
+
function thirdAtFoot(layout, y0, cx, w, h, v, boxes, texts) {
|
|
414
|
+
texts.push(label(v, v.words.thirdPlace, cx, y0 + 12, "middle"));
|
|
415
|
+
boxes.push({ rect: { x: cx - w / 2, y: y0 + 18, w, h }, tie: layout.ties[layout.third], order: [0, 1], bye: null, byes: [], final: false, champion: false });
|
|
407
416
|
}
|
|
408
417
|
// ---------------------------------------------------------------------------------------------- one box
|
|
409
|
-
function drawBox(p, plan, o,
|
|
418
|
+
function drawBox(p, plan, o, v, ctx) {
|
|
410
419
|
const { rect, tie } = p;
|
|
411
420
|
const { x, y, w, h } = rect;
|
|
412
421
|
const detail = plan.detail;
|
|
@@ -416,7 +425,7 @@ function drawBox(p, plan, o, words, ctx) {
|
|
|
416
425
|
const parts = [];
|
|
417
426
|
parts.push(`<rect class="tb-box" x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" rx="${num(Math.min(7, h / 4))}"${o.theme === "none" ? ' fill="none" stroke="currentColor" stroke-opacity=".4"' : ""}/>`);
|
|
418
427
|
const r = tie.result;
|
|
419
|
-
const notes = noteParts(tie,
|
|
428
|
+
const notes = noteParts(tie, v);
|
|
420
429
|
const noteH = detail !== "mark" && h >= 50 && notes.length ? 13 : 0;
|
|
421
430
|
const rowH = (h - noteH) / 2;
|
|
422
431
|
const rowsOrder = p.order;
|
|
@@ -446,19 +455,25 @@ function drawBox(p, plan, o, words, ctx) {
|
|
|
446
455
|
rowParts.push(drawMark(team, x + 6 + md / 2, cy, md, ctx));
|
|
447
456
|
tx = x + 6 + md + 5;
|
|
448
457
|
}
|
|
449
|
-
const score = scoresOn && r.score ?
|
|
450
|
-
const pensText = scoresOn && r.pens ? `(${r.pens[k]})` : "";
|
|
451
|
-
const bye = p.
|
|
458
|
+
const score = scoresOn && r.score ? v.number(r.score[k]) : "";
|
|
459
|
+
const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
|
|
460
|
+
const bye = p.byes.includes(k);
|
|
452
461
|
const right = x + w - 6;
|
|
453
462
|
const scoreW = score ? textWidth(score, font + 1) : 0;
|
|
454
463
|
const pensW = pensText ? textWidth(pensText, font - 2) + 3 : 0;
|
|
455
|
-
const
|
|
464
|
+
const seed = seedsOn && team?.seed !== undefined ? `${v.number(team.seed)} ` : "";
|
|
465
|
+
const nm = team ? (detail === "full" ? team.name : abbrOf(team, v)) : v.words.tbd;
|
|
466
|
+
// The word for a bye goes beside the score only where the name still fits beside it.
|
|
467
|
+
const byeFits = bye && right - tx - (scoreW + pensW) - (textWidth(v.words.bye, Math.max(font - 2, 8)) + 6) - 6 >= textWidth(`${seed}${nm}`, font);
|
|
468
|
+
const byeText = byeFits ? v.words.bye : "";
|
|
456
469
|
const byeW = byeText ? textWidth(byeText, Math.max(font - 2, 8)) : 0;
|
|
457
470
|
const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
471
|
+
// A name that does not fit is set a little smaller before it is cut.
|
|
472
|
+
let nameFont = font;
|
|
473
|
+
while (nameFont > 9.5 && textWidth(`${seed}${nm}`, nameFont) > avail)
|
|
474
|
+
nameFont -= 0.5;
|
|
475
|
+
const text = fit(`${seed}${nm}`, nameFont, Math.max(avail, 8));
|
|
476
|
+
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>`);
|
|
462
477
|
if (score)
|
|
463
478
|
rowParts.push(`<text class="tb-score" x="${num(right)}" y="${num(cy + (font + 1) * 0.35)}" font-size="${num(font + 1)}" text-anchor="end" font-variant-numeric="tabular-nums">${esc(score)}</text>`);
|
|
464
479
|
if (pensText)
|
|
@@ -474,5 +489,5 @@ function drawBox(p, plan, o, words, ctx) {
|
|
|
474
489
|
}
|
|
475
490
|
}
|
|
476
491
|
const cls2 = `tb-tie${p.final ? " tb-final" : ""}${p.champion ? " tb-has-champ" : ""}`;
|
|
477
|
-
return `<g class="${cls2}"><title>${esc(describeTie(tie,
|
|
492
|
+
return `<g class="${cls2}"><title>${esc(describeTie(tie, v))}</title>${parts.join("")}</g>`;
|
|
478
493
|
}
|
package/dist/rounds.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { BracketLayout, DrawOptions } from "./types.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type Voice } from "./locales.ts";
|
|
3
3
|
/** The name of a round that has `remaining` teams left in it, counting from the final: Final, Semi-finals, Quarter-finals, Round of 16… */
|
|
4
4
|
export declare function roundName(remaining: number): string;
|
|
5
5
|
/** The generic names of `count` rounds, first round first: `roundNamesFor(3)` is Quarter-finals, Semi-finals, Final. */
|
|
6
6
|
export declare function roundNamesFor(count: number): string[];
|
|
7
7
|
/** The name of round `round` (1 is the first) of a layout, from the names given (which may be only the last rounds) or the generic ones. */
|
|
8
|
-
export declare function roundLabel(layout: BracketLayout, round: number, names: DrawOptions["roundNames"],
|
|
8
|
+
export declare function roundLabel(layout: BracketLayout, round: number, names: DrawOptions["roundNames"], v: Voice): string;
|
package/dist/rounds.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fill } from "./
|
|
1
|
+
import { fill } from "./locales.js";
|
|
2
2
|
/** The name of a round that has `remaining` teams left in it, counting from the final: Final, Semi-finals, Quarter-finals, Round of 16… */
|
|
3
3
|
export function roundName(remaining) {
|
|
4
4
|
if (remaining <= 2)
|
|
@@ -14,13 +14,13 @@ export function roundNamesFor(count) {
|
|
|
14
14
|
return Array.from({ length: count }, (_, i) => roundName(2 ** (count - i)));
|
|
15
15
|
}
|
|
16
16
|
/** The name of round `round` (1 is the first) of a layout, from the names given (which may be only the last rounds) or the generic ones. */
|
|
17
|
-
export function roundLabel(layout, round, names,
|
|
17
|
+
export function roundLabel(layout, round, names, v) {
|
|
18
18
|
const list = names ? ("rounds" in names ? names.rounds.map((r) => r.name) : names) : null;
|
|
19
19
|
const from = layout.rounds - round; // 0 for the final
|
|
20
20
|
const named = list?.[list.length - 1 - from];
|
|
21
21
|
if (named)
|
|
22
|
-
return named;
|
|
22
|
+
return v.round(named);
|
|
23
23
|
const generic = 2 ** (from + 1);
|
|
24
24
|
const remaining = layout.rounds > 7 ? 0 : generic;
|
|
25
|
-
return remaining ? roundName(remaining) : fill(words.round, { n: round });
|
|
25
|
+
return remaining ? v.round(roundName(remaining)) : fill(v.words.round, { n: v.number(round) });
|
|
26
26
|
}
|
package/dist/table.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import type { BracketInput, BracketLayout, DrawOptions, Matchup
|
|
2
|
-
export interface TableOptions {
|
|
3
|
-
words?: Partial<Words>;
|
|
4
|
-
roundNames?: DrawOptions["roundNames"];
|
|
1
|
+
import type { BracketInput, BracketLayout, DrawOptions, Matchup } from "./types.ts";
|
|
2
|
+
export interface TableOptions extends Pick<DrawOptions, "locale" | "words" | "teamName" | "roundNames"> {
|
|
5
3
|
/** The table's caption. Default: the summary ("16 teams, 4 rounds"). */
|
|
6
4
|
caption?: string;
|
|
7
5
|
/** A class for the `<table>`. Default `tb-table`. */
|
|
@@ -9,6 +7,7 @@ export interface TableOptions {
|
|
|
9
7
|
}
|
|
10
8
|
/**
|
|
11
9
|
* The bracket as an HTML table, for a reader the drawing does not serve: one row for every tie, round by round, with the
|
|
12
|
-
* two sides, the score, the winner in bold
|
|
10
|
+
* two sides, the score, the winner in bold, the date where the tie has one (written as the language writes dates) and a note.
|
|
11
|
+
* It is a plain, valid `<table>` with a caption and header cells, in the language of `locale`.
|
|
13
12
|
*/
|
|
14
13
|
export declare function bracketTable(input: BracketLayout | readonly Matchup[] | BracketInput, options?: TableOptions): string;
|