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.cjs.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
|
*/
|
|
@@ -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,11 @@ 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);
|
|
186
199
|
this.init();
|
|
187
200
|
}
|
|
188
201
|
init() {
|
|
@@ -192,6 +205,11 @@ class GanttChart {
|
|
|
192
205
|
this.setupEvents();
|
|
193
206
|
this.handleResize();
|
|
194
207
|
}
|
|
208
|
+
updateLoadMoreConf() {
|
|
209
|
+
this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
|
|
210
|
+
this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
|
|
211
|
+
this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
|
|
212
|
+
}
|
|
195
213
|
buildTaskMap() {
|
|
196
214
|
this.taskMap.clear();
|
|
197
215
|
this.data.forEach((row, rowIndex) => {
|
|
@@ -199,7 +217,7 @@ class GanttChart {
|
|
|
199
217
|
});
|
|
200
218
|
}
|
|
201
219
|
setupEvents() {
|
|
202
|
-
this.container.addEventListener("scroll", this.
|
|
220
|
+
this.container.addEventListener("scroll", this.handleScroll);
|
|
203
221
|
this.handleResize = this.handleResize.bind(this);
|
|
204
222
|
if (window.ResizeObserver) {
|
|
205
223
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
@@ -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,13 @@ 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);
|
|
244
263
|
this.container.remove();
|
|
245
264
|
}
|
|
246
265
|
calculateFullTimeline() {
|
|
247
|
-
const currentYear =
|
|
266
|
+
const currentYear = this.today.getFullYear();
|
|
248
267
|
let minDate = new Date(9999, 0, 1);
|
|
249
268
|
let maxDate = new Date(1e3, 0, 1);
|
|
250
269
|
if (this.data.length === 0) {
|
|
@@ -308,7 +327,14 @@ class GanttChart {
|
|
|
308
327
|
this.updateDimensions();
|
|
309
328
|
this.render();
|
|
310
329
|
}
|
|
330
|
+
// Add this method to register the data loading callback
|
|
331
|
+
setOnDataLoadCallback(callback) {
|
|
332
|
+
this.onDataLoad = callback;
|
|
333
|
+
}
|
|
311
334
|
handleScroll(e) {
|
|
335
|
+
if (this.showTooltip) {
|
|
336
|
+
this.handleMouseLeave();
|
|
337
|
+
}
|
|
312
338
|
const target = e.target;
|
|
313
339
|
this.scrollLeft = target.scrollLeft;
|
|
314
340
|
this.scrollTop = target.scrollTop;
|
|
@@ -316,7 +342,134 @@ class GanttChart {
|
|
|
316
342
|
detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
|
|
317
343
|
});
|
|
318
344
|
this.container.dispatchEvent(event);
|
|
319
|
-
|
|
345
|
+
if (this.config.enabledLoadMore.length > 0) {
|
|
346
|
+
if (this.scrollLoadTimer !== null) {
|
|
347
|
+
clearTimeout(this.scrollLoadTimer);
|
|
348
|
+
this.scrollLoadTimer = null;
|
|
349
|
+
}
|
|
350
|
+
this.scrollLoadTimer = window.setTimeout(() => {
|
|
351
|
+
this.checkScrollLoad();
|
|
352
|
+
this.scrollLoadTimer = null;
|
|
353
|
+
}, 100);
|
|
354
|
+
}
|
|
355
|
+
requestAnimationFrame(() => {
|
|
356
|
+
this.scrolling = true;
|
|
357
|
+
this.render(true);
|
|
358
|
+
this.scrolling = false;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
// checkScrollLoad method
|
|
362
|
+
async checkScrollLoad() {
|
|
363
|
+
if (this.isLoadingData || !this.onDataLoad) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const scrollLeft = this.scrollLeft;
|
|
367
|
+
const scrollTop = this.scrollTop;
|
|
368
|
+
const viewportWidth = this.viewportWidth;
|
|
369
|
+
const viewportHeight = this.viewportHeight;
|
|
370
|
+
const totalWidth = this.totalWidth;
|
|
371
|
+
const totalHeight = this.totalHeight;
|
|
372
|
+
const atLeftEdge = scrollLeft <= 5;
|
|
373
|
+
const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
|
|
374
|
+
const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
|
|
375
|
+
try {
|
|
376
|
+
if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
|
|
377
|
+
await this.loadMoreData("left");
|
|
378
|
+
console.log("left-loadMoreData::", this.data);
|
|
379
|
+
} else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
|
|
380
|
+
await this.loadMoreData("right");
|
|
381
|
+
console.log("right-loadMoreData::", this.data);
|
|
382
|
+
} else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
|
|
383
|
+
await this.loadMoreData("bottom");
|
|
384
|
+
console.log("bottom-loadMoreData::", this.data);
|
|
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,13 +508,13 @@ 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;
|
|
356
513
|
if (task.actualStart) {
|
|
357
514
|
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
358
515
|
}
|
|
359
516
|
if (task.actualEnd) {
|
|
360
|
-
x_actual_end = this.dateToX(DateUtils.addDays(
|
|
517
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
361
518
|
}
|
|
362
519
|
this.taskPositions.set(task.id, {
|
|
363
520
|
x_plan_start,
|
|
@@ -383,9 +540,11 @@ class GanttChart {
|
|
|
383
540
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
384
541
|
}
|
|
385
542
|
}
|
|
386
|
-
render() {
|
|
543
|
+
render(scrolling = false) {
|
|
387
544
|
this.updateVirtualRanges();
|
|
388
|
-
|
|
545
|
+
if (!scrolling) {
|
|
546
|
+
this.calculateAllTaskPositions();
|
|
547
|
+
}
|
|
389
548
|
this.renderHeader();
|
|
390
549
|
this.renderMain();
|
|
391
550
|
}
|
|
@@ -395,82 +554,132 @@ class GanttChart {
|
|
|
395
554
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
396
555
|
ctx.save();
|
|
397
556
|
ctx.translate(-this.scrollLeft, 0);
|
|
398
|
-
ctx.fillStyle =
|
|
557
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
399
558
|
ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
|
|
400
559
|
ctx.textBaseline = "middle";
|
|
401
560
|
ctx.textRendering = "optimizeLegibility";
|
|
402
561
|
let currentDate = new Date(this.visibleDateRange.start);
|
|
403
562
|
currentDate = this.getIterationStartDate(currentDate);
|
|
404
|
-
|
|
405
|
-
|
|
563
|
+
const visibleBlocks = [];
|
|
564
|
+
let calcDate = new Date(currentDate);
|
|
565
|
+
while (calcDate <= this.visibleDateRange.end) {
|
|
406
566
|
let nextDate;
|
|
567
|
+
let upperText = "";
|
|
407
568
|
switch (this.config.viewMode) {
|
|
408
569
|
case "Day":
|
|
409
|
-
|
|
570
|
+
upperText = DateUtils.format(calcDate, "yyyy年MM月");
|
|
571
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
410
572
|
break;
|
|
411
573
|
case "Week":
|
|
412
|
-
|
|
574
|
+
const weekStart = DateUtils.getStartOfWeek(calcDate);
|
|
575
|
+
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
576
|
+
nextDate = DateUtils.addDays(weekStart, 7);
|
|
413
577
|
break;
|
|
414
578
|
case "Month":
|
|
415
|
-
|
|
579
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
580
|
+
nextDate = DateUtils.addMonths(calcDate, 1);
|
|
416
581
|
break;
|
|
417
582
|
case "Year":
|
|
418
|
-
|
|
583
|
+
if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
|
|
584
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
585
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
586
|
+
} else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
|
|
587
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
588
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
589
|
+
} else {
|
|
590
|
+
calcDate = DateUtils.addDays(calcDate, 1);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
default:
|
|
595
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
const startX = this.dateToX(calcDate);
|
|
599
|
+
const endX = this.dateToX(nextDate);
|
|
600
|
+
visibleBlocks.push({
|
|
601
|
+
startX,
|
|
602
|
+
endX,
|
|
603
|
+
text: upperText,
|
|
604
|
+
yPos: h * 0.35
|
|
605
|
+
});
|
|
606
|
+
calcDate = nextDate;
|
|
607
|
+
}
|
|
608
|
+
let currentDateForLower = new Date(currentDate);
|
|
609
|
+
while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
|
|
610
|
+
let nextDate;
|
|
611
|
+
switch (this.config.viewMode) {
|
|
612
|
+
case "Day":
|
|
613
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
614
|
+
break;
|
|
615
|
+
case "Week":
|
|
616
|
+
nextDate = DateUtils.addDays(currentDateForLower, 7);
|
|
617
|
+
break;
|
|
618
|
+
case "Month":
|
|
619
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
620
|
+
break;
|
|
621
|
+
case "Year":
|
|
622
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
419
623
|
break;
|
|
420
624
|
default:
|
|
421
|
-
nextDate = DateUtils.addDays(
|
|
625
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
422
626
|
break;
|
|
423
627
|
}
|
|
424
|
-
if (nextDate.getTime() ===
|
|
425
|
-
|
|
628
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
629
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
426
630
|
} else {
|
|
427
|
-
|
|
631
|
+
currentDateForLower = nextDate;
|
|
428
632
|
}
|
|
429
633
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
634
|
+
const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
|
|
635
|
+
ctx.fillStyle = "#333";
|
|
636
|
+
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
637
|
+
ctx.textAlign = "left";
|
|
638
|
+
groupedBlocks.forEach((group) => {
|
|
639
|
+
const visibleStart = Math.max(group.startX, this.scrollLeft);
|
|
640
|
+
const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
|
|
641
|
+
if (visibleEnd > visibleStart) {
|
|
642
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
643
|
+
ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
|
|
644
|
+
ctx.fillStyle = "#333";
|
|
645
|
+
ctx.fillText(group.text, visibleStart + 5, group.yPos);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
while (currentDateForLower <= this.visibleDateRange.end) {
|
|
649
|
+
const x = this.dateToX(currentDateForLower);
|
|
650
|
+
let lowerText = "";
|
|
651
|
+
let nextDate;
|
|
433
652
|
switch (this.config.viewMode) {
|
|
434
653
|
case "Day":
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
654
|
+
lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
|
|
655
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
438
656
|
break;
|
|
439
657
|
case "Week":
|
|
440
|
-
const weekStart = DateUtils.getStartOfWeek(
|
|
441
|
-
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
658
|
+
const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
|
|
442
659
|
lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
|
|
443
660
|
nextDate = DateUtils.addDays(weekStart, 7);
|
|
444
661
|
break;
|
|
445
662
|
case "Month":
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
nextDate = DateUtils.addMonths(currentDate, 1);
|
|
663
|
+
lowerText = `${currentDateForLower.getMonth() + 1}月`;
|
|
664
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
449
665
|
break;
|
|
450
666
|
case "Year":
|
|
451
|
-
if (
|
|
452
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
667
|
+
if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
|
|
453
668
|
lowerText = `上半年`;
|
|
454
|
-
nextDate = DateUtils.addMonths(
|
|
455
|
-
} else if (
|
|
456
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
669
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
670
|
+
} else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
|
|
457
671
|
lowerText = `下半年`;
|
|
458
|
-
nextDate = DateUtils.addMonths(
|
|
672
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
459
673
|
} else {
|
|
460
|
-
|
|
674
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
461
675
|
continue;
|
|
462
676
|
}
|
|
463
677
|
break;
|
|
678
|
+
default:
|
|
679
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
680
|
+
break;
|
|
464
681
|
}
|
|
465
682
|
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
683
|
ctx.fillStyle = "#000412";
|
|
475
684
|
ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
476
685
|
ctx.textAlign = "center";
|
|
@@ -480,10 +689,10 @@ class GanttChart {
|
|
|
480
689
|
ctx.lineTo(x, h);
|
|
481
690
|
ctx.strokeStyle = "#e0e0e0";
|
|
482
691
|
ctx.stroke();
|
|
483
|
-
if (nextDate.getTime() ===
|
|
484
|
-
|
|
692
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
693
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
485
694
|
} else {
|
|
486
|
-
|
|
695
|
+
currentDateForLower = nextDate;
|
|
487
696
|
}
|
|
488
697
|
}
|
|
489
698
|
ctx.beginPath();
|
|
@@ -493,6 +702,22 @@ class GanttChart {
|
|
|
493
702
|
ctx.stroke();
|
|
494
703
|
ctx.restore();
|
|
495
704
|
}
|
|
705
|
+
// Helper method to group consecutive blocks with same text
|
|
706
|
+
groupConsecutiveBlocks(blocks) {
|
|
707
|
+
if (blocks.length === 0) return [];
|
|
708
|
+
const grouped = [];
|
|
709
|
+
let currentGroup = { ...blocks[0] };
|
|
710
|
+
for (let i = 1; i < blocks.length; i++) {
|
|
711
|
+
if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
|
|
712
|
+
currentGroup.endX = blocks[i].endX;
|
|
713
|
+
} else {
|
|
714
|
+
grouped.push(currentGroup);
|
|
715
|
+
currentGroup = { ...blocks[i] };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
grouped.push(currentGroup);
|
|
719
|
+
return grouped;
|
|
720
|
+
}
|
|
496
721
|
renderMain() {
|
|
497
722
|
const ctx = this.mainCtx;
|
|
498
723
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -716,8 +941,8 @@ class GanttChart {
|
|
|
716
941
|
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
717
942
|
ctx.lineWidth = 4;
|
|
718
943
|
ctx.beginPath();
|
|
719
|
-
ctx.moveTo(pos.x_plan_start, taskY);
|
|
720
|
-
ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
|
|
944
|
+
ctx.moveTo(pos.x_plan_start + 2, taskY);
|
|
945
|
+
ctx.lineTo(pos.x_plan_start + width * percent_plan - 2, taskY);
|
|
721
946
|
ctx.stroke();
|
|
722
947
|
}
|
|
723
948
|
ctx.fillStyle = "#000";
|
|
@@ -753,6 +978,9 @@ class GanttChart {
|
|
|
753
978
|
return styles;
|
|
754
979
|
}
|
|
755
980
|
handleMouseMove(e) {
|
|
981
|
+
if (this.scrolling) {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
756
984
|
const rect = this.mainCanvas.getBoundingClientRect();
|
|
757
985
|
const mouseX = e.clientX - rect.left;
|
|
758
986
|
const mouseY = e.clientY - rect.top;
|
|
@@ -765,15 +993,16 @@ class GanttChart {
|
|
|
765
993
|
if (this.config.tooltipFormat) {
|
|
766
994
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
767
995
|
if (!htmlStr) {
|
|
768
|
-
|
|
996
|
+
this.handleMouseLeave();
|
|
997
|
+
return;
|
|
769
998
|
}
|
|
770
999
|
this.tooltip.innerHTML = htmlStr;
|
|
771
1000
|
} else {
|
|
772
1001
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
773
|
-
const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(
|
|
1002
|
+
const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
|
|
774
1003
|
if (date >= pStart && date < pEnd) return true;
|
|
775
1004
|
if (task.actualStart) {
|
|
776
|
-
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(
|
|
1005
|
+
const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
|
|
777
1006
|
if (date >= aStart && date < aEnd) return true;
|
|
778
1007
|
}
|
|
779
1008
|
return false;
|
|
@@ -787,6 +1016,7 @@ class GanttChart {
|
|
|
787
1016
|
this.tooltip.innerHTML = html;
|
|
788
1017
|
}
|
|
789
1018
|
this.tooltip.style.display = "block";
|
|
1019
|
+
this.showTooltip = true;
|
|
790
1020
|
if (this.config.tooltipColor === "white") {
|
|
791
1021
|
this.tooltip.style.background = "#fff";
|
|
792
1022
|
this.tooltip.style.color = "#000";
|
|
@@ -816,6 +1046,7 @@ class GanttChart {
|
|
|
816
1046
|
}
|
|
817
1047
|
handleMouseLeave() {
|
|
818
1048
|
this.tooltip.style.display = "none";
|
|
1049
|
+
this.showTooltip = false;
|
|
819
1050
|
}
|
|
820
1051
|
/**
|
|
821
1052
|
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
@@ -827,17 +1058,29 @@ class GanttChart {
|
|
|
827
1058
|
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
828
1059
|
}
|
|
829
1060
|
/**
|
|
830
|
-
* scroll to specified date position, default to minDate
|
|
1061
|
+
* horizontal scroll to specified date position, default to minDate
|
|
831
1062
|
*
|
|
832
1063
|
* @param date
|
|
833
1064
|
*/
|
|
834
|
-
|
|
1065
|
+
horizontalScrollTo(date) {
|
|
835
1066
|
const startDate = date ? date : this.minDate;
|
|
836
1067
|
if (startDate) {
|
|
837
1068
|
const xPosition = this.dateToX(startDate);
|
|
838
1069
|
this.container.scrollTo({ left: xPosition - 80 });
|
|
839
1070
|
}
|
|
840
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* vertical scroll to specified position
|
|
1074
|
+
*
|
|
1075
|
+
* @param params
|
|
1076
|
+
*/
|
|
1077
|
+
verticalScrollTo(params) {
|
|
1078
|
+
if (params && (params.rowId || params.rowIndex)) {
|
|
1079
|
+
const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
|
|
1080
|
+
const yPosition = this.config.rowHeight * rowIndex;
|
|
1081
|
+
this.container.scrollTo({ top: yPosition - 80 });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
841
1084
|
}
|
|
842
1085
|
exports.DateUtils = DateUtils;
|
|
843
1086
|
exports.GanttChart = GanttChart;
|
package/dist/index.css
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
|
*/
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
font-size: 13px;
|
|
81
81
|
line-height: 1.6;
|
|
82
82
|
pointer-events: none;
|
|
83
|
-
z-index:
|
|
83
|
+
z-index: 2147483640;
|
|
84
84
|
max-width: 300px;
|
|
85
85
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
86
86
|
}
|