diff --git a/browser/cypress/fixtures/grch37_reference_stub.json b/browser/cypress/fixtures/grch37_reference_stub.json index 7d6cf9440..8a0c5738b 100644 --- a/browser/cypress/fixtures/grch37_reference_stub.json +++ b/browser/cypress/fixtures/grch37_reference_stub.json @@ -1342,6 +1342,7 @@ "flags": [] }, "exac_regional_missense_constraint_regions": null, + "gnomad_v2_regional_missense_constraint_regions": null, "heterozygous_variant_cooccurrence_counts": [ { "csq": "lof_lof", diff --git a/browser/src/GenePage/GenePage.tsx b/browser/src/GenePage/GenePage.tsx index 8a11e5aa4..a215a0573 100644 --- a/browser/src/GenePage/GenePage.tsx +++ b/browser/src/GenePage/GenePage.tsx @@ -19,6 +19,7 @@ import { ReferenceGenome, hasExons, isExac, + isV2, } from '@gnomad/dataset-metadata/metadata' import ConstraintTable from '../ConstraintTable/ConstraintTable' import VariantCooccurrenceCountsTable, { @@ -31,6 +32,7 @@ import GnomadPageHeading from '../GnomadPageHeading' import InfoButton from '../help/InfoButton' import Link from '../Link' import RegionalConstraintTrack from '../RegionalConstraintTrack' +import RegionalMissenseConstraintTrack, { RegionalMissenseConstraint} from '../RegionalMissenseConstraintTrack' import RegionCoverageTrack from '../RegionPage/RegionCoverageTrack' import RegionViewer from '../RegionViewer/ZoomableRegionViewer' import { TrackPage, TrackPageSection } from '../TrackPage' @@ -106,6 +108,7 @@ export type Gene = GeneMetadata & { id: string }[] exac_regional_missense_constraint_regions?: any + gnomad_v2_regional_missense_constraint?: RegionalMissenseConstraint variants: Variant[] structural_variants: StructuralVariant[] clinvar_variants: ClinvarVariant[] @@ -568,6 +571,13 @@ const GenePage = ({ datasetId, gene, geneId }: Props) => { /> )} + {isV2(datasetId) && ( + <RegionalMissenseConstraintTrack + regionalMissenseConstraint={gene.gnomad_v2_regional_missense_constraint} + gene={gene} + /> + )} + {/* eslint-disable-next-line no-nested-ternary */} {hasStructuralVariants(datasetId) ? ( <StructuralVariantsInGene datasetId={datasetId} gene={gene} zoomRegion={zoomRegion} /> diff --git a/browser/src/GenePage/GenePageContainer.tsx b/browser/src/GenePage/GenePageContainer.tsx index 694f1c4bc..e78711c20 100644 --- a/browser/src/GenePage/GenePageContainer.tsx +++ b/browser/src/GenePage/GenePageContainer.tsx @@ -224,6 +224,22 @@ query ${operationName}($geneId: String, $geneSymbol: String, $referenceGenome: R obs_exp chisq_diff_null } + gnomad_v2_regional_missense_constraint { + passed_qc + has_no_rmc_evidence + regions { + chrom + start + stop + aa_start + aa_stop + obs_mis + exp_mis + obs_exp + chisq_diff_null + p_value + } + } short_tandem_repeats(dataset: $shortTandemRepeatDatasetId) @include(if: $includeShortTandemRepeats) { id } diff --git a/browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap b/browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap index 7288a560d..adf955538 100644 --- a/browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap +++ b/browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap @@ -8584,6 +8584,101 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1" has no unexpected changes 1` </div> </div> </div> + <div + className="Track__OuterWrapper-sc-1sdyh2h-0 bBMGlf" + > + <div + className="Track__InnerWrapper-sc-1sdyh2h-1 cEOGGC" + > + <div + className="Track__SidePanel-sc-1sdyh2h-3 iSYzDq" + style={ + { + "width": 115, + } + } + > + <div + className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu" + > + <span> + Regional missense constraint + </span> + <button + className="InfoButton__Button-sc-13t5e82-0 gxsoyZ" + onClick={[Function]} + type="button" + > + <img + alt="" + aria-hidden="true" + src="test-file-stub" + /> + <span + style={ + { + "border": "0", + "clip": "rect(0 0 0 0)", + "height": "1px", + "margin": "-1px", + "overflow": "hidden", + "padding": "0", + "position": "absolute", + "whiteSpace": "nowrap", + "width": "1px", + } + } + > + More information + </span> + </button> + </div> + </div> + <div + className="Track__CenterPanel-sc-1sdyh2h-4 iAyrrk" + style={ + { + "width": 799, + } + } + > + <div + className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW" + > + <svg + height={35} + width={799} + > + <text + dy="1.0rem" + textAnchor="middle" + x={399.5} + y={17.5} + > + <tspan> + This gene was not searched for evidence of regional missense constraint. See our + + </tspan> + <tspan + fill="#0000ff" + > + <a + className="Link-sc-14lgydv-0-Link jBvaYQ" + href="/help" + onClick={[Function]} + > + help page + </a> + </tspan> + <tspan> + for additional information. + </tspan> + </text> + </svg> + </div> + </div> + </div> + </div> <div className="TrackPage__TrackPageSection-sc-1xq3qi7-1 fQVUsz" > @@ -10043,6 +10138,101 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_controls" has no unexpected c </div> </div> </div> + <div + className="Track__OuterWrapper-sc-1sdyh2h-0 bBMGlf" + > + <div + className="Track__InnerWrapper-sc-1sdyh2h-1 cEOGGC" + > + <div + className="Track__SidePanel-sc-1sdyh2h-3 iSYzDq" + style={ + { + "width": 115, + } + } + > + <div + className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu" + > + <span> + Regional missense constraint + </span> + <button + className="InfoButton__Button-sc-13t5e82-0 gxsoyZ" + onClick={[Function]} + type="button" + > + <img + alt="" + aria-hidden="true" + src="test-file-stub" + /> + <span + style={ + { + "border": "0", + "clip": "rect(0 0 0 0)", + "height": "1px", + "margin": "-1px", + "overflow": "hidden", + "padding": "0", + "position": "absolute", + "whiteSpace": "nowrap", + "width": "1px", + } + } + > + More information + </span> + </button> + </div> + </div> + <div + className="Track__CenterPanel-sc-1sdyh2h-4 iAyrrk" + style={ + { + "width": 799, + } + } + > + <div + className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW" + > + <svg + height={35} + width={799} + > + <text + dy="1.0rem" + textAnchor="middle" + x={399.5} + y={17.5} + > + <tspan> + This gene was not searched for evidence of regional missense constraint. See our + + </tspan> + <tspan + fill="#0000ff" + > + <a + className="Link-sc-14lgydv-0-Link jBvaYQ" + href="/help" + onClick={[Function]} + > + help page + </a> + </tspan> + <tspan> + for additional information. + </tspan> + </text> + </svg> + </div> + </div> + </div> + </div> <div className="TrackPage__TrackPageSection-sc-1xq3qi7-1 fQVUsz" > @@ -11502,6 +11692,101 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_cancer" has no unexpected </div> </div> </div> + <div + className="Track__OuterWrapper-sc-1sdyh2h-0 bBMGlf" + > + <div + className="Track__InnerWrapper-sc-1sdyh2h-1 cEOGGC" + > + <div + className="Track__SidePanel-sc-1sdyh2h-3 iSYzDq" + style={ + { + "width": 115, + } + } + > + <div + className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu" + > + <span> + Regional missense constraint + </span> + <button + className="InfoButton__Button-sc-13t5e82-0 gxsoyZ" + onClick={[Function]} + type="button" + > + <img + alt="" + aria-hidden="true" + src="test-file-stub" + /> + <span + style={ + { + "border": "0", + "clip": "rect(0 0 0 0)", + "height": "1px", + "margin": "-1px", + "overflow": "hidden", + "padding": "0", + "position": "absolute", + "whiteSpace": "nowrap", + "width": "1px", + } + } + > + More information + </span> + </button> + </div> + </div> + <div + className="Track__CenterPanel-sc-1sdyh2h-4 iAyrrk" + style={ + { + "width": 799, + } + } + > + <div + className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW" + > + <svg + height={35} + width={799} + > + <text + dy="1.0rem" + textAnchor="middle" + x={399.5} + y={17.5} + > + <tspan> + This gene was not searched for evidence of regional missense constraint. See our + + </tspan> + <tspan + fill="#0000ff" + > + <a + className="Link-sc-14lgydv-0-Link jBvaYQ" + href="/help" + onClick={[Function]} + > + help page + </a> + </tspan> + <tspan> + for additional information. + </tspan> + </text> + </svg> + </div> + </div> + </div> + </div> <div className="TrackPage__TrackPageSection-sc-1xq3qi7-1 fQVUsz" > @@ -12961,6 +13246,101 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_neuro" has no unexpected </div> </div> </div> + <div + className="Track__OuterWrapper-sc-1sdyh2h-0 bBMGlf" + > + <div + className="Track__InnerWrapper-sc-1sdyh2h-1 cEOGGC" + > + <div + className="Track__SidePanel-sc-1sdyh2h-3 iSYzDq" + style={ + { + "width": 115, + } + } + > + <div + className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu" + > + <span> + Regional missense constraint + </span> + <button + className="InfoButton__Button-sc-13t5e82-0 gxsoyZ" + onClick={[Function]} + type="button" + > + <img + alt="" + aria-hidden="true" + src="test-file-stub" + /> + <span + style={ + { + "border": "0", + "clip": "rect(0 0 0 0)", + "height": "1px", + "margin": "-1px", + "overflow": "hidden", + "padding": "0", + "position": "absolute", + "whiteSpace": "nowrap", + "width": "1px", + } + } + > + More information + </span> + </button> + </div> + </div> + <div + className="Track__CenterPanel-sc-1sdyh2h-4 iAyrrk" + style={ + { + "width": 799, + } + } + > + <div + className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW" + > + <svg + height={35} + width={799} + > + <text + dy="1.0rem" + textAnchor="middle" + x={399.5} + y={17.5} + > + <tspan> + This gene was not searched for evidence of regional missense constraint. See our + + </tspan> + <tspan + fill="#0000ff" + > + <a + className="Link-sc-14lgydv-0-Link jBvaYQ" + href="/help" + onClick={[Function]} + > + help page + </a> + </tspan> + <tspan> + for additional information. + </tspan> + </text> + </svg> + </div> + </div> + </div> + </div> <div className="TrackPage__TrackPageSection-sc-1xq3qi7-1 fQVUsz" > @@ -14420,6 +14800,101 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_topmed" has no unexpected </div> </div> </div> + <div + className="Track__OuterWrapper-sc-1sdyh2h-0 bBMGlf" + > + <div + className="Track__InnerWrapper-sc-1sdyh2h-1 cEOGGC" + > + <div + className="Track__SidePanel-sc-1sdyh2h-3 iSYzDq" + style={ + { + "width": 115, + } + } + > + <div + className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu" + > + <span> + Regional missense constraint + </span> + <button + className="InfoButton__Button-sc-13t5e82-0 gxsoyZ" + onClick={[Function]} + type="button" + > + <img + alt="" + aria-hidden="true" + src="test-file-stub" + /> + <span + style={ + { + "border": "0", + "clip": "rect(0 0 0 0)", + "height": "1px", + "margin": "-1px", + "overflow": "hidden", + "padding": "0", + "position": "absolute", + "whiteSpace": "nowrap", + "width": "1px", + } + } + > + More information + </span> + </button> + </div> + </div> + <div + className="Track__CenterPanel-sc-1sdyh2h-4 iAyrrk" + style={ + { + "width": 799, + } + } + > + <div + className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW" + > + <svg + height={35} + width={799} + > + <text + dy="1.0rem" + textAnchor="middle" + x={399.5} + y={17.5} + > + <tspan> + This gene was not searched for evidence of regional missense constraint. See our + + </tspan> + <tspan + fill="#0000ff" + > + <a + className="Link-sc-14lgydv-0-Link jBvaYQ" + href="/help" + onClick={[Function]} + > + help page + </a> + </tspan> + <tspan> + for additional information. + </tspan> + </text> + </svg> + </div> + </div> + </div> + </div> <div className="TrackPage__TrackPageSection-sc-1xq3qi7-1 fQVUsz" > diff --git a/browser/src/RegionalMissenseConstraintTrack.spec.tsx b/browser/src/RegionalMissenseConstraintTrack.spec.tsx new file mode 100644 index 000000000..4d6969090 --- /dev/null +++ b/browser/src/RegionalMissenseConstraintTrack.spec.tsx @@ -0,0 +1,56 @@ +import { describe, expect, test } from '@jest/globals' + +// eslint-disable-next-line import/no-unresolved, import/extensions +import { regionIntersections } from './RegionalMissenseConstraintTrack' + +describe('RegionaMissenseConstraintTrack', () => { + test('regionIntersections', () => { + const testCases = [ + { + regions1: [ + { start: 2, stop: 4, i: 1 }, + { start: 6, stop: 8, i: 2 }, + ], + regions2: [ + { start: 2, stop: 6, j: 1 }, + { start: 6, stop: 9, j: 2 }, + ], + expected: [ + { start: 2, stop: 4, i: 1, j: 1 }, + { start: 6, stop: 8, i: 2, j: 2 }, + ], + }, + { + regions1: [ + { start: 2, stop: 4, i: 1 }, + { start: 6, stop: 8, i: 2 }, + ], + regions2: [{ start: 1, stop: 8, j: 1 }], + expected: [ + { start: 2, stop: 4, i: 1, j: 1 }, + { start: 6, stop: 8, i: 2, j: 1 }, + ], + }, + { + regions1: [ + { start: 2, stop: 4, i: 1 }, + { start: 6, stop: 8, i: 2 }, + ], + regions2: [ + { start: 1, stop: 3, j: 1 }, + { start: 3, stop: 5, j: 2 }, + { start: 5, stop: 9, j: 3 }, + ], + expected: [ + { start: 2, stop: 3, i: 1, j: 1 }, + { start: 3, stop: 4, i: 1, j: 2 }, + { start: 6, stop: 8, i: 2, j: 3 }, + ], + }, + ] + + testCases.forEach(({ regions1, regions2, expected }) => { + expect(regionIntersections([regions1, regions2])).toEqual(expected) + }) + }) +}) diff --git a/browser/src/RegionalMissenseConstraintTrack.tsx b/browser/src/RegionalMissenseConstraintTrack.tsx new file mode 100644 index 000000000..7757d4e4d --- /dev/null +++ b/browser/src/RegionalMissenseConstraintTrack.tsx @@ -0,0 +1,444 @@ +import React from 'react' +import styled from 'styled-components' + +// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module '@gno... Remove this comment to see the full error message +import { Track } from '@gnomad/region-viewer' +import { TooltipAnchor } from '@gnomad/ui' + +import Link from './Link' + +import InfoButton from './help/InfoButton' +import { Gene } from './GenePage/GenePage' + +type RegionalMissenseConstraintRegion = { + chrom: string + start: number + stop: number + aa_start: string | null + aa_stop: string | null + obs_mis: number | undefined + exp_mis: number + obs_exp: number + chisq_diff_null: number | undefined + p_value: number + z_score: number | undefined +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 1em; +` + +const PlotWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; +` + +const RegionAttributeList = styled.dl` + margin: 0; + + div { + margin-bottom: 0.25em; + } + + dt { + display: inline; + font-weight: bold; + } + + dd { + display: inline; + margin-left: 0.5em; + } +` + +export const regionIntersections = (regionArrays: { start: number; stop: number }[][]) => { + const sortedRegionsArrays = regionArrays.map((regions) => + [...regions].sort((a, b) => a.start - b.start) + ) + + const intersections = [] + + const indices = sortedRegionsArrays.map(() => 0) + + while (sortedRegionsArrays.every((regions, i) => indices[i] < regions.length)) { + const maxStart = Math.max(...sortedRegionsArrays.map((regions, i) => regions[indices[i]].start)) + const minStop = Math.min(...sortedRegionsArrays.map((regions, i) => regions[indices[i]].stop)) + + if (maxStart < minStop) { + const next = Object.assign( + // @ts-ignore TS2556: A spread argument must either have a tuple type or be ... + ...[ + {}, + ...sortedRegionsArrays.map((regions: { [x: string]: any }, i) => regions[indices[i]]), + { + start: maxStart, + stop: minStop, + }, + ] + ) + + intersections.push(next) + } + + sortedRegionsArrays.forEach((regions, i) => { + if (regions[indices[i]].stop === minStop) { + indices[i] += 1 + } + }) + } + + return intersections +} + +// https://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=5 +const colorScale = { + not_significant: '#e2e2e2', + least: '#9b001f', + less: '#de351b', + middle: '#fd8d3c', + greater: '#fecc5c', + greatest: '#ffffb2', +} + +function regionColor(region: RegionalMissenseConstraintRegion) { + if (region.z_score) { + return region.z_score > 3.09 ? colorScale.middle : colorScale.not_significant + } + + let color + if (region.obs_exp > 0.8) { + color = colorScale.greatest + } else if (region.obs_exp > 0.6) { + color = colorScale.greater + } else if (region.obs_exp > 0.4) { + color = colorScale.middle + } else if (region.obs_exp > 0.2) { + color = colorScale.less + } else { + color = colorScale.least + } + + return region.p_value > 0.001 ? colorScale.not_significant : color +} + +const LegendWrapper = styled.div` + display: flex; + + @media (max-width: 600px) { + flex-direction: column; + align-items: center; + } +` + +const Legend = () => { + return ( + <LegendWrapper> + <span>Missense observed/expected</span> + <svg width={170} height={25}> + <rect x={10} y={0} width={30} height={10} stroke="#000" fill={colorScale.least} /> + <rect x={40} y={0} width={30} height={10} stroke="#000" fill={colorScale.less} /> + <rect x={70} y={0} width={30} height={10} stroke="#000" fill={colorScale.middle} /> + <rect x={100} y={0} width={30} height={10} stroke="#000" fill={colorScale.greater} /> + <rect x={130} y={0} width={30} height={10} stroke="#000" fill={colorScale.greatest} /> + <text x={10} y={10} fontSize="10" dy="1.2em" textAnchor="middle"> + 0.0 + </text> + <text x={40} y={10} fontSize="10" dy="1.2em" textAnchor="middle"> + 0.2 + </text> + <text x={70} y={10} fontSize="10" dy="1.2em" textAnchor="middle"> + 0.4 + </text> + <text x={100} y={10} fontSize="10" dy="1.2em" textAnchor="middle"> + 0.6 + </text> + <text x={130} y={10} fontSize="10" dy="1.2em" textAnchor="middle"> + 0.8 + </text> + <text x={160} y={10} fontSize="10" dy="1.2em" textAnchor="middle"> + 1.0+ + </text> + </svg> + <svg width={170} height={25}> + <rect x={10} y={0} width={20} height={10} stroke="#000" fill={colorScale.not_significant} /> + <text x={35} y={0} fontSize="10" dy="1em" textAnchor="start"> + Not significant (p > 1e-3) + </text> + </svg> + </LegendWrapper> + ) +} + +const renderNumber = (number: number | undefined) => { + return number === undefined || number === null ? '-' : number.toPrecision(4) +} + +const renderNumberExponential = (number: number | undefined) => { + return number === undefined || number === null ? '-' : number.toExponential(3) +} + +const printAAorNA = (aa: string | null) => { + if (aa === null) { + return 'n/a' + } + return aa +} + +type RegionTooltipProps = { + region: RegionalMissenseConstraintRegion + isTranscriptWide: boolean +} + +const RegionTooltip = ({ region, isTranscriptWide }: RegionTooltipProps) => { + if (isTranscriptWide) { + return ( + <RegionAttributeList> + <div> + <dt>Missense observed/expected:</dt> + <dd>{`${renderNumber(region.obs_exp)} (${region.obs_mis}/${renderNumber( + region.exp_mis + )})`}</dd> + </div> + <br /> + <div>The observed/expected ratio for this gene is transcript-wide.</div> + </RegionAttributeList> + ) + } + return ( + <RegionAttributeList> + <div> + <dt>Coordinates:</dt> + <dd>{`${region.chrom}:${region.start}-${region.stop}`}</dd> + </div> + <div> + <dt>Amino acids:</dt> + <dd>{`${printAAorNA(region.aa_start)}-${printAAorNA(region.aa_stop)}`}</dd> + </div> + <div> + <dt>Missense observed/expected:</dt> + <dd>{`${renderNumber(region.obs_exp)} (${region.obs_mis}/${renderNumber( + region.exp_mis + )})`}</dd> + </div> + <div> + <dt>p-value:</dt> + <dd> + {renderNumberExponential(region.p_value)} + {region.p_value !== null && region.p_value > 0.001 && ' (not significant)'} + </dd> + </div> + </RegionAttributeList> + ) +} + +const SidePanel = styled.div` + display: flex; + align-items: center; + height: 100%; +` + +const TopPanel = styled.div` + display: flex; + justify-content: flex-end; + width: 100%; + margin-bottom: 5px; +` + +export type RegionalMissenseConstraint = { + has_no_rmc_evidence: boolean + passed_qc: boolean + regions: RegionalMissenseConstraintRegion[] +} + +type Props = { + regionalMissenseConstraint?: RegionalMissenseConstraint + gene: Gene +} + +type TrackProps = { + scalePosition: (input: number) => number + width: number +} + +const RegionalMissenseConstraintTrack = ({ regionalMissenseConstraint, gene }: Props) => { + if ( + !regionalMissenseConstraint || + regionalMissenseConstraint.regions === null || + (regionalMissenseConstraint.passed_qc === false && + regionalMissenseConstraint.has_no_rmc_evidence === false) + ) { + return ( + <Track + renderLeftPanel={() => ( + <SidePanel> + <span>Regional missense constraint</span> + <InfoButton topic="regional-constraint" /> + </SidePanel> + )} + > + {({ width }: { width: number }) => ( + <> + <PlotWrapper> + <svg height={35} width={width}> + <text x={width / 2} y={35 / 2} dy="1.0rem" textAnchor="middle"> + <tspan> + This gene was not searched for evidence of regional missense constraint. See our{' '} + </tspan> + <tspan fill="#0000ff"> + <Link to="/help">help page</Link> + </tspan> + <tspan> for additional information.</tspan> + </text> + </svg> + </PlotWrapper> + </> + )} + </Track> + ) + } + + // This transcript was searched, but no RMC evidence was found + // instead, use the available gene level constraint data to display a single + // region for the RMC track + if (regionalMissenseConstraint.has_no_rmc_evidence) { + // eslint-disable-next-line no-param-reassign + regionalMissenseConstraint.regions = [] + + if (gene.gnomad_constraint) { + // eslint-disable-next-line no-param-reassign + regionalMissenseConstraint.regions = [ + { + chrom: gene.chrom, + start: Math.min(gene.start, gene.stop), + stop: Math.max(gene.start, gene.stop), + obs_mis: gene.gnomad_constraint.obs_mis, + exp_mis: gene.gnomad_constraint.exp_mis, + obs_exp: gene.gnomad_constraint.oe_mis, + z_score: gene.gnomad_constraint.mis_z, + p_value: -0.01, + chisq_diff_null: undefined, + aa_start: null, + aa_stop: null, + }, + ] + } + } + + const constrainedExons = regionIntersections([ + regionalMissenseConstraint.regions, + gene.exons.filter((exon) => exon.feature_type === 'CDS'), + ]) + + return ( + <Wrapper> + <Track + renderLeftPanel={() => ( + <SidePanel> + <span>Regional missense constraint</span> + <InfoButton topic="regional-constraint" /> + </SidePanel> + )} + > + {({ scalePosition, width }: TrackProps) => ( + <> + <TopPanel> + <Legend /> + </TopPanel> + <PlotWrapper> + <svg height={55} width={width}> + {constrainedExons.map((region: RegionalMissenseConstraintRegion) => { + const startX = scalePosition(region.start) + const stopX = scalePosition(region.stop) + const regionWidth = stopX - startX + + return ( + <TooltipAnchor + key={`${region.start}-${region.stop}`} + // @ts-expect-error - from TooltipAnchor component of GBTK + region={region} + isTranscript={regionalMissenseConstraint.regions.length === 1} + tooltipComponent={RegionTooltip} + > + <g> + <rect + x={startX} + y={0} + width={regionWidth} + height={15} + fill={regionColor(region)} + stroke="black" + /> + </g> + </TooltipAnchor> + ) + })} + <g transform="translate(0,20)"> + {regionalMissenseConstraint.regions.map( + (region: RegionalMissenseConstraintRegion, index: number) => { + const startX = scalePosition(region.start) + const stopX = scalePosition(region.stop) + const regionWidth = stopX - startX + const midX = (startX + stopX) / 2 + // const offset = index * 15 + const offset = index * 0 + + return ( + <g key={`${region.start}-${region.stop}`}> + <line + x1={startX} + y1={2 + offset} + x2={startX} + y2={11 + offset} + stroke="#424242" + /> + <line + x1={startX} + y1={7 + offset} + x2={stopX} + y2={7 + offset} + stroke="#424242" + /> + <line + x1={stopX} + y1={2 + offset} + x2={stopX} + y2={11 + offset} + stroke="#424242" + /> + {regionWidth > 40 && ( + <> + <rect + x={midX - 15} + y={3 + offset} + width={30} + height={5} + fill="#fafafa" + /> + <text x={midX} y={8 + offset} dy="0.33em" textAnchor="middle"> + {region.obs_exp.toFixed(2)} + </text> + </> + )} + </g> + ) + } + )} + </g> + </svg> + </PlotWrapper> + </> + )} + </Track> + </Wrapper> + ) +} + +RegionalMissenseConstraintTrack.defaultProps = { + height: 15, +} + +export default RegionalMissenseConstraintTrack diff --git a/data-pipeline/src/data_pipeline/datasets/gnomad_v2/gnomad_v2_regional_missense_constraint.py b/data-pipeline/src/data_pipeline/datasets/gnomad_v2/gnomad_v2_regional_missense_constraint.py new file mode 100644 index 000000000..2dab02952 --- /dev/null +++ b/data-pipeline/src/data_pipeline/datasets/gnomad_v2/gnomad_v2_regional_missense_constraint.py @@ -0,0 +1,82 @@ +import hail as hl + + +def prepare_gnomad_v2_regional_missense_constraint(path): + ds = hl.read_table(path) + + # rename key field transcript_id to transcript to allow merging in genes pipeline + ds_with_rmc = ds.transmute(transcript_id=ds.transcript) + ds_with_rmc = ds_with_rmc.key_by("transcript_id") + ds_with_rmc = ds_with_rmc.drop("transcript") + + # explode then collect to rename fields for consistency with ExAC RMC + ds_with_rmc = ds_with_rmc.explode("regions") + ds_with_rmc = ds_with_rmc.select( + regions=hl.struct( + chrom=ds_with_rmc.regions.start_coordinate.contig, + start=hl.min(ds_with_rmc.regions.start_coordinate.position, ds_with_rmc.regions.stop_coordinate.position), + stop=hl.max(ds_with_rmc.regions.start_coordinate.position, ds_with_rmc.regions.stop_coordinate.position), + aa_start=ds_with_rmc.regions.start_aa, + aa_stop=ds_with_rmc.regions.stop_aa, + obs_mis=ds_with_rmc.regions.obs, + exp_mis=ds_with_rmc.regions.exp, + obs_exp=ds_with_rmc.regions.oe, + chisq_diff_null=ds_with_rmc.regions.chisq, + p_value=ds_with_rmc.regions.p, + ), + ) + ds_with_rmc = ds_with_rmc.group_by("transcript_id").aggregate(regions=hl.agg.collect(ds_with_rmc.row_value).regions) + + ds_with_rmc = ds_with_rmc.group_by("transcript_id").aggregate(regions_array=hl.agg.collect(ds_with_rmc.row_value)) + ds_with_rmc = ds_with_rmc.annotate(has_no_rmc_evidence=hl.bool(False)) + ds_with_rmc = ds_with_rmc.annotate( + passed_qc=hl.if_else( + hl.set(ds_with_rmc.globals.rmc_transcripts_qc).contains(ds_with_rmc.transcript_id), + hl.bool(True), + hl.bool(False), + ) + ) + ds_with_rmc = ds_with_rmc.select( + has_no_rmc_evidence=ds_with_rmc.has_no_rmc_evidence, + passed_qc=ds_with_rmc.passed_qc, + regions=hl.sorted(ds_with_rmc.regions_array[0].regions, lambda region: region.start), + ) + + # create a hailtable with a row for every transcript included in the no_rmc set + # the browser needs to be able to distinguish between transcripts that were + # searched and had no RMC evidence, vs those that were not searched, for display + # purposes + no_rmc_set = ds.globals.transcripts_no_rmc_all + no_rmc_list = list(no_rmc_set.collect()[0]) + ds_no_rmc = hl.utils.range_table(1) + ds_no_rmc = ds_no_rmc.annotate( + transcript_id=(hl.array(no_rmc_list)), + has_no_rmc_evidence=hl.bool(True), + passed_qc=hl.bool(False), + regions=hl.empty_array( + hl.tstruct( + chrom=hl.tstr, + start=hl.tint32, + stop=hl.tint32, + aa_start=hl.tstr, + aa_stop=hl.tstr, + obs_mis=hl.tint64, + exp_mis=hl.tfloat64, + obs_exp=hl.tfloat64, + chisq_diff_null=hl.tfloat64, + p_value=hl.tfloat64, + ) + ), + ) + ds_no_rmc = ds_no_rmc.explode(ds_no_rmc["transcript_id"]) + ds_no_rmc = ds_no_rmc.key_by("transcript_id") + ds_no_rmc = ds_no_rmc.drop("idx") + + # combine the hail table of those transcripts with evidence, and those transcripts + # searched without evidence + ds_all_searched = ds_with_rmc.union(ds_no_rmc) + + # Don't need the information in globals for the browser + ds_all_searched = ds_all_searched.select_globals() + + return ds_all_searched diff --git a/data-pipeline/src/data_pipeline/pipelines/genes.py b/data-pipeline/src/data_pipeline/pipelines/genes.py index 2ceddad60..c21c4e0c8 100644 --- a/data-pipeline/src/data_pipeline/pipelines/genes.py +++ b/data-pipeline/src/data_pipeline/pipelines/genes.py @@ -18,6 +18,9 @@ from data_pipeline.datasets.exac.exac_constraint import prepare_exac_constraint from data_pipeline.datasets.exac.exac_regional_missense_constraint import prepare_exac_regional_missense_constraint from data_pipeline.datasets.gnomad_v2.gnomad_v2_constraint import prepare_gnomad_v2_constraint +from data_pipeline.datasets.gnomad_v2.gnomad_v2_regional_missense_constraint import ( + prepare_gnomad_v2_regional_missense_constraint, +) from data_pipeline.pipelines.variant_cooccurrence_counts import ( annotate_table_with_variant_cooccurrence_counts, @@ -199,6 +202,14 @@ "/genes/homozygous_variant_cooccurrence_counts.ht", ) +pipeline.add_task( + "prepare_gnomad_v2_regional_missense_constraint", + prepare_gnomad_v2_regional_missense_constraint, + "/constraint/gnomad_v2_regional_missense_constraint.ht", + # TODO: before merging - update to a more permanent location for this data + {"path": "gs://gnomad-rgrant-data-pipeline/output/constraint/20230926_rmc_demo"}, +) + ############################################### # Annotate genes ############################################### @@ -256,6 +267,7 @@ def annotate_with_preferred_transcript(table_path): "exac_constraint": pipeline.get_task("prepare_exac_constraint"), "exac_regional_missense_constraint": pipeline.get_task("prepare_exac_regional_missense_constraint"), "gnomad_constraint": pipeline.get_task("prepare_gnomad_v2_constraint"), + "gnomad_v2_regional_missense_constraint": pipeline.get_task("prepare_gnomad_v2_regional_missense_constraint"), }, {"join_on": "preferred_transcript_id"}, ) diff --git a/graphql-api/src/graphql/types/constraint/gnomad-regional-missense-constraint.graphql b/graphql-api/src/graphql/types/constraint/gnomad-regional-missense-constraint.graphql new file mode 100644 index 000000000..310f3080f --- /dev/null +++ b/graphql-api/src/graphql/types/constraint/gnomad-regional-missense-constraint.graphql @@ -0,0 +1,18 @@ +type GnomadV2RegionalMissenseConstraintRegion { + chrom: String + start: Int + stop: Int + aa_start: String + aa_stop: String + obs_mis: Int + exp_mis: Float + obs_exp: Float + chisq_diff_null: Float + p_value: Float +} + +type GnomadV2RegionalMissenseConstraint { + has_no_rmc_evidence: Boolean + passed_qc: Boolean + regions: [GnomadV2RegionalMissenseConstraintRegion] +} diff --git a/graphql-api/src/graphql/types/gene.graphql b/graphql-api/src/graphql/types/gene.graphql index 3dd245618..6fe57f3ff 100644 --- a/graphql-api/src/graphql/types/gene.graphql +++ b/graphql-api/src/graphql/types/gene.graphql @@ -65,6 +65,8 @@ type Gene { pext: Pext gnomad_constraint: GnomadConstraint + gnomad_v2_regional_missense_constraint: GnomadV2RegionalMissenseConstraint + exac_constraint: ExacConstraint exac_regional_missense_constraint_regions: [ExacRegionalMissenseConstraintRegion!]