gantt-canvas-chart 1.2.0 → 1.3.1

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.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.2.0
2
+ * gantt-canvas-chart v1.3.1
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -93,6 +93,8 @@
93
93
  mainCanvas;
94
94
  scrollDummy;
95
95
  tooltip;
96
+ scrolling;
97
+ showTooltip;
96
98
  headerCtx;
97
99
  mainCtx;
98
100
  timelineStart;
@@ -112,9 +114,14 @@
112
114
  resizeObserver;
113
115
  taskPositions;
114
116
  taskMap;
115
- boundHandleMouseMove;
116
- boundHandleMouseLeave;
117
- boundHandleScroll;
117
+ isLoadingData = false;
118
+ hasMoreDataLeft = true;
119
+ hasMoreDataRight = true;
120
+ hasMoreDataBottom = true;
121
+ lastScrollLeft = 0;
122
+ lastScrollTop = 0;
123
+ onDataLoad = null;
124
+ scrollLoadTimer = null;
118
125
  constructor(rootContainer, data, config = {}) {
119
126
  const container = document.createElement("div");
120
127
  const scrollEl = document.createElement("div");
@@ -133,6 +140,8 @@
133
140
  this.rootContainer = rootContainer;
134
141
  this.container = container;
135
142
  this.data = data;
143
+ this.scrolling = false;
144
+ this.showTooltip = false;
136
145
  this.config = {
137
146
  viewMode: "Month",
138
147
  rowHeight: 48,
@@ -150,11 +159,14 @@
150
159
  todayColor: "#ff4d4f",
151
160
  offsetTop: 0,
152
161
  offsetLeft: 0,
162
+ enabledLoadMore: [],
153
163
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
154
164
  planBorderColor: "#C1EFCF",
155
165
  actualBgColor: "#5AC989",
166
+ headerBgColor: "#f9f9f9",
156
167
  ...config
157
168
  };
169
+ this.updateLoadMoreConf();
158
170
  this.headerCanvas = headerCanvas;
159
171
  this.mainCanvas = mainCanvas;
160
172
  this.scrollDummy = scrollEl;
@@ -181,10 +193,12 @@
181
193
  this.totalHeight = 0;
182
194
  this.taskPositions = /* @__PURE__ */ new Map();
183
195
  this.taskMap = /* @__PURE__ */ new Map();
184
- this.boundHandleMouseMove = this.handleMouseMove.bind(this);
185
- this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
186
- this.boundHandleScroll = this.handleScroll.bind(this);
187
- this.scrollToStartDate = this.scrollToStartDate.bind(this);
196
+ this.handleMouseMove = this.handleMouseMove.bind(this);
197
+ this.handleMouseLeave = this.handleMouseLeave.bind(this);
198
+ this.handleScroll = this.handleScroll.bind(this);
199
+ this.horizontalScrollTo = this.horizontalScrollTo.bind(this);
200
+ this.verticalScrollTo = this.verticalScrollTo.bind(this);
201
+ this.handleResize = this.handleResize.bind(this);
188
202
  this.init();
189
203
  }
190
204
  init() {
@@ -194,6 +208,11 @@
194
208
  this.setupEvents();
195
209
  this.handleResize();
196
210
  }
211
+ updateLoadMoreConf() {
212
+ this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
213
+ this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
214
+ this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
215
+ }
197
216
  buildTaskMap() {
198
217
  this.taskMap.clear();
199
218
  this.data.forEach((row, rowIndex) => {
@@ -201,8 +220,7 @@
201
220
  });
202
221
  }
203
222
  setupEvents() {
204
- this.container.addEventListener("scroll", this.boundHandleScroll);
205
- this.handleResize = this.handleResize.bind(this);
223
+ this.container.addEventListener("scroll", this.handleScroll);
206
224
  if (window.ResizeObserver) {
207
225
  this.resizeObserver = new ResizeObserver(this.handleResize);
208
226
  setTimeout(() => {
@@ -210,8 +228,8 @@
210
228
  }, 100);
211
229
  }
212
230
  if (this.config.showTooltip) {
213
- this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
214
- this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
231
+ this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
232
+ this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
215
233
  }
216
234
  }
