@diagrammo/dgmo 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -116,9 +116,35 @@ Multi-series: comma-separated values matching the `series` list. Single series o
116
116
 
117
117
  Options: `series`, `xlabel`, `ylabel`, `labels`.
118
118
 
119
+ **Era bands** — annotate named time periods with background shading:
120
+
121
+ ```
122
+ chart: line
123
+ title: U.S. Strategic Petroleum Reserve
124
+ ylabel: Million Barrels
125
+
126
+ era '81 -> '89: Reagan (red)
127
+ era '89 -> '93: Bush (red)
128
+ era '93 -> '01: Clinton (blue)
129
+
130
+ '81: 230
131
+ '85: 493
132
+ '89: 580
133
+ '93: 587
134
+ '01: 550
135
+ ```
136
+
137
+ Syntax: `era <start> -> <end>: <label> [(<color>)]`
138
+
139
+ - `start` and `end` must exactly match category labels in the data
140
+ - Color is optional; defaults to the palette's blue
141
+ - Band label is hidden if the era spans fewer than 3 category slots
142
+ - Works on `line`, `multi-line`, and `area` charts
143
+ - Era boundary labels are always pinned visible on the x-axis even when auto-skip is active
144
+
119
145
  ### area
120
146
 
121
- Same syntax as `line`. Renders as a filled area chart.
147
+ Same syntax as `line`, including era bands. Renders as a filled area chart.
122
148
 
123
149
  ### pie
124
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/chart.ts CHANGED
@@ -20,6 +20,13 @@ export interface ChartDataPoint {
20
20
  lineNumber: number;
21
21
  }
22
22
 
23
+ export interface ChartEra {
24
+ start: string; // exact category label, e.g. "'77"
25
+ end: string; // exact category label, e.g. "'81"
26
+ label: string; // display name, e.g. "Carter"
27
+ color: string | null; // resolved CSS color, or null → palette default
28
+ }
29
+
23
30
  import type { DgmoError } from './diagnostics';
24
31
 
