@operato/gantt 2.0.0-beta.13 → 2.0.0-beta.15

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 (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/src/gantt-chart-controller.d.ts +32 -0
  3. package/dist/src/gantt-chart-controller.js +393 -0
  4. package/dist/src/gantt-chart-controller.js.map +1 -0
  5. package/dist/src/locales.d.ts +52 -0
  6. package/dist/src/locales.js +79 -0
  7. package/dist/src/locales.js.map +1 -0
  8. package/dist/src/ox-gantt-chart.d.ts +10 -17
  9. package/dist/src/ox-gantt-chart.js +135 -435
  10. package/dist/src/ox-gantt-chart.js.map +1 -1
  11. package/dist/src/ox-gantt-resize-splitter.d.ts +2 -3
  12. package/dist/src/ox-gantt-resize-splitter.js +15 -22
  13. package/dist/src/ox-gantt-resize-splitter.js.map +1 -1
  14. package/dist/src/ox-gantt.d.ts +14 -1
  15. package/dist/src/ox-gantt.js +149 -28
  16. package/dist/src/ox-gantt.js.map +1 -1
  17. package/dist/src/ox-task-tooltip.d.ts +12 -0
  18. package/dist/src/ox-task-tooltip.js +132 -0
  19. package/dist/src/ox-task-tooltip.js.map +1 -0
  20. package/dist/src/types.d.ts +10 -3
  21. package/dist/src/types.js.map +1 -1
  22. package/dist/stories/ox-gantt.stories.d.ts +12 -0
  23. package/dist/stories/ox-gantt.stories.js +100 -88
  24. package/dist/stories/ox-gantt.stories.js.map +1 -1
  25. package/dist/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +3 -2
  27. package/src/gantt-chart-controller.ts +470 -0
  28. package/src/locales.ts +78 -0
  29. package/src/ox-gantt-chart.ts +143 -481
  30. package/src/ox-gantt-resize-splitter.ts +18 -25
  31. package/src/ox-gantt.ts +153 -30
  32. package/src/ox-task-tooltip.ts +120 -0
  33. package/src/types.ts +11 -3
  34. package/stories/ox-gantt.stories.ts +105 -88
  35. package/translations/en.json +11 -1
  36. package/translations/ja.json +11 -1
  37. package/translations/ko.json +11 -1
  38. package/translations/ms.json +11 -1
  39. package/translations/zh.json +11 -1
  40. package/dist/src/locale.d.ts +0 -1
  41. package/dist/src/locale.js +0 -49
  42. package/dist/src/locale.js.map +0 -1
  43. package/src/locale.ts +0 -51
@@ -1,14 +1,16 @@
1
+ import './ox-task-tooltip'
2
+
1
3
  import { LitElement, html, css, PropertyValues, PropertyValueMap } from 'lit'
2
- import { customElement, property, query, state } from 'lit/decorators.js'
3
- import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
4
+ import { customElement, property, query } from 'lit/decorators.js'
4
5
  import * as d3 from 'd3'
6
+ import { locales } from './locales'
5
7
 
6
- import { LEVEL, TaskTag } from './types'
7
- import { levelDown, levelUp } from './utils'
8
-
8
+ import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
9
9
  import { ScrollbarStyles } from '@operato/styles'
10
-
10
+ import { GanttChartController } from './gantt-chart-controller'
11
11
  import { Task } from './types.js'
12
+ import { levelDown, levelUp } from './utils'
13
+ import { OxTaskTooltip } from './ox-task-tooltip.js'
12
14
 
13
15
  type TimeScale = 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'
14
16
 
@@ -20,23 +22,17 @@ export class OxGanttChart extends LitElement {
20
22
  css`
21
23
  :host {
22
24
  display: flex;
23
-
24
25
  overflow: hidden;
26
+ position: relative;
25
27
  }
26
-
27
28
  div[chart-container] {
28
29
  display: block;
29
-
30
30
  overflow-x: auto;
31
- overflow-y: clip;
32
-
33
- position: relative;
31
+ overflow-y: hidden;
34
32
  }
35
-
36
33
  svg {
37
34
  min-width: 100%;
38
35
  }
39
-
40
36
  svg[header] {
41
37
  display: block;
42
38
  min-width: 100%;
@@ -44,467 +40,152 @@ export class OxGanttChart extends LitElement {
44
40
  border-bottom: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
45
41
  height: 40px;
46
42
  background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
47
-
48
43
  position: sticky;
49
44
  top: 0;
50
45
  z-index: 1;
51
46
  }
52
-
53
47
  .top-axis text,
54
48
  .bottom-axis text {
55
49
  font-size: 12px;
56
50
  }
57
-
58
51
  .grid line {
59
52
  stroke: #e0e0e0;
60
53
  }
61
-
62
54
  .grid path {
63
55
  stroke-width: 0;
64
56
  }
65
-
66
57
  .no-select {
67
58
  user-select: none;
68
59
  }
60
+ .selected {
61
+ stroke-width: 2px;
62
+ stroke: var(--ox-gantt-selected-color, blue);
63
+ }
69
64
  `
70
65
  ]
71
66
 
72
- @property({ type: String, attribute: 'from-date' })
73
- fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0] /* beginning of this year */
74
-
75
- @property({ type: String, attribute: 'to-date' })
76
- toDate: string = new Date(new Date().getFullYear(), 11, 31).toISOString().split('T')[0] /* end of this year */
77
-
67
+ @property({ type: String, attribute: 'from-date' }) fromDate: string = new Date(new Date().getFullYear(), 0, 1)
68
+ .toISOString()
69
+ .split('T')[0]
70
+ @property({ type: String, attribute: 'to-date' }) toDate: string = new Date(new Date().getFullYear(), 11, 31)
71
+ .toISOString()
72
+ .split('T')[0]
78
73
  @property({ type: String, attribute: 'axis-scale' }) axisScale: TimeScale = 'week-day'
