@diagrammo/dgmo 0.8.3 → 0.8.5
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-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +452 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +188 -185
- package/dist/editor.cjs +338 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +307 -0
- package/dist/editor.js.map +1 -0
- package/dist/highlight.cjs +560 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.cts +32 -0
- package/dist/highlight.d.ts +32 -0
- package/dist/highlight.js +530 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +3467 -1078
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +3466 -1078
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +46 -37
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +9 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +71 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +100 -55
- package/src/chart.ts +91 -28
- package/src/class/parser.ts +41 -12
- package/src/cli.ts +211 -62
- package/src/completion.ts +378 -183
- package/src/d3.ts +1044 -303
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +69 -23
- package/src/echarts.ts +646 -153
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight-api.ts +444 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +222 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +48 -14
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +197 -71
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +46 -25
- package/src/graph/state-parser.ts +47 -17
- package/src/index.ts +96 -31
- package/src/infra/parser.ts +157 -53
- package/src/infra/renderer.ts +723 -271
- package/src/initiative-status/parser.ts +138 -44
- package/src/kanban/parser.ts +25 -14
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +69 -22
- package/src/palettes/index.ts +3 -2
- package/src/sequence/parser.ts +193 -61
- package/src/sitemap/parser.ts +65 -29
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- package/src/utils/parsing.ts +75 -31
package/src/gantt/renderer.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { getSeriesColors } from '../palettes';
|
|
|
9
9
|
import { mix } from '../palettes/color-utils';
|
|
10
10
|
import { resolveTagColor } from '../utils/tag-groups';
|
|
11
11
|
import { computeTimeTicks } from '../d3';
|
|
12
|
-
import { buildHolidaySet, formatDateKey } from '../utils/duration';
|
|
13
12
|
import {
|
|
14
13
|
LEGEND_HEIGHT,
|
|
15
14
|
LEGEND_PILL_PAD,
|
|
@@ -23,10 +22,19 @@ import {
|
|
|
23
22
|
LEGEND_ICON_W,
|
|
24
23
|
measureLegendText,
|
|
25
24
|
} from '../utils/legend-constants';
|
|
26
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
TITLE_FONT_SIZE,
|
|
27
|
+
TITLE_FONT_WEIGHT,
|
|
28
|
+
TITLE_Y,
|
|
29
|
+
} from '../utils/title-constants';
|
|
27
30
|
import type { PaletteColors } from '../palettes';
|
|
28
31
|
import type { D3ExportDimensions } from '../d3';
|
|
29
|
-
import type {
|
|
32
|
+
import type {
|
|
33
|
+
ResolvedSchedule,
|
|
34
|
+
ResolvedTask,
|
|
35
|
+
ResolvedGroup,
|
|
36
|
+
Weekday,
|
|
37
|
+
} from './types';
|
|
30
38
|
import type { TagGroup, TagEntry } from '../utils/tag-groups';
|
|
31
39
|
|
|
32
40
|
// ── Constants ───────────────────────────────────────────────
|
|
@@ -34,14 +42,13 @@ import type { TagGroup, TagEntry } from '../utils/tag-groups';
|
|
|
34
42
|
const BAR_H = 22;
|
|
35
43
|
const ROW_GAP = 6;
|
|
36
44
|
const GROUP_GAP = 14;
|
|
37
|
-
const GROUP_LABEL_GAP = 8;
|
|
38
45
|
const MILESTONE_SIZE = 10;
|
|
39
46
|
const MIN_LEFT_MARGIN = 120;
|
|
40
47
|
const BOTTOM_MARGIN = 40;
|
|
41
48
|
const RIGHT_MARGIN = 20;
|
|
42
|
-
const CHAR_W = 6.5;
|
|
43
|
-
const LABEL_PAD = 8;
|
|
44
|
-
const LABEL_GAP = 5;
|
|
49
|
+
const CHAR_W = 6.5; // estimated px per character for bar labels
|
|
50
|
+
const LABEL_PAD = 8; // inner padding to decide if label fits inside bar
|
|
51
|
+
const LABEL_GAP = 5; // gap between bar edge and external label
|
|
45
52
|
|
|
46
53
|
// ── Bar label placement ─────────────────────────────────────
|
|
47
54
|
|
|
@@ -57,7 +64,7 @@ function computeBarLabel(
|
|
|
57
64
|
x1: number,
|
|
58
65
|
barWidth: number,
|
|
59
66
|
innerWidth: number,
|
|
60
|
-
textColor: string
|
|
67
|
+
textColor: string
|
|
61
68
|
): BarLabelPlacement | null {
|
|
62
69
|
const textWidth = label.length * CHAR_W;
|
|
63
70
|
const x2 = x1 + barWidth;
|
|
@@ -81,7 +88,12 @@ function computeBarLabel(
|
|
|
81
88
|
const availWidth = x1 - LABEL_GAP;
|
|
82
89
|
if (availWidth > CHAR_W * 3) {
|
|
83
90
|
const maxChars = Math.floor(availWidth / CHAR_W) - 1;
|
|
84
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
x: x1 - LABEL_GAP,
|
|
93
|
+
anchor: 'end',
|
|
94
|
+
fill: textColor,
|
|
95
|
+
text: label.slice(0, maxChars) + '\u2026',
|
|
96
|
+
};
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
return null;
|
|
@@ -100,7 +112,7 @@ function renderLabelBand(
|
|
|
100
112
|
color: string,
|
|
101
113
|
palette: PaletteColors,
|
|
102
114
|
cssPrefix: 'group' | 'lane',
|
|
103
|
-
dataAttr?: { key: string; value: string }
|
|
115
|
+
dataAttr?: { key: string; value: string }
|
|
104
116
|
): void {
|
|
105
117
|
const bandX = 5;
|
|
106
118
|
const bandW = leftMargin - 7;
|
|
@@ -108,14 +120,19 @@ function renderLabelBand(
|
|
|
108
120
|
const clipId = `gantt-band-clip-${bandClipCounter++}`;
|
|
109
121
|
|
|
110
122
|
// ClipPath matching the tint band shape
|
|
111
|
-
svg
|
|
123
|
+
svg
|
|
124
|
+
.append('clipPath')
|
|
125
|
+
.attr('id', clipId)
|
|
112
126
|
.append('rect')
|
|
113
|
-
.attr('x', bandX)
|
|
114
|
-
.attr('
|
|
127
|
+
.attr('x', bandX)
|
|
128
|
+
.attr('y', bandY)
|
|
129
|
+
.attr('width', bandW)
|
|
130
|
+
.attr('height', BAR_H)
|
|
115
131
|
.attr('rx', BAND_RADIUS);
|
|
116
132
|
|
|
117
133
|
// Tint band
|
|
118
|
-
const tint = svg
|
|
134
|
+
const tint = svg
|
|
135
|
+
.append('rect')
|
|
119
136
|
.attr('class', `gantt-${cssPrefix}-band-bg`)
|
|
120
137
|
.attr('x', bandX)
|
|
121
138
|
.attr('y', bandY)
|
|
@@ -126,7 +143,8 @@ function renderLabelBand(
|
|
|
126
143
|
.style('pointer-events', 'none');
|
|
127
144
|
|
|
128
145
|
// Accent strip inside the tint, clipped to the band's rounded shape
|
|
129
|
-
const accent = svg
|
|
146
|
+
const accent = svg
|
|
147
|
+
.append('rect')
|
|
130
148
|
.attr('class', `gantt-${cssPrefix}-band-accent`)
|
|
131
149
|
.attr('x', bandX)
|
|
132
150
|
.attr('y', bandY)
|
|
@@ -147,11 +165,14 @@ function appendTaskIcon(
|
|
|
147
165
|
label: string,
|
|
148
166
|
isMilestone: boolean,
|
|
149
167
|
iconColor: string,
|
|
150
|
-
textColor: string
|
|
168
|
+
textColor: string
|
|
151
169
|
): void {
|
|
152
170
|
const icon = isMilestone ? '◆' : '●';
|
|
153
171
|
textEl.append('tspan').attr('fill', iconColor).text(icon);
|
|
154
|
-
textEl
|
|
172
|
+
textEl
|
|
173
|
+
.append('tspan')
|
|
174
|
+
.attr('fill', textColor)
|
|
175
|
+
.text(' ' + label);
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
// ── Interactive Options ─────────────────────────────────────
|
|
@@ -177,7 +198,7 @@ export function renderGantt(
|
|
|
177
198
|
palette: PaletteColors,
|
|
178
199
|
isDark: boolean,
|
|
179
200
|
options?: GanttInteractiveOptions,
|
|
180
|
-
exportDims?: D3ExportDimensions
|
|
201
|
+
exportDims?: D3ExportDimensions
|
|
181
202
|
): void {
|
|
182
203
|
// Clear previous content
|
|
183
204
|
container.innerHTML = '';
|
|
@@ -200,9 +221,12 @@ export function renderGantt(
|
|
|
200
221
|
// ── Compute layout dimensions ───────────────────────────
|
|
201
222
|
|
|
202
223
|
const seriesColors = getSeriesColors(palette);
|
|
203
|
-
let currentActiveGroup: string | null =
|
|
204
|
-
|
|
205
|
-
|
|
224
|
+
let currentActiveGroup: string | null =
|
|
225
|
+
options?.currentActiveGroup !== undefined
|
|
226
|
+
? options.currentActiveGroup
|
|
227
|
+
: resolved.tagGroups.length > 0
|
|
228
|
+
? resolved.tagGroups[0].name
|
|
229
|
+
: null;
|
|
206
230
|
let criticalPathActive = false;
|
|
207
231
|
|
|
208
232
|
// ── Build row list (structural vs tag mode) ─────────────
|
|
@@ -216,17 +240,21 @@ export function renderGantt(
|
|
|
216
240
|
// Compute left margin based on longest visible label (include ● /◆ prefix for tasks)
|
|
217
241
|
const allLabels = isTagMode
|
|
218
242
|
? [
|
|
219
|
-
...rows
|
|
220
|
-
|
|
243
|
+
...rows
|
|
244
|
+
.filter((r): r is LaneHeaderRow => r.type === 'lane-header')
|
|
245
|
+
.map((r) => r.laneName),
|
|
246
|
+
...rows
|
|
247
|
+
.filter((r): r is TaskRow => r.type === 'task')
|
|
248
|
+
.map((r) => '● ' + r.task.task.label),
|
|
221
249
|
]
|
|
222
250
|
: [
|
|
223
|
-
...resolved.tasks.map(t => '● ' + t.task.label),
|
|
224
|
-
...resolved.groups.map(g => {
|
|
251
|
+
...resolved.tasks.map((t) => '● ' + t.task.label),
|
|
252
|
+
...resolved.groups.map((g) => {
|
|
225
253
|
const px = g.depth <= 2 ? g.depth * 14 : 2 * 14 + (g.depth - 2) * 8;
|
|
226
254
|
return ' '.repeat(Math.ceil(px / 7)) + g.name;
|
|
227
255
|
}),
|
|
228
256
|
];
|
|
229
|
-
const maxLabelLen = Math.max(...allLabels.map(l => l.length), 10);
|
|
257
|
+
const maxLabelLen = Math.max(...allLabels.map((l) => l.length), 10);
|
|
230
258
|
const leftMargin = Math.max(MIN_LEFT_MARGIN, maxLabelLen * 7 + 30);
|
|
231
259
|
|
|
232
260
|
const totalRows = rows.length;
|
|
@@ -234,13 +262,16 @@ export function renderGantt(
|
|
|
234
262
|
// Vertical layout — matches timeline pattern (d3.ts:3649-3655)
|
|
235
263
|
const title = resolved.options.title;
|
|
236
264
|
const titleHeight = title ? 50 : 20;
|
|
237
|
-
const tagLegendReserve =
|
|
265
|
+
const tagLegendReserve =
|
|
266
|
+
resolved.tagGroups.length > 0 ? LEGEND_HEIGHT + 8 : 0;
|
|
238
267
|
const topDateLabelReserve = 22; // tick (6) + gap (4) + label height (~12)
|
|
239
|
-
const hasOverheadLabels =
|
|
268
|
+
const hasOverheadLabels =
|
|
269
|
+
resolved.markers.length > 0 || resolved.eras.length > 0;
|
|
240
270
|
const markerLabelReserve = hasOverheadLabels ? 18 : 0; // markers/eras extend above date labels
|
|
241
271
|
const CONTENT_TOP_PAD = 16; // breathing room between scale labels and first row
|
|
242
272
|
|
|
243
|
-
const marginTop =
|
|
273
|
+
const marginTop =
|
|
274
|
+
titleHeight + tagLegendReserve + topDateLabelReserve + markerLabelReserve;
|
|
244
275
|
|
|
245
276
|
// Content area
|
|
246
277
|
const contentH = isTagMode
|
|
@@ -283,19 +314,33 @@ export function renderGantt(
|
|
|
283
314
|
|
|
284
315
|
// ── Tag legend (interactive) ────────────────────────────
|
|
285
316
|
|
|
286
|
-
const hasCriticalPath =
|
|
317
|
+
const hasCriticalPath =
|
|
318
|
+
resolved.options.criticalPath &&
|
|
319
|
+
resolved.tasks.some((t) => t.isCriticalPath);
|
|
287
320
|
|
|
288
321
|
function drawLegend() {
|
|
289
322
|
svg.selectAll('.gantt-tag-legend-container').remove();
|
|
290
323
|
if (resolved.tagGroups.length > 0 || hasCriticalPath) {
|
|
291
324
|
const legendY = titleHeight;
|
|
292
325
|
renderTagLegend(
|
|
293
|
-
svg,
|
|
294
|
-
|
|
326
|
+
svg,
|
|
327
|
+
g,
|
|
328
|
+
resolved.tagGroups,
|
|
329
|
+
currentActiveGroup,
|
|
330
|
+
leftMargin,
|
|
331
|
+
innerWidth,
|
|
332
|
+
legendY,
|
|
333
|
+
palette,
|
|
334
|
+
isDark,
|
|
335
|
+
hasCriticalPath,
|
|
336
|
+
criticalPathActive,
|
|
337
|
+
resolved.options.optionLineNumbers,
|
|
295
338
|
(groupName) => {
|
|
296
339
|
// Toggle active group
|
|
297
|
-
currentActiveGroup =
|
|
298
|
-
|
|
340
|
+
currentActiveGroup =
|
|
341
|
+
currentActiveGroup?.toLowerCase() === groupName.toLowerCase()
|
|
342
|
+
? null
|
|
343
|
+
: groupName;
|
|
299
344
|
if (onActiveGroupChange) onActiveGroupChange(currentActiveGroup);
|
|
300
345
|
drawLegend();
|
|
301
346
|
recolorBars();
|
|
@@ -307,7 +352,7 @@ export function renderGantt(
|
|
|
307
352
|
currentSwimlaneGroup,
|
|
308
353
|
onSwimlaneChange,
|
|
309
354
|
viewMode,
|
|
310
|
-
resolved.tasks
|
|
355
|
+
resolved.tasks
|
|
311
356
|
);
|
|
312
357
|
}
|
|
313
358
|
}
|
|
@@ -316,9 +361,15 @@ export function renderGantt(
|
|
|
316
361
|
g.selectAll<SVGGElement, unknown>('.gantt-task').each(function () {
|
|
317
362
|
const el = d3Selection.select(this);
|
|
318
363
|
const taskId = el.attr('data-task-id');
|
|
319
|
-
const rt = resolved.tasks.find(t => t.task.id === taskId);
|
|
364
|
+
const rt = resolved.tasks.find((t) => t.task.id === taskId);
|
|
320
365
|
if (!rt) return;
|
|
321
|
-
const color = resolveTaskColor(
|
|
366
|
+
const color = resolveTaskColor(
|
|
367
|
+
rt,
|
|
368
|
+
currentActiveGroup,
|
|
369
|
+
resolved,
|
|
370
|
+
seriesColors,
|
|
371
|
+
palette
|
|
372
|
+
);
|
|
322
373
|
const fillColor = mix(color, palette.bg, 30);
|
|
323
374
|
el.select('rect').attr('fill', fillColor).attr('stroke', color);
|
|
324
375
|
});
|
|
@@ -349,7 +400,18 @@ export function renderGantt(
|
|
|
349
400
|
// ── Weekend + holiday bands ─────────────────────────────
|
|
350
401
|
|
|
351
402
|
renderWeekendBands(g, resolved, xScale, innerHeight, palette, isDark);
|
|
352
|
-
renderHolidayBands(
|
|
403
|
+
renderHolidayBands(
|
|
404
|
+
g,
|
|
405
|
+
svg,
|
|
406
|
+
resolved,
|
|
407
|
+
xScale,
|
|
408
|
+
innerHeight,
|
|
409
|
+
palette,
|
|
410
|
+
isDark,
|
|
411
|
+
marginTop - 4,
|
|
412
|
+
leftMargin,
|
|
413
|
+
onClickItem
|
|
414
|
+
);
|
|
353
415
|
renderErasAndMarkers(g, svg, resolved, xScale, innerHeight, palette);
|
|
354
416
|
|
|
355
417
|
// ── Today marker (line rendered before rows so it paints behind task bars) ──
|
|
@@ -366,7 +428,8 @@ export function renderGantt(
|
|
|
366
428
|
}
|
|
367
429
|
todayX = xScale(dateToFractionalYear(todayDate));
|
|
368
430
|
if (todayX >= 0 && todayX <= innerWidth) {
|
|
369
|
-
const todayLine = g
|
|
431
|
+
const todayLine = g
|
|
432
|
+
.append('line')
|
|
370
433
|
.attr('class', 'gantt-today')
|
|
371
434
|
.attr('x1', todayX)
|
|
372
435
|
.attr('y1', 0)
|
|
@@ -377,9 +440,11 @@ export function renderGantt(
|
|
|
377
440
|
.attr('stroke-dasharray', '6 4')
|
|
378
441
|
.attr('opacity', 0.7)
|
|
379
442
|
.attr('pointer-events', 'none');
|
|
380
|
-
if (todayMarkerLineNum)
|
|
443
|
+
if (todayMarkerLineNum)
|
|
444
|
+
todayLine.attr('data-line-number', String(todayMarkerLineNum));
|
|
381
445
|
|
|
382
|
-
const todayLabel = g
|
|
446
|
+
const todayLabel = g
|
|
447
|
+
.append('text')
|
|
383
448
|
.attr('class', 'gantt-today')
|
|
384
449
|
.attr('x', todayX)
|
|
385
450
|
.attr('y', innerHeight + 24)
|
|
@@ -389,23 +454,33 @@ export function renderGantt(
|
|
|
389
454
|
.attr('opacity', 0.7)
|
|
390
455
|
.attr('pointer-events', 'none')
|
|
391
456
|
.text('Today');
|
|
392
|
-
if (todayMarkerLineNum)
|
|
457
|
+
if (todayMarkerLineNum)
|
|
458
|
+
todayLabel.attr('data-line-number', String(todayMarkerLineNum));
|
|
393
459
|
}
|
|
394
460
|
}
|
|
395
461
|
|
|
396
462
|
// ── Render rows ─────────────────────────────────────────
|
|
397
463
|
|
|
398
464
|
// Track task positions for dependency arrows
|
|
399
|
-
const taskPositions = new Map<
|
|
465
|
+
const taskPositions = new Map<
|
|
466
|
+
string,
|
|
467
|
+
{ x1: number; x2: number; y: number }
|
|
468
|
+
>();
|
|
400
469
|
// Track collapsed group bar positions so hidden-task arrows redirect there
|
|
401
|
-
const groupPositions = new Map<
|
|
470
|
+
const groupPositions = new Map<
|
|
471
|
+
string,
|
|
472
|
+
{ x1: number; x2: number; y: number }
|
|
473
|
+
>();
|
|
402
474
|
// Track lane header positions for collapsed lane arrow redirection (tag mode)
|
|
403
|
-
const lanePositions = new Map<
|
|
475
|
+
const lanePositions = new Map<
|
|
476
|
+
string,
|
|
477
|
+
{ x1: number; x2: number; y: number }
|
|
478
|
+
>();
|
|
404
479
|
// Map task ID → lane name for collapsed lane lookup (tag mode)
|
|
405
480
|
const taskLaneMap = new Map<string, string>();
|
|
406
481
|
if (isTagMode && currentSwimlaneGroup) {
|
|
407
482
|
const tagGroup = resolved.tagGroups.find(
|
|
408
|
-
tg => tg.name.toLowerCase() === currentSwimlaneGroup.toLowerCase()
|
|
483
|
+
(tg) => tg.name.toLowerCase() === currentSwimlaneGroup.toLowerCase()
|
|
409
484
|
);
|
|
410
485
|
if (tagGroup) {
|
|
411
486
|
const tagKey = tagGroup.name.toLowerCase();
|
|
@@ -413,7 +488,9 @@ export function renderGantt(
|
|
|
413
488
|
let value = rt.effectiveMetadata[tagKey];
|
|
414
489
|
if (!value && tagGroup.defaultValue) value = tagGroup.defaultValue;
|
|
415
490
|
if (value) {
|
|
416
|
-
const entry = tagGroup.entries.find(
|
|
491
|
+
const entry = tagGroup.entries.find(
|
|
492
|
+
(e) => e.value.toLowerCase() === value!.toLowerCase()
|
|
493
|
+
);
|
|
417
494
|
if (entry) taskLaneMap.set(rt.task.id, entry.value);
|
|
418
495
|
}
|
|
419
496
|
}
|
|
@@ -424,13 +501,14 @@ export function renderGantt(
|
|
|
424
501
|
for (const row of rows) {
|
|
425
502
|
if (row.type === 'lane-header') {
|
|
426
503
|
// ── Lane header (tag swimlane mode) ──
|
|
427
|
-
const laneColor =
|
|
504
|
+
const laneColor =
|
|
505
|
+
row.laneColor === '#999999' ? palette.textMuted : row.laneColor;
|
|
428
506
|
const toggleIcon = row.isCollapsed ? '►' : '▼';
|
|
429
507
|
const labelX = 10;
|
|
430
508
|
|
|
431
509
|
// Compute lane bar x range from task dates
|
|
432
510
|
let lx1 = 0;
|
|
433
|
-
let lx2 = innerWidth;
|
|
511
|
+
let lx2 = innerWidth; // eslint-disable-line no-useless-assignment
|
|
434
512
|
let laneBarWidth = innerWidth;
|
|
435
513
|
if (row.laneStartDate && row.laneEndDate) {
|
|
436
514
|
lx1 = xScale(dateToFractionalYear(row.laneStartDate));
|
|
@@ -438,9 +516,21 @@ export function renderGantt(
|
|
|
438
516
|
laneBarWidth = Math.max(lx2 - lx1, 2);
|
|
439
517
|
}
|
|
440
518
|
|
|
441
|
-
lanePositions.set(row.laneName, {
|
|
519
|
+
lanePositions.set(row.laneName, {
|
|
520
|
+
x1: lx1,
|
|
521
|
+
x2: lx1 + laneBarWidth,
|
|
522
|
+
y: yOffset + BAR_H / 2,
|
|
523
|
+
});
|
|
442
524
|
|
|
443
|
-
renderLabelBand(
|
|
525
|
+
renderLabelBand(
|
|
526
|
+
svg,
|
|
527
|
+
marginTop + yOffset + BAR_H / 2,
|
|
528
|
+
leftMargin,
|
|
529
|
+
laneColor,
|
|
530
|
+
palette,
|
|
531
|
+
'lane',
|
|
532
|
+
{ key: 'data-lane', value: row.laneName }
|
|
533
|
+
);
|
|
444
534
|
const labelG = svg
|
|
445
535
|
.append('g')
|
|
446
536
|
.attr('class', 'gantt-lane-header')
|
|
@@ -453,7 +543,14 @@ export function renderGantt(
|
|
|
453
543
|
.on('mouseenter', () => {
|
|
454
544
|
highlightLane(g, svg, row.tagKey, row.laneName);
|
|
455
545
|
if (row.laneStartDate && row.laneEndDate) {
|
|
456
|
-
showGanttDateIndicators(
|
|
546
|
+
showGanttDateIndicators(
|
|
547
|
+
g,
|
|
548
|
+
xScale,
|
|
549
|
+
row.laneStartDate,
|
|
550
|
+
row.laneEndDate,
|
|
551
|
+
innerHeight,
|
|
552
|
+
laneColor
|
|
553
|
+
);
|
|
457
554
|
}
|
|
458
555
|
})
|
|
459
556
|
.on('mouseleave', () => {
|
|
@@ -471,17 +568,32 @@ export function renderGantt(
|
|
|
471
568
|
.attr('font-size', '11px')
|
|
472
569
|
.attr('font-weight', 'bold')
|
|
473
570
|
.attr('fill', laneColor)
|
|
474
|
-
.text(
|
|
571
|
+
.text(
|
|
572
|
+
toggleIcon +
|
|
573
|
+
' ' +
|
|
574
|
+
row.laneName +
|
|
575
|
+
(row.aggregateProgress !== null
|
|
576
|
+
? ` ${Math.round(row.aggregateProgress)}%`
|
|
577
|
+
: '')
|
|
578
|
+
);
|
|
475
579
|
|
|
476
580
|
if (laneBarWidth > 0) {
|
|
477
581
|
const barFill = mix(laneColor, palette.bg, 30);
|
|
478
|
-
const laneBandG = g
|
|
582
|
+
const laneBandG = g
|
|
583
|
+
.append('g')
|
|
479
584
|
.attr('class', 'gantt-lane-band-group')
|
|
480
585
|
.attr('data-lane', row.laneName)
|
|
481
586
|
.on('mouseenter', () => {
|
|
482
587
|
highlightLane(g, svg, row.tagKey, row.laneName);
|
|
483
588
|
if (row.laneStartDate && row.laneEndDate) {
|
|
484
|
-
showGanttDateIndicators(
|
|
589
|
+
showGanttDateIndicators(
|
|
590
|
+
g,
|
|
591
|
+
xScale,
|
|
592
|
+
row.laneStartDate,
|
|
593
|
+
row.laneEndDate,
|
|
594
|
+
innerHeight,
|
|
595
|
+
laneColor
|
|
596
|
+
);
|
|
485
597
|
}
|
|
486
598
|
})
|
|
487
599
|
.on('mouseleave', () => {
|
|
@@ -489,7 +601,8 @@ export function renderGantt(
|
|
|
489
601
|
hideGanttDateIndicators(g);
|
|
490
602
|
});
|
|
491
603
|
|
|
492
|
-
laneBandG
|
|
604
|
+
laneBandG
|
|
605
|
+
.append('rect')
|
|
493
606
|
.attr('class', 'gantt-lane-band')
|
|
494
607
|
.attr('x', lx1)
|
|
495
608
|
.attr('y', yOffset)
|
|
@@ -502,11 +615,15 @@ export function renderGantt(
|
|
|
502
615
|
|
|
503
616
|
// Aggregate progress fill
|
|
504
617
|
if (row.aggregateProgress !== null && row.aggregateProgress > 0) {
|
|
505
|
-
laneBandG
|
|
618
|
+
laneBandG
|
|
619
|
+
.append('rect')
|
|
506
620
|
.attr('class', 'gantt-lane-progress')
|
|
507
621
|
.attr('x', lx1)
|
|
508
622
|
.attr('y', yOffset)
|
|
509
|
-
.attr(
|
|
623
|
+
.attr(
|
|
624
|
+
'width',
|
|
625
|
+
laneBarWidth * Math.min(row.aggregateProgress / 100, 1)
|
|
626
|
+
)
|
|
510
627
|
.attr('height', BAR_H)
|
|
511
628
|
.attr('fill', laneColor)
|
|
512
629
|
.attr('opacity', 0.5)
|
|
@@ -518,13 +635,28 @@ export function renderGantt(
|
|
|
518
635
|
} else if (row.type === 'group') {
|
|
519
636
|
const group = row.group;
|
|
520
637
|
const isCollapsed = collapsedGroups?.has(group.name) ?? false;
|
|
521
|
-
const indent = ' '.repeat(group.depth);
|
|
522
638
|
const toggleIcon = isCollapsed ? '►' : '▼';
|
|
523
639
|
|
|
524
640
|
// Group label with toggle — resolve tag color from group metadata
|
|
525
|
-
const tagColor = resolveTagColor(
|
|
526
|
-
|
|
527
|
-
|
|
641
|
+
const tagColor = resolveTagColor(
|
|
642
|
+
group.metadata,
|
|
643
|
+
resolved.tagGroups,
|
|
644
|
+
currentActiveGroup,
|
|
645
|
+
true
|
|
646
|
+
);
|
|
647
|
+
const groupColor =
|
|
648
|
+
tagColor && tagColor !== '#999999'
|
|
649
|
+
? tagColor
|
|
650
|
+
: group.color || palette.textMuted;
|
|
651
|
+
renderLabelBand(
|
|
652
|
+
svg,
|
|
653
|
+
marginTop + yOffset + BAR_H / 2,
|
|
654
|
+
leftMargin,
|
|
655
|
+
groupColor,
|
|
656
|
+
palette,
|
|
657
|
+
'group',
|
|
658
|
+
{ key: 'data-group', value: group.name }
|
|
659
|
+
);
|
|
528
660
|
const labelG = svg
|
|
529
661
|
.append('g')
|
|
530
662
|
.attr('class', 'gantt-group-label')
|
|
@@ -536,14 +668,22 @@ export function renderGantt(
|
|
|
536
668
|
})
|
|
537
669
|
.on('mouseenter', () => {
|
|
538
670
|
highlightGroup(g, svg, group.name);
|
|
539
|
-
showGanttDateIndicators(
|
|
671
|
+
showGanttDateIndicators(
|
|
672
|
+
g,
|
|
673
|
+
xScale,
|
|
674
|
+
group.startDate,
|
|
675
|
+
group.endDate,
|
|
676
|
+
innerHeight,
|
|
677
|
+
groupColor
|
|
678
|
+
);
|
|
540
679
|
})
|
|
541
680
|
.on('mouseleave', () => {
|
|
542
681
|
resetHighlight(g, svg);
|
|
543
682
|
hideGanttDateIndicators(g);
|
|
544
683
|
});
|
|
545
684
|
|
|
546
|
-
const groupIndent =
|
|
685
|
+
const groupIndent =
|
|
686
|
+
group.depth <= 2 ? group.depth * 14 : 2 * 14 + (group.depth - 2) * 8;
|
|
547
687
|
const labelX = 10 + groupIndent;
|
|
548
688
|
labelG
|
|
549
689
|
.append('text')
|
|
@@ -554,7 +694,12 @@ export function renderGantt(
|
|
|
554
694
|
.attr('font-size', '11px')
|
|
555
695
|
.attr('font-weight', 'bold')
|
|
556
696
|
.attr('fill', palette.text)
|
|
557
|
-
.text(
|
|
697
|
+
.text(
|
|
698
|
+
toggleIcon +
|
|
699
|
+
' ' +
|
|
700
|
+
group.name +
|
|
701
|
+
(group.progress !== null ? ` ${Math.round(group.progress)}%` : '')
|
|
702
|
+
);
|
|
558
703
|
|
|
559
704
|
// Group bar
|
|
560
705
|
const gStart = dateToFractionalYear(group.startDate);
|
|
@@ -566,20 +711,29 @@ export function renderGantt(
|
|
|
566
711
|
if (isCollapsed) {
|
|
567
712
|
// Summary bar (full height, shows aggregate progress)
|
|
568
713
|
const barWidth = Math.max(gx2 - gx1, 2);
|
|
569
|
-
const summaryG = g
|
|
714
|
+
const summaryG = g
|
|
715
|
+
.append('g')
|
|
570
716
|
.attr('class', 'gantt-group-summary')
|
|
571
717
|
.attr('data-group', group.name)
|
|
572
718
|
.attr('data-line-number', String(group.lineNumber))
|
|
573
719
|
.on('mouseenter', () => {
|
|
574
720
|
highlightGroup(g, svg, group.name);
|
|
575
|
-
showGanttDateIndicators(
|
|
721
|
+
showGanttDateIndicators(
|
|
722
|
+
g,
|
|
723
|
+
xScale,
|
|
724
|
+
group.startDate,
|
|
725
|
+
group.endDate,
|
|
726
|
+
innerHeight,
|
|
727
|
+
groupColor
|
|
728
|
+
);
|
|
576
729
|
})
|
|
577
730
|
.on('mouseleave', () => {
|
|
578
731
|
resetHighlight(g, svg);
|
|
579
732
|
hideGanttDateIndicators(g);
|
|
580
733
|
});
|
|
581
734
|
|
|
582
|
-
summaryG
|
|
735
|
+
summaryG
|
|
736
|
+
.append('rect')
|
|
583
737
|
.attr('x', gx1)
|
|
584
738
|
.attr('y', yOffset)
|
|
585
739
|
.attr('width', barWidth)
|
|
@@ -591,7 +745,8 @@ export function renderGantt(
|
|
|
591
745
|
|
|
592
746
|
// Aggregate progress fill
|
|
593
747
|
if (group.progress !== null && group.progress > 0) {
|
|
594
|
-
summaryG
|
|
748
|
+
summaryG
|
|
749
|
+
.append('rect')
|
|
595
750
|
.attr('x', gx1)
|
|
596
751
|
.attr('y', yOffset)
|
|
597
752
|
.attr('width', barWidth * Math.min(group.progress / 100, 1))
|
|
@@ -601,8 +756,16 @@ export function renderGantt(
|
|
|
601
756
|
}
|
|
602
757
|
|
|
603
758
|
// Bar label (inside → after → before → truncate)
|
|
604
|
-
const summaryLabel =
|
|
605
|
-
|
|
759
|
+
const summaryLabel =
|
|
760
|
+
group.name +
|
|
761
|
+
(group.progress !== null ? ` ${Math.round(group.progress)}%` : '');
|
|
762
|
+
const summaryPlacement = computeBarLabel(
|
|
763
|
+
summaryLabel,
|
|
764
|
+
gx1,
|
|
765
|
+
barWidth,
|
|
766
|
+
innerWidth,
|
|
767
|
+
palette.text
|
|
768
|
+
);
|
|
606
769
|
if (summaryPlacement) {
|
|
607
770
|
summaryG
|
|
608
771
|
.append('text')
|
|
@@ -618,25 +781,38 @@ export function renderGantt(
|
|
|
618
781
|
}
|
|
619
782
|
|
|
620
783
|
// Track collapsed group position for dependency arrow redirection
|
|
621
|
-
groupPositions.set(group.name, {
|
|
784
|
+
groupPositions.set(group.name, {
|
|
785
|
+
x1: gx1,
|
|
786
|
+
x2: gx1 + barWidth,
|
|
787
|
+
y: yOffset + BAR_H / 2,
|
|
788
|
+
});
|
|
622
789
|
} else {
|
|
623
790
|
// Expanded: bar spanning group date range (matches task bar style)
|
|
624
791
|
const groupBarWidth = Math.max(gx2 - gx1, 2);
|
|
625
792
|
const bandFill = mix(groupColor, palette.bg, 30);
|
|
626
|
-
const groupBarG = g
|
|
793
|
+
const groupBarG = g
|
|
794
|
+
.append('g')
|
|
627
795
|
.attr('class', 'gantt-group-bar')
|
|
628
796
|
.attr('data-group', group.name)
|
|
629
797
|
.attr('data-line-number', String(group.lineNumber))
|
|
630
798
|
.on('mouseenter', () => {
|
|
631
799
|
highlightGroup(g, svg, group.name);
|
|
632
|
-
showGanttDateIndicators(
|
|
800
|
+
showGanttDateIndicators(
|
|
801
|
+
g,
|
|
802
|
+
xScale,
|
|
803
|
+
group.startDate,
|
|
804
|
+
group.endDate,
|
|
805
|
+
innerHeight,
|
|
806
|
+
groupColor
|
|
807
|
+
);
|
|
633
808
|
})
|
|
634
809
|
.on('mouseleave', () => {
|
|
635
810
|
resetHighlight(g, svg);
|
|
636
811
|
hideGanttDateIndicators(g);
|
|
637
812
|
});
|
|
638
813
|
|
|
639
|
-
groupBarG
|
|
814
|
+
groupBarG
|
|
815
|
+
.append('rect')
|
|
640
816
|
.attr('x', gx1)
|
|
641
817
|
.attr('y', yOffset)
|
|
642
818
|
.attr('width', groupBarWidth)
|
|
@@ -648,7 +824,8 @@ export function renderGantt(
|
|
|
648
824
|
|
|
649
825
|
// Aggregate progress fill
|
|
650
826
|
if (group.progress !== null && group.progress > 0) {
|
|
651
|
-
groupBarG
|
|
827
|
+
groupBarG
|
|
828
|
+
.append('rect')
|
|
652
829
|
.attr('class', 'gantt-group-progress')
|
|
653
830
|
.attr('x', gx1)
|
|
654
831
|
.attr('y', yOffset)
|
|
@@ -659,8 +836,16 @@ export function renderGantt(
|
|
|
659
836
|
}
|
|
660
837
|
|
|
661
838
|
// Bar label (inside → after → before → truncate)
|
|
662
|
-
const expandedLabel =
|
|
663
|
-
|
|
839
|
+
const expandedLabel =
|
|
840
|
+
group.name +
|
|
841
|
+
(group.progress !== null ? ` ${Math.round(group.progress)}%` : '');
|
|
842
|
+
const expandedPlacement = computeBarLabel(
|
|
843
|
+
expandedLabel,
|
|
844
|
+
gx1,
|
|
845
|
+
groupBarWidth,
|
|
846
|
+
innerWidth,
|
|
847
|
+
palette.text
|
|
848
|
+
);
|
|
664
849
|
if (expandedPlacement) {
|
|
665
850
|
groupBarG
|
|
666
851
|
.append('text')
|
|
@@ -683,7 +868,13 @@ export function renderGantt(
|
|
|
683
868
|
const task = rt.task;
|
|
684
869
|
|
|
685
870
|
// Resolve bar color early so icon tspan can use it
|
|
686
|
-
const barColor = resolveTaskColor(
|
|
871
|
+
const barColor = resolveTaskColor(
|
|
872
|
+
rt,
|
|
873
|
+
currentActiveGroup,
|
|
874
|
+
resolved,
|
|
875
|
+
seriesColors,
|
|
876
|
+
palette
|
|
877
|
+
);
|
|
687
878
|
|
|
688
879
|
// Task label on the left (left-aligned with indent; flat in tag mode)
|
|
689
880
|
const depth = rt.groupPath.length;
|
|
@@ -717,7 +908,13 @@ export function renderGantt(
|
|
|
717
908
|
resetHighlight(g, svg);
|
|
718
909
|
});
|
|
719
910
|
|
|
720
|
-
appendTaskIcon(
|
|
911
|
+
appendTaskIcon(
|
|
912
|
+
taskLabel,
|
|
913
|
+
task.label,
|
|
914
|
+
rt.isMilestone,
|
|
915
|
+
barColor,
|
|
916
|
+
palette.text
|
|
917
|
+
);
|
|
721
918
|
|
|
722
919
|
// Tag attributes on label for legend hover matching
|
|
723
920
|
for (const [key, value] of Object.entries(rt.effectiveMetadata)) {
|
|
@@ -747,7 +944,14 @@ export function renderGantt(
|
|
|
747
944
|
})
|
|
748
945
|
.on('mouseenter', () => {
|
|
749
946
|
highlightMilestone(g, svg, task.id);
|
|
750
|
-
showGanttDateIndicators(
|
|
947
|
+
showGanttDateIndicators(
|
|
948
|
+
g,
|
|
949
|
+
xScale,
|
|
950
|
+
rt.startDate,
|
|
951
|
+
null,
|
|
952
|
+
innerHeight,
|
|
953
|
+
barColor
|
|
954
|
+
);
|
|
751
955
|
// Show label next to diamond
|
|
752
956
|
g.append('text')
|
|
753
957
|
.attr('class', 'gantt-milestone-hover-label')
|
|
@@ -778,7 +982,8 @@ export function renderGantt(
|
|
|
778
982
|
|
|
779
983
|
const fillColor = mix(barColor, palette.bg, 30);
|
|
780
984
|
|
|
781
|
-
const taskG = g
|
|
985
|
+
const taskG = g
|
|
986
|
+
.append('g')
|
|
782
987
|
.attr('class', 'gantt-task')
|
|
783
988
|
.attr('data-line-number', String(task.lineNumber))
|
|
784
989
|
.attr('data-task-name', task.label)
|
|
@@ -793,7 +998,14 @@ export function renderGantt(
|
|
|
793
998
|
highlightDeps(g, svg, task.id, resolved);
|
|
794
999
|
}
|
|
795
1000
|
highlightTaskLabel(svg, task.lineNumber);
|
|
796
|
-
showGanttDateIndicators(
|
|
1001
|
+
showGanttDateIndicators(
|
|
1002
|
+
g,
|
|
1003
|
+
xScale,
|
|
1004
|
+
rt.startDate,
|
|
1005
|
+
rt.endDate,
|
|
1006
|
+
innerHeight,
|
|
1007
|
+
barColor
|
|
1008
|
+
);
|
|
797
1009
|
})
|
|
798
1010
|
.on('mouseleave', () => {
|
|
799
1011
|
if (resolved.options.dependencies) {
|
|
@@ -813,7 +1025,8 @@ export function renderGantt(
|
|
|
813
1025
|
}
|
|
814
1026
|
|
|
815
1027
|
// Uncertainty gradient — fade out the trailing edge unless progress > 80%
|
|
816
|
-
const showUncertainFade =
|
|
1028
|
+
const showUncertainFade =
|
|
1029
|
+
rt.isUncertain && (task.progress === null || task.progress <= 80);
|
|
817
1030
|
let barFill: string = fillColor;
|
|
818
1031
|
let barStroke: string = barColor;
|
|
819
1032
|
if (showUncertainFade) {
|
|
@@ -822,20 +1035,52 @@ export function renderGantt(
|
|
|
822
1035
|
: svg.select<SVGDefsElement>('defs');
|
|
823
1036
|
|
|
824
1037
|
const fillGradId = `gantt-uncertain-fill-${task.id}`;
|
|
825
|
-
const fillGrad = defs
|
|
1038
|
+
const fillGrad = defs
|
|
1039
|
+
.append('linearGradient')
|
|
826
1040
|
.attr('id', fillGradId)
|
|
827
|
-
.attr('x1', '0')
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1041
|
+
.attr('x1', '0')
|
|
1042
|
+
.attr('x2', '1')
|
|
1043
|
+
.attr('y1', '0')
|
|
1044
|
+
.attr('y2', '0');
|
|
1045
|
+
fillGrad
|
|
1046
|
+
.append('stop')
|
|
1047
|
+
.attr('offset', '0%')
|
|
1048
|
+
.attr('stop-color', fillColor)
|
|
1049
|
+
.attr('stop-opacity', 1);
|
|
1050
|
+
fillGrad
|
|
1051
|
+
.append('stop')
|
|
1052
|
+
.attr('offset', '50%')
|
|
1053
|
+
.attr('stop-color', fillColor)
|
|
1054
|
+
.attr('stop-opacity', 1);
|
|
1055
|
+
fillGrad
|
|
1056
|
+
.append('stop')
|
|
1057
|
+
.attr('offset', '100%')
|
|
1058
|
+
.attr('stop-color', fillColor)
|
|
1059
|
+
.attr('stop-opacity', 0);
|
|
831
1060
|
|
|
832
1061
|
const strokeGradId = `gantt-uncertain-stroke-${task.id}`;
|
|
833
|
-
const strokeGrad = defs
|
|
1062
|
+
const strokeGrad = defs
|
|
1063
|
+
.append('linearGradient')
|
|
834
1064
|
.attr('id', strokeGradId)
|
|
835
|
-
.attr('x1', '0')
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1065
|
+
.attr('x1', '0')
|
|
1066
|
+
.attr('x2', '1')
|
|
1067
|
+
.attr('y1', '0')
|
|
1068
|
+
.attr('y2', '0');
|
|
1069
|
+
strokeGrad
|
|
1070
|
+
.append('stop')
|
|
1071
|
+
.attr('offset', '0%')
|
|
1072
|
+
.attr('stop-color', barColor)
|
|
1073
|
+
.attr('stop-opacity', 1);
|
|
1074
|
+
strokeGrad
|
|
1075
|
+
.append('stop')
|
|
1076
|
+
.attr('offset', '50%')
|
|
1077
|
+
.attr('stop-color', barColor)
|
|
1078
|
+
.attr('stop-opacity', 1);
|
|
1079
|
+
strokeGrad
|
|
1080
|
+
.append('stop')
|
|
1081
|
+
.attr('offset', '100%')
|
|
1082
|
+
.attr('stop-color', barColor)
|
|
1083
|
+
.attr('stop-opacity', 0);
|
|
839
1084
|
|
|
840
1085
|
barFill = `url(#${fillGradId})`;
|
|
841
1086
|
barStroke = `url(#${strokeGradId})`;
|
|
@@ -863,12 +1108,28 @@ export function renderGantt(
|
|
|
863
1108
|
const fadeStart = Math.min(50 * ratio, 100);
|
|
864
1109
|
const defs = svg.select<SVGDefsElement>('defs');
|
|
865
1110
|
const progGradId = `gantt-uncertain-progress-${task.id}`;
|
|
866
|
-
const progGrad = defs
|
|
1111
|
+
const progGrad = defs
|
|
1112
|
+
.append('linearGradient')
|
|
867
1113
|
.attr('id', progGradId)
|
|
868
|
-
.attr('x1', '0')
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1114
|
+
.attr('x1', '0')
|
|
1115
|
+
.attr('x2', '1')
|
|
1116
|
+
.attr('y1', '0')
|
|
1117
|
+
.attr('y2', '0');
|
|
1118
|
+
progGrad
|
|
1119
|
+
.append('stop')
|
|
1120
|
+
.attr('offset', '0%')
|
|
1121
|
+
.attr('stop-color', barColor)
|
|
1122
|
+
.attr('stop-opacity', 1);
|
|
1123
|
+
progGrad
|
|
1124
|
+
.append('stop')
|
|
1125
|
+
.attr('offset', `${fadeStart}%`)
|
|
1126
|
+
.attr('stop-color', barColor)
|
|
1127
|
+
.attr('stop-opacity', 1);
|
|
1128
|
+
progGrad
|
|
1129
|
+
.append('stop')
|
|
1130
|
+
.attr('offset', '100%')
|
|
1131
|
+
.attr('stop-color', barColor)
|
|
1132
|
+
.attr('stop-opacity', 0);
|
|
872
1133
|
progressFill = `url(#${progGradId})`;
|
|
873
1134
|
}
|
|
874
1135
|
taskG
|
|
@@ -887,9 +1148,14 @@ export function renderGantt(
|
|
|
887
1148
|
taskG.attr('data-critical-path', 'true');
|
|
888
1149
|
}
|
|
889
1150
|
|
|
890
|
-
|
|
891
1151
|
// Bar label (inside → after → before → truncate)
|
|
892
|
-
const labelPlacement = computeBarLabel(
|
|
1152
|
+
const labelPlacement = computeBarLabel(
|
|
1153
|
+
task.label,
|
|
1154
|
+
x1,
|
|
1155
|
+
barWidth,
|
|
1156
|
+
innerWidth,
|
|
1157
|
+
palette.text
|
|
1158
|
+
);
|
|
893
1159
|
if (labelPlacement) {
|
|
894
1160
|
taskG
|
|
895
1161
|
.append('text')
|
|
@@ -904,7 +1170,11 @@ export function renderGantt(
|
|
|
904
1170
|
}
|
|
905
1171
|
|
|
906
1172
|
// Track bar position for arrows
|
|
907
|
-
taskPositions.set(task.id, {
|
|
1173
|
+
taskPositions.set(task.id, {
|
|
1174
|
+
x1,
|
|
1175
|
+
x2: x1 + barWidth,
|
|
1176
|
+
y: yOffset + BAR_H / 2,
|
|
1177
|
+
});
|
|
908
1178
|
}
|
|
909
1179
|
|
|
910
1180
|
yOffset += BAR_H + ROW_GAP;
|
|
@@ -914,12 +1184,14 @@ export function renderGantt(
|
|
|
914
1184
|
// ── Today hover overlay (rendered after rows so it receives pointer events) ──
|
|
915
1185
|
|
|
916
1186
|
if (todayDate && todayX >= 0 && todayX <= innerWidth) {
|
|
917
|
-
const todayHoverG = g
|
|
1187
|
+
const todayHoverG = g
|
|
1188
|
+
.append('g')
|
|
918
1189
|
.attr('class', 'gantt-today-hover')
|
|
919
1190
|
.style('cursor', 'pointer');
|
|
920
1191
|
|
|
921
1192
|
// Invisible wide hit rect for easy hovering
|
|
922
|
-
todayHoverG
|
|
1193
|
+
todayHoverG
|
|
1194
|
+
.append('rect')
|
|
923
1195
|
.attr('x', todayX - 10)
|
|
924
1196
|
.attr('y', -6)
|
|
925
1197
|
.attr('width', 20)
|
|
@@ -931,17 +1203,48 @@ export function renderGantt(
|
|
|
931
1203
|
todayHoverG
|
|
932
1204
|
.on('mouseenter', () => {
|
|
933
1205
|
// Fade everything
|
|
934
|
-
g.selectAll<SVGGElement, unknown>('.gantt-task').attr(
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
g.selectAll<SVGElement, unknown>(
|
|
943
|
-
|
|
944
|
-
|
|
1206
|
+
g.selectAll<SVGGElement, unknown>('.gantt-task').attr(
|
|
1207
|
+
'opacity',
|
|
1208
|
+
FADE_OPACITY
|
|
1209
|
+
);
|
|
1210
|
+
g.selectAll<SVGElement, unknown>('.gantt-milestone').attr(
|
|
1211
|
+
'opacity',
|
|
1212
|
+
FADE_OPACITY
|
|
1213
|
+
);
|
|
1214
|
+
g.selectAll<SVGElement, unknown>(
|
|
1215
|
+
'.gantt-group-bar, .gantt-group-summary'
|
|
1216
|
+
).attr('opacity', FADE_OPACITY);
|
|
1217
|
+
svg
|
|
1218
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
1219
|
+
.attr('opacity', FADE_OPACITY);
|
|
1220
|
+
svg
|
|
1221
|
+
.selectAll<SVGTextElement, unknown>('.gantt-task-label')
|
|
1222
|
+
.attr('opacity', FADE_OPACITY);
|
|
1223
|
+
svg
|
|
1224
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
1225
|
+
.attr('opacity', FADE_OPACITY);
|
|
1226
|
+
g.selectAll<SVGElement, unknown>(
|
|
1227
|
+
'.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group'
|
|
1228
|
+
).attr('opacity', FADE_OPACITY);
|
|
1229
|
+
g.selectAll<SVGElement, unknown>(
|
|
1230
|
+
'.gantt-dep-arrow, .gantt-dep-arrowhead'
|
|
1231
|
+
).attr('opacity', FADE_OPACITY);
|
|
1232
|
+
g.selectAll<SVGElement, unknown>('.gantt-era-group').attr(
|
|
1233
|
+
'opacity',
|
|
1234
|
+
FADE_OPACITY
|
|
1235
|
+
);
|
|
1236
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
1237
|
+
'opacity',
|
|
1238
|
+
FADE_OPACITY
|
|
1239
|
+
);
|
|
1240
|
+
showGanttDateIndicators(
|
|
1241
|
+
g,
|
|
1242
|
+
xScale,
|
|
1243
|
+
todayDateObj,
|
|
1244
|
+
null,
|
|
1245
|
+
innerHeight,
|
|
1246
|
+
todayColor
|
|
1247
|
+
);
|
|
945
1248
|
})
|
|
946
1249
|
.on('mouseleave', () => {
|
|
947
1250
|
resetHighlight(g, svg);
|
|
@@ -952,13 +1255,33 @@ export function renderGantt(
|
|
|
952
1255
|
// ── Dependency arrows ───────────────────────────────────
|
|
953
1256
|
|
|
954
1257
|
if (resolved.options.dependencies) {
|
|
955
|
-
renderDependencyArrows(
|
|
1258
|
+
renderDependencyArrows(
|
|
1259
|
+
g,
|
|
1260
|
+
resolved,
|
|
1261
|
+
taskPositions,
|
|
1262
|
+
groupPositions,
|
|
1263
|
+
collapsedGroups,
|
|
1264
|
+
palette,
|
|
1265
|
+
isDark,
|
|
1266
|
+
isTagMode,
|
|
1267
|
+
lanePositions,
|
|
1268
|
+
collapsedLanes,
|
|
1269
|
+
taskLaneMap
|
|
1270
|
+
);
|
|
956
1271
|
}
|
|
957
1272
|
}
|
|
958
1273
|
|
|
959
1274
|
// ── Weekend Band Rendering ──────────────────────────────────
|
|
960
1275
|
|
|
961
|
-
const JS_DAY_TO_WEEKDAY: Weekday[] = [
|
|
1276
|
+
const JS_DAY_TO_WEEKDAY: Weekday[] = [
|
|
1277
|
+
'sun',
|
|
1278
|
+
'mon',
|
|
1279
|
+
'tue',
|
|
1280
|
+
'wed',
|
|
1281
|
+
'thu',
|
|
1282
|
+
'fri',
|
|
1283
|
+
'sat',
|
|
1284
|
+
];
|
|
962
1285
|
|
|
963
1286
|
function renderWeekendBands(
|
|
964
1287
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
@@ -966,7 +1289,7 @@ function renderWeekendBands(
|
|
|
966
1289
|
xScale: d3Scale.ScaleLinear<number, number>,
|
|
967
1290
|
innerHeight: number,
|
|
968
1291
|
palette: PaletteColors,
|
|
969
|
-
isDark: boolean
|
|
1292
|
+
isDark: boolean
|
|
970
1293
|
): void {
|
|
971
1294
|
const workweek = new Set(resolved.holidays.workweek);
|
|
972
1295
|
const start = new Date(resolved.startDate);
|
|
@@ -985,14 +1308,34 @@ function renderWeekendBands(
|
|
|
985
1308
|
bandStart = new Date(current);
|
|
986
1309
|
} else if (!isWeekend && bandStart) {
|
|
987
1310
|
// Draw band from bandStart to current
|
|
988
|
-
drawBand(
|
|
1311
|
+
drawBand(
|
|
1312
|
+
g,
|
|
1313
|
+
xScale,
|
|
1314
|
+
bandStart,
|
|
1315
|
+
current,
|
|
1316
|
+
innerHeight,
|
|
1317
|
+
palette,
|
|
1318
|
+
isDark,
|
|
1319
|
+
'gantt-weekend-band',
|
|
1320
|
+
0.04
|
|
1321
|
+
);
|
|
989
1322
|
bandStart = null;
|
|
990
1323
|
}
|
|
991
1324
|
current.setDate(current.getDate() + 1);
|
|
992
1325
|
}
|
|
993
1326
|
// Close any trailing band
|
|
994
1327
|
if (bandStart) {
|
|
995
|
-
drawBand(
|
|
1328
|
+
drawBand(
|
|
1329
|
+
g,
|
|
1330
|
+
xScale,
|
|
1331
|
+
bandStart,
|
|
1332
|
+
current,
|
|
1333
|
+
innerHeight,
|
|
1334
|
+
palette,
|
|
1335
|
+
isDark,
|
|
1336
|
+
'gantt-weekend-band',
|
|
1337
|
+
0.04
|
|
1338
|
+
);
|
|
996
1339
|
}
|
|
997
1340
|
}
|
|
998
1341
|
|
|
@@ -1008,20 +1351,48 @@ function renderHolidayBands(
|
|
|
1008
1351
|
isDark: boolean,
|
|
1009
1352
|
headerY: number,
|
|
1010
1353
|
chartLeftMargin: number,
|
|
1011
|
-
onClickItem?: (lineNumber: number) => void
|
|
1354
|
+
onClickItem?: (lineNumber: number) => void
|
|
1012
1355
|
): void {
|
|
1013
1356
|
for (const h of resolved.holidays.dates) {
|
|
1014
1357
|
const start = new Date(h.date + 'T00:00:00');
|
|
1015
1358
|
const end = new Date(start);
|
|
1016
1359
|
end.setDate(end.getDate() + 1);
|
|
1017
|
-
drawHolidayBand(
|
|
1360
|
+
drawHolidayBand(
|
|
1361
|
+
g,
|
|
1362
|
+
svg,
|
|
1363
|
+
xScale,
|
|
1364
|
+
start,
|
|
1365
|
+
end,
|
|
1366
|
+
innerHeight,
|
|
1367
|
+
palette,
|
|
1368
|
+
isDark,
|
|
1369
|
+
h.label,
|
|
1370
|
+
h.lineNumber,
|
|
1371
|
+
headerY,
|
|
1372
|
+
chartLeftMargin,
|
|
1373
|
+
onClickItem
|
|
1374
|
+
);
|
|
1018
1375
|
}
|
|
1019
1376
|
|
|
1020
1377
|
for (const r of resolved.holidays.ranges) {
|
|
1021
1378
|
const start = new Date(r.startDate + 'T00:00:00');
|
|
1022
1379
|
const end = new Date(r.endDate + 'T00:00:00');
|
|
1023
1380
|
end.setDate(end.getDate() + 1);
|
|
1024
|
-
drawHolidayBand(
|
|
1381
|
+
drawHolidayBand(
|
|
1382
|
+
g,
|
|
1383
|
+
svg,
|
|
1384
|
+
xScale,
|
|
1385
|
+
start,
|
|
1386
|
+
end,
|
|
1387
|
+
innerHeight,
|
|
1388
|
+
palette,
|
|
1389
|
+
isDark,
|
|
1390
|
+
r.label,
|
|
1391
|
+
r.lineNumber,
|
|
1392
|
+
headerY,
|
|
1393
|
+
chartLeftMargin,
|
|
1394
|
+
onClickItem
|
|
1395
|
+
);
|
|
1025
1396
|
}
|
|
1026
1397
|
}
|
|
1027
1398
|
|
|
@@ -1034,7 +1405,7 @@ function drawBand(
|
|
|
1034
1405
|
palette: PaletteColors,
|
|
1035
1406
|
_isDark: boolean,
|
|
1036
1407
|
className: string,
|
|
1037
|
-
opacity: number
|
|
1408
|
+
opacity: number
|
|
1038
1409
|
): void {
|
|
1039
1410
|
const x1 = xScale(dateToFractionalYear(start));
|
|
1040
1411
|
const x2 = xScale(dateToFractionalYear(end));
|
|
@@ -1064,7 +1435,7 @@ function drawHolidayBand(
|
|
|
1064
1435
|
lineNumber: number,
|
|
1065
1436
|
headerY: number,
|
|
1066
1437
|
chartLeftMargin: number,
|
|
1067
|
-
onClickItem?: (lineNumber: number) => void
|
|
1438
|
+
onClickItem?: (lineNumber: number) => void
|
|
1068
1439
|
): void {
|
|
1069
1440
|
const x1 = xScale(dateToFractionalYear(start));
|
|
1070
1441
|
const x2 = xScale(dateToFractionalYear(end));
|
|
@@ -1074,13 +1445,15 @@ function drawHolidayBand(
|
|
|
1074
1445
|
const baseOpacity = 0.08;
|
|
1075
1446
|
const hoverOpacity = 0.18;
|
|
1076
1447
|
|
|
1077
|
-
const bandG = g
|
|
1448
|
+
const bandG = g
|
|
1449
|
+
.append('g')
|
|
1078
1450
|
.attr('class', 'gantt-holiday-band')
|
|
1079
1451
|
.attr('data-line-number', String(lineNumber))
|
|
1080
1452
|
.style('cursor', onClickItem ? 'pointer' : 'default');
|
|
1081
1453
|
|
|
1082
1454
|
// Band rect
|
|
1083
|
-
const bandRect = bandG
|
|
1455
|
+
const bandRect = bandG
|
|
1456
|
+
.append('rect')
|
|
1084
1457
|
.attr('x', x1)
|
|
1085
1458
|
.attr('y', 0)
|
|
1086
1459
|
.attr('width', bandW)
|
|
@@ -1092,7 +1465,8 @@ function drawHolidayBand(
|
|
|
1092
1465
|
// Background rect to mask date labels underneath
|
|
1093
1466
|
const labelX = chartLeftMargin + x1 + bandW / 2;
|
|
1094
1467
|
const textLen = label.length * 6 + 8;
|
|
1095
|
-
const labelBg = svg
|
|
1468
|
+
const labelBg = svg
|
|
1469
|
+
.append('rect')
|
|
1096
1470
|
.attr('class', 'gantt-holiday-hover-bg')
|
|
1097
1471
|
.attr('data-line-number', String(lineNumber))
|
|
1098
1472
|
.attr('x', labelX - textLen / 2)
|
|
@@ -1104,7 +1478,8 @@ function drawHolidayBand(
|
|
|
1104
1478
|
.attr('opacity', 0)
|
|
1105
1479
|
.attr('pointer-events', 'none');
|
|
1106
1480
|
|
|
1107
|
-
const labelText = svg
|
|
1481
|
+
const labelText = svg
|
|
1482
|
+
.append('text')
|
|
1108
1483
|
.attr('class', 'gantt-holiday-hover-label')
|
|
1109
1484
|
.attr('data-line-number', String(lineNumber))
|
|
1110
1485
|
.attr('x', labelX)
|
|
@@ -1141,7 +1516,7 @@ function drawHolidayBand(
|
|
|
1141
1516
|
function findCollapsedGroupPos(
|
|
1142
1517
|
rt: ResolvedTask,
|
|
1143
1518
|
collapsedGroups: Set<string> | undefined,
|
|
1144
|
-
groupPositions: Map<string, { x1: number; x2: number; y: number }
|
|
1519
|
+
groupPositions: Map<string, { x1: number; x2: number; y: number }>
|
|
1145
1520
|
): { x1: number; x2: number; y: number } | undefined {
|
|
1146
1521
|
if (!collapsedGroups) return undefined;
|
|
1147
1522
|
// Walk the task's group path and find the first collapsed group with a position
|
|
@@ -1157,7 +1532,7 @@ function findCollapsedLanePos(
|
|
|
1157
1532
|
rt: ResolvedTask,
|
|
1158
1533
|
collapsedLanes: Set<string> | undefined,
|
|
1159
1534
|
taskLaneMap: Map<string, string>,
|
|
1160
|
-
lanePositions: Map<string, { x1: number; x2: number; y: number }
|
|
1535
|
+
lanePositions: Map<string, { x1: number; x2: number; y: number }>
|
|
1161
1536
|
): { x1: number; x2: number; y: number } | undefined {
|
|
1162
1537
|
if (!collapsedLanes) return undefined;
|
|
1163
1538
|
const laneName = taskLaneMap.get(rt.task.id);
|
|
@@ -1178,28 +1553,38 @@ function renderDependencyArrows(
|
|
|
1178
1553
|
isTagMode: boolean,
|
|
1179
1554
|
lanePositions: Map<string, { x1: number; x2: number; y: number }>,
|
|
1180
1555
|
collapsedLanes: Set<string> | undefined,
|
|
1181
|
-
taskLaneMap: Map<string, string
|
|
1556
|
+
taskLaneMap: Map<string, string>
|
|
1182
1557
|
): void {
|
|
1183
1558
|
// Deduplicate arrows that collapse to the same source→target position
|
|
1184
1559
|
const drawnArrows = new Set<string>();
|
|
1185
1560
|
|
|
1186
1561
|
// Build arrow list from task dependencies
|
|
1187
1562
|
for (const rt of resolved.tasks) {
|
|
1188
|
-
const sourcePos =
|
|
1189
|
-
??
|
|
1563
|
+
const sourcePos =
|
|
1564
|
+
taskPositions.get(rt.task.id) ??
|
|
1565
|
+
(isTagMode
|
|
1190
1566
|
? findCollapsedLanePos(rt, collapsedLanes, taskLaneMap, lanePositions)
|
|
1191
1567
|
: findCollapsedGroupPos(rt, collapsedGroups, groupPositions));
|
|
1192
1568
|
if (!sourcePos) continue;
|
|
1193
1569
|
|
|
1194
1570
|
for (const dep of rt.task.dependencies) {
|
|
1195
1571
|
// Find target task
|
|
1196
|
-
const targetTask = resolved.tasks.find(
|
|
1197
|
-
|
|
1572
|
+
const targetTask = resolved.tasks.find(
|
|
1573
|
+
(t) =>
|
|
1574
|
+
t.task.label === dep.targetName ||
|
|
1575
|
+
`${t.groupPath.join('.')}.${t.task.label}`.endsWith(dep.targetName)
|
|
1576
|
+
);
|
|
1198
1577
|
if (!targetTask) continue;
|
|
1199
1578
|
|
|
1200
|
-
const targetPos =
|
|
1201
|
-
??
|
|
1202
|
-
|
|
1579
|
+
const targetPos =
|
|
1580
|
+
taskPositions.get(targetTask.task.id) ??
|
|
1581
|
+
(isTagMode
|
|
1582
|
+
? findCollapsedLanePos(
|
|
1583
|
+
targetTask,
|
|
1584
|
+
collapsedLanes,
|
|
1585
|
+
taskLaneMap,
|
|
1586
|
+
lanePositions
|
|
1587
|
+
)
|
|
1203
1588
|
: findCollapsedGroupPos(targetTask, collapsedGroups, groupPositions));
|
|
1204
1589
|
if (!targetPos) continue;
|
|
1205
1590
|
|
|
@@ -1257,7 +1642,8 @@ function renderDependencyArrows(
|
|
|
1257
1642
|
const midX = (sx + tx) / 2;
|
|
1258
1643
|
const midY = (sy + ty) / 2;
|
|
1259
1644
|
// Background rect for readability
|
|
1260
|
-
const labelEl = g
|
|
1645
|
+
const labelEl = g
|
|
1646
|
+
.append('text')
|
|
1261
1647
|
.attr('class', 'gantt-dep-label')
|
|
1262
1648
|
.attr('data-dep-from', rt.task.id)
|
|
1263
1649
|
.attr('data-dep-to', targetTask.task.id)
|
|
@@ -1287,7 +1673,12 @@ function renderDependencyArrows(
|
|
|
1287
1673
|
}
|
|
1288
1674
|
}
|
|
1289
1675
|
|
|
1290
|
-
function arrowheadPoints(
|
|
1676
|
+
function arrowheadPoints(
|
|
1677
|
+
x: number,
|
|
1678
|
+
y: number,
|
|
1679
|
+
size: number,
|
|
1680
|
+
angle: number
|
|
1681
|
+
): string {
|
|
1291
1682
|
const a1 = angle + Math.PI * 0.8;
|
|
1292
1683
|
const a2 = angle - Math.PI * 0.8;
|
|
1293
1684
|
return `${x},${y} ${x + size * Math.cos(a1)},${y + size * Math.sin(a1)} ${x + size * Math.cos(a2)},${y + size * Math.sin(a2)}`;
|
|
@@ -1297,43 +1688,94 @@ function arrowheadPoints(x: number, y: number, size: number, angle: number): str
|
|
|
1297
1688
|
|
|
1298
1689
|
function applyCriticalPathHighlight(
|
|
1299
1690
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1300
|
-
chartG: d3Selection.Selection<SVGGElement, unknown, null, undefined
|
|
1691
|
+
chartG: d3Selection.Selection<SVGGElement, unknown, null, undefined>
|
|
1301
1692
|
) {
|
|
1302
1693
|
chartG.selectAll<SVGGElement, unknown>('.gantt-task').each(function () {
|
|
1303
1694
|
const el = d3Selection.select(this);
|
|
1304
|
-
el.attr(
|
|
1695
|
+
el.attr(
|
|
1696
|
+
'opacity',
|
|
1697
|
+
el.attr('data-critical-path') === 'true' ? 1 : FADE_OPACITY
|
|
1698
|
+
);
|
|
1305
1699
|
});
|
|
1306
|
-
chartG
|
|
1307
|
-
|
|
1700
|
+
chartG
|
|
1701
|
+
.selectAll<SVGElement, unknown>('.gantt-milestone')
|
|
1702
|
+
.attr('opacity', FADE_OPACITY);
|
|
1703
|
+
chartG
|
|
1704
|
+
.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary')
|
|
1705
|
+
.attr('opacity', FADE_OPACITY);
|
|
1308
1706
|
svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').each(function () {
|
|
1309
1707
|
const el = d3Selection.select(this);
|
|
1310
|
-
el.attr(
|
|
1708
|
+
el.attr(
|
|
1709
|
+
'opacity',
|
|
1710
|
+
el.attr('data-critical-path') === 'true' ? 1 : FADE_OPACITY
|
|
1711
|
+
);
|
|
1311
1712
|
});
|
|
1312
|
-
svg
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
svg
|
|
1316
|
-
|
|
1713
|
+
svg
|
|
1714
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
1715
|
+
.attr('opacity', FADE_OPACITY);
|
|
1716
|
+
svg
|
|
1717
|
+
.selectAll<
|
|
1718
|
+
SVGElement,
|
|
1719
|
+
unknown
|
|
1720
|
+
>('.gantt-group-band-bg, .gantt-group-band-accent')
|
|
1721
|
+
.attr('opacity', FADE_OPACITY);
|
|
1722
|
+
svg
|
|
1723
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
1724
|
+
.attr('opacity', FADE_OPACITY);
|
|
1725
|
+
svg
|
|
1726
|
+
.selectAll<
|
|
1727
|
+
SVGElement,
|
|
1728
|
+
unknown
|
|
1729
|
+
>('.gantt-lane-band-bg, .gantt-lane-band-accent')
|
|
1730
|
+
.attr('opacity', FADE_OPACITY);
|
|
1731
|
+
chartG
|
|
1732
|
+
.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent')
|
|
1733
|
+
.attr('opacity', FADE_OPACITY);
|
|
1317
1734
|
// Show critical path arrows at full opacity, fade others
|
|
1318
|
-
chartG
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1735
|
+
chartG
|
|
1736
|
+
.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead')
|
|
1737
|
+
.each(function () {
|
|
1738
|
+
const el = d3Selection.select(this);
|
|
1739
|
+
el.attr(
|
|
1740
|
+
'opacity',
|
|
1741
|
+
el.attr('data-critical-path') === 'true' ? 0.7 : FADE_OPACITY
|
|
1742
|
+
);
|
|
1743
|
+
});
|
|
1322
1744
|
}
|
|
1323
1745
|
|
|
1324
1746
|
function resetHighlightAll(
|
|
1325
1747
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1326
|
-
chartG: d3Selection.Selection<SVGGElement, unknown, null, undefined
|
|
1748
|
+
chartG: d3Selection.Selection<SVGGElement, unknown, null, undefined>
|
|
1327
1749
|
) {
|
|
1328
|
-
chartG
|
|
1329
|
-
|
|
1330
|
-
|
|
1750
|
+
chartG
|
|
1751
|
+
.selectAll<SVGGElement, unknown>('.gantt-task, .gantt-milestone')
|
|
1752
|
+
.attr('opacity', 1);
|
|
1753
|
+
chartG
|
|
1754
|
+
.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary')
|
|
1755
|
+
.attr('opacity', 1);
|
|
1756
|
+
svg
|
|
1757
|
+
.selectAll<SVGTextElement, unknown>('.gantt-task-label')
|
|
1758
|
+
.attr('opacity', 1);
|
|
1331
1759
|
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', 1);
|
|
1332
|
-
svg
|
|
1760
|
+
svg
|
|
1761
|
+
.selectAll<
|
|
1762
|
+
SVGElement,
|
|
1763
|
+
unknown
|
|
1764
|
+
>('.gantt-group-band-bg, .gantt-group-band-accent')
|
|
1765
|
+
.attr('opacity', 1);
|
|
1333
1766
|
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', 1);
|
|
1334
|
-
svg
|
|
1335
|
-
|
|
1336
|
-
|
|
1767
|
+
svg
|
|
1768
|
+
.selectAll<
|
|
1769
|
+
SVGElement,
|
|
1770
|
+
unknown
|
|
1771
|
+
>('.gantt-lane-band-bg, .gantt-lane-band-accent')
|
|
1772
|
+
.attr('opacity', 1);
|
|
1773
|
+
chartG
|
|
1774
|
+
.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent')
|
|
1775
|
+
.attr('opacity', 1);
|
|
1776
|
+
chartG
|
|
1777
|
+
.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead')
|
|
1778
|
+
.attr('opacity', 0.5);
|
|
1337
1779
|
}
|
|
1338
1780
|
|
|
1339
1781
|
// ── Swimlane Icon Helper ─────────────────────────────────────
|
|
@@ -1343,9 +1785,10 @@ function drawSwimlaneIcon(
|
|
|
1343
1785
|
x: number,
|
|
1344
1786
|
y: number,
|
|
1345
1787
|
isActive: boolean,
|
|
1346
|
-
palette: PaletteColors
|
|
1788
|
+
palette: PaletteColors
|
|
1347
1789
|
): d3Selection.Selection<SVGGElement, unknown, null, undefined> {
|
|
1348
|
-
const iconG = parent
|
|
1790
|
+
const iconG = parent
|
|
1791
|
+
.append('g')
|
|
1349
1792
|
.attr('class', 'gantt-swimlane-icon')
|
|
1350
1793
|
.attr('transform', `translate(${x}, ${y})`);
|
|
1351
1794
|
|
|
@@ -1356,7 +1799,8 @@ function drawSwimlaneIcon(
|
|
|
1356
1799
|
const gap = 3;
|
|
1357
1800
|
|
|
1358
1801
|
for (let i = 0; i < barWidths.length; i++) {
|
|
1359
|
-
iconG
|
|
1802
|
+
iconG
|
|
1803
|
+
.append('rect')
|
|
1360
1804
|
.attr('x', 0)
|
|
1361
1805
|
.attr('y', i * gap)
|
|
1362
1806
|
.attr('width', barWidths[i])
|
|
@@ -1387,7 +1831,7 @@ function renderTagLegend(
|
|
|
1387
1831
|
currentSwimlaneGroup?: string | null,
|
|
1388
1832
|
onSwimlaneChange?: (group: string | null) => void,
|
|
1389
1833
|
legendViewMode?: boolean,
|
|
1390
|
-
resolvedTasks?: ResolvedTask[]
|
|
1834
|
+
resolvedTasks?: ResolvedTask[]
|
|
1391
1835
|
): void {
|
|
1392
1836
|
const groupBg = isDark
|
|
1393
1837
|
? mix(palette.surface, palette.bg, 50)
|
|
@@ -1396,10 +1840,16 @@ function renderTagLegend(
|
|
|
1396
1840
|
// Build visible groups: active group expanded + swimlane group as compact pill
|
|
1397
1841
|
let visibleGroups: TagGroup[];
|
|
1398
1842
|
if (activeGroupName) {
|
|
1399
|
-
const activeGroup = tagGroups.filter(
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1843
|
+
const activeGroup = tagGroups.filter(
|
|
1844
|
+
(g) => g.name.toLowerCase() === activeGroupName.toLowerCase()
|
|
1845
|
+
);
|
|
1846
|
+
const swimlaneGroup =
|
|
1847
|
+
currentSwimlaneGroup &&
|
|
1848
|
+
currentSwimlaneGroup.toLowerCase() !== activeGroupName.toLowerCase()
|
|
1849
|
+
? tagGroups.filter(
|
|
1850
|
+
(g) => g.name.toLowerCase() === currentSwimlaneGroup.toLowerCase()
|
|
1851
|
+
)
|
|
1852
|
+
: [];
|
|
1403
1853
|
visibleGroups = [...swimlaneGroup, ...activeGroup];
|
|
1404
1854
|
} else {
|
|
1405
1855
|
visibleGroups = tagGroups;
|
|
@@ -1425,7 +1875,10 @@ function renderTagLegend(
|
|
|
1425
1875
|
const key = group.name.toLowerCase();
|
|
1426
1876
|
const used = usedValues.get(key);
|
|
1427
1877
|
if (used && used.size > 0) {
|
|
1428
|
-
filteredEntries.set(
|
|
1878
|
+
filteredEntries.set(
|
|
1879
|
+
key,
|
|
1880
|
+
group.entries.filter((e) => used.has(e.value.toLowerCase()))
|
|
1881
|
+
);
|
|
1429
1882
|
} else {
|
|
1430
1883
|
filteredEntries.set(key, group.entries);
|
|
1431
1884
|
}
|
|
@@ -1435,17 +1888,27 @@ function renderTagLegend(
|
|
|
1435
1888
|
const groupWidths: number[] = [];
|
|
1436
1889
|
let totalW = 0;
|
|
1437
1890
|
for (const group of visibleGroups) {
|
|
1438
|
-
const isActive =
|
|
1439
|
-
|
|
1891
|
+
const isActive =
|
|
1892
|
+
activeGroupName?.toLowerCase() === group.name.toLowerCase();
|
|
1893
|
+
const isSwimlane =
|
|
1894
|
+
currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
|
|
1440
1895
|
const showIcon = !legendViewMode && tagGroups.length > 0;
|
|
1441
1896
|
const iconReserve = showIcon ? LEGEND_ICON_W : 0;
|
|
1442
|
-
const pillW =
|
|
1897
|
+
const pillW =
|
|
1898
|
+
measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) +
|
|
1899
|
+
LEGEND_PILL_PAD +
|
|
1900
|
+
iconReserve;
|
|
1443
1901
|
let groupW = pillW;
|
|
1444
1902
|
if (isActive) {
|
|
1445
|
-
const entries =
|
|
1903
|
+
const entries =
|
|
1904
|
+
filteredEntries.get(group.name.toLowerCase()) ?? group.entries;
|
|
1446
1905
|
let entriesW = 0;
|
|
1447
1906
|
for (const entry of entries) {
|
|
1448
|
-
entriesW +=
|
|
1907
|
+
entriesW +=
|
|
1908
|
+
LEGEND_DOT_R * 2 +
|
|
1909
|
+
LEGEND_ENTRY_DOT_GAP +
|
|
1910
|
+
measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
1911
|
+
LEGEND_ENTRY_TRAIL;
|
|
1449
1912
|
}
|
|
1450
1913
|
groupW = LEGEND_CAPSULE_PAD * 2 + pillW + 4 + entriesW;
|
|
1451
1914
|
} else if (isSwimlane && !isActive) {
|
|
@@ -1459,7 +1922,8 @@ function renderTagLegend(
|
|
|
1459
1922
|
|
|
1460
1923
|
// Critical Path pill width
|
|
1461
1924
|
const cpLabel = 'Critical Path';
|
|
1462
|
-
const cpPillW =
|
|
1925
|
+
const cpPillW =
|
|
1926
|
+
measureLegendText(cpLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1463
1927
|
if (hasCriticalPath) {
|
|
1464
1928
|
if (visibleGroups.length > 0) totalW += LEGEND_GROUP_GAP;
|
|
1465
1929
|
totalW += cpPillW;
|
|
@@ -1469,7 +1933,8 @@ function renderTagLegend(
|
|
|
1469
1933
|
const containerWidth = chartLeftMargin + chartInnerWidth + RIGHT_MARGIN;
|
|
1470
1934
|
const legendX = (containerWidth - totalW) / 2;
|
|
1471
1935
|
|
|
1472
|
-
const legendRow = svg
|
|
1936
|
+
const legendRow = svg
|
|
1937
|
+
.append('g')
|
|
1473
1938
|
.attr('class', 'gantt-tag-legend-container')
|
|
1474
1939
|
.attr('transform', `translate(${legendX}, ${legendY})`);
|
|
1475
1940
|
|
|
@@ -1477,25 +1942,36 @@ function renderTagLegend(
|
|
|
1477
1942
|
|
|
1478
1943
|
for (let i = 0; i < visibleGroups.length; i++) {
|
|
1479
1944
|
const group = visibleGroups[i];
|
|
1480
|
-
const isActive =
|
|
1481
|
-
|
|
1945
|
+
const isActive =
|
|
1946
|
+
activeGroupName?.toLowerCase() === group.name.toLowerCase();
|
|
1947
|
+
const isSwimlane =
|
|
1948
|
+
currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
|
|
1482
1949
|
const showIcon = !legendViewMode && tagGroups.length > 0;
|
|
1483
1950
|
const iconReserve = showIcon ? LEGEND_ICON_W : 0;
|
|
1484
|
-
const pillW =
|
|
1485
|
-
|
|
1951
|
+
const pillW =
|
|
1952
|
+
measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) +
|
|
1953
|
+
LEGEND_PILL_PAD +
|
|
1954
|
+
iconReserve;
|
|
1955
|
+
const pillH = isActive
|
|
1956
|
+
? LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2
|
|
1957
|
+
: LEGEND_HEIGHT;
|
|
1486
1958
|
const groupW = groupWidths[i];
|
|
1487
1959
|
|
|
1488
|
-
const gEl = legendRow
|
|
1960
|
+
const gEl = legendRow
|
|
1961
|
+
.append('g')
|
|
1489
1962
|
.attr('transform', `translate(${cursorX}, 0)`)
|
|
1490
1963
|
.attr('class', 'gantt-tag-legend-group')
|
|
1491
1964
|
.attr('data-tag-group', group.name)
|
|
1492
1965
|
.attr('data-line-number', String(group.lineNumber))
|
|
1493
1966
|
.style('cursor', 'pointer')
|
|
1494
|
-
.on('click', () => {
|
|
1967
|
+
.on('click', () => {
|
|
1968
|
+
if (onToggle) onToggle(group.name);
|
|
1969
|
+
});
|
|
1495
1970
|
|
|
1496
1971
|
if (isActive) {
|
|
1497
1972
|
// Outer capsule background
|
|
1498
|
-
gEl
|
|
1973
|
+
gEl
|
|
1974
|
+
.append('rect')
|
|
1499
1975
|
.attr('width', groupW)
|
|
1500
1976
|
.attr('height', LEGEND_HEIGHT)
|
|
1501
1977
|
.attr('rx', LEGEND_HEIGHT / 2)
|
|
@@ -1506,7 +1982,8 @@ function renderTagLegend(
|
|
|
1506
1982
|
const pillYOff = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
1507
1983
|
|
|
1508
1984
|
// Pill background
|
|
1509
|
-
gEl
|
|
1985
|
+
gEl
|
|
1986
|
+
.append('rect')
|
|
1510
1987
|
.attr('x', pillXOff)
|
|
1511
1988
|
.attr('y', pillYOff)
|
|
1512
1989
|
.attr('width', pillW)
|
|
@@ -1516,7 +1993,8 @@ function renderTagLegend(
|
|
|
1516
1993
|
|
|
1517
1994
|
// Active pill border
|
|
1518
1995
|
if (isActive) {
|
|
1519
|
-
gEl
|
|
1996
|
+
gEl
|
|
1997
|
+
.append('rect')
|
|
1520
1998
|
.attr('x', pillXOff)
|
|
1521
1999
|
.attr('y', pillYOff)
|
|
1522
2000
|
.attr('width', pillW)
|
|
@@ -1528,8 +2006,10 @@ function renderTagLegend(
|
|
|
1528
2006
|
}
|
|
1529
2007
|
|
|
1530
2008
|
// Pill text (offset to leave room for icon on right)
|
|
1531
|
-
const textW =
|
|
1532
|
-
|
|
2009
|
+
const textW =
|
|
2010
|
+
measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
2011
|
+
gEl
|
|
2012
|
+
.append('text')
|
|
1533
2013
|
.attr('x', pillXOff + textW / 2)
|
|
1534
2014
|
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
1535
2015
|
.attr('text-anchor', 'middle')
|
|
@@ -1544,17 +2024,16 @@ function renderTagLegend(
|
|
|
1544
2024
|
const iconY = (LEGEND_HEIGHT - 10) / 2;
|
|
1545
2025
|
const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimlane, palette);
|
|
1546
2026
|
iconEl.append('title').text(`Group by ${group.name}`);
|
|
1547
|
-
iconEl
|
|
1548
|
-
.
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
});
|
|
2027
|
+
iconEl.style('cursor', 'pointer').on('click', (event: Event) => {
|
|
2028
|
+
event.stopPropagation();
|
|
2029
|
+
if (onSwimlaneChange) {
|
|
2030
|
+
onSwimlaneChange(
|
|
2031
|
+
currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase()
|
|
2032
|
+
? null
|
|
2033
|
+
: group.name
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
1558
2037
|
}
|
|
1559
2038
|
|
|
1560
2039
|
// Entries (when active — expanded color group, only used values)
|
|
@@ -1566,20 +2045,23 @@ function renderTagLegend(
|
|
|
1566
2045
|
const entryValue = entry.value.toLowerCase();
|
|
1567
2046
|
|
|
1568
2047
|
// Wrap dot + label in a <g> for hover targeting
|
|
1569
|
-
const entryG = gEl
|
|
2048
|
+
const entryG = gEl
|
|
2049
|
+
.append('g')
|
|
1570
2050
|
.attr('class', 'gantt-legend-entry')
|
|
1571
2051
|
.attr('data-line-number', String(entry.lineNumber))
|
|
1572
2052
|
.style('cursor', 'pointer');
|
|
1573
2053
|
|
|
1574
2054
|
// Dot
|
|
1575
|
-
entryG
|
|
2055
|
+
entryG
|
|
2056
|
+
.append('circle')
|
|
1576
2057
|
.attr('cx', ex + LEGEND_DOT_R)
|
|
1577
2058
|
.attr('cy', LEGEND_HEIGHT / 2)
|
|
1578
2059
|
.attr('r', LEGEND_DOT_R)
|
|
1579
2060
|
.attr('fill', entry.color);
|
|
1580
2061
|
|
|
1581
2062
|
// Label
|
|
1582
|
-
entryG
|
|
2063
|
+
entryG
|
|
2064
|
+
.append('text')
|
|
1583
2065
|
.attr('x', ex + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP)
|
|
1584
2066
|
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 2)
|
|
1585
2067
|
.attr('text-anchor', 'start')
|
|
@@ -1590,28 +2072,48 @@ function renderTagLegend(
|
|
|
1590
2072
|
// Hover: highlight matching tasks + labels + lane headers, fade others
|
|
1591
2073
|
entryG
|
|
1592
2074
|
.on('mouseenter', () => {
|
|
1593
|
-
chartG
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
2075
|
+
chartG
|
|
2076
|
+
.selectAll<SVGGElement, unknown>('.gantt-task')
|
|
2077
|
+
.each(function () {
|
|
2078
|
+
const el = d3Selection.select(this);
|
|
2079
|
+
const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
|
|
2080
|
+
el.attr('opacity', matches ? 1 : FADE_OPACITY);
|
|
2081
|
+
});
|
|
2082
|
+
chartG
|
|
2083
|
+
.selectAll<SVGElement, unknown>('.gantt-milestone')
|
|
2084
|
+
.attr('opacity', FADE_OPACITY);
|
|
2085
|
+
chartG
|
|
2086
|
+
.selectAll<
|
|
2087
|
+
SVGElement,
|
|
2088
|
+
unknown
|
|
2089
|
+
>('.gantt-group-bar, .gantt-group-summary')
|
|
2090
|
+
.attr('opacity', FADE_OPACITY);
|
|
1600
2091
|
// Fade left-side task labels
|
|
1601
|
-
svg
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
2092
|
+
svg
|
|
2093
|
+
.selectAll<SVGTextElement, unknown>('.gantt-task-label')
|
|
2094
|
+
.each(function () {
|
|
2095
|
+
const el = d3Selection.select(this);
|
|
2096
|
+
const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
|
|
2097
|
+
el.attr('opacity', matches ? 1 : FADE_OPACITY);
|
|
2098
|
+
});
|
|
1606
2099
|
// Fade group labels
|
|
1607
|
-
svg
|
|
2100
|
+
svg
|
|
2101
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
2102
|
+
.attr('opacity', FADE_OPACITY);
|
|
1608
2103
|
// Fade non-matching lane headers + bands + accents
|
|
1609
|
-
svg
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2104
|
+
svg
|
|
2105
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
2106
|
+
.each(function () {
|
|
2107
|
+
const el = d3Selection.select(this);
|
|
2108
|
+
const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
|
|
2109
|
+
el.attr('opacity', matches ? 1 : FADE_OPACITY);
|
|
2110
|
+
});
|
|
2111
|
+
chartG
|
|
2112
|
+
.selectAll<
|
|
2113
|
+
SVGElement,
|
|
2114
|
+
unknown
|
|
2115
|
+
>('.gantt-lane-band, .gantt-lane-accent')
|
|
2116
|
+
.attr('opacity', FADE_OPACITY);
|
|
1615
2117
|
})
|
|
1616
2118
|
.on('mouseleave', () => {
|
|
1617
2119
|
if (criticalPathActive) {
|
|
@@ -1621,7 +2123,11 @@ function renderTagLegend(
|
|
|
1621
2123
|
}
|
|
1622
2124
|
});
|
|
1623
2125
|
|
|
1624
|
-
ex +=
|
|
2126
|
+
ex +=
|
|
2127
|
+
LEGEND_DOT_R * 2 +
|
|
2128
|
+
LEGEND_ENTRY_DOT_GAP +
|
|
2129
|
+
measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
2130
|
+
LEGEND_ENTRY_TRAIL;
|
|
1625
2131
|
}
|
|
1626
2132
|
}
|
|
1627
2133
|
|
|
@@ -1631,21 +2137,26 @@ function renderTagLegend(
|
|
|
1631
2137
|
// Critical Path pill
|
|
1632
2138
|
if (hasCriticalPath) {
|
|
1633
2139
|
const cpLineNum = optionLineNumbers['critical-path'];
|
|
1634
|
-
const cpG = legendRow
|
|
2140
|
+
const cpG = legendRow
|
|
2141
|
+
.append('g')
|
|
1635
2142
|
.attr('transform', `translate(${cursorX}, 0)`)
|
|
1636
2143
|
.attr('class', 'gantt-legend-critical-path')
|
|
1637
2144
|
.style('cursor', 'pointer')
|
|
1638
|
-
.on('click', () => {
|
|
2145
|
+
.on('click', () => {
|
|
2146
|
+
if (onToggleCriticalPath) onToggleCriticalPath();
|
|
2147
|
+
});
|
|
1639
2148
|
if (cpLineNum) cpG.attr('data-line-number', String(cpLineNum));
|
|
1640
2149
|
|
|
1641
|
-
cpG
|
|
2150
|
+
cpG
|
|
2151
|
+
.append('rect')
|
|
1642
2152
|
.attr('width', cpPillW)
|
|
1643
2153
|
.attr('height', LEGEND_HEIGHT)
|
|
1644
2154
|
.attr('rx', LEGEND_HEIGHT / 2)
|
|
1645
2155
|
.attr('fill', criticalPathActive ? palette.bg : groupBg);
|
|
1646
2156
|
|
|
1647
2157
|
if (criticalPathActive) {
|
|
1648
|
-
cpG
|
|
2158
|
+
cpG
|
|
2159
|
+
.append('rect')
|
|
1649
2160
|
.attr('width', cpPillW)
|
|
1650
2161
|
.attr('height', LEGEND_HEIGHT)
|
|
1651
2162
|
.attr('rx', LEGEND_HEIGHT / 2)
|
|
@@ -1654,7 +2165,8 @@ function renderTagLegend(
|
|
|
1654
2165
|
.attr('stroke-width', 0.75);
|
|
1655
2166
|
}
|
|
1656
2167
|
|
|
1657
|
-
cpG
|
|
2168
|
+
cpG
|
|
2169
|
+
.append('text')
|
|
1658
2170
|
.attr('x', cpPillW / 2)
|
|
1659
2171
|
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
1660
2172
|
.attr('text-anchor', 'middle')
|
|
@@ -1690,7 +2202,7 @@ function renderErasAndMarkers(
|
|
|
1690
2202
|
resolved: ResolvedSchedule,
|
|
1691
2203
|
xScale: d3Scale.ScaleLinear<number, number>,
|
|
1692
2204
|
innerHeight: number,
|
|
1693
|
-
palette: PaletteColors
|
|
2205
|
+
palette: PaletteColors
|
|
1694
2206
|
): void {
|
|
1695
2207
|
// Eras: semi-transparent background bands
|
|
1696
2208
|
for (let i = 0; i < resolved.eras.length; i++) {
|
|
@@ -1705,11 +2217,13 @@ function renderErasAndMarkers(
|
|
|
1705
2217
|
const eraStartDate = parseDateStringToDate(era.startDate);
|
|
1706
2218
|
const eraEndDate = parseDateStringToDate(era.endDate);
|
|
1707
2219
|
|
|
1708
|
-
const eraG = g
|
|
2220
|
+
const eraG = g
|
|
2221
|
+
.append('g')
|
|
1709
2222
|
.attr('class', 'gantt-era-group')
|
|
1710
2223
|
.attr('data-line-number', String(era.lineNumber));
|
|
1711
2224
|
|
|
1712
|
-
const eraRect = eraG
|
|
2225
|
+
const eraRect = eraG
|
|
2226
|
+
.append('rect')
|
|
1713
2227
|
.attr('class', 'gantt-era')
|
|
1714
2228
|
.attr('x', sx)
|
|
1715
2229
|
.attr('y', 0)
|
|
@@ -1719,7 +2233,8 @@ function renderErasAndMarkers(
|
|
|
1719
2233
|
.attr('opacity', baseEraOpacity);
|
|
1720
2234
|
|
|
1721
2235
|
// Era label (above date scale, same zone as markers)
|
|
1722
|
-
eraG
|
|
2236
|
+
eraG
|
|
2237
|
+
.append('text')
|
|
1723
2238
|
.attr('class', 'gantt-era-label')
|
|
1724
2239
|
.attr('x', (sx + ex) / 2)
|
|
1725
2240
|
.attr('y', -24)
|
|
@@ -1733,18 +2248,46 @@ function renderErasAndMarkers(
|
|
|
1733
2248
|
eraG
|
|
1734
2249
|
.on('mouseenter', () => {
|
|
1735
2250
|
// Fade everything
|
|
1736
|
-
g.selectAll<SVGGElement, unknown>('.gantt-task').attr(
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
g.selectAll<SVGElement, unknown>(
|
|
2251
|
+
g.selectAll<SVGGElement, unknown>('.gantt-task').attr(
|
|
2252
|
+
'opacity',
|
|
2253
|
+
FADE_OPACITY
|
|
2254
|
+
);
|
|
2255
|
+
g.selectAll<SVGElement, unknown>('.gantt-milestone').attr(
|
|
2256
|
+
'opacity',
|
|
2257
|
+
FADE_OPACITY
|
|
2258
|
+
);
|
|
2259
|
+
g.selectAll<SVGElement, unknown>(
|
|
2260
|
+
'.gantt-group-bar, .gantt-group-summary'
|
|
2261
|
+
).attr('opacity', FADE_OPACITY);
|
|
2262
|
+
svg
|
|
2263
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
2264
|
+
.attr('opacity', FADE_OPACITY);
|
|
2265
|
+
svg
|
|
2266
|
+
.selectAll<SVGTextElement, unknown>('.gantt-task-label')
|
|
2267
|
+
.attr('opacity', FADE_OPACITY);
|
|
2268
|
+
svg
|
|
2269
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
2270
|
+
.attr('opacity', FADE_OPACITY);
|
|
2271
|
+
g.selectAll<SVGElement, unknown>(
|
|
2272
|
+
'.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group'
|
|
2273
|
+
).attr('opacity', FADE_OPACITY);
|
|
2274
|
+
g.selectAll<SVGElement, unknown>(
|
|
2275
|
+
'.gantt-dep-arrow, .gantt-dep-arrowhead'
|
|
2276
|
+
).attr('opacity', FADE_OPACITY);
|
|
2277
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2278
|
+
'opacity',
|
|
2279
|
+
FADE_OPACITY
|
|
2280
|
+
);
|
|
1745
2281
|
// Highlight this era
|
|
1746
2282
|
eraRect.attr('opacity', hoverEraOpacity);
|
|
1747
|
-
showGanttDateIndicators(
|
|
2283
|
+
showGanttDateIndicators(
|
|
2284
|
+
g,
|
|
2285
|
+
xScale,
|
|
2286
|
+
eraStartDate,
|
|
2287
|
+
eraEndDate,
|
|
2288
|
+
innerHeight,
|
|
2289
|
+
color
|
|
2290
|
+
);
|
|
1748
2291
|
})
|
|
1749
2292
|
.on('mouseleave', () => {
|
|
1750
2293
|
resetHighlight(g, svg);
|
|
@@ -1762,13 +2305,15 @@ function renderErasAndMarkers(
|
|
|
1762
2305
|
const labelY = -24;
|
|
1763
2306
|
const diamondY = labelY + 14;
|
|
1764
2307
|
|
|
1765
|
-
const markerG = g
|
|
2308
|
+
const markerG = g
|
|
2309
|
+
.append('g')
|
|
1766
2310
|
.attr('class', 'gantt-marker-group')
|
|
1767
2311
|
.attr('data-line-number', String(marker.lineNumber))
|
|
1768
2312
|
.style('cursor', 'pointer');
|
|
1769
2313
|
|
|
1770
2314
|
// Invisible hit rect for easier clicking/hovering
|
|
1771
|
-
markerG
|
|
2315
|
+
markerG
|
|
2316
|
+
.append('rect')
|
|
1772
2317
|
.attr('x', mx - 40)
|
|
1773
2318
|
.attr('y', labelY - 12)
|
|
1774
2319
|
.attr('width', 80)
|
|
@@ -1777,7 +2322,8 @@ function renderErasAndMarkers(
|
|
|
1777
2322
|
.attr('pointer-events', 'all');
|
|
1778
2323
|
|
|
1779
2324
|
// Label above diamond
|
|
1780
|
-
markerG
|
|
2325
|
+
markerG
|
|
2326
|
+
.append('text')
|
|
1781
2327
|
.attr('class', 'gantt-marker-label')
|
|
1782
2328
|
.attr('x', mx)
|
|
1783
2329
|
.attr('y', labelY)
|
|
@@ -1788,13 +2334,18 @@ function renderErasAndMarkers(
|
|
|
1788
2334
|
.text(marker.label);
|
|
1789
2335
|
|
|
1790
2336
|
// Diamond below label
|
|
1791
|
-
markerG
|
|
1792
|
-
.
|
|
2337
|
+
markerG
|
|
2338
|
+
.append('path')
|
|
2339
|
+
.attr(
|
|
2340
|
+
'd',
|
|
2341
|
+
`M${mx},${diamondY - diamondSize} l${diamondSize},${diamondSize} l-${diamondSize},${diamondSize} l-${diamondSize},-${diamondSize} Z`
|
|
2342
|
+
)
|
|
1793
2343
|
.attr('fill', color)
|
|
1794
2344
|
.attr('opacity', 0.9);
|
|
1795
2345
|
|
|
1796
2346
|
// Dashed line from diamond down
|
|
1797
|
-
markerG
|
|
2347
|
+
markerG
|
|
2348
|
+
.append('line')
|
|
1798
2349
|
.attr('class', 'gantt-marker')
|
|
1799
2350
|
.attr('x1', mx)
|
|
1800
2351
|
.attr('y1', diamondY + diamondSize)
|
|
@@ -1811,21 +2362,53 @@ function renderErasAndMarkers(
|
|
|
1811
2362
|
markerG
|
|
1812
2363
|
.on('mouseenter', () => {
|
|
1813
2364
|
// Fade everything
|
|
1814
|
-
g.selectAll<SVGGElement, unknown>('.gantt-task').attr(
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
g.selectAll<SVGElement, unknown>(
|
|
2365
|
+
g.selectAll<SVGGElement, unknown>('.gantt-task').attr(
|
|
2366
|
+
'opacity',
|
|
2367
|
+
FADE_OPACITY
|
|
2368
|
+
);
|
|
2369
|
+
g.selectAll<SVGElement, unknown>('.gantt-milestone').attr(
|
|
2370
|
+
'opacity',
|
|
2371
|
+
FADE_OPACITY
|
|
2372
|
+
);
|
|
2373
|
+
g.selectAll<SVGElement, unknown>(
|
|
2374
|
+
'.gantt-group-bar, .gantt-group-summary'
|
|
2375
|
+
).attr('opacity', FADE_OPACITY);
|
|
2376
|
+
svg
|
|
2377
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
2378
|
+
.attr('opacity', FADE_OPACITY);
|
|
2379
|
+
svg
|
|
2380
|
+
.selectAll<SVGTextElement, unknown>('.gantt-task-label')
|
|
2381
|
+
.attr('opacity', FADE_OPACITY);
|
|
2382
|
+
svg
|
|
2383
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
2384
|
+
.attr('opacity', FADE_OPACITY);
|
|
2385
|
+
g.selectAll<SVGElement, unknown>(
|
|
2386
|
+
'.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group'
|
|
2387
|
+
).attr('opacity', FADE_OPACITY);
|
|
2388
|
+
g.selectAll<SVGElement, unknown>(
|
|
2389
|
+
'.gantt-dep-arrow, .gantt-dep-arrowhead'
|
|
2390
|
+
).attr('opacity', FADE_OPACITY);
|
|
2391
|
+
g.selectAll<SVGElement, unknown>('.gantt-era-group').attr(
|
|
2392
|
+
'opacity',
|
|
2393
|
+
FADE_OPACITY
|
|
2394
|
+
);
|
|
1823
2395
|
// Fade other markers but keep this one highlighted
|
|
1824
|
-
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2396
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2397
|
+
'opacity',
|
|
2398
|
+
FADE_OPACITY
|
|
2399
|
+
);
|
|
1825
2400
|
markerG.attr('opacity', 1);
|
|
1826
2401
|
markerLine.attr('opacity', 0.8);
|
|
1827
2402
|
markerDiamond.attr('opacity', 0);
|
|
1828
|
-
showGanttDateIndicators(
|
|
2403
|
+
showGanttDateIndicators(
|
|
2404
|
+
g,
|
|
2405
|
+
xScale,
|
|
2406
|
+
markerDate,
|
|
2407
|
+
null,
|
|
2408
|
+
innerHeight,
|
|
2409
|
+
color,
|
|
2410
|
+
{ skipStartLine: true }
|
|
2411
|
+
);
|
|
1829
2412
|
})
|
|
1830
2413
|
.on('mouseleave', () => {
|
|
1831
2414
|
resetHighlight(g, svg);
|
|
@@ -1841,7 +2424,7 @@ function renderErasAndMarkers(
|
|
|
1841
2424
|
* Used for eras and markers which store dates as strings.
|
|
1842
2425
|
*/
|
|
1843
2426
|
function parseDateStringToDate(s: string): Date {
|
|
1844
|
-
const parts = s.split('-').map(p => parseInt(p, 10));
|
|
2427
|
+
const parts = s.split('-').map((p) => parseInt(p, 10));
|
|
1845
2428
|
const year = parts[0];
|
|
1846
2429
|
const month = parts.length >= 2 ? parts[1] - 1 : 0;
|
|
1847
2430
|
const day = parts.length >= 3 ? parts[2] : 1;
|
|
@@ -1864,28 +2447,34 @@ function highlightDeps(
|
|
|
1864
2447
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
1865
2448
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1866
2449
|
taskId: string,
|
|
1867
|
-
resolved: ResolvedSchedule
|
|
2450
|
+
resolved: ResolvedSchedule
|
|
1868
2451
|
): void {
|
|
1869
2452
|
// Find immediate predecessors and successors
|
|
1870
2453
|
const related = new Set<string>([taskId]);
|
|
1871
|
-
const task = resolved.tasks.find(t => t.task.id === taskId);
|
|
2454
|
+
const task = resolved.tasks.find((t) => t.task.id === taskId);
|
|
1872
2455
|
if (!task) return;
|
|
1873
2456
|
|
|
1874
2457
|
// Predecessors: tasks whose deps point to this task
|
|
1875
2458
|
for (const rt of resolved.tasks) {
|
|
1876
2459
|
for (const dep of rt.task.dependencies) {
|
|
1877
2460
|
// Check if this dep points to our task
|
|
1878
|
-
if (
|
|
1879
|
-
|
|
2461
|
+
if (
|
|
2462
|
+
dep.targetName === task.task.label ||
|
|
2463
|
+
`${task.groupPath.join('.')}.${task.task.label}`.endsWith(
|
|
2464
|
+
dep.targetName
|
|
2465
|
+
)
|
|
2466
|
+
) {
|
|
1880
2467
|
related.add(rt.task.id);
|
|
1881
2468
|
}
|
|
1882
2469
|
}
|
|
1883
2470
|
}
|
|
1884
2471
|
// Successors: tasks this task has deps pointing to
|
|
1885
2472
|
for (const dep of task.task.dependencies) {
|
|
1886
|
-
const target = resolved.tasks.find(
|
|
1887
|
-
t
|
|
1888
|
-
|
|
2473
|
+
const target = resolved.tasks.find(
|
|
2474
|
+
(t) =>
|
|
2475
|
+
t.task.label === dep.targetName ||
|
|
2476
|
+
`${t.groupPath.join('.')}.${t.task.label}`.endsWith(dep.targetName)
|
|
2477
|
+
);
|
|
1889
2478
|
if (target) related.add(target.task.id);
|
|
1890
2479
|
}
|
|
1891
2480
|
|
|
@@ -1895,14 +2484,28 @@ function highlightDeps(
|
|
|
1895
2484
|
const id = el.attr('data-task-id');
|
|
1896
2485
|
el.attr('opacity', id && related.has(id) ? 1 : FADE_OPACITY);
|
|
1897
2486
|
});
|
|
1898
|
-
g.selectAll<SVGGElement, unknown>('.gantt-milestone').attr(
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
g.selectAll<SVGElement, unknown>(
|
|
2487
|
+
g.selectAll<SVGGElement, unknown>('.gantt-milestone').attr(
|
|
2488
|
+
'opacity',
|
|
2489
|
+
FADE_OPACITY
|
|
2490
|
+
);
|
|
2491
|
+
g.selectAll<SVGElement, unknown>(
|
|
2492
|
+
'.gantt-group-bar, .gantt-group-summary'
|
|
2493
|
+
).attr('opacity', FADE_OPACITY);
|
|
2494
|
+
svg
|
|
2495
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
2496
|
+
.attr('opacity', FADE_OPACITY);
|
|
2497
|
+
svg
|
|
2498
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
2499
|
+
.attr('opacity', FADE_OPACITY);
|
|
2500
|
+
g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr(
|
|
2501
|
+
'opacity',
|
|
2502
|
+
FADE_OPACITY
|
|
2503
|
+
);
|
|
1903
2504
|
|
|
1904
2505
|
// Fade dependency arrows not connected to related tasks
|
|
1905
|
-
g.selectAll<SVGElement, unknown>(
|
|
2506
|
+
g.selectAll<SVGElement, unknown>(
|
|
2507
|
+
'.gantt-dep-arrow, .gantt-dep-arrowhead'
|
|
2508
|
+
).each(function () {
|
|
1906
2509
|
const el = d3Selection.select(this);
|
|
1907
2510
|
const from = el.attr('data-dep-from');
|
|
1908
2511
|
const to = el.attr('data-dep-to');
|
|
@@ -1910,13 +2513,16 @@ function highlightDeps(
|
|
|
1910
2513
|
el.attr('opacity', isRelated ? 0.5 : FADE_OPACITY);
|
|
1911
2514
|
});
|
|
1912
2515
|
// Fade markers
|
|
1913
|
-
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2516
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2517
|
+
'opacity',
|
|
2518
|
+
FADE_OPACITY
|
|
2519
|
+
);
|
|
1914
2520
|
}
|
|
1915
2521
|
|
|
1916
2522
|
function highlightGroup(
|
|
1917
2523
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
1918
2524
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1919
|
-
groupName: string
|
|
2525
|
+
groupName: string
|
|
1920
2526
|
): void {
|
|
1921
2527
|
// Fade tasks not in this group
|
|
1922
2528
|
g.selectAll<SVGGElement, unknown>('.gantt-task').each(function () {
|
|
@@ -1929,7 +2535,9 @@ function highlightGroup(
|
|
|
1929
2535
|
el.attr('opacity', el.attr('data-group') === groupName ? 1 : FADE_OPACITY);
|
|
1930
2536
|
});
|
|
1931
2537
|
// Fade other group bars
|
|
1932
|
-
g.selectAll<SVGElement, unknown>(
|
|
2538
|
+
g.selectAll<SVGElement, unknown>(
|
|
2539
|
+
'.gantt-group-bar, .gantt-group-summary'
|
|
2540
|
+
).each(function () {
|
|
1933
2541
|
const el = d3Selection.select(this);
|
|
1934
2542
|
el.attr('opacity', el.attr('data-group') === groupName ? 1 : FADE_OPACITY);
|
|
1935
2543
|
});
|
|
@@ -1944,23 +2552,44 @@ function highlightGroup(
|
|
|
1944
2552
|
el.attr('opacity', el.attr('data-group') === groupName ? 1 : FADE_OPACITY);
|
|
1945
2553
|
});
|
|
1946
2554
|
// Fade group bands not matching
|
|
1947
|
-
svg
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2555
|
+
svg
|
|
2556
|
+
.selectAll<
|
|
2557
|
+
SVGElement,
|
|
2558
|
+
unknown
|
|
2559
|
+
>('.gantt-group-band-bg, .gantt-group-band-accent')
|
|
2560
|
+
.each(function () {
|
|
2561
|
+
const el = d3Selection.select(this);
|
|
2562
|
+
el.attr(
|
|
2563
|
+
'opacity',
|
|
2564
|
+
el.attr('data-group') === groupName ? 1 : FADE_OPACITY
|
|
2565
|
+
);
|
|
2566
|
+
});
|
|
1951
2567
|
// Fade lane elements
|
|
1952
|
-
svg
|
|
1953
|
-
|
|
1954
|
-
|
|
2568
|
+
svg
|
|
2569
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
2570
|
+
.attr('opacity', FADE_OPACITY);
|
|
2571
|
+
svg
|
|
2572
|
+
.selectAll<
|
|
2573
|
+
SVGElement,
|
|
2574
|
+
unknown
|
|
2575
|
+
>('.gantt-lane-band-bg, .gantt-lane-band-accent')
|
|
2576
|
+
.attr('opacity', FADE_OPACITY);
|
|
2577
|
+
g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr(
|
|
2578
|
+
'opacity',
|
|
2579
|
+
FADE_OPACITY
|
|
2580
|
+
);
|
|
1955
2581
|
// Fade markers
|
|
1956
|
-
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2582
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2583
|
+
'opacity',
|
|
2584
|
+
FADE_OPACITY
|
|
2585
|
+
);
|
|
1957
2586
|
}
|
|
1958
2587
|
|
|
1959
2588
|
function highlightLane(
|
|
1960
2589
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
1961
2590
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
1962
2591
|
tagKey: string,
|
|
1963
|
-
laneName: string
|
|
2592
|
+
laneName: string
|
|
1964
2593
|
): void {
|
|
1965
2594
|
const tagAttr = `data-tag-${tagKey}`;
|
|
1966
2595
|
const laneValue = laneName.toLowerCase();
|
|
@@ -1991,22 +2620,39 @@ function highlightLane(
|
|
|
1991
2620
|
el.attr('opacity', el.attr('data-lane') === laneName ? 1 : FADE_OPACITY);
|
|
1992
2621
|
});
|
|
1993
2622
|
// Fade lane bands not matching
|
|
1994
|
-
svg
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2623
|
+
svg
|
|
2624
|
+
.selectAll<
|
|
2625
|
+
SVGElement,
|
|
2626
|
+
unknown
|
|
2627
|
+
>('.gantt-lane-band-bg, .gantt-lane-band-accent')
|
|
2628
|
+
.each(function () {
|
|
2629
|
+
const el = d3Selection.select(this);
|
|
2630
|
+
el.attr('opacity', el.attr('data-lane') === laneName ? 1 : FADE_OPACITY);
|
|
2631
|
+
});
|
|
1998
2632
|
// Fade group elements (not relevant in lane mode)
|
|
1999
|
-
g.selectAll<SVGElement, unknown>(
|
|
2000
|
-
|
|
2001
|
-
|
|
2633
|
+
g.selectAll<SVGElement, unknown>(
|
|
2634
|
+
'.gantt-group-bar, .gantt-group-summary'
|
|
2635
|
+
).attr('opacity', FADE_OPACITY);
|
|
2636
|
+
svg
|
|
2637
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
2638
|
+
.attr('opacity', FADE_OPACITY);
|
|
2639
|
+
svg
|
|
2640
|
+
.selectAll<
|
|
2641
|
+
SVGElement,
|
|
2642
|
+
unknown
|
|
2643
|
+
>('.gantt-group-band-bg, .gantt-group-band-accent')
|
|
2644
|
+
.attr('opacity', FADE_OPACITY);
|
|
2002
2645
|
// Fade markers
|
|
2003
|
-
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2646
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2647
|
+
'opacity',
|
|
2648
|
+
FADE_OPACITY
|
|
2649
|
+
);
|
|
2004
2650
|
}
|
|
2005
2651
|
|
|
2006
2652
|
function highlightTask(
|
|
2007
2653
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
2008
2654
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
2009
|
-
taskId: string
|
|
2655
|
+
taskId: string
|
|
2010
2656
|
): void {
|
|
2011
2657
|
// Fade tasks not matching
|
|
2012
2658
|
g.selectAll<SVGGElement, unknown>('.gantt-task').each(function () {
|
|
@@ -2014,31 +2660,60 @@ function highlightTask(
|
|
|
2014
2660
|
el.attr('opacity', el.attr('data-task-id') === taskId ? 1 : FADE_OPACITY);
|
|
2015
2661
|
});
|
|
2016
2662
|
// Fade milestones not matching
|
|
2017
|
-
g.selectAll<SVGElement, unknown>('.gantt-milestone').attr(
|
|
2663
|
+
g.selectAll<SVGElement, unknown>('.gantt-milestone').attr(
|
|
2664
|
+
'opacity',
|
|
2665
|
+
FADE_OPACITY
|
|
2666
|
+
);
|
|
2018
2667
|
// Fade task labels not matching
|
|
2019
2668
|
svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').each(function () {
|
|
2020
2669
|
const el = d3Selection.select(this);
|
|
2021
2670
|
el.attr('opacity', el.attr('data-task-id') === taskId ? 1 : FADE_OPACITY);
|
|
2022
2671
|
});
|
|
2023
2672
|
// Fade group/lane elements
|
|
2024
|
-
g.selectAll<SVGElement, unknown>(
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
svg
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2673
|
+
g.selectAll<SVGElement, unknown>(
|
|
2674
|
+
'.gantt-group-bar, .gantt-group-summary'
|
|
2675
|
+
).attr('opacity', FADE_OPACITY);
|
|
2676
|
+
svg
|
|
2677
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
2678
|
+
.attr('opacity', FADE_OPACITY);
|
|
2679
|
+
svg
|
|
2680
|
+
.selectAll<
|
|
2681
|
+
SVGElement,
|
|
2682
|
+
unknown
|
|
2683
|
+
>('.gantt-group-band-bg, .gantt-group-band-accent')
|
|
2684
|
+
.attr('opacity', FADE_OPACITY);
|
|
2685
|
+
svg
|
|
2686
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
2687
|
+
.attr('opacity', FADE_OPACITY);
|
|
2688
|
+
svg
|
|
2689
|
+
.selectAll<
|
|
2690
|
+
SVGElement,
|
|
2691
|
+
unknown
|
|
2692
|
+
>('.gantt-lane-band-bg, .gantt-lane-band-accent')
|
|
2693
|
+
.attr('opacity', FADE_OPACITY);
|
|
2694
|
+
g.selectAll<SVGElement, unknown>(
|
|
2695
|
+
'.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group'
|
|
2696
|
+
).attr('opacity', FADE_OPACITY);
|
|
2697
|
+
g.selectAll<SVGElement, unknown>(
|
|
2698
|
+
'.gantt-dep-arrow, .gantt-dep-arrowhead'
|
|
2699
|
+
).attr('opacity', FADE_OPACITY);
|
|
2031
2700
|
// Fade markers
|
|
2032
|
-
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2701
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2702
|
+
'opacity',
|
|
2703
|
+
FADE_OPACITY
|
|
2704
|
+
);
|
|
2033
2705
|
}
|
|
2034
2706
|
|
|
2035
2707
|
function highlightMilestone(
|
|
2036
2708
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
2037
2709
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
2038
|
-
taskId: string
|
|
2710
|
+
taskId: string
|
|
2039
2711
|
): void {
|
|
2040
2712
|
// Fade tasks
|
|
2041
|
-
g.selectAll<SVGGElement, unknown>('.gantt-task').attr(
|
|
2713
|
+
g.selectAll<SVGGElement, unknown>('.gantt-task').attr(
|
|
2714
|
+
'opacity',
|
|
2715
|
+
FADE_OPACITY
|
|
2716
|
+
);
|
|
2042
2717
|
// Fade milestones not matching
|
|
2043
2718
|
g.selectAll<SVGElement, unknown>('.gantt-milestone').each(function () {
|
|
2044
2719
|
const el = d3Selection.select(this);
|
|
@@ -2050,20 +2725,43 @@ function highlightMilestone(
|
|
|
2050
2725
|
el.attr('opacity', el.attr('data-task-id') === taskId ? 1 : FADE_OPACITY);
|
|
2051
2726
|
});
|
|
2052
2727
|
// Fade group/lane elements
|
|
2053
|
-
g.selectAll<SVGElement, unknown>(
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
svg
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2728
|
+
g.selectAll<SVGElement, unknown>(
|
|
2729
|
+
'.gantt-group-bar, .gantt-group-summary'
|
|
2730
|
+
).attr('opacity', FADE_OPACITY);
|
|
2731
|
+
svg
|
|
2732
|
+
.selectAll<SVGGElement, unknown>('.gantt-group-label')
|
|
2733
|
+
.attr('opacity', FADE_OPACITY);
|
|
2734
|
+
svg
|
|
2735
|
+
.selectAll<
|
|
2736
|
+
SVGElement,
|
|
2737
|
+
unknown
|
|
2738
|
+
>('.gantt-group-band-bg, .gantt-group-band-accent')
|
|
2739
|
+
.attr('opacity', FADE_OPACITY);
|
|
2740
|
+
svg
|
|
2741
|
+
.selectAll<SVGGElement, unknown>('.gantt-lane-header')
|
|
2742
|
+
.attr('opacity', FADE_OPACITY);
|
|
2743
|
+
svg
|
|
2744
|
+
.selectAll<
|
|
2745
|
+
SVGElement,
|
|
2746
|
+
unknown
|
|
2747
|
+
>('.gantt-lane-band-bg, .gantt-lane-band-accent')
|
|
2748
|
+
.attr('opacity', FADE_OPACITY);
|
|
2749
|
+
g.selectAll<SVGElement, unknown>(
|
|
2750
|
+
'.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group'
|
|
2751
|
+
).attr('opacity', FADE_OPACITY);
|
|
2752
|
+
g.selectAll<SVGElement, unknown>(
|
|
2753
|
+
'.gantt-dep-arrow, .gantt-dep-arrowhead'
|
|
2754
|
+
).attr('opacity', FADE_OPACITY);
|
|
2060
2755
|
// Fade markers
|
|
2061
|
-
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2756
|
+
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr(
|
|
2757
|
+
'opacity',
|
|
2758
|
+
FADE_OPACITY
|
|
2759
|
+
);
|
|
2062
2760
|
}
|
|
2063
2761
|
|
|
2064
2762
|
function highlightTaskLabel(
|
|
2065
2763
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
2066
|
-
lineNumber: number
|
|
2764
|
+
lineNumber: number
|
|
2067
2765
|
): void {
|
|
2068
2766
|
const ln = String(lineNumber);
|
|
2069
2767
|
svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').each(function () {
|
|
@@ -2073,24 +2771,47 @@ function highlightTaskLabel(
|
|
|
2073
2771
|
}
|
|
2074
2772
|
|
|
2075
2773
|
function resetTaskLabels(
|
|
2076
|
-
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined
|
|
2774
|
+
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>
|
|
2077
2775
|
): void {
|
|
2078
|
-
svg
|
|
2776
|
+
svg
|
|
2777
|
+
.selectAll<SVGTextElement, unknown>('.gantt-task-label')
|
|
2778
|
+
.attr('opacity', 1);
|
|
2079
2779
|
}
|
|
2080
2780
|
|
|
2081
2781
|
function resetHighlight(
|
|
2082
2782
|
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
2083
|
-
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined
|
|
2783
|
+
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>
|
|
2084
2784
|
): void {
|
|
2085
|
-
g.selectAll<SVGGElement, unknown>('.gantt-task, .gantt-milestone').attr(
|
|
2086
|
-
|
|
2785
|
+
g.selectAll<SVGGElement, unknown>('.gantt-task, .gantt-milestone').attr(
|
|
2786
|
+
'opacity',
|
|
2787
|
+
1
|
|
2788
|
+
);
|
|
2789
|
+
g.selectAll<SVGElement, unknown>(
|
|
2790
|
+
'.gantt-group-bar, .gantt-group-summary'
|
|
2791
|
+
).attr('opacity', 1);
|
|
2087
2792
|
svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', 1);
|
|
2088
|
-
svg
|
|
2089
|
-
|
|
2793
|
+
svg
|
|
2794
|
+
.selectAll<
|
|
2795
|
+
SVGElement,
|
|
2796
|
+
unknown
|
|
2797
|
+
>('.gantt-group-band-bg, .gantt-group-band-accent')
|
|
2798
|
+
.attr('opacity', 1);
|
|
2799
|
+
svg
|
|
2800
|
+
.selectAll<SVGTextElement, unknown>('.gantt-task-label')
|
|
2801
|
+
.attr('opacity', 1);
|
|
2090
2802
|
svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', 1);
|
|
2091
|
-
svg
|
|
2092
|
-
|
|
2093
|
-
|
|
2803
|
+
svg
|
|
2804
|
+
.selectAll<
|
|
2805
|
+
SVGElement,
|
|
2806
|
+
unknown
|
|
2807
|
+
>('.gantt-lane-band-bg, .gantt-lane-band-accent')
|
|
2808
|
+
.attr('opacity', 1);
|
|
2809
|
+
g.selectAll<SVGElement, unknown>(
|
|
2810
|
+
'.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group'
|
|
2811
|
+
).attr('opacity', 1);
|
|
2812
|
+
g.selectAll<SVGElement, unknown>(
|
|
2813
|
+
'.gantt-dep-arrow, .gantt-dep-arrowhead'
|
|
2814
|
+
).attr('opacity', 0.5);
|
|
2094
2815
|
g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', 1);
|
|
2095
2816
|
g.selectAll<SVGElement, unknown>('.gantt-era-group').attr('opacity', 1);
|
|
2096
2817
|
}
|
|
@@ -2099,13 +2820,30 @@ function resetHighlight(
|
|
|
2099
2820
|
|
|
2100
2821
|
type GroupRow = { type: 'group'; group: ResolvedGroup };
|
|
2101
2822
|
type TaskRow = { type: 'task'; task: ResolvedTask };
|
|
2102
|
-
type LaneHeaderRow = {
|
|
2823
|
+
type LaneHeaderRow = {
|
|
2824
|
+
type: 'lane-header';
|
|
2825
|
+
laneName: string;
|
|
2826
|
+
laneColor: string;
|
|
2827
|
+
aggregateProgress: number | null;
|
|
2828
|
+
tagKey: string;
|
|
2829
|
+
isCollapsed: boolean;
|
|
2830
|
+
laneStartDate: Date | null;
|
|
2831
|
+
laneEndDate: Date | null;
|
|
2832
|
+
};
|
|
2103
2833
|
type Row = GroupRow | TaskRow | LaneHeaderRow;
|
|
2104
2834
|
|
|
2105
2835
|
// Public type aliases (prefixed to avoid collisions in consumer code)
|
|
2106
|
-
export type {
|
|
2836
|
+
export type {
|
|
2837
|
+
GroupRow as GanttGroupRow,
|
|
2838
|
+
TaskRow as GanttTaskRow,
|
|
2839
|
+
LaneHeaderRow as GanttLaneHeaderRow,
|
|
2840
|
+
Row as GanttRow,
|
|
2841
|
+
};
|
|
2107
2842
|
|
|
2108
|
-
function buildRowList(
|
|
2843
|
+
function buildRowList(
|
|
2844
|
+
resolved: ResolvedSchedule,
|
|
2845
|
+
collapsedGroups?: Set<string>
|
|
2846
|
+
): Row[] {
|
|
2109
2847
|
const rows: Row[] = [];
|
|
2110
2848
|
const groupMap = new Map<string, ResolvedGroup>();
|
|
2111
2849
|
for (const g of resolved.groups) {
|
|
@@ -2139,7 +2877,7 @@ function buildRowList(resolved: ResolvedSchedule, collapsedGroups?: Set<string>)
|
|
|
2139
2877
|
const seenGroups = new Set<string>();
|
|
2140
2878
|
for (const rt of sortedTasks) {
|
|
2141
2879
|
// Check if any group in this task's path is collapsed
|
|
2142
|
-
const isHidden = rt.groupPath.some(g => collapsedGroups?.has(g));
|
|
2880
|
+
const isHidden = rt.groupPath.some((g) => collapsedGroups?.has(g));
|
|
2143
2881
|
if (isHidden) {
|
|
2144
2882
|
// Still insert collapsed group headers if not seen
|
|
2145
2883
|
for (const groupName of rt.groupPath) {
|
|
@@ -2177,10 +2915,10 @@ function buildRowList(resolved: ResolvedSchedule, collapsedGroups?: Set<string>)
|
|
|
2177
2915
|
export function buildTagLaneRowList(
|
|
2178
2916
|
resolved: ResolvedSchedule,
|
|
2179
2917
|
swimlaneGroup: string,
|
|
2180
|
-
collapsedLanes?: Set<string
|
|
2918
|
+
collapsedLanes?: Set<string>
|
|
2181
2919
|
): Row[] | null {
|
|
2182
2920
|
const tagGroup = resolved.tagGroups.find(
|
|
2183
|
-
g => g.name.toLowerCase() === swimlaneGroup.toLowerCase()
|
|
2921
|
+
(g) => g.name.toLowerCase() === swimlaneGroup.toLowerCase()
|
|
2184
2922
|
);
|
|
2185
2923
|
if (!tagGroup) return null;
|
|
2186
2924
|
|
|
@@ -2217,8 +2955,14 @@ export function buildTagLaneRowList(
|
|
|
2217
2955
|
const aggregateProgress = durationWeightedProgress(tasks);
|
|
2218
2956
|
|
|
2219
2957
|
// Compute lane date range from tasks
|
|
2220
|
-
const laneStartDate =
|
|
2221
|
-
|
|
2958
|
+
const laneStartDate =
|
|
2959
|
+
tasks.length > 0
|
|
2960
|
+
? new Date(Math.min(...tasks.map((t) => t.startDate.getTime())))
|
|
2961
|
+
: null;
|
|
2962
|
+
const laneEndDate =
|
|
2963
|
+
tasks.length > 0
|
|
2964
|
+
? new Date(Math.max(...tasks.map((t) => t.endDate.getTime())))
|
|
2965
|
+
: null;
|
|
2222
2966
|
|
|
2223
2967
|
const isCollapsed = collapsedLanes?.has(entry.value) ?? false;
|
|
2224
2968
|
rows.push({
|
|
@@ -2243,8 +2987,14 @@ export function buildTagLaneRowList(
|
|
|
2243
2987
|
unbucketed.sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
|
|
2244
2988
|
const aggregateProgress = durationWeightedProgress(unbucketed);
|
|
2245
2989
|
|
|
2246
|
-
const noLaneStartDate =
|
|
2247
|
-
|
|
2990
|
+
const noLaneStartDate =
|
|
2991
|
+
unbucketed.length > 0
|
|
2992
|
+
? new Date(Math.min(...unbucketed.map((t) => t.startDate.getTime())))
|
|
2993
|
+
: null;
|
|
2994
|
+
const noLaneEndDate =
|
|
2995
|
+
unbucketed.length > 0
|
|
2996
|
+
? new Date(Math.max(...unbucketed.map((t) => t.endDate.getTime())))
|
|
2997
|
+
: null;
|
|
2248
2998
|
|
|
2249
2999
|
const noLaneName = `No ${tagGroup.name}`;
|
|
2250
3000
|
const isCollapsed = collapsedLanes?.has(noLaneName) ?? false;
|
|
@@ -2283,14 +3033,18 @@ function durationWeightedProgress(tasks: ResolvedTask[]): number | null {
|
|
|
2283
3033
|
hasProgress = true;
|
|
2284
3034
|
}
|
|
2285
3035
|
}
|
|
2286
|
-
return hasProgress && totalDuration > 0
|
|
3036
|
+
return hasProgress && totalDuration > 0
|
|
3037
|
+
? totalProgress / totalDuration
|
|
3038
|
+
: null;
|
|
2287
3039
|
}
|
|
2288
3040
|
|
|
2289
3041
|
function dateToFractionalYear(d: Date): number {
|
|
2290
3042
|
const y = d.getFullYear();
|
|
2291
3043
|
const startOfYear = new Date(y, 0, 1);
|
|
2292
3044
|
const endOfYear = new Date(y + 1, 0, 1);
|
|
2293
|
-
const fraction =
|
|
3045
|
+
const fraction =
|
|
3046
|
+
(d.getTime() - startOfYear.getTime()) /
|
|
3047
|
+
(endOfYear.getTime() - startOfYear.getTime());
|
|
2294
3048
|
return y + fraction;
|
|
2295
3049
|
}
|
|
2296
3050
|
|
|
@@ -2301,7 +3055,20 @@ function diamondPoints(cx: number, cy: number, size: number): string {
|
|
|
2301
3055
|
|
|
2302
3056
|
// ── Hover Date Indicators ───────────────────────────────────
|
|
2303
3057
|
|
|
2304
|
-
const MONTH_ABBR = [
|
|
3058
|
+
const MONTH_ABBR = [
|
|
3059
|
+
'Jan',
|
|
3060
|
+
'Feb',
|
|
3061
|
+
'Mar',
|
|
3062
|
+
'Apr',
|
|
3063
|
+
'May',
|
|
3064
|
+
'Jun',
|
|
3065
|
+
'Jul',
|
|
3066
|
+
'Aug',
|
|
3067
|
+
'Sep',
|
|
3068
|
+
'Oct',
|
|
3069
|
+
'Nov',
|
|
3070
|
+
'Dec',
|
|
3071
|
+
];
|
|
2305
3072
|
|
|
2306
3073
|
function formatGanttDate(d: Date): string {
|
|
2307
3074
|
const base = `${MONTH_ABBR[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
|
|
@@ -2318,7 +3085,7 @@ function showGanttDateIndicators(
|
|
|
2318
3085
|
endDate: Date | null,
|
|
2319
3086
|
innerHeight: number,
|
|
2320
3087
|
color: string,
|
|
2321
|
-
options?: { skipStartLine?: boolean }
|
|
3088
|
+
options?: { skipStartLine?: boolean }
|
|
2322
3089
|
): void {
|
|
2323
3090
|
// Fade existing scale ticks and today marker
|
|
2324
3091
|
g.selectAll('.gantt-scale-tick').attr('opacity', 0.05);
|
|
@@ -2326,7 +3093,8 @@ function showGanttDateIndicators(
|
|
|
2326
3093
|
|
|
2327
3094
|
// Wrap all hover indicators in a group that ignores pointer events,
|
|
2328
3095
|
// so they don't steal mouseleave from the element being hovered.
|
|
2329
|
-
const hg = g
|
|
3096
|
+
const hg = g
|
|
3097
|
+
.append('g')
|
|
2330
3098
|
.attr('class', 'gantt-hover-date')
|
|
2331
3099
|
.attr('pointer-events', 'none');
|
|
2332
3100
|
|
|
@@ -2403,12 +3171,14 @@ function showGanttDateIndicators(
|
|
|
2403
3171
|
.attr('opacity', 0.6);
|
|
2404
3172
|
|
|
2405
3173
|
// Reposition start labels to avoid overlap
|
|
2406
|
-
hg.selectAll<SVGTextElement, unknown>('text.gantt-hover-date').each(
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
el.
|
|
3174
|
+
hg.selectAll<SVGTextElement, unknown>('text.gantt-hover-date').each(
|
|
3175
|
+
function () {
|
|
3176
|
+
const el = d3Selection.select(this);
|
|
3177
|
+
if (el.text() === startLabel) {
|
|
3178
|
+
el.attr('x', startLabelX).attr('text-anchor', startAnchor);
|
|
3179
|
+
}
|
|
2410
3180
|
}
|
|
2411
|
-
|
|
3181
|
+
);
|
|
2412
3182
|
|
|
2413
3183
|
// End date — top label
|
|
2414
3184
|
hg.append('text')
|
|
@@ -2435,7 +3205,7 @@ function showGanttDateIndicators(
|
|
|
2435
3205
|
}
|
|
2436
3206
|
|
|
2437
3207
|
function hideGanttDateIndicators(
|
|
2438
|
-
g: d3Selection.Selection<SVGGElement, unknown, null, undefined
|
|
3208
|
+
g: d3Selection.Selection<SVGGElement, unknown, null, undefined>
|
|
2439
3209
|
): void {
|
|
2440
3210
|
g.selectAll('.gantt-hover-date').remove();
|
|
2441
3211
|
// Restore scale tick opacity
|
|
@@ -2453,20 +3223,20 @@ function resolveTaskColor(
|
|
|
2453
3223
|
activeTagGroup: string | null,
|
|
2454
3224
|
resolved: ResolvedSchedule,
|
|
2455
3225
|
seriesColors: string[],
|
|
2456
|
-
palette: PaletteColors
|
|
3226
|
+
palette: PaletteColors
|
|
2457
3227
|
): string {
|
|
2458
3228
|
// Try tag-based coloring first
|
|
2459
3229
|
const tagColor = resolveTagColor(
|
|
2460
3230
|
rt.effectiveMetadata,
|
|
2461
3231
|
resolved.tagGroups,
|
|
2462
|
-
activeTagGroup
|
|
3232
|
+
activeTagGroup
|
|
2463
3233
|
);
|
|
2464
3234
|
if (tagColor && tagColor !== '#999999') return tagColor;
|
|
2465
3235
|
|
|
2466
3236
|
// Fall back to group-based coloring
|
|
2467
3237
|
if (rt.groupPath.length > 0) {
|
|
2468
3238
|
const topGroup = rt.groupPath[0];
|
|
2469
|
-
const groupIdx = resolved.groups.findIndex(g => g.name === topGroup);
|
|
3239
|
+
const groupIdx = resolved.groups.findIndex((g) => g.name === topGroup);
|
|
2470
3240
|
if (groupIdx >= 0) {
|
|
2471
3241
|
const group = resolved.groups[groupIdx];
|
|
2472
3242
|
if (group.color) return group.color;
|
|
@@ -2483,7 +3253,7 @@ function renderTimeScaleHorizontal(
|
|
|
2483
3253
|
scale: d3Scale.ScaleLinear<number, number>,
|
|
2484
3254
|
innerWidth: number,
|
|
2485
3255
|
innerHeight: number,
|
|
2486
|
-
textColor: string
|
|
3256
|
+
textColor: string
|
|
2487
3257
|
): void {
|
|
2488
3258
|
const [domainMin, domainMax] = scale.domain();
|
|
2489
3259
|
const ticks = computeTimeTicks(domainMin, domainMax, scale);
|