gantt-canvas-chart 1.0.0 → 1.0.1-alpha.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.
@@ -26,6 +26,9 @@ export declare class GanttChart {
26
26
  private resizeObserver;
27
27
  private taskPositions;
28
28
  private taskMap;
29
+ private boundHandleMouseMove;
30
+ private boundHandleMouseLeave;
31
+ private boundHandleScroll;
29
32
  constructor(rootContainer: HTMLElement, data: GanttData, config?: GanttConfig);
30
33
  private init;
31
34
  private buildTaskMap;
@@ -58,4 +61,11 @@ export declare class GanttChart {
58
61
  private handleMouseMove;
59
62
  private getTaskTooltipHtml;
60
63
  private handleMouseLeave;
64
+ /**
65
+ * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
66
+ * @param diffMilliseconds 距离目标日期时间的差异毫秒数
67
+ * @param pixelsPerDay 每日的像素数
68
+ * @returns
69
+ */
70
+ static getTaskWidthPercent(diffMilliseconds: number, pixelsPerDay: number): number;
61
71
  }
@@ -13,6 +13,9 @@ export interface Task {
13
13
  styleClass?: string;
14
14
  planBorderColor?: string;
15
15
  actualBgColor?: string;
16
+ _data?: any;
17
+ planOffsetPercent?: [number, number];
18
+ actualOffsetPercent?: [number, number];
16
19
  }
17
20
  export interface Row {
18
21
  id: string;
@@ -35,8 +38,16 @@ export interface GanttConfig {
35
38
  showCenterRemark?: boolean;
36
39
  showTooltip?: boolean;
37
40
  tooltipColor?: 'black' | 'white';
41
+ todayColor?: string;
38
42
  offsetTop?: number;
39
43
  offsetLeft?: number;
44
+ viewFactors?: {
45
+ Day: number;
46
+ Week: number;
47
+ Month: number;
48
+ Year: number;
49
+ };
50
+ tooltipFormat?: null | ((task: Row, date: Date, config: GanttConfig) => string);
40
51
  }
41
52
  export interface TaskPosition {
42
53
  x_plan_start: number;
package/dist/index.cjs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.0.0
2
+ * gantt-canvas-chart v1.0.1-alpha.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -22,7 +22,10 @@ class DateUtils {
22
22
  format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
23
23
  for (let k in o)
24
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));
25
+ format = format.replace(
26
+ RegExp.$1,
27
+ RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
28
+ );
26
29
  return format;
27
30
  }
28
31
  static addDays(date, days) {
@@ -42,7 +45,9 @@ class DateUtils {
42
45
  return r;
43
46
  }
44
47
  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);
48
+ return Math.round(
49
+ (new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()).getTime() - new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()).getTime()) / this.ONE_DAY_MS
50
+ );
46
51
  }
