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.cjs.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
|
*/
|
|
@@ -91,6 +91,8 @@ class GanttChart {
|
|
|
91
91
|
mainCanvas;
|
|
92
92
|
scrollDummy;
|
|
93
93
|
tooltip;
|
|
94
|
+
scrolling;
|
|
95
|
+
showTooltip;
|
|
94
96
|
headerCtx;
|
|
95
97
|
mainCtx;
|
|
96
98
|
timelineStart;
|
|
@@ -110,9 +112,14 @@ class GanttChart {
|
|
|
110
112
|
resizeObserver;
|
|
111
113
|
taskPositions;
|
|
112
114
|
taskMap;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
isLoadingData = false;
|
|
116
|
+
hasMoreDataLeft = true;
|
|
117
|
+
hasMoreDataRight = true;
|
|
118
|
+
hasMoreDataBottom = true;
|
|
119
|
+
lastScrollLeft = 0;
|
|
120
|
+
lastScrollTop = 0;
|
|
121
|
+
onDataLoad = null;
|
|
122
|
+
scrollLoadTimer = null;
|
|
116
123
|
constructor(rootContainer, data, config = {}) {
|
|
117
124
|
const container = document.createElement("div");
|
|
118
125
|
const scrollEl = document.createElement("div");
|
|
@@ -131,6 +138,8 @@ class GanttChart {
|
|
|
131
138
|
this.rootContainer = rootContainer;
|
|
132
139
|
this.container = container;
|
|
133
140
|
this.data = data;
|
|
141
|
+
this.scrolling = false;
|
|
142
|
+
this.showTooltip = false;
|
|
134
143
|
this.config = {
|
|
135
144
|
viewMode: "Month",
|
|
136
145
|
rowHeight: 48,
|
|
@@ -148,11 +157,14 @@ class GanttChart {
|
|
|
148
157
|
todayColor: "#ff4d4f",
|
|
149
158
|
offsetTop: 0,
|
|
150
159
|
offsetLeft: 0,
|
|
160
|
+
enabledLoadMore: [],
|
|
151
161
|
viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
|
|
152
162
|
planBorderColor: "#C1EFCF",
|
|
153
163
|
actualBgColor: "#5AC989",
|
|
164
|
+
headerBgColor: "#f9f9f9",
|
|
154
165
|
...config
|
|
155
166
|
};
|
|
167
|
+
this.updateLoadMoreConf();
|
|
156
168
|
this.headerCanvas = headerCanvas;
|
|
157
169
|
this.mainCanvas = mainCanvas;
|
|
158
170
|
this.scrollDummy = scrollEl;
|
|
@@ -179,10 +191,12 @@ class GanttChart {
|
|
|
179
191
|
this.totalHeight = 0;
|
|
180
192
|
this.taskPositions = /* @__PURE__ */ new Map();
|
|
181
193
|
this.taskMap = /* @__PURE__ */ new Map();
|
|
182
|
-
this.
|
|
183
|
-
this.
|
|
184
|
-
this.
|
|
185
|
-
this.
|
|
194
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
195
|
+
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
|
196
|
+
this.handleScroll = this.handleScroll.bind(this);
|
|
197
|
+
this.horizontalScrollTo = this.horizontalScrollTo.bind(this);
|
|
198
|
+
this.verticalScrollTo = this.verticalScrollTo.bind(this);
|
|
199
|
+
this.handleResize = this.handleResize.bind(this);
|
|
186
200
|
this.init();
|
|
187
201
|
}
|
|
188
202
|
init() {
|
|
@@ -192,6 +206,11 @@ class GanttChart {
|
|
|
192
206
|
this.setupEvents();
|
|
193
207
|
this.handleResize();
|
|
194
208
|
}
|
|
209
|
+
updateLoadMoreConf() {
|
|
210
|
+
this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
|
|
211
|
+
this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
|
|
212
|
+
this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
|
|
213
|
+
}
|
|
195
214
|
buildTaskMap() {
|
|
196
215
|
this.taskMap.clear();
|
|
197
216
|
this.data.forEach((row, rowIndex) => {
|
|
@@ -199,8 +218,7 @@ class GanttChart {
|
|
|
199
218
|
});
|
|
200
219
|
}
|
|
201
220
|
setupEvents() {
|
|
202
|
-
this.container.addEventListener("scroll", this.
|
|
203
|
-
this.handleResize = this.handleResize.bind(this);
|
|
221
|
+
this.container.addEventListener("scroll", this.handleScroll);
|
|
204
222
|
if (window.ResizeObserver) {
|
|
205
223
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
206
224
|
setTimeout(() => {
|
|
@@ -208,8 +226,8 @@ class GanttChart {
|
|
|
208
226
|
}, 100);
|
|
209
227
|
}
|
|
210
228
|
if (this.config.showTooltip) {
|
|
211
|
-
this.mainCanvas.addEventListener("mousemove", this.
|
|
212
|
-
this.mainCanvas.addEventListener("mouseleave", this.
|
|
229
|
+
this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
|
|
230
|
+
this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
|
|
213
231
|
}
|
|
214
232
|
}
|
|
215
233
|
updateConfig(newConfig) {
|
|
@@ -220,6 +238,7 @@ class GanttChart {
|
|
|
220
238
|
this.updatePixelsPerDay();
|
|
221
239
|
this.calculateFullTimeline();
|
|
222
240
|
}
|
|
241
|
+
this.updateLoadMoreConf();
|
|
223
242
|
this.updateDimensions();
|
|
224
243
|
this.render();
|
|
225
244
|
}
|
|
@@ -238,13 +257,16 @@ class GanttChart {
|
|
|
238
257
|
if (this.resizeObserver) {
|
|
239
258
|
this.resizeObserver.disconnect();
|
|
240
259
|
}
|
|
241
|
-
this.container.removeEventListener("scroll", this.
|
|
242
|
-
this.mainCanvas.removeEventListener("mousemove", this.
|
|
243
|
-
this.mainCanvas.removeEventListener("mouseleave", this.
|
|
260
|
+
this.container.removeEventListener("scroll", this.handleScroll);
|
|
261
|
+
this.mainCanvas.removeEventListener("mousemove", this.handleMouseMove);
|
|
262
|
+
this.mainCanvas.removeEventListener("mouseleave", this.handleMouseLeave);
|
|
263
|
+
this.data = [];
|
|
264
|
+
this.taskMap.clear();
|
|
265
|
+
this.taskPositions.clear();
|
|
244
266
|
this.container.remove();
|
|
245
267
|
}
|
|
246
268
|
calculateFullTimeline() {
|
|
247
|
-
const currentYear =
|
|
269
|
+
const currentYear = this.today.getFullYear();
|
|
248
270
|
let minDate = new Date(9999, 0, 1);
|
|
249
271
|
let maxDate = new Date(1e3, 0, 1);
|
|
250
272
|
if (this.data.length === 0) {
|
|
@@ -308,7 +330,14 @@ class GanttChart {
|
|
|
308
330
|
this.updateDimensions();
|
|
309
331
|
this.render();
|
|
310
332
|
}
|
|
333
|
+
// Add this method to register the data loading callback
|
|
334
|
+
setOnDataLoadCallback(callback) {
|
|
335
|
+
this.onDataLoad = callback;
|
|
336
|
+
}
|
|
311
337
|
handleScroll(e) {
|
|
338
|
+
if (this.showTooltip) {
|
|
339
|
+
this.handleMouseLeave();
|
|
340
|
+
}
|
|
312
341
|
const target = e.target;
|
|
313
342
|
this.scrollLeft = target.scrollLeft;
|
|
314
343
|
this.scrollTop = target.scrollTop;
|
|
@@ -316,7 +345,131 @@ class GanttChart {
|
|
|
316
345
|
detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
|
|
317
346
|
});
|
|
318
347
|
this.container.dispatchEvent(event);
|
|
319
|
-
|
|
348
|
+
if (this.config.enabledLoadMore.length > 0) {
|
|
349
|
+
if (this.scrollLoadTimer !== null) {
|
|
350
|
+
clearTimeout(this.scrollLoadTimer);
|
|
351
|
+
this.scrollLoadTimer = null;
|
|
352
|
+
}
|
|
353
|
+
this.scrollLoadTimer = window.setTimeout(() => {
|
|
354
|
+
this.checkScrollLoad();
|
|
355
|
+
this.scrollLoadTimer = null;
|
|
356
|
+
}, 100);
|
|
357
|
+
}
|
|
358
|
+
requestAnimationFrame(() => {
|
|
359
|
+
this.scrolling = true;
|
|
360
|
+
this.render(true);
|
|
361
|
+
this.scrolling = false;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
// checkScrollLoad method
|
|
365
|
+
async checkScrollLoad() {
|
|
366
|
+
if (this.isLoadingData || !this.onDataLoad) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const scrollLeft = this.scrollLeft;
|
|
370
|
+
const scrollTop = this.scrollTop;
|
|
371
|
+
const viewportWidth = this.viewportWidth;
|
|
372
|
+
const viewportHeight = this.viewportHeight;
|
|
373
|
+
const totalWidth = this.totalWidth;
|
|
374
|
+
const totalHeight = this.totalHeight;
|
|
375
|
+
const atLeftEdge = scrollLeft <= 5;
|
|
376
|
+
const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
|
|
377
|
+
const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
|
|
378
|
+
try {
|
|
379
|
+
if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
|
|
380
|
+
await this.loadMoreData("left");
|
|
381
|
+
} else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
|
|
382
|
+
await this.loadMoreData("right");
|
|
383
|
+
} else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
|
|
384
|
+
await this.loadMoreData("bottom");
|
|
385
|
+
}
|
|
386
|
+
} finally {
|
|
387
|
+
this.lastScrollLeft = scrollLeft;
|
|
388
|
+
this.lastScrollTop = scrollTop;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Add this method to reset scroll loading state
|
|
392
|
+
resetScrollLoadingState() {
|
|
393
|
+
this.hasMoreDataLeft = true;
|
|
394
|
+
this.hasMoreDataRight = true;
|
|
395
|
+
this.hasMoreDataBottom = true;
|
|
396
|
+
this.lastScrollLeft = 0;
|
|
397
|
+
this.lastScrollTop = 0;
|
|
398
|
+
if (this.scrollLoadTimer !== null) {
|
|
399
|
+
clearTimeout(this.scrollLoadTimer);
|
|
400
|
+
this.scrollLoadTimer = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Add this new method to load additional data
|
|
404
|
+
async loadMoreData(direction) {
|
|
405
|
+
if (this.isLoadingData || !this.onDataLoad) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
this.isLoadingData = true;
|
|
409
|
+
try {
|
|
410
|
+
let newData = null;
|
|
411
|
+
switch (direction) {
|
|
412
|
+
case "left":
|
|
413
|
+
newData = await this.onDataLoad("left", { date: this.minDate });
|
|
414
|
+
if (newData && newData.length > 0) {
|
|
415
|
+
this.prependData(newData);
|
|
416
|
+
} else {
|
|
417
|
+
this.hasMoreDataLeft = false;
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
case "right":
|
|
421
|
+
newData = await this.onDataLoad("right", { date: this.maxDate });
|
|
422
|
+
if (newData && newData.length > 0) {
|
|
423
|
+
this.appendData(newData);
|
|
424
|
+
} else {
|
|
425
|
+
this.hasMoreDataRight = false;
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
case "bottom":
|
|
429
|
+
const currentRowCount = this.data.length;
|
|
430
|
+
newData = await this.onDataLoad("bottom", { offset: currentRowCount });
|
|
431
|
+
if (newData && newData.length > 0) {
|
|
432
|
+
this.appendRows(newData);
|
|
433
|
+
} else {
|
|
434
|
+
this.hasMoreDataBottom = false;
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
if (newData && newData.length > 0) {
|
|
439
|
+
this.buildTaskMap();
|
|
440
|
+
this.calculateFullTimeline();
|
|
441
|
+
this.updateDimensions();
|
|
442
|
+
this.render();
|
|
443
|
+
}
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.error("Error loading additional data:", error);
|
|
446
|
+
} finally {
|
|
447
|
+
this.isLoadingData = false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Add this method to append data to the right
|
|
451
|
+
appendData(newData) {
|
|
452
|
+
newData.forEach((newRow, index) => {
|
|
453
|
+
if (index < this.data.length) {
|
|
454
|
+
this.data[index].tasks = [...this.data[index].tasks, ...newRow.tasks];
|
|
455
|
+
} else {
|
|
456
|
+
this.data.push(newRow);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
// Add this method to prepend data to the left
|
|
461
|
+
prependData(newData) {
|
|
462
|
+
newData.forEach((newRow, index) => {
|
|
463
|
+
if (index < this.data.length) {
|
|
464
|
+
this.data[index].tasks = [...newRow.tasks, ...this.data[index].tasks];
|
|
465
|
+
} else {
|
|
466
|
+
this.data.push(newRow);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
// Add this method to append rows to the bottom
|
|
471
|
+
appendRows(newData) {
|
|
472
|
+
this.data.push(...newData);
|
|
320
473
|
}
|
|
321
474
|
setScrollTop(scrollTop) {
|
|
322
475
|
if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
|
|
@@ -330,10 +483,14 @@ class GanttChart {
|
|
|
330
483
|
}
|
|
331
484
|
updateDimensions() {
|
|
332
485
|
const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
this.
|
|
336
|
-
|
|
486
|
+
const newTotalWidth = totalDays * this.pixelsPerDay;
|
|
487
|
+
const newTotalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
|
|
488
|
+
if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
|
|
489
|
+
this.totalWidth = newTotalWidth;
|
|
490
|
+
this.totalHeight = newTotalHeight;
|
|
491
|
+
this.scrollDummy.style.width = `${this.totalWidth}px`;
|
|
492
|
+
this.scrollDummy.style.height = `${this.totalHeight}px`;
|
|
493
|
+
}
|
|
337
494
|
}
|
|
338
495
|
setupCanvas(canvas, width, height) {
|
|
339
496
|
canvas.width = width * this.devicePixelRatio;
|
|
@@ -351,19 +508,41 @@ class GanttChart {
|
|
|
351
508
|
const y = i * this.config.rowHeight;
|
|
352
509
|
row.tasks.forEach((task) => {
|
|
353
510
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
354
|
-
const x_plan_end = this.dateToX(DateUtils.addDays(
|
|
511
|
+
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
355
512
|
let x_actual_start = null, x_actual_end = null;
|
|
513
|
+
let offset_x_plan_start = NaN, offset_x_plan_end = NaN;
|
|
514
|
+
let offset_x_actual_start = null, offset_x_actual_end = null;
|
|
515
|
+
let x_plan_width = 0;
|
|
516
|
+
let x_actual_width = 0;
|
|
517
|
+
const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
|
|
518
|
+
const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
|
|
519
|
+
if (x_plan_start && x_plan_end) {
|
|
520
|
+
x_plan_width = x_plan_end - x_plan_start;
|
|
521
|
+
offset_x_plan_start = x_plan_start + x_plan_width * offsetX;
|
|
522
|
+
x_plan_end && (offset_x_plan_end = offset_x_plan_start + x_plan_width * percent_plan);
|
|
523
|
+
}
|
|
356
524
|
if (task.actualStart) {
|
|
357
525
|
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
358
526
|
}
|
|
359
527
|
if (task.actualEnd) {
|
|
360
|
-
x_actual_end = this.dateToX(DateUtils.addDays(
|
|
528
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
529
|
+
}
|
|
530
|
+
if (x_actual_start) {
|
|
531
|
+
x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
|
|
532
|
+
offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
|
|
533
|
+
x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
|
|
361
534
|
}
|
|
362
535
|
this.taskPositions.set(task.id, {
|
|
363
536
|
x_plan_start,
|
|
364
537
|
x_plan_end,
|
|
365
538
|
x_actual_start,
|
|
366
539
|
x_actual_end,
|
|
540
|
+
offset_x_plan_start,
|
|
541
|
+
offset_x_plan_end,
|
|
542
|
+
offset_x_actual_start,
|
|
543
|
+
offset_x_actual_end,
|
|
544
|
+
x_plan_width,
|
|
545
|
+
x_actual_width,
|
|
367
546
|
y: y + this.config.rowHeight * 0.5,
|
|
368
547
|
row: i
|
|
369
548
|
});
|
|
@@ -383,9 +562,11 @@ class GanttChart {
|
|
|
383
562
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
384
563
|
}
|
|
385
564
|
}
|
|
386
|
-
render() {
|
|
565
|
+
render(scrolling = false) {
|
|
387
566
|
this.updateVirtualRanges();
|
|
388
|
-
|
|
567
|
+
if (!scrolling) {
|
|
568
|
+
this.calculateAllTaskPositions();
|
|
569
|
+
}
|
|
389
570
|
this.renderHeader();
|
|
390
571
|
this.renderMain();
|
|
391
572
|
}
|
|
@@ -395,82 +576,132 @@ class GanttChart {
|
|
|
395
576
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
396
577
|
ctx.save();
|
|
397
578
|
ctx.translate(-this.scrollLeft, 0);
|
|
398
|
-
ctx.fillStyle =
|
|
579
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
399
580
|
ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
|
|
400
581
|
ctx.textBaseline = "middle";
|
|
401
582
|
ctx.textRendering = "optimizeLegibility";
|
|
402
583
|
let currentDate = new Date(this.visibleDateRange.start);
|
|
403
584
|
currentDate = this.getIterationStartDate(currentDate);
|
|
404
|
-
|
|
405
|
-
|
|
585
|
+
const visibleBlocks = [];
|
|
586
|
+
let calcDate = new Date(currentDate);
|
|
587
|
+
while (calcDate <= this.visibleDateRange.end) {
|
|
588
|
+
let nextDate;
|
|
589
|
+
let upperText = "";
|
|
590
|
+
switch (this.config.viewMode) {
|
|
591
|
+
case "Day":
|
|
592
|
+
upperText = DateUtils.format(calcDate, "yyyy年MM月");
|
|
593
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
594
|
+
break;
|
|
595
|
+
case "Week":
|
|
596
|
+
const weekStart = DateUtils.getStartOfWeek(calcDate);
|
|
597
|
+
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
598
|
+
nextDate = DateUtils.addDays(weekStart, 7);
|
|
599
|
+
break;
|
|
600
|
+
case "Month":
|
|
601
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
602
|
+
nextDate = DateUtils.addMonths(calcDate, 1);
|
|
603
|
+
break;
|
|
604
|
+
case "Year":
|
|
605
|
+
if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
|
|
606
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
607
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
608
|
+
} else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
|
|
609
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
610
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
611
|
+
} else {
|
|
612
|
+
calcDate = DateUtils.addDays(calcDate, 1);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
break;
|
|
616
|
+
default:
|
|
617
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
const startX = this.dateToX(calcDate);
|
|
621
|
+
const endX = this.dateToX(nextDate);
|
|
622
|
+
visibleBlocks.push({
|
|
623
|
+
startX,
|
|
624
|
+
endX,
|
|
625
|
+
text: upperText,
|
|
626
|
+
yPos: h * 0.35
|
|
627
|
+
});
|
|
628
|
+
calcDate = nextDate;
|
|
629
|
+
}
|
|
630
|
+
let currentDateForLower = new Date(currentDate);
|
|
631
|
+
while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
|
|
406
632
|
let nextDate;
|
|
407
633
|
switch (this.config.viewMode) {
|
|
408
634
|
case "Day":
|
|
409
|
-
nextDate = DateUtils.addDays(
|
|
635
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
410
636
|
break;
|
|
411
637
|
case "Week":
|
|
412
|
-
nextDate = DateUtils.addDays(
|
|
638
|
+
nextDate = DateUtils.addDays(currentDateForLower, 7);
|
|
413
639
|
break;
|
|
414
640
|
case "Month":
|
|
415
|
-
nextDate = DateUtils.addMonths(
|
|
641
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
416
642
|
break;
|
|
417
643
|
case "Year":
|
|
418
|
-
nextDate = DateUtils.addMonths(
|
|
644
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
419
645
|
break;
|
|
420
646
|
default:
|
|
421
|
-
nextDate = DateUtils.addDays(
|
|
647
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
422
648
|
break;
|
|
423
649
|
}
|
|
424
|
-
if (nextDate.getTime() ===
|
|
425
|
-
|
|
650
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
651
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
426
652
|
} else {
|
|
427
|
-
|
|
653
|
+
currentDateForLower = nextDate;
|
|
428
654
|
}
|
|
429
655
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
656
|
+
const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
|
|
657
|
+
ctx.fillStyle = "#333";
|
|
658
|
+
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
659
|
+
ctx.textAlign = "left";
|
|
660
|
+
groupedBlocks.forEach((group) => {
|
|
661
|
+
const visibleStart = Math.max(group.startX, this.scrollLeft);
|
|
662
|
+
const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
|
|
663
|
+
if (visibleEnd > visibleStart) {
|
|
664
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
665
|
+
ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
|
|
666
|
+
ctx.fillStyle = "#333";
|
|
667
|
+
ctx.fillText(group.text, visibleStart + 5, group.yPos);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
while (currentDateForLower <= this.visibleDateRange.end) {
|
|
671
|
+
const x = this.dateToX(currentDateForLower);
|
|
672
|
+
let lowerText = "";
|
|
673
|
+
let nextDate;
|
|
433
674
|
switch (this.config.viewMode) {
|
|
434
675
|
case "Day":
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
676
|
+
lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
|
|
677
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
438
678
|
break;
|
|
439
679
|
case "Week":
|
|
440
|
-
const weekStart = DateUtils.getStartOfWeek(
|
|
441
|
-
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
680
|
+
const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
|
|
442
681
|
lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
|
|
443
682
|
nextDate = DateUtils.addDays(weekStart, 7);
|
|
444
683
|
break;
|
|
445
684
|
case "Month":
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
nextDate = DateUtils.addMonths(currentDate, 1);
|
|
685
|
+
lowerText = `${currentDateForLower.getMonth() + 1}月`;
|
|
686
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
449
687
|
break;
|
|
450
688
|
case "Year":
|
|
451
|
-
if (
|
|
452
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
689
|
+
if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
|
|
453
690
|
lowerText = `上半年`;
|
|
454
|
-
nextDate = DateUtils.addMonths(
|
|
455
|
-
} else if (
|
|
456
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
691
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
692
|
+
} else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
|
|
457
693
|
lowerText = `下半年`;
|
|
458
|
-
nextDate = DateUtils.addMonths(
|
|
694
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
459
695
|
} else {
|
|
460
|
-
|
|
696
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
461
697
|
continue;
|
|
462
698
|
}
|
|
463
699
|
break;
|
|
700
|
+
default:
|
|
701
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
702
|
+
break;
|
|
464
703
|
}
|
|
465
704
|
const unitWidth = this.dateToX(nextDate) - x;
|
|
466
|
-
if (upperText !== lastUpperText) {
|
|
467
|
-
ctx.fillStyle = "#333";
|
|
468
|
-
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
469
|
-
ctx.textRendering = "optimizeLegibility";
|
|
470
|
-
ctx.textAlign = "left";
|
|
471
|
-
ctx.fillText(upperText, x + 5, h * 0.35);
|
|
472
|
-
lastUpperText = upperText;
|
|
473
|
-
}
|
|
474
705
|
ctx.fillStyle = "#000412";
|
|
475
706
|
ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
476
707
|
ctx.textAlign = "center";
|
|
@@ -480,10 +711,10 @@ class GanttChart {
|
|
|
480
711
|
ctx.lineTo(x, h);
|
|
481
712
|
ctx.strokeStyle = "#e0e0e0";
|
|
482
713
|
ctx.stroke();
|
|
483
|
-
if (nextDate.getTime() ===
|
|
484
|
-
|
|
714
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
715
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
485
716
|
} else {
|
|
486
|
-
|
|
717
|
+
currentDateForLower = nextDate;
|
|
487
718
|
}
|
|
488
719
|
}
|
|
489
720
|
ctx.beginPath();
|
|
@@ -493,6 +724,22 @@ class GanttChart {
|
|
|
493
724
|
ctx.stroke();
|
|
494
725
|
ctx.restore();
|
|
495
726
|
}
|
|
727
|
+
// Helper method to group consecutive blocks with same text
|
|
728
|
+
groupConsecutiveBlocks(blocks) {
|
|
729
|
+
if (blocks.length === 0) return [];
|
|
730
|
+
const grouped = [];
|
|
731
|
+
let currentGroup = { ...blocks[0] };
|
|
732
|
+
for (let i = 1; i < blocks.length; i++) {
|
|
733
|
+
if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
|
|
734
|
+
currentGroup.endX = blocks[i].endX;
|
|
735
|
+
} else {
|
|
736
|
+
grouped.push(currentGroup);
|
|
737
|
+
currentGroup = { ...blocks[i] };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
grouped.push(currentGroup);
|
|
741
|
+
return grouped;
|
|
742
|
+
}
|
|
496
743
|
renderMain() {
|
|
497
744
|
const ctx = this.mainCtx;
|
|
498
745
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -540,9 +787,9 @@ class GanttChart {
|
|
|
540
787
|
if (!fromPos) return;
|
|
541
788
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
542
789
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
543
|
-
const fromX = Math.max(fromPos.
|
|
790
|
+
const fromX = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
|
|
544
791
|
const fromY = fromPos.y;
|
|
545
|
-
const toX = Math.min(toPos.
|
|
792
|
+
const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
|
|
546
793
|
const toY = toPos.y;
|
|
547
794
|
ctx.beginPath();
|
|
548
795
|
if (isAdjacent) {
|
|
@@ -697,42 +944,35 @@ class GanttChart {
|
|
|
697
944
|
}
|
|
698
945
|
drawTask(ctx, task, y, pos) {
|
|
699
946
|
const offset = 4;
|
|
700
|
-
const width = pos.x_plan_end - pos.x_plan_start;
|
|
701
947
|
const taskY = y + this.config.rowHeight * 0.15 + offset;
|
|
702
948
|
const taskHeight = this.config.rowHeight * 0.7 - offset;
|
|
703
949
|
const textY = y + this.config.rowHeight / 2 + offset;
|
|
704
950
|
const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
|
|
705
|
-
const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
|
|
706
951
|
if (this.config.showActual && pos.x_actual_start) {
|
|
707
952
|
ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
|
|
708
|
-
|
|
709
|
-
pos.x_actual_start += aWidth * offsetX_actual;
|
|
710
|
-
pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
|
|
711
|
-
ctx.fillRect(Math.round(pos.x_actual_start), Math.round(taskY + 2), Math.round(aWidth * percent_actual), Math.round(taskHeight - 2));
|
|
953
|
+
ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
|
|
712
954
|
}
|
|
713
955
|
if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
|
|
714
956
|
ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
|
|
715
|
-
pos.x_plan_start += width * offsetX;
|
|
716
|
-
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
717
957
|
ctx.lineWidth = 4;
|
|
718
958
|
ctx.beginPath();
|
|
719
|
-
ctx.moveTo(pos.
|
|
720
|
-
ctx.lineTo(pos.
|
|
959
|
+
ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
|
|
960
|
+
ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
|
|
721
961
|
ctx.stroke();
|
|
722
962
|
}
|
|
723
963
|
ctx.fillStyle = "#000";
|
|
724
964
|
if (this.config.showLeftRemark && task.leftRemark) {
|
|
725
965
|
ctx.textAlign = "right";
|
|
726
|
-
ctx.fillText(task.leftRemark, Math.round(Math.min(...[pos.
|
|
966
|
+
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));
|
|
727
967
|
}
|
|
728
968
|
if (this.config.showRightRemark && task.rightRemark) {
|
|
729
969
|
ctx.textAlign = "left";
|
|
730
|
-
ctx.fillText(task.rightRemark, Math.round(Math.max(...[pos.
|
|
970
|
+
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));
|
|
731
971
|
}
|
|
732
972
|
if (this.config.showCenterRemark && task.centerRemark) {
|
|
733
|
-
const centerX = pos.
|
|
973
|
+
const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
|
|
734
974
|
ctx.textAlign = "center";
|
|
735
|
-
ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.
|
|
975
|
+
ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.offset_x_actual_end - pos.offset_x_actual_start));
|
|
736
976
|
}
|
|
737
977
|
}
|
|
738
978
|
getTaskStyles(task) {
|
|
@@ -753,6 +993,9 @@ class GanttChart {
|
|
|
753
993
|
return styles;
|
|
754
994
|
}
|
|
755
995
|
handleMouseMove(e) {
|
|
996
|
+
if (this.scrolling) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
756
999
|
const rect = this.mainCanvas.getBoundingClientRect();
|
|
757
1000
|
const mouseX = e.clientX - rect.left;
|
|
758
1001
|
const mouseY = e.clientY - rect.top;
|
|
@@ -765,15 +1008,16 @@ class GanttChart {
|
|
|
765
1008
|
if (this.config.tooltipFormat) {
|
|
766
1009
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
767
1010
|
if (!htmlStr) {
|
|
768
|
-
|
|
1011
|
+
this.handleMouseLeave();
|
|
1012
|
+
return;
|
|
769
1013
|
}
|
|
770
1014
|
this.tooltip.innerHTML = htmlStr;
|
|
771
1015
|
} else {
|
|
772
1016
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
773
|
-
const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(
|
|
1017
|
+
const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
|
|
774
1018
|
if (date >= pStart && date < pEnd) return true;
|
|
775
1019
|
if (task.actualStart) {
|
|
776
|
-
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(
|
|
1020
|
+
const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
|
|
777
1021
|
if (date >= aStart && date < aEnd) return true;
|
|
778
1022
|
}
|
|
779
1023
|
return false;
|
|
@@ -787,6 +1031,7 @@ class GanttChart {
|
|
|
787
1031
|
this.tooltip.innerHTML = html;
|
|
788
1032
|
}
|
|
789
1033
|
this.tooltip.style.display = "block";
|
|
1034
|
+
this.showTooltip = true;
|
|
790
1035
|
if (this.config.tooltipColor === "white") {
|
|
791
1036
|
this.tooltip.style.background = "#fff";
|
|
792
1037
|
this.tooltip.style.color = "#000";
|
|
@@ -816,6 +1061,7 @@ class GanttChart {
|
|
|
816
1061
|
}
|
|
817
1062
|
handleMouseLeave() {
|
|
818
1063
|
this.tooltip.style.display = "none";
|
|
1064
|
+
this.showTooltip = false;
|
|
819
1065
|
}
|
|
820
1066
|
/**
|
|
821
1067
|
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
@@ -827,17 +1073,29 @@ class GanttChart {
|
|
|
827
1073
|
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
828
1074
|
}
|
|
829
1075
|
/**
|
|
830
|
-
* scroll to specified date position, default to minDate
|
|
1076
|
+
* horizontal scroll to specified date position, default to minDate
|
|
831
1077
|
*
|
|
832
1078
|
* @param date
|
|
833
1079
|
*/
|
|
834
|
-
|
|
1080
|
+
horizontalScrollTo(date) {
|
|
835
1081
|
const startDate = date ? date : this.minDate;
|
|
836
1082
|
if (startDate) {
|
|
837
1083
|
const xPosition = this.dateToX(startDate);
|
|
838
1084
|
this.container.scrollTo({ left: xPosition - 80 });
|
|
839
1085
|
}
|
|
840
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* vertical scroll to specified position
|
|
1089
|
+
*
|
|
1090
|
+
* @param params
|
|
1091
|
+
*/
|
|
1092
|
+
verticalScrollTo(params) {
|
|
1093
|
+
if (params && (params.rowId || params.rowIndex)) {
|
|
1094
|
+
const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
|
|
1095
|
+
const yPosition = this.config.rowHeight * rowIndex;
|
|
1096
|
+
this.container.scrollTo({ top: yPosition - 80 });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
841
1099
|
}
|
|
842
1100
|
exports.DateUtils = DateUtils;
|
|
843
1101
|
exports.GanttChart = GanttChart;
|