diff --git a/app/RootContext.ts b/app/RootContext.ts index 130190d..703778d 100644 --- a/app/RootContext.ts +++ b/app/RootContext.ts @@ -1,18 +1,104 @@ import {createContext} from "react"; -export interface State { - history: string - titre_type: string +type PlotDisplay = "facet" | "trace" + +export interface PlotOptions { + [index: string]: PlotDisplay +} + +export interface PlotConfig { + key: string + displayName: string + type: string, + lineColors?: any + fillColors?: any +} + +export interface Model { + key: string + displayName: string + datasets: Dataset[] + regressionModels: Covariate[] + plots: PlotConfig[] + variables: Covariate[] +} + +export interface Dataset { + key: string + displayName: string +} + +export interface Covariate { + displayName: string + key: string +} + +export interface AppState { + models: Model[] + selectedModel: Model + selectedDataset: Dataset + selectedRegressionModel: Covariate + selectedPlotOptions: { [index: string]: PlotOptions } } -export interface ContextValue { - state: State +export interface AppContext { + state: AppState dispatch: () => void } -export const initialState = {history: "Facet", titre_type: "Trace"} -export const RootContext = createContext({state: initialState, dispatch: () => null}) +const biomarkerModel: Model = { + displayName: "Biomarker Kinetics", + key: "biomarker", + datasets: [{ + key: "legacy", + displayName: "SARS-CoV2-legacy" + }], + regressionModels: [ + {key: "infection_history", displayName: "Infection history"}, + {key: "last_exp_type", displayName: "Last exposure type"} + ], + plots: [{ + key: "pop_fits", + displayName: "Population fits", + type: "line", + lineColors: [ + `rgba(204, 102, 119, 1)`, + `rgba(221, 204, 119, 1)`, + `rgba(136, 204, 238, 1)`, + `rgba(136, 34, 85, 1)`, + `rgba(68, 170, 153, 1)`, + `rgba(226, 226, 226, 1)`] + , + fillColors: [ + `rgba(204, 102, 119, 0.3)`, + `rgba(221, 204, 119, 0.3)`, + `rgba(136, 204, 238, 0.3)`, + `rgba(136, 34, 85, 0.3)`, + `rgba(68, 170, 153, 0.3)`, + `rgba(226, 226, 226, 0.3)` + + ] + }], + variables: [{key: "titre_type", displayName: "Titre type"}] +} + +export const initialState: AppState = { + models: [biomarkerModel], + selectedModel: biomarkerModel, + selectedDataset: biomarkerModel.datasets[0], + selectedRegressionModel: biomarkerModel.regressionModels[0], + selectedPlotOptions: Object.fromEntries(biomarkerModel.plots.map(p => [p.key, + Object.fromEntries(biomarkerModel.variables.concat([biomarkerModel.regressionModels[0]]).map( + v => [v.key, "trace"] + )) + ])) +} + +export const RootContext = createContext({ + state: initialState, + dispatch: () => null +}) -export function rootReducer(oldState, newState): State { +export function rootReducer(oldState, newState): AppState { return {...newState} } diff --git a/app/components/ConfiguredPlot.tsx b/app/components/ConfiguredPlot.tsx new file mode 100644 index 0000000..8bfa0cf --- /dev/null +++ b/app/components/ConfiguredPlot.tsx @@ -0,0 +1,83 @@ +import {useContext} from "react"; +import {AppContext, Covariate, PlotConfig, RootContext} from "~/RootContext"; +import {Col, Row} from "react-bootstrap"; +import LocalPlot from "~/components/LocalPlot"; + +function Facet({ + data, + facets, + traces, + covariate, + value, + facetVariables, + traceVariables, + plot + }: { + data: any[], + facets: { [k: string]: string[] } + traces: { [k: string]: string[] } + covariate: Covariate + value: string, + facetVariables: Covariate[], + traceVariables: Covariate[], + plot: PlotConfig +}) { + const filteredData = data.filter(d => d[covariate.key] == value); + const otherFacetVariables = facetVariables.filter(v => v.key != covariate.key); + if (otherFacetVariables.length == 0) { + return + } else { + const nextFacetVariable = otherFacetVariables.pop()!!; + const facetValues = facets[nextFacetVariable.key]; + return facetValues.map(v => [
{value}
, ]) + } +} + + +export default function ConfiguredPlot({plot, data}) { + const {state} = useContext(RootContext); + const variables = [state.selectedRegressionModel].concat(state.selectedModel.variables); + const settings = state.selectedPlotOptions[plot.key]; + + const facetVariables = variables.filter(v => settings[v.key] == "facet"); + const traceVariables = variables.filter(v => settings[v.key] == "trace"); + + const facets = Object.fromEntries(facetVariables.map(v => [v.key, [...new Set(data.map(entry => entry[v.key]))]])); + const traces = Object.fromEntries(traceVariables.map(v => [v.key, [...new Set(data.map(entry => entry[v.key]))]])); + + if (facetVariables.length > 0) { + const firstFacet = facetVariables[0]; + const facetValues = facets[firstFacet.key]; + + return + {facetValues.map(v => )} + + } else { + return + } +} diff --git a/app/components/LineChart.tsx b/app/components/LineChart.tsx deleted file mode 100644 index a9126b3..0000000 --- a/app/components/LineChart.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { - Area, - CartesianGrid, - ComposedChart, - Legend, - Line, - XAxis, - YAxis, - Tooltip, ResponsiveContainer -} from "recharts"; -import React, {useContext, useEffect, useState} from "react"; -import {ContextValue, RootContext, State} from "../RootContext"; - -interface Dat { - t: number - me: number - lo: number - hi: number - titre_type: string - infection_history: string -} - -interface Props { - data: Dat[], - history: string - titre_type: string -} - -const colors = [{ - "ancestral": "#CC6677", - "alpha": "#DDCC77", - "delta": "#88CCEE" -}, { - "ancestral": "#882255", - "alpha": "#44AA99", - "delta": "#e2e2e2" -}] - -export const useIsServerSide = () => { - const [isServerSide, setIsServerSide] = useState(true); - - useEffect(() => { - setIsServerSide(false); - }, [setIsServerSide]); - - return isServerSide; -}; - -function legend(state: State, titre_type: string, history: string) { - let legendName = "median"; - if (state.titre_type == "Trace") { - legendName += " " + titre_type.toLowerCase() - } - if (state.history == "Trace") { - legendName += " " + history.toLowerCase() - } - return legendName -} - -export default function LineChart({data, history, titre_type}: Props) { - const {state} = useContext(RootContext); - const allData = data.filter(entry => - (state.history == "Trace" || entry.infection_history == history) && (state.titre_type == "Trace" || entry.titre_type == titre_type) - ).map(entry => ({ - ...entry, - CI: [entry.hi, entry.lo] - })); - - const titre_types = [...new Set(allData.map(entry => entry.titre_type))] - const histories = [...new Set(allData.map(entry => entry.infection_history))] - - const subsets = titre_types.flatMap(t => - histories.map(h => ({idx: [t, h], data: allData.filter(d => (d.titre_type == t && d.infection_history == h))}))) - const isServerSide = useIsServerSide(); - if (isServerSide) return null; - return [
{history} {titre_type}
, - - - - - { - subsets.map(({idx, data}, num) => [ - 0}>, - , - ]) - } - - - ] - -} diff --git a/app/components/LocalPlot.client.tsx b/app/components/LocalPlot.client.tsx index bd13c64..113a662 100644 --- a/app/components/LocalPlot.client.tsx +++ b/app/components/LocalPlot.client.tsx @@ -1,6 +1,6 @@ import React, {useContext} from 'react'; import Plot from 'react-plotly.js'; -import {ContextValue, RootContext, State} from "~/RootContext"; +import {Covariate, PlotConfig} from "~/RootContext"; interface Dat { t: number @@ -13,93 +13,82 @@ interface Dat { interface Props { data: Dat[], - history: string - titre_type: string + traceVariables: Covariate[] + traces: { [k: string]: string[] } + value: string + plot: PlotConfig } -const colors = (alpha: string) => [{ - "ancestral": `rgba(204, 102, 119, ${alpha})`, - "alpha": `rgba(221, 204, 119, ${alpha})`, - "delta": `rgba(136, 204, 238, ${alpha})` -}, { - "ancestral": `rgba(136, 34, 85, ${alpha})`, - "alpha": `rgba(68, 170, 153, ${alpha})`, - "delta": `rgba(226, 226, 226, ${alpha})` -}] - -const colorsSolid = colors("1") -const colorsAlpha = colors("0.3") - -function legend(state: State, titre_type: string, history: string) { - let legendName = "median"; - if (state.titre_type == "Trace") { - legendName += " " + titre_type.toLowerCase() - } - if (state.history == "Trace") { - legendName += " " + history.toLowerCase() - } - return legendName +function showLegend(traces: Covariate[]) { + return traces.length > 0 } -function showLegend(state: State) { - return state.history == "Trace" || state.history == "Trace" +function permuteArrays(first, next, ...rest) { + if (!first) return []; + if (!next) next = [""]; + if (rest.length) next = permuteArrays(next, ...rest); + return first.flatMap(a => next.map(b => [a, b].flat())); } -export default function LocalPlot({data, history, titre_type}: Props) { - const {state} = useContext(RootContext); - +export default function LocalPlot({data, traceVariables, traces, value, plot}: Props) { - const allData = data.filter(entry => - (state.history == "Trace" || entry.infection_history == history) && (state.titre_type == "Trace" || entry.titre_type == titre_type) - ).map(entry => ({ - ...entry, - CI: [entry.hi, entry.lo] - })); + let traceDatasets = [data]; + let traceDefinitions = permuteArrays(...traceVariables.map(v => traces[v.key])) - const titre_types = [...new Set(allData.map(entry => entry.titre_type))] - const histories = [...new Set(allData.map(entry => entry.infection_history))] - - const subsets = titre_types.flatMap(t => - histories.map(h => { - const dat = allData.filter(d => (d.titre_type == t && d.infection_history == h)) - const times = dat.map(d => d.t); + if (traceDefinitions.length > 0) { + traceDatasets = traceDefinitions.map(td => + data.filter(d => { + let include = true; + for (let i = 0; i < traceVariables.length; i++) { + if (d[traceVariables[i].key] != td[i]) { + include = false; + break; + } + } + return include + }) + ); + } - return [{ - x: times, - y: dat.map(d => d.lo), - name: legend(state, t, h), - line: {color: "transparent"}, - marker: {color: colorsSolid[histories.indexOf(h)][t.toLowerCase()]}, - showlegend: false, - type: "scatter", - mode: "lines" - }, { - y: dat.map(d => d.me), - x: times, - name: legend(state, t, h), - type: 'scatter', - mode: 'lines', - fill: "tonexty", - fillcolor: colorsAlpha[histories.indexOf(h)][t.toLowerCase()], - showlegend: showLegend(state), - marker: {color: colorsSolid[histories.indexOf(h)][t.toLowerCase()]}, - }, { - x: times, - y: dat.map(d => d.hi), - name: legend(state, t, h), - line: {width: 0}, - showlegend: false, - type: "scatter", - mode: "lines", - fill: "tonexty", - fillcolor: colorsAlpha[histories.indexOf(h)][t.toLowerCase()], - }] - })).flat() + const subsets = traceDatasets.map((dataset, i) => { + const times = dataset.map(d => d.t); + const seriesName = traceDefinitions.length > 0 ? traceDefinitions[i].join(" ") : ""; + return [{ + x: times, + y: dataset.map(d => d.lo), + name: seriesName, + line: {color: "transparent"}, + marker: {color: plot.lineColors[i]}, + showlegend: false, + type: "scatter", + mode: "lines" + }, { + y: dataset.map(d => d.me), + x: times, + name: seriesName, + type: 'scatter', + mode: 'lines', + fill: "tonexty", + fillcolor: plot.fillColors[i], + showlegend: showLegend(traceVariables), + marker: {color: plot.lineColors[i]}, + }, { + x: times, + y: dataset.map(d => d.hi), + name: seriesName, + line: {width: 0}, + showlegend: false, + type: "scatter", + mode: "lines", + fill: "tonexty", + fillcolor: plot.fillColors[i], + }] + }).flat(); return void + selected: string +} + +const CovariateOptions = ({ covariate, onSelect, selected}: Props): ReactElement => { + return + + {covariate.displayName} + + + + + + + + +} + +interface PlotFormProps { + plot: PlotConfig +} + +export default function PlotForm({plot}): ReactElement[] { + + const {state, dispatch} = useContext(RootContext) + + function onSelect(e) { + const newState = {...state} + newState.selectedPlotOptions[plot.key][e.target.id] = e.target.value; + dispatch(newState); + } + + return [{plot.displayName}, + + + Choose how each variable is displayed + + {[state.selectedRegressionModel].concat(state.selectedModel.variables) + .map(o => )} + ,
] +} diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index f566b9e..9abf702 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -1,11 +1,13 @@ import Form from 'react-bootstrap/Form'; import {Col, Row} from "react-bootstrap"; import React, {useContext} from "react"; -import {ContextValue, RootContext} from "../RootContext"; +import {AppContext, RootContext} from "../RootContext"; +import PlotForm from "./PlotForm"; export default function Sidebar() { - const {state, dispatch} = useContext(RootContext) + const {state, dispatch} = useContext(RootContext) + function onSelect(e) { const newState = {...state} newState[e.target.id] = e.target.value @@ -18,58 +20,26 @@ export default function Sidebar() { Model - - + {state.models.map(m => + )} Dataset - + {state.selectedModel.datasets.map(d => + )} - Hierarchical variables + Covariates - - - + {state.selectedModel.regressionModels.map(c => + )} - -
- Population fits - - - Choose how each variable is displayed - - - - Titre type - - - - - - - - - - - Infection history - - - - - - - - -
- - Peak titre values - + {state.selectedModel.plots.map(p => )} diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 6d35e37..3afacab 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,17 +1,13 @@ -import {Col, Row} from "react-bootstrap"; - import {json} from "@remix-run/node"; // or cloudflare/deno -import {useLoaderData} from "@remix-run/react"; import {fs} from "../utils/fs-promises.server"; -import {ContextValue, RootContext} from "../RootContext"; +import {AppContext, RootContext} from "../RootContext"; import {useContext} from "react"; -import LocalPlot from "../components/LocalPlot"; +import ConfiguredPlot from "~/components/ConfiguredPlot"; +import {useLoaderData} from "@remix-run/react"; export const loader = async () => { const jsonDirectory = "./data"; - // Read the json data file data.json - const fileContents = await fs.readFile(jsonDirectory + "/res.json", "utf8"); - // Parse the json data file contents into a json object + const fileContents = await fs.readFile([jsonDirectory, "legacy", "infection_history", "res.json"].join("/"), "utf8"); const data = JSON.parse(fileContents); return json( @@ -20,33 +16,7 @@ export const loader = async () => { }; export default function Index() { + const {state} = useContext(RootContext); const data = useLoaderData(); - const {state} = useContext(RootContext); - - const titre_types = [...new Set(data.map(entry => entry.titre_type))] - const histories = [...new Set(data.map(entry => entry.infection_history))] - if (state.history == "Facet") { - return {histories.map((h, hidx) => { - if (state.titre_type == "Facet") { - return titre_types.map((t, tidx) => - - - ) - } else { - return - } - })} - } else if (state.titre_type == "Facet") { - return { - titre_types.map((t, tidx) => - - )} - - } else { - return - - - - - } + return state.selectedModel.plots.map(p => ) } diff --git a/data/res.json b/data/legacy/infection_history/res.json similarity index 100% rename from data/res.json rename to data/legacy/infection_history/res.json diff --git a/eslint.config.js b/eslint.config.mjs similarity index 77% rename from eslint.config.js rename to eslint.config.mjs index 208e978..a36c388 100644 --- a/eslint.config.js +++ b/eslint.config.mjs @@ -6,7 +6,13 @@ import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; export default [ {files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]}, - {languageOptions: {parserOptions: {ecmaFeatures: {jsx: true}}}}, + { + settings: { + react: { + version: "detect", + }, + }, languageOptions: {parserOptions: {ecmaFeatures: {jsx: true}}} + }, {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, {languageOptions: {globals: globals.browser}}, pluginJs.configs.recommended,