47
52
  static diffDaysInclusive(date1, date2) {
48
53
  return this.diffDays(date1, date2) + 1;
@@ -59,8 +64,7 @@ class DateUtils {
59
64
  static getStartOfWeek(d) {
60
65
  const date = new Date(d);
61
66
  const day = date.getDay() || 7;
62
- if (day !== 1)
63
- date.setHours(-24 * (day - 1));
67
+ if (day !== 1) date.setHours(-24 * (day - 1));
64
68
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
65
69
  }
66
70
  static getStartOfMonth(d) {
@@ -104,6 +108,9 @@ class GanttChart {
104
108
  resizeObserver;
105
109
  taskPositions;
106
110
  taskMap;
111
+ boundHandleMouseMove;
112
+ boundHandleMouseLeave;
113
+ boundHandleScroll;
107
114
  constructor(rootContainer, data, config = {}) {
108
115
  const container = document.createElement("div");
109
116
  const scrollEl = document.createElement("div");
@@ -134,9 +141,12 @@ class GanttChart {
134
141
  showRightRemark: false,
135
142
  showCenterRemark: false,
136
143
  showTooltip: true,
144
+ tooltipFormat: null,
137
145
  tooltipColor: "black",
146
+ todayColor: "#ff4d4f",
138
147
  offsetTop: 0,
139
148
  offsetLeft: 0,
149
+ viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
140
150
  planBorderColor: "#caeed2",
141
151
  actualBgColor: "#78c78f",
142
152
  ...config
@@ -165,6 +175,9 @@ class GanttChart {
165
175
  this.totalHeight = 0;
166
176
  this.taskPositions = /* @__PURE__ */ new Map();
167
177
  this.taskMap = /* @__PURE__ */ new Map();
178
+ this.boundHandleMouseMove = this.handleMouseMove.bind(this);
179
+ this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
180
+ this.boundHandleScroll = this.handleScroll.bind(this);
168
181
  this.init();
169
182
  }
170
183
  init() {
@@ -181,7 +194,7 @@ class GanttChart {
181
194
  });
182
195
  }
183
196
  setupEvents() {
184
- this.container.addEventListener("scroll", this.handleScroll.bind(this));
197
+ this.container.addEventListener("scroll", this.boundHandleScroll);
185
198
  this.handleResize = this.handleResize.bind(this);
186
199
  if (window.ResizeObserver) {
187
200
  this.resizeObserver = new ResizeObserver(this.handleResize);
@@ -190,8 +203,8 @@ class GanttChart {
190
203
  }, 100);
191
204
  }
192
205
  if (this.config.showTooltip) {
193
- this.mainCanvas.addEventListener("mousemove", this.handleMouseMove.bind(this));
194
- this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave.bind(this));
206
+ this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
207
+ this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
195
208
  }
196
209
  }
197
210
  updateConfig(newConfig) {
@@ -216,6 +229,9 @@ class GanttChart {
216
229
  if (this.resizeObserver) {
217
230
  this.resizeObserver.disconnect();
218
231
  }
232
+ this.container.removeEventListener("scroll", this.boundHandleScroll);
233
+ this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
234
+ this.mainCanvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
219
235
  this.container.remove();
220
236
  }
221
237
  calculateFullTimeline() {
@@ -228,17 +244,13 @@ class GanttChart {
228
244
  this.taskMap.forEach(({ task }) => {
229
245
  const pStart = new Date(task.planStart);
230
246
  const pEnd = new Date(task.planEnd);
231
- if (pStart < minDate)
232
- minDate = pStart;
233
- if (pEnd > maxDate)
234
- maxDate = pEnd;
247
+ if (pStart < minDate) minDate = pStart;
248
+ if (pEnd > maxDate) maxDate = pEnd;
235
249
  if (task.actualStart) {
236
250
  const aStart = new Date(task.actualStart);
237
251
  const aEnd = new Date(task.actualEnd);
238
- if (aStart < minDate)
239
- minDate = aStart;
240
- if (aEnd > maxDate)
241
- maxDate = aEnd;
252
+ if (aStart < minDate) minDate = aStart;
253
+ if (aEnd > maxDate) maxDate = aEnd;
242
254
  }
243
255
  });
244
256
  }
@@ -265,8 +277,7 @@ class GanttChart {
265
277
  }
266
278
  }
267
279
  updatePixelsPerDay() {
268
- const viewFactors = { Day: 80, Week: 20, Month: 15, Year: 6 };
269
- this.pixelsPerDay = viewFactors[this.config.viewMode];
280
+ this.pixelsPerDay = this.config.viewFactors[this.config.viewMode];
270
281
  }
271
282
  dateToX(date) {
272
283
  return DateUtils.diffDays(this.timelineStart, date) * this.pixelsPerDay;
@@ -294,8 +305,7 @@ class GanttChart {
294
305
  requestAnimationFrame(() => this.render());
295
306
  }
296
307
  setScrollTop(scrollTop) {
297
- if (this.scrollTop !== scrollTop)
298
- this.container.scrollTop = scrollTop;
308
+ if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
299
309
  }
300
310
  updateVirtualRanges() {
301
311
  const buffer = 200;
@@ -504,16 +514,13 @@ class GanttChart {
504
514
  ctx.lineCap = "round";
505
515
  ctx.lineJoin = "round";
506
516
  this.taskMap.forEach(({ task }) => {
507
- if (!task.dependencies || task.dependencies.length === 0)
508
- return;
517
+ if (!task.dependencies || task.dependencies.length === 0) return;
509
518
  const toPos = this.taskPositions.get(task.id);
510
- if (!toPos)
511
- return;
519
+ if (!toPos) return;
512
520
  const toRowIndex = this.taskMap.get(task.id).row;
513
521
  task.dependencies.forEach((depId) => {
514
522
  const fromPos = this.taskPositions.get(depId);
515
- if (!fromPos)
516
- return;
523
+ if (!fromPos) return;
517
524
  const fromRowIndex = this.taskMap.get(depId).row;
518
525
  const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
519
526
  const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
@@ -566,12 +573,10 @@ class GanttChart {
566
573
  for (let i = 0; i < this.data.length; i++) {
567
574
  const row = this.data[i];
568
575
  const y = i * this.config.rowHeight;
569
- if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight)
570
- continue;
576
+ if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
571
577
  row.tasks.forEach((task) => {
572
578
  const pos = this.taskPositions.get(task.id);
573
- if (!pos)
574
- return;
579
+ if (!pos) return;
575
580
  if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
576
581
  if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
577
582
  return;
@@ -587,8 +592,7 @@ class GanttChart {
587
592
  if (this.config.showRowLines) {
588
593
  for (let i = 0; i <= this.data.length; i++) {
589
594
  const y = i * this.config.rowHeight;
590
- if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight)
591
- continue;
595
+ if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
592
596
  ctx.moveTo(this.scrollLeft, y);
593
597
  ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
594
598
  }
@@ -649,17 +653,14 @@ class GanttChart {
649
653
  nextDate = DateUtils.addMonths(currentDate, 6);
650
654
  else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1)
651
655
  nextDate = DateUtils.addMonths(currentDate, 6);
652
- else
653
- nextDate = DateUtils.addDays(currentDate, 1);
656
+ else nextDate = DateUtils.addDays(currentDate, 1);
654
657
  break;
655
658
  default:
656
659
  nextDate = DateUtils.addDays(currentDate, 1);
657
660
  break;
658
661
  }
659
- if (nextDate.getTime() === currentDate.getTime())
660
- currentDate = DateUtils.addDays(currentDate, 1);
661
- else
662
- currentDate = nextDate;
662
+ if (nextDate.getTime() === currentDate.getTime()) currentDate = DateUtils.addDays(currentDate, 1);
663
+ else currentDate = nextDate;
663
664
  }
664
665
  }
665
666
  ctx.stroke();
@@ -667,7 +668,7 @@ class GanttChart {
667
668
  drawToday(ctx) {
668
669
  const x = this.dateToX(this.today);
669
670
  if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
670
- ctx.strokeStyle = "#ff4d4f";
671
+ ctx.strokeStyle = this.config.todayColor;
671
672
  ctx.lineWidth = 2;
672
673
  ctx.beginPath();
673
674
  ctx.moveTo(x, this.scrollTop);
@@ -681,17 +682,23 @@ class GanttChart {
681
682
  const taskY = y + this.config.rowHeight * 0.15 + offset;
682
683
  const taskHeight = this.config.rowHeight * 0.7 - offset;
683
684
  const textY = y + this.config.rowHeight / 2 + offset;
685
+ const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
686
+ const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
684
687
  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
688
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
687
- ctx.fillRect(pos.x_actual_start, taskY, aWidth, taskHeight);
689
+ const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
690
+ pos.x_actual_start += aWidth * offsetX_actual;
691
+ pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
692
+ ctx.fillRect(pos.x_actual_start, taskY, aWidth * percent_actual, taskHeight);
688
693
  }
689
694
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
690
695
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
696
+ pos.x_plan_start += width * offsetX;
697
+ pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
691
698
  ctx.lineWidth = 4;
692
699
  ctx.beginPath();
693
700
  ctx.moveTo(pos.x_plan_start, taskY);
694
- ctx.lineTo(pos.x_plan_start + width, taskY);
701
+ ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
695
702
  ctx.stroke();
696
703
  }
697
704
  ctx.fillStyle = "#333";
@@ -734,25 +741,32 @@ class GanttChart {
734
741
  const chartY = mouseY + this.scrollTop;
735
742
  const rowIndex = Math.floor(chartY / this.config.rowHeight);
736
743
  const date = this.xToDate(chartX);
737
- if (rowIndex < 0 || rowIndex >= this.data.length)
738
- return this.handleMouseLeave();
744
+ if (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
739
745
  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;
746
+ if (this.config.tooltipFormat) {
747
+ const htmlStr = this.config.tooltipFormat(row, date, this.config);
748
+ if (!htmlStr) {
749
+ return this.handleMouseLeave();
748
750
  }
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;
751
+ this.tooltip.innerHTML = htmlStr;
752
+ } else {
753
+ const overlappingTasks = row.tasks.filter((task) => {
754
+ const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
755
+ if (date >= pStart && date < pEnd) return true;
756
+ if (task.actualStart) {
757
+ const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
758
+ if (date >= aStart && date < aEnd) return true;
759
+ }
760
+ return false;
761
+ });
762
+ if (overlappingTasks.length === 0) return this.handleMouseLeave();
763
+ let html = `<strong>${row.name}</strong> (${DateUtils.format(
764
+ date,
765
+ "yyyy-MM-dd"
766
+ )})<hr class="__gantt_tooltip-divider">`;
767
+ overlappingTasks.forEach((task) => html += this.getTaskTooltipHtml(task));
768
+ this.tooltip.innerHTML = html;
769
+ }
756
770
  this.tooltip.style.display = "block";
757
771
  if (this.config.tooltipColor === "white") {
758
772
  this.tooltip.style.background = "#fff";
@@ -760,10 +774,8 @@ class GanttChart {
760
774
  }
761
775
  const tipRect = this.tooltip.getBoundingClientRect();
762
776
  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;
777
+ if (x + tipRect.width > window.innerWidth) x = e.clientX - 15 - tipRect.width;
778
+ if (y + tipRect.height > window.innerHeight) y = e.clientY - 15 - tipRect.height;
767
779
  this.tooltip.style.left = `${x + this.config.offsetLeft}px`;
768
780
  this.tooltip.style.top = `${y + this.config.offsetTop}px`;
769
781
  }
@@ -786,5 +798,15 @@ class GanttChart {
786
798
  handleMouseLeave() {
787
799
  this.tooltip.style.display = "none";
788
800
  }
801
+ /**
802
+ * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
803
+ * @param diffMilliseconds 距离目标日期时间的差异毫秒数
804
+ * @param pixelsPerDay 每日的像素数
805
+ * @returns
806
+ */
807
+ static getTaskWidthPercent(diffMilliseconds, pixelsPerDay) {
808
+ return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
809
+ }
789
810
  }
811
+ exports.DateUtils = DateUtils;
790
812
  exports.GanttChart = GanttChart;
package/dist/index.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.0.0
2
+ * gantt-canvas-chart v1.0.1-alpha.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.0.0
2
+ * gantt-canvas-chart v1.0.1-alpha.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -20,7 +20,10 @@ class DateUtils {
20
20
  format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
21
21
  for (let k in o)
22
22
  if (new RegExp("(" + k + ")").test(format))
23
- format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
23
+ format = format.replace(
24
+ RegExp.$1,
25
+ RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
26
+ );
24
27
  return format;
25
28
  }
26
29
  static addDays(date, days) {
@@ -40,7 +43,9 @@ class DateUtils {
40
43
  return r;
41
44
  }
42
45
  static diffDays(date1, date2) {
43
- 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
+ return Math.round(
47
+ (new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()).getTime() - new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()).getTime()) / this.ONE_DAY_MS
48
+ );
44
49
  }
45
50
  static diffDaysInclusive(date1, date2) {
46
51
  return this.diffDays(date1, date2) + 1;
@@ -57,8 +62,7 @@ class DateUtils {
57
62
  static getStartOfWeek(d) {
58
63
  const date = new Date(d);
59
64
  const day = date.getDay() || 7;
60
- if (day !== 1)
61
- date.setHours(-24 * (day - 1));
65
+ if (day !== 1) date.setHours(-24 * (day - 1));
62
66
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
63
67
  }
64
68
  static getStartOfMonth(d) {
@@ -102,6 +106,9 @@ class GanttChart {
102
106
  resizeObserver;
103
107
  taskPositions;
104
108
  taskMap;
109
+ boundHandleMouseMove;
110
+ boundHandleMouseLeave;
111
+ boundHandleScroll;
105
112
  constructor(rootContainer, data, config = {}) {
106
113
  const container = document.createElement("div");
107
114
  const scrollEl = document.createElement("div");
@@ -132,9 +139,12 @@ class GanttChart {
132
139
  showRightRemark: false,
133
140
  showCenterRemark: false,
134
141
  showTooltip: true,
142
+ tooltipFormat: null,
135
143
  tooltipColor: "black",
144
+ todayColor: "#ff4d4f",
136
145
  offsetTop: 0,
137
146
  offsetLeft: 0,
147
+ viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
138
148
  planBorderColor: "#caeed2",
139
149
  actualBgColor: "#78c78f",
140
150
  ...config
@@ -163,6 +173,9 @@ class GanttChart {
163
173
  this.totalHeight = 0;
164
174
  this.taskPositions = /* @__PURE__ */ new Map();
165
175
  this.taskMap = /* @__PURE__ */ new Map();
176
+ this.boundHandleMouseMove = this.handleMouseMove.bind(this);
177
+ this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
178
+ this.boundHandleScroll = this.handleScroll.bind(this);
166
179
  this.init();
167
180
  }
168
181
  init() {
@@ -179,7 +192,7 @@ class GanttChart {
179
192
  });
180
193
  }
181
194
  setupEvents() {
182
- this.container.addEventListener("scroll", this.handleScroll.bind(this));
195
+ this.container.addEventListener("scroll", this.boundHandleScroll);
183
196
  this.handleResize = this.handleResize.bind(this);
184
197
  if (window.ResizeObserver) {
185
198
  this.resizeObserver = new ResizeObserver(this.handleResize);
@@ -188,8 +201,8 @@ class GanttChart {
188
201
  }, 100);
189
202
  }
190
203
  if (this.config.showTooltip) {
191
- this.mainCanvas.addEventListener("mousemove", this.handleMouseMove.bind(this));
192
- this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave.bind(this));
204
+ this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
205
+ this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
193
206
  }
194
207
  }
195
208
  updateConfig(newConfig) {
@@ -214,6 +227,9 @@ class GanttChart {
214
227
  if (this.resizeObserver) {
215
228
  this.resizeObserver.disconnect();
216
229
  }
230
+ this.container.removeEventListener("scroll", this.boundHandleScroll);
231
+ this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
232
+ this.mainCanvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
217
233
  this.container.remove();
218
234
  }
219
235
  calculateFullTimeline() {
@@ -226,17 +242,13 @@ class GanttChart {
226
242
  this.taskMap.forEach(({ task }) => {
227
243
  const pStart = new Date(task.planStart);
228
244
  const pEnd = new Date(task.planEnd);
229
- if (pStart < minDate)
230
- minDate = pStart;
231
- if (pEnd > maxDate)
232
- maxDate = pEnd;
245
+ if (pStart < minDate) minDate = pStart;
246
+ if (pEnd > maxDate) maxDate = pEnd;
233
247
  if (task.actualStart) {
234
248
  const aStart = new Date(task.actualStart);
235
249
  const aEnd = new Date(task.actualEnd);
236
- if (aStart < minDate)
237
- minDate = aStart;
238
- if (aEnd > maxDate)
239
- maxDate = aEnd;
250
+ if (aStart < minDate) minDate = aStart;
251
+ if (aEnd > maxDate) maxDate = aEnd;
240
252
  }
241
253
  });
242
254
  }
@@ -263,8 +275,7 @@ class GanttChart {
263
275
  }
264
276
  }
265
277
  updatePixelsPerDay() {
266
- const viewFactors = { Day: 80, Week: 20, Month: 15, Year: 6 };
267
- this.pixelsPerDay = viewFactors[this.config.viewMode];
278
+ this.pixelsPerDay = this.config.viewFactors[this.config.viewMode];
268
279
  }
269
280
  dateToX(date) {
270
281
  return DateUtils.diffDays(this.timelineStart, date) * this.pixelsPerDay;
@@ -292,8 +303,7 @@ class GanttChart {
292
303
  requestAnimationFrame(() => this.render());
293
304
  }
294
305
  setScrollTop(scrollTop) {
295
- if (this.scrollTop !== scrollTop)
296
- this.container.scrollTop = scrollTop;
306
+ if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
297
307
  }
298
308
  updateVirtualRanges() {
299
309
  const buffer = 200;
@@ -502,16 +512,13 @@ class GanttChart {
502
512
  ctx.lineCap = "round";
503
513
  ctx.lineJoin = "round";
504
514
  this.taskMap.forEach(({ task }) => {
505
- if (!task.dependencies || task.dependencies.length === 0)
506
- return;
515
+ if (!task.dependencies || task.dependencies.length === 0) return;
507
516
  const toPos = this.taskPositions.get(task.id);
508
- if (!toPos)
509
- return;
517
+ if (!toPos) return;
510
518
  const toRowIndex = this.taskMap.get(task.id).row;
511
519
  task.dependencies.forEach((depId) => {
512
520
  const fromPos = this.taskPositions.get(depId);
513
- if (!fromPos)
514
- return;
521
+ if (!fromPos) return;
515
522
  const fromRowIndex = this.taskMap.get(depId).row;
516
523
  const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
517
524
  const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
@@ -564,12 +571,10 @@ class GanttChart {
564
571
  for (let i = 0; i < this.data.length; i++) {
565
572
  const row = this.data[i];
566
573
  const y = i * this.config.rowHeight;
567
- if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight)
568
- continue;
574
+ if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
569
575
  row.tasks.forEach((task) => {
570
576
  const pos = this.taskPositions.get(task.id);
571
- if (!pos)
572
- return;
577
+ if (!pos) return;
573
578
  if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
574
579
  if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
575
580
  return;
@@ -585,8 +590,7 @@ class GanttChart {
585
590
  if (this.config.showRowLines) {
586
591
  for (let i = 0; i <= this.data.length; i++) {
587
592
  const y = i * this.config.rowHeight;
588
- if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight)
589
- continue;
593
+ if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
590
594
  ctx.moveTo(this.scrollLeft, y);
591
595
  ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
592
596
  }
@@ -647,17 +651,14 @@ class GanttChart {
647
651
  nextDate = DateUtils.addMonths(currentDate, 6);
648
652
  else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1)
649
653
  nextDate = DateUtils.addMonths(currentDate, 6);
650
- else
651
- nextDate = DateUtils.addDays(currentDate, 1);
654
+ else nextDate = DateUtils.addDays(currentDate, 1);
652
655
  break;
653
656
  default:
654
657
  nextDate = DateUtils.addDays(currentDate, 1);
655
658
  break;
656
659
  }
657
- if (nextDate.getTime() === currentDate.getTime())
658
- currentDate = DateUtils.addDays(currentDate, 1);
659
- else
660
- currentDate = nextDate;
660
+ if (nextDate.getTime() === currentDate.getTime()) currentDate = DateUtils.addDays(currentDate, 1);
661
+ else currentDate = nextDate;
661
662
  }
662
663
  }
663
664
  ctx.stroke();
@@ -665,7 +666,7 @@ class GanttChart {
665
666
  drawToday(ctx) {
666
667
  const x = this.dateToX(this.today);
667
668
  if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
668
- ctx.strokeStyle = "#ff4d4f";
669
+ ctx.strokeStyle = this.config.todayColor;
669
670
  ctx.lineWidth = 2;
670
671
  ctx.beginPath();
671
672
  ctx.moveTo(x, this.scrollTop);
@@ -679,17 +680,23 @@ class GanttChart {
679
680
  const taskY = y + this.config.rowHeight * 0.15 + offset;
680
681
  const taskHeight = this.config.rowHeight * 0.7 - offset;
681
682
  const textY = y + this.config.rowHeight / 2 + offset;
683
+ const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
684
+ const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
682
685
  if (this.config.showActual && pos.x_actual_start) {
683
- const aWidth = (pos.x_actual_end ? pos.x_actual_end : pos.x_plan_end) - pos.x_actual_start;
684
686
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
685
- ctx.fillRect(pos.x_actual_start, taskY, aWidth, taskHeight);
687
+ const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
688
+ pos.x_actual_start += aWidth * offsetX_actual;
689
+ pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
690
+ ctx.fillRect(pos.x_actual_start, taskY, aWidth * percent_actual, taskHeight);
686
691
  }
687
692
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
688
693
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
694
+ pos.x_plan_start += width * offsetX;
695
+ pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
689
696
  ctx.lineWidth = 4;
690
697
  ctx.beginPath();
691
698
  ctx.moveTo(pos.x_plan_start, taskY);
692
- ctx.lineTo(pos.x_plan_start + width, taskY);
699
+ ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
693
700
  ctx.stroke();
694
701
  }
695
702
  ctx.fillStyle = "#333";
@@ -732,25 +739,32 @@ class GanttChart {
732
739
  const chartY = mouseY + this.scrollTop;
733
740
  const rowIndex = Math.floor(chartY / this.config.rowHeight);
734
741
  const date = this.xToDate(chartX);
735
- if (rowIndex < 0 || rowIndex >= this.data.length)
736
- return this.handleMouseLeave();
742
+ if (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
737
743
  const row = this.data[rowIndex];
738
- const overlappingTasks = row.tasks.filter((task) => {
739
- const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
740
- if (date >= pStart && date < pEnd)
741
- return true;
742
- if (task.actualStart) {
743
- const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
744
- if (date >= aStart && date < aEnd)
745
- return true;
744
+ if (this.config.tooltipFormat) {
745
+ const htmlStr = this.config.tooltipFormat(row, date, this.config);
746
+ if (!htmlStr) {
747
+ return this.handleMouseLeave();
746
748
  }
747
- return false;
748
- });
749
- if (overlappingTasks.length === 0)
750
- return this.handleMouseLeave();
751
- let html = `<strong>${row.name}</strong> (${DateUtils.format(date, "yyyy-MM-dd")})<hr class="__gantt_tooltip-divider">`;
752
- overlappingTasks.forEach((task) => html += this.getTaskTooltipHtml(task));
753
- this.tooltip.innerHTML = html;
749
+ this.tooltip.innerHTML = htmlStr;
750
+ } else {
751
+ const overlappingTasks = row.tasks.filter((task) => {
752
+ const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
753
+ if (date >= pStart && date < pEnd) return true;
754
+ if (task.actualStart) {
755
+ const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
756
+ if (date >= aStart && date < aEnd) return true;
757
+ }
758
+ return false;
759
+ });
760
+ if (overlappingTasks.length === 0) return this.handleMouseLeave();
761
+ let html = `<strong>${row.name}</strong> (${DateUtils.format(
762
+ date,
763
+ "yyyy-MM-dd"
764
+ )})<hr class="__gantt_tooltip-divider">`;
765
+ overlappingTasks.forEach((task) => html += this.getTaskTooltipHtml(task));
766
+ this.tooltip.innerHTML = html;
767
+ }
754
768
  this.tooltip.style.display = "block";
755
769
  if (this.config.tooltipColor === "white") {
756
770
  this.tooltip.style.background = "#fff";
@@ -758,10 +772,8 @@ class GanttChart {
758
772
  }
759
773
  const tipRect = this.tooltip.getBoundingClientRect();
760
774
  let x = e.clientX + 15, y = e.clientY + 15;
761
- if (x + tipRect.width > window.innerWidth)
762
- x = e.clientX - 15 - tipRect.width;
763
- if (y + tipRect.height > window.innerHeight)
764
- y = e.clientY - 15 - tipRect.height;
775
+ if (x + tipRect.width > window.innerWidth) x = e.clientX - 15 - tipRect.width;
776
+ if (y + tipRect.height > window.innerHeight) y = e.clientY - 15 - tipRect.height;
765
777
  this.tooltip.style.left = `${x + this.config.offsetLeft}px`;
766
778
  this.tooltip.style.top = `${y + this.config.offsetTop}px`;
767
779
  }
@@ -784,7 +796,17 @@ class GanttChart {
784
796
  handleMouseLeave() {
785
797
  this.tooltip.style.display = "none";
786
798
  }
799
+ /**
800
+ * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
801
+ * @param diffMilliseconds 距离目标日期时间的差异毫秒数
802
+ * @param pixelsPerDay 每日的像素数
803
+ * @returns
804
+ */
805
+ static getTaskWidthPercent(diffMilliseconds, pixelsPerDay) {
806
+ return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
807
+ }
787
808
  }
788
809
  export {
810
+ DateUtils,
789
811
  GanttChart
790
812
  };
package/dist/index.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * gantt-canvas-chart v1.0.0
2
+ * gantt-canvas-chart v1.0.1-alpha.0
3
3
  * (c) 2025-present chandq
4
4
  * Released under the MIT License.
5
5
  */
@@ -24,7 +24,10 @@
24
24
  format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
25
25
  for (let k in o)
26
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));
27
+ format = format.replace(
28
+ RegExp.$1,
29
+ RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
30
+ );
28
31
  return format;
29
32
  }
30
33
  static addDays(date, days) {
@@ -44,7 +47,9 @@
44
47
  return r;
45
48
  }
46
49
  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);
50
+ return Math.round(
51
+ (new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()).getTime() - new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()).getTime()) / this.ONE_DAY_MS
52
+ );
48
53
  }
49
54
  static diffDaysInclusive(date1, date2) {
50
55
  return this.diffDays(date1, date2) + 1;
@@ -61,8 +66,7 @@
61
66
  static getStartOfWeek(d) {
62
67
  const date = new Date(d);
63
68
  const day = date.getDay() || 7;
64
- if (day !== 1)
65
- date.setHours(-24 * (day - 1));
69
+ if (day !== 1) date.setHours(-24 * (day - 1));
66
70
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
67
71
  }
68
72
  static getStartOfMonth(d) {
@@ -106,6 +110,9 @@
106
110
  resizeObserver;
107
111
  taskPositions;
108
112
  taskMap;
113
+ boundHandleMouseMove;
114
+ boundHandleMouseLeave;
115
+ boundHandleScroll;
109
116
  constructor(rootContainer, data, config = {}) {
110
117
  const container = document.createElement("div");
111
118
  const scrollEl = document.createElement("div");
@@ -136,9 +143,12 @@
136
143
  showRightRemark: false,
137
144
  showCenterRemark: false,
138
145
  showTooltip: true,
146
+ tooltipFormat: null,
139
147
  tooltipColor: "black",
148
+ todayColor: "#ff4d4f",
140
149
  offsetTop: 0,
141
150
  offsetLeft: 0,
151
+ viewFactors: { Day: 80, Week: 20, Month: 15, Year: 6 },
142
152
  planBorderColor: "#caeed2",
143
153
  actualBgColor: "#78c78f",
144
154
  ...config
@@ -167,6 +177,9 @@
167
177
  this.totalHeight = 0;
168
178
  this.taskPositions = /* @__PURE__ */ new Map();
169
179
  this.taskMap = /* @__PURE__ */ new Map();
180
+ this.boundHandleMouseMove = this.handleMouseMove.bind(this);
181
+ this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
182
+ this.boundHandleScroll = this.handleScroll.bind(this);
170
183
  this.init();
171
184
  }
172
185
  init() {
@@ -183,7 +196,7 @@
183
196
  });
184
197
  }
185
198
  setupEvents() {
186
- this.container.addEventListener("scroll", this.handleScroll.bind(this));
199
+ this.container.addEventListener("scroll", this.boundHandleScroll);
187
200
  this.handleResize = this.handleResize.bind(this);
188
201
  if (window.ResizeObserver) {
189
202
  this.resizeObserver = new ResizeObserver(this.handleResize);
@@ -192,8 +205,8 @@
192
205
  }, 100);
193
206
  }
194
207
  if (this.config.showTooltip) {
195
- this.mainCanvas.addEventListener("mousemove", this.handleMouseMove.bind(this));
196
- this.mainCanvas.addEventListener("mouseleave", this.handleMouseLeave.bind(this));
208
+ this.mainCanvas.addEventListener("mousemove", this.boundHandleMouseMove);
209
+ this.mainCanvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
197
210
  }
198
211
  }
199
212
  updateConfig(newConfig) {
@@ -218,6 +231,9 @@
218
231
  if (this.resizeObserver) {
219
232
  this.resizeObserver.disconnect();
220
233
  }
234
+ this.container.removeEventListener("scroll", this.boundHandleScroll);
235
+ this.mainCanvas.removeEventListener("mousemove", this.boundHandleMouseMove);
236
+ this.mainCanvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
221
237
  this.container.remove();
222
238
  }
223
239
  calculateFullTimeline() {
@@ -230,17 +246,13 @@
230
246
  this.taskMap.forEach(({ task }) => {
231
247
  const pStart = new Date(task.planStart);
232
248
  const pEnd = new Date(task.planEnd);
233
- if (pStart < minDate)
234
- minDate = pStart;
235
- if (pEnd > maxDate)
236
- maxDate = pEnd;
249
+ if (pStart < minDate) minDate = pStart;
250
+ if (pEnd > maxDate) maxDate = pEnd;
237
251
  if (task.actualStart) {
238
252
  const aStart = new Date(task.actualStart);
239
253
  const aEnd = new Date(task.actualEnd);
240
- if (aStart < minDate)
241
- minDate = aStart;
242
- if (aEnd > maxDate)
243
- maxDate = aEnd;
254
+ if (aStart < minDate) minDate = aStart;
255
+ if (aEnd > maxDate) maxDate = aEnd;
244
256
  }
245
257
  });
246
258
  }
@@ -267,8 +279,7 @@
267
279
  }
268
280
  }
269
281
  updatePixelsPerDay() {
270
- const viewFactors = { Day: 80, Week: 20, Month: 15, Year: 6 };
271
- this.pixelsPerDay = viewFactors[this.config.viewMode];
282
+ this.pixelsPerDay = this.config.viewFactors[this.config.viewMode];
272
283
  }
273
284
  dateToX(date) {
274
285
  return DateUtils.diffDays(this.timelineStart, date) * this.pixelsPerDay;
@@ -296,8 +307,7 @@
296
307
  requestAnimationFrame(() => this.render());
297
308
  }
298
309
  setScrollTop(scrollTop) {
299
- if (this.scrollTop !== scrollTop)
300
- this.container.scrollTop = scrollTop;
310
+ if (this.scrollTop !== scrollTop) this.container.scrollTop = scrollTop;
301
311
  }
302
312
  updateVirtualRanges() {
303
313
  const buffer = 200;
@@ -506,16 +516,13 @@
506
516
  ctx.lineCap = "round";
507
517
  ctx.lineJoin = "round";
508
518
  this.taskMap.forEach(({ task }) => {
509
- if (!task.dependencies || task.dependencies.length === 0)
510
- return;
519
+ if (!task.dependencies || task.dependencies.length === 0) return;
511
520
  const toPos = this.taskPositions.get(task.id);
512
- if (!toPos)
513
- return;
521
+ if (!toPos) return;
514
522
  const toRowIndex = this.taskMap.get(task.id).row;
515
523
  task.dependencies.forEach((depId) => {
516
524
  const fromPos = this.taskPositions.get(depId);
517
- if (!fromPos)
518
- return;
525
+ if (!fromPos) return;
519
526
  const fromRowIndex = this.taskMap.get(depId).row;
520
527
  const isAdjacent = Math.abs(toRowIndex - fromRowIndex) === 1;
521
528
  const fromX = Math.max(fromPos.x_plan_end, fromPos.x_actual_end || fromPos.x_plan_end);
@@ -568,12 +575,10 @@
568
575
  for (let i = 0; i < this.data.length; i++) {
569
576
  const row = this.data[i];
570
577
  const y = i * this.config.rowHeight;
571
- if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight)
572
- continue;
578
+ if (y + this.config.rowHeight < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
573
579
  row.tasks.forEach((task) => {
574
580
  const pos = this.taskPositions.get(task.id);
575
- if (!pos)
576
- return;
581
+ if (!pos) return;
577
582
  if (pos.x_plan_end < this.scrollLeft || pos.x_plan_start > this.scrollLeft + this.viewportWidth) {
578
583
  if (!pos.x_actual_start || pos.x_actual_end < this.scrollLeft || pos.x_actual_start > this.scrollLeft + this.viewportWidth)
579
584
  return;
@@ -589,8 +594,7 @@
589
594
  if (this.config.showRowLines) {
590
595
  for (let i = 0; i <= this.data.length; i++) {
591
596
  const y = i * this.config.rowHeight;
592
- if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight)
593
- continue;
597
+ if (y < this.scrollTop || y > this.scrollTop + this.viewportHeight) continue;
594
598
  ctx.moveTo(this.scrollLeft, y);
595
599
  ctx.lineTo(this.scrollLeft + this.viewportWidth, y);
596
600
  }
@@ -651,17 +655,14 @@
651
655
  nextDate = DateUtils.addMonths(currentDate, 6);
652
656
  else if (currentDate.getMonth() === 6 && currentDate.getDate() === 1)
653
657
  nextDate = DateUtils.addMonths(currentDate, 6);
654
- else
655
- nextDate = DateUtils.addDays(currentDate, 1);
658
+ else nextDate = DateUtils.addDays(currentDate, 1);
656
659
  break;
657
660
  default:
658
661
  nextDate = DateUtils.addDays(currentDate, 1);
659
662
  break;
660
663
  }
661
- if (nextDate.getTime() === currentDate.getTime())
662
- currentDate = DateUtils.addDays(currentDate, 1);
663
- else
664
- currentDate = nextDate;
664
+ if (nextDate.getTime() === currentDate.getTime()) currentDate = DateUtils.addDays(currentDate, 1);
665
+ else currentDate = nextDate;
665
666
  }
666
667
  }
667
668
  ctx.stroke();
@@ -669,7 +670,7 @@
669
670
  drawToday(ctx) {
670
671
  const x = this.dateToX(this.today);
671
672
  if (x >= this.scrollLeft && x <= this.scrollLeft + this.viewportWidth) {
672
- ctx.strokeStyle = "#ff4d4f";
673
+ ctx.strokeStyle = this.config.todayColor;
673
674
  ctx.lineWidth = 2;
674
675
  ctx.beginPath();
675
676
  ctx.moveTo(x, this.scrollTop);
@@ -683,17 +684,23 @@
683
684
  const taskY = y + this.config.rowHeight * 0.15 + offset;
684
685
  const taskHeight = this.config.rowHeight * 0.7 - offset;
685
686
  const textY = y + this.config.rowHeight / 2 + offset;
687
+ const [offsetX_actual, percent_actual] = this.config.viewMode === "Day" && task.actualOffsetPercent ? task.actualOffsetPercent : [0, 1];
688
+ const [offsetX, percent_plan] = this.config.viewMode === "Day" && task.planOffsetPercent ? task.planOffsetPercent : [0, 1];
686
689
  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
690
  ctx.fillStyle = task.actualBgColor ? task.actualBgColor : this.config.actualBgColor;
689
- ctx.fillRect(pos.x_actual_start, taskY, aWidth, taskHeight);
691
+ const aWidth = (pos.x_actual_end ? pos.x_actual_end : this.dateToX(this.today)) - pos.x_actual_start;
692
+ pos.x_actual_start += aWidth * offsetX_actual;
693
+ pos.x_actual_end && (pos.x_actual_end = pos.x_actual_start + aWidth * percent_actual);
694
+ ctx.fillRect(pos.x_actual_start, taskY, aWidth * percent_actual, taskHeight);
690
695
  }
691
696
  if (this.config.showPlan && pos.x_plan_start && pos.x_plan_end) {
692
697
  ctx.strokeStyle = task.planBorderColor ? task.planBorderColor : this.config.planBorderColor;
698
+ pos.x_plan_start += width * offsetX;
699
+ pos.x_plan_end && (pos.x_plan_end = pos.x_plan_start + width * percent_plan);
693
700
  ctx.lineWidth = 4;
694
701
  ctx.beginPath();
695
702
  ctx.moveTo(pos.x_plan_start, taskY);
696
- ctx.lineTo(pos.x_plan_start + width, taskY);
703
+ ctx.lineTo(pos.x_plan_start + width * percent_plan, taskY);
697
704
  ctx.stroke();
698
705
  }
699
706
  ctx.fillStyle = "#333";
@@ -736,25 +743,32 @@
736
743
  const chartY = mouseY + this.scrollTop;
737
744
  const rowIndex = Math.floor(chartY / this.config.rowHeight);
738
745
  const date = this.xToDate(chartX);
739
- if (rowIndex < 0 || rowIndex >= this.data.length)
740
- return this.handleMouseLeave();
746
+ if (rowIndex < 0 || rowIndex >= this.data.length) return this.handleMouseLeave();
741
747
  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;
748
+ if (this.config.tooltipFormat) {
749
+ const htmlStr = this.config.tooltipFormat(row, date, this.config);
750
+ if (!htmlStr) {
751
+ return this.handleMouseLeave();
750
752
  }
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;
753
+ this.tooltip.innerHTML = htmlStr;
754
+ } else {
755
+ const overlappingTasks = row.tasks.filter((task) => {
756
+ const pStart = new Date(task.planStart), pEnd = DateUtils.addDays(new Date(task.planEnd), 1);
757
+ if (date >= pStart && date < pEnd) return true;
758
+ if (task.actualStart) {
759
+ const aStart = new Date(task.actualStart), aEnd = DateUtils.addDays(new Date(task.actualEnd), 1);
760
+ if (date >= aStart && date < aEnd) return true;
761
+ }
762
+ return false;
763
+ });
764
+ if (overlappingTasks.length === 0) return this.handleMouseLeave();
765
+ let html = `<strong>${row.name}</strong> (${DateUtils.format(
766
+ date,
767
+ "yyyy-MM-dd"
768
+ )})<hr class="__gantt_tooltip-divider">`;
769
+ overlappingTasks.forEach((task) => html += this.getTaskTooltipHtml(task));
770
+ this.tooltip.innerHTML = html;
771
+ }
758
772
  this.tooltip.style.display = "block";
759
773
  if (this.config.tooltipColor === "white") {
760
774
  this.tooltip.style.background = "#fff";
@@ -762,10 +776,8 @@
762
776
  }
763
777
  const tipRect = this.tooltip.getBoundingClientRect();
764
778
  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;
779
+ if (x + tipRect.width > window.innerWidth) x = e.clientX - 15 - tipRect.width;
780
+ if (y + tipRect.height > window.innerHeight) y = e.clientY - 15 - tipRect.height;
769
781
  this.tooltip.style.left = `${x + this.config.offsetLeft}px`;
770
782
  this.tooltip.style.top = `${y + this.config.offsetTop}px`;
771
783
  }
@@ -788,7 +800,17 @@
788
800
  handleMouseLeave() {
789
801
  this.tooltip.style.display = "none";
790
802
  }
803
+ /**
804
+ * 计算任务宽度占的百分比(方便绘制精确到具体时间的每日任务)
805
+ * @param diffMilliseconds 距离目标日期时间的差异毫秒数
806
+ * @param pixelsPerDay 每日的像素数
807
+ * @returns
808
+ */
809
+ static getTaskWidthPercent(diffMilliseconds, pixelsPerDay) {
810
+ return diffMilliseconds * pixelsPerDay / DateUtils.ONE_DAY_MS;
811
+ }
791
812
  }
813
+ exports2.DateUtils = DateUtils;
792
814
  exports2.GanttChart = GanttChart;
793
815
  Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
794
816
  }));
package/dist/main.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { GanttChart } from './core/ganttChart';
2
+ import { DateUtils } from './core/dateUtils';
2
3
 
3
- export { GanttChart };
4
+ export { GanttChart, DateUtils };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gantt-canvas-chart",
3
- "version": "1.0.0",
3
+ "version": "1.0.1-alpha.0",
4
4
  "description": "High performance Gantt chart component based on Canvas, can be applied to any framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.es.js",