@inb/oeb_visualizations 0.1.0 → 0.1.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.
- package/README.md +74 -11
- package/dist/oeb-visualizations.cjs.js +159 -36
- package/dist/oeb-visualizations.esm.js +158 -35
- package/dist/oeb-visualizations.min.js +1 -1
- package/dist/oeb-visualizations.umd.js +159 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,19 +20,82 @@ If you want to contribute to this project, please read the [contributing guideli
|
|
|
20
20
|
|
|
21
21
|
## Development
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
This project is built using [Vue.js](https://vuejs.org/). The components are written in [Vue](https://vuejs.org/) and the bundling is done using [Rollup](https://rollupjs.org/). The rollup configuration is in the `rollup.config.js` file and the main entry point is the `src/index.js` file, all the components are exported from this file.
|
|
24
|
+
The components are written in the `src/components` folder. Each component should be in a separate folder and should contain the `.vue` file and, optionally, a `.scss` file. In order to be used in other applications, the package should be built, which creates a `dist` folder with the compiled files.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
The documentation is built using [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). The content of the documentation is written in markdown and should be added to the `docs` folder.
|
|
27
|
+
The configuration of the documentation is in the `mkdocs.yml` file. This file contains the structure of the documentation, including navigation, the theme used, etc. More information about how to configure the documentation can be found in the [documentation](https://squidfunk.github.io/mkdocs-material/reference/).
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
- How the built is configured.
|
|
29
|
-
- How to push to npm.
|
|
30
|
-
- How to install package locally for development.
|
|
29
|
+
### How to add a new visualization.
|
|
31
30
|
|
|
31
|
+
If you want to add a new visualization, you should create a new component for it. Follow the steps below:
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
- How to serve documentation locally.
|
|
36
|
-
- GitHub Actions in place. When do they run?
|
|
33
|
+
- Create the new component in the `src/components` folder. A component can be written in only one file or, if it is complex, it can be divided into smaller components in a folder.
|
|
34
|
+
- Add the new component to the `index.js`, as following:
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
```js
|
|
37
|
+
import newComponent from './components/newComponent.vue'
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
newComponent,
|
|
41
|
+
...
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> If the new component is a complex component, it should be divided into smaller components. In this case, the `index.js` file should export the smaller components and the main component.
|
|
46
|
+
|
|
47
|
+
> The documentation should be updated to include the new component.
|
|
48
|
+
|
|
49
|
+
### How to document a new component.
|
|
50
|
+
|
|
51
|
+
To document a new component, add the new component to the documentation as a single file in the `docs/components` folder. Documentation is written in markdown and should contain at least the following sections:
|
|
52
|
+
|
|
53
|
+
- Description of how the data being represented.
|
|
54
|
+
- How to use it.
|
|
55
|
+
- Example of usage.
|
|
56
|
+
- Props and events.
|
|
57
|
+
|
|
58
|
+
### How to build the package locally.
|
|
59
|
+
|
|
60
|
+
To build the package locally, run:
|
|
61
|
+
```bash
|
|
62
|
+
npm run build
|
|
63
|
+
```
|
|
64
|
+
This will create a `dist` folder with the compiled files.
|
|
65
|
+
The component will be available for other applications running locally to use.
|
|
66
|
+
To use the component in another application, you should link the package locally. To do so, run:
|
|
67
|
+
```bash
|
|
68
|
+
npm link
|
|
69
|
+
```
|
|
70
|
+
Then, in the application where you want to use the component, run:
|
|
71
|
+
```bash
|
|
72
|
+
npm link @inb/oeb_visualizations
|
|
73
|
+
```
|
|
74
|
+
This will link the local package to the application.
|
|
75
|
+
|
|
76
|
+
### How to serve the documentation locally.
|
|
77
|
+
|
|
78
|
+
The documentation is built using [mkdocs](https://www.mkdocs.org/). To serve the documentation locally, run:
|
|
79
|
+
```bash
|
|
80
|
+
mkdocs serve
|
|
81
|
+
```
|
|
82
|
+
This will start a local server and the documentation will be available at `http://localhost:8000/`.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Publishing
|
|
86
|
+
|
|
87
|
+
### Publishing a new version of the package to npm
|
|
88
|
+
|
|
89
|
+
To publish a new version of the package to [npm](https://www.npmjs.com), follow the steps below:
|
|
90
|
+
|
|
91
|
+
- Update the version in the `package.json` file. If all commit messages follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) standard, you can use the `cz bump --version-scheme semver` command to update the version and the `CHANGELOG.md` file.
|
|
92
|
+
- Run the `npm run build` command to build the package.
|
|
93
|
+
- Run the `npm publish` command to publish the package to npmjs.
|
|
94
|
+
|
|
95
|
+
This package is published under the `@inb` scope as `@inb/oeb_visualizations`.
|
|
96
|
+
The package is available at [https://www.npmjs.com/package/@inb/oeb_visualizations](https://www.npmjs.com/package/@inb/oeb_visualizations).
|
|
97
|
+
|
|
98
|
+
### Publishing the documentation
|
|
99
|
+
|
|
100
|
+
The documentation is built and deployed to the `gh-pages` branch using GitHub Actions. Each time a new commit is pushed to the `main` branch, the documentation is built and deployed to the `gh-pages` branch.
|
|
101
|
+
The documentation is available at [https://inab.github.io/oeb-visualizations/](https://inab.github.io/oeb-visualizations/).
|
|
@@ -13,6 +13,13 @@ var script$1 = {
|
|
|
13
13
|
}
|
|
14
14
|
}),
|
|
15
15
|
props: {
|
|
16
|
+
xrange: {
|
|
17
|
+
/*
|
|
18
|
+
xrange is the range of the x axis in ms.
|
|
19
|
+
*/
|
|
20
|
+
type: Array,
|
|
21
|
+
required: false
|
|
22
|
+
},
|
|
16
23
|
dtick: {
|
|
17
24
|
/*
|
|
18
25
|
dtick is the interval between ticks on the x axis in ms.
|
|
@@ -72,6 +79,14 @@ var script$1 = {
|
|
|
72
79
|
required: false,
|
|
73
80
|
default: 350
|
|
74
81
|
},
|
|
82
|
+
width: {
|
|
83
|
+
/*
|
|
84
|
+
width is the width of the plot in px.
|
|
85
|
+
*/
|
|
86
|
+
type: Number,
|
|
87
|
+
required: false,
|
|
88
|
+
default: 700
|
|
89
|
+
},
|
|
75
90
|
week: {
|
|
76
91
|
/*
|
|
77
92
|
whether the plot is used to show data of one week
|
|
@@ -123,9 +138,12 @@ var script$1 = {
|
|
|
123
138
|
if (value[i].code !== null && typeof value[i].code !== 'number') {
|
|
124
139
|
console.error(`[oeb-visualizations warn] code must be null or a number in dataItems prop item (at position ${i})`);
|
|
125
140
|
}
|
|
126
|
-
// Date must be a string containing a date
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
// Date must be a string containing a parseable date, or a number (timestamp)
|
|
142
|
+
const validDateString = typeof value[i].date === 'string' && !isNaN(Date.parse(value[i].date));
|
|
143
|
+
const validDateNumber = typeof value[i].date === 'number';
|
|
144
|
+
if (!validDateString && !validDateNumber) {
|
|
145
|
+
console.error(`[oeb-visualizations warn] date must be a string containing a date or a number in dataItems prop item (at position ${i})`);
|
|
146
|
+
console.error(`[oeb-visualizations warn] date type is ${typeof value[i].date} and the value is ${value[i].date}`);
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
// And date cannot be null
|
|
@@ -138,7 +156,13 @@ var script$1 = {
|
|
|
138
156
|
}
|
|
139
157
|
},
|
|
140
158
|
mounted() {
|
|
141
|
-
|
|
159
|
+
// Compute the tallest online bar from real measurements only.
|
|
160
|
+
// Math.max over an array containing null coerces null -> 0, so a site
|
|
161
|
+
// that was down the whole window would yield 0 and render the
|
|
162
|
+
// offline/NA bars with zero height (invisible). Ignore nulls/NaN and
|
|
163
|
+
// apply a floor so down-only sites still draw full-height bars.
|
|
164
|
+
const times = this.dataItems.map(item => item.access_time).filter(t => typeof t === 'number' && !isNaN(t));
|
|
165
|
+
this.max_access_time = times.length ? Math.max(...times) : 100;
|
|
142
166
|
var traces = this.buildOnlineTraces(this.dataItems); // generate line traces
|
|
143
167
|
|
|
144
168
|
traces = traces.concat(this.buildOfflineNATraces(this.dataItems)); // generate bar traces
|
|
@@ -149,6 +173,7 @@ var script$1 = {
|
|
|
149
173
|
showlegend: true,
|
|
150
174
|
autosize: true,
|
|
151
175
|
height: this.height,
|
|
176
|
+
width: this.width,
|
|
152
177
|
margin: {
|
|
153
178
|
l: 50,
|
|
154
179
|
r: 50,
|
|
@@ -176,7 +201,9 @@ var script$1 = {
|
|
|
176
201
|
tick0: this.xaxisTickZero(),
|
|
177
202
|
dtick: this.xaxisTickD(),
|
|
178
203
|
tickangle: this.xaxisTickAngle(),
|
|
179
|
-
tickformat: this.xaxisTickFormat()
|
|
204
|
+
tickformat: this.xaxisTickFormat(),
|
|
205
|
+
tickvals: this.sixMonths ? this.monthTickVales() : null,
|
|
206
|
+
range: this.xaxisRange()
|
|
180
207
|
},
|
|
181
208
|
yaxis: {
|
|
182
209
|
title: this.yaxisTitle,
|
|
@@ -253,10 +280,16 @@ var script$1 = {
|
|
|
253
280
|
var sum = 0;
|
|
254
281
|
var nonNullValues = 0;
|
|
255
282
|
for (let i = 0; i < data.length; i++) {
|
|
256
|
-
|
|
257
|
-
|
|
283
|
+
// Only genuine successful measurements belong on the online
|
|
284
|
+
// line. An error code with a recorded access_time must not
|
|
285
|
+
// sneak in (it would draw green and skew the average); treat it
|
|
286
|
+
// as a break, just like a null access_time.
|
|
287
|
+
if (data[i].access_time !== null && !this.isErrorCode(data[i].code)) {
|
|
288
|
+
// keep adding to subarray. Snap the x to the day centre so
|
|
289
|
+
// the online marker sits in the middle of its day, on the
|
|
290
|
+
// same grid as the offline/NA bars.
|
|
258
291
|
subarrayTime.push(data[i].access_time);
|
|
259
|
-
subarrayDate.push(data[i].date);
|
|
292
|
+
subarrayDate.push(this.dayCenter(data[i].date));
|
|
260
293
|
sum += data[i].access_time;
|
|
261
294
|
nonNullValues += 1;
|
|
262
295
|
} else {
|
|
@@ -380,16 +413,71 @@ var script$1 = {
|
|
|
380
413
|
traces.push(this.buildAvgAccessTimeTrace(subarrays.average_access_time));
|
|
381
414
|
return traces;
|
|
382
415
|
},
|
|
416
|
+
isErrorCode(code) {
|
|
417
|
+
/*
|
|
418
|
+
Returns true if the HTTP code denotes the server being offline.
|
|
419
|
+
Single source of truth shared by the online-line and offline-bar
|
|
420
|
+
logic so redness is decided consistently by the response code.
|
|
421
|
+
*/
|
|
422
|
+
const errorCodes = [400, 403, 404, 408, 500, 502, 503, 504];
|
|
423
|
+
return errorCodes.includes(code);
|
|
424
|
+
},
|
|
425
|
+
dayCenter(date) {
|
|
426
|
+
/*
|
|
427
|
+
Snaps a date to the centre (12:00) of its UTC calendar day, returned
|
|
428
|
+
as ms since epoch. The whole chart is a daily grid: null placeholders
|
|
429
|
+
arrive at 00:00Z and real probes mid-afternoon, so aligning every
|
|
430
|
+
point and every bar to the day centre keeps them on one grid. That is
|
|
431
|
+
what makes blocks sit centred on their day and prevents bars from
|
|
432
|
+
bleeding into a neighbouring day's measurement.
|
|
433
|
+
*/
|
|
434
|
+
const d = new Date(date);
|
|
435
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
436
|
+
return d.getTime() + 1000 * 3600 * 24 / 2;
|
|
437
|
+
},
|
|
438
|
+
computeDaySlots(data) {
|
|
439
|
+
/*
|
|
440
|
+
Returns, for every data point, the bar slot it should occupy: a full
|
|
441
|
+
calendar day centred on that day (see dayCenter). Consecutive
|
|
442
|
+
offline/NA days therefore tile into a single block, and because every
|
|
443
|
+
online point is snapped to the same daily grid a bar never overlaps a
|
|
444
|
+
measurement.
|
|
445
|
+
Arguments:
|
|
446
|
+
- data: array of objects with a "date" key
|
|
447
|
+
Returns an array (parallel to data) of objects with keys:
|
|
448
|
+
- center: slot centre (day centre), in ms since epoch
|
|
449
|
+
- width: slot width, in ms (one full day)
|
|
450
|
+
*/
|
|
451
|
+
const DAY = 1000 * 3600 * 24;
|
|
452
|
+
return data.map(item => ({
|
|
453
|
+
center: this.dayCenter(item.date),
|
|
454
|
+
width: DAY
|
|
455
|
+
}));
|
|
456
|
+
},
|
|
383
457
|
extractOfflineNADates(data) {
|
|
458
|
+
/*
|
|
459
|
+
This function extracts the bar slots of points with no successful
|
|
460
|
+
measurement. Depending on the code, the server is offline or NA on
|
|
461
|
+
those dates.
|
|
462
|
+
Arguments:
|
|
463
|
+
- data: array of objects with keys "access_time", "date" and "code". access_time and code can be null
|
|
464
|
+
Returns an object with keys:
|
|
465
|
+
- NA: array of slots (see computeDaySlots) for code null (no information available)
|
|
466
|
+
- down: array of slots for code in errorCodes (server offline)
|
|
467
|
+
Classification is driven by the HTTP code, not by access_time
|
|
468
|
+
null-ness: an error response that happened to record an access_time
|
|
469
|
+
is still "offline", and a missing code is "no information available".
|
|
470
|
+
*/
|
|
471
|
+
const slots = this.computeDaySlots(data);
|
|
384
472
|
const resultNA = [];
|
|
385
473
|
const resultOffline = [];
|
|
386
474
|
for (let i = 0; i < data.length; i++) {
|
|
387
|
-
//
|
|
388
|
-
if (data[i].
|
|
389
|
-
resultNA.push(
|
|
390
|
-
//
|
|
391
|
-
} else if (data[i].
|
|
392
|
-
resultOffline.push(
|
|
475
|
+
// no code at all -> grey "No information available"
|
|
476
|
+
if (data[i].code === null) {
|
|
477
|
+
resultNA.push(slots[i]);
|
|
478
|
+
// an error code -> red "Offline"
|
|
479
|
+
} else if (this.isErrorCode(data[i].code)) {
|
|
480
|
+
resultOffline.push(slots[i]);
|
|
393
481
|
}
|
|
394
482
|
}
|
|
395
483
|
return {
|
|
@@ -401,10 +489,12 @@ var script$1 = {
|
|
|
401
489
|
/*
|
|
402
490
|
This function builds a bar trace.
|
|
403
491
|
Arguments:
|
|
404
|
-
- data: object with keys "dates" and "
|
|
405
|
-
- dates: array of dates
|
|
492
|
+
- data: object with keys "dates", "access_times", "colors" and "widths"
|
|
493
|
+
- dates: array of dates (slot centres, in ms)
|
|
406
494
|
- access_times: array of access_times
|
|
407
495
|
- colors: array of colors in rgba format (eg:"rgba(255, 153, 145,0.8)")
|
|
496
|
+
- widths: array of per-bar widths in ms, so each bar fills its
|
|
497
|
+
own time slot (see computeDaySlots) instead of a fixed day
|
|
408
498
|
- name: name of the trace
|
|
409
499
|
*/
|
|
410
500
|
const trace = {
|
|
@@ -419,35 +509,43 @@ var script$1 = {
|
|
|
419
509
|
showlegend: showlegend,
|
|
420
510
|
hoverinfo: hoverinfo,
|
|
421
511
|
hovertemplate: hovertemplate,
|
|
422
|
-
width:
|
|
512
|
+
width: data.widths
|
|
423
513
|
};
|
|
424
|
-
|
|
425
|
-
console.log(trace);
|
|
426
514
|
return trace;
|
|
427
515
|
},
|
|
428
|
-
buildBarTraces(
|
|
516
|
+
buildBarTraces(slots, color, label, group) {
|
|
429
517
|
/*
|
|
430
518
|
This function builds the series used to display the offline/NA bars.
|
|
431
|
-
For each
|
|
432
|
-
|
|
433
|
-
|
|
519
|
+
For each slot two stacked columns are created that together span the
|
|
520
|
+
whole plot height: a thin strong-coloured band sitting on the axis,
|
|
521
|
+
and a light-coloured column filling the rest of the height above it.
|
|
434
522
|
Arguments:
|
|
435
|
-
-
|
|
523
|
+
- slots: array of slots ({ center, width }), see computeDaySlots
|
|
436
524
|
- color: color of the bars
|
|
437
525
|
Returns array with offline and NA traces
|
|
438
526
|
*/
|
|
439
527
|
|
|
440
528
|
const colorStrong = `rgba(${color},.8)`;
|
|
441
529
|
const colorLight = `rgba(${color},.2)`;
|
|
530
|
+
const centers = slots.map(s => s.center);
|
|
531
|
+
const widths = slots.map(s => s.width);
|
|
532
|
+
|
|
533
|
+
// Total background height (keeps the previous y-axis extent). The
|
|
534
|
+
// strong colour is a thin band on the axis; the light colour fills
|
|
535
|
+
// the remaining height stacked on top of it.
|
|
536
|
+
const totalHeight = 2 + this.max_access_time * 1.1;
|
|
537
|
+
const baseHeight = totalHeight * 0.03;
|
|
442
538
|
const dataShort = {
|
|
443
|
-
dates:
|
|
444
|
-
access_times: Array(
|
|
445
|
-
colors: Array(
|
|
539
|
+
dates: centers,
|
|
540
|
+
access_times: Array(slots.length).fill(baseHeight),
|
|
541
|
+
colors: Array(slots.length).fill(colorStrong),
|
|
542
|
+
widths: widths
|
|
446
543
|
};
|
|
447
544
|
const dataTall = {
|
|
448
|
-
dates:
|
|
449
|
-
access_times: Array(
|
|
450
|
-
colors: Array(
|
|
545
|
+
dates: centers,
|
|
546
|
+
access_times: Array(slots.length).fill(totalHeight - baseHeight),
|
|
547
|
+
colors: Array(slots.length).fill(colorLight),
|
|
548
|
+
widths: widths
|
|
451
549
|
};
|
|
452
550
|
const hovertemplate = `<b>${label}</b><br>%{x|%d %b %Y}<br>%{y} ms <extra></extra>`;
|
|
453
551
|
const traceShort = this.barTrace(dataShort, label, false, 'skip', '', group);
|
|
@@ -469,12 +567,10 @@ var script$1 = {
|
|
|
469
567
|
// Down arrays
|
|
470
568
|
if (arrays.down.length > 0) {
|
|
471
569
|
traces = traces.concat(this.buildBarTraces(arrays.down, this.colorOffline, 'Offline', 'down'));
|
|
472
|
-
console.log(arrays.down);
|
|
473
570
|
}
|
|
474
571
|
// NA arrays
|
|
475
572
|
if (arrays.NA.length > 0) {
|
|
476
573
|
traces = traces.concat(this.buildBarTraces(arrays.NA, this.colorNA, 'No information available', 'na'));
|
|
477
|
-
console.log(arrays.NA);
|
|
478
574
|
}
|
|
479
575
|
return traces;
|
|
480
576
|
},
|
|
@@ -489,7 +585,7 @@ var script$1 = {
|
|
|
489
585
|
return "%A<br>%d %b";
|
|
490
586
|
}
|
|
491
587
|
if (this.sixMonths) {
|
|
492
|
-
return "%b";
|
|
588
|
+
return "%d %b";
|
|
493
589
|
} else {
|
|
494
590
|
return "%d %b";
|
|
495
591
|
}
|
|
@@ -504,7 +600,7 @@ var script$1 = {
|
|
|
504
600
|
return 0;
|
|
505
601
|
}
|
|
506
602
|
if (this.sixMonths) {
|
|
507
|
-
return
|
|
603
|
+
return 45;
|
|
508
604
|
} else {
|
|
509
605
|
return 45;
|
|
510
606
|
}
|
|
@@ -525,7 +621,10 @@ var script$1 = {
|
|
|
525
621
|
console.log('six months ago: ' + new Date(lastDate.setMonth(lastDate.getMonth() - 6)));
|
|
526
622
|
return new Date(lastDate.setMonth(lastDate.getMonth() - 6));
|
|
527
623
|
} else {
|
|
528
|
-
|
|
624
|
+
// Anchor day ticks at the day centre (noon) so each label sits
|
|
625
|
+
// under the middle of its day block, aligned with the online dot
|
|
626
|
+
// (which is snapped to the same day centre).
|
|
627
|
+
return this.dayCenter(this.dataItems[0].date);
|
|
529
628
|
}
|
|
530
629
|
},
|
|
531
630
|
xaxisMode() {
|
|
@@ -534,6 +633,30 @@ var script$1 = {
|
|
|
534
633
|
} else {
|
|
535
634
|
return "instant";
|
|
536
635
|
}
|
|
636
|
+
},
|
|
637
|
+
xaxisRange() {
|
|
638
|
+
if (this.xrange) {
|
|
639
|
+
return this.xrange;
|
|
640
|
+
}
|
|
641
|
+
// Span whole calendar days so the first and last day blocks (which
|
|
642
|
+
// are centred on their day, one full day wide) are fully visible.
|
|
643
|
+
const DAY = 1000 * 3600 * 24;
|
|
644
|
+
const firstDay = new Date(this.dataItems[0].date);
|
|
645
|
+
firstDay.setUTCHours(0, 0, 0, 0);
|
|
646
|
+
const lastDay = new Date(this.dataItems[this.dataItems.length - 1].date);
|
|
647
|
+
lastDay.setUTCHours(0, 0, 0, 0);
|
|
648
|
+
return [firstDay.getTime(), lastDay.getTime() + DAY];
|
|
649
|
+
},
|
|
650
|
+
monthTickVales() {
|
|
651
|
+
// First day of month of last date and previous five months
|
|
652
|
+
const lastDate = new Date(this.dataItems[this.dataItems.length - 1].date);
|
|
653
|
+
const lastMonth = lastDate.getMonth();
|
|
654
|
+
const lastYear = lastDate.getFullYear();
|
|
655
|
+
const tickValues = [];
|
|
656
|
+
for (let i = 0; i < 6; i++) {
|
|
657
|
+
tickValues.push(new Date(lastYear, lastMonth - i, 1));
|
|
658
|
+
}
|
|
659
|
+
return tickValues;
|
|
537
660
|
}
|
|
538
661
|
}
|
|
539
662
|
};function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier
|
|
@@ -640,7 +763,7 @@ const __vue_inject_styles__$1 = undefined;
|
|
|
640
763
|
/* scoped */
|
|
641
764
|
const __vue_scope_id__$1 = undefined;
|
|
642
765
|
/* module identifier */
|
|
643
|
-
const __vue_module_identifier__$1 = "data-v-
|
|
766
|
+
const __vue_module_identifier__$1 = "data-v-bab9d544";
|
|
644
767
|
/* functional template */
|
|
645
768
|
const __vue_is_functional_template__$1 = false;
|
|
646
769
|
/* style inject */
|
|
@@ -17,6 +17,13 @@ var script$1 = {
|
|
|
17
17
|
}
|
|
18
18
|
}),
|
|
19
19
|
props: {
|
|
20
|
+
xrange: {
|
|
21
|
+
/*
|
|
22
|
+
xrange is the range of the x axis in ms.
|
|
23
|
+
*/
|
|
24
|
+
type: Array,
|
|
25
|
+
required: false
|
|
26
|
+
},
|
|
20
27
|
dtick: {
|
|
21
28
|
/*
|
|
22
29
|
dtick is the interval between ticks on the x axis in ms.
|
|
@@ -76,6 +83,14 @@ var script$1 = {
|
|
|
76
83
|
required: false,
|
|
77
84
|
default: 350
|
|
78
85
|
},
|
|
86
|
+
width: {
|
|
87
|
+
/*
|
|
88
|
+
width is the width of the plot in px.
|
|
89
|
+
*/
|
|
90
|
+
type: Number,
|
|
91
|
+
required: false,
|
|
92
|
+
default: 700
|
|
93
|
+
},
|
|
79
94
|
week: {
|
|
80
95
|
/*
|
|
81
96
|
whether the plot is used to show data of one week
|
|
@@ -127,9 +142,12 @@ var script$1 = {
|
|
|
127
142
|
if (value[i].code !== null && typeof value[i].code !== 'number') {
|
|
128
143
|
console.error(`[oeb-visualizations warn] code must be null or a number in dataItems prop item (at position ${i})`);
|
|
129
144
|
}
|
|
130
|
-
// Date must be a string containing a date
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
// Date must be a string containing a parseable date, or a number (timestamp)
|
|
146
|
+
const validDateString = typeof value[i].date === 'string' && !isNaN(Date.parse(value[i].date));
|
|
147
|
+
const validDateNumber = typeof value[i].date === 'number';
|
|
148
|
+
if (!validDateString && !validDateNumber) {
|
|
149
|
+
console.error(`[oeb-visualizations warn] date must be a string containing a date or a number in dataItems prop item (at position ${i})`);
|
|
150
|
+
console.error(`[oeb-visualizations warn] date type is ${typeof value[i].date} and the value is ${value[i].date}`);
|
|
133
151
|
}
|
|
134
152
|
|
|
135
153
|
// And date cannot be null
|
|
@@ -142,7 +160,13 @@ var script$1 = {
|
|
|
142
160
|
}
|
|
143
161
|
},
|
|
144
162
|
mounted() {
|
|
145
|
-
|
|
163
|
+
// Compute the tallest online bar from real measurements only.
|
|
164
|
+
// Math.max over an array containing null coerces null -> 0, so a site
|
|
165
|
+
// that was down the whole window would yield 0 and render the
|
|
166
|
+
// offline/NA bars with zero height (invisible). Ignore nulls/NaN and
|
|
167
|
+
// apply a floor so down-only sites still draw full-height bars.
|
|
168
|
+
const times = this.dataItems.map(item => item.access_time).filter(t => typeof t === 'number' && !isNaN(t));
|
|
169
|
+
this.max_access_time = times.length ? Math.max(...times) : 100;
|
|
146
170
|
var traces = this.buildOnlineTraces(this.dataItems); // generate line traces
|
|
147
171
|
|
|
148
172
|
traces = traces.concat(this.buildOfflineNATraces(this.dataItems)); // generate bar traces
|
|
@@ -153,6 +177,7 @@ var script$1 = {
|
|
|
153
177
|
showlegend: true,
|
|
154
178
|
autosize: true,
|
|
155
179
|
height: this.height,
|
|
180
|
+
width: this.width,
|
|
156
181
|
margin: {
|
|
157
182
|
l: 50,
|
|
158
183
|
r: 50,
|
|
@@ -180,7 +205,9 @@ var script$1 = {
|
|
|
180
205
|
tick0: this.xaxisTickZero(),
|
|
181
206
|
dtick: this.xaxisTickD(),
|
|
182
207
|
tickangle: this.xaxisTickAngle(),
|
|
183
|
-
tickformat: this.xaxisTickFormat()
|
|
208
|
+
tickformat: this.xaxisTickFormat(),
|
|
209
|
+
tickvals: this.sixMonths ? this.monthTickVales() : null,
|
|
210
|
+
range: this.xaxisRange()
|
|
184
211
|
},
|
|
185
212
|
yaxis: {
|
|
186
213
|
title: this.yaxisTitle,
|
|
@@ -257,10 +284,16 @@ var script$1 = {
|
|
|
257
284
|
var sum = 0;
|
|
258
285
|
var nonNullValues = 0;
|
|
259
286
|
for (let i = 0; i < data.length; i++) {
|
|
260
|
-
|
|
261
|
-
|
|
287
|
+
// Only genuine successful measurements belong on the online
|
|
288
|
+
// line. An error code with a recorded access_time must not
|
|
289
|
+
// sneak in (it would draw green and skew the average); treat it
|
|
290
|
+
// as a break, just like a null access_time.
|
|
291
|
+
if (data[i].access_time !== null && !this.isErrorCode(data[i].code)) {
|
|
292
|
+
// keep adding to subarray. Snap the x to the day centre so
|
|
293
|
+
// the online marker sits in the middle of its day, on the
|
|
294
|
+
// same grid as the offline/NA bars.
|
|
262
295
|
subarrayTime.push(data[i].access_time);
|
|
263
|
-
subarrayDate.push(data[i].date);
|
|
296
|
+
subarrayDate.push(this.dayCenter(data[i].date));
|
|
264
297
|
sum += data[i].access_time;
|
|
265
298
|
nonNullValues += 1;
|
|
266
299
|
} else {
|
|
@@ -384,16 +417,71 @@ var script$1 = {
|
|
|
384
417
|
traces.push(this.buildAvgAccessTimeTrace(subarrays.average_access_time));
|
|
385
418
|
return traces;
|
|
386
419
|
},
|
|
420
|
+
isErrorCode(code) {
|
|
421
|
+
/*
|
|
422
|
+
Returns true if the HTTP code denotes the server being offline.
|
|
423
|
+
Single source of truth shared by the online-line and offline-bar
|
|
424
|
+
logic so redness is decided consistently by the response code.
|
|
425
|
+
*/
|
|
426
|
+
const errorCodes = [400, 403, 404, 408, 500, 502, 503, 504];
|
|
427
|
+
return errorCodes.includes(code);
|
|
428
|
+
},
|
|
429
|
+
dayCenter(date) {
|
|
430
|
+
/*
|
|
431
|
+
Snaps a date to the centre (12:00) of its UTC calendar day, returned
|
|
432
|
+
as ms since epoch. The whole chart is a daily grid: null placeholders
|
|
433
|
+
arrive at 00:00Z and real probes mid-afternoon, so aligning every
|
|
434
|
+
point and every bar to the day centre keeps them on one grid. That is
|
|
435
|
+
what makes blocks sit centred on their day and prevents bars from
|
|
436
|
+
bleeding into a neighbouring day's measurement.
|
|
437
|
+
*/
|
|
438
|
+
const d = new Date(date);
|
|
439
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
440
|
+
return d.getTime() + 1000 * 3600 * 24 / 2;
|
|
441
|
+
},
|
|
442
|
+
computeDaySlots(data) {
|
|
443
|
+
/*
|
|
444
|
+
Returns, for every data point, the bar slot it should occupy: a full
|
|
445
|
+
calendar day centred on that day (see dayCenter). Consecutive
|
|
446
|
+
offline/NA days therefore tile into a single block, and because every
|
|
447
|
+
online point is snapped to the same daily grid a bar never overlaps a
|
|
448
|
+
measurement.
|
|
449
|
+
Arguments:
|
|
450
|
+
- data: array of objects with a "date" key
|
|
451
|
+
Returns an array (parallel to data) of objects with keys:
|
|
452
|
+
- center: slot centre (day centre), in ms since epoch
|
|
453
|
+
- width: slot width, in ms (one full day)
|
|
454
|
+
*/
|
|
455
|
+
const DAY = 1000 * 3600 * 24;
|
|
456
|
+
return data.map(item => ({
|
|
457
|
+
center: this.dayCenter(item.date),
|
|
458
|
+
width: DAY
|
|
459
|
+
}));
|
|
460
|
+
},
|
|
387
461
|
extractOfflineNADates(data) {
|
|
462
|
+
/*
|
|
463
|
+
This function extracts the bar slots of points with no successful
|
|
464
|
+
measurement. Depending on the code, the server is offline or NA on
|
|
465
|
+
those dates.
|
|
466
|
+
Arguments:
|
|
467
|
+
- data: array of objects with keys "access_time", "date" and "code". access_time and code can be null
|
|
468
|
+
Returns an object with keys:
|
|
469
|
+
- NA: array of slots (see computeDaySlots) for code null (no information available)
|
|
470
|
+
- down: array of slots for code in errorCodes (server offline)
|
|
471
|
+
Classification is driven by the HTTP code, not by access_time
|
|
472
|
+
null-ness: an error response that happened to record an access_time
|
|
473
|
+
is still "offline", and a missing code is "no information available".
|
|
474
|
+
*/
|
|
475
|
+
const slots = this.computeDaySlots(data);
|
|
388
476
|
const resultNA = [];
|
|
389
477
|
const resultOffline = [];
|
|
390
478
|
for (let i = 0; i < data.length; i++) {
|
|
391
|
-
//
|
|
392
|
-
if (data[i].
|
|
393
|
-
resultNA.push(
|
|
394
|
-
//
|
|
395
|
-
} else if (data[i].
|
|
396
|
-
resultOffline.push(
|
|
479
|
+
// no code at all -> grey "No information available"
|
|
480
|
+
if (data[i].code === null) {
|
|
481
|
+
resultNA.push(slots[i]);
|
|
482
|
+
// an error code -> red "Offline"
|
|
483
|
+
} else if (this.isErrorCode(data[i].code)) {
|
|
484
|
+
resultOffline.push(slots[i]);
|
|
397
485
|
}
|
|
398
486
|
}
|
|
399
487
|
return {
|
|
@@ -405,10 +493,12 @@ var script$1 = {
|
|
|
405
493
|
/*
|
|
406
494
|
This function builds a bar trace.
|
|
407
495
|
Arguments:
|
|
408
|
-
- data: object with keys "dates" and "
|
|
409
|
-
- dates: array of dates
|
|
496
|
+
- data: object with keys "dates", "access_times", "colors" and "widths"
|
|
497
|
+
- dates: array of dates (slot centres, in ms)
|
|
410
498
|
- access_times: array of access_times
|
|
411
499
|
- colors: array of colors in rgba format (eg:"rgba(255, 153, 145,0.8)")
|
|
500
|
+
- widths: array of per-bar widths in ms, so each bar fills its
|
|
501
|
+
own time slot (see computeDaySlots) instead of a fixed day
|
|
412
502
|
- name: name of the trace
|
|
413
503
|
*/
|
|
414
504
|
const trace = {
|
|
@@ -423,35 +513,43 @@ var script$1 = {
|
|
|
423
513
|
showlegend: showlegend,
|
|
424
514
|
hoverinfo: hoverinfo,
|
|
425
515
|
hovertemplate: hovertemplate,
|
|
426
|
-
width:
|
|
516
|
+
width: data.widths
|
|
427
517
|
};
|
|
428
|
-
|
|
429
|
-
console.log(trace);
|
|
430
518
|
return trace;
|
|
431
519
|
},
|
|
432
|
-
buildBarTraces(
|
|
520
|
+
buildBarTraces(slots, color, label, group) {
|
|
433
521
|
/*
|
|
434
522
|
This function builds the series used to display the offline/NA bars.
|
|
435
|
-
For each
|
|
436
|
-
|
|
437
|
-
|
|
523
|
+
For each slot two stacked columns are created that together span the
|
|
524
|
+
whole plot height: a thin strong-coloured band sitting on the axis,
|
|
525
|
+
and a light-coloured column filling the rest of the height above it.
|
|
438
526
|
Arguments:
|
|
439
|
-
-
|
|
527
|
+
- slots: array of slots ({ center, width }), see computeDaySlots
|
|
440
528
|
- color: color of the bars
|
|
441
529
|
Returns array with offline and NA traces
|
|
442
530
|
*/
|
|
443
531
|
|
|
444
532
|
const colorStrong = `rgba(${color},.8)`;
|
|
445
533
|
const colorLight = `rgba(${color},.2)`;
|
|
534
|
+
const centers = slots.map(s => s.center);
|
|
535
|
+
const widths = slots.map(s => s.width);
|
|
536
|
+
|
|
537
|
+
// Total background height (keeps the previous y-axis extent). The
|
|
538
|
+
// strong colour is a thin band on the axis; the light colour fills
|
|
539
|
+
// the remaining height stacked on top of it.
|
|
540
|
+
const totalHeight = 2 + this.max_access_time * 1.1;
|
|
541
|
+
const baseHeight = totalHeight * 0.03;
|
|
446
542
|
const dataShort = {
|
|
447
|
-
dates:
|
|
448
|
-
access_times: Array(
|
|
449
|
-
colors: Array(
|
|
543
|
+
dates: centers,
|
|
544
|
+
access_times: Array(slots.length).fill(baseHeight),
|
|
545
|
+
colors: Array(slots.length).fill(colorStrong),
|
|
546
|
+
widths: widths
|
|
450
547
|
};
|
|
451
548
|
const dataTall = {
|
|
452
|
-
dates:
|
|
453
|
-
access_times: Array(
|
|
454
|
-
colors: Array(
|
|
549
|
+
dates: centers,
|
|
550
|
+
access_times: Array(slots.length).fill(totalHeight - baseHeight),
|
|
551
|
+
colors: Array(slots.length).fill(colorLight),
|
|
552
|
+
widths: widths
|
|
455
553
|
};
|
|
456
554
|
const hovertemplate = `<b>${label}</b><br>%{x|%d %b %Y}<br>%{y} ms <extra></extra>`;
|
|
457
555
|
const traceShort = this.barTrace(dataShort, label, false, 'skip', '', group);
|
|
@@ -473,12 +571,10 @@ var script$1 = {
|
|
|
473
571
|
// Down arrays
|
|
474
572
|
if (arrays.down.length > 0) {
|
|
475
573
|
traces = traces.concat(this.buildBarTraces(arrays.down, this.colorOffline, 'Offline', 'down'));
|
|
476
|
-
console.log(arrays.down);
|
|
477
574
|
}
|
|
478
575
|
// NA arrays
|
|
479
576
|
if (arrays.NA.length > 0) {
|
|
480
577
|
traces = traces.concat(this.buildBarTraces(arrays.NA, this.colorNA, 'No information available', 'na'));
|
|
481
|
-
console.log(arrays.NA);
|
|
482
578
|
}
|
|
483
579
|
return traces;
|
|
484
580
|
},
|
|
@@ -493,7 +589,7 @@ var script$1 = {
|
|
|
493
589
|
return "%A<br>%d %b";
|
|
494
590
|
}
|
|
495
591
|
if (this.sixMonths) {
|
|
496
|
-
return "%b";
|
|
592
|
+
return "%d %b";
|
|
497
593
|
} else {
|
|
498
594
|
return "%d %b";
|
|
499
595
|
}
|
|
@@ -508,7 +604,7 @@ var script$1 = {
|
|
|
508
604
|
return 0;
|
|
509
605
|
}
|
|
510
606
|
if (this.sixMonths) {
|
|
511
|
-
return
|
|
607
|
+
return 45;
|
|
512
608
|
} else {
|
|
513
609
|
return 45;
|
|
514
610
|
}
|
|
@@ -529,7 +625,10 @@ var script$1 = {
|
|
|
529
625
|
console.log('six months ago: ' + new Date(lastDate.setMonth(lastDate.getMonth() - 6)));
|
|
530
626
|
return new Date(lastDate.setMonth(lastDate.getMonth() - 6));
|
|
531
627
|
} else {
|
|
532
|
-
|
|
628
|
+
// Anchor day ticks at the day centre (noon) so each label sits
|
|
629
|
+
// under the middle of its day block, aligned with the online dot
|
|
630
|
+
// (which is snapped to the same day centre).
|
|
631
|
+
return this.dayCenter(this.dataItems[0].date);
|
|
533
632
|
}
|
|
534
633
|
},
|
|
535
634
|
xaxisMode() {
|
|
@@ -538,6 +637,30 @@ var script$1 = {
|
|
|
538
637
|
} else {
|
|
539
638
|
return "instant";
|
|
540
639
|
}
|
|
640
|
+
},
|
|
641
|
+
xaxisRange() {
|
|
642
|
+
if (this.xrange) {
|
|
643
|
+
return this.xrange;
|
|
644
|
+
}
|
|
645
|
+
// Span whole calendar days so the first and last day blocks (which
|
|
646
|
+
// are centred on their day, one full day wide) are fully visible.
|
|
647
|
+
const DAY = 1000 * 3600 * 24;
|
|
648
|
+
const firstDay = new Date(this.dataItems[0].date);
|
|
649
|
+
firstDay.setUTCHours(0, 0, 0, 0);
|
|
650
|
+
const lastDay = new Date(this.dataItems[this.dataItems.length - 1].date);
|
|
651
|
+
lastDay.setUTCHours(0, 0, 0, 0);
|
|
652
|
+
return [firstDay.getTime(), lastDay.getTime() + DAY];
|
|
653
|
+
},
|
|
654
|
+
monthTickVales() {
|
|
655
|
+
// First day of month of last date and previous five months
|
|
656
|
+
const lastDate = new Date(this.dataItems[this.dataItems.length - 1].date);
|
|
657
|
+
const lastMonth = lastDate.getMonth();
|
|
658
|
+
const lastYear = lastDate.getFullYear();
|
|
659
|
+
const tickValues = [];
|
|
660
|
+
for (let i = 0; i < 6; i++) {
|
|
661
|
+
tickValues.push(new Date(lastYear, lastMonth - i, 1));
|
|
662
|
+
}
|
|
663
|
+
return tickValues;
|
|
541
664
|
}
|
|
542
665
|
}
|
|
543
666
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var oeb_visualizations=function(e,t){"use strict";function i(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var s=i(t);function a(e){return Math.random().toString(36).replace("0.",e||"")}var r=function(e,t,i,s,a,r,o,n,l,c){"boolean"!=typeof o&&(l=n,n=o,o=!1);var d,h="function"==typeof i?i.options:i;if(e&&e.render&&(h.render=e.render,h.staticRenderFns=e.staticRenderFns,h._compiled=!0,a&&(h.functional=!0)),s&&(h._scopeId=s),r?(d=function(e){(e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),t&&t.call(this,l(e)),e&&e._registeredComponents&&e._registeredComponents.add(r)},h._ssrRegister=d):t&&(d=o?function(){t.call(this,c(this.$root.$options.shadowRoot))}:function(e){t.call(this,n(e))}),d)if(h.functional){var u=h.render;h.render=function(e,t){return d.call(t),u(e,t)}}else{var m=h.beforeCreate;h.beforeCreate=m?[].concat(m,d):[d]}return i};var o=r({render:function(){var e=this,t=e.$createElement;return(e._self._c||t)("div",{attrs:{id:e.divId}})},staticRenderFns:[]},undefined,{name:"accessibilityPlot",data:()=>({divId:a("acc_plot_"),max_access_time:0,config:{displaylogo:!1,responsive:!0,modeBarButtonsToRemove:["lasso"]}}),props:{dtick:{type:Number,required:!1,default:864e5},xaxisTitle:{type:String,required:!1,default:"Date"},yaxisTitle:{type:String,required:!1,default:"Access time (ms)"},colorOnline:{type:String,required:!1,default:"111, 176, 129"},colorOffline:{type:String,required:!1,default:"255, 153, 145"},colorNA:{type:String,required:!1,default:"204,204,204"},height:{type:Number,required:!1,default:350},week:{type:Boolean,required:!1,default:!1},sixMonths:{type:Boolean,required:!1,default:!1},dataItems:{type:Array,required:!0,validator:function(e){for(let t=0;t<e.length;t++)e[t].hasOwnProperty("access_time")?e[t].hasOwnProperty("date")?e[t].hasOwnProperty("code")||console.error(`[oeb-visualizations warn] code key is missing in dataItems prop item (at position ${t})`):console.error(`[oeb-visualizations warn] date key is missing in dataItems prop item (at position ${t})`):console.error(`[oeb-visualizations warn] access_time key is missing in dataItems prop item (at position ${t})`),null!==e[t].access_time&&"number"!=typeof e[t].access_time&&console.error(`[oeb-visualizations warn] access_time must be null or a number in dataItems prop item (at position ${t})`),null!==e[t].code&&"number"!=typeof e[t].code&&console.error(`[oeb-visualizations warn] code must be null or a number in dataItems prop item (at position ${t})`),("string"!=typeof e[t].date||isNaN(Date.parse(e[t].date)))&&console.error(`[oeb-visualizations warn] date must be a string containing a date in dataItems prop item (at position ${t})`),null===e[t].date&&console.error(`[oeb-visualizations warn] date cannot be null in dataItems prop item (at position ${t})`);return!0}}},mounted(){this.max_access_time=Math.max(...this.dataItems.map((e=>e.access_time)));var e=this.buildOnlineTraces(this.dataItems);e=e.concat(this.buildOfflineNATraces(this.dataItems));const t={bargap:0,barmode:"stack",showlegend:!0,autosize:!0,height:this.height,margin:{l:50,r:50,b:50,t:20,pad:4},xaxis:{type:"date",ticklabelmode:this.xaxisMode(),title:this.xaxisTitle,font:{size:10},tickfont:{size:10},showgrid:!!this.sixMonths,griddash:"dot",gridwidth:1,gridcolor:"#d9d7d7",showspikes:!0,spikedash:"4px",spikethickness:1,tick0:this.xaxisTickZero(),dtick:this.xaxisTickD(),tickangle:this.xaxisTickAngle(),tickformat:this.xaxisTickFormat()},yaxis:{title:this.yaxisTitle,titlefont:{size:10},tickfont:{size:10}},template:"plotly_white",legend:{orientation:"h",yanchor:"bottom",y:-.5,xanchor:"left",x:.05,font:{size:8}},hoverlabel:{bgcolor:"#FFF"},hovermode:"closest"};console.log(this.divId),s.default.newPlot(this.divId,e,t,this.config)},methods:{generateColor(e,t){let i=[];for(let s=0;s<30;s++)switch(e[s]){case"up":i.push(`rgba(${this.colorOnline},0)`);break;case"down":i.push(`rgba(${this.colorOffline},${t})`);break;case"NA":i.push(`rgba(${this.colorNA},${t})`)}return i},extractSubarraysBetweenNullValues(e){var t={access_time:[],date:[],average_access_time:0},i=[],s=[],a=0,r=0;for(let o=0;o<e.length;o++)null!==e[o].access_time?(i.push(e[o].access_time),s.push(e[o].date),a+=e[o].access_time,r+=1):i.length>0&&(t.access_time.push(i),t.date.push(s),i=[],s=[]);return t.access_time.push(i),t.date.push(s),t.average_access_time=a/r,t},buildAccessTimeTraces(e){var t=[];for(let i=0;i<e.access_time.length;i++){const s=e.access_time[i],a={x:e.date[i],y:s,name:"Online",legendgroup:"up",showlegend:0===i,mode:"markers+lines",type:"scatter",fill:"tozeroy",fillcolor:`rgba(${this.colorOnline},.2)`,connectgaps:!1,line:{color:`rgba(${this.colorOnline},.8)`,width:1.5},marker:{size:5},hovertemplate:"<b>Online</b><br>%{x|%d %b %Y}<br>%{y} ms <extra></extra>",hoveron:"points+fills"};t.push(a)}return t},buildAvgAccessTimeTrace(e){const t=new Date(this.dataItems[0].date);this.week?t.setDate(t.getDate()-.3):t.setDate(t.getDate()-1);const i=new Date(this.dataItems[this.dataItems.length-1].date);this.week?i.setDate(i.getDate()+.3):i.setDate(i.getDate()+1);return{x:[t,i],y:[e,e],name:"Average access time",showlegend:!0,mode:"lines",type:"scatter",line:{color:`rgba(${this.colorOnline},.6)`,width:1.5,dash:"4px"},marker:{size:5},hovertemplate:"<b>Average access time</b><br>%{y:.2f} ms <extra></extra>"}},buildOnlineTraces(e){const t=this.extractSubarraysBetweenNullValues(e);var i=this.buildAccessTimeTraces(t);return i.push(this.buildAvgAccessTimeTrace(t.average_access_time)),i},extractOfflineNADates(e){const t=[],i=[];for(let s=0;s<e.length;s++)null===e[s].access_time&&null===e[s].code?t.push(e[s].date):null===e[s].access_time&&i.push(e[s].date);return{NA:t,down:i}},barTrace(e,t,i,s,a,r){const o={x:e.dates,y:e.access_times,marker:{color:e.colors},name:t,type:"bar",legendgroup:r,showlegend:i,hoverinfo:s,hovertemplate:a,width:864e5};return console.log(o),o},buildBarTraces(e,t,i,s){const a=`rgba(${t},.8)`,r=`rgba(${t},.2)`,o={dates:e,access_times:Array(e.length).fill(2),colors:Array(e.length).fill(a)},n={dates:e,access_times:Array(e.length).fill(1.1*this.max_access_time),colors:Array(e.length).fill(r)},l=`<b>${i}</b><br>%{x|%d %b %Y}<br>%{y} ms <extra></extra>`;return[this.barTrace(o,i,!1,"skip","",s),this.barTrace(n,i,!0,"all",l,s)]},buildOfflineNATraces(e){var t=[];const i=this.extractOfflineNADates(e);return i.down.length>0&&(t=t.concat(this.buildBarTraces(i.down,this.colorOffline,"Offline","down")),console.log(i.down)),i.NA.length>0&&(t=t.concat(this.buildBarTraces(i.NA,this.colorNA,"No information available","na")),console.log(i.NA)),t},xaxisTickFormat(){return this.week?"%A<br>%d %b":this.sixMonths?"%b":"%d %b"},xaxisTickAngle(){return this.week||this.sixMonths?0:45},xaxisTickD(){return!0===this.sixMonths?"M1":this.dtick},xaxisTickZero(){if(!0===this.sixMonths){console.log("zero six months ago");const e=new Date(this.dataItems[this.dataItems.length-1].date);return console.log(e),console.log("six months ago: "+new Date(e.setMonth(e.getMonth()-6))),new Date(e.setMonth(e.getMonth()-6))}return this.dataItems[0]},xaxisMode(){return!0===this.sixMonths?"period":"instant"}}},undefined,!1,undefined,void 0,void 0);var n=r({render:function(){var e=this,t=e.$createElement;return(e._self._c||t)("div",{attrs:{id:e.divId}})},staticRenderFns:[]},undefined,{name:"citationsPlot",data:()=>({divId:a("cit_plot_")}),props:{dataTraces:{type:Array,required:!0},stack:{type:Boolean,required:!1,default:!1},colors:{type:Array,required:!1,default:()=>["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd"]},height:{type:Number,required:!1,default:400},showlegend:{type:Boolean,required:!1,default:!0},title:{type:String,required:!1,default:""},xaxisTitle:{type:String,required:!1,default:"Year"},yaxisTitle:{type:String,required:!1,default:"Number of citations"},darkMode:{type:Boolean,required:!1,default:!1}},mounted(){const e=this.buildTraces(),t={showlegend:this.showlegend,autosize:!0,height:this.height,margin:{l:50,r:50,b:70,t:50,pad:4},xaxis:{title:this.xaxisTitle,font:{size:10},tickfont:{size:10},tickmode:"linear",color:this.darkMode?"white":"black"},yaxis:{title:this.yaxisTitle,titlefont:{size:10},tickfont:{size:10},font:{size:10},color:this.darkMode?"white":"black"},legend:{orientation:"h",yanchor:"bottom",y:1.02,xanchor:"right",x:1,font:{size:8,color:this.darkMode?"white":"black"}},hoverlabel:{color:this.darkMode?"white":"black"},hovermode:this.stack?"x unified":"closest",hoverdistance:70,plot_bgcolor:this.darkMode?"rgb(38, 50, 56)":"white",paper_bgcolor:this.darkMode?"rgb(38, 50, 56)":"white"};s.default.newPlot(this.divId,e,t)},methods:{buildTraces(){const e=[];for(let t=0;t<this.dataTraces.length;t++){const i={x:this.dataTraces[t].data.map((e=>e.year)),y:this.dataTraces[t].data.map((e=>e.citations)),mode:"lines+markers",name:this.dataTraces[t].label,hovertemplate:this.hoverTemplate(),marker:{size:5},line:{color:this.colors[t],width:1.8},stackgroup:this.stack?"one":null};e.push(i)}return e},hoverTemplate(){return this.stack?"%{y} citations <extra></extra>":"%{y} citations in %{x} <extra></extra>"}}},undefined,!1,undefined,void 0,void 0),l=Object.freeze({__proto__:null,accessibilityPlot:o,citationsPlot:n});const c=function(e){c.installed||(c.installed=!0,Object.entries(l).forEach((([t,i])=>{e.component(t,i)})))},d={install:c};{let e=null;"undefined"!=typeof window?e=window.Vue:"undefined"!=typeof global&&(e=global.Vue),e&&e.use(d)}return e.accessibilityPlot=o,e.citationsPlot=n,e.default=d,Object.defineProperty(e,"__esModule",{value:!0}),e}({},Plotly);
|
|
1
|
+
var oeb_visualizations=function(e,t){"use strict";function i(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var s=i(t);function a(e){return Math.random().toString(36).replace("0.",e||"")}var r=function(e,t,i,s,a,r,o,n,l,d){"boolean"!=typeof o&&(l=n,n=o,o=!1);var c,h="function"==typeof i?i.options:i;if(e&&e.render&&(h.render=e.render,h.staticRenderFns=e.staticRenderFns,h._compiled=!0,a&&(h.functional=!0)),s&&(h._scopeId=s),r?(c=function(e){(e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),t&&t.call(this,l(e)),e&&e._registeredComponents&&e._registeredComponents.add(r)},h._ssrRegister=c):t&&(c=o?function(){t.call(this,d(this.$root.$options.shadowRoot))}:function(e){t.call(this,n(e))}),c)if(h.functional){var u=h.render;h.render=function(e,t){return c.call(t),u(e,t)}}else{var m=h.beforeCreate;h.beforeCreate=m?[].concat(m,c):[c]}return i};var o=r({render:function(){var e=this,t=e.$createElement;return(e._self._c||t)("div",{attrs:{id:e.divId}})},staticRenderFns:[]},undefined,{name:"accessibilityPlot",data:()=>({divId:a("acc_plot_"),max_access_time:0,config:{displaylogo:!1,responsive:!0,modeBarButtonsToRemove:["lasso"]}}),props:{xrange:{type:Array,required:!1},dtick:{type:Number,required:!1,default:864e5},xaxisTitle:{type:String,required:!1,default:"Date"},yaxisTitle:{type:String,required:!1,default:"Access time (ms)"},colorOnline:{type:String,required:!1,default:"111, 176, 129"},colorOffline:{type:String,required:!1,default:"255, 153, 145"},colorNA:{type:String,required:!1,default:"204,204,204"},height:{type:Number,required:!1,default:350},width:{type:Number,required:!1,default:700},week:{type:Boolean,required:!1,default:!1},sixMonths:{type:Boolean,required:!1,default:!1},dataItems:{type:Array,required:!0,validator:function(e){for(let t=0;t<e.length;t++){e[t].hasOwnProperty("access_time")?e[t].hasOwnProperty("date")?e[t].hasOwnProperty("code")||console.error(`[oeb-visualizations warn] code key is missing in dataItems prop item (at position ${t})`):console.error(`[oeb-visualizations warn] date key is missing in dataItems prop item (at position ${t})`):console.error(`[oeb-visualizations warn] access_time key is missing in dataItems prop item (at position ${t})`),null!==e[t].access_time&&"number"!=typeof e[t].access_time&&console.error(`[oeb-visualizations warn] access_time must be null or a number in dataItems prop item (at position ${t})`),null!==e[t].code&&"number"!=typeof e[t].code&&console.error(`[oeb-visualizations warn] code must be null or a number in dataItems prop item (at position ${t})`);const i="string"==typeof e[t].date&&!isNaN(Date.parse(e[t].date)),s="number"==typeof e[t].date;i||s||(console.error(`[oeb-visualizations warn] date must be a string containing a date or a number in dataItems prop item (at position ${t})`),console.error(`[oeb-visualizations warn] date type is ${typeof e[t].date} and the value is ${e[t].date}`)),null===e[t].date&&console.error(`[oeb-visualizations warn] date cannot be null in dataItems prop item (at position ${t})`)}return!0}}},mounted(){const e=this.dataItems.map((e=>e.access_time)).filter((e=>"number"==typeof e&&!isNaN(e)));this.max_access_time=e.length?Math.max(...e):100;var t=this.buildOnlineTraces(this.dataItems);t=t.concat(this.buildOfflineNATraces(this.dataItems));const i={bargap:0,barmode:"stack",showlegend:!0,autosize:!0,height:this.height,width:this.width,margin:{l:50,r:50,b:50,t:20,pad:4},xaxis:{type:"date",ticklabelmode:this.xaxisMode(),title:this.xaxisTitle,font:{size:10},tickfont:{size:10},showgrid:!!this.sixMonths,griddash:"dot",gridwidth:1,gridcolor:"#d9d7d7",showspikes:!0,spikedash:"4px",spikethickness:1,tick0:this.xaxisTickZero(),dtick:this.xaxisTickD(),tickangle:this.xaxisTickAngle(),tickformat:this.xaxisTickFormat(),tickvals:this.sixMonths?this.monthTickVales():null,range:this.xaxisRange()},yaxis:{title:this.yaxisTitle,titlefont:{size:10},tickfont:{size:10}},template:"plotly_white",legend:{orientation:"h",yanchor:"bottom",y:-.5,xanchor:"left",x:.05,font:{size:8}},hoverlabel:{bgcolor:"#FFF"},hovermode:"closest"};console.log(this.divId),s.default.newPlot(this.divId,t,i,this.config)},methods:{generateColor(e,t){let i=[];for(let s=0;s<30;s++)switch(e[s]){case"up":i.push(`rgba(${this.colorOnline},0)`);break;case"down":i.push(`rgba(${this.colorOffline},${t})`);break;case"NA":i.push(`rgba(${this.colorNA},${t})`)}return i},extractSubarraysBetweenNullValues(e){var t={access_time:[],date:[],average_access_time:0},i=[],s=[],a=0,r=0;for(let o=0;o<e.length;o++)null===e[o].access_time||this.isErrorCode(e[o].code)?i.length>0&&(t.access_time.push(i),t.date.push(s),i=[],s=[]):(i.push(e[o].access_time),s.push(this.dayCenter(e[o].date)),a+=e[o].access_time,r+=1);return t.access_time.push(i),t.date.push(s),t.average_access_time=a/r,t},buildAccessTimeTraces(e){var t=[];for(let i=0;i<e.access_time.length;i++){const s=e.access_time[i],a={x:e.date[i],y:s,name:"Online",legendgroup:"up",showlegend:0===i,mode:"markers+lines",type:"scatter",fill:"tozeroy",fillcolor:`rgba(${this.colorOnline},.2)`,connectgaps:!1,line:{color:`rgba(${this.colorOnline},.8)`,width:1.5},marker:{size:5},hovertemplate:"<b>Online</b><br>%{x|%d %b %Y}<br>%{y} ms <extra></extra>",hoveron:"points+fills"};t.push(a)}return t},buildAvgAccessTimeTrace(e){const t=new Date(this.dataItems[0].date);this.week?t.setDate(t.getDate()-.3):t.setDate(t.getDate()-1);const i=new Date(this.dataItems[this.dataItems.length-1].date);this.week?i.setDate(i.getDate()+.3):i.setDate(i.getDate()+1);return{x:[t,i],y:[e,e],name:"Average access time",showlegend:!0,mode:"lines",type:"scatter",line:{color:`rgba(${this.colorOnline},.6)`,width:1.5,dash:"4px"},marker:{size:5},hovertemplate:"<b>Average access time</b><br>%{y:.2f} ms <extra></extra>"}},buildOnlineTraces(e){const t=this.extractSubarraysBetweenNullValues(e);var i=this.buildAccessTimeTraces(t);return i.push(this.buildAvgAccessTimeTrace(t.average_access_time)),i},isErrorCode:e=>[400,403,404,408,500,502,503,504].includes(e),dayCenter(e){const t=new Date(e);return t.setUTCHours(0,0,0,0),t.getTime()+432e5},computeDaySlots(e){return e.map((e=>({center:this.dayCenter(e.date),width:864e5})))},extractOfflineNADates(e){const t=this.computeDaySlots(e),i=[],s=[];for(let a=0;a<e.length;a++)null===e[a].code?i.push(t[a]):this.isErrorCode(e[a].code)&&s.push(t[a]);return{NA:i,down:s}},barTrace:(e,t,i,s,a,r)=>({x:e.dates,y:e.access_times,marker:{color:e.colors},name:t,type:"bar",legendgroup:r,showlegend:i,hoverinfo:s,hovertemplate:a,width:e.widths}),buildBarTraces(e,t,i,s){const a=`rgba(${t},.8)`,r=`rgba(${t},.2)`,o=e.map((e=>e.center)),n=e.map((e=>e.width)),l=2+1.1*this.max_access_time,d=.03*l,c={dates:o,access_times:Array(e.length).fill(d),colors:Array(e.length).fill(a),widths:n},h={dates:o,access_times:Array(e.length).fill(l-d),colors:Array(e.length).fill(r),widths:n},u=`<b>${i}</b><br>%{x|%d %b %Y}<br>%{y} ms <extra></extra>`;return[this.barTrace(c,i,!1,"skip","",s),this.barTrace(h,i,!0,"all",u,s)]},buildOfflineNATraces(e){var t=[];const i=this.extractOfflineNADates(e);return i.down.length>0&&(t=t.concat(this.buildBarTraces(i.down,this.colorOffline,"Offline","down"))),i.NA.length>0&&(t=t.concat(this.buildBarTraces(i.NA,this.colorNA,"No information available","na"))),t},xaxisTickFormat(){return this.week?"%A<br>%d %b":(this.sixMonths,"%d %b")},xaxisTickAngle(){return this.week?0:(this.sixMonths,45)},xaxisTickD(){return!0===this.sixMonths?"M1":this.dtick},xaxisTickZero(){if(!0===this.sixMonths){console.log("zero six months ago");const e=new Date(this.dataItems[this.dataItems.length-1].date);return console.log(e),console.log("six months ago: "+new Date(e.setMonth(e.getMonth()-6))),new Date(e.setMonth(e.getMonth()-6))}return this.dayCenter(this.dataItems[0].date)},xaxisMode(){return!0===this.sixMonths?"period":"instant"},xaxisRange(){if(this.xrange)return this.xrange;const e=new Date(this.dataItems[0].date);e.setUTCHours(0,0,0,0);const t=new Date(this.dataItems[this.dataItems.length-1].date);return t.setUTCHours(0,0,0,0),[e.getTime(),t.getTime()+864e5]},monthTickVales(){const e=new Date(this.dataItems[this.dataItems.length-1].date),t=e.getMonth(),i=e.getFullYear(),s=[];for(let e=0;e<6;e++)s.push(new Date(i,t-e,1));return s}}},undefined,!1,undefined,void 0,void 0);var n=r({render:function(){var e=this,t=e.$createElement;return(e._self._c||t)("div",{attrs:{id:e.divId}})},staticRenderFns:[]},undefined,{name:"citationsPlot",data:()=>({divId:a("cit_plot_")}),props:{dataTraces:{type:Array,required:!0},stack:{type:Boolean,required:!1,default:!1},colors:{type:Array,required:!1,default:()=>["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd"]},height:{type:Number,required:!1,default:400},showlegend:{type:Boolean,required:!1,default:!0},title:{type:String,required:!1,default:""},xaxisTitle:{type:String,required:!1,default:"Year"},yaxisTitle:{type:String,required:!1,default:"Number of citations"},darkMode:{type:Boolean,required:!1,default:!1}},mounted(){const e=this.buildTraces(),t={showlegend:this.showlegend,autosize:!0,height:this.height,margin:{l:50,r:50,b:70,t:50,pad:4},xaxis:{title:this.xaxisTitle,font:{size:10},tickfont:{size:10},tickmode:"linear",color:this.darkMode?"white":"black"},yaxis:{title:this.yaxisTitle,titlefont:{size:10},tickfont:{size:10},font:{size:10},color:this.darkMode?"white":"black"},legend:{orientation:"h",yanchor:"bottom",y:1.02,xanchor:"right",x:1,font:{size:8,color:this.darkMode?"white":"black"}},hoverlabel:{color:this.darkMode?"white":"black"},hovermode:this.stack?"x unified":"closest",hoverdistance:70,plot_bgcolor:this.darkMode?"rgb(38, 50, 56)":"white",paper_bgcolor:this.darkMode?"rgb(38, 50, 56)":"white"};s.default.newPlot(this.divId,e,t)},methods:{buildTraces(){const e=[];for(let t=0;t<this.dataTraces.length;t++){const i={x:this.dataTraces[t].data.map((e=>e.year)),y:this.dataTraces[t].data.map((e=>e.citations)),mode:"lines+markers",name:this.dataTraces[t].label,hovertemplate:this.hoverTemplate(),marker:{size:5},line:{color:this.colors[t],width:1.8},stackgroup:this.stack?"one":null};e.push(i)}return e},hoverTemplate(){return this.stack?"%{y} citations <extra></extra>":"%{y} citations in %{x} <extra></extra>"}}},undefined,!1,undefined,void 0,void 0),l=Object.freeze({__proto__:null,accessibilityPlot:o,citationsPlot:n});const d=function(e){d.installed||(d.installed=!0,Object.entries(l).forEach((([t,i])=>{e.component(t,i)})))},c={install:d};{let e=null;"undefined"!=typeof window?e=window.Vue:"undefined"!=typeof global&&(e=global.Vue),e&&e.use(c)}return e.accessibilityPlot=o,e.citationsPlot=n,e.default=c,Object.defineProperty(e,"__esModule",{value:!0}),e}({},Plotly);
|
|
@@ -13,6 +13,13 @@ var script$1 = {
|
|
|
13
13
|
}
|
|
14
14
|
}),
|
|
15
15
|
props: {
|
|
16
|
+
xrange: {
|
|
17
|
+
/*
|
|
18
|
+
xrange is the range of the x axis in ms.
|
|
19
|
+
*/
|
|
20
|
+
type: Array,
|
|
21
|
+
required: false
|
|
22
|
+
},
|
|
16
23
|
dtick: {
|
|
17
24
|
/*
|
|
18
25
|
dtick is the interval between ticks on the x axis in ms.
|
|
@@ -72,6 +79,14 @@ var script$1 = {
|
|
|
72
79
|
required: false,
|
|
73
80
|
default: 350
|
|
74
81
|
},
|
|
82
|
+
width: {
|
|
83
|
+
/*
|
|
84
|
+
width is the width of the plot in px.
|
|
85
|
+
*/
|
|
86
|
+
type: Number,
|
|
87
|
+
required: false,
|
|
88
|
+
default: 700
|
|
89
|
+
},
|
|
75
90
|
week: {
|
|
76
91
|
/*
|
|
77
92
|
whether the plot is used to show data of one week
|
|
@@ -123,9 +138,12 @@ var script$1 = {
|
|
|
123
138
|
if (value[i].code !== null && typeof value[i].code !== 'number') {
|
|
124
139
|
console.error(`[oeb-visualizations warn] code must be null or a number in dataItems prop item (at position ${i})`);
|
|
125
140
|
}
|
|
126
|
-
// Date must be a string containing a date
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
// Date must be a string containing a parseable date, or a number (timestamp)
|
|
142
|
+
const validDateString = typeof value[i].date === 'string' && !isNaN(Date.parse(value[i].date));
|
|
143
|
+
const validDateNumber = typeof value[i].date === 'number';
|
|
144
|
+
if (!validDateString && !validDateNumber) {
|
|
145
|
+
console.error(`[oeb-visualizations warn] date must be a string containing a date or a number in dataItems prop item (at position ${i})`);
|
|
146
|
+
console.error(`[oeb-visualizations warn] date type is ${typeof value[i].date} and the value is ${value[i].date}`);
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
// And date cannot be null
|
|
@@ -138,7 +156,13 @@ var script$1 = {
|
|
|
138
156
|
}
|
|
139
157
|
},
|
|
140
158
|
mounted() {
|
|
141
|
-
|
|
159
|
+
// Compute the tallest online bar from real measurements only.
|
|
160
|
+
// Math.max over an array containing null coerces null -> 0, so a site
|
|
161
|
+
// that was down the whole window would yield 0 and render the
|
|
162
|
+
// offline/NA bars with zero height (invisible). Ignore nulls/NaN and
|
|
163
|
+
// apply a floor so down-only sites still draw full-height bars.
|
|
164
|
+
const times = this.dataItems.map(item => item.access_time).filter(t => typeof t === 'number' && !isNaN(t));
|
|
165
|
+
this.max_access_time = times.length ? Math.max(...times) : 100;
|
|
142
166
|
var traces = this.buildOnlineTraces(this.dataItems); // generate line traces
|
|
143
167
|
|
|
144
168
|
traces = traces.concat(this.buildOfflineNATraces(this.dataItems)); // generate bar traces
|
|
@@ -149,6 +173,7 @@ var script$1 = {
|
|
|
149
173
|
showlegend: true,
|
|
150
174
|
autosize: true,
|
|
151
175
|
height: this.height,
|
|
176
|
+
width: this.width,
|
|
152
177
|
margin: {
|
|
153
178
|
l: 50,
|
|
154
179
|
r: 50,
|
|
@@ -176,7 +201,9 @@ var script$1 = {
|
|
|
176
201
|
tick0: this.xaxisTickZero(),
|
|
177
202
|
dtick: this.xaxisTickD(),
|
|
178
203
|
tickangle: this.xaxisTickAngle(),
|
|
179
|
-
tickformat: this.xaxisTickFormat()
|
|
204
|
+
tickformat: this.xaxisTickFormat(),
|
|
205
|
+
tickvals: this.sixMonths ? this.monthTickVales() : null,
|
|
206
|
+
range: this.xaxisRange()
|
|
180
207
|
},
|
|
181
208
|
yaxis: {
|
|
182
209
|
title: this.yaxisTitle,
|
|
@@ -253,10 +280,16 @@ var script$1 = {
|
|
|
253
280
|
var sum = 0;
|
|
254
281
|
var nonNullValues = 0;
|
|
255
282
|
for (let i = 0; i < data.length; i++) {
|
|
256
|
-
|
|
257
|
-
|
|
283
|
+
// Only genuine successful measurements belong on the online
|
|
284
|
+
// line. An error code with a recorded access_time must not
|
|
285
|
+
// sneak in (it would draw green and skew the average); treat it
|
|
286
|
+
// as a break, just like a null access_time.
|
|
287
|
+
if (data[i].access_time !== null && !this.isErrorCode(data[i].code)) {
|
|
288
|
+
// keep adding to subarray. Snap the x to the day centre so
|
|
289
|
+
// the online marker sits in the middle of its day, on the
|
|
290
|
+
// same grid as the offline/NA bars.
|
|
258
291
|
subarrayTime.push(data[i].access_time);
|
|
259
|
-
subarrayDate.push(data[i].date);
|
|
292
|
+
subarrayDate.push(this.dayCenter(data[i].date));
|
|
260
293
|
sum += data[i].access_time;
|
|
261
294
|
nonNullValues += 1;
|
|
262
295
|
} else {
|
|
@@ -380,16 +413,71 @@ var script$1 = {
|
|
|
380
413
|
traces.push(this.buildAvgAccessTimeTrace(subarrays.average_access_time));
|
|
381
414
|
return traces;
|
|
382
415
|
},
|
|
416
|
+
isErrorCode(code) {
|
|
417
|
+
/*
|
|
418
|
+
Returns true if the HTTP code denotes the server being offline.
|
|
419
|
+
Single source of truth shared by the online-line and offline-bar
|
|
420
|
+
logic so redness is decided consistently by the response code.
|
|
421
|
+
*/
|
|
422
|
+
const errorCodes = [400, 403, 404, 408, 500, 502, 503, 504];
|
|
423
|
+
return errorCodes.includes(code);
|
|
424
|
+
},
|
|
425
|
+
dayCenter(date) {
|
|
426
|
+
/*
|
|
427
|
+
Snaps a date to the centre (12:00) of its UTC calendar day, returned
|
|
428
|
+
as ms since epoch. The whole chart is a daily grid: null placeholders
|
|
429
|
+
arrive at 00:00Z and real probes mid-afternoon, so aligning every
|
|
430
|
+
point and every bar to the day centre keeps them on one grid. That is
|
|
431
|
+
what makes blocks sit centred on their day and prevents bars from
|
|
432
|
+
bleeding into a neighbouring day's measurement.
|
|
433
|
+
*/
|
|
434
|
+
const d = new Date(date);
|
|
435
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
436
|
+
return d.getTime() + 1000 * 3600 * 24 / 2;
|
|
437
|
+
},
|
|
438
|
+
computeDaySlots(data) {
|
|
439
|
+
/*
|
|
440
|
+
Returns, for every data point, the bar slot it should occupy: a full
|
|
441
|
+
calendar day centred on that day (see dayCenter). Consecutive
|
|
442
|
+
offline/NA days therefore tile into a single block, and because every
|
|
443
|
+
online point is snapped to the same daily grid a bar never overlaps a
|
|
444
|
+
measurement.
|
|
445
|
+
Arguments:
|
|
446
|
+
- data: array of objects with a "date" key
|
|
447
|
+
Returns an array (parallel to data) of objects with keys:
|
|
448
|
+
- center: slot centre (day centre), in ms since epoch
|
|
449
|
+
- width: slot width, in ms (one full day)
|
|
450
|
+
*/
|
|
451
|
+
const DAY = 1000 * 3600 * 24;
|
|
452
|
+
return data.map(item => ({
|
|
453
|
+
center: this.dayCenter(item.date),
|
|
454
|
+
width: DAY
|
|
455
|
+
}));
|
|
456
|
+
},
|
|
383
457
|
extractOfflineNADates(data) {
|
|
458
|
+
/*
|
|
459
|
+
This function extracts the bar slots of points with no successful
|
|
460
|
+
measurement. Depending on the code, the server is offline or NA on
|
|
461
|
+
those dates.
|
|
462
|
+
Arguments:
|
|
463
|
+
- data: array of objects with keys "access_time", "date" and "code". access_time and code can be null
|
|
464
|
+
Returns an object with keys:
|
|
465
|
+
- NA: array of slots (see computeDaySlots) for code null (no information available)
|
|
466
|
+
- down: array of slots for code in errorCodes (server offline)
|
|
467
|
+
Classification is driven by the HTTP code, not by access_time
|
|
468
|
+
null-ness: an error response that happened to record an access_time
|
|
469
|
+
is still "offline", and a missing code is "no information available".
|
|
470
|
+
*/
|
|
471
|
+
const slots = this.computeDaySlots(data);
|
|
384
472
|
const resultNA = [];
|
|
385
473
|
const resultOffline = [];
|
|
386
474
|
for (let i = 0; i < data.length; i++) {
|
|
387
|
-
//
|
|
388
|
-
if (data[i].
|
|
389
|
-
resultNA.push(
|
|
390
|
-
//
|
|
391
|
-
} else if (data[i].
|
|
392
|
-
resultOffline.push(
|
|
475
|
+
// no code at all -> grey "No information available"
|
|
476
|
+
if (data[i].code === null) {
|
|
477
|
+
resultNA.push(slots[i]);
|
|
478
|
+
// an error code -> red "Offline"
|
|
479
|
+
} else if (this.isErrorCode(data[i].code)) {
|
|
480
|
+
resultOffline.push(slots[i]);
|
|
393
481
|
}
|
|
394
482
|
}
|
|
395
483
|
return {
|
|
@@ -401,10 +489,12 @@ var script$1 = {
|
|
|
401
489
|
/*
|
|
402
490
|
This function builds a bar trace.
|
|
403
491
|
Arguments:
|
|
404
|
-
- data: object with keys "dates" and "
|
|
405
|
-
- dates: array of dates
|
|
492
|
+
- data: object with keys "dates", "access_times", "colors" and "widths"
|
|
493
|
+
- dates: array of dates (slot centres, in ms)
|
|
406
494
|
- access_times: array of access_times
|
|
407
495
|
- colors: array of colors in rgba format (eg:"rgba(255, 153, 145,0.8)")
|
|
496
|
+
- widths: array of per-bar widths in ms, so each bar fills its
|
|
497
|
+
own time slot (see computeDaySlots) instead of a fixed day
|
|
408
498
|
- name: name of the trace
|
|
409
499
|
*/
|
|
410
500
|
const trace = {
|
|
@@ -419,35 +509,43 @@ var script$1 = {
|
|
|
419
509
|
showlegend: showlegend,
|
|
420
510
|
hoverinfo: hoverinfo,
|
|
421
511
|
hovertemplate: hovertemplate,
|
|
422
|
-
width:
|
|
512
|
+
width: data.widths
|
|
423
513
|
};
|
|
424
|
-
|
|
425
|
-
console.log(trace);
|
|
426
514
|
return trace;
|
|
427
515
|
},
|
|
428
|
-
buildBarTraces(
|
|
516
|
+
buildBarTraces(slots, color, label, group) {
|
|
429
517
|
/*
|
|
430
518
|
This function builds the series used to display the offline/NA bars.
|
|
431
|
-
For each
|
|
432
|
-
|
|
433
|
-
|
|
519
|
+
For each slot two stacked columns are created that together span the
|
|
520
|
+
whole plot height: a thin strong-coloured band sitting on the axis,
|
|
521
|
+
and a light-coloured column filling the rest of the height above it.
|
|
434
522
|
Arguments:
|
|
435
|
-
-
|
|
523
|
+
- slots: array of slots ({ center, width }), see computeDaySlots
|
|
436
524
|
- color: color of the bars
|
|
437
525
|
Returns array with offline and NA traces
|
|
438
526
|
*/
|
|
439
527
|
|
|
440
528
|
const colorStrong = `rgba(${color},.8)`;
|
|
441
529
|
const colorLight = `rgba(${color},.2)`;
|
|
530
|
+
const centers = slots.map(s => s.center);
|
|
531
|
+
const widths = slots.map(s => s.width);
|
|
532
|
+
|
|
533
|
+
// Total background height (keeps the previous y-axis extent). The
|
|
534
|
+
// strong colour is a thin band on the axis; the light colour fills
|
|
535
|
+
// the remaining height stacked on top of it.
|
|
536
|
+
const totalHeight = 2 + this.max_access_time * 1.1;
|
|
537
|
+
const baseHeight = totalHeight * 0.03;
|
|
442
538
|
const dataShort = {
|
|
443
|
-
dates:
|
|
444
|
-
access_times: Array(
|
|
445
|
-
colors: Array(
|
|
539
|
+
dates: centers,
|
|
540
|
+
access_times: Array(slots.length).fill(baseHeight),
|
|
541
|
+
colors: Array(slots.length).fill(colorStrong),
|
|
542
|
+
widths: widths
|
|
446
543
|
};
|
|
447
544
|
const dataTall = {
|
|
448
|
-
dates:
|
|
449
|
-
access_times: Array(
|
|
450
|
-
colors: Array(
|
|
545
|
+
dates: centers,
|
|
546
|
+
access_times: Array(slots.length).fill(totalHeight - baseHeight),
|
|
547
|
+
colors: Array(slots.length).fill(colorLight),
|
|
548
|
+
widths: widths
|
|
451
549
|
};
|
|
452
550
|
const hovertemplate = `<b>${label}</b><br>%{x|%d %b %Y}<br>%{y} ms <extra></extra>`;
|
|
453
551
|
const traceShort = this.barTrace(dataShort, label, false, 'skip', '', group);
|
|
@@ -469,12 +567,10 @@ var script$1 = {
|
|
|
469
567
|
// Down arrays
|
|
470
568
|
if (arrays.down.length > 0) {
|
|
471
569
|
traces = traces.concat(this.buildBarTraces(arrays.down, this.colorOffline, 'Offline', 'down'));
|
|
472
|
-
console.log(arrays.down);
|
|
473
570
|
}
|
|
474
571
|
// NA arrays
|
|
475
572
|
if (arrays.NA.length > 0) {
|
|
476
573
|
traces = traces.concat(this.buildBarTraces(arrays.NA, this.colorNA, 'No information available', 'na'));
|
|
477
|
-
console.log(arrays.NA);
|
|
478
574
|
}
|
|
479
575
|
return traces;
|
|
480
576
|
},
|
|
@@ -489,7 +585,7 @@ var script$1 = {
|
|
|
489
585
|
return "%A<br>%d %b";
|
|
490
586
|
}
|
|
491
587
|
if (this.sixMonths) {
|
|
492
|
-
return "%b";
|
|
588
|
+
return "%d %b";
|
|
493
589
|
} else {
|
|
494
590
|
return "%d %b";
|
|
495
591
|
}
|
|
@@ -504,7 +600,7 @@ var script$1 = {
|
|
|
504
600
|
return 0;
|
|
505
601
|
}
|
|
506
602
|
if (this.sixMonths) {
|
|
507
|
-
return
|
|
603
|
+
return 45;
|
|
508
604
|
} else {
|
|
509
605
|
return 45;
|
|
510
606
|
}
|
|
@@ -525,7 +621,10 @@ var script$1 = {
|
|
|
525
621
|
console.log('six months ago: ' + new Date(lastDate.setMonth(lastDate.getMonth() - 6)));
|
|
526
622
|
return new Date(lastDate.setMonth(lastDate.getMonth() - 6));
|
|
527
623
|
} else {
|
|
528
|
-
|
|
624
|
+
// Anchor day ticks at the day centre (noon) so each label sits
|
|
625
|
+
// under the middle of its day block, aligned with the online dot
|
|
626
|
+
// (which is snapped to the same day centre).
|
|
627
|
+
return this.dayCenter(this.dataItems[0].date);
|
|
529
628
|
}
|
|
530
629
|
},
|
|
531
630
|
xaxisMode() {
|
|
@@ -534,6 +633,30 @@ var script$1 = {
|
|
|
534
633
|
} else {
|
|
535
634
|
return "instant";
|
|
536
635
|
}
|
|
636
|
+
},
|
|
637
|
+
xaxisRange() {
|
|
638
|
+
if (this.xrange) {
|
|
639
|
+
return this.xrange;
|
|
640
|
+
}
|
|
641
|
+
// Span whole calendar days so the first and last day blocks (which
|
|
642
|
+
// are centred on their day, one full day wide) are fully visible.
|
|
643
|
+
const DAY = 1000 * 3600 * 24;
|
|
644
|
+
const firstDay = new Date(this.dataItems[0].date);
|
|
645
|
+
firstDay.setUTCHours(0, 0, 0, 0);
|
|
646
|
+
const lastDay = new Date(this.dataItems[this.dataItems.length - 1].date);
|
|
647
|
+
lastDay.setUTCHours(0, 0, 0, 0);
|
|
648
|
+
return [firstDay.getTime(), lastDay.getTime() + DAY];
|
|
649
|
+
},
|
|
650
|
+
monthTickVales() {
|
|
651
|
+
// First day of month of last date and previous five months
|
|
652
|
+
const lastDate = new Date(this.dataItems[this.dataItems.length - 1].date);
|
|
653
|
+
const lastMonth = lastDate.getMonth();
|
|
654
|
+
const lastYear = lastDate.getFullYear();
|
|
655
|
+
const tickValues = [];
|
|
656
|
+
for (let i = 0; i < 6; i++) {
|
|
657
|
+
tickValues.push(new Date(lastYear, lastMonth - i, 1));
|
|
658
|
+
}
|
|
659
|
+
return tickValues;
|
|
537
660
|
}
|
|
538
661
|
}
|
|
539
662
|
};function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier
|
|
@@ -640,7 +763,7 @@ const __vue_inject_styles__$1 = undefined;
|
|
|
640
763
|
/* scoped */
|
|
641
764
|
const __vue_scope_id__$1 = undefined;
|
|
642
765
|
/* module identifier */
|
|
643
|
-
const __vue_module_identifier__$1 = "data-v-
|
|
766
|
+
const __vue_module_identifier__$1 = "data-v-bab9d544";
|
|
644
767
|
/* functional template */
|
|
645
768
|
const __vue_is_functional_template__$1 = false;
|
|
646
769
|
/* style inject */
|