CorrelationAnalysis.vue 13 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
      <svg width="100%" height="100%" v-show="! shownAnalysisResults.init">
23
24
25
26
27
        <g :style="{ transform: `translate(${margin.left}px, ${margin.top}px)` }">
          <circle :cx="scales.x(point.x)"
                  :cy="scales.y(point.y)"
                  r="4"
                  :data-idx="idx"
28
                  v-for="(point, idx) in shownPoints.all">
29
          </circle>
30
          <line id="lin-reg-line"
31
32
33
34
                :x1="tweened.regLine.x1"
                :x2="tweened.regLine.x2"
                :y1="tweened.regLine.y1"
                :y2="tweened.regLine.y2">
35
          </line>
36
37
38
39
40
          <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>
41
42
43
44
45
46
          <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
47
          </rect>
48
49
50
51
52
53
          <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
54
          </rect>
55
56
57
58
        </g>
      </svg>
    </div>

59
  </div>
60
61
62
63
</template>


<script>
64
65
  import DataBox from '../DataBox.vue'
  import requestHandling from '../mixins/request-handling'
66
  import * as d3 from 'd3'
67
  import { TweenLite } from 'gsap'
68
69
  export default {
    name: 'correlation-analysis',
70
    data () {
71
      return {
72
73
        width: 0,
        height: 0,
74
75
76
77
78
        xyData: [],
        annotationData: [],
        get args () {
          return {
            x: `$${this.xyData[0]}$`,
79
80
            y: `$${this.xyData[1]}$`,
            ids: this.selectedPoints.map(d => d.id)
81
          }
82
        },
83

84
        shownAnalysisResults: {
85
          init: true,  // will disappear after being initially set
86
87
88
89
90
91
92
93
94
95
96
97
98
99
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
          method: '',
          x_label: '',
          y_label: '',
          get data() {
            return {
              id: [],
              [this.x_label]: [],
              [this.y_label]: []
            }
          }
100
        },
101
        tmpAnalysisResults: {
102
          init: true,  // will disappear after being initially set
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
          method: '',
          x_label: '',
          y_label: '',
          get data() {
            return {
              id: [],
              [this.x_label]: [],
              [this.y_label]: []
            }
          }
        },
118
        selectedPoints: [],
119
120
121
122
123
124
125
        tweened: {
          regLine: {},
          histogramAttr: {
            xAttr: [],
            yAttr: []
          },
        }
126
127
      }
    },
128
129
130
    computed: {
      disabled () {
        return this.xyData.length !== 2
131
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
132
133
      margin () {
        const left = this.width / 3
134
135
        const top = 20
        const right = 20
Sascha Herzinger's avatar
Sascha Herzinger committed
136
137
138
        const bottom = this.height / 3
        return { left, top, right, bottom }
      },
139
140
141
142
      padded () {
        const width = this.width - this.margin.left - this.margin.right
        const height = this.height - this.margin.top - this.margin.bottom
        return { width, height }
143
      },
144
145
      shownPoints () {
        const xs = [], ys = [], ids = []
146
147
148
149
150
151
152
153
154
155
156
157
        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]
            xs.push(x)
            ys.push(y)
            ids.push(id)
            return {x, y, id}
          })
        }
158
159
160
161
        return { xs, ys, ids, all }
      },
      tmpPoints() {
        const xs = [], ys = [], ids = []
162
163
164
165
166
167
168
169
170
171
172
173
        let all = []
        if (! this.tmpAnalysisResults.init) {
          const all = Object.keys(this.tmpAnalysisResults.data.id).map(key => {
            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}
          })
        }
174
        return { xs, ys, ids, all }
175
      },
176
177
      scales () {
        const x = d3.scaleLinear()
178
          .domain(d3.extent(this.shownPoints.xs))
179
180
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
181
          .domain(d3.extent(this.shownPoints.ys))
182
183
184
          .range([this.padded.height, 0])
        return { x, y }
      },
185
186
187
188
189
190
191
192
193
      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 }
      },
194
195
196
197
198
199
200
201
202
203
204
      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 }
      },
