Skip to content
Merged
77 changes: 77 additions & 0 deletions src/components/CalibrationTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<template>
<table v-if="evidenceStrengthsForDisplay" class="mave-calibrations-table">
<tr>
<th>Evidence Strength</th>
<th v-for="evidenceStrength of evidenceStrengthsForDisplay" :key="evidenceStrength"><b>{{ evidenceStrength }}</b></th>
</tr>
<tr>
<td>Likelihood Ratio+</td>
<td v-for="likelihoodRatio of positiveLikelihoodRatiosForDisplay" :key="likelihoodRatio">{{ likelihoodRatio }}</td>
</tr>
<tr>
<td>Score Threshold</td>
<td v-for="threshold of thresholdsForDisplay" :key="threshold">{{ threshold }}</td>
</tr>
</table>
</template>

<script lang="ts">
import {defineComponent} from 'vue'
import { Calibrations } from '@/lib/calibrations';

export default defineComponent({
name: 'CalibrationTable',
components: { },

props: {
calibrations: {
type: Object as () => {[key: string]: Calibrations},
required: true
},

selectedCalibration: {
type: String,
required: true
}
},

computed: {
selectedCalibrations: function () {
return this.calibrations[this.selectedCalibration]
},

evidenceStrengthsForDisplay: function() {
return this.selectedCalibrations.evidenceStrengths
},

positiveLikelihoodRatiosForDisplay: function() {
return this.selectedCalibrations.positiveLikelihoodRatios
},

thresholdsForDisplay: function() {
return this.selectedCalibrations.thresholds
},
},

methods: {
titleCase(s: string) {
return s.replace (/^[-_]*(.)/, (_, c) => c.toUpperCase()) // Initial char (after -/_)
.replace (/[-_]+(.)/g, (_, c) => ' ' + c.toUpperCase()) // First char after each -/_
}
}
})
</script>

<style>
table.mave-calibrations-table {
border-collapse: collapse;
margin: 1em auto 0.5em auto;
}

table.mave-calibrations-table td,
table.mave-calibrations-table th {
border: 1px solid gray;
padding: 0.5em 1em;
text-align: center;
}
</style>
182 changes: 125 additions & 57 deletions src/components/ScoreSetHistogram.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
<template>
<TabMenu class="mave-histogram-viz-select" v-if="hasTabBar" v-model:activeIndex="activeViz" :model="vizOptions" />
<div v-if="hasTabBar && showShaders" class="mave-histogram-controls">
<div class="flex flex-wrap gap-3">
Shader Options:
<div class="flex align-items-center gap-1">
<RadioButton v-model="activeShader" inputId="unsetShader" name="shader" value="null" />
<label for="unsetShader">No Shader</label>
</div>
<div class="flex items-center gap-2" v-if="scoreSet.scoreRanges">
<RadioButton v-model="activeShader" inputId="rangeShader" name="shader" value="range" />
<label for="rangeShader">Score Ranges</label>
</div>
<div class="flex items-center gap-2" v-if="scoreSet.scoreCalibrations">
<RadioButton v-model="activeShader" inputId="calibrationShader" name="shader" value="threshold" />
<label for="calibrationShader">Calibration Thresholds</label>
</div>
</div>
</div>
<div v-if="showControls" class="mave-histogram-controls">
<div class="mave-histogram-control">
<label for="mave-histogram-star-select" class="mave-histogram-control-label">Minimum ClinVar review status 'gold stars': </label>
Expand All @@ -15,15 +32,21 @@
</div>
</div>
</div>
<div v-if="showCalibrations" class="mave-histogram-controls">
<Dropdown v-model="activeCalibrationKey" :options="Object.keys(scoreSet.scoreCalibrations)" :optionLabel="titleCase" style="width: 100%;" />
</div>
<div class="mave-histogram-container" ref="histogramContainer" />
<div v-if="showCalibrations && activeCalibrationKey">
<CalibrationTable :calibrations="scoreSet.scoreCalibrations" :selected-calibration="activeCalibrationKey"></CalibrationTable>
</div>
</template>

