Published
Edited
Mar 1, 2022
20 stars
COVID Infection and Vaccination
US COVID-19 Streamgraph
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
controller = chart.update(dateIdx)
Insert cell
lines = svg => {
let chartG = svg
.append('g')
.attr('transform', `translate(${[height + 10, 300]})`);
let lineG = chartG.selectAll('path');
let pointG = chartG.selectAll('circle');
return idx => {
const filteredData = stackedLineData.map(d => ({
...d,
line: d.line.filter(t => t[0] <= idx),
point: d.line.find(t => t[0] == idx) || [0, 0]
}));
lineG = lineG
.data(filteredData, d => d.status)
.join(
enter =>
enter
.append('path')
.attr('test', d => console.log('enter:', d))
.attr('d', d => line(d.line))
.attr('fill', 'none')
.attr('stroke', d => statusToColor[d.status]),
update =>
update
.attr('test', d => console.log('update:', d))
.attr('d', d => line(d.line))
);
pointG = pointG
.data(filteredData, d => d.status)
.join(
enter =>
enter
.append('circle')
.attr('r', 2)
.attr('display', d => (d.point[1] ? null : 'none'))
.attr('fill', d => statusToColor[d.status])
.attr('cx', d => lineXScale(d.point[0]))
.attr('cy', d => lineYScale(d.point[1])),
update =>
update
.attr('display', d => (d.point[1] ? null : 'none'))
.attr('cx', d => lineXScale(d.point[0]))
.attr('cy', d => lineYScale(d.point[1]))
);
};
}
Insert cell
stackedLineData.map(d => ({
...d,
line: d.line.filter(t => t[0] <= 20),
point: d.line.find(t => t[0] == 20) || [0, 0]
}))
Insert cell
dateLabel = svg => {
let labelG = svg
.append('text')
.attr('font-size', 20)
.attr('text-anchor', 'end')
.attr('transform', `translate(${[height, margin.top - 10]})`);
let format = d3.timeFormat('%b %d, %Y');
return idx => {
labelG.text(format(dateRange[idx]));
};
}
Insert cell
vaccineMatrix = svg => {
let covidG = svg
.append('g')
.attr(
'transform',
`rotate(90) translate(${[0, -height - margin.left - margin.right]})`
)
.selectAll('rect');
return idx => {
covidG = covidG.data(allMatrixData[idx].vaccine).join(
enter =>
enter
.append('rect')
.attr('width', matrixScale.bandwidth() * .95)
.attr('height', matrixScale.bandwidth() * .95)
.attr(
'transform',
(d, i) =>
`translate(${[
matrixScale(i % matrixWidth),
matrixScale(Math.floor(i / matrixWidth))
]})`
)
.attr('fill', d => statusToColor[d])
.style('display', d => (d == status.NONE ? 'none' : null)),
update =>
update
.attr('fill', d => statusToColor[d])
.style('display', d => (d == status.NONE ? 'none' : null))
);
};
}
Insert cell
covidMatrix = svg => {
let covidG = svg.append('g').selectAll('rect');
return idx => {
covidG = covidG.data(allMatrixData[idx].covid).join(
enter =>
enter
.append('rect')
.attr('width', matrixScale.bandwidth())
.attr('height', matrixScale.bandwidth())
.attr(
'transform',
(d, i) =>
`translate(${[
matrixScale(i % matrixWidth),
matrixScale(Math.floor(i / matrixWidth))
]})`
)
.attr('fill', d => statusToColor[d]),
update => update.attr('fill', d => statusToColor[d])
);
};
}
Insert cell
labels = svg => {
let labelG = svg
.append('g')
.attr('transform', `translate(${[height + 10, margin.top + 15]})`)
.selectAll('g');
return idx => {
labelG = labelG
.data(allLabelData[idx], d => d.label)
.join(
enter =>
enter
.append('g')
.attr(
'transform',
(d, i) => `translate(${[0, i * 40 + d.section * 20]})`
)
.attr('display', d => (d.count == '0 People' ? 'none' : null))
.call(g =>
g
.append('text')
.attr('class', 'label')
.text(d => d.label)
.attr('fill', d => d.color)
)
.call(g =>
g
.append('text')
.attr('class', 'percent')
.text(d => d.percent)
.attr('transform', `translate(0,15)`)
.attr('font-size', 12)
)
.call(g =>
g
.append('text')
.attr('class', 'count')
.text(d => d.count)
.attr('text-anchor', 'end')
.attr('transform', `translate(250,15)`)
.attr('font-size', 12)
),
update =>
update
.attr('display', d => (d.count == '0 People' ? 'none' : null))
.call(g => g.select('.label').text(d => d.label))
.call(g => g.select('.percent').text(d => d.percent))
.call(g => g.select('.count').text(d => d.count))
);
};
}
Insert cell
Insert cell
allLabelData = dateRange.map((_d, i) => {
const labels = [];
Object.values(displayOrder).forEach((section, sIdx) =>
section.forEach(status =>
labels.push({
section: sIdx,
label: statusToLabelText[status],
color: statusToColor[status],
count:
Math.round(realCountData[i][status]).toLocaleString() + ' People',
percent:
d3.format('.2r')((realCountData[i][status] / usPop) * 100) +
'% of U.S.'
})
)
);
return labels;
})
Insert cell
statusToColor = ({
[status.NONE]: '#c2c2c2',
[status.SUSPECTED_COVID]: '#e78',
[status.CONFIRMED_COVID]: '#d35',
[status.COVID_DEATH]: '#638',
[status.PARTIAL_VACCINATION]: '#69d',
[status.FULL_VACCINATION]: '#37c'
})
Insert cell
statusToLabelText = ({
[status.SUSPECTED_COVID]: 'Suspected COVID Infection',
[status.CONFIRMED_COVID]: 'Confirmed COVID Case',
[status.COVID_DEATH]: 'Confirmed COVID Death',
[status.PARTIAL_VACCINATION]: 'Partial Vaccination',
[status.FULL_VACCINATION]: 'Full Vaccination'
})
Insert cell
line = d3
.line()
.x(d => lineXScale(d[0]))
.y(d => lineYScale(d[1]))
Insert cell
lineYScale = d3
.scaleLinear()
.domain([0, d3.max(stackedLineData, d => d3.max(d.line, t => t[1]))])
.range([120, 0])
Insert cell
lineXScale = d3
.scaleLinear()
.domain(d3.extent(dateRange, (_, i) => i))
.range([0, 245])
Insert cell
matrixScale = d3
.scaleBand()
.domain(new Array(matrixWidth).fill(0).map((_, i) => i))
.range([margin.top, height - margin.bottom])
.paddingInner(0.01)
Insert cell
margin = ({
left: 20,
right: 20,
top: 40,
bottom: 0
})
Insert cell
height = 550
Insert cell
Insert cell
stackedLineData = {
const makeLine = fn => {
return realCountData.map((d, i) => [i, fn(d)]).filter(d => d[1]);
};
return [
{
status: status.SUSPECTED_COVID,
line: makeLine(
d =>
d[status.COVID_DEATH] +
d[status.CONFIRMED_COVID] +
d[status.SUSPECTED_COVID]
)
},
{
status: status.CONFIRMED_COVID,
line: makeLine(d => d[status.COVID_DEATH] + d[status.CONFIRMED_COVID])
},
{
status: status.PARTIAL_VACCINATION,
line: makeLine(
d => d[status.FULL_VACCINATION] + d[status.PARTIAL_VACCINATION]
)
},
{
status: status.FULL_VACCINATION,
line: makeLine(d => d[status.FULL_VACCINATION])
}
];
}
Insert cell
allMatrixData = dateRange.map((_d, i) => ({
covid: makeMatrixData(dotCountData[i], 'covid'),
vaccine: makeMatrixData(dotCountData[i], 'vaccine')
}))
Insert cell
makeMatrixData = (dotCounts, mode) => {
dotCounts = dotCounts[mode];
let currentStatusIndex = 0;
let currentStatusCount = 0;
const data = [];
for (let i = 0; i < matrixWidth * matrixWidth; i++) {
if (currentStatusCount >= dotCounts[currentStatusIndex]) {
currentStatusIndex += 1;
currentStatusCount = 0;
if (currentStatusCount >= dotCounts[currentStatusIndex]) {
currentStatusIndex += 1;
currentStatusCount = 0;
if (currentStatusCount >= dotCounts[currentStatusIndex]) {
currentStatusIndex += 1;
currentStatusCount = 0;
}
}
}
data.push(displayOrder[mode][currentStatusIndex] || status.NONE);
currentStatusCount += 1;
}
return data;
}
Insert cell
dotCountData = dateRange.map((_, i) => {
return {
covid: displayOrder['covid'].map(status =>
Math.round(realCountData[i][status] / popPerDot)
),
vaccine: displayOrder['vaccine'].map(status =>
Math.round(realCountData[i][status] / popPerDot)
)
};
})
Insert cell
displayOrder = ({
covid: [status.COVID_DEATH, status.CONFIRMED_COVID, status.SUSPECTED_COVID],
vaccine: [status.FULL_VACCINATION, status.PARTIAL_VACCINATION]
})
Insert cell
popPerDot = usPop / matrixWidth / matrixWidth
Insert cell
matrixWidth = 32
Insert cell
Insert cell
Insert cell
realCountData = dateRange.map(date => {
const covidData = cumulativeCovid.find(
d => d[0].toISOString() == date.toISOString()
);
const vaxData = cumulativeVaccine.find(
d => d[0].toISOString() == date.toISOString()
) || [
null,
{
partial: 0,
full: 0
}
];
return {
[status.SUSPECTED_COVID]:
covidData[1].suspectedCases - covidData[1].cases - covidData[1].deaths,
[status.CONFIRMED_COVID]: covidData[1].cases - covidData[1].deaths,
[status.COVID_DEATH]: covidData[1].deaths,
[status.PARTIAL_VACCINATION]: vaxData[1].partial - vaxData[1].full,
[status.FULL_VACCINATION]: vaxData[1].full
};
})
Insert cell
status = ({
NONE: 0,
SUSPECTED_COVID: 1,
CONFIRMED_COVID: 2,
COVID_DEATH: 3,
PARTIAL_VACCINATION: 4,
FULL_VACCINATION: 5
})
Insert cell
cumulativeVaccine = {
const initialData = usVaccineDataRaw
.map(d => [
d.date,
{ partial: d.people_vaccinated, full: d.people_fully_vaccinated }
])
.filter(d => d[1].partial || d[1].full);

const day_in_ms = 86400000;
let vaxStart = null;
for (var i = 0; i < initialData.length - 1; i++) {
if (initialData[i + 1][0] - initialData[i][0] > 86400000) {
const missingDate = new Date(
new Date(initialData[i][0]).setDate(initialData[i][0].getDate() + 1)
);
const missingData = {
partial:
(initialData[i + 1][1].partial + initialData[i][1].partial) / 2,
full: (initialData[i + 1][1].full + initialData[i][1].full) / 2
};
initialData.splice(i + 1, 0, [missingDate, missingData]);
}
}

return initialData;
}
Insert cell
dateRange = cumulativeCovid.map(d => d[0]).slice(0, -1)
Insert cell
cumulativeCovid = {
const dailyCovid = d3.rollups(
stateCovidDataRaw,
d => ({ cases: d3.sum(d, t => t.cases), deaths: d3.sum(d, t => t.deaths) }),
d => d.date
);
let cases = 0,
deaths = 0,
suspectedCases = 0;
dailyCovid.forEach(d => {
cases += d[1].cases;
deaths += d[1].deaths;
const suspectedCasesObj = suspectedInfectionsRaw.find(
t => t.date.toISOString() == d[0].toISOString()
);
if (
suspectedCasesObj != undefined &&
suspectedCasesObj.total_infected_mean != null
) {
suspectedCases = suspectedCasesObj.total_infected_mean;
} else {
suspectedCases += suspectedCaseMultiplier * d[1].cases;
}
d[1] = {
cases,
deaths,
suspectedCases
};
});
return dailyCovid;
}
Insert cell
suspectedCaseMultiplier = 2.5
Insert cell
Insert cell
suspectedInfectionsRaw = d3.csv(
'https://raw.githubusercontent.com/youyanggu/covid19-infection-estimates-latest/main/latest_all_estimates_us.csv',
d3.autoType
)
Insert cell
usVaccineDataRaw = d3.csv(
'https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/vaccinations/country_data/United%20States.csv',
d3.autoType
)
Insert cell
stateCovidDataRaw = d3.csv(
'https://raw.githubusercontent.com/nytimes/covid-19-data/master/rolling-averages/us-states.csv',
d3.autoType
)
Insert cell
Insert cell
import { Scrubber } from "@mbostock/scrubber"
Insert cell
d3 = require('d3')
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more