Heatmap.vue 17 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
15
          <legend>Expression Level</legend>
          <div>
16
            <label>
17
              <input type="radio" value="mean" v-model="rankingMethod">
18
19
              Mean
            </label>
20
21
          </div>
          <div>
22
            <label>
23
              <input type="radio" value="median" v-model="rankingMethod">
24
25
              Median
            </label>
26
27
          </div>
        </fieldset>
28
        <fieldset class="fjs-fieldset">
29
30
          <legend>Expression Variability</legend>
          <div>
31
            <label>
32
              <input type="radio" value="variance" v-model="rankingMethod">
33
34
              Variance
            </label>
35
36
          </div>
        </fieldset>
37
        <fieldset class="fjs-fieldset">
38
39
          <legend>Differential Expression</legend>
          <div>
40
            <label>
41
              <input type="radio" value="logFC" v-model="rankingMethod">
42
43
              logFC
            </label>
44
45
          </div>
          <div>
46
            <label>
47
              <input type="radio" value="t" v-model="rankingMethod">
48
49
              t
            </label>
50
51
          </div>
          <div>
52
            <label>
53
              <input type="radio" value="F" v-model="rankingMethod">
54
55
              F
            </label>
56
57
          </div>
          <div>
58
            <label>
59
              <input type="radio" value="B" v-model="rankingMethod">
60
61
              B
            </label>
62
63
          </div>
          <div>
64
            <label>
65
              <input type="radio" value="P.Val" v-model="rankingMethod">
66
67
              P.Value
            </label>
68
69
          </div>
          <div>
70
            <label>
71
              <input type="radio" value="adj.P.Val" v-model="rankingMethod">
72
73
              adj.P.Value
            </label>
74
75
76
77
78
79
          </div>
        </fieldset>
      </div>

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

96
        <fieldset class="fjs-cluster-option-fieldset fjs-fieldset" v-if="cluster.algorithm === 'hclust'">
97
98
          <legend>Options</legend>
          <div class="fjs-hclust-selects">
Sascha Herzinger's avatar
Sascha Herzinger committed
99
            <select v-model="cluster.options.method">
100
101
102
              <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
103
                      v-model="cluster.options.method">
104
105
106
                {{ value }}
              </option>
            </select>
Sascha Herzinger's avatar
Sascha Herzinger committed
107
            <select v-model="cluster.options.metric">
108
109
110
              <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
111
                      v-model="cluster.options.metric">
112
113
114
115
116
                {{ value }}
              </option>
            </select>
          </div>
          <div class="fjs-cluster-ranges">
117
118
119
120
121
122
            <label>
              <input type="range"
                     min="1" max="20"
                     v-model="cluster.options.n_row_clusters"/>
              {{ cluster.options.n_row_clusters }} Row Clusters
            </label>
123
124
          </div>
          <div class="fjs-cluster-ranges">
125
126
127
128
129
130
            <label>
              <input type="range"
                     min="1" max="20"
                     v-model="cluster.options.n_col_clusters"/>
              {{ cluster.options.n_col_clusters }} Col Clusters
            </label>
131
132
133
          </div>
        </fieldset>

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

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

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

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

510
511
512
513
514
515
516
517
518
519
520
521
522
523
  .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
524
          margin-top: 3px
525
        .fjs-hclust-selects
526
          display: flex
527
528
529
530
531
532
533
534
535
536
537
538
539
          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
540
        stroke-width: 1px
541
        shape-rendering: crispEdges
542
</style>