diff --git a/src/components/AssayFactSheet.vue b/src/components/AssayFactSheet.vue index 6a2baf86..23ac5cc5 100644 --- a/src/components/AssayFactSheet.vue +++ b/src/components/AssayFactSheet.vue @@ -77,48 +77,63 @@ -
Clinical Performance
+
+ Clinical Performance* +
-
+
OddsPath – Normal
{{ roundOddsPath( - scoreSet.scoreRanges.investigatorProvided.ranges.find((r) => r.classification === 'normal').oddsPath + primaryScoreRange?.ranges.find((r) => r.classification === 'normal').oddsPath ?.ratio ) }} {{ - scoreSet.scoreRanges.investigatorProvided.ranges.find((r) => r.classification === 'normal').oddsPath + primaryScoreRange?.ranges.find((r) => r.classification === 'normal').oddsPath ?.evidence }}
+
OddsPath normal not provided
OddsPath – Abnormal
{{ roundOddsPath( - scoreSet.scoreRanges.investigatorProvided.ranges.find((r) => r.classification === 'abnormal').oddsPath + primaryScoreRange?.ranges.find((r) => r.classification === 'abnormal').oddsPath ?.ratio ) }} {{ - scoreSet.scoreRanges.investigatorProvided.ranges.find((r) => r.classification === 'abnormal').oddsPath + primaryScoreRange?.ranges.find((r) => r.classification === 'abnormal').oddsPath ?.evidence }}
+
OddsPath abnormal not provided
+
+
+ *OddsPath data from non-primary source(s): + +
OddsPath values are not provided for this score set.
@@ -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({ @@ -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) + 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) - } + }, } }) diff --git a/src/components/RangeTable.vue b/src/components/RangeTable.vue index 0566c376..7b9ae3d7 100644 --- a/src/components/RangeTable.vue +++ b/src/components/RangeTable.vue @@ -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', @@ -152,7 +156,7 @@ export default defineComponent({ required: true }, sources: { - type: Array as PropType<{dbName: string; identifier: string; url: string}[]>, + type: Array as PropType, required: false, default: () => [] } @@ -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) } }, @@ -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) }, diff --git a/src/components/ScoreSetHistogram.vue b/src/components/ScoreSetHistogram.vue index 7070af88..3f4a010c 100644 --- a/src/components/ScoreSetHistogram.vue +++ b/src/components/ScoreSetHistogram.vue @@ -117,7 +117,7 @@
- +
@@ -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[], @@ -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) @@ -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) @@ -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) } @@ -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} } diff --git a/src/lib/ranges.ts b/src/lib/ranges.ts index 243c1081..f8e338c9 100644 --- a/src/lib/ranges.ts +++ b/src/lib/ranges.ts @@ -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 diff --git a/src/lib/score-sets.ts b/src/lib/score-sets.ts index cd30b4be..9ed2d2b3 100644 --- a/src/lib/score-sets.ts +++ b/src/lib/score-sets.ts @@ -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"] /** @@ -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 +}