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.cjs.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
  */
@@ -91,12 +91,14 @@ class GanttChart {
91
91
  mainCanvas;
92
92
  scrollDummy;
93
93
  tooltip;
94
+ scrolling;
95
+ showTooltip;
94
96
  headerCtx;
95
97
  mainCtx;
96
98
  timelineStart;
97
99
  timelineEnd;
98
100
  minDate;
99
- // private maxDate: Date | null;
101
+ maxDate;
100
102
  pixelsPerDay;
101
103
  scrollLeft;
102
104
  scrollTop;
@@ -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;
@@ -166,6 +178,7 @@ class GanttChart {
166
178
  this.timelineStart = /* @__PURE__ */ new Date();
167
179
  this.timelineEnd = /* @__PURE__ */ new Date();
168
180
  this.minDate = null;
181
+ this.maxDate = null;
169
182
  this.pixelsPerDay = 40;
170
183
  this.scrollLeft = 0;
171
184
  this.scrollTop = 0;
@@ -178,10 +191,11 @@ class GanttChart {
178
191
  this.totalHeight = 0;
179
192
  this.taskPositions = /* @__PURE__ */ new Map();
180
193
  this.taskMap = /* @__PURE__ */ new Map();
181
- this.boundHandleMouseMove = this.handleMouseMove.bind(this);
182
- this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
183
- this.boundHandleScroll = this.handleScroll.bind(this);
184
- 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);
185
199
  this.init();
186
200
  }
187
201
  init() {
@@ -191,6 +205,11 @@ class GanttChart {
191
205
  this.setupEvents();
192
206
  this.handleResize();
193
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
+ }
194
213
  buildTaskMap() {
195
214
  this.taskMap.clear();
196
215
  this.data.forEach((row, rowIndex) => {
@@ -198,7 +217,7 @@ class GanttChart {
198
217
  });
199
218
  }
200
219
  setupEvents() {
201
- this.container.addEventListener("scroll", this.boundHandleScroll);
220
+ this.container.addEventListener("scroll", this.handleScroll);
202
221
  this.handleResize = this.handleResize.bind(this);
203
222
  if (window.ResizeObserver) {
204
223
  this.resizeObserver = new ResizeObserver(this.handleResize);
@@ -207,8 +226,8 @@ class GanttChart {
207
226
  }, 100);
208
227
  }
209
228
  if (this.config.showTooltip) {
210
- this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
211
- this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
229
+ this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
230
+ this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
212
231
  }
213
232
  }
214
233
  updateConfig(newConfig) {
@@ -219,6 +238,7 @@ class GanttChart {
219
238
  this.updatePixelsPerDay();
220
239
  this.calculateFullTimeline();
221
240
  }
241
+ this.updateLoadMoreConf();
222
242
  this.updateDimensions();
223
243
  this.render();
224
244
  }
@@ -237,17 +257,18 @@ class GanttChart {
237
257
  if (this.resizeObserver) {
238
258
  this.resizeObserver.disconnect();
239
259
  }
240
- this.container.removeEventListener("scroll", this.boundHandleScroll);
241
- this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
242
- 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);
243
263
  this.container.remove();
244
264
  }
245
265
  calculateFullTimeline() {
266
+ const currentYear = this.today.getFullYear();
246
267
  let minDate = new Date(9999, 0, 1);
247
268
  let maxDate = new Date(1e3, 0, 1);
248
269
  if (this.data.length === 0) {
249
270
  minDate = /* @__PURE__ */ new Date();
250
- maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(), 30);
271
+ maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(), 60);
251
272
  } else {
252
273
  this.taskMap.forEach(({ task }) => {
253
274
  const pStart = new Date(task.planStart);
@@ -263,8 +284,11 @@ class GanttChart {
263
284
  });
264
285
  }
265
286
  this.minDate = minDate;
266
- minDate = DateUtils.addDays(minDate, -7);
267
- maxDate = DateUtils.addDays(maxDate, 14);
287
+ this.maxDate = maxDate;
288
+ const minYear = minDate.getFullYear();
289
+ const maxYear = maxDate.getFullYear();
290
+ minDate = DateUtils.addDays(minYear === 9999 ? new Date(currentYear, 0, 1) : minDate, -7);
291
+ maxDate = DateUtils.addDays(maxYear === 1e3 ? new Date(currentYear + 1, 0, 1) : maxDate, 14);
268
292
  switch (this.config.viewMode) {
269
293
  case "Year":
270
294
  this.timelineStart = DateUtils.getStartOfYear(minDate);
@@ -303,7 +327,14 @@ class GanttChart {
303
327
  this.updateDimensions();
304
328
  this.render();
305
329
  }
330
+ // Add this method to register the data loading callback
331
+ setOnDataLoadCallback(callback) {
332
+ this.onDataLoad = callback;
333
+ }
306
334
  handleScroll(e) {
335
+ if (this.showTooltip) {
336
+ this.handleMouseLeave();
337
+ }
307
338
  const target = e.target;
308
339
  this.scrollLeft = target.scrollLeft;
309
340
  this.scrollTop = target.scrollTop;
@@ -311,7 +342,134 @@ class GanttChart {
311
342
  detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
312
343
  });
313
344
  this.container.dispatchEvent(event);
314
- 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);
315
473
  }
