Skip to content

Commit

Permalink
feat: tissue type filter
Browse files Browse the repository at this point in the history
  • Loading branch information
MillenniumFalconMechanic committed Nov 29, 2023
1 parent 5f520b3 commit 2aeda19
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 17 deletions.
1 change: 1 addition & 0 deletions frontend/src/common/analytics/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum EVENTS {
FILTER_SELECT_SUSPENSION_TYPE = "FILTER_SELECT_SUSPENSION_TYPE",
FILTER_SELECT_SYSTEM = "FILTER_SELECT_SYSTEM",
FILTER_SELECT_TISSUE = "FILTER_SELECT_TISSUE",
FILTER_SELECT_TISSUE_TYPE = "FILTER_SELECT_TISSUE_TYPE",
WMG_SELECT_GENE = "WMG_SELECT_GENE",
WMG_SELECT_ORGANISM = "WMG_SELECT_ORGANISM",
WMG_SELECT_TISSUE = "WMG_SELECT_TISSUE",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/common/featureFlags/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export enum FEATURES {
GENE_SETS = "gs",
CURATOR = "curator",
FILTER = "filter",
SCHEMA_4_0_0 = "schema4",
}
67 changes: 60 additions & 7 deletions frontend/src/common/queries/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
CATEGORY_VALUE_KEY,
CollectionRow,
DatasetRow,
TISSUE_TYPE,
} from "src/components/common/Filter/common/entities";
import { checkIsOverMaxCellCount } from "src/components/common/Grid/common/utils";
import { API_URL } from "src/configs/configs";
Expand Down Expand Up @@ -129,18 +130,25 @@ export interface DatasetResponse {
revised_at?: number;
sex: Ontology[];
suspension_type: string[];
tissue: Ontology[];
tissue: TissueOntology[];
tissue_ancestors: string[];
}

/**
* Model of /datasets/index JSON response that has been modified to include calculated fields that facilitate filter
* functionality.
*/
export interface ProcessedDatasetResponse extends DatasetResponse {
export type ProcessedDatasetResponse = Omit<DatasetResponse, "tissue"> & {
cellTypeCalculated: string[]; // Field to drive cell class, cell subclass and cell type filter functionality.
tissue: Ontology[]; // Processed dataset has tissue TissueOntology[] mapped to Ontology[].
tissueCalculated: string[]; // Field to drive tissue system, tissue organ and tissue filter functionality.
}
tissueType: TISSUE_TYPE[]; // Field to drive tissue type filter functionality.
};

/**
* Tissue-specifc ontology model: label, ontology ID and tissue
*/
export type TissueOntology = Ontology & { tissue_type: TISSUE_TYPE };

/**
* Query key for caching collections returned from /collections/index endpoint.
Expand Down Expand Up @@ -338,6 +346,7 @@ function aggregateCollectionDatasetRows(
...accum.tissue_ancestors,
...collectionDatasetRow.tissue_ancestors,
],
tissueType: [...accum.tissueType, ...collectionDatasetRow.tissueType],
};
},
{
Expand All @@ -355,6 +364,7 @@ function aggregateCollectionDatasetRows(
tissue: [],
tissueCalculated: [],
tissue_ancestors: [],
tissueType: [],
}
);

Expand Down Expand Up @@ -382,6 +392,7 @@ function aggregateCollectionDatasetRows(
tissue: uniqueOntologies(aggregatedCategoryValues.tissue),
tissueCalculated: [...new Set(aggregatedCategoryValues.tissueCalculated)],
tissue_ancestors: [...new Set(aggregatedCategoryValues.tissue_ancestors)],
tissueType: [...new Set(aggregatedCategoryValues.tissueType)],
};
}

Expand Down Expand Up @@ -435,6 +446,32 @@ function buildCollectionRows(
return collectionRows;
}

/**
* Convert Schema 4.0.0+ tissue ontology model (label, ontology term ID and
* tissue type) to "core" ontology model (label and ontology term ID only)
* by tagging the label and ontology term ID with tissue type and dropping
* tissue type value. Continuing with this tagged format minimizes changes
* to the core filter functionality with migration to Schema 4.0.0 and beyond.
* @param tissue - Tissue ontology to convert.
* @returns Tissue ontology converted to true ontology object.
*/
function createTaggedTissueOntology(tissue: TissueOntology): Ontology {
const { label, ontology_term_id, tissue_type } = tissue;
// Handle tissue type tissue (or undefined for pre-4.0.0).
if (!tissue_type || tissue_type === TISSUE_TYPE.TISSUE) {
return {
label,
ontology_term_id,
};
}

// Handle cell culture or organoid.
return {
label: `${label} (${tissue_type})`,
ontology_term_id: `${ontology_term_id} (${tissue_type})`,
};
}

