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