From 840be6de4264375ab0823104473010b8a25f3267 Mon Sep 17 00:00:00 2001 From: "alex.hill@gmail.com" Date: Tue, 30 Jul 2024 13:16:34 +0100 Subject: [PATCH 1/4] convert existing graphs to plotly --- app/components/LocalPlot.client.tsx | 109 ++++++++++++++++++++++++++++ app/components/LocalPlot.tsx | 23 ++++++ app/routes/_index.tsx | 18 ++--- package-lock.json | 8 +- package.json | 2 +- remix.config.js | 5 +- 6 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 app/components/LocalPlot.client.tsx create mode 100644 app/components/LocalPlot.tsx diff --git a/app/components/LocalPlot.client.tsx b/app/components/LocalPlot.client.tsx new file mode 100644 index 0000000..bd13c64 --- /dev/null +++ b/app/components/LocalPlot.client.tsx @@ -0,0 +1,109 @@ +import React, {useContext} from 'react'; +import Plot from 'react-plotly.js'; +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 = (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(state: State) { + return state.history == "Trace" || state.history == "Trace" +} + +export default function LocalPlot({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 => { + const dat = allData.filter(d => (d.titre_type == t && d.infection_history == h)) + const times = dat.map(d => d.t); + + 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() + + return +} diff --git a/app/components/LocalPlot.tsx b/app/components/LocalPlot.tsx new file mode 100644 index 0000000..c346659 --- /dev/null +++ b/app/components/LocalPlot.tsx @@ -0,0 +1,23 @@ +import {ClientOnly} from "remix-utils/client-only" +import Plot from "./LocalPlot.client"; + +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 +} + +export default function LocalPlot(props: Props) { + return + {() => ()} + +} diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 7fbd876..6d35e37 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -3,14 +3,9 @@ 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 {dirname} from 'path'; -import {fileURLToPath} from 'url'; -import LineChart from "../components/LineChart"; import {ContextValue, RootContext} from "../RootContext"; import {useContext} from "react"; - -// const __filename = fileURLToPath(import.meta.url); -// const __dirname = dirname(__filename); +import LocalPlot from "../components/LocalPlot"; export const loader = async () => { const jsonDirectory = "./data"; @@ -30,30 +25,27 @@ export default function Index() { 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 + return } })} } else if (state.titre_type == "Facet") { return { titre_types.map((t, tidx) => - + )} } else { return - + } diff --git a/package-lock.json b/package-lock.json index 8e5844f..049ca23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "express": "^4.19.2", "isbot": "^4.4.0", "lucide-react": "^0.397.0", - "plotly.js": "^2.33.0", + "plotly.js": "^2.34.0", "react": "^18.3.1", "react-bootstrap": "^2.10.3", "react-dom": "^18.3.1", @@ -10986,9 +10986,9 @@ } }, "node_modules/plotly.js": { - "version": "2.33.0", - "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.33.0.tgz", - "integrity": "sha512-pzuf6hSUCaSYmEag2b2DngkHdYMn+U/QMSC/UJOLIS8yd2UwIG1iGUmOR7pqZIS87oKx/+cMoG8aknGytgJKig==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.34.0.tgz", + "integrity": "sha512-dG2LC6wY6AUR1jsnriBi9xbigLPEEXXOHhLo97dRiZAWZVS6lZCmXXZ227U4rsoluXyfyqQezaKq7svolap8Dw==", "dependencies": { "@plotly/d3": "3.8.2", "@plotly/d3-sankey": "0.7.2", diff --git a/package.json b/package.json index d3c275e..bb82529 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "express": "^4.19.2", "isbot": "^4.4.0", "lucide-react": "^0.397.0", - "plotly.js": "^2.33.0", + "plotly.js": "^2.34.0", "react": "^18.3.1", "react-bootstrap": "^2.10.3", "react-dom": "^18.3.1", diff --git a/remix.config.js b/remix.config.js index 48fa19f..f49debf 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,5 +1,8 @@ /** @type {import("@remix-run/dev").AppConfig} */ module.exports = { serverModuleFormat: "cjs", - browserNodeBuiltinsPolyfill: { modules: { url: true, path: true } } + browserNodeBuiltinsPolyfill: { modules: { url: true, path: true } }, + serverDependenciesToBundle: [ + /remix-utils/ // not "remix-utils" + ], } From 07befc497407b21561fcf8797e2a7195efb6e5cd Mon Sep 17 00:00:00 2001 From: "alex.hill@gmail.com" Date: Wed, 31 Jul 2024 22:25:06 +0100 Subject: [PATCH 2/4] refactor to use plotly, and to remove all hardcoded vars --- app/RootContext.ts | 102 ++++++++++++-- app/components/ConfiguredPlot.tsx | 83 +++++++++++ app/components/LineChart.tsx | 128 ----------------- app/components/LocalPlot.client.tsx | 139 +++++++++---------- app/components/LocalPlot.tsx | 7 +- app/components/PlotForm.tsx | 53 +++++++ app/components/Sidebar.tsx | 54 ++----- app/routes/_index.tsx | 42 +----- data/{ => legacy/infection_history}/res.json | 0 eslint.config.js => eslint.config.mjs | 8 +- 10 files changed, 324 insertions(+), 292 deletions(-) create mode 100644 app/components/ConfiguredPlot.tsx delete mode 100644 app/components/LineChart.tsx create mode 100644 app/components/PlotForm.tsx rename data/{ => legacy/infection_history}/res.json (100%) rename eslint.config.js => eslint.config.mjs (77%) 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, From fb65fdde41eb4a71137c59c800a98d2dae08c854 Mon Sep 17 00:00:00 2001 From: "alex.hill@gmail.com" Date: Wed, 31 Jul 2024 23:14:26 +0100 Subject: [PATCH 3/4] wire up sidebar --- app/components/LocalPlot.client.tsx | 3 +-- app/components/LocalPlot.tsx | 3 +-- app/components/Sidebar.tsx | 26 +++++++++++++++++++------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/components/LocalPlot.client.tsx b/app/components/LocalPlot.client.tsx index 113a662..d33574b 100644 --- a/app/components/LocalPlot.client.tsx +++ b/app/components/LocalPlot.client.tsx @@ -7,8 +7,7 @@ interface Dat { me: number lo: number hi: number - titre_type: string - infection_history: string + [index: string]: any } interface Props { diff --git a/app/components/LocalPlot.tsx b/app/components/LocalPlot.tsx index 1b768af..c45fea1 100644 --- a/app/components/LocalPlot.tsx +++ b/app/components/LocalPlot.tsx @@ -7,8 +7,7 @@ interface Dat { me: number lo: number hi: number - titre_type: string - infection_history: string + [index: string]: any } interface Props { diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index 9abf702..cabb9f7 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -8,9 +8,21 @@ export default function Sidebar() { const {state, dispatch} = useContext(RootContext) - function onSelect(e) { + function onSelectModel(e) { const newState = {...state} - newState[e.target.id] = e.target.value + newState.selectedModel = state.models.find(m => m.key == e.target.value)!! + dispatch(newState); + } + + function onSelectData(e) { + const newState = {...state} + newState.selectedDataset = state.selectedModel.datasets.find(d => d.key == e.target.value)!! + dispatch(newState); + } + + function onSelectCovariates(e) { + const newState = {...state} + newState.selectedRegressionModel = state.selectedModel.regressionModels.find(c => c.key == e.target.value)!! dispatch(newState); } @@ -18,22 +30,22 @@ export default function Sidebar() {
- Model - + Model + {state.models.map(m => )} Dataset - + {state.selectedModel.datasets.map(d => )} - Covariates - + Covariates + {state.selectedModel.regressionModels.map(c => )} From b614eef78d3f481367b0b52733bc540e552fc780 Mon Sep 17 00:00:00 2001 From: "alex.hill@gmail.com" Date: Wed, 31 Jul 2024 23:26:53 +0100 Subject: [PATCH 4/4] lint --- app/components/ConfiguredPlot.tsx | 38 ++++++++++++++++++----------- app/components/LocalPlot.client.tsx | 6 ++--- app/components/LocalPlot.tsx | 6 +---- app/components/PlotForm.tsx | 10 +++----- app/components/Sidebar.tsx | 11 ++++++--- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/app/components/ConfiguredPlot.tsx b/app/components/ConfiguredPlot.tsx index 8bfa0cf..7770056 100644 --- a/app/components/ConfiguredPlot.tsx +++ b/app/components/ConfiguredPlot.tsx @@ -3,6 +3,21 @@ import {AppContext, Covariate, PlotConfig, RootContext} from "~/RootContext"; import {Col, Row} from "react-bootstrap"; import LocalPlot from "~/components/LocalPlot"; +interface Dat { + [index: string]: string | number +} + +interface Props { + data: Dat[], + facets: { [k: string]: string[] } + traces: { [k: string]: string[] } + covariate: Covariate + value: string, + facetVariables: Covariate[], + traceVariables: Covariate[], + plot: PlotConfig +} + function Facet({ data, facets, @@ -12,26 +27,18 @@ function Facet({ facetVariables, traceVariables, plot - }: { - data: any[], - facets: { [k: string]: string[] } - traces: { [k: string]: string[] } - covariate: Covariate - value: string, - facetVariables: Covariate[], - traceVariables: Covariate[], - plot: PlotConfig -}) { + }: Props) { const filteredData = data.filter(d => d[covariate.key] == value); const otherFacetVariables = facetVariables.filter(v => v.key != covariate.key); - if (otherFacetVariables.length == 0) { + const nextFacetVariable = otherFacetVariables.pop(); + if (!nextFacetVariable) { return } else { - const nextFacetVariable = otherFacetVariables.pop()!!; + const facetValues = facets[nextFacetVariable.key]; return facetValues.map(v => [
{value}
, (RootContext); const variables = [state.selectedRegressionModel].concat(state.selectedModel.variables); const settings = state.selectedPlotOptions[plot.key]; diff --git a/app/components/LocalPlot.client.tsx b/app/components/LocalPlot.client.tsx index d33574b..709e35c 100644 --- a/app/components/LocalPlot.client.tsx +++ b/app/components/LocalPlot.client.tsx @@ -1,4 +1,4 @@ -import React, {useContext} from 'react'; +import React from 'react'; import Plot from 'react-plotly.js'; import {Covariate, PlotConfig} from "~/RootContext"; @@ -7,7 +7,7 @@ interface Dat { me: number lo: number hi: number - [index: string]: any + [index: string]: string | number } interface Props { @@ -32,7 +32,7 @@ function permuteArrays(first, next, ...rest) { export default function LocalPlot({data, traceVariables, traces, value, plot}: Props) { let traceDatasets = [data]; - let traceDefinitions = permuteArrays(...traceVariables.map(v => traces[v.key])) + const traceDefinitions = permuteArrays(...traceVariables.map(v => traces[v.key])) if (traceDefinitions.length > 0) { traceDatasets = traceDefinitions.map(td => diff --git a/app/components/LocalPlot.tsx b/app/components/LocalPlot.tsx index c45fea1..8ea86cb 100644 --- a/app/components/LocalPlot.tsx +++ b/app/components/LocalPlot.tsx @@ -3,11 +3,7 @@ import Plot from "./LocalPlot.client"; import {Covariate, PlotConfig} from "~/RootContext"; interface Dat { - t: number - me: number - lo: number - hi: number - [index: string]: any + [index: string]: string | number } interface Props { diff --git a/app/components/PlotForm.tsx b/app/components/PlotForm.tsx index 5933354..bd73dc5 100644 --- a/app/components/PlotForm.tsx +++ b/app/components/PlotForm.tsx @@ -1,11 +1,11 @@ import Form from "react-bootstrap/Form"; import {Col, Row} from "react-bootstrap"; -import React, {ReactElement, useContext} from "react"; -import {AppContext, Covariate, RootContext, PlotConfig} from "~/RootContext"; +import React, {ChangeEventHandler, ReactElement, useContext} from "react"; +import {AppContext, Covariate, RootContext} from "~/RootContext"; interface Props { covariate: Covariate; - onSelect: (e: any) => void + onSelect: ChangeEventHandler selected: string } @@ -23,10 +23,6 @@ const CovariateOptions = ({ covariate, onSelect, selected}: Props): ReactElement } -interface PlotFormProps { - plot: PlotConfig -} - export default function PlotForm({plot}): ReactElement[] { const {state, dispatch} = useContext(RootContext) diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index cabb9f7..2cda531 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -1,5 +1,5 @@ import Form from 'react-bootstrap/Form'; -import {Col, Row} from "react-bootstrap"; +import {Col} from "react-bootstrap"; import React, {useContext} from "react"; import {AppContext, RootContext} from "../RootContext"; import PlotForm from "./PlotForm"; @@ -10,19 +10,22 @@ export default function Sidebar() { function onSelectModel(e) { const newState = {...state} - newState.selectedModel = state.models.find(m => m.key == e.target.value)!! + newState.selectedModel = state.models + .find(m => m.key == e.target.value) ?? state.selectedModel dispatch(newState); } function onSelectData(e) { const newState = {...state} - newState.selectedDataset = state.selectedModel.datasets.find(d => d.key == e.target.value)!! + newState.selectedDataset = state.selectedModel.datasets + .find(d => d.key == e.target.value) ?? state.selectedDataset dispatch(newState); } function onSelectCovariates(e) { const newState = {...state} - newState.selectedRegressionModel = state.selectedModel.regressionModels.find(c => c.key == e.target.value)!! + newState.selectedRegressionModel = state.selectedModel.regressionModels + .find(c => c.key == e.target.value) ?? state.selectedRegressionModel dispatch(newState); }