79
74
  @property({ type: Boolean, attribute: 'extend-grid-lines' }) extendGridLines: boolean = false
80
75
  @property({ type: Array }) tasks: Task[] = []
81
-
82
- @state() baseWidth: number = 30
83
- @state() level: LEVEL = this.axisScale
84
-
85
- private fromTime: Date = new Date(this.fromDate!)
86
- private toTime: Date = new Date(this.toDate)
87
- private totalDays: number = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
88
- private _timeScale!: d3.ScaleTime<number, number, never>
76
+ @property({ type: String, attribute: 'selected-task-id' }) selectedTaskId?: string
77
+ @property({ type: String }) locale: string = 'en-US'
89
78
 
90
79
  @query('svg[header]') svgHeader!: SVGElement
91
80
  @query('svg[body]') svgBody!: SVGElement
92
81
  @query('[chart-container]') container!: HTMLDivElement
82
+ @query('ox-task-tooltip') tooltip!: OxTaskTooltip // 툴팁 요소 쿼리 추가
83
+
84
+ private ganttChartController: GanttChartController
85
+
86
+ constructor() {
87
+ super()
88
+ this.ganttChartController = new GanttChartController(this, this.fromDate, this.toDate, this.axisScale)
89
+ }
93
90
 
94
91
  protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
95
92
  this.initHeaderGestureHandlers()
96
93
  }
97
94
 
