@diagrammo/dgmo 0.26.0 → 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 +4182 -2704
- package/dist/advanced.d.cts +266 -58
- package/dist/advanced.d.ts +266 -58
- package/dist/advanced.js +4182 -2698
- package/dist/auto.cjs +4042 -2581
- package/dist/auto.js +124 -122
- package/dist/auto.mjs +4042 -2581
- 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 +4067 -2583
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +4067 -2583
- package/dist/internal.cjs +4182 -2704
- package/dist/internal.d.cts +266 -58
- package/dist/internal.d.ts +266 -58
- package/dist/internal.js +4182 -2698
- 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 +1 -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 -0
- package/src/class/renderer.ts +58 -2
- package/src/class/types.ts +3 -0
- package/src/cli.ts +4 -4
- package/src/completion.ts +26 -12
- 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 -0
- 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 +34 -1
- package/src/graph/flowchart-renderer.ts +78 -64
- 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 +78 -64
- package/src/graph/types.ts +13 -0
- package/src/index.ts +1 -1
- package/src/infra/layout.ts +46 -26
- 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/renderer.ts +30 -43
- package/src/pyramid/renderer.ts +4 -5
- package/src/raci/renderer.ts +34 -68
- 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,6 +3,13 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
5
|
import * as d3Selection from 'd3-selection';
|
|
6
|
+
import {
|
|
7
|
+
renderNoteBox,
|
|
8
|
+
renderNoteConnector,
|
|
9
|
+
renderNoteBadge,
|
|
10
|
+
noteConnectorPoints,
|
|
11
|
+
NOTE_BADGE_RADIUS,
|
|
12
|
+
} from '../utils/note-box';
|
|
6
13
|
import * as d3Shape from 'd3-shape';
|
|
7
14
|
import { FONT_FAMILY } from '../fonts';
|
|
8
15
|
import { renderLegendD3 } from '../utils/legend-d3';
|
|
@@ -19,7 +26,13 @@ import {
|
|
|
19
26
|
TITLE_FONT_WEIGHT,
|
|
20
27
|
TITLE_Y,
|
|
21
28
|
} from '../utils/title-constants';
|
|
22
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
contrastText,
|
|
31
|
+
mix,
|
|
32
|
+
relativeLuminance,
|
|
33
|
+
shapeFill,
|
|
34
|
+
valueRampColor,
|
|
35
|
+
} from '../palettes/color-utils';
|
|
23
36
|
import { resolveColor } from '../colors';
|
|
24
37
|
import { resolveTagColor } from '../utils/tag-groups';
|
|
25
38
|
import type { TagGroup } from '../utils/tag-groups';
|
|
@@ -32,6 +45,11 @@ import {
|
|
|
32
45
|
import type { ParsedBoxesAndLines, BLNode } from './types';
|
|
33
46
|
import type { BLLayoutResult, BLLayoutNode, BLLayoutEdge } from './layout';
|
|
34
47
|
import { ScaleContext } from '../utils/scaling';
|
|
48
|
+
import {
|
|
49
|
+
CHAR_WIDTH_RATIO,
|
|
50
|
+
measureText,
|
|
51
|
+
truncateText,
|
|
52
|
+
} from '../utils/text-measure';
|
|
35
53
|
|
|
36
54
|
// ── Constants (aligned with infra pattern) ─────────────────
|
|
37
55
|
const DIAGRAM_PADDING = 20;
|
|
@@ -49,7 +67,6 @@ const ARROWHEAD_H = 4;
|
|
|
49
67
|
const DESC_FONT_SIZE = 10; // matches infra META_FONT_SIZE
|
|
50
68
|
const DESC_LINE_HEIGHT = 1.4; // 14px row height at 10px font (matches infra META_LINE_HEIGHT)
|
|
51
69
|
const MAX_DESC_LINES = 6;
|
|
52
|
-
const CHAR_WIDTH_RATIO = 0.6;
|
|
53
70
|
const NODE_TEXT_PADDING = 12;
|
|
54
71
|
const GROUP_RX = 8;
|
|
55
72
|
const GROUP_LABEL_FONT_SIZE = 14;
|
|
@@ -127,16 +144,14 @@ function fitLabelToHeader(
|
|
|
127
144
|
fontSize >= MIN_NODE_FONT_SIZE;
|
|
128
145
|
fontSize--
|
|
129
146
|
) {
|
|
130
|
-
|
|
131
|
-
const maxChars = Math.floor(maxTextWidth / charWidth);
|
|
132
|
-
if (maxChars < 2) continue;
|
|
147
|
+
if (maxTextWidth < measureText('MM', fontSize)) continue;
|
|
133
148
|
|
|
134
|
-
// Wrap words into lines
|
|
149
|
+
// Wrap words into lines (greedy, by measured pixel width)
|
|
135
150
|
const lines: string[] = [];
|
|
136
151
|
let current = '';
|
|
137
152
|
for (const word of words) {
|
|
138
153
|
const test = current ? `${current} ${word}` : word;
|
|
139
|
-
if (test
|
|
154
|
+
if (measureText(test, fontSize) <= maxTextWidth) {
|
|
140
155
|
current = test;
|
|
141
156
|
} else {
|
|
142
157
|
if (current) lines.push(current);
|
|
@@ -145,15 +160,18 @@ function fitLabelToHeader(
|
|
|
145
160
|
}
|
|
146
161
|
if (current) lines.push(current);
|
|
147
162
|
|
|
163
|
+
const fits = (l: string): boolean =>
|
|
164
|
+
measureText(l, fontSize) <= maxTextWidth;
|
|
165
|
+
|
|
148
166
|
// All lines fit at this font? Done.
|
|
149
|
-
if (lines.length <= maxLines && lines.every(
|
|
167
|
+
if (lines.length <= maxLines && lines.every(fits)) {
|
|
150
168
|
return { lines, fontSize };
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
// Lines fit in count but some are too wide? Truncate those lines.
|
|
154
172
|
if (lines.length <= maxLines) {
|
|
155
173
|
const result = lines.map((l) =>
|
|
156
|
-
l
|
|
174
|
+
fits(l) ? l : truncateText(l, fontSize, maxTextWidth)
|
|
157
175
|
);
|
|
158
176
|
return { lines: result, fontSize };
|
|
159
177
|
}
|
|
@@ -161,25 +179,21 @@ function fitLabelToHeader(
|
|
|
161
179
|
// Too many lines — take first maxLines, truncate last + any oversized
|
|
162
180
|
const result = lines
|
|
163
181
|
.slice(0, maxLines)
|
|
164
|
-
.map((l) =>
|
|
165
|
-
l.length > maxChars ? l.slice(0, maxChars - 1) + '\u2026' : l
|
|
166
|
-
);
|
|
182
|
+
.map((l) => (fits(l) ? l : truncateText(l, fontSize, maxTextWidth)));
|
|
167
183
|
// In-bounds: result has exactly maxLines entries (from .slice(0, maxLines)).
|
|
168
184
|
const last = result[maxLines - 1]!;
|
|
169
185
|
if (!last.endsWith('\u2026')) {
|
|
170
|
-
result[maxLines - 1] =
|
|
171
|
-
last
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
result[maxLines - 1] = truncateText(
|
|
187
|
+
last + '\u2026',
|
|
188
|
+
fontSize,
|
|
189
|
+
maxTextWidth
|
|
190
|
+
);
|
|
174
191
|
}
|
|
175
192
|
return { lines: result, fontSize };
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
// Fallback at min font
|
|
179
|
-
const
|
|
180
|
-
const maxChars = Math.floor(maxTextWidth / charWidth);
|
|
181
|
-
const truncated =
|
|
182
|
-
label.length > maxChars ? label.slice(0, maxChars - 1) + '\u2026' : label;
|
|
196
|
+
const truncated = truncateText(label, MIN_NODE_FONT_SIZE, maxTextWidth);
|
|
183
197
|
return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
|
|
184
198
|
}
|
|
185
199
|
|
|
@@ -194,20 +208,40 @@ function nodeColors(
|
|
|
194
208
|
value: {
|
|
195
209
|
active: boolean;
|
|
196
210
|
hue: string;
|
|
211
|
+
/** Two explicit endpoint colours (`box-metric Risk green red`). When set, the
|
|
212
|
+
* value's position on the ramp is carried by HUE, so the box follows the
|
|
213
|
+
* STANDARD box convention (solid colour outline + 25% faded fill) rather than
|
|
214
|
+
* the map's saturated choropleth fill. A single-colour ramp encodes value by
|
|
215
|
+
* saturation/lightness and keeps the choropleth fill (no hue to spare). */
|
|
216
|
+
twoColor: boolean;
|
|
197
217
|
fillForValue: (v: number) => string;
|
|
198
218
|
},
|
|
199
219
|
solid?: boolean
|
|
200
220
|
): { fill: string; stroke: string; text: string } {
|
|
201
221
|
// Untagged-neutral fill, reused by the value path for no-value boxes.
|
|
202
222
|
const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
|
|
203
|
-
// Value dimension active: choropleth tint by the node's value, neutral when a
|
|
204
|
-
// box has no value (mirror map: `value !== undefined ? fillForValue : neutral`).
|
|
205
223
|
if (value.active) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
if (node.value === undefined) {
|
|
225
|
+
// No-value box: neutral fill, ramp-hue stroke (present so the app's
|
|
226
|
+
// --bl-node-stroke hover-dim still works).
|
|
227
|
+
const text = contrastText(
|
|
228
|
+
neutralFill,
|
|
229
|
+
palette.textOnFillLight,
|
|
230
|
+
palette.textOnFillDark
|
|
231
|
+
);
|
|
232
|
+
return { fill: neutralFill, stroke: value.hue, text };
|
|
233
|
+
}
|
|
234
|
+
// Value box: render the ramp colour like any tagged box — a 25% faded
|
|
235
|
+
// (muted) fill + a solid colour outline; `solid-fill` opts into the full
|
|
236
|
+
// fill. The outline differs by ramp kind: a two-colour ramp carries value by
|
|
237
|
+
// HUE, so each box's outline is its own ramp colour (red→green); a
|
|
238
|
+
// single-colour ramp has one hue, so the outline is the constant ramp hue and
|
|
239
|
+
// value reads from the muted fill depth.
|
|
240
|
+
const rampColor = value.fillForValue(node.value);
|
|
241
|
+
const fill = shapeFill(palette, rampColor, isDark, {
|
|
242
|
+
...(solid !== undefined && { solid }),
|
|
243
|
+
});
|
|
244
|
+
const stroke = value.twoColor ? rampColor : value.hue;
|
|
211
245
|
const text = contrastText(
|
|
212
246
|
fill,
|
|
213
247
|
palette.textOnFillLight,
|
|
@@ -306,6 +340,43 @@ function ensureArrowMarkers(
|
|
|
306
340
|
}
|
|
307
341
|
}
|
|
308
342
|
|
|
343
|
+
// ── Edge label placement ───────────────────────────────────
|
|
344
|
+
|
|
345
|
+
/** Point at the half-way arc length along an edge polyline — the geometric
|
|
346
|
+
* centre of the connector, so a label sits in the gap BETWEEN the two nodes
|
|
347
|
+
* (ELK's own label anchor drifts toward the target and ends up clipped under
|
|
348
|
+
* it). Falls back gracefully for degenerate point lists. */
|
|
349
|
+
function edgePolylineMidpoint(
|
|
350
|
+
points: ReadonlyArray<{ readonly x: number; readonly y: number }>
|
|
351
|
+
): { x: number; y: number } {
|
|
352
|
+
if (points.length === 0) return { x: 0, y: 0 };
|
|
353
|
+
if (points.length === 1) return { x: points[0]!.x, y: points[0]!.y };
|
|
354
|
+
let total = 0;
|
|
355
|
+
const segLen: number[] = [];
|
|
356
|
+
for (let k = 1; k < points.length; k++) {
|
|
357
|
+
const len = Math.hypot(
|
|
358
|
+
points[k]!.x - points[k - 1]!.x,
|
|
359
|
+
points[k]!.y - points[k - 1]!.y
|
|
360
|
+
);
|
|
361
|
+
segLen.push(len);
|
|
362
|
+
total += len;
|
|
363
|
+
}
|
|
364
|
+
let half = total / 2;
|
|
365
|
+
for (let k = 1; k < points.length; k++) {
|
|
366
|
+
const len = segLen[k - 1]!;
|
|
367
|
+
if (half <= len || k === points.length - 1) {
|
|
368
|
+
const t = len === 0 ? 0 : Math.min(1, half / len);
|
|
369
|
+
return {
|
|
370
|
+
x: points[k - 1]!.x + (points[k]!.x - points[k - 1]!.x) * t,
|
|
371
|
+
y: points[k - 1]!.y + (points[k]!.y - points[k - 1]!.y) * t,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
half -= len;
|
|
375
|
+
}
|
|
376
|
+
const last = points[points.length - 1]!;
|
|
377
|
+
return { x: last.x, y: last.y };
|
|
378
|
+
}
|
|
379
|
+
|
|
309
380
|
// ── Edge label overlap resolution ──────────────────────────
|
|
310
381
|
|
|
311
382
|
function resolveEdgeLabelOverlaps(
|
|
@@ -407,19 +478,32 @@ export function renderBoxesAndLines(
|
|
|
407
478
|
.filter((n) => n.value !== undefined)
|
|
408
479
|
.map((n) => n.value!);
|
|
409
480
|
const hasRamp = nodeValues.length > 0;
|
|
410
|
-
|
|
411
|
-
|
|
481
|
+
// Anchor the low end at the lowest value (not 0) to maximise within-diagram
|
|
482
|
+
// dynamic range; mirrors the map's region-metric ramp. Equal-value data
|
|
483
|
+
// (rampMin === rampMax) falls back to t = 1 in fillForValue below.
|
|
484
|
+
const rampMin = hasRamp ? Math.min(...nodeValues) : 0;
|
|
412
485
|
const rampMax = Math.max(...nodeValues);
|
|
413
486
|
// Default hue = palette.primary (NOT red like the map — boxes have no water to
|
|
414
487
|
// stand out against, and red reads as alarm on a neutral metric). A trailing
|
|
415
488
|
// color on `box-metric` overrides.
|
|
416
489
|
const rampHue =
|
|
417
490
|
resolveColor(parsed.boxMetricColor ?? '', palette) ?? palette.primary;
|
|
491
|
+
// Explicit LOW endpoint (`box-metric Risk green red`); absent ⇒ single-colour
|
|
492
|
+
// (neutral low). Only recognized names peel, so resolveColor always succeeds.
|
|
493
|
+
const rampLow = parsed.boxMetricLowColor
|
|
494
|
+
? (resolveColor(parsed.boxMetricLowColor, palette) ?? undefined)
|
|
495
|
+
: undefined;
|
|
418
496
|
// Lift the ramp anchor off the near-black surface on dark themes so the
|
|
419
497
|
// lowest values read as a clear muted tint rather than sinking to the surface.
|
|
420
498
|
const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
|
|
499
|
+
const rampLowFloor = mix(rampHue, rampBase, RAMP_FLOOR);
|
|
421
500
|
const fillForValue = (v: number): string => {
|
|
422
501
|
const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
|
|
502
|
+
// Two-colour ramp: shared low→high interpolation (direct or via midpoint).
|
|
503
|
+
if (rampLow !== undefined)
|
|
504
|
+
return valueRampColor(rampLow, rampHue, t, { isDark });
|
|
505
|
+
// Single/zero-colour: byte-identical to pre-change (same numeric pct, no
|
|
506
|
+
// float round-trip).
|
|
423
507
|
const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
|
|
424
508
|
return mix(rampHue, rampBase, pct);
|
|
425
509
|
};
|
|
@@ -464,8 +548,8 @@ export function renderBoxesAndLines(
|
|
|
464
548
|
gradient: {
|
|
465
549
|
min: rampMin,
|
|
466
550
|
max: rampMax,
|
|
467
|
-
|
|
468
|
-
|
|
551
|
+
low: rampLow ?? rampLowFloor,
|
|
552
|
+
high: rampHue,
|
|
469
553
|
},
|
|
470
554
|
}
|
|
471
555
|
: null;
|
|
@@ -829,22 +913,19 @@ export function renderBoxesAndLines(
|
|
|
829
913
|
path.attr('marker-start', `url(#${revId})`);
|
|
830
914
|
}
|
|
831
915
|
|
|
832
|
-
// Edge label —
|
|
833
|
-
//
|
|
834
|
-
|
|
835
|
-
|
|
916
|
+
// Edge label — centred on the connector's polyline midpoint (the gap
|
|
917
|
+
// between the two nodes), NOT ELK's target-biased anchor. For parallel
|
|
918
|
+
// edges, nudge above/below so each line's label clears the line.
|
|
919
|
+
if (le.label && le.points.length > 0) {
|
|
920
|
+
const lw = measureText(le.label, sEdgeLabelFontSize);
|
|
836
921
|
const labelH = sEdgeLabelFontSize + 6;
|
|
837
|
-
|
|
922
|
+
const mid = edgePolylineMidpoint(le.points);
|
|
923
|
+
let ly = mid.y;
|
|
838
924
|
if (le.parallelCount > 1 && le.yOffset !== 0) {
|
|
839
|
-
|
|
840
|
-
const lineY = le.labelY + 10 + le.yOffset; // +10 to undo the -10 in layout
|
|
841
|
-
const labelShift = le.yOffset < 0 ? -labelH : labelH;
|
|
842
|
-
ly = lineY + labelShift * 0.5;
|
|
843
|
-
} else {
|
|
844
|
-
ly = le.labelY + le.yOffset;
|
|
925
|
+
ly += (le.yOffset < 0 ? -labelH : labelH) * 0.5;
|
|
845
926
|
}
|
|
846
927
|
labelPositions.push({
|
|
847
|
-
x:
|
|
928
|
+
x: mid.x,
|
|
848
929
|
y: ly,
|
|
849
930
|
width: lw + 8,
|
|
850
931
|
height: labelH,
|
|
@@ -903,15 +984,24 @@ export function renderBoxesAndLines(
|
|
|
903
984
|
if (isHidden) continue;
|
|
904
985
|
}
|
|
905
986
|
|
|
987
|
+
const solid = parsed.options['solid-fill'] === 'on';
|
|
906
988
|
const colors = nodeColors(
|
|
907
989
|
node,
|
|
908
990
|
parsed.tagGroups,
|
|
909
991
|
activeGroup,
|
|
910
992
|
palette,
|
|
911
993
|
isDark,
|
|
912
|
-
{
|
|
913
|
-
|
|
994
|
+
{
|
|
995
|
+
active: activeIsValue,
|
|
996
|
+
hue: rampHue,
|
|
997
|
+
twoColor: rampLow !== undefined,
|
|
998
|
+
fillForValue,
|
|
999
|
+
},
|
|
1000
|
+
solid
|
|
914
1001
|
);
|
|
1002
|
+
// Divider matches the org-card convention: the box stroke normally, but the
|
|
1003
|
+
// contrast text colour in solid mode (where stroke == fill and would vanish).
|
|
1004
|
+
const dividerStroke = solid ? colors.text : colors.stroke;
|
|
915
1005
|
|
|
916
1006
|
const nodeG = diagramG
|
|
917
1007
|
.append('g')
|
|
@@ -980,13 +1070,14 @@ export function renderBoxesAndLines(
|
|
|
980
1070
|
.attr('text-anchor', 'middle')
|
|
981
1071
|
.attr('dominant-baseline', 'central')
|
|
982
1072
|
.attr('font-size', fitted.fontSize)
|
|
983
|
-
.attr('font-weight', '
|
|
1073
|
+
.attr('font-weight', 'bold')
|
|
984
1074
|
.attr('fill', colors.text)
|
|
985
1075
|
// In-bounds by loop guard.
|
|
986
1076
|
.text(labelLines[li]!);
|
|
987
1077
|
}
|
|
988
1078
|
|
|
989
|
-
//
|
|
1079
|
+
// Single divider under the title (org-card convention) — everything else
|
|
1080
|
+
// renders below it as one body section (no second divider / footer band).
|
|
990
1081
|
const sepY = -ln.height / 2 + headerH;
|
|
991
1082
|
nodeG
|
|
992
1083
|
.append('line')
|
|
@@ -994,12 +1085,15 @@ export function renderBoxesAndLines(
|
|
|
994
1085
|
.attr('y1', sepY)
|
|
995
1086
|
.attr('x2', ln.width / 2)
|
|
996
1087
|
.attr('y2', sepY)
|
|
997
|
-
.attr('stroke',
|
|
1088
|
+
.attr('stroke', dividerStroke)
|
|
998
1089
|
.attr('stroke-opacity', 0.3)
|
|
999
1090
|
.attr('stroke-width', 1);
|
|
1000
1091
|
|
|
1001
1092
|
const descStartY = sepY + 4 + sDescFontSize;
|
|
1002
1093
|
const maxTextWidth = ln.width - NODE_TEXT_PADDING * 2;
|
|
1094
|
+
// Char budget for the shared (char-based) bullet-aware wrapper. Derived
|
|
1095
|
+
// from the shared average glyph ratio so it stays in step with the
|
|
1096
|
+
// pixel measurer used everywhere else here.
|
|
1003
1097
|
const charsPerLine = Math.floor(
|
|
1004
1098
|
maxTextWidth / (sDescFontSize * CHAR_WIDTH_RATIO)
|
|
1005
1099
|
);
|
|
@@ -1046,6 +1140,16 @@ export function renderBoxesAndLines(
|
|
|
1046
1140
|
const BULLET_GLYPH_X = -ln.width / 2 + 6;
|
|
1047
1141
|
const BULLET_BODY_X = BULLET_GLYPH_X + 10;
|
|
1048
1142
|
|
|
1143
|
+
// Description must stay legible on ANY fill. On the default light/tinted
|
|
1144
|
+
// fills keep the subtle muted grey; on a dark/saturated fill (e.g.
|
|
1145
|
+
// solid-fill) the fixed grey sinks in — switch to a muted tint of the
|
|
1146
|
+
// box's contrast-correct text colour so it reads while staying
|
|
1147
|
+
// subordinate to the title.
|
|
1148
|
+
const descColor =
|
|
1149
|
+
relativeLuminance(colors.fill) > 0.5
|
|
1150
|
+
? palette.textMuted
|
|
1151
|
+
: mix(colors.text, colors.fill, 75);
|
|
1152
|
+
|
|
1049
1153
|
for (let li = 0; li < visibleLines.length; li++) {
|
|
1050
1154
|
// In-bounds by loop guard.
|
|
1051
1155
|
const line = visibleLines[li]!;
|
|
@@ -1053,8 +1157,8 @@ export function renderBoxesAndLines(
|
|
|
1053
1157
|
// Truncate last line if there are more lines beyond the cap
|
|
1054
1158
|
if (truncated && li === visibleLines.length - 1) {
|
|
1055
1159
|
lineText =
|
|
1056
|
-
lineText
|
|
1057
|
-
? lineText
|
|
1160
|
+
measureText(lineText, sDescFontSize) >= maxTextWidth
|
|
1161
|
+
? truncateText(lineText, sDescFontSize, maxTextWidth)
|
|
1058
1162
|
: lineText + '\u2026';
|
|
1059
1163
|
}
|
|
1060
1164
|
const y = descStartY + li * descLineH;
|
|
@@ -1066,7 +1170,7 @@ export function renderBoxesAndLines(
|
|
|
1066
1170
|
.attr('text-anchor', 'start')
|
|
1067
1171
|
.attr('dominant-baseline', 'central')
|
|
1068
1172
|
.attr('font-size', sDescFontSize)
|
|
1069
|
-
.attr('fill',
|
|
1173
|
+
.attr('fill', descColor)
|
|
1070
1174
|
.text('\u2022');
|
|
1071
1175
|
}
|
|
1072
1176
|
const isBullet =
|
|
@@ -1078,7 +1182,7 @@ export function renderBoxesAndLines(
|
|
|
1078
1182
|
.attr('text-anchor', isBullet ? 'start' : 'middle')
|
|
1079
1183
|
.attr('dominant-baseline', 'central')
|
|
1080
1184
|
.attr('font-size', DESC_FONT_SIZE)
|
|
1081
|
-
.attr('fill',
|
|
1185
|
+
.attr('fill', descColor);
|
|
1082
1186
|
renderInlineText(textEl, lineText, palette, sDescFontSize);
|
|
1083
1187
|
}
|
|
1084
1188
|
|
|
@@ -1089,6 +1193,25 @@ export function renderBoxesAndLines(
|
|
|
1089
1193
|
fullText.length > 200 ? fullText.slice(0, 199) + '\u2026' : fullText;
|
|
1090
1194
|
nodeG.append('title').text(tooltipText);
|
|
1091
1195
|
}
|
|
1196
|
+
|
|
1197
|
+
// Value sits in the SAME body section, directly after the description \u2014
|
|
1198
|
+
// no second divider / footer band (org-card: title, one line, body).
|
|
1199
|
+
if (parsed.showValues && node.value !== undefined) {
|
|
1200
|
+
const valueLabel = parsed.boxMetric
|
|
1201
|
+
? `${parsed.boxMetric}: ${node.value}`
|
|
1202
|
+
: String(node.value);
|
|
1203
|
+
nodeG
|
|
1204
|
+
.append('text')
|
|
1205
|
+
.attr('class', 'bl-node-value')
|
|
1206
|
+
.attr('x', 0)
|
|
1207
|
+
.attr('y', descStartY + visibleLines.length * descLineH)
|
|
1208
|
+
.attr('text-anchor', 'middle')
|
|
1209
|
+
.attr('dominant-baseline', 'central')
|
|
1210
|
+
.attr('font-size', VALUE_FONT_SIZE)
|
|
1211
|
+
.attr('font-weight', '600')
|
|
1212
|
+
.attr('fill', colors.text)
|
|
1213
|
+
.text(valueLabel);
|
|
1214
|
+
}
|
|
1092
1215
|
} else if (parsed.showValues && node.value !== undefined) {
|
|
1093
1216
|
// Plain node with show-values: label header + thin divider + a
|
|
1094
1217
|
// "Metric: value" line below (org/infra card style), instead of a
|
|
@@ -1116,20 +1239,20 @@ export function renderBoxesAndLines(
|
|
|
1116
1239
|
.attr('text-anchor', 'middle')
|
|
1117
1240
|
.attr('dominant-baseline', 'central')
|
|
1118
1241
|
.attr('font-size', fitted.fontSize)
|
|
1119
|
-
.attr('font-weight', '
|
|
1242
|
+
.attr('font-weight', 'bold')
|
|
1120
1243
|
.attr('fill', colors.text)
|
|
1121
1244
|
// In-bounds by loop guard.
|
|
1122
1245
|
.text(fitted.lines[li]!);
|
|
1123
1246
|
}
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1247
|
+
// Single divider under the title (org-card convention; solid-aware so it
|
|
1248
|
+
// stays visible when stroke == fill).
|
|
1126
1249
|
nodeG
|
|
1127
1250
|
.append('line')
|
|
1128
1251
|
.attr('x1', -ln.width / 2)
|
|
1129
1252
|
.attr('y1', sepY)
|
|
1130
1253
|
.attr('x2', ln.width / 2)
|
|
1131
1254
|
.attr('y2', sepY)
|
|
1132
|
-
.attr('stroke',
|
|
1255
|
+
.attr('stroke', dividerStroke)
|
|
1133
1256
|
.attr('stroke-opacity', 0.3)
|
|
1134
1257
|
.attr('stroke-width', 1);
|
|
1135
1258
|
// "Metric: value" centered in the space below the divider.
|
|
@@ -1167,46 +1290,50 @@ export function renderBoxesAndLines(
|
|
|
1167
1290
|
}
|
|
1168
1291
|
}
|
|
1169
1292
|
|
|
1170
|
-
// ──
|
|
1171
|
-
//
|
|
1172
|
-
//
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1293
|
+
// ── Note (floated beside the box, or a collapsed corner badge) ──
|
|
1294
|
+
// The box keeps its layout position; the note floats in adjacent space.
|
|
1295
|
+
// Coords are node-center-local (the node `<g>` is at the box center).
|
|
1296
|
+
if (ln.note) {
|
|
1297
|
+
if (ln.note.collapsed) {
|
|
1298
|
+
renderNoteBadge(
|
|
1299
|
+
nodeG,
|
|
1300
|
+
{
|
|
1301
|
+
x: ln.width / 2 - NOTE_BADGE_RADIUS - 3,
|
|
1302
|
+
y: -ln.height / 2 + NOTE_BADGE_RADIUS + 3,
|
|
1303
|
+
},
|
|
1304
|
+
palette,
|
|
1305
|
+
{
|
|
1306
|
+
isDark,
|
|
1307
|
+
...(ln.note.color && { color: ln.note.color }),
|
|
1308
|
+
lineNumber: ln.note.lineNumber,
|
|
1309
|
+
endLineNumber: ln.note.endLineNumber,
|
|
1310
|
+
}
|
|
1311
|
+
);
|
|
1312
|
+
} else {
|
|
1313
|
+
const [cx1, cy1, cx2, cy2] = noteConnectorPoints(
|
|
1314
|
+
{ width: ln.width, height: ln.height },
|
|
1315
|
+
ln.note
|
|
1316
|
+
);
|
|
1317
|
+
renderNoteConnector(nodeG, cx1, cy1, cx2, cy2, palette);
|
|
1318
|
+
renderNoteBox(
|
|
1319
|
+
nodeG,
|
|
1320
|
+
{
|
|
1321
|
+
x: ln.note.x,
|
|
1322
|
+
y: ln.note.y,
|
|
1323
|
+
width: ln.note.width,
|
|
1324
|
+
height: ln.note.height,
|
|
1325
|
+
},
|
|
1326
|
+
ln.note.lines,
|
|
1327
|
+
palette,
|
|
1328
|
+
{
|
|
1329
|
+
isDark,
|
|
1330
|
+
...(ln.note.color && { color: ln.note.color }),
|
|
1331
|
+
lineNumber: ln.note.lineNumber,
|
|
1332
|
+
endLineNumber: ln.note.endLineNumber,
|
|
1333
|
+
interactive: true,
|
|
1334
|
+
}
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1210
1337
|
}
|
|
1211
1338
|
}
|
|
1212
1339
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TagGroup } from '../utils/tag-groups';
|
|
2
2
|
import type { DgmoError } from '../diagnostics';
|
|
3
|
+
import type { DiagramNote } from '../utils/notes';
|
|
3
4
|
|
|
4
5
|
export interface BLNode {
|
|
5
6
|
readonly label: string;
|
|
@@ -37,12 +38,18 @@ export interface ParsedBoxesAndLines {
|
|
|
37
38
|
readonly groups: readonly BLGroup[];
|
|
38
39
|
readonly tagGroups: readonly TagGroup[];
|
|
39
40
|
readonly options: Readonly<Record<string, string>>;
|
|
41
|
+
/** Generic node notes (`note <Box> …`); resolved in layout. */
|
|
42
|
+
readonly notes?: readonly DiagramNote[];
|
|
40
43
|
readonly initialHiddenTagValues: ReadonlyMap<string, ReadonlySet<string>>;
|
|
41
44
|
readonly direction: 'LR' | 'TB';
|
|
42
|
-
/** `box-metric <label> [
|
|
43
|
-
* optionally sets its
|
|
45
|
+
/** `box-metric <label> [low] [high]` — names the value-ramp dimension and
|
|
46
|
+
* optionally sets its endpoint colours. One color = high hue over a neutral
|
|
47
|
+
* low; two = explicit `low high`. Mirror of map's `region-metric`. */
|
|
44
48
|
readonly boxMetric?: string;
|
|
49
|
+
/** Recognized color NAME for the ramp HIGH endpoint. */
|
|
45
50
|
readonly boxMetricColor?: string;
|
|
51
|
+
/** Recognized color NAME for the ramp LOW endpoint (two-colour form). */
|
|
52
|
+
readonly boxMetricLowColor?: string;
|
|
46
53
|
/** `show-values` — print each box's numeric value as text (opt-in). */
|
|
47
54
|
readonly showValues?: boolean;
|
|
48
55
|
readonly diagnostics: readonly DgmoError[];
|