@diagrammo/dgmo 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dgmo.md +231 -13
- package/AGENTS.md +148 -0
- package/dist/cli.cjs +341 -165
- package/dist/index.cjs +4900 -1685
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +259 -18
- package/dist/index.d.ts +259 -18
- package/dist/index.js +4642 -1436
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/c4/layout.ts +0 -5
- package/src/c4/parser.ts +0 -16
- package/src/c4/renderer.ts +7 -11
- package/src/class/layout.ts +0 -1
- package/src/class/parser.ts +28 -0
- package/src/class/renderer.ts +189 -34
- package/src/cli.ts +566 -25
- package/src/colors.ts +3 -3
- package/src/completion.ts +58 -0
- package/src/d3.ts +179 -122
- package/src/dgmo-router.ts +3 -58
- package/src/echarts.ts +96 -55
- package/src/er/parser.ts +30 -1
- package/src/er/renderer.ts +12 -7
- 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/graph/flowchart-parser.ts +27 -4
- package/src/graph/flowchart-renderer.ts +1 -2
- package/src/graph/state-parser.ts +0 -1
- package/src/graph/state-renderer.ts +1 -3
- package/src/index.ts +37 -0
- package/src/infra/compute.ts +0 -7
- package/src/infra/layout.ts +0 -2
- package/src/infra/parser.ts +46 -4
- package/src/infra/renderer.ts +49 -27
- 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 +298 -35
- package/src/initiative-status/types.ts +6 -0
- package/src/kanban/parser.ts +0 -2
- package/src/org/layout.ts +22 -59
- package/src/org/renderer.ts +11 -36
- 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/parser.ts +14 -11
- package/src/sequence/renderer.ts +5 -6
- package/src/sequence/tag-resolution.ts +0 -1
- package/src/sharing.ts +8 -0
- package/src/sitemap/layout.ts +1 -14
- package/src/sitemap/parser.ts +1 -2
- package/src/sitemap/renderer.ts +4 -7
- package/src/utils/arrows.ts +7 -7
- package/src/utils/duration.ts +212 -0
- package/src/utils/export-container.ts +40 -0
- package/src/utils/legend-constants.ts +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagram symbol extraction API.
|
|
3
|
+
*
|
|
4
|
+
* Provides DiagramSymbols interface + extractDiagramSymbols() dispatch.
|
|
5
|
+
* Each diagram type registers its own extractor via registerExtractor().
|
|
6
|
+
* All built-in extractors are registered at module init below.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { extractSymbols as extractErSymbols } from './er/parser';
|
|
10
|
+
import { extractSymbols as extractFlowchartSymbols } from './graph/flowchart-parser';
|
|
11
|
+
import { extractSymbols as extractInfraSymbols } from './infra/parser';
|
|
12
|
+
import { extractSymbols as extractClassSymbols } from './class/parser';
|
|
13
|
+
|
|
14
|
+
// ChartType is just a string — alias here for documentation clarity.
|
|
15
|
+
export type ChartType = string;
|
|
16
|
+
|
|
17
|
+
export interface DiagramSymbols {
|
|
18
|
+
kind: ChartType;
|
|
19
|
+
entities: string[]; // table names, node IDs, class names, etc.
|
|
20
|
+
keywords: string[]; // diagram-specific reserved words
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ExtractFn = (docText: string) => DiagramSymbols;
|
|
24
|
+
|
|
25
|
+
const registry = new Map<ChartType, ExtractFn>();
|
|
26
|
+
|
|
27
|
+
export function registerExtractor(kind: ChartType, fn: ExtractFn): void {
|
|
28
|
+
registry.set(kind, fn);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract diagram symbols from document text.
|
|
33
|
+
* Returns null if the chart type is unknown or has no registered extractor.
|
|
34
|
+
*/
|
|
35
|
+
export function extractDiagramSymbols(docText: string): DiagramSymbols | null {
|
|
36
|
+
// Parse chartType from first `chart:` line — lightweight, no full parser.
|
|
37
|
+
let chartType: string | null = null;
|
|
38
|
+
for (const line of docText.split('\n')) {
|
|
39
|
+
const m = line.match(/^\s*chart\s*:\s*(.+)/i);
|
|
40
|
+
if (m) {
|
|
41
|
+
chartType = m[1]!.trim().toLowerCase();
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!chartType) return null;
|
|
46
|
+
const fn = registry.get(chartType);
|
|
47
|
+
if (!fn) return null;
|
|
48
|
+
return fn(docText);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============================================================
|
|
52
|
+
// Register built-in extractors
|
|
53
|
+
// ============================================================
|
|
54
|
+
|
|
55
|
+
registerExtractor('er', extractErSymbols);
|
|
56
|
+
registerExtractor('flowchart', extractFlowchartSymbols);
|
|
57
|
+
registerExtractor('infra', extractInfraSymbols);
|
|
58
|
+
registerExtractor('class', extractClassSymbols);
|
package/src/d3.ts
CHANGED
|
@@ -1861,7 +1861,6 @@ export function renderArcDiagram(
|
|
|
1861
1861
|
const positions = groupNodes.map((n) => yScale(n)!);
|
|
1862
1862
|
const minY = Math.min(...positions) - bandPad;
|
|
1863
1863
|
const maxY = Math.max(...positions) + bandPad;
|
|
1864
|
-
const bandColor = group.color ?? mutedColor;
|
|
1865
1864
|
|
|
1866
1865
|
g.append('rect')
|
|
1867
1866
|
.attr('class', 'arc-group-band')
|
|
@@ -1996,7 +1995,6 @@ export function renderArcDiagram(
|
|
|
1996
1995
|
const positions = groupNodes.map((n) => xScale(n)!);
|
|
1997
1996
|
const minX = Math.min(...positions) - bandPad;
|
|
1998
1997
|
const maxX = Math.max(...positions) + bandPad;
|
|
1999
|
-
const bandColor = group.color ?? mutedColor;
|
|
2000
1998
|
|
|
2001
1999
|
g.append('rect')
|
|
2002
2000
|
.attr('class', 'arc-group-band')
|
|
@@ -2864,6 +2862,7 @@ export function renderTimeline(
|
|
|
2864
2862
|
const textColor = palette.text;
|
|
2865
2863
|
const mutedColor = palette.border;
|
|
2866
2864
|
const bgColor = palette.bg;
|
|
2865
|
+
const bg = isDark ? palette.surface : palette.bg;
|
|
2867
2866
|
const colors = getSeriesColors(palette);
|
|
2868
2867
|
|
|
2869
2868
|
// Assign colors to groups
|
|
@@ -3060,7 +3059,7 @@ export function renderTimeline(
|
|
|
3060
3059
|
}
|
|
3061
3060
|
}
|
|
3062
3061
|
|
|
3063
|
-
// Reserve space for tag legend at the
|
|
3062
|
+
// Reserve space for tag legend at the top of chart content (below title/headers)
|
|
3064
3063
|
const tagLegendReserve = parsed.timelineTagGroups.length > 0 ? 36 : 0;
|
|
3065
3064
|
|
|
3066
3065
|
// ================================================================
|
|
@@ -3100,9 +3099,9 @@ export function renderTimeline(
|
|
|
3100
3099
|
const scaleMargin = timelineScale ? 40 : 0;
|
|
3101
3100
|
const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
|
|
3102
3101
|
const margin = {
|
|
3103
|
-
top: 104 + markerMargin,
|
|
3102
|
+
top: 104 + markerMargin + tagLegendReserve,
|
|
3104
3103
|
right: 40 + scaleMargin,
|
|
3105
|
-
bottom: 40
|
|
3104
|
+
bottom: 40,
|
|
3106
3105
|
left: 60 + scaleMargin,
|
|
3107
3106
|
};
|
|
3108
3107
|
const innerWidth = width - margin.left - margin.right;
|
|
@@ -3254,13 +3253,15 @@ export function renderTimeline(
|
|
|
3254
3253
|
const y2 = yScale(parseTimelineDate(ev.endDate));
|
|
3255
3254
|
const rectH = Math.max(y2 - y, 4);
|
|
3256
3255
|
|
|
3257
|
-
let fill: string = evColor;
|
|
3256
|
+
let fill: string = mix(evColor, bg, 30);
|
|
3257
|
+
let stroke: string = evColor;
|
|
3258
3258
|
if (ev.uncertain) {
|
|
3259
3259
|
const gradientId = `uncertain-vg-${ev.lineNumber}`;
|
|
3260
|
+
const strokeGradientId = `uncertain-vg-s-${ev.lineNumber}`;
|
|
3260
3261
|
const defs =
|
|
3261
3262
|
svg.select('defs').node() || svg.append('defs').node();
|
|
3262
|
-
d3Selection
|
|
3263
|
-
|
|
3263
|
+
const defsEl = d3Selection.select(defs as Element);
|
|
3264
|
+
defsEl
|
|
3264
3265
|
.append('linearGradient')
|
|
3265
3266
|
.attr('id', gradientId)
|
|
3266
3267
|
.attr('x1', '0%')
|
|
@@ -3276,9 +3277,28 @@ export function renderTimeline(
|
|
|
3276
3277
|
.enter()
|
|
3277
3278
|
.append('stop')
|
|
3278
3279
|
.attr('offset', (d) => d.offset)
|
|
3279
|
-
.attr('stop-color', laneColor)
|
|
3280
|
+
.attr('stop-color', mix(laneColor, bg, 30))
|
|
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)
|
|
3280
3299
|
.attr('stop-opacity', (d) => d.opacity);
|
|
3281
3300
|
fill = `url(#${gradientId})`;
|
|
3301
|
+
stroke = `url(#${strokeGradientId})`;
|
|
3282
3302
|
}
|
|
3283
3303
|
|
|
3284
3304
|
evG
|
|
@@ -3288,7 +3308,9 @@ export function renderTimeline(
|
|
|
3288
3308
|
.attr('width', 12)
|
|
3289
3309
|
.attr('height', rectH)
|
|
3290
3310
|
.attr('rx', 4)
|
|
3291
|
-
.attr('fill', fill)
|
|
3311
|
+
.attr('fill', fill)
|
|
3312
|
+
.attr('stroke', stroke)
|
|
3313
|
+
.attr('stroke-width', 2);
|
|
3292
3314
|
evG
|
|
3293
3315
|
.append('text')
|
|
3294
3316
|
.attr('x', laneCenter + 14)
|
|
@@ -3303,9 +3325,9 @@ export function renderTimeline(
|
|
|
3303
3325
|
.attr('cx', laneCenter)
|
|
3304
3326
|
.attr('cy', y)
|
|
3305
3327
|
.attr('r', 4)
|
|
3306
|
-
.attr('fill', evColor)
|
|
3307
|
-
.attr('stroke',
|
|
3308
|
-
.attr('stroke-width',
|
|
3328
|
+
.attr('fill', mix(evColor, bg, 30))
|
|
3329
|
+
.attr('stroke', evColor)
|
|
3330
|
+
.attr('stroke-width', 2);
|
|
3309
3331
|
evG
|
|
3310
3332
|
.append('text')
|
|
3311
3333
|
.attr('x', laneCenter + 10)
|
|
@@ -3322,9 +3344,9 @@ export function renderTimeline(
|
|
|
3322
3344
|
const scaleMargin = timelineScale ? 40 : 0;
|
|
3323
3345
|
const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
|
|
3324
3346
|
const margin = {
|
|
3325
|
-
top: 104 + markerMargin,
|
|
3347
|
+
top: 104 + markerMargin + tagLegendReserve,
|
|
3326
3348
|
right: 200,
|
|
3327
|
-
bottom: 40
|
|
3349
|
+
bottom: 40,
|
|
3328
3350
|
left: 60 + scaleMargin,
|
|
3329
3351
|
};
|
|
3330
3352
|
const innerWidth = width - margin.left - margin.right;
|
|
@@ -3472,13 +3494,15 @@ export function renderTimeline(
|
|
|
3472
3494
|
const y2 = yScale(parseTimelineDate(ev.endDate));
|
|
3473
3495
|
const rectH = Math.max(y2 - y, 4);
|
|
3474
3496
|
|
|
3475
|
-
let fill: string = color;
|
|
3497
|
+
let fill: string = mix(color, bg, 30);
|
|
3498
|
+
let stroke: string = color;
|
|
3476
3499
|
if (ev.uncertain) {
|
|
3477
3500
|
const gradientId = `uncertain-v-${ev.lineNumber}`;
|
|
3501
|
+
const strokeGradientId = `uncertain-v-s-${ev.lineNumber}`;
|
|
3478
3502
|
const defs =
|
|
3479
3503
|
svg.select('defs').node() || svg.append('defs').node();
|
|
3480
|
-
d3Selection
|
|
3481
|
-
|
|
3504
|
+
const defsEl = d3Selection.select(defs as Element);
|
|
3505
|
+
defsEl
|
|
3482
3506
|
.append('linearGradient')
|
|
3483
3507
|
.attr('id', gradientId)
|
|
3484
3508
|
.attr('x1', '0%')
|
|
@@ -3494,9 +3518,28 @@ export function renderTimeline(
|
|
|
3494
3518
|
.enter()
|
|
3495
3519
|
.append('stop')
|
|
3496
3520
|
.attr('offset', (d) => d.offset)
|
|
3521
|
+
.attr('stop-color', mix(color, bg, 30))
|
|
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)
|
|
3497
3539
|
.attr('stop-color', color)
|
|
3498
3540
|
.attr('stop-opacity', (d) => d.opacity);
|
|
3499
3541
|
fill = `url(#${gradientId})`;
|
|
3542
|
+
stroke = `url(#${strokeGradientId})`;
|
|
3500
3543
|
}
|
|
3501
3544
|
|
|
3502
3545
|
evG
|
|
@@ -3506,7 +3549,9 @@ export function renderTimeline(
|
|
|
3506
3549
|
.attr('width', 12)
|
|
3507
3550
|
.attr('height', rectH)
|
|
3508
3551
|
.attr('rx', 4)
|
|
3509
|
-
.attr('fill', fill)
|
|
3552
|
+
.attr('fill', fill)
|
|
3553
|
+
.attr('stroke', stroke)
|
|
3554
|
+
.attr('stroke-width', 2);
|
|
3510
3555
|
evG
|
|
3511
3556
|
.append('text')
|
|
3512
3557
|
.attr('x', axisX + 16)
|
|
@@ -3521,9 +3566,9 @@ export function renderTimeline(
|
|
|
3521
3566
|
.attr('cx', axisX)
|
|
3522
3567
|
.attr('cy', y)
|
|
3523
3568
|
.attr('r', 4)
|
|
3524
|
-
.attr('fill', color)
|
|
3525
|
-
.attr('stroke',
|
|
3526
|
-
.attr('stroke-width',
|
|
3569
|
+
.attr('fill', mix(color, bg, 30))
|
|
3570
|
+
.attr('stroke', color)
|
|
3571
|
+
.attr('stroke-width', 2);
|
|
3527
3572
|
evG
|
|
3528
3573
|
.append('text')
|
|
3529
3574
|
.attr('x', axisX + 16)
|
|
@@ -3603,9 +3648,9 @@ export function renderTimeline(
|
|
|
3603
3648
|
// Group-sorted doesn't need legend space (group names shown on left)
|
|
3604
3649
|
const baseTopMargin = title ? 50 : 20;
|
|
3605
3650
|
const margin = {
|
|
3606
|
-
top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin,
|
|
3651
|
+
top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
|
|
3607
3652
|
right: 40,
|
|
3608
|
-
bottom: 40 + scaleMargin
|
|
3653
|
+
bottom: 40 + scaleMargin,
|
|
3609
3654
|
left: dynamicLeftMargin,
|
|
3610
3655
|
};
|
|
3611
3656
|
const innerWidth = width - margin.left - margin.right;
|
|
@@ -3781,13 +3826,15 @@ export function renderTimeline(
|
|
|
3781
3826
|
const estLabelWidth = ev.label.length * 7 + 16;
|
|
3782
3827
|
const labelFitsInside = rectW >= estLabelWidth;
|
|
3783
3828
|
|
|
3784
|
-
let fill: string = evColor;
|
|
3829
|
+
let fill: string = mix(evColor, bg, 30);
|
|
3830
|
+
let stroke: string = evColor;
|
|
3785
3831
|
if (ev.uncertain) {
|
|
3786
3832
|
// Create gradient for uncertain end - fades last 20%
|
|
3787
3833
|
const gradientId = `uncertain-${ev.lineNumber}`;
|
|
3834
|
+
const strokeGradientId = `uncertain-s-${ev.lineNumber}`;
|
|
3788
3835
|
const defs = svg.select('defs').node() || svg.append('defs').node();
|
|
3789
|
-
d3Selection
|
|
3790
|
-
|
|
3836
|
+
const defsEl = d3Selection.select(defs as Element);
|
|
3837
|
+
defsEl
|
|
3791
3838
|
.append('linearGradient')
|
|
3792
3839
|
.attr('id', gradientId)
|
|
3793
3840
|
.attr('x1', '0%')
|
|
@@ -3803,9 +3850,28 @@ export function renderTimeline(
|
|
|
3803
3850
|
.enter()
|
|
3804
3851
|
.append('stop')
|
|
3805
3852
|
.attr('offset', (d) => d.offset)
|
|
3853
|
+
.attr('stop-color', mix(evColor, bg, 30))
|
|
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)
|
|
3806
3871
|
.attr('stop-color', evColor)
|
|
3807
3872
|
.attr('stop-opacity', (d) => d.opacity);
|
|
3808
3873
|
fill = `url(#${gradientId})`;
|
|
3874
|
+
stroke = `url(#${strokeGradientId})`;
|
|
3809
3875
|
}
|
|
3810
3876
|
|
|
3811
3877
|
evG
|
|
@@ -3815,17 +3881,19 @@ export function renderTimeline(
|
|
|
3815
3881
|
.attr('width', rectW)
|
|
3816
3882
|
.attr('height', BAR_H)
|
|
3817
3883
|
.attr('rx', 4)
|
|
3818
|
-
.attr('fill', fill)
|
|
3884
|
+
.attr('fill', fill)
|
|
3885
|
+
.attr('stroke', stroke)
|
|
3886
|
+
.attr('stroke-width', 2);
|
|
3819
3887
|
|
|
3820
3888
|
if (labelFitsInside) {
|
|
3821
|
-
// Text inside bar -
|
|
3889
|
+
// Text inside bar - use textColor for readability on muted fill
|
|
3822
3890
|
evG
|
|
3823
3891
|
.append('text')
|
|
3824
3892
|
.attr('x', x + 8)
|
|
3825
3893
|
.attr('y', y)
|
|
3826
3894
|
.attr('dy', '0.35em')
|
|
3827
3895
|
.attr('text-anchor', 'start')
|
|
3828
|
-
.attr('fill',
|
|
3896
|
+
.attr('fill', textColor)
|
|
3829
3897
|
.attr('font-size', '14px')
|
|
3830
3898
|
.attr('font-weight', '700')
|
|
3831
3899
|
.text(ev.label);
|
|
@@ -3856,9 +3924,9 @@ export function renderTimeline(
|
|
|
3856
3924
|
.attr('cx', x)
|
|
3857
3925
|
.attr('cy', y)
|
|
3858
3926
|
.attr('r', 5)
|
|
3859
|
-
.attr('fill', evColor)
|
|
3860
|
-
.attr('stroke',
|
|
3861
|
-
.attr('stroke-width',
|
|
3927
|
+
.attr('fill', mix(evColor, bg, 30))
|
|
3928
|
+
.attr('stroke', evColor)
|
|
3929
|
+
.attr('stroke-width', 2);
|
|
3862
3930
|
evG
|
|
3863
3931
|
.append('text')
|
|
3864
3932
|
.attr('x', flipLeft ? x - 10 : x + 10)
|
|
@@ -3882,9 +3950,9 @@ export function renderTimeline(
|
|
|
3882
3950
|
const scaleMargin = timelineScale ? 24 : 0;
|
|
3883
3951
|
const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
|
|
3884
3952
|
const margin = {
|
|
3885
|
-
top: 104 + (timelineScale ? 40 : 0) + markerMargin,
|
|
3953
|
+
top: 104 + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
|
|
3886
3954
|
right: 40,
|
|
3887
|
-
bottom: 40 + scaleMargin
|
|
3955
|
+
bottom: 40 + scaleMargin,
|
|
3888
3956
|
left: 60,
|
|
3889
3957
|
};
|
|
3890
3958
|
const innerWidth = width - margin.left - margin.right;
|
|
@@ -4041,13 +4109,15 @@ export function renderTimeline(
|
|
|
4041
4109
|
const estLabelWidth = ev.label.length * 7 + 16;
|
|
4042
4110
|
const labelFitsInside = rectW >= estLabelWidth;
|
|
4043
4111
|
|
|
4044
|
-
let fill: string = color;
|
|
4112
|
+
let fill: string = mix(color, bg, 30);
|
|
4113
|
+
let stroke: string = color;
|
|
4045
4114
|
if (ev.uncertain) {
|
|
4046
4115
|
// Create gradient for uncertain end - fades last 20%
|
|
4047
4116
|
const gradientId = `uncertain-ts-${ev.lineNumber}`;
|
|
4117
|
+
const strokeGradientId = `uncertain-ts-s-${ev.lineNumber}`;
|
|
4048
4118
|
const defs = svg.select('defs').node() || svg.append('defs').node();
|
|
4049
|
-
d3Selection
|
|
4050
|
-
|
|
4119
|
+
const defsEl = d3Selection.select(defs as Element);
|
|
4120
|
+
defsEl
|
|
4051
4121
|
.append('linearGradient')
|
|
4052
4122
|
.attr('id', gradientId)
|
|
4053
4123
|
.attr('x1', '0%')
|
|
@@ -4063,9 +4133,28 @@ export function renderTimeline(
|
|
|
4063
4133
|
.enter()
|
|
4064
4134
|
.append('stop')
|
|
4065
4135
|
.attr('offset', (d) => d.offset)
|
|
4136
|
+
.attr('stop-color', mix(color, bg, 30))
|
|
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)
|
|
4066
4154
|
.attr('stop-color', color)
|
|
4067
4155
|
.attr('stop-opacity', (d) => d.opacity);
|
|
4068
4156
|
fill = `url(#${gradientId})`;
|
|
4157
|
+
stroke = `url(#${strokeGradientId})`;
|
|
4069
4158
|
}
|
|
4070
4159
|
|
|
4071
4160
|
evG
|
|
@@ -4075,17 +4164,19 @@ export function renderTimeline(
|
|
|
4075
4164
|
.attr('width', rectW)
|
|
4076
4165
|
.attr('height', BAR_H)
|
|
4077
4166
|
.attr('rx', 4)
|
|
4078
|
-
.attr('fill', fill)
|
|
4167
|
+
.attr('fill', fill)
|
|
4168
|
+
.attr('stroke', stroke)
|
|
4169
|
+
.attr('stroke-width', 2);
|
|
4079
4170
|
|
|
4080
4171
|
if (labelFitsInside) {
|
|
4081
|
-
// Text inside bar -
|
|
4172
|
+
// Text inside bar - use textColor for readability on muted fill
|
|
4082
4173
|
evG
|
|
4083
4174
|
.append('text')
|
|
4084
4175
|
.attr('x', x + 8)
|
|
4085
4176
|
.attr('y', y)
|
|
4086
4177
|
.attr('dy', '0.35em')
|
|
4087
4178
|
.attr('text-anchor', 'start')
|
|
4088
|
-
.attr('fill',
|
|
4179
|
+
.attr('fill', textColor)
|
|
4089
4180
|
.attr('font-size', '14px')
|
|
4090
4181
|
.attr('font-weight', '700')
|
|
4091
4182
|
.text(ev.label);
|
|
@@ -4116,9 +4207,9 @@ export function renderTimeline(
|
|
|
4116
4207
|
.attr('cx', x)
|
|
4117
4208
|
.attr('cy', y)
|
|
4118
4209
|
.attr('r', 5)
|
|
4119
|
-
.attr('fill', color)
|
|
4120
|
-
.attr('stroke',
|
|
4121
|
-
.attr('stroke-width',
|
|
4210
|
+
.attr('fill', mix(color, bg, 30))
|
|
4211
|
+
.attr('stroke', color)
|
|
4212
|
+
.attr('stroke-width', 2);
|
|
4122
4213
|
evG
|
|
4123
4214
|
.append('text')
|
|
4124
4215
|
.attr('x', flipLeft ? x - 10 : x + 10)
|
|
@@ -4150,7 +4241,8 @@ export function renderTimeline(
|
|
|
4150
4241
|
const mainSvg = d3Selection.select(container).select<SVGSVGElement>('svg');
|
|
4151
4242
|
const mainG = mainSvg.select<SVGGElement>('g');
|
|
4152
4243
|
if (!mainSvg.empty() && !mainG.empty()) {
|
|
4153
|
-
|
|
4244
|
+
// Position legend at top, below title
|
|
4245
|
+
const legendY = title ? 50 : 10;
|
|
4154
4246
|
|
|
4155
4247
|
const groupBg = isDark
|
|
4156
4248
|
? mix(palette.surface, palette.bg, 50)
|
|
@@ -4438,8 +4530,8 @@ export function renderTimeline(
|
|
|
4438
4530
|
color = ev.group && groupColorMap.has(ev.group)
|
|
4439
4531
|
? groupColorMap.get(ev.group)! : textColor;
|
|
4440
4532
|
}
|
|
4441
|
-
el.selectAll('rect').attr('fill', color);
|
|
4442
|
-
el.selectAll('circle:not(.tl-event-point-outline)').attr('fill', color);
|
|
4533
|
+
el.selectAll('rect').attr('fill', mix(color, bg, 30)).attr('stroke', color);
|
|
4534
|
+
el.selectAll('circle:not(.tl-event-point-outline)').attr('fill', mix(color, bg, 30)).attr('stroke', color);
|
|
4443
4535
|
});
|
|
4444
4536
|
}
|
|
4445
4537
|
|
|
@@ -4635,46 +4727,6 @@ function renderWordCloudAsync(
|
|
|
4635
4727
|
// Venn Diagram Math Helpers
|
|
4636
4728
|
// ============================================================
|
|
4637
4729
|
|
|
4638
|
-
function radiusFromArea(area: number): number {
|
|
4639
|
-
return Math.sqrt(area / Math.PI);
|
|
4640
|
-
}
|
|
4641
|
-
|
|
4642
|
-
function circleOverlapArea(r1: number, r2: number, d: number): number {
|
|
4643
|
-
// No overlap
|
|
4644
|
-
if (d >= r1 + r2) return 0;
|
|
4645
|
-
// Full containment
|
|
4646
|
-
if (d + Math.min(r1, r2) <= Math.max(r1, r2)) {
|
|
4647
|
-
return Math.PI * Math.min(r1, r2) ** 2;
|
|
4648
|
-
}
|
|
4649
|
-
const part1 = r1 * r1 * Math.acos((d * d + r1 * r1 - r2 * r2) / (2 * d * r1));
|
|
4650
|
-
const part2 = r2 * r2 * Math.acos((d * d + r2 * r2 - r1 * r1) / (2 * d * r2));
|
|
4651
|
-
const part3 =
|
|
4652
|
-
0.5 *
|
|
4653
|
-
Math.sqrt((-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2));
|
|
4654
|
-
return part1 + part2 - part3;
|
|
4655
|
-
}
|
|
4656
|
-
|
|
4657
|
-
function distanceForOverlap(
|
|
4658
|
-
r1: number,
|
|
4659
|
-
r2: number,
|
|
4660
|
-
targetArea: number
|
|
4661
|
-
): number {
|
|
4662
|
-
if (targetArea <= 0) return r1 + r2;
|
|
4663
|
-
const minR = Math.min(r1, r2);
|
|
4664
|
-
if (targetArea >= Math.PI * minR * minR) return Math.abs(r1 - r2);
|
|
4665
|
-
let lo = Math.abs(r1 - r2);
|
|
4666
|
-
let hi = r1 + r2;
|
|
4667
|
-
for (let i = 0; i < 64; i++) {
|
|
4668
|
-
const mid = (lo + hi) / 2;
|
|
4669
|
-
if (circleOverlapArea(r1, r2, mid) > targetArea) {
|
|
4670
|
-
lo = mid;
|
|
4671
|
-
} else {
|
|
4672
|
-
hi = mid;
|
|
4673
|
-
}
|
|
4674
|
-
}
|
|
4675
|
-
return (lo + hi) / 2;
|
|
4676
|
-
}
|
|
4677
|
-
|
|
4678
4730
|
interface Point {
|
|
4679
4731
|
x: number;
|
|
4680
4732
|
y: number;
|
|
@@ -4686,29 +4738,6 @@ interface Circle {
|
|
|
4686
4738
|
r: number;
|
|
4687
4739
|
}
|
|
4688
4740
|
|
|
4689
|
-
function thirdCirclePosition(
|
|
4690
|
-
ax: number,
|
|
4691
|
-
ay: number,
|
|
4692
|
-
dAC: number,
|
|
4693
|
-
bx: number,
|
|
4694
|
-
by: number,
|
|
4695
|
-
dBC: number
|
|
4696
|
-
): Point {
|
|
4697
|
-
const dx = bx - ax;
|
|
4698
|
-
const dy = by - ay;
|
|
4699
|
-
const dAB = Math.sqrt(dx * dx + dy * dy);
|
|
4700
|
-
if (dAB === 0) return { x: ax + dAC, y: ay };
|
|
4701
|
-
const cosA = (dAB * dAB + dAC * dAC - dBC * dBC) / (2 * dAB * dAC);
|
|
4702
|
-
const sinA = Math.sqrt(Math.max(0, 1 - cosA * cosA));
|
|
4703
|
-
const ux = dx / dAB;
|
|
4704
|
-
const uy = dy / dAB;
|
|
4705
|
-
// Place C above the AB line
|
|
4706
|
-
return {
|
|
4707
|
-
x: ax + dAC * (cosA * ux - sinA * uy),
|
|
4708
|
-
y: ay + dAC * (cosA * uy + sinA * ux),
|
|
4709
|
-
};
|
|
4710
|
-
}
|
|
4711
|
-
|
|
4712
4741
|
function fitCirclesToContainerAsymmetric(
|
|
4713
4742
|
circles: Circle[],
|
|
4714
4743
|
w: number,
|
|
@@ -5255,7 +5284,6 @@ export function renderQuadrant(
|
|
|
5255
5284
|
const init = initD3Chart(container, palette, exportDims);
|
|
5256
5285
|
if (!init) return;
|
|
5257
5286
|
const { svg, width, height, textColor } = init;
|
|
5258
|
-
const mutedColor = palette.textMuted;
|
|
5259
5287
|
const borderColor = palette.border;
|
|
5260
5288
|
|
|
5261
5289
|
// Default quadrant colors with alpha
|
|
@@ -5300,14 +5328,24 @@ export function renderQuadrant(
|
|
|
5300
5328
|
return `#${c(ar,br)}${c(ag,bg)}${c(ab,bb)}`;
|
|
5301
5329
|
};
|
|
5302
5330
|
|
|
5303
|
-
|
|
5304
|
-
|
|
5331
|
+
const bg = isDark ? palette.surface : palette.bg;
|
|
5332
|
+
|
|
5333
|
+
// Full palette color for a quadrant (used for border and label tinting)
|
|
5334
|
+
const getQuadrantColor = (
|
|
5305
5335
|
label: QuadrantLabel | null,
|
|
5306
5336
|
defaultIdx: number
|
|
5307
5337
|
): string => {
|
|
5308
5338
|
return label?.color ?? defaultColors[defaultIdx % defaultColors.length];
|
|
5309
5339
|
};
|
|
5310
5340
|
|
|
5341
|
+
// Muted fill: palette color blended 30% toward bg — matches other chart fill style
|
|
5342
|
+
const getQuadrantFill = (
|
|
5343
|
+
label: QuadrantLabel | null,
|
|
5344
|
+
defaultIdx: number
|
|
5345
|
+
): string => {
|
|
5346
|
+
return mixHex(getQuadrantColor(label, defaultIdx), bg, 30);
|
|
5347
|
+
};
|
|
5348
|
+
|
|
5311
5349
|
// Quadrant definitions: position, rect bounds, label position
|
|
5312
5350
|
const quadrantDefs: {
|
|
5313
5351
|
position: QuadrantPosition;
|
|
@@ -5378,17 +5416,16 @@ export function renderQuadrant(
|
|
|
5378
5416
|
.attr('width', (d) => d.w)
|
|
5379
5417
|
.attr('height', (d) => d.h)
|
|
5380
5418
|
.attr('fill', (d) => getQuadrantFill(d.label, d.colorIdx))
|
|
5381
|
-
.attr('stroke',
|
|
5382
|
-
.attr('stroke-width',
|
|
5419
|
+
.attr('stroke', (d) => getQuadrantColor(d.label, d.colorIdx))
|
|
5420
|
+
.attr('stroke-width', 2);
|
|
5383
5421
|
|
|
5384
5422
|
// White text for points; quadrant labels use a darkened shade of their fill
|
|
5385
|
-
const contrastColor = '#ffffff';
|
|
5386
5423
|
const shadowColor = 'rgba(0,0,0,0.4)';
|
|
5387
5424
|
|
|
5388
|
-
// Darken the
|
|
5425
|
+
// Darken the full palette color (not the muted fill) to create a watermark-style label
|
|
5389
5426
|
const getQuadrantLabelColor = (d: (typeof quadrantDefs)[number]): string => {
|
|
5390
|
-
const
|
|
5391
|
-
return mixHex('#000000',
|
|
5427
|
+
const color = getQuadrantColor(d.label, d.colorIdx);
|
|
5428
|
+
return mixHex('#000000', color, 40);
|
|
5392
5429
|
};
|
|
5393
5430
|
|
|
5394
5431
|
// Scale label font size to fit within quadrant bounds, wrapping into multiple lines if needed
|
|
@@ -5666,13 +5703,13 @@ export function renderQuadrant(
|
|
|
5666
5703
|
.attr('stroke', pointColor)
|
|
5667
5704
|
.attr('stroke-width', 2);
|
|
5668
5705
|
|
|
5669
|
-
// Label (
|
|
5706
|
+
// Label (palette text color adapts to light/dark mode)
|
|
5670
5707
|
pointG
|
|
5671
5708
|
.append('text')
|
|
5672
5709
|
.attr('x', cx)
|
|
5673
5710
|
.attr('y', cy - 10)
|
|
5674
5711
|
.attr('text-anchor', 'middle')
|
|
5675
|
-
.attr('fill',
|
|
5712
|
+
.attr('fill', textColor)
|
|
5676
5713
|
.attr('font-size', '12px')
|
|
5677
5714
|
.attr('font-weight', '700')
|
|
5678
5715
|
.style('text-shadow', `0 1px 2px ${shadowColor}`)
|
|
@@ -5962,7 +5999,7 @@ export async function renderForExport(
|
|
|
5962
5999
|
const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
|
|
5963
6000
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
5964
6001
|
|
|
5965
|
-
renderInitiativeStatus(container, isParsed, isLayout, effectivePalette, theme === 'dark',
|
|
6002
|
+
renderInitiativeStatus(container, isParsed, isLayout, effectivePalette, theme === 'dark', { exportDims: { width: exportWidth, height: exportHeight } });
|
|
5966
6003
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
5967
6004
|
}
|
|
5968
6005
|
|
|
@@ -6051,6 +6088,26 @@ export async function renderForExport(
|
|
|
6051
6088
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6052
6089
|
}
|
|
6053
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
|
+
|
|
6054
6111
|
if (detectedType === 'state') {
|
|
6055
6112
|
const { parseState } = await import('./graph/state-parser');
|
|
6056
6113
|
const { layoutGraph } = await import('./graph/layout');
|