|
|
|
// SARS-CoV-2-Viz
|
|
|
|
// Animated COVID case count visualization
|
|
|
|
// Copyright 2022 Edward L. Platt <ed@elplatt.com>
|
|
|
|
|
|
|
|
const covidUrl = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_US.csv";
|
|
|
|
|
|
|
|
// Parse covid tsv
|
|
|
|
async function parseCovidData(tsv, onProgress) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
|
|
let rows = tsv.split(/[\r\n]+/);
|
|
|
|
let keys = rows[0].split("\t");
|
|
|
|
let dates = keys.slice(11);
|
|
|
|
|
|
|
|
// Initialize data array
|
|
|
|
let dataForDay = [];
|
|
|
|
for (const date of dates) {
|
|
|
|
dataForDay.push([]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Iterate through non-header rows
|
|
|
|
const rowCount = rows.length - 1;
|
|
|
|
let percent = 0;
|
|
|
|
let rowStack = rows.slice(1).reverse();
|
|
|
|
let rowIndex = 0;
|
|
|
|
|
|
|
|
let doRows = () => {
|
|
|
|
while (true) {
|
|
|
|
|
|
|
|
if (rowStack.length == 0) {
|
|
|
|
resolve(dataForDay);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
row = rowStack.pop();
|
|
|
|
const rowData = tsvRowToJSON(keys, row);
|
|
|
|
|
|
|
|
let FIPS = rowData.FIPS;
|
|
|
|
if (typeof(FIPS) === "undefined") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
FIPS = FIPS.slice(0, FIPS.indexOf(".") || FIPS.length).padStart(5, "0");
|
|
|
|
|
|
|
|
let lastSeven = [];
|
|
|
|
for (const [index, date] of dates.entries()) {
|
|
|
|
|
|
|
|
const newCases = index == 0 ?
|
|
|
|
parseInt(rowData[date]) :
|
|
|
|
parseInt(rowData[date])
|
|
|
|
- parseInt(rowData[dates[index - 1]]);
|
|
|
|
|
|
|
|
// Update array for running average
|
|
|
|
if (lastSeven.length == 7) {
|
|
|
|
lastSeven.shift();
|
|
|
|
}
|
|
|
|
lastSeven.push(newCases);
|
|
|
|
const mean = Math.round(
|
|
|
|
lastSeven.reduce((x, y) => x + y) / lastSeven.length);
|
|
|
|
|
|
|
|
dataForDay[index].push({
|
|
|
|
FIPS: FIPS,
|
|
|
|
date: date,
|
|
|
|
count: newCases,
|
|
|
|
sevenDayMean: mean
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only update progress when it has actually changed
|
|
|
|
rowIndex += 1
|
|
|
|
let newPercent = Math.floor(100 * rowIndex / rowCount);
|
|
|
|
if (newPercent > percent) {
|
|
|
|
percent = newPercent;
|
|
|
|
onProgress(`${rowIndex} of ${rowCount} rows`);
|
|
|
|
setTimeout(doRows, 0);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTimeout(doRows, 0);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function findMaxima(dataForDay, onProgress) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
// Calculate maximum case count for each day
|
|
|
|
let maxSoFar = 0;
|
|
|
|
let day = 0;
|
|
|
|
let dateCount = dataForDay.length;
|
|
|
|
let percent = 0;
|
|
|
|
let doMax = () => {
|
|
|
|
while (true) {
|
|
|
|
|
|
|
|
if (day == dateCount) {
|
|
|
|
// Convert day's data from array to object keyed on FIPS
|
|
|
|
let result = dataForDay.map(data => {
|
|
|
|
const entries = new Map(data.map(d => [d.FIPS, d]));
|
|
|
|
return Object.fromEntries(entries);
|
|
|
|
});
|
|
|
|
resolve(result);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
let dayMax = Math.max(
|
|
|
|
...dataForDay[day].map(
|
|
|
|
d => d.sevenDayMean));
|
|
|
|
|
|
|
|
maxSoFar = Math.max(maxSoFar, dayMax);
|
|
|
|
|
|
|
|
// Add data to each county for current day
|
|
|
|
for (let [countyIndex, d] of dataForDay[day].entries()) {
|
|
|
|
d.dayMax = dayMax;
|
|
|
|
d.maxSoFar = maxSoFar;
|
|
|
|
}
|
|
|
|
|
|
|
|
day += 1;
|
|
|
|
let newPercent = Math.floor(100 * day / dateCount);
|
|
|
|
if (newPercent > percent) {
|
|
|
|
percent = newPercent;
|
|
|
|
onProgress(`${day} of ${dateCount} rows`);
|
|
|
|
setTimeout(doMax, 0);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTimeout(doMax, 0);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getData(onProgress) {
|
|
|
|
const content = fetchWithProgress(covidUrl, onProgress);
|
|
|
|
return content;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function parseData(content, onProgress) {
|
|
|
|
onProgress("initializing...");
|
|
|
|
let tsv = await csvToTSV(
|
|
|
|
content,
|
|
|
|
onProgress);
|
|
|
|
|
|
|
|
let parsed = await parseCovidData(
|
|
|
|
tsv,
|
|
|
|
(progress) => onProgress(`building data structure: ${progress}`));
|
|
|
|
|
|
|
|
let dataForDay = await findMaxima(
|
|
|
|
parsed,
|
|
|
|
(progress) => onProgress(`finding maxima: ${progress}`));
|
|
|
|
|
|
|
|
onProgress("done");
|
|
|
|
return dataForDay;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Add normalization data to each data element in dataForDay.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
dataForDay: [{FIPS_1: d1, FIPS_2: d2, ...}, ... ]
|
|
|
|
*/
|
|
|
|
function normalizeData(dataForDay, population) {
|
|
|
|
let maxSoFar = 0;
|
|
|
|
let maxSoFarPer100KCap = 0;
|
|
|
|
let normalizedForDay = [];
|
|
|
|
|
|
|
|
for (const [day, data] of dataForDay.entries()) {
|
|
|
|
|
|
|
|
let normalized = {};
|
|
|
|
|
|
|
|
// Add normalized counts
|
|
|
|
for (let [FIPS, d] of Object.entries(data)) {
|
|
|
|
const countyPop = population[FIPS];
|
|
|
|
if (FIPS
|
|
|
|
&& countyPop
|
|
|
|
&& !Number.isNaN(countyPop)
|
|
|
|
&& countyPop > 0) {
|
|
|
|
d.sevenDayMeanPer100KCap = 100000 * d.sevenDayMean / countyPop;
|
|
|
|
normalized[FIPS] = d;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate daily maxima
|
|
|
|
let dayMax = Math.max(
|
|
|
|
...Object.entries(normalized).map(
|
|
|
|
(entry) => entry[1].sevenDayMean));
|
|
|
|
let dayMaxPer100KCap = Math.max(
|
|
|
|
...Object.entries(normalized).map(
|
|
|
|
(entry) => entry[1].sevenDayMeanPer100KCap));
|
|
|
|
|
|
|
|
maxSoFar = Math.max(maxSoFar, dayMax);
|
|
|
|
maxSoFarPer100KCap = Math.max(maxSoFarPer100KCap, dayMaxPer100KCap);
|
|
|
|
|
|
|
|
// Add data to each county for current day
|
|
|
|
for (let [FIPS, d] of Object.entries(normalized)) {
|
|
|
|
d.dayMax = dayMax;
|
|
|
|
d.maxSoFar = maxSoFar;
|
|
|
|
d.dayMaxPer100KCap = dayMaxPer100KCap;
|
|
|
|
d.maxSoFarPer100KCap = maxSoFarPer100KCap;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add normalized datum to result
|
|
|
|
normalizedForDay.push(normalized);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Go back and add overall maxima
|
|
|
|
for (let [day, normalized] of normalizedForDay.entries()) {
|
|
|
|
for (let [FIPS, d] of Object.entries(normalized)) {
|
|
|
|
d.maxPer100KCap = maxSoFarPer100KCap;
|
|
|
|
d.max = maxSoFar;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return normalizedForDay;
|
|
|
|
}
|