Skip to content

Commit

Permalink
Merge pull request #4457 from broadinstitute/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
hanars authored Oct 28, 2024
2 parents 77e8464 + 5b5551a commit 424715b
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 45 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## dev

## 10/28/24
* Update RNA Tissue Type choices (REQUIRES DB MIGRATION)

## 9/19/24
* Update Biosample choices (REQUIRES DB MIGRATION)
* Add support for Azure OAuth
Expand Down
18 changes: 18 additions & 0 deletions seqr/migrations/0077_alter_rnasample_tissue_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-10-27 17:48

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('seqr', '0076_alter_individual_sex'),
]

operations = [
migrations.AlterField(
model_name='rnasample',
name='tissue_type',
field=models.CharField(choices=[('WB', 'whole_blood'), ('F', 'fibroblasts'), ('M', 'muscle'), ('L', 'lymphocytes'), ('A', 'airway_cultured_epithelium'), ('B', 'brain')], max_length=2),
),
]
1 change: 1 addition & 0 deletions seqr/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ class RnaSample(ModelWithGUID):
('M', 'muscle'),
('L', 'lymphocytes'),
('A', 'airway_cultured_epithelium'),
('B', 'brain'),
)

individual = models.ForeignKey('Individual', on_delete=models.PROTECT)
Expand Down
2 changes: 1 addition & 1 deletion seqr/views/apis/project_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def project_page_data(request, project_guid):
'parental_ids': {
'agg': ArrayAgg(JSONObject(**{k: k for k in ['id', 'guid', 'father_id', 'mother_id']})),
'format': lambda parental_ids, id_guid_map: [
{'paternalGuid': id_guid_map.get(p['father_id']), 'maternalGuid': id_guid_map.get(p['mother_id'])}
{'paternalGuid': id_guid_map.get(p['father_id']), 'maternalGuid': id_guid_map.get(p['mother_id']), 'individualGuid': p['guid']}
for p in parental_ids if p['father_id'] or p['mother_id']
],
'response_key': 'parents',
Expand Down
2 changes: 1 addition & 1 deletion seqr/views/apis/project_api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ def test_project_families(self):
self.assertTrue(family_1['hasRequiredMetadata'])
self.assertFalse(family_3['hasRequiredMetadata'])
self.assertFalse(empty_family['hasRequiredMetadata'])
self.assertListEqual(family_1['parents'], [{'maternalGuid': 'I000003_na19679', 'paternalGuid': 'I000002_na19678'}])
self.assertListEqual(family_1['parents'], [{'maternalGuid': 'I000003_na19679', 'paternalGuid': 'I000002_na19678', 'individualGuid': 'I000001_na19675'}])
self.assertListEqual(family_3['parents'], [])
self.assertListEqual(empty_family['parents'], [])
self.assertEqual(family_1['hasPhenotypePrioritization'], True)
Expand Down
4 changes: 2 additions & 2 deletions ui/pages/Project/components/FamilyTable/IndividualRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,9 @@ const EDIT_INDIVIDUAL_FIELDS = [INDIVIDUAL_FIELD_SEX, INDIVIDUAL_FIELD_AFFECTED]
)))

const mapIgvOptionsStateToProps = (state) => {
const { namespace, name } = getCurrentProject(state)
const { workspaceNamespace, workspaceName } = getCurrentProject(state)
return {
url: `/api/anvil_workspace/${namespace}/${name}/get_igv_options`,
url: `/api/anvil_workspace/${workspaceNamespace}/${workspaceName}/get_igv_options`,
}
}