217
235
  updateConfig(newConfig) {
@@ -222,6 +240,7 @@
222
240
  this.updatePixelsPerDay();
223
241
  this.calculateFullTimeline();
224
242
  }
243
+ this.updateLoadMoreConf();
225
244
  this.updateDimensions();
226
245
  this.render();
227
246
  }
@@ -240,13 +259,16 @@
240
259
  if (this.resizeObserver) {
241
260
  this.resizeObserver.disconnect();
242
261
  }
243
- this.container.removeEventListener("scroll", this.boundHandleScroll);
244
- this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
245
- this.mainCanvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
262
+ this.container.removeEventListener("scroll", this.handleScroll);
263
+ this.mainCanvas.removeEventListener("mousemove", this.handleMouseMove);
264
+ this.mainCanvas.removeEventListener("mouseleave", this.handleMouseLeave);
265
+ this.data = [];
266
+ this.taskMap.clear();
267
+ this.taskPositions.clear();
246
268
  this.container.remove();
247
269
  }
248
270
  calculateFullTimeline() {
249
- const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
271
+ const currentYear = this.today.getFullYear();
250
272
  let minDate = new Date(9999, 0, 1);
251
273
  let maxDate = new Date(1e3, 0, 1);
252
274
  if (this.data.length === 0) {
@@ -310,7 +332,14 @@
310
332
  this.updateDimensions();
311
333
  this.render();
312
334
  }
335
+ // Add this method to register the data loading callback
336
+ setOnDataLoadCallback(callback) {
337
+ this.onDataLoad = callback;
338
+ }
313
339
  handleScroll(e) {
340
+ if (this.showTooltip) {
341
+ this.handleMouseLeave();
342
+ }
314
343
  const target = e.target;
315
344
  this.scrollLeft = target.scrollLeft;
316
345
  this.scrollTop = target.scrollTop;
@@ -318,7 +347,131 @@
318
347
  detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
319
348
  });
320
349
  this.container.dispatchEvent(event);
