@diagrammo/dgmo 0.7.1 → 0.7.3
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 +165 -165
- package/dist/index.cjs +231 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +231 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/d3.ts +2 -0
- package/src/gantt/parser.ts +5 -0
- package/src/gantt/renderer.ts +353 -87
- package/src/gantt/types.ts +4 -0
- package/src/initiative-status/renderer.ts +41 -9
package/src/gantt/renderer.ts
CHANGED
|
@@ -39,6 +39,120 @@ const MILESTONE_SIZE = 10;
|
|
|
39
39
|
const MIN_LEFT_MARGIN = 120;
|
|
40
40
|
const BOTTOM_MARGIN = 40;
|
|
41
41
|
const RIGHT_MARGIN = 20;
|
|
42
|
+
const CHAR_W = 6.5; // estimated px per character for bar labels
|
|
43
|
+
const LABEL_PAD = 8; // inner padding to decide if label fits inside bar
|
|
44
|
+
const LABEL_GAP = 5; // gap between bar edge and external label
|
|
45
|
+
|
|
46
|
+
// ── Bar label placement ─────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
type BarLabelPlacement = {
|
|
49
|
+
x: number;
|
|
50
|
+
anchor: 'start' | 'end';
|
|
51
|
+
fill: string;
|
|
52
|
+
text: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function computeBarLabel(
|
|
56
|
+
label: string,
|
|
57
|
+
x1: number,
|
|
58
|
+
barWidth: number,
|
|
59
|
+
innerWidth: number,
|
|
60
|
+
textColor: string,
|
|
61
|
+
): BarLabelPlacement | null {
|
|
62
|
+
const textWidth = label.length * CHAR_W;
|
|
63
|
+
const x2 = x1 + barWidth;
|
|
64
|
+
|
|
65
|
+
// 1. Inside
|
|
66
|
+
if (textWidth < barWidth - LABEL_PAD) {
|
|
67
|
+
return { x: x1 + 6, anchor: 'start', fill: textColor, text: label };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. After (right of bar)
|
|
71
|
+
if (x2 + LABEL_GAP + textWidth <= innerWidth) {
|
|
72
|
+
return { x: x2 + LABEL_GAP, anchor: 'start', fill: textColor, text: label };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3. Before (left of bar)
|
|
76
|
+
if (x1 - LABEL_GAP - textWidth >= 0) {
|
|
77
|
+
return { x: x1 - LABEL_GAP, anchor: 'end', fill: textColor, text: label };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. Truncate to fit before the bar
|
|
81
|
+
const availWidth = x1 - LABEL_GAP;
|
|
82
|
+
if (availWidth > CHAR_W * 3) {
|
|
83
|
+
const maxChars = Math.floor(availWidth / CHAR_W) - 1;
|
|
84
|
+
return { x: x1 - LABEL_GAP, anchor: 'end', fill: textColor, text: label.slice(0, maxChars) + '\u2026' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Left-panel visual helpers ───────────────────────────────
|
|
91
|
+
|
|
92
|
+
const BAND_ACCENT_W = 4;
|
|
93
|
+
const BAND_RADIUS = 4;
|
|
94
|
+
let bandClipCounter = 0;
|
|
95
|
+
|
|
96
|
+
function renderLabelBand(
|
|
97
|
+
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
98
|
+
y: number,
|
|
99
|
+
leftMargin: number,
|
|
100
|
+
color: string,
|
|
101
|
+
palette: PaletteColors,
|
|
102
|
+
cssPrefix: 'group' | 'lane',
|
|
103
|
+
dataAttr?: { key: string; value: string },
|
|
104
|
+
): void {
|
|
105
|
+
const bandX = 5;
|
|
106
|
+
const bandW = leftMargin - 7;
|
|
107
|
+
const bandY = y - BAR_H / 2;
|
|
108
|
+
const clipId = `gantt-band-clip-${bandClipCounter++}`;
|
|
109
|
+
|
|
110
|
+
// ClipPath matching the tint band shape
|
|
111
|
+
svg.append('clipPath').attr('id', clipId)
|
|
112
|
+
.append('rect')
|
|
113
|
+
.attr('x', bandX).attr('y', bandY)
|
|
114
|
+
.attr('width', bandW).attr('height', BAR_H)
|
|
115
|
+
.attr('rx', BAND_RADIUS);
|
|
116
|
+
|
|
117
|
+
// Tint band
|
|
118
|
+
const tint = svg.append('rect')
|
|
119
|
+
.attr('class', `gantt-${cssPrefix}-band-bg`)
|
|
120
|
+
.attr('x', bandX)
|
|
121
|
+
.attr('y', bandY)
|
|
122
|
+
.attr('width', bandW)
|
|
123
|
+
.attr('height', BAR_H)
|
|
124
|
+
.attr('rx', BAND_RADIUS)
|
|
125
|
+
.attr('fill', mix(color, palette.bg, 20))
|
|
126
|
+
.style('pointer-events', 'none');
|
|
127
|
+
|
|
128
|
+
// Accent strip inside the tint, clipped to the band's rounded shape
|
|
129
|
+
const accent = svg.append('rect')
|
|
130
|
+
.attr('class', `gantt-${cssPrefix}-band-accent`)
|
|
131
|
+
.attr('x', bandX)
|
|
132
|
+
.attr('y', bandY)
|
|
133
|
+
.attr('width', BAND_ACCENT_W)
|
|
134
|
+
.attr('height', BAR_H)
|
|
135
|
+
.attr('fill', color)
|
|
136
|
+
.attr('clip-path', `url(#${clipId})`)
|
|
137
|
+
.style('pointer-events', 'none');
|
|
138
|
+
|
|
139
|
+
if (dataAttr) {
|
|
140
|
+
tint.attr(dataAttr.key, dataAttr.value);
|
|
141
|
+
accent.attr(dataAttr.key, dataAttr.value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function appendTaskIcon(
|
|
146
|
+
textEl: d3Selection.Selection<SVGTextElement, unknown, null, undefined>,
|
|
147
|
+
label: string,
|
|
148
|
+
isMilestone: boolean,
|
|
149
|
+
iconColor: string,
|
|
150
|
+
textColor: string,
|
|
151
|
+
): void {
|
|
152
|
+
const icon = isMilestone ? '◆' : '●';
|
|
153
|
+
textEl.append('tspan').attr('fill', iconColor).text(icon);
|
|
154
|
+
textEl.append('tspan').attr('fill', textColor).text(' ' + label);
|
|
155
|
+
}
|
|
42
156
|
|
|
43
157
|
// ── Interactive Options ─────────────────────────────────────
|
|
44
158
|
|
|
@@ -67,6 +181,7 @@ export function renderGantt(
|
|
|
67
181
|
): void {
|
|
68
182
|
// Clear previous content
|
|
69
183
|
container.innerHTML = '';
|
|
184
|
+
bandClipCounter = 0;
|
|
70
185
|
|
|
71
186
|
if (resolved.tasks.length === 0) return;
|
|
72
187
|
|
|
@@ -98,15 +213,18 @@ export function renderGantt(
|
|
|
98
213
|
const rows = tagRows ?? buildRowList(resolved, collapsedGroups);
|
|
99
214
|
const isTagMode = tagRows !== null;
|
|
100
215
|
|
|
101
|
-
// Compute left margin based on longest visible label
|
|
216
|
+
// Compute left margin based on longest visible label (include ● /◆ prefix for tasks)
|
|
102
217
|
const allLabels = isTagMode
|
|
103
218
|
? [
|
|
104
219
|
...rows.filter((r): r is LaneHeaderRow => r.type === 'lane-header').map(r => r.laneName),
|
|
105
|
-
...rows.filter((r): r is TaskRow => r.type === 'task').map(r => r.task.task.label),
|
|
220
|
+
...rows.filter((r): r is TaskRow => r.type === 'task').map(r => '● ' + r.task.task.label),
|
|
106
221
|
]
|
|
107
222
|
: [
|
|
108
|
-
...resolved.tasks.map(t => t.task.label),
|
|
109
|
-
...resolved.groups.map(g =>
|
|
223
|
+
...resolved.tasks.map(t => '● ' + t.task.label),
|
|
224
|
+
...resolved.groups.map(g => {
|
|
225
|
+
const px = g.depth <= 2 ? g.depth * 14 : 2 * 14 + (g.depth - 2) * 8;
|
|
226
|
+
return ' '.repeat(Math.ceil(px / 7)) + g.name;
|
|
227
|
+
}),
|
|
110
228
|
];
|
|
111
229
|
const maxLabelLen = Math.max(...allLabels.map(l => l.length), 10);
|
|
112
230
|
const leftMargin = Math.max(MIN_LEFT_MARGIN, maxLabelLen * 7 + 30);
|
|
@@ -118,9 +236,11 @@ export function renderGantt(
|
|
|
118
236
|
const titleHeight = title ? 50 : 20;
|
|
119
237
|
const tagLegendReserve = resolved.tagGroups.length > 0 ? LEGEND_HEIGHT + 8 : 0;
|
|
120
238
|
const topDateLabelReserve = 22; // tick (6) + gap (4) + label height (~12)
|
|
239
|
+
const hasOverheadLabels = resolved.markers.length > 0 || resolved.eras.length > 0;
|
|
240
|
+
const markerLabelReserve = hasOverheadLabels ? 18 : 0; // markers/eras extend above date labels
|
|
121
241
|
const CONTENT_TOP_PAD = 16; // breathing room between scale labels and first row
|
|
122
242
|
|
|
123
|
-
const marginTop = titleHeight + tagLegendReserve + topDateLabelReserve;
|
|
243
|
+
const marginTop = titleHeight + tagLegendReserve + topDateLabelReserve + markerLabelReserve;
|
|
124
244
|
|
|
125
245
|
// Content area
|
|
126
246
|
const contentH = isTagMode
|
|
@@ -171,7 +291,7 @@ export function renderGantt(
|
|
|
171
291
|
const legendY = titleHeight;
|
|
172
292
|
renderTagLegend(
|
|
173
293
|
svg, g, resolved.tagGroups, currentActiveGroup, leftMargin, innerWidth,
|
|
174
|
-
legendY, palette, isDark, hasCriticalPath, criticalPathActive,
|
|
294
|
+
legendY, palette, isDark, hasCriticalPath, criticalPathActive, resolved.options.optionLineNumbers,
|
|
175
295
|
(groupName) => {
|
|
176
296
|
// Toggle active group
|
|
177
297
|
currentActiveGroup = currentActiveGroup?.toLowerCase() === groupName.toLowerCase()
|
|
@@ -230,7 +350,48 @@ export function renderGantt(
|
|
|
230
350
|
|
|
231
351
|
renderWeekendBands(g, resolved, xScale, innerHeight, palette, isDark);
|
|
232
352
|
renderHolidayBands(g, svg, resolved, xScale, innerHeight, palette, isDark, marginTop - 4, leftMargin, onClickItem);
|
|
233
|
-
renderErasAndMarkers(g, resolved, xScale, innerHeight, palette);
|
|
353
|
+
renderErasAndMarkers(g, svg, resolved, xScale, innerHeight, palette);
|
|
354
|
+
|
|
355
|
+
// ── Today marker (line rendered before rows so it paints behind task bars) ──
|
|
356
|
+
|
|
357
|
+
let todayDate: Date | null = null;
|
|
358
|
+
let todayX = -1;
|
|
359
|
+
const todayColor = palette.accent || '#e74c3c';
|
|
360
|
+
const todayMarkerLineNum = resolved.options.optionLineNumbers['today-marker'];
|
|
361
|
+
if (resolved.options.todayMarker !== 'off') {
|
|
362
|
+
if (resolved.options.todayMarker === 'on') {
|
|
363
|
+
todayDate = new Date();
|
|
364
|
+
} else {
|
|
365
|
+
todayDate = new Date(resolved.options.todayMarker + 'T00:00:00');
|
|
366
|
+
}
|
|
367
|
+
todayX = xScale(dateToFractionalYear(todayDate));
|
|
368
|
+
if (todayX >= 0 && todayX <= innerWidth) {
|
|
369
|
+
const todayLine = g.append('line')
|
|
370
|
+
.attr('class', 'gantt-today')
|
|
371
|
+
.attr('x1', todayX)
|
|
372
|
+
.attr('y1', 0)
|
|
373
|
+
.attr('x2', todayX)
|
|
374
|
+
.attr('y2', innerHeight + 10)
|
|
375
|
+
.attr('stroke', todayColor)
|
|
376
|
+
.attr('stroke-width', 2)
|
|
377
|
+
.attr('stroke-dasharray', '6 4')
|
|
378
|
+
.attr('opacity', 0.7)
|
|
379
|
+
.attr('pointer-events', 'none');
|
|
380
|
+
if (todayMarkerLineNum) todayLine.attr('data-line-number', String(todayMarkerLineNum));
|
|
381
|
+
|
|
382
|
+
const todayLabel = g.append('text')
|
|
383
|
+
.attr('class', 'gantt-today')
|
|
384
|
+
.attr('x', todayX)
|
|
385
|
+
.attr('y', innerHeight + 24)
|
|
386
|
+
.attr('text-anchor', 'middle')
|
|
387
|
+
.attr('font-size', '10px')
|
|
388
|
+
.attr('fill', todayColor)
|
|
389
|
+
.attr('opacity', 0.7)
|
|
390
|
+
.attr('pointer-events', 'none')
|
|
391
|
+
.text('Today');
|
|
392
|
+
if (todayMarkerLineNum) todayLabel.attr('data-line-number', String(todayMarkerLineNum));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
234
395
|
|
|
235
396
|
// ── Render rows ─────────────────────────────────────────
|
|
236
397
|
|
|
@@ -279,6 +440,7 @@ export function renderGantt(
|
|
|
279
440
|
|
|
280
441
|
lanePositions.set(row.laneName, { x1: lx1, x2: lx1 + laneBarWidth, y: yOffset + BAR_H / 2 });
|
|
281
442
|
|
|
443
|
+
renderLabelBand(svg, marginTop + yOffset + BAR_H / 2, leftMargin, laneColor, palette, 'lane', { key: 'data-lane', value: row.laneName });
|
|
282
444
|
const labelG = svg
|
|
283
445
|
.append('g')
|
|
284
446
|
.attr('class', 'gantt-lane-header')
|
|
@@ -346,7 +508,6 @@ export function renderGantt(
|
|
|
346
508
|
.attr('y', yOffset)
|
|
347
509
|
.attr('width', laneBarWidth * Math.min(row.aggregateProgress / 100, 1))
|
|
348
510
|
.attr('height', BAR_H)
|
|
349
|
-
.attr('rx', 4)
|
|
350
511
|
.attr('fill', laneColor)
|
|
351
512
|
.attr('opacity', 0.5)
|
|
352
513
|
.attr('pointer-events', 'none');
|
|
@@ -363,6 +524,7 @@ export function renderGantt(
|
|
|
363
524
|
// Group label with toggle — resolve tag color from group metadata
|
|
364
525
|
const tagColor = resolveTagColor(group.metadata, resolved.tagGroups, currentActiveGroup, true);
|
|
365
526
|
const groupColor = (tagColor && tagColor !== '#999999') ? tagColor : (group.color || palette.textMuted);
|
|
527
|
+
renderLabelBand(svg, marginTop + yOffset + BAR_H / 2, leftMargin, groupColor, palette, 'group', { key: 'data-group', value: group.name });
|
|
366
528
|
const labelG = svg
|
|
367
529
|
.append('g')
|
|
368
530
|
.attr('class', 'gantt-group-label')
|
|
@@ -381,7 +543,8 @@ export function renderGantt(
|
|
|
381
543
|
hideGanttDateIndicators(g);
|
|
382
544
|
});
|
|
383
545
|
|
|
384
|
-
const
|
|
546
|
+
const groupIndent = group.depth <= 2 ? group.depth * 14 : 2 * 14 + (group.depth - 2) * 8;
|
|
547
|
+
const labelX = 10 + groupIndent;
|
|
385
548
|
labelG
|
|
386
549
|
.append('text')
|
|
387
550
|
.attr('x', labelX)
|
|
@@ -433,11 +596,27 @@ export function renderGantt(
|
|
|
433
596
|
.attr('y', yOffset)
|
|
434
597
|
.attr('width', barWidth * Math.min(group.progress / 100, 1))
|
|
435
598
|
.attr('height', BAR_H)
|
|
436
|
-
.attr('rx', 4)
|
|
437
599
|
.attr('fill', groupColor)
|
|
438
600
|
.attr('opacity', 0.5);
|
|
439
601
|
}
|
|
440
602
|
|
|
603
|
+
// Bar label (inside → after → before → truncate)
|
|
604
|
+
const summaryLabel = group.name + (group.progress !== null ? ` ${Math.round(group.progress)}%` : '');
|
|
605
|
+
const summaryPlacement = computeBarLabel(summaryLabel, gx1, barWidth, innerWidth, palette.text);
|
|
606
|
+
if (summaryPlacement) {
|
|
607
|
+
summaryG
|
|
608
|
+
.append('text')
|
|
609
|
+
.attr('x', summaryPlacement.x)
|
|
610
|
+
.attr('y', yOffset + BAR_H / 2)
|
|
611
|
+
.attr('dy', '0.35em')
|
|
612
|
+
.attr('font-size', '10px')
|
|
613
|
+
.attr('font-weight', 'bold')
|
|
614
|
+
.attr('text-anchor', summaryPlacement.anchor)
|
|
615
|
+
.attr('fill', summaryPlacement.fill)
|
|
616
|
+
.attr('pointer-events', 'none')
|
|
617
|
+
.text(summaryPlacement.text);
|
|
618
|
+
}
|
|
619
|
+
|
|
441
620
|
// Track collapsed group position for dependency arrow redirection
|
|
442
621
|
groupPositions.set(group.name, { x1: gx1, x2: gx1 + barWidth, y: yOffset + BAR_H / 2 });
|
|
443
622
|
} else {
|
|
@@ -475,10 +654,26 @@ export function renderGantt(
|
|
|
475
654
|
.attr('y', yOffset)
|
|
476
655
|
.attr('width', groupBarWidth * Math.min(group.progress / 100, 1))
|
|
477
656
|
.attr('height', BAR_H)
|
|
478
|
-
.attr('rx', 4)
|
|
479
657
|
.attr('fill', groupColor)
|
|
480
658
|
.attr('opacity', 0.5);
|
|
481
659
|
}
|
|
660
|
+
|
|
661
|
+
// Bar label (inside → after → before → truncate)
|
|
662
|
+
const expandedLabel = group.name + (group.progress !== null ? ` ${Math.round(group.progress)}%` : '');
|
|
663
|
+
const expandedPlacement = computeBarLabel(expandedLabel, gx1, groupBarWidth, innerWidth, palette.text);
|
|
664
|
+
if (expandedPlacement) {
|
|
665
|
+
groupBarG
|
|
666
|
+
.append('text')
|
|
667
|
+
.attr('x', expandedPlacement.x)
|
|
668
|
+
.attr('y', yOffset + BAR_H / 2)
|
|
669
|
+
.attr('dy', '0.35em')
|
|
670
|
+
.attr('font-size', '10px')
|
|
671
|
+
.attr('font-weight', 'bold')
|
|
672
|
+
.attr('text-anchor', expandedPlacement.anchor)
|
|
673
|
+
.attr('fill', expandedPlacement.fill)
|
|
674
|
+
.attr('pointer-events', 'none')
|
|
675
|
+
.text(expandedPlacement.text);
|
|
676
|
+
}
|
|
482
677
|
}
|
|
483
678
|
}
|
|
484
679
|
|
|
@@ -487,8 +682,13 @@ export function renderGantt(
|
|
|
487
682
|
const rt = row.task;
|
|
488
683
|
const task = rt.task;
|
|
489
684
|
|
|
685
|
+
// Resolve bar color early so icon tspan can use it
|
|
686
|
+
const barColor = resolveTaskColor(rt, currentActiveGroup, resolved, seriesColors, palette);
|
|
687
|
+
|
|
490
688
|
// Task label on the left (left-aligned with indent; flat in tag mode)
|
|
491
|
-
const
|
|
689
|
+
const depth = rt.groupPath.length;
|
|
690
|
+
const indent = depth <= 2 ? depth * 14 : 2 * 14 + (depth - 2) * 8;
|
|
691
|
+
const taskLabelX = isTagMode ? 20 : 6 + indent;
|
|
492
692
|
const topGroup = rt.groupPath.length > 0 ? rt.groupPath[0] : null;
|
|
493
693
|
const taskLabel = svg
|
|
494
694
|
.append('text')
|
|
@@ -503,7 +703,6 @@ export function renderGantt(
|
|
|
503
703
|
.attr('data-task-id', task.id)
|
|
504
704
|
.attr('data-group', topGroup)
|
|
505
705
|
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
506
|
-
.text(task.label)
|
|
507
706
|
.on('click', () => {
|
|
508
707
|
if (onClickItem) onClickItem(task.lineNumber);
|
|
509
708
|
})
|
|
@@ -518,6 +717,8 @@ export function renderGantt(
|
|
|
518
717
|
resetHighlight(g, svg);
|
|
519
718
|
});
|
|
520
719
|
|
|
720
|
+
appendTaskIcon(taskLabel, task.label, rt.isMilestone, barColor, palette.text);
|
|
721
|
+
|
|
521
722
|
// Tag attributes on label for legend hover matching
|
|
522
723
|
for (const [key, value] of Object.entries(rt.effectiveMetadata)) {
|
|
523
724
|
taskLabel.attr(`data-tag-${key}`, value.toLowerCase());
|
|
@@ -526,9 +727,6 @@ export function renderGantt(
|
|
|
526
727
|
taskLabel.attr('data-critical-path', 'true');
|
|
527
728
|
}
|
|
528
729
|
|
|
529
|
-
// Determine color
|
|
530
|
-
let barColor = resolveTaskColor(rt, currentActiveGroup, resolved, seriesColors, palette);
|
|
531
|
-
|
|
532
730
|
if (rt.isMilestone) {
|
|
533
731
|
// Render diamond
|
|
534
732
|
const mx = xScale(dateToFractionalYear(rt.startDate));
|
|
@@ -680,7 +878,6 @@ export function renderGantt(
|
|
|
680
878
|
.attr('y', yOffset)
|
|
681
879
|
.attr('width', progressWidth)
|
|
682
880
|
.attr('height', BAR_H)
|
|
683
|
-
.attr('rx', 4)
|
|
684
881
|
.attr('fill', progressFill)
|
|
685
882
|
.attr('opacity', 0.5);
|
|
686
883
|
}
|
|
@@ -691,18 +888,19 @@ export function renderGantt(
|
|
|
691
888
|
}
|
|
692
889
|
|
|
693
890
|
|
|
694
|
-
//
|
|
695
|
-
const
|
|
696
|
-
if (
|
|
891
|
+
// Bar label (inside → after → before → truncate)
|
|
892
|
+
const labelPlacement = computeBarLabel(task.label, x1, barWidth, innerWidth, palette.text);
|
|
893
|
+
if (labelPlacement) {
|
|
697
894
|
taskG
|
|
698
895
|
.append('text')
|
|
699
|
-
.attr('x',
|
|
896
|
+
.attr('x', labelPlacement.x)
|
|
700
897
|
.attr('y', yOffset + BAR_H / 2)
|
|
701
898
|
.attr('dy', '0.35em')
|
|
702
899
|
.attr('font-size', '10px')
|
|
703
|
-
.attr('
|
|
900
|
+
.attr('text-anchor', labelPlacement.anchor)
|
|
901
|
+
.attr('fill', labelPlacement.fill)
|
|
704
902
|
.attr('pointer-events', 'none')
|
|
705
|
-
.text(
|
|
903
|
+
.text(labelPlacement.text);
|
|
706
904
|
}
|
|
707
905
|
|
|
708
906
|
// Track bar position for arrows
|
|
@@ -713,38 +911,42 @@ export function renderGantt(
|
|
|
713
911
|
}
|
|
714
912
|
}
|
|
715
913
|
|
|
716
|
-
// ── Today
|
|
914
|
+
// ── Today hover overlay (rendered after rows so it receives pointer events) ──
|
|
717
915
|
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
} else {
|
|
723
|
-
todayDate = new Date(resolved.options.todayMarker + 'T00:00:00');
|
|
724
|
-
}
|
|
725
|
-
const todayX = xScale(dateToFractionalYear(todayDate));
|
|
726
|
-
if (todayX >= 0 && todayX <= innerWidth) {
|
|
727
|
-
g.append('line')
|
|
728
|
-
.attr('class', 'gantt-today')
|
|
729
|
-
.attr('x1', todayX)
|
|
730
|
-
.attr('y1', 0)
|
|
731
|
-
.attr('x2', todayX)
|
|
732
|
-
.attr('y2', innerHeight + 10)
|
|
733
|
-
.attr('stroke', palette.accent || '#e74c3c')
|
|
734
|
-
.attr('stroke-width', 2)
|
|
735
|
-
.attr('stroke-dasharray', '6 4')
|
|
736
|
-
.attr('opacity', 0.7);
|
|
916
|
+
if (todayDate && todayX >= 0 && todayX <= innerWidth) {
|
|
917
|
+
const todayHoverG = g.append('g')
|
|
918
|
+
.attr('class', 'gantt-today-hover')
|
|
919
|
+
.style('cursor', 'pointer');
|
|
737
920
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
921
|
+
// Invisible wide hit rect for easy hovering
|
|
922
|
+
todayHoverG.append('rect')
|
|
923
|
+
.attr('x', todayX - 10)
|
|
924
|
+
.attr('y', -6)
|
|
925
|
+
.attr('width', 20)
|
|
926
|
+
.attr('height', innerHeight + 16)
|
|
927
|
+
.attr('fill', 'transparent')
|
|
928
|
+
.attr('pointer-events', 'all');
|
|
929
|
+
|
|
930
|
+
const todayDateObj = todayDate;
|
|
931
|
+
todayHoverG
|
|
932
|
+
.on('mouseenter', () => {
|
|
933
|
+
// Fade everything
|
|
934
|
+
g.selectAll<SVGGElement, unknown>('.gantt-task').attr('opacity', FADE_OPACITY);
|
|
935
|
+
g.selectAll<SVGElement, unknown>('.gantt-milestone').attr('opacity', FADE_OPACITY);
|
|
936
|
+
g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
|
|
937
|
+
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
|
|
938
|
+
svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', FADE_OPACITY);
|
|
939
|
+
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
|
|
940
|
+
g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
|
|
941
|
+
g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
|
|
942
|
+
g.selectAll<SVGElement, unknown>('.gantt-era-group').attr('opacity', FADE_OPACITY);
|
|
943
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', FADE_OPACITY);
|
|
944
|
+
showGanttDateIndicators(g, xScale, todayDateObj, null, innerHeight, todayColor);
|
|
945
|
+
})
|
|
946
|
+
.on('mouseleave', () => {
|
|
947
|
+
resetHighlight(g, svg);
|
|
948
|
+
hideGanttDateIndicators(g);
|
|
949
|
+
});
|
|
748
950
|
}
|
|
749
951
|
|
|
750
952
|
// ── Dependency arrows ───────────────────────────────────
|
|
@@ -1029,6 +1231,7 @@ function renderDependencyArrows(
|
|
|
1029
1231
|
.attr('class', 'gantt-dep-arrow')
|
|
1030
1232
|
.attr('data-dep-from', rt.task.id)
|
|
1031
1233
|
.attr('data-dep-to', targetTask.task.id)
|
|
1234
|
+
.attr('data-line-number', String(dep.lineNumber))
|
|
1032
1235
|
.attr('data-critical-path', isCpArrow ? 'true' : null)
|
|
1033
1236
|
.attr('d', path)
|
|
1034
1237
|
.attr('fill', 'none')
|
|
@@ -1043,6 +1246,7 @@ function renderDependencyArrows(
|
|
|
1043
1246
|
.attr('class', 'gantt-dep-arrowhead')
|
|
1044
1247
|
.attr('data-dep-from', rt.task.id)
|
|
1045
1248
|
.attr('data-dep-to', targetTask.task.id)
|
|
1249
|
+
.attr('data-line-number', String(dep.lineNumber))
|
|
1046
1250
|
.attr('data-critical-path', isCpArrow ? 'true' : null)
|
|
1047
1251
|
.attr('points', arrowheadPoints(tx, ty, headSize, angle))
|
|
1048
1252
|
.attr('fill', arrowColor)
|
|
@@ -1074,7 +1278,9 @@ function applyCriticalPathHighlight(
|
|
|
1074
1278
|
el.attr('opacity', el.attr('data-critical-path') === 'true' ? 1 : FADE_OPACITY);
|
|
1075
1279
|
});
|
|
1076
1280
|
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
|
|
1281
|
+
svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', FADE_OPACITY);
|
|
1077
1282
|
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
|
|
1283
|
+
svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', FADE_OPACITY);
|
|
1078
1284
|
chartG.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr('opacity', FADE_OPACITY);
|
|
1079
1285
|
// Show critical path arrows at full opacity, fade others
|
|
1080
1286
|
chartG.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').each(function () {
|
|
@@ -1091,7 +1297,9 @@ function resetHighlightAll(
|
|
|
1091
1297
|
chartG.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', 1);
|
|
1092
1298
|
svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', 1);
|
|
1093
1299
|
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', 1);
|
|
1300
|
+
svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', 1);
|
|
1094
1301
|
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', 1);
|
|
1302
|
+
svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', 1);
|
|
1095
1303
|
chartG.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr('opacity', 1);
|
|
1096
1304
|
chartG.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', 0.5);
|
|
1097
1305
|
}
|
|
@@ -1141,6 +1349,7 @@ function renderTagLegend(
|
|
|
1141
1349
|
isDark: boolean,
|
|
1142
1350
|
hasCriticalPath: boolean,
|
|
1143
1351
|
criticalPathActive: boolean,
|
|
1352
|
+
optionLineNumbers: Record<string, number>,
|
|
1144
1353
|
onToggle?: (groupName: string) => void,
|
|
1145
1354
|
onToggleCriticalPath?: () => void,
|
|
1146
1355
|
currentSwimlaneGroup?: string | null,
|
|
@@ -1224,8 +1433,9 @@ function renderTagLegend(
|
|
|
1224
1433
|
totalW += cpPillW;
|
|
1225
1434
|
}
|
|
1226
1435
|
|
|
1227
|
-
// Center over
|
|
1228
|
-
const
|
|
1436
|
+
// Center over full container (matching title centering)
|
|
1437
|
+
const containerWidth = chartLeftMargin + chartInnerWidth + RIGHT_MARGIN;
|
|
1438
|
+
const legendX = (containerWidth - totalW) / 2;
|
|
1229
1439
|
|
|
1230
1440
|
const legendRow = svg.append('g')
|
|
1231
1441
|
.attr('class', 'gantt-tag-legend-container')
|
|
@@ -1247,6 +1457,7 @@ function renderTagLegend(
|
|
|
1247
1457
|
.attr('transform', `translate(${cursorX}, 0)`)
|
|
1248
1458
|
.attr('class', 'gantt-tag-legend-group')
|
|
1249
1459
|
.attr('data-tag-group', group.name)
|
|
1460
|
+
.attr('data-line-number', String(group.lineNumber))
|
|
1250
1461
|
.style('cursor', 'pointer')
|
|
1251
1462
|
.on('click', () => { if (onToggle) onToggle(group.name); });
|
|
1252
1463
|
|
|
@@ -1325,6 +1536,7 @@ function renderTagLegend(
|
|
|
1325
1536
|
// Wrap dot + label in a <g> for hover targeting
|
|
1326
1537
|
const entryG = gEl.append('g')
|
|
1327
1538
|
.attr('class', 'gantt-legend-entry')
|
|
1539
|
+
.attr('data-line-number', String(entry.lineNumber))
|
|
1328
1540
|
.style('cursor', 'pointer');
|
|
1329
1541
|
|
|
1330
1542
|
// Dot
|
|
@@ -1386,11 +1598,13 @@ function renderTagLegend(
|
|
|
1386
1598
|
|
|
1387
1599
|
// Critical Path pill
|
|
1388
1600
|
if (hasCriticalPath) {
|
|
1601
|
+
const cpLineNum = optionLineNumbers['critical-path'];
|
|
1389
1602
|
const cpG = legendRow.append('g')
|
|
1390
1603
|
.attr('transform', `translate(${cursorX}, 0)`)
|
|
1391
1604
|
.attr('class', 'gantt-legend-critical-path')
|
|
1392
1605
|
.style('cursor', 'pointer')
|
|
1393
1606
|
.on('click', () => { if (onToggleCriticalPath) onToggleCriticalPath(); });
|
|
1607
|
+
if (cpLineNum) cpG.attr('data-line-number', String(cpLineNum));
|
|
1394
1608
|
|
|
1395
1609
|
cpG.append('rect')
|
|
1396
1610
|
.attr('width', cpPillW)
|
|
@@ -1440,6 +1654,7 @@ const ERA_COLORS = ['#5e81ac', '#a3be8c', '#ebcb8b', '#d08770', '#b48ead'];
|
|
|
1440
1654
|
|
|
1441
1655
|
function renderErasAndMarkers(
|
|
1442
1656
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
1657
|
+
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1443
1658
|
resolved: ResolvedSchedule,
|
|
1444
1659
|
xScale: d3Scale.ScaleLinear<number, number>,
|
|
1445
1660
|
innerHeight: number,
|
|
@@ -1459,7 +1674,8 @@ function renderErasAndMarkers(
|
|
|
1459
1674
|
const eraEndDate = parseDateStringToDate(era.endDate);
|
|
1460
1675
|
|
|
1461
1676
|
const eraG = g.append('g')
|
|
1462
|
-
.attr('class', 'gantt-era-group')
|
|
1677
|
+
.attr('class', 'gantt-era-group')
|
|
1678
|
+
.attr('data-line-number', String(era.lineNumber));
|
|
1463
1679
|
|
|
1464
1680
|
const eraRect = eraG.append('rect')
|
|
1465
1681
|
.attr('class', 'gantt-era')
|
|
@@ -1470,24 +1686,36 @@ function renderErasAndMarkers(
|
|
|
1470
1686
|
.attr('fill', color)
|
|
1471
1687
|
.attr('opacity', baseEraOpacity);
|
|
1472
1688
|
|
|
1473
|
-
// Era label (
|
|
1689
|
+
// Era label (above date scale, same zone as markers)
|
|
1474
1690
|
eraG.append('text')
|
|
1475
1691
|
.attr('class', 'gantt-era-label')
|
|
1476
1692
|
.attr('x', (sx + ex) / 2)
|
|
1477
|
-
.attr('y',
|
|
1693
|
+
.attr('y', -24)
|
|
1478
1694
|
.attr('text-anchor', 'middle')
|
|
1479
1695
|
.attr('font-size', '10px')
|
|
1480
1696
|
.attr('fill', color)
|
|
1481
1697
|
.attr('opacity', 0.7)
|
|
1482
|
-
.
|
|
1698
|
+
.style('cursor', 'pointer')
|
|
1483
1699
|
.text(era.label);
|
|
1484
1700
|
|
|
1485
1701
|
eraG
|
|
1486
1702
|
.on('mouseenter', () => {
|
|
1703
|
+
// Fade everything
|
|
1704
|
+
g.selectAll<SVGGElement, unknown>('.gantt-task').attr('opacity', FADE_OPACITY);
|
|
1705
|
+
g.selectAll<SVGElement, unknown>('.gantt-milestone').attr('opacity', FADE_OPACITY);
|
|
1706
|
+
g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
|
|
1707
|
+
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
|
|
1708
|
+
svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', FADE_OPACITY);
|
|
1709
|
+
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
|
|
1710
|
+
g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
|
|
1711
|
+
g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
|
|
1712
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', FADE_OPACITY);
|
|
1713
|
+
// Highlight this era
|
|
1487
1714
|
eraRect.attr('opacity', hoverEraOpacity);
|
|
1488
1715
|
showGanttDateIndicators(g, xScale, eraStartDate, eraEndDate, innerHeight, color);
|
|
1489
1716
|
})
|
|
1490
1717
|
.on('mouseleave', () => {
|
|
1718
|
+
resetHighlight(g, svg);
|
|
1491
1719
|
eraRect.attr('opacity', baseEraOpacity);
|
|
1492
1720
|
hideGanttDateIndicators(g);
|
|
1493
1721
|
});
|
|
@@ -1504,6 +1732,7 @@ function renderErasAndMarkers(
|
|
|
1504
1732
|
|
|
1505
1733
|
const markerG = g.append('g')
|
|
1506
1734
|
.attr('class', 'gantt-marker-group')
|
|
1735
|
+
.attr('data-line-number', String(marker.lineNumber))
|
|
1507
1736
|
.style('cursor', 'pointer');
|
|
1508
1737
|
|
|
1509
1738
|
// Invisible hit rect for easier clicking/hovering
|
|
@@ -1544,20 +1773,31 @@ function renderErasAndMarkers(
|
|
|
1544
1773
|
.attr('stroke-dasharray', '6 4')
|
|
1545
1774
|
.attr('opacity', 0.5);
|
|
1546
1775
|
|
|
1547
|
-
// Hide marker
|
|
1776
|
+
// Hide marker line/diamond on hover but keep label visible
|
|
1548
1777
|
const markerLine = markerG.select('.gantt-marker');
|
|
1549
|
-
const markerLabel = markerG.select('.gantt-marker-label');
|
|
1550
1778
|
const markerDiamond = markerG.select('path');
|
|
1551
1779
|
markerG
|
|
1552
1780
|
.on('mouseenter', () => {
|
|
1553
|
-
|
|
1554
|
-
|
|
1781
|
+
// Fade everything
|
|
1782
|
+
g.selectAll<SVGGElement, unknown>('.gantt-task').attr('opacity', FADE_OPACITY);
|
|
1783
|
+
g.selectAll<SVGElement, unknown>('.gantt-milestone').attr('opacity', FADE_OPACITY);
|
|
1784
|
+
g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
|
|
1785
|
+
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
|
|
1786
|
+
svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', FADE_OPACITY);
|
|
1787
|
+
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
|
|
1788
|
+
g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
|
|
1789
|
+
g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
|
|
1790
|
+
g.selectAll<SVGElement, unknown>('.gantt-era-group').attr('opacity', FADE_OPACITY);
|
|
1791
|
+
// Fade other markers but keep this one highlighted
|
|
1792
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', FADE_OPACITY);
|
|
1793
|
+
markerG.attr('opacity', 1);
|
|
1794
|
+
markerLine.attr('opacity', 0.8);
|
|
1555
1795
|
markerDiamond.attr('opacity', 0);
|
|
1556
|
-
showGanttDateIndicators(g, xScale, markerDate, null, innerHeight, color);
|
|
1796
|
+
showGanttDateIndicators(g, xScale, markerDate, null, innerHeight, color, { skipStartLine: true });
|
|
1557
1797
|
})
|
|
1558
1798
|
.on('mouseleave', () => {
|
|
1799
|
+
resetHighlight(g, svg);
|
|
1559
1800
|
markerLine.attr('opacity', 0.5);
|
|
1560
|
-
markerLabel.attr('opacity', 1);
|
|
1561
1801
|
markerDiamond.attr('opacity', 0.9);
|
|
1562
1802
|
hideGanttDateIndicators(g);
|
|
1563
1803
|
});
|
|
@@ -1581,11 +1821,7 @@ function parseDateStringToDate(s: string): Date {
|
|
|
1581
1821
|
* Used for eras and markers which may have partial dates.
|
|
1582
1822
|
*/
|
|
1583
1823
|
function parseDateToFractionalYear(s: string): number {
|
|
1584
|
-
|
|
1585
|
-
const year = parts[0];
|
|
1586
|
-
const month = parts.length >= 2 ? parts[1] : 1;
|
|
1587
|
-
const day = parts.length >= 3 ? parts[2] : 1;
|
|
1588
|
-
return year + (month - 1) / 12 + (day - 1) / 365;
|
|
1824
|
+
return dateToFractionalYear(parseDateStringToDate(s));
|
|
1589
1825
|
}
|
|
1590
1826
|
|
|
1591
1827
|
// ── Dependency Hover Helpers ─────────────────────────────────
|
|
@@ -1641,6 +1877,8 @@ function highlightDeps(
|
|
|
1641
1877
|
const isRelated = (from && related.has(from)) || (to && related.has(to));
|
|
1642
1878
|
el.attr('opacity', isRelated ? 0.5 : FADE_OPACITY);
|
|
1643
1879
|
});
|
|
1880
|
+
// Fade markers
|
|
1881
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', FADE_OPACITY);
|
|
1644
1882
|
}
|
|
1645
1883
|
|
|
1646
1884
|
function highlightGroup(
|
|
@@ -1673,8 +1911,14 @@ function highlightGroup(
|
|
|
1673
1911
|
const el = d3Selection.select(this);
|
|
1674
1912
|
el.attr('opacity', el.attr('data-group') === groupName ? 1 : FADE_OPACITY);
|
|
1675
1913
|
});
|
|
1914
|
+
// Fade group bands not matching
|
|
1915
|
+
svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').each(function () {
|
|
1916
|
+
const el = d3Selection.select(this);
|
|
1917
|
+
el.attr('opacity', el.attr('data-group') === groupName ? 1 : FADE_OPACITY);
|
|
1918
|
+
});
|
|
1676
1919
|
// Fade lane elements
|
|
1677
1920
|
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
|
|
1921
|
+
svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', FADE_OPACITY);
|
|
1678
1922
|
g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr('opacity', FADE_OPACITY);
|
|
1679
1923
|
// Fade markers
|
|
1680
1924
|
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', FADE_OPACITY);
|
|
@@ -1714,9 +1958,15 @@ function highlightLane(
|
|
|
1714
1958
|
const el = d3Selection.select(this);
|
|
1715
1959
|
el.attr('opacity', el.attr('data-lane') === laneName ? 1 : FADE_OPACITY);
|
|
1716
1960
|
});
|
|
1961
|
+
// Fade lane bands not matching
|
|
1962
|
+
svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').each(function () {
|
|
1963
|
+
const el = d3Selection.select(this);
|
|
1964
|
+
el.attr('opacity', el.attr('data-lane') === laneName ? 1 : FADE_OPACITY);
|
|
1965
|
+
});
|
|
1717
1966
|
// Fade group elements (not relevant in lane mode)
|
|
1718
1967
|
g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
|
|
1719
1968
|
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
|
|
1969
|
+
svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', FADE_OPACITY);
|
|
1720
1970
|
// Fade markers
|
|
1721
1971
|
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', FADE_OPACITY);
|
|
1722
1972
|
}
|
|
@@ -1741,7 +1991,9 @@ function highlightTask(
|
|
|
1741
1991
|
// Fade group/lane elements
|
|
1742
1992
|
g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
|
|
1743
1993
|
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
|
|
1994
|
+
svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', FADE_OPACITY);
|
|
1744
1995
|
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
|
|
1996
|
+
svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', FADE_OPACITY);
|
|
1745
1997
|
g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
|
|
1746
1998
|
g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
|
|
1747
1999
|
// Fade markers
|
|
@@ -1768,7 +2020,9 @@ function highlightMilestone(
|
|
|
1768
2020
|
// Fade group/lane elements
|
|
1769
2021
|
g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
|
|
1770
2022
|
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
|
|
2023
|
+
svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', FADE_OPACITY);
|
|
1771
2024
|
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
|
|
2025
|
+
svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', FADE_OPACITY);
|
|
1772
2026
|
g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
|
|
1773
2027
|
g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
|
|
1774
2028
|
// Fade markers
|
|
@@ -1799,11 +2053,14 @@ function resetHighlight(
|
|
|
1799
2053
|
g.selectAll<SVGGElement, unknown>('.gantt-task, .gantt-milestone').attr('opacity', 1);
|
|
1800
2054
|
g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', 1);
|
|
1801
2055
|
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', 1);
|
|
2056
|
+
svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', 1);
|
|
1802
2057
|
svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', 1);
|
|
1803
2058
|
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', 1);
|
|
2059
|
+
svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', 1);
|
|
1804
2060
|
g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', 1);
|
|
1805
2061
|
g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', 0.5);
|
|
1806
2062
|
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', 1);
|
|
2063
|
+
g.selectAll<SVGElement, unknown>('.gantt-era-group').attr('opacity', 1);
|
|
1807
2064
|
}
|
|
1808
2065
|
|
|
1809
2066
|
// ── Row Building ────────────────────────────────────────────
|
|
@@ -2025,29 +2282,38 @@ function showGanttDateIndicators(
|
|
|
2025
2282
|
endDate: Date | null,
|
|
2026
2283
|
innerHeight: number,
|
|
2027
2284
|
color: string,
|
|
2285
|
+
options?: { skipStartLine?: boolean },
|
|
2028
2286
|
): void {
|
|
2029
2287
|
// Fade existing scale ticks and today marker
|
|
2030
2288
|
g.selectAll('.gantt-scale-tick').attr('opacity', 0.05);
|
|
2031
2289
|
g.selectAll('.gantt-today').attr('opacity', 0.05);
|
|
2032
2290
|
|
|
2291
|
+
// Wrap all hover indicators in a group that ignores pointer events,
|
|
2292
|
+
// so they don't steal mouseleave from the element being hovered.
|
|
2293
|
+
const hg = g.append('g')
|
|
2294
|
+
.attr('class', 'gantt-hover-date')
|
|
2295
|
+
.attr('pointer-events', 'none');
|
|
2296
|
+
|
|
2033
2297
|
const tickLen = 6;
|
|
2034
2298
|
const startPos = xScale(dateToFractionalYear(startDate));
|
|
2035
2299
|
const startLabel = formatGanttDate(startDate);
|
|
2036
2300
|
|
|
2037
|
-
// Start date — dashed vertical line
|
|
2038
|
-
|
|
2039
|
-
.
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2301
|
+
// Start date — dashed vertical line (skip when caller already shows its own line)
|
|
2302
|
+
if (!options?.skipStartLine) {
|
|
2303
|
+
hg.append('line')
|
|
2304
|
+
.attr('class', 'gantt-hover-date')
|
|
2305
|
+
.attr('x1', startPos)
|
|
2306
|
+
.attr('y1', -tickLen)
|
|
2307
|
+
.attr('x2', startPos)
|
|
2308
|
+
.attr('y2', innerHeight)
|
|
2309
|
+
.attr('stroke', color)
|
|
2310
|
+
.attr('stroke-width', 1.5)
|
|
2311
|
+
.attr('stroke-dasharray', '4 4')
|
|
2312
|
+
.attr('opacity', 0.6);
|
|
2313
|
+
}
|
|
2048
2314
|
|
|
2049
2315
|
// Start date — top label
|
|
2050
|
-
|
|
2316
|
+
hg.append('text')
|
|
2051
2317
|
.attr('class', 'gantt-hover-date')
|
|
2052
2318
|
.attr('x', startPos)
|
|
2053
2319
|
.attr('y', -tickLen - 4)
|
|
@@ -2058,7 +2324,7 @@ function showGanttDateIndicators(
|
|
|
2058
2324
|
.text(startLabel);
|
|
2059
2325
|
|
|
2060
2326
|
// Start date — bottom label
|
|
2061
|
-
|
|
2327
|
+
hg.append('text')
|
|
2062
2328
|
.attr('class', 'gantt-hover-date')
|
|
2063
2329
|
.attr('x', startPos)
|
|
2064
2330
|
.attr('y', innerHeight + tickLen + 12)
|
|
@@ -2089,7 +2355,7 @@ function showGanttDateIndicators(
|
|
|
2089
2355
|
}
|
|
2090
2356
|
|
|
2091
2357
|
// End date — dashed vertical line
|
|
2092
|
-
|
|
2358
|
+
hg.append('line')
|
|
2093
2359
|
.attr('class', 'gantt-hover-date')
|
|
2094
2360
|
.attr('x1', endPos)
|
|
2095
2361
|
.attr('y1', -tickLen)
|
|
@@ -2101,7 +2367,7 @@ function showGanttDateIndicators(
|
|
|
2101
2367
|
.attr('opacity', 0.6);
|
|
2102
2368
|
|
|
2103
2369
|
// Reposition start labels to avoid overlap
|
|
2104
|
-
|
|
2370
|
+
hg.selectAll<SVGTextElement, unknown>('text.gantt-hover-date').each(function () {
|
|
2105
2371
|
const el = d3Selection.select(this);
|
|
2106
2372
|
if (el.text() === startLabel) {
|
|
2107
2373
|
el.attr('x', startLabelX).attr('text-anchor', startAnchor);
|
|
@@ -2109,7 +2375,7 @@ function showGanttDateIndicators(
|
|
|
2109
2375
|
});
|
|
2110
2376
|
|
|
2111
2377
|
// End date — top label
|
|
2112
|
-
|
|
2378
|
+
hg.append('text')
|
|
2113
2379
|
.attr('class', 'gantt-hover-date')
|
|
2114
2380
|
.attr('x', endLabelX)
|
|
2115
2381
|
.attr('y', -tickLen - 4)
|
|
@@ -2120,7 +2386,7 @@ function showGanttDateIndicators(
|
|
|
2120
2386
|
.text(endLabel);
|
|
2121
2387
|
|
|
2122
2388
|
// End date — bottom label
|
|
2123
|
-
|
|
2389
|
+
hg.append('text')
|
|
2124
2390
|
.attr('class', 'gantt-hover-date')
|
|
2125
2391
|
.attr('x', endLabelX)
|
|
2126
2392
|
.attr('y', innerHeight + tickLen + 12)
|