Skip to content

Commit

Permalink
Merge pull request #940 from biomage-org/add-cell-level-to-differenti…
Browse files Browse the repository at this point in the history
…al-expression

Add cell level to differential expression
  • Loading branch information
cosa65 authored Nov 21, 2023
2 parents 9d1ee07 + 621e690 commit f317670
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 32 deletions.
66 changes: 66 additions & 0 deletions src/__test__/data/cell_level_cell_sets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
[{
"key": "clm-0",
"name": "Some Cell Level Track",
"rootNode": true,
"children": [
{
"key": "clm-0-0",
"name": "Some cell level zero",
"color": "#8c564b",
"cellIds": [1, 21, 51, 86]
},
{
"key": "clm-0-1",
"name": "Some cell level one",
"color": "#d62728",
"cellIds": [11, 42, 96]
}
],
"type": "CLM"
},
{
"key": "clm-1",
"name": "Another Cell Level Track",
"rootNode": true,
"children": [
{
"key": "clm-1-0",
"name": "Another cell level zero",
"color": "#8c564b",
"cellIds": [2, 22, 52, 87]
},
{
"key": "clm-1-1",
"name": "Another cell level one",
"color": "#d62728",
"cellIds": [15, 47, 49]
}
],
"type": "CLM"
},
{
"key": "sample-clm-0",
"name": "Sample Cell Level Track",
"rootNode": true,
"children": [
{
"key": "sample-clm-0-0",
"name": "Sample cell level zero",
"color": "#8c564b",
"cellIds": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29 , 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
},
{
"key": "sample-clm-0-1",
"name": "Sample cell level one",
"color": "#d62728",
"cellIds": [41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
71, 72, 73, 74, 75]
}
],
"type": "CLMPerSample"
}]
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,30 @@ import userEvent from '@testing-library/user-event';
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';

import { makeStore } from 'redux/store';
import mockAPI, { generateDefaultMockAPIResponses } from '__test__/test-utils/mockAPI';
import mockAPI, { generateDefaultMockAPIResponses, promiseResponse } from '__test__/test-utils/mockAPI';
import * as getBatchDiffExpr from 'utils/extraActionCreators/differentialExpression/getBatchDiffExpr';
import * as checkCanRunDiffExprModule from 'utils/extraActionCreators/differentialExpression/checkCanRunDiffExpr';
import mockLoader from 'components/Loader';

import cellSetsData from '__test__/data/cell_sets.json';
import cellLevelCellSets from '__test__/data/cell_level_cell_sets.json';

jest.spyOn(checkCanRunDiffExprModule, 'default').mockImplementation(() => 'TRUE');
jest.mock('utils/extraActionCreators/differentialExpression/getBatchDiffExpr');
jest.mock('components/Loader', () => jest.fn(() => <div data-testid='mockLoader'>Mock Loader</div>));

