CorrelationAnalysis.vue 15.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

    <input id="run-analysis-btn"
           type="button"
17
           @click="runAnalysisWrapper({init: true})"
18
19
20
21
           value="Run Analysis"
           :disabled="disabled"/>

    <div id="visualisation-section" style="height: 75%;">
22
23
24
25
26
27
28
29
30
      <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
31
32
33
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>
          <td>#Total shown points</td>
          <td>{{ shownPoints.all.length }}</td>
        </tr>
43
      </table>
44
      <svg width="100%" height="100%" v-show="! shownAnalysisResults.init">
45
        <g :style="{ transform: `translate(${margin.left}px, ${margin.top}px)` }">
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
          <g id="x-axis-1" class="fjs-corr-axis" :style="{ transform: `translate(0px, ${padded.height}px)` }"></g>
          <g id="x-axis-2" class="fjs-corr-axis"></g>
          <g id="y-axis-1" class="fjs-corr-axis"></g>
          <g id="y-axis-2" class="fjs-corr-axis" :style="{ transform: `translate(${padded.width}px, 0px)` }"></g>
          <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>
64
65
          <circle class="scatterplot-point"
                  :cx="scales.x(point.x)"
66
67
68
                  :cy="scales.y(point.y)"
                  r="4"
                  :data-idx="idx"
Sascha Herzinger's avatar
Sascha Herzinger committed
69
70
                  v-for="(point, idx) in shownPoints.all"
                  v-tooltip="point.tooltip">
71
          </circle>
72
          <line id="lin-reg-line"
73
74
75
                :x1="tweened.regLine.x1"
                :x2="tweened.regLine.x2"
                :y1="tweened.regLine.y1"
Sascha Herzinger's avatar
Sascha Herzinger committed
76
77
                :y2="tweened.regLine.y2"
                v-tooltip="regLine.tooltip">
78
          </line>
79
80
81
82
83
84
          <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
85
          </rect>
86
87
88
89
90
91
          <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
92
          </rect>
93
94
95
96
        </g>
      </svg>
    </div>

97
  </div>
98
99
100
101
</template>


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

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

        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
276
277
278
        const tooltip = {Slope: this.tmpAnalysisResults.slope, Intercept: this.tmpAnalysisResults.intercept}

        return { x1, x2, y1, y2, tooltip }
