gantt-canvas-chart 1.2.0 → 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.2.0
2
+ * gantt-canvas-chart v1.3.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -89,6 +89,8 @@ class GanttChart {
89
89
  mainCanvas;
90
90
  scrollDummy;
91
91
  tooltip;
92
+ scrolling;
93
+ showTooltip;
92
94
  headerCtx;
93
95
  mainCtx;
94
96
  timelineStart;
@@ -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;
@@ -177,10 +189,11 @@ class GanttChart {
177
189
  this.totalHeight = 0;
178
190
  this.taskPositions = /* @__PURE__ */ new Map();
179
191
  this.taskMap = /* @__PURE__ */ new Map();
180
- this.boundHandleMouseMove = this.handleMouseMove.bind(this);
181
- this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
182
- this.boundHandleScroll = this.handleScroll.bind(this);
183
- 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);
184
197
  this.init();
185
198
  }
186
199
  init() {
@@ -190,6 +203,11 @@ class GanttChart {
190
203
  this.setupEvents();
191
204
  this.handleResize();
192
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
+ }
193
211
  buildTaskMap() {
194
212
  this.taskMap.clear();
195
213
  this.data.forEach((row, rowIndex) => {
@@ -197,7 +215,7 @@ class GanttChart {
197
215
  });
198
216
  }
199
217
  setupEvents() {
200
- this.container.addEventListener("scroll", this.boundHandleScroll);
218
+ this.container.addEventListener("scroll", this.handleScroll);
201
219
  this.handleResize = this.handleResize.bind(this);
202
220
  if (window.ResizeObserver) {
203
221
  this.resizeObserver = new ResizeObserver(this.handleResize);
@@ -206,8 +224,8 @@ class GanttChart {
206
224
  }, 100);
207
225
  }
208
226
  if (this.config.showTooltip) {
209
- this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
210
- this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
227
+ this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
228
+ this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
211
229
  }
212
230
  }
213
231
  updateConfig(newConfig) {
@@ -218,6 +236,7 @@ class GanttChart {
218
236
  this.updatePixelsPerDay();
219
237
  this.calculateFullTimeline();
220
238
  }
239
+ this.updateLoadMoreConf();
221
240
  this.updateDimensions();
222
241
  this.render();
223
242
  }
@@ -236,13 +255,13 @@ class GanttChart {
236
255
  if (this.resizeObserver) {
237
256
  this.resizeObserver.disconnect();
238
257
  }
239
- this.container.removeEventListener("scroll", this.boundHandleScroll);
240
- this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
241
- 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);
242
261
  this.container.remove();
243
262
  }
244
263
  calculateFullTimeline() {
245
- const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
264
+ const currentYear = this.today.getFullYear();
246
265
  let minDate = new Date(9999, 0, 1);
247
266
  let maxDate = new Date(1e3, 0, 1);
248
267
  if (this.data.length === 0) {
@@ -306,7 +325,14 @@ class GanttChart {
306
325
  this.updateDimensions();
307
326
  this.render();
308
327
  }
328
+ // Add this method to register the data loading callback
329
+ setOnDataLoadCallback(callback) {
330
+ this.onDataLoad = callback;
331
+ }
309
332
  handleScroll(e) {
333
+ if (this.showTooltip) {
334
+ this.handleMouseLeave();
335
+ }
310
336
  const target = e.target;
311
337
  this.scrollLeft = target.scrollLeft;
312
338
  this.scrollTop = target.scrollTop;
@@ -314,7 +340,134 @@ class GanttChart {
314
340
  detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
315
341
  });
316
342
  this.container.dispatchEvent(event);
317
- 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);
318
471
  }
319
472
  setScrollTop(scrollTop) {
320
473
  if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
@@ -328,10 +481,14 @@ class GanttChart {
328
481
  }
329
482
  updateDimensions() {
330
483
  const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
331
- this.totalWidth = totalDays * this.pixelsPerDay;
332
- this.totalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
333
- this.scrollDummy.style.width = `${this.totalWidth}px`;
334
- 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
+ }
335
492
  }
