gantt-canvas-chart 1.5.1 → 1.5.3
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 +103 -23
- package/dist/index.css +1 -1
- package/dist/index.d.ts +40 -2
- package/dist/index.es.js +104 -24
- package/dist/index.umd.js +103 -23
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.5.
|
|
2
|
+
* gantt-canvas-chart v1.5.3
|
|
3
3
|
* (c) 2025-present chandq
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -82,6 +82,40 @@ function firstValidValue(...args) {
|
|
|
82
82
|
}
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
|
+
function getMinMax(values) {
|
|
86
|
+
const validNumbers = values.filter((value) => {
|
|
87
|
+
return typeof value === "number" && !isNaN(value) && value !== Infinity && value !== -Infinity;
|
|
88
|
+
});
|
|
89
|
+
if (validNumbers.length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
min: Math.min(...validNumbers),
|
|
94
|
+
max: Math.max(...validNumbers)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function getMinMaxOptimized(values) {
|
|
98
|
+
let min = Infinity;
|
|
99
|
+
let max = -Infinity;
|
|
100
|
+
let hasValidValue = false;
|
|
101
|
+
for (let i = 0; i < values.length; i++) {
|
|
102
|
+
const value = values[i];
|
|
103
|
+
if (typeof value === "number" && !isNaN(value) && value !== Infinity && value !== -Infinity) {
|
|
104
|
+
hasValidValue = true;
|
|
105
|
+
if (value < min) min = value;
|
|
106
|
+
if (value > max) max = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return hasValidValue ? { min, max } : null;
|
|
110
|
+
}
|
|
111
|
+
function dateToStart(d) {
|
|
112
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
|
113
|
+
}
|
|
114
|
+
function dateToEnd(value) {
|
|
115
|
+
const d = dateToStart(value);
|
|
116
|
+
d.setDate(d.getDate() + 1);
|
|
117
|
+
return new Date(d.getTime() - 1);
|
|
118
|
+
}
|
|
85
119
|
class GanttChart {
|
|
86
120
|
rootContainer;
|
|
87
121
|
container;
|
|
@@ -121,7 +155,9 @@ class GanttChart {
|
|
|
121
155
|
onDataLoad = null;
|
|
122
156
|
scrollLoadTimer = null;
|
|
123
157
|
constructor(rootContainer, data, config = {}) {
|
|
124
|
-
if (rootContainer
|
|
158
|
+
if (!rootContainer) {
|
|
159
|
+
throw new Error("Root container element is required");
|
|
160
|
+
} else if (rootContainer.querySelector(".__gantt-chart-container")) {
|
|
125
161
|
throw new Error("GanttChart already exists in this container");
|
|
126
162
|
}
|
|
127
163
|
const container = document.createElement("div");
|
|
@@ -217,8 +253,18 @@ class GanttChart {
|
|
|
217
253
|
}
|
|
218
254
|
buildTaskMap() {
|
|
219
255
|
this.taskMap.clear();
|
|
220
|
-
|
|
221
|
-
|
|
256
|
+
let visibleRowIndex = 0;
|
|
257
|
+
this.data.forEach((row) => {
|
|
258
|
+
row.tasks.forEach((task) => {
|
|
259
|
+
this.taskMap.set(task.id, {
|
|
260
|
+
row: row.hide ? -1 : visibleRowIndex,
|
|
261
|
+
// Use -1 for hidden rows
|
|
262
|
+
task
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
if (!row.hide) {
|
|
266
|
+
visibleRowIndex++;
|
|
267
|
+
}
|
|
222
268
|
});
|
|
223
269
|
}
|
|
224
270
|
setupEvents() {
|
|
@@ -487,10 +533,12 @@ class GanttChart {
|
|
|
487
533
|
end: this.xToDate(this.scrollLeft + this.viewportWidth + buffer)
|
|
488
534
|
};
|
|
489
535
|
}
|
|
536
|
+
// Update the updateDimensions method to calculate height based on visible rows
|
|
490
537
|
updateDimensions() {
|
|
491
538
|
const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
|
|
539
|
+
const visibleRowCount = this.data.filter((row) => !row.hide).length;
|
|
492
540
|
const newTotalWidth = totalDays * this.pixelsPerDay;
|
|
493
|
-
const newTotalHeight =
|
|
541
|
+
const newTotalHeight = visibleRowCount * this.config.rowHeight + this.config.headerHeight;
|
|
494
542
|
if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
|
|
495
543
|
this.totalWidth = newTotalWidth;
|
|
496
544
|
this.totalHeight = newTotalHeight;
|
|
@@ -509,12 +557,13 @@ class GanttChart {
|
|
|
509
557
|
}
|
|
510
558
|
calculateAllTaskPositions() {
|
|
511
559
|
this.taskPositions.clear();
|
|
512
|
-
|
|
560
|
+
let visibleRowIndex = 0;
|
|
561
|
+
for (let i = 0, len = this.data.length; i < len; i++) {
|
|
513
562
|
const row = this.data[i];
|
|
514
563
|
if (row.hide) {
|
|
515
564
|
continue;
|
|
516
565
|
}
|
|
517
|
-
const y =
|
|
566
|
+
const y = visibleRowIndex * this.config.rowHeight;
|
|
518
567
|
row.tasks.forEach((task) => {
|
|
519
568
|
if (task.hide) {
|
|
520
569
|
return;
|
|
@@ -522,7 +571,7 @@ class GanttChart {
|
|
|
522
571
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
523
572
|
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
524
573
|
let x_actual_start = null, x_actual_end = null;
|
|
525
|
-
let offset_x_plan_start =
|
|
574
|
+
let offset_x_plan_start = null, offset_x_plan_end = null;
|
|
526
575
|
let offset_x_actual_start = null, offset_x_actual_end = null;
|
|
527
576
|
let x_plan_width = 0;
|
|
528
577
|
let x_actual_width = 0;
|
|
@@ -558,9 +607,11 @@ class GanttChart {
|
|
|
558
607
|
x_plan_width,
|
|
559
608
|
x_actual_width,
|
|
560
609
|
y: y + this.config.rowHeight * 0.5,
|
|
561
|
-
row:
|
|
610
|
+
row: visibleRowIndex
|
|
611
|
+
// Use visible row index instead of original index
|
|
562
612
|
});
|
|
563
613
|
});
|
|
614
|
+
visibleRowIndex++;
|
|
564
615
|
}
|
|
565
616
|
}
|
|
566
617
|
getIterationStartDate(date) {
|
|
@@ -801,9 +852,9 @@ class GanttChart {
|
|
|
801
852
|
if (!fromPos) return;
|
|
802
853
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
803
854
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
804
|
-
const fromX =
|
|
855
|
+
const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
|
|
805
856
|
const fromY = fromPos.y;
|
|
806
|
-
const toX =
|
|
857
|
+
const toX = getMinMax([toPos.offset_x_actual_start, toPos.offset_x_plan_start])?.min;
|
|
807
858
|
const toY = toPos.y;
|
|
808
859
|
ctx.beginPath();
|
|
809
860
|
if (isAdjacent) {
|
|
@@ -850,10 +901,17 @@ class GanttChart {
|
|
|
850
901
|
ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
851
902
|
ctx.textRendering = "optimizeSpeed";
|
|
852
903
|
ctx.imageSmoothingEnabled = false;
|
|
904
|
+
let visibleRowIndex = 0;
|
|
853
905
|
for (let i = 0; i < this.data.length; i++) {
|
|
854
906
|
const row = this.data[i];
|
|
855
|
-
|
|
856
|
-
|
|
907
|
+
if (row.hide) {
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const y = visibleRowIndex * this.config.rowHeight;
|
|
911
|
+
if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) {
|
|
912
|
+
visibleRowIndex++;
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
857
915
|
row.tasks.forEach((task) => {
|
|
858
916
|
const pos = this.taskPositions.get(task.id);
|
|
859
917
|
if (!pos) return;
|
|
@@ -862,14 +920,22 @@ class GanttChart {
|
|
|
862
920
|
if (!isPlanVisible && !isActualVisible) return;
|
|
863
921
|
this.drawTask(ctx, task, y, pos);
|
|
864
922
|
});
|
|
923
|
+
visibleRowIndex++;
|
|
865
924
|
}
|
|
866
925
|
}
|
|
926
|
+
// In the drawGrid method
|
|
867
927
|
drawGrid(ctx, startDate, endDate) {
|
|
868
928
|
ctx.strokeStyle = "#e6e6e6";
|
|
869
929
|
ctx.lineWidth = 1;
|
|
870
930
|
ctx.beginPath();
|
|
871
931
|
if (this.config.showRowLines) {
|
|
872
|
-
|
|
932
|
+
let visibleRowCount = 0;
|
|
933
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
934
|
+
if (!this.data[i].hide) {
|
|
935
|
+
visibleRowCount++;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (let i = 0; i <= visibleRowCount; i++) {
|
|
873
939
|
const y = i * this.config.rowHeight;
|
|
874
940
|
if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
|
|
875
941
|
ctx.moveTo(this.scrollLeft, y);
|
|
@@ -976,11 +1042,11 @@ class GanttChart {
|
|
|
976
1042
|
ctx.fillStyle = "#000";
|
|
977
1043
|
if (this.config.showLeftRemark && task.leftRemark) {
|
|
978
1044
|
ctx.textAlign = "right";
|
|
979
|
-
ctx.fillText(task.leftRemark, Math.round(
|
|
1045
|
+
ctx.fillText(task.leftRemark, Math.round(getMinMax([pos.offset_x_plan_start, pos.offset_x_actual_start])?.min - 8 * 2), Math.round(textY));
|
|
980
1046
|
}
|
|
981
1047
|
if (this.config.showRightRemark && task.rightRemark) {
|
|
982
1048
|
ctx.textAlign = "left";
|
|
983
|
-
ctx.fillText(task.rightRemark, Math.round(
|
|
1049
|
+
ctx.fillText(task.rightRemark, Math.round(getMinMax([pos.offset_x_plan_end, pos.offset_x_actual_end])?.max + 8 * 2), Math.round(textY));
|
|
984
1050
|
}
|
|
985
1051
|
if (this.config.showCenterRemark && task.centerRemark) {
|
|
986
1052
|
const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
|
|
@@ -1014,10 +1080,21 @@ class GanttChart {
|
|
|
1014
1080
|
const mouseY = e.clientY - rect.top;
|
|
1015
1081
|
const chartX = mouseX + this.scrollLeft;
|
|
1016
1082
|
const chartY = mouseY + this.scrollTop;
|
|
1017
|
-
const
|
|
1083
|
+
const visibleRowIndex = Math.floor(chartY / this.config.rowHeight);
|
|
1084
|
+
let actualRowIndex = -1;
|
|
1085
|
+
let visibleRowCount = 0;
|
|
1086
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
1087
|
+
if (!this.data[i].hide) {
|
|
1088
|
+
if (visibleRowCount === visibleRowIndex) {
|
|
1089
|
+
actualRowIndex = i;
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
visibleRowCount++;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1018
1095
|
const date = this.xToDate(chartX);
|
|
1019
|
-
if (
|
|
1020
|
-
const row = this.data[
|
|
1096
|
+
if (actualRowIndex < 0 || actualRowIndex >= this.data.length) return this.handleMouseLeave();
|
|
1097
|
+
const row = this.data[actualRowIndex];
|
|
1021
1098
|
if (this.config.tooltipFormat) {
|
|
1022
1099
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
1023
1100
|
if (!htmlStr) {
|
|
@@ -1027,11 +1104,10 @@ class GanttChart {
|
|
|
1027
1104
|
this.tooltip.innerHTML = htmlStr;
|
|
1028
1105
|
} else {
|
|
1029
1106
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
1030
|
-
|
|
1031
|
-
if (date >= pStart && date < pEnd) return true;
|
|
1107
|
+
if (task.planStart && task.planEnd && date > dateToStart(new Date(task.planStart)) && date <= dateToEnd(new Date(task.planEnd))) return true;
|
|
1032
1108
|
if (task.actualStart) {
|
|
1033
|
-
const
|
|
1034
|
-
if (date >=
|
|
1109
|
+
const aEnd = task.actualEnd ? new Date(task.actualEnd) : this.today;
|
|
1110
|
+
if (date >= dateToStart(new Date(task.actualStart)) && date <= dateToEnd(aEnd)) return true;
|
|
1035
1111
|
}
|
|
1036
1112
|
return false;
|
|
1037
1113
|
});
|
|
@@ -1112,4 +1188,8 @@ class GanttChart {
|
|
|
1112
1188
|
}
|
|
1113
1189
|
exports.DateUtils = DateUtils;
|
|
1114
1190
|
exports.GanttChart = GanttChart;
|
|
1191
|
+
exports.dateToEnd = dateToEnd;
|
|
1192
|
+
exports.dateToStart = dateToStart;
|
|
1115
1193
|
exports.firstValidValue = firstValidValue;
|
|
1194
|
+
exports.getMinMax = getMinMax;
|
|
1195
|
+
exports.getMinMaxOptimized = getMinMaxOptimized;
|
package/dist/index.css
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将日期转换为一天的结束时间,即23点59分59秒999毫秒
|
|
3
|
+
* @param {Date} value
|
|
4
|
+
* @returns {Date}
|
|
5
|
+
*/
|
|
6
|
+
export declare function dateToEnd(value: Date): Date;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 将日期转换为一天的开始时间,即0点0分0秒0毫秒
|
|
10
|
+
* @param {Date} value
|
|
11
|
+
* @returns {Date}
|
|
12
|
+
*/
|
|
13
|
+
export declare function dateToStart(d: Date): Date;
|
|
14
|
+
|
|
1
15
|
export declare class DateUtils {
|
|
2
16
|
static readonly ONE_DAY_MS: number;
|
|
3
17
|
static format(date: Date, format?: string): string;
|
|
@@ -176,6 +190,30 @@ export declare interface GanttConfig {
|
|
|
176
190
|
|
|
177
191
|
export declare type GanttData = Row[];
|
|
178
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Finds the minimum and maximum values from a series of numbers,
|
|
195
|
+
* automatically filtering out invalid values such as undefined, null, and NaN.
|
|
196
|
+
*
|
|
197
|
+
* @param values - An array of values to process
|
|
198
|
+
* @returns An object containing min and max values, or null if no valid values exist
|
|
199
|
+
*/
|
|
200
|
+
export declare function getMinMax(values: any[]): {
|
|
201
|
+
min: number;
|
|
202
|
+
max: number;
|
|
203
|
+
} | null;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Finds the minimum and maximum values from a series of numbers,for very large datasets using a single-pass algorithm
|
|
207
|
+
* automatically filtering out invalid values such as undefined, null, and NaN.
|
|
208
|
+
*
|
|
209
|
+
* @param values - An array of values to process
|
|
210
|
+
* @returns An object containing min and max values, or null if no valid values exist
|
|
211
|
+
*/
|
|
212
|
+
export declare function getMinMaxOptimized(values: any[]): {
|
|
213
|
+
min: number;
|
|
214
|
+
max: number;
|
|
215
|
+
} | null;
|
|
216
|
+
|
|
179
217
|
export declare type LoadMoreDirection = 'left' | 'right' | 'bottom';
|
|
180
218
|
|
|
181
219
|
export declare interface Row {
|
|
@@ -213,8 +251,8 @@ export declare interface TaskPosition {
|
|
|
213
251
|
x_actual_end: number | null;
|
|
214
252
|
x_plan_width: number;
|
|
215
253
|
x_actual_width: number;
|
|
216
|
-
offset_x_plan_start: number;
|
|
217
|
-
offset_x_plan_end: number;
|
|
254
|
+
offset_x_plan_start: number | null;
|
|
255
|
+
offset_x_plan_end: number | null;
|
|
218
256
|
offset_x_actual_start: number | null;
|
|
219
257
|
offset_x_actual_end: number | null;
|
|
220
258
|
y: number;
|
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.5.
|
|
2
|
+
* gantt-canvas-chart v1.5.3
|
|
3
3
|
* (c) 2025-present chandq
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -80,6 +80,40 @@ function firstValidValue(...args) {
|
|
|
80
80
|
}
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
|
+
function getMinMax(values) {
|
|
84
|
+
const validNumbers = values.filter((value) => {
|
|
85
|
+
return typeof value === "number" && !isNaN(value) && value !== Infinity && value !== -Infinity;
|
|
86
|
+
});
|
|
87
|
+
if (validNumbers.length === 0) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
min: Math.min(...validNumbers),
|
|
92
|
+
max: Math.max(...validNumbers)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function getMinMaxOptimized(values) {
|
|
96
|
+
let min = Infinity;
|
|
97
|
+
let max = -Infinity;
|
|
98
|
+
let hasValidValue = false;
|
|
99
|
+
for (let i = 0; i < values.length; i++) {
|
|
100
|
+
const value = values[i];
|
|
101
|
+
if (typeof value === "number" && !isNaN(value) && value !== Infinity && value !== -Infinity) {
|
|
102
|
+
hasValidValue = true;
|
|
103
|
+
if (value < min) min = value;
|
|
104
|
+
if (value > max) max = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return hasValidValue ? { min, max } : null;
|
|
108
|
+
}
|
|
109
|
+
function dateToStart(d) {
|
|
110
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
|
111
|
+
}
|
|
112
|
+
function dateToEnd(value) {
|
|
113
|
+
const d = dateToStart(value);
|
|
114
|
+
d.setDate(d.getDate() + 1);
|
|
115
|
+
return new Date(d.getTime() - 1);
|
|
116
|
+
}
|
|
83
117
|
class GanttChart {
|
|
84
118
|
rootContainer;
|
|
85
119
|
container;
|
|
@@ -119,7 +153,9 @@ class GanttChart {
|
|
|
119
153
|
onDataLoad = null;
|
|
120
154
|
scrollLoadTimer = null;
|
|
121
155
|
constructor(rootContainer, data, config = {}) {
|
|
122
|
-
if (rootContainer
|
|
156
|
+
if (!rootContainer) {
|
|
157
|
+
throw new Error("Root container element is required");
|
|
158
|
+
} else if (rootContainer.querySelector(".__gantt-chart-container")) {
|
|
123
159
|
throw new Error("GanttChart already exists in this container");
|
|
124
160
|
}
|
|
125
161
|
const container = document.createElement("div");
|
|
@@ -215,8 +251,18 @@ class GanttChart {
|
|
|
215
251
|
}
|
|
216
252
|
buildTaskMap() {
|
|
217
253
|
this.taskMap.clear();
|
|
218
|
-
|
|
219
|
-
|
|
254
|
+
let visibleRowIndex = 0;
|
|
255
|
+
this.data.forEach((row) => {
|
|
256
|
+
row.tasks.forEach((task) => {
|
|
257
|
+
this.taskMap.set(task.id, {
|
|
258
|
+
row: row.hide ? -1 : visibleRowIndex,
|
|
259
|
+
// Use -1 for hidden rows
|
|
260
|
+
task
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
if (!row.hide) {
|
|
264
|
+
visibleRowIndex++;
|
|
265
|
+
}
|
|
220
266
|
});
|
|
221
267
|
}
|
|
222
268
|
setupEvents() {
|
|
@@ -485,10 +531,12 @@ class GanttChart {
|
|
|
485
531
|
end: this.xToDate(this.scrollLeft + this.viewportWidth + buffer)
|
|
486
532
|
};
|
|
487
533
|
}
|
|
534
|
+
// Update the updateDimensions method to calculate height based on visible rows
|
|
488
535
|
updateDimensions() {
|
|
489
536
|
const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
|
|
537
|
+
const visibleRowCount = this.data.filter((row) => !row.hide).length;
|
|
490
538
|
const newTotalWidth = totalDays * this.pixelsPerDay;
|
|
491
|
-
const newTotalHeight =
|
|
539
|
+
const newTotalHeight = visibleRowCount * this.config.rowHeight + this.config.headerHeight;
|
|
492
540
|
if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
|
|
493
541
|
this.totalWidth = newTotalWidth;
|
|
494
542
|
this.totalHeight = newTotalHeight;
|
|
@@ -507,12 +555,13 @@ class GanttChart {
|
|
|
507
555
|
}
|
|
508
556
|
calculateAllTaskPositions() {
|
|
509
557
|
this.taskPositions.clear();
|
|
510
|
-
|
|
558
|
+
let visibleRowIndex = 0;
|
|
559
|
+
for (let i = 0, len = this.data.length; i < len; i++) {
|
|
511
560
|
const row = this.data[i];
|
|
512
561
|
if (row.hide) {
|
|
513
562
|
continue;
|
|
514
563
|
}
|
|
515
|
-
const y =
|
|
564
|
+
const y = visibleRowIndex * this.config.rowHeight;
|
|
516
565
|
row.tasks.forEach((task) => {
|
|
517
566
|
if (task.hide) {
|
|
518
567
|
return;
|
|
@@ -520,7 +569,7 @@ class GanttChart {
|
|
|
520
569
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
521
570
|
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
522
571
|
let x_actual_start = null, x_actual_end = null;
|
|
523
|
-
let offset_x_plan_start =
|
|
572
|
+
let offset_x_plan_start = null, offset_x_plan_end = null;
|
|
524
573
|
let offset_x_actual_start = null, offset_x_actual_end = null;
|
|
525
574
|
let x_plan_width = 0;
|
|
526
575
|
let x_actual_width = 0;
|
|
@@ -556,9 +605,11 @@ class GanttChart {
|
|
|
556
605
|
x_plan_width,
|
|
557
606
|
x_actual_width,
|
|
558
607
|
y: y + this.config.rowHeight * 0.5,
|
|
559
|
-
row:
|
|
608
|
+
row: visibleRowIndex
|
|
609
|
+
// Use visible row index instead of original index
|
|
560
610
|
});
|
|
561
611
|
});
|
|
612
|
+
visibleRowIndex++;
|
|
562
613
|
}
|
|
563
614
|
}
|
|
564
615
|
getIterationStartDate(date) {
|
|
@@ -799,9 +850,9 @@ class GanttChart {
|
|
|
799
850
|
if (!fromPos) return;
|
|
800
851
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
801
852
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
802
|
-
const fromX =
|
|
853
|
+
const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
|
|
803
854
|
const fromY = fromPos.y;
|
|
804
|
-
const toX =
|
|
855
|
+
const toX = getMinMax([toPos.offset_x_actual_start, toPos.offset_x_plan_start])?.min;
|
|
805
856
|
const toY = toPos.y;
|
|
806
857
|
ctx.beginPath();
|
|
807
858
|
if (isAdjacent) {
|
|
@@ -848,10 +899,17 @@ class GanttChart {
|
|
|
848
899
|
ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
849
900
|
ctx.textRendering = "optimizeSpeed";
|
|
850
901
|
ctx.imageSmoothingEnabled = false;
|
|
902
|
+
let visibleRowIndex = 0;
|
|
851
903
|
for (let i = 0; i < this.data.length; i++) {
|
|
852
904
|
const row = this.data[i];
|
|
853
|
-
|
|
854
|
-
|
|
905
|
+
if (row.hide) {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
const y = visibleRowIndex * this.config.rowHeight;
|
|
909
|
+
if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) {
|
|
910
|
+
visibleRowIndex++;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
855
913
|
row.tasks.forEach((task) => {
|
|
856
914
|
const pos = this.taskPositions.get(task.id);
|
|
857
915
|
if (!pos) return;
|
|
@@ -860,14 +918,22 @@ class GanttChart {
|
|
|
860
918
|
if (!isPlanVisible && !isActualVisible) return;
|
|
861
919
|
this.drawTask(ctx, task, y, pos);
|
|
862
920
|
});
|
|
921
|
+
visibleRowIndex++;
|
|
863
922
|
}
|
|
864
923
|
}
|
|
924
|
+
// In the drawGrid method
|
|
865
925
|
drawGrid(ctx, startDate, endDate) {
|
|
866
926
|
ctx.strokeStyle = "#e6e6e6";
|
|
867
927
|
ctx.lineWidth = 1;
|
|
868
928
|
ctx.beginPath();
|
|
869
929
|
if (this.config.showRowLines) {
|
|
870
|
-
|
|
930
|
+
let visibleRowCount = 0;
|
|
931
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
932
|
+
if (!this.data[i].hide) {
|
|
933
|
+
visibleRowCount++;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
for (let i = 0; i <= visibleRowCount; i++) {
|
|
871
937
|
const y = i * this.config.rowHeight;
|
|
872
938
|
if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
|
|
873
939
|
ctx.moveTo(this.scrollLeft, y);
|
|
@@ -974,11 +1040,11 @@ class GanttChart {
|
|
|
974
1040
|
ctx.fillStyle = "#000";
|
|
975
1041
|
if (this.config.showLeftRemark && task.leftRemark) {
|
|
976
1042
|
ctx.textAlign = "right";
|
|
977
|
-
ctx.fillText(task.leftRemark, Math.round(
|
|
1043
|
+
ctx.fillText(task.leftRemark, Math.round(getMinMax([pos.offset_x_plan_start, pos.offset_x_actual_start])?.min - 8 * 2), Math.round(textY));
|
|
978
1044
|
}
|
|
979
1045
|
if (this.config.showRightRemark && task.rightRemark) {
|
|
980
1046
|
ctx.textAlign = "left";
|
|
981
|
-
ctx.fillText(task.rightRemark, Math.round(
|
|
1047
|
+
ctx.fillText(task.rightRemark, Math.round(getMinMax([pos.offset_x_plan_end, pos.offset_x_actual_end])?.max + 8 * 2), Math.round(textY));
|
|
982
1048
|
}
|
|
983
1049
|
if (this.config.showCenterRemark && task.centerRemark) {
|
|
984
1050
|
const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
|
|
@@ -1012,10 +1078,21 @@ class GanttChart {
|
|
|
1012
1078
|
const mouseY = e.clientY - rect.top;
|
|
1013
1079
|
const chartX = mouseX + this.scrollLeft;
|
|
1014
1080
|
const chartY = mouseY + this.scrollTop;
|
|
1015
|
-
const
|
|
1081
|
+
const visibleRowIndex = Math.floor(chartY / this.config.rowHeight);
|
|
1082
|
+
let actualRowIndex = -1;
|
|
1083
|
+
let visibleRowCount = 0;
|
|
1084
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
1085
|
+
if (!this.data[i].hide) {
|
|
1086
|
+
if (visibleRowCount === visibleRowIndex) {
|
|
1087
|
+
actualRowIndex = i;
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
visibleRowCount++;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1016
1093
|
const date = this.xToDate(chartX);
|
|
1017
|
-
if (
|
|
1018
|
-
const row = this.data[
|
|
1094
|
+
if (actualRowIndex < 0 || actualRowIndex >= this.data.length) return this.handleMouseLeave();
|
|
1095
|
+
const row = this.data[actualRowIndex];
|
|
1019
1096
|
if (this.config.tooltipFormat) {
|
|
1020
1097
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
1021
1098
|
if (!htmlStr) {
|
|
@@ -1025,11 +1102,10 @@ class GanttChart {
|
|
|
1025
1102
|
this.tooltip.innerHTML = htmlStr;
|
|
1026
1103
|
} else {
|
|
1027
1104
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
1028
|
-
|
|
1029
|
-
if (date >= pStart && date < pEnd) return true;
|
|
1105
|
+
if (task.planStart && task.planEnd && date > dateToStart(new Date(task.planStart)) && date <= dateToEnd(new Date(task.planEnd))) return true;
|
|
1030
1106
|
if (task.actualStart) {
|
|
1031
|
-
const
|
|
1032
|
-
if (date >=
|
|
1107
|
+
const aEnd = task.actualEnd ? new Date(task.actualEnd) : this.today;
|
|
1108
|
+
if (date >= dateToStart(new Date(task.actualStart)) && date <= dateToEnd(aEnd)) return true;
|
|
1033
1109
|
}
|
|
1034
1110
|
return false;
|
|
1035
1111
|
});
|
|
@@ -1111,5 +1187,9 @@ class GanttChart {
|
|
|
1111
1187
|
export {
|
|
1112
1188
|
DateUtils,
|
|
1113
1189
|
GanttChart,
|
|
1114
|
-
|
|
1190
|
+
dateToEnd,
|
|
1191
|
+
dateToStart,
|
|
1192
|
+
firstValidValue,
|
|
1193
|
+
getMinMax,
|
|
1194
|
+
getMinMaxOptimized
|
|
1115
1195
|
};
|
package/dist/index.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.5.
|
|
2
|
+
* gantt-canvas-chart v1.5.3
|
|
3
3
|
* (c) 2025-present chandq
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -84,6 +84,40 @@
|
|
|
84
84
|
}
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
|
+
function getMinMax(values) {
|
|
88
|
+
const validNumbers = values.filter((value) => {
|
|
89
|
+
return typeof value === "number" && !isNaN(value) && value !== Infinity && value !== -Infinity;
|
|
90
|
+
});
|
|
91
|
+
if (validNumbers.length === 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
min: Math.min(...validNumbers),
|
|
96
|
+
max: Math.max(...validNumbers)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function getMinMaxOptimized(values) {
|
|
100
|
+
let min = Infinity;
|
|
101
|
+
let max = -Infinity;
|
|
102
|
+
let hasValidValue = false;
|
|
103
|
+
for (let i = 0; i < values.length; i++) {
|
|
104
|
+
const value = values[i];
|
|
105
|
+
if (typeof value === "number" && !isNaN(value) && value !== Infinity && value !== -Infinity) {
|
|
106
|
+
hasValidValue = true;
|
|
107
|
+
if (value < min) min = value;
|
|
108
|
+
if (value > max) max = value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return hasValidValue ? { min, max } : null;
|
|
112
|
+
}
|
|
113
|
+
function dateToStart(d) {
|
|
114
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
|
115
|
+
}
|
|
116
|
+
function dateToEnd(value) {
|
|
117
|
+
const d = dateToStart(value);
|
|
118
|
+
d.setDate(d.getDate() + 1);
|
|
119
|
+
return new Date(d.getTime() - 1);
|
|
120
|
+
}
|
|
87
121
|
class GanttChart {
|
|
88
122
|
rootContainer;
|
|
89
123
|
container;
|
|
@@ -123,7 +157,9 @@
|
|
|
123
157
|
onDataLoad = null;
|
|
124
158
|
scrollLoadTimer = null;
|
|
125
159
|
constructor(rootContainer, data, config = {}) {
|
|
126
|
-
if (rootContainer
|
|
160
|
+
if (!rootContainer) {
|
|
161
|
+
throw new Error("Root container element is required");
|
|
162
|
+
} else if (rootContainer.querySelector(".__gantt-chart-container")) {
|
|
127
163
|
throw new Error("GanttChart already exists in this container");
|
|
128
164
|
}
|
|
129
165
|
const container = document.createElement("div");
|
|
@@ -219,8 +255,18 @@
|
|
|
219
255
|
}
|
|
220
256
|
buildTaskMap() {
|
|
221
257
|
this.taskMap.clear();
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
let visibleRowIndex = 0;
|
|
259
|
+
this.data.forEach((row) => {
|
|
260
|
+
row.tasks.forEach((task) => {
|
|
261
|
+
this.taskMap.set(task.id, {
|
|
262
|
+
row: row.hide ? -1 : visibleRowIndex,
|
|
263
|
+
// Use -1 for hidden rows
|
|
264
|
+
task
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
if (!row.hide) {
|
|
268
|
+
visibleRowIndex++;
|
|
269
|
+
}
|
|
224
270
|
});
|
|
225
271
|
}
|
|
226
272
|
setupEvents() {
|
|
@@ -489,10 +535,12 @@
|
|
|
489
535
|
end: this.xToDate(this.scrollLeft + this.viewportWidth + buffer)
|
|
490
536
|
};
|
|
491
537
|
}
|
|
538
|
+
// Update the updateDimensions method to calculate height based on visible rows
|
|
492
539
|
updateDimensions() {
|
|
493
540
|
const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
|
|
541
|
+
const visibleRowCount = this.data.filter((row) => !row.hide).length;
|
|
494
542
|
const newTotalWidth = totalDays * this.pixelsPerDay;
|
|
495
|
-
const newTotalHeight =
|
|
543
|
+
const newTotalHeight = visibleRowCount * this.config.rowHeight + this.config.headerHeight;
|
|
496
544
|
if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
|
|
497
545
|
this.totalWidth = newTotalWidth;
|
|
498
546
|
this.totalHeight = newTotalHeight;
|
|
@@ -511,12 +559,13 @@
|
|
|
511
559
|
}
|
|
512
560
|
calculateAllTaskPositions() {
|
|
513
561
|
this.taskPositions.clear();
|
|
514
|
-
|
|
562
|
+
let visibleRowIndex = 0;
|
|
563
|
+
for (let i = 0, len = this.data.length; i < len; i++) {
|
|
515
564
|
const row = this.data[i];
|
|
516
565
|
if (row.hide) {
|
|
517
566
|
continue;
|
|
518
567
|
}
|
|
519
|
-
const y =
|
|
568
|
+
const y = visibleRowIndex * this.config.rowHeight;
|
|
520
569
|
row.tasks.forEach((task) => {
|
|
521
570
|
if (task.hide) {
|
|
522
571
|
return;
|
|
@@ -524,7 +573,7 @@
|
|
|
524
573
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
525
574
|
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
526
575
|
let x_actual_start = null, x_actual_end = null;
|
|
527
|
-
let offset_x_plan_start =
|
|
576
|
+
let offset_x_plan_start = null, offset_x_plan_end = null;
|
|
528
577
|
let offset_x_actual_start = null, offset_x_actual_end = null;
|
|
529
578
|
let x_plan_width = 0;
|
|
530
579
|
let x_actual_width = 0;
|
|
@@ -560,9 +609,11 @@
|
|
|
560
609
|
x_plan_width,
|
|
561
610
|
x_actual_width,
|
|
562
611
|
y: y + this.config.rowHeight * 0.5,
|
|
563
|
-
row:
|
|
612
|
+
row: visibleRowIndex
|
|
613
|
+
// Use visible row index instead of original index
|
|
564
614
|
});
|
|
565
615
|
});
|
|
616
|
+
visibleRowIndex++;
|
|
566
617
|
}
|
|
567
618
|
}
|
|
568
619
|
getIterationStartDate(date) {
|
|
@@ -803,9 +854,9 @@
|
|
|
803
854
|
if (!fromPos) return;
|
|
804
855
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
805
856
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
806
|
-
const fromX =
|
|
857
|
+
const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
|
|
807
858
|
const fromY = fromPos.y;
|
|
808
|
-
const toX =
|
|
859
|
+
const toX = getMinMax([toPos.offset_x_actual_start, toPos.offset_x_plan_start])?.min;
|
|
809
860
|
const toY = toPos.y;
|
|
810
861
|
ctx.beginPath();
|
|
811
862
|
if (isAdjacent) {
|
|
@@ -852,10 +903,17 @@
|
|
|
852
903
|
ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
853
904
|
ctx.textRendering = "optimizeSpeed";
|
|
854
905
|
ctx.imageSmoothingEnabled = false;
|
|
906
|
+
let visibleRowIndex = 0;
|
|
855
907
|
for (let i = 0; i < this.data.length; i++) {
|
|
856
908
|
const row = this.data[i];
|
|
857
|
-
|
|
858
|
-
|
|
909
|
+
if (row.hide) {
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const y = visibleRowIndex * this.config.rowHeight;
|
|
913
|
+
if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) {
|
|
914
|
+
visibleRowIndex++;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
859
917
|
row.tasks.forEach((task) => {
|
|
860
918
|
const pos = this.taskPositions.get(task.id);
|
|
861
919
|
if (!pos) return;
|
|
@@ -864,14 +922,22 @@
|
|
|
864
922
|
if (!isPlanVisible && !isActualVisible) return;
|
|
865
923
|
this.drawTask(ctx, task, y, pos);
|
|
866
924
|
});
|
|
925
|
+
visibleRowIndex++;
|
|
867
926
|
}
|
|
868
927
|
}
|
|
928
|
+
// In the drawGrid method
|
|
869
929
|
drawGrid(ctx, startDate, endDate) {
|
|
870
930
|
ctx.strokeStyle = "#e6e6e6";
|
|
871
931
|
ctx.lineWidth = 1;
|
|
872
932
|
ctx.beginPath();
|
|
873
933
|
if (this.config.showRowLines) {
|
|
874
|
-
|
|
934
|
+
let visibleRowCount = 0;
|
|
935
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
936
|
+
if (!this.data[i].hide) {
|
|
937
|
+
visibleRowCount++;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
for (let i = 0; i <= visibleRowCount; i++) {
|
|
875
941
|
const y = i * this.config.rowHeight;
|
|
876
942
|
if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
|
|
877
943
|
ctx.moveTo(this.scrollLeft, y);
|
|
@@ -978,11 +1044,11 @@
|
|
|
978
1044
|
ctx.fillStyle = "#000";
|
|
979
1045
|
if (this.config.showLeftRemark && task.leftRemark) {
|
|
980
1046
|
ctx.textAlign = "right";
|
|
981
|
-
ctx.fillText(task.leftRemark, Math.round(
|
|
1047
|
+
ctx.fillText(task.leftRemark, Math.round(getMinMax([pos.offset_x_plan_start, pos.offset_x_actual_start])?.min - 8 * 2), Math.round(textY));
|
|
982
1048
|
}
|
|
983
1049
|
if (this.config.showRightRemark && task.rightRemark) {
|
|
984
1050
|
ctx.textAlign = "left";
|
|
985
|
-
ctx.fillText(task.rightRemark, Math.round(
|
|
1051
|
+
ctx.fillText(task.rightRemark, Math.round(getMinMax([pos.offset_x_plan_end, pos.offset_x_actual_end])?.max + 8 * 2), Math.round(textY));
|
|
986
1052
|
}
|
|
987
1053
|
if (this.config.showCenterRemark && task.centerRemark) {
|
|
988
1054
|
const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
|
|
@@ -1016,10 +1082,21 @@
|
|
|
1016
1082
|
const mouseY = e.clientY - rect.top;
|
|
1017
1083
|
const chartX = mouseX + this.scrollLeft;
|
|
1018
1084
|
const chartY = mouseY + this.scrollTop;
|
|
1019
|
-
const
|
|
1085
|
+
const visibleRowIndex = Math.floor(chartY / this.config.rowHeight);
|
|
1086
|
+
let actualRowIndex = -1;
|
|
1087
|
+
let visibleRowCount = 0;
|
|
1088
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
1089
|
+
if (!this.data[i].hide) {
|
|
1090
|
+
if (visibleRowCount === visibleRowIndex) {
|
|
1091
|
+
actualRowIndex = i;
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
visibleRowCount++;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1020
1097
|
const date = this.xToDate(chartX);
|
|
1021
|
-
if (
|
|
1022
|
-
const row = this.data[
|
|
1098
|
+
if (actualRowIndex < 0 || actualRowIndex >= this.data.length) return this.handleMouseLeave();
|
|
1099
|
+
const row = this.data[actualRowIndex];
|
|
1023
1100
|
if (this.config.tooltipFormat) {
|
|
1024
1101
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
1025
1102
|
if (!htmlStr) {
|
|
@@ -1029,11 +1106,10 @@
|
|
|
1029
1106
|
this.tooltip.innerHTML = htmlStr;
|
|
1030
1107
|
} else {
|
|
1031
1108
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
1032
|
-
|
|
1033
|
-
if (date >= pStart && date < pEnd) return true;
|
|
1109
|
+
if (task.planStart && task.planEnd && date > dateToStart(new Date(task.planStart)) && date <= dateToEnd(new Date(task.planEnd))) return true;
|
|
1034
1110
|
if (task.actualStart) {
|
|
1035
|
-
const
|
|
1036
|
-
if (date >=
|
|
1111
|
+
const aEnd = task.actualEnd ? new Date(task.actualEnd) : this.today;
|
|
1112
|
+
if (date >= dateToStart(new Date(task.actualStart)) && date <= dateToEnd(aEnd)) return true;
|
|
1037
1113
|
}
|
|
1038
1114
|
return false;
|
|
1039
1115
|
});
|
|
@@ -1114,6 +1190,10 @@
|
|
|
1114
1190
|
}
|
|
1115
1191
|
exports2.DateUtils = DateUtils;
|
|
1116
1192
|
exports2.GanttChart = GanttChart;
|
|
1193
|
+
exports2.dateToEnd = dateToEnd;
|
|
1194
|
+
exports2.dateToStart = dateToStart;
|
|
1117
1195
|
exports2.firstValidValue = firstValidValue;
|
|
1196
|
+
exports2.getMinMax = getMinMax;
|
|
1197
|
+
exports2.getMinMaxOptimized = getMinMaxOptimized;
|
|
1118
1198
|
Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
|
|
1119
1199
|
}));
|