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

4
    <control-panel class="fjs-control-panel">
Sascha Herzinger's avatar
Sascha Herzinger committed
5
6
7
8
9
10
11
12
13
14
      <data-box class="fjs-data-box"
                header="Numerical Variables"
                dataType="numerical"
                v-on:update="update_numData">
      </data-box>
      <data-box class="fjs-data-box"
                header="Categorical Variables"
                dataType="categorical"
                v-on:update="update_catData">
      </data-box>
15
16
      <hr class="fjs-seperator"/>
      <div class="fjs-parameter-container">
17
        <label>
18
          <input type="checkbox" v-model="params.showData"/>
19
20
          Show Points
        </label>
21
        <br/>
22
        <label>
23
          <input type="checkbox" v-model="params.jitter"/>
24
25
          Jitter Data
        </label>
26
        <br/>
27
        <label>
28
          <input type="checkbox" v-model="params.showKDE"/>
29
30
          Show Density Est.
        </label>
31
32
      </div>
    </control-panel>
Sascha Herzinger's avatar
Sascha Herzinger committed
33

34
    <svg :width="width" :height="height">
35
      <rect x="0" y="0" :height="height" :width="width" style="opacity: 0;" @click="resetFilter"></rect>
36
      <g :transform="`translate(${margin.left}, ${margin.top})`">
37
38
39
        <text :x="this.padded.width / 2"
              class="fjs-anova-results"
              v-if="Object.keys(this.results.anova).length">
40
41
42
          ANOVA -- F-value: {{ this.results.anova.f_value.toFixed(4) }}
          &nbsp p-value: {{ this.results.anova.p_value.toFixed(4) }}
        </text>
43
44
45
46
        <g class="fjs-boxplot-axis fjs-x-axis" :transform="`translate(0, ${padded.height})`"></g>
        <g class="fjs-boxplot-axis fjs-y-axis"></g>
        <g class="fjs-box"
           :transform="`translate(${scales.x(label)}, 0)`"
47
           v-tooltip="{placement: 'bottom'}"
48
49
           :title="label"
           :data-label="label"
50
           @click="setIDFilter(label)"
51
52
53
54
55
           @mouseenter="showTooltip(label)"
           @mouseleave="hideTooltip(label)"
           v-for="label in labels" >
          <line class="fjs-upper-whisker"
                :title="results.statistics[label].u_wsk"
56
                v-tooltip="{placement: 'right'}"
57
                :x1="- boxplotWidth / 6"
58
                :y1="boxes[label].u_wsk"
59
                :x2="boxplotWidth / 6"
60
                :y2="boxes[label].u_wsk">
61
62
63
          </line>
          <line class="fjs-lower-whisker"
                :title="results.statistics[label].l_wsk"
64
                v-tooltip="{placement: 'right'}"
65
                :x1="- boxplotWidth / 6"
66
                :y1="boxes[label].l_wsk"
67
                :x2="boxplotWidth / 6"
68
                :y2="boxes[label].l_wsk">
69
70
71
          </line>
          <line class="fjs-upper-quartile"
                :title="results.statistics[label].u_qrt"
72
                v-tooltip="{placement: 'left'}"
73
                :x1="- boxplotWidth / 2"
74
                :y1="boxes[label].u_qrt"
75
                :x2="boxplotWidth / 2"
76
                :y2="boxes[label].u_qrt">
77
78
79
          </line>
          <line class="fjs-lower-quartile"
                :title="results.statistics[label].l_qrt"
80
                v-tooltip="{placement: 'left'}"
81
                :x1="- boxplotWidth / 2"
82
                :y1="boxes[label].l_qrt"
83
                :x2="boxplotWidth / 2"
84
                :y2="boxes[label].l_qrt">
85
86
87
          </line>
          <line class="fjs-median"
                :title="results.statistics[label].median"
88
                v-tooltip="{placement: 'right'}"
89
                :x1="- boxplotWidth / 2"
