@diagrammo/dgmo 0.8.18 → 0.8.20
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/dist/cli.cjs +89 -130
- package/dist/index.cjs +1202 -993
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +216 -114
- package/dist/index.d.ts +216 -114
- package/dist/index.js +1211 -985
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +73 -0
- package/package.json +22 -9
- package/src/boxes-and-lines/parser.ts +8 -3
- package/src/c4/parser.ts +8 -7
- package/src/class/parser.ts +6 -0
- package/src/cli.ts +1 -9
- package/src/d3.ts +16 -234
- package/src/dgmo-router.ts +97 -5
- package/src/diagnostics.ts +16 -6
- package/src/echarts.ts +43 -10
- package/src/er/parser.ts +22 -2
- package/src/gantt/renderer.ts +153 -91
- package/src/graph/flowchart-parser.ts +89 -52
- package/src/graph/state-parser.ts +60 -35
- package/src/index.ts +23 -18
- package/src/infra/parser.ts +9 -2
- package/src/kanban/renderer.ts +2 -2
- package/src/palettes/color-utils.ts +4 -12
- package/src/palettes/index.ts +0 -4
- package/src/render.ts +30 -16
- package/src/sequence/collapse.ts +169 -0
- package/src/sequence/parser.ts +21 -4
- package/src/sequence/renderer.ts +198 -52
- package/src/sharing.ts +86 -49
- package/src/sitemap/renderer.ts +1 -6
- package/src/utils/arrows.ts +180 -11
- package/src/utils/d3-types.ts +4 -0
- package/src/utils/legend-constants.ts +11 -4
- package/src/utils/legend-d3.ts +171 -0
- package/src/utils/legend-layout.ts +140 -13
- package/src/utils/legend-types.ts +45 -0
- package/src/utils/time-ticks.ts +213 -0
- package/src/branding.ts +0 -67
- package/src/dgmo-mermaid.ts +0 -262
- package/src/palettes/mermaid-bridge.ts +0 -220
|
@@ -114,6 +114,79 @@ no-option-name // off
|
|
|
114
114
|
- Bare keyword = on; `no-` prefix = off
|
|
115
115
|
- Must appear before diagram content
|
|
116
116
|
|
|
117
|
+
### 1.9 In-Arrow Message Labels
|
|
118
|
+
|
|
119
|
+
An **in-arrow label** is the text embedded inside an arrow between the opening delimiter and the arrow token, as in `A -label-> B`.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
A -label-> B
|
|
123
|
+
^ ^---^ ^^
|
|
124
|
+
| | ||
|
|
125
|
+
| | |+- destination id
|
|
126
|
+
| | +- arrow token
|
|
127
|
+
| +- label text (plain, no markdown)
|
|
128
|
+
+- opening delimiter (matches arrow type)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Chart types that support in-arrow labels**: sequence, flowchart, state, infra, c4, er, class, boxes-and-lines.
|
|
132
|
+
|
|
133
|
+
#### Cheat sheet
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
// happy-path: labels are plain text with punctuation allowed
|
|
137
|
+
A -location[]-> B // label = "location[]"
|
|
138
|
+
A -a[b]c-> B // label = "a[b]c"
|
|
139
|
+
A -{json}-> B // label = "{json}"
|
|
140
|
+
|
|
141
|
+
// unicode: all scripts and emoji preserved verbatim
|
|
142
|
+
A -café-> B
|
|
143
|
+
A -日本語-> B
|
|
144
|
+
A -🎉-> B
|
|
145
|
+
|
|
146
|
+
// punctuation is literal — no markdown interpretation
|
|
147
|
+
A -(parenthetical)-> B // label = "(parenthetical)" (NOT a color)
|
|
148
|
+
A -*emphasis*-> B // label = "*emphasis*" (NOT bold)
|
|
149
|
+
A -`code`-> B // label = "`code`" (NOT a code span)
|
|
150
|
+
|
|
151
|
+
// forbidden: -> and ~> substrings inside a label
|
|
152
|
+
A -uses -> chain-> B // ERROR (E_ARROW_SUBSTRING_IN_LABEL)
|
|
153
|
+
// migration: move the label to the post-colon form
|
|
154
|
+
A -> B: uses -> chain // works for charts that accept post-colon labels
|
|
155
|
+
|
|
156
|
+
// migration from pre-gauntlet (legacy) syntax
|
|
157
|
+
A -Makes calls [HTTP]-> B // label is now the FULL "Makes calls [HTTP]"
|
|
158
|
+
A -Makes calls-> B | tech: HTTP // preferred: technology on target metadata
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Character-set contract
|
|
162
|
+
|
|
163
|
+
- **Allowed**: any Unicode codepoint except the forbidden list below. Brackets `[] {} ()`, pipes `|`, quotes `"' `, backticks, punctuation, digits, emoji, ZWJ sequences, combining marks — all pass through as literal characters.
|
|
164
|
+
- **Forbidden substrings**: `->` and `~>`. These terminate the arrow. If you need them inside a label, use the post-colon form (`A -> B: uses -> to chain`) on chart types that support it; there is no escape mechanism.
|
|
165
|
+
- **Forbidden characters**: C0 control characters U+0000–U+001F except U+0009 (tab), and U+007F (DEL). Silent renderer breakage and log-injection surface — no legitimate use case.
|
|
166
|
+
- **Whitespace**: leading and trailing whitespace is trimmed; internal whitespace runs (including tabs, non-breaking spaces, and zero-width spaces) are **preserved**, never collapsed.
|
|
167
|
+
- **Plain text only**: no markdown interpretation. `*foo*` renders as `*foo*`, not italicized. `[label](url)` renders as literal `[label](url)`, not a hyperlink. Clickable URLs belong in notes, not in in-arrow labels.
|
|
168
|
+
- **HTML-safe**: all renderers emit label text as a DOM text node. `<script>alert(1)</script>` renders as literal text — the entire label is a sequence of codepoints, not a markup fragment.
|
|
169
|
+
|
|
170
|
+
#### Color suffix (flowchart and state only)
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
A -(red)-> B // colored edge, no label
|
|
174
|
+
A -(notacolor)-> B // label = "(notacolor)" (fall-through)
|
|
175
|
+
A -(red) uses-> B // label = "(red) uses" (combined form not supported)
|
|
176
|
+
A -red-> B // label = "red" (bare word is always a label)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A parenthesized palette color is only recognized when the entire label between the opening `-` and the arrow token is exactly `(colorName)` and `colorName` is one of the 11 names in §1.5. Any other content falls through to the label. To combine a color and a label, use the post-colon or pipe-metadata form instead.
|
|
180
|
+
|
|
181
|
+
#### Migrating from pre-gauntlet syntax
|
|
182
|
+
|
|
183
|
+
Two legacy forms changed with this spec:
|
|
184
|
+
|
|
185
|
+
1. **C4 trailing `[technology]` sugar is removed.** A C4 arrow like `-Makes calls [HTTPS]-> API` used to extract `HTTPS` as the technology annotation. The full `Makes calls [HTTPS]` is now the label. Use the post-colon or pipe form for technology: `-Makes calls-> API | tech: HTTPS`.
|
|
186
|
+
2. **Bare palette color suffixes are a literal label.** `A -red-> B` on flowchart/state used to be accepted as a bare color suffix in some surfaces. It is now always a label with text `red`. To color an edge, use the `-(red)->` parens form.
|
|
187
|
+
|
|
188
|
+
No code migration is required for in-arrow label character escaping — any label that was valid before remains valid, with one exception: if your label happened to contain the literal substring `->` or `~>`, the parser now rejects it with `E_ARROW_SUBSTRING_IN_LABEL`. Move those labels to the post-colon form.
|
|
189
|
+
|
|
117
190
|
---
|
|
118
191
|
|
|
119
192
|
## 2. Sequence Diagrams
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diagrammo/dgmo",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.20",
|
|
4
4
|
"description": "DGMO diagram markup language — parser, renderer, and color system",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -55,10 +55,12 @@
|
|
|
55
55
|
],
|
|
56
56
|
"sideEffects": false,
|
|
57
57
|
"scripts": {
|
|
58
|
-
"
|
|
58
|
+
"codegen": "lezer-generator src/editor/dgmo.grammar -o src/editor/dgmo.grammar.js",
|
|
59
|
+
"prebuild": "rm -rf dist && pnpm codegen",
|
|
59
60
|
"build": "tsup",
|
|
60
61
|
"typecheck": "tsc --noEmit",
|
|
61
62
|
"dev": "tsup --watch",
|
|
63
|
+
"pretest": "pnpm codegen",
|
|
62
64
|
"test": "vitest run",
|
|
63
65
|
"test:watch": "vitest",
|
|
64
66
|
"gallery": "pnpm build && node scripts/generate-gallery.mjs",
|
|
@@ -69,7 +71,13 @@
|
|
|
69
71
|
"check:duplication": "jscpd ./src",
|
|
70
72
|
"check:deadcode": "knip",
|
|
71
73
|
"check:spelling": "cspell \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
74
|
+
"check:all": "pnpm check:deadcode && pnpm check:spelling && pnpm check:duplication && pnpm check:circular && pnpm check:deps && pnpm check:security && pnpm build && pnpm check:publish && pnpm check:types",
|
|
75
|
+
"check:circular": "madge --circular --extensions ts src/ --json | node -e \"const c=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); const n=c.length; if(n>7){console.error('New circular deps found ('+n+' > 7 known type-only cycles)');process.exit(1)}else if(n>0){console.log(n+' known type-only/dynamic cycles (safe)')}else{console.log('No circular dependencies')}\"",
|
|
76
|
+
"check:deps": "depcheck --ignores='@codemirror/language,@lezer/*,husky,lint-staged,tsup'",
|
|
77
|
+
"check:security": "pnpm audit --prod",
|
|
78
|
+
"check:publish": "publint",
|
|
72
79
|
"check:size": "pnpm build && du -sh dist/ && echo '---' && ls -lh dist/*.js dist/*.cjs",
|
|
80
|
+
"check:types": "attw --pack . --ignore-rules no-resolution",
|
|
73
81
|
"postinstall": "node -e \"console.log('\\n💡 Claude Code user? Run: dgmo --install-claude-skill\\n')\"",
|
|
74
82
|
"prepare": "husky"
|
|
75
83
|
},
|
|
@@ -83,10 +91,11 @@
|
|
|
83
91
|
"d3-selection": "^3.0.0",
|
|
84
92
|
"d3-shape": "^3.2.0",
|
|
85
93
|
"echarts": "^6.0.0",
|
|
86
|
-
"jsdom": "^29.0.
|
|
94
|
+
"jsdom": "^29.0.2",
|
|
87
95
|
"lz-string": "^1.5.0"
|
|
88
96
|
},
|
|
89
97
|
"devDependencies": {
|
|
98
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
90
99
|
"@codemirror/language": "^6.12.3",
|
|
91
100
|
"@eslint/js": "^10.0.1",
|
|
92
101
|
"@lezer/generator": "^1.8.0",
|
|
@@ -97,18 +106,22 @@
|
|
|
97
106
|
"@types/d3-selection": "^3.0.11",
|
|
98
107
|
"@types/d3-shape": "^3.1.8",
|
|
99
108
|
"@types/jsdom": "^28.0.1",
|
|
100
|
-
"cspell": "^
|
|
109
|
+
"cspell": "^10.0.0",
|
|
110
|
+
"depcheck": "^1.4.7",
|
|
101
111
|
"esbuild": "^0.28.0",
|
|
102
112
|
"eslint": "^10.2.0",
|
|
113
|
+
"eslint-plugin-security": "^4.0.0",
|
|
103
114
|
"husky": "^9.1.7",
|
|
104
|
-
"jscpd": "^4.0.
|
|
105
|
-
"knip": "^6.3.
|
|
115
|
+
"jscpd": "^4.0.9",
|
|
116
|
+
"knip": "^6.3.1",
|
|
106
117
|
"lint-staged": "^16.4.0",
|
|
107
|
-
"
|
|
118
|
+
"madge": "^8.0.0",
|
|
119
|
+
"prettier": "^3.8.2",
|
|
120
|
+
"publint": "^0.3.18",
|
|
108
121
|
"tsup": "^8.5.1",
|
|
109
122
|
"typescript": "^6.0.2",
|
|
110
|
-
"typescript-eslint": "^8.58.
|
|
111
|
-
"vitest": "^4.1.
|
|
123
|
+
"typescript-eslint": "^8.58.1",
|
|
124
|
+
"vitest": "^4.1.4"
|
|
112
125
|
},
|
|
113
126
|
"lint-staged": {
|
|
114
127
|
"*.ts": [
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { makeDgmoError, suggest } from '../diagnostics';
|
|
6
6
|
import type { DgmoError } from '../diagnostics';
|
|
7
|
+
import { parseInArrowLabel } from '../utils/arrows';
|
|
7
8
|
import type { ParsedBoxesAndLines, BLNode, BLEdge, BLGroup } from './types';
|
|
8
9
|
import {
|
|
9
10
|
matchTagBlockHeading,
|
|
@@ -607,7 +608,9 @@ function parseEdgeLine(
|
|
|
607
608
|
const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
|
|
608
609
|
if (biLabeledMatch) {
|
|
609
610
|
const source = resolveEndpoint(biLabeledMatch[1].trim());
|
|
610
|
-
const
|
|
611
|
+
const labelResult = parseInArrowLabel(biLabeledMatch[2], lineNum);
|
|
612
|
+
diagnostics.push(...labelResult.diagnostics);
|
|
613
|
+
const label = labelResult.label;
|
|
611
614
|
let rest = biLabeledMatch[3].trim();
|
|
612
615
|
|
|
613
616
|
let metadata: Record<string, string> = {};
|
|
@@ -631,7 +634,7 @@ function parseEdgeLine(
|
|
|
631
634
|
return {
|
|
632
635
|
source,
|
|
633
636
|
target: resolveEndpoint(rest),
|
|
634
|
-
label
|
|
637
|
+
label,
|
|
635
638
|
bidirectional: true,
|
|
636
639
|
lineNumber: lineNum,
|
|
637
640
|
metadata,
|
|
@@ -675,7 +678,9 @@ function parseEdgeLine(
|
|
|
675
678
|
const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
|
|
676
679
|
if (labeledMatch) {
|
|
677
680
|
const source = resolveEndpoint(labeledMatch[1].trim());
|
|
678
|
-
const
|
|
681
|
+
const labelResult = parseInArrowLabel(labeledMatch[2], lineNum);
|
|
682
|
+
diagnostics.push(...labelResult.diagnostics);
|
|
683
|
+
const label = labelResult.label;
|
|
679
684
|
let rest = labeledMatch[3].trim();
|
|
680
685
|
|
|
681
686
|
if (label) {
|
package/src/c4/parser.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { PaletteColors } from '../palettes';
|
|
6
6
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
7
|
+
import { parseInArrowLabel } from '../utils/arrows';
|
|
7
8
|
import type { TagGroup } from '../utils/tag-groups';
|
|
8
9
|
import {
|
|
9
10
|
matchTagBlockHeading,
|
|
@@ -519,14 +520,14 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
519
520
|
break;
|
|
520
521
|
}
|
|
521
522
|
|
|
522
|
-
//
|
|
523
|
-
|
|
523
|
+
// TD-5: the trailing `[tech]` sugar is no longer extracted from the
|
|
524
|
+
// in-arrow label. The entire label stays as the label. Technology
|
|
525
|
+
// metadata comes from post-colon or pipe metadata on the target.
|
|
526
|
+
// Also run TD-13/TD-14 validation on the label characters.
|
|
527
|
+
const labelResult = parseInArrowLabel(rawLabel, lineNumber);
|
|
528
|
+
labelResult.diagnostics.forEach((d) => result.diagnostics.push(d));
|
|
529
|
+
const label: string | undefined = labelResult.label;
|
|
524
530
|
let technology: string | undefined;
|
|
525
|
-
const techMatch = rawLabel.match(/\[([^\]]+)\]\s*$/);
|
|
526
|
-
if (techMatch) {
|
|
527
|
-
label = rawLabel.substring(0, techMatch.index!).trim() || undefined;
|
|
528
|
-
technology = techMatch[1].trim();
|
|
529
|
-
}
|
|
530
531
|
|
|
531
532
|
// Extract pipe metadata from target body (e.g. "Database | tech: SQL")
|
|
532
533
|
let target = targetBody;
|
package/src/class/parser.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveColorWithDiagnostic } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
3
|
import { makeDgmoError, formatDgmoError } from '../diagnostics';
|
|
4
|
+
import { validateLabelCharacters } from '../utils/arrows';
|
|
4
5
|
import {
|
|
5
6
|
measureIndent,
|
|
6
7
|
parseFirstLine,
|
|
@@ -251,6 +252,11 @@ export function parseClassDiagram(
|
|
|
251
252
|
|
|
252
253
|
getOrCreateClass(targetName, lineNumber);
|
|
253
254
|
|
|
255
|
+
if (label) {
|
|
256
|
+
result.diagnostics.push(
|
|
257
|
+
...validateLabelCharacters(label, lineNumber)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
254
260
|
result.relationships.push({
|
|
255
261
|
source: currentClass.id,
|
|
256
262
|
target: classId(targetName),
|
package/src/cli.ts
CHANGED
|
@@ -160,7 +160,6 @@ Key options:
|
|
|
160
160
|
- \`--theme <theme>\` — \`light\` (default), \`dark\`, \`transparent\`
|
|
161
161
|
- \`--palette <name>\` — \`nord\` (default), \`solarized\`, \`catppuccin\`, \`rose-pine\`, \`gruvbox\`, \`tokyo-night\`, \`one-dark\`, \`bold\`
|
|
162
162
|
- \`--copy\` — copy the URL to clipboard (use with \`-o url\`)
|
|
163
|
-
- \`--branding\` — add diagrammo.app branding to exports
|
|
164
163
|
- \`--chart-types\` — list all supported chart types
|
|
165
164
|
|
|
166
165
|
## Supported Chart Types
|
|
@@ -502,7 +501,6 @@ Options:
|
|
|
502
501
|
--c4-system <name> System to drill into (with --c4-level containers or components)
|
|
503
502
|
--c4-container <name> Container to drill into (with --c4-level components)
|
|
504
503
|
--tag-group <name> Pre-select a tag group for static export coloring
|
|
505
|
-
--branding Add diagrammo.app branding to exports
|
|
506
504
|
--copy Copy URL to clipboard (only with -o url)
|
|
507
505
|
--json Output structured JSON to stdout
|
|
508
506
|
--chart-types List all supported chart types
|
|
@@ -532,7 +530,6 @@ function parseArgs(argv: string[]): {
|
|
|
532
530
|
palette: string;
|
|
533
531
|
help: boolean;
|
|
534
532
|
version: boolean;
|
|
535
|
-
branding: boolean;
|
|
536
533
|
copy: boolean;
|
|
537
534
|
json: boolean;
|
|
538
535
|
chartTypes: boolean;
|
|
@@ -553,7 +550,6 @@ function parseArgs(argv: string[]): {
|
|
|
553
550
|
palette: 'nord',
|
|
554
551
|
help: false,
|
|
555
552
|
version: false,
|
|
556
|
-
branding: false,
|
|
557
553
|
copy: false,
|
|
558
554
|
json: false,
|
|
559
555
|
chartTypes: false,
|
|
@@ -637,9 +633,6 @@ function parseArgs(argv: string[]): {
|
|
|
637
633
|
} else if (arg === '--tag-group') {
|
|
638
634
|
result.tagGroup = args[++i];
|
|
639
635
|
i++;
|
|
640
|
-
} else if (arg === '--branding') {
|
|
641
|
-
result.branding = true;
|
|
642
|
-
i++;
|
|
643
636
|
} else if (arg === '--json') {
|
|
644
637
|
result.json = true;
|
|
645
638
|
i++;
|
|
@@ -1251,10 +1244,9 @@ async function main(): Promise<void> {
|
|
|
1251
1244
|
}
|
|
1252
1245
|
}
|
|
1253
1246
|
|
|
1254
|
-
const svg = await render(content, {
|
|
1247
|
+
const { svg } = await render(content, {
|
|
1255
1248
|
theme: opts.theme,
|
|
1256
1249
|
palette: opts.palette,
|
|
1257
|
-
branding: opts.branding,
|
|
1258
1250
|
c4Level: opts.c4Level,
|
|
1259
1251
|
c4System: opts.c4System,
|
|
1260
1252
|
c4Container: opts.c4Container,
|
package/src/d3.ts
CHANGED
|
@@ -4,8 +4,9 @@ import * as d3Shape from 'd3-shape';
|
|
|
4
4
|
import * as d3Array from 'd3-array';
|
|
5
5
|
import cloud from 'd3-cloud';
|
|
6
6
|
import { FONT_FAMILY } from './fonts';
|
|
7
|
-
import { injectBranding } from './branding';
|
|
8
7
|
import { computeQuadrantPointLabels, type LabelRect } from './label-layout';
|
|
8
|
+
import { MONTH_ABBR, computeTimeTicks } from './utils/time-ticks';
|
|
9
|
+
import type { D3ExportDimensions } from './utils/d3-types';
|
|
9
10
|
|
|
10
11
|
// ============================================================
|
|
11
12
|
// Types
|
|
@@ -133,10 +134,7 @@ interface QuadrantLabels {
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
/** Optional explicit dimensions for CLI/export rendering (bypasses DOM layout). */
|
|
136
|
-
export
|
|
137
|
-
width?: number;
|
|
138
|
-
height?: number;
|
|
139
|
-
}
|
|
137
|
+
export type { D3ExportDimensions } from './utils/d3-types';
|
|
140
138
|
|
|
141
139
|
export interface ParsedVisualization {
|
|
142
140
|
type: VisualizationType | null;
|
|
@@ -331,34 +329,6 @@ export function parseTimelineDate(s: string): number {
|
|
|
331
329
|
);
|
|
332
330
|
}
|
|
333
331
|
|
|
334
|
-
/** Convert a fractional year number back to a Date (inverse of parseTimelineDate). */
|
|
335
|
-
function fractionalYearToDate(frac: number): Date {
|
|
336
|
-
const year = Math.floor(frac);
|
|
337
|
-
const remainder = frac - year;
|
|
338
|
-
// Inverse of: (month-1)/12 + (day-1)/365 + hour/8760 + minute/525600
|
|
339
|
-
const monthFrac = remainder * 12;
|
|
340
|
-
const month = Math.floor(monthFrac); // 0-based
|
|
341
|
-
const monthRemainder = remainder - month / 12;
|
|
342
|
-
const dayFrac = monthRemainder * 365; // fractional day-of-year offset
|
|
343
|
-
const day = Math.floor(dayFrac) + 1;
|
|
344
|
-
const dayRemainder = dayFrac - Math.floor(dayFrac);
|
|
345
|
-
const hourFrac = dayRemainder * 24;
|
|
346
|
-
const hour = Math.floor(hourFrac);
|
|
347
|
-
const minute = Math.round((hourFrac - hour) * 60);
|
|
348
|
-
return new Date(year, month, day, hour, minute);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** Convert a Date to a fractional year number. */
|
|
352
|
-
function dateToFractionalYear(d: Date): number {
|
|
353
|
-
return (
|
|
354
|
-
d.getFullYear() +
|
|
355
|
-
d.getMonth() / 12 +
|
|
356
|
-
(d.getDate() - 1) / 365 +
|
|
357
|
-
d.getHours() / 8760 +
|
|
358
|
-
d.getMinutes() / 525600
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
332
|
/**
|
|
363
333
|
* Adds a duration to a date string and returns the resulting date string.
|
|
364
334
|
* Supports: d (days), w (weeks), m (months), y (years), h (hours), min (minutes)
|
|
@@ -2885,21 +2855,6 @@ function renderMarkers(
|
|
|
2885
2855
|
// Timeline Time Scale
|
|
2886
2856
|
// ============================================================
|
|
2887
2857
|
|
|
2888
|
-
const MONTH_ABBR = [
|
|
2889
|
-
'Jan',
|
|
2890
|
-
'Feb',
|
|
2891
|
-
'Mar',
|
|
2892
|
-
'Apr',
|
|
2893
|
-
'May',
|
|
2894
|
-
'Jun',
|
|
2895
|
-
'Jul',
|
|
2896
|
-
'Aug',
|
|
2897
|
-
'Sep',
|
|
2898
|
-
'Oct',
|
|
2899
|
-
'Nov',
|
|
2900
|
-
'Dec',
|
|
2901
|
-
];
|
|
2902
|
-
|
|
2903
2858
|
/**
|
|
2904
2859
|
* Converts a DSL date string (YYYY, YYYY-MM, YYYY-MM-DD, or YYYY-MM-DD HH:MM) to a human-readable label.
|
|
2905
2860
|
* '1718' → '1718'
|
|
@@ -2947,173 +2902,6 @@ function formatBoundaryLabel(dateStr: string, otherDateStr: string): string {
|
|
|
2947
2902
|
return formatDateLabel(dateStr);
|
|
2948
2903
|
}
|
|
2949
2904
|
|
|
2950
|
-
/**
|
|
2951
|
-
* Computes adaptive tick marks for a timeline scale.
|
|
2952
|
-
* - Multi-year spans → year ticks
|
|
2953
|
-
* - Within ~1 year → month ticks
|
|
2954
|
-
* - Within ~3 months → week ticks (1st, 8th, 15th, 22nd)
|
|
2955
|
-
*
|
|
2956
|
-
* Optional boundary parameters add ticks at exact data start/end:
|
|
2957
|
-
* - boundaryStart/boundaryEnd: numeric date values
|
|
2958
|
-
* - boundaryStartLabel/boundaryEndLabel: formatted labels for those dates
|
|
2959
|
-
*/
|
|
2960
|
-
export function computeTimeTicks(
|
|
2961
|
-
domainMin: number,
|
|
2962
|
-
domainMax: number,
|
|
2963
|
-
scale: d3Scale.ScaleLinear<number, number>,
|
|
2964
|
-
boundaryStart?: number,
|
|
2965
|
-
boundaryEnd?: number,
|
|
2966
|
-
boundaryStartLabel?: string,
|
|
2967
|
-
boundaryEndLabel?: string
|
|
2968
|
-
): { pos: number; label: string }[] {
|
|
2969
|
-
const minYear = Math.floor(domainMin);
|
|
2970
|
-
const maxYear = Math.floor(domainMax);
|
|
2971
|
-
const span = domainMax - domainMin;
|
|
2972
|
-
|
|
2973
|
-
let ticks: { pos: number; label: string }[] = [];
|
|
2974
|
-
|
|
2975
|
-
// Year ticks for multi-year spans (need at least 2 boundaries)
|
|
2976
|
-
const firstYear = Math.ceil(domainMin);
|
|
2977
|
-
const lastYear = Math.floor(domainMax);
|
|
2978
|
-
if (lastYear >= firstYear + 1) {
|
|
2979
|
-
// Decimate ticks for long spans so labels don't overlap
|
|
2980
|
-
const yearSpan = lastYear - firstYear;
|
|
2981
|
-
let step = 1;
|
|
2982
|
-
if (yearSpan > 80) step = 20;
|
|
2983
|
-
else if (yearSpan > 40) step = 10;
|
|
2984
|
-
else if (yearSpan > 20) step = 5;
|
|
2985
|
-
else if (yearSpan > 10) step = 2;
|
|
2986
|
-
|
|
2987
|
-
// Align to step boundary so ticks land on round years (1700, 1710, …)
|
|
2988
|
-
const alignedFirst = Math.ceil(firstYear / step) * step;
|
|
2989
|
-
for (let y = alignedFirst; y <= lastYear; y += step) {
|
|
2990
|
-
ticks.push({ pos: scale(y), label: String(y) });
|
|
2991
|
-
}
|
|
2992
|
-
} else if (span > 0.25) {
|
|
2993
|
-
// Month ticks for spans > ~3 months
|
|
2994
|
-
const crossesYear = maxYear > minYear;
|
|
2995
|
-
for (let y = minYear; y <= maxYear + 1; y++) {
|
|
2996
|
-
for (let m = 1; m <= 12; m++) {
|
|
2997
|
-
const val = y + (m - 1) / 12;
|
|
2998
|
-
if (val > domainMax) break;
|
|
2999
|
-
if (val >= domainMin) {
|
|
3000
|
-
ticks.push({
|
|
3001
|
-
pos: scale(val),
|
|
3002
|
-
label: crossesYear
|
|
3003
|
-
? `${MONTH_ABBR[m - 1]} '${String(y).slice(-2)}`
|
|
3004
|
-
: MONTH_ABBR[m - 1],
|
|
3005
|
-
});
|
|
3006
|
-
}
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
} else if (span <= 0.000685) {
|
|
3010
|
-
// Minute ticks for spans ≤ ~6 hours
|
|
3011
|
-
// Adaptive step: >3h → 30min, >1h → 15min, >30min → 10min, else 5min
|
|
3012
|
-
let stepMin = 5;
|
|
3013
|
-
const spanHours = span * 8760;
|
|
3014
|
-
if (spanHours > 3) stepMin = 30;
|
|
3015
|
-
else if (spanHours > 1) stepMin = 15;
|
|
3016
|
-
else if (spanHours > 0.5) stepMin = 10;
|
|
3017
|
-
|
|
3018
|
-
// Iterate from the start hour boundary
|
|
3019
|
-
const startDate = fractionalYearToDate(domainMin);
|
|
3020
|
-
// Round down to nearest step boundary
|
|
3021
|
-
startDate.setMinutes(
|
|
3022
|
-
Math.floor(startDate.getMinutes() / stepMin) * stepMin,
|
|
3023
|
-
0,
|
|
3024
|
-
0
|
|
3025
|
-
);
|
|
3026
|
-
|
|
3027
|
-
while (true) {
|
|
3028
|
-
const val = dateToFractionalYear(startDate);
|
|
3029
|
-
if (val > domainMax) break;
|
|
3030
|
-
if (val >= domainMin) {
|
|
3031
|
-
const hh = String(startDate.getHours()).padStart(2, '0');
|
|
3032
|
-
const mm = String(startDate.getMinutes()).padStart(2, '0');
|
|
3033
|
-
ticks.push({ pos: scale(val), label: `${hh}:${mm}` });
|
|
3034
|
-
}
|
|
3035
|
-
startDate.setMinutes(startDate.getMinutes() + stepMin);
|
|
3036
|
-
}
|
|
3037
|
-
} else if (span <= 0.00822) {
|
|
3038
|
-
// Hour ticks for spans ≤ ~3 days
|
|
3039
|
-
// Adaptive step: >2d → 6h, >1d → 3h, >12h → 2h, else 1h
|
|
3040
|
-
let stepHour = 1;
|
|
3041
|
-
const spanHours = span * 8760;
|
|
3042
|
-
if (spanHours > 48) stepHour = 6;
|
|
3043
|
-
else if (spanHours > 24) stepHour = 3;
|
|
3044
|
-
else if (spanHours > 12) stepHour = 2;
|
|
3045
|
-
|
|
3046
|
-
// For single-day spans, just show HH:MM without the date prefix
|
|
3047
|
-
const singleDay = spanHours <= 24;
|
|
3048
|
-
|
|
3049
|
-
const startDate = fractionalYearToDate(domainMin);
|
|
3050
|
-
// Round down to nearest step boundary
|
|
3051
|
-
startDate.setHours(
|
|
3052
|
-
Math.floor(startDate.getHours() / stepHour) * stepHour,
|
|
3053
|
-
0,
|
|
3054
|
-
0,
|
|
3055
|
-
0
|
|
3056
|
-
);
|
|
3057
|
-
|
|
3058
|
-
while (true) {
|
|
3059
|
-
const val = dateToFractionalYear(startDate);
|
|
3060
|
-
if (val > domainMax) break;
|
|
3061
|
-
if (val >= domainMin) {
|
|
3062
|
-
const hh = String(startDate.getHours()).padStart(2, '0');
|
|
3063
|
-
const mm = String(startDate.getMinutes()).padStart(2, '0');
|
|
3064
|
-
if (singleDay) {
|
|
3065
|
-
ticks.push({ pos: scale(val), label: `${hh}:${mm}` });
|
|
3066
|
-
} else {
|
|
3067
|
-
const mon = MONTH_ABBR[startDate.getMonth()];
|
|
3068
|
-
const d = startDate.getDate();
|
|
3069
|
-
ticks.push({ pos: scale(val), label: `${mon} ${d} ${hh}:${mm}` });
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
startDate.setHours(startDate.getHours() + stepHour);
|
|
3073
|
-
}
|
|
3074
|
-
} else {
|
|
3075
|
-
// Week ticks for spans ≤ ~3 months (1st, 8th, 15th, 22nd of each month)
|
|
3076
|
-
for (let y = minYear; y <= maxYear + 1; y++) {
|
|
3077
|
-
for (let m = 1; m <= 12; m++) {
|
|
3078
|
-
for (const d of [1, 8, 15, 22]) {
|
|
3079
|
-
const val = y + (m - 1) / 12 + (d - 1) / 365;
|
|
3080
|
-
if (val > domainMax) break;
|
|
3081
|
-
if (val >= domainMin) {
|
|
3082
|
-
ticks.push({
|
|
3083
|
-
pos: scale(val),
|
|
3084
|
-
label: `${MONTH_ABBR[m - 1]} ${d}`,
|
|
3085
|
-
});
|
|
3086
|
-
}
|
|
3087
|
-
}
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
|
-
}
|
|
3091
|
-
|
|
3092
|
-
// Add boundary ticks at exact data start/end if provided
|
|
3093
|
-
// When a boundary tick collides with a standard tick, replace the standard tick
|
|
3094
|
-
const collisionThreshold = 40; // pixels
|
|
3095
|
-
|
|
3096
|
-
if (boundaryStart !== undefined && boundaryStartLabel) {
|
|
3097
|
-
const boundaryPos = scale(boundaryStart);
|
|
3098
|
-
// Remove any standard ticks that would collide with the start boundary
|
|
3099
|
-
ticks = ticks.filter(
|
|
3100
|
-
(t) => Math.abs(t.pos - boundaryPos) >= collisionThreshold
|
|
3101
|
-
);
|
|
3102
|
-
ticks.unshift({ pos: boundaryPos, label: boundaryStartLabel });
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
|
-
if (boundaryEnd !== undefined && boundaryEndLabel) {
|
|
3106
|
-
const boundaryPos = scale(boundaryEnd);
|
|
3107
|
-
// Remove any standard ticks that would collide with the end boundary
|
|
3108
|
-
ticks = ticks.filter(
|
|
3109
|
-
(t) => Math.abs(t.pos - boundaryPos) >= collisionThreshold
|
|
3110
|
-
);
|
|
3111
|
-
ticks.push({ pos: boundaryPos, label: boundaryEndLabel });
|
|
3112
|
-
}
|
|
3113
|
-
|
|
3114
|
-
return ticks;
|
|
3115
|
-
}
|
|
3116
|
-
|
|
3117
2905
|
/**
|
|
3118
2906
|
* Renders adaptive tick marks along the time axis.
|
|
3119
2907
|
* Optional boundary parameters add ticks at exact data start/end.
|
|
@@ -6678,8 +6466,7 @@ function createExportContainer(width: number, height: number): HTMLDivElement {
|
|
|
6678
6466
|
function finalizeSvgExport(
|
|
6679
6467
|
container: HTMLDivElement,
|
|
6680
6468
|
theme: string,
|
|
6681
|
-
palette: PaletteColors
|
|
6682
|
-
options?: { branding?: boolean }
|
|
6469
|
+
palette: PaletteColors
|
|
6683
6470
|
): string {
|
|
6684
6471
|
const svgEl = container.querySelector('svg');
|
|
6685
6472
|
if (!svgEl) return '';
|
|
@@ -6694,10 +6481,6 @@ function finalizeSvgExport(
|
|
|
6694
6481
|
svgEl.querySelectorAll('[data-export-ignore]').forEach((el) => el.remove());
|
|
6695
6482
|
const svgHtml = svgEl.outerHTML;
|
|
6696
6483
|
document.body.removeChild(container);
|
|
6697
|
-
if (options?.branding !== false) {
|
|
6698
|
-
const brandColor = theme === 'transparent' ? '#888' : palette.textMuted;
|
|
6699
|
-
return injectBranding(svgHtml, brandColor);
|
|
6700
|
-
}
|
|
6701
6484
|
return svgHtml;
|
|
6702
6485
|
}
|
|
6703
6486
|
|
|
@@ -6716,7 +6499,6 @@ export async function renderForExport(
|
|
|
6716
6499
|
swimlaneTagGroup?: string | null;
|
|
6717
6500
|
},
|
|
6718
6501
|
options?: {
|
|
6719
|
-
branding?: boolean;
|
|
6720
6502
|
c4Level?: 'context' | 'containers' | 'components' | 'deployment';
|
|
6721
6503
|
c4System?: string;
|
|
6722
6504
|
c4Container?: string;
|
|
@@ -6778,7 +6560,7 @@ export async function renderForExport(
|
|
|
6778
6560
|
activeTagGroup,
|
|
6779
6561
|
hiddenAttributes
|
|
6780
6562
|
);
|
|
6781
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6563
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
6782
6564
|
}
|
|
6783
6565
|
|
|
6784
6566
|
if (detectedType === 'sitemap') {
|
|
@@ -6832,7 +6614,7 @@ export async function renderForExport(
|
|
|
6832
6614
|
activeTagGroup,
|
|
6833
6615
|
hiddenAttributes
|
|
6834
6616
|
);
|
|
6835
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6617
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
6836
6618
|
}
|
|
6837
6619
|
|
|
6838
6620
|
if (detectedType === 'kanban') {
|
|
@@ -6856,7 +6638,7 @@ export async function renderForExport(
|
|
|
6856
6638
|
options?.tagGroup
|
|
6857
6639
|
),
|
|
6858
6640
|
});
|
|
6859
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6641
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
6860
6642
|
}
|
|
6861
6643
|
|
|
6862
6644
|
if (detectedType === 'class') {
|
|
@@ -6884,7 +6666,7 @@ export async function renderForExport(
|
|
|
6884
6666
|
undefined,
|
|
6885
6667
|
{ width: exportWidth, height: exportHeight }
|
|
6886
6668
|
);
|
|
6887
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6669
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
6888
6670
|
}
|
|
6889
6671
|
|
|
6890
6672
|
if (detectedType === 'er') {
|
|
@@ -6917,7 +6699,7 @@ export async function renderForExport(
|
|
|
6917
6699
|
options?.tagGroup
|
|
6918
6700
|
)
|
|
6919
6701
|
);
|
|
6920
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6702
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
6921
6703
|
}
|
|
6922
6704
|
|
|
6923
6705
|
if (detectedType === 'boxes-and-lines') {
|
|
@@ -6948,7 +6730,7 @@ export async function renderForExport(
|
|
|
6948
6730
|
activeTagGroup: options?.tagGroup,
|
|
6949
6731
|
}
|
|
6950
6732
|
);
|
|
6951
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6733
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
6952
6734
|
}
|
|
6953
6735
|
|
|
6954
6736
|
if (detectedType === 'c4') {
|
|
@@ -7009,7 +6791,7 @@ export async function renderForExport(
|
|
|
7009
6791
|
options?.tagGroup
|
|
7010
6792
|
)
|
|
7011
6793
|
);
|
|
7012
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6794
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
7013
6795
|
}
|
|
7014
6796
|
|
|
7015
6797
|
if (detectedType === 'flowchart') {
|
|
@@ -7033,7 +6815,7 @@ export async function renderForExport(
|
|
|
7033
6815
|
undefined,
|
|
7034
6816
|
{ width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
|
|
7035
6817
|
);
|
|
7036
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6818
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
7037
6819
|
}
|
|
7038
6820
|
|
|
7039
6821
|
if (detectedType === 'infra') {
|
|
@@ -7086,7 +6868,7 @@ export async function renderForExport(
|
|
|
7086
6868
|
infraSvg.setAttribute('width', String(exportWidth));
|
|
7087
6869
|
infraSvg.setAttribute('height', String(exportHeight));
|
|
7088
6870
|
}
|
|
7089
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6871
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
7090
6872
|
}
|
|
7091
6873
|
|
|
7092
6874
|
if (detectedType === 'gantt') {
|
|
@@ -7111,7 +6893,7 @@ export async function renderForExport(
|
|
|
7111
6893
|
undefined,
|
|
7112
6894
|
{ width: EXPORT_W, height: EXPORT_H }
|
|
7113
6895
|
);
|
|
7114
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6896
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
7115
6897
|
}
|
|
7116
6898
|
|
|
7117
6899
|
if (detectedType === 'state') {
|
|
@@ -7135,7 +6917,7 @@ export async function renderForExport(
|
|
|
7135
6917
|
undefined,
|
|
7136
6918
|
{ width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
|
|
7137
6919
|
);
|
|
7138
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
6920
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
7139
6921
|
}
|
|
7140
6922
|
|
|
7141
6923
|
const parsed = parseVisualization(content, palette);
|
|
@@ -7235,5 +7017,5 @@ export async function renderForExport(
|
|
|
7235
7017
|
);
|
|
7236
7018
|
}
|
|
7237
7019
|
|
|
7238
|
-
return finalizeSvgExport(container, theme, effectivePalette
|
|
7020
|
+
return finalizeSvgExport(container, theme, effectivePalette);
|
|
7239
7021
|
}
|