@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
package/src/cli.ts
CHANGED
|
@@ -136,7 +136,7 @@ Key options:
|
|
|
136
136
|
- \`-o <file>\` — output file; format inferred from extension (\`.svg\` → SVG, else PNG)
|
|
137
137
|
- \`-o url\` — output a shareable diagrammo.app URL
|
|
138
138
|
- \`--theme <theme>\` — \`light\` (default), \`dark\`, \`transparent\`
|
|
139
|
-
- \`--palette <name>\` — \`
|
|
139
|
+
- \`--palette <name>\` — \`slate\` (default), \`atlas\`, \`blueprint\`, \`nord\`, \`tidewater\`, \`catppuccin\`, \`tokyo-night\`
|
|
140
140
|
- \`--copy\` — copy the URL to clipboard (use with \`-o url\`)
|
|
141
141
|
- \`--chart-types\` — list all supported chart types
|
|
142
142
|
|
|
@@ -309,7 +309,7 @@ end ❌ not needed — indentation closes blocks in sequence dia
|
|
|
309
309
|
|
|
310
310
|
## Tips
|
|
311
311
|
|
|
312
|
-
- Default theme: \`light\`, default palette: \`
|
|
312
|
+
- Default theme: \`light\`, default palette: \`slate\` — ask the user their preference before a final export.
|
|
313
313
|
- Stdin mode for quick renders: \`echo "..." | dgmo -o out.png\`
|
|
314
314
|
- For C4, \`--c4-level\` drills from context → containers → components → deployment.
|
|
315
315
|
- When auto-detection picks the wrong chart type, add an explicit \`chart:\` directive.
|
|
@@ -477,7 +477,7 @@ Options:
|
|
|
477
477
|
Use -o url to output a shareable diagrammo.app URL
|
|
478
478
|
With stdin and no -o, PNG is written to stdout
|
|
479
479
|
--theme <theme> Theme: ${THEMES.join(', ')} (default: light)
|
|
480
|
-
--palette <name> Palette: ${PALETTES.join(', ')} (default:
|
|
480
|
+
--palette <name> Palette: ${PALETTES.join(', ')} (default: slate)
|
|
481
481
|
--copy Copy URL to clipboard (only with -o url)
|
|
482
482
|
--json Output structured JSON to stdout
|
|
483
483
|
--chart-types List all supported chart types
|
|
@@ -528,7 +528,7 @@ function parseArgs(argv: string[]): {
|
|
|
528
528
|
input: undefined as string | undefined,
|
|
529
529
|
output: undefined as string | undefined,
|
|
530
530
|
theme: 'light' as (typeof THEMES)[number],
|
|
531
|
-
palette: '
|
|
531
|
+
palette: 'slate',
|
|
532
532
|
help: false,
|
|
533
533
|
version: false,
|
|
534
534
|
copy: false,
|
package/src/completion-types.ts
CHANGED
|
@@ -15,7 +15,6 @@ export type ChartType = string;
|
|
|
15
15
|
export interface DiagramSymbols {
|
|
16
16
|
kind: ChartType;
|
|
17
17
|
entities: string[]; // table names, node IDs, class names, etc.
|
|
18
|
-
keywords: string[]; // diagram-specific reserved words
|
|
19
18
|
/**
|
|
20
19
|
* Map of alias-literal → canonical entity name, collected from
|
|
21
20
|
* `Name as <alias>` declarations in the document. Editor surfaces
|
package/src/completion.ts
CHANGED
|
@@ -16,7 +16,11 @@ import { extractSymbols as extractFlowchartSymbols } from './graph/flowchart-par
|
|
|
16
16
|
import { extractSymbols as extractInfraSymbols } from './infra/parser';
|
|
17
17
|
import { extractSymbols as extractClassSymbols } from './class/parser';
|
|
18
18
|
import { extractPertSymbols } from './pert/parser';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
parseFirstLine,
|
|
21
|
+
ALL_CHART_TYPES,
|
|
22
|
+
measureIndent,
|
|
23
|
+
} from './utils/parsing';
|
|
20
24
|
import { RECOGNIZED_COLOR_NAMES } from './colors';
|
|
21
25
|
|
|
22
26
|
const RECOGNIZED_COLOR_SET: ReadonlySet<string> = new Set(
|
|
@@ -92,14 +96,8 @@ const GLOBAL_DIRECTIVES: Record<string, DirectiveValueSpec> = {
|
|
|
92
96
|
description: 'Color palette name',
|
|
93
97
|
values: [
|
|
94
98
|
'nord',
|
|
95
|
-
'solarized',
|
|
96
99
|
'catppuccin',
|
|
97
|
-
'rose-pine',
|
|
98
|
-
'gruvbox',
|
|
99
100
|
'tokyo-night',
|
|
100
|
-
'one-dark',
|
|
101
|
-
'dracula',
|
|
102
|
-
'monokai',
|
|
103
101
|
'atlas',
|
|
104
102
|
'blueprint',
|
|
105
103
|
'slate',
|
|
@@ -289,12 +287,13 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
289
287
|
],
|
|
290
288
|
[
|
|
291
289
|
'flowchart',
|
|
292
|
-
// Spec §5 §4.6: direction-lr, orientation-vertical, solid-fill
|
|
290
|
+
// Spec §5 §4.6: direction-lr, orientation-vertical, solid-fill, no-notes
|
|
293
291
|
withGlobals({
|
|
294
292
|
'direction-lr': { description: 'Switch to left-to-right layout' },
|
|
295
293
|
'orientation-vertical': {
|
|
296
294
|
description: 'Use vertical orientation for ranks',
|
|
297
295
|
},
|
|
296
|
+
'no-notes': { description: 'Suppress all node note boxes' },
|
|
298
297
|
}),
|
|
299
298
|
],
|
|
300
299
|
['class', withGlobals({})],
|
|
@@ -363,9 +362,10 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
363
362
|
],
|
|
364
363
|
[
|
|
365
364
|
'state',
|
|
366
|
-
// Spec §6 §5.
|
|
365
|
+
// Spec §6 §5.6: direction-tb, solid-fill, no-notes.
|
|
367
366
|
withGlobals({
|
|
368
367
|
'direction-tb': { description: 'Switch to top-to-bottom layout' },
|
|
368
|
+
'no-notes': { description: 'Suppress all state note boxes' },
|
|
369
369
|
}),
|
|
370
370
|
],
|
|
371
371
|
[
|
|
@@ -447,7 +447,10 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
447
447
|
direction: { description: 'Layout direction', values: ['LR', 'TB'] },
|
|
448
448
|
'active-tag': { description: 'Active tag group name' },
|
|
449
449
|
hide: { description: 'Hide tag:value pairs' },
|
|
450
|
-
'box-metric': {
|
|
450
|
+
'box-metric': {
|
|
451
|
+
description:
|
|
452
|
+
'Metric label for the value ramp, with an optional trailing [low] [high] color pair',
|
|
453
|
+
},
|
|
451
454
|
'show-values': { description: 'Print box values as text' },
|
|
452
455
|
}),
|
|
453
456
|
],
|
|
@@ -519,7 +522,10 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
519
522
|
// content keywords, not directives; metadata keys (value/label/style) live
|
|
520
523
|
// in the reserved-key registry.
|
|
521
524
|
withGlobals({
|
|
522
|
-
'region-metric': {
|
|
525
|
+
'region-metric': {
|
|
526
|
+
description:
|
|
527
|
+
'Label for the region value ramp, with an optional trailing [low] [high] color pair',
|
|
528
|
+
},
|
|
523
529
|
'poi-metric': {
|
|
524
530
|
description: 'Label for the POI value (marker size) channel',
|
|
525
531
|
},
|
|
@@ -548,11 +554,19 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
548
554
|
'no-region-labels': {
|
|
549
555
|
description: 'Turn off subdivision name labels (on by default)',
|
|
550
556
|
},
|
|
557
|
+
'no-region-value': {
|
|
558
|
+
description:
|
|
559
|
+
'Turn off the metric value shown under each region (on by default)',
|
|
560
|
+
},
|
|
551
561
|
'no-poi-labels': { description: 'Turn off POI labels (on by default)' },
|
|
552
562
|
'no-colorize': {
|
|
553
563
|
description:
|
|
554
564
|
'Force plain green-land reference dress (regions are auto-coloured by default)',
|
|
555
565
|
},
|
|
566
|
+
'no-cities': {
|
|
567
|
+
description:
|
|
568
|
+
'Turn off the subtle city dots scattered across the basemap (on by default)',
|
|
569
|
+
},
|
|
556
570
|
'no-cluster-pois': {
|
|
557
571
|
description:
|
|
558
572
|
'Always fan out coincident POI markers instead of collapsing them into a count badge',
|
|
@@ -648,6 +662,69 @@ export const ENTITY_TYPES = new Map<string, string[]>([
|
|
|
648
662
|
],
|
|
649
663
|
]);
|
|
650
664
|
|
|
665
|
+
// ============================================================
|
|
666
|
+
// Structural keywords for line-leading completion
|
|
667
|
+
// ============================================================
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Chart-type-specific structural keywords offered on an empty/start-of-line in
|
|
671
|
+
* the data zone (block openers like `loop`, section headers like `containers`,
|
|
672
|
+
* the `tag` block declaration, etc.). This is the single source of truth for
|
|
673
|
+
* the editor's structural-keyword popup — every entry MUST be a token the
|
|
674
|
+
* corresponding parser actually recognizes (validated by the
|
|
675
|
+
* completion-conformance suite). Do NOT add removed/diagnostic-only tokens
|
|
676
|
+
* (e.g. cycle's `no-descriptions`) or tokens the parser ignores.
|
|
677
|
+
*
|
|
678
|
+
* Chart types not listed here have no structural keywords (most data charts).
|
|
679
|
+
*/
|
|
680
|
+
export const STRUCTURAL_KEYWORDS = new Map<string, string[]>([
|
|
681
|
+
['sequence', ['if', 'else', 'loop', 'parallel', 'note', 'tag']],
|
|
682
|
+
['gantt', ['era', 'marker', 'holiday', 'workweek', 'parallel', 'tag']],
|
|
683
|
+
['c4', ['containers', 'components', 'deployment', 'tag']],
|
|
684
|
+
['timeline', ['era', 'marker', 'tag']],
|
|
685
|
+
['org', ['tag']],
|
|
686
|
+
['kanban', ['tag']],
|
|
687
|
+
['sitemap', ['tag']],
|
|
688
|
+
['infra', ['tag']],
|
|
689
|
+
['pert', ['tag']],
|
|
690
|
+
['mindmap', ['tag']],
|
|
691
|
+
['boxes-and-lines', ['tag']],
|
|
692
|
+
['er', ['tag']],
|
|
693
|
+
['cycle', ['direction-counterclockwise', 'circle-nodes']],
|
|
694
|
+
['journey-map', ['persona', 'tag']],
|
|
695
|
+
['raci', ['roles']],
|
|
696
|
+
['tech-radar', ['rings']],
|
|
697
|
+
[
|
|
698
|
+
'wireframe',
|
|
699
|
+
[
|
|
700
|
+
'nav',
|
|
701
|
+
'tabs',
|
|
702
|
+
'table',
|
|
703
|
+
'image',
|
|
704
|
+
'modal',
|
|
705
|
+
'skeleton',
|
|
706
|
+
'alert',
|
|
707
|
+
'progress',
|
|
708
|
+
'chart',
|
|
709
|
+
'mobile',
|
|
710
|
+
'tag',
|
|
711
|
+
],
|
|
712
|
+
],
|
|
713
|
+
['class', ['abstract', 'interface', 'enum', 'extends', 'implements']],
|
|
714
|
+
]);
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Chart types that support `tag` block declarations (and thus the
|
|
718
|
+
* `alias`/`default` sub-keywords inside a tag block). Derived from
|
|
719
|
+
* STRUCTURAL_KEYWORDS so the two can never drift — a chart supports tag blocks
|
|
720
|
+
* iff it offers the `tag` keyword.
|
|
721
|
+
*/
|
|
722
|
+
export const TAG_SUPPORTING_TYPES: ReadonlySet<string> = new Set(
|
|
723
|
+
[...STRUCTURAL_KEYWORDS]
|
|
724
|
+
.filter(([, kws]) => kws.includes('tag'))
|
|
725
|
+
.map(([type]) => type)
|
|
726
|
+
);
|
|
727
|
+
|
|
651
728
|
// ============================================================
|
|
652
729
|
// Pipe metadata for inline `| key value` on data lines
|
|
653
730
|
// ============================================================
|
|
@@ -992,7 +1069,6 @@ function extractSequenceSymbols(docText: string): DiagramSymbols {
|
|
|
992
1069
|
return {
|
|
993
1070
|
kind: 'sequence',
|
|
994
1071
|
entities,
|
|
995
|
-
keywords: ['if', 'else', 'loop', 'parallel', 'note'],
|
|
996
1072
|
};
|
|
997
1073
|
}
|
|
998
1074
|
|
|
@@ -1031,7 +1107,7 @@ function extractStateSymbols(docText: string): DiagramSymbols {
|
|
|
1031
1107
|
}
|
|
1032
1108
|
}
|
|
1033
1109
|
|
|
1034
|
-
return { kind: 'state', entities
|
|
1110
|
+
return { kind: 'state', entities };
|
|
1035
1111
|
}
|
|
1036
1112
|
|
|
1037
1113
|
// ============================================================
|
|
@@ -1247,7 +1323,7 @@ function extractSitemapSymbols(docText: string): DiagramSymbols {
|
|
|
1247
1323
|
}
|
|
1248
1324
|
}
|
|
1249
1325
|
|
|
1250
|
-
return { kind: 'sitemap', entities
|
|
1326
|
+
return { kind: 'sitemap', entities };
|
|
1251
1327
|
}
|
|
1252
1328
|
|
|
1253
1329
|
// ============================================================
|
|
@@ -1326,7 +1402,6 @@ function extractC4Symbols(docText: string): DiagramSymbols {
|
|
|
1326
1402
|
return {
|
|
1327
1403
|
kind: 'c4',
|
|
1328
1404
|
entities,
|
|
1329
|
-
keywords: ['containers', 'components', 'deployment'],
|
|
1330
1405
|
};
|
|
1331
1406
|
}
|
|
1332
1407
|
|
|
@@ -1429,7 +1504,7 @@ function extractGanttSymbols(docText: string): DiagramSymbols {
|
|
|
1429
1504
|
}
|
|
1430
1505
|
}
|
|
1431
1506
|
|
|
1432
|
-
return { kind: 'gantt', entities
|
|
1507
|
+
return { kind: 'gantt', entities };
|
|
1433
1508
|
}
|
|
1434
1509
|
|
|
1435
1510
|
// ============================================================
|
|
@@ -1487,7 +1562,7 @@ function extractBoxesAndLinesSymbols(docText: string): DiagramSymbols {
|
|
|
1487
1562
|
if (label && !entities.includes(label)) entities.push(label);
|
|
1488
1563
|
}
|
|
1489
1564
|
|
|
1490
|
-
return { kind: 'boxes-and-lines', entities
|
|
1565
|
+
return { kind: 'boxes-and-lines', entities };
|
|
1491
1566
|
}
|
|
1492
1567
|
|
|
1493
1568
|
// ============================================================
|
|
@@ -1540,7 +1615,7 @@ function extractOrgSymbols(docText: string): DiagramSymbols {
|
|
|
1540
1615
|
if (label && !entities.includes(label)) entities.push(label);
|
|
1541
1616
|
}
|
|
1542
1617
|
|
|
1543
|
-
return { kind: 'org', entities
|
|
1618
|
+
return { kind: 'org', entities };
|
|
1544
1619
|
}
|
|
1545
1620
|
|
|
1546
1621
|
// ============================================================
|
|
@@ -1592,7 +1667,7 @@ function extractKanbanSymbols(docText: string): DiagramSymbols {
|
|
|
1592
1667
|
}
|
|
1593
1668
|
}
|
|
1594
1669
|
|
|
1595
|
-
return { kind: 'kanban', entities
|
|
1670
|
+
return { kind: 'kanban', entities };
|
|
1596
1671
|
}
|
|
1597
1672
|
|
|
1598
1673
|
// ============================================================
|
|
@@ -1635,7 +1710,7 @@ function extractMindmapSymbols(docText: string): DiagramSymbols {
|
|
|
1635
1710
|
if (label && !entities.includes(label)) entities.push(label);
|
|
1636
1711
|
}
|
|
1637
1712
|
|
|
1638
|
-
return { kind: 'mindmap', entities
|
|
1713
|
+
return { kind: 'mindmap', entities };
|
|
1639
1714
|
}
|
|
1640
1715
|
|
|
1641
1716
|
// ============================================================
|
|
@@ -1668,7 +1743,7 @@ function extractPyramidSymbols(docText: string): DiagramSymbols {
|
|
|
1668
1743
|
if (label && !entities.includes(label)) entities.push(label);
|
|
1669
1744
|
}
|
|
1670
1745
|
|
|
1671
|
-
return { kind: 'pyramid', entities
|
|
1746
|
+
return { kind: 'pyramid', entities };
|
|
1672
1747
|
}
|
|
1673
1748
|
|
|
1674
1749
|
// ============================================================
|
|
@@ -1701,7 +1776,7 @@ function extractRingSymbols(docText: string): DiagramSymbols {
|
|
|
1701
1776
|
if (label && !entities.includes(label)) entities.push(label);
|
|
1702
1777
|
}
|
|
1703
1778
|
|
|
1704
|
-
return { kind: 'ring', entities
|
|
1779
|
+
return { kind: 'ring', entities };
|
|
1705
1780
|
}
|
|
1706
1781
|
|
|
1707
1782
|
// ============================================================
|
|
@@ -1736,7 +1811,7 @@ function extractArcSymbols(docText: string): DiagramSymbols {
|
|
|
1736
1811
|
}
|
|
1737
1812
|
}
|
|
1738
1813
|
|
|
1739
|
-
return { kind: 'arc', entities
|
|
1814
|
+
return { kind: 'arc', entities };
|
|
1740
1815
|
}
|
|
1741
1816
|
|
|
1742
1817
|
// ============================================================
|
|
@@ -1775,7 +1850,7 @@ function extractSankeySymbols(docText: string): DiagramSymbols {
|
|
|
1775
1850
|
}
|
|
1776
1851
|
}
|
|
1777
1852
|
|
|
1778
|
-
return { kind: 'sankey', entities
|
|
1853
|
+
return { kind: 'sankey', entities };
|
|
1779
1854
|
}
|
|
1780
1855
|
|
|
1781
1856
|
// ============================================================
|
|
@@ -1835,7 +1910,7 @@ function extractTimelineSymbols(docText: string): DiagramSymbols {
|
|
|
1835
1910
|
if (label && !entities.includes(label)) entities.push(label);
|
|
1836
1911
|
}
|
|
1837
1912
|
|
|
1838
|
-
return { kind: 'timeline', entities
|
|
1913
|
+
return { kind: 'timeline', entities };
|
|
1839
1914
|
}
|
|
1840
1915
|
|
|
1841
1916
|
// ============================================================
|
|
@@ -1867,7 +1942,7 @@ function extractVennSymbols(docText: string): DiagramSymbols {
|
|
|
1867
1942
|
if (label && !entities.includes(label)) entities.push(label);
|
|
1868
1943
|
}
|
|
1869
1944
|
|
|
1870
|
-
return { kind: 'venn', entities
|
|
1945
|
+
return { kind: 'venn', entities };
|
|
1871
1946
|
}
|
|
1872
1947
|
|
|
1873
1948
|
// ============================================================
|
|
@@ -1901,7 +1976,7 @@ function extractQuadrantSymbols(docText: string): DiagramSymbols {
|
|
|
1901
1976
|
if (label && !entities.includes(label)) entities.push(label);
|
|
1902
1977
|
}
|
|
1903
1978
|
|
|
1904
|
-
return { kind: 'quadrant', entities
|
|
1979
|
+
return { kind: 'quadrant', entities };
|
|
1905
1980
|
}
|
|
1906
1981
|
|
|
1907
1982
|
// ============================================================
|
|
@@ -1936,7 +2011,7 @@ function extractSlopeSymbols(docText: string): DiagramSymbols {
|
|
|
1936
2011
|
if (label && !entities.includes(label)) entities.push(label);
|
|
1937
2012
|
}
|
|
1938
2013
|
|
|
1939
|
-
return { kind: 'slope', entities
|
|
2014
|
+
return { kind: 'slope', entities };
|
|
1940
2015
|
}
|
|
1941
2016
|
|
|
1942
2017
|
// ============================================================
|
|
@@ -1983,7 +2058,7 @@ function extractDataChartSymbols(docText: string): DiagramSymbols {
|
|
|
1983
2058
|
}
|
|
1984
2059
|
}
|
|
1985
2060
|
|
|
1986
|
-
return { kind: chartType, entities
|
|
2061
|
+
return { kind: chartType, entities };
|
|
1987
2062
|
}
|
|
1988
2063
|
|
|
1989
2064
|
// ============================================================
|
|
@@ -2034,23 +2109,6 @@ registerExtractor('chord', extractDataChartSymbols);
|
|
|
2034
2109
|
|
|
2035
2110
|
function extractTechRadarSymbols(docText: string): DiagramSymbols {
|
|
2036
2111
|
const entities: string[] = [];
|
|
2037
|
-
const keywords: string[] = [
|
|
2038
|
-
'rings',
|
|
2039
|
-
'quadrant',
|
|
2040
|
-
'ring',
|
|
2041
|
-
'trend',
|
|
2042
|
-
'new',
|
|
2043
|
-
'up',
|
|
2044
|
-
'down',
|
|
2045
|
-
'stable',
|
|
2046
|
-
'top-left',
|
|
2047
|
-
'top-right',
|
|
2048
|
-
'bottom-left',
|
|
2049
|
-
'bottom-right',
|
|
2050
|
-
'alias',
|
|
2051
|
-
'aka',
|
|
2052
|
-
'color',
|
|
2053
|
-
];
|
|
2054
2112
|
|
|
2055
2113
|
// Extract ring names and aliases from the rings block
|
|
2056
2114
|
const lines = docText.split('\n');
|
|
@@ -2078,7 +2136,7 @@ function extractTechRadarSymbols(docText: string): DiagramSymbols {
|
|
|
2078
2136
|
}
|
|
2079
2137
|
}
|
|
2080
2138
|
|
|
2081
|
-
return { kind: 'tech-radar', entities
|
|
2139
|
+
return { kind: 'tech-radar', entities };
|
|
2082
2140
|
}
|
|
2083
2141
|
|
|
2084
2142
|
// ============================================================
|
|
@@ -2121,7 +2179,6 @@ function extractCycleSymbols(docText: string): DiagramSymbols {
|
|
|
2121
2179
|
return {
|
|
2122
2180
|
kind: 'cycle',
|
|
2123
2181
|
entities,
|
|
2124
|
-
keywords: ['direction-counterclockwise', 'circle-nodes'],
|
|
2125
2182
|
};
|
|
2126
2183
|
}
|
|
2127
2184
|
|
|
@@ -2169,7 +2226,7 @@ function extractRaciSymbols(docText: string): DiagramSymbols {
|
|
|
2169
2226
|
continue;
|
|
2170
2227
|
}
|
|
2171
2228
|
|
|
2172
|
-
const indent = line
|
|
2229
|
+
const indent = measureIndent(line);
|
|
2173
2230
|
|
|
2174
2231
|
// Header directives
|
|
2175
2232
|
if (indent === 0) {
|
|
@@ -2218,7 +2275,6 @@ function extractRaciSymbols(docText: string): DiagramSymbols {
|
|
|
2218
2275
|
return {
|
|
2219
2276
|
kind: chartType,
|
|
2220
2277
|
entities,
|
|
2221
|
-
keywords: ['variant', 'roles'],
|
|
2222
2278
|
};
|
|
2223
2279
|
}
|
|
2224
2280
|
|
|
@@ -2271,6 +2327,5 @@ function extractJourneyMapSymbols(docText: string): DiagramSymbols {
|
|
|
2271
2327
|
return {
|
|
2272
2328
|
kind: 'journey-map',
|
|
2273
2329
|
entities,
|
|
2274
|
-
keywords: ['persona', 'pain', 'opportunity', 'thought', 'description'],
|
|
2275
2330
|
};
|
|
2276
2331
|
}
|
package/src/cycle/layout.ts
CHANGED
|
@@ -16,23 +16,18 @@ import {
|
|
|
16
16
|
wrapDescriptionLines,
|
|
17
17
|
type WrappedDescLine,
|
|
18
18
|
} from '../utils/wrapped-desc';
|
|
19
|
+
import { measureText, wrapTextToWidth } from '../utils/text-measure';
|
|
19
20
|
|
|
20
21
|
/** Minimum arc angle in radians (~15°) to keep arcs readable. */
|
|
21
22
|
const MIN_ARC_ANGLE = (15 * Math.PI) / 180;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* Estimated character width at 11px description font (Inter).
|
|
31
|
-
* Average glyph width is ~5.5–6.0 px for typical English text — using 6.0
|
|
32
|
-
* gives us a small margin of safety for wide glyphs (m, w) without leaving
|
|
33
|
-
* obvious dead space on the right side of the rectangle.
|
|
34
|
-
*/
|
|
35
|
-
const DESC_CHAR_W = 6.0;
|
|
24
|
+
// ── Font sizes (must match cycle/renderer.ts) ──
|
|
25
|
+
// Layout sizes nodes/labels from text; the renderer draws them. Both measure
|
|
26
|
+
// the same string at the same font size so reserved space matches ink.
|
|
27
|
+
const LABEL_FONT_SIZE = 13;
|
|
28
|
+
const CIRCLE_LABEL_FONT_SIZE = 16;
|
|
29
|
+
const DESC_FONT_SIZE = 11;
|
|
30
|
+
const EDGE_LABEL_FONT_SIZE = 11;
|
|
36
31
|
|
|
37
32
|
/** Minimum node width. */
|
|
38
33
|
const MIN_NODE_WIDTH = 70;
|
|
@@ -105,7 +100,7 @@ export function computeCycleLayout(
|
|
|
105
100
|
const hasDesc = !hideDescriptions && node.description.length > 0;
|
|
106
101
|
const labelWidth = Math.max(
|
|
107
102
|
MIN_NODE_WIDTH,
|
|
108
|
-
node.label
|
|
103
|
+
measureText(node.label, LABEL_FONT_SIZE) + NODE_PAD_X * 2
|
|
109
104
|
);
|
|
110
105
|
|
|
111
106
|
if (circleNodes) {
|
|
@@ -422,9 +417,13 @@ function wrapDescForWidth(
|
|
|
422
417
|
description: readonly string[],
|
|
423
418
|
nodeWidth: number
|
|
424
419
|
): WrappedDescLine[] {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
420
|
+
// Wrap to a real pixel budget. `wrapDescriptionLines` is bullet-aware (it
|
|
421
|
+
// emits bullet-first/cont kinds the renderer relies on), so feed it a pixel
|
|
422
|
+
// `lengthFn` + pixel limit instead of a character count.
|
|
423
|
+
const textWidth = Math.max(40, nodeWidth - NODE_PAD_X * 2);
|
|
424
|
+
return wrapDescriptionLines([...description], textWidth, (s) =>
|
|
425
|
+
measureText(s, DESC_FONT_SIZE)
|
|
426
|
+
);
|
|
428
427
|
}
|
|
429
428
|
|
|
430
429
|
// ── Renderer-aligned font/line-height clamps ──
|
|
@@ -463,11 +462,12 @@ function renderedDescNodeHeight(numLines: number, scale: number): number {
|
|
|
463
462
|
// ── Edge-label wrapping (shared with renderer) ──
|
|
464
463
|
|
|
465
464
|
/**
|
|
466
|
-
* Maximum
|
|
465
|
+
* Maximum pixel width per line for edge labels and edge descriptions.
|
|
467
466
|
* Long single-line text gets wrapped to multiple lines so it doesn't
|
|
468
|
-
* shoot off-canvas when positioned at a cycle quadrant.
|
|
467
|
+
* shoot off-canvas when positioned at a cycle quadrant. ~197px ≈ the old
|
|
468
|
+
* 32-char budget at the 11px edge-label font.
|
|
469
469
|
*/
|
|
470
|
-
export const
|
|
470
|
+
export const EDGE_LABEL_MAX_WIDTH = 197;
|
|
471
471
|
|
|
472
472
|
/**
|
|
473
473
|
* Wrap an edge label string + description lines into rendered lines.
|
|
@@ -478,37 +478,18 @@ export const EDGE_LABEL_MAX_CHARS = 32;
|
|
|
478
478
|
export function wrapEdgeLabelText(
|
|
479
479
|
label: string | undefined,
|
|
480
480
|
description: readonly string[],
|
|
481
|
-
|
|
481
|
+
maxWidth: number = EDGE_LABEL_MAX_WIDTH
|
|
482
482
|
): { labelLines: string[]; descLines: string[] } {
|
|
483
|
-
const labelLines = label
|
|
483
|
+
const labelLines = label
|
|
484
|
+
? wrapTextToWidth(label, EDGE_LABEL_FONT_SIZE, maxWidth)
|
|
485
|
+
: [];
|
|
484
486
|
const descLines: string[] = [];
|
|
485
487
|
for (const d of description) {
|
|
486
|
-
descLines.push(...
|
|
488
|
+
descLines.push(...wrapTextToWidth(d, EDGE_LABEL_FONT_SIZE, maxWidth));
|
|
487
489
|
}
|
|
488
490
|
return { labelLines, descLines };
|
|
489
491
|
}
|
|
490
492
|
|
|
491
|
-
// ── Helper: word-wrap lines ──
|
|
492
|
-
|
|
493
|
-
function wrapLines(lines: readonly string[], charsPerLine: number): string[] {
|
|
494
|
-
const result: string[] = [];
|
|
495
|
-
for (const line of lines) {
|
|
496
|
-
const words = line.split(/\s+/);
|
|
497
|
-
let current = '';
|
|
498
|
-
for (const word of words) {
|
|
499
|
-
const test = current ? `${current} ${word}` : word;
|
|
500
|
-
if (test.length > charsPerLine && current) {
|
|
501
|
-
result.push(current);
|
|
502
|
-
current = word;
|
|
503
|
-
} else {
|
|
504
|
-
current = test;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (current) result.push(current);
|
|
508
|
-
}
|
|
509
|
-
return result;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
493
|
// ── Helper: circle node dimensions ──
|
|
513
494
|
|
|
514
495
|
function computeCircleNodeDims(
|
|
@@ -516,7 +497,7 @@ function computeCircleNodeDims(
|
|
|
516
497
|
hasDesc: boolean
|
|
517
498
|
): { width: number; height: number; wrappedDesc: WrappedDescLine[] } {
|
|
518
499
|
if (!hasDesc) {
|
|
519
|
-
const textW = node.label
|
|
500
|
+
const textW = measureText(node.label, CIRCLE_LABEL_FONT_SIZE);
|
|
520
501
|
const r = Math.max(MIN_CIRCLE_RADIUS, textW / 2 + CIRCLE_PAD);
|
|
521
502
|
return { width: r * 2, height: r * 2, wrappedDesc: [] };
|
|
522
503
|
}
|
|
@@ -529,7 +510,7 @@ function computeCircleNodeDims(
|
|
|
529
510
|
const textBlockH = totalLines * DESC_LINE_HEIGHT + CIRCLE_PAD;
|
|
530
511
|
|
|
531
512
|
if (textBlockH / 2 <= r * 0.85) {
|
|
532
|
-
const labelW = node.label
|
|
513
|
+
const labelW = measureText(node.label, CIRCLE_LABEL_FONT_SIZE);
|
|
533
514
|
const labelY = -textBlockH / 2 + DESC_LINE_HEIGHT;
|
|
534
515
|
const availW = 2 * Math.sqrt(Math.max(0, r * r - labelY * labelY));
|
|
535
516
|
if (labelW <= availW - CIRCLE_PAD) {
|
|
@@ -563,29 +544,32 @@ function wrapLinesForCircle(
|
|
|
563
544
|
descriptions: readonly string[],
|
|
564
545
|
radius: number
|
|
565
546
|
): string[] {
|
|
566
|
-
// First pass: wrap
|
|
547
|
+
// First pass: wrap to the center pixel-width to estimate line count.
|
|
567
548
|
const centerWidth = radius * 2 * 0.75;
|
|
568
|
-
const
|
|
569
|
-
|
|
549
|
+
const roughWrapped = descriptions.flatMap((d) =>
|
|
550
|
+
wrapTextToWidth(d, DESC_FONT_SIZE, centerWidth)
|
|
551
|
+
);
|
|
570
552
|
const totalLines = 1 + roughWrapped.length; // +1 for label line
|
|
571
553
|
const blockH = totalLines * DESC_LINE_HEIGHT;
|
|
572
554
|
|
|
573
|
-
// Second pass: re-wrap each source line with position-aware width
|
|
555
|
+
// Second pass: re-wrap each source line with a position-aware pixel width —
|
|
556
|
+
// wider near the circle's vertical center, narrower toward the edges.
|
|
574
557
|
const result: string[] = [];
|
|
575
558
|
let lineIdx = 1; // start after label line
|
|
576
559
|
for (const srcLine of descriptions) {
|
|
577
|
-
const words = srcLine.split(/\s+/);
|
|
560
|
+
const words = srcLine.split(/\s+/).filter((w) => w.length > 0);
|
|
578
561
|
let current = '';
|
|
579
562
|
for (const word of words) {
|
|
580
|
-
// Compute available width at this line's y position
|
|
563
|
+
// Compute available pixel width at this line's y position.
|
|
581
564
|
const y = -blockH / 2 + (lineIdx + 0.5) * DESC_LINE_HEIGHT;
|
|
582
565
|
const rSq = radius * radius;
|
|
583
|
-
const availPx =
|
|
584
|
-
|
|
585
|
-
|
|
566
|
+
const availPx = Math.max(
|
|
567
|
+
20,
|
|
568
|
+
y * y < rSq ? 2 * Math.sqrt(rSq - y * y) - CIRCLE_PAD * 2 : centerWidth
|
|
569
|
+
);
|
|
586
570
|
|
|
587
571
|
const test = current ? `${current} ${word}` : word;
|
|
588
|
-
if (test
|
|
572
|
+
if (measureText(test, DESC_FONT_SIZE) > availPx && current) {
|
|
589
573
|
result.push(current);
|
|
590
574
|
lineIdx++;
|
|
591
575
|
current = word;
|
|
@@ -725,16 +709,18 @@ function computeEdgePaths(
|
|
|
725
709
|
edge.description
|
|
726
710
|
);
|
|
727
711
|
const lineCount = labelLines.length + descLines.length;
|
|
728
|
-
let
|
|
729
|
-
for (const l of labelLines)
|
|
730
|
-
|
|
712
|
+
let labelPxW = 0;
|
|
713
|
+
for (const l of labelLines)
|
|
714
|
+
labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE));
|
|
715
|
+
for (const l of descLines)
|
|
716
|
+
labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE));
|
|
731
717
|
const { labelX, labelY, labelAngle } = computeEdgeLabelPosition(
|
|
732
718
|
midAngle,
|
|
733
719
|
radius,
|
|
734
720
|
cx,
|
|
735
721
|
cy,
|
|
736
722
|
lineCount,
|
|
737
|
-
|
|
723
|
+
labelPxW,
|
|
738
724
|
layoutNodes
|
|
739
725
|
);
|
|
740
726
|
const layoutEdge: CycleLayoutEdge = {
|
|
@@ -769,10 +755,10 @@ function computeEdgeLabelPosition(
|
|
|
769
755
|
cx: number,
|
|
770
756
|
cy: number,
|
|
771
757
|
lineCount: number,
|
|
772
|
-
|
|
758
|
+
labelPxW: number,
|
|
773
759
|
layoutNodes: CycleLayoutNode[]
|
|
774
760
|
): { labelX: number; labelY: number; labelAngle: number } {
|
|
775
|
-
if (lineCount === 0 ||
|
|
761
|
+
if (lineCount === 0 || labelPxW === 0) {
|
|
776
762
|
return {
|
|
777
763
|
labelX: cx + radius * Math.cos(midAngle),
|
|
778
764
|
labelY: cy + radius * Math.sin(midAngle),
|
|
@@ -781,7 +767,7 @@ function computeEdgeLabelPosition(
|
|
|
781
767
|
}
|
|
782
768
|
|
|
783
769
|
const EDGE_LABEL_CORNER_OFFSET = 10;
|
|
784
|
-
const labelW =
|
|
770
|
+
const labelW = labelPxW;
|
|
785
771
|
const labelH = lineCount * 15;
|
|
786
772
|
const cosT = Math.cos(midAngle);
|
|
787
773
|
const sinT = Math.sin(midAngle);
|
|
@@ -867,9 +853,6 @@ function computeEdgeLabelPosition(
|
|
|
867
853
|
};
|
|
868
854
|
}
|
|
869
855
|
|
|
870
|
-
/** Estimated character width at 11px edge label font. */
|
|
871
|
-
const EDGE_LABEL_CHAR_W = 7;
|
|
872
|
-
|
|
873
856
|
/**
|
|
874
857
|
* Check if edge labels overflow the canvas and return a reduced radius if needed.
|
|
875
858
|
* Returns null if everything fits.
|
|
@@ -910,12 +893,12 @@ function fitToCanvas(
|
|
|
910
893
|
le.label,
|
|
911
894
|
edge.description
|
|
912
895
|
);
|
|
913
|
-
let
|
|
914
|
-
for (const l of labelLines)
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
896
|
+
let textWidth = 0;
|
|
897
|
+
for (const l of labelLines)
|
|
898
|
+
textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE));
|
|
899
|
+
for (const l of descLines)
|
|
900
|
+
textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE));
|
|
901
|
+
if (textWidth === 0) continue;
|
|
919
902
|
|
|
920
903
|
// Determine text-anchor direction from label angle (mirrors renderer logic)
|
|
921
904
|
const normAngle =
|