321
- requestAnimationFrame(() => this.render());
350
+ if (this.config.enabledLoadMore.length > 0) {
351
+ if (this.scrollLoadTimer !== null) {
352
+ clearTimeout(this.scrollLoadTimer);
353
+ this.scrollLoadTimer = null;
354
+ }
355
+ this.scrollLoadTimer = window.setTimeout(() => {
356
+ this.checkScrollLoad();
357
+ this.scrollLoadTimer = null;
358
+ }, 100);
359
+ }
360
+ requestAnimationFrame(() => {
361
+ this.scrolling = true;
362
+ this.render(true);
363
+ this.scrolling = false;
364
+ });
365
+ }
366
+ // checkScrollLoad method
367
+ async checkScrollLoad() {
368
+ if (this.isLoadingData || !this.onDataLoad) {
369
+ return;
370
+ }
371
+ const scrollLeft = this.scrollLeft;
372
+ const scrollTop = this.scrollTop;
373
+ const viewportWidth = this.viewportWidth;
374
+ const viewportHeight = this.viewportHeight;
375
+ const totalWidth = this.totalWidth;
376
+ const totalHeight = this.totalHeight;
377
+ const atLeftEdge = scrollLeft <= 5;
378
+ const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
379
+ const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
380
+ try {
381
+ if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
382
+ await this.loadMoreData("left");
383
+ } else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
384
+ await this.loadMoreData("right");
385
+ } else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
386
+ await this.loadMoreData("bottom");
387
+ }
388
+ } finally {
389
+ this.lastScrollLeft = scrollLeft;
390
+ this.lastScrollTop = scrollTop;
391
+ }
392
+ }
393
+ // Add this method to reset scroll loading state
394
+ resetScrollLoadingState() {
395
+ this.hasMoreDataLeft = true;
396
+ this.hasMoreDataRight = true;
397
+ this.hasMoreDataBottom = true;
398
+ this.lastScrollLeft = 0;
399
+ this.lastScrollTop = 0;
400
+ if (this.scrollLoadTimer !== null) {
401
+ clearTimeout(this.scrollLoadTimer);
402
+ this.scrollLoadTimer = null;
403
+ }
404
+ }
405
+ // Add this new method to load additional data
406
+ async loadMoreData(direction) {
407
+ if (this.isLoadingData || !this.onDataLoad) {
408
+ return;
409
+ }
410
+ this.isLoadingData = true;
411
+ try {
412
+ let newData = null;
413
+ switch (direction) {
414
+ case "left":
415
+ newData = await this.onDataLoad("left", { date: this.minDate });
416
+ if (newData && newData.length > 0) {
417
+ this.prependData(newData);
418
+ } else {
419
+ this.hasMoreDataLeft = false;
420
+ }
421
+ break;
422
+ case "right":
423
+ newData = await this.onDataLoad("right", { date: this.maxDate });
424
+ if (newData && newData.length > 0) {
425
+ this.appendData(newData);
426
+ } else {
427
+ this.hasMoreDataRight = false;
428
+ }
429
+ break;
430
+ case "bottom":
431
+ const currentRowCount = this.data.length;
432
+ newData = await this.onDataLoad("bottom", { offset: currentRowCount });
433
+ if (newData && newData.length > 0) {
434
+ this.appendRows(newData);
435
+ } else {
436
+ this.hasMoreDataBottom = false;
437
+ }
438
+ break;
439
+ }
440
+ if (newData && newData.length > 0) {
441
+ this.buildTaskMap();
442
+ this.calculateFullTimeline();
443
+ this.updateDimensions();
444
+ this.render();
445
+ }
446
+ } catch (error) {
447
+ console.error("Error loading additional data:", error);
448
+ } finally {
449
+ this.isLoadingData = false;
450
+ }
451
+ }
452
+ // Add this method to append data to the right
453
+ appendData(newData) {
454
+ newData.forEach((newRow, index) => {
455
+ if (index < this.data.length) {
456
+ this.data[index].tasks = [...this.data[index].tasks, ...newRow.tasks];
457
+ } else {
458
+ this.data.push(newRow);
459
+ }
460
+ });
461
+ }
462
+ // Add this method to prepend data to the left
463
+ prependData(newData) {
464
+ newData.forEach((newRow, index) => {
465
+ if (index < this.data.length) {
466
+ this.data[index].tasks = [...newRow.tasks, ...this.data[index].tasks];
467
+ } else {
468
+ this.data.push(newRow);
469
+ }
470
+ });
471
+ }
472
+ // Add this method to append rows to the bottom
473
+ appendRows(newData) {
474
+ this.data.push(...newData);
322
475
  }
323
476
  setScrollTop(scrollTop) {
324
477
  if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
@@ -332,10 +485,14 @@
332
485
  }
333
486
  updateDimensions() {
334
487
  const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
335
- this.totalWidth = totalDays * this.pixelsPerDay;
336
- this.totalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
337
- this.scrollDummy.style.width = `${this.totalWidth}px`;
338
- this.scrollDummy.style.height = `${this.totalHeight}px`;
488
+ const newTotalWidth = totalDays * this.pixelsPerDay;
489
+ const newTotalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
490
+ if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
491
+ this.totalWidth = newTotalWidth;
492
+ this.totalHeight = newTotalHeight;
493
+ this.scrollDummy.style.width = `${this.totalWidth}px`;
494
+ this.scrollDummy.style.height = `${this.totalHeight}px`;
495
+ }
339
496
  }
