@diagrammo/dgmo 0.6.2 → 0.7.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/.claude/commands/dgmo.md +231 -13
- package/AGENTS.md +148 -0
- package/dist/cli.cjs +341 -165
- package/dist/index.cjs +4900 -1685
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +259 -18
- package/dist/index.d.ts +259 -18
- package/dist/index.js +4642 -1436
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/c4/layout.ts +0 -5
- package/src/c4/parser.ts +0 -16
- package/src/c4/renderer.ts +7 -11
- package/src/class/layout.ts +0 -1
- package/src/class/parser.ts +28 -0
- package/src/class/renderer.ts +189 -34
- package/src/cli.ts +566 -25
- package/src/colors.ts +3 -3
- package/src/completion.ts +58 -0
- package/src/d3.ts +179 -122
- package/src/dgmo-router.ts +3 -58
- package/src/echarts.ts +96 -55
- package/src/er/parser.ts +30 -1
- package/src/er/renderer.ts +12 -7
- package/src/gantt/calculator.ts +677 -0
- package/src/gantt/parser.ts +761 -0
- package/src/gantt/renderer.ts +2125 -0
- package/src/gantt/resolver.ts +144 -0
- package/src/gantt/types.ts +168 -0
- package/src/graph/flowchart-parser.ts +27 -4
- package/src/graph/flowchart-renderer.ts +1 -2
- package/src/graph/state-parser.ts +0 -1
- package/src/graph/state-renderer.ts +1 -3
- package/src/index.ts +37 -0
- package/src/infra/compute.ts +0 -7
- package/src/infra/layout.ts +0 -2
- package/src/infra/parser.ts +46 -4
- package/src/infra/renderer.ts +49 -27
- package/src/initiative-status/filter.ts +63 -0
- package/src/initiative-status/layout.ts +319 -67
- package/src/initiative-status/parser.ts +200 -25
- package/src/initiative-status/renderer.ts +298 -35
- package/src/initiative-status/types.ts +6 -0
- package/src/kanban/parser.ts +0 -2
- package/src/org/layout.ts +22 -59
- package/src/org/renderer.ts +11 -36
- package/src/palettes/dracula.ts +60 -0
- package/src/palettes/index.ts +8 -6
- package/src/palettes/monokai.ts +60 -0
- package/src/palettes/registry.ts +4 -2
- package/src/sequence/parser.ts +14 -11
- package/src/sequence/renderer.ts +5 -6
- package/src/sequence/tag-resolution.ts +0 -1
- package/src/sharing.ts +8 -0
- package/src/sitemap/layout.ts +1 -14
- package/src/sitemap/parser.ts +1 -2
- package/src/sitemap/renderer.ts +4 -7
- package/src/utils/arrows.ts +7 -7
- package/src/utils/duration.ts +212 -0
- package/src/utils/export-container.ts +40 -0
- package/src/utils/legend-constants.ts +1 -0
|
@@ -5,11 +5,26 @@
|
|
|
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 { runInExportContainer, extractExportSvg } from '../utils/export-container';
|
|
9
|
+
import {
|
|
10
|
+
LEGEND_HEIGHT,
|
|
11
|
+
LEGEND_PILL_PAD,
|
|
12
|
+
LEGEND_PILL_FONT_SIZE,
|
|
13
|
+
LEGEND_PILL_FONT_W,
|
|
14
|
+
LEGEND_CAPSULE_PAD,
|
|
15
|
+
LEGEND_DOT_R,
|
|
16
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
17
|
+
LEGEND_ENTRY_FONT_W,
|
|
18
|
+
LEGEND_ENTRY_DOT_GAP,
|
|
19
|
+
LEGEND_ENTRY_TRAIL,
|
|
20
|
+
LEGEND_GROUP_GAP,
|
|
21
|
+
} from '../utils/legend-constants';
|
|
8
22
|
import { contrastText, mix } from '../palettes/color-utils';
|
|
23
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
9
24
|
import type { PaletteColors } from '../palettes';
|
|
10
25
|
import type { ParsedInitiativeStatus, InitiativeStatus } from './types';
|
|
11
26
|
import type { ParticipantType } from '../sequence/parser';
|
|
12
|
-
import type { ISLayoutResult
|
|
27
|
+
import type { ISLayoutResult } from './layout';
|
|
13
28
|
import { parseInitiativeStatus } from './parser';
|
|
14
29
|
import { layoutInitiativeStatus } from './layout';
|
|
15
30
|
|
|
@@ -66,6 +81,44 @@ function edgeStrokeColor(status: InitiativeStatus, palette: PaletteColors, isDar
|
|
|
66
81
|
return statusColor(status, palette, isDark);
|
|
67
82
|
}
|
|
68
83
|
|
|
84
|
+
// ============================================================
|
|
85
|
+
// Legend helpers
|
|
86
|
+
// ============================================================
|
|
87
|
+
|
|
88
|
+
interface ISLegendEntry {
|
|
89
|
+
label: string;
|
|
90
|
+
statusKey: InitiativeStatus;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const IS_STATUS_LABELS: Record<string, string> = {
|
|
94
|
+
done: 'Done',
|
|
95
|
+
wip: 'In Progress',
|
|
96
|
+
todo: 'To Do',
|
|
97
|
+
na: 'N/A',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const IS_STATUS_ORDER: InitiativeStatus[] = ['todo', 'wip', 'done', 'na'];
|
|
101
|
+
|
|
102
|
+
function collectStatuses(parsed: ParsedInitiativeStatus): ISLegendEntry[] {
|
|
103
|
+
const present = new Set<string>();
|
|
104
|
+
for (const n of parsed.nodes) {
|
|
105
|
+
if (n.status) present.add(n.status);
|
|
106
|
+
}
|
|
107
|
+
return IS_STATUS_ORDER
|
|
108
|
+
.filter((s) => s !== null && present.has(s))
|
|
109
|
+
.map((s) => ({ label: IS_STATUS_LABELS[s!], statusKey: s }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const LEGEND_GROUP_NAME = 'Status';
|
|
113
|
+
|
|
114
|
+
function legendEntriesWidth(entries: ISLegendEntry[]): number {
|
|
115
|
+
let w = 0;
|
|
116
|
+
for (const e of entries) {
|
|
117
|
+
w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
118
|
+
}
|
|
119
|
+
return w;
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
// ============================================================
|
|
70
123
|
// Edge path generator
|
|
71
124
|
// ============================================================
|
|
@@ -409,15 +462,31 @@ function renderNodeShape(
|
|
|
409
462
|
// Main renderer
|
|
410
463
|
// ============================================================
|
|
411
464
|
|
|
465
|
+
export interface ISRenderOptions {
|
|
466
|
+
onClickItem?: (lineNumber: number) => void;
|
|
467
|
+
exportDims?: { width?: number; height?: number };
|
|
468
|
+
legendActive?: boolean | null;
|
|
469
|
+
activeTagGroup?: string | null;
|
|
470
|
+
hiddenTagValues?: Map<string, Set<string>>;
|
|
471
|
+
tagGroups?: TagGroup[];
|
|
472
|
+
}
|
|
473
|
+
|
|
412
474
|
export function renderInitiativeStatus(
|
|
413
475
|
container: HTMLDivElement,
|
|
414
476
|
parsed: ParsedInitiativeStatus,
|
|
415
477
|
layout: ISLayoutResult,
|
|
416
478
|
palette: PaletteColors,
|
|
417
479
|
isDark: boolean,
|
|
418
|
-
|
|
419
|
-
exportDims?: { width?: number; height?: number }
|
|
480
|
+
options?: ISRenderOptions
|
|
420
481
|
): void {
|
|
482
|
+
const {
|
|
483
|
+
onClickItem,
|
|
484
|
+
exportDims,
|
|
485
|
+
legendActive,
|
|
486
|
+
activeTagGroup,
|
|
487
|
+
hiddenTagValues,
|
|
488
|
+
tagGroups,
|
|
489
|
+
} = options ?? {};
|
|
421
490
|
// Clear existing content
|
|
422
491
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
423
492
|
|
|
@@ -425,20 +494,28 @@ export function renderInitiativeStatus(
|
|
|
425
494
|
const height = exportDims?.height ?? container.clientHeight;
|
|
426
495
|
if (width <= 0 || height <= 0) return;
|
|
427
496
|
|
|
497
|
+
const legendEntries = collectStatuses(parsed);
|
|
498
|
+
const hasLegend = legendEntries.length > 1;
|
|
499
|
+
const isLegendExpanded = legendActive !== false;
|
|
500
|
+
|
|
501
|
+
const effectiveTagGroups = tagGroups ?? parsed.tagGroups ?? [];
|
|
502
|
+
const hasTagGroups = effectiveTagGroups.length > 0;
|
|
503
|
+
|
|
428
504
|
const titleHeight = parsed.title ? 40 : 0;
|
|
505
|
+
const LEGEND_FIXED_GAP = 8;
|
|
506
|
+
const legendReserve = (hasLegend || hasTagGroups) ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
429
507
|
|
|
430
508
|
// Scale to fit
|
|
431
509
|
const diagramW = layout.width;
|
|
432
510
|
const diagramH = layout.height;
|
|
433
|
-
const availH = height - titleHeight;
|
|
511
|
+
const availH = height - titleHeight - legendReserve;
|
|
434
512
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
435
513
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
436
514
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
437
515
|
|
|
438
516
|
const scaledW = diagramW * scale;
|
|
439
|
-
const scaledH = diagramH * scale;
|
|
440
517
|
const offsetX = (width - scaledW) / 2;
|
|
441
|
-
const offsetY = titleHeight + DIAGRAM_PADDING;
|
|
518
|
+
const offsetY = titleHeight + legendReserve + DIAGRAM_PADDING;
|
|
442
519
|
|
|
443
520
|
// Create SVG
|
|
444
521
|
const svg = d3Selection
|
|
@@ -498,6 +575,201 @@ export function renderInitiativeStatus(
|
|
|
498
575
|
}
|
|
499
576
|
}
|
|
500
577
|
|
|
578
|
+
// ── Legend ──
|
|
579
|
+
if (hasLegend || hasTagGroups) {
|
|
580
|
+
const groupBg = isDark
|
|
581
|
+
? mix(palette.surface, palette.bg, 50)
|
|
582
|
+
: mix(palette.surface, palette.bg, 30);
|
|
583
|
+
|
|
584
|
+
// Build legend groups: Status + tag groups
|
|
585
|
+
interface LegendGroup {
|
|
586
|
+
name: string;
|
|
587
|
+
key: string; // lowercase key for data attribute
|
|
588
|
+
isStatus: boolean;
|
|
589
|
+
entries: { label: string; color: string; value: string }[];
|
|
590
|
+
width: number; // total width when expanded
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const legendGroups: LegendGroup[] = [];
|
|
594
|
+
|
|
595
|
+
// Status group (always first if entries exist)
|
|
596
|
+
if (hasLegend) {
|
|
597
|
+
const statusEntries = legendEntries.map((e) => ({
|
|
598
|
+
label: e.label,
|
|
599
|
+
color: statusColor(e.statusKey, palette, isDark),
|
|
600
|
+
value: e.statusKey ?? 'na',
|
|
601
|
+
}));
|
|
602
|
+
const pillW = LEGEND_GROUP_NAME.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
603
|
+
const entrW = legendEntriesWidth(legendEntries);
|
|
604
|
+
legendGroups.push({
|
|
605
|
+
name: LEGEND_GROUP_NAME,
|
|
606
|
+
key: 'status',
|
|
607
|
+
isStatus: true,
|
|
608
|
+
entries: statusEntries,
|
|
609
|
+
width: LEGEND_CAPSULE_PAD * 2 + pillW + LEGEND_ENTRY_TRAIL + entrW,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Tag groups
|
|
614
|
+
for (const tg of effectiveTagGroups) {
|
|
615
|
+
const entries = tg.entries.map((e) => ({
|
|
616
|
+
label: e.value,
|
|
617
|
+
color: e.color || palette.textMuted,
|
|
618
|
+
value: e.value.toLowerCase(),
|
|
619
|
+
}));
|
|
620
|
+
const pillW = tg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
621
|
+
let entrW = 0;
|
|
622
|
+
for (const e of entries) {
|
|
623
|
+
entrW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
624
|
+
}
|
|
625
|
+
legendGroups.push({
|
|
626
|
+
name: tg.name,
|
|
627
|
+
key: tg.name.toLowerCase(),
|
|
628
|
+
isStatus: false,
|
|
629
|
+
entries,
|
|
630
|
+
width: LEGEND_CAPSULE_PAD * 2 + pillW + 4 + entrW,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Determine which group is active/expanded
|
|
635
|
+
const activeKey = activeTagGroup?.toLowerCase() ?? null;
|
|
636
|
+
const isStatusExpanded = isLegendExpanded && activeKey === null;
|
|
637
|
+
|
|
638
|
+
// When a tag group is active, only show that group (mutual exclusion).
|
|
639
|
+
// When no tag group is active, show all pills (Status expanded + tag pills minified).
|
|
640
|
+
const visibleLegendGroups = activeKey !== null
|
|
641
|
+
? legendGroups.filter((lg) => !lg.isStatus && lg.key === activeKey)
|
|
642
|
+
: legendGroups;
|
|
643
|
+
|
|
644
|
+
// Compute total legend width
|
|
645
|
+
let totalLegendW = 0;
|
|
646
|
+
for (const lg of visibleLegendGroups) {
|
|
647
|
+
const isActive = lg.isStatus ? isStatusExpanded : (activeKey === lg.key);
|
|
648
|
+
const pillW = lg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
649
|
+
totalLegendW += isActive ? lg.width : pillW;
|
|
650
|
+
totalLegendW += LEGEND_GROUP_GAP;
|
|
651
|
+
}
|
|
652
|
+
totalLegendW -= LEGEND_GROUP_GAP; // remove trailing gap
|
|
653
|
+
|
|
654
|
+
const legendX = (width - totalLegendW) / 2;
|
|
655
|
+
const legendY = titleHeight;
|
|
656
|
+
|
|
657
|
+
const legendRow = svg
|
|
658
|
+
.append('g')
|
|
659
|
+
.attr('class', 'is-legend-row')
|
|
660
|
+
.attr('transform', `translate(${legendX}, ${legendY})`);
|
|
661
|
+
|
|
662
|
+
let cursorX = 0;
|
|
663
|
+
|
|
664
|
+
for (const lg of visibleLegendGroups) {
|
|
665
|
+
const isActive = lg.isStatus ? isStatusExpanded : (activeKey === lg.key);
|
|
666
|
+
const pillW = lg.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
667
|
+
const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
668
|
+
const groupW = isActive ? lg.width : pillW;
|
|
669
|
+
|
|
670
|
+
const gEl = legendRow
|
|
671
|
+
.append('g')
|
|
672
|
+
.attr('transform', `translate(${cursorX}, 0)`)
|
|
673
|
+
.attr('class', 'is-legend-group')
|
|
674
|
+
.attr('data-legend-group', lg.key)
|
|
675
|
+
.style('cursor', 'pointer');
|
|
676
|
+
|
|
677
|
+
if (isActive) {
|
|
678
|
+
// Outer capsule background
|
|
679
|
+
gEl.append('rect')
|
|
680
|
+
.attr('width', groupW)
|
|
681
|
+
.attr('height', LEGEND_HEIGHT)
|
|
682
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
683
|
+
.attr('fill', groupBg);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
687
|
+
const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
688
|
+
|
|
689
|
+
// Pill background
|
|
690
|
+
gEl.append('rect')
|
|
691
|
+
.attr('x', pillXOff)
|
|
692
|
+
.attr('y', pillYOff)
|
|
693
|
+
.attr('width', pillW)
|
|
694
|
+
.attr('height', pillH)
|
|
695
|
+
.attr('rx', pillH / 2)
|
|
696
|
+
.attr('fill', isActive ? palette.bg : groupBg);
|
|
697
|
+
|
|
698
|
+
// Active pill border
|
|
699
|
+
if (isActive) {
|
|
700
|
+
gEl.append('rect')
|
|
701
|
+
.attr('x', pillXOff)
|
|
702
|
+
.attr('y', pillYOff)
|
|
703
|
+
.attr('width', pillW)
|
|
704
|
+
.attr('height', pillH)
|
|
705
|
+
.attr('rx', pillH / 2)
|
|
706
|
+
.attr('fill', 'none')
|
|
707
|
+
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
708
|
+
.attr('stroke-width', 0.75);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Pill text
|
|
712
|
+
gEl.append('text')
|
|
713
|
+
.attr('x', pillXOff + pillW / 2)
|
|
714
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
715
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
716
|
+
.attr('font-weight', '500')
|
|
717
|
+
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
718
|
+
.attr('text-anchor', 'middle')
|
|
719
|
+
.attr('font-family', FONT_FAMILY)
|
|
720
|
+
.text(lg.name);
|
|
721
|
+
|
|
722
|
+
// Entries inside capsule (active only)
|
|
723
|
+
if (isActive) {
|
|
724
|
+
// Determine which values are hidden for this group
|
|
725
|
+
const hiddenSet = !lg.isStatus ? hiddenTagValues?.get(lg.key) : undefined;
|
|
726
|
+
|
|
727
|
+
let entryX = pillXOff + pillW + 4;
|
|
728
|
+
for (const entry of lg.entries) {
|
|
729
|
+
const isHidden = hiddenSet?.has(entry.value) ?? false;
|
|
730
|
+
|
|
731
|
+
const entryG = gEl.append('g')
|
|
732
|
+
.attr('data-legend-entry', entry.value)
|
|
733
|
+
.style('cursor', !lg.isStatus ? 'pointer' : 'default');
|
|
734
|
+
|
|
735
|
+
if (isHidden) {
|
|
736
|
+
// Hidden: hollow ring + dimmed text (strikethrough-like)
|
|
737
|
+
entryG.append('circle')
|
|
738
|
+
.attr('cx', entryX + LEGEND_DOT_R)
|
|
739
|
+
.attr('cy', LEGEND_HEIGHT / 2)
|
|
740
|
+
.attr('r', LEGEND_DOT_R)
|
|
741
|
+
.attr('fill', 'none')
|
|
742
|
+
.attr('stroke', entry.color)
|
|
743
|
+
.attr('stroke-width', 1.2)
|
|
744
|
+
.attr('opacity', 0.5);
|
|
745
|
+
} else {
|
|
746
|
+
// Visible: solid dot
|
|
747
|
+
entryG.append('circle')
|
|
748
|
+
.attr('cx', entryX + LEGEND_DOT_R)
|
|
749
|
+
.attr('cy', LEGEND_HEIGHT / 2)
|
|
750
|
+
.attr('r', LEGEND_DOT_R)
|
|
751
|
+
.attr('fill', entry.color);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
entryG.append('text')
|
|
755
|
+
.attr('x', entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP)
|
|
756
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
757
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
758
|
+
.attr('fill', palette.textMuted)
|
|
759
|
+
.attr('font-family', FONT_FAMILY)
|
|
760
|
+
.attr('opacity', isHidden ? 0.4 : 1)
|
|
761
|
+
.attr('text-decoration', isHidden ? 'line-through' : 'none')
|
|
762
|
+
.text(entry.label);
|
|
763
|
+
|
|
764
|
+
entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
cursorX += groupW + LEGEND_GROUP_GAP;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
}
|
|
772
|
+
|
|
501
773
|
// Content group
|
|
502
774
|
const contentG = svg
|
|
503
775
|
.append('g')
|
|
@@ -768,7 +1040,15 @@ export function renderInitiativeStatus(
|
|
|
768
1040
|
.append('g')
|
|
769
1041
|
.attr('transform', `translate(${node.x}, ${node.y})`)
|
|
770
1042
|
.attr('class', 'is-node')
|
|
771
|
-
.attr('data-line-number', String(node.lineNumber))
|
|
1043
|
+
.attr('data-line-number', String(node.lineNumber))
|
|
1044
|
+
.attr('data-is-status', node.status ?? 'na');
|
|
1045
|
+
|
|
1046
|
+
// Tag data attributes for hover dimming
|
|
1047
|
+
if (node.metadata) {
|
|
1048
|
+
for (const [key, val] of Object.entries(node.metadata)) {
|
|
1049
|
+
nodeG.attr(`data-tag-${key}`, val.toLowerCase());
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
772
1052
|
|
|
773
1053
|
if (onClickItem) {
|
|
774
1054
|
nodeG.style('cursor', 'pointer').on('click', () => {
|
|
@@ -787,14 +1067,16 @@ export function renderInitiativeStatus(
|
|
|
787
1067
|
.attr('fill', 'transparent')
|
|
788
1068
|
.attr('class', 'is-node-hit-area');
|
|
789
1069
|
|
|
1070
|
+
// Always use status coloring regardless of legend state
|
|
790
1071
|
const fill = nodeFill(node.status, palette, isDark);
|
|
791
1072
|
const stroke = nodeStroke(node.status, palette, isDark);
|
|
792
1073
|
renderNodeShape(nodeG, node.shape, node.width, node.height, fill, stroke);
|
|
793
1074
|
|
|
1075
|
+
const textColor = contrastText(fill, '#eceff4', '#2e3440');
|
|
1076
|
+
|
|
794
1077
|
// Label placement: actors put label below the figure, others center inside
|
|
795
1078
|
const isActor = node.shape === 'actor';
|
|
796
1079
|
if (isActor) {
|
|
797
|
-
const textColor = nodeTextColor(node.status, palette, isDark);
|
|
798
1080
|
const fitted = fitTextToNode(node.label, node.width, node.height * 0.35);
|
|
799
1081
|
const labelY = node.height / 2 - fitted.fontSize * 0.3;
|
|
800
1082
|
for (let li = 0; li < fitted.lines.length; li++) {
|
|
@@ -811,7 +1093,6 @@ export function renderInitiativeStatus(
|
|
|
811
1093
|
}
|
|
812
1094
|
} else {
|
|
813
1095
|
const fitted = fitTextToNode(node.label, node.width, node.height);
|
|
814
|
-
const textColor = nodeTextColor(node.status, palette, isDark);
|
|
815
1096
|
const totalTextHeight = fitted.lines.length * fitted.fontSize * 1.3;
|
|
816
1097
|
const startY = -totalTextHeight / 2 + fitted.fontSize * 0.65;
|
|
817
1098
|
|
|
@@ -846,40 +1127,22 @@ export function renderInitiativeStatusForExport(
|
|
|
846
1127
|
const layout = layoutInitiativeStatus(parsed);
|
|
847
1128
|
const isDark = theme === 'dark';
|
|
848
1129
|
|
|
1130
|
+
const legendEntries = collectStatuses(parsed);
|
|
1131
|
+
const EXPORT_LEGEND_GAP = 8;
|
|
1132
|
+
const legendReserve = legendEntries.length > 1 ? LEGEND_HEIGHT + EXPORT_LEGEND_GAP : 0;
|
|
849
1133
|
const titleOffset = parsed.title ? 40 : 0;
|
|
850
1134
|
const exportWidth = layout.width + DIAGRAM_PADDING * 2;
|
|
851
|
-
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + titleOffset;
|
|
1135
|
+
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + titleOffset + legendReserve;
|
|
852
1136
|
|
|
853
|
-
|
|
854
|
-
container.style.width = `${exportWidth}px`;
|
|
855
|
-
container.style.height = `${exportHeight}px`;
|
|
856
|
-
container.style.position = 'absolute';
|
|
857
|
-
container.style.left = '-9999px';
|
|
858
|
-
document.body.appendChild(container);
|
|
859
|
-
|
|
860
|
-
try {
|
|
1137
|
+
return runInExportContainer(exportWidth, exportHeight, (container) => {
|
|
861
1138
|
renderInitiativeStatus(
|
|
862
1139
|
container,
|
|
863
1140
|
parsed,
|
|
864
1141
|
layout,
|
|
865
1142
|
palette,
|
|
866
1143
|
isDark,
|
|
867
|
-
|
|
868
|
-
{ width: exportWidth, height: exportHeight }
|
|
1144
|
+
{ exportDims: { width: exportWidth, height: exportHeight } }
|
|
869
1145
|
);
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (!svgEl) return '';
|
|
873
|
-
|
|
874
|
-
if (theme === 'transparent') {
|
|
875
|
-
svgEl.style.background = 'none';
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
879
|
-
svgEl.style.fontFamily = FONT_FAMILY;
|
|
880
|
-
|
|
881
|
-
return svgEl.outerHTML;
|
|
882
|
-
} finally {
|
|
883
|
-
document.body.removeChild(container);
|
|
884
|
-
}
|
|
1146
|
+
return extractExportSvg(container, theme);
|
|
1147
|
+
});
|
|
885
1148
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { DgmoError } from '../diagnostics';
|
|
6
6
|
import type { ParticipantType } from '../sequence/parser';
|
|
7
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
7
8
|
|
|
8
9
|
export type InitiativeStatus = 'done' | 'wip' | 'todo' | 'na' | null;
|
|
9
10
|
|
|
@@ -14,6 +15,7 @@ export interface ISNode {
|
|
|
14
15
|
status: InitiativeStatus;
|
|
15
16
|
shape: ParticipantType;
|
|
16
17
|
lineNumber: number;
|
|
18
|
+
metadata: Record<string, string>;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface ISEdge {
|
|
@@ -22,6 +24,7 @@ export interface ISEdge {
|
|
|
22
24
|
label?: string; // e.g. "getUser"
|
|
23
25
|
status: InitiativeStatus;
|
|
24
26
|
lineNumber: number;
|
|
27
|
+
metadata: Record<string, string>;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export interface ISGroup {
|
|
@@ -37,7 +40,10 @@ export interface ParsedInitiativeStatus {
|
|
|
37
40
|
nodes: ISNode[];
|
|
38
41
|
edges: ISEdge[];
|
|
39
42
|
groups: ISGroup[];
|
|
43
|
+
tagGroups: TagGroup[];
|
|
40
44
|
options: Record<string, string>;
|
|
45
|
+
/** Initial hidden tag values from `hide:` DSL directive. Map<groupKey, Set<values>> */
|
|
46
|
+
initialHiddenTagValues: Map<string, Set<string>>;
|
|
41
47
|
diagnostics: DgmoError[];
|
|
42
48
|
error: string | null;
|
|
43
49
|
}
|
package/src/kanban/parser.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { PaletteColors } from '../palettes';
|
|
2
|
-
import type { DgmoError } from '../diagnostics';
|
|
3
2
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
4
3
|
import { resolveColor } from '../colors';
|
|
5
4
|
import { matchTagBlockHeading } from '../utils/tag-groups';
|
|
@@ -15,7 +14,6 @@ import type {
|
|
|
15
14
|
KanbanColumn,
|
|
16
15
|
KanbanCard,
|
|
17
16
|
KanbanTagGroup,
|
|
18
|
-
KanbanTagEntry,
|
|
19
17
|
} from './types';
|
|
20
18
|
|
|
21
19
|
// ============================================================
|
package/src/org/layout.ts
CHANGED
|
@@ -81,8 +81,6 @@ export interface OrgLayoutResult {
|
|
|
81
81
|
// ============================================================
|
|
82
82
|
|
|
83
83
|
const CHAR_WIDTH = 7.5;
|
|
84
|
-
const LABEL_FONT_SIZE = 13;
|
|
85
|
-
const META_FONT_SIZE = 11;
|
|
86
84
|
const META_LINE_HEIGHT = 16;
|
|
87
85
|
const HEADER_HEIGHT = 28;
|
|
88
86
|
const SEPARATOR_GAP = 6;
|
|
@@ -1117,8 +1115,6 @@ export function layoutOrg(
|
|
|
1117
1115
|
let finalWidth = totalWidth;
|
|
1118
1116
|
let finalHeight = totalHeight;
|
|
1119
1117
|
|
|
1120
|
-
const legendPosition = parsed.options?.['legend-position'] ?? 'top';
|
|
1121
|
-
|
|
1122
1118
|
// When a tag group is active, only that group is laid out (full size).
|
|
1123
1119
|
// When none is active, all groups are laid out minified — unless
|
|
1124
1120
|
// expandAllLegend is set (export mode), which shows all groups expanded.
|
|
@@ -1128,66 +1124,33 @@ export function layoutOrg(
|
|
|
1128
1124
|
const allExpanded = expandAllLegend && activeTagGroup == null;
|
|
1129
1125
|
const effectiveW = (g: OrgLegendGroup) =>
|
|
1130
1126
|
activeTagGroup != null || allExpanded ? g.width : g.minifiedWidth;
|
|
1131
|
-
const effectiveH = (g: OrgLegendGroup) =>
|
|
1132
|
-
activeTagGroup != null || allExpanded ? g.height : g.minifiedHeight;
|
|
1133
1127
|
|
|
1134
1128
|
if (visibleGroups.length > 0) {
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
const shift = (finalWidth - totalWidth) / 2;
|
|
1145
|
-
for (const n of layoutNodes) n.x += shift;
|
|
1146
|
-
for (const c of containers) c.x += shift;
|
|
1147
|
-
for (const e of layoutEdges) {
|
|
1148
|
-
for (const p of e.points) p.x += shift;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
const contentBottom = totalHeight - MARGIN;
|
|
1153
|
-
const legendY = contentBottom + LEGEND_GAP;
|
|
1154
|
-
const startX = (finalWidth - totalGroupsWidth) / 2;
|
|
1155
|
-
|
|
1156
|
-
let cx = startX;
|
|
1157
|
-
for (const g of visibleGroups) {
|
|
1158
|
-
g.x = cx;
|
|
1159
|
-
g.y = legendY;
|
|
1160
|
-
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT;
|
|
1164
|
-
} else {
|
|
1165
|
-
// Top: horizontal row above chart content, left-aligned
|
|
1166
|
-
const legendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
|
|
1167
|
-
|
|
1168
|
-
// Push all chart content down
|
|
1169
|
-
for (const n of layoutNodes) n.y += legendShift;
|
|
1170
|
-
for (const c of containers) c.y += legendShift;
|
|
1171
|
-
for (const e of layoutEdges) {
|
|
1172
|
-
for (const p of e.points) p.y += legendShift;
|
|
1173
|
-
}
|
|
1129
|
+
// Top: horizontal row above chart content, left-aligned
|
|
1130
|
+
const legendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
|
|
1131
|
+
|
|
1132
|
+
// Push all chart content down
|
|
1133
|
+
for (const n of layoutNodes) n.y += legendShift;
|
|
1134
|
+
for (const c of containers) c.y += legendShift;
|
|
1135
|
+
for (const e of layoutEdges) {
|
|
1136
|
+
for (const p of e.points) p.y += legendShift;
|
|
1137
|
+
}
|
|
1174
1138
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1139
|
+
const totalGroupsWidth =
|
|
1140
|
+
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1141
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1178
1142
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1143
|
+
let cx = MARGIN;
|
|
1144
|
+
for (const g of visibleGroups) {
|
|
1145
|
+
g.x = cx;
|
|
1146
|
+
g.y = MARGIN;
|
|
1147
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1148
|
+
}
|
|
1185
1149
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1150
|
+
finalHeight += legendShift;
|
|
1151
|
+
const neededWidth = totalGroupsWidth + MARGIN * 2;
|
|
1152
|
+
if (neededWidth > finalWidth) {
|
|
1153
|
+
finalWidth = neededWidth;
|
|
1191
1154
|
}
|
|
1192
1155
|
}
|
|
1193
1156
|
|
package/src/org/renderer.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import * as d3Selection from 'd3-selection';
|
|
6
6
|
import { FONT_FAMILY } from '../fonts';
|
|
7
|
+
import { runInExportContainer, extractExportSvg } from '../utils/export-container';
|
|
7
8
|
import type { PaletteColors } from '../palettes';
|
|
8
9
|
import { mix } from '../palettes/color-utils';
|
|
9
10
|
import type { ParsedOrg } from './parser';
|
|
10
|
-
import type { OrgLayoutResult
|
|
11
|
+
import type { OrgLayoutResult } from './layout';
|
|
11
12
|
import { parseOrg } from './parser';
|
|
12
13
|
import { layoutOrg } from './layout';
|
|
13
14
|
import {
|
|
@@ -114,7 +115,6 @@ export function renderOrg(
|
|
|
114
115
|
|
|
115
116
|
const titleOffset = parsed.title ? TITLE_HEIGHT : 0;
|
|
116
117
|
const legendOnly = layout.nodes.length === 0;
|
|
117
|
-
const legendPosition = parsed.options?.['legend-position'] ?? 'bottom';
|
|
118
118
|
const hasLegend = layout.legend.length > 0;
|
|
119
119
|
|
|
120
120
|
// In app mode (not export), render the legend at a fixed size outside the
|
|
@@ -147,10 +147,9 @@ export function renderOrg(
|
|
|
147
147
|
// Center the diagram
|
|
148
148
|
const scaledW = diagramW * scale;
|
|
149
149
|
const offsetX = (width - scaledW) / 2;
|
|
150
|
-
const offsetY =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
: DIAGRAM_PADDING + titleReserve;
|
|
150
|
+
const offsetY = fixedLegend
|
|
151
|
+
? DIAGRAM_PADDING + legendReserve + titleReserve
|
|
152
|
+
: DIAGRAM_PADDING + titleReserve;
|
|
154
153
|
|
|
155
154
|
// Create SVG
|
|
156
155
|
const svg = d3Selection
|
|
@@ -515,9 +514,7 @@ export function renderOrg(
|
|
|
515
514
|
.attr('class', 'org-legend-fixed')
|
|
516
515
|
.attr(
|
|
517
516
|
'transform',
|
|
518
|
-
|
|
519
|
-
? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT})`
|
|
520
|
-
: `translate(0, ${DIAGRAM_PADDING + titleReserve})`
|
|
517
|
+
`translate(0, ${DIAGRAM_PADDING + titleReserve})`
|
|
521
518
|
)
|
|
522
519
|
: contentG;
|
|
523
520
|
const legendParent = legendParentBase;
|
|
@@ -687,38 +684,16 @@ export function renderOrgForExport(
|
|
|
687
684
|
const layout = layoutOrg(parsed, undefined, undefined, exportHidden);
|
|
688
685
|
const isDark = theme === 'dark';
|
|
689
686
|
|
|
690
|
-
// Create offscreen container
|
|
691
|
-
const container = document.createElement('div');
|
|
692
687
|
const titleOffset = parsed.title ? TITLE_HEIGHT : 0;
|
|
693
688
|
const exportWidth = layout.width + DIAGRAM_PADDING * 2;
|
|
694
|
-
const exportHeight =
|
|
695
|
-
layout.height + DIAGRAM_PADDING * 2 + titleOffset;
|
|
689
|
+
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + titleOffset;
|
|
696
690
|
|
|
697
|
-
|
|
698
|
-
container
|
|
699
|
-
container.style.position = 'absolute';
|
|
700
|
-
container.style.left = '-9999px';
|
|
701
|
-
document.body.appendChild(container);
|
|
702
|
-
|
|
703
|
-
try {
|
|
704
|
-
// No hiddenAttributes passed to renderOrg — export never shows eye icons
|
|
691
|
+
// No hiddenAttributes passed to renderOrg — export never shows eye icons
|
|
692
|
+
return runInExportContainer(exportWidth, exportHeight, (container) => {
|
|
705
693
|
renderOrg(container, parsed, layout, palette, isDark, undefined, {
|
|
706
694
|
width: exportWidth,
|
|
707
695
|
height: exportHeight,
|
|
708
696
|
});
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (!svgEl) return '';
|
|
712
|
-
|
|
713
|
-
if (theme === 'transparent') {
|
|
714
|
-
svgEl.style.background = 'none';
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
718
|
-
svgEl.style.fontFamily = FONT_FAMILY;
|
|
719
|
-
|
|
720
|
-
return svgEl.outerHTML;
|
|
721
|
-
} finally {
|
|
722
|
-
document.body.removeChild(container);
|
|
723
|
-
}
|
|
697
|
+
return extractExportSvg(container, theme);
|
|
698
|
+
});
|
|
724
699
|
}
|