Boxplot.vue 15 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
163
          variables: this.numData,
          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
190
191
          let [variable, category, subset] = label.split('//')
          subset = parseInt(subset.substring(1)) - 1  // revert subset string formatting
          points[label] = this.results.data
192
            .filter(d => d.subset === subset && d.category === category && typeof d[variable] === 'number')
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
            .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>
`
                }
              }
            })
211
212
213
        })
        return points
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
214
215
216
217
218
219
220
221
222
223
224
225
226
      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
      },
227
      numOfBoxplots () {
Sascha Herzinger's avatar
Sascha Herzinger committed
228
        return this.labels.length
229
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
230
      boxplotWidth () {
231
        const maxBoxplotWidth = this.padded.width / 4
232
        const minBoxplotWidth = 10
233
234
        let boxplotWidth = this.padded.width / this.numOfBoxplots - this.padded.width * 0.05
        boxplotWidth = boxplotWidth > maxBoxplotWidth ? maxBoxplotWidth : boxplotWidth
235
        boxplotWidth = boxplotWidth < minBoxplotWidth ? minBoxplotWidth : boxplotWidth
236
237
238
239
240
241
        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
242
        const padding = (extent[1] - extent[0]) / 20
Sascha Herzinger's avatar
Sascha Herzinger committed
243
        const x = d3.scalePoint()
Sascha Herzinger's avatar
Sascha Herzinger committed
244
          .domain(this.labels)
Sascha Herzinger's avatar
Sascha Herzinger committed
245
          .range([0, this.padded.width])
246
          .padding(0.5)
247
        const y = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
248
          .domain([extent[0] - padding, extent[1] + padding])
Sascha Herzinger's avatar
Sascha Herzinger committed
249
          .range([this.padded.height, 0])
Sascha Herzinger's avatar
Sascha Herzinger committed
250
251
        return { x, y }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
      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
275
      axis () {
Sascha Herzinger's avatar
Sascha Herzinger committed
276
277
        const x = d3.axisBottom(this.scales.x).tickFormat(d => {
          // noinspection JSSuspiciousNameCombination
278
          return truncateTextUntil({text: d, font: `0.875rem Roboto`, maxWidth: this.margin.bottom})
Sascha Herzinger's avatar
Sascha Herzinger committed
279
        })
Sascha Herzinger's avatar
Sascha Herzinger committed
280
        const y = d3.axisLeft(this.scales.y)
281
        return { x, y }
Sascha Herzinger's avatar
Sascha Herzinger committed
282
283
      }
    },
284
285
286
    // 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
287
    watch: {
Sascha Herzinger's avatar
Sascha Herzinger committed
288
289
      'boxes': {
        handler: function (newBoxes) {
290
291
          const labels = Object.keys(newBoxes)
          labels.forEach((label, i) => {
Sascha Herzinger's avatar
Sascha Herzinger committed
292
293
294
            if (typeof this.tweened.boxes[label] === 'undefined') {
              this.$set(this.tweened.boxes, label, newBoxes[label])
            } else {
295
296
              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
297
298
299
300
            }
          })
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
301
      'args': {
302
        handler: function () {
Sascha Herzinger's avatar
Sascha Herzinger committed
303
          if (this.validArgs) {
Sascha Herzinger's avatar
Sascha Herzinger committed
304
            this.runAnalysisWrapper(this.args)
Sascha Herzinger's avatar
Sascha Herzinger committed
305
306
          }
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
307
308
309
      },
      'axis': {
        handler: function (newAxis) {
310
          this.$nextTick(() => {
Sascha Herzinger's avatar
Sascha Herzinger committed
311
            d3.select(this.$el.querySelector('.fjs-x-axis'))
312
313
314
              .call(newAxis.x)
              .selectAll('text')
              .attr('transform', 'rotate(20)')
Sascha Herzinger's avatar
Sascha Herzinger committed
315
            d3.select(this.$el.querySelector('.fjs-y-axis'))
316
317
318
              .call(newAxis.y)
          })
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
319
320
321
      }
    },
    methods: {
322
323
      getTippyInstances (label) {
        return [
Sascha Herzinger's avatar
Sascha Herzinger committed
324
325
326
327
328
          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`)
329
330
331
        ].map(el => {
          const uuid = el.getAttribute('data-uuid')
          return { el, tip: this._tippyInstances[uuid] }
332
333
        })
      },
334
335
336
      showTooltip (label) {
        this.getTippyInstances(label).forEach(d => d.tip.show(d.tip.getPopperElement(d.el)))
      },
337
      hideTooltip (label) {
338
        this.getTippyInstances(label).forEach(d => d.tip.hide(d.tip.getPopperElement(d.el)))
339
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
340
341
342
343
344
345
346
      update_numData (ids) {
        this.numData = ids
      },
      update_catData (ids) {
        this.catData = ids
      },
      handleResize () {
Sascha Herzinger's avatar
Sascha Herzinger committed
347
        const container = this.$el.querySelector('.fjs-chart svg')
Sascha Herzinger's avatar
Sascha Herzinger committed
348
349
350
351
        // noinspection JSSuspiciousNameCombination
        this.height = container.getBoundingClientRect().width
        this.width = container.getBoundingClientRect().width
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
352
      runAnalysisWrapper (args) {
Sascha Herzinger's avatar
Sascha Herzinger committed
353
        // function made available via requestHandling mixin
Sascha Herzinger's avatar
Sascha Herzinger committed
354
        runAnalysis({task_name: 'compute-boxplot', args})
Sascha Herzinger's avatar
Sascha Herzinger committed
355
356
357
358
359
360
361
362
363
364
365
          .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: {
366
      ControlPanel,
Sascha Herzinger's avatar
Sascha Herzinger committed
367
368
      DataBox,
      Chart
Sascha Herzinger's avatar
Sascha Herzinger committed
369
    },
370
371
372
    directives: {
      tooltip
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
    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
391
    .fjs-control-panel
Sascha Herzinger's avatar
Sascha Herzinger committed
392
    .fjs-chart
Sascha Herzinger's avatar
Sascha Herzinger committed
393
394
      flex: 1
      display: flex
395
396
      .fjs-tooltip
        position: absolute
Sascha Herzinger's avatar
Sascha Herzinger committed
397
398
      svg
        flex: 1
399
400
401
402
403
404
        .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
405
            stroke-width: 1px
406
407
408
409
410
411
412
413
          .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
414
        .fjs-points
Sascha Herzinger's avatar
Sascha Herzinger committed
415
416
417
418
          stroke: white
          stroke-width: 1px
        .fjs-points:hover
          opacity: 0.4
Sascha Herzinger's avatar
Sascha Herzinger committed
419
420
421
        .fjs-kde
          fill: none
          stroke: black
422
          stroke-width: 0.2%
Sascha Herzinger's avatar
Sascha Herzinger committed
423
424
425
426
427
</style>


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