From 48d6b5478202bbb4c428abb07ea5820052d307c5 Mon Sep 17 00:00:00 2001 From: Elissa Alarmani Date: Wed, 13 Dec 2023 10:39:21 -0500 Subject: [PATCH 1/5] add v4 CNV tracks to GraphQL API --- .../src/graphql/resolvers/cnv-coverage.ts | 27 +- .../graphql/types/copy-number-variant.graphql | 12 + graphql-api/src/graphql/types/gene.graphql | 2 + graphql-api/src/graphql/types/region.graphql | 3 + .../src/queries/cnv-coverage-queries.ts | 258 +++++++++++++++--- 5 files changed, 267 insertions(+), 35 deletions(-) diff --git a/graphql-api/src/graphql/resolvers/cnv-coverage.ts b/graphql-api/src/graphql/resolvers/cnv-coverage.ts index 8c68b5ef7..4fbb11658 100644 --- a/graphql-api/src/graphql/resolvers/cnv-coverage.ts +++ b/graphql-api/src/graphql/resolvers/cnv-coverage.ts @@ -1,12 +1,33 @@ -import { fetchTrackCallableCoverageForGene } from '../../queries/cnv-coverage-queries' +import { + fetchDelBurdenCoverageForGene, + fetchDelBurdenCoverageForRegion, + fetchDupBurdenCoverageForGene, + fetchDupBurdenCoverageForRegion, + fetchTrackCallableCoverageForGene, + fetchTrackCallableCoverageForRegion, +} from '../../queries/cnv-coverage-queries' -const resolveTrackCallableCoverageInGene = async (obj: any, args: any, ctx: any) => { - return fetchTrackCallableCoverageForGene(ctx.esClient, args.dataset, obj) +const createResolver = (fetchCoverage: any) => async (obj: any, args: any, ctx: any) => { + return fetchCoverage(ctx.esClient, args.dataset, obj) } +const resolveTrackCallableCoverageInGene = createResolver(fetchTrackCallableCoverageForGene) +const resolveDelBurdenCoverageInGene = createResolver(fetchDelBurdenCoverageForGene) +const resolveDupBurdenCoverageInGene = createResolver(fetchDupBurdenCoverageForGene) +const resolveTrackCallableCoverageInRegion = createResolver(fetchTrackCallableCoverageForRegion) +const resolveDelBurdenCoverageInRegion = createResolver(fetchDelBurdenCoverageForRegion) +const resolveDupBurdenCoverageInRegion = createResolver(fetchDupBurdenCoverageForRegion) + const resolvers = { Gene: { cnv_track_callable_coverage: resolveTrackCallableCoverageInGene, + cnv_del_burden_coverage: resolveDelBurdenCoverageInGene, + cnv_dup_burden_coverage: resolveDupBurdenCoverageInGene, + }, + Region: { + cnv_track_callable_coverage: resolveTrackCallableCoverageInRegion, + cnv_del_burden_coverage: resolveDelBurdenCoverageInRegion, + cnv_dup_burden_coverage: resolveDupBurdenCoverageInRegion, }, } diff --git a/graphql-api/src/graphql/types/copy-number-variant.graphql b/graphql-api/src/graphql/types/copy-number-variant.graphql index f9fe1af07..0d7b2779b 100644 --- a/graphql-api/src/graphql/types/copy-number-variant.graphql +++ b/graphql-api/src/graphql/types/copy-number-variant.graphql @@ -48,4 +48,16 @@ type CopyNumberVariantDetails { type CNVTrackCallableCoverageBin { xpos: Float! percent_callable: Float + position: Float + contig: String +} + +type CNVDelBurdenCoverageBin{ + xpos: Float! + burden_del: Float +} + +type CNVDupBurdenCoverageBin{ + xpos: Float! + burden_dup: Float } \ No newline at end of file diff --git a/graphql-api/src/graphql/types/gene.graphql b/graphql-api/src/graphql/types/gene.graphql index dfd7bd187..3592d90d5 100644 --- a/graphql-api/src/graphql/types/gene.graphql +++ b/graphql-api/src/graphql/types/gene.graphql @@ -79,6 +79,8 @@ type Gene { coverage(dataset: DatasetId): FeatureCoverage! @cost(value: 5) mitochondrial_coverage(dataset: DatasetId!): [MitochondrialCoverageBin!] @cost(value: 5) cnv_track_callable_coverage(dataset: CopyNumberVariantDatasetId!): [CNVTrackCallableCoverageBin!] @cost(value: 5) + cnv_del_burden_coverage(dataset: CopyNumberVariantDatasetId!): [CNVDelBurdenCoverageBin!] @cost(value: 5) + cnv_dup_burden_coverage(dataset: CopyNumberVariantDatasetId!): [CNVDupBurdenCoverageBin!] @cost(value: 5) short_tandem_repeats(dataset: DatasetId!): [ShortTandemRepeat!]! @cost(value: 5) heterozygous_variant_cooccurrence_counts: [HeterozygousVariantCooccurrenceCounts!]! diff --git a/graphql-api/src/graphql/types/region.graphql b/graphql-api/src/graphql/types/region.graphql index 91fce5bac..0cb30d812 100644 --- a/graphql-api/src/graphql/types/region.graphql +++ b/graphql-api/src/graphql/types/region.graphql @@ -32,11 +32,14 @@ type Region { structural_variants(dataset: StructuralVariantDatasetId!): [StructuralVariant!]! @cost(value: 10) mitochondrial_variants(dataset: DatasetId!): [MitochondrialVariant!]! @cost(value: 10) copy_number_variants(dataset: CopyNumberVariantDatasetId!): [CopyNumberVariant!]! @cost(value: 10) + cnv_del_burden_coverage(dataset: CopyNumberVariantDatasetId!): [CNVDelBurdenCoverageBin!] @cost(value: 5) + cnv_dup_burden_coverage(dataset: CopyNumberVariantDatasetId!): [CNVDupBurdenCoverageBin!] @cost(value: 5) clinvar_variants: [ClinVarVariant!] @cost(value: 10) coverage(dataset: DatasetId!): RegionCoverage! mitochondrial_coverage(dataset: DatasetId!): [MitochondrialCoverageBin!] @cost(value: 5) + cnv_track_callable_coverage(dataset: CopyNumberVariantDatasetId!): [CNVTrackCallableCoverageBin!] @cost(value: 5) short_tandem_repeats(dataset: DatasetId!): [ShortTandemRepeat!]! @cost(value: 5) } diff --git a/graphql-api/src/queries/cnv-coverage-queries.ts b/graphql-api/src/queries/cnv-coverage-queries.ts index df63f93e9..3753e70f5 100644 --- a/graphql-api/src/queries/cnv-coverage-queries.ts +++ b/graphql-api/src/queries/cnv-coverage-queries.ts @@ -7,6 +7,8 @@ import { assertDatasetAndReferenceGenomeMatch } from './helpers/validation-helpe const COVERAGE_INDICES = { gnomad_cnv_r4: { track_callable: 'gnomad_v4_cnv_track_callable_coverage', + del_burden: 'gnomad_v4_cnv_del_burden', + dup_burden: 'gnomad_v4_cnv_dup_burden', }, } @@ -14,44 +16,122 @@ const COVERAGE_INDICES = { // Base query // ================================================================================================ -const fetchTrackCallableCoverage = async (esClient: any, { regions }: any) => { - const requestBody = { - query: { - bool: { - filter: [ - { - bool: { - should: regions.map(({ xstart, xstop }: any) => ({ - range: { xpos: { gte: xstart, lte: xstop } }, - })), - }, +const fetchTrackCallableCoverage = async (esClient: any, { index, contig, regions }: any) => { + try { + const response = await esClient.search({ + index, + type: '_doc', + size: 10000, + body: { + query: { + bool: { + filter: [ + { term: { contig } }, + { + bool: { + should: regions.map(({ start, stop }: any) => ({ + range: { position: { gte: start, lte: stop } }, + })), + }, + }, + ], }, - ], + }, }, - }, + }) + return response.body.hits.hits.map((hit: any) => ({ + xpos: hit._source.xpos, + percent_callable: Math.ceil(parseFloat(hit._source.percent_callable || 0) * 100) / 100, + position: hit._source.position, + contig: hit._source.contig + })) + } catch (error) { + throw new Error(`Couldn't fetch coverage, ${error}`) } +} - const response = await esClient.search({ - index: 'gnomad_v4_cnv_track_callable_coverage', - type: '_doc', - size: 2, - body: requestBody, - }) - return response.body.hits.hits.map((hit: any) => ({ - xpos: parseFloat(hit._source.xpos), - percent_callable: Math.ceil((hit._source.percent_callable || 0) * 100) / 100, - })) + +const fetchDelBurdenCoverage = async (esClient: any, { index, contig, regions }: any) => { + try { + const response = await esClient.search({ + index, + type: '_doc', + size: 10000, + body: { + query: { + bool: { + filter: [ + { term: { contig } }, + { + bool: { + should: regions.map(({ start, stop }: any) => ({ + range: { position: { gte: start, lte: stop } }, + })), + }, + }, + ], + }, + }, + }, + }) + return response.body.hits.hits.map((hit: any) => ({ + xpos: hit._source.xpos, + burden_del: parseFloat(hit._source.burden_del) , + position: hit._source.position, + contig: hit._source.contig + })) + } catch (error) { + throw new Error(`Couldn't fetch coverage, ${error}`) + } +} + +const fetchDupBurdenCoverage = async (esClient: any, { index, contig, regions }: any) => { + try { + const response = await esClient.search({ + index, + type: '_doc', + size: 10000, + body: { + query: { + bool: { + filter: [ + { term: { contig } }, + { + bool: { + should: regions.map(({ start, stop }: any) => ({ + range: { position: { gte: start, lte: stop } }, + })), + }, + }, + ], + }, + }, + }, + }) + return response.body.hits.hits.map((hit: any) => ({ + xpos: hit._source.xpos, + burden_dup: parseFloat(hit._source.burden_dup) , + position: hit._source.position, + contig: hit._source.contig + })) + } catch (error) { + throw new Error(`Couldn't fetch coverage, ${error}`) + } } // ================================================================================================ // Region queries // ================================================================================================ -export const fetchTrackCallableCoverageForRegion = (esClient: any, datasetId: any, region: any) => { +export const fetchTrackCallableCoverageForRegion = async ( + esClient: any, + datasetId: any, + region: any +) => { assertDatasetAndReferenceGenomeMatch(datasetId, region.reference_genome) - if (!datasetId.startsWith('gnomad_cnv_')) { - throw new UserVisibleError('Track callabe coverage is not available for non-CNVs') + if (!datasetId.startsWith('gnomad_cnv')) { + throw new UserVisibleError('Track callable coverage is not available for non-CNVs') } // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message @@ -60,11 +140,67 @@ export const fetchTrackCallableCoverageForRegion = (esClient: any, datasetId: an const regionSize = region.stop - region.start + 150 const bucketSize = Math.max(Math.floor(regionSize / 500), 1) - return fetchTrackCallableCoverage(esClient, { + const trackCallableCoverage = await fetchTrackCallableCoverage(esClient, { index: trackCallableCoverageIndex, + contig: region.chrom, + regions: [{ start: region.start - 75, stop: region.stop + 75 }], + bucketSize, + }) + + return trackCallableCoverage +} + +export const fetchDelBurdenCoverageForRegion = async ( + esClient: any, + datasetId: any, + region: any +) => { + assertDatasetAndReferenceGenomeMatch(datasetId, region.reference_genome) + + if (!datasetId.startsWith('gnomad_cnv')) { + throw new UserVisibleError('Deletion burden track coverage is not available for non-CNVs') + } + + // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message + const delBurdenCoverageIndex = COVERAGE_INDICES[datasetId] + + const regionSize = region.stop - region.start + 150 + const bucketSize = Math.max(Math.floor(regionSize / 500), 1) + + const delBurdenCoverage = await fetchDelBurdenCoverage(esClient, { + index: delBurdenCoverageIndex, + contig: region.chrom, regions: [{ start: region.start - 75, stop: region.stop + 75 }], bucketSize, }) + + return delBurdenCoverage +} + +export const fetchDupBurdenCoverageForRegion = async ( + esClient: any, + datasetId: any, + region: any +) => { + assertDatasetAndReferenceGenomeMatch(datasetId, region.reference_genome) + + if (!datasetId.startsWith('gnomad_cnv')) { + throw new UserVisibleError('Duplication burden track coverage is not available for non-CNVs') + } + + // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message + const dupBurdenCoverageIndex = COVERAGE_INDICES[datasetId] + + const regionSize = region.stop - region.start + 150 + const bucketSize = Math.max(Math.floor(regionSize / 500), 1) + + const dupBurdenCoverage = await fetchDupBurdenCoverage(esClient, { + index: dupBurdenCoverageIndex, + regions: [{ start: region.start - 75, stop: region.stop + 75 }], + bucketSize, + }) + + return dupBurdenCoverage } // ================================================================================================ @@ -78,8 +214,8 @@ export const _fetchTrackCallableCoverageForGene = async ( ) => { assertDatasetAndReferenceGenomeMatch(datasetId, gene.reference_genome) - if (!datasetId.startsWith('gnomad_cnv_')) { - throw new UserVisibleError('Track callabe coverage is not available for non-CNVs') + if (!datasetId.startsWith('gnomad_cnv')) { + throw new UserVisibleError('Track callable coverage is not available for non-CNVs') } const paddedExons = extendRegions(75, gene.exons) @@ -95,13 +231,71 @@ export const _fetchTrackCallableCoverageForGene = async ( const trackCallableCoverageIndex = COVERAGE_INDICES[datasetId] const trackCallableCoverage = await fetchTrackCallableCoverage(esClient, { index: trackCallableCoverageIndex, + contig: gene.chrom, regions: mergedExons, bucketSize, }) - return { - cnv_track_callable_coverage: trackCallableCoverage || [], - } + return trackCallableCoverage } export const fetchTrackCallableCoverageForGene = _fetchTrackCallableCoverageForGene + +export const _fetchDelBurdenCoverageForGene = async (esClient: any, datasetId: any, gene: any) => { + assertDatasetAndReferenceGenomeMatch(datasetId, gene.reference_genome) + + if (!datasetId.startsWith('gnomad_cnv')) { + throw new UserVisibleError('Deletion burden track coverage is not available for non-CNVs') + } + + const paddedExons = extendRegions(75, gene.exons) + + const mergedExons = mergeOverlappingRegions( + paddedExons.sort((a: any, b: any) => a.start - b.start) + ) + + const totalIntervalSize = totalRegionSize(mergedExons) + const bucketSize = Math.max(Math.floor(totalIntervalSize / 500), 1) + + // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message + const delBurdenCoverageIndex = COVERAGE_INDICES[datasetId] + const delBurdenCoverage = await fetchDelBurdenCoverage(esClient, { + index: delBurdenCoverageIndex, + contig: gene.chrom, + regions: mergedExons, + bucketSize, + }) + + return delBurdenCoverage +} + +export const fetchDelBurdenCoverageForGene = _fetchDelBurdenCoverageForGene + +export const _fetchDupBurdenCoverageForGene = async (esClient: any, datasetId: any, gene: any) => { + assertDatasetAndReferenceGenomeMatch(datasetId, gene.reference_genome) + + if (!datasetId.startsWith('gnomad_cnv')) { + throw new UserVisibleError('Duplicarion burden track coverage is not available for non-CNVs') + } + + const paddedExons = extendRegions(75, gene.exons) + + const mergedExons = mergeOverlappingRegions( + paddedExons.sort((a: any, b: any) => a.start - b.start) + ) + + const totalIntervalSize = totalRegionSize(mergedExons) + const bucketSize = Math.max(Math.floor(totalIntervalSize / 500), 1) + + // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message + const dupBurdenCoverageIndex = COVERAGE_INDICES[datasetId] + const dupBurdenCoverage = await fetchDupBurdenCoverage(esClient, { + index: dupBurdenCoverageIndex, + regions: mergedExons, + bucketSize, + }) + + return dupBurdenCoverage +} + +export const fetchDupBurdenCoverageForGene = _fetchDupBurdenCoverageForGene From f2de4404e7a29e221aed8cc850bf057ab9decb99 Mon Sep 17 00:00:00 2001 From: Elissa Alarmani Date: Wed, 13 Dec 2023 11:43:13 -0500 Subject: [PATCH 2/5] add percent callable track to gene pages --- ...rVariantsGenePercentCallableTrack.spec.tsx | 73 + ...NumberVariantsGenePercentCallableTrack.tsx | 85 + browser/src/GenePage/GenePage.spec.tsx | 12 + browser/src/GenePage/GenePage.tsx | 5 + ...antsGenePercentCallableTrack.spec.tsx.snap | 35 + .../__snapshots__/GenePage.spec.tsx.snap | 2208 ++++++++++++----- 6 files changed, 1826 insertions(+), 592 deletions(-) create mode 100644 browser/src/GenePage/CopyNumberVariantsGenePercentCallableTrack.spec.tsx create mode 100644 browser/src/GenePage/CopyNumberVariantsGenePercentCallableTrack.tsx create mode 100644 browser/src/GenePage/__snapshots__/CopyNumberVariantsGenePercentCallableTrack.spec.tsx.snap diff --git a/browser/src/GenePage/CopyNumberVariantsGenePercentCallableTrack.spec.tsx b/browser/src/GenePage/CopyNumberVariantsGenePercentCallableTrack.spec.tsx new file mode 100644 index 000000000..032160c60 --- /dev/null +++ b/browser/src/GenePage/CopyNumberVariantsGenePercentCallableTrack.spec.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { createRenderer } from 'react-test-renderer/shallow' + +import { jest, describe, expect, test } from '@jest/globals' +import { mockQueries } from '../../../tests/__helpers__/queries' +import Query, { BaseQuery } from '../Query' + +import CopyNumberVariantsGenePercentCallableTrack from './CopyNumberVariantsGenePercentCallableTrack' + +import { allDatasetIds, hasCopyNumberVariantCoverage } from '@gnomad/dataset-metadata/metadata' +import geneFactory from '../__factories__/Gene' + +jest.mock('../Query', () => { + const originalModule = jest.requireActual('../Query') + + return { + __esModule: true, + ...(originalModule as object), + default: jest.fn(), + BaseQuery: jest.fn(), + } +}) + +const { resetMockApiCalls, resetMockApiResponses, simulateApiResponse, setMockApiResponses } = + mockQueries() + +beforeEach(() => { + Query.mockImplementation( + jest.fn(({ query, children, operationName, variables }) => + simulateApiResponse('Query', query, children, operationName, variables) + ) + ) + ;(BaseQuery as any).mockImplementation( + jest.fn(({ query, children, operationName, variables }) => + simulateApiResponse('BaseQuery', query, children, operationName, variables) + ) + ) +}) + +afterEach(() => { + resetMockApiCalls() + resetMockApiResponses() +}) + +const datasetsWithCoverage = allDatasetIds.filter((datasetId) => + hasCopyNumberVariantCoverage(datasetId) +) + +describe.each(datasetsWithCoverage)( + 'CopyNumberVariantsGenePercentCallableTrack with dataset %s', + (datasetId) => { + test('queries with appropriate params', () => { + const gene = geneFactory.build() + setMockApiResponses({ + CopyNumberVariantsGenePercentCallableTrack: () => ({ + gene: { + cnv_track_callable_coverage: [], + }, + }), + }) + + const shallowRenderer = createRenderer() + shallowRenderer.render( + + ) + + expect(shallowRenderer.getRenderOutput()).toMatchSnapshot() + }) + } +) diff --git a/browser/src/GenePage/CopyNumberVariantsGenePercentCallableTrack.tsx b/browser/src/GenePage/CopyNumberVariantsGenePercentCallableTrack.tsx new file mode 100644 index 000000000..914a00c23 --- /dev/null +++ b/browser/src/GenePage/CopyNumberVariantsGenePercentCallableTrack.tsx @@ -0,0 +1,85 @@ +import React from 'react' + +import { referenceGenome } from '@gnomad/dataset-metadata/metadata' +// import CoverageTrack from '../CoverageTrack' +import Query from '../Query' +import DiscreteBarPlot from '../DiscreteBarPlot' + +type OwnProps = { + datasetId: string + chrom: string + start: number + stop: number +} + +// @ts-expect-error TS(2456) FIXME: Type alias 'Props' circularly references itself. +type Props = OwnProps & typeof CopyNumberVariantsGenePercentCallableTrack.defaultProps + +// @ts-expect-error TS(7022) FIXME: 'CopyNumberVariantsGenePercentCallableTrack' implicitly has type '... Remove this comment to see the full error message +const CopyNumberVariantsGenePercentCallableTrack = ({ datasetId, geneId }: Props) => { + const operationName = 'CopyNumberVariantsGenePercentCallableTrack' + const query = ` + query ${operationName}($geneId: String!, $datasetId: CopyNumberVariantDatasetId!, $referenceGenome: ReferenceGenomeId!) { + gene(gene_id: $geneId, reference_genome: $referenceGenome) { + cnv_track_callable_coverage(dataset: $datasetId) { + xpos + percent_callable + position + contig + } + start + stop + chrom + } + } +` + return ( + { + if (!data.gene || !data.gene.cnv_track_callable_coverage) { + return false + } + return data.gene && data.gene.cnv_track_callable_coverage + }} + > + {({ data }: any) => { + const transformedArray = data.gene.cnv_track_callable_coverage.map((item: any) => ({ + pos: item.position, + percent_callable: item.percent_callable, + xpos: item.xpos, + })) + transformedArray.sort((a: any, b: any) => a.xpos - b.xpos) + + const coverage = [ + { + color: 'rgb(70, 130, 180)', + buckets: transformedArray, + name: 'percent callable', + opacity: 0.7, + }, + ] + const geneStart = data.gene.start + const geneStop = data.gene.stop + const geneChrom = Number(data.gene.chrom) + + return ( + + ) + }} + + ) +} + +export default CopyNumberVariantsGenePercentCallableTrack diff --git a/browser/src/GenePage/GenePage.spec.tsx b/browser/src/GenePage/GenePage.spec.tsx index f4ace7de3..ff62fe628 100644 --- a/browser/src/GenePage/GenePage.spec.tsx +++ b/browser/src/GenePage/GenePage.spec.tsx @@ -74,6 +74,9 @@ forDatasetsNotMatching(svRegexp, 'GenePage with non-SV dataset "%s"', (datasetId CopyNumberVariantsInGene: () => ({ gene: { copy_number_variants: [] }, }), + CopyNumberVariantsGenePercentCallableTrack: () => ({ + gene: { cnv_track_callable_coverage: [] }, + }), }) ) @@ -146,6 +149,9 @@ forDatasetsMatching(cnvRegexp, 'GenePage with CNV dataset "%s"', (datasetId) => coverage: {}, }, }), + CopyNumberVariantsGenePercentCallableTrack: () => ({ + gene: { cnv_track_callable_coverage: [] }, + }), }) const tree = renderer.create( withDummyRouter() @@ -164,6 +170,9 @@ forDatasetsMatching(cnvRegexp, 'GenePage with CNV dataset "%s"', (datasetId) => coverage: {}, }, }), + CopyNumberVariantsGenePercentCallableTrack: () => ({ + gene: { cnv_track_callable_coverage: [] }, + }), }) renderer.create( withDummyRouter() @@ -208,6 +217,9 @@ describe.each([ CopyNumberVariantsInGene: () => ({ gene: { copy_number_variants: [] }, }), + CopyNumberVariantsGenePercentCallableTrack: () => ({ + gene: { cnv_track_callable_coverage: [] }, + }), }) renderer.create( withDummyRouter() diff --git a/browser/src/GenePage/GenePage.tsx b/browser/src/GenePage/GenePage.tsx index 96dc44b6c..b7a733f74 100644 --- a/browser/src/GenePage/GenePage.tsx +++ b/browser/src/GenePage/GenePage.tsx @@ -62,6 +62,7 @@ import { CopyNumberVariant, } from '../VariantPage/VariantPage' import CopyNumberVariantsInGene from './CopyNumberVariantsInGene' +import CopyNumberVariantsGenePercentCallableTrack from './CopyNumberVariantsGenePercentCallableTrack' export type Strand = '+' | '-' @@ -609,6 +610,10 @@ const GenePage = ({ datasetId, gene, geneId }: Props) => { zoomRegion={zoomRegion} /> )} + + {hasCopyNumberVariants(datasetId) && ( + + )} ) diff --git a/browser/src/GenePage/__snapshots__/CopyNumberVariantsGenePercentCallableTrack.spec.tsx.snap b/browser/src/GenePage/__snapshots__/CopyNumberVariantsGenePercentCallableTrack.spec.tsx.snap new file mode 100644 index 000000000..877944ee1 --- /dev/null +++ b/browser/src/GenePage/__snapshots__/CopyNumberVariantsGenePercentCallableTrack.spec.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CopyNumberVariantsGenePercentCallableTrack with dataset gnomad_cnv_r4 queries with appropriate params 1`] = ` + + [Function] + +`; diff --git a/browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap b/browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap index fe8936c5c..9bc2fa9ec 100644 --- a/browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap +++ b/browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap @@ -1465,275 +1465,787 @@ exports[`GenePage with CNV dataset "gnomad_cnv_r4" has no unexpected changes 1`] > No variants found - - - -`; - -exports[`GenePage with SV dataset "gnomad_sv_r2_1" has no unexpected changes 1`] = ` -
-
-
-
-

- FAKEGENE - - -

-
+
+ +
+
+ + +`; + +exports[`GenePage with SV dataset "gnomad_sv_r2_1" has no unexpected changes 1`] = ` +
+
+ +

+ Constraint not available for this + gene +

+
+
+ + Viewing full + gene + . + + +
+
+
+
+
+
    + + +
+
+
+
+
+ Fraction of individuals with coverage over 20 +
+
+
+
+ + + + + + + + 0.1 + + + + + + + + + + 0.2 + + + + + + + + + + 0.3 + + + + + + + + + + 0.4 + + + + + + + + + + 0.5 + + + + + + + + + + 0.6 + + + + + + + + + + 0.7 + + + + + + + + + + 0.8 + + + + + + + + + + 0.9 + + + + + + + + + + 1.0 + + + + + + + + + + + +
+
+
+
+
+ Include: +
    +
  • +
-
+ + +
  • - Variant co-occurrence - -
  • -
    -

    - Constraint not available for this - gene -

    + className="GenePage__LegendSwatch-sc-rcxzgc-9 gUeepi" + color="#424242" + height={4} + /> + + +
  • + +
  • +
    - - - Viewing full - gene - . - - -
    -
    -
      - +
      + + Positive strand +
      +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + No variants found +
    +
    +
    +
    +
      +
    • + + percent callable +
    • +
    @@ -10106,8 +11279,8 @@ exports[`GenePage with non-SV dataset "gnomad_cnv_r4" has no unexpected changes stroke="#333" x1={0} x2={799} - y1={190} - y2={190} + y1={207} + y2={207} /> @@ -10115,155 +11288,6 @@ exports[`GenePage with non-SV dataset "gnomad_cnv_r4" has no unexpected changes
    -
    - Include: -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    -
    -
    -
    -
    - - Positive strand -
    -
    -
    -
    - - - -
    -
    -
    -
    -
    -
    - No variants found -
    From 93d0c988b7108577e729e10be2150b9901e83dbf Mon Sep 17 00:00:00 2001 From: Elissa Alarmani Date: Wed, 13 Dec 2023 11:43:45 -0500 Subject: [PATCH 3/5] add cnv percent callable track to region pages --- ...ariantsRegionPercentCallableTrack.spec.tsx | 73 ++++++ ...mberVariantsRegionPercentCallableTrack.tsx | 75 ++++++ browser/src/RegionPage/RegionPage.tsx | 7 + ...tsRegionPercentCallableTrack.spec.tsx.snap | 34 +++ .../__snapshots__/RegionPage.spec.tsx.snap | 216 ++++++++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 browser/src/RegionPage/CopyNumberVariantsRegionPercentCallableTrack.spec.tsx create mode 100644 browser/src/RegionPage/CopyNumberVariantsRegionPercentCallableTrack.tsx create mode 100644 browser/src/RegionPage/__snapshots__/CopyNumberVariantsRegionPercentCallableTrack.spec.tsx.snap diff --git a/browser/src/RegionPage/CopyNumberVariantsRegionPercentCallableTrack.spec.tsx b/browser/src/RegionPage/CopyNumberVariantsRegionPercentCallableTrack.spec.tsx new file mode 100644 index 000000000..03d398b76 --- /dev/null +++ b/browser/src/RegionPage/CopyNumberVariantsRegionPercentCallableTrack.spec.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { createRenderer } from 'react-test-renderer/shallow' + +import { jest, describe, expect, test } from '@jest/globals' +import { mockQueries } from '../../../tests/__helpers__/queries' +import Query, { BaseQuery } from '../Query' + +import CopyNumberVariantsRegionPercentCallableTrack from './CopyNumberVariantsRegionPercentCallableTrack' + +import { allDatasetIds, hasCopyNumberVariantCoverage } from '@gnomad/dataset-metadata/metadata' + +jest.mock('../Query', () => { + const originalModule = jest.requireActual('../Query') + + return { + __esModule: true, + ...(originalModule as object), + default: jest.fn(), + BaseQuery: jest.fn(), + } +}) + +const { resetMockApiCalls, resetMockApiResponses, simulateApiResponse, setMockApiResponses } = + mockQueries() + +beforeEach(() => { + Query.mockImplementation( + jest.fn(({ query, children, operationName, variables }) => + simulateApiResponse('Query', query, children, operationName, variables) + ) + ) + ;(BaseQuery as any).mockImplementation( + jest.fn(({ query, children, operationName, variables }) => + simulateApiResponse('BaseQuery', query, children, operationName, variables) + ) + ) +}) + +afterEach(() => { + resetMockApiCalls() + resetMockApiResponses() +}) + +const datasetsWithCoverage = allDatasetIds.filter((datasetId) => + hasCopyNumberVariantCoverage(datasetId) +) + +describe.each(datasetsWithCoverage)( + 'CopyNumberVariantsRegionPercentCallableTrack with dataset %s', + (datasetId) => { + test('queries with appropriate params', () => { + setMockApiResponses({ + CopyNumberVariantsRegionPercentCallableTrack: () => ({ + region: { + cnv_track_callable_coverage: [], + }, + }), + }) + + const shallowRenderer = createRenderer() + shallowRenderer.render( + + ) + + expect(shallowRenderer.getRenderOutput()).toMatchSnapshot() + }) + } +) diff --git a/browser/src/RegionPage/CopyNumberVariantsRegionPercentCallableTrack.tsx b/browser/src/RegionPage/CopyNumberVariantsRegionPercentCallableTrack.tsx new file mode 100644 index 000000000..a6f84a17b --- /dev/null +++ b/browser/src/RegionPage/CopyNumberVariantsRegionPercentCallableTrack.tsx @@ -0,0 +1,75 @@ +import React from 'react' + +import { referenceGenome } from '@gnomad/dataset-metadata/metadata' +import Query from '../Query' +import DiscreteBarPlot from '../DiscreteBarPlot' + +type OwnProps = { + datasetId: string + chrom: string + start: number + stop: number +} + +// @ts-expect-error TS(2456) FIXME: Type alias 'Props' circularly references itself. +type Props = OwnProps & typeof CopyNumberVariantsRegionPercentCallableTrack.defaultProps + +// @ts-expect-error TS(7022) FIXME: 'CopyNumberVariantsRegionPercentCallableTrack' implicitly has type '... Remove this comment to see the full error message +const CopyNumberVariantsRegionPercentCallableTrack = ({ datasetId, chrom, start, stop }: Props) => { + const operationName = 'CopyNumberVariantsRegionPercentCallableTrack' + const query = ` + query ${operationName}($datasetId: CopyNumberVariantDatasetId!, $chrom: String!, $start: Int!, $stop: Int!, $referenceGenome: ReferenceGenomeId!) { + region(chrom: $chrom, start: $start, stop: $stop, reference_genome: $referenceGenome) { + cnv_track_callable_coverage(dataset: $datasetId) { + xpos + percent_callable + position + contig + } + } + } +` + return ( + { + return data.region && data.region.cnv_track_callable_coverage + }} + > + {({ data }: any) => { + const transformedArray = data.region.cnv_track_callable_coverage.map((item: any) => ({ + pos: item.position, + percent_callable: item.percent_callable, + xpos: item.xpos, + })) + transformedArray.sort((a: any, b: any) => a.xpos - b.xpos) + + const coverage = [ + { + color: 'rgb(70, 130, 180)', + buckets: transformedArray, + name: 'percent callable', + opacity: 0.7, + }, + ] + + return ( + + ) + }} + + ) +} + +export default CopyNumberVariantsRegionPercentCallableTrack diff --git a/browser/src/RegionPage/RegionPage.tsx b/browser/src/RegionPage/RegionPage.tsx index 534cd5260..5fae551bf 100644 --- a/browser/src/RegionPage/RegionPage.tsx +++ b/browser/src/RegionPage/RegionPage.tsx @@ -30,6 +30,7 @@ import RegionInfo from './RegionInfo' import RegularVariantsInRegion from './VariantsInRegion' import StructuralVariantsInRegion from './StructuralVariantsInRegion' import CopyNumberVariantsInRegion from './CopyNumberVariantsInRegion' +import CopyNumberVariantsRegionTrackCallable from './CopyNumberVariantsRegionPercentCallableTrack' const RegionInfoColumnWrapper = styled.div` display: flex; @@ -188,6 +189,12 @@ const RegionPage = ({ datasetId, region }: RegionPageProps) => { )} {variantsInRegion(datasetId, region)} + ) diff --git a/browser/src/RegionPage/__snapshots__/CopyNumberVariantsRegionPercentCallableTrack.spec.tsx.snap b/browser/src/RegionPage/__snapshots__/CopyNumberVariantsRegionPercentCallableTrack.spec.tsx.snap new file mode 100644 index 000000000..ecb173174 --- /dev/null +++ b/browser/src/RegionPage/__snapshots__/CopyNumberVariantsRegionPercentCallableTrack.spec.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CopyNumberVariantsRegionPercentCallableTrack with dataset gnomad_cnv_r4 queries with appropriate params 1`] = ` + + [Function] + +`; diff --git a/browser/src/RegionPage/__snapshots__/RegionPage.spec.tsx.snap b/browser/src/RegionPage/__snapshots__/RegionPage.spec.tsx.snap index 45a4967ff..0fddd8c8f 100644 --- a/browser/src/RegionPage/__snapshots__/RegionPage.spec.tsx.snap +++ b/browser/src/RegionPage/__snapshots__/RegionPage.spec.tsx.snap @@ -137,6 +137,12 @@ exports[`RegionPage with "exac" dataset has no unexpected changes for a mitochon } } /> + `; @@ -270,6 +276,12 @@ exports[`RegionPage with "exac" dataset has no unexpected changes for a non-mito } } /> + `; @@ -411,6 +423,12 @@ exports[`RegionPage with "gnomad_cnv_r4" dataset has no unexpected changes for a } } /> + `; @@ -555,6 +573,12 @@ exports[`RegionPage with "gnomad_cnv_r4" dataset has no unexpected changes for a } } /> + `; @@ -696,6 +720,12 @@ exports[`RegionPage with "gnomad_r2_1" dataset has no unexpected changes for a m } } /> + `; @@ -829,6 +859,12 @@ exports[`RegionPage with "gnomad_r2_1" dataset has no unexpected changes for a n } } /> + `; @@ -970,6 +1006,12 @@ exports[`RegionPage with "gnomad_r2_1_controls" dataset has no unexpected change } } /> + `; @@ -1103,6 +1145,12 @@ exports[`RegionPage with "gnomad_r2_1_controls" dataset has no unexpected change } } /> + `; @@ -1244,6 +1292,12 @@ exports[`RegionPage with "gnomad_r2_1_non_cancer" dataset has no unexpected chan } } /> + `; @@ -1377,6 +1431,12 @@ exports[`RegionPage with "gnomad_r2_1_non_cancer" dataset has no unexpected chan } } /> + `; @@ -1518,6 +1578,12 @@ exports[`RegionPage with "gnomad_r2_1_non_neuro" dataset has no unexpected chang } } /> + `; @@ -1651,6 +1717,12 @@ exports[`RegionPage with "gnomad_r2_1_non_neuro" dataset has no unexpected chang } } /> + `; @@ -1792,6 +1864,12 @@ exports[`RegionPage with "gnomad_r2_1_non_topmed" dataset has no unexpected chan } } /> + `; @@ -1925,6 +2003,12 @@ exports[`RegionPage with "gnomad_r2_1_non_topmed" dataset has no unexpected chan } } /> + `; @@ -2074,6 +2158,12 @@ exports[`RegionPage with "gnomad_r3" dataset has no unexpected changes for a mit } } /> + `; @@ -2215,6 +2305,12 @@ exports[`RegionPage with "gnomad_r3" dataset has no unexpected changes for a non } } /> + `; @@ -2364,6 +2460,12 @@ exports[`RegionPage with "gnomad_r3_controls_and_biobanks" dataset has no unexpe } } /> + `; @@ -2505,6 +2607,12 @@ exports[`RegionPage with "gnomad_r3_controls_and_biobanks" dataset has no unexpe } } /> + `; @@ -2654,6 +2762,12 @@ exports[`RegionPage with "gnomad_r3_non_cancer" dataset has no unexpected change } } /> + `; @@ -2795,6 +2909,12 @@ exports[`RegionPage with "gnomad_r3_non_cancer" dataset has no unexpected change } } /> + `; @@ -2944,6 +3064,12 @@ exports[`RegionPage with "gnomad_r3_non_neuro" dataset has no unexpected changes } } /> + `; @@ -3085,6 +3211,12 @@ exports[`RegionPage with "gnomad_r3_non_neuro" dataset has no unexpected changes } } /> + `; @@ -3234,6 +3366,12 @@ exports[`RegionPage with "gnomad_r3_non_topmed" dataset has no unexpected change } } /> + `; @@ -3375,6 +3513,12 @@ exports[`RegionPage with "gnomad_r3_non_topmed" dataset has no unexpected change } } /> + `; @@ -3524,6 +3668,12 @@ exports[`RegionPage with "gnomad_r3_non_v2" dataset has no unexpected changes fo } } /> + `; @@ -3665,6 +3815,12 @@ exports[`RegionPage with "gnomad_r3_non_v2" dataset has no unexpected changes fo } } /> + `; @@ -3806,6 +3962,12 @@ exports[`RegionPage with "gnomad_r4" dataset has no unexpected changes for a mit } } /> + `; @@ -3939,6 +4101,12 @@ exports[`RegionPage with "gnomad_r4" dataset has no unexpected changes for a non } } /> + `; @@ -4080,6 +4248,12 @@ exports[`RegionPage with "gnomad_sv_r2_1" dataset has no unexpected changes for } } /> + `; @@ -4224,6 +4398,12 @@ exports[`RegionPage with "gnomad_sv_r2_1" dataset has no unexpected changes for } } /> + `; @@ -4365,6 +4545,12 @@ exports[`RegionPage with "gnomad_sv_r2_1_controls" dataset has no unexpected cha } } /> + `; @@ -4509,6 +4695,12 @@ exports[`RegionPage with "gnomad_sv_r2_1_controls" dataset has no unexpected cha } } /> + `; @@ -4650,6 +4842,12 @@ exports[`RegionPage with "gnomad_sv_r2_1_non_neuro" dataset has no unexpected ch } } /> + `; @@ -4794,6 +4992,12 @@ exports[`RegionPage with "gnomad_sv_r2_1_non_neuro" dataset has no unexpected ch } } /> + `; @@ -4943,6 +5147,12 @@ exports[`RegionPage with "gnomad_sv_r4" dataset has no unexpected changes for a } } /> + `; @@ -5095,6 +5305,12 @@ exports[`RegionPage with "gnomad_sv_r4" dataset has no unexpected changes for a } } /> + `; From 4e172740b7a9e0c4fae9118d9f9f9ea9864e0e83 Mon Sep 17 00:00:00 2001 From: Elissa Alarmani Date: Wed, 13 Dec 2023 11:44:27 -0500 Subject: [PATCH 4/5] remove original cnv specific coverage track --- .../CopyNumberVariantsRegionCoverageTrack.tsx | 78 ------------------- 1 file changed, 78 deletions(-) delete mode 100644 browser/src/RegionPage/CopyNumberVariantsRegionCoverageTrack.tsx diff --git a/browser/src/RegionPage/CopyNumberVariantsRegionCoverageTrack.tsx b/browser/src/RegionPage/CopyNumberVariantsRegionCoverageTrack.tsx deleted file mode 100644 index 1aab97f70..000000000 --- a/browser/src/RegionPage/CopyNumberVariantsRegionCoverageTrack.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react' - -import { - DatasetId, - labelForDataset, - referenceGenome, - hasMitochondrialGenomeCoverage, -} from '@gnomad/dataset-metadata/metadata' -import CoverageTrack from '../CoverageTrack' -import Query from '../Query' -import StatusMessage from '../StatusMessage' - -const operationName = 'CopyNumberVariantsCoverageInRegion' -const query = ` -query ${operationName}($start: Int!, $stop: Int!, $datasetId: DatasetId!, $referenceGenome: ReferenceGenomeId!) { - region(chrom: $chrom, start: $start, stop: $stop, reference_genome: $referenceGenome) { - copy_number_variants_coverage(dataset: $datasetId) { - xpos - percent_callable - } - } -} -` - -type Props = { - datasetId: DatasetId - chrom: number - start: number - stop: number -} - -const CopyNumberVariantsRegionCoverageTrack = ({ datasetId, chrom, start, stop }: Props) => { - if (!hasMitochondrialGenomeCoverage(datasetId)) { - return ( - - Copy Number Variant exome coverage is not available in {labelForDataset(datasetId)} - - ) - } - - return ( - { - return data.region && data.region.copy_number_variant_coverage - }} - > - {({ data }: any) => { - const coverage = [ - { - color: 'rgb(115, 171, 61)', - buckets: data.region.copy_number_variant_coverage, - name: 'copy number variant coverage', // TODO - opacity: 0.7, - }, - ] - - return ( - `${chrom}-${start}-${stop}_coverage`} - height={190} - maxCoverage={3000} - datasetId={datasetId} - /> - ) - }} - - ) -} - -export default CopyNumberVariantsRegionCoverageTrack From eaedaa68e9903dec6e8f1e2fecbcbe71d2760f15 Mon Sep 17 00:00:00 2001 From: Elissa Alarmani Date: Wed, 13 Dec 2023 12:08:50 -0500 Subject: [PATCH 5/5] create discrete barplot for percent callable track --- browser/src/CoverageTrack.tsx | 8 +- browser/src/DiscreteBarPlot.tsx | 172 ++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 browser/src/DiscreteBarPlot.tsx diff --git a/browser/src/CoverageTrack.tsx b/browser/src/CoverageTrack.tsx index d0f7b6983..29c9fdb0c 100644 --- a/browser/src/CoverageTrack.tsx +++ b/browser/src/CoverageTrack.tsx @@ -16,7 +16,7 @@ const TopPanel = styled.div` width: 100%; ` -const LegendWrapper = styled.ul` +export const LegendWrapper = styled.ul` display: flex; flex-direction: row; padding: 0; @@ -24,12 +24,12 @@ const LegendWrapper = styled.ul` list-style-type: none; ` -const LegendItem = styled.li` +export const LegendItem = styled.li` display: flex; margin-left: 1em; ` -const LegendSwatch = styled.span` +export const LegendSwatch = styled.span` display: inline-block; width: 1em; height: 1em; @@ -46,7 +46,7 @@ const LegendSwatch = styled.span` } ` -type LegendProps = { +export type LegendProps = { datasets: { color: string name: string diff --git a/browser/src/DiscreteBarPlot.tsx b/browser/src/DiscreteBarPlot.tsx new file mode 100644 index 000000000..01aed239c --- /dev/null +++ b/browser/src/DiscreteBarPlot.tsx @@ -0,0 +1,172 @@ +import React, { useRef } from 'react' +import styled from 'styled-components' +import { scaleLinear } from 'd3-scale' +import { LegendWrapper, LegendItem, LegendSwatch, LegendProps } from './CoverageTrack' + +// @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 { Button } from '@gnomad/ui' +import { AxisLeft } from '@vx/axis' + +const TopPanel = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; + width: 100%; +` +const TitlePanel = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + padding-right: 40px; +` +type PercentCallableValues = { + pos: number + percent_callable: number + xpos: number +} + +type GroupedData = { + startPos: number + endPos: number + percent_callable: number +} + +const Legend = ({ datasets }: LegendProps) => ( + + {datasets.map((dataset) => ( + + {/* @ts-expect-error TS(2769) FIXME: No overload matches this call. */} + + {dataset.name} + + ))} + +) + +const groupDiscreteData = (buckets: PercentCallableValues[]): GroupedData[] => { + const groupedData: GroupedData[] = [] + + buckets.forEach((entry) => { + const prevEntry = groupedData.length > 0 ? groupedData[groupedData.length - 1] : null + + if (prevEntry && prevEntry.percent_callable === entry.percent_callable) { + prevEntry.endPos = entry.pos + 1 + } else { + groupedData.push({ + startPos: entry.pos, + endPos: entry.pos, + percent_callable: entry.percent_callable, + }) + } + }) + return groupedData +} + +const margin = { + top: 7, + left: 60, +} + +const DiscreteBarPlot = ({ + datasets, + height, + regionStart, + regionStop, + chrom, +}: { + datasets: { color: string; buckets: PercentCallableValues[]; name: string; opacity: number }[] + height: number + regionStart: number + regionStop: number + chrom: number +}) => { + const groupedData = groupDiscreteData(datasets[0].buckets) + + const plotHeight = height + margin.top + const yScale = scaleLinear().domain([0, 1]).range([plotHeight, margin.top]) + + const plotRef = useRef(null) + + const exportPlot = () => { + if (plotRef.current) { + const serializer = new XMLSerializer() + const data = serializer.serializeToString(plotRef.current) + const blob = new Blob(['\r\n', data], { + type: 'image/svg+xml;charset=utf-8', + }) + + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `${chrom}-${regionStart}-${regionStop}_percent_callable.svg` + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + URL.revokeObjectURL(url) + } + } + + return ( + Percent Callable } + renderTopPanel={() => ( + + + + + )} + > + {({ width }: { width: number }) => { + const plotWidth = width - margin.left + + const xDomain = [regionStart - 75, regionStop + 75] + + const xScale = scaleLinear().domain(xDomain).range([0, plotWidth]) + return ( +
    + + ({ + dx: '-0.25em', + dy: '0.25em', + fill: '#000', + fontSize: 10, + textAnchor: 'end', + })} + scale={yScale} + stroke="#333" + /> + + {/* eslint-disable react/no-array-index-key */} + {groupedData.map((entry: GroupedData, index: number) => { + return ( + + ) + })} + + + +
    + ) + }} + + ) +} +export default DiscreteBarPlot