PCA.vue 15.1 KB
Newer Older
Sascha Herzinger's avatar
Sascha Herzinger committed
1
<template>
2
  <chart v-on:resize="resize">
Sascha Herzinger's avatar
Sascha Herzinger committed
3

4
    <control-panel class="fjs-control-panel" name="PCA Panel">
Sascha Herzinger's avatar
Sascha Herzinger committed
5
      <data-box class="fjs-data-box"
6
                header="Numerical Variables"
7
                :dataTypes="['numerical', 'numerical_array']"
Sascha Herzinger's avatar
Sascha Herzinger committed
8
9
10
                v-on:update="update_featureData">
      </data-box>
      <data-box class="fjs-data-box"
11
                header="Categorical Variables"
12
                :dataTypes="['categorical']"
Sascha Herzinger's avatar
Sascha Herzinger committed
13
14
                v-on:update="update_categoryData">
      </data-box>
15
      <hr class="fjs-seperator"/>
16
      <div>
17
18
        <label>
          <select v-model="pcX">
19
            <option :value="i" v-for="i in pcomponents">Principal Component {{i + 1}}</option>
20
21
22
23
24
25
          </select>
          X-Axis
        </label>
        <br/>
        <label>
          <select v-model="pcY">
26
            <option :value="i" v-for="i in pcomponents">Principal Component {{i + 1}}</option>
27
28
29
          </select>
          Y-Axis
        </label>
30
      </div>
31
      <div>
32
33
34
35
        <label>
          <input type="checkbox" v-model="params.whiten"/>
          Whiten Output
        </label>
36
      </div>
Sascha Herzinger's avatar
Sascha Herzinger committed
37
38
    </control-panel>

39
    <svg :width="width" :height="height">
40
      <g :transform="`translate(${margin.left}, ${margin.top})`">
Sascha Herzinger's avatar
Sascha Herzinger committed
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
        <html2svg :right="padded.width">
          <draggable>
            <div class="fjs-legend">
              <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">
                <div :style="{background: color}"></div>
                <span>&nbsp{{ categories[i] }}</span>
              </div>
            </div>
          </draggable>
        </html2svg>
57
58
59
60
        <g class="fjs-axis" ref="yAxis2" :transform="`translate(${padded.width}, 0)`"></g>
        <g class="fjs-axis" ref="xAxis2"></g>
        <g class="fjs-axis" ref="xAxis1" :transform="`translate(0, ${padded.height})`"></g>
        <g class="fjs-axis" ref="yAxis1"></g>
61
        <crosshair :width="padded.width" :height="padded.height"/>
62
        <image :xlink:href="dataUrls.main" :width="padded.width" :height="padded.height"></image>
63
        <g class="fjs-brush" ref="brush"></g>
64
        <text :x="padded.width / 2"
65
              :y="- margin.top / 2"
Sascha Herzinger's avatar
Sascha Herzinger committed
66
              text-anchor="middle"
67
              v-show="results.data.id.length">
68
          Principal Component {{pcX + 1}} (Variance Ratio: {{ results.variance_ratios[pcX].toFixed(2) }})
69
70
        </text>
        <text text-anchor="middle"
Sascha Herzinger's avatar
Sascha Herzinger committed
71
              :transform="`translate(${this.padded.width + this.margin.right / 2}, ${this.padded.height / 2})rotate(90)`"
72
              v-show="results.data.id.length">
73
          Principal Component {{pcY + 1}} (Variance Ratio: {{ results.variance_ratios[pcY].toFixed(2) }})
74
        </text>
75
        <g v-for="loading in loadings">
76
77
78
79
80
81
82
83
84
85
86
87
          <line class="fjs-loadings"
                :x1="loading.x1"
                :x2="loading.x2"
                :y1="loading.y1"
                :y2="loading.y2">
          </line>
          <text class="fjs-loading-label"
                :x="loading.x2"
                :y="loading.y2"
                text-anchor="middle">
            {{ loading.feature }}
          </text>
88
89
90
          <g class="fjs-pc-distribution fjs-pc-x-distribution"
             :transform="`translate(0, ${padded.height + margin.bottom / 2})`">
            <line :x2="padded.width"></line>
91
            <image :xlink:href="dataUrls.xDist"
92
93
94
95
                   :y="-pointSize / 2"
                   :width="padded.width"
                   :height="pointSize">
            </image>
96
          </g>
