import TimeseriesLoader from './loaders/Timeseries'; import { STATE_NAMES, STATISTIC_CONFIGS, TIMESERIES_CHART_TYPES, TIMESERIES_LOOKBACK_DAYS, TIMESERIES_STATISTICS, } from '../constants'; import useIsVisible from '../hooks/useIsVisible'; import { getIndiaDateYesterdayISO, parseIndiaDate, retry, } from '../utils/commonFunctions'; import {PinIcon, ReplyIcon} from '@primer/octicons-react'; import classnames from 'classnames'; import {min} from 'd3-array'; import {formatISO, subDays} from 'date-fns'; import equal from 'fast-deep-equal'; import { memo, useCallback, useEffect, useMemo, useRef, useState, lazy, Suspense, } from 'react'; import {useTranslation} from 'react-i18next'; import {useLocalStorage, useWindowSize} from 'react-use'; const Timeseries = lazy(() => retry(() => import('./Timeseries'))); const TimeseriesBrush = lazy(() => retry(() => import('./TimeseriesBrush'))); function TimeseriesExplorer({ stateCode, timeseries, date: timelineDate, regionHighlighted, setRegionHighlighted, anchor, setAnchor, expandTable = false, hideVaccinated = false, noRegionHighlightedDistrictData, }) { const {t} = useTranslation(); const [lookback, setLookback] = useLocalStorage('timeseriesLookbackDays', 90); const [chartType, setChartType] = useLocalStorage('chartType', 'delta'); const [isUniform, setIsUniform] = useLocalStorage('isUniform', false); const [isLog, setIsLog] = useLocalStorage('isLog', false); const [isMovingAverage, setIsMovingAverage] = useLocalStorage( 'isMovingAverage', false ); const stateCodeDateRange = Object.keys(timeseries?.[stateCode]?.dates || {}); const beginningDate = stateCodeDateRange[0] || timelineDate || getIndiaDateYesterdayISO(); const endDate = min([ stateCodeDateRange[stateCodeDateRange.length - 1], timelineDate || getIndiaDateYesterdayISO(), ]); const [brushSelectionEnd, setBrushSelectionEnd] = useState(endDate); useEffect(() => { setBrushSelectionEnd(endDate); }, [endDate]); const brushSelectionStart = lookback !== null ? formatISO(subDays(parseIndiaDate(brushSelectionEnd), lookback), { representation: 'date', }) : beginningDate; const explorerElement = useRef(); const isVisible = useIsVisible(explorerElement, {once: true}); const {width} = useWindowSize(); const selectedRegion = useMemo(() => { if (timeseries?.[regionHighlighted.stateCode]?.districts) { return { stateCode: regionHighlighted.stateCode, districtName: regionHighlighted.districtName, }; } else { return { stateCode: regionHighlighted.stateCode, districtName: null, }; } }, [timeseries, regionHighlighted.stateCode, regionHighlighted.districtName]); const selectedTimeseries = useMemo(() => { if (selectedRegion.districtName) { return timeseries?.[selectedRegion.stateCode]?.districts?.[ selectedRegion.districtName ]?.dates; } else { return timeseries?.[selectedRegion.stateCode]?.dates; } }, [timeseries, selectedRegion.stateCode, selectedRegion.districtName]); const regions = useMemo(() => { const states = Object.keys(timeseries || {}) .filter((code) => code !== stateCode) .sort((code1, code2) => STATE_NAMES[code1].localeCompare(STATE_NAMES[code2]) ) .map((code) => { return { stateCode: code, districtName: null, }; }); const districts = Object.keys(timeseries || {}).reduce((acc1, code) => { return [ ...acc1, ...Object.keys(timeseries?.[code]?.districts || {}).reduce( (acc2, districtName) => { return [ ...acc2, { stateCode: code, districtName: districtName, }, ]; }, [] ), ]; }, []); return [ { stateCode: stateCode, districtName: null, }, ...states, ...districts, ]; }, [timeseries, stateCode]); const dropdownRegions = useMemo(() => { if ( regions.find( (region) => region.stateCode === regionHighlighted.stateCode && region.districtName === regionHighlighted.districtName ) ) return regions; return [ ...regions, { stateCode: regionHighlighted.stateCode, districtName: regionHighlighted.districtName, }, ]; }, [regionHighlighted.stateCode, regionHighlighted.districtName, regions]); const dates = useMemo( () => Object.keys(selectedTimeseries || {}).filter((date) => date <= endDate), [selectedTimeseries, endDate] ); const brushSelectionDates = useMemo( () => dates.filter( (date) => brushSelectionStart <= date && date <= brushSelectionEnd ), [dates, brushSelectionStart, brushSelectionEnd] ); const handleChange = useCallback( ({target}) => { setRegionHighlighted(JSON.parse(target.value)); }, [setRegionHighlighted] ); const resetDropdown = useCallback(() => { setRegionHighlighted({ stateCode: stateCode, districtName: null, }); }, [stateCode, setRegionHighlighted]); const statistics = useMemo( () => TIMESERIES_STATISTICS.filter( (statistic) => (!(STATISTIC_CONFIGS[statistic]?.category === 'vaccinated') || !hideVaccinated) && // (chartType === 'total' || statistic !== 'active') && (chartType === 'delta' || statistic !== 'tpr') ), [chartType, hideVaccinated] ); return ( <div className={classnames( 'TimeseriesExplorer fadeInUp', { stickied: anchor === 'timeseries', }, {expanded: expandTable} )} style={{ display: anchor && anchor !== 'timeseries' && (!expandTable || width < 769) ? 'none' : '', }} ref={explorerElement} > <div className="timeseries-header"> <div className={classnames('anchor', 'fadeInUp', { stickied: anchor === 'timeseries', })} style={{ display: expandTable && width >= 769 ? 'none' : '', }} onClick={ setAnchor && setAnchor.bind(this, anchor === 'timeseries' ? null : 'timeseries') } > <PinIcon /> </div> <h1>{t('Spread Trends')}</h1> <div className="tabs"> {Object.entries(TIMESERIES_CHART_TYPES).map( ([ctype, value], index) => ( <div className={`tab ${chartType === ctype ? 'focused' : ''}`} key={ctype} onClick={setChartType.bind(this, ctype)} > <h4>{t(value)}</h4> </div> ) )} </div> <div className="timeseries-options"> <div className="scale-modes"> <label className="main">{`${t('Scale Modes')}:`}</label> <div className="timeseries-mode"> <label htmlFor="timeseries-mode">{t('Uniform')}</label> <input id="timeseries-mode" type="checkbox" className="switch" checked={isUniform} aria-label={t('Checked by default to scale uniformly.')} onChange={setIsUniform.bind(this, !isUniform)} /> </div> <div className={`timeseries-mode ${ chartType !== 'total' ? 'disabled' : '' }`} > <label htmlFor="timeseries-logmode">{t('Logarithmic')}</label> <input id="timeseries-logmode" type="checkbox" checked={chartType === 'total' && isLog} className="switch" disabled={chartType !== 'total'} onChange={setIsLog.bind(this, !isLog)} /> </div> </div> <div className={`timeseries-mode ${ chartType === 'total' ? 'disabled' : '' } moving-average`} > <label htmlFor="timeseries-moving-average"> {t('7 day Moving Average')} </label> <input id="timeseries-moving-average" type="checkbox" checked={chartType === 'delta' && isMovingAverage} className="switch" disabled={chartType !== 'delta'} onChange={setIsMovingAverage.bind(this, !isMovingAverage)} /> </div> </div> </div> {dropdownRegions && ( <div className="state-selection"> <div className="dropdown"> <select value={JSON.stringify(selectedRegion)} onChange={handleChange} > {dropdownRegions .filter( (region) => STATE_NAMES[region.stateCode] !== region.districtName ) .map((region) => { return ( <option value={JSON.stringify(region)} key={`${region.stateCode}-${region.districtName}`} > {region.districtName ? t(region.districtName) : t(STATE_NAMES[region.stateCode])} </option> ); })} </select> </div> <div className="reset-icon" onClick={resetDropdown}> <ReplyIcon /> </div> </div> )} {isVisible && ( <Suspense fallback={<TimeseriesLoader />}> <Timeseries timeseries={selectedTimeseries} regionHighlighted={selectedRegion} dates={brushSelectionDates} {...{ statistics, endDate, chartType, isUniform, isLog, isMovingAverage, noRegionHighlightedDistrictData, }} /> <TimeseriesBrush timeseries={selectedTimeseries} regionHighlighted={selectedRegion} currentBrushSelection={[brushSelectionStart, brushSelectionEnd]} animationIndex={statistics.length} {...{dates, endDate, lookback, setBrushSelectionEnd, setLookback}} /> </Suspense> )} {!isVisible && <div style={{height: '50rem'}} />} <div className="pills fadeInUp" style={{animationDelay: `${(1 + statistics.length) * 250}ms`}} > {TIMESERIES_LOOKBACK_DAYS.map((numDays) => ( <button key={numDays} type="button" className={classnames({ selected: numDays === lookback, })} onClick={setLookback.bind(this, numDays)} > {numDays !== null ? `${numDays} ${t('days')}` : t('Beginning')} </button> ))} </div> </div> ); } const isEqual = (prevProps, currProps) => { if (currProps.forceRender) { return false; } else if (!currProps.timeseries && prevProps.timeseries) { return true; } else if (currProps.timeseries && !prevProps.timeseries) { return false; } else if ( !equal( currProps.regionHighlighted.stateCode, prevProps.regionHighlighted.stateCode ) ) { return false; } else if ( !equal( currProps.regionHighlighted.districtName, prevProps.regionHighlighted.districtName ) ) { return false; } else if (!equal(currProps.date, prevProps.date)) { return false; } else if (!equal(currProps.anchor, prevProps.anchor)) { return false; } else if (!equal(currProps.expandTable, prevProps.expandTable)) { return false; } else if (!equal(currProps.hideVaccinated, prevProps.hideVaccinated)) { return false; } else if ( !equal( currProps.noRegionHighlightedDistrictData, prevProps.noRegionHighlightedDistrictData ) ) { return false; } return true; }; export default memo(TimeseriesExplorer, isEqual);