diff --git a/packages/oncoprintjs/src/js/oncoprinttrackinfoview.ts b/packages/oncoprintjs/src/js/oncoprinttrackinfoview.ts index cee6747b0e6..7b2154ad791 100644 --- a/packages/oncoprintjs/src/js/oncoprinttrackinfoview.ts +++ b/packages/oncoprintjs/src/js/oncoprinttrackinfoview.ts @@ -84,8 +84,10 @@ export default class OncoprintTrackInfoView { let formattedPercent = ''; if (isNaN(float)) { - formattedPercent = 'N/P'; - suffix = ''; // we don't want any suffix in this case + // Non-numeric info (the "N/P" sentinel, or a warning glyph) + // is shown verbatim rather than coerced to a percentage. + formattedPercent = text; + suffix = ''; } else if (isNumber(float)) { formattedPercent = float < 1 && float > 0 diff --git a/src/shared/components/oncoprint/DeltaUtils.ts b/src/shared/components/oncoprint/DeltaUtils.ts index ec7a831a0f4..831cb022d94 100644 --- a/src/shared/components/oncoprint/DeltaUtils.ts +++ b/src/shared/components/oncoprint/DeltaUtils.ts @@ -1560,6 +1560,9 @@ export function transitionHeatmapTrack( nextProps.caseLinkOutInTooltips ), track_info: nextSpec.info || '', + $track_info_tooltip_elt: nextSpec.infoTooltip + ? $('
' + nextSpec.infoTooltip + '
') + : undefined, onSortDirectionChange: nextProps.onTrackSortDirectionChange, expansion_of: expansionParentKey ? trackSpecKeyToTrackId[expansionParentKey] @@ -1627,6 +1630,14 @@ export function transitionHeatmapTrack( if (nextSpec.info !== prevSpec.info && nextSpec.info !== undefined) { oncoprint.setTrackInfo(trackId, nextSpec.info); } + if (nextSpec.infoTooltip !== prevSpec.infoTooltip) { + oncoprint.setTrackInfoTooltip( + trackId, + nextSpec.infoTooltip + ? $('
' + nextSpec.infoTooltip + '
') + : undefined + ); + } if ( nextSpec.movable !== prevSpec.movable && nextSpec.movable !== undefined @@ -1733,6 +1744,9 @@ export function transitionCategoricalTrack( nextProps.caseLinkOutInTooltips ), track_info: nextSpec.info || '', + $track_info_tooltip_elt: nextSpec.infoTooltip + ? $('
' + nextSpec.infoTooltip + '
') + : undefined, onSortDirectionChange: nextProps.onTrackSortDirectionChange, }; const newTrackId = oncoprint.addTracks([trackParams])[0]; diff --git a/src/shared/components/oncoprint/Oncoprint.tsx b/src/shared/components/oncoprint/Oncoprint.tsx index 3b486c23bc6..724a180b609 100644 --- a/src/shared/components/oncoprint/Oncoprint.tsx +++ b/src/shared/components/oncoprint/Oncoprint.tsx @@ -214,6 +214,7 @@ export interface IHeatmapTrackSpec extends IBaseHeatmapTrackSpec { data: IBaseHeatmapTrackDatum[]; // can be IGeneHeatmapTrackDatum or IGenericAssayHeatmapTrackDatum naLegendLabel?: string; info?: string; + infoTooltip?: string; labelColor?: string; labelCircleColor?: string; labelFontWeight?: string; @@ -256,6 +257,7 @@ export interface ICategoricalTrackSpec { naLegendLabel?: string; description?: string; info?: string; + infoTooltip?: string; } export const GENETIC_TRACK_GROUP_INDEX = 1; diff --git a/src/shared/components/oncoprint/OncoprintUtils.ts b/src/shared/components/oncoprint/OncoprintUtils.ts index ee089131626..33116bd75a8 100644 --- a/src/shared/components/oncoprint/OncoprintUtils.ts +++ b/src/shared/components/oncoprint/OncoprintUtils.ts @@ -74,6 +74,10 @@ import { isGenericAssayCategoricalProfile, isGenericAssayHeatmapProfile, } from 'shared/components/oncoprint/ResultsViewOncoprintUtils'; +import { getProfileDescriptionCaveat } from 'shared/lib/GenericAssayUtils/GenericAssayCommonUtils'; + +// Warning glyph shown in a track's info slot when its profile carries a caveat. +export const PROFILE_CAVEAT_GLYPH = '⚠'; import { ExtendedClinicalAttribute } from 'pages/resultsView/ResultsViewPageStoreUtils'; import { CaseAggregatedData } from 'shared/model/CaseAggregatedData'; import { AnnotatedExtendedAlteration } from 'shared/model/AnnotatedExtendedAlteration'; @@ -1651,11 +1655,16 @@ export function makeGenericAssayProfileCategoricalTracksMobxPromise( const entityLinkMap = oncoprint.genericAssayPromises .genericAssayEntitiesGroupedByGenericAssayTypeLinkMap .result![profile.genericAssayType]; + const caveat = getProfileDescriptionCaveat(profile.description); return { key: `GENERICASSAYCATEGORICALTRACK_${molecularProfileId},${entityId}`, label: query.entityName, description: query.description, + info: caveat ? PROFILE_CAVEAT_GLYPH : undefined, + infoTooltip: caveat + ? `${PROFILE_CAVEAT_GLYPH} Caveat: ${caveat}` + : undefined, molecularProfileId: query.molecularProfileId, molecularProfileName: molecularProfileIdToMolecularProfile[molecularProfileId] @@ -1760,11 +1769,16 @@ export function makeGenericAssayProfileHeatmapTracksMobxPromise( const entityLinkMap = oncoprint.genericAssayPromises .genericAssayEntitiesGroupedByGenericAssayTypeLinkMap .result![profile.genericAssayType]; + const caveat = getProfileDescriptionCaveat(profile.description); return { key: `GENERICASSAYHEATMAPTRACK_${molecularProfileId},${entityId}`, label: query.entityName, description: query.description, + info: caveat ? PROFILE_CAVEAT_GLYPH : undefined, + infoTooltip: caveat + ? `${PROFILE_CAVEAT_GLYPH} Caveat: ${caveat}` + : undefined, molecularProfileId: query.molecularProfileId, molecularProfileName: molecularProfileIdToMolecularProfile[molecularProfileId] diff --git a/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.spec.ts b/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.spec.ts index 2ec11f51cb9..f8804352e5d 100644 --- a/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.spec.ts +++ b/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.spec.ts @@ -9,6 +9,7 @@ import { filterGenericAssayEntitiesByGenes, makeGenericAssayPlotsTabOption, filterGenericAssayOptionsByGenes, + getProfileDescriptionCaveat, } from './GenericAssayCommonUtils'; import { getServerConfig } from 'config/config'; import ServerConfigDefaults from 'config/serverConfigDefaults'; @@ -471,4 +472,39 @@ describe('GenericAssayCommonUtils', () => { ); }); }); + + describe('getProfileDescriptionCaveat()', () => { + it('returns undefined when description is undefined or empty', () => { + assert.isUndefined(getProfileDescriptionCaveat(undefined)); + assert.isUndefined(getProfileDescriptionCaveat('')); + }); + + it('returns undefined when there is no caveat marker', () => { + assert.isUndefined( + getProfileDescriptionCaveat('Cell Type Relative Fractions.') + ); + }); + + it('extracts the caveat text after the marker', () => { + assert.equal( + getProfileDescriptionCaveat( + 'Cell Type Relative Fractions. Caveat: Fractions are derived from CD45 FACS-sorted scRNA-seq.' + ), + 'Fractions are derived from CD45 FACS-sorted scRNA-seq.' + ); + }); + + it('is case-insensitive on the marker', () => { + assert.equal( + getProfileDescriptionCaveat('caveat: something to note'), + 'something to note' + ); + }); + + it('returns undefined when the marker has no following text', () => { + assert.isUndefined( + getProfileDescriptionCaveat('Some description. Caveat: ') + ); + }); + }); }); diff --git a/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.ts b/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.ts index d85a8b9d5aa..62ff1f68e73 100644 --- a/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.ts +++ b/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.ts @@ -118,9 +118,8 @@ export async function fetchGenericAssayMetaByMolecularProfileIdsGroupByMolecular } = {}; for (const profile of genericAssayProfiles) { const suffix = getSuffixOfMolecularProfile(profile); - genericAssayMetaGroupByMolecularProfileId[ - profile.molecularProfileId - ] = metaBySuffix[suffix] || []; + genericAssayMetaGroupByMolecularProfileId[profile.molecularProfileId] = + metaBySuffix[suffix] || []; } return genericAssayMetaGroupByMolecularProfileId; @@ -228,12 +227,14 @@ export function fetchGenericAssayDataByStableIdsAndMolecularIds( stableIds: string[], molecularProfileIds: string[] ) { - return getClient().fetchGenericAssayDataInMultipleMolecularProfilesUsingPOST({ - genericAssayDataMultipleStudyFilter: { - genericAssayStableIds: stableIds, - molecularProfileIds: molecularProfileIds, - } as GenericAssayDataMultipleStudyFilter, - }); + return getClient().fetchGenericAssayDataInMultipleMolecularProfilesUsingPOST( + { + genericAssayDataMultipleStudyFilter: { + genericAssayStableIds: stableIds, + molecularProfileIds: molecularProfileIds, + } as GenericAssayDataMultipleStudyFilter, + } + ); } export function makeGenericAssayOption(meta: GenericAssayMeta) { @@ -356,6 +357,21 @@ export function getCategoryOrderByGenericAssayType(genericAssayType: string) { : undefined; } +// A profile description may carry a curated caveat about how its data should be +// interpreted, flagged with a "Caveat:" marker (e.g. data derived from a sorted +// cell population). Everything after the marker is the caveat text, surfaced as +// a per-track warning so viewers see it without reading the profile description. +export function getProfileDescriptionCaveat( + description: string | undefined +): string | undefined { + if (!description) { + return undefined; + } + const match = /caveat:\s*([\s\S]+)/i.exec(description); + const caveat = match && match[1].trim(); + return caveat ? caveat : undefined; +} + export function constructGeneRegex(hugoGeneSymbol: string) { // Match whole gene symbol text and case-insensitive, non-alphanumeric characters is allowed after gene symbol // Sometimes, gene symbol might appear in description text