CorrelationAnalysis.vue 18.9 KB
Newer Older
1
<template>
2
  <div :class="`fjs-correlation-analysis fjs-vm-uid-${this._uid}`">
3
4
5
    <div class="fjs-data-box-container">
      <data-box class="fjs-data-box"
                header="X and Y variables"
6
7
8
                dataType="numerical"
                v-on:update="update_xyData">
      </data-box>
9
10
      <data-box class="fjs-data-box"
                header="Annotations"
11
12
13
                dataType="categorical"
                v-on:update="update_annotationData">
      </data-box>
14
    </div>
15

16
    <div class="fjs-parameter-container">
17
      <span>{{ error }}</span>
18
19
20
21
22
23
24
25
26
      <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>
27
    </div>
28

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


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

308
309
        x1 = x1 < 0 ? 0 : x1
        x1 = x1 > this.width ? this.width : x1
310

311
312
        x2 = x2 < 0 ? 0 : x2
        x2 = x2 > this.width ? this.width : x2
313

314
315
        y1 = y1 < 0 ? 0 : y1
        y1 = y1 > this.height ? this.height : y1
316

317
318
        y2 = y2 < 0 ? 0 : y2
        y2 = y2 > this.height ? this.height : y2
319

Sascha Herzinger's avatar
Sascha Herzinger committed
320
321
322
        const tooltip = {Slope: this.tmpAnalysisResults.slope, Intercept: this.tmpAnalysisResults.intercept}

        return { x1, x2, y1, y2, tooltip }
323
324
325
326
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
327
          .on('end', () => {
328
            this.error = ''
329
            if (!d3.event.selection) {
330
              this.selectedPoints = []
331
            } else {
332
333
334
335
336
337
338
339
              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
340
                return
341
              }
342
            }
Sascha Herzinger's avatar
Sascha Herzinger committed
343
            store.dispatch('setFilter', {filter: 'ids', value: this.selectedPoints.map(d => d.id)})
344
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
345
346
      },
      histograms () {
347
        const BINS = 14
348
349
350
        let xBins = []
        let yBins = []
        if (!this.tmpAnalysisResults.init) {
351
352
          const [xMin, xMax] = d3.extent(this.tmpPoints.xs)
          const [yMin, yMax] = d3.extent(this.tmpPoints.ys)
353
354
355
          const xThresholds = d3.range(xMin, xMax, (xMax - xMin) / BINS)
          const yThresholds = d3.range(yMin, yMax, (yMax - yMin) / BINS)
          xBins = d3.histogram()
356
            .domain([xMin, xMax])
357
358
            .thresholds(xThresholds)(this.tmpPoints.xs)
          yBins = d3.histogram()
359
            .domain([yMin, yMax])
360
361
            .thresholds(yThresholds)(this.tmpPoints.ys)
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
362
363
364
        return { xBins, yBins }
      },
      histogramScales () {
365
366
        const xExtent = d3.extent(this.histograms.xBins.map(d => d.length))
        const yExtent = d3.extent(this.histograms.yBins.map(d => d.length))
367
        // no, I didn't mix up x and y.
Sascha Herzinger's avatar
Sascha Herzinger committed
368
        const x = d3.scaleLinear()
369
370
          .domain(yExtent)
          .range([yExtent[0] ? 10 : 0, this.margin.left])
Sascha Herzinger's avatar
Sascha Herzinger committed
371
        const y = d3.scaleLinear()
372
373
          .domain(xExtent)
          .range([xExtent[0] ? 10 : 0, this.margin.bottom])
Sascha Herzinger's avatar
Sascha Herzinger committed
374
375
        return { x, y }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
376
377
378
379
380
381
382
383
384
385
386
387
388
389
      histPolyPoints () {
        const bottom = this.histograms.xBins.map(d => {
          return `${this.scales.x(d.x0)},${this.padded.height}, ` +
            `${this.scales.x(d.x0)},${this.padded.height + this.histogramScales.y(d.length)} ` +
            `${this.scales.x(d.x1)},${this.padded.height + this.histogramScales.y(d.length)} ` +
            `${this.scales.x(d.x1)},${this.padded.height}`
        }).join(' ')
        const left = this.histograms.yBins.map(d => {
          return `${0},${this.scales.y(d.x0)} ` +
            `${-this.histogramScales.x(d.length)},${this.scales.y(d.x0)} ` +
            `${-this.histogramScales.x(d.length)},${this.scales.y(d.x1)} ` +
            `${0},${this.scales.y(d.x1)}`
        }).join(' ')
        return { bottom, left }
390
      }
391
392
    },
    watch: {
Sascha Herzinger's avatar
Sascha Herzinger committed
393
394
395
396
397
398
399
400
401
402
403
404
      'regLine': {
        handler: function (newRegLine) {
          TweenLite.to(this.tweened.regLine, 0.5, newRegLine)
        }
      },
      'histPolyPoints': {
        handler: function (newPoints) {
          // we use d3 instead of TweenLite here because d3 can transition point paths
          d3.select('.fjs-histogram-polyline.fjs-bottom').transition().duration(500).attr('points', newPoints.bottom)
          d3.select('.fjs-histogram-polyline.fjs-left').transition().duration(500).attr('points', newPoints.left)
        }
      },
405
406
      'args': {
        handler: function (newArgs, oldArgs) {
Sascha Herzinger's avatar
Sascha Herzinger committed
407
          // if our data selection change we will want to re-initialize the current view
408
409
410
          const init = newArgs.x !== oldArgs.x ||
            newArgs.y !== oldArgs.y ||
            JSON.stringify(newArgs.annotations) !== JSON.stringify(oldArgs.annotations)
Sascha Herzinger's avatar
Sascha Herzinger committed
411
412
          const args = this.args
          args.id_filter = init ? [] : args.id_filter
413
          if (!this.disabled) {
Sascha Herzinger's avatar
Sascha Herzinger committed
414
            this.runAnalysisWrapper({init, args})
415
416
417
          }
        }
      },
418
      'axis': {
419
        handler: function (newAxis) {
420
421
422
423
          d3.select(`.fjs-vm-uid-${this._uid} .fjs-x-axis-1`).call(newAxis.x1)
          d3.select(`.fjs-vm-uid-${this._uid} .fjs-x-axis-2`).call(newAxis.x2)
          d3.select(`.fjs-vm-uid-${this._uid} .fjs-y-axis-1`).call(newAxis.y1)
          d3.select(`.fjs-vm-uid-${this._uid} .fjs-y-axis-2`).call(newAxis.y2)
424
        }
425
426
      },
      'brush': {
427
        handler: function (newBrush) {
428
          d3.select(`.fjs-vm-uid-${this._uid} .fjs-brush`).call(newBrush)
429
        }
430
      },
431
432
433
434
435
      '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) {
Sascha Herzinger's avatar
Sascha Herzinger committed
436
437
            // FIXME: This will probably not work because args is a computed property, not static data
            this.args.id_filter = newIDFilter
438
439
          }
        }
