@operato/gantt 7.0.60 → 7.0.69

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.
@@ -1,15 +1,15 @@
1
- import { LitElement, html, css, PropertyValues } from 'lit'
1
+ import { LitElement, html, css } from 'lit'
2
2
  import { customElement, property, query } from 'lit/decorators.js'
3
- import * as d3 from 'd3'
3
+ import { ResourceChartController } from './resource-chart-controller'
4
+ import { Task, Resource, TimeScale } from './types'
4
5
 
6
+ import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
5
7
  import { ScrollbarStyles } from '@operato/styles'
6
8
 
7
- import { drawTimeScaleHeader } from './draw-time-scale-header'
8
- import { Task, TimeScale } from './types'
9
-
10
9
  @customElement('ox-resource-chart')
11
10
  export class OxResourceChart extends LitElement {
12
11
  static styles = [
12
+ MDTypeScaleStyles,
13
13
  ScrollbarStyles,
14
14
  css`
15
15
  :host {
@@ -19,18 +19,16 @@ export class OxResourceChart extends LitElement {
19
19
  }
20
20
 
21
21
  div[chart-container] {
22
- display: flex;
23
22
  flex-direction: column;
24
-
25
23
  overflow-x: auto;
26
- overflow-y: auto;
24
+ overflow-y: hidden;
27
25
  }
28
26
 
29
27
  svg[header] {
30
28
  display: block;
31
29
  min-width: 100%;
32
30
  box-sizing: border-box;
33
- border-bottom: 1px solid var(--ox-gantt-header-border-color, var(--md-sys-color-on-secondary-container));
31
+ border-bottom: 1px solid var(--ox-gantt-header-border-color, var(--md-sys-color-outline));
34
32
  height: 30px;
35
33
  background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
36
34
  color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
@@ -40,16 +38,10 @@ export class OxResourceChart extends LitElement {
40
38
  }
41
39
 
42
40
  svg[body] {
43
- flex: 1;
44
41
  display: block;
45
42
  min-width: 100%;
46
43
  }
47
44
 
48
- .top-axis text,
49
- .bottom-axis text {
50
- font-size: 12px;
51
- }
52
-
53
45
  .grid line {
54
46
  stroke: #e0e0e0;
55
47
  }
@@ -57,280 +49,92 @@ export class OxResourceChart extends LitElement {
57
49
  .grid path {
58
50
  stroke-width: 0;
59
51
  }
60
-
61
- .no-select {
62
- user-select: none;
63
- }
64
-
65
- .selected {
66
- stroke-width: 2px;
67
- stroke: var(--ox-gantt-selected-color, blue);
68
- }
69
52
  `
70
53
  ]
71
54
 
72
55
  @property({ type: String }) fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0]
73
56
  @property({ type: String }) toDate: string = new Date(new Date().getFullYear(), 11, 31).toISOString().split('T')[0]
74
- @property({ type: String }) axisScale: string = 'week-day'
75
- @property({ type: Array }) resources: any[] = []
57
+ @property({ type: String }) axisScale: TimeScale = 'week-day'
58
+ @property({ type: Array }) resources: Resource[] = []
76
59
  @property({ type: Array }) tasks: Task[] = []
77
60
 
78
- private timeScale: any
79
- private baseWidth: number = 30
80
- private level: TimeScale = 'week-day'
81
- private fromTime?: Date
82
- private toTime?: Date
83
-
84
- @query('[chart-container') container!: HTMLDivElement
61
+ @query('[chart-container]') container!: HTMLDivElement
85
62
  @query('svg[header]') svgHeader!: SVGSVGElement
86
63
  @query('svg[body]') svgBody!: SVGSVGElement
87
64
 
