doctype html head title= $title + ' | Covid-19' meta(charset="utf8") link(rel="stylesheet" href="/bootstrap.css") script(src="/Chart.bundle.js") style. acronym, abbr { border-bottom: 1px dotted #999999; cursor: help; } table td { vertical-align: middle !important; } 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; } .hero-tooltip table { border-collapse: collapse; } .hero-tooltip th, .hero-tooltip td { padding: 2px 4px; } .hero-tooltip { position: absolute; top: 0; left: 0; opacity: 0; pointer-events: none; background-image: linear-gradient(to bottom, rgba(52, 52, 52, 0.75), rgba(24, 24, 24, 0.75)); color: white; text-shadow: 1px 1px 1px black; border-radius: 2px; box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.75); z-index: 1; } .tooltip-bordered { border-top: 1px solid #707070; } [class^="tooltip-color-"] { width: 10px; height: 10px; border: 2px solid black; display: inline-block; } [class^="tooltip-value-"] { text-align: right; font-family: monospace; } .geo-bg-dark { background-color: #8e8e8e; color: white; } .cases-bg-dark { background-color: #aaa55e; color: white; } .deaths-bg-dark { background-color: #a65353; color: white; } .other-bg-dark { background-color: #336556; color: white; } .geo-bg { background-color: #eeeeee; } .cases-bg { background-color: #f9f6d5; } .deaths-bg { background-color: #eac8c8; } .other-bg { background-color: #9ed0c2; } .table-sm { font-size: 80%; } .table-sm code { font-size: 110%; color: inherit; } script. Chart.pluginService.register({ beforeDraw: (chart) => { const ctx = chart.chart.ctx; ctx.save(); ctx.fillStyle = 'white'; ctx.fillRect(0, 0, chart.width, chart.height); ctx.restore(); } }); // https://stackoverflow.com/a/45172506 Chart.defaults.lineVerticalTooltip = Chart.defaults.line; Chart.controllers.lineVerticalTooltip = Chart.controllers.line.extend({ draw: function(ease) { Chart.controllers.line.prototype.draw.call(this, ease); if (this.chart.tooltip._active && this.chart.tooltip._active.length) { const activePoint = this.chart.tooltip._active[0]; const ctx = this.chart.ctx; const x = activePoint.tooltipPosition().x; const topY = this.chart.legend.bottom; const bottomY = this.chart.chartArea.bottom; ctx.save(); ctx.beginPath(); ctx.moveTo(x, topY); ctx.lineTo(x, bottomY); ctx.lineWidth = 1; ctx.strokeStyle = 'rgba(96, 96, 96, 0.75)'; ctx.stroke(); ctx.restore(); } } }); const charts = { logarithmic: false, 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, // }, // ], // } // } // }); // charts.trends.push({ // chart, // maxValue, // }); } function setAxisType(type) { 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.heroMaxValues[i])); axis.ticks.max = Math.pow(10, maxLogPower); axis.ticks.callback = value => Number(value.toString()).toLocaleString(); } else { axis.type = 'linear'; delete axis.ticks.max; axis.ticks.callback = value => value; } chart.update(); const selector = '.set-axis-' + type; const otherSelector = type === 'linear' ? '.set-axis-logarithmic' : '.set-axis-linear'; document.querySelector(selector).disabled = true; document.querySelector(otherSelector).disabled = false; }); charts.trends.forEach((data) => { const axis = data.chart.options.scales.yAxes[0]; if (type === 'logarithmic') { axis.type = 'logarithmic'; const maxLogPower = Math.ceil(Math.log10(data.maxValue)); axis.ticks.max = Math.pow(10, maxLogPower); axis.ticks.callback = value => Number(value.toString()).toLocaleString(); } else { axis.type = 'linear'; delete axis.ticks.max; axis.ticks.callback = value => value; } data.chart.update(); }); } function makeHeroChart( id, type, title, labels, cumulative, daily, rollingAverage, ) { const canvas = document.getElementById(id); const maxValue = cumulative.reduce((max, value) => Math.max(max, value), 0); charts.heroMaxValues.push(maxValue); const firstNonZeroDeathIndex = cumulative.findIndex(value => value > 0); const start = Math.max(0, firstNonZeroDeathIndex - 2); const end = cumulative.length; const totalData = cumulative.slice(start, end); const newData = daily.slice(start, end); const rollingData = rollingAverage.slice(start, end); const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; const realLabels = labels .slice(start, end) .map((date) => date.replace(/\d{4}-(\d\d?)-(\d\d?)/, (_, m, d) => `${months[Number(m) - 1]} ${Number(d)}`)); 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: 'Cumulative', data: totalData, fill: '2', borderColor: 'rgba(' + totalColor.join(',') + ')', backgroundColor: 'rgba(' + totalColor.join(',') + ',0.25)', borderWidth: 1, pointRadius, }, { label: 'New', data: newData, fill: false, 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: { responsive: true, title: { display: true, position: 'top', text: title, fontSize: 18, }, tooltips: { intersect: false, axis: 'x', enabled: false, custom: function(tooltipModel) { const tooltipEl = document.getElementById('hero-tooltip-' + type); if (tooltipModel.opacity === 0) { tooltipEl.style.opacity = '0'; return; } if (tooltipModel.dataPoints) { const getDataPoint = (index) => tooltipModel.dataPoints.find(point => point.datasetIndex === index); tooltipEl.querySelector('.tooltip-title').textContent = tooltipModel.title.join(' '); const setData = (cls, index, colorIndex) => { colorIndex = typeof(colorIndex) === 'number' ? colorIndex : index; const dataPoint = getDataPoint(index); tooltipEl.querySelector('.tooltip-value-' + cls).textContent = dataPoint ? Number(dataPoint.value).toLocaleString() : 'n/a'; const colorDataPoint = getDataPoint(colorIndex); if (colorDataPoint) { const realColorIndex = tooltipModel.dataPoints.indexOf(colorDataPoint); const color = tooltipModel.labelColors[realColorIndex]; if (color) { const colorEl = tooltipEl.querySelector('.tooltip-color-' + cls); colorEl.style.backgroundColor = color.backgroundColor; colorEl.style.borderColor = color.borderColor; } } }; setData(type + '-total', 0); setData(type + '-new', 1, 2); } const position = this._chart.canvas.getBoundingClientRect(); tooltipEl.style.opacity = '1'; tooltipEl.style.fontFamily = tooltipModel._bodyFontFamily; tooltipEl.style.fontSize = tooltipModel.bodyFontSize + 'px'; tooltipEl.style.fontStyle = tooltipModel._bodyFontStyle; tooltipEl.style.padding = tooltipModel.yPadding + 'px ' + tooltipModel.xPadding + 'px'; const tooltipSize = tooltipEl.getBoundingClientRect(); const chartArea = this._chart.chartArea; const chartHeight = chartArea.bottom - chartArea.top; tooltipEl.style.left = (position.left + window.pageXOffset + tooltipModel.x) + 'px'; tooltipEl.style.top = (position.top + window.pageYOffset + chartArea.top + (chartHeight / 2) - (tooltipSize.height / 2)) + 'px'; } }, scales: { yAxes: [ { display: true, type: 'logarithmic', ticks: { precision: 0, beginAtZero: true, min: 0, max: Math.pow(10, Math.ceil(Math.log10(maxValue))), callback: value => Number(value.toString()).toLocaleString(), }, afterBuildTicks: (axis, ticks) => { if (axis.type === 'logarithmic') { return ticks.filter((value) => { const logValue = Math.log10(value); return value === 0 || Math.round(logValue) === logValue; }); } return ticks; }, }, ], xAxes: [ { display: true, }, ], } } })); } body mixin formatNumber(num) = Number(num).toLocaleString() mixin sortableLinks(col, notCentered) div.d-flex(class=(!notCentered ? "justify-content-center" : "")) div.sortables.mr-2(style="font-size: 50%") a(href="#sort:" + col + ":asc") ▲ br a(href="#sort:" + col + ":desc") ▼ div block mixin heroChart() div.card.mb-4 div.card-body.position-relative 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'; const casesPerMillion = Math.round(data.casesPerMillion).toLocaleString() + '/1M'; const totalDeaths = data.total.toLocaleString(); const totalCases = data.cases.total.toLocaleString(); let name = data.name; if (data.county) { name += ', ' + data.state; } if (data.country) { name += ', ' + data.country; } const heroCasesTitle = [ `Covid-19: ${name} (${population})`, `${totalCases} cases (${casesPerMillion})`, ]; const heroDeathsTitle = [ `Covid-19: ${name} (${population})`, `${totalDeaths} deaths (${deathsPerMillion})`, ]; script. makeHeroChart( '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))}, ); mixin dataTable(items, label, type) - const hasPopulation = type !== 'state' || data.name === 'United States'; 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=(hasPopulation ? 6 : 4)) Cases th.text-center.font-weight-bold.deaths-bg-dark(colspan=(hasPopulation ? 6 : 4)) Deaths th.text-center.font-weight-bold.other-bg-dark(colspan="2") Other 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-last14"): +sortableLinks("cases-last14"): abbr(title="Last 14 days") L14 if hasPopulation th.cases-bg(data-col="cases-last14-million") +sortableLinks("cases-last14-million"): abbr(title="Last 14 days per million residents") L14/1M if hasPopulation th.deaths-bg(data-col="million"): +sortableLinks("million") per 1M th.deaths-bg(data-col="total"): +sortableLinks("total") Total th.sorted.deaths-bg(data-col="today"): +sortableLinks("today") Today th.deaths-bg(data-col="yesterday"): +sortableLinks("yesterday") Yesterday th.deaths-bg(data-col="last14"): +sortableLinks("last14"): abbr(title="Last 14 days") L14 if hasPopulation th.deaths-bg(data-col="last14-million") +sortableLinks("last14-million"): abbr(title="Last 14 days per million residents") L14/1M th.other-bg(data-col="cfr"): +sortableLinks("cfr") acronym(title="Case Fatality Rate") CFR //th.text-center.other-bg Trend - items.sort((a, b) => { 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; } catch (e) { return 0; } }); tbody: each item, i in items - const getValue = offset => (item.timeSeriesDaily[item.timeSeriesDaily.length - offset] || {}).value || 0; const getDelta = offset => (item.timeSeriesDaily[item.timeSeriesDaily.length - offset] || {}).delta || 0; const getValueCases = offset => (item.cases.timeSeriesDaily[item.cases.timeSeriesDaily.length - offset] || {}).value || 0; const getDeltaCases = offset => (item.cases.timeSeriesDaily[item.cases.timeSeriesDaily.length - offset] || {}).delta || 0; const today = getDelta(1); const yesterday = getDelta(2); const last7 = getValue(1) - getValue(7); const casesToday = getDeltaCases(1); const casesLast7 = getValueCases(1) - getValueCases(7); const casesYesterday = getDeltaCases(2); const last14 = getValue(1) - getValue(14); const casesLast14 = getValueCases(1) - getValueCases(14); const last14Avg = hasPopulation && item.population ? last14 * 1000000 / item.population : null; const casesLast14Avg = hasPopulation && item.population ? casesLast14 * 1000000 / item.population : null; tr( id=("row-" + (item.safeName || '_')) data-name=(item.name || '_') 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-cases-last14=casesLast14 data-cases-last14-million=casesLast14Avg data-population=item.population data-total=item.total data-million=item.deathsPerMillion data-today=today data-yesterday=yesterday data-last7=last7 data-last14=last14 data-last14-million=last14Avg data-growth=item.deathGrowthRate data-cfr=item.caseFatalityRate ) td.sort-order= i + 1 td: +renderItemName(item) if hasPopulation td.text-right: code: +formatNumber(item.population) 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(casesLast14) if hasPopulation td.text-right: code: +formatNumber(Math.round(casesLast14Avg)) 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(last14) 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))}, // ); 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 - const generationDate = new Date().toISOString(); | Data from #[a(href="https://github.com/CSSEGISandData/COVID-19") Johns Hopkins CSSE] div | Generated: #[time.generation-date(datetime=generationDate title=generationDate)= generationDate] div - const lastUpdateISO = lastUpdate.toISOString(); | Data updated: #[time.update-date(datetime=lastUpdateISO title=lastUpdateISO)= lastUpdateISO] 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-cases.hero-tooltip div.text-center(style="font-size: 125%"): strong.tooltip-title table.tooltip-bordered tr td: span.tooltip-color-cases-total th Total Cases td.tooltip-value-cases-total tr 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 td.tooltip-value-deaths-total tr td: span.tooltip-color-deaths-new th New Deaths td.tooltip-value-deaths-new script. (function() { const table = document.getElementById('table'); const headerRow = table.querySelector('thead.headers tr'); const headers = [].slice.call(headerRow.querySelectorAll('th')); const tbody = table.querySelector('tbody'); const allRows = [].slice.call(tbody.querySelectorAll('tbody tr')); const resortTable = (col) => { let nextChild = null; const highlightedIndex = headers.findIndex(cell => cell.getAttribute('data-col') === col); headers.forEach((cell, i) => { if (i !== highlightedIndex) { cell.classList.remove('sorted'); } else { cell.classList.add('sorted'); } }); for (let i = allRows.length - 1; i >= 0; i--) { const row = allRows[i]; if (!row) { continue; } const cells = [].slice.call(row.querySelectorAll('td')); cells.forEach((cell, i) => { if (i !== highlightedIndex) { cell.classList.remove('sorted'); } else { cell.classList.add('sorted'); } }); if (row === nextChild) { continue; } tbody.insertBefore(row, nextChild); row.querySelector('.sort-order').textContent = (i + 1).toString(); nextChild = row; } }; const handleSort = (value, dir) => { const newSortDir = dir === 'desc' ? 'desc' : 'asc'; const sortByNumberThenName = (attr) => { allRows.sort((a, b) => { const aValue = Number(a.getAttribute('data-' + attr)); const bValue = Number(b.getAttribute('data-' + attr)); if (aValue === bValue) { const aName = a.getAttribute('data-name'); const bName = b.getAttribute('data-name'); return aName.localeCompare(bName); } if (aValue === null) { return 1; } if (bValue === null) { return -1; } return aValue < bValue ? (newSortDir === 'asc' ? -1 : 1) : (newSortDir === 'asc' ? 1 : -1); }); resortTable(value); }; switch (value) { case 'name': allRows.sort((a, b) => { const aName = a.getAttribute('data-name'); const bName = b.getAttribute('data-name'); if (newSortDir === 'asc') { return aName.localeCompare(bName); } return bName.localeCompare(aName); }); resortTable('name'); break; case 'total': case 'cases-total': case 'cases-today': case 'cases-yesterday': case 'cases-million': case 'cases-last7': case 'cases-last14': case 'cases-last14-million': case 'cases-growth': case 'today': case 'yesterday': case 'last7': case 'last14': case 'last14-million': case 'population': case 'million': case 'growth': case 'cfr': sortByNumberThenName(value); break; } }; const handleHash = (hash) => { const sortValue = hash.replace(/^#sort:/, '').split(':'); handleSort(sortValue[0], sortValue[1]); }; window.addEventListener('hashchange', () => { handleHash(window.location.hash); }); handleHash(window.location.hash); const setDate = (selector) => { const node = document.querySelector(selector); node.textContent = new Date(node.getAttribute('datetime')).toLocaleString(); }; setDate('.generation-date'); setDate('.update-date'); }());