340
497
  setupCanvas(canvas, width, height) {
341
498
  canvas.width = width * this.devicePixelRatio;
@@ -353,19 +510,41 @@
353
510
  const y = i * this.config.rowHeight;
354
511
  row.tasks.forEach((task) => {
355
512
  const x_plan_start = this.dateToX(new Date(task.planStart));
356
- const x_plan_end = this.dateToX(DateUtils.addDays(new Date(task.planEnd), 1));
513
+ const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
357
514
  let x_actual_start = null, x_actual_end = null;
515
+ let offset_x_plan_start = NaN, offset_x_plan_end = NaN;
516
+ let offset_x_actual_start = null, offset_x_actual_end = null;
517
+ let x_plan_width = 0;
518
+ let x_actual_width = 0;
519
+ const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
520
+ const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
521
+ if (x_plan_start && x_plan_end) {
522
+ x_plan_width = x_plan_end - x_plan_start;
523
+ offset_x_plan_start = x_plan_start + x_plan_width * offsetX;
524
+ x_plan_end && (offset_x_plan_end = offset_x_plan_start + x_plan_width * percent_plan);
525
+ }
358
526
  if (task.actualStart) {
359
527
  x_actual_start = this.dateToX(new Date(task.actualStart));
360
528
  }
361
529
  if (task.actualEnd) {
362
- x_actual_end = this.dateToX(DateUtils.addDays(new Date(task.actualEnd), 1));
530
+ x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
531
+ }
532
+ if (x_actual_start) {
533
+ x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
534
+ offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
535
+ x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
363
536
  }
364
537
  this.taskPositions.set(task.id, {
365
538
  x_plan_start,
366
539
  x_plan_end,
367
540
  x_actual_start,
368
541
  x_actual_end,
542
+ offset_x_plan_start,
543
+ offset_x_plan_end,
544
+ offset_x_actual_start,
545
+ offset_x_actual_end,
546
+ x_plan_width,
547
+ x_actual_width,
369
548
  y: y + this.config.rowHeight * 0.5,
370
549
  row: i
371
550
  });
@@ -385,9 +564,11 @@
385
564
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
386
565
  }
387
566
  }
388
- render() {
567
+ render(scrolling = false) {
389
568
  this.updateVirtualRanges();
390
- this.calculateAllTaskPositions();
569
+ if (!scrolling) {
570
+ this.calculateAllTaskPositions();
571
+ }
391
572
  this.renderHeader();
392
573
  this.renderMain();
393
574
  }
@@ -397,82 +578,132 @@
397
578
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
398
579
  ctx.save();
399
580
  ctx.translate(-this.scrollLeft, 0);
400
- ctx.fillStyle = "#f9f9f9";
581
+ ctx.fillStyle = this.config.headerBgColor;
401
582
  ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
402
583
  ctx.textBaseline = "middle";
403
584
  ctx.textRendering = "optimizeLegibility";
404
585
  let currentDate = new Date(this.visibleDateRange.start);
405
586
  currentDate = this.getIterationStartDate(currentDate);
406
- let lastUpperText = "";
407
- while (this.dateToX(currentDate) < this.scrollLeft - this.pixelsPerDay * 7) {
587
+ const visibleBlocks = [];
588
+ let calcDate = new Date(currentDate);
589
+ while (calcDate <= this.visibleDateRange.end) {
590
+ let nextDate;
591
+ let upperText = "";
592
+ switch (this.config.viewMode) {
593
+ case "Day":
594
+ upperText = DateUtils.format(calcDate, "yyyy年MM月");
595
+ nextDate = DateUtils.addDays(calcDate, 1);
596
+ break;
597
+ case "Week":
598
+ const weekStart = DateUtils.getStartOfWeek(calcDate);
599
+ upperText = DateUtils.format(weekStart, "yyyy年MM月");
600
+ nextDate = DateUtils.addDays(weekStart, 7);
601
+ break;
602
+ case "Month":
603
+ upperText = `${calcDate.getFullYear()}年`;
604
+ nextDate = DateUtils.addMonths(calcDate, 1);
605
+ break;
606
+ case "Year":
607
+ if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
608
+ upperText = `${calcDate.getFullYear()}年`;
609
+ nextDate = DateUtils.addMonths(calcDate, 6);
610
+ } else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
611
+ upperText = `${calcDate.getFullYear()}年`;
612
+ nextDate = DateUtils.addMonths(calcDate, 6);
613
+ } else {
614
+ calcDate = DateUtils.addDays(calcDate, 1);
615
+ continue;
616
+ }
617
+ break;
618
+ default:
619
+ nextDate = DateUtils.addDays(calcDate, 1);
620
+ break;
621
+ }
622
+ const startX = this.dateToX(calcDate);
623
+ const endX = this.dateToX(nextDate);
624
+ visibleBlocks.push({
625
+ startX,
626
+ endX,
627
+ text: upperText,
628
+ yPos: h * 0.35
629
+ });
630
+ calcDate = nextDate;
631
+ }
632
+ let currentDateForLower = new Date(currentDate);
633
+ while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
408
634
  let nextDate;
409
635
  switch (this.config.viewMode) {
410
636
  case "Day":
411
- nextDate = DateUtils.addDays(currentDate, 1);
637
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
412
638
  break;
413
639
  case "Week":
414
- nextDate = DateUtils.addDays(currentDate, 7);
640
+ nextDate = DateUtils.addDays(currentDateForLower, 7);
415
641
  break;
416
642
  case "Month":
417
- nextDate = DateUtils.addMonths(currentDate, 1);
643
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
418
644
  break;
419
645
  case "Year":
420
- nextDate = DateUtils.addMonths(currentDate, 6);
646
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
421
647
  break;
422
648
  default:
423
- nextDate = DateUtils.addDays(currentDate, 1);
649
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
424
650
  break;
425
651
  }
426
- if (nextDate.getTime() === currentDate.getTime()) {
427
- currentDate = DateUtils.addDays(currentDate, 1);
652
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
653
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
428
654
  } else {
429
- currentDate = nextDate;
655
+ currentDateForLower = nextDate;
430
656
  }
431
657
  }
