From 9b89d8969875a2f2123078f08fc452eeae30609c Mon Sep 17 00:00:00 2001 From: Adam Abeshouse Date: Thu, 30 Sep 2021 16:57:25 -0400 Subject: [PATCH] Show filtered case and sample counts in atlas table Signed-off-by: Adam Abeshouse --- components/ExploreTabs.tsx | 14 ++++++ components/WPAtlasTable.tsx | 51 +++++++++++++++++++-- lib/filterHelpers.ts | 45 ++++++++++++++++++- lib/helpers.ts | 1 + pages/explore.tsx | 88 +++++++++++++++++++++++-------------- 5 files changed, 161 insertions(+), 38 deletions(-) diff --git a/components/ExploreTabs.tsx b/components/ExploreTabs.tsx index dd9a8112..ea596bd5 100644 --- a/components/ExploreTabs.tsx +++ b/components/ExploreTabs.tsx @@ -9,12 +9,19 @@ import CaseTable from './CaseTable'; import FileTable from './FileTable'; import WPAtlasTable from './WPAtlasTable'; import { DataSchemaData } from '../lib/dataSchemaHelpers'; +import { + ExploreSelectedFilter, + ISelectedFiltersByAttrName, +} from '../lib/types'; interface IExploreTabsProps { router: NextRouter; filteredFiles: Entity[]; + nonAtlasSelectedFiltersByAttrName: ISelectedFiltersByAttrName; samples: Entity[]; cases: Entity[]; + filteredCasesByNonAtlasFilters: Entity[]; + filteredSamplesByNonAtlasFilters: Entity[]; wpData: WPAtlas[]; schemaDataById?: { [schemaDataId: string]: DataSchemaData }; getGroupsByPropertyFiltered: any; @@ -178,6 +185,13 @@ const ExploreTabs: React.FunctionComponent = observer( props.filteredSynapseAtlasesByNonAtlasFilters } onSelectAtlas={props.onSelectAtlas} + filteredCases={props.filteredCasesByNonAtlasFilters} + filteredBiospecimens={ + props.filteredSamplesByNonAtlasFilters + } + selectedFiltersByAttrName={ + props.nonAtlasSelectedFiltersByAttrName + } /> )} diff --git a/components/WPAtlasTable.tsx b/components/WPAtlasTable.tsx index c0209072..870dfc23 100644 --- a/components/WPAtlasTable.tsx +++ b/components/WPAtlasTable.tsx @@ -2,7 +2,7 @@ import _ from 'lodash'; import { NextRouter } from 'next/router'; import React from 'react'; import { getDefaultDataTableStyle } from '../lib/dataTableHelpers'; -import { Atlas, setTab } from '../lib/helpers'; +import { Atlas, Entity, setTab } from '../lib/helpers'; import EnhancedDataTable from './EnhancedDataTable'; import { observer } from 'mobx-react'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -12,6 +12,11 @@ import { faDownload } from '@fortawesome/free-solid-svg-icons'; import { ExploreTab } from './ExploreTabs'; import { Button, Modal } from 'react-bootstrap'; import getAtlasMetaData from '../lib/getAtlasMetaData'; +import { + AttributeNames, + ExploreSelectedFilter, + ISelectedFiltersByAttrName, +} from '../lib/types'; interface IWPAtlasTableProps { router: NextRouter; @@ -19,6 +24,9 @@ interface IWPAtlasTableProps { selectedAtlases?: Atlas[]; filteredAtlases?: Atlas[]; onSelectAtlas?: (selected: Atlas[]) => void; + selectedFiltersByAttrName: ISelectedFiltersByAttrName; + filteredCases: Entity[]; + filteredBiospecimens: Entity[]; } const atlasMetadata = getAtlasMetaData(); @@ -131,6 +139,21 @@ export default class WPAtlasTable extends React.Component { ); } + @computed get filteredCasesByAtlas() { + return _.groupBy(this.props.filteredCases, (c: Entity) => c.atlasid); + } + + @computed get filteredBiospecimensByAtlas() { + return _.groupBy( + this.props.filteredBiospecimens, + (c: Entity) => c.atlasid + ); + } + + @computed get shouldShowFilteredFractions() { + return !_.isEmpty(this.props.selectedFiltersByAttrName); + } + get columns() { return [ { @@ -193,7 +216,18 @@ export default class WPAtlasTable extends React.Component { grow: 0.5, selector: 'num_cases', cell: (atlas: Atlas) => ( - {atlas.num_cases} + + {this.shouldShowFilteredFractions + ? `${ + ( + this.filteredCasesByAtlas[ + atlas.htan_id + ] || [] + ).length + }/` + : ''} + {atlas.num_cases} + ), sortable: true, }, @@ -201,7 +235,18 @@ export default class WPAtlasTable extends React.Component { name: 'Biospecimens', selector: 'num_biospecimens', cell: (atlas: Atlas) => ( - {atlas.num_biospecimens} + + {this.shouldShowFilteredFractions + ? `${ + ( + this.filteredBiospecimensByAtlas[ + atlas.htan_id + ] || [] + ).length + }/` + : ''} + {atlas.num_biospecimens} + ), sortable: true, }, diff --git a/lib/filterHelpers.ts b/lib/filterHelpers.ts index eeff2096..87d2ff30 100644 --- a/lib/filterHelpers.ts +++ b/lib/filterHelpers.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; -import { Entity } from './helpers'; +import { Entity, filterObject } from './helpers'; import { AttributeMap, AttributeNames, @@ -167,3 +167,46 @@ export function makeOptions( }; }); } + +export function getFilteredCases( + filteredFiles: Entity[], + selectedFiltersByAttrName: ISelectedFiltersByAttrName, + showAllCases: boolean +) { + const cases = _.chain(filteredFiles) + .flatMapDeep((f: Entity) => f.cases) + .uniqBy((f) => f.HTANParticipantID) + .value(); + + if (showAllCases) { + return cases; + } else { + const caseFilters = filterObject( + selectedFiltersByAttrName, + (filters, attrName) => + !!AttributeMap[attrName as AttributeNames].caseFilter + ); + return filterFiles(caseFilters, cases); + } +} + +export function getFilteredSamples( + filteredFiles: Entity[], + filteredCases: Entity[], + showAllSamples: boolean +) { + const samples = _.chain(filteredFiles) + .flatMapDeep((file) => file.biospecimen) + .uniqBy((f) => f.HTANBiospecimenID) + .value(); + + if (showAllSamples) { + return samples; + } else { + const filteredCaseIds = _.keyBy( + filteredCases, + (c) => c.HTANParticipantID + ); + return samples.filter((s) => s.HTANParentID in filteredCaseIds); + } +} diff --git a/lib/helpers.ts b/lib/helpers.ts index a602c8cf..e7e828e6 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -8,6 +8,7 @@ import { WPAtlas } from '../types'; import { ExploreOptionType, ExploreSelectedFilter, + ISelectedFiltersByAttrName, SynapseAtlas, SynapseData, SynapseSchema, diff --git a/pages/explore.tsx b/pages/explore.tsx index 0a07eb5f..b733a68d 100644 --- a/pages/explore.tsx +++ b/pages/explore.tsx @@ -17,6 +17,8 @@ import { ScaleLoader } from 'react-spinners'; import { getAtlasList, WORDPRESS_BASE_URL } from '../ApiUtil'; import { filterFiles, + getFilteredCases, + getFilteredSamples, groupFilesByAttrNameAndValue, } from '../lib/filterHelpers'; import { @@ -201,50 +203,55 @@ class Search extends React.Component< @computed get filteredFiles() { - //const start = performance.now(); const ret = filterFiles( this.selectedFiltersByAttrName, this.state.files ); - //console.log('filtering cost', performance.now() - start); return ret; } + @computed + get filteredFilesByNonAtlasFilters() { + return filterFiles( + this.nonAtlasSelectedFiltersByAttrName, + this.state.files + ); + } + @computed get filteredSamples() { - const samples = _.chain(this.filteredFiles) - .flatMapDeep((file) => file.biospecimen) - .uniqBy((f) => f.HTANBiospecimenID) - .value(); + return getFilteredSamples( + this.filteredFiles, + this.filteredCases, + this.showAllBiospecimens + ); + } - if (this.showAllBiospecimens) { - return samples; - } else { - const filteredCaseIds = _.keyBy( - this.filteredCases, - (c) => c.HTANParticipantID - ); - return samples.filter((s) => s.HTANParentID in filteredCaseIds); - } + @computed + get filteredSamplesByNonAtlasFilters() { + return getFilteredSamples( + this.filteredFilesByNonAtlasFilters, + this.filteredCasesByNonAtlasFilters, + this.showAllBiospecimens + ); } @computed get filteredCases() { - const cases = _.chain(this.filteredFiles) - .flatMapDeep((f: Entity) => f.cases) - .uniqBy((f) => f.HTANParticipantID) - .value(); + return getFilteredCases( + this.filteredFiles, + this.selectedFiltersByAttrName, + this.showAllCases + ); + } - if (this.showAllCases) { - return cases; - } else { - const caseFilters = filterObject( - this.selectedFiltersByAttrName, - (filters, attrName) => - !!AttributeMap[attrName as AttributeNames].caseFilter - ); - return filterFiles(caseFilters, cases); - } + @computed + get filteredCasesByNonAtlasFilters() { + return getFilteredCases( + this.filteredFilesByNonAtlasFilters, + this.nonAtlasSelectedFiltersByAttrName, + this.showAllCases + ); } @computed get atlasMap() { @@ -283,14 +290,18 @@ class Search extends React.Component< } } + @computed get nonAtlasSelectedFiltersByAttrName() { + return _.omit(this.selectedFiltersByAttrName, [ + AttributeNames.AtlasName, + ]); + } + @computed get filteredAtlasesByNonAtlasFilters() { - const filtersExpectAtlasFilters = _.omit( - this.selectedFiltersByAttrName, - [AttributeNames.AtlasName] - ); + const filtersExceptAtlasFilters = this + .nonAtlasSelectedFiltersByAttrName; - return _.chain(filterFiles(filtersExpectAtlasFilters, this.state.files)) + return _.chain(filterFiles(filtersExceptAtlasFilters, this.state.files)) .map((f) => f.atlasid) .uniq() .map((id) => this.atlasMap[id]) @@ -360,6 +371,15 @@ class Search extends React.Component< onSelectAtlas={this.onSelectAtlas} samples={this.filteredSamples} cases={this.filteredCases} + filteredCasesByNonAtlasFilters={ + this.filteredCasesByNonAtlasFilters + } + filteredSamplesByNonAtlasFilters={ + this.filteredSamplesByNonAtlasFilters + } + nonAtlasSelectedFiltersByAttrName={ + this.nonAtlasSelectedFiltersByAttrName + } wpData={this.props.wpAtlases} getGroupsByPropertyFiltered={ this.getGroupsByPropertyFiltered