336
493
  setupCanvas(canvas, width, height) {
337
494
  canvas.width = width * this.devicePixelRatio;
@@ -349,13 +506,13 @@ class GanttChart {
349
506
  const y = i * this.config.rowHeight;
350
507
  row.tasks.forEach((task) => {
351
508
  const x_plan_start = this.dateToX(new Date(task.planStart));
352
- 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));
353
510
  let x_actual_start = null, x_actual_end = null;
354
511
  if (task.actualStart) {
355
512
  x_actual_start = this.dateToX(new Date(task.actualStart));
356
513
  }
357
514
  if (task.actualEnd) {
358
- x_actual_end = this.dateToX(DateUtils.addDays(new Date(task.actualEnd), 1));
515
+ x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
359
516
  }
360
517
  this.taskPositions.set(task.id, {
361
518
  x_plan_start,
@@ -381,9 +538,11 @@ class GanttChart {
381
538
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
382
539
  }
383
540
  }
384
- render() {
541
+ render(scrolling = false) {
385
542
  this.updateVirtualRanges();
386
- this.calculateAllTaskPositions();
543
+ if (!scrolling) {
544
+ this.calculateAllTaskPositions();
545
+ }
387
546
  this.renderHeader();
388
547
  this.renderMain();
389
548
  }
@@ -393,82 +552,132 @@ class GanttChart {
393
552
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
394
553
  ctx.save();
395
554
  ctx.translate(-this.scrollLeft, 0);
396
- ctx.fillStyle = "#f9f9f9";
555
+ ctx.fillStyle = this.config.headerBgColor;
397
556
  ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
398
557
  ctx.textBaseline = "middle";
399
558
  ctx.textRendering = "optimizeLegibility";
400
559
  let currentDate = new Date(this.visibleDateRange.start);
401
560
  currentDate = this.getIterationStartDate(currentDate);
402
- let lastUpperText = "";
403
- while (this.dateToX(currentDate) < this.scrollLeft - this.pixelsPerDay * 7) {
561
+ const visibleBlocks = [];
562
+ let calcDate = new Date(currentDate);
563
+ while (calcDate <= this.visibleDateRange.end) {
404
564
  let nextDate;
565
+ let upperText = "";
405
566
  switch (this.config.viewMode) {
406
567
  case "Day":
407
- nextDate = DateUtils.addDays(currentDate, 1);
568
+ upperText = DateUtils.format(calcDate, "yyyy年MM月");
569
+ nextDate = DateUtils.addDays(calcDate, 1);
408
570
  break;
409
571
  case "Week":
410
- nextDate = DateUtils.addDays(currentDate, 7);
572
+ const weekStart = DateUtils.getStartOfWeek(calcDate);
573
+ upperText = DateUtils.format(weekStart, "yyyy年MM月");
574
+ nextDate = DateUtils.addDays(weekStart, 7);
411
575
  break;
412
576
  case "Month":
413
- nextDate = DateUtils.addMonths(currentDate, 1);
577
+ upperText = `${calcDate.getFullYear()}年`;
578
+ nextDate = DateUtils.addMonths(calcDate, 1);
414
579
  break;
415
580
  case "Year":
416
- 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);
417
621
  break;
418
622
  default:
419
- nextDate = DateUtils.addDays(currentDate, 1);
623
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
420
624
  break;
421
625
  }
422
- if (nextDate.getTime() === currentDate.getTime()) {
423
- currentDate = DateUtils.addDays(currentDate, 1);
626
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
627
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
424
628
  } else {
425
- currentDate = nextDate;
629
+ currentDateForLower = nextDate;
426
630
  }
427
631
  }
428
- while (currentDate <= this.visibleDateRange.end) {
429
- const x = this.dateToX(currentDate);
430
- 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;
431
650
  switch (this.config.viewMode) {
432
651
  case "Day":
433
- upperText = DateUtils.format(currentDate, "yyyy年MM月");
434
- lowerText = `${DateUtils.format(currentDate, "d")} ${DateUtils.format(currentDate, "W")}`;
435
- nextDate = DateUtils.addDays(currentDate, 1);
652
+ lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
653
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
436
654
  break;
437
655
  case "Week":
438
- const weekStart = DateUtils.getStartOfWeek(currentDate);
439
- upperText = DateUtils.format(weekStart, "yyyy年MM月");
656
+ const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
440
657
  lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
441
658
  nextDate = DateUtils.addDays(weekStart, 7);
442
659
  break;
443
660
  case "Month":
444
- upperText = `${currentDate.getFullYear()}年`;
445
- lowerText = `${currentDate.getMonth() + 1}月`;
446
- nextDate = DateUtils.addMonths(currentDate, 1);
661
+ lowerText = `${currentDateForLower.getMonth() + 1}月`;
662
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
447
663
  break;
448
664
  case "Year":
449
- if (currentDate.getMonth() === 0 && currentDate.getDate() === 1) {
450
- upperText = `${currentDate.getFullYear()}年`;
665
+ if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
451
666
  lowerText = `上半年`;
452
- nextDate = DateUtils.addMonths(currentDate, 6);
453
- } else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1) {
454
- upperText = `${currentDate.getFullYear()}年`;
667
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
668
+ } else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
455
669
  lowerText = `下半年`;
456
- nextDate = DateUtils.addMonths(currentDate, 6);
670
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
457
671
  } else {
458
- currentDate = DateUtils.addDays(currentDate, 1);
672
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
459
673
  continue;
460
674
  }
461
675
  break;
676
+ default:
677
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
678
+ break;
462
679
  }
463
680
  const unitWidth = this.dateToX(nextDate) - x;
464
- if (upperText !== lastUpperText) {
465
- ctx.fillStyle = "#333";
466
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
467
- ctx.textRendering = "optimizeLegibility";
468
- ctx.textAlign = "left";
469
- ctx.fillText(upperText, x + 5, h * 0.35);
470
- lastUpperText = upperText;
471
- }
472
681
  ctx.fillStyle = "#000412";
473
682
  ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
474
683
  ctx.textAlign = "center";
@@ -478,10 +687,10 @@ class GanttChart {
478
687
  ctx.lineTo(x, h);
479
688
  ctx.strokeStyle = "#e0e0e0";
480
689
  ctx.stroke();
481
- if (nextDate.getTime() === currentDate.getTime()) {
482
- currentDate = DateUtils.addDays(currentDate, 1);
690
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
691
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
483
692
  } else {
484
- currentDate = nextDate;
693
+ currentDateForLower = nextDate;
485
694
  }
486
695
  }
487
696
  ctx.beginPath();
@@ -491,6 +700,22 @@ class GanttChart {
491
700
  ctx.stroke();
492
701
  ctx.restore();
493
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
+ }
494
719
  renderMain() {
495
720
  const ctx = this.mainCtx;
496
721
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -714,8 +939,8 @@ class GanttChart {
714
939
  pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
715
940
  ctx.lineWidth = 4;
716
941
  ctx.beginPath();
717
- ctx.moveTo(pos.x_plan_start, taskY);
718
- 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);
719
944
  ctx.stroke();
720
945
  }
721
946
  ctx.fillStyle = "#000";
@@ -751,6 +976,9 @@ class GanttChart {
751
976
  return styles;
752
977
  }
753
978
  handleMouseMove(e) {
979
+ if (this.scrolling) {
980
+ return;
981
+ }
754
982
  const rect = this.mainCanvas.getBoundingClientRect();
755
983
  const mouseX = e.clientX - rect.left;
756
984
  const mouseY = e.clientY - rect.top;
@@ -763,15 +991,16 @@ class GanttChart {
763
991
  if (this.config.tooltipFormat) {
764
992
  const htmlStr = this.config.tooltipFormat(row, date, this.config);
765
993
  if (!htmlStr) {
766
- return this.handleMouseLeave();
994
+ this.handleMouseLeave();
995
+ return;
767
996
  }
768
997
  this.tooltip.innerHTML = htmlStr;
769
998
  } else {
770
999
  const overlappingTasks = row.tasks.filter((task) => {
771
- 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);
772
1001
  if (date >= pStart && date < pEnd) return true;
773
1002
  if (task.actualStart) {
774
- 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);
775
1004
  if (date >= aStart && date < aEnd) return true;
776
1005
  }
777
1006
  return false;
@@ -785,6 +1014,7 @@ class GanttChart {
785
1014
  this.tooltip.innerHTML = html;
786
1015
  }
787
1016
  this.tooltip.style.display = "block";
1017
+ this.showTooltip = true;
788
1018
  if (this.config.tooltipColor === "white") {
789
1019
  this.tooltip.style.background = "#fff";
790
1020
  this.tooltip.style.color = "#000";
@@ -814,6 +1044,7 @@ class GanttChart {
814
1044
  }
815
1045
  handleMouseLeave() {
816
1046
  this.tooltip.style.display = "none";
1047
+ this.showTooltip = false;
817
1048
  }
818
1049
  /**
819
1050
  * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
@@ -825,17 +1056,29 @@ class GanttChart {
825
1056
  return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
826
1057
  }
827
1058
  /**
828
- * scroll to specified date position, default to minDate
1059
+ * horizontal scroll to specified date position, default to minDate
829
1060
  *
830
1061
  * @param date
831
1062
  */
832
- scrollToStartDate(date) {
1063
+ horizontalScrollTo(date) {
833
1064
  const startDate = date ? date : this.minDate;
834
1065
  if (startDate) {
835
1066
  const xPosition = this.dateToX(startDate);
836
1067
  this.container.scrollTo({ left: xPosition - 80 });
837
1068
  }
838
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
+ }
839
1082
  }
840
1083
  export {
841
1084
  DateUtils,