Published
Edited
Dec 3, 2021
7 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
rawCountyData = d3.csv(
'https://raw.githubusercontent.com/nytimes/covid-19-data/master/us-counties.csv',
d => {
// NYT's data doesn't assign NYC a FIPS, so we manually assign if the FIPS the census
// uses for "New York County"
if (d.county === "New York City") d.fips = "36061"
// Parse the time _not_ using UTC midnight
d.date = d3.timeParse('%Y-%m-%d')(d.date)
// Convert cases and deaths to numbers
d.cases = +d.cases
d.deaths = +d.deaths
return d
}
)
Insert cell
Insert cell
countyData = d3.nest().key(d => d.fips).entries(rawCountyData)
Insert cell
Insert cell
countyPopulations = d3.csv(
await FileAttachment("ACSDT5Y2018.B01003_data_with_overlays_2020-06-12T132213.csv").url(),
d => {
if (d.GEO_ID === 'id') return null
return {
fips: d.GEO_ID.replace('0500000US', ''),
name: d.NAME,
population: +d.B01003_001E,
}
}
)
Insert cell
countyPopulations.filter(c => c.name.includes('New York'))
Insert cell
Insert cell
annotatedCounties = countyData.map(({ key, values }) => {
const latestDate = d3.max(values, v => v.date)

// We get 15 days worth of data so we can calculate deltas for 14 days
const startDate = dfns.sub(latestDate, { weeks: 2, days: 1 })

const countyPopulationObj = countyPopulations.find(c => +c.fips === +key)
const totalPopulation = countyPopulationObj ? +countyPopulationObj.population : null
// Annotate the data with deltas
const valuesWithDeltas = values.filter(v => v.date > startDate).sort((a, b) => a.date - b.date).map(
// Add new cases and new deaths as difference between previous day. Return null for day -15
({ cases, deaths, ...rest }, i, arr) => (i > 0 ? {
...rest,
cases,
newCases: cases - arr[i-1].cases,
deaths,
newDeaths: deaths - arr[i-1].deaths,
} : null)
// Remove the null value representing day -15
).filter(v => !!v)

// Use these points to calculate a regression of new cases per capita
const regressionPoints = valuesWithDeltas.filter(v => v.date > startDate).map(v => [
dfns.differenceInDays(v.date, startDate),
v.newCases / totalPopulation
])
// Total all cases for the past two weeks
const totalCases = d3.sum(valuesWithDeltas, d => d.newCases)

// Return data annotated with deltas and the regression
return {
county: valuesWithDeltas.length > 0 ? valuesWithDeltas[0].county : null,
state: valuesWithDeltas.length > 0 ? valuesWithDeltas[0].state : null,
fips: key,
days: valuesWithDeltas,
totalPopulation,
regressionPoints,
regression: totalCases < caseThreshold ? null : ss.linearRegression(regressionPoints),
}
})
Insert cell
Insert cell
Insert cell
colorScale = {
// The following code generates a linear scale with endpoints based on the data's
// 80% and 20% percentiles. I've retired it in favor of a static 10-log scale, but
// have left the code intact in case I decide to bring it back.
//
// const stepCount = 11
// const scaleMax = Math.max(
// d3.quantile(annotatedCounties, 0.8, d => d.regression.m),
// Math.abs(d3.quantile(annotatedCounties, 0.2, d => d.regression.m))
// )
// // How many steps are in half the scale?
// const stepSize = scaleMax * 2 / (stepCount)
// // Define the domain as an array of thresholds
// // We increase the max value a little bit to ensure that the max value doesn't get
// // left out. Float arithmetic can be a little tricky sometimes.
// const domain = d3.range(-scaleMax + stepSize, scaleMax - (stepSize / 2), stepSize)
return d3.scaleThreshold(
[
-1/100000,
-1/1000000,
-1/10000000,
1/10000000,
1/1000000,
1/100000,
],
[...d3.schemeRdBu[7]].reverse()
)
}
Insert cell
Insert cell
countyChart = datum => {
const chartWidth = 400
const chartHeight = chartWidth * 0.5
const margin = { left: 40, right: 40, top: 10, bottom: 20 }
const formatNumber = d3.format('.2r')
const xScale = d3.scaleTime()
.domain(d3.extent(datum.days, d => d.date))
.range([margin.left, chartWidth - margin.right])
const yScale = d3.scaleLinear()
.domain([0, d3.max(datum.days, d => d.newCases)])
.range([chartHeight - margin.bottom, margin.top])
// Undo the conversion to per-capita while generating the linear regression line
const linearRegressionLine = ss.linearRegressionLine({
m: datum.regression.m * datum.totalPopulation,
b: datum.regression.b * datum.totalPopulation,
})
// Get linear regression in the form of x more cases per day per day for display
const perDay = datum.regression.m * datum.totalPopulation
// Get slope as rendered in degrees for rendering text along the regression line
const slope = Math.atan(
(yScale(linearRegressionLine(0)) - yScale(linearRegressionLine(14)))
/
(margin.left - chartWidth + margin.right)
) * 180 / Math.PI
// Prepare grid measurements
const xLabelTicks = xScale.ticks(d3.timeDay.every(2))
const xGridTicks = xScale.ticks(d3.timeDay.every(1))
return html`
<svg viewbox="0 0 ${chartWidth} ${chartHeight}">
<!-- grid -->
<g>
${xLabelTicks.map(date => svg`
<text
x="${xScale(date)}"
y="${chartHeight - margin.bottom + 15}"
font-size="11"
font-family="var(--sans-serif)"
text-anchor="middle"
>${d3.timeFormat('%-m/%-d')(date)}</text>
`)}
${xGridTicks.map(date => svg`
<line
x1="${xScale(date)}"
x2="${xScale(date)}"
y1="0"
y2="${xLabelTicks.find(d => dfns.isEqual(d, date)) ? chartHeight - margin.bottom : chartHeight}"
stroke="#EEE"
/>
`)}
</g>

<!-- data points -->
<g>
${datum.days.map(d => svg`
<circle cx="${xScale(d.date)}" cy="${yScale(d.newCases)}" r="3" fill="#666" />
<text
x="${xScale(d.date) + 6}"
y="${yScale(d.newCases) + 4}"
font-size="11"
font-family="var(--sans-serif)"
fill="#CCC"
>${d.newCases}</text>
`)}
</g>

<!-- regression line -->
<line
x1="${margin.left}"
x2="${chartWidth - margin.right}"
y1="${yScale(linearRegressionLine(0))}"
y2="${yScale(linearRegressionLine(14))}"
stroke="#444"
stroke-dasharray="3"
/>

<!-- regression label -->

<text
x="${margin.left + chartWidth / 2 - margin.right}"
y="${yScale(linearRegressionLine(7)) - 7}"
transform-origin="${margin.left + chartWidth / 2 - margin.right} ${yScale(linearRegressionLine(7)) - 7}"
text-anchor="middle"
transform="rotate(${slope})"
font-size="12"
font-family="var(--sans-serif)"
paint-order="stroke"
stroke="#FFF"
stroke-width="4"
>
daily cases ${perDay > 0 ? 'rising' : 'falling'} by
<tspan font-weight="bold">
~${formatNumber(Math.abs(perDay))}
</tspan>
per day
</text>

<!-- start and end indicators -->

<circle cx="${margin.left}" cy="${yScale(linearRegressionLine(0))}" r="2" fill="#444" />
<text
x="${margin.left - 5}"
y="${yScale(linearRegressionLine(0)) + 5}"
text-anchor="end"
font-size="12"
font-family="sans-serif"
>${formatNumber(linearRegressionLine(0))}</text>

<circle cx="${chartWidth - margin.right}" cy="${yScale(linearRegressionLine(14))}" r="2" fill="#444" />
<text
x="${chartWidth - margin.right + 5}"
y="${yScale(linearRegressionLine(14)) + 3}"
font-size="12"
font-family="sans-serif"
text-anchor="start"
>${formatNumber(linearRegressionLine(14))}</text>
</svg>
`
}
Insert cell
countyTable = (days) => html`
<table class="county-tables__table">
${days.slice(0, 7).map(d => html`
<tr>
<th>
${d3.timeFormat('%-m/%-d')(d.date)}
</th>
<td>
${d.newCases}
</td>
<td>
</td>
</tr>
`)}
</table>
`
Insert cell
countyTables = (datum) => html`
<div class="county-tables">
<header class="county-tables__header">
<h2 class="county-tables__name">${datum.county}, ${datum.state}</h2>
<div class="county-tables__population">Pop. ${d3.format(',d')(datum.totalPopulation)}</div>
</header>
<div class="county-tables__tables">
${[
countyTable(datum.days.slice(0, 7)),
countyTable(datum.days.slice(7, 14)),
]}
</div>
</div>
`
Insert cell
countySummary = (datum) => html`
<div class="county-summary">
<div class="county-summary__columns">
${[countyTables(datum), html`<div class="county-chart">${countyChart(datum)}</div>`]}
</div>
</div>
`
Insert cell
Insert cell
Insert cell
Insert cell
html`<style type="text/css">
.county-summary__columns {
display: flex;
flex-direction: row;
}

@media(max-width: 600px) {
.county-summary__columns {
flex-direction: column;
}

.county-chart {
width: 100%;
margin-top: 20px;
flex-grow: 1;
}
}

.county-chart {
margin-right: 20px;
width: 400px;
flex-grow: 0;
}

.county-tables {
flex-grow: 1;
}

.county-tables__name, .county-tables__population {
display: inline;
}
.county-tables__population {
font-family: var(--sans-serif);
color: #666;
}

.county-tables__tables {
display: flex;
flex-direction: row;
justify-content: space-between;
}

.county-tables__table {
width: 48%;
}
</style>`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
us = FileAttachment("counties-albers-10m.json").json()
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