@diagrammo/dgmo 0.30.0 → 0.32.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/.cursorrules +4 -1
- package/.github/copilot-instructions.md +4 -1
- package/.windsurfrules +4 -1
- package/README.md +21 -3
- package/SKILL.md +4 -1
- package/dist/advanced.cjs +1853 -623
- package/dist/advanced.d.cts +143 -16
- package/dist/advanced.d.ts +143 -16
- package/dist/advanced.js +1846 -623
- package/dist/auto.cjs +1640 -581
- package/dist/auto.js +99 -99
- package/dist/auto.mjs +1640 -581
- package/dist/cli.cjs +148 -147
- package/dist/index.cjs +1643 -662
- package/dist/index.js +1643 -662
- package/docs/ai-integration.md +4 -1
- package/docs/language-reference.md +282 -27
- package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
- package/gallery/fixtures/c4-full.dgmo +4 -5
- package/gallery/fixtures/c4.dgmo +2 -3
- package/package.json +7 -1
- package/src/advanced.ts +10 -0
- package/src/boxes-and-lines/focus.ts +257 -0
- package/src/boxes-and-lines/layout-search.ts +345 -65
- package/src/boxes-and-lines/layout.ts +11 -1
- package/src/boxes-and-lines/parser.ts +97 -4
- package/src/boxes-and-lines/renderer.ts +111 -8
- package/src/boxes-and-lines/types.ts +9 -0
- package/src/c4/parser.ts +8 -7
- package/src/c4/renderer.ts +7 -5
- package/src/chart-type-registry.ts +129 -4
- package/src/chart-types.ts +3 -3
- package/src/chart.ts +18 -1
- package/src/class/renderer.ts +4 -2
- package/src/cli-banner.ts +107 -0
- package/src/cli.ts +13 -0
- package/src/colors.ts +247 -2
- package/src/cycle/parser.ts +2 -7
- package/src/d3.ts +67 -54
- package/src/diagnostics.ts +17 -0
- package/src/dimensions.ts +9 -13
- package/src/echarts.ts +42 -14
- package/src/er/parser.ts +6 -1
- package/src/er/renderer.ts +4 -2
- package/src/gantt/parser.ts +44 -7
- package/src/graph/flowchart-parser.ts +77 -3
- package/src/graph/flowchart-renderer.ts +4 -2
- package/src/graph/state-renderer.ts +6 -4
- package/src/infra/parser.ts +80 -0
- package/src/infra/renderer.ts +8 -4
- package/src/journey-map/parser.ts +23 -8
- package/src/journey-map/renderer.ts +1 -1
- package/src/kanban/parser.ts +8 -7
- package/src/kanban/renderer.ts +1 -1
- package/src/map/context-labels.ts +134 -27
- package/src/map/geo.ts +10 -2
- package/src/map/layout.ts +259 -4
- package/src/map/parser.ts +2 -0
- package/src/map/renderer.ts +49 -25
- package/src/map/resolver.ts +68 -19
- package/src/mindmap/parser.ts +15 -7
- package/src/mindmap/renderer.ts +55 -15
- package/src/org/parser.ts +8 -7
- package/src/org/renderer.ts +89 -127
- package/src/palettes/color-utils.ts +19 -4
- package/src/palettes/index.ts +1 -0
- package/src/pert/renderer.ts +15 -10
- package/src/pyramid/parser.ts +2 -7
- package/src/quadrant/renderer.ts +2 -2
- package/src/raci/parser.ts +2 -7
- package/src/raci/renderer.ts +5 -5
- package/src/ring/parser.ts +2 -7
- package/src/sequence/parser.ts +18 -7
- package/src/sequence/renderer.ts +4 -4
- package/src/sitemap/parser.ts +8 -7
- package/src/sitemap/renderer.ts +37 -39
- package/src/tech-radar/parser.ts +2 -7
- package/src/timeline/renderer.ts +15 -5
- package/src/utils/card.ts +183 -0
- package/src/utils/parsing.ts +13 -1
- package/src/utils/scaling.ts +38 -81
- package/src/utils/tag-groups.ts +48 -10
- package/src/utils/visual-conventions.ts +61 -0
- package/src/visualizations/parse.ts +6 -1
- package/src/wireframe/parser.ts +6 -1
|
@@ -42,6 +42,11 @@ import type { PaletteColors } from '../palettes';
|
|
|
42
42
|
|
|
43
43
|
const MAX_GROUP_DEPTH = 2;
|
|
44
44
|
|
|
45
|
+
// §1.4/§1.10 legacy-pipe detection — module-scope so they aren't re-created per
|
|
46
|
+
// line. `|` inside a directed (`->`) or undirected (`~>`) arrow label is valid.
|
|
47
|
+
const ARROW_LABEL_PIPE_DIRECTED_RE = /-\S*\|\S*->/;
|
|
48
|
+
const ARROW_LABEL_PIPE_UNDIRECTED_RE = /~\S*\|\S*~>/;
|
|
49
|
+
|
|
45
50
|
/** Boxes-and-lines requires explicit first line — no heuristic detection. */
|
|
46
51
|
export function looksLikeBoxesAndLines(_content: string): boolean {
|
|
47
52
|
return false;
|
|
@@ -127,6 +132,8 @@ export function parseBoxesAndLines(
|
|
|
127
132
|
const nodes: MutBLNode[] = [];
|
|
128
133
|
const edges: MutBLEdge[] = [];
|
|
129
134
|
const groups: MutBLGroup[] = [];
|
|
135
|
+
// Trailing `layout` block (Canvas Editor spike): node-id → absolute {x,y}.
|
|
136
|
+
const nodePositions = new Map<string, { x: number; y: number }>();
|
|
130
137
|
const result: Writable<ParsedBoxesAndLines> = {
|
|
131
138
|
type: 'boxes-and-lines',
|
|
132
139
|
title: null,
|
|
@@ -178,6 +185,11 @@ export function parseBoxesAndLines(
|
|
|
178
185
|
|
|
179
186
|
// Tag block state
|
|
180
187
|
let contentStarted = false;
|
|
188
|
+
// `layout` coordinate-block state (Canvas Editor spike). Unlike tag blocks,
|
|
189
|
+
// this is a TRAILING appendix — it may appear after diagram content.
|
|
190
|
+
let inLayoutBlock = false;
|
|
191
|
+
const LAYOUT_ENTRY_RE =
|
|
192
|
+
/^(.+?):\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/;
|
|
181
193
|
let currentTagGroup: Writable<TagGroup> | null = null;
|
|
182
194
|
// metaAliasMap: tag-group metadata-key aliases (per A1).
|
|
183
195
|
const metaAliasMap = new Map<string, string>();
|
|
@@ -240,8 +252,8 @@ export function parseBoxesAndLines(
|
|
|
240
252
|
// regions.
|
|
241
253
|
if (
|
|
242
254
|
trimmed.includes('|') &&
|
|
243
|
-
|
|
244
|
-
|
|
255
|
+
!ARROW_LABEL_PIPE_DIRECTED_RE.test(trimmed) &&
|
|
256
|
+
!ARROW_LABEL_PIPE_UNDIRECTED_RE.test(trimmed)
|
|
245
257
|
) {
|
|
246
258
|
result.diagnostics.push(
|
|
247
259
|
makeDgmoError(
|
|
@@ -397,7 +409,12 @@ export function parseBoxesAndLines(
|
|
|
397
409
|
if (tagBlockMatch.inlineValues) {
|
|
398
410
|
for (const rawVal of tagBlockMatch.inlineValues) {
|
|
399
411
|
const { text: cleanVal, isDefault } = stripDefaultModifier(rawVal);
|
|
400
|
-
const { label, color } = extractColor(
|
|
412
|
+
const { label, color } = extractColor(
|
|
413
|
+
cleanVal,
|
|
414
|
+
palette,
|
|
415
|
+
result.diagnostics,
|
|
416
|
+
lineNum
|
|
417
|
+
);
|
|
401
418
|
newTagGroup.entries.push({
|
|
402
419
|
value: label,
|
|
403
420
|
color: color ?? AUTO_TAG_COLOR_SENTINEL,
|
|
@@ -417,7 +434,12 @@ export function parseBoxesAndLines(
|
|
|
417
434
|
// Tag group entries (indented under tag heading)
|
|
418
435
|
if (currentTagGroup && !contentStarted && indent > 0) {
|
|
419
436
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
420
|
-
const { label, color } = extractColor(
|
|
437
|
+
const { label, color } = extractColor(
|
|
438
|
+
cleanEntry,
|
|
439
|
+
palette,
|
|
440
|
+
result.diagnostics,
|
|
441
|
+
lineNum
|
|
442
|
+
);
|
|
421
443
|
currentTagGroup.entries.push({
|
|
422
444
|
value: label,
|
|
423
445
|
color: color ?? AUTO_TAG_COLOR_SENTINEL,
|
|
@@ -436,6 +458,52 @@ export function parseBoxesAndLines(
|
|
|
436
458
|
currentTagGroup = null;
|
|
437
459
|
}
|
|
438
460
|
|
|
461
|
+
// `layout` coordinate block (Canvas Editor spike). A bare `layout` heading
|
|
462
|
+
// at indent 0 opens the block; indented `<node-id>: <x>, <y>` entries map a
|
|
463
|
+
// node to an absolute position. Any non-indented line closes it. Quarantined
|
|
464
|
+
// before group/node/edge matching so the entries don't parse as nodes.
|
|
465
|
+
if (!inLayoutBlock && indent === 0 && trimmed === 'layout') {
|
|
466
|
+
// Disambiguate from a node legitimately NAMED `layout`: only treat this as
|
|
467
|
+
// the coordinate appendix when the next non-blank line is an indented
|
|
468
|
+
// `<id>: <x>, <y>` entry. Otherwise fall through and parse `layout` as a
|
|
469
|
+
// normal node (no silent data loss).
|
|
470
|
+
let isBlock = false;
|
|
471
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
472
|
+
const peek = lines[j]!;
|
|
473
|
+
if (!peek.trim()) continue;
|
|
474
|
+
isBlock = measureIndent(peek) > 0 && LAYOUT_ENTRY_RE.test(peek.trim());
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
if (isBlock) {
|
|
478
|
+
flushDescription();
|
|
479
|
+
closeGroupsToIndent(0);
|
|
480
|
+
inLayoutBlock = true;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (inLayoutBlock) {
|
|
485
|
+
if (indent > 0) {
|
|
486
|
+
const lm = trimmed.match(LAYOUT_ENTRY_RE);
|
|
487
|
+
if (lm) {
|
|
488
|
+
nodePositions.set(lm[1]!.trim(), {
|
|
489
|
+
x: Number(lm[2]),
|
|
490
|
+
y: Number(lm[3]),
|
|
491
|
+
});
|
|
492
|
+
} else {
|
|
493
|
+
result.diagnostics.push(
|
|
494
|
+
makeDgmoError(
|
|
495
|
+
lineNum,
|
|
496
|
+
`Invalid layout entry "${trimmed}" — expected "<node-id>: <x>, <y>"`,
|
|
497
|
+
'warning'
|
|
498
|
+
)
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
// indent 0 → block ends; fall through to process this line normally.
|
|
504
|
+
inLayoutBlock = false;
|
|
505
|
+
}
|
|
506
|
+
|
|
439
507
|
// Description collection: indented non-edge lines under a node
|
|
440
508
|
if (descState !== null) {
|
|
441
509
|
if (indent > descState.indent) {
|
|
@@ -762,6 +830,31 @@ export function parseBoxesAndLines(
|
|
|
762
830
|
// passed to extractColor above), so auto colors match for consistency.
|
|
763
831
|
finalizeAutoTagColors(result.tagGroups as Writable<TagGroup>[]);
|
|
764
832
|
|
|
833
|
+
// Attach parsed `layout` positions. Validate coverage: unknown ids warn; a
|
|
834
|
+
// PARTIAL block (some nodes unpositioned) is honored by neither pin nor seed —
|
|
835
|
+
// the layout engine ignores it and auto-lays-out (Decision 3, AC12), so emit a
|
|
836
|
+
// single diagnostic naming the gap.
|
|
837
|
+
if (nodePositions.size > 0) {
|
|
838
|
+
const nodeLabelSet = new Set(result.nodes.map((n) => n.label));
|
|
839
|
+
for (const id of nodePositions.keys()) {
|
|
840
|
+
if (!nodeLabelSet.has(id)) {
|
|
841
|
+
pushWarning(0, `layout entry for unknown node "${id}" (ignored)`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const unpositioned = result.nodes
|
|
845
|
+
.filter((n) => !nodePositions.has(n.label))
|
|
846
|
+
.map((n) => n.label);
|
|
847
|
+
if (unpositioned.length > 0) {
|
|
848
|
+
pushWarning(
|
|
849
|
+
0,
|
|
850
|
+
`layout block is partial — ${unpositioned.length} node(s) without coordinates ` +
|
|
851
|
+
`(${unpositioned.slice(0, 5).join(', ')}${unpositioned.length > 5 ? '…' : ''}); ` +
|
|
852
|
+
`ignoring the block and auto-laying-out`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
result.nodePositions = nodePositions;
|
|
856
|
+
}
|
|
857
|
+
|
|
765
858
|
// Post-parse: inject default tag metadata and validate tag values
|
|
766
859
|
if (result.tagGroups.length > 0) {
|
|
767
860
|
injectDefaultTagMetadata(result.nodes, result.tagGroups);
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
relativeLuminance,
|
|
31
31
|
shapeFill,
|
|
32
32
|
valueRampColor,
|
|
33
|
+
themeBaseBg,
|
|
33
34
|
} from '../palettes/color-utils';
|
|
34
35
|
import { resolveColor } from '../colors';
|
|
35
36
|
import { resolveTagColor } from '../utils/tag-groups';
|
|
@@ -56,9 +57,13 @@ const DIAGRAM_PADDING = 20;
|
|
|
56
57
|
const NODE_FONT_SIZE = 11;
|
|
57
58
|
const MIN_NODE_FONT_SIZE = 9;
|
|
58
59
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
import {
|
|
61
|
+
EDGE_STROKE_WIDTH,
|
|
62
|
+
NODE_STROKE_WIDTH,
|
|
63
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
61
64
|
const NODE_RX = 8;
|
|
65
|
+
// Intentional deviation (conventions §3): boxes-and-lines uses a 4px collapse
|
|
66
|
+
// bar (and 4px separator gap in layout.ts) — denser than the 6px default.
|
|
62
67
|
const COLLAPSE_BAR_HEIGHT = 4;
|
|
63
68
|
const ARROWHEAD_W = 5;
|
|
64
69
|
const ARROWHEAD_H = 4;
|
|
@@ -89,6 +94,15 @@ const lineGeneratorTB = d3Shape
|
|
|
89
94
|
.y((d) => d.y)
|
|
90
95
|
.curve(d3Shape.curveBasis);
|
|
91
96
|
|
|
97
|
+
// Straight (linear) generator for pinned-layout connectors (Canvas Editor
|
|
98
|
+
// spike). curveBasis collapses a 2-point polyline, so border-clipped straight
|
|
99
|
+
// edges must draw linearly.
|
|
100
|
+
const lineGeneratorStraight = d3Shape
|
|
101
|
+
.line<{ x: number; y: number }>()
|
|
102
|
+
.x((d) => d.x)
|
|
103
|
+
.y((d) => d.y)
|
|
104
|
+
.curve(d3Shape.curveLinear);
|
|
105
|
+
|
|
92
106
|
// ── Text fitting ───────────────────────────────────────────
|
|
93
107
|
|
|
94
108
|
function splitCamelCase(word: string): string[] {
|
|
@@ -425,6 +439,11 @@ interface BLRenderOptions {
|
|
|
425
439
|
/** When 'app', the description toggle is hosted by the app overlay strip
|
|
426
440
|
* (inline gear suppressed, controls row + anchor reserved). */
|
|
427
441
|
controlsHost?: 'app' | 'inline';
|
|
442
|
+
/** Explicit value-ramp domain override. When provided, the choropleth ramp
|
|
443
|
+
* uses these endpoints instead of computing min/max from `parsed.nodes`.
|
|
444
|
+
* Focus mode passes the GLOBAL (pre-filter) domain so neighbor colours stay
|
|
445
|
+
* stable when only a subset is rendered (Decision 20 / FM1). */
|
|
446
|
+
rampDomain?: { min: number; max: number };
|
|
428
447
|
}
|
|
429
448
|
|
|
430
449
|
export function renderBoxesAndLines(
|
|
@@ -446,6 +465,7 @@ export function renderBoxesAndLines(
|
|
|
446
465
|
onToggleControlsExpand,
|
|
447
466
|
exportMode = false,
|
|
448
467
|
controlsHost,
|
|
468
|
+
rampDomain,
|
|
449
469
|
} = options ?? {};
|
|
450
470
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
451
471
|
|
|
@@ -479,8 +499,10 @@ export function renderBoxesAndLines(
|
|
|
479
499
|
// Anchor the low end at the lowest value (not 0) to maximise within-diagram
|
|
480
500
|
// dynamic range; mirrors the map's region-metric ramp. Equal-value data
|
|
481
501
|
// (rampMin === rampMax) falls back to t = 1 in fillForValue below.
|
|
482
|
-
|
|
483
|
-
|
|
502
|
+
// A caller-supplied domain (focus mode) wins so colours don't shift when a
|
|
503
|
+
// subset is rendered; otherwise derive from the nodes on screen.
|
|
504
|
+
const rampMin = rampDomain?.min ?? (hasRamp ? Math.min(...nodeValues) : 0);
|
|
505
|
+
const rampMax = rampDomain?.max ?? Math.max(...nodeValues);
|
|
484
506
|
// Default hue = palette.primary (NOT red like the map — boxes have no water to
|
|
485
507
|
// stand out against, and red reads as alarm on a neutral metric). A trailing
|
|
486
508
|
// color on `box-metric` overrides.
|
|
@@ -614,8 +636,43 @@ export function renderBoxesAndLines(
|
|
|
614
636
|
const scaleY = height / (contentH + sDiagramPadding * 2);
|
|
615
637
|
const scale = Math.min(scaleX, scaleY, 3);
|
|
616
638
|
|
|
617
|
-
|
|
618
|
-
|
|
639
|
+
// Pinned (`layout`-block) coordinates can leave the content's real extent
|
|
640
|
+
// sitting off-centre inside layout.width/height — e.g. nodes pinned far from
|
|
641
|
+
// the origin bake a wide gap on one side. Re-centre the content within its
|
|
642
|
+
// own box by equalizing opposite margins. Gated to pinned diagrams so the
|
|
643
|
+
// auto-layout path (and its snapshots) stays byte-identical: that path
|
|
644
|
+
// already produces symmetric margins.
|
|
645
|
+
let centerShiftX = 0;
|
|
646
|
+
let centerShiftY = 0;
|
|
647
|
+
if (parsed.nodePositions && parsed.nodePositions.size > 0) {
|
|
648
|
+
let bMinX = Infinity,
|
|
649
|
+
bMinY = Infinity,
|
|
650
|
+
bMaxX = -Infinity,
|
|
651
|
+
bMaxY = -Infinity;
|
|
652
|
+
const accB = (x: number, y: number) => {
|
|
653
|
+
if (x < bMinX) bMinX = x;
|
|
654
|
+
if (x > bMaxX) bMaxX = x;
|
|
655
|
+
if (y < bMinY) bMinY = y;
|
|
656
|
+
if (y > bMaxY) bMaxY = y;
|
|
657
|
+
};
|
|
658
|
+
for (const n of layout.nodes) {
|
|
659
|
+
accB(n.x - n.width / 2, n.y - n.height / 2);
|
|
660
|
+
accB(n.x + n.width / 2, n.y + n.height / 2);
|
|
661
|
+
}
|
|
662
|
+
for (const g of layout.groups) {
|
|
663
|
+
accB(g.x - g.width / 2, g.y - g.height / 2);
|
|
664
|
+
accB(g.x + g.width / 2, g.y + g.height / 2);
|
|
665
|
+
}
|
|
666
|
+
for (const e of layout.edges) for (const p of e.points) accB(p.x, p.y);
|
|
667
|
+
if (Number.isFinite(bMinX)) {
|
|
668
|
+
centerShiftX = (layout.width - bMaxX - bMinX) / 2;
|
|
669
|
+
centerShiftY = (layout.height - bMaxY - bMinY) / 2;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const offsetX = (width - contentW * scale) / 2 + centerShiftX * scale;
|
|
674
|
+
const offsetY =
|
|
675
|
+
sDiagramPadding + titleOffset + legendH + centerShiftY * scale;
|
|
619
676
|
|
|
620
677
|
const svg: D3Svg = d3Selection
|
|
621
678
|
.select(container)
|
|
@@ -703,7 +760,7 @@ export function renderBoxesAndLines(
|
|
|
703
760
|
|
|
704
761
|
if (group.collapsed) {
|
|
705
762
|
// Collapsed: solid rounded rect matching node style + 6px collapse bar
|
|
706
|
-
const fillColor =
|
|
763
|
+
const fillColor = themeBaseBg(palette, isDark);
|
|
707
764
|
const strokeColor = palette.border;
|
|
708
765
|
|
|
709
766
|
groupG
|
|
@@ -896,7 +953,11 @@ export function renderBoxesAndLines(
|
|
|
896
953
|
edgeGroups.set(i, edgeG as unknown as D3G);
|
|
897
954
|
|
|
898
955
|
const markerId = `bl-arrow-${color.replace('#', '')}`;
|
|
899
|
-
const gen =
|
|
956
|
+
const gen = le.straight
|
|
957
|
+
? lineGeneratorStraight
|
|
958
|
+
: parsed.direction === 'TB'
|
|
959
|
+
? lineGeneratorTB
|
|
960
|
+
: lineGeneratorLR;
|
|
900
961
|
const path = edgeG
|
|
901
962
|
.append('path')
|
|
902
963
|
.attr('class', 'bl-edge')
|
|
@@ -1394,6 +1455,48 @@ export function renderBoxesAndLines(
|
|
|
1394
1455
|
});
|
|
1395
1456
|
legendG.selectAll('[data-legend-group]').classed('bl-legend-group', true);
|
|
1396
1457
|
}
|
|
1458
|
+
|
|
1459
|
+
// ── Focus mode: one reusable hover-reveal icon (interactive only) ──
|
|
1460
|
+
// A single hidden icon the app repositions over the hovered box/group and
|
|
1461
|
+
// stamps `data-focus-id`/`data-focus-kind` on (Decision 22 / ADR-4) — NOT one
|
|
1462
|
+
// per node (~4k elements on a large graph). Appended to the SVG root so the
|
|
1463
|
+
// app positions it in root (screen-mapped) coordinates, counter-scaled to a
|
|
1464
|
+
// constant size regardless of fit. Excluded from export like org's icon.
|
|
1465
|
+
if (!exportDims && !exportMode) {
|
|
1466
|
+
const iconSize = 14;
|
|
1467
|
+
const focusG = svg
|
|
1468
|
+
.append('g')
|
|
1469
|
+
.attr('class', 'bl-focus-icon')
|
|
1470
|
+
.attr('data-export-ignore', 'true')
|
|
1471
|
+
.style('display', 'none')
|
|
1472
|
+
.style('pointer-events', 'auto')
|
|
1473
|
+
.style('cursor', 'pointer');
|
|
1474
|
+
// Hit area
|
|
1475
|
+
focusG
|
|
1476
|
+
.append('rect')
|
|
1477
|
+
.attr('x', -3)
|
|
1478
|
+
.attr('y', -3)
|
|
1479
|
+
.attr('width', iconSize + 6)
|
|
1480
|
+
.attr('height', iconSize + 6)
|
|
1481
|
+
.attr('fill', 'transparent');
|
|
1482
|
+
// Scope/target icon: outer circle + inner dot (mirrors org-focus-icon)
|
|
1483
|
+
const cx = iconSize / 2;
|
|
1484
|
+
const cy = iconSize / 2;
|
|
1485
|
+
focusG
|
|
1486
|
+
.append('circle')
|
|
1487
|
+
.attr('cx', cx)
|
|
1488
|
+
.attr('cy', cy)
|
|
1489
|
+
.attr('r', iconSize / 2 - 1)
|
|
1490
|
+
.attr('fill', palette.bg)
|
|
1491
|
+
.attr('stroke', palette.textMuted)
|
|
1492
|
+
.attr('stroke-width', 1.5);
|
|
1493
|
+
focusG
|
|
1494
|
+
.append('circle')
|
|
1495
|
+
.attr('cx', cx)
|
|
1496
|
+
.attr('cy', cy)
|
|
1497
|
+
.attr('r', 2)
|
|
1498
|
+
.attr('fill', palette.textMuted);
|
|
1499
|
+
}
|
|
1397
1500
|
}
|
|
1398
1501
|
|
|
1399
1502
|
// ── Export helper ──────────────────────────────────────────
|
|
@@ -42,6 +42,15 @@ export interface ParsedBoxesAndLines {
|
|
|
42
42
|
readonly notes?: readonly DiagramNote[];
|
|
43
43
|
readonly initialHiddenTagValues: ReadonlyMap<string, ReadonlySet<string>>;
|
|
44
44
|
readonly direction: 'LR' | 'TB';
|
|
45
|
+
/** Optional per-node absolute positions, parsed from a trailing `layout`
|
|
46
|
+
* block (`<node-id>: <x>, <y>`). Diagram-space coordinates. When present and
|
|
47
|
+
* covering EVERY node, the layout engine bypasses auto-placement and pins
|
|
48
|
+
* nodes here (see Decision 3 — two clean modes). A partial block is ignored
|
|
49
|
+
* with a diagnostic (AC12). Experimental — Canvas Editor spike. */
|
|
50
|
+
readonly nodePositions?: ReadonlyMap<
|
|
51
|
+
string,
|
|
52
|
+
{ readonly x: number; readonly y: number }
|
|
53
|
+
>;
|
|
45
54
|
/** `box-metric <label> [low] [high]` — names the value-ramp dimension and
|
|
46
55
|
* optionally sets its endpoint colours. One color = high hue over a neutral
|
|
47
56
|
* low; two = explicit `low high`. Mirror of map's `region-metric`. */
|
package/src/c4/parser.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
descriptionBareRemovedMessage,
|
|
9
9
|
formatDgmoError,
|
|
10
10
|
makeDgmoError,
|
|
11
|
+
makeFail,
|
|
11
12
|
METADATA_DIAGNOSTIC_CODES,
|
|
12
13
|
pipeOperatorRemovedMessage,
|
|
13
14
|
suggest,
|
|
@@ -296,12 +297,7 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
296
297
|
result.error = formatDgmoError(diag);
|
|
297
298
|
};
|
|
298
299
|
|
|
299
|
-
const fail = (
|
|
300
|
-
const diag = makeDgmoError(line, message);
|
|
301
|
-
result.diagnostics.push(diag);
|
|
302
|
-
result.error = formatDgmoError(diag);
|
|
303
|
-
return result;
|
|
304
|
-
};
|
|
300
|
+
const fail = makeFail(result);
|
|
305
301
|
|
|
306
302
|
if (!content?.trim()) {
|
|
307
303
|
return fail(0, 'No content provided');
|
|
@@ -455,7 +451,12 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
455
451
|
const indent = measureIndent(line);
|
|
456
452
|
if (indent > 0) {
|
|
457
453
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
458
|
-
const { label, color } = extractColor(
|
|
454
|
+
const { label, color } = extractColor(
|
|
455
|
+
cleanEntry,
|
|
456
|
+
palette,
|
|
457
|
+
result.diagnostics,
|
|
458
|
+
lineNumber
|
|
459
|
+
);
|
|
459
460
|
// Bare value (no explicit color) → keep it; the post-parse
|
|
460
461
|
// finalize pass assigns a deterministic palette color.
|
|
461
462
|
if (isDefault) {
|
package/src/c4/renderer.ts
CHANGED
|
@@ -37,16 +37,18 @@ const DESC_FONT_SIZE = 11;
|
|
|
37
37
|
const DESC_LINE_HEIGHT = 16;
|
|
38
38
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
39
39
|
const TECH_FONT_SIZE = 10;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
import {
|
|
41
|
+
EDGE_STROKE_WIDTH,
|
|
42
|
+
NODE_STROKE_WIDTH,
|
|
43
|
+
CARD_RADIUS,
|
|
44
|
+
META_FONT_SIZE,
|
|
45
|
+
META_LINE_HEIGHT,
|
|
46
|
+
} from '../utils/visual-conventions'; // shared (Story 111.1)
|
|
43
47
|
const CARD_H_PAD = 20;
|
|
44
48
|
const CARD_V_PAD = 14;
|
|
45
49
|
const TYPE_LABEL_HEIGHT = 18;
|
|
46
50
|
const DIVIDER_GAP = 6;
|
|
47
51
|
const NAME_HEIGHT = 20;
|
|
48
|
-
const META_FONT_SIZE = 11;
|
|
49
|
-
const META_LINE_HEIGHT = 16;
|
|
50
52
|
const BOUNDARY_LABEL_FONT_SIZE = 12;
|
|
51
53
|
const BOUNDARY_STROKE_WIDTH = 1.5;
|
|
52
54
|
const BOUNDARY_RADIUS = 8;
|
|
@@ -87,6 +87,10 @@ export interface ChartTypeDescriptor {
|
|
|
87
87
|
readonly category: RenderCategory;
|
|
88
88
|
readonly parse: ParseFn;
|
|
89
89
|
readonly measure?: (content: string) => ContentCounts;
|
|
90
|
+
readonly minDims?: (counts: ContentCounts) => {
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
};
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
// ============================================================
|
|
@@ -210,6 +214,92 @@ function measureInfra(content: string): ContentCounts {
|
|
|
210
214
|
return { nodes: parsed.nodes.length };
|
|
211
215
|
}
|
|
212
216
|
|
|
217
|
+
// ============================================================
|
|
218
|
+
// minDims() implementations — relocated verbatim from computeMinDimensions() in
|
|
219
|
+
// utils/scaling.ts so the registry owns per-type minimum-dimension formulas
|
|
220
|
+
// alongside measure(). Each maps ContentCounts → {width,height}. Types without a
|
|
221
|
+
// minDims fall back to {300,200} (the old switch `default`) via the
|
|
222
|
+
// REGISTRY_BY_ID lookup in dimensions.ts.
|
|
223
|
+
// ============================================================
|
|
224
|
+
|
|
225
|
+
function minDimsSequence(c: ContentCounts): { width: number; height: number } {
|
|
226
|
+
return {
|
|
227
|
+
width: Math.max((c.participants ?? 2) * 80, 320),
|
|
228
|
+
height: Math.max((c.messages ?? 1) * 20 + 120, 200),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function minDimsRaci(c: ContentCounts): { width: number; height: number } {
|
|
232
|
+
return {
|
|
233
|
+
width: Math.max((c.roles ?? 2) * 50 + 180, 300),
|
|
234
|
+
height: Math.max((c.tasks ?? 1) * 28 + 80, 200),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function minDimsMindmap(c: ContentCounts): { width: number; height: number } {
|
|
238
|
+
return {
|
|
239
|
+
width: Math.max((c.nodes ?? 3) * 30, 300),
|
|
240
|
+
height: Math.max((c.depth ?? 2) * 60, 200),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function minDimsTechRadar(): { width: number; height: number } {
|
|
244
|
+
return { width: 360, height: 400 };
|
|
245
|
+
}
|
|
246
|
+
function minDimsHeatmap(c: ContentCounts): { width: number; height: number } {
|
|
247
|
+
return {
|
|
248
|
+
width: Math.max((c.columns ?? 3) * 40, 300),
|
|
249
|
+
height: Math.max((c.rows ?? 3) * 30 + 60, 200),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function minDimsArc(c: ContentCounts): { width: number; height: number } {
|
|
253
|
+
return {
|
|
254
|
+
width: 300,
|
|
255
|
+
height: Math.max((c.nodes ?? 3) * 20 + 120, 200),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function minDimsOrg(c: ContentCounts): { width: number; height: number } {
|
|
259
|
+
return {
|
|
260
|
+
width: Math.max((c.nodes ?? 3) * 60, 300),
|
|
261
|
+
height: Math.max((c.depth ?? 2) * 80, 200),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function minDimsGantt(c: ContentCounts): { width: number; height: number } {
|
|
265
|
+
return {
|
|
266
|
+
width: 400,
|
|
267
|
+
height: Math.max((c.tasks ?? 3) * 24 + 80, 200),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function minDimsKanban(c: ContentCounts): { width: number; height: number } {
|
|
271
|
+
return {
|
|
272
|
+
width: Math.max((c.columns ?? 3) * 120, 360),
|
|
273
|
+
height: 300,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// er + class share this formula.
|
|
277
|
+
function minDimsEntities(c: ContentCounts): { width: number; height: number } {
|
|
278
|
+
return {
|
|
279
|
+
width: Math.max((c.nodes ?? 2) * 140, 300),
|
|
280
|
+
height: Math.max((c.nodes ?? 2) * 80, 200),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// flowchart + state share this formula.
|
|
284
|
+
function minDimsGraph(c: ContentCounts): { width: number; height: number } {
|
|
285
|
+
return {
|
|
286
|
+
width: Math.max((c.nodes ?? 3) * 60, 300),
|
|
287
|
+
height: Math.max((c.nodes ?? 3) * 50, 200),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function minDimsPert(c: ContentCounts): { width: number; height: number } {
|
|
291
|
+
return {
|
|
292
|
+
width: Math.max((c.tasks ?? 3) * 80, 340),
|
|
293
|
+
height: Math.max((c.tasks ?? 3) * 40 + 80, 200),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function minDimsInfra(c: ContentCounts): { width: number; height: number } {
|
|
297
|
+
return {
|
|
298
|
+
width: Math.max((c.nodes ?? 3) * 80, 300),
|
|
299
|
+
height: Math.max((c.nodes ?? 3) * 60, 200),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
213
303
|
// ============================================================
|
|
214
304
|
// THE REGISTRY — ordered to match the previous chartTypeParsers grouping
|
|
215
305
|
// (structured diagrams, standard ECharts, extended ECharts, D3 visualizations,
|
|
@@ -224,32 +314,49 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
|
|
|
224
314
|
category: 'diagram',
|
|
225
315
|
parse: parseSequenceDgmo,
|
|
226
316
|
measure: measureSequence,
|
|
317
|
+
minDims: minDimsSequence,
|
|
227
318
|
},
|
|
228
319
|
{
|
|
229
320
|
id: 'flowchart',
|
|
230
321
|
category: 'diagram',
|
|
231
322
|
parse: parseFlowchart,
|
|
232
323
|
measure: measureFlowchart,
|
|
324
|
+
minDims: minDimsGraph,
|
|
233
325
|
},
|
|
234
326
|
{
|
|
235
327
|
id: 'class',
|
|
236
328
|
category: 'diagram',
|
|
237
329
|
parse: parseClassDiagram,
|
|
238
330
|
measure: measureClass,
|
|
331
|
+
minDims: minDimsEntities,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'er',
|
|
335
|
+
category: 'diagram',
|
|
336
|
+
parse: parseERDiagram,
|
|
337
|
+
measure: measureER,
|
|
338
|
+
minDims: minDimsEntities,
|
|
239
339
|
},
|
|
240
|
-
{ id: 'er', category: 'diagram', parse: parseERDiagram, measure: measureER },
|
|
241
340
|
{
|
|
242
341
|
id: 'state',
|
|
243
342
|
category: 'diagram',
|
|
244
343
|
parse: parseState,
|
|
245
344
|
measure: measureStateGraph,
|
|
345
|
+
minDims: minDimsGraph,
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: 'org',
|
|
349
|
+
category: 'diagram',
|
|
350
|
+
parse: parseOrg,
|
|
351
|
+
measure: measureOrg,
|
|
352
|
+
minDims: minDimsOrg,
|
|
246
353
|
},
|
|
247
|
-
{ id: 'org', category: 'diagram', parse: parseOrg, measure: measureOrg },
|
|
248
354
|
{
|
|
249
355
|
id: 'kanban',
|
|
250
356
|
category: 'diagram',
|
|
251
357
|
parse: parseKanban,
|
|
252
358
|
measure: measureKanban,
|
|
359
|
+
minDims: minDimsKanban,
|
|
253
360
|
},
|
|
254
361
|
{ id: 'c4', category: 'diagram', parse: parseC4 },
|
|
255
362
|
{ id: 'sitemap', category: 'diagram', parse: parseSitemap },
|
|
@@ -258,24 +365,39 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
|
|
|
258
365
|
category: 'diagram',
|
|
259
366
|
parse: parseInfra,
|
|
260
367
|
measure: measureInfra,
|
|
368
|
+
minDims: minDimsInfra,
|
|
261
369
|
},
|
|
262
370
|
{
|
|
263
371
|
id: 'gantt',
|
|
264
372
|
category: 'diagram',
|
|
265
373
|
parse: parseGantt,
|
|
266
374
|
measure: measureGantt,
|
|
375
|
+
minDims: minDimsGantt,
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
id: 'pert',
|
|
379
|
+
category: 'diagram',
|
|
380
|
+
parse: parsePert,
|
|
381
|
+
measure: measurePert,
|
|
382
|
+
minDims: minDimsPert,
|
|
267
383
|
},
|
|
268
|
-
{ id: 'pert', category: 'diagram', parse: parsePert, measure: measurePert },
|
|
269
384
|
{ id: 'boxes-and-lines', category: 'diagram', parse: parseBoxesAndLines },
|
|
270
385
|
{
|
|
271
386
|
id: 'mindmap',
|
|
272
387
|
category: 'diagram',
|
|
273
388
|
parse: parseMindmap,
|
|
274
389
|
measure: measureMindmap,
|
|
390
|
+
minDims: minDimsMindmap,
|
|
275
391
|
},
|
|
276
392
|
{ id: 'wireframe', category: 'diagram', parse: parseWireframe },
|
|
277
393
|
{ id: 'journey-map', category: 'diagram', parse: parseJourneyMap },
|
|
278
|
-
{
|
|
394
|
+
{
|
|
395
|
+
id: 'raci',
|
|
396
|
+
category: 'diagram',
|
|
397
|
+
parse: parseRaci,
|
|
398
|
+
measure: measureRaci,
|
|
399
|
+
minDims: minDimsRaci,
|
|
400
|
+
},
|
|
279
401
|
{ id: 'rasci', category: 'diagram', parse: parseRaci, measure: measureRaci },
|
|
280
402
|
{ id: 'daci', category: 'diagram', parse: parseRaci, measure: measureRaci },
|
|
281
403
|
|
|
@@ -300,6 +422,7 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
|
|
|
300
422
|
category: 'data-chart',
|
|
301
423
|
parse: parseHeatmap,
|
|
302
424
|
measure: measureHeatmap,
|
|
425
|
+
minDims: minDimsHeatmap,
|
|
303
426
|
},
|
|
304
427
|
{ id: 'funnel', category: 'data-chart', parse: parseFunnel },
|
|
305
428
|
|
|
@@ -311,6 +434,7 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
|
|
|
311
434
|
category: 'visualization',
|
|
312
435
|
parse: parseArc,
|
|
313
436
|
measure: measureArc,
|
|
437
|
+
minDims: minDimsArc,
|
|
314
438
|
},
|
|
315
439
|
{ id: 'timeline', category: 'visualization', parse: parseTimeline },
|
|
316
440
|
{ id: 'venn', category: 'visualization', parse: parseVenn },
|
|
@@ -322,6 +446,7 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
|
|
|
322
446
|
category: 'visualization',
|
|
323
447
|
parse: parseTechRadar,
|
|
324
448
|
measure: measureTechRadar,
|
|
449
|
+
minDims: minDimsTechRadar,
|
|
325
450
|
},
|
|
326
451
|
{ id: 'cycle', category: 'visualization', parse: parseCycle },
|
|
327
452
|
{ id: 'pyramid', category: 'visualization', parse: parsePyramid },
|
package/src/chart-types.ts
CHANGED
|
@@ -46,7 +46,7 @@ export const chartTypes: readonly ChartTypeMeta[] = [
|
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
id: 'sequence',
|
|
49
|
-
description: 'Message
|
|
49
|
+
description: 'Message request and response interaction flows',
|
|
50
50
|
fallback: true,
|
|
51
51
|
},
|
|
52
52
|
{
|
|
@@ -143,7 +143,7 @@ export const chartTypes: readonly ChartTypeMeta[] = [
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
id: 'slope',
|
|
146
|
-
description: 'Change between
|
|
146
|
+
description: 'Change between 2 time periods',
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
149
|
id: 'sankey',
|
|
@@ -173,7 +173,7 @@ export const chartTypes: readonly ChartTypeMeta[] = [
|
|
|
173
173
|
// ── Tier 4 — General-purpose data charts ──────────────────
|
|
174
174
|
{
|
|
175
175
|
id: 'bar',
|
|
176
|
-
description: 'Categorical comparisons',
|
|
176
|
+
description: 'Categorical comparisons for 3 - 5 figures',
|
|
177
177
|
fallback: true,
|
|
178
178
|
},
|
|
179
179
|
{
|