432
- while (currentDate <= this.visibleDateRange.end) {
433
- const x = this.dateToX(currentDate);
434
- let upperText = "", lowerText = "", nextDate;
658
+ const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
659
+ ctx.fillStyle = "#333";
660
+ ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
661
+ ctx.textAlign = "left";
662
+ groupedBlocks.forEach((group) => {
663
+ const visibleStart = Math.max(group.startX, this.scrollLeft);
664
+ const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
665
+ if (visibleEnd > visibleStart) {
666
+ ctx.fillStyle = this.config.headerBgColor;
667
+ ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
668
+ ctx.fillStyle = "#333";
669
+ ctx.fillText(group.text, visibleStart + 5, group.yPos);
670
+ }
671
+ });
672
+ while (currentDateForLower <= this.visibleDateRange.end) {
673
+ const x = this.dateToX(currentDateForLower);
674
+ let lowerText = "";
675
+ let nextDate;
435
676
  switch (this.config.viewMode) {
436
677
  case "Day":
437
- upperText = DateUtils.format(currentDate, "yyyy年MM月");
438
- lowerText = `${DateUtils.format(currentDate, "d")} ${DateUtils.format(currentDate, "W")}`;
439
- nextDate = DateUtils.addDays(currentDate, 1);
678
+ lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
679
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
440
680
  break;
441
681
  case "Week":
442
- const weekStart = DateUtils.getStartOfWeek(currentDate);
443
- upperText = DateUtils.format(weekStart, "yyyy年MM月");
682
+ const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
444
683
  lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
445
684
  nextDate = DateUtils.addDays(weekStart, 7);
446
685
  break;
447
686
  case "Month":
448
- upperText = `${currentDate.getFullYear()}年`;
449
- lowerText = `${currentDate.getMonth() + 1}月`;
450
- nextDate = DateUtils.addMonths(currentDate, 1);
687
+ lowerText = `${currentDateForLower.getMonth() + 1}月`;
688
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
451
689
  break;
452
690
  case "Year":
453
- if (currentDate.getMonth() === 0 && currentDate.getDate() === 1) {
454
- upperText = `${currentDate.getFullYear()}年`;
691
+ if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
455
692
  lowerText = `上半年`;
456
- nextDate = DateUtils.addMonths(currentDate, 6);
457
- } else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1) {
458
- upperText = `${currentDate.getFullYear()}年`;
693
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
694
+ } else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
459
695
  lowerText = `下半年`;
460
- nextDate = DateUtils.addMonths(currentDate, 6);
696
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
461
697
  } else {
462
- currentDate = DateUtils.addDays(currentDate, 1);
698
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
463
699
  continue;
464
700
  }
465
701
  break;
702
+ default:
703
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
704
+ break;
466
705
  }
467
706
  const unitWidth = this.dateToX(nextDate) - x;
