@teipublisher/pb-components 1.32.2 → 1.34.1
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/CHANGELOG.md +41 -0
- package/dist/demo/demos.json +4 -1
- package/dist/demo/pb-timeline.html +122 -0
- package/dist/demo/pb-timeline2.html +94 -0
- package/dist/demo/timeline-dev-data.json +3 -0
- package/dist/pb-components-bundle.js +508 -252
- package/dist/pb-elements.json +287 -0
- package/i18n/common/de.json +4 -0
- package/i18n/common/en.json +4 -0
- package/package.json +1 -1
- package/pb-elements.json +287 -0
- package/src/parse-date-service.js +266 -0
- package/src/pb-browse-docs.js +25 -8
- package/src/pb-components.js +1 -0
- package/src/pb-load.js +17 -6
- package/src/pb-timeline.js +741 -0
- package/src/pb-view.js +59 -10
- package/src/search-result-service.js +521 -0
package/src/pb-view.js
CHANGED
|
@@ -161,6 +161,14 @@ export class PbView extends pbMixin(LitElement) {
|
|
|
161
161
|
url: {
|
|
162
162
|
type: String
|
|
163
163
|
},
|
|
164
|
+
/**
|
|
165
|
+
* If set, rewrite URLs to load pages as static HTML files,
|
|
166
|
+
* so no TEI Publisher instance is required. Use this in combination with
|
|
167
|
+
* [tei-publisher-static](https://github.com/eeditiones/tei-publisher-static).
|
|
168
|
+
*/
|
|
169
|
+
static: {
|
|
170
|
+
type: Boolean
|
|
171
|
+
},
|
|
164
172
|
/**
|
|
165
173
|
* The server returns footnotes separately. Set this property
|
|
166
174
|
* if you wish to append them to the main text.
|
|
@@ -328,6 +336,7 @@ export class PbView extends pbMixin(LitElement) {
|
|
|
328
336
|
this._selector = new Map();
|
|
329
337
|
this._chunks = [];
|
|
330
338
|
this._scrollTarget = null;
|
|
339
|
+
this.static = false;
|
|
331
340
|
}
|
|
332
341
|
|
|
333
342
|
attributeChangedCallback(name, oldVal, newVal) {
|
|
@@ -584,20 +593,56 @@ export class PbView extends pbMixin(LitElement) {
|
|
|
584
593
|
|
|
585
594
|
const loadContent = this.shadowRoot.getElementById('loadContent');
|
|
586
595
|
|
|
587
|
-
if (
|
|
596
|
+
if (this.static) {
|
|
597
|
+
this._staticUrl(params).then((url) => {
|
|
598
|
+
loadContent.url = url;
|
|
599
|
+
loadContent.generateRequest();
|
|
600
|
+
});
|
|
601
|
+
} else {
|
|
602
|
+
if (!this.url) {
|
|
603
|
+
if (this.minApiVersion('1.0.0')) {
|
|
604
|
+
this.url = "api/parts";
|
|
605
|
+
} else {
|
|
606
|
+
this.url = "modules/lib/components.xql";
|
|
607
|
+
}
|
|
608
|
+
}
|
|
588
609
|
if (this.minApiVersion('1.0.0')) {
|
|
589
|
-
|
|
610
|
+
loadContent.url = `${this.getEndpoint()}/${this.url}/${encodeURIComponent(this.getDocument().path)}/json`;
|
|
590
611
|
} else {
|
|
591
|
-
|
|
612
|
+
loadContent.url = `${this.getEndpoint()}/${this.url}`;
|
|
592
613
|
}
|
|
614
|
+
loadContent.params = params;
|
|
615
|
+
loadContent.generateRequest();
|
|
593
616
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Use a static URL to load pre-generated content.
|
|
621
|
+
*/
|
|
622
|
+
async _staticUrl(params) {
|
|
623
|
+
function createKey(paramNames) {
|
|
624
|
+
const urlComponents = [];
|
|
625
|
+
paramNames.sort().forEach(key => {
|
|
626
|
+
if (params[key]) {
|
|
627
|
+
urlComponents.push(`${key}=${params[key]}`);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
return urlComponents.join('&');
|
|
598
631
|
}
|
|
599
|
-
|
|
600
|
-
|
|
632
|
+
|
|
633
|
+
const index = await fetch(`index.json`)
|
|
634
|
+
.then((response) => response.json());
|
|
635
|
+
const paramNames = ['odd', 'view', 'xpath'];
|
|
636
|
+
this.querySelectorAll('pb-param').forEach((param) => paramNames.push(`user.${param.getAttribute('name')}`));
|
|
637
|
+
let url = createKey([...paramNames, 'root']);
|
|
638
|
+
let file = index[url];
|
|
639
|
+
if (!file) {
|
|
640
|
+
url = createKey(paramNames);
|
|
641
|
+
file = index[url];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
console.log('<pb-view> Static lookup %s: %s', url, file);
|
|
645
|
+
return `${file}`;
|
|
601
646
|
}
|
|
602
647
|
|
|
603
648
|
_clear() {
|
|
@@ -838,7 +883,11 @@ export class PbView extends pbMixin(LitElement) {
|
|
|
838
883
|
let link = document.createElement('link');
|
|
839
884
|
link.setAttribute('rel', 'stylesheet');
|
|
840
885
|
link.setAttribute('type', 'text/css');
|
|
841
|
-
|
|
886
|
+
if (this.static) {
|
|
887
|
+
link.setAttribute('href', `/css/${this.getOdd()}.css`);
|
|
888
|
+
} else {
|
|
889
|
+
link.setAttribute('href', `${this.getEndpoint()}/transform/${this.getOdd()}.css`);
|
|
890
|
+
}
|
|
842
891
|
links.push(link);
|
|
843
892
|
|
|
844
893
|
if (this.loadCss) {
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { get as i18n } from "./pb-i18n.js";
|
|
2
|
+
|
|
3
|
+
export class SearchResultService {
|
|
4
|
+
/*
|
|
5
|
+
* SEARCH RESULT OBJECT
|
|
6
|
+
* Service that loads initial data from a datasource,
|
|
7
|
+
* can be a database or an API, and converts it in
|
|
8
|
+
* a format that can be used by the pb-timeline component.
|
|
9
|
+
*
|
|
10
|
+
* public methods:
|
|
11
|
+
* getMinDateStr()
|
|
12
|
+
* getMaxDateStr()
|
|
13
|
+
* getMinDate()
|
|
14
|
+
* getMaxDate()
|
|
15
|
+
* export()
|
|
16
|
+
* getIntervalSizes()
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* CONSRTUCTOR INPUTS EXPLAINED
|
|
21
|
+
* jsonData: data to load, object with
|
|
22
|
+
* keys => valid datestrings formatted YYYY-MM-DD
|
|
23
|
+
* values => number of results for this day
|
|
24
|
+
* maxInterval: max amount of bins allowed
|
|
25
|
+
* scopes: array of all 6 possible values for scope
|
|
26
|
+
*/
|
|
27
|
+
constructor(jsonData = {}, maxInterval = 60, scopes = ["D", "W", "M", "Y", "5Y", "10Y"]) {
|
|
28
|
+
this.data = { invalid: {}, valid: {} };
|
|
29
|
+
this.maxInterval = maxInterval;
|
|
30
|
+
this.scopes = scopes;
|
|
31
|
+
this._validateJsonData(jsonData);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* based on the loaded jsonData, compute
|
|
36
|
+
* - min date as dateStr or utc-date-object
|
|
37
|
+
* - max date as dateStr or utc-date-object
|
|
38
|
+
*/
|
|
39
|
+
getMinDateStr() {
|
|
40
|
+
return Object.keys(this.data.valid).sort()[0];
|
|
41
|
+
}
|
|
42
|
+
getMaxDateStr() {
|
|
43
|
+
let days = Object.keys(this.data.valid);
|
|
44
|
+
return days.sort()[days.length - 1];
|
|
45
|
+
}
|
|
46
|
+
getMinDate() {
|
|
47
|
+
return this._dateStrToUTCDate(this.getMinDateStr());
|
|
48
|
+
}
|
|
49
|
+
getMaxDate() {
|
|
50
|
+
return this._dateStrToUTCDate(this.getMaxDateStr());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getEndOfRangeDate(scope, date) {
|
|
54
|
+
return this._UTCDateToDateStr(this._increaseDateBy(scope, date));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* exports data for each scope
|
|
59
|
+
* when no argument is provided, the optimal scope based
|
|
60
|
+
* on the maxInterval (default 60) will be assigned
|
|
61
|
+
*/
|
|
62
|
+
export(scope) {
|
|
63
|
+
// auto assign scope when no argument provided
|
|
64
|
+
scope = scope || this._autoAssignScope();
|
|
65
|
+
// validate scope
|
|
66
|
+
if (!this.scopes.includes(scope)) {
|
|
67
|
+
throw new Error(`invalid scope provided, expected: ["10Y", "5Y", "Y", "M", "W", "D"]. Got: "${scope}"`);
|
|
68
|
+
}
|
|
69
|
+
// initialize object to export
|
|
70
|
+
const exportData = {
|
|
71
|
+
data: [],
|
|
72
|
+
scope: scope,
|
|
73
|
+
binTitleRotated: this._binTitleRotatedLookup(scope)
|
|
74
|
+
}
|
|
75
|
+
if (Object.keys(this.data.valid).length === 0) {
|
|
76
|
+
return exportData;
|
|
77
|
+
}
|
|
78
|
+
// get start and end date
|
|
79
|
+
const startCategory = this._classify(this.getMinDateStr(), scope);
|
|
80
|
+
const startDateStr = this._getFirstDay(startCategory);
|
|
81
|
+
let currentDate = this._dateStrToUTCDate(startDateStr);
|
|
82
|
+
const endDate = this.getMaxDate();
|
|
83
|
+
// iterate until end of intervall reached, add binObject for each step
|
|
84
|
+
while (currentDate <= endDate) {
|
|
85
|
+
const currentDateStr = this._UTCDateToDateStr(currentDate);
|
|
86
|
+
const currentCategory = this._classify(currentDateStr, scope);
|
|
87
|
+
exportData.data.push(this._buildBinObject(currentDateStr, currentCategory, scope));
|
|
88
|
+
currentDate = this._increaseDateBy(scope, currentDate);
|
|
89
|
+
}
|
|
90
|
+
// count all values
|
|
91
|
+
Object.keys(this.data.valid).sort().forEach(dateStr => {
|
|
92
|
+
const currentCategory = this._classify(dateStr, scope);
|
|
93
|
+
const targetBinObject = exportData.data.find(it => it.category === currentCategory);
|
|
94
|
+
try {
|
|
95
|
+
const value = this.data.valid[dateStr];
|
|
96
|
+
if (typeof value === 'object') {
|
|
97
|
+
targetBinObject.value += value.count || 0;
|
|
98
|
+
if (value.info) {
|
|
99
|
+
targetBinObject.info = targetBinObject.info.concat(value.info);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
targetBinObject.value += this.data.valid[dateStr] || 0;
|
|
103
|
+
}
|
|
104
|
+
} catch(e) {
|
|
105
|
+
console.log(e);
|
|
106
|
+
console.log("currentCategory");
|
|
107
|
+
console.log(currentCategory);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (this.data.invalid) {
|
|
111
|
+
let invalid = 0;
|
|
112
|
+
let info = [];
|
|
113
|
+
Object.values(this.data.invalid).forEach((value) => {
|
|
114
|
+
if (typeof value === 'object') {
|
|
115
|
+
invalid += value.count || 0;
|
|
116
|
+
info = info.concat(value.info);
|
|
117
|
+
} else {
|
|
118
|
+
invalid += value;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
if (invalid > 0) {
|
|
122
|
+
exportData.data.push({
|
|
123
|
+
tooltip: i18n('timeline.unknown'),
|
|
124
|
+
title: i18n('timeline.unknown'),
|
|
125
|
+
// binTitle: i18n('timeline.unknown'),
|
|
126
|
+
category: '?',
|
|
127
|
+
separator: true,
|
|
128
|
+
value: invalid,
|
|
129
|
+
info
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return exportData;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/*
|
|
137
|
+
* returns optimal scope based on the maxInterval
|
|
138
|
+
* by computing the scope that meets the criteria
|
|
139
|
+
* nbr of bins <= maxInterval
|
|
140
|
+
*/
|
|
141
|
+
_autoAssignScope() {
|
|
142
|
+
for (let i = 0; i < this.scopes.length; i++) {
|
|
143
|
+
if (this._computeIntervalSize(this.scopes[i]) <= this.maxInterval) {
|
|
144
|
+
return this.scopes[i];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Interval too big! Computed: ${this._computeIntervalSize(this.scopes[i])}. Allowed: ${this.maxInterval}. Try to increase maxInterval.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
* splits input data in 2 sections
|
|
152
|
+
* => valid data
|
|
153
|
+
* => invalid (if not a vaid date, for example 2012-00-00 is invalid)
|
|
154
|
+
*/
|
|
155
|
+
_validateJsonData(jsonData) {
|
|
156
|
+
Object.keys(jsonData).sort().forEach(key => {
|
|
157
|
+
if (this._isValidDateStr(key)) {
|
|
158
|
+
this.data.valid[key] = jsonData[key];
|
|
159
|
+
} else {
|
|
160
|
+
this.data.invalid[key] = jsonData[key];
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
* lookup table which bin titles should be rotated
|
|
167
|
+
*/
|
|
168
|
+
_binTitleRotatedLookup(scope) {
|
|
169
|
+
const lookup = {
|
|
170
|
+
"10Y": true,
|
|
171
|
+
"5Y": true,
|
|
172
|
+
"Y": true,
|
|
173
|
+
"M": false, // only exception not to rotate in monthly scope
|
|
174
|
+
"W": true,
|
|
175
|
+
"D": true,
|
|
176
|
+
}
|
|
177
|
+
return lookup[scope];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
* Helper method that builds a binObject that
|
|
182
|
+
* can be read by the pb-timeline component
|
|
183
|
+
*/
|
|
184
|
+
_buildBinObject(dateStr, category, scope) {
|
|
185
|
+
const split = dateStr.split("-");
|
|
186
|
+
const yearStr = split[0];
|
|
187
|
+
const monthStr = split[1];
|
|
188
|
+
const dayStr = split[2];
|
|
189
|
+
// for all scopes this remains the same
|
|
190
|
+
const binObject = {
|
|
191
|
+
dateStr: dateStr,
|
|
192
|
+
category: category,
|
|
193
|
+
value: 0,
|
|
194
|
+
info: []
|
|
195
|
+
}
|
|
196
|
+
// scope specific bin data
|
|
197
|
+
if (scope === "10Y") {
|
|
198
|
+
binObject.tooltip = `${category} - ${Number(category) + 9}`; // 1900 - 1999
|
|
199
|
+
binObject.selectionStart = `${category}`;
|
|
200
|
+
binObject.selectionEnd = `${Number(category) + 9}`;
|
|
201
|
+
// seperator every 100 years (10 bins)
|
|
202
|
+
if (Number(category) % 100 === 0) {
|
|
203
|
+
binObject.title = `${category} - ${Number(category) + 99}`;
|
|
204
|
+
binObject.binTitle = category;
|
|
205
|
+
binObject.seperator = true;
|
|
206
|
+
};
|
|
207
|
+
} else if (scope === "5Y") {
|
|
208
|
+
binObject.tooltip = `${category} - ${Number(category) + 4}`; // 1995 - 1999
|
|
209
|
+
binObject.selectionStart = `${category}`;
|
|
210
|
+
binObject.selectionEnd = `${Number(category) + 4}`;
|
|
211
|
+
// seperator every 50 years (10 bins)
|
|
212
|
+
if (Number(category) % 50 === 0) {
|
|
213
|
+
binObject.title = `${category} - ${Number(category) + 49}`;
|
|
214
|
+
binObject.binTitle = category;
|
|
215
|
+
binObject.seperator = true;
|
|
216
|
+
}
|
|
217
|
+
} else if (scope === "Y") {
|
|
218
|
+
binObject.tooltip = category;
|
|
219
|
+
binObject.selectionStart = category;
|
|
220
|
+
binObject.selectionEnd = category;
|
|
221
|
+
// seperator every 10 years (10 bins)
|
|
222
|
+
if (Number(category) % 10 === 0) {
|
|
223
|
+
binObject.title = `${category} - ${Number(category) + 9}`;
|
|
224
|
+
binObject.binTitle = `${category}`;
|
|
225
|
+
binObject.seperator = true;
|
|
226
|
+
}
|
|
227
|
+
} else if (scope === "M") {
|
|
228
|
+
const monthNum = Number(monthStr);
|
|
229
|
+
const month = this._monthLookup(monthNum); // Jan,Feb,Mar,...,Nov,Dez
|
|
230
|
+
binObject.binTitle = month[0]; // J,F,M,A,M,J,J,..N,D
|
|
231
|
+
binObject.tooltip = `${month} ${yearStr}`; // May 1996
|
|
232
|
+
binObject.selectionStart = `${month} ${yearStr}`;
|
|
233
|
+
binObject.selectionEnd = `${month} ${yearStr}`;
|
|
234
|
+
// every first of the month
|
|
235
|
+
if (monthNum === 1) {
|
|
236
|
+
binObject.title = yearStr; // YYYY
|
|
237
|
+
binObject.seperator = true;
|
|
238
|
+
}
|
|
239
|
+
} else if (scope === "W") {
|
|
240
|
+
const week = category.split("-")[1];; // => W52
|
|
241
|
+
binObject.tooltip = `${yearStr} ${week}`; // 1996 W52
|
|
242
|
+
binObject.selectionStart = `${yearStr} ${week}`; // 1996 W52
|
|
243
|
+
binObject.selectionEnd = `${yearStr} ${week}`; // 1996 W52
|
|
244
|
+
let currentDate = this._dateStrToUTCDate(dateStr);
|
|
245
|
+
let lastWeek = this._addDays(currentDate, -7);
|
|
246
|
+
// title and binTitle every first monday of the month
|
|
247
|
+
if (currentDate.getUTCMonth() !== lastWeek.getUTCMonth()) {
|
|
248
|
+
binObject.binTitle = week;
|
|
249
|
+
binObject.title = this._monthLookup(currentDate.getUTCMonth() + 1);
|
|
250
|
+
}
|
|
251
|
+
// seperator every start of the year
|
|
252
|
+
binObject.seperator = week === "W1";
|
|
253
|
+
} else if (scope === "D") {
|
|
254
|
+
binObject.tooltip = dateStr;
|
|
255
|
+
binObject.selectionStart = dateStr;
|
|
256
|
+
binObject.selectionEnd = dateStr;
|
|
257
|
+
// every monday
|
|
258
|
+
if (this._dateStrToUTCDate(dateStr).getUTCDay() === 1) {
|
|
259
|
+
binObject.binTitle = `${Number(dayStr)}.${Number(monthStr)}`;
|
|
260
|
+
binObject.title = `${this._classify(dateStr, "W").replace("-", " ")}`;
|
|
261
|
+
binObject.seperator = true;
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
throw new Error(`invalid scope provided, expected: ["10Y", "5Y", "Y", "M", "W", "D"]. Got: "${scope}"`);
|
|
265
|
+
}
|
|
266
|
+
return binObject;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* ...classifies dateStr into category (based on scope)
|
|
271
|
+
* EXAMPLES:
|
|
272
|
+
* _classify("2016-01-12", "10Y") // => "2010"
|
|
273
|
+
* _classify("2016-01-12", "5Y") // => "2015"
|
|
274
|
+
* _classify("2016-01-12", "Y") // => "2016"
|
|
275
|
+
* _classify("2016-01-12", "M") // => "2010-01"
|
|
276
|
+
* _classify("2016-01-12", "W") // => "2016-W2"
|
|
277
|
+
* _classify("2016-01-12", "D") // => "2016-01-12"
|
|
278
|
+
*/
|
|
279
|
+
_classify(dateStr, scope) { // returns category (as string)
|
|
280
|
+
if (!dateStr.match(/^\d{4}-\d{2}-\d{2}$/)) { // quick validate dateStr
|
|
281
|
+
throw new Error(`invalid dateStr format, expected "YYYY-MM-DD", got: "${dateStr}".`);
|
|
282
|
+
}
|
|
283
|
+
if (!dateStr || !scope) { // both inputs provided
|
|
284
|
+
throw new Error(`both inputs must be provided. Got dateStr=${dateStr}, scope=${scope}`);
|
|
285
|
+
}
|
|
286
|
+
switch (scope) {
|
|
287
|
+
case "10Y": case "5Y":
|
|
288
|
+
const intervalSize = Number(scope.replace("Y", ""));
|
|
289
|
+
const startYear = Math.floor(Number(dateStr.split("-")[0]) / intervalSize) * intervalSize;
|
|
290
|
+
return startYear.toString();
|
|
291
|
+
case "Y":
|
|
292
|
+
return dateStr.substr(0, 4);
|
|
293
|
+
case "M":
|
|
294
|
+
return dateStr.substr(0, 7);
|
|
295
|
+
case "W":
|
|
296
|
+
const UTCDate = this._dateStrToUTCDate(dateStr);
|
|
297
|
+
return this._UTCDateToWeekFormat(UTCDate);
|
|
298
|
+
case "D":
|
|
299
|
+
return dateStr;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/*
|
|
304
|
+
* ...gets first day as UTC Date, based on the category
|
|
305
|
+
* EXAMPLES:
|
|
306
|
+
* _getFirstDay("2010") // => 2010-01-01
|
|
307
|
+
* _getFirstDay("2010-12") // => 2010-12-01
|
|
308
|
+
* _getFirstDay("2010-W10") // => 2010-03-08
|
|
309
|
+
*/
|
|
310
|
+
_getFirstDay(categoryStr) {
|
|
311
|
+
if (categoryStr.match(/^\d{4}-\d{2}-\d{2}$/)) { // YYYY-MM-DD => return same value
|
|
312
|
+
return categoryStr;
|
|
313
|
+
}
|
|
314
|
+
if (categoryStr.match(/^\d{4}-\d{2}$/)) { // YYYY-MM
|
|
315
|
+
return `${categoryStr}-01`; // add -01
|
|
316
|
+
}
|
|
317
|
+
if (categoryStr.match(/^\d{4}$/)) { // YYYY
|
|
318
|
+
return `${categoryStr}-01-01`; // add -01-01
|
|
319
|
+
}
|
|
320
|
+
if (categoryStr.match(/^\d{4}-W([1-9]|[1-4][0-9]|5[0-3])$/)) { // YYYY-W? // ? => [1-53]
|
|
321
|
+
// |YYYY-W |1-9 | 10-49 | 50-53 |
|
|
322
|
+
const split = categoryStr.split("-");
|
|
323
|
+
const year = Number(split[0]);
|
|
324
|
+
const weekNumber = Number(split[1].replace("W", ""));
|
|
325
|
+
return this._getDateStrOfISOWeek(year, weekNumber);
|
|
326
|
+
}
|
|
327
|
+
throw new Error("invalid categoryStr");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/*
|
|
331
|
+
* converts dateStr (YYYY-MM-DD) to a date object in UTC time
|
|
332
|
+
*/
|
|
333
|
+
_dateStrToUTCDate(dateStr) {
|
|
334
|
+
if (!this._isValidDateStr(dateStr)) {
|
|
335
|
+
throw new Error(`invalid dateStr, expected "YYYY-MM-DD" with month[1-12] and day[1-31], got: "${dateStr}".`);
|
|
336
|
+
}
|
|
337
|
+
const split = dateStr.split("-");
|
|
338
|
+
const year = Number(split[0]);
|
|
339
|
+
const month = Number(split[1]);
|
|
340
|
+
const day = Number(split[2]);
|
|
341
|
+
return new Date(Date.UTC(year, month - 1, day));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/*
|
|
345
|
+
* converts a UTC date object to a dateStr (YYYY-MM-DD)
|
|
346
|
+
*/
|
|
347
|
+
_UTCDateToDateStr(UTCDate) {
|
|
348
|
+
return UTCDate.toISOString().split("T")[0];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/*
|
|
352
|
+
* example:
|
|
353
|
+
* 1 Jan 2020 => 2020-W1
|
|
354
|
+
*/
|
|
355
|
+
_UTCDateToWeekFormat(UTCDate) {
|
|
356
|
+
const year = this._getISOWeekYear(UTCDate);
|
|
357
|
+
const weekNbr = this._getISOWeek(UTCDate);
|
|
358
|
+
return `${year}-W${weekNbr}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/*
|
|
362
|
+
* returns the ISO week (_getISOWeek) or year (_getISOWeekYear)
|
|
363
|
+
* as number based on a UTC date.
|
|
364
|
+
*/
|
|
365
|
+
_getISOWeek(UTCDate) { // https://weeknumber.net/how-to/javascript
|
|
366
|
+
let date = new Date(UTCDate.getTime());
|
|
367
|
+
date.setHours(0, 0, 0, 0);
|
|
368
|
+
// Thursday in current week decides the year.
|
|
369
|
+
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
|
|
370
|
+
// January 4 is always in week 1.
|
|
371
|
+
let week1 = new Date(date.getFullYear(), 0, 4);
|
|
372
|
+
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
|
|
373
|
+
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
|
|
374
|
+
- 3 + (week1.getDay() + 6) % 7) / 7);
|
|
375
|
+
}
|
|
376
|
+
/*
|
|
377
|
+
* returns the ISO week year as number based on a UTC date
|
|
378
|
+
* this is only needed for rollovers, for example:
|
|
379
|
+
* => 1.jan 2011 is in W52 of year 2010.
|
|
380
|
+
*/
|
|
381
|
+
_getISOWeekYear(UTCDate) { // https://weeknumber.net/how-to/javascript
|
|
382
|
+
var date = new Date(UTCDate.getTime());
|
|
383
|
+
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
|
|
384
|
+
return date.getFullYear();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/*
|
|
388
|
+
* given the year and weeknumber -> return dateStr (YYYY-MM-DD)
|
|
389
|
+
*/
|
|
390
|
+
_getDateStrOfISOWeek(year, weekNumber) { // https://stackoverflow.com/a/16591175/6272061
|
|
391
|
+
let simple = new Date(Date.UTC(year, 0, 1 + (weekNumber - 1) * 7));
|
|
392
|
+
let dow = simple.getUTCDay();
|
|
393
|
+
let ISOweekStart = simple;
|
|
394
|
+
if (dow <= 4)
|
|
395
|
+
ISOweekStart.setDate(simple.getDate() - simple.getUTCDay() + 1);
|
|
396
|
+
else
|
|
397
|
+
ISOweekStart.setDate(simple.getDate() + 8 - simple.getUTCDay());
|
|
398
|
+
return ISOweekStart.toISOString().split("T")[0];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/*
|
|
402
|
+
* compute the interval sizes based on the scope
|
|
403
|
+
* prediction only, not the actuall export of the data
|
|
404
|
+
*/
|
|
405
|
+
getIntervalSizes() {
|
|
406
|
+
return {
|
|
407
|
+
"D": this._computeIntervalSize("D"),
|
|
408
|
+
"W": this._computeIntervalSize("W"),
|
|
409
|
+
"M": this._computeIntervalSize("M"),
|
|
410
|
+
"Y": this._computeIntervalSize("Y"),
|
|
411
|
+
"5Y": this._computeIntervalSize("5Y"),
|
|
412
|
+
"10Y": this._computeIntervalSize("10Y"),
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
_computeIntervalSize(scope) {
|
|
416
|
+
const maxDate = this.getMaxDateStr();
|
|
417
|
+
if (!maxDate) {
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
const endDate = this._dateStrToUTCDate(maxDate);
|
|
421
|
+
const firstDayDateStr = this._getFirstDay(this._classify(this.getMinDateStr(), scope));
|
|
422
|
+
let currentDate = this._dateStrToUTCDate(firstDayDateStr);
|
|
423
|
+
let count = 0;
|
|
424
|
+
while (currentDate <= endDate) {
|
|
425
|
+
count++;
|
|
426
|
+
currentDate = this._increaseDateBy(scope, currentDate);
|
|
427
|
+
}
|
|
428
|
+
return count;
|
|
429
|
+
}
|
|
430
|
+
_increaseDateBy(scope, date) {
|
|
431
|
+
switch (scope) {
|
|
432
|
+
case "D":
|
|
433
|
+
return this._addDays(date, 1);
|
|
434
|
+
case "W":
|
|
435
|
+
return this._addDays(date, 7);
|
|
436
|
+
case "M":
|
|
437
|
+
return this._addMonths(date, 1);
|
|
438
|
+
case "Y":
|
|
439
|
+
return this._addYears(date, 1);
|
|
440
|
+
case "5Y":
|
|
441
|
+
return this._addYears(date, 5);
|
|
442
|
+
case "10Y":
|
|
443
|
+
return this._addYears(date, 10);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/*
|
|
448
|
+
* functions that add n days (_addDays), months (_addMonths)
|
|
449
|
+
* or years (_addYears) to a UTC date object
|
|
450
|
+
* returns the computed new UTC date
|
|
451
|
+
*/
|
|
452
|
+
_addDays(UTCDate, days) {
|
|
453
|
+
let newUTCDate = new Date(UTCDate.valueOf());
|
|
454
|
+
newUTCDate.setUTCDate(newUTCDate.getUTCDate() + days);
|
|
455
|
+
return newUTCDate;
|
|
456
|
+
}
|
|
457
|
+
_addMonths(UTCdate, months) {
|
|
458
|
+
let newUTCDate = new Date(UTCdate.valueOf());
|
|
459
|
+
let d = newUTCDate.getUTCDate();
|
|
460
|
+
newUTCDate.setUTCMonth(newUTCDate.getUTCMonth() + +months);
|
|
461
|
+
if (newUTCDate.getUTCDate() != d) {
|
|
462
|
+
newUTCDate.setUTCDate(0);
|
|
463
|
+
}
|
|
464
|
+
return newUTCDate;
|
|
465
|
+
}
|
|
466
|
+
_addYears(UTCdate, years) {
|
|
467
|
+
let newUTCDate = new Date(UTCdate.valueOf());
|
|
468
|
+
newUTCDate.setUTCFullYear(newUTCDate.getUTCFullYear() + years);
|
|
469
|
+
return newUTCDate;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/*
|
|
473
|
+
* Validates dateStr. rules:
|
|
474
|
+
* => year: 4 digit number
|
|
475
|
+
* => month: [1-12]
|
|
476
|
+
* => day: [1-31]
|
|
477
|
+
*/
|
|
478
|
+
_isValidDateStr(str) {
|
|
479
|
+
if (!str) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let split = str.split("-");
|
|
484
|
+
if (split.length !== 3) return false;
|
|
485
|
+
let year = split[0];
|
|
486
|
+
let month = split[1];
|
|
487
|
+
let day = split[2];
|
|
488
|
+
if (year === "0000" || day === "00" || month === "00") return false;
|
|
489
|
+
if (Number(day) < 1 || Number(day) > 31) return false;
|
|
490
|
+
if (Number(month) < 1 || Number(month) > 12) return false;
|
|
491
|
+
// if all checks are passed => valid datestring!
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/*
|
|
496
|
+
* Converts month number (str or number) to a 3 char
|
|
497
|
+
* abbreviation of the month (in english)
|
|
498
|
+
*/
|
|
499
|
+
_monthLookup(num) {
|
|
500
|
+
if (num > 12 || num < 1) {
|
|
501
|
+
throw new Error(`invalid 'num' provided, expected 1-12. Got: ${num}`);
|
|
502
|
+
}
|
|
503
|
+
const lookup = {
|
|
504
|
+
"1": "Jan",
|
|
505
|
+
"2": "Feb",
|
|
506
|
+
"3": "Mar",
|
|
507
|
+
"4": "Apr",
|
|
508
|
+
"5": "May",
|
|
509
|
+
"6": "Jun",
|
|
510
|
+
"7": "Jul",
|
|
511
|
+
"8": "Aug",
|
|
512
|
+
"9": "Sep",
|
|
513
|
+
"10": "Oct",
|
|
514
|
+
"11": "Nov",
|
|
515
|
+
"12": "Dec",
|
|
516
|
+
}
|
|
517
|
+
return lookup[num.toString()];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
|