Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions src/components/AssayFactSheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,48 +77,63 @@
</div>
</div>
</div>
<div class="mavedb-assay-facts-section-title">Clinical Performance</div>
<div class="mavedb-assay-facts-section-title">
Clinical Performance<sup v-if="!primaryScoreRangeIsInvestigatorProvided">*</sup>
</div>
<div class="mavedb-assay-facts-section">
<div v-if="scoreSet.scoreRanges?.investigatorProvided?.ranges[0]?.oddsPath?.ratio">
<div v-if="primaryScoreRange?.ranges.some((r) => r.oddsPath)">
<div class="mavedb-assay-facts-row">
<div class="mavedb-assay-facts-label">OddsPath – Normal</div>
<div
v-if="scoreSet.scoreRanges?.investigatorProvided?.ranges?.some((r) => r.classification === 'normal')"
v-if="primaryScoreRange?.ranges?.some((r) => r.classification === 'normal' && r.oddsPath)"
class="mavedb-assay-facts-value"
>
{{
roundOddsPath(
scoreSet.scoreRanges.investigatorProvided.ranges.find((r) => r.classification === 'normal').oddsPath
primaryScoreRange?.ranges.find((r) => r.classification === 'normal').oddsPath
?.ratio
)
}}
<span class="mavedb-classification-badge mavedb-blue">
{{
scoreSet.scoreRanges.investigatorProvided.ranges.find((r) => r.classification === 'normal').oddsPath
primaryScoreRange?.ranges.find((r) => r.classification === 'normal').oddsPath
?.evidence
}}
</span>
</div>
<div v-else class="mavedb-assay-facts-value">OddsPath normal not provided</div>
</div>
<div class="mavedb-assay-facts-row">
<div class="mavedb-assay-facts-label">OddsPath – Abnormal</div>
<div
v-if="scoreSet.scoreRanges?.investigatorProvided?.ranges?.some((r) => r.classification === 'abnormal')"
v-if="primaryScoreRange?.ranges?.some((r) => r.classification === 'abnormal' && r.oddsPath)"
class="mavedb-assay-facts-value"
>
{{
roundOddsPath(
scoreSet.scoreRanges.investigatorProvided.ranges.find((r) => r.classification === 'abnormal').oddsPath
primaryScoreRange?.ranges.find((r) => r.classification === 'abnormal').oddsPath
?.ratio
)
}}
<span class="mavedb-classification-badge mavedb-red strong">
{{
scoreSet.scoreRanges.investigatorProvided.ranges.find((r) => r.classification === 'abnormal').oddsPath
primaryScoreRange?.ranges.find((r) => r.classification === 'abnormal').oddsPath
?.evidence
}}
</span>
</div>
<div v-else class="mavedb-assay-facts-value">OddsPath abnormal not provided</div>
</div>
<div v-if="!primaryScoreRangeIsInvestigatorProvided" style="font-size: 10px; margin-top: 4px">
<sup>*</sup>OddsPath data from non-primary source(s):
<template v-if="oddsPathSources">
(
<template v-for="(s,i) in oddsPathSources" :key="s.url">
<a :href="s.url" rel="noopener" target="_blank">{{ s.url }}</a><span v-if="i < oddsPathSources.length - 1">, </span>
</template>
).
</template>
<template v-else>.</template>
</div>
</div>
<div v-else>OddsPath values are not provided for this score set.</div>
Expand All @@ -130,7 +145,7 @@
import _ from 'lodash'
import {defineComponent, PropType} from 'vue'

import {getScoreSetFirstAuthor} from '@/lib/score-sets'
import {getScoreSetFirstAuthor, matchSources} from '@/lib/score-sets'
import type {components} from '@/schema/openapi'

export default defineComponent({
Expand Down Expand Up @@ -204,13 +219,38 @@ export default defineComponent({
default:
return null
}
},

primaryScoreRange: function () {
if (this.scoreSet.scoreRanges == null) {
return null
}

return Object.values(this.scoreSet.scoreRanges).filter(
(sr) => sr?.primary
)[0] || this.scoreSet.scoreRanges?.investigatorProvided || null
},

primaryScoreRangeIsInvestigatorProvided: function () {
if (this.scoreSet.scoreRanges == null) {
return false
}

return this.primaryScoreRange === this.scoreSet.scoreRanges?.investigatorProvided
},
oddsPathSources() {
console.log(this.primaryScoreRange)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log(this.primaryScoreRange)

return matchSources(this.primaryScoreRange?.oddsPathSource, this.sources)
},
sources: function () {
return this.scoreSet.primaryPublicationIdentifiers.concat(this.scoreSet.secondaryPublicationIdentifiers)
}
},

methods: {
roundOddsPath: function (oddsPath: number | undefined) {
return oddsPath?.toPrecision(5)
}
},
}
})
</script>
Expand Down
21 changes: 7 additions & 14 deletions src/components/RangeTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ import Button from 'primevue/button'
import {defineComponent, PropType} from 'vue'

