CorrelationAnalysis.vue 18.8 KB
Newer Older
1
<template>
Sascha Herzinger's avatar
Sascha Herzinger committed
2
  <div class="fjs-correlation-analysis">
3
4

    <control-panel class="fjs-control-panel">
5
6
      <data-box class="fjs-data-box"
                header="X and Y variables"
7
8
9
                dataType="numerical"
                v-on:update="update_xyData">
      </data-box>
10
      <data-box class="fjs-data-box"
11
                header="Categories"
12
                dataType="categorical"
13
                v-on:update="update_categoryData">
14
      </data-box>
15
      <hr class="fjs-seperator"/>
16
17
18
19
      <fieldset class="fjs-correlation-method">
        <legend>Correlation Method</legend>
        <input type="radio" id="fjs-param-method-1" value="pearson" v-model="params.method">
        <label for="fjs-param-method-1">Pearson</label>
20
        <br/>
21
22
        <input type="radio" id="fjs-param-method-2" value="spearman" v-model="params.method">
        <label for="fjs-param-method-2">Spearman</label>
23
        <br/>
24
25
26
        <input type="radio" id="fjs-param-method-3" value="kendall" v-model="params.method">
        <label for="fjs-param-method-3">Kendall</label>
      </fieldset>
27
    </control-panel>
28

29
    <div class="fjs-vis-container">
30
      <svg :height="height" :width="width">
31
        <g :transform="`translate(${margin.left}, ${margin.top})`">
32
33
34
35
36
          <g class="fjs-corr-axis fjs-x-axis-1" :transform="`translate(0, ${padded.height})`"></g>
          <g class="fjs-corr-axis fjs-x-axis-2"></g>
          <g class="fjs-corr-axis fjs-y-axis-1"></g>
          <g class="fjs-corr-axis fjs-y-axis-2" :transform="`translate(${padded.width}, 0)`"></g>
          <g class="fjs-brush"></g>
37
38
          <text :x="padded.width / 2"
                y="-10"
39
                text-anchor="middle">
Sascha Herzinger's avatar
Sascha Herzinger committed
40
            {{ shownResults.x_label }}
41
42
43
44
45
          </text>
          <text :x="padded.width + 10"
                :y="padded.height / 2"
                text-anchor="middle"
                :transform="`rotate(90 ${padded.width + 10} ${padded.height / 2})`">
Sascha Herzinger's avatar
Sascha Herzinger committed
46
            {{ shownResults.y_label }}
47
          </text>
48
49
50
          <circle class="fjs-scatterplot-point"
                  :cx="scales.x(point.x)"
                  :cy="scales.y(point.y)"
51
                  r="0.4%"
52
                  :fill="categoryColors[categories.indexOf(point.category) % categoryColors.length]"
53
                  :stroke="subsetColors[point.subset]"
54
55
                  :title="point.tooltip"
                  v-tooltip
56
57
                  v-for="point in shownPoints.all">
          </circle>
58
          <line class="fjs-lin-reg-line"
59
60
                :title="regLine.tooltip"
                v-tooltip="{followCursor: true}"
Sascha Herzinger's avatar
Sascha Herzinger committed
61
62
63
                :x1="tweened.regLine.x1"
                :x2="tweened.regLine.x2"
                :y1="tweened.regLine.y1"
64
                :y2="tweened.regLine.y2">
65
          </line>
Sascha Herzinger's avatar
Sascha Herzinger committed
66
67
          <polyline class="fjs-histogram-polyline fjs-bottom" points=""></polyline>
          <polyline class="fjs-histogram-polyline fjs-left" points=""></polyline>
68
69
        </g>
      </svg>
70
71
72
73
      <div class="fjs-table-container">
        <table class="fjs-stats-table">
          <caption>Selected points</caption>
          <tr>
Sascha Herzinger's avatar
Sascha Herzinger committed
74
            <td>Coefficient</td>
75
            <td>{{ parseFloat(tmpResults.coef).toFixed(4) }}</td>
76
77
78
          </tr>
          <tr>
            <td>p-value</td>
79
            <td>{{ parseFloat(tmpResults.p_value).toFixed(4) }}</td>
80
81
          </tr>
          <tr>
