@johnmorrisdotca/taisen-brackets 0.6.1 → 0.8.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/dist/render.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { layoutBracket } from "./layout.js";
2
2
  import { abbrOf, describeTie, noteParts } from "./matchup.js";
3
+ import { shidoCards, sideScore } from "./judo-text.js";
3
4
  import { cleanSvg, drawEmblem, drawMark, hasTrophy as hasTrophyOf, isEmblemId, markDefs, safeHref, safeUrl } from "./marks.js";
4
5
  import { roundLabel } from "./rounds.js";
5
6
  import { styleFor } from "./theme.js";
@@ -115,7 +116,7 @@ export function drawBracket(input, options = {}) {
115
116
  const orientation = options.orientation && options.orientation !== "auto" ? options.orientation : kind === "series" || sizeName === "phone" || sizeName === "card" || (size.height !== null && size.width < size.height * 0.8) ? "vertical" : "horizontal";
116
117
  const road = options.look === "road";
117
118
  const hasBg = typeof options.background === "string" || (typeof options.background === "object" && options.background !== null);
118
- const prefix = options.id ?? `tb-${hash(JSON.stringify([v.locale.code, layout.teams.map((t) => t.name), layout.ties.map((t) => [t.id, t.sides.map((s) => s?.id ?? null), t.winner, t.result.score, t.round]), options.size, orientation, options.detail, theme, options.title, options.subtitle, options.seeds, options.scores, options.champion, options.markShape, options.roundLabels, options.thirdPlace, options.trophy ? 1 : 0, road ? 1 : 0, options.highlightPath ?? null, options.flags ? 1 : 0, ...(hasBg ? [options.background, options.vs, options.codes, options.seeds] : []), ...(options.flagHref ? ["h"] : []), ...(kind !== "tree" ? [kind, options.at, options.standings, options.qualify] : []), ...(options.headers ? ["head"] : [])]))}`;
119
+ const prefix = options.id ?? `tb-${hash(JSON.stringify([v.locale.code, layout.teams.map((t) => t.name), layout.ties.map((t) => [t.id, t.sides.map((s) => s?.id ?? null), t.winner, t.result.score, t.round, ...(t.judo ? [t.judo.method, t.judo.goldenScore, t.judo.seconds, t.judo.sides] : [])]), options.size, orientation, options.detail, theme, options.title, options.subtitle, options.seeds, options.scores, options.champion, options.markShape, options.roundLabels, options.thirdPlace, options.trophy ? 1 : 0, road ? 1 : 0, options.highlightPath ?? null, options.flags ? 1 : 0, ...(hasBg ? [options.background, options.vs, options.codes, options.seeds] : []), ...(options.flagHref ? ["h"] : []), ...(kind !== "tree" ? [kind, options.at, options.standings, options.qualify] : []), ...(options.headers ? ["head"] : [])]))}`;
119
120
  const ctx = { radii: new Set(), symbols: new Map(), prefix, shape: options.markShape ?? "round", look: road ? "road" : "classic", flags: options.flags, flagHref: options.flagHref, theme: theme === "light" ? "light" : theme === "dark" || theme === "poster" ? "dark" : "inherit", flagCache: new Map(), defs: new Map() };
120
121
  const path = pathOf(layout, options.highlightPath);
121
122
  const linked = { count: 0 };
@@ -124,10 +125,10 @@ export function drawBracket(input, options = {}) {
124
125
  // The group tables and the play-in are not the tree: they plan and draw their own parts, and share the frame (the style, the background, the title) with it.
125
126
  const own = kind === "groups" ? drawGroups(layout, options, v, size, ctx, road, linked) : kind === "playin" ? drawPlayIn(layout, options, v, size, ctx, road, linked) : null;
126
127
  if (own)
127
- return frame({ prefix, v, options, theme, road, kind, title, desc: own.desc ?? desc, width: own.width, height: own.height, followed: false, links: [], parts: [...own.body, ...own.texts], linkCount: linked.count, ctx });
128
+ return frame({ prefix, v, options, theme, road, kind, title, desc: own.desc ?? desc, width: own.width, height: own.height, followed: false, links: [], parts: [...own.body, ...own.texts], linkCount: linked.count, ctx, judo: layout.ties.some((t) => t.judo !== undefined) });
128
129
  const plan = planBracket(layout, options, v, size, orientation === "vertical", ctx, road, path, kind);
129
130
  const drawn = plan.boxes.map((b) => drawBox(b, plan, options, v, ctx, linked, layout.place[b.tie.index])); // draws the marks, so before the defs are written
130
- return frame({ prefix, v, options, theme, road, kind, title, desc, width: plan.width, height: plan.height, followed: path.size > 0, links: plan.links, parts: [...drawn, ...plan.texts, ...plan.extras], linkCount: linked.count, ctx });
131
+ return frame({ prefix, v, options, theme, road, kind, title, desc, width: plan.width, height: plan.height, followed: path.size > 0, links: plan.links, parts: [...drawn, ...plan.texts, ...plan.extras], linkCount: linked.count, ctx, judo: layout.ties.some((t) => t.judo !== undefined) });
131
132
  }
132
133
  /** The drawing's frame: the root `<svg>` with its title, its style, the defs the marks wrote and the background, round the parts a layout drew. */
133
134
  function frame(f) {
@@ -135,7 +136,7 @@ function frame(f) {
135
136
  const picture = resolveBackground(options.background, theme, f.width, f.height, prefix);
136
137
  const defs = markDefs(ctx) + (picture?.defs ?? "");
137
138
  const scope = scopeOf(options, prefix);
138
- const style = theme === "none" || options.style === false ? "" : `<style>${styleFor(`.${scope}`, theme, { road, links: f.linkCount > 0, follow: f.followed, bgAuto: picture?.auto, layout: f.kind !== "tree" || options.headers === true }, picture?.inks)}</style>`;
139
+ const style = theme === "none" || options.style === false ? "" : `<style>${styleFor(`.${scope}`, theme, { road, links: f.linkCount > 0, follow: f.followed, bgAuto: picture?.auto, layout: f.kind !== "tree" || options.headers === true, judo: f.judo }, picture?.inks)}</style>`;
139
140
  const bg = options.background === false ? "" : `<rect class="tb-bg" width="${num(f.width)}" height="${num(f.height)}"${theme === "none" ? ' fill="none"' : ""}/>${picture?.body ?? ""}`;
140
141
  return [
141
142
  `<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(v.locale.code)}" class="taisen ${prefix}${scope !== prefix ? ` ${scope}` : ""}${road ? " tb-look-road" : ""}${f.followed ? " tb-follow" : ""}${options.className ? ` ${esc(options.className)}` : ""}" viewBox="0 0 ${num(f.width)} ${num(f.height)}" width="${num(f.width)}" height="${num(f.height)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${prefix}-t ${prefix}-d">`,
@@ -168,7 +169,7 @@ export function bracketStyle(options) {
168
169
  if (theme === "none")
169
170
  return "";
170
171
  const picture = resolveBackground(options.background, theme, 100, 100, "x");
171
- return styleFor(`.${scope}`, theme, { road: options.look === "road", links: typeof options.href === "function", follow: options.highlightPath !== undefined, bgAuto: picture?.auto, layout: (options.layout ?? "tree") !== "tree" || options.headers === true }, picture?.inks);
172
+ return styleFor(`.${scope}`, theme, { road: options.look === "road", links: typeof options.href === "function", follow: options.highlightPath !== undefined, bgAuto: picture?.auto, layout: (options.layout ?? "tree") !== "tree" || options.headers === true, judo: options.judo === true }, picture?.inks);
172
173
  }
173
174
  /** The groups' names with the east first, for the series card: the names the teams carry, ordered so that a name with "east" comes before one with "west". */
174
175
  function eastFirst(input) {
@@ -181,6 +182,8 @@ function eastFirst(input) {
181
182
  }
182
183
  // ---------------------------------------------------------------------------------------------- the plan
183
184
  function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
185
+ // The ties kept apart below the tree are a little taller in the road look when any of them has a note to write (a judo contest's method and time), so that the note has its line.
186
+ const OH = road && [...layout.other, ...layout.robin, ...layout.playIn].some((i) => noteParts(layout.ties[i], v).length > 0) ? 66 : OTHER_H;
184
187
  // The layouts that draw each tie their own way take the tree's places and lines, and fill the boxes themselves.
185
188
  const V = VARIANTS.includes(kind) ? kind : null;
186
189
  const pad = vertical ? 14 : 22;
@@ -238,7 +241,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
238
241
  const top = pad + titleH + (hasGroups ? groupRoom : 0) + labelRoom;
239
242
  others = othersOf(layout);
240
243
  ow = clamp(D * 1.1, 120, 200);
241
- extraH0 = othersHeight(others.length, W0, pad, ow, OTHER_H) + thirdBlock;
244
+ extraH0 = othersHeight(others.length, W0, pad, ow, OH) + thirdBlock;
242
245
  const bodyH = size.height === null ? rows * 60 : size.height - top - pad - extraH0;
243
246
  rowStep = bodyH / rows;
244
247
  S = clamp(rowStep - (road ? 7 : 9), 24, road ? 132 : 104);
@@ -264,6 +267,8 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
264
267
  const tagW = tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 8))) + 8 : 0;
265
268
  // The road look's marks and scores are large: they are scaled down together until a row fits its box, before a name is dropped for its abbreviation.
266
269
  const anyNotes = layout.ties.some((t) => noteParts(t, v).length > 0);
270
+ // The widest score a row may carry: two digits, or a judo contest's marks ("W2 Y3").
271
+ const probe = layout.ties.reduce((best, t) => [0, 1].reduce((b, k) => { const x = sideScore(t, k, v); return wide(x, 10) > wide(b, 10) ? x : b; }, best), "00");
267
272
  // The road look's boxes of marks alone carry each score under its mark, unless marks alone were asked for.
268
273
  const markScores = road && o.detail !== "mark" && (o.scores ?? true) && layout.ties.some((t) => t.result.score !== null);
269
274
  const fitOf = (w, h, named) => {
@@ -273,7 +278,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
273
278
  const anyPens = layout.ties.some((t) => t.result.pens !== null);
274
279
  const extra = (anyBye ? wide(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? wide("(00)", Math.max(score * 0.55, 8)) + 5 : 0);
275
280
  const fixed = clamp(rowH * 0.16, 5, 12) + clamp(rowH * 0.18, 5, 10) + 8 + 10 + (tagW > 0 ? tagW + 3 : 0);
276
- const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + wide("00", score) + extra;
281
+ const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + wide(probe, score) + extra;
277
282
  return Math.min(1, (w - fixed) / varying);
278
283
  };
279
284
  let fitNow = 1;
@@ -284,7 +289,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
284
289
  const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
285
290
  const anyPens = layout.ties.some((t) => t.result.pens !== null);
286
291
  const extra = (anyBye ? boldWidth(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? boldWidth("(00)", Math.max(font - 2, 8)) + 3 : 0);
287
- const needed = 6 + md + 5 + longest * (font / 13) + 8 + boldWidth("00", score) + extra + 14;
292
+ const needed = 6 + md + 5 + longest * (font / 13) + 8 + boldWidth(probe, score) + extra + 14;
288
293
  const named = o.detail && o.detail !== "auto" ? o.detail : w >= needed && h >= 36 ? "full" : w >= 76 && h >= 28 ? "compact" : "mark";
289
294
  return named === "mark" && o.markShape === "none" ? "compact" : named;
290
295
  };
@@ -389,7 +394,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
389
394
  dl = pick(wl, hl);
390
395
  const strays = othersOf(layout);
391
396
  const ows = Math.min(availW, 200);
392
- const strayH = othersHeight(strays.length, W0, pad, ows, OTHER_H);
397
+ const strayH = othersHeight(strays.length, W0, pad, ows, OH);
393
398
  const labelH = showLabels ? 22 : 8;
394
399
  const content = (champ ? champH + 14 : 0) + labelH + hl + (withThird ? 8 + 18 + hl + 8 : 0) + strayH;
395
400
  const top0 = pad + titleH;
@@ -410,7 +415,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
410
415
  y += 8 + 18 + hl + 8;
411
416
  }
412
417
  if (strays.length > 0)
413
- othersBlock(layout, strays, y + 6, W0, pad, ows, OTHER_H, v, road, boxes, texts);
418
+ othersBlock(layout, strays, y + 6, W0, pad, ows, OH, v, road, boxes, texts);
414
419
  texts.push(...headings(v, o, W0, pad, poster));
415
420
  return finish({ width: W0, height, boxes, links: [], texts, extras, detail: dl, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
416
421
  }
@@ -518,7 +523,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
518
523
  }
519
524
  if (bottomThird)
520
525
  thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H, v, road, boxes, texts);
521
- othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OTHER_H, v, road, boxes, texts);
526
+ othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OH, v, road, boxes, texts);
522
527
  texts.push(...headings(v, o, width, pad, poster));
