@diagrammo/dgmo 0.8.21 → 0.8.23

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 (114) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +1 -0
  3. package/dist/cli.cjs +145 -93
  4. package/dist/editor.cjs +20 -3
  5. package/dist/editor.cjs.map +1 -1
  6. package/dist/editor.js +20 -3
  7. package/dist/editor.js.map +1 -1
  8. package/dist/highlight.cjs +15 -2
  9. package/dist/highlight.cjs.map +1 -1
  10. package/dist/highlight.js +15 -2
  11. package/dist/highlight.js.map +1 -1
  12. package/dist/index.cjs +20843 -14937
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +426 -17
  15. package/dist/index.d.ts +426 -17
  16. package/dist/index.js +20795 -14912
  17. package/dist/index.js.map +1 -1
  18. package/dist/internal.cjs +380 -0
  19. package/dist/internal.cjs.map +1 -0
  20. package/dist/internal.d.cts +179 -0
  21. package/dist/internal.d.ts +179 -0
  22. package/dist/internal.js +337 -0
  23. package/dist/internal.js.map +1 -0
  24. package/docs/guide/chart-cycle.md +156 -0
  25. package/docs/guide/chart-journey-map.md +179 -0
  26. package/docs/guide/chart-pyramid.md +111 -0
  27. package/docs/guide/chart-sitemap.md +18 -1
  28. package/docs/guide/chart-tech-radar.md +219 -0
  29. package/docs/guide/registry.json +6 -0
  30. package/docs/language-reference.md +177 -6
  31. package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
  32. package/gallery/fixtures/c4-full.dgmo +2 -2
  33. package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
  34. package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
  35. package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
  36. package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
  37. package/gallery/fixtures/gantt-full.dgmo +2 -2
  38. package/gallery/fixtures/gantt.dgmo +2 -2
  39. package/gallery/fixtures/infra-full.dgmo +2 -2
  40. package/gallery/fixtures/infra.dgmo +1 -1
  41. package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
  42. package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
  43. package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
  44. package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
  45. package/gallery/fixtures/sequence-tags.dgmo +2 -2
  46. package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
  47. package/gallery/fixtures/tech-radar.dgmo +36 -0
  48. package/gallery/fixtures/timeline.dgmo +1 -1
  49. package/package.json +11 -1
  50. package/src/boxes-and-lines/layout.ts +309 -33
  51. package/src/boxes-and-lines/parser.ts +86 -10
  52. package/src/boxes-and-lines/renderer.ts +250 -91
  53. package/src/boxes-and-lines/types.ts +1 -1
  54. package/src/c4/layout.ts +8 -8
  55. package/src/c4/parser.ts +35 -2
  56. package/src/c4/renderer.ts +19 -3
  57. package/src/c4/types.ts +1 -0
  58. package/src/chart.ts +14 -7
  59. package/src/cli.ts +5 -35
  60. package/src/completion.ts +233 -41
  61. package/src/cycle/layout.ts +723 -0
  62. package/src/cycle/parser.ts +352 -0
  63. package/src/cycle/renderer.ts +566 -0
  64. package/src/cycle/types.ts +98 -0
  65. package/src/d3.ts +107 -8
  66. package/src/dgmo-router.ts +82 -3
  67. package/src/echarts.ts +8 -5
  68. package/src/editor/dgmo.grammar +5 -1
  69. package/src/editor/dgmo.grammar.js +1 -1
  70. package/src/editor/keywords.ts +17 -0
  71. package/src/gantt/parser.ts +2 -8
  72. package/src/graph/flowchart-parser.ts +15 -21
  73. package/src/graph/state-parser.ts +5 -10
  74. package/src/index.ts +63 -2
  75. package/src/infra/layout.ts +218 -74
  76. package/src/infra/parser.ts +32 -8
  77. package/src/infra/renderer.ts +14 -8
  78. package/src/infra/types.ts +10 -3
  79. package/src/internal.ts +16 -0
  80. package/src/journey-map/layout.ts +386 -0
  81. package/src/journey-map/parser.ts +540 -0
  82. package/src/journey-map/renderer.ts +1521 -0
  83. package/src/journey-map/types.ts +47 -0
  84. package/src/kanban/parser.ts +3 -10
  85. package/src/kanban/renderer.ts +31 -15
  86. package/src/mindmap/parser.ts +12 -18
  87. package/src/mindmap/renderer.ts +14 -13
  88. package/src/mindmap/text-wrap.ts +22 -12
  89. package/src/mindmap/types.ts +2 -2
  90. package/src/org/collapse.ts +81 -0
  91. package/src/org/parser.ts +2 -6
  92. package/src/org/renderer.ts +212 -4
  93. package/src/pyramid/parser.ts +172 -0
  94. package/src/pyramid/renderer.ts +684 -0
  95. package/src/pyramid/types.ts +28 -0
  96. package/src/render.ts +2 -8
  97. package/src/sequence/parser.ts +62 -20
  98. package/src/sequence/renderer.ts +146 -40
  99. package/src/sharing.ts +1 -0
  100. package/src/sitemap/layout.ts +21 -6
  101. package/src/sitemap/parser.ts +26 -17
  102. package/src/sitemap/renderer.ts +34 -0
  103. package/src/sitemap/types.ts +1 -0
  104. package/src/tech-radar/index.ts +14 -0
  105. package/src/tech-radar/interactive.ts +1112 -0
  106. package/src/tech-radar/layout.ts +190 -0
  107. package/src/tech-radar/parser.ts +385 -0
  108. package/src/tech-radar/renderer.ts +1159 -0
  109. package/src/tech-radar/shared.ts +187 -0
  110. package/src/tech-radar/types.ts +81 -0
  111. package/src/utils/description-helpers.ts +33 -0
  112. package/src/utils/legend-layout.ts +3 -1
  113. package/src/utils/parsing.ts +47 -7
  114. package/src/utils/tag-groups.ts +46 -60
