gantt-canvas-chart 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +323 -75
- package/dist/index.css +2 -2
- package/dist/index.d.ts +41 -11
- package/dist/index.es.js +323 -75
- package/dist/index.umd.js +323 -75
- package/package.json +1 -1
package/dist/index.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,12 +91,14 @@ class GanttChart {
|
|
|
91
91
|
mainCanvas;
|
|
92
92
|
scrollDummy;
|
|
93
93
|
tooltip;
|
|
94
|
+
scrolling;
|
|
95
|
+
showTooltip;
|
|
94
96
|
headerCtx;
|
|
95
97
|
mainCtx;
|
|
96
98
|
timelineStart;
|
|
97
99
|
timelineEnd;
|
|
98
100
|
minDate;
|
|
99
|
-
|
|
101
|
+
maxDate;
|
|
100
102
|
pixelsPerDay;
|
|
101
103
|
scrollLeft;
|
|
102
104
|
scrollTop;
|
|
@@ -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;
|
|
@@ -166,6 +178,7 @@ class GanttChart {
|
|
|
166
178
|
this.timelineStart = /* @__PURE__ */ new Date();
|
|
167
179
|
this.timelineEnd = /* @__PURE__ */ new Date();
|
|
168
180
|
this.minDate = null;
|
|
181
|
+
this.maxDate = null;
|
|
169
182
|
this.pixelsPerDay = 40;
|
|
170
183
|
this.scrollLeft = 0;
|
|
171
184
|
this.scrollTop = 0;
|
|
@@ -178,10 +191,11 @@ class GanttChart {
|
|
|
178
191
|
this.totalHeight = 0;
|
|
179
192
|
this.taskPositions = /* @__PURE__ */ new Map();
|
|
180
193
|
this.taskMap = /* @__PURE__ */ new Map();
|
|
181
|
-
this.
|
|
182
|
-
this.
|
|
183
|
-
this.
|
|
184
|
-
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);
|
|
185
199
|
this.init();
|
|
186
200
|
}
|
|
187
201
|
init() {
|
|
@@ -191,6 +205,11 @@ class GanttChart {
|
|
|
191
205
|
this.setupEvents();
|
|
192
206
|
this.handleResize();
|
|
193
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
|
+
}
|
|
194
213
|
buildTaskMap() {
|
|
195
214
|
this.taskMap.clear();
|
|
196
215
|
this.data.forEach((row, rowIndex) => {
|
|
@@ -198,7 +217,7 @@ class GanttChart {
|
|
|
198
217
|
});
|
|
199
218
|
}
|
|
200
219
|
setupEvents() {
|
|
201
|
-
this.container.addEventListener("scroll", this.
|
|
220
|
+
this.container.addEventListener("scroll", this.handleScroll);
|
|
202
221
|
this.handleResize = this.handleResize.bind(this);
|
|
203
222
|
if (window.ResizeObserver) {
|
|
204
223
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
@@ -207,8 +226,8 @@ class GanttChart {
|
|
|
207
226
|
}, 100);
|
|
208
227
|
}
|
|
209
228
|
if (this.config.showTooltip) {
|
|
210
|
-
this.mainCanvas.addEventListener("mousemove", this.
|
|
211
|
-
this.mainCanvas.addEventListener("mouseleave", this.
|
|
229
|
+
this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
|
|
230
|
+
this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
|
|
212
231
|
}
|
|
213
232
|
}
|
|
214
233
|
updateConfig(newConfig) {
|
|
@@ -219,6 +238,7 @@ class GanttChart {
|
|
|
219
238
|
this.updatePixelsPerDay();
|
|
220
239
|
this.calculateFullTimeline();
|
|
221
240
|
}
|
|
241
|
+
this.updateLoadMoreConf();
|
|
222
242
|
this.updateDimensions();
|
|
223
243
|
this.render();
|
|
224
244
|
}
|
|
@@ -237,17 +257,18 @@ class GanttChart {
|
|
|
237
257
|
if (this.resizeObserver) {
|
|
238
258
|
this.resizeObserver.disconnect();
|
|
239
259
|
}
|
|
240
|
-
this.container.removeEventListener("scroll", this.
|
|
241
|
-
this.mainCanvas.removeEventListener("mousemove", this.
|
|
242
|
-
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);
|
|
243
263
|
this.container.remove();
|
|
244
264
|
}
|
|
245
265
|
calculateFullTimeline() {
|
|
266
|
+
const currentYear = this.today.getFullYear();
|
|
246
267
|
let minDate = new Date(9999, 0, 1);
|
|
247
268
|
let maxDate = new Date(1e3, 0, 1);
|
|
248
269
|
if (this.data.length === 0) {
|
|
249
270
|
minDate = /* @__PURE__ */ new Date();
|
|
250
|
-
maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(),
|
|
271
|
+
maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(), 60);
|
|
251
272
|
} else {
|
|
252
273
|
this.taskMap.forEach(({ task }) => {
|
|
253
274
|
const pStart = new Date(task.planStart);
|
|
@@ -263,8 +284,11 @@ class GanttChart {
|
|
|
263
284
|
});
|
|
264
285
|
}
|
|
265
286
|
this.minDate = minDate;
|
|
266
|
-
|
|
267
|
-
|
|
287
|
+
this.maxDate = maxDate;
|
|
288
|
+
const minYear = minDate.getFullYear();
|
|
289
|
+
const maxYear = maxDate.getFullYear();
|
|
290
|
+
minDate = DateUtils.addDays(minYear === 9999 ? new Date(currentYear, 0, 1) : minDate, -7);
|
|
291
|
+
maxDate = DateUtils.addDays(maxYear === 1e3 ? new Date(currentYear + 1, 0, 1) : maxDate, 14);
|
|
268
292
|
switch (this.config.viewMode) {
|
|
269
293
|
case "Year":
|
|
270
294
|
this.timelineStart = DateUtils.getStartOfYear(minDate);
|
|
@@ -303,7 +327,14 @@ class GanttChart {
|
|
|
303
327
|
this.updateDimensions();
|
|
304
328
|
this.render();
|
|
305
329
|
}
|
|
330
|
+
// Add this method to register the data loading callback
|
|
331
|
+
setOnDataLoadCallback(callback) {
|
|
332
|
+
this.onDataLoad = callback;
|
|
333
|
+
}
|
|
306
334
|
handleScroll(e) {
|
|
335
|
+
if (this.showTooltip) {
|
|
336
|
+
this.handleMouseLeave();
|
|
337
|
+
}
|
|
307
338
|
const target = e.target;
|
|
308
339
|
this.scrollLeft = target.scrollLeft;
|
|
309
340
|
this.scrollTop = target.scrollTop;
|
|
@@ -311,7 +342,134 @@ class GanttChart {
|
|
|
311
342
|
detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
|
|
312
343
|
});
|
|
313
344
|
this.container.dispatchEvent(event);
|
|
314
|
-
|
|
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);
|
|
315
473
|
}
|
|
316
474
|
setScrollTop(scrollTop) {
|
|
317
475
|
if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
|
|
@@ -325,10 +483,14 @@ class GanttChart {
|
|
|
325
483
|
}
|
|
326
484
|
updateDimensions() {
|
|
327
485
|
const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
this.
|
|
331
|
-
|
|
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
|
+
}
|
|
332
494
|
}
|
|
333
495
|
setupCanvas(canvas, width, height) {
|
|
334
496
|
canvas.width = width * this.devicePixelRatio;
|
|
@@ -346,13 +508,13 @@ class GanttChart {
|
|
|
346
508
|
const y = i * this.config.rowHeight;
|
|
347
509
|
row.tasks.forEach((task) => {
|
|
348
510
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
349
|
-
const x_plan_end = this.dateToX(DateUtils.addDays(
|
|
511
|
+
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
350
512
|
let x_actual_start = null, x_actual_end = null;
|
|
351
513
|
if (task.actualStart) {
|
|
352
514
|
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
353
515
|
}
|
|
354
516
|
if (task.actualEnd) {
|
|
355
|
-
x_actual_end = this.dateToX(DateUtils.addDays(
|
|
517
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
356
518
|
}
|
|
357
519
|
this.taskPositions.set(task.id, {
|
|
358
520
|
x_plan_start,
|
|
@@ -378,9 +540,11 @@ class GanttChart {
|
|
|
378
540
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
379
541
|
}
|
|
380
542
|
}
|
|
381
|
-
render() {
|
|
543
|
+
render(scrolling = false) {
|
|
382
544
|
this.updateVirtualRanges();
|
|
383
|
-
|
|
545
|
+
if (!scrolling) {
|
|
546
|
+
this.calculateAllTaskPositions();
|
|
547
|
+
}
|
|
384
548
|
this.renderHeader();
|
|
385
549
|
this.renderMain();
|
|
386
550
|
}
|
|
@@ -390,82 +554,132 @@ class GanttChart {
|
|
|
390
554
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
391
555
|
ctx.save();
|
|
392
556
|
ctx.translate(-this.scrollLeft, 0);
|
|
393
|
-
ctx.fillStyle =
|
|
557
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
394
558
|
ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
|
|
395
559
|
ctx.textBaseline = "middle";
|
|
396
560
|
ctx.textRendering = "optimizeLegibility";
|
|
397
561
|
let currentDate = new Date(this.visibleDateRange.start);
|
|
398
562
|
currentDate = this.getIterationStartDate(currentDate);
|
|
399
|
-
|
|
400
|
-
|
|
563
|
+
const visibleBlocks = [];
|
|
564
|
+
let calcDate = new Date(currentDate);
|
|
565
|
+
while (calcDate <= this.visibleDateRange.end) {
|
|
401
566
|
let nextDate;
|
|
567
|
+
let upperText = "";
|
|
402
568
|
switch (this.config.viewMode) {
|
|
403
569
|
case "Day":
|
|
404
|
-
|
|
570
|
+
upperText = DateUtils.format(calcDate, "yyyy年MM月");
|
|
571
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
405
572
|
break;
|
|
406
573
|
case "Week":
|
|
407
|
-
|
|
574
|
+
const weekStart = DateUtils.getStartOfWeek(calcDate);
|
|
575
|
+
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
576
|
+
nextDate = DateUtils.addDays(weekStart, 7);
|
|
408
577
|
break;
|
|
409
578
|
case "Month":
|
|
410
|
-
|
|
579
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
580
|
+
nextDate = DateUtils.addMonths(calcDate, 1);
|
|
411
581
|
break;
|
|
412
582
|
case "Year":
|
|
413
|
-
|
|
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);
|
|
414
623
|
break;
|
|
415
624
|
default:
|
|
416
|
-
nextDate = DateUtils.addDays(
|
|
625
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
417
626
|
break;
|
|
418
627
|
}
|
|
419
|
-
if (nextDate.getTime() ===
|
|
420
|
-
|
|
628
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
629
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
421
630
|
} else {
|
|
422
|
-
|
|
631
|
+
currentDateForLower = nextDate;
|
|
423
632
|
}
|
|
424
633
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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;
|
|
428
652
|
switch (this.config.viewMode) {
|
|
429
653
|
case "Day":
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
654
|
+
lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
|
|
655
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
433
656
|
break;
|
|
434
657
|
case "Week":
|
|
435
|
-
const weekStart = DateUtils.getStartOfWeek(
|
|
436
|
-
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
658
|
+
const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
|
|
437
659
|
lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
|
|
438
660
|
nextDate = DateUtils.addDays(weekStart, 7);
|
|
439
661
|
break;
|
|
440
662
|
case "Month":
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
nextDate = DateUtils.addMonths(currentDate, 1);
|
|
663
|
+
lowerText = `${currentDateForLower.getMonth() + 1}月`;
|
|
664
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
444
665
|
break;
|
|
445
666
|
case "Year":
|
|
446
|
-
if (
|
|
447
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
667
|
+
if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
|
|
448
668
|
lowerText = `上半年`;
|
|
449
|
-
nextDate = DateUtils.addMonths(
|
|
450
|
-
} else if (
|
|
451
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
669
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
670
|
+
} else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
|
|
452
671
|
lowerText = `下半年`;
|
|
453
|
-
nextDate = DateUtils.addMonths(
|
|
672
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
454
673
|
} else {
|
|
455
|
-
|
|
674
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
456
675
|
continue;
|
|
457
676
|
}
|
|
458
677
|
break;
|
|
678
|
+
default:
|
|
679
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
680
|
+
break;
|
|
459
681
|
}
|
|
460
682
|
const unitWidth = this.dateToX(nextDate) - x;
|
|
461
|
-
if (upperText !== lastUpperText) {
|
|
462
|
-
ctx.fillStyle = "#333";
|
|
463
|
-
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
464
|
-
ctx.textRendering = "optimizeLegibility";
|
|
465
|
-
ctx.textAlign = "left";
|
|
466
|
-
ctx.fillText(upperText, x + 5, h * 0.35);
|
|
467
|
-
lastUpperText = upperText;
|
|
468
|
-
}
|
|
469
683
|
ctx.fillStyle = "#000412";
|
|
470
684
|
ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
471
685
|
ctx.textAlign = "center";
|
|
@@ -475,10 +689,10 @@ class GanttChart {
|
|
|
475
689
|
ctx.lineTo(x, h);
|
|
476
690
|
ctx.strokeStyle = "#e0e0e0";
|
|
477
691
|
ctx.stroke();
|
|
478
|
-
if (nextDate.getTime() ===
|
|
479
|
-
|
|
692
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
693
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
480
694
|
} else {
|
|
481
|
-
|
|
695
|
+
currentDateForLower = nextDate;
|
|
482
696
|
}
|
|
483
697
|
}
|
|
484
698
|
ctx.beginPath();
|
|
@@ -488,6 +702,22 @@ class GanttChart {
|
|
|
488
702
|
ctx.stroke();
|
|
489
703
|
ctx.restore();
|
|
490
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
|
+
}
|
|
491
721
|
renderMain() {
|
|
492
722
|
const ctx = this.mainCtx;
|
|
493
723
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -683,7 +913,7 @@ class GanttChart {
|
|
|
683
913
|
const x = this.dateToX(this.today);
|
|
684
914
|
if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
|
|
685
915
|
ctx.strokeStyle = this.config.todayColor;
|
|
686
|
-
ctx.lineWidth =
|
|
916
|
+
ctx.lineWidth = 1;
|
|
687
917
|
ctx.beginPath();
|
|
688
918
|
ctx.moveTo(x, this.scrollTop);
|
|
689
919
|
ctx.lineTo(x, this.scrollTop + this.viewportHeight);
|
|
@@ -711,8 +941,8 @@ class GanttChart {
|
|
|
711
941
|
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
712
942
|
ctx.lineWidth = 4;
|
|
713
943
|
ctx.beginPath();
|
|
714
|
-
ctx.moveTo(pos.x_plan_start, taskY);
|
|
715
|
-
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);
|
|
716
946
|
ctx.stroke();
|
|
717
947
|
}
|
|
718
948
|
ctx.fillStyle = "#000";
|
|
@@ -748,6 +978,9 @@ class GanttChart {
|
|
|
748
978
|
return styles;
|
|
749
979
|
}
|
|
750
980
|
handleMouseMove(e) {
|
|
981
|
+
if (this.scrolling) {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
751
984
|
const rect = this.mainCanvas.getBoundingClientRect();
|
|
752
985
|
const mouseX = e.clientX - rect.left;
|
|
753
986
|
const mouseY = e.clientY - rect.top;
|
|
@@ -760,15 +993,16 @@ class GanttChart {
|
|
|
760
993
|
if (this.config.tooltipFormat) {
|
|
761
994
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
762
995
|
if (!htmlStr) {
|
|
763
|
-
|
|
996
|
+
this.handleMouseLeave();
|
|
997
|
+
return;
|
|
764
998
|
}
|
|
765
999
|
this.tooltip.innerHTML = htmlStr;
|
|
766
1000
|
} else {
|
|
767
1001
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
768
|
-
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);
|
|
769
1003
|
if (date >= pStart && date < pEnd) return true;
|
|
770
1004
|
if (task.actualStart) {
|
|
771
|
-
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);
|
|
772
1006
|
if (date >= aStart && date < aEnd) return true;
|
|
773
1007
|
}
|
|
774
1008
|
return false;
|
|
@@ -782,6 +1016,7 @@ class GanttChart {
|
|
|
782
1016
|
this.tooltip.innerHTML = html;
|
|
783
1017
|
}
|
|
784
1018
|
this.tooltip.style.display = "block";
|
|
1019
|
+
this.showTooltip = true;
|
|
785
1020
|
if (this.config.tooltipColor === "white") {
|
|
786
1021
|
this.tooltip.style.background = "#fff";
|
|
787
1022
|
this.tooltip.style.color = "#000";
|
|
@@ -811,6 +1046,7 @@ class GanttChart {
|
|
|
811
1046
|
}
|
|
812
1047
|
handleMouseLeave() {
|
|
813
1048
|
this.tooltip.style.display = "none";
|
|
1049
|
+
this.showTooltip = false;
|
|
814
1050
|
}
|
|
815
1051
|
/**
|
|
816
1052
|
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
@@ -822,17 +1058,29 @@ class GanttChart {
|
|
|
822
1058
|
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
823
1059
|
}
|
|
824
1060
|
/**
|
|
825
|
-
* scroll to specified date position, default to minDate
|
|
1061
|
+
* horizontal scroll to specified date position, default to minDate
|
|
826
1062
|
*
|
|
827
1063
|
* @param date
|
|
828
1064
|
*/
|
|
829
|
-
|
|
1065
|
+
horizontalScrollTo(date) {
|
|
830
1066
|
const startDate = date ? date : this.minDate;
|
|
831
1067
|
if (startDate) {
|
|
832
1068
|
const xPosition = this.dateToX(startDate);
|
|
833
1069
|
this.container.scrollTo({ left: xPosition - 80 });
|
|
834
1070
|
}
|
|
835
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
|
+
}
|
|
836
1084
|
}
|
|
837
1085
|
exports.DateUtils = DateUtils;
|
|
838
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
|
}
|