@diagrammo/dgmo 0.8.3 → 0.8.4

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.
Files changed (112) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +185 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +153 -153
  9. package/dist/editor.cjs +336 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +305 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/index.cjs +3336 -1055
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +3336 -1055
  18. package/dist/index.js.map +1 -1
  19. package/docs/language-reference.md +30 -29
  20. package/gallery/fixtures/arc.dgmo +18 -0
  21. package/gallery/fixtures/area.dgmo +19 -0
  22. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  23. package/gallery/fixtures/bar.dgmo +10 -0
  24. package/gallery/fixtures/c4-full.dgmo +52 -0
  25. package/gallery/fixtures/c4.dgmo +17 -0
  26. package/gallery/fixtures/chord.dgmo +12 -0
  27. package/gallery/fixtures/class-basic.dgmo +14 -0
  28. package/gallery/fixtures/class-full.dgmo +43 -0
  29. package/gallery/fixtures/doughnut.dgmo +8 -0
  30. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  31. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  32. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  33. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  34. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  35. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  36. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  37. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  38. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  39. package/gallery/fixtures/function.dgmo +8 -0
  40. package/gallery/fixtures/funnel.dgmo +7 -0
  41. package/gallery/fixtures/gantt-full.dgmo +49 -0
  42. package/gallery/fixtures/gantt.dgmo +42 -0
  43. package/gallery/fixtures/heatmap.dgmo +8 -0
  44. package/gallery/fixtures/infra-full.dgmo +78 -0
  45. package/gallery/fixtures/infra-overload.dgmo +25 -0
  46. package/gallery/fixtures/infra.dgmo +47 -0
  47. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  48. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  49. package/gallery/fixtures/initiative-status.dgmo +9 -0
  50. package/gallery/fixtures/line.dgmo +19 -0
  51. package/gallery/fixtures/multi-line.dgmo +11 -0
  52. package/gallery/fixtures/org-basic.dgmo +16 -0
  53. package/gallery/fixtures/org-full.dgmo +69 -0
  54. package/gallery/fixtures/org-teams.dgmo +25 -0
  55. package/gallery/fixtures/pie.dgmo +9 -0
  56. package/gallery/fixtures/polar-area.dgmo +8 -0
  57. package/gallery/fixtures/quadrant.dgmo +18 -0
  58. package/gallery/fixtures/radar.dgmo +8 -0
  59. package/gallery/fixtures/sankey.dgmo +31 -0
  60. package/gallery/fixtures/scatter.dgmo +21 -0
  61. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  62. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  63. package/gallery/fixtures/sequence.dgmo +35 -0
  64. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  65. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  66. package/gallery/fixtures/slope.dgmo +8 -0
  67. package/gallery/fixtures/spr-eras.dgmo +62 -0
  68. package/gallery/fixtures/state.dgmo +30 -0
  69. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  70. package/gallery/fixtures/timeline.dgmo +32 -0
  71. package/gallery/fixtures/venn.dgmo +10 -0
  72. package/gallery/fixtures/wordcloud.dgmo +24 -0
  73. package/package.json +51 -2
  74. package/src/c4/layout.ts +372 -90
  75. package/src/c4/parser.ts +100 -55
  76. package/src/chart.ts +91 -28
  77. package/src/class/parser.ts +41 -12
  78. package/src/cli.ts +168 -61
  79. package/src/completion.ts +378 -183
  80. package/src/d3.ts +887 -288
  81. package/src/dgmo-mermaid.ts +16 -13
  82. package/src/dgmo-router.ts +69 -23
  83. package/src/echarts.ts +646 -153
  84. package/src/editor/dgmo.grammar +69 -0
  85. package/src/editor/dgmo.grammar.d.ts +2 -0
  86. package/src/editor/dgmo.grammar.js +18 -0
  87. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  88. package/src/editor/dgmo.grammar.terms.js +35 -0
  89. package/src/editor/highlight.ts +36 -0
  90. package/src/editor/index.ts +28 -0
  91. package/src/editor/keywords.ts +220 -0
  92. package/src/editor/tokens.ts +30 -0
  93. package/src/er/parser.ts +48 -14
  94. package/src/er/renderer.ts +112 -53
  95. package/src/gantt/calculator.ts +91 -29
  96. package/src/gantt/parser.ts +197 -71
  97. package/src/gantt/renderer.ts +1120 -350
  98. package/src/graph/flowchart-parser.ts +46 -25
  99. package/src/graph/state-parser.ts +47 -17
  100. package/src/infra/parser.ts +157 -53
  101. package/src/infra/renderer.ts +723 -271
  102. package/src/initiative-status/parser.ts +138 -44
  103. package/src/kanban/parser.ts +25 -14
  104. package/src/org/layout.ts +111 -44
  105. package/src/org/parser.ts +69 -22
  106. package/src/palettes/index.ts +3 -2
  107. package/src/sequence/parser.ts +193 -61
  108. package/src/sitemap/parser.ts +65 -29
  109. package/src/utils/arrows.ts +2 -22
  110. package/src/utils/duration.ts +39 -21
  111. package/src/utils/legend-constants.ts +0 -2
  112. package/src/utils/parsing.ts +75 -31
