From 9998780b45d26125d0d38eb64259f3e2500c0b34 Mon Sep 17 00:00:00 2001 From: Riley Grant Date: Wed, 8 May 2024 10:45:35 -0500 Subject: [PATCH] refactor(browser): update merge ancestry to prefer joint data --- .../mergeExomeAndGenomeData.spec.ts | 83 +++++++++++++++++-- .../VariantList/mergeExomeAndGenomeData.ts | 40 ++++++--- .../VariantPage/GnomadPopulationsTable.tsx | 49 ++--------- browser/src/VariantPage/VariantPage.tsx | 2 +- 4 files changed, 114 insertions(+), 60 deletions(-) diff --git a/browser/src/VariantList/mergeExomeAndGenomeData.spec.ts b/browser/src/VariantList/mergeExomeAndGenomeData.spec.ts index f2fb43d69..02ba4ffd5 100644 --- a/browser/src/VariantList/mergeExomeAndGenomeData.spec.ts +++ b/browser/src/VariantList/mergeExomeAndGenomeData.spec.ts @@ -4,13 +4,12 @@ import { populationFactory, variantFactory } from '../__factories__/Variant' import { Population } from '../VariantPage/VariantPage' import { - mergeExomeAndGenomePopulationData, + mergeExomeGenomeAndJointPopulationData, mergeExomeAndGenomeData, } from './mergeExomeAndGenomeData' -import { PopulationId } from '@gnomad/dataset-metadata/gnomadPopulations' type AncestryGroupShorthand = { - id: PopulationId + id: string value: number } @@ -37,7 +36,7 @@ const createAncestryGroupObjects = ( return geneticAncestryGroupObjects } -describe('mergeExomeAndGenomePopulationData', () => { +describe('mergeExomeGenomeAndJointPopulationData', () => { it('returns expected values when exomes and genomes have the same populations', () => { const geneticAncestryGroupObjects = createAncestryGroupObjects( [ @@ -54,7 +53,10 @@ describe('mergeExomeAndGenomePopulationData', () => { genome: { populations: geneticAncestryGroupObjects }, }) - const result = mergeExomeAndGenomePopulationData(testVariant.exome!, testVariant.genome!) + const result = mergeExomeGenomeAndJointPopulationData({ + exomePopulations: testVariant.exome!.populations, + genomePopulations: testVariant.genome!.populations, + }) const expected = [ { ac: 2, ac_hemi: 4, ac_hom: 6, an: 20, id: 'afr' }, @@ -91,7 +93,10 @@ describe('mergeExomeAndGenomePopulationData', () => { genome: { populations: genomeGeneticAncestryGroupObjects }, }) - const result = mergeExomeAndGenomePopulationData(testVariant.exome!, testVariant.genome!) + const result = mergeExomeGenomeAndJointPopulationData({ + exomePopulations: testVariant.exome!.populations, + genomePopulations: testVariant.genome!.populations, + }) const expected = [ { ac: 9, ac_hemi: 11, ac_hom: 13, an: 90, id: 'afr' }, @@ -129,7 +134,10 @@ describe('mergeExomeAndGenomePopulationData', () => { genome: { populations: genomeGeneticAncestryGroupObjects }, }) - const result = mergeExomeAndGenomePopulationData(testVariant.exome!, testVariant.genome!) + const result = mergeExomeGenomeAndJointPopulationData({ + exomePopulations: testVariant.exome!.populations, + genomePopulations: testVariant.genome!.populations, + }) const expected = [ { ac: 17, ac_hemi: 19, ac_hom: 21, an: 170, id: 'afr' }, @@ -166,7 +174,10 @@ describe('mergeExomeAndGenomePopulationData', () => { genome: { populations: genomeGeneticAncestryGroupObjects }, }) - const result = mergeExomeAndGenomePopulationData(testVariant.exome!, testVariant.genome!) + const result = mergeExomeGenomeAndJointPopulationData({ + exomePopulations: testVariant.exome!.populations, + genomePopulations: testVariant.genome!.populations, + }) const expected = [ { ac: 33, ac_hemi: 35, ac_hom: 37, an: 330, id: 'eur' }, @@ -176,6 +187,62 @@ describe('mergeExomeAndGenomePopulationData', () => { expect(result).toStrictEqual(expected) }) + + it('returns expected values when joint values are present', () => { + const exomeGeneticAncestryGroupObjects = createAncestryGroupObjects( + [ + { id: 'eur', value: 1 }, + { id: 'afr', value: 2 }, + { id: 'remaining', value: 4 }, + ], + false + ) + + const genomeGeneticAncestryGroupObjects = createAncestryGroupObjects( + [ + { id: 'afr', value: 8 }, + { id: 'remaining', value: 16 }, + { id: 'eur', value: 32 }, + ], + false + ) + + const jointGeneticAncestryGroupObjects = [ + { ac: 16, hemizygote_count: 17, homozygote_count: 18, an: 160, id: 'afr' }, + { ac: 16, hemizygote_count: 17, homozygote_count: 18, an: 160, id: 'afr_XX' }, + { ac: 16, hemizygote_count: 17, homozygote_count: 18, an: 160, id: 'afr_YY' }, + { ac: 32, hemizygote_count: 33, homozygote_count: 34, an: 320, id: 'remaining' }, + { ac: 64, hemizygote_count: 65, homozygote_count: 66, an: 640, id: 'eur' }, + { ac: 128, hemizygote_count: 129, homozygote_count: 130, an: 1280, id: 'mid' }, + ] + + const testVariant = variantFactory.build({ + variant_id: 'test_variant', + exome: { populations: exomeGeneticAncestryGroupObjects }, + genome: { populations: genomeGeneticAncestryGroupObjects }, + joint: { populations: jointGeneticAncestryGroupObjects as Population[] }, + }) + + const result = mergeExomeGenomeAndJointPopulationData({ + exomePopulations: testVariant.exome!.populations, + genomePopulations: testVariant.genome!.populations, + jointPopulations: testVariant.joint!.populations, + }) + + const expectedJointGeneticAncestryGroupObjects = createAncestryGroupObjects( + [ + { id: 'afr', value: 16 }, + { id: 'afr_XX', value: 16 }, + { id: 'afr_YY', value: 16 }, + { id: 'remaining', value: 32 }, + { id: 'eur', value: 64 }, + { id: 'mid', value: 128 }, + ], + true + ) + + expect(result).toStrictEqual(expectedJointGeneticAncestryGroupObjects) + }) }) describe('mergeExomeAndGenomeData', () => { diff --git a/browser/src/VariantList/mergeExomeAndGenomeData.ts b/browser/src/VariantList/mergeExomeAndGenomeData.ts index baeeb8944..28ea6651c 100644 --- a/browser/src/VariantList/mergeExomeAndGenomeData.ts +++ b/browser/src/VariantList/mergeExomeAndGenomeData.ts @@ -1,16 +1,34 @@ import { Filter } from '../QCFilter' -import { Population, SequencingType, Variant } from '../VariantPage/VariantPage' +import { Population, Variant } from '../VariantPage/VariantPage' // safe math on possibly null values const add = (n1: any, n2: any) => (n1 || 0) + (n2 || 0) -export const mergeExomeAndGenomePopulationData = ( - exome: SequencingType, - genome: SequencingType -) => { +export const mergeExomeGenomeAndJointPopulationData = ({ + exomePopulations = [], + genomePopulations = [], + jointPopulations = null, +}: { + exomePopulations: Population[] + genomePopulations: Population[] + jointPopulations?: Population[] | null +}) => { + if (jointPopulations) { + return ( + jointPopulations + // filter to remove duplicate XX an XY keys from joint populations array + .filter((item, index, self) => index === self.findIndex((t) => t.id === item.id)) + .map((jointPopulation) => ({ + ...jointPopulation, + ac_hemi: jointPopulation.hemizygote_count, + ac_hom: jointPopulation.homozygote_count, + })) + ) + } + const populations: { [key: string]: Population } = {} - exome.populations.forEach((exomePopulation: Population) => { + exomePopulations.forEach((exomePopulation: Population) => { populations[exomePopulation.id] = { id: exomePopulation.id, ac: exomePopulation.ac, @@ -20,7 +38,7 @@ export const mergeExomeAndGenomePopulationData = ( } }) - genome.populations.forEach((genomePopulation: Population) => { + genomePopulations.forEach((genomePopulation: Population) => { if (genomePopulation.id in populations) { const entry = populations[genomePopulation.id] populations[genomePopulation.id] = { @@ -108,10 +126,10 @@ export const mergeExomeAndGenomeData = (variants: Variant[]): MergedVariant[] => const genomeFilters: Filter[] = genomeOrNone.filters const combinedFilters = exomeFilters.concat(genomeFilters) - const combinedPopulations = mergeExomeAndGenomePopulationData( - exomeOrNone as SequencingType, - genomeOrNone as SequencingType - ) + const combinedPopulations = mergeExomeGenomeAndJointPopulationData({ + exomePopulations: exomeOrNone.populations, + genomePopulations: genomeOrNone.populations, + }) return { ...variant, diff --git a/browser/src/VariantPage/GnomadPopulationsTable.tsx b/browser/src/VariantPage/GnomadPopulationsTable.tsx index 4ad93396f..e27e2701f 100644 --- a/browser/src/VariantPage/GnomadPopulationsTable.tsx +++ b/browser/src/VariantPage/GnomadPopulationsTable.tsx @@ -7,6 +7,7 @@ import { GNOMAD_POPULATION_NAMES } from '@gnomad/dataset-metadata/gnomadPopulati import { DatasetId, hasV2Genome } from '@gnomad/dataset-metadata/metadata' import { PopulationsTable } from './PopulationsTable' import { Population } from './VariantPage' +import { mergeExomeGenomeAndJointPopulationData } from '../VariantList/mergeExomeAndGenomeData' const ControlSection = styled.div` margin-top: 1em; @@ -16,38 +17,6 @@ const ControlSection = styled.div` } ` -/** - * Merge frequency information for multiple populations with the same ID. - * This is used to add exome and genome population frequencies. - * - * @param {Object[]} populations Array of populations. - */ -const mergePopulations = (populations: any) => { - const indices = {} - const merged = [] - - for (let i = 0; i < populations.length; i += 1) { - const pop = populations[i] - - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message - const popIndex = indices[pop.id] - if (popIndex === undefined) { - merged.push({ ...pop }) - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message - indices[pop.id] = merged.length - 1 - } else { - merged[popIndex].ac += pop.ac - merged[popIndex].an += pop.an - if (pop.ac_hemi !== null) { - merged[popIndex].ac_hemi += pop.ac_hemi - } - merged[popIndex].ac_hom += pop.ac_hom - } - } - - return merged -} - const addPopulationNames = (populations: any) => { return populations.map((pop: any) => { let name @@ -137,15 +106,15 @@ export class GnomadPopulationsTable extends Component< this.props const { includeExomes, includeGenomes } = this.state - let includedPopulations: any = [] - if (includeExomes) { - includedPopulations = includedPopulations.concat(exomePopulations) - } - if (includeGenomes) { - includedPopulations = includedPopulations.concat(genomePopulations) - } + const mergedPopulations = mergeExomeGenomeAndJointPopulationData({ + exomePopulations: includeExomes ? exomePopulations : [], + genomePopulations: includeGenomes ? genomePopulations : [], + }) + + const mergedPopulationsWithNames = addPopulationNames(mergedPopulations) + const mergedNestedPopulationsWithNames = nestPopulations(mergedPopulationsWithNames) + let populations = mergedNestedPopulationsWithNames - let populations = nestPopulations(addPopulationNames(mergePopulations(includedPopulations))) if (hasV2Genome(datasetId) && includeGenomes) { populations = populations.map((pop) => { if (pop.id === 'eas') { diff --git a/browser/src/VariantPage/VariantPage.tsx b/browser/src/VariantPage/VariantPage.tsx index e5f584dea..3fb711e35 100644 --- a/browser/src/VariantPage/VariantPage.tsx +++ b/browser/src/VariantPage/VariantPage.tsx @@ -149,7 +149,7 @@ export type Population = { ac_hemi?: number | null ac_hom?: number hemizygote_count?: number | null - homozygote_count?: number | null + homozygote_count?: number } export type LocalAncestryPopulation = {