Heatmap.vue 17.1 KB
Newer Older
Sascha Herzinger's avatar
Sascha Herzinger committed
1
<template>
2
  <chart v-on:resize="resize">
3
    <control-panel class="fjs-control-panel" name="Heatmap Panel">
Sascha Herzinger's avatar
Sascha Herzinger committed
4
      <data-box class="fjs-data-box"
5
                header="Numerical Variables"
6
                :dataTypes="['numerical_array']"
Sascha Herzinger's avatar
Sascha Herzinger committed
7
8
                v-on:update="update_numericArrayData">
      </data-box>
9
      <hr class="fjs-seperator"/>
Sascha Herzinger's avatar
Sascha Herzinger committed
10

11
12
      <div class="fjs-ranking-params">
        <span class="fjs-param-header">Ranking Criteria</span>
13
        <fieldset class="fjs-expression-ranking fjs-fieldset">
14
          <legend>Expression Level</legend>
15
          <!--FIXME: Make this dynamic similar to volcanoplot-->
16
          <div>
17
            <label>
18
              <input type="radio" value="mean" v-model="rankingMethod">
19
20
              Mean
            </label>
21
22
          </div>
          <div>
23
            <label>
24
              <input type="radio" value="median" v-model="rankingMethod">
25
26
              Median
            </label>
27
28
          </div>
        </fieldset>
29
        <fieldset class="fjs-fieldset">
30
31
          <legend>Expression Variability</legend>
          <div>
32
            <label>
33
              <input type="radio" value="variance" v-model="rankingMethod">
34
35
              Variance
            </label>
36
37
          </div>
        </fieldset>
38
        <fieldset class="fjs-fieldset">
39
40
          <legend>Differential Expression</legend>
          <div>
41
            <label>
42
              <input type="radio" value="logFC" v-model="rankingMethod">
43
44
              logFC
            </label>
45
46
          </div>
          <div>
47
            <label>
48
              <input type="radio" value="t" v-model="rankingMethod">
49
50
              t
            </label>
51
52
          </div>
          <div>
53
            <label>
54
              <input type="radio" value="F" v-model="rankingMethod">
55
56
              F
            </label>
57
58
          </div>
          <div>
59
            <label>
60
              <input type="radio" value="B" v-model="rankingMethod">
61
62
              B
            </label>
63
64
          </div>
          <div>
65
            <label>
66
              <input type="radio" value="P.Val" v-model="rankingMethod">
67
68
              P.Value
            </label>
69
70
          </div>
          <div>
71
            <label>
72
              <input type="radio" value="adj.P.Val" v-model="rankingMethod">
73
74
              adj.P.Value
            </label>
75
76
77
78
79
80
          </div>
        </fieldset>
      </div>

      <div class="fjs-clustering-params">
        <span class="fjs-param-header">Heatmap Clustering</span>
81
        <fieldset class="fjs-fieldset">
82
83
          <legend>Algorithm</legend>
          <div>
84
85
86
87
            <label>
              <input type="radio" value="hclust" v-model="cluster.algorithm"/>
              Hierarch.
            </label>
88
89
          </div>
          <div>
90
91
92
93
            <label>
              <input type="radio" value="kmeans" v-model="cluster.algorithm"/>
              KMeans
            </label>
94
95
96
          </div>
        </fieldset>

97
        <fieldset class="fjs-cluster-option-fieldset fjs-fieldset" v-if="cluster.algorithm === 'hclust'">
98
99
          <legend>Options</legend>
          <div class="fjs-hclust-selects">
Sascha Herzinger's avatar
Sascha Herzinger committed
100
            <select v-model="cluster.options.method">
101
102
103
              <option value="" selected disabled>-- Method --</option>
              <option :value="value"
                      v-for="value in ['single', 'complete', 'average', 'weighted', 'centroid', 'median', 'ward']"
Sascha Herzinger's avatar
Sascha Herzinger committed
104
                      v-model="cluster.options.method">
105
106
107
                {{ value }}
              </option>
            </select>
Sascha Herzinger's avatar
Sascha Herzinger committed
108
            <select v-model="cluster.options.metric">
109
110
111
              <option value="" selected disabled>-- Metric --</option>
              <option :value="value"
                      v-for="value in ['euclidean', 'sqeuclidean', 'cityblock', 'correlation', 'cosine']"
Sascha Herzinger's avatar
Sascha Herzinger committed
112
                      v-model="cluster.options.metric">
113
114
115
116
117
                {{ value }}
              </option>
            </select>
          </div>
          <div class="fjs-cluster-ranges">
