diff --git a/browser/src/GenePage/GenePage.tsx b/browser/src/GenePage/GenePage.tsx index fdab3431d..e686aaecc 100644 --- a/browser/src/GenePage/GenePage.tsx +++ b/browser/src/GenePage/GenePage.tsx @@ -22,6 +22,7 @@ import { isExac, hasCopyNumberVariants, isV2, + getTopLevelDataset, } from '@gnomad/dataset-metadata/metadata' import ConstraintTable from '../ConstraintTable/ConstraintTable' import VariantCooccurrenceCountsTable, { @@ -50,7 +51,7 @@ import MitochondrialGeneCoverageTrack from './MitochondrialGeneCoverageTrack' import MitochondrialVariantsInGene from './MitochondrialVariantsInGene' import { getPreferredTranscript } from './preferredTranscript' import StructuralVariantsInGene from './StructuralVariantsInGene' -import TissueExpressionTrack from './TissueExpressionTrack' +import TissueExpressionTrack, { TranscriptWithTissueExpression } from './TissueExpressionTrack' import VariantsInGene from './VariantsInGene' import { GnomadConstraint } from '../ConstraintTable/GnomadConstraintTable' @@ -71,6 +72,8 @@ import { LegendSwatch, } from '../ChartStyles' import { logButtonClick } from '../analytics' +import { GtexTissueExpression } from './TranscriptsTissueExpression' +import { GTEX_TISSUES } from '../gtex' export type Strand = '+' | '-' @@ -88,6 +91,30 @@ export type GeneMetadata = { flags: string[] } +export type GeneTranscript = { + transcript_id: string + transcript_version: string + exons: { + feature_type: string + start: number + stop: number + }[] + gtex_tissue_expression: GtexTissueExpression | null +} + +export type Pext = { + regions: { + start: number + stop: number + mean: number + tissues: { + tissue: string + value: number + }[] + }[] + flags: string[] +} + export type Gene = GeneMetadata & { reference_genome: ReferenceGenome name?: string @@ -100,29 +127,11 @@ export type Gene = GeneMetadata & { start: number stop: number }[] - transcripts: { - transcript_id: string - transcript_version: string - exons: { - feature_type: string - start: number - stop: number - }[] - }[] + transcripts: GeneTranscript[] flags: string[] gnomad_constraint?: GnomadConstraint exac_constraint?: ExacConstraint - pext?: { - regions: { - start: number - stop: number - mean: number - tissues: { - [key: string]: number - } - }[] - flags: string[] - } + pext?: Pext short_tandem_repeats?: { id: string }[] @@ -517,6 +526,8 @@ const GenePage = ({ datasetId, gene, geneId }: Props) => { { {hasCodingExons && gene.chrom !== 'M' && gene.pext && ( @@ -564,7 +576,6 @@ const GenePage = ({ datasetId, gene, geneId }: Props) => { & { [key: string]: TissueDetail | undefined } gene: Gene includeNonCodingTranscripts: boolean includeUTRs: boolean @@ -50,6 +48,8 @@ type GeneTranscriptsTrack = { const GeneTranscriptsTrack = ({ datasetId, + isTissueExpressionAvailable, + gtexTissues, gene, includeNonCodingTranscripts, includeUTRs, @@ -57,17 +57,12 @@ const GeneTranscriptsTrack = ({ preferredTranscriptDescription, }: GeneTranscriptsTrack) => { const transcriptsTrack = useRef(null) - - const gtexTissues = GTEX_TISSUES[getTopLevelDataset(datasetId)] - - const isTissueExpressionAvailable = gene.reference_genome === 'GRCh37' const [showTissueExpressionModal, setShowTissueExpressionModal] = useState(false) const maxMeanExpression = isTissueExpressionAvailable ? max( - // @ts-ignore - gene.transcripts.map((transcript: Transcript) => - mean(transcript.gtex_tissue_expression!.map((tissue) => tissue.value)) + (gene.transcripts as TranscriptWithTissueExpression[]).map( + (transcript) => mean(transcript.gtex_tissue_expression.map((tissue) => tissue.value))! ) ) : undefined @@ -133,15 +128,36 @@ const GeneTranscriptsTrack = ({ renderTranscriptRightPanel={ !isTissueExpressionAvailable ? null - : ({ transcript, width }: any) => { + : ({ + transcript, + width, + }: { + transcript: TranscriptWithTissueExpression + width: number + }) => { if (width < 36) { return null } - const meanExpression = mean(Object.values(transcript.gtex_tissue_expression)) - const maxExpression = max(Object.values(transcript.gtex_tissue_expression)) - const tissueMostExpressedIn = Object.keys(transcript.gtex_tissue_expression).find( - (tissue: any) => transcript.gtex_tissue_expression[tissue] === maxExpression + const meanExpression = mean( + transcript.gtex_tissue_expression.map( + (tissueExpression) => tissueExpression.value + ) + )! + const maxExpression = max( + transcript.gtex_tissue_expression.map( + (tissueExpression) => tissueExpression.value + ) + )! + const tissueMostExpressedIn = transcript.gtex_tissue_expression.find( + (tissue) => tissue.value === maxExpression + )!.tissue + + const circleRadiusMeanContribution = meanExpression === 0 ? 0 : 0.25 + const circleRadiusMaxMeanContribution = + maxMeanExpression === 0 ? 0 : meanExpression / maxMeanExpression! // if the right panel render, maxMean is defined + const circleRadius = Math.sqrt( + circleRadiusMeanContribution + 23.75 * circleRadiusMaxMeanContribution ) return ( @@ -150,10 +166,9 @@ const GeneTranscriptsTrack = ({ // @ts-expect-error TS(2322) FIXME: Type '{ children: Element; tooltip: string; }' is ... Remove this comment to see the full error message tooltip={`Mean expression across all tissues = ${meanExpression.toFixed( 2 - )} TPM\nMost expressed in ${gtexTissues[tissueMostExpressedIn].fullName} (${ - // @ts-expect-error TS(2532) FIXME: Object is possibly 'undefined'. - maxExpression.toFixed(2) - } TPM)`} + )} TPM\nMost expressed in ${ + gtexTissues[tissueMostExpressedIn]!.fullName + } (${maxExpression.toFixed(2)} TPM)`} > - + ) } @@ -196,7 +198,8 @@ const GeneTranscriptsTrack = ({ }} > { const roundedRegions = expressionRegions.map((region: any) => ({ @@ -70,7 +68,8 @@ const TRACK_HEIGHT = 20 const heightScale = scaleLinear().domain([0, 1]).range([0, TRACK_HEIGHT]).clamp(true) type PextRegionsPlotProps = { - datasetId: DatasetId + // datasetId: DatasetId + gtexTissues: Partial color: string regions: { start: number @@ -155,7 +154,8 @@ type ExpressedTissue = { } type IndividualTissueTrackProps = { - datasetId: DatasetId + // datasetId: DatasetId + gtexTissues: Partial exons: { start: number stop: number @@ -170,14 +170,14 @@ type IndividualTissueTrackProps = { maxMeanTranscriptExpressionInAnyTissue: number meanTranscriptExpressionInTissue: number tissue: string - transcriptWithMaxExpressionInTissue?: { + transcriptWithMaxExpressionInTissue: { transcript_id: string transcript_version: string - } + } | null } const IndividualTissueTrack = ({ - datasetId, + gtexTissues, exons, expressionRegions, maxTranscriptExpressionInTissue, @@ -186,7 +186,6 @@ const IndividualTissueTrack = ({ tissue, transcriptWithMaxExpressionInTissue, }: IndividualTissueTrackProps) => { - const gtexTissuesForDataset = GTEX_TISSUES[getTopLevelDataset(datasetId)] const isExpressed = expressionRegions.some( (region: any) => region.tissues.find((tissueObject: ExpressedTissue) => tissueObject.tissue === tissue) @@ -221,8 +220,8 @@ const IndividualTissueTrack = ({ 2 )} TPM\nMax transcript expression in this tissue = ${maxTranscriptExpressionInTissue.toFixed( 2 - )} (${transcriptWithMaxExpressionInTissue.transcript_id}.${ - transcriptWithMaxExpressionInTissue.transcript_version + )} (${transcriptWithMaxExpressionInTissue!.transcript_id}.${ + transcriptWithMaxExpressionInTissue!.transcript_version })` : // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message `Gene is not expressed in ${gtexTissuesForDataset[tissue].fullName}` @@ -258,9 +257,9 @@ const IndividualTissueTrack = ({ return ( @@ -325,8 +324,13 @@ const RightPanel = styled.div` margin-top: 1.25em; ` +// Omit gtex then re-include to remove the possible null, as this component only renders if gtex and pext are non null +export type TranscriptWithTissueExpression = Omit & { + gtex_tissue_expression: GtexTissueExpression +} + type TissueExpressionTrackProps = { - datasetId: DatasetId + gtexTissues: Partial exons: { start: number stop: number @@ -341,13 +345,13 @@ type TissueExpressionTrackProps = { }[] }[] flags: string[] - transcripts: Transcript[] + transcripts: TranscriptWithTissueExpression[] preferredTranscriptId?: string preferredTranscriptDescription?: string | React.ReactNode } const TissueExpressionTrack = ({ - datasetId, + gtexTissues, exons, expressionRegions, flags, @@ -360,38 +364,43 @@ const TissueExpressionTrack = ({ useState(false) const [tissueFilterText, setTissueFilterText] = useState('') const mainTrack = useRef() - - const gtexTissuesInDataset = GTEX_TISSUES[getTopLevelDataset(datasetId)] - - const [sortTissuesBy, setSortTissuesBy] = useState('alphabetical') + const [sortTissuesBy, setSortTissuesBy] = useState<'alphabetical' | 'mean-expression'>( + 'alphabetical' + ) type ExpressionByTissueDetails = { maxTranscriptExpressionInTissue: number meanTranscriptExpressionInTissue: number - transcriptWithMaxExpressionInTissue: string + transcriptWithMaxExpressionInTissue: { + transcript_id: string + transcript_version: string + } | null } type ExpressionByTissue = Record - const expressionByTissue: ExpressionByTissue = Object.keys(gtexTissuesInDataset).reduce( + const expressionByTissue: ExpressionByTissue = Object.keys(gtexTissues).reduce( (acc, tissueId) => { let maxTranscriptExpressionInTissue = 0 let transcriptWithMaxExpressionInTissue = null - transcripts.forEach((transcript: Transcript) => { - const expressionInTissue = transcript.gtex_tissue_expression!.find( + transcripts.forEach((transcript) => { + const expressionInTissue = transcript.gtex_tissue_expression.find( (tissue) => tissue.tissue === tissueId )!.value - if (expressionInTissue! > maxTranscriptExpressionInTissue) { + if (expressionInTissue > maxTranscriptExpressionInTissue) { maxTranscriptExpressionInTissue = expressionInTissue! - transcriptWithMaxExpressionInTissue = transcript + transcriptWithMaxExpressionInTissue = { + transcript_id: transcript.transcript_id, + transcript_version: transcript.transcript_version, + } } }) const meanTranscriptExpressionInTissue = mean( transcripts.map( - (transcript: Transcript) => - transcript.gtex_tissue_expression!.find((tissue) => tissue.tissue === tissueId)!.value + (transcript) => + transcript.gtex_tissue_expression.find((tissue) => tissue.tissue === tissueId)!.value ) ) @@ -409,11 +418,11 @@ const TissueExpressionTrack = ({ const maxMeanTranscriptExpressionInAnyTissue = max( Object.values(expressionByTissue).map((v) => v.meanTranscriptExpressionInTissue) - ) + )! let tissues if (sortTissuesBy === 'mean-expression') { - tissues = Object.entries(gtexTissuesInDataset) + tissues = Object.entries(gtexTissues) .sort((t1, t2) => { const t1Expression = expressionByTissue[t1[0]].meanTranscriptExpressionInTissue const t2Expression = expressionByTissue[t2[0]].meanTranscriptExpressionInTissue @@ -424,7 +433,7 @@ const TissueExpressionTrack = ({ }) .map((t: any) => t[0]) } else { - tissues = Object.entries(gtexTissuesInDataset) + tissues = Object.entries(gtexTissues) .sort((t1, t2) => t1[1].fullName.localeCompare(t2[1].fullName)) .map((t) => t[0]) } @@ -493,6 +502,7 @@ const TissueExpressionTrack = ({ return ( r.mean)} scalePosition={scalePosition} @@ -571,21 +581,18 @@ const TissueExpressionTrack = ({ {(tissueFilterText ? tissues.filter(tissuePredicate(tissueFilterText)) : tissues).map( (tissue: any) => ( transcript.exons.some((exon: any) => exon.feature_type === 'CDS') type TranscriptsTissueExpressionProps = { - datasetId: DatasetId - transcripts: Transcript[] - // transcripts: { - // transcript_id: string - // transcript_version: string - // exons: { - // feature_type: string - // start: number - // stop: number - // }[] - // }[] + gtexTissues: Partial + transcripts: TranscriptWithTissueExpression[] includeNonCodingTranscripts: boolean preferredTranscriptId?: string preferredTranscriptDescription?: string | React.ReactNode @@ -46,7 +37,7 @@ export type TissueExpression = { export type GtexTissueExpression = TissueExpression[] const TranscriptsTissueExpression = ({ - datasetId, + gtexTissues, transcripts, includeNonCodingTranscripts, preferredTranscriptId, @@ -54,7 +45,6 @@ const TranscriptsTissueExpression = ({ defaultSortTissuesBy, }: TranscriptsTissueExpressionProps) => { const [sortTranscriptsBy, setSortTranscriptsBy] = useState('default') - const gtexTissuesForDataset = GTEX_TISSUES[getTopLevelDataset(datasetId)] let renderedTranscripts = sortTranscriptsBy === 'default' @@ -63,11 +53,11 @@ const TranscriptsTissueExpression = ({ const t1Expression = t1.gtex_tissue_expression.find( (tissue: TissueExpression) => tissue.tissue === sortTranscriptsBy - ).value || 0 + )!.value || 0 const t2Expression = t2.gtex_tissue_expression.find( (tissue: TissueExpression) => tissue.tissue === sortTranscriptsBy - ).value || 0 + )!.value || 0 if (t1Expression === t2Expression) { return t1.transcript_id.localeCompare(t2.transcript_id) @@ -79,9 +69,7 @@ const TranscriptsTissueExpression = ({ const [sortTissuesBy, setSortTissuesBy] = useState(defaultSortTissuesBy) let tissues if (sortTissuesBy === 'mean-expression') { - const meanExpressionByTissue: Record = Object.keys( - gtexTissuesForDataset - ).reduce( + const meanExpressionByTissue: Record = Object.keys(gtexTissues).reduce( (acc, tissueId) => ({ ...acc, [tissueId]: mean( @@ -95,7 +83,7 @@ const TranscriptsTissueExpression = ({ }), {} ) - tissues = Object.entries(gtexTissuesForDataset) + tissues = Object.entries(gtexTissues) .sort((t1, t2) => { const t1Expression = meanExpressionByTissue[t1[0]] const t2Expression = meanExpressionByTissue[t2[0]] @@ -106,7 +94,7 @@ const TranscriptsTissueExpression = ({ }) .map((t: any) => t[0]) } else { - tissues = Object.entries(gtexTissuesForDataset) + tissues = Object.entries(gtexTissues) .sort((t1, t2) => t1[1].fullName.localeCompare(t2[1].fullName)) .map((t) => t[0]) } @@ -128,7 +116,7 @@ const TranscriptsTissueExpression = ({ > - {Object.entries(gtexTissuesForDataset).map(([tissueId, tissueDetails]) => { + {Object.entries(gtexTissues).map(([tissueId, tissueDetails]) => { return (