Sascha Herzinger's avatar
Sascha Herzinger committed
82
            <td>Method</td>
Sascha Herzinger's avatar
Sascha Herzinger committed
83
            <td>{{ tmpResults.method }}</td>
84
85
86
87
88
89
90
          </tr>
          <tr>
            <td>#Points</td>
            <td>{{ tmpPoints.all.length }}</td>
          </tr>
        </table>
        <table class="fjs-stats-table"
Sascha Herzinger's avatar
Sascha Herzinger committed
91
               v-for="(stats, i) in tmpResults.subsets">
92
93
          <caption>Subset: {{ i + 1 }}</caption>
          <tr>
Sascha Herzinger's avatar
Sascha Herzinger committed
94
            <td>Coefficient</td>
95
            <td>{{ parseFloat(stats.coef).toFixed(4) }}</td>
96
97
98
          </tr>
          <tr>
            <td>p-value</td>
99
            <td>{{ parseFloat(stats.p_value).toFixed(4) }}</td>
100
101
          </tr>
          <tr>
Sascha Herzinger's avatar
Sascha Herzinger committed
102
            <td>Method</td>
Sascha Herzinger's avatar
Sascha Herzinger committed
103
            <td>{{ tmpResults.method }}</td>
104
105
106
          </tr>
          <tr>
            <td>#Points</td>
Sascha Herzinger's avatar
Sascha Herzinger committed
107
            <td>{{ tmpPoints.subsets.filter(function(d) { return d === i}).length }}</td>
108
109
110
          </tr>
        </table>
      </div>
111
    </div>
112
  </div>
113
114
115
</template>

<script>
Sascha Herzinger's avatar
Sascha Herzinger committed
116
  import DataBox from '../components/DataBox.vue'
117
  import store from '../../store/store'
Sascha Herzinger's avatar
Sascha Herzinger committed
118
  import runAnalysis from '../mixins/run-analysis'
119
  import * as d3 from 'd3'
Sascha Herzinger's avatar
Sascha Herzinger committed
120
  import { TweenLite } from 'gsap'
121
  import tooltip from '../directives/tooltip.js'
122
  import deepFreeze from 'deep-freeze-strict'
123
  import ControlPanel from '../components/ControlPanel.vue'