118
119
120
121
122
123
            <label>
              <input type="range"
                     min="1" max="20"
                     v-model="cluster.options.n_row_clusters"/>
              {{ cluster.options.n_row_clusters }} Row Clusters
            </label>
124
125
          </div>
          <div class="fjs-cluster-ranges">
126
127
128
129
130
131
            <label>
              <input type="range"
                     min="1" max="20"
                     v-model="cluster.options.n_col_clusters"/>
              {{ cluster.options.n_col_clusters }} Col Clusters
            </label>
132
133
134
          </div>
        </fieldset>

135
        <fieldset class="fjs-cluster-option-fieldset fjs-fieldset" v-if="cluster.algorithm === 'kmeans'">
136
137
          <legend>Options</legend>
          <div class="fjs-cluster-ranges">
138
139
            <label>
            <input type="range"
140
                   min="1" max="20"
Sascha Herzinger's avatar
Sascha Herzinger committed
141
                   v-model="cluster.options.n_row_centroids"/>
142
143
              {{ cluster.options.n_row_centroids }} Row Centroids
            </label>
144
145
          </div>
          <div class="fjs-cluster-ranges">
146
147
            <label>
            <input type="range"
148
                   min="1" max="20"
Sascha Herzinger's avatar
Sascha Herzinger committed
149
                   v-model="cluster.options.n_col_centroids"/>
150
151
              {{ cluster.options.n_col_centroids }} Col Centroids
            </label>
152
153
154
          </div>
        </fieldset>
      </div>
155
    </control-panel>
Sascha Herzinger's avatar
Sascha Herzinger committed
156

157
    <svg :height="height" :width="width">
158
      <g :transform="`translate(${margin.left}, ${margin.top})`">
159
        <image :xlink:href="dataUrl" :width="padded.width" :height="padded.height"></image>
160
161
162
163
164
165
166
        <rect class="fjs-sig-bar"
              :x="bar.x"
              :y="bar.y"
              :height="bar.height"
              :width="bar.width"
              :fill="bar.fill"
              :title="bar.tooltip"
167
              v-for="bar in sigBars"
168
169
170
171
172
              v-tooltip>
        </rect>
      </g>
    </svg>
  </chart>
Sascha Herzinger's avatar
Sascha Herzinger committed
173
174
175
176
</template>

<script>
  import DataBox from '../components/DataBox.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
177
178
  import ControlPanel from '../components/ControlPanel.vue'
  import Chart from '../components/Chart.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
179
  import store from '../../store/store'
180
  import RunAnalysis from '../mixins/run-analysis'
Sascha Herzinger's avatar
Sascha Herzinger committed
181
182
183
  import * as d3 from 'd3'
  import tooltip from '../directives/tooltip.js'
  import deepFreeze from 'deep-freeze-strict'
184
185
  import getHDPICanvas from '../../utils/high-dpi-canvas'
  import StateSaver from '../mixins/state-saver'
