@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
package/src/er/layout.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import dagre from '@dagrejs/dagre';
|
|
2
|
+
import { measureText } from '../utils/text-measure';
|
|
3
|
+
import {
|
|
4
|
+
resolveNotes,
|
|
5
|
+
buildPlacedNotes,
|
|
6
|
+
noteCanvasShift,
|
|
7
|
+
type PlacedNote,
|
|
8
|
+
} from '../utils/notes';
|
|
2
9
|
import type { ParsedERDiagram, ERTable, ERRelationship } from './types';
|
|
3
10
|
|
|
4
11
|
// ============================================================
|
|
@@ -12,6 +19,13 @@ export interface ERLayoutNode extends ERTable {
|
|
|
12
19
|
readonly height: number;
|
|
13
20
|
readonly headerHeight: number;
|
|
14
21
|
readonly columnsHeight: number;
|
|
22
|
+
/** A note floated beside this table (never moves the box). */
|
|
23
|
+
readonly note?: PlacedNote;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ERLayoutOptions {
|
|
27
|
+
/** 1-based source lines of notes the user collapsed (corner badge). */
|
|
28
|
+
collapsedNotes?: ReadonlySet<number>;
|
|
15
29
|
}
|
|
16
30
|
|
|
17
31
|
export interface ERLayoutEdge {
|
|
@@ -35,9 +49,15 @@ export interface ERLayoutResult {
|
|
|
35
49
|
// ============================================================
|
|
36
50
|
|
|
37
51
|
const MIN_WIDTH = 140;
|
|
38
|
-
const CHAR_WIDTH = 7.5;
|
|
39
52
|
const PADDING_X = 24;
|
|
40
53
|
const HEADER_BASE = 36;
|
|
54
|
+
// Font sizes mirror the renderer: table name at TABLE_FONT_SIZE, columns at
|
|
55
|
+
// COLUMN_FONT_SIZE. Keep these in sync with renderer.ts.
|
|
56
|
+
const TABLE_FONT_SIZE = 13;
|
|
57
|
+
const COLUMN_FONT_SIZE = 11;
|
|
58
|
+
const EDGE_LABEL_FONT_SIZE = 11;
|
|
59
|
+
// Renderer offsets column text by 16px when a constraint icon is present.
|
|
60
|
+
const CONSTRAINT_ICON_WIDTH = 16;
|
|
41
61
|
const MEMBER_LINE_HEIGHT = 18;
|
|
42
62
|
const COMPARTMENT_PADDING_Y = 8;
|
|
43
63
|
const SEPARATOR_HEIGHT = 1;
|
|
@@ -55,14 +75,18 @@ function computeNodeDimensions(table: ERTable): {
|
|
|
55
75
|
headerHeight: number;
|
|
56
76
|
columnsHeight: number;
|
|
57
77
|
} {
|
|
58
|
-
|
|
78
|
+
// Width: max rendered pixel width of table name and column rows. Measure
|
|
79
|
+
// each at the font size the renderer draws it with; reserve the constraint
|
|
80
|
+
// icon's horizontal offset when a column carries one.
|
|
81
|
+
let maxTextWidth = measureText(table.name, TABLE_FONT_SIZE);
|
|
59
82
|
for (const col of table.columns) {
|
|
60
83
|
let colText = col.name;
|
|
61
|
-
if (col.type) colText +=
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
if (col.type) colText += `: ${col.type}`;
|
|
85
|
+
let colWidth = measureText(colText, COLUMN_FONT_SIZE);
|
|
86
|
+
if (col.constraints.length > 0) colWidth += CONSTRAINT_ICON_WIDTH;
|
|
87
|
+
maxTextWidth = Math.max(maxTextWidth, colWidth);
|
|
64
88
|
}
|
|
65
|
-
const width = Math.max(MIN_WIDTH,
|
|
89
|
+
const width = Math.max(MIN_WIDTH, maxTextWidth + PADDING_X);
|
|
66
90
|
const headerHeight = HEADER_BASE;
|
|
67
91
|
|
|
68
92
|
let columnsHeight = 0;
|
|
@@ -227,7 +251,7 @@ function layoutComponent(
|
|
|
227
251
|
if (rel.label && (ed?.points ?? []).length > 0) {
|
|
228
252
|
const pts = ed!.points;
|
|
229
253
|
const mid = pts[Math.floor(pts.length / 2)];
|
|
230
|
-
const hw = (rel.label
|
|
254
|
+
const hw = (measureText(rel.label, EDGE_LABEL_FONT_SIZE) + 8) / 2;
|
|
231
255
|
minX = Math.min(minX, mid.x - hw);
|
|
232
256
|
maxX = Math.max(maxX, mid.x + hw);
|
|
233
257
|
}
|
|
@@ -333,7 +357,10 @@ function packComponents(
|
|
|
333
357
|
// Layout engine
|
|
334
358
|
// ============================================================
|
|
335
359
|
|
|
336
|
-
export function layoutERDiagram(
|
|
360
|
+
export function layoutERDiagram(
|
|
361
|
+
parsed: ParsedERDiagram,
|
|
362
|
+
options?: ERLayoutOptions
|
|
363
|
+
): ERLayoutResult {
|
|
337
364
|
if (parsed.tables.length === 0) {
|
|
338
365
|
return { nodes: [], edges: [], width: 0, height: 0 };
|
|
339
366
|
}
|
|
@@ -415,24 +442,89 @@ export function layoutERDiagram(parsed: ParsedERDiagram): ERLayoutResult {
|
|
|
415
442
|
};
|
|
416
443
|
});
|
|
417
444
|
|
|
418
|
-
// ── 6.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
445
|
+
// ── 6. Notes ─────────────────────────────────────────────────────────────────
|
|
446
|
+
// Resolve note anchors (no diagnostics — the parser already emitted them).
|
|
447
|
+
// `no-notes` drops the footprint so layout matches an un-annotated diagram.
|
|
448
|
+
// The note floats beside its table WITHOUT moving it (ER nodes are center
|
|
449
|
+
// -positioned → default side right).
|
|
450
|
+
const notesSuppressed = parsed.options?.['no-notes'] === 'on';
|
|
451
|
+
const noteByNode =
|
|
452
|
+
notesSuppressed || !parsed.notes
|
|
453
|
+
? new Map()
|
|
454
|
+
: resolveNotes(
|
|
455
|
+
parsed.notes,
|
|
456
|
+
parsed.tables.map((t) => ({ id: t.id, label: t.name }))
|
|
457
|
+
);
|
|
458
|
+
const placedNotes = buildPlacedNotes(
|
|
459
|
+
layoutNodes.map((n) => ({
|
|
460
|
+
id: n.id,
|
|
461
|
+
x: n.x,
|
|
462
|
+
y: n.y,
|
|
463
|
+
width: n.width,
|
|
464
|
+
height: n.height,
|
|
465
|
+
})),
|
|
466
|
+
noteByNode,
|
|
467
|
+
'TB',
|
|
468
|
+
options?.collapsedNotes
|
|
469
|
+
);
|
|
470
|
+
const notedNodes: ERLayoutNode[] = layoutNodes.map((n) => {
|
|
471
|
+
const note = placedNotes.get(n.id);
|
|
472
|
+
return note ? { ...n, note } : n;
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ── 7. Content bbox (incl. floated notes) + off-canvas shift ─────────────────
|
|
476
|
+
let bbMinX = Infinity;
|
|
477
|
+
let bbMinY = Infinity;
|
|
478
|
+
let bbMaxX = -Infinity;
|
|
479
|
+
let bbMaxY = -Infinity;
|
|
480
|
+
const extend = (l: number, t: number, r: number, b: number): void => {
|
|
481
|
+
if (l < bbMinX) bbMinX = l;
|
|
482
|
+
if (t < bbMinY) bbMinY = t;
|
|
483
|
+
if (r > bbMaxX) bbMaxX = r;
|
|
484
|
+
if (b > bbMaxY) bbMaxY = b;
|
|
485
|
+
};
|
|
486
|
+
for (const node of notedNodes) {
|
|
487
|
+
extend(
|
|
488
|
+
node.x - node.width / 2,
|
|
489
|
+
node.y - node.height / 2,
|
|
490
|
+
node.x + node.width / 2,
|
|
491
|
+
node.y + node.height / 2
|
|
492
|
+
);
|
|
493
|
+
if (node.note && !node.note.collapsed) {
|
|
494
|
+
extend(
|
|
495
|
+
node.x + node.note.x,
|
|
496
|
+
node.y + node.note.y,
|
|
497
|
+
node.x + node.note.x + node.note.width,
|
|
498
|
+
node.y + node.note.y + node.note.height
|
|
499
|
+
);
|
|
500
|
+
}
|
|
424
501
|
}
|
|
425
502
|
for (const edge of layoutEdges) {
|
|
426
|
-
for (const pt of edge.points)
|
|
427
|
-
maxX = Math.max(maxX, pt.x);
|
|
428
|
-
maxY = Math.max(maxY, pt.y);
|
|
429
|
-
}
|
|
503
|
+
for (const pt of edge.points) extend(pt.x, pt.y, pt.x, pt.y);
|
|
430
504
|
}
|
|
505
|
+
if (!Number.isFinite(bbMinX)) {
|
|
506
|
+
bbMinX = 0;
|
|
507
|
+
bbMinY = 0;
|
|
508
|
+
bbMaxX = 0;
|
|
509
|
+
bbMaxY = 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const { shiftX, shiftY } = noteCanvasShift(bbMinX, bbMinY);
|
|
513
|
+
const shifted = shiftX !== 0 || shiftY !== 0;
|
|
514
|
+
const finalNodes = shifted
|
|
515
|
+
? notedNodes.map((n) => ({ ...n, x: n.x + shiftX, y: n.y + shiftY }))
|
|
516
|
+
: notedNodes;
|
|
517
|
+
const finalEdges = shifted
|
|
518
|
+
? layoutEdges.map((e) => ({
|
|
519
|
+
...e,
|
|
520
|
+
points: e.points.map((pt) => ({ x: pt.x + shiftX, y: pt.y + shiftY })),
|
|
521
|
+
}))
|
|
522
|
+
: layoutEdges;
|
|
431
523
|
|
|
432
524
|
return {
|
|
433
|
-
nodes:
|
|
434
|
-
edges:
|
|
435
|
-
width:
|
|
436
|
-
height:
|
|
525
|
+
nodes: finalNodes,
|
|
526
|
+
edges: finalEdges,
|
|
527
|
+
width: bbMaxX + shiftX + HALF_MARGIN,
|
|
528
|
+
height: bbMaxY + shiftY + HALF_MARGIN,
|
|
437
529
|
};
|
|
438
530
|
}
|
package/src/er/parser.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
stripDefaultModifier,
|
|
32
32
|
} from '../utils/tag-groups';
|
|
33
33
|
import type { TagGroup } from '../utils/tag-groups';
|
|
34
|
+
import { tryCollectNote, resolveNotes, type DiagramNote } from '../utils/notes';
|
|
34
35
|
import type { Writable } from '../utils/brand';
|
|
35
36
|
import type {
|
|
36
37
|
ParsedERDiagram,
|
|
@@ -252,6 +253,7 @@ export function parseERDiagram(
|
|
|
252
253
|
// assign into it (table.metadata is `Readonly<Record<...>>` per the spec).
|
|
253
254
|
const tableMetadataMap = new Map<string, Record<string, string>>();
|
|
254
255
|
let currentTable: Writable<ERTable> | null = null;
|
|
256
|
+
const notes: DiagramNote[] = [];
|
|
255
257
|
let contentStarted = false;
|
|
256
258
|
let currentTagGroup: Writable<TagGroup> | null = null;
|
|
257
259
|
// metaAliasMap: tag-group metadata-key aliases (per A1 convention).
|
|
@@ -466,6 +468,21 @@ export function parseERDiagram(
|
|
|
466
468
|
currentTable = null;
|
|
467
469
|
contentStarted = true;
|
|
468
470
|
|
|
471
|
+
// Note annotation (top-level): `note <Table> [inline body]` + an optional
|
|
472
|
+
// indented body. `note -> X` is excluded so it can still parse as content.
|
|
473
|
+
const noteResult = tryCollectNote(
|
|
474
|
+
lines,
|
|
475
|
+
i,
|
|
476
|
+
indent,
|
|
477
|
+
palette,
|
|
478
|
+
result.diagnostics
|
|
479
|
+
);
|
|
480
|
+
if (noteResult) {
|
|
481
|
+
if (noteResult.note) notes.push(noteResult.note);
|
|
482
|
+
i = noteResult.lastIndex;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
469
486
|
// Reject top-level relationships — must be indented under source table
|
|
470
487
|
const rel = parseRelationship(trimmed, lineNumber, pushError);
|
|
471
488
|
if (rel) {
|
|
@@ -596,6 +613,17 @@ export function parseERDiagram(
|
|
|
596
613
|
}
|
|
597
614
|
}
|
|
598
615
|
|
|
616
|
+
// Resolve note refs against table names (forward refs OK). The id→note
|
|
617
|
+
// binding is recomputed in layout; this pass surfaces diagnostics.
|
|
618
|
+
if (notes.length > 0) {
|
|
619
|
+
result.notes = notes;
|
|
620
|
+
resolveNotes(
|
|
621
|
+
notes,
|
|
622
|
+
result.tables.map((t) => ({ id: t.id, label: t.name })),
|
|
623
|
+
result.diagnostics
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
599
627
|
// Warn about isolated tables (not in any relationship)
|
|
600
628
|
if (
|
|
601
629
|
result.tables.length >= 2 &&
|
package/src/er/renderer.ts
CHANGED
|
@@ -18,6 +18,14 @@ import {
|
|
|
18
18
|
TITLE_Y,
|
|
19
19
|
} from '../utils/title-constants';
|
|
20
20
|
import { ScaleContext } from '../utils/scaling';
|
|
21
|
+
import { measureText } from '../utils/text-measure';
|
|
22
|
+
import {
|
|
23
|
+
renderNoteBox,
|
|
24
|
+
renderNoteConnector,
|
|
25
|
+
renderNoteBadge,
|
|
26
|
+
noteConnectorPoints,
|
|
27
|
+
NOTE_BADGE_RADIUS,
|
|
28
|
+
} from '../utils/note-box';
|
|
21
29
|
import type { ParsedERDiagram, ERConstraint } from './types';
|
|
22
30
|
import type { ERLayoutResult } from './layout';
|
|
23
31
|
import { parseERDiagram } from './parser';
|
|
@@ -388,8 +396,7 @@ export function renderERDiagram(
|
|
|
388
396
|
const midIdx = Math.floor(pts.length / 2);
|
|
389
397
|
// In-bounds: midIdx is in [0, pts.length-1] since pts.length >= 2.
|
|
390
398
|
const midPt = pts[midIdx]!;
|
|
391
|
-
const
|
|
392
|
-
const bgW = labelLen * 7 + 8;
|
|
399
|
+
const bgW = measureText(edge.label, sEdgeLabelFontSize) + 8;
|
|
393
400
|
const bgH = 16;
|
|
394
401
|
|
|
395
402
|
edgeG
|
|
@@ -549,6 +556,52 @@ export function renderERDiagram(
|
|
|
549
556
|
memberY += sMemberLineHeight;
|
|
550
557
|
}
|
|
551
558
|
}
|
|
559
|
+
|
|
560
|
+
// ── Note (floated beside the table, or a collapsed corner badge) ──
|
|
561
|
+
// The table keeps its layout position; the note floats in adjacent
|
|
562
|
+
// space. Coords are node-center-local (the node `<g>` is at the center).
|
|
563
|
+
if (node.note) {
|
|
564
|
+
if (node.note.collapsed) {
|
|
565
|
+
renderNoteBadge(
|
|
566
|
+
nodeG,
|
|
567
|
+
{
|
|
568
|
+
x: w / 2 - NOTE_BADGE_RADIUS - 3,
|
|
569
|
+
y: -h / 2 + NOTE_BADGE_RADIUS + 3,
|
|
570
|
+
},
|
|
571
|
+
palette,
|
|
572
|
+
{
|
|
573
|
+
isDark,
|
|
574
|
+
...(node.note.color && { color: node.note.color }),
|
|
575
|
+
lineNumber: node.note.lineNumber,
|
|
576
|
+
endLineNumber: node.note.endLineNumber,
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
} else {
|
|
580
|
+
const [cx1, cy1, cx2, cy2] = noteConnectorPoints(
|
|
581
|
+
{ width: w, height: h },
|
|
582
|
+
node.note
|
|
583
|
+
);
|
|
584
|
+
renderNoteConnector(nodeG, cx1, cy1, cx2, cy2, palette);
|
|
585
|
+
renderNoteBox(
|
|
586
|
+
nodeG,
|
|
587
|
+
{
|
|
588
|
+
x: node.note.x,
|
|
589
|
+
y: node.note.y,
|
|
590
|
+
width: node.note.width,
|
|
591
|
+
height: node.note.height,
|
|
592
|
+
},
|
|
593
|
+
node.note.lines,
|
|
594
|
+
palette,
|
|
595
|
+
{
|
|
596
|
+
isDark,
|
|
597
|
+
...(node.note.color && { color: node.note.color }),
|
|
598
|
+
lineNumber: node.note.lineNumber,
|
|
599
|
+
endLineNumber: node.note.endLineNumber,
|
|
600
|
+
interactive: true,
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
552
605
|
}
|
|
553
606
|
|
|
554
607
|
// ── Tag Legend ──
|
package/src/er/types.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface ERRelationship {
|
|
|
35
35
|
|
|
36
36
|
import type { DgmoError } from '../diagnostics';
|
|
37
37
|
import type { TagGroup } from '../utils/tag-groups';
|
|
38
|
+
import type { DiagramNote } from '../utils/notes';
|
|
38
39
|
|
|
39
40
|
export interface ParsedERDiagram {
|
|
40
41
|
readonly type: 'er';
|
|
@@ -44,6 +45,8 @@ export interface ParsedERDiagram {
|
|
|
44
45
|
readonly tables: readonly ERTable[];
|
|
45
46
|
readonly relationships: readonly ERRelationship[];
|
|
46
47
|
readonly tagGroups: readonly TagGroup[];
|
|
48
|
+
/** Generic node notes (`note <Table> …`); resolved in layout. */
|
|
49
|
+
readonly notes?: readonly DiagramNote[];
|
|
47
50
|
readonly diagnostics: readonly DgmoError[];
|
|
48
51
|
readonly error: string | null;
|
|
49
52
|
}
|
package/src/gantt/renderer.ts
CHANGED
|
@@ -10,7 +10,12 @@ import { contrastText, mix, shapeFill } from '../palettes/color-utils';
|
|
|
10
10
|
import { normalizeName } from '../utils/name-normalize';
|
|
11
11
|
import { resolveTagColor, resolveActiveTagGroup } from '../utils/tag-groups';
|
|
12
12
|
import { ScaleContext } from '../utils/scaling';
|
|
13
|
-
import { computeTimeTicks } from '../utils/time-ticks';
|
|
13
|
+
import { computeTimeTicks, MONTH_ABBR } from '../utils/time-ticks';
|
|
14
|
+
import {
|
|
15
|
+
measureText,
|
|
16
|
+
truncateText,
|
|
17
|
+
CHAR_WIDTH_RATIO,
|
|
18
|
+
} from '../utils/text-measure';
|
|
14
19
|
import {
|
|
15
20
|
LEGEND_HEIGHT,
|
|
16
21
|
LEGEND_PILL_PAD,
|
|
@@ -60,7 +65,6 @@ const MILESTONE_SIZE = 10;
|
|
|
60
65
|
const MIN_LEFT_MARGIN = 120;
|
|
61
66
|
const BOTTOM_MARGIN = 40;
|
|
62
67
|
const RIGHT_MARGIN = 20;
|
|
63
|
-
const CHAR_W = 6.5; // estimated px per character for bar labels
|
|
64
68
|
const LABEL_PAD = 8; // inner padding to decide if label fits inside bar
|
|
65
69
|
const LABEL_GAP = 5; // gap between bar edge and external label
|
|
66
70
|
|
|
@@ -80,11 +84,11 @@ function computeBarLabel(
|
|
|
80
84
|
innerWidth: number,
|
|
81
85
|
textColor: string,
|
|
82
86
|
onFillColor?: string,
|
|
83
|
-
|
|
87
|
+
fontSize = 10,
|
|
84
88
|
labelPad = LABEL_PAD,
|
|
85
89
|
labelGap = LABEL_GAP
|
|
86
90
|
): BarLabelPlacement | null {
|
|
87
|
-
const textWidth = label
|
|
91
|
+
const textWidth = measureText(label, fontSize);
|
|
88
92
|
const x2 = x1 + barWidth;
|
|
89
93
|
|
|
90
94
|
if (textWidth < barWidth - labelPad) {
|
|
@@ -105,13 +109,15 @@ function computeBarLabel(
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
const availWidth = x1 - labelGap;
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
// Roughly three average glyphs' worth of room before truncation is worthwhile.
|
|
113
|
+
if (availWidth > fontSize * CHAR_WIDTH_RATIO * 3) {
|
|
114
|
+
const truncated = truncateText(label, fontSize, availWidth);
|
|
115
|
+
if (!truncated) return null;
|
|
110
116
|
return {
|
|
111
117
|
x: x1 - labelGap,
|
|
112
118
|
anchor: 'end',
|
|
113
119
|
fill: textColor,
|
|
114
|
-
text:
|
|
120
|
+
text: truncated,
|
|
115
121
|
};
|
|
116
122
|
}
|
|
117
123
|
|
|
@@ -270,25 +276,34 @@ export function renderGantt(
|
|
|
270
276
|
const rows = tagRows ?? buildRowList(resolved, collapsedGroups);
|
|
271
277
|
const isTagMode = tagRows !== null;
|
|
272
278
|
|
|
273
|
-
// Compute left margin based on longest visible label (include ● /◆ prefix
|
|
274
|
-
|
|
279
|
+
// Compute left margin based on longest visible label (include ● /◆ prefix
|
|
280
|
+
// for tasks). Labels render at font 11; group labels carry a pixel indent.
|
|
281
|
+
const LABEL_FONT = 11;
|
|
282
|
+
const labelWidths = isTagMode
|
|
275
283
|
? [
|
|
276
284
|
...rows
|
|
277
285
|
.filter((r): r is LaneHeaderRow => r.type === 'lane-header')
|
|
278
|
-
.map((r) => r.laneName),
|
|
286
|
+
.map((r) => measureText(r.laneName, LABEL_FONT)),
|
|
279
287
|
...rows
|
|
280
288
|
.filter((r): r is TaskRow => r.type === 'task')
|
|
281
|
-
.map((r) => '● ' + r.task.task.label),
|
|
289
|
+
.map((r) => measureText('● ' + r.task.task.label, LABEL_FONT)),
|
|
282
290
|
]
|
|
283
291
|
: [
|
|
284
|
-
...resolved.tasks.map((t) =>
|
|
292
|
+
...resolved.tasks.map((t) =>
|
|
293
|
+
measureText('● ' + t.task.label, LABEL_FONT)
|
|
294
|
+
),
|
|
285
295
|
...resolved.groups.map((g) => {
|
|
286
|
-
const
|
|
287
|
-
|
|
296
|
+
const indentPx =
|
|
297
|
+
g.depth <= 2 ? g.depth * 14 : 2 * 14 + (g.depth - 2) * 8;
|
|
298
|
+
return indentPx + measureText(g.name, LABEL_FONT);
|
|
288
299
|
}),
|
|
289
300
|
];
|
|
290
|
-
|
|
291
|
-
const
|
|
301
|
+
// Floor on a 10-char label so very short schedules still get a usable gutter.
|
|
302
|
+
const maxLabelWidth = Math.max(
|
|
303
|
+
...labelWidths,
|
|
304
|
+
measureText('0000000000', LABEL_FONT)
|
|
305
|
+
);
|
|
306
|
+
const leftMargin = Math.max(MIN_LEFT_MARGIN, maxLabelWidth + 30);
|
|
292
307
|
|
|
293
308
|
const totalRows = rows.length;
|
|
294
309
|
|
|
@@ -356,7 +371,6 @@ export function renderGantt(
|
|
|
356
371
|
const sMilestoneSize = ctx.structural(MILESTONE_SIZE);
|
|
357
372
|
const sBottomMargin = ctx.aesthetic(BOTTOM_MARGIN);
|
|
358
373
|
const sRightMargin = ctx.aesthetic(RIGHT_MARGIN);
|
|
359
|
-
const sCharW = ctx.text(CHAR_W, 4);
|
|
360
374
|
const sLabelPad = ctx.structural(LABEL_PAD);
|
|
361
375
|
const sLabelGap = ctx.structural(LABEL_GAP);
|
|
362
376
|
const sBandAccentW = ctx.structural(BAND_ACCENT_W);
|
|
@@ -923,7 +937,7 @@ export function renderGantt(
|
|
|
923
937
|
palette.textOnFillLight,
|
|
924
938
|
palette.textOnFillDark
|
|
925
939
|
),
|
|
926
|
-
|
|
940
|
+
sFont10,
|
|
927
941
|
sLabelPad,
|
|
928
942
|
sLabelGap
|
|
929
943
|
);
|
|
@@ -1007,7 +1021,7 @@ export function renderGantt(
|
|
|
1007
1021
|
palette.textOnFillLight,
|
|
1008
1022
|
palette.textOnFillDark
|
|
1009
1023
|
),
|
|
1010
|
-
|
|
1024
|
+
sFont10,
|
|
1011
1025
|
sLabelPad,
|
|
1012
1026
|
sLabelGap
|
|
1013
1027
|
);
|
|
@@ -1311,7 +1325,7 @@ export function renderGantt(
|
|
|
1311
1325
|
palette.textOnFillLight,
|
|
1312
1326
|
palette.textOnFillDark
|
|
1313
1327
|
),
|
|
1314
|
-
|
|
1328
|
+
sFont10,
|
|
1315
1329
|
sLabelPad,
|
|
1316
1330
|
sLabelGap
|
|
1317
1331
|
);
|
|
@@ -1625,7 +1639,7 @@ function drawHolidayBand(
|
|
|
1625
1639
|
// Hover label in SVG-space (date header row) — hidden by default
|
|
1626
1640
|
// Background rect to mask date labels underneath
|
|
1627
1641
|
const labelX = chartLeftMargin + x1 + bandW / 2;
|
|
1628
|
-
const textLen = label
|
|
1642
|
+
const textLen = measureText(label, 10) + 8;
|
|
1629
1643
|
const labelBg = svg
|
|
1630
1644
|
.append('rect')
|
|
1631
1645
|
.attr('class', 'gantt-holiday-hover-bg')
|
|
@@ -1957,6 +1971,15 @@ function drawSwimlaneIcon(
|
|
|
1957
1971
|
.attr('class', 'gantt-swimlane-icon')
|
|
1958
1972
|
.attr('transform', `translate(${x}, ${y})`);
|
|
1959
1973
|
|
|
1974
|
+
// Transparent hit area so the whole icon (not just the 2px bars) is clickable
|
|
1975
|
+
iconG
|
|
1976
|
+
.append('rect')
|
|
1977
|
+
.attr('x', -5)
|
|
1978
|
+
.attr('y', -5)
|
|
1979
|
+
.attr('width', 22)
|
|
1980
|
+
.attr('height', 18)
|
|
1981
|
+
.attr('fill', 'transparent');
|
|
1982
|
+
|
|
1960
1983
|
const color = isActive ? palette.primary : palette.textMuted;
|
|
1961
1984
|
const opacity = isActive ? 1 : 0.35;
|
|
1962
1985
|
const barWidths = [8, 12, 6];
|
|
@@ -2342,7 +2365,7 @@ function renderTagLegend(
|
|
|
2342
2365
|
|
|
2343
2366
|
// ── Era & Marker Rendering ──────────────────────────────────
|
|
2344
2367
|
|
|
2345
|
-
const ERA_COLORS = ['#
|
|
2368
|
+
const ERA_COLORS = ['#3b6ea5', '#5b9357', '#c9a227', '#cc7a33', '#7d5ba6'];
|
|
2346
2369
|
|
|
2347
2370
|
function renderErasAndMarkers(
|
|
2348
2371
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
@@ -2467,7 +2490,7 @@ function renderErasAndMarkers(
|
|
|
2467
2490
|
for (let i = 0; i < resolved.markers.length; i++) {
|
|
2468
2491
|
const marker = resolved.markers[i];
|
|
2469
2492
|
if (!marker) continue; // In-bounds by loop guard; appeases TS.
|
|
2470
|
-
const color = marker.color || palette.accent || '#
|
|
2493
|
+
const color = marker.color || palette.accent || '#3a9188';
|
|
2471
2494
|
// In-bounds: markerXs.length === resolved.markers.length.
|
|
2472
2495
|
const mx = markerXs[i]!;
|
|
2473
2496
|
const markerDate = parseDateStringToDate(marker.date);
|
|
@@ -3499,21 +3522,6 @@ function diamondPoints(cx: number, cy: number, size: number): string {
|
|
|
3499
3522
|
|
|
3500
3523
|
// ── Hover Date Indicators ───────────────────────────────────
|
|
3501
3524
|
|
|
3502
|
-
const MONTH_ABBR = [
|
|
3503
|
-
'Jan',
|
|
3504
|
-
'Feb',
|
|
3505
|
-
'Mar',
|
|
3506
|
-
'Apr',
|
|
3507
|
-
'May',
|
|
3508
|
-
'Jun',
|
|
3509
|
-
'Jul',
|
|
3510
|
-
'Aug',
|
|
3511
|
-
'Sep',
|
|
3512
|
-
'Oct',
|
|
3513
|
-
'Nov',
|
|
3514
|
-
'Dec',
|
|
3515
|
-
];
|
|
3516
|
-
|
|
3517
3525
|
function formatGanttDate(d: Date): string {
|
|
3518
3526
|
const base = `${MONTH_ABBR[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
|
|
3519
3527
|
if (d.getHours() === 0 && d.getMinutes() === 0) return base;
|
package/src/gantt/resolver.ts
CHANGED
|
@@ -55,6 +55,13 @@ export function collectTasks(nodes: readonly GanttNode[]): GanttTask[] {
|
|
|
55
55
|
/** Strip bracket syntax: `[Backend].API Design` → `Backend.API Design` */
|
|
56
56
|
const BRACKET_GROUP_RE = /^\[(.+?)\]\.(.+)$/;
|
|
57
57
|
|
|
58
|
+
/** Disambiguation message when a name matches more than one task. */
|
|
59
|
+
function ambiguousTaskMessage(trimmed: string, suggestions: string[]): string {
|
|
60
|
+
return `Multiple tasks match "${trimmed}". Did you mean ${suggestions
|
|
61
|
+
.map((s) => `\`${s}\``)
|
|
62
|
+
.join(' or ')}?`;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
export function resolveTaskName(
|
|
59
66
|
name: string,
|
|
60
67
|
allTasks: GanttTask[]
|
|
@@ -86,7 +93,7 @@ export function resolveTaskName(
|
|
|
86
93
|
);
|
|
87
94
|
return {
|
|
88
95
|
kind: 'ambiguous',
|
|
89
|
-
message:
|
|
96
|
+
message: ambiguousTaskMessage(trimmed, suggestions),
|
|
90
97
|
};
|
|
91
98
|
}
|
|
92
99
|
|
|
@@ -113,7 +120,7 @@ export function resolveTaskName(
|
|
|
113
120
|
);
|
|
114
121
|
return {
|
|
115
122
|
kind: 'ambiguous',
|
|
116
|
-
message:
|
|
123
|
+
message: ambiguousTaskMessage(trimmed, suggestions),
|
|
117
124
|
};
|
|
118
125
|
}
|
|
119
126
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Edge spline — basis curve clamped to its endpoints
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import * as d3Shape from 'd3-shape';
|
|
6
|
+
|
|
7
|
+
const lineGenerator = d3Shape
|
|
8
|
+
.line<{ x: number; y: number }>()
|
|
9
|
+
.x((d) => d.x)
|
|
10
|
+
.y((d) => d.y)
|
|
11
|
+
.curve(d3Shape.curveBasis);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a smooth edge path through dagre waypoints.
|
|
15
|
+
*
|
|
16
|
+
* `d3.curveBasis` already begins with `moveTo(P0)` and ends with
|
|
17
|
+
* `lineTo(Pn)`, so the path reaches both node borders and its final
|
|
18
|
+
* segment carries a real direction (correct `marker-end` orientation).
|
|
19
|
+
*
|
|
20
|
+
* NOTE: do NOT clamp by triplicating the endpoints — that appends
|
|
21
|
+
* zero-length trailing segments, and WebKit then computes a degenerate
|
|
22
|
+
* tangent for the arrowhead, rendering it at the wrong angle (resvg and
|
|
23
|
+
* Chromium tolerate it; WKWebView does not).
|
|
24
|
+
*/
|
|
25
|
+
export function edgeSplinePath(
|
|
26
|
+
points: ReadonlyArray<{ readonly x: number; readonly y: number }>
|
|
27
|
+
): string | null {
|
|
28
|
+
return lineGenerator(points as { x: number; y: number }[]);
|
|
29
|
+
}
|
|
@@ -17,7 +17,14 @@ import {
|
|
|
17
17
|
} from '../utils/parsing';
|
|
18
18
|
import { normalizeName, displayName } from '../utils/name-normalize';
|
|
19
19
|
import type { Writable } from '../utils/brand';
|
|
20
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
ParsedGraph,
|
|
22
|
+
GraphNode,
|
|
23
|
+
GraphEdge,
|
|
24
|
+
GraphShape,
|
|
25
|
+
GraphNote,
|
|
26
|
+
} from './types';
|
|
27
|
+
import { tryCollectNote, resolveNotes } from './notes';
|
|
21
28
|
|
|
22
29
|
// ============================================================
|
|
23
30
|
// Helpers
|
|
@@ -260,6 +267,7 @@ export function parseFlowchart(
|
|
|
260
267
|
|
|
261
268
|
const nodeMap = new Map<string, GraphNode>();
|
|
262
269
|
const indentStack: { nodeId: string; indent: number }[] = [];
|
|
270
|
+
const notes: GraphNote[] = [];
|
|
263
271
|
let contentStarted = false;
|
|
264
272
|
let firstLineParsed = false;
|
|
265
273
|
|
|
@@ -499,6 +507,23 @@ export function parseFlowchart(
|
|
|
499
507
|
}
|
|
500
508
|
}
|
|
501
509
|
|
|
510
|
+
// Note annotation: `note <ref> [inline body]` + optional indented
|
|
511
|
+
// body. Handled before options so `note foo bar` is never mistaken
|
|
512
|
+
// for an option. Only `note -> X` (arrow immediately after `note`) is
|
|
513
|
+
// excluded so it can edge; arrows are allowed inside a note body.
|
|
514
|
+
const noteResult = tryCollectNote(
|
|
515
|
+
lines,
|
|
516
|
+
i,
|
|
517
|
+
indent,
|
|
518
|
+
palette,
|
|
519
|
+
result.diagnostics
|
|
520
|
+
);
|
|
521
|
+
if (noteResult) {
|
|
522
|
+
if (noteResult.note) notes.push(noteResult.note);
|
|
523
|
+
i = noteResult.lastIndex;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
502
527
|
// Options (space-separated, before content)
|
|
503
528
|
if (!contentStarted) {
|
|
504
529
|
// Bare boolean: direction-lr
|
|
@@ -559,6 +584,14 @@ export function parseFlowchart(
|
|
|
559
584
|
result.error = formatDgmoError(diag);
|
|
560
585
|
}
|
|
561
586
|
|
|
587
|
+
// Resolve note refs (forward refs OK — runs after all nodes parsed).
|
|
588
|
+
// Emits diagnostics for unknown/ambiguous/duplicate refs; the resolved
|
|
589
|
+
// binding is recomputed in layout from `result.notes`.
|
|
590
|
+
if (notes.length > 0) {
|
|
591
|
+
result.notes = notes;
|
|
592
|
+
resolveNotes(notes, result.nodes, result.diagnostics);
|
|
593
|
+
}
|
|
594
|
+
|
|
562
595
|
// Warn about orphaned nodes (not referenced in any edge)
|
|
563
596
|
if (result.nodes.length >= 2 && result.edges.length >= 1 && !result.error) {
|
|
564
597
|
const connectedIds = new Set<string>();
|