@diagrammo/dgmo 0.6.3 → 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.
- package/dist/cli.cjs +180 -178
- package/dist/index.cjs +5296 -2209
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +236 -16
- package/dist/index.d.ts +236 -16
- package/dist/index.js +12423 -9343
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/c4/renderer.ts +6 -6
- package/src/class/renderer.ts +183 -7
- package/src/cli.ts +3 -11
- package/src/colors.ts +3 -3
- package/src/d3.ts +128 -23
- package/src/dgmo-router.ts +3 -1
- package/src/er/renderer.ts +11 -5
- package/src/gantt/calculator.ts +677 -0
- package/src/gantt/parser.ts +761 -0
- package/src/gantt/renderer.ts +2125 -0
- package/src/gantt/resolver.ts +144 -0
- package/src/gantt/types.ts +168 -0
- package/src/index.ts +27 -0
- package/src/infra/renderer.ts +48 -12
- package/src/initiative-status/filter.ts +63 -0
- package/src/initiative-status/layout.ts +319 -67
- package/src/initiative-status/parser.ts +200 -25
- package/src/initiative-status/renderer.ts +293 -10
- package/src/initiative-status/types.ts +6 -0
- package/src/org/layout.ts +22 -55
- package/src/org/renderer.ts +4 -8
- package/src/palettes/dracula.ts +60 -0
- package/src/palettes/index.ts +8 -6
- package/src/palettes/monokai.ts +60 -0
- package/src/palettes/registry.ts +4 -2
- package/src/sequence/renderer.ts +5 -4
- package/src/sharing.ts +8 -0
- package/src/sitemap/renderer.ts +4 -4
- package/src/utils/duration.ts +212 -0
- package/src/utils/legend-constants.ts +1 -0
package/package.json
CHANGED
package/src/c4/renderer.ts
CHANGED
|
@@ -256,7 +256,7 @@ export function renderC4Context(
|
|
|
256
256
|
|
|
257
257
|
const scaledW = diagramW * scale;
|
|
258
258
|
const offsetX = (width - scaledW) / 2;
|
|
259
|
-
const offsetY = titleHeight + DIAGRAM_PADDING;
|
|
259
|
+
const offsetY = titleHeight + DIAGRAM_PADDING + legendReserveH;
|
|
260
260
|
|
|
261
261
|
const svg = d3Selection
|
|
262
262
|
.select(container)
|
|
@@ -592,12 +592,12 @@ export function renderC4Context(
|
|
|
592
592
|
|
|
593
593
|
// ── Legend ──
|
|
594
594
|
if (hasLegend) {
|
|
595
|
-
// App mode: fixed overlay at SVG
|
|
595
|
+
// App mode: fixed overlay at SVG top so it's always readable regardless of scale.
|
|
596
596
|
// Export mode: render inside scaled contentG at layout coordinates.
|
|
597
597
|
const legendParent = fixedLegend
|
|
598
598
|
? svg.append('g')
|
|
599
599
|
.attr('class', 'c4-legend-fixed')
|
|
600
|
-
.attr('transform', `translate(0, ${
|
|
600
|
+
.attr('transform', `translate(0, ${DIAGRAM_PADDING + titleHeight})`)
|
|
601
601
|
: contentG.append('g').attr('class', 'c4-legend');
|
|
602
602
|
if (activeTagGroup) {
|
|
603
603
|
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
@@ -1298,7 +1298,7 @@ export function renderC4Containers(
|
|
|
1298
1298
|
|
|
1299
1299
|
const scaledW = diagramW * scale;
|
|
1300
1300
|
const offsetX = (width - scaledW) / 2;
|
|
1301
|
-
const offsetY = titleHeight + DIAGRAM_PADDING;
|
|
1301
|
+
const offsetY = titleHeight + DIAGRAM_PADDING + legendReserveH;
|
|
1302
1302
|
|
|
1303
1303
|
const svg = d3Selection
|
|
1304
1304
|
.select(container)
|
|
@@ -1707,12 +1707,12 @@ export function renderC4Containers(
|
|
|
1707
1707
|
|
|
1708
1708
|
// ── Legend ──
|
|
1709
1709
|
if (hasLegend) {
|
|
1710
|
-
// App mode: fixed overlay at SVG
|
|
1710
|
+
// App mode: fixed overlay at SVG top so it's always readable regardless of scale.
|
|
1711
1711
|
// Export mode: render inside scaled contentG at layout coordinates.
|
|
1712
1712
|
const legendParent = fixedLegend
|
|
1713
1713
|
? svg.append('g')
|
|
1714
1714
|
.attr('class', 'c4-legend-fixed')
|
|
1715
|
-
.attr('transform', `translate(0, ${
|
|
1715
|
+
.attr('transform', `translate(0, ${DIAGRAM_PADDING + titleHeight})`)
|
|
1716
1716
|
: contentG.append('g').attr('class', 'c4-legend');
|
|
1717
1717
|
if (activeTagGroup) {
|
|
1718
1718
|
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
package/src/class/renderer.ts
CHANGED
|
@@ -6,6 +6,18 @@ import * as d3Selection from 'd3-selection';
|
|
|
6
6
|
import * as d3Shape from 'd3-shape';
|
|
7
7
|
import { FONT_FAMILY } from '../fonts';
|
|
8
8
|
import { runInExportContainer, extractExportSvg } from '../utils/export-container';
|
|
9
|
+
import {
|
|
10
|
+
LEGEND_HEIGHT,
|
|
11
|
+
LEGEND_PILL_PAD,
|
|
12
|
+
LEGEND_PILL_FONT_SIZE,
|
|
13
|
+
LEGEND_PILL_FONT_W,
|
|
14
|
+
LEGEND_CAPSULE_PAD,
|
|
15
|
+
LEGEND_DOT_R,
|
|
16
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
17
|
+
LEGEND_ENTRY_FONT_W,
|
|
18
|
+
LEGEND_ENTRY_DOT_GAP,
|
|
19
|
+
LEGEND_ENTRY_TRAIL,
|
|
20
|
+
} from '../utils/legend-constants';
|
|
9
21
|
import type { PaletteColors } from '../palettes';
|
|
10
22
|
import { mix } from '../palettes/color-utils';
|
|
11
23
|
import type { ParsedClassDiagram, ClassModifier, RelationshipType } from './types';
|
|
@@ -51,6 +63,49 @@ function nodeStroke(palette: PaletteColors, modifier: ClassModifier | undefined,
|
|
|
51
63
|
return nodeColor ?? modifierColor(modifier, palette, colorOff);
|
|
52
64
|
}
|
|
53
65
|
|
|
66
|
+
// ============================================================
|
|
67
|
+
// Legend helpers
|
|
68
|
+
// ============================================================
|
|
69
|
+
|
|
70
|
+
interface ClassLegendEntry {
|
|
71
|
+
label: string;
|
|
72
|
+
colorKey: keyof PaletteColors['colors'];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CLASS_TYPE_MAP: Record<string, ClassLegendEntry> = {
|
|
76
|
+
class: { label: 'Class', colorKey: 'teal' },
|
|
77
|
+
abstract: { label: 'Abstract', colorKey: 'purple' },
|
|
78
|
+
interface: { label: 'Interface', colorKey: 'blue' },
|
|
79
|
+
enum: { label: 'Enum', colorKey: 'yellow' },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const CLASS_TYPE_ORDER = ['class', 'abstract', 'interface', 'enum'];
|
|
83
|
+
|
|
84
|
+
function collectClassTypes(parsed: ParsedClassDiagram): ClassLegendEntry[] {
|
|
85
|
+
if (parsed.options?.color === 'off') return [];
|
|
86
|
+
|
|
87
|
+
const present = new Set<string>();
|
|
88
|
+
for (const c of parsed.classes) {
|
|
89
|
+
if (c.color) continue; // explicit color override — skip
|
|
90
|
+
present.add(c.modifier ?? 'class');
|
|
91
|
+
}
|
|
92
|
+
return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map((k) => CLASS_TYPE_MAP[k]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const LEGEND_GROUP_NAME = 'Type';
|
|
96
|
+
|
|
97
|
+
function legendEntriesWidth(entries: ClassLegendEntry[]): number {
|
|
98
|
+
let w = 0;
|
|
99
|
+
for (const e of entries) {
|
|
100
|
+
w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + e.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
101
|
+
}
|
|
102
|
+
return w;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function classTypeKey(modifier: ClassModifier | undefined): string {
|
|
106
|
+
return modifier ?? 'class';
|
|
107
|
+
}
|
|
108
|
+
|
|
54
109
|
// ============================================================
|
|
55
110
|
// Visibility symbols
|
|
56
111
|
// ============================================================
|
|
@@ -107,7 +162,8 @@ export function renderClassDiagram(
|
|
|
107
162
|
palette: PaletteColors,
|
|
108
163
|
isDark: boolean,
|
|
109
164
|
onClickItem?: (lineNumber: number) => void,
|
|
110
|
-
exportDims?: { width?: number; height?: number }
|
|
165
|
+
exportDims?: { width?: number; height?: number },
|
|
166
|
+
legendActive?: boolean | null
|
|
111
167
|
): void {
|
|
112
168
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
113
169
|
|
|
@@ -115,17 +171,22 @@ export function renderClassDiagram(
|
|
|
115
171
|
const height = exportDims?.height ?? container.clientHeight;
|
|
116
172
|
if (width <= 0 || height <= 0) return;
|
|
117
173
|
|
|
174
|
+
const legendEntries = collectClassTypes(parsed);
|
|
175
|
+
const hasLegend = legendEntries.length > 1; // only show when multiple types present
|
|
176
|
+
|
|
118
177
|
const titleHeight = parsed.title ? 40 : 0;
|
|
178
|
+
const LEGEND_FIXED_GAP = 8;
|
|
179
|
+
const legendReserve = hasLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
119
180
|
const diagramW = layout.width;
|
|
120
181
|
const diagramH = layout.height;
|
|
121
|
-
const availH = height - titleHeight;
|
|
182
|
+
const availH = height - titleHeight - legendReserve;
|
|
122
183
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
123
184
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
124
185
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
125
186
|
|
|
126
187
|
const scaledW = diagramW * scale;
|
|
127
188
|
const offsetX = (width - scaledW) / 2;
|
|
128
|
-
const offsetY = titleHeight + DIAGRAM_PADDING;
|
|
189
|
+
const offsetY = titleHeight + legendReserve + DIAGRAM_PADDING;
|
|
129
190
|
|
|
130
191
|
const svg = d3Selection
|
|
131
192
|
.select(container)
|
|
@@ -244,6 +305,114 @@ export function renderClassDiagram(
|
|
|
244
305
|
}
|
|
245
306
|
}
|
|
246
307
|
|
|
308
|
+
// ── Legend ──
|
|
309
|
+
// legendActive: true = expanded (default), false = collapsed pill only
|
|
310
|
+
const isLegendExpanded = legendActive !== false;
|
|
311
|
+
if (hasLegend) {
|
|
312
|
+
const groupBg = isDark
|
|
313
|
+
? mix(palette.surface, palette.bg, 50)
|
|
314
|
+
: mix(palette.surface, palette.bg, 30);
|
|
315
|
+
|
|
316
|
+
const pillWidth = LEGEND_GROUP_NAME.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
317
|
+
const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
|
|
318
|
+
const entriesW = legendEntriesWidth(legendEntries);
|
|
319
|
+
|
|
320
|
+
const totalW = isLegendExpanded
|
|
321
|
+
? LEGEND_CAPSULE_PAD * 2 + pillWidth + LEGEND_ENTRY_TRAIL + entriesW
|
|
322
|
+
: pillWidth;
|
|
323
|
+
|
|
324
|
+
const legendX = (width - totalW) / 2;
|
|
325
|
+
const legendY = titleHeight;
|
|
326
|
+
|
|
327
|
+
const legendG = svg
|
|
328
|
+
.append('g')
|
|
329
|
+
.attr('class', 'cd-legend')
|
|
330
|
+
.attr('data-legend-group', 'type')
|
|
331
|
+
.attr('transform', `translate(${legendX}, ${legendY})`)
|
|
332
|
+
.style('cursor', 'pointer');
|
|
333
|
+
|
|
334
|
+
if (isLegendExpanded) {
|
|
335
|
+
// Outer capsule
|
|
336
|
+
legendG.append('rect')
|
|
337
|
+
.attr('width', totalW)
|
|
338
|
+
.attr('height', LEGEND_HEIGHT)
|
|
339
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
340
|
+
.attr('fill', groupBg);
|
|
341
|
+
|
|
342
|
+
// Inner pill
|
|
343
|
+
legendG.append('rect')
|
|
344
|
+
.attr('x', LEGEND_CAPSULE_PAD)
|
|
345
|
+
.attr('y', LEGEND_CAPSULE_PAD)
|
|
346
|
+
.attr('width', pillWidth)
|
|
347
|
+
.attr('height', pillH)
|
|
348
|
+
.attr('rx', pillH / 2)
|
|
349
|
+
.attr('fill', palette.bg);
|
|
350
|
+
|
|
351
|
+
legendG.append('rect')
|
|
352
|
+
.attr('x', LEGEND_CAPSULE_PAD)
|
|
353
|
+
.attr('y', LEGEND_CAPSULE_PAD)
|
|
354
|
+
.attr('width', pillWidth)
|
|
355
|
+
.attr('height', pillH)
|
|
356
|
+
.attr('rx', pillH / 2)
|
|
357
|
+
.attr('fill', 'none')
|
|
358
|
+
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
359
|
+
.attr('stroke-width', 0.75);
|
|
360
|
+
|
|
361
|
+
legendG.append('text')
|
|
362
|
+
.attr('x', LEGEND_CAPSULE_PAD + pillWidth / 2)
|
|
363
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
364
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
365
|
+
.attr('font-weight', '500')
|
|
366
|
+
.attr('fill', palette.text)
|
|
367
|
+
.attr('text-anchor', 'middle')
|
|
368
|
+
.attr('font-family', FONT_FAMILY)
|
|
369
|
+
.text(LEGEND_GROUP_NAME);
|
|
370
|
+
|
|
371
|
+
// Entries
|
|
372
|
+
let entryX = LEGEND_CAPSULE_PAD + pillWidth + LEGEND_ENTRY_TRAIL;
|
|
373
|
+
for (const entry of legendEntries) {
|
|
374
|
+
const color = palette.colors[entry.colorKey];
|
|
375
|
+
const typeKey = CLASS_TYPE_ORDER.find((k) => CLASS_TYPE_MAP[k] === entry)!;
|
|
376
|
+
|
|
377
|
+
const entryG = legendG.append('g')
|
|
378
|
+
.attr('data-legend-entry', typeKey);
|
|
379
|
+
|
|
380
|
+
entryG.append('circle')
|
|
381
|
+
.attr('cx', entryX + LEGEND_DOT_R)
|
|
382
|
+
.attr('cy', LEGEND_HEIGHT / 2)
|
|
383
|
+
.attr('r', LEGEND_DOT_R)
|
|
384
|
+
.attr('fill', color);
|
|
385
|
+
|
|
386
|
+
entryG.append('text')
|
|
387
|
+
.attr('x', entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP)
|
|
388
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
389
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
390
|
+
.attr('fill', palette.textMuted)
|
|
391
|
+
.attr('font-family', FONT_FAMILY)
|
|
392
|
+
.text(entry.label);
|
|
393
|
+
|
|
394
|
+
entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.label.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
// Collapsed: single muted pill
|
|
398
|
+
legendG.append('rect')
|
|
399
|
+
.attr('width', pillWidth)
|
|
400
|
+
.attr('height', LEGEND_HEIGHT)
|
|
401
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
402
|
+
.attr('fill', groupBg);
|
|
403
|
+
|
|
404
|
+
legendG.append('text')
|
|
405
|
+
.attr('x', pillWidth / 2)
|
|
406
|
+
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
407
|
+
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
408
|
+
.attr('font-weight', '500')
|
|
409
|
+
.attr('fill', palette.textMuted)
|
|
410
|
+
.attr('text-anchor', 'middle')
|
|
411
|
+
.attr('font-family', FONT_FAMILY)
|
|
412
|
+
.text(LEGEND_GROUP_NAME);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
247
416
|
// ── Content group ──
|
|
248
417
|
const contentG = svg
|
|
249
418
|
.append('g')
|
|
@@ -320,7 +489,8 @@ export function renderClassDiagram(
|
|
|
320
489
|
.attr('transform', `translate(${node.x}, ${node.y})`)
|
|
321
490
|
.attr('class', 'cd-class')
|
|
322
491
|
.attr('data-line-number', String(node.lineNumber))
|
|
323
|
-
.attr('data-node-id', node.id)
|
|
492
|
+
.attr('data-node-id', node.id)
|
|
493
|
+
.attr('data-cd-type', classTypeKey(node.modifier));
|
|
324
494
|
|
|
325
495
|
if (onClickItem) {
|
|
326
496
|
nodeG.style('cursor', 'pointer').on('click', () => {
|
|
@@ -331,8 +501,11 @@ export function renderClassDiagram(
|
|
|
331
501
|
const w = node.width;
|
|
332
502
|
const h = node.height;
|
|
333
503
|
const colorOff = parsed.options?.color === 'off';
|
|
334
|
-
|
|
335
|
-
const
|
|
504
|
+
// When legend is collapsed, use neutral color for nodes without explicit color
|
|
505
|
+
const neutralize = hasLegend && !isLegendExpanded && !node.color;
|
|
506
|
+
const effectiveColor = neutralize ? palette.primary : node.color;
|
|
507
|
+
const fill = nodeFill(palette, isDark, node.modifier, effectiveColor, colorOff);
|
|
508
|
+
const stroke = nodeStroke(palette, node.modifier, effectiveColor, colorOff);
|
|
336
509
|
|
|
337
510
|
// Outer rectangle
|
|
338
511
|
nodeG.append('rect')
|
|
@@ -504,8 +677,11 @@ export function renderClassDiagramForExport(
|
|
|
504
677
|
const layout = layoutClassDiagram(parsed);
|
|
505
678
|
const isDark = theme === 'dark';
|
|
506
679
|
|
|
680
|
+
const legendEntries = collectClassTypes(parsed);
|
|
681
|
+
const EXPORT_LEGEND_GAP = 8;
|
|
682
|
+
const legendReserve = legendEntries.length > 1 ? LEGEND_HEIGHT + EXPORT_LEGEND_GAP : 0;
|
|
507
683
|
const exportWidth = layout.width + DIAGRAM_PADDING * 2;
|
|
508
|
-
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? 40 : 0);
|
|
684
|
+
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? 40 : 0) + legendReserve;
|
|
509
685
|
|
|
510
686
|
return runInExportContainer(exportWidth, exportHeight, (container) => {
|
|
511
687
|
renderClassDiagram(
|
package/src/cli.ts
CHANGED
|
@@ -8,21 +8,13 @@ import { render } from './render';
|
|
|
8
8
|
import { parseDgmo, getAllChartTypes } from './dgmo-router';
|
|
9
9
|
import { parseDgmoChartType } from './dgmo-router';
|
|
10
10
|
import { formatDgmoError } from './diagnostics';
|
|
11
|
-
import { getPalette } from './palettes
|
|
11
|
+
import { getPalette, getAvailablePalettes } from './palettes';
|
|
12
12
|
import { DEFAULT_FONT_NAME } from './fonts';
|
|
13
13
|
import { encodeDiagramUrl } from './sharing';
|
|
14
14
|
import { resolveOrgImports } from './org/resolver';
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'solarized',
|
|
19
|
-
'catppuccin',
|
|
20
|
-
'rose-pine',
|
|
21
|
-
'gruvbox',
|
|
22
|
-
'tokyo-night',
|
|
23
|
-
'one-dark',
|
|
24
|
-
'bold',
|
|
25
|
-
];
|
|
16
|
+
// Derived from the palette registry so new palettes are auto-included.
|
|
17
|
+
const PALETTES = getAvailablePalettes().map((p) => p.id);
|
|
26
18
|
|
|
27
19
|
const THEMES = ['light', 'dark', 'transparent'] as const;
|
|
28
20
|
|
package/src/colors.ts
CHANGED
|
@@ -41,9 +41,9 @@ export const colorNames: Record<string, string> = {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Resolves a color name
|
|
44
|
+
* Resolves a color name to a valid CSS color.
|
|
45
45
|
* When a palette is provided, named colors resolve against its color map first.
|
|
46
|
-
* Hex codes
|
|
46
|
+
* Hex codes are NOT supported — use named colors only.
|
|
47
47
|
* Unknown names are returned as-is.
|
|
48
48
|
*/
|
|
49
49
|
export function resolveColor(
|
|
@@ -51,7 +51,6 @@ export function resolveColor(
|
|
|
51
51
|
palette?: { colors: Record<string, string> }
|
|
52
52
|
): string {
|
|
53
53
|
const lower = color.toLowerCase();
|
|
54
|
-
if (lower.startsWith('#')) return lower;
|
|
55
54
|
|
|
56
55
|
if (palette) {
|
|
57
56
|
const named = palette.colors[lower];
|
|
@@ -59,6 +58,7 @@ export function resolveColor(
|
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
if (colorNames[lower]) return colorNames[lower];
|
|
61
|
+
// Unknown color name — return as-is so callers can detect + warn
|
|
62
62
|
return color;
|
|
63
63
|
}
|
|
64
64
|
|
package/src/d3.ts
CHANGED
|
@@ -3059,7 +3059,7 @@ export function renderTimeline(
|
|
|
3059
3059
|
}
|
|
3060
3060
|
}
|
|
3061
3061
|
|
|
3062
|
-
// Reserve space for tag legend at the
|
|
3062
|
+
// Reserve space for tag legend at the top of chart content (below title/headers)
|
|
3063
3063
|
const tagLegendReserve = parsed.timelineTagGroups.length > 0 ? 36 : 0;
|
|
3064
3064
|
|
|
3065
3065
|
// ================================================================
|
|
@@ -3099,9 +3099,9 @@ export function renderTimeline(
|
|
|
3099
3099
|
const scaleMargin = timelineScale ? 40 : 0;
|
|
3100
3100
|
const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
|
|
3101
3101
|
const margin = {
|
|
3102
|
-
top: 104 + markerMargin,
|
|
3102
|
+
top: 104 + markerMargin + tagLegendReserve,
|
|
3103
3103
|
right: 40 + scaleMargin,
|
|
3104
|
-
bottom: 40
|
|
3104
|
+
bottom: 40,
|
|
3105
3105
|
left: 60 + scaleMargin,
|
|
3106
3106
|
};
|
|
3107
3107
|
const innerWidth = width - margin.left - margin.right;
|
|
@@ -3254,12 +3254,14 @@ export function renderTimeline(
|
|
|
3254
3254
|
const rectH = Math.max(y2 - y, 4);
|
|
3255
3255
|
|
|
3256
3256
|
let fill: string = mix(evColor, bg, 30);
|
|
3257
|
+
let stroke: string = evColor;
|
|
3257
3258
|
if (ev.uncertain) {
|
|
3258
3259
|
const gradientId = `uncertain-vg-${ev.lineNumber}`;
|
|
3260
|
+
const strokeGradientId = `uncertain-vg-s-${ev.lineNumber}`;
|
|
3259
3261
|
const defs =
|
|
3260
3262
|
svg.select('defs').node() || svg.append('defs').node();
|
|
3261
|
-
d3Selection
|
|
3262
|
-
|
|
3263
|
+
const defsEl = d3Selection.select(defs as Element);
|
|
3264
|
+
defsEl
|
|
3263
3265
|
.append('linearGradient')
|
|
3264
3266
|
.attr('id', gradientId)
|
|
3265
3267
|
.attr('x1', '0%')
|
|
@@ -3277,7 +3279,26 @@ export function renderTimeline(
|
|
|
3277
3279
|
.attr('offset', (d) => d.offset)
|
|
3278
3280
|
.attr('stop-color', mix(laneColor, bg, 30))
|
|
3279
3281
|
.attr('stop-opacity', (d) => d.opacity);
|
|
3282
|
+
defsEl
|
|
3283
|
+
.append('linearGradient')
|
|
3284
|
+
.attr('id', strokeGradientId)
|
|
3285
|
+
.attr('x1', '0%')
|
|
3286
|
+
.attr('y1', '0%')
|
|
3287
|
+
.attr('x2', '0%')
|
|
3288
|
+
.attr('y2', '100%')
|
|
3289
|
+
.selectAll('stop')
|
|
3290
|
+
.data([
|
|
3291
|
+
{ offset: '0%', opacity: 1 },
|
|
3292
|
+
{ offset: '80%', opacity: 1 },
|
|
3293
|
+
{ offset: '100%', opacity: 0 },
|
|
3294
|
+
])
|
|
3295
|
+
.enter()
|
|
3296
|
+
.append('stop')
|
|
3297
|
+
.attr('offset', (d) => d.offset)
|
|
3298
|
+
.attr('stop-color', evColor)
|
|
3299
|
+
.attr('stop-opacity', (d) => d.opacity);
|
|
3280
3300
|
fill = `url(#${gradientId})`;
|
|
3301
|
+
stroke = `url(#${strokeGradientId})`;
|
|
3281
3302
|
}
|
|
3282
3303
|
|
|
3283
3304
|
evG
|
|
@@ -3288,7 +3309,7 @@ export function renderTimeline(
|
|
|
3288
3309
|
.attr('height', rectH)
|
|
3289
3310
|
.attr('rx', 4)
|
|
3290
3311
|
.attr('fill', fill)
|
|
3291
|
-
.attr('stroke',
|
|
3312
|
+
.attr('stroke', stroke)
|
|
3292
3313
|
.attr('stroke-width', 2);
|
|
3293
3314
|
evG
|
|
3294
3315
|
.append('text')
|
|
@@ -3323,9 +3344,9 @@ export function renderTimeline(
|
|
|
3323
3344
|
const scaleMargin = timelineScale ? 40 : 0;
|
|
3324
3345
|
const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
|
|
3325
3346
|
const margin = {
|
|
3326
|
-
top: 104 + markerMargin,
|
|
3347
|
+
top: 104 + markerMargin + tagLegendReserve,
|
|
3327
3348
|
right: 200,
|
|
3328
|
-
bottom: 40
|
|
3349
|
+
bottom: 40,
|
|
3329
3350
|
left: 60 + scaleMargin,
|
|
3330
3351
|
};
|
|
3331
3352
|
const innerWidth = width - margin.left - margin.right;
|
|
@@ -3474,12 +3495,14 @@ export function renderTimeline(
|
|
|
3474
3495
|
const rectH = Math.max(y2 - y, 4);
|
|
3475
3496
|
|
|
3476
3497
|
let fill: string = mix(color, bg, 30);
|
|
3498
|
+
let stroke: string = color;
|
|
3477
3499
|
if (ev.uncertain) {
|
|
3478
3500
|
const gradientId = `uncertain-v-${ev.lineNumber}`;
|
|
3501
|
+
const strokeGradientId = `uncertain-v-s-${ev.lineNumber}`;
|
|
3479
3502
|
const defs =
|
|
3480
3503
|
svg.select('defs').node() || svg.append('defs').node();
|
|
3481
|
-
d3Selection
|
|
3482
|
-
|
|
3504
|
+
const defsEl = d3Selection.select(defs as Element);
|
|
3505
|
+
defsEl
|
|
3483
3506
|
.append('linearGradient')
|
|
3484
3507
|
.attr('id', gradientId)
|
|
3485
3508
|
.attr('x1', '0%')
|
|
@@ -3497,7 +3520,26 @@ export function renderTimeline(
|
|
|
3497
3520
|
.attr('offset', (d) => d.offset)
|
|
3498
3521
|
.attr('stop-color', mix(color, bg, 30))
|
|
3499
3522
|
.attr('stop-opacity', (d) => d.opacity);
|
|
3523
|
+
defsEl
|
|
3524
|
+
.append('linearGradient')
|
|
3525
|
+
.attr('id', strokeGradientId)
|
|
3526
|
+
.attr('x1', '0%')
|
|
3527
|
+
.attr('y1', '0%')
|
|
3528
|
+
.attr('x2', '0%')
|
|
3529
|
+
.attr('y2', '100%')
|
|
3530
|
+
.selectAll('stop')
|
|
3531
|
+
.data([
|
|
3532
|
+
{ offset: '0%', opacity: 1 },
|
|
3533
|
+
{ offset: '80%', opacity: 1 },
|
|
3534
|
+
{ offset: '100%', opacity: 0 },
|
|
3535
|
+
])
|
|
3536
|
+
.enter()
|
|
3537
|
+
.append('stop')
|
|
3538
|
+
.attr('offset', (d) => d.offset)
|
|
3539
|
+
.attr('stop-color', color)
|
|
3540
|
+
.attr('stop-opacity', (d) => d.opacity);
|
|
3500
3541
|
fill = `url(#${gradientId})`;
|
|
3542
|
+
stroke = `url(#${strokeGradientId})`;
|
|
3501
3543
|
}
|
|
3502
3544
|
|
|
3503
3545
|
evG
|
|
@@ -3508,7 +3550,7 @@ export function renderTimeline(
|
|
|
3508
3550
|
.attr('height', rectH)
|
|
3509
3551
|
.attr('rx', 4)
|
|
3510
3552
|
.attr('fill', fill)
|
|
3511
|
-
.attr('stroke',
|
|
3553
|
+
.attr('stroke', stroke)
|
|
3512
3554
|
.attr('stroke-width', 2);
|
|
3513
3555
|
evG
|
|
3514
3556
|
.append('text')
|
|
@@ -3606,9 +3648,9 @@ export function renderTimeline(
|
|
|
3606
3648
|
// Group-sorted doesn't need legend space (group names shown on left)
|
|
3607
3649
|
const baseTopMargin = title ? 50 : 20;
|
|
3608
3650
|
const margin = {
|
|
3609
|
-
top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin,
|
|
3651
|
+
top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
|
|
3610
3652
|
right: 40,
|
|
3611
|
-
bottom: 40 + scaleMargin
|
|
3653
|
+
bottom: 40 + scaleMargin,
|
|
3612
3654
|
left: dynamicLeftMargin,
|
|
3613
3655
|
};
|
|
3614
3656
|
const innerWidth = width - margin.left - margin.right;
|
|
@@ -3785,12 +3827,14 @@ export function renderTimeline(
|
|
|
3785
3827
|
const labelFitsInside = rectW >= estLabelWidth;
|
|
3786
3828
|
|
|
3787
3829
|
let fill: string = mix(evColor, bg, 30);
|
|
3830
|
+
let stroke: string = evColor;
|
|
3788
3831
|
if (ev.uncertain) {
|
|
3789
3832
|
// Create gradient for uncertain end - fades last 20%
|
|
3790
3833
|
const gradientId = `uncertain-${ev.lineNumber}`;
|
|
3834
|
+
const strokeGradientId = `uncertain-s-${ev.lineNumber}`;
|
|
3791
3835
|
const defs = svg.select('defs').node() || svg.append('defs').node();
|
|
3792
|
-
d3Selection
|
|
3793
|
-
|
|
3836
|
+
const defsEl = d3Selection.select(defs as Element);
|
|
3837
|
+
defsEl
|
|
3794
3838
|
.append('linearGradient')
|
|
3795
3839
|
.attr('id', gradientId)
|
|
3796
3840
|
.attr('x1', '0%')
|
|
@@ -3808,7 +3852,26 @@ export function renderTimeline(
|
|
|
3808
3852
|
.attr('offset', (d) => d.offset)
|
|
3809
3853
|
.attr('stop-color', mix(evColor, bg, 30))
|
|
3810
3854
|
.attr('stop-opacity', (d) => d.opacity);
|
|
3855
|
+
defsEl
|
|
3856
|
+
.append('linearGradient')
|
|
3857
|
+
.attr('id', strokeGradientId)
|
|
3858
|
+
.attr('x1', '0%')
|
|
3859
|
+
.attr('y1', '0%')
|
|
3860
|
+
.attr('x2', '100%')
|
|
3861
|
+
.attr('y2', '0%')
|
|
3862
|
+
.selectAll('stop')
|
|
3863
|
+
.data([
|
|
3864
|
+
{ offset: '0%', opacity: 1 },
|
|
3865
|
+
{ offset: '80%', opacity: 1 },
|
|
3866
|
+
{ offset: '100%', opacity: 0 },
|
|
3867
|
+
])
|
|
3868
|
+
.enter()
|
|
3869
|
+
.append('stop')
|
|
3870
|
+
.attr('offset', (d) => d.offset)
|
|
3871
|
+
.attr('stop-color', evColor)
|
|
3872
|
+
.attr('stop-opacity', (d) => d.opacity);
|
|
3811
3873
|
fill = `url(#${gradientId})`;
|
|
3874
|
+
stroke = `url(#${strokeGradientId})`;
|
|
3812
3875
|
}
|
|
3813
3876
|
|
|
3814
3877
|
evG
|
|
@@ -3819,7 +3882,7 @@ export function renderTimeline(
|
|
|
3819
3882
|
.attr('height', BAR_H)
|
|
3820
3883
|
.attr('rx', 4)
|
|
3821
3884
|
.attr('fill', fill)
|
|
3822
|
-
.attr('stroke',
|
|
3885
|
+
.attr('stroke', stroke)
|
|
3823
3886
|
.attr('stroke-width', 2);
|
|
3824
3887
|
|
|
3825
3888
|
if (labelFitsInside) {
|
|
@@ -3887,9 +3950,9 @@ export function renderTimeline(
|
|
|
3887
3950
|
const scaleMargin = timelineScale ? 24 : 0;
|
|
3888
3951
|
const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
|
|
3889
3952
|
const margin = {
|
|
3890
|
-
top: 104 + (timelineScale ? 40 : 0) + markerMargin,
|
|
3953
|
+
top: 104 + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
|
|
3891
3954
|
right: 40,
|
|
3892
|
-
bottom: 40 + scaleMargin
|
|
3955
|
+
bottom: 40 + scaleMargin,
|
|
3893
3956
|
left: 60,
|
|
3894
3957
|
};
|
|
3895
3958
|
const innerWidth = width - margin.left - margin.right;
|
|
@@ -4047,12 +4110,14 @@ export function renderTimeline(
|
|
|
4047
4110
|
const labelFitsInside = rectW >= estLabelWidth;
|
|
4048
4111
|
|
|
4049
4112
|
let fill: string = mix(color, bg, 30);
|
|
4113
|
+
let stroke: string = color;
|
|
4050
4114
|
if (ev.uncertain) {
|
|
4051
4115
|
// Create gradient for uncertain end - fades last 20%
|
|
4052
4116
|
const gradientId = `uncertain-ts-${ev.lineNumber}`;
|
|
4117
|
+
const strokeGradientId = `uncertain-ts-s-${ev.lineNumber}`;
|
|
4053
4118
|
const defs = svg.select('defs').node() || svg.append('defs').node();
|
|
4054
|
-
d3Selection
|
|
4055
|
-
|
|
4119
|
+
const defsEl = d3Selection.select(defs as Element);
|
|
4120
|
+
defsEl
|
|
4056
4121
|
.append('linearGradient')
|
|
4057
4122
|
.attr('id', gradientId)
|
|
4058
4123
|
.attr('x1', '0%')
|
|
@@ -4070,7 +4135,26 @@ export function renderTimeline(
|
|
|
4070
4135
|
.attr('offset', (d) => d.offset)
|
|
4071
4136
|
.attr('stop-color', mix(color, bg, 30))
|
|
4072
4137
|
.attr('stop-opacity', (d) => d.opacity);
|
|
4138
|
+
defsEl
|
|
4139
|
+
.append('linearGradient')
|
|
4140
|
+
.attr('id', strokeGradientId)
|
|
4141
|
+
.attr('x1', '0%')
|
|
4142
|
+
.attr('y1', '0%')
|
|
4143
|
+
.attr('x2', '100%')
|
|
4144
|
+
.attr('y2', '0%')
|
|
4145
|
+
.selectAll('stop')
|
|
4146
|
+
.data([
|
|
4147
|
+
{ offset: '0%', opacity: 1 },
|
|
4148
|
+
{ offset: '80%', opacity: 1 },
|
|
4149
|
+
{ offset: '100%', opacity: 0 },
|
|
4150
|
+
])
|
|
4151
|
+
.enter()
|
|
4152
|
+
.append('stop')
|
|
4153
|
+
.attr('offset', (d) => d.offset)
|
|
4154
|
+
.attr('stop-color', color)
|
|
4155
|
+
.attr('stop-opacity', (d) => d.opacity);
|
|
4073
4156
|
fill = `url(#${gradientId})`;
|
|
4157
|
+
stroke = `url(#${strokeGradientId})`;
|
|
4074
4158
|
}
|
|
4075
4159
|
|
|
4076
4160
|
evG
|
|
@@ -4081,7 +4165,7 @@ export function renderTimeline(
|
|
|
4081
4165
|
.attr('height', BAR_H)
|
|
4082
4166
|
.attr('rx', 4)
|
|
4083
4167
|
.attr('fill', fill)
|
|
4084
|
-
.attr('stroke',
|
|
4168
|
+
.attr('stroke', stroke)
|
|
4085
4169
|
.attr('stroke-width', 2);
|
|
4086
4170
|
|
|
4087
4171
|
if (labelFitsInside) {
|
|
@@ -4157,7 +4241,8 @@ export function renderTimeline(
|
|
|
4157
4241
|
const mainSvg = d3Selection.select(container).select<SVGSVGElement>('svg');
|
|
4158
4242
|
const mainG = mainSvg.select<SVGGElement>('g');
|
|
4159
4243
|
if (!mainSvg.empty() && !mainG.empty()) {
|
|
4160
|
-
|
|
4244
|
+
// Position legend at top, below title
|
|
4245
|
+
const legendY = title ? 50 : 10;
|
|
4161
4246
|
|
|
4162
4247
|
const groupBg = isDark
|
|
4163
4248
|
? mix(palette.surface, palette.bg, 50)
|
|
@@ -5914,7 +5999,7 @@ export async function renderForExport(
|
|
|
5914
5999
|
const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
|
|
5915
6000
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
5916
6001
|
|
|
5917
|
-
renderInitiativeStatus(container, isParsed, isLayout, effectivePalette, theme === 'dark',
|
|
6002
|
+
renderInitiativeStatus(container, isParsed, isLayout, effectivePalette, theme === 'dark', { exportDims: { width: exportWidth, height: exportHeight } });
|
|
5918
6003
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
5919
6004
|
}
|
|
5920
6005
|
|
|
@@ -6003,6 +6088,26 @@ export async function renderForExport(
|
|
|
6003
6088
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6004
6089
|
}
|
|
6005
6090
|
|
|
6091
|
+
if (detectedType === 'gantt') {
|
|
6092
|
+
const { parseGantt } = await import('./gantt/parser');
|
|
6093
|
+
const { calculateSchedule } = await import('./gantt/calculator');
|
|
6094
|
+
const { renderGantt } = await import('./gantt/renderer');
|
|
6095
|
+
|
|
6096
|
+
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6097
|
+
const ganttParsed = parseGantt(content, effectivePalette);
|
|
6098
|
+
if (ganttParsed.error) return '';
|
|
6099
|
+
|
|
6100
|
+
const resolved = calculateSchedule(ganttParsed);
|
|
6101
|
+
if (resolved.error || resolved.tasks.length === 0) return '';
|
|
6102
|
+
|
|
6103
|
+
const EXPORT_W = 1200;
|
|
6104
|
+
const EXPORT_H = 800;
|
|
6105
|
+
const container = createExportContainer(EXPORT_W, EXPORT_H);
|
|
6106
|
+
|
|
6107
|
+
renderGantt(container, resolved, effectivePalette, theme === 'dark', undefined, { width: EXPORT_W, height: EXPORT_H });
|
|
6108
|
+
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6109
|
+
}
|
|
6110
|
+
|
|
6006
6111
|
if (detectedType === 'state') {
|
|
6007
6112
|
const { parseState } = await import('./graph/state-parser');
|
|
6008
6113
|
const { layoutGraph } = await import('./graph/layout');
|