@johnmorrisdotca/taisen-brackets 0.5.0 → 0.6.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 +25 -0
- package/NOTICE.md +2 -0
- package/README.md +159 -29
- package/dist/furniture.d.ts +10 -0
- package/dist/furniture.js +29 -0
- package/dist/groups.d.ts +50 -0
- package/dist/groups.js +204 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/layout.js +56 -6
- package/dist/layouts.d.ts +48 -0
- package/dist/layouts.js +388 -0
- package/dist/locales.js +10 -0
- package/dist/marks.d.ts +2 -0
- package/dist/marks.js +1 -0
- package/dist/matchup.js +1 -0
- package/dist/playin.d.ts +8 -0
- package/dist/playin.js +232 -0
- package/dist/render.d.ts +8 -1
- package/dist/render.js +223 -34
- package/dist/seeding.js +4 -2
- package/dist/table.js +6 -3
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +6 -0
- package/dist/types.d.ts +66 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -3
package/dist/groups.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { layoutBracket } from "./layout.js";
|
|
2
|
+
import { fill, voiceOf } from "./locales.js";
|
|
3
|
+
import { abbrOf } from "./matchup.js";
|
|
4
|
+
import { drawMark } from "./marks.js";
|
|
5
|
+
import { bold, headingTexts, titleHeight } from "./furniture.js";
|
|
6
|
+
import { esc, fit, num } from "./text.js";
|
|
7
|
+
const isLayout = (x) => typeof x === "object" && x !== null && "ties" in x && "sides" in x && "rounds" in x;
|
|
8
|
+
/**
|
|
9
|
+
* The tables of the group stage: one for each group, from the round-robin games (`round: 0`) of the ties. A team's group is its `group`; teams with none take the group of the teams they
|
|
10
|
+
* played (the games make groups of the teams they join), and a group the teams do not name is lettered A, B, C. Rows are in order of points, then goal difference, then goals scored, then
|
|
11
|
+
* name (`standings: "record"` orders by wins, then fewest losses).
|
|
12
|
+
*/
|
|
13
|
+
export function groupStandings(input, options = {}) {
|
|
14
|
+
const v = voiceOf(options);
|
|
15
|
+
const layout = isLayout(input) ? input : layoutBracket(input);
|
|
16
|
+
return tablesOf(layout, options.standings ?? "points", v);
|
|
17
|
+
}
|
|
18
|
+
function tablesOf(layout, mode, v) {
|
|
19
|
+
const games = layout.robin.map((i) => layout.ties[i]).filter((t) => t.sides[0] !== null && t.sides[1] !== null);
|
|
20
|
+
// The games join teams into groups.
|
|
21
|
+
const parent = new Map();
|
|
22
|
+
const find = (a) => { let r = a; while (parent.get(r) !== r)
|
|
23
|
+
r = parent.get(r); return r; };
|
|
24
|
+
for (const t of games)
|
|
25
|
+
for (const s of t.sides)
|
|
26
|
+
if (s && !parent.has(s.id))
|
|
27
|
+
parent.set(s.id, s.id);
|
|
28
|
+
for (const t of games)
|
|
29
|
+
parent.set(find(t.sides[0].id), find(t.sides[1].id));
|
|
30
|
+
const named = new Map();
|
|
31
|
+
for (const t of layout.teams)
|
|
32
|
+
if (typeof t.group === "string" && t.group !== "")
|
|
33
|
+
named.set(t.id, t.group);
|
|
34
|
+
// A joined set of teams takes the group its teams name (the most common one).
|
|
35
|
+
const joined = new Map();
|
|
36
|
+
for (const id of parent.keys()) {
|
|
37
|
+
const g = named.get(id);
|
|
38
|
+
if (!g)
|
|
39
|
+
continue;
|
|
40
|
+
const root = find(id);
|
|
41
|
+
const counts = joined.get(root) ?? new Map();
|
|
42
|
+
counts.set(g, (counts.get(g) ?? 0) + 1);
|
|
43
|
+
joined.set(root, counts);
|
|
44
|
+
}
|
|
45
|
+
const keyOf = (team) => {
|
|
46
|
+
const own = named.get(team.id);
|
|
47
|
+
if (own)
|
|
48
|
+
return own;
|
|
49
|
+
if (!parent.has(team.id))
|
|
50
|
+
return null;
|
|
51
|
+
const counts = joined.get(find(team.id));
|
|
52
|
+
if (counts)
|
|
53
|
+
return [...counts].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))[0][0];
|
|
54
|
+
return `#${String(find(team.id))}`;
|
|
55
|
+
};
|
|
56
|
+
const byKey = new Map();
|
|
57
|
+
for (const team of layout.teams) {
|
|
58
|
+
const key = keyOf(team);
|
|
59
|
+
if (key === null)
|
|
60
|
+
continue;
|
|
61
|
+
byKey.set(key, [...(byKey.get(key) ?? []), team]);
|
|
62
|
+
}
|
|
63
|
+
const order = [...byKey.keys()].sort((a, b) => (a.startsWith("#") !== b.startsWith("#") ? (a.startsWith("#") ? 1 : -1) : a.localeCompare(b, "en", { numeric: true })));
|
|
64
|
+
let lettered = 0;
|
|
65
|
+
return order.map((key) => {
|
|
66
|
+
const members = byKey.get(key);
|
|
67
|
+
const ids = new Set(members.map((t) => t.id));
|
|
68
|
+
const rows = members.map((team) => ({ team, played: 0, won: 0, drawn: 0, lost: 0, goalsFor: 0, goalsAgainst: 0, diff: 0, points: 0 }));
|
|
69
|
+
const rowOf = new Map(rows.map((r) => [r.team.id, r]));
|
|
70
|
+
for (const t of games) {
|
|
71
|
+
if (!ids.has(t.sides[0].id) || !ids.has(t.sides[1].id) || t.result.score === null)
|
|
72
|
+
continue;
|
|
73
|
+
const [a, b] = t.result.score;
|
|
74
|
+
const ra = rowOf.get(t.sides[0].id), rb = rowOf.get(t.sides[1].id);
|
|
75
|
+
for (const [r, f, g] of [[ra, a, b], [rb, b, a]]) {
|
|
76
|
+
r.played++;
|
|
77
|
+
r.goalsFor += f;
|
|
78
|
+
r.goalsAgainst += g;
|
|
79
|
+
if (f > g) {
|
|
80
|
+
r.won++;
|
|
81
|
+
r.points += 3;
|
|
82
|
+
}
|
|
83
|
+
else if (f < g)
|
|
84
|
+
r.lost++;
|
|
85
|
+
else {
|
|
86
|
+
r.drawn++;
|
|
87
|
+
r.points += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const r of rows)
|
|
92
|
+
r.diff = r.goalsFor - r.goalsAgainst;
|
|
93
|
+
const byName = (x, y) => (x.team.name < y.team.name ? -1 : x.team.name > y.team.name ? 1 : 0);
|
|
94
|
+
rows.sort(mode === "record" ? (x, y) => y.won - x.won || x.lost - y.lost || byName(x, y) : (x, y) => y.points - x.points || y.diff - x.diff || y.goalsFor - x.goalsFor || byName(x, y));
|
|
95
|
+
const letter = String.fromCharCode(65 + lettered);
|
|
96
|
+
const auto = key.startsWith("#");
|
|
97
|
+
if (auto)
|
|
98
|
+
lettered++;
|
|
99
|
+
const name = auto ? fill(v.words.groupName, { n: letter }) : /^[A-Za-z0-9]$/.test(key) ? fill(v.words.groupName, { n: key.toUpperCase() }) : key;
|
|
100
|
+
return { key: auto ? letter : key, name, rows };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** The table of the groups as HTML, for a reader the drawing does not serve: one `<table>` a group, its caption the group's name, a row a team. */
|
|
104
|
+
export function groupsTable(input, options = {}) {
|
|
105
|
+
const v = voiceOf(options);
|
|
106
|
+
const w = v.words;
|
|
107
|
+
const mode = options.standings ?? "points";
|
|
108
|
+
const layout = isLayout(input) ? input : layoutBracket(input);
|
|
109
|
+
const heads = mode === "points" ? [w.colPlayed, w.colWon, w.colDrawn, w.colLost, w.colDiff, w.colPoints] : [w.colWon, w.colLost];
|
|
110
|
+
return tablesOf(layout, mode, v).map((g) => [
|
|
111
|
+
`<table class="${esc(options.className ?? "tb-table")}" lang="${esc(v.locale.code)}">`,
|
|
112
|
+
`<caption>${esc(g.name)}</caption>`,
|
|
113
|
+
`<thead><tr><th scope="col">${esc(w.columnTie)}</th>${heads.map((h) => `<th scope="col">${esc(h)}</th>`).join("")}</tr></thead>`,
|
|
114
|
+
`<tbody>${g.rows.map((r) => `<tr><th scope="row">${esc(v.team(r.team))}</th>${cells(r, mode, v).map((c) => `<td>${esc(c)}</td>`).join("")}</tr>`).join("")}</tbody>`,
|
|
115
|
+
"</table>",
|
|
116
|
+
].join("\n")).join("\n");
|
|
117
|
+
}
|
|
118
|
+
const signed = (n, v) => (n > 0 ? `+${v.number(n)}` : v.number(n));
|
|
119
|
+
const cells = (r, mode, v) => mode === "points" ? [v.number(r.played), v.number(r.won), v.number(r.drawn), v.number(r.lost), signed(r.diff, v), v.number(r.points)] : [v.number(r.won), v.number(r.lost)];
|
|
120
|
+
/** Draws the group tables as cards in a grid. */
|
|
121
|
+
export function drawGroups(layout, o, v, size, ctx, road, linked) {
|
|
122
|
+
void linked;
|
|
123
|
+
const mode = o.standings ?? "points";
|
|
124
|
+
const tables = tablesOf(layout, mode, v);
|
|
125
|
+
if (tables.length === 0)
|
|
126
|
+
throw new RangeError("taisen-brackets: the groups layout needs round-robin games (round: 0) or teams with a group");
|
|
127
|
+
const w = v.words;
|
|
128
|
+
const W = size.width;
|
|
129
|
+
const pad = W < 500 ? 14 : 22;
|
|
130
|
+
const heads = mode === "points" ? [w.colPlayed, w.colWon, w.colDrawn, w.colLost, w.colDiff, w.colPoints] : [w.colWon, w.colLost];
|
|
131
|
+
const colW = mode === "points" ? (W < 500 ? 31 : 36) : 44;
|
|
132
|
+
const colsW = heads.length * colW;
|
|
133
|
+
const gap = 14;
|
|
134
|
+
const rowH = road ? 34 : 30;
|
|
135
|
+
const maxRows = Math.max(...tables.map((t) => t.rows.length));
|
|
136
|
+
const tileH = 28;
|
|
137
|
+
const headH = 18;
|
|
138
|
+
const cardH = 10 + tileH + 6 + headH + maxRows * rowH + 10;
|
|
139
|
+
const minCard = colsW + 150;
|
|
140
|
+
const avail = W - 2 * pad;
|
|
141
|
+
const maxPer = Math.max(1, Math.min(tables.length, Math.floor((avail + gap) / (minCard + gap))));
|
|
142
|
+
const lines = Math.ceil(tables.length / maxPer);
|
|
143
|
+
const per = Math.ceil(tables.length / lines);
|
|
144
|
+
const cardW = Math.min(400, (avail - gap * (per - 1)) / per);
|
|
145
|
+
const titleH = titleHeight(o);
|
|
146
|
+
const top0 = pad + titleH;
|
|
147
|
+
const content = lines * cardH + (lines - 1) * gap;
|
|
148
|
+
const height = Math.max(size.height ?? 0, top0 + content + pad);
|
|
149
|
+
const y00 = top0 + Math.max(0, (height - top0 - pad - content) / 2);
|
|
150
|
+
const md = clamp(rowH - 9, 14, 28);
|
|
151
|
+
const qualify = Math.max(0, Math.floor(o.qualify ?? 0));
|
|
152
|
+
const body = [];
|
|
153
|
+
const descParts = [];
|
|
154
|
+
tables.forEach((g, n) => {
|
|
155
|
+
const line = Math.floor(n / per);
|
|
156
|
+
const inLine = Math.min(per, tables.length - line * per);
|
|
157
|
+
const x = pad + (avail - (inLine * cardW + (inLine - 1) * gap)) / 2 + (n % per) * (cardW + gap);
|
|
158
|
+
const y = y00 + line * (cardH + gap);
|
|
159
|
+
const parts = [];
|
|
160
|
+
parts.push(`<rect class="tb-box tb-gcard" x="${num(x)}" y="${num(y)}" width="${num(cardW)}" height="${num(cardH)}" rx="10"${o.theme === "none" ? ' fill="none" stroke="currentColor" stroke-opacity=".4"' : ""}/>`);
|
|
161
|
+
parts.push(`<rect class="tb-gtile" x="${num(x + 8)}" y="${num(y + 10)}" width="${num(cardW - 16)}" height="${tileH}" rx="7"/>`);
|
|
162
|
+
parts.push(`<text class="tb-gtile-t" x="${num(x + cardW / 2)}" y="${num(y + 10 + tileH / 2 + 5)}" font-size="14" letter-spacing=".06em" text-anchor="middle">${esc(fit(o.theme === "poster" ? v.upper(g.name) : g.name, 14, cardW - 36, (t, s) => bold(t, s) * 1.06))}</text>`);
|
|
163
|
+
const headY = y + 10 + tileH + 6;
|
|
164
|
+
const colsX = x + cardW - 10 - colsW;
|
|
165
|
+
heads.forEach((h, i) => parts.push(`<text class="tb-ghead" x="${num(colsX + i * colW + colW / 2)}" y="${num(headY + 12)}" font-size="10" text-anchor="middle">${esc(h)}</text>`));
|
|
166
|
+
const rows0 = headY + headH;
|
|
167
|
+
g.rows.forEach((r, i) => {
|
|
168
|
+
const ry = rows0 + i * rowH;
|
|
169
|
+
const cy = ry + rowH / 2;
|
|
170
|
+
const out = i < qualify;
|
|
171
|
+
if (i > 0)
|
|
172
|
+
parts.push(`<line class="tb-gsep" x1="${num(x + 10)}" x2="${num(x + cardW - 10)}" y1="${num(ry)}" y2="${num(ry)}"/>`);
|
|
173
|
+
if (out)
|
|
174
|
+
parts.push(`<rect class="tb-qual" x="${num(x + 6)}" y="${num(ry + 1)}" width="${num(cardW - 12)}" height="${num(rowH - 2)}" rx="6"/>`);
|
|
175
|
+
const row = [];
|
|
176
|
+
let nx = x + 12;
|
|
177
|
+
if (ctx.shape !== "none") {
|
|
178
|
+
row.push(drawMark(r.team, nx + md / 2, cy, md, ctx));
|
|
179
|
+
nx += md + 8;
|
|
180
|
+
}
|
|
181
|
+
const room = colsX - 6 - nx;
|
|
182
|
+
let nf = road ? 14 : 13;
|
|
183
|
+
const full = r.team.name;
|
|
184
|
+
const abbr = abbrOf(r.team, v);
|
|
185
|
+
let label = full;
|
|
186
|
+
while (nf > 10 && bold(label, nf) > room)
|
|
187
|
+
nf -= 0.5;
|
|
188
|
+
if (bold(label, nf) > room) {
|
|
189
|
+
label = abbr;
|
|
190
|
+
nf = road ? 14 : 13;
|
|
191
|
+
while (nf > 10 && bold(label, nf) > room)
|
|
192
|
+
nf -= 0.5;
|
|
193
|
+
}
|
|
194
|
+
row.push(`<text class="tb-gname" x="${num(nx)}" y="${num(cy + nf * 0.35)}" font-size="${num(nf)}">${esc(fit(label, nf, Math.max(room, 8), bold))}</text>`);
|
|
195
|
+
cells(r, mode, v).forEach((c, k) => row.push(`<text class="tb-gcell${k === heads.length - 1 ? " tb-gcell-p" : ""}" x="${num(colsX + k * colW + colW / 2)}" y="${num(cy + 4.2)}" font-size="12.5" text-anchor="middle">${esc(c)}</text>`));
|
|
196
|
+
parts.push(`<g class="tb-grow${out ? " tb-qualified" : ""}">${row.join("")}</g>`);
|
|
197
|
+
});
|
|
198
|
+
const summary = g.rows.map((r) => `${v.team(r.team)} ${cells(r, mode, v).join(" ")}`).join(w.noteJoin);
|
|
199
|
+
descParts.push(`${g.name}: ${summary}`);
|
|
200
|
+
body.push(`<g class="tb-group"><title>${esc(`${g.name}: ${summary}`)}</title>${parts.join("")}</g>`);
|
|
201
|
+
});
|
|
202
|
+
return { width: W, height, body, texts: headingTexts(v, o, W, pad), desc: `${descParts.join(w.sep)}${w.end}` };
|
|
203
|
+
}
|
|
204
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from "./layout-entry.ts";
|
|
2
|
-
export { drawBracket, describeBracket, localizedLayout, SIZES } from "./render.ts";
|
|
2
|
+
export { drawBracket, describeBracket, localizedLayout, bracketStyle, SIZES, LAYOUTS } from "./render.ts";
|
|
3
|
+
export { groupStandings, groupsTable } from "./groups.ts";
|
|
4
|
+
export type { GroupRow, GroupTable } from "./groups.ts";
|
|
3
5
|
export { bracketTable } from "./table.ts";
|
|
4
6
|
export type { TableOptions } from "./table.ts";
|
|
5
7
|
export { roadToFinal } from "./road.ts";
|
|
6
8
|
export type { RoadChart, RoadDecided, RoadFinal, RoadFinalist, RoadGroupMatch, RoadInput, RoadMatch, RoadOptions, RoadRecord, RoadScore, RoadSize, RoadTeam, RoadTotals } from "./road.ts";
|
|
7
|
-
export { THEMES, ROAD_THEMES } from "./theme.ts";
|
|
9
|
+
export { THEMES, ROAD_THEMES, CONFERENCE_COLORS } from "./theme.ts";
|
|
8
10
|
export type { Palette, RoadPalette } from "./theme.ts";
|
|
9
11
|
export { cleanSvg, safeUrl, safeHref, isEmblemId } from "./marks.ts";
|
|
10
12
|
export { abbreviate } from "./text.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export * from "./layout-entry.js";
|
|
2
|
-
export { drawBracket, describeBracket, localizedLayout, SIZES } from "./render.js";
|
|
2
|
+
export { drawBracket, describeBracket, localizedLayout, bracketStyle, SIZES, LAYOUTS } from "./render.js";
|
|
3
|
+
export { groupStandings, groupsTable } from "./groups.js";
|
|
3
4
|
export { bracketTable } from "./table.js";
|
|
4
5
|
export { roadToFinal } from "./road.js";
|
|
5
|
-
export { THEMES, ROAD_THEMES } from "./theme.js";
|
|
6
|
+
export { THEMES, ROAD_THEMES, CONFERENCE_COLORS } from "./theme.js";
|
|
6
7
|
export { cleanSvg, safeUrl, safeHref, isEmblemId } from "./marks.js";
|
|
7
8
|
export { abbreviate } from "./text.js";
|
|
8
9
|
export { BACKGROUND_PRESETS, FAMILY_BACKGROUNDS_VERSION, backgroundSvg, contrastRatio } from "./family-backgrounds.js";
|
package/dist/layout.js
CHANGED
|
@@ -17,10 +17,52 @@ export function layoutBracket(input, options = {}) {
|
|
|
17
17
|
throw new Error(`taisen-brackets: two matchups have the id ${JSON.stringify(t.id)}`);
|
|
18
18
|
byId.set(t.id, t.index);
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
20
|
+
// A play-in game is outside the tree; so are a third-place match and a round-robin game.
|
|
21
|
+
const playIns = ties.filter((t) => matchups[t.index].stage === "play-in").map((t) => t.index);
|
|
22
|
+
const isPlayIn = new Set(playIns);
|
|
23
|
+
const thirds = ties.filter((t) => t.third && !isPlayIn.has(t.index)).map((t) => t.index);
|
|
24
|
+
const robin = ties.filter((t) => !t.third && !isPlayIn.has(t.index) && matchups[t.index].round === 0).map((t) => t.index);
|
|
25
|
+
const inTree = ties.filter((t) => !t.third && !isPlayIn.has(t.index) && matchups[t.index].round !== 0).map((t) => t.index);
|
|
23
26
|
const isTree = new Set(inTree);
|
|
27
|
+
// Where each side comes from, when `feeds` says so with more than a tie's winner (`{ tie, result }`), or names a play-in game: those edges are not the tree's.
|
|
28
|
+
const feedOf = (f) => (typeof f === "string" ? { id: f, result: "winner" } : { id: f.tie, result: f.result });
|
|
29
|
+
const from = new Map();
|
|
30
|
+
for (const t of ties) {
|
|
31
|
+
const feeds = matchups[t.index].feeds;
|
|
32
|
+
if (!feeds)
|
|
33
|
+
continue;
|
|
34
|
+
const sources = feeds.map((f) => {
|
|
35
|
+
if (f == null)
|
|
36
|
+
return null;
|
|
37
|
+
const { id, result } = feedOf(f);
|
|
38
|
+
if (result !== "winner" && result !== "loser")
|
|
39
|
+
throw new Error(`taisen-brackets: matchup ${JSON.stringify(t.id)} is fed by the ${JSON.stringify(String(result))} of ${JSON.stringify(id)}: a feed's result is "winner" or "loser"`);
|
|
40
|
+
const c = byId.get(id);
|
|
41
|
+
if (c === undefined)
|
|
42
|
+
throw new Error(`taisen-brackets: matchup ${JSON.stringify(t.id)} is fed by ${JSON.stringify(id)}, which is not a matchup`);
|
|
43
|
+
if (c === t.index)
|
|
44
|
+
throw new Error(`taisen-brackets: matchup ${JSON.stringify(t.id)} is fed by itself`);
|
|
45
|
+
return { tie: c, result };
|
|
46
|
+
});
|
|
47
|
+
const special = isPlayIn.has(t.index) || feeds.some((f) => f != null && (typeof f !== "string" || isPlayIn.has(byId.get(f) ?? -1)));
|
|
48
|
+
if (special)
|
|
49
|
+
from.set(t.index, sources);
|
|
50
|
+
}
|
|
51
|
+
// A tie cannot be fed, through winners or losers, by a tie it feeds.
|
|
52
|
+
const state = new Map();
|
|
53
|
+
const visit = (i, trail) => {
|
|
54
|
+
if (state.get(i) === 2)
|
|
55
|
+
return;
|
|
56
|
+
if (state.get(i) === 1)
|
|
57
|
+
throw new Error(`taisen-brackets: matchups feed each other in a circle: ${[...trail.slice(trail.indexOf(i)), i].map((k) => JSON.stringify(ties[k].id)).join(" -> ")}`);
|
|
58
|
+
state.set(i, 1);
|
|
59
|
+
for (const src of matchups[i].feeds ?? [])
|
|
60
|
+
if (src != null)
|
|
61
|
+
visit(byId.get(feedOf(src).id) ?? i, [...trail, i]);
|
|
62
|
+
state.set(i, 2);
|
|
63
|
+
};
|
|
64
|
+
for (const t of ties)
|
|
65
|
+
visit(t.index, []);
|
|
24
66
|
const winnerId = (i) => (ties[i].winner === null ? null : ties[i].sides[ties[i].winner]?.id ?? null);
|
|
25
67
|
const sideIdOf = (i, k) => ties[i].sides[k]?.id ?? null;
|
|
26
68
|
// Explicit feeds: the tie named for side k is a child of this one.
|
|
@@ -34,10 +76,14 @@ export function layoutBracket(input, options = {}) {
|
|
|
34
76
|
continue;
|
|
35
77
|
explicit.add(j);
|
|
36
78
|
const list = [];
|
|
37
|
-
feeds.forEach((
|
|
38
|
-
if (
|
|
79
|
+
feeds.forEach((feed, k) => {
|
|
80
|
+
if (feed == null)
|
|
39
81
|
return;
|
|
82
|
+
const { id, result } = feedOf(feed);
|
|
40
83
|
const c = byId.get(id);
|
|
84
|
+
// A loser, or a play-in game's winner, goes where `from` says; it takes no part in the tree.
|
|
85
|
+
if (result === "loser" || (c !== undefined && isPlayIn.has(c)))
|
|
86
|
+
return;
|
|
41
87
|
if (c === undefined)
|
|
42
88
|
throw new Error(`taisen-brackets: matchup ${JSON.stringify(ties[j].id)} is fed by ${JSON.stringify(id)}, which is not a matchup`);
|
|
43
89
|
if (!isTree.has(c))
|
|
@@ -121,6 +167,10 @@ export function layoutBracket(input, options = {}) {
|
|
|
121
167
|
ties[i] = { ...ties[i], round: round[i] };
|
|
122
168
|
for (const i of robin)
|
|
123
169
|
ties[i] = { ...ties[i], round: 0 };
|
|
170
|
+
for (const i of playIns)
|
|
171
|
+
ties[i] = { ...ties[i], round: 0, playIn: true };
|
|
172
|
+
for (const [i, sources] of from)
|
|
173
|
+
ties[i] = { ...ties[i], from: sources };
|
|
124
174
|
const place = new Array(n).fill("o");
|
|
125
175
|
const subtree = (i) => [i, ...(children.get(i) ?? []).flatMap(subtree)];
|
|
126
176
|
const groupOf = (t) => t?.group ?? null;
|
|
@@ -265,7 +315,7 @@ export function layoutBracket(input, options = {}) {
|
|
|
265
315
|
return {
|
|
266
316
|
ties, shape, sides, final, finalIndex: fin, columns, rounds,
|
|
267
317
|
rows: Math.max(1, ...sides.map((s) => s.rows)),
|
|
268
|
-
other, robin, third: thirds[0] ?? null, place, parent, champion,
|
|
318
|
+
other, robin, playIn: playIns, third: thirds[0] ?? null, place, parent, champion,
|
|
269
319
|
teams: [...teamMap.values()],
|
|
270
320
|
};
|
|
271
321
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DrawOptions, Place, Tie } from "./types.ts";
|
|
2
|
+
import type { Voice } from "./locales.ts";
|
|
3
|
+
import { type MarkContext } from "./marks.ts";
|
|
4
|
+
export type VariantKind = "rows" | "shields" | "update" | "columns" | "series";
|
|
5
|
+
/** What one box is drawn from. */
|
|
6
|
+
export interface PaintEnv {
|
|
7
|
+
kind: VariantKind;
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
w: number;
|
|
11
|
+
h: number;
|
|
12
|
+
tie: Tie;
|
|
13
|
+
/** Which side is drawn on top, then which below. */
|
|
14
|
+
order: readonly [0 | 1, 0 | 1];
|
|
15
|
+
/** The half of the tree the box is in: `l`, `r`, `f` the final, `o` elsewhere. */
|
|
16
|
+
side: Place;
|
|
17
|
+
final: boolean;
|
|
18
|
+
road: boolean;
|
|
19
|
+
/** How much a box says: `full` names, `compact` abbreviations, `mark` marks alone. */
|
|
20
|
+
detail: "full" | "compact" | "mark";
|
|
21
|
+
vertical: boolean;
|
|
22
|
+
/** Seed tags are drawn: the seed, the tag or the tie's own. */
|
|
23
|
+
tags: boolean;
|
|
24
|
+
/** A team's three letters under its mark where a box is marks alone. */
|
|
25
|
+
codes: boolean;
|
|
26
|
+
scoresOn: boolean;
|
|
27
|
+
v: Voice;
|
|
28
|
+
ctx: MarkContext;
|
|
29
|
+
o: DrawOptions;
|
|
30
|
+
/** The class of a team's group: won, lost or pending. */
|
|
31
|
+
cls: (k: 0 | 1) => string;
|
|
32
|
+
/** Wraps a team's row in its link, where the drawing has one. */
|
|
33
|
+
link: (k: 0 | 1, inner: string, x: number, y: number, w: number, h: number) => string;
|
|
34
|
+
/** The seed tag of a side (`""` for none). */
|
|
35
|
+
tagOf: (tie: Tie, k: 0 | 1) => string;
|
|
36
|
+
/** The widest name and the widest three-letter name of the whole drawing, in ems, so that every box makes the same choice about what fits. */
|
|
37
|
+
em: {
|
|
38
|
+
full: number;
|
|
39
|
+
abbr: number;
|
|
40
|
+
tag: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** The shield's outline as a path, `size` across and as high, its top left at (x, y). */
|
|
44
|
+
export declare function shieldPath(x: number, y: number, size: number): string;
|
|
45
|
+
/** The parts of one box, after its own `<rect class="tb-box">`. */
|
|
46
|
+
export declare function paintVariant(e: PaintEnv): string[];
|
|
47
|
+
/** The width a one-line series needs: "1 BOS 4–0 MIA 8" set at `size`. */
|
|
48
|
+
export declare function seriesLineWidth(tie: Tie, v: Voice, size: number, scoresOn: boolean): number;
|