sprintify-ui 0.2.9 → 0.2.11

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 (35) hide show
  1. package/dist/sprintify-ui.es.js +4 -4
  2. package/dist/types/src/components/BaseActionItem.vue.d.ts +27 -20
  3. package/dist/types/src/components/BaseActionItemButton.vue.d.ts +21 -28
  4. package/dist/types/src/components/BaseAddressForm.vue.d.ts +63 -50
  5. package/dist/types/src/components/BaseBadge.vue.d.ts +41 -40
  6. package/dist/types/src/components/BaseBoolean.vue.d.ts +9 -14
  7. package/dist/types/src/components/BaseCropper.vue.d.ts +42 -25
  8. package/dist/types/src/components/BaseCropperModal.vue.d.ts +18 -17
  9. package/dist/types/src/components/BaseDataIteratorSectionBox.vue.d.ts +11 -14
  10. package/dist/types/src/components/BaseDataIteratorSectionButton.vue.d.ts +12 -15
  11. package/dist/types/src/components/BaseDataIteratorSectionModal.vue.d.ts +20 -17
  12. package/dist/types/src/components/BaseDataTableRowAction.vue.d.ts +18 -15
  13. package/dist/types/src/components/BaseDatePicker.vue.d.ts +107 -74
  14. package/dist/types/src/components/BaseDraggable.vue.d.ts +35 -20
  15. package/dist/types/src/components/BaseFilePicker.vue.d.ts +43 -42
  16. package/dist/types/src/components/BaseFilePickerCrop.vue.d.ts +43 -40
  17. package/dist/types/src/components/BaseFileUploader.vue.d.ts +83 -62
  18. package/dist/types/src/components/BaseGantt.vue.d.ts +424 -0
  19. package/dist/types/src/components/BaseHeader.vue.d.ts +81 -66
  20. package/dist/types/src/components/BaseIconPicker.vue.d.ts +27 -34
  21. package/dist/types/src/components/BaseLayoutNotificationItemContent.vue.d.ts +18 -15
  22. package/dist/types/src/components/BaseSideNavigation.vue.d.ts +11 -26
  23. package/dist/types/src/components/BaseSideNavigationItem.vue.d.ts +27 -32
  24. package/dist/types/src/components/BaseTabItem.vue.d.ts +27 -32
  25. package/dist/types/src/components/BaseTabs.vue.d.ts +11 -26
  26. package/dist/types/src/services/gantt/format.d.ts +24 -0
  27. package/dist/types/src/services/gantt/timescale.d.ts +26 -0
  28. package/dist/types/src/services/gantt/types.d.ts +67 -0
  29. package/package.json +1 -1
  30. package/src/components/BaseGantt.stories.js +130 -0
  31. package/src/components/BaseGantt.vue +333 -0
  32. package/src/components/BaseTextareaAutoresize.vue +2 -2
  33. package/src/services/gantt/format.ts +113 -0
  34. package/src/services/gantt/timescale.ts +243 -0
  35. package/src/services/gantt/types.ts +75 -0
