@diagrammo/dgmo 0.8.22 → 0.8.25
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.
- package/.claude/commands/dgmo.md +60 -72
- package/dist/cli.cjs +123 -116
- package/dist/editor.cjs +3 -2
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +3 -2
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +3 -2
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +1649 -442
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -23
- package/dist/index.d.ts +196 -23
- package/dist/index.js +1631 -440
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +677 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +267 -0
- package/dist/internal.d.ts +267 -0
- package/dist/internal.js +633 -0
- package/dist/internal.js.map +1 -0
- package/docs/guide/chart-area.md +17 -17
- package/docs/guide/chart-bar-stacked.md +12 -12
- package/docs/guide/chart-cycle.md +156 -0
- package/docs/guide/chart-doughnut.md +10 -10
- package/docs/guide/chart-funnel.md +9 -9
- package/docs/guide/chart-heatmap.md +10 -10
- package/docs/guide/chart-journey-map.md +179 -0
- package/docs/guide/chart-kanban.md +2 -0
- package/docs/guide/chart-line.md +19 -19
- package/docs/guide/chart-multi-line.md +16 -16
- package/docs/guide/chart-pie.md +11 -11
- package/docs/guide/chart-polar-area.md +10 -10
- package/docs/guide/chart-pyramid.md +111 -0
- package/docs/guide/chart-radar.md +9 -9
- package/docs/guide/chart-scatter.md +24 -27
- package/docs/guide/index.md +3 -3
- package/docs/guide/registry.json +5 -0
- package/docs/language-reference.md +108 -26
- package/fonts/Inter-Bold.ttf +0 -0
- package/fonts/Inter-Regular.ttf +0 -0
- package/fonts/LICENSE-Inter.txt +92 -0
- package/gallery/fixtures/bar-stacked.dgmo +12 -6
- package/gallery/fixtures/heatmap.dgmo +12 -6
- package/gallery/fixtures/multi-line.dgmo +11 -7
- package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
- package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
- package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
- package/gallery/fixtures/quadrant.dgmo +8 -8
- package/gallery/fixtures/scatter.dgmo +12 -12
- package/package.json +14 -2
- package/src/boxes-and-lines/parser.ts +13 -2
- package/src/boxes-and-lines/renderer.ts +22 -13
- package/src/chart-type-scoring.ts +162 -0
- package/src/chart-types.ts +437 -0
- package/src/cli.ts +152 -101
- package/src/completion.ts +9 -48
- package/src/cycle/layout.ts +19 -28
- package/src/cycle/renderer.ts +59 -32
- package/src/cycle/types.ts +21 -0
- package/src/d3.ts +30 -3
- package/src/dgmo-router.ts +98 -73
- package/src/echarts.ts +1 -1
- package/src/editor/keywords.ts +3 -2
- package/src/fonts.ts +3 -2
- package/src/gantt/parser.ts +5 -1
- package/src/index.ts +37 -3
- package/src/infra/parser.ts +3 -3
- package/src/internal.ts +20 -0
- package/src/journey-map/layout.ts +7 -3
- package/src/journey-map/parser.ts +5 -1
- package/src/journey-map/renderer.ts +112 -47
- package/src/kanban/parser.ts +5 -1
- package/src/org/collapse.ts +82 -4
- package/src/org/parser.ts +1 -1
- package/src/org/renderer.ts +221 -4
- package/src/pyramid/parser.ts +172 -0
- package/src/pyramid/renderer.ts +684 -0
- package/src/pyramid/types.ts +28 -0
- package/src/render.ts +2 -8
- package/src/sequence/parser.ts +64 -22
- package/src/sequence/participant-inference.ts +0 -1
- package/src/sequence/renderer.ts +97 -265
- package/src/sharing.ts +0 -1
- package/src/sitemap/parser.ts +1 -1
- package/src/tech-radar/interactive.ts +54 -0
- package/src/utils/parsing.ts +1 -0
- package/src/utils/tag-groups.ts +35 -5
- package/src/wireframe/parser.ts +3 -1
package/src/cli.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
4
|
+
import { homedir, platform } from 'node:os';
|
|
5
5
|
import { resolve, join, basename, extname } from 'node:path';
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
7
|
import { Resvg } from '@resvg/resvg-js';
|
|
8
8
|
import { render } from './render';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
parseDgmo,
|
|
11
|
+
getAllChartTypes,
|
|
12
|
+
CHART_TYPE_DESCRIPTIONS,
|
|
13
|
+
} from './dgmo-router';
|
|
10
14
|
import { parseDgmoChartType } from './dgmo-router';
|
|
11
15
|
import { formatDgmoError } from './diagnostics';
|
|
12
16
|
import { getPalette, getAvailablePalettes } from './palettes';
|
|
@@ -19,40 +23,6 @@ const PALETTES = getAvailablePalettes().map((p) => p.id);
|
|
|
19
23
|
|
|
20
24
|
const THEMES = ['light', 'dark', 'transparent'] as const;
|
|
21
25
|
|
|
22
|
-
const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
23
|
-
bar: 'Bar chart — categorical comparisons',
|
|
24
|
-
line: 'Line chart — trends over time',
|
|
25
|
-
'multi-line': 'Multi-line chart — multiple series trends',
|
|
26
|
-
area: 'Area chart — filled line chart',
|
|
27
|
-
pie: 'Pie chart — part-to-whole proportions',
|
|
28
|
-
doughnut: 'Doughnut chart — ring-style pie chart',
|
|
29
|
-
radar: 'Radar chart — multi-dimensional metrics',
|
|
30
|
-
'polar-area': 'Polar area chart — radial bar chart',
|
|
31
|
-
'bar-stacked': 'Stacked bar chart — multi-series categorical',
|
|
32
|
-
scatter: 'Scatter plot — 2D data points or bubble chart',
|
|
33
|
-
sankey: 'Sankey diagram — flow/allocation visualization',
|
|
34
|
-
chord: 'Chord diagram — circular flow relationships',
|
|
35
|
-
function: 'Function plot — mathematical expressions',
|
|
36
|
-
heatmap: 'Heatmap — matrix intensity visualization',
|
|
37
|
-
funnel: 'Funnel chart — conversion pipeline',
|
|
38
|
-
slope: 'Slope chart — change between two periods',
|
|
39
|
-
wordcloud: 'Word cloud — term frequency visualization',
|
|
40
|
-
arc: 'Arc diagram — network relationships',
|
|
41
|
-
timeline: 'Timeline — events, eras, and date ranges',
|
|
42
|
-
venn: 'Venn diagram — set overlaps',
|
|
43
|
-
quadrant: 'Quadrant chart — 2x2 positioning matrix',
|
|
44
|
-
sequence: 'Sequence diagram — message/interaction flows',
|
|
45
|
-
flowchart: 'Flowchart — decision trees and process flows',
|
|
46
|
-
class: 'Class diagram — UML class hierarchies',
|
|
47
|
-
er: 'ER diagram — database schemas and relationships',
|
|
48
|
-
org: 'Org chart — hierarchical tree structures',
|
|
49
|
-
kanban: 'Kanban board — task/workflow columns',
|
|
50
|
-
c4: 'C4 diagram — system architecture (context, container, component, deployment)',
|
|
51
|
-
infra: 'Infra chart — infrastructure traffic flow with rps computation',
|
|
52
|
-
'boxes-and-lines':
|
|
53
|
-
'Boxes and lines — general-purpose node-edge diagrams with groups and tags',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
26
|
const CLAUDE_SKILL_CONTENT = `# dgmo — Diagrammo Diagram Assistant
|
|
57
27
|
|
|
58
28
|
You are helping the user author, render, and share diagrams using the \`dgmo\` CLI and \`.dgmo\` file format.
|
|
@@ -497,10 +467,6 @@ Options:
|
|
|
497
467
|
With stdin and no -o, PNG is written to stdout
|
|
498
468
|
--theme <theme> Theme: ${THEMES.join(', ')} (default: light)
|
|
499
469
|
--palette <name> Palette: ${PALETTES.join(', ')} (default: nord)
|
|
500
|
-
--c4-level <level> C4 render level: context (default), containers, components, deployment
|
|
501
|
-
--c4-system <name> System to drill into (with --c4-level containers or components)
|
|
502
|
-
--c4-container <name> Container to drill into (with --c4-level components)
|
|
503
|
-
--tag-group <name> Pre-select a tag group for static export coloring
|
|
504
470
|
--copy Copy URL to clipboard (only with -o url)
|
|
505
471
|
--json Output structured JSON to stdout
|
|
506
472
|
--chart-types List all supported chart types
|
|
@@ -512,6 +478,11 @@ Options:
|
|
|
512
478
|
--install-codex-integration
|
|
513
479
|
Full Codex CLI setup: write AGENTS.md to the project and configure
|
|
514
480
|
the dgmo MCP server in .codex/config.toml (project) or ~/.codex/config.toml (global)
|
|
481
|
+
--install-claude-desktop-integration
|
|
482
|
+
Full Claude Desktop setup: install @diagrammo/dgmo-mcp if needed,
|
|
483
|
+
then merge the dgmo MCP entry into Claude Desktop's config file
|
|
484
|
+
(~/Library/Application Support/Claude/claude_desktop_config.json on macOS,
|
|
485
|
+
%APPDATA%/Claude/... on Windows, ~/.config/Claude/... on Linux)
|
|
515
486
|
--help Show this help
|
|
516
487
|
--version Show version`);
|
|
517
488
|
}
|
|
@@ -538,10 +509,7 @@ function parseArgs(argv: string[]): {
|
|
|
538
509
|
installClaudeSkill: boolean;
|
|
539
510
|
installClaudeCodeIntegration: boolean;
|
|
540
511
|
installCodexIntegration: boolean;
|
|
541
|
-
|
|
542
|
-
c4System: string | undefined;
|
|
543
|
-
c4Container: string | undefined;
|
|
544
|
-
tagGroup: string | undefined;
|
|
512
|
+
installClaudeDesktopIntegration: boolean;
|
|
545
513
|
} {
|
|
546
514
|
const result = {
|
|
547
515
|
input: undefined as string | undefined,
|
|
@@ -558,14 +526,7 @@ function parseArgs(argv: string[]): {
|
|
|
558
526
|
installClaudeSkill: false,
|
|
559
527
|
installClaudeCodeIntegration: false,
|
|
560
528
|
installCodexIntegration: false,
|
|
561
|
-
|
|
562
|
-
| 'context'
|
|
563
|
-
| 'containers'
|
|
564
|
-
| 'components'
|
|
565
|
-
| 'deployment',
|
|
566
|
-
c4System: undefined as string | undefined,
|
|
567
|
-
c4Container: undefined as string | undefined,
|
|
568
|
-
tagGroup: undefined as string | undefined,
|
|
529
|
+
installClaudeDesktopIntegration: false,
|
|
569
530
|
};
|
|
570
531
|
|
|
571
532
|
const args = argv.slice(2); // skip node + script
|
|
@@ -609,30 +570,6 @@ function parseArgs(argv: string[]): {
|
|
|
609
570
|
}
|
|
610
571
|
result.palette = val;
|
|
611
572
|
i++;
|
|
612
|
-
} else if (arg === '--c4-level') {
|
|
613
|
-
const val = args[++i];
|
|
614
|
-
if (
|
|
615
|
-
val !== 'context' &&
|
|
616
|
-
val !== 'containers' &&
|
|
617
|
-
val !== 'components' &&
|
|
618
|
-
val !== 'deployment'
|
|
619
|
-
) {
|
|
620
|
-
console.error(
|
|
621
|
-
`Error: Invalid C4 level "${val}". Valid levels: context, containers, components, deployment`
|
|
622
|
-
);
|
|
623
|
-
process.exit(1);
|
|
624
|
-
}
|
|
625
|
-
result.c4Level = val;
|
|
626
|
-
i++;
|
|
627
|
-
} else if (arg === '--c4-system') {
|
|
628
|
-
result.c4System = args[++i];
|
|
629
|
-
i++;
|
|
630
|
-
} else if (arg === '--c4-container') {
|
|
631
|
-
result.c4Container = args[++i];
|
|
632
|
-
i++;
|
|
633
|
-
} else if (arg === '--tag-group') {
|
|
634
|
-
result.tagGroup = args[++i];
|
|
635
|
-
i++;
|
|
636
573
|
} else if (arg === '--json') {
|
|
637
574
|
result.json = true;
|
|
638
575
|
i++;
|
|
@@ -648,6 +585,9 @@ function parseArgs(argv: string[]): {
|
|
|
648
585
|
} else if (arg === '--install-codex-integration') {
|
|
649
586
|
result.installCodexIntegration = true;
|
|
650
587
|
i++;
|
|
588
|
+
} else if (arg === '--install-claude-desktop-integration') {
|
|
589
|
+
result.installClaudeDesktopIntegration = true;
|
|
590
|
+
i++;
|
|
651
591
|
} else if (arg === '--copy') {
|
|
652
592
|
result.copy = true;
|
|
653
593
|
i++;
|
|
@@ -673,12 +613,19 @@ function inferFormat(outputPath: string | undefined): 'svg' | 'png' | 'url' {
|
|
|
673
613
|
return 'png';
|
|
674
614
|
}
|
|
675
615
|
|
|
616
|
+
const BUNDLED_FONTS = [
|
|
617
|
+
join(__dirname, '..', 'fonts', 'Inter-Regular.ttf'),
|
|
618
|
+
join(__dirname, '..', 'fonts', 'Inter-Bold.ttf'),
|
|
619
|
+
];
|
|
620
|
+
|
|
676
621
|
function svgToPng(svg: string, background?: string): Buffer {
|
|
622
|
+
const fontFiles = BUNDLED_FONTS.filter((f) => existsSync(f));
|
|
677
623
|
const resvg = new Resvg(svg, {
|
|
678
624
|
fitTo: { mode: 'zoom', value: 2 },
|
|
679
625
|
...(background ? { background } : {}),
|
|
680
626
|
font: {
|
|
681
|
-
loadSystemFonts:
|
|
627
|
+
loadSystemFonts: fontFiles.length === 0,
|
|
628
|
+
...(fontFiles.length > 0 ? { fontFiles } : {}),
|
|
682
629
|
defaultFontFamily: DEFAULT_FONT_NAME,
|
|
683
630
|
sansSerifFamily: DEFAULT_FONT_NAME,
|
|
684
631
|
},
|
|
@@ -745,7 +692,7 @@ async function main(): Promise<void> {
|
|
|
745
692
|
} else {
|
|
746
693
|
for (const id of types) {
|
|
747
694
|
const desc = CHART_TYPE_DESCRIPTIONS[id];
|
|
748
|
-
console.log(desc ? `${id} — ${desc
|
|
695
|
+
console.log(desc ? `${id} — ${desc}` : id);
|
|
749
696
|
}
|
|
750
697
|
}
|
|
751
698
|
return;
|
|
@@ -1068,6 +1015,133 @@ async function main(): Promise<void> {
|
|
|
1068
1015
|
return;
|
|
1069
1016
|
}
|
|
1070
1017
|
|
|
1018
|
+
if (opts.installClaudeDesktopIntegration) {
|
|
1019
|
+
const ask = (prompt: string): Promise<string> =>
|
|
1020
|
+
new Promise((resolve) => {
|
|
1021
|
+
const rl = createInterface({
|
|
1022
|
+
input: process.stdin,
|
|
1023
|
+
output: process.stdout,
|
|
1024
|
+
});
|
|
1025
|
+
rl.question(prompt, (answer) => {
|
|
1026
|
+
rl.close();
|
|
1027
|
+
resolve(answer);
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// Check / install dgmo-mcp binary
|
|
1032
|
+
let dgmoMcpInstalled = false;
|
|
1033
|
+
try {
|
|
1034
|
+
execSync('which dgmo-mcp', { stdio: 'pipe' });
|
|
1035
|
+
dgmoMcpInstalled = true;
|
|
1036
|
+
} catch {
|
|
1037
|
+
/* not found */
|
|
1038
|
+
}
|
|
1039
|
+
if (!dgmoMcpInstalled) {
|
|
1040
|
+
const ans = await ask(
|
|
1041
|
+
'\ndgmo-mcp not found. Install @diagrammo/dgmo-mcp globally now? [Y/n] '
|
|
1042
|
+
);
|
|
1043
|
+
const yes =
|
|
1044
|
+
ans === '' || ans.toLowerCase() === 'y' || ans.toLowerCase() === 'yes';
|
|
1045
|
+
if (yes) {
|
|
1046
|
+
console.log('Installing @diagrammo/dgmo-mcp...');
|
|
1047
|
+
try {
|
|
1048
|
+
execSync('npm install -g @diagrammo/dgmo-mcp', { stdio: 'inherit' });
|
|
1049
|
+
console.log('✓ @diagrammo/dgmo-mcp installed');
|
|
1050
|
+
} catch {
|
|
1051
|
+
console.error('Error: Failed to install @diagrammo/dgmo-mcp.');
|
|
1052
|
+
console.error('Try manually: npm install -g @diagrammo/dgmo-mcp');
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
console.log(
|
|
1056
|
+
' Skipped. Install later with: npm install -g @diagrammo/dgmo-mcp'
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
} else {
|
|
1060
|
+
console.log('✓ dgmo-mcp already installed');
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Resolve the Claude Desktop config path for the current platform.
|
|
1064
|
+
// macOS and Windows use the documented Claude Desktop paths; Linux
|
|
1065
|
+
// doesn't have a first-party build yet, but community installs follow
|
|
1066
|
+
// the XDG config convention.
|
|
1067
|
+
const os = platform();
|
|
1068
|
+
let configPath: string;
|
|
1069
|
+
if (os === 'darwin') {
|
|
1070
|
+
configPath = join(
|
|
1071
|
+
homedir(),
|
|
1072
|
+
'Library',
|
|
1073
|
+
'Application Support',
|
|
1074
|
+
'Claude',
|
|
1075
|
+
'claude_desktop_config.json'
|
|
1076
|
+
);
|
|
1077
|
+
} else if (os === 'win32') {
|
|
1078
|
+
const appData =
|
|
1079
|
+
process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming');
|
|
1080
|
+
configPath = join(appData, 'Claude', 'claude_desktop_config.json');
|
|
1081
|
+
} else {
|
|
1082
|
+
configPath = join(
|
|
1083
|
+
homedir(),
|
|
1084
|
+
'.config',
|
|
1085
|
+
'Claude',
|
|
1086
|
+
'claude_desktop_config.json'
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Read existing config (or start fresh). Non-JSON contents are treated
|
|
1091
|
+
// as corruption and we bail — the user needs to resolve it manually so
|
|
1092
|
+
// we don't silently overwrite something they care about.
|
|
1093
|
+
type ClaudeDesktopConfig = {
|
|
1094
|
+
mcpServers?: Record<
|
|
1095
|
+
string,
|
|
1096
|
+
{ command: string; args?: string[]; env?: Record<string, string> }
|
|
1097
|
+
>;
|
|
1098
|
+
[key: string]: unknown;
|
|
1099
|
+
};
|
|
1100
|
+
let config: ClaudeDesktopConfig = {};
|
|
1101
|
+
if (existsSync(configPath)) {
|
|
1102
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
1103
|
+
if (raw.trim().length > 0) {
|
|
1104
|
+
try {
|
|
1105
|
+
config = JSON.parse(raw) as ClaudeDesktopConfig;
|
|
1106
|
+
} catch {
|
|
1107
|
+
console.error(
|
|
1108
|
+
`Error: ${configPath} exists but is not valid JSON. Fix it manually and re-run, or remove the file to regenerate.`
|
|
1109
|
+
);
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const existingDgmo = config.mcpServers?.dgmo;
|
|
1116
|
+
if (existingDgmo && existingDgmo.command === 'dgmo-mcp') {
|
|
1117
|
+
console.log(`✓ dgmo MCP server already configured in ${configPath}`);
|
|
1118
|
+
} else {
|
|
1119
|
+
if (existingDgmo) {
|
|
1120
|
+
const ans = await ask(
|
|
1121
|
+
`\nA "dgmo" entry already exists in ${configPath}. Overwrite? [y/N] `
|
|
1122
|
+
);
|
|
1123
|
+
if (ans.toLowerCase() !== 'y' && ans.toLowerCase() !== 'yes') {
|
|
1124
|
+
console.log(' Skipped.');
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
config.mcpServers = {
|
|
1129
|
+
...(config.mcpServers ?? {}),
|
|
1130
|
+
dgmo: { command: 'dgmo-mcp' },
|
|
1131
|
+
};
|
|
1132
|
+
mkdirSync(join(configPath, '..'), { recursive: true });
|
|
1133
|
+
writeFileSync(
|
|
1134
|
+
configPath,
|
|
1135
|
+
JSON.stringify(config, null, 2) + '\n',
|
|
1136
|
+
'utf-8'
|
|
1137
|
+
);
|
|
1138
|
+
console.log(`✓ dgmo MCP server configured: ${configPath}`);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
console.log('\nRestart Claude Desktop to activate the MCP server.');
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1071
1145
|
// Determine input source
|
|
1072
1146
|
let content: string;
|
|
1073
1147
|
let inputBasename: string | undefined;
|
|
@@ -1225,32 +1299,9 @@ async function main(): Promise<void> {
|
|
|
1225
1299
|
}
|
|
1226
1300
|
}
|
|
1227
1301
|
|
|
1228
|
-
// Validate C4 options
|
|
1229
|
-
if (opts.c4Level === 'containers' && !opts.c4System) {
|
|
1230
|
-
exitWithJsonError(
|
|
1231
|
-
'Error: --c4-system is required when --c4-level is containers'
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
if (opts.c4Level === 'components') {
|
|
1235
|
-
if (!opts.c4System) {
|
|
1236
|
-
exitWithJsonError(
|
|
1237
|
-
'Error: --c4-system is required when --c4-level is components'
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
if (!opts.c4Container) {
|
|
1241
|
-
exitWithJsonError(
|
|
1242
|
-
'Error: --c4-container is required when --c4-level is components'
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
1302
|
const { svg } = await render(content, {
|
|
1248
1303
|
theme: opts.theme,
|
|
1249
1304
|
palette: opts.palette,
|
|
1250
|
-
c4Level: opts.c4Level,
|
|
1251
|
-
c4System: opts.c4System,
|
|
1252
|
-
c4Container: opts.c4Container,
|
|
1253
|
-
tagGroup: opts.tagGroup,
|
|
1254
1305
|
});
|
|
1255
1306
|
|
|
1256
1307
|
if (!svg) {
|
package/src/completion.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { extractSymbols as extractFlowchartSymbols } from './graph/flowchart-par
|
|
|
16
16
|
import { extractSymbols as extractInfraSymbols } from './infra/parser';
|
|
17
17
|
import { extractSymbols as extractClassSymbols } from './class/parser';
|
|
18
18
|
import { parseFirstLine, ALL_CHART_TYPES } from './utils/parsing';
|
|
19
|
+
import { CHART_TYPE_DESCRIPTIONS } from './dgmo-router';
|
|
19
20
|
|
|
20
21
|
// ============================================================
|
|
21
22
|
// Symbol extraction
|
|
@@ -237,10 +238,6 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
237
238
|
description: 'Show activation bars',
|
|
238
239
|
values: ['on', 'off'],
|
|
239
240
|
},
|
|
240
|
-
'collapse-notes': {
|
|
241
|
-
description: 'Collapse note blocks',
|
|
242
|
-
values: ['yes', 'no'],
|
|
243
|
-
},
|
|
244
241
|
'active-tag': { description: 'Active tag group name' },
|
|
245
242
|
}),
|
|
246
243
|
],
|
|
@@ -381,56 +378,20 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
381
378
|
persona: { description: 'Define the journey persona' },
|
|
382
379
|
}),
|
|
383
380
|
],
|
|
381
|
+
[
|
|
382
|
+
'pyramid',
|
|
383
|
+
withGlobals({
|
|
384
|
+
inverted: { description: 'Flip apex to the bottom (funnel orientation)' },
|
|
385
|
+
color: { description: 'Override layer color (pipe metadata)' },
|
|
386
|
+
description: { description: 'Layer description (pipe or indented body)' },
|
|
387
|
+
}),
|
|
388
|
+
],
|
|
384
389
|
]);
|
|
385
390
|
|
|
386
391
|
// ============================================================
|
|
387
392
|
// Chart types array (for chart type completion popup)
|
|
388
393
|
// ============================================================
|
|
389
394
|
|
|
390
|
-
const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
391
|
-
// Data charts
|
|
392
|
-
bar: 'Bar chart',
|
|
393
|
-
line: 'Line chart',
|
|
394
|
-
pie: 'Pie chart',
|
|
395
|
-
doughnut: 'Doughnut chart',
|
|
396
|
-
area: 'Area chart',
|
|
397
|
-
'polar-area': 'Polar area chart',
|
|
398
|
-
radar: 'Radar chart',
|
|
399
|
-
'bar-stacked': 'Stacked bar chart',
|
|
400
|
-
// Extended charts
|
|
401
|
-
scatter: 'Scatter plot',
|
|
402
|
-
heatmap: 'Heatmap',
|
|
403
|
-
sankey: 'Sankey flow diagram',
|
|
404
|
-
chord: 'Chord diagram',
|
|
405
|
-
funnel: 'Funnel chart',
|
|
406
|
-
function: 'Mathematical function plot',
|
|
407
|
-
// Visualizations
|
|
408
|
-
slope: 'Slope chart',
|
|
409
|
-
wordcloud: 'Word cloud',
|
|
410
|
-
arc: 'Arc diagram',
|
|
411
|
-
timeline: 'Timeline',
|
|
412
|
-
venn: 'Venn diagram',
|
|
413
|
-
quadrant: 'Quadrant chart',
|
|
414
|
-
// Diagrams
|
|
415
|
-
sequence: 'Sequence diagram',
|
|
416
|
-
flowchart: 'Flowchart',
|
|
417
|
-
class: 'Class diagram',
|
|
418
|
-
er: 'Entity-relationship diagram',
|
|
419
|
-
org: 'Organization chart',
|
|
420
|
-
kanban: 'Kanban board',
|
|
421
|
-
c4: 'C4 architecture diagram',
|
|
422
|
-
state: 'State diagram',
|
|
423
|
-
sitemap: 'Sitemap diagram',
|
|
424
|
-
infra: 'Infrastructure diagram',
|
|
425
|
-
gantt: 'Gantt chart',
|
|
426
|
-
'boxes-and-lines': 'Boxes and lines diagram',
|
|
427
|
-
mindmap: 'Mindmap diagram',
|
|
428
|
-
wireframe: 'UI wireframe diagram',
|
|
429
|
-
'tech-radar': 'Technology adoption radar (ThoughtWorks style)',
|
|
430
|
-
cycle: 'Cycle diagram (circular process flow)',
|
|
431
|
-
'journey-map': 'User journey map with emotion curve',
|
|
432
|
-
};
|
|
433
|
-
|
|
434
395
|
/** All chart types with descriptions, for chart type autocomplete. Excludes `multi-line` alias. */
|
|
435
396
|
export const CHART_TYPES: ReadonlyArray<{ name: string; description: string }> =
|
|
436
397
|
[...ALL_CHART_TYPES]
|
package/src/cycle/layout.ts
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
// Cycle Diagram — Layout Engine
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_EDGE_WIDTH,
|
|
7
|
+
MIN_EDGE_WIDTH,
|
|
8
|
+
arrowHeadLength,
|
|
9
|
+
type ParsedCycle,
|
|
10
|
+
type CycleLayoutNode,
|
|
11
|
+
type CycleLayoutEdge,
|
|
12
|
+
type CycleLayoutResult,
|
|
10
13
|
} from './types';
|
|
11
14
|
|
|
12
15
|
/** Minimum arc angle in radians (~15°) to keep arcs readable. */
|
|
@@ -519,11 +522,6 @@ function circleRectExitAngle(
|
|
|
519
522
|
return (insideAngle + outsideAngle) / 2;
|
|
520
523
|
}
|
|
521
524
|
|
|
522
|
-
/** Default edge stroke width (must match renderer). */
|
|
523
|
-
const DEFAULT_EDGE_WIDTH = 3;
|
|
524
|
-
/** Arrowhead marker width in stroke-width units (must match renderer). */
|
|
525
|
-
const ARROWHEAD_MARKER_W = 8;
|
|
526
|
-
|
|
527
525
|
/** Compute edge paths for all edges in the parsed diagram. */
|
|
528
526
|
function computeEdgePaths(
|
|
529
527
|
layoutNodes: CycleLayoutNode[],
|
|
@@ -536,9 +534,13 @@ function computeEdgePaths(
|
|
|
536
534
|
return parsed.edges.map((edge) => {
|
|
537
535
|
const src = layoutNodes[edge.sourceIndex];
|
|
538
536
|
const tgt = layoutNodes[edge.targetIndex];
|
|
539
|
-
const strokeWidth =
|
|
540
|
-
|
|
541
|
-
|
|
537
|
+
const strokeWidth = Math.max(
|
|
538
|
+
edge.width ?? DEFAULT_EDGE_WIDTH,
|
|
539
|
+
MIN_EDGE_WIDTH
|
|
540
|
+
);
|
|
541
|
+
// Arrowhead effective reach: full length minus the 10% overlap that
|
|
542
|
+
// slides the marker back to cover the stroke/arrowhead junction line.
|
|
543
|
+
const arrowLen = arrowHeadLength(strokeWidth) * 0.9;
|
|
542
544
|
const { path, labelX, labelY, labelAngle } = buildEdgeArc(
|
|
543
545
|
src,
|
|
544
546
|
tgt,
|
|
@@ -578,7 +580,7 @@ function fitToCanvas(
|
|
|
578
580
|
height: number,
|
|
579
581
|
_isClockwise: boolean
|
|
580
582
|
): { radius: number } | null {
|
|
581
|
-
const PADDING =
|
|
583
|
+
const PADDING = 30;
|
|
582
584
|
let contentMinX = Infinity,
|
|
583
585
|
contentMaxX = -Infinity;
|
|
584
586
|
let contentMinY = Infinity,
|
|
@@ -672,20 +674,9 @@ function buildEdgeArc(
|
|
|
672
674
|
): { path: string; labelX: number; labelY: number; labelAngle: number } {
|
|
673
675
|
const dir = isClockwise ? 1 : -1;
|
|
674
676
|
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
: circleRectExitAngle(
|
|
679
|
-
src.x,
|
|
680
|
-
src.y,
|
|
681
|
-
src.width / 2,
|
|
682
|
-
src.height / 2,
|
|
683
|
-
cx,
|
|
684
|
-
cy,
|
|
685
|
-
radius,
|
|
686
|
-
src.angle,
|
|
687
|
-
dir
|
|
688
|
-
);
|
|
677
|
+
// Start arc from the source node's center angle — the node renders on top
|
|
678
|
+
// of the edge, so the overlap is hidden and there's no visible gap.
|
|
679
|
+
const startAngle = src.angle;
|
|
689
680
|
|
|
690
681
|
// Find where the cycle circle exits the target node
|
|
691
682
|
const nodeEndAngle = tgt.isCircle
|
package/src/cycle/renderer.ts
CHANGED
|
@@ -23,13 +23,15 @@ import { renderInlineText } from '../utils/inline-markdown';
|
|
|
23
23
|
import type { PaletteColors } from '../palettes';
|
|
24
24
|
import type { D3ExportDimensions } from '../utils/d3-types';
|
|
25
25
|
import type { CompactViewState } from '../sharing';
|
|
26
|
-
import
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_EDGE_WIDTH,
|
|
28
|
+
MIN_EDGE_WIDTH,
|
|
29
|
+
arrowHeadLength,
|
|
30
|
+
type ParsedCycle,
|
|
31
|
+
} from './types';
|
|
27
32
|
import { computeCycleLayout } from './layout';
|
|
28
33
|
|
|
29
34
|
// ── Constants ────────────────────────────────────────────────
|
|
30
|
-
const DEFAULT_EDGE_WIDTH = 3;
|
|
31
|
-
const ARROWHEAD_W = 8;
|
|
32
|
-
const ARROWHEAD_H = 8;
|
|
33
35
|
const NODE_FONT_SIZE = 13;
|
|
34
36
|
const DESC_FONT_SIZE = 11;
|
|
35
37
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
@@ -183,22 +185,28 @@ export function renderCycle(
|
|
|
183
185
|
// Resolve default node color: first palette color (uniform)
|
|
184
186
|
const defaultNodeColor = palette.primary;
|
|
185
187
|
|
|
186
|
-
// ── Arrowhead markers ──
|
|
187
|
-
const
|
|
188
|
-
for (
|
|
189
|
-
const edge = parsed.edges[i];
|
|
188
|
+
// ── Arrowhead markers (per color+width, markerUnits=strokeWidth) ──
|
|
189
|
+
const markerKeys = new Set<string>();
|
|
190
|
+
for (const edge of parsed.edges) {
|
|
190
191
|
const color = resolveEdgeColor(edge, parsed, palette, defaultNodeColor);
|
|
191
|
-
|
|
192
|
+
const sw = Math.max(edge.width ?? DEFAULT_EDGE_WIDTH, MIN_EDGE_WIDTH);
|
|
193
|
+
const key = `${color}|${sw}`;
|
|
194
|
+
if (!markerKeys.has(key)) {
|
|
195
|
+
markerKeys.add(key);
|
|
196
|
+
ensureArrowMarker(defs, color, sw);
|
|
197
|
+
}
|
|
192
198
|
}
|
|
193
|
-
ensureArrowMarkers(defs, arrowColors);
|
|
194
199
|
|
|
195
200
|
// ── Render edges (below nodes) ──
|
|
196
201
|
for (let i = 0; i < layout.edges.length; i++) {
|
|
197
202
|
const le = layout.edges[i];
|
|
198
203
|
const edge = parsed.edges[i];
|
|
199
204
|
const color = resolveEdgeColor(edge, parsed, palette, defaultNodeColor);
|
|
200
|
-
const strokeWidth =
|
|
201
|
-
|
|
205
|
+
const strokeWidth = Math.max(
|
|
206
|
+
edge.width ?? DEFAULT_EDGE_WIDTH,
|
|
207
|
+
MIN_EDGE_WIDTH
|
|
208
|
+
);
|
|
209
|
+
const markerId = arrowMarkerId(color, strokeWidth);
|
|
202
210
|
|
|
203
211
|
const edgeG = g.append('g').attr('class', 'cycle-edge');
|
|
204
212
|
|
|
@@ -514,26 +522,45 @@ function resolveEdgeColor(
|
|
|
514
522
|
return defaultNodeColor;
|
|
515
523
|
}
|
|
516
524
|
|
|
517
|
-
|
|
525
|
+
/** Stable marker ID for a (color, strokeWidth) pair. */
|
|
526
|
+
function arrowMarkerId(color: string, strokeWidth: number): string {
|
|
527
|
+
return `cycle-arrow-${color.replace('#', '')}-w${strokeWidth}`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Create an arrowhead marker using markerUnits="strokeWidth" (SVG default)
|
|
532
|
+
* with per-edge dimensions. The marker base automatically equals the stroke
|
|
533
|
+
* width — no gaps or lollipop effects. Marker dimensions are computed so
|
|
534
|
+
* the rendered arrowhead length follows a sublinear formula:
|
|
535
|
+
*
|
|
536
|
+
* rendered length = markerWidth × strokeWidth = arrowHeadLength(sw)
|
|
537
|
+
* → markerWidth = arrowHeadLength(sw) / sw
|
|
538
|
+
*
|
|
539
|
+
* The height is fixed at 1 strokeWidth unit so the base = stroke width.
|
|
540
|
+
*/
|
|
541
|
+
function ensureArrowMarker(
|
|
518
542
|
defs: d3Selection.Selection<SVGDefsElement, unknown, null, undefined>,
|
|
519
|
-
|
|
543
|
+
color: string,
|
|
544
|
+
strokeWidth: number
|
|
520
545
|
): void {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
546
|
+
const id = arrowMarkerId(color, strokeWidth);
|
|
547
|
+
// Marker dimensions in strokeWidth units.
|
|
548
|
+
// Rendered size = mw × sw (length) and mh × sw (height).
|
|
549
|
+
const mw = arrowHeadLength(strokeWidth) / strokeWidth;
|
|
550
|
+
// Height proportional to length (½ ratio) but at least 1.5× stroke width
|
|
551
|
+
// so the arrowhead is always visibly wider than the stroke.
|
|
552
|
+
const mh = Math.max(1.5, mw * 0.5);
|
|
553
|
+
|
|
554
|
+
defs
|
|
555
|
+
.append('marker')
|
|
556
|
+
.attr('id', id)
|
|
557
|
+
.attr('viewBox', `0 0 ${mw} ${mh}`)
|
|
558
|
+
.attr('refX', mw * 0.1)
|
|
559
|
+
.attr('refY', mh / 2)
|
|
560
|
+
.attr('markerWidth', mw)
|
|
561
|
+
.attr('markerHeight', mh)
|
|
562
|
+
.attr('orient', 'auto')
|
|
563
|
+
.append('polygon')
|
|
564
|
+
.attr('points', `0,0 ${mw},${mh / 2} 0,${mh}`)
|
|
565
|
+
.attr('fill', color);
|
|
539
566
|
}
|
package/src/cycle/types.ts
CHANGED
|
@@ -64,6 +64,27 @@ export interface CycleLayoutEdge {
|
|
|
64
64
|
label?: string;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// ============================================================
|
|
68
|
+
// Shared arrow-sizing helpers (used by both layout + renderer)
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
/** Default edge stroke width. */
|
|
72
|
+
export const DEFAULT_EDGE_WIDTH = 3;
|
|
73
|
+
/** Minimum rendered stroke width — thinner strokes produce unusable arrowheads. */
|
|
74
|
+
export const MIN_EDGE_WIDTH = 2;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compute the desired arrowhead length in user-space pixels using sublinear
|
|
78
|
+
* scaling. The renderer uses markerUnits="strokeWidth" with computed marker
|
|
79
|
+
* dimensions so the arrowhead base always matches the stroke width (no gaps,
|
|
80
|
+
* no lollipop effect) while the rendered length follows this formula.
|
|
81
|
+
*/
|
|
82
|
+
const BASE_ARROW_SIZE = 8;
|
|
83
|
+
const ARROW_SCALE = 6;
|
|
84
|
+
export function arrowHeadLength(strokeWidth: number): number {
|
|
85
|
+
return BASE_ARROW_SIZE + ARROW_SCALE * Math.sqrt(strokeWidth);
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
export interface CycleLayoutResult {
|
|
68
89
|
nodes: CycleLayoutNode[];
|
|
69
90
|
edges: CycleLayoutEdge[];
|