@diagrammo/dgmo 0.87.0 → 0.88.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.
@@ -3609,6 +3609,12 @@ interface GanttInteractiveOptions {
3609
3609
  * change), matching every other ControlsStrip-hosted chart type.
3610
3610
  */
3611
3611
  controlsHost?: 'app' | 'inline';
3612
+ /**
3613
+ * The host scales the SVG to fit the container on both axes (the app's
3614
+ * preview pane). A chart taller than the container is laid out wider, to the
3615
+ * container's aspect ratio, so it fills the width instead of a centred column.
3616
+ */
3617
+ fitToContainer?: boolean;
3612
3618
  /** Initial Critical Path highlight state (used when `controlsHost` is `'app'`). */
3613
3619
  criticalPathActive?: boolean;
3614
3620
  /** Initial Dependencies-arrow visibility (used when `controlsHost` is `'app'`). */
package/dist/advanced.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import {
2
- layoutBracket,
3
2
  renderBracket,
4
3
  renderBracketForExport
5
- } from "./chunk-Q2U5635V.js";
4
+ } from "./chunk-WXPBP4IT.js";
6
5
  import {
7
6
  renderRaci,
8
7
  renderRaciForExport
@@ -134,7 +133,7 @@ import {
134
133
  import {
135
134
  buildTagLaneRowList,
136
135
  renderGantt
137
- } from "./chunk-AGO3PFGS.js";
136
+ } from "./chunk-MVCPO6LI.js";
138
137
  import {
139
138
  renderState,
140
139
  renderStateForExport
@@ -295,7 +294,7 @@ import {
295
294
  focusEventLine,
296
295
  renderEventLine,
297
296
  renderEventLineForExport
298
- } from "./chunk-FBKPSU3A.js";
297
+ } from "./chunk-5LD7SHXO.js";
299
298
  import "./chunk-NPWVOFB3.js";
300
299
  import "./chunk-R2K5L4UA.js";
301
300
  import {
@@ -323,7 +322,10 @@ import {
323
322
  renderSlopeChart,
324
323
  renderVenn,
325
324
  renderWordCloud
326
- } from "./chunk-PJQZWJ5I.js";
325
+ } from "./chunk-VW52W5F6.js";
326
+ import {
327
+ layoutBracket
328
+ } from "./chunk-ABP5BUD6.js";
327
329
  import "./chunk-D4M25W6R.js";
328
330
  import {
329
331
  CHART_TYPE_DESCRIPTIONS,
@@ -377,7 +379,7 @@ import "./chunk-2CW5ZTNH.js";
377
379
  import {
378
380
  formatDateLabel,
379
381
  renderTimeline
380
- } from "./chunk-LPXC4I4R.js";
382
+ } from "./chunk-JLETTWUJ.js";
381
383
  import {
382
384
  computeTimeTicks
383
385
  } from "./chunk-YRYKTYMQ.js";
package/dist/block.js CHANGED
@@ -4,7 +4,8 @@ import {
4
4
  } from "./chunk-D3K4U3ZM.js";
5
5
  import {
6
6
  render
7
- } from "./chunk-PJQZWJ5I.js";
7
+ } from "./chunk-VW52W5F6.js";
8
+ import "./chunk-ABP5BUD6.js";
8
9
  import {
9
10
  docsLink,
10
11
  renderErrorCard
@@ -23,7 +24,7 @@ import "./chunk-2CW5ZTNH.js";
23
24
  import {
24
25
  escapeAttr,
25
26
  escapeHtml
26
- } from "./chunk-LPXC4I4R.js";
27
+ } from "./chunk-JLETTWUJ.js";
27
28
  import "./chunk-YRYKTYMQ.js";
28
29
  import "./chunk-24YPR5EH.js";
