From 443ce2483d9db3fdb2558fe486562a7c3e72b31c Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Wed, 2 Oct 2024 14:07:56 +0100 Subject: [PATCH] refactor(frontend): generate administration IDs during stratification (#513) (#533) When a dataset doesn't contain an administration ID column, generate one during the stratification step. Assign a unique administration ID to each group by default. --- .../features/data/CreateDosingProtocols.tsx | 9 ++- frontend-v2/src/features/data/LoadData.tsx | 4 +- .../src/features/data/LoadDataStepper.tsx | 10 +-- frontend-v2/src/features/data/MapDosing.tsx | 2 +- .../src/features/data/Stratification.tsx | 61 ++++++++++++++----- 5 files changed, 59 insertions(+), 27 deletions(-) diff --git a/frontend-v2/src/features/data/CreateDosingProtocols.tsx b/frontend-v2/src/features/data/CreateDosingProtocols.tsx index 454794ab..ccf4ca05 100644 --- a/frontend-v2/src/features/data/CreateDosingProtocols.tsx +++ b/frontend-v2/src/features/data/CreateDosingProtocols.tsx @@ -38,11 +38,10 @@ const CreateDosingProtocols: FC = ({ (field) => field === "Amount" || state.normalisedFields.get(field) === "Amount", ); - const dosingRows: Row[] = - administrationIdField === "Group ID" - ? state.data - : // ignore rows with no amount and administration ID set to 0. - state.data.filter((row) => parseInt(row[administrationIdField])); + // ignore rows with no amount and administration ID set to 0. + const dosingRows: Row[] = state.data.filter((row) => + parseInt(row[administrationIdField]), + ); if (!amountField) { const newNormalisedFields = new Map([ ...state.normalisedFields.entries(), diff --git a/frontend-v2/src/features/data/LoadData.tsx b/frontend-v2/src/features/data/LoadData.tsx index ea1bb3db..2a4801ee 100644 --- a/frontend-v2/src/features/data/LoadData.tsx +++ b/frontend-v2/src/features/data/LoadData.tsx @@ -146,14 +146,14 @@ const LoadData: FC = ({ state, firstTime }) => { normalisedFields, normalisedHeaders: [...normalisedFields.values()], }); - const primaryCohort = + const groupColumn = fields.find( (field) => normalisedFields.get(field) === "Cat Covariate", ) || "Group"; const errors = csvData.errors .map((e) => e.message) .concat(fieldValidation.errors); - state.setPrimaryCohort(primaryCohort); + state.setGroupColumn(groupColumn); state.setErrors(errors); state.setWarnings(fieldValidation.warnings); if (csvData.data.length > 0 && csvData.meta.fields) { diff --git a/frontend-v2/src/features/data/LoadDataStepper.tsx b/frontend-v2/src/features/data/LoadDataStepper.tsx index 8723aabf..2e988db8 100644 --- a/frontend-v2/src/features/data/LoadDataStepper.tsx +++ b/frontend-v2/src/features/data/LoadDataStepper.tsx @@ -66,8 +66,8 @@ export type StepperState = { setWarnings: (warnings: string[]) => void; amountUnit?: string; setAmountUnit: (amountUnit: string) => void; - primaryCohort: string; - setPrimaryCohort: (primaryCohort: string) => void; + groupColumn: string; + setGroupColumn: (primaryCohort: string) => void; }; function validateSubjectProtocols(protocols: IProtocol[]) { @@ -96,7 +96,7 @@ const LoadDataStepper: FC = ({ csv = "", onCancel, onFinish }) => { ); const [timeUnit, setTimeUnit] = useState(undefined); const [amountUnit, setAmountUnit] = useState(undefined); - const [primaryCohort, setPrimaryCohort] = useState("Group"); + const [groupColumn, setGroupColumn] = useState("Group"); const selectedProject = useSelector( (state: RootState) => state.main.selectedProject, ); @@ -121,8 +121,8 @@ const LoadDataStepper: FC = ({ csv = "", onCancel, onFinish }) => { setTimeUnit, amountUnit, setAmountUnit, - primaryCohort, - setPrimaryCohort, + groupColumn, + setGroupColumn, get fields() { return [...normalisedFields.keys()]; }, diff --git a/frontend-v2/src/features/data/MapDosing.tsx b/frontend-v2/src/features/data/MapDosing.tsx index 73df0e70..f8c4012a 100644 --- a/frontend-v2/src/features/data/MapDosing.tsx +++ b/frontend-v2/src/features/data/MapDosing.tsx @@ -82,7 +82,7 @@ const MapDosing: FC = ({ state, firstTime }: IMapDosing) => { /> ) : ( protocols.length <= 1); } +/** + * Generate a unique administration ID for each group. + * @param data + * @returns data with administration ID column. + */ +function generateAdministrationIds(data: { [key: string]: string }[]) { + const newData = data.map((row) => ({ ...row })); + const uniqueGroupIds = [...new Set(data.map((row) => row["Group ID"]))]; + newData.forEach((row) => { + const administrationId = uniqueGroupIds.indexOf(row["Group ID"]) + 1; + row["Administration ID"] = `${administrationId}`; + }); + return newData; +} + +/** + * Assign a group ID to each row based on a categorical covariate column. + * @param data + * @param columnName + * @returns data with group ID and administration ID columns. + */ +function groupDataRows(data: { [key: string]: string }[], columnName: string) { + const newData = data.map((row) => ({ ...row })); + newData.forEach((row) => { + row["Group ID"] = row[columnName] || "1"; + }); + return newData; +} + interface IStratification { state: StepperState; firstTime: boolean; @@ -55,12 +84,15 @@ const Stratification: FC = ({ state }: IStratification) => { const values = state.data.map((row) => row[field]); return [...new Set(values)]; }); + const administrationIdField = state.fields.find( + (field) => state.normalisedFields.get(field) === "Administration ID", + ); const [firstRow] = state.data; const [tab, setTab] = useState(0); - const { primaryCohort, setPrimaryCohort } = state; + const { groupColumn, setGroupColumn } = state; - const groups = groupsFromCatCovariate(state, primaryCohort); + const groups = groupsFromCatCovariate(state, groupColumn); const isValidGrouping = validateGroupMembers(groups); const groupErrorMessage = "Invalid group subjects. Each subject ID can only belong to a single cohort."; @@ -92,10 +124,10 @@ const Stratification: FC = ({ state }: IStratification) => { } if (!firstRow["Group ID"]) { - const newData = [...state.data]; - newData.forEach((row) => { - row["Group ID"] = row[primaryCohort] || "1"; - }); + let newData = groupDataRows(state.data, groupColumn); + if (!administrationIdField) { + newData = generateAdministrationIds(newData); + } state.setData(newData); state.setNormalisedFields( new Map([...state.normalisedFields.entries(), ["Group ID", "Group ID"]]), @@ -106,12 +138,13 @@ const Stratification: FC = ({ state }: IStratification) => { setTab(newValue); }; - const handlePrimaryChange = (event: ChangeEvent) => { - setPrimaryCohort(event.target.value); - const newData = [...state.data]; - newData.forEach((row) => { - row["Group ID"] = row[event.target.value]; - }); + const handleGroupChange = (event: ChangeEvent) => { + const newGroup = event.target.value; + let newData = groupDataRows(state.data, newGroup); + if (!administrationIdField) { + newData = generateAdministrationIds(newData); + } + setGroupColumn(newGroup); state.setData(newData); }; @@ -147,7 +180,7 @@ const Stratification: FC = ({ state }: IStratification) => { {catCovariates.map((field, index) => { const primaryLabel = `heading-primary field-${field}`; - const isPrimary = primaryCohort === field; + const isPrimary = groupColumn === field; return ( {field} @@ -159,7 +192,7 @@ const Stratification: FC = ({ state }: IStratification) => { name="primary" value={field} checked={isPrimary} - onChange={handlePrimaryChange} + onChange={handleGroupChange} inputProps={{ "aria-labelledby": primaryLabel }} />