@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/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 { ColumnConfig, Task, TimeScale } from './types.js'
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
- .task-container {
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
- ox-gantt-resize-splitter {
48
- width: 4px;
49
- cursor: ew-resize;
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('.chart-container') chartContainer!: HTMLDivElement
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" @scroll=${this.syncScroll}>${this.renderTaskTable()}</div>
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
- vertical
159
- @splitter-dragstart=${(e: CustomEvent) => this.resizeStart(e)}
160
- @splitter-drag=${(e: CustomEvent) => this.resizeDrag(e)}
161
- @splitter-dragend=${(e: CustomEvent) => this.resizeEnd(e)}
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
- <ox-gantt-chart
164
- class="chart-container"
165
- @scroll=${this.syncScroll}
166
- .fromDate=${this.fromDate}
167
- .toDate=${this.toDate}
168
- .tasks=${this.tasks}
169
- .selectedTaskId=${this.selectedTaskId}
170
- .axisScale=${this.timeScale}
171
- ?extend-grid-lines=${this.extendGridLines}
172
- @task-clicked=${this.handleTaskClicked}
173
- ></ox-gantt-chart>
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>` : null
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
- ${mergedTasks.map(
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.taskContainer.style.width = '254px'
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
- updated(changes: PropertyValues<this>) {}
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.taskContainer) {
391
+ if (target === this.taskTableContainer) {
284
392
  // Sync chart scroll with table scroll
285
- const newScrollTop = this.taskContainer.scrollTop
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.taskContainer.scrollTop = newScrollTop
403
+ this.taskTableContainer.scrollTop = newScrollTop
296
404
 
297
405
  // Ensure table scroll matches chart scroll
298
- this.ganttChart.setVerticalOffset(this.taskContainer.scrollTop)
406
+ this.ganttChart.setVerticalOffset(this.taskTableContainer.scrollTop)
299
407
  }
300
408
  }
301
409
 
302
- resizeStart(e: CustomEvent) {
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
- resizeDrag(e: CustomEvent) {
423
+ resizeHorizontalDrag(e: CustomEvent) {
308
424
  if (!this._isDragging) return
309
425
  const delta = e.detail
310
- const taskContainer = this.taskContainer
426
+ this.taskTableContainer.style.width = `${this._startWidth + delta.x}px`
427
+ this.resourceTableContainer.style.width = `${this._startWidth + delta.x}px`
428
+ }
311
429
 
312
- if (taskContainer) {
313
- taskContainer.style.width = `${this._startWidth + delta.x}px`
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
- resizeEnd(e: CustomEvent) {
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
+ }