@@ -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 { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y } from '../utils/title-constants';
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 { ResolvedSchedule, ResolvedTask, ResolvedGroup, Weekday } from './types';
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; // estimated px per character for bar labels
43
- const LABEL_PAD = 8; // inner padding to decide if label fits inside bar
44
- const LABEL_GAP = 5; // gap between bar edge and external label
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 { x: x1 - LABEL_GAP, anchor: 'end', fill: textColor, text: label.slice(0, maxChars) + '\u2026' };
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.append('clipPath').attr('id', clipId)
123
+ svg
124
+ .append('clipPath')
125
+ .attr('id', clipId)
112
126
  .append('rect')
113
- .attr('x', bandX).attr('y', bandY)
114
- .attr('width', bandW).attr('height', BAR_H)
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.append('rect')
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.append('rect')
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.append('tspan').attr('fill', textColor).text(' ' + label);
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 = options?.currentActiveGroup !== undefined
204
- ? options.currentActiveGroup
205
- : (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
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.filter((r): r is LaneHeaderRow => r.type === 'lane-header').map(r => r.laneName),
220
- ...rows.filter((r): r is TaskRow => r.type === 'task').map(r => '● ' + r.task.task.label),
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 = resolved.tagGroups.length > 0 ? LEGEND_HEIGHT + 8 : 0;
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 = resolved.markers.length > 0 || resolved.eras.length > 0;
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 = titleHeight + tagLegendReserve + topDateLabelReserve + markerLabelReserve;
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 = resolved.options.criticalPath && resolved.tasks.some(t => t.isCriticalPath);
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, g, resolved.tagGroups, currentActiveGroup, leftMargin, innerWidth,
294
- legendY, palette, isDark, hasCriticalPath, criticalPathActive, resolved.options.optionLineNumbers,
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 = currentActiveGroup?.toLowerCase() === groupName.toLowerCase()
298
- ? null : groupName;
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(rt, currentActiveGroup, resolved, seriesColors, palette);
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(g, svg, resolved, xScale, innerHeight, palette, isDark, marginTop - 4, leftMargin, onClickItem);
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.append('line')
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) todayLine.attr('data-line-number', String(todayMarkerLineNum));
443
+ if (todayMarkerLineNum)
444
+ todayLine.attr('data-line-number', String(todayMarkerLineNum));
381
445
 
