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 CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.0
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 = 0;
241
- this.scrollLeft = 0;
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 = NaN, offset_x_plan_end = NaN;
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
- if (task.actualStart) {
539
- x_actual_start = this.dateToX(new Date(task.actualStart));
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 = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
838
+ const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
809
839
  const fromY = fromPos.y;
810
- const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
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
- if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
865
- if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
866
- return;
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(Math.min(...[pos.offset_x_plan_start, pos.offset_x_actual_start].filter((val) => val !== null && val !== void 0)) - 8), Math.round(textY));
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(Math.max(...[pos.offset_x_plan_end, pos.offset_x_actual_end].filter((val) => val !== null && val !== void 0)) + 8), Math.round(textY));
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
- const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
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 aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
1039
- if (date >= aStart && date < aEnd) return true;
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
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.0
2
+ * gantt-canvas-chart v1.5.2
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
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
- private taskMap;
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.0
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 = 0;
239
- this.scrollLeft = 0;
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 = NaN, offset_x_plan_end = NaN;
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
- if (task.actualStart) {
537
- x_actual_start = this.dateToX(new Date(task.actualStart));
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 = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
836
+ const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
807
837
  const fromY = fromPos.y;
808
- const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
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
- if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
863
- if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
864
- return;
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(Math.min(...[pos.offset_x_plan_start, pos.offset_x_actual_start].filter((val) => val !== null && val !== void 0)) - 8), Math.round(textY));
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(Math.max(...[pos.offset_x_plan_end, pos.offset_x_actual_end].filter((val) => val !== null && val !== void 0)) + 8), Math.round(textY));
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
- const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
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 aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
1037
- if (date >= aStart && date < aEnd) return true;
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
- firstValidValue
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.0
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 = 0;
243
- this.scrollLeft = 0;
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 = NaN, offset_x_plan_end = NaN;
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
- if (task.actualStart) {
541
- x_actual_start = this.dateToX(new Date(task.actualStart));
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 = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
840
+ const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
811
841
  const fromY = fromPos.y;
812
- const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
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
- if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
867
- if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
868
- return;
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(Math.min(...[pos.offset_x_plan_start, pos.offset_x_actual_start].filter((val) => val !== null && val !== void 0)) - 8), Math.round(textY));
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(Math.max(...[pos.offset_x_plan_end, pos.offset_x_actual_end].filter((val) => val !== null && val !== void 0)) + 8), Math.round(textY));
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
- const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
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 aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
1041
- if (date >= aStart && date < aEnd) return true;
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
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gantt-canvas-chart",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "High performance Gantt chart component based on Canvas, can be applied to any framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.es.js",