gantt-canvas-chart 1.5.0 → 1.5.2
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 +61 -29
- package/dist/index.css +1 -1
- package/dist/index.d.ts +62 -3
- package/dist/index.es.js +62 -30
- package/dist/index.umd.js +61 -29
- 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.2
|
|
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;
|
|
@@ -237,8 +271,8 @@ class GanttChart {
|
|
|
237
271
|
updateConfig(newConfig) {
|
|
238
272
|
Object.assign(this.config, newConfig);
|
|
239
273
|
if (newConfig.viewMode) {
|
|
240
|
-
this.container.scrollLeft =
|
|
241
|
-
this.scrollLeft =
|
|
274
|
+
this.container.scrollLeft = this.config.scrollEdgeThresholds + 2;
|
|
275
|
+
this.scrollLeft = this.config.scrollEdgeThresholds + 2;
|
|
242
276
|
this.updatePixelsPerDay();
|
|
243
277
|
this.calculateFullTimeline();
|
|
244
278
|
}
|
|
@@ -522,7 +556,7 @@ class GanttChart {
|
|
|
522
556
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
523
557
|
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
524
558
|
let x_actual_start = null, x_actual_end = null;
|
|
525
|
-
let offset_x_plan_start =
|
|
559
|
+
let offset_x_plan_start = null, offset_x_plan_end = null;
|
|
526
560
|
let offset_x_actual_start = null, offset_x_actual_end = null;
|
|
527
561
|
let x_plan_width = 0;
|
|
528
562
|
let x_actual_width = 0;
|
|
@@ -535,21 +569,17 @@ class GanttChart {
|
|
|
535
569
|
x_plan_end && (offset_x_plan_end = offset_x_plan_start + x_plan_width * percent_plan);
|
|
536
570
|
isValidPlanTask = true;
|
|
537
571
|
}
|
|
538
|
-
|
|
539
|
-
|
|
572
|
+
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
573
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd ? task.actualEnd : this.today, 1));
|
|
574
|
+
if (x_actual_start && x_actual_end && x_actual_start < x_actual_end) {
|
|
575
|
+
x_actual_width = x_actual_end - x_actual_start;
|
|
576
|
+
offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
|
|
577
|
+
offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual;
|
|
540
578
|
isValidActualTask = true;
|
|
541
579
|
}
|
|
542
|
-
if (task.actualEnd) {
|
|
543
|
-
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
544
|
-
}
|
|
545
580
|
if (!isValidPlanTask && !isValidActualTask) {
|
|
546
581
|
return;
|
|
547
582
|
}
|
|
548
|
-
if (x_actual_start) {
|
|
549
|
-
x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
|
|
550
|
-
offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
|
|
551
|
-
x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
|
|
552
|
-
}
|
|
553
583
|
this.taskPositions.set(task.id, {
|
|
554
584
|
x_plan_start,
|
|
555
585
|
x_plan_end,
|
|
@@ -805,9 +835,9 @@ class GanttChart {
|
|
|
805
835
|
if (!fromPos) return;
|
|
806
836
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
807
837
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
808
|
-
const fromX =
|
|
838
|
+
const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
|
|
809
839
|
const fromY = fromPos.y;
|
|
810
|
-
const toX =
|
|
840
|
+
const toX = getMinMax([toPos.offset_x_actual_start, toPos.offset_x_plan_start])?.min;
|
|
811
841
|
const toY = toPos.y;
|
|
812
842
|
ctx.beginPath();
|
|
813
843
|
if (isAdjacent) {
|
|
@@ -861,10 +891,9 @@ class GanttChart {
|
|
|
861
891
|
row.tasks.forEach((task) => {
|
|
862
892
|
const pos = this.taskPositions.get(task.id);
|
|
863
893
|
if (!pos) return;
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
894
|
+
const isPlanVisible = pos.x_plan_end >= this.scrollLeft && pos.x_plan_start <= this.scrollLeft + this.viewportWidth;
|
|
895
|
+
const isActualVisible = pos.x_actual_start && pos.x_actual_end && pos.x_actual_end >= this.scrollLeft && pos.x_actual_start <= this.scrollLeft + this.viewportWidth;
|
|
896
|
+
if (!isPlanVisible && !isActualVisible) return;
|
|
868
897
|
this.drawTask(ctx, task, y, pos);
|
|
869
898
|
});
|
|
870
899
|
}
|
|
@@ -950,7 +979,7 @@ class GanttChart {
|
|
|
950
979
|
ctx.stroke();
|
|
951
980
|
}
|
|
952
981
|
drawToday(ctx) {
|
|
953
|
-
const x = this.dateToX(this.today);
|
|
982
|
+
const x = this.dateToX(DateUtils.addDays(this.today, 1));
|
|
954
983
|
if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
|
|
955
984
|
ctx.strokeStyle = this.config.todayColor;
|
|
956
985
|
ctx.lineWidth = 1;
|
|
@@ -981,11 +1010,11 @@ class GanttChart {
|
|
|
981
1010
|
ctx.fillStyle = "#000";
|
|
982
1011
|
if (this.config.showLeftRemark && task.leftRemark) {
|
|
983
1012
|
ctx.textAlign = "right";
|
|
984
|
-
ctx.fillText(task.leftRemark, Math.round(
|
|
1013
|
+
ctx.fillText(task.leftRemark, Math.round(getMinMax([pos.offset_x_plan_start, pos.offset_x_actual_start])?.min - 8 * 2), Math.round(textY));
|
|
985
1014
|
}
|
|
986
1015
|
if (this.config.showRightRemark && task.rightRemark) {
|
|
987
1016
|
ctx.textAlign = "left";
|
|
988
|
-
ctx.fillText(task.rightRemark, Math.round(
|
|
1017
|
+
ctx.fillText(task.rightRemark, Math.round(getMinMax([pos.offset_x_plan_end, pos.offset_x_actual_end])?.max + 8 * 2), Math.round(textY));
|
|
989
1018
|
}
|
|
990
1019
|
if (this.config.showCenterRemark && task.centerRemark) {
|
|
991
1020
|
const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
|
|
@@ -1032,11 +1061,10 @@ class GanttChart {
|
|
|
1032
1061
|
this.tooltip.innerHTML = htmlStr;
|
|
1033
1062
|
} else {
|
|
1034
1063
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
1035
|
-
|
|
1036
|
-
if (date >= pStart && date < pEnd) return true;
|
|
1064
|
+
if (task.planStart && task.planEnd && date > dateToStart(new Date(task.planStart)) && date <= dateToEnd(new Date(task.planEnd))) return true;
|
|
1037
1065
|
if (task.actualStart) {
|
|
1038
|
-
const
|
|
1039
|
-
if (date >=
|
|
1066
|
+
const aEnd = task.actualEnd ? new Date(task.actualEnd) : this.today;
|
|
1067
|
+
if (date >= dateToStart(new Date(task.actualStart)) && date <= dateToEnd(aEnd)) return true;
|
|
1040
1068
|
}
|
|
1041
1069
|
return false;
|
|
1042
1070
|
});
|
|
@@ -1099,7 +1127,7 @@ class GanttChart {
|
|
|
1099
1127
|
const startDate = date ? date : this.minDate;
|
|
1100
1128
|
if (startDate) {
|
|
1101
1129
|
const xPosition = this.dateToX(startDate);
|
|
1102
|
-
this.container.scrollTo({ left: xPosition - 80 });
|
|
1130
|
+
this.container.scrollTo({ left: Math.max(this.config.scrollEdgeThresholds + 2, xPosition - 80) });
|
|
1103
1131
|
}
|
|
1104
1132
|
}
|
|
1105
1133
|
/**
|
|
@@ -1111,10 +1139,14 @@ class GanttChart {
|
|
|
1111
1139
|
if (params && (params.rowId || params.rowIndex)) {
|
|
1112
1140
|
const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
|
|
1113
1141
|
const yPosition = this.config.rowHeight * rowIndex;
|
|
1114
|
-
this.container.scrollTo({ top: yPosition - 80 });
|
|
1142
|
+
this.container.scrollTo({ top: Math.max(this.config.scrollEdgeThresholds + 2, yPosition - 80) });
|
|
1115
1143
|
}
|
|
1116
1144
|
}
|
|
1117
1145
|
}
|
|
1118
1146
|
exports.DateUtils = DateUtils;
|
|
1119
1147
|
exports.GanttChart = GanttChart;
|
|
1148
|
+
exports.dateToEnd = dateToEnd;
|
|
1149
|
+
exports.dateToStart = dateToStart;
|
|
1120
1150
|
exports.firstValidValue = firstValidValue;
|
|
1151
|
+
exports.getMinMax = getMinMax;
|
|
1152
|
+
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;
|
|
@@ -20,6 +34,24 @@ export declare class DateUtils {
|
|
|
20
34
|
*/
|
|
21
35
|
export declare function firstValidValue(...args: any[]): any;
|
|
22
36
|
|
|
37
|
+
/**
|
|
38
|
+
* GanttChart Class
|
|
39
|
+
*
|
|
40
|
+
* Core Features:
|
|
41
|
+
* 1. Virtual Rendering: Only renders tasks visible in the current viewport for performance optimization
|
|
42
|
+
* 2. Dynamic Scrolling: Supports both horizontal and vertical scrolling with virtualized data loading
|
|
43
|
+
* 3. Dual Timeline Display: Shows both planned and actual timelines for tasks
|
|
44
|
+
* 4. Data Validation Handling:
|
|
45
|
+
* - Automatically handles invalid data where start dates are after end dates by skipping rendering
|
|
46
|
+
* - For tasks with only actual start time (no end time), automatically extends visualization to today
|
|
47
|
+
* - Filters out tasks with completely invalid date ranges
|
|
48
|
+
* 5. Dependency Visualization: Draws arrows between dependent tasks with smart routing
|
|
49
|
+
* 6. Infinite Scroll Loading: Supports dynamic data loading when scrolling to edges
|
|
50
|
+
* 7. Responsive Design: Adapts to container size changes using ResizeObserver
|
|
51
|
+
* 8. Tooltip System: Provides detailed information on hover with customizable formatting
|
|
52
|
+
* 9. Multiple View Modes: Supports Day/Week/Month/Year views with appropriate scaling
|
|
53
|
+
* 10. Custom Styling: Allows per-task styling through configuration options
|
|
54
|
+
*/
|
|
23
55
|
export declare class GanttChart {
|
|
24
56
|
private rootContainer;
|
|
25
57
|
container: HTMLElement;
|
|
@@ -49,7 +81,10 @@ export declare class GanttChart {
|
|
|
49
81
|
private totalHeight;
|
|
50
82
|
private resizeObserver;
|
|
51
83
|
private taskPositions;
|
|
52
|
-
|
|
84
|
+
taskMap: Map<string, {
|
|
85
|
+
row: number;
|
|
86
|
+
task: Task;
|
|
87
|
+
}>;
|
|
53
88
|
private isLoadingData;
|
|
54
89
|
private hasMoreDataLeft;
|
|
55
90
|
private hasMoreDataRight;
|
|
@@ -155,6 +190,30 @@ export declare interface GanttConfig {
|
|
|
155
190
|
|
|
156
191
|
export declare type GanttData = Row[];
|
|
157
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
|
+
|
|
158
217
|
export declare type LoadMoreDirection = 'left' | 'right' | 'bottom';
|
|
159
218
|
|
|
160
219
|
export declare interface Row {
|
|
@@ -192,8 +251,8 @@ export declare interface TaskPosition {
|
|
|
192
251
|
x_actual_end: number | null;
|
|
193
252
|
x_plan_width: number;
|
|
194
253
|
x_actual_width: number;
|
|
195
|
-
offset_x_plan_start: number;
|
|
196
|
-
offset_x_plan_end: number;
|
|
254
|
+
offset_x_plan_start: number | null;
|
|
255
|
+
offset_x_plan_end: number | null;
|
|
197
256
|
offset_x_actual_start: number | null;
|
|
198
257
|
offset_x_actual_end: number | null;
|
|
199
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.2
|
|
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;
|
|
@@ -235,8 +269,8 @@ class GanttChart {
|
|
|
235
269
|
updateConfig(newConfig) {
|
|
236
270
|
Object.assign(this.config, newConfig);
|
|
237
271
|
if (newConfig.viewMode) {
|
|
238
|
-
this.container.scrollLeft =
|
|
239
|
-
this.scrollLeft =
|
|
272
|
+
this.container.scrollLeft = this.config.scrollEdgeThresholds + 2;
|
|
273
|
+
this.scrollLeft = this.config.scrollEdgeThresholds + 2;
|
|
240
274
|
this.updatePixelsPerDay();
|
|
241
275
|
this.calculateFullTimeline();
|
|
242
276
|
}
|
|
@@ -520,7 +554,7 @@ class GanttChart {
|
|
|
520
554
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
521
555
|
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
522
556
|
let x_actual_start = null, x_actual_end = null;
|
|
523
|
-
let offset_x_plan_start =
|
|
557
|
+
let offset_x_plan_start = null, offset_x_plan_end = null;
|
|
524
558
|
let offset_x_actual_start = null, offset_x_actual_end = null;
|
|
525
559
|
let x_plan_width = 0;
|
|
526
560
|
let x_actual_width = 0;
|
|
@@ -533,21 +567,17 @@ class GanttChart {
|
|
|
533
567
|
x_plan_end && (offset_x_plan_end = offset_x_plan_start + x_plan_width * percent_plan);
|
|
534
568
|
isValidPlanTask = true;
|
|
535
569
|
}
|
|
536
|
-
|
|
537
|
-
|
|
570
|
+
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
571
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd ? task.actualEnd : this.today, 1));
|
|
572
|
+
if (x_actual_start && x_actual_end && x_actual_start < x_actual_end) {
|
|
573
|
+
x_actual_width = x_actual_end - x_actual_start;
|
|
574
|
+
offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
|
|
575
|
+
offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual;
|
|
538
576
|
isValidActualTask = true;
|
|
539
577
|
}
|
|
540
|
-
if (task.actualEnd) {
|
|
541
|
-
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
542
|
-
}
|
|
543
578
|
if (!isValidPlanTask && !isValidActualTask) {
|
|
544
579
|
return;
|
|
545
580
|
}
|
|
546
|
-
if (x_actual_start) {
|
|
547
|
-
x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
|
|
548
|
-
offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
|
|
549
|
-
x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
|
|
550
|
-
}
|
|
551
581
|
this.taskPositions.set(task.id, {
|
|
552
582
|
x_plan_start,
|
|
553
583
|
x_plan_end,
|
|
@@ -803,9 +833,9 @@ class GanttChart {
|
|
|
803
833
|
if (!fromPos) return;
|
|
804
834
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
805
835
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
806
|
-
const fromX =
|
|
836
|
+
const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
|
|
807
837
|
const fromY = fromPos.y;
|
|
808
|
-
const toX =
|
|
838
|
+
const toX = getMinMax([toPos.offset_x_actual_start, toPos.offset_x_plan_start])?.min;
|
|
809
839
|
const toY = toPos.y;
|
|
810
840
|
ctx.beginPath();
|
|
811
841
|
if (isAdjacent) {
|
|
@@ -859,10 +889,9 @@ class GanttChart {
|
|
|
859
889
|
row.tasks.forEach((task) => {
|
|
860
890
|
const pos = this.taskPositions.get(task.id);
|
|
861
891
|
if (!pos) return;
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
892
|
+
const isPlanVisible = pos.x_plan_end >= this.scrollLeft && pos.x_plan_start <= this.scrollLeft + this.viewportWidth;
|
|
893
|
+
const isActualVisible = pos.x_actual_start && pos.x_actual_end && pos.x_actual_end >= this.scrollLeft && pos.x_actual_start <= this.scrollLeft + this.viewportWidth;
|
|
894
|
+
if (!isPlanVisible && !isActualVisible) return;
|
|
866
895
|
this.drawTask(ctx, task, y, pos);
|
|
867
896
|
});
|
|
868
897
|
}
|
|
@@ -948,7 +977,7 @@ class GanttChart {
|
|
|
948
977
|
ctx.stroke();
|
|
949
978
|
}
|
|
950
979
|
drawToday(ctx) {
|
|
951
|
-
const x = this.dateToX(this.today);
|
|
980
|
+
const x = this.dateToX(DateUtils.addDays(this.today, 1));
|
|
952
981
|
if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
|
|
953
982
|
ctx.strokeStyle = this.config.todayColor;
|
|
954
983
|
ctx.lineWidth = 1;
|
|
@@ -979,11 +1008,11 @@ class GanttChart {
|
|
|
979
1008
|
ctx.fillStyle = "#000";
|
|
980
1009
|
if (this.config.showLeftRemark && task.leftRemark) {
|
|
981
1010
|
ctx.textAlign = "right";
|
|
982
|
-
ctx.fillText(task.leftRemark, Math.round(
|
|
1011
|
+
ctx.fillText(task.leftRemark, Math.round(getMinMax([pos.offset_x_plan_start, pos.offset_x_actual_start])?.min - 8 * 2), Math.round(textY));
|
|
983
1012
|
}
|
|
984
1013
|
if (this.config.showRightRemark && task.rightRemark) {
|
|
985
1014
|
ctx.textAlign = "left";
|
|
986
|
-
ctx.fillText(task.rightRemark, Math.round(
|
|
1015
|
+
ctx.fillText(task.rightRemark, Math.round(getMinMax([pos.offset_x_plan_end, pos.offset_x_actual_end])?.max + 8 * 2), Math.round(textY));
|
|
987
1016
|
}
|
|
988
1017
|
if (this.config.showCenterRemark && task.centerRemark) {
|
|
989
1018
|
const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
|
|
@@ -1030,11 +1059,10 @@ class GanttChart {
|
|
|
1030
1059
|
this.tooltip.innerHTML = htmlStr;
|
|
1031
1060
|
} else {
|
|
1032
1061
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
1033
|
-
|
|
1034
|
-
if (date >= pStart && date < pEnd) return true;
|
|
1062
|
+
if (task.planStart && task.planEnd && date > dateToStart(new Date(task.planStart)) && date <= dateToEnd(new Date(task.planEnd))) return true;
|
|
1035
1063
|
if (task.actualStart) {
|
|
1036
|
-
const
|
|
1037
|
-
if (date >=
|
|
1064
|
+
const aEnd = task.actualEnd ? new Date(task.actualEnd) : this.today;
|
|
1065
|
+
if (date >= dateToStart(new Date(task.actualStart)) && date <= dateToEnd(aEnd)) return true;
|
|
1038
1066
|
}
|
|
1039
1067
|
return false;
|
|
1040
1068
|
});
|
|
@@ -1097,7 +1125,7 @@ class GanttChart {
|
|
|
1097
1125
|
const startDate = date ? date : this.minDate;
|
|
1098
1126
|
if (startDate) {
|
|
1099
1127
|
const xPosition = this.dateToX(startDate);
|
|
1100
|
-
this.container.scrollTo({ left: xPosition - 80 });
|
|
1128
|
+
this.container.scrollTo({ left: Math.max(this.config.scrollEdgeThresholds + 2, xPosition - 80) });
|
|
1101
1129
|
}
|
|
1102
1130
|
}
|
|
1103
1131
|
/**
|
|
@@ -1109,12 +1137,16 @@ class GanttChart {
|
|
|
1109
1137
|
if (params && (params.rowId || params.rowIndex)) {
|
|
1110
1138
|
const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
|
|
1111
1139
|
const yPosition = this.config.rowHeight * rowIndex;
|
|
1112
|
-
this.container.scrollTo({ top: yPosition - 80 });
|
|
1140
|
+
this.container.scrollTo({ top: Math.max(this.config.scrollEdgeThresholds + 2, yPosition - 80) });
|
|
1113
1141
|
}
|
|
1114
1142
|
}
|
|
1115
1143
|
}
|
|
1116
1144
|
export {
|
|
1117
1145
|
DateUtils,
|
|
1118
1146
|
GanttChart,
|
|
1119
|
-
|
|
1147
|
+
dateToEnd,
|
|
1148
|
+
dateToStart,
|
|
1149
|
+
firstValidValue,
|
|
1150
|
+
getMinMax,
|
|
1151
|
+
getMinMaxOptimized
|
|
1120
1152
|
};
|
package/dist/index.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.5.
|
|
2
|
+
* gantt-canvas-chart v1.5.2
|
|
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;
|
|
@@ -239,8 +273,8 @@
|
|
|
239
273
|
updateConfig(newConfig) {
|
|
240
274
|
Object.assign(this.config, newConfig);
|
|
241
275
|
if (newConfig.viewMode) {
|
|
242
|
-
this.container.scrollLeft =
|
|
243
|
-
this.scrollLeft =
|
|
276
|
+
this.container.scrollLeft = this.config.scrollEdgeThresholds + 2;
|
|
277
|
+
this.scrollLeft = this.config.scrollEdgeThresholds + 2;
|
|
244
278
|
this.updatePixelsPerDay();
|
|
245
279
|
this.calculateFullTimeline();
|
|
246
280
|
}
|
|
@@ -524,7 +558,7 @@
|
|
|
524
558
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
525
559
|
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
526
560
|
let x_actual_start = null, x_actual_end = null;
|
|
527
|
-
let offset_x_plan_start =
|
|
561
|
+
let offset_x_plan_start = null, offset_x_plan_end = null;
|
|
528
562
|
let offset_x_actual_start = null, offset_x_actual_end = null;
|
|
529
563
|
let x_plan_width = 0;
|
|
530
564
|
let x_actual_width = 0;
|
|
@@ -537,21 +571,17 @@
|
|
|
537
571
|
x_plan_end && (offset_x_plan_end = offset_x_plan_start + x_plan_width * percent_plan);
|
|
538
572
|
isValidPlanTask = true;
|
|
539
573
|
}
|
|
540
|
-
|
|
541
|
-
|
|
574
|
+
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
575
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd ? task.actualEnd : this.today, 1));
|
|
576
|
+
if (x_actual_start && x_actual_end && x_actual_start < x_actual_end) {
|
|
577
|
+
x_actual_width = x_actual_end - x_actual_start;
|
|
578
|
+
offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
|
|
579
|
+
offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual;
|
|
542
580
|
isValidActualTask = true;
|
|
543
581
|
}
|
|
544
|
-
if (task.actualEnd) {
|
|
545
|
-
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
546
|
-
}
|
|
547
582
|
if (!isValidPlanTask && !isValidActualTask) {
|
|
548
583
|
return;
|
|
549
584
|
}
|
|
550
|
-
if (x_actual_start) {
|
|
551
|
-
x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
|
|
552
|
-
offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
|
|
553
|
-
x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
|
|
554
|
-
}
|
|
555
585
|
this.taskPositions.set(task.id, {
|
|
556
586
|
x_plan_start,
|
|
557
587
|
x_plan_end,
|
|
@@ -807,9 +837,9 @@
|
|
|
807
837
|
if (!fromPos) return;
|
|
808
838
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
809
839
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
810
|
-
const fromX =
|
|
840
|
+
const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
|
|
811
841
|
const fromY = fromPos.y;
|
|
812
|
-
const toX =
|
|
842
|
+
const toX = getMinMax([toPos.offset_x_actual_start, toPos.offset_x_plan_start])?.min;
|
|
813
843
|
const toY = toPos.y;
|
|
814
844
|
ctx.beginPath();
|
|
815
845
|
if (isAdjacent) {
|
|
@@ -863,10 +893,9 @@
|
|
|
863
893
|
row.tasks.forEach((task) => {
|
|
864
894
|
const pos = this.taskPositions.get(task.id);
|
|
865
895
|
if (!pos) return;
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
896
|
+
const isPlanVisible = pos.x_plan_end >= this.scrollLeft && pos.x_plan_start <= this.scrollLeft + this.viewportWidth;
|
|
897
|
+
const isActualVisible = pos.x_actual_start && pos.x_actual_end && pos.x_actual_end >= this.scrollLeft && pos.x_actual_start <= this.scrollLeft + this.viewportWidth;
|
|
898
|
+
if (!isPlanVisible && !isActualVisible) return;
|
|
870
899
|
this.drawTask(ctx, task, y, pos);
|
|
871
900
|
});
|
|
872
901
|
}
|
|
@@ -952,7 +981,7 @@
|
|
|
952
981
|
ctx.stroke();
|
|
953
982
|
}
|
|
954
983
|
drawToday(ctx) {
|
|
955
|
-
const x = this.dateToX(this.today);
|
|
984
|
+
const x = this.dateToX(DateUtils.addDays(this.today, 1));
|
|
956
985
|
if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
|
|
957
986
|
ctx.strokeStyle = this.config.todayColor;
|
|
958
987
|
ctx.lineWidth = 1;
|
|
@@ -983,11 +1012,11 @@
|
|
|
983
1012
|
ctx.fillStyle = "#000";
|
|
984
1013
|
if (this.config.showLeftRemark && task.leftRemark) {
|
|
985
1014
|
ctx.textAlign = "right";
|
|
986
|
-
ctx.fillText(task.leftRemark, Math.round(
|
|
1015
|
+
ctx.fillText(task.leftRemark, Math.round(getMinMax([pos.offset_x_plan_start, pos.offset_x_actual_start])?.min - 8 * 2), Math.round(textY));
|
|
987
1016
|
}
|
|
988
1017
|
if (this.config.showRightRemark && task.rightRemark) {
|
|
989
1018
|
ctx.textAlign = "left";
|
|
990
|
-
ctx.fillText(task.rightRemark, Math.round(
|
|
1019
|
+
ctx.fillText(task.rightRemark, Math.round(getMinMax([pos.offset_x_plan_end, pos.offset_x_actual_end])?.max + 8 * 2), Math.round(textY));
|
|
991
1020
|
}
|
|
992
1021
|
if (this.config.showCenterRemark && task.centerRemark) {
|
|
993
1022
|
const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
|
|
@@ -1034,11 +1063,10 @@
|
|
|
1034
1063
|
this.tooltip.innerHTML = htmlStr;
|
|
1035
1064
|
} else {
|
|
1036
1065
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
1037
|
-
|
|
1038
|
-
if (date >= pStart && date < pEnd) return true;
|
|
1066
|
+
if (task.planStart && task.planEnd && date > dateToStart(new Date(task.planStart)) && date <= dateToEnd(new Date(task.planEnd))) return true;
|
|
1039
1067
|
if (task.actualStart) {
|
|
1040
|
-
const
|
|
1041
|
-
if (date >=
|
|
1068
|
+
const aEnd = task.actualEnd ? new Date(task.actualEnd) : this.today;
|
|
1069
|
+
if (date >= dateToStart(new Date(task.actualStart)) && date <= dateToEnd(aEnd)) return true;
|
|
1042
1070
|
}
|
|
1043
1071
|
return false;
|
|
1044
1072
|
});
|
|
@@ -1101,7 +1129,7 @@
|
|
|
1101
1129
|
const startDate = date ? date : this.minDate;
|
|
1102
1130
|
if (startDate) {
|
|
1103
1131
|
const xPosition = this.dateToX(startDate);
|
|
1104
|
-
this.container.scrollTo({ left: xPosition - 80 });
|
|
1132
|
+
this.container.scrollTo({ left: Math.max(this.config.scrollEdgeThresholds + 2, xPosition - 80) });
|
|
1105
1133
|
}
|
|
1106
1134
|
}
|
|
1107
1135
|
/**
|
|
@@ -1113,12 +1141,16 @@
|
|
|
1113
1141
|
if (params && (params.rowId || params.rowIndex)) {
|
|
1114
1142
|
const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
|
|
1115
1143
|
const yPosition = this.config.rowHeight * rowIndex;
|
|
1116
|
-
this.container.scrollTo({ top: yPosition - 80 });
|
|
1144
|
+
this.container.scrollTo({ top: Math.max(this.config.scrollEdgeThresholds + 2, yPosition - 80) });
|
|
1117
1145
|
}
|
|
1118
1146
|
}
|
|
1119
1147
|
}
|
|
1120
1148
|
exports2.DateUtils = DateUtils;
|
|
1121
1149
|
exports2.GanttChart = GanttChart;
|
|
1150
|
+
exports2.dateToEnd = dateToEnd;
|
|
1151
|
+
exports2.dateToStart = dateToStart;
|
|
1122
1152
|
exports2.firstValidValue = firstValidValue;
|
|
1153
|
+
exports2.getMinMax = getMinMax;
|
|
1154
|
+
exports2.getMinMaxOptimized = getMinMaxOptimized;
|
|
1123
1155
|
Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
|
|
1124
1156
|
}));
|