@johnmorrisdotca/taisen-brackets 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/NOTICE.md +19 -0
- package/README.md +392 -2
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/layout-entry.d.ts +6 -0
- package/dist/layout-entry.js +5 -0
- package/dist/layout.d.ts +8 -0
- package/dist/layout.js +261 -0
- package/dist/marks.d.ts +31 -0
- package/dist/marks.js +84 -0
- package/dist/matchup.d.ts +11 -0
- package/dist/matchup.js +147 -0
- package/dist/presets-entry.d.ts +3 -0
- package/dist/presets-entry.js +2 -0
- package/dist/presets.d.ts +43 -0
- package/dist/presets.js +81 -0
- package/dist/render.d.ts +13 -0
- package/dist/render.js +478 -0
- package/dist/rounds.d.ts +8 -0
- package/dist/rounds.js +26 -0
- package/dist/seeding.d.ts +60 -0
- package/dist/seeding.js +141 -0
- package/dist/table.d.ts +14 -0
- package/dist/table.js +35 -0
- package/dist/text.d.ts +16 -0
- package/dist/text.js +62 -0
- package/dist/theme.d.ts +21 -0
- package/dist/theme.js +53 -0
- package/dist/types.d.ts +281 -0
- package/dist/types.js +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/words.d.ts +7 -0
- package/dist/words.js +33 -0
- package/package.json +100 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are written here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.1] - 2026-10-14
|
|
10
|
+
|
|
11
|
+
- The title is now "Taisen Brackets 対戦" in the README, the demo and the package description, matching the package name.
|
|
12
|
+
|
|
13
|
+
## [0.1.0] - 2026-10-14
|
|
14
|
+
|
|
15
|
+
First version.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`layoutBracket`**: the rounds, halves, rows, byes and results of a knockout, worked out from its ties (who plays whom, or `feeds`), for ties that are one game (with extra time and a shoot-out), a best-of-N series, two or more legs added up, or a drawn game replayed. Two halves that meet at the final, named for the groups when they split cleanly, or one tree left to right; a third-place match placed apart; ties that lead nowhere kept in `other`.
|
|
20
|
+
- **`drawBracket`**: the bracket as one SVG string, wide (1200 by 675), square (1080 by 1080) or phone (390 wide, the rounds as rows), with full, compact or mark-only boxes, round or square team marks from a logo address or inline SVG (each written once as a `<symbol>`), the champion in the centre with a trophy slot, light, dark and a gold-on-navy poster look, every colour a `--tb-*` custom property, `role="img"` with a title and description, and ids from a hash of the input.
|
|
21
|
+
- **`bracketTable`** and **`describeBracket`**: the same bracket as an HTML table and as a sentence.
|
|
22
|
+
- **`singleElimination`**, **`advanceWinners`**, **`standardSeeds`** and **`matchupsFromGames`**: brackets for any number of teams, seeded in the standard way with byes, results carried forward, and matchups from a flat list of games.
|
|
23
|
+
- **`playoffFormat`** and **`playoffEras`**: round names, series lengths, byes and teams by era for the NHL, NBA, MLB, NFL, CFL and the FIFA World Cup.
|
|
24
|
+
- Entry points `/layout` and `/presets`; no dependencies.
|
|
25
|
+
- 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.
|
|
26
|
+
- Playoff formats checked against English Wikipedia, each era citing its articles (docs/formats.md). The year is the year the playoffs are played in.
|
|
27
|
+
|
|
28
|
+
[Unreleased]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.1.1...HEAD
|
|
29
|
+
[0.1.1]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.1.0...v0.1.1
|
|
30
|
+
[0.1.0]: https://github.com/johnmorrisdotca/taisen-brackets/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Morris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Notice: where the facts and the pictures come from
|
|
2
|
+
|
|
3
|
+
The code of this package is under the MIT licence (see [LICENSE](LICENSE)). The package draws what it is given and carries no data except the formats below. The demo, the docs and the README's pictures carry results and flags, listed here with their terms.
|
|
4
|
+
|
|
5
|
+
| What | Made from | Terms |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `playoffFormat`: round names, series lengths, byes and teams by era | Facts (years, round names, series lengths, numbers of teams, byes) read on 2026-10-13 from English Wikipedia's articles on each playoff and tournament; the articles are cited era by era in [docs/formats.md](docs/formats.md). The MediaWiki API was read one request at a time, a second or more apart, with a User-Agent naming this project | Wikipedia's text is CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/). Only facts are taken, no sentence or table, and the articles are credited in docs/formats.md |
|
|
8
|
+
| The demo's World Cup results of 1938 and 2022 (`demo/data/worldcup-1938-2022.js`) | The Fjelstul World Cup Database, https://github.com/jfjelstul/worldcup, © 2023 Joshua C. Fjelstul, Ph.D., read through the public API of [spxn.com](https://spxn.com), which holds it as the source of those matches | **CC BY-SA 4.0** (https://creativecommons.org/licenses/by-sa/4.0/legalcode). That file is a selection of the database's fields and is shared under the same licence, with this credit. The pictures in `docs/images` that draw these results (the World Cup 2022 and 1938 brackets, and the phone pictures) are adaptations of it and carry the same licence and credit. The package, which has no data in it, is MIT |
|
|
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
|
+
| The demo's NHL results (`demo/data/nhl-2024.js`) | Every game of the 2024 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
|
+
| 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 |
|
|
12
|
+
| 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 |
|
|
13
|
+
| 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 |
|
|
14
|
+
| 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 |
|
|
15
|
+
| The demo's page | Written for this package, with the family stylesheet `demo/family.css` | MIT |
|
|
16
|
+
|
|
17
|
+
The flags in the demo are for the nations of the World Cups of 2022 and 2026: `AR AT AU BA BE BR CA CD CH CI CO CV DE DZ EC EG ES FR GB-ENG GH HR JP KR MA MX NL NO PL PT PY SE SN US ZA`. The package itself ships no flag, no logo and no data of the leagues.
|
|
18
|
+
|
|
19
|
+
No code, text or picture was taken from the bracket libraries compared in [docs/compare.md](docs/compare.md); that page records what each says of itself.
|
package/README.md
CHANGED
|
@@ -1,3 +1,393 @@
|
|
|
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. One function, no dependencies.</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
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>
|
|
8
|
+
<a href="https://www.npmjs.com/package/@johnmorrisdotca/taisen-brackets"><img alt="npm" src="https://img.shields.io/npm/v/@johnmorrisdotca/taisen-brackets?color=2f5d4a"></a>
|
|
9
|
+
<a href="./LICENSE"><img alt="MIT licence" src="https://img.shields.io/badge/licence-MIT-2f5d4a"></a>
|
|
10
|
+
<img alt="No dependencies" src="https://img.shields.io/badge/dependencies-0-2f5d4a">
|
|
11
|
+
<img alt="TypeScript" src="https://img.shields.io/badge/types-TypeScript-3178c6">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center"><a href="https://johnmorrisdotca.github.io/taisen-brackets/"><strong>Open the demo →</strong></a> · <a href="./docs/layout.md">Layout</a> · <a href="./docs/drawing.md">Drawing</a> · <a href="./docs/formats.md">Formats</a> · <a href="./docs/compare.md">How it compares</a></p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<picture>
|
|
18
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/hero-desk-dark.png">
|
|
19
|
+
<img alt="The 2022 World Cup knockout: two halves of eight teams meeting at the final in the centre, with Argentina's round flag and the word Champion above it and the third-place match below. Each box has both teams' round flags, abbreviations and scores, with penalties in brackets." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/hero-desk-light.png" width="720">
|
|
20
|
+
</picture>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<table align="center">
|
|
24
|
+
<tr>
|
|
25
|
+
<td align="center" valign="top">
|
|
26
|
+
<img alt="The poster look: the 2026 World Cup knockout of 32 nations as round flags on dark navy joined by gold lines, Spain's flag in the centre above the word Champion." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/poster-desk.png" width="470"><br><em>The poster look: flags only, gold on navy (the 2026 World Cup).</em>
|
|
27
|
+
</td>
|
|
28
|
+
<td align="center" valign="top">
|
|
29
|
+
<img alt="The 2022 World Cup on a phone: the champion at the top, then the rounds as rows, the first round at the top and at the bottom, the final in the middle, and the third-place match at the foot." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/phone-light.png" width="190"><br><em>On a phone the rounds are rows.</em>
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
34
|
+
## In 30 seconds
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install @johnmorrisdotca/taisen-brackets
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { drawBracket } from "@johnmorrisdotca/taisen-brackets";
|
|
42
|
+
|
|
43
|
+
const svg = drawBracket([
|
|
44
|
+
{ sides: ["Aurora", "Harbour"], score: [2, 1], date: "2026-06-01" },
|
|
45
|
+
{ sides: ["Cascade", "Delta"], score: [0, 1], date: "2026-06-01" },
|
|
46
|
+
{ sides: ["Aurora", "Delta"], score: [3, 3], extraTime: true, pens: [4, 2], date: "2026-06-08" },
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
console.log(svg.startsWith("<svg")); // true
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Put the string in a page (`element.innerHTML = svg`), write it to a file, or send it from a server: it is plain SVG, with no script and no network. A tie names its two sides and, when it has been played, its score; which tie feeds which, the rounds, the halves and the champion are worked out from who plays whom.
|
|
53
|
+
|
|
54
|
+
## Who it is for
|
|
55
|
+
|
|
56
|
+
- **Sports sites** that show a playoff or a World Cup from the results they already hold, on a server or in the browser.
|
|
57
|
+
- **Anyone with a poster or a social card to make**: the same data draws a wide, a square or a phone-shaped bracket, in a gold-on-navy look, with round flags.
|
|
58
|
+
- **Tournament organisers** who need a bracket for any number of teams, with byes, that fills in as the games are played.
|
|
59
|
+
- **Readers who need more than a picture**: every drawing has a title and a description, and `bracketTable` writes the same bracket as a plain HTML table.
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- **Ties of every kind**: a single game (with extra time and a shoot-out), a best-of-N series (by games, or by games won), two or more legs added up, and a drawn game replayed. The winner is worked out from the games, or named.
|
|
64
|
+
- **The layout is worked out**: rounds (from who plays whom, or given), which tie feeds which, two halves meeting at the final (named for the conferences when the groups split them cleanly) or one tree left to right, byes, seeds, a third-place match placed apart, and ties that lead nowhere drawn below.
|
|
65
|
+
- **A bracket that is not played yet**: name the tree with `feeds`, or let `singleElimination` build it for any number of teams, seeded in the standard way, with byes; `advanceWinners` carries results forward.
|
|
66
|
+
- **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.
|
|
67
|
+
- **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.
|
|
68
|
+
- **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.
|
|
69
|
+
- **The champion in the centre**, with a slot for your trophy.
|
|
70
|
+
- **Accessible**: `role="img"` with a title and a description, a tooltip sentence on every tie, and `bracketTable`.
|
|
71
|
+
- **Formats by era**: NHL, NBA, MLB, NFL, CFL and FIFA World Cup round names, series lengths and byes.
|
|
72
|
+
- **Deterministic**: no clock, no random number, no counter; the same input gives the same string, and ids come from a hash of the input.
|
|
73
|
+
- **No dependencies**, ESM with types, about 40 kB packed.
|
|
74
|
+
|
|
75
|
+
## Use it in your project
|
|
76
|
+
|
|
77
|
+
### Install
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
npm install @johnmorrisdotca/taisen-brackets
|
|
81
|
+
# or: pnpm add @johnmorrisdotca/taisen-brackets
|
|
82
|
+
# or: yarn add @johnmorrisdotca/taisen-brackets
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It is ESM, and `require` works on Node 22.12 and later. It needs no DOM: the functions return strings. Three entry points: `@johnmorrisdotca/taisen-brackets` (everything), `/layout` (the layout and the helpers, no drawing) and `/presets` (the formats).
|
|
86
|
+
|
|
87
|
+
## Examples
|
|
88
|
+
|
|
89
|
+
### A bracket from its ties
|
|
90
|
+
|
|
91
|
+
A matchup is two `sides` (a team's id, or a whole team) and, once played, the result. Teams can be listed once, with names, marks and groups; a side named by an id that is not listed is a team of that name.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import { drawBracket, layoutBracket } from "@johnmorrisdotca/taisen-brackets";
|
|
95
|
+
|
|
96
|
+
const teams = [
|
|
97
|
+
{ id: "NED", name: "Netherlands" }, { id: "ARG", name: "Argentina" },
|
|
98
|
+
{ id: "CRO", name: "Croatia" }, { id: "BRA", name: "Brazil" },
|
|
99
|
+
];
|
|
100
|
+
const matchups = [
|
|
101
|
+
{ sides: ["NED", "ARG"], score: [2, 2], extraTime: true, pens: [3, 4], date: "2022-12-09" },
|
|
102
|
+
{ sides: ["CRO", "BRA"], score: [1, 1], extraTime: true, pens: [4, 2], date: "2022-12-09" },
|
|
103
|
+
{ sides: ["ARG", "CRO"], score: [3, 0], date: "2022-12-13" },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const layout = layoutBracket({ matchups, teams });
|
|
107
|
+
console.log(layout.rounds); // 2
|
|
108
|
+
console.log(layout.champion.name); // Argentina
|
|
109
|
+
console.log(layout.shape); // halves
|
|
110
|
+
|
|
111
|
+
const svg = drawBracket(layout, { title: "Semi-finals and final" });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`layoutBracket` is the sport-agnostic part: it gives the rounds, the halves, the rows, and for each tie its result. `drawBracket` takes the same input, or a layout you already have.
|
|
115
|
+
|
|
116
|
+
### A playoff series, and a format by era
|
|
117
|
+
|
|
118
|
+
A series is `bestOf` and the games won; or give the games and it counts them. `playoffFormat(league, season)` has the league's round names for the era, and the halves take the conferences' names when the teams' `group` splits them.
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
import { drawBracket, playoffFormat } from "@johnmorrisdotca/taisen-brackets";
|
|
122
|
+
|
|
123
|
+
const format = playoffFormat("nhl", 2024);
|
|
124
|
+
console.log(format.rounds.map((round) => round.name).join(", ")); // First Round, Second Round, Conference Finals, Stanley Cup Final
|
|
125
|
+
|
|
126
|
+
const svg = drawBracket(
|
|
127
|
+
{
|
|
128
|
+
teams: [
|
|
129
|
+
{ id: "FLA", name: "Florida Panthers", abbr: "FLA", group: "Eastern" }, { id: "BOS", name: "Boston Bruins", abbr: "BOS", group: "Eastern" },
|
|
130
|
+
{ id: "EDM", name: "Edmonton Oilers", abbr: "EDM", group: "Western" }, { id: "DAL", name: "Dallas Stars", abbr: "DAL", group: "Western" },
|
|
131
|
+
],
|
|
132
|
+
matchups: [
|
|
133
|
+
{ sides: ["FLA", "BOS"], bestOf: 7, score: [4, 2], date: "2024-05-06" },
|
|
134
|
+
{ sides: ["DAL", "EDM"], bestOf: 7, score: [2, 4], date: "2024-05-23" },
|
|
135
|
+
{ sides: ["FLA", "EDM"], bestOf: 7, score: [4, 3], date: "2024-06-08" },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
{ roundNames: format, title: "Stanley Cup playoffs 2024" },
|
|
139
|
+
);
|
|
140
|
+
console.log(svg.includes("Eastern")); // true
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Two legs, replays, a shoot-out
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
import { layoutBracket } from "@johnmorrisdotca/taisen-brackets";
|
|
147
|
+
|
|
148
|
+
const layout = layoutBracket([
|
|
149
|
+
{ id: "a", sides: ["Aurora", "Harbour"], kind: "aggregate", legs: [{ score: [2, 0] }, { score: [0, 1] }] },
|
|
150
|
+
{ id: "b", sides: ["Cascade", "Delta"], games: [{ score: [1, 1] }, { score: [2, 0], replay: true }] },
|
|
151
|
+
{ id: "c", sides: ["Aurora", "Cascade"], score: [1, 1], extraTime: true, pens: [3, 5] },
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
for (const tie of layout.ties) console.log(tie.result.kind, tie.result.score.join("-"), tie.sides[tie.winner].name);
|
|
155
|
+
// aggregate 2-1 Aurora
|
|
156
|
+
// replay 2-0 Cascade
|
|
157
|
+
// game 1-1 Cascade
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A drawn game that was replayed is one tie, decided by the replay (the 1934 and 1938 World Cups). Level on aggregate, a shoot-out settles it, or name the `winner` yourself (away goals, a coin toss) and add a `note`.
|
|
161
|
+
|
|
162
|
+
### Byes, seeds, and a bracket for any number of teams
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
import { advanceWinners, drawBracket, singleElimination } from "@johnmorrisdotca/taisen-brackets";
|
|
166
|
+
|
|
167
|
+
const teams = Array.from({ length: 6 }, (_, i) => ({ id: `T${i + 1}`, name: `Team ${i + 1}` }));
|
|
168
|
+
const { matchups } = singleElimination(teams, { thirdPlace: true });
|
|
169
|
+
// Two first-round ties (R1-2 and R1-4: the top two seeds have byes), two semi-finals, the final and the third-place match.
|
|
170
|
+
console.log(matchups.length); // 6
|
|
171
|
+
|
|
172
|
+
// Results as they come in, by the tie's id; the sides of later ties fill in from the winners.
|
|
173
|
+
const played = matchups.map((m) => (m.id === "R1-2" ? { ...m, score: [2, 1] } : m));
|
|
174
|
+
const next = advanceWinners(played);
|
|
175
|
+
console.log(next.find((m) => m.id === "R2-1").sides); // [ 'T1', 'T4' ]
|
|
176
|
+
|
|
177
|
+
const svg = drawBracket({ teams, matchups: next }, { seeds: true });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The top seeds skip the first round; the layout knows which side of a tie had the bye (`box.bye`), and a drawing with no result yet shows "TBD" where a side is not known.
|
|
181
|
+
|
|
182
|
+
### From a flat list of games
|
|
183
|
+
|
|
184
|
+
Most data sources give a flat list of games. `matchupsFromGames` groups them into ties by stage and pair: several games of one pair in a stage are a replay, unless the stage is two-legged (`aggregate`) or a series (`series`).
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
import { drawBracket, matchupsFromGames } from "@johnmorrisdotca/taisen-brackets";
|
|
188
|
+
|
|
189
|
+
const matchups = matchupsFromGames(
|
|
190
|
+
[
|
|
191
|
+
{ stage: "semi", home: "ARG", away: "CRO", homeScore: 3, awayScore: 0, date: "2022-12-13" },
|
|
192
|
+
{ stage: "semi", home: "FRA", away: "MAR", homeScore: 2, awayScore: 0, date: "2022-12-14" },
|
|
193
|
+
{ stage: "third", home: "CRO", away: "MAR", homeScore: 2, awayScore: 1, date: "2022-12-17" },
|
|
194
|
+
{ stage: "final", home: "ARG", away: "FRA", homeScore: 3, awayScore: 3, extraTime: true, homePens: 4, awayPens: 2, date: "2022-12-18" },
|
|
195
|
+
],
|
|
196
|
+
{ stages: ["semi", "final"], third: "third" },
|
|
197
|
+
);
|
|
198
|
+
console.log(matchups.length); // 4
|
|
199
|
+
const svg = drawBracket(matchups);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Sizes, shapes and looks
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
import { drawBracket } from "@johnmorrisdotca/taisen-brackets";
|
|
206
|
+
|
|
207
|
+
const bracket = [
|
|
208
|
+
{ sides: ["A", "B"], score: [1, 0], date: "1" }, { sides: ["C", "D"], score: [0, 2], date: "1" }, { sides: ["A", "D"], score: [2, 1], date: "2" },
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
drawBracket(bracket); // wide: 1200 by 675
|
|
212
|
+
drawBracket(bracket, { size: "square" }); // 1080 by 1080
|
|
213
|
+
drawBracket(bracket, { size: "phone" }); // 390 wide, the rounds as rows
|
|
214
|
+
drawBracket(bracket, { size: { width: 800, height: 500 } });
|
|
215
|
+
drawBracket(bracket, { shape: "ltr" }); // one tree, left to right, the champion at the end
|
|
216
|
+
drawBracket(bracket, { theme: "dark" }); // "auto" (the default) follows the page, "light", "dark", "poster"
|
|
217
|
+
drawBracket(bracket, { theme: "poster", detail: "mark", roundLabels: false, title: "Knockout stage" });
|
|
218
|
+
drawBracket(bracket, { background: false }); // transparent
|
|
219
|
+
drawBracket(bracket, { theme: "none" }); // no <style>: your CSS draws it, currentColor is the fallback
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`detail` is `full` (names and scores), `compact` (abbreviations), `mark` (only the marks) or `auto`, which takes the fullest one that fits the box and the longest name.
|
|
223
|
+
|
|
224
|
+
### Team marks, and round flags
|
|
225
|
+
|
|
226
|
+
A team's `mark` is the address of an image (`https://`, `data:image/`, or a relative path) or inline SVG. It is drawn in a circle (`markShape: "round"`, the default), a square, or left out (`"none"`). A team with no mark is its initials on its `color`.
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
import { drawBracket } from "@johnmorrisdotca/taisen-brackets";
|
|
230
|
+
import { flag } from "@johnmorrisdotca/hata/load";
|
|
231
|
+
|
|
232
|
+
// Hata draws a flag as an SVG string, already framed for a circle.
|
|
233
|
+
const teams = [
|
|
234
|
+
{ id: "ARG", name: "Argentina", mark: await flag("AR", { shape: "round" }) },
|
|
235
|
+
{ id: "FRA", name: "France", mark: await flag("FR", { shape: "round" }) },
|
|
236
|
+
{ id: "ENG", name: "England", mark: await flag("GB-ENG", { shape: "round" }) },
|
|
237
|
+
{ id: "BRA", name: "Brazil", mark: await flag("BR", { shape: "round" }) },
|
|
238
|
+
];
|
|
239
|
+
const matchups = [
|
|
240
|
+
{ sides: ["ARG", "BRA"], score: [1, 0], date: "1" }, { sides: ["FRA", "ENG"], score: [2, 1], date: "1" },
|
|
241
|
+
{ sides: ["ARG", "FRA"], score: [3, 3], extraTime: true, pens: [4, 2], date: "2" },
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
const poster = drawBracket({ teams, matchups }, { theme: "poster", detail: "mark", roundLabels: false });
|
|
245
|
+
// Each flag is written once, as a <symbol>, and used wherever the team appears.
|
|
246
|
+
console.log(poster.includes("<symbol")); // true
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Hata is not a dependency of Taisen: you bring the marks. Inline SVG is cleaned as it goes in (scripts, event attributes, `<style>`, `<foreignObject>` and `javascript:` addresses are removed), and an image address must be http(s), relative or a `data:image/` one. Anything else draws the team's initials.
|
|
250
|
+
|
|
251
|
+
### The champion, and a trophy
|
|
252
|
+
|
|
253
|
+
Once the final is played the champion is in the centre with the word "Champion" (on a phone, at the top, so that no line runs through it). Pass your own trophy, inline SVG or an image address, as `trophy`, and it is drawn above. `champion: false` leaves it out.
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
import { drawBracket } from "@johnmorrisdotca/taisen-brackets";
|
|
257
|
+
|
|
258
|
+
const trophy = '<svg viewBox="0 0 24 24"><path d="M6 3h12v5a6 6 0 0 1-12 0zM10 14h4v4h3v3H7v-3h3z" fill="currentColor"/></svg>';
|
|
259
|
+
const svg = drawBracket([{ sides: ["A", "B"], score: [2, 1] }], { trophy });
|
|
260
|
+
console.log(svg.includes('class="tb-gold"')); // true
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### A table for readers the picture does not serve
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
import { bracketTable, describeBracket } from "@johnmorrisdotca/taisen-brackets";
|
|
267
|
+
|
|
268
|
+
const bracket = [{ sides: ["Aurora", "Harbour"], score: [2, 1] }];
|
|
269
|
+
console.log(describeBracket(bracket)); // 2 teams, 1 round. Final: Aurora 2, Harbour 1. Winner: Aurora. Champion: Aurora.
|
|
270
|
+
console.log(bracketTable(bracket).startsWith("<table")); // true
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`bracketTable` writes a plain `<table>` with a caption and header cells, a row for each tie, the winner in bold, and the note ("a.e.t.", "pens 4–2", "agg.", "in 7").
|
|
274
|
+
|
|
275
|
+
### Your own words
|
|
276
|
+
|
|
277
|
+
Every word the package writes is in `WORDS`; pass `words` to replace any: `{ champion: "Vainqueur", thirdPlace: "Match pour la 3e place", tbd: "À déterminer" }`. Round names come from `roundNames`: a list, or a format from `playoffFormat`.
|
|
278
|
+
|
|
279
|
+
### In React, Vue and the rest
|
|
280
|
+
|
|
281
|
+
The functions return strings, so any framework can use them:
|
|
282
|
+
|
|
283
|
+
```tsx no-check
|
|
284
|
+
// React
|
|
285
|
+
<div dangerouslySetInnerHTML={{ __html: drawBracket(bracket, { size: "phone" }) }} />
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The string is built from your data, with every name escaped; do not put untrusted SVG in `mark` or `trophy` without knowing that it is cleaned (it is) and that an image address you pass is loaded by the visitor's browser.
|
|
289
|
+
|
|
290
|
+
## API
|
|
291
|
+
|
|
292
|
+
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.
|
|
293
|
+
|
|
294
|
+
- `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`, `words`, `groupOrder`, `id`, `className`.
|
|
295
|
+
- `layoutBracket(input, options?)`: the rounds, halves, rows, byes and results, without drawing. `shape: "ltr"` and `groupOrder` are its options.
|
|
296
|
+
- `bracketTable(input, options?)`: the HTML table.
|
|
297
|
+
- `describeBracket(input, options?)`: the sentence a screen reader gets.
|
|
298
|
+
- `singleElimination(teams, options?)`, `advanceWinners(matchups)`, `standardSeeds(size)`: brackets for any number of teams.
|
|
299
|
+
- `matchupsFromGames(games, options)`: matchups from a flat list of games.
|
|
300
|
+
- `resolveMatchup`, `collectTeams`, `describeTie`, `noteParts`: one tie's result, the teams of a bracket, a tie in a sentence.
|
|
301
|
+
- `playoffFormat(league, year)`, `playoffEras(league)`, `LEAGUES`: the formats by era (`nhl`, `nba`, `mlb`, `nfl`, `cfl`, `worldcup`).
|
|
302
|
+
- `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
|
|
303
|
+
- `SIZES`, `THEMES`, `WORDS`, `VERSION`: the sizes, the looks' colours, the words, the version.
|
|
304
|
+
- `cleanSvg(markup)`, `safeUrl(url)`, `abbreviate(name)`: the rules for marks, and the short name made from a name.
|
|
305
|
+
|
|
306
|
+
## Sports covered
|
|
307
|
+
|
|
308
|
+
`playoffFormat(league, year)` gives the round names, the length of each series, the number of teams and the byes; [docs/formats.md](./docs/formats.md) lists the eras.
|
|
309
|
+
|
|
310
|
+
| League | Covered | Rounds in the latest era |
|
|
311
|
+
| --- | --- | --- |
|
|
312
|
+
| NHL | 1975 on (no playoffs in 2005), with 1980–81, 1982–86, 1987–93, 1994–2013, 2014 on, 2020 and 2021 as their own eras | First Round, Second Round, Conference Finals, Stanley Cup Final |
|
|
313
|
+
| NBA | 1977 on | First Round, Conference Semifinals, Conference Finals, NBA Finals |
|
|
314
|
+
| MLB | 1969 on (no postseason in 1994) | Wild Card Series, Division Series, League Championship Series, World Series |
|
|
315
|
+
| NFL | 1970 on, with 1982 as its own | Wild Card, Divisional, Conference Championship, Super Bowl |
|
|
316
|
+
| CFL | 1996 on | Division Semi-Final, Division Final, Grey Cup |
|
|
317
|
+
| 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 |
|
|
318
|
+
| Any other | single elimination for any number of teams | Round of N … Final |
|
|
319
|
+
|
|
320
|
+
Each era was checked against English Wikipedia (facts only; the articles are cited in [docs/formats.md](./docs/formats.md)). The year is the year the playoffs are played in: the NHL's 2024 is the 2023-24 season. A season did something of its own (a strike, a pandemic) has its own entry, and a later one is not covered until it is checked.
|
|
321
|
+
|
|
322
|
+
## Theming
|
|
323
|
+
|
|
324
|
+
Every colour is `var(--tb-<name>, <the look's value>)`, so a page restyles a drawing without touching it:
|
|
325
|
+
|
|
326
|
+
```css
|
|
327
|
+
.my-bracket { --tb-ink: #123; --tb-win: #c60; --tb-box: #fff; --tb-line: #99a; }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The names are `bg`, `ink`, `muted`, `line`, `box`, `edge`, `win`, `gold`, `plain` and `font`. `theme: "auto"` is the light look and, under `prefers-color-scheme: dark` or `<html data-theme="dark">`, the dark one (`data-theme="light"` keeps it light). See [docs/drawing.md](./docs/drawing.md).
|
|
331
|
+
|
|
332
|
+
## Limits
|
|
333
|
+
|
|
334
|
+
- A bracket is a tree: every tie feeds one tie. Double elimination and round-robin groups are not drawn (a round-robin game, `round: 0`, is kept out of the tree and drawn below).
|
|
335
|
+
- Text is measured by an estimate from character classes, not a font file, so that the string is the same on every machine; a long name is cut with an ellipsis a few percent early or late.
|
|
336
|
+
- A series with no `bestOf` and no winner is not decided, whatever the games won say.
|
|
337
|
+
- On a phone the champion is at the top and the third-place match at the foot; in a wide bracket of two halves they are in the centre.
|
|
338
|
+
- Names, round names and the words are English; pass `words` and `roundNames` for another language. Japanese words are on the roadmap.
|
|
339
|
+
|
|
340
|
+
## Accessibility
|
|
341
|
+
|
|
342
|
+
The `<svg>` is `role="img"`, labelled by a `<title>` and a `<desc>` that says how many teams and rounds, the final and the champion; each tie has a tooltip sentence ("Argentina 3, France 3 (a.e.t., pens 4-2). Winner: Argentina"); the winner is marked by weight and a ring, not by colour alone; text is at least 8 units. For the full bracket in a reader's own order, put `bracketTable` beside the picture. The contrast of the light and dark looks' text on their boxes is held to WCAG AA by a test.
|
|
343
|
+
|
|
344
|
+
## Browser and runtime support
|
|
345
|
+
|
|
346
|
+
Any current browser, and Node 22 or later. The package makes no network request and keeps nothing.
|
|
347
|
+
|
|
348
|
+
## Roadmap
|
|
349
|
+
|
|
350
|
+
- Double elimination, and round-robin groups beside a knockout.
|
|
351
|
+
- Japanese words.
|
|
352
|
+
- A custom element and a React helper, as in the other packages of the family.
|
|
353
|
+
- A PNG renderer (through an optional peer).
|
|
354
|
+
|
|
355
|
+
## The name
|
|
356
|
+
|
|
357
|
+
*Taisen* (対戦, たいせん) is Japanese for a matchup, a contest between two sides: 対 is "against" or "facing" and 戦 is "fight" or "match". On a fixture list the word is 対戦カード. A bracket is a tree of taisen.
|
|
358
|
+
|
|
359
|
+
## How it compares
|
|
360
|
+
|
|
361
|
+
Libraries that draw brackets exist for several frameworks. Taisen's difference is what it does with the data: it takes the games as they were played (series, legs, replays, shoot-outs), works out the tree, and draws one deterministic SVG string that needs no framework, no DOM and no script. [docs/compare.md](./docs/compare.md) has the table: brackets-viewer.js, react-tournament-brackets, react-brackets, bracketry and jquery-bracket, with each one's licence read from its repository and package. Only ideas are taken from them, no code.
|
|
362
|
+
|
|
363
|
+
## Where it comes from
|
|
364
|
+
|
|
365
|
+
The layout began as the playoff bracket of [spxn.com](https://spxn.com), the historical record of five leagues, where one layout draws a hockey playoff and a World Cup. [docs/migrating-from-spxn.md](./docs/migrating-from-spxn.md) is how that site would use the package.
|
|
366
|
+
|
|
367
|
+
## Development
|
|
368
|
+
|
|
369
|
+
```sh
|
|
370
|
+
pnpm install
|
|
371
|
+
pnpm check # lint, type-check, tests, build, and the packed package installed and run
|
|
372
|
+
pnpm test:readme # the README's and the docs' examples, run against the built package
|
|
373
|
+
pnpm dev # the demo on http://127.0.0.1:7100/ (this package's block of ports is 7100 to 7199)
|
|
374
|
+
pnpm test:demo # the demo, played in a real browser at a phone's and a desk's width
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
The tests use `node:test` on the TypeScript source (Node 22.18 or later runs it as it is).
|
|
378
|
+
|
|
379
|
+
## Contributing
|
|
380
|
+
|
|
381
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) and the [code of conduct](./CODE_OF_CONDUCT.md). Report a security problem as [SECURITY.md](./SECURITY.md) says.
|
|
382
|
+
|
|
383
|
+
## Changes
|
|
384
|
+
|
|
385
|
+
[CHANGELOG.md](./CHANGELOG.md).
|
|
386
|
+
|
|
387
|
+
## Credits
|
|
388
|
+
|
|
389
|
+
The demo's results are real and are credited in [NOTICE.md](./NOTICE.md), which is also where their terms are. The World Cups of 1938 and 2022 are from [The Fjelstul World Cup Database](https://github.com/jfjelstul/worldcup) (© 2023 Joshua C. Fjelstul, Ph.D., [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/legalcode); the demo's extract and the pictures of those two brackets carry the same licence). The World Cup of 2026 is from [openfootball](https://github.com/openfootball/worldcup) (CC0). The NHL playoffs are from the NHL. The information used for the MLB postseason was obtained free of charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at [www.retrosheet.org](https://www.retrosheet.org). All were read through [spxn.com](https://spxn.com). The round flags are drawn by [Hata](https://github.com/johnmorrisdotca/hata). The formats by era were checked against English Wikipedia (CC BY-SA 4.0; facts only; see [docs/formats.md](./docs/formats.md)).
|
|
390
|
+
|
|
391
|
+
## Licence
|
|
392
|
+
|
|
393
|
+
MIT, © John Morris. See [LICENSE](./LICENSE) and [NOTICE.md](./NOTICE.md).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./layout-entry.ts";
|
|
2
|
+
export { drawBracket, describeBracket, SIZES } from "./render.ts";
|
|
3
|
+
export { bracketTable } from "./table.ts";
|
|
4
|
+
export type { TableOptions } from "./table.ts";
|
|
5
|
+
export { THEMES } from "./theme.ts";
|
|
6
|
+
export type { Palette } from "./theme.ts";
|
|
7
|
+
export { WORDS } from "./words.ts";
|
|
8
|
+
export { cleanSvg, safeUrl } from "./marks.ts";
|
|
9
|
+
export { abbreviate } from "./text.ts";
|
|
10
|
+
export { VERSION } from "./version.ts";
|
|
11
|
+
export { playoffFormat, playoffEras, LEAGUES } from "./presets.ts";
|
|
12
|
+
export type { League, PlayoffFormat, PlayoffRound } from "./presets.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./layout-entry.js";
|
|
2
|
+
export { drawBracket, describeBracket, SIZES } from "./render.js";
|
|
3
|
+
export { bracketTable } from "./table.js";
|
|
4
|
+
export { THEMES } from "./theme.js";
|
|
5
|
+
export { WORDS } from "./words.js";
|
|
6
|
+
export { cleanSvg, safeUrl } from "./marks.js";
|
|
7
|
+
export { abbreviate } from "./text.js";
|
|
8
|
+
export { VERSION } from "./version.js";
|
|
9
|
+
export { playoffFormat, playoffEras, LEAGUES } from "./presets.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./types.ts";
|
|
2
|
+
export { layoutBracket } from "./layout.ts";
|
|
3
|
+
export { collectTeams, resolveMatchup, describeTie, noteParts } from "./matchup.ts";
|
|
4
|
+
export { roundName, roundNamesFor, roundLabel } from "./rounds.ts";
|
|
5
|
+
export { standardSeeds, singleElimination, advanceWinners, matchupsFromGames } from "./seeding.ts";
|
|
6
|
+
export type { SingleEliminationOptions, PlayedGame, GamesOptions } from "./seeding.ts";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { layoutBracket } from "./layout.js";
|
|
3
|
+
export { collectTeams, resolveMatchup, describeTie, noteParts } from "./matchup.js";
|
|
4
|
+
export { roundName, roundNamesFor, roundLabel } from "./rounds.js";
|
|
5
|
+
export { standardSeeds, singleElimination, advanceWinners, matchupsFromGames } from "./seeding.js";
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BracketInput, BracketLayout, LayoutOptions, Matchup } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The shape of a knockout, worked out from its ties: which tie feeds which (a winner goes on to the next tie its team
|
|
4
|
+
* plays, or the tie that `feeds` names), the rounds, which half each tie sits in, and which side has a bye. A tie is
|
|
5
|
+
* a game, a series, two legs or a replayed game, so one layout draws a hockey playoff and a World Cup. Pure and
|
|
6
|
+
* sport-agnostic: no DOM, no drawing.
|
|
7
|
+
*/
|
|
8
|
+
export declare function layoutBracket(input: readonly Matchup[] | BracketInput, options?: LayoutOptions): BracketLayout;
|