Boxplot.vue 14.7 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
17
18
19
20
21
22
23
24
25
26
      <hr class="fjs-seperator"/>
      <div class="fjs-parameter-container">
        <input id="fjs-show-data-check" type="checkbox" v-model="params.showData"/>
        <label for="fjs-show-data-check">Show Points</label>
        <br/>
        <input id="fjs-jitter-data-check" type="checkbox" v-model="params.jitter"/>
        <label for="fjs-jitter-data-check">Jitter Data</label>
        <br/>
        <input id="fjs-show-kde-check" type="checkbox" v-model="params.showKDE"/>
        <label for="fjs-show-kde-check">Show Density Est.</label>
      </div>
    </control-panel>
Sascha Herzinger's avatar
Sascha Herzinger committed
27

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

  </chart>
Sascha Herzinger's avatar
Sascha Herzinger committed
114
115
116
117
</template>

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

<style lang="sass" scoped>
393
  @import '~assets/base.sass'
394
395
  svg
    .fjs-box
Sascha Herzinger's avatar
Sascha Herzinger committed
396
      cursor: pointer
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
      .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%
Sascha Herzinger's avatar
Sascha Herzinger committed
415
416
417
418
419
</style>


<!--CSS for dynamically created components-->
<style lang="sass">
420
  .fjs-boxplot-axis
Sascha Herzinger's avatar
Sascha Herzinger committed
421
422
423
    shape-rendering: crispEdges
    .tick
      shape-rendering: crispEdges
Sascha Herzinger's avatar
Sascha Herzinger committed
424
      text
425
        font-size: 0.75em
Sascha Herzinger's avatar
Sascha Herzinger committed
426
    line
427
428
429
      stroke: #E2E2E2
    path
      stroke: none
430
431
432
433
    .fjs-x-axis
      .tick
        text
          text-anchor: start
434
          font-size: 0.75em
Sascha Herzinger's avatar
Sascha Herzinger committed
435
</style>