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.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,6 +93,8 @@
|
|
|
93
93
|
mainCanvas;
|
|
94
94
|
scrollDummy;
|
|
95
95
|
tooltip;
|
|
96
|
+
scrolling;
|
|
97
|
+
showTooltip;
|
|
96
98
|
headerCtx;
|
|
97
99
|
mainCtx;
|
|
98
100
|
timelineStart;
|
|
@@ -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;
|
|
@@ -181,10 +193,11 @@
|
|
|
181
193
|
this.totalHeight = 0;
|
|
182
194
|
this.taskPositions = /* @__PURE__ */ new Map();
|
|
183
195
|
this.taskMap = /* @__PURE__ */ new Map();
|
|
184
|
-
this.
|
|
185
|
-
this.
|
|
186
|
-
this.
|
|
187
|
-
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);
|
|
188
201
|
this.init();
|
|
189
202
|
}
|
|
190
203
|
init() {
|
|
@@ -194,6 +207,11 @@
|
|
|
194
207
|
this.setupEvents();
|
|
195
208
|
this.handleResize();
|
|
196
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
|
+
}
|
|
197
215
|
buildTaskMap() {
|
|
198
216
|
this.taskMap.clear();
|
|
199
217
|
this.data.forEach((row, rowIndex) => {
|
|
@@ -201,7 +219,7 @@
|
|
|
201
219
|
});
|
|
202
220
|
}
|
|
203
221
|
setupEvents() {
|
|
204
|
-
this.container.addEventListener("scroll", this.
|
|
222
|
+
this.container.addEventListener("scroll", this.handleScroll);
|
|
205
223
|
this.handleResize = this.handleResize.bind(this);
|
|
206
224
|
if (window.ResizeObserver) {
|
|
207
225
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
@@ -210,8 +228,8 @@
|
|
|
210
228
|
}, 100);
|
|
211
229
|
}
|
|
212
230
|
if (this.config.showTooltip) {
|
|
213
|
-
this.mainCanvas.addEventListener("mousemove", this.
|
|
214
|
-
this.mainCanvas.addEventListener("mouseleave", this.
|
|
231
|
+
this.mainCanvas.addEventListener("mousemove", this.handleMouseMove);
|
|
232
|
+
this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave);
|
|
215
233
|
}
|
|
216
234
|
}
|
|
217
235
|
updateConfig(newConfig) {
|
|
@@ -222,6 +240,7 @@
|
|
|
222
240
|
this.updatePixelsPerDay();
|
|
223
241
|
this.calculateFullTimeline();
|
|
224
242
|
}
|
|
243
|
+
this.updateLoadMoreConf();
|
|
225
244
|
this.updateDimensions();
|
|
226
245
|
this.render();
|
|
227
246
|
}
|
|
@@ -240,13 +259,13 @@
|
|
|
240
259
|
if (this.resizeObserver) {
|
|
241
260
|
this.resizeObserver.disconnect();
|
|
242
261
|
}
|
|
243
|
-
this.container.removeEventListener("scroll", this.
|
|
244
|
-
this.mainCanvas.removeEventListener("mousemove", this.
|
|
245
|
-
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);
|
|
246
265
|
this.container.remove();
|
|
247
266
|
}
|
|
248
267
|
calculateFullTimeline() {
|
|
249
|
-
const currentYear =
|
|
268
|
+
const currentYear = this.today.getFullYear();
|
|
250
269
|
let minDate = new Date(9999, 0, 1);
|
|
251
270
|
let maxDate = new Date(1e3, 0, 1);
|
|
252
271
|
if (this.data.length === 0) {
|
|
@@ -310,7 +329,14 @@
|
|
|
310
329
|
this.updateDimensions();
|
|
311
330
|
this.render();
|
|
312
331
|
}
|
|
332
|
+
// Add this method to register the data loading callback
|
|
333
|
+
setOnDataLoadCallback(callback) {
|
|
334
|
+
this.onDataLoad = callback;
|
|
335
|
+
}
|
|
313
336
|
handleScroll(e) {
|
|
337
|
+
if (this.showTooltip) {
|
|
338
|
+
this.handleMouseLeave();
|
|
339
|
+
}
|
|
314
340
|
const target = e.target;
|
|
315
341
|
this.scrollLeft = target.scrollLeft;
|
|
316
342
|
this.scrollTop = target.scrollTop;
|
|
@@ -318,7 +344,134 @@
|
|
|
318
344
|
detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
|
|
319
345
|
});
|
|
320
346
|
this.container.dispatchEvent(event);
|
|
321
|
-
|
|
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);
|
|
322
475
|
}
|
|
323
476
|
setScrollTop(scrollTop) {
|
|
324
477
|
if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
|
|
@@ -332,10 +485,14 @@
|
|
|
332
485
|
}
|
|
333
486
|
updateDimensions() {
|
|
334
487
|
const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
this.
|
|
338
|
-
|
|
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
|
+
}
|
|
339
496
|
}
|
|
340
497
|
setupCanvas(canvas, width, height) {
|
|
341
498
|
canvas.width = width * this.devicePixelRatio;
|
|
@@ -353,13 +510,13 @@
|
|
|
353
510
|
const y = i * this.config.rowHeight;
|
|
354
511
|
row.tasks.forEach((task) => {
|
|
355
512
|
const x_plan_start = this.dateToX(new Date(task.planStart));
|
|
356
|
-
const x_plan_end = this.dateToX(DateUtils.addDays(
|
|
513
|
+
const x_plan_end = this.dateToX(DateUtils.addDays(task.planEnd, 1));
|
|
357
514
|
let x_actual_start = null, x_actual_end = null;
|
|
358
515
|
if (task.actualStart) {
|
|
359
516
|
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
360
517
|
}
|
|
361
518
|
if (task.actualEnd) {
|
|
362
|
-
x_actual_end = this.dateToX(DateUtils.addDays(
|
|
519
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
363
520
|
}
|
|
364
521
|
this.taskPositions.set(task.id, {
|
|
365
522
|
x_plan_start,
|
|
@@ -385,9 +542,11 @@
|
|
|
385
542
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
386
543
|
}
|
|
387
544
|
}
|
|
388
|
-
render() {
|
|
545
|
+
render(scrolling = false) {
|
|
389
546
|
this.updateVirtualRanges();
|
|
390
|
-
|
|
547
|
+
if (!scrolling) {
|
|
548
|
+
this.calculateAllTaskPositions();
|
|
549
|
+
}
|
|
391
550
|
this.renderHeader();
|
|
392
551
|
this.renderMain();
|
|
393
552
|
}
|
|
@@ -397,82 +556,132 @@
|
|
|
397
556
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
398
557
|
ctx.save();
|
|
399
558
|
ctx.translate(-this.scrollLeft, 0);
|
|
400
|
-
ctx.fillStyle =
|
|
559
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
401
560
|
ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
|
|
402
561
|
ctx.textBaseline = "middle";
|
|
403
562
|
ctx.textRendering = "optimizeLegibility";
|
|
404
563
|
let currentDate = new Date(this.visibleDateRange.start);
|
|
405
564
|
currentDate = this.getIterationStartDate(currentDate);
|
|
406
|
-
|
|
407
|
-
|
|
565
|
+
const visibleBlocks = [];
|
|
566
|
+
let calcDate = new Date(currentDate);
|
|
567
|
+
while (calcDate <= this.visibleDateRange.end) {
|
|
408
568
|
let nextDate;
|
|
569
|
+
let upperText = "";
|
|
409
570
|
switch (this.config.viewMode) {
|
|
410
571
|
case "Day":
|
|
411
|
-
|
|
572
|
+
upperText = DateUtils.format(calcDate, "yyyy年MM月");
|
|
573
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
412
574
|
break;
|
|
413
575
|
case "Week":
|
|
414
|
-
|
|
576
|
+
const weekStart = DateUtils.getStartOfWeek(calcDate);
|
|
577
|
+
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
578
|
+
nextDate = DateUtils.addDays(weekStart, 7);
|
|
415
579
|
break;
|
|
416
580
|
case "Month":
|
|
417
|
-
|
|
581
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
582
|
+
nextDate = DateUtils.addMonths(calcDate, 1);
|
|
418
583
|
break;
|
|
419
584
|
case "Year":
|
|
420
|
-
|
|
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);
|
|
421
625
|
break;
|
|
422
626
|
default:
|
|
423
|
-
nextDate = DateUtils.addDays(
|
|
627
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
424
628
|
break;
|
|
425
629
|
}
|
|
426
|
-
if (nextDate.getTime() ===
|
|
427
|
-
|
|
630
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
631
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
428
632
|
} else {
|
|
429
|
-
|
|
633
|
+
currentDateForLower = nextDate;
|
|
430
634
|
}
|
|
431
635
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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;
|
|
435
654
|
switch (this.config.viewMode) {
|
|
436
655
|
case "Day":
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
656
|
+
lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
|
|
657
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
440
658
|
break;
|
|
441
659
|
case "Week":
|
|
442
|
-
const weekStart = DateUtils.getStartOfWeek(
|
|
443
|
-
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
660
|
+
const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
|
|
444
661
|
lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
|
|
445
662
|
nextDate = DateUtils.addDays(weekStart, 7);
|
|
446
663
|
break;
|
|
447
664
|
case "Month":
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
nextDate = DateUtils.addMonths(currentDate, 1);
|
|
665
|
+
lowerText = `${currentDateForLower.getMonth() + 1}月`;
|
|
666
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
451
667
|
break;
|
|
452
668
|
case "Year":
|
|
453
|
-
if (
|
|
454
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
669
|
+
if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
|
|
455
670
|
lowerText = `上半年`;
|
|
456
|
-
nextDate = DateUtils.addMonths(
|
|
457
|
-
} else if (
|
|
458
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
671
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
672
|
+
} else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
|
|
459
673
|
lowerText = `下半年`;
|
|
460
|
-
nextDate = DateUtils.addMonths(
|
|
674
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
461
675
|
} else {
|
|
462
|
-
|
|
676
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
463
677
|
continue;
|
|
464
678
|
}
|
|
465
679
|
break;
|
|
680
|
+
default:
|
|
681
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
682
|
+
break;
|
|
466
683
|
}
|
|
467
684
|
const unitWidth = this.dateToX(nextDate) - x;
|
|
468
|
-
if (upperText !== lastUpperText) {
|
|
469
|
-
ctx.fillStyle = "#333";
|
|
470
|
-
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
471
|
-
ctx.textRendering = "optimizeLegibility";
|
|
472
|
-
ctx.textAlign = "left";
|
|
473
|
-
ctx.fillText(upperText, x + 5, h * 0.35);
|
|
474
|
-
lastUpperText = upperText;
|
|
475
|
-
}
|
|
476
685
|
ctx.fillStyle = "#000412";
|
|
477
686
|
ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
478
687
|
ctx.textAlign = "center";
|
|
@@ -482,10 +691,10 @@
|
|
|
482
691
|
ctx.lineTo(x, h);
|
|
483
692
|
ctx.strokeStyle = "#e0e0e0";
|
|
484
693
|
ctx.stroke();
|
|
485
|
-
if (nextDate.getTime() ===
|
|
486
|
-
|
|
694
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
695
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
487
696
|
} else {
|
|
488
|
-
|
|
697
|
+
currentDateForLower = nextDate;
|
|
489
698
|
}
|
|
490
699
|
}
|
|
491
700
|
ctx.beginPath();
|
|
@@ -495,6 +704,22 @@
|
|
|
495
704
|
ctx.stroke();
|
|
496
705
|
ctx.restore();
|
|
497
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
|
+
}
|
|
498
723
|
renderMain() {
|
|
499
724
|
const ctx = this.mainCtx;
|
|
500
725
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -718,8 +943,8 @@
|
|
|
718
943
|
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
719
944
|
ctx.lineWidth = 4;
|
|
720
945
|
ctx.beginPath();
|
|
721
|
-
ctx.moveTo(pos.x_plan_start, taskY);
|
|
722
|
-
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);
|
|
723
948
|
ctx.stroke();
|
|
724
949
|
}
|
|
725
950
|
ctx.fillStyle = "#000";
|
|
@@ -755,6 +980,9 @@
|
|
|
755
980
|
return styles;
|
|
756
981
|
}
|
|
757
982
|
handleMouseMove(e) {
|
|
983
|
+
if (this.scrolling) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
758
986
|
const rect = this.mainCanvas.getBoundingClientRect();
|
|
759
987
|
const mouseX = e.clientX - rect.left;
|
|
760
988
|
const mouseY = e.clientY - rect.top;
|
|
@@ -767,15 +995,16 @@
|
|
|
767
995
|
if (this.config.tooltipFormat) {
|
|
768
996
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
769
997
|
if (!htmlStr) {
|
|
770
|
-
|
|
998
|
+
this.handleMouseLeave();
|
|
999
|
+
return;
|
|
771
1000
|
}
|
|
772
1001
|
this.tooltip.innerHTML = htmlStr;
|
|
773
1002
|
} else {
|
|
774
1003
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
775
|
-
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);
|
|
776
1005
|
if (date >= pStart && date < pEnd) return true;
|
|
777
1006
|
if (task.actualStart) {
|
|
778
|
-
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);
|
|
779
1008
|
if (date >= aStart && date < aEnd) return true;
|
|
780
1009
|
}
|
|
781
1010
|
return false;
|
|
@@ -789,6 +1018,7 @@
|
|
|
789
1018
|
this.tooltip.innerHTML = html;
|
|
790
1019
|
}
|
|
791
1020
|
this.tooltip.style.display = "block";
|
|
1021
|
+
this.showTooltip = true;
|
|
792
1022
|
if (this.config.tooltipColor === "white") {
|
|
793
1023
|
this.tooltip.style.background = "#fff";
|
|
794
1024
|
this.tooltip.style.color = "#000";
|
|
@@ -818,6 +1048,7 @@
|
|
|
818
1048
|
}
|
|
819
1049
|
handleMouseLeave() {
|
|
820
1050
|
this.tooltip.style.display = "none";
|
|
1051
|
+
this.showTooltip = false;
|
|
821
1052
|
}
|
|
822
1053
|
/**
|
|
823
1054
|
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
@@ -829,17 +1060,29 @@
|
|
|
829
1060
|
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
830
1061
|
}
|
|
831
1062
|
/**
|
|
832
|
-
* scroll to specified date position, default to minDate
|
|
1063
|
+
* horizontal scroll to specified date position, default to minDate
|
|
833
1064
|
*
|
|
834
1065
|
* @param date
|
|
835
1066
|
*/
|
|
836
|
-
|
|
1067
|
+
horizontalScrollTo(date) {
|
|
837
1068
|
const startDate = date ? date : this.minDate;
|
|
838
1069
|
if (startDate) {
|
|
839
1070
|
const xPosition = this.dateToX(startDate);
|
|
840
1071
|
this.container.scrollTo({ left: xPosition - 80 });
|
|
841
1072
|
}
|
|
842
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
|
+
}
|
|
843
1086
|
}
|
|
844
1087
|
exports2.DateUtils = DateUtils;
|
|
845
1088
|
exports2.GanttChart = GanttChart;
|