Skip to content
This repository was archived by the owner on Nov 2, 2021. It is now read-only.

Feat: Add 14-day case growth rate to table #2493

Merged
merged 3 commits into from
Jun 7, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
"@primer/octicons-react": "^14.2.1",
"@testing-library/jest-dom": "^5.13.0",
"@testing-library/react": "^11.2.7",
"@tippyjs/react": "^4.2.5",
"classnames": "^2.2.6",
"corejs-typeahead": "^1.3.1",
"d3-array": "^2.11.0",
8 changes: 1 addition & 7 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@ import {retry} from './utils/commonFunctions';

import {lazy, useState, Suspense, useEffect} from 'react';
import {Route, Redirect, Switch, useLocation} from 'react-router-dom';
import useDarkMode from 'use-dark-mode';

const Home = lazy(() => retry(() => import('./components/Home')));
const About = lazy(() => retry(() => import('./components/About')));
@@ -16,7 +15,6 @@ const LanguageSwitcher = lazy(() =>
);

const App = () => {
const darkMode = useDarkMode(false);
const [showLanguageSwitcher, setShowLanguageSwitcher] = useState(false);
const location = useLocation();

@@ -70,11 +68,7 @@ const App = () => {
/>
</Suspense>

<Navbar
pages={pages}
{...{darkMode}}
{...{showLanguageSwitcher, setShowLanguageSwitcher}}
/>
<Navbar {...{pages, showLanguageSwitcher, setShowLanguageSwitcher}} />

<Suspense fallback={<div />}>
<Switch location={location}>
48 changes: 20 additions & 28 deletions src/App.scss
Original file line number Diff line number Diff line change
@@ -494,6 +494,7 @@ h6 {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-right: 1rem;

& > * {
align-self: center;
@@ -503,11 +504,6 @@ h6 {
width: calc(100% - 2.5rem);
}

span {
margin-bottom: auto;
margin-right: 1rem;
}

svg {
align-self: flex-start;
cursor: pointer;
@@ -3868,13 +3864,14 @@ footer {
font-weight: 900;
margin-top: 0.5rem;

&:hover {
color: $blue;
}

svg {
height: 1rem;
margin-bottom: -0.25rem;
margin-bottom: -0.1rem;
margin-left: 0.25rem;
}

&:hover {
color: $blue;
}
}
}
@@ -4208,28 +4205,23 @@ footer {
}

.Tooltip {
display: flex;
flex-direction: column;
background-color: $black !important;
border-radius: 8px !important;

.tippy-arrow {
color: $black;
}

.message {
background: #000;
border-radius: 5px;
display: flex;
flex-direction: row;
color: $white;
font-size: 13px;
font-weight: 400;
justify-content: flex-end;
max-width: 30vw;
min-width: 10rem;
padding: 0.5rem;
pointer-events: none;
right: 0;
top: 0;
transform: translate3d(2rem, 0, 0);
font-weight: 600;
margin: 0;
padding: 0 0.5rem;

p {
color: $white !important;
margin: 0;
div {
margin-bottom: 0.5rem;
margin-top: 0.5rem;
}
}
}
20 changes: 0 additions & 20 deletions src/animations.js
Original file line number Diff line number Diff line change
@@ -30,26 +30,6 @@ export const SLIDE_OUT_MOBILE = {
zIndex: 999,
};

/*
* Tooltip
*/

export const TOOLTIP_FADE_IN = {
opacity: 1,
transform: 'translate3d(0, 0px, 0)',
zIndex: 999,
position: 'absolute',
pointerEvents: 'none',
};

export const TOOLTIP_FADE_OUT = {
opacity: 0,
transform: 'translate3d(0, 2px, 0)',
zIndex: 999,
position: 'absolute',
pointerEvents: 'none',
};

/*
* Language Switcher
*/
30 changes: 7 additions & 23 deletions src/components/About.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
import Footer from './Footer';

import {API_DOMAIN} from '../constants';
import {fetcher} from '../utils/commonFunctions';

import {useState, useEffect} from 'react';
import {useEffect} from 'react';
import {Helmet} from 'react-helmet';

// TODO(slightlyoff): factor out common JSON parsing & caching of this file
const DATA_URL = `${API_DOMAIN}/website_data.json`;
import useSWR from 'swr';

function About() {
const [faq, setFaq] = useState([]);

useEffect(() => {
getFAQs();
}, []);
const {data} = useSWR(`${API_DOMAIN}/website_data.json`, fetcher, {
suspense: true,
});

useEffect(() => {
window.scrollTo(0, 0);
}, []);

const getFAQs = () => {
fetch(DATA_URL)
.then((response) => {
return response.json();
})
.then((data) => {
setFaq(data.faq);
})
.catch((error) => {
console.log(error);
});
};

return (
<>
<Helmet>
@@ -43,7 +27,7 @@ function About() {
</Helmet>

<div className="About">
{faq.map((faq, index) => {
{data.faq.map((faq, index) => {
return (
<div
key={index}
21 changes: 6 additions & 15 deletions src/components/DeltaBarGraph.js
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import {
getStatistic,
} from '../utils/commonFunctions';

import {min, max} from 'd3-array';
import {extent} from 'd3-array';
import {axisBottom} from 'd3-axis';
import {scaleBand, scaleLinear} from 'd3-scale';
import {select} from 'd3-selection';
@@ -44,21 +44,12 @@ function DeltaBarGraph({timeseries, statistic, lookback}) {
.range([margin.left, chartRight])
.paddingInner(width / 1000);

const [statisticMin, statisticMax] = extent(dates, (date) =>
getDeltaStatistic(timeseries?.[date], statistic)
);

const yScale = scaleLinear()
.domain([
Math.min(
0,
min(dates, (date) =>
getDeltaStatistic(timeseries?.[date], statistic)
) || 0
),
Math.max(
1,
max(dates, (date) =>
getDeltaStatistic(timeseries?.[date], statistic)
) || 0
),
])
.domain([Math.min(0, statisticMin || 0), Math.max(1, statisticMax || 0)])
.range([chartBottom, margin.top]);

const xAxis = axisBottom(xScale)
2 changes: 1 addition & 1 deletion src/components/DistrictRow.js
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ function DistrictRow({
<div className="cell">
<div className="state-name">{t(districtName)}</div>
{data?.meta?.notes && (
<Tooltip {...{data: data.meta.notes}}>
<Tooltip message={data.meta.notes}>
<InfoIcon size={16} />
</Tooltip>
)}
10 changes: 6 additions & 4 deletions src/components/HeaderCell.js
Original file line number Diff line number Diff line change
@@ -35,6 +35,8 @@ function StateHeaderCell({handleSort, sortData, setSortData, statistic}) {
}
};

const statisticConfig = STATISTIC_CONFIGS[statistic];

return (
<div
className="cell heading"
@@ -54,13 +56,13 @@ function StateHeaderCell({handleSort, sortData, setSortData, statistic}) {
<div>
{t(
toTitleCase(
STATISTIC_CONFIGS[statistic]?.tableConfig?.displayName ||
STATISTIC_CONFIGS[statistic].displayName
statisticConfig?.tableConfig?.displayName ||
statisticConfig.displayName
)
)}
</div>
{statistic === 'other' && (
<Tooltip data={'Migrated cases or non-COVID deaths'}>
{statisticConfig?.tableConfig?.notes && (
<Tooltip message={t(statisticConfig.tableConfig.notes)}>
<InfoIcon size={14} />
</Tooltip>
)}
8 changes: 5 additions & 3 deletions src/components/MapLegend.js
Original file line number Diff line number Diff line change
@@ -44,16 +44,18 @@ function MapLegend({data, statistic, mapViz, mapScale}) {
svg.selectAll('.axis > *:not(.axistext)').remove();
svg.select('.axistext').text('');

const domainMax = mapScale.domain()[1];
const [, domainMax] = mapScale.domain();

const legend = svg
.select('.circles')
.attr('transform', `translate(48,40)`)
.attr('text-anchor', 'middle');

const legendRadius = [0.1, 0.4, 1].map((d) => d * domainMax);

legend
.selectAll('circle')
.data([domainMax / 10, (domainMax * 2) / 5, domainMax])
.data(legendRadius)
.join('circle')
.attr('fill', 'none')
.attr('stroke', '#ccc')
@@ -71,7 +73,7 @@ function MapLegend({data, statistic, mapViz, mapScale}) {
axisRight(yScale)
.tickSize(0)
.tickPadding(0)
.tickValues([domainMax / 10, (domainMax * 2) / 5, domainMax])
.tickValues(legendRadius)
.tickFormat((num) =>
formatNumber(
num,
13 changes: 8 additions & 5 deletions src/components/MapSwitcher.js
Original file line number Diff line number Diff line change
@@ -22,13 +22,16 @@ const MapSwitcher = ({mapStatistic, setMapStatistic}) => {

useEffect(() => {
if (width > 0) {
const isPresent = LEVEL_STATISTICS.indexOf(mapStatistic) >= 0;
ReactDOM.unstable_batchedUpdates(() => {
springApi.start({
transform: `translate3d(${
(width * LEVEL_STATISTICS.indexOf(mapStatistic)) /
LEVEL_STATISTICS.length
}px, 0, 0)`,
opacity: 1,
transform: isPresent
? `translate3d(${
(width * LEVEL_STATISTICS.indexOf(mapStatistic)) /
LEVEL_STATISTICS.length
}px, 0, 0)`
: null,
opacity: isPresent ? 1 : 0,
background: `${STATISTIC_CONFIGS[mapStatistic].color}20`,
delay: count === 0 ? 1500 : 0,
onStart: setClicked.bind(this, true),
49 changes: 27 additions & 22 deletions src/components/MapVisualizer.js
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ import {formatNumber, toTitleCase} from '../utils/commonFunctions';

import {AlertIcon} from '@primer/octicons-react';
import classnames from 'classnames';
import {max} from 'd3-array';
import {extent} from 'd3-array';
import {json} from 'd3-fetch';
import {geoIdentity, geoPath} from 'd3-geo';
import {scaleSqrt, scaleSequential} from 'd3-scale';
@@ -126,43 +126,48 @@ function MapVisualizer({
);
}, [geoData, isDistrictView]);

const statisticMax = useMemo(() => {
const [statisticMin, statisticMax] = useMemo(() => {
const stateCodes = Object.keys(data).filter(
(stateCode) =>
stateCode !== 'TT' && Object.keys(MAP_META).includes(stateCode)
);

return !isDistrictView
? max(stateCodes, (stateCode) => getStatistic(data[stateCode]))
: max(stateCodes, (stateCode) => {
const districts = Object.keys(
data[stateCode]?.districts || []
).filter(
(districtName) =>
(districtsSet?.[stateCode] || new Set()).has(districtName) ||
(mapViz === MAP_VIZS.BUBBLES &&
districtName === UNKNOWN_DISTRICT_KEY)
);
return districts && districts.length > 0
? max(districts, (districtName) =>
getStatistic(data[stateCode].districts[districtName])
)
: 0;
});
if (!isDistrictView) {
return extent(stateCodes, (stateCode) => getStatistic(data[stateCode]));
} else {
const districtData = stateCodes.reduce((res, stateCode) => {
const districts = Object.keys(data[stateCode]?.districts || []).filter(
(districtName) =>
(districtsSet?.[stateCode] || new Set()).has(districtName) ||
(mapViz === MAP_VIZS.BUBBLES &&
districtName === UNKNOWN_DISTRICT_KEY)
);
res.push(
...districts.map((districtName) =>
getStatistic(data[stateCode].districts[districtName])
)
);
return res;
}, []);
return extent(districtData);
}
}, [data, isDistrictView, getStatistic, mapViz, districtsSet]);

const mapScale = useMemo(() => {
if (mapViz === MAP_VIZS.BUBBLES) {
return scaleSqrt([0, Math.max(statisticMax, 1)], [0, 40])
return scaleSqrt(
[Math.min(0, statisticMin || 0), Math.max(1, statisticMax || 0)],
[0, 40]
)
.clamp(true)
.nice(3);
} else {
return scaleSequential(
[0, Math.max(1, statisticMax)],
[Math.min(0, statisticMin || 0), Math.max(1, statisticMax || 0)],
colorInterpolator(statistic)
).clamp(true);
}
}, [mapViz, statistic, statisticMax]);
}, [mapViz, statistic, statisticMin, statisticMax]);

const fillColor = useCallback(
(d) => {
9 changes: 3 additions & 6 deletions src/components/Navbar.js
Original file line number Diff line number Diff line change
@@ -12,19 +12,16 @@ import {useTranslation} from 'react-i18next';
import {Link} from 'react-router-dom';
import {useTransition, animated} from 'react-spring';
import {useLockBodyScroll, usePageLeave, useWindowSize} from 'react-use';
import useDarkMode from 'use-dark-mode';

function Navbar({
pages,
darkMode,
showLanguageSwitcher,
setShowLanguageSwitcher,
}) {
function Navbar({pages, showLanguageSwitcher, setShowLanguageSwitcher}) {
const {i18n, t} = useTranslation();
const currentLanguage = Object.keys(locales).includes(i18n?.language)
? i18n?.language
: i18n?.options?.fallbackLng[0];

const [expand, setExpand] = useState(false);
const darkMode = useDarkMode(false);

useLockBodyScroll(expand);
const windowSize = useWindowSize();
2 changes: 1 addition & 1 deletion src/components/Row.js
Original file line number Diff line number Diff line change
@@ -184,7 +184,7 @@ function Row({
{t(STATE_NAMES[stateCode]) || districtNameStr}
</div>
{data?.meta?.notes && (
<Tooltip {...{data: data.meta.notes}}>
<Tooltip message={data.meta.notes}>
<InfoIcon size={16} />
</Tooltip>
)}
2 changes: 1 addition & 1 deletion src/components/StateMetaCard.js
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ function StateMetaCard({
<div className={`meta-item ${className}`}>
<div className="meta-item-top">
<h3>{title}</h3>
<Tooltip {...{data: formula}}>
<Tooltip message={formula}>
<InfoIcon size={16} />
</Tooltip>
</div>
24 changes: 20 additions & 4 deletions src/components/Table.js
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ import {
TABLE_STATISTICS_EXPANDED,
UNASSIGNED_STATE_CODE,
} from '../constants';
import {getTableStatistic, retry} from '../utils/commonFunctions';
import {getStatistic, getTableStatistic, retry} from '../utils/commonFunctions';

import {
FilterIcon,
@@ -23,7 +23,7 @@ import {
import classnames from 'classnames';
import equal from 'fast-deep-equal';
import produce from 'immer';
import {memo, useCallback, useEffect, useState, lazy} from 'react';
import {memo, useCallback, useEffect, useMemo, useState, lazy} from 'react';
import {
ChevronLeft,
ChevronsLeft,
@@ -83,12 +83,28 @@ function Table({
config: config.wobbly,
});

const [districts, setDistricts] = useState();
const [allDistricts, setAllDistricts] = useState();

const [tableOption, setTableOption] = useState('States');
const [isPerLakh, setIsPerLakh] = useState(false);
const [isInfoVisible, setIsInfoVisible] = useState(false);

const districts = useMemo(() => {
if (!isPerLakh) {
return allDistricts;
} else {
return Object.keys(allDistricts)
.filter(
(districtKey) =>
getStatistic(allDistricts[districtKey], 'total', 'population') > 0
)
.reduce((res, districtKey) => {
res[districtKey] = allDistricts[districtKey];
return res;
}, {});
}
}, [isPerLakh, allDistricts]);

const numPages = Math.ceil(
Object.keys(districts || {}).length / DISTRICT_TABLE_COUNT
);
@@ -153,7 +169,7 @@ function Table({
workerInstance.getDistricts(states);
workerInstance.addEventListener('message', (message) => {
if (message.data.type !== 'RPC') {
setDistricts(message.data);
setAllDistricts(message.data);
workerInstance.terminate();
}
});
63 changes: 23 additions & 40 deletions src/components/Tooltip.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,33 @@
import {TOOLTIP_FADE_IN, TOOLTIP_FADE_OUT} from '../animations';
import Tippy from '@tippyjs/react';
import {useCallback} from 'react';

import {useCallback, useState} from 'react';
import {useTransition, animated} from 'react-spring';

const Tooltip = ({data, children}) => {
const [isTooltipVisible, setIsTooltipVisible] = useState(false);

const transitions = useTransition(isTooltipVisible, {
from: TOOLTIP_FADE_OUT,
enter: TOOLTIP_FADE_IN,
leave: TOOLTIP_FADE_OUT,
config: {
mass: 1,
tension: 210,
friction: 20,
},
});
import 'tippy.js/dist/tippy.css';
import 'tippy.js/animations/shift-away.css';

function Tooltip({children, message}) {
const handleClick = useCallback((e) => e.stopPropagation(), []);

return (
<span
<Tippy
className="Tooltip"
style={{position: 'relative'}}
onMouseEnter={setIsTooltipVisible.bind(this, true)}
onMouseLeave={setIsTooltipVisible.bind(this, false)}
onClick={handleClick.bind(this)}
content={
<p
className="message"
dangerouslySetInnerHTML={{
__html: message
.split('\n')
.map((text) => `<div>${text}</div>`)
.join(''),
}}
></p>
}
arrow={false}
animation="shift-away"
touch="hold"
>
{children}

{transitions(
(style, item) =>
item && (
<animated.div {...{style}}>
<div className="message">
<p
dangerouslySetInnerHTML={{
__html: data.replace(/\n/g, '<br/>'),
}}
></p>
</div>
</animated.div>
)
)}
</span>
<div onClick={handleClick.bind(this)}>{children}</div>
</Tippy>
);
};
}

export default Tooltip;
6 changes: 3 additions & 3 deletions src/components/Updates.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {UPDATES_COUNT} from '../constants';
import {capitalize} from '../utils/commonFunctions';

import {CrossReferenceIcon} from '@primer/octicons-react';
import {format, formatDistance} from 'date-fns';
import {Fragment, useLayoutEffect} from 'react';
import {Send} from 'react-feather';
import {useTranslation} from 'react-i18next';

const newDate = new Date();
@@ -78,8 +78,8 @@ function Updates({updates}) {
rel="noopener noreferrer"
>
<h4>
<Send />
{t('Join Instant Updates channel')}
{t('Get updates on Telegram')}
<CrossReferenceIcon />
</h4>
</a>
</div>
8 changes: 7 additions & 1 deletion src/components/VaccinationHeader.js
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import {formatNumber, getStatistic} from '../utils/commonFunctions';
import {ShieldCheckIcon} from '@primer/octicons-react';
import classnames from 'classnames';
import equal from 'fast-deep-equal';
import {useState, memo} from 'react';
import {useEffect, useRef, useState, memo} from 'react';
import {useTranslation} from 'react-i18next';
import {animated, useSpring, Globals} from 'react-spring';

@@ -15,6 +15,7 @@ Globals.assign({colors: null});
function ProgressBar({dose1, dose2}) {
const {t} = useTranslation();
const [highlightedDose, setHighlightedDose] = useState(2);
const isMounted = useRef(false);

const doseSpring = useSpring({
dose1,
@@ -23,8 +24,13 @@ function ProgressBar({dose1, dose2}) {
dose1: 0,
dose2: 0,
},
delay: isMounted.current ? 0 : 2000,
});

useEffect(() => {
isMounted.current = true;
}, []);

return (
<div
className="progress-bar-wrapper fadeInUp"
17 changes: 14 additions & 3 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// export const API_DOMAIN = 'http://192.168.1.69:8080';
// export const API_DOMAIN = 'http://localhost:8080';
// export const API_DOMAIN =
// 'https://raw.githubusercontent.com/shuklaayush/api/test/tmp';
export const API_DOMAIN = 'https://api.covid19india.org';
@@ -54,6 +54,7 @@ export const STATISTIC_CONFIGS = {
format: 'long',
tableConfig: {
showDelta: true,
notes: 'Migrated cases or non-COVID deaths',
},
},
tested: {
@@ -67,7 +68,7 @@ export const STATISTIC_CONFIGS = {
},
},
vaccinated1: {
displayName: 'vaccinated with at least one dose',
displayName: 'vaccinated (at least one dose)',
color: '#fb5581',
format: 'short',
hideZero: true,
@@ -105,7 +106,7 @@ export const STATISTIC_CONFIGS = {
category: 'tested',
tableConfig: {
type: 'delta7',
displayName: 'test positivity ratio (last 7 days)',
notes: 'Calculated over last 7 days',
},
},
cfr: {
@@ -130,6 +131,16 @@ export const STATISTIC_CONFIGS = {
hide: true,
},
},
caseGrowth: {
displayName: 'Case Growth',
format: '%',
nonLinear: true,
canBeInfinite: true,
tableConfig: {
notes:
'Percentage growth of cases last week compared to the week a fortnight ago',
},
},
population: {
displayName: 'population',
format: 'short',
49 changes: 34 additions & 15 deletions src/utils/commonFunctions.js
Original file line number Diff line number Diff line change
@@ -138,11 +138,13 @@ export const getStatistic = (

if (expiredDate !== null) {
if (STATISTIC_CONFIGS[statistic]?.category === 'tested') {
const days = differenceInDays(
expiredDate,
parseIndiaDate(data?.meta?.tested?.['last_updated'])
);
if (days > TESTED_EXPIRING_DAYS) {
if (
!data?.meta?.tested?.['last_updated'] ||
differenceInDays(
expiredDate,
parseIndiaDate(data?.meta?.tested?.['last_updated'])
) > TESTED_EXPIRING_DAYS
) {
return 0;
}
}
@@ -155,11 +157,11 @@ export const getStatistic = (
}

if (normalizedByPopulationPer === 'million') {
multiplyFactor *= 1e6 / data?.meta?.population || 0;
multiplyFactor *= 1e6 / data?.meta?.population;
} else if (normalizedByPopulationPer === 'lakh') {
multiplyFactor *= 1e5 / data?.meta?.population || 0;
multiplyFactor *= 1e5 / data?.meta?.population;
} else if (normalizedByPopulationPer === 'hundred') {
multiplyFactor *= 1e2 / data?.meta?.population || 0;
multiplyFactor *= 1e2 / data?.meta?.population;
}

let val;
@@ -172,7 +174,7 @@ export const getStatistic = (
if (statistic === 'active') {
val = active;
} else if (statistic === 'activeRatio') {
val = (100 * active) / confirmed;
val = 100 * (active / confirmed);
}
} else if (statistic === 'vaccinated') {
const dose1 = data?.[type]?.vaccinated1 || 0;
@@ -181,24 +183,41 @@ export const getStatistic = (
} else if (statistic === 'tpr') {
const confirmed = data?.[type]?.confirmed || 0;
const tested = data?.[type]?.tested || 0;
val = (100 * confirmed) / tested;
val = 100 * (confirmed / tested);
} else if (statistic === 'cfr') {
const deceased = data?.[type]?.deceased || 0;
const confirmed = data?.[type]?.confirmed || 0;
val = (100 * deceased) / confirmed;
val = 100 * (deceased / confirmed);
} else if (statistic === 'recoveryRatio') {
const recovered = data?.[type]?.recovered || 0;
const confirmed = data?.[type]?.confirmed || 0;
val = (100 * recovered) / confirmed;
val = 100 * (recovered / confirmed);
} else if (statistic === 'caseGrowth') {
const confirmedDeltaLastWeek = data?.delta7?.confirmed || 0;
const confirmedDeltaTwoWeeksAgo = data?.delta21_14?.confirmed || 0;
val =
type === 'total'
? 100 *
((confirmedDeltaLastWeek - confirmedDeltaTwoWeeksAgo) /
confirmedDeltaTwoWeeksAgo)
: 0;
} else if (statistic === 'population') {
val = type === 'total' ? data?.meta?.population || 0 : 0;
val = type === 'total' ? data?.meta?.population : 0;
} else {
val = data?.[type]?.[statistic];
}

const isLinear = !STATISTIC_CONFIGS[statistic]?.nonLinear;
const statisticConfig = STATISTIC_CONFIGS[statistic];

return ((isLinear && multiplyFactor) || 1) * (isFinite(val) && val) || 0;
multiplyFactor = (statisticConfig?.nonLinear && 1) || multiplyFactor;

if (statisticConfig?.canBeInfinite) {
val = (!isNaN(val) && val) || 0;
} else {
val = (isFinite(val) && val) || 0;
}

return multiplyFactor * val || 0;
};

export const getTableStatistic = (data, statistic, args) => {
124 changes: 74 additions & 50 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -1219,15 +1219,15 @@
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==

"@eslint/eslintrc@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.1.tgz#442763b88cecbe3ee0ec7ca6d6dd6168550cbf14"
integrity sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ==
"@eslint/eslintrc@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.2.tgz#f63d0ef06f5c0c57d76c4ab5f63d3835c51b0179"
integrity sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==
dependencies:
ajv "^6.12.4"
debug "^4.1.1"
espree "^7.3.0"
globals "^12.1.0"
globals "^13.9.0"
ignore "^4.0.6"
import-fresh "^3.2.1"
js-yaml "^3.13.1"
@@ -1515,6 +1515,11 @@
schema-utils "^2.6.5"
source-map "^0.7.3"

"@popperjs/core@^2.8.3":
version "2.9.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==

"@primer/octicons-react@^14.2.1":
version "14.2.1"
resolved "https://registry.yarnpkg.com/@primer/octicons-react/-/octicons-react-14.2.1.tgz#03a2a68a48e67317c5363078b4847c2cb33582d3"
@@ -1877,6 +1882,13 @@
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^7.28.1"

"@tippyjs/react@^4.2.5":
version "4.2.5"
resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.5.tgz#9b5837db93a1cac953962404df906aef1a18e80d"
integrity sha512-YBLgy+1zznBNbx4JOoOdFXWMLXjBh9hLPwRtq3s8RRdrez2l3tPBRt2m2909wZd9S1KUeKjOOYYsnitccI9I3A==
dependencies:
tippy.js "^6.3.1"

"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -2013,9 +2025,9 @@
integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==

"@types/node@*":
version "15.12.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.0.tgz#6a459d261450a300e6865faeddb5af01c3389bb3"
integrity sha512-+aHJvoCsVhO2ZCuT4o5JtcPrCPyDE3+1nvbDprYes+pPkEsbjH7AGUCNtjMOXS0fqH14t+B7yLzaqSz92FPWyw==
version "15.12.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.1.tgz#9b60797dee1895383a725f828a869c86c6caa5c2"
integrity sha512-zyxJM8I1c9q5sRMtVF+zdd13Jt6RU4r4qfhTd7lQubyThvLfx6yYekWSQjGCGV2Tkecgxnlpl/DNlb6Hg+dmEw==

"@types/normalize-package-data@^2.4.0":
version "2.4.0"
@@ -3469,9 +3481,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"

caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001219:
version "1.0.30001234"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001234.tgz#8fc2e709e3b0679d7af7f073a1c661155c39b975"
integrity sha512-a3gjUVKkmwLdNysa1xkUAwN2VfJUJyVW47rsi3aCbkRCtbHAfo+rOsCqVw29G6coQ8gzAPb5XBXwiGHwme3isA==
version "1.0.30001235"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001235.tgz#ad5ca75bc5a1f7b12df79ad806d715a43a5ac4ed"
integrity sha512-zWEwIVqnzPkSAXOUlQnPW2oKoYb2aLQ4Q5ejdjBcnH63rfypaW34CxaeBn1VMya2XaEU3P/R2qHpWyj+l0BT1A==

capture-exit@^2.0.0:
version "2.0.0"
@@ -3953,27 +3965,27 @@ copy-to-clipboard@^3.3.1:
toggle-selection "^1.0.6"

core-js-compat@^3.6.2, core-js-compat@^3.9.0, core-js-compat@^3.9.1:
version "3.13.1"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.13.1.tgz#05444caa8f153be0c67db03cf8adb8ec0964e58e"
integrity sha512-mdrcxc0WznfRd8ZicEZh1qVeJ2mu6bwQFh8YVUK48friy/FOwFV5EJj9/dlh+nMQ74YusdVfBFDuomKgUspxWQ==
version "3.14.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.14.0.tgz#b574dabf29184681d5b16357bd33d104df3d29a5"
integrity sha512-R4NS2eupxtiJU+VwgkF9WTpnSfZW4pogwKHd8bclWU2sp93Pr5S1uYJI84cMOubJRou7bcfL0vmwtLslWN5p3A==
dependencies:
browserslist "^4.16.6"
semver "7.0.0"

core-js-pure@^3.0.0:
version "3.13.1"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.13.1.tgz#5d139d346780f015f67225f45ee2362a6bed6ba1"
integrity sha512-wVlh0IAi2t1iOEh16y4u1TRk6ubd4KvLE8dlMi+3QUI6SfKphQUh7tAwihGGSQ8affxEXpVIPpOdf9kjR4v4Pw==
version "3.14.0"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.14.0.tgz#72bcfacba74a65ffce04bf94ae91d966e80ee553"
integrity sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g==

core-js@^2.4.0:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==

core-js@^3.6.5:
version "3.13.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.13.1.tgz#30303fabd53638892062d8b4e802cac7599e9fb7"
integrity sha512-JqveUc4igkqwStL2RTRn/EPFGBOfEZHxJl/8ej1mXJR75V3go2mFF4bmUYkEIT1rveHKnkUlcJX/c+f1TyIovQ==
version "3.14.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.14.0.tgz#62322b98c71cc2018b027971a69419e2425c2a6c"
integrity sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==

core-util-is@~1.0.0:
version "1.0.2"
@@ -4363,13 +4375,20 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=

d3-array@2, d3-array@>=2.5, d3-array@^2.11.0, d3-array@^2.3.0:
d3-array@2, d3-array@^2.11.0, d3-array@^2.3.0:
version "2.12.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81"
integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
dependencies:
internmap "^1.0.0"

d3-array@>=2.5:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.0.1.tgz#ca45c263f5bb780ab5a34a6e1d3d5883fe4a8d14"
integrity sha512-l3Bh5o8RSoC3SBm5ix6ogaFW+J6rOUm42yOtZ2sQPCEvCqUMepeX7zgrlLLGIemxgOyo9s2CsWEidnLv5PwwRw==
dependencies:
internmap "1 - 2"

d3-axis@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-2.1.0.tgz#978db534092711117d032fad5d733d206307f6a0"
@@ -4431,9 +4450,9 @@ d3-fetch@^2.0.0:
integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==

d3-format@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.0.0.tgz#49971ce004d9969a2d6eb8385e3bac735a843a2f"
integrity sha512-vzHbrRFaD/Jx1zpiQWojO2Lvgmf7pIsA/D4b6ipd1hxBYzMKsZRsF3i+SWlHz8IpMF4epyKUdG+vhxEYUYB/1g==
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.0.1.tgz#e41b81b2ab79277141ec1404aa5d05001da64084"
integrity sha512-hdL7+HBIohpgfolhBxr1KX47VMD6+vVD/oEFrxk5yhmzV2prk99EkFKYpXuhVkFpTgHdJ6/4bYcjdLPPXV4tIA==

d3-geo@^2.0.1:
version "2.0.1"
@@ -4929,9 +4948,9 @@ ejs@^3.1.5:
jake "^10.6.1"

electron-to-chromium@^1.3.564, electron-to-chromium@^1.3.723:
version "1.3.748"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.748.tgz#16638a8130f407ae5bf2fc168f2173574deb36c5"
integrity sha512-fmIKfYALVeEybk/L2ucdgt7jN3JsbGtg3K9pmF/MRWgkeADBI1VSAa5IzdG2gZwTxsnsrFtdMpOTSM5mrBRKVQ==
version "1.3.749"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.749.tgz#0ecebc529ceb49dd2a7c838ae425236644c3439a"
integrity sha512-F+v2zxZgw/fMwPz/VUGIggG4ZndDsYy0vlpthi3tjmDZlcfbhN5mYW0evXUsBr2sUtuDANFtle410A9u/sd/4A==

elliptic@^6.5.3:
version "6.5.4"
@@ -5464,12 +5483,12 @@ eslint@^2.7.0:
user-home "^2.0.0"

eslint@^7.11.0, eslint@^7.27.0:
version "7.27.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.27.0.tgz#665a1506d8f95655c9274d84bd78f7166b07e9c7"
integrity sha512-JZuR6La2ZF0UD384lcbnd0Cgg6QJjiCwhMD6eU4h/VGPcVGwawNNzKU41tgokGXnfjOOyI6QIffthhJTPzzuRA==
version "7.28.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.28.0.tgz#435aa17a0b82c13bb2be9d51408b617e49c1e820"
integrity sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.1"
"@eslint/eslintrc" "^0.4.2"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
@@ -5486,7 +5505,7 @@ eslint@^7.11.0, eslint@^7.27.0:
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
glob-parent "^5.1.2"
globals "^13.6.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
@@ -5641,9 +5660,9 @@ execa@^4.0.0:
strip-final-newline "^2.0.0"

execa@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.0.tgz#3ea50ee863d226bfa323528cce1684e7481dfe46"
integrity sha512-CkdUB7s2y6S+d4y+OM/+ZtQcJCiKUCth4cNImGMqrt2zEVtW2rfHGspQBE1GDo6LjeNIQmTPKXqTCKjqFKyu3A==
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
dependencies:
cross-spawn "^7.0.3"
get-stream "^6.0.0"
@@ -6227,7 +6246,7 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"

glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0:
glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -6267,14 +6286,7 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==

globals@^12.1.0:
version "12.4.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
dependencies:
type-fest "^0.8.1"

globals@^13.6.0:
globals@^13.6.0, globals@^13.9.0:
version "13.9.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb"
integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==
@@ -6957,6 +6969,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"

"internmap@1 - 2":
version "2.0.1"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.1.tgz#33d0fa016185397549fb1a14ea3dbe5a2949d1cd"
integrity sha512-Ujwccrj9FkGqjbY3iVoxD1VV+KdZZeENx0rphrtzmRXbFvkFO88L80BL/zeSIguX/7T+y8k04xqtgWgS5vxwxw==

internmap@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
@@ -10218,9 +10235,9 @@ prettier-linter-helpers@^1.0.0:
fast-diff "^1.1.2"

prettier@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
version "2.3.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==

pretty-bytes@^5.3.0:
version "5.6.0"
@@ -11232,9 +11249,9 @@ rollup@^1.31.1:
acorn "^7.1.0"

rollup@^2.43.1, rollup@^2.50.6:
version "2.50.6"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.50.6.tgz#24e2211caf9031081656e98a5e5e94d3b5e786e2"
integrity sha512-6c5CJPLVgo0iNaZWWliNu1Kl43tjP9LZcp6D/tkf2eLH2a9/WeHxg9vfTFl8QV/2SOyaJX37CEm9XuGM0rviUg==
version "2.51.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.51.0.tgz#ffd847882283998fc8611cd57af917f173b4ab5c"
integrity sha512-ITLt9sScNCBVspSHauw/W49lEZ0vjN8LyCzSNsNaqT67vTss2lYEfOyxltX8hjrhr1l/rQwmZ2wazzEqhZ/fUg==
optionalDependencies:
fsevents "~2.3.1"

@@ -12490,6 +12507,13 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3:
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==

tippy.js@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.1.tgz#3788a007be7015eee0fd589a66b98fb3f8f10181"
integrity sha512-JnFncCq+rF1dTURupoJ4yPie5Cof978inW6/4S6kmWV7LL9YOSEVMifED3KdrVPEG+Z/TFH2CDNJcQEfaeuQww==
dependencies:
"@popperjs/core" "^2.8.3"

tmpl@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"