Boxplot.vue 14.6 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 { TweenLite } 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
297
          const labels = Object.keys(newBoxes)
          labels.forEach((label, i) => {
Sascha Herzinger's avatar
Sascha Herzinger committed
298
299
300
            if (typeof this.tweened.boxes[label] === 'undefined') {
              this.$set(this.tweened.boxes, label, newBoxes[label])
            } else {
301
302
              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
303
304
305
306
            }
          })
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
307
      'args': {
308
        handler: function () {
309
          if (this.validArgs && !this.hasSetFilter) {
Sascha Herzinger's avatar
Sascha Herzinger committed
310
            this.runAnalysisWrapper(this.args)
Sascha Herzinger's avatar
Sascha Herzinger committed
311
          }
312
          this.hasSetFilter = false
Sascha Herzinger's avatar
Sascha Herzinger committed
313
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
314
315
316
      },
      'axis': {
        handler: function (newAxis) {
317
          this.$nextTick(() => {
Sascha Herzinger's avatar
Sascha Herzinger committed
318
            d3.select(this.$el.querySelector('.fjs-x-axis'))
319
320
321
              .call(newAxis.x)
              .selectAll('text')
              .attr('transform', 'rotate(20)')
Sascha Herzinger's avatar
Sascha Herzinger committed
322
            d3.select(this.$el.querySelector('.fjs-y-axis'))
323
324
325
              .call(newAxis.y)
          })
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
326
327
328
      }
    },
    methods: {
329
330
      getTippyInstances (label) {
        return [
Sascha Herzinger's avatar
Sascha Herzinger committed
331
332
333
334
335
          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`)
336
337
338
        ].map(el => {
          const uuid = el.getAttribute('data-uuid')
          return { el, tip: this._tippyInstances[uuid] }
339
340
        })
      },
341
342
343
      showTooltip (label) {
        this.getTippyInstances(label).forEach(d => d.tip.show(d.tip.getPopperElement(d.el)))
      },
344
      hideTooltip (label) {
345
        this.getTippyInstances(label).forEach(d => d.tip.hide(d.tip.getPopperElement(d.el)))
346
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
347
348
349
350
351
352
      update_numData (ids) {
        this.numData = ids
      },
      update_catData (ids) {
        this.catData = ids
      },
353
354
355
356
357
358
359
360
      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
      },
361
362
363
      resize ({height, width}) {
        this.height = height
        this.width = width
Sascha Herzinger's avatar
Sascha Herzinger committed
364
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
365
      runAnalysisWrapper (args) {
Sascha Herzinger's avatar
Sascha Herzinger committed
366
        runAnalysis({task_name: 'compute-boxplot', args})
Sascha Herzinger's avatar
Sascha Herzinger committed
367
368
          .then(response => {
            const results = JSON.parse(response)
369
            results.data = JSON.parse(results.data)
Sascha Herzinger's avatar
Sascha Herzinger committed
370
371
372
373
374
375
376
            deepFreeze(results) // massively improve performance by telling Vue that the objects properties won't change
            this.results = results
          })
          .catch(error => console.error(error))
      }
    },
    components: {
377
      ControlPanel,
Sascha Herzinger's avatar
Sascha Herzinger committed
378
379
      DataBox,
      Chart
Sascha Herzinger's avatar
Sascha Herzinger committed
380
    },
381
382
    directives: {
      tooltip
Sascha Herzinger's avatar
Sascha Herzinger committed
383
384
385
386
387
388
389
    }
  }
</script>

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

390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
  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
415
416
417
418
419
</style>


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