gantt-canvas-chart 1.5.2 → 1.5.4

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.2
2
+ * gantt-canvas-chart v1.5.4
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -155,7 +155,9 @@ class GanttChart {
155
155
  onDataLoad = null;
156
156
  scrollLoadTimer = null;
157
157
  constructor(rootContainer, data, config = {}) {
158
- 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")) {
159
161
  throw new Error("GanttChart already exists in this container");
160
162
  }
161
163
  const container = document.createElement("div");
@@ -195,6 +197,7 @@ class GanttChart {
195
197
  offsetTop: 0,
196
198
  offsetLeft: 0,
197
199
  scrollEdgeThresholds: 10,
200
+ xGap: 0,
198
201
  enabledLoadMore: [],
199
202
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
200
203
  planBorderColor: "#C1EFCF",
@@ -251,8 +254,18 @@ class GanttChart {
251
254
  }
252
255
  buildTaskMap() {
253
256
  this.taskMap.clear();
254
- this.data.forEach((row, rowIndex) => {
255
- row.tasks.forEach((task) => this.taskMap.set(task.id, { row: rowIndex, task }));
257
+ let visibleRowIndex = 0;
258
+ this.data.forEach((row) => {
259
+ row.tasks.forEach((task) => {
260
+ this.taskMap.set(task.id, {
261
+ row: row.hide ? -1 : visibleRowIndex,
262
+ // Use -1 for hidden rows
263
+ task
264
+ });
265
+ });
266
+ if (!row.hide) {
267
+ visibleRowIndex++;
268
+ }
256
269
  });
257
270
  }
258
271
  setupEvents() {
@@ -521,10 +534,12 @@ class GanttChart {
521
534
  end: this.xToDate(this.scrollLeft + this.viewportWidth + buffer)
522
535
  };
523
536
  }
537
+ // Update the updateDimensions method to calculate height based on visible rows
524
538
  updateDimensions() {
525
539
  const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
540
+ const visibleRowCount = this.data.filter((row) => !row.hide).length;
526
541
  const newTotalWidth = totalDays * this.pixelsPerDay;
527
- const newTotalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
542
+ const newTotalHeight = visibleRowCount * this.config.rowHeight + this.config.headerHeight;
528
543
  if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
529
544
  this.totalWidth = newTotalWidth;
530
545
  this.totalHeight = newTotalHeight;
@@ -539,16 +554,26 @@ class GanttChart {
539
554
  canvas.style.height = `${height}px`;
540
555
  const ctx = canvas.getContext("2d");
541
556
  ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
557
+ ctx.textBaseline = "middle";
558
+ ctx.imageSmoothingEnabled = false;
542
559
  return ctx;
543
560
  }
561
+ /**
562
+ * 辅助函数:像素对齐(确保MAC/Windows等不同设备上1px线条清晰)
563
+ * 用于 1px 线条,使其落在 x.5 位置,填满一个物理像素,避免模糊
564
+ */
565
+ snap(val) {
566
+ return Math.floor(val) + 0.5;
567
+ }
544
568
  calculateAllTaskPositions() {
545
569
  this.taskPositions.clear();
546
- for (let i = 0; i < this.data.length; i++) {
570
+ let visibleRowIndex = 0;
571
+ for (let i = 0, len = this.data.length; i < len; i++) {
547
572
  const row = this.data[i];
548
573
  if (row.hide) {
549
574
  continue;
550
575
  }
551
- const y = i * this.config.rowHeight;
576
+ const y = visibleRowIndex * this.config.rowHeight;
552
577
  row.tasks.forEach((task) => {
553
578
  if (task.hide) {
554
579
  return;
@@ -592,9 +617,11 @@ class GanttChart {
592
617
  x_plan_width,
593
618
  x_actual_width,
594
619
  y: y + this.config.rowHeight * 0.5,
595
- row: i
620
+ row: visibleRowIndex
621
+ // Use visible row index instead of original index
596
622
  });
597
623
  });
624
+ visibleRowIndex++;
598
625
  }
599
626
  }
600
627
  getIterationStartDate(date) {
@@ -623,9 +650,9 @@ class GanttChart {
623
650
  const h = this.config.headerHeight;
624
651
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
625
652
  ctx.save();
626
- ctx.translate(-this.scrollLeft, 0);
653
+ ctx.translate(-Math.round(this.scrollLeft), 0);
627
654
  ctx.fillStyle = this.config.headerBgColor;
628
- ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
655
+ ctx.fillRect(Math.round(this.scrollLeft), 0, this.viewportWidth, h);
629
656
  ctx.textBaseline = "middle";
630
657
  ctx.textRendering = "optimizeLegibility";
631
658
  let currentDate = new Date(this.visibleDateRange.start);
@@ -703,20 +730,20 @@ class GanttChart {
703
730
  }
704
731
  const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
705
732
  ctx.fillStyle = "#333";
706
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
733
+ ctx.font = "bold 14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
707
734
  ctx.textAlign = "left";
708
735
  groupedBlocks.forEach((group) => {
709
736
  const visibleStart = Math.max(group.startX, this.scrollLeft);
710
737
  const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
711
738
  if (visibleEnd > visibleStart) {
712
739
  ctx.fillStyle = this.config.headerBgColor;
713
- ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
740
+ ctx.fillRect(Math.round(visibleStart), 0, Math.round(visibleEnd - visibleStart), Math.round(h * 0.5));
714
741
  ctx.fillStyle = "#333";
715
- ctx.fillText(group.text, visibleStart + 5, group.yPos);
742
+ ctx.fillText(group.text, Math.round(visibleStart + 5), Math.round(group.yPos));
716
743
  }
717
744
  });
718
745
  while (currentDateForLower <= this.visibleDateRange.end) {
719
- const x = this.dateToX(currentDateForLower);
746
+ const x = this.snap(this.dateToX(currentDateForLower));
720
747
  let lowerText = "";
721
748
  let nextDate;
722
749
  switch (this.config.viewMode) {
@@ -751,7 +778,7 @@ class GanttChart {
751
778
  }
752
779
  const unitWidth = this.dateToX(nextDate) - x;
753
780
  ctx.fillStyle = "#000412";
754
- ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
781
+ ctx.font = "14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
755
782
  ctx.textAlign = "center";
756
783
  ctx.fillText(lowerText, Math.round(x + unitWidth / 2), Math.round(h * 0.7));
757
784
  ctx.beginPath();
@@ -765,11 +792,6 @@ class GanttChart {
765
792
  currentDateForLower = nextDate;
766
793
  }
767
794
  }
768
- ctx.beginPath();
769
- ctx.moveTo(this.scrollLeft, h - 0.5);
770
- ctx.lineTo(this.scrollLeft + this.viewportWidth, h - 0.5);
771
- ctx.strokeStyle = "#e0e0e0";
772
- ctx.stroke();
773
795
  ctx.restore();
774
796
  }
775
797
  // Helper method to group consecutive blocks with same text
@@ -792,7 +814,7 @@ class GanttChart {
792
814
  const ctx = this.mainCtx;
793
815
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
794
816
  ctx.save();
795
- ctx.translate(-this.scrollLeft, -this.scrollTop);
817
+ ctx.translate(-Math.round(this.scrollLeft), -Math.round(this.scrollTop));
796
818
  const { start: startDate, end: endDate } = this.visibleDateRange;
797
819
  this.drawGrid(ctx, startDate, endDate);
798
820
  this.drawToday(ctx);
@@ -881,13 +903,20 @@ class GanttChart {
881
903
  }
882
904
  drawAllTasks(ctx) {
883
905
  ctx.textBaseline = "middle";
884
- ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
906
+ ctx.font = "12px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
885
907
  ctx.textRendering = "optimizeSpeed";
886
908
  ctx.imageSmoothingEnabled = false;
909
+ let visibleRowIndex = 0;
887
910
  for (let i = 0; i < this.data.length; i++) {
888
911
  const row = this.data[i];
889
- const y = i * this.config.rowHeight;
890
- if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
912
+ if (row.hide) {
913
+ continue;
914
+ }
915
+ const y = visibleRowIndex * this.config.rowHeight;
916
+ if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) {
917
+ visibleRowIndex++;
918
+ continue;
919
+ }
891
920
  row.tasks.forEach((task) => {
892
921
  const pos = this.taskPositions.get(task.id);
893
922
  if (!pos) return;
@@ -896,18 +925,27 @@ class GanttChart {
896
925
  if (!isPlanVisible && !isActualVisible) return;
897
926
  this.drawTask(ctx, task, y, pos);
898
927
  });
928
+ visibleRowIndex++;
899
929
  }
900
930
  }
931
+ // In the drawGrid method
901
932
  drawGrid(ctx, startDate, endDate) {
902
933
  ctx.strokeStyle = "#e6e6e6";
903
934
  ctx.lineWidth = 1;
904
935
  ctx.beginPath();
905
936
  if (this.config.showRowLines) {
906
- for (let i = 0; i <= this.data.length; i++) {
937
+ let visibleRowCount = 0;
938
+ for (let i = 0; i < this.data.length; i++) {
939
+ if (!this.data[i].hide) {
940
+ visibleRowCount++;
941
+ }
942
+ }
943
+ for (let i = 0; i <= visibleRowCount; i++) {
907
944
  const y = i * this.config.rowHeight;
908
945
  if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
909
- ctx.moveTo(this.scrollLeft, y);
910
- ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
946
+ const sharpY = this.snap(y);
947
+ ctx.moveTo(this.scrollLeft, sharpY);
948
+ ctx.lineTo(this.scrollLeft + this.viewportWidth, sharpY);
911
949
  }
912
950
  }
913
951
  if (this.config.showColLines) {
@@ -948,7 +986,7 @@ class GanttChart {
948
986
  currentDate = nextDate;
949
987
  }
950
988
  while (currentDate <= endDate) {
951
- const x = this.dateToX(currentDate);
989
+ const x = this.snap(this.dateToX(currentDate));
952
990
  ctx.moveTo(x, this.scrollTop);
953
991
  ctx.lineTo(x, this.scrollTop + this.viewportHeight);
954
992
  switch (this.config.viewMode) {
@@ -997,14 +1035,14 @@ class GanttChart {
997
1035
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
998
1036
  if (this.config.showActual && pos.x_actual_start) {
999
1037
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
1000
- ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
1038
+ ctx.fillRect(pos.offset_x_actual_start + this.config.xGap, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual - this.config.xGap), Math.round(taskHeight - 2));
1001
1039
  }
1002
1040
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
1003
1041
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
1004
1042
  ctx.lineWidth = 4;
1005
1043
  ctx.beginPath();
1006
- ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
1007
- ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
1044
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2 + this.config.xGap, taskY);
1045
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2 - this.config.xGap, taskY);
1008
1046
  ctx.stroke();
1009
1047
  }
1010
1048
  ctx.fillStyle = "#000";
@@ -1048,10 +1086,21 @@ class GanttChart {
1048
1086
  const mouseY = e.clientY - rect.top;
1049
1087
  const chartX = mouseX + this.scrollLeft;
1050
1088
  const chartY = mouseY + this.scrollTop;
1051
- const rowIndex = Math.floor(chartY / this.config.rowHeight);
1089
+ const visibleRowIndex = Math.floor(chartY / this.config.rowHeight);
1090
+ let actualRowIndex = -1;
1091
+ let visibleRowCount = 0;
1092
+ for (let i = 0; i < this.data.length; i++) {
1093
+ if (!this.data[i].hide) {
1094
+ if (visibleRowCount === visibleRowIndex) {
1095
+ actualRowIndex = i;
1096
+ break;
1097
+ }
1098
+ visibleRowCount++;
1099
+ }
1100
+ }
1052
1101
  const date = this.xToDate(chartX);
1053
- if (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
1054
- const row = this.data[rowIndex];
1102
+ if (actualRowIndex < 0 || actualRowIndex >= this.data.length) return this.handleMouseLeave();
1103
+ const row = this.data[actualRowIndex];
1055
1104
  if (this.config.tooltipFormat) {
1056
1105
  const htmlStr = this.config.tooltipFormat(row, date, this.config);
1057
1106
  if (!htmlStr) {
package/dist/index.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.2
2
+ * gantt-canvas-chart v1.5.4
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -10,7 +10,8 @@
10
10
  overflow: hidden;
11
11
  position: relative;
12
12
  background: white;
13
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
13
+ font-family: Roboto, PingFang SC, Noto Sans SC, Microsoft YaHei UI, Microsoft YaHei, Segoe UI, Helvetica Neue,
14
+ Helvetica, Arial, sans-serif;
14
15
  }
15
16
 
16
17
  .__gantt-chart-container {
@@ -19,7 +20,8 @@
19
20
  position: relative;
20
21
  background: #ffffff;
21
22
  height: 100%;
22
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
23
+ font-family: Roboto, PingFang SC, Noto Sans SC, Microsoft YaHei UI, Microsoft YaHei, Segoe UI, Helvetica Neue,
24
+ Helvetica, Arial, sans-serif;
23
25
  }
24
26
 
25
27
  .__gantt-scroll-dummy {
package/dist/index.d.ts CHANGED
@@ -118,6 +118,11 @@ export declare class GanttChart {
118
118
  private updateVirtualRanges;
119
119
  private updateDimensions;
120
120
  private setupCanvas;
121
+ /**
122
+ * 辅助函数:像素对齐(确保MAC/Windows等不同设备上1px线条清晰)
123
+ * 用于 1px 线条,使其落在 x.5 位置,填满一个物理像素,避免模糊
124
+ */
125
+ snap(val: number): number;
121
126
  private calculateAllTaskPositions;
122
127
  private getIterationStartDate;
123
128
  render(scrolling?: boolean): void;
@@ -178,6 +183,7 @@ export declare interface GanttConfig {
178
183
  offsetTop?: number;
179
184
  offsetLeft?: number;
180
185
  scrollEdgeThresholds?: number;
186
+ xGap?: number;
181
187
  enabledLoadMore?: [LoadMoreDirection?, LoadMoreDirection?, LoadMoreDirection?];
182
188
  viewFactors?: {
183
189
  Day: number;
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.2
2
+ * gantt-canvas-chart v1.5.4
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -153,7 +153,9 @@ class GanttChart {
153
153
  onDataLoad = null;
154
154
  scrollLoadTimer = null;
155
155
  constructor(rootContainer, data, config = {}) {
156
- 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")) {
157
159
  throw new Error("GanttChart already exists in this container");
158
160
  }
159
161
  const container = document.createElement("div");
@@ -193,6 +195,7 @@ class GanttChart {
193
195
  offsetTop: 0,
194
196
  offsetLeft: 0,
195
197
  scrollEdgeThresholds: 10,
198
+ xGap: 0,
196
199
  enabledLoadMore: [],
197
200
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
198
201
  planBorderColor: "#C1EFCF",
@@ -249,8 +252,18 @@ class GanttChart {
249
252
  }
250
253
  buildTaskMap() {
251
254
  this.taskMap.clear();
252
- this.data.forEach((row, rowIndex) => {
253
- row.tasks.forEach((task) => this.taskMap.set(task.id, { row: rowIndex, task }));
255
+ let visibleRowIndex = 0;
256
+ this.data.forEach((row) => {
257
+ row.tasks.forEach((task) => {
258
+ this.taskMap.set(task.id, {
259
+ row: row.hide ? -1 : visibleRowIndex,
260
+ // Use -1 for hidden rows
261
+ task
262
+ });
263
+ });
264
+ if (!row.hide) {
265
+ visibleRowIndex++;
266
+ }
254
267
  });
255
268
  }
256
269
  setupEvents() {
@@ -519,10 +532,12 @@ class GanttChart {
519
532
  end: this.xToDate(this.scrollLeft + this.viewportWidth + buffer)
520
533
  };
521
534
  }
535
+ // Update the updateDimensions method to calculate height based on visible rows
522
536
  updateDimensions() {
523
537
  const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
538
+ const visibleRowCount = this.data.filter((row) => !row.hide).length;
524
539
  const newTotalWidth = totalDays * this.pixelsPerDay;
525
- const newTotalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
540
+ const newTotalHeight = visibleRowCount * this.config.rowHeight + this.config.headerHeight;
526
541
  if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
527
542
  this.totalWidth = newTotalWidth;
528
543
  this.totalHeight = newTotalHeight;
@@ -537,16 +552,26 @@ class GanttChart {
537
552
  canvas.style.height = `${height}px`;
538
553
  const ctx = canvas.getContext("2d");
539
554
  ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
555
+ ctx.textBaseline = "middle";
556
+ ctx.imageSmoothingEnabled = false;
540
557
  return ctx;
541
558
  }
559
+ /**
560
+ * 辅助函数:像素对齐(确保MAC/Windows等不同设备上1px线条清晰)
561
+ * 用于 1px 线条,使其落在 x.5 位置,填满一个物理像素,避免模糊
562
+ */
563
+ snap(val) {
564
+ return Math.floor(val) + 0.5;
565
+ }
542
566
  calculateAllTaskPositions() {
543
567
  this.taskPositions.clear();
544
- for (let i = 0; i < this.data.length; i++) {
568
+ let visibleRowIndex = 0;
569
+ for (let i = 0, len = this.data.length; i < len; i++) {
545
570
  const row = this.data[i];
546
571
  if (row.hide) {
547
572
  continue;
548
573
  }
549
- const y = i * this.config.rowHeight;
574
+ const y = visibleRowIndex * this.config.rowHeight;
550
575
  row.tasks.forEach((task) => {
551
576
  if (task.hide) {
552
577
  return;
@@ -590,9 +615,11 @@ class GanttChart {
590
615
  x_plan_width,
591
616
  x_actual_width,
592
617
  y: y + this.config.rowHeight * 0.5,
593
- row: i
618
+ row: visibleRowIndex
619
+ // Use visible row index instead of original index
594
620
  });
595
621
  });
622
+ visibleRowIndex++;
596
623
  }
597
624
  }
598
625
  getIterationStartDate(date) {
@@ -621,9 +648,9 @@ class GanttChart {
621
648
  const h = this.config.headerHeight;
622
649
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
623
650
  ctx.save();
624
- ctx.translate(-this.scrollLeft, 0);
651
+ ctx.translate(-Math.round(this.scrollLeft), 0);
625
652
  ctx.fillStyle = this.config.headerBgColor;
626
- ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
653
+ ctx.fillRect(Math.round(this.scrollLeft), 0, this.viewportWidth, h);
627
654
  ctx.textBaseline = "middle";
628
655
  ctx.textRendering = "optimizeLegibility";
629
656
  let currentDate = new Date(this.visibleDateRange.start);
@@ -701,20 +728,20 @@ class GanttChart {
701
728
  }
702
729
  const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
703
730
  ctx.fillStyle = "#333";
704
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
731
+ ctx.font = "bold 14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
705
732
  ctx.textAlign = "left";
706
733
  groupedBlocks.forEach((group) => {
707
734
  const visibleStart = Math.max(group.startX, this.scrollLeft);
708
735
  const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
709
736
  if (visibleEnd > visibleStart) {
710
737
  ctx.fillStyle = this.config.headerBgColor;
711
- ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
738
+ ctx.fillRect(Math.round(visibleStart), 0, Math.round(visibleEnd - visibleStart), Math.round(h * 0.5));
712
739
  ctx.fillStyle = "#333";
713
- ctx.fillText(group.text, visibleStart + 5, group.yPos);
740
+ ctx.fillText(group.text, Math.round(visibleStart + 5), Math.round(group.yPos));
714
741
  }
715
742
  });
716
743
  while (currentDateForLower <= this.visibleDateRange.end) {
717
- const x = this.dateToX(currentDateForLower);
744
+ const x = this.snap(this.dateToX(currentDateForLower));
718
745
  let lowerText = "";
719
746
  let nextDate;
720
747
  switch (this.config.viewMode) {
@@ -749,7 +776,7 @@ class GanttChart {
749
776
  }
750
777
  const unitWidth = this.dateToX(nextDate) - x;
751
778
  ctx.fillStyle = "#000412";
752
- ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
779
+ ctx.font = "14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
753
780
  ctx.textAlign = "center";
754
781
  ctx.fillText(lowerText, Math.round(x + unitWidth / 2), Math.round(h * 0.7));
755
782
  ctx.beginPath();
@@ -763,11 +790,6 @@ class GanttChart {
763
790
  currentDateForLower = nextDate;
764
791
  }
765
792
  }
766
- ctx.beginPath();
767
- ctx.moveTo(this.scrollLeft, h - 0.5);
768
- ctx.lineTo(this.scrollLeft + this.viewportWidth, h - 0.5);
769
- ctx.strokeStyle = "#e0e0e0";
770
- ctx.stroke();
771
793
  ctx.restore();
772
794
  }
773
795
  // Helper method to group consecutive blocks with same text
@@ -790,7 +812,7 @@ class GanttChart {
790
812
  const ctx = this.mainCtx;
791
813
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
792
814
  ctx.save();
793
- ctx.translate(-this.scrollLeft, -this.scrollTop);
815
+ ctx.translate(-Math.round(this.scrollLeft), -Math.round(this.scrollTop));
794
816
  const { start: startDate, end: endDate } = this.visibleDateRange;
795
817
  this.drawGrid(ctx, startDate, endDate);
796
818
  this.drawToday(ctx);
@@ -879,13 +901,20 @@ class GanttChart {
879
901
  }
880
902
  drawAllTasks(ctx) {
881
903
  ctx.textBaseline = "middle";
882
- ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
904
+ ctx.font = "12px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
883
905
  ctx.textRendering = "optimizeSpeed";
884
906
  ctx.imageSmoothingEnabled = false;
907
+ let visibleRowIndex = 0;
885
908
  for (let i = 0; i < this.data.length; i++) {
886
909
  const row = this.data[i];
887
- const y = i * this.config.rowHeight;
888
- if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
910
+ if (row.hide) {
911
+ continue;
912
+ }
913
+ const y = visibleRowIndex * this.config.rowHeight;
914
+ if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) {
915
+ visibleRowIndex++;
916
+ continue;
917
+ }
889
918
  row.tasks.forEach((task) => {
890
919
  const pos = this.taskPositions.get(task.id);
891
920
  if (!pos) return;
@@ -894,18 +923,27 @@ class GanttChart {
894
923
  if (!isPlanVisible && !isActualVisible) return;
895
924
  this.drawTask(ctx, task, y, pos);
896
925
  });
926
+ visibleRowIndex++;
897
927
  }
898
928
  }
929
+ // In the drawGrid method
899
930
  drawGrid(ctx, startDate, endDate) {
900
931
  ctx.strokeStyle = "#e6e6e6";
901
932
  ctx.lineWidth = 1;
902
933
  ctx.beginPath();
903
934
  if (this.config.showRowLines) {
904
- for (let i = 0; i <= this.data.length; i++) {
935
+ let visibleRowCount = 0;
936
+ for (let i = 0; i < this.data.length; i++) {
937
+ if (!this.data[i].hide) {
938
+ visibleRowCount++;
939
+ }
940
+ }
941
+ for (let i = 0; i <= visibleRowCount; i++) {
905
942
  const y = i * this.config.rowHeight;
906
943
  if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
907
- ctx.moveTo(this.scrollLeft, y);
908
- ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
944
+ const sharpY = this.snap(y);
945
+ ctx.moveTo(this.scrollLeft, sharpY);
946
+ ctx.lineTo(this.scrollLeft + this.viewportWidth, sharpY);
909
947
  }
910
948
  }
911
949
  if (this.config.showColLines) {
@@ -946,7 +984,7 @@ class GanttChart {
946
984
  currentDate = nextDate;
947
985
  }
948
986
  while (currentDate <= endDate) {
949
- const x = this.dateToX(currentDate);
987
+ const x = this.snap(this.dateToX(currentDate));
950
988
  ctx.moveTo(x, this.scrollTop);
951
989
  ctx.lineTo(x, this.scrollTop + this.viewportHeight);
952
990
  switch (this.config.viewMode) {
@@ -995,14 +1033,14 @@ class GanttChart {
995
1033
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
996
1034
  if (this.config.showActual && pos.x_actual_start) {
997
1035
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
998
- ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
1036
+ ctx.fillRect(pos.offset_x_actual_start + this.config.xGap, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual - this.config.xGap), Math.round(taskHeight - 2));
999
1037
  }
1000
1038
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
1001
1039
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
1002
1040
  ctx.lineWidth = 4;
1003
1041
  ctx.beginPath();
1004
- ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
1005
- ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
1042
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2 + this.config.xGap, taskY);
1043
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2 - this.config.xGap, taskY);
1006
1044
  ctx.stroke();
1007
1045
  }
1008
1046
  ctx.fillStyle = "#000";
@@ -1046,10 +1084,21 @@ class GanttChart {
1046
1084
  const mouseY = e.clientY - rect.top;
1047
1085
  const chartX = mouseX + this.scrollLeft;
1048
1086
  const chartY = mouseY + this.scrollTop;
1049
- const rowIndex = Math.floor(chartY / this.config.rowHeight);
1087
+ const visibleRowIndex = Math.floor(chartY / this.config.rowHeight);
1088
+ let actualRowIndex = -1;
1089
+ let visibleRowCount = 0;
1090
+ for (let i = 0; i < this.data.length; i++) {
1091
+ if (!this.data[i].hide) {
1092
+ if (visibleRowCount === visibleRowIndex) {
1093
+ actualRowIndex = i;
1094
+ break;
1095
+ }
1096
+ visibleRowCount++;
1097
+ }
1098
+ }
1050
1099
  const date = this.xToDate(chartX);
1051
- if (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
1052
- const row = this.data[rowIndex];
1100
+ if (actualRowIndex < 0 || actualRowIndex >= this.data.length) return this.handleMouseLeave();
1101
+ const row = this.data[actualRowIndex];
1053
1102
  if (this.config.tooltipFormat) {
1054
1103
  const htmlStr = this.config.tooltipFormat(row, date, this.config);
1055
1104
  if (!htmlStr) {
package/dist/index.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.2
2
+ * gantt-canvas-chart v1.5.4
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -157,7 +157,9 @@
157
157
  onDataLoad = null;
158
158
  scrollLoadTimer = null;
159
159
  constructor(rootContainer, data, config = {}) {
160
- 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")) {
161
163
  throw new Error("GanttChart already exists in this container");
162
164
  }
163
165
  const container = document.createElement("div");
@@ -197,6 +199,7 @@
197
199
  offsetTop: 0,
198
200
  offsetLeft: 0,
199
201
  scrollEdgeThresholds: 10,
202
+ xGap: 0,
200
203
  enabledLoadMore: [],
201
204
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
202
205
  planBorderColor: "#C1EFCF",
@@ -253,8 +256,18 @@
253
256
  }
254
257
  buildTaskMap() {
255
258
  this.taskMap.clear();
256
- this.data.forEach((row, rowIndex) => {
257
- row.tasks.forEach((task) => this.taskMap.set(task.id, { row: rowIndex, task }));
259
+ let visibleRowIndex = 0;
260
+ this.data.forEach((row) => {
261
+ row.tasks.forEach((task) => {
262
+ this.taskMap.set(task.id, {
263
+ row: row.hide ? -1 : visibleRowIndex,
264
+ // Use -1 for hidden rows
265
+ task
266
+ });
267
+ });
268
+ if (!row.hide) {
269
+ visibleRowIndex++;
270
+ }
258
271
  });
259
272
  }
260
273
  setupEvents() {
@@ -523,10 +536,12 @@
523
536
  end: this.xToDate(this.scrollLeft + this.viewportWidth + buffer)
524
537
  };
525
538
  }
539
+ // Update the updateDimensions method to calculate height based on visible rows
526
540
  updateDimensions() {
527
541
  const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
542
+ const visibleRowCount = this.data.filter((row) => !row.hide).length;
528
543
  const newTotalWidth = totalDays * this.pixelsPerDay;
529
- const newTotalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
544
+ const newTotalHeight = visibleRowCount * this.config.rowHeight + this.config.headerHeight;
530
545
  if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
531
546
  this.totalWidth = newTotalWidth;
532
547
  this.totalHeight = newTotalHeight;
@@ -541,16 +556,26 @@
541
556
  canvas.style.height = `${height}px`;
542
557
  const ctx = canvas.getContext("2d");
543
558
  ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
559
+ ctx.textBaseline = "middle";
560
+ ctx.imageSmoothingEnabled = false;
544
561
  return ctx;
545
562
  }
563
+ /**
564
+ * 辅助函数:像素对齐(确保MAC/Windows等不同设备上1px线条清晰)
565
+ * 用于 1px 线条,使其落在 x.5 位置,填满一个物理像素,避免模糊
566
+ */
567
+ snap(val) {
568
+ return Math.floor(val) + 0.5;
569
+ }
546
570
  calculateAllTaskPositions() {
547
571
  this.taskPositions.clear();
548
- for (let i = 0; i < this.data.length; i++) {
572
+ let visibleRowIndex = 0;
573
+ for (let i = 0, len = this.data.length; i < len; i++) {
549
574
  const row = this.data[i];
550
575
  if (row.hide) {
551
576
  continue;
552
577
  }
553
- const y = i * this.config.rowHeight;
578
+ const y = visibleRowIndex * this.config.rowHeight;
554
579
  row.tasks.forEach((task) => {
555
580
  if (task.hide) {
556
581
  return;
@@ -594,9 +619,11 @@
594
619
  x_plan_width,
595
620
  x_actual_width,
596
621
  y: y + this.config.rowHeight * 0.5,
597
- row: i
622
+ row: visibleRowIndex
623
+ // Use visible row index instead of original index
598
624
  });
599
625
  });
626
+ visibleRowIndex++;
600
627
  }
601
628
  }
602
629
  getIterationStartDate(date) {
@@ -625,9 +652,9 @@
625
652
  const h = this.config.headerHeight;
626
653
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
627
654
  ctx.save();
628
- ctx.translate(-this.scrollLeft, 0);
655
+ ctx.translate(-Math.round(this.scrollLeft), 0);
629
656
  ctx.fillStyle = this.config.headerBgColor;
630
- ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
657
+ ctx.fillRect(Math.round(this.scrollLeft), 0, this.viewportWidth, h);
631
658
  ctx.textBaseline = "middle";
632
659
  ctx.textRendering = "optimizeLegibility";
633
660
  let currentDate = new Date(this.visibleDateRange.start);
@@ -705,20 +732,20 @@
705
732
  }
706
733
  const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
707
734
  ctx.fillStyle = "#333";
708
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
735
+ ctx.font = "bold 14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
709
736
  ctx.textAlign = "left";
710
737
  groupedBlocks.forEach((group) => {
711
738
  const visibleStart = Math.max(group.startX, this.scrollLeft);
712
739
  const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
713
740
  if (visibleEnd > visibleStart) {
714
741
  ctx.fillStyle = this.config.headerBgColor;
715
- ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
742
+ ctx.fillRect(Math.round(visibleStart), 0, Math.round(visibleEnd - visibleStart), Math.round(h * 0.5));
716
743
  ctx.fillStyle = "#333";
717
- ctx.fillText(group.text, visibleStart + 5, group.yPos);
744
+ ctx.fillText(group.text, Math.round(visibleStart + 5), Math.round(group.yPos));
718
745
  }
719
746
  });
720
747
  while (currentDateForLower <= this.visibleDateRange.end) {
721
- const x = this.dateToX(currentDateForLower);
748
+ const x = this.snap(this.dateToX(currentDateForLower));
722
749
  let lowerText = "";
723
750
  let nextDate;
724
751
  switch (this.config.viewMode) {
@@ -753,7 +780,7 @@
753
780
  }
754
781
  const unitWidth = this.dateToX(nextDate) - x;
755
782
  ctx.fillStyle = "#000412";
756
- ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
783
+ ctx.font = "14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
757
784
  ctx.textAlign = "center";
758
785
  ctx.fillText(lowerText, Math.round(x + unitWidth / 2), Math.round(h * 0.7));
759
786
  ctx.beginPath();
@@ -767,11 +794,6 @@
767
794
  currentDateForLower = nextDate;
768
795
  }
769
796
  }
770
- ctx.beginPath();
771
- ctx.moveTo(this.scrollLeft, h - 0.5);
772
- ctx.lineTo(this.scrollLeft + this.viewportWidth, h - 0.5);
773
- ctx.strokeStyle = "#e0e0e0";
774
- ctx.stroke();
775
797
  ctx.restore();
776
798
  }
777
799
  // Helper method to group consecutive blocks with same text
@@ -794,7 +816,7 @@
794
816
  const ctx = this.mainCtx;
795
817
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
796
818
  ctx.save();
797
- ctx.translate(-this.scrollLeft, -this.scrollTop);
819
+ ctx.translate(-Math.round(this.scrollLeft), -Math.round(this.scrollTop));
798
820
  const { start: startDate, end: endDate } = this.visibleDateRange;
799
821
  this.drawGrid(ctx, startDate, endDate);
800
822
  this.drawToday(ctx);
@@ -883,13 +905,20 @@
883
905
  }
884
906
  drawAllTasks(ctx) {
885
907
  ctx.textBaseline = "middle";
886
- ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
908
+ ctx.font = "12px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
887
909
  ctx.textRendering = "optimizeSpeed";
888
910
  ctx.imageSmoothingEnabled = false;
911
+ let visibleRowIndex = 0;
889
912
  for (let i = 0; i < this.data.length; i++) {
890
913
  const row = this.data[i];
891
- const y = i * this.config.rowHeight;
892
- if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
914
+ if (row.hide) {
915
+ continue;
916
+ }
917
+ const y = visibleRowIndex * this.config.rowHeight;
918
+ if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) {
919
+ visibleRowIndex++;
920
+ continue;
921
+ }
893
922
  row.tasks.forEach((task) => {
894
923
  const pos = this.taskPositions.get(task.id);
895
924
  if (!pos) return;
@@ -898,18 +927,27 @@
898
927
  if (!isPlanVisible && !isActualVisible) return;
899
928
  this.drawTask(ctx, task, y, pos);
900
929
  });
930
+ visibleRowIndex++;
901
931
  }
902
932
  }
933
+ // In the drawGrid method
903
934
  drawGrid(ctx, startDate, endDate) {
904
935
  ctx.strokeStyle = "#e6e6e6";
905
936
  ctx.lineWidth = 1;
906
937
  ctx.beginPath();
907
938
  if (this.config.showRowLines) {
908
- for (let i = 0; i <= this.data.length; i++) {
939
+ let visibleRowCount = 0;
940
+ for (let i = 0; i < this.data.length; i++) {
941
+ if (!this.data[i].hide) {
942
+ visibleRowCount++;
943
+ }
944
+ }
945
+ for (let i = 0; i <= visibleRowCount; i++) {
909
946
  const y = i * this.config.rowHeight;
910
947
  if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
911
- ctx.moveTo(this.scrollLeft, y);
912
- ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
948
+ const sharpY = this.snap(y);
949
+ ctx.moveTo(this.scrollLeft, sharpY);
950
+ ctx.lineTo(this.scrollLeft + this.viewportWidth, sharpY);
913
951
  }
914
952
  }
915
953
  if (this.config.showColLines) {
@@ -950,7 +988,7 @@
950
988
  currentDate = nextDate;
951
989
  }
952
990
  while (currentDate <= endDate) {
953
- const x = this.dateToX(currentDate);
991
+ const x = this.snap(this.dateToX(currentDate));
954
992
  ctx.moveTo(x, this.scrollTop);
955
993
  ctx.lineTo(x, this.scrollTop + this.viewportHeight);
956
994
  switch (this.config.viewMode) {
@@ -999,14 +1037,14 @@
999
1037
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
1000
1038
  if (this.config.showActual && pos.x_actual_start) {
1001
1039
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
1002
- ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
1040
+ ctx.fillRect(pos.offset_x_actual_start + this.config.xGap, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual - this.config.xGap), Math.round(taskHeight - 2));
1003
1041
  }
1004
1042
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
1005
1043
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
1006
1044
  ctx.lineWidth = 4;
1007
1045
  ctx.beginPath();
1008
- ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
1009
- ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
1046
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2 + this.config.xGap, taskY);
1047
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2 - this.config.xGap, taskY);
1010
1048
  ctx.stroke();
1011
1049
  }
1012
1050
  ctx.fillStyle = "#000";
@@ -1050,10 +1088,21 @@
1050
1088
  const mouseY = e.clientY - rect.top;
1051
1089
  const chartX = mouseX + this.scrollLeft;
1052
1090
  const chartY = mouseY + this.scrollTop;
1053
- const rowIndex = Math.floor(chartY / this.config.rowHeight);
1091
+ const visibleRowIndex = Math.floor(chartY / this.config.rowHeight);
1092
+ let actualRowIndex = -1;
1093
+ let visibleRowCount = 0;
1094
+ for (let i = 0; i < this.data.length; i++) {
1095
+ if (!this.data[i].hide) {
1096
+ if (visibleRowCount === visibleRowIndex) {
1097
+ actualRowIndex = i;
1098
+ break;
1099
+ }
1100
+ visibleRowCount++;
1101
+ }
1102
+ }
1054
1103
  const date = this.xToDate(chartX);
1055
- if (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
1056
- const row = this.data[rowIndex];
1104
+ if (actualRowIndex < 0 || actualRowIndex >= this.data.length) return this.handleMouseLeave();
1105
+ const row = this.data[actualRowIndex];
1057
1106
  if (this.config.tooltipFormat) {
1058
1107
  const htmlStr = this.config.tooltipFormat(row, date, this.config);
1059
1108
  if (!htmlStr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gantt-canvas-chart",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
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",