CorrelationAnalysis.vue 16.5 KB
Newer Older
1
<template>
2
3
4
  <div style="height: 100%; width: 100%">

    <div id="data-box-section" style="height: 25%;">
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
17
18
19
20
21
22
23
    <div style="text-align: center;">
      <button id="run-analysis-btn"
              type="button"
              @click="runAnalysisWrapper({init: true})"
              :disabled="disabled">Run Analysis</button><br/>
      <br/>
      <span>{{ error }}</span>
    </div>
    <br/>
24
    <div id="visualisation-section" style="height: 75%;">
25
26
27
28
29
30
31
32
33
      <table class="stats-table" v-show="! shownAnalysisResults.init">
        <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
40
41
42
        <tr>
          <td>Correlation statistic</td>
          <td>{{ tmpAnalysisResults.method }}</td>
        </tr>
        <tr>
          <td>#Selected points</td>
          <td>{{ tmpPoints.all.length }}</td>
        </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
      <svg width="100%" height="100%" v-show="! shownAnalysisResults.init">
48
49
        <g :transform="`translate(${margin.left}, ${margin.top})`">
          <g id="x-axis-1" class="fjs-corr-axis" :transform="`translate(0, ${padded.height})`"></g>
50
51
          <g id="x-axis-2" class="fjs-corr-axis"></g>
          <g id="y-axis-1" class="fjs-corr-axis"></g>
52
          <g id="y-axis-2" class="fjs-corr-axis" :transform="`translate(${padded.width}, 0)`"></g>
53
54
55
56
57
58
59
60
61
62
63
64
65
66
          <g id="brush"></g>
          <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>
67
68
          <circle class="scatterplot-point"
                  :cx="scales.x(point.x)"
69
70
71
                  :cy="scales.y(point.y)"
                  r="4"
                  :data-idx="idx"
Sascha Herzinger's avatar
Sascha Herzinger committed
72
                  v-for="(point, idx) in shownPoints.all"
73
                  v-svgtooltip="point.tooltip">
74
          </circle>
75
          <line id="lin-reg-line"
76
77
78
                :x1="tweened.regLine.x1"
                :x2="tweened.regLine.x2"
                :y1="tweened.regLine.y1"
Sascha Herzinger's avatar
Sascha Herzinger committed
79
                :y2="tweened.regLine.y2"
80
                v-svgtooltip="regLine.tooltip">
81
          </line>
82
83
84
85
86
87
          <rect class="histogram-rect"
                :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
88
          </rect>
89
90
91
92
93
94
          <rect class="histogram-rect"
                :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
95
          </rect>
96
97
98
99
        </g>
      </svg>
    </div>

100
  </div>
101
102
103
104
</template>


<script>
105
106
  import DataBox from '../DataBox.vue'
  import requestHandling from '../mixins/request-handling'
107
  import * as d3 from 'd3'
108
  import svgtooltip from '../mixins/v-svgtooltip'
109
  import { TweenLite } from 'gsap'
