@diagrammo/dgmo 0.7.3 → 0.8.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/AGENTS.md +15 -20
- package/README.md +56 -58
- package/dist/cli.cjs +188 -181
- package/dist/index.cjs +3506 -1057
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -43
- package/dist/index.d.ts +196 -43
- package/dist/index.js +3493 -1057
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +629 -289
- package/package.json +1 -1
- package/src/c4/layout.ts +6 -9
- package/src/c4/parser.ts +189 -83
- package/src/c4/renderer.ts +8 -9
- package/src/chart.ts +296 -83
- package/src/class/parser.ts +54 -37
- package/src/class/renderer.ts +8 -8
- package/src/cli.ts +8 -8
- package/src/colors.ts +4 -1
- package/src/completion.ts +757 -10
- package/src/d3.ts +310 -73
- package/src/dgmo-router.ts +63 -8
- package/src/echarts.ts +726 -231
- package/src/er/parser.ts +94 -76
- package/src/er/renderer.ts +6 -5
- package/src/gantt/parser.ts +144 -69
- package/src/gantt/renderer.ts +50 -14
- package/src/gantt/types.ts +3 -3
- package/src/graph/flowchart-parser.ts +97 -37
- package/src/graph/flowchart-renderer.ts +4 -3
- package/src/graph/state-parser.ts +50 -31
- package/src/graph/state-renderer.ts +4 -3
- package/src/index.ts +14 -5
- package/src/infra/compute.ts +1 -0
- package/src/infra/layout.ts +3 -0
- package/src/infra/parser.ts +291 -92
- package/src/infra/renderer.ts +172 -30
- package/src/infra/types.ts +5 -0
- package/src/initiative-status/layout.ts +1 -1
- package/src/initiative-status/parser.ts +121 -47
- package/src/initiative-status/renderer.ts +42 -23
- package/src/initiative-status/types.ts +10 -2
- package/src/kanban/parser.ts +60 -37
- package/src/kanban/renderer.ts +2 -2
- package/src/kanban/types.ts +1 -0
- package/src/org/layout.ts +9 -9
- package/src/org/parser.ts +39 -40
- package/src/org/renderer.ts +5 -6
- package/src/org/resolver.ts +26 -19
- package/src/render.ts +1 -1
- package/src/sequence/parser.ts +304 -95
- package/src/sequence/renderer.ts +9 -9
- package/src/sitemap/layout.ts +3 -4
- package/src/sitemap/parser.ts +57 -49
- package/src/sitemap/renderer.ts +6 -7
- package/src/utils/arrows.ts +25 -6
- package/src/utils/duration.ts +43 -7
- package/src/utils/legend-constants.ts +26 -0
- package/src/utils/legend-svg.ts +167 -0
- package/src/utils/parsing.ts +247 -7
- package/src/utils/tag-groups.ts +160 -15
- package/src/utils/title-constants.ts +9 -0
package/src/infra/renderer.ts
CHANGED
|
@@ -18,15 +18,15 @@ import {
|
|
|
18
18
|
LEGEND_HEIGHT,
|
|
19
19
|
LEGEND_PILL_PAD,
|
|
20
20
|
LEGEND_PILL_FONT_SIZE,
|
|
21
|
-
LEGEND_PILL_FONT_W,
|
|
22
21
|
LEGEND_CAPSULE_PAD,
|
|
23
22
|
LEGEND_DOT_R,
|
|
24
23
|
LEGEND_ENTRY_FONT_SIZE,
|
|
25
|
-
LEGEND_ENTRY_FONT_W,
|
|
26
24
|
LEGEND_ENTRY_DOT_GAP,
|
|
27
25
|
LEGEND_ENTRY_TRAIL,
|
|
28
26
|
LEGEND_GROUP_GAP,
|
|
27
|
+
measureLegendText,
|
|
29
28
|
} from '../utils/legend-constants';
|
|
29
|
+
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y, TITLE_OFFSET } from '../utils/title-constants';
|
|
30
30
|
|
|
31
31
|
// ============================================================
|
|
32
32
|
// Constants
|
|
@@ -49,6 +49,9 @@ const COLLAPSE_BAR_HEIGHT = 6;
|
|
|
49
49
|
const COLLAPSE_BAR_INSET = 0;
|
|
50
50
|
|
|
51
51
|
const LEGEND_FIXED_GAP = 16; // gap between fixed legend and scaled diagram — local, not shared
|
|
52
|
+
const SPEED_BADGE_H_PAD = 5; // horizontal padding inside active speed badge
|
|
53
|
+
const SPEED_BADGE_V_PAD = 3; // vertical padding inside active speed badge
|
|
54
|
+
const SPEED_BADGE_GAP = 6; // gap between speed option slots
|
|
52
55
|
|
|
53
56
|
// Health colors (from UX spec)
|
|
54
57
|
const COLOR_HEALTHY = '#22c55e';
|
|
@@ -930,6 +933,7 @@ function renderEdgePaths(
|
|
|
930
933
|
isDark: boolean,
|
|
931
934
|
animate: boolean,
|
|
932
935
|
direction: 'LR' | 'TB',
|
|
936
|
+
speedMultiplier: number = 1,
|
|
933
937
|
) {
|
|
934
938
|
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
935
939
|
const maxRps = Math.max(...edges.map((e) => e.computedRps), 1);
|
|
@@ -954,14 +958,18 @@ function renderEdgePaths(
|
|
|
954
958
|
.attr('class', 'infra-edge')
|
|
955
959
|
.attr('data-line-number', edge.lineNumber);
|
|
956
960
|
|
|
957
|
-
edgeG.append('path')
|
|
961
|
+
const edgePath = edgeG.append('path')
|
|
958
962
|
.attr('d', pathD)
|
|
959
963
|
.attr('fill', 'none')
|
|
960
964
|
.attr('stroke', color)
|
|
961
965
|
.attr('stroke-width', strokeW);
|
|
966
|
+
if (edge.async) {
|
|
967
|
+
edgePath.attr('stroke-dasharray', '6 4');
|
|
968
|
+
}
|
|
962
969
|
|
|
963
970
|
if (animate && edge.computedRps > 0) {
|
|
964
|
-
const
|
|
971
|
+
const baseDur = flowDuration(edge.computedRps, maxRps);
|
|
972
|
+
const dur = speedMultiplier > 0 ? baseDur / speedMultiplier : baseDur;
|
|
965
973
|
|
|
966
974
|
// Particles traveling along the path — always green (overloaded nodes
|
|
967
975
|
// already have red styling + reject particles to show the problem)
|
|
@@ -1490,6 +1498,7 @@ function computeRejectedRps(node: InfraLayoutNode): number {
|
|
|
1490
1498
|
function renderRejectParticles(
|
|
1491
1499
|
svg: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
1492
1500
|
nodes: InfraLayoutNode[],
|
|
1501
|
+
speedMultiplier: number = 1,
|
|
1493
1502
|
) {
|
|
1494
1503
|
// Compute max rejected RPS across all nodes for scaling
|
|
1495
1504
|
const rejectMap: { node: InfraLayoutNode; rejected: number }[] = [];
|
|
@@ -1504,7 +1513,8 @@ function renderRejectParticles(
|
|
|
1504
1513
|
for (const { node, rejected } of rejectMap) {
|
|
1505
1514
|
const t = Math.min(rejected / maxRejected, 1);
|
|
1506
1515
|
const count = Math.round(REJECT_COUNT_MIN + t * (REJECT_COUNT_MAX - REJECT_COUNT_MIN));
|
|
1507
|
-
const
|
|
1516
|
+
const baseDur = REJECT_DURATION_MAX - t * (REJECT_DURATION_MAX - REJECT_DURATION_MIN);
|
|
1517
|
+
const dur = speedMultiplier > 0 ? baseDur / speedMultiplier : baseDur;
|
|
1508
1518
|
|
|
1509
1519
|
const nodeBottom = node.y + node.height / 2;
|
|
1510
1520
|
|
|
@@ -1593,10 +1603,10 @@ export function computeInfraLegendGroups(
|
|
|
1593
1603
|
color: r.color,
|
|
1594
1604
|
key: r.name.toLowerCase().replace(/\s+/g, '-'),
|
|
1595
1605
|
}));
|
|
1596
|
-
const pillWidth = 'Capabilities'
|
|
1606
|
+
const pillWidth = measureLegendText('Capabilities', LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1597
1607
|
let entriesWidth = 0;
|
|
1598
1608
|
for (const e of entries) {
|
|
1599
|
-
entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.value
|
|
1609
|
+
entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
|
|
1600
1610
|
}
|
|
1601
1611
|
groups.push({
|
|
1602
1612
|
name: 'Capabilities',
|
|
@@ -1614,16 +1624,16 @@ export function computeInfraLegendGroups(
|
|
|
1614
1624
|
if (tv.color) {
|
|
1615
1625
|
entries.push({
|
|
1616
1626
|
value: tv.name,
|
|
1617
|
-
color: resolveColor(tv.color, palette),
|
|
1627
|
+
color: resolveColor(tv.color, palette) ?? tv.color,
|
|
1618
1628
|
key: tv.name.toLowerCase(),
|
|
1619
1629
|
});
|
|
1620
1630
|
}
|
|
1621
1631
|
}
|
|
1622
1632
|
if (entries.length === 0) continue;
|
|
1623
|
-
const pillWidth = tg.name
|
|
1633
|
+
const pillWidth = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1624
1634
|
let entriesWidth = 0;
|
|
1625
1635
|
for (const e of entries) {
|
|
1626
|
-
entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.value
|
|
1636
|
+
entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
|
|
1627
1637
|
}
|
|
1628
1638
|
groups.push({
|
|
1629
1639
|
name: tg.name,
|
|
@@ -1638,6 +1648,20 @@ export function computeInfraLegendGroups(
|
|
|
1638
1648
|
return groups;
|
|
1639
1649
|
}
|
|
1640
1650
|
|
|
1651
|
+
/** Compute total width for the playback pill (speed only). */
|
|
1652
|
+
function computePlaybackWidth(playback: InfraPlaybackState | undefined): number {
|
|
1653
|
+
if (!playback) return 0;
|
|
1654
|
+
const pillWidth = measureLegendText('Playback', LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1655
|
+
if (!playback.expanded) return pillWidth;
|
|
1656
|
+
|
|
1657
|
+
let entriesW = 8; // gap after pill
|
|
1658
|
+
entriesW += LEGEND_PILL_FONT_SIZE * 0.8 + 6; // play/pause
|
|
1659
|
+
for (const s of playback.speedOptions) {
|
|
1660
|
+
entriesW += measureLegendText(`${s}x`, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
|
|
1661
|
+
}
|
|
1662
|
+
return LEGEND_CAPSULE_PAD * 2 + pillWidth + entriesW;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1641
1665
|
function renderLegend(
|
|
1642
1666
|
rootSvg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1643
1667
|
legendGroups: InfraLegendGroup[],
|
|
@@ -1646,8 +1670,9 @@ function renderLegend(
|
|
|
1646
1670
|
palette: PaletteColors,
|
|
1647
1671
|
isDark: boolean,
|
|
1648
1672
|
activeGroup: string | null,
|
|
1673
|
+
playback?: InfraPlaybackState,
|
|
1649
1674
|
) {
|
|
1650
|
-
if (legendGroups.length === 0) return;
|
|
1675
|
+
if (legendGroups.length === 0 && !playback) return;
|
|
1651
1676
|
|
|
1652
1677
|
const legendG = rootSvg.append('g')
|
|
1653
1678
|
.attr('transform', `translate(0, ${legendY})`);
|
|
@@ -1659,8 +1684,11 @@ function renderLegend(
|
|
|
1659
1684
|
// Compute centered positions
|
|
1660
1685
|
const effectiveW = (g: InfraLegendGroup) =>
|
|
1661
1686
|
activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
|
|
1687
|
+
const playbackW = computePlaybackWidth(playback);
|
|
1688
|
+
const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP : 0;
|
|
1662
1689
|
const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0)
|
|
1663
|
-
+ (legendGroups.length - 1) * LEGEND_GROUP_GAP
|
|
1690
|
+
+ (legendGroups.length - 1) * LEGEND_GROUP_GAP
|
|
1691
|
+
+ trailingGaps + playbackW;
|
|
1664
1692
|
let cursorX = (totalWidth - totalLegendW) / 2;
|
|
1665
1693
|
|
|
1666
1694
|
for (const group of legendGroups) {
|
|
@@ -1671,7 +1699,7 @@ function renderLegend(
|
|
|
1671
1699
|
: mix(palette.surface, palette.bg, 30);
|
|
1672
1700
|
|
|
1673
1701
|
const pillLabel = group.name;
|
|
1674
|
-
const pillWidth = pillLabel
|
|
1702
|
+
const pillWidth = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1675
1703
|
|
|
1676
1704
|
const gEl = legendG
|
|
1677
1705
|
.append('g')
|
|
@@ -1754,19 +1782,131 @@ function renderLegend(
|
|
|
1754
1782
|
.attr('fill', palette.textMuted)
|
|
1755
1783
|
.text(entry.value);
|
|
1756
1784
|
|
|
1757
|
-
entryX = textX + entry.value
|
|
1785
|
+
entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
|
|
1758
1786
|
}
|
|
1759
1787
|
}
|
|
1760
1788
|
|
|
1761
1789
|
cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
|
|
1762
1790
|
}
|
|
1763
1791
|
|
|
1792
|
+
// Playback pill — speed + pause only
|
|
1793
|
+
if (playback) {
|
|
1794
|
+
const isExpanded = playback.expanded;
|
|
1795
|
+
const groupBg = isDark
|
|
1796
|
+
? mix(palette.bg, palette.text, 85)
|
|
1797
|
+
: mix(palette.bg, palette.text, 92);
|
|
1798
|
+
|
|
1799
|
+
const pillLabel = 'Playback';
|
|
1800
|
+
const pillWidth = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1801
|
+
const fullW = computePlaybackWidth(playback);
|
|
1802
|
+
|
|
1803
|
+
const pbG = legendG
|
|
1804
|
+
.append('g')
|
|
1805
|
+
.attr('transform', `translate(${cursorX}, 0)`)
|
|
1806
|
+
.attr('class', 'infra-legend-group infra-playback-pill')
|
|
1807
|
+
.style('cursor', 'pointer');
|
|
1808
|
+
|
|
1809
|
+
if (isExpanded) {
|
|
1810
|
+
pbG.append('rect')
|
|
1811
|
+
.attr('width', fullW)
|
|
1812
|
+
.attr('height', LEGEND_HEIGHT)
|
|
1813
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
1814
|
+
.attr('fill', groupBg);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
|
|
1818
|
+
const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
|
|
1819
|
+
const pillH = LEGEND_HEIGHT - (isExpanded ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
1820
|
+
|
|
1821
|
+
pbG.append('rect')
|
|
1822
|
+
.attr('x', pillXOff).attr('y', pillYOff)
|
|
1823
|
+
.attr('width', pillWidth).attr('height', pillH)
|
|
1824
|
+
.attr('rx', pillH / 2)
|
|
1825
|
+
.attr('fill', isExpanded ? palette.bg : groupBg);
|
|
1826
|
+
|
|
1827
|
+
if (isExpanded) {
|
|
1828
|
+
pbG.append('rect')
|
|
1829
|
+
.attr('x', pillXOff).attr('y', pillYOff)
|
|
1830
|
+
.attr('width', pillWidth).attr('height', pillH)
|
|
1831
|
+
.attr('rx', pillH / 2)
|
|
1832
|
+
.attr('fill', 'none')
|
|
1833
|
+
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
1834
|
+
.attr('stroke-width', 0.75);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
pbG.append('text')
|
|
1838
|
+
.attr('x', pillXOff + pillWidth / 2)
|
|
1839
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
1840
|
+
.attr('font-family', FONT_FAMILY)
|
|
1841
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
1842
|
+
.attr('font-weight', '500')
|
|
1843
|
+
.attr('fill', isExpanded ? palette.text : palette.textMuted)
|
|
1844
|
+
.attr('text-anchor', 'middle')
|
|
1845
|
+
.text(pillLabel);
|
|
1846
|
+
|
|
1847
|
+
if (isExpanded) {
|
|
1848
|
+
let entryX = pillXOff + pillWidth + 8;
|
|
1849
|
+
const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
|
|
1850
|
+
|
|
1851
|
+
const ppLabel = playback.paused ? '▶' : '⏸';
|
|
1852
|
+
pbG.append('text')
|
|
1853
|
+
.attr('x', entryX).attr('y', entryY)
|
|
1854
|
+
.attr('font-family', FONT_FAMILY)
|
|
1855
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
1856
|
+
.attr('fill', palette.textMuted)
|
|
1857
|
+
.attr('data-playback-action', 'toggle-pause')
|
|
1858
|
+
.style('cursor', 'pointer')
|
|
1859
|
+
.text(ppLabel);
|
|
1860
|
+
entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
|
|
1861
|
+
|
|
1862
|
+
for (const s of playback.speedOptions) {
|
|
1863
|
+
const label = `${s}x`;
|
|
1864
|
+
const isActive = playback.speed === s;
|
|
1865
|
+
const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
|
|
1866
|
+
const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
|
|
1867
|
+
const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
|
|
1868
|
+
|
|
1869
|
+
const speedG = pbG.append('g')
|
|
1870
|
+
.attr('data-playback-action', 'set-speed')
|
|
1871
|
+
.attr('data-playback-value', String(s))
|
|
1872
|
+
.style('cursor', 'pointer');
|
|
1873
|
+
|
|
1874
|
+
speedG.append('rect')
|
|
1875
|
+
.attr('x', entryX)
|
|
1876
|
+
.attr('y', badgeY)
|
|
1877
|
+
.attr('width', slotW)
|
|
1878
|
+
.attr('height', badgeH)
|
|
1879
|
+
.attr('rx', badgeH / 2)
|
|
1880
|
+
.attr('fill', isActive ? palette.primary : 'transparent');
|
|
1881
|
+
|
|
1882
|
+
speedG.append('text')
|
|
1883
|
+
.attr('x', entryX + slotW / 2).attr('y', entryY)
|
|
1884
|
+
.attr('font-family', FONT_FAMILY)
|
|
1885
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
1886
|
+
.attr('font-weight', isActive ? '600' : '400')
|
|
1887
|
+
.attr('fill', isActive ? palette.bg : palette.textMuted)
|
|
1888
|
+
.attr('text-anchor', 'middle')
|
|
1889
|
+
.text(label);
|
|
1890
|
+
entryX += slotW + SPEED_BADGE_GAP;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
cursorX += fullW + LEGEND_GROUP_GAP;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1764
1897
|
}
|
|
1765
1898
|
|
|
1766
1899
|
// ============================================================
|
|
1767
1900
|
// Main render
|
|
1768
1901
|
// ============================================================
|
|
1769
1902
|
|
|
1903
|
+
export interface InfraPlaybackState {
|
|
1904
|
+
expanded: boolean;
|
|
1905
|
+
paused: boolean;
|
|
1906
|
+
speed: number;
|
|
1907
|
+
speedOptions: readonly number[];
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1770
1910
|
export function renderInfra(
|
|
1771
1911
|
container: HTMLDivElement,
|
|
1772
1912
|
layout: InfraLayoutResult,
|
|
@@ -1777,7 +1917,7 @@ export function renderInfra(
|
|
|
1777
1917
|
tagGroups?: InfraTagGroup[],
|
|
1778
1918
|
activeGroup?: string | null,
|
|
1779
1919
|
animate?: boolean,
|
|
1780
|
-
|
|
1920
|
+
playback?: InfraPlaybackState | null,
|
|
1781
1921
|
expandedNodeIds?: Set<string> | null,
|
|
1782
1922
|
exportMode?: boolean,
|
|
1783
1923
|
collapsedNodes?: Set<string> | null,
|
|
@@ -1787,12 +1927,12 @@ export function renderInfra(
|
|
|
1787
1927
|
|
|
1788
1928
|
// Build legend groups
|
|
1789
1929
|
const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette, layout.edges);
|
|
1790
|
-
const hasLegend = legendGroups.length > 0;
|
|
1930
|
+
const hasLegend = legendGroups.length > 0 || !!playback;
|
|
1791
1931
|
// In app mode (not export), legend is rendered as a separate fixed-size SVG
|
|
1792
1932
|
const fixedLegend = !exportMode && hasLegend;
|
|
1793
1933
|
const legendOffset = hasLegend && !fixedLegend ? LEGEND_HEIGHT : 0;
|
|
1794
1934
|
|
|
1795
|
-
const titleOffset = title ?
|
|
1935
|
+
const titleOffset = title ? TITLE_OFFSET : 0;
|
|
1796
1936
|
const totalWidth = layout.width;
|
|
1797
1937
|
const totalHeight = layout.height + titleOffset + legendOffset;
|
|
1798
1938
|
|
|
@@ -1800,28 +1940,29 @@ export function renderInfra(
|
|
|
1800
1940
|
|
|
1801
1941
|
// In app mode with legend + title, render the title as a separate fixed-size SVG
|
|
1802
1942
|
// so the legend can be inserted between title and diagram.
|
|
1803
|
-
const fixedTitleH = fixedLegend && title ?
|
|
1943
|
+
const fixedTitleH = fixedLegend && title ? TITLE_OFFSET : 0;
|
|
1804
1944
|
const diagramViewHeight = fixedLegend
|
|
1805
1945
|
? layout.height + (title && !fixedTitleH ? titleOffset : 0) + legendOffset
|
|
1806
1946
|
: totalHeight;
|
|
1807
1947
|
|
|
1808
1948
|
if (fixedTitleH) {
|
|
1949
|
+
const titleContainerW = container.clientWidth || totalWidth;
|
|
1809
1950
|
const titleSvg = d3Selection.select(container)
|
|
1810
1951
|
.append('svg')
|
|
1811
1952
|
.attr('class', 'infra-title-fixed')
|
|
1812
1953
|
.attr('width', '100%')
|
|
1813
1954
|
.attr('height', fixedTitleH)
|
|
1814
|
-
.attr('viewBox', `0 0 ${
|
|
1955
|
+
.attr('viewBox', `0 0 ${titleContainerW} ${fixedTitleH}`)
|
|
1815
1956
|
.attr('preserveAspectRatio', 'xMidYMid meet')
|
|
1816
1957
|
.style('display', 'block');
|
|
1817
1958
|
titleSvg.append('text')
|
|
1818
1959
|
.attr('class', 'chart-title')
|
|
1819
|
-
.attr('x',
|
|
1820
|
-
.attr('y',
|
|
1960
|
+
.attr('x', titleContainerW / 2)
|
|
1961
|
+
.attr('y', TITLE_Y)
|
|
1821
1962
|
.attr('text-anchor', 'middle')
|
|
1822
1963
|
.attr('font-family', FONT_FAMILY)
|
|
1823
|
-
.attr('font-size',
|
|
1824
|
-
.attr('font-weight',
|
|
1964
|
+
.attr('font-size', TITLE_FONT_SIZE)
|
|
1965
|
+
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
1825
1966
|
.attr('fill', palette.text)
|
|
1826
1967
|
.attr('data-line-number', titleLineNumber != null ? titleLineNumber : '')
|
|
1827
1968
|
.text(title!);
|
|
@@ -1890,11 +2031,11 @@ export function renderInfra(
|
|
|
1890
2031
|
rootSvg.append('text')
|
|
1891
2032
|
.attr('class', 'chart-title')
|
|
1892
2033
|
.attr('x', totalWidth / 2)
|
|
1893
|
-
.attr('y',
|
|
2034
|
+
.attr('y', TITLE_Y)
|
|
1894
2035
|
.attr('text-anchor', 'middle')
|
|
1895
2036
|
.attr('font-family', FONT_FAMILY)
|
|
1896
|
-
.attr('font-size',
|
|
1897
|
-
.attr('font-weight',
|
|
2037
|
+
.attr('font-size', TITLE_FONT_SIZE)
|
|
2038
|
+
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
1898
2039
|
.attr('fill', palette.text)
|
|
1899
2040
|
.attr('data-line-number', titleLineNumber != null ? titleLineNumber : '')
|
|
1900
2041
|
.text(title);
|
|
@@ -1902,7 +2043,8 @@ export function renderInfra(
|
|
|
1902
2043
|
|
|
1903
2044
|
// Render layers: groups (back), edge paths, nodes, reject particles, edge labels (front)
|
|
1904
2045
|
renderGroups(svg, layout.groups, palette, isDark);
|
|
1905
|
-
|
|
2046
|
+
const speedMultiplier = playback?.speed ?? 1;
|
|
2047
|
+
renderEdgePaths(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction, speedMultiplier);
|
|
1906
2048
|
const fanoutSourceIds = collectFanoutSourceIds(layout.edges);
|
|
1907
2049
|
const scaledGroupIds = new Set<string>(
|
|
1908
2050
|
layout.groups
|
|
@@ -1915,7 +2057,7 @@ export function renderInfra(
|
|
|
1915
2057
|
);
|
|
1916
2058
|
renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, expandedNodeIds, activeGroup, layout.options, collapsedNodes, tagGroups ?? [], fanoutSourceIds, scaledGroupIds);
|
|
1917
2059
|
if (shouldAnimate) {
|
|
1918
|
-
renderRejectParticles(svg, layout.nodes);
|
|
2060
|
+
renderRejectParticles(svg, layout.nodes, speedMultiplier);
|
|
1919
2061
|
}
|
|
1920
2062
|
renderEdgeLabels(svg, layout.edges, layout.nodes, layout.groups, palette, isDark, shouldAnimate, layout.direction);
|
|
1921
2063
|
|
|
@@ -1933,12 +2075,12 @@ export function renderInfra(
|
|
|
1933
2075
|
.attr('preserveAspectRatio', 'xMidYMid meet')
|
|
1934
2076
|
.style('display', 'block')
|
|
1935
2077
|
.style('pointer-events', 'none');
|
|
1936
|
-
renderLegend(legendSvg, legendGroups, containerWidth, LEGEND_FIXED_GAP / 2, palette, isDark, activeGroup ?? null);
|
|
2078
|
+
renderLegend(legendSvg, legendGroups, containerWidth, LEGEND_FIXED_GAP / 2, palette, isDark, activeGroup ?? null, playback ?? undefined);
|
|
1937
2079
|
// Re-enable pointer events on interactive legend elements
|
|
1938
2080
|
legendSvg.selectAll('.infra-legend-group').style('pointer-events', 'auto');
|
|
1939
2081
|
} else {
|
|
1940
2082
|
// Export mode: render legend at top (below title)
|
|
1941
|
-
renderLegend(rootSvg, legendGroups, totalWidth, titleOffset, palette, isDark, activeGroup ?? null);
|
|
2083
|
+
renderLegend(rootSvg, legendGroups, totalWidth, titleOffset, palette, isDark, activeGroup ?? null, playback ?? undefined);
|
|
1942
2084
|
}
|
|
1943
2085
|
}
|
|
1944
2086
|
}
|
package/src/infra/types.ts
CHANGED
|
@@ -62,6 +62,7 @@ export interface InfraNode {
|
|
|
62
62
|
groupId: string | null;
|
|
63
63
|
tags: Record<string, string>; // tagGroup -> tagValue
|
|
64
64
|
isEdge: boolean; // true for the `edge` entry-point component
|
|
65
|
+
nodeType?: string; // database, cache, queue, service, gateway, storage, function, network
|
|
65
66
|
description?: string;
|
|
66
67
|
lineNumber: number;
|
|
67
68
|
}
|
|
@@ -70,6 +71,7 @@ export interface InfraEdge {
|
|
|
70
71
|
sourceId: string;
|
|
71
72
|
targetId: string;
|
|
72
73
|
label: string;
|
|
74
|
+
async: boolean;
|
|
73
75
|
split: number | null; // percentage 0-100, or null if not declared
|
|
74
76
|
fanout: number | null; // request multiplier: target receives inbound * (split/100) * fanout RPS
|
|
75
77
|
lineNumber: number;
|
|
@@ -82,6 +84,8 @@ export interface InfraGroup {
|
|
|
82
84
|
instances?: number | string;
|
|
83
85
|
/** Whether this group should be collapsed by default in the source. */
|
|
84
86
|
collapsed?: boolean;
|
|
87
|
+
/** Pipe metadata on the group header, cascaded to children. */
|
|
88
|
+
metadata?: Record<string, string>;
|
|
85
89
|
lineNumber: number;
|
|
86
90
|
}
|
|
87
91
|
|
|
@@ -175,6 +179,7 @@ export interface ComputedInfraEdge {
|
|
|
175
179
|
sourceId: string;
|
|
176
180
|
targetId: string;
|
|
177
181
|
label: string;
|
|
182
|
+
async: boolean;
|
|
178
183
|
computedRps: number;
|
|
179
184
|
split: number; // resolved split (always 0-100)
|
|
180
185
|
fanout: number | null;
|
|
@@ -56,7 +56,7 @@ export interface ISLayoutResult {
|
|
|
56
56
|
height: number;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const STATUS_PRIORITY: Record<string, number> = { todo: 3,
|
|
59
|
+
const STATUS_PRIORITY: Record<string, number> = { todo: 4, blocked: 3, doing: 2, done: 1, na: 0 };
|
|
60
60
|
|
|
61
61
|
export function rollUpStatus(members: { status: InitiativeStatus }[]): InitiativeStatus {
|
|
62
62
|
let worst: InitiativeStatus = null;
|