2020-04-27 03:04:16 +00:00
|
|
|
doctype
|
|
|
|
|
|
|
|
html
|
|
|
|
head
|
2020-04-27 07:33:42 +00:00
|
|
|
title= $title + ' | Covid-19'
|
2020-04-27 03:04:16 +00:00
|
|
|
meta(charset="utf8")
|
|
|
|
link(rel="stylesheet" href="/bootstrap.css")
|
|
|
|
script(src="/Chart.bundle.js")
|
|
|
|
style.
|
2020-06-01 06:41:21 +00:00
|
|
|
acronym, abbr {
|
|
|
|
border-bottom: 1px dotted #999999;
|
|
|
|
cursor: help;
|
|
|
|
}
|
2020-04-27 03:04:16 +00:00
|
|
|
table td {
|
|
|
|
vertical-align: middle !important;
|
|
|
|
}
|
2020-04-27 15:54:26 +00:00
|
|
|
th.sorted, td.sorted {
|
2020-05-02 19:27:46 +00:00
|
|
|
background-color: #e0eaf7;
|
|
|
|
}
|
2020-05-03 01:57:59 +00:00
|
|
|
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;
|
|
|
|
}
|
2020-11-16 00:39:38 +00:00
|
|
|
.hero-tooltip table {
|
2020-05-03 18:48:53 +00:00
|
|
|
border-collapse: collapse;
|
|
|
|
}
|
2020-11-16 00:39:38 +00:00
|
|
|
.hero-tooltip th, .hero-tooltip td {
|
2020-05-03 18:48:53 +00:00
|
|
|
padding: 2px 4px;
|
|
|
|
}
|
2020-11-16 00:39:38 +00:00
|
|
|
.hero-tooltip {
|
2020-05-09 16:44:51 +00:00
|
|
|
position: absolute;
|
2020-05-17 20:30:19 +00:00
|
|
|
top: 0;
|
|
|
|
left: 0;
|
|
|
|
opacity: 0;
|
2020-05-09 16:44:51 +00:00
|
|
|
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;
|
|
|
|
}
|
2020-05-03 18:48:53 +00:00
|
|
|
[class^="tooltip-color-"] {
|
|
|
|
width: 10px;
|
|
|
|
height: 10px;
|
|
|
|
border: 2px solid black;
|
|
|
|
display: inline-block;
|
|
|
|
}
|
|
|
|
[class^="tooltip-value-"] {
|
|
|
|
text-align: right;
|
|
|
|
font-family: monospace;
|
|
|
|
}
|
2020-05-02 19:27:46 +00:00
|
|
|
.geo-bg-dark {
|
|
|
|
background-color: #8e8e8e;
|
|
|
|
color: white;
|
|
|
|
}
|
|
|
|
.cases-bg-dark {
|
|
|
|
background-color: #aaa55e;
|
|
|
|
color: white;
|
|
|
|
}
|
|
|
|
.deaths-bg-dark {
|
|
|
|
background-color: #a65353;
|
|
|
|
color: white;
|
|
|
|
}
|
2020-05-04 23:44:15 +00:00
|
|
|
.other-bg-dark {
|
|
|
|
background-color: #336556;
|
|
|
|
color: white;
|
|
|
|
}
|
2020-05-02 19:27:46 +00:00
|
|
|
.geo-bg {
|
|
|
|
background-color: #eeeeee;
|
|
|
|
}
|
|
|
|
.cases-bg {
|
|
|
|
background-color: #f9f6d5;
|
|
|
|
}
|
|
|
|
.deaths-bg {
|
|
|
|
background-color: #eac8c8;
|
2020-04-27 15:54:26 +00:00
|
|
|
}
|
2020-05-04 23:44:15 +00:00
|
|
|
.other-bg {
|
|
|
|
background-color: #9ed0c2;
|
|
|
|
}
|
2020-04-27 15:54:26 +00:00
|
|
|
.table-sm {
|
|
|
|
font-size: 80%;
|
|
|
|
}
|
2020-04-30 02:51:23 +00:00
|
|
|
.table-sm code {
|
|
|
|
font-size: 110%;
|
|
|
|
color: inherit;
|
|
|
|
}
|
2020-04-27 15:54:26 +00:00
|
|
|
|
|
|
|
script.
|
2020-04-30 03:59:42 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-05-01 06:28:21 +00:00
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-04-29 05:12:53 +00:00
|
|
|
const charts = {
|
|
|
|
logarithmic: false,
|
2020-11-16 00:39:38 +00:00
|
|
|
heroCharts: [],
|
2020-04-29 05:12:53 +00:00
|
|
|
trends: [],
|
2020-11-16 00:39:38 +00:00
|
|
|
heroMaxValues: [],
|
2020-04-29 05:12:53 +00:00
|
|
|
};
|
|
|
|
|
2020-05-03 01:57:59 +00:00
|
|
|
function makeSparkline(id, deathData, caseData) {
|
2020-11-16 00:39:38 +00:00
|
|
|
// 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,
|
|
|
|
// });
|
2020-04-29 05:12:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function setAxisType(type) {
|
2020-11-16 00:39:38 +00:00
|
|
|
charts.heroCharts.forEach((chart, i) => {
|
|
|
|
const axis = chart.options.scales.yAxes[0];
|
2020-04-29 05:12:53 +00:00
|
|
|
if (type === 'logarithmic') {
|
|
|
|
axis.type = 'logarithmic';
|
2020-11-16 00:39:38 +00:00
|
|
|
const maxLogPower = Math.ceil(Math.log10(charts.heroMaxValues[i]));
|
2020-04-29 05:12:53 +00:00
|
|
|
axis.ticks.max = Math.pow(10, maxLogPower);
|
2020-04-30 03:59:42 +00:00
|
|
|
axis.ticks.callback = value => Number(value.toString()).toLocaleString();
|
2020-04-29 05:12:53 +00:00
|
|
|
} else {
|
|
|
|
axis.type = 'linear';
|
|
|
|
delete axis.ticks.max;
|
|
|
|
axis.ticks.callback = value => value;
|
|
|
|
}
|
|
|
|
|
2020-11-16 00:39:38 +00:00
|
|
|
chart.update();
|
2020-04-29 05:12:53 +00:00
|
|
|
|
|
|
|
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;
|
2020-11-16 00:39:38 +00:00
|
|
|
});
|
2020-04-29 05:12:53 +00:00
|
|
|
|
|
|
|
charts.trends.forEach((data) => {
|
|
|
|
const axis = data.chart.options.scales.yAxes[0];
|
2020-04-30 02:51:23 +00:00
|
|
|
if (type === 'logarithmic') {
|
2020-04-29 05:12:53 +00:00
|
|
|
axis.type = 'logarithmic';
|
|
|
|
const maxLogPower = Math.ceil(Math.log10(data.maxValue));
|
|
|
|
axis.ticks.max = Math.pow(10, maxLogPower);
|
2020-04-30 03:59:42 +00:00
|
|
|
axis.ticks.callback = value => Number(value.toString()).toLocaleString();
|
2020-04-29 05:12:53 +00:00
|
|
|
} else {
|
|
|
|
axis.type = 'linear';
|
|
|
|
delete axis.ticks.max;
|
|
|
|
axis.ticks.callback = value => value;
|
|
|
|
}
|
|
|
|
|
|
|
|
data.chart.update();
|
|
|
|
});
|
2020-04-27 15:54:26 +00:00
|
|
|
}
|
|
|
|
|
2020-05-02 19:27:46 +00:00
|
|
|
function makeHeroChart(
|
|
|
|
id,
|
2020-11-16 00:39:38 +00:00
|
|
|
type,
|
2020-05-02 19:27:46 +00:00
|
|
|
title,
|
|
|
|
labels,
|
2020-11-16 00:39:38 +00:00
|
|
|
cumulative,
|
|
|
|
daily,
|
2020-05-02 19:27:46 +00:00
|
|
|
rollingAverage,
|
|
|
|
) {
|
2020-04-27 15:54:26 +00:00
|
|
|
const canvas = document.getElementById(id);
|
2020-11-16 00:39:38 +00:00
|
|
|
const maxValue = cumulative.reduce((max, value) => Math.max(max, value), 0);
|
|
|
|
charts.heroMaxValues.push(maxValue);
|
2020-04-29 05:12:53 +00:00
|
|
|
|
2020-11-16 00:39:38 +00:00
|
|
|
const firstNonZeroDeathIndex = cumulative.findIndex(value => value > 0);
|
2020-05-01 06:28:21 +00:00
|
|
|
const start = Math.max(0, firstNonZeroDeathIndex - 2);
|
2020-11-16 00:39:38 +00:00
|
|
|
const end = cumulative.length;
|
2020-05-01 06:28:21 +00:00
|
|
|
|
2020-11-16 00:39:38 +00:00
|
|
|
const totalData = cumulative.slice(start, end);
|
|
|
|
const newData = daily.slice(start, end);
|
2020-05-01 06:28:21 +00:00
|
|
|
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)}`));
|
|
|
|
|
2020-11-16 00:39:38 +00:00
|
|
|
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'), {
|
2020-05-01 06:28:21 +00:00
|
|
|
type: 'lineVerticalTooltip',
|
2020-04-27 15:54:26 +00:00
|
|
|
data: {
|
2020-05-01 06:28:21 +00:00
|
|
|
labels: realLabels,
|
2020-04-27 15:54:26 +00:00
|
|
|
datasets: [
|
|
|
|
{
|
2020-11-16 00:39:38 +00:00
|
|
|
label: 'Cumulative',
|
|
|
|
data: totalData,
|
2020-05-02 19:27:46 +00:00
|
|
|
fill: '2',
|
2020-11-16 00:39:38 +00:00
|
|
|
borderColor: 'rgba(' + totalColor.join(',') + ')',
|
|
|
|
backgroundColor: 'rgba(' + totalColor.join(',') + ',0.25)',
|
2020-05-02 19:27:46 +00:00
|
|
|
borderWidth: 1,
|
2020-11-16 00:39:38 +00:00
|
|
|
pointRadius,
|
2020-05-02 19:27:46 +00:00
|
|
|
},
|
|
|
|
{
|
2020-11-16 00:39:38 +00:00
|
|
|
label: 'New',
|
|
|
|
data: newData,
|
2020-05-02 19:27:46 +00:00
|
|
|
fill: false,
|
2020-11-16 00:39:38 +00:00
|
|
|
borderColor: 'rgba(' + dailyColor.join(',') + ',0.15)',
|
|
|
|
backgroundColor: 'rgba(' + dailyColor.join(',') + ',0.15)',
|
2020-05-02 19:27:46 +00:00
|
|
|
borderWidth: 1,
|
2020-11-16 00:39:38 +00:00
|
|
|
pointRadius,
|
2020-05-02 19:27:46 +00:00
|
|
|
},
|
|
|
|
{
|
2020-11-16 00:39:38 +00:00
|
|
|
label: 'New (7 day avg.)',
|
2020-05-01 06:28:21 +00:00
|
|
|
data: rollingData,
|
2020-04-30 02:51:23 +00:00
|
|
|
fill: 'origin',
|
2020-11-16 00:39:38 +00:00
|
|
|
borderColor: 'rgba(' + rollingColor.join(',') + ')',
|
|
|
|
backgroundColor: 'rgba(' + rollingColor.join(',') + ',0.25)',
|
2020-04-27 15:54:26 +00:00
|
|
|
borderWidth: 1,
|
2020-11-16 00:39:38 +00:00
|
|
|
pointRadius,
|
2020-04-30 03:59:42 +00:00
|
|
|
},
|
2020-11-16 00:39:38 +00:00
|
|
|
|
2020-04-27 15:54:26 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
options: {
|
2020-05-02 19:27:46 +00:00
|
|
|
responsive: true,
|
2020-04-27 15:54:26 +00:00
|
|
|
title: {
|
|
|
|
display: true,
|
|
|
|
position: 'top',
|
|
|
|
text: title,
|
2020-04-27 23:10:38 +00:00
|
|
|
fontSize: 18,
|
2020-04-27 15:54:26 +00:00
|
|
|
},
|
|
|
|
tooltips: {
|
|
|
|
intersect: false,
|
|
|
|
axis: 'x',
|
2020-05-03 18:48:53 +00:00
|
|
|
enabled: false,
|
|
|
|
custom: function(tooltipModel) {
|
2020-11-16 00:39:38 +00:00
|
|
|
const tooltipEl = document.getElementById('hero-tooltip-' + type);
|
2020-05-03 18:48:53 +00:00
|
|
|
|
|
|
|
if (tooltipModel.opacity === 0) {
|
|
|
|
tooltipEl.style.opacity = '0';
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tooltipModel.dataPoints) {
|
2020-05-09 16:44:51 +00:00
|
|
|
const getDataPoint = (index) => tooltipModel.dataPoints.find(point => point.datasetIndex === index);
|
|
|
|
|
2020-05-03 18:48:53 +00:00
|
|
|
tooltipEl.querySelector('.tooltip-title').textContent = tooltipModel.title.join(' ');
|
|
|
|
const setData = (cls, index, colorIndex) => {
|
|
|
|
colorIndex = typeof(colorIndex) === 'number' ? colorIndex : index;
|
2020-05-09 16:44:51 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2020-05-03 18:48:53 +00:00
|
|
|
};
|
|
|
|
|
2020-11-16 00:39:38 +00:00
|
|
|
setData(type + '-total', 0);
|
|
|
|
setData(type + '-new', 1, 2);
|
2020-05-03 18:48:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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';
|
|
|
|
}
|
2020-04-27 15:54:26 +00:00
|
|
|
},
|
|
|
|
scales: {
|
|
|
|
yAxes: [
|
|
|
|
{
|
|
|
|
display: true,
|
2020-04-30 02:51:23 +00:00
|
|
|
type: 'logarithmic',
|
2020-04-27 15:54:26 +00:00
|
|
|
ticks: {
|
|
|
|
precision: 0,
|
|
|
|
beginAtZero: true,
|
2020-04-30 02:51:23 +00:00
|
|
|
min: 0,
|
2020-11-16 00:39:38 +00:00
|
|
|
max: Math.pow(10, Math.ceil(Math.log10(maxValue))),
|
2020-04-30 03:59:42 +00:00
|
|
|
callback: value => Number(value.toString()).toLocaleString(),
|
2020-04-29 05:12:53 +00:00
|
|
|
},
|
|
|
|
afterBuildTicks: (axis, ticks) => {
|
|
|
|
if (axis.type === 'logarithmic') {
|
|
|
|
return ticks.filter((value) => {
|
|
|
|
const logValue = Math.log10(value);
|
2020-05-09 16:44:51 +00:00
|
|
|
return value === 0 || Math.round(logValue) === logValue;
|
2020-04-29 05:12:53 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return ticks;
|
|
|
|
},
|
2020-04-27 15:54:26 +00:00
|
|
|
},
|
|
|
|
],
|
|
|
|
xAxes: [
|
|
|
|
{
|
|
|
|
display: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
}
|
2020-11-16 00:39:38 +00:00
|
|
|
}));
|
2020-04-27 15:54:26 +00:00
|
|
|
}
|
2020-04-27 03:04:16 +00:00
|
|
|
body
|
|
|
|
mixin formatNumber(num)
|
|
|
|
= Number(num).toLocaleString()
|
|
|
|
|
2020-05-02 19:27:46 +00:00
|
|
|
mixin sortableLinks(col, notCentered)
|
|
|
|
div.d-flex(class=(!notCentered ? "justify-content-center" : ""))
|
|
|
|
div.sortables.mr-2(style="font-size: 50%")
|
2020-04-27 15:54:26 +00:00
|
|
|
a(href="#sort:" + col + ":asc") ▲
|
2020-05-02 19:27:46 +00:00
|
|
|
br
|
2020-04-27 15:54:26 +00:00
|
|
|
a(href="#sort:" + col + ":desc") ▼
|
2020-05-02 19:27:46 +00:00
|
|
|
div
|
2020-04-27 15:54:26 +00:00
|
|
|
block
|
|
|
|
|
2020-04-29 05:22:17 +00:00
|
|
|
mixin heroChart()
|
2020-04-27 23:10:38 +00:00
|
|
|
div.card.mb-4
|
2020-04-29 05:12:53 +00:00
|
|
|
div.card-body.position-relative
|
2020-11-16 00:39:38 +00:00
|
|
|
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")
|
2020-04-29 05:22:17 +00:00
|
|
|
-
|
|
|
|
const population = 'pop. ' + data.population.toLocaleString();
|
2020-06-01 06:41:21 +00:00
|
|
|
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();
|
2020-07-21 05:31:02 +00:00
|
|
|
let name = data.name;
|
|
|
|
if (data.county) {
|
|
|
|
name += ', ' + data.state;
|
|
|
|
}
|
|
|
|
if (data.country) {
|
|
|
|
name += ', ' + data.country;
|
|
|
|
}
|
2020-11-16 00:39:38 +00:00
|
|
|
const heroCasesTitle = [
|
2020-07-21 05:31:02 +00:00
|
|
|
`Covid-19: ${name} (${population})`,
|
2020-07-21 05:01:13 +00:00
|
|
|
`${totalCases} cases (${casesPerMillion})`,
|
2020-11-16 00:39:38 +00:00
|
|
|
];
|
|
|
|
const heroDeathsTitle = [
|
|
|
|
`Covid-19: ${name} (${population})`,
|
2020-07-21 05:01:13 +00:00
|
|
|
`${totalDeaths} deaths (${deathsPerMillion})`,
|
2020-04-30 02:51:23 +00:00
|
|
|
];
|
2020-04-27 23:10:38 +00:00
|
|
|
script.
|
|
|
|
makeHeroChart(
|
2020-11-16 00:39:38 +00:00
|
|
|
'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)},
|
2020-04-27 23:10:38 +00:00
|
|
|
!{JSON.stringify(data.timeSeriesDaily.map(x => x.key))},
|
|
|
|
!{JSON.stringify(data.timeSeriesDaily.map(x => x.value))},
|
|
|
|
!{JSON.stringify(data.timeSeriesDaily.map(x => x.delta))},
|
2020-04-30 02:51:23 +00:00
|
|
|
!{JSON.stringify(data.rollingAverageDaily.map(x => x.delta))},
|
2020-04-27 23:10:38 +00:00
|
|
|
);
|
|
|
|
|
2020-04-29 00:58:53 +00:00
|
|
|
mixin dataTable(items, label, type)
|
|
|
|
- const hasPopulation = type !== 'state' || data.name === 'United States';
|
|
|
|
|
2020-05-03 01:57:59 +00:00
|
|
|
div#table: table.table.table-sm.table-hover
|
2020-04-29 00:58:53 +00:00
|
|
|
thead: tr
|
2020-05-02 19:27:46 +00:00
|
|
|
th.text-center.font-weight-bold.geo-bg-dark(colspan=(hasPopulation ? 3 : 2)) Geography
|
2020-07-21 05:01:13 +00:00
|
|
|
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
|
2020-05-04 23:44:15 +00:00
|
|
|
th.text-center.font-weight-bold.other-bg-dark(colspan="2") Other
|
2020-05-02 19:27:46 +00:00
|
|
|
thead.headers: tr
|
|
|
|
th.geo-bg #
|
|
|
|
th.geo-bg(data-col="name"): +sortableLinks("name", true)= label
|
2020-04-29 00:58:53 +00:00
|
|
|
if hasPopulation
|
2020-05-02 19:27:46 +00:00
|
|
|
th.geo-bg(data-col="population"): +sortableLinks("population") Population
|
|
|
|
|
2020-05-03 01:57:59 +00:00
|
|
|
if hasPopulation
|
|
|
|
th.cases-bg(data-col="cases-million"): +sortableLinks("cases-million") per 1M
|
2020-05-02 19:27:46 +00:00
|
|
|
th.cases-bg(data-col="cases-total"): +sortableLinks("cases-total") Total
|
|
|
|
th.cases-bg(data-col="cases-today"): +sortableLinks("cases-today") Today
|
2020-05-03 01:57:59 +00:00
|
|
|
th.cases-bg(data-col="cases-yesterday"): +sortableLinks("cases-yesterday") Yesterday
|
2020-06-01 06:41:21 +00:00
|
|
|
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
|
2020-05-02 19:27:46 +00:00
|
|
|
|
|
|
|
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
|
2020-06-01 06:41:21 +00:00
|
|
|
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
|
2020-05-04 23:44:15 +00:00
|
|
|
|
|
|
|
th.other-bg(data-col="cfr"): +sortableLinks("cfr")
|
2020-06-01 06:41:21 +00:00
|
|
|
acronym(title="Case Fatality Rate") CFR
|
2020-11-16 00:39:38 +00:00
|
|
|
//th.text-center.other-bg Trend
|
2020-04-29 00:58:53 +00:00
|
|
|
|
|
|
|
-
|
|
|
|
items.sort((a, b) => {
|
2020-11-16 00:39:38 +00:00
|
|
|
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;
|
|
|
|
}
|
2020-04-29 00:58:53 +00:00
|
|
|
|
2020-11-16 00:39:38 +00:00
|
|
|
return yesterdayA < yesterdayB ? 1 : -1;
|
|
|
|
} catch (e) {
|
|
|
|
return 0;
|
|
|
|
}
|
2020-04-29 00:58:53 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
tbody: each item, i in items
|
2020-04-29 01:17:03 +00:00
|
|
|
-
|
|
|
|
const getValue = offset => (item.timeSeriesDaily[item.timeSeriesDaily.length - offset] || {}).value || 0;
|
|
|
|
const getDelta = offset => (item.timeSeriesDaily[item.timeSeriesDaily.length - offset] || {}).delta || 0;
|
2020-05-02 19:27:46 +00:00
|
|
|
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;
|
2020-04-29 01:17:03 +00:00
|
|
|
const today = getDelta(1);
|
2020-04-30 02:51:23 +00:00
|
|
|
const yesterday = getDelta(2);
|
2020-04-29 01:17:03 +00:00
|
|
|
const last7 = getValue(1) - getValue(7);
|
2020-05-02 19:27:46 +00:00
|
|
|
const casesToday = getDeltaCases(1);
|
2020-05-03 01:57:59 +00:00
|
|
|
const casesLast7 = getValueCases(1) - getValueCases(7);
|
2020-05-03 04:00:30 +00:00
|
|
|
const casesYesterday = getDeltaCases(2);
|
2020-06-01 06:41:21 +00:00
|
|
|
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;
|
2020-04-29 00:58:53 +00:00
|
|
|
tr(
|
2020-05-01 06:54:24 +00:00
|
|
|
id=("row-" + (item.safeName || '_'))
|
|
|
|
data-name=(item.name || '_')
|
2020-05-03 01:57:59 +00:00
|
|
|
data-cases-total=item.cases.total
|
2020-05-02 19:27:46 +00:00
|
|
|
data-cases-today=casesToday
|
2020-05-03 01:57:59 +00:00
|
|
|
data-cases-last7=casesLast7
|
|
|
|
data-cases-million=item.casesPerMillion
|
|
|
|
data-cases-yesterday=casesYesterday
|
|
|
|
data-cases-growth=item.caseGrowthRate
|
2020-06-01 06:41:21 +00:00
|
|
|
data-cases-last14=casesLast14
|
|
|
|
data-cases-last14-million=casesLast14Avg
|
2020-04-29 00:58:53 +00:00
|
|
|
data-population=item.population
|
|
|
|
data-total=item.total
|
|
|
|
data-million=item.deathsPerMillion
|
2020-04-29 01:17:03 +00:00
|
|
|
data-today=today
|
2020-04-30 02:51:23 +00:00
|
|
|
data-yesterday=yesterday
|
2020-04-29 01:17:03 +00:00
|
|
|
data-last7=last7
|
2020-06-01 06:41:21 +00:00
|
|
|
data-last14=last14
|
|
|
|
data-last14-million=last14Avg
|
2020-04-29 00:58:53 +00:00
|
|
|
data-growth=item.deathGrowthRate
|
2020-05-04 23:44:15 +00:00
|
|
|
data-cfr=item.caseFatalityRate
|
2020-04-29 00:58:53 +00:00
|
|
|
)
|
|
|
|
td.sort-order= i + 1
|
|
|
|
td: +renderItemName(item)
|
|
|
|
if hasPopulation
|
2020-04-29 01:17:03 +00:00
|
|
|
td.text-right: code: +formatNumber(item.population)
|
2020-05-03 01:57:59 +00:00
|
|
|
|
|
|
|
if hasPopulation
|
|
|
|
td.text-right: code: +formatNumber(Math.round(item.casesPerMillion))
|
|
|
|
td.text-right: code: +formatNumber(item.cases.total)
|
2020-05-02 19:27:46 +00:00
|
|
|
td.text-right: code: +formatNumber(casesToday)
|
2020-05-03 01:57:59 +00:00
|
|
|
td.text-right: code: +formatNumber(casesYesterday)
|
2020-06-01 06:41:21 +00:00
|
|
|
td.text-right: code: +formatNumber(casesLast14)
|
|
|
|
if hasPopulation
|
|
|
|
td.text-right: code: +formatNumber(Math.round(casesLast14Avg))
|
2020-05-02 19:27:46 +00:00
|
|
|
if hasPopulation
|
2020-04-29 01:17:03 +00:00
|
|
|
td.text-right: code: +formatNumber(Math.round(item.deathsPerMillion))
|
|
|
|
td.text-right: code: +formatNumber(item.total)
|
|
|
|
td.text-right.sorted: code: +formatNumber(today)
|
2020-04-30 02:51:23 +00:00
|
|
|
td.text-right: code: +formatNumber(yesterday)
|
2020-06-01 06:41:21 +00:00
|
|
|
td.text-right: code: +formatNumber(last14)
|
|
|
|
if hasPopulation
|
|
|
|
td.text-right: code: +formatNumber(Math.round(last14Avg))
|
2020-05-04 23:44:15 +00:00
|
|
|
td.text-right: code= Number(item.caseFatalityRate * 100).toFixed(2) + '%'
|
2020-11-16 00:39:38 +00:00
|
|
|
//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))},
|
|
|
|
// );
|
2020-04-29 00:58:53 +00:00
|
|
|
|
2020-05-03 01:57:59 +00:00
|
|
|
div.container-fluid.mt-2(style="max-width: 1600px")
|
2020-05-02 19:27:46 +00:00
|
|
|
h1.text-center Covid-19 Data
|
2020-04-29 00:38:11 +00:00
|
|
|
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]
|
2020-04-27 03:04:16 +00:00
|
|
|
|
2020-04-29 00:38:11 +00:00
|
|
|
hr
|
|
|
|
|
2020-04-29 05:12:53 +00:00
|
|
|
div.main-content.mt-4.position-relative
|
2020-11-16 00:39:38 +00:00
|
|
|
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
|
2020-04-27 03:04:16 +00:00
|
|
|
block main
|
|
|
|
|
2020-11-16 00:39:38 +00:00
|
|
|
div#hero-tooltip-cases.hero-tooltip
|
2020-05-09 16:44:51 +00:00
|
|
|
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
|
2020-11-16 00:39:38 +00:00
|
|
|
|
|
|
|
div#hero-tooltip-deaths.hero-tooltip
|
|
|
|
div.text-center(style="font-size: 125%"): strong.tooltip-title
|
|
|
|
table.tooltip-bordered
|
2020-05-09 16:44:51 +00:00
|
|
|
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
|
|
|
|
|
2020-04-27 15:54:26 +00:00
|
|
|
script.
|
|
|
|
(function() {
|
|
|
|
const table = document.getElementById('table');
|
2020-05-02 19:27:46 +00:00
|
|
|
const headerRow = table.querySelector('thead.headers tr');
|
2020-04-27 15:54:26 +00:00
|
|
|
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');
|
2020-05-01 06:54:24 +00:00
|
|
|
return aName.localeCompare(bName);
|
2020-04-27 15:54:26 +00:00
|
|
|
}
|
|
|
|
|
2020-06-01 06:41:21 +00:00
|
|
|
if (aValue === null) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
if (bValue === null) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2020-04-27 15:54:26 +00:00
|
|
|
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':
|
2020-05-02 19:27:46 +00:00
|
|
|
case 'cases-total':
|
|
|
|
case 'cases-today':
|
2020-05-03 01:57:59 +00:00
|
|
|
case 'cases-yesterday':
|
|
|
|
case 'cases-million':
|
|
|
|
case 'cases-last7':
|
2020-06-01 06:41:21 +00:00
|
|
|
case 'cases-last14':
|
|
|
|
case 'cases-last14-million':
|
2020-05-03 01:57:59 +00:00
|
|
|
case 'cases-growth':
|
2020-04-29 01:17:03 +00:00
|
|
|
case 'today':
|
2020-04-30 02:51:23 +00:00
|
|
|
case 'yesterday':
|
2020-04-29 01:17:03 +00:00
|
|
|
case 'last7':
|
2020-06-01 06:41:21 +00:00
|
|
|
case 'last14':
|
|
|
|
case 'last14-million':
|
2020-04-28 03:02:31 +00:00
|
|
|
case 'population':
|
|
|
|
case 'million':
|
|
|
|
case 'growth':
|
2020-05-04 23:44:15 +00:00
|
|
|
case 'cfr':
|
2020-04-28 03:02:31 +00:00
|
|
|
sortByNumberThenName(value);
|
2020-04-27 15:54:26 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleHash = (hash) => {
|
|
|
|
const sortValue = hash.replace(/^#sort:/, '').split(':');
|
|
|
|
handleSort(sortValue[0], sortValue[1]);
|
|
|
|
};
|
|
|
|
|
|
|
|
window.addEventListener('hashchange', () => {
|
|
|
|
handleHash(window.location.hash);
|
|
|
|
});
|
2020-04-27 03:04:16 +00:00
|
|
|
|
2020-04-27 15:54:26 +00:00
|
|
|
handleHash(window.location.hash);
|
2020-04-29 00:38:11 +00:00
|
|
|
|
|
|
|
const setDate = (selector) => {
|
|
|
|
const node = document.querySelector(selector);
|
|
|
|
node.textContent = new Date(node.getAttribute('datetime')).toLocaleString();
|
|
|
|
};
|
|
|
|
|
|
|
|
setDate('.generation-date');
|
|
|
|
setDate('.update-date');
|
2020-04-27 15:54:26 +00:00
|
|
|
}());
|