29
30
  import "./chunk-XCKNFFKM.js";
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-S7W75MIM.js";
10
10
  import {
11
11
  formatDateLabel
12
- } from "./chunk-LPXC4I4R.js";
12
+ } from "./chunk-JLETTWUJ.js";
13
13
  import {
14
14
  CARD_RADIUS,
15
15
  HAIRLINE_DASH,
@@ -1,51 +1,10 @@
1
- import {
2
- renderInlineText
3
- } from "./chunk-S7W75MIM.js";
4
- import {
5
- layoutInlineHeader
6
- } from "./chunk-3DOQOXXI.js";
7
- import {
8
- TITLE_FONT_SIZE,
9
- TITLE_FONT_WEIGHT,
10
- TITLE_Y
11
- } from "./chunk-VLC6WNRD.js";
12
- import {
13
- renderIntegratedLegend
14
- } from "./chunk-LHL6HDUL.js";
15
- import {
16
- getLegendExtent,
17
- getMaxLegendReservedHeight
18
- } from "./chunk-FM3WAVB6.js";
19
- import {
20
- FONT_FAMILY
21
- } from "./chunk-ICLB27PZ.js";
22
- import {
23
- LEGEND_GROUP_GAP
24
- } from "./chunk-RH2PROYG.js";
25
1
  import {
26
2
  measureText
27
3
  } from "./chunk-KAO7JL4I.js";
28
- import {
29
- resolveTagColor,
30
- tagAttrKey
31
- } from "./chunk-L5AFONQU.js";
32
- import {
33
- contrastText,
34
- getSeriesColors,
35
- mix,
36
- shapeFill,
37
- themeBaseBg
38
- } from "./chunk-7HWY4QAJ.js";
39
- import {
40
- resolveColor
41
- } from "./chunk-2M4IGSCY.js";
42
4
  import {
43
5
  makeDgmoError
44
6
  } from "./chunk-BYU54C3S.js";
45
7
 
46
- // src/bracket/renderer.ts
47
- import * as d3Selection from "d3-selection";
48
-
49
8
  // src/bracket/layout.ts
50
9
  var BOX_W = 176;
51
10
  var BOX_H = 30;
@@ -390,8 +349,8 @@ function layoutBracket(parsed) {
390
349
  (g) => g.built.nodes.map((n) => commentaryLineCount(n.commentary))
391
350
  )
392
351
  );
393
- const COMMENT_LH2 = 15;
394
- const rowScale = maxComment > 0 ? Math.max(ROW, 2 * BOX_H + PAIR_GAP + maxComment * COMMENT_LH2 + 16) : ROW;
352
+ const COMMENT_LH = 15;
353
+ const rowScale = maxComment > 0 ? Math.max(ROW, 2 * BOX_H + PAIR_GAP + maxComment * COMMENT_LH + 16) : ROW;
395
354
  for (const g of groups) positionSide(g.built, rowScale);
396
355
  const champResults = parsed.matches.filter(
397
356
  (m) => m.side === null && parsed.sides.length >= 1
@@ -560,7 +519,7 @@ function layoutBracket(parsed) {
560
519
  const wl = commentaryLineCount(m.commentary);
561
520
  maxY = Math.max(
562
521
  maxY,
563
- m.y + half + BOX_H / 2 + 13 + (wl - 1) * COMMENT_LH2 + 4
522
+ m.y + half + BOX_H / 2 + 13 + (wl - 1) * COMMENT_LH + 4
564
523
  );
565
524
  }
566
525
  }
@@ -613,391 +572,11 @@ function layoutBracket(parsed) {
613
572
  };
614
573
  }
615
574
 
