Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor trajectories plot #738

Draft
wants to merge 29 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0c0d849
refactor: move and rename DeterministicLinePlot to ResultsTrajectorie…
ivan-aksamentov Jun 10, 2020
477d689
refactor: temporarily move formatter functions into component body
ivan-aksamentov Jun 10, 2020
d94ae06
refactor: reduce duplication, improve readability
ivan-aksamentov Jun 10, 2020
284a22a
refactor: reformat duplicated code for clarity
ivan-aksamentov Jun 10, 2020
6cd3dfa
refactor: clarify the intent by using a sort function
ivan-aksamentov Jun 11, 2020
c431dab
refactor: temporarily replace iterated value with array access for cl…
ivan-aksamentov Jun 11, 2020
c3845ff
refactor: select only necessary scenario state
ivan-aksamentov Jun 11, 2020
3458257
fix: don't take the middle into account
ivan-aksamentov Jun 11, 2020
ab71ef2
fix: naming conventions
ivan-aksamentov Jun 11, 2020
5ccbfc0
refactor: accept a tuple in sort2
ivan-aksamentov Jun 11, 2020
5630c7d
refactor: rename sort2 to sortPair
ivan-aksamentov Jun 11, 2020
f65bbea
refactor: name variables
ivan-aksamentov Jun 11, 2020
1146242
refactor: name functions
ivan-aksamentov Jun 11, 2020
831812e
refactor: extract variable
ivan-aksamentov Jun 11, 2020
9447215
refactor: organize verifyTuple function
ivan-aksamentov Jun 11, 2020
fede79f
refactor: move verify tuple function closer to usage
ivan-aksamentov Jun 11, 2020
c734c45
refactor: replace duplication with a loop
ivan-aksamentov Jun 11, 2020
5ecc051
fix: replace wrong functor
ivan-aksamentov Jun 11, 2020
0cebafa
refactor: rename function to clarify intent
ivan-aksamentov Jun 11, 2020
4e4a52a
fix: add missing import
ivan-aksamentov Jun 12, 2020
e25092f
refactor: cleanup and memoize number formatters
ivan-aksamentov Jun 12, 2020
4e49a0d
fix: adjust path in tsc ignore list
ivan-aksamentov Jun 12, 2020
a822edf
Merge remote-tracking branch 'origin/master' into refactor/trajectori…
ivan-aksamentov Jun 12, 2020
dcb4c7b
fix: lint
ivan-aksamentov Jun 12, 2020
8089cec
refactor: extract hardcoded value into a variable
ivan-aksamentov Jun 12, 2020
56af656
refactor: extract inline handler
ivan-aksamentov Jun 12, 2020
15c11e3
refactor: make property names consistent
ivan-aksamentov Jun 12, 2020
888cd20
refactor: (wip) extract functions from plot component body
ivan-aksamentov Jun 12, 2020
290b963
Merge remote-tracking branch 'origin/master' into refactor/trajectori…
ivan-aksamentov Jun 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ module.exports = {
'src/algorithms/model.ts', // FIXME
'src/algorithms/results.ts', // FIXME
'src/components/Main/Results/AgeBarChart.tsx', // FIXME
'src/components/Main/Results/DeterministicLinePlot.tsx', // FIXME
'src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx', // FIXME
'src/components/Main/Results/Utils.ts', // FIXME
],
rules: {
Expand Down
2 changes: 1 addition & 1 deletion config/webpack/webpack.client.babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ export default {
'!src/algorithms/model.ts', // FIXME
'!src/algorithms/results.ts', // FIXME
'!src/components/Main/Results/AgeBarChart.tsx', // FIXME
'!src/components/Main/Results/DeterministicLinePlot.tsx', // FIXME
'!src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx', // FIXME
'!src/components/Main/Results/Utils.ts', // FIXME
// end

Expand Down
2 changes: 1 addition & 1 deletion cypress/integration/results.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="cypress" />

const resultsCharts = ['DeterministicLinePlot', 'AgeBarChart', 'OutcomeRatesTable']
const resultsCharts = ['ResultsTrajectoriesPlot', 'AgeBarChart', 'OutcomeRatesTable']

context('The results card', () => {
beforeEach(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/algorithms/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ export function collectTotals(trajectory: SimulationTimePoint[], ages: string[])
}

function title(name: string): string {
return name === 'critical' ? 'ICU' : name
return name === 'critical' ? 'icu' : name
}

export interface SerializeTrajectoryParams {
Expand Down
157 changes: 96 additions & 61 deletions src/algorithms/preparePlotData.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,104 @@
import type { Trajectory, PlotDatum } from './types/Result.types'
import { verifyPositive, verifyTuple } from '../components/Main/Results/Utils'
/* eslint-disable no-param-reassign */

import { pickBy, mapValues, pick } from 'lodash'
import { isNumeric, max, min } from 'mathjs'

import type { Trajectory, PlotDatum, Line, Area, PlotData } from './types/Result.types'
import { MaybeNumber } from '../components/Main/Results/Utils'
import { soa } from './utils/soa'

import { sortPair } from './utils/sortPair'
// import { linesToPlot, areasToPlot, DATA_POINTS } from '../components/Main/Results/ChartCommon'

export function preparePlotData(trajectory: Trajectory): PlotDatum[] {
export function takePositiveValues<T extends { [key: string]: number }>(obj: T) {
return pickBy(obj, (value) => value > 0) as T
}

export function roundValues<T extends { [key: string]: number }>(obj: T) {
return mapValues(obj, Math.round)
}

export function verifyPositive(x: number): MaybeNumber {
const xRounded = Math.round(x)
return xRounded > 0 ? xRounded : undefined
}

export function verifyTuple([low, mid, upp]: [MaybeNumber, MaybeNumber, MaybeNumber]): [number, number] | undefined {
low = verifyPositive(low ?? 0)
mid = verifyPositive(mid ?? 0)
upp = verifyPositive(upp ?? 0)

if (isNumeric(low) && isNumeric(upp) && isNumeric(mid)) {
return [min(low, mid), max(mid, upp)]
}

if (isNumeric(low) && isNumeric(upp)) {
return [low, upp]
}

if (!isNumeric(low) && isNumeric(upp) && isNumeric(mid)) {
return [0.0001, max(mid, upp)]
}

if (!isNumeric(low) && isNumeric(upp)) {
return [0.0001, upp]
}

return undefined
}

export function verifyTuples<T extends { [key: string]: MaybeNumber[] }>(obj: T) {
return mapValues(obj, (x) => verifyTuple(x))
}

export function preparePlotData(trajectory: Trajectory) {
const { lower, middle, upper } = trajectory

return middle.map((x, day) => {
const data = middle.map((_0, day) => {
const previousDay = day > 6 ? day - 7 : 0
const centerWeeklyDeaths = x.cumulative.fatality.total - middle[previousDay].cumulative.fatality.total
// NOTE: this is using the upper and lower trajectories
const extremeWeeklyDeaths1 = upper[day].cumulative.fatality.total - upper[previousDay].cumulative.fatality.total
const extremeWeeklyDeaths2 = lower[day].cumulative.fatality.total - lower[previousDay].cumulative.fatality.total
const upperWeeklyDeaths = extremeWeeklyDeaths1 > extremeWeeklyDeaths2 ? extremeWeeklyDeaths1 : extremeWeeklyDeaths2
const lowerWeeklyDeaths = extremeWeeklyDeaths1 > extremeWeeklyDeaths2 ? extremeWeeklyDeaths2 : extremeWeeklyDeaths1

return {
time: x.time,
lines: {
susceptible: verifyPositive(x.current.susceptible.total),
infectious: verifyPositive(x.current.infectious.total),
severe: verifyPositive(x.current.severe.total),
critical: verifyPositive(x.current.critical.total),
overflow: verifyPositive(x.current.overflow.total),
recovered: verifyPositive(x.cumulative.recovered.total),
fatality: verifyPositive(x.cumulative.fatality.total),
weeklyFatality: verifyPositive(centerWeeklyDeaths),
},
// Error bars
areas: {
susceptible: verifyTuple(
[verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total)],
x.current.susceptible.total,
),
infectious: verifyTuple(
[verifyPositive(lower[day].current.infectious.total), verifyPositive(upper[day].current.infectious.total)],
x.current.infectious.total,
),
severe: verifyTuple(
[verifyPositive(lower[day].current.severe.total), verifyPositive(upper[day].current.severe.total)],
x.current.severe.total,
),
critical: verifyTuple(
[verifyPositive(lower[day].current.critical.total), verifyPositive(upper[day].current.critical.total)],
x.current.critical.total,
),
overflow: verifyTuple(
[verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total)],
x.current.overflow.total,
),
recovered: verifyTuple(
[
verifyPositive(lower[day].cumulative.recovered.total),
verifyPositive(upper[day].cumulative.recovered.total),
],
x.cumulative.recovered.total,
),
fatality: verifyTuple(
[verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total)],
x.cumulative.fatality.total,
),
weeklyFatality: verifyTuple(
[verifyPositive(lowerWeeklyDeaths), verifyPositive(upperWeeklyDeaths)],
x.cumulative.fatality.total - middle[previousDay].cumulative.fatality.total,
),
},

const weeklyFatalityMiddle = middle[day].cumulative.fatality.total - middle[previousDay].cumulative.fatality.total // prettier-ignore
let weeklyFatalityLower = lower[day].cumulative.fatality.total - lower[previousDay].cumulative.fatality.total // prettier-ignore
let weeklyFatalityUpper = upper[day].cumulative.fatality.total - upper[previousDay].cumulative.fatality.total // prettier-ignore

;[weeklyFatalityLower, weeklyFatalityUpper] = sortPair([weeklyFatalityLower, weeklyFatalityUpper]) // prettier-ignore

let lines: Line = {
susceptible: middle[day].current.susceptible.total,
infectious: middle[day].current.infectious.total,
severe: middle[day].current.severe.total,
critical: middle[day].current.critical.total,
overflow: middle[day].current.overflow.total,
recovered: middle[day].cumulative.recovered.total,
fatality: middle[day].cumulative.fatality.total,
weeklyFatality: weeklyFatalityMiddle,
}

lines = takePositiveValues(lines)
lines = roundValues(lines)

const areasRaw = {
susceptible: [ lower[day].current.susceptible.total, middle[day].current.susceptible.total, upper[day].current.susceptible.total ], // prettier-ignore
infectious: [ lower[day].current.infectious.total, middle[day].current.infectious.total, upper[day].current.infectious.total ], // prettier-ignore
severe: [ lower[day].current.severe.total, middle[day].current.severe.total, upper[day].current.severe.total ], // prettier-ignore
critical: [ lower[day].current.critical.total, middle[day].current.critical.total, upper[day].current.critical.total ], // prettier-ignore
overflow: [ lower[day].current.overflow.total, middle[day].current.overflow.total, upper[day].current.overflow.total ], // prettier-ignore
recovered: [ lower[day].cumulative.recovered.total, middle[day].cumulative.recovered.total, upper[day].cumulative.recovered.total ], // prettier-ignore
fatality: [ lower[day].cumulative.fatality.total, middle[day].cumulative.fatality.total, upper[day].cumulative.fatality.total ], // prettier-ignore
weeklyFatality: [ weeklyFatalityLower, weeklyFatalityMiddle, weeklyFatalityUpper ] // prettier-ignore
}

const areas: Area = verifyTuples(areasRaw)

return { time: middle[day].time, lines, areas }
})

const { time, lines, areas } = (soa(data) as unknown) as PlotData

let linesObject = soa(lines)
let areasObject = soa(areas)

return { linesObject, areasObject }
// TODO: sort by time
// plotData.sort((a, b) => (a.time > b.time ? 1 : -1))
}
60 changes: 58 additions & 2 deletions src/algorithms/types/Result.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,66 @@ export interface TimeSeriesWithRange {
upper: TimeSeries
}

export interface Line {
susceptible?: number
infectious?: number
severe?: number
critical?: number
overflow?: number
recovered?: number
fatality?: number
weeklyFatality?: number
}

export type Pair<T> = [T, T]

export interface Area {
susceptible?: Pair<number>
infectious?: Pair<number>
severe?: Pair<number>
critical?: Pair<number>
overflow?: Pair<number>
recovered?: Pair<number>
fatality?: Pair<number>
weeklyFatality?: Pair<number>
}

export interface PlotDatum {
time: number
lines: Record<string, number | undefined>
areas: Record<string, [number, number] | undefined>
lines: Line
areas: Area
}

// TODO: should not intersect with AreaObject
// otherwise properties will be overwritten
export interface LineObject {
susceptible?: number[]
infectious?: number[]
severe?: number[]
critical?: number[]
overflow?: number[]
recovered?: number[]
fatality?: number[]
weeklyFatality?: number[]
}

// TODO: should not intersect with LineObject
// otherwise properties will be overwritten
export interface AreaObject {
susceptible?: Pair<number>[]
infectious?: Pair<number>[]
severe?: Pair<number>[]
critical?: Pair<number>[]
overflow?: Pair<number>[]
recovered?: Pair<number>[]
fatality?: Pair<number>[]
weeklyFatality?: Pair<number>[]
}

export interface PlotData {
time: number[]
linesObject: LineObject
areasObject: AreaObject
}

export interface AlgorithmResult {
Expand Down
88 changes: 88 additions & 0 deletions src/algorithms/utils/__tests__/soa.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { cloneDeep } from 'lodash'
import { soa } from '../soa'

describe('soa', () => {
it('converts an empty array to an empty object', () => {
expect(soa([])).toStrictEqual({})
})

it('converts a 1-element array', () => {
expect(soa([{ foo: 42, bar: 3.14 }])).toStrictEqual({ foo: [42], bar: [3.14] })
})

it('converts a 2-element array', () => {
expect(
soa([
{ foo: 42, bar: 3.14 },
{ foo: 2.72, bar: -5 },
]),
).toStrictEqual({
foo: [42, 2.72],
bar: [3.14, -5],
})
})

it('converts a 3-element array', () => {
expect(
soa([
{ foo: 42, bar: 3.14 },
{ foo: 2.72, bar: -5 },
{ foo: 0, bar: 7 },
]),
).toStrictEqual({
foo: [42, 2.72, 0],
bar: [3.14, -5, 7],
})
})

it('converts a array of objects with properties of different types', () => {
expect(
soa([
{ foo: 42, bar: 'a' },
{ foo: 2.72, bar: 'b' },
{ foo: 0, bar: 'c' },
]),
).toStrictEqual({
foo: [42, 2.72, 0],
bar: ['a', 'b', 'c'],
})
})

it('converts a array of objects of mixed types', () => {
expect(
soa([
{ foo: 'a', bar: 42 },
{ foo: 2.72, bar: { x: 5, y: -3 } },
{ foo: null, bar: false },
]),
).toStrictEqual({
foo: ['a', 2.72, null],
bar: [42, { x: 5, y: -3 }, false],
})
})

it('preserves holes', () => {
expect(
soa([
{ foo: undefined, bar: 42 },
{ foo: undefined, bar: 98 },
{ foo: undefined, bar: 76 },
]),
).toStrictEqual({
foo: [undefined, undefined, undefined],
bar: [42, 98, 76],
})
})

it('does not modify the arguments', () => {
const data = [
{ foo: 'a', bar: 42 },
{ foo: 'b', bar: 98 },
{ foo: 'c', bar: 76 },
]

const dataCopy = cloneDeep(data)
soa(data)
expect(data).toStrictEqual(dataCopy)
})
})
Loading