diff --git a/browser/src/GenePage/GenePage.tsx b/browser/src/GenePage/GenePage.tsx index 15be9baa8..f93a8712b 100644 --- a/browser/src/GenePage/GenePage.tsx +++ b/browser/src/GenePage/GenePage.tsx @@ -50,7 +50,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 +71,7 @@ import { LegendSwatch, } from '../ChartStyles' import { logButtonClick } from '../analytics' +import { GtexTissueExpression } from './TranscriptsTissueExpression' export type Strand = '+' | '-' @@ -88,6 +89,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 +125,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 +524,7 @@ const GenePage = ({ datasetId, gene, geneId }: Props) => { { exons={cdsCompositeExons} expressionRegions={gene.pext.regions} flags={gene.pext.flags} - transcripts={gene.transcripts} + transcripts={gene.transcripts as TranscriptWithTissueExpression[]} // if a gene has pext, it has gtex preferredTranscriptId={preferredTranscriptId} preferredTranscriptDescription={preferredTranscriptDescription} /> @@ -563,7 +571,6 @@ const GenePage = ({ datasetId, gene, geneId }: Props) => { { +}: GeneTranscriptsTrack) => { const transcriptsTrack = useRef(null) - - const isTissueExpressionAvailable = gene.reference_genome === 'GRCh37' const [showTissueExpressionModal, setShowTissueExpressionModal] = useState(false) + const gtexTissues: Partial> = {} + if (isTissueExpressionAvailable) { + const preferredTranscript = (gene.transcripts as TranscriptWithTissueExpression[]).find( + (transcript) => transcript.transcript_id === preferredTranscriptId + ) + preferredTranscript!.gtex_tissue_expression.forEach((tissue) => { + gtexTissues[tissue.tissue as GtexTissueName] = { + fullName: ALL_GTEX_TISSUES[tissue.tissue as GtexTissueName].fullName || tissue.tissue, + color: ALL_GTEX_TISSUES[tissue.tissue as GtexTissueName].color || '#888888', + value: tissue.value, + } + }) + } + const maxMeanExpression = isTissueExpressionAvailable ? max( - gene.transcripts.map((transcript: any) => - mean(Object.values(transcript.gtex_tissue_expression)) + (gene.transcripts as TranscriptWithTissueExpression[]).map( + (transcript) => mean(transcript.gtex_tissue_expression.map((tissue) => tissue.value))! ) ) : undefined @@ -129,15 +140,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! + const circleRadius = Math.sqrt( + circleRadiusMeanContribution + 23.75 * circleRadiusMaxMeanContribution ) return ( @@ -147,12 +179,8 @@ const GeneTranscriptsTrack = ({ tooltip={`Mean expression across all tissues = ${meanExpression.toFixed( 2 )} TPM\nMost expressed in ${ - // @ts-expect-error TS(2538) FIXME: Type 'undefined' cannot be used as an index type. - GTEX_TISSUE_NAMES[tissueMostExpressedIn] - } (${ - // @ts-expect-error TS(2532) FIXME: Object is possibly 'undefined'. - maxExpression.toFixed(2) - } TPM)`} + gtexTissues[tissueMostExpressedIn as GtexTissueName]!.fullName + } (${maxExpression.toFixed(2)} TPM)`} > - + ) } @@ -195,7 +210,8 @@ const GeneTranscriptsTrack = ({ }} > { const roundedRegions = expressionRegions.map((region: any) => ({ @@ -67,6 +68,7 @@ const TRACK_HEIGHT = 20 const heightScale = scaleLinear().domain([0, 1]).range([0, TRACK_HEIGHT]).clamp(true) type PextRegionsPlotProps = { + gtexTissues: Partial color: string regions: { start: number @@ -145,8 +147,13 @@ const NotExpressedMessage = styled.div` color: gray; font-size: 10px; ` +type ExpressedTissue = { + tissue: string + value: number +} -type OwnIndividualTissueTrackProps = { +type IndividualTissueTrackProps = { + gtexTissues: Partial exons: { start: number stop: number @@ -155,26 +162,20 @@ type OwnIndividualTissueTrackProps = { start: number stop: number mean?: number - tissues: { - [key: string]: number - } + tissues: ExpressedTissue[] }[] maxTranscriptExpressionInTissue: number maxMeanTranscriptExpressionInAnyTissue: number meanTranscriptExpressionInTissue: number - tissue: string - transcriptWithMaxExpressionInTissue?: { + tissue: GtexTissueName + transcriptWithMaxExpressionInTissue: { transcript_id: string transcript_version: string - } + } | null } -// @ts-expect-error TS(2456) FIXME: Type alias 'IndividualTissueTrackProps' circularly... Remove this comment to see the full error message -type IndividualTissueTrackProps = OwnIndividualTissueTrackProps & - typeof IndividualTissueTrack.defaultProps - -// @ts-expect-error TS(7022) FIXME: 'IndividualTissueTrack' implicitly has type 'any' ... Remove this comment to see the full error message const IndividualTissueTrack = ({ + gtexTissues, exons, expressionRegions, maxTranscriptExpressionInTissue, @@ -183,11 +184,16 @@ const IndividualTissueTrack = ({ tissue, transcriptWithMaxExpressionInTissue, }: IndividualTissueTrackProps) => { - const isExpressed = expressionRegions.some((region: any) => region.tissues[tissue] !== 0) + const isExpressed = expressionRegions.some( + (region: any) => + region.tissues.find((tissueObject: ExpressedTissue) => tissueObject.tissue === tissue) + ?.value !== 0 + ) + return ( {GTEX_TISSUE_NAMES[tissue]}} + renderLeftPanel={() => {gtexTissues[tissue].fullName}} renderRightPanel={({ width }: any) => width > 36 && ( @@ -212,11 +218,10 @@ 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 ${GTEX_TISSUE_NAMES[tissue]}` + : `Gene is not expressed in ${gtexTissues[tissue]!.fullName}` } > @@ -249,9 +254,14 @@ const IndividualTissueTrack = ({ return ( r.tissues[tissue])} + gtexTissues={gtexTissues} + color={gtexTissues[tissue]!.color} + regions={getPlotRegions( + expressionRegions, + (r: any) => + r.tissues.find((tissueObject: ExpressedTissue) => tissueObject.tissue === tissue) + ?.value || 0 + )} scalePosition={scalePosition} width={width} /> @@ -310,7 +320,12 @@ const RightPanel = styled.div` margin-top: 1.25em; ` -type OwnTissueExpressionTrackProps = { +// 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 = { exons: { start: number stop: number @@ -320,28 +335,22 @@ type OwnTissueExpressionTrackProps = { stop: number mean?: number tissues: { - [key: string]: number - } - }[] - flags: string[] - transcripts: { - transcript_id: string - transcript_version: string - exons: { - feature_type: string - start: number - stop: number + tissue: string + value: number }[] }[] + flags: string[] + transcripts: TranscriptWithTissueExpression[] preferredTranscriptId?: string preferredTranscriptDescription?: string | React.ReactNode } -// @ts-expect-error TS(2456) FIXME: Type alias 'TissueExpressionTrackProps' circularly... Remove this comment to see the full error message -type TissueExpressionTrackProps = OwnTissueExpressionTrackProps & - typeof TissueExpressionTrack.defaultProps +export type GtexTissueDetail = { + fullName: string + color: string + value: number +} -// @ts-expect-error TS(7022) FIXME: 'TissueExpressionTrack' implicitly has type 'any' ... Remove this comment to see the full error message const TissueExpressionTrack = ({ exons, expressionRegions, @@ -355,57 +364,96 @@ const TissueExpressionTrack = ({ useState(false) const [tissueFilterText, setTissueFilterText] = useState('') const mainTrack = useRef() + const [sortTissuesBy, setSortTissuesBy] = useState<'alphabetical' | 'mean-expression'>( + 'alphabetical' + ) - const [sortTissuesBy, setSortTissuesBy] = useState('alphabetical') - - const expressionByTissue = Object.keys(GTEX_TISSUE_NAMES).reduce((acc, tissueId) => { - let maxTranscriptExpressionInTissue = 0 - let transcriptWithMaxExpressionInTissue = null - transcripts.forEach((transcript: any) => { - const expressionInTissue = transcript.gtex_tissue_expression[tissueId] - if (expressionInTissue > maxTranscriptExpressionInTissue) { - maxTranscriptExpressionInTissue = expressionInTissue - transcriptWithMaxExpressionInTissue = transcript + const gtexTissues: Partial> = {} + transcripts + .find((transcript) => transcript.transcript_id === preferredTranscriptId) + ?.gtex_tissue_expression.forEach((tissue) => { + gtexTissues[tissue.tissue as GtexTissueName] = { + fullName: ALL_GTEX_TISSUES[tissue.tissue as GtexTissueName].fullName || tissue.tissue, + color: ALL_GTEX_TISSUES[tissue.tissue as GtexTissueName].color || '#888888', + value: tissue.value, } }) - const meanTranscriptExpressionInTissue = mean( - transcripts.map((transcript: any) => transcript.gtex_tissue_expression[tissueId]) - ) + type ExpressionByTissueDetails = { + maxTranscriptExpressionInTissue: number + meanTranscriptExpressionInTissue: number + transcriptWithMaxExpressionInTissue: { + transcript_id: string + transcript_version: string + } | null + } + type ExpressionByTissue = Record - return { - ...acc, - [tissueId]: { - maxTranscriptExpressionInTissue, - meanTranscriptExpressionInTissue, - transcriptWithMaxExpressionInTissue, - }, - } - }, {}) + const expressionByTissue: ExpressionByTissue = Object.keys(gtexTissues).reduce( + (acc, tissueId) => { + let maxTranscriptExpressionInTissue = 0 + let transcriptWithMaxExpressionInTissue = null - const maxMeanTranscriptExpressionInAnyTissue = max( - Object.values(expressionByTissue).map((v: any) => v.meanTranscriptExpressionInTissue) - ) + transcripts.forEach((transcript) => { + const expressionInTissue = transcript.gtex_tissue_expression.find( + (tissue) => tissue.tissue === tissueId + ) - let tissues - if (sortTissuesBy === 'mean-expression') { - tissues = Object.entries(GTEX_TISSUE_NAMES) - .sort((t1: any, t2: any) => { - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message - const t1Expression = expressionByTissue[t1[0]].meanTranscriptExpressionInTissue - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message - const t2Expression = expressionByTissue[t2[0]].meanTranscriptExpressionInTissue - if (t1Expression === t2Expression) { - return t1[1].localeCompare(t2[1]) + if (expressionInTissue && expressionInTissue.value > maxTranscriptExpressionInTissue) { + maxTranscriptExpressionInTissue = expressionInTissue.value + transcriptWithMaxExpressionInTissue = { + transcript_id: transcript.transcript_id, + transcript_version: transcript.transcript_version, + } } - return t2Expression - t1Expression }) - .map((t: any) => t[0]) - } else { - tissues = Object.entries(GTEX_TISSUE_NAMES) - .sort((t1: any, t2: any) => t1[1].localeCompare(t2[1])) - .map((t: any) => t[0]) - } + + const meanTranscriptExpressionInTissue = mean( + transcripts + .map( + (transcript) => + transcript.gtex_tissue_expression.find((tissue) => tissue.tissue === tissueId)?.value + ) + .filter((value): value is number => value !== undefined) + ) + + return { + ...acc, + [tissueId]: { + maxTranscriptExpressionInTissue, + meanTranscriptExpressionInTissue, + transcriptWithMaxExpressionInTissue, + }, + } + }, + {} + ) + + const maxMeanTranscriptExpressionInAnyTissue = max( + Object.values(expressionByTissue).map((v) => v.meanTranscriptExpressionInTissue) + )! + + const tissues = + sortTissuesBy === 'mean-expression' + ? Object.entries(gtexTissues) + .sort((t1, t2) => { + const t1Expression = expressionByTissue[t1[0]].meanTranscriptExpressionInTissue + const t2Expression = expressionByTissue[t2[0]].meanTranscriptExpressionInTissue + if (t1Expression === t2Expression) { + return ALL_GTEX_TISSUES[t1[0] as GtexTissueName].fullName.localeCompare( + ALL_GTEX_TISSUES[t2[0] as GtexTissueName].fullName + ) + } + return t2Expression - t1Expression + }) + .map((t: any) => t[0]) + : Object.entries(gtexTissues) + .sort((t1, t2) => + ALL_GTEX_TISSUES[t1[0] as GtexTissueName].fullName.localeCompare( + ALL_GTEX_TISSUES[t2[0] as GtexTissueName].fullName + ) + ) + .map((t) => t[0]) const isExpressed = expressionRegions.some((region: any) => region.mean !== 0) @@ -471,6 +519,7 @@ const TissueExpressionTrack = ({ return ( r.mean)} scalePosition={scalePosition} @@ -547,27 +596,27 @@ const TissueExpressionTrack = ({ }} {(tissueFilterText ? tissues.filter(tissuePredicate(tissueFilterText)) : tissues).map( - (tissue: any) => ( - - ) + (tissue: any) => { + return ( + + ) + } )}