Skip to content

Commit

Permalink
feat: download data as a CSV
Browse files Browse the repository at this point in the history
Convert group dosing data and group observations to a single CSV file for all subject groups. CSV headers are taken from a standardised list used by the Django app. Remove the previous download button, on the last step of the data stepper, and replace it with a download button in the Data tab.
  • Loading branch information
eatyourgreens committed Apr 29, 2024
1 parent 9d84cf1 commit 81ec69f
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 55 deletions.
35 changes: 28 additions & 7 deletions frontend-v2/src/features/data/Data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { FC, useState } from "react";
import { useSelector } from "react-redux";
import { useProjectRetrieveQuery, useUnitListQuery } from "../../app/backendApi";
import { RootState } from "../../app/store";
import { Box, Button, Tab, Tabs, Typography } from "@mui/material";
import { Box, Button, Stack, Tab, Tabs, Typography } from "@mui/material";
import { DataGrid } from '@mui/x-data-grid';
import LoadDataStepper from "./LoadDataStepper";
import useDataset from "../../hooks/useDataset";
import generateCSV from './generateCSV';

function displayUnitSymbol(symbol: string | undefined) {
return symbol === '' ? 'dimensionless' : symbol;
Expand All @@ -28,6 +29,7 @@ const Data:FC = () => {
const [isLoading, setIsLoading] = useState(false);

function handleNewUpload() {

if (groups.length === 0 || window.confirm('Are you sure you want to delete the current dataset?')) {
setIsLoading(true);
}
Expand All @@ -52,6 +54,17 @@ const Data:FC = () => {
};
}

function downloadCSV() {
const csv = generateCSV(dataset, groups, subjectBiomarkers, units);
const blob = new Blob(['\ufeff', csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${project?.name} data.csv`;
a.click();
window.URL.revokeObjectURL(url);
}

const group = groups[tab];
const protocols = group?.protocols || [];
const dosingRows = protocols.flatMap(protocol => {
Expand Down Expand Up @@ -113,12 +126,20 @@ const Data:FC = () => {
<LoadDataStepper onFinish={onUploadComplete} onCancel={onCancel} /> :
<>
<Box sx={{ display: 'flex', justifyContent: 'end' }}>
<Button
variant="outlined"
onClick={handleNewUpload}
>
Upload new dataset
</Button>
<Stack spacing={1}>
<Button
variant="outlined"
onClick={handleNewUpload}
>
Upload new dataset
</Button>
<Button
variant="outlined"
onClick={downloadCSV}
>
Download CSV
</Button>
</Stack>
</Box>
<Tabs value={tab} onChange={handleTabChange}>ß
{groups?.map((group, index) => (
Expand Down
65 changes: 21 additions & 44 deletions frontend-v2/src/features/data/PreviewData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,54 +37,31 @@ const PreviewData: FC<IPreviewData> = ({ state, firstTime }: IPreviewData) => {
fields.push('Observation_unit')
}

function downloadCSV() {
const csvHeaders = fields.join(',');
const csvBody = data.map(row => fields.map(field => row[field]).join(',')).join('\n');
const csv = `${csvHeaders}\n${csvBody}`;
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'data.csv';
a.click();
window.URL.revokeObjectURL(url);
}

return (
<>
<Box component="div" sx={{ maxHeight: "40vh", overflow: 'auto', overflowX: 'auto' }}>
<Table>
<TableHead>
<TableRow>
<Box component="div" sx={{ maxHeight: "40vh", overflow: 'auto', overflowX: 'auto' }}>
<Table>
<TableHead>
<TableRow>
{fields.map((field, index) => (
<TableCell key={index}>
<Typography variant="h6" component="div" sx={{ flexGrow: 1, marginBottom: 1 }} align="center">
{field}
</Typography>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data.map((row, index) => (
<TableRow key={index}>
{fields.map((field, index) => (
<TableCell key={index}>
<Typography variant="h6" component="div" sx={{ flexGrow: 1, marginBottom: 1 }} align="center">
{field}
</Typography>
</TableCell>
<TableCell key={index}>{row[field]}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data.map((row, index) => (
<TableRow key={index}>
{fields.map((field, index) => (
<TableCell key={index}>{row[field]}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'right' }}>
<Button
variant="outlined"
onClick={downloadCSV}
>
Download CSV
</Button>
</Box>
</>
))}
</TableBody>
</Table>
</Box>
)
}

Expand Down
111 changes: 111 additions & 0 deletions frontend-v2/src/features/data/generateCSV.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import Papa from 'papaparse';
import {
DatasetRead,
ProtocolRead,
SubjectGroupListApiResponse,
UnitListApiResponse,
UnitRead
} from '../../app/backendApi';

type Row = { [key: string]: any };
type Data = Row[];
type SubjectBiomarker = {
subjectId: any;
subjectDatasetId: number | undefined;
time: any;
timeUnit: UnitRead | undefined;
value: any;
unit: UnitRead | undefined;
qname: string | undefined;
label: string;
id: number;
};

const HEADERS: string[] = [
'ID', 'Group', 'Time', 'Time_unit',
'Observation', 'Observation_unit', 'Observation_id', 'Observation Variable',
'Administration ID', 'Amount', 'Amt_unit', 'Amount Variable', 'Infusion_time',
'II', 'ADDL'
];

function parseDosingRow(
protocol: ProtocolRead,
units: UnitListApiResponse | undefined,
groupId: string | undefined,
adminId: number
) {
const amountUnit = units?.find(unit => unit.id === protocol.amount_unit)?.symbol || '';
const timeUnit = units?.find(unit => unit.id === protocol.time_unit)?.symbol || '';
const qname = protocol.mapped_qname;
const doseType = protocol.dose_type;
return protocol.doses.map(dose => ({
'Administration ID': adminId,
Group: groupId,
Amount: dose.amount.toString(),
'Amt_unit': amountUnit,
Time: dose.start_time.toString(),
'Time_unit': timeUnit,
'Infusion_time': dose.duration,
'ADDL': (dose?.repeats || 1) - 1,
'II': dose.repeat_interval,
'Amount Variable': qname,
Observation: '.'
}))
}

function parseBiomarkerRow(row: SubjectBiomarker, groupId: string | undefined): Row {
return ({
'ID': row.subjectDatasetId,
'Time': row.time.toString(),
'Time_unit': row.timeUnit?.symbol,
'Observation': row.value.toString(),
'Observation_unit': row.unit?.symbol,
'Observation_id': row.label,
'Observation Variable': row.qname,
Group: groupId,
Amount: '.'
});
}

export default function generateCSV(
dataset: DatasetRead | undefined,
groups: SubjectGroupListApiResponse,
subjectBiomarkers: SubjectBiomarker[][] | undefined,
units: UnitListApiResponse | undefined
) {
const rows: Data = [];
groups.forEach((group, groupIndex) => {
const groupId = group?.id_in_dataset || group?.name;

const dosingRows: Row[] = group.protocols.flatMap((protocol, protocolIndex) => {
const adminId = groupIndex + 1 + protocolIndex;
return parseDosingRow(protocol, units, groupId, adminId);
});

const observationRows: Row[] = (subjectBiomarkers || []).flatMap(biomarkerRows => biomarkerRows.map(row => {
const group = dataset?.groups?.find(group => group.subjects.includes(row.subjectId));
const groupId = group?.id_in_dataset || group?.name;
return parseBiomarkerRow(row, groupId);
}))
.filter((row: Row) => row.Group === groupId);

const subjects = new Set(observationRows?.map((row: Row) => row['ID']));
subjects.forEach(subjectId => {
const subjectObservations = observationRows ?
observationRows.filter((row: Row) => row['ID'] === subjectId) :
[];
const subjectDosing: Row[] = dosingRows
.filter(row => row.Group === groupId)
.map(row => ({ ...row, 'ID': subjectId }));
subjectDosing.concat(subjectObservations)
.forEach((observation: Row) => {
const dataRow: Row = {};
HEADERS.forEach(header => {
dataRow[header] = observation[header];
});
rows.push(dataRow);
});
});
});
return Papa.unparse(rows);
}
10 changes: 6 additions & 4 deletions frontend-v2/src/features/data/normaliseDataHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@ const normalisation = {
'Administration ID': ['administration id', 'cmt', 'adm'],
'Amount': ['amount', 'amt'],
'Amount Unit': ['amount unit', 'amt_unit', 'amt_units', 'amtunit', 'amtunits', 'units_amt'],
'Amount Variable': ['amount variable'],
'Cat Covariate': ['cat covariate', 'cat', 'sex', 'gender', 'group', 'cohort', 'study', 'route', 'matrix'],
'Censoring': ['cens', 'blq', 'lloq'],
'Cont Covariate': ['cont covariate', 'weight', 'wt', 'bw', 'age', 'dose'],
'Event ID': ['event id', 'evid'],
'ID': ['id', 'subject', 'animal number'],
'Ignored Observation': ['ignored observation', 'mdv'],
'Infusion Duration': ['infusion duration', 'dur'],
'Infusion Duration': ['infusion duration', 'infusion_time', 'dur'],
'Infusion Rate': ['infusion rate', 'rate'],
'Interdose Interval': ['interdose interval', 'ii', 'tau', 'dosing interval'],
'Observation': ['observation', 'dv', 'c', 'y', 'conc', 'cobs', 'obs'],
'Observation Unit': ['observation unit', 'dv_units', 'c_units', 'y_units', 'yunit', 'cunit', 'obs_units', 'obs_unit', 'obsunit', 'units_conc'],
'Observation ID': ['observation id', 'ytype', 'dvid'],
'Observation Unit': ['observation unit', 'observation_unit', 'dv_units', 'c_units', 'y_units', 'yunit', 'cunit', 'obs_units', 'obs_unit', 'obsunit', 'units_conc'],
'Observation ID': ['observation id', 'observation_id', 'ytype', 'dvid'],
'Observation Variable': ['observation variable'],
'Occasion': ['occasion', 'occ'],
'Regressor': ['x', 'regressor'],
'Time': ['time', 't', 'ivar'],
'Time Unit': ['time_unit', 'time_units', 't_units', 'tunit', 'units_time'],
'Time Unit': ['time unit', 'time_unit', 'time_units', 't_units', 'tunit', 'units_time'],
'Unit': ['unit', 'units'],
}

Expand Down

0 comments on commit 81ec69f

Please sign in to comment.