316
474
  setScrollTop(scrollTop) {
317
475
  if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
@@ -325,10 +483,14 @@ class GanttChart {
325
483
  }
326
484
  updateDimensions() {
327
485
  const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
328
- this.totalWidth = totalDays * this.pixelsPerDay;
329
- this.totalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
330
- this.scrollDummy.style.width = `${this.totalWidth}px`;
331
- 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
+ }
332
494
  }
333
495
  setupCanvas(canvas, width, height) {
334
496
  canvas.width = width * this.devicePixelRatio;
@@ -346,13 +508,13 @@ class GanttChart {
346
508
  const y = i * this.config.rowHeight;
347
509
  row.tasks.forEach((task) => {
348
510
  const x_plan_start = this.dateToX(new Date(task.planStart));
349
- 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));
350
512
  let x_actual_start = null, x_actual_end = null;
351
513
  if (task.actualStart) {
352
514
  x_actual_start = this.dateToX(new Date(task.actualStart));
353
515
  }
354
516
  if (task.actualEnd) {
355
- x_actual_end = this.dateToX(DateUtils.addDays(new Date(task.actualEnd), 1));
517
+ x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
356
518
  }
357
519
  this.taskPositions.set(task.id, {
358
520
  x_plan_start,
@@ -378,9 +540,11 @@ class GanttChart {
378
540
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
379
541
  }
380
542
  }
381
- render() {
543
+ render(scrolling = false) {
382
544
  this.updateVirtualRanges();
383
- this.calculateAllTaskPositions();
545
+ if (!scrolling) {
546
+ this.calculateAllTaskPositions();
547
+ }
384
548
  this.renderHeader();
385
549
  this.renderMain();
386
550
  }
@@ -390,82 +554,132 @@ class GanttChart {
390
554
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
391
555
  ctx.save();
392
556
  ctx.translate(-this.scrollLeft, 0);
393
- ctx.fillStyle = "#f9f9f9";
557
+ ctx.fillStyle = this.config.headerBgColor;
394
558
  ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
395
559
  ctx.textBaseline = "middle";
396
560
  ctx.textRendering = "optimizeLegibility";
397
561
  let currentDate = new Date(this.visibleDateRange.start);
398
562
  currentDate = this.getIterationStartDate(currentDate);
399
- let lastUpperText = "";
400
- while (this.dateToX(currentDate) < this.scrollLeft - this.pixelsPerDay * 7) {
563
+ const visibleBlocks = [];
564
+ let calcDate = new Date(currentDate);
565
+ while (calcDate <= this.visibleDateRange.end) {
401
566
  let nextDate;
567
+ let upperText = "";
402
568
  switch (this.config.viewMode) {
403
569
  case "Day":
404
- nextDate = DateUtils.addDays(currentDate, 1);
570
+ upperText = DateUtils.format(calcDate, "yyyy年MM月");
571
+ nextDate = DateUtils.addDays(calcDate, 1);
405
572
  break;
406
573
  case "Week":
407
- nextDate = DateUtils.addDays(currentDate, 7);
574
+ const weekStart = DateUtils.getStartOfWeek(calcDate);
575
+ upperText = DateUtils.format(weekStart, "yyyy年MM月");
576
+ nextDate = DateUtils.addDays(weekStart, 7);
408
577
  break;
409
578
  case "Month":
410
- nextDate = DateUtils.addMonths(currentDate, 1);
579
+ upperText = `${calcDate.getFullYear()}年`;
580
+ nextDate = DateUtils.addMonths(calcDate, 1);
411
581
  break;
412
582
  case "Year":
413
- 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);
414
623
  break;
415
624
  default:
416
- nextDate = DateUtils.addDays(currentDate, 1);
625
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
417
626
  break;
418
627
  }
419
- if (nextDate.getTime() === currentDate.getTime()) {
420
- currentDate = DateUtils.addDays(currentDate, 1);
628
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
629
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
421
630
  } else {
422
- currentDate = nextDate;
631
+ currentDateForLower = nextDate;
423
632
  }
424
633
  }
425
- while (currentDate <= this.visibleDateRange.end) {
426
- const x = this.dateToX(currentDate);
427
- 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;
428
652
  switch (this.config.viewMode) {
429
653
  case "Day":
430
- upperText = DateUtils.format(currentDate, "yyyy年MM月");
431
- lowerText = `${DateUtils.format(currentDate, "d")} ${DateUtils.format(currentDate, "W")}`;
432
- nextDate = DateUtils.addDays(currentDate, 1);
654
+ lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
655
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
433
656
  break;
434
657
  case "Week":
435
- const weekStart = DateUtils.getStartOfWeek(currentDate);
436
- upperText = DateUtils.format(weekStart, "yyyy年MM月");
658
+ const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
437
659
  lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
438
660
  nextDate = DateUtils.addDays(weekStart, 7);
439
661
  break;
440
662
  case "Month":
441
- upperText = `${currentDate.getFullYear()}年`;
442
- lowerText = `${currentDate.getMonth() + 1}月`;
443
- nextDate = DateUtils.addMonths(currentDate, 1);
663
+ lowerText = `${currentDateForLower.getMonth() + 1}月`;
664
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
444
665
  break;
445
666
  case "Year":
446
- if (currentDate.getMonth() === 0 && currentDate.getDate() === 1) {
447
- upperText = `${currentDate.getFullYear()}年`;
667
+ if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
448
668
  lowerText = `上半年`;
449
- nextDate = DateUtils.addMonths(currentDate, 6);
450
- } else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1) {
451
- upperText = `${currentDate.getFullYear()}年`;
669
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
670
+ } else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
452
671
  lowerText = `下半年`;
453
- nextDate = DateUtils.addMonths(currentDate, 6);
672
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
454
673
  } else {
455
- currentDate = DateUtils.addDays(currentDate, 1);
674
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
456
675
  continue;
457
676
  }
458
677
  break;
678
+ default:
679
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
680
+ break;
459
681
  }
460
682
  const unitWidth = this.dateToX(nextDate) - x;
461
- if (upperText !== lastUpperText) {
462
- ctx.fillStyle = "#333";
463
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
464
- ctx.textRendering = "optimizeLegibility";
465
- ctx.textAlign = "left";
466
- ctx.fillText(upperText, x + 5, h * 0.35);
467
- lastUpperText = upperText;
468
- }
469
683
  ctx.fillStyle = "#000412";
470
684
  ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
471
685
  ctx.textAlign = "center";
@@ -475,10 +689,10 @@ class GanttChart {
475
689
  ctx.lineTo(x, h);
476
690
  ctx.strokeStyle = "#e0e0e0";
477
691
  ctx.stroke();
478
- if (nextDate.getTime() === currentDate.getTime()) {
479
- currentDate = DateUtils.addDays(currentDate, 1);
692
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
693
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
480
694
  } else {
481
- currentDate = nextDate;
695
+ currentDateForLower = nextDate;
482
696
  }
483
697
  }
484
698
  ctx.beginPath();
@@ -488,6 +702,22 @@ class GanttChart {
488
702
  ctx.stroke();
489
703
  ctx.restore();
490
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
+ }
491
721
  renderMain() {
492
722
  const ctx = this.mainCtx;
493
723
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -683,7 +913,7 @@ class GanttChart {
683
913
  const x = this.dateToX(this.today);
684
914
  if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
685
915
  ctx.strokeStyle = this.config.todayColor;
686
- ctx.lineWidth = 2;
916
+ ctx.lineWidth = 1;
687
917
  ctx.beginPath();
688
918
  ctx.moveTo(x, this.scrollTop);
689
919
  ctx.lineTo(x, this.scrollTop + this.viewportHeight);
@@ -711,8 +941,8 @@ class GanttChart {
711
941
  pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
712
942
  ctx.lineWidth = 4;
713
943
  ctx.beginPath();
714
- ctx.moveTo(pos.x_plan_start, taskY);
715
- 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);
716
946
  ctx.stroke();
717
947
  }
718
948
  ctx.fillStyle = "#000";
@@ -748,6 +978,9 @@ class GanttChart {
748
978
  return styles;
749
979
  }
750
980
  handleMouseMove(e) {
981
+ if (this.scrolling) {
982
+ return;
983
+ }
751
984
  const rect = this.mainCanvas.getBoundingClientRect();
752
985
  const mouseX = e.clientX - rect.left;
753
986
  const mouseY = e.clientY - rect.top;
@@ -760,15 +993,16 @@ class GanttChart {
760
993
  if (this.config.tooltipFormat) {
761
994
  const htmlStr = this.config.tooltipFormat(row, date, this.config);
762
995
  if (!htmlStr) {
763
- return this.handleMouseLeave();
996
+ this.handleMouseLeave();
997
+ return;
764
998
  }
765
999
  this.tooltip.innerHTML = htmlStr;
766
1000
  } else {
767
1001
  const overlappingTasks = row.tasks.filter((task) => {
768
- 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);
769
1003
  if (date >= pStart && date < pEnd) return true;
770
1004
  if (task.actualStart) {
771
- 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);
772
1006
  if (date >= aStart && date < aEnd) return true;
773
1007
  }
774
1008
  return false;
@@ -782,6 +1016,7 @@ class GanttChart {
782
1016
  this.tooltip.innerHTML = html;
783
1017
  }
784
1018
  this.tooltip.style.display = "block";
1019
+ this.showTooltip = true;
785
1020
  if (this.config.tooltipColor === "white") {
786
1021
  this.tooltip.style.background = "#fff";
787
1022
  this.tooltip.style.color = "#000";
@@ -811,6 +1046,7 @@ class GanttChart {
811
1046
  }
812
1047
  handleMouseLeave() {
813
1048
  this.tooltip.style.display = "none";
1049
+ this.showTooltip = false;
814
1050
  }
815
1051
  /**
816
1052
  * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
@@ -822,17 +1058,29 @@ class GanttChart {
822
1058
  return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
823
1059
  }
824
1060
  /**
825
- * scroll to specified date position, default to minDate
1061
+ * horizontal scroll to specified date position, default to minDate
826
1062
  *
827
1063
  * @param date
828
1064
  */
829
- scrollToStartDate(date) {
1065
+ horizontalScrollTo(date) {
830
1066
  const startDate = date ? date : this.minDate;
831
1067
  if (startDate) {
832
1068
  const xPosition = this.dateToX(startDate);
833
1069
  this.container.scrollTo({ left: xPosition - 80 });
834
1070
  }
835
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
+ }
836
1084
  }
837
1085
  exports.DateUtils = DateUtils;
838
1086
  exports.GanttChart = GanttChart;
package/dist/index.css 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
  */
@@ -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
  }