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

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

17
    <div class="fjs-parameter-container">
18
      <span>{{ error }}</span>
19
20
21
22
23
24
25
26
27
      <fieldset class="fjs-correlation-method">
        <legend>Correlation Method</legend>
        <input type="radio" id="fjs-param-method-1" value="pearson" v-model="params.method">
        <label for="fjs-param-method-1">Pearson</label>
        <input type="radio" id="fjs-param-method-2" value="spearman" v-model="params.method">
        <label for="fjs-param-method-2">Spearman</label>
        <input type="radio" id="fjs-param-method-3" value="kendall" v-model="params.method">
        <label for="fjs-param-method-3">Kendall</label>
      </fieldset>
28
    </div>
29

30
    <div class="fjs-vis-container">
31
      <svg :width="width"
32
           :height="height">
33
        <g :transform="`translate(${margin.left}, ${margin.top})`">
34
35
36
37
38
          <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>
39
40
41
42
43
44
45
46
47
48
49
50
51
          <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
52
          <icon class="fjs-scatterplot-point"
53
                :shape="point.subset"
Sascha Herzinger's avatar
Sascha Herzinger committed
54
55
                :cx="scales.x(point.x)"
                :cy="scales.y(point.y)"
56
                :size="9"
57
58
                v-svgtooltip="point.tooltip"
                :fill="annotationColors[annotations.indexOf(point.annotation) % annotationColors.length]"
59
                :key="`${point.id}-${scales.x(point.x)}-${scales.y(point.y)}`"
60
                v-for="point in shownPoints.all">
Sascha Herzinger's avatar
Sascha Herzinger committed
61
          </icon>
62
          <line class="fjs-lin-reg-line"
63
64
65
                :x1="tweened.regLine.x1"
                :x2="tweened.regLine.x2"
                :y1="tweened.regLine.y1"
Sascha Herzinger's avatar
Sascha Herzinger committed
66
                :y2="tweened.regLine.y2"
67
                v-svgtooltip="regLine.tooltip">
68
          </line>
69
          <rect class="fjs-histogram-rect"
70
71
72
73
74
                :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
75
          </rect>
76
          <rect class="fjs-histogram-rect"
77
78
79
80
81
                :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
82
          </rect>
83
84
        </g>
      </svg>
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
      <div class="fjs-table-container">
        <table class="fjs-stats-table">
          <caption>Selected points</caption>
          <tr>
            <td>Corr. Coef.</td>
            <td>{{ tmpAnalysisResults.coef }}</td>
          </tr>
          <tr>
            <td>p-value</td>
            <td>{{ tmpAnalysisResults.p_value }}</td>
          </tr>
          <tr>
            <td>Correlation method</td>
            <td>{{ tmpAnalysisResults.method }}</td>
          </tr>
          <tr>
            <td>#Points</td>
            <td>{{ tmpPoints.all.length }}</td>
          </tr>
        </table>
        <table class="fjs-stats-table"
               v-for="(stats, i) in shownAnalysisResults.subsets">
          <caption>Subset: {{ i + 1 }}</caption>
          <tr>
            <td>Corr. Coef.</td>
            <td>{{ stats.coef }}</td>
          </tr>
          <tr>
            <td>p-value</td>
            <td>{{ stats.p_value }}</td>
          </tr>
          <tr>
            <td>Correlation method</td>
            <td>{{ tmpAnalysisResults.method }}</td>
          </tr>
          <tr>
            <td>#Points</td>
            <td>{{ tmpPoints.subsets.filter(d => d === i).length }}</td>
          </tr>
        </table>
      </div>
126
127
    </div>

128
  </div>
129
130
131
132
</template>


<script>
133
  import DataBox from '../DataBox.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
134
  import Icon from '../Icon.vue'
135
136
  import store from '../../store/store'
  import types from '../../store/mutation-types'
Sascha Herzinger's avatar
Sascha Herzinger committed
137
  import requestHandling from '../methods/request-handling'
138
  import * as d3 from 'd3'
Sascha Herzinger's avatar
Sascha Herzinger committed
139
  import svgtooltip from '../directives/v-svgtooltip'
140
  import { TweenLite } from 'gsap'