25
32
  export interface ParsedChart {
@@ -36,6 +43,7 @@ export interface ParsedChart {
36
43
  label?: string;
37
44
  labels?: 'name' | 'value' | 'percent' | 'full';
38
45
  data: ChartDataPoint[];
46
+ eras?: ChartEra[];
39
47
  diagnostics: DgmoError[];
40
48
  error: string | null;
41
49
  }
@@ -87,9 +95,11 @@ export function parseChart(
87
95
  palette?: PaletteColors
88
96
  ): ParsedChart {
89
97
  const lines = content.split('\n');
98
+ const parsedEras: ChartEra[] = [];
90
99
  const result: ParsedChart = {
91
100
  type: 'bar',
92
101
  data: [],
102
+ eras: parsedEras,
93
103
  diagnostics: [],
94
104
  error: null,
95
105
  };
@@ -117,6 +127,18 @@ export function parseChart(
117
127
  // Skip comments
118
128
  if (trimmed.startsWith('//')) continue;
119
129
 
130
+ // Era line: must be matched before colon-split (era '77 -> '81: Carter has colons inside)
131
+ const eraMatch = trimmed.match(/^era\s+(.+?)\s*->\s*(.+?)\s*:\s*(.+?)(?:\s*\(([^)]+)\))?\s*$/);
132
+ if (eraMatch) {
133
+ parsedEras.push({
134
+ start: eraMatch[1].trim(),
135
+ end: eraMatch[2].trim(),
136
+ label: eraMatch[3].trim(),
137
+ color: eraMatch[4] ? resolveColor(eraMatch[4].trim(), palette) : null,
138
+ });
139
+ continue;
140
+ }
141
+
120
142
  // Parse key: value pairs
121
143
  const colonIndex = trimmed.indexOf(':');
122
144
  if (colonIndex === -1) continue;
@@ -214,6 +236,11 @@ export function parseChart(
214
236
  }
215
237
  }
216
238
 
239
+ // Eras are only valid for line, multi-line (aliased to 'line'), and area chart types
240
+ if (result.type !== 'line' && result.type !== 'area') {
241
+ result.eras = undefined;
242
+ }
243
+
217
244
  // Validation
218
245
  const setChartError = (line: number, message: string) => {
219
246
  const diag = makeDgmoError(line, message);
package/src/cli.ts CHANGED
@@ -3,7 +3,7 @@ import { execSync } from 'node:child_process';
3
3
  import { resolve, basename, extname } from 'node:path';
4
4
  import { Resvg } from '@resvg/resvg-js';
5
5
  import { render } from './render';
6
- import { parseDgmo, DGMO_CHART_TYPE_MAP } from './dgmo-router';
6
+ import { parseDgmo, getAllChartTypes } from './dgmo-router';
7
7
  import { parseDgmoChartType } from './dgmo-router';
8
8
  import { formatDgmoError } from './diagnostics';
9
9
  import { getPalette } from './palettes/registry';
@@ -276,7 +276,7 @@ async function main(): Promise<void> {
276
276
  }
277
277
 
278
278
  if (opts.chartTypes) {
279
- const types = Object.keys(DGMO_CHART_TYPE_MAP);
279
+ const types = getAllChartTypes();
280
280
  if (opts.json) {
281
281
  const chartTypes = types.map((id) => ({
282
282
  id,
package/src/d3.ts CHANGED
@@ -10,7 +10,7 @@ import { injectBranding } from './branding';
10
10
  // Types
11
11
  // ============================================================
12
12
 
13
- export type D3ChartType =
13
+ export type VisualizationType =
14
14
  | 'slope'
15
15
  | 'wordcloud'
16
16
  | 'arc'
@@ -136,8 +136,8 @@ export interface D3ExportDimensions {
136
136
  height?: number;
137
137
  }
138
138
 
139
- export interface ParsedD3 {
140
- type: D3ChartType | null;
139
+ export interface ParsedVisualization {
140
+ type: VisualizationType | null;
141
141
  title: string | null;
142
142
  titleLineNumber: number | null;
143
143
  orientation: 'horizontal' | 'vertical';
@@ -328,8 +328,8 @@ export function addDurationToDate(
328
328
  /**
329
329
  * Parses D3 chart text format into structured data.
330
330
  */
331
- export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
332
- const result: ParsedD3 = {
331
+ export function parseVisualization(content: string, palette?: PaletteColors): ParsedVisualization {
332
+ const result: ParsedVisualization = {
333
333
  type: null,
334
334
  title: null,
335
335
  titleLineNumber: null,
@@ -367,7 +367,7 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
367
367
  error: null,
368
368
  };
369
369
 
370
- const fail = (line: number, message: string): ParsedD3 => {
370
+ const fail = (line: number, message: string): ParsedVisualization => {
371
371
  const diag = makeDgmoError(line, message);
372
372
  result.diagnostics.push(diag);
373
373
  result.error = formatDgmoError(diag);
@@ -1301,7 +1301,7 @@ const SLOPE_CHAR_WIDTH = 8; // approximate px per character at 14px
1301
1301
  */
1302
1302
  export function renderSlopeChart(
1303
1303
  container: HTMLDivElement,
1304
- parsed: ParsedD3,
1304
+ parsed: ParsedVisualization,
1305
1305
  palette: PaletteColors,
1306
1306
  isDark: boolean,
1307
1307
  onClickItem?: (lineNumber: number) => void,
@@ -1690,7 +1690,7 @@ const ARC_MARGIN = { top: 60, right: 40, bottom: 60, left: 40 };
1690
1690
  */
1691
1691
  export function renderArcDiagram(
1692
1692
  container: HTMLDivElement,
1693
- parsed: ParsedD3,
1693
+ parsed: ParsedVisualization,
1694
1694
  palette: PaletteColors,
1695
1695
  _isDark: boolean,
1696
1696
  onClickItem?: (lineNumber: number) => void,
@@ -2809,7 +2809,7 @@ function buildEraTooltipHtml(era: TimelineEra): string {
2809
2809
  */
2810
2810
  export function renderTimeline(
2811
2811
  container: HTMLDivElement,
2812
- parsed: ParsedD3,
2812
+ parsed: ParsedVisualization,
2813
2813
  palette: PaletteColors,
2814
2814
  isDark: boolean,
2815
2815
  onClickItem?: (lineNumber: number) => void,
@@ -4446,7 +4446,7 @@ function getRotateFn(mode: WordCloudRotate): () => number {
4446
4446
  */
4447
4447
  export function renderWordCloud(
4448
4448
  container: HTMLDivElement,
4449
- parsed: ParsedD3,
4449
+ parsed: ParsedVisualization,
4450
4450
  palette: PaletteColors,
4451
4451
  _isDark: boolean,
4452
4452
  onClickItem?: (lineNumber: number) => void,
@@ -4526,7 +4526,7 @@ export function renderWordCloud(
4526
4526
 
4527
4527
  function renderWordCloudAsync(
4528
4528
  container: HTMLDivElement,
4529
- parsed: ParsedD3,
4529
+ parsed: ParsedVisualization,
4530
4530
  palette: PaletteColors,
4531
4531
  _isDark: boolean,
4532
4532
  exportDims?: D3ExportDimensions
@@ -4790,7 +4790,7 @@ function regionCentroid(circles: Circle[], inside: boolean[]): Point {
4790
4790
 
4791
4791
  export function renderVenn(
4792
4792
  container: HTMLDivElement,
4793
- parsed: ParsedD3,
4793
+ parsed: ParsedVisualization,
4794
4794
  palette: PaletteColors,
4795
4795
  isDark: boolean,
4796
4796
  onClickItem?: (lineNumber: number) => void,
@@ -4890,8 +4890,6 @@ export function renderVenn(
4890
4890
  .attr('fill-opacity', 0.35)
4891
4891
  .attr('stroke', setColors[i])
4892
4892
  .attr('stroke-width', 2)
4893
- .attr('class', 'venn-fill-circle')
4894
- .attr('data-line-number', String(vennSets[i].lineNumber))
4895
4893
  .style('pointer-events', 'none') as d3Selection.Selection<
4896
4894
  SVGCircleElement,
4897
4895
  unknown,
@@ -4959,12 +4957,9 @@ export function renderVenn(
4959
4957
  .attr('fill', 'white')
4960
4958
  .attr('fill-opacity', 0)
4961
4959
  .attr('class', 'venn-region-overlay')
4960
+ .attr('data-line-number', regionLineNumber != null ? String(regionLineNumber) : '0')
4962
4961
  .attr('clip-path', `url(#${clipId})`);
4963
4962
 
4964
- if (regionLineNumber != null) {
4965
- el.attr('data-line-number', String(regionLineNumber));
4966
- }
4967
-
4968
4963
  if (excluded.length > 0) {
4969
4964
  // Mask subtracts excluded circles so only the exact region shape highlights
4970
4965
  const maskId = `vvm-${key}`;
@@ -4986,7 +4981,7 @@ export function renderVenn(
4986
4981
 
4987
4982
  const showRegionOverlay = (idxs: number[]) => {
4988
4983
  const key = [...idxs].sort((a, b) => a - b).join('-');
4989
- overlayEls.forEach((el, k) => el.attr('fill-opacity', k === key ? 0.3 : 0));
4984
+ overlayEls.forEach((el, k) => el.attr('fill-opacity', k === key ? 0 : 0.55));
4990
4985
  };
4991
4986
  const hideAllOverlays = () => {
4992
4987
  overlayEls.forEach(el => el.attr('fill-opacity', 0));
@@ -5217,7 +5212,7 @@ type QuadrantPosition =
5217
5212
  */
5218
5213
  export function renderQuadrant(
5219
5214
  container: HTMLDivElement,
5220
- parsed: ParsedD3,
5215
+ parsed: ParsedVisualization,
5221
5216
  palette: PaletteColors,
5222
5217
  isDark: boolean,
5223
5218
  onClickItem?: (lineNumber: number) => void,
@@ -5776,7 +5771,7 @@ function finalizeSvgExport(
5776
5771
  * Renders a D3 chart to an SVG string for export.
5777
5772
  * Creates a detached DOM element, renders into it, extracts the SVG, then cleans up.
5778
5773
  */
5779
- export async function renderD3ForExport(
5774
+ export async function renderForExport(
5780
5775
  content: string,
5781
5776
  theme: 'light' | 'dark' | 'transparent',
5782
5777
  palette?: PaletteColors,
@@ -5788,7 +5783,7 @@ export async function renderD3ForExport(
5788
5783
  },
5789
5784
  options?: { branding?: boolean; c4Level?: 'context' | 'containers' | 'components' | 'deployment'; c4System?: string; c4Container?: string; scenario?: string }
5790
5785
  ): Promise<string> {
5791
- // Flowchart and org chart use their own parser pipelines — intercept before parseD3()
5786
+ // Flowchart and org chart use their own parser pipelines — intercept before parseVisualization()
5792
5787
  const { parseDgmoChartType } = await import('./dgmo-router');
5793
5788
  const detectedType = parseDgmoChartType(content);
5794
5789
 
@@ -6053,8 +6048,8 @@ export async function renderD3ForExport(
6053
6048
  return finalizeSvgExport(container, theme, effectivePalette, options);
6054
6049
  }
6055
6050
 
6056
- const parsed = parseD3(content, palette);
6057
- // Allow sequence diagrams through even if parseD3 errors —
6051
+ const parsed = parseVisualization(content, palette);
6052
+ // Allow sequence diagrams through even if parseVisualization errors —
6058
6053
  // sequence is parsed by its own dedicated parser (parseSequenceDgmo)
6059
6054
  // and may not have a "chart:" line (auto-detected from arrow syntax).
6060
6055
  if (parsed.error && parsed.type !== 'sequence') {
@@ -8,8 +8,8 @@ import { looksLikeState, parseState } from './graph/state-parser';
8
8
  import { looksLikeClassDiagram, parseClassDiagram } from './class/parser';
9
9
  import { looksLikeERDiagram, parseERDiagram } from './er/parser';
10
10
  import { parseChart } from './chart';
11
- import { parseEChart } from './echarts';
12
- import { parseD3 } from './d3';
11
+ import { parseExtendedChart } from './echarts';
12
+ 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';
@@ -19,18 +19,15 @@ import { parseInfra } from './infra/parser';
19
19
  import type { DgmoError } from './diagnostics';
20
20
 
21
21
  /**
22
- * Framework identifiers used by the .dgmo router.
23
- * Maps to the existing preview components and export paths.
22
+ * Framework identifiers used by the .dgmo router internally.
23
+ * Not part of the public API use RenderCategory instead.
24
24
  */
25
- export type DgmoFramework = 'echart' | 'd3' | 'mermaid';
25
+ type DgmoFramework = 'echart' | 'd3' | 'mermaid';
26
26
 
27
27
  /**
28
- * Maps every supported chart type string to its backing framework.
29
- *
30
- * ECharts: standard chart types (bar, line, pie, etc.), scatter, flow/relationship diagrams, math, heatmap
31
- * D3: slope, wordcloud, arc diagram, timeline
28
+ * Maps every supported chart type string to its backing framework (internal).
32
29
  */
33
- export const DGMO_CHART_TYPE_MAP: Record<string, DgmoFramework> = {
30
+ const DGMO_CHART_TYPE_MAP: Record<string, DgmoFramework> = {
34
31
  // Standard charts (via ECharts)
35
32
  bar: 'echart',
36
33
  line: 'echart',
@@ -71,9 +68,10 @@ export const DGMO_CHART_TYPE_MAP: Record<string, DgmoFramework> = {
71
68
  };
72
69
 
73
70
  /**
74
- * Returns the framework for a given chart type, or `null` if unknown.
71
+ * Returns the internal framework for a given chart type, or `null` if unknown.
72
+ * Internal only — use getRenderCategory() for public dispatch.
75
73
  */
76
- export function getDgmoFramework(chartType: string): DgmoFramework | null {
74
+ function getDgmoFramework(chartType: string): DgmoFramework | null {
77
75
  return DGMO_CHART_TYPE_MAP[chartType.toLowerCase()] ?? null;
78
76
  }
79
77
 
@@ -107,13 +105,69 @@ export function parseDgmoChartType(content: string): string | null {
107
105
  return null;
108
106
  }
109
107
 
110
- /** Standard chart types parsed by parseChart (then rendered via ECharts). */
111
- export const STANDARD_CHART_TYPES = new Set([
108
+ // ============================================================
109
+ // Public render-category API
110
+ // ============================================================
111
+
112
+ /** User-visible rendering category for dispatch and routing. */
113
+ export type RenderCategory = 'data-chart' | 'visualization' | 'diagram';
114
+
115
+ const DATA_CHART_TYPES = new Set([
116
+ 'bar', 'line', 'pie', 'doughnut', 'area', 'polar-area', 'radar',
117
+ 'bar-stacked', 'multi-line', 'scatter', 'sankey', 'chord', 'function',
118
+ 'heatmap', 'funnel',
119
+ ]);
120
+ const VISUALIZATION_TYPES = new Set([
121
+ 'slope', 'wordcloud', 'arc', 'timeline', 'venn', 'quadrant',
122
+ ]);
123
+ const DIAGRAM_TYPES = new Set([
124
+ 'sequence', 'flowchart', 'class', 'er', 'org', 'kanban', 'c4',
125
+ 'initiative-status', 'state', 'sitemap', 'infra',
126
+ ]);
127
+ const EXTENDED_CHART_TYPES = new Set([
128
+ 'scatter', 'sankey', 'chord', 'function', 'heatmap', 'funnel',
129
+ ]);
130
+
131
+ /**
132
+ * Returns the render category for a given chart type, or `null` if unknown.
133
+ * Use this instead of the internal framework map for dispatch in consumers.
134
+ */
135
+ export function getRenderCategory(chartType: string): RenderCategory | null {
136
+ const type = chartType.toLowerCase();
137
+ if (DATA_CHART_TYPES.has(type)) return 'data-chart';
138
+ if (VISUALIZATION_TYPES.has(type)) return 'visualization';
139
+ if (DIAGRAM_TYPES.has(type)) return 'diagram';
140
+ return null;
141
+ }
142
+
143
+ /**
144
+ * Returns true if the chart type is an extended chart type
145
+ * handled by parseExtendedChart (scatter, sankey, chord, function, heatmap, funnel).
146
+ * Returns false for standard chart types and all other types.
147
+ */
148
+ export function isExtendedChartType(chartType: string): boolean {
149
+ return EXTENDED_CHART_TYPES.has(chartType.toLowerCase());
150
+ }
151
+
152
+ /** Standard chart types parsed by parseChart (then rendered via ECharts). Internal use. */
153
+ const STANDARD_CHART_TYPES = new Set([
112
154
  'bar', 'line', 'multi-line', 'area', 'pie', 'doughnut',
113
155
  'radar', 'polar-area', 'bar-stacked',
114
156
  ]);
115
157
 
116
- // ECharts-native types parsed by parseEChart
158
+ /**
159
+ * Returns all supported chart type identifiers.
160
+ * Useful for CLI enumeration and autocomplete.
161
+ */
162
+ export function getAllChartTypes(): string[] {
163
+ return [
164
+ ...DATA_CHART_TYPES,
165
+ ...VISUALIZATION_TYPES,
166
+ ...DIAGRAM_TYPES,
167
+ ];
168
+ }
169
+
170
+ // ECharts-native types parsed by parseExtendedChart
117
171
  const ECHART_TYPES = new Set([
118
172
  'scatter', 'sankey', 'chord', 'function', 'heatmap', 'funnel',
119
173
  ]);
@@ -141,8 +195,8 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
141
195
  const chartType = parseDgmoChartType(content);
142
196
 
143
197
  if (!chartType) {
144
- // No chart type detected — try D3 parser as fallback (it handles missing chart: line)
145
- return { diagnostics: parseD3(content).diagnostics };
198
+ // No chart type detected — try visualization parser as fallback (it handles missing chart: line)
199
+ return { diagnostics: parseVisualization(content).diagnostics };
146
200
  }
147
201
 
148
202
  const directParser = PARSE_DISPATCH.get(chartType);
@@ -152,9 +206,9 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
152
206
  return { diagnostics: parseChart(content).diagnostics };
153
207
  }
154
208
  if (ECHART_TYPES.has(chartType)) {
155
- return { diagnostics: parseEChart(content).diagnostics };
209
+ return { diagnostics: parseExtendedChart(content).diagnostics };
156
210
  }
157
211
 
158
- // D3 types (slope, wordcloud, arc, timeline, venn, quadrant)
159
- return { diagnostics: parseD3(content).diagnostics };
212
+ // Visualization types (slope, wordcloud, arc, timeline, venn, quadrant)
213
+ return { diagnostics: parseVisualization(content).diagnostics };
160
214
  }