tsviewer 0.1.0

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.
@@ -0,0 +1,595 @@
1
+ <template>
2
+ <div
3
+ ref="ts_viewer"
4
+ class="timeseries-viewer"
5
+ >
6
+ <timeseries-scrubber
7
+ ref="scrubber"
8
+ :ts_start="ts_start"
9
+ :ts_end="ts_end"
10
+ :c-width="cWidth"
11
+ :label-width="labelWidth"
12
+ :cursor-loc="cursorLoc"
13
+ :start="start"
14
+ :duration="duration"
15
+ :constants="constants"
16
+ @setStart="updateStart"
17
+ />
18
+
19
+ <div id="channelCanvas">
20
+ <!-- Channel labels -->
21
+ <div
22
+ id="channelLabels"
23
+ ref="channelLabels"
24
+ :style="channelStyle"
25
+ >
26
+ <div
27
+ v-for="item in viewerChannels"
28
+ v-show="item.visible"
29
+ :key="item.label"
30
+ >
31
+ <div
32
+ class="chLabelWrap"
33
+ :data-id="item.id"
34
+ @tap="onLabelTap"
35
+ >
36
+ <div class="labelDiv" :style="_cpStyleLabels">
37
+ {{ item.label }}
38
+ </div>
39
+ <div
40
+ class="chLabelIndWrap"
41
+ v-if="!hideLabelInfo"
42
+ :selected="item.selected"
43
+ >
44
+ <div
45
+ class="chLabelInd"
46
+
47
+ >
48
+ {{ _computeLabelInfo(item, globalZoomMult, item.rowScale) }}
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Timeseries viewport -->
56
+ <timeseries-viewer-canvas
57
+ ref="viewerCanvas"
58
+ :window_height="window_height"
59
+ :window_width="window_width"
60
+ :duration="duration"
61
+ :start="start"
62
+ :c-width="cWidth"
63
+ :c-height="cHeight"
64
+ :constants="constants"
65
+ :ts-start="ts_start"
66
+ :ts-end="ts_end"
67
+ :cursor-loc="cursorLoc"
68
+ :global-zoom-mult="globalZoomMult"
69
+ @setStart="updateStart"
70
+ @setCursor="setCursor"
71
+ @setGlobalZoom="setGlobalZoom"
72
+ @setDuration="setDuration"
73
+ @channelsInitialized="onChannelsInitialized"
74
+ @annLayersInitialized="onAnnLayersInitialized"
75
+ @closeAnnotationLayerWindow="onCloseAnnotationLayerWindow"
76
+ @addAnnotation="onOpenAnnotationWindow"
77
+ @updateAnnotation="onUpdateAnnotation"
78
+ />
79
+ </div>
80
+
81
+ <timeseries-viewer-toolbar
82
+ :constants="constants"
83
+ :duration="duration"
84
+ :start="start"
85
+ @pageBack="onPageBack"
86
+ @pageForward="onPageForward"
87
+ @incrementZoom="onIncrementZoom"
88
+ @decrementZoom="onDecrementZoom"
89
+ @updateDuration="onUpdateDuration"
90
+ @nextAnnotation="onNextAnnotation"
91
+ @previousAnnotation="onPreviousAnnotation"
92
+ @setStart="updateStart"
93
+ />
94
+
95
+ <!-- <timeseries-filter-modal-->
96
+ <!-- ref="filterWindow"-->
97
+ <!-- :filter-window-open="filterWindowOpen"-->
98
+ <!-- @closeWindow="onCloseFilterWindow"-->
99
+ <!-- />-->
100
+
101
+ <!-- <timeseries-annotation-modal-->
102
+ <!-- ref="annotationModal"-->
103
+ <!-- :visible.sync="annotationWindowOpen"-->
104
+ <!-- @closeWindow="onCloseAnnotationWindow"-->
105
+ <!-- @createUpdateAnnotation="onCreateUpdateAnnotation"-->
106
+ <!-- />-->
107
+
108
+ <!-- <timeseries-annotation-layer-modal-->
109
+ <!-- ref="layerModal"-->
110
+ <!-- :annotation-layer-window-open="annotationLayerWindowOpen"-->
111
+ <!-- @closeWindow="onCloseAnnotationLayerWindow"-->
112
+ <!-- @createLayer="onCreateAnnotationLayer"-->
113
+ <!-- />-->
114
+
115
+ </div>
116
+ </template>
117
+
118
+ <script>
119
+ import {
120
+ head,
121
+ propOr,
122
+ isEmpty
123
+ } from 'ramda'
124
+ import ViewerActiveTool from '@/mixins/viewer-active-tool'
125
+ import Request from '@/mixins/request'
126
+ import TsAnnotation from '@/mixins/ts-annotation'
127
+ import TimeseriesScrubber from '@/components/TSScrubber.vue'
128
+ import TimeseriesViewerCanvas from '@/components/TSViewerCanvas.vue'
129
+ import TimeseriesViewerToolbar from '@/components/TSViewerToolbar.vue'
130
+ export default {
131
+ name: 'TimeseriesViewer',
132
+ components:{
133
+ TimeseriesScrubber,
134
+ TimeseriesViewerCanvas,
135
+ TimeseriesViewerToolbar
136
+ //'timeseries-filter-modal': () => import('@/components/viewers/TSViewer/TSFilterModal.vue'),
137
+ //'timeseries-annotation-layer-modal': () => import('@/components/viewers/TSViewer/TSViewerLayerWindow.vue'),
138
+ //'timeseries-annotation-modal': () => import('@/components/viewers/TSViewer/TSAnnotationModal.vue')
139
+ },
140
+ mixins: [
141
+ Request,
142
+ ViewerActiveTool,
143
+ TsAnnotation,
144
+ ],
145
+ watch: {
146
+ viewerSidePanelOpen: {
147
+ handler: function() {
148
+ // console.log('resized triggered')
149
+ this.onResize()
150
+ },
151
+ immediate: true
152
+ },
153
+ userToken: {
154
+ handler: async function(token) {
155
+ await this.$store.dispatch('updateUserToken', token)
156
+ },
157
+ immediate: true
158
+ },
159
+ packageId: {
160
+ handler: async function(id) {
161
+ await this.$store.dispatch('openViewer', {
162
+ packageId: id,
163
+ packageType: this.packageType
164
+ })
165
+ },
166
+ immediate: true
167
+ }
168
+ },
169
+ props: {
170
+ userToken: {
171
+ type: String,
172
+ default: () => ''
173
+ },
174
+ packageId: {
175
+ type: String,
176
+ default: () => ''
177
+ },
178
+ packageType: {
179
+ type: String,
180
+ default: () => ''
181
+ }
182
+ },
183
+ computed: {
184
+ viewerChannels: function() {
185
+ return this.$store.getters.viewerChannels
186
+ },
187
+ viewerAnnotations: function() {
188
+ return this.$store.getters.viewerAnnotations
189
+ },
190
+ viewerSidePanelOpen: function() {
191
+ return this.$store.getters.viewerSidePanelOpen
192
+ },
193
+ viewerMontageScheme: function() {
194
+ return this.$store.getters.viewerMontageScheme
195
+ },
196
+ channelStyle: function() {
197
+ return {
198
+ height: this.window_height - 110 + 'px'
199
+ }
200
+ },
201
+ hideLabelInfo: function() {
202
+ let hide = false;
203
+ const nrChannels = this.nrVisChannels
204
+ if (this.cHeight/nrChannels < 30) {
205
+ hide = true;
206
+ }
207
+ return hide;
208
+ },
209
+ nrVisChannels: function() {
210
+ return this.viewerChannels.reduce((accumulator, currentValue) => {
211
+ if(currentValue.visible) {
212
+ return accumulator + 1
213
+ }
214
+ return accumulator
215
+ }, 0)
216
+ },
217
+ _cpStyleLabels: function() {
218
+ const h = Math.max(1, Math.min(12, (this.window_height - 110)/this.nrVisChannels-2));
219
+ return 'font-size:' + h + 'px; height:' + h + 'px';
220
+ },
221
+ },
222
+ data: function () {
223
+ return {
224
+ constants: {
225
+ TIMEUNIT: 'microSeconds', // Basis for time
226
+ XOFFSET: 0, // X-offset of graph in canvas
227
+ XGRIDSPACING: 1000000, // Time in microseconds between vertical lines
228
+ NRPXPERLABEL: 150, // Number of pixels per label on x-axis
229
+ USEREALTIME: true, // If true than interpret timepoints as UTC microseconds.
230
+ DEFAULTDPI: 96, // Default pixels per inch
231
+ ANNOTATIONLABELHEIGHT: 20, // Height of annotation label
232
+ ROUNDDATAPIXELS: false, // If true, canvas point will be rounded to integer pixels for faster render (faster)
233
+ MINMAXPOLYGON: true, // If true, then polygon is rendered thru minMax values, otherwise vertical lines (faster)
234
+ PAGESIZEDIVIDER: 0.5, // Number of pages that span the current canvas.
235
+ PREFETCHPAGES: 2, // Number of pages to read ahead of view.
236
+ LIMITANNFETCH: 500, // Maximum number of annotations that are fetched per request
237
+ USEMEDIAN: false, // Use Median instead of mean for centering channels
238
+ CURSOROFFSET: 5, // Offset of cursor canvas
239
+ SEGMENTSPAN: 1209600000000, // One week of gap-data is returned per request.
240
+ MAXRECURSION: 20, // Maximum recursion depth of gap-data requests (max 2 years)
241
+ MAXDURATION: 600000000, // Maximum duration window (5min)
242
+ INITDURATION: 15000000 // Initial duration window (15sec)
243
+ },
244
+ summary: {},
245
+ ts_start: null,
246
+ ts_end: null,
247
+ window_height: 0,
248
+ window_width:0,
249
+ start:0, // Start Timestamp of viewer in microseconds
250
+ duration: 0, // Length of data in viewer in microseconds (ignore gaps)
251
+ cWidth: 0,
252
+ cHeight: 0,
253
+ labelWidth:0,
254
+ globalZoomMult: 1,
255
+ cursorLoc: 1/10,
256
+ filterWindowOpen: false,
257
+ annotationWindowOpen: false,
258
+ annotationLayerWindowOpen: false,
259
+ annotationDelete: null,
260
+ isTsAnnotationDeleteDialogVisible: false,
261
+ }
262
+ },
263
+ mounted: function () {
264
+ this.window_height = window.innerHeight - 144 - 250;
265
+ this.window_width = this.$refs.ts_viewer.offsetWidth
266
+ window.addEventListener('resize', this.onResize)
267
+ const labelDiv = this.$refs.channelLabels
268
+ this.labelWidth = labelDiv.clientWidth
269
+ this.cWidth = (this.window_width - labelDiv.clientWidth - 5 - 10)
270
+ this.cHeight = (this.window_height - 88)
271
+ this.duration = this.constants['INITDURATION']
272
+ },
273
+ beforeDestroy() {
274
+ window.removeEventListener('resize', this.onResize)
275
+ },
276
+ methods: {
277
+ openEditAnnotationDialog: function(annotation) {
278
+ this.$store.dispatch('setActiveAnnotation', annotation).then(() =>{
279
+ this.$refs.viewerCanvas.renderAnnotationCanvas()
280
+ this.annotationWindowOpen = true
281
+ })
282
+ },
283
+ onUpdateAnnotation: function(annotation) {
284
+ this.openEditAnnotationDialog(annotation)
285
+ },
286
+ onCreateUpdateAnnotation: function(annotation) {
287
+ this.annotationWindowOpen = false
288
+ if (annotation.id) {
289
+ this.updateAnnotation()
290
+ } else {
291
+ this.addAnnotation()
292
+ }
293
+ },
294
+ onAnnotationUpdated: function() {
295
+ this.$refs.viewerCanvas.renderAnnotationCanvas()
296
+ },
297
+ onOpenAnnotationWindow: function() {
298
+ this.annotationWindowOpen = true
299
+ },
300
+ confirmDeleteAnnotation: function(annotation) {
301
+ this.annotationDelete = annotation
302
+ this.isTsAnnotationDeleteDialogVisible = true
303
+ },
304
+ deleteAnnotation: function(annotation) {
305
+ this.isTsAnnotationDeleteDialogVisible = false
306
+ this.removeAnnotation(annotation)
307
+ },
308
+ onAnnotationDeleted: function() {
309
+ this.$refs.viewerCanvas.renderAnnotationCanvas()
310
+ },
311
+ onAddAnnotation: function (start, duration, onAll, label, description, layer) {
312
+ this.addAnnotation(start, duration, onAll, label, description, layer)
313
+ },
314
+ onAnnotationCreated: function() {
315
+ this.$refs.viewerCanvas.renderAnnotationCanvas()
316
+ },
317
+ onCreateAnnotationLayer: function (newLayer) {
318
+ this.$refs.viewerCanvas.createAnnotationLayer(newLayer)
319
+ },
320
+ onCloseAnnotationLayerWindow: function() {
321
+ this.annotationLayerWindowOpen = false
322
+ },
323
+ onCloseAnnotationWindow: function() {
324
+ this.$refs.viewerCanvas.resetFocusedAnnotation()
325
+ this.$refs.viewerCanvas.renderAnnotationCanvas()
326
+ this.annotationWindowOpen = false
327
+ },
328
+ onCloseFilterWindow: function() {
329
+ this.filterWindowOpen = false
330
+ },
331
+ onLabelTap: function(e) {
332
+ e.stopPropagation();
333
+ e.preventDefault();
334
+ const append = e.detail.sourceEvent.metaKey;
335
+ this.selectChannel({channelId: e.currentTarget.dataset.id, append:append});
336
+ this.$refs.viewerCanvas.renderAll()
337
+ },
338
+ onNextAnnotation: function () {
339
+ this.start = this.$refs.viewerCanvas.getNextAnnotation()
340
+ },
341
+ onPreviousAnnotation: function () {
342
+ this.start = this.$refs.viewerCanvas.getPreviousAnnotation()
343
+ },
344
+ onUpdateDuration: function(value) {
345
+ this.setDuration(value * 1e6)
346
+ },
347
+ onIncrementZoom: function () {
348
+ this.globalZoomMult = this.globalZoomMult * 1.25
349
+ },
350
+ onDecrementZoom: function () {
351
+ this.globalZoomMult = this.globalZoomMult * 0.8
352
+ },
353
+ onAnnLayersInitialized: function () {
354
+ this.$refs.scrubber.getAnnotations()
355
+ },
356
+ onChannelsInitialized: function (channels) {
357
+ this.initViewerStart((channels))
358
+ // TODO: Bring back
359
+ // this.$refs.scrubber.initSegmentSpans()
360
+ // Resize the canvas as label length likely changed
361
+ this.$nextTick(() => {
362
+ this.onResize()
363
+ })
364
+ },
365
+ onPageBack: function() {
366
+ //TODO: Update logic to track gap over all channels
367
+ let setStart = this.start - (3*this.duration)/4
368
+
369
+ //TODO: Bring back when we have segment sections information
370
+ // let channelOneSegments = this.viewerChannels[0].dataSegments
371
+ //
372
+ // let i = 0;
373
+ // for(let segment in channelOneSegments) {
374
+ // if (channelOneSegments[segment] > setStart) {
375
+ // break
376
+ // }
377
+ // i++
378
+ // }
379
+ //
380
+ // // If new page completely in gap --> set start to next timestamp with data
381
+ // if(i % 2 == 0) {
382
+ // setStart = channelOneSegments[i-1] - 0.5*this.duration
383
+ // }
384
+ this.start = setStart
385
+ },
386
+ onPageForward: function() {
387
+ //TODO: Update logic to track gap over all channels
388
+ let setStart = this.start + (3*this.duration)/4
389
+
390
+ //TODO: Bring back when we have segment sections information
391
+ // let channelOneSegments = this.viewerChannels[0].dataSegments
392
+ //
393
+ // let i = 0;
394
+ // for(let segment in channelOneSegments) {
395
+ // if (channelOneSegments[segment] > setStart) {
396
+ // break
397
+ // }
398
+ // i++
399
+ // }
400
+ //
401
+ // // If new page completely in gap --> set start to next timestamp with data
402
+ // if(i % 2 == 0) {
403
+ // setStart = channelOneSegments[i] - 0.5*this.duration
404
+ // }
405
+
406
+ this.start = setStart
407
+ },
408
+ selectAnnotation: function(payload) {
409
+ let rsPeriod = this.$refs.viewerCanvas.rsPeriod
410
+ this.updateStart(payload.annotation.start - ((this.cursorLoc*this.cWidth - this.constants['CURSOROFFSET']) * rsPeriod))
411
+ },
412
+ selectChannel: function(payload) {
413
+ const _channels = this.viewerChannels.map(channel => {
414
+ const selected = channel.selected
415
+ if (payload['append'] === false) {
416
+ channel.selected = false
417
+ }
418
+ if (payload['channelId'] === channel.id) {
419
+ channel.selected = !selected
420
+ }
421
+ return channel
422
+ })
423
+ this.$store.dispatch('setChannels', _channels)
424
+ },
425
+ selectChannels: function(ids, append) {
426
+ const channels = this.viewerChannels.map(channel => {
427
+ if (append === false) {
428
+ channel.selected = false
429
+ }
430
+ if( channel.id in ids) {
431
+ channel.selected = true
432
+ }
433
+ return channel
434
+ })
435
+ this.$store.dispatch('setChannels', channels)
436
+ },
437
+ updateStart: function(value) {
438
+ // console.log('setting start to: ' + value)
439
+ this.start = value
440
+ },
441
+ setCursor: function(value) {
442
+ // set the cursor location as a fraction of the width of the canvas
443
+ this.cursorLoc = value
444
+ },
445
+ setGlobalZoom: function(value) {
446
+ // console.log('setGlobalZoom')
447
+ this.globalZoomMult = value
448
+ },
449
+ setDuration: function(value) {
450
+ if (value > this.constants['MAXDURATION']) {
451
+ this.duration = this.constants['MAXDURATION']
452
+ } else {
453
+ this.duration = value
454
+ }
455
+ },
456
+ getChannelId: function(channel) {
457
+ const isViewingMontage = this.viewerMontageScheme !== 'NOT_MONTAGED'
458
+ let id = propOr('', 'id', channel)
459
+ let list = []
460
+ if (isViewingMontage) {
461
+ list = id.split('_')
462
+ id = list.length ? head(list) : id // remove channel name from id
463
+ }
464
+ return id
465
+ },
466
+ onResize() {
467
+ if (this.$refs.ts_viewer === undefined) {
468
+ return
469
+ }
470
+ this.window_height = window.innerHeight - 144 - 250;
471
+ this.window_width = this.$refs.ts_viewer.offsetWidth
472
+ const labelDiv = this.$refs.channelLabels;
473
+ this.labelWidth = labelDiv.clientWidth
474
+ this.cWidth = (this.window_width - labelDiv.clientWidth - 16);
475
+ this.cHeight = (this.window_height - 88);
476
+ },
477
+ _computeLabelInfo: function(item, globalZoomMult, rowscale) {
478
+ const n = ( ( (this.constants['DEFAULTDPI'] * window.devicePixelRatio)/(globalZoomMult * rowscale) )/25.4).toFixed(1);
479
+ return n+ ' ' + item.unit + '/mm'
480
+ },
481
+ initViewerStart: function(channels) {
482
+ // const channels = this.activeViewer.channels
483
+ if (channels.length > 0) {
484
+ // Find Global start and end
485
+ this.ts_start = channels[0].content.start
486
+ this.ts_end = channels[0].content.end
487
+ for (let ic = 1; ic<channels.length; ic++) {
488
+ if (channels[ic].content.start < this.ts_start) {
489
+ this.ts_start = channels[ic].content.start
490
+ }
491
+ if (channels[ic].content.end > this.ts_end) {
492
+ this.ts_end = channels[ic].content.end
493
+ }
494
+ }
495
+ this.start = this.ts_start
496
+ }
497
+ },
498
+ openLayerWindow: function(payload) {
499
+ const layerModal = this.$refs.layerModal
500
+ layerModal.isCreating = payload.isCreating
501
+ if (!payload.isCreating) {
502
+ layerModal.layer = payload.layer
503
+ } else {
504
+ layerModal.layer = {}
505
+ // layerModal.setColorByIndex(this.viewerAnnotations.length % layerModal.colorOptions.length)
506
+ }
507
+ this.annotationLayerWindowOpen = true
508
+ },
509
+ openFilterWindow: function(payload) {
510
+ const channels = propOr([], 'channels', payload);
511
+ const filter = propOr('', 'filter', payload);
512
+ const filterWindow = this.$refs.filterWindow;
513
+ filterWindow.onChannels = channels;
514
+ if (!isEmpty(filter)) {
515
+ filterWindow.input0 = filter.input0;
516
+ filterWindow.input1 = filter.input1;
517
+ for (let i=0; i<filterWindow._filters.length; i++) {
518
+ if (filterWindow._filters[i].value === filter.type) {
519
+ filterWindow.selectedFilter = filter.type;
520
+ break;
521
+ }
522
+ }
523
+ for (let i=0; i<filterWindow._notchValues.length; i++) {
524
+ if (filterWindow._notchValues[i].value === filter.notchFreq) {
525
+ filterWindow.selectedNotch = filter.notchFreq;
526
+ break;
527
+ }
528
+ }
529
+ } else {
530
+ filterWindow.input0 = NaN;
531
+ filterWindow.input1 = NaN;
532
+ filterWindow.selectedFilter = null;
533
+ filterWindow.selectedNotch = null;
534
+ }
535
+ this.filterWindowOpen = true
536
+ },
537
+ setTimeseriesFilters: function(payload) {
538
+ this.$refs.viewerCanvas.setFilters(payload)
539
+ }
540
+ }
541
+ }
542
+ </script>
543
+
544
+ <style lang="scss" scoped>
545
+ .timeseries-viewer {
546
+ display: flex;
547
+ flex-direction: column;
548
+ }
549
+ #channelCanvas {
550
+ display: flex;
551
+ height:100%;
552
+ background-color: white;
553
+ //flex: 1;
554
+ }
555
+ #channelLabels {
556
+ display: flex;
557
+ flex-direction: column;
558
+ justify-content: space-around;
559
+ line-height: normal;
560
+ margin-bottom: 40px;
561
+ min-width: 75px;
562
+ }
563
+ .chLabelWrap {
564
+ display: flex;
565
+ flex-direction: column;
566
+ align-items: center;
567
+ cursor: pointer;
568
+ }
569
+ .chLabelIndWrap {
570
+ position: relative;
571
+ display: flex;
572
+ flex-direction: row;
573
+ justify-content: space-around;
574
+ width: 100%;
575
+ color: #3c54a4;
576
+ }
577
+ .chLabelInd {
578
+ font-size: 0.6em;
579
+ min-width: 70px;
580
+ color: rgb(150,150,150);
581
+ text-align: right;
582
+ white-space: nowrap;
583
+ }
584
+ .labelDiv[selected] {
585
+ color:#295eff; /*#ff9800;/*#358855;*/
586
+ }
587
+ .chLabelIndWrap[selected]{
588
+ color:#295eff; /*#ff9800; /*#358855;*/
589
+ }
590
+ .labelDiv {
591
+ align-self: flex-end;
592
+ white-space: nowrap;
593
+ color: var(--neuron);
594
+ }
595
+ </style>