@diagrammo/dgmo 0.4.2 → 0.4.3
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/.claude/skills/dgmo-chart/SKILL.md +28 -0
- package/.claude/skills/dgmo-generate/SKILL.md +1 -0
- package/.claude/skills/dgmo-sequence/SKILL.md +24 -1
- package/.cursorrules +27 -2
- package/.github/copilot-instructions.md +36 -3
- package/.windsurfrules +27 -2
- package/README.md +12 -3
- package/dist/cli.cjs +197 -154
- package/dist/index.cjs +8371 -3200
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +502 -58
- package/dist/index.d.ts +502 -58
- package/dist/index.js +8594 -3444
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +336 -17
- package/docs/migration-sequence-color-to-tags.md +98 -0
- package/package.json +1 -1
- package/src/c4/renderer.ts +1 -20
- package/src/class/renderer.ts +1 -11
- package/src/cli.ts +40 -0
- package/src/d3.ts +92 -2
- package/src/dgmo-router.ts +11 -0
- package/src/echarts.ts +74 -8
- package/src/er/parser.ts +29 -3
- package/src/er/renderer.ts +1 -15
- package/src/graph/flowchart-parser.ts +7 -30
- package/src/graph/flowchart-renderer.ts +62 -69
- package/src/graph/layout.ts +5 -0
- package/src/graph/state-parser.ts +388 -0
- package/src/graph/state-renderer.ts +496 -0
- package/src/graph/types.ts +4 -2
- package/src/index.ts +42 -1
- package/src/infra/compute.ts +1113 -0
- package/src/infra/layout.ts +575 -0
- package/src/infra/parser.ts +559 -0
- package/src/infra/renderer.ts +1509 -0
- package/src/infra/roles.ts +60 -0
- package/src/infra/serialize.ts +67 -0
- package/src/infra/types.ts +221 -0
- package/src/infra/validation.ts +192 -0
- package/src/initiative-status/layout.ts +56 -61
- package/src/initiative-status/renderer.ts +13 -13
- package/src/kanban/renderer.ts +1 -24
- package/src/org/layout.ts +28 -37
- package/src/org/parser.ts +16 -1
- package/src/org/renderer.ts +159 -121
- package/src/org/resolver.ts +90 -23
- package/src/palettes/color-utils.ts +30 -0
- package/src/render.ts +2 -0
- package/src/sequence/parser.ts +202 -42
- package/src/sequence/renderer.ts +576 -113
- package/src/sequence/tag-resolution.ts +163 -0
- package/src/sitemap/collapse.ts +187 -0
- package/src/sitemap/layout.ts +738 -0
- package/src/sitemap/parser.ts +489 -0
- package/src/sitemap/renderer.ts +774 -0
- package/src/sitemap/types.ts +42 -0
- package/src/utils/tag-groups.ts +119 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// ============================================================
|
|
2
|
-
// Initiative Status Diagram — Layout
|
|
2
|
+
// Initiative Status Diagram — Layout
|
|
3
|
+
//
|
|
4
|
+
// Uses dagre for rank assignment, node ordering, and edge
|
|
5
|
+
// routing. Edge waypoints are taken directly from dagre's
|
|
6
|
+
// output without modification.
|
|
3
7
|
// ============================================================
|
|
4
8
|
|
|
5
9
|
import dagre from '@dagrejs/dagre';
|
|
6
|
-
import type { ParsedInitiativeStatus,
|
|
10
|
+
import type { ParsedInitiativeStatus, InitiativeStatus } from './types';
|
|
7
11
|
|
|
8
12
|
export interface ISLayoutNode {
|
|
9
13
|
label: string;
|
|
@@ -43,8 +47,6 @@ export interface ISLayoutResult {
|
|
|
43
47
|
height: number;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
// Roll up child statuses: worst (least-progressed) wins
|
|
47
|
-
// Priority: todo > wip > done > na > null
|
|
48
50
|
const STATUS_PRIORITY: Record<string, number> = { todo: 3, wip: 2, done: 1, na: 0 };
|
|
49
51
|
|
|
50
52
|
function rollUpStatus(members: ISLayoutNode[]): InitiativeStatus {
|
|
@@ -60,60 +62,48 @@ function rollUpStatus(members: ISLayoutNode[]): InitiativeStatus {
|
|
|
60
62
|
return worst;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
// Golden ratio fixed-size nodes — all boxes are identical dimensions
|
|
64
65
|
const PHI = 1.618;
|
|
65
66
|
const NODE_HEIGHT = 60;
|
|
66
|
-
const NODE_WIDTH = Math.round(NODE_HEIGHT * PHI);
|
|
67
|
+
const NODE_WIDTH = Math.round(NODE_HEIGHT * PHI);
|
|
67
68
|
const GROUP_PADDING = 20;
|
|
69
|
+
const NODESEP = 80;
|
|
70
|
+
const RANKSEP = 160;
|
|
71
|
+
|
|
72
|
+
// ============================================================
|
|
73
|
+
// Main layout function
|
|
74
|
+
// ============================================================
|
|
68
75
|
|
|
69
76
|
export function layoutInitiativeStatus(parsed: ParsedInitiativeStatus): ISLayoutResult {
|
|
70
77
|
if (parsed.nodes.length === 0) {
|
|
71
78
|
return { nodes: [], edges: [], groups: [], width: 0, height: 0 };
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
// Build and run dagre graph
|
|
74
82
|
const hasGroups = parsed.groups.length > 0;
|
|
75
83
|
const g = new dagre.graphlib.Graph({ multigraph: true, compound: hasGroups });
|
|
76
|
-
g.setGraph({
|
|
77
|
-
rankdir: 'LR',
|
|
78
|
-
nodesep: 80,
|
|
79
|
-
ranksep: 160,
|
|
80
|
-
edgesep: 40,
|
|
81
|
-
});
|
|
84
|
+
g.setGraph({ rankdir: 'LR', nodesep: NODESEP, ranksep: RANKSEP });
|
|
82
85
|
g.setDefaultEdgeLabel(() => ({}));
|
|
83
86
|
|
|
84
|
-
// Add group parent nodes
|
|
85
87
|
for (const group of parsed.groups) {
|
|
86
|
-
|
|
87
|
-
g.setNode(groupId, { label: group.label, clusterLabelPos: 'top' });
|
|
88
|
+
g.setNode(`__group_${group.label}`, { label: group.label, clusterLabelPos: 'top' });
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
-
// Add nodes — all same size (golden ratio)
|
|
91
90
|
for (const node of parsed.nodes) {
|
|
92
91
|
g.setNode(node.label, { label: node.label, width: NODE_WIDTH, height: NODE_HEIGHT });
|
|
93
92
|
}
|
|
94
|
-
|
|
95
|
-
// Assign children to group parents
|
|
96
93
|
for (const group of parsed.groups) {
|
|
97
94
|
const groupId = `__group_${group.label}`;
|
|
98
95
|
for (const nodeLabel of group.nodeLabels) {
|
|
99
|
-
if (g.hasNode(nodeLabel))
|
|
100
|
-
g.setParent(nodeLabel, groupId);
|
|
101
|
-
}
|
|
96
|
+
if (g.hasNode(nodeLabel)) g.setParent(nodeLabel, groupId);
|
|
102
97
|
}
|
|
103
98
|
}
|
|
104
|
-
|
|
105
|
-
// Add edges — use multigraph names to allow duplicates between same pair
|
|
106
99
|
for (let i = 0; i < parsed.edges.length; i++) {
|
|
107
100
|
const edge = parsed.edges[i];
|
|
108
101
|
g.setEdge(edge.source, edge.target, { label: edge.label ?? '' }, `e${i}`);
|
|
109
102
|
}
|
|
110
103
|
|
|
111
|
-
// Compute layout
|
|
112
104
|
dagre.layout(g);
|
|
113
105
|
|
|
114
|
-
// Extract
|
|
115
|
-
// (crossing minimization). We don't reorder post-layout because
|
|
116
|
-
// that would desync edge waypoints from node positions.
|
|
106
|
+
// Extract node positions
|
|
117
107
|
const layoutNodes: ISLayoutNode[] = parsed.nodes.map((node) => {
|
|
118
108
|
const pos = g.node(node.label);
|
|
119
109
|
return {
|
|
@@ -128,35 +118,51 @@ export function layoutInitiativeStatus(parsed: ParsedInitiativeStatus): ISLayout
|
|
|
128
118
|
};
|
|
129
119
|
});
|
|
130
120
|
|
|
131
|
-
|
|
121
|
+
const nodeMap = new Map(layoutNodes.map((n) => [n.label, n]));
|
|
122
|
+
const allNodeX = layoutNodes.map((n) => n.x);
|
|
123
|
+
|
|
124
|
+
// Adjacent-rank edges: 4-point elbow (perpendicular exit/entry, no crossings).
|
|
125
|
+
// Multi-rank edges: dagre's interior waypoints for obstacle avoidance, with
|
|
126
|
+
// first/last points pinned to exact node boundaries at node-center Y.
|
|
132
127
|
const layoutEdges: ISLayoutEdge[] = parsed.edges.map((edge, i) => {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
const src = nodeMap.get(edge.source)!;
|
|
129
|
+
const tgt = nodeMap.get(edge.target)!;
|
|
130
|
+
const exitX = src.x + src.width / 2;
|
|
131
|
+
const enterX = tgt.x - tgt.width / 2;
|
|
132
|
+
const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
|
|
133
|
+
const dagrePoints: { x: number; y: number }[] = dagreEdge?.points ?? [];
|
|
134
|
+
const hasIntermediateRank = allNodeX.some((x) => x > src.x + 20 && x < tgt.x - 20);
|
|
135
|
+
const step = Math.min((enterX - exitX) * 0.15, 20);
|
|
136
|
+
|
|
137
|
+
const fixedDagrePoints = dagrePoints.length >= 2 ? [
|
|
138
|
+
{ x: exitX, y: src.y },
|
|
139
|
+
...dagrePoints.slice(1, -1),
|
|
140
|
+
{ x: enterX, y: tgt.y },
|
|
141
|
+
] : dagrePoints;
|
|
142
|
+
|
|
143
|
+
const points = (tgt.x > src.x && !hasIntermediateRank)
|
|
144
|
+
? [
|
|
145
|
+
{ x: exitX, y: src.y },
|
|
146
|
+
{ x: exitX + step, y: src.y },
|
|
147
|
+
{ x: enterX - step, y: tgt.y },
|
|
148
|
+
{ x: enterX, y: tgt.y },
|
|
149
|
+
]
|
|
150
|
+
: fixedDagrePoints;
|
|
151
|
+
return { source: edge.source, target: edge.target, label: edge.label,
|
|
152
|
+
status: edge.status, lineNumber: edge.lineNumber, points };
|
|
142
153
|
});
|
|
143
154
|
|
|
144
|
-
// Compute group bounding boxes
|
|
155
|
+
// Compute group bounding boxes
|
|
145
156
|
const layoutGroups: ISLayoutGroup[] = [];
|
|
146
157
|
if (parsed.groups.length > 0) {
|
|
147
|
-
const
|
|
158
|
+
const nMap = new Map(layoutNodes.map((n) => [n.label, n]));
|
|
148
159
|
for (const group of parsed.groups) {
|
|
149
160
|
const members = group.nodeLabels
|
|
150
|
-
.map((label) =>
|
|
161
|
+
.map((label) => nMap.get(label))
|
|
151
162
|
.filter((n): n is ISLayoutNode => n !== undefined);
|
|
152
|
-
|
|
153
163
|
if (members.length === 0) continue;
|
|
154
164
|
|
|
155
|
-
let minX = Infinity;
|
|
156
|
-
let minY = Infinity;
|
|
157
|
-
let maxX = -Infinity;
|
|
158
|
-
let maxY = -Infinity;
|
|
159
|
-
|
|
165
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
160
166
|
for (const member of members) {
|
|
161
167
|
const left = member.x - member.width / 2;
|
|
162
168
|
const right = member.x + member.width / 2;
|
|
@@ -189,29 +195,18 @@ export function layoutInitiativeStatus(parsed: ParsedInitiativeStatus): ISLayout
|
|
|
189
195
|
if (right > totalWidth) totalWidth = right;
|
|
190
196
|
if (bottom > totalHeight) totalHeight = bottom;
|
|
191
197
|
}
|
|
192
|
-
// Also consider group bounds
|
|
193
198
|
for (const group of layoutGroups) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (right > totalWidth) totalWidth = right;
|
|
197
|
-
if (bottom > totalHeight) totalHeight = bottom;
|
|
199
|
+
if (group.x + group.width > totalWidth) totalWidth = group.x + group.width;
|
|
200
|
+
if (group.y + group.height > totalHeight) totalHeight = group.y + group.height;
|
|
198
201
|
}
|
|
199
|
-
// Also consider edge control points
|
|
200
202
|
for (const edge of layoutEdges) {
|
|
201
203
|
for (const pt of edge.points) {
|
|
202
204
|
if (pt.x > totalWidth) totalWidth = pt.x;
|
|
203
205
|
if (pt.y > totalHeight) totalHeight = pt.y;
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
|
-
// Add margin
|
|
207
208
|
totalWidth += 40;
|
|
208
209
|
totalHeight += 40;
|
|
209
210
|
|
|
210
|
-
return {
|
|
211
|
-
nodes: layoutNodes,
|
|
212
|
-
edges: layoutEdges,
|
|
213
|
-
groups: layoutGroups,
|
|
214
|
-
width: totalWidth,
|
|
215
|
-
height: totalHeight,
|
|
216
|
-
};
|
|
211
|
+
return { nodes: layoutNodes, edges: layoutEdges, groups: layoutGroups, width: totalWidth, height: totalHeight };
|
|
217
212
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as d3Selection from 'd3-selection';
|
|
6
6
|
import * as d3Shape from 'd3-shape';
|
|
7
7
|
import { FONT_FAMILY } from '../fonts';
|
|
8
|
-
import { contrastText } from '../palettes/color-utils';
|
|
8
|
+
import { contrastText, mix } from '../palettes/color-utils';
|
|
9
9
|
import type { PaletteColors } from '../palettes';
|
|
10
10
|
import type { ParsedInitiativeStatus, InitiativeStatus } from './types';
|
|
11
11
|
import type { ParticipantType } from '../sequence/parser';
|
|
@@ -37,17 +37,6 @@ const GROUP_LABEL_FONT_SIZE = 11;
|
|
|
37
37
|
// Color helpers
|
|
38
38
|
// ============================================================
|
|
39
39
|
|
|
40
|
-
function mix(a: string, b: string, pct: number): string {
|
|
41
|
-
const parse = (h: string) => {
|
|
42
|
-
const r = h.replace('#', '');
|
|
43
|
-
const f = r.length === 3 ? r[0]+r[0]+r[1]+r[1]+r[2]+r[2] : r;
|
|
44
|
-
return [parseInt(f.substring(0,2),16), parseInt(f.substring(2,4),16), parseInt(f.substring(4,6),16)];
|
|
45
|
-
};
|
|
46
|
-
const [ar,ag,ab] = parse(a), [br,bg,bb] = parse(b), t = pct/100;
|
|
47
|
-
const c = (x: number, y: number) => Math.round(x*t + y*(1-t)).toString(16).padStart(2,'0');
|
|
48
|
-
return `#${c(ar,br)}${c(ag,bg)}${c(ab,bb)}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
40
|
function statusColor(status: InitiativeStatus, palette: PaletteColors, isDark: boolean): string {
|
|
52
41
|
switch (status) {
|
|
53
42
|
case 'done': return palette.colors.green;
|
|
@@ -80,10 +69,13 @@ function edgeStrokeColor(status: InitiativeStatus, palette: PaletteColors, isDar
|
|
|
80
69
|
// Edge path generator
|
|
81
70
|
// ============================================================
|
|
82
71
|
|
|
72
|
+
// curveMonotoneX: interpolates through all control points and guarantees no
|
|
73
|
+
// Y-overshoot between consecutive points. Works for both our 4-point elbows
|
|
74
|
+
// (adjacent-rank) and dagre's fixed waypoints (multi-rank).
|
|
83
75
|
const lineGenerator = d3Shape.line<{ x: number; y: number }>()
|
|
84
76
|
.x((d) => d.x)
|
|
85
77
|
.y((d) => d.y)
|
|
86
|
-
.curve(d3Shape.
|
|
78
|
+
.curve(d3Shape.curveMonotoneX);
|
|
87
79
|
|
|
88
80
|
// ============================================================
|
|
89
81
|
// Text fitting — wrap or shrink to fit fixed-size nodes
|
|
@@ -672,6 +664,14 @@ export function renderInitiativeStatus(
|
|
|
672
664
|
|
|
673
665
|
const pathD = lineGenerator(edge.points);
|
|
674
666
|
if (pathD) {
|
|
667
|
+
// Transparent wide hit area behind the visible edge
|
|
668
|
+
edgeG
|
|
669
|
+
.append('path')
|
|
670
|
+
.attr('d', pathD)
|
|
671
|
+
.attr('fill', 'none')
|
|
672
|
+
.attr('stroke', 'transparent')
|
|
673
|
+
.attr('stroke-width', 16);
|
|
674
|
+
|
|
675
675
|
edgeG
|
|
676
676
|
.append('path')
|
|
677
677
|
.attr('d', pathD)
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as d3Selection from 'd3-selection';
|
|
6
6
|
import { FONT_FAMILY } from '../fonts';
|
|
7
7
|
import type { PaletteColors } from '../palettes';
|
|
8
|
+
import { mix } from '../palettes/color-utils';
|
|
8
9
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
9
10
|
import type { ParsedKanban, KanbanColumn, KanbanCard, KanbanTagGroup } from './types';
|
|
10
11
|
import { parseKanban } from './parser';
|
|
@@ -40,30 +41,6 @@ const LEGEND_FONT_SIZE = 11;
|
|
|
40
41
|
const LEGEND_DOT_R = 4;
|
|
41
42
|
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
42
43
|
|
|
43
|
-
// ============================================================
|
|
44
|
-
// Color helpers
|
|
45
|
-
// ============================================================
|
|
46
|
-
|
|
47
|
-
function mix(a: string, b: string, pct: number): string {
|
|
48
|
-
const parse = (h: string) => {
|
|
49
|
-
const r = h.replace('#', '');
|
|
50
|
-
const f = r.length === 3 ? r[0] + r[0] + r[1] + r[1] + r[2] + r[2] : r;
|
|
51
|
-
return [
|
|
52
|
-
parseInt(f.substring(0, 2), 16),
|
|
53
|
-
parseInt(f.substring(2, 4), 16),
|
|
54
|
-
parseInt(f.substring(4, 6), 16),
|
|
55
|
-
];
|
|
56
|
-
};
|
|
57
|
-
const [ar, ag, ab] = parse(a);
|
|
58
|
-
const [br, bg, bb] = parse(b);
|
|
59
|
-
const t = pct / 100;
|
|
60
|
-
const c = (x: number, y: number) =>
|
|
61
|
-
Math.round(x * t + y * (1 - t))
|
|
62
|
-
.toString(16)
|
|
63
|
-
.padStart(2, '0');
|
|
64
|
-
return `#${c(ar, br)}${c(ag, bg)}${c(ab, bb)}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
44
|
// ============================================================
|
|
68
45
|
// Tag color resolution
|
|
69
46
|
// ============================================================
|
package/src/org/layout.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { hierarchy, tree } from 'd3-hierarchy';
|
|
6
6
|
import type { ParsedOrg, OrgNode, OrgTagGroup } from './parser';
|
|
7
|
+
import { resolveTagColor, injectDefaultTagMetadata } from '../utils/tag-groups';
|
|
7
8
|
|
|
8
9
|
// ============================================================
|
|
9
10
|
// Types
|
|
@@ -169,20 +170,9 @@ function resolveNodeColor(
|
|
|
169
170
|
tagGroups: OrgTagGroup[],
|
|
170
171
|
activeGroupName: string | null
|
|
171
172
|
): string | undefined {
|
|
173
|
+
// Explicit inline (color) always wins — handled before tag resolution
|
|
172
174
|
if (node.color) return node.color;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const group = tagGroups.find(
|
|
176
|
-
(g) => g.name.toLowerCase() === activeGroupName.toLowerCase()
|
|
177
|
-
);
|
|
178
|
-
if (!group) return undefined;
|
|
179
|
-
const metaValue =
|
|
180
|
-
node.metadata[group.name.toLowerCase()] ??
|
|
181
|
-
(node.isContainer ? undefined : group.defaultValue);
|
|
182
|
-
if (!metaValue) return '#999999';
|
|
183
|
-
return group.entries.find(
|
|
184
|
-
(e) => e.value.toLowerCase() === metaValue.toLowerCase()
|
|
185
|
-
)?.color ?? '#999999';
|
|
175
|
+
return resolveTagColor(node.metadata, tagGroups, activeGroupName, node.isContainer);
|
|
186
176
|
}
|
|
187
177
|
|
|
188
178
|
// ============================================================
|
|
@@ -325,38 +315,33 @@ function computeLegendGroups(
|
|
|
325
315
|
|
|
326
316
|
/**
|
|
327
317
|
* Inject default tag group values into non-container node metadata.
|
|
328
|
-
*
|
|
318
|
+
* Delegates to shared `injectDefaultTagMetadata` with org-specific skip logic.
|
|
329
319
|
*/
|
|
330
320
|
function injectDefaultMetadata(
|
|
331
321
|
roots: OrgNode[],
|
|
332
322
|
tagGroups: OrgTagGroup[]
|
|
333
323
|
): void {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
if (defaults.length === 0) return;
|
|
341
|
-
|
|
342
|
-
const walk = (node: OrgNode) => {
|
|
343
|
-
if (!node.isContainer) {
|
|
344
|
-
for (const { key, value } of defaults) {
|
|
345
|
-
if (!(key in node.metadata)) {
|
|
346
|
-
node.metadata[key] = value;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
for (const child of node.children) walk(child);
|
|
324
|
+
// Flatten all nodes (recursive) for the shared utility
|
|
325
|
+
const allNodes: OrgNode[] = [];
|
|
326
|
+
const collect = (node: OrgNode) => {
|
|
327
|
+
allNodes.push(node);
|
|
328
|
+
for (const child of node.children) collect(child);
|
|
351
329
|
};
|
|
352
|
-
for (const root of roots)
|
|
330
|
+
for (const root of roots) collect(root);
|
|
331
|
+
|
|
332
|
+
injectDefaultTagMetadata(
|
|
333
|
+
allNodes,
|
|
334
|
+
tagGroups,
|
|
335
|
+
(entity) => (entity as OrgNode).isContainer
|
|
336
|
+
);
|
|
353
337
|
}
|
|
354
338
|
|
|
355
339
|
export function layoutOrg(
|
|
356
340
|
parsed: ParsedOrg,
|
|
357
341
|
hiddenCounts?: Map<string, number>,
|
|
358
342
|
activeTagGroup?: string | null,
|
|
359
|
-
hiddenAttributes?: Set<string
|
|
343
|
+
hiddenAttributes?: Set<string>,
|
|
344
|
+
expandAllLegend?: boolean
|
|
360
345
|
): OrgLayoutResult {
|
|
361
346
|
if (parsed.roots.length === 0) {
|
|
362
347
|
// Legend-only: compute and position legend groups even without nodes
|
|
@@ -510,9 +495,13 @@ export function layoutOrg(
|
|
|
510
495
|
(d) => d.data.orgNode.id !== '__virtual_root__'
|
|
511
496
|
);
|
|
512
497
|
|
|
513
|
-
// Collect max actual card height per depth level
|
|
498
|
+
// Collect max actual card height per depth level.
|
|
499
|
+
// Exclude __stack_ placeholders — their aggregate height (multiple
|
|
500
|
+
// stacked cards) would inflate the level max and push sibling
|
|
501
|
+
// subtrees' deeper children far below where they need to be.
|
|
514
502
|
const levelMaxHeight = new Map<number, number>();
|
|
515
503
|
for (const d of descendants) {
|
|
504
|
+
if (d.data.orgNode.id.startsWith('__stack_')) continue;
|
|
516
505
|
const cur = levelMaxHeight.get(d.depth) ?? 0;
|
|
517
506
|
if (d.data.height > cur) levelMaxHeight.set(d.depth, d.data.height);
|
|
518
507
|
}
|
|
@@ -1128,14 +1117,16 @@ export function layoutOrg(
|
|
|
1128
1117
|
const legendPosition = parsed.options?.['legend-position'] ?? 'top';
|
|
1129
1118
|
|
|
1130
1119
|
// When a tag group is active, only that group is laid out (full size).
|
|
1131
|
-
// When none is active, all groups are laid out minified
|
|
1120
|
+
// When none is active, all groups are laid out minified — unless
|
|
1121
|
+
// expandAllLegend is set (export mode), which shows all groups expanded.
|
|
1132
1122
|
const visibleGroups = activeTagGroup != null
|
|
1133
1123
|
? legendGroups.filter((g) => g.name.toLowerCase() === activeTagGroup.toLowerCase())
|
|
1134
1124
|
: legendGroups;
|
|
1125
|
+
const allExpanded = expandAllLegend && activeTagGroup == null;
|
|
1135
1126
|
const effectiveW = (g: OrgLegendGroup) =>
|
|
1136
|
-
activeTagGroup != null ? g.width : g.minifiedWidth;
|
|
1127
|
+
activeTagGroup != null || allExpanded ? g.width : g.minifiedWidth;
|
|
1137
1128
|
const effectiveH = (g: OrgLegendGroup) =>
|
|
1138
|
-
activeTagGroup != null ? g.height : g.minifiedHeight;
|
|
1129
|
+
activeTagGroup != null || allExpanded ? g.height : g.minifiedHeight;
|
|
1139
1130
|
|
|
1140
1131
|
if (visibleGroups.length > 0) {
|
|
1141
1132
|
if (legendPosition === 'bottom') {
|
package/src/org/parser.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { PaletteColors } from '../palettes';
|
|
|
2
2
|
import type { DgmoError } from '../diagnostics';
|
|
3
3
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
4
4
|
import type { TagGroup, TagEntry } from '../utils/tag-groups';
|
|
5
|
-
import { isTagBlockHeading, matchTagBlockHeading } from '../utils/tag-groups';
|
|
5
|
+
import { isTagBlockHeading, matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
|
|
6
6
|
import {
|
|
7
7
|
measureIndent,
|
|
8
8
|
extractColor,
|
|
@@ -292,6 +292,21 @@ export function parseOrg(
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
// Validate tag group values on nodes
|
|
296
|
+
if (result.tagGroups.length > 0) {
|
|
297
|
+
// Flatten all nodes for the shared validation utility
|
|
298
|
+
const allNodes: OrgNode[] = [];
|
|
299
|
+
const collectAll = (nodes: OrgNode[]) => {
|
|
300
|
+
for (const node of nodes) {
|
|
301
|
+
allNodes.push(node);
|
|
302
|
+
collectAll(node.children);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
collectAll(result.roots);
|
|
306
|
+
|
|
307
|
+
validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
|
|
308
|
+
}
|
|
309
|
+
|
|
295
310
|
if (result.roots.length === 0 && result.tagGroups.length === 0 && !result.error) {
|
|
296
311
|
const diag = makeDgmoError(1, 'No nodes found in org chart');
|
|
297
312
|
result.diagnostics.push(diag);
|