@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.
Files changed (68) hide show
  1. package/.claude/commands/dgmo.md +300 -0
  2. package/.cursorrules +20 -2
  3. package/.github/copilot-instructions.md +20 -2
  4. package/.windsurfrules +20 -2
  5. package/AGENTS.md +23 -3
  6. package/dist/cli.cjs +191 -189
  7. package/dist/editor.cjs +5 -18
  8. package/dist/editor.cjs.map +1 -1
  9. package/dist/editor.js +5 -18
  10. package/dist/editor.js.map +1 -1
  11. package/dist/highlight.cjs +543 -0
  12. package/dist/highlight.cjs.map +1 -0
  13. package/dist/highlight.d.cts +32 -0
  14. package/dist/highlight.d.ts +32 -0
  15. package/dist/highlight.js +513 -0
  16. package/dist/highlight.js.map +1 -0
  17. package/dist/index.cjs +3253 -3356
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +77 -56
  20. package/dist/index.d.ts +77 -56
  21. package/dist/index.js +3247 -3349
  22. package/dist/index.js.map +1 -1
  23. package/docs/ai-integration.md +1 -1
  24. package/docs/language-reference.md +113 -33
  25. package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
  26. package/gallery/fixtures/slope.dgmo +7 -6
  27. package/package.json +26 -6
  28. package/src/boxes-and-lines/collapse.ts +78 -0
  29. package/src/boxes-and-lines/layout.ts +319 -0
  30. package/src/boxes-and-lines/parser.ts +694 -0
  31. package/src/boxes-and-lines/renderer.ts +848 -0
  32. package/src/boxes-and-lines/types.ts +40 -0
  33. package/src/c4/parser.ts +10 -5
  34. package/src/c4/renderer.ts +232 -56
  35. package/src/chart.ts +9 -4
  36. package/src/cli.ts +49 -6
  37. package/src/completion.ts +25 -33
  38. package/src/d3.ts +187 -46
  39. package/src/dgmo-router.ts +3 -7
  40. package/src/echarts.ts +38 -2
  41. package/src/editor/highlight-api.ts +444 -0
  42. package/src/editor/keywords.ts +6 -19
  43. package/src/er/parser.ts +10 -4
  44. package/src/gantt/parser.ts +7 -4
  45. package/src/gantt/renderer.ts +3 -5
  46. package/src/index.ts +106 -50
  47. package/src/infra/parser.ts +7 -5
  48. package/src/infra/renderer.ts +2 -2
  49. package/src/kanban/parser.ts +7 -5
  50. package/src/kanban/renderer.ts +43 -18
  51. package/src/org/parser.ts +7 -4
  52. package/src/org/renderer.ts +40 -29
  53. package/src/sequence/parser.ts +11 -5
  54. package/src/sequence/renderer.ts +114 -45
  55. package/src/sitemap/parser.ts +8 -4
  56. package/src/sitemap/renderer.ts +137 -57
  57. package/src/utils/legend-svg.ts +44 -20
  58. package/src/utils/parsing.ts +1 -1
  59. package/src/utils/tag-groups.ts +21 -1
  60. package/gallery/fixtures/initiative-status-full.dgmo +0 -46
  61. package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
  62. package/gallery/fixtures/initiative-status.dgmo +0 -9
  63. package/src/initiative-status/collapse.ts +0 -76
  64. package/src/initiative-status/filter.ts +0 -63
  65. package/src/initiative-status/layout.ts +0 -650
  66. package/src/initiative-status/parser.ts +0 -629
  67. package/src/initiative-status/renderer.ts +0 -1199
  68. 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 32 chart types
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, initiative-status, sitemap, infra
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 === '--help' || arg === '-h') {
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
- 'initiative-status',
491
+ 'boxes-and-lines',
485
492
  {
486
493
  node: {
487
- done: { description: 'Completed' },
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
- // Initiative-status extractor
929
+ // Boxes-and-lines extractor
939
930
  // ============================================================
940
931
 
941
- const IS_ARROW_RE = /^(\S+)\s+(?:-.*)?->\s+(\S+)/;
932
+ const BL_ARROW_RE = /^(\S+)\s+(?:-.*)?(?:->|<->)\s+(\S+)/;
942
933
 
943
- function extractInitiativeStatusSymbols(docText: string): DiagramSymbols {
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
- // Edge lines: Source -> Target or Source -label-> Target
972
- const arrowMatch = trimmed.match(IS_ARROW_RE);
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: Label | status or just Label (at root indent)
982
- if (indent === 0) {
983
- const label = trimmed.split('|')[0].trim();
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: 'initiative-status', entities, keywords: [] };
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('initiative-status', extractInitiativeStatusSymbols);
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: heading)
574
+ // Timeline tag group entries (indented under tag heading)
573
575
  if (currentTimelineTagGroup && indent > 0) {
574
- const trimmedEntry = line;
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) currentTimelineTagGroup.defaultValue = label;
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*,\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
- 'Missing or invalid periods line. Provide at least 2 comma-separated period labels (e.g., "2020, 2024")'
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
- 'No data lines found. Add data as "Label: value1, value2" (e.g., "Apple: 25, 35")'
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 = isActive ? LG_CAPSULE_PAD : 0;
4909
- const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
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 === 'initiative-status') {
6718
- const { parseInitiativeStatus } =
6719
- await import('./initiative-status/parser');
6720
- const { layoutInitiativeStatus } =
6721
- await import('./initiative-status/layout');
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 isParsed = parseInitiativeStatus(content);
6727
- if (isParsed.error || isParsed.nodes.length === 0) return '';
6867
+ const blParsed = parseBoxesAndLines(content);
6868
+ if (blParsed.error || blParsed.nodes.length === 0) return '';
6728
6869
 
6729
- const isLayout = layoutInitiativeStatus(isParsed);
6870
+ const blLayout = layoutBoxesAndLines(blParsed);
6730
6871
  const PADDING = 20;
6731
- const titleOffset = isParsed.title ? 40 : 0;
6732
- const exportWidth = isLayout.width + PADDING * 2;
6733
- const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
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
- renderInitiativeStatus(
6877
+ renderBoxesAndLinesForExport(
6737
6878
  container,
6738
- isParsed,
6739
- isLayout,
6879
+ blParsed,
6880
+ blLayout,
6740
6881
  effectivePalette,
6741
6882
  theme === 'dark',
6742
6883
  { exportDims: { width: exportWidth, height: exportHeight } }
@@ -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 ? ['40%', '70%'] : ['0%', '70%'],
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: ['10%', '70%'],
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,