@operato/gantt 2.0.0-beta.5 → 7.0.0-rc.0

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 (47) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/src/gantt-chart-controller.d.ts +32 -0
  3. package/dist/src/gantt-chart-controller.js +395 -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 +11 -20
  9. package/dist/src/ox-gantt-chart.js +141 -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 +17 -3
  15. package/dist/src/ox-gantt.js +169 -29
  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 +11 -4
  21. package/dist/src/types.js.map +1 -1
  22. package/dist/src/utils.d.ts +3 -3
  23. package/dist/src/utils.js.map +1 -1
  24. package/dist/stories/ox-gantt.stories.d.ts +19 -2
  25. package/dist/stories/ox-gantt.stories.js +104 -90
  26. package/dist/stories/ox-gantt.stories.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +7 -4
  29. package/src/gantt-chart-controller.ts +473 -0
  30. package/src/locales.ts +78 -0
  31. package/src/ox-gantt-chart.ts +151 -484
  32. package/src/ox-gantt-resize-splitter.ts +18 -25
  33. package/src/ox-gantt.ts +174 -31
  34. package/src/ox-task-tooltip.ts +120 -0
  35. package/src/types.ts +13 -5
  36. package/src/utils.ts +4 -4
  37. package/stories/ox-gantt.stories.ts +118 -91
  38. package/themes/grist-theme.css +0 -2
  39. package/translations/en.json +11 -1
  40. package/translations/ja.json +11 -1
  41. package/translations/ko.json +11 -1
  42. package/translations/ms.json +11 -1
  43. package/translations/zh.json +11 -1
  44. package/dist/src/locale.d.ts +0 -1
  45. package/dist/src/locale.js +0 -49
  46. package/dist/src/locale.js.map +0 -1
  47. package/src/locale.ts +0 -51
@@ -1,16 +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
-
11
- import { Task } from './types.js'
12
-
13
- type TimeScale = 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'
10
+ import { GanttChartController } from './gantt-chart-controller'
11
+ import { Task, TimeScale } from './types.js'
12
+ import { levelDown, levelUp } from './utils'
13
+ import { OxTaskTooltip } from './ox-task-tooltip.js'
14
14
 
15
15
  @customElement('ox-gantt-chart')
