Skip to content

Commit 8b918f2

Browse files
committed
feat: map dosing and observation variables (#351)
* Map data to model variables Show a table of dose amount data (and units), with select menus to map these to model inputs. Show a table of observation variables (and units), with select menus to map those to model outputs. * Add a mapped qname to biomarkers Store the mapped variable qname on biomarker types. Read it from OBSERVATION_VARIABLE in a dataset. * Support ES2015 * Allow for multiple variable mappings Map each Administration ID to a dosing compartment. Map each Observation ID to a model output. Add new columns to the CSV data, with mappings and optional units. * Preview the final dataset Add a final step to the upload, which will preview the final CSV before saving it. * Save modified dataset to backend - load or create a dataset when we start an upload. - save the dataset when we finish an upload. - modify the `/datasets/:dataset_id:/csv` endpoint to accept a JSON string. - update the dataset API to allow filtering by project ID. * Allow for a single unit column - When there's a single unit column, use that column for both dosing and observations. - Split the CSV data into dosing rows and observation rows. - Add administration route to the Map Dosing screen. - Allow for dimensionless observation units. - Filter mapped observation variables for compatibility with the observation unit value.
1 parent 61944c9 commit 8b918f2

17 files changed

+665
-65
lines changed

frontend-v2/src/app/backendApi.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@ const injectedRtkApi = api.injectEndpoints({
236236
}),
237237
}),
238238
datasetList: build.query<DatasetListApiResponse, DatasetListApiArg>({
239-
query: () => ({ url: `/api/dataset/` }),
239+
query: (queryArg) => ({
240+
url: `/api/dataset/`,
241+
params: { project_id: queryArg.projectId },
242+
}),
240243
}),
241244
datasetCreate: build.mutation<
242245
DatasetCreateApiResponse,
@@ -1158,7 +1161,10 @@ export type CompoundDestroyApiArg = {
11581161
id: number;
11591162
};
11601163
export type DatasetListApiResponse = /** status 200 */ DatasetRead[];
1161-
export type DatasetListApiArg = void;
1164+
export type DatasetListApiArg = {
1165+
/** Filter results by project ID */
1166+
projectId?: number;
1167+
};
11621168
export type DatasetCreateApiResponse = /** status 201 */ DatasetRead;
11631169
export type DatasetCreateApiArg = {
11641170
dataset: Dataset;
@@ -1671,6 +1677,7 @@ export type BiomarkerType = {
16711677
display?: boolean;
16721678
color?: number;
16731679
axis?: boolean;
1680+
mapped_qname?: string;
16741681
stored_unit: number;
16751682
dataset: number;
16761683
display_unit: number;
@@ -1689,6 +1696,7 @@ export type BiomarkerTypeRead = {
16891696
display?: boolean;
16901697
color?: number;
16911698
axis?: boolean;
1699+
mapped_qname?: string;
16921700
stored_unit: number;
16931701
dataset: number;
16941702
display_unit: number;
@@ -1701,6 +1709,7 @@ export type PatchedBiomarkerType = {
17011709
display?: boolean;
17021710
color?: number;
17031711
axis?: boolean;
1712+
mapped_qname?: string;
17041713
stored_unit?: number;
17051714
dataset?: number;
17061715
display_unit?: number;
@@ -1719,6 +1728,7 @@ export type PatchedBiomarkerTypeRead = {
17191728
display?: boolean;
17201729
color?: number;
17211730
axis?: boolean;
1731+
mapped_qname?: string;
17221732
stored_unit?: number;
17231733
dataset?: number;
17241734
display_unit?: number;
@@ -2127,9 +2137,7 @@ export type Inference = {
21272137
time_elapsed?: number;
21282138
number_of_function_evals?: number;
21292139
task_id?: string | null;
2130-
metadata?: {
2131-
[key: string]: any;
2132-
};
2140+
metadata?: any;
21332141
project: number;
21342142
algorithm?: number;
21352143
initialization_inference?: number | null;
@@ -2149,9 +2157,7 @@ export type InferenceRead = {
21492157
time_elapsed?: number;
21502158
number_of_function_evals?: number;
21512159
task_id?: string | null;
2152-
metadata?: {
2153-
[key: string]: any;
2154-
};
2160+
metadata?: any;
21552161
project: number;
21562162
algorithm?: number;
21572163
initialization_inference?: number | null;
@@ -2170,9 +2176,7 @@ export type PatchedInference = {
21702176
time_elapsed?: number;
21712177
number_of_function_evals?: number;
21722178
task_id?: string | null;
2173-
metadata?: {
2174-
[key: string]: any;
2175-
};
2179+
metadata?: any;
21762180
project?: number;
21772181
algorithm?: number;
21782182
initialization_inference?: number | null;
@@ -2192,9 +2196,7 @@ export type PatchedInferenceRead = {
21922196
time_elapsed?: number;
21932197
number_of_function_evals?: number;
21942198
task_id?: string | null;
2195-
metadata?: {
2196-
[key: string]: any;
2197-
};
2199+
metadata?: any;
21982200
project?: number;
21992201
algorithm?: number;
22002202
initialization_inference?: number | null;

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

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Alert, Box, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
2-
import Papa, { ParseError, ParseMeta } from 'papaparse'
3-
import React, {useCallback, useState} from 'react'
1+
import { Alert, Box, Stack } from '@mui/material';
2+
import Papa from 'papaparse'
3+
import { FC, useCallback, useState} from 'react'
44
import {useDropzone} from 'react-dropzone'
55
import MapHeaders from './MapHeaders';
6-
import { manditoryHeaders, normaliseHeader, normalisedHeaders } from './normaliseDataHeaders';
6+
import { manditoryHeaders, normaliseHeader } from './normaliseDataHeaders';
77
import { StepperState } from './LoadDataStepper';
88

99
export type Row = {[key: string]: string};
@@ -48,7 +48,7 @@ const validateNormalisedFields = (fields: Field[]) => {
4848
return errors;
4949
}
5050

51-
const LoadData: React.FC<ILoadDataProps> = ({state, firstTime}) => {
51+
const LoadData: FC<ILoadDataProps> = ({state, firstTime}) => {
5252
const [errors, setErrors] = useState<string[]>(firstTime ? [] : validateNormalisedFields(state.normalisedFields));
5353
const [showData, setShowData] = useState<boolean>(state.data.length > 0 && state.fields.length > 0);
5454

@@ -62,7 +62,6 @@ const LoadData: React.FC<ILoadDataProps> = ({state, firstTime}) => {
6262
// Parse the CSV data
6363
const rawCsv = reader.result as string;
6464
const csvData = Papa.parse(rawCsv.trim(), { header: true });
65-
const data = csvData.data as Data;
6665
const fields = csvData.meta.fields || [];
6766
const normalisedFields = fields.map(normaliseHeader);
6867
const errors = csvData.errors.map((e) => e.message).concat(validateNormalisedFields(normalisedFields));
@@ -77,7 +76,7 @@ const LoadData: React.FC<ILoadDataProps> = ({state, firstTime}) => {
7776
reader.readAsText(file)
7877
})
7978

80-
}, [])
79+
}, [state])
8180
const {getRootProps, getInputProps} = useDropzone({onDrop})
8281

8382
const setNormalisedFields = (fields: Field[]) => {

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

+78-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import * as React from 'react';
1+
import { FC, useEffect } from 'react';
2+
import { useSelector } from "react-redux";
3+
import Papa from 'papaparse'
24
import Box from '@mui/material/Box';
35
import Stepper from '@mui/material/Stepper';
46
import Step from '@mui/material/Step';
@@ -8,9 +10,18 @@ import Typography from '@mui/material/Typography';
810
import LoadData from './LoadData';
911
import { useState } from 'react';
1012
import MapObservations from './MapObservations';
13+
import MapDosing from './MapDosing';
14+
import PreviewData from './PreviewData';
15+
import { RootState } from "../../app/store";
16+
import {
17+
DatasetRead,
18+
useDatasetListQuery,
19+
useDatasetCreateMutation,
20+
useDatasetCsvUpdateMutation,
21+
} from '../../app/backendApi';
1122

12-
const stepLabels = ['Upload Data', 'Map Observations'];
13-
const stepComponents = [LoadData, MapObservations];
23+
const stepLabels = ['Upload Data', 'Map Dosing', 'Map Observations', 'Preview Dataset'];
24+
const stepComponents = [LoadData, MapDosing, MapObservations, PreviewData];
1425

1526
type Row = {[key: string]: string};
1627
type Data = Row[];
@@ -27,17 +38,30 @@ export type StepperState = {
2738
setData: (data: Data) => void;
2839
amountUnit?: string;
2940
setAmountUnit: (amountUnit: string) => void;
30-
observationUnits?: {[key: string]: string};
31-
setObservationUnits: (observationUnits: {[key: string]: string}) => void;
3241
}
3342

34-
const LoadDataStepper: React.FC = () => {
43+
const LoadDataStepper: FC = () => {
44+
const [dataset, setDataset] = useState<null | DatasetRead>(null);
3545
const [data, setData] = useState<Data>([]);
3646
const [fields, setFields] = useState<string[]>([]);
3747
const [normalisedFields, setNormalisedFields] = useState<string[]>([]);
3848
const [timeUnit, setTimeUnit] = useState<string | undefined>(undefined);
3949
const [amountUnit, setAmountUnit] = useState<string | undefined>(undefined);
40-
const [observationUnits, setObservationUnits] = useState<{[key: string]: string}>({});
50+
const selectedProject = useSelector(
51+
(state: RootState) => state.main.selectedProject,
52+
);
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();
61+
const [
62+
updateDataset
63+
] = useDatasetCsvUpdateMutation();
64+
4165

4266
const state = {
4367
fields,
@@ -49,13 +73,50 @@ const LoadDataStepper: React.FC = () => {
4973
timeUnit,
5074
setTimeUnit,
5175
amountUnit,
52-
setAmountUnit,
53-
observationUnits,
54-
setObservationUnits,
76+
setAmountUnit
5577
};
5678

5779
const [stepState, setStepState] = useState({ activeStep: 0, maxStep: 0 });
5880
const StepComponent = stepComponents[stepState.activeStep];
81+
const isFinished = stepState.activeStep === stepLabels.length;
82+
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+
105+
useEffect(function onFinished() {
106+
if (isFinished && dataset?.id) {
107+
try {
108+
const csv = Papa.unparse(data);
109+
updateDataset({
110+
id: dataset.id,
111+
datasetCsv: {
112+
csv
113+
}
114+
})
115+
} catch (e) {
116+
console.error(e);
117+
}
118+
}
119+
}, [isFinished, updateDataset, dataset?.id, data])
59120

60121
const handleNext = () => {
61122
setStepState((prevActiveStep) => ({
@@ -77,10 +138,15 @@ const LoadDataStepper: React.FC = () => {
77138
</Step>
78139
))}
79140
</Stepper>
80-
<Typography>{stepState.activeStep === stepLabels.length ? 'The process is completed' : <StepComponent state={state} firstTime={stepState.activeStep === stepState.maxStep}/>}</Typography>
141+
<Typography>
142+
{isFinished ?
143+
'The process is completed' :
144+
<StepComponent state={state} firstTime={stepState.activeStep === stepState.maxStep}/>
145+
}
146+
</Typography>
81147
<Box sx={{ display: 'flex', justifyContent: 'space-between', marginTop: 1 }}>
82148
<Button disabled={stepState.activeStep === 0} onClick={handleBack}>Back</Button>
83-
<Button variant="contained" color="primary" onClick={handleNext}>
149+
<Button disabled={isFinished} variant="contained" color="primary" onClick={handleNext}>
84150
{stepState.activeStep === stepLabels.length - 1 ? 'Finish' : 'Next'}
85151
</Button>
86152
</Box>

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

+2-12
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
1-
import * as React from 'react';
1+
import { useState } from 'react';
22
import Button from '@mui/material/Button';
3-
import Avatar from '@mui/material/Avatar';
4-
import List from '@mui/material/List';
5-
import ListItem from '@mui/material/ListItem';
6-
import ListItemAvatar from '@mui/material/ListItemAvatar';
7-
import ListItemButton from '@mui/material/ListItemButton';
8-
import ListItemText from '@mui/material/ListItemText';
93
import DialogTitle from '@mui/material/DialogTitle';
104
import Dialog from '@mui/material/Dialog';
11-
import PersonIcon from '@mui/icons-material/Person';
12-
import AddIcon from '@mui/icons-material/Add';
13-
import Typography from '@mui/material/Typography';
14-
import { blue } from '@mui/material/colors';
155
import LoadDataStepper from './LoadDataStepper';
166
import { DialogContent } from '@mui/material';
177

@@ -38,7 +28,7 @@ function LoadDataDialog(props: LoadDataDialogProps) {
3828
}
3929

4030
export default function LoadDataTab() {
41-
const [open, setOpen] = React.useState(false);
31+
const [open, setOpen] = useState(false);
4232

4333
const handleClickOpen = () => {
4434
setOpen(true);

0 commit comments

Comments
 (0)