@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.
Files changed (339) hide show
  1. package/dist/axis-columns.d.ts +70 -0
  2. package/dist/axis-columns.d.ts.map +1 -0
  3. package/dist/axis-columns.js +104 -0
  4. package/dist/axis-columns.js.map +1 -0
  5. package/dist/band-scale.d.ts.map +1 -1
  6. package/dist/calendar-resolver.d.ts +27 -0
  7. package/dist/calendar-resolver.d.ts.map +1 -0
  8. package/dist/calendar-resolver.js +132 -0
  9. package/dist/calendar-resolver.js.map +1 -0
  10. package/dist/calendar.d.ts +0 -19
  11. package/dist/calendar.d.ts.map +1 -1
  12. package/dist/calendar.js +4 -44
  13. package/dist/calendar.js.map +1 -1
  14. package/dist/canvas-width.d.ts +23 -0
  15. package/dist/canvas-width.d.ts.map +1 -0
  16. package/dist/canvas-width.js +26 -0
  17. package/dist/canvas-width.js.map +1 -0
  18. package/dist/capacity.d.ts +28 -0
  19. package/dist/capacity.d.ts.map +1 -1
  20. package/dist/capacity.js +64 -29
  21. package/dist/capacity.js.map +1 -1
  22. package/dist/chart-translation.d.ts +22 -0
  23. package/dist/chart-translation.d.ts.map +1 -0
  24. package/dist/chart-translation.js +83 -0
  25. package/dist/chart-translation.js.map +1 -0
  26. package/dist/container-header-geometry.d.ts +64 -0
  27. package/dist/container-header-geometry.d.ts.map +1 -0
  28. package/dist/container-header-geometry.js +89 -0
  29. package/dist/container-header-geometry.js.map +1 -0
  30. package/dist/dsl-utils.d.ts +3 -4
  31. package/dist/dsl-utils.d.ts.map +1 -1
  32. package/dist/dsl-utils.js +8 -25
  33. package/dist/dsl-utils.js.map +1 -1
  34. package/dist/edge-routing.d.ts +69 -78
  35. package/dist/edge-routing.d.ts.map +1 -1
  36. package/dist/edge-routing.js +271 -401
  37. package/dist/edge-routing.js.map +1 -1
  38. package/dist/frame-tab-geometry.d.ts +24 -14
  39. package/dist/frame-tab-geometry.d.ts.map +1 -1
  40. package/dist/frame-tab-geometry.js +69 -23
  41. package/dist/frame-tab-geometry.js.map +1 -1
  42. package/dist/header-card-geometry.d.ts +12 -0
  43. package/dist/header-card-geometry.d.ts.map +1 -1
  44. package/dist/header-card-geometry.js +14 -2
  45. package/dist/header-card-geometry.js.map +1 -1
  46. package/dist/header-logo.d.ts +28 -0
  47. package/dist/header-logo.d.ts.map +1 -0
  48. package/dist/header-logo.js +51 -0
  49. package/dist/header-logo.js.map +1 -0
  50. package/dist/i18n.d.ts +11 -0
  51. package/dist/i18n.d.ts.map +1 -1
  52. package/dist/i18n.js +11 -1
  53. package/dist/i18n.js.map +1 -1
  54. package/dist/index.d.ts +20 -8
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +17 -7
  57. package/dist/index.js.map +1 -1
  58. package/dist/inline-date-pin-geometry.d.ts +19 -11
  59. package/dist/inline-date-pin-geometry.d.ts.map +1 -1
  60. package/dist/inline-date-pin-geometry.js +45 -46
  61. package/dist/inline-date-pin-geometry.js.map +1 -1
  62. package/dist/item-bar-geometry.d.ts +192 -4
  63. package/dist/item-bar-geometry.d.ts.map +1 -1
  64. package/dist/item-bar-geometry.js +258 -5
  65. package/dist/item-bar-geometry.js.map +1 -1
  66. package/dist/item-port-geometry.d.ts +13 -0
  67. package/dist/item-port-geometry.d.ts.map +1 -0
  68. package/dist/item-port-geometry.js +63 -0
  69. package/dist/item-port-geometry.js.map +1 -0
  70. package/dist/layout-context.d.ts +115 -51
  71. package/dist/layout-context.d.ts.map +1 -1
  72. package/dist/layout-context.js +96 -5
  73. package/dist/layout-context.js.map +1 -1
  74. package/dist/layout-insights.d.ts.map +1 -1
  75. package/dist/layout-insights.js +73 -1
  76. package/dist/layout-insights.js.map +1 -1
  77. package/dist/layout.d.ts +35 -0
  78. package/dist/layout.d.ts.map +1 -1
  79. package/dist/layout.js +18 -1315
  80. package/dist/layout.js.map +1 -1
  81. package/dist/nodes/anchor-node.d.ts.map +1 -1
  82. package/dist/nodes/anchor-node.js +11 -4
  83. package/dist/nodes/anchor-node.js.map +1 -1
  84. package/dist/nodes/content-extent.d.ts +24 -0
  85. package/dist/nodes/content-extent.d.ts.map +1 -0
  86. package/dist/nodes/content-extent.js +100 -0
  87. package/dist/nodes/content-extent.js.map +1 -0
  88. package/dist/nodes/dependency-edges.d.ts +6 -0
  89. package/dist/nodes/dependency-edges.d.ts.map +1 -0
  90. package/dist/nodes/dependency-edges.js +148 -0
  91. package/dist/nodes/dependency-edges.js.map +1 -0
  92. package/dist/nodes/footnote-node.d.ts +24 -3
  93. package/dist/nodes/footnote-node.d.ts.map +1 -1
  94. package/dist/nodes/footnote-node.js +129 -25
  95. package/dist/nodes/footnote-node.js.map +1 -1
  96. package/dist/nodes/group-node.d.ts +10 -21
  97. package/dist/nodes/group-node.d.ts.map +1 -1
  98. package/dist/nodes/group-node.js +79 -154
  99. package/dist/nodes/group-node.js.map +1 -1
  100. package/dist/nodes/header-node.d.ts +56 -0
  101. package/dist/nodes/header-node.d.ts.map +1 -0
  102. package/dist/nodes/header-node.js +163 -0
  103. package/dist/nodes/header-node.js.map +1 -0
  104. package/dist/nodes/include-node.d.ts +22 -12
  105. package/dist/nodes/include-node.d.ts.map +1 -1
  106. package/dist/nodes/include-node.js +95 -62
  107. package/dist/nodes/include-node.js.map +1 -1
  108. package/dist/nodes/item-measure.d.ts +135 -0
  109. package/dist/nodes/item-measure.d.ts.map +1 -0
  110. package/dist/nodes/item-measure.js +514 -0
  111. package/dist/nodes/item-measure.js.map +1 -0
  112. package/dist/nodes/item-node.d.ts +162 -9
  113. package/dist/nodes/item-node.d.ts.map +1 -1
  114. package/dist/nodes/item-node.js +256 -69
  115. package/dist/nodes/item-node.js.map +1 -1
  116. package/dist/nodes/marker-band.d.ts +118 -0
  117. package/dist/nodes/marker-band.d.ts.map +1 -0
  118. package/dist/nodes/marker-band.js +303 -0
  119. package/dist/nodes/marker-band.js.map +1 -0
  120. package/dist/nodes/marker-geometry.js +1 -1
  121. package/dist/nodes/marker-geometry.js.map +1 -1
  122. package/dist/nodes/milestone-node.d.ts +112 -15
  123. package/dist/nodes/milestone-node.d.ts.map +1 -1
  124. package/dist/nodes/milestone-node.js +246 -95
  125. package/dist/nodes/milestone-node.js.map +1 -1
  126. package/dist/nodes/nowline-node.d.ts +8 -0
  127. package/dist/nodes/nowline-node.d.ts.map +1 -0
  128. package/dist/nodes/nowline-node.js +74 -0
  129. package/dist/nodes/nowline-node.js.map +1 -0
  130. package/dist/nodes/parallel-node.d.ts +7 -12
  131. package/dist/nodes/parallel-node.d.ts.map +1 -1
  132. package/dist/nodes/parallel-node.js +66 -32
  133. package/dist/nodes/parallel-node.js.map +1 -1
  134. package/dist/nodes/region-markers.d.ts +41 -0
  135. package/dist/nodes/region-markers.d.ts.map +1 -0
  136. package/dist/nodes/region-markers.js +130 -0
  137. package/dist/nodes/region-markers.js.map +1 -0
  138. package/dist/nodes/roadmap-node.d.ts +9 -72
  139. package/dist/nodes/roadmap-node.d.ts.map +1 -1
  140. package/dist/nodes/roadmap-node.js +38 -788
  141. package/dist/nodes/roadmap-node.js.map +1 -1
  142. package/dist/nodes/row-track.d.ts +33 -0
  143. package/dist/nodes/row-track.d.ts.map +1 -0
  144. package/dist/nodes/row-track.js +170 -0
  145. package/dist/nodes/row-track.js.map +1 -0
  146. package/dist/nodes/sequence-item.d.ts +25 -0
  147. package/dist/nodes/sequence-item.d.ts.map +1 -0
  148. package/dist/nodes/sequence-item.js +396 -0
  149. package/dist/nodes/sequence-item.js.map +1 -0
  150. package/dist/nodes/sequence-one.d.ts +5 -0
  151. package/dist/nodes/sequence-one.d.ts.map +1 -0
  152. package/dist/nodes/sequence-one.js +26 -0
  153. package/dist/nodes/sequence-one.js.map +1 -0
  154. package/dist/nodes/swimlane-node.d.ts +5 -17
  155. package/dist/nodes/swimlane-node.d.ts.map +1 -1
  156. package/dist/nodes/swimlane-node.js +62 -147
  157. package/dist/nodes/swimlane-node.js.map +1 -1
  158. package/dist/nodes/wave-node.d.ts +52 -0
  159. package/dist/nodes/wave-node.d.ts.map +1 -0
  160. package/dist/nodes/wave-node.js +483 -0
  161. package/dist/nodes/wave-node.js.map +1 -0
  162. package/dist/non-working-display.d.ts +14 -0
  163. package/dist/non-working-display.d.ts.map +1 -0
  164. package/dist/non-working-display.js +28 -0
  165. package/dist/non-working-display.js.map +1 -0
  166. package/dist/phases/date-window.d.ts +48 -0
  167. package/dist/phases/date-window.d.ts.map +1 -0
  168. package/dist/phases/date-window.js +130 -0
  169. package/dist/phases/date-window.js.map +1 -0
  170. package/dist/phases/finalize.d.ts +198 -0
  171. package/dist/phases/finalize.d.ts.map +1 -0
  172. package/dist/phases/finalize.js +1058 -0
  173. package/dist/phases/finalize.js.map +1 -0
  174. package/dist/phases/measure.d.ts +77 -0
  175. package/dist/phases/measure.d.ts.map +1 -0
  176. package/dist/phases/measure.js +222 -0
  177. package/dist/phases/measure.js.map +1 -0
  178. package/dist/phases/place.d.ts +28 -0
  179. package/dist/phases/place.d.ts.map +1 -0
  180. package/dist/phases/place.js +225 -0
  181. package/dist/phases/place.js.map +1 -0
  182. package/dist/renderable.d.ts.map +1 -1
  183. package/dist/renderable.js +23 -3
  184. package/dist/renderable.js.map +1 -1
  185. package/dist/resolve-today.d.ts +11 -1
  186. package/dist/resolve-today.d.ts.map +1 -1
  187. package/dist/resolve-today.js +26 -12
  188. package/dist/resolve-today.js.map +1 -1
  189. package/dist/row-packer.d.ts +42 -16
  190. package/dist/row-packer.d.ts.map +1 -1
  191. package/dist/row-packer.js +51 -78
  192. package/dist/row-packer.js.map +1 -1
  193. package/dist/schedule.d.ts +58 -1
  194. package/dist/schedule.d.ts.map +1 -1
  195. package/dist/schedule.js +280 -106
  196. package/dist/schedule.js.map +1 -1
  197. package/dist/start-rules.d.ts +27 -0
  198. package/dist/start-rules.d.ts.map +1 -0
  199. package/dist/start-rules.js +91 -0
  200. package/dist/start-rules.js.map +1 -0
  201. package/dist/start-terms.d.ts +23 -0
  202. package/dist/start-terms.d.ts.map +1 -0
  203. package/dist/start-terms.js +27 -0
  204. package/dist/start-terms.js.map +1 -0
  205. package/dist/status-dot.d.ts +46 -0
  206. package/dist/status-dot.d.ts.map +1 -0
  207. package/dist/status-dot.js +78 -0
  208. package/dist/status-dot.js.map +1 -0
  209. package/dist/style-resolution.d.ts.map +1 -1
  210. package/dist/style-resolution.js +23 -43
  211. package/dist/style-resolution.js.map +1 -1
  212. package/dist/text-measure.d.ts +28 -0
  213. package/dist/text-measure.d.ts.map +1 -0
  214. package/dist/text-measure.js +70 -0
  215. package/dist/text-measure.js.map +1 -0
  216. package/dist/themes/contrast.d.ts +4 -0
  217. package/dist/themes/contrast.d.ts.map +1 -0
  218. package/dist/themes/contrast.js +32 -0
  219. package/dist/themes/contrast.js.map +1 -0
  220. package/dist/themes/dark.d.ts.map +1 -1
  221. package/dist/themes/dark.js +28 -1
  222. package/dist/themes/dark.js.map +1 -1
  223. package/dist/themes/grayscale.d.ts.map +1 -1
  224. package/dist/themes/grayscale.js +28 -1
  225. package/dist/themes/grayscale.js.map +1 -1
  226. package/dist/themes/index.d.ts +4 -11
  227. package/dist/themes/index.d.ts.map +1 -1
  228. package/dist/themes/index.js +6 -4
  229. package/dist/themes/index.js.map +1 -1
  230. package/dist/themes/light.d.ts.map +1 -1
  231. package/dist/themes/light.js +28 -1
  232. package/dist/themes/light.js.map +1 -1
  233. package/dist/themes/shape.d.ts +31 -0
  234. package/dist/themes/shape.d.ts.map +1 -1
  235. package/dist/themes/shared.d.ts +44 -50
  236. package/dist/themes/shared.d.ts.map +1 -1
  237. package/dist/themes/shared.js +79 -83
  238. package/dist/themes/shared.js.map +1 -1
  239. package/dist/time-scale.d.ts +42 -6
  240. package/dist/time-scale.d.ts.map +1 -1
  241. package/dist/time-scale.js +109 -17
  242. package/dist/time-scale.js.map +1 -1
  243. package/dist/types.d.ts +528 -11
  244. package/dist/types.d.ts.map +1 -1
  245. package/dist/view-preset.d.ts +61 -6
  246. package/dist/view-preset.d.ts.map +1 -1
  247. package/dist/view-preset.js +439 -24
  248. package/dist/view-preset.js.map +1 -1
  249. package/dist/wave-barrier.d.ts +107 -0
  250. package/dist/wave-barrier.d.ts.map +1 -0
  251. package/dist/wave-barrier.js +174 -0
  252. package/dist/wave-barrier.js.map +1 -0
  253. package/dist/wave-hatch.d.ts +4 -0
  254. package/dist/wave-hatch.d.ts.map +1 -0
  255. package/dist/wave-hatch.js +21 -0
  256. package/dist/wave-hatch.js.map +1 -0
  257. package/dist/wave-layout.d.ts +99 -0
  258. package/dist/wave-layout.d.ts.map +1 -0
  259. package/dist/wave-layout.js +130 -0
  260. package/dist/wave-layout.js.map +1 -0
  261. package/dist/working-calendar.d.ts +128 -4
  262. package/dist/working-calendar.d.ts.map +1 -1
  263. package/dist/working-calendar.js +725 -51
  264. package/dist/working-calendar.js.map +1 -1
  265. package/dist/working-span.d.ts +11 -0
  266. package/dist/working-span.d.ts.map +1 -0
  267. package/dist/working-span.js +18 -0
  268. package/dist/working-span.js.map +1 -0
  269. package/package.json +7 -7
  270. package/src/axis-columns.ts +140 -0
  271. package/src/calendar-resolver.ts +161 -0
  272. package/src/calendar.ts +4 -45
  273. package/src/canvas-width.ts +36 -0
  274. package/src/capacity.ts +71 -30
  275. package/src/chart-translation.ts +88 -0
  276. package/src/container-header-geometry.ts +116 -0
  277. package/src/dsl-utils.ts +8 -26
  278. package/src/edge-routing.ts +347 -491
  279. package/src/frame-tab-geometry.ts +78 -25
  280. package/src/header-card-geometry.ts +17 -2
  281. package/src/header-logo.ts +67 -0
  282. package/src/i18n.ts +22 -1
  283. package/src/index.ts +97 -12
  284. package/src/inline-date-pin-geometry.ts +55 -58
  285. package/src/item-bar-geometry.ts +341 -5
  286. package/src/item-port-geometry.ts +69 -0
  287. package/src/layout-context.ts +210 -72
  288. package/src/layout-insights.ts +109 -2
  289. package/src/layout.ts +57 -1530
  290. package/src/nodes/anchor-node.ts +11 -4
  291. package/src/nodes/content-extent.ts +107 -0
  292. package/src/nodes/dependency-edges.ts +171 -0
  293. package/src/nodes/footnote-node.ts +171 -31
  294. package/src/nodes/group-node.ts +92 -215
  295. package/src/nodes/header-node.ts +255 -0
  296. package/src/nodes/include-node.ts +124 -110
  297. package/src/nodes/item-measure.ts +723 -0
  298. package/src/nodes/item-node.ts +358 -80
  299. package/src/nodes/marker-band.ts +406 -0
  300. package/src/nodes/marker-geometry.ts +1 -1
  301. package/src/nodes/milestone-node.ts +321 -136
  302. package/src/nodes/nowline-node.ts +87 -0
  303. package/src/nodes/parallel-node.ts +69 -39
  304. package/src/nodes/region-markers.ts +194 -0
  305. package/src/nodes/roadmap-node.ts +46 -950
  306. package/src/nodes/row-track.ts +217 -0
  307. package/src/nodes/sequence-item.ts +433 -0
  308. package/src/nodes/sequence-one.ts +40 -0
  309. package/src/nodes/swimlane-node.ts +66 -206
  310. package/src/nodes/wave-node.ts +609 -0
  311. package/src/non-working-display.ts +32 -0
  312. package/src/phases/date-window.ts +191 -0
  313. package/src/phases/finalize.ts +1303 -0
  314. package/src/phases/measure.ts +329 -0
  315. package/src/phases/place.ts +318 -0
  316. package/src/renderable.ts +23 -3
  317. package/src/resolve-today.ts +28 -25
  318. package/src/row-packer.ts +85 -84
  319. package/src/schedule.ts +362 -114
  320. package/src/start-rules.ts +113 -0
  321. package/src/start-terms.ts +38 -0
  322. package/src/status-dot.ts +81 -0
  323. package/src/style-resolution.ts +27 -57
  324. package/src/text-measure.ts +71 -0
  325. package/src/themes/contrast.ts +34 -0
  326. package/src/themes/dark.ts +28 -1
  327. package/src/themes/grayscale.ts +28 -1
  328. package/src/themes/index.ts +10 -7
  329. package/src/themes/light.ts +28 -1
  330. package/src/themes/shape.ts +38 -0
  331. package/src/themes/shared.ts +85 -93
  332. package/src/time-scale.ts +117 -19
  333. package/src/types.ts +529 -20
  334. package/src/view-preset.ts +520 -27
  335. package/src/wave-barrier.ts +242 -0
  336. package/src/wave-hatch.ts +25 -0
  337. package/src/wave-layout.ts +203 -0
  338. package/src/working-calendar.ts +875 -57
  339. package/src/working-span.ts +19 -0
