Boxplot.vue 14 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
30
31
32
33
34
35
36
37
38
39
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
    <svg :width="width"
         :height="height">
      <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"
           @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
112
        </g>
113
114
115
116
      </g>
    </svg>

  </chart>
Sascha Herzinger's avatar
Sascha Herzinger committed
117
118
119
120
</template>

<script>
  import DataBox from '../components/DataBox.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
121
122
  import ControlPanel from '../components/ControlPanel.vue'
  import Chart from '../components/Chart.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
123
  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
  import deepFreeze from 'deep-freeze-strict'
Sascha Herzinger's avatar
Sascha Herzinger committed
128
  import { truncateTextUntil } from '../mixins/utils'
129
  import tooltip from '../directives/tooltip'
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
          features: this.numData,
162
          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
          let [feature, category, subset] = label.split('//')
189
190
          subset = parseInt(subset.substring(1)) - 1  // revert subset string formatting
          points[label] = this.results.data
191
192
193
194
            .filter(d => d.subset === subset &&
              d.feature === feature &&
              d.category === category &&
              typeof d.value === 'number')
195
196
197
            .map(d => {
              return {
                id: d.id,
198
                value: d.value,
199
200
201
202
203
204
                jitter: this.params.jitter ? Math.random() * this.boxplotWidth / 2 : this.boxplotWidth / 2,
                subset: d.subset,
                category: d.category,
                get tooltip () {
                  return `
<div>
205
  <p>${d.feature}: ${this.value}</p>
206
207
208
209
210
211
212
  <p>Category: ${this.category}</p>
  <p>Subset: ${this.subset + 1}</p>
</div>
`
                }
              }
            })
213
214
215
        })
        return points
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
216
217
218
219
220
221
222
223
224
225
226
227
228
      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
      },
