From 0c0d8497a774292e898ea49a8dbf18737380d083 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Wed, 10 Jun 2020 19:29:36 +0200 Subject: [PATCH 01/27] refactor: move and rename DeterministicLinePlot to ResultsTrajectoriesPlot --- config/webpack/webpack.client.babel.ts | 2 +- cypress/integration/results.spec.ts | 2 +- .../Main/PrintPreview/PrintPreview.tsx | 4 +-- src/components/Main/Results/ResultsCard.tsx | 4 +-- .../ResultsTrajectoriesPlot.scss} | 0 .../ResultsTrajectoriesPlot.tsx} | 26 +++++++++---------- 6 files changed, 19 insertions(+), 19 deletions(-) rename src/components/Main/{Results/DeterministicLinePlot.scss => ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.scss} (100%) rename src/components/Main/{Results/DeterministicLinePlot.tsx => ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx} (93%) diff --git a/config/webpack/webpack.client.babel.ts b/config/webpack/webpack.client.babel.ts index 062364a29..1a3a576a1 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/Results/ResultsTrajectoriesPlot.tsx', // FIXME // end '!src/**/*.(spec|test).{js,jsx,ts,tsx}', 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/components/Main/PrintPreview/PrintPreview.tsx b/src/components/Main/PrintPreview/PrintPreview.tsx index e3c324047..9db5bf5c0 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' @@ -288,7 +288,7 @@ export function PrintPreviewDisconnected({

{t('Results')}

- +
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/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/Results/DeterministicLinePlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx similarity index 93% rename from src/components/Main/Results/DeterministicLinePlot.tsx rename to src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx index 7143886ed..60c497dfd 100644 --- a/src/components/Main/Results/DeterministicLinePlot.tsx +++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx @@ -32,7 +32,7 @@ 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 { calculatePosition, scrollToRef } from '../Results/chartHelper' import { linesToPlot, areasToPlot, @@ -40,14 +40,14 @@ import { DATA_POINTS, translatePlots, defaultEnabledPlots, -} from './ChartCommon' -import { LinePlotTooltip } from './LinePlotTooltip' -import { MitigationPlot } from './MitigationLinePlot' -import { R0Plot } from './R0LinePlot' +} from '../Results/ChartCommon' +import { LinePlotTooltip } from '../Results/LinePlotTooltip' +import { MitigationPlot } from '../Results/MitigationLinePlot' +import { R0Plot } from '../Results/R0LinePlot' -import { verifyPositive, computeNewEmpiricalCases } from './Utils' +import { verifyPositive, computeNewEmpiricalCases } from '../Results/Utils' -import './DeterministicLinePlot.scss' +import './ResultsTrajectoriesPlot.scss' const ASPECT_RATIO = 16 / 9 @@ -68,7 +68,7 @@ function legendFormatter(enabledPlots: string[], value?: LegendPayload['value'], return {value} } -export interface DeterministicLinePlotProps { +export interface ResultsTrajectoriesPlotProps { scenarioData: ScenarioDatum result?: AlgorithmResult caseCountsData?: CaseCountsDatum[] @@ -87,13 +87,13 @@ const mapStateToProps = (state: State) => ({ const mapDispatchToProps = {} // eslint-disable-next-line sonarjs/cognitive-complexity -export function DeterministicLinePlotDiconnected({ +export function ResultsTrajectoriesPlotDiconnected({ scenarioData, result, caseCountsData, isLogScale, shouldFormatNumbers, -}: DeterministicLinePlotProps) { +}: ResultsTrajectoriesPlotProps) { const { t } = useTranslation() const chartRef = React.useRef(null) const [enabledPlots, setEnabledPlots] = useState(defaultEnabledPlots) @@ -204,7 +204,7 @@ export function DeterministicLinePlotDiconnected({ const yTickFormatter = (value: number) => formatNumberRounded(value) return ( -
+
{({ width }: { width?: number }) => { if (!width) { @@ -330,6 +330,6 @@ export function DeterministicLinePlotDiconnected({ ) } -const DeterministicLinePlot = connect(mapStateToProps, mapDispatchToProps)(DeterministicLinePlotDiconnected) +const ResultsTrajectoriesPlot = connect(mapStateToProps, mapDispatchToProps)(ResultsTrajectoriesPlotDiconnected) -export { DeterministicLinePlot } +export { ResultsTrajectoriesPlot } From 477d689582c54075cd23305929fb4caf4a1a0905 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Wed, 10 Jun 2020 21:58:01 +0200 Subject: [PATCH 02/27] refactor: temporarily move formatter functions into component body --- .../ResultsTrajectoriesPlot.tsx | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx index 60c497dfd..645c8c52f 100644 --- a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx +++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx @@ -51,23 +51,6 @@ import './ResultsTrajectoriesPlot.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 ResultsTrajectoriesPlotProps { scenarioData: ScenarioDatum result?: AlgorithmResult @@ -192,17 +175,29 @@ export function ResultsTrajectoriesPlotDiconnected({ // @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 xTickFormatter = (tick: string | number) => new Date(tick).toISOString().slice(0, 10) const yTickFormatter = (value: number) => formatNumberRounded(value) + const legendFormatter = (enabledPlots: string[]) => (value?: LegendPayload['value'], entry?: LegendPayload) => { + let activeClassName = 'legend-inactive' + if (entry?.dataKey && enabledPlots.includes(entry.dataKey)) { + activeClassName = 'legend' + } + + return {value} + } + + const tooltipValueFormatter = (value: number | string) => + typeof value === 'number' ? formatNumber(Number(value)) : value + return (
@@ -223,7 +218,7 @@ export function ResultsTrajectoriesPlotDiconnected({ height={height / 4} tMin={tMin} tMax={tMax} - labelFormatter={labelFormatter} + labelFormatter={xTickFormatter} tooltipValueFormatter={tooltipValueFormatter} tooltipPosition={tooltipPosition} /> @@ -266,7 +261,7 @@ export function ResultsTrajectoriesPlotDiconnected({ /> ( - legendFormatter(enabledPlots, value, entry) - } + formatter={legendFormatter(enabledPlots)} onClick={(e) => { const plots = enabledPlots.slice(0) enabledPlots.includes(e.dataKey) ? plots.splice(plots.indexOf(e.dataKey), 1) : plots.push(e.dataKey) From d94ae0667ca06534ce5a95d370be2716e004dcbc Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 00:58:38 +0200 Subject: [PATCH 03/27] refactor: reduce duplication, improve readability --- src/algorithms/preparePlotData.ts | 37 ++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 857fe3934..170df1af9 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -1,32 +1,47 @@ +import { pickBy, mapValues } from 'lodash' + import type { Trajectory, PlotDatum } from './types/Result.types' import { verifyPositive, verifyTuple } from '../components/Main/Results/Utils' // import { linesToPlot, areasToPlot, DATA_POINTS } from '../components/Main/Results/ChartCommon' +export function filterPositiveValues(obj: T) { + return pickBy(obj, (value) => value > 0) as T +} + +export function roundValues(obj: T) { + return mapValues(obj, verifyPositive) as T +} + export function preparePlotData(trajectory: Trajectory): PlotDatum[] { const { lower, middle, upper } = trajectory return middle.map((x, 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 + let lines = { + susceptible: x.current.susceptible.total, + infectious: x.current.infectious.total, + severe: x.current.severe.total, + critical: x.current.critical.total, + overflow: x.current.overflow.total, + recovered: x.cumulative.recovered.total, + fatality: x.cumulative.fatality.total, + weeklyFatality: centerWeeklyDeaths, + } + + lines = filterPositiveValues(lines) + lines = roundValues(lines) + 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 + lines, areas: { susceptible: verifyTuple( [verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total)], From 284a22a6a076d5807b9db0adbc5c3c523b60a06d Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 00:59:08 +0200 Subject: [PATCH 04/27] refactor: reformat duplicated code for clarity --- src/algorithms/preparePlotData.ts | 43 ++++++------------------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 170df1af9..5974c349b 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -43,41 +43,14 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { time: x.time, lines, 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, - ), + susceptible: verifyTuple([verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total)], x.current.susceptible.total), // prettier-ignore + infectious: verifyTuple([verifyPositive(lower[day].current.infectious.total), verifyPositive(upper[day].current.infectious.total)], x.current.infectious.total), // prettier-ignore + severe: verifyTuple([verifyPositive(lower[day].current.severe.total), verifyPositive(upper[day].current.severe.total)], x.current.severe.total), // prettier-ignore + critical: verifyTuple([verifyPositive(lower[day].current.critical.total), verifyPositive(upper[day].current.critical.total)], x.current.critical.total), // prettier-ignore + overflow: verifyTuple([verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total)], x.current.overflow.total), // prettier-ignore + recovered: verifyTuple([verifyPositive(lower[day].cumulative.recovered.total), verifyPositive(upper[day].cumulative.recovered.total)], x.cumulative.recovered.total), // prettier-ignore + fatality: verifyTuple([verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total)], x.cumulative.fatality.total), // prettier-ignore + weeklyFatality: verifyTuple([verifyPositive(lowerWeeklyDeaths), verifyPositive(upperWeeklyDeaths)], centerWeeklyDeaths) // prettier-ignore }, } }) From 6cd3dfaaf71e84356ff7ede934f93be8ef4d929f Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 02:03:41 +0200 Subject: [PATCH 05/27] refactor: clarify the intent by using a sort function --- src/algorithms/preparePlotData.ts | 17 +++--- src/algorithms/utils/__tests__/sort.test.ts | 61 +++++++++++++++++++++ src/algorithms/utils/__tests__/swap.test.ts | 27 +++++++++ src/algorithms/utils/sort.ts | 9 +++ src/algorithms/utils/swap.ts | 4 ++ 5 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 src/algorithms/utils/__tests__/sort.test.ts create mode 100644 src/algorithms/utils/__tests__/swap.test.ts create mode 100644 src/algorithms/utils/sort.ts create mode 100644 src/algorithms/utils/swap.ts diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 5974c349b..80b0d576d 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -2,6 +2,8 @@ import { pickBy, mapValues } from 'lodash' import type { Trajectory, PlotDatum } from './types/Result.types' import { verifyPositive, verifyTuple } from '../components/Main/Results/Utils' + +import { sort } from './utils/sort' // import { linesToPlot, areasToPlot, DATA_POINTS } from '../components/Main/Results/ChartCommon' export function filterPositiveValues(obj: T) { @@ -17,13 +19,10 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { return middle.map((x, 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 + let weeklyFatalityMiddle = x.cumulative.fatality.total - middle[previousDay].cumulative.fatality.total + 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, weeklyFatalityMiddle, weeklyFatalityUpper] = sort(weeklyFatalityLower, weeklyFatalityMiddle, weeklyFatalityUpper) // prettier-ignore let lines = { susceptible: x.current.susceptible.total, @@ -33,7 +32,7 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { overflow: x.current.overflow.total, recovered: x.cumulative.recovered.total, fatality: x.cumulative.fatality.total, - weeklyFatality: centerWeeklyDeaths, + weeklyFatality: weeklyFatalityMiddle, } lines = filterPositiveValues(lines) @@ -50,7 +49,7 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { overflow: verifyTuple([verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total)], x.current.overflow.total), // prettier-ignore recovered: verifyTuple([verifyPositive(lower[day].cumulative.recovered.total), verifyPositive(upper[day].cumulative.recovered.total)], x.cumulative.recovered.total), // prettier-ignore fatality: verifyTuple([verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total)], x.cumulative.fatality.total), // prettier-ignore - weeklyFatality: verifyTuple([verifyPositive(lowerWeeklyDeaths), verifyPositive(upperWeeklyDeaths)], centerWeeklyDeaths) // prettier-ignore + weeklyFatality: verifyTuple([verifyPositive(weeklyFatalityLower), verifyPositive(weeklyFatalityUpper)], weeklyFatalityMiddle) // prettier-ignore }, } }) diff --git a/src/algorithms/utils/__tests__/sort.test.ts b/src/algorithms/utils/__tests__/sort.test.ts new file mode 100644 index 000000000..228a1dc52 --- /dev/null +++ b/src/algorithms/utils/__tests__/sort.test.ts @@ -0,0 +1,61 @@ +import { sort } from '../sort' + +describe('sort', () => { + it('sorts already ordered', async () => { + expect(sort(-5, 3.14, 42)).toStrictEqual([-5, 3.14, 42]) + }) + + it('sorts any unordered numbers', async () => { + expect(sort(3.14, -5, 42)).toStrictEqual([-5, 3.14, 42]) + }) + + it('sorts equals: all', async () => { + expect(sort(3.14, 3.14, 3.14)).toStrictEqual([3.14, 3.14, 3.14]) + }) + + it('sorts equals: all zeros', async () => { + expect(sort(0, 0, 0)).toStrictEqual([0, 0, 0]) + }) + + it('sorts equals: left', async () => { + expect(sort(42, 2, 2)).toStrictEqual([2, 2, 42]) + }) + + it('sorts equals: sides', async () => { + expect(sort(2, 42, 2)).toStrictEqual([2, 2, 42]) + }) + + it('sorts equals: right', async () => { + expect(sort(42, 42, 2)).toStrictEqual([2, 42, 42]) + }) + + it('sorts: 1, 2, 3', async () => { + expect(sort(1, 2, 3)).toStrictEqual([1, 2, 3]) + }) + + it('sorts: 2, 1, 3', async () => { + expect(sort(2, 1, 3)).toStrictEqual([1, 2, 3]) + }) + + it('sorts: 1, 3, 2', async () => { + expect(sort(1, 3, 2)).toStrictEqual([1, 2, 3]) + }) + + it('sorts: 3, 1, 2', async () => { + expect(sort(3, 1, 2)).toStrictEqual([1, 2, 3]) + }) + + it('sorts: 3, 2, 1', async () => { + expect(sort(3, 2, 1)).toStrictEqual([1, 2, 3]) + }) + + it('does not mutate arguments', async () => { + const a = -5 + const b = 3.14 + const c = 42 + const [, ,] = sort(c, a, b) + expect(a).toBe(-5) + expect(b).toBe(3.14) + expect(c).toBe(42) + }) +}) 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/sort.ts b/src/algorithms/utils/sort.ts new file mode 100644 index 000000000..37a50a8e3 --- /dev/null +++ b/src/algorithms/utils/sort.ts @@ -0,0 +1,9 @@ +/* eslint-disable no-param-reassign */ + +/** Puts 3 given numbers in ascending order. Does not mutate arguments. */ +export function sort(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/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] +} From c431dab857dd6e5d53ab1521fb7b4b8a4a1781d3 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 02:08:12 +0200 Subject: [PATCH 06/27] refactor: temporarily replace iterated value with array access for clarity --- src/algorithms/preparePlotData.ts | 40 ++++++++++++++++--------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 80b0d576d..55aa5384e 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -17,21 +17,23 @@ export function roundValues(obj: T) { export function preparePlotData(trajectory: Trajectory): PlotDatum[] { const { lower, middle, upper } = trajectory - return middle.map((x, day) => { + return middle.map((_0, day) => { const previousDay = day > 6 ? day - 7 : 0 - let weeklyFatalityMiddle = x.cumulative.fatality.total - middle[previousDay].cumulative.fatality.total - 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 + + let 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, weeklyFatalityMiddle, weeklyFatalityUpper] = sort(weeklyFatalityLower, weeklyFatalityMiddle, weeklyFatalityUpper) // prettier-ignore let lines = { - susceptible: x.current.susceptible.total, - infectious: x.current.infectious.total, - severe: x.current.severe.total, - critical: x.current.critical.total, - overflow: x.current.overflow.total, - recovered: x.cumulative.recovered.total, - fatality: x.cumulative.fatality.total, + 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, } @@ -39,16 +41,16 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { lines = roundValues(lines) return { - time: x.time, + time: middle[day].time, lines, areas: { - susceptible: verifyTuple([verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total)], x.current.susceptible.total), // prettier-ignore - infectious: verifyTuple([verifyPositive(lower[day].current.infectious.total), verifyPositive(upper[day].current.infectious.total)], x.current.infectious.total), // prettier-ignore - severe: verifyTuple([verifyPositive(lower[day].current.severe.total), verifyPositive(upper[day].current.severe.total)], x.current.severe.total), // prettier-ignore - critical: verifyTuple([verifyPositive(lower[day].current.critical.total), verifyPositive(upper[day].current.critical.total)], x.current.critical.total), // prettier-ignore - overflow: verifyTuple([verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total)], x.current.overflow.total), // prettier-ignore - recovered: verifyTuple([verifyPositive(lower[day].cumulative.recovered.total), verifyPositive(upper[day].cumulative.recovered.total)], x.cumulative.recovered.total), // prettier-ignore - fatality: verifyTuple([verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total)], x.cumulative.fatality.total), // prettier-ignore + susceptible: verifyTuple([verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total)], middle[day].current.susceptible.total), // prettier-ignore + infectious: verifyTuple([verifyPositive(lower[day].current.infectious.total), verifyPositive(upper[day].current.infectious.total)], middle[day].current.infectious.total), // prettier-ignore + severe: verifyTuple([verifyPositive(lower[day].current.severe.total), verifyPositive(upper[day].current.severe.total)], middle[day].current.severe.total), // prettier-ignore + critical: verifyTuple([verifyPositive(lower[day].current.critical.total), verifyPositive(upper[day].current.critical.total)], middle[day].current.critical.total), // prettier-ignore + overflow: verifyTuple([verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total)], middle[day].current.overflow.total), // prettier-ignore + recovered: verifyTuple([verifyPositive(lower[day].cumulative.recovered.total), verifyPositive(upper[day].cumulative.recovered.total)], middle[day].cumulative.recovered.total), // prettier-ignore + fatality: verifyTuple([verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total)], middle[day].cumulative.fatality.total), // prettier-ignore weeklyFatality: verifyTuple([verifyPositive(weeklyFatalityLower), verifyPositive(weeklyFatalityUpper)], weeklyFatalityMiddle) // prettier-ignore }, } From c3845ff934a8573adf93f6fb124f67dd996dce65 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 02:35:12 +0200 Subject: [PATCH 07/27] refactor: select only necessary scenario state --- .../ResultsTrajectoriesPlot.tsx | 34 +++++++++++-------- src/state/scenario/scenario.selectors.ts | 9 +++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx index 645c8c52f..245f4e40f 100644 --- a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx +++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx @@ -22,14 +22,19 @@ import { import { useTranslation } from 'react-i18next' -import type { ScenarioDatum, CaseCountsDatum } from '../../../algorithms/types/Param.types' +import type { CaseCountsDatum, MitigationInterval } 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 { + selectCaseCountsData, + selectHospitalBeds, + selectIcuBeds, + selectMitigationIntervals, +} from '../../../state/scenario/scenario.selectors' import { selectIsLogScale, selectShouldFormatNumbers } from '../../../state/settings/settings.selectors' import { calculatePosition, scrollToRef } from '../Results/chartHelper' @@ -45,14 +50,16 @@ import { LinePlotTooltip } from '../Results/LinePlotTooltip' import { MitigationPlot } from '../Results/MitigationLinePlot' import { R0Plot } from '../Results/R0LinePlot' -import { verifyPositive, computeNewEmpiricalCases } from '../Results/Utils' +import { computeNewEmpiricalCases } from '../Results/Utils' import './ResultsTrajectoriesPlot.scss' const ASPECT_RATIO = 16 / 9 export interface ResultsTrajectoriesPlotProps { - scenarioData: ScenarioDatum + hospitalBeds?: number + icuBeds?: number + mitigationIntervals: MitigationInterval[] result?: AlgorithmResult caseCountsData?: CaseCountsDatum[] isLogScale: boolean @@ -60,7 +67,9 @@ export interface ResultsTrajectoriesPlotProps { } const mapStateToProps = (state: State) => ({ - scenarioData: selectScenarioData(state), + hospitalBeds: selectHospitalBeds(state), + icuBeds: selectIcuBeds(state), + mitigationIntervals: selectMitigationIntervals(state), result: selectResult(state), caseCountsData: selectCaseCountsData(state), isLogScale: selectIsLogScale(state), @@ -71,7 +80,9 @@ const mapDispatchToProps = {} // eslint-disable-next-line sonarjs/cognitive-complexity export function ResultsTrajectoriesPlotDiconnected({ - scenarioData, + hospitalBeds, + icuBeds, + mitigationIntervals, result, caseCountsData, isLogScale, @@ -88,11 +99,6 @@ export function ResultsTrajectoriesPlotDiconnected({ 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) @@ -119,13 +125,13 @@ export function ResultsTrajectoriesPlotDiconnected({ 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, + hospitalBeds, + ICUbeds: icuBeds, })) ?? [] const plotData = [ ...result.plotData.map((x) => { - const dpoint = { time: x.time, hospitalBeds: nHospitalBeds, ICUbeds: nICUBeds } + const dpoint = { time: x.time, hospitalBeds, ICUbeds: icuBeds } Object.keys(x.lines).forEach((d) => { dpoint[d] = enabledPlots.includes(d) ? x.lines[d] : undefined }) diff --git a/src/state/scenario/scenario.selectors.ts b/src/state/scenario/scenario.selectors.ts index d681baba4..fca375bbc 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 '../../components/Main/Results/Utils' + 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 From 3458257c85333356de56b564af7290f9dbbef02d Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 03:39:26 +0200 Subject: [PATCH 08/27] fix: don't take the middle into account --- src/algorithms/preparePlotData.ts | 6 +- src/algorithms/utils/__tests__/sort.test.ts | 61 -------------------- src/algorithms/utils/__tests__/sort2.test.ts | 27 +++++++++ src/algorithms/utils/__tests__/sort3.test.ts | 61 ++++++++++++++++++++ src/algorithms/utils/sort2.ts | 5 ++ src/algorithms/utils/{sort.ts => sort3.ts} | 2 +- 6 files changed, 97 insertions(+), 65 deletions(-) delete mode 100644 src/algorithms/utils/__tests__/sort.test.ts create mode 100644 src/algorithms/utils/__tests__/sort2.test.ts create mode 100644 src/algorithms/utils/__tests__/sort3.test.ts create mode 100644 src/algorithms/utils/sort2.ts rename src/algorithms/utils/{sort.ts => sort3.ts} (78%) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 55aa5384e..26418435c 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -3,7 +3,7 @@ import { pickBy, mapValues } from 'lodash' import type { Trajectory, PlotDatum } from './types/Result.types' import { verifyPositive, verifyTuple } from '../components/Main/Results/Utils' -import { sort } from './utils/sort' +import { sort2 } from './utils/sort2' // import { linesToPlot, areasToPlot, DATA_POINTS } from '../components/Main/Results/ChartCommon' export function filterPositiveValues(obj: T) { @@ -20,11 +20,11 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { return middle.map((_0, day) => { const previousDay = day > 6 ? day - 7 : 0 - let weeklyFatalityMiddle = middle[day].cumulative.fatality.total - middle[previousDay].cumulative.fatality.total // prettier-ignore + 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, weeklyFatalityMiddle, weeklyFatalityUpper] = sort(weeklyFatalityLower, weeklyFatalityMiddle, weeklyFatalityUpper) // prettier-ignore + ;[weeklyFatalityLower, weeklyFatalityUpper] = sort2(weeklyFatalityLower, weeklyFatalityUpper) // prettier-ignore let lines = { susceptible: middle[day].current.susceptible.total, diff --git a/src/algorithms/utils/__tests__/sort.test.ts b/src/algorithms/utils/__tests__/sort.test.ts deleted file mode 100644 index 228a1dc52..000000000 --- a/src/algorithms/utils/__tests__/sort.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { sort } from '../sort' - -describe('sort', () => { - it('sorts already ordered', async () => { - expect(sort(-5, 3.14, 42)).toStrictEqual([-5, 3.14, 42]) - }) - - it('sorts any unordered numbers', async () => { - expect(sort(3.14, -5, 42)).toStrictEqual([-5, 3.14, 42]) - }) - - it('sorts equals: all', async () => { - expect(sort(3.14, 3.14, 3.14)).toStrictEqual([3.14, 3.14, 3.14]) - }) - - it('sorts equals: all zeros', async () => { - expect(sort(0, 0, 0)).toStrictEqual([0, 0, 0]) - }) - - it('sorts equals: left', async () => { - expect(sort(42, 2, 2)).toStrictEqual([2, 2, 42]) - }) - - it('sorts equals: sides', async () => { - expect(sort(2, 42, 2)).toStrictEqual([2, 2, 42]) - }) - - it('sorts equals: right', async () => { - expect(sort(42, 42, 2)).toStrictEqual([2, 42, 42]) - }) - - it('sorts: 1, 2, 3', async () => { - expect(sort(1, 2, 3)).toStrictEqual([1, 2, 3]) - }) - - it('sorts: 2, 1, 3', async () => { - expect(sort(2, 1, 3)).toStrictEqual([1, 2, 3]) - }) - - it('sorts: 1, 3, 2', async () => { - expect(sort(1, 3, 2)).toStrictEqual([1, 2, 3]) - }) - - it('sorts: 3, 1, 2', async () => { - expect(sort(3, 1, 2)).toStrictEqual([1, 2, 3]) - }) - - it('sorts: 3, 2, 1', async () => { - expect(sort(3, 2, 1)).toStrictEqual([1, 2, 3]) - }) - - it('does not mutate arguments', async () => { - const a = -5 - const b = 3.14 - const c = 42 - const [, ,] = sort(c, a, b) - expect(a).toBe(-5) - expect(b).toBe(3.14) - expect(c).toBe(42) - }) -}) diff --git a/src/algorithms/utils/__tests__/sort2.test.ts b/src/algorithms/utils/__tests__/sort2.test.ts new file mode 100644 index 000000000..c5a5684f3 --- /dev/null +++ b/src/algorithms/utils/__tests__/sort2.test.ts @@ -0,0 +1,27 @@ +import { sort2 } from '../sort2' + +describe('sort2', () => { + it('sorts already ordered', async () => { + expect(sort2(-5, 3.14)).toStrictEqual([-5, 3.14]) + }) + + it('sorts any unordered numbers', async () => { + expect(sort2(3.14, -5)).toStrictEqual([-5, 3.14]) + }) + + it('sorts equals: all', async () => { + expect(sort2(3.14, 3.14)).toStrictEqual([3.14, 3.14]) + }) + + it('sorts equals: all zeros', async () => { + expect(sort2(0, 0)).toStrictEqual([0, 0]) + }) + + it('does not mutate arguments', async () => { + const a = 3.14 + const b = -5 + const [, ,] = sort2(a, b) + expect(a).toBe(3.14) + expect(b).toBe(-5) + }) +}) 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/sort2.ts b/src/algorithms/utils/sort2.ts new file mode 100644 index 000000000..bf8f1d861 --- /dev/null +++ b/src/algorithms/utils/sort2.ts @@ -0,0 +1,5 @@ +/** Puts 2 given numbers in ascending order. Does not mutate arguments. */ +export function sort2(a: T, b: T) { + if (a < b) return [a, b] + return [b, a] +} diff --git a/src/algorithms/utils/sort.ts b/src/algorithms/utils/sort3.ts similarity index 78% rename from src/algorithms/utils/sort.ts rename to src/algorithms/utils/sort3.ts index 37a50a8e3..2a6076232 100644 --- a/src/algorithms/utils/sort.ts +++ b/src/algorithms/utils/sort3.ts @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ /** Puts 3 given numbers in ascending order. Does not mutate arguments. */ -export function sort(a: T, b: T, c: T) { +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] From ab71ef2c55eb445e34d54ca96dd93cc002288e17 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 03:40:28 +0200 Subject: [PATCH 09/27] fix: naming conventions --- src/components/Main/Results/Utils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Main/Results/Utils.ts b/src/components/Main/Results/Utils.ts index c9d91c70a..fec6b6d0b 100644 --- a/src/components/Main/Results/Utils.ts +++ b/src/components/Main/Results/Utils.ts @@ -1,13 +1,13 @@ import type { CaseCountsDatum } from '../../../algorithms/types/Param.types' -export type maybeNumber = number | undefined +export type MaybeNumber = number | undefined -export function verifyPositive(x: number): maybeNumber { +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 { +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] @@ -29,8 +29,8 @@ 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 From 5ccbfc0853e50a43a9f031952abba4ae0cd0333a Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 03:54:02 +0200 Subject: [PATCH 10/27] refactor: accept a tuple in sort2 --- src/algorithms/preparePlotData.ts | 2 +- src/algorithms/utils/__tests__/sort2.test.ts | 18 +++++++++++++----- src/algorithms/utils/sort2.ts | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 26418435c..c0a086705 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -24,7 +24,7 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { 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] = sort2(weeklyFatalityLower, weeklyFatalityUpper) // prettier-ignore + ;[weeklyFatalityLower, weeklyFatalityUpper] = sort2([weeklyFatalityLower, weeklyFatalityUpper]) // prettier-ignore let lines = { susceptible: middle[day].current.susceptible.total, diff --git a/src/algorithms/utils/__tests__/sort2.test.ts b/src/algorithms/utils/__tests__/sort2.test.ts index c5a5684f3..6f7c23e6b 100644 --- a/src/algorithms/utils/__tests__/sort2.test.ts +++ b/src/algorithms/utils/__tests__/sort2.test.ts @@ -2,26 +2,34 @@ import { sort2 } from '../sort2' describe('sort2', () => { it('sorts already ordered', async () => { - expect(sort2(-5, 3.14)).toStrictEqual([-5, 3.14]) + expect(sort2([-5, 3.14])).toStrictEqual([-5, 3.14]) }) it('sorts any unordered numbers', async () => { - expect(sort2(3.14, -5)).toStrictEqual([-5, 3.14]) + expect(sort2([3.14, -5])).toStrictEqual([-5, 3.14]) }) it('sorts equals: all', async () => { - expect(sort2(3.14, 3.14)).toStrictEqual([3.14, 3.14]) + expect(sort2([3.14, 3.14])).toStrictEqual([3.14, 3.14]) }) it('sorts equals: all zeros', async () => { - expect(sort2(0, 0)).toStrictEqual([0, 0]) + expect(sort2([0, 0])).toStrictEqual([0, 0]) }) it('does not mutate arguments', async () => { const a = 3.14 const b = -5 - const [, ,] = sort2(a, b) + const [, ,] = sort2([a, b]) expect(a).toBe(3.14) expect(b).toBe(-5) }) + + it('overwrites locals', async () => { + let a = 3.14 + let b = -5 + ;[a, b] = sort2([a, b]) + expect(a).toBe(-5) + expect(b).toBe(3.14) + }) }) diff --git a/src/algorithms/utils/sort2.ts b/src/algorithms/utils/sort2.ts index bf8f1d861..4a8697377 100644 --- a/src/algorithms/utils/sort2.ts +++ b/src/algorithms/utils/sort2.ts @@ -1,5 +1,5 @@ /** Puts 2 given numbers in ascending order. Does not mutate arguments. */ -export function sort2(a: T, b: T) { +export function sort2([a, b]: [T, T]) { if (a < b) return [a, b] return [b, a] } From 5630c7da31e01739422d0a3bd16ff48f1b1bc3be Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 03:55:09 +0200 Subject: [PATCH 11/27] refactor: rename sort2 to sortPair --- src/algorithms/preparePlotData.ts | 4 ++-- .../{sort2.test.ts => sortPair.test.ts} | 16 ++++++++-------- src/algorithms/utils/{sort2.ts => sortPair.ts} | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) rename src/algorithms/utils/__tests__/{sort2.test.ts => sortPair.test.ts} (56%) rename src/algorithms/utils/{sort2.ts => sortPair.ts} (66%) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index c0a086705..aeb8f4b6b 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -3,7 +3,7 @@ import { pickBy, mapValues } from 'lodash' import type { Trajectory, PlotDatum } from './types/Result.types' import { verifyPositive, verifyTuple } from '../components/Main/Results/Utils' -import { sort2 } from './utils/sort2' +import { sortPair } from './utils/sortPair' // import { linesToPlot, areasToPlot, DATA_POINTS } from '../components/Main/Results/ChartCommon' export function filterPositiveValues(obj: T) { @@ -24,7 +24,7 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { 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] = sort2([weeklyFatalityLower, weeklyFatalityUpper]) // prettier-ignore + ;[weeklyFatalityLower, weeklyFatalityUpper] = sortPair([weeklyFatalityLower, weeklyFatalityUpper]) // prettier-ignore let lines = { susceptible: middle[day].current.susceptible.total, diff --git a/src/algorithms/utils/__tests__/sort2.test.ts b/src/algorithms/utils/__tests__/sortPair.test.ts similarity index 56% rename from src/algorithms/utils/__tests__/sort2.test.ts rename to src/algorithms/utils/__tests__/sortPair.test.ts index 6f7c23e6b..83a2ea1fe 100644 --- a/src/algorithms/utils/__tests__/sort2.test.ts +++ b/src/algorithms/utils/__tests__/sortPair.test.ts @@ -1,26 +1,26 @@ -import { sort2 } from '../sort2' +import { sortPair } from '../sortPair' -describe('sort2', () => { +describe('sortPair', () => { it('sorts already ordered', async () => { - expect(sort2([-5, 3.14])).toStrictEqual([-5, 3.14]) + expect(sortPair([-5, 3.14])).toStrictEqual([-5, 3.14]) }) it('sorts any unordered numbers', async () => { - expect(sort2([3.14, -5])).toStrictEqual([-5, 3.14]) + expect(sortPair([3.14, -5])).toStrictEqual([-5, 3.14]) }) it('sorts equals: all', async () => { - expect(sort2([3.14, 3.14])).toStrictEqual([3.14, 3.14]) + expect(sortPair([3.14, 3.14])).toStrictEqual([3.14, 3.14]) }) it('sorts equals: all zeros', async () => { - expect(sort2([0, 0])).toStrictEqual([0, 0]) + expect(sortPair([0, 0])).toStrictEqual([0, 0]) }) it('does not mutate arguments', async () => { const a = 3.14 const b = -5 - const [, ,] = sort2([a, b]) + const [, ,] = sortPair([a, b]) expect(a).toBe(3.14) expect(b).toBe(-5) }) @@ -28,7 +28,7 @@ describe('sort2', () => { it('overwrites locals', async () => { let a = 3.14 let b = -5 - ;[a, b] = sort2([a, b]) + ;[a, b] = sortPair([a, b]) expect(a).toBe(-5) expect(b).toBe(3.14) }) diff --git a/src/algorithms/utils/sort2.ts b/src/algorithms/utils/sortPair.ts similarity index 66% rename from src/algorithms/utils/sort2.ts rename to src/algorithms/utils/sortPair.ts index 4a8697377..1415c9e1b 100644 --- a/src/algorithms/utils/sort2.ts +++ b/src/algorithms/utils/sortPair.ts @@ -1,5 +1,5 @@ /** Puts 2 given numbers in ascending order. Does not mutate arguments. */ -export function sort2([a, b]: [T, T]) { +export function sortPair([a, b]: [T, T]) { if (a < b) return [a, b] return [b, a] } From f65bbea157ae17866307904b5ada03a0e76c48cc Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 04:02:02 +0200 Subject: [PATCH 12/27] refactor: name variables --- src/components/Main/Results/Utils.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/Main/Results/Utils.ts b/src/components/Main/Results/Utils.ts index fec6b6d0b..393dd5c4e 100644 --- a/src/components/Main/Results/Utils.ts +++ b/src/components/Main/Results/Utils.ts @@ -8,18 +8,27 @@ export function verifyPositive(x: number): MaybeNumber { } 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] + const [lo, up] = x + const mi = center ? verifyPositive(center) : undefined + + if (lo !== undefined && up !== undefined && mi !== undefined) { + // prettier-ignore + return [ + lo < mi ? lo : mi, + up > mi ? up : mi, + ] } - if (x[0] !== undefined && x[1] !== undefined) { - return [x[0], x[1]] + + if (lo !== undefined && up !== undefined) { + return [lo, up] } - if (x[0] === undefined && x[1] !== undefined && centerVal !== undefined) { - return [0.0001, x[1] > centerVal ? x[1] : centerVal] + + if (lo === undefined && up !== undefined && mi !== undefined) { + return [0.0001, up > mi ? up : mi] } - if (x[0] === undefined && x[1] !== undefined) { - return [0.0001, x[1]] + + if (lo === undefined && up !== undefined) { + return [0.0001, up] } return undefined From 11462425355bfa26f0e77bbd5589417a14facfbd Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 05:00:58 +0200 Subject: [PATCH 13/27] refactor: name functions --- src/components/Main/Results/Utils.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/components/Main/Results/Utils.ts b/src/components/Main/Results/Utils.ts index 393dd5c4e..1bd1f0f87 100644 --- a/src/components/Main/Results/Utils.ts +++ b/src/components/Main/Results/Utils.ts @@ -1,3 +1,5 @@ +import { min, max, isNumeric } from 'mathjs' + import type { CaseCountsDatum } from '../../../algorithms/types/Param.types' export type MaybeNumber = number | undefined @@ -8,27 +10,23 @@ export function verifyPositive(x: number): MaybeNumber { } export function verifyTuple(x: [MaybeNumber, MaybeNumber], center: MaybeNumber): [number, number] | undefined { - const [lo, up] = x - const mi = center ? verifyPositive(center) : undefined + const [low, upp] = x + const mid = center ? verifyPositive(center) : undefined - if (lo !== undefined && up !== undefined && mi !== undefined) { - // prettier-ignore - return [ - lo < mi ? lo : mi, - up > mi ? up : mi, - ] + if (isNumeric(low) && isNumeric(upp) && isNumeric(mid)) { + return [min(low, mid), max(mid, upp)] } - if (lo !== undefined && up !== undefined) { - return [lo, up] + if (isNumeric(low) && isNumeric(upp)) { + return [low, upp] } - if (lo === undefined && up !== undefined && mi !== undefined) { - return [0.0001, up > mi ? up : mi] + if (!isNumeric(low) && isNumeric(upp) && isNumeric(mid)) { + return [0.0001, max(mid, upp)] } - if (lo === undefined && up !== undefined) { - return [0.0001, up] + if (!isNumeric(low) && isNumeric(upp)) { + return [0.0001, upp] } return undefined From 831812efc44e1920f36fb24fdc71ef6b1926500a Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 05:01:48 +0200 Subject: [PATCH 14/27] refactor: extract variable --- src/algorithms/preparePlotData.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index aeb8f4b6b..14ea930c9 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -40,19 +40,17 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { lines = filterPositiveValues(lines) lines = roundValues(lines) - return { - time: middle[day].time, - lines, - areas: { - susceptible: verifyTuple([verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total)], middle[day].current.susceptible.total), // prettier-ignore - infectious: verifyTuple([verifyPositive(lower[day].current.infectious.total), verifyPositive(upper[day].current.infectious.total)], middle[day].current.infectious.total), // prettier-ignore - severe: verifyTuple([verifyPositive(lower[day].current.severe.total), verifyPositive(upper[day].current.severe.total)], middle[day].current.severe.total), // prettier-ignore - critical: verifyTuple([verifyPositive(lower[day].current.critical.total), verifyPositive(upper[day].current.critical.total)], middle[day].current.critical.total), // prettier-ignore - overflow: verifyTuple([verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total)], middle[day].current.overflow.total), // prettier-ignore - recovered: verifyTuple([verifyPositive(lower[day].cumulative.recovered.total), verifyPositive(upper[day].cumulative.recovered.total)], middle[day].cumulative.recovered.total), // prettier-ignore - fatality: verifyTuple([verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total)], middle[day].cumulative.fatality.total), // prettier-ignore - weeklyFatality: verifyTuple([verifyPositive(weeklyFatalityLower), verifyPositive(weeklyFatalityUpper)], weeklyFatalityMiddle) // prettier-ignore - }, + let areas = { + susceptible: verifyTuple([verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total)], middle[day].current.susceptible.total), // prettier-ignore + infectious: verifyTuple([verifyPositive(lower[day].current.infectious.total), verifyPositive(upper[day].current.infectious.total)], middle[day].current.infectious.total), // prettier-ignore + severe: verifyTuple([verifyPositive(lower[day].current.severe.total), verifyPositive(upper[day].current.severe.total)], middle[day].current.severe.total), // prettier-ignore + critical: verifyTuple([verifyPositive(lower[day].current.critical.total), verifyPositive(upper[day].current.critical.total)], middle[day].current.critical.total), // prettier-ignore + overflow: verifyTuple([verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total)], middle[day].current.overflow.total), // prettier-ignore + recovered: verifyTuple([verifyPositive(lower[day].cumulative.recovered.total), verifyPositive(upper[day].cumulative.recovered.total)], middle[day].cumulative.recovered.total), // prettier-ignore + fatality: verifyTuple([verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total)], middle[day].cumulative.fatality.total), // prettier-ignore + weeklyFatality: verifyTuple([verifyPositive(weeklyFatalityLower), verifyPositive(weeklyFatalityUpper)], weeklyFatalityMiddle) // prettier-ignore } + + return { time: middle[day].time, lines, areas } }) } From 944721523b2c16196bdb78391f027dbcfe6f8c7a Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 05:33:31 +0200 Subject: [PATCH 15/27] refactor: organize verifyTuple function --- src/algorithms/preparePlotData.ts | 16 ++++++++-------- src/components/Main/Results/Utils.ts | 5 +---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 14ea930c9..4d27046cf 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -41,14 +41,14 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { lines = roundValues(lines) let areas = { - susceptible: verifyTuple([verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total)], middle[day].current.susceptible.total), // prettier-ignore - infectious: verifyTuple([verifyPositive(lower[day].current.infectious.total), verifyPositive(upper[day].current.infectious.total)], middle[day].current.infectious.total), // prettier-ignore - severe: verifyTuple([verifyPositive(lower[day].current.severe.total), verifyPositive(upper[day].current.severe.total)], middle[day].current.severe.total), // prettier-ignore - critical: verifyTuple([verifyPositive(lower[day].current.critical.total), verifyPositive(upper[day].current.critical.total)], middle[day].current.critical.total), // prettier-ignore - overflow: verifyTuple([verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total)], middle[day].current.overflow.total), // prettier-ignore - recovered: verifyTuple([verifyPositive(lower[day].cumulative.recovered.total), verifyPositive(upper[day].cumulative.recovered.total)], middle[day].cumulative.recovered.total), // prettier-ignore - fatality: verifyTuple([verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total)], middle[day].cumulative.fatality.total), // prettier-ignore - weeklyFatality: verifyTuple([verifyPositive(weeklyFatalityLower), verifyPositive(weeklyFatalityUpper)], weeklyFatalityMiddle) // prettier-ignore + susceptible: verifyTuple(verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total), verifyPositive(middle[day].current.susceptible.total)), // prettier-ignore + infectious: verifyTuple(verifyPositive(lower[day].current.infectious.total), verifyPositive(upper[day].current.infectious.total), verifyPositive(middle[day].current.infectious.total)), // prettier-ignore + severe: verifyTuple(verifyPositive(lower[day].current.severe.total), verifyPositive(upper[day].current.severe.total), verifyPositive(middle[day].current.severe.total)), // prettier-ignore + critical: verifyTuple(verifyPositive(lower[day].current.critical.total), verifyPositive(upper[day].current.critical.total), verifyPositive(middle[day].current.critical.total)), // prettier-ignore + overflow: verifyTuple(verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total), verifyPositive(middle[day].current.overflow.total)), // prettier-ignore + recovered: verifyTuple(verifyPositive(lower[day].cumulative.recovered.total), verifyPositive(upper[day].cumulative.recovered.total), verifyPositive(middle[day].cumulative.recovered.total)), // prettier-ignore + fatality: verifyTuple(verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total), verifyPositive(middle[day].cumulative.fatality.total)), // prettier-ignore + weeklyFatality: verifyTuple(verifyPositive(weeklyFatalityLower), verifyPositive(weeklyFatalityUpper), verifyPositive(weeklyFatalityMiddle)) // prettier-ignore } return { time: middle[day].time, lines, areas } diff --git a/src/components/Main/Results/Utils.ts b/src/components/Main/Results/Utils.ts index 1bd1f0f87..00415cfc7 100644 --- a/src/components/Main/Results/Utils.ts +++ b/src/components/Main/Results/Utils.ts @@ -9,10 +9,7 @@ export function verifyPositive(x: number): MaybeNumber { return xRounded > 0 ? xRounded : undefined } -export function verifyTuple(x: [MaybeNumber, MaybeNumber], center: MaybeNumber): [number, number] | undefined { - const [low, upp] = x - const mid = center ? verifyPositive(center) : undefined - +export function verifyTuple(low: MaybeNumber, mid: MaybeNumber, upp: MaybeNumber): [number, number] | undefined { if (isNumeric(low) && isNumeric(upp) && isNumeric(mid)) { return [min(low, mid), max(mid, upp)] } From fede79fe2217c1425932abaf2570b45f28475898 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 05:35:34 +0200 Subject: [PATCH 16/27] refactor: move verify tuple function closer to usage --- src/algorithms/preparePlotData.ts | 23 ++++++++++++++++++++++- src/components/Main/Results/Utils.ts | 22 ---------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 4d27046cf..703377889 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -1,7 +1,8 @@ import { pickBy, mapValues } from 'lodash' +import { isNumeric, max, min } from 'mathjs' import type { Trajectory, PlotDatum } from './types/Result.types' -import { verifyPositive, verifyTuple } from '../components/Main/Results/Utils' +import { MaybeNumber, verifyPositive } from '../components/Main/Results/Utils' import { sortPair } from './utils/sortPair' // import { linesToPlot, areasToPlot, DATA_POINTS } from '../components/Main/Results/ChartCommon' @@ -14,6 +15,26 @@ export function roundValues(obj: T) { return mapValues(obj, verifyPositive) as T } +export function verifyTuple(low: MaybeNumber, mid: MaybeNumber, upp: MaybeNumber): [number, number] | undefined { + 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 preparePlotData(trajectory: Trajectory): PlotDatum[] { const { lower, middle, upper } = trajectory diff --git a/src/components/Main/Results/Utils.ts b/src/components/Main/Results/Utils.ts index 00415cfc7..297734973 100644 --- a/src/components/Main/Results/Utils.ts +++ b/src/components/Main/Results/Utils.ts @@ -1,5 +1,3 @@ -import { min, max, isNumeric } from 'mathjs' - import type { CaseCountsDatum } from '../../../algorithms/types/Param.types' export type MaybeNumber = number | undefined @@ -9,26 +7,6 @@ export function verifyPositive(x: number): MaybeNumber { return xRounded > 0 ? xRounded : undefined } -export function verifyTuple(low: MaybeNumber, mid: MaybeNumber, upp: MaybeNumber): [number, number] | undefined { - 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 computeNewEmpiricalCases( timeWindow: number, field: string, From c734c451ca117e503fd4d4b12b6048548bb4c409 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 06:29:33 +0200 Subject: [PATCH 17/27] refactor: replace duplication with a loop --- src/algorithms/preparePlotData.ts | 39 +++++++++++++++++------- src/components/Main/Results/Utils.ts | 5 --- src/state/scenario/scenario.selectors.ts | 2 +- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 703377889..3552ad6e4 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -1,8 +1,10 @@ +/* eslint-disable no-param-reassign */ + import { pickBy, mapValues } from 'lodash' import { isNumeric, max, min } from 'mathjs' import type { Trajectory, PlotDatum } from './types/Result.types' -import { MaybeNumber, verifyPositive } from '../components/Main/Results/Utils' +import { MaybeNumber } from '../components/Main/Results/Utils' import { sortPair } from './utils/sortPair' // import { linesToPlot, areasToPlot, DATA_POINTS } from '../components/Main/Results/ChartCommon' @@ -15,7 +17,16 @@ export function roundValues(obj: T) { return mapValues(obj, verifyPositive) as T } -export function verifyTuple(low: MaybeNumber, mid: MaybeNumber, upp: MaybeNumber): [number, number] | undefined { +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)] } @@ -35,6 +46,10 @@ export function verifyTuple(low: MaybeNumber, mid: MaybeNumber, upp: MaybeNumber return undefined } +export function verifyTuples(obj: T) { + return mapValues(obj, (x) => verifyTuple(x)) +} + export function preparePlotData(trajectory: Trajectory): PlotDatum[] { const { lower, middle, upper } = trajectory @@ -61,17 +76,19 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { lines = filterPositiveValues(lines) lines = roundValues(lines) - let areas = { - susceptible: verifyTuple(verifyPositive(lower[day].current.susceptible.total), verifyPositive(upper[day].current.susceptible.total), verifyPositive(middle[day].current.susceptible.total)), // prettier-ignore - infectious: verifyTuple(verifyPositive(lower[day].current.infectious.total), verifyPositive(upper[day].current.infectious.total), verifyPositive(middle[day].current.infectious.total)), // prettier-ignore - severe: verifyTuple(verifyPositive(lower[day].current.severe.total), verifyPositive(upper[day].current.severe.total), verifyPositive(middle[day].current.severe.total)), // prettier-ignore - critical: verifyTuple(verifyPositive(lower[day].current.critical.total), verifyPositive(upper[day].current.critical.total), verifyPositive(middle[day].current.critical.total)), // prettier-ignore - overflow: verifyTuple(verifyPositive(lower[day].current.overflow.total), verifyPositive(upper[day].current.overflow.total), verifyPositive(middle[day].current.overflow.total)), // prettier-ignore - recovered: verifyTuple(verifyPositive(lower[day].cumulative.recovered.total), verifyPositive(upper[day].cumulative.recovered.total), verifyPositive(middle[day].cumulative.recovered.total)), // prettier-ignore - fatality: verifyTuple(verifyPositive(lower[day].cumulative.fatality.total), verifyPositive(upper[day].cumulative.fatality.total), verifyPositive(middle[day].cumulative.fatality.total)), // prettier-ignore - weeklyFatality: verifyTuple(verifyPositive(weeklyFatalityLower), verifyPositive(weeklyFatalityUpper), verifyPositive(weeklyFatalityMiddle)) // prettier-ignore + 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 = verifyTuples(areasRaw) + return { time: middle[day].time, lines, areas } }) } diff --git a/src/components/Main/Results/Utils.ts b/src/components/Main/Results/Utils.ts index 297734973..310837c2e 100644 --- a/src/components/Main/Results/Utils.ts +++ b/src/components/Main/Results/Utils.ts @@ -2,11 +2,6 @@ import type { CaseCountsDatum } from '../../../algorithms/types/Param.types' export type MaybeNumber = number | undefined -export function verifyPositive(x: number): MaybeNumber { - const xRounded = Math.round(x) - return xRounded > 0 ? xRounded : undefined -} - export function computeNewEmpiricalCases( timeWindow: number, field: string, diff --git a/src/state/scenario/scenario.selectors.ts b/src/state/scenario/scenario.selectors.ts index fca375bbc..ffc282088 100644 --- a/src/state/scenario/scenario.selectors.ts +++ b/src/state/scenario/scenario.selectors.ts @@ -7,7 +7,7 @@ import type { ScenarioParameters, } from '../../algorithms/types/Param.types' -import { verifyPositive } from '../../components/Main/Results/Utils' +import { verifyPositive } from '../../algorithms/preparePlotData' import type { State } from '../reducer' From 5ecc0514267df06183a3fa03a6597decdd65d72e Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 06:36:53 +0200 Subject: [PATCH 18/27] fix: replace wrong functor --- src/algorithms/preparePlotData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 3552ad6e4..e0caf89c0 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -14,7 +14,7 @@ export function filterPositiveValues(obj: T } export function roundValues(obj: T) { - return mapValues(obj, verifyPositive) as T + return mapValues(obj, Math.round) } export function verifyPositive(x: number): MaybeNumber { From 0cebafae8e51ab1ae7caf9d97aedeb0886e8c9a8 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 11 Jun 2020 06:38:17 +0200 Subject: [PATCH 19/27] refactor: rename function to clarify intent --- src/algorithms/preparePlotData.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index e0caf89c0..68030a8ca 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -9,7 +9,7 @@ import { MaybeNumber } from '../components/Main/Results/Utils' import { sortPair } from './utils/sortPair' // import { linesToPlot, areasToPlot, DATA_POINTS } from '../components/Main/Results/ChartCommon' -export function filterPositiveValues(obj: T) { +export function takePositiveValues(obj: T) { return pickBy(obj, (value) => value > 0) as T } @@ -73,7 +73,7 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { weeklyFatality: weeklyFatalityMiddle, } - lines = filterPositiveValues(lines) + lines = takePositiveValues(lines) lines = roundValues(lines) const areasRaw = { From 4e4a52acd0ecfd3ed3ade01e10156464aa44209a Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Fri, 12 Jun 2020 07:55:15 +0200 Subject: [PATCH 20/27] fix: add missing import --- src/components/Main/Results/Utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Main/Results/Utils.ts b/src/components/Main/Results/Utils.ts index 310837c2e..77b709fe2 100644 --- a/src/components/Main/Results/Utils.ts +++ b/src/components/Main/Results/Utils.ts @@ -1,5 +1,7 @@ import type { CaseCountsDatum } from '../../../algorithms/types/Param.types' +import { verifyPositive } from '../../../algorithms/preparePlotData' + export type MaybeNumber = number | undefined export function computeNewEmpiricalCases( From e25092fa1f3d24207fcada5b55e8c38545b82387 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Fri, 12 Jun 2020 07:55:27 +0200 Subject: [PATCH 21/27] refactor: cleanup and memoize number formatters --- src/components/Main/Results/AgeBarChart.tsx | 10 ++++------ src/components/Main/Results/OutcomeRatesTable.tsx | 7 +++---- .../Main/Results/OutcomesDetailsTable.tsx | 6 +++--- .../ResultsTrajectoriesPlot.tsx | 8 +++----- src/helpers/numberFormat.ts | 15 +++++++++++++-- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/components/Main/Results/AgeBarChart.tsx b/src/components/Main/Results/AgeBarChart.tsx index 68614f4e5..4cf018aee 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,7 +22,7 @@ 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' @@ -63,6 +63,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 @@ -84,7 +85,7 @@ export function AgeBarChartDisconnected({ num = Number.parseFloat(num) } - return numberFormatter(true, false)(num) + return formatNumber(num) }, } @@ -92,9 +93,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) 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/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx index 245f4e40f..99899d741 100644 --- a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx +++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-ignore */ -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import _ from 'lodash' import { connect } from 'react-redux' @@ -26,7 +26,7 @@ import type { CaseCountsDatum, MitigationInterval } from '../../../algorithms/ty import type { AlgorithmResult } from '../../../algorithms/types/Result.types' -import { numberFormatter } from '../../../helpers/numberFormat' +import { getNumberFormatters } from '../../../helpers/numberFormat' import { selectResult } from '../../../state/algorithm/algorithm.selectors' import { State } from '../../../state/reducer' import { @@ -91,9 +91,7 @@ export function ResultsTrajectoriesPlotDiconnected({ const { t } = useTranslation() const chartRef = React.useRef(null) const [enabledPlots, setEnabledPlots] = useState(defaultEnabledPlots) - - const formatNumber = numberFormatter(!!shouldFormatNumbers, false) - const formatNumberRounded = numberFormatter(!!shouldFormatNumbers, true) + const { formatNumber, formatNumberRounded } = useMemo(() => getNumberFormatters({ shouldFormatNumbers }), [shouldFormatNumbers]) // prettier-ignore if (!result) { return null 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 } +} From 4e49a0d8c8c8d5e13be241e16a6732af2bcb216c Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Fri, 12 Jun 2020 07:56:51 +0200 Subject: [PATCH 22/27] fix: adjust path in tsc ignore list --- config/webpack/webpack.client.babel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/webpack/webpack.client.babel.ts b/config/webpack/webpack.client.babel.ts index 1a3a576a1..7fbf04e17 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/ResultsTrajectoriesPlot.tsx', // FIXME + '!src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx', // FIXME // end '!src/**/*.(spec|test).{js,jsx,ts,tsx}', From dcb4c7b2aeb6564ddde1f2415727980da0f95c5f Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Fri, 12 Jun 2020 08:24:08 +0200 Subject: [PATCH 23/27] fix: lint --- .eslintrc.js | 2 +- .../Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx index 99899d741..04b5b8ec2 100644 --- a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx +++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-ts-ignore */ import React, { useMemo, useState } from 'react' import _ from 'lodash' From 8089cec4e2452cb2aa490a51c745b48af254dbd2 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Fri, 12 Jun 2020 08:24:30 +0200 Subject: [PATCH 24/27] refactor: extract hardcoded value into a variable --- .../Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx | 5 +++-- src/constants.ts | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx index 04b5b8ec2..c1b368828 100644 --- a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx +++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx @@ -24,6 +24,7 @@ import { useTranslation } from 'react-i18next' import type { CaseCountsDatum, MitigationInterval } from '../../../algorithms/types/Param.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' @@ -98,9 +99,9 @@ export function ResultsTrajectoriesPlotDiconnected({ // 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 [newEmpiricalCases] = computeNewEmpiricalCases(CASE_COUNTS_INTERVAL_DAYS, 'cases', caseCountsData) - const [weeklyEmpiricalDeaths] = computeNewEmpiricalCases(7, 'deaths', caseCountsData) + const [weeklyEmpiricalDeaths] = computeNewEmpiricalCases(CASE_COUNTS_INTERVAL_DAYS, 'deaths', caseCountsData) const hasObservations = { [DATA_POINTS.ObservedCases]: caseCountsData && caseCountsData.some((d) => d.cases), 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 From 56af6560cc7bd4ea9699a5aaadf9f6e2f1dd4646 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Fri, 12 Jun 2020 17:04:45 +0200 Subject: [PATCH 25/27] refactor: extract inline handler --- .../ResultsTrajectoriesPlot.tsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx index c1b368828..3842384c6 100644 --- a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx +++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import _ from 'lodash' import { connect } from 'react-redux' @@ -92,6 +92,18 @@ export function ResultsTrajectoriesPlotDiconnected({ const chartRef = React.useRef(null) const [enabledPlots, setEnabledPlots] = useState(defaultEnabledPlots) const { formatNumber, formatNumberRounded } = useMemo(() => getNumberFormatters({ shouldFormatNumbers }), [shouldFormatNumbers]) // prettier-ignore + const handleLegendClick = useCallback( + ({ dataKey }) => { + const plots = enabledPlots.slice(0) + if (enabledPlots.includes(dataKey)) { + plots.splice(plots.indexOf(dataKey), 1) + } else { + plots.push(dataKey) + } + setEnabledPlots(plots) + }, + [enabledPlots], + ) if (!result) { return null @@ -276,15 +288,7 @@ export function ResultsTrajectoriesPlotDiconnected({ )} /> - { - const plots = enabledPlots.slice(0) - enabledPlots.includes(e.dataKey) ? plots.splice(plots.indexOf(e.dataKey), 1) : plots.push(e.dataKey) - setEnabledPlots(plots) - }} - /> + {translatePlots(t, observationsHavingDataToPlot).map((d) => ( From 15c11e336804d9a580813189c1d74b3549315001 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Fri, 12 Jun 2020 17:13:46 +0200 Subject: [PATCH 26/27] refactor: make property names consistent --- src/algorithms/model.ts | 2 +- src/components/Main/Results/ChartCommon.ts | 12 ++++++------ .../ResultsTrajectoriesPlot.tsx | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) 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/components/Main/Results/ChartCommon.ts b/src/components/Main/Results/ChartCommon.ts index 90fc48cfb..9be5b776a 100644 --- a/src/components/Main/Results/ChartCommon.ts +++ b/src/components/Main/Results/ChartCommon.ts @@ -22,12 +22,12 @@ export const DATA_POINTS = { CumulativeCases: 'cumulativeCases', NewCases: 'newCases', HospitalBeds: 'hospitalBeds', - ICUbeds: 'ICUbeds', + icuBeds: 'icuBeds', /* Observed */ ObservedDeaths: 'observedDeaths', ObservedCases: 'cases', ObservedHospitalized: 'currentHospitalized', - ObservedICU: 'ICU', + ObservedICU: 'icu', ObservedNewCases: 'newCases', ObservedWeeklyDeaths: 'weeklyDeaths', } @@ -40,11 +40,11 @@ export const defaultEnabledPlots = [ 'recovered', 'weeklyFatality', 'hospitalBeds', - 'ICUbeds', + 'icuBeds', /* Observed */ 'cases', 'currentHospitalized', - 'ICU', + 'icu', 'newCases', 'weeklyDeaths', ] @@ -61,7 +61,7 @@ export const colors = { [DATA_POINTS.CumulativeCases]: '#aaaaaa', [DATA_POINTS.NewCases]: '#edaf5f', [DATA_POINTS.HospitalBeds]: '#bbbbbb', - [DATA_POINTS.ICUbeds]: '#cccccc', + [DATA_POINTS.icuBeds]: '#cccccc', } export const linesToPlot: LineProps[] = [ @@ -79,7 +79,7 @@ export const linesToPlot: LineProps[] = [ legendType: 'line', }, { key: DATA_POINTS.HospitalBeds, color: colors.hospitalBeds, name: 'Total hospital beds', legendType: 'none' }, - { key: DATA_POINTS.ICUbeds, color: colors.ICUbeds, name: 'Total ICU/ICM beds', legendType: 'none' }, + { key: DATA_POINTS.icuBeds, color: colors.icuBeds, name: 'Total ICU/ICM beds', legendType: 'none' }, ] export const areasToPlot: LineProps[] = [ diff --git a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx index 3842384c6..13294b737 100644 --- a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx +++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx @@ -132,16 +132,16 @@ export function ResultsTrajectoriesPlotDiconnected({ currentHospitalized: enabledPlots.includes(DATA_POINTS.ObservedHospitalized) ? d.hospitalized || undefined : undefined, - ICU: enabledPlots.includes(DATA_POINTS.ObservedICU) ? d.icu || 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: icuBeds, + icuBeds, })) ?? [] const plotData = [ ...result.plotData.map((x) => { - const dpoint = { time: x.time, hospitalBeds, ICUbeds: icuBeds } + const dpoint = { time: x.time, hospitalBeds, icuBeds } Object.keys(x.lines).forEach((d) => { dpoint[d] = enabledPlots.includes(d) ? x.lines[d] : undefined }) @@ -172,7 +172,7 @@ export function ResultsTrajectoriesPlotDiconnected({ }) // determine the max of enabled plots w/o the hospital capacity - const dataKeys = enabledPlots.filter((d) => d !== DATA_POINTS.HospitalBeds && d !== DATA_POINTS.ICUbeds) + 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])))) @@ -193,7 +193,7 @@ export function ResultsTrajectoriesPlotDiconnected({ }) const tooltipItemsToDisplay = Object.keys(tooltipItems).filter( - (itemKey: string) => itemKey !== 'time' && itemKey !== 'hospitalBeds' && itemKey !== 'ICUbeds', + (itemKey: string) => itemKey !== 'time' && itemKey !== 'hospitalBeds' && itemKey !== 'icuBeds', ) const logScaleString: YAxisProps['scale'] = isLogScale ? 'log' : 'linear' From 888cd20844a627b23f04081dbaafcbb09cb1c6d0 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Sat, 13 Jun 2020 00:33:08 +0200 Subject: [PATCH 27/27] refactor: (wip) extract functions from plot component body --- src/algorithms/preparePlotData.ts | 22 +- src/algorithms/types/Result.types.ts | 60 +++- src/algorithms/utils/__tests__/soa.test.ts | 88 +++++ src/algorithms/utils/soa.ts | 17 + src/components/Main/Results/AgeBarChart.tsx | 18 +- src/components/Main/Results/ChartCommon.ts | 211 +++++------- src/components/Main/Results/ChartTooltip.tsx | 4 +- .../Main/Results/LinePlotTooltip.tsx | 6 +- .../ResultsTrajectoriesPlot.tsx | 313 +++++++++++------- 9 files changed, 459 insertions(+), 280 deletions(-) create mode 100644 src/algorithms/utils/__tests__/soa.test.ts create mode 100644 src/algorithms/utils/soa.ts diff --git a/src/algorithms/preparePlotData.ts b/src/algorithms/preparePlotData.ts index 68030a8ca..9b9f3508b 100644 --- a/src/algorithms/preparePlotData.ts +++ b/src/algorithms/preparePlotData.ts @@ -1,10 +1,11 @@ /* eslint-disable no-param-reassign */ -import { pickBy, mapValues } from 'lodash' +import { pickBy, mapValues, pick } from 'lodash' import { isNumeric, max, min } from 'mathjs' -import type { Trajectory, PlotDatum } from './types/Result.types' +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' @@ -50,10 +51,10 @@ export function verifyTuples(obj: T) return mapValues(obj, (x) => verifyTuple(x)) } -export function preparePlotData(trajectory: Trajectory): PlotDatum[] { +export function preparePlotData(trajectory: Trajectory) { const { lower, middle, upper } = trajectory - return middle.map((_0, day) => { + const data = middle.map((_0, day) => { const previousDay = day > 6 ? day - 7 : 0 const weeklyFatalityMiddle = middle[day].cumulative.fatality.total - middle[previousDay].cumulative.fatality.total // prettier-ignore @@ -62,7 +63,7 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { ;[weeklyFatalityLower, weeklyFatalityUpper] = sortPair([weeklyFatalityLower, weeklyFatalityUpper]) // prettier-ignore - let lines = { + let lines: Line = { susceptible: middle[day].current.susceptible.total, infectious: middle[day].current.infectious.total, severe: middle[day].current.severe.total, @@ -87,8 +88,17 @@ export function preparePlotData(trajectory: Trajectory): PlotDatum[] { weeklyFatality: [ weeklyFatalityLower, weeklyFatalityMiddle, weeklyFatalityUpper ] // prettier-ignore } - const areas = verifyTuples(areasRaw) + 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/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/components/Main/Results/AgeBarChart.tsx b/src/components/Main/Results/AgeBarChart.tsx index 06d09f7a8..21663d203 100644 --- a/src/components/Main/Results/AgeBarChart.tsx +++ b/src/components/Main/Results/AgeBarChart.tsx @@ -28,7 +28,7 @@ import { selectAgeDistributionData, selectSeverityDistributionData } from '../.. 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' @@ -191,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/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/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx index 13294b737..c87f36ba1 100644 --- a/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx +++ b/src/components/Main/ResultsTrajectoriesPlot/ResultsTrajectoriesPlot.tsx @@ -1,8 +1,7 @@ import React, { useCallback, useMemo, useState } from 'react' -import _ from 'lodash' +import { maxBy, minBy, zipWith, pick } from 'lodash' import { connect } from 'react-redux' - import ReactResizeDetector from 'react-resize-detector' import { CartesianGrid, @@ -22,6 +21,7 @@ import { 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' @@ -39,12 +39,11 @@ import { selectIsLogScale, selectShouldFormatNumbers } from '../../../state/sett import { calculatePosition, scrollToRef } from '../Results/chartHelper' import { - linesToPlot, - areasToPlot, - observationsToPlot, - DATA_POINTS, + linesMetaDefault, + casesMetaDefault, translatePlots, - defaultEnabledPlots, + constantsMetaDefault, + LineProps, } from '../Results/ChartCommon' import { LinePlotTooltip } from '../Results/LinePlotTooltip' import { MitigationPlot } from '../Results/MitigationLinePlot' @@ -53,6 +52,7 @@ 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 @@ -78,7 +78,127 @@ const mapStateToProps = (state: State) => ({ const mapDispatchToProps = {} -// eslint-disable-next-line sonarjs/cognitive-complexity +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, @@ -90,111 +210,37 @@ export function ResultsTrajectoriesPlotDiconnected({ }: ResultsTrajectoriesPlotProps) { const { t } = useTranslation() const chartRef = React.useRef(null) - const [enabledPlots, setEnabledPlots] = useState(defaultEnabledPlots) const { formatNumber, formatNumberRounded } = useMemo(() => getNumberFormatters({ shouldFormatNumbers }), [shouldFormatNumbers]) // prettier-ignore - const handleLegendClick = useCallback( - ({ dataKey }) => { - const plots = enabledPlots.slice(0) - if (enabledPlots.includes(dataKey)) { - plots.splice(plots.indexOf(dataKey), 1) - } else { - plots.push(dataKey) - } - setEnabledPlots(plots) - }, - [enabledPlots], - ) + 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 } - // 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 { plotData } = result - const [weeklyEmpiricalDeaths] = computeNewEmpiricalCases(CASE_COUNTS_INTERVAL_DAYS, 'deaths', caseCountsData) + const { linesObject, areasObject } = getPlotData({ plotData, linesMeta }) + // const points = getObservations({ caseCountsData, pointsMeta }) + // const constants = getConstants({ hospitalBeds, icuBeds }) - 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 data = aos({ ...linesObject, ...areasObject }) + let meta = [...linesMeta, ...areasMeta, ...pointsMeta, ...constantsMeta] - 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 plotData = [ - ...result.plotData.map((x) => { - const dpoint = { time: x.time, hospitalBeds, icuBeds } - 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) { + // const data = [...lines, ...areas, ...points, ...constants] + + if (meta.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 { xDomain: { tMin, tMax }, yDomain } = getDomain({ data, meta, isLogScale }) // prettier-ignore - 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 tooltipItemsToDisplay = getTooltipItems({ consolidatedPlotData }) const logScaleString: YAxisProps['scale'] = isLogScale ? 'log' : 'linear' @@ -202,12 +248,13 @@ export function ResultsTrajectoriesPlotDiconnected({ const yTickFormatter = (value: number) => formatNumberRounded(value) - const legendFormatter = (enabledPlots: string[]) => (value?: LegendPayload['value'], entry?: LegendPayload) => { + 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} } @@ -249,7 +296,7 @@ export function ResultsTrajectoriesPlotDiconnected({ onClick={() => scrollToRef(chartRef)} width={width} height={height} - data={consolidatedPlotData} + data={data} throttleDelay={75} margin={{ left: 5, @@ -257,8 +304,6 @@ export function ResultsTrajectoriesPlotDiconnected({ bottom: 5, }} > - - @@ -288,40 +333,56 @@ export function ResultsTrajectoriesPlotDiconnected({ )} /> - + - {translatePlots(t, observationsHavingDataToPlot).map((d) => ( - + {translatePlots(t, points).map(({ dataKey, color, name, legendType }) => ( + ))} - {translatePlots(t, linesToPlot).map((d) => ( + {translatePlots(t, lines).map(({ dataKey, color, name, legendType }) => ( ))} - {translatePlots(t, areasToPlot).map((d) => ( + {translatePlots(t, areas).map(({ dataKey, color, name, legendType }) => ( ))} + + {translatePlots(t, constantsMetaDefault).map(({ dataKey, color, name, legendType }) => ( + + ))} + + )