@@ -0,0 +1,333 @@
1
+ <template>
2
+ <div
3
+ class="flex"
4
+ :style="{
5
+ maxHeight: `${maxHeight}px`,
6
+ }"
7
+ >
8
+ <!-- Sidebar -->
9
+ <div
10
+ class="border-r border-slate-300 relative shrink-0"
11
+ :style="{ minWidth: `${SIDEBAR_WIDTH}px` }"
12
+ >
13
+ <!-- Top-left Corner-->
14
+ <div
15
+ :style="{
16
+ height: `${HEADER_HEIGHT}px`,
17
+ zIndex: 1
18
+ }"
19
+ class="border-b relative border-slate-300 bg-white"
20
+ />
21
+
22
+ <!-- Sidebar Items-->
23
+
24
+ <ul
25
+ class=""
26
+ :style="{
27
+ transform: `translateY(${scrollY}px)`,
28
+ }"
29
+ >
30
+ <li
31
+ v-for="row in rowsInternal"
32
+ :key="row.id"
33
+ class="border-b border-slate-300 flex last:border-none"
34
+ :style="{
35
+ height: `${props.rowHeight}px`,
36
+ }"
37
+ >
38
+ <slot
39
+ name="sidebarItem"
40
+ :row="row"
41
+ >
42
+ <div class="px-2 flex items-center">
43
+ <p class="font-semibold text-sm">
44
+ {{ row.name }}
45
+ </p>
46
+ </div>
47
+ </slot>
48
+ </li>
49
+ </ul>
50
+ </div>
51
+
52
+ <!-- Content -->
53
+
54
+ <div
55
+ ref="contentRef"
56
+ class="grow flex flex-col relative overflow-hidden"
57
+ >
58
+ <!-- Time Scale Header -->
59
+
60
+ <div
61
+ class="bg-white border-b shrink-0 border-slate-300 w-full"
62
+ :style="{
63
+ zIndex: 1,
64
+ height: HEADER_HEIGHT + 'px',
65
+ width: `${width}px`,
66
+ transform: `translateX(${scrollX}px)`,
67
+ }"
68
+ >
69
+ <svg
70
+ :view-box="`${width} ${HEADER_HEIGHT}`"
71
+ :width="width"
72
+ :height="HEADER_HEIGHT"
73
+ >
74
+ <g
75
+ v-for="group in groups"
76
+ :key="group.x"
77
+ >
78
+ <text
79
+ :x="group.labelX"
80
+ :y="15"
81
+ class="text-[12px] font-semibold text-slate-900"
82
+ fill="currentColor"
83
+ :text-anchor="group.labelTextAnchor"
84
+ >
85
+ {{ group.label }}
86
+ </text>
87
+ <line
88
+ :x1="group.x + group.width"
89
+ :x2="group.x + group.width"
90
+ :y1="0"
91
+ :y2="HEADER_HEIGHT"
92
+ :stroke="slate[300]"
93
+ ></line>
94
+ </g>
95
+
96
+ <g
97
+ v-for="tick in ticks"
98
+ :key="tick.x"
99
+ :transform="`translate(${tick.x}, 0)`"
100
+ >
101
+ <text
102
+ :x="tick.align == 'middle' ? tick.width / 2 : 0"
103
+ :y="33"
104
+ class="text-[11px] font-normal text-slate-600"
105
+ fill="currentColor"
106
+ text-anchor="middle"
107
+ >
108
+ {{ tick.label }}
109
+ </text>
110
+
111
+ <line
112
+ v-if="tick.align == 'middle'"
113
+ :x1="tick.width"
114
+ :x2="tick.width"
115
+ :y1="24"
116
+ :y2="HEADER_HEIGHT"
117
+ :stroke="slate[300]"
118
+ ></line>
119
+ </g>
120
+ </svg>
121
+ </div>
122
+
123
+ <div
124
+ v-if="currentGroup"
125
+ class="absolute top-0 left-0 inline-flex"
126
+ :style="{
127
+ zIndex: 1,
128
+ }"
129
+ >
130
+ <div class="text-xs font-semibold pt-[3px] bg-white px-2">
131
+ {{ currentGroup.label }}
132
+ </div>
133
+ <div class="bg-gradient-to-r from-white to-transparent w-20" />
134
+ </div>
135
+
136
+ <!-- Gantt Items -->
137
+
138
+ <ul
139
+ ref="itemsRef"
140
+ class="relative overflow-scroll grow"
141
+ >
142
+ <li
143
+ v-for="row in rowsInternal"
144
+ :key="row.id"
145
+ class="border-b relative border-slate-300 last:border-none"
146
+ :style="{
147
+ height: `${props.rowHeight}px`,
148
+ width: `${width}px`,
149
+ }"
150
+ >
151
+ <button
152
+ v-for="item in row.items"
153
+ :key="item.id"
154
+ type="button"
155
+ class="absolute flex"
156
+ :style="{
157
+ transform: `translate(${item.x}px, ${item.y}px)`,
158
+ height: item.height + 'px',
159
+ width: item.width + 'px',
160
+ }"
161
+ :title="`${item.name} - ${item.start.toFormat('yyyy-MM-dd HH:mm:ss')} - ${item.end.toFormat('yyyy-MM-dd HH:mm:ss')}`"
162
+ @click="$emit('item:click', item)"
163
+ >
164
+ <slot
165
+ name="item"
166
+ :item="item"
167
+ >
168
+ <div
169
+ :style="{
170
+ backgroundColor: item.color,
171
+ }"
172
+ class="flex w-full h-full items-center rounded hover:opacity-80 duration-200"
173
+ >
174
+ <p
175
+ class="text-white text-xs font-medium px-2 py-1 truncate"
176
+ style="text-shadow: 0.5px 0.5px rgba(0,0,0,0.1);"
177
+ >
178
+ {{ item.name }}
179
+ </p>
180
+ </div>
181
+ </slot>
182
+ </button>
183
+ </li>
184
+ </ul>
185
+
186
+ <!-- Vertical lines -->
187
+
188
+ <div
189
+ class="absolute top-0 left-0 pointer-events-none"
190
+ :style="{
191
+ width: `${width}px`,
192
+ height: height + 'px',
193
+ transform: `translateX(${scrollX}px)`,
194
+ }"
195
+ >
196
+ <svg
197
+ :view-box="`${width} ${HEADER_HEIGHT}`"
198
+ :width="width"
199
+ :height="height"
200
+ >
201
+ <g
202
+ v-for="group in groups"
203
+ :key="group.x"
204
+ >
205
+ <line
206
+ :x1="group.x"
207
+ :x2="group.x"
208
+ y1="0"
209
+ :y2="height"
210
+ stroke="black"
211
+ :opacity="0.2"
212
+ ></line>
213
+ </g>
214
+ <g
215
+ v-for="tick in ticks"
216
+ :key="tick.x"
217
+ >
218
+ <line
219
+ :x1="tick.x"
220
+ :x2="tick.x"
221
+ :y1="0"
222
+ :y2="height"
223
+ stroke="black"
224
+ :opacity="0.1"
225
+ ></line>
226
+ </g>
227
+ </svg>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </template>
232
+
233
+ <script lang="ts" setup>
234
+ import { useElementSize, useScroll } from '@vueuse/core';
235
+ import { Format } from '@/services/gantt/format';
236
+ import { FormatConfig, GanttRow, GanttRowFormatted, Group, Tick } from '@/services/gantt/types';
237
+ import { debounce } from 'lodash';
238
+ import { slate } from 'tailwindcss/colors';
239
+
240
+ const props = withDefaults(defineProps<{
241
+ rows: GanttRow[],
242
+ rowHeight?: number,
243
+ rowPadding?: number,
244
+ maxHeight?: number,
245
+ }>(), {
246
+ rowHeight: 40,
247
+ rowPadding: 4,
248
+ maxHeight: undefined,
249
+ });
250
+
251
+ defineEmits([
252
+ 'item:click',
253
+ ]);
254
+
255
+ // Config
256
+
257
+ const SIDEBAR_WIDTH = 120;
258
+ const HEADER_HEIGHT = 40;
259
+
260
+ // Init
261
+
262
+ const contentRef = ref<HTMLDivElement | null>(null);
263
+ const contentSize = useElementSize(contentRef);
264
+
265
+ const width = ref(800);
266
+ const height = ref(100);
267
+
268
+ const rowsInternal = ref<GanttRowFormatted[]>([]);
269
+ const groups = ref<Group[]>([]);
270
+ const ticks = ref<Tick[]>([]);
271
+
272
+ const config = computed<FormatConfig>(() => {
273
+ return {
274
+ rows: props.rows,
275
+ minWidth: contentSize.width.value,
276
+ rowHeight: props.rowHeight,
277
+ rowPadding: props.rowPadding,
278
+ };
279
+ });
280
+
281
+ function init() {
282
+
283
+ const format = (new Format(config.value)).handle();
284
+
285
+ width.value = format.width;
286
+ height.value = format.height;
287
+
288
+ rowsInternal.value = format.rows;
289
+
290
+ groups.value = format.groups;
291
+ ticks.value = format.ticks;
292
+ }
293
+
294
+ // Init triggers
295
+
296
+ const initDebounced = debounce(init, 200);
297
+
298
+ watch(() => config.value, initDebounced, { deep: true });
299
+
300
+ // Scroll
301
+
302
+ const itemsRef = ref<HTMLUListElement | null>(null);
303
+
304
+ const scrollX = ref(0);
305
+ const scrollY = ref(0);
306
+
307
+ useScroll(itemsRef, {
308
+ onScroll: (e: any) => {
309
+ if (!e.target) return;
310
+
311
+ scrollX.value = -e.target.scrollLeft;
312
+ scrollY.value = -e.target.scrollTop;
313
+ },
314
+ });
315
+
316
+ // Current Group
317
+
318
+ const currentGroup = computed<Group | undefined>(() => {
319
+
320
+ const offsetLeft = -scrollX.value;
321
+
322
+ return groups.value
323
+ // Sort by x descending
324
+ .sort((a, b) => {
325
+ return b.x - a.x;
326
+ })
327
+ .find((group) => {
328
+ // - 1 to avoid flashing when scrolling
329
+ return group.x <= offsetLeft && group.x + group.width >= offsetLeft - 1;
330
+ });
331
+ });
332
+
333
+ </script>
@@ -143,8 +143,8 @@ const textareaClasses = computed(() => {
143
143
  [
144
144
  BASE_TEXTAREA_CLASSES,
145
145
  hasErrorInternal.value ? 'border-red-500' : 'border-slate-300',
146
- ]
147
- //props.twTextarea
146
+ ],
147
+ props.twTextarea
148
148
  );
