PCA.vue 14.6 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">
Sascha Herzinger's avatar
Sascha Herzinger committed
5
      <data-box class="fjs-data-box"
6
                header="Numerical Variables"
7
                dataType="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"
Sascha Herzinger's avatar
Sascha Herzinger committed
12
13
14
                dataType="categorical"
                v-on:update="update_categoryData">
      </data-box>
15
      <hr class="fjs-seperator"/>
16
17
18
19
20
21
22
23
24
25
      <div>
        <label for="fjs-pc-x-select">X-Axis</label>
        <select id="fjs-pc-x-select" v-model="pcX">
          <option :value="i" v-for="i in components">Principal Component {{i}}</option>
        </select><br/>
        <label for="fjs-pc-y-select">Y-Axis</label>
        <select id="fjs-pc-y-select" v-model="pcY">
          <option :value="i" v-for="i in components">Principal Component {{i}}</option>
        </select>
      </div>
26
27
      <div>
        <input id="fjs-whiten-check" type="checkbox" v-model="params.whiten"/>
28
        <label for="fjs-whiten-check">Whiten Output</label>
29
      </div>
Sascha Herzinger's avatar
Sascha Herzinger committed
30
31
    </control-panel>

32
    <svg :width="width" :height="height">
33
      <g :transform="`translate(${margin.left}, ${margin.top})`">
34
        <svg-canvas name="fjs-canvas" :width="padded.width" :height="padded.height"/>
Sascha Herzinger's avatar
Sascha Herzinger committed
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
        <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>
51
        <crosshair :width="padded.width" :height="padded.height"/>
52
        <g class="fjs-brush"></g>
53
54
55
56
        <g class="fjs-axis fjs-y-axis-2" :transform="`translate(${padded.width}, 0)`"></g>
        <g class="fjs-axis fjs-x-axis-2"></g>
        <g class="fjs-axis fjs-x-axis-1" :transform="`translate(0, ${padded.height})`"></g>
        <g class="fjs-axis fjs-y-axis-1"></g>
57
        <text :x="padded.width / 2"
58
              :y="- margin.top / 2"
Sascha Herzinger's avatar
Sascha Herzinger committed
59
              text-anchor="middle"
60
61
              v-show="results.data.id.length">
          Principal Component {{pcX}} (Variance Ratio: {{ results.variance_ratios[pcX].toFixed(2) }})
62
63
        </text>
        <text text-anchor="middle"
Sascha Herzinger's avatar
Sascha Herzinger committed
64
              :transform="`translate(${this.padded.width + this.margin.right / 2}, ${this.padded.height / 2})rotate(90)`"
65
66
              v-show="results.data.id.length">
          Principal Component {{pcY}} (Variance Ratio: {{ results.variance_ratios[pcY].toFixed(2) }})
67
        </text>
68
        <g v-for="loading in loadings">
69
70
71
72
73
74
75
76
77
78
79
80
          <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>
81
82
83
          <g class="fjs-pc-distribution fjs-pc-x-distribution"
             :transform="`translate(0, ${padded.height + margin.bottom / 2})`">
            <line :x2="padded.width"></line>
84
85
86
87
88
            <svg-canvas name="fjs-pc-x-distribution-canvas"
                        :y="-pointSize / 2"
                        :width="padded.width"
                        :height="pointSize">
            </svg-canvas>
89
          </g>
90
91
92
          <g class="fjs-pc-distribution fjs-pc-y-distribution"
             :transform="`translate(${- margin.left / 2}, 0)`">
            <line :y2="padded.height"></line>
93
94
95
96
97
            <svg-canvas name="fjs-pc-y-distribution-canvas"
                        :x="-pointSize / 2"
                        :width="pointSize"
                        :height="padded.height">
            </svg-canvas>
98
          </g>
99
        </g>
100
101
102
      </g>
    </svg>
  </chart>
Sascha Herzinger's avatar
Sascha Herzinger committed
103
104
105
106
107
108
</template>

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

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

405
406
407
408
  svg
    .fjs-loadings
      stroke: #f00
      stroke-width: 1px
409
410
411
412
413
    .fjs-pc-distribution
      line
        stroke: #000
        stroke-width: 1px
        shape-rendering: crispEdges
414
415
416
  .fjs-control-panel
    select
      margin: 0 0 0.5vh 0
Sascha Herzinger's avatar
Sascha Herzinger committed
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
  .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
434
435
436
437
</style>

<!--CSS for dynamically created components-->
<style lang="sass">
438
  .fjs-axis
439
440
441
    shape-rendering: crispEdges
    .tick
      shape-rendering: crispEdges
442
443
444
445
446
447
      line
        stroke: #E2E2E2
      text
        font-size: 0.75em
    path
      stroke: none
Sascha Herzinger's avatar
Sascha Herzinger committed
448
</style>