From 676d412b4cbecfe4fa255fbcf3c919bc5ac35335 Mon Sep 17 00:00:00 2001 From: Phil Darnowsky Date: Wed, 22 Jan 2025 15:53:53 -0500 Subject: [PATCH] fixup: add quality description and q-score filters to reads --- ...TandemRepeatAlleleSizeDistributionPlot.tsx | 30 +--- .../ShortTandemRepeatColorBySelect.tsx | 8 +- .../ShortTandemRepeatPage.tsx | 4 +- .../ShortTandemRepeatReads.tsx | 153 ++++++++++++++---- browser/src/ShortTandemRepeatPage/qScore.ts | 48 ++++++ reads/src/resolveShortTandemRepeatReads.js | 14 ++ reads/src/schema.js | 10 ++ 7 files changed, 207 insertions(+), 60 deletions(-) create mode 100644 browser/src/ShortTandemRepeatPage/qScore.ts diff --git a/browser/src/ShortTandemRepeatPage/ShortTandemRepeatAlleleSizeDistributionPlot.tsx b/browser/src/ShortTandemRepeatPage/ShortTandemRepeatAlleleSizeDistributionPlot.tsx index 8130d3a1d..95584d965 100644 --- a/browser/src/ShortTandemRepeatPage/ShortTandemRepeatAlleleSizeDistributionPlot.tsx +++ b/browser/src/ShortTandemRepeatPage/ShortTandemRepeatAlleleSizeDistributionPlot.tsx @@ -17,6 +17,7 @@ import { GenotypeQuality, qualityDescriptionLabels, } from './qualityDescription' +import { qScoreLabels, QScoreBin, qScoreKeys } from './qScore' // The 100% width/height container is necessary the component // to size to fit its container vs staying at its initial size. @@ -41,21 +42,6 @@ export type ScaleType = | 'linear-truncated-1000' | 'log' -export const qScoreKeys = [ - '0', - '0.1', - '0.2', - '0.3', - '0.4', - '0.5', - '0.6', - '0.7', - '0.8', - '0.9', - '1', -] as const - -export type QScoreBin = (typeof qScoreKeys)[number] export type ColorByValue = GenotypeQuality | QScoreBin | Sex | PopulationId | '' export type AlleleSizeDistributionItem = { @@ -112,20 +98,6 @@ const colorMap: Record> = { }, } as const -const qScoreLabels: Record = { - '0': '0 to 0.05', - '0.1': '0.05 to 0.15', - '0.2': '0.15 to 0.25', - '0.3': '0.25 to 0.35', - '0.4': '0.35 to 0.45', - '0.5': '0.45 to 0.55', - '0.6': '0.55 to 0.65', - '0.7': '0.65 to 0.75', - '0.8': '0.75 to 0.85', - '0.9': '0.85 to 0.95', - '1': '0.95 to 1', -} - const fixedLegendLabels: Partial>> = { quality_description: qualityDescriptionLabels, q_score: qScoreLabels, diff --git a/browser/src/ShortTandemRepeatPage/ShortTandemRepeatColorBySelect.tsx b/browser/src/ShortTandemRepeatPage/ShortTandemRepeatColorBySelect.tsx index 074dd4f5b..56c53283e 100644 --- a/browser/src/ShortTandemRepeatPage/ShortTandemRepeatColorBySelect.tsx +++ b/browser/src/ShortTandemRepeatPage/ShortTandemRepeatColorBySelect.tsx @@ -41,9 +41,13 @@ const ShortTandemRepeatColorBySelect = ({ } }} > - + {Object.entries(colorByLabels).map(([key, label]) => ( - + ))} diff --git a/browser/src/ShortTandemRepeatPage/ShortTandemRepeatPage.tsx b/browser/src/ShortTandemRepeatPage/ShortTandemRepeatPage.tsx index 28a471df4..9544f8c99 100644 --- a/browser/src/ShortTandemRepeatPage/ShortTandemRepeatPage.tsx +++ b/browser/src/ShortTandemRepeatPage/ShortTandemRepeatPage.tsx @@ -1,4 +1,4 @@ -import React, { SetStateAction, useState, Dispatch } from 'react' +import React, { useState } from 'react' import styled from 'styled-components' import { Badge, Button, ExternalLink, List, ListItem, Modal, Select } from '@gnomad/ui' @@ -16,7 +16,6 @@ import ShortTandemRepeatPopulationOptions from './ShortTandemRepeatPopulationOpt import ShortTandemRepeatColorBySelect from './ShortTandemRepeatColorBySelect' import ShortTandemRepeatAlleleSizeDistributionPlot, { ColorBy, - QScoreBin, Sex, ScaleType, AlleleSizeDistributionItem, @@ -38,6 +37,7 @@ import { import ShortTandemRepeatAdjacentRepeatSection from './ShortTandemRepeatAdjacentRepeatSection' import { PopulationId } from '@gnomad/dataset-metadata/gnomadPopulations' import { GenotypeQuality } from './qualityDescription' +import { QScoreBin } from './qScore' type ShortTandemRepeatReferenceRegion = { chrom: string diff --git a/browser/src/ShortTandemRepeatPage/ShortTandemRepeatReads.tsx b/browser/src/ShortTandemRepeatPage/ShortTandemRepeatReads.tsx index becb4d162..d9c8174ab 100644 --- a/browser/src/ShortTandemRepeatPage/ShortTandemRepeatReads.tsx +++ b/browser/src/ShortTandemRepeatPage/ShortTandemRepeatReads.tsx @@ -1,4 +1,13 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, + Dispatch, + SetStateAction, + ChangeEvent, +} from 'react' import styled from 'styled-components' import { Button, Input, Select } from '@gnomad/ui' @@ -11,7 +20,12 @@ import StatusMessage from '../StatusMessage' import useRequest from '../useRequest' import ControlSection from '../VariantPage/ControlSection' import { ShortTandemRepeat } from './ShortTandemRepeatPage' -import { GenotypeQuality, qualityDescriptionLabels } from './qualityDescription' +import { + GenotypeQuality, + qualityDescriptionLabels, + genotypeQualityKeys, +} from './qualityDescription' +import { qScoreKeys, QScoreBin, qScoreLabels, QScoreBinBounds, qScoreBinBounds } from './qScore' const ShortTandemRepeatReadImageWrapper = styled.div` width: 100%; @@ -144,7 +158,7 @@ const fetchNumReads = ({ datasetId, shortTandemRepeatId, filter }: any) => { variables: { datasetId, shortTandemRepeatId, - filter, + filter: parseReadsFilter(filter), }, }), method: 'POST', @@ -156,6 +170,15 @@ const fetchNumReads = ({ datasetId, shortTandemRepeatId, filter }: any) => { .then((response) => response.data.short_tandem_repeat_reads.num_reads) } +type ParsedReadsFilter = Omit & { + q_score: QScoreBinBounds | null +} + +const parseReadsFilter = (filter: Filters): ParsedReadsFilter => { + const binBounds = filter.q_score ? qScoreBinBounds[filter.q_score] : null + return { ...filter, q_score: binBounds } +} + const fetchReads = ({ datasetId, shortTandemRepeatId, filter, limit, offset }: any) => { return fetch('/reads/', { body: JSON.stringify({ @@ -185,7 +208,7 @@ const fetchReads = ({ datasetId, shortTandemRepeatId, filter, limit, offset }: a variables: { datasetId, shortTandemRepeatId, - filter, + filter: parseReadsFilter(filter), limit, offset, }, @@ -333,11 +356,11 @@ const ShortTandemRepeatReads = ({ ) } -const ShortTandemRepeatReadsAllelesFilterControlsWrapper = styled.div` +const ShortTandemRepeatReadsFilterControlsWrapper = styled.div` margin-bottom: 1em; ` -const ShortTandemRepeatReadsAllelesFilterControlWrapper = styled.div` +const ShortTandemRepeatReadsFilterControlWrapper = styled.div` margin-bottom: 0.5em; input { @@ -349,6 +372,23 @@ const Label = styled.label` padding-right: 1em; ` +type SharedFilters = { + population: string | null + sex: string | null +} + +type Filters = SharedFilters & { + alleles: + | { + repeat_unit: string | null + min_repeats: number | null + max_repeats: number | null + }[] + | null + q_score: QScoreBin | null + quality_description: GenotypeQuality | null +} + type ShortTandemRepeatReadsAllelesFilterControlsProps = { shortTandemRepeat: ShortTandemRepeat value: { @@ -357,20 +397,20 @@ type ShortTandemRepeatReadsAllelesFilterControlsProps = { max_repeats: number | null }[] maxRepeats: number - onChange: (...args: any[]) => any + onChangeCallback: (...args: any[]) => any alleleSizeDistributionRepeatUnits: string[] } const ShortTandemRepeatReadsAllelesFilterControls = ({ value, maxRepeats, - onChange, + onChangeCallback, alleleSizeDistributionRepeatUnits, }: ShortTandemRepeatReadsAllelesFilterControlsProps) => { return ( - + {[0, 1].map((alleleIndex) => ( - + Allele {alleleIndex + 1}:  {' '} {/* eslint-disable jsx-a11y/label-has-associated-control */} {alleleSizeDistributionRepeatUnits.length > 1 && ( @@ -381,7 +421,7 @@ const ShortTandemRepeatReadsAllelesFilterControls = ({ value={value[alleleIndex].repeat_unit || ''} onChange={(e: any) => { const newRepeatUnit = e.target.value - onChange( + onChangeCallback( value.map((v, i) => i === alleleIndex ? { ...v, repeat_unit: newRepeatUnit } : v ) @@ -407,7 +447,7 @@ const ShortTandemRepeatReadsAllelesFilterControls = ({ value={value[alleleIndex].min_repeats} onChange={(e: any) => { const newMinRepeats = Math.max(Math.min(Number(e.target.value), maxRepeats), 0) - onChange( + onChangeCallback( value.map((v, i) => i === alleleIndex ? { ...v, min_repeats: newMinRepeats } : v ) @@ -425,7 +465,7 @@ const ShortTandemRepeatReadsAllelesFilterControls = ({ value={value[alleleIndex].max_repeats} onChange={(e: any) => { const newMaxRepeats = Math.max(Math.min(Number(e.target.value), maxRepeats), 0) - onChange( + onChangeCallback( value.map((v, i) => i === alleleIndex ? { ...v, max_repeats: newMaxRepeats } : v ) @@ -434,28 +474,80 @@ const ShortTandemRepeatReadsAllelesFilterControls = ({ /> {/* eslint-enable jsx-a11y/label-has-associated-control */} - + ))} - + ) } -type Filters = { - population: string | null - sex: string | null - alleles: - | { - repeat_unit: string | null - min_repeats: number | null - max_repeats: number | null - }[] - | null +type ShortTandemRepeatReadsQualityFilterControlsProps = { + shortTandemRepeat: ShortTandemRepeat + filter: Filters + setFilter: Dispatch> +} + +const ShortTandemRepeatReadsQualityFilterControls = ({ + filter, + setFilter, +}: ShortTandemRepeatReadsQualityFilterControlsProps) => { + return ( + + + {' '} + + + + + + ) } type ShortTandemRepeatReadsContainerProps = { datasetId: string shortTandemRepeat: ShortTandemRepeat - filter: Omit + filter: SharedFilters maxRepeats: number alleleSizeDistributionRepeatUnits: string[] } @@ -487,6 +579,8 @@ const ShortTandemRepeatReadsContainer = ({ max_repeats: maxRepeats, }, ], + q_score: null, + quality_description: null, }) if (baseFilter.population !== filter.population || baseFilter.sex !== filter.sex) { @@ -501,12 +595,17 @@ const ShortTandemRepeatReadsContainer = ({ { + onChangeCallback={(newAllelesFilter) => { setFilter((prevFilter) => ({ ...prevFilter, alleles: newAllelesFilter })) }} maxRepeats={maxRepeats} alleleSizeDistributionRepeatUnits={alleleSizeDistributionRepeatUnits} /> + = { + '0': '0 to 0.05', + '0.1': '0.05 to 0.15', + '0.2': '0.15 to 0.25', + '0.3': '0.25 to 0.35', + '0.4': '0.35 to 0.45', + '0.5': '0.45 to 0.55', + '0.6': '0.55 to 0.65', + '0.7': '0.65 to 0.75', + '0.8': '0.75 to 0.85', + '0.9': '0.85 to 0.95', + '1': '0.95 to 1', +} + +export type QScoreBinBounds = { + min: number + max: number +} + +export const qScoreBinBounds: Record = { + '0': { min: 0, max: 0.05 }, + '0.1': { min: 0.05, max: 0.15 }, + '0.2': { min: 0.15, max: 0.25 }, + '0.3': { min: 0.25, max: 0.35 }, + '0.4': { min: 0.35, max: 0.45 }, + '0.5': { min: 0.45, max: 0.55 }, + '0.6': { min: 0.55, max: 0.65 }, + '0.7': { min: 0.65, max: 0.75 }, + '0.8': { min: 0.75, max: 0.85 }, + '0.9': { min: 0.85, max: 0.95 }, + '1': { min: 0.95, max: 1 }, +} diff --git a/reads/src/resolveShortTandemRepeatReads.js b/reads/src/resolveShortTandemRepeatReads.js index 742818284..4d8d65cc4 100644 --- a/reads/src/resolveShortTandemRepeatReads.js +++ b/reads/src/resolveShortTandemRepeatReads.js @@ -21,6 +21,20 @@ const buildWhere = ({ id, filter }) => { params[':sex'] = filter.sex } + if (filter.quality_description) { + where += ' AND `quality_description` = :quality_description' + params[':quality_description'] = filter.quality_description + } + + if (filter.q_score) { + // For most bins, we want reads with q-score less than the upper + // bound of the bin, but a read can have a q-score of 1.0, and we want + // those included in the 0.95-1 bin. + const adjusted_max = filter.q_score.max === 1 ? 1.1 : filter.q_score.max + where += ' AND `q` >= :min_q_score AND `q` < :max_q_score' + params[':min_q_score'] = filter.q_score.min + params[':max_q_score'] = adjusted_max + } if (filter.alleles && filter.alleles.length > 0) { if (filter.alleles.length > 2) { throw new UserVisibleError('Invalid alleles filter') diff --git a/reads/src/schema.js b/reads/src/schema.js index bc9f11ecc..ac8faefca 100644 --- a/reads/src/schema.js +++ b/reads/src/schema.js @@ -149,12 +149,22 @@ const ShortTandemRepeatReadsAlleleFilterType = new GraphQLInputObjectType({ }, }) +const ShortTandemRepeatReadsQScoreBinFilterType = new GraphQLInputObjectType({ + name: 'ShortTandemRepeatReadsQScoreBinFilterType', + fields: { + min: { type: GraphQLFloat }, + max: { type: GraphQLFloat }, + }, +}) + const ShortTandemRepeatReadsFilterType = new GraphQLInputObjectType({ name: 'ShortTandemRepeatReadsFilter', fields: { population: { type: GraphQLString }, sex: { type: GraphQLString }, alleles: { type: new GraphQLList(new GraphQLNonNull(ShortTandemRepeatReadsAlleleFilterType)) }, + q_score: { type: ShortTandemRepeatReadsQScoreBinFilterType }, + quality_description: { type: GraphQLString }, }, })