@diagrammo/dgmo 0.6.2 → 0.7.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 (61) hide show
  1. package/.claude/commands/dgmo.md +231 -13
  2. package/AGENTS.md +148 -0
  3. package/dist/cli.cjs +341 -165
  4. package/dist/index.cjs +4900 -1685
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +259 -18
  7. package/dist/index.d.ts +259 -18
  8. package/dist/index.js +4642 -1436
  9. package/dist/index.js.map +1 -1
  10. package/package.json +5 -3
  11. package/src/c4/layout.ts +0 -5
  12. package/src/c4/parser.ts +0 -16
  13. package/src/c4/renderer.ts +7 -11
  14. package/src/class/layout.ts +0 -1
  15. package/src/class/parser.ts +28 -0
  16. package/src/class/renderer.ts +189 -34
  17. package/src/cli.ts +566 -25
  18. package/src/colors.ts +3 -3
  19. package/src/completion.ts +58 -0
  20. package/src/d3.ts +179 -122
  21. package/src/dgmo-router.ts +3 -58
  22. package/src/echarts.ts +96 -55
  23. package/src/er/parser.ts +30 -1
  24. package/src/er/renderer.ts +12 -7
  25. package/src/gantt/calculator.ts +677 -0
  26. package/src/gantt/parser.ts +761 -0
  27. package/src/gantt/renderer.ts +2125 -0
  28. package/src/gantt/resolver.ts +144 -0
  29. package/src/gantt/types.ts +168 -0
  30. package/src/graph/flowchart-parser.ts +27 -4
  31. package/src/graph/flowchart-renderer.ts +1 -2
  32. package/src/graph/state-parser.ts +0 -1
  33. package/src/graph/state-renderer.ts +1 -3
  34. package/src/index.ts +37 -0
  35. package/src/infra/compute.ts +0 -7
  36. package/src/infra/layout.ts +0 -2
  37. package/src/infra/parser.ts +46 -4
  38. package/src/infra/renderer.ts +49 -27
  39. package/src/initiative-status/filter.ts +63 -0
  40. package/src/initiative-status/layout.ts +319 -67
  41. package/src/initiative-status/parser.ts +200 -25
  42. package/src/initiative-status/renderer.ts +298 -35
  43. package/src/initiative-status/types.ts +6 -0
  44. package/src/kanban/parser.ts +0 -2
  45. package/src/org/layout.ts +22 -59
  46. package/src/org/renderer.ts +11 -36
  47. package/src/palettes/dracula.ts +60 -0
  48. package/src/palettes/index.ts +8 -6
  49. package/src/palettes/monokai.ts +60 -0
  50. package/src/palettes/registry.ts +4 -2
  51. package/src/sequence/parser.ts +14 -11
  52. package/src/sequence/renderer.ts +5 -6
  53. package/src/sequence/tag-resolution.ts +0 -1
  54. package/src/sharing.ts +8 -0
  55. package/src/sitemap/layout.ts +1 -14
  56. package/src/sitemap/parser.ts +1 -2
  57. package/src/sitemap/renderer.ts +4 -7
  58. package/src/utils/arrows.ts +7 -7
  59. package/src/utils/duration.ts +212 -0
  60. package/src/utils/export-container.ts +40 -0
  61. package/src/utils/legend-constants.ts +1 -0
@@ -16,65 +16,9 @@ import { parseC4 } from './c4/parser';
16
16
  import { looksLikeInitiativeStatus, parseInitiativeStatus } from './initiative-status/parser';
17
17
  import { looksLikeSitemap, parseSitemap } from './sitemap/parser';
18
18
  import { parseInfra } from './infra/parser';
19
+ import { parseGantt } from './gantt/parser';
19
20
  import type { DgmoError } from './diagnostics';
20
21
 