523
528
  return finish({ width, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
524
529
  }
@@ -531,7 +536,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
531
536
  const bandCount = halves ? 2 * bandsPerSide : nc + (champ ? 1 : 0);
532
537
  others = othersOf(layout);
533
538
  ow = Math.min(W0 - 2 * pad, 150);
534
- const othersV = othersHeight(others.length, W0, pad, ow, OTHER_H);
539
+ const othersV = othersHeight(others.length, W0, pad, ow, OH);
535
540
  // The height the page needs for boxes `h` high with `gap` between the rounds, and what it keeps for the final's band and the third-place match.
536
541
  const needOf = (h, gap) => {
537
542
  const finalHere = V === "columns" ? Math.max(h, finalH) : detail === "mark" ? Math.max(h, 52) : h;
@@ -664,7 +669,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
664
669
  }
665
670
  if (bottomThird)
666
671
  thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, road ? Math.max(boxW, 200) : 200), thirdH2, v, road, boxes, texts);
667
- othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow, OTHER_H, v, road, boxes, texts);
672
+ othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow, OH, v, road, boxes, texts);
668
673
  texts.push(...headings(v, o, W0, pad, poster));
669
674
  return finish({ width: W0, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
670
675
  }
@@ -868,7 +873,10 @@ function othersBlock(layout, list, y0, width, pad, ow, oh, v, road, boxes, texts
868
873
  return;
869
874
  const cols = Math.max(1, Math.floor((width - 2 * pad + 10) / (ow + 10)));
870
875
  const x0 = pad + (width - 2 * pad - (Math.min(cols, list.length) * (ow + 10) - 10)) / 2;
871
- texts.push(label(v, road, v.words.other, width / 2, y0 + 12, "middle"));
876
+ const stages = new Set(list.map((i) => layout.ties[i].stage ?? "other"));
877
+ const w = v.words;
878
+ const heading = stages.size === 1 && stages.has("repechage") ? w.judoRepechage : stages.size === 1 && stages.has("bronze") ? w.judoBronze : [...stages].every((x) => x === "repechage" || x === "bronze") ? w.judoRepechageBronze : w.other;
879
+ texts.push(label(v, road, heading, width / 2, y0 + 12, "middle"));
872
880
  list.forEach((i, n) => {
873
881
  const rect = { x: x0 + (n % cols) * (ow + 10), y: y0 + 24 + Math.floor(n / cols) * (oh + 10), w: ow, h: oh };
874
882
  boxes.push({ rect, tie: layout.ties[i], order: [0, 1], bye: null, byes: [], final: false, champion: false });
@@ -932,7 +940,7 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
932
940
  const withScore = plan.markScores && scoresOn && r.score !== null;
933
941
  const tags = [0, 1].map((k) => (plan.tags ? tagOf(tie, k) : ""));
934
942
  const tagH = tags.some((t) => t !== "") ? 12 : 0;
935
- const scoreOf = (k) => (withScore && r.score ? v.number(r.score[k]) : "");
943
+ const scoreOf = (k) => (withScore ? sideScore(tie, k, v) : "");
936
944
  const pensOf = (k) => (withScore && r.pens ? `(${v.number(r.pens[k])})` : "");
937
945
  const hasPens = withScore && r.pens !== null;
938
946
  const lay = (m) => {
@@ -1028,7 +1036,13 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
1028
1036
  rowParts.push(`<rect class="tb-tagbox" x="${num(tx)}" y="${num(cy - (tf + 4) / 2)}" width="${num(tw)}" height="${num(tf + 4)}" rx="${num((tf + 4) / 2)}"/><text class="tb-tag" x="${num(tx + tw / 2)}" y="${num(cy + tf * 0.35)}" font-size="${num(tf)}" text-anchor="middle">${esc(tag)}</text>`);
1029
1037
  tx += tw + 3;
1030
1038
  }
1031
- const score = scoresOn && r.score ? v.number(r.score[k]) : "";
1039
+ const score = scoresOn ? sideScore(tie, k, v) : "";
1040
+ // A judo contest: a bar at the row's edge in the colour of the side (the first is white, the second blue) and its shido cards beside the score.
1041
+ if (tie.judo)
1042
+ rowParts.push(`<rect class="${k === 0 ? "tb-jw" : "tb-jb"}" x="${num(x + 1.5)}" y="${num(cy - rowH * 0.3)}" width="3" height="${num(rowH * 0.6)}" rx="1.5"/>`);
1043
+ const cards = tie.judo && scoresOn ? shidoCards(tie, k) : [];
1044
+ const cardSize = Math.max(5, Math.min(9, rowH * 0.3));
1045
+ const cardsW = cards.length ? cards.length * (cardSize * 0.72 + 2) + 4 : 0;
1032
1046
  const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
1033
1047
  const bye = p.byes.includes(k);
1034
1048
  const right = x + w - padX;
@@ -1040,7 +1054,8 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
1040
1054
  const byeFits = bye && right - tx - (scoreW + pensW) - (mw(v.words.bye, small2) + 6) - 6 >= mw(`${seed}${nm}`, font);
1041
1055
  const byeText = byeFits ? v.words.bye : "";
1042
1056
  const byeW = byeText ? mw(byeText, small2) : 0;
1043
- const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6;
1057
+ const cardsOn = cardsW > 0 && right - tx - (scoreW + pensW) - cardsW - 6 >= 36;
1058
+ const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6 - (cardsOn ? cardsW : 0);
1044
1059
  // A name that does not fit is set a little smaller before it is cut.
1045
1060
  let nameFont = font;
1046
1061
  while (nameFont > 9.5 && mw(`${seed}${nm}`, nameFont) > avail)
@@ -1049,6 +1064,10 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
1049
1064
  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>`);
1050
1065
  if (score)
1051
1066
  rowParts.push(`<text class="tb-score" x="${num(right)}" y="${num(cy + scoreFont * 0.35)}" font-size="${num(scoreFont)}" text-anchor="end" font-variant-numeric="tabular-nums">${esc(score)}</text>`);
1067
+ if (cardsOn) {
1068
+ const cw = cardSize * 0.72;
1069
+ cards.forEach((c, n) => rowParts.push(`<rect class="${c === "r" ? "tb-sh-r" : "tb-sh-y"}" x="${num(right - scoreW - pensW - cardsW + 2 + n * (cw + 2))}" y="${num(cy - cardSize / 2)}" width="${num(cw)}" height="${num(cardSize)}" rx="1"/>`));
1070
+ }
1052
1071
  if (pensText)
1053
1072
  rowParts.push(`<text class="tb-muted" x="${num(right - scoreW - (road ? 4 : 3))}" y="${num(cy + small * 0.35)}" font-size="${num(small)}" text-anchor="end">${esc(pensText)}</text>`);
1054
1073
  if (byeText)
@@ -1059,7 +1078,7 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
1059
1078
  if (noteH) {
1060
1079
  const f = road ? 9 : 9.5;
1061
1080
  const line = road ? v.upper(notes.join(" · ")) : notes.join(" · ");
1062
- parts.push(`<text class="tb-muted${road ? " tb-note" : ""}" x="${num(x + w / 2)}" y="${num(y + h - (road ? 5 : 4))}" font-size="${f}"${road ? ' font-weight="700" letter-spacing=".05em"' : ""} text-anchor="middle">${esc(fit(line, f * (road ? 1.1 : 1), w - 8))}</text>`);
1081
+ parts.push(`<text class="tb-muted${road ? " tb-note" : ""}" x="${num(x + w / 2)}" y="${num(y + h - (road ? 5 : 4))}" font-size="${f}"${road ? ' font-weight="700" letter-spacing=".05em"' : ""} text-anchor="middle">${esc(fit(line, f * (road ? 1.1 : 1), w - 8, boldWidth))}</text>`);
1063
1082
  }
1064
1083
  }
1065
1084
  const extra = tieClassOf(o, tie);
package/dist/table.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { layoutBracket } from "./layout.js";
2
2
  import { noteLong } from "./matchup.js";
3
+ import { sideScore } from "./judo-text.js";
3
4
  import { fill, voiceOf } from "./locales.js";
4
5
  import { localizedLayout } from "./render.js";
5
6
  import { roundLabel } from "./rounds.js";
@@ -23,12 +24,12 @@ export function bracketTable(input, options = {}) {
23
24
  const text = team ? esc(team.name) : esc(w.tbd);
24
25
  return t.winner === k ? `<strong>${text}</strong>` : text;
25
26
  };
26
- const score = (t) => (t.result.score ? fill(w.scoreText, { a: v.number(t.result.score[0]), b: v.number(t.result.score[1]) }) : "");
27
+ const score = (t) => (t.result.score ? fill(w.scoreText, { a: sideScore(t, 0, v), b: sideScore(t, 1, v) }) : "");
27
28
  // Where a side comes from, in words, when the tie says so: "Loser of A v B".
28
29
  const named = (i) => fill(w.tieVs, { a: layout.ties[i].sides[0]?.name ?? w.tbd, b: layout.ties[i].sides[1]?.name ?? w.tbd });
29
30
  const origins = (t) => (t.from ?? []).flatMap((s) => (s ? [fill(s.result === "loser" ? w.loserOf : w.winnerOf, { name: named(s.tie) })] : []));
30
31
  const rows = order.map((t) => {
31
- const roundText = t.third ? w.thirdPlace : t.playIn ? w.playIn : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
32
+ const roundText = t.third ? w.thirdPlace : t.stage === "repechage" ? w.judoRepechage : t.stage === "bronze" ? w.judoBronze : t.playIn ? w.playIn : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
32
33
  const date = dated ? `<td>${esc(v.date(t.date) ?? "")}</td>` : "";
33
34
  return `<tr><th scope="row">${esc(roundText)}</th>${date}<td>${name(t, 0)}</td><td>${esc(score(t))}</td><td>${name(t, 1)}</td><td>${esc([...noteLong(t, v), ...origins(t)].join(w.noteJoin))}</td></tr>`;
34
35
  });
package/dist/theme.d.ts CHANGED
@@ -36,7 +36,16 @@ export interface StyleParts {
36
36
  bgAuto?: boolean;
37
37
  /** The rules of the layouts of their own (slots, shields, conference colours, header tiles, arrows, group cards). */
38
38
  layout?: boolean;
39
+ /** A judo bracket: the white and blue side bars and the shido cards (`--tb-judo-white`, `--tb-judo-blue`, `--tb-shido`, `--tb-hansoku`). */
40
+ judo?: boolean;
39
41
  }
42
+ /** The colours of a judo contest: the white and the blue judogi, the yellow shido card and the red hansoku-make card. */
43
+ export declare const JUDO_COLORS: Readonly<{
44
+ white: "#ffffff";
45
+ blue: "#1f4fa8";
46
+ shido: "#f0c000";
47
+ hansoku: "#d3302a";
48
+ }>;
40
49
  /** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
41
50
  export declare const CONFERENCE_COLORS: readonly [string, string];
42
51
  /** The colours a background says to draw in (`backgroundSvg`'s `ink`): what `styleFor` takes in place of a look's own palette. */
package/dist/theme.js CHANGED
@@ -10,6 +10,8 @@ export const ROAD_THEMES = Object.freeze({
10
10
  poster: { ring: "#f6f1e3", edge: "rgba(0,0,0,.5)", panel: "#1b2766", spine: "#d9b24c" },
11
11
  });
12
12
  const FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Hiragino Sans", "Yu Gothic UI", "Noto Sans CJK JP", Meiryo, sans-serif';
13
+ /** The colours of a judo contest: the white and the blue judogi, the yellow shido card and the red hansoku-make card. */
14
+ export const JUDO_COLORS = Object.freeze({ white: "#ffffff", blue: "#1f4fa8", shido: "#f0c000", hansoku: "#d3302a" });
13
15
  /** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
14
16
  export const CONFERENCE_COLORS = Object.freeze(["#2457c5", "#b3261e"]);
15
17
  /** A palette and a road palette from a background's ink. */
@@ -59,6 +61,9 @@ function rules(p, r, poster, withFont, parts, side) {
59
61
  const [ca, cb] = CONFERENCE_COLORS;
60
62
  list.push([[".tb-slot"], `fill:${v("plain")};fill-opacity:.55;stroke:${v("edge")};stroke-width:1`], [[".tb-won .tb-slot"], `fill-opacity:.9;stroke:${v("win")};stroke-width:1.6`], [[".tb-lost .tb-slot"], "fill-opacity:.35"], [[".tb-emptybar"], `fill:${v("plain")};opacity:.8`], [[".tb-seed"], "font-variant-numeric:tabular-nums"], [[".tb-seedn"], `fill:${v("muted")};opacity:.85`], [[".tb-shield"], `fill:none;stroke:${v("edge")};stroke-width:1.4`], [[".tb-won .tb-shield"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-shield.tb-empty"], `fill:${v("plain")};fill-opacity:.5`], [[".tb-vs", ".tb-code"], `fill:${v("muted")};font-weight:700`], [[".tb-c0 .tb-box"], `stroke:var(--tb-conf-a, ${ca});stroke-width:1.6`], [[".tb-c1 .tb-box"], `stroke:var(--tb-conf-b, ${cb});stroke-width:1.6`], [[".tb-atag"], `fill:${v("ink")}`], [[".tb-atag-t"], `fill:${v("bg")}`], [[".tb-c0 .tb-atag"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-c1 .tb-atag"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-c0 .tb-atag-t", ".tb-c1 .tb-atag-t"], "fill:#fff"], [[".tb-head"], `fill:${rv("panel", "panel")}`], [[".tb-head.tb-c0"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-head.tb-c1"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-head-t"], `fill:${v("ink")};font-weight:800`], [[".tb-head.tb-c0 + .tb-head-t", ".tb-head.tb-c1 + .tb-head-t"], "fill:#fff"], [[".tb-arrow"], `stroke:${v("line")};stroke-width:1.6;fill:none;stroke-linecap:round;stroke-linejoin:round`], [[".tb-arrow-loser"], "stroke-dasharray:5 4"], [[".tb-arrowhead"], `fill:${v("line")};stroke:none`], [[".tb-gcard"], `fill:${v("box")};stroke:${v("edge")};stroke-width:1`], [[".tb-gtile"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-gtile-t"], "fill:#fff;font-weight:800"], [[".tb-ghead"], `fill:${v("muted")};font-weight:700`], [[".tb-gcell"], `fill:${v("ink")};font-variant-numeric:tabular-nums`], [[".tb-gcell-p"], "font-weight:800"], [[".tb-gname"], `fill:${v("ink")};font-weight:700`], [[".tb-qual"], `fill:${v("win")};fill-opacity:.16`], [[".tb-gsep"], `stroke:${v("edge")};stroke-width:1;stroke-opacity:.6`]);
61
63
  }
64
+ if (parts.judo === true) {
65
+ list.push([[".tb-jw"], `fill:var(--tb-judo-white, ${JUDO_COLORS.white});stroke:${v("edge")};stroke-width:.8`], [[".tb-jb"], `fill:var(--tb-judo-blue, ${JUDO_COLORS.blue});stroke:${v("edge")};stroke-width:.8`], [[".tb-sh-y"], `fill:var(--tb-shido, ${JUDO_COLORS.shido});stroke:rgba(0,0,0,.35);stroke-width:.6`], [[".tb-sh-r"], `fill:var(--tb-hansoku, ${JUDO_COLORS.hansoku});stroke:rgba(0,0,0,.35);stroke-width:.6`]);
66
+ }
62
67
  if (parts.follow === true) {
63
68
  list.push([[".tb-tie"], "opacity:.4"], [[".tb-tie.tb-on-path"], "opacity:1"], [[".tb-on-path .tb-box"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-line"], "stroke-opacity:.25"], [[".tb-line.tb-path"], `stroke:${v("win")};stroke-width:4.5;stroke-opacity:1;stroke-linecap:round;stroke-dasharray:.1 9`]);
64
69
  }
package/dist/types.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * The types of Taisen: what goes in (teams and matchups), what the layout works out (ties, rounds, halves) and what
3
3
  * the drawing takes as options. Nothing here runs; the functions are in `matchup.ts`, `layout.ts` and `render.ts`.
4
4
  */
5
+ import type { JudoOutcome, JudoResult } from "./judo-model.ts";
5
6
  /** A side's key: a team code, a franchise id, anything that is unique within one bracket. */
6
7
  export type SideId = string | number;
7
8
  /** A side of a tie: a team, with the marks and labels a drawing needs. Only `id` and `name` are required. */
@@ -106,8 +107,13 @@ export interface Matchup {
106
107
  walkover?: boolean;
107
108
  /** This is the third-place match: it leads nowhere and is drawn apart. */
108
109
  third?: boolean;
109
- /** `"play-in"`: a game of a play-in (the NBA's) that is outside the rounds of the tree: its winner (and loser, see `feeds`) goes on to a place the tree's ties name. Drawn by the `playin` layout and, apart, under the other layouts' trees. */
110
- stage?: "play-in";
110
+ /**
111
+ * `"play-in"`: a game of a play-in (the NBA's) that is outside the rounds of the tree: its winner (and loser, see `feeds`) goes on to a place the tree's ties name. Drawn by the `playin` layout and, apart, under the other layouts' trees.
112
+ * `"repechage"` and `"bronze"`: a judo repechage contest and a bronze medal contest, fed by the losers of the tree's ties (`judoBracket` writes them); drawn apart under the tree.
113
+ */
114
+ stage?: "play-in" | "repechage" | "bronze";
115
+ /** A judo contest's scores: ippon, waza-ari, yuko, shido, hansoku-make, golden score and time. The first side is white, the second blue. The score, the winner and the notes come from it; see `judoOutcome`. */
116
+ judo?: JudoResult;
111
117
  /** The side playing at home (0 or 1), for the `at` wording of the `columns` layout: the home side is drawn second, after "AT". Left out, the second side. */
112
118
  home?: 0 | 1;
113
119
  note?: string;
@@ -151,8 +157,12 @@ export interface Tie {
151
157
  seeds?: readonly [string | number | null, string | number | null];
152
158
  /** Where each side comes from, when the matchup's `feeds` name it: another tie's winner or its loser. Left out for a tie that says nothing of the kind. */
153
159
  from?: readonly [Source | null, Source | null];
154
- /** The tie is a play-in game (`Matchup.stage`). */
160
+ /** The tie is a play-in game, a judo repechage contest or a bronze medal contest (`Matchup.stage`). */
155
161
  playIn?: boolean;
162
+ /** The tie's `Matchup.stage`, when it has one. */
163
+ stage?: "play-in" | "repechage" | "bronze";
164
+ /** A judo contest's result, checked and worked out (`Matchup.judo`). */
165
+ judo?: JudoOutcome;
156
166
  /** The side at home (`Matchup.home`), when given. */
157
167
  home?: 0 | 1;
158
168
  }
@@ -342,6 +352,46 @@ export interface Words {
342
352
  /** `{name}`: a side that is the winner or loser of another tie, in a sentence or a table: "Loser of {name}". */
343
353
  winnerOf: string;
344
354
  loserOf: string;
355
+ /** Judo. The marks of the scores on a box (ippon, waza-ari, yuko), the same written out, and how a contest was won. */
356
+ judoIppon: string;
357
+ judoWazaAri: string;
358
+ judoYuko: string;
359
+ judoIpponLong: string;
360
+ judoWazaAriLong: string;
361
+ judoYukoLong: string;
362
+ judoMethodIppon: string;
363
+ judoMethodAwasete: string;
364
+ judoMethodScore: string;
365
+ judoMethodHansoku: string;
366
+ judoMethodFusen: string;
367
+ judoMethodKiken: string;
368
+ /** Golden score, short and in a sentence. */
369
+ judoGolden: string;
370
+ judoGoldenLong: string;
371
+ /** `{t}`: the contest time ("3:42"). */
372
+ judoTime: string;
373
+ /** `{name}`, `{n}`: a side's shido ("Kenji Nakamura: 2 shido"). */
374
+ judoShidoOf: string;
375
+ /** `{a}`, `{b}`: the sides' shido in a short note. */
376
+ judoShidoShort: string;
377
+ /** The headings over the repechage contests and the bronze medal contests, alone and together; the sides' colours. */
378
+ judoRepechage: string;
379
+ judoBronze: string;
380
+ judoRepechageBronze: string;
381
+ judoWhite: string;
382
+ judoBlue: string;
383
+ /** `{sex}`, `{w}` (the number of kilograms, no sign): a weight category up to and including that weight ("Men -66 kg"), one over it ("Women +78 kg"), and the two sexes' words. */
384
+ judoWeightClass: string;
385
+ judoWeightOver: string;
386
+ judoMen: string;
387
+ judoWomen: string;
388
+ /** The scoreboard of a contest: the osaekomi timer's label, the shido row's, a mat (`{n}`), a contest (`{n}`), the coach (`{name}`), and the drawing's default title. */
389
+ judoOsaekomi: string;
390
+ judoShido: string;
391
+ judoMat: string;
392
+ judoContest: string;
393
+ judoCoach: string;
394
+ judoBoardTitle: string;
345
395
  }
346
396
  /** A language: its words, the names of the rounds (by their English names), and the tag `Intl` formats numbers and dates for. */
347
397
  export interface Locale {
@@ -352,6 +402,7 @@ export interface Locale {
352
402
  /** Round names by their English name: `{ "Round of 16": "Huitièmes de finale" }`. A round with no entry keeps its name (or `roundOf`). */
353
403
  rounds: Readonly<Record<string, string>>;
354
404
  }
405
+ export type { JudoMethod, JudoOutcome, JudoResult, JudoSide } from "./judo-model.ts";
355
406
  export type ThemeName = "auto" | "light" | "dark" | "poster" | "none";
356
407
  export type SizeName = "wide" | "square" | "phone" | "card";
357
408
  export interface Size {
@@ -434,6 +485,8 @@ export interface DrawOptions extends LayoutOptions {
434
485
  standings?: "points" | "record";
435
486
  /** In the `groups` layout, how many teams of each group go through: their rows are marked. Default 0, none. */
436
487
  qualify?: number;
488
+ /** For `bracketStyle`: include the judo rules (the white and blue side bars, the shido cards). `drawBracket` adds them by itself when a tie has judo scores. */
489
+ judo?: boolean;
437
490
  /** Write the `<style>` into the drawing. Default true; false leaves it out, for a second drawing of a page that shares the first's (`styleScope`, `bracketStyle`). */
438
491
  style?: boolean;
439
492
  /** A class that carries this drawing's rules in place of its own: two drawings with the same `styleScope` take their colours from one `<style>`, and the second can leave it out (`style: false`). A word of letters, digits, `-` and `_`. */
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.6.1";
1
+ export declare const VERSION = "0.8.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.1";
1
+ export const VERSION = "0.8.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/taisen-brackets",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
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, as a tall card or for a phone, in a classic or a road look (large round flags, big scores, a team's dotted path, a link on every box), in light, dark or a gold-on-navy poster look, in eight layouts (the tree, rows, shields, a score-update card, tall columns, the NBA's play-in, a series card and group tables), on one of twenty-five poster backgrounds (the playing surfaces of five sports among them) or your own, with each team's logo or round flag, the champion in the centre, and an accessible table; and the road to the final of either finalist. 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",