Sascha Herzinger's avatar
Sascha Herzinger committed
186
187
188
189
  export default {
    name: 'heatmap',
    data () {
      return {
Sascha Herzinger's avatar
Sascha Herzinger committed
190
191
        width: 0,
        height: 0,
Sascha Herzinger's avatar
Sascha Herzinger committed
192
        colorScale: d3.interpolateCool,
Sascha Herzinger's avatar
Sascha Herzinger committed
193
        subsetColors: d3.schemeCategory10,
Sascha Herzinger's avatar
Sascha Herzinger committed
194
        numericArrayDataIds: [],
Sascha Herzinger's avatar
Sascha Herzinger committed
195
196
197
198
199
200
201
202
203
204
205
        rankingMethod: 'mean',
        cluster: {
          algorithm: 'hclust',
          options: {
            method: '',
            metric: '',
            n_row_clusters: 5,
            n_col_clusters: 5,
            n_row_centroids: 5,
            n_col_centroids: 5
          },
206
207
          colColors: d3.schemeCategory10,
          rowColors: d3.schemeCategory10.slice().reverse(),
Sascha Herzinger's avatar
Sascha Herzinger committed
208
209
210
          results: {
            rows: [],
            cols: []
211
212
          }
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
213
        results: {
214
215
          data: {id: [], feature: [], value: [], zscore: []},
          stats: {feature: []}
216
217
        },
        dataUrl: ''
Sascha Herzinger's avatar
Sascha Herzinger committed
218
219
220
      }
    },
    computed: {
Sascha Herzinger's avatar
Sascha Herzinger committed
221
      mainArgs () {
Sascha Herzinger's avatar
Sascha Herzinger committed
222
223
224
        return {
          numerical_arrays: this.numericArrayDataIds,
          numericals: [],
Sascha Herzinger's avatar
Sascha Herzinger committed
225
          categoricals: [],
Sascha Herzinger's avatar
Sascha Herzinger committed
226
          ranking_method: this.rankingMethod,
Sascha Herzinger's avatar
Sascha Herzinger committed
227
          id_filter: this.idFilter.value,
228
          max_rows: 100,
Sascha Herzinger's avatar
Sascha Herzinger committed
229
          subsets: store.getters.subsets
Sascha Herzinger's avatar
Sascha Herzinger committed
230
231
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
232
233
      clusterArgs () {
        const df = {}
234
235
236
        this.results.data.id.forEach((d, i) => {
          if (typeof df[d] === 'undefined') {
            df[d] = {}
Sascha Herzinger's avatar
Sascha Herzinger committed
237
          }
238
          df[d][this.results.data.feature[i]] = this.results.data.zscore[i]
Sascha Herzinger's avatar
Sascha Herzinger committed
239
240
241
242
243
244
245
        })
        return {
          df: df,
          cluster_algo: this.cluster.algorithm,
          options: {
            method: this.cluster.options.method,
            metric: this.cluster.options.metric,
246
247
248
249
            n_row_clusters: parseInt(this.cluster.options.n_row_clusters),
            n_col_clusters: parseInt(this.cluster.options.n_col_clusters),
            n_row_centroids: parseInt(this.cluster.options.n_row_centroids),
            n_col_centroids: parseInt(this.cluster.options.n_col_centroids)
Sascha Herzinger's avatar
Sascha Herzinger committed
250
251
          }
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
252
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
253
254
255
      idFilter () {
        return store.getters.filter('ids')
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
256
      margin () {
257
258
259
260
        const left = this.width / 15
        const top = 10
        const right = 10
        const bottom = 10
Sascha Herzinger's avatar
Sascha Herzinger committed
261
262
263
        return { left, top, right, bottom }
      },
      padded () {
264
265
266
267
        let width = this.width - this.margin.left - this.margin.right
        let height = this.height - this.margin.top - this.margin.bottom
        width = width < 0 ? 0 : width
        height = height < 0 ? 0 : height
Sascha Herzinger's avatar
Sascha Herzinger committed
268
269
        return { width, height }
      },
270
271
272
      canvas () {
        return getHDPICanvas(this.padded.width, this.padded.height)
      },
273
274
275
276
277
      cols () {
        let cols = []
        if (this.cluster.results.cols.length) {
          cols = this.cluster.results.cols.map(d => d[0])
        } else {
278
          cols = [...new Set(this.results.data.id)]
279
280
281
282
283
        }
        cols = cols.concat(['$padding_col$', '$cluster_col$'])
        return cols
      },
      rows () {
Sascha Herzinger's avatar
Sascha Herzinger committed
284
        let rows = ['$subset_row$', '$cluster_row$', '$padding_row$']
285
286
287
        if (this.cluster.results.rows.length) {
          rows = rows.concat(this.cluster.results.rows.map(d => d[0]))
        } else {
288
          rows = rows.concat([...new Set(this.results.data.feature)])
289
290
291
292
        }
        return rows
      },
      grid () {
293
294
295
        const maxWidth = this.padded.width / this.cols.length
        let maxHeight = this.padded.height / this.rows.length
        const gridSize = maxWidth < maxHeight ? maxWidth : maxHeight
296
        return {
297
298
          main: { height: gridSize, width: gridSize },
          rowCluster: { height: gridSize, width: gridSize },
Sascha Herzinger's avatar
Sascha Herzinger committed
299
          colCluster: { height: gridSize, width: gridSize },
300
          padding: { height: gridSize, width: gridSize }
301
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
302
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
303
304
      scales () {
        const x = d3.scaleOrdinal()
305
306
307
308
309
310
311
          .domain(this.cols)
          .range((() => {
            let range = []
            for (let i = 0; i < this.cols.length - 2; i++) {
              range.push(i * this.grid.main.width)
            }
            range = range.concat([
312
              (this.cols.length - 2) * this.grid.main.width, // '$padding_col$'
313
314
315
316
              (this.cols.length - 2) * this.grid.main.width + this.grid.padding.width // '$cluster_col$'
            ])
            return range
          })())
Sascha Herzinger's avatar
Sascha Herzinger committed
317
        const y = d3.scaleOrdinal()
318
319
320
          .domain(this.rows)
          .range((() => {
            let range = [
321
322
              0, // '$cluster_row$'
              this.grid.colCluster.height // '$padding_row$'
323
324
325
326
327
328
            ]
            for (let i = 2; i < this.rows.length; i++) {
              range.push(this.grid.colCluster.height + this.grid.padding.height + (i - 2) * this.grid.main.height)
            }
            return range
          })())
Sascha Herzinger's avatar
Sascha Herzinger committed
329
        return { x, y }
Sascha Herzinger's avatar
Sascha Herzinger committed
330
      },
331
      currentStats () {
332
        return this.results.stats[this.rankingMethod]
333
334
      },
      sigScales () {
335
        const x = d3.scaleLinear()
336
337
338
339
340
          .domain(d3.extent(this.currentStats))
          .range([0, this.margin.left])
        const y = this.scales.y // has the same y scale than the heatmap grid
        return { x, y }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
341
      cells () {
342
        const cells = []
343
        this.results.data.id.forEach((d, i) => {
344
          cells.push({
345
346
            x: this.scales.x(d),
            y: this.scales.y(this.results.data.feature[i]),
347
348
            width: this.grid.main.width,
            height: this.grid.main.height,
349
            fill: this.colorScale(1 / (1 + Math.pow(Math.E, -this.results.data.zscore[i]))),
Sascha Herzinger's avatar
Sascha Herzinger committed
350
351
            tooltip: `
<div>
352
353
354
355
  <p>Column: ${d}</p>
  <p>Row: ${this.results.data.feature[i]}</p>
  <p>Value: ${this.results.data.value[i]}</p>
  <p>z-Score ${this.results.data.zscore[i]}</p>
Sascha Herzinger's avatar
Sascha Herzinger committed
356
</div>
Sascha Herzinger's avatar
Sascha Herzinger committed
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
`
          })
        })
        const idSubsetMap = this.results.data.id.map((d, i) => { return {id: d, subset: this.results.data.subset[i]} })
          .reduce((acc, curr) => {
            if (typeof acc[curr.id] === 'undefined') {
              acc[curr.id] = curr.subset
            }
            return acc
          }, {})
        Object.keys(idSubsetMap).forEach(d => {
          cells.push({
            x: this.scales.x(d),
            y: this.scales.y('$subset_row$'),
            width: this.grid.main.width,
            height: this.grid.main.height,
            fill: this.subsetColors[idSubsetMap[d] % this.subsetColors.length],
            tooltip: `
<div>
  <p>Col: ${d}</p>
  <p>Subset: ${idSubsetMap[d]}</p>
</div>
Sascha Herzinger's avatar
Sascha Herzinger committed
379
`
380
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
381
        })
382
383
384
385
386
387
388
        this.cluster.results.rows.forEach(d => {
          cells.push({
            x: this.scales.x('$cluster_col$'),
            y: this.scales.y(d[0]),
            width: this.grid.rowCluster.width,
            height: this.grid.rowCluster.height,
            fill: this.cluster.rowColors[d[1] % this.cluster.rowColors.length],
Sascha Herzinger's avatar
Sascha Herzinger committed
389
390
            tooltip: `
<div>
391
  <p>Row: ${d[0]}</p>
Sascha Herzinger's avatar
Sascha Herzinger committed
392
393
394
  <p>Cluster: ${d[1]}</p>
</div>
`
395
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
396
        })
397
398
399
400
401
402
403
        this.cluster.results.cols.forEach(d => {
          cells.push({
            x: this.scales.x(d[0]),
            y: this.scales.y('$cluster_row$'),
            width: this.grid.colCluster.width,
            height: this.grid.colCluster.height,
            fill: this.cluster.colColors[d[1] % this.cluster.colColors.length],
Sascha Herzinger's avatar
Sascha Herzinger committed
404
405
            tooltip: `
<div>
406
  <p>Column: ${d[0]}</p>
Sascha Herzinger's avatar
Sascha Herzinger committed
407
408
  <p>Cluster: ${d[1]}</p>
</div>
Sascha Herzinger's avatar
Sascha Herzinger committed
409
`
410
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
411
        })
412
        return cells
Sascha Herzinger's avatar
Sascha Herzinger committed
413
      },
414
      sigBars () {
415
        return this.results.stats.feature.map((d, i) => {
416
          return {
417
418
419
            x: -this.sigScales.x(this.results.stats[this.rankingMethod][i]),
            y: this.sigScales.y(d),
            width: this.sigScales.x(this.results.stats[this.rankingMethod][i]),
420
            height: this.grid.main.height,
421
422
            fill: this.results.stats[this.rankingMethod][i] < 0 ? '#0072ff' : '#ff006a',
            tooltip: '<div>' + Object.keys(this.results.stats).map(key => {
Sascha Herzinger's avatar
Sascha Herzinger committed
423
              const selected = key === this.rankingMethod ? '<span style="font-weight: bold;">[selected]<span> ' : ''
424
              return `<p>${selected}${key}: ${this.results.stats[key][i]}</p>`
Sascha Herzinger's avatar
Sascha Herzinger committed
425
            }).join('') + '</div>'
426
427
          }
        })
Sascha Herzinger's avatar
Sascha Herzinger committed
428
429
430
      }
    },
    methods: {
Sascha Herzinger's avatar
Sascha Herzinger committed
431
      computeHeatmap (args) {
432
        this.runAnalysis('compute-heatmap', args)
Sascha Herzinger's avatar
Sascha Herzinger committed
433
434
435
436
          .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
Sascha Herzinger's avatar
Sascha Herzinger committed
437
438
439
          })
      },
      computeCluster (args) {
440
        this.runAnalysis('compute-cluster', args)
Sascha Herzinger's avatar
Sascha Herzinger committed
441
442
          .then(response => {
            const results = JSON.parse(response)
443
            deepFreeze(results)
Sascha Herzinger's avatar
Sascha Herzinger committed
444
445
            this.cluster.results.rows = results['row_clusters']
            this.cluster.results.cols = results['col_clusters']
Sascha Herzinger's avatar
Sascha Herzinger committed
446
447
          })
      },
448
      resize (width, height) {
449
        this.width = width
450
        this.height = height
Sascha Herzinger's avatar
Sascha Herzinger committed
451
452
453
      },
      update_numericArrayData (ids) {
        this.numericArrayDataIds = ids
454
455
      },
      drawCells (cells) {
456
457
        const ctx = this.canvas.getContext('2d')
        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
458
459
460
461
462
        cells.forEach(d => {
          ctx.beginPath()
          ctx.fillStyle = d.fill
          ctx.fillRect(d.x, d.y, d.width, d.height)
        })
463
        this.dataUrl = this.canvas.toDataURL()
Sascha Herzinger's avatar
Sascha Herzinger committed
464
465
466
      }
    },
    watch: {
Sascha Herzinger's avatar
Sascha Herzinger committed
467
468
469
470
471
472
473
474
475
476
477
478
      'mainArgs': {
        handler: function (args) {
          if (args.numerical_arrays.length > 0) {
            this.computeHeatmap(args)
          }
        }
      },
      'clusterArgs': {
        handler: function (args) {
          if ((args.cluster_algo === 'hclust' && args.options.method && args.options.metric) ||
            args.cluster_algo === 'kmeans') {
            this.computeCluster(args)
Sascha Herzinger's avatar
Sascha Herzinger committed
479
480
          }
        }
481
482
483
484
485
      },
      'cells': {
        handler: function (newCells) {
          this.$nextTick(() => this.drawCells(newCells))
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
486
487
488
      }
    },
    components: {
489
      ControlPanel,
Sascha Herzinger's avatar
Sascha Herzinger committed
490
491
      DataBox,
      Chart
Sascha Herzinger's avatar
Sascha Herzinger committed
492
493
494
    },
    directives: {
      tooltip
495
496
    },
    mixins: [
497
498
      StateSaver,
      RunAnalysis
499
500
501
502
503
    ],
    mounted () {
      this.registerDataToSave([
        'numericArrayDataIds', 'rankingMethod', 'cluster'
      ])
Sascha Herzinger's avatar
Sascha Herzinger committed
504
505
506
507
508
    }
  }
</script>

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

511
512
513
514
515
516
517
518
519
520
521
522
523
524
  .fjs-control-panel
    .fjs-param-header
      text-align: center
      margin: 10px 0 5px 0
    .fjs-ranking-params
      display: flex
      flex-direction: column
      flex-shrink: 0
    .fjs-clustering-params
      display: flex
      flex-direction: column
      flex-shrink: 0
      .fjs-cluster-option-fieldset
        div
525
          margin-top: 3px
526
        .fjs-hclust-selects
527
          display: flex
528
529
530
531
532
533
534
535
536
537
538
539
540
          flex-direction: row
          justify-content: space-between
          select
            width: 49%
        .fjs-cluster-ranges
          text-align: center
    svg
      .fjs-cell
        stroke: none
        shape-rendering: crispEdges
      .fjs-cell:hover
        opacity: 0.4
      .fjs-sig-bar
Sascha Herzinger's avatar
Sascha Herzinger committed
541
        stroke-width: 1px
542
        shape-rendering: crispEdges
543
</style>