<script lang="ts">
import * as d3 from 'd3'
import { variantNotNullOrNA } from '@/lib/mave-hgvs'
import { saveChartAsFile } from '@/lib/chart-export'
import _ from 'lodash'
import Checkbox from 'primevue/checkbox'
import Dropdown from 'primevue/dropdown'
import RadioButton from 'primevue/radiobutton'
import Rating from 'primevue/rating'
import TabMenu from 'primevue/tabmenu'
import {defineComponent} from 'vue'
Expand All @@ -35,7 +58,10 @@ import {
CLINVAR_REVIEW_STATUS_STARS,
PATHOGENIC_CLINICAL_SIGNIFICANCE_CLASSIFICATIONS
} from '@/lib/clinvar'
import makeHistogram, {DEFAULT_SERIES_COLOR, Histogram, HistogramSerieOptions} from '@/lib/histogram'
import makeHistogram, {DEFAULT_SERIES_COLOR, Histogram, HistogramSerieOptions, HistogramDatum} from '@/lib/histogram'
import CalibrationTable from '@/components/CalibrationTable.vue'
import { prepareThresholdsForHistogram } from '@/lib/calibrations'
import { prepareRangesForHistogram } from '@/lib/ranges'

const CLNSIG_FIELD = 'mavedb_clnsig'
const CLNREVSTAT_FIELD = 'mavedb_clnrevstat'
Expand All @@ -52,7 +78,7 @@ const DEFAULT_MIN_STAR_RATING = 1

