@operato/gantt 2.0.0-beta.14 → 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 (42) hide show
  1. package/CHANGELOG.md +19 -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 +134 -440
  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 +13 -1
  15. package/dist/src/ox-gantt.js +144 -27
  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.js +91 -85
  23. package/dist/stories/ox-gantt.stories.js.map +1 -1
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +3 -2
  26. package/src/gantt-chart-controller.ts +470 -0
  27. package/src/locales.ts +78 -0
  28. package/src/ox-gantt-chart.ts +141 -487
  29. package/src/ox-gantt-resize-splitter.ts +18 -25
  30. package/src/ox-gantt.ts +151 -29
  31. package/src/ox-task-tooltip.ts +120 -0
  32. package/src/types.ts +11 -3
  33. package/stories/ox-gantt.stories.ts +93 -85
  34. package/translations/en.json +11 -1
  35. package/translations/ja.json +11 -1
  36. package/translations/ko.json +11 -1
  37. package/translations/ms.json +11 -1
  38. package/translations/zh.json +11 -1
  39. package/dist/src/locale.d.ts +0 -1
  40. package/dist/src/locale.js +0 -49
  41. package/dist/src/locale.js.map +0 -1
  42. 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,475 +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))
105
-
106
- this._timeScale = undefined
107
- redrawRequired = true
97
+ this.ganttChartController.updateRange(this.fromDate, this.toDate)
108
98
  }
109
99
 
110
- if (changedProperties.has('axisScale')) {
111
- 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'
112
104
  }
113
105
 