124
125
  export default {
    name: 'correlation-analysis',
126
    data () {
127
      return {
128
        error: '',
129
130
        width: 0,
        height: 0,
Sascha Herzinger's avatar
Sascha Herzinger committed
131
        yAxisTickWidth: 0,
132
        xyData: [],
133
134
        categoryData: [],
        categoryColors: d3.schemeCategory10,
135
        subsetColors: d3.schemeCategory10.slice().reverse(),
136
137
138
        params: {
          method: 'pearson'
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
139
        shownResults: {  // initially computed
140
141
142
143
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
144
          method: 'pearson',
145
146
          x_label: '',
          y_label: '',
147
          data: []
148
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
149
        tmpResults: {  // on-the-fly computed
150
151
152
153
          coef: 0,
          p_value: 0,
          slope: 0,
          intercept: 0,
154
          method: 'pearson',
155
156
          x_label: '',
          y_label: '',
157
          data: []
158
        },
Sascha Herzinger's avatar
Sascha Herzinger committed
159
160
161
162
        selectedPoints: [],
        tweened: {
          regLine: {x1: 0, x2: 0, y1: 0, y2: 0}
        }
163
164
      }
    },
165
    computed: {
166
167
168
      idFilter () {
        return store.getters.filter('ids')
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
169
170
      validArgs () {
        return this.xyData.length === 2
171
      },
172
173
      args () {
        return {
174
175
          x: this.xyData[0],
          y: this.xyData[1],
176
          id_filter: this.selectedPoints.map(d => d.id),
177
178
          method: this.params.method,
          subsets: store.getters.subsets,
179
          categories: this.categoryData
180
181
        }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
182
183
      margin () {
        const left = this.width / 3
184
185
        const top = 50
        const right = 50
Sascha Herzinger's avatar
Sascha Herzinger committed
186
187
188
        const bottom = this.height / 3
        return { left, top, right, bottom }
      },
189
190
191
192
      padded () {
        const width = this.width - this.margin.left - this.margin.right
        const height = this.height - this.margin.top - this.margin.bottom
        return { width, height }
193
      },
194
195
      categories () {
        return this.shownPoints.categories.filter((d, i, arr) => arr.indexOf(d) === i)  // make unique
196
      },
197
      shownPoints () {
198
199
200
        const xs = []
        const ys = []
        const ids = []
201
        const subsets = []
202
        const categories = []
203
204
205
206
207
        const all = this.shownResults.data.map(d => {
          const x = d[this.shownResults.x_label]
          const y = d[this.shownResults.y_label]
          const id = d.id
          const subset = d.subset
208
          const category = d.category
209
210
211
212
213
          let tooltip = `
<div>
  <p>${[this.shownResults.x_label]}: ${x}</p>
  <p>${[this.shownResults.y_label]}: ${y}</p>
  <p>Subset: ${subset}</p>
214
  ${typeof category !== 'undefined' ? '<p>Category: ' + category + '</p>' : ''}
215
216
217
218
219
220
</div>
`
          xs.push(x)
          ys.push(y)
          ids.push(id)
          subsets.push(subset)
221
222
          categories.push(category)
          return {x, y, id, subset, category, tooltip}
223
        })
224
        return { xs, ys, ids, subsets, categories, all }
225
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
226
      tmpPoints () {
227
228
        const xs = []
        const ys = []
229
230
        const ids = []
        const subsets = []
231
        const categories = []
232
233
234
235
236
        const all = this.tmpResults.data.map(d => {
          const x = d[this.tmpResults.x_label]
          const y = d[this.tmpResults.y_label]
          const id = d.id
          const subset = d.subset
237
          const category = d.category
238
239
240
241
          xs.push(x)
          ys.push(y)
          ids.push(id)
          subsets.push(subset)
242
243
          categories.push(category)
          return {x, y, id, subset, category}
244
        })
245
        return { xs, ys, ids, subsets, categories, all }
246
      },
247
248
      scales () {
        const x = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
249
250
251
252
253
          .domain((() => {
            const xExtent = d3.extent(this.shownPoints.xs)
            const xPadding = (xExtent[1] - xExtent[0]) / 10
            return [xExtent[0] - xPadding, xExtent[1] + xPadding]
          })())
254
255
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
Sascha Herzinger's avatar
Sascha Herzinger committed
256
257
258
259
260
          .domain((() => {
            const yExtent = d3.extent(this.shownPoints.ys)
            const yPadding = (yExtent[1] - yExtent[0]) / 10
            return [yExtent[0] - yPadding, yExtent[1] + yPadding]
          })())
261
262
263
264
265
266
267
268
269
270
          .range([this.padded.height, 0])
        return { x, y }
      },
      axis () {
        const x1 = d3.axisTop(this.scales.x)
        const y1 = d3.axisRight(this.scales.y)
        const x2 = d3.axisBottom(this.scales.x)
          .tickSizeInner(this.padded.height - 23)
          .tickFormat('')
        const y2 = d3.axisLeft(this.scales.y)
Sascha Herzinger's avatar
Sascha Herzinger committed
271
          .tickSizeInner(this.padded.width - this.yAxisTickWidth - 10)
272
273
274
          .tickFormat('')
        return { x1, x2, y1, y2 }
      },
275
      regLine () {
276
277
        const minX = d3.min(this.tmpPoints.xs)
        const maxX = d3.max(this.tmpPoints.xs)
278
        let x1 = this.scales.x(minX)
Sascha Herzinger's avatar
Sascha Herzinger committed
279
        let y1 = this.scales.y(this.tmpResults.intercept + this.tmpResults.slope * minX)
280
        let x2 = this.scales.x(maxX)
Sascha Herzinger's avatar
Sascha Herzinger committed
281
        let y2 = this.scales.y(this.tmpResults.intercept + this.tmpResults.slope * maxX)
282

283
284
        x1 = x1 < 0 ? 0 : x1
        x1 = x1 > this.width ? this.width : x1
285

286
287
        x2 = x2 < 0 ? 0 : x2
        x2 = x2 > this.width ? this.width : x2
288

289
290
        y1 = y1 < 0 ? 0 : y1
        y1 = y1 > this.height ? this.height : y1
291

292
293
        y2 = y2 < 0 ? 0 : y2
        y2 = y2 > this.height ? this.height : y2
294

295
296
297
298
299
300
        const tooltip = `
<div>
  <p>Slope: ${this.tmpResults.slope}</p>
  <p>Intercept: ${this.tmpResults.intercept}</p>
</div>
`
Sascha Herzinger's avatar
Sascha Herzinger committed
301
        return { x1, x2, y1, y2, tooltip }
302
303
304
305
      },
      brush () {
        return d3.brush()
          .extent([[0, 0], [this.padded.width, this.padded.height]])
306
          .on('end', () => {
307
            this.error = ''
308
            if (!d3.event.selection) {
309
              this.selectedPoints = []
310
            } else {
311
312
313
314
315
316
317
318
              const [[x0, y0], [x1, y1]] = d3.event.selection
              this.selectedPoints = this.shownPoints.all.filter(d => {
                const x = this.scales.x(d.x)
                const y = this.scales.y(d.y)
                return x0 <= x && x <= x1 && y0 <= y && y <= y1
              })
              if (this.selectedPoints.length > 0 && this.selectedPoints.length < 3) {
                this.error = 'Selection must be zero (everything is selected) or greater than two.'
Sascha Herzinger's avatar
Sascha Herzinger committed
319
                return
320
              }
321
            }
Sascha Herzinger's avatar
Sascha Herzinger committed
322
            store.dispatch('setFilter', {filter: 'ids', value: this.selectedPoints.map(d => d.id)})
323
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
324
325
      },
      histograms () {
326
        const BINS = 14
327
328
        let xBins = []
        let yBins = []
329
330
331
332
333
        const [xMin, xMax] = d3.extent(this.tmpPoints.xs)
        const [yMin, yMax] = d3.extent(this.tmpPoints.ys)
        const xThresholds = d3.range(xMin, xMax, (xMax - xMin) / BINS)
        const yThresholds = d3.range(yMin, yMax, (yMax - yMin) / BINS)
        if (this.tmpPoints.xs.length) {
334
          xBins = d3.histogram()
335
            .domain([xMin, xMax])
336
            .thresholds(xThresholds)(this.tmpPoints.xs)
337
338
        }
        if (this.tmpPoints.ys.length) {
339
          yBins = d3.histogram()
340
            .domain([yMin, yMax])
341
342
            .thresholds(yThresholds)(this.tmpPoints.ys)
        }
Sascha Herzinger's avatar
Sascha Herzinger committed
343
344
345
        return { xBins, yBins }
      },
      histogramScales () {
346
347
        const xExtent = d3.extent(this.histograms.xBins.map(d => d.length))
        const yExtent = d3.extent(this.histograms.yBins.map(d => d.length))
348
        // no, I didn't mix up x and y.
Sascha Herzinger's avatar
Sascha Herzinger committed
349
        const x = d3.scaleLinear()
350
351
          .domain(yExtent)
          .range([yExtent[0] ? 10 : 0, this.margin.left])
Sascha Herzinger's avatar
Sascha Herzinger committed
352
        const y = d3.scaleLinear()
353
354
          .domain(xExtent)
          .range([xExtent[0] ? 10 : 0, this.margin.bottom])
Sascha Herzinger's avatar
Sascha Herzinger committed
355
356
        return { x, y }
      },
Sascha Herzinger's avatar
Sascha Herzinger committed
357
358
      histPolyPoints () {
        const bottom = this.histograms.xBins.map(d => {
Sascha Herzinger's avatar
Sascha Herzinger committed
359
360
361
362
          return `${this.scales.x(d.x0)},${this.padded.height + 1}, ` +
            `${this.scales.x(d.x0)},${this.padded.height + this.histogramScales.y(d.length) + 1} ` +
            `${this.scales.x(d.x1)},${this.padded.height + this.histogramScales.y(d.length) + 1} ` +
            `${this.scales.x(d.x1)},${this.padded.height + 1}`
Sascha Herzinger's avatar
Sascha Herzinger committed
363
364
365
366
367
368
369
370
        }).join(' ')
        const left = this.histograms.yBins.map(d => {
          return `${0},${this.scales.y(d.x0)} ` +
            `${-this.histogramScales.x(d.length)},${this.scales.y(d.x0)} ` +
            `${-this.histogramScales.x(d.length)},${this.scales.y(d.x1)} ` +
            `${0},${this.scales.y(d.x1)}`
        }).join(' ')
        return { bottom, left }
371
      }
372
    },
373
374
375
    // 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.
376
    watch: {
Sascha Herzinger's avatar
Sascha Herzinger committed
377
378
379
380
381
382
383
      'regLine': {
        handler: function (newRegLine) {
          TweenLite.to(this.tweened.regLine, 0.5, newRegLine)
        }
      },
      'histPolyPoints': {
        handler: function (newPoints) {
384
385
          this.$nextTick(() => {
            // we use d3 instead of TweenLite here because d3 can transition point paths
Sascha Herzinger's avatar
Sascha Herzinger committed
386
            d3.select(this.$el.querySelector('.fjs-histogram-polyline.fjs-bottom'))
387
388
389
              .transition()
              .duration(500)
              .attr('points', newPoints.bottom)
Sascha Herzinger's avatar
Sascha Herzinger committed
390
            d3.select(this.$el.querySelector('.fjs-histogram-polyline.fjs-left'))
391
392
393
394
              .transition()
              .duration(500)
              .attr('points', newPoints.left)
          })
Sascha Herzinger's avatar
Sascha Herzinger committed
395
396
        }
      },
397
398
      'args': {
        handler: function (newArgs, oldArgs) {
Sascha Herzinger's avatar
Sascha Herzinger committed
399
          // if our data selection change we will want to re-initialize the current view
400
401
          const init = newArgs.x !== oldArgs.x ||
            newArgs.y !== oldArgs.y ||
402
            JSON.stringify(newArgs.categories) !== JSON.stringify(oldArgs.categories)
Sascha Herzinger's avatar
Sascha Herzinger committed
403
404
          const args = this.args
          args.id_filter = init ? [] : args.id_filter
Sascha Herzinger's avatar
Sascha Herzinger committed
405
          if (this.validArgs) {
Sascha Herzinger's avatar
Sascha Herzinger committed
406
            this.runAnalysisWrapper({init, args})
407
408
409
          }
        }
      },
410
      'axis': {
411
        handler: function (newAxis) {
412
          this.$nextTick(() => {
Sascha Herzinger's avatar
Sascha Herzinger committed
413
414
415
416
417
            d3.select(this.$el.querySelector('.fjs-x-axis-1')).call(newAxis.x1)
            d3.select(this.$el.querySelector('.fjs-x-axis-2')).call(newAxis.x2)
            d3.select(this.$el.querySelector('.fjs-y-axis-1')).call(newAxis.y1)
            d3.select(this.$el.querySelector('.fjs-y-axis-2')).call(newAxis.y2)
            const text = this.$el.querySelector('.fjs-y-axis-1 text')
Sascha Herzinger's avatar
Sascha Herzinger committed
418
419
420
421
422
423
            if (text) {
              const width = Math.ceil(text.getBoundingClientRect().width)
              if (width !== this.yAxisTickWidth) {
                this.yAxisTickWidth = width
              }
            }
424
          })
425
        }
426
427
      },
      'brush': {
428
        handler: function (newBrush) {
429
          this.$nextTick(() => {
Sascha Herzinger's avatar
Sascha Herzinger committed
430
            d3.select(this.$el.querySelector('.fjs-brush')).call(newBrush)
431
          })
432
        }
433
      },
434
435
436
437
438
      'idFilter': {
        handler: function (newIDFilter) {
          const isFiltered = (newIDFilter.length === this.selectedPoints.length) &&
            this.selectedPoints.map(d => d.id).every(id => newIDFilter.indexOf(id) !== -1)
          if (!isFiltered) {
Sascha Herzinger's avatar
Sascha Herzinger committed
439
440
            // FIXME: This will probably not work because args is a computed property, not static data
            this.args.id_filter = newIDFilter
441
442
          }
        }
443
444
      }
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
445
    mounted () {
446
      window.addEventListener('resize', this.handleResize)
447
      this.handleResize()
448
    },
Sascha Herzinger's avatar
Sascha Herzinger committed
449
    beforeDestroy () {
450
      window.removeEventListener('resize', this.handleResize)
451
    },
452
    components: {
453
454
      ControlPanel,
      DataBox
455
    },
456
457
458
    directives: {
      tooltip
    },
459
    methods: {
460
      runAnalysisWrapper ({init, args}) {
461
        // function made available via requestHandling mixin
Sascha Herzinger's avatar
Sascha Herzinger committed
462
        runAnalysis({task_name: 'compute-correlation', args})
463
          .then(response => {
464
            const results = JSON.parse(response)
465
466
            const data = JSON.parse(results.data)
            results.data = Object.keys(data).map(key => data[key])
467
            deepFreeze(results) // massively improve performance by telling Vue that the objects properties won't change
468
            if (init) {
Sascha Herzinger's avatar
Sascha Herzinger committed
469
470
              this.shownResults = results
              this.tmpResults = results
471
            } else {
Sascha Herzinger's avatar
Sascha Herzinger committed
472
              this.tmpResults = results
473
            }
474
          })
475
476
          .catch(error => console.error(error))
      },
477
      handleResize () {
Sascha Herzinger's avatar
Sascha Herzinger committed
478
        const container = this.$el.querySelector(`.fjs-vis-container svg`)
479
        // noinspection JSSuspiciousNameCombination
480
481
        this.height = container.getBoundingClientRect().width
        this.width = container.getBoundingClientRect().width
482
483
484
485
      },
      update_xyData (ids) {
        this.xyData = ids
      },
486
487
      update_categoryData (ids) {
        this.categoryData = ids
488
      }
489
490
491
492
493
    }
  }
</script>


494
495
496
497
498
499
<style lang="sass" scoped>
  @import './src/assets/base.sass'

  *
    font-family: Roboto, sans-serif

500
  .fjs-correlation-analysis
501
502
    height: 100%
    width: 100%
503
504
    display: flex
    flex-direction: column
505
506
507
508
509
510
511
512
513
514
    .fjs-control-panel
      hr
        width: 100%
        margin: 10% 0 10% 0
    .fjs-correlation-method
      white-space: nowrap
      border: solid 1px #fff
      text-align: left
      border-radius: 8px
      margin: 1%
515

516
517
518
519
    .fjs-vis-container
      flex: 1
      display: flex
      svg
520
        flex: 4
521
522
        .fjs-lin-reg-line
          stroke: #ff5e00
Sascha Herzinger's avatar
Sascha Herzinger committed
523
          stroke-width: 0.3%
524
525
        .fjs-lin-reg-line:hover
          opacity: 0.4
Sascha Herzinger's avatar
Sascha Herzinger committed
526
        .fjs-histogram-polyline
527
          shape-rendering: crispEdges
Sascha Herzinger's avatar
Sascha Herzinger committed
528
          stroke: white
529
          stroke-width: 1px
530
531
          fill: #ffd100
        .fjs-scatterplot-point
532
          stroke-width: 1
533
534
535
536
537
        .fjs-scatterplot-point:hover
          fill: #f00
        .fjs-brush
          stroke-width: 0
      .fjs-table-container
538
        flex: 1
539
540
541
        display: flex
        flex-direction: column
        .fjs-stats-table
542
543
          width: 100%
          margin: 1%
544
545
          border-spacing: 0
          border-collapse: collapse
546
          font-size: 0.875rem
Sascha Herzinger's avatar
Sascha Herzinger committed
547
548
549
550
551
552
553
554
          tr:nth-child(even)
            background-color: #ddd
          td, th
            overflow: hidden
            text-overflow: ellipsis
            white-space: nowrap
            border: 1px #ccc solid
            border-collapse: collapse
555
            padding: 2%
556
557
558
</style>

<!--CSS for dynamically created components-->
559
560
561
562
563
<style lang="sass">
  .fjs-corr-axis
    shape-rendering: crispEdges
    .tick
      shape-rendering: crispEdges
Sascha Herzinger's avatar
Sascha Herzinger committed
564
565
566
      line
        stroke: #999
      text
567
        font-size: 0.875rem
568
</style>