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 +51 -27
- package/dist/index.css +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.es.js +51 -27
- package/dist/index.umd.js +51 -27
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.
|
|
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
|
|
373
|
-
const
|
|
374
|
-
const
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
945
|
-
ctx.lineTo(pos.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
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.
|
|
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
|
|
371
|
-
const
|
|
372
|
-
const
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
943
|
-
ctx.lineTo(pos.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
375
|
-
const
|
|
376
|
-
const
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
947
|
-
ctx.lineTo(pos.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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) {
|