21
- /**
22
- * Framework identifiers used by the .dgmo router internally.
23
- * Not part of the public API — use RenderCategory instead.
24
- */
25
- type DgmoFramework = 'echart' | 'd3' | 'mermaid';
26
-
27
- /**
28
- * Maps every supported chart type string to its backing framework (internal).
29
- */
30
- const DGMO_CHART_TYPE_MAP: Record<string, DgmoFramework> = {
31
- // Standard charts (via ECharts)
32
- bar: 'echart',
33
- line: 'echart',
34
- 'multi-line': 'echart',
35
- area: 'echart',
36
- pie: 'echart',
37
- doughnut: 'echart',
38
- radar: 'echart',
39
- 'polar-area': 'echart',
40
- 'bar-stacked': 'echart',
41
-
42
- // ECharts
43
- scatter: 'echart',
44
- sankey: 'echart',
45
- chord: 'echart',
46
- function: 'echart',
47
- heatmap: 'echart',
48
- funnel: 'echart',
49
-
50
- // D3
51
- slope: 'd3',
52
- wordcloud: 'd3',
53
- arc: 'd3',
54
- timeline: 'd3',
55
- venn: 'd3',
56
- quadrant: 'd3',
57
- sequence: 'd3',
58
- flowchart: 'd3',
59
- class: 'd3',
60
- er: 'd3',
61
- org: 'd3',
62
- kanban: 'd3',
63
- c4: 'd3',
64
- 'initiative-status': 'd3',
65
- state: 'd3',
66
- sitemap: 'd3',
67
- infra: 'd3',
68
- };
69
-
70
- /**
71
- * Returns the internal framework for a given chart type, or `null` if unknown.
72
- * Internal only — use getRenderCategory() for public dispatch.
73
- */
74
- function getDgmoFramework(chartType: string): DgmoFramework | null {
75
- return DGMO_CHART_TYPE_MAP[chartType.toLowerCase()] ?? null;
76
- }
77
-
78
22
  /**
79
23
  * Extracts the `chart:` type value from raw file content.
80
24
  * Falls back to inference when no explicit `chart:` line is found
@@ -122,7 +66,7 @@ const VISUALIZATION_TYPES = new Set([
122
66
  ]);
123
67
  const DIAGRAM_TYPES = new Set([
124
68
  'sequence', 'flowchart', 'class', 'er', 'org', 'kanban', 'c4',
125
- 'initiative-status', 'state', 'sitemap', 'infra',
69
+ 'initiative-status', 'state', 'sitemap', 'infra', 'gantt',
126
70
  ]);
127
71
  const EXTENDED_CHART_TYPES = new Set([
128
72
  'scatter', 'sankey', 'chord', 'function', 'heatmap', 'funnel',
@@ -185,6 +129,7 @@ const PARSE_DISPATCH = new Map<string, (content: string) => { diagnostics: DgmoE
185
129
  ['state', (c) => parseState(c)],
186
130
  ['sitemap', (c) => parseSitemap(c)],
187
131
  ['infra', (c) => parseInfra(c)],
132
+ ['gantt', (c) => parseGantt(c)],
188
133
  ]);
189
134
 
190
135
  /**
package/src/echarts.ts CHANGED
@@ -86,6 +86,7 @@ export interface ParsedExtendedChart {
86
86
 
87
87
  import type { PaletteColors } from './palettes';
88
88
  import { getSeriesColors, getSegmentColors } from './palettes';
89
+ import { mix } from './palettes/color-utils';
89
90
  import { parseChart } from './chart';
90
91
  import type { ParsedChart, ChartEra } from './chart';
91
92
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
@@ -98,6 +99,7 @@ import { collectIndentedValues, extractColor, measureIndent, parseSeriesNames }
98
99
 
99
100
  const EMPHASIS_SELF = { focus: 'self' as const, blurScope: 'global' as const };
100
101
  const CHART_BASE: Pick<EChartsOption, 'backgroundColor' | 'animation'> = { backgroundColor: 'transparent', animation: false };
102
+ const CHART_BORDER_WIDTH = 2;
101
103
 
102
104
  // ============================================================
103
105
  // Parser
@@ -487,10 +489,12 @@ export function buildExtendedChartOption(
487
489
 
488
490
  // Chord diagram
489
491
  if (parsed.type === 'chord') {
492
+ const bg = isDark ? palette.surface : palette.bg;
490
493
  return buildChordOption(
491
494
  parsed,
492
495
  textColor,
493
496
  colors,
497
+ bg,
494
498
  titleConfig,
495
499
  tooltipTheme
496
500
  );
@@ -512,6 +516,7 @@ export function buildExtendedChartOption(
512
516
 
513
517
  // Scatter plot
514
518
  if (parsed.type === 'scatter') {
519
+ const bg = isDark ? palette.surface : palette.bg;
515
520
  return buildScatterOption(
516
521
  parsed,
517
522
  palette,
@@ -519,6 +524,7 @@ export function buildExtendedChartOption(
519
524
  axisLineColor,
520
525
  gridOpacity,
521
526
  colors,
527
+ bg,
522
528
  titleConfig,
523
529
  tooltipTheme
524
530
  );
@@ -526,10 +532,12 @@ export function buildExtendedChartOption(
526
532
 
527
533
  // Funnel chart
528
534
  if (parsed.type === 'funnel') {
535
+ const bg = isDark ? palette.surface : palette.bg;
529
536
  return buildFunnelOption(
530
537
  parsed,
531
538
  textColor,
532
539
  colors,
540
+ bg,
533
541
  titleConfig,
534
542
  tooltipTheme
535
543
  );
@@ -539,6 +547,7 @@ export function buildExtendedChartOption(
539
547
  return buildHeatmapOption(
540
548
  parsed,
541
549
  palette,
550
+ isDark,
542
551
  textColor,
543
552
  axisLineColor,
544
553
  titleConfig,
@@ -616,6 +625,7 @@ function buildChordOption(
616
625
  parsed: ParsedExtendedChart,
617
626
  textColor: string,
618
627
  colors: string[],
628
+ bg: string,
619
629
  titleConfig: EChartsOption['title'],
620
630
  tooltipTheme: Record<string, unknown>
621
631
  ): EChartsOption {
@@ -647,12 +657,13 @@ function buildChordOption(
647
657
  }
648
658
 
649
659
  // Create category data for nodes with colors
650
- const categories = nodeNames.map((name, index) => ({
651
- name,
652
- itemStyle: {
653
- color: colors[index % colors.length],
654
- },
655
- }));
660
+ const categories = nodeNames.map((name, index) => {
661
+ const stroke = colors[index % colors.length];
662
+ return {
663
+ name,
664
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
665
+ };
666
+ });
656
667
 
657
668
  return {
658
669
  ...CHART_BASE,
@@ -876,6 +887,7 @@ function buildScatterOption(
876
887
  axisLineColor: string,
877
888
  gridOpacity: number,
878
889
  colors: string[],
890
+ bg: string,
879
891
  titleConfig: EChartsOption['title'],
880
892
  tooltipTheme: Record<string, unknown>
881
893
  ): EChartsOption {
@@ -919,7 +931,9 @@ function buildScatterOption(
919
931
  const data = categoryPoints.map((p) => ({
920
932
  name: p.name,
921
933
  value: hasSize ? [p.x, p.y, p.size ?? 0] : [p.x, p.y],
922
- ...(p.color && { itemStyle: { color: p.color } }),
934
+ ...(p.color && {
935
+ itemStyle: { color: mix(p.color, bg, 30), borderColor: p.color, borderWidth: CHART_BORDER_WIDTH },
936
+ }),
923
937
  }));
924
938
 
925
939
  return {
@@ -929,23 +943,24 @@ function buildScatterOption(
929
943
  ...(hasSize
930
944
  ? { symbolSize: (val: number[]) => val[2] }
931
945
  : { symbolSize: defaultSize }),
932
- itemStyle: { color: catColor },
946
+ itemStyle: { color: mix(catColor, bg, 30), borderColor: catColor, borderWidth: CHART_BORDER_WIDTH },
933
947
  label: labelConfig,
934
948
  emphasis: emphasisConfig,
935
949
  };
936
950
  });
937
951
  } else {
938
952
  // Single series — per-point colors
939
- const data = points.map((p, index) => ({
940
- name: p.name,
941
- value: hasSize ? [p.x, p.y, p.size ?? 0] : [p.x, p.y],
942
- ...(hasSize
943
- ? { symbolSize: p.size ?? defaultSize }
944
- : { symbolSize: defaultSize }),
945
- itemStyle: {
946
- color: p.color ?? colors[index % colors.length],
947
- },
948
- }));
953
+ const data = points.map((p, index) => {
954
+ const stroke = p.color ?? colors[index % colors.length];
955
+ return {
956
+ name: p.name,
957
+ value: hasSize ? [p.x, p.y, p.size ?? 0] : [p.x, p.y],
958
+ ...(hasSize
959
+ ? { symbolSize: p.size ?? defaultSize }
960
+ : { symbolSize: defaultSize }),
961
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
962
+ };
963
+ });
949
964
 
950
965
  series = [
951
966
  {
@@ -1065,11 +1080,13 @@ function buildScatterOption(
1065
1080
  function buildHeatmapOption(
1066
1081
  parsed: ParsedExtendedChart,
1067
1082
  palette: PaletteColors,
1083
+ isDark: boolean,
1068
1084
  textColor: string,
1069
1085
  axisLineColor: string,
1070
1086
  titleConfig: EChartsOption['title'],
1071
1087
  tooltipTheme: Record<string, unknown>
1072
1088
  ): EChartsOption {
1089
+ const bg = isDark ? palette.surface : palette.bg;
1073
1090
  const heatmapRows = parsed.heatmapRows ?? [];
1074
1091
  const columns = parsed.columns ?? [];
1075
1092
  const rowLabels = heatmapRows.map((r) => r.label);
@@ -1144,10 +1161,10 @@ function buildHeatmapOption(
1144
1161
  top: 'center',
1145
1162
  inRange: {
1146
1163
  color: [
1147
- palette.primary,
1148
- palette.colors.cyan,
1149
- palette.colors.yellow,
1150
- palette.colors.orange,
1164
+ mix(palette.primary, bg, 30),
1165
+ mix(palette.colors.cyan, bg, 30),
1166
+ mix(palette.colors.yellow, bg, 30),
1167
+ mix(palette.colors.orange, bg, 30),
1151
1168
  ],
1152
1169
  },
1153
1170
  textStyle: {
@@ -1158,9 +1175,13 @@ function buildHeatmapOption(
1158
1175
  {
1159
1176
  type: 'heatmap',
1160
1177
  data,
1178
+ itemStyle: {
1179
+ borderWidth: 2,
1180
+ borderColor: bg,
1181
+ },
1161
1182
  label: {
1162
1183
  show: true,
1163
- color: '#ffffff',
1184
+ color: textColor,
1164
1185
  fontSize: 14,
1165
1186
  fontWeight: 'bold' as const,
1166
1187
  },
@@ -1183,6 +1204,7 @@ function buildFunnelOption(
1183
1204
  parsed: ParsedExtendedChart,
1184
1205
  textColor: string,
1185
1206
  colors: string[],
1207
+ bg: string,
1186
1208
  titleConfig: EChartsOption['title'],
1187
1209
  tooltipTheme: Record<string, unknown>
1188
1210
  ): EChartsOption {
@@ -1190,14 +1212,18 @@ function buildFunnelOption(
1190
1212
  const sorted = [...parsed.data].sort((a, b) => b.value - a.value);
1191
1213
  const topValue = sorted.length > 0 ? sorted[0].value : 1;
1192
1214
 
1193
- const data = sorted.map((d) => ({
1194
- name: d.label,
1195
- value: d.value,
1196
- itemStyle: {
1197
- color: d.color ?? colors[parsed.data.indexOf(d) % colors.length],
1198
- borderWidth: 0,
1199
- },
1200
- }));
1215
+ const data = sorted.map((d) => {
1216
+ const stroke = d.color ?? colors[parsed.data.indexOf(d) % colors.length];
1217
+ return {
1218
+ name: d.label,
1219
+ value: d.value,
1220
+ itemStyle: {
1221
+ color: mix(stroke, bg, 30),
1222
+ borderColor: stroke,
1223
+ borderWidth: CHART_BORDER_WIDTH,
1224
+ },
1225
+ };
1226
+ });
1201
1227
 
1202
1228
  // Build lookup for tooltip: previous step value (in sorted order)
1203
1229
  const prevValueMap = new Map<string, number>();
@@ -1394,12 +1420,13 @@ export function buildSimpleChartOption(
1394
1420
  if (parsed.error) return {};
1395
1421
 
1396
1422
  const { textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme } = buildChartCommons(parsed, palette, isDark);
1423
+ const bg = isDark ? palette.surface : palette.bg;
1397
1424
 
1398
1425
  switch (parsed.type) {
1399
1426
  case 'bar':
1400
- return buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth);
1427
+ return buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth);
1401
1428
  case 'bar-stacked':
1402
- return buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth);
1429
+ return buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth);
1403
1430
  case 'line':
1404
1431
  return parsed.seriesNames
1405
1432
  ? buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth)
@@ -1407,13 +1434,13 @@ export function buildSimpleChartOption(
1407
1434
  case 'area':
1408
1435
  return buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, tooltipTheme, chartWidth);
1409
1436
  case 'pie':
1410
- return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), titleConfig, tooltipTheme, false);
1437
+ return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), bg, titleConfig, tooltipTheme, false);
1411
1438
  case 'doughnut':
1412
- return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), titleConfig, tooltipTheme, true);
1439
+ return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), bg, titleConfig, tooltipTheme, true);
1413
1440
  case 'radar':
1414
- return buildRadarOption(parsed, palette, textColor, gridOpacity, colors, titleConfig, tooltipTheme);
1441
+ return buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, titleConfig, tooltipTheme);
1415
1442
  case 'polar-area':
1416
- return buildPolarAreaOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), titleConfig, tooltipTheme);
1443
+ return buildPolarAreaOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), bg, titleConfig, tooltipTheme);
1417
1444
  }
1418
1445
  }
1419
1446
 
@@ -1439,6 +1466,7 @@ function buildBarOption(
1439
1466
  splitLineColor: string,
1440
1467
  gridOpacity: number,
1441
1468
  colors: string[],
1469
+ bg: string,
1442
1470
  titleConfig: EChartsOption['title'],
1443
1471
  tooltipTheme: Record<string, unknown>,
1444
1472
  chartWidth?: number
@@ -1446,10 +1474,13 @@ function buildBarOption(
1446
1474
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
1447
1475
  const isHorizontal = parsed.orientation === 'horizontal';
1448
1476
  const labels = parsed.data.map((d) => d.label);
1449
- const data = parsed.data.map((d, i) => ({
1450
- value: d.value,
1451
- itemStyle: { color: d.color ?? colors[i % colors.length] },
1452
- }));
1477
+ const data = parsed.data.map((d, i) => {
1478
+ const stroke = d.color ?? colors[i % colors.length];
1479
+ return {
1480
+ value: d.value,
1481
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
1482
+ };
1483
+ });
1453
1484
 
1454
1485
  // When category labels are on the y-axis (horizontal bars), they can be wide —
1455
1486
  // compute a nameGap that clears the longest label so the ylabel doesn't overlap.
@@ -1701,15 +1732,19 @@ function buildPieOption(
1701
1732
  parsed: ParsedChart,
1702
1733
  textColor: string,
1703
1734
  colors: string[],
1735
+ bg: string,
1704
1736
  titleConfig: EChartsOption['title'],
1705
1737
  tooltipTheme: Record<string, unknown>,
1706
1738
  isDoughnut: boolean
1707
1739
  ): EChartsOption {
1708
- const data = parsed.data.map((d, i) => ({
1709
- name: d.label,
1710
- value: d.value,
1711
- itemStyle: { color: d.color ?? colors[i % colors.length] },
1712
- }));
1740
+ const data = parsed.data.map((d, i) => {
1741
+ const stroke = d.color ?? colors[i % colors.length];
1742
+ return {
1743
+ name: d.label,
1744
+ value: d.value,
1745
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
1746
+ };
1747
+ });
1713
1748
 
1714
1749
  return {
1715
1750
  ...CHART_BASE,
@@ -1741,12 +1776,13 @@ function buildPieOption(
1741
1776
  function buildRadarOption(
1742
1777
  parsed: ParsedChart,
1743
1778
  palette: PaletteColors,
1779
+ isDark: boolean,
1744
1780
  textColor: string,
1745
1781
  gridOpacity: number,
1746
- colors: string[],
1747
1782
  titleConfig: EChartsOption['title'],
1748
1783
  tooltipTheme: Record<string, unknown>
1749
1784
  ): EChartsOption {
1785
+ const bg = isDark ? palette.surface : palette.bg;
1750
1786
  const radarColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
1751
1787
  const values = parsed.data.map((d) => d.value);
1752
1788
  const maxValue = Math.max(...values) * 1.15;
@@ -1785,7 +1821,7 @@ function buildRadarOption(
1785
1821
  {
1786
1822
  value: values,
1787
1823
  name: parsed.series ?? 'Value',
1788
- areaStyle: { color: radarColor, opacity: 0.25 },
1824
+ areaStyle: { color: mix(radarColor, bg, 30) },
1789
1825
  lineStyle: { color: radarColor },
1790
1826
  itemStyle: { color: radarColor },
1791
1827
  symbol: 'circle',
@@ -1811,14 +1847,18 @@ function buildPolarAreaOption(
1811
1847
  parsed: ParsedChart,
1812
1848
  textColor: string,
1813
1849
  colors: string[],
1850
+ bg: string,
1814
1851
  titleConfig: EChartsOption['title'],
1815
1852
  tooltipTheme: Record<string, unknown>
1816
1853
  ): EChartsOption {
1817
- const data = parsed.data.map((d, i) => ({
1818
- name: d.label,
1819
- value: d.value,
1820
- itemStyle: { color: d.color ?? colors[i % colors.length] },
1821
- }));
1854
+ const data = parsed.data.map((d, i) => {
1855
+ const stroke = d.color ?? colors[i % colors.length];
1856
+ return {
1857
+ name: d.label,
1858
+ value: d.value,
1859
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
1860
+ };
1861
+ });
1822
1862
 
1823
1863
  return {
1824
1864
  ...CHART_BASE,
@@ -1855,6 +1895,7 @@ function buildBarStackedOption(
1855
1895
  splitLineColor: string,
1856
1896
  gridOpacity: number,
1857
1897
  colors: string[],
1898
+ bg: string,
1858
1899
  titleConfig: EChartsOption['title'],
1859
1900
  tooltipTheme: Record<string, unknown>,
1860
1901
  chartWidth?: number
@@ -1874,12 +1915,12 @@ function buildBarStackedOption(
1874
1915
  type: 'bar' as const,
1875
1916
  stack: 'total',
1876
1917
  data,
1877
- itemStyle: { color },
1918
+ itemStyle: { color: mix(color, bg, 30), borderColor: color, borderWidth: CHART_BORDER_WIDTH },
1878
1919
  label: {
1879
1920
  show: true,
1880
1921
  position: 'inside' as const,
1881
1922
  formatter: '{c}',
1882
- color: '#ffffff',
1923
+ color: textColor,
1883
1924
  fontSize: 14,
1884
1925
  fontWeight: 'bold' as const,
1885
1926
  fontFamily: FONT_FAMILY,
package/src/er/parser.ts CHANGED
@@ -7,7 +7,6 @@ import type { TagGroup } from '../utils/tag-groups';
7
7
  import type {
8
8
  ParsedERDiagram,
9
9
  ERTable,
10
- ERColumn,
11
10
  ERConstraint,
12
11
  ERCardinality,
13
12
  } from './types';
@@ -463,3 +462,33 @@ export function looksLikeERDiagram(content: string): boolean {
463
462
 
464
463
  return false;
465
464
  }
465
+
466
+ // ============================================================
467
+ // Symbol extraction (for completion API)
468
+ // ============================================================
469
+
470
+ import type { DiagramSymbols } from '../completion';
471
+
472
+ /**
473
+ * Extract table names (entities) and ER keywords from document text.
474
+ * Used by the dgmo completion API for ghost hints and popup completions.
475
+ */
476
+ export function extractSymbols(docText: string): DiagramSymbols {
477
+ const entities: string[] = [];
478
+ let inMetadata = true;
479
+ for (const rawLine of docText.split('\n')) {
480
+ const line = rawLine.trim();
481
+ if (inMetadata && /^chart\s*:/i.test(line)) continue;
482
+ if (inMetadata && /^[a-z-]+\s*:/i.test(line)) continue; // metadata key
483
+ inMetadata = false;
484
+ if (line.length === 0) continue;
485
+ if (/^\s/.test(rawLine)) continue; // indented = column definition, not table
486
+ const m = TABLE_DECL_RE.exec(line);
487
+ if (m) entities.push(m[1]!);
488
+ }
489
+ return {
490
+ kind: 'er',
491
+ entities,
492
+ keywords: ['pk', 'fk', 'unique', 'nullable', '1', '*', '?'],
493
+ };
494
+ }
@@ -17,13 +17,12 @@ import {
17
17
  LEGEND_CAPSULE_PAD,
18
18
  LEGEND_DOT_R,
19
19
  LEGEND_ENTRY_FONT_SIZE,
20
- LEGEND_ENTRY_FONT_W,
21
20
  LEGEND_ENTRY_DOT_GAP,
22
21
  LEGEND_ENTRY_TRAIL,
23
22
  LEGEND_GROUP_GAP,
24
23
  } from '../utils/legend-constants';
