Survivalplot.vue 11.9 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
    <chart v-on:resize="resize">
        <control-panel class="fjs-control-panel" name="Survivalplot Panel">
            <data-box class="fjs-data-box"
                      header="Duration [numerical]"
                      :data-types="['numerical']"
                      v-on:update="updateDurationVariable">
            </data-box>
            <data-box class="fjs-data-box"
                      header="Groups (optional) [categorical]"
                      :data-types="['categorical']"
                      v-on:update="updateGroupVariable">
            </data-box>
            <data-box class="fjs-data-box"
                      header="Observed (optional) [categorical]"
                      :data-types="['categorical']"
                      v-on:update="updateObservedVariable">
            </data-box>
Sascha Herzinger's avatar
Sascha Herzinger committed
19
            <hr class="fjs-seperator"/>
Sascha Herzinger's avatar
Sascha Herzinger committed
20
21
22
23
24
25
26
27
28
29
30
            <div class="fjs-settings">
                <fieldset class="fjs-fieldset">
                    <legend>Estimator</legend>
                    <div v-for="method in estimators">
                        <label>
                            <input type="radio" :value="method" v-model="estimator">
                            {{ method }}
                        </label>
                    </div>
                </fieldset>
                <div>
Sascha Herzinger's avatar
Sascha Herzinger committed
31
                    <label>
Sascha Herzinger's avatar
Sascha Herzinger committed
32
33
                        Ignore Subsets
                        <input type="checkbox" v-model="ignoreSubsets"/>
Sascha Herzinger's avatar
Sascha Herzinger committed
34
35
36
                    </label>
                </div>
            </div>
Sascha Herzinger's avatar
Sascha Herzinger committed
37

38
39
40
        </control-panel>
        <svg :height="height" :width="width">
            <g :transform="`translate(${margin.left}, ${margin.top})`">
41
                <rect :width="padded.width" :height="padded.height" style="opacity: 0"></rect>
Sascha Herzinger's avatar
Sascha Herzinger committed
42
43
44
45
46
47
48
49
50
51
52
53
54
55
                <html2svg :right="padded.width">
                    <draggable>
                        <div class="fjs-legend">
                            <div class="fjs-legend-element" v-for="group in groups">
                                <svg width="1vw" height="1vw">
                                    <rect width="1vw" height="1vw" :fill="group.color"></rect>
                                </svg>

                                <span>{{ group.name }}</span>
                            </div>
                        </div>
                    </draggable>
                </html2svg>
                <crosshair :width="padded.width" :height="padded.height"/>
56
57
58
                <g class="fjs-axis" ref="yAxis2" :transform="`translate(${padded.width}, 0)`"></g>
                <g class="fjs-axis" ref="xAxis2"></g>
                <g class="fjs-axis" ref="xAxis1" :transform="`translate(0, ${padded.height})`"></g>
Sascha Herzinger's avatar
Sascha Herzinger committed
59
                <g class="fjs-axis" ref="yAxis1"></g>
Sascha Herzinger's avatar
Sascha Herzinger committed
60
61
62
63
                <text :transform="`translate(${padded.width / 2}, ${padded.height + margin.bottom * 0.90})`"
                      text-anchor="middle">
                    {{ results.label }}
                </text>
64
65
66
67
68
69
70
71
72
73
                <g class="fjs-paths">
                    <path class="fjs-estimate-path"
                          :style="{stroke: path.color}"
                          :d="path.d" v-for="path in paths">
                    </path>
                    <path class="fjs-ci-path"
                          :style="{fill: path.color}"
                          :d="path.d" v-for="path in ciPaths">
                    </path>
                </g>
74
75
76
77
78
79
80
81
82
83
84
85
86
87
            </g>
        </svg>
    </chart>
</template>

<script>
  import ControlPanel from '../components/ControlPanel.vue'
  import Chart from '../components/Chart.vue'
  import DataBox from '../components/DataBox.vue'
  import RunAnalysis from '../mixins/run-analysis'
  import store from '../../store/store'
  import deepFreeze from 'deep-freeze-strict'
  import * as d3 from 'd3'
  import Crosshair from '../components/Crosshair.vue'
Sascha Herzinger's avatar
Sascha Herzinger committed
88
89
  import Html2svg from '../components/HTML2SVG.vue'
  import Draggable from '../components/Draggable.vue'
