diff --git a/frontend-v2/package.json b/frontend-v2/package.json index 120f7066..124c64af 100644 --- a/frontend-v2/package.json +++ b/frontend-v2/package.json @@ -8,6 +8,7 @@ "@fontsource/comfortaa": "^5.0.18", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.12.2", + "@mui/x-data-grid": "^6.19.6", "@reduxjs/toolkit": "^1.8.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.0.1", diff --git a/frontend-v2/src/app/backendApi.ts b/frontend-v2/src/app/backendApi.ts index 6fd8a602..6df87699 100644 --- a/frontend-v2/src/app/backendApi.ts +++ b/frontend-v2/src/app/backendApi.ts @@ -2004,6 +2004,7 @@ export type Protocol = { datetime?: string | null; name: string; dose_type?: DoseTypeEnum; + mapped_qname?: string; project?: number | null; compound?: number | null; time_unit?: number | null; @@ -2019,6 +2020,7 @@ export type ProtocolRead = { datetime?: string | null; name: string; dose_type?: DoseTypeEnum; + mapped_qname?: string; project?: number | null; compound?: number | null; time_unit?: number | null; @@ -2381,6 +2383,7 @@ export type PatchedProtocol = { datetime?: string | null; name?: string; dose_type?: DoseTypeEnum; + mapped_qname?: string; project?: number | null; compound?: number | null; time_unit?: number | null; @@ -2396,6 +2399,7 @@ export type PatchedProtocolRead = { datetime?: string | null; name?: string; dose_type?: DoseTypeEnum; + mapped_qname?: string; project?: number | null; compound?: number | null; time_unit?: number | null; diff --git a/frontend-v2/src/features/data/Data.tsx b/frontend-v2/src/features/data/Data.tsx index b8b54540..65e2d80e 100644 --- a/frontend-v2/src/features/data/Data.tsx +++ b/frontend-v2/src/features/data/Data.tsx @@ -1,26 +1,21 @@ -import { DynamicTabs, TabPanel } from "../../components/DynamicTabs"; -import LoadDataTab from "./LoadDataTab"; -import { SubPageName } from "../main/mainSlice"; +import { FC, useState } from "react"; +import { Button } from "@mui/material"; +import LoadDataStepper from "./LoadDataStepper"; -const Data: React.FC = () => { - const tabKeys = [ - SubPageName.LOAD_DATA, - SubPageName.STRATIFICATION, - SubPageName.VISUALISATION - ]; - return ( - - - - - - - - - - - - ); +const Data:FC = () => { + const [isLoading, setIsLoading] = useState(false); + function handleNewUpload() { + setIsLoading(true); + } + function onUploadComplete() { + setIsLoading(false); + } + + return isLoading ? + : + ; } export default Data; diff --git a/frontend-v2/src/features/data/LoadDataStepper.tsx b/frontend-v2/src/features/data/LoadDataStepper.tsx index 28a1c618..431e8199 100644 --- a/frontend-v2/src/features/data/LoadDataStepper.tsx +++ b/frontend-v2/src/features/data/LoadDataStepper.tsx @@ -13,15 +13,16 @@ import MapObservations from './MapObservations'; import MapDosing from './MapDosing'; import PreviewData from './PreviewData'; import { RootState } from "../../app/store"; -import { - DatasetRead, - useDatasetListQuery, - useDatasetCreateMutation, - useDatasetCsvUpdateMutation, -} from '../../app/backendApi'; +import { DatasetRead, useDatasetCsvUpdateMutation } from '../../app/backendApi'; +import Stratification from './Stratification'; +import useDataset from '../../hooks/useDataset'; -const stepLabels = ['Upload Data', 'Map Dosing', 'Map Observations', 'Preview Dataset']; -const stepComponents = [LoadData, MapDosing, MapObservations, PreviewData]; +interface IStepper { + onFinish: () => void +} + +const stepLabels = ['Upload Data', 'Map Dosing', 'Map Observations', 'Stratification', 'Preview Dataset']; +const stepComponents = [LoadData, MapDosing, MapObservations, Stratification, PreviewData]; type Row = {[key: string]: string}; type Data = Row[]; @@ -40,8 +41,7 @@ export type StepperState = { setAmountUnit: (amountUnit: string) => void; } -const LoadDataStepper: FC = () => { - const [dataset, setDataset] = useState(null); +const LoadDataStepper: FC = ({ onFinish }) => { const [data, setData] = useState([]); const [fields, setFields] = useState([]); const [normalisedFields, setNormalisedFields] = useState([]); @@ -50,18 +50,10 @@ const LoadDataStepper: FC = () => { const selectedProject = useSelector( (state: RootState) => state.main.selectedProject, ); - const selectedProjectOrZero = selectedProject || 0; - const { data: datasets = [], isLoading: isDatasetLoading } = useDatasetListQuery( - { projectId: selectedProjectOrZero }, - { skip: !selectedProject }, - ); - const [ - createDataset - ] = useDatasetCreateMutation(); const [ - updateDataset + updateDatasetCsv ] = useDatasetCsvUpdateMutation(); - + const { dataset, updateDataset } = useDataset(selectedProject); const state = { fields, @@ -80,43 +72,26 @@ const LoadDataStepper: FC = () => { const StepComponent = stepComponents[stepState.activeStep]; const isFinished = stepState.activeStep === stepLabels.length; - useEffect(function onDataLoad() { - async function addDataset() { - let [dataset] = datasets; - if (!dataset) { - const response = await createDataset({ - dataset: { - name: 'New Dataset', - project: selectedProjectOrZero, - } - }); - if ('data' in response && response.data) { - dataset = response.data; - } - } - console.log({dataset}); - setDataset(dataset); - } - if (!isDatasetLoading) { - addDataset(); - } - }, [datasets, createDataset, isDatasetLoading]); - useEffect(function onFinished() { if (isFinished && dataset?.id) { try { const csv = Papa.unparse(data); - updateDataset({ + updateDatasetCsv({ id: dataset.id, datasetCsv: { csv } }) + .unwrap() + .then(data => { + updateDataset(data as unknown as DatasetRead); + onFinish(); + }); } catch (e) { console.error(e); } } - }, [isFinished, updateDataset, dataset?.id, data]) + }, [isFinished, onFinish, updateDatasetCsv, updateDataset, dataset?.id, data]) const handleNext = () => { setStepState((prevActiveStep) => ({ diff --git a/frontend-v2/src/features/data/LoadDataTab.tsx b/frontend-v2/src/features/data/LoadDataTab.tsx deleted file mode 100644 index 864c534d..00000000 --- a/frontend-v2/src/features/data/LoadDataTab.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useState } from 'react'; -import Button from '@mui/material/Button'; -import DialogTitle from '@mui/material/DialogTitle'; -import Dialog from '@mui/material/Dialog'; -import LoadDataStepper from './LoadDataStepper'; -import { DialogContent } from '@mui/material'; - -export interface LoadDataDialogProps { - open: boolean; - onClose: () => void; -} - -function LoadDataDialog(props: LoadDataDialogProps) { - const { onClose, open } = props; - - const handleClose = () => { - onClose(); - }; - - return ( - - Upload New Dataset - - - - - ); -} - -export default function LoadDataTab() { - const [open, setOpen] = useState(false); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( -
- - -
- ); -} \ No newline at end of file diff --git a/frontend-v2/src/features/data/MapDosing.tsx b/frontend-v2/src/features/data/MapDosing.tsx index cb087ac5..1ddd6535 100644 --- a/frontend-v2/src/features/data/MapDosing.tsx +++ b/frontend-v2/src/features/data/MapDosing.tsx @@ -84,7 +84,7 @@ const MapDosing: FC = ({ state, firstTime }: IMapDosing) => { const { value } = event.target; nextData.filter(row => administrationIdField ? row[administrationIdField] === id : true) .forEach(row => { - row['Dosing Variable'] = value; + row['Amount Variable'] = value; }) state.setData(nextData); } @@ -134,7 +134,7 @@ const MapDosing: FC = ({ state, firstTime }: IMapDosing) => { {uniqueAdministrationIds.map((adminId, index) => { const currentRow = dosingRows.find(row => administrationIdField ? row[administrationIdField] === adminId : true); - const selectedVariable = variables?.find(variable => variable.qname === currentRow?.['Dosing Variable']); + const selectedVariable = variables?.find(variable => variable.qname === currentRow?.['Amount Variable']); const compatibleUnits = units?.find(unit => unit.id === selectedVariable?.unit)?.compatible_units; const adminUnit = amountUnitField && currentRow && currentRow[amountUnitField]; const amount = amountField && currentRow?.[amountField]; diff --git a/frontend-v2/src/features/data/PreviewData.tsx b/frontend-v2/src/features/data/PreviewData.tsx index 9713a8f5..0488e024 100644 --- a/frontend-v2/src/features/data/PreviewData.tsx +++ b/frontend-v2/src/features/data/PreviewData.tsx @@ -11,7 +11,8 @@ const PreviewData: FC = ({ state, firstTime }: IPreviewData) => { const { data } = state; const fields = [ ...state.fields, - 'Dosing Variable', + 'cohort', + 'Amount Variable', 'Observation Variable' ]; if (!state.normalisedFields.find(field => field === 'Amount Unit')) { diff --git a/frontend-v2/src/features/data/ProtocolDataGrid.tsx b/frontend-v2/src/features/data/ProtocolDataGrid.tsx new file mode 100644 index 00000000..5e745803 --- /dev/null +++ b/frontend-v2/src/features/data/ProtocolDataGrid.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import { DataGrid } from '@mui/x-data-grid'; +import { IProtocol } from './protocolUtils'; +import { StepperState } from './LoadDataStepper'; + +interface IProtocolDataGrid { + protocol: IProtocol + state: StepperState +} + +const ProtocolDataGrid: FC = ({ protocol, state }) => { + const idField = state.fields.find((field, index) => state.normalisedFields[index] === 'ID'); + const amountField = state.fields.find((field, index) => state.normalisedFields[index] === 'Amount'); + const { subjects } = protocol; + const protocolRows = state.data.filter(row => { + const subjectId = idField && row[idField]; + const amount = amountField && +row[amountField]; + return subjects.includes(subjectId || '') && amount; + }).map(row => { + const subjectId = (idField && +row[idField]) || 0; + return { id: +subjectId, ...row }; + }); + const protocolColumns = state.fields.map((field) => ({ field, headerName: field })); + return ( + + ); +} + +export default ProtocolDataGrid; diff --git a/frontend-v2/src/features/data/Stratification.tsx b/frontend-v2/src/features/data/Stratification.tsx new file mode 100644 index 00000000..ba06af36 --- /dev/null +++ b/frontend-v2/src/features/data/Stratification.tsx @@ -0,0 +1,144 @@ +import { ChangeEvent, FC, useState } from 'react'; +import { + Box, + Checkbox, + Radio, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + Tabs, + Tab, + Typography +} from "@mui/material"; +import { StepperState } from "./LoadDataStepper"; +import { getProtocols, getSubjectDoses } from "./protocolUtils"; +import ProtocolDataGrid from './ProtocolDataGrid'; + + + +interface IStratification { + state: StepperState; + firstTime: boolean; +} + +const Stratification: FC = ({ state, firstTime }: IStratification) => { + const subjectDoses = getSubjectDoses(state); + const protocols = getProtocols(subjectDoses); + const catCovariates = state.fields.filter((field, index) => + state.normalisedFields[index] === 'Cat Covariate' && field.toLowerCase() !== 'route' + ); + const uniqueCovariateValues = catCovariates.map(field => { + const values = state.data.map(row => row[field]); + return [...new Set(values)]; + }); + + const [firstRow] = state.data; + const primaryCohort = catCovariates.find((field => firstRow[field] === firstRow.cohort)); + const [primary, setPrimary] = useState(primaryCohort || ''); + const [secondary, setSecondary] = useState([]); + const [tab, setTab] = useState(0); + + const handleTabChange = (event: ChangeEvent<{}>, newValue: number) => { + setTab(newValue); + } + + const handlePrimaryChange = (event: ChangeEvent) => { + setPrimary(event.target.value); + const nextData = [...state.data]; + nextData.forEach(row => { + row.cohort = row[event.target.value]; + }); + state.setData(nextData); + }; + + const handleSecondaryChange = (event: ChangeEvent) => { + if (!event.target.checked) { + const newState = secondary.filter(value => value !== event.target.value); + setSecondary(newState); + return; + } else { + const newState = new Set([...secondary, event.target.value]); + setSecondary([...newState]); + } + }; + + function a11yProps(index: number) { + return { + id: `protocol-tab-${index}`, + 'aria-controls': `protocol-tabpanel`, + }; + } + + return ( + <> + {!!catCovariates.length && + + + + + Covariate + + + Values + + + Primary Grouping + + + Secondary Grouping + + + + + {catCovariates.map((field, index) => { + const primaryLabel = `heading-primary field-${field}`; + const secondaryLabel = `heading-secondary field-${field}`; + const isPrimary = primary === field; + return ( + + {field} + {uniqueCovariateValues[index].join(',')} + + + + + + + + ); + })} + +
+ } + + {protocols.map((protocol, index) => ( + + ))} + + + + + + ); +} + +export default Stratification; diff --git a/frontend-v2/src/features/data/protocolUtils.ts b/frontend-v2/src/features/data/protocolUtils.ts new file mode 100644 index 00000000..c82d51d4 --- /dev/null +++ b/frontend-v2/src/features/data/protocolUtils.ts @@ -0,0 +1,88 @@ +import { StepperState } from './LoadDataStepper'; + +export interface IDose { + subject: string, + amount: number, + amountUnit?: string, + route?: string, + time?: number, + timeUnit?: string +} + +export type SubjectDoses = IDose[]; + +export interface IProtocol { + label: string, + doses: IDose[], + subjects: string[] +} + +export function getSubjectDoses(state: StepperState): SubjectDoses[] { + const idField = state.fields.find((field, index) => state.normalisedFields[index] === 'ID'); + const amountField = state.fields.find((field, index) => state.normalisedFields[index] === 'Amount'); + const amountUnitField = state.fields.find( + (field, index) => ['Amount Unit', 'Unit'].includes(state.normalisedFields[index]) + ); + const timeField = state.fields.find((field, index) => state.normalisedFields[index] === 'Time'); + const timeUnitField = state.fields.find((field, index) => state.normalisedFields[index] === 'Time Unit'); + const routeField = state.fields.find(field => field.toLowerCase() === 'route'); + const subjectIds = state.data.map(row => idField && row[idField]); + const uniqueSubjectIds = [...new Set(subjectIds)]; + const subjectDoses = uniqueSubjectIds.map(subjectId => { + const subjectRows = state.data.filter(row => idField && row[idField] === subjectId); + return subjectRows.map(row => { + const amount = amountField && +row[amountField]; + const amountUnit = amountUnitField && row[amountUnitField]; + const time = timeField && +row[timeField]; + const timeUnit = timeUnitField && row[timeUnitField]; + const route = routeField ? row[routeField] : ''; + return { + subject: subjectId || '', + amount: amount || 0, + amountUnit, + label: `Dose: ${amount} ${route}`, + route, + time: time || 0, + timeUnit + }; + }) + .filter(row => !!row.amount); + }); + return subjectDoses; +} + +export function stripDoses(subjectDosing: IDose[] = []) { +// strip subject ID from dosing rows. + return subjectDosing.map((dose: IDose) => { + const { subject, ...rest } = dose; + return rest; + }) +} + +export function uniqueDoses(subjectDoses: SubjectDoses[] = []) { + // assume duplicate doses have the same JSON string representation. + const doses = subjectDoses.map(subjectDosing => JSON.stringify(stripDoses(subjectDosing))); + return [...new Set(doses)].map(dose => JSON.parse(dose)); +} + +export function getProtocols(subjectDoses: SubjectDoses[] = []): IProtocol[] { + return uniqueDoses(subjectDoses).map(doses => { + const subjects: string[] = []; + subjectDoses.forEach(subjectDosing => { + const subjectId = subjectDosing[0].subject; + if (subjectUsesProtocol(subjectDosing, doses)) { + subjects.push(subjectId); + } + }); + return { + label: doses[0].label, + doses, + subjects + }; + }); +} + +export function subjectUsesProtocol(subjectDoses: IDose[], protocolDoses: IDose[]) { + const doses = stripDoses(subjectDoses); + return JSON.stringify(doses) === JSON.stringify(protocolDoses); +} diff --git a/frontend-v2/src/features/trial/DatasetDoses.tsx b/frontend-v2/src/features/trial/DatasetDoses.tsx new file mode 100644 index 00000000..1baddfcd --- /dev/null +++ b/frontend-v2/src/features/trial/DatasetDoses.tsx @@ -0,0 +1,56 @@ +import { FC } from "react"; +import { TableCell, TableRow, Typography } from "@mui/material"; +import { + ProjectRead, + ProtocolRead, + UnitRead +} from "../../app/backendApi"; + +interface Props { + project: ProjectRead; + protocol: ProtocolRead; + units: UnitRead[]; +} + +const DatasetDoses: FC = ({ protocol, units }) => { + return ( + <> + {protocol.doses.map((dose) => ( + + {protocol?.mapped_qname || ''} + + {dose.amount} + + + {protocol.amount_unit && ( + + {units.find((u) => u.id === protocol.amount_unit)?.symbol} + + )} + + + {dose.repeats} + + + {dose.start_time} + + + {dose.duration} + + + {dose.repeat_interval} + + + {protocol.time_unit && ( + + {units.find((u) => u.id === protocol.time_unit)?.symbol} + + )} + + + ))} + + ); +}; + +export default DatasetDoses; diff --git a/frontend-v2/src/features/trial/Doses.tsx b/frontend-v2/src/features/trial/Doses.tsx index 8fcec21b..a2565860 100644 --- a/frontend-v2/src/features/trial/Doses.tsx +++ b/frontend-v2/src/features/trial/Doses.tsx @@ -74,10 +74,6 @@ const Doses: FC = ({ project, protocol, units }) => { return
Loading...
; } - if (!variable) { - return
Variable not found
; - } - const handleAddRow = () => { appendDose({ amount: 0, repeats: 0, start_time: 0, repeat_interval: 1 }); }; @@ -95,7 +91,7 @@ const Doses: FC = ({ project, protocol, units }) => { <> {doses.map((dose, index) => ( - {variable.name} + {variable?.name || ''} { +const Protocols: FC = () => { + const [tab, setTab] = useState(0); + const handleTabChange = (event: ChangeEvent<{}>, newValue: number) => { + setTab(newValue); + }; const selectedProject = useSelector( (state: RootState) => state.main.selectedProject, ); @@ -38,6 +47,9 @@ const Protocols: React.FC = () => { { compoundId: project?.compound || 0 }, { skip: !project?.compound }, ); + + const { dataset } = useDataset(selectedProject); + console.log('trials', dataset) const loading = [isProjectLoading, isProtocolsLoading, unitsLoading].some( (x) => x, @@ -63,91 +75,121 @@ const Protocols: React.FC = () => { } }); + function a11yProps(index: number) { + return { + id: `protocol-tab-${index}`, + 'aria-controls': `protocol-tabpanel`, + }; + } + + const selectedProtocols = tab === 0 ? filteredProtocols : [dataset?.protocols[tab-1]]; + const DosesComponent = tab === 0 ? Doses : DatasetDoses; return ( - - - - - -
- {" "} - Site of Admin - - Defines the site of drug administration. A1/A1_t/A1_f = IV, Aa - = SC or PO. The site of drug administration can be selected - under Model/ Map Variables - -
-
- - {" "} -
Dose
-
- -
+ <> + + + {dataset?.protocols.map((protocol, index) => ( + + ))} + + + +
+ + + +
+ {" "} + Site of Admin + + Defines the site of drug administration. A1/A1_t/A1_f = IV, Aa + = SC or PO. The site of drug administration can be selected + under Model/ Map Variables + +
+
+ {" "} - Dose Unit - - Default selection: mg/kg for preclinical, mg for clinical - - - - - {" "} -
Number of Doses
-
- -
+
Dose
+ + +
+ {" "} + Dose Unit + + Default selection: mg/kg for preclinical, mg for clinical + +
+
+ {" "} - Start Time - - Time of the first dose - -
-
- -
+
Number of Doses
+ + +
+ {" "} + Start Time + + Time of the first dose + +
+
+ +
+ {" "} + Dose Duration + + Duration of dosing. For IV bolus PO/SC dosing use the default + value 0.0833 h + +
+
+ {" "} - Dose Duration - - Duration of dosing. For IV bolus PO/SC dosing use the default - value 0.0833 h - -
-
- - {" "} -
Dosing Interval
-
- - {" "} -
Time Unit
-
- -
+
Dosing Interval
+ + {" "} - Remove{" "} -
-
-
-
- - {protocols?.length === 0 && ( - - No protocols found +
Time Unit
+ + {tab === 0 && + +
+ {" "} + Remove{" "} +
+
+ }
- )} - {filteredProtocols?.map((protocol) => ( - - ))} -
-
-
+ + + {protocols?.length === 0 && ( + + No protocols found + + )} + {selectedProtocols?.map((protocol) => { + return protocol ? ( + + ) : null; + })} + + + + + ); }; diff --git a/frontend-v2/src/hooks/useDataset.ts b/frontend-v2/src/hooks/useDataset.ts new file mode 100644 index 00000000..61d6757c --- /dev/null +++ b/frontend-v2/src/hooks/useDataset.ts @@ -0,0 +1,53 @@ +import { useCallback, useEffect, useState } from 'react'; +import { + DatasetRead, + useDatasetListQuery, + useDatasetCreateMutation +} from '../app/backendApi'; + +// assume only one dataset per project for the time being. +let appDataset: DatasetRead | null = null; + +export default function useDataset(selectedProject: number | null) { + const [dataset, setDataset] = useState(appDataset); + const selectedProjectOrZero = selectedProject || 0; + const { data: datasets = [], isLoading: isDatasetLoading } = useDatasetListQuery( + { projectId: selectedProjectOrZero }, + { skip: !selectedProject }, + ); + const [ + createDataset + ] = useDatasetCreateMutation(); + + useEffect(function onDataLoad() { + async function addDataset() { + let [newDataset] = datasets; + if (!newDataset) { + const response = await createDataset({ + dataset: { + name: 'New Dataset', + project: selectedProjectOrZero, + } + }); + if ('data' in response && response.data) { + newDataset = response.data; + } + } + appDataset = newDataset; + setDataset(newDataset); + } + if (selectedProjectOrZero && !isDatasetLoading) { + addDataset(); + } + }, [datasets, createDataset, isDatasetLoading, selectedProjectOrZero]); + + const updateDataset = useCallback((newDataset: DatasetRead) => { + appDataset = newDataset; + setDataset(newDataset); + }, []); + + return { + dataset: appDataset, + updateDataset + }; +} diff --git a/frontend-v2/yarn.lock b/frontend-v2/yarn.lock index 719a6fba..7e804f02 100644 --- a/frontend-v2/yarn.lock +++ b/frontend-v2/yarn.lock @@ -1191,7 +1191,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.9": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== @@ -2090,6 +2090,16 @@ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.11.tgz#36b99a88f8010dc716128e568dc05681a69dc7ae" integrity sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w== +"@mui/utils@^5.14.16": + version "5.15.11" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.11.tgz#a71804d6d6025783478fd1aca9afbf83d9b789c7" + integrity sha512-D6bwqprUa9Stf8ft0dcMqWyWDKEo7D+6pB1k8WajbqlYIRA8J8Kw9Ra7PSZKKePGBGWO+/xxrX1U8HpG/aXQCw== + dependencies: + "@babel/runtime" "^7.23.9" + "@types/prop-types" "^15.7.11" + prop-types "^15.8.1" + react-is "^18.2.0" + "@mui/utils@^5.15.0": version "5.15.0" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.0.tgz#87b4db92dd2ddf3e2676377427f50662124013b4" @@ -2100,6 +2110,17 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/x-data-grid@^6.19.6": + version "6.19.6" + resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-6.19.6.tgz#6334bb70a7a2685fc1cf3ed902172661c3206f3f" + integrity sha512-jpZkX1Gnlo87gKcD10mKMY8YoAzUD8Cv3/IvedH3FINDKO3hnraMeOciKDeUk0tYSj8RUDB02kpTHCM8ojLVBA== + dependencies: + "@babel/runtime" "^7.23.2" + "@mui/utils" "^5.14.16" + clsx "^2.0.0" + prop-types "^15.8.1" + reselect "^4.1.8" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -11975,12 +11996,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3, tslib@^2.1.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - -tslib@^2.4.0: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== diff --git a/pkpdapp/pkpdapp/migrations/0007_protocol_mapped_qname.py b/pkpdapp/pkpdapp/migrations/0007_protocol_mapped_qname.py new file mode 100644 index 00000000..e038186f --- /dev/null +++ b/pkpdapp/pkpdapp/migrations/0007_protocol_mapped_qname.py @@ -0,0 +1,27 @@ +# +# This file is part of PKPDApp (https://github.com/pkpdapp-team/pkpdapp) which +# is released under the BSD 3-clause license. See accompanying LICENSE.md for +# copyright notice and full license details. +# +# Generated by Django 3.2.24 on 2024-03-08 08:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pkpdapp', '0006_biomarkertype_mapped_qname'), + ] + + operations = [ + migrations.AddField( + model_name='protocol', + name='mapped_qname', + field=models.CharField( + default='', + help_text='qname of the mapped dosing compartment for each dose', + max_length=50 + ), + ), + ] diff --git a/pkpdapp/pkpdapp/models/dataset.py b/pkpdapp/pkpdapp/models/dataset.py index 524b81f6..3a75dcb3 100644 --- a/pkpdapp/pkpdapp/models/dataset.py +++ b/pkpdapp/pkpdapp/models/dataset.py @@ -97,12 +97,13 @@ def replace_data(self, data: pd.DataFrame): # create subject protocol for i, row in data[ - ['SUBJECT_ID', 'ROUTE', "AMOUNT_UNIT"] + ['SUBJECT_ID', 'ROUTE', "AMOUNT_UNIT", "AMOUNT_VARIABLE"] ].drop_duplicates().iterrows(): subject_id = row['SUBJECT_ID'] route = row['ROUTE'] amount_unit = Unit.objects.get(symbol=row['AMOUNT_UNIT']) subject = subjects[subject_id] + mapped_qname = row['AMOUNT_VARIABLE'] if route == 'IV': route = Protocol.DoseType.DIRECT else: @@ -115,7 +116,8 @@ def replace_data(self, data: pd.DataFrame): ), time_unit=time_unit, amount_unit=amount_unit, - dose_type=route + dose_type=route, + mapped_qname=mapped_qname ) subject.save() diff --git a/pkpdapp/pkpdapp/models/protocol.py b/pkpdapp/pkpdapp/models/protocol.py index 10a2a862..6864c0ad 100644 --- a/pkpdapp/pkpdapp/models/protocol.py +++ b/pkpdapp/pkpdapp/models/protocol.py @@ -75,6 +75,12 @@ class DoseType(models.TextChoices): help_text='unit for the amount value stored in each dose' ) + mapped_qname = models.CharField( + default='', + max_length=50, + help_text='qname of the mapped dosing compartment for each dose' + ) + __original_dose_type = None def __init__(self, *args, **kwargs): diff --git a/pkpdapp/pkpdapp/utils/data_parser.py b/pkpdapp/pkpdapp/utils/data_parser.py index c41b4320..3ffb55f6 100644 --- a/pkpdapp/pkpdapp/utils/data_parser.py +++ b/pkpdapp/pkpdapp/utils/data_parser.py @@ -30,6 +30,9 @@ class DataParser: "AMOUNT_UNIT": [ "Amt_unit", "Amt_units", "AMTUNIT", "UNIT", "AMOUNT_UNIT" ], + "AMOUNT_VARIABLE": [ + "Amount Variable", "AMOUNT_VARIABLE" + ], "OBSERVATION": [ "DV", "Observation", "Y", "YVAL", "OBSERVATION", "OBSERVATION_VALUE", "OBSERVATIONVALUE" @@ -68,6 +71,7 @@ class DataParser: optional_cols = [ "TIME_UNIT", "AMOUNT_UNIT", + "AMOUNT_VARIABLE", "OBSERVATION_UNIT", "OBSERVATION_NAME", "OBSERVATION_VARIABLE", @@ -76,7 +80,7 @@ class DataParser: "INFUSION_TIME", ] - altername_unit_names = { + alternate_unit_names = { "h": ["hour"], "day": ["d"], } @@ -144,7 +148,7 @@ def validate(self, data: pd.DataFrame): # map alternate unit names to standard names inv_altername_unit_names = {} - for k, v in self.altername_unit_names.items(): + for k, v in self.alternate_unit_names.items(): for v2 in v: inv_altername_unit_names[v2] = k @@ -165,6 +169,10 @@ def map_unit_names(x): if "OBSERVATION_VARIABLE" not in found_cols: data["OBSERVATION_VARIABLE"] = "" + # put in blank amount variable if not present + if "AMOUNT_VARIABLE" not in found_cols: + data["AMOUNT_VARIABLE"] = "" + # put in default compound name if not present if "COMPOUND" not in found_cols: data["COMPOUND"] = "unknown compound" diff --git a/pkpdapp/schema.yml b/pkpdapp/schema.yml index b9d68dd9..b0160f30 100644 --- a/pkpdapp/schema.yml +++ b/pkpdapp/schema.yml @@ -4389,6 +4389,10 @@ components: maxLength: 100 dose_type: $ref: '#/components/schemas/DoseTypeEnum' + mapped_qname: + type: string + description: qname of the mapped dosing compartment for each dose + maxLength: 50 project: type: integer nullable: true @@ -4909,6 +4913,10 @@ components: maxLength: 100 dose_type: $ref: '#/components/schemas/DoseTypeEnum' + mapped_qname: + type: string + description: qname of the mapped dosing compartment for each dose + maxLength: 50 project: type: integer nullable: true