tooltip styles, code cleanup, case doubling

This commit is contained in:
tmont 2020-05-03 11:48:53 -07:00
parent a714af2c1d
commit f5ba23cc75
2 changed files with 181 additions and 195 deletions

View File

@ -239,6 +239,27 @@ const processRecords = async () => {
return calcGrowthRate(ts, ts.length - 1, 7); return calcGrowthRate(ts, ts.length - 1, 7);
}; };
const convertTsObjectToArray = (obj) => {
return Object.keys(obj).sort().map((date) => {
return {
key: date,
value: obj[date].value,
delta: obj[date].delta,
};
})
};
const mergeTsArrayIntoObject = (source, target) => {
source.forEach((ts) => {
target[ts.key] = target[ts.key] || {
value: 0,
delta: 0,
};
target[ts.key].value += ts.value;
target[ts.key].delta += ts.delta;
});
};
const normalizeRecord = (record) => { const normalizeRecord = (record) => {
record.timeSeriesDaily = []; record.timeSeriesDaily = [];
record.timeSeriesMonthly = []; record.timeSeriesMonthly = [];
@ -508,41 +529,11 @@ const processRecords = async () => {
stateItem.total += record.total; stateItem.total += record.total;
stateItem.cases.total += record.cases.total; stateItem.cases.total += record.cases.total;
record.timeSeriesDaily.forEach((ts) => {
stateItem.timeSeriesDaily[ts.key] = stateItem.timeSeriesDaily[ts.key] || {
value: 0,
delta: 0,
};
stateItem.timeSeriesDaily[ts.key].value += ts.value;
stateItem.timeSeriesDaily[ts.key].delta += ts.delta;
});
record.timeSeriesMonthly.forEach((ts) => { mergeTsArrayIntoObject(record.timeSeriesDaily, stateItem.timeSeriesDaily);
stateItem.timeSeriesMonthly[ts.key] = stateItem.timeSeriesMonthly[ts.key] || { mergeTsArrayIntoObject(record.timeSeriesMonthly, stateItem.timeSeriesMonthly);
value: 0, mergeTsArrayIntoObject(record.cases.timeSeriesDaily, stateItem.cases.timeSeriesDaily);
delta: 0, mergeTsArrayIntoObject(record.cases.timeSeriesMonthly, stateItem.cases.timeSeriesMonthly);
};
stateItem.timeSeriesMonthly[ts.key].value += ts.value;
stateItem.timeSeriesMonthly[ts.key].delta += ts.delta;
});
record.cases.timeSeriesDaily.forEach((ts) => {
stateItem.cases.timeSeriesDaily[ts.key] = stateItem.cases.timeSeriesDaily[ts.key] || {
value: 0,
delta: 0,
};
stateItem.cases.timeSeriesDaily[ts.key].value += ts.value;
stateItem.cases.timeSeriesDaily[ts.key].delta += ts.delta;
});
record.cases.timeSeriesMonthly.forEach((ts) => {
stateItem.cases.timeSeriesMonthly[ts.key] = stateItem.cases.timeSeriesMonthly[ts.key] || {
value: 0,
delta: 0,
};
stateItem.cases.timeSeriesMonthly[ts.key].value += ts.value;
stateItem.cases.timeSeriesMonthly[ts.key].delta += ts.delta;
});
stateItem.counties.push(record); stateItem.counties.push(record);
} else { } else {
@ -556,41 +547,10 @@ const processRecords = async () => {
item.total += record.total; item.total += record.total;
item.cases.total += record.cases.total; item.cases.total += record.cases.total;
record.timeSeriesDaily.forEach((ts) => { mergeTsArrayIntoObject(record.timeSeriesDaily, item.timeSeriesDaily);
item.timeSeriesDaily[ts.key] = item.timeSeriesDaily[ts.key] || { mergeTsArrayIntoObject(record.timeSeriesMonthly, item.timeSeriesMonthly);
value: 0, mergeTsArrayIntoObject(record.cases.timeSeriesDaily, item.cases.timeSeriesDaily);
delta: 0, mergeTsArrayIntoObject(record.cases.timeSeriesMonthly, item.cases.timeSeriesMonthly);
};
item.timeSeriesDaily[ts.key].value += ts.value;
item.timeSeriesDaily[ts.key].delta += ts.delta;
});
record.timeSeriesMonthly.forEach((ts) => {
item.timeSeriesMonthly[ts.key] = item.timeSeriesMonthly[ts.key] || {
value: 0,
delta: 0,
};
item.timeSeriesMonthly[ts.key].value += ts.value;
item.timeSeriesMonthly[ts.key].delta += ts.delta;
});
record.cases.timeSeriesDaily.forEach((ts) => {
item.cases.timeSeriesDaily[ts.key] = item.cases.timeSeriesDaily[ts.key] || {
value: 0,
delta: 0,
};
item.cases.timeSeriesDaily[ts.key].value += ts.value;
item.cases.timeSeriesDaily[ts.key].delta += ts.delta;
});
record.cases.timeSeriesMonthly.forEach((ts) => {
item.cases.timeSeriesMonthly[ts.key] = item.cases.timeSeriesMonthly[ts.key] || {
value: 0,
delta: 0,
};
item.cases.timeSeriesMonthly[ts.key].value += ts.value;
item.cases.timeSeriesMonthly[ts.key].delta += ts.delta;
});
}); });
Object.keys(perStateTotals).forEach((stateName) => { Object.keys(perStateTotals).forEach((stateName) => {
@ -608,36 +568,12 @@ const processRecords = async () => {
population: item.population, population: item.population,
deathsPerMillion: item.population > 0 ? item.total / item.population * 1000000 : 0, deathsPerMillion: item.population > 0 ? item.total / item.population * 1000000 : 0,
casesPerMillion: item.population > 0 ? item.cases.total / item.population * 1000000 : 0, casesPerMillion: item.population > 0 ? item.cases.total / item.population * 1000000 : 0,
timeSeriesDaily: Object.keys(item.timeSeriesDaily).sort().map((date) => { timeSeriesDaily: convertTsObjectToArray(item.timeSeriesDaily),
return { timeSeriesMonthly: convertTsObjectToArray(item.timeSeriesMonthly),
key: date,
value: item.timeSeriesDaily[date].value,
delta: item.timeSeriesDaily[date].delta,
};
}),
timeSeriesMonthly: Object.keys(item.timeSeriesMonthly).sort().map((date) => {
return {
key: date,
value: item.timeSeriesMonthly[date].value,
delta: item.timeSeriesMonthly[date].delta,
};
}),
cases: { cases: {
total: item.cases.total, total: item.cases.total,
timeSeriesDaily: Object.keys(item.cases.timeSeriesDaily).sort().map((date) => { timeSeriesDaily: convertTsObjectToArray(item.cases.timeSeriesDaily),
return { timeSeriesMonthly: convertTsObjectToArray(item.cases.timeSeriesMonthly),
key: date,
value: item.cases.timeSeriesDaily[date].value,
delta: item.cases.timeSeriesDaily[date].delta,
};
}),
timeSeriesMonthly: Object.keys(item.cases.timeSeriesMonthly).sort().map((date) => {
return {
key: date,
value: item.cases.timeSeriesMonthly[date].value,
delta: item.cases.timeSeriesMonthly[date].delta,
};
}),
}, },
}; };
@ -646,6 +582,7 @@ const processRecords = async () => {
stateItem.rollingAverageDaily = getRollingAverage(stateItem); stateItem.rollingAverageDaily = getRollingAverage(stateItem);
stateItem.doublingDaily = getDoublingTime(stateItem); stateItem.doublingDaily = getDoublingTime(stateItem);
stateItem.cases.rollingAverageDaily = getRollingAverage(stateItem.cases); stateItem.cases.rollingAverageDaily = getRollingAverage(stateItem.cases);
stateItem.cases.doublingDaily = getDoublingTime(stateItem.cases);
// insert into states array for the country // insert into states array for the country
perCountryTotals[item.country].states.push(stateItem); perCountryTotals[item.country].states.push(stateItem);
@ -672,36 +609,12 @@ const processRecords = async () => {
population: item.population, population: item.population,
deathsPerMillion: item.population > 0 ? item.total / item.population * 1000000 : 0, deathsPerMillion: item.population > 0 ? item.total / item.population * 1000000 : 0,
casesPerMillion: item.population > 0 ? item.cases.total / item.population * 1000000 : 0, casesPerMillion: item.population > 0 ? item.cases.total / item.population * 1000000 : 0,
timeSeriesDaily: Object.keys(item.timeSeriesDaily).sort().map((date) => { timeSeriesDaily: convertTsObjectToArray(item.timeSeriesDaily),
return { timeSeriesMonthly: convertTsObjectToArray(item.timeSeriesMonthly),
key: date,
value: item.timeSeriesDaily[date].value,
delta: item.timeSeriesDaily[date].delta,
};
}),
timeSeriesMonthly: Object.keys(item.timeSeriesMonthly).sort().map((date) => {
return {
key: date,
value: item.timeSeriesMonthly[date].value,
delta: item.timeSeriesMonthly[date].delta,
};
}),
cases: { cases: {
total: item.cases.total, total: item.cases.total,
timeSeriesDaily: Object.keys(item.cases.timeSeriesDaily).sort().map((date) => { timeSeriesDaily: convertTsObjectToArray(item.cases.timeSeriesDaily),
return { timeSeriesMonthly: convertTsObjectToArray(item.cases.timeSeriesMonthly),
key: date,
value: item.cases.timeSeriesDaily[date].value,
delta: item.cases.timeSeriesDaily[date].delta,
};
}),
timeSeriesMonthly: Object.keys(item.cases.timeSeriesMonthly).sort().map((date) => {
return {
key: date,
value: item.cases.timeSeriesMonthly[date].value,
delta: item.cases.timeSeriesMonthly[date].delta,
};
}),
}, },
}; };
@ -710,6 +623,7 @@ const processRecords = async () => {
countryItem.rollingAverageDaily = getRollingAverage(countryItem); countryItem.rollingAverageDaily = getRollingAverage(countryItem);
countryItem.doublingDaily = getDoublingTime(countryItem); countryItem.doublingDaily = getDoublingTime(countryItem);
countryItem.cases.rollingAverageDaily = getRollingAverage(countryItem.cases); countryItem.cases.rollingAverageDaily = getRollingAverage(countryItem.cases);
countryItem.cases.doublingDaily = getDoublingTime(countryItem.cases);
return countryItem; return countryItem;
}); });
@ -731,77 +645,23 @@ const processRecords = async () => {
worldData.total += countryData.total; worldData.total += countryData.total;
worldData.cases.total += countryData.cases.total; worldData.cases.total += countryData.cases.total;
countryData.timeSeriesDaily.forEach((ts) => { mergeTsArrayIntoObject(countryData.timeSeriesDaily, worldData.timeSeriesDaily);
worldData.timeSeriesDaily[ts.key] = worldData.timeSeriesDaily[ts.key] || { mergeTsArrayIntoObject(countryData.timeSeriesMonthly, worldData.timeSeriesMonthly);
value: 0, mergeTsArrayIntoObject(countryData.cases.timeSeriesDaily, worldData.cases.timeSeriesDaily);
delta: 0, mergeTsArrayIntoObject(countryData.cases.timeSeriesMonthly, worldData.cases.timeSeriesMonthly);
};
worldData.timeSeriesDaily[ts.key].value += ts.value;
worldData.timeSeriesDaily[ts.key].delta += ts.delta;
});
countryData.timeSeriesMonthly.forEach((ts) => {
worldData.timeSeriesMonthly[ts.key] = worldData.timeSeriesMonthly[ts.key] || {
value: 0,
delta: 0,
};
worldData.timeSeriesMonthly[ts.key].value += ts.value;
worldData.timeSeriesMonthly[ts.key].delta += ts.delta;
});
countryData.cases.timeSeriesDaily.forEach((ts) => {
worldData.cases.timeSeriesDaily[ts.key] = worldData.cases.timeSeriesDaily[ts.key] || {
value: 0,
delta: 0,
};
worldData.cases.timeSeriesDaily[ts.key].value += ts.value;
worldData.cases.timeSeriesDaily[ts.key].delta += ts.delta;
});
countryData.cases.timeSeriesMonthly.forEach((ts) => {
worldData.cases.timeSeriesMonthly[ts.key] = worldData.cases.timeSeriesMonthly[ts.key] || {
value: 0,
delta: 0,
};
worldData.cases.timeSeriesMonthly[ts.key].value += ts.value;
worldData.cases.timeSeriesMonthly[ts.key].delta += ts.delta;
});
}); });
worldData.timeSeriesDaily = Object.keys(worldData.timeSeriesDaily).sort().map((date) => { worldData.timeSeriesDaily = convertTsObjectToArray(worldData.timeSeriesDaily);
return { worldData.timeSeriesMonthly = convertTsObjectToArray(worldData.timeSeriesMonthly);
key: date, worldData.cases.timeSeriesDaily = convertTsObjectToArray(worldData.cases.timeSeriesDaily);
value: worldData.timeSeriesDaily[date].value, worldData.cases.timeSeriesMonthly = convertTsObjectToArray(worldData.cases.timeSeriesMonthly);
delta: worldData.timeSeriesDaily[date].delta,
};
});
worldData.timeSeriesMonthly = Object.keys(worldData.timeSeriesMonthly).sort().map((date) => {
return {
key: date,
value: worldData.timeSeriesMonthly[date].value,
delta: worldData.timeSeriesMonthly[date].delta,
};
});
worldData.cases.timeSeriesDaily = Object.keys(worldData.cases.timeSeriesDaily).sort().map((date) => {
return {
key: date,
value: worldData.cases.timeSeriesDaily[date].value,
delta: worldData.cases.timeSeriesDaily[date].delta,
};
});
worldData.cases.timeSeriesMonthly = Object.keys(worldData.cases.timeSeriesMonthly).sort().map((date) => {
return {
key: date,
value: worldData.cases.timeSeriesMonthly[date].value,
delta: worldData.cases.timeSeriesMonthly[date].delta,
};
});
worldData.deathGrowthRate = getGrowthRate(worldData.timeSeriesDaily); worldData.deathGrowthRate = getGrowthRate(worldData.timeSeriesDaily);
worldData.casesGrowthRate = getGrowthRate(worldData.cases.timeSeriesDaily); worldData.casesGrowthRate = getGrowthRate(worldData.cases.timeSeriesDaily);
worldData.rollingAverageDaily = getRollingAverage(worldData); worldData.rollingAverageDaily = getRollingAverage(worldData);
worldData.doublingDaily = getDoublingTime(worldData); worldData.doublingDaily = getDoublingTime(worldData);
worldData.cases.rollingAverageDaily = getRollingAverage(worldData.cases); worldData.cases.rollingAverageDaily = getRollingAverage(worldData.cases);
worldData.cases.doublingDaily = getDoublingTime(worldData.cases);
worldData.population = 7781841000; worldData.population = 7781841000;
worldData.deathsPerMillion = worldData.total / worldData.population * 1000000; worldData.deathsPerMillion = worldData.total / worldData.population * 1000000;

