@operato/gantt 8.0.0-beta.0 → 8.0.0-beta.2

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.
@@ -1,142 +0,0 @@
1
- import { LitElement, html, css } from 'lit'
2
- import { customElement, property, query } from 'lit/decorators.js'
3
-
4
- @customElement('ox-task-tooltip')
5
- export class OxTaskTooltip extends LitElement {
6
- static styles = css`
7
- :host {
8
- position: fixed;
9
- background-color: var(--ox-gantt-tooltip-background-color, var(--md-sys-color-surface-variant, #f9f9f9));
10
- border: 1px solid var(--ox-gantt-tooltip-border-color, var(--md-sys-color-outline, #ccc));
11
- padding: var(--spacing-medium, 10px);
12
- border-radius: var(--spacing-medium, 8px);
13
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
14
- visibility: hidden;
15
- z-index: 10;
16
- max-width: 300px;
17
- font-family: Roboto;
18
- font-size: 14px;
19
- line-height: 1.6;
20
- color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
21
- transition:
22
- visibility 0.1s,
23
- opacity 0.1s;
24
- opacity: 0;
25
- }
26
- :host([visible]) {
27
- visibility: visible;
28
- opacity: 1;
29
- }
30
- .title {
31
- font-weight: bold;
32
- font-size: 16px;
33
- margin-bottom: 8px;
34
- color: var(--ox-gantt-tooltip-title-color, var(--md-sys-color-primary, #0078d4));
35
- outline: none;
36
- }
37
- .detail {
38
- display: grid;
39
- grid-template-columns: auto auto 1fr;
40
- gap: 4px;
41
- align-items: center;
42
- }
43
- .label {
44
- font-weight: bold;
45
- color: var(--ox-gantt-tooltip-color, var(--md-sys-color-secondary, #333));
46
- text-align: right;
47
- }
48
- .colon {
49
- text-align: center;
50
- color: var(--ox-gantt-tooltip-color, var(--md-sys-color-secondary, #333));
51
- }
52
- .value {
53
- text-align: left;
54
- color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
55
- }
56
- .close-button {
57
- position: absolute;
58
- top: 4px;
59
- right: 8px;
60
- background: none;
61
- border: none;
62
- font-size: 16px;
63
- cursor: pointer;
64
- color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
65
- }
66
- `
67
-
68
- @property({ type: String }) title = ''
69
- @property({ type: String }) startDate = ''
70
- @property({ type: String }) endDate = ''
71
- @property({ type: Number }) progress = 0
72
- @property({ type: String }) type = ''
73
- @property({ type: Boolean, attribute: true }) visible: boolean = false
74
-
75
- @query('.title') titleElement!: HTMLDivElement
76
-
77
- show(x: number, y: number) {
78
- const tooltipWidth = this.offsetWidth
79
- const tooltipHeight = this.offsetHeight
80
- const offsetX = 10
81
- const offsetY = 10
82
-
83
- // 기본 위치 계산
84
- let left = x + offsetX
85
- let top = y + offsetY
86
-
87
- // 화면 경계 확인 및 조정
88
- const screenWidth = window.innerWidth
89
- const screenHeight = window.innerHeight
90
-
91
- if (left + tooltipWidth > screenWidth) {
92
- left = x - tooltipWidth - offsetX
93
- }
94
- if (top + tooltipHeight > screenHeight) {
95
- top = y - tooltipHeight - offsetY
96
- }
97
-
98
- // offsetParent 요소를 기준으로 위치 설정
99
- if (this.offsetParent) {
100
- const parentRect = this.offsetParent!.getBoundingClientRect()
101
- this.style.left = `${left - parentRect.left}px`
102
- this.style.top = `${top - parentRect.top}px`
103
- } else {
104
- this.style.left = `${left}px`
105
- this.style.top = `${top}px`
106
-
107
- const myRect = this.getBoundingClientRect()
108
-
109
- /* 실제 바운드와 설정한 left/top 이 일치하지 않는다면, 부모 어딘가에서 transform 되었다고 판단하고, 보정해줌 */
110
- if (myRect.left !== left || myRect.top !== top) {
111
- this.style.left = `${left + (left - myRect.left)}px`
112
- this.style.top = `${top + (top - myRect.top)}px`
113
- }
114
- }
115
-
116
- this.setAttribute('visible', '')
117
-
118
- setTimeout(() => {
119
- this.titleElement.addEventListener('blur', this.hide)
120
- this.titleElement.focus({ preventScroll: true })
121
- }, 500)
122
- }
123
-
124
- hide = () => {
125
- this.titleElement.removeEventListener('blur', this.hide)
126
- this.removeAttribute('visible')
127
- }
128
-
129
- render() {
130
- return html`
131
- <div class="close-button" @click=${this.hide} @touchstart=${this.hide}>✕</div>
132
- <div class="title" tabindex="0">${this.title}</div>
133
-
134
- <div class="detail">
135
- <span class="label">Type</span><span class="colon">:</span><span class="value">${this.type}</span>
136
- <span class="label">Start Date</span><span class="colon">:</span><span class="value">${this.startDate}</span>
137
- <span class="label">End Date</span><span class="colon">:</span><span class="value">${this.endDate}</span>
138
- <span class="label">Progress</span><span class="colon">:</span><span class="value">${this.progress}%</span>
139
- </div>
140
- `
141
- }
142
- }
@@ -1,284 +0,0 @@
1
- import { LitElement, ReactiveController, ReactiveControllerHost } from 'lit'
2
- import * as d3 from 'd3'
3
- import { TimeScale, Task, Resource } from './types'
4
- import { drawTimeScaleHeader, getScale, getChartWidth } from './draw-time-scale-header'
5
-
6
- export class ResourceChartController implements ReactiveController {
7
- private host: ReactiveControllerHost & LitElement
8
- private fromTime: Date
9
- private toTime: Date
10
- private totalDays: number
11
- private _timeScale?: d3.ScaleTime<number, number, never>
12
-
13
- private touchStartY?: number
14
- private touchStartX?: number
15
- private scrollStartX?: number
16
- private scrollStartY?: number
17
-
18
- public baseWidth: number = 30
19
- public level: TimeScale
20
- public resources: Resource[] = []
21
- public tasks: Task[] = []
22
-
23
- constructor(host: ReactiveControllerHost & LitElement, fromDate: string, toDate: string, axisScale: TimeScale) {
24
- this.host = host
25
- this.fromTime = new Date(fromDate)
26
- this.toTime = new Date(toDate)
27
- this.level = axisScale
28
- this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
29
- this.host.addController(this)
30
- }
31
-
32
- hostConnected() {
33
- this.updateTimeScale()
34
- }
35
-
36
- get timeScale() {
37
- if (!this._timeScale) {
38
- this._timeScale = d3
39
- .scaleTime()
40
- .domain([d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1)])
41
- .range([0, this.getChartWidth()])
42
- }
43
- return this._timeScale
44
- }
45
-
46
- public syncTimeScale(detail: { baseWidth: number; level: TimeScale }) {
47
- this.baseWidth = detail.baseWidth
48
- this.level = detail.level
49
-
50
- this.updateTimeScale()
51
- }
52
-
53
- updateRange(fromDate: string, toDate: string) {
54
- this.fromTime = new Date(fromDate)
55
- this.toTime = new Date(toDate)
56
- this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
57
- this._timeScale = undefined
58
- this.host.requestUpdate()
59
- }
60
-
61
- setResources(resources: Resource[]) {
62
- this.resources = resources
63
- this.host.requestUpdate()
64
- }
65
-
66
- setTasks(tasks: Task[]) {
67
- this.tasks = tasks
68
- this.host.requestUpdate()
69
- }
70
-
71
- updateTimeScale() {
72
- this.timeScale.range([0, this.getChartWidth()])
73
- }
74
-
75
- getScale() {
76
- return getScale(this.level)
77
- }
78
-
79
- getChartWidth() {
80
- return getChartWidth(this.fromTime, this.toTime, this.baseWidth, this.level)
81
- }
82
-
83
- drawAxes(svgHeader: SVGElement) {
84
- drawTimeScaleHeader(svgHeader, this.timeScale, this.fromTime, this.toTime, this.baseWidth, this.level, false)
85
- }
86
-
87
- drawBody(svgBody: SVGElement) {
88
- const body = d3.select(svgBody)
89
-
90
- body.selectAll('*').remove()
91
- body.attr('width', this.getChartWidth())
92
-
93
- if (this.resources.length > 0) {
94
- const chartHeight = this.resources.length * 30
95
-
96
- const addGridLines = (ticks: Date[]) => {
97
- ticks.forEach(tick => {
98
- const startX = this.timeScale(tick)
99
- body
100
- .append('line')
101
- .attr('x1', startX)
102
- .attr('x2', startX)
103
- .attr('y1', 0)
104
- .attr('y2', chartHeight) // Ensure vertical lines extend to the bottom
105
- .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
106
- .attr('stroke-width', 1)
107
- })
108
- }
109
-
110
- let ticks
111
- if (this.level === 'week-day') {
112
- ticks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
113
- } else if (this.level === 'quarter-week') {
114
- ticks = d3.timeWeek.every(1)!.range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
115
- } else if (this.level === 'year-month') {
116
- ticks = d3.timeMonth.every(1)!.range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
117
- } else if (this.level === 'year-quarter') {
118
- ticks = d3.timeMonth.every(3)!.range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
119
- }
120
-
121
- addGridLines(ticks!)
122
-
123
- this.renderResourceAllocations(body, ticks!, chartHeight)
124
- }
125
- }
126
-
127
- renderResourceAllocations(
128
- body: d3.Selection<SVGElement, unknown, null, undefined>,
129
- ticks: Date[],
130
- chartHeight: number
131
- ) {
132
- const resourceAllocations = this.calculateResourceAllocations(ticks)
133
-
134
- this.resources.forEach((resource, index) => {
135
- const y = index * 30 + 15 // Center the text vertically in the row
136
- ticks.forEach(tick => {
137
- const tickEnd = this.getTickEnd(tick)
138
- const dateKey = this.formatDateKey(tick, tickEnd)
139
- const allocation = resourceAllocations[resource.type][dateKey]
140
- if (allocation > 0) {
141
- const startX = this.timeScale(tick)
142
- const tickInterval = this.timeScale(tickEnd) - startX
143
-
144
- body
145
- .append('text')
146
- .attr('x', startX + tickInterval / 2)
147
- .attr('y', y)
148
- .attr('text-anchor', 'middle')
149
- .attr('alignment-baseline', 'middle')
150
- .attr('dominant-baseline', 'middle')
151
- .attr('stroke', 'var(--ox-gantt-color-text, var(--md-sys-color-on-primary-container))')
152
- .text(allocation)
153
- .style('font-size', '12px')
154
- }
155
- })
156
- })
157
-
158
- // Draw horizontal grid lines for each resource row
159
- for (let i = 0; i <= this.resources.length; i++) {
160
- body
161
- .append('line')
162
- .attr('x1', 0)
163
- .attr('x2', this.getChartWidth())
164
- .attr('y1', i * 30)
165
- .attr('y2', i * 30)
166
- .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
167
- .attr('stroke-width', 1)
168
- }
169
- }
170
-
171
- calculateResourceAllocations(ticks: Date[]): { [key: string]: { [key: string]: number } } {
172
- const resourceAllocations: { [key: string]: { [key: string]: number } } = {}
173
-
174
- this.resources.forEach(resource => {
175
- resourceAllocations[resource.type] = {}
176
- ticks.forEach(tick => {
177
- const tickEnd = this.getTickEnd(tick)
178
- const dateKey = this.formatDateKey(tick, tickEnd)
179
- resourceAllocations[resource.type][dateKey] = 0
180
- })
181
- })
182
-
183
- this.tasks.forEach(task => {
184
- const startDate = new Date(task.startDate)
185
- const endDate = new Date(task.endDate)
186
- let currentDate = startDate
187
-
188
- while (currentDate <= endDate) {
189
- const tick = ticks.find(t => currentDate >= t && currentDate < this.getTickEnd(t))
190
- if (tick) {
191
- const tickEnd = this.getTickEnd(tick)
192
- const dateKey = this.formatDateKey(tick, tickEnd)
193
-
194
- ;(task.resources || []).forEach(resource => {
195
- if (resourceAllocations[resource.type][dateKey] !== undefined) {
196
- resourceAllocations[resource.type][dateKey] += resource.allocated
197
- }
198
- })
199
- }
200
-
201
- currentDate.setDate(currentDate.getDate() + 1)
202
- }
203
- })
204
-
205
- return resourceAllocations
206
- }
207
-
208
- getTickEnd(tick: Date): Date {
209
- if (this.level === 'week-day') {
210
- return d3.timeDay.offset(tick, 1)
211
- } else if (this.level === 'quarter-week') {
212
- return d3.timeWeek.offset(tick, 1)
213
- } else if (this.level === 'year-month') {
214
- return d3.timeMonth.offset(tick, 1)
215
- } else if (this.level === 'year-quarter') {
216
- return d3.timeMonth.offset(tick, 3)
217
- }
218
- return d3.timeDay.offset(tick, 1)
219
- }
220
-
221
- formatDateKey(start: Date, end: Date): string {
222
- return `${start.toISOString().split('T')[0]}_${end.toISOString().split('T')[0]}`
223
- }
224
-
225
- handleWheelForBody(event: WheelEvent, container: HTMLDivElement) {
226
- event.preventDefault()
227
- event.stopPropagation()
228
-
229
- const deltaModeFactor =
230
- event.deltaMode === WheelEvent.DOM_DELTA_LINE
231
- ? 16
232
- : event.deltaMode === WheelEvent.DOM_DELTA_PAGE
233
- ? container.clientHeight
234
- : 1
235
- const deltaX = event.deltaX * deltaModeFactor
236
- const deltaY = event.deltaY * deltaModeFactor
237
-
238
- container.scrollLeft += deltaX
239
- container.scrollTop += deltaY
240
-
241
- const customEvent = new CustomEvent('scroll', {
242
- detail: {
243
- scrollLeft: container.scrollLeft,
244
- scrollTop: container.scrollTop
245
- },
246
- bubbles: true,
247
- composed: true
248
- })
249
-
250
- this.host.dispatchEvent(customEvent)
251
- }
252
-
253
- handleTouchStartForBody(event: TouchEvent, container: HTMLDivElement) {
254
- event.stopPropagation()
255
-
256
- this.touchStartX = event.touches[0].clientX
257
- this.touchStartY = event.touches[0].clientY
258
-
259
- this.scrollStartX = container.scrollLeft
260
- this.scrollStartY = container.scrollTop
261
- }
262
-
263
- handleTouchMoveForBody(event: TouchEvent, container: HTMLDivElement) {
264
- const touchCurrentX = event.touches[0].clientX
265
- const touchCurrentY = event.touches[0].clientY
266
-
267
- const deltaX = this.touchStartX! - touchCurrentX
268
- const deltaY = this.touchStartY! - touchCurrentY
269
-
270
- container.scrollLeft = this.scrollStartX! + deltaX
271
- container.scrollTop = this.scrollStartY! + deltaY
272
-
273
- const customEvent = new CustomEvent('scroll', {
274
- detail: {
275
- scrollLeft: container.scrollLeft,
276
- scrollTop: container.scrollTop
277
- },
278
- bubbles: true,
279
- composed: true
280
- })
281
-
282
- this.host.dispatchEvent(customEvent)
283
- }
284
- }
package/src/types.ts DELETED
@@ -1,33 +0,0 @@
1
- export type TaskTag = 'done' | 'active' | 'critical' | 'milestone'
2
- export type TaskType = 'group' | 'task' | 'milestone' | 'phase'
3
-
4
- export interface Task {
5
- id: string
6
- title: string
7
- type: TaskType
8
- startDate: Date | string
9
- endDate: Date | string
10
- progress?: number
11
- dependsOn?: string
12
- duration: number
13
- tags?: TaskTag[]
14
- style?: string
15
- children?: Task[]
16
- resources?: Resource[]
17
- collapsed?: boolean
18
- }
19
-
20
- export interface ColumnConfig {
21
- name: string
22
- label: string
23
- visible: boolean
24
- width: number
25
- order: number
26
- }
27
-
28
- export type TimeScale = 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'
29
-
30
- export interface Resource {
31
- type: string
32
- allocated: number
33
- }
package/src/utils.ts DELETED
@@ -1,13 +0,0 @@
1
- import { TimeScale } from './types'
2
-
3
- const levels: TimeScale[] = ['week-day', 'quarter-week', 'year-month', 'year-quarter']
4
-
5
- export function levelUp(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'): TimeScale {
6
- const current = Math.min(levels.length, levels.indexOf(level) + 1)
7
- return levels[current]
8
- }
9
-
10
- export function levelDown(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'): TimeScale {
11
- const current = Math.max(0, levels.indexOf(level) - 1)
12
- return levels[current]
13
- }
@@ -1,125 +0,0 @@
1
- import { html, TemplateResult } from 'lit'
2
- import { CommonHeaderStyles } from '@operato/styles'
3
- import '../src/ox-gantt-chart.js'
4
-
5
- import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
6
-
7
- export default {
8
- title: 'ox-gantt-chart',
9
- component: 'ox-gantt-chart',
10
- argTypes: {
11
- axisScale: { control: 'select', options: ['year-quarter', 'year-month', 'quarter-week', 'week-day'] },
12
- fromDate: { control: 'string' },
13
- toDate: { control: 'string' },
14
- extendGridLines: { control: 'boolean' }
15
- }
16
- }
17
-
18
- interface Story<T> {
19
- (args: T): TemplateResult
20
- args?: Partial<T>
21
- argTypes?: Record<string, unknown>
22
- }
23
-
24
- interface ArgTypes {
25
- axisScale: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'
26
- fromDate: string
27
- toDate: string
28
- extendGridLines: boolean
29
- }
30
-
31
- const Template: Story<ArgTypes> = ({
32
- axisScale = 'week-day',
33
- fromDate = '',
34
- toDate = '',
35
- extendGridLines
36
- }: ArgTypes) => html`
37
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet" />
38
-
39
- <link href="/themes/light.css" rel="stylesheet" />
40
- <link href="/themes/dark.css" rel="stylesheet" />
41
- <link href="/themes/spacing.css" rel="stylesheet" />
42
- <link href="/themes/md-typescale-styles.css" rel="stylesheet" />
43
-
44
- <link
45
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL@20..48,100..700,0..1"
46
- rel="stylesheet"
47
- />
48
- <link
49
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL@20..48,100..700,0..1"
50
- rel="stylesheet"
51
- />
52
- <link
53
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp:opsz,wght,FILL@20..48,100..700,0..1"
54
- rel="stylesheet"
55
- />
56
-
57
- <style>
58
- ${MDTypeScaleStyles.cssText}
59
- </style>
60
-
61
- <style>
62
- ${CommonHeaderStyles.cssText}
63
- </style>
64
-
65
- <style>
66
- body {
67
- width: 100%;
68
- height: 800px;
69
- text-align: center;
70
-
71
- background-color: var(--md-sys-color-background);
72
- color: var(--md-sys-color-on-background);
73
- }
74
-
75
- ox-gantt {
76
- width: 90%;
77
- height: 500px;
78
- box-sizing: border-box;
79
-
80
- background-color: var(--md-sys-color-primary-container);
81
- color: var(--md-sys-color-on-primary-container);
82
- }
83
- </style>
84
-
85
- <style>
86
- ox-gantt-chart {
87
- height: 500px;
88
- border: 1px solid black;
89
- }
90
- </style>
91
-
92
- <ox-gantt-chart
93
- .axisScale=${axisScale}
94
- .fromDate=${fromDate}
95
- .toDate=${toDate}
96
- ?extend-grid-lines=${extendGridLines}
97
- ></ox-gantt-chart>
98
- `
99
-
100
- export const YearMonth = Template.bind({})
101
- YearMonth.args = {
102
- axisScale: 'year-quarter',
103
- fromDate: '2020-01-01',
104
- toDate: '2025-12-31'
105
- }
106
-
107
- YearMonth.args = {
108
- axisScale: 'year-month',
109
- fromDate: '2022-01-01',
110
- toDate: '2024-12-31'
111
- }
112
-
113
- export const MonthWeek = Template.bind({})
114
- MonthWeek.args = {
115
- axisScale: 'quarter-week',
116
- fromDate: '2022-01-01',
117
- toDate: '2023-03-31'
118
- }
119
-
120
- export const WeekDay = Template.bind({})
121
- WeekDay.args = {
122
- axisScale: 'week-day',
123
- fromDate: '2022-01-01',
124
- toDate: '2022-11-15'
125
- }