import React from 'react'; import { ResponsiveContainer, Tooltip, Line, Area, Legend, ReferenceLine, YAxis, XAxis, CartesianGrid, ComposedChart, } from 'recharts'; import { Typography } from '@material-ui/core'; import Grid from '@material-ui/core/Grid'; import { myShortNumber } from '../Util'; import { AntSwitch } from "./AntSwitch.js" import { makeStyles } from '@material-ui/core/styles'; import { mergeDataSeries, makeDataSeriesFromTotal } from "./DataSeries"; const Util = require('covidmodule').Util; const moment = require("moment"); const useStyles = makeStyles(theme => ({ customtooltip: { backgroundColor: "#FFFFFF", }, })); const DeathTooltip = (props) => { const classes = useStyles(); const { active } = props; if (active) { const { payload, label } = props; let deaths_mean; let deathsTotal_mean; let actualDeath_total; let actualDeath_daily; payload.map(p => { p = p.payload; if ("deaths_mean" in p) { deaths_mean = p.deaths_mean; } if ("deathsTotal_mean" in p) { deathsTotal_mean = p.deathsTotal_mean; } if ("actualDeath_total" in p) { actualDeath_total = p.actualDeath_total; } if ("actualDeath_daily" in p) { actualDeath_daily = p.actualDeath_daily; } return null; }); return ( <div className={classes.customtooltip}> <Typography variant="body1" noWrap> {label} </Typography> {deaths_mean && <Typography variant="body2" noWrap> {`Projected Daily Death: ${deaths_mean}`} </Typography> } {deathsTotal_mean && <Typography variant="body2" noWrap> {`Projected Total: ${deathsTotal_mean}`} </Typography> } {actualDeath_daily && <Typography variant="body2" noWrap> {`Actual Daily: ${actualDeath_daily}`} </Typography> } {actualDeath_total && <Typography variant="body2" noWrap> {`Actual Total: ${actualDeath_total}`} </Typography> } </div> ); } return null; } const keydeath = { key_lower: "deaths_lower", key_upper: "deaths_upper", key_delta: "delta", key_mean: "deaths_mean", key_upper_cumulative: "deathsTotal_upper", key_lower_cumulative: "deathsTotal_lower", key_delta_cumulative: "deathsTotal_delta", key_mean_cumulative: "deathsTotal_mean", } const GraphDeathProjection = (props) => { const [sourceData, setSourceData] = React.useState(null); const [actual_deaths_total, setActualDeath] = React.useState(null); React.useEffect(() => { if (props.source.projectionsAsync) { props.source.projectionsAsync() .then(data => setSourceData(data)); } props.source.deathsAsync() .then(data => setActualDeath(data)); }, [props.source]) if (props.source.projectionsAsync && (!sourceData || sourceData.length === 0)) { return <div> Loading</div>; } if (!actual_deaths_total) { return <div> Loading</div>; } let death_entrys = makeDataSeriesFromTotal( actual_deaths_total, "actualDeath_total", "actualDeath_daily", "actualDeath_moving_avg") let max_date = null; if (props.source.projectionsAsync) { const [formateddata, max_date2] = formatData(sourceData, keydeath); max_date = max_date2; for (let item of formateddata) { let entry = actual_deaths_total[item.fulldate]; if (entry) { item.actualDeath_total = entry; let lastday = moment(item.fulldate, "MM/DD/YYYY").subtract(1, "days").format("MM/DD/YYYY"); let lastentry = actual_deaths_total[lastday]; if (lastentry !== null) { item.actualDeath_daily = entry - lastentry; } } } death_entrys = mergeDataSeries(death_entrys, formateddata); } return <GraphDeathProjectionRender data={death_entrys} max_date={max_date} max_label="Peak Death" data_keys={keydeath} tooltip={<DeathTooltip />} />; } const formatData = (data, keys) => { data = data.map(d => { d.fulldate = Util.normalize_date(d.date); d.name = moment(d.fulldate, "MM/DD/YYYY").format("M/D"); return d; }); data = data.sort((a, b) => moment(a.fulldate, "MM/DD/YYYY").toDate() - (moment(b.fulldate, "MM/DD/YYYY")).toDate()); let deathsTotal_mean = 0; let deathsTotal_upper = 0; let deathsTotal_lower = 0; let max_death = 0; let max_date = 0; data = data.map(d => { let r = {}; let mean = Math.round(d[keys.key_mean]); let lower = Math.round(d[keys.key_lower]); let upper = Math.round(d[keys.key_upper]); r[keys.key_mean] = mean; r[keys.key_lower] = lower; r[keys.key_upper] = upper; r[keys.key_delta] = upper - lower; if (max_death < mean) { max_death = mean; max_date = d.fulldate; } deathsTotal_mean += mean; r[keys.key_mean_cumulative] = deathsTotal_mean; deathsTotal_upper += upper; deathsTotal_lower += lower; r[keys.key_lower_cumulative] = deathsTotal_lower; r[keys.key_delta_cumulative] = deathsTotal_upper - deathsTotal_lower; r.fulldate = d.fulldate; r.name = d.name; return r; }); return [data, max_date]; } const GraphDeathProjectionRender = (props) => { let data = props.data; const max_date = props.max_date; const data_keys = props.data_keys; data = data.map(d => { d.name = moment(d.fulldate, "MM/DD/YYYY").format("M/D"); return d; }); data = data.sort((a, b) => moment(a.fulldate, "MM/DD/YYYY").toDate() - (moment(b.fulldate, "MM/DD/YYYY")).toDate()); const [state, setState] = React.useState({ showall: false, }); const handleLogScaleToggle = event => { setState({ ...state, showall: !state.showall }); }; const cutoff = moment().subtract(30, 'days') const future = moment().add(30, 'days') data = data.filter(d => { let day = moment(d.fulldate, "MM/DD/YYYY"); return day.isAfter(cutoff) && day.isBefore(future); }); const formatYAxis = (tickItem) => { return myShortNumber(tickItem); } return <> <Grid container alignItems="center" spacing={1}> <Grid item onClick={handleLogScaleToggle}> <Typography> Daily </Typography> </Grid> <Grid item> <AntSwitch checked={state.showall} onClick={handleLogScaleToggle} /> </Grid> <Grid item onClick={handleLogScaleToggle}> <Typography> Total </Typography> </Grid> <Grid item></Grid> </Grid> <ResponsiveContainer height={300} > <ComposedChart data={data} margin={{ top: 5, right: 30, left: 5, bottom: 5 }} > <XAxis dataKey="name" /> <YAxis yAxisId={0} tickFormatter={formatYAxis} /> <ReferenceLine x={moment(max_date, "MM/DD/YYYY").format("M/D")} label={{ value: props.max_label, fill: '#a3a3a3' }} stroke="#e3e3e3" strokeWidth={3} /> <CartesianGrid stroke="#d5d5d5" strokeDasharray="5 5" /> <Line type="monotone" dataKey={data_keys.key_mean} stroke="#000000" dot={{ r: 1 }} yAxisId={0} strokeWidth={2} /> <Area type='monotone' dataKey={data_keys.key_lower} stackId="1" stroke='#8884d8' fill='#FFFFFF' /> <Area type='monotone' dataKey={data_keys.key_delta} stackId="1" stroke='#82ca9d' fill='#82ca9d' /> <Line type="monotone" dataKey="actualDeath_daily" stroke="#FF0000" dot={{ r: 1 }} strokeDasharray="2 2" yAxisId={0} strokeWidth={2} /> <Line type="monotone" dataKey="actualDeath_moving_avg" stroke="#FF0000" dot={{ r: 1 }} yAxisId={0} strokeWidth={3} /> {state.showall && <Line type="monotone" dataKey={data_keys.key_mean_cumulative} dot={{ r: 1 }} stroke="#000000" yAxisId={0} strokeWidth={1} />} {state.showall && <Line type="monotone" dataKey="actualDeath_total" dot={{ r: 1 }} stroke="#ff0000" yAxisId={0} strokeWidth={2} />} {state.showall && <Area type='monotone' dataKey={data_keys.key_lower_cumulative} stackId="2" stroke='#8884d8' fill='#FFFFFF' />} {state.showall && <Area type='monotone' dataKey={data_keys.key_delta_cumulative} stackId="2" stroke='#82ca9d' fill='#82ca9d' />} <Tooltip content={props.tooltip} /> <Legend verticalAlign="top" payload={[ { value: 'Actual Death', type: 'line', color: '#ff0000' }, { value: 'Projection', type: 'line', color: '#000000' }, ]} /> </ComposedChart> </ResponsiveContainer> <Typography variant="body2"> Source: The Institute for Health Metrics and Evaluation </Typography> </> } function maybeDeathProjectionTabFor(source) { if (source.deathsAsync) { return { id: 'peakdeath', label: 'Death', graph: GraphDeathProjection, }; } else { return undefined; } } export { maybeDeathProjectionTabFor, GraphDeathProjection }