package/src/d3.ts CHANGED
@@ -19,7 +19,10 @@ export type VisualizationType =
19
19
  | 'timeline'
20
20
  | 'venn'
21
21
  | 'quadrant'
22
- | 'sequence';
22
+ | 'sequence'
23
+ | 'tech-radar'
24
+ | 'cycle'
25
+ | 'pyramid';
23
26
 
24
27
  interface D3DataItem {
25
28
  label: string;
@@ -184,6 +187,7 @@ import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
184
187
  import {
185
188
  collectIndentedValues,
186
189
  extractColor,
190
+ normalizeNumericToken,
187
191
  parseFirstLine,
188
192
  parsePipeMetadata,
189
193
  MULTIPLE_PIPE_ERROR,
@@ -632,7 +636,7 @@ export function parseVisualization(
632
636
  // Arc link line: source -> target(color) weight
633
637
  if (result.type === 'arc') {
634
638
  const linkMatch = line.match(
635
- /^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(?:\s+(\d+(?:\.\d+)?))?$/
639
+ /^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(?:\s+(-?[\d,_]+(?:\.[\d]+)?))?$/
636
640
  );
637
641
  if (linkMatch) {
638
642
  const source = linkMatch[1].trim();
@@ -648,7 +652,9 @@ export function parseVisualization(
648
652
  result.links.push({
649
653
  source,
650
654
  target,
651
- value: linkMatch[4] ? parseFloat(linkMatch[4]) : 1,
655
+ value: linkMatch[4]
656
+ ? parseFloat(normalizeNumericToken(linkMatch[4]) ?? linkMatch[4])
657
+ : 1,
652
658
  color: linkColor,
653
659
  lineNumber,
654
660
  });
@@ -1028,7 +1034,7 @@ export function parseVisualization(
1028
1034
 
1029
1035
  // Data points: Label x, y OR Label x y
1030
1036
  const pointMatch = line.match(
1031
- /^(.+?)\s+([0-9]*\.?[0-9]+)\s*[,\s]\s*([0-9]*\.?[0-9]+)\s*$/
1037
+ /^(.+?)\s+(-?[0-9][0-9,_]*(?:\.[0-9]+)?)\s*[,\s]\s*(-?[0-9][0-9,_]*(?:\.[0-9]+)?)\s*$/
1032
1038
  );
1033
1039
  if (pointMatch) {
1034
1040
  const label = pointMatch[1].trim();
@@ -1042,8 +1048,12 @@ export function parseVisualization(
1042
1048
  ) {
1043
1049
  result.quadrantPoints.push({
1044
1050
  label,
1045
- x: parseFloat(pointMatch[2]),
1046
- y: parseFloat(pointMatch[3]),
1051
+ x: parseFloat(
1052
+ normalizeNumericToken(pointMatch[2]) ?? pointMatch[2]
1053
+ ),
1054
+ y: parseFloat(
1055
+ normalizeNumericToken(pointMatch[3]) ?? pointMatch[3]
1056
+ ),
1047
1057
  lineNumber,
1048
1058
  });
1049
1059
  }
@@ -1201,7 +1211,8 @@ export function parseVisualization(
1201
1211
  // Scan from right, capped at P values
1202
1212
  let rightIdx = tokens.length - 1;
1203
1213
  while (rightIdx >= 0 && values.length < P) {
1204
- const raw = tokens[rightIdx].replace(/,/g, '');
1214
+ const raw =
1215
+ normalizeNumericToken(tokens[rightIdx]) ?? tokens[rightIdx];
1205
1216
  const num = parseFloat(raw);
1206
1217
  if (!isNaN(num) && /^-?\d/.test(raw)) {
1207
1218
  values.unshift(num);
@@ -1385,8 +1396,11 @@ export function parseVisualization(
1385
1396
  } else if (colonIndex === -1) {
1386
1397
  // Try "word weight" or "multi-word-label weight" space-separated format
1387
1398
  const lastSpace = line.lastIndexOf(' ');
1399
+ const rawWeight = lastSpace >= 0 ? line.substring(lastSpace + 1) : '';
1388
1400
  const maybeWeight =
1389
- lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
1401
+ lastSpace >= 0
1402
+ ? parseFloat(normalizeNumericToken(rawWeight) ?? rawWeight)
1403
+ : NaN;
1390
1404
  if (lastSpace >= 0 && !isNaN(maybeWeight) && maybeWeight > 0) {
1391
1405
  result.words.push({
1392
1406
  text: line.substring(0, lastSpace).trim(),
@@ -7041,6 +7055,91 @@ export async function renderForExport(
7041
7055
  return finalizeSvgExport(container, theme, effectivePalette);
7042
7056
  }
7043
7057
 
7058
+ if (detectedType === 'tech-radar') {
7059
+ const { parseTechRadar } = await import('./tech-radar/parser');
7060
+ const { renderTechRadarForExport } = await import('./tech-radar/renderer');
7061
+
7062
+ const effectivePalette = await resolveExportPalette(theme, palette);
7063
+ const radarParsed = parseTechRadar(content);
7064
+ if (radarParsed.error || radarParsed.quadrants.length === 0) return '';
7065
+
7066
+ const RADAR_EXPORT_W = 1600;
7067
+ const RADAR_EXPORT_H = 1200;
7068
+ const container = createExportContainer(RADAR_EXPORT_W, RADAR_EXPORT_H);
7069
+ renderTechRadarForExport(
7070
+ container,
7071
+ radarParsed,
7072
+ effectivePalette,
7073
+ theme === 'dark',
7074
+ { width: RADAR_EXPORT_W, height: RADAR_EXPORT_H },
7075
+ viewState
7076
+ );
7077
+ return finalizeSvgExport(container, theme, effectivePalette);
7078
+ }
7079
+
7080
+ if (detectedType === 'journey-map') {
7081
+ const { parseJourneyMap } = await import('./journey-map/parser');
7082
+ const { renderJourneyMap } = await import('./journey-map/renderer');
7083
+ const { layoutJourneyMap } = await import('./journey-map/layout');
7084
+
7085
+ const effectivePalette = await resolveExportPalette(theme, palette);
7086
+ const jmParsed = parseJourneyMap(content, effectivePalette);
7087
+ if (
7088
+ jmParsed.error ||
7089
+ (jmParsed.phases.length === 0 && jmParsed.steps.length === 0)
7090
+ )
7091
+ return '';
7092
+
7093
+ const jmLayout = layoutJourneyMap(jmParsed, effectivePalette);
7094
+ const container = createExportContainer(
7095
+ jmLayout.totalWidth,
7096
+ jmLayout.totalHeight
7097
+ );
7098
+ renderJourneyMap(container, jmParsed, effectivePalette, theme === 'dark', {
7099
+ exportDims: { width: jmLayout.totalWidth, height: jmLayout.totalHeight },
7100
+ });
7101
+ return finalizeSvgExport(container, theme, effectivePalette);
7102
+ }
7103
+
7104
+ if (detectedType === 'cycle') {
7105
+ const { parseCycle } = await import('./cycle/parser');
7106
+ const { renderCycleForExport } = await import('./cycle/renderer');
7107
+
7108
+ const effectivePalette = await resolveExportPalette(theme, palette);
7109
+ const cycleParsed = parseCycle(content);
7110
+ if (cycleParsed.error || cycleParsed.nodes.length === 0) return '';
7111
+
7112
+ const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
7113
+ renderCycleForExport(
7114
+ container,
7115
+ cycleParsed,
7116
+ effectivePalette,
7117
+ theme === 'dark',
7118
+ { width: EXPORT_WIDTH, height: EXPORT_HEIGHT },
7119
+ viewState
7120
+ );
7121
+ return finalizeSvgExport(container, theme, effectivePalette);
7122
+ }
7123
+
7124
+ if (detectedType === 'pyramid') {
7125
+ const { parsePyramid } = await import('./pyramid/parser');
7126
+ const { renderPyramidForExport } = await import('./pyramid/renderer');
7127
+
7128
+ const effectivePalette = await resolveExportPalette(theme, palette);
7129
+ const pyramidParsed = parsePyramid(content);
7130
+ if (pyramidParsed.error || pyramidParsed.layers.length === 0) return '';
7131
+
7132
+ const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
7133
+ renderPyramidForExport(
7134
+ container,
7135
+ pyramidParsed,
7136
+ effectivePalette,
7137
+ theme === 'dark',
7138
+ { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
7139
+ );
7140
+ return finalizeSvgExport(container, theme, effectivePalette);
7141
+ }
7142
+
7044
7143
  const parsed = parseVisualization(content, palette);
7045
7144
  // Allow sequence diagrams through even if parseVisualization errors —
7046
7145
  // sequence is parsed by its own dedicated parser (parseSequenceDgmo)
@@ -19,6 +19,10 @@ import { parseGantt } from './gantt/parser';
19
19
  import { parseBoxesAndLines } from './boxes-and-lines/parser';
20
20
  import { parseMindmap } from './mindmap/parser';
21
21
  import { parseWireframe } from './wireframe/parser';
22
+ import { parseTechRadar } from './tech-radar/parser';
23
+ import { parseCycle } from './cycle/parser';
24
+ import { parseJourneyMap } from './journey-map/parser';
25
+ import { parsePyramid } from './pyramid/parser';
22
26
  import { parseFirstLine } from './utils/parsing';
23
27
  import { makeDgmoError, suggest } from './diagnostics';
24
28
  import type { DgmoError } from './diagnostics';
@@ -137,6 +141,9 @@ const VISUALIZATION_TYPES = new Set([
137
141
  'timeline',
138
142
  'venn',
139
143
  'quadrant',
144
+ 'tech-radar',
145
+ 'cycle',
146
+ 'pyramid',
140
147
  ]);
141
148
  const DIAGRAM_TYPES = new Set([
142
149
  'sequence',
@@ -153,6 +160,7 @@ const DIAGRAM_TYPES = new Set([
153
160
  'boxes-and-lines',
154
161
  'mindmap',
155
162
  'wireframe',
163
+ 'journey-map',
156
164
  ]);
157
165
  const EXTENDED_CHART_TYPES = new Set([
158
166
  'scatter',
@@ -205,6 +213,63 @@ export function getAllChartTypes(): string[] {
205
213
  return [...DATA_CHART_TYPES, ...VISUALIZATION_TYPES, ...DIAGRAM_TYPES];
206
214
  }
207
215
 
216
+ /**
217
+ * Canonical descriptions for every supported chart type. Shared by the CLI
218
+ * `--chart-types` flag, the editor autocomplete popup, and the MCP
219
+ * `list_chart_types` tool so all three surfaces stay in sync.
220
+ */
221
+ export const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
222
+ bar: 'Bar chart — categorical comparisons',
223
+ line: 'Line chart — trends over time; supports era bands (era start -> end Label (color)) for annotating named periods',
224
+ 'multi-line':
225
+ 'Multi-line chart — multiple series trends over time; supports era bands',
226
+ area: 'Area chart — filled line chart; supports era bands',
227
+ pie: 'Pie chart — part-to-whole proportions',
228
+ doughnut: 'Doughnut chart — ring-style pie chart',
229
+ radar: 'Radar chart — multi-dimensional metrics',
230
+ 'polar-area': 'Polar area chart — radial bar chart',
231
+ 'bar-stacked': 'Stacked bar chart — multi-series categorical',
232
+ scatter: 'Scatter plot — 2D data points or bubble chart',
233
+ sankey: 'Sankey diagram — flow/allocation visualization',
234
+ chord: 'Chord diagram — circular flow relationships',
235
+ function: 'Function plot — mathematical expressions',
236
+ heatmap: 'Heatmap — matrix intensity visualization',
237
+ funnel: 'Funnel chart — conversion pipeline',
238
+ slope: 'Slope chart — change between two periods',
239
+ wordcloud: 'Word cloud — term frequency visualization',
240
+ arc: 'Arc diagram — network relationships',
241
+ timeline: 'Timeline — events, eras, and date ranges',
242
+ venn: 'Venn diagram — set overlaps',
243
+ quadrant: 'Quadrant chart — 2x2 positioning matrix',
244
+ 'tech-radar':
245
+ 'Tech radar — technology adoption quadrants (adopt/trial/assess/hold)',
246
+ cycle:
247
+ 'Cycle diagram — cyclical process visualization (PDCA, OODA, DevOps loops)',
248
+ sequence: 'Sequence diagram — message/interaction flows',
249
+ flowchart: 'Flowchart — decision trees and process flows',
250
+ class: 'Class diagram — UML class hierarchies',
251
+ er: 'ER diagram — database schemas and relationships',
252
+ org: 'Org chart — hierarchical tree structures',
253
+ kanban: 'Kanban board — task/workflow columns',
254
+ c4: 'C4 diagram — system architecture (context, container, component, deployment)',
255
+ state: 'State diagram — state machine / lifecycle transitions',
256
+ sitemap:
257
+ 'Sitemap — navigable UI structure with pages, groups, and cross-link arrows',
258
+ infra:
259
+ 'Infrastructure diagram — traffic flow with RPS computation, capacity modeling, and latency analysis',
260
+ gantt:
261
+ 'Gantt chart — project scheduling with task dependencies and milestones',
262
+ 'boxes-and-lines':
263
+ 'Boxes and lines — general-purpose node-edge diagrams with nested groups, tags, and shape inference',
264
+ mindmap: 'Mindmap — radial hierarchy of ideas branching from a central topic',
265
+ wireframe:
266
+ 'Wireframe — low-fidelity UI layout with panels, controls, and annotations',
267
+ 'journey-map':
268
+ 'Journey map — user experience flow with emotion scores, phases, and annotations',
269
+ pyramid:
270
+ 'Pyramid — hierarchical layered pyramid (Maslow, DIKW, learning pyramid); inverted for funnel-of-learning style',
271
+ };
272
+
208
273
  // ECharts-native types parsed by parseExtendedChart
209
274
  const ECHART_TYPES = new Set([
210
275
  'scatter',
@@ -234,6 +299,10 @@ const PARSE_DISPATCH = new Map<
234
299
  ['boxes-and-lines', (c) => parseBoxesAndLines(c)],
235
300
  ['mindmap', (c) => parseMindmap(c)],
236
301
  ['wireframe', (c) => parseWireframe(c)],
302
+ ['tech-radar', (c) => parseTechRadar(c)],
303
+ ['cycle', (c) => parseCycle(c)],
304
+ ['journey-map', (c) => parseJourneyMap(c)],
305
+ ['pyramid', (c) => parsePyramid(c)],
237
306
  ]);
238
307
 
239
308
  /**
@@ -251,7 +320,10 @@ const ALL_KNOWN_TYPES = new Set([
251
320
  * Parse DGMO content and return diagnostics without rendering.
252
321
  * Useful for the CLI and editor to surface all errors before attempting render.
253
322
  */
254
- export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
323
+ export function parseDgmo(content: string): {
324
+ diagnostics: DgmoError[];
325
+ chartType: string | null;
326
+ } {
255
327
  const chartType = parseDgmoChartType(content);
256
328
 
257
329
  if (!chartType) {
@@ -259,11 +331,14 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
259
331
  const colonDiag = detectColonChartType(content);
260
332
  if (colonDiag) {
261
333
  const fallback = parseVisualization(content).diagnostics;
262
- return { diagnostics: [colonDiag, ...fallback] };
334
+ return { diagnostics: [colonDiag, ...fallback], chartType: null };
263
335
  }
264
336
 
265
337
  // No chart type detected — try visualization parser as fallback
266
- return { diagnostics: parseVisualization(content).diagnostics };
338
+ return {
339
+ diagnostics: parseVisualization(content).diagnostics,
340
+ chartType: null,
341
+ };
267
342
  }
268
343
 
269
344
  const directParser = PARSE_DISPATCH.get(chartType);
@@ -271,6 +346,7 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
271
346
  const result = directParser(content);
272
347
  return {
273
348
  diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
349
+ chartType,
274
350
  };
275
351
  }
276
352
 
@@ -278,12 +354,14 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
278
354
  const result = parseChart(content);
279
355
  return {
280
356
  diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
357
+ chartType,
281
358
  };
282
359
  }
283
360
  if (ECHART_TYPES.has(chartType)) {
284
361
  const result = parseExtendedChart(content);
285
362
  return {
286
363
  diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
364
+ chartType,
287
365
  };
288
366
  }
289
367
 
@@ -291,6 +369,7 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
291
369
  const result = parseVisualization(content);
292
370
  return {
293
371
  diagnostics: [...result.diagnostics, ...detectEmptyContent(content)],
372
+ chartType,
294
373
  };
295
374
  }
296
375
 
package/src/echarts.ts CHANGED
@@ -150,6 +150,7 @@ import {
150
150
  collectIndentedValues,
151
151
  extractColor,
152
152
  measureIndent,
153
+ normalizeNumericToken,
153
154
  parseFirstLine,
154
155
  parseSeriesNames,
155
156
  } from './utils/parsing';
@@ -359,10 +360,11 @@ export function parseExtendedChart(
359
360
 
360
361
  // Sankey/chord link syntax: Source -> Target Value (directed) or Source -- Target Value (undirected)
361
362
  const arrowMatch = trimmed.match(
362
- /^(.+?)\s*(->|--)\s*(.+?)\s+(\d+(?:\.\d+)?)\s*(?:\(([^)]+)\))?\s*$/
363
+ /^(.+?)\s*(->|--)\s*(.+?)\s+(-?[\d,_]+(?:\.[\d]+)?)\s*(?:\(([^)]+)\))?\s*$/
363
364
  );
364
365
  if (arrowMatch) {
365
- const [, rawSource, arrow, rawTarget, val, rawLinkColor] = arrowMatch;
366
+ const [, rawSource, arrow, rawTarget, rawVal, rawLinkColor] = arrowMatch;
367
+ const val = normalizeNumericToken(rawVal) ?? rawVal;
366
368
  const { label: source, color: sourceColor } = extractColor(
367
369
  rawSource.trim(),
368
370
  palette
@@ -409,7 +411,7 @@ export function parseExtendedChart(
409
411
  // Parse "TargetName value (linkColor)" or "TargetName(nodeColor) value (linkColor)"
410
412
  // Strip trailing (color) annotation before parseDataRowValues — it can't handle it
411
413
  const valColorMatch = trimmed.match(
412
- /(\d+(?:\.\d+)?)\s*\(([^)]+)\)\s*$/
414
+ /(-?[\d,_]+(?:\.[\d]+)?)\s*\(([^)]+)\)\s*$/
413
415
  );
414
416
  const strippedLine = valColorMatch
415
417
  ? trimmed.replace(/\s*\([^)]+\)\s*$/, '')
@@ -449,9 +451,10 @@ export function parseExtendedChart(
449
451
 
450
452
  // Bare label at indent 0 (or any indent without a value) = new source node
451
453
  const spaceIdx = trimmed.indexOf(' ');
454
+ const lastTok = trimmed.substring(trimmed.lastIndexOf(' ') + 1);
452
455
  const hasNumericSuffix =
453
456
  spaceIdx >= 0 &&
454
- !isNaN(parseFloat(trimmed.substring(trimmed.lastIndexOf(' ') + 1)));
457
+ !isNaN(parseFloat(normalizeNumericToken(lastTok) ?? lastTok));
455
458
  if (!hasNumericSuffix) {
456
459
  while (sankeyStack.length && sankeyStack.at(-1)!.indent >= indent) {
457
460
  sankeyStack.pop();
@@ -1910,7 +1913,7 @@ function buildFunnelOption(
1910
1913
  bottom: 20,
1911
1914
  width: '60%',
1912
1915
  sort: 'descending' as const,
1913
- gap: 2,
1916
+ gap: 0,
1914
1917
  minSize: '8%',
1915
1918
  };
1916
1919
 
@@ -30,7 +30,11 @@ contentPart {
30
30
  Duration { $[0-9]+ ("." $[0-9]+)? ("min" | "bd" | "h" | "d" | "w" | "m" | "q" | "y" | "s") "?"? }
31
31
  DateLiteral { $[0-9] $[0-9] $[0-9] $[0-9] "-" $[0-9] $[0-9] ("-" $[0-9] $[0-9])? }
32
32
  Percentage { $[0-9]+ ("." $[0-9]+)? "%" }
33
- Number { $[0-9]+ ("." $[0-9]+)? }
33
+ Number {
34
+ $[0-9] $[0-9]? $[0-9]? ("," $[0-9] $[0-9] $[0-9])+ ("." $[0-9]+)? |
35
+ $[0-9]+ ("_" $[0-9]+)+ ("." $[0-9]+)? |
36
+ $[0-9]+ ("." $[0-9]+)?
37
+ }
34
38
 
35
39
  SectionMarker { "==" }
36
40
  Url { "http" "s"? "://" ![ \t\n|,)\]>]+ }
@@ -10,7 +10,7 @@ export const parser = LRParser.deserialize({
10
10
  maxTerm: 40,
11
11
  skippedNodes: [0],
12
12
  repeatNodeCount: 2,
13
- tokenData: ":T~RxOX#oXY#tYZ$PZp#opq#tqt#oux#oxy$Uyz$tz{${{|%S|}%Z}!O%b!O!P#o!P!Q%q!Q![&b![!]-Y!]!^#o!^!_-a!_!`-h!`!a-u!a!b-|!b!c#o!c!}.T!}#O1Z#O#P#o#P#Q1`#Q#R#o#R#S.T#S#T#o#T#[.T#[#]1g#]#o.T#o#p#o#p#q9g#q#r#o#r#s9n#s;'S#o;'S;=`9}<%lO#o~#tOq~~#yQv~XY#tpq#t~$UOw~~$]Qc~q~}!O$c#T#o$c~$fRyz$o}!O$c#T#o$c~$tOg~~${Od~q~~%SOn~q~~%ZOk~q~~%bOj~q~~%iPl~q~!`!a%l~%qOX~~%vPq~!P!Q%y~&OSV~OY%yZ;'S%y;'S;=`&[<%lO%y~&_P;=`<%l%y~&iZ^~q~uv'[!O!P'a!Q![)Q#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~'aO]~~'dP!Q!['g~'lY^~uv'[!Q!['g#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~(_P#W#X(b~(gPZ~!a!b(j~(oOZ~~(tQZ~!a!b(j#]#^(z~(}P#b#c(b~)VZ^~uv'[!O!P'a!Q![)x#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~)}Z^~uv'[!O!P'a!Q![*p#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~*u[^~uv'[}!O+k!O!P'a!Q![,b#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~+nP!Q![+q~+tP!Q![+w~+|P[~}!O,P~,SP!Q![,V~,YP!Q![,]~,bO[~~,gZ^~uv'[!O!P'a!Q![,b#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~-aOi~q~~-hOe~q~~-mPq~!_!`-p~-uO_~~-|Of~q~~.TOo~q~~.[_p~q~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#o/Z~/`_p~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#o/Z~0b]qr/Zst/Zvw/Zwx/Z{|/Z!O!P/Z!P!Q/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#o/Z~1`Oa~~1gOb~q~~1nap~q~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#h/Z#h#i2s#i#o/Z~2xap~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#h/Z#h#i3}#i#o/Z~4Sap~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#d/Z#d#e5X#e#o/Z~5^bp~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z![!]6f!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#g/Z#g#h8`#h#o/Z~6iP!P!Q6l~6oP!P!Q6r~6uYOX7eZp7eqy7ez|7e}!`7e!a#P7e#Q#p7e#q;'S7e;'S;=`8Y<%lO7e~7jY`~OX7eZp7eqy7ez|7e}!`7e!a#P7e#Q#p7e#q;'S7e;'S;=`8Y<%lO7e~8]P;=`<%l7e~8e`p~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z![!]6f!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#o/Z~9nOh~q~~9uPm~q~!`!a9x~9}OY~~:QP;=`<%l#o",
13
+ tokenData: "<O~RxOX#oXY#tYZ$PZp#opq#tqt#oux#oxy$Uyz$tz{${{|%S|}%Z}!O%b!O!P#o!P!Q%q!Q![&b![!]/T!]!^#o!^!_/[!_!`/c!`!a/p!a!b/w!b!c#o!c!}0O!}#O3U#O#P#o#P#Q3Z#Q#R#o#R#S0O#S#T#o#T#[0O#[#]3b#]#o0O#o#p#o#p#q;b#q#r#o#r#s;i#s;'S#o;'S;=`;x<%lO#o~#tOq~~#yQv~XY#tpq#t~$UOw~~$]Qc~q~}!O$c#T#o$c~$fRyz$o}!O$c#T#o$c~$tOg~~${Od~q~~%SOn~q~~%ZOk~q~~%bOj~q~~%iPl~q~!`!a%l~%qOX~~%vPq~!P!Q%y~&OSV~OY%yZ;'S%y;'S;=`&[<%lO%y~&_P;=`<%l%y~&i]^~q~uv'b|}'g!O!P(d!Q![*T#R#S.o#U#V)_#W#X)e#[#])e#a#b)r#e#f)e#g#h)e#k#l)e#m#n)e~'gO]~~'jP!Q!['m~'pP!Q!['s~'vP!Q!['y~(OQ^~|}'g!O!P(U~(XP!Q![([~(aP^~!Q![([~(gP!Q![(j~(oY^~uv'b!Q![(j#U#V)_#W#X)e#[#])e#a#b)r#e#f)e#g#h)e#k#l)e#m#n)e~)bP#W#X)e~)jPZ~!a!b)m~)rOZ~~)wQZ~!a!b)m#]#^)}~*QP#b#c)e~*Y]^~uv'b|}'g!O!P(d!Q![+R#R#S.o#U#V)_#W#X)e#[#])e#a#b)r#e#f)e#g#h)e#k#l)e#m#n)e~+W]^~uv'b|}'g!O!P(d!Q![,P#R#S.o#U#V)_#W#X)e#[#])e#a#b)r#e#f)e#g#h)e#k#l)e#m#n)e~,U]^~uv'b}!O,}!O!P(d!Q![-t#R#S.o#U#V)_#W#X)e#[#])e#a#b)r#e#f)e#g#h)e#k#l)e#m#n)e~-QP!Q![-T~-WP!Q![-Z~-`P[~}!O-c~-fP!Q![-i~-lP!Q![-o~-tO[~~-y[^~uv'b!O!P(d!Q![-t#R#S.o#U#V)_#W#X)e#[#])e#a#b)r#e#f)e#g#h)e#k#l)e#m#n)e~.rP!Q![.u~.zR^~!O!P(U!Q![.u#R#S.o~/[Oi~q~~/cOe~q~~/hPq~!_!`/k~/pO_~~/wOf~q~~0OOo~q~~0V_p~q~qr1Ust1Uvw1Uwx1U{|1U}!O2Y!O!P1U!P!Q1U!Q![1U!_!`1U!a!b1U!b!c1U!c!}1U#R#S1U#T#o1U~1Z_p~qr1Ust1Uvw1Uwx1U{|1U}!O2Y!O!P1U!P!Q1U!Q![1U!_!`1U!a!b1U!b!c1U!c!}1U#R#S1U#T#o1U~2]]qr1Ust1Uvw1Uwx1U{|1U!O!P1U!P!Q1U!_!`1U!a!b1U!b!c1U!c!}1U#R#S1U#T#o1U~3ZOa~~3bOb~q~~3iap~q~qr1Ust1Uvw1Uwx1U{|1U}!O2Y!O!P1U!P!Q1U!Q![1U!_!`1U!a!b1U!b!c1U!c!}1U#R#S1U#T#h1U#h#i4n#i#o1U~4sap~qr1Ust1Uvw1Uwx1U{|1U}!O2Y!O!P1U!P!Q1U!Q![1U!_!`1U!a!b1U!b!c1U!c!}1U#R#S1U#T#h1U#h#i5x#i#o1U~5}ap~qr1Ust1Uvw1Uwx1U{|1U}!O2Y!O!P1U!P!Q1U!Q![1U!_!`1U!a!b1U!b!c1U!c!}1U#R#S1U#T#d1U#d#e7S#e#o1U~7Xbp~qr1Ust1Uvw1Uwx1U{|1U}!O2Y!O!P1U!P!Q1U!Q![1U![!]8a!_!`1U!a!b1U!b!c1U!c!}1U#R#S1U#T#g1U#g#h:Z#h#o1U~8dP!P!Q8g~8jP!P!Q8m~8pYOX9`Zp9`qy9`z|9`}!`9`!a#P9`#Q#p9`#q;'S9`;'S;=`:T<%lO9`~9eY`~OX9`Zp9`qy9`z|9`}!`9`!a#P9`#Q#p9`#q;'S9`;'S;=`:T<%lO9`~:WP;=`<%l9`~:``p~qr1Ust1Uvw1Uwx1U{|1U}!O2Y!O!P1U!P!Q1U!Q![1U![!]8a!_!`1U!a!b1U!b!c1U!c!}1U#R#S1U#T#o1U~;iOh~q~~;pPm~q~!`!a;s~;xOY~~;{P;=`<%l#o",
14
14
  tokenizers: [0],
15
15
  topRules: {"Document":[0,6]},
16
16
  specialized: [{term: 32, get: (value, stack) => (specializeKeyword(value, stack) << 1), external: specializeKeyword}],
@@ -14,6 +14,10 @@ export const CHART_TYPES = new Set([
14
14
  'gantt',
15
15
  'boxes-and-lines',
16
16
  'wireframe',
17
+ 'tech-radar',
18
+ 'mindmap',
19
+ 'journey-map',
20
+ 'pyramid',
17
21
  // Data chart types
18
22
  'bar',
19
23
  'line',
@@ -65,6 +69,10 @@ export const METADATA_KEYS = new Set([
65
69
  'top-left',
66
70
  'bottom-right',
67
71
  'bottom-left',
72
+ // Tech-radar pipe metadata
73
+ 'quadrant',
74
+ 'ring',
75
+ 'trend',
68
76
  ]);
69
77
 
70
78
  /** Tag declaration keyword. */
@@ -82,6 +90,8 @@ export const DIRECTIVE_KEYWORDS = new Set([
82
90
  'critical-path',
83
91
  'no-dependencies',
84
92
  'sort',
93
+ // Tech-radar
94
+ 'rings',
85
95
  // Tags
86
96
  'tags',
87
97
  'import',
@@ -149,6 +159,8 @@ export const DIRECTIVE_KEYWORDS = new Set([
149
159
  // Layout
150
160
  'direction-tb',
151
161
  'direction-lr',
162
+ // Pyramid
163
+ 'inverted',
152
164
  // Data chart metadata
153
165
  'title',
154
166
  'series',
@@ -194,6 +206,11 @@ export const STATUS_KEYWORDS = new Set([
194
206
  'in-progress',
195
207
  'backlog',
196
208
  'ready',
209
+ // Tech-radar trend values
210
+ 'new',
211
+ 'up',
212
+ 'down',
213
+ 'stable',
197
214
  ]);
198
215
 
199
216
  /** Modifier keywords — adjust declarations. */
@@ -744,7 +744,7 @@ export function parseGantt(
744
744
 
745
745
  // First segment could be empty (just `[Group]`) or have metadata
746
746
  let metadata: Record<string, string> = {};
747
- let color: string | null = null;
747
+ const color: string | null = null;
748
748
 
749
749
  const pipeWarn = () => warn(lineNumber, MULTIPLE_PIPE_ERROR);
750
750
  if (segments.length > 0 && segments[0].trim()) {
@@ -758,14 +758,8 @@ export function parseGantt(
758
758
  );
759
759
  }
760
760
 
761
- // Extract color from group name if present
762
- const nameExtracted = extractColor(groupMatch[1], palette);
763
- if (nameExtracted.color) {
764
- color = nameExtracted.color;
765
- }
766
-
767
761
  const group: GanttGroup = {
768
- name: nameExtracted.label,
762
+ name: groupMatch[1],
769
763
  color,
770
764
  metadata,
771
765
  lineNumber,
@@ -5,7 +5,6 @@ import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
5
5
  import { parseInArrowLabel, matchColorParens } from '../utils/arrows';
6
6
  import {
7
7
  measureIndent,
8
- extractColor,
9
8
  inferArrowColor,
10
9
  parseFirstLine,
11
10
  OPTION_NOCOLON_RE,
@@ -32,55 +31,50 @@ interface NodeRef {
32
31
  * Try to parse a node reference from a text fragment.
33
32
  * Order matters: subroutine & document before process.
34
33
  */
35
- function parseNodeRef(text: string, palette?: PaletteColors): NodeRef | null {
34
+ function parseNodeRef(text: string): NodeRef | null {
36
35
  const t = text.trim();
37
36
  if (!t) return null;
38
37
 
39
38
  // Subroutine: [[Label]]
40
39
  let m = t.match(/^\[\[([^\]]+)\]\]$/);
41
40
  if (m) {
42
- const { label, color } = extractColor(m[1].trim(), palette);
43
- return {
44
- id: nodeId('subroutine', label),
45
- label,
46
- shape: 'subroutine',
47
- color,
48
- };
41
+ const label = m[1].trim();
42
+ return { id: nodeId('subroutine', label), label, shape: 'subroutine' };
49
43
  }
50
44
 
51
45
  // Document: [Label~]
52
46
  m = t.match(/^\[([^\]]+)~\]$/);
53
47
  if (m) {
54
- const { label, color } = extractColor(m[1].trim(), palette);
55
- return { id: nodeId('document', label), label, shape: 'document', color };
48
+ const label = m[1].trim();
49
+ return { id: nodeId('document', label), label, shape: 'document' };
56
50
  }
57
51
 
58
52
  // Process: [Label]
59
53
  m = t.match(/^\[([^\]]+)\]$/);
60
54
  if (m) {
61
- const { label, color } = extractColor(m[1].trim(), palette);
62
- return { id: nodeId('process', label), label, shape: 'process', color };
55
+ const label = m[1].trim();
56
+ return { id: nodeId('process', label), label, shape: 'process' };
63
57
  }
64
58
 
65
59
  // Terminal: (Label) — use .+ (greedy) so (Label(color)) matches outermost parens
66
60
  m = t.match(/^\((.+)\)$/);
67
61
  if (m) {
68
- const { label, color } = extractColor(m[1].trim(), palette);
69
- return { id: nodeId('terminal', label), label, shape: 'terminal', color };
62
+ const label = m[1].trim();
63
+ return { id: nodeId('terminal', label), label, shape: 'terminal' };
70
64
  }
71
65
 
72
66
  // Decision: <Label>
73
67
  m = t.match(/^<([^>]+)>$/);
74
68
  if (m) {
75
- const { label, color } = extractColor(m[1].trim(), palette);
76
- return { id: nodeId('decision', label), label, shape: 'decision', color };
69
+ const label = m[1].trim();
70
+ return { id: nodeId('decision', label), label, shape: 'decision' };
77
71
  }
78
72
 
79
73
  // I/O: /Label/
80
74
  m = t.match(/^\/([^/]+)\/$/);
81
75
  if (m) {
82
- const { label, color } = extractColor(m[1].trim(), palette);
83
- return { id: nodeId('io', label), label, shape: 'io', color };
76
+ const label = m[1].trim();
77
+ return { id: nodeId('io', label), label, shape: 'io' };
84
78
  }
85
79
 
86
80
  return null;
@@ -370,7 +364,7 @@ export function parseFlowchart(
370
364
 
371
365
  if (segments.length === 1) {
372
366
  // Single node reference, no arrows
373
- const ref = parseNodeRef(segments[0], palette);
367
+ const ref = parseNodeRef(segments[0]);
374
368
  if (ref) {
375
369
  const node = getOrCreateNode(ref, lineNumber);
376
370
  indentStack.push({ nodeId: node.id, indent });
@@ -398,7 +392,7 @@ export function parseFlowchart(
398
392
  }
399
393
 
400
394
  // This is a node text segment
401
- const ref = parseNodeRef(seg, palette);
395
+ const ref = parseNodeRef(seg);
402
396
  if (!ref) continue;
403
397
 
404
398
  const node = getOrCreateNode(ref, lineNumber);
@@ -5,7 +5,6 @@ import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
5
5
  import { parseInArrowLabel, matchColorParens } from '../utils/arrows';
6
6
  import {
7
7
  measureIndent,
8
- extractColor,
9
8
  parseFirstLine,
10
9
  OPTION_NOCOLON_RE,
11
10
  ALL_CHART_TYPES,
@@ -173,10 +172,7 @@ interface NodeRef {
173
172
  color?: string;
174
173
  }
175
174
 
176
- function parseStateNodeRef(
177
- text: string,
178
- palette?: PaletteColors
179
- ): NodeRef | null {
175
+ function parseStateNodeRef(text: string): NodeRef | null {
180
176
  const t = text.trim();
181
177
  if (!t) return null;
182
178
 
@@ -189,14 +185,13 @@ function parseStateNodeRef(
189
185
  };
190
186
  }
191
187
 
192
- // State: bare text with optional (color) suffix
193
- const { label, color } = extractColor(t, palette);
188
+ // State: bare text
189
+ const label = t;
194
190
  if (!label) return null;
195
191
  return {
196
192
  id: `state:${label.toLowerCase().trim()}`,
197
193
  label,
198
194
  shape: 'state',
199
- color,
200
195
  };
201
196
  }
202
197
 
@@ -380,7 +375,7 @@ export function parseState(
380
375
 
381
376
  if (segments.length === 1) {
382
377
  // Single state reference, no arrows — this is the canonical definition
383
- const ref = parseStateNodeRef(segments[0], palette);
378
+ const ref = parseStateNodeRef(segments[0]);
384
379
  if (ref) {
385
380
  const node = getOrCreateNode(ref, lineNumber);
386
381
  // Standalone heading is the "definition" — update lineNumber so
@@ -409,7 +404,7 @@ export function parseState(
409
404
  continue;
410
405
  }
411
406
 
412
- const ref = parseStateNodeRef(seg, palette);
407
+ const ref = parseStateNodeRef(seg);
413
408
  if (!ref) continue;
414
409
 
415
410
  const node = getOrCreateNode(ref, lineNumber);