diff --git a/package-lock.json b/package-lock.json index e21fa28..7f5703a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "react-hot-toast": "^2.4.1", "react-markdown": "^9.0.1", "react-multi-email": "^1.0.23", - "recharts": "^2.12.7", + "recharts": "^2.13.0", "remark-gfm": "^4.0.0", "sharp": "^0.33.4", "swr": "^2.2.5" @@ -11483,14 +11483,14 @@ } }, "node_modules/recharts": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", - "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0.tgz", + "integrity": "sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", + "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", @@ -11520,6 +11520,11 @@ "node": ">=6" } }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", diff --git a/package.json b/package.json index d5de3c6..926b908 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "react-hot-toast": "^2.4.1", "react-markdown": "^9.0.1", "react-multi-email": "^1.0.23", - "recharts": "^2.12.7", + "recharts": "^2.13.0", "remark-gfm": "^4.0.0", "sharp": "^0.33.4", "swr": "^2.2.5" diff --git a/src/app/dashboard/models/[modelId]/components/DatasetResults.tsx b/src/app/dashboard/models/[modelId]/components/DatasetResults.tsx index e88a0d9..eeff44f 100644 --- a/src/app/dashboard/models/[modelId]/components/DatasetResults.tsx +++ b/src/app/dashboard/models/[modelId]/components/DatasetResults.tsx @@ -34,6 +34,8 @@ import { getKeyValue, useDisclosure } from '@nextui-org/react'; import DoaTableCell from '@/app/dashboard/models/[modelId]/components/DoaTableCell'; import FeatureEditModal from '@/app/dashboard/models/[modelId]/components/FeatureEditModal'; import DoaModal from '@/app/dashboard/models/[modelId]/components/DoaModal'; +import { Tab, Tabs } from '@nextui-org/tabs'; +import PBPKPlots from '@/app/dashboard/models/[modelId]/components/PBPKPlots'; const log = logger.child({ module: 'dataset' }); @@ -136,107 +138,117 @@ export default function DatasetResults({ const tableRows = resultTableData.rows; return ( -
- {dataset?.status === 'FAILURE' && model.isAdmin && ( -
- - } - > -

- legacy prediction service: {model.legacyPredictionService} -

-

- Failure reason: {dataset?.failureReason} -

-
-
-
- )} -

- Result -

-
- - ID {datasetId} - - {getDatasetStatusNode(dataset)} -
- {!isLoaded && ( -
- - - - -
- )} - {isLoaded && dataset?.status === 'SUCCESS' && ( - <> -
- +
+ + +
+ {dataset?.status === 'FAILURE' && model.isAdmin && ( +
+ + } + > +

+ legacy prediction service: {model.legacyPredictionService} +

+

+ Failure reason: {dataset?.failureReason} +

+
+
+
+ )} +
+ + ID {datasetId} + + {getDatasetStatusNode(dataset)} +
+ {!isLoaded && ( +
+ + + + +
+ )} + {isLoaded && dataset?.status === 'SUCCESS' && ( + <> +
+ +
+ + + {(column) => ( + {column.label} + )} + + + + {(item) => ( + + {(columnKey) => { + if (columnKey === 'doa') { + return ( + + { + setSelectedRow(item); + onDoaModalOpen(); + }} + /> + + ); + } + return ( + + {getKeyValue(item, columnKey)} + + ); + }} + + )} + +
+ + )} + + {selectedRow && ( + + )}
- - - {(column) => ( - {column.label} - )} - - - - {(item) => ( - - {(columnKey) => { - if (columnKey === 'doa') { - return ( - - { - setSelectedRow(item); - onDoaModalOpen(); - }} - /> - - ); - } - return ( - {getKeyValue(item, columnKey)} - ); - }} - - )} - -
- - )} - - {selectedRow && ( - - )} +
+ {model.type === 'R_PBPK' && dataset && dataset.status === 'SUCCESS' && ( + + + + )} +
); } diff --git a/src/app/dashboard/models/[modelId]/components/PBPKPlots.tsx b/src/app/dashboard/models/[modelId]/components/PBPKPlots.tsx new file mode 100644 index 0000000..9d7c72c --- /dev/null +++ b/src/app/dashboard/models/[modelId]/components/PBPKPlots.tsx @@ -0,0 +1,152 @@ +import { DatasetDto, FeatureDto, ModelDto } from '@/app/api.types'; +import React, { useMemo } from 'react'; +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +interface PBPKPlotsProps { + model: ModelDto; + dataset: DatasetDto; +} + +const groupByUnits = ( + dependentFeatures: FeatureDto[], +): Partial> => { + return Object.groupBy( + dependentFeatures, + (feature) => feature.units ?? 'No Units', + ); +}; + +type PBPKPlot = { + unit: string; + plotData: PBPKPlotData[]; +}; + +type PBPKPlotData = { + time: number; +} & { + [key: string]: number; +}; + +function generatePlots( + groupFeaturesByUnits: Partial>, + dataset: DatasetDto, +): PBPKPlot[] { + return Object.entries(groupFeaturesByUnits).map(([unit, features]) => { + const plotData = dataset.result!.map((resultRow: any) => { + const time = parseFloat(resultRow.time); + const featureValues: Record = {}; + features!.forEach((feature: FeatureDto) => { + return (featureValues[feature.key] = parseFloat( + resultRow[feature.key], + )); + }); + + return { + time, + ...featureValues, + }; + }); + + return { + unit, + plotData, + }; + }); +} + +const lineColors = [ + '#3498db', // Bright Blue + '#f1c40f', // Vibrant Yellow + '#e74c3c', // Deep Red + '#2ecc71', // Lime Green + '#9b59b6', // Rich Plum + '#1abc9c', // Seafoam Green + '#16a085', // Forest Green + '#27ae60', // Mint Green + '#2980b9', // Navy Blue + '#8e44ad', // Lavender + '#d35400', // Burnt Orange + '#c0392b', // Crimson Red + '#7f8c8d', // Dark Gray + '#2c3e50', // Charcoal Gray + '#95a5a6', // Light Gray + '#f7dc6f', // Golden Yellow + '#66d9ef', // Sky Blue + '#b2fffc', // Pale Blue + '#8bc34a', // Olive Green + '#ff99cc', // Pastel Pink +]; + +export default function PBPKPlots({ model, dataset }: PBPKPlotsProps) { + const groupFeaturesByUnits: Partial> = useMemo( + () => + Object.groupBy( + model.dependentFeatures, + (feature) => feature.units ?? 'No Units', + ), + [model.dependentFeatures], + ); + + if (!dataset.result || !groupFeaturesByUnits) { + return null; + } + + const plots: PBPKPlot[] = generatePlots(groupFeaturesByUnits, dataset); + + return ( +
+ {plots.map((plot, index) => ( + <> +
+ + + + + + + + + {Object.entries(plot.plotData[0]).map(([key, value], index) => { + if (key === 'time') return null; + return ( + + ); + })} + + +
+ + ))} +
+ ); +}