@diagrammo/dgmo 0.8.9 → 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 +191 -191
- package/dist/index.cjs +1318 -724
- 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 +1314 -724
- 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/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/org/renderer.ts
CHANGED
|
@@ -16,20 +16,14 @@ import { parseOrg } from './parser';
|
|
|
16
16
|
import { layoutOrg } from './layout';
|
|
17
17
|
import {
|
|
18
18
|
LEGEND_HEIGHT,
|
|
19
|
-
LEGEND_PILL_PAD,
|
|
20
|
-
LEGEND_PILL_FONT_SIZE,
|
|
21
|
-
LEGEND_CAPSULE_PAD,
|
|
22
|
-
LEGEND_DOT_R,
|
|
23
|
-
LEGEND_ENTRY_FONT_SIZE,
|
|
24
|
-
LEGEND_ENTRY_DOT_GAP,
|
|
25
|
-
LEGEND_ENTRY_TRAIL,
|
|
26
19
|
LEGEND_GROUP_GAP,
|
|
27
20
|
LEGEND_EYE_SIZE,
|
|
28
21
|
LEGEND_EYE_GAP,
|
|
29
22
|
EYE_OPEN_PATH,
|
|
30
23
|
EYE_CLOSED_PATH,
|
|
31
|
-
measureLegendText,
|
|
32
24
|
} from '../utils/legend-constants';
|
|
25
|
+
import { renderLegendD3 } from '../utils/legend-d3';
|
|
26
|
+
import type { LegendConfig, LegendState } from '../utils/legend-types';
|
|
33
27
|
|
|
34
28
|
// ============================================================
|
|
35
29
|
// Constants
|
|
@@ -487,33 +481,17 @@ export function renderOrg(
|
|
|
487
481
|
}
|
|
488
482
|
}
|
|
489
483
|
|
|
490
|
-
// Render legend —
|
|
484
|
+
// Render legend — capsule pills.
|
|
491
485
|
// In app mode (fixedLegend): render at native size outside the scaled group.
|
|
492
486
|
// In export mode: skip legend (unless legend-only chart).
|
|
493
487
|
// Legend-only (no nodes): all groups rendered as expanded capsules inside scaled group.
|
|
494
488
|
if (fixedLegend || legendOnly || (exportDims && hasLegend)) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return group.name.toLowerCase() === activeTagGroup.toLowerCase();
|
|
500
|
-
});
|
|
489
|
+
const groups = layout.legend.map((g) => ({
|
|
490
|
+
name: g.name,
|
|
491
|
+
entries: g.entries.map((e) => ({ value: e.value, color: e.color })),
|
|
492
|
+
}));
|
|
501
493
|
|
|
502
|
-
|
|
503
|
-
let fixedPositions: Map<string, number> | undefined;
|
|
504
|
-
if (fixedLegend && visibleGroups.length > 0) {
|
|
505
|
-
fixedPositions = new Map();
|
|
506
|
-
const effectiveW = (g: (typeof visibleGroups)[0]) =>
|
|
507
|
-
activeTagGroup != null ? g.width : g.minifiedWidth;
|
|
508
|
-
const totalW =
|
|
509
|
-
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
510
|
-
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
511
|
-
let cx = (width - totalW) / 2;
|
|
512
|
-
for (const g of visibleGroups) {
|
|
513
|
-
fixedPositions.set(g.name, cx);
|
|
514
|
-
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
494
|
+
const eyeAddonWidth = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
|
|
517
495
|
|
|
518
496
|
// Choose parent: unscaled group for fixedLegend, contentG for legend-only
|
|
519
497
|
const legendParentBase = fixedLegend
|
|
@@ -521,152 +499,107 @@ export function renderOrg(
|
|
|
521
499
|
.append('g')
|
|
522
500
|
.attr('class', 'org-legend-fixed')
|
|
523
501
|
.attr('transform', `translate(0, ${DIAGRAM_PADDING + titleReserve})`)
|
|
524
|
-
: contentG;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
.
|
|
558
|
-
.
|
|
559
|
-
.attr('height', LEGEND_HEIGHT)
|
|
560
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
561
|
-
.attr('fill', groupBg);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
565
|
-
const pillYOff = LEGEND_CAPSULE_PAD;
|
|
566
|
-
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
567
|
-
|
|
568
|
-
// Pill background
|
|
569
|
-
gEl
|
|
570
|
-
.append('rect')
|
|
571
|
-
.attr('x', pillXOff)
|
|
572
|
-
.attr('y', pillYOff)
|
|
573
|
-
.attr('width', pillWidth)
|
|
574
|
-
.attr('height', pillH)
|
|
575
|
-
.attr('rx', pillH / 2)
|
|
576
|
-
.attr('fill', isActive ? palette.bg : groupBg);
|
|
577
|
-
|
|
578
|
-
// Active pill border
|
|
579
|
-
if (isActive) {
|
|
580
|
-
gEl
|
|
581
|
-
.append('rect')
|
|
582
|
-
.attr('x', pillXOff)
|
|
583
|
-
.attr('y', pillYOff)
|
|
584
|
-
.attr('width', pillWidth)
|
|
585
|
-
.attr('height', pillH)
|
|
586
|
-
.attr('rx', pillH / 2)
|
|
587
|
-
.attr('fill', 'none')
|
|
588
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
589
|
-
.attr('stroke-width', 0.75);
|
|
502
|
+
: contentG.append('g');
|
|
503
|
+
|
|
504
|
+
let legendHandle;
|
|
505
|
+
if (legendOnly) {
|
|
506
|
+
// Legend-only mode: render each group expanded individually at layout positions
|
|
507
|
+
for (const lg of layout.legend) {
|
|
508
|
+
const singleConfig: LegendConfig = {
|
|
509
|
+
groups: [
|
|
510
|
+
{
|
|
511
|
+
name: lg.name,
|
|
512
|
+
entries: lg.entries.map((e) => ({
|
|
513
|
+
value: e.value,
|
|
514
|
+
color: e.color,
|
|
515
|
+
})),
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
519
|
+
mode: 'fixed',
|
|
520
|
+
};
|
|
521
|
+
const singleState: LegendState = { activeGroup: lg.name };
|
|
522
|
+
const groupG = legendParentBase
|
|
523
|
+
.append('g')
|
|
524
|
+
.attr('transform', `translate(${lg.x}, ${lg.y})`);
|
|
525
|
+
renderLegendD3(
|
|
526
|
+
groupG,
|
|
527
|
+
singleConfig,
|
|
528
|
+
singleState,
|
|
529
|
+
palette,
|
|
530
|
+
isDark,
|
|
531
|
+
undefined,
|
|
532
|
+
lg.width
|
|
533
|
+
);
|
|
534
|
+
groupG
|
|
535
|
+
.selectAll('[data-legend-group]')
|
|
536
|
+
.classed('org-legend-group', true);
|
|
590
537
|
}
|
|
538
|
+
legendHandle = null;
|
|
539
|
+
} else {
|
|
540
|
+
const legendConfig: LegendConfig = {
|
|
541
|
+
groups,
|
|
542
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
543
|
+
mode: 'fixed',
|
|
544
|
+
capsulePillAddonWidth: eyeAddonWidth,
|
|
545
|
+
};
|
|
546
|
+
const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
|
|
547
|
+
legendHandle = renderLegendD3(
|
|
548
|
+
legendParentBase,
|
|
549
|
+
legendConfig,
|
|
550
|
+
legendState,
|
|
551
|
+
palette,
|
|
552
|
+
isDark,
|
|
553
|
+
undefined,
|
|
554
|
+
fixedLegend ? width : layout.width
|
|
555
|
+
);
|
|
556
|
+
legendParentBase
|
|
557
|
+
.selectAll('[data-legend-group]')
|
|
558
|
+
.classed('org-legend-group', true);
|
|
559
|
+
}
|
|
591
560
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
.
|
|
598
|
-
.attr('font-weight', '500')
|
|
599
|
-
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
600
|
-
.attr('text-anchor', 'middle')
|
|
601
|
-
.text(pillLabel);
|
|
602
|
-
|
|
603
|
-
// Eye icon for visibility toggle (active only, app mode)
|
|
604
|
-
if (isActive && fixedLegend) {
|
|
605
|
-
const groupKey = group.name.toLowerCase();
|
|
561
|
+
// Inject eye icons into active group capsules (app mode only)
|
|
562
|
+
if (fixedLegend && legendHandle) {
|
|
563
|
+
const computedLayout = legendHandle.getLayout();
|
|
564
|
+
if (computedLayout.activeCapsule?.addonX != null) {
|
|
565
|
+
const capsule = computedLayout.activeCapsule;
|
|
566
|
+
const groupKey = capsule.groupName.toLowerCase();
|
|
606
567
|
const isHidden = hiddenAttributes?.has(groupKey) ?? false;
|
|
607
|
-
const eyeX = pillXOff + pillWidth + LEGEND_EYE_GAP;
|
|
608
|
-
const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
|
|
609
|
-
const hitPad = 6;
|
|
610
568
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
eyeG
|
|
620
|
-
.append('rect')
|
|
621
|
-
.attr('x', eyeX - hitPad)
|
|
622
|
-
.attr('y', eyeY - hitPad)
|
|
623
|
-
.attr('width', LEGEND_EYE_SIZE + hitPad * 2)
|
|
624
|
-
.attr('height', LEGEND_EYE_SIZE + hitPad * 2)
|
|
625
|
-
.attr('fill', 'transparent')
|
|
626
|
-
.attr('pointer-events', 'all');
|
|
627
|
-
|
|
628
|
-
eyeG
|
|
629
|
-
.append('path')
|
|
630
|
-
.attr('d', isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH)
|
|
631
|
-
.attr('transform', `translate(${eyeX}, ${eyeY})`)
|
|
632
|
-
.attr('fill', 'none')
|
|
633
|
-
.attr('stroke', palette.textMuted)
|
|
634
|
-
.attr('stroke-width', 1.2)
|
|
635
|
-
.attr('stroke-linecap', 'round')
|
|
636
|
-
.attr('stroke-linejoin', 'round');
|
|
637
|
-
}
|
|
569
|
+
// Find the rendered active group <g> and append eye icon
|
|
570
|
+
const activeGroupEl = legendParentBase.select(
|
|
571
|
+
`[data-legend-group="${groupKey}"]`
|
|
572
|
+
);
|
|
573
|
+
if (!activeGroupEl.empty()) {
|
|
574
|
+
const eyeX = capsule.addonX!;
|
|
575
|
+
const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
|
|
576
|
+
const hitPad = 6;
|
|
638
577
|
|
|
639
|
-
|
|
640
|
-
if (isActive) {
|
|
641
|
-
const eyeShift = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
|
|
642
|
-
let entryX = pillXOff + pillWidth + 4 + eyeShift;
|
|
643
|
-
for (const entry of group.entries) {
|
|
644
|
-
const entryG = gEl
|
|
578
|
+
const eyeG = activeGroupEl
|
|
645
579
|
.append('g')
|
|
646
|
-
.attr('
|
|
647
|
-
.
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
.
|
|
653
|
-
.attr('
|
|
654
|
-
.attr('
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
.
|
|
662
|
-
.attr('
|
|
663
|
-
.attr('
|
|
664
|
-
.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
LEGEND_ENTRY_TRAIL;
|
|
580
|
+
.attr('class', 'org-legend-eye')
|
|
581
|
+
.attr('data-legend-visibility', groupKey)
|
|
582
|
+
.style('cursor', 'pointer')
|
|
583
|
+
.attr('opacity', isHidden ? 0.4 : 0.7);
|
|
584
|
+
|
|
585
|
+
eyeG
|
|
586
|
+
.append('rect')
|
|
587
|
+
.attr('x', eyeX - hitPad)
|
|
588
|
+
.attr('y', eyeY - hitPad)
|
|
589
|
+
.attr('width', LEGEND_EYE_SIZE + hitPad * 2)
|
|
590
|
+
.attr('height', LEGEND_EYE_SIZE + hitPad * 2)
|
|
591
|
+
.attr('fill', 'transparent')
|
|
592
|
+
.attr('pointer-events', 'all');
|
|
593
|
+
|
|
594
|
+
eyeG
|
|
595
|
+
.append('path')
|
|
596
|
+
.attr('d', isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH)
|
|
597
|
+
.attr('transform', `translate(${eyeX}, ${eyeY})`)
|
|
598
|
+
.attr('fill', 'none')
|
|
599
|
+
.attr('stroke', palette.textMuted)
|
|
600
|
+
.attr('stroke-width', 1.2)
|
|
601
|
+
.attr('stroke-linecap', 'round')
|
|
602
|
+
.attr('stroke-linejoin', 'round');
|
|
670
603
|
}
|
|
671
604
|
}
|
|
672
605
|
}
|
package/src/render.ts
CHANGED
|
@@ -15,11 +15,26 @@ async function ensureDom(): Promise<void> {
|
|
|
15
15
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
|
16
16
|
const win = dom.window;
|
|
17
17
|
|
|
18
|
-
Object.defineProperty(globalThis, 'document', {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Object.defineProperty(globalThis, '
|
|
18
|
+
Object.defineProperty(globalThis, 'document', {
|
|
19
|
+
value: win.document,
|
|
20
|
+
configurable: true,
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(globalThis, 'window', {
|
|
23
|
+
value: win,
|
|
24
|
+
configurable: true,
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
27
|
+
value: win.navigator,
|
|
28
|
+
configurable: true,
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(globalThis, 'HTMLElement', {
|
|
31
|
+
value: win.HTMLElement,
|
|
32
|
+
configurable: true,
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(globalThis, 'SVGElement', {
|
|
35
|
+
value: win.SVGElement,
|
|
36
|
+
configurable: true,
|
|
37
|
+
});
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
/**
|
|
@@ -52,24 +67,39 @@ export async function render(
|
|
|
52
67
|
c4System?: string;
|
|
53
68
|
c4Container?: string;
|
|
54
69
|
tagGroup?: string;
|
|
55
|
-
|
|
70
|
+
/** Legend state for export — controls which tag group is shown in exported SVG. */
|
|
71
|
+
legendState?: { activeGroup?: string; hiddenAttributes?: string[] };
|
|
72
|
+
}
|
|
56
73
|
): Promise<string> {
|
|
57
74
|
const theme = options?.theme ?? 'light';
|
|
58
75
|
const paletteName = options?.palette ?? 'nord';
|
|
59
76
|
const branding = options?.branding ?? false;
|
|
60
77
|
|
|
61
|
-
const paletteColors =
|
|
78
|
+
const paletteColors =
|
|
79
|
+
getPalette(paletteName)[theme === 'dark' ? 'dark' : 'light'];
|
|
62
80
|
|
|
63
81
|
const chartType = parseDgmoChartType(content);
|
|
64
82
|
const category = chartType ? getRenderCategory(chartType) : null;
|
|
65
83
|
|
|
84
|
+
// Build orgExportState from legendState if provided
|
|
85
|
+
const legendExportState = options?.legendState
|
|
86
|
+
? {
|
|
87
|
+
activeTagGroup: options.legendState.activeGroup ?? null,
|
|
88
|
+
hiddenAttributes: options.legendState.hiddenAttributes
|
|
89
|
+
? new Set(options.legendState.hiddenAttributes)
|
|
90
|
+
: undefined,
|
|
91
|
+
}
|
|
92
|
+
: undefined;
|
|
93
|
+
|
|
66
94
|
if (category === 'data-chart') {
|
|
67
|
-
return renderExtendedChartForExport(content, theme, paletteColors, {
|
|
95
|
+
return renderExtendedChartForExport(content, theme, paletteColors, {
|
|
96
|
+
branding,
|
|
97
|
+
});
|
|
68
98
|
}
|
|
69
99
|
|
|
70
100
|
// Visualization/diagram and unknown/null types all go through the unified renderer
|
|
71
101
|
await ensureDom();
|
|
72
|
-
return renderForExport(content, theme, paletteColors,
|
|
102
|
+
return renderForExport(content, theme, paletteColors, legendExportState, {
|
|
73
103
|
branding,
|
|
74
104
|
c4Level: options?.c4Level,
|
|
75
105
|
c4System: options?.c4System,
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -24,18 +24,9 @@ import type {
|
|
|
24
24
|
import { isSequenceBlock, isSequenceSection, isSequenceNote } from './parser';
|
|
25
25
|
import { resolveSequenceTags } from './tag-resolution';
|
|
26
26
|
import type { ResolvedTagMap } from './tag-resolution';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
LEGEND_PILL_FONT_SIZE,
|
|
31
|
-
LEGEND_CAPSULE_PAD,
|
|
32
|
-
LEGEND_DOT_R,
|
|
33
|
-
LEGEND_ENTRY_FONT_SIZE,
|
|
34
|
-
LEGEND_ENTRY_DOT_GAP,
|
|
35
|
-
LEGEND_ENTRY_TRAIL,
|
|
36
|
-
LEGEND_GROUP_GAP,
|
|
37
|
-
measureLegendText,
|
|
38
|
-
} from '../utils/legend-constants';
|
|
27
|
+
import { LEGEND_HEIGHT } from '../utils/legend-constants';
|
|
28
|
+
import { renderLegendD3 } from '../utils/legend-d3';
|
|
29
|
+
import type { LegendConfig, LegendState } from '../utils/legend-types';
|
|
39
30
|
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
|
|
40
31
|
|
|
41
32
|
// ============================================================
|
|
@@ -1598,146 +1589,35 @@ export function renderSequenceDiagram(
|
|
|
1598
1589
|
// Render legend pills for tag groups
|
|
1599
1590
|
if (parsed.tagGroups.length > 0) {
|
|
1600
1591
|
const legendY = TOP_MARGIN + titleOffset;
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
totalWidth: number;
|
|
1611
|
-
entries: Array<{ value: string; color: string }>;
|
|
1612
|
-
}> = [];
|
|
1613
|
-
for (const tg of parsed.tagGroups) {
|
|
1614
|
-
if (tg.entries.length === 0) continue;
|
|
1615
|
-
const isActive =
|
|
1616
|
-
!!activeTagGroup &&
|
|
1617
|
-
tg.name.toLowerCase() === activeTagGroup.toLowerCase();
|
|
1618
|
-
const pillWidth =
|
|
1619
|
-
measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1620
|
-
const entries = tg.entries.map((e) => ({
|
|
1621
|
-
value: e.value,
|
|
1622
|
-
color: resolveColor(e.color) ?? e.color,
|
|
1592
|
+
// Resolve tag colors for legend entries
|
|
1593
|
+
const resolvedGroups = parsed.tagGroups
|
|
1594
|
+
.filter((tg) => tg.entries.length > 0)
|
|
1595
|
+
.map((tg) => ({
|
|
1596
|
+
name: tg.name,
|
|
1597
|
+
entries: tg.entries.map((e) => ({
|
|
1598
|
+
value: e.value,
|
|
1599
|
+
color: resolveColor(e.color) ?? e.color,
|
|
1600
|
+
})),
|
|
1623
1601
|
}));
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
let legendX = (svgWidth - totalLegendWidth) / 2;
|
|
1644
|
-
|
|
1645
|
-
const legendContainer = svg.append('g').attr('class', 'sequence-legend');
|
|
1646
|
-
if (activeTagGroup) {
|
|
1647
|
-
legendContainer.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
for (const item of legendItems) {
|
|
1651
|
-
const gEl = legendContainer
|
|
1652
|
-
.append('g')
|
|
1653
|
-
.attr('transform', `translate(${legendX}, ${legendY})`)
|
|
1654
|
-
.attr('class', 'sequence-legend-group')
|
|
1655
|
-
.attr('data-legend-group', item.group.name.toLowerCase())
|
|
1656
|
-
.style('cursor', 'pointer');
|
|
1657
|
-
|
|
1658
|
-
// Outer capsule background (active only)
|
|
1659
|
-
if (item.isActive) {
|
|
1660
|
-
gEl
|
|
1661
|
-
.append('rect')
|
|
1662
|
-
.attr('width', item.totalWidth)
|
|
1663
|
-
.attr('height', LEGEND_HEIGHT)
|
|
1664
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
1665
|
-
.attr('fill', groupBg);
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
1669
|
-
const pillYOff = LEGEND_CAPSULE_PAD;
|
|
1670
|
-
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
1671
|
-
|
|
1672
|
-
// Pill background
|
|
1673
|
-
gEl
|
|
1674
|
-
.append('rect')
|
|
1675
|
-
.attr('x', pillXOff)
|
|
1676
|
-
.attr('y', pillYOff)
|
|
1677
|
-
.attr('width', item.pillWidth)
|
|
1678
|
-
.attr('height', pillH)
|
|
1679
|
-
.attr('rx', pillH / 2)
|
|
1680
|
-
.attr('fill', item.isActive ? palette.bg : groupBg);
|
|
1681
|
-
|
|
1682
|
-
// Active pill border
|
|
1683
|
-
if (item.isActive) {
|
|
1684
|
-
gEl
|
|
1685
|
-
.append('rect')
|
|
1686
|
-
.attr('x', pillXOff)
|
|
1687
|
-
.attr('y', pillYOff)
|
|
1688
|
-
.attr('width', item.pillWidth)
|
|
1689
|
-
.attr('height', pillH)
|
|
1690
|
-
.attr('rx', pillH / 2)
|
|
1691
|
-
.attr('fill', 'none')
|
|
1692
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
1693
|
-
.attr('stroke-width', 0.75);
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
// Pill text
|
|
1697
|
-
gEl
|
|
1698
|
-
.append('text')
|
|
1699
|
-
.attr('x', pillXOff + item.pillWidth / 2)
|
|
1700
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
1701
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
1702
|
-
.attr('font-weight', '500')
|
|
1703
|
-
.attr('fill', item.isActive ? palette.text : palette.textMuted)
|
|
1704
|
-
.attr('text-anchor', 'middle')
|
|
1705
|
-
.text(item.group.name);
|
|
1706
|
-
|
|
1707
|
-
// Entries inside capsule (active only)
|
|
1708
|
-
if (item.isActive) {
|
|
1709
|
-
let entryX = pillXOff + item.pillWidth + 4;
|
|
1710
|
-
for (const entry of item.entries) {
|
|
1711
|
-
const entryG = gEl
|
|
1712
|
-
.append('g')
|
|
1713
|
-
.attr('data-legend-entry', entry.value.toLowerCase())
|
|
1714
|
-
.style('cursor', 'pointer');
|
|
1715
|
-
|
|
1716
|
-
entryG
|
|
1717
|
-
.append('circle')
|
|
1718
|
-
.attr('cx', entryX + LEGEND_DOT_R)
|
|
1719
|
-
.attr('cy', LEGEND_HEIGHT / 2)
|
|
1720
|
-
.attr('r', LEGEND_DOT_R)
|
|
1721
|
-
.attr('fill', entry.color);
|
|
1722
|
-
|
|
1723
|
-
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
1724
|
-
entryG
|
|
1725
|
-
.append('text')
|
|
1726
|
-
.attr('x', textX)
|
|
1727
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
1728
|
-
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
1729
|
-
.attr('fill', palette.textMuted)
|
|
1730
|
-
.text(entry.value);
|
|
1731
|
-
|
|
1732
|
-
entryX =
|
|
1733
|
-
textX +
|
|
1734
|
-
measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
1735
|
-
LEGEND_ENTRY_TRAIL;
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
legendX += item.totalWidth + LEGEND_GROUP_GAP;
|
|
1740
|
-
}
|
|
1602
|
+
const legendConfig: LegendConfig = {
|
|
1603
|
+
groups: resolvedGroups,
|
|
1604
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
1605
|
+
mode: 'fixed',
|
|
1606
|
+
};
|
|
1607
|
+
const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
|
|
1608
|
+
const legendG = svg
|
|
1609
|
+
.append('g')
|
|
1610
|
+
.attr('class', 'sequence-legend')
|
|
1611
|
+
.attr('transform', `translate(0,${legendY})`);
|
|
1612
|
+
renderLegendD3(
|
|
1613
|
+
legendG,
|
|
1614
|
+
legendConfig,
|
|
1615
|
+
legendState,
|
|
1616
|
+
palette,
|
|
1617
|
+
isDark,
|
|
1618
|
+
undefined,
|
|
1619
|
+
svgWidth
|
|
1620
|
+
);
|
|
1741
1621
|
}
|
|
1742
1622
|
|
|
1743
1623
|
// Render group boxes (behind participant shapes)
|