describe('Batch differential expression tests ', () => {
let storeState = null;
const mockApiResponses = _.merge(generateDefaultMockAPIResponses(fake.EXPERIMENT_ID));

const customCellSetsData = _.cloneDeep(cellSetsData);
// Add the cell level cell sets
customCellSetsData.cellSets.push(...cellLevelCellSets);

const mockApiResponses = {
...generateDefaultMockAPIResponses(fake.EXPERIMENT_ID),
[`experiments/${fake.EXPERIMENT_ID}/cellSets$`]: () => promiseResponse(JSON.stringify(customCellSetsData)),
};

let getBatchDiffExprSpy;

beforeEach(async () => {
Expand All @@ -35,6 +47,7 @@ describe('Batch differential expression tests ', () => {
storeState = makeStore();
getBatchDiffExprSpy = jest.spyOn(getBatchDiffExpr, 'default');
});

const secondOptionText = 'Compare between two selected samples/groups in a cell set for all cell sets';
const renderPage = async () => {
await act(async () => render(
Expand All @@ -52,23 +65,75 @@ describe('Batch differential expression tests ', () => {
it('Shows correct input fields for each comparison option', async () => {
await renderPage();

const compareForCellSetsRadio = screen.getByLabelText(secondOptionText);
const compareForSamplesRadio = screen.getByLabelText(/Compare two cell sets for all samples\/groups/i);
const compareBetweenSamplesRadio = screen.getByLabelText(secondOptionText);
const compareBetweenCellSetsRadio = screen.getByLabelText(/Compare two cell sets for all samples\/groups/i);

expect(screen.getByText(/Select the cell sets for which marker genes are to be computed in batch:/i)).toBeInTheDocument();
expect(screen.getByText('Select a cell set...')).toBeInTheDocument();

// Check compareForCellSetsRadio
await act(() => userEvent.click(compareForCellSetsRadio));
await act(() => userEvent.click(screen.getByText('Select a cell set...')));

const cellBasedClasses = [
'Fake louvain clusters',
'Custom cell sets',
'Some Cell Level Track',
'Another Cell Level Track'
];

const sampleBasedClasses = [
'Samples',
'Track_1',
'Sample Cell Level Track'
];

const sampleBasedSets = [
'KO', 'WT1', 'WT2', 'KMeta', 'WMetaT', 'Sample cell level zero', 'Sample cell level one',
];

// Shows the correct cell classes as options
cellBasedClasses.forEach((text) => {
expect(screen.getByText(text)).toBeInTheDocument();
})

// Doesn't show the samples or sample-based metadata cell classes as options
sampleBasedClasses.forEach((text) => {
expect(screen.queryByText(text)).not.toBeInTheDocument();
})

// Check compareBetweenSamplesRadio
await act(() => userEvent.click(compareBetweenSamplesRadio));
expect(screen.getByText(/Select the comparison sample\/groups for which batch/i)).toBeInTheDocument();
expect(screen.getByText(/In batch for each cell set in:/i)).toBeInTheDocument();
expect(screen.getByText(/Select a cell set.../i)).toBeInTheDocument();

// Check compareForSamplesRadio
await act(() => userEvent.click(compareForSamplesRadio));
expect(screen.getAllByText('Select a sample/group...')).toHaveLength(2);
screen.getAllByText('Select a sample/group...').forEach((match) => {
expect(match).toBeVisible();
});

await act(() => userEvent.click(screen.getAllByText('Select a sample/group...')[0]));

// Shows the sample options and their classes
[...sampleBasedSets, ...sampleBasedClasses].forEach((text) => {
expect(screen.getByText(text)).toBeInTheDocument();
});

// Check compareBetweenCellSetsRadio
await act(() => userEvent.click(compareBetweenCellSetsRadio));
expect(screen.getByText(/Select the comparison cell sets for which batch/i)).toBeInTheDocument();
expect(screen.getByText(/In batch for each sample\/group in:/i)).toBeInTheDocument();
expect(screen.getByText(/Select samples or metadata.../i)).toBeInTheDocument();

await act(() => userEvent.click(screen.getAllByText('Select a cell set...')[0]));


const someLouvainCellSets = Array.from({ length: 11 }, (x, index) => `Cluster ${index}`);
// Shows only some of the louvain options and not the others
// Because it is a virtual list which only renders the options that enter the
// options that fit in the display
[...someLouvainCellSets, 'fake louvain clusters'].forEach((text) => {
expect(screen.getByText(text)).toBeInTheDocument();
});
});

it('sending a request should work', async () => {
Expand Down Expand Up @@ -107,6 +172,7 @@ describe('Batch differential expression tests ', () => {
['louvain-0', 'louvain-1', 'louvain-2', 'louvain-3', 'louvain-4', 'louvain-5', 'louvain-6', 'louvain-7', 'louvain-8', 'louvain-9', 'louvain-10', 'louvain-11', 'louvain-12', 'louvain-13']);
});
});

it('shows the Loader while fetching data', async () => {
getBatchDiffExpr.mockImplementation(() => new Promise((resolve) => {
setTimeout(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const DiffExprCompute = (props) => {
* Constructs a form item, a `Select` field with selectable clusters.
*/
const renderClusterSelectorItem = ({
title, option, filterType,
title, option, filterTypes,
}) => {
if (!cellSets.accessible) {
return (
Expand All @@ -113,7 +113,7 @@ const DiffExprCompute = (props) => {
<DiffExprSelect
title={title}
option={option}
filterType={filterType}
filterTypes={filterTypes}
onSelectCluster={onSelectCluster}
selectedComparison={comparisonGroup[selectedComparison]}
cellSets={cellSets}
Expand Down Expand Up @@ -234,40 +234,40 @@ const DiffExprCompute = (props) => {
renderClusterSelectorItem({
title: 'Compare cell set:',
option: 'cellSet',
filterType: 'cellSets',
filterTypes: ['cellSets', 'CLM'],
})
}

{renderClusterSelectorItem({
title: 'and cell set:',
option: 'compareWith',
filterType: 'cellSets',
filterTypes: ['cellSets', 'CLM'],
})}

{renderClusterSelectorItem({
title: 'within sample/group:',
option: 'basis',
filterType: 'metadataCategorical',
filterTypes: ['metadataCategorical', 'CLMPerSample'],
})}
</>
) : (
<>
{renderClusterSelectorItem({
title: 'Compare cell set:',
option: 'basis',
filterType: 'cellSets',
filterTypes: ['cellSets', 'CLM'],
})}

{renderClusterSelectorItem({
title: 'between sample/group:',
option: 'cellSet',
filterType: 'metadataCategorical',
filterTypes: ['metadataCategorical', 'CLMPerSample'],
})}

{renderClusterSelectorItem({
title: 'and sample/group:',
option: 'compareWith',
filterType: 'metadataCategorical',
filterTypes: ['metadataCategorical', 'CLMPerSample'],
})}
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ const { Option, OptGroup } = Select;

const DiffExprSelect = (props) => {
const {
title, option, filterType, onSelectCluster, selectedComparison, cellSets, value
title, option, filterTypes, onSelectCluster, selectedComparison, cellSets, value
} = props;
// Depending on the cell set type specified, set the default name
const placeholder = filterType === 'metadataCategorical' ? 'sample/group' : 'cell set';
const placeholder = filterTypes.includes('metadataCategorical') ? 'sample/group' : 'cell set';
const { hierarchy, properties } = cellSets;
const tree = composeTree(hierarchy, properties, filterType);
const tree = composeTree(hierarchy, properties, filterTypes);

const renderChildren = (rootKey, children) => {
if (!children || children.length === 0) { return (<></>); }
Expand Down Expand Up @@ -96,7 +96,7 @@ DiffExprSelect.propTypes = {
selectedComparison: PropTypes.object.isRequired,
cellSets: PropTypes.object.isRequired,
option: PropTypes.string.isRequired,
filterType: PropTypes.string.isRequired,
filterTypes: PropTypes.array.isRequired,
onSelectCluster: PropTypes.func.isRequired,
};
export default DiffExprSelect;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import React, { useState } from 'react';
import {
Dropdown, Button, Tooltip,
Dropdown, Button,
} from 'antd';
import PropTypes from 'prop-types';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ const BatchDiffExpression = (props) => {
const cellSets = useSelector(getCellSets());
const { properties, hierarchy } = cellSets;
const experimentName = useSelector((state) => state.experimentSettings.info.experimentName);

const cellSetNodes = useSelector(getCellSetsHierarchyByType('cellSets'));
const clmNodes = useSelector(getCellSetsHierarchyByType('CLM'));

const metadataCellSetNodes = useSelector(getCellSetsHierarchyByType('metadataCategorical'));
const clmPerSampleNodes = useSelector(getCellSetsHierarchyByType('CLMPerSample'));

const [dataLoading, setDataLoading] = useState();

Expand All @@ -65,6 +69,16 @@ const BatchDiffExpression = (props) => {

const [sample] = useSelector(getCellSetsHierarchyByKeys(['sample']));

const cellDefinedNodes = useMemo(
() => [...cellSetNodes, ...clmNodes],
[cellSetNodes, clmNodes],
);

const sampleDefinedNodes = useMemo(
() => [...metadataCellSetNodes, ...clmPerSampleNodes],
[metadataCellSetNodes, clmPerSampleNodes],
);

const isDatasetUnisample = useMemo(() => sample?.children.length === 1, [sample]);
useEffect(() => {
dispatch(loadCellSets(experimentId));
Expand Down Expand Up @@ -182,7 +196,7 @@ const BatchDiffExpression = (props) => {
onChange={(value) => changeComparison({ basis: value })}
value={comparison.basis}
style={{ width: '40%' }}
options={getSelectOptions(cellSetNodes)}
options={getSelectOptions(cellDefinedNodes)}
/>
<br />
</>
Expand All @@ -196,7 +210,7 @@ const BatchDiffExpression = (props) => {
<DiffExprSelect
title='Compare sample/group:'
option='cellSet'
filterType='metadataCategorical'
filterTypes={['metadataCategorical', 'CLMPerSample']}
onSelectCluster={(cellSet) => changeComparison({ cellSet })}
selectedComparison={{ cellSet: comparison.cellSet }}
value={comparison.cellSet}
Expand All @@ -205,7 +219,7 @@ const BatchDiffExpression = (props) => {
<DiffExprSelect
title='To sample/group:'
option='compareWith'
filterType='metadataCategorical'
filterTypes={['metadataCategorical', 'CLMPerSample']}
onSelectCluster={(cellSet) => changeComparison({ compareWith: cellSet })}
selectedComparison={{ cellSet: comparison.cellSet }}
value={comparison.compareWith}
Expand All @@ -218,7 +232,7 @@ const BatchDiffExpression = (props) => {
onChange={(value) => changeComparison({ basis: value })}
value={comparison.basis}
style={{ width: '33.5%' }}
options={getSelectOptions(cellSetNodes)}
options={getSelectOptions(cellDefinedNodes)}
/>
</>
);
Expand All @@ -231,7 +245,7 @@ const BatchDiffExpression = (props) => {
<DiffExprSelect
title='Compare cell set:'
option='cellSet'
filterType='cellSets'
filterTypes={['cellSets', 'CLM']}
onSelectCluster={(cellSet) => changeComparison({ cellSet })}
selectedComparison={{ cellSet: comparison.cellSet }}
value={comparison.cellSet}
Expand All @@ -240,7 +254,7 @@ const BatchDiffExpression = (props) => {
<DiffExprSelect
title='To cell set:'
option='compareWith'
filterType='cellSets'
filterTypes={['cellSets', 'CLM']}
onSelectCluster={(cellSet) => changeComparison({ compareWith: cellSet })}
selectedComparison={{ cellSet: comparison.cellSet }}
value={comparison.compareWith}
Expand All @@ -253,7 +267,7 @@ const BatchDiffExpression = (props) => {
onChange={(value) => changeComparison({ basis: value })}
value={comparison.basis}
style={{ width: '34%' }}
options={getSelectOptions(metadataCellSetNodes)}
options={getSelectOptions(sampleDefinedNodes)}
/>
</>
);
Expand Down
Loading

0 comments on commit f317670

Please sign in to comment.