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

  </chart>
Sascha Herzinger's avatar
Sascha Herzinger committed
115
116
117
118
</template>

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

<style lang="sass" scoped>
394
  @import '~assets/base.sass'
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-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>