616
- // src/bracket/renderer.ts
617
- var PADDING = 16;
618
- var TITLE_BAND = 40;
619
- var HALF_SPAN2 = (BOX_H + PAIR_GAP) / 2;
620
- var COMMENT_LH = 15;
621
- var LEGEND_POSITION = {
622
- placement: "top-center",
623
- titleRelation: "below-title"
624
- };
625
- function clip(s, maxW, fs, bold) {
626
- if (measureText(s, fs, { bold }) <= maxW) return s;
627
- let lo = 0;
628
- let hi = s.length;
629
- while (lo < hi) {
630
- const mid = Math.ceil((lo + hi) / 2);
631
- if (measureText(s.slice(0, mid) + "\u2026", fs, { bold }) <= maxW) lo = mid;
632
- else hi = mid - 1;
633
- }
634
- return lo <= 0 ? "" : s.slice(0, lo) + "\u2026";
635
- }
636
- function paintForAccent(palette, isDark, accent, fillMode) {
637
- const base = themeBaseBg(palette, isDark);
638
- return {
639
- accent,
640
- ink: palette.text,
641
- muted: mix(palette.text, base, 52),
642
- // Loser / TBD outlines + connectors are ink-based (not the pale border) so
643
- // they stay legible on top of a colored, shaded column band.
644
- hair: mix(palette.text, base, 34),
645
- wire: mix(palette.text, base, 30),
646
- track: mix(palette.border, base, 55),
647
- // §1.9 fill family rides through shapeFill: solid ⇒ raw accent,
648
- // outline ⇒ theme base bg (accent stays on the stroke), absent ⇒ 25% tint.
649
- tint: shapeFill(palette, accent, isDark, { mode: fillMode })
650
- };
651
- }
652
- function paintFor(palette, isDark, sideColor, fillMode) {
653
- const accent = sideColor && resolveColor(sideColor, palette) || sideColor || getSeriesColors(palette)[0];
654
- return paintForAccent(palette, isDark, accent, fillMode);
655
- }
656
- function resolveBracketLegend(parsed, palette) {
657
- const activeTag = parsed.activeTag;
658
- const activeGroup = activeTag ? parsed.tagGroups.find((g) => tagAttrKey(g.name) === tagAttrKey(activeTag)) : void 0;
659
- const legendGroups = activeGroup && !parsed.noLegend ? [
660
- {
661
- name: activeGroup.name,
662
- entries: activeGroup.entries.map((e) => ({
663
- value: e.value,
664
- color: resolveColor(e.color, palette) ?? e.color
665
- }))
666
- }
667
- ] : [];
668
- return { activeGroup, legendGroups };
669
- }
670
- function bracketExportDimensions(parsed, palette) {
671
- const layout = layoutBracket(parsed);
672
- const width = layout.width + PADDING * 2;
673
- const titleH = parsed.title ? TITLE_BAND : 0;
674
- const { legendGroups } = resolveBracketLegend(parsed, palette);
675
- const legendReserve = legendGroups.length > 0 ? getMaxLegendReservedHeight(
676
- { groups: legendGroups, position: LEGEND_POSITION, mode: "export" },
677
- width
678
- ) + LEGEND_GROUP_GAP : 0;
679
- const height = titleH + legendReserve + PADDING + layout.height + PADDING;
680
- return { width, height };
681
- }
682
- function renderBracket(container, parsed, palette, isDark, exportDims) {
683
- d3Selection.select(container).selectAll(":not([data-d3-tooltip])").remove();
684
- if (parsed.error) return;
685
- const layout = layoutBracket(parsed);
686
- const width = exportDims?.width ?? container.clientWidth;
687
- const height = exportDims?.height ?? container.clientHeight;
688
- if (width <= 0 || height <= 0) return;
689
- const showTitle = !!parsed.title;
690
- const titleH = showTitle ? TITLE_BAND : 0;
691
- const exportMode = exportDims !== void 0;
692
- const seedByName = /* @__PURE__ */ new Map();
693
- for (const s of parsed.seeds)
694
- if (!seedByName.has(s.name)) seedByName.set(s.name, s.seed);
695
- const activeTag = parsed.activeTag;
696
- const activeKey = activeTag ? tagAttrKey(activeTag) : null;
697
- const tagValueOf = (name) => {
698
- if (!name || !activeKey) return null;
699
- const meta = parsed.competitorMeta.get(name);
700
- return meta?.[activeKey] ?? null;
701
- };
702
- const tagColorOf = (name) => {
703
- if (!name || !activeTag) return null;
704
- const meta = parsed.competitorMeta.get(name);
705
- if (!meta || !activeKey || !meta[activeKey]) return null;
706
- const c = resolveTagColor(meta, [...parsed.tagGroups], activeTag);
707
- return c ? resolveColor(c, palette) ?? c : null;
708
- };
709
- const { activeGroup, legendGroups } = resolveBracketLegend(parsed, palette);
710
- const legendReserve = legendGroups.length > 0 ? getMaxLegendReservedHeight(
711
- { groups: legendGroups, position: LEGEND_POSITION, mode: "preview" },
712
- width
713
- ) + LEGEND_GROUP_GAP : 0;
714
- const inlineRequested = parsed.legendInline === true;
715
- const legendExtent = inlineRequested && legendGroups.length > 0 ? getLegendExtent(
716
- {
717
- groups: legendGroups,
718
- position: {
719
- placement: "top-center",
720
- titleRelation: "inline-with-title"
721
- },
722
- mode: exportMode ? "export" : "preview"
723
- },
724
- { activeGroup: activeGroup?.name ?? null },
725
- width
726
- ) : { width: 0, height: 0 };
727
- const header = layoutInlineHeader({
728
- requested: inlineRequested,
729
- title: parsed.title ?? "",
730
- hasLegend: legendGroups.length > 0,
731
- legendWidth: legendExtent.width,
732
- legendHeight: legendExtent.height,
733
- containerWidth: width,
734
- titleBandHeight: titleH,
735
- legendReserve,
736
- titleBaselineY: TITLE_Y,
737
- titleFontSize: TITLE_FONT_SIZE
738
- });
739
- const svg = d3Selection.select(container).append("svg").attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`).attr("preserveAspectRatio", "xMidYMin meet").attr("xmlns", "http://www.w3.org/2000/svg").attr("class", "dgmo-bracket").attr("role", "img").attr(
740
- "aria-label",
741
- `Tournament bracket${parsed.title ? ` \u2014 ${parsed.title}` : ""}${layout.champion ? `, champion ${layout.champion}` : ""}`
742
- ).style("font-family", FONT_FAMILY);
743
- if (showTitle) {
744
- const t = svg.append("text").attr("class", "chart-title").attr("x", header.titleX).attr("y", TITLE_Y).attr("text-anchor", header.titleAnchor).attr("fill", palette.text).attr("font-family", FONT_FAMILY).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).text(parsed.title);
745
- if (parsed.titleLineNumber !== null)
746
- t.attr("data-line-number", parsed.titleLineNumber);
747
- }
748
- const areaY = (header.inline ? titleH : titleH + legendReserve) + PADDING;
749
- const areaX = PADDING;
750
- const areaW = Math.max(1, width - PADDING * 2);
751
- const areaH = Math.max(1, height - areaY - PADDING);
752
- const scale = Math.min(1, areaW / layout.width, areaH / layout.height);
753
- const offsetX = areaX + (areaW - layout.width * scale) / 2;
754
- const offsetY = areaY;
755
- const root = svg.append("g").attr("class", "dgmo-bracket-body").attr("transform", `translate(${offsetX},${offsetY}) scale(${scale})`);
756
- if (legendGroups.length > 0) {
757
- const legendG = svg.append("g").attr("class", "dgmo-bracket-legend").attr(
758
- "transform",
759
- header.inline ? `translate(${header.legendX}, ${header.legendY})` : `translate(0, ${titleH})`
760
- );
761
- renderIntegratedLegend(legendG, {
762
- groups: legendGroups,
763
- palette: {
764
- bg: palette.bg,
765
- surface: palette.surface,
766
- text: palette.text,
767
- textMuted: palette.textMuted,
768
- primary: palette.primary
769
- },
770
- isDark,
771
- width,
772
- mode: exportMode ? "export" : "preview",
773
- position: {
774
- placement: "top-center",
775
- titleRelation: header.inline ? "inline-with-title" : "below-title"
776
- },
777
- activeGroup: activeGroup?.name ?? null
778
- });
779
- }
780
- const baseAccent = parsed.accentColor ?? resolveColor("blue", palette) ?? getSeriesColors(palette)[0];
781
- const fillMode = parsed.fillMode;
782
- const basePaint = paintForAccent(palette, isDark, baseAccent, fillMode);
783
- const sideColorByLabel = /* @__PURE__ */ new Map();
784
- for (const s of parsed.sides) sideColorByLabel.set(s.label, s.color);
785
- const sideBarH = layout.sideLabels.length ? 30 : 0;
786
- const bandTop = sideBarH + 2;
787
- const roundY = sideBarH + 24;
788
- for (const col of layout.columns) {
789
- if (!col.color) continue;
790
- const covered = layout.sideLabels.some(
791
- (s) => col.x >= s.x0 && col.x <= s.x1
792
- );
793
- const top = covered ? bandTop : 2;
794
- const c = resolveColor(col.color, palette) ?? col.color;
795
- root.append("rect").attr("x", col.x - BOX_W / 2 - 10).attr("y", top).attr("width", BOX_W + 20).attr("height", Math.max(0, layout.height - top)).attr("rx", 10).attr(
796
- "fill",
797
- fillMode === "outline" ? themeBaseBg(palette, isDark) : mix(c, palette.bg, isDark ? 12 : 8)
798
- ).call((sel) => {
799
- if (fillMode === "outline")
800
- sel.attr("stroke", mix(c, palette.bg, 35)).attr("stroke-width", 1);
801
- });
802
- }
803
- for (const s of layout.sideLabels) {
804
- const c = s.color ? resolveColor(s.color, palette) ?? s.color : basePaint.accent;
805
- root.append("rect").attr("x", s.x0 - 8).attr("y", 2).attr("width", s.x1 - s.x0 + 16).attr("height", 24).attr("rx", 8).attr(
806
- "fill",
807
- fillMode === "outline" ? themeBaseBg(palette, isDark) : mix(c, palette.bg, isDark ? 20 : 14)
808
- ).attr("stroke", fillMode === "outline" ? c : mix(c, palette.bg, 55)).attr("stroke-width", 1);
809
- root.append("text").attr("x", (s.x0 + s.x1) / 2).attr("y", 18).attr("text-anchor", "middle").attr("font-size", 13).attr("font-weight", "bold").attr("letter-spacing", 0.3).attr("fill", c).text(clip(s.label, s.x1 - s.x0, 13, true));
810
- }
811
- for (const col of layout.columns) {
812
- const c = col.color ? resolveColor(col.color, palette) ?? col.color : basePaint.muted;
813
- const covered = layout.sideLabels.some(
814
- (s) => col.x >= s.x0 && col.x <= s.x1
815
- );
816
- root.append("text").attr("x", col.x).attr("y", covered ? roundY : 22).attr("text-anchor", "middle").attr("font-size", covered ? 14 : 15).attr("font-weight", "bold").attr("fill", c).attr("letter-spacing", 0.4).text(clip(col.label, BOX_W + 20, covered ? 14 : 15, true));
817
- }
818
- for (const m of layout.matches) {
819
- drawConnector(root, m, m.topFrom, m.y - HALF_SPAN2, basePaint);
820
- drawConnector(root, m, m.botFrom, m.y + HALF_SPAN2, basePaint);
821
- }
822
- for (const m of layout.matches) {
823
- const sidePaint = m.side && sideColorByLabel.get(m.side) ? paintFor(palette, isDark, sideColorByLabel.get(m.side), fillMode) : basePaint;
824
- const topSeed = seedByName.get(m.top ?? "") ?? null;
825
- const botSeed = seedByName.get(m.bot ?? "") ?? null;
826
- let homeName = m.home;
827
- if (!homeName && topSeed !== null && botSeed !== null) {
828
- homeName = topSeed <= botSeed ? m.top : m.bot;
829
- }
830
- const [wSeed, lSeed] = m.winner === "top" ? [topSeed, botSeed] : m.winner === "bot" ? [botSeed, topSeed] : [null, null];
831
- const isUpset = wSeed !== null && lSeed !== null && wSeed > lSeed && !m.isChampionship;
832
- const scoreParts = m.score ? m.score.split(/[-–]/).map((p) => p.trim()) : null;
833
- const winScore = scoreParts?.length === 2 ? scoreParts[0] : "";
834
- const loseScore = scoreParts?.length === 2 ? scoreParts[1] : "";
835
- const champWinner = m.isChampionship ? layout.champion : null;
836
- const mkInfo = (side, name) => {
837
- const isWinner = m.winner === side;
838
- const isLoser = m.winner !== null && m.winner !== side;
839
- return {
840
- isWinner,
841
- isLoser,
842
- pending: m.pending,
843
- isChampion: champWinner !== null && isWinner,
844
- score: isWinner ? winScore : isLoser ? loseScore : "",
845
- seed: seedByName.get(name ?? "") ?? null,
846
- tagColor: tagColorOf(name),
847
- tagValue: tagValueOf(name),
848
- isHome: homeName !== null && homeName === name
849
- };
850
- };
851
- drawBox(
852
- root,
853
- m.x,
854
- m.y - HALF_SPAN2,
855
- m.top,
856
- mkInfo("top", m.top),
857
- sidePaint,
858
- palette,
859
- isDark,
860
- m,
861
- activeKey,
862
- fillMode
863
- );
864
- drawBox(
865
- root,
866
- m.x,
867
- m.y + HALF_SPAN2,
868
- m.bot,
869
- mkInfo("bot", m.bot),
870
- sidePaint,
871
- palette,
872
- isDark,
873
- m,
874
- activeKey,
875
- fillMode
876
- );
877
- if (isUpset) drawUpset(root, m, sidePaint);
878
- if (m.commentary.length) drawCommentary(root, m, palette);
879
- }
880
- }
881
- function drawConnector(g, m, from, toCy, paint) {
882
- if (!from) return;
883
- const parentLeft = m.x - BOX_W / 2;
884
- const parentRight = m.x + BOX_W / 2;
885
- const childIsLeft = from.x < m.x;
886
- const childEdge = childIsLeft ? from.x + BOX_W / 2 : from.x - BOX_W / 2;
887
- const parentEdge = childIsLeft ? parentLeft : parentRight;
888
- const midX = (childEdge + parentEdge) / 2;
889
- const vLo = Math.min(from.yA, from.yB, toCy);
890
- const vHi = Math.max(from.yA, from.yB, toCy);
891
- const d = from.yA !== from.yB ? `M ${childEdge} ${from.yA} H ${midX} M ${childEdge} ${from.yB} H ${midX} M ${midX} ${vLo} V ${vHi} M ${midX} ${toCy} H ${parentEdge}` : `M ${childEdge} ${from.y} H ${midX} V ${toCy} H ${parentEdge}`;
892
- g.append("path").attr("d", d).attr("fill", "none").attr("stroke", paint.wire).attr("stroke-width", 1.5);
893
- }
894
- function drawBox(g, cx, cy, name, info, sidePaint, palette, isDark, m, activeKey, fillMode) {
895
- const x = cx - BOX_W / 2;
896
- const y = cy - BOX_H / 2;
897
- const paint = info.tagColor ? paintForAccent(palette, isDark, info.tagColor, fillMode) : sidePaint;
898
- const cell = g.append("g").attr("class", "dgmo-bracket-box");
899
- if (m.lineNumber !== null) cell.attr("data-line-number", m.lineNumber);
900
- if (activeKey && info.tagValue)
901
- cell.attr(`data-tag-${activeKey}`, info.tagValue.toLowerCase());
902
- const isTBD = name === null;
903
- let fill = "none";
904
- let stroke = paint.hair;
905
- let sw = 1.25;
906
- let dash = null;
907
- let textColor = paint.ink;
908
- let weight = "bold";
909
- if (info.isChampion) {
910
- fill = paint.tint;
911
- stroke = paint.accent;
912
- sw = 3;
913
- weight = "bold";
914
- } else if (isTBD) {
915
- fill = paint.track;
916
- dash = "4 4";
917
- textColor = paint.muted;
918
- weight = "normal";
919
- } else if (info.isWinner) {
920
- fill = paint.tint;
921
- stroke = paint.accent;
922
- sw = 2;
923
- weight = "bold";
924
- } else if (info.isLoser) {
925
- stroke = paint.hair;
926
- textColor = paint.muted;
927
- weight = "normal";
928
- } else if (info.pending) {
929
- stroke = mix(paint.accent, paint.hair, 50);
930
- }
931
- const onSolid = fillMode === "solid" && (info.isChampion || info.isWinner) ? contrastText(
932
- paint.tint,
933
- palette.textOnFillLight,
934
- palette.textOnFillDark
935
- ) : null;
936
- if (onSolid) textColor = onSolid;
937
- const gutterColor = onSolid ?? paint.muted;
938
- const rect = cell.append("rect").attr("x", x).attr("y", y).attr("width", BOX_W).attr("height", BOX_H).attr("rx", 6).attr("fill", fill).attr("stroke", stroke).attr("stroke-width", sw);
939
- if (dash) rect.attr("stroke-dasharray", dash);
940
- let leftPad = 11;
941
- if (info.seed !== null) {
942
- cell.append("text").attr("x", x + 9).attr("y", cy + 4).attr("text-anchor", "start").attr("font-size", 10).attr("font-weight", "bold").attr("fill", gutterColor).text(info.seed);
943
- leftPad = 9 + Math.max(12, measureText(String(info.seed), 10, { bold: true })) + 6;
944
- }
945
- let rightPad = 11;
946
- if (info.score) rightPad += 20;
947
- if (info.isHome) {
948
- drawHome(cell, x + BOX_W - rightPad - 6, cy, gutterColor);
949
- rightPad += 16;
950
- }
951
- cell.append("text").attr("x", x + leftPad).attr("y", cy + 4).attr("text-anchor", "start").attr("font-size", 12.5).attr("font-weight", weight).attr("fill", textColor).text(
952
- clip(
953
- isTBD ? "TBD" : name,
954
- BOX_W - leftPad - rightPad,
955
- 12.5,
956
- weight === "bold"
957
- )
958
- );
959
- if (info.score) {
960
- cell.append("text").attr("x", x + BOX_W - 11).attr("y", cy + 4).attr("text-anchor", "end").attr("font-size", 12).attr("font-weight", info.isWinner ? "bold" : "normal").attr("fill", info.isWinner ? textColor : gutterColor).text(info.score);
961
- }
962
- }
963
- function drawHome(cell, hx, cy, color) {
964
- const s = 5;
965
- const yTop = cy - s - 1;
966
- const d = `M ${hx - s} ${cy - 1} L ${hx} ${yTop} L ${hx + s} ${cy - 1} L ${hx + s} ${cy + s} L ${hx - s} ${cy + s} Z`;
967
- cell.append("path").attr("d", d).attr("fill", "none").attr("stroke", color).attr("stroke-width", 1.2).attr("stroke-linejoin", "round");
968
- }
969
- function drawUpset(g, m, paint) {
970
- g.append("text").attr("x", m.x + BOX_W / 2).attr("y", m.y - HALF_SPAN2 - BOX_H / 2 - 4).attr("text-anchor", "end").attr("font-size", 9).attr("font-weight", "bold").attr("letter-spacing", 0.6).attr("fill", paint.accent).text("UPSET");
971
- }
972
- function drawCommentary(g, m, palette) {
973
- const x = m.x - BOX_W / 2 + 2;
974
- let y = m.y + HALF_SPAN2 + BOX_H / 2 + 13;
975
- const muted = mix(palette.text, palette.bg, 45);
976
- const bulletW = measureText("\u2022 ", 11);
977
- const emit = (line, lx) => {
978
- const t = g.append("text").attr("x", lx).attr("y", y).attr("text-anchor", "start").attr("font-size", 11).attr("font-style", "italic").attr("fill", muted);
979
- renderInlineText(t, line, palette, 11);
980
- y += COMMENT_LH;
981
- };
982
- for (const raw of m.commentary) {
983
- const isBullet = raw.startsWith("\u2022 ");
984
- if (isBullet) {
985
- const body = raw.slice(2);
986
- wrapText(body, COMMENT_W - bulletW, 11).forEach(
987
- (line, i) => i === 0 ? emit(`\u2022 ${line}`, x) : emit(line, x + bulletW)
988
- );
989
- } else {
990
- for (const line of wrapText(raw, COMMENT_W, 11)) emit(line, x);
991
- }
992
- }
993
- }
994
- function renderBracketForExport(container, parsed, palette, isDark, exportDims) {
995
- renderBracket(container, parsed, palette, isDark, exportDims);
996
- }
997
-
998
575
  export {
999
- layoutBracket,
1000
- bracketExportDimensions,
1001
- renderBracket,
1002
- renderBracketForExport
576
+ BOX_W,
577
+ BOX_H,
578
+ PAIR_GAP,
579
+ COMMENT_W,
580
+ wrapText,
581
+ layoutBracket
1003
582
  };
@@ -72,6 +72,8 @@ function escapeAttr(s) {
72
72
  // src/timeline/renderer.ts
73
73
  import * as d3Scale from "d3-scale";
74
74
  import * as d3Selection from "d3-selection";
75
+ var MIN_TOP_SCALE_BAND = 26;
76
+ var MIN_BOTTOM_BAND = 30;
75
77
  function renderEras(g, eras, scale, isVertical, innerWidth, innerHeight, onEnter, onLeave, hasScale = false, _tooltip = null, palette, reservedLabelY, fillMode, outlineBg) {
76
78
  const eraColors = getEraColors(
77
79
  palette ?? getPalette(DEFAULT_PALETTE_ID).light
@@ -799,11 +801,12 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
799
801
  const MARKER_ROW_H = ctx.structural(22);
800
802
  const eraReserve = timelineEras.length > 0 ? ERA_ROW_H : 0;
801
803
  const markerReserve = timelineMarkers.length > 0 ? MARKER_ROW_H : 0;
802
- const topScaleH = timelineScale ? ctx.structural(40) : 0;
804
+ const topScaleH = timelineScale ? Math.max(ctx.structural(40), MIN_TOP_SCALE_BAND) : 0;
805
+ const hasGroupLegend = timelineGroups.length > 0 && !parsed.noLegend;
803
806
  const margin = {
804
- top: ctx.aesthetic(104) + topScaleH + eraReserve + markerReserve + tagLegendReserve,
807
+ top: 50 + (hasGroupLegend ? 54 : ctx.aesthetic(54)) + topScaleH + eraReserve + markerReserve + tagLegendReserve,
805
808
  right: ctx.aesthetic(40),
806
- bottom: ctx.aesthetic(40) + scaleMargin,
809
+ bottom: Math.max(ctx.aesthetic(40), MIN_BOTTOM_BAND) + scaleMargin,
807
810
  left: ctx.aesthetic(60)
808
811
  };
809
812
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
@@ -874,8 +877,8 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
874
877
  formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
875
878
  );
876
879
  }
877
- if (timelineGroups.length > 0 && !parsed.noLegend) {
878
- const legendY = timelineScale ? -ctx.aesthetic(75) : -ctx.aesthetic(55);
880
+ if (hasGroupLegend) {
881
+ const legendY = timelineScale ? -(topScaleH + 35) : -55;
879
882
  renderTimelineGroupLegend(
880
883
  g,
881
884
  timelineGroups,
@@ -1033,7 +1036,7 @@ function renderTimelineHorizontalGrouped(container, parsed, palette, isDark, set
1033
1036
  const MARKER_ROW_H = ctx.structural(22);
1034
1037
  const eraReserve = timelineEras.length > 0 ? ERA_ROW_H : 0;
1035
1038
  const markerReserve = timelineMarkers.length > 0 ? MARKER_ROW_H : 0;
1036
- const topScaleH = timelineScale ? ctx.structural(40) : 0;
1039
+ const topScaleH = timelineScale ? Math.max(ctx.structural(40), MIN_TOP_SCALE_BAND) : 0;
1037
1040
  const maxGroupNameLen = Math.max(...lanes.map((l) => l.name.length)) + 2;
1038
1041
  const maxEventLabelLen = Math.max(
1039
1042
  0,
@@ -1044,11 +1047,11 @@ function renderTimelineHorizontalGrouped(container, parsed, palette, isDark, set
1044
1047
  ctx.aesthetic(140),
1045
1048
  maxLeftLabelLen * sCharW + ctx.aesthetic(30)
1046
1049
  );
1047
- const baseTopMargin = title ? ctx.aesthetic(50) : ctx.aesthetic(20);
1050
+ const baseTopMargin = title ? 50 : ctx.aesthetic(20);
1048
1051
  const margin = {
1049
1052
  top: baseTopMargin + topScaleH + eraReserve + markerReserve + tagLegendReserve,
1050
1053
  right: ctx.aesthetic(40),
1051
- bottom: ctx.aesthetic(40) + scaleMargin,
1054
+ bottom: Math.max(ctx.aesthetic(40), MIN_BOTTOM_BAND) + scaleMargin,
1052
1055
  left: dynamicLeftMargin
1053
1056
  };
1054
1057
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
@@ -1136,7 +1139,7 @@ function renderTimelineHorizontalGrouped(container, parsed, palette, isDark, set
1136
1139
  g.append("rect").attr("class", "tl-group-header-bg").attr("data-group", lane.name).attr("x", bandX).attr("y", bandY).attr("width", bandW).attr("height", bandH).attr("rx", sBandRx).attr("fill", shapeFill(palette, laneColor, isDark, { mode: fillMode })).style("pointer-events", "none");
1137
1140
  g.append("rect").attr("class", "tl-group-header-accent").attr("data-group", lane.name).attr("x", bandX).attr("y", bandY).attr("width", sBandAccentW).attr("height", bandH).attr("fill", laneColor).attr("clip-path", `url(#${clipId})`).style("pointer-events", "none");
1138
1141
  const headerG = g.append("g").attr("class", "tl-group-header").attr("data-group", lane.name).style("cursor", "pointer").on("mouseenter", () => fadeToGroup(g, lane.name)).on("mouseleave", () => fadeReset(g)).on("click", () => toggleGroup(lane.name));
1139
- headerG.append("text").attr("x", -margin.left + ctx.aesthetic(10)).attr("y", curY + sBarH / 2).attr("dy", "0.35em").attr("text-anchor", "start").attr("fill", textColor).attr("font-size", sLaneHeaderFont).attr("font-weight", "bold").text(`${toggleIcon} ${lane.name}`);
1142
+ headerG.append("text").attr("x", bandX + 5).attr("y", curY + sBarH / 2).attr("dy", "0.35em").attr("text-anchor", "start").attr("fill", textColor).attr("font-size", sLaneHeaderFont).attr("font-weight", "bold").text(`${toggleIcon} ${lane.name}`);
1140
1143
  if (lane.events.length > 0) {
1141
1144
  const evDates = lane.events.map((ev) => parseTimelineDate(ev.date));
1142
1145
  const evEndDates = lane.events.filter((ev) => ev.endDate).map((ev) => parseTimelineDate(ev.endDate));