CorrelationAnalysis.vue 18.1 KB
Newer Older
1
<template>
2
  <div :class="`fjs-vm-root fjs-vm-root-${this._uid}`">
3

4
    <div class="fjs-data-box-section">
5
6
7
8
9
10
11
12
      <data-box header="X and Y variables"
                dataType="numerical"
                v-on:update="update_xyData">
      </data-box>
      <data-box header="Annotations"
                dataType="categorical"
                v-on:update="update_annotationData">
      </data-box>
13
    </div>
14

15
16
    <div class="fjs-controls-section">
      <button class="fjs-run-analysis-btn"
17
              type="button"
18
              @click="runAnalysisWrapper({init: true, args})"
19
              :disabled="disabled">&#9654;</button><br/>
20
21
22
      <br/>
      <span>{{ error }}</span>
    </div>
23

24
25
    <div class="fjs-visualisation-section">
      <table class="fjs-stats-table" v-show="!shownAnalysisResults.init">
26
27
28
29
30
31
32
33
        <tr>
          <td>Corr. Coef.</td>
          <td>{{ tmpAnalysisResults.coef }}</td>
        </tr>
        <tr>
          <td>p-value</td>
          <td>{{ tmpAnalysisResults.p_value }}</td>
        </tr>
Sascha Herzinger's avatar
Sascha Herzinger committed
34
35
36
37
38
39
        <tr>
          <td>Correlation statistic</td>
          <td>{{ tmpAnalysisResults.method }}</td>
        </tr>
        <tr>
          <td>#Selected points</td>
40
          <td>{{ tmpPoints.xs.length }}</td>
Sascha Herzinger's avatar
Sascha Herzinger committed
41
42
        </tr>
        <tr>
Sascha Herzinger's avatar
Sascha Herzinger committed
43
          <td>#Displayed points</td>
Sascha Herzinger's avatar
Sascha Herzinger committed
44
45
          <td>{{ shownPoints.all.length }}</td>
        </tr>
46
      </table>
47
48
49
      <svg :width="width"
           :height="height"
           v-show="!shownAnalysisResults.init">
50
        <g :transform="`translate(${margin.left}, ${margin.top})`">
51
52
53
54
55
          <g class="fjs-corr-axis fjs-x-axis-1" :transform="`translate(0, ${padded.height})`"></g>
          <g class="fjs-corr-axis fjs-x-axis-2"></g>
          <g class="fjs-corr-axis fjs-y-axis-1"></g>
          <g class="fjs-corr-axis fjs-y-axis-2" :transform="`translate(${padded.width}, 0)`"></g>
          <g class="fjs-brush"></g>
56
57
58
59
60
61
62
63
64
65
66
67
68
          <text :x="padded.width / 2"
                y="-10"
                text-anchor="middle"
                font-size="16">
            {{ shownAnalysisResults.x_label }}
          </text>
          <text :x="padded.width + 10"
                :y="padded.height / 2"
                text-anchor="middle"
                font-size="16"
                :transform="`rotate(90 ${padded.width + 10} ${padded.height / 2})`">
            {{ shownAnalysisResults.y_label }}
          </text>
Sascha Herzinger's avatar
Sascha Herzinger committed
69
          <icon class="fjs-scatterplot-point"
70
                :shape="point.subset"
Sascha Herzinger's avatar
Sascha Herzinger committed
71
72
                :cx="scales.x(point.x)"
                :cy="scales.y(point.y)"
73
                :size="9"
Sascha Herzinger's avatar
Sascha Herzinger committed
74
75
76
77
                v-for="point in shownPoints.all"
                :key="point.id"
                v-svgtooltip="point.tooltip">
          </icon>
78
          <line class="fjs-lin-reg-line"
79
80
81
                :x1="tweened.regLine.x1"
                :x2="tweened.regLine.x2"
                :y1="tweened.regLine.y1"
Sascha Herzinger's avatar
Sascha Herzinger committed
82
                :y2="tweened.regLine.y2"
83
                v-svgtooltip="regLine.tooltip">
84
          </line>
85
          <rect class="fjs-histogram-rect"
86
87
88
89
90
                :x="attr.x"
                :y="attr.y"
                :width="attr.width"
                :height="attr.height"
                v-for="attr in tweened.histogramAttr.xAttr">
Sascha Herzinger's avatar
Sascha Herzinger committed
91
          </rect>
92
          <rect class="fjs-histogram-rect"
93
94
95
96
97
                :x="attr.x"
                :y="attr.y"
                :width="attr.width"
                :height="attr.height"
                v-for="attr in tweened.histogramAttr.yAttr">
