@operato/gantt 7.0.59 → 7.0.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/src/draw-time-scale-header.js +1 -1
- package/dist/src/draw-time-scale-header.js.map +1 -1
- package/dist/src/gantt-chart-controller.d.ts +2 -0
- package/dist/src/gantt-chart-controller.js +23 -8
- package/dist/src/gantt-chart-controller.js.map +1 -1
- package/dist/src/ox-gantt-chart.js +38 -36
- package/dist/src/ox-gantt-chart.js.map +1 -1
- package/dist/src/ox-gantt.d.ts +6 -4
- package/dist/src/ox-gantt.js +73 -78
- package/dist/src/ox-gantt.js.map +1 -1
- package/dist/src/ox-resource-chart.d.ts +11 -27
- package/dist/src/ox-resource-chart.js +55 -227
- package/dist/src/ox-resource-chart.js.map +1 -1
- package/dist/src/ox-task-tooltip.js +3 -3
- package/dist/src/ox-task-tooltip.js.map +1 -1
- package/dist/src/resource-chart-controller.d.ts +44 -0
- package/dist/src/resource-chart-controller.js +225 -0
- package/dist/src/resource-chart-controller.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/draw-time-scale-header.ts +1 -1
- package/src/gantt-chart-controller.ts +31 -7
- package/src/ox-gantt-chart.ts +43 -36
- package/src/ox-gantt.ts +71 -78
- package/src/ox-resource-chart.ts +57 -253
- package/src/ox-task-tooltip.ts +3 -3
- package/src/resource-chart-controller.ts +283 -0
- package/tsconfig.json +1 -0
package/src/ox-resource-chart.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { LitElement, html, css
|
|
1
|
+
import { LitElement, html, css } from 'lit'
|
|
2
2
|
import { customElement, property, query } from 'lit/decorators.js'
|
|
3
|
-
import
|
|
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:
|
|
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-
|
|
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:
|
|
75
|
-
@property({ type: Array }) resources:
|
|
57
|
+
@property({ type: String }) axisScale: TimeScale = 'week-day'
|
|
58
|
+
@property({ type: Array }) resources: Resource[] = []
|
|
76
59
|
@property({ type: Array }) tasks: Task[] = []
|
|
77
60
|
|
|
78
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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:
|
|
98
|
-
if (changedProperties.has('fromDate') || changedProperties.has('toDate')
|
|
99
|
-
this.
|
|
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')
|
|
103
|
-
this.
|
|
77
|
+
if (changedProperties.has('resources')) {
|
|
78
|
+
this.resourceChartController.setResources(this.resources)
|
|
79
|
+
this.updateChartHeight() // 리소스의 수에 따라 차트의 높이를 업데이트합니다.
|
|
104
80
|
}
|
|
105
81
|
|
|
106
|
-
|
|
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
|
-
|
|
217
|
-
this.
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
314
|
-
this.
|
|
121
|
+
this.resourceChartController.syncTimeScale(detail)
|
|
122
|
+
this.requestUpdate()
|
|
123
|
+
}
|
|
315
124
|
|
|
316
|
-
|
|
317
|
-
this.
|
|
318
|
-
this.drawBody()
|
|
125
|
+
getHorizontalOffset(): number {
|
|
126
|
+
return this.container.scrollLeft
|
|
319
127
|
}
|
|
320
128
|
|
|
321
|
-
|
|
322
|
-
this.container.
|
|
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
|
-
|
|
330
|
-
this.container.
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
getHorizontalOffset(): number {
|
|
334
|
-
return this.container.scrollLeft
|
|
137
|
+
setVerticalOffset(offset: number) {
|
|
138
|
+
this.container.scrollTop = offset
|
|
335
139
|
}
|
|
336
140
|
}
|
package/src/ox-task-tooltip.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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,283 @@
|
|
|
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
|
+
.text(allocation)
|
|
152
|
+
.style('font-size', '12px')
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Draw horizontal grid lines for each resource row
|
|
158
|
+
for (let i = 0; i <= this.resources.length; i++) {
|
|
159
|
+
body
|
|
160
|
+
.append('line')
|
|
161
|
+
.attr('x1', 0)
|
|
162
|
+
.attr('x2', this.getChartWidth())
|
|
163
|
+
.attr('y1', i * 30)
|
|
164
|
+
.attr('y2', i * 30)
|
|
165
|
+
.attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
|
|
166
|
+
.attr('stroke-width', 1)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
calculateResourceAllocations(ticks: Date[]): { [key: string]: { [key: string]: number } } {
|
|
171
|
+
const resourceAllocations: { [key: string]: { [key: string]: number } } = {}
|
|
172
|
+
|
|
173
|
+
this.resources.forEach(resource => {
|
|
174
|
+
resourceAllocations[resource.type] = {}
|
|
175
|
+
ticks.forEach(tick => {
|
|
176
|
+
const tickEnd = this.getTickEnd(tick)
|
|
177
|
+
const dateKey = this.formatDateKey(tick, tickEnd)
|
|
178
|
+
resourceAllocations[resource.type][dateKey] = 0
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
this.tasks.forEach(task => {
|
|
183
|
+
const startDate = new Date(task.startDate)
|
|
184
|
+
const endDate = new Date(task.endDate)
|
|
185
|
+
let currentDate = startDate
|
|
186
|
+
|
|
187
|
+
while (currentDate <= endDate) {
|
|
188
|
+
const tick = ticks.find(t => currentDate >= t && currentDate < this.getTickEnd(t))
|
|
189
|
+
if (tick) {
|
|
190
|
+
const tickEnd = this.getTickEnd(tick)
|
|
191
|
+
const dateKey = this.formatDateKey(tick, tickEnd)
|
|
192
|
+
|
|
193
|
+
;(task.resources || []).forEach(resource => {
|
|
194
|
+
if (resourceAllocations[resource.type][dateKey] !== undefined) {
|
|
195
|
+
resourceAllocations[resource.type][dateKey] += resource.allocated
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
currentDate.setDate(currentDate.getDate() + 1)
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
return resourceAllocations
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getTickEnd(tick: Date): Date {
|
|
208
|
+
if (this.level === 'week-day') {
|
|
209
|
+
return d3.timeDay.offset(tick, 1)
|
|
210
|
+
} else if (this.level === 'quarter-week') {
|
|
211
|
+
return d3.timeWeek.offset(tick, 1)
|
|
212
|
+
} else if (this.level === 'year-month') {
|
|
213
|
+
return d3.timeMonth.offset(tick, 1)
|
|
214
|
+
} else if (this.level === 'year-quarter') {
|
|
215
|
+
return d3.timeMonth.offset(tick, 3)
|
|
216
|
+
}
|
|
217
|
+
return d3.timeDay.offset(tick, 1)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
formatDateKey(start: Date, end: Date): string {
|
|
221
|
+
return `${start.toISOString().split('T')[0]}_${end.toISOString().split('T')[0]}`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
handleWheelForBody(event: WheelEvent, container: HTMLDivElement) {
|
|
225
|
+
event.preventDefault()
|
|
226
|
+
event.stopPropagation()
|
|
227
|
+
|
|
228
|
+
const deltaModeFactor =
|
|
229
|
+
event.deltaMode === WheelEvent.DOM_DELTA_LINE
|
|
230
|
+
? 16
|
|
231
|
+
: event.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
|
232
|
+
? container.clientHeight
|
|
233
|
+
: 1
|
|
234
|
+
const deltaX = event.deltaX * deltaModeFactor
|
|
235
|
+
const deltaY = event.deltaY * deltaModeFactor
|
|
236
|
+
|
|
237
|
+
container.scrollLeft += deltaX
|
|
238
|
+
container.scrollTop += deltaY
|
|
239
|
+
|
|
240
|
+
const customEvent = new CustomEvent('scroll', {
|
|
241
|
+
detail: {
|
|
242
|
+
scrollLeft: container.scrollLeft,
|
|
243
|
+
scrollTop: container.scrollTop
|
|
244
|
+
},
|
|
245
|
+
bubbles: true,
|
|
246
|
+
composed: true
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
this.host.dispatchEvent(customEvent)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
handleTouchStartForBody(event: TouchEvent, container: HTMLDivElement) {
|
|
253
|
+
event.stopPropagation()
|
|
254
|
+
|
|
255
|
+
this.touchStartX = event.touches[0].clientX
|
|
256
|
+
this.touchStartY = event.touches[0].clientY
|
|
257
|
+
|
|
258
|
+
this.scrollStartX = container.scrollLeft
|
|
259
|
+
this.scrollStartY = container.scrollTop
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
handleTouchMoveForBody(event: TouchEvent, container: HTMLDivElement) {
|
|
263
|
+
const touchCurrentX = event.touches[0].clientX
|
|
264
|
+
const touchCurrentY = event.touches[0].clientY
|
|
265
|
+
|
|
266
|
+
const deltaX = this.touchStartX! - touchCurrentX
|
|
267
|
+
const deltaY = this.touchStartY! - touchCurrentY
|
|
268
|
+
|
|
269
|
+
container.scrollLeft = this.scrollStartX! + deltaX
|
|
270
|
+
container.scrollTop = this.scrollStartY! + deltaY
|
|
271
|
+
|
|
272
|
+
const customEvent = new CustomEvent('scroll', {
|
|
273
|
+
detail: {
|
|
274
|
+
scrollLeft: container.scrollLeft,
|
|
275
|
+
scrollTop: container.scrollTop
|
|
276
|
+
},
|
|
277
|
+
bubbles: true,
|
|
278
|
+
composed: true
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
this.host.dispatchEvent(customEvent)
|
|
282
|
+
}
|
|
283
|
+
}
|