@operato/gantt 7.0.20 → 7.0.22

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/src/draw-time-scale-header.d.ts +5 -0
  3. package/dist/src/draw-time-scale-header.js +154 -0
  4. package/dist/src/draw-time-scale-header.js.map +1 -0
  5. package/dist/src/gantt-chart-controller.d.ts +1 -1
  6. package/dist/src/gantt-chart-controller.js +6 -161
  7. package/dist/src/gantt-chart-controller.js.map +1 -1
  8. package/dist/src/ox-gantt-chart.d.ts +6 -1
  9. package/dist/src/ox-gantt-chart.js +39 -8
  10. package/dist/src/ox-gantt-chart.js.map +1 -1
  11. package/dist/src/ox-gantt-resize-splitter.js +8 -7
  12. package/dist/src/ox-gantt-resize-splitter.js.map +1 -1
  13. package/dist/src/ox-gantt.d.ts +24 -15
  14. package/dist/src/ox-gantt.js +224 -82
  15. package/dist/src/ox-gantt.js.map +1 -1
  16. package/dist/src/ox-resource-chart.d.ts +42 -0
  17. package/dist/src/ox-resource-chart.js +317 -0
  18. package/dist/src/ox-resource-chart.js.map +1 -0
  19. package/dist/src/types.d.ts +5 -0
  20. package/dist/src/types.js.map +1 -1
  21. package/dist/stories/ox-gantt.stories.js +75 -4
  22. package/dist/stories/ox-gantt.stories.js.map +1 -1
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +2 -2
  25. package/src/draw-time-scale-header.ts +192 -0
  26. package/src/gantt-chart-controller.ts +6 -189
  27. package/src/ox-gantt-chart.ts +46 -9
  28. package/src/ox-gantt-resize-splitter.ts +8 -7
  29. package/src/ox-gantt.ts +231 -86
  30. package/src/ox-resource-chart.ts +328 -0
  31. package/src/types.ts +6 -0
  32. package/stories/ox-gantt.stories.ts +75 -4
  33. package/translations/en.json +4 -1
  34. package/translations/ja.json +3 -1
  35. package/translations/ko.json +4 -1
  36. package/translations/ms.json +3 -1
  37. package/translations/zh.json +3 -1
