gantt-canvas-chart 1.0.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.
@@ -0,0 +1,790 @@
1
+ /*!
2
+ * gantt-canvas-chart v1.0.0
3
+ * (c) 2025-present chandq
4
+ * Released under the MIT License.
5
+ */
6
+
7
+ "use strict";
8
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
+ class DateUtils {
10
+ static ONE_DAY_MS = 24 * 60 * 60 * 1e3;
11
+ static format(date, format = "yyyy-MM-dd") {
12
+ const o = {
13
+ "M+": date.getMonth() + 1,
14
+ "d+": date.getDate(),
15
+ "h+": date.getHours(),
16
+ "m+": date.getMinutes(),
17
+ "s+": date.getSeconds(),
18
+ "q+": Math.floor((date.getMonth() + 3) / 3),
19
+ W: ["日", "一", "二", "三", "四", "五", "六"][date.getDay()]
20
+ };
21
+ if (/(y+)/.test(format))
22
+ format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
23
+ for (let k in o)
24
+ if (new RegExp("(" + k + ")").test(format))
25
+ format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
26
+ return format;
27
+ }
28
+ static addDays(date, days) {
29
+ const r = new Date(date);
30
+ r.setDate(r.getDate() + days);
31
+ return r;
32
+ }
33
+ static addMonths(date, months) {
34
+ const d = new Date(date);
35
+ d.setDate(1);
36
+ d.setMonth(d.getMonth() + months);
37
+ return d;
38
+ }
39
+ static addYears(date, years) {
40
+ const r = new Date(date);
41
+ r.setFullYear(r.getFullYear() + years);
42
+ return r;
43
+ }
44
+ static diffDays(date1, date2) {
45
+ return Math.round((new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()).getTime() - new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()).getTime()) / this.ONE_DAY_MS);
46
+ }
47
+ static diffDaysInclusive(date1, date2) {
48
+ return this.diffDays(date1, date2) + 1;
49
+ }
50
+ static getDaysInMonth(year, month) {
51
+ return new Date(year, month + 1, 0).getDate();
52
+ }
53
+ static getWeekNumber(d) {
54
+ d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
55
+ d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
56
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
57
+ return Math.ceil(((d - yearStart) / 864e5 + 1) / 7);
58
+ }
59
+ static getStartOfWeek(d) {
60
+ const date = new Date(d);
61
+ const day = date.getDay() || 7;
62
+ if (day !== 1)
63
+ date.setHours(-24 * (day - 1));
64
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
65
+ }
66
+ static getStartOfMonth(d) {
67
+ return new Date(d.getFullYear(), d.getMonth(), 1);
68
+ }
69
+ static getStartOfYear(d) {
70
+ return new Date(d.getFullYear(), 0, 1);
71
+ }
72
+ }
73
+ function firstValidValue(...args) {
74
+ for (let i = 0; i < args.length; i++) {
75
+ if (args[i] !== null && args[i] !== void 0) {
76
+ return args[i];
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ class GanttChart {
82
+ rootContainer;
83
+ container;
84
+ data;
85
+ config;
86
+ headerCanvas;
87
+ mainCanvas;
88
+ scrollDummy;
89
+ tooltip;
90
+ headerCtx;
91
+ mainCtx;
92
+ timelineStart;
93
+ timelineEnd;
94
+ pixelsPerDay;
95
+ scrollLeft;
96
+ scrollTop;
97
+ visibleDateRange;
98
+ today;
99
+ devicePixelRatio;
100
+ viewportWidth;
101
+ viewportHeight;
102
+ totalWidth;
103
+ totalHeight;
104
+ resizeObserver;
105
+ taskPositions;
106
+ taskMap;
107
+ constructor(rootContainer, data, config = {}) {
108
+ const container = document.createElement("div");
109
+ const scrollEl = document.createElement("div");
110
+ const headerCanvas = document.createElement("canvas");
111
+ const mainCanvas = document.createElement("canvas");
112
+ container.classList.add("__gantt-chart-container");
113
+ scrollEl.classList.add("__gantt-scroll-dummy");
114
+ headerCanvas.classList.add("__gantt-header-canvas");
115
+ mainCanvas.classList.add("__gantt-main-canvas");
116
+ container.appendChild(scrollEl);
117
+ container.appendChild(headerCanvas);
118
+ container.appendChild(mainCanvas);
119
+ rootContainer.classList.add("__gantt-chart-wrapper");
120
+ rootContainer.appendChild(container);
121
+ this.resizeObserver = null;
122
+ this.rootContainer = rootContainer;
123
+ this.container = container;
124
+ this.data = data;
125
+ this.config = {
126
+ viewMode: "Month",
127
+ rowHeight: 48,
128
+ headerHeight: 56,
129
+ showPlan: true,
130
+ showActual: true,
131
+ showRowLines: true,
132
+ showColLines: true,
133
+ showLeftRemark: false,
134
+ showRightRemark: false,
135
+ showCenterRemark: false,
136
+ showTooltip: true,
137
+ tooltipColor: "black",
138
+ offsetTop: 0,
139
+ offsetLeft: 0,
140
+ planBorderColor: "#caeed2",
141
+ actualBgColor: "#78c78f",
142
+ ...config
143
+ };
144
+ this.headerCanvas = headerCanvas;
145
+ this.mainCanvas = mainCanvas;
146
+ this.scrollDummy = scrollEl;
147
+ const tooltip = document.createElement("div");
148
+ tooltip.classList.add("__gantt-tooltip");
149
+ document.body.appendChild(tooltip);
150
+ this.tooltip = tooltip;
151
+ this.mainCanvas.style.top = `${this.config.headerHeight}px`;
152
+ this.headerCtx = this.headerCanvas.getContext("2d");
153
+ this.mainCtx = this.mainCanvas.getContext("2d");
154
+ this.timelineStart = /* @__PURE__ */ new Date();
155
+ this.timelineEnd = /* @__PURE__ */ new Date();
156
+ this.pixelsPerDay = 40;
157
+ this.scrollLeft = 0;
158
+ this.scrollTop = 0;
159
+ this.visibleDateRange = { start: /* @__PURE__ */ new Date(), end: /* @__PURE__ */ new Date() };
160
+ this.today = new Date((/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0));
161
+ this.devicePixelRatio = window.devicePixelRatio || 1;
162
+ this.viewportWidth = 0;
163
+ this.viewportHeight = 0;
164
+ this.totalWidth = 0;
165
+ this.totalHeight = 0;
166
+ this.taskPositions = /* @__PURE__ */ new Map();
167
+ this.taskMap = /* @__PURE__ */ new Map();
168
+ this.init();
169
+ }
170
+ init() {
171
+ this.buildTaskMap();
172
+ this.calculateFullTimeline();
173
+ this.updatePixelsPerDay();
174
+ this.setupEvents();
175
+ this.handleResize();
176
+ }
177
+ buildTaskMap() {
178
+ this.taskMap.clear();
179
+ this.data.forEach((row, rowIndex) => {
180
+ row.tasks.forEach((task) => this.taskMap.set(task.id, { row: rowIndex, task }));
181
+ });
182
+ }
183
+ setupEvents() {
184
+ this.container.addEventListener("scroll", this.handleScroll.bind(this));
185
+ this.handleResize = this.handleResize.bind(this);
186
+ if (window.ResizeObserver) {
187
+ this.resizeObserver = new ResizeObserver(this.handleResize);
188
+ setTimeout(() => {
189
+ this.resizeObserver.observe(this.rootContainer);
190
+ }, 100);
191
+ }
192
+ if (this.config.showTooltip) {
193
+ this.mainCanvas.addEventListener("mousemove", this.handleMouseMove.bind(this));
194
+ this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave.bind(this));
195
+ }
196
+ }
197
+ updateConfig(newConfig) {
198
+ Object.assign(this.config, newConfig);
199
+ if (newConfig.viewMode) {
200
+ this.container.scrollLeft = 0;
201
+ this.scrollLeft = 0;
202
+ this.updatePixelsPerDay();
203
+ this.calculateFullTimeline();
204
+ }
205
+ this.updateDimensions();
206
+ this.render();
207
+ }
208
+ setData(newData) {
209
+ this.data = newData;
210
+ this.buildTaskMap();
211
+ this.calculateFullTimeline();
212
+ this.updateDimensions();
213
+ this.render();
214
+ }
215
+ destroy() {
216
+ if (this.resizeObserver) {
217
+ this.resizeObserver.disconnect();
218
+ }
219
+ this.container.remove();
220
+ }
221
+ calculateFullTimeline() {
222
+ let minDate = new Date(9999, 0, 1);
223
+ let maxDate = new Date(1e3, 0, 1);
224
+ if (this.data.length === 0) {
225
+ minDate = /* @__PURE__ */ new Date();
226
+ maxDate = DateUtils.addDays(/* @__PURE__ */ new Date(), 30);
227
+ } else {
228
+ this.taskMap.forEach(({ task }) => {
229
+ const pStart = new Date(task.planStart);
230
+ const pEnd = new Date(task.planEnd);
231
+ if (pStart < minDate)
232
+ minDate = pStart;
233
+ if (pEnd > maxDate)
234
+ maxDate = pEnd;
235
+ if (task.actualStart) {
236
+ const aStart = new Date(task.actualStart);
237
+ const aEnd = new Date(task.actualEnd);
238
+ if (aStart < minDate)
239
+ minDate = aStart;
240
+ if (aEnd > maxDate)
241
+ maxDate = aEnd;
242
+ }
243
+ });
244
+ }
245
+ minDate = DateUtils.addDays(minDate, -7);
246
+ maxDate = DateUtils.addDays(maxDate, 14);
247
+ switch (this.config.viewMode) {
248
+ case "Year":
249
+ this.timelineStart = DateUtils.getStartOfYear(minDate);
250
+ this.timelineEnd = DateUtils.addDays(DateUtils.addYears(DateUtils.getStartOfYear(maxDate), 1), -1);
251
+ break;
252
+ case "Month":
253
+ this.timelineStart = DateUtils.getStartOfMonth(minDate);
254
+ this.timelineEnd = DateUtils.addDays(DateUtils.addMonths(DateUtils.getStartOfMonth(maxDate), 1), -1);
255
+ break;
256
+ case "Week":
257
+ this.timelineStart = DateUtils.getStartOfWeek(minDate);
258
+ this.timelineEnd = DateUtils.addDays(DateUtils.getStartOfWeek(maxDate), 7);
259
+ break;
260
+ case "Day":
261
+ default:
262
+ this.timelineStart = DateUtils.addDays(minDate, -3);
263
+ this.timelineEnd = DateUtils.addDays(maxDate, 3);
264
+ break;
265
+ }
266
+ }
267
+ updatePixelsPerDay() {
268
+ const viewFactors = { Day: 80, Week: 20, Month: 15, Year: 6 };
269
+ this.pixelsPerDay = viewFactors[this.config.viewMode];
270
+ }
271
+ dateToX(date) {
272
+ return DateUtils.diffDays(this.timelineStart, date) * this.pixelsPerDay;
273
+ }
274
+ xToDate(x) {
275
+ return DateUtils.addDays(this.timelineStart, x / this.pixelsPerDay);
276
+ }
277
+ handleResize() {
278
+ this.viewportWidth = this.container.clientWidth;
279
+ this.viewportHeight = this.container.clientHeight;
280
+ this.mainCanvas.style.top = `${this.config.headerHeight}px`;
281
+ this.setupCanvas(this.headerCanvas, this.viewportWidth, this.config.headerHeight);
282
+ this.setupCanvas(this.mainCanvas, this.viewportWidth, this.viewportHeight - this.config.headerHeight);
283
+ this.updateDimensions();
284
+ this.render();
285
+ }
286
+ handleScroll(e) {
287
+ const target = e.target;
288
+ this.scrollLeft = target.scrollLeft;
289
+ this.scrollTop = target.scrollTop;
290
+ const event = new CustomEvent("ganttscroll", {
291
+ detail: { scrollTop: this.scrollTop, scrollLeft: this.scrollLeft }
292
+ });
293
+ this.container.dispatchEvent(event);
294
+ requestAnimationFrame(() => this.render());
295
+ }
296
+ setScrollTop(scrollTop) {
297
+ if (this.scrollTop !== scrollTop)
298
+ this.container.scrollTop = scrollTop;
299
+ }
300
+ updateVirtualRanges() {
301
+ const buffer = 200;
302
+ this.visibleDateRange = {
303
+ start: this.xToDate(this.scrollLeft - buffer),
304
+ end: this.xToDate(this.scrollLeft + this.viewportWidth + buffer)
305
+ };
306
+ }
307
+ updateDimensions() {
308
+ const totalDays = DateUtils.diffDays(this.timelineStart, this.timelineEnd) + 1;
309
+ this.totalWidth = totalDays * this.pixelsPerDay;
310
+ this.totalHeight = this.data.length * this.config.rowHeight + this.config.headerHeight;
311
+ this.scrollDummy.style.width = `${this.totalWidth}px`;
312
+ this.scrollDummy.style.height = `${this.totalHeight}px`;
313
+ }
314
+ setupCanvas(canvas, width, height) {
315
+ canvas.width = width * this.devicePixelRatio;
316
+ canvas.height = height * this.devicePixelRatio;
317
+ canvas.style.width = `${width}px`;
318
+ canvas.style.height = `${height}px`;
319
+ const ctx = canvas.getContext("2d");
320
+ ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
321
+ }
322
+ calculateAllTaskPositions() {
323
+ this.taskPositions.clear();
324
+ for (let i = 0; i < this.data.length; i++) {
325
+ const row = this.data[i];
326
+ const y = i * this.config.rowHeight;
327
+ row.tasks.forEach((task) => {
328
+ const x_plan_start = this.dateToX(new Date(task.planStart));
329
+ const x_plan_end = this.dateToX(DateUtils.addDays(new Date(task.planEnd), 1));
330
+ let x_actual_start = null, x_actual_end = null;
331
+ if (task.actualStart) {
332
+ x_actual_start = this.dateToX(new Date(task.actualStart));
333
+ }
334
+ if (task.actualEnd) {
335
+ x_actual_end = this.dateToX(DateUtils.addDays(new Date(task.actualEnd), 1));
336
+ }
337
+ this.taskPositions.set(task.id, {
338
+ x_plan_start,
339
+ x_plan_end,
340
+ x_actual_start,
341
+ x_actual_end,
342
+ y: y + this.config.rowHeight * 0.5,
343
+ row: i
344
+ });
345
+ });
346
+ }
347
+ }
348
+ getIterationStartDate(date) {
349
+ switch (this.config.viewMode) {
350
+ case "Year":
351
+ return DateUtils.getStartOfYear(date);
352
+ case "Month":
353
+ return DateUtils.getStartOfMonth(date);
354
+ case "Week":
355
+ return DateUtils.getStartOfWeek(date);
356
+ case "Day":
357
+ default:
358
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
359
+ }
360
+ }
361
+ render() {
362
+ this.updateVirtualRanges();
363
+ this.calculateAllTaskPositions();
364
+ this.renderHeader();
365
+ this.renderMain();
366
+ }
367
+ renderHeader() {
368
+ const ctx = this.headerCtx;
369
+ const h = this.config.headerHeight;
370
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
371
+ ctx.save();
372
+ ctx.translate(-this.scrollLeft, 0);
373
+ ctx.fillStyle = "#f9f9f9";
374
+ ctx.fillRect(this.scrollLeft, 0, this.viewportWidth, h);
375
+ ctx.textBaseline = "middle";
376
+ let currentDate = new Date(this.visibleDateRange.start);
377
+ currentDate = this.getIterationStartDate(currentDate);
378
+ let lastUpperText = "";
379
+ while (this.dateToX(currentDate) < this.scrollLeft - this.pixelsPerDay * 7) {
380
+ let nextDate;
381
+ switch (this.config.viewMode) {
382
+ case "Day":
383
+ nextDate = DateUtils.addDays(currentDate, 1);
384
+ break;
385
+ case "Week":
386
+ nextDate = DateUtils.addDays(currentDate, 7);
387
+ break;
388
+ case "Month":
389
+ nextDate = DateUtils.addMonths(currentDate, 1);
390
+ break;
391
+ case "Year":
392
+ nextDate = DateUtils.addMonths(currentDate, 6);
393
+ break;
394
+ default:
395
+ nextDate = DateUtils.addDays(currentDate, 1);
396
+ break;
397
+ }
398
+ if (nextDate.getTime() === currentDate.getTime()) {
399
+ currentDate = DateUtils.addDays(currentDate, 1);
400
+ } else {
401
+ currentDate = nextDate;
402
+ }
403
+ }
404
+ while (currentDate <= this.visibleDateRange.end) {
405
+ const x = this.dateToX(currentDate);
406
+ let upperText = "", lowerText = "", nextDate;
407
+ switch (this.config.viewMode) {
408
+ case "Day":
409
+ upperText = DateUtils.format(currentDate, "yyyy年MM月");
410
+ lowerText = `${DateUtils.format(currentDate, "d")} ${DateUtils.format(currentDate, "W")}`;
411
+ nextDate = DateUtils.addDays(currentDate, 1);
412
+ break;
413
+ case "Week":
414
+ const weekStart = DateUtils.getStartOfWeek(currentDate);
415
+ upperText = DateUtils.format(weekStart, "yyyy年MM月");
416
+ lowerText = `第${DateUtils.getWeekNumber(weekStart)}周`;
417
+ nextDate = DateUtils.addDays(weekStart, 7);
418
+ break;
419
+ case "Month":
420
+ upperText = `${currentDate.getFullYear()}年`;
421
+ lowerText = `${currentDate.getMonth() + 1}月`;
422
+ nextDate = DateUtils.addMonths(currentDate, 1);
423
+ break;
424
+ case "Year":
425
+ if (currentDate.getMonth() === 0 && currentDate.getDate() === 1) {
426
+ upperText = `${currentDate.getFullYear()}年`;
427
+ lowerText = `上半年`;
428
+ nextDate = DateUtils.addMonths(currentDate, 6);
429
+ } else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1) {
430
+ upperText = `${currentDate.getFullYear()}年`;
431
+ lowerText = `下半年`;
432
+ nextDate = DateUtils.addMonths(currentDate, 6);
433
+ } else {
434
+ currentDate = DateUtils.addDays(currentDate, 1);
435
+ continue;
436
+ }
437
+ break;
438
+ }
439
+ const unitWidth = this.dateToX(nextDate) - x;
440
+ if (upperText !== lastUpperText) {
441
+ ctx.fillStyle = "#333";
442
+ ctx.font = "bold 13px sans-serif";
443
+ ctx.textAlign = "left";
444
+ ctx.fillText(upperText, x + 5, h * 0.35);
445
+ lastUpperText = upperText;
446
+ }
447
+ ctx.fillStyle = "#666";
448
+ ctx.font = "12px sans-serif";
449
+ ctx.textAlign = "center";
450
+ ctx.fillText(lowerText, x + unitWidth / 2, h * 0.7);
451
+ ctx.beginPath();
452
+ ctx.moveTo(x, h * 0.5);
453
+ ctx.lineTo(x, h);
454
+ ctx.strokeStyle = "#e0e0e0";
455
+ ctx.stroke();
456
+ if (nextDate.getTime() === currentDate.getTime()) {
457
+ currentDate = DateUtils.addDays(currentDate, 1);
458
+ } else {
459
+ currentDate = nextDate;
460
+ }
461
+ }
462
+ ctx.beginPath();
463
+ ctx.moveTo(this.scrollLeft, h - 0.5);
464
+ ctx.lineTo(this.scrollLeft + this.viewportWidth, h - 0.5);
465
+ ctx.strokeStyle = "#e0e0e0";
466
+ ctx.stroke();
467
+ ctx.restore();
468
+ }
469
+ renderMain() {
470
+ const ctx = this.mainCtx;
471
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
472
+ ctx.save();
473
+ ctx.translate(-this.scrollLeft, -this.scrollTop);
474
+ const { start: startDate, end: endDate } = this.visibleDateRange;
475
+ this.drawGrid(ctx, startDate, endDate);
476
+ this.drawToday(ctx);
477
+ this.drawAllDependencies(ctx);
478
+ this.drawAllTasks(ctx);
479
+ ctx.restore();
480
+ }
481
+ // Helper: Draw arrow
482
+ drawArrow(ctx, x, y, direction) {
483
+ const size = 6;
484
+ ctx.beginPath();
485
+ ctx.moveTo(x, y);
486
+ if (direction === "right") {
487
+ ctx.lineTo(x - size, y - size * 0.6);
488
+ ctx.lineTo(x - size, y + size * 0.6);
489
+ } else if (direction === "down") {
490
+ ctx.lineTo(x - size * 0.6, y - size);
491
+ ctx.lineTo(x + size * 0.6, y - size);
492
+ } else if (direction === "up") {
493
+ ctx.lineTo(x - size * 0.6, y + size);
494
+ ctx.lineTo(x + size * 0.6, y + size);
495
+ }
496
+ ctx.fillStyle = ctx.strokeStyle;
497
+ ctx.fill();
498
+ }
499
+ drawAllDependencies(ctx) {
500
+ const lineColor = "#64748b";
501
+ const lineWidth = 1;
502
+ ctx.strokeStyle = lineColor;
503
+ ctx.lineWidth = lineWidth;
504
+ ctx.lineCap = "round";
505
+ ctx.lineJoin = "round";
506
+ this.taskMap.forEach(({ task }) => {
507
+ if (!task.dependencies || task.dependencies.length === 0)
508
+ return;
509
+ const toPos = this.taskPositions.get(task.id);
510
+ if (!toPos)
511
+ return;
512
+ const toRowIndex = this.taskMap.get(task.id).row;
513
+ task.dependencies.forEach((depId) => {
514
+ const fromPos = this.taskPositions.get(depId);
515
+ if (!fromPos)
516
+ return;
517
+ const fromRowIndex = this.taskMap.get(depId).row;
518
+ const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
519
+ const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
520
+ const fromY = fromPos.y;
521
+ const toX = Math.min(toPos.x_plan_start, toPos.x_actual_start || toPos.x_plan_start);
522
+ const toY = toPos.y;
523
+ ctx.beginPath();
524
+ if (isAdjacent) {
525
+ const isDown = toRowIndex > fromRowIndex;
526
+ const rowH = this.config.rowHeight;
527
+ const fromY_Edge = fromY + (isDown ? rowH * 0.3 : -rowH * 0.3);
528
+ const toY_Edge = toY + (isDown ? -rowH * 0.3 : rowH * 0.3);
529
+ const midY = (fromY + toY) / 2;
530
+ ctx.moveTo(fromX, fromY_Edge);
531
+ ctx.lineTo(fromX, midY);
532
+ ctx.lineTo(toX, midY);
533
+ ctx.lineTo(toX, toY_Edge);
534
+ ctx.stroke();
535
+ this.drawArrow(ctx, toX, toY_Edge, isDown ? "down" : "up");
536
+ } else {
537
+ const gap = 15;
538
+ const exitX = fromX + gap;
539
+ ctx.moveTo(fromX, fromY);
540
+ if (toX > exitX + gap) {
541
+ ctx.lineTo(exitX, fromY);
542
+ ctx.lineTo(exitX, toY);
543
+ ctx.lineTo(toX, toY);
544
+ ctx.stroke();
545
+ this.drawArrow(ctx, toX, toY, "right");
546
+ } else {
547
+ const isDown = toRowIndex > fromRowIndex;
548
+ const rowH = this.config.rowHeight;
549
+ const targetGapY = toY - (isDown ? rowH / 2 : -rowH / 2);
550
+ const entryX = toX - gap;
551
+ ctx.lineTo(exitX, fromY);
552
+ ctx.lineTo(exitX, targetGapY);
553
+ ctx.lineTo(entryX, targetGapY);
554
+ ctx.lineTo(entryX, toY);
555
+ ctx.lineTo(toX, toY);
556
+ ctx.stroke();
557
+ this.drawArrow(ctx, toX, toY, "right");
558
+ }
559
+ }
560
+ });
561
+ });
562
+ }
563
+ drawAllTasks(ctx) {
564
+ ctx.textBaseline = "middle";
565
+ ctx.font = "12px Arial";
566
+ for (let i = 0; i < this.data.length; i++) {
567
+ const row = this.data[i];
568
+ const y = i * this.config.rowHeight;
569
+ if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight)
570
+ continue;
571
+ row.tasks.forEach((task) => {
572
+ const pos = this.taskPositions.get(task.id);
573
+ if (!pos)
574
+ return;
575
+ if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
576
+ if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
577
+ return;
578
+ }
579
+ this.drawTask(ctx, task, y, pos);
580
+ });
581
+ }
582
+ }
583
+ drawGrid(ctx, startDate, endDate) {
584
+ ctx.strokeStyle = "#f0f0f0";
585
+ ctx.lineWidth = 1;
586
+ ctx.beginPath();
587
+ if (this.config.showRowLines) {
588
+ for (let i = 0; i <= this.data.length; i++) {
589
+ const y = i * this.config.rowHeight;
590
+ if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight)
591
+ continue;
592
+ ctx.moveTo(this.scrollLeft, y);
593
+ ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
594
+ }
595
+ }
596
+ if (this.config.showColLines) {
597
+ let currentDate = startDate;
598
+ switch (this.config.viewMode) {
599
+ case "Day":
600
+ currentDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
601
+ break;
602
+ case "Week":
603
+ currentDate = DateUtils.getStartOfWeek(startDate);
604
+ break;
605
+ case "Month":
606
+ currentDate = DateUtils.getStartOfMonth(startDate);
607
+ break;
608
+ case "Year":
609
+ currentDate = DateUtils.getStartOfYear(startDate);
610
+ break;
611
+ }
612
+ let nextDate;
613
+ while (this.dateToX(currentDate) < this.scrollLeft - this.pixelsPerDay * 7) {
614
+ switch (this.config.viewMode) {
615
+ case "Day":
616
+ nextDate = DateUtils.addDays(currentDate, 1);
617
+ break;
618
+ case "Week":
619
+ nextDate = DateUtils.addDays(currentDate, 7);
620
+ break;
621
+ case "Month":
622
+ nextDate = DateUtils.addMonths(currentDate, 1);
623
+ break;
624
+ case "Year":
625
+ nextDate = currentDate.getMonth() === 0 && currentDate.getDate() === 1 ? DateUtils.addMonths(currentDate, 6) : DateUtils.addMonths(currentDate, 6);
626
+ break;
627
+ default:
628
+ nextDate = DateUtils.addDays(currentDate, 1);
629
+ break;
630
+ }
631
+ currentDate = nextDate;
632
+ }
633
+ while (currentDate <= endDate) {
634
+ const x = this.dateToX(currentDate);
635
+ ctx.moveTo(x, this.scrollTop);
636
+ ctx.lineTo(x, this.scrollTop + this.viewportHeight);
637
+ switch (this.config.viewMode) {
638
+ case "Day":
639
+ nextDate = DateUtils.addDays(currentDate, 1);
640
+ break;
641
+ case "Week":
642
+ nextDate = DateUtils.addDays(currentDate, 7);
643
+ break;
644
+ case "Month":
645
+ nextDate = DateUtils.addMonths(currentDate, 1);
646
+ break;
647
+ case "Year":
648
+ if (currentDate.getMonth() === 0 && currentDate.getDate() === 1)
649
+ nextDate = DateUtils.addMonths(currentDate, 6);
650
+ else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1)
651
+ nextDate = DateUtils.addMonths(currentDate, 6);
652
+ else
653
+ nextDate = DateUtils.addDays(currentDate, 1);
654
+ break;
655
+ default:
656
+ nextDate = DateUtils.addDays(currentDate, 1);
657
+ break;
658
+ }
659
+ if (nextDate.getTime() === currentDate.getTime())
660
+ currentDate = DateUtils.addDays(currentDate, 1);
661
+ else
662
+ currentDate = nextDate;
663
+ }
664
+ }
665
+ ctx.stroke();
666
+ }
667
+ drawToday(ctx) {
668
+ const x = this.dateToX(this.today);
669
+ if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
670
+ ctx.strokeStyle = "#ff4d4f";
671
+ ctx.lineWidth = 2;
672
+ ctx.beginPath();
673
+ ctx.moveTo(x, this.scrollTop);
674
+ ctx.lineTo(x, this.scrollTop + this.viewportHeight);
675
+ ctx.stroke();
676
+ }
677
+ }
678
+ drawTask(ctx, task, y, pos) {
679
+ const offset = 4;
680
+ const width = pos.x_plan_end - pos.x_plan_start;
681
+ const taskY = y + this.config.rowHeight * 0.15 + offset;
682
+ const taskHeight = this.config.rowHeight * 0.7 - offset;
683
+ const textY = y + this.config.rowHeight / 2 + offset;
684
+ if (this.config.showActual && pos.x_actual_start) {
685
+ const aWidth = (pos.x_actual_end ? pos.x_actual_end : pos.x_plan_end) - pos.x_actual_start;
686
+ ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
687
+ ctx.fillRect(pos.x_actual_start, taskY, aWidth, taskHeight);
688
+ }
689
+ if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
690
+ ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
691
+ ctx.lineWidth = 4;
692
+ ctx.beginPath();
693
+ ctx.moveTo(pos.x_plan_start, taskY);
694
+ ctx.lineTo(pos.x_plan_start + width, taskY);
695
+ ctx.stroke();
696
+ }
697
+ ctx.fillStyle = "#333";
698
+ if (this.config.showLeftRemark && task.leftRemark) {
699
+ ctx.textAlign = "right";
700
+ ctx.fillText(task.leftRemark, Math.min(...[pos.x_plan_start, pos.x_actual_start].filter((val) => val !== null && val !== void 0)) - 8, textY);
701
+ }
702
+ if (this.config.showRightRemark && task.rightRemark) {
703
+ ctx.textAlign = "left";
704
+ ctx.fillText(task.rightRemark, Math.max(...[pos.x_plan_end, pos.x_actual_end].filter((val) => val !== null && val !== void 0)) + 8, textY);
705
+ }
706
+ if (this.config.showCenterRemark && task.centerRemark) {
707
+ const centerX = pos.x_actual_start + (pos.x_actual_end - pos.x_actual_start) / 2;
708
+ ctx.textAlign = "center";
709
+ ctx.fillText(task.centerRemark, centerX, textY, pos.x_actual_end - pos.x_actual_start);
710
+ }
711
+ }
712
+ getTaskStyles(task) {
713
+ let styles = { planBorder: "#2563eb", actualBg: "#3b82f6" };
714
+ if (task.styleClass === "demo1-task") {
715
+ styles.planBorder = "#fb923c";
716
+ styles.actualBg = "#fdba74";
717
+ }
718
+ if (task.styleClass === "demo2-plan") {
719
+ styles.planBorder = "#22c55e";
720
+ }
721
+ if (task.styleClass === "demo2-completed") {
722
+ styles.actualBg = "#86efac";
723
+ }
724
+ if (task.styleClass === "demo2-leave") {
725
+ styles.actualBg = "#f43f5e";
726
+ }
727
+ return styles;
728
+ }
729
+ handleMouseMove(e) {
730
+ const rect = this.mainCanvas.getBoundingClientRect();
731
+ const mouseX = e.clientX - rect.left;
732
+ const mouseY = e.clientY - rect.top;
733
+ const chartX = mouseX + this.scrollLeft;
734
+ const chartY = mouseY + this.scrollTop;
735
+ const rowIndex = Math.floor(chartY / this.config.rowHeight);
736
+ const date = this.xToDate(chartX);
737
+ if (rowIndex < 0 || rowIndex >= this.data.length)
738
+ return this.handleMouseLeave();
739
+ const row = this.data[rowIndex];
740
+ const overlappingTasks = row.tasks.filter((task) => {
741
+ const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
742
+ if (date >= pStart && date < pEnd)
743
+ return true;
744
+ if (task.actualStart) {
745
+ const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
746
+ if (date >= aStart && date < aEnd)
747
+ return true;
748
+ }
749
+ return false;
750
+ });
751
+ if (overlappingTasks.length === 0)
752
+ return this.handleMouseLeave();
753
+ let html = `<strong>${row.name}</strong> (${DateUtils.format(date, "yyyy-MM-dd")})<hr class="__gantt_tooltip-divider">`;
754
+ overlappingTasks.forEach((task) => html += this.getTaskTooltipHtml(task));
755
+ this.tooltip.innerHTML = html;
756
+ this.tooltip.style.display = "block";
757
+ if (this.config.tooltipColor === "white") {
758
+ this.tooltip.style.background = "#fff";
759
+ this.tooltip.style.color = "#000";
760
+ }
761
+ const tipRect = this.tooltip.getBoundingClientRect();
762
+ let x = e.clientX + 15, y = e.clientY + 15;
763
+ if (x + tipRect.width > window.innerWidth)
764
+ x = e.clientX - 15 - tipRect.width;
765
+ if (y + tipRect.height > window.innerHeight)
766
+ y = e.clientY - 15 - tipRect.height;
767
+ this.tooltip.style.left = `${x + this.config.offsetLeft}px`;
768
+ this.tooltip.style.top = `${y + this.config.offsetTop}px`;
769
+ }
770
+ getTaskTooltipHtml(task) {
771
+ if (task.type === "leave") {
772
+ const days = DateUtils.diffDaysInclusive(new Date(task.actualStart), new Date(task.actualEnd));
773
+ return `<div><span style="color: ${firstValidValue(task.actualBgColor, this.config.actualBgColor, "#f43f5e")};">■</span> <strong>${task.name} (${days}天)</strong><br><span class="__gantt_tooltip-indent">${task.actualStart} 到 ${task.actualEnd}</span></div>`;
774
+ }
775
+ let html = `<div><span style="color: ${firstValidValue(task.actualBgColor, this.config.actualBgColor, this.config.planBorderColor, this.getTaskStyles(task).planBorder)};">■</span> <strong>${task.name}</strong><br>`;
776
+ if (this.config.showPlan) {
777
+ const days = DateUtils.diffDaysInclusive(new Date(task.planStart), new Date(task.planEnd));
778
+ html += `<span class="__gantt_tooltip-indent">计划: ${task.planStart} - ${task.planEnd} (${days}天)</span><br>`;
779
+ }
780
+ if (this.config.showActual && task.actualStart) {
781
+ const days = DateUtils.diffDaysInclusive(new Date(task.actualStart), new Date(task.actualEnd));
782
+ html += `<span class="__gantt_tooltip-indent">实际: ${task.actualStart} - ${task.actualEnd} (${days}天)</span><br>`;
783
+ }
784
+ return html + "</div>";
785
+ }
786
+ handleMouseLeave() {
787
+ this.tooltip.style.display = "none";
788
+ }
789
+ }
790
+ exports.GanttChart = GanttChart;