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

  </chart>
Sascha Herzinger's avatar
Sascha Herzinger committed
119
120
121
122
</template>

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

<style lang="sass" scoped>
392
  @import './../../../src/assets/base.sass'
Sascha Herzinger's avatar
Sascha Herzinger committed
393

394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
  svg
    .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
        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-points
      stroke: white
      stroke-width: 1px
    .fjs-points:hover
      opacity: 0.4
    .fjs-kde
      fill: none
      stroke: black
      stroke-width: 0.2%
Sascha Herzinger's avatar
Sascha Herzinger committed
419
420
421
422
423
</style>


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