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.umd.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
  */
@@ -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,11 @@
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);
188
201
  this.init();
189
202
  }
190
203
  init() {
@@ -194,6 +207,11 @@
194
207
  this.setupEvents();
195
208
  this.handleResize();
196
209
  }
210
+ updateLoadMoreConf() {
211
+ this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
212
+ this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
213
+ this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
214
+ }
197
215
  buildTaskMap() {
198
216
  this.taskMap.clear();
199
217
  this.data.forEach((row, rowIndex) => {
@@ -201,7 +219,7 @@
201
219
  });
202
220
  }
203
221
  setupEvents() {
204
- this.container.addEventListener("scroll", this.boundHandleScroll);
222
+ this.container.addEventListener("scroll", this.handleScroll);
205
223
  this.handleResize = this.handleResize.bind(this);
206
224
  if (window.ResizeObserver) {
207
225
  this.resizeObserver = new ResizeObserver(this.handleResize);
@@ -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,13 @@
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);
246
265
  this.container.remove();
247
266
  }
248
267
  calculateFullTimeline() {
249
- const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
268
+ const currentYear = this.today.getFullYear();
250
269
  let minDate = new Date(9999, 0, 1);
251
270
  let maxDate = new Date(1e3, 0, 1);
252
271
  if (this.data.length === 0) {
@@ -310,7 +329,14 @@
310
329
  this.updateDimensions();
311
330
  this.render();
312
331
  }
332
+ // Add this method to register the data loading callback
333
+ setOnDataLoadCallback(callback) {
334
+ this.onDataLoad = callback;
335
+ }
313
336
  handleScroll(e) {
337
+ if (this.showTooltip) {
338
+ this.handleMouseLeave();
339
+ }
314
340
  const target = e.target;
315
341
  this.scrollLeft = target.scrollLeft;
316
342
  this.scrollTop = target.scrollTop;
@@ -318,7 +344,134 @@
318
344
  detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
319
345
  });
320
346
  this.container.dispatchEvent(event);
321
- requestAnimationFrame(() => this.render());
347
+ if (this.config.enabledLoadMore.length > 0) {
348
+ if (this.scrollLoadTimer !== null) {
349
+ clearTimeout(this.scrollLoadTimer);
350
+ this.scrollLoadTimer = null;
351
+ }
352
+ this.scrollLoadTimer = window.setTimeout(() => {
353
+ this.checkScrollLoad();
354
+ this.scrollLoadTimer = null;
355
+ }, 100);
356
+ }
357
+ requestAnimationFrame(() => {
358
+ this.scrolling = true;
359
+ this.render(true);
360
+ this.scrolling = false;
361
+ });
362
+ }
363
+ // checkScrollLoad method
364
+ async checkScrollLoad() {
365
+ if (this.isLoadingData || !this.onDataLoad) {
366
+ return;
367
+ }
368
+ const scrollLeft = this.scrollLeft;
369
+ const scrollTop = this.scrollTop;
370
+ const viewportWidth = this.viewportWidth;
371
+ const viewportHeight = this.viewportHeight;
372
+ const totalWidth = this.totalWidth;
373
+ const totalHeight = this.totalHeight;
374
+ const atLeftEdge = scrollLeft <= 5;
375
+ const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
376
+ const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
377
+ try {
378
+ if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
379
+ await this.loadMoreData("left");
380
+ console.log("left-loadMoreData::", this.data);
381
+ } else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
382
+ await this.loadMoreData("right");
383
+ console.log("right-loadMoreData::", this.data);
384
+ } else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
385
+ await this.loadMoreData("bottom");
386
+ console.log("bottom-loadMoreData::", this.data);
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,13 +510,13 @@
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;
358
515
  if (task.actualStart) {
359
516
  x_actual_start = this.dateToX(new Date(task.actualStart));
360
517
  }
361
518
  if (task.actualEnd) {
362
- x_actual_end = this.dateToX(DateUtils.addDays(new Date(task.actualEnd), 1));
519
+ x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
363
520
  }
