@diagrammo/dgmo 0.47.0 → 0.48.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/auto.js +1 -1
- package/dist/auto.mjs +1 -1
- package/dist/element.js +1 -1
- package/dist/element.mjs +1 -1
- package/package.json +19 -70
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -5719
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.cjs +0 -526
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -160
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/dist/themes-1-CuKpeH.d.cts +0 -908
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/parser.ts +0 -427
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/layout.ts +0 -2441
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/calculator.ts +0 -979
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/time-ticks.ts +0 -253
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Collapse Engine — polymorphic tree projection (Story 110.1)
|
|
3
|
-
// ============================================================
|
|
4
|
-
//
|
|
5
|
-
// One engine owns the walk · filter · tally phases of collapse for the
|
|
6
|
-
// hierarchical chart types (org, mindmap, sitemap). Each chart type supplies a
|
|
7
|
-
// small `TreeCollapseShape` descriptor; the only genuine divergences across the
|
|
8
|
-
// three — the per-node tally rule and the per-type deep clone — live in the
|
|
9
|
-
// descriptor, not in copied algorithm bodies. Edge re-termination (sitemap)
|
|
10
|
-
// lives in ./tree-edges.
|
|
11
|
-
|
|
12
|
-
/** Per-chart-type descriptor that adapts a node shape to the collapse engine. */
|
|
13
|
-
export interface TreeCollapseShape<N> {
|
|
14
|
-
/** Stable id matched against the collapsed-id set. */
|
|
15
|
-
getId(node: N): string;
|
|
16
|
-
/** A node's children (a readonly view of its live child array). */
|
|
17
|
-
getChildren(node: N): readonly N[];
|
|
18
|
-
/** Deep-clone a node into a mutable copy (children cloned recursively). */
|
|
19
|
-
clone(node: N): N;
|
|
20
|
-
/** Replace a (cloned, mutable) node's children. */
|
|
21
|
-
setChildren(node: N, children: readonly N[]): void;
|
|
22
|
-
/**
|
|
23
|
-
* Whether this node contributes 1 to an ancestor's hidden-descendant tally.
|
|
24
|
-
* org/sitemap exclude structural containers (`!isContainer`); mindmap counts
|
|
25
|
-
* every node. This is the count-rule divergence, surfaced as one predicate.
|
|
26
|
-
*/
|
|
27
|
-
countsAsHidden(node: N): boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface TreeCollapseResult<N> {
|
|
31
|
-
/** Deep-cloned roots with each collapsed node's subtree pruned. */
|
|
32
|
-
roots: N[];
|
|
33
|
-
/** nodeId → count of hidden descendants, computed from the ORIGINAL tree. */
|
|
34
|
-
hiddenCounts: Map<string, number>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function countDescendants<N>(node: N, shape: TreeCollapseShape<N>): number {
|
|
38
|
-
let count = 0;
|
|
39
|
-
for (const child of shape.getChildren(node)) {
|
|
40
|
-
count +=
|
|
41
|
-
(shape.countsAsHidden(child) ? 1 : 0) + countDescendants(child, shape);
|
|
42
|
-
}
|
|
43
|
-
return count;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Tally hidden descendants from the ORIGINAL (unpruned) tree so nested
|
|
47
|
-
* collapses don't lose ancestor totals. */
|
|
48
|
-
function computeHiddenCounts<N>(
|
|
49
|
-
nodes: readonly N[],
|
|
50
|
-
collapsedIds: Set<string>,
|
|
51
|
-
hiddenCounts: Map<string, number>,
|
|
52
|
-
shape: TreeCollapseShape<N>
|
|
53
|
-
): void {
|
|
54
|
-
for (const node of nodes) {
|
|
55
|
-
const children = shape.getChildren(node);
|
|
56
|
-
if (collapsedIds.has(shape.getId(node)) && children.length > 0) {
|
|
57
|
-
hiddenCounts.set(shape.getId(node), countDescendants(node, shape));
|
|
58
|
-
}
|
|
59
|
-
computeHiddenCounts(children, collapsedIds, hiddenCounts, shape);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Drop the children of collapsed nodes on the cloned tree. */
|
|
64
|
-
function pruneCollapsed<N>(
|
|
65
|
-
node: N,
|
|
66
|
-
collapsedIds: Set<string>,
|
|
67
|
-
shape: TreeCollapseShape<N>
|
|
68
|
-
): void {
|
|
69
|
-
for (const child of shape.getChildren(node)) {
|
|
70
|
-
pruneCollapsed(child, collapsedIds, shape);
|
|
71
|
-
}
|
|
72
|
-
if (
|
|
73
|
-
collapsedIds.has(shape.getId(node)) &&
|
|
74
|
-
shape.getChildren(node).length > 0
|
|
75
|
-
) {
|
|
76
|
-
shape.setChildren(node, []);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Collapse a node tree: tally hidden-descendant counts from the original tree,
|
|
82
|
-
* then return deep-cloned roots with each collapsed node's subtree pruned.
|
|
83
|
-
* Never mutates the input.
|
|
84
|
-
*
|
|
85
|
-
* Callers that need reference identity with the original parsed object on an
|
|
86
|
-
* empty collapse set should short-circuit before calling this.
|
|
87
|
-
*/
|
|
88
|
-
export function collapseTree<N>(
|
|
89
|
-
roots: readonly N[],
|
|
90
|
-
collapsedIds: Set<string>,
|
|
91
|
-
shape: TreeCollapseShape<N>
|
|
92
|
-
): TreeCollapseResult<N> {
|
|
93
|
-
const hiddenCounts = new Map<string, number>();
|
|
94
|
-
computeHiddenCounts(roots, collapsedIds, hiddenCounts, shape);
|
|
95
|
-
|
|
96
|
-
const clonedRoots = roots.map((root) => shape.clone(root));
|
|
97
|
-
for (const root of clonedRoots) {
|
|
98
|
-
pruneCollapsed(root, collapsedIds, shape);
|
|
99
|
-
}
|
|
100
|
-
return { roots: clonedRoots, hiddenCounts };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Collect every node id reachable from `roots`. */
|
|
104
|
-
export function collectTreeIds<N>(
|
|
105
|
-
roots: readonly N[],
|
|
106
|
-
shape: Pick<TreeCollapseShape<N>, 'getId' | 'getChildren'>
|
|
107
|
-
): Set<string> {
|
|
108
|
-
const ids = new Set<string>();
|
|
109
|
-
const walk = (nodes: readonly N[]): void => {
|
|
110
|
-
for (const node of nodes) {
|
|
111
|
-
ids.add(shape.getId(node));
|
|
112
|
-
walk(shape.getChildren(node));
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
walk(roots);
|
|
116
|
-
return ids;
|
|
117
|
-
}
|
package/src/utils/d3-helpers.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Shared D3 rendering helpers — Story 109.2 (arch-review).
|
|
3
|
-
//
|
|
4
|
-
// The genuinely-shared primitives the per-visualization renderers and the
|
|
5
|
-
// export-dispatch handlers all reuse: SVG bootstrap (initD3Chart), chart-title
|
|
6
|
-
// rendering, the tooltip element, and the offscreen export-container lifecycle.
|
|
7
|
-
// Extracted verbatim from d3.ts so a venn renderer and a timeline renderer share
|
|
8
|
-
// these helpers without sharing an 8,800-line file.
|
|
9
|
-
// ============================================================
|
|
10
|
-
|
|
11
|
-
import * as d3Selection from 'd3-selection';
|
|
12
|
-
import { FONT_FAMILY } from '../fonts';
|
|
13
|
-
import type { PaletteColors } from '../palettes';
|
|
14
|
-
import { getSeriesColors } from '../palettes';
|
|
15
|
-
import type { D3ExportDimensions } from './d3-types';
|
|
16
|
-
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y } from './title-constants';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Renders a chart title on the SVG with optional click interaction.
|
|
20
|
-
*/
|
|
21
|
-
export function renderChartTitle(
|
|
22
|
-
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
23
|
-
title: string | undefined | null,
|
|
24
|
-
titleLineNumber: number | undefined | null,
|
|
25
|
-
width: number,
|
|
26
|
-
textColor: string,
|
|
27
|
-
onClickItem?: (lineNumber: number) => void
|
|
28
|
-
): void {
|
|
29
|
-
if (!title) return;
|
|
30
|
-
const titleEl = svg
|
|
31
|
-
.append('text')
|
|
32
|
-
.attr('class', 'chart-title')
|
|
33
|
-
.attr('x', width / 2)
|
|
34
|
-
.attr('y', TITLE_Y)
|
|
35
|
-
.attr('text-anchor', 'middle')
|
|
36
|
-
.attr('fill', textColor)
|
|
37
|
-
.attr('font-size', TITLE_FONT_SIZE)
|
|
38
|
-
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
39
|
-
.style('cursor', onClickItem && titleLineNumber ? 'pointer' : 'default')
|
|
40
|
-
.text(title);
|
|
41
|
-
if (titleLineNumber) {
|
|
42
|
-
titleEl.attr('data-line-number', titleLineNumber);
|
|
43
|
-
if (onClickItem) {
|
|
44
|
-
titleEl
|
|
45
|
-
.on('click', () => onClickItem(titleLineNumber))
|
|
46
|
-
.on('mouseenter', function () {
|
|
47
|
-
d3Selection.select(this).attr('opacity', 0.7);
|
|
48
|
-
})
|
|
49
|
-
.on('mouseleave', function () {
|
|
50
|
-
d3Selection.select(this).attr('opacity', 1);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Initializes a D3 chart: clears existing content, creates SVG, resolves palette colors.
|
|
58
|
-
* Returns null if the container has zero dimensions.
|
|
59
|
-
*/
|
|
60
|
-
export function initD3Chart(
|
|
61
|
-
container: HTMLDivElement,
|
|
62
|
-
palette: PaletteColors,
|
|
63
|
-
exportDims?: D3ExportDimensions
|
|
64
|
-
): {
|
|
65
|
-
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>;
|
|
66
|
-
width: number;
|
|
67
|
-
height: number;
|
|
68
|
-
textColor: string;
|
|
69
|
-
mutedColor: string;
|
|
70
|
-
bgColor: string;
|
|
71
|
-
colors: string[];
|
|
72
|
-
} | null {
|
|
73
|
-
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
74
|
-
const width = exportDims?.width ?? container.clientWidth;
|
|
75
|
-
const height = exportDims?.height ?? container.clientHeight;
|
|
76
|
-
if (width <= 0 || height <= 0) return null;
|
|
77
|
-
const textColor = palette.text;
|
|
78
|
-
const mutedColor = palette.border;
|
|
79
|
-
const bgColor = palette.bg;
|
|
80
|
-
const colors = getSeriesColors(palette);
|
|
81
|
-
const svg = d3Selection
|
|
82
|
-
.select(container)
|
|
83
|
-
.append('svg')
|
|
84
|
-
.attr('width', width)
|
|
85
|
-
.attr('height', height)
|
|
86
|
-
.attr('viewBox', `0 0 ${width} ${height}`)
|
|
87
|
-
.style('background', bgColor);
|
|
88
|
-
return { svg, width, height, textColor, mutedColor, bgColor, colors };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Creates (or reuses) the shared hover tooltip element inside a chart container.
|
|
93
|
-
*/
|
|
94
|
-
export function createTooltip(
|
|
95
|
-
container: HTMLElement,
|
|
96
|
-
palette: PaletteColors,
|
|
97
|
-
isDark: boolean
|
|
98
|
-
): HTMLDivElement {
|
|
99
|
-
container.style.position = 'relative';
|
|
100
|
-
|
|
101
|
-
// Reuse existing tooltip element if present (avoids DOM churn on re-renders)
|
|
102
|
-
const existing = container.querySelector<HTMLDivElement>('[data-d3-tooltip]');
|
|
103
|
-
if (existing) {
|
|
104
|
-
existing.style.display = 'none';
|
|
105
|
-
existing.style.background = palette.surface;
|
|
106
|
-
existing.style.color = palette.text;
|
|
107
|
-
existing.style.boxShadow = isDark
|
|
108
|
-
? '0 2px 6px rgba(0,0,0,0.3)'
|
|
109
|
-
: '0 2px 6px rgba(0,0,0,0.12)';
|
|
110
|
-
return existing;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const tip = document.createElement('div');
|
|
114
|
-
tip.setAttribute('data-d3-tooltip', '');
|
|
115
|
-
tip.style.position = 'absolute';
|
|
116
|
-
tip.style.display = 'none';
|
|
117
|
-
tip.style.pointerEvents = 'none';
|
|
118
|
-
tip.style.background = palette.surface;
|
|
119
|
-
tip.style.color = palette.text;
|
|
120
|
-
tip.style.padding = '6px 10px';
|
|
121
|
-
tip.style.borderRadius = '4px';
|
|
122
|
-
tip.style.fontSize = '12px';
|
|
123
|
-
tip.style.lineHeight = '1.4';
|
|
124
|
-
tip.style.whiteSpace = 'nowrap';
|
|
125
|
-
tip.style.zIndex = '10';
|
|
126
|
-
tip.style.boxShadow = isDark
|
|
127
|
-
? '0 2px 6px rgba(0,0,0,0.3)'
|
|
128
|
-
: '0 2px 6px rgba(0,0,0,0.12)';
|
|
129
|
-
container.appendChild(tip);
|
|
130
|
-
return tip;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Positions and shows the shared hover tooltip near the pointer, clamped to its
|
|
135
|
-
* container. Shared by the slope and timeline renderers.
|
|
136
|
-
*/
|
|
137
|
-
export function showTooltip(
|
|
138
|
-
tooltip: HTMLDivElement,
|
|
139
|
-
html: string,
|
|
140
|
-
event: MouseEvent
|
|
141
|
-
): void {
|
|
142
|
-
tooltip.innerHTML = html;
|
|
143
|
-
tooltip.style.display = 'block';
|
|
144
|
-
const container = tooltip.parentElement!;
|
|
145
|
-
const rect = container.getBoundingClientRect();
|
|
146
|
-
let left = event.clientX - rect.left + 12;
|
|
147
|
-
let top = event.clientY - rect.top - 28;
|
|
148
|
-
// Clamp so tooltip stays inside the container
|
|
149
|
-
const tipW = tooltip.offsetWidth;
|
|
150
|
-
const tipH = tooltip.offsetHeight;
|
|
151
|
-
if (left + tipW > rect.width) left = rect.width - tipW - 4;
|
|
152
|
-
if (top < 0) top = event.clientY - rect.top + 16;
|
|
153
|
-
if (top + tipH > rect.height) top = rect.height - tipH - 4;
|
|
154
|
-
tooltip.style.left = `${left}px`;
|
|
155
|
-
tooltip.style.top = `${top}px`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function hideTooltip(tooltip: HTMLDivElement): void {
|
|
159
|
-
tooltip.style.display = 'none';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ============================================================
|
|
163
|
-
// Export-render container lifecycle
|
|
164
|
-
// ============================================================
|
|
165
|
-
|
|
166
|
-
export const EXPORT_WIDTH = 1200;
|
|
167
|
-
export const EXPORT_HEIGHT = 800;
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Resolves the palette for export, falling back to Nord light/dark.
|
|
171
|
-
*/
|
|
172
|
-
export async function resolveExportPalette(
|
|
173
|
-
theme: string,
|
|
174
|
-
palette?: PaletteColors
|
|
175
|
-
): Promise<PaletteColors> {
|
|
176
|
-
if (palette) return palette;
|
|
177
|
-
const { getPalette } = await import('../palettes');
|
|
178
|
-
return theme === 'dark' ? getPalette('nord').dark : getPalette('nord').light;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Creates an offscreen container for export rendering.
|
|
183
|
-
*/
|
|
184
|
-
export function createExportContainer(
|
|
185
|
-
width: number,
|
|
186
|
-
height: number
|
|
187
|
-
): HTMLDivElement {
|
|
188
|
-
const container = document.createElement('div');
|
|
189
|
-
container.style.width = `${width}px`;
|
|
190
|
-
container.style.height = `${height}px`;
|
|
191
|
-
container.style.position = 'absolute';
|
|
192
|
-
container.style.left = '-9999px';
|
|
193
|
-
document.body.appendChild(container);
|
|
194
|
-
return container;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Extracts the SVG from a container, applies common export styling, and cleans up.
|
|
199
|
-
*/
|
|
200
|
-
export function finalizeSvgExport(
|
|
201
|
-
container: HTMLDivElement,
|
|
202
|
-
theme: string,
|
|
203
|
-
palette: PaletteColors
|
|
204
|
-
): string {
|
|
205
|
-
const svgEl = container.querySelector('svg');
|
|
206
|
-
if (!svgEl) return '';
|
|
207
|
-
if (theme === 'transparent') {
|
|
208
|
-
svgEl.style.background = 'none';
|
|
209
|
-
} else if (!svgEl.style.background) {
|
|
210
|
-
svgEl.style.background = palette.bg;
|
|
211
|
-
}
|
|
212
|
-
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
213
|
-
svgEl.style.fontFamily = FONT_FAMILY;
|
|
214
|
-
// Strip elements marked for export exclusion (e.g., inactive legend pills)
|
|
215
|
-
svgEl.querySelectorAll('[data-export-ignore]').forEach((el) => el.remove());
|
|
216
|
-
const svgHtml = svgEl.outerHTML;
|
|
217
|
-
document.body.removeChild(container);
|
|
218
|
-
return svgHtml;
|
|
219
|
-
}
|
package/src/utils/d3-types.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export interface D3ExportDimensions {
|
|
2
|
-
width?: number;
|
|
3
|
-
height?: number;
|
|
4
|
-
/** Map-only: when true, the map renderer suppresses its global stretch-fill and
|
|
5
|
-
* contain-fits (letterbox) instead. Set by `mapExportDimensions` when the export
|
|
6
|
-
* canvas was clamped/floored away from the map's content aspect, so the
|
|
7
|
-
* off-aspect canvas doesn't re-distort. Ignored by all non-map renderers. */
|
|
8
|
-
preferContain?: boolean;
|
|
9
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Description Helpers — shared utilities for node descriptions
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Try to strip a leading `description:` keyword from a line.
|
|
7
|
-
* Matches only the colon form: `description: text`.
|
|
8
|
-
* The bare form (`description text`) returns `needsColon: true`
|
|
9
|
-
* so callers can emit a diagnostic.
|
|
10
|
-
*/
|
|
11
|
-
export function tryStripDescriptionKeyword(line: string): {
|
|
12
|
-
isKeyword: boolean;
|
|
13
|
-
needsColon?: boolean;
|
|
14
|
-
text: string;
|
|
15
|
-
} {
|
|
16
|
-
const colonMatch = line.match(/^description\s*:\s+(.+)$/i);
|
|
17
|
-
if (colonMatch) return { isKeyword: true, text: colonMatch[1]! };
|
|
18
|
-
const bareMatch = line.match(/^description\s+(.+)$/i);
|
|
19
|
-
if (bareMatch)
|
|
20
|
-
return { isKeyword: true, needsColon: true, text: bareMatch[1]! };
|
|
21
|
-
return { isKeyword: false, text: line };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Pre-process a single description line:
|
|
26
|
-
* - `- text` → `• text` (bullet)
|
|
27
|
-
* - `http example.com` → `https://example.com` (bare URL normalization)
|
|
28
|
-
*/
|
|
29
|
-
export function preprocessDescriptionLine(line: string): string {
|
|
30
|
-
// Bullet transform
|
|
31
|
-
if (line.startsWith('- ')) line = '\u2022 ' + line.slice(2);
|
|
32
|
-
// Bare URL normalization
|
|
33
|
-
line = line.replace(
|
|
34
|
-
/\bhttps?\s+([\w][\w.-]+\.[a-z]{2,}(?:\/\S*)?)/gi,
|
|
35
|
-
(_, domain) => `https://${domain}`
|
|
36
|
-
);
|
|
37
|
-
return line;
|
|
38
|
-
}
|
package/src/utils/duration.ts
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Duration & Business Day Arithmetic
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
Duration,
|
|
7
|
-
DurationUnit,
|
|
8
|
-
GanttHolidays,
|
|
9
|
-
Offset,
|
|
10
|
-
Weekday,
|
|
11
|
-
} from '../gantt/types';
|
|
12
|
-
|
|
13
|
-
// ── Weekday constants ─────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
/** JS Date.getDay() → Weekday mapping (0=Sun, 1=Mon, ..., 6=Sat) */
|
|
16
|
-
const JS_DAY_TO_WEEKDAY: Weekday[] = [
|
|
17
|
-
'sun',
|
|
18
|
-
'mon',
|
|
19
|
-
'tue',
|
|
20
|
-
'wed',
|
|
21
|
-
'thu',
|
|
22
|
-
'fri',
|
|
23
|
-
'sat',
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Check if a date is a workday (not a weekend and not a holiday).
|
|
28
|
-
*/
|
|
29
|
-
export function isWorkday(
|
|
30
|
-
date: Date,
|
|
31
|
-
workweek: readonly Weekday[],
|
|
32
|
-
holidaySet: Set<string>
|
|
33
|
-
): boolean {
|
|
34
|
-
// date.getDay() returns 0..6; JS_DAY_TO_WEEKDAY is length 7.
|
|
35
|
-
const dayName = JS_DAY_TO_WEEKDAY[date.getDay()]!;
|
|
36
|
-
if (!workweek.includes(dayName)) return false;
|
|
37
|
-
if (holidaySet.has(formatDateKey(date))) return false;
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Format a Date as YYYY-MM-DD for holiday set lookups.
|
|
43
|
-
*/
|
|
44
|
-
export function formatDateKey(date: Date): string {
|
|
45
|
-
const y = date.getFullYear();
|
|
46
|
-
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
47
|
-
const d = String(date.getDate()).padStart(2, '0');
|
|
48
|
-
return `${y}-${m}-${d}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Build a Set of holiday date strings for efficient lookup.
|
|
53
|
-
* Expands date ranges into individual dates.
|
|
54
|
-
*/
|
|
55
|
-
export function buildHolidaySet(holidays: GanttHolidays): Set<string> {
|
|
56
|
-
const set = new Set<string>();
|
|
57
|
-
|
|
58
|
-
for (const h of holidays.dates) {
|
|
59
|
-
set.add(h.date);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
for (const range of holidays.ranges) {
|
|
63
|
-
const start = new Date(range.startDate + 'T00:00:00');
|
|
64
|
-
const end = new Date(range.endDate + 'T00:00:00');
|
|
65
|
-
const current = new Date(start);
|
|
66
|
-
while (current <= end) {
|
|
67
|
-
set.add(formatDateKey(current));
|
|
68
|
-
current.setDate(current.getDate() + 1);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return set;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Add business days to a start date, skipping weekends and holidays.
|
|
77
|
-
*
|
|
78
|
-
* For fractional business days, rounds to the nearest whole day first.
|
|
79
|
-
* Handles both positive amounts (forward) and zero (returns start date).
|
|
80
|
-
*/
|
|
81
|
-
export function addBusinessDays(
|
|
82
|
-
startDate: Date,
|
|
83
|
-
count: number,
|
|
84
|
-
workweek: readonly Weekday[],
|
|
85
|
-
holidaySet: Set<string>,
|
|
86
|
-
direction: 1 | -1 = 1
|
|
87
|
-
): Date {
|
|
88
|
-
const days = Math.round(Math.abs(count));
|
|
89
|
-
if (days === 0) return new Date(startDate);
|
|
90
|
-
|
|
91
|
-
// Guard: an invalid start date would produce an infinite loop because
|
|
92
|
-
// isWorkday() always returns false for NaN day-of-week. This happens
|
|
93
|
-
// mid-edit when the user is typing a partial date like `start 2026-`.
|
|
94
|
-
if (Number.isNaN(startDate.getTime())) return new Date(startDate);
|
|
95
|
-
|
|
96
|
-
// Guard: a workweek with no workdays (or fully blocked by holidays) would
|
|
97
|
-
// also loop forever. Bound the search at days * 14 calendar days, which
|
|
98
|
-
// covers up to 2 weeks of skipped days per business day requested.
|
|
99
|
-
const current = new Date(startDate);
|
|
100
|
-
let remaining = days;
|
|
101
|
-
let safety = days * 14 + 14;
|
|
102
|
-
|
|
103
|
-
while (remaining > 0 && safety-- > 0) {
|
|
104
|
-
current.setDate(current.getDate() + direction);
|
|
105
|
-
if (isWorkday(current, workweek, holidaySet)) {
|
|
106
|
-
remaining--;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return current;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Add a gantt duration to a start date, producing an end date.
|
|
115
|
-
*
|
|
116
|
-
* Calendar units (d, w, m, q, y) ignore holidays.
|
|
117
|
-
* Business day units (bd) skip weekends and holidays.
|
|
118
|
-
*/
|
|
119
|
-
export function addGanttDuration(
|
|
120
|
-
startDate: Date,
|
|
121
|
-
duration: Duration,
|
|
122
|
-
holidays: GanttHolidays,
|
|
123
|
-
holidaySet: Set<string>,
|
|
124
|
-
direction: 1 | -1 = 1,
|
|
125
|
-
opts?: { sprintLength?: Duration }
|
|
126
|
-
): Date {
|
|
127
|
-
const { amount, unit } = duration;
|
|
128
|
-
|
|
129
|
-
switch (unit) {
|
|
130
|
-
case 'bd':
|
|
131
|
-
return addBusinessDays(
|
|
132
|
-
startDate,
|
|
133
|
-
amount,
|
|
134
|
-
holidays.workweek,
|
|
135
|
-
holidaySet,
|
|
136
|
-
direction
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
case 'd': {
|
|
140
|
-
const result = new Date(startDate);
|
|
141
|
-
result.setDate(result.getDate() + Math.round(amount) * direction);
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
case 'w': {
|
|
146
|
-
const result = new Date(startDate);
|
|
147
|
-
result.setDate(result.getDate() + Math.round(amount * 7) * direction);
|
|
148
|
-
return result;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
case 'm': {
|
|
152
|
-
const result = new Date(startDate);
|
|
153
|
-
const wholeMonths =
|
|
154
|
-
direction === -1 ? Math.round(amount) : Math.floor(amount);
|
|
155
|
-
const fractionalDays =
|
|
156
|
-
direction === -1 ? 0 : Math.round((amount - wholeMonths) * 30);
|
|
157
|
-
result.setMonth(result.getMonth() + wholeMonths * direction);
|
|
158
|
-
if (fractionalDays > 0) {
|
|
159
|
-
result.setDate(result.getDate() + fractionalDays * direction);
|
|
160
|
-
}
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
case 'q': {
|
|
165
|
-
const result = new Date(startDate);
|
|
166
|
-
const totalMonths = amount * 3;
|
|
167
|
-
const wholeMonths =
|
|
168
|
-
direction === -1 ? Math.round(totalMonths) : Math.floor(totalMonths);
|
|
169
|
-
const fractionalDays =
|
|
170
|
-
direction === -1 ? 0 : Math.round((totalMonths - wholeMonths) * 30);
|
|
171
|
-
result.setMonth(result.getMonth() + wholeMonths * direction);
|
|
172
|
-
if (fractionalDays > 0) {
|
|
173
|
-
result.setDate(result.getDate() + fractionalDays * direction);
|
|
174
|
-
}
|
|
175
|
-
return result;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
case 'y': {
|
|
179
|
-
const result = new Date(startDate);
|
|
180
|
-
const wholeYears =
|
|
181
|
-
direction === -1 ? Math.round(amount) : Math.floor(amount);
|
|
182
|
-
const fractionalMonths =
|
|
183
|
-
direction === -1 ? 0 : Math.round((amount - wholeYears) * 12);
|
|
184
|
-
result.setFullYear(result.getFullYear() + wholeYears * direction);
|
|
185
|
-
if (fractionalMonths > 0) {
|
|
186
|
-
result.setMonth(result.getMonth() + fractionalMonths * direction);
|
|
187
|
-
}
|
|
188
|
-
return result;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
case 'h': {
|
|
192
|
-
const result = new Date(startDate);
|
|
193
|
-
result.setTime(result.getTime() + amount * 3600000 * direction);
|
|
194
|
-
return result;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
case 'min': {
|
|
198
|
-
const result = new Date(startDate);
|
|
199
|
-
result.setTime(result.getTime() + amount * 60000 * direction);
|
|
200
|
-
return result;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
case 's': {
|
|
204
|
-
if (!opts?.sprintLength) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
'Sprint duration unit "s" requires sprintLength configuration'
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
const sl = opts.sprintLength;
|
|
210
|
-
const totalDays = amount * sl.amount * (sl.unit === 'w' ? 7 : 1);
|
|
211
|
-
const result = new Date(startDate);
|
|
212
|
-
result.setDate(result.getDate() + Math.round(totalDays) * direction);
|
|
213
|
-
return result;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Parse a duration string like "3bd" or "5d".
|
|
220
|
-
*/
|
|
221
|
-
export function parseDuration(s: string): Duration | null {
|
|
222
|
-
const match = s.trim().match(/^(\d+(?:\.\d+)?)(min|bd|d|w|m|q|y|h|s)$/);
|
|
223
|
-
if (!match) return null;
|
|
224
|
-
return { amount: parseFloat(match[1]!), unit: match[2] as DurationUnit };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Parse an offset string like "5bd", "-3bd", or "0d".
|
|
229
|
-
* Returns null if the format is invalid.
|
|
230
|
-
* Explicit '+' prefix (e.g. "+5bd") returns null — caller should warn.
|
|
231
|
-
*/
|
|
232
|
-
export function parseOffset(value: string): Offset | null {
|
|
233
|
-
const trimmed = value.trim();
|
|
234
|
-
let direction: 1 | -1 = 1;
|
|
235
|
-
let remainder = trimmed;
|
|
236
|
-
|
|
237
|
-
if (trimmed.startsWith('-')) {
|
|
238
|
-
direction = -1;
|
|
239
|
-
remainder = trimmed.slice(1);
|
|
240
|
-
} else if (trimmed.startsWith('+')) {
|
|
241
|
-
return null; // explicit + is not supported
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const duration = parseDuration(remainder);
|
|
245
|
-
if (!duration) return null;
|
|
246
|
-
return { duration, direction };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Parse an offset prefix like "+4w", "+10bd", "-3d".
|
|
251
|
-
* Unlike parseOffset(), this accepts explicit '+' prefix (used by new gantt syntax).
|
|
252
|
-
*/
|
|
253
|
-
export function parseOffsetPrefix(value: string): Offset | null {
|
|
254
|
-
const trimmed = value.trim();
|
|
255
|
-
let direction: 1 | -1 = 1;
|
|
256
|
-
let remainder = trimmed;
|
|
257
|
-
|
|
258
|
-
if (trimmed.startsWith('+')) {
|
|
259
|
-
remainder = trimmed.slice(1);
|
|
260
|
-
} else if (trimmed.startsWith('-')) {
|
|
261
|
-
direction = -1;
|
|
262
|
-
remainder = trimmed.slice(1);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const duration = parseDuration(remainder);
|
|
266
|
-
if (!duration) return null;
|
|
267
|
-
return { duration, direction };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Parse a date string (YYYY-MM-DD, YYYY-MM, YYYY, or YYYY-MM-DD HH:MM) into a Date object.
|
|
272
|
-
* Returns midnight local time unless HH:MM is specified.
|
|
273
|
-
*/
|
|
274
|
-
export function parseGanttDate(s: string): Date {
|
|
275
|
-
// Split on space to detect optional time component
|
|
276
|
-
const spaceIdx = s.indexOf(' ');
|
|
277
|
-
let datePart = s;
|
|
278
|
-
let hour = 0;
|
|
279
|
-
let minute = 0;
|
|
280
|
-
|
|
281
|
-
if (spaceIdx !== -1) {
|
|
282
|
-
datePart = s.slice(0, spaceIdx);
|
|
283
|
-
const timePart = s.slice(spaceIdx + 1);
|
|
284
|
-
const timeParts = timePart.split(':');
|
|
285
|
-
if (timeParts.length === 2) {
|
|
286
|
-
const h = parseInt(timeParts[0]!, 10);
|
|
287
|
-
const m = parseInt(timeParts[1]!, 10);
|
|
288
|
-
if (h >= 0 && h <= 23 && m >= 0 && m <= 59) {
|
|
289
|
-
hour = h;
|
|
290
|
-
minute = m;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const parts = datePart.split('-').map((p) => parseInt(p, 10));
|
|
296
|
-
// split() always returns ≥1 element, so parts[0] is defined.
|
|
297
|
-
const year = parts[0]!;
|
|
298
|
-
const month = parts.length >= 2 ? parts[1]! - 1 : 0; // JS months are 0-based
|
|
299
|
-
const day = parts.length >= 3 ? parts[2]! : 1;
|
|
300
|
-
return new Date(year, month, day, hour, minute);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Format a Date as YYYY-MM-DD string, or YYYY-MM-DD HH:MM if time is non-midnight.
|
|
305
|
-
*/
|
|
306
|
-
export function formatGanttDate(date: Date): string {
|
|
307
|
-
const dateStr = formatDateKey(date);
|
|
308
|
-
const h = date.getHours();
|
|
309
|
-
const m = date.getMinutes();
|
|
310
|
-
if (h === 0 && m === 0) return dateStr;
|
|
311
|
-
return `${dateStr} ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
312
|
-
}
|