Skip to content

Commit

Permalink
Merge pull request #8 from seroanalytics/colors
Browse files Browse the repository at this point in the history
i7 dynamic colors by variable
  • Loading branch information
hillalex authored Aug 2, 2024
2 parents 1b578eb + 01aabc9 commit 3b19d22
Show file tree
Hide file tree
Showing 17 changed files with 8,331 additions and 5,252 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: 🔎 Test

on:
push:
branches:
- main
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: 🔎 Test
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4

- name: ⎔ Setup node
uses: actions/setup-node@v4

- name: 📥 Install deps
run: npm install --frozen-lockfile

- name: 🔎 Test
run: npm run test

- name: Upload coverage
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# epikinetics-app [![Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostatus.org/#wip)
[![⬣ Lint](https://github.com/seroanalytics/epikinetics-app/actions/workflows/lint.yml/badge.svg)](https://github.com/seroanalytics/epikinetics-app/actions/workflows/lint.yml)
[![🔨 Build](https://github.com/seroanalytics/epikinetics-app/actions/workflows/build.yml/badge.svg)](https://github.com/seroanalytics/epikinetics-app/actions/workflows/build.yml)
[![codecov](https://codecov.io/gh/seroanalytics/epikinetics-app/graph/badge.svg?token=FH6QSJGNVR)](https://codecov.io/gh/seroanalytics/epikinetics-app)

Browser application for exploring [epikinetics](https://seroanalytics.github.io/epikinetics/) model results.
Based on the `remix` Javascript/Typescript framework.
Expand Down
62 changes: 4 additions & 58 deletions app/RootContext.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,5 @@
import {createContext} from "react";

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[]
selectedPlotOptions: { [index: string]: PlotOptions }
}
import {AppState, Model} from "~/types";

export interface AppContext {
state: AppState
Expand All @@ -57,31 +20,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<AppContext>({
Expand Down
4 changes: 3 additions & 1 deletion app/components/ConfiguredPlot.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, {useContext} from "react";
import {AppContext, Covariate, PlotConfig, RootContext} from "~/RootContext";
import {AppContext, RootContext} from "~/RootContext";
import {Col, Row} from "react-bootstrap";
import LocalPlot from "~/components/LocalPlot";
import useSelectedModel from "~/hooks/useSelectedModel";
import {Covariate, PlotConfig} from "~/types";

interface Dat {
[index: string]: string | number
Expand All @@ -18,6 +19,7 @@ interface Props {
facetVariables: Covariate[],
traceVariables: Covariate[],
plot: PlotConfig
key: string
}

function Facet({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,33 @@
import React from 'react';
import Plot from 'react-plotly.js';
import {Covariate, PlotConfig} from "~/RootContext";
import {Covariate, Dict} from "~/types";
import {getColors, permuteArrays} from "~/utils/plotUtils";

interface Dat {
t: number
me: number
lo: number
hi: number

[index: string]: string | number
}

interface Props {
data: Dat[],
traceVariables: Covariate[]
traces: { [k: string]: string[] }
traces: Dict<string[]>
value: string
parent: string
plot: PlotConfig
}

function showLegend(traces: Covariate[]) {
return traces.length > 0
}

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, traceVariables, traces, value, parent, plot}: Props) {
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 =>
Expand All @@ -53,12 +47,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.line},
showlegend: false,
legendgroup: i,
type: "scatter",
Expand All @@ -71,9 +66,9 @@ export default function LocalPlot({data, traceVariables, traces, value, parent,
type: 'scatter',
mode: 'lines',
fill: "tonexty",
fillcolor: plot.fillColors[i],
fillcolor: color.fill,
showlegend: showLegend(traceVariables),
marker: {color: plot.lineColors[i]},
marker: {color: color.line},
}, {
x: times,
y: dataset.map(d => d.hi),
Expand All @@ -84,7 +79,7 @@ export default function LocalPlot({data, traceVariables, traces, value, parent,
type: "scatter",
mode: "lines",
fill: "tonexty",
fillcolor: plot.fillColors[i],
fillcolor: color.fill,
}]
}).flat();

Expand Down
13 changes: 10 additions & 3 deletions app/components/LocalPlot.tsx
Original file line number Diff line number Diff line change
@@ -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";
import {Covariate, PlotConfig} from "~/types";

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[]
Expand All @@ -17,6 +24,6 @@ interface Props {

export default function LocalPlot(props: Props) {
return <ClientOnly>
{() => (<Plot {...props} />)}
{() => PlotByType(props.plot.type, props)}
</ClientOnly>
}
3 changes: 2 additions & 1 deletion app/components/PlotForm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Form from "react-bootstrap/Form";
import {Col, Row} from "react-bootstrap";
import React, {ChangeEventHandler, ReactElement, useContext} from "react";
import {AppContext, Covariate, PlotConfig, RootContext} from "~/RootContext";
import {AppContext, RootContext} from "~/RootContext";
import useSelectedModel from "~/hooks/useSelectedModel";
import {Covariate, PlotConfig} from "~/types";

interface Props {
covariate: Covariate;
Expand Down
1 change: 1 addition & 0 deletions app/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'd3-scale-chromatic';
2 changes: 1 addition & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function Layout({children}: { children: React.ReactNode }) {
initialState as ReducerState<AppReducer>
);
const [status, selected] = useSelectedModel();
if (status == 200) {
if (status == 200 && selected) {
const {selectedModel, selectedRegressionModel} = selected;

const newState = {...appState}
Expand Down
39 changes: 39 additions & 0 deletions app/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
type PlotDisplay = "facet" | "trace"

export interface Dict<T> {
[index: string]: T
}

export interface PlotOptions {
[index: string]: PlotDisplay
}

export interface PlotConfig {
key: string
displayName: string
type: string
}

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[]
selectedPlotOptions: { [index: string]: PlotOptions }
}
62 changes: 62 additions & 0 deletions app/utils/plotUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {interpolateBlues, interpolateOranges, interpolatePurples, interpolateGreens, interpolateReds, interpolateGreys} from "d3-scale-chromatic";
import {Covariate, Dict} from "~/types";

// @ts-expect-error this function doesn't lend itself well to typing
export function permuteArrays(first, next, ...rest) {
if (!first) return [];
if (!next) return first.map(a => [a]);
if (rest.length) next = permuteArrays(next, ...rest);
return first.flatMap(a => next.map(b => [a, b].flat()));
}

const colorFunctions = [
interpolateBlues,
interpolateOranges,
interpolatePurples,
interpolateGreens,
interpolateReds,
interpolateGreys
]

export function addOpacity(color: string) {
return color.replace(')', ', 0.3)').replace('rgb', 'rgba');
}

export interface ColorOptions {
line: string
fill: string
}

export function getColors(traces: Dict<string[]>,
traceVariables: Covariate[],
index: number,
traceDefinition: string[],
numTraces: number): ColorOptions {
if (traceVariables.length == 0) {
const color = colorFunctions[0](0.5);
return {line: color, fill: addOpacity(color)}
}
if (traceVariables.length == 1) {
const colorIndex = index % colorFunctions.length;
const color = colorFunctions[colorIndex](0.2 * (Math.floor(index / colorFunctions.length) + 1));
return {line: color, fill: 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 {line: color, fill: 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 {line: color, fill: addOpacity(color)}
}
}
14 changes: 14 additions & 0 deletions jest.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
testEnvironment: "node",
transform: {
"^.+.tsx?$": ["ts-jest",{
diagnostics: false,
}],
},
moduleNameMapper: {
'^d3-scale-chromatic': '<rootDir>/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.js',
'^d3-interpolate': '<rootDir>/node_modules/d3-interpolate/dist/d3-interpolate.js',
'^d3-color': '<rootDir>/node_modules/d3-color/dist/d3-color.js',
},
}
Loading

0 comments on commit 3b19d22

Please sign in to comment.