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.umd.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
|
*/
|
|
@@ -93,12 +93,14 @@
|
|
|
93
93
|
mainCanvas;
|
|
94
94
|
scrollDummy;
|
|
95
95
|
tooltip;
|
|
96
|
+
scrolling;
|
|
97
|
+
showTooltip;
|
|
96
98
|
headerCtx;
|
|
97
99
|
mainCtx;
|
|
98
100
|
timelineStart;
|
|
99
101
|
timelineEnd;
|
|
100
102
|
minDate;
|
|
101
|
-
|
|
103
|
+
maxDate;
|
|
102
104
|
pixelsPerDay;
|
|
103
105
|
scrollLeft;
|
|
104
106
|
scrollTop;
|
|
@@ -112,9 +114,14 @@
|
|
|
112
114
|
resizeObserver;
|
|
113
115
|
taskPositions;
|
|
114
116
|
taskMap;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
isLoadingData = false;
|
|
118
|
+
hasMoreDataLeft = true;
|
|
119
|
+
hasMoreDataRight = true;
|
|
120
|
+
hasMoreDataBottom = true;
|
|
121
|
+
lastScrollLeft = 0;
|
|
122
|
+
lastScrollTop = 0;
|
|
123
|
+
onDataLoad = null;
|
|
124
|
+
scrollLoadTimer = null;
|
|
118
125
|
constructor(rootContainer, data, config = {}) {
|
|
119
126
|
const container = document.createElement("div");
|
|
120
127
|
const scrollEl = document.createElement("div");
|
|
@@ -133,6 +140,8 @@
|
|
|
133
140
|
this.rootContainer = rootContainer;
|
|
134
141
|
this.container = container;
|
|
135
142
|
this.data = data;
|
|
143
|
+
this.scrolling = false;
|
|
144
|
+
this.showTooltip = false;
|
|
136
145
|
this.config = {
|
|
137
146
|
viewMode: "Month",
|
|
138
147
|
rowHeight: 48,
|
|
@@ -150,11 +159,14 @@
|
|
|
150
159
|
todayColor: "#ff4d4f",
|
|
151
160
|
offsetTop: 0,
|
|
152
161
|
offsetLeft: 0,
|
|
162
|
+
enabledLoadMore: [],
|
|
153
163
|
viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
|
|
154
164
|
planBorderColor: "#C1EFCF",
|
|
155
165
|
actualBgColor: "#5AC989",
|
|
166
|
+
headerBgColor: "#f9f9f9",
|
|
156
167
|
...config
|
|
157
168
|
};
|
|
169
|
+
this.updateLoadMoreConf();
|
|
158
170
|
this.headerCanvas = headerCanvas;
|
|
159
171
|
this.mainCanvas = mainCanvas;
|
|
160
172
|
this.scrollDummy = scrollEl;
|
|
@@ -168,6 +180,7 @@
|
|
|
168
180
|
this.timelineStart = /* @__PURE__ */ new Date();
|
|
169
181
|
this.timelineEnd = /* @__PURE__ */ new Date();
|
|
170
182
|
this.minDate = null;
|
|
183
|
+
this.maxDate = null;
|
|
171
184
|
this.pixelsPerDay = 40;
|
|
172
185
|
this.scrollLeft = 0;
|
|
173
186
|
this.scrollTop = 0;
|
|
@@ -180,10 +193,11 @@
|
|
|
180
193
|
this.totalHeight = 0;
|
|
181
194
|
this.taskPositions = /* @__PURE__ */ new Map();
|
|
182
195
|
this.taskMap = /* @__PURE__ */ new Map();
|
|
183
|
-
this.
|
|
184
|
-
this.
|
|
185
|
-
this.
|
|
186
|
-
this.
|
|
196
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
197
|
+
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
|
198
|
+
this.handleScroll = this.handleScroll.bind(this);
|
|
199
|
+
this.horizontalScrollTo = this.horizontalScrollTo.bind(this);
|
|
200
|
+
this.verticalScrollTo = this.verticalScrollTo.bind(this);
|
|
187
201
|
this.init();
|
|
188
202
|
}
|
|
189
203
|
init() {
|
|
@@ -193,6 +207,11 @@
|
|
|
193
207
|
this.setupEvents();
|
|
194
208
|
this.handleResize();
|
|
195
209
|
}
|
|
210
|
+
updateLoadMoreConf() {
|
|
211
|
+
this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
|
|
212
|
+
this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
|
|
213
|
+
this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
|
|
214
|
+
}
|
|
196
215
|
buildTaskMap() {
|
|
197
216
|
this.taskMap.clear();
|
|
198
217
|
this.data.forEach((row, rowIndex) => {
|
|
@@ -200,7 +219,7 @@
|
|
|
200
219
|
});
|
|
201
220
|
}
|
|
202
221
|
setupEvents() {
|
|
203
|
-
this.container.addEventListener("scroll", this.
|
|
222
|
+
this.container.addEventListener("scroll", this.handleScroll);
|
|
204
223
|
this.handleResize = this.handleResize.bind(this);
|
|
205
224
|
if (window.ResizeObserver) {
|
|
206
225
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
@@ -209,8 +228,8 @@
|
|
|
209
228
|
}, 100);
|
|
210
229
|
}
|
|
211
230
|
if (this.config.showTooltip) {
|
|
212
|
-
this.mainCanvas.addEventListener("mousemove", this.
|
|
213
|
-
this.mainCanvas.addEventListener("mouseleave", this.
|
|
231
|
+
this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
|
|
232
|
+
this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
|
|
214
233
|
}
|
|
215
234
|
}
|
|
216
235
|
updateConfig(newConfig) {
|
|
@@ -221,6 +240,7 @@
|
|
|
221
240
|
this.updatePixelsPerDay();
|
|
222
241
|
this.calculateFullTimeline();
|
|
223
242
|
}
|
|
243
|
+
this.updateLoadMoreConf();
|
|
224
244
|
this.updateDimensions();
|
|
225
245
|
this.render();
|
|
226
246
|
}
|
|
@@ -239,17 +259,18 @@
|
|
|
239
259
|
if (this.resizeObserver) {
|
|
240
260
|
this.resizeObserver.disconnect();
|
|
241
261
|
}
|
|
242
|
-
this.container.removeEventListener("scroll", this.
|
|
243
|
-
this.mainCanvas.removeEventListener("mousemove", this.
|
|
244
|
-
this.mainCanvas.removeEventListener("mouseleave", this.
|
|
262
|
+
this.container.removeEventListener("scroll", this.handleScroll);
|
|
263
|
+
this.mainCanvas.removeEventListener("mousemove", this.handleMouseMove);
|
|
264
|
+
this.mainCanvas.removeEventListener("mouseleave", this.handleMouseLeave);
|
|
245
265
|
this.container.remove();
|
|
246
266
|
}
|
|
247
267
|
calculateFullTimeline() {
|
|
268
|
+
const currentYear = this.today.getFullYear();
|
|
248
269
|
let minDate = new Date(9999, 0, 1);
|
|
249
270
|
let maxDate = new Date(1e3, 0, 1);
|
|
250
271
|
if (this.data.length === 0) {
|
|
251
272
|
minDate = /* @__PURE__ */ new Date();
|
|
252
|
-
maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(),
|
|
273
|
+
maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(), 60);
|
|
253
274
|
} else {
|
|
254
275
|
this.taskMap.forEach(({ task }) => {
|
|
255
276
|
const pStart = new Date(task.planStart);
|
|
@@ -265,8 +286,11 @@
|
|
|
265
286
|
});
|
|
266
287
|
}
|
|
267
288
|
this.minDate = minDate;
|
|
268
|
-
|
|
269
|
-
|
|
289
|
+
this.maxDate = maxDate;
|
|
290
|
+
const minYear = minDate.getFullYear();
|
|
291
|
+
const maxYear = maxDate.getFullYear();
|
|
292
|
+
minDate = DateUtils.addDays(minYear === 9999 ? new Date(currentYear, 0, 1) : minDate, -7);
|
|
293
|
+
maxDate = DateUtils.addDays(maxYear === 1e3 ? new Date(currentYear + 1, 0, 1) : maxDate, 14);
|
|
270
294
|
switch (this.config.viewMode) {
|
|
271
295
|
case "Year":
|
|
272
296
|
this.timelineStart = DateUtils.getStartOfYear(minDate);
|
|
@@ -305,7 +329,14 @@
|
|
|
305
329
|
this.updateDimensions();
|
|
306
330
|
this.render();
|
|
307
331
|
}
|
|
332
|
+
// Add this method to register the data loading callback
|
|
333
|
+
setOnDataLoadCallback(callback) {
|
|
334
|
+
this.onDataLoad = callback;
|
|
335
|
+
}
|
|
308
336
|
handleScroll(e) {
|
|
337
|
+
if (this.showTooltip) {
|
|
338
|
+
this.handleMouseLeave();
|
|
339
|
+
}
|
|
309
340
|
const target = e.target;
|
|
310
341
|
this.scrollLeft = target.scrollLeft;
|
|
311
342
|
this.scrollTop = target.scrollTop;
|
|
@@ -313,7 +344,134 @@
|
|
|
313
344
|
detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
|
|
314
345
|
});
|
|
315
346
|
this.container.dispatchEvent(event);
|
|
316
|
-
|
|
347
|
+
if (this.config.enabledLoadMore.length > 0) {
|
|
348
|
+
if (this.scrollLoadTimer !== null) {
|
|
349
|
+
clearTimeout(this.scrollLoadTimer);
|
|
350
|
+
this.scrollLoadTimer = null;
|
|
351
|
+
}
|
|
352
|
+
this.scrollLoadTimer = window.setTimeout(() => {
|
|
353
|
+
this.checkScrollLoad();
|
|
354
|
+
this.scrollLoadTimer = null;
|
|
355
|
+
}, 100);
|
|
356
|
+
}
|
|
357
|
+
requestAnimationFrame(() => {
|
|
358
|
+
this.scrolling = true;
|
|
359
|
+
this.render(true);
|
|
360
|
+
this.scrolling = false;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
// checkScrollLoad method
|
|
364
|
+
async checkScrollLoad() {
|
|
365
|
+
if (this.isLoadingData || !this.onDataLoad) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const scrollLeft = this.scrollLeft;
|
|
369
|
+
const scrollTop = this.scrollTop;
|
|
370
|
+
const viewportWidth = this.viewportWidth;
|
|
371
|
+
const viewportHeight = this.viewportHeight;
|
|
372
|
+
const totalWidth = this.totalWidth;
|
|
373
|
+
const totalHeight = this.totalHeight;
|
|
374
|
+
const atLeftEdge = scrollLeft <= 5;
|
|
375
|
+
const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
|
|
376
|
+
const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
|
|
377
|
+
try {
|
|
378
|
+
if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
|
|
379
|
+
await this.loadMoreData("left");
|
|
380
|
+
console.log("left-loadMoreData::", this.data);
|
|
381
|
+
} else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
|
|
382
|
+
await this.loadMoreData("right");
|
|
383
|
+
console.log("right-loadMoreData::", this.data);
|
|
384
|
+
} else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
|
|
385
|
+
await this.loadMoreData("bottom");
|
|
386
|
+
console.log("bottom-loadMoreData::", this.data);
|
|
387
|
+
}
|
|
388
|
+
} finally {
|
|
389
|
+
this.lastScrollLeft = scrollLeft;
|
|
390
|
+
this.lastScrollTop = scrollTop;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Add this method to reset scroll loading state
|
|
394
|
+
resetScrollLoadingState() {
|
|
395
|
+
this.hasMoreDataLeft = true;
|
|
396
|
+
this.hasMoreDataRight = true;
|
|
397
|
+
this.hasMoreDataBottom = true;
|
|
398
|
+
this.lastScrollLeft = 0;
|
|
399
|
+
this.lastScrollTop = 0;
|
|
400
|
+
if (this.scrollLoadTimer !== null) {
|
|
401
|
+
clearTimeout(this.scrollLoadTimer);
|
|
402
|
+
this.scrollLoadTimer = null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Add this new method to load additional data
|
|
406
|
+
async loadMoreData(direction) {
|
|
407
|
+
if (this.isLoadingData || !this.onDataLoad) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.isLoadingData = true;
|
|
411
|
+
try {
|
|
412
|
+
let newData = null;
|
|
413
|
+
switch (direction) {
|
|
414
|
+
case "left":
|
|
415
|
+
newData = await this.onDataLoad("left", { date: this.minDate });
|
|
416
|
+
if (newData && newData.length > 0) {
|
|
417
|
+
this.prependData(newData);
|
|
418
|
+
} else {
|
|
419
|
+
this.hasMoreDataLeft = false;
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
case "right":
|
|
423
|
+
newData = await this.onDataLoad("right", { date: this.maxDate });
|
|
424
|
+
if (newData && newData.length > 0) {
|
|
425
|
+
this.appendData(newData);
|
|
426
|
+
} else {
|
|
427
|
+
this.hasMoreDataRight = false;
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
case "bottom":
|
|
431
|
+
const currentRowCount = this.data.length;
|
|
432
|
+
newData = await this.onDataLoad("bottom", { offset: currentRowCount });
|
|
433
|
+
if (newData && newData.length > 0) {
|
|
434
|
+
this.appendRows(newData);
|
|
435
|
+
} else {
|
|
436
|
+
this.hasMoreDataBottom = false;
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
if (newData && newData.length > 0) {
|
|
441
|
+
this.buildTaskMap();
|
|
442
|
+
this.calculateFullTimeline();
|
|
443
|
+
this.updateDimensions();
|
|
444
|
+
this.render();
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.error("Error loading additional data:", error);
|
|
448
|
+
} finally {
|
|
449
|
+
this.isLoadingData = false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Add this method to append data to the right
|
|
453
|
+
appendData(newData) {
|
|
454
|
+
newData.forEach((newRow, index) => {
|
|
455
|
+
if (index < this.data.length) {
|
|
456
|
+
this.data[index].tasks = [...this.data[index].tasks, ...newRow.tasks];
|
|
457
|
+
} else {
|
|
458
|
+
this.data.push(newRow);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// Add this method to prepend data to the left
|
|
463
|
+
prependData(newData) {
|
|
464
|
+
newData.forEach((newRow, index) => {
|
|
465
|
+
if (index < this.data.length) {
|
|
466
|
+
this.data[index].tasks = [...newRow.tasks, ...this.data[index].tasks];
|
|
467
|
+
} else {
|
|
468
|
+
this.data.push(newRow);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
// Add this method to append rows to the bottom
|
|
473
|
+
appendRows(newData) {
|
|
474
|
+
this.data.push(...newData);
|
|
317
475
|
}
|
|
318
476
|
setScrollTop(scrollTop) {
|
|
319
477
|
if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
|
|
@@ -327,10 +485,14 @@
|
|
|
327
485
|
}
|
|
328
486
|
updateDimensions() {
|
|
329
487
|
const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
this.
|
|
333
|
-
|
|
488
|
+
const newTotalWidth = totalDays * this.pixelsPerDay;
|
|
489
|
+
const newTotalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
|
|
490
|
+
if (this.totalWidth !== newTotalWidth || this.totalHeight !== newTotalHeight) {
|
|
491
|
+
this.totalWidth = newTotalWidth;
|
|
492
|
+
this.totalHeight = newTotalHeight;
|
|
493
|
+
this.scrollDummy.style.width = `${this.totalWidth}px`;
|
|
494
|
+
this.scrollDummy.style.height = `${this.totalHeight}px`;
|
|
495
|
+
}
|
|
334
496
|
}
|
|
335
497
|
setupCanvas(canvas, width, height) {
|
|
336
498
|
canvas.width = width * this.devicePixelRatio;
|
|
@@ -348,13 +510,13 @@
|
|
|
348
510
|
const y = i * this.config.rowHeight;
|
|
349
511
|
row.tasks.forEach((task) => {
|
|
350
512
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
351
|
-
const x_plan_end = this.dateToX(DateUtils.addDays(
|
|
513
|
+
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
352
514
|
let x_actual_start = null, x_actual_end = null;
|
|
353
515
|
if (task.actualStart) {
|
|
354
516
|
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
355
517
|
}
|
|
356
518
|
if (task.actualEnd) {
|
|
357
|
-
x_actual_end = this.dateToX(DateUtils.addDays(
|
|
519
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
358
520
|
}
|
|
359
521
|
this.taskPositions.set(task.id, {
|
|
360
522
|
x_plan_start,
|
|
@@ -380,9 +542,11 @@
|
|
|
380
542
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
381
543
|
}
|
|
382
544
|
}
|
|
383
|
-
render() {
|
|
545
|
+
render(scrolling = false) {
|
|
384
546
|
this.updateVirtualRanges();
|
|
385
|
-
|
|
547
|
+
if (!scrolling) {
|
|
548
|
+
this.calculateAllTaskPositions();
|
|
549
|
+
}
|
|
386
550
|
this.renderHeader();
|
|
387
551
|
this.renderMain();
|
|
388
552
|
}
|
|
@@ -392,82 +556,132 @@
|
|
|
392
556
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
393
557
|
ctx.save();
|
|
394
558
|
ctx.translate(-this.scrollLeft, 0);
|
|
395
|
-
ctx.fillStyle =
|
|
559
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
396
560
|
ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
|
|
397
561
|
ctx.textBaseline = "middle";
|
|
398
562
|
ctx.textRendering = "optimizeLegibility";
|
|
399
563
|
let currentDate = new Date(this.visibleDateRange.start);
|
|
400
564
|
currentDate = this.getIterationStartDate(currentDate);
|
|
401
|
-
|
|
402
|
-
|
|
565
|
+
const visibleBlocks = [];
|
|
566
|
+
let calcDate = new Date(currentDate);
|
|
567
|
+
while (calcDate <= this.visibleDateRange.end) {
|
|
403
568
|
let nextDate;
|
|
569
|
+
let upperText = "";
|
|
404
570
|
switch (this.config.viewMode) {
|
|
405
571
|
case "Day":
|
|
406
|
-
|
|
572
|
+
upperText = DateUtils.format(calcDate, "yyyy年MM月");
|
|
573
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
407
574
|
break;
|
|
408
575
|
case "Week":
|
|
409
|
-
|
|
576
|
+
const weekStart = DateUtils.getStartOfWeek(calcDate);
|
|
577
|
+
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
578
|
+
nextDate = DateUtils.addDays(weekStart, 7);
|
|
410
579
|
break;
|
|
411
580
|
case "Month":
|
|
412
|
-
|
|
581
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
582
|
+
nextDate = DateUtils.addMonths(calcDate, 1);
|
|
413
583
|
break;
|
|
414
584
|
case "Year":
|
|
415
|
-
|
|
585
|
+
if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
|
|
586
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
587
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
588
|
+
} else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
|
|
589
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
590
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
591
|
+
} else {
|
|
592
|
+
calcDate = DateUtils.addDays(calcDate, 1);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
default:
|
|
597
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
const startX = this.dateToX(calcDate);
|
|
601
|
+
const endX = this.dateToX(nextDate);
|
|
602
|
+
visibleBlocks.push({
|
|
603
|
+
startX,
|
|
604
|
+
endX,
|
|
605
|
+
text: upperText,
|
|
606
|
+
yPos: h * 0.35
|
|
607
|
+
});
|
|
608
|
+
calcDate = nextDate;
|
|
609
|
+
}
|
|
610
|
+
let currentDateForLower = new Date(currentDate);
|
|
611
|
+
while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
|
|
612
|
+
let nextDate;
|
|
613
|
+
switch (this.config.viewMode) {
|
|
614
|
+
case "Day":
|
|
615
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
616
|
+
break;
|
|
617
|
+
case "Week":
|
|
618
|
+
nextDate = DateUtils.addDays(currentDateForLower, 7);
|
|
619
|
+
break;
|
|
620
|
+
case "Month":
|
|
621
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
622
|
+
break;
|
|
623
|
+
case "Year":
|
|
624
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
416
625
|
break;
|
|
417
626
|
default:
|
|
418
|
-
nextDate = DateUtils.addDays(
|
|
627
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
419
628
|
break;
|
|
420
629
|
}
|
|
421
|
-
if (nextDate.getTime() ===
|
|
422
|
-
|
|
630
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
631
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
423
632
|
} else {
|
|
424
|
-
|
|
633
|
+
currentDateForLower = nextDate;
|
|
425
634
|
}
|
|
426
635
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
636
|
+
const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
|
|
637
|
+
ctx.fillStyle = "#333";
|
|
638
|
+
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
639
|
+
ctx.textAlign = "left";
|
|
640
|
+
groupedBlocks.forEach((group) => {
|
|
641
|
+
const visibleStart = Math.max(group.startX, this.scrollLeft);
|
|
642
|
+
const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
|
|
643
|
+
if (visibleEnd > visibleStart) {
|
|
644
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
645
|
+
ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
|
|
646
|
+
ctx.fillStyle = "#333";
|
|
647
|
+
ctx.fillText(group.text, visibleStart + 5, group.yPos);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
while (currentDateForLower <= this.visibleDateRange.end) {
|
|
651
|
+
const x = this.dateToX(currentDateForLower);
|
|
652
|
+
let lowerText = "";
|
|
653
|
+
let nextDate;
|
|
430
654
|
switch (this.config.viewMode) {
|
|
431
655
|
case "Day":
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
656
|
+
lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
|
|
657
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
435
658
|
break;
|
|
436
659
|
case "Week":
|
|
437
|
-
const weekStart = DateUtils.getStartOfWeek(
|
|
438
|
-
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
660
|
+
const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
|
|
439
661
|
lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
|
|
440
662
|
nextDate = DateUtils.addDays(weekStart, 7);
|
|
441
663
|
break;
|
|
442
664
|
case "Month":
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
nextDate = DateUtils.addMonths(currentDate, 1);
|
|
665
|
+
lowerText = `${currentDateForLower.getMonth() + 1}月`;
|
|
666
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
446
667
|
break;
|
|
447
668
|
case "Year":
|
|
448
|
-
if (
|
|
449
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
669
|
+
if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
|
|
450
670
|
lowerText = `上半年`;
|
|
451
|
-
nextDate = DateUtils.addMonths(
|
|
452
|
-
} else if (
|
|
453
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
671
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
672
|
+
} else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
|
|
454
673
|
lowerText = `下半年`;
|
|
455
|
-
nextDate = DateUtils.addMonths(
|
|
674
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
456
675
|
} else {
|
|
457
|
-
|
|
676
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
458
677
|
continue;
|
|
459
678
|
}
|
|
460
679
|
break;
|
|
680
|
+
default:
|
|
681
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
682
|
+
break;
|
|
461
683
|
}
|
|
462
684
|
const unitWidth = this.dateToX(nextDate) - x;
|
|
463
|
-
if (upperText !== lastUpperText) {
|
|
464
|
-
ctx.fillStyle = "#333";
|
|
465
|
-
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
466
|
-
ctx.textRendering = "optimizeLegibility";
|
|
467
|
-
ctx.textAlign = "left";
|
|
468
|
-
ctx.fillText(upperText, x + 5, h * 0.35);
|
|
469
|
-
lastUpperText = upperText;
|
|
470
|
-
}
|
|
471
685
|
ctx.fillStyle = "#000412";
|
|
472
686
|
ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
473
687
|
ctx.textAlign = "center";
|
|
@@ -477,10 +691,10 @@
|
|
|
477
691
|
ctx.lineTo(x, h);
|
|
478
692
|
ctx.strokeStyle = "#e0e0e0";
|
|
479
693
|
ctx.stroke();
|
|
480
|
-
if (nextDate.getTime() ===
|
|
481
|
-
|
|
694
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
695
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
482
696
|
} else {
|
|
483
|
-
|
|
697
|
+
currentDateForLower = nextDate;
|
|
484
698
|
}
|
|
485
699
|
}
|
|
486
700
|
ctx.beginPath();
|
|
@@ -490,6 +704,22 @@
|
|
|
490
704
|
ctx.stroke();
|
|
491
705
|
ctx.restore();
|
|
492
706
|
}
|
|
707
|
+
// Helper method to group consecutive blocks with same text
|
|
708
|
+
groupConsecutiveBlocks(blocks) {
|
|
709
|
+
if (blocks.length === 0) return [];
|
|
710
|
+
const grouped = [];
|
|
711
|
+
let currentGroup = { ...blocks[0] };
|
|
712
|
+
for (let i = 1; i < blocks.length; i++) {
|
|
713
|
+
if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
|
|
714
|
+
currentGroup.endX = blocks[i].endX;
|
|
715
|
+
} else {
|
|
716
|
+
grouped.push(currentGroup);
|
|
717
|
+
currentGroup = { ...blocks[i] };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
grouped.push(currentGroup);
|
|
721
|
+
return grouped;
|
|
722
|
+
}
|
|
493
723
|
renderMain() {
|
|
494
724
|
const ctx = this.mainCtx;
|
|
495
725
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -685,7 +915,7 @@
|
|
|
685
915
|
const x = this.dateToX(this.today);
|
|
686
916
|
if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
|
|
687
917
|
ctx.strokeStyle = this.config.todayColor;
|
|
688
|
-
ctx.lineWidth =
|
|
918
|
+
ctx.lineWidth = 1;
|
|
689
919
|
ctx.beginPath();
|
|
690
920
|
ctx.moveTo(x, this.scrollTop);
|
|
691
921
|
ctx.lineTo(x, this.scrollTop + this.viewportHeight);
|
|
@@ -713,8 +943,8 @@
|
|
|
713
943
|
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
714
944
|
ctx.lineWidth = 4;
|
|
715
945
|
ctx.beginPath();
|
|
716
|
-
ctx.moveTo(pos.x_plan_start, taskY);
|
|
717
|
-
ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
|
|
946
|
+
ctx.moveTo(pos.x_plan_start + 2, taskY);
|
|
947
|
+
ctx.lineTo(pos.x_plan_start + width * percent_plan - 2, taskY);
|
|
718
948
|
ctx.stroke();
|
|
719
949
|
}
|
|
720
950
|
ctx.fillStyle = "#000";
|
|
@@ -750,6 +980,9 @@
|
|
|
750
980
|
return styles;
|
|
751
981
|
}
|
|
752
982
|
handleMouseMove(e) {
|
|
983
|
+
if (this.scrolling) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
753
986
|
const rect = this.mainCanvas.getBoundingClientRect();
|
|
754
987
|
const mouseX = e.clientX - rect.left;
|
|
755
988
|
const mouseY = e.clientY - rect.top;
|
|
@@ -762,15 +995,16 @@
|
|
|
762
995
|
if (this.config.tooltipFormat) {
|
|
763
996
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
764
997
|
if (!htmlStr) {
|
|
765
|
-
|
|
998
|
+
this.handleMouseLeave();
|
|
999
|
+
return;
|
|
766
1000
|
}
|
|
767
1001
|
this.tooltip.innerHTML = htmlStr;
|
|
768
1002
|
} else {
|
|
769
1003
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
770
|
-
const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(
|
|
1004
|
+
const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
|
|
771
1005
|
if (date >= pStart && date < pEnd) return true;
|
|
772
1006
|
if (task.actualStart) {
|
|
773
|
-
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(
|
|
1007
|
+
const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
|
|
774
1008
|
if (date >= aStart && date < aEnd) return true;
|
|
775
1009
|
}
|
|
776
1010
|
return false;
|
|
@@ -784,6 +1018,7 @@
|
|
|
784
1018
|
this.tooltip.innerHTML = html;
|
|
785
1019
|
}
|
|
786
1020
|
this.tooltip.style.display = "block";
|
|
1021
|
+
this.showTooltip = true;
|
|
787
1022
|
if (this.config.tooltipColor === "white") {
|
|
788
1023
|
this.tooltip.style.background = "#fff";
|
|
789
1024
|
this.tooltip.style.color = "#000";
|
|
@@ -813,6 +1048,7 @@
|
|
|
813
1048
|
}
|
|
814
1049
|
handleMouseLeave() {
|
|
815
1050
|
this.tooltip.style.display = "none";
|
|
1051
|
+
this.showTooltip = false;
|
|
816
1052
|
}
|
|
817
1053
|
/**
|
|
818
1054
|
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
@@ -824,17 +1060,29 @@
|
|
|
824
1060
|
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
825
1061
|
}
|
|
826
1062
|
/**
|
|
827
|
-
* scroll to specified date position, default to minDate
|
|
1063
|
+
* horizontal scroll to specified date position, default to minDate
|
|
828
1064
|
*
|
|
829
1065
|
* @param date
|
|
830
1066
|
*/
|
|
831
|
-
|
|
1067
|
+
horizontalScrollTo(date) {
|
|
832
1068
|
const startDate = date ? date : this.minDate;
|
|
833
1069
|
if (startDate) {
|
|
834
1070
|
const xPosition = this.dateToX(startDate);
|
|
835
1071
|
this.container.scrollTo({ left: xPosition - 80 });
|
|
836
1072
|
}
|
|
837
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* vertical scroll to specified position
|
|
1076
|
+
*
|
|
1077
|
+
* @param params
|
|
1078
|
+
*/
|
|
1079
|
+
verticalScrollTo(params) {
|
|
1080
|
+
if (params && (params.rowId || params.rowIndex)) {
|
|
1081
|
+
const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
|
|
1082
|
+
const yPosition = this.config.rowHeight * rowIndex;
|
|
1083
|
+
this.container.scrollTo({ top: yPosition - 80 });
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
838
1086
|
}
|
|
839
1087
|
exports2.DateUtils = DateUtils;
|
|
840
1088
|
exports2.GanttChart = GanttChart;
|