@@ -2,11 +2,11 @@
2
2
  // helper. Each anchor sits in the marker row of the timeline and emits
3
3
  // a downward cut line through the chart. Marker-row placement (row +
4
4
  // label box + left/right label side) is decided up-front in
5
- // `roadmap-node.ts` (`packMarkerRow`) so anchors and milestones share
5
+ // `marker-band.ts` (`packMarkerRow`) so anchors and milestones share
6
6
  // the same row stack — see `LayoutContext.markerRowPlacements`.
7
7
 
8
8
  import type { AnchorDeclaration } from '@nowline/core';
9
- import { parseDate, propValue } from '../dsl-utils.js';
9
+ import { formatIsoDate, parseIsoDate, propValue } from '../dsl-utils.js';
10
10
  import type { LayoutContext } from '../layout-context.js';
11
11
  import { resolveStyle } from '../style-resolution.js';
12
12
  import type { Point, PositionedAnchor } from '../types.js';
@@ -29,7 +29,7 @@ export class AnchorNode {
29
29
  */
30
30
  place(ctx: LayoutContext, _milestoneXs: Set<number>): PositionedAnchor | null {
31
31
  const dateRaw = propValue(this.anchor.properties, 'date');
32
- const date = parseDate(dateRaw);
32
+ const date = parseIsoDate(dateRaw);
33
33
  if (!date) return null;
34
34
  const x = ctx.scale.forwardWithinDomain(date);
35
35
  if (x === null) return null;
@@ -47,7 +47,7 @@ export class AnchorNode {
47
47
  ctx.entityLeftEdges.set(this.id, x);
48
48
  ctx.entityRightEdges.set(this.id, x);
49
49
  ctx.entityMidpoints.set(this.id, center);
50
- return {
50
+ const positioned: PositionedAnchor = {
51
51
  id: this.id,
52
52
  title: this.anchor.title ?? this.id,
53
53
  center,
@@ -60,6 +60,13 @@ export class AnchorNode {
60
60
  labelBox,
61
61
  labelSide,
62
62
  };
63
+ // A date the axis hides keeps its own day as a tooltip
64
+ // (specs/working-calendar.md §7.4). Under `show` the day is drawn,
65
+ // and the anchor sits on it.
66
+ if (!ctx.scale.showsNonWorking && !ctx.calendar.isWorkingDay(date)) {
67
+ positioned.hiddenDate = formatIsoDate(date);
68
+ }
69
+ return positioned;
63
70
  }
64
71
  }
65
72
 
@@ -0,0 +1,107 @@
1
+ // content-extent — shared helpers that measure how far right placed
2
+ // content reaches. `maxLeafItemRightX` measures the rightmost edge
3
+ // reached by leaf item bars inside a set of swimlanes, recursing
4
+ // through nested swimlanes and group/parallel `children`;
5
+ // `trackChildPaintedRightX` measures everything one track child paints,
6
+ // spilled captions included.
7
+ //
8
+ // A `PositionedItem.box` is always the bar itself, never a spilled
9
+ // caption (see `PositionedItem.textSpills`), so the leaf walk is immune
10
+ // to caption spill by construction. That makes it a stricter measure
11
+ // than `RowPacker.usedRightX()` / `SwimlaneNode`'s `usedRightX`,
12
+ // which both fold in `spillX` (the caption's reserved column).
13
+ //
14
+ // Used by:
15
+ // - Finalize's timeline extension (`extendTimeline`, f2), which
16
+ // grows the date window to cover overflowing content boxes
17
+ // without letting overhanging captions drag it out too.
18
+ // - `buildIncludeRegions`, to size the dashed include bracket to
19
+ // its nested content's bars rather than a spilled caption.
20
+ //
21
+ // `maxSpilledCaptionRightX` is the other half of f2: a spilled caption
22
+ // past the axis end grows the axis by the one column it was capped to.
23
+
24
+ import { containerHeaderRightX } from '../container-header-geometry.js';
25
+ import type { PositionedSwimlane, PositionedTrackChild } from '../types.js';
26
+
27
+ /**
28
+ * Rightmost x anything in a placed track child paints: an item's bar, its
29
+ * spilled caption, decorations and chips; a group's or parallel's box, its
30
+ * header (a title may run past the bracket's end) and, recursively, its
31
+ * children. A container's box spans only its dates, so a caption one of its
32
+ * children spills past that edge shows up here and nowhere else. The row
33
+ * track feeds it to the packer so the canvas grows to it.
34
+ */
35
+ export function trackChildPaintedRightX(child: PositionedTrackChild): number {
36
+ if (child.kind === 'item') {
37
+ // `decorationsRightX` leads the spill column even when nothing
38
+ // spills, so it counts only when something does.
39
+ const spills =
40
+ child.textSpills || child.dotSpills || child.iconSpills || child.footnoteSpills;
41
+ return Math.max(
42
+ child.box.x + child.box.width,
43
+ spills ? child.decorationsRightX : Number.NEGATIVE_INFINITY,
44
+ child.chipsOutside ? child.chipsRightX : Number.NEGATIVE_INFINITY,
45
+ );
46
+ }
47
+ let right = Math.max(child.box.x + child.box.width, containerHeaderRightX(child));
48
+ for (const sub of child.children) right = Math.max(right, trackChildPaintedRightX(sub));
49
+ return right;
50
+ }
51
+
52
+ /** Rightmost x reached by any leaf item bar across `swimlanes`, recursing
53
+ * into nested swimlanes and group/parallel children. Returns 0 when the
54
+ * set contains no items — callers combine this with another baseline
55
+ * (e.g. `originX` or a chrome-derived minimum) via `Math.max`. */
56
+ export function maxLeafItemRightX(swimlanes: PositionedSwimlane[]): number {
57
+ let max = 0;
58
+ for (const lane of swimlanes) {
59
+ max = Math.max(max, maxTrackChildRightX(lane.children));
60
+ if (lane.nested.length > 0) {
61
+ max = Math.max(max, maxLeafItemRightX(lane.nested));
62
+ }
63
+ }
64
+ return max;
65
+ }
66
+
67
+ function maxTrackChildRightX(children: PositionedTrackChild[]): number {
68
+ let max = 0;
69
+ for (const child of children) {
70
+ if (child.kind === 'item') {
71
+ max = Math.max(max, child.box.x + child.box.width);
72
+ } else {
73
+ max = Math.max(max, maxTrackChildRightX(child.children));
74
+ }
75
+ }
76
+ return max;
77
+ }
78
+
79
+ /**
80
+ * Rightmost x a spilled item caption reaches across `swimlanes` (its spill
81
+ * column: icon, caption, footnotes, dot), recursing like
82
+ * `maxLeafItemRightX`; 0 when no caption spills. Finalize f2 grows the
83
+ * axis by one column for a caption that ends past it (specs/rendering.md
84
+ * § "Caption cap at the chart's end").
85
+ */
86
+ export function maxSpilledCaptionRightX(swimlanes: PositionedSwimlane[]): number {
87
+ let max = 0;
88
+ for (const lane of swimlanes) {
89
+ max = Math.max(max, maxTrackChildCaptionRightX(lane.children));
90
+ if (lane.nested.length > 0) {
91
+ max = Math.max(max, maxSpilledCaptionRightX(lane.nested));
92
+ }
93
+ }
94
+ return max;
95
+ }
96
+
97
+ function maxTrackChildCaptionRightX(children: PositionedTrackChild[]): number {
98
+ let max = 0;
99
+ for (const child of children) {
100
+ if (child.kind === 'item') {
101
+ if (child.textSpills) max = Math.max(max, child.decorationsRightX);
102
+ } else {
103
+ max = Math.max(max, maxTrackChildCaptionRightX(child.children));
104
+ }
105
+ }
106
+ return max;
107
+ }
@@ -0,0 +1,171 @@
1
+ // Dependency edges (finalize step f11): every `after:` reference of a
2
+ // placed item becomes an arrow on the dates (specs/true-width-layout.md
3
+ // §3.4), its ends read off the final model. The host chart and each
4
+ // isolated region route their own, among their own lanes.
5
+
6
+ import type { ItemDeclaration, SwimlaneDeclaration } from '@nowline/core';
7
+ import { isItemDeclaration } from '@nowline/core';
8
+ import { trackNodes } from '@nowline/core/internal';
9
+ import { containerHeaderRightX } from '../container-header-geometry.js';
10
+ import { propValues } from '../dsl-utils.js';
11
+ import {
12
+ type ArrowEnd,
13
+ arrowHead,
14
+ type DependencyRouteRequest,
15
+ type RouteObstacles,
16
+ routeDependencies,
17
+ } from '../edge-routing.js';
18
+ import { containerArrowEnd, itemArrowEnd } from '../item-port-geometry.js';
19
+ import type { LayoutContext } from '../layout-context.js';
20
+ import { resolveStyle } from '../style-resolution.js';
21
+ import type {
22
+ PositionedDependencyEdge,
23
+ PositionedGroup,
24
+ PositionedIncludeRegion,
25
+ PositionedParallel,
26
+ PositionedSwimlane,
27
+ PositionedTrackChild,
28
+ } from '../types.js';
29
+ import { trackChildPaintedRightX } from './content-extent.js';
30
+ import { syntheticItemKey } from './sequence-item.js';
31
+
32
+ // Every item under the lanes, keyed by id. Title-only items register under a
33
+ // synthetic, non-referenceable handle so they still appear as dependency-edge
34
+ // targets.
35
+ export function collectItems(swimlanes: SwimlaneDeclaration[]): Map<string, ItemDeclaration> {
36
+ const out = new Map<string, ItemDeclaration>();
37
+ for (const node of trackNodes(swimlanes)) {
38
+ if (isItemDeclaration(node)) out.set(node.name ?? syntheticItemKey(node), node);
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /** A target that starts within this of a marker's date touches its line. */
44
+ const ON_LINE_PX = 0.01;
45
+
46
+ /**
47
+ * What the router must route around, and every group and parallel by id
48
+ * (the containers an `after:` may name), from the lanes it routes among.
49
+ * Include regions in `includes` count as obstacles only: their own arrows
50
+ * are routed separately.
51
+ */
52
+ function collectRouting(
53
+ swimlanes: PositionedSwimlane[],
54
+ includes: PositionedIncludeRegion[],
55
+ ): { obstacles: RouteObstacles; containers: Map<string, PositionedGroup | PositionedParallel> } {
56
+ const obstacles: RouteObstacles = { blocks: [], edges: [] };
57
+ const containers = new Map<string, PositionedGroup | PositionedParallel>();
58
+ const line = (x: number, width: number, y: number): void => {
59
+ obstacles.blocks.push({ x, y, width, height: 0 });
60
+ };
61
+ const visitChild = (child: PositionedTrackChild, register: boolean): void => {
62
+ const { box } = child;
63
+ if (child.kind === 'item') {
64
+ // The bar with whatever it spills right of itself (caption,
65
+ // decorations, chips), so a horizontal leg stays out of the text.
66
+ obstacles.blocks.push({ ...box, width: trackChildPaintedRightX(child) - box.x });
67
+ obstacles.edges.push(
68
+ { x: box.x, top: box.y, bottom: box.y + box.height },
69
+ { x: box.x + box.width, top: box.y, bottom: box.y + box.height },
70
+ );
71
+ return;
72
+ }
73
+ if (register && child.id) containers.set(child.id, child);
74
+ if (!(child.kind === 'group' && child.waveOnly)) {
75
+ if (child.headerHeight > 0) {
76
+ obstacles.blocks.push({
77
+ x: box.x,
78
+ y: box.y,
79
+ width: Math.max(box.width, containerHeaderRightX(child) - box.x),
80
+ height: child.headerHeight,
81
+ });
82
+ }
83
+ // Nor along the body's bottom edge.
84
+ line(box.x, box.width, box.y + box.height);
85
+ obstacles.edges.push(
86
+ { x: box.x, top: box.y, bottom: box.y + box.height },
87
+ { x: box.x + box.width, top: box.y, bottom: box.y + box.height },
88
+ );
89
+ }
90
+ for (const sub of child.children) visitChild(sub, register);
91
+ };
92
+ const visitLane = (lane: PositionedSwimlane, register: boolean): void => {
93
+ // A leg never runs along a lane's edge.
94
+ line(lane.box.x, lane.box.width, lane.box.y);
95
+ line(lane.box.x, lane.box.width, lane.box.y + lane.box.height);
96
+ for (const child of lane.children) visitChild(child, register);
97
+ for (const nested of lane.nested) visitLane(nested, register);
98
+ };
99
+ for (const lane of swimlanes) visitLane(lane, true);
100
+ // A region's containers are not referenceable from the host.
101
+ for (const region of includes) {
102
+ line(region.box.x, region.box.width, region.box.y);
103
+ line(region.box.x, region.box.width, region.box.y + region.box.height);
104
+ for (const lane of region.nestedSwimlanes) visitLane(lane, false);
105
+ }
106
+ return { obstacles, containers };
107
+ }
108
+
109
+ export function buildDependencies(
110
+ items: Map<string, ItemDeclaration>,
111
+ swimlanes: PositionedSwimlane[],
112
+ includes: PositionedIncludeRegion[],
113
+ ctx: LayoutContext,
114
+ ): PositionedDependencyEdge[] {
115
+ const { obstacles, containers } = collectRouting(swimlanes, includes);
116
+ const bandwidth = ctx.bandScale.bandwidth();
117
+ const style = resolveStyle('item', [], ctx.styleCtx);
118
+ // Each slot keeps the arrow's place in `after:` order, whichever way it
119
+ // is built, so the model lists arrows as the file declares them.
120
+ const slots: Array<PositionedDependencyEdge | DependencyRouteRequest | null> = [];
121
+
122
+ for (const [id, item] of items) {
123
+ const placed = ctx.placedItems.get(id);
124
+ if (!placed) continue;
125
+ const target = itemArrowEnd(placed, bandwidth);
126
+ for (const pred of propValues(item.properties, 'after')) {
127
+ const predItem = ctx.placedItems.get(pred);
128
+ const predContainer = predItem ? undefined : containers.get(pred);
129
+ let source: ArrowEnd | undefined;
130
+ if (predItem) source = itemArrowEnd(predItem, bandwidth);
131
+ else if (predContainer) source = containerArrowEnd(predContainer);
132
+ if (source) {
133
+ slots.push({ fromId: pred, toId: id, source, target });
134
+ continue;
135
+ }
136
+ // An anchor or milestone: its cut line is the stem. An item that
137
+ // starts on it touches the line and needs no arrow; one that
138
+ // starts later gets a dotted stub from the line to its start
139
+ // (specs/true-width-layout.md §3.5).
140
+ const marker = ctx.entityMidpoints.get(pred);
141
+ if (!marker || target.startX - marker.x <= ON_LINE_PX) continue;
142
+ const y = target.rowY ?? target.top;
143
+ slots.push({
144
+ fromId: pred,
145
+ toId: id,
146
+ waypoints: [
147
+ { x: marker.x, y },
148
+ { x: target.paintedX, y },
149
+ ],
150
+ slack: true,
151
+ ...arrowHead(target.bottom - target.top),
152
+ style,
153
+ });
154
+ }
155
+ }
156
+
157
+ const requests = slots.filter((s): s is DependencyRouteRequest => s !== null && 'source' in s);
158
+ const routed = routeDependencies(requests, obstacles, ctx.scale.pixelsPerDay);
159
+ const byRequest = new Map(requests.map((r, i) => [r, routed[i]]));
160
+ const out: PositionedDependencyEdge[] = [];
161
+ for (const slot of slots) {
162
+ if (!slot) continue;
163
+ if (!('source' in slot)) {
164
+ out.push(slot);
165
+ continue;
166
+ }
167
+ const r = byRequest.get(slot);
168
+ if (r) out.push({ ...r, style });
169
+ }
170
+ return out;
171
+ }
@@ -1,60 +1,200 @@
1
- // FootnoteNode + buildFootnotes — footnote area at the bottom of the
2
- // chart. Each footnote gets a stable index (1-based, sorted by id) and
1
+ // FootnoteNode + indexFootnotes / buildFootnotePanel — footnote area at the bottom of the
2
+ // chart. Each footnote gets a stable index (1-based, in document order) and
3
3
  // records its `on:` host references so item / swimlane sequencing can
4
- // emit the matching superscript indicators. The panel grows to fit:
5
- // 28 px header + (entries × FOOTNOTE_ROW_HEIGHT) + 16 px padding.
4
+ // emit the matching superscript indicators. An isolated region's
5
+ // footnotes continue the numbering after the host's, in include order,
6
+ // and share the one panel under a heading row with the region's title.
7
+ // An entry whose text does not fit the panel's width wraps onto more
8
+ // rows at the title's x. The panel grows to fit: 28 px header + (entry
9
+ // rows and region headings × FOOTNOTE_ROW_HEIGHT) + 16 px padding.
6
10
 
7
11
  import type { FootnoteDeclaration } from '@nowline/core';
8
12
  import { propValues } from '../dsl-utils.js';
9
13
  import type { LayoutContext } from '../layout-context.js';
10
14
  import { resolveStyle } from '../style-resolution.js';
15
+ import { estimateTextWidth, wrapText } from '../text-measure.js';
11
16
  import {
12
17
  FOOTNOTE_HEADER_HEIGHT_PX,
13
18
  FOOTNOTE_PANEL_PADDING_PX,
14
19
  FOOTNOTE_ROW_HEIGHT,
15
20
  } from '../themes/shared.js';
16
- import type { BoundingBox, PositionedFootnoteArea, PositionedFootnoteEntry } from '../types.js';
21
+ import type {
22
+ BoundingBox,
23
+ PositionedFootnoteArea,
24
+ PositionedFootnoteEntry,
25
+ PositionedFootnoteLine,
26
+ } from '../types.js';
17
27
 
18
- export interface BuiltFootnotes {
19
- area: PositionedFootnoteArea;
28
+ /** Each footnote's 1-based number (in document order) and its `on:` host ids. */
29
+ export interface FootnoteIndex {
20
30
  index: Map<string, number>;
21
31
  hosts: Map<string, string[]>;
22
32
  }
23
33
 
24
- export function buildFootnotes(
34
+ /**
35
+ * Pure: depends only on the footnote declarations. Numbers start after
36
+ * `precedingCount` (an isolated region continues after the host and the
37
+ * regions before it). The map's order is document order: the resolver
38
+ * inserts a file's own footnotes as declared, then each merged include's.
39
+ */
40
+ export function indexFootnotes(
25
41
  footnotes: Map<string, FootnoteDeclaration>,
26
- ctx: LayoutContext,
27
- chartBottomY: number,
28
- ): BuiltFootnotes {
29
- const entries: PositionedFootnoteEntry[] = [];
42
+ precedingCount = 0,
43
+ ): FootnoteIndex {
30
44
  const index = new Map<string, number>();
31
45
  const hosts = new Map<string, string[]>();
32
- const ordered = [...footnotes.entries()].sort(([a], [b]) => a.localeCompare(b));
33
- ordered.forEach(([id, f], i) => {
34
- const n = i + 1;
35
- index.set(id, n);
46
+ [...footnotes.entries()].forEach(([id, f], i) => {
47
+ index.set(id, precedingCount + i + 1);
36
48
  hosts.set(id, propValues(f.properties, 'on'));
37
- entries.push({
38
- number: n,
39
- title: f.title ?? id,
40
- description: f.description?.text,
41
- style: resolveStyle('footnote', f.properties, ctx.styleCtx),
42
- });
43
49
  });
50
+ return { index, hosts };
51
+ }
52
+
53
+ /** An isolated region's footnotes, numbered and styled by its own context. */
54
+ export interface RegionFootnotes {
55
+ /** The region's title, as on its label tab. */
56
+ title: string;
57
+ footnotes: Map<string, FootnoteDeclaration>;
58
+ ctx: LayoutContext;
59
+ }
60
+
61
+ /** Font sizes (px) of an entry's title and description. */
62
+ const FOOTNOTE_TEXT_FONT_SIZE_PX = 11;
63
+ /** The description column never starts closer than this to the title's x. */
64
+ const FOOTNOTE_DESCRIPTION_MIN_OFFSET_PX = 120;
65
+ /** Per-character advance the description column is placed by. */
66
+ const FOOTNOTE_TITLE_CHAR_ADVANCE_PX = 6;
67
+ const DESCRIPTION_LEAD = '— ';
68
+
69
+ /**
70
+ * The panel below the chart, spanning the final canvas (`chartRightX`):
71
+ * the host's footnotes, then each region's under its title, each in
72
+ * document order. The canvas does not grow for the panel: an entry whose
73
+ * title or description runs past the panel's inner right edge wraps onto
74
+ * more rows, which start at the title's x and use the whole width.
75
+ */
76
+ export function buildFootnotePanel(
77
+ footnotes: Map<string, FootnoteDeclaration>,
78
+ ctx: LayoutContext,
79
+ chartBottomY: number,
80
+ chartRightX: number,
81
+ regions: readonly RegionFootnotes[] = [],
82
+ ): PositionedFootnoteArea {
83
+ const boxX = FOOTNOTE_PANEL_PADDING_PX;
84
+ const boxY = chartBottomY + FOOTNOTE_PANEL_PADDING_PX;
85
+ const boxWidth = chartRightX - 2 * FOOTNOTE_PANEL_PADDING_PX;
86
+ const titleX = boxX + 2 * FOOTNOTE_PANEL_PADDING_PX;
87
+ const textRightX = boxX + boxWidth - FOOTNOTE_PANEL_PADDING_PX;
88
+ const firstRowY = boxY + FOOTNOTE_HEADER_HEIGHT_PX + FOOTNOTE_PANEL_PADDING_PX;
89
+ const entries: PositionedFootnoteEntry[] = [];
90
+ let row = 0;
91
+ const add = (
92
+ decls: Map<string, FootnoteDeclaration>,
93
+ source: LayoutContext,
94
+ region?: string,
95
+ ): void => {
96
+ for (const [id, f] of decls) {
97
+ const title = f.title ?? id;
98
+ const description = f.description?.text;
99
+ const lines = entryLines(title, description, titleX, textRightX);
100
+ const y = firstRowY + row * FOOTNOTE_ROW_HEIGHT;
101
+ const at = (l: { x: number; row: number; text: string }): PositionedFootnoteLine => ({
102
+ x: l.x,
103
+ y: y + l.row * FOOTNOTE_ROW_HEIGHT,
104
+ text: l.text,
105
+ });
106
+ const entry: PositionedFootnoteEntry = {
107
+ number: source.footnoteIndex.get(id) ?? entries.length + 1,
108
+ y,
109
+ title,
110
+ titleLines: lines.title.map(at),
111
+ description,
112
+ style: resolveStyle('footnote', f.properties, source.styleCtx),
113
+ };
114
+ if (lines.description.length > 0) entry.descriptionLines = lines.description.map(at);
115
+ if (region !== undefined) entry.region = region;
116
+ entries.push(entry);
117
+ row += lines.rows;
118
+ }
119
+ };
120
+ add(footnotes, ctx);
121
+ for (const r of regions) {
122
+ if (r.footnotes.size === 0) continue;
123
+ // The region's heading takes a row of its own above its entries.
124
+ const headingY = firstRowY + row * FOOTNOTE_ROW_HEIGHT;
125
+ row += 1;
126
+ const first = entries.length;
127
+ add(r.footnotes, r.ctx, r.title);
128
+ entries[first].regionHeadingY = headingY;
129
+ }
44
130
  const box: BoundingBox = {
45
- x: FOOTNOTE_PANEL_PADDING_PX,
46
- y: chartBottomY + FOOTNOTE_PANEL_PADDING_PX,
47
- width: ctx.chartRightX - 2 * FOOTNOTE_PANEL_PADDING_PX,
131
+ x: boxX,
132
+ y: boxY,
133
+ width: boxWidth,
48
134
  height:
49
135
  entries.length === 0
50
136
  ? 0
51
- : FOOTNOTE_HEADER_HEIGHT_PX +
52
- entries.length * FOOTNOTE_ROW_HEIGHT +
53
- FOOTNOTE_PANEL_PADDING_PX,
137
+ : FOOTNOTE_HEADER_HEIGHT_PX + row * FOOTNOTE_ROW_HEIGHT + FOOTNOTE_PANEL_PADDING_PX,
54
138
  };
139
+ return { box, entries };
140
+ }
141
+
142
+ interface EntryLine {
143
+ x: number;
144
+ /** Rows below the entry's first row. */
145
+ row: number;
146
+ text: string;
147
+ }
148
+
149
+ /**
150
+ * Wrap one entry into rows. The title starts at `titleX`; the description
151
+ * follows on the title's row at the description column (`— ` first) and
152
+ * wraps at `titleX`. A wrapped title, or a description whose first word
153
+ * does not fit beside the title, starts the description on a row of its
154
+ * own. A single word wider than the panel stays whole on its row, as
155
+ * `wrapText` never splits a word.
156
+ */
157
+ function entryLines(
158
+ title: string,
159
+ description: string | undefined,
160
+ titleX: number,
161
+ textRightX: number,
162
+ ): { title: EntryLine[]; description: EntryLine[]; rows: number } {
163
+ const fullWidth = textRightX - titleX;
164
+ const titleRows = fitsOrWraps(title, fullWidth, fullWidth);
165
+ const titleLines = titleRows.map((text, row) => ({ x: titleX, row, text }));
166
+ if (!description) return { title: titleLines, description: [], rows: titleRows.length };
167
+
168
+ const leadWidth = estimateTextWidth(DESCRIPTION_LEAD, FOOTNOTE_TEXT_FONT_SIZE_PX);
169
+ const firstWord = description.trim().split(/\s+/)[0] ?? '';
170
+ const columnX =
171
+ titleX +
172
+ Math.max(FOOTNOTE_DESCRIPTION_MIN_OFFSET_PX, title.length * FOOTNOTE_TITLE_CHAR_ADVANCE_PX);
173
+ const besideTitle =
174
+ titleRows.length === 1 &&
175
+ leadWidth + estimateTextWidth(firstWord, FOOTNOTE_TEXT_FONT_SIZE_PX) <=
176
+ textRightX - columnX;
177
+ const firstX = besideTitle ? columnX : titleX;
178
+ const firstRow = besideTitle ? titleRows.length - 1 : titleRows.length;
179
+ const rows = fitsOrWraps(description, fullWidth, textRightX - firstX - leadWidth);
180
+ const descriptionLines = rows.map((text, i) => ({
181
+ x: i === 0 ? firstX : titleX,
182
+ row: firstRow + i,
183
+ text: i === 0 ? `${DESCRIPTION_LEAD}${text}` : text,
184
+ }));
55
185
  return {
56
- area: { box, entries },
57
- index,
58
- hosts,
186
+ title: titleLines,
187
+ description: descriptionLines,
188
+ rows: firstRow + rows.length,
59
189
  };
60
190
  }
191
+
192
+ /**
193
+ * `text` as is when it fits `firstWidth`, else wrapped (`wrapText`, which
194
+ * collapses whitespace), so an entry that fits keeps its exact text.
195
+ */
196
+ function fitsOrWraps(text: string, width: number, firstWidth: number): string[] {
197
+ if (estimateTextWidth(text, FOOTNOTE_TEXT_FONT_SIZE_PX) <= firstWidth) return [text];
198
+ const rows = wrapText(text, width, FOOTNOTE_TEXT_FONT_SIZE_PX, firstWidth);
199
+ return rows.length > 0 ? rows : [text];
200
+ }