141
142
  export default {
    name: 'correlation-analysis',
143
    data () {
144
      return {
145
        error: '',
146
147
        width: 0,
        height: 0,
148
149
        xyData: [],
        annotationData: [],
150
        annotationColors: d3.schemeCategory10,
151
152
153
        params: {
          method: 'pearson'
        },
154
        shownAnalysisResults: {
155
          init: true,  // will disappear after being initially set
156
157
158
159
160
161
162
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
          method: '',
          x_label: '',
          y_label: '',
Sascha Herzinger's avatar
Sascha Herzinger committed
163
          get data () {
164
165
166
167
168
169
            return {
              id: [],
              [this.x_label]: [],
              [this.y_label]: []
            }
          }
170
        },
171
        tmpAnalysisResults: {
172
          init: true,  // will disappear after being initially set
173
174
175
176
177
178
179
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
          method: '',
          x_label: '',
          y_label: '',
Sascha Herzinger's avatar
Sascha Herzinger committed
180
          get data () {
181
182
183
184
185
186
187
            return {
              id: [],
              [this.x_label]: [],
              [this.y_label]: []
            }
          }
        },
188
        selectedPoints: [],
189
190
191
192
193
        tweened: {
          regLine: {},
          histogramAttr: {
            xAttr: [],
            yAttr: []
194
          }
195
        }
196
197
      }
    },
198
    computed: {
199
200
201
      idFilter () {
        return store.getters.filter('ids')
      },
202
203
      disabled () {
        return this.xyData.length !== 2
204
      },
205
206
207
208
      args () {
        return {
          x: `$${this.xyData[0]}$`,
          y: `$${this.xyData[1]}$`,
209
          id_filter: this.selectedPoints.map(d => d.id),
210
211
          method: this.params.method,
          subsets: store.getters.subsets,
212
          annotations: this.annotationData.map(d => `$${d}$`)
213
214
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
215
216
      margin () {
        const left = this.width / 3
217
218
        const top = 50
        const right = 50
Sascha Herzinger's avatar
Sascha Herzinger committed
219
220
221
        const bottom = this.height / 3
        return { left, top, right, bottom }
      },
222
223
224
225
      padded () {
        const width = this.width - this.margin.left - this.margin.right
        const height = this.height - this.margin.top - this.margin.bottom
        return { width, height }
226
      },
227
228
229
      annotations () {
        return this.shownPoints.annotations.filter((d, i, arr) => arr.indexOf(d) === i)  // make unique
      },
230
      shownPoints () {
231
232
233
        const xs = []
        const ys = []
        const ids = []
234
        const subsets = []
235
        const annotations = []
236
        let all = []
237
        if (!this.shownAnalysisResults.init) {
238
239
240
241
242
243
          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
244
245
246
            const tooltip = {
              [this.shownAnalysisResults.x_label]: x,
              [this.shownAnalysisResults.y_label]: y,
247
248
249
250
              subset
            }
            if (typeof annotation !== 'undefined') {
              tooltip.annotation = annotation
251
            }
252
253
254
            xs.push(x)
            ys.push(y)
            ids.push(id)
255
            subsets.push(subset)
256
257
            annotations.push(annotation)
            return {x, y, id, subset, annotation, tooltip}
258
259
          })
        }
260
        return { xs, ys, ids, subsets, annotations, all }
261
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
262
      tmpPoints () {
263
264
        const xs = []
        const ys = []
265
266
267
268
        const ids = []
        const subsets = []
        const annotations = []
        let all = []
269
        if (!this.tmpAnalysisResults.init) {
270
271
272
273
274
275
          all = this.tmpAnalysisResults.data.map(d => {
            const x = d[this.tmpAnalysisResults.x_label]
            const y = d[this.tmpAnalysisResults.y_label]
            const id = d.id
            const subset = d.subset
            const annotation = d.annotation
276
277
            xs.push(x)
            ys.push(y)
278
279
280
281
            ids.push(id)
            subsets.push(subset)
            annotations.push(annotation)
            return {x, y, id, subset, annotation}
282
283
          })
        }
284
        return { xs, ys, ids, subsets, annotations, all }
285
      },
286
287
      scales () {
        const x = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
288
289
290
291
292
          .domain((() => {
            const xExtent = d3.extent(this.shownPoints.xs)
            const xPadding = (xExtent[1] - xExtent[0]) / 10
            return [xExtent[0] - xPadding, xExtent[1] + xPadding]
          })())
293
294
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
295
296
297
298
299
          .domain((() => {
            const yExtent = d3.extent(this.shownPoints.ys)
            const yPadding = (yExtent[1] - yExtent[0]) / 10
            return [yExtent[0] - yPadding, yExtent[1] + yPadding]
          })())
300
301
302
303
304
305
306
307
308
309
310
311
312
313
          .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 }
      },
314
      regLine () {
315
        if (this.tmpAnalysisResults.init) {
316
317
          return { x1: 0, x2: 0, y1: 0, y2: 0 }
        }
318
319
        const minX = d3.min(this.tmpPoints.xs)
        const maxX = d3.max(this.tmpPoints.xs)
320
        let x1 = this.scales.x(minX)
321
        let y1 = this.scales.y(this.tmpAnalysisResults.intercept + this.tmpAnalysisResults.slope * minX)
322
        let x2 = this.scales.x(maxX)
323
        let y2 = this.scales.y(this.tmpAnalysisResults.intercept + this.tmpAnalysisResults.slope * maxX)
324

325
326
        x1 = x1 < 0 ? 0 : x1
        x1 = x1 > this.width ? this.width : x1
327

328
329
        x2 = x2 < 0 ? 0 : x2
        x2 = x2 > this.width ? this.width : x2
330

331
332
        y1 = y1 < 0 ? 0 : y1
        y1 = y1 > this.height ? this.height : y1
333

334
335
        y2 = y2 < 0 ? 0 : y2
        y2 = y2 > this.height ? this.height : y2
336

Sascha Herzinger's avatar
Sascha Herzinger committed
337
338
339
        const tooltip = {Slope: this.tmpAnalysisResults.slope, Intercept: this.tmpAnalysisResults.intercept}

        return { x1, x2, y1, y2, tooltip }
340
341
342
343
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
344
          .on('end', () => {
345
            this.error = ''
346
            if (!d3.event.selection) {
347
              this.selectedPoints = []
348
            } else {
349
350
351
352
353
354
355
356
              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
357
                return
358
              }
359
            }
360
            store.commit(types.SET_FILTER, {filter: 'ids', value: this.selectedPoints.map(d => d.id)})
361
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
362
363
      },
      histograms () {
364
        const BINS = 14
365
366
367
        let xBins = []
        let yBins = []
        if (!this.tmpAnalysisResults.init) {
368
369
          const [xMin, xMax] = d3.extent(this.tmpPoints.xs)
          const [yMin, yMax] = d3.extent(this.tmpPoints.ys)
370
371
372
          const xThresholds = d3.range(xMin, xMax, (xMax - xMin) / BINS)
          const yThresholds = d3.range(yMin, yMax, (yMax - yMin) / BINS)
          xBins = d3.histogram()
373
            .domain([xMin, xMax])
374
375
            .thresholds(xThresholds)(this.tmpPoints.xs)
          yBins = d3.histogram()
376
            .domain([yMin, yMax])
377
378
            .thresholds(yThresholds)(this.tmpPoints.ys)
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
379
380
381
        return { xBins, yBins }
      },
      histogramScales () {
382
383
        const xExtent = d3.extent(this.histograms.xBins.map(d => d.length))
        const yExtent = d3.extent(this.histograms.yBins.map(d => d.length))
384
        // no, I didn't mix up x and y.
Sascha Herzinger's avatar
Sascha Herzinger committed
385
        const x = d3.scaleLinear()
386
387
          .domain(yExtent)
          .range([yExtent[0] ? 10 : 0, this.margin.left])
Sascha Herzinger's avatar
Sascha Herzinger committed
388
        const y = d3.scaleLinear()
389
390
          .domain(xExtent)
          .range([xExtent[0] ? 10 : 0, this.margin.bottom])
Sascha Herzinger's avatar
Sascha Herzinger committed
391
392
        return { x, y }
      },
393
394
395
396
      histogramAttr () {
        const xAttr = this.histograms.xBins.map(d => {
          return {
            x: this.scales.x(d.x0),
Sascha Herzinger's avatar
Sascha Herzinger committed
397
            y: this.padded.height + 1,
398
399
400
401
402
403
            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 {
404
            x: -this.histogramScales.x(d.length),
405
406
407
408
409
410
411
412
            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 }
      }
413
414
    },
    watch: {
415
416
417
418
419
420
421
422
423
424
      'args': {
        handler: function (newArgs, oldArgs) {
          const init = newArgs.x !== oldArgs.x ||
            newArgs.y !== oldArgs.y ||
            JSON.stringify(newArgs.annotations) !== JSON.stringify(oldArgs.annotations)
          if (!this.disabled) {
            this.runAnalysisWrapper({init, args: this.args})
          }
        }
      },
425
      'axis': {
426
        handler: function (newAxis) {
427
428
429
430
          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)
431
        }
432
433
      },
      'brush': {
434
        handler: function (newBrush) {
435
          d3.select(`.fjs-vm-root-${this._uid} .fjs-brush`).call(newBrush)
436
        }
437
438
      },
      'regLine': {
439
        handler: function (newRegLine, oldRegLine) {
440
441
442
443
444
445
446
          const coords = oldRegLine
          const targetCoords = newRegLine
          targetCoords.onUpdate = () => { this.tweened.regLine = coords }
          TweenLite.to(coords, 0.5, targetCoords)
        }
      },
      'histogramAttr': {
447
        handler: function (newHistogramAttr, oldHistogramAttr) {
448
449
450
          let i = Math.max.apply(null, [newHistogramAttr.xAttr.length, oldHistogramAttr.xAttr.length])
          let j = Math.max.apply(null, [newHistogramAttr.yAttr.length, oldHistogramAttr.yAttr.length])

451
          while (i--) {
452
            const ii = i
453
            let xAttr = oldHistogramAttr.xAttr[i] ? oldHistogramAttr.xAttr[i]
Sascha Herzinger's avatar
Sascha Herzinger committed
454
              : { x: this.padded.width / 2, y: this.padded.height, width: 0, height: 0 }
455
456
457
            const xAttrTarget = newHistogramAttr.xAttr[i] ? newHistogramAttr.xAttr[i] : { width: 0 }
            xAttrTarget.onUpdate = () => { this.tweened.histogramAttr.xAttr[ii] = xAttr }
            TweenLite.to(xAttr, 0.5, xAttrTarget)
458
459
          }

460
          while (j--) {
461
            const jj = j
462
            const yAttr = oldHistogramAttr.yAttr[j] ? oldHistogramAttr.yAttr[j]
Sascha Herzinger's avatar
Sascha Herzinger committed
463
              : { x: 0, y: this.padded.height / 2, width: 0, height: 0 }
464
465
466
            const yAttrTarget = newHistogramAttr.yAttr[j] ? newHistogramAttr.yAttr[j] : { height: 0 }
            yAttrTarget.onUpdate = () => { this.tweened.histogramAttr.yAttr[jj] = yAttr }
            TweenLite.to(yAttr, 0.5, yAttrTarget)
467
468
          }
        }
469
470
471
472
473
474
475
      },
      '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
476
            args.id_filter = newIDFilter
477
478
          }
        }
479
480
      }
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
481
    mounted () {
482
      window.addEventListener('resize', this.handleResize)
483
      this.handleResize()
484
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
485
    beforeDestroy () {
486
      window.removeEventListener('resize', this.handleResize)
487
    },
488
    components: {
Sascha Herzinger's avatar
Sascha Herzinger committed
489
490
      DataBox,
      Icon
491
492
    },
    mixins: [
Sascha Herzinger's avatar
Sascha Herzinger committed
493
      requestHandling,
494
      svgtooltip
495
496
    ],
    methods: {
497
      runAnalysisWrapper ({init, args}) {
498
        args['subsets'] = store.getters.subsets
499
        // function made available via requestHandling mixin
500
        this.runAnalysis({task_name: 'compute-correlation', args})
501
          .then(response => {
502
            const results = JSON.parse(response)
503
504
            const data = JSON.parse(results.data)
            results.data = Object.keys(data).map(key => data[key])
505
506
507
508
509
510
            if (init) {
              this.shownAnalysisResults = results
              this.tmpAnalysisResults = results
            } else {
              this.tmpAnalysisResults = results
            }
511
          })
512
          .catch(error => console.error(error))
513
          .then(this.handleResize)
514
      },
515
516
      handleResize () {
        const container = this.$el.querySelector(`.fjs-vm-root-${this._uid} .fjs-vis-container svg`)
517
        // noinspection JSSuspiciousNameCombination
518
519
        this.height = container.clientWidth
        this.width = container.clientWidth
520
521
522
523
524
525
      },
      update_xyData (ids) {
        this.xyData = ids
      },
      update_annotationData (ids) {
        this.annotationData = ids
526
      }
527
528
529
530
531
    }
  }
</script>


532
533
534
535
536
537
<style lang="sass" scoped>
  @import './src/assets/base.sass'

  *
    font-family: Roboto, sans-serif

538
  .fjs-vm-root
539
540
    height: 100%
    width: 100%
541
542
    display: flex
    flex-direction: column
543

544
545
546
547
548
549
550
551
552
    .fjs-data-box-container
      width: 70%
      height: 160px
      overflow: hidden
      margin: 0 auto
      .fjs-data-box:nth-child(1)
        float: left
      .fjs-data-box:nth-child(2)
        float: right
553

554
    .fjs-parameter-container
555
      text-align: center
556
557
558
559
560
      .fjs-correlation-method
        width: 0
        margin: auto
        white-space: nowrap

561

562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
    .fjs-vis-container
      flex: 1
      display: flex
      justify-content: center
      svg
        float: left
        flex: 1
        .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-scatterplot-point
          stroke-width: 0
          shape-rendering: crispEdges
        .fjs-scatterplot-point:hover
          fill: #f00
        .fjs-brush
          stroke-width: 0
      .fjs-table-container
        float: left
        display: flex
        flex-direction: column
        .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
602
603
604
</style>

<!--CSS for dynamically created components-->
605
606
607
608
609
610
611
612
613
<style lang="sass">
  @import './src/assets/svgtooltip.sass'

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