@johnmorrisdotca/taisen-brackets 0.2.0 → 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 CHANGED
@@ -6,6 +6,17 @@ All notable changes to this project are written here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] - 2026-10-14
10
+
11
+ ### Added
12
+
13
+ - **MLS**: `playoffFormat("mls", year)` for the format since 2023 (18 teams, nine a conference; a wild card match, a best-of-three round one, then single matches to the MLS Cup; seven teams of each conference skip the wild card match), checked against English Wikipedia, with the round names in all five languages. A demo example, the 2024 MLS Cup Playoffs, read from spxn.com's MLS data (openfootball, CC0, checked against English Wikipedia, CC BY-SA 4.0), with a test that pins its champion (LA Galaxy) and final (2–1) and a picture.
14
+ - `LayoutBox.byes`: every side that skipped the earlier rounds, which includes both sides of a later-round tie that no tie feeds (MLS's round one); each such side says "bye" beside its score.
15
+
16
+ ### Fixed
17
+
18
+ - A series counts a game level at the end of play as won by the side that won the shoot-out (MLS's best-of-three round one), so its score and winner are right.
19
+
9
20
  ## [0.2.0] - 2026-10-14
10
21
 
11
22
  ### Added
@@ -41,7 +52,8 @@ First version.
41
52
  - A demo with real results read from spxn.com (the NHL 2024 playoffs, the World Cups of 2022, 1938 and 2026, the MLB 2022 postseason; sources and terms in NOTICE.md), two made-up examples (two-legged ties, a 32-team field) and a flags-only poster of the 2026 World Cup, in a playground that changes the size, look and shape.
42
53
  - Playoff formats checked against English Wikipedia, each era citing its articles (docs/formats.md). The year is the year the playoffs are played in.
43
54
 
44
- [Unreleased]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.2.0...HEAD
55
+ [Unreleased]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.2.1...HEAD
56
+ [0.2.1]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.2.0...v0.2.1
45
57
  [0.2.0]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.1.1...v0.2.0
46
58
  [0.1.1]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.1.0...v0.1.1
47
59
  [0.1.0]: https://github.com/johnmorrisdotca/taisen-brackets/releases/tag/v0.1.0
package/NOTICE.md CHANGED
@@ -9,6 +9,7 @@ The code of this package is under the MIT licence (see [LICENSE](LICENSE)). The
9
9
  | The demo's World Cup results of 2026 (`demo/data/worldcup-2026.js`) | openfootball/worldcup, https://github.com/openfootball/worldcup, read through spxn.com | CC0 |
10
10
  | The demo's NHL results (`demo/data/nhl-2024.js`, `demo/data/nhl-1984.js`) | Every game of the 2024 and the 1984 Stanley Cup playoffs and each team's conference, from the NHL ("NHL", as spxn.com credits it), read through spxn.com | Facts; credited to the NHL |
11
11
  | The demo's NBA results (`demo/data/nba-2024.js`) | Every game of the 2024 NBA playoffs and each team's conference, from English Wikipedia (https://en.wikipedia.org/wiki/2024_NBA_playoffs), read through spxn.com, which holds them as sourced from Wikipedia | **CC BY-SA 4.0** (https://creativecommons.org/licenses/by-sa/4.0/legalcode). That file is an adaptation and is shared under the same licence, with this credit; the pictures of the NBA bracket carry it too |
12
+ | The demo's MLS results (`demo/data/mls-2024.js`) | Every match of the 2024 MLS Cup Playoffs and each club's conference, read through spxn.com, whose MLS data is from openfootball/world (CC0, https://github.com/openfootball/world) checked against English Wikipedia (https://en.wikipedia.org/wiki/2024_MLS_Cup_Playoffs) | **CC BY-SA 4.0** (Wikipedia is in the mix, so the file is an adaptation shared under the same licence, with this credit); openfootball's part is CC0; the pictures of the MLS bracket carry the same licence |
12
13
  | The demo's NFL results (`demo/data/nfl-2024.js`) | Every game of the playoffs of the 2024 NFL season (January and February 2025) and each team's conference, from nflverse (https://github.com/nflverse/nflverse-data), read through spxn.com | CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), credited to nflverse |
13
14
  | The demo's CFL results (`demo/data/cfl-2024.js`) | Every game of the 2024 CFL playoffs and each team's division, from English Wikipedia (https://en.wikipedia.org/wiki/2024_CFL_season), read through spxn.com | **CC BY-SA 4.0**, as for the NBA file above; the pictures of the CFL bracket carry it too |
14
15
  | The demo's MLB results (`demo/data/mlb-2022.js`) | Every game of the 2022 MLB postseason, read through spxn.com. The information used here was obtained free of charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at www.retrosheet.org | Retrosheet's notice, kept as it asks |
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <h1 align="center">Taisen Brackets <sub>対戦</sub></h1>
2
2
 
3
3
  <p align="center"><strong>Knockout brackets as SVG, from a list of ties.</strong><br>
4
- Give it the games, series, two-legged ties or replays of a tournament and it works out the rounds, the halves, the byes and who feeds whom, then draws the bracket: two halves that meet at the final, or one tree left to right, wide, square or tall for a phone, in light, dark or a gold-on-navy poster look, with each team's logo or round flag, the champion in the centre, and a table for readers the drawing does not serve. NHL, NBA, MLB, NFL, CFL and World Cup formats by era, in English, French, Japanese, Spanish and German. One function, no dependencies.</p>
4
+ Give it the games, series, two-legged ties or replays of a tournament and it works out the rounds, the halves, the byes and who feeds whom, then draws the bracket: two halves that meet at the final, or one tree left to right, wide, square or tall for a phone, in light, dark or a gold-on-navy poster look, with each team's logo or round flag, the champion in the centre, and a table for readers the drawing does not serve. NHL, NBA, MLB, NFL, CFL, MLS and World Cup formats by era, in English, French, Japanese, Spanish and German. One function, no dependencies.</p>
5
5
 
6
6
  <p align="center">
7
7
  <a href="https://github.com/johnmorrisdotca/taisen-brackets/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/johnmorrisdotca/taisen-brackets/actions/workflows/ci.yml/badge.svg"></a>
@@ -51,6 +51,11 @@ One layout draws them all, each from real results: the demo has a picker for the
51
51
  <td align="center" valign="top"><img alt="The 1938 World Cup: replays." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/replays-desk-light.png" width="360"><br><em>World Cup 1938, replays</em></td>
52
52
  <td align="center" valign="top"><img alt="The 2026 World Cup as a flags-only poster, in Japanese." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/poster-desk-ja.png" width="360"><br><em>World Cup 2026 poster, in Japanese</em></td>
53
53
  </tr>
54
+ <tr>
55
+ <td align="center" valign="top"><img alt="The 2024 MLS Cup Playoffs: a wild card match, a best-of-three round one, then single matches to the MLS Cup, LA Galaxy as champion." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/mls-desk-light.png" width="360"><br><em>MLS 2024, best of three</em></td>
56
+ <td></td>
57
+ <td></td>
58
+ </tr>
54
59
  </table>
55
60
 
56
61
  ## In 30 seconds
@@ -90,7 +95,7 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
90
95
  - **Team marks you supply**: a logo address or inline SVG, drawn round or square; a team with none is its initials on a colour. Round flags from [Hata](https://github.com/johnmorrisdotca/hata) go in as they are.
91
96
  - **The champion in the centre**, with a slot for your trophy.
92
97
  - **Accessible**: `role="img"` with a title and a description, a tooltip sentence on every tie, and `bracketTable`.
93
- - **Formats by era**: NHL, NBA, MLB, NFL, CFL and FIFA World Cup round names, series lengths and byes.
98
+ - **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
94
99
  - **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.
95
100
  - **Deterministic**: no clock, no random number, no counter; the same input gives the same string, and ids come from a hash of the input.
96
101
  - **No dependencies**, ESM with types, about 40 kB packed.
@@ -321,7 +326,7 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
321
326
  - `singleElimination(teams, options?)`, `advanceWinners(matchups)`, `standardSeeds(size)`: brackets for any number of teams.
322
327
  - `matchupsFromGames(games, options)`: matchups from a flat list of games.
323
328
  - `resolveMatchup`, `collectTeams`, `describeTie`, `noteParts`, `noteLong`: one tie's result, the teams of a bracket, a tie in a sentence.
324
- - `playoffFormat(league, year)`, `playoffEras(league)`, `LEAGUES`: the formats by era (`nhl`, `nba`, `mlb`, `nfl`, `cfl`, `worldcup`).
329
+ - `playoffFormat(league, year)`, `playoffEras(league)`, `LEAGUES`: the formats by era (`nhl`, `nba`, `mlb`, `nfl`, `cfl`, `mls`, `worldcup`).
325
330
  - `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
326
331
  - `LOCALES`, `LOCALE_CODES`, `defineLocale`, `localeOf`, `voiceOf`, `countryName`: the languages (see Languages), a language of your own, the formats and the names of countries from `Intl`. `localizedLayout(layout, voice)` is a layout with the team names in a language.
327
332
  - `SIZES`, `THEMES`, `WORDS`, `VERSION`: the sizes, the looks' colours, the English words, the version.
@@ -375,6 +380,7 @@ console.log(svg.includes('lang="nl"')); // true
375
380
  | MLB | 1969 on (no postseason in 1994) | Wild Card Series, Division Series, League Championship Series, World Series |
376
381
  | NFL | 1970 on, with 1982 as its own | Wild Card, Divisional, Conference Championship, Super Bowl |
377
382
  | CFL | 1996 on | Division Semi-Final, Division Final, Grey Cup |
383
+ | MLS | 2023 on | Wild Card Round, Round One (best of three), Conference Semifinals, Conference Finals, MLS Cup |
378
384
  | FIFA World Cup | every edition with a knockout, 1930 to 2026 (1950 had none) | Round of 32 (2026), Round of 16, Quarter-finals, Semi-finals, Final |
379
385
  | Any other | single elimination for any number of teams | Round of N … Final |
380
386
 
package/dist/layout.js CHANGED
@@ -140,6 +140,16 @@ export function layoutBracket(input, options = {}) {
140
140
  const other = (1 - fed);
141
141
  return ties[i].sides[other] ? other : null;
142
142
  };
143
+ /** Every side that skipped the rounds before this tie's: the bye side, or both sides of a later-round tie that nothing feeds. */
144
+ const byesOf = (i) => {
145
+ const one = byeOf(i);
146
+ if (one !== null)
147
+ return [one];
148
+ const ch = children.get(i) ?? [];
149
+ if (round[i] <= 1 || ch.length > 0)
150
+ return [];
151
+ return [0, 1].filter((k) => ties[i].sides[k] !== null);
152
+ };
143
153
  /** Lays one tree out: leaves take the next rows, a tie sits at the middle of the ones that feed it. */
144
154
  const lay = (root, mark) => {
145
155
  const boxes = [];
@@ -148,7 +158,7 @@ export function layoutBracket(input, options = {}) {
148
158
  const ch = children.get(i) ?? [];
149
159
  const ys = ch.map(go);
150
160
  const y = ys.length ? ys.reduce((a, b) => a + b, 0) / ys.length : next++;
151
- boxes.push({ i, round: round[i], y, to: null, fed: ch.length > 0, order: orderOf(i), bye: byeOf(i) });
161
+ boxes.push({ i, round: round[i], y, to: null, fed: ch.length > 0, order: orderOf(i), bye: byeOf(i), byes: byesOf(i) });
152
162
  place[i] = mark;
153
163
  return y;
154
164
  };
package/dist/locales.js CHANGED
@@ -79,6 +79,9 @@ const ROUNDS = {
79
79
  "Division Series": ["Série de division", "ディビジョンシリーズ", "Serie divisional", "Division Series"],
80
80
  "League Championship Series": ["Série de championnat", "リーグチャンピオンシップシリーズ", "Serie de Campeonato de Liga", "League Championship Series"],
81
81
  "World Series": ["Série mondiale", "ワールドシリーズ", "Serie Mundial", "World Series"],
82
+ "Wild Card Round": ["Tour de wild card", "ワイルドカードラウンド", "Ronda de comodín", "Wild-Card-Runde"],
83
+ "Round One": ["Premier tour", "1回戦", "Primera ronda", "Erste Runde"],
84
+ "MLS Cup": ["Coupe MLS", "MLSカップ", "Copa MLS", "MLS Cup"],
82
85
  "Wild Card": ["Wild card", "ワイルドカード・プレーオフ", "Comodín", "Wild-Card-Runde"],
83
86
  "Divisional": ["Ronde divisionnaire", "ディビジョナル・プレーオフ", "Ronda divisional", "Divisional Round"],
84
87
  "Divisional Playoffs": ["Ronde divisionnaire", "ディビジョナル・プレーオフ", "Ronda divisional", "Divisional Round"],
package/dist/matchup.js CHANGED
@@ -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.score[0] > g.score[1]).length, games.filter((g) => g.score[1] > g.score[0]).length];
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;
package/dist/presets.d.ts CHANGED
@@ -4,7 +4,7 @@
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
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
@@ -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.js CHANGED
@@ -137,7 +137,11 @@ function planBracket(layout, o, v, size, vertical, ctx) {
137
137
  const rowH = h / 2;
138
138
  const font = clamp(rowH * 0.5, 8.5, 16);
139
139
  const md = clamp(rowH * 0.7, 10, 28);
140
- const needed = 6 + md + 5 + longest * (font / 13) + 8 + textWidth("00", font + 1) + 14;
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;
141
145
  const named = o.detail && o.detail !== "auto" ? o.detail : w >= needed && h >= 36 ? "full" : w >= 76 && h >= 28 ? "compact" : "mark";
142
146
  return named === "mark" && o.markShape === "none" ? "compact" : named;
143
147
  };
@@ -188,14 +192,14 @@ function planBracket(layout, o, v, size, vertical, ctx) {
188
192
  for (const b of sd.boxes) {
189
193
  const rect = place(sd.key, b);
190
194
  rects.set(b.i, rect);
191
- 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 });
192
196
  }
193
197
  const midX = (halves ? starts[nc] : starts[nc] ?? 0) + (halves ? centreT : 0) / 2;
194
198
  if (halves && layout.final) {
195
199
  const fy = treeY0 + layout.final.y * rowStep + rowStep / 2;
196
200
  const rect = { x: midX - finalW / 2, y: fy - finalH / 2, w: finalW, h: finalH };
197
201
  rects.set(layout.final.i, rect);
198
- 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 });
199
203
  if (showLabels)
200
204
  texts.push(label(v, roundLabel(layout, layout.rounds, o.roundNames, v), midX, rect.y - 7, "middle"));
201
205
  if (champ)
@@ -205,7 +209,7 @@ function planBracket(layout, o, v, size, vertical, ctx) {
205
209
  const r3 = { x: midX - finalW / 2, y: rect.y + rect.h + 14 + 18, w: finalW, h: boxH };
206
210
  rects.set(layout.third, r3);
207
211
  texts.push(label(v, v.words.thirdPlace, midX, r3.y - 6, "middle"));
208
- boxes.push({ rect: r3, tie: t3, order: [0, 1], bye: null, final: false, champion: false });
212
+ boxes.push({ rect: r3, tie: t3, order: [0, 1], bye: null, byes: [], final: false, champion: false });
209
213
  }
210
214
  }
211
215
  else if (!halves) {
@@ -287,7 +291,7 @@ function planBracket(layout, o, v, size, vertical, ctx) {
287
291
  const c = layout.columns.indexOf(b.round);
288
292
  const rect = { x: xOf(b.y), y: bandStart(sd.key, c), w: S, h: D };
289
293
  rects.set(b.i, rect);
290
- 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 });
291
295
  }
292
296
  const midX = W0 / 2;
293
297
  if (halves && layout.final) {
@@ -301,7 +305,7 @@ function planBracket(layout, o, v, size, vertical, ctx) {
301
305
  }
302
306
  const rect = { x: midX - finalW / 2, y: y + 6, w: finalW, h: finalH };
303
307
  rects.set(layout.final.i, rect);
304
- 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 });
305
309
  }
306
310
  else if (!halves && champ && layout.finalIndex !== null) {
307
311
  const fr = rects.get(layout.finalIndex);
@@ -402,13 +406,13 @@ function othersBlock(layout, list, y0, width, pad, ow, oh, v, boxes, texts) {
402
406
  texts.push(label(v, v.words.other, width / 2, y0 + 12, "middle"));
403
407
  list.forEach((i, n) => {
404
408
  const rect = { x: x0 + (n % cols) * (ow + 10), y: y0 + 24 + Math.floor(n / cols) * (oh + 10), w: ow, h: oh };
405
- 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 });
406
410
  });
407
411
  }
408
412
  /** The third-place match with its label, centred on `cx`, its label at `y0`. */
409
413
  function thirdAtFoot(layout, y0, cx, w, h, v, boxes, texts) {
410
414
  texts.push(label(v, v.words.thirdPlace, cx, y0 + 12, "middle"));
411
- 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 });
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 });
412
416
  }
413
417
  // ---------------------------------------------------------------------------------------------- one box
414
418
  function drawBox(p, plan, o, v, ctx) {
@@ -453,17 +457,23 @@ function drawBox(p, plan, o, v, ctx) {
453
457
  }
454
458
  const score = scoresOn && r.score ? v.number(r.score[k]) : "";
455
459
  const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
456
- const bye = p.bye === k;
460
+ const bye = p.byes.includes(k);
457
461
  const right = x + w - 6;
458
462
  const scoreW = score ? textWidth(score, font + 1) : 0;
459
463
  const pensW = pensText ? textWidth(pensText, font - 2) + 3 : 0;
460
- const byeText = bye ? v.words.bye : "";
461
- const byeW = byeText ? textWidth(byeText, Math.max(font - 2, 8)) : 0;
462
- const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6;
463
464
  const seed = seedsOn && team?.seed !== undefined ? `${v.number(team.seed)} ` : "";
464
465
  const nm = team ? (detail === "full" ? team.name : abbrOf(team, v)) : v.words.tbd;
465
- const text = fit(`${seed}${nm}`, font, Math.max(avail, 8));
466
- rowParts.push(`<text class="tb-name${team ? "" : " tb-muted"}" x="${num(tx)}" y="${num(cy + font * 0.35)}" font-size="${num(font)}">${esc(text)}</text>`);
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 : "";
469
+ const byeW = byeText ? textWidth(byeText, Math.max(font - 2, 8)) : 0;
470
+ const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6;
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>`);
467
477
  if (score)
468
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>`);
469
479
  if (pensText)
package/dist/types.d.ts CHANGED
@@ -147,6 +147,8 @@ export interface LayoutBox {
147
147
  order: readonly [0 | 1, 0 | 1];
148
148
  /** The side that had a bye (it came from no tie, though the other did), or `null`. */
149
149
  bye: 0 | 1 | null;
150
+ /** Every side that skipped the earlier rounds: the `bye` side, and both sides of a tie of a later round that no tie feeds (MLS's round one, where seven teams of nine skip the wild card round). */
151
+ byes: readonly (0 | 1)[];
150
152
  }
151
153
  export interface LayoutSide {
152
154
  /** `l` the left half (the top half on a phone), `r` the right. */
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.0";
1
+ export declare const VERSION = "0.2.1";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.2.0";
1
+ export const VERSION = "0.2.1";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/taisen-brackets",
3
- "version": "0.2.0",
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 or for a phone, in light, dark or a gold-on-navy poster look, with each team's logo or round flag, the champion in the centre, and an accessible table. NHL, NBA, MLB, NFL, CFL and World Cup formats by era, in English, French, Japanese, Spanish and German. Zero dependencies.",
3
+ "version": "0.2.1",
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 or for a phone, in light, dark or a gold-on-navy poster look, with each team's logo or round flag, the champion in the centre, and an accessible table. 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",
7
7
  "tournament",
@@ -17,6 +17,7 @@
17
17
  "mlb",
18
18
  "nfl",
19
19
  "cfl",
20
+ "mls",
20
21
  "soccer",
21
22
  "sports",
22
23
  "multilingual",