diff --git a/generate.js b/generate.js index 9c64939..a1e59ed 100755 --- a/generate.js +++ b/generate.js @@ -124,6 +124,10 @@ const processRecords = async () => { }); console.log(`parsed US states population CSV in ${Date.now() - start}ms`); + // let tsCasesUS = []; + // let populationUSStateRecords = []; + // let tsUSRecords = []; + start = Date.now(); let populationCountriesRecords = parseCsv(populationCountriesRaw, { cast: true, @@ -176,7 +180,7 @@ const processRecords = async () => { } // tsGlobalRecords = tsGlobalRecords.filter((record) => { - // return record['Country/Region'] === 'US'; + // return record['Country/Region'] === 'Germany'; // }); const getRollingAverage = (item) => { diff --git a/tmpl/master.pug b/tmpl/master.pug index 039919c..1f9068e 100644 --- a/tmpl/master.pug +++ b/tmpl/master.pug @@ -29,13 +29,13 @@ html bottom: -1px; border-bottom: 2px solid #b5b5b5; } - #hero-tooltip table { + .hero-tooltip table { border-collapse: collapse; } - #hero-tooltip th, #hero-tooltip td { + .hero-tooltip th, .hero-tooltip td { padding: 2px 4px; } - #hero-tooltip { + .hero-tooltip { position: absolute; top: 0; left: 0; @@ -135,83 +135,84 @@ html const charts = { logarithmic: false, - heroChart: null, + heroCharts: [], trends: [], + heroMaxValues: [], }; function makeSparkline(id, deathData, caseData) { - const canvas = document.getElementById(id); - 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 chart = new Chart(canvas.getContext('2d'), { - type: 'line', - data: { - labels: new Array(deathData.length), - datasets: [ - { - data: caseData, - borderColor: 'rgb(161,150,20)', - borderWidth: 1, - backgroundColor: 'rgba(161,150,20, 0.25)', - fill: '1', - }, - { - data: deathData, - borderColor: 'rgb(196, 64, 64)', - borderWidth: 1, - backgroundColor: 'rgba(196, 64, 64, 0.25)', - fill: 'origin', - }, - ], - }, - options: { - responsive: false, - legend: { - display: false, - }, - elements: { - point: { - radius: 0, - }, - }, - tooltips: { - enabled: false, - }, - scales: { - yAxes: [ - { - display: false, - type: 'logarithmic', - ticks: { - precision: 0, - beginAtZero: true, - min: 0, - max: Math.max(max, 2), // this is necessary for some reason - callback: value => Number(value.toString()), - } - }, - ], - xAxes: [ - { - display: false, - }, - ], - } - } - }); + // const canvas = document.getElementById(id); + // 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 chart = new Chart(canvas.getContext('2d'), { + // type: 'line', + // data: { + // labels: new Array(deathData.length), + // datasets: [ + // { + // data: caseData, + // borderColor: 'rgb(161,150,20)', + // borderWidth: 1, + // backgroundColor: 'rgba(161,150,20, 0.25)', + // fill: '1', + // }, + // { + // data: deathData, + // borderColor: 'rgb(196, 64, 64)', + // borderWidth: 1, + // backgroundColor: 'rgba(196, 64, 64, 0.25)', + // fill: 'origin', + // }, + // ], + // }, + // options: { + // responsive: false, + // legend: { + // display: false, + // }, + // elements: { + // point: { + // radius: 0, + // }, + // }, + // tooltips: { + // enabled: false, + // }, + // scales: { + // yAxes: [ + // { + // display: false, + // type: 'logarithmic', + // ticks: { + // precision: 0, + // beginAtZero: true, + // min: 0, + // max: Math.max(max, 2), // this is necessary for some reason + // callback: value => Number(value.toString()), + // } + // }, + // ], + // xAxes: [ + // { + // display: false, + // }, + // ], + // } + // } + // }); - charts.trends.push({ - chart, - maxValue, - }); + // charts.trends.push({ + // chart, + // maxValue, + // }); } function setAxisType(type) { - if (charts.heroChart) { - const axis = charts.heroChart.options.scales.yAxes[0]; + charts.heroCharts.forEach((chart, i) => { + const axis = chart.options.scales.yAxes[0]; if (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.callback = value => Number(value.toString()).toLocaleString(); } else { @@ -220,7 +221,7 @@ html axis.ticks.callback = value => value; } - charts.heroChart.update(); + chart.update(); const selector = '.set-axis-' + type; const otherSelector = type === 'linear' ? @@ -229,7 +230,7 @@ html document.querySelector(selector).disabled = true; document.querySelector(otherSelector).disabled = false; - } + }); charts.trends.forEach((data) => { const axis = data.chart.options.scales.yAxes[0]; @@ -250,26 +251,24 @@ html function makeHeroChart( id, + type, title, labels, - totalDeaths, - newDeaths, + cumulative, + daily, rollingAverage, - totalCases, - newCases, ) { 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 end = totalDeaths.length; + const end = cumulative.length; - const totalData = totalDeaths.slice(start, end); - const newData = newDeaths.slice(start, end); + const totalData = cumulative.slice(start, end); + const newData = daily.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' ]; @@ -277,52 +276,45 @@ html .slice(start, end) .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', data: { labels: realLabels, datasets: [ { - label: 'Cases', - 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', + label: 'Cumulative', data: totalData, - fill: '3', - borderColor: 'rgb(196, 64, 64)', - backgroundColor: 'rgba(196, 64, 64, 0.25)', + fill: '2', + borderColor: 'rgba(' + totalColor.join(',') + ')', + backgroundColor: 'rgba(' + totalColor.join(',') + ',0.25)', borderWidth: 1, + pointRadius, }, { - label: 'New Deaths (rolling)', - data: rollingData, - fill: 'origin', - borderColor: 'rgb(20,24,59)', - backgroundColor: 'rgba(96, 96, 164, 0.25)', - borderWidth: 1, - }, - { - label: 'New Deaths', + label: 'New', data: newData, fill: false, - borderColor: 'rgb(20,24,59, 0.15)', - backgroundColor: 'rgb(20,24,59, 0.15)', + borderColor: 'rgba(' + dailyColor.join(',') + ',0.15)', + backgroundColor: 'rgba(' + dailyColor.join(',') + ',0.15)', 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: { @@ -338,7 +330,7 @@ html axis: 'x', enabled: false, custom: function(tooltipModel) { - const tooltipEl = document.getElementById('hero-tooltip'); + const tooltipEl = document.getElementById('hero-tooltip-' + type); if (tooltipModel.opacity === 0) { tooltipEl.style.opacity = '0'; @@ -367,10 +359,8 @@ html } }; - setData('cases-total', 0); - setData('cases-new', 1); - setData('deaths-total', 2); - setData('deaths-new', 4, 3); + setData(type + '-total', 0); + setData(type + '-new', 1, 2); } const position = this._chart.canvas.getBoundingClientRect(); @@ -397,7 +387,7 @@ html precision: 0, beginAtZero: true, 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(), }, afterBuildTicks: (axis, ticks) => { @@ -419,7 +409,7 @@ html ], } } - }); + })); } body mixin formatNumber(num) @@ -437,21 +427,11 @@ html mixin heroChart() div.card.mb-4 div.card-body.position-relative - div.position-absolute(style="top: 10px; right: 10px; z-index: 2;") - 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 - div.mx-auto.position-relative(style="max-width: 1024px; z-index: 1") - canvas.mx-auto(id="main-chart" width="1024" height="576") + div.row + div.col-12.col-lg-6.position-relative + canvas.mx-auto(id="main-chart-cases" width="512" height="288") + div.col-12.col-lg-6.position-relative + canvas.mx-auto(id="main-chart-deaths" width="512" height="288") - const population = 'pop. ' + data.population.toLocaleString(); const deathsPerMillion = Math.round(data.deathsPerMillion).toLocaleString() + '/1M'; @@ -465,21 +445,32 @@ html if (data.country) { name += ', ' + data.country; } - const heroTitle = [ + const heroCasesTitle = [ `Covid-19: ${name} (${population})`, `${totalCases} cases (${casesPerMillion})`, + ]; + const heroDeathsTitle = [ + `Covid-19: ${name} (${population})`, `${totalDeaths} deaths (${deathsPerMillion})`, ]; script. makeHeroChart( - 'main-chart', - !{JSON.stringify(heroTitle)}, + 'main-chart-cases', + '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.value))}, !{JSON.stringify(data.timeSeriesDaily.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) @@ -519,17 +510,21 @@ html th.other-bg(data-col="cfr"): +sortableLinks("cfr") acronym(title="Case Fatality Rate") CFR - th.text-center.other-bg Trend + //th.text-center.other-bg Trend - items.sort((a, b) => { - const yesterdayA = a.timeSeriesDaily[a.timeSeriesDaily.length - 1].delta; - const yesterdayB = b.timeSeriesDaily[b.timeSeriesDaily.length - 1].delta; - if (yesterdayA === yesterdayB) { - return a.name && b.name ? a.name.localeCompare(b.name) : 0; - } + try { + const yesterdayA = a.timeSeriesDaily[a.timeSeriesDaily.length - 1].delta; + const yesterdayB = b.timeSeriesDaily[b.timeSeriesDaily.length - 1].delta; + if (yesterdayA === yesterdayB) { + return a.name && b.name ? a.name.localeCompare(b.name) : 0; + } - return yesterdayA < yesterdayB ? 1 : -1; + return yesterdayA < yesterdayB ? 1 : -1; + } catch (e) { + return 0; + } }); tbody: each item, i in items @@ -592,14 +587,14 @@ html if hasPopulation td.text-right: code: +formatNumber(Math.round(last14Avg)) td.text-right: code= Number(item.caseFatalityRate * 100).toFixed(2) + '%' - td - canvas.mx-auto(id="sparkline-" + i width="100" height="35") - script. - makeSparkline( - "sparkline-#{i}", - #{JSON.stringify(item.rollingAverageDaily.slice(-14).map(x => x.delta))}, - #{JSON.stringify(item.cases.rollingAverageDaily.slice(-14).map(x => x.delta))}, - ); + //td + // canvas.mx-auto(id="sparkline-" + i width="100" height="35") + // script. + // makeSparkline( + // "sparkline-#{i}", + // #{JSON.stringify(item.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") h1.text-center Covid-19 Data @@ -616,9 +611,22 @@ html hr 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 - div#hero-tooltip + div#hero-tooltip-cases.hero-tooltip div.text-center(style="font-size: 125%"): strong.tooltip-title table.tooltip-bordered tr @@ -629,6 +637,10 @@ html td: span.tooltip-color-cases-new th New Cases 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 td: span.tooltip-color-deaths-total th Total Deaths