CorrelationAnalysis.vue 16.5 KB
Newer Older
1
<template>
2
  <chart v-on:resize="resize">
3

4
    <control-panel class="fjs-control-panel">
5
      <data-box class="fjs-data-box"
6
7
                header="Numerical Variables"
                dataType="numerical,numerical_array"
8
9
                v-on:update="update_xyData">
      </data-box>
10
      <data-box class="fjs-data-box"
11
                header="Categorical Variables"
12
                dataType="categorical"
13
                v-on:update="update_categoryData">
14
      </data-box>
15
      <hr class="fjs-seperator"/>
16
17
18
19
      <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>
20
        <br/>
21
22
        <input type="radio" id="fjs-param-method-2" value="spearman" v-model="params.method">
        <label for="fjs-param-method-2">Spearman</label>
23
        <br/>
24
25
26
        <input type="radio" id="fjs-param-method-3" value="kendall" v-model="params.method">
        <label for="fjs-param-method-3">Kendall</label>
      </fieldset>
27
    </control-panel>
28

29
    <svg :height="height" :width="width">
30
      <g :transform="`translate(${margin.left}, ${margin.top})`">
31
        <svg-canvas name="fjs-canvas" :width="padded.width" :height="padded.height"/>
Sascha Herzinger's avatar
Sascha Herzinger committed
32
        <html2svg :right="padded.width">
33
34
          <draggable>
            <div class="fjs-legend">
Sascha Herzinger's avatar
Sascha Herzinger committed
35
36
37
38
39
40
41
42
43
              <span>Corr. Coef.: {{ tmpResults.coef.toFixed(4) }}</span>
              <span>p-value: {{ tmpResults.p_value.toFixed(4) }}</span>
              <div v-for="point, i in legendSubsetPoints">
                <svg :width="pointSize * 2" :height="pointSize * 2">
                  <polygon :points="point"></polygon>
                </svg>
                <span>S{{ i + 1 }}</span>
              </div>
              <div class="fjs-legend-category" v-for="color, i in legendCategoryColors">
Sascha Herzinger's avatar
Sascha Herzinger committed
44
                <div :style="{background: color}"></div>
Sascha Herzinger's avatar
Sascha Herzinger committed
45
46
                <span>&nbsp{{ categories[i] }}</span>
              </div>
47
48
            </div>
          </draggable>
49
        </html2svg>
50
        <crosshair :width="padded.width" :height="padded.height"/>
51
        <g class="fjs-corr-axis fjs-y-axis-2" :transform="`translate(${padded.width}, 0)`"></g>
52
        <g class="fjs-corr-axis fjs-x-axis-2"></g>
53
        <g class="fjs-corr-axis fjs-x-axis-1" :transform="`translate(0, ${padded.height})`"></g>
54
55
        <g class="fjs-corr-axis fjs-y-axis-1"></g>
        <g class="fjs-brush"></g>
56
57
        <text class="fjs-axis-label"
              :x="padded.width / 2"
58
              :y="-margin.top / 2"
59
60
61
              text-anchor="middle">
          {{ shownResults.x_label }}
        </text>
62
63
        <text class="fjs-axis-label"
              text-anchor="middle"
64
              :transform="`translate(${padded.width + margin.right / 2},${padded.height / 2})rotate(90)`">
65
66
67
          {{ shownResults.y_label }}
        </text>
        <line class="fjs-lin-reg-line"
68
69
70
71
              :x1="regLine.x1"
              :x2="regLine.x2"
              :y1="regLine.y1"
              :y2="regLine.y2"
72
              :title="regLine.tooltip"
73
              v-tooltip="{followCursor: true}">
74
        </line>
75
76
77
78
79
80
81
82
83
84
85
86
87
88
        <rect class="fjs-histogram fjs-histogram-bottom"
              :width="bin.width"
              :height="bin.height"
              :x="bin.x"
              :y="bin.y"
              v-for="bin in histogram.bottom">
        </rect>
        <rect class="fjs-histogram fjs-histogram-left"
              :width="bin.width"
              :height="bin.height"
              :x="bin.x"
              :y="bin.y"
              v-for="bin in histogram.left">
        </rect>