import {EVIDENCE_STRENGTHS_REVERSED, ScoreRanges, ScoreRange} from '@/lib/ranges'
import {matchSources} from '@/lib/score-sets'
import {components} from '@/schema/openapi'

type PublicationIdentifiers = components['schemas']['ScoreSet']['primaryPublicationIdentifiers'][0]

export default defineComponent({
name: 'RangeTable',
Expand All @@ -152,7 +156,7 @@ export default defineComponent({
required: true
},
sources: {
type: Array as PropType<{dbName: string; identifier: string; url: string}[]>,
type: Array as PropType<PublicationIdentifiers[]>,
required: false,
default: () => []
}
Expand Down Expand Up @@ -198,10 +202,10 @@ export default defineComponent({
return [...this.scoreRanges.ranges].sort(this.compareScoreRanges)
},
thresholdSources() {
return this.matchSources(this.scoreRanges.source)
return matchSources(this.scoreRanges.source, this.sources)
},
oddsPathSources() {
return this.matchSources(this.scoreRanges.oddsPathSource)
return matchSources(this.scoreRanges.oddsPathSource, this.sources)
}
},

Expand Down Expand Up @@ -238,17 +242,6 @@ export default defineComponent({
.replace(/[-_]+(.)/g, (_, c) => ' ' + c.toUpperCase())
.replace(/([a-z])([A-Z])/g, '$1 $2')
},
matchSources(
sourceArr: Array<{dbName: string; identifier: string}> | undefined
): {dbName: string; identifier: string; url: string}[] | null {
if (!Array.isArray(sourceArr) || !this.sources) return null
const matchedSources = []
for (const source of sourceArr) {
const match = this.sources.find((s) => s.dbName === source.dbName && s.identifier === source.identifier)
if (match) matchedSources.push(match)
}
return matchedSources.length > 0 ? matchedSources : null
},
roundOddsPath(rangeBound: number) {
return rangeBound.toPrecision(3)
},
Expand Down
24 changes: 12 additions & 12 deletions src/components/ScoreSetHistogram.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
<div v-if="showRanges && activeRange" class="mave-range-table-container">
<Accordion collapse-icon="pi pi-minus" expand-icon="pi pi-plus">
<AccordionTab class="mave-range-table-tab" header="Score Range Details">
<RangeTable :score-ranges="activeRange" :score-ranges-name="activeRangeKey?.label" :sources="allSources" />
<RangeTable :score-ranges="activeRange" :score-ranges-name="activeRangeKey.label" :sources="allSources" />
</AccordionTab>
</Accordion>
</div>
Expand Down Expand Up @@ -251,7 +251,7 @@ export default defineComponent({

activeViz: 0,
showRanges: scoreSetHasRanges,
activeRangeKey: null as {label: string; value: string} | null,
activeRangeKey: {label: 'None', value: null} as {label: string; value: string | null},

clinicalControls: [] as ClinicalControl[],
clinicalControlOptions: [] as ClinicalControlOption[],
Expand Down Expand Up @@ -710,7 +710,7 @@ export default defineComponent({
// Line 3: Score and classification
if (variant.score) {
let binClassificationLabel = ''
if (bin && this.activeRangeKey && this.activeRange) {
if (bin && this.activeRangeKey.value && this.activeRange) {
// TODO#491: Refactor this calculation into the creation of variant objects so we may just access the property of the variant which tells us its classification.
const binClassification = this.histogramShaders[this.activeRangeKey.value]?.find(
(range: HistogramShader) => shaderOverlapsBin(range, bin)
Expand All @@ -731,7 +731,7 @@ export default defineComponent({
parts.push(`Bin range: ${bin.x0} to ${bin.x1}`)

//Line 6: Bin Classification
if (this.activeRangeKey && this.activeRange) {
if (this.activeRangeKey.value && this.activeRange) {
// TODO#491: Refactor this calculation into the creation of histogram bins so we don't need to repeat it every time we construct a tooltip.
const binClassifications = this.histogramShaders[this.activeRangeKey.value]?.filter(
(range: HistogramShader) => shaderOverlapsBin(range, bin)
Expand Down Expand Up @@ -980,7 +980,7 @@ export default defineComponent({

// Only render clinical specific viz options if such features are enabled.
if (this.config.CLINICAL_FEATURES_ENABLED && this.showRanges) {
this.histogram.renderShader(this.activeRangeKey?.value)
this.histogram.renderShader(this.activeRangeKey.value)
} else {
this.histogram.renderShader(null)
}
Expand Down Expand Up @@ -1101,18 +1101,18 @@ export default defineComponent({
},

defaultRangeKey: function () {
if (this.activeRangeKey) {
if (this.activeRangeKey.value) {
return this.activeRangeKey
}

const defaultInvestigatorProvidedIndex = this.activeRangeOptions.findIndex(
(option) => option.value == 'investigatorProvided'
)
const primaryRange = this.activeRanges ? Object.entries(this.activeRanges).find(([, v]) => v.primary) : null
const primaryRangeKey = primaryRange ? primaryRange[0] : null

if (defaultInvestigatorProvidedIndex >= 0) {
return this.activeRangeOptions[defaultInvestigatorProvidedIndex]
// use the primary range if it exists, otherwise use investigatorProvided if it exists, otherwise none.
if (primaryRangeKey) {
return this.activeRangeOptions.find((option) => option.value === primaryRangeKey) || {label: 'None', value: null}
} else if (this.activeRangeOptions.length > 0) {
return {label: 'None', value: null} // return this.activeRangeOptions[0]
return this.activeRangeOptions.find((option) => option.value === "investigatorProvided") || {label: 'None', value: null}
} else {
return {label: 'None', value: null}
}
Expand Down
5 changes: 4 additions & 1 deletion src/lib/ranges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ export interface pillarProjectParameterSet {
// but we allow their implicit possibility as well.
export interface ScoreSetRanges {
investigatorProvided: ScoreRanges
pillarProject: ScoreRanges
zeibergCalibration: ScoreRanges
scottCalibration: ScoreRanges
fayerCalibration: ScoreRanges
[key: string]: ScoreRanges
}

export interface ScoreRanges {
title: string
researchUseOnly: boolean
primary?: boolean
baselineScore?: number
baselineScoreDescription?: string | undefined
oddsPathSource?: [{ identifier: string; dbName: string }] | undefined
Expand Down
37 changes: 36 additions & 1 deletion src/lib/score-sets.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import _ from 'lodash'

import type {components} from '@/schema/openapi'
import type { components } from '@/schema/openapi'

type ScoreSet = components['schemas']['ScoreSet']
type PublicationIdentifier = components['schemas']['ScoreSet']['primaryPublicationIdentifiers'][0]
type Author = components["schemas"]["PublicationAuthors"]

/**
Expand Down Expand Up @@ -51,3 +52,37 @@ export function getScoreSetShortName(scoreSet: ScoreSet): string {
const parts = [authors, gene, year?.toString()].filter((x) => x != null)
return parts.length > 0 ? parts.join(' ') : (scoreSet.title ?? scoreSet.shortDescription ?? 'Score set')
}



/**
* Filters a collection of publication identifier objects and returns only those
* that match the dbName + identifier pairs provided in a source array.
*
* Each element in sourceArr is treated as a (dbName, identifier) criterion. The
* function searches the sources list for exact matches (both dbName and
* identifier must match) and returns the subset that satisfies at least one
* criterion. If no matches are found, or if either input is missing/invalid,
* null is returned.
*
* Matching preserves the original objects from the sources list (it does not
* clone or transform them) and maintains the order in which matching criteria
* appear in sourceArr (not the original order in sources).
*
*
* @param sourceArr An array of (dbName, identifier) pairs to match against the sources list. If not an array, the function returns null.
* @param sources A list of publication identifiers to search (e.g., fetched from an API). Must support Array.prototype.find; if undefined, returns null.
* @returns An array of matched publication identifiers (in the order of the matching criteria) or null if there are no matches or inputs are invalid.
*/
export function matchSources(
sourceArr: Array<{ dbName: string; identifier: string }> | undefined,
sources: PublicationIdentifier[] | undefined
): PublicationIdentifier[] | null {
if (!Array.isArray(sourceArr) || !sources) return null
const matchedSources = []
for (const source of sourceArr) {
const match = sources.find((s) => s.dbName === source.dbName && s.identifier === source.identifier)
if (match) matchedSources.push(match)
}
return matchedSources.length > 0 ? matchedSources : null
}