@diagrammo/dgmo 0.2.27 → 0.3.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.
Files changed (49) hide show
  1. package/.claude/skills/dgmo-chart/SKILL.md +107 -0
  2. package/.claude/skills/dgmo-flowchart/SKILL.md +61 -0
  3. package/.claude/skills/dgmo-generate/SKILL.md +58 -0
  4. package/.claude/skills/dgmo-sequence/SKILL.md +83 -0
  5. package/.cursorrules +117 -0
  6. package/.github/copilot-instructions.md +117 -0
  7. package/.windsurfrules +117 -0
  8. package/README.md +10 -3
  9. package/dist/cli.cjs +366 -918
  10. package/dist/index.cjs +581 -396
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +39 -24
  13. package/dist/index.d.ts +39 -24
  14. package/dist/index.js +578 -395
  15. package/dist/index.js.map +1 -1
  16. package/docs/ai-integration.md +125 -0
  17. package/docs/language-reference.md +786 -0
  18. package/package.json +15 -8
  19. package/src/c4/parser.ts +90 -74
  20. package/src/c4/renderer.ts +13 -12
  21. package/src/c4/types.ts +6 -4
  22. package/src/chart.ts +3 -2
  23. package/src/class/layout.ts +17 -12
  24. package/src/class/parser.ts +22 -52
  25. package/src/class/renderer.ts +44 -46
  26. package/src/class/types.ts +1 -1
  27. package/src/cli.ts +130 -19
  28. package/src/d3.ts +1 -1
  29. package/src/dgmo-mermaid.ts +1 -1
  30. package/src/dgmo-router.ts +1 -1
  31. package/src/echarts.ts +33 -13
  32. package/src/er/parser.ts +34 -43
  33. package/src/er/types.ts +1 -1
  34. package/src/graph/flowchart-parser.ts +2 -25
  35. package/src/graph/types.ts +1 -1
  36. package/src/index.ts +5 -0
  37. package/src/initiative-status/parser.ts +36 -7
  38. package/src/initiative-status/types.ts +1 -1
  39. package/src/kanban/parser.ts +32 -53
  40. package/src/kanban/renderer.ts +9 -8
  41. package/src/kanban/types.ts +6 -14
  42. package/src/org/parser.ts +47 -87
  43. package/src/org/resolver.ts +11 -12
  44. package/src/sequence/parser.ts +97 -15
  45. package/src/sequence/renderer.ts +62 -69
  46. package/src/utils/arrows.ts +75 -0
  47. package/src/utils/inline-markdown.ts +75 -0
  48. package/src/utils/parsing.ts +67 -0
  49. package/src/utils/tag-groups.ts +76 -0