88
- render() {
89
- return html`
90
- <div chart-container>
91
- <svg header></svg>
92
- <svg body></svg>
93
- </div>
94
- `
65
+ private resourceChartController: ResourceChartController
66
+
67
+ constructor() {
68
+ super()
69
+ this.resourceChartController = new ResourceChartController(this, this.fromDate, this.toDate, this.axisScale)
95
70
  }
96
71
 
97
- updated(changedProperties: PropertyValues<this>) {
98
- if (changedProperties.has('fromDate') || changedProperties.has('toDate') || changedProperties.has('axisScale')) {
99
- this.updateTimeScale()
72
+ updated(changedProperties: Map<string | number | symbol, unknown>) {
73
+ if (changedProperties.has('fromDate') || changedProperties.has('toDate')) {
74
+ this.resourceChartController.updateRange(this.fromDate, this.toDate)
100
75
  }
101
76
 
102
- if (changedProperties.has('resources') || changedProperties.has('tasks')) {
103
- this.drawBody()
77
+ if (changedProperties.has('resources')) {
78
+ this.resourceChartController.setResources(this.resources)
79
+ this.updateChartHeight() // 리소스의 수에 따라 차트의 높이를 업데이트합니다.
104
80
  }
105
81
 
106
- this.drawAxes()
107
- }
108
-
109
- updateTimeScale() {
110
- this.fromTime = new Date(this.fromDate)
111
- this.toTime = new Date(this.toDate)
112
-
113
- this.timeScale = d3
114
- .scaleTime()
115
- .domain([d3.timeDay.floor(new Date(this.fromDate)), d3.timeDay.offset(new Date(this.toDate), 1)])
116
- .range([0, this.getChartWidth()])
117
- }
118
-
119
- getChartWidth() {
120
- const totalDays = d3.timeDay.count(new Date(this.fromDate), d3.timeDay.offset(new Date(this.toDate), 1))
121
- return (totalDays * this.baseWidth) / this.getScale()
122
- }
123
-
124
- getScale() {
125
- if (this.level == 'quarter-week') {
126
- return 7 / 2
127
- } else if (this.level == 'year-month') {
128
- return 30 / 2
129
- } else if (this.level === 'year-quarter') {
130
- return 90 / 2
82
+ if (changedProperties.has('tasks')) {
83
+ this.resourceChartController.setTasks(this.tasks)
84
+ this.updateChartHeight() // 리소스의 수에 따라 차트의 높이를 업데이트합니다.
131
85
  }
132
- return 1
133
- }
134
-
135
- drawAxes() {
136
- drawTimeScaleHeader(
137
- this.svgHeader as any,
138
- this.timeScale,
139
- this.fromTime!,
140
- this.toTime!,
141
- this.baseWidth,
142
- this.level,
143
- false /* 1 level only header */
144
- )
145
- }
146
-
147
- drawBody() {
148
- const body = d3.select(this.svgBody)
149
-
150
- body.selectAll('*').remove()
151
- body.attr('width', this.getChartWidth())
152
-
153
- if (this.resources.length > 0) {
154
- const chartHeight = this.resources.length * 30
155
-
156
- const addVerticalGridLines = (ticks: Date[]) => {
157
- ticks.forEach(tick => {
158
- const startX = this.timeScale(tick)
159
- body
160
- .append('line')
161
- .attr('x1', startX)
162
- .attr('x2', startX)
163
- .attr('y1', 0)
164
- .attr('y2', chartHeight) // Ensure vertical lines extend to the bottom
165
- .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
166
- .attr('stroke-width', 1)
167
- })
168
- }
169
-
170
- const addHorizontalGridLines = () => {
171
- this.resources.forEach((_, index) => {
172
- const y = (index + 1) * 30
173
- body
174
- .append('line')
175
- .attr('x1', 0)
176
- .attr('x2', this.getChartWidth())
177
- .attr('y1', y)
178
- .attr('y2', y)
179
- .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
180
- .attr('stroke-width', 1)
181
- })
182
-
183
- // Ensure the last horizontal line extends to the bottom of the chart
184
- body
185
- .append('line')
186
- .attr('x1', 0)
187
- .attr('x2', this.getChartWidth())
188
- .attr('y1', chartHeight)
189
- .attr('y2', chartHeight)
190
- .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
191
- .attr('stroke-width', 1)
192
- }
193
-
194
- let ticks
195
- if (this.level === 'week-day') {
196
- ticks = d3.timeDay
197
- .every(1)!
198
- .range(d3.timeDay.floor(new Date(this.fromDate)), d3.timeDay.offset(new Date(this.toDate), 1))
199
- } else if (this.level === 'quarter-week') {
200
- ticks = d3.timeWeek
201
- .every(1)!
202
- .range(d3.timeWeek.floor(new Date(this.fromDate)), d3.timeWeek.offset(new Date(this.toDate), 1))
203
- } else if (this.level === 'year-month') {
204
- ticks = d3.timeMonth
205
- .every(1)!
206
- .range(d3.timeMonth.floor(new Date(this.fromDate)), d3.timeMonth.offset(new Date(this.toDate), 1))
207
- } else if (this.level === 'year-quarter') {
208
- ticks = d3.timeMonth
209
- .every(3)!
210
- .range(d3.timeMonth.floor(new Date(this.fromDate)), d3.timeMonth.offset(new Date(this.toDate), 3))
211
- }
212
-
213
- addVerticalGridLines(ticks!)
214
- addHorizontalGridLines()
215
86
 
216
- // Render the resource allocations
217
- this.renderResourceAllocations(body, ticks!)
87
+ if (changedProperties.has('axisScale')) {
88
+ this.resourceChartController.level = this.axisScale
89
+ this.resourceChartController.updateTimeScale()
218
90
  }
219
- }
220
-
221
- renderResourceAllocations(body: d3.Selection<SVGSVGElement, unknown, null, undefined>, ticks: Date[]) {
222
- const resourceAllocations = this.calculateResourceAllocations(ticks)
223
-
224
- this.resources.forEach((resource, index) => {
225
- const y = index * 30 + 15 // Center the text vertically in the row
226
- ticks.forEach(tick => {
227
- const tickEnd = this.getTickEnd(tick)
228
- const dateKey = this.formatDateKey(tick, tickEnd)
229
- const allocation = resourceAllocations[resource.type][dateKey]
230
- if (allocation > 0) {
231
- const startX = this.timeScale(tick)
232
- const tickInterval = this.timeScale(tickEnd) - startX
233
91
 
234
- body
235
- .append('text')
236
- .attr('x', startX + tickInterval / 2)
237
- .attr('y', y)
238
- .attr('text-anchor', 'middle')
239
- .attr('alignment-baseline', 'middle')
240
- .attr('dominant-baseline', 'middle')
241
- .text(allocation)
242
- .style('font-size', '12px')
243
- }
244
- })
245
- })
92
+ // 차트 렌더링
93
+ this.resourceChartController.drawAxes(this.svgHeader)
94
+ this.resourceChartController.drawBody(this.svgBody)
246
95
  }
247
96
 
248
- calculateResourceAllocations(ticks: Date[]) {
249
- const resourceAllocations: { [key: string]: { [key: string]: number } } = {}
250
-
251
- // Initialize the resourceAllocations object
252
- this.resources.forEach(resource => {
253
- resourceAllocations[resource.type] = {}
254
- ticks.forEach(tick => {
255
- const tickEnd = this.getTickEnd(tick)
256
- const dateKey = this.formatDateKey(tick, tickEnd)
257
- resourceAllocations[resource.type][dateKey] = 0
258
- })
259
- })
260
-
261
- // Function to process tasks recursively
262
- const processTask = (task: Task) => {
263
- const startDate = new Date(task.startDate)
264
- const endDate = new Date(task.endDate)
265
- let currentDate = startDate
97
+ updateChartHeight() {
98
+ const resourceCount = this.resources.length
99
+ const rowHeight = 30 // 리소스 하나당 할당된 높이 (픽셀)
100
+ const minHeight = 200 // 최소 높이
101
+ const chartHeight = Math.max(resourceCount * rowHeight, minHeight)
266
102
 
267
- while (currentDate <= endDate) {
268
- const tick = ticks.find(t => currentDate >= t && currentDate < this.getTickEnd(t))
269
- if (tick) {
270
- const tickEnd = this.getTickEnd(tick)
271
- const dateKey = this.formatDateKey(tick, tickEnd)
272
-
273
- ;(task.resources || []).forEach(resource => {
274
- if (resourceAllocations[resource.type][dateKey] !== undefined) {
275
- resourceAllocations[resource.type][dateKey] += resource.allocated
276
- }
277
- })
278
- }
279
-
280
- currentDate.setDate(currentDate.getDate() + 1)
281
- }
282
-
283
- // Recursively process child tasks
284
- if (task.children) {
285
- task.children.forEach(childTask => processTask(childTask))
286
- }
287
- }
288
-
289
- // Process all tasks
290
- this.tasks.forEach(task => processTask(task))
291
-
292
- return resourceAllocations
103
+ this.svgBody.style.height = `${chartHeight}px`
293
104
  }
294
105
 
295
- getTickEnd(tick: Date) {
296
- if (this.level === 'week-day') {
297
- return d3.timeDay.offset(tick, 1)
298
- } else if (this.level === 'quarter-week') {
299
- return d3.timeWeek.offset(tick, 1)
300
- } else if (this.level === 'year-month') {
301
- return d3.timeMonth.offset(tick, 1)
302
- } else if (this.level === 'year-quarter') {
303
- return d3.timeMonth.offset(tick, 3)
304
- }
305
- return d3.timeDay.offset(tick, 1)
306
- }
307
-
308
- formatDateKey(start: Date, end: Date) {
309
- return `${start.toISOString().split('T')[0]}_${end.toISOString().split('T')[0]}`
106
+ render() {
107
+ return html`
108
+ <div
109
+ chart-container
110
+ @wheel=${(e: WheelEvent) => this.resourceChartController.handleWheelForBody(e, this.container)}
111
+ @touchstart=${(e: TouchEvent) => this.resourceChartController.handleTouchStartForBody(e, this.container)}
112
+ @touchmove=${(e: TouchEvent) => this.resourceChartController.handleTouchMoveForBody(e, this.container)}
113
+ >
114
+ <svg header></svg>
115
+ <svg body></svg>
116
+ </div>
117
+ `
310
118
  }
311
119
 
312
120
  syncTimeScale(detail: { baseWidth: number; level: TimeScale }) {
313
- this.baseWidth = detail.baseWidth
314
- this.level = detail.level
121
+ this.resourceChartController.syncTimeScale(detail)
122
+ this.requestUpdate()
123
+ }
315
124
 
316
- this.updateTimeScale()
317
- this.drawAxes()
318
- this.drawBody()
125
+ getHorizontalOffset(): number {
126
+ return this.container.scrollLeft
319
127
  }
320
128
 
321
- setVerticalOffset(offset: number) {
322
- this.container.scrollTop = offset
129
+ setHorizontalOffset(offset: number) {
130
+ this.container.scrollLeft = offset
323
131
  }
324
132
 
325
133
  getVerticalOffset(): number {
326
134
  return this.container.scrollTop
327
135
  }
328
136
 
329
- setHorizontalOffset(offset: number) {
330
- this.container.scrollLeft = offset
331
- }
332
-
333
- getHorizontalOffset(): number {
334
- return this.container.scrollLeft
137
+ setVerticalOffset(offset: number) {
138
+ this.container.scrollTop = offset
335
139
  }
336
140
  }
@@ -6,7 +6,7 @@ export class OxTaskTooltip extends LitElement {
6
6
  static styles = css`
7
7
  :host {
8
8
  position: fixed; /* position: absolute로 변경 */
9
- background-color: var(--ox-gantt-tooltip-background-color, var(--md-sys-color-surface, #f9f9f9));
9
+ background-color: var(--ox-gantt-tooltip-background-color, var(--md-sys-color-surface-variant, #f9f9f9));
10
10
  border: 1px solid var(--ox-gantt-tooltip-border-color, var(--md-sys-color-outline, #ccc));
11
11
  padding: var(--spacing-medium, 10px);
12
12
  border-radius: var(--spacing-medium, 8px);
@@ -41,12 +41,12 @@ export class OxTaskTooltip extends LitElement {
41
41
  }
42
42
  .label {
43
43
  font-weight: bold;
44
- color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface-variant, #333));
44
+ color: var(--ox-gantt-tooltip-color, var(--md-sys-color-secondary, #333));
45
45
  text-align: right; /* 라벨을 우측 정렬 */
46
46
  }
47
47
  .colon {
48
48
  text-align: center;
49
- color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
49
+ color: var(--ox-gantt-tooltip-color, var(--md-sys-color-secondary, #333));
50
50
  }
51
51
  .value {
52
52
  text-align: left; /* 값을 좌측 정렬 */
@@ -0,0 +1,284 @@
1
+ import { LitElement, ReactiveController, ReactiveControllerHost } from 'lit'
2
+ import * as d3 from 'd3'
3
+ import { TimeScale, Task, Resource } from './types'
4
+ import { drawTimeScaleHeader, getScale, getChartWidth } from './draw-time-scale-header'
5
+
6
+ export class ResourceChartController implements ReactiveController {
7
+ private host: ReactiveControllerHost & LitElement
8
+ private fromTime: Date
9
+ private toTime: Date
10
+ private totalDays: number
11
+ private _timeScale?: d3.ScaleTime<number, number, never>
12
+
13
+ private touchStartY?: number
14
+ private touchStartX?: number
15
+ private scrollStartX?: number
16
+ private scrollStartY?: number
17
+
18
+ public baseWidth: number = 30
19
+ public level: TimeScale
20
+ public resources: Resource[] = []
21
+ public tasks: Task[] = []
22
+
23
+ constructor(host: ReactiveControllerHost & LitElement, fromDate: string, toDate: string, axisScale: TimeScale) {
24
+ this.host = host
25
+ this.fromTime = new Date(fromDate)
26
+ this.toTime = new Date(toDate)
27
+ this.level = axisScale
28
+ this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
29
+ this.host.addController(this)
30
+ }
31
+
32
+ hostConnected() {
33
+ this.updateTimeScale()
34
+ }
35
+
36
+ get timeScale() {
37
+ if (!this._timeScale) {
38
+ this._timeScale = d3
39
+ .scaleTime()
40
+ .domain([d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1)])
41
+ .range([0, this.getChartWidth()])
42
+ }
43
+ return this._timeScale
44
+ }
45
+
46
+ public syncTimeScale(detail: { baseWidth: number; level: TimeScale }) {
47
+ this.baseWidth = detail.baseWidth
48
+ this.level = detail.level
49
+
50
+ this.updateTimeScale()
51
+ }
52
+
53
+ updateRange(fromDate: string, toDate: string) {
54
+ this.fromTime = new Date(fromDate)
55
+ this.toTime = new Date(toDate)
56
+ this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
57
+ this._timeScale = undefined
58
+ this.host.requestUpdate()
59
+ }
60
+
61
+ setResources(resources: Resource[]) {
62
+ this.resources = resources
63
+ this.host.requestUpdate()
64
+ }
65
+
66
+ setTasks(tasks: Task[]) {
67
+ this.tasks = tasks
68
+ this.host.requestUpdate()
69
+ }
70
+
71
+ updateTimeScale() {
72
+ this.timeScale.range([0, this.getChartWidth()])
73
+ }
74
+
75
+ getScale() {
76
+ return getScale(this.level)
77
+ }
78
+
79
+ getChartWidth() {
80
+ return getChartWidth(this.fromTime, this.toTime, this.baseWidth, this.level)
81
+ }
82
+
83
+ drawAxes(svgHeader: SVGElement) {
84
+ drawTimeScaleHeader(svgHeader, this.timeScale, this.fromTime, this.toTime, this.baseWidth, this.level, false)
85
+ }
86
+
87
+ drawBody(svgBody: SVGElement) {
88
+ const body = d3.select(svgBody)
89
+
90
+ body.selectAll('*').remove()
91
+ body.attr('width', this.getChartWidth())
92
+
93
+ if (this.resources.length > 0) {
94
+ const chartHeight = this.resources.length * 30
95
+
96
+ const addGridLines = (ticks: Date[]) => {
97
+ ticks.forEach(tick => {
98
+ const startX = this.timeScale(tick)
99
+ body
100
+ .append('line')
101
+ .attr('x1', startX)
102
+ .attr('x2', startX)
103
+ .attr('y1', 0)
104
+ .attr('y2', chartHeight) // Ensure vertical lines extend to the bottom
105
+ .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
106
+ .attr('stroke-width', 1)
107
+ })
108
+ }
109
+
110
+ let ticks
111
+ if (this.level === 'week-day') {
112
+ ticks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
113
+ } else if (this.level === 'quarter-week') {
114
+ ticks = d3.timeWeek.every(1)!.range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
115
+ } else if (this.level === 'year-month') {
116
+ ticks = d3.timeMonth.every(1)!.range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
117
+ } else if (this.level === 'year-quarter') {
118
+ ticks = d3.timeMonth.every(3)!.range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
119
+ }
120
+
121
+ addGridLines(ticks!)
122
+
123
+ this.renderResourceAllocations(body, ticks!, chartHeight)
124
+ }
125
+ }
126
+
127
+ renderResourceAllocations(
128
+ body: d3.Selection<SVGElement, unknown, null, undefined>,
129
+ ticks: Date[],
130
+ chartHeight: number
131
+ ) {
132
+ const resourceAllocations = this.calculateResourceAllocations(ticks)
133
+
134
+ this.resources.forEach((resource, index) => {
135
+ const y = index * 30 + 15 // Center the text vertically in the row
136
+ ticks.forEach(tick => {
137
+ const tickEnd = this.getTickEnd(tick)
138
+ const dateKey = this.formatDateKey(tick, tickEnd)
139
+ const allocation = resourceAllocations[resource.type][dateKey]
140
+ if (allocation > 0) {
141
+ const startX = this.timeScale(tick)
142
+ const tickInterval = this.timeScale(tickEnd) - startX
143
+
144
+ body
145
+ .append('text')
146
+ .attr('x', startX + tickInterval / 2)
147
+ .attr('y', y)
148
+ .attr('text-anchor', 'middle')
149
+ .attr('alignment-baseline', 'middle')
150
+ .attr('dominant-baseline', 'middle')
151
+ .attr('stroke', 'var(--ox-gantt-color-text, var(--md-sys-color-on-primary-container))')
152
+ .text(allocation)
153
+ .style('font-size', '12px')
154
+ }
155
+ })
156
+ })
157
+
158
+ // Draw horizontal grid lines for each resource row
159
+ for (let i = 0; i <= this.resources.length; i++) {
160
+ body
161
+ .append('line')
162
+ .attr('x1', 0)
163
+ .attr('x2', this.getChartWidth())
164
+ .attr('y1', i * 30)
165
+ .attr('y2', i * 30)
166
+ .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
167
+ .attr('stroke-width', 1)
168
+ }
169
+ }
170
+
171
+ calculateResourceAllocations(ticks: Date[]): { [key: string]: { [key: string]: number } } {
172
+ const resourceAllocations: { [key: string]: { [key: string]: number } } = {}
173
+
174
+ this.resources.forEach(resource => {
175
+ resourceAllocations[resource.type] = {}
176
+ ticks.forEach(tick => {
177
+ const tickEnd = this.getTickEnd(tick)
178
+ const dateKey = this.formatDateKey(tick, tickEnd)
179
+ resourceAllocations[resource.type][dateKey] = 0
180
+ })
181
+ })
182
+
183
+ this.tasks.forEach(task => {
184
+ const startDate = new Date(task.startDate)
185
+ const endDate = new Date(task.endDate)
186
+ let currentDate = startDate
187
+
188
+ while (currentDate <= endDate) {
189
+ const tick = ticks.find(t => currentDate >= t && currentDate < this.getTickEnd(t))
190
+ if (tick) {
191
+ const tickEnd = this.getTickEnd(tick)
192
+ const dateKey = this.formatDateKey(tick, tickEnd)
193
+
194
+ ;(task.resources || []).forEach(resource => {
195
+ if (resourceAllocations[resource.type][dateKey] !== undefined) {
196
+ resourceAllocations[resource.type][dateKey] += resource.allocated
197
+ }
198
+ })
199
+ }
200
+
201
+ currentDate.setDate(currentDate.getDate() + 1)
202
+ }
203
+ })
204
+
205
+ return resourceAllocations
206
+ }
207
+
208
+ getTickEnd(tick: Date): Date {
209
+ if (this.level === 'week-day') {
210
+ return d3.timeDay.offset(tick, 1)
211
+ } else if (this.level === 'quarter-week') {
212
+ return d3.timeWeek.offset(tick, 1)
213
+ } else if (this.level === 'year-month') {
214
+ return d3.timeMonth.offset(tick, 1)
215
+ } else if (this.level === 'year-quarter') {
216
+ return d3.timeMonth.offset(tick, 3)
217
+ }
218
+ return d3.timeDay.offset(tick, 1)
219
+ }
220
+
221
+ formatDateKey(start: Date, end: Date): string {
222
+ return `${start.toISOString().split('T')[0]}_${end.toISOString().split('T')[0]}`
223
+ }
224
+
225
+ handleWheelForBody(event: WheelEvent, container: HTMLDivElement) {
226
+ event.preventDefault()
227
+ event.stopPropagation()
228
+
229
+ const deltaModeFactor =
230
+ event.deltaMode === WheelEvent.DOM_DELTA_LINE
231
+ ? 16
232
+ : event.deltaMode === WheelEvent.DOM_DELTA_PAGE
233
+ ? container.clientHeight
234
+ : 1
235
+ const deltaX = event.deltaX * deltaModeFactor
236
+ const deltaY = event.deltaY * deltaModeFactor
237
+
238
+ container.scrollLeft += deltaX
239
+ container.scrollTop += deltaY
240
+
241
+ const customEvent = new CustomEvent('scroll', {
242
+ detail: {
243
+ scrollLeft: container.scrollLeft,
244
+ scrollTop: container.scrollTop
245
+ },
246
+ bubbles: true,
247
+ composed: true
248
+ })
249
+
250
+ this.host.dispatchEvent(customEvent)
251
+ }
252
+
253
+ handleTouchStartForBody(event: TouchEvent, container: HTMLDivElement) {
254
+ event.stopPropagation()
255
+
256
+ this.touchStartX = event.touches[0].clientX
257
+ this.touchStartY = event.touches[0].clientY
258
+
259
+ this.scrollStartX = container.scrollLeft
260
+ this.scrollStartY = container.scrollTop
261
+ }
262
+
263
+ handleTouchMoveForBody(event: TouchEvent, container: HTMLDivElement) {
264
+ const touchCurrentX = event.touches[0].clientX
265
+ const touchCurrentY = event.touches[0].clientY
266
+
267
+ const deltaX = this.touchStartX! - touchCurrentX
268
+ const deltaY = this.touchStartY! - touchCurrentY
269
+
270
+ container.scrollLeft = this.scrollStartX! + deltaX
271
+ container.scrollTop = this.scrollStartY! + deltaY
272
+
273
+ const customEvent = new CustomEvent('scroll', {
274
+ detail: {
275
+ scrollLeft: container.scrollLeft,
276
+ scrollTop: container.scrollTop
277
+ },
278
+ bubbles: true,
279
+ composed: true
280
+ })
281
+
282
+ this.host.dispatchEvent(customEvent)
283
+ }
284
+ }