Expand Down
65 changes: 36 additions & 29 deletions ui/pages/Project/components/ProjectOverview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import { updateProjectMmeContact, loadMmeSubmissions, updateAnvilWorkspace } fro
import {
getCurrentProject,
getAnalysisStatusCounts,
getProjectAnalysisGroupFamilyIndividualCounts,
getProjectAnalysisGroupDataLoadedFamilyIndividualCounts,
getProjectAnalysisGroupFamilySizeHistogram,
getProjectAnalysisGroupDataLoadedFamilySizeHistogram,
getProjectAnalysisGroupSamplesByTypes,
getProjectAnalysisGroupMmeSubmissionDetails,
getMmeSubmissionsLoading,
Expand All @@ -46,17 +46,15 @@ const FAMILY_SIZE_LABELS = {
}

const FAMILY_STRUCTURE_SIZE_LABELS = {
2: plural => ` duo${plural ? 's' : ''}`,
3: plural => ` trio${plural ? 's' : ''}`,
4: plural => ` quad${plural ? 's' : ''}`,
5: plural => ` trio${plural ? 's' : ''}+`,
2: 'duo',
3: 'trio',
4: 'quad',
}

const FAMILY_STRUCTURE_HOVER = {
2: 'A family with one parent and one child',
3: 'A family with two parents and one child',
4: 'A family with two parents and two children',
5: 'A family with two parents and three or more other family members',
}

const SAMPLE_TYPE_LOOKUP = SAMPLE_TYPE_OPTIONS.reduce(
Expand Down Expand Up @@ -160,19 +158,24 @@ const MatchmakerSubmissionOverview = connect(
mapMatchmakerSubmissionsStateToProps, mapDispatchToProps,
)(BaseMatchmakerSubmissionOverview)

const FamiliesIndividuals = React.memo(({ canEdit, hasCaseReview, familyCounts, user, title }) => {
const familySizeHistogram = familyCounts.reduce((acc, { size, numParents }) => {
const familySize = Math.min(size, 5)
const sizeAcc = acc[familySize] || { total: 0, withParents: 0 }
sizeAcc.total += 1
if (familySize === 2 && numParents) {
sizeAcc.withParents += 1
} else if (familySize > 2 && numParents === 2) {
sizeAcc.withParents += 1
const MAX_FAMILY_HIST_SIZE = 5

const FamiliesIndividuals = React.memo(({ canEdit, hasCaseReview, familySizes, user, title }) => {
const familiesCount = Object.values(familySizes).reduce((acc, { total }) => acc + total, 0)
const individualsCount = Object.entries(familySizes).reduce((acc, [size, { total }]) => acc + (size * total), 0)
const familySizeHistogram = Object.entries(familySizes).reduce((acc, [size, counts]) => {
if (size <= MAX_FAMILY_HIST_SIZE) {
return { ...acc, [size]: counts }
}
if (!acc[MAX_FAMILY_HIST_SIZE]) {
acc[MAX_FAMILY_HIST_SIZE] = { total: 0, withParents: 0, trioPlus: 0, quadPlus: 0 }
}
return { ...acc, [familySize]: sizeAcc }
acc[MAX_FAMILY_HIST_SIZE].total += counts.total
acc[MAX_FAMILY_HIST_SIZE].trioPlus += counts.trioPlus
acc[MAX_FAMILY_HIST_SIZE].quadPlus += acc[MAX_FAMILY_HIST_SIZE].withParents + counts.withParents + counts.quadPlus
acc[MAX_FAMILY_HIST_SIZE].withParents = 0
return acc
}, {})
const individualsCount = familyCounts.reduce((acc, { size }) => acc + size, 0)

let editIndividualsButton = null
if (user && (user.isPm || (hasCaseReview && canEdit))) {
Expand All @@ -185,25 +188,29 @@ const FamiliesIndividuals = React.memo(({ canEdit, hasCaseReview, familyCounts,
<DetailSection
title={(
<span>
{`${Object.keys(familyCounts).length} Families${title || ''},`}
{`${familiesCount} Families${title || ''},`}
<br />
{`${individualsCount} Individuals${title || ''}`}
</span>
)}
content={
sortBy(Object.entries(familySizeHistogram)).map(([size, { total, withParents }]) => (
sortBy(Object.entries(familySizeHistogram)).map(([size, { total, withParents, trioPlus, quadPlus }]) => (
<div key={size}>
{`${total} famil${total === 1 ? 'y' : 'ies'} with ${FAMILY_SIZE_LABELS[size] || size} individual${size === '1' ? '' : 's'}`}
{withParents > 0 && (
<div>
{[
[withParents, FAMILY_STRUCTURE_SIZE_LABELS[size], FAMILY_STRUCTURE_HOVER[size], total > 1],
[trioPlus, 'trio+', 'A family with two parents, one child, and other family members'],
[quadPlus, 'quad+', 'A family with two parents, at least two children, and other family members'],
].filter(([count]) => count > 0).map(([count, label, hover, plural]) => (
<div key="label">
&nbsp;&nbsp;&nbsp;&nbsp;
{withParents}
{count}
<Popup
trigger={<span>{FAMILY_STRUCTURE_SIZE_LABELS[size](total > 1)}</span>}
content={FAMILY_STRUCTURE_HOVER[size]}
trigger={<span>{` ${label}${plural ? 's' : ''}`}</span>}
content={hover}
/>
</div>
)}
))}
</div>
))
}
Expand All @@ -213,7 +220,7 @@ const FamiliesIndividuals = React.memo(({ canEdit, hasCaseReview, familyCounts,
})

FamiliesIndividuals.propTypes = {
familyCounts: PropTypes.arrayOf(PropTypes.object).isRequired,
familySizes: PropTypes.object.isRequired,
canEdit: PropTypes.bool,
hasCaseReview: PropTypes.bool,
user: PropTypes.object,
Expand All @@ -222,12 +229,12 @@ FamiliesIndividuals.propTypes = {

const mapFamiliesStateToProps = (state, ownProps) => ({
user: getUser(state),
familyCounts: getProjectAnalysisGroupFamilyIndividualCounts(state, ownProps),
familySizes: getProjectAnalysisGroupFamilySizeHistogram(state, ownProps),
})

const mapDataLoadedFamiliesStateToProps = (state, ownProps) => ({
title: ' With Data',
familyCounts: getProjectAnalysisGroupDataLoadedFamilyIndividualCounts(state, ownProps),
familySizes: getProjectAnalysisGroupDataLoadedFamilySizeHistogram(state, ownProps),
})

const FamiliesIndividualsOverview = connect(mapFamiliesStateToProps)(FamiliesIndividuals)
Expand Down
59 changes: 47 additions & 12 deletions ui/pages/Project/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,30 +97,65 @@ export const getProjectAnalysisGroupFamiliesByGuid = createSelector(
},
)

export const getProjectAnalysisGroupFamilyIndividualCounts = createSelector(
const getFamilySizeHistogram = familyCounts => familyCounts.reduce((acc, { size, parents }) => {
const parentCounts = Object.values(parents.reduce(
(parentAcc, { maternalGuid, paternalGuid }) => {
const parentKey = `${maternalGuid || ''}-${paternalGuid || ''}`
const parent = parentAcc[parentKey] || {
numParents: [maternalGuid, paternalGuid].filter(g => g).length,
numChildren: 0,
}
parent.numChildren += 1
return { ...parentAcc, [parentKey]: parent }
}, {},
))
const sizeAcc = acc[size] || { total: 0, withParents: 0, trioPlus: 0, quadPlus: 0 }
sizeAcc.total += 1
const mainParentCount = parentCounts.find(({ numParents }) => numParents === (size === 2 ? 1 : 2))
const mainFamilySize = mainParentCount ? mainParentCount.numChildren + mainParentCount.numParents : 0
if (mainFamilySize === size) {
sizeAcc.withParents += 1
} else if (mainFamilySize === 3) {
sizeAcc.trioPlus += 1
} else if (mainFamilySize > 3) {
sizeAcc.quadPlus += 1
}
return { ...acc, [size]: sizeAcc }
}, {})

export const getProjectAnalysisGroupFamilySizeHistogram = createSelector(
getProjectAnalysisGroupFamiliesByGuid,
familiesByGuid => Object.values(familiesByGuid).map(family => ({
familiesByGuid => getFamilySizeHistogram(Object.values(familiesByGuid).map(family => ({
size: (family.individualGuids || []).length,
numParents: (family.parents || []).length === 1 ?
[family.parents[0].maternalGuid, family.parents[0].paternalGuid].filter(g => g).length : 0,
})),
parents: family.parents || [],
}))),
)

export const getProjectAnalysisGroupDataLoadedFamilyIndividualCounts = createSelector(
export const getProjectAnalysisGroupDataLoadedFamilySizeHistogram = createSelector(
getProjectAnalysisGroupFamiliesByGuid,
getSamplesByFamily,
(familiesByGuid, samplesByFamily) => Object.values(familiesByGuid).map(((family) => {
(familiesByGuid, samplesByFamily) => getFamilySizeHistogram(Object.values(familiesByGuid).map(((family) => {
const sampleIndividuals = new Set((samplesByFamily[family.familyGuid] || []).filter(
sample => sample.isActive,
).map(sample => sample.individualGuid))
const hasSampleParentCounts = (family.parents || []).map(
({ maternalGuid, paternalGuid }) => [maternalGuid, paternalGuid].filter(guid => sampleIndividuals.has(guid)),
).filter(parents => parents.length > 0)
const hasSampleParents = (family.parents || []).reduce(
(acc, { individualGuid, maternalGuid, paternalGuid }) => {
const hasSampleMaternal = sampleIndividuals.has(maternalGuid)
const hasSamplePaternal = sampleIndividuals.has(paternalGuid)
if (sampleIndividuals.has(individualGuid) && (hasSampleMaternal || hasSamplePaternal)) {
acc.push({
maternalGuid: hasSampleMaternal ? maternalGuid : null,
paternalGuid: hasSamplePaternal ? paternalGuid : null,
})
}
return acc
}, [],
)
return {
size: sampleIndividuals.size,
numParents: hasSampleParentCounts.length === 1 ? hasSampleParentCounts[0].length : 0,
parents: hasSampleParents,
}
})).filter(({ size }) => size > 0),
})).filter(({ size }) => size > 0)),
)

export const getProjectAnalysisGroupIndividualsByGuid = createSelector(
Expand Down
4 changes: 4 additions & 0 deletions ui/shared/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ export const CATEGORY_FAMILY_FILTERS = {
// INDIVIDUAL FIELDS
const SEX_MALE = 'M'
const SEX_FEMALE = 'F'
const SEX_UNKNOWN = 'U'
const MALE_ANEUPLOIDIES = ['XXY', 'XYY']
const FEMALE_ANEUPLOIDIES = ['XXX', 'X0']
export const SEX_OPTIONS = [
Expand All @@ -409,6 +410,7 @@ export const SEX_OPTIONS = [
export const SIMPLIFIED_SEX_LOOKUP = {
...[SEX_MALE, ...MALE_ANEUPLOIDIES].reduce((acc, val) => ({ ...acc, [val]: SEX_MALE }), {}),
...[SEX_FEMALE, ...FEMALE_ANEUPLOIDIES].reduce((acc, val) => ({ ...acc, [val]: SEX_FEMALE }), {}),
[SEX_UNKNOWN]: SEX_UNKNOWN,
}

export const SEX_LOOKUP = SEX_OPTIONS.reduce(
Expand Down Expand Up @@ -2004,6 +2006,8 @@ export const TISSUE_DISPLAY = {
F: 'Fibroblast',
M: 'Muscle',
L: 'Lymphocyte',
A: 'Airway Cultured Epithelium',
B: 'Brain',
}

export const RNASEQ_JUNCTION_PADDING = 200
Expand Down

0 comments on commit 424715b

Please sign in to comment.