468
- if (upperText !== lastUpperText) {
469
- ctx.fillStyle = "#333";
470
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
471
- ctx.textRendering = "optimizeLegibility";
472
- ctx.textAlign = "left";
473
- ctx.fillText(upperText, x + 5, h * 0.35);
474
- lastUpperText = upperText;
475
- }
476
707
  ctx.fillStyle = "#000412";
477
708
  ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
478
709
  ctx.textAlign = "center";
@@ -482,10 +713,10 @@
482
713
  ctx.lineTo(x, h);
483
714
  ctx.strokeStyle = "#e0e0e0";
484
715
  ctx.stroke();
485
- if (nextDate.getTime() === currentDate.getTime()) {
486
- currentDate = DateUtils.addDays(currentDate, 1);
716
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
717
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
487
718
  } else {
488
- currentDate = nextDate;
719
+ currentDateForLower = nextDate;
489
720
  }
490
721
  }
491
722
  ctx.beginPath();
@@ -495,6 +726,22 @@
495
726
  ctx.stroke();
496
727
  ctx.restore();
497
728
  }
729
+ // Helper method to group consecutive blocks with same text
730
+ groupConsecutiveBlocks(blocks) {
731
+ if (blocks.length === 0) return [];
732
+ const grouped = [];
733
+ let currentGroup = { ...blocks[0] };
734
+ for (let i = 1; i < blocks.length; i++) {
735
+ if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
736
+ currentGroup.endX = blocks[i].endX;
737
+ } else {
738
+ grouped.push(currentGroup);
739
+ currentGroup = { ...blocks[i] };
740
+ }
741
+ }
742
+ grouped.push(currentGroup);
743
+ return grouped;
744
+ }
498
745
  renderMain() {
499
746
  const ctx = this.mainCtx;
500
747
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -542,9 +789,9 @@
542
789
  if (!fromPos) return;
543
790
  const fromRowIndex = this.taskMap.get(depId).row;
544
791
  const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
545
- const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
792
+ const fromX = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
546
793
  const fromY = fromPos.y;
547
- const toX = Math.min(toPos.x_plan_start, toPos.x_actual_start || toPos.x_plan_start);
794
+ const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
548
795
  const toY = toPos.y;
549
796
  ctx.beginPath();
550
797
  if (isAdjacent) {
@@ -699,42 +946,35 @@
699
946
  }
700
947
  drawTask(ctx, task, y, pos) {
701
948
  const offset = 4;
702
- const width = pos.x_plan_end - pos.x_plan_start;
703
949
  const taskY = y + this.config.rowHeight * 0.15 + offset;
704
950
  const taskHeight = this.config.rowHeight * 0.7 - offset;
705
951
  const textY = y + this.config.rowHeight / 2 + offset;
706
952
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
707
- const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
708
953
  if (this.config.showActual && pos.x_actual_start) {
709
954
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
710
- const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
711
- pos.x_actual_start += aWidth * offsetX_actual;
712
- pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
713
- ctx.fillRect(Math.round(pos.x_actual_start), Math.round(taskY + 2), Math.round(aWidth * percent_actual), Math.round(taskHeight - 2));
955
+ ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
714
956
  }
715
957
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
716
958
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
717
- pos.x_plan_start += width * offsetX;
718
- pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
719
959
  ctx.lineWidth = 4;
720
960
  ctx.beginPath();
721
- ctx.moveTo(pos.x_plan_start, taskY);
722
- ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
961
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
962
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
723
963
  ctx.stroke();
724
964
  }
725
965
  ctx.fillStyle = "#000";
726
966
  if (this.config.showLeftRemark && task.leftRemark) {
727
967
  ctx.textAlign = "right";
728
- ctx.fillText(task.leftRemark, Math.round(Math.min(...[pos.x_plan_start, pos.x_actual_start].filter((val) => val !== null && val !== void 0)) - 8), Math.round(textY));
968
+ ctx.fillText(task.leftRemark, Math.round(Math.min(...[pos.offset_x_plan_start, pos.offset_x_actual_start].filter((val) => val !== null && val !== void 0)) - 8), Math.round(textY));
729
969
  }
730
970
  if (this.config.showRightRemark && task.rightRemark) {
731
971
  ctx.textAlign = "left";
732
- ctx.fillText(task.rightRemark, Math.round(Math.max(...[pos.x_plan_end, pos.x_actual_end].filter((val) => val !== null && val !== void 0)) + 8), Math.round(textY));
972
+ ctx.fillText(task.rightRemark, Math.round(Math.max(...[pos.offset_x_plan_end, pos.offset_x_actual_end].filter((val) => val !== null && val !== void 0)) + 8), Math.round(textY));
733
973
  }
734
974
  if (this.config.showCenterRemark && task.centerRemark) {
735
- const centerX = pos.x_actual_start + (pos.x_actual_end - pos.x_actual_start) / 2;
975
+ const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
736
976
  ctx.textAlign = "center";
737
- ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.x_actual_end - pos.x_actual_start));
977
+ ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.offset_x_actual_end - pos.offset_x_actual_start));
738
978
  }