97
98
99
          <g class="fjs-pc-distribution fjs-pc-y-distribution"
             :transform="`translate(${- margin.left / 2}, 0)`">
            <line :y2="padded.height"></line>
100
            <image :xlink:href="dataUrls.yDist"
101
102
103
104
                   :x="-pointSize / 2"
                   :width="pointSize"
                   :height="padded.height">
            </image>
105
          </g>
106
        </g>
107
108
109
      </g>
    </svg>
  </chart>
Sascha Herzinger's avatar
Sascha Herzinger committed
110
111
112
113
114
115
</template>

<script>
  import DataBox from '../components/DataBox.vue'
  import ControlPanel from '../components/ControlPanel.vue'
  import Chart from '../components/Chart.vue'
116
  import { getPolygonPointsForSubset } from '../../utils/utils'
Sascha Herzinger's avatar
Sascha Herzinger committed
117
  import store from '../../store/store'
118
  import runAnalysis from '../../utils/run-analysis'
Sascha Herzinger's avatar
Sascha Herzinger committed
119
120
121
  import * as d3 from 'd3'
  import tooltip from '../directives/tooltip.js'
  import deepFreeze from 'deep-freeze-strict'
Sascha Herzinger's avatar
Sascha Herzinger committed
122
  import Crosshair from '../components/Crosshair.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
123
124
  import Html2svg from '../components/HTML2SVG.vue'
  import Draggable from '../components/Draggable.vue'
125
126
  import getHDPICanvas from '../../utils/high-dpi-canvas'
  import StateSaver from '../mixins/state-saver'
