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.
- package/dist/sprintify-ui.es.js +4 -4
- package/dist/types/src/components/BaseActionItem.vue.d.ts +27 -20
- package/dist/types/src/components/BaseActionItemButton.vue.d.ts +21 -28
- package/dist/types/src/components/BaseAddressForm.vue.d.ts +63 -50
- package/dist/types/src/components/BaseBadge.vue.d.ts +41 -40
- package/dist/types/src/components/BaseBoolean.vue.d.ts +9 -14
- package/dist/types/src/components/BaseCropper.vue.d.ts +42 -25
- package/dist/types/src/components/BaseCropperModal.vue.d.ts +18 -17
- package/dist/types/src/components/BaseDataIteratorSectionBox.vue.d.ts +11 -14
- package/dist/types/src/components/BaseDataIteratorSectionButton.vue.d.ts +12 -15
- package/dist/types/src/components/BaseDataIteratorSectionModal.vue.d.ts +20 -17
- package/dist/types/src/components/BaseDataTableRowAction.vue.d.ts +18 -15
- package/dist/types/src/components/BaseDatePicker.vue.d.ts +107 -74
- package/dist/types/src/components/BaseDraggable.vue.d.ts +35 -20
- package/dist/types/src/components/BaseFilePicker.vue.d.ts +43 -42
- package/dist/types/src/components/BaseFilePickerCrop.vue.d.ts +43 -40
- package/dist/types/src/components/BaseFileUploader.vue.d.ts +83 -62
- package/dist/types/src/components/BaseGantt.vue.d.ts +424 -0
- package/dist/types/src/components/BaseHeader.vue.d.ts +81 -66
- package/dist/types/src/components/BaseIconPicker.vue.d.ts +27 -34
- package/dist/types/src/components/BaseLayoutNotificationItemContent.vue.d.ts +18 -15
- package/dist/types/src/components/BaseSideNavigation.vue.d.ts +11 -26
- package/dist/types/src/components/BaseSideNavigationItem.vue.d.ts +27 -32
- package/dist/types/src/components/BaseTabItem.vue.d.ts +27 -32
- package/dist/types/src/components/BaseTabs.vue.d.ts +11 -26
- package/dist/types/src/services/gantt/format.d.ts +24 -0
- package/dist/types/src/services/gantt/timescale.d.ts +26 -0
- package/dist/types/src/services/gantt/types.d.ts +67 -0
- package/package.json +1 -1
- package/src/components/BaseGantt.stories.js +130 -0
- package/src/components/BaseGantt.vue +333 -0
- package/src/components/BaseTextareaAutoresize.vue +2 -2
- package/src/services/gantt/format.ts +113 -0
- package/src/services/gantt/timescale.ts +243 -0
- 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>
|
|
@@ -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
|
+
}
|