@diagrammo/dgmo 0.8.8 → 0.8.10
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/AGENTS.md +3 -0
- package/dist/cli.cjs +181 -179
- package/dist/index.cjs +1425 -933
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +147 -1
- package/dist/index.d.ts +147 -1
- package/dist/index.js +1421 -933
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +28 -2
- package/gallery/fixtures/sitemap-full.dgmo +1 -0
- package/package.json +1 -1
- package/src/boxes-and-lines/layout.ts +48 -8
- package/src/boxes-and-lines/parser.ts +59 -13
- package/src/boxes-and-lines/renderer.ts +33 -137
- package/src/c4/renderer.ts +25 -138
- package/src/class/renderer.ts +185 -186
- package/src/d3.ts +114 -191
- package/src/echarts.ts +99 -214
- package/src/er/renderer.ts +52 -245
- package/src/gantt/renderer.ts +140 -182
- package/src/index.ts +21 -1
- package/src/infra/renderer.ts +91 -244
- package/src/kanban/renderer.ts +22 -129
- package/src/org/renderer.ts +103 -170
- package/src/render.ts +39 -9
- package/src/sequence/renderer.ts +31 -151
- package/src/sitemap/layout.ts +180 -38
- package/src/sitemap/parser.ts +64 -23
- package/src/sitemap/renderer.ts +73 -161
- package/src/utils/legend-constants.ts +6 -0
- package/src/utils/legend-d3.ts +400 -0
- package/src/utils/legend-layout.ts +495 -0
- package/src/utils/legend-svg.ts +26 -0
- package/src/utils/legend-types.ts +169 -0
package/src/er/renderer.ts
CHANGED
|
@@ -9,18 +9,9 @@ import type { PaletteColors } from '../palettes';
|
|
|
9
9
|
import { mix } from '../palettes/color-utils';
|
|
10
10
|
import { getSeriesColors } from '../palettes';
|
|
11
11
|
import { resolveTagColor } from '../utils/tag-groups';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
LEGEND_PILL_FONT_SIZE,
|
|
16
|
-
LEGEND_CAPSULE_PAD,
|
|
17
|
-
LEGEND_DOT_R,
|
|
18
|
-
LEGEND_ENTRY_FONT_SIZE,
|
|
19
|
-
LEGEND_ENTRY_DOT_GAP,
|
|
20
|
-
LEGEND_ENTRY_TRAIL,
|
|
21
|
-
LEGEND_GROUP_GAP,
|
|
22
|
-
measureLegendText,
|
|
23
|
-
} from '../utils/legend-constants';
|
|
12
|
+
import { LEGEND_HEIGHT } from '../utils/legend-constants';
|
|
13
|
+
import { renderLegendD3 } from '../utils/legend-d3';
|
|
14
|
+
import type { LegendConfig, LegendState } from '../utils/legend-types';
|
|
24
15
|
import {
|
|
25
16
|
TITLE_FONT_SIZE,
|
|
26
17
|
TITLE_FONT_WEIGHT,
|
|
@@ -557,96 +548,30 @@ export function renderERDiagram(
|
|
|
557
548
|
|
|
558
549
|
// ── Tag Legend ──
|
|
559
550
|
if (parsed.tagGroups.length > 0) {
|
|
560
|
-
const LEGEND_PILL_H = LEGEND_HEIGHT - 6;
|
|
561
|
-
const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
|
|
562
|
-
const LEGEND_GAP = 8;
|
|
563
|
-
|
|
564
|
-
const legendG = svg.append('g').attr('class', 'er-tag-legend');
|
|
565
|
-
|
|
566
|
-
if (activeTagGroup) {
|
|
567
|
-
legendG.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
let legendX = DIAGRAM_PADDING;
|
|
571
551
|
const legendY = DIAGRAM_PADDING + titleHeight;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
legendX += labelWidth;
|
|
593
|
-
|
|
594
|
-
// Entries
|
|
595
|
-
for (const entry of group.entries) {
|
|
596
|
-
const pillG = groupG
|
|
597
|
-
.append('g')
|
|
598
|
-
.attr('data-legend-entry', entry.value.toLowerCase())
|
|
599
|
-
.style('cursor', 'pointer');
|
|
600
|
-
|
|
601
|
-
// Estimate text width
|
|
602
|
-
const tmpText = legendG
|
|
603
|
-
.append('text')
|
|
604
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
605
|
-
.attr('font-family', FONT_FAMILY)
|
|
606
|
-
.text(entry.value);
|
|
607
|
-
const textW =
|
|
608
|
-
tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
|
|
609
|
-
tmpText.remove();
|
|
610
|
-
|
|
611
|
-
const pillW = textW + LEGEND_PILL_PAD * 2;
|
|
612
|
-
|
|
613
|
-
pillG
|
|
614
|
-
.append('rect')
|
|
615
|
-
.attr('x', legendX)
|
|
616
|
-
.attr('y', legendY)
|
|
617
|
-
.attr('width', pillW)
|
|
618
|
-
.attr('height', LEGEND_PILL_H)
|
|
619
|
-
.attr('rx', LEGEND_PILL_RX)
|
|
620
|
-
.attr('ry', LEGEND_PILL_RX)
|
|
621
|
-
.attr(
|
|
622
|
-
'fill',
|
|
623
|
-
mix(entry.color, isDark ? palette.surface : palette.bg, 25)
|
|
624
|
-
)
|
|
625
|
-
.attr('stroke', entry.color)
|
|
626
|
-
.attr('stroke-width', 1);
|
|
627
|
-
|
|
628
|
-
pillG
|
|
629
|
-
.append('text')
|
|
630
|
-
.attr('x', legendX + pillW / 2)
|
|
631
|
-
.attr('y', legendY + LEGEND_PILL_H / 2)
|
|
632
|
-
.attr('text-anchor', 'middle')
|
|
633
|
-
.attr('dominant-baseline', 'central')
|
|
634
|
-
.attr('fill', palette.text)
|
|
635
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
636
|
-
.attr('font-family', FONT_FAMILY)
|
|
637
|
-
.text(entry.value);
|
|
638
|
-
|
|
639
|
-
legendX += pillW + LEGEND_GAP;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
legendX += LEGEND_GROUP_GAP;
|
|
643
|
-
}
|
|
552
|
+
const legendConfig: LegendConfig = {
|
|
553
|
+
groups: parsed.tagGroups,
|
|
554
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
555
|
+
mode: 'fixed',
|
|
556
|
+
};
|
|
557
|
+
const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
|
|
558
|
+
const legendG = svg
|
|
559
|
+
.append('g')
|
|
560
|
+
.attr('class', 'er-tag-legend')
|
|
561
|
+
.attr('transform', `translate(0,${legendY})`);
|
|
562
|
+
renderLegendD3(
|
|
563
|
+
legendG,
|
|
564
|
+
legendConfig,
|
|
565
|
+
legendState,
|
|
566
|
+
palette,
|
|
567
|
+
isDark,
|
|
568
|
+
undefined,
|
|
569
|
+
viewW
|
|
570
|
+
);
|
|
571
|
+
legendG.selectAll('[data-legend-group]').classed('er-legend-group', true);
|
|
644
572
|
}
|
|
645
573
|
|
|
646
574
|
// ── Semantic Legend ──
|
|
647
|
-
// Rendered when semantic role detection is enabled (no tag groups, no explicit colors).
|
|
648
|
-
// Follows the sequence-legend pattern: one clickable "Role" group pill that expands
|
|
649
|
-
// to show colored-dot entries. Clicking toggles semanticColorsActive on/off.
|
|
650
575
|
if (semanticRoles) {
|
|
651
576
|
const presentRoles = ROLE_ORDER.filter((role) => {
|
|
652
577
|
for (const r of semanticRoles.values()) {
|
|
@@ -656,156 +581,38 @@ export function renderERDiagram(
|
|
|
656
581
|
});
|
|
657
582
|
|
|
658
583
|
if (presentRoles.length > 0) {
|
|
659
|
-
// Measure actual text widths for consistent spacing regardless of character mix.
|
|
660
|
-
// Falls back to a character-count estimate in jsdom/test environments.
|
|
661
|
-
const measureLabelW = (text: string, fontSize: number): number => {
|
|
662
|
-
const dummy = svg
|
|
663
|
-
.append('text')
|
|
664
|
-
.attr('font-size', fontSize)
|
|
665
|
-
.attr('font-family', FONT_FAMILY)
|
|
666
|
-
.attr('visibility', 'hidden')
|
|
667
|
-
.text(text);
|
|
668
|
-
const measured =
|
|
669
|
-
(dummy.node() as SVGTextElement | null)?.getComputedTextLength?.() ??
|
|
670
|
-
0;
|
|
671
|
-
dummy.remove();
|
|
672
|
-
return measured > 0 ? measured : text.length * fontSize * 0.6;
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
const labelWidths = new Map<EntityRole, number>();
|
|
676
|
-
for (const role of presentRoles) {
|
|
677
|
-
labelWidths.set(
|
|
678
|
-
role,
|
|
679
|
-
measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE)
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const groupBg = isDark
|
|
684
|
-
? mix(palette.surface, palette.bg, 50)
|
|
685
|
-
: mix(palette.surface, palette.bg, 30);
|
|
686
|
-
|
|
687
|
-
const groupName = 'Role';
|
|
688
|
-
const pillWidth =
|
|
689
|
-
measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
690
|
-
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
691
|
-
|
|
692
|
-
let totalWidth: number;
|
|
693
|
-
let entriesWidth = 0;
|
|
694
|
-
if (semanticActive) {
|
|
695
|
-
for (const role of presentRoles) {
|
|
696
|
-
entriesWidth +=
|
|
697
|
-
LEGEND_DOT_R * 2 +
|
|
698
|
-
LEGEND_ENTRY_DOT_GAP +
|
|
699
|
-
labelWidths.get(role)! +
|
|
700
|
-
LEGEND_ENTRY_TRAIL;
|
|
701
|
-
}
|
|
702
|
-
totalWidth =
|
|
703
|
-
LEGEND_CAPSULE_PAD * 2 +
|
|
704
|
-
pillWidth +
|
|
705
|
-
LEGEND_ENTRY_TRAIL +
|
|
706
|
-
entriesWidth;
|
|
707
|
-
} else {
|
|
708
|
-
totalWidth = pillWidth;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
const legendX = (viewW - totalWidth) / 2;
|
|
712
584
|
const legendY = DIAGRAM_PADDING + titleHeight;
|
|
713
|
-
|
|
714
|
-
|
|
585
|
+
const semanticGroups = [
|
|
586
|
+
{
|
|
587
|
+
name: 'Role',
|
|
588
|
+
entries: presentRoles.map((role) => ({
|
|
589
|
+
value: ROLE_LABELS[role],
|
|
590
|
+
color: palette.colors[ROLE_COLORS[role]],
|
|
591
|
+
})),
|
|
592
|
+
},
|
|
593
|
+
];
|
|
594
|
+
const legendConfig: LegendConfig = {
|
|
595
|
+
groups: semanticGroups,
|
|
596
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
597
|
+
mode: 'fixed',
|
|
598
|
+
};
|
|
599
|
+
const legendState: LegendState = {
|
|
600
|
+
activeGroup: semanticActive ? 'Role' : null,
|
|
601
|
+
};
|
|
602
|
+
const legendG = svg
|
|
715
603
|
.append('g')
|
|
716
604
|
.attr('class', 'er-semantic-legend')
|
|
717
|
-
.attr('
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
.attr('fill', groupBg);
|
|
729
|
-
|
|
730
|
-
semanticLegendG
|
|
731
|
-
.append('rect')
|
|
732
|
-
.attr('x', LEGEND_CAPSULE_PAD)
|
|
733
|
-
.attr('y', LEGEND_CAPSULE_PAD)
|
|
734
|
-
.attr('width', pillWidth)
|
|
735
|
-
.attr('height', pillH)
|
|
736
|
-
.attr('rx', pillH / 2)
|
|
737
|
-
.attr('fill', palette.bg);
|
|
738
|
-
|
|
739
|
-
semanticLegendG
|
|
740
|
-
.append('rect')
|
|
741
|
-
.attr('x', LEGEND_CAPSULE_PAD)
|
|
742
|
-
.attr('y', LEGEND_CAPSULE_PAD)
|
|
743
|
-
.attr('width', pillWidth)
|
|
744
|
-
.attr('height', pillH)
|
|
745
|
-
.attr('rx', pillH / 2)
|
|
746
|
-
.attr('fill', 'none')
|
|
747
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
748
|
-
.attr('stroke-width', 0.75);
|
|
749
|
-
|
|
750
|
-
semanticLegendG
|
|
751
|
-
.append('text')
|
|
752
|
-
.attr('x', LEGEND_CAPSULE_PAD + pillWidth / 2)
|
|
753
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
754
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
755
|
-
.attr('font-weight', '500')
|
|
756
|
-
.attr('fill', palette.text)
|
|
757
|
-
.attr('text-anchor', 'middle')
|
|
758
|
-
.attr('font-family', FONT_FAMILY)
|
|
759
|
-
.text(groupName);
|
|
760
|
-
|
|
761
|
-
let entryX = LEGEND_CAPSULE_PAD + pillWidth + LEGEND_ENTRY_TRAIL;
|
|
762
|
-
for (const role of presentRoles) {
|
|
763
|
-
const label = ROLE_LABELS[role];
|
|
764
|
-
const roleColor = palette.colors[ROLE_COLORS[role]];
|
|
765
|
-
|
|
766
|
-
const entryG = semanticLegendG
|
|
767
|
-
.append('g')
|
|
768
|
-
.attr('data-legend-entry', role);
|
|
769
|
-
|
|
770
|
-
entryG
|
|
771
|
-
.append('circle')
|
|
772
|
-
.attr('cx', entryX + LEGEND_DOT_R)
|
|
773
|
-
.attr('cy', LEGEND_HEIGHT / 2)
|
|
774
|
-
.attr('r', LEGEND_DOT_R)
|
|
775
|
-
.attr('fill', roleColor);
|
|
776
|
-
|
|
777
|
-
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
778
|
-
entryG
|
|
779
|
-
.append('text')
|
|
780
|
-
.attr('x', textX)
|
|
781
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
782
|
-
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
783
|
-
.attr('fill', palette.textMuted)
|
|
784
|
-
.attr('font-family', FONT_FAMILY)
|
|
785
|
-
.text(label);
|
|
786
|
-
|
|
787
|
-
entryX = textX + labelWidths.get(role)! + LEGEND_ENTRY_TRAIL;
|
|
788
|
-
}
|
|
789
|
-
} else {
|
|
790
|
-
// ── Collapsed: single muted pill, no entries ──
|
|
791
|
-
semanticLegendG
|
|
792
|
-
.append('rect')
|
|
793
|
-
.attr('width', pillWidth)
|
|
794
|
-
.attr('height', LEGEND_HEIGHT)
|
|
795
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
796
|
-
.attr('fill', groupBg);
|
|
797
|
-
|
|
798
|
-
semanticLegendG
|
|
799
|
-
.append('text')
|
|
800
|
-
.attr('x', pillWidth / 2)
|
|
801
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
802
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
803
|
-
.attr('font-weight', '500')
|
|
804
|
-
.attr('fill', palette.textMuted)
|
|
805
|
-
.attr('text-anchor', 'middle')
|
|
806
|
-
.attr('font-family', FONT_FAMILY)
|
|
807
|
-
.text(groupName);
|
|
808
|
-
}
|
|
605
|
+
.attr('transform', `translate(0,${legendY})`);
|
|
606
|
+
renderLegendD3(
|
|
607
|
+
legendG,
|
|
608
|
+
legendConfig,
|
|
609
|
+
legendState,
|
|
610
|
+
palette,
|
|
611
|
+
isDark,
|
|
612
|
+
undefined,
|
|
613
|
+
viewW
|
|
614
|
+
);
|
|
615
|
+
legendG.selectAll('[data-legend-group]').classed('er-legend-group', true);
|
|
809
616
|
}
|
|
810
617
|
}
|
|
811
618
|
}
|
package/src/gantt/renderer.ts
CHANGED
|
@@ -22,6 +22,12 @@ import {
|
|
|
22
22
|
LEGEND_ICON_W,
|
|
23
23
|
measureLegendText,
|
|
24
24
|
} from '../utils/legend-constants';
|
|
25
|
+
import { renderLegendD3 } from '../utils/legend-d3';
|
|
26
|
+
import type {
|
|
27
|
+
LegendConfig,
|
|
28
|
+
LegendState,
|
|
29
|
+
LegendCallbacks,
|
|
30
|
+
} from '../utils/legend-types';
|
|
25
31
|
import {
|
|
26
32
|
TITLE_FONT_SIZE,
|
|
27
33
|
TITLE_FONT_WEIGHT,
|
|
@@ -1893,7 +1899,7 @@ function renderTagLegend(
|
|
|
1893
1899
|
const isSwimlane =
|
|
1894
1900
|
currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
|
|
1895
1901
|
const showIcon = !legendViewMode && tagGroups.length > 0;
|
|
1896
|
-
const iconReserve = showIcon ? LEGEND_ICON_W : 0;
|
|
1902
|
+
const iconReserve = showIcon && isActive ? LEGEND_ICON_W : 0;
|
|
1897
1903
|
const pillW =
|
|
1898
1904
|
measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) +
|
|
1899
1905
|
LEGEND_PILL_PAD +
|
|
@@ -1940,196 +1946,148 @@ function renderTagLegend(
|
|
|
1940
1946
|
|
|
1941
1947
|
let cursorX = 0;
|
|
1942
1948
|
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
const isActive =
|
|
1946
|
-
activeGroupName?.toLowerCase() === group.name.toLowerCase();
|
|
1947
|
-
const isSwimlane =
|
|
1948
|
-
currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
|
|
1949
|
+
// Render tag groups via centralized legend system
|
|
1950
|
+
if (visibleGroups.length > 0) {
|
|
1949
1951
|
const showIcon = !legendViewMode && tagGroups.length > 0;
|
|
1950
1952
|
const iconReserve = showIcon ? LEGEND_ICON_W : 0;
|
|
1951
|
-
const pillW =
|
|
1952
|
-
measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) +
|
|
1953
|
-
LEGEND_PILL_PAD +
|
|
1954
|
-
iconReserve;
|
|
1955
|
-
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
1956
|
-
const groupW = groupWidths[i];
|
|
1957
|
-
|
|
1958
|
-
const gEl = legendRow
|
|
1959
|
-
.append('g')
|
|
1960
|
-
.attr('transform', `translate(${cursorX}, 0)`)
|
|
1961
|
-
.attr('class', 'gantt-tag-legend-group')
|
|
1962
|
-
.attr('data-tag-group', group.name)
|
|
1963
|
-
.attr('data-line-number', String(group.lineNumber))
|
|
1964
|
-
.style('cursor', 'pointer')
|
|
1965
|
-
.on('click', () => {
|
|
1966
|
-
if (onToggle) onToggle(group.name);
|
|
1967
|
-
});
|
|
1968
|
-
|
|
1969
|
-
if (isActive) {
|
|
1970
|
-
// Outer capsule background
|
|
1971
|
-
gEl
|
|
1972
|
-
.append('rect')
|
|
1973
|
-
.attr('width', groupW)
|
|
1974
|
-
.attr('height', LEGEND_HEIGHT)
|
|
1975
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
1976
|
-
.attr('fill', groupBg);
|
|
1977
|
-
}
|
|
1978
1953
|
|
|
1979
|
-
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
.attr('height', pillH)
|
|
1989
|
-
.attr('rx', pillH / 2)
|
|
1990
|
-
.attr('fill', isActive ? palette.bg : groupBg);
|
|
1991
|
-
|
|
1992
|
-
// Active pill border
|
|
1993
|
-
if (isActive) {
|
|
1994
|
-
gEl
|
|
1995
|
-
.append('rect')
|
|
1996
|
-
.attr('x', pillXOff)
|
|
1997
|
-
.attr('y', pillYOff)
|
|
1998
|
-
.attr('width', pillW)
|
|
1999
|
-
.attr('height', pillH)
|
|
2000
|
-
.attr('rx', pillH / 2)
|
|
2001
|
-
.attr('fill', 'none')
|
|
2002
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
2003
|
-
.attr('stroke-width', 0.75);
|
|
2004
|
-
}
|
|
1954
|
+
// Build groups with filtered entries
|
|
1955
|
+
const legendGroups = visibleGroups.map((g) => {
|
|
1956
|
+
const key = g.name.toLowerCase();
|
|
1957
|
+
const entries = filteredEntries.get(key) ?? g.entries;
|
|
1958
|
+
return {
|
|
1959
|
+
name: g.name,
|
|
1960
|
+
entries: entries.map((e) => ({ value: e.value, color: e.color })),
|
|
1961
|
+
};
|
|
1962
|
+
});
|
|
2005
1963
|
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
1964
|
+
const legendConfig: LegendConfig = {
|
|
1965
|
+
groups: legendGroups,
|
|
1966
|
+
position: {
|
|
1967
|
+
placement: 'top-center' as const,
|
|
1968
|
+
titleRelation: 'below-title' as const,
|
|
1969
|
+
},
|
|
1970
|
+
mode: 'fixed' as const,
|
|
1971
|
+
capsulePillAddonWidth: iconReserve,
|
|
1972
|
+
};
|
|
1973
|
+
const legendState: LegendState = { activeGroup: activeGroupName };
|
|
1974
|
+
|
|
1975
|
+
const tagGroupsW =
|
|
1976
|
+
visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) +
|
|
1977
|
+
Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
|
|
1978
|
+
const tagGroupG = legendRow.append('g');
|
|
1979
|
+
|
|
1980
|
+
const legendCallbacks: LegendCallbacks = {
|
|
1981
|
+
onGroupToggle: onToggle,
|
|
1982
|
+
onEntryHover: (groupName, entryValue) => {
|
|
1983
|
+
const tagKey = groupName.toLowerCase();
|
|
1984
|
+
if (entryValue) {
|
|
1985
|
+
const ev = entryValue.toLowerCase();
|
|
1986
|
+
chartG
|
|
1987
|
+
.selectAll<SVGGElement, unknown>('.gantt-task')
|
|
1988
|
+
.each(function () {
|
|
1989
|
+
const el = d3Selection.select(this);
|
|
1990
|
+
el.attr(
|
|
1991
|
+
'opacity',
|
|
1992
|
+
el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
|
|
1993
|
+
);
|
|
1994
|
+
});
|
|
1995
|
+
chartG
|
|
1996
|
+
.selectAll<SVGElement, unknown>('.gantt-milestone')
|
|
1997
|
+
.attr('opacity', FADE_OPACITY);
|
|
1998
|
+
chartG
|
|
1999
|
+
.selectAll<
|
|
2000
|
+
SVGElement,
|
|
2001
|
+
unknown
|
|
2002
|
+
>('.gantt-group-bar, .gantt-group-summary')
|
|
2003
|
+
.attr('opacity', FADE_OPACITY);
|
|
2004
|
+
svg
|
|
2005
|
+
.selectAll<SVGTextElement, unknown>('.gantt-task-label')
|
|
2006
|
+
.each(function () {
|
|
2007
|
+
const el = d3Selection.select(this);
|
|
2008
|
+
el.attr(
|
|
2009
|
+
'opacity',
|
|
2010
|
+
el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
|
|
2011
|
+
);
|
|
2012
|
+
});
|
|
2013
|
+
svg
|
|
2014
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
2015
|
+
.attr('opacity', FADE_OPACITY);
|
|
2016
|
+
svg
|
|
2017
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
2018
|
+
.each(function () {
|
|
2019
|
+
const el = d3Selection.select(this);
|
|
2020
|
+
el.attr(
|
|
2021
|
+
'opacity',
|
|
2022
|
+
el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
|
|
2023
|
+
);
|
|
2024
|
+
});
|
|
2025
|
+
chartG
|
|
2026
|
+
.selectAll<
|
|
2027
|
+
SVGElement,
|
|
2028
|
+
unknown
|
|
2029
|
+
>('.gantt-lane-band, .gantt-lane-accent')
|
|
2030
|
+
.attr('opacity', FADE_OPACITY);
|
|
2031
|
+
} else {
|
|
2032
|
+
if (criticalPathActive) {
|
|
2033
|
+
applyCriticalPathHighlight(svg, chartG);
|
|
2034
|
+
} else {
|
|
2035
|
+
resetHighlightAll(svg, chartG);
|
|
2036
|
+
}
|
|
2033
2037
|
}
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
.text(entry.value);
|
|
2069
|
-
|
|
2070
|
-
// Hover: highlight matching tasks + labels + lane headers, fade others
|
|
2071
|
-
entryG
|
|
2072
|
-
.on('mouseenter', () => {
|
|
2073
|
-
chartG
|
|
2074
|
-
.selectAll<SVGGElement, unknown>('.gantt-task')
|
|
2075
|
-
.each(function () {
|
|
2076
|
-
const el = d3Selection.select(this);
|
|
2077
|
-
const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
|
|
2078
|
-
el.attr('opacity', matches ? 1 : FADE_OPACITY);
|
|
2079
|
-
});
|
|
2080
|
-
chartG
|
|
2081
|
-
.selectAll<SVGElement, unknown>('.gantt-milestone')
|
|
2082
|
-
.attr('opacity', FADE_OPACITY);
|
|
2083
|
-
chartG
|
|
2084
|
-
.selectAll<
|
|
2085
|
-
SVGElement,
|
|
2086
|
-
unknown
|
|
2087
|
-
>('.gantt-group-bar, .gantt-group-summary')
|
|
2088
|
-
.attr('opacity', FADE_OPACITY);
|
|
2089
|
-
// Fade left-side task labels
|
|
2090
|
-
svg
|
|
2091
|
-
.selectAll<SVGTextElement, unknown>('.gantt-task-label')
|
|
2092
|
-
.each(function () {
|
|
2093
|
-
const el = d3Selection.select(this);
|
|
2094
|
-
const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
|
|
2095
|
-
el.attr('opacity', matches ? 1 : FADE_OPACITY);
|
|
2096
|
-
});
|
|
2097
|
-
// Fade group labels
|
|
2098
|
-
svg
|
|
2099
|
-
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
2100
|
-
.attr('opacity', FADE_OPACITY);
|
|
2101
|
-
// Fade non-matching lane headers + bands + accents
|
|
2102
|
-
svg
|
|
2103
|
-
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
2104
|
-
.each(function () {
|
|
2105
|
-
const el = d3Selection.select(this);
|
|
2106
|
-
const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
|
|
2107
|
-
el.attr('opacity', matches ? 1 : FADE_OPACITY);
|
|
2108
|
-
});
|
|
2109
|
-
chartG
|
|
2110
|
-
.selectAll<
|
|
2111
|
-
SVGElement,
|
|
2112
|
-
unknown
|
|
2113
|
-
>('.gantt-lane-band, .gantt-lane-accent')
|
|
2114
|
-
.attr('opacity', FADE_OPACITY);
|
|
2115
|
-
})
|
|
2116
|
-
.on('mouseleave', () => {
|
|
2117
|
-
if (criticalPathActive) {
|
|
2118
|
-
applyCriticalPathHighlight(svg, chartG);
|
|
2119
|
-
} else {
|
|
2120
|
-
resetHighlightAll(svg, chartG);
|
|
2038
|
+
},
|
|
2039
|
+
onGroupRendered: (groupName, groupEl, _isActive) => {
|
|
2040
|
+
// Add swimlane icon and data attributes
|
|
2041
|
+
const group = visibleGroups.find((g) => g.name === groupName);
|
|
2042
|
+
if (group) {
|
|
2043
|
+
groupEl
|
|
2044
|
+
.attr('data-tag-group', group.name)
|
|
2045
|
+
.attr('data-line-number', String(group.lineNumber));
|
|
2046
|
+
}
|
|
2047
|
+
if (showIcon && _isActive) {
|
|
2048
|
+
const isSwimlane =
|
|
2049
|
+
currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase();
|
|
2050
|
+
const textW =
|
|
2051
|
+
measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) +
|
|
2052
|
+
LEGEND_PILL_PAD;
|
|
2053
|
+
const pillXOff = LEGEND_CAPSULE_PAD;
|
|
2054
|
+
const iconX = pillXOff + textW + 3;
|
|
2055
|
+
const iconY = (LEGEND_HEIGHT - 10) / 2;
|
|
2056
|
+
const iconEl = drawSwimlaneIcon(
|
|
2057
|
+
groupEl,
|
|
2058
|
+
iconX,
|
|
2059
|
+
iconY,
|
|
2060
|
+
isSwimlane,
|
|
2061
|
+
palette
|
|
2062
|
+
);
|
|
2063
|
+
iconEl.append('title').text(`Group by ${groupName}`);
|
|
2064
|
+
iconEl.style('cursor', 'pointer').on('click', (event: Event) => {
|
|
2065
|
+
event.stopPropagation();
|
|
2066
|
+
if (onSwimlaneChange) {
|
|
2067
|
+
onSwimlaneChange(
|
|
2068
|
+
currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase()
|
|
2069
|
+
? null
|
|
2070
|
+
: groupName
|
|
2071
|
+
);
|
|
2121
2072
|
}
|
|
2122
2073
|
});
|
|
2074
|
+
}
|
|
2075
|
+
},
|
|
2076
|
+
};
|
|
2123
2077
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2078
|
+
renderLegendD3(
|
|
2079
|
+
tagGroupG,
|
|
2080
|
+
legendConfig,
|
|
2081
|
+
legendState,
|
|
2082
|
+
palette,
|
|
2083
|
+
isDark,
|
|
2084
|
+
legendCallbacks,
|
|
2085
|
+
tagGroupsW
|
|
2086
|
+
);
|
|
2131
2087
|
|
|
2132
|
-
|
|
2088
|
+
for (let i = 0; i < visibleGroups.length; i++) {
|
|
2089
|
+
cursorX += groupWidths[i] + LEGEND_GROUP_GAP;
|
|
2090
|
+
}
|
|
2133
2091
|
}
|
|
2134
2092
|
|
|
2135
2093
|
// Critical Path pill
|