Sascha Herzinger's avatar
Sascha Herzinger committed
127
128
129
130
131
132
133
134
  export default {
    name: 'pca-analysis',
    data () {
      return {
        height: 0,
        width: 0,
        featureData: [],
        categoryData: [],
135
        results: {
136
137
138
139
140
141
142
143
144
145
146
147
          data: {
            0: [],
            1: [],
            id: [],
            subset: [],
            category: []
          },
          loadings: {
            0: [],
            1: [],
            feature: []
          },
148
          variance_ratios: [0, 0]
149
        },
150
151
        pcX: 0,
        pcY: 1,
Sascha Herzinger's avatar
Sascha Herzinger committed
152
        categoryColors: d3.schemeCategory10,
153
154
        subsetColors: d3.schemeCategory10.slice().reverse(),
        selectedPoints: [],
155
156
157
        hasSetFilter: false,
        params: {
          whiten: false
158
159
160
161
162
        },
        dataUrls: {
          main: '',
          xDist: '',
          yDist: ''
163
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
164
165
166
167
168
169
170
171
172
173
      }
    },
    computed: {
      idFilter () {
        return store.getters.filter('ids')
      },
      args () {
        return {
          features: this.featureData,
          categories: this.categoryData,
174
          whiten: this.params.whiten,
Sascha Herzinger's avatar
Sascha Herzinger committed
175
176
177
178
179
180
181
182
          id_filter: this.idFilter,
          subsets: store.getters.subsets
        }
      },
      validArgs () {
        return this.featureData.length > 1
      },
      margin () {
183
        const left = this.width / 20
184
185
        const top = this.height / 20
        const right = this.width / 20
186
        const bottom = this.height / 20
Sascha Herzinger's avatar
Sascha Herzinger committed
187
188
189
190
191
192
193
        return { left, top, right, bottom }
      },
      padded () {
        const width = this.width - this.margin.left - this.margin.right
        const height = this.height - this.margin.top - this.margin.bottom
        return { width, height }
      },
194
      pointSize () {
Sascha Herzinger's avatar
Sascha Herzinger committed
195
        return this.padded.width / 125
196
      },
197
198
199
200
201
202
203
      canvas () {
        return {
          main: getHDPICanvas(this.padded.width, this.padded.height),
          xDist: getHDPICanvas(this.padded.width, this.pointSize),
          yDist: getHDPICanvas(this.pointSize, this.padded.height)
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
204
      scales () {
205
        const x = d3.scaleLinear()
206
          .domain((() => {
207
            const xExtent = d3.extent(this.results.data[this.pcX])
208
209
210
            const xPadding = (xExtent[1] - xExtent[0]) / 10
            return [xExtent[0] - xPadding, xExtent[1] + xPadding]
          })())
211
212
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
213
          .domain((() => {
214
            const yExtent = d3.extent(this.results.data[this.pcY])
215
216
217
            const yPadding = (yExtent[1] - yExtent[0]) / 10
            return [yExtent[0] - yPadding, yExtent[1] + yPadding]
          })())
218
219
          .range([this.padded.height, 0])
        return { x, y }
Sascha Herzinger's avatar
Sascha Herzinger committed
220
      },
221
222
      loadingScales () {
        const x = d3.scaleLinear()
223
          .domain(d3.extent(this.scales.x.domain().concat(this.results.loadings[this.pcX])))
224
225
          .range(this.scales.x.range())
        const y = d3.scaleLinear()
226
          .domain(d3.extent(this.scales.y.domain().concat(this.results.loadings[this.pcY])))
227
228
229
          .range(this.scales.y.range())
        return { x, y }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
230
      points () {
231
        return this.results.data.id.map((d, i) => {
232
          return {
233
234
235
236
237
            x: this.scales.x(this.results.data[this.pcX][i]),
            y: this.scales.y(this.results.data[this.pcY][i]),
            id: d,
            category: this.results.data.category[i],
            subset: this.results.data.subset[i],
238
            shape: getPolygonPointsForSubset(
239
240
241
              {
                cx: this.scales.x(this.results.data[this.pcX][i]),
                cy: this.scales.y(this.results.data[this.pcY][i]),
Sascha Herzinger's avatar
Sascha Herzinger committed
242
                size: this.pointSize,
243
244
245
                subset: this.results.data.subset[i]
              }
            ),
246
247
            tooltip: `
<div>
248
249
250
  <p>ID: ${d}</p>
  <p>Subset: ${this.results.data.subset[i]}</p>
  ${this.results.data.category[i] !== '' ? '<p>Category: ' + this.results.data.category[i] + '</p>' : ''}
251
252
253
254
255
</div>
`
          }
        })
      },
256
      loadings () {
257
        return this.results.loadings.feature.map((d, i) => {
258
          return {
259
260
            x1: this.loadingScales.x(0),
            y1: this.loadingScales.y(0),
261
262
263
            x2: this.loadingScales.x(this.results.loadings[this.pcX][i]),
            y2: this.loadingScales.y(this.results.loadings[this.pcY][i]),
            feature: d
264
265
266
          }
        })
      },
267
268
      pcomponents () {
        return Object.keys(this.results.loadings).filter(d => d !== 'feature').map(d => parseInt(d))
269
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
270
271
272
      subsets () {
        return [...new Set(this.results.data.subset)]
      },
273
      categories () {
274
        return [...new Set(this.results.data.category)]
Sascha Herzinger's avatar
Sascha Herzinger committed
275
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
      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]
        })
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
291
      axis () {
292
293
294
295
296
297
298
299
300
        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)
          .tickFormat('')
        const y2 = d3.axisLeft(this.scales.y)
          .tickSizeInner(this.padded.width)
          .tickFormat('')
        return { x1, x2, y1, y2 }
301
302
303
304
305
306
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
          .on('end', () => {
            if (!d3.event.selection) {
307
308
309
              if (this.selectedPoints.length === 0) {
                return
              }
310
311
312
313
314
315
316
317
318
319
              this.selectedPoints = []
            } else {
              const [[x0, y0], [x1, y1]] = d3.event.selection
              this.selectedPoints = this.points.filter(d => {
                return x0 <= d.x && d.x <= x1 && y0 <= d.y && d.y <= y1
              })
            }
            store.dispatch('setFilter', {filter: 'ids', value: this.selectedPoints.map(d => d.id)})
            this.hasSetFilter = true
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
320
321
322
323
      }
    },
    watch: {
      'args': {
324
325
326
        handler: function (newArgs) {
          if (this.validArgs && !this.hasSetFilter) {
            this.runAnalysisWrapper(newArgs)
Sascha Herzinger's avatar
Sascha Herzinger committed
327
          }
328
          this.hasSetFilter = false
Sascha Herzinger's avatar
Sascha Herzinger committed
329
330
331
332
333
        }
      },
      'axis': {
        handler: function (newAxis) {
          this.$nextTick(() => {
334
335
336
337
            d3.select(this.$refs.yAxis2).call(newAxis.y2)
            d3.select(this.$refs.xAxis2).call(newAxis.x2)
            d3.select(this.$refs.xAxis1).call(newAxis.x1)
            d3.select(this.$refs.yAxis1).call(newAxis.y1)
Sascha Herzinger's avatar
Sascha Herzinger committed
338
339
          })
        }
340
341
342
343
      },
      'brush': {
        handler: function (newBrush) {
          this.$nextTick(() => {
344
            d3.select(this.$refs.brush).call(newBrush)
345
346
          })
        }
347
348
349
350
351
352
      },
      'points': {
        handler: function (newPoints) {
          this.$nextTick(() => this.drawScatterPoints(newPoints))
          this.$nextTick(() => this.drawDistPoints(newPoints))
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
353
354
355
356
      }
    },
    methods: {
      runAnalysisWrapper (args) {
357
        runAnalysis('compute-pca', args)
Sascha Herzinger's avatar
Sascha Herzinger committed
358
359
360
361
362
363
364
          .then(response => {
            const results = JSON.parse(response)
            deepFreeze(results) // massively improve performance by telling Vue that the objects properties won't change
            this.results = results
          })
          .catch(error => console.error(error))
      },
365
      drawScatterPoints (points) {
366
        const canvas = this.canvas.main
367
368
369
370
371
372
373
374
375
376
377
378
        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()
        })
379
        this.dataUrls.main = canvas.toDataURL()
380
381
      },
      drawDistPoints (points) {
382
383
        const xCanvas = this.canvas.xDist
        const yCanvas = this.canvas.yDist
384
385
        const xctx = xCanvas.getContext('2d')
        const yctx = yCanvas.getContext('2d')
386
387
388
389
390
        xctx.clearRect(0, 0, xCanvas.width, xCanvas.height)
        yctx.clearRect(0, 0, yCanvas.width, yCanvas.height)
        xctx.globalAlpha = 0.05
        yctx.globalAlpha = 0.05
        points.forEach(point => {
391
392
          xctx.beginPath()
          yctx.beginPath()
393
          xctx.fillRect(point.x - this.pointSize / 2, 0, this.pointSize, this.pointSize)
394
          yctx.fillRect(0, point.y - this.pointSize / 2, this.pointSize, this.pointSize)
395
        })
396
397
        this.dataUrls.xDist = xCanvas.toDataURL()
        this.dataUrls.yDist = yCanvas.toDataURL()
398
      },
399
      resize (width, height) {
400
        this.width = width
401
        this.height = height
Sascha Herzinger's avatar
Sascha Herzinger committed
402
403
404
405
406
407
408
409
410
411
412
      },
      update_featureData (ids) {
        this.featureData = ids
      },
      update_categoryData (ids) {
        this.categoryData = ids
      }
    },
    components: {
      ControlPanel,
      DataBox,
Sascha Herzinger's avatar
Sascha Herzinger committed
413
      Chart,
Sascha Herzinger's avatar
Sascha Herzinger committed
414
415
416
      Crosshair,
      Html2svg,
      Draggable
Sascha Herzinger's avatar
Sascha Herzinger committed
417
418
419
    },
    directives: {
      tooltip
420
421
422
423
424
425
426
427
    },
    mixins: [
      StateSaver
    ],
    mounted () {
      this.registerDataToSave([
        'featureData', 'categoryData', 'pcX', 'pcY', 'params'
      ])
Sascha Herzinger's avatar
Sascha Herzinger committed
428
429
430
431
432
    }
  }
</script>

<style lang="sass" scoped>
433
  @import '~assets/base.sass'
434

435
436
437
438
  svg
    .fjs-loadings
      stroke: #f00
      stroke-width: 1px
439
440
441
442
443
    .fjs-pc-distribution
      line
        stroke: #000
        stroke-width: 1px
        shape-rendering: crispEdges
444
445
446
  .fjs-control-panel
    select
      margin: 0 0 0.5vh 0
Sascha Herzinger's avatar
Sascha Herzinger committed
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
  .fjs-legend
    display: flex
    flex-direction: column
    resize: both
    overflow: auto
    transform: translateZ(0)
    polygon
      fill: #7b7b7b
    .fjs-legend-category
      display: flex
      div
        flex: 1
      span
        flex: 8
        overflow: hidden
        white-space: nowrap
        text-overflow: ellipsis
464
465
466
467
</style>

<!--CSS for dynamically created components-->
<style lang="sass">
468
  @import '~assets/d3.sass'
469
  .fjs-axis
470
471
472
    shape-rendering: crispEdges
    .tick
      shape-rendering: crispEdges
473
474
475
476
477
478
      line
        stroke: #E2E2E2
      text
        font-size: 0.75em
    path
      stroke: none
Sascha Herzinger's avatar
Sascha Herzinger committed
479
</style>