739
979
  }
740
980
  getTaskStyles(task) {
@@ -755,6 +995,9 @@
755
995
  return styles;
756
996
  }
757
997
  handleMouseMove(e) {
998
+ if (this.scrolling) {
999
+ return;
1000
+ }
758
1001
  const rect = this.mainCanvas.getBoundingClientRect();
759
1002
  const mouseX = e.clientX - rect.left;
760
1003
  const mouseY = e.clientY - rect.top;
@@ -767,15 +1010,16 @@
767
1010
  if (this.config.tooltipFormat) {
768
1011
  const htmlStr = this.config.tooltipFormat(row, date, this.config);
769
1012
  if (!htmlStr) {
770
- return this.handleMouseLeave();
1013
+ this.handleMouseLeave();
1014
+ return;
771
1015
  }
772
1016
  this.tooltip.innerHTML = htmlStr;
773
1017
  } else {
774
1018
  const overlappingTasks = row.tasks.filter((task) => {
775
- const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
1019
+ const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
776
1020
  if (date >= pStart && date < pEnd) return true;
777
1021
  if (task.actualStart) {
778
- const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
1022
+ const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
779
1023
  if (date >= aStart && date < aEnd) return true;
780
1024
  }
781
1025
  return false;
@@ -789,6 +1033,7 @@
789
1033
  this.tooltip.innerHTML = html;
790
1034
  }
791
1035
  this.tooltip.style.display = "block";
1036
+ this.showTooltip = true;
792
1037
  if (this.config.tooltipColor === "white") {
793
1038
  this.tooltip.style.background = "#fff";
794
1039
  this.tooltip.style.color = "#000";
@@ -818,6 +1063,7 @@
818
1063
  }
819
1064
  handleMouseLeave() {
820
1065
  this.tooltip.style.display = "none";
1066
+ this.showTooltip = false;
821
1067
  }
822
1068
  /**
823
1069
  * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
@@ -829,17 +1075,29 @@
829
1075
  return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
830
1076
  }
831
1077
  /**
832
- * scroll to specified date position, default to minDate
1078
+ * horizontal scroll to specified date position, default to minDate
833
1079
  *
834
1080
  * @param date
835
1081
  */
836
- scrollToStartDate(date) {
1082
+ horizontalScrollTo(date) {
837
1083
  const startDate = date ? date : this.minDate;
838
1084
  if (startDate) {
839
1085
  const xPosition = this.dateToX(startDate);
840
1086
  this.container.scrollTo({ left: xPosition - 80 });
841
1087
  }
842
1088
  }
1089
+ /**
1090
+ * vertical scroll to specified position
1091
+ *
1092
+ * @param params
1093
+ */
1094
+ verticalScrollTo(params) {
1095
+ if (params && (params.rowId || params.rowIndex)) {
1096
+ const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
1097
+ const yPosition = this.config.rowHeight * rowIndex;
1098
+ this.container.scrollTo({ top: yPosition - 80 });
1099
+ }
1100
+ }
843
1101
  }
844
1102
  exports2.DateUtils = DateUtils;
845
1103
  exports2.GanttChart = GanttChart;