separated case/death graphs, removed trend graph

This commit is contained in:
tmont 2020-11-15 16:39:38 -08:00
parent 6f762b0524
commit 8566acc2a6
2 changed files with 176 additions and 160 deletions

View File

@ -124,6 +124,10 @@ const processRecords = async () => {
}); });
console.log(`parsed US states population CSV in ${Date.now() - start}ms`); console.log(`parsed US states population CSV in ${Date.now() - start}ms`);
// let tsCasesUS = [];
// let populationUSStateRecords = [];
// let tsUSRecords = [];
start = Date.now(); start = Date.now();
let populationCountriesRecords = parseCsv(populationCountriesRaw, { let populationCountriesRecords = parseCsv(populationCountriesRaw, {
cast: true, cast: true,
@ -176,7 +180,7 @@ const processRecords = async () => {
} }
// tsGlobalRecords = tsGlobalRecords.filter((record) => { // tsGlobalRecords = tsGlobalRecords.filter((record) => {
// return record['Country/Region'] === 'US'; // return record['Country/Region'] === 'Germany';
// }); // });
const getRollingAverage = (item) => { const getRollingAverage = (item) => {

View File

@ -29,13 +29,13 @@ html
bottom: -1px; bottom: -1px;
border-bottom: 2px solid #b5b5b5; border-bottom: 2px solid #b5b5b5;
} }
#hero-tooltip table { .hero-tooltip table {
border-collapse: collapse; border-collapse: collapse;
} }
#hero-tooltip th, #hero-tooltip td { .hero-tooltip th, .hero-tooltip td {
padding: 2px 4px; padding: 2px 4px;
} }
#hero-tooltip { .hero-tooltip {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -135,83 +135,84 @@ html
const charts = { const charts = {
logarithmic: false, logarithmic: false,
heroChart: null, heroCharts: [],
trends: [], trends: [],
heroMaxValues: [],
}; };
function makeSparkline(id, deathData, caseData) { function makeSparkline(id, deathData, caseData) {
const canvas = document.getElementById(id); // const canvas = document.getElementById(id);
const maxValue = caseData.reduce((max, value) => Math.max(max, value), 0); // const maxValue = caseData.reduce((max, value) => Math.max(max, value), 0);
const max = maxValue > 0 ? Math.pow(10, Math.ceil(Math.log10(maxValue))) : 0 // const max = maxValue > 0 ? Math.pow(10, Math.ceil(Math.log10(maxValue))) : 0
const chart = new Chart(canvas.getContext('2d'), { // const chart = new Chart(canvas.getContext('2d'), {
type: 'line', // type: 'line',
data: { // data: {
labels: new Array(deathData.length), // labels: new Array(deathData.length),
datasets: [ // datasets: [
{ // {
data: caseData, // data: caseData,
borderColor: 'rgb(161,150,20)', // borderColor: 'rgb(161,150,20)',
borderWidth: 1, // borderWidth: 1,
backgroundColor: 'rgba(161,150,20, 0.25)', // backgroundColor: 'rgba(161,150,20, 0.25)',
fill: '1', // fill: '1',
}, // },
{ // {
data: deathData, // data: deathData,
borderColor: 'rgb(196, 64, 64)', // borderColor: 'rgb(196, 64, 64)',
borderWidth: 1, // borderWidth: 1,
backgroundColor: 'rgba(196, 64, 64, 0.25)', // backgroundColor: 'rgba(196, 64, 64, 0.25)',
fill: 'origin', // fill: 'origin',
}, // },
], // ],
}, // },
options: { // options: {
responsive: false, // responsive: false,
legend: { // legend: {
display: false, // display: false,
}, // },
elements: { // elements: {
point: { // point: {
radius: 0, // radius: 0,
}, // },
}, // },
tooltips: { // tooltips: {
enabled: false, // enabled: false,
}, // },
scales: { // scales: {
yAxes: [ // yAxes: [
{ // {
display: false, // display: false,
type: 'logarithmic', // type: 'logarithmic',
ticks: { // ticks: {
precision: 0, // precision: 0,
beginAtZero: true, // beginAtZero: true,
min: 0, // min: 0,
max: Math.max(max, 2), // this is necessary for some reason // max: Math.max(max, 2), // this is necessary for some reason
callback: value => Number(value.toString()), // callback: value => Number(value.toString()),
} // }
}, // },
], // ],
xAxes: [ // xAxes: [
{ // {
display: false, // display: false,
}, // },
], // ],
} // }
} // }
}); // });
charts.trends.push({ // charts.trends.push({
chart, // chart,
maxValue, // maxValue,
}); // });
} }
function setAxisType(type) { function setAxisType(type) {
if (charts.heroChart) { charts.heroCharts.forEach((chart, i) => {
const axis = charts.heroChart.options.scales.yAxes[0]; const axis = chart.options.scales.yAxes[0];
if (type === 'logarithmic') { if (type === 'logarithmic') {
axis.type = 'logarithmic'; axis.type = 'logarithmic';
const maxLogPower = Math.ceil(Math.log10(charts.heroMaxValue)); const maxLogPower = Math.ceil(Math.log10(charts.heroMaxValues[i]));
axis.ticks.max = Math.pow(10, maxLogPower); axis.ticks.max = Math.pow(10, maxLogPower);
axis.ticks.callback = value => Number(value.toString()).toLocaleString(); axis.ticks.callback = value => Number(value.toString()).toLocaleString();
} else { } else {
@ -220,7 +221,7 @@ html
axis.ticks.callback = value => value; axis.ticks.callback = value => value;
} }
charts.heroChart.update(); chart.update();
const selector = '.set-axis-' + type; const selector = '.set-axis-' + type;
const otherSelector = type === 'linear' ? const otherSelector = type === 'linear' ?
@ -229,7 +230,7 @@ html
document.querySelector(selector).disabled = true; document.querySelector(selector).disabled = true;
document.querySelector(otherSelector).disabled = false; document.querySelector(otherSelector).disabled = false;
} });
charts.trends.forEach((data) => { charts.trends.forEach((data) => {
const axis = data.chart.options.scales.yAxes[0]; const axis = data.chart.options.scales.yAxes[0];
@ -250,26 +251,24 @@ html
function makeHeroChart( function makeHeroChart(
id, id,
type,
title, title,
labels, labels,
totalDeaths, cumulative,
newDeaths, daily,
rollingAverage, rollingAverage,
totalCases,
newCases,
) { ) {
const canvas = document.getElementById(id); const canvas = document.getElementById(id);
charts.heroMaxValue = totalCases.reduce((max, value) => Math.max(max, value), 0); const maxValue = cumulative.reduce((max, value) => Math.max(max, value), 0);
charts.heroMaxValues.push(maxValue);
const firstNonZeroDeathIndex = totalDeaths.findIndex(value => value > 0); const firstNonZeroDeathIndex = cumulative.findIndex(value => value > 0);
const start = Math.max(0, firstNonZeroDeathIndex - 2); const start = Math.max(0, firstNonZeroDeathIndex - 2);
const end = totalDeaths.length; const end = cumulative.length;
const totalData = totalDeaths.slice(start, end); const totalData = cumulative.slice(start, end);
const newData = newDeaths.slice(start, end); const newData = daily.slice(start, end);
const rollingData = rollingAverage.slice(start, end); const rollingData = rollingAverage.slice(start, end);
const totalCaseData = totalCases.slice(start, end);
const newCaseData = newCases.slice(start, end);
const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
@ -277,52 +276,45 @@ html
.slice(start, end) .slice(start, end)
.map((date) => date.replace(/\d{4}-(\d\d?)-(\d\d?)/, (_, m, d) => `${months[Number(m) - 1]} ${Number(d)}`)); .map((date) => date.replace(/\d{4}-(\d\d?)-(\d\d?)/, (_, m, d) => `${months[Number(m) - 1]} ${Number(d)}`));
charts.heroChart = new Chart(canvas.getContext('2d'), { const pointRadius = 1;
const totalColor = type === 'cases' ? [220, 126, 26 ] : [194, 57, 57];
const dailyColor = type === 'cases' ? [83, 83, 83 ] : [83, 83, 83 ];
const rollingColor = type === 'cases' ? [187, 178, 16] : [208, 105, 167];
charts.heroCharts.push(new Chart(canvas.getContext('2d'), {
type: 'lineVerticalTooltip', type: 'lineVerticalTooltip',
data: { data: {
labels: realLabels, labels: realLabels,
datasets: [ datasets: [
{ {
label: 'Cases', label: 'Cumulative',
data: totalCaseData,
fill: '2',
borderColor: 'rgb(161,150,20)',
backgroundColor: 'rgba(161,150,20, 0.25)',
borderWidth: 1,
},
{
label: 'New Cases',
data: newCaseData,
fill: false,
borderColor: 'rgba(206,108,46,0.5)',
backgroundColor: 'rgba(206,108,46,0.5)',
borderWidth: 1,
pointRadius: 0,
},
{
label: 'Deaths',
data: totalData, data: totalData,
fill: '3', fill: '2',
borderColor: 'rgb(196, 64, 64)', borderColor: 'rgba(' + totalColor.join(',') + ')',
backgroundColor: 'rgba(196, 64, 64, 0.25)', backgroundColor: 'rgba(' + totalColor.join(',') + ',0.25)',
borderWidth: 1, borderWidth: 1,
pointRadius,
}, },
{ {
label: 'New Deaths (rolling)', label: 'New',
data: rollingData,
fill: 'origin',
borderColor: 'rgb(20,24,59)',
backgroundColor: 'rgba(96, 96, 164, 0.25)',
borderWidth: 1,
},
{
label: 'New Deaths',
data: newData, data: newData,
fill: false, fill: false,
borderColor: 'rgb(20,24,59, 0.15)', borderColor: 'rgba(' + dailyColor.join(',') + ',0.15)',
backgroundColor: 'rgb(20,24,59, 0.15)', backgroundColor: 'rgba(' + dailyColor.join(',') + ',0.15)',
borderWidth: 1, borderWidth: 1,
pointRadius,
}, },
{
label: 'New (7 day avg.)',
data: rollingData,
fill: 'origin',
borderColor: 'rgba(' + rollingColor.join(',') + ')',
backgroundColor: 'rgba(' + rollingColor.join(',') + ',0.25)',
borderWidth: 1,
pointRadius,
},
], ],
}, },
options: { options: {
@ -338,7 +330,7 @@ html
axis: 'x', axis: 'x',
enabled: false, enabled: false,
custom: function(tooltipModel) { custom: function(tooltipModel) {
const tooltipEl = document.getElementById('hero-tooltip'); const tooltipEl = document.getElementById('hero-tooltip-' + type);
if (tooltipModel.opacity === 0) { if (tooltipModel.opacity === 0) {
tooltipEl.style.opacity = '0'; tooltipEl.style.opacity = '0';
@ -367,10 +359,8 @@ html
} }
}; };
setData('cases-total', 0); setData(type + '-total', 0);
setData('cases-new', 1); setData(type + '-new', 1, 2);
setData('deaths-total', 2);
setData('deaths-new', 4, 3);
} }
const position = this._chart.canvas.getBoundingClientRect(); const position = this._chart.canvas.getBoundingClientRect();
@ -397,7 +387,7 @@ html
precision: 0, precision: 0,
beginAtZero: true, beginAtZero: true,
min: 0, min: 0,
max: Math.pow(10, Math.ceil(Math.log10(charts.heroMaxValue))), max: Math.pow(10, Math.ceil(Math.log10(maxValue))),
callback: value => Number(value.toString()).toLocaleString(), callback: value => Number(value.toString()).toLocaleString(),
}, },
afterBuildTicks: (axis, ticks) => { afterBuildTicks: (axis, ticks) => {
@ -419,7 +409,7 @@ html
], ],
} }
} }
}); }));
} }
body body
mixin formatNumber(num) mixin formatNumber(num)
@ -437,21 +427,11 @@ html
mixin heroChart() mixin heroChart()
div.card.mb-4 div.card.mb-4
div.card-body.position-relative div.card-body.position-relative
div.position-absolute(style="top: 10px; right: 10px; z-index: 2;") div.row
div.btn-group.btn-group-sm div.col-12.col-lg-6.position-relative
button.btn.btn-secondary.set-axis-linear( canvas.mx-auto(id="main-chart-cases" width="512" height="288")
type="button" div.col-12.col-lg-6.position-relative
onclick="setAxisType('linear')" canvas.mx-auto(id="main-chart-deaths" width="512" height="288")
autocomplete="off"
) Linear
button.btn.btn-secondary.set-axis-logarithmic(
type="button"
onclick="setAxisType('logarithmic')"
autocomplete="off"
disabled
) Logarithmic
div.mx-auto.position-relative(style="max-width: 1024px; z-index: 1")
canvas.mx-auto(id="main-chart" width="1024" height="576")
- -
const population = 'pop. ' + data.population.toLocaleString(); const population = 'pop. ' + data.population.toLocaleString();
const deathsPerMillion = Math.round(data.deathsPerMillion).toLocaleString() + '/1M'; const deathsPerMillion = Math.round(data.deathsPerMillion).toLocaleString() + '/1M';
@ -465,21 +445,32 @@ html
if (data.country) { if (data.country) {
name += ', ' + data.country; name += ', ' + data.country;
} }
const heroTitle = [ const heroCasesTitle = [
`Covid-19: ${name} (${population})`, `Covid-19: ${name} (${population})`,
`${totalCases} cases (${casesPerMillion})`, `${totalCases} cases (${casesPerMillion})`,
];
const heroDeathsTitle = [
`Covid-19: ${name} (${population})`,
`${totalDeaths} deaths (${deathsPerMillion})`, `${totalDeaths} deaths (${deathsPerMillion})`,
]; ];
script. script.
makeHeroChart( makeHeroChart(
'main-chart', 'main-chart-cases',
!{JSON.stringify(heroTitle)}, 'cases',
!{JSON.stringify(heroCasesTitle)},
!{JSON.stringify(data.cases.timeSeriesDaily.map(x => x.key))},
!{JSON.stringify(data.cases.timeSeriesDaily.map(x => x.value))},
!{JSON.stringify(data.cases.timeSeriesDaily.map(x => x.delta))},
!{JSON.stringify(data.cases.rollingAverageDaily.map(x => x.delta))},
);
makeHeroChart(
'main-chart-deaths',
'deaths',
!{JSON.stringify(heroDeathsTitle)},
!{JSON.stringify(data.timeSeriesDaily.map(x => x.key))}, !{JSON.stringify(data.timeSeriesDaily.map(x => x.key))},
!{JSON.stringify(data.timeSeriesDaily.map(x => x.value))}, !{JSON.stringify(data.timeSeriesDaily.map(x => x.value))},
!{JSON.stringify(data.timeSeriesDaily.map(x => x.delta))}, !{JSON.stringify(data.timeSeriesDaily.map(x => x.delta))},
!{JSON.stringify(data.rollingAverageDaily.map(x => x.delta))}, !{JSON.stringify(data.rollingAverageDaily.map(x => x.delta))},
!{JSON.stringify(data.cases.timeSeriesDaily.map(x => x.value))},
!{JSON.stringify(data.cases.timeSeriesDaily.map(x => x.delta))},
); );
mixin dataTable(items, label, type) mixin dataTable(items, label, type)
@ -519,10 +510,11 @@ html
th.other-bg(data-col="cfr"): +sortableLinks("cfr") th.other-bg(data-col="cfr"): +sortableLinks("cfr")
acronym(title="Case Fatality Rate") CFR acronym(title="Case Fatality Rate") CFR
th.text-center.other-bg Trend //th.text-center.other-bg Trend
- -
items.sort((a, b) => { items.sort((a, b) => {
try {
const yesterdayA = a.timeSeriesDaily[a.timeSeriesDaily.length - 1].delta; const yesterdayA = a.timeSeriesDaily[a.timeSeriesDaily.length - 1].delta;
const yesterdayB = b.timeSeriesDaily[b.timeSeriesDaily.length - 1].delta; const yesterdayB = b.timeSeriesDaily[b.timeSeriesDaily.length - 1].delta;
if (yesterdayA === yesterdayB) { if (yesterdayA === yesterdayB) {
@ -530,6 +522,9 @@ html
} }
return yesterdayA < yesterdayB ? 1 : -1; return yesterdayA < yesterdayB ? 1 : -1;
} catch (e) {
return 0;
}
}); });
tbody: each item, i in items tbody: each item, i in items
@ -592,14 +587,14 @@ html
if hasPopulation if hasPopulation
td.text-right: code: +formatNumber(Math.round(last14Avg)) td.text-right: code: +formatNumber(Math.round(last14Avg))
td.text-right: code= Number(item.caseFatalityRate * 100).toFixed(2) + '%' td.text-right: code= Number(item.caseFatalityRate * 100).toFixed(2) + '%'
td //td
canvas.mx-auto(id="sparkline-" + i width="100" height="35") // canvas.mx-auto(id="sparkline-" + i width="100" height="35")
script. // script.
makeSparkline( // makeSparkline(
"sparkline-#{i}", // "sparkline-#{i}",
#{JSON.stringify(item.rollingAverageDaily.slice(-14).map(x => x.delta))}, // #{JSON.stringify(item.rollingAverageDaily.slice(-14).map(x => x.delta))},
#{JSON.stringify(item.cases.rollingAverageDaily.slice(-14).map(x => x.delta))}, // #{JSON.stringify(item.cases.rollingAverageDaily.slice(-14).map(x => x.delta))},
); // );
div.container-fluid.mt-2(style="max-width: 1600px") div.container-fluid.mt-2(style="max-width: 1600px")
h1.text-center Covid-19 Data h1.text-center Covid-19 Data
@ -616,9 +611,22 @@ html
hr hr
div.main-content.mt-4.position-relative div.main-content.mt-4.position-relative
div.position-absolute(style="z-index: 2; left: 50%; transform: translateX(-50%) translateY(12%);")
div.btn-group.btn-group-sm
button.btn.btn-secondary.set-axis-linear(
type="button"
onclick="setAxisType('linear')"
autocomplete="off"
) Linear
button.btn.btn-secondary.set-axis-logarithmic(
type="button"
onclick="setAxisType('logarithmic')"
autocomplete="off"
disabled
) Logarithmic
block main block main
div#hero-tooltip div#hero-tooltip-cases.hero-tooltip
div.text-center(style="font-size: 125%"): strong.tooltip-title div.text-center(style="font-size: 125%"): strong.tooltip-title
table.tooltip-bordered table.tooltip-bordered
tr tr
@ -629,6 +637,10 @@ html
td: span.tooltip-color-cases-new td: span.tooltip-color-cases-new
th New Cases th New Cases
td.tooltip-value-cases-new td.tooltip-value-cases-new
div#hero-tooltip-deaths.hero-tooltip
div.text-center(style="font-size: 125%"): strong.tooltip-title
table.tooltip-bordered
tr tr
td: span.tooltip-color-deaths-total td: span.tooltip-color-deaths-total
th Total Deaths th Total Deaths