You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

158 lines
4.1 KiB
JavaScript

// 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
function parseCovidData(tsv, onProgress) {
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;
for (const [index, row] of rows.slice(1).entries()) {
onProgress("0 of ${rowCount} rows");
const rowData = tsvRowToJSON(keys, row);
// Only update progress when it has actually changed
let newPercent = Math.floor(100 * index / rowCount);
if (newPercent > percent) {
percent = newPercent;
}
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
});
}
}
onProgress("${rowCount} of ${rowCount}");
// Calculate maximum case count for each day
let maxSoFar = 0;
for (const [day, date] of dates.entries()) {
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;
}
}
// Convert day's data from array to object keyed on FIPS
return dataForDay.map(data => {
const entries = new Map(data.map(d => [d.FIPS, d]));
return Object.fromEntries(entries);
});
}
async function getData(onProgress) {
const content = await fetchWithProgress(covidUrl, onProgress);
return content;
}
async function parseData(content, onProgress) {
onProgress("initializing...");
let tsv = csvToTSV(
content,
(progress) => onProgress("parsing csv: {$progress}"));
let dataForDay = parseCovidData(
tsv,
(progress) => onProgress("building data structure: {$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);
}
return normalizedForDay;
}