364
521
  this.taskPositions.set(task.id, {
365
522
  x_plan_start,
@@ -385,9 +542,11 @@
385
542
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
386
543
  }
387
544
  }
388
- render() {
545
+ render(scrolling = false) {
389
546
  this.updateVirtualRanges();
390
- this.calculateAllTaskPositions();
547
+ if (!scrolling) {
548
+ this.calculateAllTaskPositions();
549
+ }
391
550
  this.renderHeader();
392
551
  this.renderMain();
393
552
  }
@@ -397,82 +556,132 @@
397
556
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
398
557
  ctx.save();
399
558
  ctx.translate(-this.scrollLeft, 0);
400
- ctx.fillStyle = "#f9f9f9";
559
+ ctx.fillStyle = this.config.headerBgColor;
401
560
  ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
402
561
  ctx.textBaseline = "middle";
403
562
  ctx.textRendering = "optimizeLegibility";
404
563
  let currentDate = new Date(this.visibleDateRange.start);
405
564
  currentDate = this.getIterationStartDate(currentDate);
406
- let lastUpperText = "";
407
- while (this.dateToX(currentDate) < this.scrollLeft - this.pixelsPerDay * 7) {
565
+ const visibleBlocks = [];
566
+ let calcDate = new Date(currentDate);
567
+ while (calcDate <= this.visibleDateRange.end) {
408
568
  let nextDate;
569
+ let upperText = "";
409
570
  switch (this.config.viewMode) {
410
571
  case "Day":
411
- nextDate = DateUtils.addDays(currentDate, 1);
572
+ upperText = DateUtils.format(calcDate, "yyyy年MM月");
573
+ nextDate = DateUtils.addDays(calcDate, 1);
412
574
  break;
413
575
  case "Week":
414
- nextDate = DateUtils.addDays(currentDate, 7);
576
+ const weekStart = DateUtils.getStartOfWeek(calcDate);
577
+ upperText = DateUtils.format(weekStart, "yyyy年MM月");
578
+ nextDate = DateUtils.addDays(weekStart, 7);
415
579
  break;
416
580
  case "Month":
417
- nextDate = DateUtils.addMonths(currentDate, 1);
581
+ upperText = `${calcDate.getFullYear()}年`;
582
+ nextDate = DateUtils.addMonths(calcDate, 1);
418
583
  break;
419
584
  case "Year":
420
- nextDate = DateUtils.addMonths(currentDate, 6);
585
+ if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
586
+ upperText = `${calcDate.getFullYear()}年`;
587
+ nextDate = DateUtils.addMonths(calcDate, 6);
588
+ } else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
589
+ upperText = `${calcDate.getFullYear()}年`;
590
+ nextDate = DateUtils.addMonths(calcDate, 6);
591
+ } else {
592
+ calcDate = DateUtils.addDays(calcDate, 1);
593
+ continue;
594
+ }
595
+ break;
596
+ default:
597
+ nextDate = DateUtils.addDays(calcDate, 1);
598
+ break;
599
+ }
600
+ const startX = this.dateToX(calcDate);
601
+ const endX = this.dateToX(nextDate);
602
+ visibleBlocks.push({
603
+ startX,
604
+ endX,
605
+ text: upperText,
606
+ yPos: h * 0.35
607
+ });
608
+ calcDate = nextDate;
609
+ }
610
+ let currentDateForLower = new Date(currentDate);
611
+ while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
612
+ let nextDate;
613
+ switch (this.config.viewMode) {
614
+ case "Day":
615
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
616
+ break;
617
+ case "Week":
618
+ nextDate = DateUtils.addDays(currentDateForLower, 7);
619
+ break;
620
+ case "Month":
621
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
622
+ break;
623
+ case "Year":
624
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
421
625
  break;
422
626
  default:
423
- nextDate = DateUtils.addDays(currentDate, 1);
627
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
424
628
  break;
425
629
  }
426
- if (nextDate.getTime() === currentDate.getTime()) {
427
- currentDate = DateUtils.addDays(currentDate, 1);
630
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
631
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
428
632
  } else {
429
- currentDate = nextDate;
633
+ currentDateForLower = nextDate;
430
634
  }
