Skip to content

Commit 502b27c

Browse files
committed
feat: data stratification (#361)
- Replace the tabbed interface for data uploads with a stepper. - Add utilities to group subjects by protocol. - Add stratification to the stepper. - Stratify by dose protocols for the time being. - Display each protocol group as a MUI data grid. - Display protocol groups as tabs in the trial design view. - Add the dosing compartment qname to the protocol model. - Add a 'cohort' field to the CSV.
1 parent 8b918f2 commit 502b27c

20 files changed

+621
-218
lines changed

frontend-v2/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"@fontsource/comfortaa": "^5.0.18",
99
"@mui/icons-material": "^5.11.16",
1010
"@mui/material": "^5.12.2",
11+
"@mui/x-data-grid": "^6.19.6",
1112
"@reduxjs/toolkit": "^1.8.1",
1213
"@testing-library/jest-dom": "^5.16.4",
1314
"@testing-library/react": "^13.0.1",

frontend-v2/src/app/backendApi.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2004,6 +2004,7 @@ export type Protocol = {
20042004
datetime?: string | null;
20052005
name: string;
20062006
dose_type?: DoseTypeEnum;
2007+
mapped_qname?: string;
20072008
project?: number | null;
20082009
compound?: number | null;
20092010
time_unit?: number | null;
@@ -2019,6 +2020,7 @@ export type ProtocolRead = {
20192020
datetime?: string | null;
20202021
name: string;
20212022
dose_type?: DoseTypeEnum;
2023+
mapped_qname?: string;
20222024
project?: number | null;
20232025
compound?: number | null;
20242026
time_unit?: number | null;
@@ -2381,6 +2383,7 @@ export type PatchedProtocol = {
23812383
datetime?: string | null;
23822384
name?: string;
23832385
dose_type?: DoseTypeEnum;
2386+
mapped_qname?: string;
23842387
project?: number | null;
23852388
compound?: number | null;
23862389
time_unit?: number | null;
@@ -2396,6 +2399,7 @@ export type PatchedProtocolRead = {
23962399
datetime?: string | null;
23972400
name?: string;
23982401
dose_type?: DoseTypeEnum;
2402+
mapped_qname?: string;
23992403
project?: number | null;
24002404
compound?: number | null;
24012405
time_unit?: number | null;
+17-22
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
1-
import { DynamicTabs, TabPanel } from "../../components/DynamicTabs";
2-
import LoadDataTab from "./LoadDataTab";
3-
import { SubPageName } from "../main/mainSlice";
1+
import { FC, useState } from "react";
2+
import { Button } from "@mui/material";
3+
import LoadDataStepper from "./LoadDataStepper";
44

5-
const Data: React.FC = () => {
6-
const tabKeys = [
7-
SubPageName.LOAD_DATA,
8-
SubPageName.STRATIFICATION,
9-
SubPageName.VISUALISATION
10-
];
11-
return (
12-
<DynamicTabs tabNames={tabKeys}>
13-
<TabPanel>
14-
<LoadDataTab />
15-
</TabPanel>
16-
<TabPanel>
17-
<LoadDataTab />
18-
</TabPanel>
19-
<TabPanel>
20-
<LoadDataTab />
21-
</TabPanel>
22-
</DynamicTabs>
23-
);
5+
const Data:FC = () => {
6+
const [isLoading, setIsLoading] = useState(false);
7+
function handleNewUpload() {
8+
setIsLoading(true);
9+
}
10+
function onUploadComplete() {
11+
setIsLoading(false);
12+
}
13+
14+
return isLoading ?
15+
<LoadDataStepper onFinish={onUploadComplete} /> :
16+
<Button variant="outlined" onClick={handleNewUpload}>
17+
Upload new dataset
18+
</Button>;
2419
}
2520

2621
export default Data;

frontend-v2/src/features/data/LoadDataStepper.tsx

+19-44
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ import MapObservations from './MapObservations';
1313
import MapDosing from './MapDosing';
1414
import PreviewData from './PreviewData';
1515
import { RootState } from "../../app/store";
16-
import {
17-
DatasetRead,
18-
useDatasetListQuery,
19-
useDatasetCreateMutation,
20-
useDatasetCsvUpdateMutation,
21-
} from '../../app/backendApi';
16+
import { DatasetRead, useDatasetCsvUpdateMutation } from '../../app/backendApi';
17+
import Stratification from './Stratification';
18+
import useDataset from '../../hooks/useDataset';
2219

23-
const stepLabels = ['Upload Data', 'Map Dosing', 'Map Observations', 'Preview Dataset'];
24-
const stepComponents = [LoadData, MapDosing, MapObservations, PreviewData];
20+
interface IStepper {
21+
onFinish: () => void
22+
}
23+
24+
const stepLabels = ['Upload Data', 'Map Dosing', 'Map Observations', 'Stratification', 'Preview Dataset'];
25+
const stepComponents = [LoadData, MapDosing, MapObservations, Stratification, PreviewData];
2526

2627
type Row = {[key: string]: string};
2728
type Data = Row[];
@@ -40,8 +41,7 @@ export type StepperState = {
4041
setAmountUnit: (amountUnit: string) => void;
4142
}
4243

43-
const LoadDataStepper: FC = () => {
44-
const [dataset, setDataset] = useState<null | DatasetRead>(null);
44+
const LoadDataStepper: FC<IStepper> = ({ onFinish }) => {
4545
const [data, setData] = useState<Data>([]);
4646
const [fields, setFields] = useState<string[]>([]);
4747
const [normalisedFields, setNormalisedFields] = useState<string[]>([]);
@@ -50,18 +50,10 @@ const LoadDataStepper: FC = () => {
5050
const selectedProject = useSelector(
5151
(state: RootState) => state.main.selectedProject,
5252
);
53-
const selectedProjectOrZero = selectedProject || 0;
54-
const { data: datasets = [], isLoading: isDatasetLoading } = useDatasetListQuery(
55-
{ projectId: selectedProjectOrZero },
56-
{ skip: !selectedProject },
57-
);
58-
const [
59-
createDataset
60-
] = useDatasetCreateMutation();
6153
const [
62-
updateDataset
54+
updateDatasetCsv
6355
] = useDatasetCsvUpdateMutation();
64-
56+
const { dataset, updateDataset } = useDataset(selectedProject);
6557

6658
const state = {
6759
fields,
@@ -80,43 +72,26 @@ const LoadDataStepper: FC = () => {
8072
const StepComponent = stepComponents[stepState.activeStep];
8173
const isFinished = stepState.activeStep === stepLabels.length;
8274

83-
useEffect(function onDataLoad() {
84-
async function addDataset() {
85-
let [dataset] = datasets;
86-
if (!dataset) {
87-
const response = await createDataset({
88-
dataset: {
89-
name: 'New Dataset',
90-
project: selectedProjectOrZero,
91-
}
92-
});
93-
if ('data' in response && response.data) {
94-
dataset = response.data;
95-
}
96-
}
97-
console.log({dataset});
98-
setDataset(dataset);
99-
}
100-
if (!isDatasetLoading) {
101-
addDataset();
102-
}
103-
}, [datasets, createDataset, isDatasetLoading]);
104-
10575
useEffect(function onFinished() {
10676
if (isFinished && dataset?.id) {
10777
try {
10878
const csv = Papa.unparse(data);
109-
updateDataset({
79+
updateDatasetCsv({
11080
id: dataset.id,
11181
datasetCsv: {
11282
csv
11383
}
11484
})
85+
.unwrap()
86+
.then(data => {
87+
updateDataset(data as unknown as DatasetRead);
88+
onFinish();
89+
});
11590
} catch (e) {
11691
console.error(e);
11792
}
11893
}
119-
}, [isFinished, updateDataset, dataset?.id, data])
94+
}, [isFinished, onFinish, updateDatasetCsv, updateDataset, dataset?.id, data])
12095

12196
const handleNext = () => {
12297
setStepState((prevActiveStep) => ({

frontend-v2/src/features/data/LoadDataTab.tsx

-52
This file was deleted.

frontend-v2/src/features/data/MapDosing.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const MapDosing: FC<IMapDosing> = ({ state, firstTime }: IMapDosing) => {
8484
const { value } = event.target;
8585
nextData.filter(row => administrationIdField ? row[administrationIdField] === id : true)
8686
.forEach(row => {
87-
row['Dosing Variable'] = value;
87+
row['Amount Variable'] = value;
8888
})
8989
state.setData(nextData);
9090
}
@@ -134,7 +134,7 @@ const MapDosing: FC<IMapDosing> = ({ state, firstTime }: IMapDosing) => {
134134
<TableBody>
135135
{uniqueAdministrationIds.map((adminId, index) => {
136136
const currentRow = dosingRows.find(row => administrationIdField ? row[administrationIdField] === adminId : true);
137-
const selectedVariable = variables?.find(variable => variable.qname === currentRow?.['Dosing Variable']);
137+
const selectedVariable = variables?.find(variable => variable.qname === currentRow?.['Amount Variable']);
138138
const compatibleUnits = units?.find(unit => unit.id === selectedVariable?.unit)?.compatible_units;
139139
const adminUnit = amountUnitField && currentRow && currentRow[amountUnitField];
140140
const amount = amountField && currentRow?.[amountField];

frontend-v2/src/features/data/PreviewData.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const PreviewData: FC<IPreviewData> = ({ state, firstTime }: IPreviewData) => {
1111
const { data } = state;
1212
const fields = [
1313
...state.fields,
14-
'Dosing Variable',
14+
'cohort',
15+
'Amount Variable',
1516
'Observation Variable'
1617
];
1718
if (!state.normalisedFields.find(field => field === 'Amount Unit')) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { FC } from 'react';
2+
import { DataGrid } from '@mui/x-data-grid';
3+
import { IProtocol } from './protocolUtils';
4+
import { StepperState } from './LoadDataStepper';
5+
6+
interface IProtocolDataGrid {
7+
protocol: IProtocol
8+
state: StepperState
9+
}
10+
11+
const ProtocolDataGrid: FC<IProtocolDataGrid> = ({ protocol, state }) => {
12+
const idField = state.fields.find((field, index) => state.normalisedFields[index] === 'ID');
13+
const amountField = state.fields.find((field, index) => state.normalisedFields[index] === 'Amount');
14+
const { subjects } = protocol;
15+
const protocolRows = state.data.filter(row => {
16+
const subjectId = idField && row[idField];
17+
const amount = amountField && +row[amountField];
18+
return subjects.includes(subjectId || '') && amount;
19+
}).map(row => {
20+
const subjectId = (idField && +row[idField]) || 0;
21+
return { id: +subjectId, ...row };
22+
});
23+
const protocolColumns = state.fields.map((field) => ({ field, headerName: field }));
24+
return (
25+
<DataGrid
26+
rows={protocolRows}
27+
columns={protocolColumns}
28+
checkboxSelection
29+
/>
30+
);
31+
}
32+
33+
export default ProtocolDataGrid;

0 commit comments

Comments
 (0)