Sascha Herzinger's avatar
Sascha Herzinger committed
98
          </rect>
99
100
101
102
        </g>
      </svg>
    </div>

103
  </div>
104
105
106
107
</template>


<script>
108
  import DataBox from '../DataBox.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
109
  import Icon from '../Icon.vue'
110
111
  import store from '../../store/store'
  import types from '../../store/mutation-types'
Sascha Herzinger's avatar
Sascha Herzinger committed
112
  import requestHandling from '../methods/request-handling'
113
  import * as d3 from 'd3'
Sascha Herzinger's avatar
Sascha Herzinger committed
114
  import svgtooltip from '../directives/v-svgtooltip'
115
  import { TweenLite } from 'gsap'
116
  import $ from 'jquery'
117
118
  export default {
    name: 'correlation-analysis',
119
    data () {
120
      return {
121
        error: '',
122
123
        width: 0,
        height: 0,
124
125
        xyData: [],
        annotationData: [],
126
        shownAnalysisResults: {
127
          init: true,  // will disappear after being initially set
128
129
130
131
132
133
134
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
          method: '',
          x_label: '',
          y_label: '',
Sascha Herzinger's avatar
Sascha Herzinger committed
135
          get data () {
136
137
138
139
140
141
            return {
              id: [],
              [this.x_label]: [],
              [this.y_label]: []
            }
          }
142
        },
143
        tmpAnalysisResults: {
144
          init: true,  // will disappear after being initially set
145
146
147
148
149
150
151
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
          method: '',
          x_label: '',
          y_label: '',
Sascha Herzinger's avatar
Sascha Herzinger committed
152
          get data () {
153
154
155
156
157
158
159
            return {
              id: [],
              [this.x_label]: [],
              [this.y_label]: []
            }
          }
        },
160
        selectedPoints: [],
161
162
163
164
165
        tweened: {
          regLine: {},
          histogramAttr: {
            xAttr: [],
            yAttr: []
166
          }
167
        }
168
169
      }
    },
170
    computed: {
171
172
173
      idFilter () {
        return store.getters.filter('ids')
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
174
175
176
      subsets () {
        return store.getters.subsets
      },
177
178
      disabled () {
        return this.xyData.length !== 2
179
      },
180
181
182
183
      args () {
        return {
          x: `$${this.xyData[0]}$`,
          y: `$${this.xyData[1]}$`,
184
          id_filter: this.selectedPoints.map(d => d.id),
185
186
          method: 'pearson',
          annotations: this.annotationData.map(d => `$${d}$`)
187
188
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
189
190
      margin () {
        const left = this.width / 3
191
192
        const top = 50
        const right = 50
Sascha Herzinger's avatar
Sascha Herzinger committed
193
194
195
        const bottom = this.height / 3
        return { left, top, right, bottom }
      },
196
197
198
199
      padded () {
        const width = this.width - this.margin.left - this.margin.right
        const height = this.height - this.margin.top - this.margin.bottom
        return { width, height }
200
      },
201
      shownPoints () {
202
203
204
        const xs = []
        const ys = []
        const ids = []
205
        const subsets = []
206
        const annotations = []
207
        let all = []
208
        if (!this.shownAnalysisResults.init) {
209
210
211
212
213
214
          all = this.shownAnalysisResults.data.map(d => {
            const x = d[this.shownAnalysisResults.x_label]
            const y = d[this.shownAnalysisResults.y_label]
            const id = d.id
            const subset = d.subset
            const annotation = d.annotation
215
216
217
            const tooltip = {
              [this.shownAnalysisResults.x_label]: x,
              [this.shownAnalysisResults.y_label]: y,
218
219
220
221
              subset
            }
            if (typeof annotation !== 'undefined') {
              tooltip.annotation = annotation
222
            }
223
224
225
            xs.push(x)
            ys.push(y)
            ids.push(id)
226
            subsets.push(subset)
227
228
            annotations.push(annotation)
            return {x, y, id, subset, annotation, tooltip}
229
230
          })
        }
231
        return { xs, ys, ids, subsets, annotations, all }
232
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
233
      tmpPoints () {
234
235
236
        const xs = []
        const ys = []
        if (!this.tmpAnalysisResults.init) {
237
238
239
          this.tmpAnalysisResults.data.forEach(d => {
            const x = d[this.shownAnalysisResults.x_label]
            const y = d[this.shownAnalysisResults.y_label]
240
241
242
243
            xs.push(x)
            ys.push(y)
          })
        }
244
        return { xs, ys }
245
      },
246
247
      scales () {
        const x = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
248
249
250
251
252
          .domain((() => {
            const xExtent = d3.extent(this.shownPoints.xs)
            const xPadding = (xExtent[1] - xExtent[0]) / 10
            return [xExtent[0] - xPadding, xExtent[1] + xPadding]
          })())
253
254
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
255
256
257
258
259
          .domain((() => {
            const yExtent = d3.extent(this.shownPoints.ys)
            const yPadding = (yExtent[1] - yExtent[0]) / 10
            return [yExtent[0] - yPadding, yExtent[1] + yPadding]
          })())
260
261
262
263
264
265
266
267
268
269
270
271
272
273
          .range([this.padded.height, 0])
        return { x, y }
      },
      axis () {
        const x1 = d3.axisTop(this.scales.x)
        const y1 = d3.axisRight(this.scales.y)
        const x2 = d3.axisBottom(this.scales.x)
          .tickSizeInner(this.padded.height - 23)
          .tickFormat('')
        const y2 = d3.axisLeft(this.scales.y)
          .tickSizeInner(this.padded.width - 23)
          .tickFormat('')
        return { x1, x2, y1, y2 }
      },
274
      regLine () {
275
        if (this.tmpAnalysisResults.init) {
276
277
          return { x1: 0, x2: 0, y1: 0, y2: 0 }
        }
278
279
        const minX = d3.min(this.tmpPoints.xs)
        const maxX = d3.max(this.tmpPoints.xs)
280
        let x1 = this.scales.x(minX)
281
        let y1 = this.scales.y(this.tmpAnalysisResults.intercept + this.tmpAnalysisResults.slope * minX)
282
        let x2 = this.scales.x(maxX)
283
        let y2 = this.scales.y(this.tmpAnalysisResults.intercept + this.tmpAnalysisResults.slope * maxX)
284

285
286
        x1 = x1 < 0 ? 0 : x1
        x1 = x1 > this.width ? this.width : x1
287

288
289
        x2 = x2 < 0 ? 0 : x2
        x2 = x2 > this.width ? this.width : x2
290

291
292
        y1 = y1 < 0 ? 0 : y1
        y1 = y1 > this.height ? this.height : y1
293

294
295
        y2 = y2 < 0 ? 0 : y2
        y2 = y2 > this.height ? this.height : y2
296

Sascha Herzinger's avatar
Sascha Herzinger committed
297
298
299
        const tooltip = {Slope: this.tmpAnalysisResults.slope, Intercept: this.tmpAnalysisResults.intercept}

        return { x1, x2, y1, y2, tooltip }
300
301
302
303
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
304
          .on('end', () => {
305
            this.error = ''
306
            if (!d3.event.selection) {
307
              this.selectedPoints = []
308
              this.runAnalysisWrapper({init: false, args: this.args})
309
            } else {
310
311
312
313
314
315
316
317
              const [[x0, y0], [x1, y1]] = d3.event.selection
              this.selectedPoints = this.shownPoints.all.filter(d => {
                const x = this.scales.x(d.x)
                const y = this.scales.y(d.y)
                return x0 <= x && x <= x1 && y0 <= y && y <= y1
              })
              if (this.selectedPoints.length > 0 && this.selectedPoints.length < 3) {
                this.error = 'Selection must be zero (everything is selected) or greater than two.'
Sascha Herzinger's avatar
Sascha Herzinger committed
318
                return
319
320
321
              } else {
                this.runAnalysisWrapper({init: false, args: this.args})
              }
322
            }
323
            store.commit(types.SET_FILTER, {filter: 'ids', value: this.selectedPoints.map(d => d.id)})
324
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
325
326
      },
      histograms () {
327
        const BINS = 14
328
329
330
        let xBins = []
        let yBins = []
        if (!this.tmpAnalysisResults.init) {
331
332
          const [xMin, xMax] = d3.extent(this.tmpPoints.xs)
          const [yMin, yMax] = d3.extent(this.tmpPoints.ys)
333
334
335
          const xThresholds = d3.range(xMin, xMax, (xMax - xMin) / BINS)
          const yThresholds = d3.range(yMin, yMax, (yMax - yMin) / BINS)
          xBins = d3.histogram()
336
            .domain([xMin, xMax])
337
338
            .thresholds(xThresholds)(this.tmpPoints.xs)
          yBins = d3.histogram()
339
            .domain([yMin, yMax])
340
341
            .thresholds(yThresholds)(this.tmpPoints.ys)
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
342
343
344
        return { xBins, yBins }
      },
      histogramScales () {
345
346
        const xExtent = d3.extent(this.histograms.xBins.map(d => d.length))
        const yExtent = d3.extent(this.histograms.yBins.map(d => d.length))
347
        // no, I didn't mix up x and y.
Sascha Herzinger's avatar
Sascha Herzinger committed
348
        const x = d3.scaleLinear()
349
350
          .domain(yExtent)
          .range([yExtent[0] ? 10 : 0, this.margin.left])
Sascha Herzinger's avatar
Sascha Herzinger committed
351
        const y = d3.scaleLinear()
352
353
          .domain(xExtent)
          .range([xExtent[0] ? 10 : 0, this.margin.bottom])
Sascha Herzinger's avatar
Sascha Herzinger committed
354
355
        return { x, y }
      },
356
357
358
359
      histogramAttr () {
        const xAttr = this.histograms.xBins.map(d => {
          return {
            x: this.scales.x(d.x0),
Sascha Herzinger's avatar
Sascha Herzinger committed
360
            y: this.padded.height + 1,
361
362
363
364
365
366
            width: this.scales.x(d.x1) - this.scales.x(d.x0),
            height: this.histogramScales.y(d.length)
          }
        })
        const yAttr = this.histograms.yBins.map(d => {
          return {
367
            x: -this.histogramScales.x(d.length),
368
369
370
371
372
373
374
375
            y: this.scales.y(d.x1),
            width: this.histogramScales.x(d.length),
            height: this.scales.y(d.x0) - this.scales.y(d.x1)
          }
        })

        return { xAttr, yAttr }
      }
376
377
    },
    watch: {
378
      'axis': {
379
        handler: function (newAxis) {
380
381
382
383
          d3.select(`.fjs-vm-root-${this._uid} .fjs-x-axis-1`).call(newAxis.x1)
          d3.select(`.fjs-vm-root-${this._uid} .fjs-x-axis-2`).call(newAxis.x2)
          d3.select(`.fjs-vm-root-${this._uid} .fjs-y-axis-1`).call(newAxis.y1)
          d3.select(`.fjs-vm-root-${this._uid} .fjs-y-axis-2`).call(newAxis.y2)
384
        }
385
386
      },
      'brush': {
387
        handler: function (newBrush) {
388
          d3.select(`.fjs-vm-root-${this._uid} .fjs-brush`).call(newBrush)
389
        }
390
391
      },
      'regLine': {
392
        handler: function (newRegLine, oldRegLine) {
393
394
395
396
397
398
399
          const coords = oldRegLine
          const targetCoords = newRegLine
          targetCoords.onUpdate = () => { this.tweened.regLine = coords }
          TweenLite.to(coords, 0.5, targetCoords)
        }
      },
      'histogramAttr': {
400
        handler: function (newHistogramAttr, oldHistogramAttr) {
401
402
403
          let i = Math.max.apply(null, [newHistogramAttr.xAttr.length, oldHistogramAttr.xAttr.length])
          let j = Math.max.apply(null, [newHistogramAttr.yAttr.length, oldHistogramAttr.yAttr.length])

404
          while (i--) {
405
            const ii = i
406
            let xAttr = oldHistogramAttr.xAttr[i] ? oldHistogramAttr.xAttr[i]
Sascha Herzinger's avatar
Sascha Herzinger committed
407
              : { x: this.padded.width / 2, y: this.padded.height, width: 0, height: 0 }
408
409
410
            const xAttrTarget = newHistogramAttr.xAttr[i] ? newHistogramAttr.xAttr[i] : { width: 0 }
            xAttrTarget.onUpdate = () => { this.tweened.histogramAttr.xAttr[ii] = xAttr }
            TweenLite.to(xAttr, 0.5, xAttrTarget)
411
412
          }

413
          while (j--) {
414
            const jj = j
415
            const yAttr = oldHistogramAttr.yAttr[j] ? oldHistogramAttr.yAttr[j]
Sascha Herzinger's avatar
Sascha Herzinger committed
416
              : { x: 0, y: this.padded.height / 2, width: 0, height: 0 }
417
418
419
            const yAttrTarget = newHistogramAttr.yAttr[j] ? newHistogramAttr.yAttr[j] : { height: 0 }
            yAttrTarget.onUpdate = () => { this.tweened.histogramAttr.yAttr[jj] = yAttr }
            TweenLite.to(yAttr, 0.5, yAttrTarget)
420
421
          }
        }
422
423
424
425
426
427
428
      },
      'idFilter': {
        handler: function (newIDFilter) {
          const isFiltered = (newIDFilter.length === this.selectedPoints.length) &&
            this.selectedPoints.map(d => d.id).every(id => newIDFilter.indexOf(id) !== -1)
          if (!isFiltered) {
            const args = this.args
429
            args.id_filter = newIDFilter
430
431
432
            this.runAnalysisWrapper({init: false, args})
          }
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
433
434
435
436
437
      },
      'subsets': {
        handler: function () {
          this.runAnalysisWrapper({init: false, args: this.args})
        }
438
439
      }
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
440
    mounted () {
441
      window.addEventListener('resize', this.onResize)
442
      this.onResize()  // initial call
443
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
444
    beforeDestroy () {
445
446
      window.removeEventListener('resize', this.onResize)
    },
447
    components: {
Sascha Herzinger's avatar
Sascha Herzinger committed
448
449
      DataBox,
      Icon
450
451
    },
    mixins: [
Sascha Herzinger's avatar
Sascha Herzinger committed
452
      requestHandling,
453
      svgtooltip
454
455
    ],
    methods: {
456
      runAnalysisWrapper ({init, args}) {
457
        args['subsets'] = store.getters.subsets
458
        // function made available via requestHandling mixin
459
        this.runAnalysis({task_name: 'compute-correlation', args})
460
          .then(response => {
461
            const results = JSON.parse(response)
462
463
            const data = JSON.parse(results.data)
            results.data = Object.keys(data).map(key => data[key])
464
465
466
467
468
469
            if (init) {
              this.shownAnalysisResults = results
              this.tmpAnalysisResults = results
            } else {
              this.tmpAnalysisResults = results
            }
470
          })
471
472
473
          .catch(error => console.error(error))
      },
      onResize () {
474
475
        const tableHeight = $(this.$el.querySelector(`.fjs-vm-root-${this._uid} .fjs-stats-table`)).outerHeight(true)
        const section = this.$el.querySelector(`.fjs-vm-root-${this._uid} .fjs-visualisation-section`)
476
477
478
479
480
        const height = section.clientHeight - tableHeight
        const width = section.clientWidth
        this.height = height > width ? width : height // we want to have a square
        // noinspection JSSuspiciousNameCombination
        this.width = this.height
481
482
483
484
485
486
      },
      update_xyData (ids) {
        this.xyData = ids
      },
      update_annotationData (ids) {
        this.annotationData = ids
487
      }
488
489
490
491
492
    }
  }
</script>


493
494
495
496
497
498
<style lang="sass" scoped>
  @import './src/assets/base.sass'

  *
    font-family: Roboto, sans-serif

499
  .fjs-vm-root
500
501
502
    height: 100%
    width: 100%

503
504
505
506
507
    .fjs-data-box-section
      text-align: center
      height: 15%
      > *
        display: inline-block
508

509
510
511
512
513
514
515
516
517
518
519
    .fjs-controls-section
      height: 5%
      text-align: center
      .fjs-run-analysis-btn
        margin: 10px
        width: 100px
        height: 30px
        box-shadow: 2px 2px 4px 0 #999
        font-size: 20px
      .fjs-run-analysis-btn:not([disabled]):hover
        cursor: pointer
520

521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
    .fjs-visualisation-section
      height: 80%
      .fjs-lin-reg-line
        stroke: #ff5e00
        stroke-width: 4px
      .fjs-lin-reg-line:hover
        opacity: 0.4
      .fjs-histogram-rect
        stroke: #fff
        shape-rendering: crispEdges
        stroke-width: 0px
        fill: #ffd100
      .fjs-stats-table
        margin: 5px
        border-spacing: 0
        border-collapse: collapse
        font-size: 14px
        float: right
      .fjs-stats-table tr:nth-child(even)
        background-color: #ddd
      .fjs-stats-table, .fjs-stats-table td, .fjs-stats-table th
        border: 1px #ccc solid
        border-collapse: collapse
        padding: 5px
Sascha Herzinger's avatar
Sascha Herzinger committed
545
546
      .fjs-scatterplot-point
        fill: #000
547
        stroke-width: 0
548
549
550
551
      .fjs-scatterplot-point:hover
        fill: #f00
      .fjs-brush
        stroke-width: 0
552
553
554
</style>

<!--CSS for dynamically created components-->
555
556
557
558
559
560
561
562
563
<style lang="sass">
  @import './src/assets/svgtooltip.sass'

  .fjs-corr-axis
    shape-rendering: crispEdges
    .tick
      shape-rendering: crispEdges
    line
      stroke: #999
564
</style>