import React, { useEffect, useMemo, useState, useRef } from 'react'
import { useDispatch } from 'react-redux'
import styled from 'styled-components'
import {
LineChart,
Line,
XAxis,
YAxis,
ReferenceArea,
Tooltip,
// Label,
ResponsiveContainer,
Legend,
ReferenceLine,
} from 'recharts'
import { ChartTitle, ChartLabel, Icon } from '../../components'
import colors from '../../config/colors'
import useGetLineChartData from '../../hooks/useGetLineChartData'
import { useSize } from '../../hooks/useSize'
import { paramsActions } from '../../stores/paramsStore'
const { setDataParams } = paramsActions
const ChartContainer = styled.span`
display: block;
width: 100%;
height: 100%;
span {
color: white;
}
user-select: none;
`
const DockPopButton = styled.button`
position: absolute;
top: 0;
right: 0;
width: 2em;
height: 2em;
background: none;
border: none;
padding: 0.25em;
z-index: 1;
cursor: pointer;
svg g {
fill: ${colors.yellow};
}
`
const monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'June',
'July',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
]
const numberFormatter = (val) =>
val > 1000000 ? `${val / 1000000}M` : val > 1000 ? `${val / 1000}K` : val
const dateFormatter = (val) => {
let tempDate = (new Date(val).getMonth() + 1) % 12
return tempDate === 0 ? val.slice(0, 4) : monthNames[tempDate]
}
const CustomTick = ({labelFormatter, ...props}) => {
return <text {...props}>{labelFormatter(props.payload.value)}</text>
}
const getDateRange = ({ startDate, endDate }, monthIncrement = 1) => {
let dateArray = []
let currentDate = new Date(startDate)
while (currentDate <= new Date(endDate)) {
dateArray.push(new Date(currentDate))
currentDate.setMonth(currentDate.getMonth() + monthIncrement)
}
return dateArray.map((date) => date.toISOString().slice(0, 10))
}
const startAndEndDates = {
startDate: new Date('02/01/2020'),
endDate: new Date(),
}
const rangeIncrement = (maximum) => {
let returnArray = []
const increment = 2 * 10 ** (`${maximum}`.length - 1)
for (let i = 0; i < maximum; i += increment) {
returnArray.push(i)
}
return returnArray
}
const getXAxisMonths = (width) => {
if (width > 800) {
return [
'01',
'02',
'03',
'04',
'05',
'06',
'07',
'08',
'09',
'10',
'11',
'12',
]
}
if (width > 600) {
return ['01', '04', '07', '10']
}
if (width > 450) {
return ['01', '07']
}
return ['01']
}
const CustomTooltip = ({ active, payload, background }) => {
try {
if (active) {
return (
<div
style={{
background: background,
padding: '10px',
borderRadius: '4px',
boxShadow:
'0px 5px 5px -3px rgba(0,0,0,0.2), 0px 8px 10px 1px rgba(0,0,0,0.14), 0px 3px 14px 2px rgba(0,0,0,0.12)',
}}
>
<p
style={{
color: background === '#000000' ? 'white' : 'black',
padding: '5px 0 0 0',
}}
>
{payload[0].payload.date}
</p>
{payload.map((data, idx) => (
<p
style={{
color: data.color,
padding: '5px 0 0 0',
fontWeight: 600,
}}
key={`tooltip-inner-text-${idx}`}
>
{data.name}:{' '}
{!isNaN(+data.payload[data.dataKey])
? (
Math.round(
data.payload[data.dataKey] * 10
) / 10
).toLocaleString('en')
: data.payload[data.dataKey]}
</p>
))}
</div>
)
}
} catch {
return null
}
return null
}
const LabelText = {
cases: {
x1label: 'Cumulative Cases',
x2label: '7 Day Average',
title: 'Cases',
},
deaths: {
x1label: 'Cumulative Deaths',
x2label: '7 Day Average',
title: 'Deaths',
},
vaccines_fully_vaccinated: {
x1label: 'Total Vaccinations',
x2label: '7 Day Average',
title: 'Population Fully Vaccinated',
},
testing_wk_pos: {
x1label: '',
x2label: '7 Day Average',
title: 'Testing Positivity',
},
}
const colorSchemes = {
light: {
highlightColor: colors.strongOrange,
mediumColor: colors.darkgray,
gridColor: colors.black,
backgroundColor: colors.white,
solidColor: colors.black,
},
dark: {
highlightColor: colors.yellow,
mediumColor: colors.lightgray,
gridColor: `${colors.white}88`,
backgroundColor: colors.black,
solidColor: colors.white,
},
}
/**
* Component that returns the inner guts of the chart, for easy memoization
*
* @component
*/
function LineChartInner({
resetDock = () => { },
docked = false,
table = 'cases',
logChart = false,
showSummarized = false,
populationNormalized = false,
shouldShowVariants = false,
colorScheme = 'dark',
geoid = [],
loadedCallback = () => { },
}) {
// For light and dark color schemes
const {
highlightColor,
mediumColor,
gridColor,
backgroundColor,
solidColor,
} = colorSchemes[colorScheme]
const qualtitiveScale = {
light: colors.qualtitiveScaleLight,
dark: colors.qualtitiveScaleDark,
}[colorScheme]
const dispatch = useDispatch()
const handleChange = (e) => {
if (e?.activeTooltipIndex) {
dispatch(setDataParams({ nIndex: e.activeTooltipIndex }))
}
}
// width listener
const ChartRef = useRef(null)
const { width } = useSize(ChartRef)
const xAxisMonths = getXAxisMonths(width)
const dateRange = getDateRange(startAndEndDates)
const filteredTickMonths = dateRange.filter((f) =>
xAxisMonths.includes(f.slice(-5, -3))
)
// line chart data
const {
// currentData,
currIndex,
currRange,
chartData,
maximums,
isTimeseries,
selectionKeys,
selectionNames,
} = useGetLineChartData({
table,
geoid,
})
// Print layout hack
// reporting back loaded state add 500ms delay to allow render time
useEffect(() => {
setTimeout(() => loadedCallback(!!(maximums && chartData)), 500)
}, [!!(maximums && chartData)])
// get first word of snake case, if relevant
const chartTitle = table.split('_')[0]
const [activeLine, setActiveLine] = useState(false)
const handleLegendHover = (o) =>
setActiveLine(+o.dataKey.split('Weekly')[0])
const handleLegendLeave = () => setActiveLine(false)
const { x1label, x2label, title } = LabelText[table]
const memoizedInnerComponents = useMemo(() => {
if (maximums && chartData) {
return (
<>
{selectionKeys.length === 0 && (
<Line
type="monotone"
yAxisId="right"
dataKey={`sum${populationNormalized ? '100k' : ''}`}
name={x1label}
stroke={mediumColor}
dot={false}
isAnimationActive={false}
/>
)}
{selectionKeys.length === 1 &&
selectionKeys.map((geoid, idx) => (
<Line
type="monotone"
yAxisId="right"
dataKey={`${geoid}Sum${populationNormalized ? '100k' : ''
}`}
name={selectionNames[idx] + ' Cumulative'}
stroke={
selectionKeys.length === 1
? mediumColor
: selectionKeys.length >
qualtitiveScale.length
? mediumColor
: qualtitiveScale[idx]
}
// strokeDasharray={"2,2"}
dot={false}
isAnimationActive={false}
key={`line-${idx}`}
/>
))}
{selectionKeys.length === 0 && (
<Line
type="monotone"
yAxisId="left"
dataKey={`weekly${populationNormalized ? '100k' : ''
}`}
name={x2label}
stroke={highlightColor}
dot={false}
isAnimationActive={false}
/>
)}
{selectionKeys.length > 0 &&
selectionKeys.map((geoid, idx) => (
<Line
type="monotone"
yAxisId="left"
key={`line-weekly-${geoid}`}
dataKey={`${geoid}Weekly${populationNormalized ? '100k' : ''
}`}
name={selectionNames[idx] + ' 7-Day Ave'}
stroke={
selectionKeys.length === 1
? highlightColor
: selectionKeys.length >
qualtitiveScale.length
? highlightColor
: qualtitiveScale[idx]
}
dot={false}
isAnimationActive={false}
strokeOpacity={activeLine === geoid ? 1 : 0.7}
strokeWidth={activeLine === geoid ? 3 : 1}
/>
))}
{selectionKeys.length > 1 && showSummarized && (
<Line
type="monotone"
yAxisId="right"
dataKey="keySum"
name="Total For Selection"
stroke={mediumColor}
strokeWidth={3}
dot={false}
isAnimationActive={false}
/>
)}
{selectionKeys.length < qualtitiveScale.length && (
<Legend
onMouseEnter={handleLegendHover}
onMouseLeave={handleLegendLeave}
margin={{ top: 40, left: 0, right: 0, bottom: 0 }}
iconType="plainline"
/>
)}
{shouldShowVariants && (
<>
<ReferenceLine
x="2020-12-18"
yAxisId="left"
stroke="gray"
strokeWidth={0.5}
label={{
value: 'Alpha, Beta',
angle: 90,
position: 'left',
fill: 'gray',
}}
/>
<ReferenceLine
x="2021-01-11"
yAxisId="left"
stroke="gray"
strokeWidth={0.5}
label={{
value: 'Gamma',
angle: 90,
position: 'left',
fill: 'gray',
}}
/>
<ReferenceLine
x="2021-05-11"
yAxisId="left"
stroke="gray"
strokeWidth={0.5}
label={{
value: 'Delta',
angle: 90,
position: 'left',
fill: 'gray',
}}
/>
<ReferenceLine
x="2021-11-26"
yAxisId="left"
stroke="gray"
strokeWidth={0.5}
label={{
value: 'Omicron',
angle: 90,
position: 'left',
fill: 'gray',
}}
/>
</>
)}
</>
)
} else {
return null
}
}, [
JSON.stringify({
maximums,
selectionKeys,
chartData,
docked,
table,
logChart,
showSummarized,
populationNormalized,
shouldShowVariants,
colorScheme,
geoid,
}),
])
// REFERENCE AREA COORDS
const x1 = chartData[currIndex - currRange]?.date || '2020-01-21'
const areaX1 = x1 < '2020-01-20' ? '2020-01-20' : x1
const x2 =
chartData[currIndex]?.date || chartData[chartData.length - 1]?.date
const areaX2 = x2
if (maximums && chartData) {
return (
<ChartContainer ref={ChartRef} id="lineChart">
{!docked && (
<DockPopButton
onClick={resetDock}
title="Dock Line Chart Panel"
>
<Icon symbol="popOut" />
</DockPopButton>
)}
{selectionNames.length < 2 ? (
<ChartTitle color={gridColor}>
<span>
{title}
{selectionNames.length
? `: ${selectionNames[0]}`
: ''}
</span>
</ChartTitle>
) : (
<ChartTitle color={gridColor}>
<span>{chartTitle} over time</span>
</ChartTitle>
)}
<ChartLabel
color={highlightColor}
left={colorScheme === 'light' ? -20 : -30}
>
{x2label}
</ChartLabel>
<ChartLabel color={mediumColor} right={-35}>
{x1label}
</ChartLabel>
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={chartData}
margin={{
top: 0,
right: colorScheme === 'light' ? 30 : 20,
left: 5,
bottom: 60,
}}
onClick={isTimeseries ? handleChange : null}
>
<XAxis
dataKey="date"
ticks={filteredTickMonths}
minTickGap={-50}
domain={[
startAndEndDates.startDate
.toISOString()
.slice(0, 10),
startAndEndDates.endDate
.toISOString()
.slice(0, 10),
]}
tick={(props) => (
<CustomTick
{...props}
style={{
fill:
props.payload.value.slice(
-5,
-3
) === '01'
? solidColor
: gridColor,
fontSize: '10px',
fontFamily: 'Lato',
fontWeight: 600,
transform: 'translateY(10px)',
}}
labelFormatter={dateFormatter}
/>
)}
/>
<YAxis
yAxisId="right"
orientation="right"
type="number"
scale={logChart ? 'log' : 'linear'}
domain={[0.01, 'dataMax']}
allowDataOverflow
ticks={rangeIncrement({ maximum: maximums.count })}
tick={
<CustomTick
style={{
fill: mediumColor,
fontSize: '10px',
fontFamily: 'Lato',
fontWeight: 600,
transform: 'translateY(px)',
}}
labelFormatter={numberFormatter}
/>
}
/>
<YAxis
yAxisId="left"
orientation="left"
scale={logChart ? 'log' : 'linear'}
domain={[0.01, 'dataMax']}
allowDataOverflow
ticks={rangeIncrement({ maximum: maximums.sum })}
tick={
<CustomTick
style={{
fill: highlightColor,
fontSize: '10px',
fontFamily: 'Lato',
fontWeight: 600,
transform: 'translateY(2px)',
}}
labelFormatter={numberFormatter}
/>
}
/>
{memoizedInnerComponents}
<Tooltip
content={({ active, payload }) => (
<CustomTooltip
background={backgroundColor}
{...{
active,
payload,
}}
/>
)}
/>
<ReferenceArea
yAxisId="right"
x1={areaX1}
x2={areaX2}
fill="white"
fillOpacity={0.15}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
</ChartContainer>
)
} else {
return null
}
}
export default LineChartInner
Source