gantt-canvas-chart 1.0.0 → 1.0.1-alpha.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/core/ganttChart.d.ts +10 -0
- package/dist/core/types.d.ts +11 -0
- package/dist/index.cjs.js +85 -63
- package/dist/index.css +1 -1
- package/dist/index.es.js +85 -63
- package/dist/index.umd.js +85 -63
- package/dist/main.d.ts +2 -1
- package/package.json +1 -1
|
@@ -26,6 +26,9 @@ export declare class GanttChart {
|
|
|
26
26
|
private resizeObserver;
|
|
27
27
|
private taskPositions;
|
|
28
28
|
private taskMap;
|
|
29
|
+
private boundHandleMouseMove;
|
|
30
|
+
private boundHandleMouseLeave;
|
|
31
|
+
private boundHandleScroll;
|
|
29
32
|
constructor(rootContainer: HTMLElement, data: GanttData, config?: GanttConfig);
|
|
30
33
|
private init;
|
|
31
34
|
private buildTaskMap;
|
|
@@ -58,4 +61,11 @@ export declare class GanttChart {
|
|
|
58
61
|
private handleMouseMove;
|
|
59
62
|
private getTaskTooltipHtml;
|
|
60
63
|
private handleMouseLeave;
|
|
64
|
+
/**
|
|
65
|
+
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
66
|
+
* @param diffMilliseconds 距离目标日期时间的差异毫秒数
|
|
67
|
+
* @param pixelsPerDay 每日的像素数
|
|
68
|
+
* @returns
|
|
69
|
+
*/
|
|
70
|
+
static getTaskWidthPercent(diffMilliseconds: number, pixelsPerDay: number): number;
|
|
61
71
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export interface Task {
|
|
|
13
13
|
styleClass?: string;
|
|
14
14
|
planBorderColor?: string;
|
|
15
15
|
actualBgColor?: string;
|
|
16
|
+
_data?: any;
|
|
17
|
+
planOffsetPercent?: [number, number];
|
|
18
|
+
actualOffsetPercent?: [number, number];
|
|
16
19
|
}
|
|
17
20
|
export interface Row {
|
|
18
21
|
id: string;
|
|
@@ -35,8 +38,16 @@ export interface GanttConfig {
|
|
|
35
38
|
showCenterRemark?: boolean;
|
|
36
39
|
showTooltip?: boolean;
|
|
37
40
|
tooltipColor?: 'black' | 'white';
|
|
41
|
+
todayColor?: string;
|
|
38
42
|
offsetTop?: number;
|
|
39
43
|
offsetLeft?: number;
|
|
44
|
+
viewFactors?: {
|
|
45
|
+
Day: number;
|
|
46
|
+
Week: number;
|
|
47
|
+
Month: number;
|
|
48
|
+
Year: number;
|
|
49
|
+
};
|
|
50
|
+
tooltipFormat?: null | ((task: Row, date: Date, config: GanttConfig) => string);
|
|
40
51
|
}
|
|
41
52
|
export interface TaskPosition {
|
|
42
53
|
x_plan_start: number;
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.0.0
|
|
2
|
+
* gantt-canvas-chart v1.0.1-alpha.0
|
|
3
3
|
* (c) 2025-present chandq
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -22,7 +22,10 @@ class DateUtils {
|
|
|
22
22
|
format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
|
|
23
23
|
for (let k in o)
|
|
24
24
|
if (new RegExp("(" + k + ")").test(format))
|
|
25
|
-
format = format.replace(
|
|
25
|
+
format = format.replace(
|
|
26
|
+
RegExp.$1,
|
|
27
|
+
RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
|
|
28
|
+
);
|
|
26
29
|
return format;
|
|
27
30
|
}
|
|
28
31
|
static addDays(date, days) {
|
|
@@ -42,7 +45,9 @@ class DateUtils {
|
|
|
42
45
|
return r;
|
|
43
46
|
}
|
|
44
47
|
static diffDays(date1, date2) {
|
|
45
|
-
return Math.round(
|
|
48
|
+
return Math.round(
|
|
49
|
+
(new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()).getTime() - new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()).getTime()) / this.ONE_DAY_MS
|
|
50
|
+
);
|
|
46
51
|
}
|
|
47
52
|
static diffDaysInclusive(date1, date2) {
|
|
48
53
|
return this.diffDays(date1, date2) + 1;
|
|
@@ -59,8 +64,7 @@ class DateUtils {
|
|
|
59
64
|
static getStartOfWeek(d) {
|
|
60
65
|
const date = new Date(d);
|
|
61
66
|
const day = date.getDay() || 7;
|
|
62
|
-
if (day !== 1)
|
|
63
|
-
date.setHours(-24 * (day - 1));
|
|
67
|
+
if (day !== 1) date.setHours(-24 * (day - 1));
|
|
64
68
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
65
69
|
}
|
|
66
70
|
static getStartOfMonth(d) {
|
|
@@ -104,6 +108,9 @@ class GanttChart {
|
|
|
104
108
|
resizeObserver;
|
|
105
109
|
taskPositions;
|
|
106
110
|
taskMap;
|
|
111
|
+
boundHandleMouseMove;
|
|
112
|
+
boundHandleMouseLeave;
|
|
113
|
+
boundHandleScroll;
|
|
107
114
|
constructor(rootContainer, data, config = {}) {
|
|
108
115
|
const container = document.createElement("div");
|
|
109
116
|
const scrollEl = document.createElement("div");
|
|
@@ -134,9 +141,12 @@ class GanttChart {
|
|
|
134
141
|
showRightRemark: false,
|
|
135
142
|
showCenterRemark: false,
|
|
136
143
|
showTooltip: true,
|
|
144
|
+
tooltipFormat: null,
|
|
137
145
|
tooltipColor: "black",
|
|
146
|
+
todayColor: "#ff4d4f",
|
|
138
147
|
offsetTop: 0,
|
|
139
148
|
offsetLeft: 0,
|
|
149
|
+
viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
|
|
140
150
|
planBorderColor: "#caeed2",
|
|
141
151
|
actualBgColor: "#78c78f",
|
|
142
152
|
...config
|
|
@@ -165,6 +175,9 @@ class GanttChart {
|
|
|
165
175
|
this.totalHeight = 0;
|
|
166
176
|
this.taskPositions = /* @__PURE__ */ new Map();
|
|
167
177
|
this.taskMap = /* @__PURE__ */ new Map();
|
|
178
|
+
this.boundHandleMouseMove = this.handleMouseMove.bind(this);
|
|
179
|
+
this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
|
|
180
|
+
this.boundHandleScroll = this.handleScroll.bind(this);
|
|
168
181
|
this.init();
|
|
169
182
|
}
|
|
170
183
|
init() {
|
|
@@ -181,7 +194,7 @@ class GanttChart {
|
|
|
181
194
|
});
|
|
182
195
|
}
|
|
183
196
|
setupEvents() {
|
|
184
|
-
this.container.addEventListener("scroll", this.
|
|
197
|
+
this.container.addEventListener("scroll", this.boundHandleScroll);
|
|
185
198
|
this.handleResize = this.handleResize.bind(this);
|
|
186
199
|
if (window.ResizeObserver) {
|
|
187
200
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
@@ -190,8 +203,8 @@ class GanttChart {
|
|
|
190
203
|
}, 100);
|
|
191
204
|
}
|
|
192
205
|
if (this.config.showTooltip) {
|
|
193
|
-
this.mainCanvas.addEventListener("mousemove", this.
|
|
194
|
-
this.mainCanvas.addEventListener("mouseleave", this.
|
|
206
|
+
this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
|
|
207
|
+
this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
|
|
195
208
|
}
|
|
196
209
|
}
|
|
197
210
|
updateConfig(newConfig) {
|
|
@@ -216,6 +229,9 @@ class GanttChart {
|
|
|
216
229
|
if (this.resizeObserver) {
|
|
217
230
|
this.resizeObserver.disconnect();
|
|
218
231
|
}
|
|
232
|
+
this.container.removeEventListener("scroll", this.boundHandleScroll);
|
|
233
|
+
this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
|
|
234
|
+
this.mainCanvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
|
|
219
235
|
this.container.remove();
|
|
220
236
|
}
|
|
221
237
|
calculateFullTimeline() {
|
|
@@ -228,17 +244,13 @@ class GanttChart {
|
|
|
228
244
|
this.taskMap.forEach(({ task }) => {
|
|
229
245
|
const pStart = new Date(task.planStart);
|
|
230
246
|
const pEnd = new Date(task.planEnd);
|
|
231
|
-
if (pStart < minDate)
|
|
232
|
-
|
|
233
|
-
if (pEnd > maxDate)
|
|
234
|
-
maxDate = pEnd;
|
|
247
|
+
if (pStart < minDate) minDate = pStart;
|
|
248
|
+
if (pEnd > maxDate) maxDate = pEnd;
|
|
235
249
|
if (task.actualStart) {
|
|
236
250
|
const aStart = new Date(task.actualStart);
|
|
237
251
|
const aEnd = new Date(task.actualEnd);
|
|
238
|
-
if (aStart < minDate)
|
|
239
|
-
|
|
240
|
-
if (aEnd > maxDate)
|
|
241
|
-
maxDate = aEnd;
|
|
252
|
+
if (aStart < minDate) minDate = aStart;
|
|
253
|
+
if (aEnd > maxDate) maxDate = aEnd;
|
|
242
254
|
}
|
|
243
255
|
});
|
|
244
256
|
}
|
|
@@ -265,8 +277,7 @@ class GanttChart {
|
|
|
265
277
|
}
|
|
266
278
|
}
|
|
267
279
|
updatePixelsPerDay() {
|
|
268
|
-
|
|
269
|
-
this.pixelsPerDay = viewFactors[this.config.viewMode];
|
|
280
|
+
this.pixelsPerDay = this.config.viewFactors[this.config.viewMode];
|
|
270
281
|
}
|
|
271
282
|
dateToX(date) {
|
|
272
283
|
return DateUtils.diffDays(this.timelineStart, date) * this.pixelsPerDay;
|
|
@@ -294,8 +305,7 @@ class GanttChart {
|
|
|
294
305
|
requestAnimationFrame(() => this.render());
|
|
295
306
|
}
|
|
296
307
|
setScrollTop(scrollTop) {
|
|
297
|
-
if (this.scrollTop !== scrollTop)
|
|
298
|
-
this.container.scrollTop = scrollTop;
|
|
308
|
+
if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
|
|
299
309
|
}
|
|
300
310
|
updateVirtualRanges() {
|
|
301
311
|
const buffer = 200;
|
|
@@ -504,16 +514,13 @@ class GanttChart {
|
|
|
504
514
|
ctx.lineCap = "round";
|
|
505
515
|
ctx.lineJoin = "round";
|
|
506
516
|
this.taskMap.forEach(({ task }) => {
|
|
507
|
-
if (!task.dependencies || task.dependencies.length === 0)
|
|
508
|
-
return;
|
|
517
|
+
if (!task.dependencies || task.dependencies.length === 0) return;
|
|
509
518
|
const toPos = this.taskPositions.get(task.id);
|
|
510
|
-
if (!toPos)
|
|
511
|
-
return;
|
|
519
|
+
if (!toPos) return;
|
|
512
520
|
const toRowIndex = this.taskMap.get(task.id).row;
|
|
513
521
|
task.dependencies.forEach((depId) => {
|
|
514
522
|
const fromPos = this.taskPositions.get(depId);
|
|
515
|
-
if (!fromPos)
|
|
516
|
-
return;
|
|
523
|
+
if (!fromPos) return;
|
|
517
524
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
518
525
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
519
526
|
const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
|
|
@@ -566,12 +573,10 @@ class GanttChart {
|
|
|
566
573
|
for (let i = 0; i < this.data.length; i++) {
|
|
567
574
|
const row = this.data[i];
|
|
568
575
|
const y = i * this.config.rowHeight;
|
|
569
|
-
if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight)
|
|
570
|
-
continue;
|
|
576
|
+
if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
|
|
571
577
|
row.tasks.forEach((task) => {
|
|
572
578
|
const pos = this.taskPositions.get(task.id);
|
|
573
|
-
if (!pos)
|
|
574
|
-
return;
|
|
579
|
+
if (!pos) return;
|
|
575
580
|
if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
|
|
576
581
|
if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
|
|
577
582
|
return;
|
|
@@ -587,8 +592,7 @@ class GanttChart {
|
|
|
587
592
|
if (this.config.showRowLines) {
|
|
588
593
|
for (let i = 0; i <= this.data.length; i++) {
|
|
589
594
|
const y = i * this.config.rowHeight;
|
|
590
|
-
if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight)
|
|
591
|
-
continue;
|
|
595
|
+
if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
|
|
592
596
|
ctx.moveTo(this.scrollLeft, y);
|
|
593
597
|
ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
|
|
594
598
|
}
|
|
@@ -649,17 +653,14 @@ class GanttChart {
|
|
|
649
653
|
nextDate = DateUtils.addMonths(currentDate, 6);
|
|
650
654
|
else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1)
|
|
651
655
|
nextDate = DateUtils.addMonths(currentDate, 6);
|
|
652
|
-
else
|
|
653
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
656
|
+
else nextDate = DateUtils.addDays(currentDate, 1);
|
|
654
657
|
break;
|
|
655
658
|
default:
|
|
656
659
|
nextDate = DateUtils.addDays(currentDate, 1);
|
|
657
660
|
break;
|
|
658
661
|
}
|
|
659
|
-
if (nextDate.getTime() === currentDate.getTime())
|
|
660
|
-
|
|
661
|
-
else
|
|
662
|
-
currentDate = nextDate;
|
|
662
|
+
if (nextDate.getTime() === currentDate.getTime()) currentDate = DateUtils.addDays(currentDate, 1);
|
|
663
|
+
else currentDate = nextDate;
|
|
663
664
|
}
|
|
664
665
|
}
|
|
665
666
|
ctx.stroke();
|
|
@@ -667,7 +668,7 @@ class GanttChart {
|
|
|
667
668
|
drawToday(ctx) {
|
|
668
669
|
const x = this.dateToX(this.today);
|
|
669
670
|
if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
|
|
670
|
-
ctx.strokeStyle =
|
|
671
|
+
ctx.strokeStyle = this.config.todayColor;
|
|
671
672
|
ctx.lineWidth = 2;
|
|
672
673
|
ctx.beginPath();
|
|
673
674
|
ctx.moveTo(x, this.scrollTop);
|
|
@@ -681,17 +682,23 @@ class GanttChart {
|
|
|
681
682
|
const taskY = y + this.config.rowHeight * 0.15 + offset;
|
|
682
683
|
const taskHeight = this.config.rowHeight * 0.7 - offset;
|
|
683
684
|
const textY = y + this.config.rowHeight / 2 + offset;
|
|
685
|
+
const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
|
|
686
|
+
const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
|
|
684
687
|
if (this.config.showActual && pos.x_actual_start) {
|
|
685
|
-
const aWidth = (pos.x_actual_end ? pos.x_actual_end : pos.x_plan_end) - pos.x_actual_start;
|
|
686
688
|
ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
|
|
687
|
-
|
|
689
|
+
const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
|
|
690
|
+
pos.x_actual_start += aWidth * offsetX_actual;
|
|
691
|
+
pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
|
|
692
|
+
ctx.fillRect(pos.x_actual_start, taskY, aWidth * percent_actual, taskHeight);
|
|
688
693
|
}
|
|
689
694
|
if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
|
|
690
695
|
ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
|
|
696
|
+
pos.x_plan_start += width * offsetX;
|
|
697
|
+
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
691
698
|
ctx.lineWidth = 4;
|
|
692
699
|
ctx.beginPath();
|
|
693
700
|
ctx.moveTo(pos.x_plan_start, taskY);
|
|
694
|
-
ctx.lineTo(pos.x_plan_start + width, taskY);
|
|
701
|
+
ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
|
|
695
702
|
ctx.stroke();
|
|
696
703
|
}
|
|
697
704
|
ctx.fillStyle = "#333";
|
|
@@ -734,25 +741,32 @@ class GanttChart {
|
|
|
734
741
|
const chartY = mouseY + this.scrollTop;
|
|
735
742
|
const rowIndex = Math.floor(chartY / this.config.rowHeight);
|
|
736
743
|
const date = this.xToDate(chartX);
|
|
737
|
-
if (rowIndex < 0 || rowIndex >= this.data.length)
|
|
738
|
-
return this.handleMouseLeave();
|
|
744
|
+
if (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
|
|
739
745
|
const row = this.data[rowIndex];
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
if (
|
|
743
|
-
return
|
|
744
|
-
if (task.actualStart) {
|
|
745
|
-
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
|
|
746
|
-
if (date >= aStart && date < aEnd)
|
|
747
|
-
return true;
|
|
746
|
+
if (this.config.tooltipFormat) {
|
|
747
|
+
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
748
|
+
if (!htmlStr) {
|
|
749
|
+
return this.handleMouseLeave();
|
|
748
750
|
}
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
751
|
+
this.tooltip.innerHTML = htmlStr;
|
|
752
|
+
} else {
|
|
753
|
+
const overlappingTasks = row.tasks.filter((task) => {
|
|
754
|
+
const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
|
|
755
|
+
if (date >= pStart && date < pEnd) return true;
|
|
756
|
+
if (task.actualStart) {
|
|
757
|
+
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
|
|
758
|
+
if (date >= aStart && date < aEnd) return true;
|
|
759
|
+
}
|
|
760
|
+
return false;
|
|
761
|
+
});
|
|
762
|
+
if (overlappingTasks.length === 0) return this.handleMouseLeave();
|
|
763
|
+
let html = `<strong>${row.name}</strong> (${DateUtils.format(
|
|
764
|
+
date,
|
|
765
|
+
"yyyy-MM-dd"
|
|
766
|
+
)})<hr class="__gantt_tooltip-divider">`;
|
|
767
|
+
overlappingTasks.forEach((task) => html += this.getTaskTooltipHtml(task));
|
|
768
|
+
this.tooltip.innerHTML = html;
|
|
769
|
+
}
|
|
756
770
|
this.tooltip.style.display = "block";
|
|
757
771
|
if (this.config.tooltipColor === "white") {
|
|
758
772
|
this.tooltip.style.background = "#fff";
|
|
@@ -760,10 +774,8 @@ class GanttChart {
|
|
|
760
774
|
}
|
|
761
775
|
const tipRect = this.tooltip.getBoundingClientRect();
|
|
762
776
|
let x = e.clientX + 15, y = e.clientY + 15;
|
|
763
|
-
if (x + tipRect.width > window.innerWidth)
|
|
764
|
-
|
|
765
|
-
if (y + tipRect.height > window.innerHeight)
|
|
766
|
-
y = e.clientY - 15 - tipRect.height;
|
|
777
|
+
if (x + tipRect.width > window.innerWidth) x = e.clientX - 15 - tipRect.width;
|
|
778
|
+
if (y + tipRect.height > window.innerHeight) y = e.clientY - 15 - tipRect.height;
|
|
767
779
|
this.tooltip.style.left = `${x + this.config.offsetLeft}px`;
|
|
768
780
|
this.tooltip.style.top = `${y + this.config.offsetTop}px`;
|
|
769
781
|
}
|
|
@@ -786,5 +798,15 @@ class GanttChart {
|
|
|
786
798
|
handleMouseLeave() {
|
|
787
799
|
this.tooltip.style.display = "none";
|
|
788
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
803
|
+
* @param diffMilliseconds 距离目标日期时间的差异毫秒数
|
|
804
|
+
* @param pixelsPerDay 每日的像素数
|
|
805
|
+
* @returns
|
|
806
|
+
*/
|
|
807
|
+
static getTaskWidthPercent(diffMilliseconds, pixelsPerDay) {
|
|
808
|
+
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
809
|
+
}
|
|
789
810
|
}
|
|
811
|
+
exports.DateUtils = DateUtils;
|
|
790
812
|
exports.GanttChart = GanttChart;
|
package/dist/index.css
CHANGED
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.0.0
|
|
2
|
+
* gantt-canvas-chart v1.0.1-alpha.0
|
|
3
3
|
* (c) 2025-present chandq
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -20,7 +20,10 @@ class DateUtils {
|
|
|
20
20
|
format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
|
|
21
21
|
for (let k in o)
|
|
22
22
|
if (new RegExp("(" + k + ")").test(format))
|
|
23
|
-
format = format.replace(
|
|
23
|
+
format = format.replace(
|
|
24
|
+
RegExp.$1,
|
|
25
|
+
RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
|
|
26
|
+
);
|
|
24
27
|
return format;
|
|
25
28
|
}
|
|
26
29
|
static addDays(date, days) {
|
|
@@ -40,7 +43,9 @@ class DateUtils {
|
|
|
40
43
|
return r;
|
|
41
44
|
}
|
|
42
45
|
static diffDays(date1, date2) {
|
|
43
|
-
return Math.round(
|
|
46
|
+
return Math.round(
|
|
47
|
+
(new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()).getTime() - new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()).getTime()) / this.ONE_DAY_MS
|
|
48
|
+
);
|
|
44
49
|
}
|
|
45
50
|
static diffDaysInclusive(date1, date2) {
|
|
46
51
|
return this.diffDays(date1, date2) + 1;
|
|
@@ -57,8 +62,7 @@ class DateUtils {
|
|
|
57
62
|
static getStartOfWeek(d) {
|
|
58
63
|
const date = new Date(d);
|
|
59
64
|
const day = date.getDay() || 7;
|
|
60
|
-
if (day !== 1)
|
|
61
|
-
date.setHours(-24 * (day - 1));
|
|
65
|
+
if (day !== 1) date.setHours(-24 * (day - 1));
|
|
62
66
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
63
67
|
}
|
|
64
68
|
static getStartOfMonth(d) {
|
|
@@ -102,6 +106,9 @@ class GanttChart {
|
|
|
102
106
|
resizeObserver;
|
|
103
107
|
taskPositions;
|
|
104
108
|
taskMap;
|
|
109
|
+
boundHandleMouseMove;
|
|
110
|
+
boundHandleMouseLeave;
|
|
111
|
+
boundHandleScroll;
|
|
105
112
|
constructor(rootContainer, data, config = {}) {
|
|
106
113
|
const container = document.createElement("div");
|
|
107
114
|
const scrollEl = document.createElement("div");
|
|
@@ -132,9 +139,12 @@ class GanttChart {
|
|
|
132
139
|
showRightRemark: false,
|
|
133
140
|
showCenterRemark: false,
|
|
134
141
|
showTooltip: true,
|
|
142
|
+
tooltipFormat: null,
|
|
135
143
|
tooltipColor: "black",
|
|
144
|
+
todayColor: "#ff4d4f",
|
|
136
145
|
offsetTop: 0,
|
|
137
146
|
offsetLeft: 0,
|
|
147
|
+
viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
|
|
138
148
|
planBorderColor: "#caeed2",
|
|
139
149
|
actualBgColor: "#78c78f",
|
|
140
150
|
...config
|
|
@@ -163,6 +173,9 @@ class GanttChart {
|
|
|
163
173
|
this.totalHeight = 0;
|
|
164
174
|
this.taskPositions = /* @__PURE__ */ new Map();
|
|
165
175
|
this.taskMap = /* @__PURE__ */ new Map();
|
|
176
|
+
this.boundHandleMouseMove = this.handleMouseMove.bind(this);
|
|
177
|
+
this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
|
|
178
|
+
this.boundHandleScroll = this.handleScroll.bind(this);
|
|
166
179
|
this.init();
|
|
167
180
|
}
|
|
168
181
|
init() {
|
|
@@ -179,7 +192,7 @@ class GanttChart {
|
|
|
179
192
|
});
|
|
180
193
|
}
|
|
181
194
|
setupEvents() {
|
|
182
|
-
this.container.addEventListener("scroll", this.
|
|
195
|
+
this.container.addEventListener("scroll", this.boundHandleScroll);
|
|
183
196
|
this.handleResize = this.handleResize.bind(this);
|
|
184
197
|
if (window.ResizeObserver) {
|
|
185
198
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
@@ -188,8 +201,8 @@ class GanttChart {
|
|
|
188
201
|
}, 100);
|
|
189
202
|
}
|
|
190
203
|
if (this.config.showTooltip) {
|
|
191
|
-
this.mainCanvas.addEventListener("mousemove", this.
|
|
192
|
-
this.mainCanvas.addEventListener("mouseleave", this.
|
|
204
|
+
this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
|
|
205
|
+
this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
|
|
193
206
|
}
|
|
194
207
|
}
|
|
195
208
|
updateConfig(newConfig) {
|
|
@@ -214,6 +227,9 @@ class GanttChart {
|
|
|
214
227
|
if (this.resizeObserver) {
|
|
215
228
|
this.resizeObserver.disconnect();
|
|
216
229
|
}
|
|
230
|
+
this.container.removeEventListener("scroll", this.boundHandleScroll);
|
|
231
|
+
this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
|
|
232
|
+
this.mainCanvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
|
|
217
233
|
this.container.remove();
|
|
218
234
|
}
|
|
219
235
|
calculateFullTimeline() {
|
|
@@ -226,17 +242,13 @@ class GanttChart {
|
|
|
226
242
|
this.taskMap.forEach(({ task }) => {
|
|
227
243
|
const pStart = new Date(task.planStart);
|
|
228
244
|
const pEnd = new Date(task.planEnd);
|
|
229
|
-
if (pStart < minDate)
|
|
230
|
-
|
|
231
|
-
if (pEnd > maxDate)
|
|
232
|
-
maxDate = pEnd;
|
|
245
|
+
if (pStart < minDate) minDate = pStart;
|
|
246
|
+
if (pEnd > maxDate) maxDate = pEnd;
|
|
233
247
|
if (task.actualStart) {
|
|
234
248
|
const aStart = new Date(task.actualStart);
|
|
235
249
|
const aEnd = new Date(task.actualEnd);
|
|
236
|
-
if (aStart < minDate)
|
|
237
|
-
|
|
238
|
-
if (aEnd > maxDate)
|
|
239
|
-
maxDate = aEnd;
|
|
250
|
+
if (aStart < minDate) minDate = aStart;
|
|
251
|
+
if (aEnd > maxDate) maxDate = aEnd;
|
|
240
252
|
}
|
|
241
253
|
});
|
|
242
254
|
}
|
|
@@ -263,8 +275,7 @@ class GanttChart {
|
|
|
263
275
|
}
|
|
264
276
|
}
|
|
265
277
|
updatePixelsPerDay() {
|
|
266
|
-
|
|
267
|
-
this.pixelsPerDay = viewFactors[this.config.viewMode];
|
|
278
|
+
this.pixelsPerDay = this.config.viewFactors[this.config.viewMode];
|
|
268
279
|
}
|
|
269
280
|
dateToX(date) {
|
|
270
281
|
return DateUtils.diffDays(this.timelineStart, date) * this.pixelsPerDay;
|
|
@@ -292,8 +303,7 @@ class GanttChart {
|
|
|
292
303
|
requestAnimationFrame(() => this.render());
|
|
293
304
|
}
|
|
294
305
|
setScrollTop(scrollTop) {
|
|
295
|
-
if (this.scrollTop !== scrollTop)
|
|
296
|
-
this.container.scrollTop = scrollTop;
|
|
306
|
+
if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
|
|
297
307
|
}
|
|
298
308
|
updateVirtualRanges() {
|
|
299
309
|
const buffer = 200;
|
|
@@ -502,16 +512,13 @@ class GanttChart {
|
|
|
502
512
|
ctx.lineCap = "round";
|
|
503
513
|
ctx.lineJoin = "round";
|
|
504
514
|
this.taskMap.forEach(({ task }) => {
|
|
505
|
-
if (!task.dependencies || task.dependencies.length === 0)
|
|
506
|
-
return;
|
|
515
|
+
if (!task.dependencies || task.dependencies.length === 0) return;
|
|
507
516
|
const toPos = this.taskPositions.get(task.id);
|
|
508
|
-
if (!toPos)
|
|
509
|
-
return;
|
|
517
|
+
if (!toPos) return;
|
|
510
518
|
const toRowIndex = this.taskMap.get(task.id).row;
|
|
511
519
|
task.dependencies.forEach((depId) => {
|
|
512
520
|
const fromPos = this.taskPositions.get(depId);
|
|
513
|
-
if (!fromPos)
|
|
514
|
-
return;
|
|
521
|
+
if (!fromPos) return;
|
|
515
522
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
516
523
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
517
524
|
const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
|
|
@@ -564,12 +571,10 @@ class GanttChart {
|
|
|
564
571
|
for (let i = 0; i < this.data.length; i++) {
|
|
565
572
|
const row = this.data[i];
|
|
566
573
|
const y = i * this.config.rowHeight;
|
|
567
|
-
if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight)
|
|
568
|
-
continue;
|
|
574
|
+
if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
|
|
569
575
|
row.tasks.forEach((task) => {
|
|
570
576
|
const pos = this.taskPositions.get(task.id);
|
|
571
|
-
if (!pos)
|
|
572
|
-
return;
|
|
577
|
+
if (!pos) return;
|
|
573
578
|
if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
|
|
574
579
|
if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
|
|
575
580
|
return;
|
|
@@ -585,8 +590,7 @@ class GanttChart {
|
|
|
585
590
|
if (this.config.showRowLines) {
|
|
586
591
|
for (let i = 0; i <= this.data.length; i++) {
|
|
587
592
|
const y = i * this.config.rowHeight;
|
|
588
|
-
if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight)
|
|
589
|
-
continue;
|
|
593
|
+
if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
|
|
590
594
|
ctx.moveTo(this.scrollLeft, y);
|
|
591
595
|
ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
|
|
592
596
|
}
|
|
@@ -647,17 +651,14 @@ class GanttChart {
|
|
|
647
651
|
nextDate = DateUtils.addMonths(currentDate, 6);
|
|
648
652
|
else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1)
|
|
649
653
|
nextDate = DateUtils.addMonths(currentDate, 6);
|
|
650
|
-
else
|
|
651
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
654
|
+
else nextDate = DateUtils.addDays(currentDate, 1);
|
|
652
655
|
break;
|
|
653
656
|
default:
|
|
654
657
|
nextDate = DateUtils.addDays(currentDate, 1);
|
|
655
658
|
break;
|
|
656
659
|
}
|
|
657
|
-
if (nextDate.getTime() === currentDate.getTime())
|
|
658
|
-
|
|
659
|
-
else
|
|
660
|
-
currentDate = nextDate;
|
|
660
|
+
if (nextDate.getTime() === currentDate.getTime()) currentDate = DateUtils.addDays(currentDate, 1);
|
|
661
|
+
else currentDate = nextDate;
|
|
661
662
|
}
|
|
662
663
|
}
|
|
663
664
|
ctx.stroke();
|
|
@@ -665,7 +666,7 @@ class GanttChart {
|
|
|
665
666
|
drawToday(ctx) {
|
|
666
667
|
const x = this.dateToX(this.today);
|
|
667
668
|
if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
|
|
668
|
-
ctx.strokeStyle =
|
|
669
|
+
ctx.strokeStyle = this.config.todayColor;
|
|
669
670
|
ctx.lineWidth = 2;
|
|
670
671
|
ctx.beginPath();
|
|
671
672
|
ctx.moveTo(x, this.scrollTop);
|
|
@@ -679,17 +680,23 @@ class GanttChart {
|
|
|
679
680
|
const taskY = y + this.config.rowHeight * 0.15 + offset;
|
|
680
681
|
const taskHeight = this.config.rowHeight * 0.7 - offset;
|
|
681
682
|
const textY = y + this.config.rowHeight / 2 + offset;
|
|
683
|
+
const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
|
|
684
|
+
const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
|
|
682
685
|
if (this.config.showActual && pos.x_actual_start) {
|
|
683
|
-
const aWidth = (pos.x_actual_end ? pos.x_actual_end : pos.x_plan_end) - pos.x_actual_start;
|
|
684
686
|
ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
|
|
685
|
-
|
|
687
|
+
const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
|
|
688
|
+
pos.x_actual_start += aWidth * offsetX_actual;
|
|
689
|
+
pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
|
|
690
|
+
ctx.fillRect(pos.x_actual_start, taskY, aWidth * percent_actual, taskHeight);
|
|
686
691
|
}
|
|
687
692
|
if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
|
|
688
693
|
ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
|
|
694
|
+
pos.x_plan_start += width * offsetX;
|
|
695
|
+
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
689
696
|
ctx.lineWidth = 4;
|
|
690
697
|
ctx.beginPath();
|
|
691
698
|
ctx.moveTo(pos.x_plan_start, taskY);
|
|
692
|
-
ctx.lineTo(pos.x_plan_start + width, taskY);
|
|
699
|
+
ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
|
|
693
700
|
ctx.stroke();
|
|
694
701
|
}
|
|
695
702
|
ctx.fillStyle = "#333";
|
|
@@ -732,25 +739,32 @@ class GanttChart {
|
|
|
732
739
|
const chartY = mouseY + this.scrollTop;
|
|
733
740
|
const rowIndex = Math.floor(chartY / this.config.rowHeight);
|
|
734
741
|
const date = this.xToDate(chartX);
|
|
735
|
-
if (rowIndex < 0 || rowIndex >= this.data.length)
|
|
736
|
-
return this.handleMouseLeave();
|
|
742
|
+
if (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
|
|
737
743
|
const row = this.data[rowIndex];
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
if (
|
|
741
|
-
return
|
|
742
|
-
if (task.actualStart) {
|
|
743
|
-
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
|
|
744
|
-
if (date >= aStart && date < aEnd)
|
|
745
|
-
return true;
|
|
744
|
+
if (this.config.tooltipFormat) {
|
|
745
|
+
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
746
|
+
if (!htmlStr) {
|
|
747
|
+
return this.handleMouseLeave();
|
|
746
748
|
}
|
|
747
|
-
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
749
|
+
this.tooltip.innerHTML = htmlStr;
|
|
750
|
+
} else {
|
|
751
|
+
const overlappingTasks = row.tasks.filter((task) => {
|
|
752
|
+
const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
|
|
753
|
+
if (date >= pStart && date < pEnd) return true;
|
|
754
|
+
if (task.actualStart) {
|
|
755
|
+
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
|
|
756
|
+
if (date >= aStart && date < aEnd) return true;
|
|
757
|
+
}
|
|
758
|
+
return false;
|
|
759
|
+
});
|
|
760
|
+
if (overlappingTasks.length === 0) return this.handleMouseLeave();
|
|
761
|
+
let html = `<strong>${row.name}</strong> (${DateUtils.format(
|
|
762
|
+
date,
|
|
763
|
+
"yyyy-MM-dd"
|
|
764
|
+
)})<hr class="__gantt_tooltip-divider">`;
|
|
765
|
+
overlappingTasks.forEach((task) => html += this.getTaskTooltipHtml(task));
|
|
766
|
+
this.tooltip.innerHTML = html;
|
|
767
|
+
}
|
|
754
768
|
this.tooltip.style.display = "block";
|
|
755
769
|
if (this.config.tooltipColor === "white") {
|
|
756
770
|
this.tooltip.style.background = "#fff";
|
|
@@ -758,10 +772,8 @@ class GanttChart {
|
|
|
758
772
|
}
|
|
759
773
|
const tipRect = this.tooltip.getBoundingClientRect();
|
|
760
774
|
let x = e.clientX + 15, y = e.clientY + 15;
|
|
761
|
-
if (x + tipRect.width > window.innerWidth)
|
|
762
|
-
|
|
763
|
-
if (y + tipRect.height > window.innerHeight)
|
|
764
|
-
y = e.clientY - 15 - tipRect.height;
|
|
775
|
+
if (x + tipRect.width > window.innerWidth) x = e.clientX - 15 - tipRect.width;
|
|
776
|
+
if (y + tipRect.height > window.innerHeight) y = e.clientY - 15 - tipRect.height;
|
|
765
777
|
this.tooltip.style.left = `${x + this.config.offsetLeft}px`;
|
|
766
778
|
this.tooltip.style.top = `${y + this.config.offsetTop}px`;
|
|
767
779
|
}
|
|
@@ -784,7 +796,17 @@ class GanttChart {
|
|
|
784
796
|
handleMouseLeave() {
|
|
785
797
|
this.tooltip.style.display = "none";
|
|
786
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
801
|
+
* @param diffMilliseconds 距离目标日期时间的差异毫秒数
|
|
802
|
+
* @param pixelsPerDay 每日的像素数
|
|
803
|
+
* @returns
|
|
804
|
+
*/
|
|
805
|
+
static getTaskWidthPercent(diffMilliseconds, pixelsPerDay) {
|
|
806
|
+
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
807
|
+
}
|
|
787
808
|
}
|
|
788
809
|
export {
|
|
810
|
+
DateUtils,
|
|
789
811
|
GanttChart
|
|
790
812
|
};
|
package/dist/index.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.0.0
|
|
2
|
+
* gantt-canvas-chart v1.0.1-alpha.0
|
|
3
3
|
* (c) 2025-present chandq
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
|
|
25
25
|
for (let k in o)
|
|
26
26
|
if (new RegExp("(" + k + ")").test(format))
|
|
27
|
-
format = format.replace(
|
|
27
|
+
format = format.replace(
|
|
28
|
+
RegExp.$1,
|
|
29
|
+
RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
|
|
30
|
+
);
|
|
28
31
|
return format;
|
|
29
32
|
}
|
|
30
33
|
static addDays(date, days) {
|
|
@@ -44,7 +47,9 @@
|
|
|
44
47
|
return r;
|
|
45
48
|
}
|
|
46
49
|
static diffDays(date1, date2) {
|
|
47
|
-
return Math.round(
|
|
50
|
+
return Math.round(
|
|
51
|
+
(new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()).getTime() - new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()).getTime()) / this.ONE_DAY_MS
|
|
52
|
+
);
|
|
48
53
|
}
|
|
49
54
|
static diffDaysInclusive(date1, date2) {
|
|
50
55
|
return this.diffDays(date1, date2) + 1;
|
|
@@ -61,8 +66,7 @@
|
|
|
61
66
|
static getStartOfWeek(d) {
|
|
62
67
|
const date = new Date(d);
|
|
63
68
|
const day = date.getDay() || 7;
|
|
64
|
-
if (day !== 1)
|
|
65
|
-
date.setHours(-24 * (day - 1));
|
|
69
|
+
if (day !== 1) date.setHours(-24 * (day - 1));
|
|
66
70
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
67
71
|
}
|
|
68
72
|
static getStartOfMonth(d) {
|
|
@@ -106,6 +110,9 @@
|
|
|
106
110
|
resizeObserver;
|
|
107
111
|
taskPositions;
|
|
108
112
|
taskMap;
|
|
113
|
+
boundHandleMouseMove;
|
|
114
|
+
boundHandleMouseLeave;
|
|
115
|
+
boundHandleScroll;
|
|
109
116
|
constructor(rootContainer, data, config = {}) {
|
|
110
117
|
const container = document.createElement("div");
|
|
111
118
|
const scrollEl = document.createElement("div");
|
|
@@ -136,9 +143,12 @@
|
|
|
136
143
|
showRightRemark: false,
|
|
137
144
|
showCenterRemark: false,
|
|
138
145
|
showTooltip: true,
|
|
146
|
+
tooltipFormat: null,
|
|
139
147
|
tooltipColor: "black",
|
|
148
|
+
todayColor: "#ff4d4f",
|
|
140
149
|
offsetTop: 0,
|
|
141
150
|
offsetLeft: 0,
|
|
151
|
+
viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
|
|
142
152
|
planBorderColor: "#caeed2",
|
|
143
153
|
actualBgColor: "#78c78f",
|
|
144
154
|
...config
|
|
@@ -167,6 +177,9 @@
|
|
|
167
177
|
this.totalHeight = 0;
|
|
168
178
|
this.taskPositions = /* @__PURE__ */ new Map();
|
|
169
179
|
this.taskMap = /* @__PURE__ */ new Map();
|
|
180
|
+
this.boundHandleMouseMove = this.handleMouseMove.bind(this);
|
|
181
|
+
this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
|
|
182
|
+
this.boundHandleScroll = this.handleScroll.bind(this);
|
|
170
183
|
this.init();
|
|
171
184
|
}
|
|
172
185
|
init() {
|
|
@@ -183,7 +196,7 @@
|
|
|
183
196
|
});
|
|
184
197
|
}
|
|
185
198
|
setupEvents() {
|
|
186
|
-
this.container.addEventListener("scroll", this.
|
|
199
|
+
this.container.addEventListener("scroll", this.boundHandleScroll);
|
|
187
200
|
this.handleResize = this.handleResize.bind(this);
|
|
188
201
|
if (window.ResizeObserver) {
|
|
189
202
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
@@ -192,8 +205,8 @@
|
|
|
192
205
|
}, 100);
|
|
193
206
|
}
|
|
194
207
|
if (this.config.showTooltip) {
|
|
195
|
-
this.mainCanvas.addEventListener("mousemove", this.
|
|
196
|
-
this.mainCanvas.addEventListener("mouseleave", this.
|
|
208
|
+
this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
|
|
209
|
+
this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
|
|
197
210
|
}
|
|
198
211
|
}
|
|
199
212
|
updateConfig(newConfig) {
|
|
@@ -218,6 +231,9 @@
|
|
|
218
231
|
if (this.resizeObserver) {
|
|
219
232
|
this.resizeObserver.disconnect();
|
|
220
233
|
}
|
|
234
|
+
this.container.removeEventListener("scroll", this.boundHandleScroll);
|
|
235
|
+
this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
|
|
236
|
+
this.mainCanvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
|
|
221
237
|
this.container.remove();
|
|
222
238
|
}
|
|
223
239
|
calculateFullTimeline() {
|
|
@@ -230,17 +246,13 @@
|
|
|
230
246
|
this.taskMap.forEach(({ task }) => {
|
|
231
247
|
const pStart = new Date(task.planStart);
|
|
232
248
|
const pEnd = new Date(task.planEnd);
|
|
233
|
-
if (pStart < minDate)
|
|
234
|
-
|
|
235
|
-
if (pEnd > maxDate)
|
|
236
|
-
maxDate = pEnd;
|
|
249
|
+
if (pStart < minDate) minDate = pStart;
|
|
250
|
+
if (pEnd > maxDate) maxDate = pEnd;
|
|
237
251
|
if (task.actualStart) {
|
|
238
252
|
const aStart = new Date(task.actualStart);
|
|
239
253
|
const aEnd = new Date(task.actualEnd);
|
|
240
|
-
if (aStart < minDate)
|
|
241
|
-
|
|
242
|
-
if (aEnd > maxDate)
|
|
243
|
-
maxDate = aEnd;
|
|
254
|
+
if (aStart < minDate) minDate = aStart;
|
|
255
|
+
if (aEnd > maxDate) maxDate = aEnd;
|
|
244
256
|
}
|
|
245
257
|
});
|
|
246
258
|
}
|
|
@@ -267,8 +279,7 @@
|
|
|
267
279
|
}
|
|
268
280
|
}
|
|
269
281
|
updatePixelsPerDay() {
|
|
270
|
-
|
|
271
|
-
this.pixelsPerDay = viewFactors[this.config.viewMode];
|
|
282
|
+
this.pixelsPerDay = this.config.viewFactors[this.config.viewMode];
|
|
272
283
|
}
|
|
273
284
|
dateToX(date) {
|
|
274
285
|
return DateUtils.diffDays(this.timelineStart, date) * this.pixelsPerDay;
|
|
@@ -296,8 +307,7 @@
|
|
|
296
307
|
requestAnimationFrame(() => this.render());
|
|
297
308
|
}
|
|
298
309
|
setScrollTop(scrollTop) {
|
|
299
|
-
if (this.scrollTop !== scrollTop)
|
|
300
|
-
this.container.scrollTop = scrollTop;
|
|
310
|
+
if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
|
|
301
311
|
}
|
|
302
312
|
updateVirtualRanges() {
|
|
303
313
|
const buffer = 200;
|
|
@@ -506,16 +516,13 @@
|
|
|
506
516
|
ctx.lineCap = "round";
|
|
507
517
|
ctx.lineJoin = "round";
|
|
508
518
|
this.taskMap.forEach(({ task }) => {
|
|
509
|
-
if (!task.dependencies || task.dependencies.length === 0)
|
|
510
|
-
return;
|
|
519
|
+
if (!task.dependencies || task.dependencies.length === 0) return;
|
|
511
520
|
const toPos = this.taskPositions.get(task.id);
|
|
512
|
-
if (!toPos)
|
|
513
|
-
return;
|
|
521
|
+
if (!toPos) return;
|
|
514
522
|
const toRowIndex = this.taskMap.get(task.id).row;
|
|
515
523
|
task.dependencies.forEach((depId) => {
|
|
516
524
|
const fromPos = this.taskPositions.get(depId);
|
|
517
|
-
if (!fromPos)
|
|
518
|
-
return;
|
|
525
|
+
if (!fromPos) return;
|
|
519
526
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
520
527
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
521
528
|
const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
|
|
@@ -568,12 +575,10 @@
|
|
|
568
575
|
for (let i = 0; i < this.data.length; i++) {
|
|
569
576
|
const row = this.data[i];
|
|
570
577
|
const y = i * this.config.rowHeight;
|
|
571
|
-
if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight)
|
|
572
|
-
continue;
|
|
578
|
+
if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
|
|
573
579
|
row.tasks.forEach((task) => {
|
|
574
580
|
const pos = this.taskPositions.get(task.id);
|
|
575
|
-
if (!pos)
|
|
576
|
-
return;
|
|
581
|
+
if (!pos) return;
|
|
577
582
|
if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
|
|
578
583
|
if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
|
|
579
584
|
return;
|
|
@@ -589,8 +594,7 @@
|
|
|
589
594
|
if (this.config.showRowLines) {
|
|
590
595
|
for (let i = 0; i <= this.data.length; i++) {
|
|
591
596
|
const y = i * this.config.rowHeight;
|
|
592
|
-
if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight)
|
|
593
|
-
continue;
|
|
597
|
+
if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
|
|
594
598
|
ctx.moveTo(this.scrollLeft, y);
|
|
595
599
|
ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
|
|
596
600
|
}
|
|
@@ -651,17 +655,14 @@
|
|
|
651
655
|
nextDate = DateUtils.addMonths(currentDate, 6);
|
|
652
656
|
else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1)
|
|
653
657
|
nextDate = DateUtils.addMonths(currentDate, 6);
|
|
654
|
-
else
|
|
655
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
658
|
+
else nextDate = DateUtils.addDays(currentDate, 1);
|
|
656
659
|
break;
|
|
657
660
|
default:
|
|
658
661
|
nextDate = DateUtils.addDays(currentDate, 1);
|
|
659
662
|
break;
|
|
660
663
|
}
|
|
661
|
-
if (nextDate.getTime() === currentDate.getTime())
|
|
662
|
-
|
|
663
|
-
else
|
|
664
|
-
currentDate = nextDate;
|
|
664
|
+
if (nextDate.getTime() === currentDate.getTime()) currentDate = DateUtils.addDays(currentDate, 1);
|
|
665
|
+
else currentDate = nextDate;
|
|
665
666
|
}
|
|
666
667
|
}
|
|
667
668
|
ctx.stroke();
|
|
@@ -669,7 +670,7 @@
|
|
|
669
670
|
drawToday(ctx) {
|
|
670
671
|
const x = this.dateToX(this.today);
|
|
671
672
|
if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
|
|
672
|
-
ctx.strokeStyle =
|
|
673
|
+
ctx.strokeStyle = this.config.todayColor;
|
|
673
674
|
ctx.lineWidth = 2;
|
|
674
675
|
ctx.beginPath();
|
|
675
676
|
ctx.moveTo(x, this.scrollTop);
|
|
@@ -683,17 +684,23 @@
|
|
|
683
684
|
const taskY = y + this.config.rowHeight * 0.15 + offset;
|
|
684
685
|
const taskHeight = this.config.rowHeight * 0.7 - offset;
|
|
685
686
|
const textY = y + this.config.rowHeight / 2 + offset;
|
|
687
|
+
const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
|
|
688
|
+
const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
|
|
686
689
|
if (this.config.showActual && pos.x_actual_start) {
|
|
687
|
-
const aWidth = (pos.x_actual_end ? pos.x_actual_end : pos.x_plan_end) - pos.x_actual_start;
|
|
688
690
|
ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
|
|
689
|
-
|
|
691
|
+
const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
|
|
692
|
+
pos.x_actual_start += aWidth * offsetX_actual;
|
|
693
|
+
pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
|
|
694
|
+
ctx.fillRect(pos.x_actual_start, taskY, aWidth * percent_actual, taskHeight);
|
|
690
695
|
}
|
|
691
696
|
if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
|
|
692
697
|
ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
|
|
698
|
+
pos.x_plan_start += width * offsetX;
|
|
699
|
+
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
693
700
|
ctx.lineWidth = 4;
|
|
694
701
|
ctx.beginPath();
|
|
695
702
|
ctx.moveTo(pos.x_plan_start, taskY);
|
|
696
|
-
ctx.lineTo(pos.x_plan_start + width, taskY);
|
|
703
|
+
ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
|
|
697
704
|
ctx.stroke();
|
|
698
705
|
}
|
|
699
706
|
ctx.fillStyle = "#333";
|
|
@@ -736,25 +743,32 @@
|
|
|
736
743
|
const chartY = mouseY + this.scrollTop;
|
|
737
744
|
const rowIndex = Math.floor(chartY / this.config.rowHeight);
|
|
738
745
|
const date = this.xToDate(chartX);
|
|
739
|
-
if (rowIndex < 0 || rowIndex >= this.data.length)
|
|
740
|
-
return this.handleMouseLeave();
|
|
746
|
+
if (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
|
|
741
747
|
const row = this.data[rowIndex];
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
if (
|
|
745
|
-
return
|
|
746
|
-
if (task.actualStart) {
|
|
747
|
-
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
|
|
748
|
-
if (date >= aStart && date < aEnd)
|
|
749
|
-
return true;
|
|
748
|
+
if (this.config.tooltipFormat) {
|
|
749
|
+
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
750
|
+
if (!htmlStr) {
|
|
751
|
+
return this.handleMouseLeave();
|
|
750
752
|
}
|
|
751
|
-
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
753
|
+
this.tooltip.innerHTML = htmlStr;
|
|
754
|
+
} else {
|
|
755
|
+
const overlappingTasks = row.tasks.filter((task) => {
|
|
756
|
+
const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
|
|
757
|
+
if (date >= pStart && date < pEnd) return true;
|
|
758
|
+
if (task.actualStart) {
|
|
759
|
+
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
|
|
760
|
+
if (date >= aStart && date < aEnd) return true;
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
});
|
|
764
|
+
if (overlappingTasks.length === 0) return this.handleMouseLeave();
|
|
765
|
+
let html = `<strong>${row.name}</strong> (${DateUtils.format(
|
|
766
|
+
date,
|
|
767
|
+
"yyyy-MM-dd"
|
|
768
|
+
)})<hr class="__gantt_tooltip-divider">`;
|
|
769
|
+
overlappingTasks.forEach((task) => html += this.getTaskTooltipHtml(task));
|
|
770
|
+
this.tooltip.innerHTML = html;
|
|
771
|
+
}
|
|
758
772
|
this.tooltip.style.display = "block";
|
|
759
773
|
if (this.config.tooltipColor === "white") {
|
|
760
774
|
this.tooltip.style.background = "#fff";
|
|
@@ -762,10 +776,8 @@
|
|
|
762
776
|
}
|
|
763
777
|
const tipRect = this.tooltip.getBoundingClientRect();
|
|
764
778
|
let x = e.clientX + 15, y = e.clientY + 15;
|
|
765
|
-
if (x + tipRect.width > window.innerWidth)
|
|
766
|
-
|
|
767
|
-
if (y + tipRect.height > window.innerHeight)
|
|
768
|
-
y = e.clientY - 15 - tipRect.height;
|
|
779
|
+
if (x + tipRect.width > window.innerWidth) x = e.clientX - 15 - tipRect.width;
|
|
780
|
+
if (y + tipRect.height > window.innerHeight) y = e.clientY - 15 - tipRect.height;
|
|
769
781
|
this.tooltip.style.left = `${x + this.config.offsetLeft}px`;
|
|
770
782
|
this.tooltip.style.top = `${y + this.config.offsetTop}px`;
|
|
771
783
|
}
|
|
@@ -788,7 +800,17 @@
|
|
|
788
800
|
handleMouseLeave() {
|
|
789
801
|
this.tooltip.style.display = "none";
|
|
790
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
805
|
+
* @param diffMilliseconds 距离目标日期时间的差异毫秒数
|
|
806
|
+
* @param pixelsPerDay 每日的像素数
|
|
807
|
+
* @returns
|
|
808
|
+
*/
|
|
809
|
+
static getTaskWidthPercent(diffMilliseconds, pixelsPerDay) {
|
|
810
|
+
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
811
|
+
}
|
|
791
812
|
}
|
|
813
|
+
exports2.DateUtils = DateUtils;
|
|
792
814
|
exports2.GanttChart = GanttChart;
|
|
793
815
|
Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
|
|
794
816
|
}));
|
package/dist/main.d.ts
CHANGED
package/package.json
CHANGED