110
111
  export default {
    name: 'correlation-analysis',
112
    data () {
113
      return {
114
        error: '',
115
116
        width: 0,
        height: 0,
117
118
119
120
121
        xyData: [],
        annotationData: [],
        get args () {
          return {
            x: `$${this.xyData[0]}$`,
122
123
            y: `$${this.xyData[1]}$`,
            ids: this.selectedPoints.map(d => d.id)
124
          }
125
        },
126

127
        shownAnalysisResults: {
128
          init: true,  // will disappear after being initially set
129
130
131
132
133
134
135
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
          method: '',
          x_label: '',
          y_label: '',
Sascha Herzinger's avatar
Sascha Herzinger committed
136
          get data () {
137
138
139
140
141
142
            return {
              id: [],
              [this.x_label]: [],
              [this.y_label]: []
            }
          }
143
        },
144
        tmpAnalysisResults: {
145
          init: true,  // will disappear after being initially set
146
147
148
149
150
151
152
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
          method: '',
          x_label: '',
          y_label: '',
Sascha Herzinger's avatar
Sascha Herzinger committed
153
          get data () {
154
155
156
157
158
159
160
            return {
              id: [],
              [this.x_label]: [],
              [this.y_label]: []
            }
          }
        },
161
        selectedPoints: [],
162
163
164
165
166
167
168
        tweened: {
          regLine: {},
          histogramAttr: {
            xAttr: [],
            yAttr: []
          },
        }
169
170
      }
    },
171
172
173
    computed: {
      disabled () {
        return this.xyData.length !== 2
174
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
175
176
      margin () {
        const left = this.width / 3
177
178
        const top = 50
        const right = 50
Sascha Herzinger's avatar
Sascha Herzinger committed
179
180
181
        const bottom = this.height / 3
        return { left, top, right, bottom }
      },
182
183
184
185
      padded () {
        const width = this.width - this.margin.left - this.margin.right
        const height = this.height - this.margin.top - this.margin.bottom
        return { width, height }
186
      },
187
188
      shownPoints () {
        const xs = [], ys = [], ids = []
189
190
191
192
193
194
        let all = []
        if (! this.shownAnalysisResults.init) {
          all = Object.keys(this.shownAnalysisResults.data.id).map(key => {
            const x = this.shownAnalysisResults.data[this.shownAnalysisResults.x_label][key]
            const y = this.shownAnalysisResults.data[this.shownAnalysisResults.y_label][key]
            const id = this.shownAnalysisResults.data.id[key]
Sascha Herzinger's avatar
Sascha Herzinger committed
195
            const tooltip = {[this.shownAnalysisResults.x_label]: x, [this.shownAnalysisResults.y_label]: y}
196
197
198
            xs.push(x)
            ys.push(y)
            ids.push(id)
Sascha Herzinger's avatar
Sascha Herzinger committed
199
            return {x, y, id, tooltip}
200
201
          })
        }
202
203
        return { xs, ys, ids, all }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
204
      tmpPoints () {
205
        const xs = [], ys = [], ids = []
206
207
        let all = []
        if (! this.tmpAnalysisResults.init) {
Sascha Herzinger's avatar
Sascha Herzinger committed
208
          all = Object.keys(this.tmpAnalysisResults.data.id).map(key => {
209
210
211
212
213
214
215
216
217
            const x = this.tmpAnalysisResults.data[this.tmpAnalysisResults.x_label][key]
            const y = this.tmpAnalysisResults.data[this.tmpAnalysisResults.y_label][key]
            const id = this.tmpAnalysisResults.data.id[key]
            xs.push(x)
            ys.push(y)
            ids.push(id)
            return {x, y, id}
          })
        }
218
        return { xs, ys, ids, all }
219
      },
220
221
      scales () {
        const x = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
222
223
224
225
226
          .domain((() => {
            const xExtent = d3.extent(this.shownPoints.xs)
            const xPadding = (xExtent[1] - xExtent[0]) / 10
            return [xExtent[0] - xPadding, xExtent[1] + xPadding]
          })())
227
228
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
229
230
231
232
233
          .domain((() => {
            const yExtent = d3.extent(this.shownPoints.ys)
            const yPadding = (yExtent[1] - yExtent[0]) / 10
            return [yExtent[0] - yPadding, yExtent[1] + yPadding]
          })())
234
235
236
          .range([this.padded.height, 0])
        return { x, y }
      },
237
238
239
240
241
242
243
244
245
      tmpScales () {
        const x = d3.scaleLinear()
          .domain(d3.extent(this.tmpPoints.xs))
          .range([this.scales.x(d3.min(this.tmpPoints.xs)), this.scales.x(d3.max(this.tmpPoints.xs))])
        const y = d3.scaleLinear()
          .domain(d3.extent(this.tmpPoints.ys))
          .range([this.scales.y(this.tmpPoints.max), this.scales.x(this.tmpPoints.min)])
        return { x, y }
      },
246
247
248
249
250
251
252
253
254
255
256
      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 }
      },
257
      regLine () {
258
        if (this.tmpAnalysisResults.init) {
259
260
          return { x1: 0, x2: 0, y1: 0, y2: 0 }
        }
261
262
        const minX = d3.min(this.tmpPoints.xs)
        const maxX = d3.max(this.tmpPoints.xs)
263
        let x1 = this.scales.x(minX)
264
        let y1 = this.scales.y(this.tmpAnalysisResults.intercept + this.tmpAnalysisResults.slope * minX)
265
        let x2 = this.scales.x(maxX)
266
        let y2 = this.scales.y(this.tmpAnalysisResults.intercept + this.tmpAnalysisResults.slope * maxX)
267
268
269
270
271
272
273
274
275
276
277
278
279

        x1 = x1 < 0 ? 0 : x1;
        x1 = x1 > this.width ? this.width : x1;

        x2 = x2 < 0 ? 0 : x2;
        x2 = x2 > this.width ? this.width : x2;

        y1 = y1 < 0 ? 0 : y1;
        y1 = y1 > this.height ? this.height : y1;

        y2 = y2 < 0 ? 0 : y2;
        y2 = y2 > this.height ? this.height : y2;

Sascha Herzinger's avatar
Sascha Herzinger committed
280
281
282
        const tooltip = {Slope: this.tmpAnalysisResults.slope, Intercept: this.tmpAnalysisResults.intercept}

        return { x1, x2, y1, y2, tooltip }
283
284
285
286
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
287
          .on('end', () => {
288
            this.error = ''
289
290
291
292
293
            if (! d3.event.selection) {
              this.selectedPoints = []
              this.runAnalysisWrapper({init: false})
              return
            }
294
            const [[x0, y0], [x1, y1]] = d3.event.selection
295
            this.selectedPoints = this.shownPoints.all.filter(d => {
296
297
298
              const x = this.scales.x(d.x)
              const y = this.scales.y(d.y)
              return x0 <= x && x <= x1 && y0 <= y && y <= y1;
299
            })
300
301
302
303
304
            if (this.selectedPoints.length > 0 && this.selectedPoints.length < 3) {
              this.error = 'Selection must be zero (everything is selected) or greater than two.'
            } else {
              this.runAnalysisWrapper({init: false})
            }
305
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
306
307
      },
      histograms () {
308
        const BINS = 14
309
310
311
312
313
314
315
316
317
318
319
320
321
        let xBins = [], yBins = []
        if (! this.tmpAnalysisResults.init) {
          const [xMin, xMax] = this.tmpScales.x.domain()
          const [yMin, yMax] = this.tmpScales.y.domain()
          const xThresholds = d3.range(xMin, xMax, (xMax - xMin) / BINS)
          const yThresholds = d3.range(yMin, yMax, (yMax - yMin) / BINS)
          xBins = d3.histogram()
            .domain(this.tmpScales.x.domain())
            .thresholds(xThresholds)(this.tmpPoints.xs)
          yBins = d3.histogram()
            .domain(this.tmpScales.y.domain())
            .thresholds(yThresholds)(this.tmpPoints.ys)
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
322
323
324
        return { xBins, yBins }
      },
      histogramScales () {
325
326
        const xExtent = d3.extent(this.histograms.xBins.map(d => d.length))
        const yExtent = d3.extent(this.histograms.yBins.map(d => d.length))
Sascha Herzinger's avatar
Sascha Herzinger committed
327
328
        // no, I didn't mix up xBins and yBins.
        const x = d3.scaleLinear()
329
330
          .domain(yExtent)
          .range([yExtent[0] ? 10 : 0, this.margin.left])
Sascha Herzinger's avatar
Sascha Herzinger committed
331
        const y = d3.scaleLinear()
332
333
          .domain(xExtent)
          .range([xExtent[0] ? 10 : 0, this.margin.bottom])
Sascha Herzinger's avatar
Sascha Herzinger committed
334
335
        return { x, y }
      },
336
337
338
339
      histogramAttr () {
        const xAttr = this.histograms.xBins.map(d => {
          return {
            x: this.scales.x(d.x0),
Sascha Herzinger's avatar
Sascha Herzinger committed
340
            y: this.padded.height + 1,
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
            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 {
            x: - this.histogramScales.x(d.length),
            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 }
      }
356
357
    },
    watch: {
358
359
360
361
362
363
      'axis': {
        handler: function(newAxis) {
          d3.select('#x-axis-1').call(newAxis.x1)
          d3.select('#x-axis-2').call(newAxis.x2)
          d3.select('#y-axis-1').call(newAxis.y1)
          d3.select('#y-axis-2').call(newAxis.y2)
364
        }
365
366
367
368
      },
      'brush': {
        handler: function(newBrush) {
          d3.select('#brush').call(newBrush)
369
        }
370
371
372
      },
      'regLine': {
        handler: function(newRegLine, oldRegLine) {
373
374
375
376
377
378
379
380
          const coords = oldRegLine
          const targetCoords = newRegLine
          targetCoords.onUpdate = () => { this.tweened.regLine = coords }
          TweenLite.to(coords, 0.5, targetCoords)
        }
      },
      'histogramAttr': {
        handler: function(newHistogramAttr, oldHistogramAttr) {
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
          let i = Math.max.apply(null, [newHistogramAttr.xAttr.length, oldHistogramAttr.xAttr.length])
          let j = Math.max.apply(null, [newHistogramAttr.yAttr.length, oldHistogramAttr.yAttr.length])

          while (i --) {
            const ii = i
            const xAttr = oldHistogramAttr.xAttr[i] ?
              oldHistogramAttr.xAttr[i] : { x: this.padded.width / 2, y: this.padded.height, width: 0, height: 0 }
            const xAttr_target = newHistogramAttr.xAttr[i] ? newHistogramAttr.xAttr[i] : { width: 0 }
            xAttr_target.onUpdate = () => { this.tweened.histogramAttr.xAttr[ii] = xAttr }
            TweenLite.to(xAttr, 0.5, xAttr_target)
          }

          while (j --) {
            const jj = j
            const yAttr = oldHistogramAttr.yAttr[j] ?
              oldHistogramAttr.yAttr[j] : { x: 0, y: this.padded.height / 2, width: 0, height: 0 }
            const yAttr_target = newHistogramAttr.yAttr[j] ? newHistogramAttr.yAttr[j] : { height: 0 }
            yAttr_target.onUpdate = () => { this.tweened.histogramAttr.yAttr[jj] = yAttr }
            TweenLite.to(yAttr, 0.5, yAttr_target)
400
401
          }
        }
402
403
      }
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
404
    mounted () {
405
406
      window.addEventListener('resize', this.onResize)
      this.onResize() // initial call
407
      this.tmpAnalysisResults = this.shownAnalysisResults
408
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
409
    beforeDestroy () {
410
411
      window.removeEventListener('resize', this.onResize)
    },
412
413
414
415
    components: {
      DataBox
    },
    mixins: [
Sascha Herzinger's avatar
Sascha Herzinger committed
416
      requestHandling,
417
      svgtooltip
418
419
    ],
    methods: {
420
      runAnalysisWrapper ({init}) {
421
422
423
424
        let args = this.args
        if (init) {
          args.ids = []
        }
425
        // function made available via requestHandling mixin
426
        this.runAnalysis({job_name: 'compute-correlation', args: args})
427
          .then(response => {
428
429
            const results = JSON.parse(response)
            results.data = JSON.parse(results.data)
430
431
432
433
434
435
            if (init) {
              this.shownAnalysisResults = results
              this.tmpAnalysisResults = results
            } else {
              this.tmpAnalysisResults = results
            }
436
          })
437
438
439
440
441
442
          .catch(error => console.error(error))
      },
      onResize () {
        const section = this.$el.querySelector('#visualisation-section')
        this.height = section.clientHeight
        this.width = section.clientWidth
443
444
445
446
447
448
      },
      update_xyData (ids) {
        this.xyData = ids
      },
      update_annotationData (ids) {
        this.annotationData = ids
449
      }
450
451
452
453
454
455
    }
  }
</script>


<style scoped>
Sascha Herzinger's avatar
Sascha Herzinger committed
456
457
  * {
    font-family: Roboto, sans-serif;
458
459
  }

460
  #data-box-section {
461
462
463
464
    display: flex;
    flex-direction: row;
    justify-content: space-around;
  }
465

466
467
468
469
  #run-analysis-btn {
    width: 100%;
    height: 20px;
  }
470
471
472
473
474

  #lin-reg-line {
    stroke: #ff5e00;
    stroke-width: 4px;
  }
475

Sascha Herzinger's avatar
Sascha Herzinger committed
476
  #lin-reg-line:hover {
Sascha Herzinger's avatar
Sascha Herzinger committed
477
    opacity: 0.4;
478
479
  }

480
  .histogram-rect {
481
    stroke: #fff;
482
    shape-rendering: crispEdges;
483
    stroke-width: 0px;
484
485
    fill: #ffd100;
  }
486
487
488
489
490

  .stats-table {
    margin: 5px;
    border-spacing: 0;
    border-collapse: collapse;
Sascha Herzinger's avatar
Sascha Herzinger committed
491
492
    font-size: 14px;
    float: right;
493
494
  }

Sascha Herzinger's avatar
Sascha Herzinger committed
495
496
497
498
  .stats-table tr:nth-child(even) {
    background-color: #ddd;
  }

499
  .stats-table, .stats-table td, .stats-table th {
Sascha Herzinger's avatar
Sascha Herzinger committed
500
    border: 1px #ccc solid;
501
502
503
    border-collapse: collapse;
    padding: 5px;
  }
504

Sascha Herzinger's avatar
Sascha Herzinger committed
505
  .scatterplot-point:hover {
506
    fill: #f00;
Sascha Herzinger's avatar
Sascha Herzinger committed
507
    opacity: 0.4;
508
  }
Sascha Herzinger's avatar
Sascha Herzinger committed
509
510
511
512

  #brush {
    stroke-width: 0;
  }
513
514
515
516
517
518
519
520
521
522

  #run-analysis-btn {
    width: 200px;
    height: 30px;
    box-shadow: 2px 2px 4px 0 #999;
  }

  #run-analysis-btn:not([disabled]):hover {
    cursor: pointer;
  }
523
524
525
</style>

<!--CSS for dynamically created components-->
Sascha Herzinger's avatar
Sascha Herzinger committed
526

527
<style src="../../assets/base.css"></style>
528
<style src="../../assets/svgtooltip.css"></style>
529
<style>
Sascha Herzinger's avatar
Sascha Herzinger committed
530
531
532
533
  .fjs-corr-axis {
    shape-rendering: crispEdges;
  }

534
535
536
  .fjs-corr-axis .tick {
    shape-rendering: crispEdges;
  }
Sascha Herzinger's avatar
Sascha Herzinger committed
537
538
539
540

  .fjs-corr-axis line {
    stroke: #999;
  }
541
</style>