@diagrammo/dgmo 0.8.4 → 0.8.6
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 +300 -0
- package/.cursorrules +20 -2
- package/.github/copilot-instructions.md +20 -2
- package/.windsurfrules +20 -2
- package/AGENTS.md +23 -3
- package/dist/cli.cjs +191 -189
- package/dist/editor.cjs +5 -18
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +5 -18
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +543 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.cts +32 -0
- package/dist/highlight.d.ts +32 -0
- package/dist/highlight.js +513 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +3253 -3356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -56
- package/dist/index.d.ts +77 -56
- package/dist/index.js +3247 -3349
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +113 -33
- package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
- package/gallery/fixtures/slope.dgmo +7 -6
- package/package.json +26 -6
- package/src/boxes-and-lines/collapse.ts +78 -0
- package/src/boxes-and-lines/layout.ts +319 -0
- package/src/boxes-and-lines/parser.ts +694 -0
- package/src/boxes-and-lines/renderer.ts +848 -0
- package/src/boxes-and-lines/types.ts +40 -0
- package/src/c4/parser.ts +10 -5
- package/src/c4/renderer.ts +232 -56
- package/src/chart.ts +9 -4
- package/src/cli.ts +49 -6
- package/src/completion.ts +25 -33
- package/src/d3.ts +187 -46
- package/src/dgmo-router.ts +3 -7
- package/src/echarts.ts +38 -2
- package/src/editor/highlight-api.ts +444 -0
- package/src/editor/keywords.ts +6 -19
- package/src/er/parser.ts +10 -4
- package/src/gantt/parser.ts +7 -4
- package/src/gantt/renderer.ts +3 -5
- package/src/index.ts +106 -50
- package/src/infra/parser.ts +7 -5
- package/src/infra/renderer.ts +2 -2
- package/src/kanban/parser.ts +7 -5
- package/src/kanban/renderer.ts +43 -18
- package/src/org/parser.ts +7 -4
- package/src/org/renderer.ts +40 -29
- package/src/sequence/parser.ts +11 -5
- package/src/sequence/renderer.ts +114 -45
- package/src/sitemap/parser.ts +8 -4
- package/src/sitemap/renderer.ts +137 -57
- package/src/utils/legend-svg.ts +44 -20
- package/src/utils/parsing.ts +1 -1
- package/src/utils/tag-groups.ts +21 -1
- package/gallery/fixtures/initiative-status-full.dgmo +0 -46
- package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
- package/gallery/fixtures/initiative-status.dgmo +0 -9
- package/src/initiative-status/collapse.ts +0 -76
- package/src/initiative-status/filter.ts +0 -63
- package/src/initiative-status/layout.ts +0 -650
- package/src/initiative-status/parser.ts +0 -629
- package/src/initiative-status/renderer.ts +0 -1199
- package/src/initiative-status/types.ts +0 -57
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
2
|
+
import type { DgmoError } from '../diagnostics';
|
|
3
|
+
|
|
4
|
+
export interface BLNode {
|
|
5
|
+
label: string;
|
|
6
|
+
lineNumber: number;
|
|
7
|
+
metadata: Record<string, string>;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BLEdge {
|
|
12
|
+
source: string;
|
|
13
|
+
target: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
bidirectional: boolean;
|
|
16
|
+
lineNumber: number;
|
|
17
|
+
metadata: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BLGroup {
|
|
21
|
+
label: string;
|
|
22
|
+
children: string[];
|
|
23
|
+
lineNumber: number;
|
|
24
|
+
metadata: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ParsedBoxesAndLines {
|
|
28
|
+
type: 'boxes-and-lines';
|
|
29
|
+
title: string | null;
|
|
30
|
+
titleLineNumber: number | null;
|
|
31
|
+
nodes: BLNode[];
|
|
32
|
+
edges: BLEdge[];
|
|
33
|
+
groups: BLGroup[];
|
|
34
|
+
tagGroups: TagGroup[];
|
|
35
|
+
options: Record<string, string>;
|
|
36
|
+
initialHiddenTagValues: Map<string, Set<string>>;
|
|
37
|
+
direction: 'LR' | 'TB';
|
|
38
|
+
diagnostics: DgmoError[];
|
|
39
|
+
error: string | null;
|
|
40
|
+
}
|
package/src/c4/parser.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
import type { PaletteColors } from '../palettes';
|
|
6
6
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
7
7
|
import type { TagGroup } from '../utils/tag-groups';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
matchTagBlockHeading,
|
|
10
|
+
stripDefaultModifier,
|
|
11
|
+
} from '../utils/tag-groups';
|
|
9
12
|
import { inferParticipantType } from '../sequence/participant-inference';
|
|
10
13
|
import {
|
|
11
14
|
measureIndent,
|
|
@@ -319,11 +322,12 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
319
322
|
}
|
|
320
323
|
}
|
|
321
324
|
|
|
322
|
-
// Tag group entries — first entry is the default
|
|
325
|
+
// Tag group entries — first entry is the default unless another is marked `default`
|
|
323
326
|
if (currentTagGroup && !contentStarted) {
|
|
324
327
|
const indent = measureIndent(line);
|
|
325
328
|
if (indent > 0) {
|
|
326
|
-
const {
|
|
329
|
+
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
330
|
+
const { label, color } = extractColor(cleanEntry, palette);
|
|
327
331
|
if (!color) {
|
|
328
332
|
pushError(
|
|
329
333
|
lineNumber,
|
|
@@ -331,8 +335,9 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
331
335
|
);
|
|
332
336
|
continue;
|
|
333
337
|
}
|
|
334
|
-
|
|
335
|
-
|
|
338
|
+
if (isDefault) {
|
|
339
|
+
currentTagGroup.defaultValue = label;
|
|
340
|
+
} else if (currentTagGroup.entries.length === 0) {
|
|
336
341
|
currentTagGroup.defaultValue = label;
|
|
337
342
|
}
|
|
338
343
|
currentTagGroup.entries.push({
|
package/src/c4/renderer.ts
CHANGED
|
@@ -11,7 +11,13 @@ import { renderInlineText } from '../utils/inline-markdown';
|
|
|
11
11
|
import type { ParsedC4 } from './types';
|
|
12
12
|
import type { C4LayoutResult, C4LayoutEdge, C4LegendGroup } from './layout';
|
|
13
13
|
import { parseC4 } from './parser';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
layoutC4Context,
|
|
16
|
+
layoutC4Containers,
|
|
17
|
+
layoutC4Components,
|
|
18
|
+
layoutC4Deployment,
|
|
19
|
+
collectCardMetadata,
|
|
20
|
+
} from './layout';
|
|
15
21
|
import {
|
|
16
22
|
LEGEND_HEIGHT,
|
|
17
23
|
LEGEND_PILL_FONT_SIZE,
|
|
@@ -81,10 +87,14 @@ function typeColor(
|
|
|
81
87
|
): string {
|
|
82
88
|
if (nodeColor) return nodeColor;
|
|
83
89
|
switch (type) {
|
|
84
|
-
case 'person':
|
|
85
|
-
|
|
86
|
-
case '
|
|
87
|
-
|
|
90
|
+
case 'person':
|
|
91
|
+
return palette.colors.blue;
|
|
92
|
+
case 'container':
|
|
93
|
+
return palette.colors.purple;
|
|
94
|
+
case 'component':
|
|
95
|
+
return palette.colors.green;
|
|
96
|
+
default:
|
|
97
|
+
return palette.colors.teal;
|
|
88
98
|
}
|
|
89
99
|
}
|
|
90
100
|
|
|
@@ -247,7 +257,9 @@ export function renderC4Context(
|
|
|
247
257
|
const fixedLegend = !exportDims && hasLegend;
|
|
248
258
|
const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
|
|
249
259
|
const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
250
|
-
const diagramH = fixedLegend
|
|
260
|
+
const diagramH = fixedLegend
|
|
261
|
+
? layout.height - legendLayoutSpace
|
|
262
|
+
: layout.height;
|
|
251
263
|
const availH = height - titleHeight - legendReserveH;
|
|
252
264
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
253
265
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
@@ -308,7 +320,10 @@ export function renderC4Context(
|
|
|
308
320
|
.attr('fill', palette.text)
|
|
309
321
|
.attr('font-size', TITLE_FONT_SIZE)
|
|
310
322
|
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
311
|
-
.style(
|
|
323
|
+
.style(
|
|
324
|
+
'cursor',
|
|
325
|
+
onClickItem && parsed.titleLineNumber ? 'pointer' : 'default'
|
|
326
|
+
)
|
|
312
327
|
.text(parsed.title);
|
|
313
328
|
|
|
314
329
|
if (parsed.titleLineNumber) {
|
|
@@ -415,7 +430,7 @@ export function renderC4Context(
|
|
|
415
430
|
edgeG
|
|
416
431
|
.append('text')
|
|
417
432
|
.attr('x', midPt.x)
|
|
418
|
-
.attr('y',
|
|
433
|
+
.attr('y', labelText ? textY + 18 : textY + 4)
|
|
419
434
|
.attr('text-anchor', 'middle')
|
|
420
435
|
.attr('fill', edgeColor)
|
|
421
436
|
.attr('font-size', TECH_FONT_SIZE)
|
|
@@ -444,7 +459,8 @@ export function renderC4Context(
|
|
|
444
459
|
// Fall back to the group's defaultValue so hover-dimming works for
|
|
445
460
|
// nodes that inherit the default (e.g. sc: Internal default).
|
|
446
461
|
const tagGroup = parsed.tagGroups.find(
|
|
447
|
-
(g) =>
|
|
462
|
+
(g) =>
|
|
463
|
+
g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
|
|
448
464
|
);
|
|
449
465
|
if (tagGroup?.defaultValue) {
|
|
450
466
|
nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
|
|
@@ -573,12 +589,17 @@ export function renderC4Context(
|
|
|
573
589
|
// Drillable accent bar — solid bar at bottom of card, clipped to rounded corners
|
|
574
590
|
if (node.drillable) {
|
|
575
591
|
const clipId = `clip-drill-${node.id.replace(/\s+/g, '-')}`;
|
|
576
|
-
nodeG
|
|
592
|
+
nodeG
|
|
593
|
+
.append('clipPath')
|
|
594
|
+
.attr('id', clipId)
|
|
577
595
|
.append('rect')
|
|
578
|
-
.attr('x', -w / 2)
|
|
579
|
-
.attr('
|
|
596
|
+
.attr('x', -w / 2)
|
|
597
|
+
.attr('y', -h / 2)
|
|
598
|
+
.attr('width', w)
|
|
599
|
+
.attr('height', h)
|
|
580
600
|
.attr('rx', CARD_RADIUS);
|
|
581
|
-
nodeG
|
|
601
|
+
nodeG
|
|
602
|
+
.append('rect')
|
|
582
603
|
.attr('x', -w / 2)
|
|
583
604
|
.attr('y', h / 2 - DRILL_BAR_HEIGHT)
|
|
584
605
|
.attr('width', w)
|
|
@@ -594,14 +615,22 @@ export function renderC4Context(
|
|
|
594
615
|
// App mode: fixed overlay at SVG top so it's always readable regardless of scale.
|
|
595
616
|
// Export mode: render inside scaled contentG at layout coordinates.
|
|
596
617
|
const legendParent = fixedLegend
|
|
597
|
-
? svg
|
|
618
|
+
? svg
|
|
619
|
+
.append('g')
|
|
598
620
|
.attr('class', 'c4-legend-fixed')
|
|
599
621
|
.attr('transform', `translate(0, ${DIAGRAM_PADDING + titleHeight})`)
|
|
600
622
|
: contentG.append('g').attr('class', 'c4-legend');
|
|
601
623
|
if (activeTagGroup) {
|
|
602
624
|
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
603
625
|
}
|
|
604
|
-
renderLegend(
|
|
626
|
+
renderLegend(
|
|
627
|
+
legendParent as GSelection,
|
|
628
|
+
layout,
|
|
629
|
+
palette,
|
|
630
|
+
isDark,
|
|
631
|
+
activeTagGroup,
|
|
632
|
+
fixedLegend ? width : null
|
|
633
|
+
);
|
|
605
634
|
}
|
|
606
635
|
}
|
|
607
636
|
|
|
@@ -938,8 +967,14 @@ function pointToPolylineDist(
|
|
|
938
967
|
|
|
939
968
|
/** Check if a rect overlaps another rect. */
|
|
940
969
|
function rectsOverlap(
|
|
941
|
-
ax: number,
|
|
942
|
-
|
|
970
|
+
ax: number,
|
|
971
|
+
ay: number,
|
|
972
|
+
aw: number,
|
|
973
|
+
ah: number,
|
|
974
|
+
bx: number,
|
|
975
|
+
by: number,
|
|
976
|
+
bw: number,
|
|
977
|
+
bh: number,
|
|
943
978
|
pad: number
|
|
944
979
|
): boolean {
|
|
945
980
|
return !(
|
|
@@ -1006,7 +1041,7 @@ function placeEdgeLabels(
|
|
|
1006
1041
|
const placedRects: { x: number; y: number; w: number; h: number }[] = [];
|
|
1007
1042
|
|
|
1008
1043
|
// Bias samples toward target end (50–90%) where edges have diverged
|
|
1009
|
-
const SAMPLES = [0.
|
|
1044
|
+
const SAMPLES = [0.4, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9];
|
|
1010
1045
|
|
|
1011
1046
|
// Pre-compute candidate positions for each label
|
|
1012
1047
|
const candidates = labels.map((lbl) => {
|
|
@@ -1021,7 +1056,8 @@ function placeEdgeLabels(
|
|
|
1021
1056
|
order.sort((a, b) => {
|
|
1022
1057
|
const midA = interpolateAlongPath(allPaths[labels[a]!.edgeIdx]!, 0.5);
|
|
1023
1058
|
const midB = interpolateAlongPath(allPaths[labels[b]!.edgeIdx]!, 0.5);
|
|
1024
|
-
let nearA = 0,
|
|
1059
|
+
let nearA = 0,
|
|
1060
|
+
nearB = 0;
|
|
1025
1061
|
for (let e = 0; e < allPaths.length; e++) {
|
|
1026
1062
|
if (e === labels[a]!.edgeIdx) continue;
|
|
1027
1063
|
if (pointToPolylineDist(midA, allPaths[e]!) < 100) nearA++;
|
|
@@ -1055,7 +1091,19 @@ function placeEdgeLabels(
|
|
|
1055
1091
|
// Penalty for overlapping already-placed labels
|
|
1056
1092
|
let labelOverlapPenalty = 0;
|
|
1057
1093
|
for (const placed of placedRects) {
|
|
1058
|
-
if (
|
|
1094
|
+
if (
|
|
1095
|
+
rectsOverlap(
|
|
1096
|
+
pt.x,
|
|
1097
|
+
pt.y,
|
|
1098
|
+
lbl.bgW,
|
|
1099
|
+
lbl.bgH,
|
|
1100
|
+
placed.x,
|
|
1101
|
+
placed.y,
|
|
1102
|
+
placed.w,
|
|
1103
|
+
placed.h,
|
|
1104
|
+
6
|
|
1105
|
+
)
|
|
1106
|
+
) {
|
|
1059
1107
|
labelOverlapPenalty += 200;
|
|
1060
1108
|
}
|
|
1061
1109
|
}
|
|
@@ -1063,7 +1111,19 @@ function placeEdgeLabels(
|
|
|
1063
1111
|
// Penalty for overlapping boundary/obstacle rects (e.g. boundary labels)
|
|
1064
1112
|
if (obstacleRects) {
|
|
1065
1113
|
for (const obs of obstacleRects) {
|
|
1066
|
-
if (
|
|
1114
|
+
if (
|
|
1115
|
+
rectsOverlap(
|
|
1116
|
+
pt.x,
|
|
1117
|
+
pt.y,
|
|
1118
|
+
lbl.bgW,
|
|
1119
|
+
lbl.bgH,
|
|
1120
|
+
obs.x + obs.w / 2,
|
|
1121
|
+
obs.y + obs.h / 2,
|
|
1122
|
+
obs.w,
|
|
1123
|
+
obs.h,
|
|
1124
|
+
6
|
|
1125
|
+
)
|
|
1126
|
+
) {
|
|
1067
1127
|
labelOverlapPenalty += 200;
|
|
1068
1128
|
}
|
|
1069
1129
|
}
|
|
@@ -1087,31 +1147,87 @@ function placeEdgeLabels(
|
|
|
1087
1147
|
const nx = -tan.y / tLen;
|
|
1088
1148
|
const ny = tan.x / tLen;
|
|
1089
1149
|
const offsetDist = lbl.bgH / 2 + 4;
|
|
1090
|
-
const sideA = {
|
|
1091
|
-
|
|
1150
|
+
const sideA = {
|
|
1151
|
+
x: bestPt.x + nx * offsetDist,
|
|
1152
|
+
y: bestPt.y + ny * offsetDist,
|
|
1153
|
+
};
|
|
1154
|
+
const sideB = {
|
|
1155
|
+
x: bestPt.x - nx * offsetDist,
|
|
1156
|
+
y: bestPt.y - ny * offsetDist,
|
|
1157
|
+
};
|
|
1092
1158
|
|
|
1093
1159
|
// Score each side: clearance from other edges + overlap with placed labels
|
|
1094
|
-
let scoreA = Infinity,
|
|
1160
|
+
let scoreA = Infinity,
|
|
1161
|
+
scoreB = Infinity;
|
|
1095
1162
|
for (let e = 0; e < allPaths.length; e++) {
|
|
1096
1163
|
if (e === ownEdgeIdx) continue;
|
|
1097
1164
|
scoreA = Math.min(scoreA, pointToPolylineDist(sideA, allPaths[e]!));
|
|
1098
1165
|
scoreB = Math.min(scoreB, pointToPolylineDist(sideB, allPaths[e]!));
|
|
1099
1166
|
}
|
|
1100
1167
|
for (const placed of placedRects) {
|
|
1101
|
-
if (
|
|
1168
|
+
if (
|
|
1169
|
+
rectsOverlap(
|
|
1170
|
+
sideA.x,
|
|
1171
|
+
sideA.y,
|
|
1172
|
+
lbl.bgW,
|
|
1173
|
+
lbl.bgH,
|
|
1174
|
+
placed.x,
|
|
1175
|
+
placed.y,
|
|
1176
|
+
placed.w,
|
|
1177
|
+
placed.h,
|
|
1178
|
+
6
|
|
1179
|
+
)
|
|
1180
|
+
) {
|
|
1102
1181
|
scoreA -= 200;
|
|
1103
1182
|
}
|
|
1104
|
-
if (
|
|
1183
|
+
if (
|
|
1184
|
+
rectsOverlap(
|
|
1185
|
+
sideB.x,
|
|
1186
|
+
sideB.y,
|
|
1187
|
+
lbl.bgW,
|
|
1188
|
+
lbl.bgH,
|
|
1189
|
+
placed.x,
|
|
1190
|
+
placed.y,
|
|
1191
|
+
placed.w,
|
|
1192
|
+
placed.h,
|
|
1193
|
+
6
|
|
1194
|
+
)
|
|
1195
|
+
) {
|
|
1105
1196
|
scoreB -= 200;
|
|
1106
1197
|
}
|
|
1107
1198
|
}
|
|
1108
1199
|
if (obstacleRects) {
|
|
1109
1200
|
for (const obs of obstacleRects) {
|
|
1110
|
-
const cx = obs.x + obs.w / 2,
|
|
1111
|
-
|
|
1201
|
+
const cx = obs.x + obs.w / 2,
|
|
1202
|
+
cy = obs.y + obs.h / 2;
|
|
1203
|
+
if (
|
|
1204
|
+
rectsOverlap(
|
|
1205
|
+
sideA.x,
|
|
1206
|
+
sideA.y,
|
|
1207
|
+
lbl.bgW,
|
|
1208
|
+
lbl.bgH,
|
|
1209
|
+
cx,
|
|
1210
|
+
cy,
|
|
1211
|
+
obs.w,
|
|
1212
|
+
obs.h,
|
|
1213
|
+
6
|
|
1214
|
+
)
|
|
1215
|
+
) {
|
|
1112
1216
|
scoreA -= 200;
|
|
1113
1217
|
}
|
|
1114
|
-
if (
|
|
1218
|
+
if (
|
|
1219
|
+
rectsOverlap(
|
|
1220
|
+
sideB.x,
|
|
1221
|
+
sideB.y,
|
|
1222
|
+
lbl.bgW,
|
|
1223
|
+
lbl.bgH,
|
|
1224
|
+
cx,
|
|
1225
|
+
cy,
|
|
1226
|
+
obs.w,
|
|
1227
|
+
obs.h,
|
|
1228
|
+
6
|
|
1229
|
+
)
|
|
1230
|
+
) {
|
|
1115
1231
|
scoreB -= 200;
|
|
1116
1232
|
}
|
|
1117
1233
|
}
|
|
@@ -1138,19 +1254,25 @@ function renderLegend(
|
|
|
1138
1254
|
/** When set, center groups horizontally across this width (fixed overlay mode). */
|
|
1139
1255
|
fixedWidth?: number | null
|
|
1140
1256
|
): void {
|
|
1141
|
-
const visibleGroups =
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1257
|
+
const visibleGroups =
|
|
1258
|
+
activeTagGroup != null
|
|
1259
|
+
? layout.legend.filter(
|
|
1260
|
+
(g) => g.name.toLowerCase() === (activeTagGroup ?? '').toLowerCase()
|
|
1261
|
+
)
|
|
1262
|
+
: layout.legend;
|
|
1263
|
+
|
|
1264
|
+
const pillWidthOf = (g: C4LegendGroup) =>
|
|
1265
|
+
measureLegendText(g.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1266
|
+
const effectiveW = (g: C4LegendGroup) =>
|
|
1267
|
+
activeTagGroup != null ? g.width : pillWidthOf(g);
|
|
1147
1268
|
|
|
1148
1269
|
// In fixed mode, compute centered x-positions
|
|
1149
1270
|
let fixedPositions: Map<string, number> | null = null;
|
|
1150
1271
|
if (fixedWidth != null && visibleGroups.length > 0) {
|
|
1151
1272
|
fixedPositions = new Map();
|
|
1152
|
-
const totalW =
|
|
1153
|
-
|
|
1273
|
+
const totalW =
|
|
1274
|
+
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1275
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1154
1276
|
let cx = Math.max(DIAGRAM_PADDING, (fixedWidth - totalW) / 2);
|
|
1155
1277
|
for (const g of visibleGroups) {
|
|
1156
1278
|
fixedPositions.set(g.name, cx);
|
|
@@ -1190,8 +1312,8 @@ function renderLegend(
|
|
|
1190
1312
|
}
|
|
1191
1313
|
|
|
1192
1314
|
const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
1193
|
-
const pillY =
|
|
1194
|
-
const pillH = LEGEND_HEIGHT -
|
|
1315
|
+
const pillY = LEGEND_CAPSULE_PAD;
|
|
1316
|
+
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
1195
1317
|
|
|
1196
1318
|
gEl
|
|
1197
1319
|
.append('rect')
|
|
@@ -1249,7 +1371,10 @@ function renderLegend(
|
|
|
1249
1371
|
.attr('fill', palette.textMuted)
|
|
1250
1372
|
.text(entry.value);
|
|
1251
1373
|
|
|
1252
|
-
entryX =
|
|
1374
|
+
entryX =
|
|
1375
|
+
textX +
|
|
1376
|
+
measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
1377
|
+
LEGEND_ENTRY_TRAIL;
|
|
1253
1378
|
}
|
|
1254
1379
|
}
|
|
1255
1380
|
}
|
|
@@ -1289,7 +1414,9 @@ export function renderC4Containers(
|
|
|
1289
1414
|
const fixedLegend = !exportDims && hasLegend;
|
|
1290
1415
|
const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
|
|
1291
1416
|
const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
1292
|
-
const diagramH = fixedLegend
|
|
1417
|
+
const diagramH = fixedLegend
|
|
1418
|
+
? layout.height - legendLayoutSpace
|
|
1419
|
+
: layout.height;
|
|
1293
1420
|
const availH = height - titleHeight - legendReserveH;
|
|
1294
1421
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
1295
1422
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
@@ -1348,7 +1475,10 @@ export function renderC4Containers(
|
|
|
1348
1475
|
.attr('fill', palette.text)
|
|
1349
1476
|
.attr('font-size', TITLE_FONT_SIZE)
|
|
1350
1477
|
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
1351
|
-
.style(
|
|
1478
|
+
.style(
|
|
1479
|
+
'cursor',
|
|
1480
|
+
onClickItem && parsed.titleLineNumber ? 'pointer' : 'default'
|
|
1481
|
+
)
|
|
1352
1482
|
.text(parsed.title);
|
|
1353
1483
|
|
|
1354
1484
|
if (parsed.titleLineNumber) {
|
|
@@ -1453,7 +1583,12 @@ export function renderC4Containers(
|
|
|
1453
1583
|
}
|
|
1454
1584
|
|
|
1455
1585
|
// ── Collect boundary label rects as obstacles for edge label placement ──
|
|
1456
|
-
const boundaryLabelObstacles: {
|
|
1586
|
+
const boundaryLabelObstacles: {
|
|
1587
|
+
x: number;
|
|
1588
|
+
y: number;
|
|
1589
|
+
w: number;
|
|
1590
|
+
h: number;
|
|
1591
|
+
}[] = [];
|
|
1457
1592
|
if (layout.boundary) {
|
|
1458
1593
|
const b = layout.boundary;
|
|
1459
1594
|
const labelText = `${b.label} \u2014 ${b.typeLabel}`;
|
|
@@ -1468,7 +1603,13 @@ export function renderC4Containers(
|
|
|
1468
1603
|
}
|
|
1469
1604
|
|
|
1470
1605
|
// ── Edges (behind nodes) ──
|
|
1471
|
-
renderEdges(
|
|
1606
|
+
renderEdges(
|
|
1607
|
+
contentG as GSelection,
|
|
1608
|
+
layout.edges,
|
|
1609
|
+
palette,
|
|
1610
|
+
onClickItem,
|
|
1611
|
+
boundaryLabelObstacles
|
|
1612
|
+
);
|
|
1472
1613
|
|
|
1473
1614
|
// ── Nodes ──
|
|
1474
1615
|
for (const node of layout.nodes) {
|
|
@@ -1488,7 +1629,8 @@ export function renderC4Containers(
|
|
|
1488
1629
|
// Fall back to the group's defaultValue so hover-dimming works for
|
|
1489
1630
|
// nodes that inherit the default (e.g. sc: Internal default).
|
|
1490
1631
|
const tagGroup = parsed.tagGroups.find(
|
|
1491
|
-
(g) =>
|
|
1632
|
+
(g) =>
|
|
1633
|
+
g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
|
|
1492
1634
|
);
|
|
1493
1635
|
if (tagGroup?.defaultValue) {
|
|
1494
1636
|
nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
|
|
@@ -1519,7 +1661,14 @@ export function renderC4Containers(
|
|
|
1519
1661
|
|
|
1520
1662
|
// Card background — shape-specific
|
|
1521
1663
|
if (shape === 'database' || shape === 'cache') {
|
|
1522
|
-
drawCylinderCard(
|
|
1664
|
+
drawCylinderCard(
|
|
1665
|
+
nodeG as GSelection,
|
|
1666
|
+
w,
|
|
1667
|
+
h,
|
|
1668
|
+
fill,
|
|
1669
|
+
stroke,
|
|
1670
|
+
shape === 'cache'
|
|
1671
|
+
);
|
|
1523
1672
|
} else {
|
|
1524
1673
|
drawCardRect(nodeG as GSelection, w, h, fill, stroke, isExternalShape);
|
|
1525
1674
|
}
|
|
@@ -1557,7 +1706,12 @@ export function renderC4Containers(
|
|
|
1557
1706
|
const iconCx = -totalWidth / 2 + PERSON_ICON_W / 2;
|
|
1558
1707
|
const textX = iconCx + PERSON_ICON_W / 2 + gap;
|
|
1559
1708
|
|
|
1560
|
-
drawPersonIcon(
|
|
1709
|
+
drawPersonIcon(
|
|
1710
|
+
nodeG as GSelection,
|
|
1711
|
+
iconCx,
|
|
1712
|
+
yPos + NAME_FONT_SIZE / 2 - 2,
|
|
1713
|
+
stroke
|
|
1714
|
+
);
|
|
1561
1715
|
|
|
1562
1716
|
nodeG
|
|
1563
1717
|
.append('text')
|
|
@@ -1688,12 +1842,17 @@ export function renderC4Containers(
|
|
|
1688
1842
|
// Drillable accent bar — solid bar at bottom of card, clipped to rounded corners
|
|
1689
1843
|
if (node.drillable) {
|
|
1690
1844
|
const clipId = `clip-drill-${node.id.replace(/\s+/g, '-')}`;
|
|
1691
|
-
nodeG
|
|
1845
|
+
nodeG
|
|
1846
|
+
.append('clipPath')
|
|
1847
|
+
.attr('id', clipId)
|
|
1692
1848
|
.append('rect')
|
|
1693
|
-
.attr('x', -w / 2)
|
|
1694
|
-
.attr('
|
|
1849
|
+
.attr('x', -w / 2)
|
|
1850
|
+
.attr('y', -h / 2)
|
|
1851
|
+
.attr('width', w)
|
|
1852
|
+
.attr('height', h)
|
|
1695
1853
|
.attr('rx', CARD_RADIUS);
|
|
1696
|
-
nodeG
|
|
1854
|
+
nodeG
|
|
1855
|
+
.append('rect')
|
|
1697
1856
|
.attr('x', -w / 2)
|
|
1698
1857
|
.attr('y', h / 2 - DRILL_BAR_HEIGHT)
|
|
1699
1858
|
.attr('width', w)
|
|
@@ -1709,14 +1868,22 @@ export function renderC4Containers(
|
|
|
1709
1868
|
// App mode: fixed overlay at SVG top so it's always readable regardless of scale.
|
|
1710
1869
|
// Export mode: render inside scaled contentG at layout coordinates.
|
|
1711
1870
|
const legendParent = fixedLegend
|
|
1712
|
-
? svg
|
|
1871
|
+
? svg
|
|
1872
|
+
.append('g')
|
|
1713
1873
|
.attr('class', 'c4-legend-fixed')
|
|
1714
1874
|
.attr('transform', `translate(0, ${DIAGRAM_PADDING + titleHeight})`)
|
|
1715
1875
|
: contentG.append('g').attr('class', 'c4-legend');
|
|
1716
1876
|
if (activeTagGroup) {
|
|
1717
1877
|
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
1718
1878
|
}
|
|
1719
|
-
renderLegend(
|
|
1879
|
+
renderLegend(
|
|
1880
|
+
legendParent as GSelection,
|
|
1881
|
+
layout,
|
|
1882
|
+
palette,
|
|
1883
|
+
isDark,
|
|
1884
|
+
activeTagGroup,
|
|
1885
|
+
fixedLegend ? width : null
|
|
1886
|
+
);
|
|
1720
1887
|
}
|
|
1721
1888
|
}
|
|
1722
1889
|
|
|
@@ -1841,9 +2008,18 @@ export function renderC4Deployment(
|
|
|
1841
2008
|
isDark: boolean,
|
|
1842
2009
|
onClickItem?: (lineNumber: number) => void,
|
|
1843
2010
|
exportDims?: { width?: number; height?: number },
|
|
1844
|
-
activeTagGroup?: string | null
|
|
2011
|
+
activeTagGroup?: string | null
|
|
1845
2012
|
): void {
|
|
1846
|
-
renderC4Containers(
|
|
2013
|
+
renderC4Containers(
|
|
2014
|
+
container,
|
|
2015
|
+
parsed,
|
|
2016
|
+
layout,
|
|
2017
|
+
palette,
|
|
2018
|
+
isDark,
|
|
2019
|
+
onClickItem,
|
|
2020
|
+
exportDims,
|
|
2021
|
+
activeTagGroup
|
|
2022
|
+
);
|
|
1847
2023
|
}
|
|
1848
2024
|
|
|
1849
2025
|
/**
|
|
@@ -1852,7 +2028,7 @@ export function renderC4Deployment(
|
|
|
1852
2028
|
export function renderC4DeploymentForExport(
|
|
1853
2029
|
content: string,
|
|
1854
2030
|
theme: 'light' | 'dark' | 'transparent',
|
|
1855
|
-
palette: PaletteColors
|
|
2031
|
+
palette: PaletteColors
|
|
1856
2032
|
): string {
|
|
1857
2033
|
const parsed = parseC4(content, palette);
|
|
1858
2034
|
if (parsed.error || parsed.deployment.length === 0) return '';
|
package/src/chart.ts
CHANGED
|
@@ -330,8 +330,12 @@ export function parseChart(
|
|
|
330
330
|
// Supports comma-separated multi-values: "Jan 100, 200, 300"
|
|
331
331
|
// Supports space-separated multi-values when series are defined: "Jan 100 200 300"
|
|
332
332
|
// Supports comma-grouped numbers: "Revenue 1,200, 1,500" → [1200, 1500]
|
|
333
|
-
const
|
|
334
|
-
const
|
|
333
|
+
const seriesCount = result.seriesNames?.length ?? 0;
|
|
334
|
+
const multiValue = seriesCount >= 2;
|
|
335
|
+
const dataValues = parseDataRowValues(trimmed, {
|
|
336
|
+
multiValue,
|
|
337
|
+
expectedValues: multiValue ? seriesCount : undefined,
|
|
338
|
+
});
|
|
335
339
|
if (dataValues) {
|
|
336
340
|
const { label: rawLabel, color: pointColor } = extractColor(
|
|
337
341
|
dataValues.label,
|
|
@@ -455,7 +459,7 @@ export function parseChart(
|
|
|
455
459
|
*/
|
|
456
460
|
export function parseDataRowValues(
|
|
457
461
|
line: string,
|
|
458
|
-
options?: { multiValue?: boolean }
|
|
462
|
+
options?: { multiValue?: boolean; expectedValues?: number }
|
|
459
463
|
): { label: string; values: number[] } | null {
|
|
460
464
|
// First, normalize comma-grouped numbers: replace patterns like "1,087" with "1087"
|
|
461
465
|
// We need to be careful: commas also separate multi-values.
|
|
@@ -550,9 +554,10 @@ export function parseDataRowValues(
|
|
|
550
554
|
if (tokens.length < 2) return null;
|
|
551
555
|
|
|
552
556
|
if (options?.multiValue) {
|
|
557
|
+
const limit = options.expectedValues ?? Infinity;
|
|
553
558
|
const values: number[] = [];
|
|
554
559
|
let idx = tokens.length - 1;
|
|
555
|
-
while (idx >= 1) {
|
|
560
|
+
while (idx >= 1 && values.length < limit) {
|
|
556
561
|
const tok = tokens[idx];
|
|
557
562
|
const num = parseFloat(tok);
|
|
558
563
|
if (isNaN(num) || !isFinite(Number(tok))) break;
|