gantt-canvas-chart 1.3.0 → 1.4.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.3.0
2
+ * gantt-canvas-chart v1.4.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -121,6 +121,9 @@ class GanttChart {
121
121
  onDataLoad = null;
122
122
  scrollLoadTimer = null;
123
123
  constructor(rootContainer, data, config = {}) {
124
+ if (rootContainer.querySelector(".__gantt-chart-container")) {
125
+ throw new Error("GanttChart already exists in this container");
126
+ }
124
127
  const container = document.createElement("div");
125
128
  const scrollEl = document.createElement("div");
126
129
  const headerCanvas = document.createElement("canvas");
@@ -157,6 +160,7 @@ class GanttChart {
157
160
  todayColor: "#ff4d4f",
158
161
  offsetTop: 0,
159
162
  offsetLeft: 0,
163
+ scrollEdgeThresholds: 10,
160
164
  enabledLoadMore: [],
161
165
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
162
166
  planBorderColor: "#C1EFCF",
@@ -196,6 +200,7 @@ class GanttChart {
196
200
  this.handleScroll = this.handleScroll.bind(this);
197
201
  this.horizontalScrollTo = this.horizontalScrollTo.bind(this);
198
202
  this.verticalScrollTo = this.verticalScrollTo.bind(this);
203
+ this.handleResize = this.handleResize.bind(this);
199
204
  this.init();
200
205
  }
201
206
  init() {
@@ -218,7 +223,6 @@ class GanttChart {
218
223
  }
219
224
  setupEvents() {
220
225
  this.container.addEventListener("scroll", this.handleScroll);
221
- this.handleResize = this.handleResize.bind(this);
222
226
  if (window.ResizeObserver) {
223
227
  this.resizeObserver = new ResizeObserver(this.handleResize);
224
228
  setTimeout(() => {
@@ -260,6 +264,9 @@ class GanttChart {
260
264
  this.container.removeEventListener("scroll", this.handleScroll);
261
265
  this.mainCanvas.removeEventListener("mousemove", this.handleMouseMove);
262
266
  this.mainCanvas.removeEventListener("mouseleave", this.handleMouseLeave);
267
+ this.data = [];
268
+ this.taskMap.clear();
269
+ this.taskPositions.clear();
263
270
  this.container.remove();
264
271
  }
265
272
  calculateFullTimeline() {
@@ -369,19 +376,17 @@ class GanttChart {
369
376
  const viewportHeight = this.viewportHeight;
370
377
  const totalWidth = this.totalWidth;
371
378
  const totalHeight = this.totalHeight;
372
- const atLeftEdge = scrollLeft <= 5;
373
- const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
374
- const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
379
+ const thresholds = this.config.scrollEdgeThresholds;
380
+ const atLeftEdge = scrollLeft <= thresholds;
381
+ const atRightEdge = scrollLeft + viewportWidth >= totalWidth - thresholds;
382
+ const atBottomEdge = scrollTop + viewportHeight >= totalHeight - thresholds;
375
383
  try {
376
384
  if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
377
385
  await this.loadMoreData("left");
378
- console.log("left-loadMoreData::", this.data);
379
386
  } else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
380
387
  await this.loadMoreData("right");
381
- console.log("right-loadMoreData::", this.data);
382
388
  } else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
383
389
  await this.loadMoreData("bottom");
384
- console.log("bottom-loadMoreData::", this.data);
385
390
  }
386
391
  } finally {
387
392
  this.lastScrollLeft = scrollLeft;
@@ -390,9 +395,7 @@ class GanttChart {
390
395
  }
391
396
  // Add this method to reset scroll loading state
392
397
  resetScrollLoadingState() {
393
- this.hasMoreDataLeft = true;
394
- this.hasMoreDataRight = true;
395
- this.hasMoreDataBottom = true;
398
+ this.updateLoadMoreConf();
396
399
  this.lastScrollLeft = 0;
397
400
  this.lastScrollTop = 0;
398
401
  if (this.scrollLoadTimer !== null) {
@@ -510,17 +513,45 @@ class GanttChart {
510
513
  const x_plan_start = this.dateToX(new Date(task.planStart));
511
514
  const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
512
515
  let x_actual_start = null, x_actual_end = null;
516
+ let offset_x_plan_start = NaN, offset_x_plan_end = NaN;
517
+ let offset_x_actual_start = null, offset_x_actual_end = null;
518
+ let x_plan_width = 0;
519
+ let x_actual_width = 0;
520
+ let isValidPlanTask = false, isValidActualTask = false;
521
+ const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
522
+ const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
523
+ if (x_plan_start && x_plan_end && x_plan_start < x_plan_end) {
524
+ x_plan_width = x_plan_end - x_plan_start;
525
+ offset_x_plan_start = x_plan_start + x_plan_width * offsetX;
526
+ x_plan_end && (offset_x_plan_end = offset_x_plan_start + x_plan_width * percent_plan);
527
+ isValidPlanTask = true;
528
+ }
513
529
  if (task.actualStart) {
514
530
  x_actual_start = this.dateToX(new Date(task.actualStart));
531
+ isValidActualTask = true;
532
+ }
533
+ if (!isValidPlanTask && !isValidActualTask) {
534
+ return;
515
535
  }
516
536
  if (task.actualEnd) {
517
537
  x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
518
538
  }
539
+ if (x_actual_start) {
540
+ x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
541
+ offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
542
+ x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
543
+ }
519
544
  this.taskPositions.set(task.id, {
520
545
  x_plan_start,
521
546
  x_plan_end,
522
547
  x_actual_start,
523
548
  x_actual_end,
549
+ offset_x_plan_start,
550
+ offset_x_plan_end,
551
+ offset_x_actual_start,
552
+ offset_x_actual_end,
553
+ x_plan_width,
554
+ x_actual_width,
524
555
  y: y + this.config.rowHeight * 0.5,
525
556
  row: i
526
557
  });
@@ -765,9 +796,9 @@ class GanttChart {
765
796
  if (!fromPos) return;
766
797
  const fromRowIndex = this.taskMap.get(depId).row;
767
798
  const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
768
- const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
799
+ const fromX = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
769
800
  const fromY = fromPos.y;
770
- const toX = Math.min(toPos.x_plan_start, toPos.x_actual_start || toPos.x_plan_start);
801
+ const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
771
802
  const toY = toPos.y;
772
803
  ctx.beginPath();
773
804
  if (isAdjacent) {
@@ -922,42 +953,35 @@ class GanttChart {
922
953
  }
923
954
  drawTask(ctx, task, y, pos) {
924
955
  const offset = 4;
925
- const width = pos.x_plan_end - pos.x_plan_start;
926
956
  const taskY = y + this.config.rowHeight * 0.15 + offset;
927
957
  const taskHeight = this.config.rowHeight * 0.7 - offset;
928
958
  const textY = y + this.config.rowHeight / 2 + offset;
929
959
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
930
- const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
931
960
  if (this.config.showActual && pos.x_actual_start) {
932
961
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
933
- const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
934
- pos.x_actual_start += aWidth * offsetX_actual;
935
- pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
936
- ctx.fillRect(Math.round(pos.x_actual_start), Math.round(taskY + 2), Math.round(aWidth * percent_actual), Math.round(taskHeight - 2));
962
+ ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
937
963
  }
938
964
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
939
965
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
940
- pos.x_plan_start += width * offsetX;
941
- pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
942
966
  ctx.lineWidth = 4;
943
967
  ctx.beginPath();
944
- ctx.moveTo(pos.x_plan_start + 2, taskY);
945
- ctx.lineTo(pos.x_plan_start + width * percent_plan - 2, taskY);
968
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
969
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
946
970
  ctx.stroke();
947
971
  }
948
972
  ctx.fillStyle = "#000";
949
973
  if (this.config.showLeftRemark && task.leftRemark) {
950
974
  ctx.textAlign = "right";
951
- 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));
975
+ 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));
952
976
  }
953
977
  if (this.config.showRightRemark && task.rightRemark) {
954
978
  ctx.textAlign = "left";
955
- 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));
979
+ 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));
956
980
  }
957
981
  if (this.config.showCenterRemark && task.centerRemark) {
958
- const centerX = pos.x_actual_start + (pos.x_actual_end - pos.x_actual_start) / 2;
982
+ const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
959
983
  ctx.textAlign = "center";
960
- ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.x_actual_end - pos.x_actual_start));
984
+ ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.offset_x_actual_end - pos.offset_x_actual_start));
961
985
  }
962
986
  }