/**
* Join dataset and collection information to facilitate filter over datasets.
* @param collectionsById - Collections keyed by their ID.
Expand Down Expand Up @@ -610,7 +647,7 @@ function createCollectionRowTestId(
* @returns Number representing seconds since Unix epoch.
*/
export function calculateRecency(
response: CollectionResponse | DatasetResponse,
response: CollectionResponse | ProcessedDatasetResponse,
publisherMetadata?: PublisherMetadata
): number {
// Pull date value from publication metadata if specified.
Expand Down Expand Up @@ -840,7 +877,7 @@ function getCollectionStatus(
* @returns Tuple containing collections publication month (1-indexed) and year.
*/
function getPublicationMonthYear(
response: CollectionResponse | DatasetResponse,
response: CollectionResponse | ProcessedDatasetResponse,
publisherMetadata?: PublisherMetadata
): [number, number] {
// Pull month and year from publication metadata if specified.
Expand Down Expand Up @@ -958,19 +995,35 @@ function processCollectionResponse(
function processDatasetResponse(
dataset: DatasetResponse
): ProcessedDatasetResponse {
// Convert tissue ontology objects to core ontology objects.
const tissue = dataset.tissue.map((tissue) =>
createTaggedTissueOntology(tissue)
);

// Capture the tissue types for dataset to back corresponding filter.
const tissueType = Array.from(
new Set(
dataset.tissue.map((tissue) => tissue.tissue_type ?? TISSUE_TYPE.TISSUE)
)
);

// Build up values to facilitate ontology-aware cel type and tissue filtering.
const cellTypeCalculated = [
...tagAncestorsAsInferred(dataset.cell_type_ancestors),
...tagOntologyTermsAsExplicit(dataset.cell_type),
];
const tissueCalculated = [
...tagAncestorsAsInferred(dataset.tissue_ancestors),
...tagOntologyTermsAsExplicit(dataset.tissue),
// Use tissues mapped to Ontology[] rather than response TissueOntology[]
...tagOntologyTermsAsExplicit(tissue),
];

return {
...dataset,
cellTypeCalculated,
tissue,
tissueCalculated,
tissueType,
};
}

Expand Down Expand Up @@ -1096,7 +1149,7 @@ function tagOntologyTermsAsExplicit(ontologyTermIds: Ontology[]): string[] {

/**
* De-dupe ontologies in the given array.
* @param ontologies - Array of ontologies to remove duplicated from.
* @param ontologies - Array of ontologies to remove duplicates from.
* @returns Array containing set of ontologies.
*/
function uniqueOntologies(ontologies: Ontology[]): Ontology[] {
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/components/common/Filter/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,18 @@ const CATEGORY_FILTER_CONFIGS: CategoryFilterConfig[] = [
valueSourceKind: "NONE",
viewKind: "MULTI_PANEL",
},
{
analyticsEvent: EVENTS.FILTER_SELECT_TISSUE_TYPE,
analyticsPayloadKey: ANALYTICS_PAYLOAD_KEY.TISSUE_TYPE,
categoryFilterId: CATEGORY_FILTER_ID.TISSUE_TYPE,
filterOnKey: "tissueType",
label: "Tissue Type",
labelKind: "VALUE",
matchKind: "INCLUDES_SOME",
multiselect: true,
valueSourceKind: "NONE",
viewKind: "SELECT",
},
];

/**
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/components/common/Filter/common/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum ANALYTICS_PAYLOAD_KEY {
SUSPENSION_TYPE = "suspension_type",
SYSTEM = "system",
TISSUE = "tissue",
TISSUE_TYPE = "tissue_type",
}

// ** Label discriminating unions ** //
Expand Down Expand Up @@ -136,6 +137,7 @@ export enum CATEGORY_FILTER_ID {
"STATUS" = "STATUS",
"SUSPENSION_TYPE" = "SUSPENSION_TYPE",
"TISSUE_CALCULATED" = "TISSUE_CALCULATED",
"TISSUE_TYPE" = "TISSUE_TYPE",
}

// ** Category filter types ** //
Expand Down Expand Up @@ -342,6 +344,7 @@ export interface Categories {
tissue: Ontology[];
tissue_ancestors: string[];
tissueCalculated: string[];
tissueType: TISSUE_TYPE[];
}

/**
Expand Down Expand Up @@ -754,3 +757,12 @@ export interface SelectCategoryView {
export interface Testable {
testId?: string;
}

/**
* Set of tissue types.
*/
export enum TISSUE_TYPE {
TISSUE = "tissue",
CELL_CULTURE = "cell culture",
ORGANOID = "organoid",
}
1 change: 1 addition & 0 deletions frontend/src/views/Collections/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const COLLECTIONS_COLUMN_DENY_LIST: (string | CATEGORY_FILTER_ID)[] = [
CATEGORY_FILTER_ID.SEX,
CATEGORY_FILTER_ID.SUSPENSION_TYPE,
CATEGORY_FILTER_ID.TISSUE_CALCULATED,
CATEGORY_FILTER_ID.TISSUE_TYPE,
];

/**
Expand Down
24 changes: 19 additions & 5 deletions frontend/src/views/Collections/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,18 @@ import RevisionButton from "src/components/common/Grid/components/RevisionButton
import CategoryFilters from "src/components/common/Filter/components/Filters";
import { useViewMode, VIEW_MODE } from "src/common/hooks/useViewMode";
import { GridLoader as Loader } from "src/components/common/Grid/components/Loader/style";
import { useFeatureFlag } from "src/common/hooks/useFeatureFlag";
import { FEATURES } from "src/common/featureFlags/features";

export default function Collections(): JSX.Element {
const { mode, status } = useViewMode();

// Pop toast if user has been redirected from a tombstoned collection.
useExplainTombstoned();

// Check if we're running 4.0.0. TODO remove with #6266.
const isSchema4 = useFeatureFlag(FEATURES.SCHEMA_4_0_0);

// Filterable collection datasets joined from datasets index and collections (or user-collections) index responses.
const {
isError,
Expand Down Expand Up @@ -251,6 +256,12 @@ export default function Collections(): JSX.Element {
filter: "includesSome",
id: CATEGORY_FILTER_ID.TISSUE_CALCULATED,
},
// Hidden, required for filter.
{
accessor: "tissueType",
filter: "includesSome",
id: CATEGORY_FILTER_ID.TISSUE_TYPE,
},
],
[]
);
Expand Down Expand Up @@ -291,15 +302,18 @@ export default function Collections(): JSX.Element {
// Determine the set of categories to display for the collections view.
const categories = useMemo<Set<CATEGORY_FILTER_ID>>(() => {
return Object.values(CATEGORY_FILTER_ID)
.filter(
(categoryFilterId: CATEGORY_FILTER_ID) =>
!CATEGORY_FILTER_DENY_LIST[mode].includes(categoryFilterId)
)
.filter((categoryFilterId: CATEGORY_FILTER_ID) => {
// TODO remove with #6266.
if (categoryFilterId === CATEGORY_FILTER_ID.TISSUE_TYPE) {
return isSchema4;
}
return !CATEGORY_FILTER_DENY_LIST[mode].includes(categoryFilterId);
})
.reduce((accum, categoryFilterId: CATEGORY_FILTER_ID) => {
accum.add(categoryFilterId);
return accum;
}, new Set<CATEGORY_FILTER_ID>());
}, [mode]);
}, [mode, isSchema4]);

// Determine the hidden columns.
const hiddenColumns = useMemo<string[]>(() => COLUMN_DENY_LIST[mode], [mode]);
Expand Down
25 changes: 20 additions & 5 deletions frontend/src/views/Datasets/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import { CATEGORY_FILTER_DENY_LIST } from "src/views/Datasets/common/constants";
import { useViewMode, VIEW_MODE } from "src/common/hooks/useViewMode";
import { DatasetsView as View } from "./style";
import { GridLoader as Loader } from "src/components/common/Grid/components/Loader/style";
import { useFeatureFlag } from "src/common/hooks/useFeatureFlag";
import { FEATURES } from "src/common/featureFlags/features";

/**
* Collection ID object key.
Expand Down Expand Up @@ -88,6 +90,9 @@ export default function Datasets(): JSX.Element {
"To maintain your in-progress work on the previous dataset, we opened a new tab."
);

// Check if we're running 4.0.0. TODO remove with #6266.
const isSchema4 = useFeatureFlag(FEATURES.SCHEMA_4_0_0);

// Filterable datasets joined from datasets and collections responses.
const {
isError,
Expand Down Expand Up @@ -272,6 +277,12 @@ export default function Datasets(): JSX.Element {
filter: "includesSome",
id: CATEGORY_FILTER_ID.TISSUE_CALCULATED,
},
// Hidden, required for filter.
{
accessor: "tissueType",
filter: "includesSome",
id: CATEGORY_FILTER_ID.TISSUE_TYPE,
},
],
[]
);
Expand Down Expand Up @@ -312,6 +323,7 @@ export default function Datasets(): JSX.Element {
CATEGORY_FILTER_ID.SEX,
CATEGORY_FILTER_ID.SUSPENSION_TYPE,
CATEGORY_FILTER_ID.TISSUE_CALCULATED,
CATEGORY_FILTER_ID.TISSUE_TYPE,
EXPLORER_URL,
IS_OVER_MAX_CELL_COUNT,
],
Expand All @@ -338,15 +350,18 @@ export default function Datasets(): JSX.Element {
// Determine the set of categories to display for the datasets view.
const categories = useMemo<Set<CATEGORY_FILTER_ID>>(() => {
return Object.values(CATEGORY_FILTER_ID)
.filter(
(categoryFilterId: CATEGORY_FILTER_ID) =>
!CATEGORY_FILTER_DENY_LIST.includes(categoryFilterId)
)
.filter((categoryFilterId: CATEGORY_FILTER_ID) => {
// TODO remove with #6266.
if (categoryFilterId === CATEGORY_FILTER_ID.TISSUE_TYPE) {
return isSchema4;
}
return !CATEGORY_FILTER_DENY_LIST.includes(categoryFilterId);
})
.reduce((accum, categoryFilterId: CATEGORY_FILTER_ID) => {
accum.add(categoryFilterId);
return accum;
}, new Set<CATEGORY_FILTER_ID>());
}, []);
}, [isSchema4]);

// Set up filter instance.
const filterInstance = useCategoryFilter(
Expand Down

0 comments on commit 2aeda19

Please sign in to comment.