149
149
  });
150
150
  </script>
@@ -0,0 +1,113 @@
1
+ import { DateTime } from "luxon";
2
+ import { FormatConfig, GanttItem, GanttItemFormatted, GanttRow, GanttRowFormatted } from "./types";
3
+ import { minBy } from "lodash";
4
+ import { Timescale } from "./timescale";
5
+
6
+ export class Format {
7
+
8
+ private rows: GanttRow[];
9
+ private config: FormatConfig;
10
+
11
+ constructor(config: FormatConfig) {
12
+ this.rows = config.rows;
13
+ this.config = config;
14
+ }
15
+
16
+ public handle() {
17
+
18
+ // Format rows
19
+ const rowsFormatted = this.formatGanttRows();
20
+
21
+ // Get min and max
22
+ let { min, max } = this.getMinMax(rowsFormatted);
23
+
24
+ // Get timescale
25
+ const timescale = (new Timescale(this.config.minWidth, min, max)).handle();
26
+
27
+ // Padded min and max
28
+ min = timescale.min;
29
+ max = timescale.max;
30
+
31
+ const millisecondToPixel = timescale.millisecondToPixel;
32
+
33
+ // Set x, y, width and height
34
+
35
+ rowsFormatted.forEach((row) => {
36
+ row.items.forEach((item) => {
37
+
38
+ const x = (item.start.toMillis() - min.toMillis()) * millisecondToPixel;
39
+ const y = this.config.rowPadding;
40
+ const width = item.milliseconds * millisecondToPixel;
41
+ const height = this.config.rowHeight - (this.config.rowPadding * 2);
42
+
43
+ item.x = x;
44
+ item.y = y;
45
+ item.width = width;
46
+ item.height = height;
47
+ });
48
+ });
49
+
50
+ return {
51
+ rows: rowsFormatted,
52
+ min,
53
+ max,
54
+ millisecondToPixel,
55
+ height: this.config.rowHeight * this.rows.length,
56
+ width: timescale.width,
57
+ groups: timescale.groups,
58
+ ticks: timescale.ticks,
59
+ };
60
+ }
61
+
62
+ formatGanttRows(): GanttRowFormatted[] {
63
+ return this.rows.map((row) => {
64
+ return this.formatGanttRow(row);
65
+ }) as GanttRowFormatted[];
66
+ }
67
+
68
+ formatGanttRow(row: GanttRow): GanttRowFormatted {
69
+ const itemsFormatted = row.items.map((item) => {
70
+ return this.formatGanttItem(item);
71
+ });
72
+
73
+ return {
74
+ id: row.id,
75
+ name: row.name,
76
+ meta: row.meta,
77
+ items: itemsFormatted,
78
+ };
79
+ }
80
+
81
+ formatGanttItem(item: GanttItem): GanttItemFormatted {
82
+ const start = DateTime.fromISO(item.start);
83
+ const end = DateTime.fromISO(item.end);
84
+ const milliseconds = end.diff(start, "milliseconds").milliseconds;
85
+
86
+ return {
87
+ id: item.id,
88
+ name: item.name,
89
+ meta: item.meta,
90
+ color: item.color,
91
+ start,
92
+ end,
93
+ milliseconds,
94
+ x: 0,
95
+ y: 0,
96
+ width: 0,
97
+ height: 0,
98
+ } as GanttItemFormatted;
99
+ }
100
+
101
+ getMinMax(rowsFormatted: GanttRowFormatted[]) {
102
+ const flatItems = rowsFormatted.flatMap((row) => row.items);
103
+ const flatDateTimes = flatItems.flatMap((item) => [item.start, item.end]);
104
+
105
+ const min = minBy(flatDateTimes, (dateTime) => dateTime.toMillis())?.toMillis() || 0;
106
+ const max = Math.max(...flatDateTimes.map((dateTime) => dateTime.toMillis()));
107
+
108
+ const minDateTime = DateTime.fromMillis(min);
109
+ const maxDateTime = DateTime.fromMillis(max);
110
+
111
+ return { min: minDateTime, max: maxDateTime };
112
+ }
113
+ }
@@ -0,0 +1,243 @@
1
+ import { DateTime, DateTimeUnit } from "luxon";
2
+ import { Group, Scale, Tick } from "./types";
3
+
4
+ export class Timescale {
5
+
6
+ private minWidth: number;
7
+ private width: number;
8
+ private min: DateTime;
9
+ private max: DateTime;
10
+ private scale!: Scale;
11
+ private millisecondToPixel!: number;
12
+
13
+ constructor(minWidth: number, min: DateTime, max: DateTime) {
14
+ this.minWidth = minWidth;
15
+ this.width = minWidth;
16
+ this.min = min;
17
+ this.max = max;
18
+ }
19
+
20
+ public handle() {
21
+
22
+
23
+ // Scale
24
+
25
+ this.scale = this.getScale();
26
+
27
+ // Round min and max
28
+
29
+ this.min = this.min.startOf(this.scale.tick.step);
30
+ this.max = this.max.endOf(this.scale.tick.step);
31
+
32
+ // Number of ticks
33
+
34
+ const numberOfTicks = Math.ceil(this.max.diff(this.min, this.scale.tick.step).as(this.scale.tick.step));
35
+
36
+ // Find actual width
37
+ // If the width is less than the min width, use the min width to stretch the chart
38
+
39
+ const minWidth = numberOfTicks * this.scale.tick.size;
40
+
41
+ if (minWidth > this.minWidth) {
42
+ this.width = minWidth;
43
+ }
44
+
45
+ // millisecondToPixel coefficient
46
+
47
+ this.millisecondToPixel = this.getMillisecondToPixel();
48
+
49
+ // Groups
50
+
51
+ const groups = this.getGroups();
52
+
53
+ // Ticks
54
+
55
+ const ticks = this.getTicks();
56
+
57
+ // Return
58
+
59
+ return {
60
+ groups,
61
+ ticks,
62
+ width: this.width,
63
+ millisecondToPixel: this.millisecondToPixel,
64
+ min: this.min,
65
+ max: this.max,
66
+ }
67
+ }
68
+
69
+
70
+ private getMillisecondToPixel(): number {
71
+ const duration = this.getDiffFromMin(this.max);
72
+
73
+ // Millisecond to pixel coefficient
74
+
75
+ const millisecondToPixel = this.width / duration;
76
+
77
+ return millisecondToPixel;
78
+ }
79
+
80
+ private getGroups(): Group[] {
81
+ let groups = [] as Group[];
82
+
83
+ let current = this.min.startOf(this.scale.group.step);
84
+ const max = this.max.endOf(this.scale.group.step);
85
+
86
+ while (current <= max) {
87
+
88
+ const x = this.getDiffFromMin(current) * this.millisecondToPixel;
89
+
90
+ const millisecondsWidth = this.diffInMilliseconds(current.endOf(this.scale.group.step), current.startOf(this.scale.group.step));
91
+ const width = millisecondsWidth * this.millisecondToPixel;
92
+
93
+ const label = current.toFormat(this.scale.group.format);
94
+
95
+ const labelX = x + (width / 2);
96
+ const labelTextAnchor = "middle";
97
+
98
+ const group = {
99
+ date: current,
100
+ x,
101
+ width,
102
+ label,
103
+ labelX,
104
+ labelTextAnchor,
105
+ } as Group;
106
+
107
+ groups.push(group);
108
+
109
+ current = current.plus({ [this.scale.group.step]: 1 });
110
+ }
111
+
112
+ groups = this.mutateGroupsToKeepOneVisible(groups);
113
+
114
+ return groups;
115
+ }
116
+
117
+ private getTicks(): Tick[] {
118
+ const ticks = [] as Tick[];
119
+
120
+ let current = this.min.startOf(this.scale.group.step).startOf(this.scale.tick.step);
121
+ const max = this.max.endOf(this.scale.group.step).endOf(this.scale.tick.step);
122
+
123
+ while (current < max) {
124
+
125
+ const x = this.getDiffFromMin(current) * this.millisecondToPixel;
126
+
127
+ const millisecondsWidth = this.diffInMilliseconds(current.endOf(this.scale.tick.step), current.startOf(this.scale.tick.step));
128
+ const width = millisecondsWidth * this.millisecondToPixel;
129
+
130
+ const label = current.toFormat(this.scale.tick.format);
131
+
132
+ const tick = {
133
+ date: current,
134
+ x,
135
+ width,
136
+ label,
137
+ align: this.scale.tick.align,
138
+ } as Tick;
139
+
140
+ ticks.push(tick);
141
+
142
+ current = current.plus({ [this.scale.tick.step]: 1 });
143
+ }
144
+
145
+ return ticks;
146
+ }
147
+
148
+
149
+ private getDiffFromMin(date: DateTime): number {
150
+ return this.diffInMilliseconds(date, this.min);
151
+ }
152
+
153
+ private diffInMilliseconds(date1: DateTime, date2: DateTime): number {
154
+ return date1.diff(date2, "milliseconds").milliseconds;
155
+ }
156
+
157
+ private mutateGroupsToKeepOneVisible(groups: Group[]): Group[] {
158
+
159
+ const groupLabelWidth = 100;
160
+
161
+ const lastGroup = groups[groups.length - 1] ?? null;
162
+
163
+ if (
164
+ lastGroup &&
165
+ lastGroup.labelX + groupLabelWidth > this.width && // Is not visible
166
+ lastGroup.x + groupLabelWidth < this.width // Completely fits
167
+ ) {
168
+ lastGroup.labelX = this.width - 5;
169
+ lastGroup.labelTextAnchor = "end";
170
+ }
171
+
172
+ return groups;
173
+ }
174
+
175
+ private getScale(): Scale {
176
+
177
+ const scaleMonth = {
178
+ tick: {
179
+ format: "LLLL",
180
+ step: 'month' as DateTimeUnit,
181
+ size: 100,
182
+ align: "middle",
183
+ },
184
+ group: {
185
+ format: "yyyy",
186
+ step: "year" as DateTimeUnit,
187
+ }
188
+ }
189
+
190
+ const scaleWeek = {
191
+ tick: {
192
+ format: "dd",
193
+ step: 'week' as DateTimeUnit,
194
+ size: 60,
195
+ align: "start",
196
+ },
197
+ group: {
198
+ format: "LLLL yyyy",
199
+ step: "month" as DateTimeUnit,
200
+ }
201
+ }
202
+
203
+ const scaleDay = {
204
+ tick: {
205
+ format: "dd",
206
+ step: 'day' as DateTimeUnit,
207
+ size: 20,
208
+ align: "middle",
209
+ },
210
+ group: {
211
+ format: "LLLL yyyy",
212
+ step: "month" as DateTimeUnit,
213
+ }
214
+ }
215
+
216
+ const scaleHour = {
217
+ tick: {
218
+ format: "HH:mm",
219
+ step: 'hour' as DateTimeUnit,
220
+ size: 40,
221
+ align: "middle",
222
+ },
223
+ group: {
224
+ format: "yyyy-MM-dd",
225
+ step: "day" as DateTimeUnit,
226
+ }
227
+ }
228
+
229
+ if (this.max.diff(this.min, "months").months > 5) {
230
+ return scaleMonth;
231
+ }
232
+
233
+ if (this.max.diff(this.min, "months").months > 3) {
234
+ return scaleWeek;
235
+ }
236
+
237
+ if (this.max.diff(this.min, "days").days > 3) {
238
+ return scaleDay;
239
+ }
240
+
241
+ return scaleHour;
242
+ }
243
+ }