courthive-components 4.0.0 → 4.1.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.
@@ -0,0 +1,34 @@
1
+ import { DrawOrderGameState } from './drawOrderGameState';
2
+ import { ProjectedPressureResult, PressureSeries } from '../pressureChart/types';
3
+ import { DrawOrderScore } from './scoreDrawOrder';
4
+ import { HorizonVariant } from './pressureHorizon';
5
+ export type DrawOrderGameOptions = {
6
+ width?: number;
7
+ rowHeight?: number;
8
+ rowGap?: number;
9
+ columnGap?: number;
10
+ bands?: number;
11
+ /**
12
+ * `ribbon` is the gentler board: mirror pairs read as reflected curves, which is
13
+ * far easier to spot than comparing wall depths. `walls` is the hard mode.
14
+ */
15
+ variant?: HorizonVariant;
16
+ /** Enables the weighted fan on the ribbon. Carries no identities, so it cannot leak. */
17
+ projection?: ProjectedPressureResult;
18
+ seed?: number;
19
+ scaleName?: string;
20
+ /** Cap the field so a 128-draw stays a game rather than a chore. */
21
+ limit?: number;
22
+ roundLabels?: (roundNumber: number, index: number, total: number) => string;
23
+ onComplete?: (score: DrawOrderScore, state: DrawOrderGameState) => void;
24
+ emptyMessage?: string;
25
+ };
26
+ export type DrawOrderGameInstance = {
27
+ element: HTMLElement;
28
+ /** The current state — for a story or a host that wants to drive the board. */
29
+ getState: () => DrawOrderGameState;
30
+ reshuffle: (seed?: number) => void;
31
+ reveal: () => void;
32
+ destroy: () => void;
33
+ };
34
+ export declare function buildDrawOrderGame(container: HTMLElement, series?: PressureSeries[], options?: DrawOrderGameOptions): DrawOrderGameInstance;
@@ -0,0 +1,35 @@
1
+ import { DrawOrderScore } from './scoreDrawOrder';
2
+ export type DrawOrderGameState = {
3
+ /** participantIds in true draw-position order — the answer key. */
4
+ actualOrder: string[];
5
+ /** participantIds in the player's current order. */
6
+ order: string[];
7
+ /** The seed that dealt this board; quoting it reproduces the puzzle exactly. */
8
+ seed: number;
9
+ /** False when the deal could not avoid leaving a row in its true slot. */
10
+ deranged: boolean;
11
+ revealed: boolean;
12
+ /** Null until `revealDrawOrder`. */
13
+ score: DrawOrderScore | null;
14
+ /** Reorder operations the player has made on this board. */
15
+ moves: number;
16
+ };
17
+ export declare function createDrawOrderGame({ actualOrder, seed }: {
18
+ actualOrder: string[];
19
+ seed: number;
20
+ }): DrawOrderGameState;
21
+ /**
22
+ * Lift the row at `from` and drop it at `to`, closing the gap behind it.
23
+ *
24
+ * An out-of-range index returns the state untouched rather than throwing: a
25
+ * pointer drag can legitimately end outside the list, and that is a no-op, not
26
+ * an error. A revealed board is frozen — the answer is already on screen, so a
27
+ * further move would produce a score that no longer describes what was guessed.
28
+ */
29
+ export declare function moveSlot(state: DrawOrderGameState, from: number, to: number): DrawOrderGameState;
30
+ /** Exchange two rows in place — what a keyboard nudge does. */
31
+ export declare function swapSlots(state: DrawOrderGameState, a: number, b: number): DrawOrderGameState;
32
+ /** Score the board and freeze it. Repeated calls are idempotent. */
33
+ export declare function revealDrawOrder(state: DrawOrderGameState): DrawOrderGameState;
34
+ /** Deal a fresh board from a new seed, keeping the same field. */
35
+ export declare function reshuffleDrawOrder(state: DrawOrderGameState, seed: number): DrawOrderGameState;
@@ -0,0 +1,38 @@
1
+ import { BuildHorizonRowsParams, HorizonRowsResult, HorizonSource, HorizonLayer } from './types';
2
+ import { PressureSeriesPoint, PressureSeries } from '../pressureChart/types';
3
+ /** Four steps per arm. More than four stops reading as ordered at row heights this small. */
4
+ export declare const DEFAULT_BANDS = 4;
5
+ /**
6
+ * Floor on the domain, in ELO points. Without it a draw whose deltas are all tiny
7
+ * would autoscale until rounding noise filled the darkest band — the horizon
8
+ * equivalent of a truncated y-axis, and just as misleading.
9
+ */
10
+ export declare const MIN_HORIZON_DOMAIN = 60;
11
+ /** The value a cell paints, and whether it came from a played matchUp. */
12
+ export declare function selectCellValue(point: PressureSeriesPoint, source: HorizonSource): {
13
+ value: number | null;
14
+ fromActual: boolean;
15
+ };
16
+ /**
17
+ * Split a magnitude into paintable layers.
18
+ *
19
+ * Returns `clipped: true` when the magnitude ran past the domain — the wall is
20
+ * capped at the darkest band and the caller is expected to say so rather than
21
+ * let the reader assume the cap is the true value.
22
+ */
23
+ export declare function bandLayers({ magnitude, bands, domainMax }: {
24
+ magnitude: number;
25
+ bands?: number;
26
+ domainMax: number;
27
+ }): {
28
+ layers: HorizonLayer[];
29
+ clipped: boolean;
30
+ };
31
+ /**
32
+ * One domain across every row, so a stack of rows is actually a comparison.
33
+ *
34
+ * Letting each row autoscale is the single easiest way to make this chart lie:
35
+ * an easy road and a brutal one would paint identically.
36
+ */
37
+ export declare function resolveHorizonDomain(series: PressureSeries[], source: HorizonSource): number;
38
+ export declare function buildHorizonRows({ series, source, bands, domainMax, projection }: BuildHorizonRowsParams): HorizonRowsResult;
@@ -0,0 +1,14 @@
1
+ import { HorizonDirection } from './types';
2
+ /**
3
+ * One arm's steps, stepped in depth as well as hue so the ramp reads as ordered.
4
+ *
5
+ * `centred` mirrors the ribbon's geometry — steps grow from the middle rather than
6
+ * from an edge — so the glyph matches the chart it is keying.
7
+ */
8
+ export declare function buildArmSwatch(direction: HorizonDirection, bands: number, centred?: boolean): SVGElement;
9
+ export declare function buildHorizonLegend({ bands, scaleName, note, variant }: {
10
+ bands: number;
11
+ scaleName?: string;
12
+ note?: string;
13
+ variant?: string;
14
+ }): HTMLElement;
@@ -0,0 +1,28 @@
1
+ import { HorizonRow } from './types';
2
+ export type HorizonRibbonOptions = {
3
+ width: number;
4
+ height: number;
5
+ domainMax: number;
6
+ /** Colour bands across the domain. Must match the walls or the two stop reading alike. */
7
+ bands?: number;
8
+ /**
9
+ * How many colour bands of the domain map to HALF the row height.
10
+ *
11
+ * This is the ribbon's version of the fold, and it is the setting that decides
12
+ * whether the chart is readable. A centred line splits the row between the two
13
+ * arms, so mapping the whole domain across it leaves a median value moving about
14
+ * three pixels — measured on a 16-draw, `|delta|` has a median of 238 against a
15
+ * 668 domain, which on a 22px row is 3.4px of travel. Mapping half the domain
16
+ * instead doubles every amplitude and leaves roughly a third of cells saturating
17
+ * at the row edge, where the gradient keeps them distinguishable.
18
+ */
19
+ positionBands?: number;
20
+ /** Attach hover descriptions. MUST stay false in the game — a title names the answer. */
21
+ describe?: boolean;
22
+ roundLabels?: (roundNumber: number, index: number, total: number) => string;
23
+ scaleName?: string;
24
+ ariaLabel?: string;
25
+ /** Draw the even-match reference line. */
26
+ showZeroLine?: boolean;
27
+ };
28
+ export declare function buildHorizonRibbonSvg(row: HorizonRow, options: HorizonRibbonOptions): SVGSVGElement;
@@ -0,0 +1,19 @@
1
+ import { HorizonRow } from './types';
2
+ export type HorizonRowOptions = {
3
+ width: number;
4
+ height: number;
5
+ gap?: number;
6
+ /**
7
+ * Attach per-wall `<title>` tooltips and a descriptive aria-label. MUST stay
8
+ * false in the game: a tooltip carrying the opponent rating hands over the
9
+ * deduction the puzzle is asking for.
10
+ */
11
+ describe?: boolean;
12
+ /** Used only when `describe` is true. */
13
+ roundLabels?: (roundNumber: number, index: number, total: number) => string;
14
+ scaleName?: string;
15
+ /** Overrides the aria-label. In the game this is the slot number, never a name. */
16
+ ariaLabel?: string;
17
+ };
18
+ /** The strip for one participant. Columns are laid out on the row's own cell order. */
19
+ export declare function buildHorizonRowSvg(row: HorizonRow, options: HorizonRowOptions): SVGSVGElement;
@@ -0,0 +1,20 @@
1
+ export { createDrawOrderGame, reshuffleDrawOrder, revealDrawOrder, moveSlot, swapSlots } from './drawOrderGameState';
2
+ export { buildHorizonRows, resolveHorizonDomain, selectCellValue, bandLayers, DEFAULT_BANDS } from './horizonBands';
3
+ export { shuffleWithSeed, shuffleDeranged, createRandom, fixedPoints } from './shuffleWithSeed';
4
+ export { scoreDrawOrder, blockLevels, maxDisplacementFor } from './scoreDrawOrder';
5
+ export { buildPressureHorizon, HORIZON_ORDER, HORIZON_VARIANT } from './pressureHorizon';
6
+ export { HORIZON_DIRECTION, HORIZON_SOURCE } from './types';
7
+ export { MIN_HORIZON_DOMAIN } from './horizonBands';
8
+ export { buildDrawOrderGame } from './drawOrderGame';
9
+ export { buildHorizonLegend, buildArmSwatch } from './horizonLegend';
10
+ export { opponentSpread, weightedQuantile, INNER_QUANTILES } from './opponentSpread';
11
+ export { buildHorizonRibbonSvg } from './horizonRibbon';
12
+ export { buildHorizonRowSvg } from './horizonRow';
13
+ export type { PressureHorizonOptions, PressureHorizonInstance, HorizonOrder, HorizonVariant } from './pressureHorizon';
14
+ export type { DrawOrderGameOptions, DrawOrderGameInstance } from './drawOrderGame';
15
+ export type { DrawOrderScore, BlockLevelScore, SlotResult } from './scoreDrawOrder';
16
+ export type { DrawOrderGameState } from './drawOrderGameState';
17
+ export type { HorizonRibbonOptions } from './horizonRibbon';
18
+ export type { OpponentSpread } from './opponentSpread';
19
+ export type { HorizonRowOptions } from './horizonRow';
20
+ export type { BuildHorizonRowsParams, HorizonRowsResult, HorizonDirection, HorizonSource, HorizonSpread, HorizonLayer, HorizonCell, HorizonRow } from './types';
@@ -0,0 +1,31 @@
1
+ import { PossibleOpponent } from '../pressureChart/types';
2
+ /** Default inner-envelope quantiles. The middle half of the arrival probability. */
3
+ export declare const INNER_QUANTILES: [number, number];
4
+ export type OpponentSpread = {
5
+ /** Full range over opponents clearing the projection's threshold. */
6
+ outerLow: number;
7
+ outerHigh: number;
8
+ /** Weighted interquartile range — where the probability mass sits. */
9
+ innerLow: number;
10
+ innerHigh: number;
11
+ };
12
+ /**
13
+ * Weighted quantile over (value, weight) pairs, values ascending.
14
+ *
15
+ * Uses the inclusive definition — walk the cumulative weight and take the first
16
+ * value whose running total reaches `q` of the total. With a single opponent every
17
+ * quantile is that opponent, which is what makes round 1 collapse to a point rather
18
+ * than to an arbitrary interval.
19
+ */
20
+ export declare function weightedQuantile(sorted: {
21
+ value: number;
22
+ weight: number;
23
+ }[], q: number): number | null;
24
+ /**
25
+ * Derive both envelopes from the opponents who could arrive.
26
+ *
27
+ * Returns `null` when nobody in the pool carries a rating — the caller is expected
28
+ * to fall back to the projection's own `low`/`high`, and to draw nothing at all if
29
+ * that is absent too. Never invents a spread.
30
+ */
31
+ export declare function opponentSpread(opponents: PossibleOpponent[], quantiles?: [number, number]): OpponentSpread | null;
@@ -0,0 +1,57 @@
1
+ import { HorizonSource } from './types';
2
+ import { ProjectedPressureResult, PressureSeries } from '../pressureChart/types';
3
+ export declare const HORIZON_ORDER: {
4
+ readonly DRAW: "draw";
5
+ readonly DIFFICULTY: "difficulty";
6
+ };
7
+ export type HorizonOrder = (typeof HORIZON_ORDER)[keyof typeof HORIZON_ORDER];
8
+ /**
9
+ * How a row is drawn. Neither is a replacement for the other:
10
+ *
11
+ * - WALLS — one hard-edged column per round, magnitude folded into colour bands.
12
+ * Maximum density; reads down to 10px rows.
13
+ * - RIBBON — a connected line through the rounds inside a fan of possible
14
+ * opponents. Carries the spread the walls discard, and makes a near-zero
15
+ * delta visible instead of a 1px sliver. Wants ~22px.
16
+ */
17
+ export declare const HORIZON_VARIANT: {
18
+ readonly WALLS: "walls";
19
+ readonly RIBBON: "ribbon";
20
+ };
21
+ export type HorizonVariant = (typeof HORIZON_VARIANT)[keyof typeof HORIZON_VARIANT];
22
+ export type PressureHorizonOptions = {
23
+ /** Width of the wall strip, excluding the label gutter. */
24
+ width?: number;
25
+ rowHeight?: number;
26
+ rowGap?: number;
27
+ /** Surface gap between round columns. */
28
+ columnGap?: number;
29
+ bands?: number;
30
+ variant?: HorizonVariant;
31
+ source?: HorizonSource;
32
+ /**
33
+ * The projection `buildPressureSeries` returns beside `series`. Only the ribbon
34
+ * reads it, and only to weight the inner fan; without it the fan falls back to the
35
+ * unweighted low/high envelope and says so.
36
+ */
37
+ projection?: ProjectedPressureResult;
38
+ /** Fix the domain across several stacks so they can be read against each other. */
39
+ domainMax?: number;
40
+ showLabels?: boolean;
41
+ labelWidth?: number;
42
+ showLegend?: boolean;
43
+ showRoundHeader?: boolean;
44
+ scaleName?: string;
45
+ order?: HorizonOrder;
46
+ /** Cap the rows rendered; the number dropped is stated in the caption. */
47
+ limit?: number;
48
+ onSelect?: (series: PressureSeries) => void;
49
+ roundLabels?: (roundNumber: number, index: number, total: number) => string;
50
+ emptyMessage?: string;
51
+ };
52
+ export type PressureHorizonInstance = {
53
+ element: HTMLElement;
54
+ update: (series: PressureSeries[], options?: PressureHorizonOptions) => void;
55
+ destroy: () => void;
56
+ };
57
+ export declare function buildPressureHorizon(container: HTMLElement, series?: PressureSeries[], options?: PressureHorizonOptions): PressureHorizonInstance;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Score a guessed draw order against the real one.
3
+ *
4
+ * **Why the headline number is structural, not positional.** The obvious score —
5
+ * "how many rows landed in the right slot" — punishes a player for being right.
6
+ * A draw sheet is mirror-symmetric: swapping the top half with the bottom half,
7
+ * or the two quarters inside a half, or the two players inside a first-round
8
+ * pair, produces a bracket with identical structure and a completely different
9
+ * set of slot numbers. A player who reconstructs the pairings perfectly but
10
+ * mirrors one half scores near zero on exact slots while having solved the
11
+ * puzzle.
12
+ *
13
+ * So the score counts **blocks**: at every level of the bracket (pairs, then
14
+ * quarters-of-the-half, then halves), how many of the guessed groupings are real
15
+ * groupings — matched as unordered member sets, anywhere in the guess. That is
16
+ * invariant under every symmetry of the bracket, which is exactly the property
17
+ * the answer key needs.
18
+ *
19
+ * `exact` and `proximity` are still reported, because they are what a player
20
+ * intuitively expects to see and because their gap against `structureScore` is
21
+ * itself the interesting feedback ("you had the draw right and the sheet upside
22
+ * down"). They are not the headline.
23
+ */
24
+ export type BlockLevelScore = {
25
+ /** 2 = first-round pairs, 4 = who could meet by round 2, and so on. */
26
+ blockSize: number;
27
+ matched: number;
28
+ total: number;
29
+ };
30
+ export type SlotResult = {
31
+ slotIndex: number;
32
+ participantId: string;
33
+ /** Where this participant really sits, 0-based. */
34
+ actualIndex: number;
35
+ correct: boolean;
36
+ };
37
+ export type DrawOrderScore = {
38
+ slots: number;
39
+ /** Rows sitting in exactly the right slot. Symmetry-sensitive — read it with `structureScore`. */
40
+ exact: number;
41
+ /** Sum of |guessed index - actual index| across all rows. */
42
+ displacement: number;
43
+ /** The worst displacement a permutation of this size can achieve. */
44
+ maxDisplacement: number;
45
+ /** 1 - displacement/maxDisplacement. 1 is perfect, 0 is maximally reversed. */
46
+ proximity: number;
47
+ levels: BlockLevelScore[];
48
+ blocksMatched: number;
49
+ blocksTotal: number;
50
+ /** 0-100, the headline. Symmetry-invariant. */
51
+ structureScore: number;
52
+ /** Every block at every level was a real grouping. */
53
+ structurePerfect: boolean;
54
+ /** The guess is the actual order, slot for slot. */
55
+ perfect: boolean;
56
+ slotResults: SlotResult[];
57
+ };
58
+ /** The block sizes a field of this size actually has. Empty when it does not divide. */
59
+ export declare function blockLevels(slots: number): number[];
60
+ /**
61
+ * Maximum total displacement over all permutations of n elements.
62
+ * Reversal achieves it, and it evaluates to floor(n^2 / 2).
63
+ */
64
+ export declare function maxDisplacementFor(slots: number): number;
65
+ /**
66
+ * @throws when `guess` is not a permutation of `actual` — that is a caller bug,
67
+ * and scoring it would quietly report a number for an impossible board.
68
+ */
69
+ export declare function scoreDrawOrder({ guess, actual }: {
70
+ guess: string[];
71
+ actual: string[];
72
+ }): DrawOrderScore;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Deterministic shuffling for the draw-order game.
3
+ *
4
+ * `Math.random()` is deliberately not used. A puzzle has to be reproducible: the
5
+ * same seed must always deal the same board so two people can be given the same
6
+ * one to race on, a score can be quoted against an identifiable puzzle, and the
7
+ * shuffle can be asserted in a unit test rather than described in a comment.
8
+ */
9
+ /** mulberry32 — small, fast, and good enough for dealing a board. Not cryptographic. */
10
+ export declare function createRandom(seed: number): () => number;
11
+ /** Fisher-Yates against a seeded stream. Non-mutating. */
12
+ export declare function shuffleWithSeed<T>(items: readonly T[], seed: number): T[];
13
+ /** How many items a shuffle left sitting in their original slot. */
14
+ export declare function fixedPoints<T>(items: readonly T[], shuffled: readonly T[]): number;
15
+ /**
16
+ * A shuffle that actually looks shuffled.
17
+ *
18
+ * A plain shuffle leaves roughly one item in place on average, and a board that
19
+ * opens with three rows already correct is a worse puzzle than one that opens
20
+ * with none — the player cannot tell luck from deduction. So walk consecutive
21
+ * seeds until a derangement turns up, and if none does inside `attempts`, take
22
+ * the best of what was seen and **report which seed produced it** so the board
23
+ * stays reproducible either way.
24
+ *
25
+ * Fewer than two items cannot be deranged; that is returned as-is rather than
26
+ * spun on, with `deranged: false` so the caller is never told a fiction.
27
+ */
28
+ export declare function shuffleDeranged<T>(items: readonly T[], seed: number, attempts?: number): {
29
+ items: T[];
30
+ seed: number;
31
+ deranged: boolean;
32
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pressure horizon — type definitions.
3
+ *
4
+ * A **second, independent iteration** on `pressureChart`, not a replacement for
5
+ * it. Both read the same `PressureSeries`; they differ in what they optimise
6
+ * for:
7
+ *
8
+ * - `pressureChart` is a DETAIL view — one participant, a continuous y-axis,
9
+ * the projected ribbon visibly narrowing as the draw resolves.
10
+ * - `pressureHorizon` is a DENSITY view — one row per participant at 14-20px,
11
+ * every row on the same domain, designed to be stacked and compared by the
12
+ * dozen. Magnitude is folded out of height and into colour bands, which is
13
+ * the whole point of a horizon chart: it buys vertical space by spending
14
+ * colour.
15
+ *
16
+ * Two encodings carry direction, never colour alone:
17
+ *
18
+ * position -> a HARD round (playing up) grows from the row's baseline;
19
+ * an EASY round (playing down) hangs from the row's top edge
20
+ * hue -> red arm for hard, blue arm for easy, both ordinal light->dark
21
+ *
22
+ * That redundancy is deliberate. The palest step of each arm sits below 3:1
23
+ * against the surface (2.11:1 blue, 2.16:1 red on white), which the data-viz
24
+ * standard permits only with relief — so the legend is always present and the
25
+ * anchoring cue is doing real work, not decoration.
26
+ */
27
+ /** Which series a cell reads its value from. */
28
+ export declare const HORIZON_SOURCE: {
29
+ /** The projected expected signed delta — the wall as it looked before the round was played. */
30
+ readonly PROJECTED: "projected";
31
+ /** The realised signed delta where a round has been played, falling back to projected. */
32
+ readonly ACTUAL: "actual";
33
+ };
34
+ export type HorizonSource = (typeof HORIZON_SOURCE)[keyof typeof HORIZON_SOURCE];
35
+ /** Sign of the signed delta, named for what it means rather than for its sign. */
36
+ export declare const HORIZON_DIRECTION: {
37
+ /** Opponent rated above the participant — projected to play up. */
38
+ readonly HARD: "hard";
39
+ /** Opponent rated below the participant — projected to play down. */
40
+ readonly EASY: "easy";
41
+ };
42
+ export type HorizonDirection = (typeof HORIZON_DIRECTION)[keyof typeof HORIZON_DIRECTION];
43
+ /**
44
+ * One painted layer of a wall. `fraction` is the share of the row height this
45
+ * layer covers, so a full band is 1 and the topmost partial band is < 1.
46
+ *
47
+ * Layers are painted in ascending `bandIndex` with each one anchored to the same
48
+ * edge, so the darkest step ends up over the shallowest extent — that overlap IS
49
+ * the horizon fold, and it is why a value of 3.4 bands reads darker than 1.2
50
+ * without being any taller.
51
+ */
52
+ export type HorizonLayer = {
53
+ bandIndex: number;
54
+ fraction: number;
55
+ };
56
+ /**
57
+ * The opponent spread for a round, in signed-delta space (opponent minus own).
58
+ *
59
+ * Two envelopes, because one would lie. See `opponentSpread` for the measurement
60
+ * that decided it. `null` where the round carries no rated opponent pool.
61
+ */
62
+ export type HorizonSpread = {
63
+ /** Full range over opponents clearing the projection's arrival threshold. */
64
+ outerLow: number;
65
+ outerHigh: number;
66
+ /** Weighted interquartile range — where the arrival probability actually sits. */
67
+ innerLow: number;
68
+ innerHigh: number;
69
+ /** True when the inner envelope came from the projection rather than a low/high fallback. */
70
+ weighted: boolean;
71
+ };
72
+ /** One round of one participant — a single wall, or one vertex of a ribbon. */
73
+ export type HorizonCell = {
74
+ roundNumber: number;
75
+ /** Signed delta, or `null` where the round carries no rated opponent. */
76
+ value: number | null;
77
+ direction: HorizonDirection | null;
78
+ /** Empty when there is nothing to paint. */
79
+ layers: HorizonLayer[];
80
+ /** True when `|value|` exceeded the domain and the wall was capped. Counted, never silent. */
81
+ clipped: boolean;
82
+ /** True when the value came from a played matchUp rather than the projection. */
83
+ fromActual: boolean;
84
+ reachProbability: number;
85
+ bye: boolean;
86
+ resolved: boolean;
87
+ /**
88
+ * Where the possible opponents sit. Read by the ribbon renderer; the walls
89
+ * renderer ignores it, which is why it is `null` rather than absent when no
90
+ * projection was supplied.
91
+ */
92
+ spread: HorizonSpread | null;
93
+ };
94
+ /** One participant's full path, aligned to the shared round axis. */
95
+ export type HorizonRow = {
96
+ participantId: string;
97
+ participantName?: string;
98
+ drawPosition?: number;
99
+ cells: HorizonCell[];
100
+ };
101
+ export type BuildHorizonRowsParams = {
102
+ series: import('../pressureChart/types').PressureSeries[];
103
+ source?: HorizonSource;
104
+ bands?: number;
105
+ /** Fix the domain across a set of rows. Omit to derive it from the series. */
106
+ domainMax?: number;
107
+ /**
108
+ * The projection `buildPressureSeries` already returns beside `series`. Optional:
109
+ * supply it and each cell gains a probability-weighted inner envelope; omit it and
110
+ * the spread falls back to the projection's own low/high, which is a min/max over
111
+ * a 1% arrival threshold and therefore much wider. Taking it here rather than
112
+ * widening `PressureSeries` is what keeps `pressureChart/` untouched.
113
+ */
114
+ projection?: import('../pressureChart/types').ProjectedPressureResult;
115
+ };
116
+ export type HorizonRowsResult = {
117
+ rows: HorizonRow[];
118
+ /** The union of round numbers across every row, ascending. Columns align to this. */
119
+ roundNumbers: number[];
120
+ domainMax: number;
121
+ bands: number;
122
+ /** How many walls hit the domain cap. Surfaced in the caption when non-zero. */
123
+ clippedCells: number;
124
+ };