Volcanoplot.vue 14.7 KB
Newer Older
Sascha Herzinger's avatar
Sascha Herzinger committed
1
2
3
4
5
6
7
8
9
10
11
<template>
    <chart v-on:resize="resize">
        <control-panel class="fjs-control-panel" name="Volcanoplot Panel">
            <data-box class="fjs-data-box"
                      header="Numerical Array Variables"
                      :data-types="['numerical_array']"
                      v-on:update="update_arrays">
            </data-box>
            <hr class="fjs-seperator"/>
            <fieldset class="fjs-fieldset">
                <legend>Differential Expression Analysis</legend>
12
                <div v-for="method in rankingMethods">
Sascha Herzinger's avatar
Sascha Herzinger committed
13
                    <label>
14
15
                        <input type="radio" :value="method" v-model="rankingMethod">
                        {{ method }}
Sascha Herzinger's avatar
Sascha Herzinger committed
16
17
18
                    </label>
                </div>
            </fieldset>
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
            <div class="fjs-axis-params">
                <label>
                    X-Axis
                    <select v-model="xAxisTransform">
                        <option :value="d" v-for="d in Object.keys(transformations)">{{ d }}</option>
                    </select>
                    <select v-model="xAxisStatistic">
                        <option :value="d" v-for="d in statistics">{{ d }}</option>
                    </select>
                </label>
                <label>
                    Y-Axis
                    <select v-model="yAxisTransform">
                        <option :value="d" v-for="d in Object.keys(transformations)">{{ d }}</option>
                    </select>
                    <select v-model="yAxisStatistic">
                        <option :value="d" v-for="d in statistics">{{ d }}</option>
                    </select>
                </label>
            </div>
39
40
41
42
43
44
            <div v-if="rankingMethod === 'DESeq2'">
                <label>
                    Minimal total reads:
                    <input type="number" v-model.number="params.min_total_row_count"/>
                </label>
            </div>
Sascha Herzinger's avatar
Sascha Herzinger committed
45
46
47
        </control-panel>
        <svg :height="height" :width="width">
            <g :transform="`translate(${margin.left}, ${margin.top})`">
48
49
50
51
52
53
54
55
56
57
58
59
60
                <html2svg :left="selectionTable.left" :top="selectionTable.top">
                    <table class="fjs-selection-table"
                           ref="selectionTable"
                           :style="{'min-width': brushSelection.x1 - brushSelection.x0 + 'px'}"
                           v-show="Object.keys(brushSelection).length > 0">
                        <tr id="table-colnames">
                            <td>Feature</td>
                            <td>{{ xAxisStatistic }}</td>
                            <td>{{ yAxisStatistic }}</td>
                        </tr>
                        <tr @mouseover="highlightedFeature = d.feature"
                            @mouseout="highlightedFeature = ''"
                            v-for="d in selectedFeaturesTable">
61
62
63
64
65
                            <td>{{ d.feature }}</td>
                            <td>{{ d.xStat }}</td>
                            <td>{{ d.yStat }}</td>
                        </tr>
                    </table>
66
                </html2svg>
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
                <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>
                <text class="fjs-axis-label" :transform="`translate(${padded.width / 2}, ${padded.height})`">
                    {{ `${xAxisTransform}(${xAxisStatistic})` }}
                </text>
                <text class="fjs-axis-label" :transform="`translate(${0}, ${padded.height / 2})rotate(-90)`">
                    {{ `${yAxisTransform}(${yAxisStatistic})` }}
                </text>
                <text class="fjs-axis-label" :transform="`translate(${padded.width / 2}, ${0})`">
                    {{ `${xAxisStatistic}` }}
                </text>
                <text class="fjs-axis-label" :transform="`translate(${padded.width}, ${padded.height / 2})rotate(90)`">
                    {{ `${yAxisStatistic}` }}
                </text>
Sascha Herzinger's avatar
Sascha Herzinger committed
83
84
                <crosshair :width="padded.width" :height="padded.height"/>
                <image :xlink:href="dataUrl" :width="padded.width" :height="padded.height"></image>
