diff --git a/.eslintrc.js b/.eslintrc.js
index a5419b2a3..f4bc38972 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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: {
diff --git a/config/webpack/webpack.client.babel.ts b/config/webpack/webpack.client.babel.ts
index 326bdbf74..35323234e 100644
--- a/config/webpack/webpack.client.babel.ts
+++ b/config/webpack/webpack.client.babel.ts
@@ -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
diff --git a/cypress/integration/results.spec.ts b/cypress/integration/results.spec.ts
index f3318f28b..bb98e4d04 100644
--- a/cypress/integration/results.spec.ts
+++ b/cypress/integration/results.spec.ts
@@ -1,6 +1,6 @@
///
-const resultsCharts = ['DeterministicLinePlot', 'AgeBarChart', 'OutcomeRatesTable']
+const resultsCharts = ['ResultsTrajectoriesPlot', 'AgeBarChart', 'OutcomeRatesTable']
context('The results card', () => {
beforeEach(() => {
diff --git a/src/algorithms/model.ts b/src/algorithms/model.ts
index 329c174f8..249084aa4 100644
--- a/src/algorithms/model.ts
+++ b/src/algorithms/model.ts
@@ -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 {
diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts
index 857fe3934..9b9f3508b 100644
--- a/src/algorithms/preparePlotData.ts
+++ b/src/algorithms/preparePlotData.ts
@@ -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(obj: T) {
+ return pickBy(obj, (value) => value > 0) as T
+}
+
+export function roundValues(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(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))
}
diff --git a/src/algorithms/types/Result.types.ts b/src/algorithms/types/Result.types.ts
index bf11c2913..efadf1435 100644
--- a/src/algorithms/types/Result.types.ts
+++ b/src/algorithms/types/Result.types.ts
@@ -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]
+
+export interface Area {
+ susceptible?: Pair
+ infectious?: Pair
+ severe?: Pair
+ critical?: Pair
+ overflow?: Pair
+ recovered?: Pair
+ fatality?: Pair
+ weeklyFatality?: Pair
+}
+
export interface PlotDatum {
time: number
- lines: Record
- areas: Record
+ 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[]
+ infectious?: Pair[]
+ severe?: Pair[]
+ critical?: Pair[]
+ overflow?: Pair[]
+ recovered?: Pair[]
+ fatality?: Pair[]
+ weeklyFatality?: Pair[]
+}
+
+export interface PlotData {
+ time: number[]
+ linesObject: LineObject
+ areasObject: AreaObject
}
export interface AlgorithmResult {
diff --git a/src/algorithms/utils/__tests__/soa.test.ts b/src/algorithms/utils/__tests__/soa.test.ts
new file mode 100644
index 000000000..192cf6ec8
--- /dev/null
+++ b/src/algorithms/utils/__tests__/soa.test.ts
@@ -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)
+ })
+})
diff --git a/src/algorithms/utils/__tests__/sort3.test.ts b/src/algorithms/utils/__tests__/sort3.test.ts
new file mode 100644
index 000000000..b980cbad0
--- /dev/null
+++ b/src/algorithms/utils/__tests__/sort3.test.ts
@@ -0,0 +1,61 @@
+import { sort3 } from '../sort3'
+
+describe('sort3', () => {
+ it('sorts already ordered', async () => {
+ expect(sort3(-5, 3.14, 42)).toStrictEqual([-5, 3.14, 42])
+ })
+
+ it('sorts any unordered numbers', async () => {
+ expect(sort3(3.14, -5, 42)).toStrictEqual([-5, 3.14, 42])
+ })
+
+ it('sorts equals: all', async () => {
+ expect(sort3(3.14, 3.14, 3.14)).toStrictEqual([3.14, 3.14, 3.14])
+ })
+
+ it('sorts equals: all zeros', async () => {
+ expect(sort3(0, 0, 0)).toStrictEqual([0, 0, 0])
+ })
+
+ it('sorts equals: left', async () => {
+ expect(sort3(42, 2, 2)).toStrictEqual([2, 2, 42])
+ })
+
+ it('sorts equals: sides', async () => {
+ expect(sort3(2, 42, 2)).toStrictEqual([2, 2, 42])
+ })
+
+ it('sorts equals: right', async () => {
+ expect(sort3(42, 42, 2)).toStrictEqual([2, 42, 42])
+ })
+
+ it('sorts: 1, 2, 3', async () => {
+ expect(sort3(1, 2, 3)).toStrictEqual([1, 2, 3])
+ })
+
+ it('sorts: 2, 1, 3', async () => {
+ expect(sort3(2, 1, 3)).toStrictEqual([1, 2, 3])
+ })
+
+ it('sorts: 1, 3, 2', async () => {
+ expect(sort3(1, 3, 2)).toStrictEqual([1, 2, 3])
+ })
+
+ it('sorts: 3, 1, 2', async () => {
+ expect(sort3(3, 1, 2)).toStrictEqual([1, 2, 3])
+ })
+
+ it('sorts: 3, 2, 1', async () => {
+ expect(sort3(3, 2, 1)).toStrictEqual([1, 2, 3])
+ })
+
+ it('does not mutate arguments', async () => {
+ const a = -5
+ const b = 3.14
+ const c = 42
+ const [, ,] = sort3(c, a, b)
+ expect(a).toBe(-5)
+ expect(b).toBe(3.14)
+ expect(c).toBe(42)
+ })
+})
diff --git a/src/algorithms/utils/__tests__/sortPair.test.ts b/src/algorithms/utils/__tests__/sortPair.test.ts
new file mode 100644
index 000000000..83a2ea1fe
--- /dev/null
+++ b/src/algorithms/utils/__tests__/sortPair.test.ts
@@ -0,0 +1,35 @@
+import { sortPair } from '../sortPair'
+
+describe('sortPair', () => {
+ it('sorts already ordered', async () => {
+ expect(sortPair([-5, 3.14])).toStrictEqual([-5, 3.14])
+ })
+
+ it('sorts any unordered numbers', async () => {
+ expect(sortPair([3.14, -5])).toStrictEqual([-5, 3.14])
+ })
+
+ it('sorts equals: all', async () => {
+ expect(sortPair([3.14, 3.14])).toStrictEqual([3.14, 3.14])
+ })
+
+ it('sorts equals: all zeros', async () => {
+ expect(sortPair([0, 0])).toStrictEqual([0, 0])
+ })
+
+ it('does not mutate arguments', async () => {
+ const a = 3.14
+ const b = -5
+ const [, ,] = sortPair([a, b])
+ expect(a).toBe(3.14)
+ expect(b).toBe(-5)
+ })
+
+ it('overwrites locals', async () => {
+ let a = 3.14
+ let b = -5
+ ;[a, b] = sortPair([a, b])
+ expect(a).toBe(-5)
+ expect(b).toBe(3.14)
+ })
+})
diff --git a/src/algorithms/utils/__tests__/swap.test.ts b/src/algorithms/utils/__tests__/swap.test.ts
new file mode 100644
index 000000000..3dd8a9bab
--- /dev/null
+++ b/src/algorithms/utils/__tests__/swap.test.ts
@@ -0,0 +1,27 @@
+import { swap } from '../swap'
+
+describe('sort', () => {
+ it('swaps numbers', async () => {
+ expect(swap(1.1, 4)).toStrictEqual([4, 1.1])
+ })
+
+ it('swaps strings', async () => {
+ expect(swap('hello', 'swap')).toStrictEqual(['swap', 'hello'])
+ })
+
+ it('swaps number and string', async () => {
+ expect(swap('hello', 42)).toStrictEqual([42, 'hello'])
+ })
+
+ it('swaps undefined, without gaps', async () => {
+ expect(swap(undefined, undefined)).toStrictEqual([undefined, undefined])
+ })
+
+ it('does not mutate arguments', async () => {
+ const a = 'hello'
+ const b = 42
+ const [,] = swap(a, b)
+ expect(a).toEqual('hello')
+ expect(b).toEqual(42)
+ })
+})
diff --git a/src/algorithms/utils/soa.ts b/src/algorithms/utils/soa.ts
new file mode 100644
index 000000000..5b0924ae6
--- /dev/null
+++ b/src/algorithms/utils/soa.ts
@@ -0,0 +1,17 @@
+import { map, zipObject } from 'lodash'
+
+/**
+ * Converts array of objects to an object of arrays ("Array of Structs" -> "Struct of Arrays" in olden terminology).
+ * NOTE: The keys of the resulting object will be the same as in the *first* element of the input array.
+ * NOTE: It will work properly with mismatched objects, but mostly only makes sense if all of the elements
+ * of the input array are of the same shape and all properties are of the same type.
+ */
+export function soa(arr: T[]): { [key: string]: T[K][] } {
+ if (arr.length === 0) {
+ return {} as { [key: string]: T[K][] }
+ }
+
+ const keys = Object.keys(arr[0])
+ const something = keys.map((key) => map(arr, key))
+ return zipObject(keys, something) as { [key: string]: T[K][] }
+}
diff --git a/src/algorithms/utils/sort3.ts b/src/algorithms/utils/sort3.ts
new file mode 100644
index 000000000..2a6076232
--- /dev/null
+++ b/src/algorithms/utils/sort3.ts
@@ -0,0 +1,9 @@
+/* eslint-disable no-param-reassign */
+
+/** Puts 3 given numbers in ascending order. Does not mutate arguments. */
+export function sort3(a: T, b: T, c: T) {
+ if (a > c) [a, c] = [c, a]
+ if (a > b) [a, b] = [b, a]
+ if (b > c) [b, c] = [c, b]
+ return [a, b, c]
+}
diff --git a/src/algorithms/utils/sortPair.ts b/src/algorithms/utils/sortPair.ts
new file mode 100644
index 000000000..1415c9e1b
--- /dev/null
+++ b/src/algorithms/utils/sortPair.ts
@@ -0,0 +1,5 @@
+/** Puts 2 given numbers in ascending order. Does not mutate arguments. */
+export function sortPair([a, b]: [T, T]) {
+ if (a < b) return [a, b]
+ return [b, a]
+}
diff --git a/src/algorithms/utils/swap.ts b/src/algorithms/utils/swap.ts
new file mode 100644
index 000000000..178e234b6
--- /dev/null
+++ b/src/algorithms/utils/swap.ts
@@ -0,0 +1,4 @@
+/** Swaps order of 2 values. Does not mutate arguments. */
+export function swap(x: X, y: Y): [Y, X] {
+ return [y, x]
+}
diff --git a/src/components/Main/PrintPreview/PrintPreview.tsx b/src/components/Main/PrintPreview/PrintPreview.tsx
index bd781769d..7b5f8f4bb 100644
--- a/src/components/Main/PrintPreview/PrintPreview.tsx
+++ b/src/components/Main/PrintPreview/PrintPreview.tsx
@@ -25,7 +25,7 @@ import {
selectSeverityDistributionData,
} from '../../../state/scenario/scenario.selectors'
-import { DeterministicLinePlot } from '../Results/DeterministicLinePlot'
+import { ResultsTrajectoriesPlot } from '../ResultsTrajectoriesPlot/ResultsTrajectoriesPlot'
import { OutcomeRatesTable } from '../Results/OutcomeRatesTable'
import { AgeBarChart } from '../Results/AgeBarChart'
import { OutcomesDetailsTable } from '../Results/OutcomesDetailsTable'
@@ -293,7 +293,7 @@ export function PrintPreviewDisconnected({
{t('Results')}
-
+
diff --git a/src/components/Main/Results/AgeBarChart.tsx b/src/components/Main/Results/AgeBarChart.tsx
index 2ce603342..21663d203 100644
--- a/src/components/Main/Results/AgeBarChart.tsx
+++ b/src/components/Main/Results/AgeBarChart.tsx
@@ -1,5 +1,5 @@
import { TFunction, TFunctionResult } from 'i18next'
-import React from 'react'
+import React, { useMemo } from 'react'
import { sumBy } from 'lodash'
import { connect } from 'react-redux'
@@ -22,13 +22,13 @@ import {
import type { AlgorithmResult } from '../../../algorithms/types/Result.types'
import type { AgeDistributionDatum, SeverityDistributionDatum } from '../../../algorithms/types/Param.types'
-import { numberFormatter } from '../../../helpers/numberFormat'
+import { getNumberFormatters } from '../../../helpers/numberFormat'
import { State } from '../../../state/reducer'
import { selectAgeDistributionData, selectSeverityDistributionData } from '../../../state/scenario/scenario.selectors'
import { selectResult } from '../../../state/algorithm/algorithm.selectors'
import { selectShouldFormatNumbers, selectShouldShowPlotLabels } from '../../../state/settings/settings.selectors'
-import { colors } from './ChartCommon'
+import { CategoryColor } from './ChartCommon'
import { calculatePosition, scrollToRef } from './chartHelper'
import { ChartTooltip } from './ChartTooltip'
@@ -64,6 +64,7 @@ export function AgeBarChartDisconnected({
}: AgeBarChartProps) {
const { t: unsafeT } = useTranslation()
const casesChartRef = React.useRef(null)
+ const { formatNumber, formatNumberRounded } = useMemo(() => getNumberFormatters({ shouldFormatNumbers }), [shouldFormatNumbers]) // prettier-ignore
const t = unsafeT as SafeTFunction
@@ -85,7 +86,7 @@ export function AgeBarChartDisconnected({
num = Number.parseFloat(num)
}
- return numberFormatter(true, false)(num)
+ return formatNumber(num)
},
}
@@ -93,9 +94,6 @@ export function AgeBarChartDisconnected({
return null
}
- const formatNumber = numberFormatter(shouldFormatNumbers, false)
- const formatNumberRounded = numberFormatter(shouldFormatNumbers, true)
-
// Ensure age distribution is normalized
const Z: number = sumBy(ageDistributionData, ({ population }) => population)
const normAgeDistribution = ageDistributionData.map((d) => d.population / Z)
@@ -193,38 +191,38 @@ export function AgeBarChartDisconnected({
-
+
-
+
-
+
-
+
{
return { ...line, name: t(line.name) }
})
diff --git a/src/components/Main/Results/ChartTooltip.tsx b/src/components/Main/Results/ChartTooltip.tsx
index dd86a10a4..14e04b9d9 100644
--- a/src/components/Main/Results/ChartTooltip.tsx
+++ b/src/components/Main/Results/ChartTooltip.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import { TooltipProps, TooltipPayload } from 'recharts'
-import { colors } from './ChartCommon'
+import { CategoryColor } from './ChartCommon'
import { ResponsiveTooltipContent, TooltipItem } from './ResponsiveTooltipContent'
import './ResponsiveTooltipContent.scss'
@@ -58,7 +58,7 @@ export function ChartTooltip({ active, payload, label, valueFormatter, labelForm
name: payloadItem.name,
color:
payloadItem.color ||
- ((payloadItem.dataKey as string) in colors ? colors[payloadItem.dataKey as string] : '#bbbbbb'),
+ ((payloadItem.dataKey as string) in CategoryColor ? CategoryColor[payloadItem.dataKey as string] : '#bbbbbb'),
key: (payloadItem.dataKey as string) || payloadItem.name,
value: maybeFormatted(value),
lower: maybeFormatted(lower),
diff --git a/src/components/Main/Results/DeterministicLinePlot.tsx b/src/components/Main/Results/DeterministicLinePlot.tsx
deleted file mode 100644
index 0f35e404c..000000000
--- a/src/components/Main/Results/DeterministicLinePlot.tsx
+++ /dev/null
@@ -1,334 +0,0 @@
-import React, { useState } from 'react'
-
-import _ from 'lodash'
-import { connect } from 'react-redux'
-
-import ReactResizeDetector from 'react-resize-detector'
-import {
- CartesianGrid,
- ComposedChart,
- Legend,
- Line,
- Scatter,
- Area,
- Tooltip,
- TooltipProps,
- XAxis,
- YAxis,
- YAxisProps,
- LegendPayload,
-} from 'recharts'
-
-import { useTranslation } from 'react-i18next'
-
-import type { ScenarioDatum, CaseCountsDatum } from '../../../algorithms/types/Param.types'
-
-import type { AlgorithmResult } from '../../../algorithms/types/Result.types'
-
-import { numberFormatter } from '../../../helpers/numberFormat'
-import { selectResult } from '../../../state/algorithm/algorithm.selectors'
-import { State } from '../../../state/reducer'
-import { selectScenarioData, selectCaseCountsData } from '../../../state/scenario/scenario.selectors'
-import { selectIsLogScale, selectShouldFormatNumbers } from '../../../state/settings/settings.selectors'
-
-import { calculatePosition, scrollToRef } from './chartHelper'
-import {
- linesToPlot,
- areasToPlot,
- observationsToPlot,
- DATA_POINTS,
- translatePlots,
- defaultEnabledPlots,
-} from './ChartCommon'
-import { LinePlotTooltip } from './LinePlotTooltip'
-import { MitigationPlot } from './MitigationLinePlot'
-import { R0Plot } from './R0LinePlot'
-
-import { verifyPositive, computeNewEmpiricalCases } from './Utils'
-
-import './DeterministicLinePlot.scss'
-
-const ASPECT_RATIO = 16 / 9
-
-function xTickFormatter(tick: string | number): string {
- return new Date(tick).toISOString().slice(0, 10)
-}
-
-function labelFormatter(value: string | number): React.ReactNode {
- return xTickFormatter(value)
-}
-
-function legendFormatter(enabledPlots: string[], value?: LegendPayload['value'], entry?: LegendPayload) {
- let activeClassName = 'legend-inactive'
- if (entry?.dataKey && enabledPlots.includes(entry.dataKey)) {
- activeClassName = 'legend'
- }
-
- return {value}
-}
-
-export interface DeterministicLinePlotProps {
- scenarioData: ScenarioDatum
- result?: AlgorithmResult
- caseCountsData?: CaseCountsDatum[]
- isLogScale: boolean
- shouldFormatNumbers: boolean
-}
-
-const mapStateToProps = (state: State) => ({
- scenarioData: selectScenarioData(state),
- result: selectResult(state),
- caseCountsData: selectCaseCountsData(state),
- isLogScale: selectIsLogScale(state),
- shouldFormatNumbers: selectShouldFormatNumbers(state),
-})
-
-const mapDispatchToProps = {}
-
-// eslint-disable-next-line sonarjs/cognitive-complexity
-export function DeterministicLinePlotDiconnected({
- scenarioData,
- result,
- caseCountsData,
- isLogScale,
- shouldFormatNumbers,
-}: DeterministicLinePlotProps) {
- const { t } = useTranslation()
- const chartRef = React.useRef(null)
- const [enabledPlots, setEnabledPlots] = useState(defaultEnabledPlots)
-
- const formatNumber = numberFormatter(!!shouldFormatNumbers, false)
- const formatNumberRounded = numberFormatter(!!shouldFormatNumbers, true)
-
- if (!result) {
- return null
- }
-
- const { mitigationIntervals } = scenarioData.mitigation
-
- const nHospitalBeds = verifyPositive(scenarioData.population.hospitalBeds)
- const nICUBeds = verifyPositive(scenarioData.population.icuBeds)
-
- // NOTE: this used to use scenarioData.epidemiological.infectiousPeriodDays as
- // time interval but a weekly interval makes more sense given reporting practices
- const [newEmpiricalCases] = computeNewEmpiricalCases(7, 'cases', caseCountsData)
-
- const [weeklyEmpiricalDeaths] = computeNewEmpiricalCases(7, 'deaths', caseCountsData)
-
- const hasObservations = {
- [DATA_POINTS.ObservedCases]: caseCountsData && caseCountsData.some((d) => d.cases),
- [DATA_POINTS.ObservedICU]: caseCountsData && caseCountsData.some((d) => d.icu),
- [DATA_POINTS.ObservedDeaths]: caseCountsData && caseCountsData.some((d) => d.deaths),
- [DATA_POINTS.ObservedWeeklyDeaths]: caseCountsData && caseCountsData.some((d) => d.deaths),
- [DATA_POINTS.ObservedNewCases]: newEmpiricalCases && newEmpiricalCases.some((d) => d),
- [DATA_POINTS.ObservedHospitalized]: caseCountsData && caseCountsData.some((d) => d.hospitalized),
- }
-
- const observations =
- caseCountsData?.map((d, i) => ({
- time: new Date(d.time).getTime(),
- cases: enabledPlots.includes(DATA_POINTS.ObservedCases) ? d.cases || undefined : undefined,
- observedDeaths: enabledPlots.includes(DATA_POINTS.ObservedDeaths) ? d.deaths || undefined : undefined,
- currentHospitalized: enabledPlots.includes(DATA_POINTS.ObservedHospitalized)
- ? d.hospitalized || undefined
- : undefined,
- ICU: enabledPlots.includes(DATA_POINTS.ObservedICU) ? d.icu || undefined : undefined,
- newCases: enabledPlots.includes(DATA_POINTS.ObservedNewCases) ? newEmpiricalCases[i] : undefined,
- weeklyDeaths: enabledPlots.includes(DATA_POINTS.ObservedWeeklyDeaths) ? weeklyEmpiricalDeaths[i] : undefined,
- hospitalBeds: nHospitalBeds,
- ICUbeds: nICUBeds,
- })) ?? []
-
- const plotData = [
- ...result.plotData.map((x) => {
- const dpoint = { time: x.time, hospitalBeds: nHospitalBeds, ICUbeds: nICUBeds }
- Object.keys(x.lines).forEach((d) => {
- dpoint[d] = enabledPlots.includes(d) ? x.lines[d] : undefined
- })
- Object.keys(x.areas).forEach((d) => {
- dpoint[`${d}Area`] = enabledPlots.includes(d) ? x.areas[d] : undefined
- })
- return dpoint
- }),
- ...observations,
- ]
-
- if (plotData.length === 0) {
- return null
- }
-
- plotData.sort((a, b) => (a.time > b.time ? 1 : -1))
- const consolidatedPlotData = [plotData[0]]
- const msPerDay = 24 * 60 * 60 * 1000
- plotData.forEach((d) => {
- if (d.time - msPerDay < consolidatedPlotData[consolidatedPlotData.length - 1].time) {
- consolidatedPlotData[consolidatedPlotData.length - 1] = {
- ...d,
- ...consolidatedPlotData[consolidatedPlotData.length - 1],
- }
- } else {
- consolidatedPlotData.push(d)
- }
- })
-
- // determine the max of enabled plots w/o the hospital capacity
- const dataKeys = enabledPlots.filter((d) => d !== DATA_POINTS.HospitalBeds && d !== DATA_POINTS.ICUbeds)
- // @ts-ignore
- const yDataMax = _.max(consolidatedPlotData.map((d) => _.max(dataKeys.map((k) => d[k]))))
-
- const tMin = _.minBy(plotData, 'time')!.time // eslint-disable-line @typescript-eslint/no-non-null-assertion
- const tMax = _.maxBy(plotData, 'time')!.time // eslint-disable-line @typescript-eslint/no-non-null-assertion
-
- const observationsHavingDataToPlot = observationsToPlot().filter((itemToPlot) => {
- if (observations.length !== 0) {
- return hasObservations[itemToPlot.key]
- }
- return false
- })
-
- let tooltipItems: { [key: string]: number | undefined } = {}
- consolidatedPlotData.forEach((d) => {
- // @ts-ignore
- tooltipItems = { ...tooltipItems, ...d }
- })
- const tooltipItemsToDisplay = Object.keys(tooltipItems).filter(
- (itemKey: string) => itemKey !== 'time' && itemKey !== 'hospitalBeds' && itemKey !== 'ICUbeds',
- )
-
- const logScaleString: YAxisProps['scale'] = isLogScale ? 'log' : 'linear'
-
- const tooltipValueFormatter = (value: number | string) =>
- typeof value === 'number' ? formatNumber(Number(value)) : value
-
- const yTickFormatter = (value: number) => formatNumberRounded(value)
-
- return (
-
-
- {({ width }: { width?: number }) => {
- if (!width) {
- return
- }
-
- const height = Math.max(500, width / ASPECT_RATIO)
- const tooltipPosition = calculatePosition(height)
-
- return (
- <>
-
-
-
- scrollToRef(chartRef)}
- width={width}
- height={height}
- data={consolidatedPlotData}
- throttleDelay={75}
- margin={{
- left: 5,
- right: 5,
- bottom: 5,
- }}
- >
-
-
-
-
-
-
- (
-
- )}
- />
-
-
- >
- )
- }}
-
-
- )
-}
-
-const DeterministicLinePlot = connect(mapStateToProps, mapDispatchToProps)(DeterministicLinePlotDiconnected)
-
-export { DeterministicLinePlot }
diff --git a/src/components/Main/Results/LinePlotTooltip.tsx b/src/components/Main/Results/LinePlotTooltip.tsx
index b8df3d36d..1283bdf30 100644
--- a/src/components/Main/Results/LinePlotTooltip.tsx
+++ b/src/components/Main/Results/LinePlotTooltip.tsx
@@ -2,7 +2,7 @@ import React from 'react'
import { TooltipProps } from 'recharts'
import { useTranslation } from 'react-i18next'
-import { linesToPlot, observationsToPlot, translatePlots } from './ChartCommon'
+import { linesMetaDefault, casesMetaDefault, translatePlots } from './ChartCommon'
import { ResponsiveTooltipContent, TooltipItem } from './ResponsiveTooltipContent'
import './ResponsiveTooltipContent.scss'
@@ -44,13 +44,13 @@ export function LinePlotTooltip({
const tooltipItems = []
.concat(
- translatePlots(t, observationsToPlot()).map((observationToPlot) => ({
+ translatePlots(t, casesMetaDefault()).map((observationToPlot) => ({
...observationToPlot,
displayUndefinedAs: '-',
})) as never,
)
.concat(
- translatePlots(t, linesToPlot).map((lineToPlot) => ({
+ translatePlots(t, linesMetaDefault).map((lineToPlot) => ({
...lineToPlot,
displayUndefinedAs: 0,
})) as never,
diff --git a/src/components/Main/Results/OutcomeRatesTable.tsx b/src/components/Main/Results/OutcomeRatesTable.tsx
index c09d5418a..84b147f6b 100644
--- a/src/components/Main/Results/OutcomeRatesTable.tsx
+++ b/src/components/Main/Results/OutcomeRatesTable.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -12,7 +12,7 @@ import type { State } from '../../../state/reducer'
import { selectResult } from '../../../state/algorithm/algorithm.selectors'
import { selectShouldFormatNumbers } from '../../../state/settings/settings.selectors'
-import { numberFormatter } from '../../../helpers/numberFormat'
+import { getNumberFormatters } from '../../../helpers/numberFormat'
interface RowProps {
entry: number[]
@@ -53,13 +53,12 @@ export const OutcomeRatesTable = connect(mapStateToProps, mapDispatchToProps)(Ou
export function OutcomeRatesTableDisconnected({ result, shouldFormatNumbers, forPrint }: TableProps) {
const { t } = useTranslation()
+ const { formatNumber } = useMemo(() => getNumberFormatters({ shouldFormatNumbers }), [shouldFormatNumbers]) // prettier-ignore
if (!result) {
return null
}
- const formatNumber = numberFormatter(shouldFormatNumbers, false)
-
const endResult = {
lower: result.trajectory.lower[result.trajectory.middle.length - 1],
value: result.trajectory.middle[result.trajectory.middle.length - 1],
diff --git a/src/components/Main/Results/OutcomesDetailsTable.tsx b/src/components/Main/Results/OutcomesDetailsTable.tsx
index 6edfcb9fa..4d8413923 100644
--- a/src/components/Main/Results/OutcomesDetailsTable.tsx
+++ b/src/components/Main/Results/OutcomesDetailsTable.tsx
@@ -13,16 +13,16 @@ import type { AlgorithmResult } from '../../../algorithms/types/Result.types'
import { State } from '../../../state/reducer'
import { selectResult } from '../../../state/algorithm/algorithm.selectors'
-import { numberFormatter } from '../../../helpers/numberFormat'
+import { getNumberFormatters } from '../../../helpers/numberFormat'
import './OutcomesDetailsTable.scss'
const STEP = 7
-const formatter = numberFormatter(true, true)
+const { formatNumber } = getNumberFormatters({ shouldFormatNumbers: true })
function numberFormat(x?: number): string {
- return formatter(x ?? 0)
+ return formatNumber(x ?? 0)
}
function dateFormat(time: number) {
diff --git a/src/components/Main/Results/ResultsCard.tsx b/src/components/Main/Results/ResultsCard.tsx
index 9aa00b024..1043d23e1 100644
--- a/src/components/Main/Results/ResultsCard.tsx
+++ b/src/components/Main/Results/ResultsCard.tsx
@@ -18,7 +18,7 @@ import { CollapsibleCard } from '../../Form/CollapsibleCard'
import { CardWithControls } from '../../Form/CardWithControls'
import { AgeBarChart } from './AgeBarChart'
-import { DeterministicLinePlot } from './DeterministicLinePlot'
+import { ResultsTrajectoriesPlot } from '../ResultsTrajectoriesPlot/ResultsTrajectoriesPlot'
import { OutcomeRatesTable } from './OutcomeRatesTable'
import { OutcomesDetailsTable } from './OutcomesDetailsTable'
import { SimulationControls } from '../Controls/SimulationControls'
@@ -89,7 +89,7 @@ function ResultsCardDisconnected({ canRun, hasResult, areResultsMaximized, toggl
>
-
+
diff --git a/src/components/Main/Results/Utils.ts b/src/components/Main/Results/Utils.ts
index c9d91c70a..77b709fe2 100644
--- a/src/components/Main/Results/Utils.ts
+++ b/src/components/Main/Results/Utils.ts
@@ -1,36 +1,15 @@
import type { CaseCountsDatum } from '../../../algorithms/types/Param.types'
-export type maybeNumber = number | undefined
+import { verifyPositive } from '../../../algorithms/preparePlotData'
-export function verifyPositive(x: number): maybeNumber {
- const xRounded = Math.round(x)
- return xRounded > 0 ? xRounded : undefined
-}
-
-export function verifyTuple(x: [maybeNumber, maybeNumber], center: maybeNumber): [number, number] | undefined {
- const centerVal = center ? verifyPositive(center) : undefined
- if (x[0] !== undefined && x[1] !== undefined && centerVal !== undefined) {
- return [x[0] < centerVal ? x[0] : centerVal, x[1] > centerVal ? x[1] : centerVal]
- }
- if (x[0] !== undefined && x[1] !== undefined) {
- return [x[0], x[1]]
- }
- if (x[0] === undefined && x[1] !== undefined && centerVal !== undefined) {
- return [0.0001, x[1] > centerVal ? x[1] : centerVal]
- }
- if (x[0] === undefined && x[1] !== undefined) {
- return [0.0001, x[1]]
- }
-
- return undefined
-}
+export type MaybeNumber = number | undefined
export function computeNewEmpiricalCases(
timeWindow: number,
field: string,
cumulativeCounts?: CaseCountsDatum[],
-): [maybeNumber[], number] {
- const newEmpiricalCases: maybeNumber[] = []
+): [MaybeNumber[], number] {
+ const newEmpiricalCases: MaybeNumber[] = []
const deltaDay = Math.floor(timeWindow)
const deltaInt = timeWindow - deltaDay
diff --git a/src/components/Main/Results/DeterministicLinePlot.scss b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.scss
similarity index 100%
rename from src/components/Main/Results/DeterministicLinePlot.scss
rename to src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.scss
diff --git a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx
new file mode 100644
index 000000000..c87f36ba1
--- /dev/null
+++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx
@@ -0,0 +1,397 @@
+import React, { useCallback, useMemo, useState } from 'react'
+
+import { maxBy, minBy, zipWith, pick } from 'lodash'
+import { connect } from 'react-redux'
+import ReactResizeDetector from 'react-resize-detector'
+import {
+ CartesianGrid,
+ ComposedChart,
+ Legend,
+ Line,
+ Scatter,
+ Area,
+ Tooltip,
+ TooltipProps,
+ XAxis,
+ YAxis,
+ YAxisProps,
+ LegendPayload,
+} from 'recharts'
+
+import { useTranslation } from 'react-i18next'
+
+import type { CaseCountsDatum, MitigationInterval } from '../../../algorithms/types/Param.types'
+import { PlotDatum, PlotData } from '../../../algorithms/types/Result.types'
+
+import type { AlgorithmResult } from '../../../algorithms/types/Result.types'
+import { CASE_COUNTS_INTERVAL_DAYS } from '../../../constants'
+
+import { getNumberFormatters } from '../../../helpers/numberFormat'
+import { selectResult } from '../../../state/algorithm/algorithm.selectors'
+import { State } from '../../../state/reducer'
+import {
+ selectCaseCountsData,
+ selectHospitalBeds,
+ selectIcuBeds,
+ selectMitigationIntervals,
+} from '../../../state/scenario/scenario.selectors'
+import { selectIsLogScale, selectShouldFormatNumbers } from '../../../state/settings/settings.selectors'
+
+import { calculatePosition, scrollToRef } from '../Results/chartHelper'
+import {
+ linesMetaDefault,
+ casesMetaDefault,
+ translatePlots,
+ constantsMetaDefault,
+ LineProps,
+} from '../Results/ChartCommon'
+import { LinePlotTooltip } from '../Results/LinePlotTooltip'
+import { MitigationPlot } from '../Results/MitigationLinePlot'
+import { R0Plot } from '../Results/R0LinePlot'
+
+import { computeNewEmpiricalCases } from '../Results/Utils'
+
+import './ResultsTrajectoriesPlot.scss'
+import { soa } from 'src/algorithms/utils/soa'
+
+const ASPECT_RATIO = 16 / 9
+
+export interface ResultsTrajectoriesPlotProps {
+ hospitalBeds?: number
+ icuBeds?: number
+ mitigationIntervals: MitigationInterval[]
+ result?: AlgorithmResult
+ caseCountsData?: CaseCountsDatum[]
+ isLogScale: boolean
+ shouldFormatNumbers: boolean
+}
+
+const mapStateToProps = (state: State) => ({
+ hospitalBeds: selectHospitalBeds(state),
+ icuBeds: selectIcuBeds(state),
+ mitigationIntervals: selectMitigationIntervals(state),
+ result: selectResult(state),
+ caseCountsData: selectCaseCountsData(state),
+ isLogScale: selectIsLogScale(state),
+ shouldFormatNumbers: selectShouldFormatNumbers(state),
+})
+
+const mapDispatchToProps = {}
+
+export interface GetPlotDataParams {
+ plotData: PlotData
+ linesMeta: LineProps[]
+}
+
+export function getPlotData({ plotData, linesMeta }: GetPlotDataParams) {
+ let { linesObject, areasObject } = plotData
+
+ const areasMeta: LineProps[] = linesMeta.map((line) => {
+ const { dataKey, name } = line
+ return { ...line, dataKey: `${dataKey}Area`, name: `${name} uncertainty`, legendType: 'none' }
+ })
+
+ linesObject = pick(linesObject, Object.keys(linesMeta))
+ areasObject = pick(areasObject, Object.keys(areasMeta))
+
+ return { linesObject, areasObject }
+
+ // TODO: What is it doing?
+ // const consolidatedPlotData = [plotData[0]]
+ // const msPerDay = 24 * 60 * 60 * 1000
+ // const last = consolidatedPlotData[consolidatedPlotData.length - 1]
+ // plotData.forEach((d) => {
+ // if (d.time - msPerDay < last.time) {
+ // last = { ...d, ...last }
+ // } else {
+ // consolidatedPlotData.push(d)
+ // }
+ // })
+}
+
+function getObservations({ caseCountsData, points }) {
+ // // NOTE: this used to use scenarioData.epidemiological.infectiousPeriodDays as
+ // // time interval but a weekly interval makes more sense given reporting practices
+ // const [newEmpiricalCases] = computeNewEmpiricalCases(CASE_COUNTS_INTERVAL_DAYS, 'cases', caseCountsData)
+ // const [weeklyEmpiricalDeaths] = computeNewEmpiricalCases(CASE_COUNTS_INTERVAL_DAYS, 'deaths', caseCountsData)
+ //
+ // const hasObservations = {
+ // observedCases: caseCountsData && caseCountsData.some((d) => d.cases),
+ // observedICU: caseCountsData && caseCountsData.some((d) => d.icu),
+ // observedDeaths: caseCountsData && caseCountsData.some((d) => d.deaths),
+ // observedWeeklyDeaths: caseCountsData && caseCountsData.some((d) => d.deaths),
+ // observedNewCases: newEmpiricalCases && newEmpiricalCases.some((d) => d),
+ // observedHospitalized: caseCountsData && caseCountsData.some((d) => d.hospitalized),
+ // }
+ //
+ // const cases = caseCountsData?.filter((caseCount) => {})
+ //
+ // const observations =
+ // caseCountsData?.map((d, i) => ({
+ // time: new Date(d.time).getTime(),
+ // cases: enabledPlots.includes(DATA_POINTS.ObservedCases) ? d.cases || undefined : undefined,
+ // observedDeaths: enabledPlots.includes(DATA_POINTS.ObservedDeaths) ? d.deaths || undefined : undefined,
+ // currentHospitalized: enabledPlots.includes(DATA_POINTS.ObservedHospitalized)
+ // ? d.hospitalized || undefined
+ // : undefined,
+ // icu: enabledPlots.includes(DATA_POINTS.ObservedICU) ? d.icu || undefined : undefined,
+ // newCases: enabledPlots.includes(DATA_POINTS.ObservedNewCases) ? newEmpiricalCases[i] : undefined,
+ // weeklyDeaths: enabledPlots.includes(DATA_POINTS.ObservedWeeklyDeaths) ? weeklyEmpiricalDeaths[i] : undefined,
+ // hospitalBeds,
+ // icuBeds,
+ // })) ?? []
+ //
+ // const observationsHavingDataToPlot = casesMetaDefault().filter((itemToPlot) => {
+ // if (observations.length !== 0) {
+ // return hasObservations[itemToPlot.key]
+ // }
+ // return false
+ // })
+ return []
+}
+
+export interface GetDomainParams {
+ data: PlotData
+ isLogScale: boolean
+}
+
+export interface GetDomainsResult {
+ xDomain: { tMin: number; tMax: number }
+ yDomain: [number, number]
+}
+
+export function getDomain({ data, isLogScale }: GetDomainParams): GetDomainsResult {
+ const tMin = minBy(data, 'time')!.time
+ const tMax = maxBy(data, 'time')!.time
+ const xDomain = { tMin, tMax }
+
+ const yMin = isLogScale ? 1 : 0
+ const yMax = maxBy(data)
+ const yDomain = [yMin, yMax * 1.1]
+
+ return { xDomain, yDomain }
+}
+
+export function getTooltipItems({ consolidatedPlotData }) {
+ // let tooltipItems: { [key: string]: number | undefined } = {}
+ // consolidatedPlotData.forEach((d) => {
+ // // @ts-ignore
+ // tooltipItems = { ...tooltipItems, ...d }
+ // })
+ //
+ // const tooltipItemsToDisplay = Object.keys(tooltipItems).filter(
+ // (itemKey: string) => itemKey !== 'time' && itemKey !== 'hospitalBeds' && itemKey !== 'icuBeds',
+ // )
+ //
+ // return tooltipItemsToDisplay
+ // }
+ //
+ // /** Toggles `enabled` propery of the meta entry corresponding to a given key (if such entry exists) */
+ // function maybeToggleMeta(meta: LineProps[], dataKey: string) {
+ // const entry = meta.find((entry) => entry.dataKey === dataKey)
+ // if (entry) {
+ // return {
+ // ...meta,
+ // [dataKey]: { ...entry, enabled: !entry.enabled },
+ // }
+ // }
+ // return meta
+ return []
+}
+
+export function ResultsTrajectoriesPlotDiconnected({
+ hospitalBeds,
+ icuBeds,
+ mitigationIntervals,
+ result,
+ caseCountsData,
+ isLogScale,
+ shouldFormatNumbers,
+}: ResultsTrajectoriesPlotProps) {
+ const { t } = useTranslation()
+ const chartRef = React.useRef(null)
+ const { formatNumber, formatNumberRounded } = useMemo(() => getNumberFormatters({ shouldFormatNumbers }), [shouldFormatNumbers]) // prettier-ignore
+ const [linesMeta, setLinesMeta] = useState(linesMetaDefault)
+ const [pointsMeta, setPointsMeta] = useState(casesMetaDefault)
+ const [constantsMeta] = useState(constantsMetaDefault)
+ const handleLegendClick = useCallback(({ dataKey }) => {
+ setLinesMeta((linesMeta) => maybeToggleMeta(linesMeta, dataKey))
+ setPointsMeta((pointsMeta) => maybeToggleMeta(pointsMeta, dataKey))
+ }, [])
+
+ if (!result) {
+ return null
+ }
+
+ const { plotData } = result
+
+ const { linesObject, areasObject } = getPlotData({ plotData, linesMeta })
+ // const points = getObservations({ caseCountsData, pointsMeta })
+ // const constants = getConstants({ hospitalBeds, icuBeds })
+
+ const data = aos({ ...linesObject, ...areasObject })
+ let meta = [...linesMeta, ...areasMeta, ...pointsMeta, ...constantsMeta]
+
+ // const data = [...lines, ...areas, ...points, ...constants]
+
+ if (meta.length === 0) {
+ return null
+ }
+
+ const { xDomain: { tMin, tMax }, yDomain } = getDomain({ data, meta, isLogScale }) // prettier-ignore
+
+ const tooltipItemsToDisplay = getTooltipItems({ consolidatedPlotData })
+
+ const logScaleString: YAxisProps['scale'] = isLogScale ? 'log' : 'linear'
+
+ const xTickFormatter = (tick: string | number) => new Date(tick).toISOString().slice(0, 10)
+
+ const yTickFormatter = (value: number) => formatNumberRounded(value)
+
+ const legendFormatter = (value?: LegendPayload['value'], entry?: LegendPayload) => {
+ let activeClassName = 'legend-inactive'
+ const enabled = entry.enabled
+ console.log({ enabled })
+ if (entry?.dataKey && enabledPlots.includes(entry.dataKey)) {
+ activeClassName = 'legend'
+ }
+ return {value}
+ }
+
+ const tooltipValueFormatter = (value: number | string) =>
+ typeof value === 'number' ? formatNumber(Number(value)) : value
+
+ return (
+
+
+ {({ width }: { width?: number }) => {
+ if (!width) {
+ return
+ }
+
+ const height = Math.max(500, width / ASPECT_RATIO)
+ const tooltipPosition = calculatePosition(height)
+
+ return (
+ <>
+
+
+
+ scrollToRef(chartRef)}
+ width={width}
+ height={height}
+ data={data}
+ throttleDelay={75}
+ margin={{
+ left: 5,
+ right: 5,
+ bottom: 5,
+ }}
+ >
+
+
+
+
+ (
+
+ )}
+ />
+
+
+
+ {translatePlots(t, points).map(({ dataKey, color, name, legendType }) => (
+
+ ))}
+
+ {translatePlots(t, lines).map(({ dataKey, color, name, legendType }) => (
+
+ ))}
+
+ {translatePlots(t, areas).map(({ dataKey, color, name, legendType }) => (
+
+ ))}
+
+ {translatePlots(t, constantsMetaDefault).map(({ dataKey, color, name, legendType }) => (
+
+ ))}
+
+
+
+ >
+ )
+ }}
+
+
+ )
+}
+
+const ResultsTrajectoriesPlot = connect(mapStateToProps, mapDispatchToProps)(ResultsTrajectoriesPlotDiconnected)
+
+export { ResultsTrajectoriesPlot }
diff --git a/src/constants.ts b/src/constants.ts
index a4d360529..173f6a962 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -2,3 +2,5 @@ export const DEFAULT_SCENARIO_NAME = 'United States of America' as const
export const DEFAULT_SEVERITY_DISTRIBUTION = 'China CDC' as const
export const CUSTOM_COUNTRY_NAME = 'Custom' as const
export const NONE_COUNTRY_NAME = 'None' as const
+
+export const CASE_COUNTS_INTERVAL_DAYS = 7
diff --git a/src/helpers/numberFormat.ts b/src/helpers/numberFormat.ts
index bf88cd908..5b3f98122 100644
--- a/src/helpers/numberFormat.ts
+++ b/src/helpers/numberFormat.ts
@@ -1,11 +1,22 @@
import { numbro } from '../i18n/i18n'
-export function numberFormatter(humanize: boolean, round: boolean) {
+export interface NumberFormatter {
+ shouldFormatNumbers?: boolean
+ round?: boolean
+}
+
+export function getNumberFormatter({ shouldFormatNumbers = true, round = false }: NumberFormatter) {
return (value: number) =>
numbro(value).format({
thousandSeparated: true,
- average: humanize,
+ average: shouldFormatNumbers,
trimMantissa: true,
mantissa: round ? 0 : 2,
})
}
+
+export function getNumberFormatters({ shouldFormatNumbers = true }: NumberFormatter) {
+ const formatNumber = getNumberFormatter({ shouldFormatNumbers, round: false })
+ const formatNumberRounded = getNumberFormatter({ shouldFormatNumbers, round: true })
+ return { formatNumber, formatNumberRounded }
+}
diff --git a/src/state/scenario/scenario.selectors.ts b/src/state/scenario/scenario.selectors.ts
index d681baba4..ffc282088 100644
--- a/src/state/scenario/scenario.selectors.ts
+++ b/src/state/scenario/scenario.selectors.ts
@@ -6,6 +6,9 @@ import type {
SeverityDistributionDatum,
ScenarioParameters,
} from '../../algorithms/types/Param.types'
+
+import { verifyPositive } from '../../algorithms/preparePlotData'
+
import type { State } from '../reducer'
export const selectRunParams = (state: State): RunParams => {
@@ -48,4 +51,10 @@ export const selectScenarioParameters = ({
export const selectMitigationIntervals = (state: State): MitigationInterval[] =>
state.scenario.scenarioData.data.mitigation.mitigationIntervals
+export const selectHospitalBeds = (state: State): number | undefined =>
+ verifyPositive(state.scenario.scenarioData.data.population.hospitalBeds)
+
+export const selectIcuBeds = (state: State): number | undefined =>
+ verifyPositive(state.scenario.scenarioData.data.population.icuBeds)
+
export const selectCanRun = (state: State): boolean => state.scenario.canRun && !state.algorithm.isRunning