16
16
  export class OxGanttChart extends LitElement {
@@ -20,491 +20,177 @@ export class OxGanttChart extends LitElement {
20
20
  css`
21
21
  :host {
22
22
  display: flex;
23
-
24
23
  overflow: hidden;
24
+ position: relative;
25
25
  }
26
-
27
26
  div[chart-container] {
28
27
  display: block;
29
-
30
28
  overflow-x: auto;
31
- overflow-y: clip;
32
-
33
- position: relative;
29
+ overflow-y: hidden;
34
30
  }
35
-
36
31
  svg {
37
32
  min-width: 100%;
38
33
  }
39
-
40
34
  svg[header] {
41
35
  display: block;
42
36
  min-width: 100%;
43
37
  box-sizing: border-box;
44
- border-bottom: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
38
+ border-bottom: 1px solid var(--ox-gantt-header-border-color, var(--md-sys-color-on-secondary-container));
45
39
  height: 40px;
46
40
  background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
41
+ color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
47
42
 
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
+ }
99
+
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'
104
+ }
105
105
 
106
- redrawRequired = true
106
+ // Update locale
107
+ if (changedProperties.has('locale')) {
108
+ this.updateLocale(this.locale)
107
109
  }
108
110
 
109
- if (changedProperties.has('axisScale')) {
110
- redrawRequired = true
111
+ if (changedProperties.has('selectedTaskId')) {
112
+ this.ganttChartController.selectedTaskId = this.selectedTaskId
111
113
  }
112
114
 
113
- if (changedProperties.has('tasks')) {
114
- this.svgBody.style.height = this.tasks.length * 30 + 'px'
115
- redrawRequired = true
115
+ if (changedProperties.has('extendGridLines')) {
116
+ this.ganttChartController.extendGridLines = this.extendGridLines
117
+ }
118
+
119
+ if (changedProperties.has('axisScale')) {
120
+ this.ganttChartController.level = this.axisScale
121
+ this.ganttChartController.updateTimeScale()
116
122
  }
117
123
 
118
- redrawRequired && this.redraw()
124
+ this.ganttChartController.drawAxes(this.svgHeader)
125
+ this.ganttChartController.drawBody(this.svgBody, this.tooltip)
119
126
  }
120
127
 
121
128
  render() {
122
129
  return html`
123
- <div chart-container>
130
+ <div
131
+ chart-container
132
+ @wheel=${(e: WheelEvent) => this.ganttChartController.handleWheelForBody(e, this.container)}
133
+ @touchstart=${(e: TouchEvent) => this.ganttChartController.handleTouchStartForBody(e, this.container)}
134
+ @touchmove=${(e: TouchEvent) => this.ganttChartController.handleTouchMoveForBody(e, this.container)}
135
+ >
124
136
  <svg
125
137
  header
126
- @date-range-selected=${(e: CustomEvent) => {
127
- console.log(e.detail)
128
- }}
138
+ @date-range-selected=${(e: CustomEvent) => console.log(e.detail)}
129
139
  @wheel=${this.handleWheelForHeader.bind(this)}
130
140
  ></svg>
131
141
  <svg body></svg>
142
+ <ox-task-tooltip></ox-task-tooltip> // 툴팁 요소 추가
132
143
  </div>
133
144
  `
134
145
  }
135
146
 
136
147
  setVerticalOffset(offset: number) {
137
- console.log(offset)
138
148
  this.container.scrollTop = offset
139
149
  }
140
150
 
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
150
- }
151
-
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()
151
+ getVerticalOffset(): number {
152
+ return this.container.scrollTop
175
153
  }
176
154
 
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
- }
155
+ handleWheelForHeader(event: WheelEvent) {
156
+ event.preventDefault()
157
+ event.stopPropagation()
218
158
 
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)
159
+ if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {
160
+ this.container.scrollBy({
161
+ left: event.shiftKey ? event.deltaX : event.deltaY
248
162
  })
163
+ } else {
164
+ const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9
165
+ this.ganttChartController.baseWidth *= zoomFactor
249
166
 
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')
167
+ if (this.ganttChartController.baseWidth > 40) {
168
+ if (this.ganttChartController.level == 'week-day') {
169
+ this.ganttChartController.baseWidth = 40
170
+ } else {
171
+ this.ganttChartController.level = levelDown(this.ganttChartController.level)
172
+ this.ganttChartController.baseWidth = 30
173
+ }
174
+ } else if (this.ganttChartController.baseWidth < 30) {
175
+ if (this.ganttChartController.level == 'year-quarter') {
176
+ this.ganttChartController.baseWidth = 30
177
+ }
260
178
  }
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)
179
+ if (this.ganttChartController.baseWidth < 20) {
180
+ this.ganttChartController.level = levelUp(this.ganttChartController.level)
181
+ this.ganttChartController.baseWidth = 30
298
182
  }
299
- })
300
- }
301
183
 
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'
184
+ this.ganttChartController.updateTimeScale()
185
+ this.requestUpdate()
312
186
  }
313
187
  }
314
188
 
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)
189
+ updateLocale(locale: string) {
190
+ const localeDefinition = (locales as any)[locale || 'en-US']
191
+ if (localeDefinition) {
192
+ d3.timeFormatDefaultLocale(localeDefinition)
193
+ this.ganttChartController.updateTimeScale()
508
194
  }
509
195
  }
510
196
 
@@ -516,7 +202,7 @@ export class OxGanttChart extends LitElement {
516
202
  let lastTouchDistance: number | null = null
517
203
 
518
204
  const handleTouchStart = (event: TouchEvent) => {
519
- event.preventDefault() // 기본 터치 이벤트 막기
205
+ event.preventDefault()
520
206
 
521
207
  if (event.touches.length === 2) {
522
208
  lastTouchDistance = getTouchDistance(event.touches)
@@ -529,32 +215,32 @@ export class OxGanttChart extends LitElement {
529
215
  }
530
216
 
531
217
  const handleTouchMove = (event: TouchEvent) => {
532
- event.preventDefault() // 기본 터치 이벤트 막기
218
+ event.preventDefault()
533
219
 
534
220
  if (event.touches.length === 2 && lastTouchDistance !== null) {
535
221
  const currentDistance = getTouchDistance(event.touches)
536
222
  const zoomFactor = currentDistance / lastTouchDistance
537
- this.baseWidth *= zoomFactor
223
+ this.ganttChartController.baseWidth *= zoomFactor
538
224
 
539
- if (this.baseWidth > 60) {
540
- if (this.level == 'year-quarter') {
541
- this.baseWidth = 60
225
+ if (this.ganttChartController.baseWidth > 60) {
226
+ if (this.ganttChartController.level == 'year-quarter') {
227
+ this.ganttChartController.baseWidth = 60
542
228
  } else {
543
- this.level = levelUp(this.level)
544
- this.baseWidth = 30
229
+ this.ganttChartController.level = levelUp(this.ganttChartController.level)
230
+ this.ganttChartController.baseWidth = 30
545
231
  }
546
- } else if (this.baseWidth < 30) {
547
- if (this.level == 'week-day') {
548
- this.baseWidth = 30
232
+ } else if (this.ganttChartController.baseWidth < 30) {
233
+ if (this.ganttChartController.level == 'week-day') {
234
+ this.ganttChartController.baseWidth = 30
549
235
  } else {
550
- this.level = levelDown(this.level)
551
- this.baseWidth = 60
236
+ this.ganttChartController.level = levelDown(this.ganttChartController.level)
237
+ this.ganttChartController.baseWidth = 60
552
238
  }
553
239
  }
554
240
 
555
241
  lastTouchDistance = currentDistance
556
- this.updateTimeScale()
557
- this.redraw()
242
+ this.ganttChartController.updateTimeScale()
243
+ this.requestUpdate()
558
244
  } else if (isDragging && startX !== null) {
559
245
  const touch = event.touches[0]
560
246
  const containerOffsetX = header.node()!.getBoundingClientRect().left
@@ -564,14 +250,13 @@ export class OxGanttChart extends LitElement {
564
250
  }
565
251
 
566
252
  const handleTouchEnd = (event: TouchEvent) => {
567
- event.preventDefault() // 기본 터치 이벤트 막기
253
+ event.preventDefault()
568
254
 
569
255
  if (event.touches.length < 2) {
570
256
  lastTouchDistance = null
571
257
  }
572
258
 
573
259
  if (!isDragging || startX === null) {
574
- // 클릭 이벤트 처리
575
260
  const touch = event.changedTouches[0]
576
261
  const containerOffsetX = header.node()!.getBoundingClientRect().left
577
262
  const clickX = touch.clientX - containerOffsetX
@@ -610,7 +295,6 @@ export class OxGanttChart extends LitElement {
610
295
 
611
296
  const handleMouseUp = (event: MouseEvent) => {
612
297
  if (!isDragging || startX === null) {
613
- // 클릭 이벤트 처리
614
298
  const containerOffsetX = header.node()!.getBoundingClientRect().left
615
299
  const clickX = event.clientX - containerOffsetX
616
300
  this.finalizeSelection(clickX, clickX)
@@ -637,21 +321,21 @@ export class OxGanttChart extends LitElement {
637
321
  const header = d3.select(this.svgHeader)
638
322
  const body = d3.select(this.svgBody)
639
323
 
640
- let startDate = this.timeScale.invert(startX)
641
- let endDate = this.timeScale.invert(endX)
324
+ let startDate = this.ganttChartController.timeScale.invert(startX)
325
+ let endDate = this.ganttChartController.timeScale.invert(endX)
642
326
 
643
327
  let start, end
644
328
 
645
- if (this.level === 'week-day') {
329
+ if (this.ganttChartController.level === 'week-day') {
646
330
  start = d3.timeDay.floor(d3.min([startDate, endDate])!)
647
331
  end = d3.timeDay.offset(d3.timeDay.ceil(d3.max([startDate, endDate])!), -1)
648
- } else if (this.level === 'quarter-week') {
332
+ } else if (this.ganttChartController.level === 'quarter-week') {
649
333
  start = d3.timeWeek.floor(d3.min([startDate, endDate])!)
650
334
  end = d3.timeDay.offset(d3.timeWeek.ceil(d3.max([startDate, endDate])!), -1)
651
- } else if (this.level === 'year-month') {
335
+ } else if (this.ganttChartController.level === 'year-month') {
652
336
  start = d3.timeMonth.floor(d3.min([startDate, endDate])!)
653
337
  end = d3.timeDay.offset(d3.timeMonth.ceil(d3.max([startDate, endDate])!), -1)
654
- } else if (this.level === 'year-quarter') {
338
+ } else if (this.ganttChartController.level === 'year-quarter') {
655
339
  start = d3.timeMonth.every(3)!.floor(d3.min([startDate, endDate])!)
656
340
  end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
657
341
  }
@@ -662,9 +346,14 @@ export class OxGanttChart extends LitElement {
662
346
  header
663
347
  .append('rect')
664
348
  .attr('class', 'selection-rect')
665
- .attr('x', this.timeScale(start!))
349
+ .attr('x', this.ganttChartController.timeScale(start!))
666
350
  .attr('y', 20)
667
- .attr('width', this.timeScale(end!) - this.timeScale(start!) + this.baseWidth / this.getScale())
351
+ .attr(
352
+ 'width',
353
+ this.ganttChartController.timeScale(end!) -
354
+ this.ganttChartController.timeScale(start!) +
355
+ this.ganttChartController.baseWidth / this.ganttChartController.getScale()
356
+ )
668
357
  .attr('height', 20)
669
358
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
670
359
  .attr('opacity', '.2')
@@ -672,9 +361,14 @@ export class OxGanttChart extends LitElement {
672
361
  body
673
362
  .append('rect')
674
363
  .attr('class', 'selection-rect')
675
- .attr('x', this.timeScale(start!))
364
+ .attr('x', this.ganttChartController.timeScale(start!))
676
365
  .attr('y', 0)
677
- .attr('width', this.timeScale(end!) - this.timeScale(start!) + this.baseWidth / this.getScale())
366
+ .attr(
367
+ 'width',
368
+ this.ganttChartController.timeScale(end!) -
369
+ this.ganttChartController.timeScale(start!) +
370
+ this.ganttChartController.baseWidth / this.ganttChartController.getScale()
371
+ )
678
372
  .attr('height', '100%')
679
373
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
680
374
  .attr('opacity', '.2')
@@ -684,21 +378,21 @@ export class OxGanttChart extends LitElement {
684
378
  const header = d3.select(this.svgHeader)
685
379
  const body = d3.select(this.svgBody)
686
380
 
687
- let startDate = this.timeScale.invert(startX)
688
- let endDate = this.timeScale.invert(endX)
381
+ let startDate = this.ganttChartController.timeScale.invert(startX)
382
+ let endDate = this.ganttChartController.timeScale.invert(endX)
689
383
 
690
384
  let start, end
691
385
 
692
- if (this.level === 'week-day') {
386
+ if (this.ganttChartController.level === 'week-day') {
693
387
  start = d3.timeDay.floor(d3.min([startDate, endDate])!)
694
388
  end = d3.timeDay.offset(d3.timeDay.ceil(d3.max([startDate, endDate])!), -1)
695
- } else if (this.level === 'quarter-week') {
389
+ } else if (this.ganttChartController.level === 'quarter-week') {
696
390
  start = d3.timeWeek.floor(d3.min([startDate, endDate])!)
697
391
  end = d3.timeDay.offset(d3.timeWeek.ceil(d3.max([startDate, endDate])!), -1)
698
- } else if (this.level === 'year-month') {
392
+ } else if (this.ganttChartController.level === 'year-month') {
699
393
  start = d3.timeMonth.floor(d3.min([startDate, endDate])!)
700
394
  end = d3.timeDay.offset(d3.timeMonth.ceil(d3.max([startDate, endDate])!), -1)
701
- } else if (this.level === 'year-quarter') {
395
+ } else if (this.ganttChartController.level === 'year-quarter') {
702
396
  start = d3.timeMonth.every(3)!.floor(d3.min([startDate, endDate])!)
703
397
  end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
704
398
  }
@@ -709,9 +403,14 @@ export class OxGanttChart extends LitElement {
709
403
  header
710
404
  .append('rect')
711
405
  .attr('class', 'selection-rect')
712
- .attr('x', this.timeScale(start!))
406
+ .attr('x', this.ganttChartController.timeScale(start!))
713
407
  .attr('y', 20)
714
- .attr('width', this.timeScale(end!) - this.timeScale(start!) + this.baseWidth / this.getScale())
408
+ .attr(
409
+ 'width',
410
+ this.ganttChartController.timeScale(end!) -
411
+ this.ganttChartController.timeScale(start!) +
412
+ this.ganttChartController.baseWidth / this.ganttChartController.getScale()
413
+ )
715
414
  .attr('height', 20)
716
415
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
717
416
  .attr('opacity', '.2')
@@ -719,9 +418,14 @@ export class OxGanttChart extends LitElement {
719
418
  body
720
419
  .append('rect')
721
420
  .attr('class', 'selection-rect')
722
- .attr('x', this.timeScale(start!))
421
+ .attr('x', this.ganttChartController.timeScale(start!))
723
422
  .attr('y', 0)
724
- .attr('width', this.timeScale(end!) - this.timeScale(start!) + this.baseWidth / this.getScale())
423
+ .attr(
424
+ 'width',
425
+ this.ganttChartController.timeScale(end!) -
426
+ this.ganttChartController.timeScale(start!) +
427
+ this.ganttChartController.baseWidth / this.ganttChartController.getScale()
428
+ )
725
429
  .attr('height', '100%')
726
430
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
727
431
  .attr('opacity', '.2')
@@ -733,41 +437,4 @@ export class OxGanttChart extends LitElement {
733
437
 
734
438
  header.node()?.dispatchEvent(customEvent)
735
439
  }
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
440
  }