382
- const todayLabel = g.append('text')
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) todayLabel.attr('data-line-number', String(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<string, { x1: number; x2: number; y: number }>();
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<string, { x1: number; x2: number; y: number }>();
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<string, { x1: number; x2: number; y: number }>();
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(e => e.value.toLowerCase() === value!.toLowerCase());
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 = row.laneColor === '#999999' ? palette.textMuted : row.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, { x1: lx1, x2: lx1 + laneBarWidth, y: yOffset + BAR_H / 2 });
519
+ lanePositions.set(row.laneName, {
520
+ x1: lx1,
521
+ x2: lx1 + laneBarWidth,
522
+ y: yOffset + BAR_H / 2,
523
+ });
442
524
 
443
- renderLabelBand(svg, marginTop + yOffset + BAR_H / 2, leftMargin, laneColor, palette, 'lane', { key: 'data-lane', value: row.laneName });
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(g, xScale, row.laneStartDate, row.laneEndDate, innerHeight, laneColor);
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(toggleIcon + ' ' + row.laneName + (row.aggregateProgress !== null ? ` ${Math.round(row.aggregateProgress)}%` : ''));
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.append('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(g, xScale, row.laneStartDate, row.laneEndDate, innerHeight, laneColor);
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.append('rect')
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.append('rect')
618
+ laneBandG
619
+ .append('rect')
506
620
  .attr('class', 'gantt-lane-progress')
507
621
  .attr('x', lx1)
508
622
  .attr('y', yOffset)
509
- .attr('width', laneBarWidth * Math.min(row.aggregateProgress / 100, 1))
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(group.metadata, resolved.tagGroups, currentActiveGroup, true);
526
- const groupColor = (tagColor && tagColor !== '#999999') ? tagColor : (group.color || palette.textMuted);
527
- renderLabelBand(svg, marginTop + yOffset + BAR_H / 2, leftMargin, groupColor, palette, 'group', { key: 'data-group', value: group.name });
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(g, xScale, group.startDate, group.endDate, innerHeight, groupColor);
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 = group.depth <= 2 ? group.depth * 14 : 2 * 14 + (group.depth - 2) * 8;
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(toggleIcon + ' ' + group.name + (group.progress !== null ? ` ${Math.round(group.progress)}%` : ''));
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.append('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(g, xScale, group.startDate, group.endDate, innerHeight, groupColor);
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.append('rect')
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.append('rect')
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 = group.name + (group.progress !== null ? ` ${Math.round(group.progress)}%` : '');
605
- const summaryPlacement = computeBarLabel(summaryLabel, gx1, barWidth, innerWidth, palette.text);
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, { x1: gx1, x2: gx1 + barWidth, y: yOffset + BAR_H / 2 });
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.append('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(g, xScale, group.startDate, group.endDate, innerHeight, groupColor);
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.append('rect')
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.append('rect')
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 = group.name + (group.progress !== null ? ` ${Math.round(group.progress)}%` : '');
663
- const expandedPlacement = computeBarLabel(expandedLabel, gx1, groupBarWidth, innerWidth, palette.text);
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(rt, currentActiveGroup, resolved, seriesColors, palette);
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(taskLabel, task.label, rt.isMilestone, barColor, palette.text);
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(g, xScale, rt.startDate, null, innerHeight, barColor);
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.append('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(g, xScale, rt.startDate, rt.endDate, innerHeight, barColor);
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 = rt.isUncertain && (task.progress === null || task.progress <= 80);
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.append('linearGradient')
1038
+ const fillGrad = defs
1039
+ .append('linearGradient')
826
1040
  .attr('id', fillGradId)
827
- .attr('x1', '0').attr('x2', '1').attr('y1', '0').attr('y2', '0');
828
- fillGrad.append('stop').attr('offset', '0%').attr('stop-color', fillColor).attr('stop-opacity', 1);
829
- fillGrad.append('stop').attr('offset', '50%').attr('stop-color', fillColor).attr('stop-opacity', 1);
830
- fillGrad.append('stop').attr('offset', '100%').attr('stop-color', fillColor).attr('stop-opacity', 0);
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.append('linearGradient')
1062
+ const strokeGrad = defs
1063
+ .append('linearGradient')
834
1064
  .attr('id', strokeGradId)
835
- .attr('x1', '0').attr('x2', '1').attr('y1', '0').attr('y2', '0');
836
- strokeGrad.append('stop').attr('offset', '0%').attr('stop-color', barColor).attr('stop-opacity', 1);
837
- strokeGrad.append('stop').attr('offset', '50%').attr('stop-color', barColor).attr('stop-opacity', 1);
838
- strokeGrad.append('stop').attr('offset', '100%').attr('stop-color', barColor).attr('stop-opacity', 0);
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.append('linearGradient')
1111
+ const progGrad = defs
1112
+ .append('linearGradient')
867
1113
  .attr('id', progGradId)
868
- .attr('x1', '0').attr('x2', '1').attr('y1', '0').attr('y2', '0');
869
- progGrad.append('stop').attr('offset', '0%').attr('stop-color', barColor).attr('stop-opacity', 1);
870
- progGrad.append('stop').attr('offset', `${fadeStart}%`).attr('stop-color', barColor).attr('stop-opacity', 1);
871
- progGrad.append('stop').attr('offset', '100%').attr('stop-color', barColor).attr('stop-opacity', 0);
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(task.label, x1, barWidth, innerWidth, palette.text);
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, { x1, x2: x1 + barWidth, y: yOffset + BAR_H / 2 });
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.append('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.append('rect')
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('opacity', FADE_OPACITY);
935
- g.selectAll<SVGElement, unknown>('.gantt-milestone').attr('opacity', FADE_OPACITY);
936
- g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
937
- svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
938
- svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', FADE_OPACITY);
939
- svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
940
- g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
941
- g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
942
- g.selectAll<SVGElement, unknown>('.gantt-era-group').attr('opacity', FADE_OPACITY);
943
- g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', FADE_OPACITY);
944
- showGanttDateIndicators(g, xScale, todayDateObj, null, innerHeight, todayColor);
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(g, resolved, taskPositions, groupPositions, collapsedGroups, palette, isDark, isTagMode, lanePositions, collapsedLanes, taskLaneMap);
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[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
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(g, xScale, bandStart, current, innerHeight, palette, isDark, 'gantt-weekend-band', 0.04);
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(g, xScale, bandStart, current, innerHeight, palette, isDark, 'gantt-weekend-band', 0.04);
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(g, svg, xScale, start, end, innerHeight, palette, isDark, h.label, h.lineNumber, headerY, chartLeftMargin, onClickItem);
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(g, svg, xScale, start, end, innerHeight, palette, isDark, r.label, r.lineNumber, headerY, chartLeftMargin, onClickItem);
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.append('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.append('rect')
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.append('rect')
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.append('text')
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 = taskPositions.get(rt.task.id)
1189
- ?? (isTagMode
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(t => t.task.label === dep.targetName ||
1197
- `${t.groupPath.join('.')}.${t.task.label}`.endsWith(dep.targetName));
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 = taskPositions.get(targetTask.task.id)
1201
- ?? (isTagMode
1202
- ? findCollapsedLanePos(targetTask, collapsedLanes, taskLaneMap, lanePositions)
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.append('text')
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(x: number, y: number, size: number, angle: number): string {
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('opacity', el.attr('data-critical-path') === 'true' ? 1 : FADE_OPACITY);
1695
+ el.attr(
1696
+ 'opacity',
1697
+ el.attr('data-critical-path') === 'true' ? 1 : FADE_OPACITY
1698
+ );
1305
1699
  });
1306
- chartG.selectAll<SVGElement, unknown>('.gantt-milestone').attr('opacity', FADE_OPACITY);
1307
- chartG.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
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('opacity', el.attr('data-critical-path') === 'true' ? 1 : FADE_OPACITY);
1708
+ el.attr(
1709
+ 'opacity',
1710
+ el.attr('data-critical-path') === 'true' ? 1 : FADE_OPACITY
1711
+ );
1311
1712
  });
1312
- svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
1313
- svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', FADE_OPACITY);
1314
- svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
1315
- svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', FADE_OPACITY);
1316
- chartG.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr('opacity', FADE_OPACITY);
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.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').each(function () {
1319
- const el = d3Selection.select(this);
1320
- el.attr('opacity', el.attr('data-critical-path') === 'true' ? 0.7 : FADE_OPACITY);
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.selectAll<SVGGElement, unknown>('.gantt-task, .gantt-milestone').attr('opacity', 1);
1329
- chartG.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', 1);
1330
- svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', 1);
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.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', 1);
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.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', 1);
1335
- chartG.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr('opacity', 1);
1336
- chartG.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', 0.5);
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.append('g')
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.append('rect')
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(g => g.name.toLowerCase() === activeGroupName.toLowerCase());
1400
- const swimlaneGroup = currentSwimlaneGroup && currentSwimlaneGroup.toLowerCase() !== activeGroupName.toLowerCase()
1401
- ? tagGroups.filter(g => g.name.toLowerCase() === currentSwimlaneGroup.toLowerCase())
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(key, group.entries.filter(e => used.has(e.value.toLowerCase())));
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 = activeGroupName?.toLowerCase() === group.name.toLowerCase();
1439
- const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
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 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
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 = filteredEntries.get(group.name.toLowerCase()) ?? group.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 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
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 = measureLegendText(cpLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
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.append('g')
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 = activeGroupName?.toLowerCase() === group.name.toLowerCase();
1481
- const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
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 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
1485
- const pillH = isActive ? LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2 : LEGEND_HEIGHT;
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.append('g')
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', () => { if (onToggle) onToggle(group.name); });
1967
+ .on('click', () => {
1968
+ if (onToggle) onToggle(group.name);
1969
+ });
1495
1970
 
1496
1971
  if (isActive) {
1497
1972
  // Outer capsule background
1498
- gEl.append('rect')
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.append('rect')
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.append('rect')
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 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
1532
- gEl.append('text')
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
- .style('cursor', 'pointer')
1549
- .on('click', (event: Event) => {
1550
- event.stopPropagation();
1551
- if (onSwimlaneChange) {
1552
- onSwimlaneChange(
1553
- currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase()
1554
- ? null : group.name
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.append('g')
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.append('circle')
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.append('text')
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.selectAll<SVGGElement, unknown>('.gantt-task').each(function () {
1594
- const el = d3Selection.select(this);
1595
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
1596
- el.attr('opacity', matches ? 1 : FADE_OPACITY);
1597
- });
1598
- chartG.selectAll<SVGElement, unknown>('.gantt-milestone').attr('opacity', FADE_OPACITY);
1599
- chartG.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
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.selectAll<SVGTextElement, unknown>('.gantt-task-label').each(function () {
1602
- const el = d3Selection.select(this);
1603
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
1604
- el.attr('opacity', matches ? 1 : FADE_OPACITY);
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.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
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.selectAll<SVGGElement, unknown>('.gantt-lane-header').each(function () {
1610
- const el = d3Selection.select(this);
1611
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
1612
- el.attr('opacity', matches ? 1 : FADE_OPACITY);
1613
- });
1614
- chartG.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr('opacity', FADE_OPACITY);
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 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
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.append('g')
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', () => { if (onToggleCriticalPath) onToggleCriticalPath(); });
2145
+ .on('click', () => {
2146
+ if (onToggleCriticalPath) onToggleCriticalPath();
2147
+ });
1639
2148
  if (cpLineNum) cpG.attr('data-line-number', String(cpLineNum));
1640
2149
 
1641
- cpG.append('rect')
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.append('rect')
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.append('text')
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.append('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.append('rect')
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.append('text')
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('opacity', FADE_OPACITY);
1737
- g.selectAll<SVGElement, unknown>('.gantt-milestone').attr('opacity', FADE_OPACITY);
1738
- g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
1739
- svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
1740
- svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', FADE_OPACITY);
1741
- svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
1742
- g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
1743
- g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
1744
- g.selectAll<SVGElement, unknown>('.gantt-marker-group').attr('opacity', FADE_OPACITY);
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(g, xScale, eraStartDate, eraEndDate, innerHeight, color);
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.append('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.append('rect')
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.append('text')
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.append('path')
1792
- .attr('d', `M${mx},${diamondY - diamondSize} l${diamondSize},${diamondSize} l-${diamondSize},${diamondSize} l-${diamondSize},-${diamondSize} Z`)
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.append('line')
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('opacity', FADE_OPACITY);
1815
- g.selectAll<SVGElement, unknown>('.gantt-milestone').attr('opacity', FADE_OPACITY);
1816
- g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
1817
- svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
1818
- svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', FADE_OPACITY);
1819
- svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
1820
- g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
1821
- g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
1822
- g.selectAll<SVGElement, unknown>('.gantt-era-group').attr('opacity', FADE_OPACITY);
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('opacity', FADE_OPACITY);
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(g, xScale, markerDate, null, innerHeight, color, { skipStartLine: true });
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 (dep.targetName === task.task.label ||
1879
- `${task.groupPath.join('.')}.${task.task.label}`.endsWith(dep.targetName)) {
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(t =>
1887
- t.task.label === dep.targetName ||
1888
- `${t.groupPath.join('.')}.${t.task.label}`.endsWith(dep.targetName));
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('opacity', FADE_OPACITY);
1899
- g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
1900
- svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
1901
- svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
1902
- g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr('opacity', FADE_OPACITY);
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>('.gantt-dep-arrow, .gantt-dep-arrowhead').each(function () {
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('opacity', FADE_OPACITY);
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>('.gantt-group-bar, .gantt-group-summary').each(function () {
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.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').each(function () {
1948
- const el = d3Selection.select(this);
1949
- el.attr('opacity', el.attr('data-group') === groupName ? 1 : FADE_OPACITY);
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.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
1953
- svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', FADE_OPACITY);
1954
- g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent').attr('opacity', FADE_OPACITY);
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('opacity', FADE_OPACITY);
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.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').each(function () {
1995
- const el = d3Selection.select(this);
1996
- el.attr('opacity', el.attr('data-lane') === laneName ? 1 : FADE_OPACITY);
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>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
2000
- svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
2001
- svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', FADE_OPACITY);
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('opacity', FADE_OPACITY);
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('opacity', FADE_OPACITY);
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>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
2025
- svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
2026
- svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', FADE_OPACITY);
2027
- svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
2028
- svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', FADE_OPACITY);
2029
- g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
2030
- g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
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('opacity', FADE_OPACITY);
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('opacity', FADE_OPACITY);
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>('.gantt-group-bar, .gantt-group-summary').attr('opacity', FADE_OPACITY);
2054
- svg.selectAll<SVGGElement, unknown>('.gantt-group-label').attr('opacity', FADE_OPACITY);
2055
- svg.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', FADE_OPACITY);
2056
- svg.selectAll<SVGGElement, unknown>('.gantt-lane-header').attr('opacity', FADE_OPACITY);
2057
- svg.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', FADE_OPACITY);
2058
- g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', FADE_OPACITY);
2059
- g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', FADE_OPACITY);
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('opacity', FADE_OPACITY);
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.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', 1);
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('opacity', 1);
2086
- g.selectAll<SVGElement, unknown>('.gantt-group-bar, .gantt-group-summary').attr('opacity', 1);
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.selectAll<SVGElement, unknown>('.gantt-group-band-bg, .gantt-group-band-accent').attr('opacity', 1);
2089
- svg.selectAll<SVGTextElement, unknown>('.gantt-task-label').attr('opacity', 1);
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.selectAll<SVGElement, unknown>('.gantt-lane-band-bg, .gantt-lane-band-accent').attr('opacity', 1);
2092
- g.selectAll<SVGElement, unknown>('.gantt-lane-band, .gantt-lane-accent, .gantt-lane-band-group').attr('opacity', 1);
2093
- g.selectAll<SVGElement, unknown>('.gantt-dep-arrow, .gantt-dep-arrowhead').attr('opacity', 0.5);
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 = { type: 'lane-header'; laneName: string; laneColor: string; aggregateProgress: number | null; tagKey: string; isCollapsed: boolean; laneStartDate: Date | null; laneEndDate: Date | null };
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 { GroupRow as GanttGroupRow, TaskRow as GanttTaskRow, LaneHeaderRow as GanttLaneHeaderRow, Row as GanttRow };
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(resolved: ResolvedSchedule, collapsedGroups?: Set<string>): Row[] {
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 = tasks.length > 0 ? new Date(Math.min(...tasks.map(t => t.startDate.getTime()))) : null;
2221
- const laneEndDate = tasks.length > 0 ? new Date(Math.max(...tasks.map(t => t.endDate.getTime()))) : null;
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 = unbucketed.length > 0 ? new Date(Math.min(...unbucketed.map(t => t.startDate.getTime()))) : null;
2247
- const noLaneEndDate = unbucketed.length > 0 ? new Date(Math.max(...unbucketed.map(t => t.endDate.getTime()))) : null;
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 ? totalProgress / totalDuration : null;
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 = (d.getTime() - startOfYear.getTime()) / (endOfYear.getTime() - startOfYear.getTime());
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 = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
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.append('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(function () {
2407
- const el = d3Selection.select(this);
2408
- if (el.text() === startLabel) {
2409
- el.attr('x', startLabelX).attr('text-anchor', startAnchor);
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);