View File

@ -25,6 +25,22 @@ html
bottom: -1px; bottom: -1px;
border-bottom: 2px solid #b5b5b5; border-bottom: 2px solid #b5b5b5;
} }
#hero-tooltip table {
border-collapse: collapse;
}
#hero-tooltip th, #hero-tooltip td {
padding: 2px 4px;
}
[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 { .geo-bg-dark {
background-color: #8e8e8e; background-color: #8e8e8e;
color: white; color: white;
@ -223,6 +239,7 @@ html
doubling, doubling,
totalCases, totalCases,
newCases, newCases,
doublingCases,
) { ) {
const canvas = document.getElementById(id); const canvas = document.getElementById(id);
charts.heroMaxValue = totalCases.reduce((max, value) => Math.max(max, value), 0); charts.heroMaxValue = totalCases.reduce((max, value) => Math.max(max, value), 0);
@ -234,7 +251,8 @@ html
const totalData = totalDeaths.slice(start, end); const totalData = totalDeaths.slice(start, end);
const newData = newDeaths.slice(start, end); const newData = newDeaths.slice(start, end);
const rollingData = rollingAverage.slice(start, end); const rollingData = rollingAverage.slice(start, end);
const doublingData = doubling.slice(start, end); const deathDoublingData = doubling.slice(start, end);
const caseDoublingData = doublingCases.slice(start, end);
const totalCaseData = totalCases.slice(start, end); const totalCaseData = totalCases.slice(start, end);
const newCaseData = newCases.slice(start, end); const newCaseData = newCases.slice(start, end);
@ -286,19 +304,30 @@ html
label: 'New Deaths', label: 'New Deaths',
data: newData, data: newData,
fill: false, fill: false,
borderColor: 'rgb(96, 96, 96, 0.25)', borderColor: 'rgb(20,24,59, 0.15)',
backgroundColor: 'rgb(96, 96, 96, 0.25)', backgroundColor: 'rgb(20,24,59, 0.15)',
borderWidth: 1, borderWidth: 1,
}, },
{ {
label: 'Days to 2x deaths', label: 'Days to 2x deaths',
data: doublingData, data: deathDoublingData,
fill: false, fill: false,
borderColor: 'rgb(187,40,193, 0.5)', borderColor: 'rgba(127,30,75,0.5)',
backgroundColor: 'rgb(187,40,193, 0.5)', backgroundColor: 'rgb(127,30,75, 0.5)',
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
} borderDash: [ 4, 4 ],
},
{
label: 'Days to 2x cases',
data: caseDoublingData,
fill: false,
borderColor: 'rgba(87,86,38,0.5)',
backgroundColor: 'rgb(87,86,38, 0.5)',
borderWidth: 2,
pointRadius: 0,
borderDash: [ 4, 4 ],
},
], ],
}, },
options: { options: {
@ -313,6 +342,102 @@ html
intersect: false, intersect: false,
position: 'middle', position: 'middle',
axis: 'x', axis: 'x',
enabled: false,
custom: function(tooltipModel) {
let tooltipEl = document.getElementById('hero-tooltip');
// Create element on first render
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.id = 'hero-tooltip';
const border = '1px solid #606060';
tooltipEl.innerHTML = `
<div class="text-center" style="font-size: 125%; border-bottom: ${border};">
<strong class="tooltip-title"></strong>
</div>
<table>
<tr>
<td><span class="tooltip-color-cases-total"></span></td>
<th>Total Cases</th>
<td class="tooltip-value-cases-total"></td>
</tr>
<tr>
<td><span class="tooltip-color-cases-new"></span></td>
<th>New Cases</th>
<td class="tooltip-value-cases-new"></td>
</tr>
<tr>
<td><span class="tooltip-color-deaths-total"></span></td>
<th>Total Deaths</th>
<td class="tooltip-value-deaths-total"></td>
</tr>
<tr>
<td><span class="tooltip-color-deaths-new"></span></td>
<th>New Deaths</th>
<td class="tooltip-value-deaths-new"></td>
</tr>
</table>
<div class="text-center pt-1" style="border-top: ${border}">
Deaths 2x every <strong class="tooltip-2x-deaths"></strong><br />
Cases 2x every <strong class="tooltip-2x-cases"></strong><br />
</div>
`;
tooltipEl.style.zIndex = '1000';
document.body.appendChild(tooltipEl);
}
if (tooltipModel.opacity === 0) {
tooltipEl.style.opacity = '0';
return;
}
tooltipEl.style.backgroundImage = 'linear-gradient(to bottom, rgba(52, 52, 52, 0.75), rgba(24, 24, 24, 0.75))';
tooltipEl.style.color = 'white';
tooltipEl.style.textShadow = '1px 1px 1px black';
tooltipEl.style.borderRadius = '2px';
tooltipEl.style.boxShadow = '2px 2px 3px rgba(0, 0, 0, 0.75)';
if (tooltipModel.dataPoints) {
tooltipEl.querySelector('.tooltip-title').textContent = tooltipModel.title.join(' ');
const setData = (cls, index, colorIndex) => {
colorIndex = typeof(colorIndex) === 'number' ? colorIndex : index;
const color = tooltipModel.labelColors[colorIndex];
const value = Number(tooltipModel.dataPoints[index].value).toLocaleString();
tooltipEl.querySelector('.tooltip-value-' + cls).textContent = value;
const colorEl = tooltipEl.querySelector('.tooltip-color-' + cls);
colorEl.style.backgroundColor = color.backgroundColor;
colorEl.style.borderColor = color.borderColor;
};
setData('cases-total', 0);
setData('cases-new', 1);
setData('deaths-total', 2);
setData('deaths-new', 4, 3);
const daysToDoubleDeaths = Number(tooltipModel.dataPoints[5].value).toLocaleString();
const daysToDoubleCases = Number(tooltipModel.dataPoints[6].value).toLocaleString();
tooltipEl.querySelector('.tooltip-2x-deaths').textContent = daysToDoubleDeaths + ' days';
tooltipEl.querySelector('.tooltip-2x-cases').textContent = daysToDoubleCases + ' days';
}
const position = this._chart.canvas.getBoundingClientRect();
// Display, position, and set styles for font
tooltipEl.style.opacity = '1';
tooltipEl.style.position = 'absolute';
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';
tooltipEl.style.pointerEvents = 'none';
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: { scales: {
yAxes: [ yAxes: [
@ -400,6 +525,7 @@ html
!{JSON.stringify(data.doublingDaily.map(x => x.value))}, !{JSON.stringify(data.doublingDaily.map(x => x.value))},
!{JSON.stringify(data.cases.timeSeriesDaily.map(x => x.value))}, !{JSON.stringify(data.cases.timeSeriesDaily.map(x => x.value))},
!{JSON.stringify(data.cases.timeSeriesDaily.map(x => x.delta))}, !{JSON.stringify(data.cases.timeSeriesDaily.map(x => x.delta))},
!{JSON.stringify(data.cases.doublingDaily.map(x => x.value))},
); );
mixin dataTable(items, label, type) mixin dataTable(items, label, type)