114
- if (changedProperties.has('extendGridLines')) {
115
- redrawRequired = true
106
+ // Update locale
107
+ if (changedProperties.has('locale')) {
108
+ this.updateLocale(this.locale)
116
109
  }
117
110
 
118
- if (changedProperties.has('tasks')) {
119
- const chartContainerHeight = this.container.getBoundingClientRect().height
120
- const headerHeight = this.svgHeader.getBoundingClientRect().height
111
+ if (changedProperties.has('selectedTaskId')) {
112
+ this.ganttChartController.selectedTaskId = this.selectedTaskId
113
+ }
121
114
 
122
- this.svgBody.style.height = Math.max(this.tasks.length * 30, chartContainerHeight - headerHeight) + 'px'
123
- redrawRequired = true
115
+ if (changedProperties.has('extendGridLines')) {
116
+ this.ganttChartController.extendGridLines = this.extendGridLines
124
117
  }
125
118
 
126
- redrawRequired && this.redraw()
119
+ this.ganttChartController.drawAxes(this.svgHeader)
120
+ this.ganttChartController.drawBody(this.svgBody, this.tooltip)
127
121
  }
128
122
 
129
123
  render() {
130
124
  return html`
131
- <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
+ >
132
131
  <svg
133
132
  header
134
- @date-range-selected=${(e: CustomEvent) => {
135
- console.log(e.detail)
136
- }}
133
+ @date-range-selected=${(e: CustomEvent) => console.log(e.detail)}
137
134
  @wheel=${this.handleWheelForHeader.bind(this)}
138
135
  ></svg>
139
136
  <svg body></svg>
137
+ <ox-task-tooltip></ox-task-tooltip> // 툴팁 요소 추가
140
138
  </div>
141
139
  `
142
140
  }
143
141
 
144
142
  setVerticalOffset(offset: number) {
145
- console.log(offset)
146
143
  this.container.scrollTop = offset
147
144
  }
148
145
 
149
- getScale() {
150
- if (this.level == 'quarter-week') {
151
- return 7 / 2
152
- } else if (this.level == 'year-month') {
153
- return 30 / 2
154
- } else if (this.level == 'year-quarter') {
155
- return 90 / 2
156
- }
157
- return 1
158
- }
159
-
160
- getChartWidth() {
161
- return (this.totalDays * this.baseWidth) / this.getScale() // 총 차트 폭 계산
162
- }
163
-
164
- get timeScale() {
165
- if (!this._timeScale) {
166
- // 시간축 스케일 생성
167
- this._timeScale = d3
168
- .scaleTime()
169
- .domain([d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1)])
170
- .range([0, this.getChartWidth()])
171
- }
172
-
173
- return this._timeScale
174
- }
175
-
176
- updateTimeScale() {
177
- this.timeScale.range([0, this.getChartWidth()])
178
- }
179
-
180
- redraw() {
181
- this.drawAxes()
182
- this.drawBody()
146
+ getVerticalOffset(): number {
147
+ return this.container.scrollTop
183
148
  }
184
149
 
185
- drawBody() {
186
- const body = d3.select(this.svgBody)
187
-
188
- body.selectAll('*').remove()
189
- body.attr('width', this.getChartWidth())
190
-
191
- if (this.extendGridLines) {
192
- const addGridLines = (ticks: Date[]) => {
193
- ticks.forEach(tick => {
194
- const startX = this.timeScale(tick)
195
- body
196
- .append('line')
197
- .attr('x1', startX)
198
- .attr('x2', startX)
199
- .attr('y1', 0)
200
- .attr('y2', Number(this.svgBody.getBoundingClientRect().height) || 0)
201
- .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
202
- .attr('stroke-width', 1)
203
- })
204
- }
205
-
206
- if (this.level === 'week-day') {
207
- const dayTicks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
208
- addGridLines(dayTicks)
209
- } else if (this.level === 'quarter-week') {
210
- const weekTicks = d3.timeWeek
211
- .every(1)!
212
- .range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
213
- addGridLines(weekTicks)
214
- } else if (this.level === 'year-month') {
215
- const monthTicks = d3.timeMonth
216
- .every(1)!
217
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
218
- addGridLines(monthTicks)
219
- } else if (this.level === 'year-quarter') {
220
- const quarterTicks = d3.timeMonth
221
- .every(3)!
222
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
223
- addGridLines(quarterTicks)
224
- }
225
- }
150
+ handleWheelForHeader(event: WheelEvent) {
151
+ event.preventDefault()
152
+ event.stopPropagation()
226
153
 
227
- const taskPositions: { [key: string]: { x: number; y: number; width: number } } = {}
228
-
229
- // Add task bars
230
- this.tasks.forEach((task, index) => {
231
- const startX = this.timeScale(d3.timeDay.floor(d3.min([task.startTime, task.endTime])!))
232
- const endX = this.timeScale(d3.timeDay.offset(d3.timeDay.ceil(d3.max([task.startTime, task.endTime])!), -1))
233
-
234
- const taskWidth = endX - startX + this.baseWidth / this.getScale()
235
- const taskY = index * 30 + 7.5
236
-
237
- // Main task bar
238
- const taskBar = body
239
- .append('rect')
240
- .attr('x', startX)
241
- .attr('y', taskY)
242
- .attr('width', taskWidth)
243
- .attr('height', 15)
244
- .attr('fill', this.getTaskColor(task.tags || []))
245
- .attr('stroke', 'black')
246
- .attr('stroke-width', 1)
247
-
248
- // Add click event listener to task bar
249
- taskBar.on('click', () => {
250
- const customEvent = new CustomEvent('task-clicked', {
251
- detail: { task },
252
- bubbles: true,
253
- composed: true
254
- })
255
- 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
256
157
  })
158
+ } else {
159
+ const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9
160
+ this.ganttChartController.baseWidth *= zoomFactor
257
161
 
258
- // Progress bar
259
- if (task.progress !== undefined) {
260
- const progressWidth = taskWidth * (task.progress / 100)
261
- body
262
- .append('rect')
263
- .attr('x', startX)
264
- .attr('y', taskY + 7.5 - 3.75) // Adjust to center within the task bar
265
- .attr('width', progressWidth)
266
- .attr('height', 7.5) // Set height to 50% of the task bar height
267
- .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
+ }
268
173
  }
269
-
270
- // Save task position for linking lines
271
- taskPositions[task.id] = { x: endX + this.baseWidth / this.getScale(), y: taskY + 7.5, width: taskWidth }
272
- })
273
-
274
- // Add linking lines
275
- this.tasks.forEach(task => {
276
- if (task.after && taskPositions[task.after]) {
277
- const prevTask = taskPositions[task.after]
278
- const currTask = taskPositions[task.id]
279
-
280
- // Draw the linking line
281
- body
282
- .append('path')
283
- .attr(
284
- 'd',
285
- `
286
- M${prevTask.x},${prevTask.y}
287
- L${prevTask.x + 10},${prevTask.y}
288
- ${
289
- prevTask.x + 10 < currTask.x - 10 - currTask.width
290
- ? `
291
- L${(prevTask.x + currTask.x - currTask.width) / 2},${prevTask.y}
292
- L${(prevTask.x + currTask.x - currTask.width) / 2},${currTask.y}
293
- `
294
- : `
295
- L${prevTask.x + 10},${(prevTask.y + currTask.y) / 2}
296
- L${currTask.x - 10 - currTask.width},${(prevTask.y + currTask.y) / 2}
297
- `
298
- }
299
- L${currTask.x - 10 - currTask.width},${currTask.y}
300
- L${currTask.x - currTask.width},${currTask.y}
301
- `
302
- )
303
- .attr('stroke', 'black')
304
- .attr('fill', 'none')
305
- .attr('stroke-width', 1)
174
+ if (this.ganttChartController.baseWidth < 20) {
175
+ this.ganttChartController.level = levelUp(this.ganttChartController.level)
176
+ this.ganttChartController.baseWidth = 30
306
177
  }
307
- })
308
- }
309
178
 
310
- // Helper function to determine task color based on tags
311
- getTaskColor(tags: TaskTag[]): string {
312
- if (tags.includes('critical')) {
313
- return 'red'
314
- } else if (tags.includes('milestone')) {
315
- return 'blue'
316
- } else if (tags.includes('done')) {
317
- return 'green'
318
- } else {
319
- return 'grey'
179
+ this.ganttChartController.updateTimeScale()
180
+ this.requestUpdate()
320
181
  }
321
182
  }
322
183
 
323
- drawAxes() {
324
- const header = d3.select(this.svgHeader)
325
-
326
- header.selectAll('*').remove()
327
- header.attr('width', this.getChartWidth())
328
-
329
- // 차트 컨테이너의 폭 설정 및 스크롤 가능하도록 설정
330
- header.attr('width', this.getChartWidth())
331
-
332
- const addGridLines = (ticks: Date[], yStart: number, yEnd: number, extendable?: boolean) => {
333
- ticks.forEach(tick => {
334
- const startX = this.timeScale(tick)
335
- header
336
- .append('line')
337
- .attr('x1', startX)
338
- .attr('x2', startX)
339
- .attr('y1', yStart)
340
- .attr('y2', yEnd)
341
- .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
342
- .attr('stroke-width', 1)
343
- })
344
- }
345
-
346
- const centerLabel = (
347
- selection: d3.Selection<SVGGElement, unknown, null, any>,
348
- tickInterval: d3.CountableTimeInterval
349
- ) => {
350
- selection.selectAll('text').attr('transform', (d: any) => {
351
- const tickIntervalWidth = this.timeScale(tickInterval.offset(d, 1)) - this.timeScale(d)
352
- return `translate(${tickIntervalWidth / 2}, -2)`
353
- })
354
- }
355
-
356
- if (this.level === 'week-day') {
357
- // Level 1: week-day
358
- // Weekly axis
359
- const weekAxis = d3
360
- .axisTop(this.timeScale)
361
- .ticks(d3.timeWeek.every(1))
362
- .tickFormat(d3.timeFormat('%B %d') as any)
363
- .tickSize(0)
364
- const weekTicks = d3.timeWeek
365
- .every(1)!
366
- .range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
367
- addGridLines(weekTicks, 0, 20)
368
-
369
- const weekAxisG = header
370
- .append('g')
371
- .classed('week-axis no-select md-typescale-label-small', true)
372
- .attr('transform', `translate(0, 20)`)
373
- .call(weekAxis)
374
-
375
- centerLabel(weekAxisG, d3.timeWeek)
376
-
377
- // Daily axis
378
- const dayAxis = d3
379
- .axisTop(this.timeScale)
380
- .ticks(d3.timeDay.every(1))
381
- .tickFormat(d3.timeFormat('%d') as any)
382
- .tickSize(0)
383
-
384
- const dayTicks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
385
- addGridLines(dayTicks, 20, 40)
386
-
387
- const dayAxisG = header
388
- .append('g')
389
- .classed('day-axis no-select md-typescale-label-small', true)
390
- .attr('transform', `translate(0, 40)`)
391
- .call(dayAxis)
392
-
393
- centerLabel(dayAxisG, d3.timeDay)
394
- } else if (this.level === 'quarter-week') {
395
- // Level 2: quarter-week
396
- // Quarterly axis
397
- const quarterAxis = d3
398
- .axisTop(this.timeScale)
399
- .ticks(d3.timeMonth.every(3))
400
- .tickFormat(d3.timeFormat('%Y %b') as any)
401
- .tickSize(0)
402
-
403
- const quarterTicks = d3.timeMonth
404
- .every(3)!
405
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
406
- addGridLines(quarterTicks, 0, 20)
407
-
408
- const quarterAxisG = header
409
- .append('g')
410
- .classed('quarter-axis no-select md-typescale-label-small', true)
411
- .attr('transform', `translate(0, 20)`)
412
- .call(quarterAxis)
413
-
414
- centerLabel(quarterAxisG, d3.timeMonth.every(3) as any)
415
-
416
- // Weekly axis
417
- const weekAxis = d3
418
- .axisTop(this.timeScale)
419
- .ticks(d3.timeWeek.every(1))
420
- .tickFormat(d3.timeFormat('%b %d') as any)
421
- .tickSize(0)
422
- const weekTicks = d3.timeWeek
423
- .every(1)!
424
- .range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
425
- addGridLines(weekTicks, 20, 40)
426
-
427
- const weekAxisG = header
428
- .append('g')
429
- .classed('week-axis no-select md-typescale-label-small', true)
430
- .attr('transform', `translate(0, 40)`)
431
- .call(weekAxis)
432
-
433
- centerLabel(weekAxisG, d3.timeWeek)
434
- } else if (this.level === 'year-month') {
435
- // Level 3: year-month
436
- // Yearly axis
437
- const yearAxis = d3
438
- .axisTop(this.timeScale)
439
- .ticks(d3.timeYear.every(1))
440
- .tickFormat(d3.timeFormat('%Y') as any)
441
- .tickSize(0)
442
-
443
- const yearTicks = d3.timeYear
444
- .every(1)!
445
- .range(d3.timeYear.floor(this.fromTime), d3.timeYear.offset(this.toTime, 1))
446
- addGridLines(yearTicks, 0, 20)
447
-
448
- const yearAxisG = header
449
- .append('g')
450
- .classed('year-axis no-select md-typescale-label-small', true)
451
- .attr('transform', `translate(0, 20)`)
452
- .call(yearAxis)
453
-
454
- centerLabel(yearAxisG, d3.timeYear.every(1)! as any)
455
-
456
- // Monthly axis
457
- const monthAxis = d3
458
- .axisTop(this.timeScale)
459
- .ticks(d3.timeMonth.every(1))
460
- .tickFormat(d3.timeFormat('%b') as any)
461
- .tickSize(0)
462
-
463
- const monthTicks = d3.timeMonth
464
- .every(1)!
465
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
466
- addGridLines(monthTicks, 20, 40)
467
-
468
- const monthAxisG = header
469
- .append('g')
470
- .classed('month-axis no-select md-typescale-label-small', true)
471
- .attr('transform', `translate(0, 40)`)
472
- .call(monthAxis)
473
-
474
- centerLabel(monthAxisG, d3.timeMonth.every(1) as any)
475
- } else if (this.level === 'year-quarter') {
476
- // Level 4: year-quarter
477
- // Yearly axis
478
- const yearAxis = d3
479
- .axisTop(this.timeScale)
480
- .ticks(d3.timeYear.every(1))
481
- .tickFormat(d3.timeFormat('%Y') as any)
482
- .tickSize(0)
483
-
484
- const yearTicks = d3.timeYear
485
- .every(1)!
486
- .range(d3.timeYear.floor(this.fromTime), d3.timeYear.offset(this.toTime, 1))
487
- addGridLines(yearTicks, 0, 20)
488
-
489
- const yearAxisG = header
490
- .append('g')
491
- .classed('year-axis no-select md-typescale-label-small', true)
492
- .attr('transform', `translate(0, 20)`)
493
- .call(yearAxis)
494
-
495
- centerLabel(yearAxisG, d3.timeYear.every(1)! as any)
496
-
497
- // Quarterly axis
498
- const quarterAxis = d3
499
- .axisTop(this.timeScale)
500
- .ticks(d3.timeMonth.every(3))
501
- .tickFormat(d3.timeFormat('%Y %b') as any)
502
- .tickSize(0)
503
-
504
- const quarterTicks = d3.timeMonth
505
- .every(3)!
506
- .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
507
- addGridLines(quarterTicks, 20, 40)
508
-
509
- const quarterAxisG = header
510
- .append('g')
511
- .classed('quarter-axis no-select md-typescale-label-small', true)
512
- .attr('transform', `translate(0, 40)`)
513
- .call(quarterAxis)
514
-
515
- 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()
516
189
  }
517
190
  }
518
191
 
@@ -524,7 +197,7 @@ export class OxGanttChart extends LitElement {
524
197
  let lastTouchDistance: number | null = null
525
198
 
526
199
  const handleTouchStart = (event: TouchEvent) => {
527
- event.preventDefault() // 기본 터치 이벤트 막기
200
+ event.preventDefault()
528
201
 
529
202
  if (event.touches.length === 2) {
530
203
  lastTouchDistance = getTouchDistance(event.touches)
@@ -537,32 +210,32 @@ export class OxGanttChart extends LitElement {
537
210
  }
538
211
 
539
212
  const handleTouchMove = (event: TouchEvent) => {
540
- event.preventDefault() // 기본 터치 이벤트 막기
213
+ event.preventDefault()
541
214
 
542
215
  if (event.touches.length === 2 && lastTouchDistance !== null) {
543
216
  const currentDistance = getTouchDistance(event.touches)
544
217
  const zoomFactor = currentDistance / lastTouchDistance
545
- this.baseWidth *= zoomFactor
218
+ this.ganttChartController.baseWidth *= zoomFactor
546
219
 
547
- if (this.baseWidth > 60) {
548
- if (this.level == 'year-quarter') {
549
- this.baseWidth = 60
220
+ if (this.ganttChartController.baseWidth > 60) {
221
+ if (this.ganttChartController.level == 'year-quarter') {
222
+ this.ganttChartController.baseWidth = 60
550
223
  } else {
551
- this.level = levelUp(this.level)
552
- this.baseWidth = 30
224
+ this.ganttChartController.level = levelUp(this.ganttChartController.level)
225
+ this.ganttChartController.baseWidth = 30
553
226
  }
554
- } else if (this.baseWidth < 30) {
555
- if (this.level == 'week-day') {
556
- this.baseWidth = 30
227
+ } else if (this.ganttChartController.baseWidth < 30) {
228
+ if (this.ganttChartController.level == 'week-day') {
229
+ this.ganttChartController.baseWidth = 30
557
230
  } else {
558
- this.level = levelDown(this.level)
559
- this.baseWidth = 60
231
+ this.ganttChartController.level = levelDown(this.ganttChartController.level)
232
+ this.ganttChartController.baseWidth = 60
560
233
  }
561
234
  }
562
235
 
563
236
  lastTouchDistance = currentDistance
564
- this.updateTimeScale()
565
- this.redraw()
237
+ this.ganttChartController.updateTimeScale()
238
+ this.requestUpdate()
566
239
  } else if (isDragging && startX !== null) {
567
240
  const touch = event.touches[0]
568
241
  const containerOffsetX = header.node()!.getBoundingClientRect().left
@@ -572,14 +245,13 @@ export class OxGanttChart extends LitElement {
572
245
  }
573
246
 
574
247
  const handleTouchEnd = (event: TouchEvent) => {
575
- event.preventDefault() // 기본 터치 이벤트 막기
248
+ event.preventDefault()
576
249
 
577
250
  if (event.touches.length < 2) {
578
251
  lastTouchDistance = null
579
252
  }
580
253
 
581
254
  if (!isDragging || startX === null) {
582
- // 클릭 이벤트 처리
583
255
  const touch = event.changedTouches[0]
584
256
  const containerOffsetX = header.node()!.getBoundingClientRect().left
585
257
  const clickX = touch.clientX - containerOffsetX
@@ -618,7 +290,6 @@ export class OxGanttChart extends LitElement {
618
290
 
619
291
  const handleMouseUp = (event: MouseEvent) => {
620
292
  if (!isDragging || startX === null) {
621
- // 클릭 이벤트 처리
622
293
  const containerOffsetX = header.node()!.getBoundingClientRect().left
623
294
  const clickX = event.clientX - containerOffsetX
624
295
  this.finalizeSelection(clickX, clickX)
@@ -645,21 +316,21 @@ export class OxGanttChart extends LitElement {
645
316
  const header = d3.select(this.svgHeader)
646
317
  const body = d3.select(this.svgBody)
647
318
 
648
- let startDate = this.timeScale.invert(startX)
649
- let endDate = this.timeScale.invert(endX)
319
+ let startDate = this.ganttChartController.timeScale.invert(startX)
320
+ let endDate = this.ganttChartController.timeScale.invert(endX)
650
321
 
651
322
  let start, end
652
323
 
653
- if (this.level === 'week-day') {
324
+ if (this.ganttChartController.level === 'week-day') {
654
325
  start = d3.timeDay.floor(d3.min([startDate, endDate])!)
655
326
  end = d3.timeDay.offset(d3.timeDay.ceil(d3.max([startDate, endDate])!), -1)
656
- } else if (this.level === 'quarter-week') {
327
+ } else if (this.ganttChartController.level === 'quarter-week') {
657
328
  start = d3.timeWeek.floor(d3.min([startDate, endDate])!)
658
329
  end = d3.timeDay.offset(d3.timeWeek.ceil(d3.max([startDate, endDate])!), -1)
659
- } else if (this.level === 'year-month') {
330
+ } else if (this.ganttChartController.level === 'year-month') {
660
331
  start = d3.timeMonth.floor(d3.min([startDate, endDate])!)
661
332
  end = d3.timeDay.offset(d3.timeMonth.ceil(d3.max([startDate, endDate])!), -1)
662
- } else if (this.level === 'year-quarter') {
333
+ } else if (this.ganttChartController.level === 'year-quarter') {
663
334
  start = d3.timeMonth.every(3)!.floor(d3.min([startDate, endDate])!)
664
335
  end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
665
336
  }
@@ -670,9 +341,14 @@ export class OxGanttChart extends LitElement {
670
341
  header
671
342
  .append('rect')
672
343
  .attr('class', 'selection-rect')
673
- .attr('x', this.timeScale(start!))
344
+ .attr('x', this.ganttChartController.timeScale(start!))
674
345
  .attr('y', 20)
675
- .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
+ )
676
352
  .attr('height', 20)
677
353
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
678
354
  .attr('opacity', '.2')
@@ -680,9 +356,14 @@ export class OxGanttChart extends LitElement {
680
356
  body
681
357
  .append('rect')
682
358
  .attr('class', 'selection-rect')
683
- .attr('x', this.timeScale(start!))
359
+ .attr('x', this.ganttChartController.timeScale(start!))
684
360
  .attr('y', 0)
685
- .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
+ )
686
367
  .attr('height', '100%')
687
368
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
688
369
  .attr('opacity', '.2')
@@ -692,21 +373,21 @@ export class OxGanttChart extends LitElement {
692
373
  const header = d3.select(this.svgHeader)
693
374
  const body = d3.select(this.svgBody)
694
375
 
695
- let startDate = this.timeScale.invert(startX)
696
- let endDate = this.timeScale.invert(endX)
376
+ let startDate = this.ganttChartController.timeScale.invert(startX)
377
+ let endDate = this.ganttChartController.timeScale.invert(endX)
697
378
 
698
379
  let start, end
699
380
 
700
- if (this.level === 'week-day') {
381
+ if (this.ganttChartController.level === 'week-day') {
701
382
  start = d3.timeDay.floor(d3.min([startDate, endDate])!)
702
383
  end = d3.timeDay.offset(d3.timeDay.ceil(d3.max([startDate, endDate])!), -1)
703
- } else if (this.level === 'quarter-week') {
384
+ } else if (this.ganttChartController.level === 'quarter-week') {
704
385
  start = d3.timeWeek.floor(d3.min([startDate, endDate])!)
705
386
  end = d3.timeDay.offset(d3.timeWeek.ceil(d3.max([startDate, endDate])!), -1)
706
- } else if (this.level === 'year-month') {
387
+ } else if (this.ganttChartController.level === 'year-month') {
707
388
  start = d3.timeMonth.floor(d3.min([startDate, endDate])!)
708
389
  end = d3.timeDay.offset(d3.timeMonth.ceil(d3.max([startDate, endDate])!), -1)
709
- } else if (this.level === 'year-quarter') {
390
+ } else if (this.ganttChartController.level === 'year-quarter') {
710
391
  start = d3.timeMonth.every(3)!.floor(d3.min([startDate, endDate])!)
711
392
  end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
712
393
  }
@@ -717,9 +398,14 @@ export class OxGanttChart extends LitElement {
717
398
  header
718
399
  .append('rect')
719
400
  .attr('class', 'selection-rect')
720
- .attr('x', this.timeScale(start!))
401
+ .attr('x', this.ganttChartController.timeScale(start!))
721
402
  .attr('y', 20)
722
- .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
+ )
723
409
  .attr('height', 20)
724
410
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
725
411
  .attr('opacity', '.2')
@@ -727,9 +413,14 @@ export class OxGanttChart extends LitElement {
727
413
  body
728
414
  .append('rect')
729
415
  .attr('class', 'selection-rect')
730
- .attr('x', this.timeScale(start!))
416
+ .attr('x', this.ganttChartController.timeScale(start!))
731
417
  .attr('y', 0)
732
- .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
+ )
733
424
  .attr('height', '100%')
734
425
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
735
426
  .attr('opacity', '.2')
@@ -741,41 +432,4 @@ export class OxGanttChart extends LitElement {
741
432
 
742
433
  header.node()?.dispatchEvent(customEvent)
743
434
  }
744
-
745
- handleWheelForHeader(event: WheelEvent) {
746
- event.preventDefault()
747
- event.stopPropagation()
748
-
749
- if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {
750
- // 좌우 스크롤
751
- this.container.scrollBy({
752
- left: event.shiftKey ? event.deltaX : event.deltaY,
753
- behavior: 'smooth'
754
- })
755
- } else {
756
- // Zoom in/out
757
- const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9
758
- this.baseWidth *= zoomFactor
759
-
760
- if (this.baseWidth > 40) {
761
- if (this.level == 'week-day') {
762
- this.baseWidth = 40
763
- } else {
764
- this.level = levelDown(this.level)
765
- this.baseWidth = 30
766
- }
767
- } else if (this.baseWidth < 30) {
768
- if (this.level == 'year-quarter') {
769
- this.baseWidth = 30
770
- }
771
- }
772
- if (this.baseWidth < 20) {
773
- this.level = levelUp(this.level)
774
- this.baseWidth = 30
775
- }
776
-
777
- this.updateTimeScale()
778
- this.redraw()
779
- }
780
- }
781
435
  }