@nowline/layout 0.8.5 → 0.9.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/axis-columns.d.ts +70 -0
- package/dist/axis-columns.d.ts.map +1 -0
- package/dist/axis-columns.js +104 -0
- package/dist/axis-columns.js.map +1 -0
- package/dist/band-scale.d.ts.map +1 -1
- package/dist/calendar-resolver.d.ts +27 -0
- package/dist/calendar-resolver.d.ts.map +1 -0
- package/dist/calendar-resolver.js +132 -0
- package/dist/calendar-resolver.js.map +1 -0
- package/dist/calendar.d.ts +0 -19
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +4 -44
- package/dist/calendar.js.map +1 -1
- package/dist/canvas-width.d.ts +23 -0
- package/dist/canvas-width.d.ts.map +1 -0
- package/dist/canvas-width.js +26 -0
- package/dist/canvas-width.js.map +1 -0
- package/dist/capacity.d.ts +28 -0
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +64 -29
- package/dist/capacity.js.map +1 -1
- package/dist/chart-translation.d.ts +22 -0
- package/dist/chart-translation.d.ts.map +1 -0
- package/dist/chart-translation.js +83 -0
- package/dist/chart-translation.js.map +1 -0
- package/dist/container-header-geometry.d.ts +64 -0
- package/dist/container-header-geometry.d.ts.map +1 -0
- package/dist/container-header-geometry.js +89 -0
- package/dist/container-header-geometry.js.map +1 -0
- package/dist/dsl-utils.d.ts +3 -4
- package/dist/dsl-utils.d.ts.map +1 -1
- package/dist/dsl-utils.js +8 -25
- package/dist/dsl-utils.js.map +1 -1
- package/dist/edge-routing.d.ts +69 -78
- package/dist/edge-routing.d.ts.map +1 -1
- package/dist/edge-routing.js +271 -401
- package/dist/edge-routing.js.map +1 -1
- package/dist/frame-tab-geometry.d.ts +24 -14
- package/dist/frame-tab-geometry.d.ts.map +1 -1
- package/dist/frame-tab-geometry.js +69 -23
- package/dist/frame-tab-geometry.js.map +1 -1
- package/dist/header-card-geometry.d.ts +12 -0
- package/dist/header-card-geometry.d.ts.map +1 -1
- package/dist/header-card-geometry.js +14 -2
- package/dist/header-card-geometry.js.map +1 -1
- package/dist/header-logo.d.ts +28 -0
- package/dist/header-logo.d.ts.map +1 -0
- package/dist/header-logo.js +51 -0
- package/dist/header-logo.js.map +1 -0
- package/dist/i18n.d.ts +11 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +11 -1
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +20 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -7
- package/dist/index.js.map +1 -1
- package/dist/inline-date-pin-geometry.d.ts +19 -11
- package/dist/inline-date-pin-geometry.d.ts.map +1 -1
- package/dist/inline-date-pin-geometry.js +45 -46
- package/dist/inline-date-pin-geometry.js.map +1 -1
- package/dist/item-bar-geometry.d.ts +192 -4
- package/dist/item-bar-geometry.d.ts.map +1 -1
- package/dist/item-bar-geometry.js +258 -5
- package/dist/item-bar-geometry.js.map +1 -1
- package/dist/item-port-geometry.d.ts +13 -0
- package/dist/item-port-geometry.d.ts.map +1 -0
- package/dist/item-port-geometry.js +63 -0
- package/dist/item-port-geometry.js.map +1 -0
- package/dist/layout-context.d.ts +115 -51
- package/dist/layout-context.d.ts.map +1 -1
- package/dist/layout-context.js +96 -5
- package/dist/layout-context.js.map +1 -1
- package/dist/layout-insights.d.ts.map +1 -1
- package/dist/layout-insights.js +73 -1
- package/dist/layout-insights.js.map +1 -1
- package/dist/layout.d.ts +35 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +18 -1315
- package/dist/layout.js.map +1 -1
- package/dist/nodes/anchor-node.d.ts.map +1 -1
- package/dist/nodes/anchor-node.js +11 -4
- package/dist/nodes/anchor-node.js.map +1 -1
- package/dist/nodes/content-extent.d.ts +24 -0
- package/dist/nodes/content-extent.d.ts.map +1 -0
- package/dist/nodes/content-extent.js +100 -0
- package/dist/nodes/content-extent.js.map +1 -0
- package/dist/nodes/dependency-edges.d.ts +6 -0
- package/dist/nodes/dependency-edges.d.ts.map +1 -0
- package/dist/nodes/dependency-edges.js +148 -0
- package/dist/nodes/dependency-edges.js.map +1 -0
- package/dist/nodes/footnote-node.d.ts +24 -3
- package/dist/nodes/footnote-node.d.ts.map +1 -1
- package/dist/nodes/footnote-node.js +129 -25
- package/dist/nodes/footnote-node.js.map +1 -1
- package/dist/nodes/group-node.d.ts +10 -21
- package/dist/nodes/group-node.d.ts.map +1 -1
- package/dist/nodes/group-node.js +79 -154
- package/dist/nodes/group-node.js.map +1 -1
- package/dist/nodes/header-node.d.ts +56 -0
- package/dist/nodes/header-node.d.ts.map +1 -0
- package/dist/nodes/header-node.js +163 -0
- package/dist/nodes/header-node.js.map +1 -0
- package/dist/nodes/include-node.d.ts +22 -12
- package/dist/nodes/include-node.d.ts.map +1 -1
- package/dist/nodes/include-node.js +95 -62
- package/dist/nodes/include-node.js.map +1 -1
- package/dist/nodes/item-measure.d.ts +135 -0
- package/dist/nodes/item-measure.d.ts.map +1 -0
- package/dist/nodes/item-measure.js +514 -0
- package/dist/nodes/item-measure.js.map +1 -0
- package/dist/nodes/item-node.d.ts +162 -9
- package/dist/nodes/item-node.d.ts.map +1 -1
- package/dist/nodes/item-node.js +256 -69
- package/dist/nodes/item-node.js.map +1 -1
- package/dist/nodes/marker-band.d.ts +118 -0
- package/dist/nodes/marker-band.d.ts.map +1 -0
- package/dist/nodes/marker-band.js +303 -0
- package/dist/nodes/marker-band.js.map +1 -0
- package/dist/nodes/marker-geometry.js +1 -1
- package/dist/nodes/marker-geometry.js.map +1 -1
- package/dist/nodes/milestone-node.d.ts +112 -15
- package/dist/nodes/milestone-node.d.ts.map +1 -1
- package/dist/nodes/milestone-node.js +246 -95
- package/dist/nodes/milestone-node.js.map +1 -1
- package/dist/nodes/nowline-node.d.ts +8 -0
- package/dist/nodes/nowline-node.d.ts.map +1 -0
- package/dist/nodes/nowline-node.js +74 -0
- package/dist/nodes/nowline-node.js.map +1 -0
- package/dist/nodes/parallel-node.d.ts +7 -12
- package/dist/nodes/parallel-node.d.ts.map +1 -1
- package/dist/nodes/parallel-node.js +66 -32
- package/dist/nodes/parallel-node.js.map +1 -1
- package/dist/nodes/region-markers.d.ts +41 -0
- package/dist/nodes/region-markers.d.ts.map +1 -0
- package/dist/nodes/region-markers.js +130 -0
- package/dist/nodes/region-markers.js.map +1 -0
- package/dist/nodes/roadmap-node.d.ts +9 -72
- package/dist/nodes/roadmap-node.d.ts.map +1 -1
- package/dist/nodes/roadmap-node.js +38 -788
- package/dist/nodes/roadmap-node.js.map +1 -1
- package/dist/nodes/row-track.d.ts +33 -0
- package/dist/nodes/row-track.d.ts.map +1 -0
- package/dist/nodes/row-track.js +170 -0
- package/dist/nodes/row-track.js.map +1 -0
- package/dist/nodes/sequence-item.d.ts +25 -0
- package/dist/nodes/sequence-item.d.ts.map +1 -0
- package/dist/nodes/sequence-item.js +396 -0
- package/dist/nodes/sequence-item.js.map +1 -0
- package/dist/nodes/sequence-one.d.ts +5 -0
- package/dist/nodes/sequence-one.d.ts.map +1 -0
- package/dist/nodes/sequence-one.js +26 -0
- package/dist/nodes/sequence-one.js.map +1 -0
- package/dist/nodes/swimlane-node.d.ts +5 -17
- package/dist/nodes/swimlane-node.d.ts.map +1 -1
- package/dist/nodes/swimlane-node.js +62 -147
- package/dist/nodes/swimlane-node.js.map +1 -1
- package/dist/nodes/wave-node.d.ts +52 -0
- package/dist/nodes/wave-node.d.ts.map +1 -0
- package/dist/nodes/wave-node.js +483 -0
- package/dist/nodes/wave-node.js.map +1 -0
- package/dist/non-working-display.d.ts +14 -0
- package/dist/non-working-display.d.ts.map +1 -0
- package/dist/non-working-display.js +28 -0
- package/dist/non-working-display.js.map +1 -0
- package/dist/phases/date-window.d.ts +48 -0
- package/dist/phases/date-window.d.ts.map +1 -0
- package/dist/phases/date-window.js +130 -0
- package/dist/phases/date-window.js.map +1 -0
- package/dist/phases/finalize.d.ts +198 -0
- package/dist/phases/finalize.d.ts.map +1 -0
- package/dist/phases/finalize.js +1058 -0
- package/dist/phases/finalize.js.map +1 -0
- package/dist/phases/measure.d.ts +77 -0
- package/dist/phases/measure.d.ts.map +1 -0
- package/dist/phases/measure.js +222 -0
- package/dist/phases/measure.js.map +1 -0
- package/dist/phases/place.d.ts +28 -0
- package/dist/phases/place.d.ts.map +1 -0
- package/dist/phases/place.js +225 -0
- package/dist/phases/place.js.map +1 -0
- package/dist/renderable.d.ts.map +1 -1
- package/dist/renderable.js +23 -3
- package/dist/renderable.js.map +1 -1
- package/dist/resolve-today.d.ts +11 -1
- package/dist/resolve-today.d.ts.map +1 -1
- package/dist/resolve-today.js +26 -12
- package/dist/resolve-today.js.map +1 -1
- package/dist/row-packer.d.ts +42 -16
- package/dist/row-packer.d.ts.map +1 -1
- package/dist/row-packer.js +51 -78
- package/dist/row-packer.js.map +1 -1
- package/dist/schedule.d.ts +58 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +280 -106
- package/dist/schedule.js.map +1 -1
- package/dist/start-rules.d.ts +27 -0
- package/dist/start-rules.d.ts.map +1 -0
- package/dist/start-rules.js +91 -0
- package/dist/start-rules.js.map +1 -0
- package/dist/start-terms.d.ts +23 -0
- package/dist/start-terms.d.ts.map +1 -0
- package/dist/start-terms.js +27 -0
- package/dist/start-terms.js.map +1 -0
- package/dist/status-dot.d.ts +46 -0
- package/dist/status-dot.d.ts.map +1 -0
- package/dist/status-dot.js +78 -0
- package/dist/status-dot.js.map +1 -0
- package/dist/style-resolution.d.ts.map +1 -1
- package/dist/style-resolution.js +23 -43
- package/dist/style-resolution.js.map +1 -1
- package/dist/text-measure.d.ts +28 -0
- package/dist/text-measure.d.ts.map +1 -0
- package/dist/text-measure.js +70 -0
- package/dist/text-measure.js.map +1 -0
- package/dist/themes/contrast.d.ts +4 -0
- package/dist/themes/contrast.d.ts.map +1 -0
- package/dist/themes/contrast.js +32 -0
- package/dist/themes/contrast.js.map +1 -0
- package/dist/themes/dark.d.ts.map +1 -1
- package/dist/themes/dark.js +28 -1
- package/dist/themes/dark.js.map +1 -1
- package/dist/themes/grayscale.d.ts.map +1 -1
- package/dist/themes/grayscale.js +28 -1
- package/dist/themes/grayscale.js.map +1 -1
- package/dist/themes/index.d.ts +4 -11
- package/dist/themes/index.d.ts.map +1 -1
- package/dist/themes/index.js +6 -4
- package/dist/themes/index.js.map +1 -1
- package/dist/themes/light.d.ts.map +1 -1
- package/dist/themes/light.js +28 -1
- package/dist/themes/light.js.map +1 -1
- package/dist/themes/shape.d.ts +31 -0
- package/dist/themes/shape.d.ts.map +1 -1
- package/dist/themes/shared.d.ts +44 -50
- package/dist/themes/shared.d.ts.map +1 -1
- package/dist/themes/shared.js +79 -83
- package/dist/themes/shared.js.map +1 -1
- package/dist/time-scale.d.ts +42 -6
- package/dist/time-scale.d.ts.map +1 -1
- package/dist/time-scale.js +109 -17
- package/dist/time-scale.js.map +1 -1
- package/dist/types.d.ts +528 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/view-preset.d.ts +61 -6
- package/dist/view-preset.d.ts.map +1 -1
- package/dist/view-preset.js +439 -24
- package/dist/view-preset.js.map +1 -1
- package/dist/wave-barrier.d.ts +107 -0
- package/dist/wave-barrier.d.ts.map +1 -0
- package/dist/wave-barrier.js +174 -0
- package/dist/wave-barrier.js.map +1 -0
- package/dist/wave-hatch.d.ts +4 -0
- package/dist/wave-hatch.d.ts.map +1 -0
- package/dist/wave-hatch.js +21 -0
- package/dist/wave-hatch.js.map +1 -0
- package/dist/wave-layout.d.ts +99 -0
- package/dist/wave-layout.d.ts.map +1 -0
- package/dist/wave-layout.js +130 -0
- package/dist/wave-layout.js.map +1 -0
- package/dist/working-calendar.d.ts +128 -4
- package/dist/working-calendar.d.ts.map +1 -1
- package/dist/working-calendar.js +725 -51
- package/dist/working-calendar.js.map +1 -1
- package/dist/working-span.d.ts +11 -0
- package/dist/working-span.d.ts.map +1 -0
- package/dist/working-span.js +18 -0
- package/dist/working-span.js.map +1 -0
- package/package.json +7 -7
- package/src/axis-columns.ts +140 -0
- package/src/calendar-resolver.ts +161 -0
- package/src/calendar.ts +4 -45
- package/src/canvas-width.ts +36 -0
- package/src/capacity.ts +71 -30
- package/src/chart-translation.ts +88 -0
- package/src/container-header-geometry.ts +116 -0
- package/src/dsl-utils.ts +8 -26
- package/src/edge-routing.ts +347 -491
- package/src/frame-tab-geometry.ts +78 -25
- package/src/header-card-geometry.ts +17 -2
- package/src/header-logo.ts +67 -0
- package/src/i18n.ts +22 -1
- package/src/index.ts +97 -12
- package/src/inline-date-pin-geometry.ts +55 -58
- package/src/item-bar-geometry.ts +341 -5
- package/src/item-port-geometry.ts +69 -0
- package/src/layout-context.ts +210 -72
- package/src/layout-insights.ts +109 -2
- package/src/layout.ts +57 -1530
- package/src/nodes/anchor-node.ts +11 -4
- package/src/nodes/content-extent.ts +107 -0
- package/src/nodes/dependency-edges.ts +171 -0
- package/src/nodes/footnote-node.ts +171 -31
- package/src/nodes/group-node.ts +92 -215
- package/src/nodes/header-node.ts +255 -0
- package/src/nodes/include-node.ts +124 -110
- package/src/nodes/item-measure.ts +723 -0
- package/src/nodes/item-node.ts +358 -80
- package/src/nodes/marker-band.ts +406 -0
- package/src/nodes/marker-geometry.ts +1 -1
- package/src/nodes/milestone-node.ts +321 -136
- package/src/nodes/nowline-node.ts +87 -0
- package/src/nodes/parallel-node.ts +69 -39
- package/src/nodes/region-markers.ts +194 -0
- package/src/nodes/roadmap-node.ts +46 -950
- package/src/nodes/row-track.ts +217 -0
- package/src/nodes/sequence-item.ts +433 -0
- package/src/nodes/sequence-one.ts +40 -0
- package/src/nodes/swimlane-node.ts +66 -206
- package/src/nodes/wave-node.ts +609 -0
- package/src/non-working-display.ts +32 -0
- package/src/phases/date-window.ts +191 -0
- package/src/phases/finalize.ts +1303 -0
- package/src/phases/measure.ts +329 -0
- package/src/phases/place.ts +318 -0
- package/src/renderable.ts +23 -3
- package/src/resolve-today.ts +28 -25
- package/src/row-packer.ts +85 -84
- package/src/schedule.ts +362 -114
- package/src/start-rules.ts +113 -0
- package/src/start-terms.ts +38 -0
- package/src/status-dot.ts +81 -0
- package/src/style-resolution.ts +27 -57
- package/src/text-measure.ts +71 -0
- package/src/themes/contrast.ts +34 -0
- package/src/themes/dark.ts +28 -1
- package/src/themes/grayscale.ts +28 -1
- package/src/themes/index.ts +10 -7
- package/src/themes/light.ts +28 -1
- package/src/themes/shape.ts +38 -0
- package/src/themes/shared.ts +85 -93
- package/src/time-scale.ts +117 -19
- package/src/types.ts +529 -20
- package/src/view-preset.ts +520 -27
- package/src/wave-barrier.ts +242 -0
- package/src/wave-hatch.ts +25 -0
- package/src/wave-layout.ts +203 -0
- package/src/working-calendar.ts +875 -57
- package/src/working-span.ts +19 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// MarkerBand — the marker row's entries, packer and placements. Anchors,
|
|
2
|
+
// milestones, the titled non-working runs and the working exceptions all
|
|
3
|
+
// pack into one band of rows above the chart. The measure phase packs an
|
|
4
|
+
// estimate to size the band before any lane is placed; finalize packs
|
|
5
|
+
// every marker once the canvas is final (specs/layout-phases.md §3.3).
|
|
6
|
+
// Both build their entries and turn the packed rows into y positions
|
|
7
|
+
// with the helpers here; `markerLabelsRightX` sizes the canvas for the
|
|
8
|
+
// labels that fit on neither side.
|
|
9
|
+
|
|
10
|
+
import type { AnchorDeclaration, MilestoneDeclaration } from '@nowline/core';
|
|
11
|
+
import { parseIsoDate, propValue } from '../dsl-utils.js';
|
|
12
|
+
import { GUTTER_PX } from '../themes/shared.js';
|
|
13
|
+
import type { TimeScale } from '../time-scale.js';
|
|
14
|
+
import type {
|
|
15
|
+
BoundingBox,
|
|
16
|
+
MarkerRowPlacement,
|
|
17
|
+
PositionedNonWorkingRun,
|
|
18
|
+
PositionedWorkingException,
|
|
19
|
+
} from '../types.js';
|
|
20
|
+
import {
|
|
21
|
+
MARKER_BOLD_WIDTH_FACTOR,
|
|
22
|
+
MARKER_DIAMOND_RADIUS_PX,
|
|
23
|
+
MARKER_LABEL_GAP_PX,
|
|
24
|
+
MARKER_LABEL_HEIGHT_PX,
|
|
25
|
+
MARKER_ROW_CENTER_OFFSET_PX,
|
|
26
|
+
MARKER_ROW_PITCH_PX,
|
|
27
|
+
} from './marker-geometry.js';
|
|
28
|
+
import type { MilestonePlacement } from './milestone-node.js';
|
|
29
|
+
|
|
30
|
+
/** A packed marker band: its rows and each marker's placement at the band's y. */
|
|
31
|
+
export interface MarkerBand {
|
|
32
|
+
/** Rows the band holds: at least 1 when it has any marker, else 0. */
|
|
33
|
+
rowCount: number;
|
|
34
|
+
placements: Map<string, MarkerRowPlacement>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A marker pinned by its `date:`: a dated anchor or milestone inside the window. */
|
|
38
|
+
export type DatePinnedMarker = MarkerEntity & { date: Date; isMilestone: boolean };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The date-pinned anchors, then the date-pinned milestones, each at the x
|
|
42
|
+
* of its `date:`. A marker with no date, or a date outside the window,
|
|
43
|
+
* is left out; after-only milestones are placed with the lanes.
|
|
44
|
+
*/
|
|
45
|
+
export function datePinnedMarkerEntries(
|
|
46
|
+
anchors: ReadonlyMap<string, AnchorDeclaration>,
|
|
47
|
+
milestones: ReadonlyMap<string, MilestoneDeclaration>,
|
|
48
|
+
scale: TimeScale,
|
|
49
|
+
): DatePinnedMarker[] {
|
|
50
|
+
const entries: DatePinnedMarker[] = [];
|
|
51
|
+
const add = (
|
|
52
|
+
id: string,
|
|
53
|
+
decl: AnchorDeclaration | MilestoneDeclaration,
|
|
54
|
+
isMilestone: boolean,
|
|
55
|
+
): void => {
|
|
56
|
+
const date = parseIsoDate(propValue(decl.properties, 'date'));
|
|
57
|
+
if (!date) return;
|
|
58
|
+
const x = scale.forwardWithinDomain(date);
|
|
59
|
+
if (x === null) return;
|
|
60
|
+
entries.push({
|
|
61
|
+
id,
|
|
62
|
+
centerX: x,
|
|
63
|
+
radius: MARKER_DIAMOND_RADIUS_PX,
|
|
64
|
+
title: decl.title ?? id,
|
|
65
|
+
fontSize: 10,
|
|
66
|
+
bold: isMilestone,
|
|
67
|
+
date,
|
|
68
|
+
isMilestone,
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
for (const [id, anchor] of anchors) add(id, anchor, false);
|
|
72
|
+
for (const [id, milestone] of milestones) add(id, milestone, true);
|
|
73
|
+
return entries;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The calendar's marker-row entries: the titled non-working runs, then the
|
|
78
|
+
* working exceptions. Callers append them after the anchors and
|
|
79
|
+
* milestones.
|
|
80
|
+
*/
|
|
81
|
+
export function calendarMarkerEntries(
|
|
82
|
+
runs: ReadonlyArray<PositionedNonWorkingRun> | undefined,
|
|
83
|
+
exceptions: ReadonlyArray<PositionedWorkingException> | undefined,
|
|
84
|
+
): MarkerEntity[] {
|
|
85
|
+
return [...nonWorkingRunEntries(runs), ...workingExceptionEntries(exceptions)];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Each packed placement at the band's y: row `r`'s centre sits
|
|
90
|
+
* `MARKER_ROW_CENTER_OFFSET_PX + r * MARKER_ROW_PITCH_PX` below
|
|
91
|
+
* `markerRowY`, and its label box 4 px above the centre.
|
|
92
|
+
*/
|
|
93
|
+
export function markerRowPlacements(
|
|
94
|
+
packed: ReadonlyMap<string, PackedPlacement>,
|
|
95
|
+
markerRowY: number,
|
|
96
|
+
): Map<string, MarkerRowPlacement> {
|
|
97
|
+
const out = new Map<string, MarkerRowPlacement>();
|
|
98
|
+
for (const [id, p] of packed) {
|
|
99
|
+
const centerY = markerRowY + MARKER_ROW_CENTER_OFFSET_PX + p.rowIndex * MARKER_ROW_PITCH_PX;
|
|
100
|
+
out.set(id, {
|
|
101
|
+
rowIndex: p.rowIndex,
|
|
102
|
+
centerY,
|
|
103
|
+
labelBox: { ...p.labelBox, y: centerY - 4 },
|
|
104
|
+
labelSide: p.labelSide,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The marker-row id of a titled non-working run: `nonworking:<ISO from>`. */
|
|
111
|
+
export function nonWorkingRunMarkerId(run: PositionedNonWorkingRun): string {
|
|
112
|
+
return `nonworking:${run.from.toISOString().slice(0, 10)}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The marker-row id of a working exception: `working:<ISO from>`. */
|
|
116
|
+
export function workingExceptionMarkerId(exception: PositionedWorkingException): string {
|
|
117
|
+
return `working:${exception.from.toISOString().slice(0, 10)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A zero-radius marker-row entry for a calendar label at `x`. */
|
|
121
|
+
function calendarLabelEntry(id: string, x: number, label: string): MarkerEntity {
|
|
122
|
+
return { id, centerX: x, radius: 0, title: label, fontSize: 10, bold: false };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Marker-row entries for the titled runs (specs/working-calendar.md
|
|
127
|
+
* §7.4): a zero-radius marker at the run's x (the seam under `hide`, the
|
|
128
|
+
* band's left edge under `show`) whose title is the run's label. Callers
|
|
129
|
+
* append them after the anchors and milestones, so the stable sort in
|
|
130
|
+
* `packMarkerRow` places a marker that ties a run on x first.
|
|
131
|
+
*/
|
|
132
|
+
function nonWorkingRunEntries(
|
|
133
|
+
runs: ReadonlyArray<PositionedNonWorkingRun> | undefined,
|
|
134
|
+
): MarkerEntity[] {
|
|
135
|
+
const entries: MarkerEntity[] = [];
|
|
136
|
+
for (const run of runs ?? []) {
|
|
137
|
+
if (run.label === undefined) continue;
|
|
138
|
+
entries.push(calendarLabelEntry(nonWorkingRunMarkerId(run), run.x, run.label));
|
|
139
|
+
}
|
|
140
|
+
return entries;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Marker-row entries for the working exceptions (specs/working-calendar.md
|
|
145
|
+
* §7.4), packed beside the titled runs' and on the same terms: a zero-radius
|
|
146
|
+
* marker at the first turned day's x.
|
|
147
|
+
*/
|
|
148
|
+
function workingExceptionEntries(
|
|
149
|
+
exceptions: ReadonlyArray<PositionedWorkingException> | undefined,
|
|
150
|
+
): MarkerEntity[] {
|
|
151
|
+
return (exceptions ?? []).map((exception) =>
|
|
152
|
+
calendarLabelEntry(workingExceptionMarkerId(exception), exception.x, exception.label),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The canvas right edge the marker-row labels need. A label (anchor,
|
|
158
|
+
* milestone, titled run or working exception) that fits neither right of
|
|
159
|
+
* its x (against `chartRightX`) nor left of it (against `chartLeftX`)
|
|
160
|
+
* would fall back to the left and draw over a beside header card, so the
|
|
161
|
+
* canvas grows to hold it on the right instead. A slipped milestone's
|
|
162
|
+
* label never fits on the left, where its span runs.
|
|
163
|
+
*/
|
|
164
|
+
export function markerLabelsRightX(
|
|
165
|
+
entries: ReadonlyArray<MarkerEntity>,
|
|
166
|
+
chartLeftX: number,
|
|
167
|
+
chartRightX: number,
|
|
168
|
+
estimateTextWidth: (text: string, fontSize: number) => number,
|
|
169
|
+
): number {
|
|
170
|
+
let rightX = chartRightX;
|
|
171
|
+
for (const e of entries) {
|
|
172
|
+
const labelWidth = markerLabelWidth(e, estimateTextWidth);
|
|
173
|
+
const rightEnd = e.centerX + e.radius + MARKER_LABEL_GAP_PX + labelWidth;
|
|
174
|
+
const leftStart = e.centerX - e.radius - MARKER_LABEL_GAP_PX - labelWidth;
|
|
175
|
+
const fitsLeft = leftStart >= chartLeftX && e.slipFromX === undefined;
|
|
176
|
+
if (rightEnd > chartRightX && !fitsLeft) {
|
|
177
|
+
rightX = Math.max(rightX, rightEnd + GUTTER_PX);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return rightX;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface MarkerEntity {
|
|
184
|
+
id: string;
|
|
185
|
+
centerX: number;
|
|
186
|
+
radius: number;
|
|
187
|
+
title: string;
|
|
188
|
+
fontSize: number;
|
|
189
|
+
bold: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* A slipped milestone's planned x: its entry sits at the slipped x
|
|
192
|
+
* (`centerX`, where the label goes), and the span from the planned
|
|
193
|
+
* diamond to it is reserved on its row.
|
|
194
|
+
*/
|
|
195
|
+
slipFromX?: number;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A marker label's width: bold labels widen by `MARKER_BOLD_WIDTH_FACTOR`. */
|
|
199
|
+
function markerLabelWidth(
|
|
200
|
+
e: MarkerEntity,
|
|
201
|
+
estimateTextWidth: (text: string, fontSize: number) => number,
|
|
202
|
+
): number {
|
|
203
|
+
return estimateTextWidth(e.title, e.fontSize) * (e.bold ? MARKER_BOLD_WIDTH_FACTOR : 1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface PackedPlacement {
|
|
207
|
+
rowIndex: number;
|
|
208
|
+
labelBox: BoundingBox;
|
|
209
|
+
labelSide: 'left' | 'right';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A slack or overrun arrow between two markers, drawn on `from`'s row
|
|
214
|
+
* between the two diamonds (specs/rendering.md, Milestones, "Marker
|
|
215
|
+
* predecessors"). A milestone's arrow from a group or parallel starts on
|
|
216
|
+
* the milestone's own row and is not one.
|
|
217
|
+
*/
|
|
218
|
+
export interface MarkerArrow {
|
|
219
|
+
from: string;
|
|
220
|
+
to: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The arrows between markers that `placements` draw, source to target. */
|
|
224
|
+
export function markerArrows(placements: readonly MilestonePlacement[]): MarkerArrow[] {
|
|
225
|
+
const out: MarkerArrow[] = [];
|
|
226
|
+
for (const p of placements) {
|
|
227
|
+
for (const a of p.slackArrows ?? []) {
|
|
228
|
+
if ('marker' in a && a.marker !== p.id) out.push({ from: a.marker, to: p.id });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Pack anchors + milestones into marker rows using a bottom-first
|
|
236
|
+
* tick-order strategy. Markers default to the row CLOSEST to the
|
|
237
|
+
* chart; conflicts push them UP toward the date ticks (the inverse of
|
|
238
|
+
* swimlane items which default to the top and push down).
|
|
239
|
+
*
|
|
240
|
+
* Walk order is left-to-right by `centerX` (tick order, not file
|
|
241
|
+
* declaration). For each marker:
|
|
242
|
+
* 1. Try the bottommost existing row (highest `rowIndex`), right
|
|
243
|
+
* side first then left, working UP through the rows.
|
|
244
|
+
* 2. If neither side fits at any existing row, GROW: prepend a new
|
|
245
|
+
* row at `rowIndex = 0`. Every previously-placed marker has its
|
|
246
|
+
* `rowIndex` incremented by 1 so the bottom corridor stays
|
|
247
|
+
* stable as the band expands upward toward the ticks.
|
|
248
|
+
*
|
|
249
|
+
* `arrows` (the marker-to-marker arrows, `markerArrows`) add three rules,
|
|
250
|
+
* so an arrow crosses no other marker and ends on its target's line:
|
|
251
|
+
* - An arrow's run, on its source's row between the two diamonds, is
|
|
252
|
+
* reserved on that row like a label: no other diamond, label or run
|
|
253
|
+
* may overlap it.
|
|
254
|
+
* - A target sits at or above each source's row (cut lines run down
|
|
255
|
+
* from a diamond). A source whose target is placed and that fits no
|
|
256
|
+
* allowed row grows a fresh row right below the target instead of
|
|
257
|
+
* at the top; when the rules contradict (a marker both a source and
|
|
258
|
+
* a target on one row), it grows at the top.
|
|
259
|
+
* - A label takes a side its own arrows do not run along, when that
|
|
260
|
+
* side fits; otherwise it keeps the usual right-then-left order.
|
|
261
|
+
* With no arrows the pack is unchanged.
|
|
262
|
+
*
|
|
263
|
+
* Returns row-relative placements (no centerY yet — caller fills that
|
|
264
|
+
* in once `markerRowY` is known). Row indices follow the existing
|
|
265
|
+
* convention: 0 = top of band (closest to ticks), `rowCount - 1` =
|
|
266
|
+
* bottom of band (closest to chart). The "bottom-first" preference
|
|
267
|
+
* lives entirely in the search order and grow direction; geometry
|
|
268
|
+
* stays anchored to `markerRowY` at the top.
|
|
269
|
+
*/
|
|
270
|
+
export function packMarkerRow(
|
|
271
|
+
entries: MarkerEntity[],
|
|
272
|
+
chartLeftX: number,
|
|
273
|
+
chartRightX: number,
|
|
274
|
+
estimateTextWidth: (text: string, fontSize: number) => number,
|
|
275
|
+
arrows: readonly MarkerArrow[] = [],
|
|
276
|
+
): { placements: Map<string, PackedPlacement>; rowCount: number } {
|
|
277
|
+
const sorted = [...entries].sort((a, b) => a.centerX - b.centerX);
|
|
278
|
+
const placements = new Map<string, PackedPlacement>();
|
|
279
|
+
type Span = { left: number; right: number };
|
|
280
|
+
let rowSpans: Span[][] = [];
|
|
281
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
282
|
+
const knownArrows = arrows.filter((a) => byId.has(a.from) && byId.has(a.to));
|
|
283
|
+
|
|
284
|
+
type Side = 'left' | 'right';
|
|
285
|
+
for (const e of sorted) {
|
|
286
|
+
const labelWidth = markerLabelWidth(e, estimateTextWidth);
|
|
287
|
+
const naturalRightX = e.centerX + e.radius + MARKER_LABEL_GAP_PX;
|
|
288
|
+
const naturalLeftX = e.centerX - e.radius - MARKER_LABEL_GAP_PX - labelWidth;
|
|
289
|
+
const fitsRightCanvas = naturalRightX + labelWidth <= chartRightX;
|
|
290
|
+
const fitsLeftCanvas = naturalLeftX >= chartLeftX;
|
|
291
|
+
const diamondLeft = e.centerX - e.radius;
|
|
292
|
+
const diamondRight = e.centerX + e.radius;
|
|
293
|
+
|
|
294
|
+
const sideXLeft = (s: Side): number => (s === 'right' ? naturalRightX : naturalLeftX);
|
|
295
|
+
const sideFitsCanvas = (s: Side): boolean =>
|
|
296
|
+
s === 'right' ? fitsRightCanvas : fitsLeftCanvas;
|
|
297
|
+
// The runs of e's own arrows, on e's row between the two diamonds,
|
|
298
|
+
// and a slipped milestone's span from its planned diamond.
|
|
299
|
+
const slipRun: Span[] =
|
|
300
|
+
e.slipFromX === undefined
|
|
301
|
+
? []
|
|
302
|
+
: [{ left: e.slipFromX - e.radius, right: e.centerX - e.radius }];
|
|
303
|
+
const runs: Span[] = knownArrows
|
|
304
|
+
.filter((a) => a.from === e.id)
|
|
305
|
+
.map((a) => {
|
|
306
|
+
const to = byId.get(a.to) as MarkerEntity;
|
|
307
|
+
return {
|
|
308
|
+
left: Math.min(e.centerX, to.centerX) + e.radius,
|
|
309
|
+
right: Math.max(e.centerX, to.centerX) - to.radius,
|
|
310
|
+
};
|
|
311
|
+
})
|
|
312
|
+
.concat(slipRun)
|
|
313
|
+
.filter((r) => r.right > r.left);
|
|
314
|
+
// An arrow meets its target's line only when the target sits at or
|
|
315
|
+
// above the source's row (cut lines run down from a diamond).
|
|
316
|
+
let minRow = -1;
|
|
317
|
+
let maxRow = Number.POSITIVE_INFINITY;
|
|
318
|
+
for (const a of knownArrows) {
|
|
319
|
+
if (a.to === e.id) {
|
|
320
|
+
const from = placements.get(a.from);
|
|
321
|
+
if (from) maxRow = Math.min(maxRow, from.rowIndex);
|
|
322
|
+
}
|
|
323
|
+
if (a.from === e.id) {
|
|
324
|
+
const to = placements.get(a.to);
|
|
325
|
+
if (to) minRow = Math.max(minRow, to.rowIndex);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// A label on the side its own arrow leaves would sit on the arrow.
|
|
329
|
+
const crossesOwnRun = (s: Side): boolean => {
|
|
330
|
+
const xLeft = sideXLeft(s);
|
|
331
|
+
return runs.some((r) => r.left < xLeft + labelWidth && r.right > xLeft);
|
|
332
|
+
};
|
|
333
|
+
const freeSide = (s: Side): boolean => sideFitsCanvas(s) && !crossesOwnRun(s);
|
|
334
|
+
// A slipped milestone's label never goes back over its span; the
|
|
335
|
+
// canvas grew for it (`markerLabelsRightX`).
|
|
336
|
+
const growSide: Side =
|
|
337
|
+
freeSide('right') || e.slipFromX !== undefined
|
|
338
|
+
? 'right'
|
|
339
|
+
: freeSide('left')
|
|
340
|
+
? 'left'
|
|
341
|
+
: fitsRightCanvas
|
|
342
|
+
? 'right'
|
|
343
|
+
: 'left';
|
|
344
|
+
const collidesAt = (xLeft: number, row: number): boolean => {
|
|
345
|
+
const extLeft = Math.min(diamondLeft, xLeft);
|
|
346
|
+
const extRight = Math.max(diamondRight, xLeft + labelWidth);
|
|
347
|
+
const own = [{ left: extLeft, right: extRight }, ...runs];
|
|
348
|
+
return rowSpans[row].some((s) => own.some((o) => s.left < o.right && s.right > o.left));
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
let placedRow = -1;
|
|
352
|
+
let placedSide: Side = 'right';
|
|
353
|
+
let placedXLeft = naturalRightX;
|
|
354
|
+
|
|
355
|
+
// Bottom-first: try the highest existing row first, working up
|
|
356
|
+
// toward row 0 (closest to the ticks).
|
|
357
|
+
outer: for (let row = rowSpans.length - 1; row >= 0; row--) {
|
|
358
|
+
if (row < minRow || row > maxRow) continue;
|
|
359
|
+
for (const side of ['right', 'left'] as const) {
|
|
360
|
+
if (!freeSide(side)) continue;
|
|
361
|
+
const xLeft = sideXLeft(side);
|
|
362
|
+
if (collidesAt(xLeft, row)) continue;
|
|
363
|
+
placedRow = row;
|
|
364
|
+
placedSide = side;
|
|
365
|
+
placedXLeft = xLeft;
|
|
366
|
+
break outer;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (placedRow === -1 && minRow >= 0 && minRow < maxRow) {
|
|
371
|
+
// An arrow source that must stay at or below its target and fits
|
|
372
|
+
// no row there: a fresh row right below the target, which keeps
|
|
373
|
+
// every placed arrow's source at or below its target.
|
|
374
|
+
const at = minRow + 1;
|
|
375
|
+
for (const [id, p] of placements) {
|
|
376
|
+
if (p.rowIndex >= at) placements.set(id, { ...p, rowIndex: p.rowIndex + 1 });
|
|
377
|
+
}
|
|
378
|
+
rowSpans.splice(at, 0, []);
|
|
379
|
+
placedRow = at;
|
|
380
|
+
placedSide = growSide;
|
|
381
|
+
placedXLeft = sideXLeft(placedSide);
|
|
382
|
+
} else if (placedRow === -1) {
|
|
383
|
+
// No existing row fits — prepend a fresh top row and shift
|
|
384
|
+
// every previous placement DOWN by one slot (rowIndex += 1)
|
|
385
|
+
// so the bottom corridor stays stable.
|
|
386
|
+
for (const [id, p] of placements) {
|
|
387
|
+
placements.set(id, { ...p, rowIndex: p.rowIndex + 1 });
|
|
388
|
+
}
|
|
389
|
+
rowSpans = [[], ...rowSpans];
|
|
390
|
+
placedRow = 0;
|
|
391
|
+
placedSide = growSide;
|
|
392
|
+
placedXLeft = sideXLeft(placedSide);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const extLeft = Math.min(diamondLeft, placedXLeft);
|
|
396
|
+
const extRight = Math.max(diamondRight, placedXLeft + labelWidth);
|
|
397
|
+
rowSpans[placedRow].push({ left: extLeft, right: extRight }, ...runs);
|
|
398
|
+
placements.set(e.id, {
|
|
399
|
+
rowIndex: placedRow,
|
|
400
|
+
labelBox: { x: placedXLeft, y: 0, width: labelWidth, height: MARKER_LABEL_HEIGHT_PX },
|
|
401
|
+
labelSide: placedSide,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return { placements, rowCount: rowSpans.length };
|
|
406
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// the timeline header. The marker band stacks N rows tall when
|
|
9
9
|
// anchors and milestones can't fit on a single row without colliding.
|
|
10
10
|
// - The bare `13` (= PITCH / 2) midpoint that used to sprinkle
|
|
11
|
-
//
|
|
11
|
+
// the roadmap composition code is derived from this constant — bumping the
|
|
12
12
|
// pitch automatically re-centers labels.
|
|
13
13
|
//
|
|
14
14
|
// Diamond + label box dimensions are reused by every marker (anchor +
|