440
441
      }
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
442
    mounted () {
443
      window.addEventListener('resize', this.handleResize)
444
      this.handleResize()
445
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
446
    beforeDestroy () {
447
      window.removeEventListener('resize', this.handleResize)
448
    },
449
    components: {
Sascha Herzinger's avatar
Sascha Herzinger committed
450
      DataBox,
Sascha Herzinger's avatar
Sascha Herzinger committed
451
      TaskView
452
453
    },
    mixins: [
Sascha Herzinger's avatar
Sascha Herzinger committed
454
      requestHandling,
455
      svgtooltip
456
457
    ],
    methods: {
458
      runAnalysisWrapper ({init, args}) {
459
        // function made available via requestHandling mixin
460
        this.runAnalysis({task_name: 'compute-correlation', args})
461
          .then(response => {
462
            const results = JSON.parse(response)
463
464
            const data = JSON.parse(results.data)
            results.data = Object.keys(data).map(key => data[key])
465
            deepFreeze(results) // massively improve performance by telling Vue that the objects properties won't change
466
467
468
469
470
471
            if (init) {
              this.shownAnalysisResults = results
              this.tmpAnalysisResults = results
            } else {
              this.tmpAnalysisResults = results
            }
472
          })
473
474
          .catch(error => console.error(error))
      },
475
      handleResize () {
476
        const container = this.$el.querySelector(`.fjs-vm-uid-${this._uid} .fjs-vis-container svg`)
477
        // noinspection JSSuspiciousNameCombination
478
479
        this.height = container.getBoundingClientRect().width
        this.width = container.getBoundingClientRect().width
480
481
482
483
484
485
      },
      update_xyData (ids) {
        this.xyData = ids
      },
      update_annotationData (ids) {
        this.annotationData = ids
486
      }
487
488
489
490
491
    }
  }
</script>


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

  *
    font-family: Roboto, sans-serif

498
  .fjs-correlation-analysis
499
500
    height: 100%
    width: 100%
501
502
    display: flex
    flex-direction: column
503

504
505
    .fjs-data-box-container
      height: 160px
Sascha Herzinger's avatar
Sascha Herzinger committed
506
507
      display: flex
      justify-content: space-around
508

509
    .fjs-parameter-container
510
      text-align: center
511
512
513
      .fjs-correlation-method
        width: 0
        white-space: nowrap
Sascha Herzinger's avatar
Sascha Herzinger committed
514
515
516
517
        border: solid 1px #bbb
        text-align: left
        border-radius: 8px
        margin: 10px
518

519

520
521
522
523
524
525
526
527
528
529
    .fjs-vis-container
      flex: 1
      display: flex
      svg
        flex: 1
        .fjs-lin-reg-line
          stroke: #ff5e00
          stroke-width: 4px
        .fjs-lin-reg-line:hover
          opacity: 0.4
Sascha Herzinger's avatar
Sascha Herzinger committed
530
        .fjs-histogram-polyline
531
          shape-rendering: crispEdges
Sascha Herzinger's avatar
Sascha Herzinger committed
532
          stroke: white
533
          stroke-width: 1px
534
535
          fill: #ffd100
        .fjs-scatterplot-point
536
          stroke-width: 2
537
538
539
540
541
        .fjs-scatterplot-point:hover
          fill: #f00
        .fjs-brush
          stroke-width: 0
      .fjs-table-container
Sascha Herzinger's avatar
Sascha Herzinger committed
542
        width: 200px
543
544
545
546
547
548
549
        display: flex
        flex-direction: column
        .fjs-stats-table
          margin: 5px
          border-spacing: 0
          border-collapse: collapse
          font-size: 14px
Sascha Herzinger's avatar
Sascha Herzinger committed
550
551
552
553
554
555
556
557
558
559
          tr:nth-child(even)
            background-color: #ddd
          td, th
            max-width: 100px
            overflow: hidden
            text-overflow: ellipsis
            white-space: nowrap
            border: 1px #ccc solid
            border-collapse: collapse
            padding: 5px
560
561
562
</style>

<!--CSS for dynamically created components-->
563
564
565
566
567
568
569
570
571
<style lang="sass">
  @import './src/assets/svgtooltip.sass'

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