431
635
  }
432
- while (currentDate <= this.visibleDateRange.end) {
433
- const x = this.dateToX(currentDate);
434
- let upperText = "", lowerText = "", nextDate;
636
+ const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
637
+ ctx.fillStyle = "#333";
638
+ ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
639
+ ctx.textAlign = "left";
640
+ groupedBlocks.forEach((group) => {
641
+ const visibleStart = Math.max(group.startX, this.scrollLeft);
642
+ const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
643
+ if (visibleEnd > visibleStart) {
644
+ ctx.fillStyle = this.config.headerBgColor;
645
+ ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
646
+ ctx.fillStyle = "#333";
647
+ ctx.fillText(group.text, visibleStart + 5, group.yPos);
648
+ }
649
+ });
650
+ while (currentDateForLower <= this.visibleDateRange.end) {
651
+ const x = this.dateToX(currentDateForLower);
652
+ let lowerText = "";
653
+ let nextDate;
435
654
  switch (this.config.viewMode) {
436
655
  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);
656
+ lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
657
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
440
658
  break;
441
659
  case "Week":
442
- const weekStart = DateUtils.getStartOfWeek(currentDate);
443
- upperText = DateUtils.format(weekStart, "yyyy年MM月");
660
+ const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
444
661
  lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
445
662
  nextDate = DateUtils.addDays(weekStart, 7);
446
663
  break;
447
664
  case "Month":
448
- upperText = `${currentDate.getFullYear()}年`;
449
- lowerText = `${currentDate.getMonth() + 1}月`;
450
- nextDate = DateUtils.addMonths(currentDate, 1);
665
+ lowerText = `${currentDateForLower.getMonth() + 1}月`;
666
+ nextDate = DateUtils.addMonths(currentDateForLower, 1);
451
667
  break;
452
668
  case "Year":
453
- if (currentDate.getMonth() === 0 && currentDate.getDate() === 1) {
454
- upperText = `${currentDate.getFullYear()}年`;
669
+ if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
455
670
  lowerText = `上半年`;
456
- nextDate = DateUtils.addMonths(currentDate, 6);
457
- } else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1) {
458
- upperText = `${currentDate.getFullYear()}年`;
671
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
672
+ } else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
459
673
  lowerText = `下半年`;
460
- nextDate = DateUtils.addMonths(currentDate, 6);
674
+ nextDate = DateUtils.addMonths(currentDateForLower, 6);
461
675
  } else {
462
- currentDate = DateUtils.addDays(currentDate, 1);
676
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
463
677
  continue;
464
678
  }
465
679
  break;
680
+ default:
681
+ nextDate = DateUtils.addDays(currentDateForLower, 1);
682
+ break;
466
683
  }
467
684
  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
685
  ctx.fillStyle = "#000412";
477
686
  ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
478
687
  ctx.textAlign = "center";
@@ -482,10 +691,10 @@
482
691
  ctx.lineTo(x, h);
483
692
  ctx.strokeStyle = "#e0e0e0";
484
693
  ctx.stroke();
485
- if (nextDate.getTime() === currentDate.getTime()) {
486
- currentDate = DateUtils.addDays(currentDate, 1);
694
+ if (nextDate.getTime() === currentDateForLower.getTime()) {
695
+ currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
487
696
  } else {
488
- currentDate = nextDate;
697
+ currentDateForLower = nextDate;
489
698
  }
490
699
  }
491
700
  ctx.beginPath();
@@ -495,6 +704,22 @@
495
704
  ctx.stroke();
496
705
  ctx.restore();
497
706
  }