@@ -0,0 +1,328 @@
1
+ import { LitElement, html, css, PropertyValues } from 'lit'
2
+ import { customElement, property, query } from 'lit/decorators.js'
3
+ import * as d3 from 'd3'
4
+
5
+ import { ScrollbarStyles } from '@operato/styles'
6
+
7
+ import { drawTimeScaleHeader } from './draw-time-scale-header'
8
+ import { Task, TimeScale } from './types'
9
+
10
+ @customElement('ox-resource-chart')
11
+ export class OxResourceChart extends LitElement {
12
+ static styles = [
13
+ ScrollbarStyles,
14
+ css`
15
+ :host {
16
+ display: flex;
17
+ overflow: hidden;
18
+ position: relative;
19
+ }
20
+
21
+ div[chart-container] {
22
+ display: flex;
23
+ flex-direction: column;
24
+
25
+ overflow-x: auto;
26
+ overflow-y: auto;
27
+ }
28
+
29
+ svg[header] {
30
+ display: block;
31
+ min-width: 100%;
32
+ box-sizing: border-box;
33
+ border-bottom: 1px solid var(--ox-gantt-header-border-color, var(--md-sys-color-on-secondary-container));
34
+ height: 40px;
35
+ background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
36
+ color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
37
+ position: sticky;
38
+ top: 0;
39
+ z-index: 1;
40
+ }
41
+
42
+ svg[body] {
43
+ flex: 1;
44
+ display: block;
45
+ min-width: 100%;
46
+ }
47
+
48
+ .top-axis text,
49
+ .bottom-axis text {
50
+ font-size: 12px;
51
+ }
52
+
53
+ .grid line {
54
+ stroke: #e0e0e0;
55
+ }
56
+
57
+ .grid path {
58
+ stroke-width: 0;
59
+ }
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
+ `
70
+ ]
71
+
72
+ @property({ type: String }) fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0]
73
+ @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[] = []
76
+ @property({ type: Array }) tasks: Task[] = []
77
+
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
85
+ @query('svg[header]') svgHeader!: SVGSVGElement
86
+ @query('svg[body]') svgBody!: SVGSVGElement
87
+
88
+ render() {
89
+ return html`
90
+ <div chart-container>
91
+ <svg header></svg>
92
+ <svg body></svg>
93
+ </div>
94
+ `
95
+ }
96
+
97
+ updated(changedProperties: PropertyValues<this>) {
98
+ if (changedProperties.has('fromDate') || changedProperties.has('toDate') || changedProperties.has('axisScale')) {
99
+ this.updateTimeScale()
100
+ }
101
+
102
+ if (changedProperties.has('resources') || changedProperties.has('tasks')) {
103
+ this.drawBody()
104
+ }
105
+
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
131
+ }
132
+ return 1
133
+ }
134
+
135
+ drawAxes() {
136
+ drawTimeScaleHeader(this.svgHeader as any, this.timeScale, this.fromTime!, this.toTime!, this.baseWidth, this.level)
137
+ }
138
+
139
+ drawBody() {
140
+ const body = d3.select(this.svgBody)
141
+
142
+ body.selectAll('*').remove()
143
+ body.attr('width', this.getChartWidth())
144
+
145
+ if (this.resources.length > 0) {
146
+ const chartHeight = this.resources.length * 30
147
+
148
+ const addVerticalGridLines = (ticks: Date[]) => {
149
+ ticks.forEach(tick => {
150
+ const startX = this.timeScale(tick)
151
+ body
152
+ .append('line')
153
+ .attr('x1', startX)
154
+ .attr('x2', startX)
155
+ .attr('y1', 0)
156
+ .attr('y2', chartHeight) // Ensure vertical lines extend to the bottom
157
+ .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
158
+ .attr('stroke-width', 1)
159
+ })
160
+ }
161
+
162
+ const addHorizontalGridLines = () => {
163
+ this.resources.forEach((_, index) => {
164
+ const y = (index + 1) * 30
165
+ body
166
+ .append('line')
167
+ .attr('x1', 0)
168
+ .attr('x2', this.getChartWidth())
169
+ .attr('y1', y)
170
+ .attr('y2', y)
171
+ .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
172
+ .attr('stroke-width', 1)
173
+ })
174
+
175
+ // Ensure the last horizontal line extends to the bottom of the chart
176
+ body
177
+ .append('line')
178
+ .attr('x1', 0)
179
+ .attr('x2', this.getChartWidth())
180
+ .attr('y1', chartHeight)
181
+ .attr('y2', chartHeight)
182
+ .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
183
+ .attr('stroke-width', 1)
184
+ }
185
+
186
+ let ticks
187
+ if (this.level === 'week-day') {
188
+ ticks = d3.timeDay
189
+ .every(1)!
190
+ .range(d3.timeDay.floor(new Date(this.fromDate)), d3.timeDay.offset(new Date(this.toDate), 1))
191
+ } else if (this.level === 'quarter-week') {
192
+ ticks = d3.timeWeek
193
+ .every(1)!
194
+ .range(d3.timeWeek.floor(new Date(this.fromDate)), d3.timeWeek.offset(new Date(this.toDate), 1))
195
+ } else if (this.level === 'year-month') {
196
+ ticks = d3.timeMonth
197
+ .every(1)!
198
+ .range(d3.timeMonth.floor(new Date(this.fromDate)), d3.timeMonth.offset(new Date(this.toDate), 1))
199
+ } else if (this.level === 'year-quarter') {
200
+ ticks = d3.timeMonth
201
+ .every(3)!
202
+ .range(d3.timeMonth.floor(new Date(this.fromDate)), d3.timeMonth.offset(new Date(this.toDate), 3))
203
+ }
204
+
205
+ addVerticalGridLines(ticks!)
206
+ addHorizontalGridLines()
207
+
208
+ // Render the resource allocations
209
+ this.renderResourceAllocations(body, ticks!)
210
+ }
211
+ }
212
+
213
+ renderResourceAllocations(body: d3.Selection<SVGSVGElement, unknown, null, undefined>, ticks: Date[]) {
214
+ const resourceAllocations = this.calculateResourceAllocations(ticks)
215
+
216
+ this.resources.forEach((resource, index) => {
217
+ const y = index * 30 + 15 // Center the text vertically in the row
218
+ ticks.forEach(tick => {
219
+ const tickEnd = this.getTickEnd(tick)
220
+ const dateKey = this.formatDateKey(tick, tickEnd)
221
+ const allocation = resourceAllocations[resource.type][dateKey]
222
+ if (allocation > 0) {
223
+ const startX = this.timeScale(tick)
224
+ const tickInterval = this.timeScale(tickEnd) - startX
225
+
226
+ body
227
+ .append('text')
228
+ .attr('x', startX + tickInterval / 2)
229
+ .attr('y', y)
230
+ .attr('text-anchor', 'middle')
231
+ .attr('alignment-baseline', 'middle')
232
+ .attr('dominant-baseline', 'middle')
233
+ .text(allocation)
234
+ .style('font-size', '12px')
235
+ }
236
+ })
237
+ })
238
+ }
239
+
240
+ calculateResourceAllocations(ticks: Date[]) {
241
+ const resourceAllocations: { [key: string]: { [key: string]: number } } = {}
242
+
243
+ // Initialize the resourceAllocations object
244
+ this.resources.forEach(resource => {
245
+ resourceAllocations[resource.type] = {}
246
+ ticks.forEach(tick => {
247
+ const tickEnd = this.getTickEnd(tick)
248
+ const dateKey = this.formatDateKey(tick, tickEnd)
249
+ resourceAllocations[resource.type][dateKey] = 0
250
+ })
251
+ })
252
+
253
+ // Function to process tasks recursively
254
+ const processTask = (task: Task) => {
255
+ const startDate = new Date(task.startDate)
256
+ const endDate = new Date(task.endDate)
257
+ let currentDate = startDate
258
+
259
+ while (currentDate <= endDate) {
260
+ const tick = ticks.find(t => currentDate >= t && currentDate < this.getTickEnd(t))
261
+ if (tick) {
262
+ const tickEnd = this.getTickEnd(tick)
263
+ const dateKey = this.formatDateKey(tick, tickEnd)
264
+
265
+ ;(task.resources || []).forEach(resource => {
266
+ if (resourceAllocations[resource.type][dateKey] !== undefined) {
267
+ resourceAllocations[resource.type][dateKey] += resource.allocated
268
+ }
269
+ })
270
+ }
271
+
272
+ currentDate.setDate(currentDate.getDate() + 1)
273
+ }
274
+
275
+ // Recursively process child tasks
276
+ if (task.children) {
277
+ task.children.forEach(childTask => processTask(childTask))
278
+ }
279
+ }
280
+
281
+ // Process all tasks
282
+ this.tasks.forEach(task => processTask(task))
283
+
284
+ return resourceAllocations
285
+ }
286
+
287
+ getTickEnd(tick: Date) {
288
+ if (this.level === 'week-day') {
289
+ return d3.timeDay.offset(tick, 1)
290
+ } else if (this.level === 'quarter-week') {
291
+ return d3.timeWeek.offset(tick, 1)
292
+ } else if (this.level === 'year-month') {
293
+ return d3.timeMonth.offset(tick, 1)
294
+ } else if (this.level === 'year-quarter') {
295
+ return d3.timeMonth.offset(tick, 3)
296
+ }
297
+ return d3.timeDay.offset(tick, 1)
298
+ }
299
+
300
+ formatDateKey(start: Date, end: Date) {
301
+ return `${start.toISOString().split('T')[0]}_${end.toISOString().split('T')[0]}`
302
+ }
303
+
304
+ syncTimeScale(detail: { baseWidth: number; level: TimeScale }) {
305
+ this.baseWidth = detail.baseWidth
306
+ this.level = detail.level
307
+
308
+ this.updateTimeScale()
309
+ this.drawAxes()
310
+ this.drawBody()
311
+ }
312
+
313
+ setVerticalOffset(offset: number) {
314
+ this.container.scrollTop = offset
315
+ }
316
+
317
+ getVerticalOffset(): number {
318
+ return this.container.scrollTop
319
+ }
320
+
321
+ setHorizontalOffset(offset: number) {
322
+ this.container.scrollLeft = offset
323
+ }
324
+
325
+ getHorizontalOffset(): number {
326
+ return this.container.scrollLeft
327
+ }
328
+ }
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
+ }
@@ -78,8 +78,8 @@ const Template: Story<ArgTypes> = ({
78
78
  }
79
79
 
80
80
  ox-gantt {
81
- width: 90%;
82
- height: 500px;
81
+ width: 100%;
82
+ height: 800px;
83
83
  box-sizing: border-box;
84
84
 
85
85
  background-color: var(--md-sys-color-primary-container);
@@ -131,6 +131,7 @@ Regular.args = {
131
131
  dependsOn: '',
132
132
  length: '5d',
133
133
  tags: ['active'],
134
+ resources: [{ type: '철근/형틀공', allocated: 6 }],
134
135
  children: []
135
136
  },
136
137
  {
@@ -144,6 +145,7 @@ Regular.args = {
144
145
  dependsOn: '1',
145
146
  length: '1d',
146
147
  tags: ['milestone'],
148
+ resources: [{ type: '철근/형틀공', allocated: 6 }],
147
149
  children: []
148
150
  },
149
151
  {
@@ -169,7 +171,35 @@ Regular.args = {
169
171
  dependsOn: '',
170
172
  length: '4d',
171
173
  tags: ['done'],
172
- children: []
174
+ children: [
175
+ {
176
+ id: '3-1-1',
177
+ title: 'Coding-A',
178
+ type: 'task',
179
+ // section: developmentSection,
180
+ startDate: '2023-01-07',
181
+ endDate: '2023-01-10',
182
+ progress: 60,
183
+ dependsOn: '',
184
+ length: '4d',
185
+ tags: ['active'],
186
+ resources: [{ type: '창호공', allocated: 5 }],
187
+ children: []
188
+ },
189
+ {
190
+ id: '3-1-2',
191
+ title: 'Coding-B',
192
+ type: 'task',
193
+ // section: developmentSection,
194
+ startDate: '2023-01-11',
195
+ endDate: '2023-01-15',
196
+ dependsOn: '3-1-1',
197
+ length: '5d',
198
+ tags: ['active'],
199
+ resources: [{ type: '창호공', allocated: 3 }],
200
+ children: []
201
+ }
202
+ ]
173
203
  },
174
204
  {
175
205
  id: '3-2',
@@ -181,6 +211,7 @@ Regular.args = {
181
211
  dependsOn: '3-1',
182
212
  length: '5d',
183
213
  tags: ['active'],
214
+ resources: [{ type: '창호공', allocated: 6 }],
184
215
  children: []
185
216
  }
186
217
  ]
@@ -196,6 +227,7 @@ Regular.args = {
196
227
  dependsOn: '1',
197
228
  length: '1d',
198
229
  tags: ['done'],
230
+ resources: [{ type: '방수공', allocated: 5 }],
199
231
  children: []
200
232
  },
201
233
  {
@@ -208,6 +240,7 @@ Regular.args = {
208
240
  dependsOn: '4',
209
241
  length: '1d',
210
242
  tags: ['milestone'],
243
+ resources: [{ type: '목공', allocated: 3 }],
211
244
  children: []
212
245
  },
213
246
  {
@@ -220,6 +253,7 @@ Regular.args = {
220
253
  dependsOn: '5',
221
254
  length: '1d',
222
255
  tags: ['milestone'],
256
+ resources: [{ type: '금속공', allocated: 1 }],
223
257
  children: []
224
258
  },
225
259
  {
@@ -232,6 +266,7 @@ Regular.args = {
232
266
  dependsOn: '6',
233
267
  length: '1d',
234
268
  tags: ['milestone'],
269
+ resources: [{ type: '타일공', allocated: 6 }],
235
270
  children: []
236
271
  },
237
272
  {
@@ -244,6 +279,7 @@ Regular.args = {
244
279
  dependsOn: '7',
245
280
  length: '1d',
246
281
  tags: ['critical'],
282
+ resources: [{ type: '창호공', allocated: 7 }],
247
283
  children: []
248
284
  },
249
285
  {
@@ -256,6 +292,7 @@ Regular.args = {
256
292
  dependsOn: '8',
257
293
  length: '1d',
258
294
  tags: ['active'],
295
+ resources: [{ type: '도배공', allocated: 8 }],
259
296
  children: []
260
297
  },
261
298
  {
@@ -268,6 +305,7 @@ Regular.args = {
268
305
  dependsOn: '9',
269
306
  length: '1d',
270
307
  tags: ['critical'],
308
+ resources: [{ type: '조적공', allocated: 6 }],
271
309
  children: []
272
310
  },
273
311
  {
@@ -280,6 +318,7 @@ Regular.args = {
280
318
  dependsOn: '9',
281
319
  length: '1d',
282
320
  tags: ['milestone'],
321
+ resources: [{ type: '철근/형틀공', allocated: 6 }],
283
322
  children: []
284
323
  },
285
324
  {
@@ -291,8 +330,37 @@ Regular.args = {
291
330
  endDate: '2023-01-23',
292
331
  dependsOn: '9',
293
332
  length: '1d',
333
+ resources: [{ type: '방수공', allocated: 6 }],
294
334
  tags: ['milestone'],
295
- children: []
335
+ children: [
336
+ {
337
+ id: '12-1',
338
+ title: 'Coding-A',
339
+ type: 'task',
340
+ // section: developmentSection,
341
+ startDate: '2023-01-07',
342
+ endDate: '2023-01-10',
343
+ progress: 60,
344
+ dependsOn: '',
345
+ length: '4d',
346
+ tags: ['done'],
347
+ resources: [{ type: '방수공', allocated: 6 }],
348
+ children: []
349
+ },
350
+ {
351
+ id: '12-2',
352
+ title: 'Coding-B',
353
+ type: 'task',
354
+ // section: developmentSection,
355
+ startDate: '2023-01-11',
356
+ endDate: '2023-01-15',
357
+ dependsOn: '12-1',
358
+ length: '5d',
359
+ tags: ['active'],
360
+ resources: [{ type: '조적공', allocated: 6 }],
361
+ children: []
362
+ }
363
+ ]
296
364
  },
297
365
  {
298
366
  id: '13',
@@ -316,6 +384,7 @@ Regular.args = {
316
384
  dependsOn: '13',
317
385
  length: '1d',
318
386
  tags: ['milestone'],
387
+ resources: [{ type: '금속공', allocated: 1 }],
319
388
  children: []
320
389
  },
321
390
  {
@@ -339,6 +408,7 @@ Regular.args = {
339
408
  endDate: '2023-02-05',
340
409
  dependsOn: '15',
341
410
  length: '1d',
411
+ resources: [{ type: '창호공', allocated: 1 }],
342
412
  tags: ['active'],
343
413
  children: []
344
414
  },
@@ -376,6 +446,7 @@ Regular.args = {
376
446
  dependsOn: '18',
377
447
  length: '1d',
378
448
  tags: ['milestone'],
449
+ resources: [{ type: '창호공', allocated: 7 }],
379
450
  children: []
380
451
  }
381
452
  ]
@@ -7,5 +7,8 @@
7
7
  "label.gantt-task-end-date": "end date",
8
8
  "label.gantt-task-tags": "tags",
9
9
  "label.gantt-task-progress": "progress",
10
- "label.gantt-task-previous": "previous"
10
+ "label.gantt-task-previous": "previous",
11
+ "label.gantt-task-resources": "resources",
12
+ "label.gantt-resource-type": "type",
13
+ "label.gantt-resource-allocated": "allocated"
11
14
  }
@@ -7,5 +7,7 @@
7
7
  "label.gantt-task-end-date": "終了日",
8
8
  "label.gantt-task-tags": "タグ",
9
9
  "label.gantt-task-progress": "進捗",
10
- "label.gantt-task-previous": "前"
10
+ "label.gantt-task-previous": "前",
11
+ "label.gantt-resource-type": "タイプ",
12
+ "label.gantt-resource-allocated": "割り当て"
11
13
  }
@@ -7,5 +7,8 @@
7
7
  "label.gantt-task-end-date": "종료 날짜",
8
8
  "label.gantt-task-tags": "태그",
9
9
  "label.gantt-task-progress": "진행 상황",
10
- "label.gantt-task-previous": "이전"
10
+ "label.gantt-task-previous": "이전",
11
+ "label.gantt-task-resources": "자원",
12
+ "label.gantt-resource-type": "유형",
13
+ "label.gantt-resource-allocated": "할당"
11
14
  }
@@ -7,5 +7,7 @@
7
7
  "label.gantt-task-end-date": "tarikh tamat",
8
8
  "label.gantt-task-tags": "tag",
9
9
  "label.gantt-task-progress": "kemajuan",
10
- "label.gantt-task-previous": "sebelumnya"
10
+ "label.gantt-task-previous": "sebelumnya",
11
+ "label.gantt-resource-type": "jenis",
12
+ "label.gantt-resource-allocated": "diperuntukkan"
11
13
  }
@@ -7,5 +7,7 @@
7
7
  "label.gantt-task-end-date": "结束日期",
8
8
  "label.gantt-task-tags": "标签",
9
9
  "label.gantt-task-progress": "进度",
10
- "label.gantt-task-previous": "以前"
10
+ "label.gantt-task-previous": "以前",
11
+ "label.gantt-resource-type": "类型",
12
+ "label.gantt-resource-allocated": "分配"
11
13
  }