90
91
  export default {
    name: 'survivalplot',
Sascha Herzinger's avatar
Sascha Herzinger committed
92
    components: {Draggable, Html2svg, Crosshair, DataBox, Chart, ControlPanel},
93
94
95
96
97
98
99
100
101
    mixins: [RunAnalysis],
    data () {
      return {
        height: 0,
        width: 0,
        durationVariables: [],
        groupVariables: [],
        observedVariables: [],
        estimator: 'KaplanMeier',
Sascha Herzinger's avatar
Sascha Herzinger committed
102
103
        estimators: ['KaplanMeier', 'NelsonAalen'],
        ignoreSubsets: false,
104
105
106
107
108
109
        groupColors: d3.schemeCategory10,
        results: {
          subsets: [],
          categories: [],
          stats: {}
        }
110
111
112
113
114
115
116
117
118
      }
    },
    computed: {
      args () {
        return {
          durations: this.durationVariables,
          categories: this.groupVariables,
          event_observed: this.observedVariables,
          estimator: this.estimator,
119
          id_filter: store.getters.filter('ids').value,
Sascha Herzinger's avatar
Sascha Herzinger committed
120
          subsets: this.ignoreSubsets ? [] : store.getters.subsets
121
122
123
        }
      },
      validArgs () {
124
        return this.durationVariables.length === 1
125
126
127
128
129
130
131
132
133
134
135
136
      },
      margin () {
        const left = this.width / 15
        const top = this.height / 15
        const right = this.width / 15
        const bottom = this.height / 15
        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}
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
      },
      dataRanges () {
        let timelineGlobalMin = Number.MAX_SAFE_INTEGER
        let timelineGlobalMax = Number.MIN_SAFE_INTEGER
        let estimateGlobalMin = Number.MAX_SAFE_INTEGER
        let estimateGlobalMax = Number.MIN_SAFE_INTEGER
        this.results.categories.forEach(category => {
          this.results.subsets.forEach(subset => {
            const [localTimelineMin, localTimelineMax] = d3.extent(this.results.stats[category][subset].timeline)
            timelineGlobalMin = localTimelineMin < timelineGlobalMin ? localTimelineMin : timelineGlobalMin
            timelineGlobalMax = localTimelineMax > timelineGlobalMax ? localTimelineMax : timelineGlobalMax
            const [localEstimateMin, localEstimateMax] = d3.extent(this.results.stats[category][subset].estimate)
            estimateGlobalMin = localEstimateMin < estimateGlobalMin ? localEstimateMin : estimateGlobalMin
            estimateGlobalMax = localEstimateMax > estimateGlobalMax ? localEstimateMax : estimateGlobalMax
          })
        })
        return { timelineGlobalMin, timelineGlobalMax, estimateGlobalMin, estimateGlobalMax }
      },
      scales () {
        const x = d3.scaleLinear()
          .domain([this.dataRanges.timelineGlobalMin, this.dataRanges.timelineGlobalMax])
          .range([0, this.padded.width])
        const y = d3.scaleLinear()
          .domain([this.dataRanges.estimateGlobalMin, this.dataRanges.estimateGlobalMax])
          .range([this.padded.height, 0])
        return { x, y }
      },
      axis () {
Sascha Herzinger's avatar
Sascha Herzinger committed
165
166
        const x1 = d3.axisBottom(this.scales.x)
        const y1 = d3.axisLeft(this.scales.y)
167
168
169
170
171
172
173
174
175
        const x2 = d3.axisBottom(this.scales.x)
          .tickSizeInner(this.padded.height)
          .tickFormat('')
        const y2 = d3.axisLeft(this.scales.y)
          .tickSizeInner(this.padded.width)
          .tickFormat('')
        return { x1, x2, y1, y2 }
      },
      groups () {
Sascha Herzinger's avatar
Sascha Herzinger committed
176
        const groups = []
177
178
        this.results.categories.forEach(category => {
          this.results.subsets.forEach(subset => {
Sascha Herzinger's avatar
Sascha Herzinger committed
179
            groups.push({name: this.getGroupName(category, subset)})
180
181
          })
        })
Sascha Herzinger's avatar
Sascha Herzinger committed
182
183
        groups.forEach((group, i) => {
          group.color = this.groupColors[i % this.groupColors.length]
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
        })
        return groups
      },
      paths () {
        const paths = []
        this.results.categories.forEach(category => {
          this.results.subsets.forEach(subset => {
            let path = ''
            this.results.stats[category][subset].estimate.forEach((d, i, arr) => {
              const stats = this.results.stats[category][subset]
              const x = this.scales.x(stats.timeline[i])
              if (i === 0) {
                path += `M ${x} ${this.scales.y(d)}`
              } else {
                path += `L ${x} ${this.scales.y(arr[i - 1])}`
                path += `L ${x} ${this.scales.y(d)}`
              }
            })
            paths.push({
              d: path,
Sascha Herzinger's avatar
Sascha Herzinger committed
204
              color: this.groups.find(group => group.name === this.getGroupName(category, subset)).color
205
206
207
208
209
210
211
212
213
            })
          })
        })
        return paths
      },
      ciPaths () {
        const paths = []
        this.results.categories.forEach(category => {
          this.results.subsets.forEach(subset => {
Sascha Herzinger's avatar
Sascha Herzinger committed
214
            const stats = this.results.stats[category][subset]
215
216
            let path = ''
            let backpath = ' Z '
Sascha Herzinger's avatar
Sascha Herzinger committed
217
            this.results.stats[category][subset].ci_upper.forEach((_, i) => {
218
219
220
221
              const x = this.scales.x(stats.timeline[i])
              if (i === 0) {
                return true
              } else if (i === 1) {
Sascha Herzinger's avatar
Sascha Herzinger committed
222
223
                path += `M ${x} ${this.scales.y(stats.ci_upper[i])} `
                backpath = ` L ${x} ${this.scales.y(stats.ci_lower[i])}` + backpath
224
              } else {
Sascha Herzinger's avatar
Sascha Herzinger committed
225
226
                path += `L ${x} ${this.scales.y(stats.ci_upper[i - 1])} `
                path += `L ${x} ${this.scales.y(stats.ci_upper[i])} `
227
228
229
230
231
232
                backpath = ` L ${x} ${this.scales.y(stats.ci_lower[i - 1])}` + backpath
                backpath = ` L ${x} ${this.scales.y(stats.ci_lower[i])}` + backpath
              }
            })
            paths.push({
              d: path + backpath,
Sascha Herzinger's avatar
Sascha Herzinger committed
233
              color: this.groups.find(group => group.name === this.getGroupName(category, subset)).color
234
235
236
237
            })
          })
        })
        return paths
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
      }
    },
    methods: {
      updateDurationVariable (ids) {
        this.durationVariables = ids
      },
      updateGroupVariable (ids) {
        this.groupVariables = ids
      },
      updateObservedVariable (ids) {
        this.observedVariables = ids
      },
      resize (width, height) {
        this.width = width
        this.height = height
      },
      runAnalysisWrapper (args) {
        this.runAnalysis('survival-analysis', args)
          .then(response => {
            const results = JSON.parse(response)
            deepFreeze(results) // massively improve performance by telling Vue that the objects properties won't change
            this.results = results
          })
          .catch(error => console.error(error))
262
263
      },
      getGroupName (category, subset) {
Sascha Herzinger's avatar
Sascha Herzinger committed
264
        return `${this.results.label} [${category}] [s${subset + 1}]`
265
266
267
268
269
270
271
272
273
      }
    },
    watch: {
      'args': {
        handler: function (newArgs) {
          if (this.validArgs) {
            this.runAnalysisWrapper(newArgs)
          }
        }
274
275
276
277
278
279
280
281
282
283
      },
      'axis': {
        handler: function (newAxis) {
          this.$nextTick(() => {
            d3.select(this.$refs.xAxis1).call(newAxis.x1)
            d3.select(this.$refs.yAxis1).call(newAxis.y1)
            d3.select(this.$refs.yAxis2).call(newAxis.y2)
            d3.select(this.$refs.xAxis2).call(newAxis.x2)
          })
        }
284
285
286
287
288
289
290
291
      }
    }
  }
</script>

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

Sascha Herzinger's avatar
Sascha Herzinger committed
292
293
294
295
296
297
298
299
300
301
302
    .fjs-control-panel
        .fjs-settings > *
            margin-bottom: 1vh

    .fjs-legend
        .fjs-legend-element
            > svg
                margin-right: 0.5vw
            display: flex
            align-items: center

303
304
305
306
307
308
309
310
311
    svg
        .fjs-paths
            path
                shape-rendering: crispEdges
            .fjs-estimate-path
                stroke-width: 2px
                fill: none
            .fjs-ci-path
                opacity: 0.4
312
313
314
315
</style>

<!--CSS for dynamically created components-->
<style lang="sass">
316
    @import '~assets/d3.sass'
317

318
319
320
321
322
323
324
325
326
327
    .fjs-axis
        shape-rendering: crispEdges
        .tick
            shape-rendering: crispEdges
            line
                stroke: #E2E2E2
            text
                font-size: 0.75em
        path
            stroke: none
328
</style>