@diagrammo/dgmo 0.8.4 → 0.8.6
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/.claude/commands/dgmo.md +300 -0
- package/.cursorrules +20 -2
- package/.github/copilot-instructions.md +20 -2
- package/.windsurfrules +20 -2
- package/AGENTS.md +23 -3
- package/dist/cli.cjs +191 -189
- package/dist/editor.cjs +5 -18
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +5 -18
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +543 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.cts +32 -0
- package/dist/highlight.d.ts +32 -0
- package/dist/highlight.js +513 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +3253 -3356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -56
- package/dist/index.d.ts +77 -56
- package/dist/index.js +3247 -3349
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +113 -33
- package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
- package/gallery/fixtures/slope.dgmo +7 -6
- package/package.json +26 -6
- package/src/boxes-and-lines/collapse.ts +78 -0
- package/src/boxes-and-lines/layout.ts +319 -0
- package/src/boxes-and-lines/parser.ts +694 -0
- package/src/boxes-and-lines/renderer.ts +848 -0
- package/src/boxes-and-lines/types.ts +40 -0
- package/src/c4/parser.ts +10 -5
- package/src/c4/renderer.ts +232 -56
- package/src/chart.ts +9 -4
- package/src/cli.ts +49 -6
- package/src/completion.ts +25 -33
- package/src/d3.ts +187 -46
- package/src/dgmo-router.ts +3 -7
- package/src/echarts.ts +38 -2
- package/src/editor/highlight-api.ts +444 -0
- package/src/editor/keywords.ts +6 -19
- package/src/er/parser.ts +10 -4
- package/src/gantt/parser.ts +7 -4
- package/src/gantt/renderer.ts +3 -5
- package/src/index.ts +106 -50
- package/src/infra/parser.ts +7 -5
- package/src/infra/renderer.ts +2 -2
- package/src/kanban/parser.ts +7 -5
- package/src/kanban/renderer.ts +43 -18
- package/src/org/parser.ts +7 -4
- package/src/org/renderer.ts +40 -29
- package/src/sequence/parser.ts +11 -5
- package/src/sequence/renderer.ts +114 -45
- package/src/sitemap/parser.ts +8 -4
- package/src/sitemap/renderer.ts +137 -57
- package/src/utils/legend-svg.ts +44 -20
- package/src/utils/parsing.ts +1 -1
- package/src/utils/tag-groups.ts +21 -1
- package/gallery/fixtures/initiative-status-full.dgmo +0 -46
- package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
- package/gallery/fixtures/initiative-status.dgmo +0 -9
- package/src/initiative-status/collapse.ts +0 -76
- package/src/initiative-status/filter.ts +0 -63
- package/src/initiative-status/layout.ts +0 -650
- package/src/initiative-status/parser.ts +0 -629
- package/src/initiative-status/renderer.ts +0 -1199
- package/src/initiative-status/types.ts +0 -57
package/src/cli.ts
CHANGED
|
@@ -48,9 +48,9 @@ const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
|
48
48
|
org: 'Org chart — hierarchical tree structures',
|
|
49
49
|
kanban: 'Kanban board — task/workflow columns',
|
|
50
50
|
c4: 'C4 diagram — system architecture (context, container, component, deployment)',
|
|
51
|
-
'initiative-status':
|
|
52
|
-
'Initiative status — project roadmap with dependency tracking',
|
|
53
51
|
infra: 'Infra chart — infrastructure traffic flow with rps computation',
|
|
52
|
+
'boxes-and-lines':
|
|
53
|
+
'Boxes and lines — general-purpose node-edge diagrams with groups and tags',
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
const CLAUDE_SKILL_CONTENT = `# dgmo — Diagrammo Diagram Assistant
|
|
@@ -192,9 +192,10 @@ Key options:
|
|
|
192
192
|
| \`org\` | Hierarchical tree structures |
|
|
193
193
|
| \`kanban\` | Task / workflow columns |
|
|
194
194
|
| \`c4\` | System architecture (context → container → component → deployment) |
|
|
195
|
-
| \`initiative-status\` | Project roadmap with dependency tracking |
|
|
196
195
|
| \`sitemap\` | Website / app navigation structure |
|
|
197
196
|
| \`infra\` | Infrastructure traffic flow with rps computation |
|
|
197
|
+
| \`gantt\` | Project scheduling with dependencies |
|
|
198
|
+
| \`boxes-and-lines\` | General-purpose node-edge diagrams with groups and tags |
|
|
198
199
|
|
|
199
200
|
## Key Syntax Patterns
|
|
200
201
|
|
|
@@ -452,9 +453,9 @@ API
|
|
|
452
453
|
latency-ms: 45
|
|
453
454
|
\`\`\`
|
|
454
455
|
|
|
455
|
-
## All
|
|
456
|
+
## All 31 chart types
|
|
456
457
|
|
|
457
|
-
bar, line, multi-line, area, pie, doughnut, radar, polar-area, bar-stacked, scatter, sankey, chord, function, heatmap, funnel, slope, wordcloud, arc, timeline, venn, quadrant, sequence, flowchart, state, class, er, org, kanban, c4,
|
|
458
|
+
bar, line, multi-line, area, pie, doughnut, radar, polar-area, bar-stacked, scatter, sankey, chord, function, heatmap, funnel, slope, wordcloud, arc, timeline, venn, quadrant, sequence, flowchart, state, class, er, org, kanban, c4, sitemap, infra
|
|
458
459
|
|
|
459
460
|
## Common patterns
|
|
460
461
|
|
|
@@ -486,6 +487,7 @@ Full reference: call \`get_language_reference\` MCP tool or visit diagrammo.app/
|
|
|
486
487
|
function printHelp(): void {
|
|
487
488
|
console.log(`Usage: dgmo <input> [options]
|
|
488
489
|
cat input.dgmo | dgmo [options]
|
|
490
|
+
dgmo cat <file> Display file with syntax highlighting
|
|
489
491
|
|
|
490
492
|
Render a .dgmo file to PNG (default) or SVG.
|
|
491
493
|
|
|
@@ -534,6 +536,8 @@ function parseArgs(argv: string[]): {
|
|
|
534
536
|
copy: boolean;
|
|
535
537
|
json: boolean;
|
|
536
538
|
chartTypes: boolean;
|
|
539
|
+
cat: boolean;
|
|
540
|
+
noColor: boolean;
|
|
537
541
|
installClaudeSkill: boolean;
|
|
538
542
|
installClaudeCodeIntegration: boolean;
|
|
539
543
|
installCodexIntegration: boolean;
|
|
@@ -553,6 +557,8 @@ function parseArgs(argv: string[]): {
|
|
|
553
557
|
copy: false,
|
|
554
558
|
json: false,
|
|
555
559
|
chartTypes: false,
|
|
560
|
+
cat: false,
|
|
561
|
+
noColor: false,
|
|
556
562
|
installClaudeSkill: false,
|
|
557
563
|
installClaudeCodeIntegration: false,
|
|
558
564
|
installCodexIntegration: false,
|
|
@@ -572,7 +578,13 @@ function parseArgs(argv: string[]): {
|
|
|
572
578
|
while (i < args.length) {
|
|
573
579
|
const arg = args[i];
|
|
574
580
|
|
|
575
|
-
if (arg === '
|
|
581
|
+
if (arg === 'cat' && !result.cat && !result.input) {
|
|
582
|
+
result.cat = true;
|
|
583
|
+
i++;
|
|
584
|
+
} else if (arg === '--no-color') {
|
|
585
|
+
result.noColor = true;
|
|
586
|
+
i++;
|
|
587
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
576
588
|
result.help = true;
|
|
577
589
|
i++;
|
|
578
590
|
} else if (arg === '--version' || arg === '-v') {
|
|
@@ -746,6 +758,37 @@ async function main(): Promise<void> {
|
|
|
746
758
|
return;
|
|
747
759
|
}
|
|
748
760
|
|
|
761
|
+
if (opts.cat) {
|
|
762
|
+
const useColor =
|
|
763
|
+
!opts.noColor && !process.env.NO_COLOR && process.stdout.isTTY === true;
|
|
764
|
+
|
|
765
|
+
let catContent: string;
|
|
766
|
+
if (opts.input && opts.input !== '-') {
|
|
767
|
+
const inputPath = resolve(opts.input);
|
|
768
|
+
try {
|
|
769
|
+
catContent = readFileSync(inputPath, 'utf-8');
|
|
770
|
+
} catch {
|
|
771
|
+
console.error(`Error: Cannot read file "${inputPath}"`);
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
// Read from stdin
|
|
776
|
+
try {
|
|
777
|
+
catContent = readFileSync(0, 'utf-8');
|
|
778
|
+
} catch {
|
|
779
|
+
console.error('Error: No input file specified');
|
|
780
|
+
console.error('Usage: dgmo cat <file>');
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const { highlightDgmo, renderAnsi } =
|
|
786
|
+
await import('./editor/highlight-api');
|
|
787
|
+
const tokens = highlightDgmo(catContent);
|
|
788
|
+
process.stdout.write(renderAnsi(tokens, useColor));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
749
792
|
if (opts.installClaudeCodeIntegration) {
|
|
750
793
|
const claudeDir = join(homedir(), '.claude');
|
|
751
794
|
if (!existsSync(claudeDir)) {
|
package/src/completion.ts
CHANGED
|
@@ -273,7 +273,6 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
273
273
|
}),
|
|
274
274
|
],
|
|
275
275
|
['c4', withGlobals()],
|
|
276
|
-
['initiative-status', withGlobals()],
|
|
277
276
|
[
|
|
278
277
|
'state',
|
|
279
278
|
withGlobals({
|
|
@@ -313,6 +312,14 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
313
312
|
dependencies: { description: 'Show dependencies' },
|
|
314
313
|
}),
|
|
315
314
|
],
|
|
315
|
+
[
|
|
316
|
+
'boxes-and-lines',
|
|
317
|
+
withGlobals({
|
|
318
|
+
direction: { description: 'Layout direction', values: ['LR', 'TB'] },
|
|
319
|
+
'active-tag': { description: 'Active tag group name' },
|
|
320
|
+
hide: { description: 'Hide tag:value pairs' },
|
|
321
|
+
}),
|
|
322
|
+
],
|
|
316
323
|
]);
|
|
317
324
|
|
|
318
325
|
// ============================================================
|
|
@@ -351,11 +358,11 @@ const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
|
351
358
|
org: 'Organization chart',
|
|
352
359
|
kanban: 'Kanban board',
|
|
353
360
|
c4: 'C4 architecture diagram',
|
|
354
|
-
'initiative-status': 'Initiative status diagram',
|
|
355
361
|
state: 'State diagram',
|
|
356
362
|
sitemap: 'Sitemap diagram',
|
|
357
363
|
infra: 'Infrastructure diagram',
|
|
358
364
|
gantt: 'Gantt chart',
|
|
365
|
+
'boxes-and-lines': 'Boxes and lines diagram',
|
|
359
366
|
};
|
|
360
367
|
|
|
361
368
|
/** All chart types with descriptions, for chart type autocomplete. Excludes `multi-line` alias. */
|
|
@@ -481,28 +488,12 @@ export const PIPE_METADATA = new Map<
|
|
|
481
488
|
},
|
|
482
489
|
],
|
|
483
490
|
[
|
|
484
|
-
'
|
|
491
|
+
'boxes-and-lines',
|
|
485
492
|
{
|
|
486
493
|
node: {
|
|
487
|
-
|
|
488
|
-
doing: { description: 'In progress' },
|
|
489
|
-
todo: { description: 'Not started' },
|
|
490
|
-
blocked: { description: 'Blocked' },
|
|
491
|
-
na: { description: 'Not applicable' },
|
|
492
|
-
wip: { description: 'Work in progress (alias for doing)' },
|
|
493
|
-
paused: { description: 'Paused (alias for blocked)' },
|
|
494
|
-
waiting: { description: 'Waiting (alias for blocked)' },
|
|
495
|
-
},
|
|
496
|
-
edge: {
|
|
497
|
-
done: { description: 'Completed' },
|
|
498
|
-
doing: { description: 'In progress' },
|
|
499
|
-
todo: { description: 'Not started' },
|
|
500
|
-
blocked: { description: 'Blocked' },
|
|
501
|
-
na: { description: 'Not applicable' },
|
|
502
|
-
wip: { description: 'Work in progress (alias for doing)' },
|
|
503
|
-
paused: { description: 'Paused (alias for blocked)' },
|
|
504
|
-
waiting: { description: 'Waiting (alias for blocked)' },
|
|
494
|
+
description: { description: 'Node description text' },
|
|
505
495
|
},
|
|
496
|
+
edge: {},
|
|
506
497
|
},
|
|
507
498
|
],
|
|
508
499
|
]);
|
|
@@ -935,12 +926,12 @@ function extractGanttSymbols(docText: string): DiagramSymbols {
|
|
|
935
926
|
}
|
|
936
927
|
|
|
937
928
|
// ============================================================
|
|
938
|
-
//
|
|
929
|
+
// Boxes-and-lines extractor
|
|
939
930
|
// ============================================================
|
|
940
931
|
|
|
941
|
-
const
|
|
932
|
+
const BL_ARROW_RE = /^(\S+)\s+(?:-.*)?(?:->|<->)\s+(\S+)/;
|
|
942
933
|
|
|
943
|
-
function
|
|
934
|
+
function extractBoxesAndLinesSymbols(docText: string): DiagramSymbols {
|
|
944
935
|
const lines = docText.split('\n');
|
|
945
936
|
const entities: string[] = [];
|
|
946
937
|
let pastFirstLine = false;
|
|
@@ -968,8 +959,11 @@ function extractInitiativeStatusSymbols(docText: string): DiagramSymbols {
|
|
|
968
959
|
inTagBlock = false;
|
|
969
960
|
}
|
|
970
961
|
|
|
971
|
-
//
|
|
972
|
-
|
|
962
|
+
// Skip groups
|
|
963
|
+
if (/^\[.+?\]/.test(trimmed)) continue;
|
|
964
|
+
|
|
965
|
+
// Edge lines
|
|
966
|
+
const arrowMatch = trimmed.match(BL_ARROW_RE);
|
|
973
967
|
if (arrowMatch) {
|
|
974
968
|
const src = arrowMatch[1].split('|')[0].trim();
|
|
975
969
|
const dst = arrowMatch[2].split('|')[0].trim();
|
|
@@ -978,14 +972,12 @@ function extractInitiativeStatusSymbols(docText: string): DiagramSymbols {
|
|
|
978
972
|
continue;
|
|
979
973
|
}
|
|
980
974
|
|
|
981
|
-
// Node lines
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if (label && !entities.includes(label)) entities.push(label);
|
|
985
|
-
}
|
|
975
|
+
// Node lines
|
|
976
|
+
const label = trimmed.split('|')[0].split('[')[0].trim();
|
|
977
|
+
if (label && !entities.includes(label)) entities.push(label);
|
|
986
978
|
}
|
|
987
979
|
|
|
988
|
-
return { kind: '
|
|
980
|
+
return { kind: 'boxes-and-lines', entities, keywords: [] };
|
|
989
981
|
}
|
|
990
982
|
|
|
991
983
|
// ============================================================
|
|
@@ -1001,4 +993,4 @@ registerExtractor('state', extractStateSymbols);
|
|
|
1001
993
|
registerExtractor('sitemap', extractSitemapSymbols);
|
|
1002
994
|
registerExtractor('c4', extractC4Symbols);
|
|
1003
995
|
registerExtractor('gantt', extractGanttSymbols);
|
|
1004
|
-
registerExtractor('
|
|
996
|
+
registerExtractor('boxes-and-lines', extractBoxesAndLinesSymbols);
|
package/src/d3.ts
CHANGED
|
@@ -193,6 +193,7 @@ import {
|
|
|
193
193
|
matchTagBlockHeading,
|
|
194
194
|
validateTagValues,
|
|
195
195
|
resolveTagColor,
|
|
196
|
+
stripDefaultModifier,
|
|
196
197
|
} from './utils/tag-groups';
|
|
197
198
|
import type { TagGroup } from './utils/tag-groups';
|
|
198
199
|
import {
|
|
@@ -509,6 +510,7 @@ export function parseVisualization(
|
|
|
509
510
|
let timelineEraBlockIndent = 0;
|
|
510
511
|
let inTimelineMarkerBlock = false;
|
|
511
512
|
let timelineMarkerBlockIndent = 0;
|
|
513
|
+
let inSlopePeriodBlock = false;
|
|
512
514
|
const timelineAliasMap = new Map<string, string>();
|
|
513
515
|
const VALID_D3_TYPES = new Set([
|
|
514
516
|
'slope',
|
|
@@ -569,16 +571,16 @@ export function parseVisualization(
|
|
|
569
571
|
}
|
|
570
572
|
}
|
|
571
573
|
|
|
572
|
-
// Timeline tag group entries (indented under tag
|
|
574
|
+
// Timeline tag group entries (indented under tag heading)
|
|
573
575
|
if (currentTimelineTagGroup && indent > 0) {
|
|
574
|
-
const
|
|
575
|
-
const isDefault = /\bdefault\s*$/.test(trimmedEntry);
|
|
576
|
-
const entryText = isDefault
|
|
577
|
-
? trimmedEntry.replace(/\s+default\s*$/, '').trim()
|
|
578
|
-
: trimmedEntry;
|
|
576
|
+
const { text: entryText, isDefault } = stripDefaultModifier(line);
|
|
579
577
|
const { label, color } = extractColor(entryText, palette);
|
|
580
578
|
if (color) {
|
|
581
|
-
if (isDefault)
|
|
579
|
+
if (isDefault) {
|
|
580
|
+
currentTimelineTagGroup.defaultValue = label;
|
|
581
|
+
} else if (currentTimelineTagGroup.entries.length === 0) {
|
|
582
|
+
currentTimelineTagGroup.defaultValue = label;
|
|
583
|
+
}
|
|
582
584
|
currentTimelineTagGroup.entries.push({
|
|
583
585
|
value: label,
|
|
584
586
|
color,
|
|
@@ -1013,9 +1015,9 @@ export function parseVisualization(
|
|
|
1013
1015
|
continue;
|
|
1014
1016
|
}
|
|
1015
1017
|
|
|
1016
|
-
// Data points: Label x, y
|
|
1018
|
+
// Data points: Label x, y OR Label x y
|
|
1017
1019
|
const pointMatch = line.match(
|
|
1018
|
-
/^(.+?)\s+([0-9]*\.?[0-9]+)\s
|
|
1020
|
+
/^(.+?)\s+([0-9]*\.?[0-9]+)\s*[,\s]\s*([0-9]*\.?[0-9]+)\s*$/
|
|
1019
1021
|
);
|
|
1020
1022
|
if (pointMatch) {
|
|
1021
1023
|
const label = pointMatch[1].trim();
|
|
@@ -1099,6 +1101,164 @@ export function parseVisualization(
|
|
|
1099
1101
|
}
|
|
1100
1102
|
}
|
|
1101
1103
|
|
|
1104
|
+
// ── Slope chart: period directive + right-scan data rows ──
|
|
1105
|
+
if (result.type === 'slope') {
|
|
1106
|
+
// Period block: indented lines inside `period` block
|
|
1107
|
+
// (blank lines are pre-filtered at loop top, so only non-indented lines close the block)
|
|
1108
|
+
if (inSlopePeriodBlock) {
|
|
1109
|
+
if (indent > 0) {
|
|
1110
|
+
result.periods.push(line);
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
// Non-indented line → close block, fall through to process normally
|
|
1114
|
+
inSlopePeriodBlock = false;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Period directive: `period Label1 Label2` or bare `period` (block open)
|
|
1118
|
+
// Only accept before data rows start (F4: prevent keyword shadowing labels)
|
|
1119
|
+
if (result.data.length === 0) {
|
|
1120
|
+
const periodMatch = line.match(/^period\b(.*)$/i);
|
|
1121
|
+
if (periodMatch) {
|
|
1122
|
+
if (result.periods.length > 0 && !inSlopePeriodBlock) {
|
|
1123
|
+
// F5: warn on duplicate period directives
|
|
1124
|
+
warn(
|
|
1125
|
+
lineNumber,
|
|
1126
|
+
`Duplicate 'period' directive — periods are already defined`
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
const rest = periodMatch[1].trim();
|
|
1130
|
+
if (rest) {
|
|
1131
|
+
// One-line: `period 1715 1725`
|
|
1132
|
+
const periodLabels = rest.split(/\s+/);
|
|
1133
|
+
result.periods.push(...periodLabels);
|
|
1134
|
+
} else {
|
|
1135
|
+
// Block open: bare `period`
|
|
1136
|
+
inSlopePeriodBlock = true;
|
|
1137
|
+
}
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Migration error: bare period line (old syntax — comma-separated, no keyword)
|
|
1143
|
+
// F1: Only fire when ALL comma-separated tokens are short (≤20 chars) and non-empty
|
|
1144
|
+
if (
|
|
1145
|
+
result.periods.length === 0 &&
|
|
1146
|
+
line.includes(',') &&
|
|
1147
|
+
!line.includes(':')
|
|
1148
|
+
) {
|
|
1149
|
+
const tokens = line
|
|
1150
|
+
.split(',')
|
|
1151
|
+
.map((t) => t.trim())
|
|
1152
|
+
.filter(Boolean);
|
|
1153
|
+
const looksLikePeriods =
|
|
1154
|
+
tokens.length >= 2 && tokens.every((t) => t.length <= 20);
|
|
1155
|
+
if (looksLikePeriods) {
|
|
1156
|
+
return fail(
|
|
1157
|
+
lineNumber,
|
|
1158
|
+
`Period lines require the 'period' keyword — use 'period ${tokens.join(' ')}'`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Migration error: old colon syntax in data rows
|
|
1164
|
+
// F2: Only fire when content after colon is predominantly numeric (old "Label: val1, val2" pattern)
|
|
1165
|
+
if (line.includes(':')) {
|
|
1166
|
+
const colonPos = line.indexOf(':');
|
|
1167
|
+
const afterColon = line.substring(colonPos + 1).trim();
|
|
1168
|
+
const numericTokens = afterColon
|
|
1169
|
+
.split(/[,\s]+/)
|
|
1170
|
+
.filter((v) => /^-?\d/.test(v));
|
|
1171
|
+
// Only trigger if most tokens after the colon are numeric (old data pattern)
|
|
1172
|
+
if (numericTokens.length >= 1) {
|
|
1173
|
+
const allTokens = afterColon.split(/[,\s]+/).filter(Boolean);
|
|
1174
|
+
if (numericTokens.length >= allTokens.length * 0.5) {
|
|
1175
|
+
const label = line.substring(0, colonPos).trim();
|
|
1176
|
+
return fail(
|
|
1177
|
+
lineNumber,
|
|
1178
|
+
`Colons are no longer used in slope data rows — use '${label} ${numericTokens.join(' ')}'`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Right-scan data row parsing (requires periods to be known)
|
|
1185
|
+
if (result.periods.length >= 2) {
|
|
1186
|
+
const P = result.periods.length;
|
|
1187
|
+
const tokens = line.split(/\s+/);
|
|
1188
|
+
const values: number[] = [];
|
|
1189
|
+
|
|
1190
|
+
// Scan from right, capped at P values
|
|
1191
|
+
let rightIdx = tokens.length - 1;
|
|
1192
|
+
while (rightIdx >= 0 && values.length < P) {
|
|
1193
|
+
const raw = tokens[rightIdx].replace(/,/g, '');
|
|
1194
|
+
const num = parseFloat(raw);
|
|
1195
|
+
if (!isNaN(num) && /^-?\d/.test(raw)) {
|
|
1196
|
+
values.unshift(num);
|
|
1197
|
+
rightIdx--;
|
|
1198
|
+
} else {
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (values.length < P) {
|
|
1204
|
+
warn(
|
|
1205
|
+
lineNumber,
|
|
1206
|
+
`Data row has ${values.length} numeric value(s) but ${P} period(s) are defined — expected ${P} values`
|
|
1207
|
+
);
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Remaining left tokens = label
|
|
1212
|
+
const labelTokens = tokens.slice(0, rightIdx + 1);
|
|
1213
|
+
const joinedLabel = labelTokens.join(' ');
|
|
1214
|
+
|
|
1215
|
+
if (!joinedLabel) {
|
|
1216
|
+
warn(
|
|
1217
|
+
lineNumber,
|
|
1218
|
+
`Data row has no label — add a label before the numeric values`
|
|
1219
|
+
);
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Color annotation: `Label (color)` → extract color
|
|
1224
|
+
const colorMatch = joinedLabel.match(/^(.+?)\(([^)]+)\)\s*$/);
|
|
1225
|
+
const labelPart = colorMatch ? colorMatch[1].trim() : joinedLabel;
|
|
1226
|
+
const colorPart = colorMatch
|
|
1227
|
+
? resolveColor(colorMatch[2].trim(), palette)
|
|
1228
|
+
: null;
|
|
1229
|
+
|
|
1230
|
+
if (!labelPart) {
|
|
1231
|
+
warn(
|
|
1232
|
+
lineNumber,
|
|
1233
|
+
`Data row has no label — add a label before the numeric values`
|
|
1234
|
+
);
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// F3: Warn on purely numeric labels — likely a mistake
|
|
1239
|
+
if (/^\d[\d,.]*$/.test(labelPart)) {
|
|
1240
|
+
warn(
|
|
1241
|
+
lineNumber,
|
|
1242
|
+
`Label '${labelPart}' looks numeric — this may indicate too many values or a missing label`
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
result.data.push({
|
|
1247
|
+
label: labelPart,
|
|
1248
|
+
values,
|
|
1249
|
+
color: colorPart,
|
|
1250
|
+
lineNumber,
|
|
1251
|
+
});
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// If we get here in a slope chart, it's an unrecognized line
|
|
1256
|
+
if (firstLineParsed) {
|
|
1257
|
+
warn(lineNumber, `Unexpected line: '${line}'.`);
|
|
1258
|
+
}
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1102
1262
|
// ── Colon-separated metadata / options (legacy + data lines) ──
|
|
1103
1263
|
const colonIndex = line.indexOf(':');
|
|
1104
1264
|
|
|
@@ -1222,23 +1382,6 @@ export function parseVisualization(
|
|
|
1222
1382
|
continue;
|
|
1223
1383
|
}
|
|
1224
1384
|
|
|
1225
|
-
// Period line: comma-separated labels with no colon before first comma
|
|
1226
|
-
// e.g., "2020, 2024" or "Q1 2023, Q2 2023, Q3 2023"
|
|
1227
|
-
if (
|
|
1228
|
-
result.periods.length === 0 &&
|
|
1229
|
-
line.includes(',') &&
|
|
1230
|
-
!line.includes(':')
|
|
1231
|
-
) {
|
|
1232
|
-
const periods = line
|
|
1233
|
-
.split(',')
|
|
1234
|
-
.map((p) => p.trim())
|
|
1235
|
-
.filter(Boolean);
|
|
1236
|
-
if (periods.length >= 2) {
|
|
1237
|
-
result.periods = periods;
|
|
1238
|
-
continue;
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
1385
|
// Catch-all: nothing matched this line
|
|
1243
1386
|
// Skip on first line — chart type suggestion is handled post-loop
|
|
1244
1387
|
if (firstLineParsed) {
|
|
@@ -1408,14 +1551,14 @@ export function parseVisualization(
|
|
|
1408
1551
|
if (result.periods.length < 2) {
|
|
1409
1552
|
return fail(
|
|
1410
1553
|
1,
|
|
1411
|
-
|
|
1554
|
+
"Missing 'period' directive. Add 'period 2020 2024' before data rows (minimum 2 periods required)"
|
|
1412
1555
|
);
|
|
1413
1556
|
}
|
|
1414
1557
|
|
|
1415
1558
|
if (result.data.length === 0) {
|
|
1416
1559
|
warn(
|
|
1417
1560
|
1,
|
|
1418
|
-
|
|
1561
|
+
"No data lines found. Add data as 'Label value1 value2' (e.g., 'Blackbeard 40 4')"
|
|
1419
1562
|
);
|
|
1420
1563
|
}
|
|
1421
1564
|
|
|
@@ -4905,8 +5048,8 @@ export function renderTimeline(
|
|
|
4905
5048
|
}
|
|
4906
5049
|
|
|
4907
5050
|
const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
|
|
4908
|
-
const pillYOff =
|
|
4909
|
-
const pillH = LG_HEIGHT -
|
|
5051
|
+
const pillYOff = LG_CAPSULE_PAD;
|
|
5052
|
+
const pillH = LG_HEIGHT - LG_CAPSULE_PAD * 2;
|
|
4910
5053
|
|
|
4911
5054
|
// Pill background
|
|
4912
5055
|
gEl
|
|
@@ -6714,29 +6857,27 @@ export async function renderForExport(
|
|
|
6714
6857
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6715
6858
|
}
|
|
6716
6859
|
|
|
6717
|
-
if (detectedType === '
|
|
6718
|
-
const {
|
|
6719
|
-
|
|
6720
|
-
const {
|
|
6721
|
-
await import('./
|
|
6722
|
-
const { renderInitiativeStatus } =
|
|
6723
|
-
await import('./initiative-status/renderer');
|
|
6860
|
+
if (detectedType === 'boxes-and-lines') {
|
|
6861
|
+
const { parseBoxesAndLines } = await import('./boxes-and-lines/parser');
|
|
6862
|
+
const { layoutBoxesAndLines } = await import('./boxes-and-lines/layout');
|
|
6863
|
+
const { renderBoxesAndLinesForExport } =
|
|
6864
|
+
await import('./boxes-and-lines/renderer');
|
|
6724
6865
|
|
|
6725
6866
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6726
|
-
const
|
|
6727
|
-
if (
|
|
6867
|
+
const blParsed = parseBoxesAndLines(content);
|
|
6868
|
+
if (blParsed.error || blParsed.nodes.length === 0) return '';
|
|
6728
6869
|
|
|
6729
|
-
const
|
|
6870
|
+
const blLayout = layoutBoxesAndLines(blParsed);
|
|
6730
6871
|
const PADDING = 20;
|
|
6731
|
-
const titleOffset =
|
|
6732
|
-
const exportWidth =
|
|
6733
|
-
const exportHeight =
|
|
6872
|
+
const titleOffset = blParsed.title ? 40 : 0;
|
|
6873
|
+
const exportWidth = blLayout.width + PADDING * 2;
|
|
6874
|
+
const exportHeight = blLayout.height + PADDING * 2 + titleOffset;
|
|
6734
6875
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6735
6876
|
|
|
6736
|
-
|
|
6877
|
+
renderBoxesAndLinesForExport(
|
|
6737
6878
|
container,
|
|
6738
|
-
|
|
6739
|
-
|
|
6879
|
+
blParsed,
|
|
6880
|
+
blLayout,
|
|
6740
6881
|
effectivePalette,
|
|
6741
6882
|
theme === 'dark',
|
|
6742
6883
|
{ exportDims: { width: exportWidth, height: exportHeight } }
|
package/src/dgmo-router.ts
CHANGED
|
@@ -13,13 +13,10 @@ import { parseVisualization } from './d3';
|
|
|
13
13
|
import { parseOrg, looksLikeOrg } from './org/parser';
|
|
14
14
|
import { parseKanban } from './kanban/parser';
|
|
15
15
|
import { parseC4 } from './c4/parser';
|
|
16
|
-
import {
|
|
17
|
-
looksLikeInitiativeStatus,
|
|
18
|
-
parseInitiativeStatus,
|
|
19
|
-
} from './initiative-status/parser';
|
|
20
16
|
import { looksLikeSitemap, parseSitemap } from './sitemap/parser';
|
|
21
17
|
import { parseInfra } from './infra/parser';
|
|
22
18
|
import { parseGantt } from './gantt/parser';
|
|
19
|
+
import { parseBoxesAndLines } from './boxes-and-lines/parser';
|
|
23
20
|
import { parseFirstLine } from './utils/parsing';
|
|
24
21
|
import type { DgmoError } from './diagnostics';
|
|
25
22
|
|
|
@@ -97,7 +94,6 @@ export function parseDgmoChartType(content: string): string | null {
|
|
|
97
94
|
if (looksLikeFlowchart(content)) return 'flowchart';
|
|
98
95
|
if (looksLikeClassDiagram(content)) return 'class';
|
|
99
96
|
if (looksLikeERDiagram(content)) return 'er';
|
|
100
|
-
if (looksLikeInitiativeStatus(content)) return 'initiative-status';
|
|
101
97
|
if (looksLikeState(content)) return 'state';
|
|
102
98
|
if (looksLikeSitemap(content)) return 'sitemap';
|
|
103
99
|
if (looksLikeOrg(content)) return 'org';
|
|
@@ -147,11 +143,11 @@ const DIAGRAM_TYPES = new Set([
|
|
|
147
143
|
'org',
|
|
148
144
|
'kanban',
|
|
149
145
|
'c4',
|
|
150
|
-
'initiative-status',
|
|
151
146
|
'state',
|
|
152
147
|
'sitemap',
|
|
153
148
|
'infra',
|
|
154
149
|
'gantt',
|
|
150
|
+
'boxes-and-lines',
|
|
155
151
|
]);
|
|
156
152
|
const EXTENDED_CHART_TYPES = new Set([
|
|
157
153
|
'scatter',
|
|
@@ -226,11 +222,11 @@ const PARSE_DISPATCH = new Map<
|
|
|
226
222
|
['org', (c) => parseOrg(c)],
|
|
227
223
|
['kanban', (c) => parseKanban(c)],
|
|
228
224
|
['c4', (c) => parseC4(c)],
|
|
229
|
-
['initiative-status', (c) => parseInitiativeStatus(c)],
|
|
230
225
|
['state', (c) => parseState(c)],
|
|
231
226
|
['sitemap', (c) => parseSitemap(c)],
|
|
232
227
|
['infra', (c) => parseInfra(c)],
|
|
233
228
|
['gantt', (c) => parseGantt(c)],
|
|
229
|
+
['boxes-and-lines', (c) => parseBoxesAndLines(c)],
|
|
234
230
|
]);
|
|
235
231
|
|
|
236
232
|
/**
|
package/src/echarts.ts
CHANGED
|
@@ -2632,6 +2632,33 @@ function segmentLabelFormatter(parsed: ParsedChart): string {
|
|
|
2632
2632
|
|
|
2633
2633
|
// ── Pie / Doughnut ───────────────────────────────────────────
|
|
2634
2634
|
|
|
2635
|
+
/**
|
|
2636
|
+
* Compute pie label config: shrink radius and font when labels are long
|
|
2637
|
+
* so nothing gets truncated or wrapped.
|
|
2638
|
+
*/
|
|
2639
|
+
function pieLabelLayout(parsed: ParsedChart): {
|
|
2640
|
+
outerRadius: number;
|
|
2641
|
+
fontSize: number;
|
|
2642
|
+
} {
|
|
2643
|
+
const formatter = segmentLabelFormatter(parsed);
|
|
2644
|
+
const total = parsed.data.reduce((s, d) => s + d.value, 0);
|
|
2645
|
+
const maxLen = parsed.data.reduce((mx, d) => {
|
|
2646
|
+
const label = formatter
|
|
2647
|
+
.replace('{b}', d.label)
|
|
2648
|
+
.replace('{c}', String(d.value))
|
|
2649
|
+
.replace('{d}', total > 0 ? ((d.value / total) * 100).toFixed(2) : '0');
|
|
2650
|
+
return Math.max(mx, label.length);
|
|
2651
|
+
}, 0);
|
|
2652
|
+
|
|
2653
|
+
// Shrink radius and font for longer labels so they fit without truncation.
|
|
2654
|
+
// The chart renders in containers of varying width (800px in-app to 1200px CLI),
|
|
2655
|
+
// so we need enough margin for labels at the smallest reasonable container.
|
|
2656
|
+
if (maxLen > 30) return { outerRadius: 38, fontSize: 11 };
|
|
2657
|
+
if (maxLen > 24) return { outerRadius: 45, fontSize: 12 };
|
|
2658
|
+
if (maxLen > 18) return { outerRadius: 55, fontSize: 13 };
|
|
2659
|
+
return { outerRadius: 70, fontSize: 14 };
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2635
2662
|
function buildPieOption(
|
|
2636
2663
|
parsed: ParsedChart,
|
|
2637
2664
|
textColor: string,
|
|
@@ -2655,6 +2682,8 @@ function buildPieOption(
|
|
|
2655
2682
|
};
|
|
2656
2683
|
});
|
|
2657
2684
|
|
|
2685
|
+
const { outerRadius, fontSize } = pieLabelLayout(parsed);
|
|
2686
|
+
|
|
2658
2687
|
return {
|
|
2659
2688
|
...CHART_BASE,
|
|
2660
2689
|
...HIDE_AXES,
|
|
@@ -2666,13 +2695,16 @@ function buildPieOption(
|
|
|
2666
2695
|
series: [
|
|
2667
2696
|
{
|
|
2668
2697
|
type: 'pie',
|
|
2669
|
-
radius: isDoughnut
|
|
2698
|
+
radius: isDoughnut
|
|
2699
|
+
? [`${Math.round(outerRadius * 0.57)}%`, `${outerRadius}%`]
|
|
2700
|
+
: ['0%', `${outerRadius}%`],
|
|
2670
2701
|
data,
|
|
2671
2702
|
label: {
|
|
2672
2703
|
position: 'outside',
|
|
2673
2704
|
formatter: segmentLabelFormatter(parsed),
|
|
2674
2705
|
color: textColor,
|
|
2675
2706
|
fontFamily: FONT_FAMILY,
|
|
2707
|
+
fontSize,
|
|
2676
2708
|
},
|
|
2677
2709
|
labelLine: { show: true },
|
|
2678
2710
|
emphasis: EMPHASIS_SELF,
|
|
@@ -2790,13 +2822,17 @@ function buildPolarAreaOption(
|
|
|
2790
2822
|
{
|
|
2791
2823
|
type: 'pie',
|
|
2792
2824
|
roseType: 'radius',
|
|
2793
|
-
radius:
|
|
2825
|
+
radius: (() => {
|
|
2826
|
+
const { outerRadius } = pieLabelLayout(parsed);
|
|
2827
|
+
return [`${Math.round(outerRadius * 0.14)}%`, `${outerRadius}%`];
|
|
2828
|
+
})(),
|
|
2794
2829
|
data,
|
|
2795
2830
|
label: {
|
|
2796
2831
|
position: 'outside',
|
|
2797
2832
|
formatter: segmentLabelFormatter(parsed),
|
|
2798
2833
|
color: textColor,
|
|
2799
2834
|
fontFamily: FONT_FAMILY,
|
|
2835
|
+
fontSize: pieLabelLayout(parsed).fontSize,
|
|
2800
2836
|
},
|
|
2801
2837
|
labelLine: { show: true },
|
|
2802
2838
|
emphasis: EMPHASIS_SELF,
|