@operato/gantt 7.0.8 → 7.0.21
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/ox-gantt-chart.d.ts +4 -1
- package/dist/src/ox-gantt-chart.js +13 -8
- package/dist/src/ox-gantt-chart.js.map +1 -1
- package/dist/src/ox-gantt-resize-splitter.js +8 -7
- package/dist/src/ox-gantt-resize-splitter.js.map +1 -1
- package/dist/src/ox-gantt.d.ts +27 -15
- package/dist/src/ox-gantt.js +208 -76
- package/dist/src/ox-gantt.js.map +1 -1
- package/dist/src/ox-resource-chart.d.ts +19 -0
- package/dist/src/ox-resource-chart.js +194 -0
- package/dist/src/ox-resource-chart.js.map +1 -0
- package/dist/src/types.d.ts +5 -0
- package/dist/src/types.js.map +1 -1
- package/dist/stories/ox-gantt.stories.js +66 -2
- package/dist/stories/ox-gantt.stories.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/ox-gantt-chart.ts +16 -9
- package/src/ox-gantt-resize-splitter.ts +8 -7
- package/src/ox-gantt.ts +209 -80
- package/src/ox-resource-chart.ts +192 -0
- package/src/types.ts +6 -0
- package/stories/ox-gantt.stories.ts +66 -2
- package/translations/en.json +2 -1
- package/translations/ko.json +2 -1
package/src/ox-gantt.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { LitElement, html, css, PropertyValues, PropertyValueMap } from 'lit'
|
|
1
|
+
import { LitElement, html, css, PropertyValues, PropertyValueMap, nothing, TemplateResult } from 'lit'
|
|
2
2
|
import i18next from 'i18next'
|
|
3
3
|
import { customElement, property, query, state } from 'lit/decorators.js'
|
|
4
4
|
import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
|
|
5
5
|
import { ScrollbarStyles } from '@operato/styles'
|
|
6
6
|
|
|
7
7
|
import './ox-gantt-chart.js'
|
|
8
|
+
import './ox-resource-chart.js'
|
|
8
9
|
import './ox-gantt-resize-splitter.js'
|
|
9
10
|
import { OxGanttChart } from './ox-gantt-chart.js'
|
|
10
|
-
import {
|
|
11
|
+
import { OxResourceChart } from './ox-resource-chart.js'
|
|
12
|
+
import { ColumnConfig, Task, TimeScale, Resource } from './types.js'
|
|
11
13
|
|
|
12
14
|
@customElement('ox-gantt')
|
|
13
15
|
export class OxGantt extends LitElement {
|
|
@@ -23,13 +25,24 @@ export class OxGantt extends LitElement {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
.container {
|
|
28
|
+
flex: 1;
|
|
29
|
+
|
|
26
30
|
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
27
32
|
width: 100%;
|
|
28
33
|
height: 100%;
|
|
29
34
|
box-sizing: border-box;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
.
|
|
37
|
+
.resource-container {
|
|
38
|
+
height: 300px;
|
|
39
|
+
|
|
40
|
+
display: flex;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.resource-table-container {
|
|
33
46
|
padding: 0;
|
|
34
47
|
overflow: auto;
|
|
35
48
|
overflow-y: overlay;
|
|
@@ -37,16 +50,34 @@ export class OxGantt extends LitElement {
|
|
|
37
50
|
border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
.chart-container {
|
|
53
|
+
.resource-chart-container {
|
|
41
54
|
flex: 1;
|
|
42
55
|
padding: 0;
|
|
43
56
|
box-sizing: border-box;
|
|
44
57
|
border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
.task-container {
|
|
61
|
+
flex: 1;
|
|
62
|
+
|
|
63
|
+
display: flex;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.task-table-container {
|
|
69
|
+
padding: 0;
|
|
70
|
+
overflow: auto;
|
|
71
|
+
overflow-y: overlay;
|
|
72
|
+
box-sizing: border-box;
|
|
73
|
+
border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.task-chart-container {
|
|
77
|
+
flex: 1;
|
|
78
|
+
padding: 0;
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
table {
|
|
@@ -91,6 +122,7 @@ export class OxGantt extends LitElement {
|
|
|
91
122
|
]
|
|
92
123
|
|
|
93
124
|
@property({ type: Array }) tasks: Task[] = []
|
|
125
|
+
@property({ type: Array }) resources: Resource[] = []
|
|
94
126
|
@property({ type: String, attribute: 'from-date' })
|
|
95
127
|
fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0] /* beginning of this year */
|
|
96
128
|
@property({ type: String, attribute: 'to-date' })
|
|
@@ -136,41 +168,93 @@ export class OxGantt extends LitElement {
|
|
|
136
168
|
visible: true,
|
|
137
169
|
width: 100,
|
|
138
170
|
order: 9
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'resources',
|
|
174
|
+
label: i18next.t('label.gantt-task-resources') || 'resources',
|
|
175
|
+
visible: true,
|
|
176
|
+
width: 100,
|
|
177
|
+
order: 10
|
|
139
178
|
}
|
|
140
179
|
]
|
|
141
180
|
|
|
142
181
|
@state() selectedTaskId?: string
|
|
143
182
|
|
|
144
|
-
@query('.container') container!: HTMLDivElement
|
|
145
183
|
@query('.task-container') taskContainer!: HTMLDivElement
|
|
146
|
-
@query('.
|
|
184
|
+
@query('.task-table-container') taskTableContainer!: HTMLDivElement
|
|
185
|
+
@query('.task-chart-container') taskChartContainer!: HTMLDivElement
|
|
186
|
+
@query('.resource-container') resourceContainer!: HTMLDivElement
|
|
187
|
+
@query('.resource-table-container') resourceTableContainer!: HTMLDivElement
|
|
188
|
+
@query('.resource-chart-container') resourceChartContainer!: HTMLDivElement
|
|
147
189
|
@query('table') taskTable!: HTMLTableElement
|
|
148
190
|
@query('ox-gantt-chart') ganttChart!: OxGanttChart
|
|
191
|
+
@query('ox-resource-chart') resourceChart!: OxResourceChart
|
|
149
192
|
|
|
150
193
|
private _startWidth: number = 0
|
|
194
|
+
private _startHeight: number = 0
|
|
151
195
|
private _isDragging: boolean = false
|
|
152
196
|
|
|
153
197
|
render() {
|
|
154
198
|
return html`
|
|
155
199
|
<div class="container md-typescale-body-medium">
|
|
156
|
-
<div class="task-container"
|
|
200
|
+
<div class="task-container">
|
|
201
|
+
<div class="task-table-container" @scroll=${this.syncScroll}>${this.renderTaskTable()}</div>
|
|
202
|
+
<ox-gantt-resize-splitter
|
|
203
|
+
vertical
|
|
204
|
+
@splitter-dragstart=${(e: CustomEvent) => this.resizeHorizontalStart(e)}
|
|
205
|
+
@splitter-drag=${(e: CustomEvent) => this.resizeHorizontalDrag(e)}
|
|
206
|
+
@splitter-dragend=${(e: CustomEvent) => this.resizeHorizontalEnd(e)}
|
|
207
|
+
></ox-gantt-resize-splitter>
|
|
208
|
+
<ox-gantt-chart
|
|
209
|
+
class="task-chart-container"
|
|
210
|
+
@scroll=${(e: CustomEvent) => {
|
|
211
|
+
this.syncScroll(e)
|
|
212
|
+
this.syncResourceChartScroll(e)
|
|
213
|
+
}}
|
|
214
|
+
.fromDate=${this.fromDate}
|
|
215
|
+
.toDate=${this.toDate}
|
|
216
|
+
.tasks=${this.tasks}
|
|
217
|
+
.selectedTaskId=${this.selectedTaskId}
|
|
218
|
+
.axisScale=${this.timeScale}
|
|
219
|
+
?extend-grid-lines=${this.extendGridLines}
|
|
220
|
+
@task-clicked=${this.handleTaskClicked}
|
|
221
|
+
></ox-gantt-chart>
|
|
222
|
+
</div>
|
|
157
223
|
<ox-gantt-resize-splitter
|
|
158
|
-
|
|
159
|
-
@splitter-dragstart=${(e: CustomEvent) => this.
|
|
160
|
-
@splitter-drag=${(e: CustomEvent) => this.
|
|
161
|
-
@splitter-dragend=${(e: CustomEvent) => this.
|
|
224
|
+
horizontal
|
|
225
|
+
@splitter-dragstart=${(e: CustomEvent) => this.resizeVerticalStart(e)}
|
|
226
|
+
@splitter-drag=${(e: CustomEvent) => this.resizeVerticalDrag(e)}
|
|
227
|
+
@splitter-dragend=${(e: CustomEvent) => this.resizeVerticalEnd(e)}
|
|
162
228
|
></ox-gantt-resize-splitter>
|
|
163
|
-
<
|
|
164
|
-
class="
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
229
|
+
<div class="resource-container">
|
|
230
|
+
<div class="resource-table-container">
|
|
231
|
+
<table>
|
|
232
|
+
<thead>
|
|
233
|
+
<tr>
|
|
234
|
+
<th style="width: 100px">type</th>
|
|
235
|
+
<th>allocated</th>
|
|
236
|
+
</tr>
|
|
237
|
+
</thead>
|
|
238
|
+
<tbody>
|
|
239
|
+
${this.resources.map(resource => this.renderResourceRow(resource))}
|
|
240
|
+
</tbody>
|
|
241
|
+
</table>
|
|
242
|
+
</div>
|
|
243
|
+
<ox-gantt-resize-splitter
|
|
244
|
+
vertical
|
|
245
|
+
@splitter-dragstart=${(e: CustomEvent) => this.resizeHorizontalStart(e)}
|
|
246
|
+
@splitter-drag=${(e: CustomEvent) => this.resizeHorizontalDrag(e)}
|
|
247
|
+
@splitter-dragend=${(e: CustomEvent) => this.resizeHorizontalEnd(e)}
|
|
248
|
+
></ox-gantt-resize-splitter>
|
|
249
|
+
<ox-resource-chart
|
|
250
|
+
class="resource-chart-container"
|
|
251
|
+
.fromDate=${this.fromDate}
|
|
252
|
+
.toDate=${this.toDate}
|
|
253
|
+
.resources=${this.resources}
|
|
254
|
+
.axisScale=${this.timeScale}
|
|
255
|
+
@scroll=${this.syncTaskChartScroll}
|
|
256
|
+
></ox-resource-chart>
|
|
257
|
+
</div>
|
|
174
258
|
</div>
|
|
175
259
|
`
|
|
176
260
|
}
|
|
@@ -180,8 +264,6 @@ export class OxGantt extends LitElement {
|
|
|
180
264
|
return
|
|
181
265
|
}
|
|
182
266
|
|
|
183
|
-
const mergedTasks = this.mergeTasksBySection(this.tasks)
|
|
184
|
-
|
|
185
267
|
// Sort columns by the configured order
|
|
186
268
|
const sortedColumns = [...this.columnConfig].sort((a, b) => a.order - b.order)
|
|
187
269
|
|
|
@@ -190,32 +272,47 @@ export class OxGantt extends LitElement {
|
|
|
190
272
|
<thead>
|
|
191
273
|
<tr>
|
|
192
274
|
${sortedColumns.map(col =>
|
|
193
|
-
col.visible ? html`<th style="width: ${col.width}px">${col.label}</th>` :
|
|
275
|
+
col.visible ? html`<th style="width: ${col.width}px">${col.label}</th>` : nothing
|
|
194
276
|
)}
|
|
195
277
|
</tr>
|
|
196
278
|
</thead>
|
|
197
279
|
<tbody>
|
|
198
|
-
${
|
|
199
|
-
({ task, showSection, rowSpan }) => html`
|
|
200
|
-
<tr class="${this.selectedTaskId === task.id ? 'selected' : ''}" @click=${() => this.selectTask(task.id)}>
|
|
201
|
-
${sortedColumns.map(col => {
|
|
202
|
-
if (col.name === 'section') {
|
|
203
|
-
return col.visible && showSection
|
|
204
|
-
? html`<td rowspan=${rowSpan} style="width: ${col.width}px">${task.section?.name || ''}</td>`
|
|
205
|
-
: null
|
|
206
|
-
}
|
|
207
|
-
return col.visible
|
|
208
|
-
? html`<td style="width: ${col.width}px">${this.getTaskFieldValue(task, col.name)}</td>`
|
|
209
|
-
: null
|
|
210
|
-
})}
|
|
211
|
-
</tr>
|
|
212
|
-
`
|
|
213
|
-
)}
|
|
280
|
+
${this.renderTaskRows(this.tasks)}
|
|
214
281
|
</tbody>
|
|
215
282
|
</table>
|
|
216
283
|
`
|
|
217
284
|
}
|
|
218
285
|
|
|
286
|
+
renderTaskRows(tasks: Task[], level: number = 0): TemplateResult[] {
|
|
287
|
+
return tasks.flatMap(task => [
|
|
288
|
+
this.renderTaskRow(task, level),
|
|
289
|
+
...this.renderTaskRows(task.children || [], level + 1)
|
|
290
|
+
])
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
renderTaskRow(task: Task, level: number) {
|
|
294
|
+
return html`
|
|
295
|
+
<tr class=${this.selectedTaskId === task.id ? 'selected' : ''} @click=${() => this.selectTask(task.id)}>
|
|
296
|
+
${this.columnConfig.map(col =>
|
|
297
|
+
col.visible
|
|
298
|
+
? html`<td class="task-row" style="padding-left: ${col.name == 'id' ? level * 20 : 0}px">
|
|
299
|
+
${this.getTaskFieldValue(task, col.name)}
|
|
300
|
+
</td>`
|
|
301
|
+
: null
|
|
302
|
+
)}
|
|
303
|
+
</tr>
|
|
304
|
+
`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
renderResourceRow(resource: Resource) {
|
|
308
|
+
return html`
|
|
309
|
+
<tr>
|
|
310
|
+
<td>${resource.type}</td>
|
|
311
|
+
<td>${resource.allocated}</td>
|
|
312
|
+
</tr>
|
|
313
|
+
`
|
|
314
|
+
}
|
|
315
|
+
|
|
219
316
|
getTaskFieldValue(task: Task, field: string) {
|
|
220
317
|
switch (field) {
|
|
221
318
|
case 'id':
|
|
@@ -236,34 +333,13 @@ export class OxGantt extends LitElement {
|
|
|
236
333
|
return task.progress || ''
|
|
237
334
|
case 'dependsOn':
|
|
238
335
|
return task.dependsOn || ''
|
|
336
|
+
case 'resources':
|
|
337
|
+
return task.resources?.map(res => `${res.type}: ${res.allocated}`).join(', ') || ''
|
|
239
338
|
default:
|
|
240
339
|
return ''
|
|
241
340
|
}
|
|
242
341
|
}
|
|
243
342
|
|
|
244
|
-
mergeTasksBySection(tasks: Task[]): Array<{ task: Task; showSection: boolean; rowSpan: number }> {
|
|
245
|
-
const mergedTasks = [] as Array<{ task: Task; showSection: boolean; rowSpan: number }>
|
|
246
|
-
let currentSection = ''
|
|
247
|
-
let rowSpan = 0
|
|
248
|
-
|
|
249
|
-
;(tasks || []).forEach((task, index) => {
|
|
250
|
-
if (task.section?.name !== currentSection) {
|
|
251
|
-
currentSection = task.section?.name || ''
|
|
252
|
-
rowSpan = 1
|
|
253
|
-
mergedTasks.push({ task, showSection: true, rowSpan: 1 })
|
|
254
|
-
} else {
|
|
255
|
-
mergedTasks[mergedTasks.length - rowSpan].rowSpan = ++rowSpan
|
|
256
|
-
mergedTasks.push({ task, showSection: false, rowSpan: 0 })
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (index === tasks.length - 1 && rowSpan > 0) {
|
|
260
|
-
mergedTasks[mergedTasks.length - rowSpan].rowSpan = rowSpan
|
|
261
|
-
}
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
return mergedTasks
|
|
265
|
-
}
|
|
266
|
-
|
|
267
343
|
selectTask(taskId: string) {
|
|
268
344
|
this.selectedTaskId = taskId
|
|
269
345
|
}
|
|
@@ -273,16 +349,48 @@ export class OxGantt extends LitElement {
|
|
|
273
349
|
}
|
|
274
350
|
|
|
275
351
|
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
|
276
|
-
this.
|
|
352
|
+
this.taskTableContainer.style.width = '254px'
|
|
353
|
+
this.resourceTableContainer.style.width = '254px'
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
updated(changedProperties: PropertyValues<this>) {
|
|
357
|
+
if (changedProperties.has('tasks')) {
|
|
358
|
+
this.resources = this.aggregateResources(this.tasks)
|
|
359
|
+
}
|
|
277
360
|
}
|
|
278
361
|
|
|
279
|
-
|
|
362
|
+
aggregateResources(tasks: Task[]): Resource[] {
|
|
363
|
+
const resourceMap: { [key: string]: number } = {}
|
|
364
|
+
|
|
365
|
+
const traverseTasks = (tasks: Task[]) => {
|
|
366
|
+
tasks.forEach(task => {
|
|
367
|
+
if (task.resources) {
|
|
368
|
+
task.resources.forEach(resource => {
|
|
369
|
+
if (!resourceMap[resource.type]) {
|
|
370
|
+
resourceMap[resource.type] = 0
|
|
371
|
+
}
|
|
372
|
+
resourceMap[resource.type] += resource.allocated
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
if (task.children) {
|
|
376
|
+
traverseTasks(task.children)
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
traverseTasks(tasks)
|
|
382
|
+
|
|
383
|
+
return Object.keys(resourceMap).map(type => ({
|
|
384
|
+
type,
|
|
385
|
+
allocated: resourceMap[type]
|
|
386
|
+
}))
|
|
387
|
+
}
|
|
280
388
|
|
|
281
389
|
syncScroll(e: Event) {
|
|
282
390
|
const target = e.target as HTMLElement
|
|
283
|
-
if (target === this.
|
|
391
|
+
if (target === this.taskTableContainer) {
|
|
284
392
|
// Sync chart scroll with table scroll
|
|
285
|
-
const newScrollTop = this.
|
|
393
|
+
const newScrollTop = this.taskTableContainer.scrollTop
|
|
286
394
|
this.ganttChart.setVerticalOffset(newScrollTop)
|
|
287
395
|
|
|
288
396
|
// Ensure chart scroll matches table scroll
|
|
@@ -292,29 +400,50 @@ export class OxGantt extends LitElement {
|
|
|
292
400
|
} else if (target === this.ganttChart) {
|
|
293
401
|
// Sync table scroll with chart scroll
|
|
294
402
|
const newScrollTop = this.ganttChart.getVerticalOffset()
|
|
295
|
-
this.
|
|
403
|
+
this.taskTableContainer.scrollTop = newScrollTop
|
|
296
404
|
|
|
297
405
|
// Ensure table scroll matches chart scroll
|
|
298
|
-
this.ganttChart.setVerticalOffset(this.
|
|
406
|
+
this.ganttChart.setVerticalOffset(this.taskTableContainer.scrollTop)
|
|
299
407
|
}
|
|
300
408
|
}
|
|
301
409
|
|
|
302
|
-
|
|
410
|
+
syncResourceChartScroll(e: CustomEvent) {
|
|
411
|
+
this.resourceChart.renderRoot!.querySelector('div[chart-container]')!.scrollLeft = e.detail.offset
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
syncTaskChartScroll(e: CustomEvent) {
|
|
415
|
+
this.ganttChart.renderRoot!.querySelector('div[chart-container]')!.scrollLeft = e.detail.offset
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
resizeHorizontalStart(e: CustomEvent) {
|
|
303
419
|
this._startWidth = ((e.target as HTMLElement)?.previousElementSibling as HTMLElement)?.offsetWidth
|
|
304
420
|
this._isDragging = true
|
|
305
421
|
}
|
|
306
422
|
|
|
307
|
-
|
|
423
|
+
resizeHorizontalDrag(e: CustomEvent) {
|
|
308
424
|
if (!this._isDragging) return
|
|
309
425
|
const delta = e.detail
|
|
310
|
-
|
|
426
|
+
this.taskTableContainer.style.width = `${this._startWidth + delta.x}px`
|
|
427
|
+
this.resourceTableContainer.style.width = `${this._startWidth + delta.x}px`
|
|
428
|
+
}
|
|
311
429
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
430
|
+
resizeHorizontalEnd(e: CustomEvent) {
|
|
431
|
+
this._isDragging = false
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
resizeVerticalStart(e: CustomEvent) {
|
|
435
|
+
this._startHeight = ((e.target as HTMLElement)?.nextElementSibling as HTMLElement)?.offsetHeight
|
|
436
|
+
this._isDragging = true
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
resizeVerticalDrag(e: CustomEvent) {
|
|
440
|
+
if (!this._isDragging) return
|
|
441
|
+
const delta = e.detail
|
|
442
|
+
|
|
443
|
+
this.resourceContainer.style.height = `${this._startHeight - delta.y}px`
|
|
315
444
|
}
|
|
316
445
|
|
|
317
|
-
|
|
446
|
+
resizeVerticalEnd(e: CustomEvent) {
|
|
318
447
|
this._isDragging = false
|
|
319
448
|
}
|
|
320
449
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { LitElement, html, css, PropertyValues } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
import * as d3 from 'd3'
|
|
4
|
+
|
|
5
|
+
import { ScrollbarStyles } from '@operato/styles'
|
|
6
|
+
|
|
7
|
+
@customElement('ox-resource-chart')
|
|
8
|
+
export class OxResourceChart extends LitElement {
|
|
9
|
+
static styles = [
|
|
10
|
+
ScrollbarStyles,
|
|
11
|
+
css`
|
|
12
|
+
:host {
|
|
13
|
+
display: flex;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
position: relative;
|
|
16
|
+
}
|
|
17
|
+
div[chart-container] {
|
|
18
|
+
display: block;
|
|
19
|
+
overflow-x: auto;
|
|
20
|
+
overflow-y: hidden;
|
|
21
|
+
}
|
|
22
|
+
svg {
|
|
23
|
+
min-width: 100%;
|
|
24
|
+
}
|
|
25
|
+
svg[header] {
|
|
26
|
+
display: block;
|
|
27
|
+
min-width: 100%;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
border-bottom: 1px solid var(--ox-gantt-header-border-color, var(--md-sys-color-on-secondary-container));
|
|
30
|
+
height: 40px;
|
|
31
|
+
background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
|
|
32
|
+
color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
|
|
33
|
+
position: sticky;
|
|
34
|
+
top: 0;
|
|
35
|
+
z-index: 1;
|
|
36
|
+
}
|
|
37
|
+
svg[body] {
|
|
38
|
+
display: block;
|
|
39
|
+
min-width: 100%;
|
|
40
|
+
}
|
|
41
|
+
.top-axis text,
|
|
42
|
+
.bottom-axis text {
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
}
|
|
45
|
+
.grid line {
|
|
46
|
+
stroke: #e0e0e0;
|
|
47
|
+
}
|
|
48
|
+
.grid path {
|
|
49
|
+
stroke-width: 0;
|
|
50
|
+
}
|
|
51
|
+
.no-select {
|
|
52
|
+
user-select: none;
|
|
53
|
+
}
|
|
54
|
+
.selected {
|
|
55
|
+
stroke-width: 2px;
|
|
56
|
+
stroke: var(--ox-gantt-selected-color, blue);
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
@property({ type: String }) fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0]
|
|
62
|
+
@property({ type: String }) toDate: string = new Date(new Date().getFullYear(), 11, 31).toISOString().split('T')[0]
|
|
63
|
+
@property({ type: String }) axisScale: string = 'week-day'
|
|
64
|
+
@property({ type: Array }) resources: any[] = []
|
|
65
|
+
|
|
66
|
+
private _timeScale: any
|
|
67
|
+
private _baseWidth: number = 30
|
|
68
|
+
private _level: string = 'week-day'
|
|
69
|
+
|
|
70
|
+
render() {
|
|
71
|
+
return html`
|
|
72
|
+
<div chart-container>
|
|
73
|
+
<svg header></svg>
|
|
74
|
+
<svg body></svg>
|
|
75
|
+
</div>
|
|
76
|
+
`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
updated(changedProperties: PropertyValues<this>) {
|
|
80
|
+
if (changedProperties.has('fromDate') || changedProperties.has('toDate') || changedProperties.has('axisScale')) {
|
|
81
|
+
this.updateTimeScale()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (changedProperties.has('resources')) {
|
|
85
|
+
this.drawBody()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.drawAxes()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
updateTimeScale() {
|
|
92
|
+
this._timeScale = d3
|
|
93
|
+
.scaleTime()
|
|
94
|
+
.domain([d3.timeDay.floor(new Date(this.fromDate)), d3.timeDay.offset(new Date(this.toDate), 1)])
|
|
95
|
+
.range([0, this.getChartWidth()])
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getChartWidth() {
|
|
99
|
+
const totalDays = d3.timeDay.count(new Date(this.fromDate), d3.timeDay.offset(new Date(this.toDate), 1))
|
|
100
|
+
return (totalDays * this._baseWidth) / this.getScale()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getScale() {
|
|
104
|
+
if (this._level == 'quarter-week') {
|
|
105
|
+
return 7 / 2
|
|
106
|
+
} else if (this._level == 'year-month') {
|
|
107
|
+
return 30 / 2
|
|
108
|
+
} else if (this._level == 'year-quarter') {
|
|
109
|
+
return 90 / 2
|
|
110
|
+
}
|
|
111
|
+
return 1
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
drawAxes() {
|
|
115
|
+
const header = d3.select(this.renderRoot.querySelector('svg[header]')!)
|
|
116
|
+
|
|
117
|
+
header.selectAll('*').remove()
|
|
118
|
+
header.attr('width', this.getChartWidth())
|
|
119
|
+
|
|
120
|
+
const axisTop = d3.axisTop(this._timeScale).tickSize(-20)
|
|
121
|
+
const axisBottom = d3.axisBottom(this._timeScale).tickSize(-20)
|
|
122
|
+
|
|
123
|
+
header.append('g').attr('class', 'top-axis').attr('transform', 'translate(0, 20)').call(axisTop)
|
|
124
|
+
header.append('g').attr('class', 'bottom-axis').attr('transform', 'translate(0, 40)').call(axisBottom)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
drawBody() {
|
|
128
|
+
const body = d3.select(this.renderRoot.querySelector('svg[body]')!)
|
|
129
|
+
|
|
130
|
+
body.selectAll('*').remove()
|
|
131
|
+
body.attr('width', this.getChartWidth())
|
|
132
|
+
|
|
133
|
+
// Add grid lines for resource chart
|
|
134
|
+
if (this.resources.length > 0) {
|
|
135
|
+
const addVerticalGridLines = (ticks: Date[]) => {
|
|
136
|
+
ticks.forEach(tick => {
|
|
137
|
+
const startX = this._timeScale(tick)
|
|
138
|
+
body
|
|
139
|
+
.append('line')
|
|
140
|
+
.attr('x1', startX)
|
|
141
|
+
.attr('x2', startX)
|
|
142
|
+
.attr('y1', 0)
|
|
143
|
+
.attr('y2', this.resources.length * 30) // Adjust according to the number of resource rows
|
|
144
|
+
.attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
|
|
145
|
+
.attr('stroke-width', 1)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const addHorizontalGridLines = () => {
|
|
150
|
+
this.resources.forEach((_, index) => {
|
|
151
|
+
const y = (index + 1) * 30
|
|
152
|
+
body
|
|
153
|
+
.append('line')
|
|
154
|
+
.attr('x1', 0)
|
|
155
|
+
.attr('x2', this.getChartWidth())
|
|
156
|
+
.attr('y1', y)
|
|
157
|
+
.attr('y2', y)
|
|
158
|
+
.attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
|
|
159
|
+
.attr('stroke-width', 1)
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (this._level === 'week-day') {
|
|
164
|
+
const dayTicks = d3.timeDay
|
|
165
|
+
.every(1)!
|
|
166
|
+
.range(d3.timeDay.floor(new Date(this.fromDate)), d3.timeDay.offset(new Date(this.toDate), 1))
|
|
167
|
+
addVerticalGridLines(dayTicks)
|
|
168
|
+
} else if (this._level === 'quarter-week') {
|
|
169
|
+
const weekTicks = d3.timeWeek
|
|
170
|
+
.every(1)!
|
|
171
|
+
.range(d3.timeWeek.floor(new Date(this.fromDate)), d3.timeWeek.offset(new Date(this.toDate), 1))
|
|
172
|
+
addVerticalGridLines(weekTicks)
|
|
173
|
+
} else if (this._level === 'year-month') {
|
|
174
|
+
const monthTicks = d3.timeMonth
|
|
175
|
+
.every(1)!
|
|
176
|
+
.range(d3.timeMonth.floor(new Date(this.fromDate)), d3.timeMonth.offset(new Date(this.toDate), 1))
|
|
177
|
+
addVerticalGridLines(monthTicks)
|
|
178
|
+
} else if (this._level === 'year-quarter') {
|
|
179
|
+
const quarterTicks = d3.timeMonth
|
|
180
|
+
.every(3)!
|
|
181
|
+
.range(d3.timeMonth.floor(new Date(this.fromDate)), d3.timeMonth.offset(new Date(this.toDate), 3))
|
|
182
|
+
addVerticalGridLines(quarterTicks)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
addHorizontalGridLines()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
syncScroll(e: CustomEvent) {
|
|
190
|
+
this.shadowRoot!.querySelector('div[chart-container]')!.scrollLeft = e.detail.offset
|
|
191
|
+
}
|
|
192
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface Task {
|
|
|
13
13
|
length: string
|
|
14
14
|
tags?: TaskTag[]
|
|
15
15
|
children?: Task[]
|
|
16
|
+
resources?: Resource[]
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export interface Section {
|
|
@@ -28,3 +29,8 @@ export interface ColumnConfig {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export type TimeScale = 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'
|
|
32
|
+
|
|
33
|
+
export interface Resource {
|
|
34
|
+
type: string
|
|
35
|
+
allocated: number
|
|
36
|
+
}
|