diff --git a/app/RootContext.ts b/app/RootContext.ts index de3d19e..c75fd9a 100644 --- a/app/RootContext.ts +++ b/app/RootContext.ts @@ -9,9 +9,7 @@ export interface PlotOptions { export interface PlotConfig { key: string displayName: string - type: string, - lineColors?: any - fillColors?: any + type: string } export interface Model { @@ -57,31 +55,14 @@ const biomarkerModel: Model = { 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)` - - ] + type: "line" }], variables: [{key: "titre_type", displayName: "Titre type"}] } export const initialState: AppState = { - models: [biomarkerModel], - selectedPlotOptions: {} + models: [biomarkerModel], + selectedPlotOptions: {} } export const RootContext = createContext({ diff --git a/app/components/ConfiguredPlot.tsx b/app/components/ConfiguredPlot.tsx index 2efb6bf..8cd5dbe 100644 --- a/app/components/ConfiguredPlot.tsx +++ b/app/components/ConfiguredPlot.tsx @@ -18,6 +18,7 @@ interface Props { facetVariables: Covariate[], traceVariables: Covariate[], plot: PlotConfig + key: string } function Facet({ diff --git a/app/components/LocalPlot.client.tsx b/app/components/LinePlot.client.tsx similarity index 57% rename from app/components/LocalPlot.client.tsx rename to app/components/LinePlot.client.tsx index 68eba61..20930ce 100644 --- a/app/components/LocalPlot.client.tsx +++ b/app/components/LinePlot.client.tsx @@ -1,12 +1,14 @@ import React from 'react'; import Plot from 'react-plotly.js'; -import {Covariate, PlotConfig} from "~/RootContext"; +import {Covariate} from "~/RootContext"; +import {interpolateBlues, interpolateOranges, interpolatePurples, interpolateGreens} from "d3-scale-chromatic"; interface Dat { t: number me: number lo: number hi: number + [index: string]: string | number } @@ -16,7 +18,6 @@ interface Props { traces: { [k: string]: string[] } value: string parent: string - plot: PlotConfig } function showLegend(traces: Covariate[]) { @@ -30,10 +31,50 @@ function permuteArrays(first, next, ...rest) { return first.flatMap(a => next.map(b => [a, b].flat())); } -export default function LocalPlot({data, traceVariables, traces, value, parent, plot}: Props) { +const colorFunctions = [ + interpolateBlues, + interpolateOranges, + interpolatePurples, + interpolateGreens +] + +function addOpacity(color) { + return color.replace(')', ', 0.3)').replace('rgb', 'rgba'); +} + +function getColors(traces, traceVariables, index, traceDefinition, numTraces) { + if (traceVariables.length == 0) { + const color = interpolateOranges(0.5); + return [color, addOpacity(color)] + } + if (traceVariables.length == 1) { + const color = colorFunctions[index](0.5) + return [color, addOpacity(color)] + } + if (traceVariables.length == 2) { + const firstTraceVariable = traceDefinition[0]; + const levels = traces[traceVariables[0].key]; + const levelIndex = levels.indexOf(firstTraceVariable); + + const secondTraceVariable = traceDefinition[1]; + const secondLevels = traces[traceVariables[1].key]; + const secondLevelIndex = secondLevels.indexOf(secondTraceVariable); + + const color = colorFunctions[levelIndex]((secondLevelIndex + 1) / secondLevels.length); + return [color, addOpacity(color)] + } + if (traceVariables.length > 2) { + // at this point the graph becomes quite unreadable anyway, so just let all traces + // be variations on a color scale + const color = colorFunctions[0]((index + 1) / numTraces) + return [color, addOpacity(color)] + } +} + +export default function LinePlot({data, traceVariables, traces, value, parent}: Props) { let traceDatasets = [data]; - const 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 => @@ -53,12 +94,13 @@ export default function LocalPlot({data, traceVariables, traces, value, parent, const subsets = traceDatasets.map((dataset, i) => { const times = dataset.map(d => d.t); const seriesName = traceDefinitions.length > 0 ? traceDefinitions[i].join(" ") : ""; + const color = getColors(traces, traceVariables, i, traceDefinitions[i], traceDatasets.length); return [{ x: times, y: dataset.map(d => d.lo), name: seriesName, line: {color: "transparent"}, - marker: {color: plot.lineColors[i]}, + marker: {color: color[0]}, showlegend: false, legendgroup: i, type: "scatter", @@ -71,9 +113,9 @@ export default function LocalPlot({data, traceVariables, traces, value, parent, type: 'scatter', mode: 'lines', fill: "tonexty", - fillcolor: plot.fillColors[i], + fillcolor: color[1], showlegend: showLegend(traceVariables), - marker: {color: plot.lineColors[i]}, + marker: {color: color[0]}, }, { x: times, y: dataset.map(d => d.hi), @@ -84,7 +126,7 @@ export default function LocalPlot({data, traceVariables, traces, value, parent, type: "scatter", mode: "lines", fill: "tonexty", - fillcolor: plot.fillColors[i], + fillcolor: color[1], }] }).flat(); diff --git a/app/components/LocalPlot.tsx b/app/components/LocalPlot.tsx index d094265..a07d684 100644 --- a/app/components/LocalPlot.tsx +++ b/app/components/LocalPlot.tsx @@ -1,11 +1,18 @@ import {ClientOnly} from "remix-utils/client-only" -import Plot from "./LocalPlot.client"; import {Covariate, PlotConfig} from "~/RootContext"; +import LinePlot from "./LinePlot.client"; +import {createElement} from "react"; interface Dat { [index: string]: string | number } +function PlotByType(type: string, props: Props) { + if (type == "line") { + return createElement(LinePlot, props) + } +} + interface Props { data: Dat[], traceVariables: Covariate[] @@ -17,6 +24,6 @@ interface Props { export default function LocalPlot(props: Props) { return - {() => ()} + {() => PlotByType(props.plot.type, props)} } diff --git a/app/root.tsx b/app/root.tsx index 54126fb..ea57a24 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -31,7 +31,7 @@ export function Layout({children}: { children: React.ReactNode }) { initialState as ReducerState ); const [status, selected] = useSelectedModel(); - if (status == 200) { + if (status == 200 && selected) { const {selectedModel, selectedRegressionModel} = selected; const newState = {...appState} diff --git a/package-lock.json b/package-lock.json index 049ca23..75a64ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@remix-run/react": "^2.10.0", "bootstrap": "^5.3.3", "cross-env": "^7.0.3", + "d3-scale-chromatic": "^3.1.0", "electron": "^31.1.0", "express": "^4.19.2", "isbot": "^4.4.0", @@ -4636,6 +4637,18 @@ "node": ">=12" } }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-scale/node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", diff --git a/package.json b/package.json index bb82529..d1c7335 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@remix-run/react": "^2.10.0", "bootstrap": "^5.3.3", "cross-env": "^7.0.3", + "d3-scale-chromatic": "^3.1.0", "electron": "^31.1.0", "express": "^4.19.2", "isbot": "^4.4.0",