205
      regLine () {
206
        if (this.tmpAnalysisResults.init) {
207
208
          return { x1: 0, x2: 0, y1: 0, y2: 0 }
        }
209
210
        const minX = d3.min(this.tmpPoints.xs)
        const maxX = d3.max(this.tmpPoints.xs)
211
        let x1 = this.scales.x(minX)
212
        let y1 = this.scales.y(this.tmpAnalysisResults.intercept + this.tmpAnalysisResults.slope * minX)
213
        let x2 = this.scales.x(maxX)
214
        let y2 = this.scales.y(this.tmpAnalysisResults.intercept + this.tmpAnalysisResults.slope * maxX)
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232

        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;

        return { x1, x2, y1, y2 }
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
233
          .on('end', () => {
234
235
236
237
238
            if (! d3.event.selection) {
              this.selectedPoints = []
              this.runAnalysisWrapper({init: false})
              return
            }
239
            const [[x0, y0], [x1, y1]] = d3.event.selection
240
            this.selectedPoints = this.shownPoints.all.filter(d => {
241
242
243
              const x = this.scales.x(d.x)
              const y = this.scales.y(d.y)
              return x0 <= x && x <= x1 && y0 <= y && y <= y1;
244
            })
245
            this.runAnalysisWrapper({init: false})
246
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
247
248
      },
      histograms () {
249
250
251
252
253
254
255
256
257
258
259
260
261
262
        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
263
264
265
266
267
268
269
270
271
272
273
274
        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 }
      },
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
      histogramAttr () {
        const xAttr = this.histograms.xBins.map(d => {
          return {
            x: this.scales.x(d.x0),
            y: this.padded.height,
            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 }
      }
295
296
    },
    watch: {
297
298
299
300
301
302
      '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)
303
        }
304
305
306
307
      },
      'brush': {
        handler: function(newBrush) {
          d3.select('#brush').call(newBrush)
308
        }
309
310
311
      },
      'regLine': {
        handler: function(newRegLine, oldRegLine) {
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
          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)
          })
        }
338
339
      }
    },
340
341
342
    mounted() {
      window.addEventListener('resize', this.onResize)
      this.onResize() // initial call
343
      this.tmpAnalysisResults = this.shownAnalysisResults
344
345
346
347
    },
    beforeDestroy() {
      window.removeEventListener('resize', this.onResize)
    },
348
349
350
351
352
353
354
    components: {
      DataBox
    },
    mixins: [
      requestHandling
    ],
    methods: {
355
      runAnalysisWrapper ({init}) {
356
        // function made available via requestHandling mixin
357
        this.runAnalysis({job_name: 'compute-correlation', args: this.args})
358
          .then(response => {
359
360
            const results = JSON.parse(response)
            results.data = JSON.parse(results.data)
361
362
363
364
365
366
            if (init) {
              this.shownAnalysisResults = results
              this.tmpAnalysisResults = results
            } else {
              this.tmpAnalysisResults = results
            }
367
          })
368
369
370
371
372
373
          .catch(error => console.error(error))
      },
      onResize () {
        const section = this.$el.querySelector('#visualisation-section')
        this.height = section.clientHeight
        this.width = section.clientWidth
374
375
376
377
378
379
      },
      update_xyData (ids) {
        this.xyData = ids
      },
      update_annotationData (ids) {
        this.annotationData = ids
380
      }
381
382
383
384
385
386
    }
  }
</script>


<style scoped>
387
  #data-box-section {
388
389
390
391
    display: flex;
    flex-direction: row;
    justify-content: space-around;
  }
392

393
394
395
396
  #run-analysis-btn {
    width: 100%;
    height: 20px;
  }
397
398
399
400
401

  #lin-reg-line {
    stroke: #ff5e00;
    stroke-width: 4px;
  }
402
403
404
405
406
407
408

  .histogram-rect {
    stroke: #FFF;
    shape-rendering: crispEdges;
    stroke-width: 1px;
    fill: #ffd100;
  }
409
410
411
412
413
414
415
</style>

<!--CSS for dynamically created components-->
<style>
  .fjs-corr-axis .tick {
    shape-rendering: crispEdges;
  }
416
</style>