279
280
281
282
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
283
          .on('end', () => {
284
285
286
287
288
            if (! d3.event.selection) {
              this.selectedPoints = []
              this.runAnalysisWrapper({init: false})
              return
            }
289
            const [[x0, y0], [x1, y1]] = d3.event.selection
290
            this.selectedPoints = this.shownPoints.all.filter(d => {
291
292
293
              const x = this.scales.x(d.x)
              const y = this.scales.y(d.y)
              return x0 <= x && x <= x1 && y0 <= y && y <= y1;
294
            })
295
            this.runAnalysisWrapper({init: false})
296
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
297
298
      },
      histograms () {
299
300
301
302
303
304
305
306
307
308
309
310
311
312
        const BINS = 10
        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
313
314
315
316
317
318
319
320
321
322
323
324
        return { xBins, yBins }
      },
      histogramScales () {
        // no, I didn't mix up xBins and yBins.
        const x = d3.scaleLinear()
          .domain(d3.extent(this.histograms.yBins.map(d => d.length)))
          .range([0, this.margin.left])
        const y = d3.scaleLinear()
          .domain(d3.extent(this.histograms.xBins.map(d => d.length)))
          .range([0, this.margin.bottom])
        return { x, y }
      },
325
326
327
328
      histogramAttr () {
        const xAttr = this.histograms.xBins.map(d => {
          return {
            x: this.scales.x(d.x0),
329
            y: this.padded.height + 2,
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
            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 }
      }
345
346
    },
    watch: {
347
348
349
350
351
352
      '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)
353
        }
354
355
356
357
      },
      'brush': {
        handler: function(newBrush) {
          d3.select('#brush').call(newBrush)
358
        }
359
360
361
      },
      'regLine': {
        handler: function(newRegLine, oldRegLine) {
362
          const vm = this
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
          const coords = oldRegLine
          const targetCoords = newRegLine
          targetCoords.onUpdate = () => { this.tweened.regLine = coords }
          TweenLite.to(coords, 0.5, targetCoords)
        }
      },
      'histogramAttr': {
        handler: function(newHistogramAttr, oldHistogramAttr) {
          // this is a bit like fibonacci. We need a previous value to start with initially
          if (! oldHistogramAttr.xAttr.length || ! oldHistogramAttr.yAttr.length) {
            this.tweened.histogramAttr = newHistogramAttr
            return
          }
          oldHistogramAttr.xAttr.forEach((d, i) => {
            const attr = oldHistogramAttr.xAttr[i]
            const targetAttr = newHistogramAttr.xAttr[i]
            targetAttr.onUpdate = () => { this.tweened.histogramAttr.xAttr[i] = attr }
            TweenLite.to(attr, 0.5, targetAttr)
          })
          oldHistogramAttr.yAttr.forEach((d, i) => {
            const attr = oldHistogramAttr.yAttr[i]
            const targetAttr = newHistogramAttr.yAttr[i]
            targetAttr.onUpdate = () => { this.tweened.histogramAttr.yAttr[i] = attr }
            TweenLite.to(attr, 0.5, targetAttr)
          })
        }
389
390
      }
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
391
    mounted () {
392
393
      window.addEventListener('resize', this.onResize)
      this.onResize() // initial call
394
      this.tmpAnalysisResults = this.shownAnalysisResults
395
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
396
    beforeDestroy () {
397
398
      window.removeEventListener('resize', this.onResize)
    },
399
400
401
402
    components: {
      DataBox
    },
    mixins: [
Sascha Herzinger's avatar
Sascha Herzinger committed
403
404
      requestHandling,
      tooltip
405
406
    ],
    methods: {
407
      runAnalysisWrapper ({init}) {
408
        // function made available via requestHandling mixin
409
        this.runAnalysis({job_name: 'compute-correlation', args: this.args})
410
          .then(response => {
411
412
            const results = JSON.parse(response)
            results.data = JSON.parse(results.data)
413
414
415
416
417
418
            if (init) {
              this.shownAnalysisResults = results
              this.tmpAnalysisResults = results
            } else {
              this.tmpAnalysisResults = results
            }
419
          })
420
421
422
423
424
425
          .catch(error => console.error(error))
      },
      onResize () {
        const section = this.$el.querySelector('#visualisation-section')
        this.height = section.clientHeight
        this.width = section.clientWidth
426
427
428
429
430
431
      },
      update_xyData (ids) {
        this.xyData = ids
      },
      update_annotationData (ids) {
        this.annotationData = ids
432
      }
433
434
435
436
437
438
    }
  }
</script>


<style scoped>
Sascha Herzinger's avatar
Sascha Herzinger committed
439
440
  * {
    font-family: Roboto, sans-serif;
441
442
  }

443
  #data-box-section {
444
445
446
447
    display: flex;
    flex-direction: row;
    justify-content: space-around;
  }
448

449
450
451
452
  #run-analysis-btn {
    width: 100%;
    height: 20px;
  }
453
454
455
456
457

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

459
  #lin-reg-line :hover {
Sascha Herzinger's avatar
Sascha Herzinger committed
460
    opacity: 0.4;
461
462
  }

463
  .histogram-rect {
464
    stroke: #fff;
465
    shape-rendering: crispEdges;
466
    stroke-width: 0px;
467
468
    fill: #ffd100;
  }
469
470
471
472
473

  .stats-table {
    margin: 5px;
    border-spacing: 0;
    border-collapse: collapse;
Sascha Herzinger's avatar
Sascha Herzinger committed
474
475
    font-size: 14px;
    float: right;
476
477
478
479
480
481
482
  }

  .stats-table, .stats-table td, .stats-table th {
    border: 1px black solid;
    border-collapse: collapse;
    padding: 5px;
  }
483
484
485

  .scatterplot-point :hover {
    fill: #f00;
Sascha Herzinger's avatar
Sascha Herzinger committed
486
    opacity: 0.4;
487
  }
488
489
490
</style>

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

<style src="../../assets/tooltip.css"></style>
493
<style src="../../assets/base.css"></style>
494
495
496
497
<style>
  .fjs-corr-axis .tick {
    shape-rendering: crispEdges;
  }
Sascha Herzinger's avatar
Sascha Herzinger committed
498
499
500
501

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