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