Published
Edited
Mar 1, 2022
20 stars
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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more