gantt-canvas-chart 1.2.0 → 1.3.1
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 +344 -86
- package/dist/index.css +2 -2
- package/dist/index.d.ts +44 -9
- package/dist/index.es.js +344 -86
- package/dist/index.umd.js +344 -86
- 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.1
|
|
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,12 @@ 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);
|
|
197
|
+
this.handleResize = this.handleResize.bind(this);
|
|
184
198
|
this.init();
|
|
185
199
|
}
|
|
186
200
|
init() {
|
|
@@ -190,6 +204,11 @@ class GanttChart {
|
|
|
190
204
|
this.setupEvents();
|
|
191
205
|
this.handleResize();
|
|
192
206
|
}
|
|
207
|
+
updateLoadMoreConf() {
|
|
208
|
+
this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
|
|
209
|
+
this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
|
|
210
|
+
this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
|
|
211
|
+
}
|
|
193
212
|
buildTaskMap() {
|
|
194
213
|
this.taskMap.clear();
|
|
195
214
|
this.data.forEach((row, rowIndex) => {
|
|
@@ -197,8 +216,7 @@ class GanttChart {
|
|
|
197
216
|
});
|
|
198
217
|
}
|
|
199
218
|
setupEvents() {
|
|
200
|
-
this.container.addEventListener("scroll", this.
|
|
201
|
-
this.handleResize = this.handleResize.bind(this);
|
|
219
|
+
this.container.addEventListener("scroll", this.handleScroll);
|
|
202
220
|
if (window.ResizeObserver) {
|
|
203
221
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
204
222
|
setTimeout(() => {
|
|
@@ -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,16 @@ 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);
|
|
261
|
+
this.data = [];
|
|
262
|
+
this.taskMap.clear();
|
|
263
|
+
this.taskPositions.clear();
|
|
242
264
|
this.container.remove();
|
|
243
265
|
}
|
|
244
266
|
calculateFullTimeline() {
|
|
245
|
-
const currentYear =
|
|
267
|
+
const currentYear = this.today.getFullYear();
|
|
246
268
|
let minDate = new Date(9999, 0, 1);
|
|
247
269
|
let maxDate = new Date(1e3, 0, 1);
|
|
248
270
|
if (this.data.length === 0) {
|
|
@@ -306,7 +328,14 @@ class GanttChart {
|
|
|
306
328
|
this.updateDimensions();
|
|
307
329
|
this.render();
|
|
308
330
|
}
|
|
331
|
+
// Add this method to register the data loading callback
|
|
332
|
+
setOnDataLoadCallback(callback) {
|
|
333
|
+
this.onDataLoad = callback;
|
|
334
|
+
}
|
|
309
335
|
handleScroll(e) {
|
|
336
|
+
if (this.showTooltip) {
|
|
337
|
+
this.handleMouseLeave();
|
|
338
|
+
}
|
|
310
339
|
const target = e.target;
|
|
311
340
|
this.scrollLeft = target.scrollLeft;
|
|
312
341
|
this.scrollTop = target.scrollTop;
|
|
@@ -314,7 +343,131 @@ class GanttChart {
|
|
|
314
343
|
detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
|
|
315
344
|
});
|
|
316
345
|
this.container.dispatchEvent(event);
|
|
317
|
-
|
|
346
|
+
if (this.config.enabledLoadMore.length > 0) {
|
|
347
|
+
if (this.scrollLoadTimer !== null) {
|
|
348
|
+
clearTimeout(this.scrollLoadTimer);
|
|
349
|
+
this.scrollLoadTimer = null;
|
|
350
|
+
}
|
|
351
|
+
this.scrollLoadTimer = window.setTimeout(() => {
|
|
352
|
+
this.checkScrollLoad();
|
|
353
|
+
this.scrollLoadTimer = null;
|
|
354
|
+
}, 100);
|
|
355
|
+
}
|
|
356
|
+
requestAnimationFrame(() => {
|
|
357
|
+
this.scrolling = true;
|
|
358
|
+
this.render(true);
|
|
359
|
+
this.scrolling = false;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// checkScrollLoad method
|
|
363
|
+
async checkScrollLoad() {
|
|
364
|
+
if (this.isLoadingData || !this.onDataLoad) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const scrollLeft = this.scrollLeft;
|
|
368
|
+
const scrollTop = this.scrollTop;
|
|
369
|
+
const viewportWidth = this.viewportWidth;
|
|
370
|
+
const viewportHeight = this.viewportHeight;
|
|
371
|
+
const totalWidth = this.totalWidth;
|
|
372
|
+
const totalHeight = this.totalHeight;
|
|
373
|
+
const atLeftEdge = scrollLeft <= 5;
|
|
374
|
+
const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
|
|
375
|
+
const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
|
|
376
|
+
try {
|
|
377
|
+
if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
|
|
378
|
+
await this.loadMoreData("left");
|
|
379
|
+
} else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
|
|
380
|
+
await this.loadMoreData("right");
|
|
381
|
+
} else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
|
|
382
|
+
await this.loadMoreData("bottom");
|
|
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,19 +506,41 @@ 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;
|
|
511
|
+
let offset_x_plan_start = NaN, offset_x_plan_end = NaN;
|
|
512
|
+
let offset_x_actual_start = null, offset_x_actual_end = null;
|
|
513
|
+
let x_plan_width = 0;
|
|
514
|
+
let x_actual_width = 0;
|
|
515
|
+
const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
|
|
516
|
+
const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
|
|
517
|
+
if (x_plan_start && x_plan_end) {
|
|
518
|
+
x_plan_width = x_plan_end - x_plan_start;
|
|
519
|
+
offset_x_plan_start = x_plan_start + x_plan_width * offsetX;
|
|
520
|
+
x_plan_end && (offset_x_plan_end = offset_x_plan_start + x_plan_width * percent_plan);
|
|
521
|
+
}
|
|
354
522
|
if (task.actualStart) {
|
|
355
523
|
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
356
524
|
}
|
|
357
525
|
if (task.actualEnd) {
|
|
358
|
-
x_actual_end = this.dateToX(DateUtils.addDays(
|
|
526
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
527
|
+
}
|
|
528
|
+
if (x_actual_start) {
|
|
529
|
+
x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
|
|
530
|
+
offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
|
|
531
|
+
x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
|
|
359
532
|
}
|
|
360
533
|
this.taskPositions.set(task.id, {
|
|
361
534
|
x_plan_start,
|
|
362
535
|
x_plan_end,
|
|
363
536
|
x_actual_start,
|
|
364
537
|
x_actual_end,
|
|
538
|
+
offset_x_plan_start,
|
|
539
|
+
offset_x_plan_end,
|
|
540
|
+
offset_x_actual_start,
|
|
541
|
+
offset_x_actual_end,
|
|
542
|
+
x_plan_width,
|
|
543
|
+
x_actual_width,
|
|
365
544
|
y: y + this.config.rowHeight * 0.5,
|
|
366
545
|
row: i
|
|
367
546
|
});
|
|
@@ -381,9 +560,11 @@ class GanttChart {
|
|
|
381
560
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
382
561
|
}
|
|
383
562
|
}
|
|
384
|
-
render() {
|
|
563
|
+
render(scrolling = false) {
|
|
385
564
|
this.updateVirtualRanges();
|
|
386
|
-
|
|
565
|
+
if (!scrolling) {
|
|
566
|
+
this.calculateAllTaskPositions();
|
|
567
|
+
}
|
|
387
568
|
this.renderHeader();
|
|
388
569
|
this.renderMain();
|
|
389
570
|
}
|
|
@@ -393,82 +574,132 @@ class GanttChart {
|
|
|
393
574
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
394
575
|
ctx.save();
|
|
395
576
|
ctx.translate(-this.scrollLeft, 0);
|
|
396
|
-
ctx.fillStyle =
|
|
577
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
397
578
|
ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
|
|
398
579
|
ctx.textBaseline = "middle";
|
|
399
580
|
ctx.textRendering = "optimizeLegibility";
|
|
400
581
|
let currentDate = new Date(this.visibleDateRange.start);
|
|
401
582
|
currentDate = this.getIterationStartDate(currentDate);
|
|
402
|
-
|
|
403
|
-
|
|
583
|
+
const visibleBlocks = [];
|
|
584
|
+
let calcDate = new Date(currentDate);
|
|
585
|
+
while (calcDate <= this.visibleDateRange.end) {
|
|
586
|
+
let nextDate;
|
|
587
|
+
let upperText = "";
|
|
588
|
+
switch (this.config.viewMode) {
|
|
589
|
+
case "Day":
|
|
590
|
+
upperText = DateUtils.format(calcDate, "yyyy年MM月");
|
|
591
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
592
|
+
break;
|
|
593
|
+
case "Week":
|
|
594
|
+
const weekStart = DateUtils.getStartOfWeek(calcDate);
|
|
595
|
+
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
596
|
+
nextDate = DateUtils.addDays(weekStart, 7);
|
|
597
|
+
break;
|
|
598
|
+
case "Month":
|
|
599
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
600
|
+
nextDate = DateUtils.addMonths(calcDate, 1);
|
|
601
|
+
break;
|
|
602
|
+
case "Year":
|
|
603
|
+
if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
|
|
604
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
605
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
606
|
+
} else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
|
|
607
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
608
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
609
|
+
} else {
|
|
610
|
+
calcDate = DateUtils.addDays(calcDate, 1);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
default:
|
|
615
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
const startX = this.dateToX(calcDate);
|
|
619
|
+
const endX = this.dateToX(nextDate);
|
|
620
|
+
visibleBlocks.push({
|
|
621
|
+
startX,
|
|
622
|
+
endX,
|
|
623
|
+
text: upperText,
|
|
624
|
+
yPos: h * 0.35
|
|
625
|
+
});
|
|
626
|
+
calcDate = nextDate;
|
|
627
|
+
}
|
|
628
|
+
let currentDateForLower = new Date(currentDate);
|
|
629
|
+
while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
|
|
404
630
|
let nextDate;
|
|
405
631
|
switch (this.config.viewMode) {
|
|
406
632
|
case "Day":
|
|
407
|
-
nextDate = DateUtils.addDays(
|
|
633
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
408
634
|
break;
|
|
409
635
|
case "Week":
|
|
410
|
-
nextDate = DateUtils.addDays(
|
|
636
|
+
nextDate = DateUtils.addDays(currentDateForLower, 7);
|
|
411
637
|
break;
|
|
412
638
|
case "Month":
|
|
413
|
-
nextDate = DateUtils.addMonths(
|
|
639
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
414
640
|
break;
|
|
415
641
|
case "Year":
|
|
416
|
-
nextDate = DateUtils.addMonths(
|
|
642
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
417
643
|
break;
|
|
418
644
|
default:
|
|
419
|
-
nextDate = DateUtils.addDays(
|
|
645
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
420
646
|
break;
|
|
421
647
|
}
|
|
422
|
-
if (nextDate.getTime() ===
|
|
423
|
-
|
|
648
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
649
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
424
650
|
} else {
|
|
425
|
-
|
|
651
|
+
currentDateForLower = nextDate;
|
|
426
652
|
}
|
|
427
653
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
654
|
+
const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
|
|
655
|
+
ctx.fillStyle = "#333";
|
|
656
|
+
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
657
|
+
ctx.textAlign = "left";
|
|
658
|
+
groupedBlocks.forEach((group) => {
|
|
659
|
+
const visibleStart = Math.max(group.startX, this.scrollLeft);
|
|
660
|
+
const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
|
|
661
|
+
if (visibleEnd > visibleStart) {
|
|
662
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
663
|
+
ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
|
|
664
|
+
ctx.fillStyle = "#333";
|
|
665
|
+
ctx.fillText(group.text, visibleStart + 5, group.yPos);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
while (currentDateForLower <= this.visibleDateRange.end) {
|
|
669
|
+
const x = this.dateToX(currentDateForLower);
|
|
670
|
+
let lowerText = "";
|
|
671
|
+
let nextDate;
|
|
431
672
|
switch (this.config.viewMode) {
|
|
432
673
|
case "Day":
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
674
|
+
lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
|
|
675
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
436
676
|
break;
|
|
437
677
|
case "Week":
|
|
438
|
-
const weekStart = DateUtils.getStartOfWeek(
|
|
439
|
-
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
678
|
+
const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
|
|
440
679
|
lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
|
|
441
680
|
nextDate = DateUtils.addDays(weekStart, 7);
|
|
442
681
|
break;
|
|
443
682
|
case "Month":
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
nextDate = DateUtils.addMonths(currentDate, 1);
|
|
683
|
+
lowerText = `${currentDateForLower.getMonth() + 1}月`;
|
|
684
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
447
685
|
break;
|
|
448
686
|
case "Year":
|
|
449
|
-
if (
|
|
450
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
687
|
+
if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
|
|
451
688
|
lowerText = `上半年`;
|
|
452
|
-
nextDate = DateUtils.addMonths(
|
|
453
|
-
} else if (
|
|
454
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
689
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
690
|
+
} else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
|
|
455
691
|
lowerText = `下半年`;
|
|
456
|
-
nextDate = DateUtils.addMonths(
|
|
692
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
457
693
|
} else {
|
|
458
|
-
|
|
694
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
459
695
|
continue;
|
|
460
696
|
}
|
|
461
697
|
break;
|
|
698
|
+
default:
|
|
699
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
700
|
+
break;
|
|
462
701
|
}
|
|
463
702
|
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
703
|
ctx.fillStyle = "#000412";
|
|
473
704
|
ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
474
705
|
ctx.textAlign = "center";
|
|
@@ -478,10 +709,10 @@ class GanttChart {
|
|
|
478
709
|
ctx.lineTo(x, h);
|
|
479
710
|
ctx.strokeStyle = "#e0e0e0";
|
|
480
711
|
ctx.stroke();
|
|
481
|
-
if (nextDate.getTime() ===
|
|
482
|
-
|
|
712
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
713
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
483
714
|
} else {
|
|
484
|
-
|
|
715
|
+
currentDateForLower = nextDate;
|
|
485
716
|
}
|
|
486
717
|
}
|
|
487
718
|
ctx.beginPath();
|
|
@@ -491,6 +722,22 @@ class GanttChart {
|
|
|
491
722
|
ctx.stroke();
|
|
492
723
|
ctx.restore();
|
|
493
724
|
}
|
|
725
|
+
// Helper method to group consecutive blocks with same text
|
|
726
|
+
groupConsecutiveBlocks(blocks) {
|
|
727
|
+
if (blocks.length === 0) return [];
|
|
728
|
+
const grouped = [];
|
|
729
|
+
let currentGroup = { ...blocks[0] };
|
|
730
|
+
for (let i = 1; i < blocks.length; i++) {
|
|
731
|
+
if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
|
|
732
|
+
currentGroup.endX = blocks[i].endX;
|
|
733
|
+
} else {
|
|
734
|
+
grouped.push(currentGroup);
|
|
735
|
+
currentGroup = { ...blocks[i] };
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
grouped.push(currentGroup);
|
|
739
|
+
return grouped;
|
|
740
|
+
}
|
|
494
741
|
renderMain() {
|
|
495
742
|
const ctx = this.mainCtx;
|
|
496
743
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -538,9 +785,9 @@ class GanttChart {
|
|
|
538
785
|
if (!fromPos) return;
|
|
539
786
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
540
787
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
541
|
-
const fromX = Math.max(fromPos.
|
|
788
|
+
const fromX = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
|
|
542
789
|
const fromY = fromPos.y;
|
|
543
|
-
const toX = Math.min(toPos.
|
|
790
|
+
const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
|
|
544
791
|
const toY = toPos.y;
|
|
545
792
|
ctx.beginPath();
|
|
546
793
|
if (isAdjacent) {
|
|
@@ -695,42 +942,35 @@ class GanttChart {
|
|
|
695
942
|
}
|
|
696
943
|
drawTask(ctx, task, y, pos) {
|
|
697
944
|
const offset = 4;
|
|
698
|
-
const width = pos.x_plan_end - pos.x_plan_start;
|
|
699
945
|
const taskY = y + this.config.rowHeight * 0.15 + offset;
|
|
700
946
|
const taskHeight = this.config.rowHeight * 0.7 - offset;
|
|
701
947
|
const textY = y + this.config.rowHeight / 2 + offset;
|
|
702
948
|
const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
|
|
703
|
-
const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
|
|
704
949
|
if (this.config.showActual && pos.x_actual_start) {
|
|
705
950
|
ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
|
|
706
|
-
|
|
707
|
-
pos.x_actual_start += aWidth * offsetX_actual;
|
|
708
|
-
pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
|
|
709
|
-
ctx.fillRect(Math.round(pos.x_actual_start), Math.round(taskY + 2), Math.round(aWidth * percent_actual), Math.round(taskHeight - 2));
|
|
951
|
+
ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
|
|
710
952
|
}
|
|
711
953
|
if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
|
|
712
954
|
ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
|
|
713
|
-
pos.x_plan_start += width * offsetX;
|
|
714
|
-
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
715
955
|
ctx.lineWidth = 4;
|
|
716
956
|
ctx.beginPath();
|
|
717
|
-
ctx.moveTo(pos.
|
|
718
|
-
ctx.lineTo(pos.
|
|
957
|
+
ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
|
|
958
|
+
ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
|
|
719
959
|
ctx.stroke();
|
|
720
960
|
}
|
|
721
961
|
ctx.fillStyle = "#000";
|
|
722
962
|
if (this.config.showLeftRemark && task.leftRemark) {
|
|
723
963
|
ctx.textAlign = "right";
|
|
724
|
-
ctx.fillText(task.leftRemark, Math.round(Math.min(...[pos.
|
|
964
|
+
ctx.fillText(task.leftRemark, Math.round(Math.min(...[pos.offset_x_plan_start, pos.offset_x_actual_start].filter((val) => val !== null && val !== void 0)) - 8), Math.round(textY));
|
|
725
965
|
}
|
|
726
966
|
if (this.config.showRightRemark && task.rightRemark) {
|
|
727
967
|
ctx.textAlign = "left";
|
|
728
|
-
ctx.fillText(task.rightRemark, Math.round(Math.max(...[pos.
|
|
968
|
+
ctx.fillText(task.rightRemark, Math.round(Math.max(...[pos.offset_x_plan_end, pos.offset_x_actual_end].filter((val) => val !== null && val !== void 0)) + 8), Math.round(textY));
|
|
729
969
|
}
|
|
730
970
|
if (this.config.showCenterRemark && task.centerRemark) {
|
|
731
|
-
const centerX = pos.
|
|
971
|
+
const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
|
|
732
972
|
ctx.textAlign = "center";
|
|
733
|
-
ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.
|
|
973
|
+
ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.offset_x_actual_end - pos.offset_x_actual_start));
|
|
734
974
|
}
|
|
735
975
|
}
|
|
736
976
|
getTaskStyles(task) {
|
|
@@ -751,6 +991,9 @@ class GanttChart {
|
|
|
751
991
|
return styles;
|
|
752
992
|
}
|
|
753
993
|
handleMouseMove(e) {
|
|
994
|
+
if (this.scrolling) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
754
997
|
const rect = this.mainCanvas.getBoundingClientRect();
|
|
755
998
|
const mouseX = e.clientX - rect.left;
|
|
756
999
|
const mouseY = e.clientY - rect.top;
|
|
@@ -763,15 +1006,16 @@ class GanttChart {
|
|
|
763
1006
|
if (this.config.tooltipFormat) {
|
|
764
1007
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
765
1008
|
if (!htmlStr) {
|
|
766
|
-
|
|
1009
|
+
this.handleMouseLeave();
|
|
1010
|
+
return;
|
|
767
1011
|
}
|
|
768
1012
|
this.tooltip.innerHTML = htmlStr;
|
|
769
1013
|
} else {
|
|
770
1014
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
771
|
-
const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(
|
|
1015
|
+
const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
|
|
772
1016
|
if (date >= pStart && date < pEnd) return true;
|
|
773
1017
|
if (task.actualStart) {
|
|
774
|
-
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(
|
|
1018
|
+
const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
|
|
775
1019
|
if (date >= aStart && date < aEnd) return true;
|
|
776
1020
|
}
|
|
777
1021
|
return false;
|
|
@@ -785,6 +1029,7 @@ class GanttChart {
|
|
|
785
1029
|
this.tooltip.innerHTML = html;
|
|
786
1030
|
}
|
|
787
1031
|
this.tooltip.style.display = "block";
|
|
1032
|
+
this.showTooltip = true;
|
|
788
1033
|
if (this.config.tooltipColor === "white") {
|
|
789
1034
|
this.tooltip.style.background = "#fff";
|
|
790
1035
|
this.tooltip.style.color = "#000";
|
|
@@ -814,6 +1059,7 @@ class GanttChart {
|
|
|
814
1059
|
}
|
|
815
1060
|
handleMouseLeave() {
|
|
816
1061
|
this.tooltip.style.display = "none";
|
|
1062
|
+
this.showTooltip = false;
|
|
817
1063
|
}
|
|
818
1064
|
/**
|
|
819
1065
|
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
@@ -825,17 +1071,29 @@ class GanttChart {
|
|
|
825
1071
|
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
826
1072
|
}
|
|
827
1073
|
/**
|
|
828
|
-
* scroll to specified date position, default to minDate
|
|
1074
|
+
* horizontal scroll to specified date position, default to minDate
|
|
829
1075
|
*
|
|
830
1076
|
* @param date
|
|
831
1077
|
*/
|
|
832
|
-
|
|
1078
|
+
horizontalScrollTo(date) {
|
|
833
1079
|
const startDate = date ? date : this.minDate;
|
|
834
1080
|
if (startDate) {
|
|
835
1081
|
const xPosition = this.dateToX(startDate);
|
|
836
1082
|
this.container.scrollTo({ left: xPosition - 80 });
|
|
837
1083
|
}
|
|
838
1084
|
}
|
|
1085
|
+
/**
|
|
1086
|
+
* vertical scroll to specified position
|
|
1087
|
+
*
|
|
1088
|
+
* @param params
|
|
1089
|
+
*/
|
|
1090
|
+
verticalScrollTo(params) {
|
|
1091
|
+
if (params && (params.rowId || params.rowIndex)) {
|
|
1092
|
+
const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
|
|
1093
|
+
const yPosition = this.config.rowHeight * rowIndex;
|
|
1094
|
+
this.container.scrollTo({ top: yPosition - 80 });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
839
1097
|
}
|
|
840
1098
|
export {
|
|
841
1099
|
DateUtils,
|