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

<style lang="sass" scoped>
408
  @import '~assets/base.sass'
409
410
  svg
    .fjs-box
Sascha Herzinger's avatar
Sascha Herzinger committed
411
      cursor: pointer
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
      .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%
430
431
    .fjs-anova-results
      text-anchor: middle
Sascha Herzinger's avatar
Sascha Herzinger committed
432
433
434
435
436
</style>


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