@diagrammo/dgmo 0.8.3 → 0.8.4
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/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +185 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +153 -153
- package/dist/editor.cjs +336 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3336 -1055
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3336 -1055
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +30 -29
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +8 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +100 -55
- package/src/chart.ts +91 -28
- package/src/class/parser.ts +41 -12
- package/src/cli.ts +168 -61
- package/src/completion.ts +378 -183
- package/src/d3.ts +887 -288
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +69 -23
- package/src/echarts.ts +646 -153
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +48 -14
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +197 -71
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +46 -25
- package/src/graph/state-parser.ts +47 -17
- package/src/infra/parser.ts +157 -53
- package/src/infra/renderer.ts +723 -271
- package/src/initiative-status/parser.ts +138 -44
- package/src/kanban/parser.ts +25 -14
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +69 -22
- package/src/palettes/index.ts +3 -2
- package/src/sequence/parser.ts +193 -61
- package/src/sitemap/parser.ts +65 -29
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- package/src/utils/parsing.ts +75 -31
package/src/er/renderer.ts
CHANGED
|
@@ -21,12 +21,21 @@ import {
|
|
|
21
21
|
LEGEND_GROUP_GAP,
|
|
22
22
|
measureLegendText,
|
|
23
23
|
} from '../utils/legend-constants';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
TITLE_FONT_SIZE,
|
|
26
|
+
TITLE_FONT_WEIGHT,
|
|
27
|
+
TITLE_Y,
|
|
28
|
+
} from '../utils/title-constants';
|
|
25
29
|
import type { ParsedERDiagram, ERConstraint } from './types';
|
|
26
30
|
import type { ERLayoutResult } from './layout';
|
|
27
31
|
import { parseERDiagram } from './parser';
|
|
28
32
|
import { layoutERDiagram } from './layout';
|
|
29
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
classifyEREntities,
|
|
35
|
+
ROLE_COLORS,
|
|
36
|
+
ROLE_LABELS,
|
|
37
|
+
ROLE_ORDER,
|
|
38
|
+
} from './classify';
|
|
30
39
|
import type { EntityRole } from './classify';
|
|
31
40
|
|
|
32
41
|
// ============================================================
|
|
@@ -50,10 +59,14 @@ const MEMBER_PADDING_X = 10;
|
|
|
50
59
|
|
|
51
60
|
function constraintIcon(constraint: ERConstraint): string {
|
|
52
61
|
switch (constraint) {
|
|
53
|
-
case 'pk':
|
|
54
|
-
|
|
55
|
-
case '
|
|
56
|
-
|
|
62
|
+
case 'pk':
|
|
63
|
+
return '\u2666'; // ♦
|
|
64
|
+
case 'fk':
|
|
65
|
+
return '\u2192'; // →
|
|
66
|
+
case 'unique':
|
|
67
|
+
return '\u25C6'; // ◆
|
|
68
|
+
case 'nullable':
|
|
69
|
+
return '\u25CB'; // ○
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
72
|
|
|
@@ -61,7 +74,8 @@ function constraintIcon(constraint: ERConstraint): string {
|
|
|
61
74
|
// Edge path generator
|
|
62
75
|
// ============================================================
|
|
63
76
|
|
|
64
|
-
const lineGenerator = d3Shape
|
|
77
|
+
const lineGenerator = d3Shape
|
|
78
|
+
.line<{ x: number; y: number }>()
|
|
65
79
|
.x((d) => d.x)
|
|
66
80
|
.y((d) => d.y)
|
|
67
81
|
.curve(d3Shape.curveBasis);
|
|
@@ -114,7 +128,8 @@ function drawCardinality(
|
|
|
114
128
|
const offset = 15;
|
|
115
129
|
const bx = point.x - ux * offset;
|
|
116
130
|
const by = point.y - uy * offset;
|
|
117
|
-
const labelText =
|
|
131
|
+
const labelText =
|
|
132
|
+
cardinality === '1' ? '1' : cardinality === '*' ? '*' : '0..1';
|
|
118
133
|
g.append('text')
|
|
119
134
|
.attr('x', bx + px * 12)
|
|
120
135
|
.attr('y', by + py * 12)
|
|
@@ -128,7 +143,7 @@ function drawCardinality(
|
|
|
128
143
|
|
|
129
144
|
// Crow's foot notation
|
|
130
145
|
const barOffset = 14; // how far back from the entity the bar sits
|
|
131
|
-
const spread = 9;
|
|
146
|
+
const spread = 9; // half-width of the perpendicular bar / prong span
|
|
132
147
|
|
|
133
148
|
if (cardinality === '1') {
|
|
134
149
|
// Single perpendicular bar
|
|
@@ -145,7 +160,7 @@ function drawCardinality(
|
|
|
145
160
|
// Crow's foot: three prongs fan out FROM a point on the line
|
|
146
161
|
// TOWARD the entity — the fork opens at the entity side.
|
|
147
162
|
const forkOrigin = 18; // distance back from entity where prongs originate
|
|
148
|
-
const forkEnd = 5;
|
|
163
|
+
const forkEnd = 5; // distance from entity where prongs terminate
|
|
149
164
|
|
|
150
165
|
// Origin point (on the line, away from entity)
|
|
151
166
|
const ox = point.x - ux * forkOrigin;
|
|
@@ -241,7 +256,8 @@ export function renderERDiagram(
|
|
|
241
256
|
// size at render time, which eliminates the stagger caused by reading clientWidth/
|
|
242
257
|
// clientHeight before the container has stabilized.
|
|
243
258
|
const naturalW = diagramW + DIAGRAM_PADDING * 2;
|
|
244
|
-
const naturalH =
|
|
259
|
+
const naturalH =
|
|
260
|
+
diagramH + titleHeight + legendReserveH + DIAGRAM_PADDING * 2;
|
|
245
261
|
|
|
246
262
|
// For export: scale the natural layout to fit the requested pixel dimensions.
|
|
247
263
|
// For live preview: render at natural scale (scale=1) and let the SVG viewBox
|
|
@@ -292,7 +308,10 @@ export function renderERDiagram(
|
|
|
292
308
|
.attr('fill', palette.text)
|
|
293
309
|
.attr('font-size', TITLE_FONT_SIZE)
|
|
294
310
|
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
295
|
-
.style(
|
|
311
|
+
.style(
|
|
312
|
+
'cursor',
|
|
313
|
+
onClickItem && parsed.titleLineNumber ? 'pointer' : 'default'
|
|
314
|
+
)
|
|
296
315
|
.text(parsed.title);
|
|
297
316
|
|
|
298
317
|
if (parsed.titleLineNumber) {
|
|
@@ -300,8 +319,12 @@ export function renderERDiagram(
|
|
|
300
319
|
if (onClickItem) {
|
|
301
320
|
titleEl
|
|
302
321
|
.on('click', () => onClickItem(parsed.titleLineNumber!))
|
|
303
|
-
.on('mouseenter', function () {
|
|
304
|
-
|
|
322
|
+
.on('mouseenter', function () {
|
|
323
|
+
d3Selection.select(this).attr('opacity', 0.7);
|
|
324
|
+
})
|
|
325
|
+
.on('mouseleave', function () {
|
|
326
|
+
d3Selection.select(this).attr('opacity', 1);
|
|
327
|
+
});
|
|
305
328
|
}
|
|
306
329
|
}
|
|
307
330
|
}
|
|
@@ -321,7 +344,8 @@ export function renderERDiagram(
|
|
|
321
344
|
? classifyEREntities(parsed.tables, parsed.relationships)
|
|
322
345
|
: null;
|
|
323
346
|
// semanticColorsActive defaults to true; false = legend collapsed, neutral color applied
|
|
324
|
-
const semanticActive =
|
|
347
|
+
const semanticActive =
|
|
348
|
+
semanticRoles !== null && (semanticColorsActive ?? true);
|
|
325
349
|
|
|
326
350
|
// ── Edges (behind nodes) ──
|
|
327
351
|
const useLabels = parsed.options.notation === 'labels';
|
|
@@ -376,7 +400,8 @@ export function renderERDiagram(
|
|
|
376
400
|
const bgW = labelLen * 7 + 8;
|
|
377
401
|
const bgH = 16;
|
|
378
402
|
|
|
379
|
-
edgeG
|
|
403
|
+
edgeG
|
|
404
|
+
.append('rect')
|
|
380
405
|
.attr('x', midPt.x - bgW / 2)
|
|
381
406
|
.attr('y', midPt.y - bgH / 2 - 1)
|
|
382
407
|
.attr('width', bgW)
|
|
@@ -386,7 +411,8 @@ export function renderERDiagram(
|
|
|
386
411
|
.attr('opacity', 0.85)
|
|
387
412
|
.attr('class', 'er-edge-label-bg');
|
|
388
413
|
|
|
389
|
-
edgeG
|
|
414
|
+
edgeG
|
|
415
|
+
.append('text')
|
|
390
416
|
.attr('x', midPt.x)
|
|
391
417
|
.attr('y', midPt.y + 4)
|
|
392
418
|
.attr('text-anchor', 'middle')
|
|
@@ -400,13 +426,23 @@ export function renderERDiagram(
|
|
|
400
426
|
// ── Nodes (top layer) ──
|
|
401
427
|
for (let ni = 0; ni < layout.nodes.length; ni++) {
|
|
402
428
|
const node = layout.nodes[ni];
|
|
403
|
-
const tagColor = resolveTagColor(
|
|
429
|
+
const tagColor = resolveTagColor(
|
|
430
|
+
node.metadata,
|
|
431
|
+
parsed.tagGroups,
|
|
432
|
+
activeTagGroup ?? null
|
|
433
|
+
);
|
|
404
434
|
const semanticColor = semanticActive
|
|
405
|
-
? palette.colors[
|
|
435
|
+
? palette.colors[
|
|
436
|
+
ROLE_COLORS[semanticRoles!.get(node.id) ?? 'unclassified']
|
|
437
|
+
]
|
|
406
438
|
: semanticRoles
|
|
407
|
-
? palette.primary
|
|
439
|
+
? palette.primary // neutral color when legend is collapsed
|
|
408
440
|
: undefined;
|
|
409
|
-
const nodeColor =
|
|
441
|
+
const nodeColor =
|
|
442
|
+
node.color ??
|
|
443
|
+
tagColor ??
|
|
444
|
+
semanticColor ??
|
|
445
|
+
seriesColors[ni % seriesColors.length];
|
|
410
446
|
|
|
411
447
|
const nodeG = contentG
|
|
412
448
|
.append('g')
|
|
@@ -442,7 +478,8 @@ export function renderERDiagram(
|
|
|
442
478
|
const stroke = nodeColor;
|
|
443
479
|
|
|
444
480
|
// Outer rectangle
|
|
445
|
-
nodeG
|
|
481
|
+
nodeG
|
|
482
|
+
.append('rect')
|
|
446
483
|
.attr('x', -w / 2)
|
|
447
484
|
.attr('y', -h / 2)
|
|
448
485
|
.attr('width', w)
|
|
@@ -457,7 +494,8 @@ export function renderERDiagram(
|
|
|
457
494
|
let yPos = -h / 2;
|
|
458
495
|
const headerCenterY = yPos + node.headerHeight / 2;
|
|
459
496
|
|
|
460
|
-
nodeG
|
|
497
|
+
nodeG
|
|
498
|
+
.append('text')
|
|
461
499
|
.attr('x', 0)
|
|
462
500
|
.attr('y', headerCenterY)
|
|
463
501
|
.attr('text-anchor', 'middle')
|
|
@@ -472,7 +510,8 @@ export function renderERDiagram(
|
|
|
472
510
|
// Columns compartment
|
|
473
511
|
if (node.columns.length > 0) {
|
|
474
512
|
// Separator line
|
|
475
|
-
nodeG
|
|
513
|
+
nodeG
|
|
514
|
+
.append('line')
|
|
476
515
|
.attr('x1', -w / 2)
|
|
477
516
|
.attr('y1', yPos)
|
|
478
517
|
.attr('x2', w / 2)
|
|
@@ -487,7 +526,8 @@ export function renderERDiagram(
|
|
|
487
526
|
const iconX = -w / 2 + MEMBER_PADDING_X;
|
|
488
527
|
const primaryConstraint = col.constraints[0];
|
|
489
528
|
if (primaryConstraint) {
|
|
490
|
-
nodeG
|
|
529
|
+
nodeG
|
|
530
|
+
.append('text')
|
|
491
531
|
.attr('x', iconX)
|
|
492
532
|
.attr('y', memberY + MEMBER_LINE_HEIGHT / 2)
|
|
493
533
|
.attr('dominant-baseline', 'central')
|
|
@@ -501,7 +541,8 @@ export function renderERDiagram(
|
|
|
501
541
|
let colText = col.name;
|
|
502
542
|
if (col.type) colText += `: ${col.type}`;
|
|
503
543
|
|
|
504
|
-
nodeG
|
|
544
|
+
nodeG
|
|
545
|
+
.append('text')
|
|
505
546
|
.attr('x', textX)
|
|
506
547
|
.attr('y', memberY + MEMBER_LINE_HEIGHT / 2)
|
|
507
548
|
.attr('dominant-baseline', 'central')
|
|
@@ -520,22 +561,23 @@ export function renderERDiagram(
|
|
|
520
561
|
const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
|
|
521
562
|
const LEGEND_GAP = 8;
|
|
522
563
|
|
|
523
|
-
const legendG = svg.append('g')
|
|
524
|
-
.attr('class', 'er-tag-legend');
|
|
564
|
+
const legendG = svg.append('g').attr('class', 'er-tag-legend');
|
|
525
565
|
|
|
526
566
|
if (activeTagGroup) {
|
|
527
567
|
legendG.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
528
568
|
}
|
|
529
569
|
|
|
530
570
|
let legendX = DIAGRAM_PADDING;
|
|
531
|
-
|
|
571
|
+
const legendY = DIAGRAM_PADDING + titleHeight;
|
|
532
572
|
|
|
533
573
|
for (const group of parsed.tagGroups) {
|
|
534
|
-
const groupG = legendG
|
|
574
|
+
const groupG = legendG
|
|
575
|
+
.append('g')
|
|
535
576
|
.attr('data-legend-group', group.name.toLowerCase());
|
|
536
577
|
|
|
537
578
|
// Group label
|
|
538
|
-
const labelText = groupG
|
|
579
|
+
const labelText = groupG
|
|
580
|
+
.append('text')
|
|
539
581
|
.attr('x', legendX)
|
|
540
582
|
.attr('y', legendY + LEGEND_PILL_H / 2)
|
|
541
583
|
.attr('dominant-baseline', 'central')
|
|
@@ -544,37 +586,47 @@ export function renderERDiagram(
|
|
|
544
586
|
.attr('font-family', FONT_FAMILY)
|
|
545
587
|
.text(`${group.name}:`);
|
|
546
588
|
|
|
547
|
-
const labelWidth =
|
|
589
|
+
const labelWidth =
|
|
590
|
+
(labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) +
|
|
591
|
+
6;
|
|
548
592
|
legendX += labelWidth;
|
|
549
593
|
|
|
550
594
|
// Entries
|
|
551
595
|
for (const entry of group.entries) {
|
|
552
|
-
const pillG = groupG
|
|
596
|
+
const pillG = groupG
|
|
597
|
+
.append('g')
|
|
553
598
|
.attr('data-legend-entry', entry.value.toLowerCase())
|
|
554
599
|
.style('cursor', 'pointer');
|
|
555
600
|
|
|
556
601
|
// Estimate text width
|
|
557
|
-
const tmpText = legendG
|
|
602
|
+
const tmpText = legendG
|
|
603
|
+
.append('text')
|
|
558
604
|
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
559
605
|
.attr('font-family', FONT_FAMILY)
|
|
560
606
|
.text(entry.value);
|
|
561
|
-
const textW =
|
|
607
|
+
const textW =
|
|
608
|
+
tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
|
|
562
609
|
tmpText.remove();
|
|
563
610
|
|
|
564
611
|
const pillW = textW + LEGEND_PILL_PAD * 2;
|
|
565
612
|
|
|
566
|
-
pillG
|
|
613
|
+
pillG
|
|
614
|
+
.append('rect')
|
|
567
615
|
.attr('x', legendX)
|
|
568
616
|
.attr('y', legendY)
|
|
569
617
|
.attr('width', pillW)
|
|
570
618
|
.attr('height', LEGEND_PILL_H)
|
|
571
619
|
.attr('rx', LEGEND_PILL_RX)
|
|
572
620
|
.attr('ry', LEGEND_PILL_RX)
|
|
573
|
-
.attr(
|
|
621
|
+
.attr(
|
|
622
|
+
'fill',
|
|
623
|
+
mix(entry.color, isDark ? palette.surface : palette.bg, 25)
|
|
624
|
+
)
|
|
574
625
|
.attr('stroke', entry.color)
|
|
575
626
|
.attr('stroke-width', 1);
|
|
576
627
|
|
|
577
|
-
pillG
|
|
628
|
+
pillG
|
|
629
|
+
.append('text')
|
|
578
630
|
.attr('x', legendX + pillW / 2)
|
|
579
631
|
.attr('y', legendY + LEGEND_PILL_H / 2)
|
|
580
632
|
.attr('text-anchor', 'middle')
|
|
@@ -607,19 +659,25 @@ export function renderERDiagram(
|
|
|
607
659
|
// Measure actual text widths for consistent spacing regardless of character mix.
|
|
608
660
|
// Falls back to a character-count estimate in jsdom/test environments.
|
|
609
661
|
const measureLabelW = (text: string, fontSize: number): number => {
|
|
610
|
-
const dummy = svg
|
|
662
|
+
const dummy = svg
|
|
663
|
+
.append('text')
|
|
611
664
|
.attr('font-size', fontSize)
|
|
612
665
|
.attr('font-family', FONT_FAMILY)
|
|
613
666
|
.attr('visibility', 'hidden')
|
|
614
667
|
.text(text);
|
|
615
|
-
const measured =
|
|
668
|
+
const measured =
|
|
669
|
+
(dummy.node() as SVGTextElement | null)?.getComputedTextLength?.() ??
|
|
670
|
+
0;
|
|
616
671
|
dummy.remove();
|
|
617
672
|
return measured > 0 ? measured : text.length * fontSize * 0.6;
|
|
618
673
|
};
|
|
619
674
|
|
|
620
675
|
const labelWidths = new Map<EntityRole, number>();
|
|
621
676
|
for (const role of presentRoles) {
|
|
622
|
-
labelWidths.set(
|
|
677
|
+
labelWidths.set(
|
|
678
|
+
role,
|
|
679
|
+
measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE)
|
|
680
|
+
);
|
|
623
681
|
}
|
|
624
682
|
|
|
625
683
|
const groupBg = isDark
|
|
@@ -627,7 +685,8 @@ export function renderERDiagram(
|
|
|
627
685
|
: mix(palette.surface, palette.bg, 30);
|
|
628
686
|
|
|
629
687
|
const groupName = 'Role';
|
|
630
|
-
const pillWidth =
|
|
688
|
+
const pillWidth =
|
|
689
|
+
measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
631
690
|
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
632
691
|
|
|
633
692
|
let totalWidth: number;
|
|
@@ -640,7 +699,11 @@ export function renderERDiagram(
|
|
|
640
699
|
labelWidths.get(role)! +
|
|
641
700
|
LEGEND_ENTRY_TRAIL;
|
|
642
701
|
}
|
|
643
|
-
totalWidth =
|
|
702
|
+
totalWidth =
|
|
703
|
+
LEGEND_CAPSULE_PAD * 2 +
|
|
704
|
+
pillWidth +
|
|
705
|
+
LEGEND_ENTRY_TRAIL +
|
|
706
|
+
entriesWidth;
|
|
644
707
|
} else {
|
|
645
708
|
totalWidth = pillWidth;
|
|
646
709
|
}
|
|
@@ -764,7 +827,8 @@ export function renderERDiagramForExport(
|
|
|
764
827
|
|
|
765
828
|
const container = document.createElement('div');
|
|
766
829
|
const exportWidth = layout.width + DIAGRAM_PADDING * 2;
|
|
767
|
-
const exportHeight =
|
|
830
|
+
const exportHeight =
|
|
831
|
+
layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? 40 : 0);
|
|
768
832
|
container.style.width = `${exportWidth}px`;
|
|
769
833
|
container.style.height = `${exportHeight}px`;
|
|
770
834
|
container.style.position = 'absolute';
|
|
@@ -772,15 +836,10 @@ export function renderERDiagramForExport(
|
|
|
772
836
|
document.body.appendChild(container);
|
|
773
837
|
|
|
774
838
|
try {
|
|
775
|
-
renderERDiagram(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
palette,
|
|
780
|
-
isDark,
|
|
781
|
-
undefined,
|
|
782
|
-
{ width: exportWidth, height: exportHeight }
|
|
783
|
-
);
|
|
839
|
+
renderERDiagram(container, parsed, layout, palette, isDark, undefined, {
|
|
840
|
+
width: exportWidth,
|
|
841
|
+
height: exportHeight,
|
|
842
|
+
});
|
|
784
843
|
|
|
785
844
|
const svgEl = container.querySelector('svg');
|
|
786
845
|
if (!svgEl) return '';
|
package/src/gantt/calculator.ts
CHANGED
|
@@ -18,7 +18,6 @@ import type {
|
|
|
18
18
|
GanttGroup,
|
|
19
19
|
GanttHolidays,
|
|
20
20
|
ResolvedSchedule,
|
|
21
|
-
ResolvedTask,
|
|
22
21
|
ResolvedGroup,
|
|
23
22
|
Offset,
|
|
24
23
|
} from './types';
|
|
@@ -27,7 +26,6 @@ import {
|
|
|
27
26
|
addGanttDuration,
|
|
28
27
|
buildHolidaySet,
|
|
29
28
|
parseGanttDate,
|
|
30
|
-
daysBetween,
|
|
31
29
|
} from '../utils/duration';
|
|
32
30
|
|
|
33
31
|
// ── Internal types ──────────────────────────────────────────
|
|
@@ -65,7 +63,8 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
65
63
|
diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
66
64
|
};
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
67
|
+
const _fail = (line: number, message: string): ResolvedSchedule => {
|
|
69
68
|
const diag = makeDgmoError(line, message);
|
|
70
69
|
diagnostics.push(diag);
|
|
71
70
|
result.error = formatDgmoError(diag);
|
|
@@ -115,7 +114,8 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
115
114
|
// ── Resolve explicit -> dependencies ────────────────────
|
|
116
115
|
|
|
117
116
|
for (const task of allTasks) {
|
|
118
|
-
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
118
|
+
const _node = taskMap.get(task.id)!;
|
|
119
119
|
for (const dep of task.dependencies) {
|
|
120
120
|
const resolved = resolveTaskName(dep.targetName, allTasks);
|
|
121
121
|
if (isResolverError(resolved)) {
|
|
@@ -129,7 +129,10 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
129
129
|
if (targetNode) {
|
|
130
130
|
// Check for redundant dependency (already a sequential predecessor)
|
|
131
131
|
if (targetNode.predecessors.includes(task.id)) {
|
|
132
|
-
warn(
|
|
132
|
+
warn(
|
|
133
|
+
dep.lineNumber,
|
|
134
|
+
`Redundant dependency: "${dep.targetName}" already follows "${task.label}" sequentially. Did you mean to wrap groups in \`parallel\`?`
|
|
135
|
+
);
|
|
133
136
|
} else {
|
|
134
137
|
targetNode.predecessors.push(task.id);
|
|
135
138
|
// Store dep offset info — we need it during scheduling
|
|
@@ -147,10 +150,10 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
147
150
|
if (!sortedIds) {
|
|
148
151
|
// Find cycle, warn, and break it by removing one explicit dep edge
|
|
149
152
|
const cycle = findCycle(taskMap);
|
|
150
|
-
const cycleStr = cycle.map(id => taskMap.get(id)!.task.label).join(' → ');
|
|
153
|
+
const cycleStr = cycle.map((id) => taskMap.get(id)!.task.label).join(' → ');
|
|
151
154
|
warn(
|
|
152
155
|
taskMap.get(cycle[0])!.task.lineNumber,
|
|
153
|
-
`Circular dependency detected: ${cycleStr}. The cycle-creating dependency was dropped
|
|
156
|
+
`Circular dependency detected: ${cycleStr}. The cycle-creating dependency was dropped.`
|
|
154
157
|
);
|
|
155
158
|
|
|
156
159
|
// Remove the last edge in the cycle to break it
|
|
@@ -200,7 +203,13 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
200
203
|
// Apply dep offset if present
|
|
201
204
|
const depOffset = depOffsetMap.get(`${predId}->${taskId}`);
|
|
202
205
|
if (depOffset) {
|
|
203
|
-
predEnd = addGanttDuration(
|
|
206
|
+
predEnd = addGanttDuration(
|
|
207
|
+
predEnd,
|
|
208
|
+
depOffset.duration,
|
|
209
|
+
parsed.holidays,
|
|
210
|
+
holidaySet,
|
|
211
|
+
depOffset.direction
|
|
212
|
+
);
|
|
204
213
|
}
|
|
205
214
|
|
|
206
215
|
if (predEnd.getTime() > start.getTime()) {
|
|
@@ -211,13 +220,25 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
211
220
|
|
|
212
221
|
// Apply task-level offset (shifts start forward or backward)
|
|
213
222
|
if (task.offset) {
|
|
214
|
-
start = addGanttDuration(
|
|
223
|
+
start = addGanttDuration(
|
|
224
|
+
start,
|
|
225
|
+
task.offset.duration,
|
|
226
|
+
parsed.holidays,
|
|
227
|
+
holidaySet,
|
|
228
|
+
task.offset.direction
|
|
229
|
+
);
|
|
215
230
|
if (start.getTime() < projectStart.getTime()) {
|
|
216
|
-
warn(
|
|
231
|
+
warn(
|
|
232
|
+
task.lineNumber,
|
|
233
|
+
`Negative offset on task '${task.label}' exceeds available range; start clamped to project start.`
|
|
234
|
+
);
|
|
217
235
|
start = new Date(projectStart);
|
|
218
236
|
}
|
|
219
237
|
} else if (start.getTime() < projectStart.getTime()) {
|
|
220
|
-
warn(
|
|
238
|
+
warn(
|
|
239
|
+
task.lineNumber,
|
|
240
|
+
`Negative offset on dependency exceeds available range; start of '${task.label}' clamped to project start.`
|
|
241
|
+
);
|
|
221
242
|
start = new Date(projectStart);
|
|
222
243
|
}
|
|
223
244
|
|
|
@@ -226,12 +247,18 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
226
247
|
let maxPredEnd = new Date(0);
|
|
227
248
|
for (const predId of node.predecessors) {
|
|
228
249
|
const predNode = taskMap.get(predId)!;
|
|
229
|
-
if (
|
|
250
|
+
if (
|
|
251
|
+
predNode.endDate &&
|
|
252
|
+
predNode.endDate.getTime() > maxPredEnd.getTime()
|
|
253
|
+
) {
|
|
230
254
|
maxPredEnd = predNode.endDate;
|
|
231
255
|
}
|
|
232
256
|
}
|
|
233
257
|
if (start.getTime() < maxPredEnd.getTime()) {
|
|
234
|
-
warn(
|
|
258
|
+
warn(
|
|
259
|
+
task.lineNumber,
|
|
260
|
+
`Explicit date ${task.explicitStart}${task.offset ? ' (with offset)' : ''} overlaps with predecessor ending ${formatDate(maxPredEnd)}. Using explicit date.`
|
|
261
|
+
);
|
|
235
262
|
}
|
|
236
263
|
}
|
|
237
264
|
|
|
@@ -242,7 +269,12 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
242
269
|
// Milestone: zero duration, end = start
|
|
243
270
|
end = new Date(start);
|
|
244
271
|
} else {
|
|
245
|
-
end = addGanttDuration(
|
|
272
|
+
end = addGanttDuration(
|
|
273
|
+
start,
|
|
274
|
+
task.duration,
|
|
275
|
+
parsed.holidays,
|
|
276
|
+
holidaySet
|
|
277
|
+
);
|
|
246
278
|
}
|
|
247
279
|
} else {
|
|
248
280
|
// Explicit date task with no duration = milestone at that date
|
|
@@ -257,7 +289,13 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
257
289
|
|
|
258
290
|
// Critical path calculation (if enabled)
|
|
259
291
|
const criticalSet = parsed.options.criticalPath
|
|
260
|
-
? computeCriticalPath(
|
|
292
|
+
? computeCriticalPath(
|
|
293
|
+
sortedIds,
|
|
294
|
+
taskMap,
|
|
295
|
+
depOffsetMap,
|
|
296
|
+
parsed.holidays,
|
|
297
|
+
holidaySet
|
|
298
|
+
)
|
|
261
299
|
: new Set<string>();
|
|
262
300
|
|
|
263
301
|
// Cascading uncertainty: uncertain if task itself is uncertain OR any predecessor is
|
|
@@ -284,7 +322,9 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
284
322
|
endDate: node.endDate!,
|
|
285
323
|
isCriticalPath: criticalSet.has(taskId),
|
|
286
324
|
isUncertain: uncertainSet.has(taskId),
|
|
287
|
-
isMilestone:
|
|
325
|
+
isMilestone:
|
|
326
|
+
node.task.duration?.amount === 0 ||
|
|
327
|
+
(!node.task.duration && !node.task.explicitStart),
|
|
288
328
|
groupPath: node.task.groupPath,
|
|
289
329
|
effectiveMetadata: node.task.metadata,
|
|
290
330
|
});
|
|
@@ -310,12 +350,14 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
310
350
|
// ── Warnings ────────────────────────────────────────────
|
|
311
351
|
|
|
312
352
|
// Missing parallel warning: 2+ top-level groups without parallel wrapper
|
|
313
|
-
const topLevelGroups = parsed.nodes.filter(n => n.kind === 'group');
|
|
353
|
+
const topLevelGroups = parsed.nodes.filter((n) => n.kind === 'group');
|
|
314
354
|
if (topLevelGroups.length >= 2) {
|
|
315
|
-
const names = topLevelGroups.map(
|
|
355
|
+
const names = topLevelGroups.map(
|
|
356
|
+
(g) => (g as GanttGroup & { kind: 'group' }).name
|
|
357
|
+
);
|
|
316
358
|
warn(
|
|
317
359
|
topLevelGroups[0].lineNumber,
|
|
318
|
-
`${names.join(' and ')} are sequential. Wrap in \`parallel\` if they should run concurrently
|
|
360
|
+
`${names.join(' and ')} are sequential. Wrap in \`parallel\` if they should run concurrently.`
|
|
319
361
|
);
|
|
320
362
|
}
|
|
321
363
|
|
|
@@ -332,7 +374,7 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
332
374
|
*/
|
|
333
375
|
function buildImplicitDeps(
|
|
334
376
|
nodes: GanttNode[],
|
|
335
|
-
taskMap: Map<string, TaskNode
|
|
377
|
+
taskMap: Map<string, TaskNode>
|
|
336
378
|
): void {
|
|
337
379
|
walkChildren(nodes, null);
|
|
338
380
|
|
|
@@ -408,7 +450,10 @@ function buildImplicitDeps(
|
|
|
408
450
|
}
|
|
409
451
|
}
|
|
410
452
|
|
|
411
|
-
function walkSequential(
|
|
453
|
+
function walkSequential(
|
|
454
|
+
children: GanttNode[],
|
|
455
|
+
afterTaskId: string | null
|
|
456
|
+
): string | null {
|
|
412
457
|
let prevTaskId = afterTaskId;
|
|
413
458
|
for (const node of children) {
|
|
414
459
|
if (node.kind === 'task') {
|
|
@@ -545,7 +590,7 @@ function findCycle(taskMap: Map<string, TaskNode>): string[] {
|
|
|
545
590
|
function breakCycle(
|
|
546
591
|
cycle: string[],
|
|
547
592
|
taskMap: Map<string, TaskNode>,
|
|
548
|
-
depOffsetMap: Map<string, Offset
|
|
593
|
+
depOffsetMap: Map<string, Offset>
|
|
549
594
|
): void {
|
|
550
595
|
if (cycle.length < 3) return; // need at least [A, B, A]
|
|
551
596
|
// Remove the edge from second-to-last → first (i.e. the edge that closes the cycle)
|
|
@@ -568,7 +613,7 @@ function computeCriticalPath(
|
|
|
568
613
|
taskMap: Map<string, TaskNode>,
|
|
569
614
|
depOffsetMap: Map<string, Offset>,
|
|
570
615
|
holidays: GanttHolidays,
|
|
571
|
-
holidaySet: Set<string
|
|
616
|
+
holidaySet: Set<string>
|
|
572
617
|
): Set<string> {
|
|
573
618
|
if (sortedIds.length === 0) return new Set();
|
|
574
619
|
|
|
@@ -610,7 +655,13 @@ function computeCriticalPath(
|
|
|
610
655
|
const succTask = taskMap.get(succId)!.task;
|
|
611
656
|
if (succTask.offset) {
|
|
612
657
|
const reverseDir = (succTask.offset.direction * -1) as 1 | -1;
|
|
613
|
-
const adjusted = addGanttDuration(
|
|
658
|
+
const adjusted = addGanttDuration(
|
|
659
|
+
new Date(succLS),
|
|
660
|
+
succTask.offset.duration,
|
|
661
|
+
holidays,
|
|
662
|
+
holidaySet,
|
|
663
|
+
reverseDir
|
|
664
|
+
);
|
|
614
665
|
succLS = adjusted.getTime();
|
|
615
666
|
}
|
|
616
667
|
|
|
@@ -618,7 +669,13 @@ function computeCriticalPath(
|
|
|
618
669
|
const depOffset = depOffsetMap.get(`${id}->${succId}`);
|
|
619
670
|
if (depOffset) {
|
|
620
671
|
const reverseDir = (depOffset.direction * -1) as 1 | -1;
|
|
621
|
-
const adjusted = addGanttDuration(
|
|
672
|
+
const adjusted = addGanttDuration(
|
|
673
|
+
new Date(succLS),
|
|
674
|
+
depOffset.duration,
|
|
675
|
+
holidays,
|
|
676
|
+
holidaySet,
|
|
677
|
+
reverseDir
|
|
678
|
+
);
|
|
622
679
|
succLS = adjusted.getTime();
|
|
623
680
|
}
|
|
624
681
|
|
|
@@ -651,7 +708,7 @@ function buildResolvedGroups(
|
|
|
651
708
|
nodes: GanttNode[],
|
|
652
709
|
taskMap: Map<string, TaskNode>,
|
|
653
710
|
groups: ResolvedGroup[],
|
|
654
|
-
depth: number
|
|
711
|
+
depth: number
|
|
655
712
|
): void {
|
|
656
713
|
for (const node of nodes) {
|
|
657
714
|
if (node.kind === 'group') {
|
|
@@ -679,8 +736,10 @@ function buildResolvedGroups(
|
|
|
679
736
|
for (const task of childTasks) {
|
|
680
737
|
const resolved = taskMap.get(task.id);
|
|
681
738
|
if (!resolved?.startDate || !resolved?.endDate) continue;
|
|
682
|
-
if (resolved.startDate.getTime() < minStart)
|
|
683
|
-
|
|
739
|
+
if (resolved.startDate.getTime() < minStart)
|
|
740
|
+
minStart = resolved.startDate.getTime();
|
|
741
|
+
if (resolved.endDate.getTime() > maxEnd)
|
|
742
|
+
maxEnd = resolved.endDate.getTime();
|
|
684
743
|
const dur = resolved.endDate.getTime() - resolved.startDate.getTime();
|
|
685
744
|
totalDuration += dur;
|
|
686
745
|
if (task.progress !== null) {
|
|
@@ -695,7 +754,10 @@ function buildResolvedGroups(
|
|
|
695
754
|
metadata: node.metadata,
|
|
696
755
|
startDate: new Date(minStart === Infinity ? 0 : minStart),
|
|
697
756
|
endDate: new Date(maxEnd === -Infinity ? 0 : maxEnd),
|
|
698
|
-
progress:
|
|
757
|
+
progress:
|
|
758
|
+
hasProgress && totalDuration > 0
|
|
759
|
+
? totalProgress / totalDuration
|
|
760
|
+
: null,
|
|
699
761
|
lineNumber: node.lineNumber,
|
|
700
762
|
depth,
|
|
701
763
|
});
|