Boxplot.vue 15.1 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
28
29
30
31

    <div class="fjs-vis-container">
      <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
116
117
118
119
120
121
122
        </g>
      </svg>
    </div>
  </div>
</template>

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

  *
    font-family: Roboto, sans-serif

  .fjs-boxplot
    height: 100%
    width: 100%
    display: flex
    flex-direction: column
392
393
394
395
    .fjs-control-panel
      hr
        width: 100%
        margin: 10% 0 10% 0
Sascha Herzinger's avatar
Sascha Herzinger committed
396
397
398
    .fjs-vis-container
      flex: 1
      display: flex
399
400
      .fjs-tooltip
        position: absolute
Sascha Herzinger's avatar
Sascha Herzinger committed
401
402
      svg
        flex: 1
403
404
405
406
407
408
        .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
409
            stroke-width: 1px
410
411
412
413
414
415
416
417
          .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
418
        .fjs-points
Sascha Herzinger's avatar
Sascha Herzinger committed
419
420
421
422
          stroke: white
          stroke-width: 1px
        .fjs-points:hover
          opacity: 0.4
Sascha Herzinger's avatar
Sascha Herzinger committed
423
424
425
        .fjs-kde
          fill: none
          stroke: black
426
          stroke-width: 0.2%
Sascha Herzinger's avatar
Sascha Herzinger committed
427
428
429
430
431
</style>


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