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 +83 -34
- package/dist/index.css +5 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.es.js +83 -34
- package/dist/index.umd.js +83 -34
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.5.
|
|
2
|
+
* gantt-canvas-chart v1.5.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
|
|
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
|
-
|
|
255
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
890
|
-
|
|
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
|
-
|
|
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
|
-
|
|
910
|
-
ctx.
|
|
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
|
|
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 (
|
|
1054
|
-
const row = this.data[
|
|
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
|
+
* 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:
|
|
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:
|
|
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
|
+
* 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
|
|
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
|
-
|
|
253
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
888
|
-
|
|
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
|
-
|
|
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
|
-
|
|
908
|
-
ctx.
|
|
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
|
|
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 (
|
|
1052
|
-
const row = this.data[
|
|
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
|
+
* 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
|
|
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
|
-
|
|
257
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
892
|
-
|
|
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
|
-
|
|
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
|
-
|
|
912
|
-
ctx.
|
|
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
|
|
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 (
|
|
1056
|
-
const row = this.data[
|
|
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) {
|