@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
package/src/view-preset.ts
CHANGED
|
@@ -5,17 +5,36 @@
|
|
|
5
5
|
//
|
|
6
6
|
// `resolveScale` parses the DSL `scale:` property (and any nested
|
|
7
7
|
// `scale` block) into a `ViewPreset`. `buildHeaderTicks` produces
|
|
8
|
-
// the `PositionedTick[]
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// the `PositionedTick[]`. Day and week ticks keep the legacy fixed
|
|
9
|
+
// stride (byte-stable with the legacy generator); month, quarter, and
|
|
10
|
+
// year ticks sit on real calendar boundaries, because a fixed day
|
|
11
|
+
// count (30d, 22d, 65d, ...) drifts across month edges and mislabels
|
|
12
|
+
// columns once the span runs long enough. Under a calendar with
|
|
13
|
+
// non-working days (`hide`) every unit takes real boundaries: days,
|
|
14
|
+
// week starts, or unit starts on the working-day axis.
|
|
11
15
|
|
|
12
16
|
import type { NowlineFile, ScaleBlock } from '@nowline/core';
|
|
13
|
-
import {
|
|
17
|
+
import { findProp } from '@nowline/core/internal';
|
|
18
|
+
import { addDays, daysBetween } from './calendar.js';
|
|
14
19
|
import { DEFAULT_LOCALE, localeStrings } from './i18n.js';
|
|
15
|
-
import {
|
|
20
|
+
import { estimateTextWidth } from './text-measure.js';
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_PIXELS_PER_DAY,
|
|
23
|
+
LABEL_THINNING,
|
|
24
|
+
TIMELINE_TICK_LABEL_FONT_SIZE_PX,
|
|
25
|
+
} from './themes/shared.js';
|
|
16
26
|
import type { TimeScale } from './time-scale.js';
|
|
17
|
-
import type {
|
|
18
|
-
|
|
27
|
+
import type {
|
|
28
|
+
PositionedNonWorkingRun,
|
|
29
|
+
PositionedTick,
|
|
30
|
+
PositionedWorkingException,
|
|
31
|
+
} from './types.js';
|
|
32
|
+
import {
|
|
33
|
+
continuousDaysPerUnit,
|
|
34
|
+
type NonWorkingRun,
|
|
35
|
+
type WorkingCalendar,
|
|
36
|
+
type WorkingExceptionRun,
|
|
37
|
+
} from './working-calendar.js';
|
|
19
38
|
|
|
20
39
|
export type ScaleUnit = 'days' | 'weeks' | 'months' | 'quarters' | 'years';
|
|
21
40
|
|
|
@@ -26,6 +45,12 @@ export interface ViewPreset {
|
|
|
26
45
|
labelEvery: number;
|
|
27
46
|
/** Pixels per `1 unit` worth of working days. */
|
|
28
47
|
pixelsPerUnit: number;
|
|
48
|
+
/**
|
|
49
|
+
* Set when `labelEvery` came from the default thinning
|
|
50
|
+
* (`LABEL_THINNING`) rather than a `scale:` literal or `label-every:`.
|
|
51
|
+
* Under `hide` the default days thinning labels week starts instead.
|
|
52
|
+
*/
|
|
53
|
+
labelEveryDefault?: true;
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
// `ScaleConfig` is kept as an alias for source-compat with the few
|
|
@@ -33,12 +58,8 @@ export interface ViewPreset {
|
|
|
33
58
|
// `ViewPreset`.
|
|
34
59
|
export type ScaleConfig = ViewPreset;
|
|
35
60
|
|
|
36
|
-
function stripColon(key: string): string {
|
|
37
|
-
return key.endsWith(':') ? key.slice(0, -1) : key;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
61
|
export function resolveScale(file: NowlineFile, scaleBlock: ScaleBlock | undefined): ViewPreset {
|
|
41
|
-
const scaleProp = file.roadmapDecl?.properties
|
|
62
|
+
const scaleProp = findProp(file.roadmapDecl?.properties ?? [], 'scale');
|
|
42
63
|
// `scale:` accepts a unit name (`days`/`weeks`/`months`/`quarters`/`years`)
|
|
43
64
|
// OR a duration literal (`1w`, `2w`, `1m`, `1q`, `1y`). The literal form
|
|
44
65
|
// is the documented default in the DSL spec; it picks the unit and uses
|
|
@@ -85,25 +106,29 @@ export function resolveScale(file: NowlineFile, scaleBlock: ScaleBlock | undefin
|
|
|
85
106
|
}
|
|
86
107
|
}
|
|
87
108
|
const defaultLabelEvery = labelEveryOverride ?? LABEL_THINNING[unit] ?? 4;
|
|
109
|
+
// Spread into the result only when the thinning is the default one.
|
|
110
|
+
const defaultFlag = (hasLabelEvery: boolean): { labelEveryDefault?: true } =>
|
|
111
|
+
labelEveryOverride === undefined && !hasLabelEvery ? { labelEveryDefault: true } : {};
|
|
88
112
|
|
|
89
113
|
if (scaleBlock) {
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
// Unit and column width come from `scale:` alone; the block only sets display.
|
|
115
|
+
const labelProp = findProp(scaleBlock.properties, 'label-every');
|
|
116
|
+
const explicitLabelEvery = labelProp ? parseInt(labelProp.value, 10) || 0 : 0;
|
|
117
|
+
const labelEvery =
|
|
118
|
+
explicitLabelEvery !== 0 ? Math.max(1, explicitLabelEvery) : defaultLabelEvery;
|
|
119
|
+
return {
|
|
120
|
+
unit,
|
|
121
|
+
labelEvery,
|
|
122
|
+
pixelsPerUnit: pixelsPerUnitOverride ?? unitPx(unit),
|
|
123
|
+
...defaultFlag(explicitLabelEvery !== 0),
|
|
124
|
+
};
|
|
101
125
|
}
|
|
102
126
|
|
|
103
127
|
return {
|
|
104
128
|
unit,
|
|
105
129
|
labelEvery: defaultLabelEvery,
|
|
106
130
|
pixelsPerUnit: pixelsPerUnitOverride ?? unitPx(unit),
|
|
131
|
+
...defaultFlag(false),
|
|
107
132
|
};
|
|
108
133
|
}
|
|
109
134
|
|
|
@@ -124,11 +149,120 @@ function unitPx(unit: ScaleUnit): number {
|
|
|
124
149
|
}
|
|
125
150
|
}
|
|
126
151
|
|
|
152
|
+
type CalendarAlignedUnit = 'months' | 'quarters' | 'years';
|
|
153
|
+
|
|
154
|
+
/** Half a pixel: a column this close to a full unit counts as full. */
|
|
155
|
+
const NARROW_COLUMN_TOLERANCE_PX = 0.5;
|
|
156
|
+
/** Two working-day boundaries closer than this share one grid line. */
|
|
157
|
+
const SAME_X_TOLERANCE_PX = 0.5;
|
|
158
|
+
const DAYS_PER_WEEK = 7;
|
|
159
|
+
|
|
160
|
+
// Months, quarters, and years vary in length (28-31d, 90-92d,
|
|
161
|
+
// 365-366d), so their ticks follow the real calendar instead of the
|
|
162
|
+
// `calendar:` preset's `days-per-*` count. That count is duration
|
|
163
|
+
// arithmetic for `1m` / `1q` / `1y` literals, not the length of a
|
|
164
|
+
// month on the date axis.
|
|
165
|
+
function isCalendarAligned(unit: ScaleUnit): unit is CalendarAlignedUnit {
|
|
166
|
+
return unit === 'months' || unit === 'quarters' || unit === 'years';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** First month / quarter / year start strictly after `date` (UTC). */
|
|
170
|
+
function nextUnitStart(date: Date, unit: CalendarAlignedUnit): Date {
|
|
171
|
+
const year = date.getUTCFullYear();
|
|
172
|
+
const month = date.getUTCMonth();
|
|
173
|
+
switch (unit) {
|
|
174
|
+
case 'months':
|
|
175
|
+
return new Date(Date.UTC(year, month + 1, 1));
|
|
176
|
+
case 'quarters':
|
|
177
|
+
return new Date(Date.UTC(year, month - (month % 3) + 3, 1));
|
|
178
|
+
case 'years':
|
|
179
|
+
return new Date(Date.UTC(year + 1, 0, 1));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isUnitStart(date: Date, unit: CalendarAlignedUnit): boolean {
|
|
184
|
+
if (date.getUTCDate() !== 1) return false;
|
|
185
|
+
const month = date.getUTCMonth();
|
|
186
|
+
switch (unit) {
|
|
187
|
+
case 'months':
|
|
188
|
+
return true;
|
|
189
|
+
case 'quarters':
|
|
190
|
+
return month % 3 === 0;
|
|
191
|
+
case 'years':
|
|
192
|
+
return month === 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Day offset from `start` of the first tick boundary at or after
|
|
198
|
+
* `start + days`. Used to pad the date window so the chart's right
|
|
199
|
+
* edge lands on a column boundary. Day and week units round up to a
|
|
200
|
+
* multiple of the fixed stride (`tickDays`); month, quarter, and year
|
|
201
|
+
* units round up to the next real month / quarter / year start.
|
|
202
|
+
*
|
|
203
|
+
* With a `calendar` that has non-working days, `days` and the result
|
|
204
|
+
* are working-day indices (specs/working-calendar.md §7.2): the content
|
|
205
|
+
* ends before the first date whose index is `ceil(days)`, and the
|
|
206
|
+
* boundary is the first day, week start or unit start at or after it.
|
|
207
|
+
* A zero or negative count pads to the start (index 0) for days and
|
|
208
|
+
* weeks, and to the first unit start at or after the start otherwise.
|
|
209
|
+
*/
|
|
210
|
+
export function tickBoundaryAtOrAfter(
|
|
211
|
+
start: Date,
|
|
212
|
+
days: number,
|
|
213
|
+
unit: ScaleUnit,
|
|
214
|
+
tickDays: number,
|
|
215
|
+
calendar?: WorkingCalendar,
|
|
216
|
+
): number {
|
|
217
|
+
// `days` can arrive from pixel arithmetic (`x / pixelsPerDay`), so
|
|
218
|
+
// absorb float noise before rounding up; otherwise content that ends
|
|
219
|
+
// exactly on a boundary would pad out a whole extra column.
|
|
220
|
+
const count = Math.ceil(days - 1e-6);
|
|
221
|
+
if (calendar?.hasNonWorkingDays) {
|
|
222
|
+
if (!isCalendarAligned(unit) && count <= 0) return 0;
|
|
223
|
+
const contentEnd =
|
|
224
|
+
count <= 0 ? start : addDays(calendar.dateAtWorkingIndex(start, count - 1), 1);
|
|
225
|
+
const boundary = isCalendarAligned(unit)
|
|
226
|
+
? unitStartAtOrAfter(contentEnd, unit)
|
|
227
|
+
: unit === 'weeks'
|
|
228
|
+
? weekStartAtOrAfter(contentEnd, start, calendar.weekStart)
|
|
229
|
+
: contentEnd;
|
|
230
|
+
return calendar.workingIndexOf(start, boundary);
|
|
231
|
+
}
|
|
232
|
+
if (!isCalendarAligned(unit)) return Math.ceil(days / tickDays) * tickDays;
|
|
233
|
+
const target = addDays(start, count);
|
|
234
|
+
return daysBetween(start, unitStartAtOrAfter(target, unit));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function unitStartAtOrAfter(date: Date, unit: CalendarAlignedUnit): Date {
|
|
238
|
+
return isUnitStart(date, unit) ? date : nextUnitStart(date, unit);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The first week start at or after `date`: the calendar's `weekStart`
|
|
243
|
+
* weekday, or, with none, a 7-day stride from the window start.
|
|
244
|
+
*/
|
|
245
|
+
function weekStartAtOrAfter(date: Date, windowStart: Date, weekStart: number | undefined): Date {
|
|
246
|
+
const offset =
|
|
247
|
+
weekStart === undefined ? -daysBetween(windowStart, date) : weekStart - date.getUTCDay();
|
|
248
|
+
return addDays(date, ((offset % DAYS_PER_WEEK) + DAYS_PER_WEEK) % DAYS_PER_WEEK);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The first week start strictly after `date`; see `weekStartAtOrAfter`. */
|
|
252
|
+
function nextWeekStart(date: Date, windowStart: Date, weekStart: number | undefined): Date {
|
|
253
|
+
return weekStartAtOrAfter(addDays(date, 1), windowStart, weekStart);
|
|
254
|
+
}
|
|
255
|
+
|
|
127
256
|
/**
|
|
128
|
-
* Build header ticks for the chart. The
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
257
|
+
* Build header ticks for the chart. The last tick is rendered (so the
|
|
258
|
+
* chart has a closing edge) but its label is suppressed because there's
|
|
259
|
+
* no following column.
|
|
260
|
+
*
|
|
261
|
+
* Day and week ticks use a fixed stride: the ith tick sits at
|
|
262
|
+
* `originX + i * stridePx`. Month, quarter, and year ticks sit on real
|
|
263
|
+
* calendar boundaries; see `buildCalendarAlignedTicks`. When the scale's
|
|
264
|
+
* calendar has non-working days the axis hides them and every unit
|
|
265
|
+
* takes the boundary path; see `buildHiddenDayTicks`.
|
|
132
266
|
*/
|
|
133
267
|
export function buildHeaderTicks(
|
|
134
268
|
scale: TimeScale,
|
|
@@ -136,6 +270,12 @@ export function buildHeaderTicks(
|
|
|
136
270
|
calendar: WorkingCalendar,
|
|
137
271
|
locale: string = DEFAULT_LOCALE,
|
|
138
272
|
): PositionedTick[] {
|
|
273
|
+
if (scale.calendar?.hasNonWorkingDays) {
|
|
274
|
+
return buildHiddenDayTicks(scale, preset, scale.calendar, locale);
|
|
275
|
+
}
|
|
276
|
+
if (isCalendarAligned(preset.unit)) {
|
|
277
|
+
return buildCalendarAlignedTicks(scale, preset, preset.unit, locale);
|
|
278
|
+
}
|
|
139
279
|
const dayPerTick = calendar.daysPerUnit(preset.unit);
|
|
140
280
|
const stridePx = dayPerTick * scale.pixelsPerDay;
|
|
141
281
|
const totalDays = Math.max(1, Math.round(scale.widthPx / scale.pixelsPerDay));
|
|
@@ -159,6 +299,359 @@ export function buildHeaderTicks(
|
|
|
159
299
|
return ticks;
|
|
160
300
|
}
|
|
161
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Ticks under `hide` (specs/working-calendar.md §7.2). Candidate
|
|
304
|
+
* boundaries are the window start plus every day (days), every week start
|
|
305
|
+
* (weeks), or every unit start (months and up), and the window end always
|
|
306
|
+
* closes the axis; x comes from the working-day scale, so a boundary on a
|
|
307
|
+
* hidden day shares the next working day's x. A column of zero width is
|
|
308
|
+
* dropped with its label.
|
|
309
|
+
*
|
|
310
|
+
* Thinning counts the kept columns. Under the default days thinning the
|
|
311
|
+
* majors are the week starts instead (the first tick only when it is
|
|
312
|
+
* one); under `hide` that is the first working day of each week, so a
|
|
313
|
+
* hidden Monday moves its week's label to Tuesday. The closing tick never
|
|
314
|
+
* has a label; its `major` flag follows the same rule, as on the
|
|
315
|
+
* fixed-stride path.
|
|
316
|
+
*
|
|
317
|
+
* At days and weeks a label is dropped only when it is wider than its
|
|
318
|
+
* column and the column is narrower than a full unit (a partial week at
|
|
319
|
+
* either edge); a full business week keeps a label wider than its 40 px.
|
|
320
|
+
* Months and up keep the #92 rule: an edge column drops a label it cannot
|
|
321
|
+
* hold. A dropped label keeps its `labelX`. Inside a run of two or more
|
|
322
|
+
* narrowed columns, a dropped label is restored when it fits its column
|
|
323
|
+
* plus half of each neighbour whose label is absent; see
|
|
324
|
+
* `restoreLabelsInNarrowRuns`.
|
|
325
|
+
*
|
|
326
|
+
* Under `show` the same boundaries apply on the calendar-day scale, so no
|
|
327
|
+
* column has zero width: week ticks still fall on week starts, a full unit
|
|
328
|
+
* counts calendar days, and at months and up a closing column with no
|
|
329
|
+
* working day merges into the one before it.
|
|
330
|
+
*/
|
|
331
|
+
function buildHiddenDayTicks(
|
|
332
|
+
scale: TimeScale,
|
|
333
|
+
preset: ViewPreset,
|
|
334
|
+
calendar: WorkingCalendar,
|
|
335
|
+
locale: string,
|
|
336
|
+
): PositionedTick[] {
|
|
337
|
+
const { unit } = preset;
|
|
338
|
+
const [start, end] = scale.domain;
|
|
339
|
+
const next = (date: Date): Date => {
|
|
340
|
+
if (isCalendarAligned(unit)) return nextUnitStart(date, unit);
|
|
341
|
+
if (unit === 'weeks') return nextWeekStart(date, start, calendar.weekStart);
|
|
342
|
+
return addDays(date, 1);
|
|
343
|
+
};
|
|
344
|
+
const candidates: Date[] = [start];
|
|
345
|
+
for (let d = next(start); d < end; d = next(d)) candidates.push(d);
|
|
346
|
+
|
|
347
|
+
// Keep the boundaries that open a column of positive width.
|
|
348
|
+
const endX = scale.forward(end);
|
|
349
|
+
const kept: Array<{ date: Date; x: number }> = [];
|
|
350
|
+
candidates.forEach((date, i) => {
|
|
351
|
+
const x = scale.forward(date);
|
|
352
|
+
const nextX = i + 1 < candidates.length ? scale.forward(candidates[i + 1]) : endX;
|
|
353
|
+
if (nextX > x && !isShownEmptyClosingColumn(scale, unit, candidates, i)) {
|
|
354
|
+
kept.push({ date, x });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const weekStartMajors =
|
|
359
|
+
unit === 'days' && preset.labelEveryDefault === true && calendar.weekStart !== undefined;
|
|
360
|
+
const isMajor = (date: Date, column: number): boolean =>
|
|
361
|
+
weekStartMajors
|
|
362
|
+
? isFirstVisibleDayOfWeek(date, calendar, scale.showsNonWorking)
|
|
363
|
+
: column % preset.labelEvery === 0;
|
|
364
|
+
// A full unit is its working days under `hide` and its calendar days
|
|
365
|
+
// under `show`, where every day has width.
|
|
366
|
+
const fullUnitDays = scale.showsNonWorking
|
|
367
|
+
? continuousDaysPerUnit(unit)
|
|
368
|
+
: calendar.daysPerUnit(unit);
|
|
369
|
+
const fullUnitPx = fullUnitDays * scale.pixelsPerDay;
|
|
370
|
+
|
|
371
|
+
const columns: TickColumn[] = kept.map(({ date, x }, column) => {
|
|
372
|
+
const columnPx = (column + 1 < kept.length ? kept[column + 1].x : endX) - x;
|
|
373
|
+
const major = isMajor(date, column);
|
|
374
|
+
const label = major ? formatTickLabel(unit, date, column, locale) : undefined;
|
|
375
|
+
const labelPx =
|
|
376
|
+
label === undefined ? 0 : estimateTextWidth(label, TIMELINE_TICK_LABEL_FONT_SIZE_PX);
|
|
377
|
+
const isNarrowed = isCalendarAligned(unit)
|
|
378
|
+
? column === 0 || column === kept.length - 1
|
|
379
|
+
: columnPx < fullUnitPx - NARROW_COLUMN_TOLERANCE_PX;
|
|
380
|
+
const isShown = label !== undefined && !(labelPx > columnPx && isNarrowed);
|
|
381
|
+
return { x, columnPx, major, label, labelPx, isNarrowed, isShown };
|
|
382
|
+
});
|
|
383
|
+
if (!isCalendarAligned(unit)) restoreLabelsInNarrowRuns(columns);
|
|
384
|
+
|
|
385
|
+
const ticks: PositionedTick[] = columns.map((c) => ({
|
|
386
|
+
x: c.x,
|
|
387
|
+
labelX: c.x + c.columnPx / 2,
|
|
388
|
+
major: c.major,
|
|
389
|
+
label: c.isShown ? c.label : undefined,
|
|
390
|
+
}));
|
|
391
|
+
if (end > start) {
|
|
392
|
+
ticks.push({
|
|
393
|
+
x: endX,
|
|
394
|
+
labelX: undefined,
|
|
395
|
+
major: isMajor(end, kept.length),
|
|
396
|
+
label: undefined,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return ticks;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
interface TickColumn {
|
|
403
|
+
x: number;
|
|
404
|
+
columnPx: number;
|
|
405
|
+
major: boolean;
|
|
406
|
+
label: string | undefined;
|
|
407
|
+
labelPx: number;
|
|
408
|
+
isNarrowed: boolean;
|
|
409
|
+
/** False when thinned or dropped by the narrow-column rule. */
|
|
410
|
+
isShown: boolean;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Days and weeks: inside a run of two or more consecutive narrowed
|
|
415
|
+
* columns (specs/working-calendar.md §7.2), a label the narrow-column rule
|
|
416
|
+
* dropped comes back when, centred on its column, it fits the column plus
|
|
417
|
+
* half of each neighbouring column whose label is absent (thinned or
|
|
418
|
+
* dropped). A neighbour with a label, or the chart edge, lends nothing.
|
|
419
|
+
*
|
|
420
|
+
* A bounded recurrence (Summer Fridays) narrows every week in its window,
|
|
421
|
+
* so dropping each label blanks the axis for the whole stretch. Read left
|
|
422
|
+
* to right, the run's labels alternate instead: the first one after a
|
|
423
|
+
* labelled full week stays dropped, the next borrows from it, and so on.
|
|
424
|
+
* A label that needs to borrow cannot sit next to a restored one, so the
|
|
425
|
+
* greedy pass never has to revisit a decision.
|
|
426
|
+
*
|
|
427
|
+
* A lone narrowed column (one holiday week) is not a run and keeps the
|
|
428
|
+
* plain rule. So does a run of two at `label-every` 1: each of its columns
|
|
429
|
+
* has a labelled full week on one side.
|
|
430
|
+
*/
|
|
431
|
+
function restoreLabelsInNarrowRuns(columns: TickColumn[]): void {
|
|
432
|
+
const roomBeside = (j: number): number => {
|
|
433
|
+
const neighbour = columns[j];
|
|
434
|
+
if (neighbour === undefined || neighbour.isShown) return 0;
|
|
435
|
+
return neighbour.columnPx / 2;
|
|
436
|
+
};
|
|
437
|
+
columns.forEach((c, i) => {
|
|
438
|
+
if (c.label === undefined || c.isShown || !c.isNarrowed) return;
|
|
439
|
+
const isInRun = columns[i - 1]?.isNarrowed === true || columns[i + 1]?.isNarrowed === true;
|
|
440
|
+
if (!isInRun) return;
|
|
441
|
+
const halfLabel = c.labelPx / 2;
|
|
442
|
+
const halfColumn = c.columnPx / 2;
|
|
443
|
+
c.isShown =
|
|
444
|
+
halfLabel <= halfColumn + roomBeside(i - 1) &&
|
|
445
|
+
halfLabel <= halfColumn + roomBeside(i + 1);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The days scale's default majors: a week start (the recurring weekend's
|
|
451
|
+
* next day). Under `hide` a hidden week start has no column, so the week's
|
|
452
|
+
* first working day takes its label instead: a Monday holiday, or a
|
|
453
|
+
* recurrence that takes Mondays out for a stretch, would otherwise leave
|
|
454
|
+
* those weeks unlabelled.
|
|
455
|
+
*/
|
|
456
|
+
function isFirstVisibleDayOfWeek(
|
|
457
|
+
date: Date,
|
|
458
|
+
calendar: WorkingCalendar,
|
|
459
|
+
showsNonWorking: boolean,
|
|
460
|
+
): boolean {
|
|
461
|
+
const weekStart = calendar.weekStart as number;
|
|
462
|
+
if (showsNonWorking) return date.getUTCDay() === weekStart;
|
|
463
|
+
const sinceWeekStart = (date.getUTCDay() - weekStart + DAYS_PER_WEEK) % DAYS_PER_WEEK;
|
|
464
|
+
for (let back = 1; back <= sinceWeekStart; back++) {
|
|
465
|
+
if (calendar.isWorkingDay(addDays(date, -back))) return false;
|
|
466
|
+
}
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* True under `show` for the last candidate boundary at months and up when
|
|
472
|
+
* its closing column holds no working day (a Sunday Feb 1 before a window
|
|
473
|
+
* that ends Mon Feb 2). That column is merged into the one before it, as
|
|
474
|
+
* `hide` drops it for having zero width. A leading sliver keeps its tick.
|
|
475
|
+
*/
|
|
476
|
+
function isShownEmptyClosingColumn(
|
|
477
|
+
scale: TimeScale,
|
|
478
|
+
unit: ScaleUnit,
|
|
479
|
+
candidates: ReadonlyArray<Date>,
|
|
480
|
+
i: number,
|
|
481
|
+
): boolean {
|
|
482
|
+
const calendar = scale.calendar;
|
|
483
|
+
if (!scale.showsNonWorking || !calendar || !isCalendarAligned(unit)) return false;
|
|
484
|
+
if (i === 0 || i !== candidates.length - 1) return false;
|
|
485
|
+
const [start, end] = scale.domain;
|
|
486
|
+
return calendar.workingIndexOf(start, candidates[i]) >= calendar.workingIndexOf(start, end);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The window's non-working runs for the model (specs/working-calendar.md
|
|
491
|
+
* §7.5), or undefined when the window holds no non-working day (always
|
|
492
|
+
* the case on the identity path). Under `hide` a run has zero width at the
|
|
493
|
+
* x of the next working day. At the days scale a run is marked `seam` when
|
|
494
|
+
* it lies strictly inside the chart and no grid line falls at its x: no
|
|
495
|
+
* major tick there, and no tick at all when the minor grid is on. Under
|
|
496
|
+
* `show` a run spans its days instead; see `shownNonWorkingRun`.
|
|
497
|
+
*/
|
|
498
|
+
export function buildNonWorkingRuns(
|
|
499
|
+
scale: TimeScale,
|
|
500
|
+
ticks: ReadonlyArray<PositionedTick>,
|
|
501
|
+
unit: ScaleUnit,
|
|
502
|
+
minorGrid: boolean,
|
|
503
|
+
): PositionedNonWorkingRun[] | undefined {
|
|
504
|
+
const calendar = scale.calendar;
|
|
505
|
+
if (!calendar?.hasNonWorkingDays) return undefined;
|
|
506
|
+
const runs = calendar.nonWorkingRuns(scale.domain[0], scale.domain[1]);
|
|
507
|
+
if (runs.length === 0) return undefined;
|
|
508
|
+
const [left, right] = scale.range;
|
|
509
|
+
if (scale.showsNonWorking) return runs.map((run) => shownNonWorkingRun(scale, run, unit));
|
|
510
|
+
const hasGridLineAt = (x: number): boolean =>
|
|
511
|
+
ticks.some((t) => Math.abs(t.x - x) < SAME_X_TOLERANCE_PX && (t.major || minorGrid));
|
|
512
|
+
return runs.map((run) => {
|
|
513
|
+
const x = scale.forward(run.from);
|
|
514
|
+
const out: PositionedNonWorkingRun = {
|
|
515
|
+
x,
|
|
516
|
+
width: scale.forward(addDays(run.through, 1)) - x,
|
|
517
|
+
from: run.from,
|
|
518
|
+
through: run.through,
|
|
519
|
+
};
|
|
520
|
+
if (run.titles.length > 0) {
|
|
521
|
+
out.titles = run.titles;
|
|
522
|
+
out.label = nonWorkingRunLabel(run, calendar.workingWeekdays);
|
|
523
|
+
out.cut = true;
|
|
524
|
+
}
|
|
525
|
+
const isInside = x - left > SAME_X_TOLERANCE_PX && right - x > SAME_X_TOLERANCE_PX;
|
|
526
|
+
if (unit === 'days' && isInside && !hasGridLineAt(x)) out.seam = true;
|
|
527
|
+
return out;
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* The window's working exceptions for the model (specs/working-calendar.md
|
|
533
|
+
* §7.4), or undefined when there are none. Each sits at its first day's x,
|
|
534
|
+
* the same under either view since a working day always has width, clamped
|
|
535
|
+
* to the window for a run that starts before it.
|
|
536
|
+
*/
|
|
537
|
+
export function buildWorkingExceptions(scale: TimeScale): PositionedWorkingException[] | undefined {
|
|
538
|
+
const runs = scale.calendar?.workingExceptionRuns(scale.domain[0], scale.domain[1]) ?? [];
|
|
539
|
+
if (runs.length === 0) return undefined;
|
|
540
|
+
const [left] = scale.range;
|
|
541
|
+
return runs.map((run) => ({
|
|
542
|
+
x: Math.max(left, scale.forward(run.from)),
|
|
543
|
+
from: run.from,
|
|
544
|
+
through: run.through,
|
|
545
|
+
titles: run.titles,
|
|
546
|
+
label: workingExceptionLabel(run),
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* The marker-row label of a working exception: its titles joined with `, `,
|
|
552
|
+
* then ` · +Nd` for the N days it puts back, every day of the run.
|
|
553
|
+
*/
|
|
554
|
+
export function workingExceptionLabel(
|
|
555
|
+
run: Pick<WorkingExceptionRun, 'from' | 'through' | 'titles'>,
|
|
556
|
+
): string {
|
|
557
|
+
const days = daysBetween(run.from, run.through) + 1;
|
|
558
|
+
return `${run.titles.join(', ')}${RUN_LABEL_DAY_SEPARATOR}+${days}d`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Separator between a run's titles and its day count: U+00B7 with a space each side. */
|
|
562
|
+
const RUN_LABEL_DAY_SEPARATOR = ' \u00b7 ';
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* The marker-row label of a titled run (specs/working-calendar.md §7.4):
|
|
566
|
+
* its titles joined with `, `, then ` · Nd`, where N counts the days of
|
|
567
|
+
* `[from, through]` the open-ended week would work, so the closure's cost
|
|
568
|
+
* reads in the DSL's working-day `d`. The suffix is dropped when N is 0.
|
|
569
|
+
*/
|
|
570
|
+
export function nonWorkingRunLabel(
|
|
571
|
+
run: Pick<NonWorkingRun, 'from' | 'through' | 'titles'>,
|
|
572
|
+
workingWeekdays: ReadonlySet<number>,
|
|
573
|
+
): string {
|
|
574
|
+
let workingDays = 0;
|
|
575
|
+
for (let day = run.from; day <= run.through; day = addDays(day, 1)) {
|
|
576
|
+
if (workingWeekdays.has(day.getUTCDay())) workingDays++;
|
|
577
|
+
}
|
|
578
|
+
const text = run.titles.join(', ');
|
|
579
|
+
return workingDays > 0 ? `${text}${RUN_LABEL_DAY_SEPARATOR}${workingDays}d` : text;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* A run under `show` (specs/working-calendar.md §7.3): it spans its days,
|
|
584
|
+
* clamped to the window, and never has a seam. At the days and weeks
|
|
585
|
+
* scales, or when it has titles, it is flagged `band` for the renderer to
|
|
586
|
+
* shade; a plain weekend at months and up stays unpainted.
|
|
587
|
+
*/
|
|
588
|
+
function shownNonWorkingRun(
|
|
589
|
+
scale: TimeScale,
|
|
590
|
+
run: NonWorkingRun,
|
|
591
|
+
unit: ScaleUnit,
|
|
592
|
+
): PositionedNonWorkingRun {
|
|
593
|
+
const [left, right] = scale.range;
|
|
594
|
+
const x = Math.max(left, scale.forward(run.from));
|
|
595
|
+
const rightX = Math.min(right, scale.forward(addDays(run.through, 1)));
|
|
596
|
+
const out: PositionedNonWorkingRun = {
|
|
597
|
+
x,
|
|
598
|
+
width: rightX - x,
|
|
599
|
+
from: run.from,
|
|
600
|
+
through: run.through,
|
|
601
|
+
};
|
|
602
|
+
if (run.titles.length > 0) {
|
|
603
|
+
out.titles = run.titles;
|
|
604
|
+
if (scale.calendar) out.label = nonWorkingRunLabel(run, scale.calendar.workingWeekdays);
|
|
605
|
+
}
|
|
606
|
+
if (unit === 'days' || unit === 'weeks' || run.titles.length > 0) out.band = true;
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Ticks at the chart's left edge, every real month / quarter / year
|
|
612
|
+
* start inside the window, and the chart's right edge. The first and
|
|
613
|
+
* last columns can be partial (a roadmap starting Jan 5 opens with a
|
|
614
|
+
* Jan 5 - Feb 1 column); each column is labelled from its own start
|
|
615
|
+
* date, so a partial column still names the month it sits in.
|
|
616
|
+
* Thinning (`labelEvery`) counts ticks from the left edge, as the
|
|
617
|
+
* fixed-stride generator does.
|
|
618
|
+
*/
|
|
619
|
+
function buildCalendarAlignedTicks(
|
|
620
|
+
scale: TimeScale,
|
|
621
|
+
preset: ViewPreset,
|
|
622
|
+
unit: CalendarAlignedUnit,
|
|
623
|
+
locale: string,
|
|
624
|
+
): PositionedTick[] {
|
|
625
|
+
const [start, end] = scale.domain;
|
|
626
|
+
const dates: Date[] = [start];
|
|
627
|
+
for (let d = nextUnitStart(start, unit); d < end; d = nextUnitStart(d, unit)) {
|
|
628
|
+
dates.push(d);
|
|
629
|
+
}
|
|
630
|
+
if (end > start) dates.push(end);
|
|
631
|
+
const xs = dates.map((d) => scale.forward(d));
|
|
632
|
+
const lastIndex = dates.length - 1;
|
|
633
|
+
return dates.map((date, i) => {
|
|
634
|
+
const x = xs[i];
|
|
635
|
+
const isMajor = i % preset.labelEvery === 0;
|
|
636
|
+
if (i === lastIndex) return { x, labelX: undefined, major: isMajor, label: undefined };
|
|
637
|
+
const columnPx = xs[i + 1] - x;
|
|
638
|
+
let label = isMajor ? formatTickLabel(unit, date, i, locale) : undefined;
|
|
639
|
+
// A partial edge column can be a sliver (a roadmap starting Mar
|
|
640
|
+
// 29, or `length:` ending just past a month start). Its centered
|
|
641
|
+
// label would spill past the chart edge, so drop the label when
|
|
642
|
+
// it doesn't fit; the tick and grid line stay.
|
|
643
|
+
const isEdgeColumn = i === 0 || i === lastIndex - 1;
|
|
644
|
+
if (
|
|
645
|
+
label !== undefined &&
|
|
646
|
+
isEdgeColumn &&
|
|
647
|
+
estimateTextWidth(label, TIMELINE_TICK_LABEL_FONT_SIZE_PX) > columnPx
|
|
648
|
+
) {
|
|
649
|
+
label = undefined;
|
|
650
|
+
}
|
|
651
|
+
return { x, labelX: x + columnPx / 2, major: isMajor, label };
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
162
655
|
function formatTickLabel(unit: ScaleUnit, date: Date, _index: number, locale: string): string {
|
|
163
656
|
switch (unit) {
|
|
164
657
|
case 'days':
|