707
+ // Helper method to group consecutive blocks with same text
708
+ groupConsecutiveBlocks(blocks) {
709
+ if (blocks.length === 0) return [];
710
+ const grouped = [];
711
+ let currentGroup = { ...blocks[0] };
712
+ for (let i = 1; i < blocks.length; i++) {
713
+ if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
714
+ currentGroup.endX = blocks[i].endX;
715
+ } else {
716
+ grouped.push(currentGroup);
717
+ currentGroup = { ...blocks[i] };
718
+ }
719
+ }
720
+ grouped.push(currentGroup);
721
+ return grouped;
722
+ }
498
723
  renderMain() {
499
724
  const ctx = this.mainCtx;
500
725
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -718,8 +943,8 @@
718
943
  pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
719
944
  ctx.lineWidth = 4;
720
945
  ctx.beginPath();
721
- ctx.moveTo(pos.x_plan_start, taskY);
722
- ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
946
+ ctx.moveTo(pos.x_plan_start + 2, taskY);
947
+ ctx.lineTo(pos.x_plan_start + width * percent_plan - 2, taskY);
723
948
  ctx.stroke();
724
949
  }
725
950
  ctx.fillStyle = "#000";
@@ -755,6 +980,9 @@
755
980
  return styles;
756
981
  }
757
982
  handleMouseMove(e) {
983
+ if (this.scrolling) {
984
+ return;
985
+ }
758
986
  const rect = this.mainCanvas.getBoundingClientRect();
759
987
  const mouseX = e.clientX - rect.left;
760
988
  const mouseY = e.clientY - rect.top;
@@ -767,15 +995,16 @@
767
995
  if (this.config.tooltipFormat) {
768
996
  const htmlStr = this.config.tooltipFormat(row, date, this.config);
769
997
  if (!htmlStr) {
770
- return this.handleMouseLeave();
998
+ this.handleMouseLeave();
999
+ return;
771
1000
  }
772
1001
  this.tooltip.innerHTML = htmlStr;
773
1002
  } else {
774
1003
  const overlappingTasks = row.tasks.filter((task) => {
775
- const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
1004
+ const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
776
1005
  if (date >= pStart && date < pEnd) return true;
777
1006
  if (task.actualStart) {
778
- const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
1007
+ const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
779
1008
  if (date >= aStart && date < aEnd) return true;
780
1009
  }
781
1010
  return false;
@@ -789,6 +1018,7 @@
789
1018
  this.tooltip.innerHTML = html;
790
1019
  }
791
1020
  this.tooltip.style.display = "block";
1021
+ this.showTooltip = true;
792
1022
  if (this.config.tooltipColor === "white") {
793
1023
  this.tooltip.style.background = "#fff";
794
1024
  this.tooltip.style.color = "#000";
@@ -818,6 +1048,7 @@
818
1048
  }
819
1049
  handleMouseLeave() {
820
1050
  this.tooltip.style.display = "none";
1051
+ this.showTooltip = false;
821
1052
  }
822
1053
  /**
823
1054
  * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
@@ -829,17 +1060,29 @@
829
1060
  return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
830
1061
  }
831
1062
  /**
832
- * scroll to specified date position, default to minDate
1063
+ * horizontal scroll to specified date position, default to minDate
833
1064
  *
834
1065
  * @param date
835
1066
  */
836
- scrollToStartDate(date) {
1067
+ horizontalScrollTo(date) {
837
1068
  const startDate = date ? date : this.minDate;
838
1069
  if (startDate) {
839
1070
  const xPosition = this.dateToX(startDate);
840
1071
  this.container.scrollTo({ left: xPosition - 80 });
841
1072
  }
842
1073
  }
1074
+ /**
1075
+ * vertical scroll to specified position
1076
+ *
1077
+ * @param params
1078
+ */
1079
+ verticalScrollTo(params) {
1080
+ if (params && (params.rowId || params.rowIndex)) {
1081
+ const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
1082
+ const yPosition = this.config.rowHeight * rowIndex;
1083
+ this.container.scrollTo({ top: yPosition - 80 });
1084
+ }
1085
+ }
843
1086
  }
844
1087
  exports2.DateUtils = DateUtils;
845
1088
  exports2.GanttChart = GanttChart;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gantt-canvas-chart",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "High performance Gantt chart component based on Canvas, can be applied to any framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.es.js",