diff --git a/seqr/fixtures/1kg_project.json b/seqr/fixtures/1kg_project.json index 1a0bbc2944..7f2b231ff1 100644 --- a/seqr/fixtures/1kg_project.json +++ b/seqr/fixtures/1kg_project.json @@ -919,8 +919,8 @@ "sample_id": "NA19679_S", "is_active": true, "elasticsearch_index":null, - "tissue_type": "M", - "data_source": "muscle_samples.tsv.gz", + "tissue_type": "F", + "data_source": "fibs_samples.tsv.gz", "loaded_date": "2017-02-05T06:42:55.397Z" } }, @@ -1376,7 +1376,7 @@ "fields": { "sample": 151, "rank": 0, - "gene_id": "ENSG00000106554", + "gene_id": "ENSG00000268903", "p_value": 1.08E-56, "z_score": 12.34, "chrom": "7", @@ -1416,7 +1416,7 @@ "fields": { "sample": 151, "rank": 3, - "gene_id": "ENSG00000106554", + "gene_id": "ENSG00000268903", "p_value": 0.1, "z_score": 12.34, "chrom": "7", @@ -1436,7 +1436,7 @@ "fields": { "sample": 151, "rank": 1, - "gene_id": "ENSG00000106554", + "gene_id": "ENSG00000268903", "p_value": 0.0001, "z_score": 12.34, "chrom": "7", @@ -1456,7 +1456,7 @@ "fields": { "sample": 151, "rank": 2, - "gene_id": "ENSG00000106554", + "gene_id": "ENSG00000268903", "p_value": 0.0001, "z_score": 12.34, "chrom": "7", @@ -1475,7 +1475,7 @@ "pk": 6, "fields": { "sample": 152, - "gene_id": "ENSG00000106554", + "gene_id": "ENSG00000268903", "rank": 0, "p_value": 0.001, "z_score": 12.34, @@ -1496,7 +1496,7 @@ "fields": { "sample": 152, "rank": 1, - "gene_id": "ENSG00000106554", + "gene_id": "ENSG00000268903", "p_value": 0.2, "z_score": 12.34, "chrom": "7", diff --git a/seqr/views/apis/individual_api_tests.py b/seqr/views/apis/individual_api_tests.py index 10736f66c4..37f7931431 100644 --- a/seqr/views/apis/individual_api_tests.py +++ b/seqr/views/apis/individual_api_tests.py @@ -999,17 +999,17 @@ def test_get_individual_rna_seq_data(self): }, }, 'spliceOutliers': { - 'ENSG00000106554': mock.ANY, + 'ENSG00000268903': mock.ANY, }, }}) self.assertDictEqual( { - 'chrom': '7', 'deltaPsi': 0.85, 'end': 132886973, 'geneId': 'ENSG00000106554', 'isSignificant': True, + 'chrom': '7', 'deltaPsi': 0.85, 'end': 132886973, 'geneId': 'ENSG00000268903', 'isSignificant': True, 'pValue': 1.08e-56, 'rareDiseaseSamplesTotal': 20, 'rareDiseaseSamplesWithJunction': 1, 'readCount': 1297, 'start': 132885746, 'strand': '*', 'type': 'psi5', 'zScore': 12.34, 'tissueType': 'F', }, - response_json['rnaSeqData'][INDIVIDUAL_GUID]['spliceOutliers']['ENSG00000106554'][0] + response_json['rnaSeqData'][INDIVIDUAL_GUID]['spliceOutliers']['ENSG00000268903'][0] ) self.assertSetEqual(set(response_json['genesById'].keys()), {'ENSG00000135953', 'ENSG00000268903'}) @@ -1026,7 +1026,7 @@ def test_get_individual_rna_seq_data_is_significant(self): self.assertEqual(2, len(significant_outliers)) self.assertListEqual( [{field: outlier[field] for field in ['start', 'end', 'pValue', 'tissueType', 'isSignificant']} - for outlier in response_rnaseq_data['spliceOutliers']['ENSG00000106554']], + for outlier in response_rnaseq_data['spliceOutliers']['ENSG00000268903']], [{'start': 132885746, 'end': 132886973, 'pValue': 1.08e-56, 'tissueType': 'F', 'isSignificant': True}, {'start': 1001, 'end': 2001, 'pValue': 0.1, 'tissueType': 'F', 'isSignificant': False}, {'start': 3000, 'end': 4000, 'pValue': 0.0001, 'tissueType': 'F', 'isSignificant': True}, diff --git a/seqr/views/apis/saved_variant_api_tests.py b/seqr/views/apis/saved_variant_api_tests.py index b99f355a44..a28f0de0f6 100644 --- a/seqr/views/apis/saved_variant_api_tests.py +++ b/seqr/views/apis/saved_variant_api_tests.py @@ -172,8 +172,10 @@ def test_saved_variant_data(self): 'outliers': { 'ENSG00000135953': { 'geneId': 'ENSG00000135953', 'zScore': 7.31, 'pValue': 0.00000000000948, 'pAdjust': 0.00000000781, - 'isSignificant': True, - }}, + 'tissueType': None, 'isSignificant': True, + } + }, + 'spliceOutliers': {}, }}) # include project tag types diff --git a/seqr/views/apis/variant_search_api_tests.py b/seqr/views/apis/variant_search_api_tests.py index b27e338b29..05b1c20314 100644 --- a/seqr/views/apis/variant_search_api_tests.py +++ b/seqr/views/apis/variant_search_api_tests.py @@ -99,7 +99,10 @@ 'VFD0000025_1248367227_r0390_10': expected_functional_tag, 'VFD0000026_1248367227_r0390_10': expected_functional_tag, }, 'locusListsByGuid': {LOCUS_LIST_GUID: {'intervals': mock.ANY}}, - 'rnaSeqData': {'I000001_na19675': {'outliers': {'ENSG00000268903': mock.ANY}}}, + 'rnaSeqData': { + 'I000001_na19675': {'outliers': {'ENSG00000268903': mock.ANY}, 'spliceOutliers': {'ENSG00000268903': mock.ANY}}, + 'I000003_na19679': {'outliers': {}, 'spliceOutliers': {'ENSG00000268903': mock.ANY}}, + }, 'phenotypeGeneScores': { 'I000001_na19675': {'ENSG00000268903': {'exomiser': EXPECTED_EXOMISER_DATA}}, 'I000002_na19678': {'ENSG00000268903': {'lirical': EXPECTED_LIRICAL_DATA}}, @@ -142,6 +145,7 @@ def _get_compound_het_es_variants(results_model, **kwargs): return deepcopy(COMP_HET_VARAINTS), 1 +@mock.patch('seqr.views.utils.orm_to_json_utils.RnaSeqSpliceOutlier.MAX_SIGNIFICANT_OUTLIER_NUM', 2) @mock.patch('seqr.views.utils.permissions_utils.safe_redis_get_json', lambda *args: None) class VariantSearchAPITest(object): @@ -164,6 +168,24 @@ def _assert_expected_search_context(self, response_json): self.assertEqual(response_json['familiesByGuid']['F000001_1']['displayName'], '1') self.assertEqual(response_json['familiesByGuid']['F000001_1']['analysisStatus'], 'Q') + def _assert_expected_rnaseq_response(self, response_json): + self.assertDictEqual( + response_json['rnaSeqData']['I000001_na19675']['outliers']['ENSG00000268903'], + {'geneId': 'ENSG00000268903', 'isSignificant': True, 'pAdjust': 1.39e-09, 'pValue': 5.88e-10, + 'tissueType': None, 'zScore': 7.08} + ) + self.assertListEqual( + sorted(response_json['rnaSeqData']['I000001_na19675']['spliceOutliers']['ENSG00000268903'], key=lambda d: d['start']), + [{'chrom': '7', 'deltaPsi': 0.85, 'end': 4000, 'geneId': 'ENSG00000268903', 'isSignificant': True, + 'pValue': 0.0001, 'rareDiseaseSamplesTotal': 20, 'rareDiseaseSamplesWithJunction': 1, 'readCount': 1297, + 'start': 3000, 'strand': '*', 'tissueType': 'F', 'type': 'psi5', 'zScore': 12.34}, + {'chrom': '7', 'deltaPsi': 0.85, 'end': 8000, 'geneId': 'ENSG00000268903', 'isSignificant': True, + 'pValue': 0.001, 'rareDiseaseSamplesTotal': 20, 'rareDiseaseSamplesWithJunction': 1, 'readCount': 1297, + 'start': 7000, 'strand': '*', 'tissueType': 'M', 'type': 'psi5', 'zScore': 12.34}, + {'chrom': '7', 'deltaPsi': 0.85, 'end': 132886973, 'geneId': 'ENSG00000268903', 'isSignificant': True, + 'pValue': 1.08e-56, 'rareDiseaseSamplesTotal': 20, 'rareDiseaseSamplesWithJunction': 1, 'readCount': 1297, + 'start': 132885746, 'strand': '*', 'tissueType': 'F', 'type': 'psi5', 'zScore': 12.34}] + ) def _assert_expected_results_family_context(self, response_json, locus_list_detail=False): self._assert_expected_results_context(response_json, locus_list_detail=locus_list_detail) @@ -188,7 +210,9 @@ def _assert_expected_results_family_context(self, response_json, locus_list_deta self.assertEqual(len(response_json['familyNotesByGuid']), 3) self.assertSetEqual(set(response_json['familyNotesByGuid']['FAN000001_1'].keys()), FAMILY_NOTE_FIELDS) - def _assert_expected_results_context(self, response_json, has_pa_detail=True, locus_list_detail=False): + self._assert_expected_rnaseq_response(response_json) + + def _assert_expected_results_context(self, response_json, has_pa_detail=True, locus_list_detail=False, rnaseq=True): gene_fields = {'locusListGuids'} gene_fields.update(GENE_VARIANT_FIELDS) basic_gene_id = next(gene_id for gene_id in ['ENSG00000268903', 'ENSG00000233653'] if gene_id in response_json['genesById']) @@ -222,6 +246,8 @@ def _assert_expected_results_context(self, response_json, has_pa_detail=True, lo if response_json['variantFunctionalDataByGuid']: self.assertSetEqual(set(next(iter(response_json['variantFunctionalDataByGuid'].values())).keys()), FUNCTIONAL_FIELDS) + if rnaseq: + self._assert_expected_rnaseq_response(response_json) @mock.patch('seqr.utils.middleware.logger.error') @mock.patch('seqr.views.apis.variant_search_api.get_variant_query_gene_counts') @@ -424,7 +450,7 @@ def test_query_variants(self, mock_get_variants, mock_get_gene_counts, mock_erro }) expected_search_response['search']['totalResults'] = 1 self.assertDictEqual(response_json, expected_search_response) - self._assert_expected_results_context(response_json, has_pa_detail=False) + self._assert_expected_results_context(response_json, has_pa_detail=False, rnaseq=False) mock_error_logger.assert_not_called() # Test cross-project discovery for analyst users @@ -507,7 +533,6 @@ def _get_variants(results_model, **kwargs): response = self.client.post(url, content_type='application/json', data=json.dumps(body)) self.assertEqual(response.status_code, 200) response_json = response.json() - self.assertSetEqual(set(response_json.keys()), set(EXPECTED_SEARCH_RESPONSE.keys())) self.assertDictEqual(response_json, EXPECTED_SEARCH_RESPONSE) self._assert_expected_results_context(response_json) self.assertSetEqual( diff --git a/seqr/views/utils/variant_utils.py b/seqr/views/utils/variant_utils.py index e80b180193..c42e33c7cd 100644 --- a/seqr/views/utils/variant_utils.py +++ b/seqr/views/utils/variant_utils.py @@ -1,13 +1,13 @@ from collections import defaultdict from django.contrib.postgres.aggregates import ArrayAgg -from django.db.models import F, Value +from django.db.models import F import logging import redis from matchmaker.models import MatchmakerSubmissionGenes, MatchmakerSubmission from reference_data.models import TranscriptInfo from seqr.models import SavedVariant, VariantSearchResults, Family, LocusList, LocusListInterval, LocusListGene, \ - RnaSeqOutlier, RnaSeqTpm, PhenotypePrioritization, Project + RnaSeqTpm, PhenotypePrioritization, Project from seqr.utils.search.utils import get_variants_for_variant_ids from seqr.utils.gene_utils import get_genes_for_variants from seqr.views.utils.json_to_orm_utils import update_model_from_json @@ -131,19 +131,9 @@ def _add_locus_lists(projects, genes, add_list_detail=False, user=None): def _get_rna_seq_outliers(gene_ids, family_guids): - # TODO change to get_json_for_rna_seq_outliers in issue #3324 - data_by_individual_gene = defaultdict(lambda: {'outliers': {}}) - - outlier_data = get_json_for_queryset( - RnaSeqOutlier.objects.filter( - gene_id__in=gene_ids, p_adjust__lt=RnaSeqOutlier.SIGNIFICANCE_THRESHOLD, sample__individual__family__guid__in=family_guids), - nested_fields=[{'fields': ('sample', 'individual', 'guid'), 'key': 'individualGuid'}], - additional_values={'isSignificant': Value(True)}, - ) - for data in outlier_data: - data_by_individual_gene[data.pop('individualGuid')]['outliers'][data['geneId']] = data + filters = {'gene_id__in': gene_ids, 'sample__individual__family__guid__in': family_guids} - return data_by_individual_gene + return get_json_for_rna_seq_outliers(filters) def get_phenotype_prioritization(family_guids, gene_ids=None): diff --git a/ui/pages/Project/reducers.js b/ui/pages/Project/reducers.js index 029c039e0f..b00c493ee3 100644 --- a/ui/pages/Project/reducers.js +++ b/ui/pages/Project/reducers.js @@ -301,9 +301,11 @@ export const searchMmeMatches = submissionGuid => (dispatch) => { } export const loadRnaSeqData = individualGuid => (dispatch, getState) => { - const data = getState().rnaSeqDataByIndividual[individualGuid] - // If variants were loaded for the individual, the significant gene data will be loaded but not all the needed data - if (!data?.outliers || Object.values(data.outliers).every(({ isSignificant }) => isSignificant)) { + const { outliers, spliceOutliers } = getState().rnaSeqDataByIndividual[individualGuid] || {} + // If variants were loaded for the individual, the significant data were loaded but not the non-significant ones + if (!outliers || !spliceOutliers || (Object.values(outliers).every(({ isSignificant }) => isSignificant) && + Object.values(spliceOutliers).flat().every(({ isSignificant }) => isSignificant)) + ) { dispatch({ type: REQUEST_RNA_SEQ_DATA }) new HttpRequestHelper(`/api/individual/${individualGuid}/rna_seq_data`, (responseJson) => { diff --git a/ui/pages/Search/fixtures.js b/ui/pages/Search/fixtures.js index 4c2728887b..fa71898fc5 100644 --- a/ui/pages/Search/fixtures.js +++ b/ui/pages/Search/fixtures.js @@ -18,7 +18,11 @@ export const LOCUS_LIST = { locusListGuid: LOCUS_LIST_GUID, name: "2017 Monogenic IBD Gene List", numEntries: 60, - parsedItems: { items: [{ geneId: 'ENSG00000164458' }], itemMap: { 'TTN': { geneId: 'ENSG00000164458', symbol: 'TTN' } } } + parsedItems: { items: [{ geneId: 'ENSG00000164458' }], itemMap: { 'TTN': { geneId: 'ENSG00000164458', symbol: 'TTN' } } }, + intervals: [ + {'chrom': '1', 'end': 7300, 'genomeVersion': '37', 'locusListGuid': 'LL00132_2017_monogenic_ibd_gen', 'locusListIntervalGuid': 'LLI0000012_test_list_edit4545_', 'start': 7200}, + {'chrom': '3', 'end': 3000, 'genomeVersion': '37', 'locusListGuid': 'LL00132_2017_monogenic_ibd_gen', 'locusListIntervalGuid': 'LLI0000013_a_new_list325_3000', 'start': 25}, + ] } export const STATE = { @@ -100,6 +104,7 @@ export const STATE = { ], sampleGuids: [], sex: 'F', + familyGuid: FAMILY_GUID, }, I021475_na19675: { affected: 'A', @@ -212,6 +217,13 @@ export const STATE = { }, }, locusListsByGuid: { [LOCUS_LIST_GUID]: LOCUS_LIST }, + rnaSeqDataByIndividual: { I021474_na19679: { + outliers: {ENSG00000136758: {geneId: "ENSG00000136758", isSignificant: true, pAdjust: 0.000225907356686287, pValue: 2.69828505929319e-9, tissueType: "M", zScore: 5.62}}, + spliceOutliers: { ENSG00000136758: [ + {chrom: "10", deltaPsi: 0.56, end: 27114400, geneId: "ENSG00000136758", isSignificant: true, pValue: 2.1234e-10, rareDiseaseSamplesTotal: 171, rareDiseaseSamplesWithJunction: 1, readCount: 1208, start: 27114300, strand: "*", tissueType: "F", type: "psi5", zScore: 2.96}, + {chrom: "11", deltaPsi: 0.56, end: 27114400, geneId: "ENSG00000136758", isSignificant: true, pValue: 2.1234e-10, rareDiseaseSamplesTotal: 171, rareDiseaseSamplesWithJunction: 1, readCount: 1208, start: 27114300, strand: "*", tissueType: "F", type: "psi5", zScore: 2.96}, + ]}, + }}, projectsByGuid: { [PROJECT_GUID] : { createdDate: '2016-05-16T05:37:08.634Z', diff --git a/ui/redux/selectors.js b/ui/redux/selectors.js index fc4d1a0435..05c666fe24 100644 --- a/ui/redux/selectors.js +++ b/ui/redux/selectors.js @@ -360,6 +360,20 @@ export const getSearchGeneBreakdownValues = createSelector( })), ) +const groupDataNestedByChrom = (initialData, groupedData, nestedKey) => groupedData.reduce( + (acc, data) => { + const { chrom } = data + if (!acc[chrom]) { + acc[chrom] = {} + } + if (!acc[chrom][nestedKey]) { + acc[chrom][nestedKey] = [] + } + acc[chrom][nestedKey].push(data) + return acc + }, initialData, +) + export const getLocusListIntervalsByChromProject = createSelector( getProjectsByGuid, getLocusListsByGuid, @@ -368,16 +382,7 @@ export const getLocusListIntervalsByChromProject = createSelector( const projectIntervals = locusListGuids.map(locusListGuid => locusListsByGuid[locusListGuid]).reduce( (acc2, { intervals = [] }) => [...acc2, ...intervals], [], ) - projectIntervals.forEach((interval) => { - if (!acc[interval.chrom]) { - acc[interval.chrom] = {} - } - if (!acc[interval.chrom][projectGuid]) { - acc[interval.chrom][projectGuid] = [] - } - acc[interval.chrom][projectGuid].push(interval) - }) - return acc + return groupDataNestedByChrom(acc, projectIntervals, projectGuid) }, {}, ), ) @@ -419,18 +424,27 @@ export const getRnaSeqSignificantJunctionData = createSelector( getGenesById, getIndividualsByGuid, getRnaSeqDataByIndividual, - (genesById, individualsByGuid, rnaSeqDataByIndividual) => Object.entries(rnaSeqDataByIndividual).reduce( - (acc, [individualGuid, rnaSeqData]) => (rnaSeqData.spliceOutliers ? { - ...acc, - [individualGuid]: Object.values(rnaSeqData.spliceOutliers).flat().filter(({ isSignificant }) => isSignificant) + (genesById, individualsByGuid, rnaSeqDataByIndividual) => Object.entries(rnaSeqDataByIndividual || {}).reduce( + (acc, [individualGuid, rnaSeqData]) => { + const individualData = Object.values(rnaSeqData.spliceOutliers || {}).flat() + .filter(({ isSignificant }) => isSignificant) .sort((a, b) => a.pValue - b.pValue) .map(({ geneId, chrom, start, end, strand, type, ...cols }) => ({ geneSymbol: (genesById[geneId] || {}).geneSymbol || geneId, idField: `${geneId}-${chrom}-${start}-${end}-${strand}-${type}`, familyGuid: individualsByGuid[individualGuid].familyGuid, + individualName: individualsByGuid[individualGuid].displayName, individualGuid, ...{ geneId, chrom, start, end, strand, type, ...cols }, - })), - } : acc), {}, + })) + return (individualData.length > 0 ? { ...acc, [individualGuid]: individualData } : acc) + }, {}, + ), +) + +export const getSpliceOutliersByChromFamily = createSelector( + getRnaSeqSignificantJunctionData, + spliceDataByIndiv => Object.values(spliceDataByIndiv).reduce( + (acc, spliceData) => (groupDataNestedByChrom(acc, spliceData, spliceData[0].familyGuid)), {}, ), ) diff --git a/ui/redux/selectors.test.js b/ui/redux/selectors.test.js index 5f33df2504..294fb06713 100644 --- a/ui/redux/selectors.test.js +++ b/ui/redux/selectors.test.js @@ -6,6 +6,8 @@ import { getSearchGeneBreakdownValues, getSelectableTagTypesByProject, getUserOptions, + getLocusListIntervalsByChromProject, + getSpliceOutliersByChromFamily, } from './selectors' import {FAMILY_GUID, GENE_ID, SEARCH, SEARCH_HASH, STATE} from "../pages/Search/fixtures"; @@ -47,3 +49,39 @@ test('getUserOptions', () => { expect(Object.keys(options).length).toEqual(7) expect(options[1]).toEqual({ key: '4MW8vPtmHG', value: '4MW8vPtmHG', text: 'Mekdes (mgetaneh@broadinstitute.org)'}) }) + +test('getLocusListIntervalsByChromProject', () => { + expect(getLocusListIntervalsByChromProject(STATE, {})).toEqual({ + ['1']: { + 'R0237_1000_genomes_demo': [ + {'chrom': '1', 'end': 7300, 'genomeVersion': '37', 'locusListGuid': 'LL00132_2017_monogenic_ibd_gen', 'locusListIntervalGuid': 'LLI0000012_test_list_edit4545_', 'start': 7200}, + ], + }, + ['3']: { + 'R0237_1000_genomes_demo': [ + {'chrom': '3', 'end': 3000, 'genomeVersion': '37', 'locusListGuid': 'LL00132_2017_monogenic_ibd_gen', 'locusListIntervalGuid': 'LLI0000013_a_new_list325_3000', 'start': 25}, + ], + } + }) +}) + +test('getSpliceOutliersByChromFamily', () => { + expect(getSpliceOutliersByChromFamily(STATE, {})).toEqual({ + ['10']: { + 'F011652_1': [ + { + familyGuid: "F011652_1", geneSymbol: "ENSG00000136758", idField: "ENSG00000136758-10-27114300-27114400-*-psi5", individualGuid: "I021474_na19679", individualName: "", + chrom: "10", deltaPsi: 0.56, end: 27114400, geneId: "ENSG00000136758", isSignificant: true, pValue: 2.1234e-10, rareDiseaseSamplesTotal: 171, rareDiseaseSamplesWithJunction: 1, readCount: 1208, start: 27114300, strand: "*", tissueType: "F", type: "psi5", zScore: 2.96, + }, + ], + }, + ['11']: { + 'F011652_1': [ + { + familyGuid: "F011652_1", geneSymbol: "ENSG00000136758", idField: "ENSG00000136758-11-27114300-27114400-*-psi5", individualGuid: "I021474_na19679", individualName: "", + chrom: "11", deltaPsi: 0.56, end: 27114400, geneId: "ENSG00000136758", isSignificant: true, pValue: 2.1234e-10, rareDiseaseSamplesTotal: 171, rareDiseaseSamplesWithJunction: 1, readCount: 1208, start: 27114300, strand: "*", tissueType: "F", type: "psi5", zScore: 2.96, + }, + ], + } + }) +}) diff --git a/ui/shared/components/panel/variants/Annotations.jsx b/ui/shared/components/panel/variants/Annotations.jsx index fe83db33fe..974d34c25d 100644 --- a/ui/shared/components/panel/variants/Annotations.jsx +++ b/ui/shared/components/panel/variants/Annotations.jsx @@ -4,16 +4,55 @@ import { connect } from 'react-redux' import styled from 'styled-components' import { Popup, Label, Icon } from 'semantic-ui-react' -import { getGenesById, getLocusListIntervalsByChromProject, getFamiliesByGuid, getUser } from 'redux/selectors' +import { getGenesById, getLocusListIntervalsByChromProject, getFamiliesByGuid, getUser, getSpliceOutliersByChromFamily } + from 'redux/selectors' import { HorizontalSpacer, VerticalSpacer } from '../../Spacers' import SearchResultsLink from '../../buttons/SearchResultsLink' import Modal from '../../modal/Modal' import { ButtonLink, HelpIcon } from '../../StyledComponents' +import RnaSeqJunctionOutliersTable from '../../table/RnaSeqJunctionOutliersTable' import { getOtherGeneNames } from '../genes/GeneDetail' import Transcripts from './Transcripts' import VariantGenes, { LocusListLabels } from './VariantGene' -import { getLocus, has37Coords, Sequence, ProteinSequence, TranscriptLink } from './VariantUtils' -import { GENOME_VERSION_37, GENOME_VERSION_38, getVariantMainTranscript, SVTYPE_LOOKUP, SVTYPE_DETAILS, SCREEN_LABELS } from '../../../utils/constants' +import { getLocus, has37Coords, Sequence, ProteinSequence, TranscriptLink, getOverlappedIntervals } from './VariantUtils' +import { + GENOME_VERSION_37, GENOME_VERSION_38, getVariantMainTranscript, SVTYPE_LOOKUP, SVTYPE_DETAILS, SCREEN_LABELS, + RNASEQ_JUNCTION_PADDING, +} from '../../../utils/constants' + +const BaseSpliceOutlierLabel = React.memo(({ variant, spliceOutliersByFamily }) => { + if (!spliceOutliersByFamily || spliceOutliersByFamily.length < 1) { + return null + } + + const overlappedOutliers = getOverlappedIntervals(variant, spliceOutliersByFamily, fGuid => fGuid, + RNASEQ_JUNCTION_PADDING) + + if (overlappedOutliers.length < 1) { + return null + } + + return ( + RNA splice} color="pink" />} + content={} + size="tiny" + wide + hoverable + /> + ) +}) + +BaseSpliceOutlierLabel.propTypes = { + spliceOutliersByFamily: PropTypes.object, + variant: PropTypes.object, +} + +const mapSpliceOutliersStateToProps = (state, ownProps) => ({ + spliceOutliersByFamily: getSpliceOutliersByChromFamily(state)[ownProps.variant.chrom], +}) + +const SpliceOutlierLabel = connect(mapSpliceOutliersStateToProps)(BaseSpliceOutlierLabel) const LargeText = styled.div` font-size: 1.2em; @@ -293,26 +332,9 @@ const BaseVariantLocusListLabels = React.memo(({ locusListIntervalsByProject, fa if (!locusListIntervalsByProject || locusListIntervalsByProject.length < 1) { return null } - const { pos, end, genomeVersion, liftedOverPos, familyGuids = [] } = variant - const locusListIntervals = familyGuids.reduce((acc, familyGuid) => ([ - ...acc, ...(locusListIntervalsByProject[familiesByGuid[familyGuid].projectGuid] || [])]), []) - if (locusListIntervals.length < 1) { - return null - } - const locusListGuids = locusListIntervals.filter((interval) => { - const variantPos = genomeVersion === interval.genomeVersion ? pos : liftedOverPos - if (!variantPos) { - return false - } - if ((variantPos >= interval.start) && (variantPos <= interval.end)) { - return true - } - if (end && !variant.endChrom) { - const variantPosEnd = variantPos + (end - pos) - return (variantPosEnd >= interval.start) && (variantPosEnd <= interval.end) - } - return false - }).map(({ locusListGuid }) => locusListGuid) + + const locusListGuids = getOverlappedIntervals(variant, locusListIntervalsByProject, + fGuid => familiesByGuid[fGuid].projectId).map(({ locusListGuid }) => locusListGuid) return locusListGuids.length > 0 && }) @@ -510,6 +532,7 @@ const Annotations = React.memo(({ variant, mainGeneId, showMainGene }) => { ).map(e =>
{e}
)]} + diff --git a/ui/shared/components/panel/variants/VariantGene.jsx b/ui/shared/components/panel/variants/VariantGene.jsx index bc1dcb7034..5d4fb0324b 100644 --- a/ui/shared/components/panel/variants/VariantGene.jsx +++ b/ui/shared/components/panel/variants/VariantGene.jsx @@ -406,8 +406,8 @@ const GENE_DETAIL_SECTIONS = [ }, { color: 'pink', - description: 'RNA-Seq Outlier', - label: 'RNA-Seq', + description: 'RNA-Seq Expression Outlier', + label: 'RNA expression', showDetails: (gene, indivGeneData) => indivGeneData?.rnaSeqData && indivGeneData.rnaSeqData[gene.geneId], detailsDisplay: (gene, indivGeneData) => (
diff --git a/ui/shared/components/panel/variants/VariantUtils.jsx b/ui/shared/components/panel/variants/VariantUtils.jsx index 8103ef22e0..664b22bde9 100644 --- a/ui/shared/components/panel/variants/VariantUtils.jsx +++ b/ui/shared/components/panel/variants/VariantUtils.jsx @@ -60,3 +60,26 @@ export const ProteinSequence = React.memo(({ hgvs }) => (interval) => { + const { pos, end, liftedOverPos, liftedOverGenomeVersion } = variant + const variantPos = (liftedOverGenomeVersion && liftedOverGenomeVersion === interval.genomeVersion) ? + liftedOverPos : pos + if (!variantPos) { + return false + } + if ((variantPos >= interval.start - padding) && (variantPos <= interval.end + padding)) { + return true + } + if (end && !variant.endChrom) { + const variantPosEnd = variantPos + (end - pos) + return (variantPosEnd >= interval.start - padding) && (variantPosEnd <= interval.end + padding) + } + return false +} + +export const getOverlappedIntervals = (variant, intervals, getIntervalGroup, padding = 0) => { + const { familyGuids = [] } = variant + return familyGuids.reduce((acc, fGuid) => ( + [...acc, ...(intervals[getIntervalGroup(fGuid)] || []).filter(variantIntervalOverlap(variant, padding))]), []) +} diff --git a/ui/shared/components/table/RnaSeqJunctionOutliersTable.jsx b/ui/shared/components/table/RnaSeqJunctionOutliersTable.jsx index fb8268ed2b..6f8888c884 100644 --- a/ui/shared/components/table/RnaSeqJunctionOutliersTable.jsx +++ b/ui/shared/components/table/RnaSeqJunctionOutliersTable.jsx @@ -2,7 +2,7 @@ import React from 'react' import { Popup, Icon } from 'semantic-ui-react' import PropTypes from 'prop-types' -import { RNASEQ_JUNCTION_PADDING } from 'shared/utils/constants' +import { RNASEQ_JUNCTION_PADDING, TISSUE_DISPLAY } from 'shared/utils/constants' import { camelcaseToTitlecase } from 'shared/utils/stringUtils' import { GeneSearchLink } from 'shared/components/buttons/SearchResultsLink' import DataTable from 'shared/components/table/DataTable' @@ -109,13 +109,29 @@ const RNA_SEQ_SPLICE_COLUMNS = [ ...OTHER_SPLICE_COLUMNS, ] +const INDIVIDUAL_NAME_COLUMN = { name: 'individualName', content: '', format: ({ individualName }) => ({individualName}) } + +const RNA_SEQ_SPLICE_POPUP_COLUMNS = [ + INDIVIDUAL_NAME_COLUMN, + { + ...JUNCTION_COLUMN, + format: ({ chrom, start, end, strand }) => `${chrom}:${start}-${end} ${strand}`, + }, + { + name: 'tissueType', + content: 'Tissue Type', + format: ({ tissueType }) => TISSUE_DISPLAY[tissueType], + }, + ...OTHER_SPLICE_COLUMNS, +] + const RnaSeqJunctionOutliersTable = React.memo( - ({ variant, reads, showReads, updateReads, data, dispatch, ...props }) => ( + ({ variant, reads, showReads, updateReads, data, showPopupColumns, dispatch, ...props }) => (
{reads}