963
987
  getTaskStyles(task) {
package/dist/index.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.3.0
2
+ * gantt-canvas-chart v1.4.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
package/dist/index.d.ts CHANGED
@@ -142,6 +142,7 @@ export declare interface GanttConfig {
142
142
  todayColor?: string;
143
143
  offsetTop?: number;
144
144
  offsetLeft?: number;
145
+ scrollEdgeThresholds?: number;
145
146
  enabledLoadMore?: [LoadMoreDirection?, LoadMoreDirection?, LoadMoreDirection?];
146
147
  viewFactors?: {
147
148
  Day: number;
@@ -187,6 +188,12 @@ export declare interface TaskPosition {
187
188
  x_plan_end: number;
188
189
  x_actual_start: number | null;
189
190
  x_actual_end: number | null;
191
+ x_plan_width: number;
192
+ x_actual_width: number;
193
+ offset_x_plan_start: number;
194
+ offset_x_plan_end: number;
195
+ offset_x_actual_start: number | null;
196
+ offset_x_actual_end: number | null;
190
197
  y: number;
191
198
  row: number;
192
199
  }
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.3.0
2
+ * gantt-canvas-chart v1.4.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -119,6 +119,9 @@ class GanttChart {
119
119
  onDataLoad = null;
120
120
  scrollLoadTimer = null;
121
121
  constructor(rootContainer, data, config = {}) {
122
+ if (rootContainer.querySelector(".__gantt-chart-container")) {
123
+ throw new Error("GanttChart already exists in this container");
124
+ }
122
125
  const container = document.createElement("div");
123
126
  const scrollEl = document.createElement("div");
124
127
  const headerCanvas = document.createElement("canvas");
@@ -155,6 +158,7 @@ class GanttChart {
155
158
  todayColor: "#ff4d4f",
156
159
  offsetTop: 0,
157
160
  offsetLeft: 0,
161
+ scrollEdgeThresholds: 10,
158
162
  enabledLoadMore: [],
159
163
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
160
164
  planBorderColor: "#C1EFCF",
@@ -194,6 +198,7 @@ class GanttChart {
194
198
  this.handleScroll = this.handleScroll.bind(this);
195
199
  this.horizontalScrollTo = this.horizontalScrollTo.bind(this);
196
200
  this.verticalScrollTo = this.verticalScrollTo.bind(this);
201
+ this.handleResize = this.handleResize.bind(this);
197
202
  this.init();
198
203
  }
199
204
  init() {
@@ -216,7 +221,6 @@ class GanttChart {
216
221
  }
217
222
  setupEvents() {
218
223
  this.container.addEventListener("scroll", this.handleScroll);
219
- this.handleResize = this.handleResize.bind(this);
220
224
  if (window.ResizeObserver) {
221
225
  this.resizeObserver = new ResizeObserver(this.handleResize);
222
226
  setTimeout(() => {
@@ -258,6 +262,9 @@ class GanttChart {
258
262
  this.container.removeEventListener("scroll", this.handleScroll);
259
263
  this.mainCanvas.removeEventListener("mousemove", this.handleMouseMove);
260
264
  this.mainCanvas.removeEventListener("mouseleave", this.handleMouseLeave);
265
+ this.data = [];
266
+ this.taskMap.clear();
267
+ this.taskPositions.clear();
261
268
  this.container.remove();
262
269
  }
263
270
  calculateFullTimeline() {
@@ -367,19 +374,17 @@ class GanttChart {
367
374
  const viewportHeight = this.viewportHeight;
368
375
  const totalWidth = this.totalWidth;
369
376
  const totalHeight = this.totalHeight;
370
- const atLeftEdge = scrollLeft <= 5;
371
- const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
372
- const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
377
+ const thresholds = this.config.scrollEdgeThresholds;
378
+ const atLeftEdge = scrollLeft <= thresholds;
379
+ const atRightEdge = scrollLeft + viewportWidth >= totalWidth - thresholds;
380
+ const atBottomEdge = scrollTop + viewportHeight >= totalHeight - thresholds;
373
381
  try {
374
382
  if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
375
383
  await this.loadMoreData("left");
376
- console.log("left-loadMoreData::", this.data);
377
384
  } else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
378
385
  await this.loadMoreData("right");
379
- console.log("right-loadMoreData::", this.data);
380
386
  } else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
381
387
  await this.loadMoreData("bottom");
382
- console.log("bottom-loadMoreData::", this.data);
383
388
  }
384
389
  } finally {
385
390
  this.lastScrollLeft = scrollLeft;
@@ -388,9 +393,7 @@ class GanttChart {
388
393
  }
389
394
  // Add this method to reset scroll loading state
390
395
  resetScrollLoadingState() {
391
- this.hasMoreDataLeft = true;
392
- this.hasMoreDataRight = true;
393
- this.hasMoreDataBottom = true;
396
+ this.updateLoadMoreConf();
394
397
  this.lastScrollLeft = 0;
395
398
  this.lastScrollTop = 0;
396
399
  if (this.scrollLoadTimer !== null) {
@@ -508,17 +511,45 @@ class GanttChart {
508
511
  const x_plan_start = this.dateToX(new Date(task.planStart));
509
512
  const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
510
513
  let x_actual_start = null, x_actual_end = null;
514
+ let offset_x_plan_start = NaN, offset_x_plan_end = NaN;
515
+ let offset_x_actual_start = null, offset_x_actual_end = null;
516
+ let x_plan_width = 0;
517
+ let x_actual_width = 0;
518
+ let isValidPlanTask = false, isValidActualTask = false;
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 && 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
+ isValidPlanTask = true;
526
+ }
511
527
  if (task.actualStart) {
512
528
  x_actual_start = this.dateToX(new Date(task.actualStart));
529
+ isValidActualTask = true;
530
+ }
531
+ if (!isValidPlanTask && !isValidActualTask) {
532
+ return;
513
533
  }
514
534
  if (task.actualEnd) {
515
535
  x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
516
536
  }
537
+ if (x_actual_start) {
538
+ x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
539
+ offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
540
+ x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
541
+ }
517
542
  this.taskPositions.set(task.id, {
518
543
  x_plan_start,
519
544
  x_plan_end,
520
545
  x_actual_start,
521
546
  x_actual_end,
547
+ offset_x_plan_start,
548
+ offset_x_plan_end,
549
+ offset_x_actual_start,
550
+ offset_x_actual_end,
551
+ x_plan_width,
552
+ x_actual_width,
522
553
  y: y + this.config.rowHeight * 0.5,
523
554
  row: i
524
555
  });
@@ -763,9 +794,9 @@ class GanttChart {
763
794
  if (!fromPos) return;
764
795
  const fromRowIndex = this.taskMap.get(depId).row;
765
796
  const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
766
- const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
797
+ const fromX = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
767
798
  const fromY = fromPos.y;
768
- const toX = Math.min(toPos.x_plan_start, toPos.x_actual_start || toPos.x_plan_start);
799
+ const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
769
800
  const toY = toPos.y;
770
801
  ctx.beginPath();
771
802
  if (isAdjacent) {
@@ -920,42 +951,35 @@ class GanttChart {
920
951
  }
921
952
  drawTask(ctx, task, y, pos) {
922
953
  const offset = 4;
923
- const width = pos.x_plan_end - pos.x_plan_start;
924
954
  const taskY = y + this.config.rowHeight * 0.15 + offset;
925
955
  const taskHeight = this.config.rowHeight * 0.7 - offset;
926
956
  const textY = y + this.config.rowHeight / 2 + offset;
927
957
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
928
- const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
929
958
  if (this.config.showActual && pos.x_actual_start) {
930
959
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
931
- const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
932
- pos.x_actual_start += aWidth * offsetX_actual;
933
- pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
934
- ctx.fillRect(Math.round(pos.x_actual_start), Math.round(taskY + 2), Math.round(aWidth * percent_actual), Math.round(taskHeight - 2));
960
+ ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
935
961
  }
936
962
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
937
963
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
938
- pos.x_plan_start += width * offsetX;
939
- pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
940
964
  ctx.lineWidth = 4;
941
965
  ctx.beginPath();
942
- ctx.moveTo(pos.x_plan_start + 2, taskY);
943
- ctx.lineTo(pos.x_plan_start + width * percent_plan - 2, taskY);
966
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
967
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
944
968
  ctx.stroke();
945
969
  }
946
970
  ctx.fillStyle = "#000";
947
971
  if (this.config.showLeftRemark && task.leftRemark) {
948
972
  ctx.textAlign = "right";
949
- 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));
973
+ 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));
950
974
  }
951
975
  if (this.config.showRightRemark && task.rightRemark) {
952
976
  ctx.textAlign = "left";
953
- 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));
977
+ 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));
954
978
  }
955
979
  if (this.config.showCenterRemark && task.centerRemark) {
956
- const centerX = pos.x_actual_start + (pos.x_actual_end - pos.x_actual_start) / 2;
980
+ const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
957
981
  ctx.textAlign = "center";
958
- ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.x_actual_end - pos.x_actual_start));
982
+ ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.offset_x_actual_end - pos.offset_x_actual_start));
959
983
  }
960
984
  }
961
985
  getTaskStyles(task) {
package/dist/index.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.3.0
2
+ * gantt-canvas-chart v1.4.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -123,6 +123,9 @@
123
123
  onDataLoad = null;
124
124
  scrollLoadTimer = null;
125
125
  constructor(rootContainer, data, config = {}) {
126
+ if (rootContainer.querySelector(".__gantt-chart-container")) {
127
+ throw new Error("GanttChart already exists in this container");
128
+ }
126
129
  const container = document.createElement("div");
127
130
  const scrollEl = document.createElement("div");
128
131
  const headerCanvas = document.createElement("canvas");
@@ -159,6 +162,7 @@
159
162
  todayColor: "#ff4d4f",
160
163
  offsetTop: 0,
161
164
  offsetLeft: 0,
165
+ scrollEdgeThresholds: 10,
162
166
  enabledLoadMore: [],
163
167
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
164
168
  planBorderColor: "#C1EFCF",
@@ -198,6 +202,7 @@
198
202
  this.handleScroll = this.handleScroll.bind(this);
199
203
  this.horizontalScrollTo = this.horizontalScrollTo.bind(this);
200
204
  this.verticalScrollTo = this.verticalScrollTo.bind(this);
205
+ this.handleResize = this.handleResize.bind(this);
201
206
  this.init();
202
207
  }
203
208
  init() {
@@ -220,7 +225,6 @@
220
225
  }
221
226
  setupEvents() {
222
227
  this.container.addEventListener("scroll", this.handleScroll);
223
- this.handleResize = this.handleResize.bind(this);
224
228
  if (window.ResizeObserver) {
225
229
  this.resizeObserver = new ResizeObserver(this.handleResize);
226
230
  setTimeout(() => {
@@ -262,6 +266,9 @@
262
266
  this.container.removeEventListener("scroll", this.handleScroll);
263
267
  this.mainCanvas.removeEventListener("mousemove", this.handleMouseMove);
264
268
  this.mainCanvas.removeEventListener("mouseleave", this.handleMouseLeave);
269
+ this.data = [];
270
+ this.taskMap.clear();
271
+ this.taskPositions.clear();
265
272
  this.container.remove();
266
273
  }
267
274
  calculateFullTimeline() {
@@ -371,19 +378,17 @@
371
378
  const viewportHeight = this.viewportHeight;
372
379
  const totalWidth = this.totalWidth;
373
380
  const totalHeight = this.totalHeight;
374
- const atLeftEdge = scrollLeft <= 5;
375
- const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
376
- const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
381
+ const thresholds = this.config.scrollEdgeThresholds;
382
+ const atLeftEdge = scrollLeft <= thresholds;
383
+ const atRightEdge = scrollLeft + viewportWidth >= totalWidth - thresholds;
384
+ const atBottomEdge = scrollTop + viewportHeight >= totalHeight - thresholds;
377
385
  try {
378
386
  if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
379
387
  await this.loadMoreData("left");
380
- console.log("left-loadMoreData::", this.data);
381
388
  } else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
382
389
  await this.loadMoreData("right");
383
- console.log("right-loadMoreData::", this.data);
384
390
  } else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
385
391
  await this.loadMoreData("bottom");
386
- console.log("bottom-loadMoreData::", this.data);
387
392
  }
388
393
  } finally {
389
394
  this.lastScrollLeft = scrollLeft;
@@ -392,9 +397,7 @@
392
397
  }
393
398
  // Add this method to reset scroll loading state
394
399
  resetScrollLoadingState() {
395
- this.hasMoreDataLeft = true;
396
- this.hasMoreDataRight = true;
397
- this.hasMoreDataBottom = true;
400
+ this.updateLoadMoreConf();
398
401
  this.lastScrollLeft = 0;
399
402
  this.lastScrollTop = 0;
400
403
  if (this.scrollLoadTimer !== null) {
@@ -512,17 +515,45 @@
512
515
  const x_plan_start = this.dateToX(new Date(task.planStart));
513
516
  const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
514
517
  let x_actual_start = null, x_actual_end = null;
518
+ let offset_x_plan_start = NaN, offset_x_plan_end = NaN;
519
+ let offset_x_actual_start = null, offset_x_actual_end = null;
520
+ let x_plan_width = 0;
521
+ let x_actual_width = 0;
522
+ let isValidPlanTask = false, isValidActualTask = false;
523
+ const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
524
+ const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
525
+ if (x_plan_start && x_plan_end && x_plan_start < x_plan_end) {
526
+ x_plan_width = x_plan_end - x_plan_start;
527
+ offset_x_plan_start = x_plan_start + x_plan_width * offsetX;
528
+ x_plan_end && (offset_x_plan_end = offset_x_plan_start + x_plan_width * percent_plan);
529
+ isValidPlanTask = true;
530
+ }
515
531
  if (task.actualStart) {
516
532
  x_actual_start = this.dateToX(new Date(task.actualStart));
533
+ isValidActualTask = true;
534
+ }
535
+ if (!isValidPlanTask && !isValidActualTask) {
536
+ return;
517
537
  }
518
538
  if (task.actualEnd) {
519
539
  x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
520
540
  }
541
+ if (x_actual_start) {
542
+ x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
543
+ offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
544
+ x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
545
+ }
521
546
  this.taskPositions.set(task.id, {
522
547
  x_plan_start,
523
548
  x_plan_end,
524
549
  x_actual_start,
525
550
  x_actual_end,
551
+ offset_x_plan_start,
552
+ offset_x_plan_end,
553
+ offset_x_actual_start,
554
+ offset_x_actual_end,
555
+ x_plan_width,
556
+ x_actual_width,
526
557
  y: y + this.config.rowHeight * 0.5,
527
558
  row: i
528
559
  });
@@ -767,9 +798,9 @@
767
798
  if (!fromPos) return;
768
799
  const fromRowIndex = this.taskMap.get(depId).row;
769
800
  const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
770
- const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
801
+ const fromX = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
771
802
  const fromY = fromPos.y;
772
- const toX = Math.min(toPos.x_plan_start, toPos.x_actual_start || toPos.x_plan_start);
803
+ const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
773
804
  const toY = toPos.y;
774
805
  ctx.beginPath();
775
806
  if (isAdjacent) {
@@ -924,42 +955,35 @@
924
955
  }
925
956
  drawTask(ctx, task, y, pos) {
926
957
  const offset = 4;
927
- const width = pos.x_plan_end - pos.x_plan_start;
928
958
  const taskY = y + this.config.rowHeight * 0.15 + offset;
929
959
  const taskHeight = this.config.rowHeight * 0.7 - offset;
930
960
  const textY = y + this.config.rowHeight / 2 + offset;
931
961
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
932
- const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
933
962
  if (this.config.showActual && pos.x_actual_start) {
934
963
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
935
- const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
936
- pos.x_actual_start += aWidth * offsetX_actual;
937
- pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
938
- ctx.fillRect(Math.round(pos.x_actual_start), Math.round(taskY + 2), Math.round(aWidth * percent_actual), Math.round(taskHeight - 2));
964
+ ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
939
965
  }
940
966
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
941
967
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
942
- pos.x_plan_start += width * offsetX;
943
- pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
944
968
  ctx.lineWidth = 4;
945
969
  ctx.beginPath();
946
- ctx.moveTo(pos.x_plan_start + 2, taskY);
947
- ctx.lineTo(pos.x_plan_start + width * percent_plan - 2, taskY);
970
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
971
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
948
972
  ctx.stroke();
949
973
  }
950
974
  ctx.fillStyle = "#000";
951
975
  if (this.config.showLeftRemark && task.leftRemark) {
952
976
  ctx.textAlign = "right";
953
- 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));
977
+ 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));
954
978
  }
955
979
  if (this.config.showRightRemark && task.rightRemark) {
956
980
  ctx.textAlign = "left";
957
- 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));
981
+ 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));
958
982
  }
959
983
  if (this.config.showCenterRemark && task.centerRemark) {
960
- const centerX = pos.x_actual_start + (pos.x_actual_end - pos.x_actual_start) / 2;
984
+ const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
961
985
  ctx.textAlign = "center";
962
- ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.x_actual_end - pos.x_actual_start));
986
+ ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.offset_x_actual_end - pos.offset_x_actual_start));
963
987
  }
964
988
  }
965
989
  getTaskStyles(task) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gantt-canvas-chart",
3
- "version": "1.3.0",
3
+ "version": "1.4.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",