@@ -406,43 +406,42 @@ export function renderClassDiagram(
406
406
  const methods = node.members.filter((m) => m.isMethod);
407
407
 
408
408
  if (isEnum) {
409
- // Enum: all members as values
410
- if (node.members.length > 0) {
411
- // Separator
412
- nodeG.append('line')
413
- .attr('x1', -w / 2)
414
- .attr('y1', yPos)
415
- .attr('x2', w / 2)
416
- .attr('y2', yPos)
417
- .attr('stroke', stroke)
418
- .attr('stroke-width', 0.5)
419
- .attr('stroke-opacity', 0.5);
420
-
421
- let memberY = yPos + COMPARTMENT_PADDING_Y;
422
- for (const member of node.members) {
423
- nodeG.append('text')
424
- .attr('x', -w / 2 + MEMBER_PADDING_X)
425
- .attr('y', memberY + MEMBER_LINE_HEIGHT / 2)
426
- .attr('dominant-baseline', 'central')
427
- .attr('fill', palette.text)
428
- .attr('font-size', MEMBER_FONT_SIZE)
429
- .text(member.name);
430
- memberY += MEMBER_LINE_HEIGHT;
431
- }
409
+ // Enum: single values compartment
410
+ // Separator
411
+ nodeG.append('line')
412
+ .attr('x1', -w / 2)
413
+ .attr('y1', yPos)
414
+ .attr('x2', w / 2)
415
+ .attr('y2', yPos)
416
+ .attr('stroke', stroke)
417
+ .attr('stroke-width', 0.5)
418
+ .attr('stroke-opacity', 0.5);
419
+
420
+ let memberY = yPos + COMPARTMENT_PADDING_Y;
421
+ for (const member of node.members) {
422
+ nodeG.append('text')
423
+ .attr('x', -w / 2 + MEMBER_PADDING_X)
424
+ .attr('y', memberY + MEMBER_LINE_HEIGHT / 2)
425
+ .attr('dominant-baseline', 'central')
426
+ .attr('fill', palette.text)
427
+ .attr('font-size', MEMBER_FONT_SIZE)
428
+ .text(member.name);
429
+ memberY += MEMBER_LINE_HEIGHT;
432
430
  }
433
431
  } else {
434
- // Fields compartment
435
- if (fields.length > 0) {
436
- // Separator
437
- nodeG.append('line')
438
- .attr('x1', -w / 2)
439
- .attr('y1', yPos)
440
- .attr('x2', w / 2)
441
- .attr('y2', yPos)
442
- .attr('stroke', stroke)
443
- .attr('stroke-width', 0.5)
444
- .attr('stroke-opacity', 0.5);
432
+ // UML 3-compartment layout: always show both separators
433
+
434
+ // Fields separator
435
+ nodeG.append('line')
436
+ .attr('x1', -w / 2)
437
+ .attr('y1', yPos)
438
+ .attr('x2', w / 2)
439
+ .attr('y2', yPos)
440
+ .attr('stroke', stroke)
441
+ .attr('stroke-width', 0.5)
442
+ .attr('stroke-opacity', 0.5);
445
443
 
444
+ if (fields.length > 0) {
446
445
  let memberY = yPos + COMPARTMENT_PADDING_Y;
447
446
  for (const field of fields) {
448
447
  const vis = visibilitySymbol(field.visibility);
@@ -463,21 +462,20 @@ export function renderClassDiagram(
463
462
 
464
463
  memberY += MEMBER_LINE_HEIGHT;
465
464
  }
466
- yPos += node.fieldsHeight;
467
465
  }
466
+ yPos += node.fieldsHeight;
467
+
468
+ // Methods separator
469
+ nodeG.append('line')
470
+ .attr('x1', -w / 2)
471
+ .attr('y1', yPos)
472
+ .attr('x2', w / 2)
473
+ .attr('y2', yPos)
474
+ .attr('stroke', stroke)
475
+ .attr('stroke-width', 0.5)
476
+ .attr('stroke-opacity', 0.5);
468
477
 
469
- // Methods compartment
470
478
  if (methods.length > 0) {
471
- // Separator
472
- nodeG.append('line')
473
- .attr('x1', -w / 2)
474
- .attr('y1', yPos)
475
- .attr('x2', w / 2)
476
- .attr('y2', yPos)
477
- .attr('stroke', stroke)
478
- .attr('stroke-width', 0.5)
479
- .attr('stroke-opacity', 0.5);
480
-
481
479
  let memberY = yPos + COMPARTMENT_PADDING_Y;
482
480
  for (const method of methods) {
483
481
  const vis = visibilitySymbol(method.visibility);
@@ -51,5 +51,5 @@ export interface ParsedClassDiagram {
51
51
  relationships: ClassRelationship[];
52
52
  options: Record<string, string>;
53
53
  diagnostics: DgmoError[];
54
- error?: string;
54
+ error: string | null;
55
55
  }
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 } from './dgmo-router';
6
+ import { parseDgmo, DGMO_CHART_TYPE_MAP } from './dgmo-router';
7
7
  import { parseDgmoChartType } from './dgmo-router';
8
8
  import { formatDgmoError } from './diagnostics';
9
9
  import { getPalette } from './palettes/registry';
@@ -24,6 +24,38 @@ const PALETTES = [
24
24
 
25
25
  const THEMES = ['light', 'dark', 'transparent'] as const;
26
26
 
27
+ const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
28
+ bar: 'Bar chart — categorical comparisons',
29
+ line: 'Line chart — trends over time',
30
+ 'multi-line': 'Multi-line chart — multiple series trends',
31
+ area: 'Area chart — filled line chart',
32
+ pie: 'Pie chart — part-to-whole proportions',
33
+ doughnut: 'Doughnut chart — ring-style pie chart',
34
+ radar: 'Radar chart — multi-dimensional metrics',
35
+ 'polar-area': 'Polar area chart — radial bar chart',
36
+ 'bar-stacked': 'Stacked bar chart — multi-series categorical',
37
+ scatter: 'Scatter plot — 2D data points or bubble chart',
38
+ sankey: 'Sankey diagram — flow/allocation visualization',
39
+ chord: 'Chord diagram — circular flow relationships',
40
+ function: 'Function plot — mathematical expressions',
41
+ heatmap: 'Heatmap — matrix intensity visualization',
42
+ funnel: 'Funnel chart — conversion pipeline',
43
+ slope: 'Slope chart — change between two periods',
44
+ wordcloud: 'Word cloud — term frequency visualization',
45
+ arc: 'Arc diagram — network relationships',
46
+ timeline: 'Timeline — events, eras, and date ranges',
47
+ venn: 'Venn diagram — set overlaps',
48
+ quadrant: 'Quadrant chart — 2x2 positioning matrix',
49
+ sequence: 'Sequence diagram — message/interaction flows',
50
+ flowchart: 'Flowchart — decision trees and process flows',
51
+ class: 'Class diagram — UML class hierarchies',
52
+ er: 'ER diagram — database schemas and relationships',
53
+ org: 'Org chart — hierarchical tree structures',
54
+ kanban: 'Kanban board — task/workflow columns',
55
+ c4: 'C4 diagram — system architecture (context, container, component, deployment)',
56
+ 'initiative-status': 'Initiative status — project roadmap with dependency tracking',
57
+ };
58
+
27
59
  function printHelp(): void {
28
60
  console.log(`Usage: dgmo <input> [options]
29
61
  cat input.dgmo | dgmo [options]
@@ -42,6 +74,8 @@ Options:
42
74
  --c4-container <name> Container to drill into (with --c4-level components)
43
75
  --no-branding Omit diagrammo.app branding from exports
44
76
  --copy Copy URL to clipboard (only with -o url)
77
+ --json Output structured JSON to stdout
78
+ --chart-types List all supported chart types
45
79
  --help Show this help
46
80
  --version Show version`);
47
81
  }
@@ -62,6 +96,8 @@ function parseArgs(argv: string[]): {
62
96
  version: boolean;
63
97
  noBranding: boolean;
64
98
  copy: boolean;
99
+ json: boolean;
100
+ chartTypes: boolean;
65
101
  c4Level: 'context' | 'containers' | 'components' | 'deployment';
66
102
  c4System: string | undefined;
67
103
  c4Container: string | undefined;
@@ -75,6 +111,8 @@ function parseArgs(argv: string[]): {
75
111
  version: false,
76
112
  noBranding: false,
77
113
  copy: false,
114
+ json: false,
115
+ chartTypes: false,
78
116
  c4Level: 'context' as 'context' | 'containers' | 'components' | 'deployment',
79
117
  c4System: undefined as string | undefined,
80
118
  c4Container: undefined as string | undefined,
@@ -134,6 +172,12 @@ function parseArgs(argv: string[]): {
134
172
  } else if (arg === '--no-branding') {
135
173
  result.noBranding = true;
136
174
  i++;
175
+ } else if (arg === '--json') {
176
+ result.json = true;
177
+ i++;
178
+ } else if (arg === '--chart-types') {
179
+ result.chartTypes = true;
180
+ i++;
137
181
  } else if (arg === '--copy') {
138
182
  result.copy = true;
139
183
  i++;
@@ -220,6 +264,23 @@ async function main(): Promise<void> {
220
264
  return;
221
265
  }
222
266
 
267
+ if (opts.chartTypes) {
268
+ const types = Object.keys(DGMO_CHART_TYPE_MAP);
269
+ if (opts.json) {
270
+ const chartTypes = types.map((id) => ({
271
+ id,
272
+ description: CHART_TYPE_DESCRIPTIONS[id] ?? id,
273
+ }));
274
+ process.stdout.write(JSON.stringify({ chartTypes }, null, 2) + '\n');
275
+ } else {
276
+ for (const id of types) {
277
+ const desc = CHART_TYPE_DESCRIPTIONS[id];
278
+ console.log(desc ? `${id} — ${desc.split(' — ')[1]}` : id);
279
+ }
280
+ }
281
+ return;
282
+ }
283
+
223
284
  // Determine input source
224
285
  let content: string;
225
286
  let inputBasename: string | undefined;
@@ -277,14 +338,30 @@ async function main(): Promise<void> {
277
338
  process.exit(1);
278
339
  }
279
340
 
341
+ const chartType = parseDgmoChartType(content);
342
+
343
+ // Helper for JSON error output
344
+ function exitWithJsonError(error: string, line?: number): never {
345
+ if (opts.json) {
346
+ process.stdout.write(JSON.stringify({
347
+ success: false,
348
+ error,
349
+ ...(line != null ? { line } : {}),
350
+ ...(chartType ? { chartType } : {}),
351
+ }, null, 2) + '\n');
352
+ } else {
353
+ console.error(error);
354
+ }
355
+ process.exit(1);
356
+ }
357
+
280
358
  // URL output — encode DSL directly, no rendering needed
281
359
  if (format === 'url') {
282
360
  const result = encodeDiagramUrl(content);
283
361
  if (result.error) {
284
- console.error(
362
+ exitWithJsonError(
285
363
  `Error: Diagram too large for URL sharing (${result.compressedSize} bytes, limit ${result.limit} bytes)`
286
364
  );
287
- process.exit(1);
288
365
  }
289
366
 
290
367
  if (opts.copy) {
@@ -303,7 +380,15 @@ async function main(): Promise<void> {
303
380
  }
304
381
  }
305
382
 
306
- process.stdout.write(result.url + '\n');
383
+ if (opts.json) {
384
+ process.stdout.write(JSON.stringify({
385
+ success: true,
386
+ url: result.url,
387
+ ...(chartType ? { chartType } : {}),
388
+ }, null, 2) + '\n');
389
+ } else {
390
+ process.stdout.write(result.url + '\n');
391
+ }
307
392
  return;
308
393
  }
309
394
 
@@ -313,10 +398,9 @@ async function main(): Promise<void> {
313
398
  // which are unavailable in Node.js — check before attempting render.
314
399
  const wordcloudRe = /^\s*chart\s*:\s*wordcloud\b/im;
315
400
  if (wordcloudRe.test(content)) {
316
- console.error(
401
+ exitWithJsonError(
317
402
  'Error: Word clouds are not supported in the CLI (requires Canvas). Use the desktop app or browser instead.'
318
403
  );
319
- process.exit(1);
320
404
  }
321
405
 
322
406
  // Parse first to collect diagnostics
@@ -325,28 +409,36 @@ async function main(): Promise<void> {
325
409
  const warnings = diagnostics.filter((d) => d.severity === 'warning');
326
410
 
327
411
  // Print warnings even if rendering succeeds
328
- for (const w of warnings) {
329
- console.error(`\u26A0 ${formatDgmoError(w)}`);
412
+ if (!opts.json) {
413
+ for (const w of warnings) {
414
+ console.error(`\u26A0 ${formatDgmoError(w)}`);
415
+ }
330
416
  }
331
417
 
332
- // Print errors
333
- for (const e of errors) {
334
- console.error(`\u2716 ${formatDgmoError(e)}`);
418
+ // Print errors and exit
419
+ if (errors.length > 0) {
420
+ if (opts.json) {
421
+ const firstError = errors[0];
422
+ exitWithJsonError(
423
+ formatDgmoError(firstError),
424
+ firstError.line,
425
+ );
426
+ }
427
+ for (const e of errors) {
428
+ console.error(`\u2716 ${formatDgmoError(e)}`);
429
+ }
335
430
  }
336
431
 
337
432
  // Validate C4 options
338
433
  if (opts.c4Level === 'containers' && !opts.c4System) {
339
- console.error('Error: --c4-system is required when --c4-level is containers');
340
- process.exit(1);
434
+ exitWithJsonError('Error: --c4-system is required when --c4-level is containers');
341
435
  }
342
436
  if (opts.c4Level === 'components') {
343
437
  if (!opts.c4System) {
344
- console.error('Error: --c4-system is required when --c4-level is components');
345
- process.exit(1);
438
+ exitWithJsonError('Error: --c4-system is required when --c4-level is components');
346
439
  }
347
440
  if (!opts.c4Container) {
348
- console.error('Error: --c4-container is required when --c4-level is components');
349
- process.exit(1);
441
+ exitWithJsonError('Error: --c4-container is required when --c4-level is components');
350
442
  }
351
443
  }
352
444
 
@@ -361,7 +453,7 @@ async function main(): Promise<void> {
361
453
 
362
454
  if (!svg) {
363
455
  if (errors.length === 0) {
364
- console.error(
456
+ exitWithJsonError(
365
457
  'Error: Failed to render diagram. The input may be empty, invalid, or use an unsupported chart type.'
366
458
  );
367
459
  }
@@ -371,7 +463,26 @@ async function main(): Promise<void> {
371
463
  // Determine output destination
372
464
  const pngBg = opts.theme === 'transparent' ? undefined : paletteColors.bg;
373
465
 
374
- if (opts.output) {
466
+ if (opts.json) {
467
+ // JSON mode: write file as normal but output JSON result to stdout
468
+ let outputPath: string | undefined;
469
+ if (opts.output) {
470
+ outputPath = resolve(opts.output);
471
+ if (format === 'svg') {
472
+ writeFileSync(outputPath, svg, 'utf-8');
473
+ } else {
474
+ writeFileSync(outputPath, svgToPng(svg, pngBg));
475
+ }
476
+ } else if (inputBasename) {
477
+ outputPath = resolve(`${inputBasename}.png`);
478
+ writeFileSync(outputPath, svgToPng(svg, pngBg));
479
+ }
480
+ process.stdout.write(JSON.stringify({
481
+ success: true,
482
+ ...(outputPath ? { output: outputPath } : {}),
483
+ ...(chartType ? { chartType } : {}),
484
+ }, null, 2) + '\n');
485
+ } else if (opts.output) {
375
486
  // Explicit output path
376
487
  const outputPath = resolve(opts.output);
377
488
  if (format === 'svg') {
package/src/d3.ts CHANGED
@@ -350,7 +350,7 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
350
350
  }
351
351
 
352
352
  // Skip comments
353
- if (line.startsWith('#') || line.startsWith('//')) {
353
+ if (line.startsWith('//')) {
354
354
  continue;
355
355
  }
356
356
 
@@ -82,7 +82,7 @@ export function parseQuadrant(content: string): ParsedQuadrant {
82
82
  const lineNumber = i + 1; // 1-indexed for editor
83
83
 
84
84
  // Skip empty lines and comments
85
- if (!line || line.startsWith('#') || line.startsWith('//')) continue;
85
+ if (!line || line.startsWith('//')) continue;
86
86
 
87
87
  // Skip the chart: directive (already consumed by router)
88
88
  if (/^chart\s*:/i.test(line)) continue;
@@ -81,7 +81,7 @@ export function parseDgmoChartType(content: string): string | null {
81
81
  for (const line of lines) {
82
82
  const trimmed = line.trim();
83
83
  // Skip empty lines and comments
84
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//'))
84
+ if (!trimmed || trimmed.startsWith('//'))
85
85
  continue;
86
86
  const match = trimmed.match(/^chart\s*:\s*(.+)/i);
87
87
  if (match) return match[1].trim().toLowerCase();
package/src/echarts.ts CHANGED
@@ -75,7 +75,7 @@ export interface ParsedEChart {
75
75
  showLabels?: boolean;
76
76
  categoryColors?: Record<string, string>;
77
77
  diagnostics: DgmoError[];
78
- error?: string;
78
+ error: string | null;
79
79
  }
80
80
 
81
81
  // ============================================================
@@ -116,6 +116,7 @@ export function parseEChart(
116
116
  type: 'scatter',
117
117
  data: [],
118
118
  diagnostics: [],
119
+ error: null,
119
120
  };
120
121
 
121
122
  // Track current category for grouped scatter charts
@@ -144,7 +145,7 @@ export function parseEChart(
144
145
  }
145
146
 
146
147
  // Skip comments
147
- if (trimmed.startsWith('#') || trimmed.startsWith('//')) continue;
148
+ if (trimmed.startsWith('//')) continue;
148
149
 
149
150
  // Check for category header: [Category Name]
150
151
  const categoryMatch = trimmed.match(/^\[(.+)\]$/);
@@ -964,7 +965,7 @@ function buildScatterOption(
964
965
  },
965
966
  }),
966
967
  grid: {
967
- left: parsed.ylabel ? '5%' : '3%',
968
+ left: parsed.ylabel ? '12%' : '3%',
968
969
  right: '4%',
969
970
  bottom: hasCategories ? '15%' : parsed.xlabel ? '10%' : '3%',
970
971
  top: parsed.title ? '15%' : '5%',
@@ -1289,18 +1290,29 @@ function makeGridAxis(
1289
1290
  splitLineColor: string,
1290
1291
  gridOpacity: number,
1291
1292
  label?: string,
1292
- data?: string[]
1293
+ data?: string[],
1294
+ nameGapOverride?: number
1293
1295
  ): Record<string, unknown> {
1296
+ const defaultGap = type === 'value' ? 75 : 40;
1294
1297
  return {
1295
1298
  type,
1296
1299
  ...(data && { data }),
1297
1300
  axisLine: { lineStyle: { color: axisLineColor } },
1298
- axisLabel: { color: textColor, fontSize: 16, fontFamily: FONT_FAMILY },
1301
+ axisLabel: {
1302
+ color: textColor,
1303
+ fontSize: type === 'category' && data ? (data.length > 10 ? 11 : data.length > 5 ? 12 : 16) : 16,
1304
+ fontFamily: FONT_FAMILY,
1305
+ ...(type === 'category' && {
1306
+ interval: 0,
1307
+ formatter: (value: string) =>
1308
+ value.replace(/([a-z])([A-Z])/g, '$1\n$2').replace(/ /g, '\n'),
1309
+ }),
1310
+ },
1299
1311
  splitLine: { lineStyle: { color: splitLineColor, opacity: gridOpacity } },
1300
1312
  ...(label && {
1301
1313
  name: label,
1302
1314
  nameLocation: 'middle',
1303
- nameGap: 40,
1315
+ nameGap: nameGapOverride ?? defaultGap,
1304
1316
  nameTextStyle: { color: textColor, fontSize: 18, fontFamily: FONT_FAMILY },
1305
1317
  }),
1306
1318
  };
@@ -1385,7 +1397,12 @@ function buildBarOption(
1385
1397
  itemStyle: { color: d.color ?? colors[i % colors.length] },
1386
1398
  }));
1387
1399
 
1388
- const categoryAxis = makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? yLabel : xLabel, labels);
1400
+ // When category labels are on the y-axis (horizontal bars), they can be wide
1401
+ // compute a nameGap that clears the longest label so the ylabel doesn't overlap.
1402
+ const hCatGap = isHorizontal && yLabel
1403
+ ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
1404
+ : undefined;
1405
+ const categoryAxis = makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? yLabel : xLabel, labels, hCatGap);
1389
1406
  const valueAxis = makeGridAxis('value', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? xLabel : yLabel);
1390
1407
 
1391
1408
  // xAxis is always the bottom axis, yAxis is always the left axis in ECharts
@@ -1400,7 +1417,7 @@ function buildBarOption(
1400
1417
  axisPointer: { type: 'shadow' },
1401
1418
  },
1402
1419
  grid: {
1403
- left: yLabel ? '5%' : '3%',
1420
+ left: yLabel ? '12%' : '3%',
1404
1421
  right: '4%',
1405
1422
  bottom: xLabel ? '10%' : '3%',
1406
1423
  top: parsed.title ? '15%' : '5%',
@@ -1448,7 +1465,7 @@ function buildLineOption(
1448
1465
  axisPointer: { type: 'line' },
1449
1466
  },
1450
1467
  grid: {
1451
- left: yLabel ? '5%' : '3%',
1468
+ left: yLabel ? '12%' : '3%',
1452
1469
  right: '4%',
1453
1470
  bottom: xLabel ? '10%' : '3%',
1454
1471
  top: parsed.title ? '15%' : '5%',
@@ -1524,7 +1541,7 @@ function buildMultiLineOption(
1524
1541
  textStyle: { color: textColor },
1525
1542
  },
1526
1543
  grid: {
1527
- left: yLabel ? '5%' : '3%',
1544
+ left: yLabel ? '12%' : '3%',
1528
1545
  right: '4%',
1529
1546
  bottom: '15%',
1530
1547
  top: parsed.title ? '15%' : '5%',
@@ -1563,7 +1580,7 @@ function buildAreaOption(
1563
1580
  axisPointer: { type: 'line' },
1564
1581
  },
1565
1582
  grid: {
1566
- left: yLabel ? '5%' : '3%',
1583
+ left: yLabel ? '12%' : '3%',
1567
1584
  right: '4%',
1568
1585
  bottom: xLabel ? '10%' : '3%',
1569
1586
  top: parsed.title ? '15%' : '5%',
@@ -1807,7 +1824,10 @@ function buildBarStackedOption(
1807
1824
  };
1808
1825
  });
1809
1826
 
1810
- const categoryAxis = makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? yLabel : xLabel, labels);
1827
+ const hCatGap = isHorizontal && yLabel
1828
+ ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
1829
+ : undefined;
1830
+ const categoryAxis = makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? yLabel : xLabel, labels, hCatGap);
1811
1831
  const valueAxis = makeGridAxis('value', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? xLabel : yLabel);
1812
1832
 
1813
1833
  return {
@@ -1825,7 +1845,7 @@ function buildBarStackedOption(
1825
1845
  textStyle: { color: textColor },
1826
1846
  },
1827
1847
  grid: {
1828
- left: yLabel ? '5%' : '3%',
1848
+ left: yLabel ? '12%' : '3%',
1829
1849
  right: '4%',
1830
1850
  bottom: '15%',
1831
1851
  top: parsed.title ? '15%' : '5%',