Boxplot.vue 15.1 KB
Newer Older
Sascha Herzinger's avatar
Sascha Herzinger committed
1
<template>
Sascha Herzinger's avatar
Sascha Herzinger committed
2
  <div class="fjs-boxplot">
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
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
        </g>
      </svg>
    </div>
117
    <task-view></task-view>
Sascha Herzinger's avatar
Sascha Herzinger committed
118
119
120
121
122
123
  </div>
</template>

<script>
  import DataBox from '../components/DataBox.vue'
  import store from '../../store/store'
Sascha Herzinger's avatar
Sascha Herzinger committed
124
  import runAnalysis from '../mixins/run-analysis'
Sascha Herzinger's avatar
Sascha Herzinger committed
125
  import * as d3 from 'd3'
Sascha Herzinger's avatar
Sascha Herzinger committed
126
  import { TweenLite } from 'gsap'
Sascha Herzinger's avatar
Sascha Herzinger committed
127
128
  import TaskView from '../components/TaskView.vue'
  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'
131
  import ControlPanel from '../components/ControlPanel.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
132
133
134
135
136
137
138
139
  export default {
    name: 'boxplot',
    data () {
      return {
        width: 0,
        height: 0,
        numData: [],
        catData: [],
140
141
142
        tooltips: {
          boxes: {}
        },
143
        params: {
Sascha Herzinger's avatar
Sascha Herzinger committed
144
145
146
          showData: false,
          jitter: false,
          showKDE: false
147
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
148
149
150
        results: {
          data: [],
          statistics: {}
Sascha Herzinger's avatar
Sascha Herzinger committed
151
152
153
        },
        tweened: {
          boxes: {}
Sascha Herzinger's avatar
Sascha Herzinger committed
154
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
155
156
157
      }
    },
    computed: {
158
159
160
      idFilter () {
        return store.getters.filter('ids')
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
161
162
      args () {
        return {
163
164
          variables: this.numData,
          categories: this.catData,
165
          id_filter: this.idFilter,
Sascha Herzinger's avatar
Sascha Herzinger committed
166
167
168
169
170
171
172
          subsets: store.getters.subsets
        }
      },
      validArgs () {
        return this.numData.length > 0
      },
      margin () {
173
        const left = 60
Sascha Herzinger's avatar
Sascha Herzinger committed
174
        const top = 10
Sascha Herzinger's avatar
Sascha Herzinger committed
175
176
        const right = this.width * 0.3
        const bottom = this.height * 0.3
Sascha Herzinger's avatar
Sascha Herzinger committed
177
178
179
180
181
182
183
        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
184
185
186
      labels () {
        return Object.keys(this.results.statistics)
      },
187
188
      points () {
        const points = {}
Sascha Herzinger's avatar
Sascha Herzinger committed
189
        this.labels.forEach(label => {
190
191
192
          let [variable, category, subset] = label.split('//')
          subset = parseInt(subset.substring(1)) - 1  // revert subset string formatting
          points[label] = this.results.data
193
            .filter(d => d.subset === subset && d.category === category && typeof d[variable] === 'number')
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
            .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>
`
                }
              }
            })
212
213
214
        })
        return points
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
215
216
217
218
219
220
221
222
223
224
225
226
227
      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
      },
228
      numOfBoxplots () {
Sascha Herzinger's avatar
Sascha Herzinger committed
229
        return this.labels.length
230
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
231
      boxplotWidth () {
232
        const maxBoxplotWidth = this.padded.width / 4
233
        const minBoxplotWidth = 10
234
235
        let boxplotWidth = this.padded.width / this.numOfBoxplots - this.padded.width * 0.05
        boxplotWidth = boxplotWidth > maxBoxplotWidth ? maxBoxplotWidth : boxplotWidth
236
        boxplotWidth = boxplotWidth < minBoxplotWidth ? minBoxplotWidth : boxplotWidth
237
238
239
240
241
242
        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
243
        const padding = (extent[1] - extent[0]) / 20
Sascha Herzinger's avatar
Sascha Herzinger committed
244
        const x = d3.scalePoint()
Sascha Herzinger's avatar
Sascha Herzinger committed
245
          .domain(this.labels)
Sascha Herzinger's avatar
Sascha Herzinger committed
246
          .range([0, this.padded.width])
247
          .padding(0.5)
248
        const y = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
249
          .domain([extent[0] - padding, extent[1] + padding])
Sascha Herzinger's avatar
Sascha Herzinger committed
250
          .range([this.padded.height, 0])
Sascha Herzinger's avatar
Sascha Herzinger committed
251
252
        return { x, y }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
      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
276
      axis () {
Sascha Herzinger's avatar
Sascha Herzinger committed
277
278
        const x = d3.axisBottom(this.scales.x).tickFormat(d => {
          // noinspection JSSuspiciousNameCombination
279
          return truncateTextUntil({text: d, font: `0.875rem Roboto`, maxWidth: this.margin.bottom})
Sascha Herzinger's avatar
Sascha Herzinger committed
280
        })
Sascha Herzinger's avatar
Sascha Herzinger committed
281
        const y = d3.axisLeft(this.scales.y)
282
        return { x, y }
Sascha Herzinger's avatar
Sascha Herzinger committed
283
284
      }
    },
285
286
287
    // 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
288
    watch: {
Sascha Herzinger's avatar
Sascha Herzinger committed
289
290
      'boxes': {
        handler: function (newBoxes) {
291
292
          const labels = Object.keys(newBoxes)
          labels.forEach((label, i) => {
Sascha Herzinger's avatar
Sascha Herzinger committed
293
294
295
            if (typeof this.tweened.boxes[label] === 'undefined') {
              this.$set(this.tweened.boxes, label, newBoxes[label])
            } else {
296
297
              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
298
299
300
301
            }
          })
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
302
      'args': {
303
        handler: function () {
Sascha Herzinger's avatar
Sascha Herzinger committed
304
          if (this.validArgs) {
Sascha Herzinger's avatar
Sascha Herzinger committed
305
            this.runAnalysisWrapper(this.args)
Sascha Herzinger's avatar
Sascha Herzinger committed
306
307
          }
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
308
309
310
      },
      'axis': {
        handler: function (newAxis) {
311
          this.$nextTick(() => {
Sascha Herzinger's avatar
Sascha Herzinger committed
312
            d3.select(this.$el.querySelector('.fjs-x-axis'))
313
314
315
              .call(newAxis.x)
              .selectAll('text')
              .attr('transform', 'rotate(20)')
Sascha Herzinger's avatar
Sascha Herzinger committed
316
            d3.select(this.$el.querySelector('.fjs-y-axis'))
317
318
319
              .call(newAxis.y)
          })
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
320
321
322
      }
    },
    methods: {
323
324
      getTippyInstances (label) {
        return [
Sascha Herzinger's avatar
Sascha Herzinger committed
325
326
327
328
329
          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`)
330
331
332
        ].map(el => {
          const uuid = el.getAttribute('data-uuid')
          return { el, tip: this._tippyInstances[uuid] }
333
334
        })
      },
335
336
337
      showTooltip (label) {
        this.getTippyInstances(label).forEach(d => d.tip.show(d.tip.getPopperElement(d.el)))
      },
338
      hideTooltip (label) {
339
        this.getTippyInstances(label).forEach(d => d.tip.hide(d.tip.getPopperElement(d.el)))
340
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
341
342
343
344
345
346
347
      update_numData (ids) {
        this.numData = ids
      },
      update_catData (ids) {
        this.catData = ids
      },
      handleResize () {
Sascha Herzinger's avatar
Sascha Herzinger committed
348
        const container = this.$el.querySelector('.fjs-vis-container svg')
Sascha Herzinger's avatar
Sascha Herzinger committed
349
350
351
352
        // noinspection JSSuspiciousNameCombination
        this.height = container.getBoundingClientRect().width
        this.width = container.getBoundingClientRect().width
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
353
      runAnalysisWrapper (args) {
Sascha Herzinger's avatar
Sascha Herzinger committed
354
        // function made available via requestHandling mixin
Sascha Herzinger's avatar
Sascha Herzinger committed
355
        runAnalysis({task_name: 'compute-boxplot', args})
Sascha Herzinger's avatar
Sascha Herzinger committed
356
357
358
359
360
361
362
363
364
365
366
          .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: {
367
      ControlPanel,
Sascha Herzinger's avatar
Sascha Herzinger committed
368
369
370
      DataBox,
      TaskView
    },
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
392
393
394
    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
395
396
397
398
    .fjs-control-panel
      hr
        width: 100%
        margin: 10% 0 10% 0
Sascha Herzinger's avatar
Sascha Herzinger committed
399
400
401
    .fjs-vis-container
      flex: 1
      display: flex
402
403
      .fjs-tooltip
        position: absolute
Sascha Herzinger's avatar
Sascha Herzinger committed
404
405
      svg
        flex: 1
406
407
408
409
410
411
        .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
412
            stroke-width: 1px
413
414
415
416
417
418
419
420
          .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
421
        .fjs-points
Sascha Herzinger's avatar
Sascha Herzinger committed
422
423
424
425
          stroke: white
          stroke-width: 1px
        .fjs-points:hover
          opacity: 0.4
Sascha Herzinger's avatar
Sascha Herzinger committed
426
427
428
        .fjs-kde
          fill: none
          stroke: black
429
          stroke-width: 0.2%
Sascha Herzinger's avatar
Sascha Herzinger committed
430
431
432
433
434
</style>


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