@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 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
- :construction: This section is under construction. :construction:
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
- ### How to add a new component.
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
- - Files are relative to the package.
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
- ### Documentation:
34
- - How documentation is organised.
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
- ## Publishing
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
- if (typeof value[i].date !== 'string' || isNaN(Date.parse(value[i].date))) {
128
- console.error(`[oeb-visualizations warn] date must be a string containing a date in dataItems prop item (at position ${i})`);
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
- this.max_access_time = Math.max(...this.dataItems.map(item => item.access_time));
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
- if (data[i].access_time !== null) {
257
- // keep adding to subarray
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
- // if the access_time is null and the code is null, it means that the monitoring was down
388
- if (data[i].access_time === null && data[i].code === null) {
389
- resultNA.push(data[i].date);
390
- // if the access_time is null and the code is in errorCodes, it means that the server is offline
391
- } else if (data[i].access_time === null) {
392
- resultOffline.push(data[i].date);
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 "access_times"
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: 1000 * 3600 * 24 * 1 // 100% of the space between two dates
512
+ width: data.widths
423
513
  };
424
-
425
- console.log(trace);
426
514
  return trace;
427
515
  },
428
- buildBarTraces(dates, color, label, group) {
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 date, two columns are created: one very short and one tall.
432
- These columns cover the whole plot height.
433
- The tall column is colored with colorStrong and the short one with colorLight.
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
- - dates: array of dates
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: dates,
444
- access_times: Array(dates.length).fill(2),
445
- colors: Array(dates.length).fill(colorStrong)
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: dates,
449
- access_times: Array(dates.length).fill(this.max_access_time * 1.1),
450
- colors: Array(dates.length).fill(colorLight)
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 0;
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
- return this.dataItems[0];
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-77c84891";
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
- if (typeof value[i].date !== 'string' || isNaN(Date.parse(value[i].date))) {
132
- console.error(`[oeb-visualizations warn] date must be a string containing a date in dataItems prop item (at position ${i})`);
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
- this.max_access_time = Math.max(...this.dataItems.map(item => item.access_time));
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
- if (data[i].access_time !== null) {
261
- // keep adding to subarray
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
- // if the access_time is null and the code is null, it means that the monitoring was down
392
- if (data[i].access_time === null && data[i].code === null) {
393
- resultNA.push(data[i].date);
394
- // if the access_time is null and the code is in errorCodes, it means that the server is offline
395
- } else if (data[i].access_time === null) {
396
- resultOffline.push(data[i].date);
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 "access_times"
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: 1000 * 3600 * 24 * 1 // 100% of the space between two dates
516
+ width: data.widths
427
517
  };
428
-
429
- console.log(trace);
430
518
  return trace;
431
519
  },
432
- buildBarTraces(dates, color, label, group) {
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 date, two columns are created: one very short and one tall.
436
- These columns cover the whole plot height.
437
- The tall column is colored with colorStrong and the short one with colorLight.
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
- - dates: array of dates
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: dates,
448
- access_times: Array(dates.length).fill(2),
449
- colors: Array(dates.length).fill(colorStrong)
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: dates,
453
- access_times: Array(dates.length).fill(this.max_access_time * 1.1),
454
- colors: Array(dates.length).fill(colorLight)
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 0;
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
- return this.dataItems[0];
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
- if (typeof value[i].date !== 'string' || isNaN(Date.parse(value[i].date))) {
128
- console.error(`[oeb-visualizations warn] date must be a string containing a date in dataItems prop item (at position ${i})`);
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
- this.max_access_time = Math.max(...this.dataItems.map(item => item.access_time));
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
- if (data[i].access_time !== null) {
257
- // keep adding to subarray
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
- // if the access_time is null and the code is null, it means that the monitoring was down
388
- if (data[i].access_time === null && data[i].code === null) {
389
- resultNA.push(data[i].date);
390
- // if the access_time is null and the code is in errorCodes, it means that the server is offline
391
- } else if (data[i].access_time === null) {
392
- resultOffline.push(data[i].date);
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 "access_times"
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: 1000 * 3600 * 24 * 1 // 100% of the space between two dates
512
+ width: data.widths
423
513
  };
424
-
425
- console.log(trace);
426
514
  return trace;
427
515
  },
428
- buildBarTraces(dates, color, label, group) {
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 date, two columns are created: one very short and one tall.
432
- These columns cover the whole plot height.
433
- The tall column is colored with colorStrong and the short one with colorLight.
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
- - dates: array of dates
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: dates,
444
- access_times: Array(dates.length).fill(2),
445
- colors: Array(dates.length).fill(colorStrong)
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: dates,
449
- access_times: Array(dates.length).fill(this.max_access_time * 1.1),
450
- colors: Array(dates.length).fill(colorLight)
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 0;
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
- return this.dataItems[0];
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-77c84891";
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 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inb/oeb_visualizations",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Collection of Vue components for data visualization in OpenEBench.",
5
5
  "author": "evamart",
6
6
  "license": "MIT",