90
                :y1="boxes[label].median"
91
                :x2="boxplotWidth / 2"
92
                :y2="boxes[label].median">
93
94
95
          </line>
          <line class="fjs-antenna"
                :x1="0"
96
                :y1="boxes[label].u_wsk"
97
                :x2="0"
98
                :y2="boxes[label].l_wsk">
99
100
101
          </line>
          <rect class="fjs-above-median-box"
                :x="- boxplotWidth / 2"
102
                :y="boxes[label].u_qrt"
103
                :width="boxplotWidth"
104
                :height="boxes[label].median - boxes[label].u_qrt">
105
106
107
          </rect>
          <rect class="fjs-below-median-box"
                :x="- boxplotWidth / 2"
108
                :y="boxes[label].median"
109
                :width="boxplotWidth"
110
                :height="boxes[label].l_qrt - boxes[label].median">
111
          </rect>
112
113
114
115
          <svg-canvas name="fjs-canvas"
                      :z-index="1"
                      :data-label="label"
                      :height="padded.height"
116
                      :width="boxplotWidth / 2"/>
117
118
          <polyline class="fjs-kde"
                    :points="kdePolyPoints[label]"
119
                    v-if="params.showKDE">
120
          </polyline>
Sascha Herzinger's avatar
Sascha Herzinger committed
121
        </g>
122
123
124
125
      </g>
    </svg>

  </chart>
Sascha Herzinger's avatar
Sascha Herzinger committed
126
127
128
129
</template>

<script>
  import DataBox from '../components/DataBox.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
130
131
  import ControlPanel from '../components/ControlPanel.vue'
  import Chart from '../components/Chart.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
132
  import store from '../../store/store'
Sascha Herzinger's avatar
Sascha Herzinger committed
133
  import runAnalysis from '../mixins/run-analysis'
Sascha Herzinger's avatar
Sascha Herzinger committed
134
135
  import * as d3 from 'd3'
  import deepFreeze from 'deep-freeze-strict'
Sascha Herzinger's avatar
Sascha Herzinger committed
136
  import { truncateTextUntil } from '../mixins/utils'
137
  import tooltip from '../directives/tooltip'
138
  import SvgCanvas from '../components/SVGCanvas.vue'
139
  import StateSaver from '../mixins/state-saver'
