@diagrammo/dgmo 0.8.9 → 0.8.11

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.
Files changed (46) hide show
  1. package/AGENTS.md +3 -0
  2. package/dist/cli.cjs +245 -672
  3. package/dist/editor.cjs.map +1 -1
  4. package/dist/editor.d.cts +2 -3
  5. package/dist/editor.d.ts +2 -3
  6. package/dist/editor.js.map +1 -1
  7. package/dist/index.cjs +1623 -800
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +153 -1
  10. package/dist/index.d.ts +153 -1
  11. package/dist/index.js +1619 -802
  12. package/dist/index.js.map +1 -1
  13. package/docs/language-reference.md +28 -2
  14. package/gallery/fixtures/sitemap-full.dgmo +1 -0
  15. package/package.json +14 -17
  16. package/src/boxes-and-lines/layout.ts +48 -8
  17. package/src/boxes-and-lines/parser.ts +59 -13
  18. package/src/boxes-and-lines/renderer.ts +34 -138
  19. package/src/c4/layout.ts +31 -10
  20. package/src/c4/renderer.ts +25 -138
  21. package/src/class/renderer.ts +185 -186
  22. package/src/d3.ts +194 -222
  23. package/src/echarts.ts +56 -57
  24. package/src/editor/index.ts +1 -2
  25. package/src/er/renderer.ts +52 -245
  26. package/src/gantt/renderer.ts +140 -182
  27. package/src/gantt/resolver.ts +19 -14
  28. package/src/index.ts +23 -1
  29. package/src/infra/renderer.ts +91 -244
  30. package/src/kanban/renderer.ts +29 -133
  31. package/src/label-layout.ts +286 -0
  32. package/src/org/renderer.ts +103 -170
  33. package/src/render.ts +39 -9
  34. package/src/sequence/parser.ts +4 -0
  35. package/src/sequence/renderer.ts +47 -154
  36. package/src/sitemap/layout.ts +180 -38
  37. package/src/sitemap/parser.ts +64 -23
  38. package/src/sitemap/renderer.ts +73 -161
  39. package/src/utils/arrows.ts +1 -1
  40. package/src/utils/legend-constants.ts +6 -0
  41. package/src/utils/legend-d3.ts +400 -0
  42. package/src/utils/legend-layout.ts +491 -0
  43. package/src/utils/legend-svg.ts +28 -2
  44. package/src/utils/legend-types.ts +166 -0
  45. package/src/utils/parsing.ts +1 -1
  46. package/src/utils/tag-groups.ts +1 -1
package/src/c4/layout.ts CHANGED
@@ -18,6 +18,27 @@ import {
18
18
  measureLegendText,
19
19
  } from '../utils/legend-constants';
20
20
 