25
24
  import type { ParsedERDiagram, ERConstraint } from './types';
26
- import type { ERLayoutResult, ERLayoutNode, ERLayoutEdge } from './layout';
25
+ import type { ERLayoutResult } from './layout';
27
26
  import { parseERDiagram } from './parser';
28
27
  import { layoutERDiagram } from './layout';
29
28
  import { classifyEREntities, ROLE_COLORS, ROLE_LABELS, ROLE_ORDER } from './classify';
@@ -225,7 +224,13 @@ export function renderERDiagram(
225
224
 
226
225
  const useSemanticColors =
227
226
  parsed.tagGroups.length === 0 && layout.nodes.every((n) => !n.color);
228
- const legendReserveH = useSemanticColors ? LEGEND_HEIGHT + DIAGRAM_PADDING : 0;
227
+ const LEGEND_FIXED_GAP = 8;
228
+ const hasTagLegend = parsed.tagGroups.length > 0;
229
+ const legendReserveH = useSemanticColors
230
+ ? LEGEND_HEIGHT + LEGEND_FIXED_GAP
231
+ : hasTagLegend
232
+ ? LEGEND_HEIGHT + LEGEND_FIXED_GAP
233
+ : 0;
229
234
 
230
235
  const titleHeight = parsed.title ? 40 : 0;
231
236
  const diagramW = layout.width;
@@ -255,13 +260,13 @@ export function renderERDiagram(
255
260
  scale = Math.min(MAX_SCALE, scaleX, scaleY);
256
261
  const scaledW = diagramW * scale;
257
262
  offsetX = (viewW - scaledW) / 2;
258
- offsetY = titleHeight + DIAGRAM_PADDING;
263
+ offsetY = titleHeight + legendReserveH + DIAGRAM_PADDING;
259
264
  } else {
260
265
  viewW = naturalW;
261
266
  viewH = naturalH;
262
267
  scale = 1;
263
268
  offsetX = DIAGRAM_PADDING;
264
- offsetY = titleHeight + DIAGRAM_PADDING;
269
+ offsetY = titleHeight + legendReserveH + DIAGRAM_PADDING;
265
270
  }
266
271
 
267
272
  if (viewW <= 0 || viewH <= 0) return;
@@ -522,7 +527,7 @@ export function renderERDiagram(
522
527
  }
523
528
 
524
529
  let legendX = DIAGRAM_PADDING;
525
- let legendY = viewH - DIAGRAM_PADDING;
530
+ let legendY = DIAGRAM_PADDING + titleHeight;
526
531
 
527
532
  for (const group of parsed.tagGroups) {
528
533
  const groupG = legendG.append('g')
@@ -640,7 +645,7 @@ export function renderERDiagram(
640
645
  }
641
646
 
642
647
  const legendX = (viewW - totalWidth) / 2;
643
- const legendY = viewH - DIAGRAM_PADDING - LEGEND_HEIGHT;
648
+ const legendY = DIAGRAM_PADDING + titleHeight;
644
649
 
645
650
  const semanticLegendG = svg
646
651
  .append('g')