Sascha Herzinger's avatar
Sascha Herzinger committed
140
141
142
143
  export default {
    name: 'boxplot',
    data () {
      return {
144
145
146
147
148
149
        numData: [],
        catData: [],
        params: {
          showData: false,
          jitter: false,
          showKDE: false
150
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
151
152
        width: 0,
        height: 0,
153
        hasSetFilter: false,
154
155
156
        tooltips: {
          boxes: {}
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
157
158
        results: {
          data: [],
159
160
          statistics: {},
          anova: {}
Sascha Herzinger's avatar
Sascha Herzinger committed
161
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
162
163
164
      }
    },
    computed: {
165
166
167
      idFilter () {
        return store.getters.filter('ids')
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
168
169
      args () {
        return {
170
171
          features: this.numData,
          categories: this.catData,
172
          id_filter: this.idFilter,
Sascha Herzinger's avatar
Sascha Herzinger committed
173
174
175
          subsets: store.getters.subsets
        }
      },
176
177
178
      pointSize () {
        return this.width / 150
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
179
      validArgs () {
180
        return this.numData.length > 0
Sascha Herzinger's avatar
Sascha Herzinger committed
181
182
      },
      margin () {
183
        const left = 10
184
        const top = this.height / 20
185
        const right = this.width / 20
186
        const bottom = this.height / 3
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 }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
194
      labels () {
Sascha Herzinger's avatar
Sascha Herzinger committed
195
        return Object.keys(this.results.statistics).sort()
Sascha Herzinger's avatar
Sascha Herzinger committed
196
      },
197
198
      points () {
        const points = {}
Sascha Herzinger's avatar
Sascha Herzinger committed
199
        this.labels.forEach(label => {
200
          let [feature, category, subset] = label.split('//')
201
202
          subset = parseInt(subset.substring(1)) - 1  // revert subset string formatting
          points[label] = this.results.data
203
204
205
206
            .filter(d => d.subset === subset &&
              d.feature === feature &&
              d.category === category &&
              typeof d.value === 'number')
207
208
209
            .map(d => {
              return {
                id: d.id,
210
                value: d.value,
211
                jitter: Math.max(this.pointSize / 2, (this.params.jitter ? Math.random() * this.boxplotWidth / 2 : this.boxplotWidth / 2) - this.pointSize / 2),
212
                subset: d.subset,
213
                category: d.category
214
215
              }
            })
216
217
218
        })
        return points
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
219
220
221
222
223
224
225
226
227
228
229
230
231
      boxes () {
        const boxes = {}
        this.labels.forEach(label => {
          boxes[label] = {
            u_wsk: this.scales.y(this.results.statistics[label].u_wsk),
            l_wsk: this.scales.y(this.results.statistics[label].l_wsk),
            u_qrt: this.scales.y(this.results.statistics[label].u_qrt),
            l_qrt: this.scales.y(this.results.statistics[label].l_qrt),
            median: this.scales.y(this.results.statistics[label].median)
          }
        })
        return boxes
      },
232
      numOfBoxplots () {
Sascha Herzinger's avatar
Sascha Herzinger committed
233
        return this.labels.length
234
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
235
      boxplotWidth () {
236
        const maxBoxplotWidth = this.padded.width / 4
237
        const minBoxplotWidth = 10
238
239
        let boxplotWidth = this.padded.width / this.numOfBoxplots - this.padded.width * 0.05
        boxplotWidth = boxplotWidth > maxBoxplotWidth ? maxBoxplotWidth : boxplotWidth
240
        boxplotWidth = boxplotWidth < minBoxplotWidth ? minBoxplotWidth : boxplotWidth
241
242
243
        return boxplotWidth
      },
      scales () {
244
        const values = this.results.data.map(d => d.value)
245
246
        const flattened = [].concat.apply([], values)
        const extent = d3.extent(flattened)
Sascha Herzinger's avatar
Sascha Herzinger committed
247
        const padding = (extent[1] - extent[0]) / 20
Sascha Herzinger's avatar
Sascha Herzinger committed
248
        const x = d3.scalePoint()
Sascha Herzinger's avatar
Sascha Herzinger committed
249
          .domain(this.labels)
Sascha Herzinger's avatar
Sascha Herzinger committed
250
          .range([0, this.padded.width])
251
          .padding(0.5)
252
        const y = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
253
          .domain([extent[0] - padding, extent[1] + padding])
Sascha Herzinger's avatar
Sascha Herzinger committed
254
          .range([this.padded.height, 0])
Sascha Herzinger's avatar
Sascha Herzinger committed
255
256
        return { x, y }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
      kdePolyPoints () {
        const polyPoints = {}
        this.labels.forEach(label => {
          polyPoints[label] = this.results.statistics[label].kde.map((d, i) => {
            return -this.kdeScales[label].y(d) + ',' + this.kdeScales[label].x(i)
          }).join(' ')
        })
        return polyPoints
      },
      kdeScales () {
        const polyPoints = {}
        this.labels.forEach(label => {
          const x = d3.scaleLinear()
            .domain([0, this.results.statistics[label].kde.length - 1])
            .range([this.scales.y(this.results.statistics[label].l_wsk),
              this.scales.y(this.results.statistics[label].u_wsk)])
          const y = d3.scaleLinear()
            .domain(d3.extent(this.results.statistics[label].kde))
            .range([0, this.boxplotWidth / 2])
          polyPoints[label] = { x, y }
        })
        return polyPoints
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
280
      axis () {
Sascha Herzinger's avatar
Sascha Herzinger committed
281
282
        const x = d3.axisBottom(this.scales.x).tickFormat(d => {
          // noinspection JSSuspiciousNameCombination
283
          return truncateTextUntil({text: d, font: `0.875em Roboto`, maxWidth: this.margin.bottom})
Sascha Herzinger's avatar
Sascha Herzinger committed
284
        })
285
286
        const y = d3.axisRight(this.scales.y)
          .tickSizeInner(this.padded.width)
287
        return { x, y }
Sascha Herzinger's avatar
Sascha Herzinger committed
288
289
      }
    },
290
291
292
    // IMPORTANT: If the code within the watchers does interact with the DOM the code should be wrapped into a $nextTick
    // statement. This helps with the integration into the Vue component lifecycle. E.g.: an animation can't be
    // applied to an element that does not exist yet.
Sascha Herzinger's avatar
Sascha Herzinger committed
293
294
    watch: {
      'args': {
295
        handler: function () {
296
          if (this.validArgs && !this.hasSetFilter) {
Sascha Herzinger's avatar
Sascha Herzinger committed
297
            this.runAnalysisWrapper(this.args)
Sascha Herzinger's avatar
Sascha Herzinger committed
298
          }
299
          this.hasSetFilter = false
Sascha Herzinger's avatar
Sascha Herzinger committed
300
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
301
302
303
      },
      'axis': {
        handler: function (newAxis) {
304
          this.$nextTick(() => {
Sascha Herzinger's avatar
Sascha Herzinger committed
305
            d3.select(this.$el.querySelector('.fjs-x-axis'))
306
307
308
              .call(newAxis.x)
              .selectAll('text')
              .attr('transform', 'rotate(20)')
Sascha Herzinger's avatar
Sascha Herzinger committed
309
            d3.select(this.$el.querySelector('.fjs-y-axis'))
310
311
312
              .call(newAxis.y)
          })
        }
313
      },
314
      'params.showData': {
315
        handler: function () { this.$nextTick(() => this.drawPoints()) }
316
      },
317
      'params.jitter': {
318
319
320
321
        handler: function () { this.$nextTick(() => this.drawPoints()) }
      },
      'points': {
        handler: function () { this.$nextTick(() => this.drawPoints()) }
Sascha Herzinger's avatar
Sascha Herzinger committed
322
323
324
      }
    },
    methods: {
325
      getTippyInstances (label) {
326
        // prepare mouseover event to populare tippy instances (s. tooltip.js)
327
328
        const event = document.createEvent('Event')
        event.initEvent('mouseover', true, true)
329
        return [
Sascha Herzinger's avatar
Sascha Herzinger committed
330
331
332
333
334
          this.$el.querySelector(`.fjs-box[data-label="${label}"] .fjs-upper-whisker`),
          this.$el.querySelector(`.fjs-box[data-label="${label}"] .fjs-lower-whisker`),
          this.$el.querySelector(`.fjs-box[data-label="${label}"] .fjs-upper-quartile`),
          this.$el.querySelector(`.fjs-box[data-label="${label}"] .fjs-lower-quartile`),
          this.$el.querySelector(`.fjs-box[data-label="${label}"] .fjs-median`)
335
        ].map(el => {
336
337
          el.dispatchEvent(event)
          return el._tippy
338
339
        })
      },
340
      showTooltip (label) {
341
        this.getTippyInstances(label).forEach(d => d.show())
342
      },
343
      hideTooltip (label) {
344
        this.getTippyInstances(label).forEach(d => d.hide())
345
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
346
      update_numData (ids) {
347
        this.numData = ids
Sascha Herzinger's avatar
Sascha Herzinger committed
348
349
      },
      update_catData (ids) {
350
        this.catData = ids
Sascha Herzinger's avatar
Sascha Herzinger committed
351
      },
352
353
354
355
356
357
358
359
      setIDFilter (label) {
        store.dispatch('setFilter', {filter: 'ids', value: this.points[label].map(d => d.id)})
        this.hasSetFilter = true
      },
      resetFilter () {
        store.dispatch('setFilter', {filter: 'ids', value: []})
        this.hasSetFilter = true
      },
360
361
      drawPoints () {
        Object.keys(this.points).forEach(label => {
362
          const canvas = this.$el.querySelector(`.fjs-canvas[data-label="${label}"]`)
363
364
          const ctx = canvas.getContext('2d')
          ctx.clearRect(0, 0, canvas.width, canvas.height)
365
          if (this.params.showData) {
366
367
368
369
370
371
372
373
374
375
376
377
378
            this.points[label].forEach(point => {
              ctx.beginPath()
              ctx.fillStyle = 'black'
              ctx.fillRect(
                point.jitter - this.pointSize / 2,
                this.scales.y(point.value) - this.pointSize / 2,
                this.pointSize,
                this.pointSize
              )
            })
          }
        })
      },
379
380
381
      resize ({height, width}) {
        this.height = height
        this.width = width
Sascha Herzinger's avatar
Sascha Herzinger committed
382
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
383
      runAnalysisWrapper (args) {
Sascha Herzinger's avatar
Sascha Herzinger committed
384
        runAnalysis({task_name: 'compute-boxplot', args})
Sascha Herzinger's avatar
Sascha Herzinger committed
385
386
          .then(response => {
            const results = JSON.parse(response)
387
            results.data = JSON.parse(results.data)
Sascha Herzinger's avatar
Sascha Herzinger committed
388
389
390
391
392
393
394
            deepFreeze(results) // massively improve performance by telling Vue that the objects properties won't change
            this.results = results
          })
          .catch(error => console.error(error))
      }
    },
    components: {
395
      SvgCanvas,
396
      ControlPanel,
Sascha Herzinger's avatar
Sascha Herzinger committed
397
398
      DataBox,
      Chart
Sascha Herzinger's avatar
Sascha Herzinger committed
399
    },
400
401
402
    mixins: [
      StateSaver
    ],
403
404
    directives: {
      tooltip
405
406
407
408
409
    },
    mounted () {
      this.registerDataToSave([
        'catData', 'numData', 'params'
      ])
Sascha Herzinger's avatar
Sascha Herzinger committed
410
411
412
413
414
    }
  }
</script>

<style lang="sass" scoped>
415
  @import '~assets/base.sass'
416
417
  svg
    .fjs-box
Sascha Herzinger's avatar
Sascha Herzinger committed
418
      cursor: pointer
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
      .fjs-median, .fjs-lower-quartile, .fjs-upper-quartile
        opacity: 1
      .fjs-lower-whisker, .fjs-upper-whisker, .fjs-antenna
        shape-rendering: crispEdges
        stroke: black
        stroke-width: 1px
      .fjs-below-median-box
        stroke: none
        fill: rgb(205, 232, 254)
        shape-rendering: crispEdges
      .fjs-above-median-box
        stroke: none
        fill: rgb(180, 221, 253)
        shape-rendering: crispEdges
    .fjs-kde
      fill: none
      stroke: black
      stroke-width: 0.2%
437
438
    .fjs-anova-results
      text-anchor: middle
Sascha Herzinger's avatar
Sascha Herzinger committed
439
440
441
442
443
</style>


<!--CSS for dynamically created components-->
<style lang="sass">
444
  .fjs-boxplot-axis
Sascha Herzinger's avatar
Sascha Herzinger committed
445
446
447
    shape-rendering: crispEdges
    .tick
      shape-rendering: crispEdges
Sascha Herzinger's avatar
Sascha Herzinger committed
448
      text
449
        font-size: 0.75em
Sascha Herzinger's avatar
Sascha Herzinger committed
450
    line
451
452
453
      stroke: #E2E2E2
    path
      stroke: none
454
455
456
457
    .fjs-x-axis
      .tick
        text
          text-anchor: start
458
          font-size: 0.75em
Sascha Herzinger's avatar
Sascha Herzinger committed
459
</style>