gantt-canvas-chart 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.1.1
2
+ * gantt-canvas-chart v1.3.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -89,12 +89,14 @@ class GanttChart {
89
89
  mainCanvas;
90
90
  scrollDummy;
91
91
  tooltip;
92
+ scrolling;
93
+ showTooltip;
92
94
  headerCtx;
93
95
  mainCtx;
94
96
  timelineStart;
95
97
  timelineEnd;
96
98
  minDate;
97
- // private maxDate: Date | null;
99
+ maxDate;
98
100
  pixelsPerDay;
99
101
  scrollLeft;
100
102
  scrollTop;
@@ -108,9 +110,14 @@ class GanttChart {
108
110
  resizeObserver;
109
111
  taskPositions;
110
112
  taskMap;
111
- boundHandleMouseMove;
112
- boundHandleMouseLeave;
113
- boundHandleScroll;
113
+ isLoadingData = false;
114
+ hasMoreDataLeft = true;
115
+ hasMoreDataRight = true;
116
+ hasMoreDataBottom = true;
117
+ lastScrollLeft = 0;
118
+ lastScrollTop = 0;
119
+ onDataLoad = null;
120
+ scrollLoadTimer = null;
114
121
  constructor(rootContainer, data, config = {}) {
115
122
  const container = document.createElement("div");
116
123
  const scrollEl = document.createElement("div");
@@ -129,6 +136,8 @@ class GanttChart {
129
136
  this.rootContainer = rootContainer;
130
137
  this.container = container;
131
138
  this.data = data;
139
+ this.scrolling = false;
140
+ this.showTooltip = false;
132
141
  this.config = {
133
142
  viewMode: "Month",
134
143
  rowHeight: 48,
@@ -146,11 +155,14 @@ class GanttChart {
146
155
  todayColor: "#ff4d4f",
147
156
  offsetTop: 0,
148
157
  offsetLeft: 0,
158
+ enabledLoadMore: [],
149
159
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
150
160
  planBorderColor: "#C1EFCF",
151
161
  actualBgColor: "#5AC989",
162
+ headerBgColor: "#f9f9f9",
152
163
  ...config
153
164
  };
165
+ this.updateLoadMoreConf();
154
166
  this.headerCanvas = headerCanvas;
155
167
  this.mainCanvas = mainCanvas;
156
168
  this.scrollDummy = scrollEl;
@@ -164,6 +176,7 @@ class GanttChart {
164
176
  this.timelineStart = /* @__PURE__ */ new Date();
165
177
  this.timelineEnd = /* @__PURE__ */ new Date();
166
178
  this.minDate = null;
179
+ this.maxDate = null;
167
180
  this.pixelsPerDay = 40;
168
181
  this.scrollLeft = 0;
169
182
  this.scrollTop = 0;
@@ -176,10 +189,11 @@ class GanttChart {
176
189
  this.totalHeight = 0;
177
190
  this.taskPositions = /* @__PURE__ */ new Map();
178
191
  this.taskMap = /* @__PURE__ */ new Map();
179
- this.boundHandleMouseMove = this.handleMouseMove.bind(this);
180
- this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
181
- this.boundHandleScroll = this.handleScroll.bind(this);
182
- this.scrollToStartDate = this.scrollToStartDate.bind(this);
192
+ this.handleMouseMove = this.handleMouseMove.bind(this);
193
+ this.handleMouseLeave = this.handleMouseLeave.bind(this);
194
+ this.handleScroll = this.handleScroll.bind(this);
195
+ this.horizontalScrollTo = this.horizontalScrollTo.bind(this);
196
+ this.verticalScrollTo = this.verticalScrollTo.bind(this);
183
197
  this.init();
184
198
  }
185
199
  init() {
@@ -189,6 +203,11 @@ class GanttChart {
189
203
  this.setupEvents();
190
204
  this.handleResize();
191
205
  }
206
+ updateLoadMoreConf() {
207
+ this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
208
+ this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
209
+ this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
210
+ }
192
211
  buildTaskMap() {
193
212
  this.taskMap.clear();
194
213
  this.data.forEach((row, rowIndex) => {
@@ -196,7 +215,7 @@ class GanttChart {
196
215
  });
197
216
  }
198
217
  setupEvents() {
199
- this.container.addEventListener("scroll", this.boundHandleScroll);
218
+ this.container.addEventListener("scroll", this.handleScroll);
200
219
  this.handleResize = this.handleResize.bind(this);
201
220
  if (window.ResizeObserver) {
202
221
  this.resizeObserver = new ResizeObserver(this.handleResize);
@@ -205,8 +224,8 @@ class GanttChart {
205
224
  }, 100);
206
225
  }
207
226
  if (this.config.showTooltip) {
208
- this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
209
- this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
227
+ this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
228
+ this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
210
229
  }
211
230
  }
212
231
  updateConfig(newConfig) {
@@ -217,6 +236,7 @@ class GanttChart {
217
236
  this.updatePixelsPerDay();
218
237
  this.calculateFullTimeline();
219
238
  }
239
+ this.updateLoadMoreConf();
220
240
  this.updateDimensions();
221
241
  this.render();
222
242
  }
@@ -235,17 +255,18 @@ class GanttChart {
235
255
  if (this.resizeObserver) {
236
256
  this.resizeObserver.disconnect();
237
257
  }
238
- this.container.removeEventListener("scroll", this.boundHandleScroll);
239
- this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
240
- this.mainCanvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
258
+ this.container.removeEventListener("scroll", this.handleScroll);
259
+ this.mainCanvas.removeEventListener("mousemove", this.handleMouseMove);
260
+ this.mainCanvas.removeEventListener("mouseleave", this.handleMouseLeave);
241
261
  this.container.remove();
242
262
  }
243
263
  calculateFullTimeline() {
264
+ const currentYear = this.today.getFullYear();
244
265
  let minDate = new Date(9999, 0, 1);
245
266
  let maxDate = new Date(1e3, 0, 1);
246
267
  if (this.data.length === 0) {
247
268
  minDate = /* @__PURE__ */ new Date();
248
- maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(), 30);
269
+ maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(), 60);
249
270
  } else {
250
271
  this.taskMap.forEach(({ task }) => {
251
272
  const pStart = new Date(task.planStart);
@@ -261,8 +282,11 @@ class GanttChart {
261
282
  });
262
283
  }
263
284
  this.minDate = minDate;
264
- minDate = DateUtils.addDays(minDate, -7);
265
- maxDate = DateUtils.addDays(maxDate, 14);
285
+ this.maxDate = maxDate;
286
+ const minYear = minDate.getFullYear();
287
+ const maxYear = maxDate.getFullYear();
288
+ minDate = DateUtils.addDays(minYear === 9999 ? new Date(currentYear, 0, 1) : minDate, -7);
289
+ maxDate = DateUtils.addDays(maxYear === 1e3 ? new Date(currentYear + 1, 0, 1) : maxDate, 14);
266
290
  switch (this.config.viewMode) {
267
291
  case "Year":
268
292
  this.timelineStart = DateUtils.getStartOfYear(minDate);
@@ -301,7 +325,14 @@ class GanttChart {
301
325
  this.updateDimensions();
302
326
  this.render();
303
327
  }
328
+ // Add this method to register the data loading callback
329
+ setOnDataLoadCallback(callback) {
330
+ this.onDataLoad = callback;
331
+ }
304
332
  handleScroll(e) {
333
+ if (this.showTooltip) {
334
+ this.handleMouseLeave();
335
+ }
305
336
  const target = e.target;
306
337
  this.scrollLeft = target.scrollLeft;
307
338
  this.scrollTop = target.scrollTop;
@@ -309,7 +340,134 @@ class GanttChart {
309
340
  detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
310
341
  });
311
342
  this.container.dispatchEvent(event);
312
- requestAnimationFrame(() => this.render());
343
+ if (this.config.enabledLoadMore.length > 0) {
344
+ if (this.scrollLoadTimer !== null) {
345
+ clearTimeout(this.scrollLoadTimer);
346
+ this.scrollLoadTimer = null;
347
+ }
348
+ this.scrollLoadTimer = window.setTimeout(() => {
349
+ this.checkScrollLoad();
350
+ this.scrollLoadTimer = null;
351
+ }, 100);
352
+ }
353
+ requestAnimationFrame(() => {
354
+ this.scrolling = true;
355
+ this.render(true);
356
+ this.scrolling = false;
357
+ });
358
+ }
359
+ // checkScrollLoad method
360
+ async checkScrollLoad() {
361
+ if (this.isLoadingData || !this.onDataLoad) {
362
+ return;
363
+ }
364
+ const scrollLeft = this.scrollLeft;
365
+ const scrollTop = this.scrollTop;
366
+ const viewportWidth = this.viewportWidth;
367
+ const viewportHeight = this.viewportHeight;
368
+ const totalWidth = this.totalWidth;
369
+ const totalHeight = this.totalHeight;
370
+ const atLeftEdge = scrollLeft <= 5;
371
+ const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
372
+ const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
373
+ try {
374
+ if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
375
+ await this.loadMoreData("left");
376
+ console.log("left-loadMoreData::", this.data);
377
+ } else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
378
+ await this.loadMoreData("right");
379
+ console.log("right-loadMoreData::", this.data);
380
+ } else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
381
+ await this.loadMoreData("bottom");
382
+ console.log("bottom-loadMoreData::", this.data);
383
+ }
384
+ } finally {
385
+ this.lastScrollLeft = scrollLeft;
386
+ this.lastScrollTop = scrollTop;
387
+ }
388
+ }
389
+ // Add this method to reset scroll loading state
390
+ resetScrollLoadingState() {
391
+ this.hasMoreDataLeft = true;
392
+ this.hasMoreDataRight = true;
393
+ this.hasMoreDataBottom = true;
394
+ this.lastScrollLeft = 0;
395
+ this.lastScrollTop = 0;
396
+ if (this.scrollLoadTimer !== null) {
397
+ clearTimeout(this.scrollLoadTimer);
398
+ this.scrollLoadTimer = null;
399
+ }
400
+ }
401
+ // Add this new method to load additional data
402
+ async loadMoreData(direction) {
403
+ if (this.isLoadingData || !this.onDataLoad) {
404
+ return;
405
+ }
406
+ this.isLoadingData = true;
407
+ try {
408
+ let newData = null;
409
+ switch (direction) {
410
+ case "left":
411
+ newData = await this.onDataLoad("left", { date: this.minDate });
412
+ if (newData && newData.length > 0) {
413
+ this.prependData(newData);
414
+ } else {
415
+ this.hasMoreDataLeft = false;
416
+ }
417
+ break;
418
+ case "right":
419
+ newData = await this.onDataLoad("right", { date: this.maxDate });
420
+ if (newData && newData.length > 0) {
421
+ this.appendData(newData);
422
+ } else {
423
+ this.hasMoreDataRight = false;
424
+ }
425
+ break;
426
+ case "bottom":
427
+ const currentRowCount = this.data.length;
428
+ newData = await this.onDataLoad("bottom", { offset: currentRowCount });
429
+ if (newData && newData.length > 0) {
430
+ this.appendRows(newData);
431
+ } else {
432
+ this.hasMoreDataBottom = false;
433
+ }
434
+ break;
435
+ }
436
+ if (newData && newData.length > 0) {
437
+ this.buildTaskMap();
438
+ this.calculateFullTimeline();
439
+ this.updateDimensions();
440
+ this.render();
441
+ }
442
+ } catch (error) {
443
+ console.error("Error loading additional data:", error);
444
+ } finally {
445
+ this.isLoadingData = false;
446
+ }
447
+ }
448
+ // Add this method to append data to the right
449
+ appendData(newData) {
450
+ newData.forEach((newRow, index) => {
451
+ if (index < this.data.length) {
452
+ this.data[index].tasks = [...this.data[index].tasks, ...newRow.tasks];
453
+ } else {
454
+ this.data.push(newRow);
455
+ }
456
+ });
457
+ }
458
+ // Add this method to prepend data to the left
459
+ prependData(newData) {
460
+ newData.forEach((newRow, index) => {
461
+ if (index < this.data.length) {
462
+ this.data[index].tasks = [...newRow.tasks, ...this.data[index].tasks];
463
+ } else {
464
+ this.data.push(newRow);
465
+ }
466
+ });
467
+ }
468
+ // Add this method to append rows to the bottom
469
+ appendRows(newData) {
470
+ this.data.push(...newData);
313
471
  }
314
472
  setScrollTop(scrollTop) {
315
473
  if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
@@ -323,10 +481,14 @@ class GanttChart {
323
481
  }
324
482
  updateDimensions() {
325
483
  const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
326
- this.totalWidth = totalDays * this.pixelsPerDay;
327
- this.totalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
328
- this.scrollDummy.style.width = `${this.totalWidth}px`;
329
- this.scrollDummy.style.height = `${this.totalHeight}px`;
484
+ const newTotalWidth = totalDays * this.pixelsPerDay;
485
+ const newTotalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
486
+ if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
487
+ this.totalWidth = newTotalWidth;
488
+ this.totalHeight = newTotalHeight;
489
+ this.scrollDummy.style.width = `${this.totalWidth}px`;
490
+ this.scrollDummy.style.height = `${this.totalHeight}px`;
491
+ }
330
492
  }
331
493
  setupCanvas(canvas, width, height) {
332
494
  canvas.width = width * this.devicePixelRatio;
@@ -344,13 +506,13 @@ class GanttChart {
344
506
  const y = i * this.config.rowHeight;
345
507
  row.tasks.forEach((task) => {
346
508
  const x_plan_start = this.dateToX(new Date(task.planStart));
347
- const x_plan_end = this.dateToX(DateUtils.addDays(new Date(task.planEnd), 1));
509
+ const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
348
510
  let x_actual_start = null, x_actual_end = null;
349
511
  if (task.actualStart) {
350
512
  x_actual_start = this.dateToX(new Date(task.actualStart));
351
513
  }
352
514
  if (task.actualEnd) {
353
- x_actual_end = this.dateToX(DateUtils.addDays(new Date(task.actualEnd), 1));
515
+ x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
354
516
  }
355
517
  this.taskPositions.set(task.id, {
356
518
  x_plan_start,
@@ -376,9 +538,11 @@ class GanttChart {
376
538
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
377
539
  }
378
540
  }
379
- render() {
541
+ render(scrolling = false) {
380
542
  this.updateVirtualRanges();
381
- this.calculateAllTaskPositions();
543
+ if (!scrolling) {
544
+ this.calculateAllTaskPositions();
545
+ }
382
546
  this.renderHeader();
383
547
  this.renderMain();
384
548
  }
@@ -388,82 +552,132 @@ class GanttChart {
388
552
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
389
553
  ctx.save();
390
554
  ctx.translate(-this.scrollLeft, 0);
391
- ctx.fillStyle = "#f9f9f9";
555
+ ctx.fillStyle = this.config.headerBgColor;
392
556
  ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
393
557
  ctx.textBaseline = "middle";
394
558
  ctx.textRendering = "optimizeLegibility";
395
559
  let currentDate = new Date(this.visibleDateRange.start);
396
560
  currentDate = this.getIterationStartDate(currentDate);
397
- let lastUpperText = "";
398
- while (this.dateToX(currentDate) < this.scrollLeft - this.pixelsPerDay * 7) {
561
+ const visibleBlocks = [];
562
+ let calcDate = new Date(currentDate);
563
+ while (calcDate <= this.visibleDateRange.end) {
399
564
  let nextDate;
565
+ let upperText = "";
400
566
  switch (this.config.viewMode) {
401
567
  case "Day":
402
- nextDate = DateUtils.addDays(currentDate, 1);
568
+ upperText = DateUtils.format(calcDate, "yyyy年MM月");
569
+ nextDate = DateUtils.addDays(calcDate, 1);
403
570
  break;
404
571
  case "Week":
405
- nextDate = DateUtils.addDays(currentDate, 7);
572
+ const weekStart = DateUtils.getStartOfWeek(calcDate);
573
+ upperText = DateUtils.format(weekStart, "yyyy年MM月");
574
+ nextDate = DateUtils.addDays(weekStart, 7);
406
575
  break;
407
576
  case "Month":
408
- nextDate = DateUtils.addMonths(currentDate, 1);
577
+ upperText = `${calcDate.getFullYear()}年`;
578
+ nextDate = DateUtils.addMonths(calcDate, 1);
409
579
  break;
410
580
  case "Year":
411
- nextDate = DateUtils.addMonths(currentDate, 6);
581
+ if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
582
+ upperText = `${calcDate.getFullYear()}年`;
583
+ nextDate = DateUtils.addMonths(calcDate, 6);
584
+ } else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
585
+ upperText = `${calcDate.getFullYear()}年`;
586
+ nextDate = DateUtils.addMonths(calcDate, 6);
587
+ } else {
588
+ calcDate = DateUtils.addDays(calcDate, 1);
589
+ continue;
590
+ }
591
+ break;
592
+ default:
593
+ nextDate = DateUtils.addDays(calcDate, 1);
594
+ break;
595
+ }
596
+ const startX = this.dateToX(calcDate);
597
+ const endX = this.dateToX(nextDate);
598
+ visibleBlocks.push({
599
+ startX,
600
+ endX,
601
+ text: upperText,
602
+ yPos: h * 0.35
603
+ });
604
+ calcDate = nextDate;
605
+ }
606
+ let currentDateForLower = new Date(currentDate);
607
+ while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
608
+ let nextDate;
609
+ switch (this.config.viewMode) {
610
+ case "Day":
611
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
612
+ break;
613
+ case "Week":
614
+ nextDate = DateUtils.addDays(currentDateForLower, 7);
615
+ break;
616
+ case "Month":
617
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
618
+ break;
619
+ case "Year":
620
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
412
621
  break;
413
622
  default:
414
- nextDate = DateUtils.addDays(currentDate, 1);
623
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
415
624
  break;
416
625
  }
417
- if (nextDate.getTime() === currentDate.getTime()) {
418
- currentDate = DateUtils.addDays(currentDate, 1);
626
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
627
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
419
628
  } else {
420
- currentDate = nextDate;
629
+ currentDateForLower = nextDate;
421
630
  }
422
631
  }
423
- while (currentDate <= this.visibleDateRange.end) {
424
- const x = this.dateToX(currentDate);
425
- let upperText = "", lowerText = "", nextDate;
632
+ const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
633
+ ctx.fillStyle = "#333";
634
+ ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
635
+ ctx.textAlign = "left";
636
+ groupedBlocks.forEach((group) => {
637
+ const visibleStart = Math.max(group.startX, this.scrollLeft);
638
+ const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
639
+ if (visibleEnd > visibleStart) {
640
+ ctx.fillStyle = this.config.headerBgColor;
641
+ ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
642
+ ctx.fillStyle = "#333";
643
+ ctx.fillText(group.text, visibleStart + 5, group.yPos);
644
+ }
645
+ });
646
+ while (currentDateForLower <= this.visibleDateRange.end) {
647
+ const x = this.dateToX(currentDateForLower);
648
+ let lowerText = "";
649
+ let nextDate;
426
650
  switch (this.config.viewMode) {
427
651
  case "Day":
428
- upperText = DateUtils.format(currentDate, "yyyy年MM月");
429
- lowerText = `${DateUtils.format(currentDate, "d")} ${DateUtils.format(currentDate, "W")}`;
430
- nextDate = DateUtils.addDays(currentDate, 1);
652
+ lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
653
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
431
654
  break;
432
655
  case "Week":
433
- const weekStart = DateUtils.getStartOfWeek(currentDate);
434
- upperText = DateUtils.format(weekStart, "yyyy年MM月");
656
+ const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
435
657
  lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
436
658
  nextDate = DateUtils.addDays(weekStart, 7);
437
659
  break;
438
660
  case "Month":
439
- upperText = `${currentDate.getFullYear()}年`;
440
- lowerText = `${currentDate.getMonth() + 1}月`;
441
- nextDate = DateUtils.addMonths(currentDate, 1);
661
+ lowerText = `${currentDateForLower.getMonth() + 1}月`;
662
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
442
663
  break;
443
664
  case "Year":
444
- if (currentDate.getMonth() === 0 && currentDate.getDate() === 1) {
445
- upperText = `${currentDate.getFullYear()}年`;
665
+ if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
446
666
  lowerText = `上半年`;
447
- nextDate = DateUtils.addMonths(currentDate, 6);
448
- } else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1) {
449
- upperText = `${currentDate.getFullYear()}年`;
667
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
668
+ } else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
450
669
  lowerText = `下半年`;
451
- nextDate = DateUtils.addMonths(currentDate, 6);
670
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
452
671
  } else {
453
- currentDate = DateUtils.addDays(currentDate, 1);
672
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
454
673
  continue;
455
674
  }
456
675
  break;
676
+ default:
677
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
678
+ break;
457
679
  }
458
680
  const unitWidth = this.dateToX(nextDate) - x;
459
- if (upperText !== lastUpperText) {
460
- ctx.fillStyle = "#333";
461
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
462
- ctx.textRendering = "optimizeLegibility";
463
- ctx.textAlign = "left";
464
- ctx.fillText(upperText, x + 5, h * 0.35);
465
- lastUpperText = upperText;
466
- }
467
681
  ctx.fillStyle = "#000412";
468
682
  ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
469
683
  ctx.textAlign = "center";
@@ -473,10 +687,10 @@ class GanttChart {
473
687
  ctx.lineTo(x, h);
474
688
  ctx.strokeStyle = "#e0e0e0";
475
689
  ctx.stroke();
476
- if (nextDate.getTime() === currentDate.getTime()) {
477
- currentDate = DateUtils.addDays(currentDate, 1);
690
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
691
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
478
692
  } else {
479
- currentDate = nextDate;
693
+ currentDateForLower = nextDate;
480
694
  }
481
695
  }
482
696
  ctx.beginPath();
@@ -486,6 +700,22 @@ class GanttChart {
486
700
  ctx.stroke();
487
701
  ctx.restore();
488
702
  }
703
+ // Helper method to group consecutive blocks with same text
704
+ groupConsecutiveBlocks(blocks) {
705
+ if (blocks.length === 0) return [];
706
+ const grouped = [];
707
+ let currentGroup = { ...blocks[0] };
708
+ for (let i = 1; i < blocks.length; i++) {
709
+ if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
710
+ currentGroup.endX = blocks[i].endX;
711
+ } else {
712
+ grouped.push(currentGroup);
713
+ currentGroup = { ...blocks[i] };
714
+ }
715
+ }
716
+ grouped.push(currentGroup);
717
+ return grouped;
718
+ }
489
719
  renderMain() {
490
720
  const ctx = this.mainCtx;
491
721
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -681,7 +911,7 @@ class GanttChart {
681
911
  const x = this.dateToX(this.today);
682
912
  if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
683
913
  ctx.strokeStyle = this.config.todayColor;
684
- ctx.lineWidth = 2;
914
+ ctx.lineWidth = 1;
685
915
  ctx.beginPath();
686
916
  ctx.moveTo(x, this.scrollTop);
687
917
  ctx.lineTo(x, this.scrollTop + this.viewportHeight);
@@ -709,8 +939,8 @@ class GanttChart {
709
939
  pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
710
940
  ctx.lineWidth = 4;
711
941
  ctx.beginPath();
712
- ctx.moveTo(pos.x_plan_start, taskY);
713
- ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
942
+ ctx.moveTo(pos.x_plan_start + 2, taskY);
943
+ ctx.lineTo(pos.x_plan_start + width * percent_plan - 2, taskY);
714
944
  ctx.stroke();
715
945
  }
716
946
  ctx.fillStyle = "#000";
@@ -746,6 +976,9 @@ class GanttChart {
746
976
  return styles;
747
977
  }
748
978
  handleMouseMove(e) {
979
+ if (this.scrolling) {
980
+ return;
981
+ }
749
982
  const rect = this.mainCanvas.getBoundingClientRect();
750
983
  const mouseX = e.clientX - rect.left;
751
984
  const mouseY = e.clientY - rect.top;
@@ -758,15 +991,16 @@ class GanttChart {
758
991
  if (this.config.tooltipFormat) {
759
992
  const htmlStr = this.config.tooltipFormat(row, date, this.config);
760
993
  if (!htmlStr) {
761
- return this.handleMouseLeave();
994
+ this.handleMouseLeave();
995
+ return;
762
996
  }
763
997
  this.tooltip.innerHTML = htmlStr;
764
998
  } else {
765
999
  const overlappingTasks = row.tasks.filter((task) => {
766
- const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
1000
+ const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
767
1001
  if (date >= pStart && date < pEnd) return true;
768
1002
  if (task.actualStart) {
769
- const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
1003
+ const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
770
1004
  if (date >= aStart && date < aEnd) return true;
771
1005
  }
772
1006
  return false;
@@ -780,6 +1014,7 @@ class GanttChart {
780
1014
  this.tooltip.innerHTML = html;
781
1015
  }
782
1016
  this.tooltip.style.display = "block";
1017
+ this.showTooltip = true;
783
1018
  if (this.config.tooltipColor === "white") {
784
1019
  this.tooltip.style.background = "#fff";
785
1020
  this.tooltip.style.color = "#000";
@@ -809,6 +1044,7 @@ class GanttChart {
809
1044
  }
810
1045
  handleMouseLeave() {
811
1046
  this.tooltip.style.display = "none";
1047
+ this.showTooltip = false;
812
1048
  }
813
1049
  /**
814
1050
  * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
@@ -820,17 +1056,29 @@ class GanttChart {
820
1056
  return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
821
1057
  }
822
1058
  /**
823
- * scroll to specified date position, default to minDate
1059
+ * horizontal scroll to specified date position, default to minDate
824
1060
  *
825
1061
  * @param date
826
1062
  */
827
- scrollToStartDate(date) {
1063
+ horizontalScrollTo(date) {
828
1064
  const startDate = date ? date : this.minDate;
829
1065
  if (startDate) {
830
1066
  const xPosition = this.dateToX(startDate);
831
1067
  this.container.scrollTo({ left: xPosition - 80 });
832
1068
  }
833
1069
  }
1070
+ /**
1071
+ * vertical scroll to specified position
1072
+ *
1073
+ * @param params
1074
+ */
1075
+ verticalScrollTo(params) {
1076
+ if (params && (params.rowId || params.rowIndex)) {
1077
+ const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
1078
+ const yPosition = this.config.rowHeight * rowIndex;
1079
+ this.container.scrollTo({ top: yPosition - 80 });
1080
+ }
1081
+ }
834
1082
  }
835
1083
  export {
836
1084
  DateUtils,