export default defineComponent({
name: 'ScoreSetHistogram',
components: { Checkbox, Rating, TabMenu, },
components: { Checkbox, Dropdown, RadioButton, Rating, TabMenu, CalibrationTable },

emits: ['exportChart'],

Expand Down Expand Up @@ -98,20 +124,28 @@ export default defineComponent({
}
},

data: () => ({
config: config,
data: function() {
const scoreSetHasRanges = config.CLINICAL_FEATURES_ENABLED && this.scoreSet.scoreRanges != null
const selectedCalibrationKey = config.CLINICAL_FEATURES_ENABLED && this.scoreSet.scoreCalibrations ? Object.keys(this.scoreSet.scoreCalibrations)[0] : null

activeViz: 0,
clinicalSignificanceClassificationOptions: CLINVAR_CLINICAL_SIGNIFICANCE_CLASSIFICATIONS,
customMinStarRating: DEFAULT_MIN_STAR_RATING,
customSelectedClinicalSignificanceClassifications: DEFAULT_CLINICAL_SIGNIFICANCE_CLASSIFICATIONS,
histogram: null as Histogram | null
}),
return {
config: config,

activeViz: 0,
activeShader: scoreSetHasRanges ? 'range' : 'null',
activeCalibrationKey: selectedCalibrationKey,

clinicalSignificanceClassificationOptions: CLINVAR_CLINICAL_SIGNIFICANCE_CLASSIFICATIONS,
customMinStarRating: DEFAULT_MIN_STAR_RATING,
customSelectedClinicalSignificanceClassifications: DEFAULT_CLINICAL_SIGNIFICANCE_CLASSIFICATIONS,
histogram: null as Histogram | null,
}
},

computed: {
series: function() {
switch (this.activeViz) {
case 1: // Clinical view
switch (this.vizOptions[this.activeViz].view) {
case 'clinical':

return [{
classifier: (d) =>
Expand All @@ -131,7 +165,7 @@ export default defineComponent({
}
}]

case 2: // Custom
case 'custom':
{
const series = [{
classifier: (d) =>
Expand Down Expand Up @@ -179,19 +213,43 @@ export default defineComponent({
return series
}

case 0: // Overall score distribution
default:
case 'calibrations':
return null

default: // Overall score distribution
return null
}
},

vizOptions: function() {
const options = [{label: 'Overall Distribution'}]
if (this.variants.some((v) => v[CLNSIG_FIELD] !== undefined)) {
options.push({label: 'Clinical View'})
options.push({label: 'Custom'})
const options = [{label: 'Overall Distribution', view: 'distribution'}]
const someVariantsHaveClinicalSignificance = this.variants.some((v) => v[CLNSIG_FIELD] !== undefined)

if (someVariantsHaveClinicalSignificance) {
options.push({label: 'Clinical View', view: 'clinical'})
}
if (this.scoreSet.scoreCalibrations) {
options.push({label: 'Calibrations', view: 'calibrations'})
}

// custom view should always come last
if (someVariantsHaveClinicalSignificance) {
options.push({label: 'Custom', view: 'custom'})
}
return options
},

shaderOptions: function() {
const options = [{label: 'Hide Shaders', view: 'null'}]

if (this.scoreSet.scoreRanges) {
options.push({label: 'Score Ranges', view: 'range'})
}

if (this.scoreSet.scoreCalibrations) {
options.push({label: 'Calibration Thresholds', view: 'calibration'})
}

return options
},

Expand All @@ -200,7 +258,19 @@ export default defineComponent({
},

showControls: function() {
return this.activeViz == 2
return this.activeViz == this.vizOptions.findIndex(item => item.view === 'custom')
},

showCalibrations: function() {
return this.activeViz == this.vizOptions.findIndex(item => item.view === 'calibrations')
},

showShaders: function() {
return this.shaderOptions.length > 1
},

activeCalibration: function() {
return this.activeCalibrationKey ? this.scoreSet.scoreCalibrations[this.activeCalibrationKey] : null
},

minStarRating: function() {
Expand Down Expand Up @@ -304,14 +374,24 @@ export default defineComponent({
watch: {
variants: {
handler: function() {
renderOrRefreshHistogram()
this.renderOrRefreshHistogram()
}
},
series: {
handler: function() {
this.renderOrRefreshHistogram()
}
},
activeCalibration: {
handler: function() {
this.renderOrRefreshHistogram()
}
},
activeShader: {
handler: function() {
this.renderOrRefreshHistogram()
}
},
externalSelection: {
handler: function(newValue) {
if (this.histogram) {
Expand All @@ -332,7 +412,7 @@ export default defineComponent({
handler: function() {
this.renderOrRefreshHistogram()
}
}
},
},

mount: function() {
Expand All @@ -354,59 +434,47 @@ export default defineComponent({
.valueField((variant) => variant.score)
.tooltipHtml(this.tooltipHtmlGetter)
}

let seriesClassifier: ((d: HistogramDatum) => number[]) | null = null
if (this.series) {
const seriesIndices = _.range(0, this.series.length)
seriesClassifier = (d: HistogramDatum) =>
seriesIndices.filter((seriesIndex) => this.series[seriesIndex].classifier(d))
}

let ranges = []
if (this.config.CLINICAL_FEATURES_ENABLED) {
switch (this.scoreSet.urn) {
case 'urn:mavedb:00000097-0-1':
ranges = [{
min: -0.748,
max: 1.307,
color: '#4444ff',
title: 'Functionally normal'
}, {
min: -5.651,
max: -1.328,
color: '#ff4444',
title: 'Functionally abnormal'
}]
break
case 'urn:mavedb:00000050-a-1':
ranges = [{
min: -7.57,
max: 0,
color: '#4444ff',
title: 'Functionally normal'
}, {
min: 0,
max: 5.39,
color: '#ff4444',
title: 'Functionally abnormal'
}]
break
}
const histogramShaders = {}
if (this.scoreSet.scoreRanges) {
histogramShaders["range"] = prepareRangesForHistogram(this.scoreSet.scoreRanges)
}
if (this.activeCalibration) {
histogramShaders["threshold"] = prepareThresholdsForHistogram(this.activeCalibration)
}

this.histogram.data(this.variants)
.seriesOptions(this.series?.map((s) => s.options) || null)
.seriesClassifier(seriesClassifier)
.title(this.hasTabBar ? null : 'Distribution of Functional Scores')
.legendNote(this.activeViz == 0 ? null : 'ClinVar data from time of publication')
.ranges(ranges)
.refresh()
.shaders(histogramShaders)

if (this.externalSelection) {
this.histogram.selectDatum(this.externalSelection)
} else {
this.histogram.clearSelection()
}
}

// Only render clinical specific viz options if such features are enabled.
this.histogram.renderShader(null)
if (this.config.CLINICAL_FEATURES_ENABLED) {
this.histogram.renderShader(this.activeShader)
}

this.histogram.refresh()
},

titleCase: (s) =>
s.replace (/^[-_]*(.)/, (_, c) => c.toUpperCase()) // Initial char (after -/_)
.replace (/[-_]+(.)/g, (_, c) => ' ' + c.toUpperCase()) // First char after each -/_
}
})
</script>
Expand Down
Loading