89
90
91
92
      </g>
    </svg>

  </chart>
93
94
95
</template>

<script>
Sascha Herzinger's avatar
Sascha Herzinger committed
96
  import DataBox from '../components/DataBox.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
97
  import ControlPanel from '../components/ControlPanel.vue'
98
  import { getPolygonPointsForSubset } from '../mixins/utils'
Sascha Herzinger's avatar
Sascha Herzinger committed
99
  import Chart from '../components/Chart.vue'
100
  import store from '../../store/store'
Sascha Herzinger's avatar
Sascha Herzinger committed
101
  import runAnalysis from '../mixins/run-analysis'
102
  import * as d3 from 'd3'
103
  import tooltip from '../directives/tooltip.js'
104
  import deepFreeze from 'deep-freeze-strict'
105
  import SvgCanvas from '../components/SVGCanvas.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
106
  import Crosshair from '../components/Crosshair.vue'
107
  import Html2svg from '../components/HTML2SVG.vue'
108
  import Draggable from '../components/Draggable.vue'
109
110
  export default {
    name: 'correlation-analysis',
111
    data () {
112
      return {
113
        error: '',
114
115
        width: 0,
        height: 0,
116
        xyData: [],
117
118
        categoryData: [],
        categoryColors: d3.schemeCategory10,
119
120
121
        params: {
          method: 'pearson'
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
122
        shownResults: {  // initially computed
123
124
125
126
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
127
          method: 'pearson',
128
129
          x_label: '',
          y_label: '',
130
          data: []
131
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
132
        tmpResults: {  // on-the-fly computed
133
134
135
136
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
137
          method: 'pearson',
138
139
          x_label: '',
          y_label: '',
140
          data: []
141
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
142
        selectedPoints: [],
143
        hasSetFilter: false
144
145
      }
    },
146
    computed: {
147
148
149
      idFilter () {
        return store.getters.filter('ids')
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
150
151
      validArgs () {
        return this.xyData.length === 2
152
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
153
154
155
      pointSize () {
        return this.padded.width / 100
      },
156
157
      args () {
        return {
158
159
          x: this.xyData[0],
          y: this.xyData[1],
160
          id_filter: this.idFilter,
161
162
          method: this.params.method,
          subsets: store.getters.subsets,
163
          categories: this.categoryData
164
165
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
166
      margin () {
167
        const left = this.width / 4
168
169
        const top = this.height / 20
        const right = this.width / 20
170
171
        const bottom = this.height / 4
        return {left, top, right, bottom}
Sascha Herzinger's avatar
Sascha Herzinger committed
172
      },
173
174
175
      padded () {
        const width = this.width - this.margin.left - this.margin.right
        const height = this.height - this.margin.top - this.margin.bottom
176
        return {width, height}
177
      },
178
      categories () {
179
        return [...new Set(this.shownResults.data.map(d => d.category))]
180
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
181
182
183
      subsets () {
        return [...new Set(this.shownResults.data.map(d => d.subset))]
      },
184
185
186
187
      points () {
        return this.shownResults.data.map(d => {
          const x = this.scales.x(d.value_x)
          const y = this.scales.y(d.value_y)
188
189
          const id = d.id
          const subset = d.subset
Sascha Herzinger's avatar
Sascha Herzinger committed
190
          const shape = getPolygonPointsForSubset({cx: x, cy: y, size: this.pointSize, subset: subset})
191
          const category = d.category
192
193
          let tooltip = `
<div>
194
195
  <p>${d.feature_x}: ${x}</p>
  <p>${d.feature_y}: ${y}</p>
196
  <p>Subset: ${subset}</p>
197
  ${typeof category !== 'undefined' ? '<p>Category: ' + category + '</p>' : ''}
198
199
</div>
`
200
          return {x, y, id, shape, subset, category, tooltip}
201
        })
202
      },
203
204
      scales () {
        const x = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
205
          .domain((() => {
206
            const xExtent = d3.extent(this.shownResults.data.map(d => d.value_x))
Sascha Herzinger's avatar
Sascha Herzinger committed
207
208
209
            const xPadding = (xExtent[1] - xExtent[0]) / 10
            return [xExtent[0] - xPadding, xExtent[1] + xPadding]
          })())
210
211
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
212
          .domain((() => {
213
            const yExtent = d3.extent(this.shownResults.data.map(d => d.value_y))
Sascha Herzinger's avatar
Sascha Herzinger committed
214
215
216
            const yPadding = (yExtent[1] - yExtent[0]) / 10
            return [yExtent[0] - yPadding, yExtent[1] + yPadding]
          })())
217
          .range([this.padded.height, 0])
218
        return {x, y}
219
220
221
222
223
      },
      axis () {
        const x1 = d3.axisTop(this.scales.x)
        const y1 = d3.axisRight(this.scales.y)
        const x2 = d3.axisBottom(this.scales.x)
224
          .tickSizeInner(this.padded.height)
225
226
          .tickFormat('')
        const y2 = d3.axisLeft(this.scales.y)
227
          .tickSizeInner(this.padded.width)
228
          .tickFormat('')
229
        return {x1, x2, y1, y2}
230
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
      legendSubsetPoints () {
        return this.subsets.map(subset => {
          return getPolygonPointsForSubset({
            cx: this.pointSize,
            cy: this.pointSize,
            size: this.pointSize,
            subset: subset
          })
        })
      },
      legendCategoryColors () {
        return this.categories.map(category => {
          return this.categoryColors[this.categories.indexOf(category) % this.categoryColors.length]
        })
      },
246
      regLine () {
247
248
249
        const xValues = this.tmpResults.data.map(d => d.value_x)
        const minX = d3.min(xValues)
        const maxX = d3.max(xValues)
250
251
252
253
        let x1 = this.scales.x(minX) || 0
        let y1 = this.scales.y(this.tmpResults.intercept + this.tmpResults.slope * minX) || 0
        let x2 = this.scales.x(maxX) || 0
        let y2 = this.scales.y(this.tmpResults.intercept + this.tmpResults.slope * maxX) || 0
254

255
256
        x1 = x1 < 0 ? 0 : x1
        x1 = x1 > this.width ? this.width : x1
257

258
259
        x2 = x2 < 0 ? 0 : x2
        x2 = x2 > this.width ? this.width : x2
260

261
262
        y1 = y1 < 0 ? 0 : y1
        y1 = y1 > this.height ? this.height : y1
263

264
265
        y2 = y2 < 0 ? 0 : y2
        y2 = y2 > this.height ? this.height : y2
266

267
268
269
270
271
272
        const tooltip = `
<div>
  <p>Slope: ${this.tmpResults.slope}</p>
  <p>Intercept: ${this.tmpResults.intercept}</p>
</div>
`
273
        return {x1, x2, y1, y2, tooltip}
274
275
276
277
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
278
          .on('end', () => {
279
            this.error = ''
280
            if (!d3.event.selection) {
281
282
283
              if (this.selectedPoints.length === 0) {
                return
              }
284
              this.selectedPoints = []
285
            } else {
286
              const [[x0, y0], [x1, y1]] = d3.event.selection
287
288
              this.selectedPoints = this.points.filter(d => {
                return x0 <= d.x && d.x <= x1 && y0 <= d.y && d.y <= y1
289
290
291
              })
              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
292
                return
293
              }
294
            }
Sascha Herzinger's avatar
Sascha Herzinger committed
295
            store.dispatch('setFilter', {filter: 'ids', value: this.selectedPoints.map(d => d.id)})
296
            this.hasSetFilter = true
297
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
298
      },
299
      histogramBins () {
300
        const BINS = 14
301
302
        let xBins = []
        let yBins = []
303
304
305
306
        const xValues = this.tmpResults.data.map(d => d.value_x)
        const yValues = this.tmpResults.data.map(d => d.value_y)
        const [xMin, xMax] = d3.extent(xValues)
        const [yMin, yMax] = d3.extent(yValues)
307
308
        const xThresholds = d3.range(xMin, xMax, (xMax - xMin) / BINS)
        const yThresholds = d3.range(yMin, yMax, (yMax - yMin) / BINS)
309
        if (xValues.length) {
310
          xBins = d3.histogram()
311
            .domain([xMin, xMax])
312
            .thresholds(xThresholds)(xValues)
313
        }
314
        if (yValues.length) {
315
          yBins = d3.histogram()
316
            .domain([yMin, yMax])
317
            .thresholds(yThresholds)(yValues)
318
        }
319
        return {xBins, yBins}
Sascha Herzinger's avatar
Sascha Herzinger committed
320
321
      },
      histogramScales () {
322
323
        const xExtent = d3.extent(this.histogramBins.xBins.map(d => d.length))
        const yExtent = d3.extent(this.histogramBins.yBins.map(d => d.length))
324
        // no, I didn't mix up x and y.
Sascha Herzinger's avatar
Sascha Herzinger committed
325
        const x = d3.scaleLinear()
326
327
          .domain(yExtent)
          .range([yExtent[0] ? 10 : 0, this.margin.left])
Sascha Herzinger's avatar
Sascha Herzinger committed
328
        const y = d3.scaleLinear()
329
330
          .domain(xExtent)
          .range([xExtent[0] ? 10 : 0, this.margin.bottom])
331
        return {x, y}
Sascha Herzinger's avatar
Sascha Herzinger committed
332
      },
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
      histogram () {
        const bottom = this.histogramBins.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 left = this.histogramBins.yBins.map(d => {
          return {
            x: -this.histogramScales.x(d.length),
            y: this.scales.y(d.x0),
            width: this.histogramScales.x(d.length),
            height: this.scales.y(d.x0) - this.scales.y(d.x1)
          }
        })
Sascha Herzinger's avatar
Sascha Herzinger committed
350
        return { bottom, left }
351
      }
352
    },
353
354
355
    // IMPORTANT: If the code within the watchers does interact with the DOM the code should be wrapped into a $nextTick
    // statement. This helps with the integration into the Vue component lifecycle. E.g.: an animation can't be
    // applied to an element that does not exist yet.
356
    watch: {
357
358
359
360
      'args': {
        handler: function (newArgs, oldArgs) {
          const init = newArgs.x !== oldArgs.x ||
            newArgs.y !== oldArgs.y ||
361
362
            JSON.stringify(newArgs.categories) !== JSON.stringify(oldArgs.categories) ||
            !this.hasSetFilter
Sascha Herzinger's avatar
Sascha Herzinger committed
363
          if (this.validArgs) {
364
            this.runAnalysisWrapper({init, args: newArgs})
365
          }
366
          this.hasSetFilter = false
367
368
        }
      },
369
      'axis': {
370
        handler: function (newAxis) {
371
          this.$nextTick(() => {
372
            d3.select(this.$el.querySelector('.fjs-y-axis-2')).call(newAxis.y2)
Sascha Herzinger's avatar
Sascha Herzinger committed
373
            d3.select(this.$el.querySelector('.fjs-x-axis-2')).call(newAxis.x2)
374
            d3.select(this.$el.querySelector('.fjs-x-axis-1')).call(newAxis.x1)
Sascha Herzinger's avatar
Sascha Herzinger committed
375
            d3.select(this.$el.querySelector('.fjs-y-axis-1')).call(newAxis.y1)
376
          })
377
        }
378
379
      },
      'brush': {
380
        handler: function (newBrush) {
381
          this.$nextTick(() => {
Sascha Herzinger's avatar
Sascha Herzinger committed
382
            d3.select(this.$el.querySelector('.fjs-brush')).call(newBrush)
383
          })
384
        }
385
386
387
388
389
      },
      'points': {
        handler: function (newPoints) {
          this.$nextTick(() => this.drawPoints(newPoints))
        }
390
391
      }
    },
392
    components: {
393
      Draggable,
394
      Html2svg,
395
      SvgCanvas,
396
      ControlPanel,
Sascha Herzinger's avatar
Sascha Herzinger committed
397
      DataBox,
Sascha Herzinger's avatar
Sascha Herzinger committed
398
399
      Chart,
      Crosshair
400
    },
401
402
403
    directives: {
      tooltip
    },
404
    methods: {
405
      runAnalysisWrapper ({init, args}) {
406
        // function made available via requestHandling mixin
Sascha Herzinger's avatar
Sascha Herzinger committed
407
        runAnalysis({task_name: 'compute-correlation', args})
408
          .then(response => {
409
            const results = JSON.parse(response)
410
            results.data = JSON.parse(results.data)
411
            deepFreeze(results) // massively improve performance by telling Vue that the objects properties won't change
412
            if (init) {
Sascha Herzinger's avatar
Sascha Herzinger committed
413
414
              this.shownResults = results
              this.tmpResults = results
415
            } else {
Sascha Herzinger's avatar
Sascha Herzinger committed
416
              this.tmpResults = results
417
            }
418
          })
419
420
          .catch(error => console.error(error))
      },
421
      drawPoints (points) {
422
        const canvas = this.$el.querySelector('.fjs-canvas')
423
424
425
426
427
428
429
430
431
432
433
434
435
        const ctx = canvas.getContext('2d')
        ctx.clearRect(0, 0, canvas.width, canvas.height)
        points.forEach(d => {
          ctx.beginPath()
          ctx.fillStyle = this.categoryColors[this.categories.indexOf(d.category) % this.categoryColors.length]
          ctx.moveTo(d.shape[0], d.shape[1])
          for (let i = 2; i < d.shape.length - 1; i += 2) {
            ctx.lineTo(d.shape[i], d.shape[i + 1])
          }
          ctx.closePath()
          ctx.fill()
        })
      },
436
437
438
      resize ({height, width}) {
        this.height = height
        this.width = width
439
440
441
442
      },
      update_xyData (ids) {
        this.xyData = ids
      },
443
444
      update_categoryData (ids) {
        this.categoryData = ids
445
      }
446
447
448
449
450
    }
  }
</script>


451
<style lang="sass" scoped>
452
  @import '~assets/base.sass'
453

454
455
456
457
458
  .fjs-control-panel
    .fjs-correlation-method
      white-space: nowrap
      border: solid 1px #fff
      text-align: left
459
      border-radius: 3px
460
      padding: 0 0.5vw 0 0.5vw
461
462
463
464
465
466
  svg
    .fjs-lin-reg-line
      stroke: #ff5e00
      stroke-width: 0.3%
    .fjs-lin-reg-line:hover
      opacity: 0.4
467
    .fjs-histogram
468
      shape-rendering: crispEdges
469
470
      stroke: #fff
      stroke-width: 1px
471
472
473
474
475
476
477
      fill: #ffd100
    .fjs-scatterplot-point
      stroke-width: 0
    .fjs-scatterplot-point:hover
      fill: #f00
    .fjs-brush
      stroke-width: 0
478
479
480
  .fjs-legend
    display: flex
    flex-direction: column
Sascha Herzinger's avatar
Sascha Herzinger committed
481
482
483
484
485
486
487
    resize: both
    overflow: auto
    transform: translateZ(0)
    polygon
      fill: #7b7b7b
    .fjs-legend-category
      display: flex
Sascha Herzinger's avatar
Sascha Herzinger committed
488
489
490
491
492
493
494
      div
        flex: 1
      span
        flex: 8
        overflow: hidden
        white-space: nowrap
        text-overflow: ellipsis
495
496
497
</style>

<!--CSS for dynamically created components-->
498
499
500
501
502
<style lang="sass">
  .fjs-corr-axis
    shape-rendering: crispEdges
    .tick
      shape-rendering: crispEdges
Sascha Herzinger's avatar
Sascha Herzinger committed
503
      line
504
        stroke: #E2E2E2
Sascha Herzinger's avatar
Sascha Herzinger committed
505
      text
506
        font-size: 0.75em
507
508
    path
      stroke: none
509
</style>