21
+ /** dagre node label shape after layout(). */
22
+ interface DagreNodeLabel {
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ /** dagre edge label shape after layout(). */
31
+ interface DagreEdgeLabel {
32
+ points: { x: number; y: number }[];
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const gNode = (g: any, name: string): DagreNodeLabel => g.node(name);
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const gEdge = (g: any, v: string, w: string): DagreEdgeLabel | undefined =>
40
+ g.edge(v, w);
41
+
21
42
  // ============================================================
22
43
  // Types
23
44
  // ============================================================
@@ -213,7 +234,7 @@ function computeEdgePenalty(
213
234
  * closer to their neighbors, producing cleaner visual layouts.
214
235
  */
215
236
  function reduceCrossings(
216
- g: dagre.graphlib.Graph,
237
+ g: InstanceType<typeof dagre.graphlib.Graph>,
217
238
  edgeList: { source: string; target: string }[],
218
239
  nodeGroupMap?: Map<string, string>
219
240
  ): void {
@@ -229,7 +250,7 @@ function reduceCrossings(
229
250
  // Build geometry map for edge-node collision scoring
230
251
  const nodeGeometry = new Map<string, NodeGeometry>();
231
252
  for (const name of g.nodes()) {
232
- const pos = g.node(name);
253
+ const pos = gNode(g, name);
233
254
  if (pos)
234
255
  nodeGeometry.set(name, {
235
256
  y: pos.y,
@@ -241,7 +262,7 @@ function reduceCrossings(
241
262
  // Group nodes by rank
242
263
  const rankMap = new Map<number, string[]>();
243
264
  for (const name of g.nodes()) {
244
- const pos = g.node(name);
265
+ const pos = gNode(g, name);
245
266
  if (!pos) continue;
246
267
  const rankY = Math.round(pos.y);
247
268
  if (!rankMap.has(rankY)) rankMap.set(rankY, []);
@@ -250,7 +271,7 @@ function reduceCrossings(
250
271
 
251
272
  // Sort each rank by current x position
252
273
  for (const [, rankNodes] of rankMap) {
253
- rankNodes.sort((a, b) => g.node(a).x - g.node(b).x);
274
+ rankNodes.sort((a, b) => gNode(g, a).x - gNode(g, b).x);
254
275
  }
255
276
 
256
277
  let anyMoved = false;
@@ -285,13 +306,13 @@ function reduceCrossings(
285
306
 
286
307
  // Collect the x-slots for this partition (sorted)
287
308
  const xSlots = partition
288
- .map((name) => g.node(name).x)
309
+ .map((name) => gNode(g, name).x)
289
310
  .sort((a, b) => a - b);
290
311
 
291
312
  // Build position map snapshot
292
313
  const basePositions = new Map<string, number>();
293
314
  for (const name of g.nodes()) {
294
- const pos = g.node(name);
315
+ const pos = gNode(g, name);
295
316
  if (pos) basePositions.set(name, pos.x);
296
317
  }
297
318
 
@@ -379,7 +400,7 @@ function reduceCrossings(
379
400
  // Apply best permutation if it differs from current
380
401
  if (bestPerm.some((name, i) => name !== partition[i])) {
381
402
  for (let i = 0; i < bestPerm.length; i++) {
382
- g.node(bestPerm[i]!).x = xSlots[i]!;
403
+ gNode(g, bestPerm[i]!).x = xSlots[i]!;
383
404
  // Update in the original rankNodes too
384
405
  const rankIdx = rankNodes.indexOf(partition[i]!);
385
406
  if (rankIdx >= 0) rankNodes[rankIdx] = bestPerm[i]!;
@@ -392,10 +413,10 @@ function reduceCrossings(
392
413
  // Recompute edge waypoints if any positions changed
393
414
  if (anyMoved) {
394
415
  for (const edge of edgeList) {
395
- const edgeData = g.edge(edge.source, edge.target);
416
+ const edgeData = gEdge(g, edge.source, edge.target);
396
417
  if (!edgeData) continue;
397
- const srcPos = g.node(edge.source);
398
- const tgtPos = g.node(edge.target);
418
+ const srcPos = gNode(g, edge.source);
419
+ const tgtPos = gNode(g, edge.target);
399
420
  if (!srcPos || !tgtPos) continue;
400
421
 
401
422
  const srcBottom = { x: srcPos.x, y: srcPos.y + srcPos.height / 2 };
@@ -9,7 +9,7 @@ import type { PaletteColors } from '../palettes';
9
9
  import { mix } from '../palettes/color-utils';
10
10
  import { renderInlineText } from '../utils/inline-markdown';
11
11
  import type { ParsedC4 } from './types';
12
- import type { C4LayoutResult, C4LayoutEdge, C4LegendGroup } from './layout';
12
+ import type { C4LayoutResult, C4LayoutEdge } from './layout';
13
13
  import { parseC4 } from './parser';
14
14
  import {
15
15
  layoutC4Context,
@@ -18,18 +18,9 @@ import {
18
18
  layoutC4Deployment,
19
19
  collectCardMetadata,
20
20
  } from './layout';
21
- import {
22
- LEGEND_HEIGHT,
23
- LEGEND_PILL_FONT_SIZE,
24
- LEGEND_PILL_PAD,
25
- LEGEND_DOT_R,
26
- LEGEND_ENTRY_FONT_SIZE,
27
- LEGEND_ENTRY_DOT_GAP,
28
- LEGEND_ENTRY_TRAIL,
29
- LEGEND_CAPSULE_PAD,
30
- LEGEND_GROUP_GAP,
31
- measureLegendText,
32
- } from '../utils/legend-constants';
21
+ import { LEGEND_HEIGHT } from '../utils/legend-constants';
22
+ import { renderLegendD3 } from '../utils/legend-d3';
23
+ import type { LegendConfig, LegendState } from '../utils/legend-types';
33
24
  import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
34
25
 
35
26
  // ============================================================
@@ -1251,133 +1242,29 @@ function renderLegend(
1251
1242
  palette: PaletteColors,
1252
1243
  isDark: boolean,
1253
1244
  activeTagGroup?: string | null,
1254
- /** When set, center groups horizontally across this width (fixed overlay mode). */
1255
1245
  fixedWidth?: number | null
1256
1246
  ): void {
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);
1268
-
1269
- // In fixed mode, compute centered x-positions
1270
- let fixedPositions: Map<string, number> | null = null;
1271
- if (fixedWidth != null && visibleGroups.length > 0) {
1272
- fixedPositions = new Map();
1273
- const totalW =
1274
- visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
1275
- (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
1276
- let cx = Math.max(DIAGRAM_PADDING, (fixedWidth - totalW) / 2);
1277
- for (const g of visibleGroups) {
1278
- fixedPositions.set(g.name, cx);
1279
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
1280
- }
1281
- }
1282
-
1283
- for (const group of visibleGroups) {
1284
- const isActive =
1285
- activeTagGroup != null &&
1286
- group.name.toLowerCase() === (activeTagGroup ?? '').toLowerCase();
1287
-
1288
- const groupBg = isDark
1289
- ? mix(palette.surface, palette.bg, 50)
1290
- : mix(palette.surface, palette.bg, 30);
1291
-
1292
- const pillLabel = group.name;
1293
- const pillWidth = pillWidthOf(group);
1294
-
1295
- const gX = fixedPositions?.get(group.name) ?? group.x;
1296
- const gY = fixedPositions != null ? 0 : group.y;
1297
-
1298
- const gEl = parent
1299
- .append('g')
1300
- .attr('transform', `translate(${gX}, ${gY})`)
1301
- .attr('class', 'c4-legend-group')
1302
- .attr('data-legend-group', group.name.toLowerCase())
1303
- .style('cursor', 'pointer');
1304
-
1305
- if (isActive) {
1306
- gEl
1307
- .append('rect')
1308
- .attr('width', group.width)
1309
- .attr('height', LEGEND_HEIGHT)
1310
- .attr('rx', LEGEND_HEIGHT / 2)
1311
- .attr('fill', groupBg);
1312
- }
1313
-
1314
- const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
1315
- const pillY = LEGEND_CAPSULE_PAD;
1316
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
1317
-
1318
- gEl
1319
- .append('rect')
1320
- .attr('x', pillX)
1321
- .attr('y', pillY)
1322
- .attr('width', pillWidth)
1323
- .attr('height', pillH)
1324
- .attr('rx', pillH / 2)
1325
- .attr('fill', isActive ? palette.bg : groupBg);
1326
-
1327
- if (isActive) {
1328
- gEl
1329
- .append('rect')
1330
- .attr('x', pillX)
1331
- .attr('y', pillY)
1332
- .attr('width', pillWidth)
1333
- .attr('height', pillH)
1334
- .attr('rx', pillH / 2)
1335
- .attr('fill', 'none')
1336
- .attr('stroke', mix(palette.textMuted, palette.bg, 50))
1337
- .attr('stroke-width', 0.75);
1338
- }
1339
-
1340
- gEl
1341
- .append('text')
1342
- .attr('x', pillX + pillWidth / 2)
1343
- .attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
1344
- .attr('font-size', LEGEND_PILL_FONT_SIZE)
1345
- .attr('font-weight', '500')
1346
- .attr('fill', isActive ? palette.text : palette.textMuted)
1347
- .attr('text-anchor', 'middle')
1348
- .text(pillLabel);
1349
-
1350
- if (isActive) {
1351
- let entryX = pillX + pillWidth + 4;
1352
- for (const entry of group.entries) {
1353
- const entryG = gEl
1354
- .append('g')
1355
- .attr('data-legend-entry', entry.value.toLowerCase())
1356
- .style('cursor', 'pointer');
1357
-
1358
- entryG
1359
- .append('circle')
1360
- .attr('cx', entryX + LEGEND_DOT_R)
1361
- .attr('cy', LEGEND_HEIGHT / 2)
1362
- .attr('r', LEGEND_DOT_R)
1363
- .attr('fill', entry.color);
1364
-
1365
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
1366
- entryG
1367
- .append('text')
1368
- .attr('x', textX)
1369
- .attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
1370
- .attr('font-size', LEGEND_ENTRY_FONT_SIZE)
1371
- .attr('fill', palette.textMuted)
1372
- .text(entry.value);
1373
-
1374
- entryX =
1375
- textX +
1376
- measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
1377
- LEGEND_ENTRY_TRAIL;
1378
- }
1379
- }
1380
- }
1247
+ const groups = layout.legend.map((g) => ({
1248
+ name: g.name,
1249
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color })),
1250
+ }));
1251
+ const legendConfig: LegendConfig = {
1252
+ groups,
1253
+ position: { placement: 'top-center', titleRelation: 'below-title' },
1254
+ mode: 'fixed',
1255
+ };
1256
+ const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
1257
+ const containerWidth = fixedWidth ?? layout.width;
1258
+ renderLegendD3(
1259
+ parent,
1260
+ legendConfig,
1261
+ legendState,
1262
+ palette,
1263
+ isDark,
1264
+ undefined,
1265
+ containerWidth
1266
+ );
1267
+ parent.selectAll('[data-legend-group]').classed('c4-legend-group', true);
1381
1268
  }
1382
1269
 
1383
1270
  // ============================================================