gantt-canvas-chart 1.5.3 → 1.6.0

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.3
2
+ * gantt-canvas-chart v1.6.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -146,6 +146,7 @@ class GanttChart {
146
146
  resizeObserver;
147
147
  taskPositions;
148
148
  taskMap;
149
+ holidaysMap;
149
150
  isLoadingData = false;
150
151
  hasMoreDataLeft = true;
151
152
  hasMoreDataRight = true;
@@ -194,9 +195,13 @@ class GanttChart {
194
195
  tooltipFormat: null,
195
196
  tooltipColor: "black",
196
197
  todayColor: "#ff4d4f",
198
+ weekendBgColor: "#f7f7f7",
199
+ holidays: [],
200
+ dateSeparator: "/",
197
201
  offsetTop: 0,
198
202
  offsetLeft: 0,
199
203
  scrollEdgeThresholds: 10,
204
+ xGap: 0,
200
205
  enabledLoadMore: [],
201
206
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
202
207
  planBorderColor: "#C1EFCF",
@@ -231,6 +236,7 @@ class GanttChart {
231
236
  this.totalHeight = 0;
232
237
  this.taskPositions = /* @__PURE__ */ new Map();
233
238
  this.taskMap = /* @__PURE__ */ new Map();
239
+ this.holidaysMap = /* @__PURE__ */ new Map();
234
240
  this.handleMouseMove = this.handleMouseMove.bind(this);
235
241
  this.handleMouseLeave = this.handleMouseLeave.bind(this);
236
242
  this.handleScroll = this.handleScroll.bind(this);
@@ -240,6 +246,9 @@ class GanttChart {
240
246
  this.init();
241
247
  }
242
248
  init() {
249
+ if (this.config.holidays.length > 0) {
250
+ this.buildHolidaysMap();
251
+ }
243
252
  this.buildTaskMap();
244
253
  this.updatePixelsPerDay();
245
254
  this.calculateFullTimeline();
@@ -288,10 +297,26 @@ class GanttChart {
288
297
  this.updatePixelsPerDay();
289
298
  this.calculateFullTimeline();
290
299
  }
300
+ if (this.config.holidays.length !== this.holidaysMap.size) {
301
+ this.buildHolidaysMap();
302
+ }
291
303
  this.updateLoadMoreConf();
292
304
  this.updateDimensions();
293
305
  this.render();
294
306
  }
307
+ buildHolidaysMap() {
308
+ this.holidaysMap.clear();
309
+ const separator = this.config.dateSeparator;
310
+ if (this.config.holidays && this.config.holidays.length > 0 && this.config.holidays[0].includes(separator)) {
311
+ this.config.holidays.forEach((holiday) => {
312
+ this.holidaysMap.set(holiday, true);
313
+ });
314
+ } else {
315
+ this.config.holidays.forEach((holiday) => {
316
+ this.holidaysMap.set(DateUtils.format(new Date(holiday), `yyyy${separator}MM${separator}dd`), true);
317
+ });
318
+ }
319
+ }
295
320
  setData(newData, newConfig) {
296
321
  this.data = newData;
297
322
  this.buildTaskMap();
@@ -553,8 +578,17 @@ class GanttChart {
553
578
  canvas.style.height = `${height}px`;
554
579
  const ctx = canvas.getContext("2d");
555
580
  ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
581
+ ctx.textBaseline = "middle";
582
+ ctx.imageSmoothingEnabled = false;
556
583
  return ctx;
557
584
  }
585
+ /**
586
+ * 辅助函数:像素对齐(确保MAC/Windows等不同设备上1px线条清晰)
587
+ * 用于 1px 线条,使其落在 x.5 位置,填满一个物理像素,避免模糊
588
+ */
589
+ snap(val) {
590
+ return Math.floor(val) + 0.5;
591
+ }
558
592
  calculateAllTaskPositions() {
559
593
  this.taskPositions.clear();
560
594
  let visibleRowIndex = 0;
@@ -640,9 +674,9 @@ class GanttChart {
640
674
  const h = this.config.headerHeight;
641
675
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
642
676
  ctx.save();
643
- ctx.translate(-this.scrollLeft, 0);
677
+ ctx.translate(-Math.round(this.scrollLeft), 0);
644
678
  ctx.fillStyle = this.config.headerBgColor;
645
- ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
679
+ ctx.fillRect(Math.round(this.scrollLeft), 0, this.viewportWidth, h);
646
680
  ctx.textBaseline = "middle";
647
681
  ctx.textRendering = "optimizeLegibility";
648
682
  let currentDate = new Date(this.visibleDateRange.start);
@@ -720,20 +754,20 @@ class GanttChart {
720
754
  }
721
755
  const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
722
756
  ctx.fillStyle = "#333";
723
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
757
+ ctx.font = "bold 14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
724
758
  ctx.textAlign = "left";
725
759
  groupedBlocks.forEach((group) => {
726
760
  const visibleStart = Math.max(group.startX, this.scrollLeft);
727
761
  const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
728
762
  if (visibleEnd > visibleStart) {
729
763
  ctx.fillStyle = this.config.headerBgColor;
730
- ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
764
+ ctx.fillRect(Math.round(visibleStart), 0, Math.round(visibleEnd - visibleStart), Math.round(h * 0.5));
731
765
  ctx.fillStyle = "#333";
732
- ctx.fillText(group.text, visibleStart + 5, group.yPos);
766
+ ctx.fillText(group.text, Math.round(visibleStart + 5), Math.round(group.yPos));
733
767
  }
734
768
  });
735
769
  while (currentDateForLower <= this.visibleDateRange.end) {
736
- const x = this.dateToX(currentDateForLower);
770
+ const x = this.snap(this.dateToX(currentDateForLower));
737
771
  let lowerText = "";
738
772
  let nextDate;
739
773
  switch (this.config.viewMode) {
@@ -768,7 +802,7 @@ class GanttChart {
768
802
  }
769
803
  const unitWidth = this.dateToX(nextDate) - x;
770
804
  ctx.fillStyle = "#000412";
771
- ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
805
+ ctx.font = "14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
772
806
  ctx.textAlign = "center";
773
807
  ctx.fillText(lowerText, Math.round(x + unitWidth / 2), Math.round(h * 0.7));
774
808
  ctx.beginPath();
@@ -782,11 +816,6 @@ class GanttChart {
782
816
  currentDateForLower = nextDate;
783
817
  }
784
818
  }
785
- ctx.beginPath();
786
- ctx.moveTo(this.scrollLeft, h - 0.5);
787
- ctx.lineTo(this.scrollLeft + this.viewportWidth, h - 0.5);
788
- ctx.strokeStyle = "#e0e0e0";
789
- ctx.stroke();
790
819
  ctx.restore();
791
820
  }
792
821
  // Helper method to group consecutive blocks with same text
@@ -809,7 +838,7 @@ class GanttChart {
809
838
  const ctx = this.mainCtx;
810
839
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
811
840
  ctx.save();
812
- ctx.translate(-this.scrollLeft, -this.scrollTop);
841
+ ctx.translate(-Math.round(this.scrollLeft), -Math.round(this.scrollTop));
813
842
  const { start: startDate, end: endDate } = this.visibleDateRange;
814
843
  this.drawGrid(ctx, startDate, endDate);
815
844
  this.drawToday(ctx);
@@ -898,7 +927,7 @@ class GanttChart {
898
927
  }
899
928
  drawAllTasks(ctx) {
900
929
  ctx.textBaseline = "middle";
901
- ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
930
+ ctx.font = "12px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
902
931
  ctx.textRendering = "optimizeSpeed";
903
932
  ctx.imageSmoothingEnabled = false;
904
933
  let visibleRowIndex = 0;
@@ -925,6 +954,7 @@ class GanttChart {
925
954
  }
926
955
  // In the drawGrid method
927
956
  drawGrid(ctx, startDate, endDate) {
957
+ const separator = this.config.dateSeparator;
928
958
  ctx.strokeStyle = "#e6e6e6";
929
959
  ctx.lineWidth = 1;
930
960
  ctx.beginPath();
@@ -938,8 +968,9 @@ class GanttChart {
938
968
  for (let i = 0; i <= visibleRowCount; i++) {
939
969
  const y = i * this.config.rowHeight;
940
970
  if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
941
- ctx.moveTo(this.scrollLeft, y);
942
- ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
971
+ const sharpY = this.snap(y);
972
+ ctx.moveTo(this.scrollLeft, sharpY);
973
+ ctx.lineTo(this.scrollLeft + this.viewportWidth, sharpY);
943
974
  }
944
975
  }
945
976
  if (this.config.showColLines) {
@@ -980,9 +1011,15 @@ class GanttChart {
980
1011
  currentDate = nextDate;
981
1012
  }
982
1013
  while (currentDate <= endDate) {
983
- const x = this.dateToX(currentDate);
1014
+ const x = this.snap(this.dateToX(currentDate));
984
1015
  ctx.moveTo(x, this.scrollTop);
985
1016
  ctx.lineTo(x, this.scrollTop + this.viewportHeight);
1017
+ if (this.config.viewMode === "Day") {
1018
+ if ([0, 6].includes(currentDate.getDay()) || this.holidaysMap.has(DateUtils.format(currentDate, `yyyy${separator}MM${separator}dd`))) {
1019
+ ctx.fillStyle = this.config.weekendBgColor;
1020
+ ctx.fillRect(x + 1, this.scrollTop, Math.round(this.pixelsPerDay - 1), Math.round(this.scrollTop + this.viewportHeight));
1021
+ }
1022
+ }
986
1023
  switch (this.config.viewMode) {
987
1024
  case "Day":
988
1025
  nextDate = DateUtils.addDays(currentDate, 1);
@@ -1029,14 +1066,14 @@ class GanttChart {
1029
1066
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
1030
1067
  if (this.config.showActual && pos.x_actual_start) {
1031
1068
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
1032
- ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
1069
+ 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));
1033
1070
  }
1034
1071
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
1035
1072
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
1036
1073
  ctx.lineWidth = 4;
1037
1074
  ctx.beginPath();
1038
- ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
1039
- ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
1075
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2 + this.config.xGap, taskY);
1076
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2 - this.config.xGap, taskY);
1040
1077
  ctx.stroke();
1041
1078
  }
1042
1079
  ctx.fillStyle = "#000";
package/dist/index.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.3
2
+ * gantt-canvas-chart v1.6.0
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
@@ -85,6 +85,7 @@ export declare class GanttChart {
85
85
  row: number;
86
86
  task: Task;
87
87
  }>;
88
+ holidaysMap: Map<string, boolean>;
88
89
  private isLoadingData;
89
90
  private hasMoreDataLeft;
90
91
  private hasMoreDataRight;
@@ -99,6 +100,7 @@ export declare class GanttChart {
99
100
  private buildTaskMap;
100
101
  private setupEvents;
101
102
  updateConfig(newConfig: GanttConfig): void;
103
+ private buildHolidaysMap;
102
104
  setData(newData: GanttData, newConfig?: GanttConfig): void;
103
105
  destroy(): void;
104
106
  private calculateFullTimeline;
@@ -118,6 +120,11 @@ export declare class GanttChart {
118
120
  private updateVirtualRanges;
119
121
  private updateDimensions;
120
122
  private setupCanvas;
123
+ /**
124
+ * 辅助函数:像素对齐(确保MAC/Windows等不同设备上1px线条清晰)
125
+ * 用于 1px 线条,使其落在 x.5 位置,填满一个物理像素,避免模糊
126
+ */
127
+ snap(val: number): number;
121
128
  private calculateAllTaskPositions;
122
129
  private getIterationStartDate;
123
130
  render(scrolling?: boolean): void;
@@ -175,9 +182,13 @@ export declare interface GanttConfig {
175
182
  showTooltip?: boolean;
176
183
  tooltipColor?: 'black' | 'white';
177
184
  todayColor?: string;
185
+ weekendBgColor?: string;
186
+ holidays?: string[];
187
+ dateSeparator?: string;
178
188
  offsetTop?: number;
179
189
  offsetLeft?: number;
180
190
  scrollEdgeThresholds?: number;
191
+ xGap?: number;
181
192
  enabledLoadMore?: [LoadMoreDirection?, LoadMoreDirection?, LoadMoreDirection?];
182
193
  viewFactors?: {
183
194
  Day: number;
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.3
2
+ * gantt-canvas-chart v1.6.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -144,6 +144,7 @@ class GanttChart {
144
144
  resizeObserver;
145
145
  taskPositions;
146
146
  taskMap;
147
+ holidaysMap;
147
148
  isLoadingData = false;
148
149
  hasMoreDataLeft = true;
149
150
  hasMoreDataRight = true;
@@ -192,9 +193,13 @@ class GanttChart {
192
193
  tooltipFormat: null,
193
194
  tooltipColor: "black",
194
195
  todayColor: "#ff4d4f",
196
+ weekendBgColor: "#f7f7f7",
197
+ holidays: [],
198
+ dateSeparator: "/",
195
199
  offsetTop: 0,
196
200
  offsetLeft: 0,
197
201
  scrollEdgeThresholds: 10,
202
+ xGap: 0,
198
203
  enabledLoadMore: [],
199
204
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
200
205
  planBorderColor: "#C1EFCF",
@@ -229,6 +234,7 @@ class GanttChart {
229
234
  this.totalHeight = 0;
230
235
  this.taskPositions = /* @__PURE__ */ new Map();
231
236
  this.taskMap = /* @__PURE__ */ new Map();
237
+ this.holidaysMap = /* @__PURE__ */ new Map();
232
238
  this.handleMouseMove = this.handleMouseMove.bind(this);
233
239
  this.handleMouseLeave = this.handleMouseLeave.bind(this);
234
240
  this.handleScroll = this.handleScroll.bind(this);
@@ -238,6 +244,9 @@ class GanttChart {
238
244
  this.init();
239
245
  }
240
246
  init() {
247
+ if (this.config.holidays.length > 0) {
248
+ this.buildHolidaysMap();
249
+ }
241
250
  this.buildTaskMap();
242
251
  this.updatePixelsPerDay();
243
252
  this.calculateFullTimeline();
@@ -286,10 +295,26 @@ class GanttChart {
286
295
  this.updatePixelsPerDay();
287
296
  this.calculateFullTimeline();
288
297
  }
298
+ if (this.config.holidays.length !== this.holidaysMap.size) {
299
+ this.buildHolidaysMap();
300
+ }
289
301
  this.updateLoadMoreConf();
290
302
  this.updateDimensions();
291
303
  this.render();
292
304
  }
305
+ buildHolidaysMap() {
306
+ this.holidaysMap.clear();
307
+ const separator = this.config.dateSeparator;
308
+ if (this.config.holidays && this.config.holidays.length > 0 && this.config.holidays[0].includes(separator)) {
309
+ this.config.holidays.forEach((holiday) => {
310
+ this.holidaysMap.set(holiday, true);
311
+ });
312
+ } else {
313
+ this.config.holidays.forEach((holiday) => {
314
+ this.holidaysMap.set(DateUtils.format(new Date(holiday), `yyyy${separator}MM${separator}dd`), true);
315
+ });
316
+ }
317
+ }
293
318
  setData(newData, newConfig) {
294
319
  this.data = newData;
295
320
  this.buildTaskMap();
@@ -551,8 +576,17 @@ class GanttChart {
551
576
  canvas.style.height = `${height}px`;
552
577
  const ctx = canvas.getContext("2d");
553
578
  ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
579
+ ctx.textBaseline = "middle";
580
+ ctx.imageSmoothingEnabled = false;
554
581
  return ctx;
555
582
  }
583
+ /**
584
+ * 辅助函数:像素对齐(确保MAC/Windows等不同设备上1px线条清晰)
585
+ * 用于 1px 线条,使其落在 x.5 位置,填满一个物理像素,避免模糊
586
+ */
587
+ snap(val) {
588
+ return Math.floor(val) + 0.5;
589
+ }
556
590
  calculateAllTaskPositions() {
557
591
  this.taskPositions.clear();
558
592
  let visibleRowIndex = 0;
@@ -638,9 +672,9 @@ class GanttChart {
638
672
  const h = this.config.headerHeight;
639
673
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
640
674
  ctx.save();
641
- ctx.translate(-this.scrollLeft, 0);
675
+ ctx.translate(-Math.round(this.scrollLeft), 0);
642
676
  ctx.fillStyle = this.config.headerBgColor;
643
- ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
677
+ ctx.fillRect(Math.round(this.scrollLeft), 0, this.viewportWidth, h);
644
678
  ctx.textBaseline = "middle";
645
679
  ctx.textRendering = "optimizeLegibility";
646
680
  let currentDate = new Date(this.visibleDateRange.start);
@@ -718,20 +752,20 @@ class GanttChart {
718
752
  }
719
753
  const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
720
754
  ctx.fillStyle = "#333";
721
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
755
+ ctx.font = "bold 14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
722
756
  ctx.textAlign = "left";
723
757
  groupedBlocks.forEach((group) => {
724
758
  const visibleStart = Math.max(group.startX, this.scrollLeft);
725
759
  const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
726
760
  if (visibleEnd > visibleStart) {
727
761
  ctx.fillStyle = this.config.headerBgColor;
728
- ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
762
+ ctx.fillRect(Math.round(visibleStart), 0, Math.round(visibleEnd - visibleStart), Math.round(h * 0.5));
729
763
  ctx.fillStyle = "#333";
730
- ctx.fillText(group.text, visibleStart + 5, group.yPos);
764
+ ctx.fillText(group.text, Math.round(visibleStart + 5), Math.round(group.yPos));
731
765
  }
732
766
  });
733
767
  while (currentDateForLower <= this.visibleDateRange.end) {
734
- const x = this.dateToX(currentDateForLower);
768
+ const x = this.snap(this.dateToX(currentDateForLower));
735
769
  let lowerText = "";
736
770
  let nextDate;
737
771
  switch (this.config.viewMode) {
@@ -766,7 +800,7 @@ class GanttChart {
766
800
  }
767
801
  const unitWidth = this.dateToX(nextDate) - x;
768
802
  ctx.fillStyle = "#000412";
769
- ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
803
+ ctx.font = "14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
770
804
  ctx.textAlign = "center";
771
805
  ctx.fillText(lowerText, Math.round(x + unitWidth / 2), Math.round(h * 0.7));
772
806
  ctx.beginPath();
@@ -780,11 +814,6 @@ class GanttChart {
780
814
  currentDateForLower = nextDate;
781
815
  }
782
816
  }
783
- ctx.beginPath();
784
- ctx.moveTo(this.scrollLeft, h - 0.5);
785
- ctx.lineTo(this.scrollLeft + this.viewportWidth, h - 0.5);
786
- ctx.strokeStyle = "#e0e0e0";
787
- ctx.stroke();
788
817
  ctx.restore();
789
818
  }
790
819
  // Helper method to group consecutive blocks with same text
@@ -807,7 +836,7 @@ class GanttChart {
807
836
  const ctx = this.mainCtx;
808
837
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
809
838
  ctx.save();
810
- ctx.translate(-this.scrollLeft, -this.scrollTop);
839
+ ctx.translate(-Math.round(this.scrollLeft), -Math.round(this.scrollTop));
811
840
  const { start: startDate, end: endDate } = this.visibleDateRange;
812
841
  this.drawGrid(ctx, startDate, endDate);
813
842
  this.drawToday(ctx);
@@ -896,7 +925,7 @@ class GanttChart {
896
925
  }
897
926
  drawAllTasks(ctx) {
898
927
  ctx.textBaseline = "middle";
899
- ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
928
+ ctx.font = "12px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
900
929
  ctx.textRendering = "optimizeSpeed";
901
930
  ctx.imageSmoothingEnabled = false;
902
931
  let visibleRowIndex = 0;
@@ -923,6 +952,7 @@ class GanttChart {
923
952
  }
924
953
  // In the drawGrid method
925
954
  drawGrid(ctx, startDate, endDate) {
955
+ const separator = this.config.dateSeparator;
926
956
  ctx.strokeStyle = "#e6e6e6";
927
957
  ctx.lineWidth = 1;
928
958
  ctx.beginPath();
@@ -936,8 +966,9 @@ class GanttChart {
936
966
  for (let i = 0; i <= visibleRowCount; i++) {
937
967
  const y = i * this.config.rowHeight;
938
968
  if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
939
- ctx.moveTo(this.scrollLeft, y);
940
- ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
969
+ const sharpY = this.snap(y);
970
+ ctx.moveTo(this.scrollLeft, sharpY);
971
+ ctx.lineTo(this.scrollLeft + this.viewportWidth, sharpY);
941
972
  }
942
973
  }
943
974
  if (this.config.showColLines) {
@@ -978,9 +1009,15 @@ class GanttChart {
978
1009
  currentDate = nextDate;
979
1010
  }
980
1011
  while (currentDate <= endDate) {
981
- const x = this.dateToX(currentDate);
1012
+ const x = this.snap(this.dateToX(currentDate));
982
1013
  ctx.moveTo(x, this.scrollTop);
983
1014
  ctx.lineTo(x, this.scrollTop + this.viewportHeight);
1015
+ if (this.config.viewMode === "Day") {
1016
+ if ([0, 6].includes(currentDate.getDay()) || this.holidaysMap.has(DateUtils.format(currentDate, `yyyy${separator}MM${separator}dd`))) {
1017
+ ctx.fillStyle = this.config.weekendBgColor;
1018
+ ctx.fillRect(x + 1, this.scrollTop, Math.round(this.pixelsPerDay - 1), Math.round(this.scrollTop + this.viewportHeight));
1019
+ }
1020
+ }
984
1021
  switch (this.config.viewMode) {
985
1022
  case "Day":
986
1023
  nextDate = DateUtils.addDays(currentDate, 1);
@@ -1027,14 +1064,14 @@ class GanttChart {
1027
1064
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
1028
1065
  if (this.config.showActual && pos.x_actual_start) {
1029
1066
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
1030
- ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
1067
+ 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));
1031
1068
  }
1032
1069
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
1033
1070
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
1034
1071
  ctx.lineWidth = 4;
1035
1072
  ctx.beginPath();
1036
- ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
1037
- ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
1073
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2 + this.config.xGap, taskY);
1074
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2 - this.config.xGap, taskY);
1038
1075
  ctx.stroke();
1039
1076
  }
1040
1077
  ctx.fillStyle = "#000";
package/dist/index.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.5.3
2
+ * gantt-canvas-chart v1.6.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -148,6 +148,7 @@
148
148
  resizeObserver;
149
149
  taskPositions;
150
150
  taskMap;
151
+ holidaysMap;
151
152
  isLoadingData = false;
152
153
  hasMoreDataLeft = true;
153
154
  hasMoreDataRight = true;
@@ -196,9 +197,13 @@
196
197
  tooltipFormat: null,
197
198
  tooltipColor: "black",
198
199
  todayColor: "#ff4d4f",
200
+ weekendBgColor: "#f7f7f7",
201
+ holidays: [],
202
+ dateSeparator: "/",
199
203
  offsetTop: 0,
200
204
  offsetLeft: 0,
201
205
  scrollEdgeThresholds: 10,
206
+ xGap: 0,
202
207
  enabledLoadMore: [],
203
208
  viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
204
209
  planBorderColor: "#C1EFCF",
@@ -233,6 +238,7 @@
233
238
  this.totalHeight = 0;
234
239
  this.taskPositions = /* @__PURE__ */ new Map();
235
240
  this.taskMap = /* @__PURE__ */ new Map();
241
+ this.holidaysMap = /* @__PURE__ */ new Map();
236
242
  this.handleMouseMove = this.handleMouseMove.bind(this);
237
243
  this.handleMouseLeave = this.handleMouseLeave.bind(this);
238
244
  this.handleScroll = this.handleScroll.bind(this);
@@ -242,6 +248,9 @@
242
248
  this.init();
243
249
  }
244
250
  init() {
251
+ if (this.config.holidays.length > 0) {
252
+ this.buildHolidaysMap();
253
+ }
245
254
  this.buildTaskMap();
246
255
  this.updatePixelsPerDay();
247
256
  this.calculateFullTimeline();
@@ -290,10 +299,26 @@
290
299
  this.updatePixelsPerDay();
291
300
  this.calculateFullTimeline();
292
301
  }
302
+ if (this.config.holidays.length !== this.holidaysMap.size) {
303
+ this.buildHolidaysMap();
304
+ }
293
305
  this.updateLoadMoreConf();
294
306
  this.updateDimensions();
295
307
  this.render();
296
308
  }
309
+ buildHolidaysMap() {
310
+ this.holidaysMap.clear();
311
+ const separator = this.config.dateSeparator;
312
+ if (this.config.holidays && this.config.holidays.length > 0 && this.config.holidays[0].includes(separator)) {
313
+ this.config.holidays.forEach((holiday) => {
314
+ this.holidaysMap.set(holiday, true);
315
+ });
316
+ } else {
317
+ this.config.holidays.forEach((holiday) => {
318
+ this.holidaysMap.set(DateUtils.format(new Date(holiday), `yyyy${separator}MM${separator}dd`), true);
319
+ });
320
+ }
321
+ }
297
322
  setData(newData, newConfig) {
298
323
  this.data = newData;
299
324
  this.buildTaskMap();
@@ -555,8 +580,17 @@
555
580
  canvas.style.height = `${height}px`;
556
581
  const ctx = canvas.getContext("2d");
557
582
  ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
583
+ ctx.textBaseline = "middle";
584
+ ctx.imageSmoothingEnabled = false;
558
585
  return ctx;
559
586
  }
587
+ /**
588
+ * 辅助函数:像素对齐(确保MAC/Windows等不同设备上1px线条清晰)
589
+ * 用于 1px 线条,使其落在 x.5 位置,填满一个物理像素,避免模糊
590
+ */
591
+ snap(val) {
592
+ return Math.floor(val) + 0.5;
593
+ }
560
594
  calculateAllTaskPositions() {
561
595
  this.taskPositions.clear();
562
596
  let visibleRowIndex = 0;
@@ -642,9 +676,9 @@
642
676
  const h = this.config.headerHeight;
643
677
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
644
678
  ctx.save();
645
- ctx.translate(-this.scrollLeft, 0);
679
+ ctx.translate(-Math.round(this.scrollLeft), 0);
646
680
  ctx.fillStyle = this.config.headerBgColor;
647
- ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
681
+ ctx.fillRect(Math.round(this.scrollLeft), 0, this.viewportWidth, h);
648
682
  ctx.textBaseline = "middle";
649
683
  ctx.textRendering = "optimizeLegibility";
650
684
  let currentDate = new Date(this.visibleDateRange.start);
@@ -722,20 +756,20 @@
722
756
  }
723
757
  const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
724
758
  ctx.fillStyle = "#333";
725
- ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
759
+ ctx.font = "bold 14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
726
760
  ctx.textAlign = "left";
727
761
  groupedBlocks.forEach((group) => {
728
762
  const visibleStart = Math.max(group.startX, this.scrollLeft);
729
763
  const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
730
764
  if (visibleEnd > visibleStart) {
731
765
  ctx.fillStyle = this.config.headerBgColor;
732
- ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
766
+ ctx.fillRect(Math.round(visibleStart), 0, Math.round(visibleEnd - visibleStart), Math.round(h * 0.5));
733
767
  ctx.fillStyle = "#333";
734
- ctx.fillText(group.text, visibleStart + 5, group.yPos);
768
+ ctx.fillText(group.text, Math.round(visibleStart + 5), Math.round(group.yPos));
735
769
  }
736
770
  });
737
771
  while (currentDateForLower <= this.visibleDateRange.end) {
738
- const x = this.dateToX(currentDateForLower);
772
+ const x = this.snap(this.dateToX(currentDateForLower));
739
773
  let lowerText = "";
740
774
  let nextDate;
741
775
  switch (this.config.viewMode) {
@@ -770,7 +804,7 @@
770
804
  }
771
805
  const unitWidth = this.dateToX(nextDate) - x;
772
806
  ctx.fillStyle = "#000412";
773
- ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
807
+ ctx.font = "14px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
774
808
  ctx.textAlign = "center";
775
809
  ctx.fillText(lowerText, Math.round(x + unitWidth / 2), Math.round(h * 0.7));
776
810
  ctx.beginPath();
@@ -784,11 +818,6 @@
784
818
  currentDateForLower = nextDate;
785
819
  }
786
820
  }
787
- ctx.beginPath();
788
- ctx.moveTo(this.scrollLeft, h - 0.5);
789
- ctx.lineTo(this.scrollLeft + this.viewportWidth, h - 0.5);
790
- ctx.strokeStyle = "#e0e0e0";
791
- ctx.stroke();
792
821
  ctx.restore();
793
822
  }
794
823
  // Helper method to group consecutive blocks with same text
@@ -811,7 +840,7 @@
811
840
  const ctx = this.mainCtx;
812
841
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
813
842
  ctx.save();
814
- ctx.translate(-this.scrollLeft, -this.scrollTop);
843
+ ctx.translate(-Math.round(this.scrollLeft), -Math.round(this.scrollTop));
815
844
  const { start: startDate, end: endDate } = this.visibleDateRange;
816
845
  this.drawGrid(ctx, startDate, endDate);
817
846
  this.drawToday(ctx);
@@ -900,7 +929,7 @@
900
929
  }
901
930
  drawAllTasks(ctx) {
902
931
  ctx.textBaseline = "middle";
903
- ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
932
+ ctx.font = "12px Roboto,PingFang SC,Noto Sans SC,Microsoft YaHei UI,Microsoft YaHei,Segoe UI,Helvetica Neue,Helvetica,Arial,sans-serif";
904
933
  ctx.textRendering = "optimizeSpeed";
905
934
  ctx.imageSmoothingEnabled = false;
906
935
  let visibleRowIndex = 0;
@@ -927,6 +956,7 @@
927
956
  }
928
957
  // In the drawGrid method
929
958
  drawGrid(ctx, startDate, endDate) {
959
+ const separator = this.config.dateSeparator;
930
960
  ctx.strokeStyle = "#e6e6e6";
931
961
  ctx.lineWidth = 1;
932
962
  ctx.beginPath();
@@ -940,8 +970,9 @@
940
970
  for (let i = 0; i <= visibleRowCount; i++) {
941
971
  const y = i * this.config.rowHeight;
942
972
  if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
943
- ctx.moveTo(this.scrollLeft, y);
944
- ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
973
+ const sharpY = this.snap(y);
974
+ ctx.moveTo(this.scrollLeft, sharpY);
975
+ ctx.lineTo(this.scrollLeft + this.viewportWidth, sharpY);
945
976
  }
946
977
  }
947
978
  if (this.config.showColLines) {
@@ -982,9 +1013,15 @@
982
1013
  currentDate = nextDate;
983
1014
  }
984
1015
  while (currentDate <= endDate) {
985
- const x = this.dateToX(currentDate);
1016
+ const x = this.snap(this.dateToX(currentDate));
986
1017
  ctx.moveTo(x, this.scrollTop);
987
1018
  ctx.lineTo(x, this.scrollTop + this.viewportHeight);
1019
+ if (this.config.viewMode === "Day") {
1020
+ if ([0, 6].includes(currentDate.getDay()) || this.holidaysMap.has(DateUtils.format(currentDate, `yyyy${separator}MM${separator}dd`))) {
1021
+ ctx.fillStyle = this.config.weekendBgColor;
1022
+ ctx.fillRect(x + 1, this.scrollTop, Math.round(this.pixelsPerDay - 1), Math.round(this.scrollTop + this.viewportHeight));
1023
+ }
1024
+ }
988
1025
  switch (this.config.viewMode) {
989
1026
  case "Day":
990
1027
  nextDate = DateUtils.addDays(currentDate, 1);
@@ -1031,14 +1068,14 @@
1031
1068
  const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
1032
1069
  if (this.config.showActual && pos.x_actual_start) {
1033
1070
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
1034
- ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
1071
+ 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));
1035
1072
  }
1036
1073
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
1037
1074
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
1038
1075
  ctx.lineWidth = 4;
1039
1076
  ctx.beginPath();
1040
- ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
1041
- ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
1077
+ ctx.moveTo(pos.offset_x_plan_start + offset / 2 + this.config.xGap, taskY);
1078
+ ctx.lineTo(pos.offset_x_plan_end - offset / 2 - this.config.xGap, taskY);
1042
1079
  ctx.stroke();
1043
1080
  }
1044
1081
  ctx.fillStyle = "#000";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gantt-canvas-chart",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
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",