229
      numOfBoxplots () {
Sascha Herzinger's avatar
Sascha Herzinger committed
230
        return this.labels.length
231
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
232
      boxplotWidth () {
233
        const maxBoxplotWidth = this.padded.width / 4
234
        const minBoxplotWidth = 10
235
236
        let boxplotWidth = this.padded.width / this.numOfBoxplots - this.padded.width * 0.05
        boxplotWidth = boxplotWidth > maxBoxplotWidth ? maxBoxplotWidth : boxplotWidth
237
        boxplotWidth = boxplotWidth < minBoxplotWidth ? minBoxplotWidth : boxplotWidth
238
239
240
        return boxplotWidth
      },
      scales () {
241
        const values = this.results.data.map(d => d.value)
242
243
        const flattened = [].concat.apply([], values)
        const extent = d3.extent(flattened)
Sascha Herzinger's avatar
Sascha Herzinger committed
244
        const padding = (extent[1] - extent[0]) / 20
Sascha Herzinger's avatar
Sascha Herzinger committed
245
        const x = d3.scalePoint()
Sascha Herzinger's avatar
Sascha Herzinger committed
246
          .domain(this.labels)
Sascha Herzinger's avatar
Sascha Herzinger committed
247
          .range([0, this.padded.width])
248
          .padding(0.5)
249
        const y = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
250
          .domain([extent[0] - padding, extent[1] + padding])
Sascha Herzinger's avatar
Sascha Herzinger committed
251
          .range([this.padded.height, 0])
Sascha Herzinger's avatar
Sascha Herzinger committed
252
253
        return { x, y }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
      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
277
      axis () {
Sascha Herzinger's avatar
Sascha Herzinger committed
278
279
        const x = d3.axisBottom(this.scales.x).tickFormat(d => {
          // noinspection JSSuspiciousNameCombination
280
          return truncateTextUntil({text: d, font: `0.875em Roboto`, maxWidth: this.margin.bottom})
Sascha Herzinger's avatar
Sascha Herzinger committed
281
        })
Sascha Herzinger's avatar
Sascha Herzinger committed
282
        const y = d3.axisLeft(this.scales.y)
283
        return { x, y }
Sascha Herzinger's avatar
Sascha Herzinger committed
284
285
      }
    },
286
287
288
    // 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
289
    watch: {
Sascha Herzinger's avatar
Sascha Herzinger committed
290
291
      'boxes': {
        handler: function (newBoxes) {
292
293
          const labels = Object.keys(newBoxes)
          labels.forEach((label, i) => {
Sascha Herzinger's avatar
Sascha Herzinger committed
294
295
296
            if (typeof this.tweened.boxes[label] === 'undefined') {
              this.$set(this.tweened.boxes, label, newBoxes[label])
            } else {
297
298
              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
299
300
301
302
            }
          })
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
303
      'args': {
304
        handler: function () {
Sascha Herzinger's avatar
Sascha Herzinger committed
305
          if (this.validArgs) {
Sascha Herzinger's avatar
Sascha Herzinger committed
306
            this.runAnalysisWrapper(this.args)
Sascha Herzinger's avatar
Sascha Herzinger committed
307
308
          }
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
309
310
311
      },
      'axis': {
        handler: function (newAxis) {
312
          this.$nextTick(() => {
Sascha Herzinger's avatar
Sascha Herzinger committed
313
            d3.select(this.$el.querySelector('.fjs-x-axis'))
314
315
316
              .call(newAxis.x)
              .selectAll('text')
              .attr('transform', 'rotate(20)')
Sascha Herzinger's avatar
Sascha Herzinger committed
317
            d3.select(this.$el.querySelector('.fjs-y-axis'))
318
319
320
              .call(newAxis.y)
          })
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
321
322
323
      }
    },
    methods: {
324
325
      getTippyInstances (label) {
        return [
Sascha Herzinger's avatar
Sascha Herzinger committed
326
327
328
329
330
          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`)
331
332
333
        ].map(el => {
          const uuid = el.getAttribute('data-uuid')
          return { el, tip: this._tippyInstances[uuid] }
334
335
        })
      },
336
337
338
      showTooltip (label) {
        this.getTippyInstances(label).forEach(d => d.tip.show(d.tip.getPopperElement(d.el)))
      },
339
      hideTooltip (label) {
340
        this.getTippyInstances(label).forEach(d => d.tip.hide(d.tip.getPopperElement(d.el)))
341
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
342
343
344
345
346
347
      update_numData (ids) {
        this.numData = ids
      },
      update_catData (ids) {
        this.catData = ids
      },
348
349
350
      resize ({height, width}) {
        this.height = height
        this.width = width
Sascha Herzinger's avatar
Sascha Herzinger committed
351
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
352
      runAnalysisWrapper (args) {
Sascha Herzinger's avatar
Sascha Herzinger committed
353
        runAnalysis({task_name: 'compute-boxplot', args})
Sascha Herzinger's avatar
Sascha Herzinger committed
354
355
          .then(response => {
            const results = JSON.parse(response)
356
            results.data = JSON.parse(results.data)
Sascha Herzinger's avatar
Sascha Herzinger committed
357
358
359
360
361
362
363
            deepFreeze(results) // massively improve performance by telling Vue that the objects properties won't change
            this.results = results
          })
          .catch(error => console.error(error))
      }
    },
    components: {
364
      ControlPanel,
Sascha Herzinger's avatar
Sascha Herzinger committed
365
366
      DataBox,
      Chart
Sascha Herzinger's avatar
Sascha Herzinger committed
367
    },
368
369
    directives: {
      tooltip
Sascha Herzinger's avatar
Sascha Herzinger committed
370
371
372
373
374
375
376
    }
  }
</script>

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

377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
  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
402
403
404
405
406
</style>


<!--CSS for dynamically created components-->
<style lang="sass">
407
  .fjs-boxplot-axis
Sascha Herzinger's avatar
Sascha Herzinger committed
408
409
410
    shape-rendering: crispEdges
    .tick
      shape-rendering: crispEdges
Sascha Herzinger's avatar
Sascha Herzinger committed
411
      text
412
        font-size: 1em
Sascha Herzinger's avatar
Sascha Herzinger committed
413
414
    line
      stroke: #999
415
416
417
418
419
    .fjs-x-axis
      .tick
        text
          text-anchor: start
          font-size: 1em
Sascha Herzinger's avatar
Sascha Herzinger committed
420
</style>