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.cjs.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
  */
@@ -91,6 +91,8 @@ class GanttChart {
91
91
  mainCanvas;
92
92
  scrollDummy;
93
93
  tooltip;
94
+ scrolling;
95
+ showTooltip;
94
96
  headerCtx;
95
97
  mainCtx;
96
98
  timelineStart;
@@ -110,9 +112,14 @@ class GanttChart {
110
112
  resizeObserver;
111
113
  taskPositions;
112
114
  taskMap;
113
- boundHandleMouseMove;
114
- boundHandleMouseLeave;
115
- boundHandleScroll;
115
+ isLoadingData = false;
116
+ hasMoreDataLeft = true;
117
+ hasMoreDataRight = true;
118
+ hasMoreDataBottom = true;
119
+ lastScrollLeft = 0;
120
+ lastScrollTop = 0;
121
+ onDataLoad = null;
122
+ scrollLoadTimer = null;
116
123
  constructor(rootContainer, data, config = {}) {
117
124
  const container = document.createElement("div");
118
125
  const scrollEl = document.createElement("div");
@@ -131,6 +138,8 @@ class GanttChart {
131
138
  this.rootContainer = rootContainer;
132
139
  this.container = container;
133
140
  this.data = data;
141
+ this.scrolling = false;
142
+ this.showTooltip = false;
134
143
  this.config = {
135
144
  viewMode: "Month",
136
145
  rowHeight: 48,
@@ -148,11 +157,14 @@ class GanttChart {
148
157
  todayColor: "#ff4d4f",
149
158
  offsetTop: 0,
150
159
  offsetLeft: 0,
160
+ enabledLoadMore: [],
151
161
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
152
162
  planBorderColor: "#C1EFCF",
153
163
  actualBgColor: "#5AC989",
164
+ headerBgColor: "#f9f9f9",
154
165
  ...config
155
166
  };
167
+ this.updateLoadMoreConf();
156
168
  this.headerCanvas = headerCanvas;
157
169
  this.mainCanvas = mainCanvas;
158
170
  this.scrollDummy = scrollEl;
@@ -179,10 +191,11 @@ class GanttChart {
179
191
  this.totalHeight = 0;
180
192
  this.taskPositions = /* @__PURE__ */ new Map();
181
193
  this.taskMap = /* @__PURE__ */ new Map();
182
- this.boundHandleMouseMove = this.handleMouseMove.bind(this);
183
- this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
184
- this.boundHandleScroll = this.handleScroll.bind(this);
185
- this.scrollToStartDate = this.scrollToStartDate.bind(this);
194
+ this.handleMouseMove = this.handleMouseMove.bind(this);
195
+ this.handleMouseLeave = this.handleMouseLeave.bind(this);
196
+ this.handleScroll = this.handleScroll.bind(this);
197
+ this.horizontalScrollTo = this.horizontalScrollTo.bind(this);
198
+ this.verticalScrollTo = this.verticalScrollTo.bind(this);
186
199
  this.init();
187
200
  }
188
201
  init() {
@@ -192,6 +205,11 @@ class GanttChart {
192
205
  this.setupEvents();
193
206
  this.handleResize();
194
207
  }
208
+ updateLoadMoreConf() {
209
+ this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
210
+ this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
211
+ this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
212
+ }
195
213
  buildTaskMap() {
196
214
  this.taskMap.clear();
197
215
  this.data.forEach((row, rowIndex) => {
@@ -199,7 +217,7 @@ class GanttChart {
199
217
  });
200
218
  }
201
219
  setupEvents() {
202
- this.container.addEventListener("scroll", this.boundHandleScroll);
220
+ this.container.addEventListener("scroll", this.handleScroll);
203
221
  this.handleResize = this.handleResize.bind(this);
204
222
  if (window.ResizeObserver) {
205
223
  this.resizeObserver = new ResizeObserver(this.handleResize);
@@ -208,8 +226,8 @@ class GanttChart {
208
226
  }, 100);
209
227
  }
210
228
  if (this.config.showTooltip) {
211
- this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
212
- this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
229
+ this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
230
+ this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
213
231
  }
214
232
  }
215
233
  updateConfig(newConfig) {
@@ -220,6 +238,7 @@ class GanttChart {
220
238
  this.updatePixelsPerDay();
221
239
  this.calculateFullTimeline();
222
240
  }
241
+ this.updateLoadMoreConf();
223
242
  this.updateDimensions();
224
243
  this.render();
225
244
  }
@@ -238,13 +257,13 @@ class GanttChart {
238
257
  if (this.resizeObserver) {
239
258
  this.resizeObserver.disconnect();
240
259
  }
241
- this.container.removeEventListener("scroll", this.boundHandleScroll);
242
- this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
243
- this.mainCanvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
260
+ this.container.removeEventListener("scroll", this.handleScroll);
261
+ this.mainCanvas.removeEventListener("mousemove", this.handleMouseMove);
262
+ this.mainCanvas.removeEventListener("mouseleave", this.handleMouseLeave);
244
263
  this.container.remove();
245
264
  }
246
265
  calculateFullTimeline() {
247
- const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
266
+ const currentYear = this.today.getFullYear();
248
267
  let minDate = new Date(9999, 0, 1);
249
268
  let maxDate = new Date(1e3, 0, 1);
250
269
  if (this.data.length === 0) {
@@ -308,7 +327,14 @@ class GanttChart {
308
327
  this.updateDimensions();
309
328
  this.render();
310
329
  }
330
+ // Add this method to register the data loading callback
331
+ setOnDataLoadCallback(callback) {
332
+ this.onDataLoad = callback;
333
+ }
311
334
  handleScroll(e) {
335
+ if (this.showTooltip) {
336
+ this.handleMouseLeave();
337
+ }
312
338
  const target = e.target;
313
339
  this.scrollLeft = target.scrollLeft;
314
340
  this.scrollTop = target.scrollTop;
@@ -316,7 +342,134 @@ class GanttChart {
316
342
  detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
317
343
  });
318
344
  this.container.dispatchEvent(event);
319
- requestAnimationFrame(() => this.render());
345
+ if (this.config.enabledLoadMore.length > 0) {
346
+ if (this.scrollLoadTimer !== null) {
347
+ clearTimeout(this.scrollLoadTimer);
348
+ this.scrollLoadTimer = null;
349
+ }
350
+ this.scrollLoadTimer = window.setTimeout(() => {
351
+ this.checkScrollLoad();
352
+ this.scrollLoadTimer = null;
353
+ }, 100);
354
+ }
355
+ requestAnimationFrame(() => {
356
+ this.scrolling = true;
357
+ this.render(true);
358
+ this.scrolling = false;
359
+ });
360
+ }
361
+ // checkScrollLoad method
362
+ async checkScrollLoad() {
363
+ if (this.isLoadingData || !this.onDataLoad) {
364
+ return;
365
+ }
366
+ const scrollLeft = this.scrollLeft;
367
+ const scrollTop = this.scrollTop;
368
+ const viewportWidth = this.viewportWidth;
369
+ const viewportHeight = this.viewportHeight;
370
+ const totalWidth = this.totalWidth;
371
+ const totalHeight = this.totalHeight;
372
+ const atLeftEdge = scrollLeft <= 5;
373
+ const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
374
+ const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
375
+ try {
376
+ if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
377
+ await this.loadMoreData("left");
378
+ console.log("left-loadMoreData::", this.data);
379
+ } else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
380
+ await this.loadMoreData("right");
381
+ console.log("right-loadMoreData::", this.data);
382
+ } else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
383
+ await this.loadMoreData("bottom");
384
+ console.log("bottom-loadMoreData::", this.data);
385
+ }
386
+ } finally {
387
+ this.lastScrollLeft = scrollLeft;
388
+ this.lastScrollTop = scrollTop;
389
+ }
390
+ }
391
+ // Add this method to reset scroll loading state
392
+ resetScrollLoadingState() {
393
+ this.hasMoreDataLeft = true;
394
+ this.hasMoreDataRight = true;
395
+ this.hasMoreDataBottom = true;
396
+ this.lastScrollLeft = 0;
397
+ this.lastScrollTop = 0;
398
+ if (this.scrollLoadTimer !== null) {
399
+ clearTimeout(this.scrollLoadTimer);
400
+ this.scrollLoadTimer = null;
401
+ }
402
+ }
403
+ // Add this new method to load additional data
404
+ async loadMoreData(direction) {
405
+ if (this.isLoadingData || !this.onDataLoad) {
406
+ return;
407
+ }
408
+ this.isLoadingData = true;
409
+ try {
410
+ let newData = null;
411
+ switch (direction) {
412
+ case "left":
413
+ newData = await this.onDataLoad("left", { date: this.minDate });
414
+ if (newData && newData.length > 0) {
415
+ this.prependData(newData);
416
+ } else {
417
+ this.hasMoreDataLeft = false;
418
+ }
419
+ break;
420
+ case "right":
421
+ newData = await this.onDataLoad("right", { date: this.maxDate });
422
+ if (newData && newData.length > 0) {
423
+ this.appendData(newData);
424
+ } else {
425
+ this.hasMoreDataRight = false;
426
+ }
427
+ break;
428
+ case "bottom":
429
+ const currentRowCount = this.data.length;
430
+ newData = await this.onDataLoad("bottom", { offset: currentRowCount });
431
+ if (newData && newData.length > 0) {
432
+ this.appendRows(newData);
433
+ } else {
434
+ this.hasMoreDataBottom = false;
435
+ }
436
+ break;
437
+ }
438
+ if (newData && newData.length > 0) {
439
+ this.buildTaskMap();
440
+ this.calculateFullTimeline();
441
+ this.updateDimensions();
442
+ this.render();
443
+ }
444
+ } catch (error) {
445
+ console.error("Error loading additional data:", error);
446
+ } finally {
447
+ this.isLoadingData = false;
448
+ }
449
+ }
450
+ // Add this method to append data to the right
451
+ appendData(newData) {
452
+ newData.forEach((newRow, index) => {
453
+ if (index < this.data.length) {
454
+ this.data[index].tasks = [...this.data[index].tasks, ...newRow.tasks];
455
+ } else {
456
+ this.data.push(newRow);
457
+ }
458
+ });
459
+ }
460
+ // Add this method to prepend data to the left
461
+ prependData(newData) {
462
+ newData.forEach((newRow, index) => {
463
+ if (index < this.data.length) {
464
+ this.data[index].tasks = [...newRow.tasks, ...this.data[index].tasks];
465
+ } else {
466
+ this.data.push(newRow);
467
+ }
468
+ });
469
+ }
470
+ // Add this method to append rows to the bottom
471
+ appendRows(newData) {
472
+ this.data.push(...newData);
320
473
  }
321
474
  setScrollTop(scrollTop) {
322
475
  if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
@@ -330,10 +483,14 @@ class GanttChart {
330
483
  }
331
484
  updateDimensions() {
332
485
  const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
333
- this.totalWidth = totalDays * this.pixelsPerDay;
334
- this.totalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
335
- this.scrollDummy.style.width = `${this.totalWidth}px`;
336
- this.scrollDummy.style.height = `${this.totalHeight}px`;
486
+ const newTotalWidth = totalDays * this.pixelsPerDay;
487
+ const newTotalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
488
+ if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
489
+ this.totalWidth = newTotalWidth;
490
+ this.totalHeight = newTotalHeight;
491
+ this.scrollDummy.style.width = `${this.totalWidth}px`;
492
+ this.scrollDummy.style.height = `${this.totalHeight}px`;
493
+ }
337
494
  }
338
495
  setupCanvas(canvas, width, height) {
339
496
  canvas.width = width * this.devicePixelRatio;
@@ -351,13 +508,13 @@ class GanttChart {
351
508
  const y = i * this.config.rowHeight;
352
509
  row.tasks.forEach((task) => {
353
510
  const x_plan_start = this.dateToX(new Date(task.planStart));
354
- const x_plan_end = this.dateToX(DateUtils.addDays(new Date(task.planEnd), 1));
511
+ const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
355
512
  let x_actual_start = null, x_actual_end = null;
356
513
  if (task.actualStart) {
357
514
  x_actual_start = this.dateToX(new Date(task.actualStart));
358
515
  }
359
516
  if (task.actualEnd) {
360
- x_actual_end = this.dateToX(DateUtils.addDays(new Date(task.actualEnd), 1));
517
+ x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
361
518
  }
362
519
  this.taskPositions.set(task.id, {
363
520
  x_plan_start,
@@ -383,9 +540,11 @@ class GanttChart {
383
540
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
384
541
  }
385
542
  }
386
- render() {
543
+ render(scrolling = false) {
387
544
  this.updateVirtualRanges();
388
- this.calculateAllTaskPositions();
545
+ if (!scrolling) {
546
+ this.calculateAllTaskPositions();
547
+ }
389
548
  this.renderHeader();
390
549
  this.renderMain();
391
550
  }
@@ -395,82 +554,132 @@ class GanttChart {
395
554
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
396
555
  ctx.save();
397
556
  ctx.translate(-this.scrollLeft, 0);
398
- ctx.fillStyle = "#f9f9f9";
557
+ ctx.fillStyle = this.config.headerBgColor;
399
558
  ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
400
559
  ctx.textBaseline = "middle";
401
560
  ctx.textRendering = "optimizeLegibility";
402
561
  let currentDate = new Date(this.visibleDateRange.start);
403
562
  currentDate = this.getIterationStartDate(currentDate);
404
- let lastUpperText = "";
405
- while (this.dateToX(currentDate) < this.scrollLeft - this.pixelsPerDay * 7) {
563
+ const visibleBlocks = [];
564
+ let calcDate = new Date(currentDate);
565
+ while (calcDate <= this.visibleDateRange.end) {
406
566
  let nextDate;
567
+ let upperText = "";
407
568
  switch (this.config.viewMode) {
408
569
  case "Day":
409
- nextDate = DateUtils.addDays(currentDate, 1);
570
+ upperText = DateUtils.format(calcDate, "yyyy年MM月");
571
+ nextDate = DateUtils.addDays(calcDate, 1);
410
572
  break;
411
573
  case "Week":
412
- nextDate = DateUtils.addDays(currentDate, 7);
574
+ const weekStart = DateUtils.getStartOfWeek(calcDate);
575
+ upperText = DateUtils.format(weekStart, "yyyy年MM月");
576
+ nextDate = DateUtils.addDays(weekStart, 7);
413
577
  break;
414
578
  case "Month":
415
- nextDate = DateUtils.addMonths(currentDate, 1);
579
+ upperText = `${calcDate.getFullYear()}年`;
580
+ nextDate = DateUtils.addMonths(calcDate, 1);
416
581
  break;
417
582
  case "Year":
418
- nextDate = DateUtils.addMonths(currentDate, 6);
583
+ if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
584
+ upperText = `${calcDate.getFullYear()}年`;
585
+ nextDate = DateUtils.addMonths(calcDate, 6);
586
+ } else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
587
+ upperText = `${calcDate.getFullYear()}年`;
588
+ nextDate = DateUtils.addMonths(calcDate, 6);
589
+ } else {
590
+ calcDate = DateUtils.addDays(calcDate, 1);
591
+ continue;
592
+ }
593
+ break;
594
+ default:
595
+ nextDate = DateUtils.addDays(calcDate, 1);
596
+ break;
597
+ }
598
+ const startX = this.dateToX(calcDate);
599
+ const endX = this.dateToX(nextDate);
600
+ visibleBlocks.push({
601
+ startX,
602
+ endX,
603
+ text: upperText,
604
+ yPos: h * 0.35
605
+ });
606
+ calcDate = nextDate;
607
+ }
608
+ let currentDateForLower = new Date(currentDate);
609
+ while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
610
+ let nextDate;
611
+ switch (this.config.viewMode) {
612
+ case "Day":
613
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
614
+ break;
615
+ case "Week":
616
+ nextDate = DateUtils.addDays(currentDateForLower, 7);
617
+ break;
618
+ case "Month":
619
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
620
+ break;
621
+ case "Year":
622
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
419
623
  break;
420
624
  default:
421
- nextDate = DateUtils.addDays(currentDate, 1);
625
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
422
626
  break;
423
627
  }
424
- if (nextDate.getTime() === currentDate.getTime()) {
425
- currentDate = DateUtils.addDays(currentDate, 1);
628
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
629
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
426
630
  } else {
427
- currentDate = nextDate;
631
+ currentDateForLower = nextDate;
428
632
  }
429
633
  }
430
- while (currentDate <= this.visibleDateRange.end) {
431
- const x = this.dateToX(currentDate);
432
- let upperText = "", lowerText = "", nextDate;
634
+ const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
635
+ ctx.fillStyle = "#333";
636
+ ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
637
+ ctx.textAlign = "left";
638
+ groupedBlocks.forEach((group) => {
639
+ const visibleStart = Math.max(group.startX, this.scrollLeft);
640
+ const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
641
+ if (visibleEnd > visibleStart) {
642
+ ctx.fillStyle = this.config.headerBgColor;
643
+ ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
644
+ ctx.fillStyle = "#333";
645
+ ctx.fillText(group.text, visibleStart + 5, group.yPos);
646
+ }
647
+ });
648
+ while (currentDateForLower <= this.visibleDateRange.end) {
649
+ const x = this.dateToX(currentDateForLower);
650
+ let lowerText = "";
651
+ let nextDate;
433
652
  switch (this.config.viewMode) {
434
653
  case "Day":
435
- upperText = DateUtils.format(currentDate, "yyyy年MM月");
436
- lowerText = `${DateUtils.format(currentDate, "d")} ${DateUtils.format(currentDate, "W")}`;
437
- nextDate = DateUtils.addDays(currentDate, 1);
654
+ lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
655
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
438
656
  break;
439
657
  case "Week":
440
- const weekStart = DateUtils.getStartOfWeek(currentDate);
441
- upperText = DateUtils.format(weekStart, "yyyy年MM月");
658
+ const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
442
659
  lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
443
660
  nextDate = DateUtils.addDays(weekStart, 7);
444
661
  break;
445
662
  case "Month":
446
- upperText = `${currentDate.getFullYear()}年`;
447
- lowerText = `${currentDate.getMonth() + 1}月`;
448
- nextDate = DateUtils.addMonths(currentDate, 1);
663
+ lowerText = `${currentDateForLower.getMonth() + 1}月`;
664
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
449
665
  break;
450
666
  case "Year":
451
- if (currentDate.getMonth() === 0 && currentDate.getDate() === 1) {
452
- upperText = `${currentDate.getFullYear()}年`;
667
+ if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
453
668
  lowerText = `上半年`;
454
- nextDate = DateUtils.addMonths(currentDate, 6);
455
- } else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1) {
456
- upperText = `${currentDate.getFullYear()}年`;
669
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
670
+ } else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
457
671
  lowerText = `下半年`;
458
- nextDate = DateUtils.addMonths(currentDate, 6);
672
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
459
673
  } else {
460
- currentDate = DateUtils.addDays(currentDate, 1);
674
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
461
675
  continue;
462
676
  }
463
677
  break;
678
+ default:
679
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
680
+ break;
464
681
  }
465
682
  const unitWidth = this.dateToX(nextDate) - x;
466
- if (upperText !== lastUpperText) {
467
- ctx.fillStyle = "#333";
468
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
469
- ctx.textRendering = "optimizeLegibility";
470
- ctx.textAlign = "left";
471
- ctx.fillText(upperText, x + 5, h * 0.35);
472
- lastUpperText = upperText;
473
- }
474
683
  ctx.fillStyle = "#000412";
475
684
  ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
476
685
  ctx.textAlign = "center";
@@ -480,10 +689,10 @@ class GanttChart {
480
689
  ctx.lineTo(x, h);
481
690
  ctx.strokeStyle = "#e0e0e0";
482
691
  ctx.stroke();
483
- if (nextDate.getTime() === currentDate.getTime()) {
484
- currentDate = DateUtils.addDays(currentDate, 1);
692
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
693
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
485
694
  } else {
486
- currentDate = nextDate;
695
+ currentDateForLower = nextDate;
487
696
  }
488
697
  }
489
698
  ctx.beginPath();
@@ -493,6 +702,22 @@ class GanttChart {
493
702
  ctx.stroke();
494
703
  ctx.restore();
495
704
  }
705
+ // Helper method to group consecutive blocks with same text
706
+ groupConsecutiveBlocks(blocks) {
707
+ if (blocks.length === 0) return [];
708
+ const grouped = [];
709
+ let currentGroup = { ...blocks[0] };
710
+ for (let i = 1; i < blocks.length; i++) {
711
+ if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
712
+ currentGroup.endX = blocks[i].endX;
713
+ } else {
714
+ grouped.push(currentGroup);
715
+ currentGroup = { ...blocks[i] };
716
+ }
717
+ }
718
+ grouped.push(currentGroup);
719
+ return grouped;
720
+ }
496
721
  renderMain() {
497
722
  const ctx = this.mainCtx;
498
723
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -716,8 +941,8 @@ class GanttChart {
716
941
  pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
717
942
  ctx.lineWidth = 4;
718
943
  ctx.beginPath();
719
- ctx.moveTo(pos.x_plan_start, taskY);
720
- ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
944
+ ctx.moveTo(pos.x_plan_start + 2, taskY);
945
+ ctx.lineTo(pos.x_plan_start + width * percent_plan - 2, taskY);
721
946
  ctx.stroke();
722
947
  }
723
948
  ctx.fillStyle = "#000";
@@ -753,6 +978,9 @@ class GanttChart {
753
978
  return styles;
754
979
  }
755
980
  handleMouseMove(e) {
981
+ if (this.scrolling) {
982
+ return;
983
+ }
756
984
  const rect = this.mainCanvas.getBoundingClientRect();
757
985
  const mouseX = e.clientX - rect.left;
758
986
  const mouseY = e.clientY - rect.top;
@@ -765,15 +993,16 @@ class GanttChart {
765
993
  if (this.config.tooltipFormat) {
766
994
  const htmlStr = this.config.tooltipFormat(row, date, this.config);
767
995
  if (!htmlStr) {
768
- return this.handleMouseLeave();
996
+ this.handleMouseLeave();
997
+ return;
769
998
  }
770
999
  this.tooltip.innerHTML = htmlStr;
771
1000
  } else {
772
1001
  const overlappingTasks = row.tasks.filter((task) => {
773
- const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
1002
+ const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
774
1003
  if (date >= pStart && date < pEnd) return true;
775
1004
  if (task.actualStart) {
776
- const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
1005
+ const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
777
1006
  if (date >= aStart && date < aEnd) return true;
778
1007
  }
779
1008
  return false;
@@ -787,6 +1016,7 @@ class GanttChart {
787
1016
  this.tooltip.innerHTML = html;
788
1017
  }
789
1018
  this.tooltip.style.display = "block";
1019
+ this.showTooltip = true;
790
1020
  if (this.config.tooltipColor === "white") {
791
1021
  this.tooltip.style.background = "#fff";
792
1022
  this.tooltip.style.color = "#000";
@@ -816,6 +1046,7 @@ class GanttChart {
816
1046
  }
817
1047
  handleMouseLeave() {
818
1048
  this.tooltip.style.display = "none";
1049
+ this.showTooltip = false;
819
1050
  }
820
1051
  /**
821
1052
  * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
@@ -827,17 +1058,29 @@ class GanttChart {
827
1058
  return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
828
1059
  }
829
1060
  /**
830
- * scroll to specified date position, default to minDate
1061
+ * horizontal scroll to specified date position, default to minDate
831
1062
  *
832
1063
  * @param date
833
1064
  */
834
- scrollToStartDate(date) {
1065
+ horizontalScrollTo(date) {
835
1066
  const startDate = date ? date : this.minDate;
836
1067
  if (startDate) {
837
1068
  const xPosition = this.dateToX(startDate);
838
1069
  this.container.scrollTo({ left: xPosition - 80 });
839
1070
  }
840
1071
  }
1072
+ /**
1073
+ * vertical scroll to specified position
1074
+ *
1075
+ * @param params
1076
+ */
1077
+ verticalScrollTo(params) {
1078
+ if (params && (params.rowId || params.rowIndex)) {
1079
+ const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
1080
+ const yPosition = this.config.rowHeight * rowIndex;
1081
+ this.container.scrollTo({ top: yPosition - 80 });
1082
+ }
1083
+ }
841
1084
  }
842
1085
  exports.DateUtils = DateUtils;
843
1086
  exports.GanttChart = GanttChart;
package/dist/index.css 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
  */
@@ -80,7 +80,7 @@
80
80
  font-size: 13px;
81
81
  line-height: 1.6;
82
82
  pointer-events: none;
83
- z-index: 1000;
83
+ z-index: 2147483640;
84
84
  max-width: 300px;
85
85
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
86
86
  }