gantt-canvas-chart 1.1.1 → 1.3.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 +323 -75
- package/dist/index.css +2 -2
- package/dist/index.d.ts +41 -11
- package/dist/index.es.js +323 -75
- package/dist/index.umd.js +323 -75
- package/package.json +1 -1
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.
|
|
2
|
+
* gantt-canvas-chart v1.3.0
|
|
3
3
|
* (c) 2025-present chandq
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -89,12 +89,14 @@ class GanttChart {
|
|
|
89
89
|
mainCanvas;
|
|
90
90
|
scrollDummy;
|
|
91
91
|
tooltip;
|
|
92
|
+
scrolling;
|
|
93
|
+
showTooltip;
|
|
92
94
|
headerCtx;
|
|
93
95
|
mainCtx;
|
|
94
96
|
timelineStart;
|
|
95
97
|
timelineEnd;
|
|
96
98
|
minDate;
|
|
97
|
-
|
|
99
|
+
maxDate;
|
|
98
100
|
pixelsPerDay;
|
|
99
101
|
scrollLeft;
|
|
100
102
|
scrollTop;
|
|
@@ -108,9 +110,14 @@ class GanttChart {
|
|
|
108
110
|
resizeObserver;
|
|
109
111
|
taskPositions;
|
|
110
112
|
taskMap;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
isLoadingData = false;
|
|
114
|
+
hasMoreDataLeft = true;
|
|
115
|
+
hasMoreDataRight = true;
|
|
116
|
+
hasMoreDataBottom = true;
|
|
117
|
+
lastScrollLeft = 0;
|
|
118
|
+
lastScrollTop = 0;
|
|
119
|
+
onDataLoad = null;
|
|
120
|
+
scrollLoadTimer = null;
|
|
114
121
|
constructor(rootContainer, data, config = {}) {
|
|
115
122
|
const container = document.createElement("div");
|
|
116
123
|
const scrollEl = document.createElement("div");
|
|
@@ -129,6 +136,8 @@ class GanttChart {
|
|
|
129
136
|
this.rootContainer = rootContainer;
|
|
130
137
|
this.container = container;
|
|
131
138
|
this.data = data;
|
|
139
|
+
this.scrolling = false;
|
|
140
|
+
this.showTooltip = false;
|
|
132
141
|
this.config = {
|
|
133
142
|
viewMode: "Month",
|
|
134
143
|
rowHeight: 48,
|
|
@@ -146,11 +155,14 @@ class GanttChart {
|
|
|
146
155
|
todayColor: "#ff4d4f",
|
|
147
156
|
offsetTop: 0,
|
|
148
157
|
offsetLeft: 0,
|
|
158
|
+
enabledLoadMore: [],
|
|
149
159
|
viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
|
|
150
160
|
planBorderColor: "#C1EFCF",
|
|
151
161
|
actualBgColor: "#5AC989",
|
|
162
|
+
headerBgColor: "#f9f9f9",
|
|
152
163
|
...config
|
|
153
164
|
};
|
|
165
|
+
this.updateLoadMoreConf();
|
|
154
166
|
this.headerCanvas = headerCanvas;
|
|
155
167
|
this.mainCanvas = mainCanvas;
|
|
156
168
|
this.scrollDummy = scrollEl;
|
|
@@ -164,6 +176,7 @@ class GanttChart {
|
|
|
164
176
|
this.timelineStart = /* @__PURE__ */ new Date();
|
|
165
177
|
this.timelineEnd = /* @__PURE__ */ new Date();
|
|
166
178
|
this.minDate = null;
|
|
179
|
+
this.maxDate = null;
|
|
167
180
|
this.pixelsPerDay = 40;
|
|
168
181
|
this.scrollLeft = 0;
|
|
169
182
|
this.scrollTop = 0;
|
|
@@ -176,10 +189,11 @@ class GanttChart {
|
|
|
176
189
|
this.totalHeight = 0;
|
|
177
190
|
this.taskPositions = /* @__PURE__ */ new Map();
|
|
178
191
|
this.taskMap = /* @__PURE__ */ new Map();
|
|
179
|
-
this.
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
182
|
-
this.
|
|
192
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
193
|
+
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
|
194
|
+
this.handleScroll = this.handleScroll.bind(this);
|
|
195
|
+
this.horizontalScrollTo = this.horizontalScrollTo.bind(this);
|
|
196
|
+
this.verticalScrollTo = this.verticalScrollTo.bind(this);
|
|
183
197
|
this.init();
|
|
184
198
|
}
|
|
185
199
|
init() {
|
|
@@ -189,6 +203,11 @@ class GanttChart {
|
|
|
189
203
|
this.setupEvents();
|
|
190
204
|
this.handleResize();
|
|
191
205
|
}
|
|
206
|
+
updateLoadMoreConf() {
|
|
207
|
+
this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
|
|
208
|
+
this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
|
|
209
|
+
this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
|
|
210
|
+
}
|
|
192
211
|
buildTaskMap() {
|
|
193
212
|
this.taskMap.clear();
|
|
194
213
|
this.data.forEach((row, rowIndex) => {
|
|
@@ -196,7 +215,7 @@ class GanttChart {
|
|
|
196
215
|
});
|
|
197
216
|
}
|
|
198
217
|
setupEvents() {
|
|
199
|
-
this.container.addEventListener("scroll", this.
|
|
218
|
+
this.container.addEventListener("scroll", this.handleScroll);
|
|
200
219
|
this.handleResize = this.handleResize.bind(this);
|
|
201
220
|
if (window.ResizeObserver) {
|
|
202
221
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
@@ -205,8 +224,8 @@ class GanttChart {
|
|
|
205
224
|
}, 100);
|
|
206
225
|
}
|
|
207
226
|
if (this.config.showTooltip) {
|
|
208
|
-
this.mainCanvas.addEventListener("mousemove", this.
|
|
209
|
-
this.mainCanvas.addEventListener("mouseleave", this.
|
|
227
|
+
this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
|
|
228
|
+
this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
|
|
210
229
|
}
|
|
211
230
|
}
|
|
212
231
|
updateConfig(newConfig) {
|
|
@@ -217,6 +236,7 @@ class GanttChart {
|
|
|
217
236
|
this.updatePixelsPerDay();
|
|
218
237
|
this.calculateFullTimeline();
|
|
219
238
|
}
|
|
239
|
+
this.updateLoadMoreConf();
|
|
220
240
|
this.updateDimensions();
|
|
221
241
|
this.render();
|
|
222
242
|
}
|
|
@@ -235,17 +255,18 @@ class GanttChart {
|
|
|
235
255
|
if (this.resizeObserver) {
|
|
236
256
|
this.resizeObserver.disconnect();
|
|
237
257
|
}
|
|
238
|
-
this.container.removeEventListener("scroll", this.
|
|
239
|
-
this.mainCanvas.removeEventListener("mousemove", this.
|
|
240
|
-
this.mainCanvas.removeEventListener("mouseleave", this.
|
|
258
|
+
this.container.removeEventListener("scroll", this.handleScroll);
|
|
259
|
+
this.mainCanvas.removeEventListener("mousemove", this.handleMouseMove);
|
|
260
|
+
this.mainCanvas.removeEventListener("mouseleave", this.handleMouseLeave);
|
|
241
261
|
this.container.remove();
|
|
242
262
|
}
|
|
243
263
|
calculateFullTimeline() {
|
|
264
|
+
const currentYear = this.today.getFullYear();
|
|
244
265
|
let minDate = new Date(9999, 0, 1);
|
|
245
266
|
let maxDate = new Date(1e3, 0, 1);
|
|
246
267
|
if (this.data.length === 0) {
|
|
247
268
|
minDate = /* @__PURE__ */ new Date();
|
|
248
|
-
maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(),
|
|
269
|
+
maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(), 60);
|
|
249
270
|
} else {
|
|
250
271
|
this.taskMap.forEach(({ task }) => {
|
|
251
272
|
const pStart = new Date(task.planStart);
|
|
@@ -261,8 +282,11 @@ class GanttChart {
|
|
|
261
282
|
});
|
|
262
283
|
}
|
|
263
284
|
this.minDate = minDate;
|
|
264
|
-
|
|
265
|
-
|
|
285
|
+
this.maxDate = maxDate;
|
|
286
|
+
const minYear = minDate.getFullYear();
|
|
287
|
+
const maxYear = maxDate.getFullYear();
|
|
288
|
+
minDate = DateUtils.addDays(minYear === 9999 ? new Date(currentYear, 0, 1) : minDate, -7);
|
|
289
|
+
maxDate = DateUtils.addDays(maxYear === 1e3 ? new Date(currentYear + 1, 0, 1) : maxDate, 14);
|
|
266
290
|
switch (this.config.viewMode) {
|
|
267
291
|
case "Year":
|
|
268
292
|
this.timelineStart = DateUtils.getStartOfYear(minDate);
|
|
@@ -301,7 +325,14 @@ class GanttChart {
|
|
|
301
325
|
this.updateDimensions();
|
|
302
326
|
this.render();
|
|
303
327
|
}
|
|
328
|
+
// Add this method to register the data loading callback
|
|
329
|
+
setOnDataLoadCallback(callback) {
|
|
330
|
+
this.onDataLoad = callback;
|
|
331
|
+
}
|
|
304
332
|
handleScroll(e) {
|
|
333
|
+
if (this.showTooltip) {
|
|
334
|
+
this.handleMouseLeave();
|
|
335
|
+
}
|
|
305
336
|
const target = e.target;
|
|
306
337
|
this.scrollLeft = target.scrollLeft;
|
|
307
338
|
this.scrollTop = target.scrollTop;
|
|
@@ -309,7 +340,134 @@ class GanttChart {
|
|
|
309
340
|
detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
|
|
310
341
|
});
|
|
311
342
|
this.container.dispatchEvent(event);
|
|
312
|
-
|
|
343
|
+
if (this.config.enabledLoadMore.length > 0) {
|
|
344
|
+
if (this.scrollLoadTimer !== null) {
|
|
345
|
+
clearTimeout(this.scrollLoadTimer);
|
|
346
|
+
this.scrollLoadTimer = null;
|
|
347
|
+
}
|
|
348
|
+
this.scrollLoadTimer = window.setTimeout(() => {
|
|
349
|
+
this.checkScrollLoad();
|
|
350
|
+
this.scrollLoadTimer = null;
|
|
351
|
+
}, 100);
|
|
352
|
+
}
|
|
353
|
+
requestAnimationFrame(() => {
|
|
354
|
+
this.scrolling = true;
|
|
355
|
+
this.render(true);
|
|
356
|
+
this.scrolling = false;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
// checkScrollLoad method
|
|
360
|
+
async checkScrollLoad() {
|
|
361
|
+
if (this.isLoadingData || !this.onDataLoad) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const scrollLeft = this.scrollLeft;
|
|
365
|
+
const scrollTop = this.scrollTop;
|
|
366
|
+
const viewportWidth = this.viewportWidth;
|
|
367
|
+
const viewportHeight = this.viewportHeight;
|
|
368
|
+
const totalWidth = this.totalWidth;
|
|
369
|
+
const totalHeight = this.totalHeight;
|
|
370
|
+
const atLeftEdge = scrollLeft <= 5;
|
|
371
|
+
const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
|
|
372
|
+
const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
|
|
373
|
+
try {
|
|
374
|
+
if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
|
|
375
|
+
await this.loadMoreData("left");
|
|
376
|
+
console.log("left-loadMoreData::", this.data);
|
|
377
|
+
} else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
|
|
378
|
+
await this.loadMoreData("right");
|
|
379
|
+
console.log("right-loadMoreData::", this.data);
|
|
380
|
+
} else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
|
|
381
|
+
await this.loadMoreData("bottom");
|
|
382
|
+
console.log("bottom-loadMoreData::", this.data);
|
|
383
|
+
}
|
|
384
|
+
} finally {
|
|
385
|
+
this.lastScrollLeft = scrollLeft;
|
|
386
|
+
this.lastScrollTop = scrollTop;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Add this method to reset scroll loading state
|
|
390
|
+
resetScrollLoadingState() {
|
|
391
|
+
this.hasMoreDataLeft = true;
|
|
392
|
+
this.hasMoreDataRight = true;
|
|
393
|
+
this.hasMoreDataBottom = true;
|
|
394
|
+
this.lastScrollLeft = 0;
|
|
395
|
+
this.lastScrollTop = 0;
|
|
396
|
+
if (this.scrollLoadTimer !== null) {
|
|
397
|
+
clearTimeout(this.scrollLoadTimer);
|
|
398
|
+
this.scrollLoadTimer = null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Add this new method to load additional data
|
|
402
|
+
async loadMoreData(direction) {
|
|
403
|
+
if (this.isLoadingData || !this.onDataLoad) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
this.isLoadingData = true;
|
|
407
|
+
try {
|
|
408
|
+
let newData = null;
|
|
409
|
+
switch (direction) {
|
|
410
|
+
case "left":
|
|
411
|
+
newData = await this.onDataLoad("left", { date: this.minDate });
|
|
412
|
+
if (newData && newData.length > 0) {
|
|
413
|
+
this.prependData(newData);
|
|
414
|
+
} else {
|
|
415
|
+
this.hasMoreDataLeft = false;
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
case "right":
|
|
419
|
+
newData = await this.onDataLoad("right", { date: this.maxDate });
|
|
420
|
+
if (newData && newData.length > 0) {
|
|
421
|
+
this.appendData(newData);
|
|
422
|
+
} else {
|
|
423
|
+
this.hasMoreDataRight = false;
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
case "bottom":
|
|
427
|
+
const currentRowCount = this.data.length;
|
|
428
|
+
newData = await this.onDataLoad("bottom", { offset: currentRowCount });
|
|
429
|
+
if (newData && newData.length > 0) {
|
|
430
|
+
this.appendRows(newData);
|
|
431
|
+
} else {
|
|
432
|
+
this.hasMoreDataBottom = false;
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
if (newData && newData.length > 0) {
|
|
437
|
+
this.buildTaskMap();
|
|
438
|
+
this.calculateFullTimeline();
|
|
439
|
+
this.updateDimensions();
|
|
440
|
+
this.render();
|
|
441
|
+
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error("Error loading additional data:", error);
|
|
444
|
+
} finally {
|
|
445
|
+
this.isLoadingData = false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Add this method to append data to the right
|
|
449
|
+
appendData(newData) {
|
|
450
|
+
newData.forEach((newRow, index) => {
|
|
451
|
+
if (index < this.data.length) {
|
|
452
|
+
this.data[index].tasks = [...this.data[index].tasks, ...newRow.tasks];
|
|
453
|
+
} else {
|
|
454
|
+
this.data.push(newRow);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
// Add this method to prepend data to the left
|
|
459
|
+
prependData(newData) {
|
|
460
|
+
newData.forEach((newRow, index) => {
|
|
461
|
+
if (index < this.data.length) {
|
|
462
|
+
this.data[index].tasks = [...newRow.tasks, ...this.data[index].tasks];
|
|
463
|
+
} else {
|
|
464
|
+
this.data.push(newRow);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// Add this method to append rows to the bottom
|
|
469
|
+
appendRows(newData) {
|
|
470
|
+
this.data.push(...newData);
|
|
313
471
|
}
|
|
314
472
|
setScrollTop(scrollTop) {
|
|
315
473
|
if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
|
|
@@ -323,10 +481,14 @@ class GanttChart {
|
|
|
323
481
|
}
|
|
324
482
|
updateDimensions() {
|
|
325
483
|
const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
this.
|
|
329
|
-
|
|
484
|
+
const newTotalWidth = totalDays * this.pixelsPerDay;
|
|
485
|
+
const newTotalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
|
|
486
|
+
if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
|
|
487
|
+
this.totalWidth = newTotalWidth;
|
|
488
|
+
this.totalHeight = newTotalHeight;
|
|
489
|
+
this.scrollDummy.style.width = `${this.totalWidth}px`;
|
|
490
|
+
this.scrollDummy.style.height = `${this.totalHeight}px`;
|
|
491
|
+
}
|
|
330
492
|
}
|
|
331
493
|
setupCanvas(canvas, width, height) {
|
|
332
494
|
canvas.width = width * this.devicePixelRatio;
|
|
@@ -344,13 +506,13 @@ class GanttChart {
|
|
|
344
506
|
const y = i * this.config.rowHeight;
|
|
345
507
|
row.tasks.forEach((task) => {
|
|
346
508
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
347
|
-
const x_plan_end = this.dateToX(DateUtils.addDays(
|
|
509
|
+
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
348
510
|
let x_actual_start = null, x_actual_end = null;
|
|
349
511
|
if (task.actualStart) {
|
|
350
512
|
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
351
513
|
}
|
|
352
514
|
if (task.actualEnd) {
|
|
353
|
-
x_actual_end = this.dateToX(DateUtils.addDays(
|
|
515
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
354
516
|
}
|
|
355
517
|
this.taskPositions.set(task.id, {
|
|
356
518
|
x_plan_start,
|
|
@@ -376,9 +538,11 @@ class GanttChart {
|
|
|
376
538
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
377
539
|
}
|
|
378
540
|
}
|
|
379
|
-
render() {
|
|
541
|
+
render(scrolling = false) {
|
|
380
542
|
this.updateVirtualRanges();
|
|
381
|
-
|
|
543
|
+
if (!scrolling) {
|
|
544
|
+
this.calculateAllTaskPositions();
|
|
545
|
+
}
|
|
382
546
|
this.renderHeader();
|
|
383
547
|
this.renderMain();
|
|
384
548
|
}
|
|
@@ -388,82 +552,132 @@ class GanttChart {
|
|
|
388
552
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
389
553
|
ctx.save();
|
|
390
554
|
ctx.translate(-this.scrollLeft, 0);
|
|
391
|
-
ctx.fillStyle =
|
|
555
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
392
556
|
ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
|
|
393
557
|
ctx.textBaseline = "middle";
|
|
394
558
|
ctx.textRendering = "optimizeLegibility";
|
|
395
559
|
let currentDate = new Date(this.visibleDateRange.start);
|
|
396
560
|
currentDate = this.getIterationStartDate(currentDate);
|
|
397
|
-
|
|
398
|
-
|
|
561
|
+
const visibleBlocks = [];
|
|
562
|
+
let calcDate = new Date(currentDate);
|
|
563
|
+
while (calcDate <= this.visibleDateRange.end) {
|
|
399
564
|
let nextDate;
|
|
565
|
+
let upperText = "";
|
|
400
566
|
switch (this.config.viewMode) {
|
|
401
567
|
case "Day":
|
|
402
|
-
|
|
568
|
+
upperText = DateUtils.format(calcDate, "yyyy年MM月");
|
|
569
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
403
570
|
break;
|
|
404
571
|
case "Week":
|
|
405
|
-
|
|
572
|
+
const weekStart = DateUtils.getStartOfWeek(calcDate);
|
|
573
|
+
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
574
|
+
nextDate = DateUtils.addDays(weekStart, 7);
|
|
406
575
|
break;
|
|
407
576
|
case "Month":
|
|
408
|
-
|
|
577
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
578
|
+
nextDate = DateUtils.addMonths(calcDate, 1);
|
|
409
579
|
break;
|
|
410
580
|
case "Year":
|
|
411
|
-
|
|
581
|
+
if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
|
|
582
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
583
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
584
|
+
} else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
|
|
585
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
586
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
587
|
+
} else {
|
|
588
|
+
calcDate = DateUtils.addDays(calcDate, 1);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
default:
|
|
593
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
const startX = this.dateToX(calcDate);
|
|
597
|
+
const endX = this.dateToX(nextDate);
|
|
598
|
+
visibleBlocks.push({
|
|
599
|
+
startX,
|
|
600
|
+
endX,
|
|
601
|
+
text: upperText,
|
|
602
|
+
yPos: h * 0.35
|
|
603
|
+
});
|
|
604
|
+
calcDate = nextDate;
|
|
605
|
+
}
|
|
606
|
+
let currentDateForLower = new Date(currentDate);
|
|
607
|
+
while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
|
|
608
|
+
let nextDate;
|
|
609
|
+
switch (this.config.viewMode) {
|
|
610
|
+
case "Day":
|
|
611
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
612
|
+
break;
|
|
613
|
+
case "Week":
|
|
614
|
+
nextDate = DateUtils.addDays(currentDateForLower, 7);
|
|
615
|
+
break;
|
|
616
|
+
case "Month":
|
|
617
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
618
|
+
break;
|
|
619
|
+
case "Year":
|
|
620
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
412
621
|
break;
|
|
413
622
|
default:
|
|
414
|
-
nextDate = DateUtils.addDays(
|
|
623
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
415
624
|
break;
|
|
416
625
|
}
|
|
417
|
-
if (nextDate.getTime() ===
|
|
418
|
-
|
|
626
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
627
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
419
628
|
} else {
|
|
420
|
-
|
|
629
|
+
currentDateForLower = nextDate;
|
|
421
630
|
}
|
|
422
631
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
632
|
+
const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
|
|
633
|
+
ctx.fillStyle = "#333";
|
|
634
|
+
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
635
|
+
ctx.textAlign = "left";
|
|
636
|
+
groupedBlocks.forEach((group) => {
|
|
637
|
+
const visibleStart = Math.max(group.startX, this.scrollLeft);
|
|
638
|
+
const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
|
|
639
|
+
if (visibleEnd > visibleStart) {
|
|
640
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
641
|
+
ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
|
|
642
|
+
ctx.fillStyle = "#333";
|
|
643
|
+
ctx.fillText(group.text, visibleStart + 5, group.yPos);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
while (currentDateForLower <= this.visibleDateRange.end) {
|
|
647
|
+
const x = this.dateToX(currentDateForLower);
|
|
648
|
+
let lowerText = "";
|
|
649
|
+
let nextDate;
|
|
426
650
|
switch (this.config.viewMode) {
|
|
427
651
|
case "Day":
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
652
|
+
lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
|
|
653
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
431
654
|
break;
|
|
432
655
|
case "Week":
|
|
433
|
-
const weekStart = DateUtils.getStartOfWeek(
|
|
434
|
-
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
656
|
+
const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
|
|
435
657
|
lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
|
|
436
658
|
nextDate = DateUtils.addDays(weekStart, 7);
|
|
437
659
|
break;
|
|
438
660
|
case "Month":
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
nextDate = DateUtils.addMonths(currentDate, 1);
|
|
661
|
+
lowerText = `${currentDateForLower.getMonth() + 1}月`;
|
|
662
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
442
663
|
break;
|
|
443
664
|
case "Year":
|
|
444
|
-
if (
|
|
445
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
665
|
+
if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
|
|
446
666
|
lowerText = `上半年`;
|
|
447
|
-
nextDate = DateUtils.addMonths(
|
|
448
|
-
} else if (
|
|
449
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
667
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
668
|
+
} else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
|
|
450
669
|
lowerText = `下半年`;
|
|
451
|
-
nextDate = DateUtils.addMonths(
|
|
670
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
452
671
|
} else {
|
|
453
|
-
|
|
672
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
454
673
|
continue;
|
|
455
674
|
}
|
|
456
675
|
break;
|
|
676
|
+
default:
|
|
677
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
678
|
+
break;
|
|
457
679
|
}
|
|
458
680
|
const unitWidth = this.dateToX(nextDate) - x;
|
|
459
|
-
if (upperText !== lastUpperText) {
|
|
460
|
-
ctx.fillStyle = "#333";
|
|
461
|
-
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
462
|
-
ctx.textRendering = "optimizeLegibility";
|
|
463
|
-
ctx.textAlign = "left";
|
|
464
|
-
ctx.fillText(upperText, x + 5, h * 0.35);
|
|
465
|
-
lastUpperText = upperText;
|
|
466
|
-
}
|
|
467
681
|
ctx.fillStyle = "#000412";
|
|
468
682
|
ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
469
683
|
ctx.textAlign = "center";
|
|
@@ -473,10 +687,10 @@ class GanttChart {
|
|
|
473
687
|
ctx.lineTo(x, h);
|
|
474
688
|
ctx.strokeStyle = "#e0e0e0";
|
|
475
689
|
ctx.stroke();
|
|
476
|
-
if (nextDate.getTime() ===
|
|
477
|
-
|
|
690
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
691
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
478
692
|
} else {
|
|
479
|
-
|
|
693
|
+
currentDateForLower = nextDate;
|
|
480
694
|
}
|
|
481
695
|
}
|
|
482
696
|
ctx.beginPath();
|
|
@@ -486,6 +700,22 @@ class GanttChart {
|
|
|
486
700
|
ctx.stroke();
|
|
487
701
|
ctx.restore();
|
|
488
702
|
}
|
|
703
|
+
// Helper method to group consecutive blocks with same text
|
|
704
|
+
groupConsecutiveBlocks(blocks) {
|
|
705
|
+
if (blocks.length === 0) return [];
|
|
706
|
+
const grouped = [];
|
|
707
|
+
let currentGroup = { ...blocks[0] };
|
|
708
|
+
for (let i = 1; i < blocks.length; i++) {
|
|
709
|
+
if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
|
|
710
|
+
currentGroup.endX = blocks[i].endX;
|
|
711
|
+
} else {
|
|
712
|
+
grouped.push(currentGroup);
|
|
713
|
+
currentGroup = { ...blocks[i] };
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
grouped.push(currentGroup);
|
|
717
|
+
return grouped;
|
|
718
|
+
}
|
|
489
719
|
renderMain() {
|
|
490
720
|
const ctx = this.mainCtx;
|
|
491
721
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -681,7 +911,7 @@ class GanttChart {
|
|
|
681
911
|
const x = this.dateToX(this.today);
|
|
682
912
|
if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
|
|
683
913
|
ctx.strokeStyle = this.config.todayColor;
|
|
684
|
-
ctx.lineWidth =
|
|
914
|
+
ctx.lineWidth = 1;
|
|
685
915
|
ctx.beginPath();
|
|
686
916
|
ctx.moveTo(x, this.scrollTop);
|
|
687
917
|
ctx.lineTo(x, this.scrollTop + this.viewportHeight);
|
|
@@ -709,8 +939,8 @@ class GanttChart {
|
|
|
709
939
|
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
710
940
|
ctx.lineWidth = 4;
|
|
711
941
|
ctx.beginPath();
|
|
712
|
-
ctx.moveTo(pos.x_plan_start, taskY);
|
|
713
|
-
ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
|
|
942
|
+
ctx.moveTo(pos.x_plan_start + 2, taskY);
|
|
943
|
+
ctx.lineTo(pos.x_plan_start + width * percent_plan - 2, taskY);
|
|
714
944
|
ctx.stroke();
|
|
715
945
|
}
|
|
716
946
|
ctx.fillStyle = "#000";
|
|
@@ -746,6 +976,9 @@ class GanttChart {
|
|
|
746
976
|
return styles;
|
|
747
977
|
}
|
|
748
978
|
handleMouseMove(e) {
|
|
979
|
+
if (this.scrolling) {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
749
982
|
const rect = this.mainCanvas.getBoundingClientRect();
|
|
750
983
|
const mouseX = e.clientX - rect.left;
|
|
751
984
|
const mouseY = e.clientY - rect.top;
|
|
@@ -758,15 +991,16 @@ class GanttChart {
|
|
|
758
991
|
if (this.config.tooltipFormat) {
|
|
759
992
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
760
993
|
if (!htmlStr) {
|
|
761
|
-
|
|
994
|
+
this.handleMouseLeave();
|
|
995
|
+
return;
|
|
762
996
|
}
|
|
763
997
|
this.tooltip.innerHTML = htmlStr;
|
|
764
998
|
} else {
|
|
765
999
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
766
|
-
const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(
|
|
1000
|
+
const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
|
|
767
1001
|
if (date >= pStart && date < pEnd) return true;
|
|
768
1002
|
if (task.actualStart) {
|
|
769
|
-
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(
|
|
1003
|
+
const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
|
|
770
1004
|
if (date >= aStart && date < aEnd) return true;
|
|
771
1005
|
}
|
|
772
1006
|
return false;
|
|
@@ -780,6 +1014,7 @@ class GanttChart {
|
|
|
780
1014
|
this.tooltip.innerHTML = html;
|
|
781
1015
|
}
|
|
782
1016
|
this.tooltip.style.display = "block";
|
|
1017
|
+
this.showTooltip = true;
|
|
783
1018
|
if (this.config.tooltipColor === "white") {
|
|
784
1019
|
this.tooltip.style.background = "#fff";
|
|
785
1020
|
this.tooltip.style.color = "#000";
|
|
@@ -809,6 +1044,7 @@ class GanttChart {
|
|
|
809
1044
|
}
|
|
810
1045
|
handleMouseLeave() {
|
|
811
1046
|
this.tooltip.style.display = "none";
|
|
1047
|
+
this.showTooltip = false;
|
|
812
1048
|
}
|
|
813
1049
|
/**
|
|
814
1050
|
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
@@ -820,17 +1056,29 @@ class GanttChart {
|
|
|
820
1056
|
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
821
1057
|
}
|
|
822
1058
|
/**
|
|
823
|
-
* scroll to specified date position, default to minDate
|
|
1059
|
+
* horizontal scroll to specified date position, default to minDate
|
|
824
1060
|
*
|
|
825
1061
|
* @param date
|
|
826
1062
|
*/
|
|
827
|
-
|
|
1063
|
+
horizontalScrollTo(date) {
|
|
828
1064
|
const startDate = date ? date : this.minDate;
|
|
829
1065
|
if (startDate) {
|
|
830
1066
|
const xPosition = this.dateToX(startDate);
|
|
831
1067
|
this.container.scrollTo({ left: xPosition - 80 });
|
|
832
1068
|
}
|
|
833
1069
|
}
|
|
1070
|
+
/**
|
|
1071
|
+
* vertical scroll to specified position
|
|
1072
|
+
*
|
|
1073
|
+
* @param params
|
|
1074
|
+
*/
|
|
1075
|
+
verticalScrollTo(params) {
|
|
1076
|
+
if (params && (params.rowId || params.rowIndex)) {
|
|
1077
|
+
const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
|
|
1078
|
+
const yPosition = this.config.rowHeight * rowIndex;
|
|
1079
|
+
this.container.scrollTo({ top: yPosition - 80 });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
834
1082
|
}
|
|
835
1083
|
export {
|
|
836
1084
|
DateUtils,
|