98
95
  updated(changedProperties: PropertyValues<this>) {
99
- var redrawRequired = false
100
-
101
96
  if (changedProperties.has('fromDate') || changedProperties.has('toDate')) {
102
- this.fromTime = new Date(this.fromDate)
103
- this.toTime = new Date(this.toDate)
104
- this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
97
+ this.ganttChartController.updateRange(this.fromDate, this.toDate)
98
+ }
105
99
 
106
- redrawRequired = true
100
+ if (changedProperties.has('tasks')) {
101
+ this.ganttChartController.setTasks(this.tasks)
102
+ this.svgBody.style.height =
103
+ Math.max(this.tasks.length * 30, 2000 /* 태스크가 많이 없어도, 빈 공간이 없도록, 충분히 높게 */) + 'px'
107
104
  }
108
105
 
109
- if (changedProperties.has('axisScale')) {
110
- redrawRequired = true
106
+ // Update locale
107
+ if (changedProperties.has('locale')) {
108
+ this.updateLocale(this.locale)
111
109
  }
112
110
 
113
- if (changedProperties.has('tasks')) {
114
- this.svgBody.style.height = this.tasks.length * 30 + 'px'
115
- redrawRequired = true
111
+ if (changedProperties.has('selectedTaskId')) {
112
+ this.ganttChartController.selectedTaskId = this.selectedTaskId
113
+ }
114
+
115
+ if (changedProperties.has('extendGridLines')) {
116
+ this.ganttChartController.extendGridLines = this.extendGridLines
116
117
  }
117
118
 
118
- redrawRequired && this.redraw()
119
+ this.ganttChartController.drawAxes(this.svgHeader)
120
+ this.ganttChartController.drawBody(this.svgBody, this.tooltip)
119
121
  }
120
122
 
121
123
  render() {
122
124
  return html`
123
- <div chart-container>
125
+ <div
126
+ chart-container
127
+ @wheel=${(e: WheelEvent) => this.ganttChartController.handleWheelForBody(e, this.container)}
128
+ @touchstart=${(e: TouchEvent) => this.ganttChartController.handleTouchStartForBody(e, this.container)}
129
+ @touchmove=${(e: TouchEvent) => this.ganttChartController.handleTouchMoveForBody(e, this.container)}
130
+ >
124
131
  <svg
125
132
  header
126
- @date-range-selected=${(e: CustomEvent) => {
127
- console.log(e.detail)
128
- }}
133
+ @date-range-selected=${(e: CustomEvent) => console.log(e.detail)}
129
134
  @wheel=${this.handleWheelForHeader.bind(this)}
130
135
  ></svg>
131
136
  <svg body></svg>
137
+ <ox-task-tooltip></ox-task-tooltip> // 툴팁 요소 추가
132
138
  </div>
133
139
  `
134
140
  }
135
141
 
136
142
  setVerticalOffset(offset: number) {
137
- console.log(offset)
138
143
  this.container.scrollTop = offset
139
144
  }
140
145
 
141
- getScale() {
142
- if (this.level == 'quarter-week') {
143
- return 7 / 2
144
- } else if (this.level == 'year-month') {
145
- return 30 / 2
146
- } else if (this.level == 'year-quarter') {
147
- return 90 / 2
148
- }
149
- return 1
146
+ getVerticalOffset(): number {
147
+ return this.container.scrollTop
150
148
  }
151
149
 
152
- getChartWidth() {
153
- return (this.totalDays * this.baseWidth) / this.getScale() // 총 차트 폭 계산
154
- }
155
-
156
- get timeScale() {
157
- if (!this._timeScale) {
158
- // 시간축 스케일 생성
159
- this._timeScale = d3
160
- .scaleTime()
161
- .domain([d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1)])
162
- .range([0, this.getChartWidth()])
163
- }
164
-
165
- return this._timeScale
166
- }
167
-
168
- updateTimeScale() {
169
- this.timeScale.range([0, this.getChartWidth()])
170
- }
171
-
172
- redraw() {
173
- this.drawAxes()
174
- this.drawBody()
175
- }
176
-
177
- drawBody() {
178
- const body = d3.select(this.svgBody)
179
-
180
- body.selectAll('*').remove()
181
- body.attr('width', this.getChartWidth())
182
-
183
- if (this.extendGridLines) {
184
- const addGridLines = (ticks: Date[]) => {
185
- ticks.forEach(tick => {
186
- const startX = this.timeScale(tick)
187
- body
188
- .append('line')
189
- .attr('x1', startX)
190
- .attr('x2', startX)
191
- .attr('y1', 0)
192
- .attr('y2', Number(this.svgBody.getBoundingClientRect().height) || 0)
193
- .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
194
- .attr('stroke-width', 1)
195
- })
196
- }
197
-
198
- if (this.level === 'week-day') {
199
- const dayTicks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
200
- addGridLines(dayTicks)
201
- } else if (this.level === 'quarter-week') {
202
- const weekTicks = d3.timeWeek
203
- .every(1)!
204
- .range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
205
- addGridLines(weekTicks)
206
- } else if (this.level === 'year-month') {
207
- const monthTicks = d3.timeMonth
208
- .every(1)!
209
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
210
- addGridLines(monthTicks)
211
- } else if (this.level === 'year-quarter') {
212
- const quarterTicks = d3.timeMonth
213
- .every(3)!
214
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
215
- addGridLines(quarterTicks)
216
- }
217
- }
150
+ handleWheelForHeader(event: WheelEvent) {
151
+ event.preventDefault()
152
+ event.stopPropagation()
218
153
 
219
- const taskPositions: { [key: string]: { x: number; y: number; width: number } } = {}
220
-
221
- // Add task bars
222
- this.tasks.forEach((task, index) => {
223
- const startX = this.timeScale(d3.timeDay.floor(d3.min([task.startTime, task.endTime])!))
224
- const endX = this.timeScale(d3.timeDay.offset(d3.timeDay.ceil(d3.max([task.startTime, task.endTime])!), -1))
225
-
226
- const taskWidth = endX - startX + this.baseWidth / this.getScale()
227
- const taskY = index * 30 + 7.5
228
-
229
- // Main task bar
230
- const taskBar = body
231
- .append('rect')
232
- .attr('x', startX)
233
- .attr('y', taskY)
234
- .attr('width', taskWidth)
235
- .attr('height', 15)
236
- .attr('fill', this.getTaskColor(task.tags || []))
237
- .attr('stroke', 'black')
238
- .attr('stroke-width', 1)
239
-
240
- // Add click event listener to task bar
241
- taskBar.on('click', () => {
242
- const customEvent = new CustomEvent('task-clicked', {
243
- detail: { task },
244
- bubbles: true,
245
- composed: true
246
- })
247
- this.dispatchEvent(customEvent)
154
+ if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {
155
+ this.container.scrollBy({
156
+ left: event.shiftKey ? event.deltaX : event.deltaY
248
157
  })
158
+ } else {
159
+ const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9
160
+ this.ganttChartController.baseWidth *= zoomFactor
249
161
 
250
- // Progress bar
251
- if (task.progress !== undefined) {
252
- const progressWidth = taskWidth * (task.progress / 100)
253
- body
254
- .append('rect')
255
- .attr('x', startX)
256
- .attr('y', taskY + 7.5 - 3.75) // Adjust to center within the task bar
257
- .attr('width', progressWidth)
258
- .attr('height', 7.5) // Set height to 50% of the task bar height
259
- .attr('fill', 'lightgreen')
162
+ if (this.ganttChartController.baseWidth > 40) {
163
+ if (this.ganttChartController.level == 'week-day') {
164
+ this.ganttChartController.baseWidth = 40
165
+ } else {
166
+ this.ganttChartController.level = levelDown(this.ganttChartController.level)
167
+ this.ganttChartController.baseWidth = 30
168
+ }
169
+ } else if (this.ganttChartController.baseWidth < 30) {
170
+ if (this.ganttChartController.level == 'year-quarter') {
171
+ this.ganttChartController.baseWidth = 30
172
+ }
260
173
  }
261
-
262
- // Save task position for linking lines
263
- taskPositions[task.id] = { x: endX + this.baseWidth / this.getScale(), y: taskY + 7.5, width: taskWidth }
264
- })
265
-
266
- // Add linking lines
267
- this.tasks.forEach(task => {
268
- if (task.after && taskPositions[task.after]) {
269
- const prevTask = taskPositions[task.after]
270
- const currTask = taskPositions[task.id]
271
-
272
- // Draw the linking line
273
- body
274
- .append('path')
275
- .attr(
276
- 'd',
277
- `
278
- M${prevTask.x},${prevTask.y}
279
- L${prevTask.x + 10},${prevTask.y}
280
- ${
281
- prevTask.x + 10 < currTask.x - 10 - currTask.width
282
- ? `
283
- L${(prevTask.x + currTask.x - currTask.width) / 2},${prevTask.y}
284
- L${(prevTask.x + currTask.x - currTask.width) / 2},${currTask.y}
285
- `
286
- : `
287
- L${prevTask.x + 10},${(prevTask.y + currTask.y) / 2}
288
- L${currTask.x - 10 - currTask.width},${(prevTask.y + currTask.y) / 2}
289
- `
290
- }
291
- L${currTask.x - 10 - currTask.width},${currTask.y}
292
- L${currTask.x - currTask.width},${currTask.y}
293
- `
294
- )
295
- .attr('stroke', 'black')
296
- .attr('fill', 'none')
297
- .attr('stroke-width', 1)
174
+ if (this.ganttChartController.baseWidth < 20) {
175
+ this.ganttChartController.level = levelUp(this.ganttChartController.level)
176
+ this.ganttChartController.baseWidth = 30
298
177
  }
299
- })
300
- }
301
178
 
302
- // Helper function to determine task color based on tags
303
- getTaskColor(tags: TaskTag[]): string {
304
- if (tags.includes('critical')) {
305
- return 'red'
306
- } else if (tags.includes('milestone')) {
307
- return 'blue'
308
- } else if (tags.includes('done')) {
309
- return 'green'
310
- } else {
311
- return 'grey'
179
+ this.ganttChartController.updateTimeScale()
180
+ this.requestUpdate()
312
181
  }
313
182
  }
314
183
 
315
- drawAxes() {
316
- const header = d3.select(this.svgHeader)
317
-
318
- header.selectAll('*').remove()
319
- header.attr('width', this.getChartWidth())
320
-
321
- // 차트 컨테이너의 폭 설정 및 스크롤 가능하도록 설정
322
- header.attr('width', this.getChartWidth())
323
-
324
- const addGridLines = (ticks: Date[], yStart: number, yEnd: number, extendable?: boolean) => {
325
- ticks.forEach(tick => {
326
- const startX = this.timeScale(tick)
327
- header
328
- .append('line')
329
- .attr('x1', startX)
330
- .attr('x2', startX)
331
- .attr('y1', yStart)
332
- .attr('y2', yEnd)
333
- .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
334
- .attr('stroke-width', 1)
335
- })
336
- }
337
-
338
- const centerLabel = (
339
- selection: d3.Selection<SVGGElement, unknown, null, any>,
340
- tickInterval: d3.CountableTimeInterval
341
- ) => {
342
- selection.selectAll('text').attr('transform', (d: any) => {
343
- const tickIntervalWidth = this.timeScale(tickInterval.offset(d, 1)) - this.timeScale(d)
344
- return `translate(${tickIntervalWidth / 2}, -2)`
345
- })
346
- }
347
-
348
- if (this.level === 'week-day') {
349
- // Level 1: week-day
350
- // Weekly axis
351
- const weekAxis = d3
352
- .axisTop(this.timeScale)
353
- .ticks(d3.timeWeek.every(1))
354
- .tickFormat(d3.timeFormat('%B %d') as any)
355
- .tickSize(0)
356
- const weekTicks = d3.timeWeek
357
- .every(1)!
358
- .range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
359
- addGridLines(weekTicks, 0, 20)
360
-
361
- const weekAxisG = header
362
- .append('g')
363
- .classed('week-axis no-select md-typescale-label-small', true)
364
- .attr('transform', `translate(0, 20)`)
365
- .call(weekAxis)
366
-
367
- centerLabel(weekAxisG, d3.timeWeek)
368
-
369
- // Daily axis
370
- const dayAxis = d3
371
- .axisTop(this.timeScale)
372
- .ticks(d3.timeDay.every(1))
373
- .tickFormat(d3.timeFormat('%d') as any)
374
- .tickSize(0)
375
-
376
- const dayTicks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
377
- addGridLines(dayTicks, 20, 40)
378
-
379
- const dayAxisG = header
380
- .append('g')
381
- .classed('day-axis no-select md-typescale-label-small', true)
382
- .attr('transform', `translate(0, 40)`)
383
- .call(dayAxis)
384
-
385
- centerLabel(dayAxisG, d3.timeDay)
386
- } else if (this.level === 'quarter-week') {
387
- // Level 2: quarter-week
388
- // Quarterly axis
389
- const quarterAxis = d3
390
- .axisTop(this.timeScale)
391
- .ticks(d3.timeMonth.every(3))
392
- .tickFormat(d3.timeFormat('%Y %b') as any)
393
- .tickSize(0)
394
-
395
- const quarterTicks = d3.timeMonth
396
- .every(3)!
397
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
398
- addGridLines(quarterTicks, 0, 20)
399
-
400
- const quarterAxisG = header
401
- .append('g')
402
- .classed('quarter-axis no-select md-typescale-label-small', true)
403
- .attr('transform', `translate(0, 20)`)
404
- .call(quarterAxis)
405
-
406
- centerLabel(quarterAxisG, d3.timeMonth.every(3) as any)
407
-
408
- // Weekly axis
409
- const weekAxis = d3
410
- .axisTop(this.timeScale)
411
- .ticks(d3.timeWeek.every(1))
412
- .tickFormat(d3.timeFormat('%b %d') as any)
413
- .tickSize(0)
414
- const weekTicks = d3.timeWeek
415
- .every(1)!
416
- .range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
417
- addGridLines(weekTicks, 20, 40)
418
-
419
- const weekAxisG = header
420
- .append('g')
421
- .classed('week-axis no-select md-typescale-label-small', true)
422
- .attr('transform', `translate(0, 40)`)
423
- .call(weekAxis)
424
-
425
- centerLabel(weekAxisG, d3.timeWeek)
426
- } else if (this.level === 'year-month') {
427
- // Level 3: year-month
428
- // Yearly axis
429
- const yearAxis = d3
430
- .axisTop(this.timeScale)
431
- .ticks(d3.timeYear.every(1))
432
- .tickFormat(d3.timeFormat('%Y') as any)
433
- .tickSize(0)
434
-
435
- const yearTicks = d3.timeYear
436
- .every(1)!
437
- .range(d3.timeYear.floor(this.fromTime), d3.timeYear.offset(this.toTime, 1))
438
- addGridLines(yearTicks, 0, 20)
439
-
440
- const yearAxisG = header
441
- .append('g')
442
- .classed('year-axis no-select md-typescale-label-small', true)
443
- .attr('transform', `translate(0, 20)`)
444
- .call(yearAxis)
445
-
446
- centerLabel(yearAxisG, d3.timeYear.every(1)! as any)
447
-
448
- // Monthly axis
449
- const monthAxis = d3
450
- .axisTop(this.timeScale)
451
- .ticks(d3.timeMonth.every(1))
452
- .tickFormat(d3.timeFormat('%b') as any)
453
- .tickSize(0)
454
-
455
- const monthTicks = d3.timeMonth
456
- .every(1)!
457
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
458
- addGridLines(monthTicks, 20, 40)
459
-
460
- const monthAxisG = header
461
- .append('g')
462
- .classed('month-axis no-select md-typescale-label-small', true)
463
- .attr('transform', `translate(0, 40)`)
464
- .call(monthAxis)
465
-
466
- centerLabel(monthAxisG, d3.timeMonth.every(1) as any)
467
- } else if (this.level === 'year-quarter') {
468
- // Level 4: year-quarter
469
- // Yearly axis
470
- const yearAxis = d3
471
- .axisTop(this.timeScale)
472
- .ticks(d3.timeYear.every(1))
473
- .tickFormat(d3.timeFormat('%Y') as any)
474
- .tickSize(0)
475
-
476
- const yearTicks = d3.timeYear
477
- .every(1)!
478
- .range(d3.timeYear.floor(this.fromTime), d3.timeYear.offset(this.toTime, 1))
479
- addGridLines(yearTicks, 0, 20)
480
-
481
- const yearAxisG = header
482
- .append('g')
483
- .classed('year-axis no-select md-typescale-label-small', true)
484
- .attr('transform', `translate(0, 20)`)
485
- .call(yearAxis)
486
-
487
- centerLabel(yearAxisG, d3.timeYear.every(1)! as any)
488
-
489
- // Quarterly axis
490
- const quarterAxis = d3
491
- .axisTop(this.timeScale)
492
- .ticks(d3.timeMonth.every(3))
493
- .tickFormat(d3.timeFormat('%Y %b') as any)
494
- .tickSize(0)
495
-
496
- const quarterTicks = d3.timeMonth
497
- .every(3)!
498
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
499
- addGridLines(quarterTicks, 20, 40)
500
-
501
- const quarterAxisG = header
502
- .append('g')
503
- .classed('quarter-axis no-select md-typescale-label-small', true)
504
- .attr('transform', `translate(0, 40)`)
505
- .call(quarterAxis)
506
-
507
- centerLabel(quarterAxisG, d3.timeMonth.every(3) as any)
184
+ updateLocale(locale: string) {
185
+ const localeDefinition = (locales as any)[locale || 'en-US']
186
+ if (localeDefinition) {
187
+ d3.timeFormatDefaultLocale(localeDefinition)
188
+ this.ganttChartController.updateTimeScale()
508
189
  }
509
190
  }
510
191
 
@@ -516,7 +197,7 @@ export class OxGanttChart extends LitElement {
516
197
  let lastTouchDistance: number | null = null
517
198
 
518
199
  const handleTouchStart = (event: TouchEvent) => {
519
- event.preventDefault() // 기본 터치 이벤트 막기
200
+ event.preventDefault()
520
201
 
521
202
  if (event.touches.length === 2) {
522
203
  lastTouchDistance = getTouchDistance(event.touches)
@@ -529,32 +210,32 @@ export class OxGanttChart extends LitElement {
529
210
  }
530
211
 
531
212
  const handleTouchMove = (event: TouchEvent) => {
532
- event.preventDefault() // 기본 터치 이벤트 막기
213
+ event.preventDefault()
533
214
 
534
215
  if (event.touches.length === 2 && lastTouchDistance !== null) {
535
216
  const currentDistance = getTouchDistance(event.touches)
536
217
  const zoomFactor = currentDistance / lastTouchDistance
537
- this.baseWidth *= zoomFactor
218
+ this.ganttChartController.baseWidth *= zoomFactor
538
219
 
539
- if (this.baseWidth > 60) {
540
- if (this.level == 'year-quarter') {
541
- this.baseWidth = 60
220
+ if (this.ganttChartController.baseWidth > 60) {
221
+ if (this.ganttChartController.level == 'year-quarter') {
222
+ this.ganttChartController.baseWidth = 60
542
223
  } else {
543
- this.level = levelUp(this.level)
544
- this.baseWidth = 30
224
+ this.ganttChartController.level = levelUp(this.ganttChartController.level)
225
+ this.ganttChartController.baseWidth = 30
545
226
  }
546
- } else if (this.baseWidth < 30) {
547
- if (this.level == 'week-day') {
548
- this.baseWidth = 30
227
+ } else if (this.ganttChartController.baseWidth < 30) {
228
+ if (this.ganttChartController.level == 'week-day') {
229
+ this.ganttChartController.baseWidth = 30
549
230
  } else {
550
- this.level = levelDown(this.level)
551
- this.baseWidth = 60
231
+ this.ganttChartController.level = levelDown(this.ganttChartController.level)
232
+ this.ganttChartController.baseWidth = 60
552
233
  }
553
234
  }
554
235
 
555
236
  lastTouchDistance = currentDistance
556
- this.updateTimeScale()
557
- this.redraw()
237
+ this.ganttChartController.updateTimeScale()
238
+ this.requestUpdate()
558
239
  } else if (isDragging && startX !== null) {
559
240
  const touch = event.touches[0]
560
241
  const containerOffsetX = header.node()!.getBoundingClientRect().left
@@ -564,14 +245,13 @@ export class OxGanttChart extends LitElement {
564
245
  }
565
246
 
566
247
  const handleTouchEnd = (event: TouchEvent) => {
567
- event.preventDefault() // 기본 터치 이벤트 막기
248
+ event.preventDefault()
568
249
 
569
250
  if (event.touches.length < 2) {
570
251
  lastTouchDistance = null
571
252
  }
572
253
 
573
254
  if (!isDragging || startX === null) {
574
- // 클릭 이벤트 처리
575
255
  const touch = event.changedTouches[0]
576
256
  const containerOffsetX = header.node()!.getBoundingClientRect().left
577
257
  const clickX = touch.clientX - containerOffsetX
@@ -610,7 +290,6 @@ export class OxGanttChart extends LitElement {
610
290
 
611
291
  const handleMouseUp = (event: MouseEvent) => {
612
292
  if (!isDragging || startX === null) {
613
- // 클릭 이벤트 처리
614
293
  const containerOffsetX = header.node()!.getBoundingClientRect().left
615
294
  const clickX = event.clientX - containerOffsetX
616
295
  this.finalizeSelection(clickX, clickX)
@@ -637,21 +316,21 @@ export class OxGanttChart extends LitElement {
637
316
  const header = d3.select(this.svgHeader)
638
317
  const body = d3.select(this.svgBody)
639
318
 
640
- let startDate = this.timeScale.invert(startX)
641
- let endDate = this.timeScale.invert(endX)
319
+ let startDate = this.ganttChartController.timeScale.invert(startX)
320
+ let endDate = this.ganttChartController.timeScale.invert(endX)
642
321
 
643
322
  let start, end
644
323
 
645
- if (this.level === 'week-day') {
324
+ if (this.ganttChartController.level === 'week-day') {
646
325
  start = d3.timeDay.floor(d3.min([startDate, endDate])!)
647
326
  end = d3.timeDay.offset(d3.timeDay.ceil(d3.max([startDate, endDate])!), -1)
648
- } else if (this.level === 'quarter-week') {
327
+ } else if (this.ganttChartController.level === 'quarter-week') {
649
328
  start = d3.timeWeek.floor(d3.min([startDate, endDate])!)
650
329
  end = d3.timeDay.offset(d3.timeWeek.ceil(d3.max([startDate, endDate])!), -1)
651
- } else if (this.level === 'year-month') {
330
+ } else if (this.ganttChartController.level === 'year-month') {
652
331
  start = d3.timeMonth.floor(d3.min([startDate, endDate])!)
653
332
  end = d3.timeDay.offset(d3.timeMonth.ceil(d3.max([startDate, endDate])!), -1)
654
- } else if (this.level === 'year-quarter') {
333
+ } else if (this.ganttChartController.level === 'year-quarter') {
655
334
  start = d3.timeMonth.every(3)!.floor(d3.min([startDate, endDate])!)
656
335
  end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
657
336
  }
@@ -662,9 +341,14 @@ export class OxGanttChart extends LitElement {
662
341
  header
663
342
  .append('rect')
664
343
  .attr('class', 'selection-rect')
665
- .attr('x', this.timeScale(start!))
344
+ .attr('x', this.ganttChartController.timeScale(start!))
666
345
  .attr('y', 20)
667
- .attr('width', this.timeScale(end!) - this.timeScale(start!) + this.baseWidth / this.getScale())
346
+ .attr(
347
+ 'width',
348
+ this.ganttChartController.timeScale(end!) -
349
+ this.ganttChartController.timeScale(start!) +
350
+ this.ganttChartController.baseWidth / this.ganttChartController.getScale()
351
+ )
668
352
  .attr('height', 20)
669
353
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
670
354
  .attr('opacity', '.2')
@@ -672,9 +356,14 @@ export class OxGanttChart extends LitElement {
672
356
  body
673
357
  .append('rect')
674
358
  .attr('class', 'selection-rect')
675
- .attr('x', this.timeScale(start!))
359
+ .attr('x', this.ganttChartController.timeScale(start!))
676
360
  .attr('y', 0)
677
- .attr('width', this.timeScale(end!) - this.timeScale(start!) + this.baseWidth / this.getScale())
361
+ .attr(
362
+ 'width',
363
+ this.ganttChartController.timeScale(end!) -
364
+ this.ganttChartController.timeScale(start!) +
365
+ this.ganttChartController.baseWidth / this.ganttChartController.getScale()
366
+ )
678
367
  .attr('height', '100%')
679
368
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
680
369
  .attr('opacity', '.2')
@@ -684,21 +373,21 @@ export class OxGanttChart extends LitElement {
684
373
  const header = d3.select(this.svgHeader)
685
374
  const body = d3.select(this.svgBody)
686
375
 
687
- let startDate = this.timeScale.invert(startX)
688
- let endDate = this.timeScale.invert(endX)
376
+ let startDate = this.ganttChartController.timeScale.invert(startX)
377
+ let endDate = this.ganttChartController.timeScale.invert(endX)
689
378
 
690
379
  let start, end
691
380
 
692
- if (this.level === 'week-day') {
381
+ if (this.ganttChartController.level === 'week-day') {
693
382
  start = d3.timeDay.floor(d3.min([startDate, endDate])!)
694
383
  end = d3.timeDay.offset(d3.timeDay.ceil(d3.max([startDate, endDate])!), -1)
695
- } else if (this.level === 'quarter-week') {
384
+ } else if (this.ganttChartController.level === 'quarter-week') {
696
385
  start = d3.timeWeek.floor(d3.min([startDate, endDate])!)
697
386
  end = d3.timeDay.offset(d3.timeWeek.ceil(d3.max([startDate, endDate])!), -1)
698
- } else if (this.level === 'year-month') {
387
+ } else if (this.ganttChartController.level === 'year-month') {
699
388
  start = d3.timeMonth.floor(d3.min([startDate, endDate])!)
700
389
  end = d3.timeDay.offset(d3.timeMonth.ceil(d3.max([startDate, endDate])!), -1)
701
- } else if (this.level === 'year-quarter') {
390
+ } else if (this.ganttChartController.level === 'year-quarter') {
702
391
  start = d3.timeMonth.every(3)!.floor(d3.min([startDate, endDate])!)
703
392
  end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
704
393
  }
@@ -709,9 +398,14 @@ export class OxGanttChart extends LitElement {
709
398
  header
710
399
  .append('rect')
711
400
  .attr('class', 'selection-rect')
712
- .attr('x', this.timeScale(start!))
401
+ .attr('x', this.ganttChartController.timeScale(start!))
713
402
  .attr('y', 20)
714
- .attr('width', this.timeScale(end!) - this.timeScale(start!) + this.baseWidth / this.getScale())
403
+ .attr(
404
+ 'width',
405
+ this.ganttChartController.timeScale(end!) -
406
+ this.ganttChartController.timeScale(start!) +
407
+ this.ganttChartController.baseWidth / this.ganttChartController.getScale()
408
+ )
715
409
  .attr('height', 20)
716
410
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
717
411
  .attr('opacity', '.2')
@@ -719,9 +413,14 @@ export class OxGanttChart extends LitElement {
719
413
  body
720
414
  .append('rect')
721
415
  .attr('class', 'selection-rect')
722
- .attr('x', this.timeScale(start!))
416
+ .attr('x', this.ganttChartController.timeScale(start!))
723
417
  .attr('y', 0)
724
- .attr('width', this.timeScale(end!) - this.timeScale(start!) + this.baseWidth / this.getScale())
418
+ .attr(
419
+ 'width',
420
+ this.ganttChartController.timeScale(end!) -
421
+ this.ganttChartController.timeScale(start!) +
422
+ this.ganttChartController.baseWidth / this.ganttChartController.getScale()
423
+ )
725
424
  .attr('height', '100%')
726
425
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
727
426
  .attr('opacity', '.2')
@@ -733,41 +432,4 @@ export class OxGanttChart extends LitElement {
733
432
 
734
433
  header.node()?.dispatchEvent(customEvent)
735
434
  }
736
-
737
- handleWheelForHeader(event: WheelEvent) {
738
- event.preventDefault()
739
- event.stopPropagation()
740
-
741
- if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {
742
- // 좌우 스크롤
743
- this.container.scrollBy({
744
- left: event.shiftKey ? event.deltaX : event.deltaY,
745
- behavior: 'smooth'
746
- })
747
- } else {
748
- // Zoom in/out
749
- const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9
750
- this.baseWidth *= zoomFactor
751
-
752
- if (this.baseWidth > 40) {
753
- if (this.level == 'week-day') {
754
- this.baseWidth = 40
755
- } else {
756
- this.level = levelDown(this.level)
757
- this.baseWidth = 30
758
- }
759
- } else if (this.baseWidth < 30) {
760
- if (this.level == 'year-quarter') {
761
- this.baseWidth = 30
762
- }
763
- }
764
- if (this.baseWidth < 20) {
765
- this.level = levelUp(this.level)
766
- this.baseWidth = 30
767
- }
768
-
769
- this.updateTimeScale()
770
- this.redraw()
771
- }
772
- }
773
435
  }