gantt-canvas-chart 1.2.0 → 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 +314 -71
- package/dist/index.css +2 -2
- package/dist/index.d.ts +38 -9
- package/dist/index.es.js +314 -71
- package/dist/index.umd.js +314 -71
- 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,6 +89,8 @@ class GanttChart {
|
|
|
89
89
|
mainCanvas;
|
|
90
90
|
scrollDummy;
|
|
91
91
|
tooltip;
|
|
92
|
+
scrolling;
|
|
93
|
+
showTooltip;
|
|
92
94
|
headerCtx;
|
|
93
95
|
mainCtx;
|
|
94
96
|
timelineStart;
|
|
@@ -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;
|
|
@@ -177,10 +189,11 @@ class GanttChart {
|
|
|
177
189
|
this.totalHeight = 0;
|
|
178
190
|
this.taskPositions = /* @__PURE__ */ new Map();
|
|
179
191
|
this.taskMap = /* @__PURE__ */ new Map();
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
182
|
-
this.
|
|
183
|
-
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);
|
|
184
197
|
this.init();
|
|
185
198
|
}
|
|
186
199
|
init() {
|
|
@@ -190,6 +203,11 @@ class GanttChart {
|
|
|
190
203
|
this.setupEvents();
|
|
191
204
|
this.handleResize();
|
|
192
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
|
+
}
|
|
193
211
|
buildTaskMap() {
|
|
194
212
|
this.taskMap.clear();
|
|
195
213
|
this.data.forEach((row, rowIndex) => {
|
|
@@ -197,7 +215,7 @@ class GanttChart {
|
|
|
197
215
|
});
|
|
198
216
|
}
|
|
199
217
|
setupEvents() {
|
|
200
|
-
this.container.addEventListener("scroll", this.
|
|
218
|
+
this.container.addEventListener("scroll", this.handleScroll);
|
|
201
219
|
this.handleResize = this.handleResize.bind(this);
|
|
202
220
|
if (window.ResizeObserver) {
|
|
203
221
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
@@ -206,8 +224,8 @@ class GanttChart {
|
|
|
206
224
|
}, 100);
|
|
207
225
|
}
|
|
208
226
|
if (this.config.showTooltip) {
|
|
209
|
-
this.mainCanvas.addEventListener("mousemove", this.
|
|
210
|
-
this.mainCanvas.addEventListener("mouseleave", this.
|
|
227
|
+
this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
|
|
228
|
+
this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
|
|
211
229
|
}
|
|
212
230
|
}
|
|
213
231
|
updateConfig(newConfig) {
|
|
@@ -218,6 +236,7 @@ class GanttChart {
|
|
|
218
236
|
this.updatePixelsPerDay();
|
|
219
237
|
this.calculateFullTimeline();
|
|
220
238
|
}
|
|
239
|
+
this.updateLoadMoreConf();
|
|
221
240
|
this.updateDimensions();
|
|
222
241
|
this.render();
|
|
223
242
|
}
|
|
@@ -236,13 +255,13 @@ class GanttChart {
|
|
|
236
255
|
if (this.resizeObserver) {
|
|
237
256
|
this.resizeObserver.disconnect();
|
|
238
257
|
}
|
|
239
|
-
this.container.removeEventListener("scroll", this.
|
|
240
|
-
this.mainCanvas.removeEventListener("mousemove", this.
|
|
241
|
-
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);
|
|
242
261
|
this.container.remove();
|
|
243
262
|
}
|
|
244
263
|
calculateFullTimeline() {
|
|
245
|
-
const currentYear =
|
|
264
|
+
const currentYear = this.today.getFullYear();
|
|
246
265
|
let minDate = new Date(9999, 0, 1);
|
|
247
266
|
let maxDate = new Date(1e3, 0, 1);
|
|
248
267
|
if (this.data.length === 0) {
|
|
@@ -306,7 +325,14 @@ class GanttChart {
|
|
|
306
325
|
this.updateDimensions();
|
|
307
326
|
this.render();
|
|
308
327
|
}
|
|
328
|
+
// Add this method to register the data loading callback
|
|
329
|
+
setOnDataLoadCallback(callback) {
|
|
330
|
+
this.onDataLoad = callback;
|
|
331
|
+
}
|
|
309
332
|
handleScroll(e) {
|
|
333
|
+
if (this.showTooltip) {
|
|
334
|
+
this.handleMouseLeave();
|
|
335
|
+
}
|
|
310
336
|
const target = e.target;
|
|
311
337
|
this.scrollLeft = target.scrollLeft;
|
|
312
338
|
this.scrollTop = target.scrollTop;
|
|
@@ -314,7 +340,134 @@ class GanttChart {
|
|
|
314
340
|
detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
|
|
315
341
|
});
|
|
316
342
|
this.container.dispatchEvent(event);
|
|
317
|
-
|
|
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);
|
|
318
471
|
}
|
|
319
472
|
setScrollTop(scrollTop) {
|
|
320
473
|
if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
|
|
@@ -328,10 +481,14 @@ class GanttChart {
|
|
|
328
481
|
}
|
|
329
482
|
updateDimensions() {
|
|
330
483
|
const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
this.
|
|
334
|
-
|
|
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
|
+
}
|
|
335
492
|
}
|
|
336
493
|
setupCanvas(canvas, width, height) {
|
|
337
494
|
canvas.width = width * this.devicePixelRatio;
|
|
@@ -349,13 +506,13 @@ class GanttChart {
|
|
|
349
506
|
const y = i * this.config.rowHeight;
|
|
350
507
|
row.tasks.forEach((task) => {
|
|
351
508
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
352
|
-
const x_plan_end = this.dateToX(DateUtils.addDays(
|
|
509
|
+
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
353
510
|
let x_actual_start = null, x_actual_end = null;
|
|
354
511
|
if (task.actualStart) {
|
|
355
512
|
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
356
513
|
}
|
|
357
514
|
if (task.actualEnd) {
|
|
358
|
-
x_actual_end = this.dateToX(DateUtils.addDays(
|
|
515
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
359
516
|
}
|
|
360
517
|
this.taskPositions.set(task.id, {
|
|
361
518
|
x_plan_start,
|
|
@@ -381,9 +538,11 @@ class GanttChart {
|
|
|
381
538
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
382
539
|
}
|
|
383
540
|
}
|
|
384
|
-
render() {
|
|
541
|
+
render(scrolling = false) {
|
|
385
542
|
this.updateVirtualRanges();
|
|
386
|
-
|
|
543
|
+
if (!scrolling) {
|
|
544
|
+
this.calculateAllTaskPositions();
|
|
545
|
+
}
|
|
387
546
|
this.renderHeader();
|
|
388
547
|
this.renderMain();
|
|
389
548
|
}
|
|
@@ -393,82 +552,132 @@ class GanttChart {
|
|
|
393
552
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
394
553
|
ctx.save();
|
|
395
554
|
ctx.translate(-this.scrollLeft, 0);
|
|
396
|
-
ctx.fillStyle =
|
|
555
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
397
556
|
ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
|
|
398
557
|
ctx.textBaseline = "middle";
|
|
399
558
|
ctx.textRendering = "optimizeLegibility";
|
|
400
559
|
let currentDate = new Date(this.visibleDateRange.start);
|
|
401
560
|
currentDate = this.getIterationStartDate(currentDate);
|
|
402
|
-
|
|
403
|
-
|
|
561
|
+
const visibleBlocks = [];
|
|
562
|
+
let calcDate = new Date(currentDate);
|
|
563
|
+
while (calcDate <= this.visibleDateRange.end) {
|
|
404
564
|
let nextDate;
|
|
565
|
+
let upperText = "";
|
|
405
566
|
switch (this.config.viewMode) {
|
|
406
567
|
case "Day":
|
|
407
|
-
|
|
568
|
+
upperText = DateUtils.format(calcDate, "yyyy年MM月");
|
|
569
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
408
570
|
break;
|
|
409
571
|
case "Week":
|
|
410
|
-
|
|
572
|
+
const weekStart = DateUtils.getStartOfWeek(calcDate);
|
|
573
|
+
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
574
|
+
nextDate = DateUtils.addDays(weekStart, 7);
|
|
411
575
|
break;
|
|
412
576
|
case "Month":
|
|
413
|
-
|
|
577
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
578
|
+
nextDate = DateUtils.addMonths(calcDate, 1);
|
|
414
579
|
break;
|
|
415
580
|
case "Year":
|
|
416
|
-
|
|
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);
|
|
417
621
|
break;
|
|
418
622
|
default:
|
|
419
|
-
nextDate = DateUtils.addDays(
|
|
623
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
420
624
|
break;
|
|
421
625
|
}
|
|
422
|
-
if (nextDate.getTime() ===
|
|
423
|
-
|
|
626
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
627
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
424
628
|
} else {
|
|
425
|
-
|
|
629
|
+
currentDateForLower = nextDate;
|
|
426
630
|
}
|
|
427
631
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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;
|
|
431
650
|
switch (this.config.viewMode) {
|
|
432
651
|
case "Day":
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
652
|
+
lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
|
|
653
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
436
654
|
break;
|
|
437
655
|
case "Week":
|
|
438
|
-
const weekStart = DateUtils.getStartOfWeek(
|
|
439
|
-
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
656
|
+
const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
|
|
440
657
|
lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
|
|
441
658
|
nextDate = DateUtils.addDays(weekStart, 7);
|
|
442
659
|
break;
|
|
443
660
|
case "Month":
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
nextDate = DateUtils.addMonths(currentDate, 1);
|
|
661
|
+
lowerText = `${currentDateForLower.getMonth() + 1}月`;
|
|
662
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
447
663
|
break;
|
|
448
664
|
case "Year":
|
|
449
|
-
if (
|
|
450
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
665
|
+
if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
|
|
451
666
|
lowerText = `上半年`;
|
|
452
|
-
nextDate = DateUtils.addMonths(
|
|
453
|
-
} else if (
|
|
454
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
667
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
668
|
+
} else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
|
|
455
669
|
lowerText = `下半年`;
|
|
456
|
-
nextDate = DateUtils.addMonths(
|
|
670
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
457
671
|
} else {
|
|
458
|
-
|
|
672
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
459
673
|
continue;
|
|
460
674
|
}
|
|
461
675
|
break;
|
|
676
|
+
default:
|
|
677
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
678
|
+
break;
|
|
462
679
|
}
|
|
463
680
|
const unitWidth = this.dateToX(nextDate) - x;
|
|
464
|
-
if (upperText !== lastUpperText) {
|
|
465
|
-
ctx.fillStyle = "#333";
|
|
466
|
-
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
467
|
-
ctx.textRendering = "optimizeLegibility";
|
|
468
|
-
ctx.textAlign = "left";
|
|
469
|
-
ctx.fillText(upperText, x + 5, h * 0.35);
|
|
470
|
-
lastUpperText = upperText;
|
|
471
|
-
}
|
|
472
681
|
ctx.fillStyle = "#000412";
|
|
473
682
|
ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
474
683
|
ctx.textAlign = "center";
|
|
@@ -478,10 +687,10 @@ class GanttChart {
|
|
|
478
687
|
ctx.lineTo(x, h);
|
|
479
688
|
ctx.strokeStyle = "#e0e0e0";
|
|
480
689
|
ctx.stroke();
|
|
481
|
-
if (nextDate.getTime() ===
|
|
482
|
-
|
|
690
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
691
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
483
692
|
} else {
|
|
484
|
-
|
|
693
|
+
currentDateForLower = nextDate;
|
|
485
694
|
}
|
|
486
695
|
}
|
|
487
696
|
ctx.beginPath();
|
|
@@ -491,6 +700,22 @@ class GanttChart {
|
|
|
491
700
|
ctx.stroke();
|
|
492
701
|
ctx.restore();
|
|
493
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
|
+
}
|
|
494
719
|
renderMain() {
|
|
495
720
|
const ctx = this.mainCtx;
|
|
496
721
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -714,8 +939,8 @@ class GanttChart {
|
|
|
714
939
|
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
715
940
|
ctx.lineWidth = 4;
|
|
716
941
|
ctx.beginPath();
|
|
717
|
-
ctx.moveTo(pos.x_plan_start, taskY);
|
|
718
|
-
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);
|
|
719
944
|
ctx.stroke();
|
|
720
945
|
}
|
|
721
946
|
ctx.fillStyle = "#000";
|
|
@@ -751,6 +976,9 @@ class GanttChart {
|
|
|
751
976
|
return styles;
|
|
752
977
|
}
|
|
753
978
|
handleMouseMove(e) {
|
|
979
|
+
if (this.scrolling) {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
754
982
|
const rect = this.mainCanvas.getBoundingClientRect();
|
|
755
983
|
const mouseX = e.clientX - rect.left;
|
|
756
984
|
const mouseY = e.clientY - rect.top;
|
|
@@ -763,15 +991,16 @@ class GanttChart {
|
|
|
763
991
|
if (this.config.tooltipFormat) {
|
|
764
992
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
765
993
|
if (!htmlStr) {
|
|
766
|
-
|
|
994
|
+
this.handleMouseLeave();
|
|
995
|
+
return;
|
|
767
996
|
}
|
|
768
997
|
this.tooltip.innerHTML = htmlStr;
|
|
769
998
|
} else {
|
|
770
999
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
771
|
-
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);
|
|
772
1001
|
if (date >= pStart && date < pEnd) return true;
|
|
773
1002
|
if (task.actualStart) {
|
|
774
|
-
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);
|
|
775
1004
|
if (date >= aStart && date < aEnd) return true;
|
|
776
1005
|
}
|
|
777
1006
|
return false;
|
|
@@ -785,6 +1014,7 @@ class GanttChart {
|
|
|
785
1014
|
this.tooltip.innerHTML = html;
|
|
786
1015
|
}
|
|
787
1016
|
this.tooltip.style.display = "block";
|
|
1017
|
+
this.showTooltip = true;
|
|
788
1018
|
if (this.config.tooltipColor === "white") {
|
|
789
1019
|
this.tooltip.style.background = "#fff";
|
|
790
1020
|
this.tooltip.style.color = "#000";
|
|
@@ -814,6 +1044,7 @@ class GanttChart {
|
|
|
814
1044
|
}
|
|
815
1045
|
handleMouseLeave() {
|
|
816
1046
|
this.tooltip.style.display = "none";
|
|
1047
|
+
this.showTooltip = false;
|
|
817
1048
|
}
|
|
818
1049
|
/**
|
|
819
1050
|
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
@@ -825,17 +1056,29 @@ class GanttChart {
|
|
|
825
1056
|
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
826
1057
|
}
|
|
827
1058
|
/**
|
|
828
|
-
* scroll to specified date position, default to minDate
|
|
1059
|
+
* horizontal scroll to specified date position, default to minDate
|
|
829
1060
|
*
|
|
830
1061
|
* @param date
|
|
831
1062
|
*/
|
|
832
|
-
|
|
1063
|
+
horizontalScrollTo(date) {
|
|
833
1064
|
const startDate = date ? date : this.minDate;
|
|
834
1065
|
if (startDate) {
|
|
835
1066
|
const xPosition = this.dateToX(startDate);
|
|
836
1067
|
this.container.scrollTo({ left: xPosition - 80 });
|
|
837
1068
|
}
|
|
838
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
|
+
}
|
|
839
1082
|
}
|
|
840
1083
|
export {
|
|
841
1084
|
DateUtils,
|