gantt-canvas-chart 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +344 -86
- package/dist/index.css +2 -2
- package/dist/index.d.ts +44 -9
- package/dist/index.es.js +344 -86
- package/dist/index.umd.js +344 -86
- package/package.json +1 -1
package/dist/index.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* gantt-canvas-chart v1.
|
|
2
|
+
* gantt-canvas-chart v1.3.1
|
|
3
3
|
* (c) 2025-present chandq
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -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,12 @@
|
|
|
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);
|
|
201
|
+
this.handleResize = this.handleResize.bind(this);
|
|
188
202
|
this.init();
|
|
189
203
|
}
|
|
190
204
|
init() {
|
|
@@ -194,6 +208,11 @@
|
|
|
194
208
|
this.setupEvents();
|
|
195
209
|
this.handleResize();
|
|
196
210
|
}
|
|
211
|
+
updateLoadMoreConf() {
|
|
212
|
+
this.hasMoreDataLeft = this.config.enabledLoadMore.includes("left");
|
|
213
|
+
this.hasMoreDataRight = this.config.enabledLoadMore.includes("right");
|
|
214
|
+
this.hasMoreDataBottom = this.config.enabledLoadMore.includes("bottom");
|
|
215
|
+
}
|
|
197
216
|
buildTaskMap() {
|
|
198
217
|
this.taskMap.clear();
|
|
199
218
|
this.data.forEach((row, rowIndex) => {
|
|
@@ -201,8 +220,7 @@
|
|
|
201
220
|
});
|
|
202
221
|
}
|
|
203
222
|
setupEvents() {
|
|
204
|
-
this.container.addEventListener("scroll", this.
|
|
205
|
-
this.handleResize = this.handleResize.bind(this);
|
|
223
|
+
this.container.addEventListener("scroll", this.handleScroll);
|
|
206
224
|
if (window.ResizeObserver) {
|
|
207
225
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
|
208
226
|
setTimeout(() => {
|
|
@@ -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,16 @@
|
|
|
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);
|
|
265
|
+
this.data = [];
|
|
266
|
+
this.taskMap.clear();
|
|
267
|
+
this.taskPositions.clear();
|
|
246
268
|
this.container.remove();
|
|
247
269
|
}
|
|
248
270
|
calculateFullTimeline() {
|
|
249
|
-
const currentYear =
|
|
271
|
+
const currentYear = this.today.getFullYear();
|
|
250
272
|
let minDate = new Date(9999, 0, 1);
|
|
251
273
|
let maxDate = new Date(1e3, 0, 1);
|
|
252
274
|
if (this.data.length === 0) {
|
|
@@ -310,7 +332,14 @@
|
|
|
310
332
|
this.updateDimensions();
|
|
311
333
|
this.render();
|
|
312
334
|
}
|
|
335
|
+
// Add this method to register the data loading callback
|
|
336
|
+
setOnDataLoadCallback(callback) {
|
|
337
|
+
this.onDataLoad = callback;
|
|
338
|
+
}
|
|
313
339
|
handleScroll(e) {
|
|
340
|
+
if (this.showTooltip) {
|
|
341
|
+
this.handleMouseLeave();
|
|
342
|
+
}
|
|
314
343
|
const target = e.target;
|
|
315
344
|
this.scrollLeft = target.scrollLeft;
|
|
316
345
|
this.scrollTop = target.scrollTop;
|
|
@@ -318,7 +347,131 @@
|
|
|
318
347
|
detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
|
|
319
348
|
});
|
|
320
349
|
this.container.dispatchEvent(event);
|
|
321
|
-
|
|
350
|
+
if (this.config.enabledLoadMore.length > 0) {
|
|
351
|
+
if (this.scrollLoadTimer !== null) {
|
|
352
|
+
clearTimeout(this.scrollLoadTimer);
|
|
353
|
+
this.scrollLoadTimer = null;
|
|
354
|
+
}
|
|
355
|
+
this.scrollLoadTimer = window.setTimeout(() => {
|
|
356
|
+
this.checkScrollLoad();
|
|
357
|
+
this.scrollLoadTimer = null;
|
|
358
|
+
}, 100);
|
|
359
|
+
}
|
|
360
|
+
requestAnimationFrame(() => {
|
|
361
|
+
this.scrolling = true;
|
|
362
|
+
this.render(true);
|
|
363
|
+
this.scrolling = false;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
// checkScrollLoad method
|
|
367
|
+
async checkScrollLoad() {
|
|
368
|
+
if (this.isLoadingData || !this.onDataLoad) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const scrollLeft = this.scrollLeft;
|
|
372
|
+
const scrollTop = this.scrollTop;
|
|
373
|
+
const viewportWidth = this.viewportWidth;
|
|
374
|
+
const viewportHeight = this.viewportHeight;
|
|
375
|
+
const totalWidth = this.totalWidth;
|
|
376
|
+
const totalHeight = this.totalHeight;
|
|
377
|
+
const atLeftEdge = scrollLeft <= 5;
|
|
378
|
+
const atRightEdge = scrollLeft + viewportWidth >= totalWidth - 5;
|
|
379
|
+
const atBottomEdge = scrollTop + viewportHeight >= totalHeight - 5;
|
|
380
|
+
try {
|
|
381
|
+
if (this.hasMoreDataLeft && atLeftEdge && scrollLeft < this.lastScrollLeft) {
|
|
382
|
+
await this.loadMoreData("left");
|
|
383
|
+
} else if (this.hasMoreDataRight && atRightEdge && scrollLeft > this.lastScrollLeft) {
|
|
384
|
+
await this.loadMoreData("right");
|
|
385
|
+
} else if (this.hasMoreDataBottom && atBottomEdge && scrollTop > this.lastScrollTop) {
|
|
386
|
+
await this.loadMoreData("bottom");
|
|
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,19 +510,41 @@
|
|
|
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;
|
|
515
|
+
let offset_x_plan_start = NaN, offset_x_plan_end = NaN;
|
|
516
|
+
let offset_x_actual_start = null, offset_x_actual_end = null;
|
|
517
|
+
let x_plan_width = 0;
|
|
518
|
+
let x_actual_width = 0;
|
|
519
|
+
const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
|
|
520
|
+
const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
|
|
521
|
+
if (x_plan_start && x_plan_end) {
|
|
522
|
+
x_plan_width = x_plan_end - x_plan_start;
|
|
523
|
+
offset_x_plan_start = x_plan_start + x_plan_width * offsetX;
|
|
524
|
+
x_plan_end && (offset_x_plan_end = offset_x_plan_start + x_plan_width * percent_plan);
|
|
525
|
+
}
|
|
358
526
|
if (task.actualStart) {
|
|
359
527
|
x_actual_start = this.dateToX(new Date(task.actualStart));
|
|
360
528
|
}
|
|
361
529
|
if (task.actualEnd) {
|
|
362
|
-
x_actual_end = this.dateToX(DateUtils.addDays(
|
|
530
|
+
x_actual_end = this.dateToX(DateUtils.addDays(task.actualEnd, 1));
|
|
531
|
+
}
|
|
532
|
+
if (x_actual_start) {
|
|
533
|
+
x_actual_width = (x_actual_end ? x_actual_end : this.dateToX(this.today)) - x_actual_start;
|
|
534
|
+
offset_x_actual_start = Math.round(x_actual_start + x_actual_width * offsetX_actual);
|
|
535
|
+
x_actual_end && (offset_x_actual_end = offset_x_actual_start + x_actual_width * percent_actual);
|
|
363
536
|
}
|
|
364
537
|
this.taskPositions.set(task.id, {
|
|
365
538
|
x_plan_start,
|
|
366
539
|
x_plan_end,
|
|
367
540
|
x_actual_start,
|
|
368
541
|
x_actual_end,
|
|
542
|
+
offset_x_plan_start,
|
|
543
|
+
offset_x_plan_end,
|
|
544
|
+
offset_x_actual_start,
|
|
545
|
+
offset_x_actual_end,
|
|
546
|
+
x_plan_width,
|
|
547
|
+
x_actual_width,
|
|
369
548
|
y: y + this.config.rowHeight * 0.5,
|
|
370
549
|
row: i
|
|
371
550
|
});
|
|
@@ -385,9 +564,11 @@
|
|
|
385
564
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
386
565
|
}
|
|
387
566
|
}
|
|
388
|
-
render() {
|
|
567
|
+
render(scrolling = false) {
|
|
389
568
|
this.updateVirtualRanges();
|
|
390
|
-
|
|
569
|
+
if (!scrolling) {
|
|
570
|
+
this.calculateAllTaskPositions();
|
|
571
|
+
}
|
|
391
572
|
this.renderHeader();
|
|
392
573
|
this.renderMain();
|
|
393
574
|
}
|
|
@@ -397,82 +578,132 @@
|
|
|
397
578
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
398
579
|
ctx.save();
|
|
399
580
|
ctx.translate(-this.scrollLeft, 0);
|
|
400
|
-
ctx.fillStyle =
|
|
581
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
401
582
|
ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
|
|
402
583
|
ctx.textBaseline = "middle";
|
|
403
584
|
ctx.textRendering = "optimizeLegibility";
|
|
404
585
|
let currentDate = new Date(this.visibleDateRange.start);
|
|
405
586
|
currentDate = this.getIterationStartDate(currentDate);
|
|
406
|
-
|
|
407
|
-
|
|
587
|
+
const visibleBlocks = [];
|
|
588
|
+
let calcDate = new Date(currentDate);
|
|
589
|
+
while (calcDate <= this.visibleDateRange.end) {
|
|
590
|
+
let nextDate;
|
|
591
|
+
let upperText = "";
|
|
592
|
+
switch (this.config.viewMode) {
|
|
593
|
+
case "Day":
|
|
594
|
+
upperText = DateUtils.format(calcDate, "yyyy年MM月");
|
|
595
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
596
|
+
break;
|
|
597
|
+
case "Week":
|
|
598
|
+
const weekStart = DateUtils.getStartOfWeek(calcDate);
|
|
599
|
+
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
600
|
+
nextDate = DateUtils.addDays(weekStart, 7);
|
|
601
|
+
break;
|
|
602
|
+
case "Month":
|
|
603
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
604
|
+
nextDate = DateUtils.addMonths(calcDate, 1);
|
|
605
|
+
break;
|
|
606
|
+
case "Year":
|
|
607
|
+
if (calcDate.getMonth() === 0 && calcDate.getDate() === 1) {
|
|
608
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
609
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
610
|
+
} else if (calcDate.getMonth() === 6 && calcDate.getDate() === 1) {
|
|
611
|
+
upperText = `${calcDate.getFullYear()}年`;
|
|
612
|
+
nextDate = DateUtils.addMonths(calcDate, 6);
|
|
613
|
+
} else {
|
|
614
|
+
calcDate = DateUtils.addDays(calcDate, 1);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
break;
|
|
618
|
+
default:
|
|
619
|
+
nextDate = DateUtils.addDays(calcDate, 1);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
const startX = this.dateToX(calcDate);
|
|
623
|
+
const endX = this.dateToX(nextDate);
|
|
624
|
+
visibleBlocks.push({
|
|
625
|
+
startX,
|
|
626
|
+
endX,
|
|
627
|
+
text: upperText,
|
|
628
|
+
yPos: h * 0.35
|
|
629
|
+
});
|
|
630
|
+
calcDate = nextDate;
|
|
631
|
+
}
|
|
632
|
+
let currentDateForLower = new Date(currentDate);
|
|
633
|
+
while (this.dateToX(currentDateForLower) < this.scrollLeft - this.pixelsPerDay * 7) {
|
|
408
634
|
let nextDate;
|
|
409
635
|
switch (this.config.viewMode) {
|
|
410
636
|
case "Day":
|
|
411
|
-
nextDate = DateUtils.addDays(
|
|
637
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
412
638
|
break;
|
|
413
639
|
case "Week":
|
|
414
|
-
nextDate = DateUtils.addDays(
|
|
640
|
+
nextDate = DateUtils.addDays(currentDateForLower, 7);
|
|
415
641
|
break;
|
|
416
642
|
case "Month":
|
|
417
|
-
nextDate = DateUtils.addMonths(
|
|
643
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
418
644
|
break;
|
|
419
645
|
case "Year":
|
|
420
|
-
nextDate = DateUtils.addMonths(
|
|
646
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
421
647
|
break;
|
|
422
648
|
default:
|
|
423
|
-
nextDate = DateUtils.addDays(
|
|
649
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
424
650
|
break;
|
|
425
651
|
}
|
|
426
|
-
if (nextDate.getTime() ===
|
|
427
|
-
|
|
652
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
653
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
428
654
|
} else {
|
|
429
|
-
|
|
655
|
+
currentDateForLower = nextDate;
|
|
430
656
|
}
|
|
431
657
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
658
|
+
const groupedBlocks = this.groupConsecutiveBlocks(visibleBlocks);
|
|
659
|
+
ctx.fillStyle = "#333";
|
|
660
|
+
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
661
|
+
ctx.textAlign = "left";
|
|
662
|
+
groupedBlocks.forEach((group) => {
|
|
663
|
+
const visibleStart = Math.max(group.startX, this.scrollLeft);
|
|
664
|
+
const visibleEnd = Math.min(group.endX, this.scrollLeft + this.viewportWidth);
|
|
665
|
+
if (visibleEnd > visibleStart) {
|
|
666
|
+
ctx.fillStyle = this.config.headerBgColor;
|
|
667
|
+
ctx.fillRect(visibleStart, 0, visibleEnd - visibleStart, h * 0.5);
|
|
668
|
+
ctx.fillStyle = "#333";
|
|
669
|
+
ctx.fillText(group.text, visibleStart + 5, group.yPos);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
while (currentDateForLower <= this.visibleDateRange.end) {
|
|
673
|
+
const x = this.dateToX(currentDateForLower);
|
|
674
|
+
let lowerText = "";
|
|
675
|
+
let nextDate;
|
|
435
676
|
switch (this.config.viewMode) {
|
|
436
677
|
case "Day":
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
nextDate = DateUtils.addDays(currentDate, 1);
|
|
678
|
+
lowerText = `${DateUtils.format(currentDateForLower, "d")} ${DateUtils.format(currentDateForLower, "W")}`;
|
|
679
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
440
680
|
break;
|
|
441
681
|
case "Week":
|
|
442
|
-
const weekStart = DateUtils.getStartOfWeek(
|
|
443
|
-
upperText = DateUtils.format(weekStart, "yyyy年MM月");
|
|
682
|
+
const weekStart = DateUtils.getStartOfWeek(currentDateForLower);
|
|
444
683
|
lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
|
|
445
684
|
nextDate = DateUtils.addDays(weekStart, 7);
|
|
446
685
|
break;
|
|
447
686
|
case "Month":
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
nextDate = DateUtils.addMonths(currentDate, 1);
|
|
687
|
+
lowerText = `${currentDateForLower.getMonth() + 1}月`;
|
|
688
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 1);
|
|
451
689
|
break;
|
|
452
690
|
case "Year":
|
|
453
|
-
if (
|
|
454
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
691
|
+
if (currentDateForLower.getMonth() === 0 && currentDateForLower.getDate() === 1) {
|
|
455
692
|
lowerText = `上半年`;
|
|
456
|
-
nextDate = DateUtils.addMonths(
|
|
457
|
-
} else if (
|
|
458
|
-
upperText = `${currentDate.getFullYear()}年`;
|
|
693
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
694
|
+
} else if (currentDateForLower.getMonth() === 6 && currentDateForLower.getDate() === 1) {
|
|
459
695
|
lowerText = `下半年`;
|
|
460
|
-
nextDate = DateUtils.addMonths(
|
|
696
|
+
nextDate = DateUtils.addMonths(currentDateForLower, 6);
|
|
461
697
|
} else {
|
|
462
|
-
|
|
698
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
463
699
|
continue;
|
|
464
700
|
}
|
|
465
701
|
break;
|
|
702
|
+
default:
|
|
703
|
+
nextDate = DateUtils.addDays(currentDateForLower, 1);
|
|
704
|
+
break;
|
|
466
705
|
}
|
|
467
706
|
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
707
|
ctx.fillStyle = "#000412";
|
|
477
708
|
ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
478
709
|
ctx.textAlign = "center";
|
|
@@ -482,10 +713,10 @@
|
|
|
482
713
|
ctx.lineTo(x, h);
|
|
483
714
|
ctx.strokeStyle = "#e0e0e0";
|
|
484
715
|
ctx.stroke();
|
|
485
|
-
if (nextDate.getTime() ===
|
|
486
|
-
|
|
716
|
+
if (nextDate.getTime() === currentDateForLower.getTime()) {
|
|
717
|
+
currentDateForLower = DateUtils.addDays(currentDateForLower, 1);
|
|
487
718
|
} else {
|
|
488
|
-
|
|
719
|
+
currentDateForLower = nextDate;
|
|
489
720
|
}
|
|
490
721
|
}
|
|
491
722
|
ctx.beginPath();
|
|
@@ -495,6 +726,22 @@
|
|
|
495
726
|
ctx.stroke();
|
|
496
727
|
ctx.restore();
|
|
497
728
|
}
|
|
729
|
+
// Helper method to group consecutive blocks with same text
|
|
730
|
+
groupConsecutiveBlocks(blocks) {
|
|
731
|
+
if (blocks.length === 0) return [];
|
|
732
|
+
const grouped = [];
|
|
733
|
+
let currentGroup = { ...blocks[0] };
|
|
734
|
+
for (let i = 1; i < blocks.length; i++) {
|
|
735
|
+
if (blocks[i].text === currentGroup.text && Math.abs(blocks[i].startX - currentGroup.endX) < 1) {
|
|
736
|
+
currentGroup.endX = blocks[i].endX;
|
|
737
|
+
} else {
|
|
738
|
+
grouped.push(currentGroup);
|
|
739
|
+
currentGroup = { ...blocks[i] };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
grouped.push(currentGroup);
|
|
743
|
+
return grouped;
|
|
744
|
+
}
|
|
498
745
|
renderMain() {
|
|
499
746
|
const ctx = this.mainCtx;
|
|
500
747
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -542,9 +789,9 @@
|
|
|
542
789
|
if (!fromPos) return;
|
|
543
790
|
const fromRowIndex = this.taskMap.get(depId).row;
|
|
544
791
|
const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
|
|
545
|
-
const fromX = Math.max(fromPos.
|
|
792
|
+
const fromX = Math.max(fromPos.offset_x_plan_end, fromPos.offset_x_actual_end || fromPos.offset_x_plan_end);
|
|
546
793
|
const fromY = fromPos.y;
|
|
547
|
-
const toX = Math.min(toPos.
|
|
794
|
+
const toX = Math.min(toPos.offset_x_plan_start, toPos.offset_x_actual_start || toPos.offset_x_plan_start);
|
|
548
795
|
const toY = toPos.y;
|
|
549
796
|
ctx.beginPath();
|
|
550
797
|
if (isAdjacent) {
|
|
@@ -699,42 +946,35 @@
|
|
|
699
946
|
}
|
|
700
947
|
drawTask(ctx, task, y, pos) {
|
|
701
948
|
const offset = 4;
|
|
702
|
-
const width = pos.x_plan_end - pos.x_plan_start;
|
|
703
949
|
const taskY = y + this.config.rowHeight * 0.15 + offset;
|
|
704
950
|
const taskHeight = this.config.rowHeight * 0.7 - offset;
|
|
705
951
|
const textY = y + this.config.rowHeight / 2 + offset;
|
|
706
952
|
const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
|
|
707
|
-
const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
|
|
708
953
|
if (this.config.showActual && pos.x_actual_start) {
|
|
709
954
|
ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
|
|
710
|
-
|
|
711
|
-
pos.x_actual_start += aWidth * offsetX_actual;
|
|
712
|
-
pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
|
|
713
|
-
ctx.fillRect(Math.round(pos.x_actual_start), Math.round(taskY + 2), Math.round(aWidth * percent_actual), Math.round(taskHeight - 2));
|
|
955
|
+
ctx.fillRect(pos.offset_x_actual_start, Math.round(taskY + 2), Math.round(pos.x_actual_width * percent_actual), Math.round(taskHeight - 2));
|
|
714
956
|
}
|
|
715
957
|
if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
|
|
716
958
|
ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
|
|
717
|
-
pos.x_plan_start += width * offsetX;
|
|
718
|
-
pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
|
|
719
959
|
ctx.lineWidth = 4;
|
|
720
960
|
ctx.beginPath();
|
|
721
|
-
ctx.moveTo(pos.
|
|
722
|
-
ctx.lineTo(pos.
|
|
961
|
+
ctx.moveTo(pos.offset_x_plan_start + offset / 2, taskY);
|
|
962
|
+
ctx.lineTo(pos.offset_x_plan_end - offset / 2, taskY);
|
|
723
963
|
ctx.stroke();
|
|
724
964
|
}
|
|
725
965
|
ctx.fillStyle = "#000";
|
|
726
966
|
if (this.config.showLeftRemark && task.leftRemark) {
|
|
727
967
|
ctx.textAlign = "right";
|
|
728
|
-
ctx.fillText(task.leftRemark, Math.round(Math.min(...[pos.
|
|
968
|
+
ctx.fillText(task.leftRemark, Math.round(Math.min(...[pos.offset_x_plan_start, pos.offset_x_actual_start].filter((val) => val !== null && val !== void 0)) - 8), Math.round(textY));
|
|
729
969
|
}
|
|
730
970
|
if (this.config.showRightRemark && task.rightRemark) {
|
|
731
971
|
ctx.textAlign = "left";
|
|
732
|
-
ctx.fillText(task.rightRemark, Math.round(Math.max(...[pos.
|
|
972
|
+
ctx.fillText(task.rightRemark, Math.round(Math.max(...[pos.offset_x_plan_end, pos.offset_x_actual_end].filter((val) => val !== null && val !== void 0)) + 8), Math.round(textY));
|
|
733
973
|
}
|
|
734
974
|
if (this.config.showCenterRemark && task.centerRemark) {
|
|
735
|
-
const centerX = pos.
|
|
975
|
+
const centerX = pos.offset_x_actual_start + (pos.offset_x_actual_end - pos.offset_x_actual_start) / 2;
|
|
736
976
|
ctx.textAlign = "center";
|
|
737
|
-
ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.
|
|
977
|
+
ctx.fillText(task.centerRemark, Math.round(centerX), Math.round(textY), Math.round(pos.offset_x_actual_end - pos.offset_x_actual_start));
|
|
738
978
|
}
|
|
739
979
|
}
|
|
740
980
|
getTaskStyles(task) {
|
|
@@ -755,6 +995,9 @@
|
|
|
755
995
|
return styles;
|
|
756
996
|
}
|
|
757
997
|
handleMouseMove(e) {
|
|
998
|
+
if (this.scrolling) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
758
1001
|
const rect = this.mainCanvas.getBoundingClientRect();
|
|
759
1002
|
const mouseX = e.clientX - rect.left;
|
|
760
1003
|
const mouseY = e.clientY - rect.top;
|
|
@@ -767,15 +1010,16 @@
|
|
|
767
1010
|
if (this.config.tooltipFormat) {
|
|
768
1011
|
const htmlStr = this.config.tooltipFormat(row, date, this.config);
|
|
769
1012
|
if (!htmlStr) {
|
|
770
|
-
|
|
1013
|
+
this.handleMouseLeave();
|
|
1014
|
+
return;
|
|
771
1015
|
}
|
|
772
1016
|
this.tooltip.innerHTML = htmlStr;
|
|
773
1017
|
} else {
|
|
774
1018
|
const overlappingTasks = row.tasks.filter((task) => {
|
|
775
|
-
const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(
|
|
1019
|
+
const pStart = new Date(task.planStart).setHours(0, 0, 0, 0), pEnd = DateUtils.addDays(task.planEnd, 1);
|
|
776
1020
|
if (date >= pStart && date < pEnd) return true;
|
|
777
1021
|
if (task.actualStart) {
|
|
778
|
-
const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(
|
|
1022
|
+
const aStart = new Date(task.actualStart).setHours(0, 0, 0, 0), aEnd = DateUtils.addDays(task.actualEnd, 1);
|
|
779
1023
|
if (date >= aStart && date < aEnd) return true;
|
|
780
1024
|
}
|
|
781
1025
|
return false;
|
|
@@ -789,6 +1033,7 @@
|
|
|
789
1033
|
this.tooltip.innerHTML = html;
|
|
790
1034
|
}
|
|
791
1035
|
this.tooltip.style.display = "block";
|
|
1036
|
+
this.showTooltip = true;
|
|
792
1037
|
if (this.config.tooltipColor === "white") {
|
|
793
1038
|
this.tooltip.style.background = "#fff";
|
|
794
1039
|
this.tooltip.style.color = "#000";
|
|
@@ -818,6 +1063,7 @@
|
|
|
818
1063
|
}
|
|
819
1064
|
handleMouseLeave() {
|
|
820
1065
|
this.tooltip.style.display = "none";
|
|
1066
|
+
this.showTooltip = false;
|
|
821
1067
|
}
|
|
822
1068
|
/**
|
|
823
1069
|
* 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
|
|
@@ -829,17 +1075,29 @@
|
|
|
829
1075
|
return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
|
|
830
1076
|
}
|
|
831
1077
|
/**
|
|
832
|
-
* scroll to specified date position, default to minDate
|
|
1078
|
+
* horizontal scroll to specified date position, default to minDate
|
|
833
1079
|
*
|
|
834
1080
|
* @param date
|
|
835
1081
|
*/
|
|
836
|
-
|
|
1082
|
+
horizontalScrollTo(date) {
|
|
837
1083
|
const startDate = date ? date : this.minDate;
|
|
838
1084
|
if (startDate) {
|
|
839
1085
|
const xPosition = this.dateToX(startDate);
|
|
840
1086
|
this.container.scrollTo({ left: xPosition - 80 });
|
|
841
1087
|
}
|
|
842
1088
|
}
|
|
1089
|
+
/**
|
|
1090
|
+
* vertical scroll to specified position
|
|
1091
|
+
*
|
|
1092
|
+
* @param params
|
|
1093
|
+
*/
|
|
1094
|
+
verticalScrollTo(params) {
|
|
1095
|
+
if (params && (params.rowId || params.rowIndex)) {
|
|
1096
|
+
const rowIndex = params.rowIndex ? params.rowIndex : this.data.findIndex((row) => row.id === params.rowId);
|
|
1097
|
+
const yPosition = this.config.rowHeight * rowIndex;
|
|
1098
|
+
this.container.scrollTo({ top: yPosition - 80 });
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
843
1101
|
}
|
|
844
1102
|
exports2.DateUtils = DateUtils;
|
|
845
1103
|
exports2.GanttChart = GanttChart;
|