@johnmorrisdotca/taisen-brackets 0.2.0 → 0.3.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 +31 -1
- package/NOTICE.md +2 -0
- package/README.md +30 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/layout.js +11 -1
- package/dist/locales.js +3 -0
- package/dist/marks.d.ts +14 -1
- package/dist/marks.js +36 -0
- package/dist/matchup.js +3 -1
- package/dist/presets-entry.d.ts +1 -1
- package/dist/presets-entry.js +1 -1
- package/dist/presets.d.ts +8 -1
- package/dist/presets.js +18 -2
- package/dist/render.js +32 -19
- package/dist/theme.js +1 -0
- package/dist/types.d.ts +30 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-10-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Emblems for the trophy.** The `trophy` option also takes an emblem id such as `"trophy:hockey-cup"`, drawn by a function passed as the new `emblems` option: any `(id, { style, size, tint, theme }) => svg`, such as `renderEmblem` from `@johnmorrisdotca/dougu-sports/emblems`. The renderer is asked for the size the trophy has in the drawing and for `theme: "light"` or `"dark"` in those looks (`"dark"` for `poster`), else `"inherit"`; `emblemStyle` (`metal`, `line`, `color`) and `emblemTint` are passed on. What it returns is cleaned like any inline SVG, drawn in a nested `<svg class="tb-emblem">` that takes its ink from `--tb-ink`, and its English `<title>` is left out. With no renderer an emblem id draws nothing and takes no room; a renderer that throws or returns something that is not an `<svg>` draws no trophy.
|
|
14
|
+
- `LEAGUE_EMBLEMS` and `PlayoffFormat.emblem`: each league's default emblem (NHL `trophy:hockey-cup`, NBA `trophy:basketball`, MLB `trophy:baseball`, NFL `trophy:football`, CFL `trophy:canadian-bowl`, MLS `trophy:cup`, World Cup `trophy:soccer-globe`), used only when a renderer is plugged in; `isEmblemId(value)`; the types `EmblemRenderer`, `EmblemRequest` and `EmblemStyle`.
|
|
15
|
+
- `@johnmorrisdotca/dougu-sports` as an optional peer dependency (and a pinned development dependency, for the tests and the demo). The package still has no dependencies, imports nothing from it, and an inline SVG or an address given as `trophy` is drawn exactly as before.
|
|
16
|
+
- The demo draws each league's emblem for its champion, with a "Trophy" switch and a Metal, Line or Colour choice; the emblems are loaded one module each from the Dougu Sports files the Pages build copies beside the page.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Every drawing's stylesheet has one more rule, `.tb-emblem{color:var(--tb-ink, …)}`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `docs/formats.md` titled the MLS section "undefined".
|
|
25
|
+
|
|
26
|
+
## [0.2.1] - 2026-10-14
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **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.
|
|
31
|
+
- `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.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- 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.
|
|
36
|
+
|
|
9
37
|
## [0.2.0] - 2026-10-14
|
|
10
38
|
|
|
11
39
|
### Added
|
|
@@ -41,7 +69,9 @@ First version.
|
|
|
41
69
|
- 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
70
|
- 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
71
|
|
|
44
|
-
[Unreleased]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.
|
|
72
|
+
[Unreleased]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.3.0...HEAD
|
|
73
|
+
[0.3.0]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.2.1...v0.3.0
|
|
74
|
+
[0.2.1]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.2.0...v0.2.1
|
|
45
75
|
[0.2.0]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.1.1...v0.2.0
|
|
46
76
|
[0.1.1]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.1.0...v0.1.1
|
|
47
77
|
[0.1.0]: https://github.com/johnmorrisdotca/taisen-brackets/releases/tag/v0.1.0
|
package/NOTICE.md
CHANGED
|
@@ -9,12 +9,14 @@ 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 |
|
|
15
16
|
| How the results were read | spxn.com's public API (a PostgREST service whose `api` schema is readable with a publishable key; the key is not kept in this repository), by `scripts/fetch-demo-data.mjs`, one request at a time, a second apart, retrieved 2026-10-14 | spxn.com is the same owner's site |
|
|
16
17
|
| The demo's two-legged ties and its 32-team generic bracket | Made up for the demo: the clubs do not exist and the teams are "Team 1" to "Team 32" | Not real |
|
|
17
18
|
| The demo's national flags (in `demo/flags-data.js`) | [Hata](https://github.com/johnmorrisdotca/hata) 1.5.0 (MIT code), a development dependency used only by `scripts/make-flags.mjs`. Each flag is a Wikimedia Commons file that Hata ships only when Commons states it public domain, CC0 or CC BY; the file, author and licence of each are in `FLAG_CREDITS` in that file and in Hata's manifest and NOTICE | Each flag's own licence; none of the flags used asks for a credit. Commons marks flags as official insignia, whose use some countries restrict independently of copyright |
|
|
19
|
+
| The demo's trophies (`demo/emblems.js`, and the `dougu-sports/` files the Pages build copies beside the page) | [Dougu Sports](https://github.com/johnmorrisdotca/dougu-sports) 0.1.0, a development dependency and an optional peer: generic emblems drawn for that package, which carry no league's or federation's name or mark | MIT, same owner; its NOTICE is in the package |
|
|
18
20
|
| The words of the languages (`src/locales.ts`) | The football terms (the rounds of the World Cup, extra time, the shoot-out, a replay, a walkover) were read on 2026-10-14 in the French, Spanish, German and Japanese Wikipedia articles on the World Cups of 2022, 2026 and 1938; the articles are cited in [docs/languages.md](docs/languages.md). The other words are written for this package; the French and Japanese were read by native-language reviewers, the Spanish and German were not | Wikipedia's text is CC BY-SA 4.0; only terms (single words and short phrases) are taken, no sentence or table, and the articles are credited in docs/languages.md |
|
|
19
21
|
| The names of countries in each language | Not in the package: `countryName` asks `Intl.DisplayNames`, which holds Unicode CLDR's names; the demo passes it each team's ISO code. The home nations and the two states that no longer exist are written in `src/locales.ts` and `demo/data.js` | CLDR is under the Unicode licence (https://www.unicode.org/license.txt) |
|
|
20
22
|
| The demo's page | Written for this package, with the family stylesheet `demo/family.css` | MIT |
|
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
|
|
@@ -88,9 +93,9 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
|
|
|
88
93
|
- **Wide, square, phone**: 1200 by 675, 1080 by 1080, or 390 wide and as tall as it needs; the rounds are columns, or rows on a phone. Boxes show names and scores, abbreviations, or only the marks, whichever fits.
|
|
89
94
|
- **Light, dark, poster**: every colour is a CSS custom property with a fallback, so the drawing follows the page's `prefers-color-scheme` or `data-theme` and can be restyled from outside.
|
|
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
|
-
- **The champion in the centre**, with a slot for your trophy.
|
|
96
|
+
- **The champion in the centre**, with a slot for your trophy: your own SVG or image, or an emblem from [Dougu Sports](https://github.com/johnmorrisdotca/dougu-sports) (a hockey cup, a basketball trophy, a globe) through an optional plug-in, with no dependency added.
|
|
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.
|
|
@@ -283,6 +288,24 @@ const svg = drawBracket([{ sides: ["A", "B"], score: [2, 1] }], { trophy });
|
|
|
283
288
|
console.log(svg.includes('class="tb-gold"')); // true
|
|
284
289
|
```
|
|
285
290
|
|
|
291
|
+
### Emblems for the trophy
|
|
292
|
+
|
|
293
|
+
The trophy can also be an emblem id, such as `"trophy:hockey-cup"`, drawn by a function you plug in as `emblems`: any function from an id and a request (`{ style, size, tint, theme }`) to an SVG string. [`@johnmorrisdotca/dougu-sports`](https://github.com/johnmorrisdotca/dougu-sports) ([gallery](https://johnmorrisdotca.github.io/dougu-sports/)) has one, `renderEmblem`, and a cup, a basketball, a baseball, a football, a Canadian-football bowl and a soccer globe among its trophies. Taisen does not import it: it is an optional peer dependency, and the package has no dependencies of its own.
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
import { drawBracket } from "@johnmorrisdotca/taisen-brackets";
|
|
297
|
+
import { renderEmblem } from "@johnmorrisdotca/dougu-sports/emblems";
|
|
298
|
+
|
|
299
|
+
const data = [{ sides: ["Aurora", "Harbour"], score: [2, 1] }];
|
|
300
|
+
const svg = drawBracket(data, { emblems: renderEmblem, trophy: "trophy:hockey-cup" });
|
|
301
|
+
console.log(svg.includes('class="tb-emblem"')); // true
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
- **The look follows the bracket.** The renderer is asked for the size the trophy has in the drawing, and for `theme: "light"` or `"dark"` when the bracket is in that look (`"dark"` for `poster`), else `"inherit"`, where the drawing's own colour (`--tb-ink`) is the ink. `emblemStyle` (`"metal"`, the default, `"line"` or `"color"`) and `emblemTint` (a hex colour) are passed on.
|
|
305
|
+
- **Each league has a default.** `LEAGUE_EMBLEMS` names one (`nhl` the hockey cup, `nba` the basketball, `mlb` the baseball, `nfl` the football, `cfl` the Canadian bowl, `mls` a cup, `worldcup` the soccer globe), and `playoffFormat(league, year).emblem` carries it: `trophy: playoffFormat("nhl", 2024).emblem`.
|
|
306
|
+
- **Without a renderer** an emblem id draws nothing and takes no room, and nothing throws; a trophy that is inline SVG or an address is drawn exactly as before, whatever `emblems` is. A renderer that throws, or returns something that is not an `<svg>`, draws no trophy. What it returns is cleaned like any inline SVG, and the emblem's own `<title>` (English only) is left out; the drawing's description names the champion in the bracket's language. `isEmblemId(value)` tells an emblem id from SVG and from an address.
|
|
307
|
+
- **No global switch.** There is no module-level setting: a drawing is a pure function of its options, so the renderer is passed to each call.
|
|
308
|
+
|
|
286
309
|
### A table for readers the picture does not serve
|
|
287
310
|
|
|
288
311
|
```js
|
|
@@ -314,14 +337,15 @@ The string is built from your data, with every name escaped; do not put untruste
|
|
|
314
337
|
|
|
315
338
|
Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part from `/layout`, the formats from `/presets`. The types are in the package; each function's doc comment has its details.
|
|
316
339
|
|
|
317
|
-
- `drawBracket(input, options?)`: the SVG string. `input` is a list of matchups, `{ matchups, teams }` or a layout. Options: `size`, `orientation`, `shape`, `detail`, `theme`, `background`, `title`, `subtitle`, `roundNames`, `roundLabels`, `scores`, `seeds`, `markShape`, `champion`, `trophy`, `thirdPlace`, `winnerHighlight`, `locale`, `words`, `teamName`, `groupOrder`, `id`, `className`.
|
|
340
|
+
- `drawBracket(input, options?)`: the SVG string. `input` is a list of matchups, `{ matchups, teams }` or a layout. Options: `size`, `orientation`, `shape`, `detail`, `theme`, `background`, `title`, `subtitle`, `roundNames`, `roundLabels`, `scores`, `seeds`, `markShape`, `champion`, `trophy`, `emblems`, `emblemStyle`, `emblemTint`, `thirdPlace`, `winnerHighlight`, `locale`, `words`, `teamName`, `groupOrder`, `id`, `className`.
|
|
318
341
|
- `layoutBracket(input, options?)`: the rounds, halves, rows, byes and results, without drawing. `shape: "ltr"` and `groupOrder` are its options.
|
|
319
342
|
- `bracketTable(input, options?)`: the HTML table.
|
|
320
343
|
- `describeBracket(input, options?)`: the sentence a screen reader gets.
|
|
321
344
|
- `singleElimination(teams, options?)`, `advanceWinners(matchups)`, `standardSeeds(size)`: brackets for any number of teams.
|
|
322
345
|
- `matchupsFromGames(games, options)`: matchups from a flat list of games.
|
|
323
346
|
- `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`).
|
|
347
|
+
- `playoffFormat(league, year)`, `playoffEras(league)`, `LEAGUES`: the formats by era (`nhl`, `nba`, `mlb`, `nfl`, `cfl`, `mls`, `worldcup`). `LEAGUE_EMBLEMS`: each league's default emblem id.
|
|
348
|
+
- `isEmblemId(value)`, and the types `EmblemRenderer`, `EmblemRequest`, `EmblemStyle`: the trophy emblems (see "Emblems for the trophy").
|
|
325
349
|
- `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
|
|
326
350
|
- `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
351
|
- `SIZES`, `THEMES`, `WORDS`, `VERSION`: the sizes, the looks' colours, the English words, the version.
|
|
@@ -375,6 +399,7 @@ console.log(svg.includes('lang="nl"')); // true
|
|
|
375
399
|
| MLB | 1969 on (no postseason in 1994) | Wild Card Series, Division Series, League Championship Series, World Series |
|
|
376
400
|
| NFL | 1970 on, with 1982 as its own | Wild Card, Divisional, Conference Championship, Super Bowl |
|
|
377
401
|
| CFL | 1996 on | Division Semi-Final, Division Final, Grey Cup |
|
|
402
|
+
| MLS | 2023 on | Wild Card Round, Round One (best of three), Conference Semifinals, Conference Finals, MLS Cup |
|
|
378
403
|
| 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
404
|
| Any other | single elimination for any number of teams | Round of N … Final |
|
|
380
405
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export { bracketTable } from "./table.ts";
|
|
|
4
4
|
export type { TableOptions } from "./table.ts";
|
|
5
5
|
export { THEMES } from "./theme.ts";
|
|
6
6
|
export type { Palette } from "./theme.ts";
|
|
7
|
-
export { cleanSvg, safeUrl } from "./marks.ts";
|
|
7
|
+
export { cleanSvg, safeUrl, isEmblemId } from "./marks.ts";
|
|
8
8
|
export { abbreviate } from "./text.ts";
|
|
9
9
|
export { VERSION } from "./version.ts";
|
|
10
|
-
export { playoffFormat, playoffEras, LEAGUES } from "./presets.ts";
|
|
10
|
+
export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.ts";
|
|
11
11
|
export type { League, PlayoffFormat, PlayoffRound } from "./presets.ts";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export * from "./layout-entry.js";
|
|
|
2
2
|
export { drawBracket, describeBracket, localizedLayout, SIZES } from "./render.js";
|
|
3
3
|
export { bracketTable } from "./table.js";
|
|
4
4
|
export { THEMES } from "./theme.js";
|
|
5
|
-
export { cleanSvg, safeUrl } from "./marks.js";
|
|
5
|
+
export { cleanSvg, safeUrl, isEmblemId } from "./marks.js";
|
|
6
6
|
export { abbreviate } from "./text.js";
|
|
7
7
|
export { VERSION } from "./version.js";
|
|
8
|
-
export { playoffFormat, playoffEras, LEAGUES } from "./presets.js";
|
|
8
|
+
export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.js";
|
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/marks.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Team } from "./types.ts";
|
|
1
|
+
import type { DrawOptions, Team } from "./types.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Takes inline SVG markup from a caller and makes it safe to put inside a drawing: the outer `<svg>`'s viewBox is kept,
|
|
4
4
|
* and scripts, event handlers, `<style>`, `<foreignObject>`, comments and `javascript:` addresses are removed. Returns the
|
|
@@ -8,6 +8,19 @@ export declare function cleanSvg(markup: string): {
|
|
|
8
8
|
viewBox: string;
|
|
9
9
|
inner: string;
|
|
10
10
|
} | null;
|
|
11
|
+
/** Whether a `trophy` value names an emblem (`"trophy:hockey-cup"`, `"medal:gold/line"`) rather than being inline SVG or an image address. */
|
|
12
|
+
export declare function isEmblemId(value: string): boolean;
|
|
13
|
+
/** Whether `drawBracket` has something to draw for this `trophy` and `emblems`: an emblem id needs a renderer; SVG and addresses do not. */
|
|
14
|
+
export declare function hasTrophy(o: Pick<DrawOptions, "trophy" | "emblems">): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The emblem a renderer makes for an id, cleaned like any inline SVG and with its `<title>` removed (the drawing says what it shows in its own
|
|
17
|
+
* words), or `null` when the renderer throws or returns something that is not an `<svg>`. The look decides the ink: `light` and `dark` are
|
|
18
|
+
* forced, `poster` is dark, and `auto` and `none` are `inherit`, which the drawing's stylesheet colours from `--tb-ink`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function drawEmblem(id: string, size: number, o: Pick<DrawOptions, "emblems" | "emblemStyle" | "emblemTint" | "theme">): {
|
|
21
|
+
viewBox: string;
|
|
22
|
+
inner: string;
|
|
23
|
+
} | null;
|
|
11
24
|
/** Whether an image address is one a drawing will load: http(s), a relative path, or a `data:image/`. */
|
|
12
25
|
export declare function safeUrl(url: string): boolean;
|
|
13
26
|
export interface MarkContext {
|
package/dist/marks.js
CHANGED
|
@@ -24,6 +24,42 @@ export function cleanSvg(markup) {
|
|
|
24
24
|
.replace(/(\s(?:xlink:)?href\s*=\s*)(["'])\s*javascript:[^"']*\2/gi, "$1$2#$2");
|
|
25
25
|
return { viewBox: box, inner };
|
|
26
26
|
}
|
|
27
|
+
const EMBLEM_ID = /^[a-z][a-z0-9-]*:[a-z0-9][a-z0-9-]*(?:\/(?:metal|line|color))?$/;
|
|
28
|
+
/** Whether a `trophy` value names an emblem (`"trophy:hockey-cup"`, `"medal:gold/line"`) rather than being inline SVG or an image address. */
|
|
29
|
+
export function isEmblemId(value) {
|
|
30
|
+
return EMBLEM_ID.test(value.trim());
|
|
31
|
+
}
|
|
32
|
+
/** Whether `drawBracket` has something to draw for this `trophy` and `emblems`: an emblem id needs a renderer; SVG and addresses do not. */
|
|
33
|
+
export function hasTrophy(o) {
|
|
34
|
+
const t = o.trophy?.trim();
|
|
35
|
+
if (!t)
|
|
36
|
+
return false;
|
|
37
|
+
return isEmblemId(t) ? typeof o.emblems === "function" : true;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The emblem a renderer makes for an id, cleaned like any inline SVG and with its `<title>` removed (the drawing says what it shows in its own
|
|
41
|
+
* words), or `null` when the renderer throws or returns something that is not an `<svg>`. The look decides the ink: `light` and `dark` are
|
|
42
|
+
* forced, `poster` is dark, and `auto` and `none` are `inherit`, which the drawing's stylesheet colours from `--tb-ink`.
|
|
43
|
+
*/
|
|
44
|
+
export function drawEmblem(id, size, o) {
|
|
45
|
+
if (typeof o.emblems !== "function")
|
|
46
|
+
return null;
|
|
47
|
+
const look = o.theme ?? "auto";
|
|
48
|
+
const request = { style: o.emblemStyle ?? "metal", size, theme: look === "light" ? "light" : look === "dark" || look === "poster" ? "dark" : "inherit" };
|
|
49
|
+
if (o.emblemTint)
|
|
50
|
+
request.tint = o.emblemTint;
|
|
51
|
+
let markup;
|
|
52
|
+
try {
|
|
53
|
+
markup = o.emblems(id.trim(), request);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (typeof markup !== "string")
|
|
59
|
+
return null;
|
|
60
|
+
const svg = cleanSvg(markup);
|
|
61
|
+
return svg ? { viewBox: svg.viewBox, inner: svg.inner.replace(/<title\b[\s\S]*?<\/title\s*>/gi, "") } : null;
|
|
62
|
+
}
|
|
27
63
|
/** Readable text on a team colour: white on a dark one, near-black on a light one; `null` when the colour is not a hex one this can read. */
|
|
28
64
|
function inkOn(color) {
|
|
29
65
|
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim())?.[1];
|
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
|
|
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-entry.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { playoffFormat, playoffEras, LEAGUES } from "./presets.ts";
|
|
1
|
+
export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.ts";
|
|
2
2
|
export type { League, PlayoffFormat, PlayoffRound } from "./presets.ts";
|
|
3
3
|
export { roundName, roundNamesFor } from "./rounds.ts";
|
package/dist/presets-entry.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { playoffFormat, playoffEras, LEAGUES } from "./presets.js";
|
|
1
|
+
export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.js";
|
|
2
2
|
export { roundName, roundNamesFor } from "./rounds.js";
|
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;
|
|
@@ -29,9 +29,16 @@ export interface PlayoffFormat {
|
|
|
29
29
|
/** The names of the two halves, left first, or null where the bracket is not split in two. */
|
|
30
30
|
groups: readonly [string, string] | null;
|
|
31
31
|
note?: string;
|
|
32
|
+
/** The league's default emblem id (`LEAGUE_EMBLEMS`), for `trophy` with an `emblems` renderer. */
|
|
33
|
+
emblem: string;
|
|
32
34
|
/** The English Wikipedia articles this entry was checked against (CC BY-SA 4.0; only facts are used). */
|
|
33
35
|
sources: readonly string[];
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The emblem each league's champion gets by default: an id of `@johnmorrisdotca/dougu-sports` (generic forms, no league's own mark). Pass it as
|
|
39
|
+
* `trophy` together with an `emblems` renderer; with no renderer it draws nothing. A cup for MLS because the MLS Cup is one.
|
|
40
|
+
*/
|
|
41
|
+
export declare const LEAGUE_EMBLEMS: Readonly<Record<League, string>>;
|
|
35
42
|
/** The leagues that have a format here. */
|
|
36
43
|
export declare const LEAGUES: readonly League[];
|
|
37
44
|
/**
|
package/dist/presets.js
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* The emblem each league's champion gets by default: an id of `@johnmorrisdotca/dougu-sports` (generic forms, no league's own mark). Pass it as
|
|
9
|
+
* `trophy` together with an `emblems` renderer; with no renderer it draws nothing. A cup for MLS because the MLS Cup is one.
|
|
10
|
+
*/
|
|
11
|
+
export const LEAGUE_EMBLEMS = Object.freeze({
|
|
12
|
+
nhl: "trophy:hockey-cup",
|
|
13
|
+
nba: "trophy:basketball",
|
|
14
|
+
mlb: "trophy:baseball",
|
|
15
|
+
nfl: "trophy:football",
|
|
16
|
+
cfl: "trophy:canadian-bowl",
|
|
17
|
+
mls: "trophy:cup",
|
|
18
|
+
worldcup: "trophy:soccer-globe",
|
|
19
|
+
});
|
|
7
20
|
/** The address of English Wikipedia articles, by title. */
|
|
8
21
|
const wiki = (...titles) => titles.map((t) => `https://en.wikipedia.org/wiki/${t.replace(/ /g, "_")}`);
|
|
9
22
|
const r = (name, bestOf = 7) => ({ name, bestOf });
|
|
@@ -47,6 +60,9 @@ const ERAS = {
|
|
|
47
60
|
cfl: [
|
|
48
61
|
{ 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
62
|
],
|
|
63
|
+
mls: [
|
|
64
|
+
{ 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") },
|
|
65
|
+
],
|
|
50
66
|
worldcup: [
|
|
51
67
|
{ 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
68
|
{ 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") },
|
|
@@ -73,9 +89,9 @@ export function playoffFormat(league, year) {
|
|
|
73
89
|
const { years: _years, except: _except, ...rest } = hit;
|
|
74
90
|
void _years;
|
|
75
91
|
void _except;
|
|
76
|
-
return { ...rest, year };
|
|
92
|
+
return { ...rest, year, emblem: LEAGUE_EMBLEMS[league] };
|
|
77
93
|
}
|
|
78
94
|
/** Every era of a league, oldest first. */
|
|
79
95
|
export function playoffEras(league) {
|
|
80
|
-
return ERAS[league].map(({ years: _y, except: _e, ...rest }) => { void _y; void _e; return { ...rest, year: rest.from }; });
|
|
96
|
+
return ERAS[league].map(({ years: _y, except: _e, ...rest }) => { void _y; void _e; return { ...rest, year: rest.from, emblem: LEAGUE_EMBLEMS[league] }; });
|
|
81
97
|
}
|
package/dist/render.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { layoutBracket } from "./layout.js";
|
|
2
2
|
import { abbrOf, describeTie, noteParts } from "./matchup.js";
|
|
3
|
-
import { cleanSvg, clipId, drawMark, safeUrl } from "./marks.js";
|
|
3
|
+
import { cleanSvg, clipId, drawEmblem, drawMark, hasTrophy as hasTrophyOf, isEmblemId, 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";
|
|
@@ -88,7 +88,7 @@ function planBracket(layout, o, v, size, vertical, ctx) {
|
|
|
88
88
|
const halves = layout.shape === "halves";
|
|
89
89
|
const nc = layout.columns.length;
|
|
90
90
|
const rows = layout.rows;
|
|
91
|
-
const hasTrophy =
|
|
91
|
+
const hasTrophy = hasTrophyOf(o) && champ;
|
|
92
92
|
// ----- slots along the rounds' axis (t): the columns of the left half, the centre, the columns of the right half.
|
|
93
93
|
// Horizontal: t is x. Vertical: t is y, and the left half is the top.
|
|
94
94
|
const slotCount = halves ? 2 * nc + 1 : nc + (champ || layout.finalIndex !== null ? 1 : 0);
|
|
@@ -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
|
-
|
|
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);
|
|
@@ -368,10 +372,13 @@ function championBlock(team, cx, top, mark, trophyH, nameFont, o, v, ctx, poster
|
|
|
368
372
|
let y = top;
|
|
369
373
|
if (o.trophy && trophyH) {
|
|
370
374
|
const t = o.trophy.trim();
|
|
371
|
-
const
|
|
372
|
-
|
|
375
|
+
const emblem = isEmblemId(t) ? drawEmblem(t, Number(num(trophyH)), o) : null;
|
|
376
|
+
const svg = emblem ? null : t.startsWith("<") ? cleanSvg(t) : null;
|
|
377
|
+
if (emblem)
|
|
378
|
+
out.push(`<svg class="tb-emblem" x="${num(cx - trophyH / 2)}" y="${num(y)}" width="${num(trophyH)}" height="${num(trophyH)}" viewBox="${esc(emblem.viewBox)}" preserveAspectRatio="xMidYMid meet" aria-hidden="true">${emblem.inner}</svg>`);
|
|
379
|
+
else if (svg)
|
|
373
380
|
out.push(`<svg class="tb-gold" x="${num(cx - trophyH / 2)}" y="${num(y)}" width="${num(trophyH)}" height="${num(trophyH)}" viewBox="${esc(svg.viewBox)}" preserveAspectRatio="xMidYMid meet">${svg.inner}</svg>`);
|
|
374
|
-
else if (safeUrl(t))
|
|
381
|
+
else if (!isEmblemId(t) && safeUrl(t))
|
|
375
382
|
out.push(`<image href="${esc(t)}" x="${num(cx - trophyH / 2)}" y="${num(y)}" width="${num(trophyH)}" height="${num(trophyH)}" preserveAspectRatio="xMidYMid meet"/>`);
|
|
376
383
|
y += trophyH + 6;
|
|
377
384
|
}
|
|
@@ -402,13 +409,13 @@ function othersBlock(layout, list, y0, width, pad, ow, oh, v, boxes, texts) {
|
|
|
402
409
|
texts.push(label(v, v.words.other, width / 2, y0 + 12, "middle"));
|
|
403
410
|
list.forEach((i, n) => {
|
|
404
411
|
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 });
|
|
412
|
+
boxes.push({ rect, tie: layout.ties[i], order: [0, 1], bye: null, byes: [], final: false, champion: false });
|
|
406
413
|
});
|
|
407
414
|
}
|
|
408
415
|
/** The third-place match with its label, centred on `cx`, its label at `y0`. */
|
|
409
416
|
function thirdAtFoot(layout, y0, cx, w, h, v, boxes, texts) {
|
|
410
417
|
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 });
|
|
418
|
+
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
419
|
}
|
|
413
420
|
// ---------------------------------------------------------------------------------------------- one box
|
|
414
421
|
function drawBox(p, plan, o, v, ctx) {
|
|
@@ -453,17 +460,23 @@ function drawBox(p, plan, o, v, ctx) {
|
|
|
453
460
|
}
|
|
454
461
|
const score = scoresOn && r.score ? v.number(r.score[k]) : "";
|
|
455
462
|
const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
|
|
456
|
-
const bye = p.
|
|
463
|
+
const bye = p.byes.includes(k);
|
|
457
464
|
const right = x + w - 6;
|
|
458
465
|
const scoreW = score ? textWidth(score, font + 1) : 0;
|
|
459
466
|
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
467
|
const seed = seedsOn && team?.seed !== undefined ? `${v.number(team.seed)} ` : "";
|
|
464
468
|
const nm = team ? (detail === "full" ? team.name : abbrOf(team, v)) : v.words.tbd;
|
|
465
|
-
|
|
466
|
-
|
|
469
|
+
// The word for a bye goes beside the score only where the name still fits beside it.
|
|
470
|
+
const byeFits = bye && right - tx - (scoreW + pensW) - (textWidth(v.words.bye, Math.max(font - 2, 8)) + 6) - 6 >= textWidth(`${seed}${nm}`, font);
|
|
471
|
+
const byeText = byeFits ? v.words.bye : "";
|
|
472
|
+
const byeW = byeText ? textWidth(byeText, Math.max(font - 2, 8)) : 0;
|
|
473
|
+
const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6;
|
|
474
|
+
// A name that does not fit is set a little smaller before it is cut.
|
|
475
|
+
let nameFont = font;
|
|
476
|
+
while (nameFont > 9.5 && textWidth(`${seed}${nm}`, nameFont) > avail)
|
|
477
|
+
nameFont -= 0.5;
|
|
478
|
+
const text = fit(`${seed}${nm}`, nameFont, Math.max(avail, 8));
|
|
479
|
+
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
480
|
if (score)
|
|
468
481
|
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
482
|
if (pensText)
|
package/dist/theme.js
CHANGED
|
@@ -23,6 +23,7 @@ function rules(p, poster, withFont) {
|
|
|
23
23
|
[[".tb-initials"], `fill:${v("ink")}`],
|
|
24
24
|
[[".tb-half"], `fill:${v("ink")}`],
|
|
25
25
|
[[".tb-gold"], `fill:${v("gold")}`],
|
|
26
|
+
[[".tb-emblem"], `color:${v("ink")}`],
|
|
26
27
|
[[".tb-champ .tb-ring"], `stroke:${v("gold")};stroke-width:3`],
|
|
27
28
|
[[".tb-champ-label"], `fill:${v("gold")}`],
|
|
28
29
|
[[".tb-halo"], `paint-order:stroke;stroke:${v("bg")};stroke-width:4px;stroke-linejoin:round`],
|
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. */
|
|
@@ -277,6 +279,20 @@ export interface Size {
|
|
|
277
279
|
}
|
|
278
280
|
/** How much a box shows: both names and scores, an abbreviation and the score, or only the marks. */
|
|
279
281
|
export type Detail = "auto" | "full" | "compact" | "mark";
|
|
282
|
+
/** How an emblem is drawn; the names are those of `@johnmorrisdotca/dougu-sports`. */
|
|
283
|
+
export type EmblemStyle = "metal" | "line" | "color";
|
|
284
|
+
/** What `drawBracket` asks an `emblems` renderer for. */
|
|
285
|
+
export interface EmblemRequest {
|
|
286
|
+
style: EmblemStyle;
|
|
287
|
+
/** Width and height in pixels. */
|
|
288
|
+
size: number;
|
|
289
|
+
/** The colour of the main part, when `emblemTint` is set. */
|
|
290
|
+
tint?: string;
|
|
291
|
+
/** `light` or `dark` for a bracket in that look (`poster` is dark); `inherit` for `auto` and `none`, where the drawing's stylesheet or the page sets the ink. */
|
|
292
|
+
theme: "inherit" | "light" | "dark";
|
|
293
|
+
}
|
|
294
|
+
/** A function from an emblem id and a request to an SVG string; `renderEmblem` of `@johnmorrisdotca/dougu-sports/emblems` is one. */
|
|
295
|
+
export type EmblemRenderer = (id: string, options: EmblemRequest) => string;
|
|
280
296
|
export interface DrawOptions extends LayoutOptions {
|
|
281
297
|
/** `wide` 1200 by 675, `square` 1080 by 1080, `phone` 390 wide and as tall as it needs; or a size of your own. Default `wide`. */
|
|
282
298
|
size?: SizeName | {
|
|
@@ -308,8 +324,21 @@ export interface DrawOptions extends LayoutOptions {
|
|
|
308
324
|
markShape?: "round" | "square" | "none";
|
|
309
325
|
/** Show the champion in the centre once the final is played. Default true. */
|
|
310
326
|
champion?: boolean;
|
|
311
|
-
/**
|
|
327
|
+
/**
|
|
328
|
+
* A trophy for the centre, drawn above the champion: inline SVG, an image address, or (with `emblems`) an emblem id such as
|
|
329
|
+
* `"trophy:hockey-cup"`. An emblem id with no `emblems` renderer draws nothing and takes no room.
|
|
330
|
+
*/
|
|
312
331
|
trophy?: string;
|
|
332
|
+
/**
|
|
333
|
+
* Draws the emblem a `trophy` id names: any function from an id and a request to an SVG string, such as `renderEmblem` from
|
|
334
|
+
* `@johnmorrisdotca/dougu-sports/emblems`. Called once per champion, with the size the trophy has in the drawing. Its SVG is cleaned
|
|
335
|
+
* like any inline SVG; a renderer that throws, or returns something that is not an `<svg>`, draws no trophy.
|
|
336
|
+
*/
|
|
337
|
+
emblems?: EmblemRenderer;
|
|
338
|
+
/** The style an emblem is drawn in: `metal` (the default), `line` or `color`. */
|
|
339
|
+
emblemStyle?: EmblemStyle;
|
|
340
|
+
/** A colour for the emblem's main part (a hex colour or `rgb()`), passed to the renderer as `tint`. */
|
|
341
|
+
emblemTint?: string;
|
|
313
342
|
/** Where the third-place match goes: `below` the final (the default) or `hide`. */
|
|
314
343
|
thirdPlace?: "below" | "hide";
|
|
315
344
|
/** Highlight the side that went through. Default true. */
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.3.0";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/taisen-brackets",
|
|
3
|
-
"version": "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.3.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, 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",
|
|
@@ -89,8 +90,17 @@
|
|
|
89
90
|
"test:demo": "pnpm site && playwright test",
|
|
90
91
|
"screenshots": "pnpm site && node scripts/screenshots.mjs"
|
|
91
92
|
},
|
|
93
|
+
"peerDependencies": {
|
|
94
|
+
"@johnmorrisdotca/dougu-sports": ">=0.1.0 <1"
|
|
95
|
+
},
|
|
96
|
+
"peerDependenciesMeta": {
|
|
97
|
+
"@johnmorrisdotca/dougu-sports": {
|
|
98
|
+
"optional": true
|
|
99
|
+
}
|
|
100
|
+
},
|
|
92
101
|
"devDependencies": {
|
|
93
102
|
"@eslint/js": "^9.0.0",
|
|
103
|
+
"@johnmorrisdotca/dougu-sports": "0.1.0",
|
|
94
104
|
"@johnmorrisdotca/hata": "1.5.0",
|
|
95
105
|
"@playwright/test": "^1.63.0",
|
|
96
106
|
"@types/node": "^24.0.0",
|