From c08e7373e283589e14b40df3e4a320c42957d169 Mon Sep 17 00:00:00 2001 From: tmont Date: Sat, 2 May 2020 18:57:59 -0700 Subject: [PATCH] added more case data, table styling --- generate.js | 35 +++++++++++++++---- tmpl/master.pug | 92 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 93 insertions(+), 34 deletions(-) diff --git a/generate.js b/generate.js index d580f7d..8a8c25f 100755 --- a/generate.js +++ b/generate.js @@ -235,8 +235,8 @@ const processRecords = async () => { return Math.pow((hi / lo), pow) - 1; }; - const getGrowthRate = (record) => { - return calcGrowthRate(record.timeSeriesDaily, record.timeSeriesDaily.length - 1, 7); + const getGrowthRate = (ts) => { + return calcGrowthRate(ts, ts.length - 1, 7); }; const normalizeRecord = (record) => { @@ -404,6 +404,7 @@ const processRecords = async () => { } record.cases = { + total: confirmedCases.timeSeriesDaily[confirmedCases.timeSeriesDaily.length - 1].value, timeSeriesDaily: confirmedCases.timeSeriesDaily, timeSeriesMonthly: confirmedCases.timeSeriesMonthly, rollingAverageDaily: confirmedCases.rollingAverageDaily, @@ -428,7 +429,9 @@ const processRecords = async () => { } record.deathsPerMillion = !!record.population ? record.total / record.population * 1000000 : 0; - record.deathGrowthRate = getGrowthRate(record); + record.casesPerMillion = !!record.population ? record.cases.total / record.population * 1000000 : 0; + record.deathGrowthRate = getGrowthRate(record.timeSeriesDaily); + record.caseGrowthRate = getGrowthRate(record.cases.timeSeriesDaily); }); tsGlobalRecords.sort((a, b) => { @@ -447,9 +450,12 @@ const processRecords = async () => { population: 0, timeSeriesDaily: {}, timeSeriesMonthly: {}, + deathsPerMillion: 0, + casesPerMillion: 0, states: [], safeName: record.countrySafeName, cases: { + total: 0, timeSeriesDaily: {}, timeSeriesMonthly: {}, }, @@ -479,9 +485,11 @@ const processRecords = async () => { total: 0, population: 0, deathsPerMillion: 0, + casesPerMillion: 0, timeSeriesDaily: {}, timeSeriesMonthly: {}, cases: { + total: 0, timeSeriesDaily: {}, timeSeriesMonthly: {}, }, @@ -499,6 +507,7 @@ const processRecords = async () => { } stateItem.total += record.total; + stateItem.cases.total += record.cases.total; record.timeSeriesDaily.forEach((ts) => { stateItem.timeSeriesDaily[ts.key] = stateItem.timeSeriesDaily[ts.key] || { value: 0, @@ -545,6 +554,8 @@ const processRecords = async () => { } item.total += record.total; + item.cases.total += record.cases.total; + record.timeSeriesDaily.forEach((ts) => { item.timeSeriesDaily[ts.key] = item.timeSeriesDaily[ts.key] || { value: 0, @@ -596,6 +607,7 @@ const processRecords = async () => { counties: item.counties, population: item.population, deathsPerMillion: item.population > 0 ? item.total / item.population * 1000000 : 0, + casesPerMillion: item.population > 0 ? item.cases.total / item.population * 1000000 : 0, timeSeriesDaily: Object.keys(item.timeSeriesDaily).sort().map((date) => { return { key: date, @@ -611,6 +623,7 @@ const processRecords = async () => { }; }), cases: { + total: item.cases.total, timeSeriesDaily: Object.keys(item.cases.timeSeriesDaily).sort().map((date) => { return { key: date, @@ -628,7 +641,8 @@ const processRecords = async () => { }, }; - stateItem.deathGrowthRate = getGrowthRate(stateItem); + stateItem.deathGrowthRate = getGrowthRate(stateItem.timeSeriesDaily); + stateItem.caseGrowthRate = getGrowthRate(stateItem.cases.timeSeriesDaily); stateItem.rollingAverageDaily = getRollingAverage(stateItem); stateItem.doublingDaily = getDoublingTime(stateItem); stateItem.cases.rollingAverageDaily = getRollingAverage(stateItem.cases); @@ -657,6 +671,7 @@ const processRecords = async () => { states: item.states, population: item.population, deathsPerMillion: item.population > 0 ? item.total / item.population * 1000000 : 0, + casesPerMillion: item.population > 0 ? item.cases.total / item.population * 1000000 : 0, timeSeriesDaily: Object.keys(item.timeSeriesDaily).sort().map((date) => { return { key: date, @@ -672,6 +687,7 @@ const processRecords = async () => { }; }), cases: { + total: item.cases.total, timeSeriesDaily: Object.keys(item.cases.timeSeriesDaily).sort().map((date) => { return { key: date, @@ -689,7 +705,8 @@ const processRecords = async () => { }, }; - countryItem.deathGrowthRate = getGrowthRate(countryItem); + countryItem.deathGrowthRate = getGrowthRate(countryItem.timeSeriesDaily); + countryItem.caseGrowthRate = getGrowthRate(countryItem.cases.timeSeriesDaily); countryItem.rollingAverageDaily = getRollingAverage(countryItem); countryItem.doublingDaily = getDoublingTime(countryItem); countryItem.cases.rollingAverageDaily = getRollingAverage(countryItem.cases); @@ -704,6 +721,7 @@ const processRecords = async () => { timeSeriesDaily: {}, timeSeriesMonthly: {}, cases: { + total: 0, timeSeriesDaily: {}, timeSeriesMonthly: {}, }, @@ -711,6 +729,7 @@ const processRecords = async () => { countryArr.forEach((countryData) => { worldData.total += countryData.total; + worldData.cases.total += countryData.cases.total; countryData.timeSeriesDaily.forEach((ts) => { worldData.timeSeriesDaily[ts.key] = worldData.timeSeriesDaily[ts.key] || { @@ -778,13 +797,15 @@ const processRecords = async () => { }; }); - worldData.deathGrowthRate = getGrowthRate(worldData); + worldData.deathGrowthRate = getGrowthRate(worldData.timeSeriesDaily); + worldData.casesGrowthRate = getGrowthRate(worldData.cases.timeSeriesDaily); worldData.rollingAverageDaily = getRollingAverage(worldData); worldData.doublingDaily = getDoublingTime(worldData); worldData.cases.rollingAverageDaily = getRollingAverage(worldData.cases); worldData.population = 7781841000; worldData.deathsPerMillion = worldData.total / worldData.population * 1000000; + worldData.casesPerMillion = worldData.cases.total / worldData.population * 1000000; console.log(`transformed data in ${Date.now() - start}ms`); @@ -792,7 +813,7 @@ const processRecords = async () => { const worldTmpl = path.join(templatesDir, 'world.pug'); const worldHtml = pug.renderFile(worldTmpl, { data: worldData, - $title: 'The World', + $title: 'Worldwide', lastUpdate, }); diff --git a/tmpl/master.pug b/tmpl/master.pug index 48ee1aa..f76d829 100644 --- a/tmpl/master.pug +++ b/tmpl/master.pug @@ -13,6 +13,18 @@ html th.sorted, td.sorted { background-color: #e0eaf7; } + thead.headers th { + position: sticky; + top: 0; + } + thead.headers th:after { + content: ''; + position: absolute; + left: 0; + width: 100%; + bottom: -1px; + border-bottom: 2px solid #b5b5b5; + } .geo-bg-dark { background-color: #8e8e8e; color: white; @@ -92,20 +104,30 @@ html trends: [], }; - function makeSparkline(id, data) { + function makeSparkline(id, deathData, caseData) { const canvas = document.getElementById(id); - const maxValue = data.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 chart = new Chart(canvas.getContext('2d'), { type: 'line', data: { - labels: new Array(data.length), - datasets: [{ - data: data, - borderColor: 'rgb(53, 120, 193)', - borderWidth: 1, - backgroundColor: 'rgba(148, 193, 250, 0.50)', - }], + 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, @@ -344,7 +366,7 @@ html mixin heroChart() div.card.mb-4 div.card-body.position-relative - div.position-absolute(style="top: 10px; right: 10px") + div.position-absolute(style="top: 10px; right: 10px; z-index: 2;") div.btn-group button.btn.btn-secondary.btn-sm.set-axis-linear( type="button" @@ -357,7 +379,8 @@ html autocomplete="off" disabled ) Logarithmic - canvas.mx-auto(id="main-chart" width="1024" height="576") + div.mx-auto.position-relative(style="max-width: 1024px; z-index: 1") + canvas.mx-auto(id="main-chart" width="1024" height="576") - const growthRate = '+' + (data.deathGrowthRate * 100).toFixed(2) + '%'; const population = 'pop. ' + data.population.toLocaleString(); @@ -382,19 +405,25 @@ html mixin dataTable(items, label, type) - const hasPopulation = type !== 'state' || data.name === 'United States'; - div#table.table-responsive: table.table.table-sm.table-hover + div#table: table.table.table-sm.table-hover thead: tr th.text-center.font-weight-bold.geo-bg-dark(colspan=(hasPopulation ? 3 : 2)) Geography - th.text-center.font-weight-bold.cases-bg-dark(colspan="2") Cases - th.text-center.font-weight-bold.deaths-bg-dark(colspan="100") Deaths + th.text-center.font-weight-bold.cases-bg-dark(colspan=(hasPopulation ? 6 : 5)) Cases + th.text-center.font-weight-bold.deaths-bg-dark(colspan=(hasPopulation ? 6 : 5)) Deaths + th.text-center.font-weight-bold.geo-bg-dark Trends thead.headers: tr th.geo-bg # th.geo-bg(data-col="name"): +sortableLinks("name", true)= label if hasPopulation th.geo-bg(data-col="population"): +sortableLinks("population") Population + if hasPopulation + th.cases-bg(data-col="cases-million"): +sortableLinks("cases-million") per 1M th.cases-bg(data-col="cases-total"): +sortableLinks("cases-total") Total th.cases-bg(data-col="cases-today"): +sortableLinks("cases-today") Today + th.cases-bg(data-col="cases-yesterday"): +sortableLinks("cases-yesterday") Yesterday + th.cases-bg(data-col="cases-last7"): +sortableLinks("cases-last7") Last 7 + th.cases-bg(data-col="cases-growth"): +sortableLinks("cases-growth") Growth if hasPopulation th.deaths-bg(data-col="million"): +sortableLinks("million") per 1M @@ -402,9 +431,8 @@ html th.sorted.deaths-bg(data-col="today"): +sortableLinks("today") Today th.deaths-bg(data-col="yesterday"): +sortableLinks("yesterday") Yesterday th.deaths-bg(data-col="last7"): +sortableLinks("last7") Last 7 - th.deaths-bg(data-col="last30"): +sortableLinks("last30") Last 30 th.deaths-bg(data-col="growth"): +sortableLinks("growth") Growth - th.text-center.deaths-bg Trend + th.text-center.geo-bg Last 14 days - items.sort((a, b) => { @@ -426,48 +454,55 @@ html const today = getDelta(1); const yesterday = getDelta(2); const last7 = getValue(1) - getValue(7); - const last30 = getValue(1) - getValue(30); - const casesTotal = getValueCases(1); const casesToday = getDeltaCases(1); + const casesLast7 = getValueCases(1) - getValueCases(7); + const casesYesterday = getDeltaCases(1); tr( id=("row-" + (item.safeName || '_')) data-name=(item.name || '_') - data-cases-total=casesTotal + data-cases-total=item.cases.total data-cases-today=casesToday + data-cases-last7=casesLast7 + data-cases-million=item.casesPerMillion + data-cases-yesterday=casesYesterday + data-cases-growth=item.caseGrowthRate data-population=item.population data-total=item.total data-million=item.deathsPerMillion data-today=today data-yesterday=yesterday data-last7=last7 - data-last30=last30 data-growth=item.deathGrowthRate ) td.sort-order= i + 1 td: +renderItemName(item) if hasPopulation td.text-right: code: +formatNumber(item.population) - td.text-right: code: +formatNumber(casesTotal) + + if hasPopulation + td.text-right: code: +formatNumber(Math.round(item.casesPerMillion)) + td.text-right: code: +formatNumber(item.cases.total) td.text-right: code: +formatNumber(casesToday) + td.text-right: code: +formatNumber(casesYesterday) + td.text-right: code: +formatNumber(casesLast7) + td.text-right: code= Number(item.caseGrowthRate * 100).toFixed(2) + '%' if hasPopulation td.text-right: code: +formatNumber(Math.round(item.deathsPerMillion)) td.text-right: code: +formatNumber(item.total) td.text-right.sorted: code: +formatNumber(today) td.text-right: code: +formatNumber(yesterday) td.text-right: code: +formatNumber(last7) - td.text-right: code: +formatNumber(last30) td.text-right: code= Number(item.deathGrowthRate * 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.rollingAverageDaily.slice(-14).map(x => x.delta))}, + #{JSON.stringify(item.cases.rollingAverageDaily.slice(-14).map(x => x.delta))}, ); - - - div.container.mt-2 + div.container-fluid.mt-2(style="max-width: 1600px") h1.text-center Covid-19 Data div.d-flex.justify-content-around.font-italic.small div @@ -561,10 +596,13 @@ html case 'total': case 'cases-total': case 'cases-today': + case 'cases-yesterday': + case 'cases-million': + case 'cases-last7': + case 'cases-growth': case 'today': case 'yesterday': case 'last7': - case 'last30': case 'population': case 'million': case 'growth':