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 CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.1
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.querySelector(".__gantt-chart-container")) {
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
- this.data.forEach((row, rowIndex) => {
221
- row.tasks.forEach((task) => this.taskMap.set(task.id, { row: rowIndex, task }));
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 = this.data.length * this.config.rowHeight + this.config.headerHeight;
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
- for (let i = 0; i < this.data.length; i++) {
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 = i * this.config.rowHeight;
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 = NaN, offset_x_plan_end = NaN;
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: i
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 = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
855
+ const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
805
856
  const fromY = fromPos.y;
806
- const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
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
- const y = i * this.config.rowHeight;
856
- if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
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
- for (let i = 0; i <= this.data.length; i++) {
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(Math.min(...[pos.offset_x_plan_start, pos.offset_x_actual_start].filter((val) => val !== null && val !== void 0)) - 8 * 2), Math.round(textY));
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(Math.max(...[pos.offset_x_plan_end, pos.offset_x_actual_end].filter((val) => val !== null && val !== void 0)) + 8 * 2), Math.round(textY));
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 rowIndex = Math.floor(chartY / this.config.rowHeight);
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 (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
1020
- const row = this.data[rowIndex];
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
- const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
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 aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
1034
- if (date >= aStart && date < aEnd) return true;
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
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.1
2
+ * gantt-canvas-chart v1.5.3
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;
@@ -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.1
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.querySelector(".__gantt-chart-container")) {
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
- this.data.forEach((row, rowIndex) => {
219
- row.tasks.forEach((task) => this.taskMap.set(task.id, { row: rowIndex, task }));
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 = this.data.length * this.config.rowHeight + this.config.headerHeight;
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
- for (let i = 0; i < this.data.length; i++) {
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 = i * this.config.rowHeight;
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 = NaN, offset_x_plan_end = NaN;
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: i
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 = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
853
+ const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
803
854
  const fromY = fromPos.y;
804
- const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
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
- const y = i * this.config.rowHeight;
854
- if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
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
- for (let i = 0; i <= this.data.length; i++) {
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(Math.min(...[pos.offset_x_plan_start, pos.offset_x_actual_start].filter((val) => val !== null && val !== void 0)) - 8 * 2), Math.round(textY));
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(Math.max(...[pos.offset_x_plan_end, pos.offset_x_actual_end].filter((val) => val !== null && val !== void 0)) + 8 * 2), Math.round(textY));
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 rowIndex = Math.floor(chartY / this.config.rowHeight);
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 (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
1018
- const row = this.data[rowIndex];
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
- const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
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 aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
1032
- if (date >= aStart && date < aEnd) return true;
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
- firstValidValue
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.1
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.querySelector(".__gantt-chart-container")) {
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
- this.data.forEach((row, rowIndex) => {
223
- row.tasks.forEach((task) => this.taskMap.set(task.id, { row: rowIndex, task }));
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 = this.data.length * this.config.rowHeight + this.config.headerHeight;
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
- for (let i = 0; i < this.data.length; i++) {
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 = i * this.config.rowHeight;
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 = NaN, offset_x_plan_end = NaN;
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: i
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 = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
857
+ const fromX = getMinMax([fromPos.offset_x_plan_end, fromPos.offset_x_actual_end])?.max;
807
858
  const fromY = fromPos.y;
808
- const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
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
- const y = i * this.config.rowHeight;
858
- if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
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
- for (let i = 0; i <= this.data.length; i++) {
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(Math.min(...[pos.offset_x_plan_start, pos.offset_x_actual_start].filter((val) => val !== null && val !== void 0)) - 8 * 2), Math.round(textY));
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(Math.max(...[pos.offset_x_plan_end, pos.offset_x_actual_end].filter((val) => val !== null && val !== void 0)) + 8 * 2), Math.round(textY));
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 rowIndex = Math.floor(chartY / this.config.rowHeight);
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 (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
1022
- const row = this.data[rowIndex];
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
- const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
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 aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
1036
- if (date >= aStart && date < aEnd) return true;
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
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gantt-canvas-chart",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
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",