Boxplot.vue 14.8 KB
Newer Older
Sascha Herzinger's avatar
Sascha Herzinger committed
1
<template>
2
  <div class="fjs-boxplot" @click="$emit('focus')">
3

4
    <control-panel class="fjs-control-panel" focus="focus">
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

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

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

<style lang="sass" scoped>
  @import './src/assets/base.sass'

  .fjs-boxplot
    height: 100%
    width: 100%
    display: flex
    flex-direction: column
392
    .fjs-control-panel
Sascha Herzinger's avatar
Sascha Herzinger committed
393
    .fjs-chart
Sascha Herzinger's avatar
Sascha Herzinger committed
394
395
396
397
      flex: 1
      display: flex
      svg
        flex: 1
398
399
400
401
402
403
        .fjs-box
          .fjs-median, .fjs-lower-quartile, .fjs-upper-quartile
            opacity: 1
          .fjs-lower-whisker, .fjs-upper-whisker, .fjs-antenna
            shape-rendering: crispEdges
            stroke: black
404
            stroke-width: 1px
405
406
407
408
409
410
411
412
          .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
413
        .fjs-points
Sascha Herzinger's avatar
Sascha Herzinger committed
414
415
416
417
          stroke: white
          stroke-width: 1px
        .fjs-points:hover
          opacity: 0.4
Sascha Herzinger's avatar
Sascha Herzinger committed
418
419
420
        .fjs-kde
          fill: none
          stroke: black
421
          stroke-width: 0.2%
Sascha Herzinger's avatar
Sascha Herzinger committed
422
423
424
425
426
</style>


<!--CSS for dynamically created components-->
<style lang="sass">
427
  .fjs-boxplot-axis
Sascha Herzinger's avatar
Sascha Herzinger committed
428
429
430
    shape-rendering: crispEdges
    .tick
      shape-rendering: crispEdges
Sascha Herzinger's avatar
Sascha Herzinger committed
431
      text
432
        font-size: 1rem
Sascha Herzinger's avatar
Sascha Herzinger committed
433
434
    line
      stroke: #999
Sascha Herzinger's avatar
Sascha Herzinger committed
435
436
437
438
  .fjs-x-axis
    .tick
      text
        text-anchor: start
439
        font-size: 1rem
Sascha Herzinger's avatar
Sascha Herzinger committed
440
</style>