@diagrammo/dgmo 0.25.5 → 0.27.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/README.md +3 -3
- package/dist/advanced.cjs +4255 -2756
- package/dist/advanced.d.cts +285 -59
- package/dist/advanced.d.ts +285 -59
- package/dist/advanced.js +4253 -2750
- package/dist/auto.cjs +4051 -2589
- package/dist/auto.js +124 -122
- package/dist/auto.mjs +4051 -2589
- package/dist/cli.cjs +172 -170
- package/dist/editor.cjs +4 -0
- package/dist/editor.js +4 -0
- package/dist/highlight.cjs +4 -0
- package/dist/highlight.js +4 -0
- package/dist/index.cjs +4076 -2591
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +4076 -2591
- package/dist/internal.cjs +4255 -2756
- package/dist/internal.d.cts +285 -59
- package/dist/internal.d.ts +285 -59
- package/dist/internal.js +4253 -2750
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/airport-collisions.json +1 -0
- package/dist/map-data/airports.json +1 -0
- package/docs/language-reference.md +68 -18
- package/gallery/fixtures/boxes-and-lines-diverging.dgmo +15 -0
- package/gallery/fixtures/map-choropleth-diverging.dgmo +9 -0
- package/gallery/fixtures/map-region-values.dgmo +13 -0
- package/gallery/fixtures/map-subnational-zoom.dgmo +12 -0
- package/gallery/fixtures/map-tagged-legs.dgmo +16 -0
- package/gallery/fixtures/map-undirected-edges.dgmo +12 -0
- package/package.json +1 -1
- package/src/advanced.ts +3 -6
- package/src/auto/index.ts +1 -1
- package/src/boxes-and-lines/layout.ts +146 -26
- package/src/boxes-and-lines/parser.ts +43 -8
- package/src/boxes-and-lines/renderer.ts +223 -96
- package/src/boxes-and-lines/types.ts +9 -2
- package/src/c4/layout.ts +14 -32
- package/src/c4/parser.ts +9 -5
- package/src/c4/renderer.ts +34 -39
- package/src/class/layout.ts +118 -18
- package/src/class/parser.ts +35 -1
- package/src/class/renderer.ts +58 -2
- package/src/class/types.ts +3 -0
- package/src/cli.ts +4 -4
- package/src/completion-types.ts +0 -1
- package/src/completion.ts +106 -51
- package/src/cycle/layout.ts +55 -72
- package/src/cycle/renderer.ts +11 -6
- package/src/d3.ts +78 -117
- package/src/diagnostics.ts +16 -0
- package/src/echarts.ts +46 -33
- package/src/editor/keywords.ts +4 -0
- package/src/er/layout.ts +114 -22
- package/src/er/parser.ts +28 -1
- package/src/er/renderer.ts +55 -2
- package/src/er/types.ts +3 -0
- package/src/gantt/renderer.ts +46 -38
- package/src/gantt/resolver.ts +9 -2
- package/src/graph/edge-spline.ts +29 -0
- package/src/graph/flowchart-parser.ts +35 -2
- package/src/graph/flowchart-renderer.ts +80 -52
- package/src/graph/layout.ts +206 -23
- package/src/graph/notes.ts +21 -0
- package/src/graph/state-parser.ts +26 -1
- package/src/graph/state-renderer.ts +80 -52
- package/src/graph/types.ts +13 -0
- package/src/index.ts +1 -1
- package/src/infra/layout.ts +46 -26
- package/src/infra/parser.ts +1 -1
- package/src/infra/renderer.ts +16 -7
- package/src/journey-map/layout.ts +38 -49
- package/src/journey-map/renderer.ts +22 -45
- package/src/kanban/renderer.ts +15 -6
- package/src/label-layout.ts +3 -3
- package/src/map/completion.ts +77 -22
- package/src/map/context-labels.ts +57 -12
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/airport-collisions.json +1 -0
- package/src/map/data/airports.json +1 -0
- package/src/map/data/types.ts +19 -0
- package/src/map/layout.ts +1196 -90
- package/src/map/legend-band.ts +2 -2
- package/src/map/load-data.ts +10 -1
- package/src/map/parser.ts +61 -32
- package/src/map/renderer.ts +284 -12
- package/src/map/resolved-types.ts +15 -1
- package/src/map/resolver.ts +132 -12
- package/src/map/types.ts +28 -8
- package/src/migrate/embedded.ts +9 -7
- package/src/mindmap/text-wrap.ts +13 -14
- package/src/org/layout.ts +19 -17
- package/src/org/renderer.ts +11 -4
- package/src/palettes/color-utils.ts +82 -21
- package/src/palettes/index.ts +0 -19
- package/src/palettes/registry.ts +1 -1
- package/src/palettes/types.ts +2 -2
- package/src/pert/layout.ts +48 -40
- package/src/pert/parser.ts +0 -14
- package/src/pert/renderer.ts +30 -43
- package/src/pyramid/renderer.ts +4 -5
- package/src/raci/renderer.ts +42 -70
- package/src/render.ts +1 -1
- package/src/ring/renderer.ts +1 -2
- package/src/sequence/parser.ts +100 -22
- package/src/sequence/renderer.ts +75 -50
- package/src/sitemap/layout.ts +27 -19
- package/src/sitemap/renderer.ts +12 -5
- package/src/tech-radar/renderer.ts +11 -35
- package/src/utils/arrow-markers.ts +51 -0
- package/src/utils/fit-canvas.ts +64 -0
- package/src/utils/legend-constants.ts +8 -54
- package/src/utils/legend-d3.ts +10 -7
- package/src/utils/legend-layout.ts +7 -4
- package/src/utils/legend-types.ts +10 -4
- package/src/utils/note-box/constants.ts +25 -0
- package/src/utils/note-box/index.ts +11 -0
- package/src/utils/note-box/metrics.ts +90 -0
- package/src/utils/note-box/svg.ts +331 -0
- package/src/utils/notes/bounds.ts +30 -0
- package/src/utils/notes/build.ts +131 -0
- package/src/utils/notes/index.ts +18 -0
- package/src/utils/notes/model.ts +19 -0
- package/src/utils/notes/parse.ts +131 -0
- package/src/utils/notes/place.ts +177 -0
- package/src/utils/notes/resolve.ts +88 -0
- package/src/utils/number-format.ts +36 -0
- package/src/utils/parsing.ts +41 -0
- package/src/utils/reserved-key-registry.ts +4 -0
- package/src/utils/text-measure.ts +122 -0
- package/src/wireframe/layout.ts +4 -2
- package/src/wireframe/renderer.ts +8 -6
- package/src/palettes/dracula.ts +0 -68
- package/src/palettes/gruvbox.ts +0 -85
- package/src/palettes/monokai.ts +0 -68
- package/src/palettes/one-dark.ts +0 -70
- package/src/palettes/rose-pine.ts +0 -84
- package/src/palettes/solarized.ts +0 -77
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
5
|
import * as d3Selection from 'd3-selection';
|
|
6
|
-
import
|
|
6
|
+
import { appendArrowheadMarkers } from '../utils/arrow-markers';
|
|
7
|
+
import { fitDiagramToCanvas } from '../utils/fit-canvas';
|
|
7
8
|
import { FONT_FAMILY } from '../fonts';
|
|
8
9
|
import type { PaletteColors } from '../palettes';
|
|
9
10
|
import { contrastText, mix, shapeFill } from '../palettes/color-utils';
|
|
@@ -11,12 +12,21 @@ import type { ParsedGraph } from './types';
|
|
|
11
12
|
import type { LayoutResult, LayoutNode } from './layout';
|
|
12
13
|
import { parseState } from './state-parser';
|
|
13
14
|
import { layoutGraph } from './layout';
|
|
15
|
+
import { edgeSplinePath } from './edge-spline';
|
|
14
16
|
import {
|
|
15
17
|
TITLE_FONT_SIZE,
|
|
16
18
|
TITLE_FONT_WEIGHT,
|
|
17
19
|
TITLE_Y,
|
|
18
20
|
} from '../utils/title-constants';
|
|
19
21
|
import { ScaleContext } from '../utils/scaling';
|
|
22
|
+
import { measureText } from '../utils/text-measure';
|
|
23
|
+
import {
|
|
24
|
+
renderNoteBox,
|
|
25
|
+
renderNoteConnector,
|
|
26
|
+
renderNoteBadge,
|
|
27
|
+
noteConnectorPoints,
|
|
28
|
+
NOTE_BADGE_RADIUS,
|
|
29
|
+
} from '../utils/note-box';
|
|
20
30
|
|
|
21
31
|
// ============================================================
|
|
22
32
|
// Constants
|
|
@@ -64,16 +74,6 @@ function stateStroke(
|
|
|
64
74
|
return nodeColor ?? stateDefaultColor(palette, colorOff);
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
// ============================================================
|
|
68
|
-
// Edge path generator
|
|
69
|
-
// ============================================================
|
|
70
|
-
|
|
71
|
-
const lineGenerator = d3Shape
|
|
72
|
-
.line<{ x: number; y: number }>()
|
|
73
|
-
.x((d) => d.x)
|
|
74
|
-
.y((d) => d.y)
|
|
75
|
-
.curve(d3Shape.curveBasis);
|
|
76
|
-
|
|
77
77
|
// ============================================================
|
|
78
78
|
// Self-loop path
|
|
79
79
|
// ============================================================
|
|
@@ -130,21 +130,23 @@ export function renderState(
|
|
|
130
130
|
|
|
131
131
|
const diagramW = layout.width;
|
|
132
132
|
const diagramH = layout.height;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
const { scale, offsetX, offsetY, canvasHeight } = fitDiagramToCanvas({
|
|
134
|
+
width,
|
|
135
|
+
height,
|
|
136
|
+
diagramW,
|
|
137
|
+
diagramH,
|
|
138
|
+
padding: sDiagramPadding,
|
|
139
|
+
titleHeight,
|
|
140
|
+
maxScale: MAX_SCALE,
|
|
141
|
+
exportMode: !!exportDims,
|
|
142
|
+
});
|
|
141
143
|
|
|
142
144
|
const svg = d3Selection
|
|
143
145
|
.select(container)
|
|
144
146
|
.append('svg')
|
|
145
147
|
.attr('width', width)
|
|
146
|
-
.attr('height',
|
|
147
|
-
.attr('viewBox', `0 0 ${width} ${
|
|
148
|
+
.attr('height', canvasHeight)
|
|
149
|
+
.attr('viewBox', `0 0 ${width} ${canvasHeight}`)
|
|
148
150
|
.attr('preserveAspectRatio', 'xMidYMin meet')
|
|
149
151
|
.style('font-family', FONT_FAMILY);
|
|
150
152
|
|
|
@@ -154,35 +156,14 @@ export function renderState(
|
|
|
154
156
|
|
|
155
157
|
const defs = svg.append('defs');
|
|
156
158
|
|
|
157
|
-
defs
|
|
158
|
-
.append('marker')
|
|
159
|
-
.attr('id', 'st-arrow')
|
|
160
|
-
.attr('viewBox', `0 0 ${sArrowheadW} ${sArrowheadH}`)
|
|
161
|
-
.attr('refX', sArrowheadW)
|
|
162
|
-
.attr('refY', sArrowheadH / 2)
|
|
163
|
-
.attr('markerWidth', sArrowheadW)
|
|
164
|
-
.attr('markerHeight', sArrowheadH)
|
|
165
|
-
.attr('orient', 'auto')
|
|
166
|
-
.append('polygon')
|
|
167
|
-
.attr('points', `0,0 ${sArrowheadW},${sArrowheadH / 2} 0,${sArrowheadH}`)
|
|
168
|
-
.attr('fill', palette.textMuted);
|
|
169
|
-
|
|
170
159
|
const edgeColors = new Set<string>();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.attr('refY', sArrowheadH / 2)
|
|
179
|
-
.attr('markerWidth', sArrowheadW)
|
|
180
|
-
.attr('markerHeight', sArrowheadH)
|
|
181
|
-
.attr('orient', 'auto')
|
|
182
|
-
.append('polygon')
|
|
183
|
-
.attr('points', `0,0 ${sArrowheadW},${sArrowheadH / 2} 0,${sArrowheadH}`)
|
|
184
|
-
.attr('fill', color);
|
|
185
|
-
}
|
|
160
|
+
appendArrowheadMarkers(defs, {
|
|
161
|
+
idPrefix: 'st',
|
|
162
|
+
width: sArrowheadW,
|
|
163
|
+
height: sArrowheadH,
|
|
164
|
+
baseFill: palette.textMuted,
|
|
165
|
+
colors: edgeColors,
|
|
166
|
+
});
|
|
186
167
|
|
|
187
168
|
if (showTitle) {
|
|
188
169
|
const titleEl = svg
|
|
@@ -281,7 +262,6 @@ export function renderState(
|
|
|
281
262
|
nodePositionMap.set(node.id, node);
|
|
282
263
|
}
|
|
283
264
|
|
|
284
|
-
const LABEL_CHAR_W = 7;
|
|
285
265
|
const LABEL_PAD = 8;
|
|
286
266
|
const LABEL_H = 16;
|
|
287
267
|
const PERP_OFFSET = 10;
|
|
@@ -298,7 +278,7 @@ export function renderState(
|
|
|
298
278
|
for (let ei = 0; ei < layout.edges.length; ei++) {
|
|
299
279
|
const edge = layout.edges[ei]!;
|
|
300
280
|
if (!edge.label) continue;
|
|
301
|
-
const bgW = edge.label
|
|
281
|
+
const bgW = measureText(edge.label, sEdgeLabelFontSize) + LABEL_PAD;
|
|
302
282
|
let lx: number, ly: number;
|
|
303
283
|
|
|
304
284
|
if (edge.source === edge.target) {
|
|
@@ -389,7 +369,7 @@ export function renderState(
|
|
|
389
369
|
}
|
|
390
370
|
}
|
|
391
371
|
} else if (edge.points.length >= 2) {
|
|
392
|
-
const pathD =
|
|
372
|
+
const pathD = edgeSplinePath(edge.points);
|
|
393
373
|
if (pathD) {
|
|
394
374
|
edgeG
|
|
395
375
|
.append('path')
|
|
@@ -433,6 +413,7 @@ export function renderState(
|
|
|
433
413
|
|
|
434
414
|
const colorOff = graph.options?.['color'] === 'off';
|
|
435
415
|
const solid = graph.options?.['solid-fill'] === 'on';
|
|
416
|
+
const noNotes = graph.options?.['no-notes'] === 'on';
|
|
436
417
|
for (const node of layout.nodes) {
|
|
437
418
|
const isCollapsedGroup = collapsedGroupIds.has(node.id);
|
|
438
419
|
|
|
@@ -459,6 +440,10 @@ export function renderState(
|
|
|
459
440
|
});
|
|
460
441
|
}
|
|
461
442
|
|
|
443
|
+
// The shape draws at its dagre position — a note never moves it, so
|
|
444
|
+
// its edges stay connected. The note floats beside it.
|
|
445
|
+
const hasNote = !!node.note && !noNotes;
|
|
446
|
+
|
|
462
447
|
if (node.shape === 'pseudostate') {
|
|
463
448
|
nodeG
|
|
464
449
|
.append('circle')
|
|
@@ -562,6 +547,49 @@ export function renderState(
|
|
|
562
547
|
.attr('font-size', sNodeFontSize)
|
|
563
548
|
.text(node.label);
|
|
564
549
|
}
|
|
550
|
+
|
|
551
|
+
if (hasNote && node.note) {
|
|
552
|
+
if (node.note.collapsed) {
|
|
553
|
+
// Collapsed → comment-bubble badge in the node's top-right corner.
|
|
554
|
+
renderNoteBadge(
|
|
555
|
+
nodeG,
|
|
556
|
+
{
|
|
557
|
+
x: node.width / 2 - NOTE_BADGE_RADIUS - 3,
|
|
558
|
+
y: -node.height / 2 + NOTE_BADGE_RADIUS + 3,
|
|
559
|
+
},
|
|
560
|
+
palette,
|
|
561
|
+
{
|
|
562
|
+
isDark,
|
|
563
|
+
...(node.note.color && { color: node.note.color }),
|
|
564
|
+
lineNumber: node.note.lineNumber,
|
|
565
|
+
endLineNumber: node.note.endLineNumber,
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
} else {
|
|
569
|
+
// Solid tether from the shape edge to the floated note, on whichever
|
|
570
|
+
// side the collision-aware placement chose.
|
|
571
|
+
const [cx1, cy1, cx2, cy2] = noteConnectorPoints(node, node.note);
|
|
572
|
+
renderNoteConnector(nodeG, cx1, cy1, cx2, cy2, palette);
|
|
573
|
+
renderNoteBox(
|
|
574
|
+
nodeG,
|
|
575
|
+
{
|
|
576
|
+
x: node.note.x,
|
|
577
|
+
y: node.note.y,
|
|
578
|
+
width: node.note.width,
|
|
579
|
+
height: node.note.height,
|
|
580
|
+
},
|
|
581
|
+
node.note.lines,
|
|
582
|
+
palette,
|
|
583
|
+
{
|
|
584
|
+
isDark,
|
|
585
|
+
...(node.note.color && { color: node.note.color }),
|
|
586
|
+
lineNumber: node.note.lineNumber,
|
|
587
|
+
endLineNumber: node.note.endLineNumber,
|
|
588
|
+
interactive: true,
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
565
593
|
}
|
|
566
594
|
}
|
|
567
595
|
|
package/src/graph/types.ts
CHANGED
|
@@ -35,6 +35,18 @@ export interface GraphGroup {
|
|
|
35
35
|
readonly lineNumber: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* A generic note as authored — anchors to a node by `ref` (the
|
|
40
|
+
* author-typed id/label) and carries a multi-line `body`. Resolution
|
|
41
|
+
* to a concrete node happens at end-of-parse via `resolveNotes`; this
|
|
42
|
+
* model is intentionally a top-level list (ADR-1), not a field on
|
|
43
|
+
* `GraphNode`, so the placement pass sees the whole set at once.
|
|
44
|
+
*/
|
|
45
|
+
// The graph note is now the chart-neutral `DiagramNote`; kept as a named
|
|
46
|
+
// alias so existing `graph/` imports of `GraphNote` stay valid.
|
|
47
|
+
import type { DiagramNote } from '../utils/notes/model';
|
|
48
|
+
export type GraphNote = DiagramNote;
|
|
49
|
+
|
|
38
50
|
import type { DgmoError } from '../diagnostics';
|
|
39
51
|
|
|
40
52
|
export interface ParsedGraph {
|
|
@@ -45,6 +57,7 @@ export interface ParsedGraph {
|
|
|
45
57
|
readonly nodes: readonly GraphNode[];
|
|
46
58
|
readonly edges: readonly GraphEdge[];
|
|
47
59
|
readonly groups?: readonly GraphGroup[];
|
|
60
|
+
readonly notes?: readonly GraphNote[];
|
|
48
61
|
readonly options: Readonly<Record<string, string>>;
|
|
49
62
|
readonly diagnostics: readonly DgmoError[];
|
|
50
63
|
readonly error: string | null;
|
package/src/index.ts
CHANGED
|
@@ -68,7 +68,7 @@ export async function render(
|
|
|
68
68
|
text: string,
|
|
69
69
|
options?: RenderOptions
|
|
70
70
|
): Promise<RenderResult> {
|
|
71
|
-
const palette = options?.palette ?? palettes.
|
|
71
|
+
const palette = options?.palette ?? palettes.slate;
|
|
72
72
|
const onError = options?.onError ?? 'svg';
|
|
73
73
|
|
|
74
74
|
const result = await renderInternal(text, {
|
package/src/infra/layout.ts
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
import dagre from '@dagrejs/dagre';
|
|
9
9
|
import type { Writable } from '../utils/brand';
|
|
10
10
|
import type { ComputedInfraModel, ComputedInfraNode } from './types';
|
|
11
|
+
import {
|
|
12
|
+
measureText,
|
|
13
|
+
truncateText,
|
|
14
|
+
CHAR_WIDTH_RATIO,
|
|
15
|
+
} from '../utils/text-measure';
|
|
11
16
|
|
|
12
17
|
// ============================================================
|
|
13
18
|
// Layout types
|
|
@@ -86,8 +91,11 @@ const NODE_SEPARATOR_GAP = 4;
|
|
|
86
91
|
const NODE_PAD_BOTTOM = 10;
|
|
87
92
|
const ROLE_DOT_ROW = 12;
|
|
88
93
|
const COLLAPSE_BAR_HEIGHT = 6;
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
// Font sizes used when sizing nodes from text — must match renderer.ts
|
|
95
|
+
// (NODE_FONT_SIZE / META_FONT_SIZE) so measured widths agree.
|
|
96
|
+
const NODE_FONT_SIZE = 13;
|
|
97
|
+
const META_FONT_SIZE = 10;
|
|
98
|
+
const EDGE_LABEL_FONT_SIZE = 11;
|
|
91
99
|
const PADDING_X = 24;
|
|
92
100
|
const GROUP_PADDING = 20;
|
|
93
101
|
const GROUP_HEADER_HEIGHT = 24;
|
|
@@ -201,8 +209,15 @@ function computeNodeWidth(
|
|
|
201
209
|
node.computedConcurrentInvocations === 0 && node.computedInstances > 1
|
|
202
210
|
? node.computedInstances
|
|
203
211
|
: 0;
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
// Badge ("3x") renders at META_FONT_SIZE at the header's right edge; reserve
|
|
213
|
+
// its width plus a small gap alongside the bold label at NODE_FONT_SIZE.
|
|
214
|
+
const badgeWidth =
|
|
215
|
+
badgeVal > 0
|
|
216
|
+
? measureText(`${badgeVal}x`, META_FONT_SIZE) +
|
|
217
|
+
2 * CHAR_WIDTH_RATIO * NODE_FONT_SIZE
|
|
218
|
+
: 0;
|
|
219
|
+
const labelWidth =
|
|
220
|
+
measureText(node.label, NODE_FONT_SIZE) + badgeWidth + PADDING_X;
|
|
206
221
|
|
|
207
222
|
// Collect all key names (including "RPS" and computed rows) to compute aligned value column
|
|
208
223
|
const allKeys: string[] = [];
|
|
@@ -269,8 +284,12 @@ function computeNodeWidth(
|
|
|
269
284
|
}
|
|
270
285
|
if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH, labelWidth);
|
|
271
286
|
|
|
272
|
-
|
|
273
|
-
//
|
|
287
|
+
// Width of the aligned key column ("key: "), measured in pixels at the meta
|
|
288
|
+
// font size — mirrors renderer.ts's valueX = x + 10 + measureText("key: ").
|
|
289
|
+
const keyColWidth = Math.max(
|
|
290
|
+
...allKeys.map((k) => measureText(`${k}: `, META_FONT_SIZE))
|
|
291
|
+
);
|
|
292
|
+
// keyColWidth + measured value width
|
|
274
293
|
let maxRowWidth = 0;
|
|
275
294
|
if (node.computedRps > 0) {
|
|
276
295
|
// RPS row may show "29.3k / 50k" when an effective cap exists
|
|
@@ -296,7 +315,7 @@ function computeNodeWidth(
|
|
|
296
315
|
: formatRps(node.computedRps);
|
|
297
316
|
maxRowWidth = Math.max(
|
|
298
317
|
maxRowWidth,
|
|
299
|
-
|
|
318
|
+
keyColWidth + measureText(rpsVal, META_FONT_SIZE)
|
|
300
319
|
);
|
|
301
320
|
}
|
|
302
321
|
// Declared property value widths only when expanded
|
|
@@ -314,20 +333,20 @@ function computeNodeWidth(
|
|
|
314
333
|
'uptime',
|
|
315
334
|
'cb-error-threshold',
|
|
316
335
|
];
|
|
317
|
-
const
|
|
336
|
+
const valStr =
|
|
318
337
|
p.key === 'max-rps' || p.key === 'ratelimit-rps'
|
|
319
|
-
? formatRpsShort(numVal)
|
|
338
|
+
? formatRpsShort(numVal)
|
|
320
339
|
: p.key === 'latency-ms' ||
|
|
321
340
|
p.key === 'cb-latency-threshold-ms' ||
|
|
322
341
|
p.key === 'duration-ms' ||
|
|
323
342
|
p.key === 'cold-start-ms'
|
|
324
|
-
? formatMs(numVal)
|
|
343
|
+
? formatMs(numVal)
|
|
325
344
|
: PCT_KEYS.includes(p.key)
|
|
326
|
-
? `${numVal}
|
|
327
|
-
: String(p.value)
|
|
345
|
+
? `${numVal}%`
|
|
346
|
+
: String(p.value);
|
|
328
347
|
maxRowWidth = Math.max(
|
|
329
348
|
maxRowWidth,
|
|
330
|
-
|
|
349
|
+
keyColWidth + measureText(valStr, META_FONT_SIZE)
|
|
331
350
|
);
|
|
332
351
|
}
|
|
333
352
|
}
|
|
@@ -337,10 +356,9 @@ function computeNodeWidth(
|
|
|
337
356
|
const msValues = expanded ? [perc.p50, perc.p90, perc.p99] : [perc.p90];
|
|
338
357
|
for (const ms of msValues) {
|
|
339
358
|
if (ms > 0) {
|
|
340
|
-
const valLen = formatMs(ms).length;
|
|
341
359
|
maxRowWidth = Math.max(
|
|
342
360
|
maxRowWidth,
|
|
343
|
-
|
|
361
|
+
keyColWidth + measureText(formatMs(ms), META_FONT_SIZE)
|
|
344
362
|
);
|
|
345
363
|
}
|
|
346
364
|
}
|
|
@@ -358,43 +376,44 @@ function computeNodeWidth(
|
|
|
358
376
|
const combinedVal = `${formatMs(perc.p90)} / ${formatMs(threshold)}`;
|
|
359
377
|
maxRowWidth = Math.max(
|
|
360
378
|
maxRowWidth,
|
|
361
|
-
|
|
379
|
+
keyColWidth + measureText(combinedVal, META_FONT_SIZE)
|
|
362
380
|
);
|
|
363
381
|
}
|
|
364
382
|
}
|
|
365
383
|
if (node.computedUptime < 1) {
|
|
366
|
-
const valLen = formatUptime(node.computedUptime).length;
|
|
367
384
|
maxRowWidth = Math.max(
|
|
368
385
|
maxRowWidth,
|
|
369
|
-
|
|
386
|
+
keyColWidth +
|
|
387
|
+
measureText(formatUptime(node.computedUptime), META_FONT_SIZE)
|
|
370
388
|
);
|
|
371
389
|
}
|
|
372
390
|
if (node.computedAvailability < 1) {
|
|
373
|
-
const valLen = formatUptime(node.computedAvailability).length;
|
|
374
391
|
maxRowWidth = Math.max(
|
|
375
392
|
maxRowWidth,
|
|
376
|
-
|
|
393
|
+
keyColWidth +
|
|
394
|
+
measureText(formatUptime(node.computedAvailability), META_FONT_SIZE)
|
|
377
395
|
);
|
|
378
396
|
}
|
|
379
397
|
// CB state row ("CB: OPEN") — inverted pill, use full text width
|
|
380
398
|
if (node.computedCbState === 'open') {
|
|
381
399
|
maxRowWidth = Math.max(
|
|
382
400
|
maxRowWidth,
|
|
383
|
-
'CB: OPEN'
|
|
401
|
+
measureText('CB: OPEN', META_FONT_SIZE) + 8
|
|
384
402
|
);
|
|
385
403
|
}
|
|
386
404
|
}
|
|
387
405
|
|
|
388
|
-
|
|
406
|
+
// Pixel-width cap for description lines, derived from the legacy 120-char cap
|
|
407
|
+
// at META_FONT_SIZE — must match renderer.ts's DESC_MAX_WIDTH.
|
|
408
|
+
const DESC_MAX_WIDTH = 120 * CHAR_WIDTH_RATIO * META_FONT_SIZE;
|
|
389
409
|
const descLines =
|
|
390
410
|
expanded && node.description && !node.isEdge ? node.description : [];
|
|
391
411
|
let descWidth = 0;
|
|
392
412
|
for (const dl of descLines) {
|
|
393
|
-
const truncated =
|
|
394
|
-
dl.length > DESC_MAX_CHARS ? dl.slice(0, DESC_MAX_CHARS - 1) + '…' : dl;
|
|
413
|
+
const truncated = truncateText(dl, META_FONT_SIZE, DESC_MAX_WIDTH);
|
|
395
414
|
descWidth = Math.max(
|
|
396
415
|
descWidth,
|
|
397
|
-
truncated
|
|
416
|
+
measureText(truncated, META_FONT_SIZE) + PADDING_X
|
|
398
417
|
);
|
|
399
418
|
}
|
|
400
419
|
return Math.max(MIN_NODE_WIDTH, labelWidth, maxRowWidth + 20, descWidth);
|
|
@@ -828,7 +847,8 @@ export function layoutInfra(
|
|
|
828
847
|
const midIdx = Math.floor(edge.points.length / 2);
|
|
829
848
|
const midPt = edge.points[midIdx];
|
|
830
849
|
if (midPt) {
|
|
831
|
-
const halfWidth =
|
|
850
|
+
const halfWidth =
|
|
851
|
+
(measureText(edge.label, EDGE_LABEL_FONT_SIZE) + 8) / 2;
|
|
832
852
|
if (midPt.x - halfWidth < minX) minX = midPt.x - halfWidth;
|
|
833
853
|
if (midPt.x + halfWidth > maxX) maxX = midPt.x + halfWidth;
|
|
834
854
|
}
|
package/src/infra/parser.ts
CHANGED
package/src/infra/renderer.ts
CHANGED
|
@@ -11,6 +11,11 @@ import type { InfraTagGroup } from './types';
|
|
|
11
11
|
import { resolveColor } from '../colors';
|
|
12
12
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
13
13
|
import { preprocessDescriptionLine } from '../utils/description-helpers';
|
|
14
|
+
import {
|
|
15
|
+
measureText,
|
|
16
|
+
truncateText,
|
|
17
|
+
CHAR_WIDTH_RATIO,
|
|
18
|
+
} from '../utils/text-measure';
|
|
14
19
|
import type {
|
|
15
20
|
InfraLayoutResult,
|
|
16
21
|
InfraLayoutNode,
|
|
@@ -706,11 +711,12 @@ const PROP_DISPLAY: Record<string, string> = {
|
|
|
706
711
|
};
|
|
707
712
|
|
|
708
713
|
const DESC_MAX_CHARS = 120;
|
|
714
|
+
/** Pixel-width cap for description lines, derived from the legacy char cap at META_FONT_SIZE. */
|
|
715
|
+
const DESC_MAX_WIDTH = DESC_MAX_CHARS * CHAR_WIDTH_RATIO * META_FONT_SIZE;
|
|
709
716
|
|
|
710
|
-
/** Truncate description text to
|
|
717
|
+
/** Truncate description text to fit DESC_MAX_WIDTH at META_FONT_SIZE. */
|
|
711
718
|
function truncateDesc(text: string): string {
|
|
712
|
-
|
|
713
|
-
return text.slice(0, DESC_MAX_CHARS - 1) + '…';
|
|
719
|
+
return truncateText(text, META_FONT_SIZE, DESC_MAX_WIDTH);
|
|
714
720
|
}
|
|
715
721
|
|
|
716
722
|
/** Keys whose values are RPS counts and should be formatted like RPS. */
|
|
@@ -1338,7 +1344,7 @@ function renderEdgeLabels(
|
|
|
1338
1344
|
|
|
1339
1345
|
const g = svg.append('g').attr('class', animate ? 'infra-edge-label' : '');
|
|
1340
1346
|
|
|
1341
|
-
const textWidth = labelText
|
|
1347
|
+
const textWidth = measureText(labelText, EDGE_LABEL_FONT_SIZE) + 8;
|
|
1342
1348
|
g.append('rect')
|
|
1343
1349
|
.attr('x', midPt.x - textWidth / 2)
|
|
1344
1350
|
.attr('y', midPt.y - 8)
|
|
@@ -1687,9 +1693,12 @@ function renderNodes(
|
|
|
1687
1693
|
|
|
1688
1694
|
const rows = [...computedSection, ...declaredSection];
|
|
1689
1695
|
|
|
1690
|
-
// Compute max key width so values align vertically
|
|
1691
|
-
|
|
1692
|
-
const
|
|
1696
|
+
// Compute max key width (pixels) so values align vertically.
|
|
1697
|
+
// Keys are drawn as "${key}: ", so measure that exact string.
|
|
1698
|
+
const maxKeyWidth = Math.max(
|
|
1699
|
+
...rows.map((r) => measureText(`${r.key}: `, sc.sMetaFontSize))
|
|
1700
|
+
);
|
|
1701
|
+
const valueX = x + 10 + maxKeyWidth;
|
|
1693
1702
|
|
|
1694
1703
|
let rowY = sepY + NODE_SEPARATOR_GAP + sc.sMetaFontSize;
|
|
1695
1704
|
const needsSectionSep =
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PaletteColors } from '../palettes';
|
|
2
2
|
import { mix, shapeFill } from '../palettes/color-utils';
|
|
3
|
+
import { measureText, wrapTextToWidth } from '../utils/text-measure';
|
|
3
4
|
import type {
|
|
4
5
|
ParsedJourneyMap,
|
|
5
6
|
JourneyMapPhase,
|
|
@@ -61,8 +62,6 @@ const TITLE_HEIGHT = 36;
|
|
|
61
62
|
const PERSONA_HEIGHT = 48;
|
|
62
63
|
// Must match renderer.ts persona panel width
|
|
63
64
|
const PERSONA_PANEL_WIDTH = 280;
|
|
64
|
-
// Approx char width for FONT_SIZE_TITLE 18px bold (Inter)
|
|
65
|
-
const TITLE_HEADER_CHAR_WIDTH = 10;
|
|
66
65
|
const HEADER_GAP = 24;
|
|
67
66
|
const CURVE_AREA_HEIGHT = 260;
|
|
68
67
|
const CARD_GAP = 8;
|
|
@@ -89,6 +88,21 @@ export function scoreToColor(score: number, palette: PaletteColors): string {
|
|
|
89
88
|
return mix(palette.colors.red, palette.colors.green, t);
|
|
90
89
|
}
|
|
91
90
|
|
|
91
|
+
// Vertical headroom reserved at the top of the curve area (px). Keep faces
|
|
92
|
+
// off the very edge while still using the full height.
|
|
93
|
+
const CURVE_TOP_RESERVE = 20;
|
|
94
|
+
|
|
95
|
+
// Map an emotion score (1-5) to a y coordinate within the curve area. Shared
|
|
96
|
+
// by the curve points, the grid lines, and the left-edge score-label faces so
|
|
97
|
+
// all three stay on the same scale (else the labels bunch at the bottom).
|
|
98
|
+
export function scoreToCurveY(score: number, curveAreaBottom: number): number {
|
|
99
|
+
return (
|
|
100
|
+
curveAreaBottom -
|
|
101
|
+
((score - 1) / 4) * (CURVE_AREA_HEIGHT - CURVE_TOP_RESERVE) -
|
|
102
|
+
10
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
// ============================================================
|
|
93
107
|
// Layout Engine
|
|
94
108
|
// ============================================================
|
|
@@ -127,35 +141,41 @@ export function layoutJourneyMap(
|
|
|
127
141
|
: parsed.steps;
|
|
128
142
|
|
|
129
143
|
// Compute step card heights based on content (matches kanban card sizing).
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// layout reserves too little vertical space and rendered text overflows.
|
|
144
|
+
// Line counts route through the same `wrapTextToWidth` the renderer uses,
|
|
145
|
+
// at the same font sizes, so reserved height always matches rendered text.
|
|
133
146
|
const annoIconIndent = ANNO_ICON_SIZE + ANNO_ICON_GAP;
|
|
134
147
|
const annoTextW = STEP_CARD_WIDTH - CARD_PADDING_X * 2 - annoIconIndent;
|
|
135
148
|
const descTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
|
|
136
|
-
const FONT_SIZE_META = 10;
|
|
137
|
-
const
|
|
149
|
+
const FONT_SIZE_META = 10; // renderer FONT_SIZE_META (desc/anno)
|
|
150
|
+
const FONT_SIZE_STEP = 12; // renderer FONT_SIZE_STEP (title)
|
|
138
151
|
|
|
139
152
|
const titleTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
|
|
140
|
-
const titleCharWidth = 6.5; // matches renderer TITLE_CHAR_WIDTH (FONT_SIZE_STEP 12px)
|
|
141
153
|
const TITLE_LINE_HEIGHT = 16;
|
|
142
154
|
|
|
143
155
|
const stepHeights = allSteps.map((step) => {
|
|
144
|
-
const titleLines =
|
|
156
|
+
const titleLines = wrapTextToWidth(
|
|
145
157
|
step.title,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
158
|
+
FONT_SIZE_STEP,
|
|
159
|
+
titleTextWidth
|
|
160
|
+
).length;
|
|
149
161
|
let h = CARD_PADDING_Y + titleLines * TITLE_LINE_HEIGHT + CARD_PADDING_Y;
|
|
150
162
|
const cardAnnos = step.annotations;
|
|
151
163
|
let contentLines = 0;
|
|
152
164
|
// Description may wrap
|
|
153
165
|
if (step.description) {
|
|
154
|
-
contentLines +=
|
|
166
|
+
contentLines += wrapTextToWidth(
|
|
167
|
+
step.description,
|
|
168
|
+
FONT_SIZE_META,
|
|
169
|
+
descTextWidth
|
|
170
|
+
).length;
|
|
155
171
|
}
|
|
156
172
|
// Annotations: all lines indented past icon
|
|
157
173
|
for (const anno of cardAnnos) {
|
|
158
|
-
contentLines +=
|
|
174
|
+
contentLines += wrapTextToWidth(
|
|
175
|
+
anno.text,
|
|
176
|
+
FONT_SIZE_META,
|
|
177
|
+
annoTextW
|
|
178
|
+
).length;
|
|
159
179
|
}
|
|
160
180
|
if (contentLines > 0) {
|
|
161
181
|
h += contentLines * CARD_META_LINE_HEIGHT + 4; // 4px bottom padding
|
|
@@ -217,10 +237,7 @@ export function layoutJourneyMap(
|
|
|
217
237
|
// Curve point
|
|
218
238
|
if (step.score !== undefined) {
|
|
219
239
|
const curveX = stepX + STEP_CARD_WIDTH / 2;
|
|
220
|
-
const curveY =
|
|
221
|
-
curveAreaBottom -
|
|
222
|
-
((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 120) -
|
|
223
|
-
10;
|
|
240
|
+
const curveY = scoreToCurveY(step.score, curveAreaBottom);
|
|
224
241
|
curvePoints.push({
|
|
225
242
|
x: curveX,
|
|
226
243
|
y: curveY,
|
|
@@ -248,10 +265,7 @@ export function layoutJourneyMap(
|
|
|
248
265
|
stepCount === 1
|
|
249
266
|
? phaseX + phaseWidth / 2
|
|
250
267
|
: phaseX + padX + (si / (stepCount - 1)) * availW;
|
|
251
|
-
const curveY =
|
|
252
|
-
curveAreaBottom -
|
|
253
|
-
((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 120) -
|
|
254
|
-
10;
|
|
268
|
+
const curveY = scoreToCurveY(step.score, curveAreaBottom);
|
|
255
269
|
curvePoints.push({
|
|
256
270
|
x: curveX,
|
|
257
271
|
y: curveY,
|
|
@@ -328,10 +342,7 @@ export function layoutJourneyMap(
|
|
|
328
342
|
|
|
329
343
|
if (step.score !== undefined) {
|
|
330
344
|
const curveX = stepX + STEP_CARD_WIDTH / 2;
|
|
331
|
-
const curveY =
|
|
332
|
-
curveAreaBottom -
|
|
333
|
-
((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 20) -
|
|
334
|
-
10;
|
|
345
|
+
const curveY = scoreToCurveY(step.score, curveAreaBottom);
|
|
335
346
|
curvePoints.push({
|
|
336
347
|
x: curveX,
|
|
337
348
|
y: curveY,
|
|
@@ -373,7 +384,7 @@ export function layoutJourneyMap(
|
|
|
373
384
|
// this, a single-step journey produces a totalWidth that's narrower than
|
|
374
385
|
// the title + persona row, and the persona panel overlaps the title.
|
|
375
386
|
const headerTitleWidth = hasTitle
|
|
376
|
-
? parsed.title
|
|
387
|
+
? measureText(parsed.title!, 18) // FONT_SIZE_TITLE (18px bold)
|
|
377
388
|
: 0;
|
|
378
389
|
const personaPanelWidth = parsed.persona ? PERSONA_PANEL_WIDTH : 0;
|
|
379
390
|
const headerWidth =
|
|
@@ -402,25 +413,3 @@ export function layoutJourneyMap(
|
|
|
402
413
|
hasThoughts,
|
|
403
414
|
};
|
|
404
415
|
}
|
|
405
|
-
|
|
406
|
-
/** Count how many visual lines a text string will occupy when wrapped. */
|
|
407
|
-
function wrapLineCount(
|
|
408
|
-
text: string,
|
|
409
|
-
maxWidth: number,
|
|
410
|
-
charWidth: number
|
|
411
|
-
): number {
|
|
412
|
-
const maxChars = Math.max(1, Math.floor(maxWidth / charWidth));
|
|
413
|
-
const words = text.split(/\s+/);
|
|
414
|
-
let lines = 1;
|
|
415
|
-
let currentLen = 0;
|
|
416
|
-
for (const word of words) {
|
|
417
|
-
const needed = currentLen > 0 ? word.length + 1 : word.length;
|
|
418
|
-
if (currentLen + needed > maxChars && currentLen > 0) {
|
|
419
|
-
lines++;
|
|
420
|
-
currentLen = word.length;
|
|
421
|
-
} else {
|
|
422
|
-
currentLen += needed;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return lines;
|
|
426
|
-
}
|