85
                <g class="fjs-brush" ref="brush"></g>
Sascha Herzinger's avatar
Sascha Herzinger committed
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
            </g>
        </svg>
    </chart>
</template>

<script>
  import Chart from '../components/Chart.vue'
  import ControlPanel from '../components/ControlPanel.vue'
  import DataBox from '../components/DataBox.vue'
  import RunAnalysis from '../mixins/run-analysis'
  import store from '../../store/store'
  import deepFreeze from 'deep-freeze-strict'
  import getHDPICanvas from '../../utils/high-dpi-canvas'
  import * as d3 from 'd3'
  import Crosshair from '../components/Crosshair.vue'
101
  import _ from 'lodash'
102
103
  import Html2svg from '../components/HTML2SVG.vue'
  import Draggable from '../components/Draggable.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
104
105
  export default {
    name: 'volcanoplot',
106
    components: {Draggable, Html2svg, Crosshair, DataBox, ControlPanel, Chart},
Sascha Herzinger's avatar
Sascha Herzinger committed
107
108
109
110
111
112
113
    mixins: [RunAnalysis],
    data () {
      return {
        height: 0,
        width: 0,
        arrays: [],
        rankingMethod: 'limma',
114
115
116
        rankingMethods: ['limma', 'DESeq2'],
        xAxisStatistic: '',
        yAxisStatistic: '',
117
        xAxisTransform: 'identity',
118
119
120
        yAxisTransform: '-log10',
        transformations: {
          'log2': Math.log2,
121
          '-log2': d => -Math.log2(d),
122
123
124
125
          'log10': Math.log10,
          '-log10': d => -Math.log10(d),
          'identity': d => d
        },
126
127
128
129
        brushSelection: {},
        selectionTable: {
          left: 0,
          top: 0
130
        },
131
        highlightedFeature: '',
132
133
134
        params: {
          min_total_row_count: 10
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
135
        results: {
136
          stats: { feature: [] }
Sascha Herzinger's avatar
Sascha Herzinger committed
137
        },
138
139
        dataUrl: '',
        selectedFeatures: []
Sascha Herzinger's avatar
Sascha Herzinger committed
140
141
142
143
144
145
146
      }
    },
    computed: {
      args () {
        return {
          numerical_arrays: this.arrays,
          id_filter: [],
147
          params: this.params,
Sascha Herzinger's avatar
Sascha Herzinger committed
148
149
150
151
152
153
154
155
          ranking_method: this.rankingMethod,
          subsets: store.getters.subsets
        }
      },
      validArgs () {
        return this.args.numerical_arrays.length > 0
      },
      margin () {
156
157
158
159
        const left = this.width / 18
        const top = this.height / 18
        const right = this.width / 18
        const bottom = this.height / 18
Sascha Herzinger's avatar
Sascha Herzinger committed
160
161
162
163
164
165
166
167
168
169
170
171
172
        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}
      },
      pointSize () {
        return this.padded.width / 125
      },
      canvas () {
        return getHDPICanvas(this.padded.width, this.padded.height)
      },
173
      points () {
174
175
176
177
178
179
180
181
182
183
184
185
186
        const xs = []
        const ys = []
        const features = []
        this.results.stats.feature.forEach((feature, i) => {
          const x = this.transformations[this.xAxisTransform](this.results.stats[this.xAxisStatistic][i])
          const y = this.transformations[this.yAxisTransform](this.results.stats[this.yAxisStatistic][i])
          if (isFinite(x) && isFinite(y)) {
            xs.push(x)
            ys.push(y)
            features.push(feature)
          }
        })
        return { xs, ys, features }
Sascha Herzinger's avatar
Sascha Herzinger committed
187
      },
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
      rawScales () {
        const x = d3.scaleLinear()
          .domain((() => {
            const xExtent = d3.extent(this.results.stats[this.xAxisStatistic])
            const xPadding = (xExtent[1] - xExtent[0]) / 10
            return [xExtent[0] - xPadding, xExtent[1] + xPadding]
          })())
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
          .domain((() => {
            const yExtent = d3.extent(this.results.stats[this.yAxisStatistic])
            const yPadding = (yExtent[1] - yExtent[0]) / 10
            return [yExtent[0] - yPadding, yExtent[1] + yPadding]
          })())
          .range([0, this.padded.height])
        return { x, y }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
205
206
      scales () {
        const x = d3.scaleLinear()
207
208
209
210
211
          .domain((() => {
            const xExtent = d3.extent(this.points.xs)
            const xPadding = (xExtent[1] - xExtent[0]) / 10
            return [xExtent[0] - xPadding, xExtent[1] + xPadding]
          })())
Sascha Herzinger's avatar
Sascha Herzinger committed
212
213
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
214
215
216
217
218
          .domain((() => {
            const yExtent = d3.extent(this.points.ys)
            const yPadding = (yExtent[1] - yExtent[0]) / 10
            return [yExtent[0] - yPadding, yExtent[1] + yPadding]
          })())
Sascha Herzinger's avatar
Sascha Herzinger committed
219
220
221
222
          .range([this.padded.height, 0])
        return { x, y }
      },
      scaledPoints () {
223
224
225
226
        return this.points.features.map((feature, i) => {
          const x = this.scales.x(this.points.xs[i])
          const y = this.scales.y(this.points.ys[i])
          return { x, y, feature }
Sascha Herzinger's avatar
Sascha Herzinger committed
227
228
229
        })
      },
      axis () {
230
231
232
233
234
235
        const x1 = d3.axisBottom(this.scales.x)
        const y1 = d3.axisLeft(this.scales.y)
        const x2 = d3.axisTop(this.rawScales.x)
          .tickSizeOuter(-this.padded.height)
        const y2 = d3.axisRight(this.rawScales.y)
          .tickSizeOuter(-this.padded.width)
236
        return {x1, x2, y1, y2}
237
238
239
240
241
242
243
      },
      statistics () {
        if ((this.rankingMethod === 'limma') && (store.getters.subsets.length === 2)) {
          return ['logFC', 'P.Value', 'feature', 'AveExpr', 't', 'adj.P.Val', 'B']
        } else if ((this.rankingMethod === 'limma') && (store.getters.subsets.length > 2)) {
          return ['F', 'P.Value', 'feature', 'AveExpr', 'adj.P.Val']
        } else if (this.rankingMethod === 'DESeq2') {
244
          return ['log2FoldChange', 'pvalue', 'baseMean', 'lfcSE', 'stat', 'padj', 'feature']
245
246
247
        } else {
          throw new Error(`Unknown ranking method: ${this.rankingMethod}`)
        }
248
249
250
251
252
253
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
          .on('brush', () => {
            if (!d3.event.sourceEvent) { return }
254
            if (d3.event.selection) {
255
              const [[x0, y0], [x1, y1]] = d3.event.selection
256
              this.brushSelection = { x0, x1, y0, y1 }
257
258
259
260
261
              this.selectedFeatures = this.scaledPoints.filter(d => {
                return x0 <= d.x && d.x <= x1 && y0 <= d.y && d.y <= y1
              })
            }
          })
262
263
264
265
266
267
268
          .on('end', () => {
            if (!d3.event.selection) {
              this.brushSelection = {}
              this.selectedFeatures = []
              this.highlightedFeature = ''
            }
          })
269
270
271
272
273
274
275
276
277
      },
      selectedFeaturesTable () {
        return this.selectedFeatures.map(d => {
          const i = this.results.stats.feature.findIndex(e => e === d.feature)
          const feature = this.results.stats.feature[i]
          const xStat = this.results.stats[this.xAxisStatistic][i].toPrecision(4)
          const yStat = this.results.stats[this.yAxisStatistic][i].toPrecision(4)
          return { feature, xStat, yStat }
        })
Sascha Herzinger's avatar
Sascha Herzinger committed
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
      }
    },
    methods: {
      resize (width, height) {
        this.width = width
        this.height = height
      },
      update_arrays (ids) {
        this.arrays = ids
      },
      runAnalysisWrapper (args) {
        this.runAnalysis('compute-volcanoplot', args)
          .then(response => {
            const results = JSON.parse(response)
            deepFreeze(results) // massively improve performance by telling Vue that the objects properties won't change
293
            this.results = results
Sascha Herzinger's avatar
Sascha Herzinger committed
294
295
296
297
298
299
300
          })
          .catch(error => console.error(error))
      },
      drawPoints (points) {
        const ctx = this.canvas.getContext('2d')
        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
        points.forEach(d => {
301
          let pointSize = this.pointSize
Sascha Herzinger's avatar
Sascha Herzinger committed
302
          ctx.beginPath()
303
304
305
306
307
308
309
          if (d.feature === this.highlightedFeature) {
            ctx.fillStyle = '#F00'
            pointSize *= 2
          } else {
            ctx.fillStyle = '#000'
          }
          ctx.fillRect(d.x - pointSize / 2, d.y - pointSize / 2, pointSize, pointSize)
Sascha Herzinger's avatar
Sascha Herzinger committed
310
311
312
313
314
315
        })
        this.dataUrl = this.canvas.toDataURL('image/png')
      }
    },
    watch: {
      'args': {
316
        handler: function (newArgs) {
Sascha Herzinger's avatar
Sascha Herzinger committed
317
318
319
320
321
322
323
          if (this.validArgs) {
            this.runAnalysisWrapper(newArgs)
          }
        }
      },
      'scaledPoints': {
        handler: function (newPoints) {
324
          this.drawPoints(newPoints)
Sascha Herzinger's avatar
Sascha Herzinger committed
325
        }
326
      },
327
328
329
330
331
      'highlightedFeature': {
        handler: function () {
          this.drawPoints(this.scaledPoints)
        }
      },
332
333
334
335
336
      'statistics': {
        handler: function (newStats, oldStats) {
          if (!_.isEqual(newStats, oldStats)) {
            this.xAxisStatistic = newStats[0]
            this.yAxisStatistic = newStats[1]
337
            this.results.stats = _.zipObject(newStats, _.times(newStats.length, () => []))
338
339
340
          }
        },
        immediate: true
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
      },
      'axis': {
        handler: function (newAxis) {
          this.$nextTick(() => {
            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)
          })
        }
      },
      'brush': {
        handler: function (newBrush) {
          this.$nextTick(() => {
            d3.select(this.$refs.brush).call(newBrush)
          })
        }
358
359
360
361
362
363
364
365
      },
      'brushSelection': {
        handler: function (newSelection) {
          const tableWidth = this.$refs.selectionTable.getBoundingClientRect().width
          const selectionWidth = newSelection.x1 - newSelection.x0
          this.selectionTable.left = newSelection.x0 - (tableWidth / 2 - selectionWidth / 2)
          this.selectionTable.top = newSelection.y1
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
366
367
368
369
370
371
372
      }
    }
  }
</script>

<style lang="sass" scoped>
    @import '~assets/base.sass'
373
374
375
376
377

    .fjs-control-panel
        .fjs-axis-params
            display: flex
            flex-direction: column
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
    svg
        .fjs-axis-label
            text-anchor: middle
    .fjs-selection-table
        border-collapse: collapse
        background: rgb(216, 217, 216)
        color: #000
        margin: 0
        #table-colnames
            border-top: 1px solid black
            border-bottom: 1px solid black
        td, tr
            border: none
        tr
            &:hover:not(#table-colnames)
                background: aqua
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
</style>

<!--CSS for dynamically created components-->
<style lang="sass">
    @import '~assets/d3.sass'

    .fjs-axis
        shape-rendering: crispEdges
        .tick
            shape-rendering: crispEdges
            line
                stroke: #E2E2E2
            text
                font-size: 0.75em
        path
            stroke: none
Sascha Herzinger's avatar
Sascha Herzinger committed
410
</style>