Published
Edited
Apr 7, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
timeSeriesChart = () => {
// default configuration
var width = 800;
var height = width * 0.5;
var timeSeries;
var colorScale;
var margin = {
top: 5,
right: 20,
bottom: 30,
left: 60
}
function timeSeriesChartReturnValue() {
const seriesNames = Object.keys(timeSeries[0]).filter(seriesName => seriesName != "date");
const seriesClass = seriesName => seriesName.includes('average') ? 'average' : 'positivity';
const yIsPercent = seriesNames.some(seriesName => seriesName.includes('positivity')); // boolean
const yMax = Math.max(...seriesNames.map(seriesName => d3.max(timeSeries.map(d => d[seriesName]))));

const xScale = d3.scaleTime()
.domain(d3.extent(timeSeries, d => d.date))
.range([margin.left, width - margin.right]);

const yScale = d3.scaleLinear()
.domain([0, yMax]).nice()
.range([height - margin.bottom, margin.top]);

const xAxis = g => g
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(xScale).ticks(d3.timeMonth, 1).tickSizeOuter(0).tickFormat(d3.timeFormat("%b %Y")));

const yAxis = g => g
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale).tickFormat(yIsPercent ? d3.format('.0%') : null));
const svg = d3.create('svg')
.attr("viewBox", [0, 0, width, height]);

svg.append("g")
.call(xAxis);

svg.append("g")
.call(yAxis);

const lineGenerator = seriesName =>
d3.line()
.defined(d => !isNaN(d[seriesName]))
.x(d => xScale(d.date))
.y(d => yScale(d[seriesName]));

const appendPath = seriesName => {
svg.append('g')
.append('path')
.datum(timeSeries)
.attr('d', lineGenerator(seriesName))
.attr('id', seriesName)
.attr('class', seriesClass(seriesName))
.style('stroke', colorScale(seriesName.split(' ')[0]))
.append('title')
.text(seriesName);
}

seriesNames.map(seriesName => appendPath(seriesName));

return svg.node();
}

// getter-setter methods that support method chaining
timeSeriesChartReturnValue.width = function(value) {
if (!arguments.length) return width;
width = value;
return this;
};

timeSeriesChartReturnValue.height = function(value) {
if (!arguments.length) return height;
height = value;
return this;
};

timeSeriesChartReturnValue.timeSeries = function(value) {
if (!arguments.length) return timeSeries;
timeSeries = value;
return this;
};

timeSeriesChartReturnValue.colorScale = function(value) {
if (!arguments.length) return colorScale;
colorScale = value;
return this;
};
timeSeriesChartReturnValue.margin = function(value) {
if (!arguments.length) return margin;
margin = value;
return this;
};

return timeSeriesChartReturnValue;

}
Insert cell
colorScale = d3.scaleOrdinal([zipCode, 'Chicago', 'Illinois'], d3.schemeSet1)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
illinoisCovidZipData = d3.csv(proxyUrl + dataUrl).then(response => response)
Insert cell
observations = illinoisCovidZipData
// parse
.map(d => ({
date: d.date,
zipcode: Number.parseInt(d.zipcode),
tests: Number.parseInt(d.total_tested_change),
cases: Number.parseInt(d.confirmed_cases_change)
}))
// exclude out-of-state
.filter(d => isIllinoisZipCode(d.zipcode))
// counts must be non-negative
.filter(d => d.tests >= 0)
.filter(d => d.cases >= 0)
// the two oldest dates are zero tests and zero cases for all ZIP codes
.filter(d => d.date != '2020-04-18')
.filter(d => d.date != '2020-04-19')
.sort((a, b) => a.date == b.date ? (a.zipcode > b.zipcode) : (a.date > b.date)) // ascending, by date, zip code within date
Insert cell
Insert cell
observedZipCodes = [...new Set(observations.map(d => d.zipcode))].sort((a, b) => a > b) // ascending
Insert cell
observedDates = [...new Set(observations.map(d => d.date))].sort((a, b) => a > b) // ascending
Insert cell
earliestDate = observedDates[0]
Insert cell
mostRecentDate = observedDates.slice(-1)[0]
Insert cell
// expected number of days, inclusive
expectedObservationCount = (() => {
const milliSecondsPerDay = 24 * 60 * 60 * 1000; // hours/day * minutes/hour * seconds/minute * milliseconds/second
return ((new Date(mostRecentDate)) - (new Date(earliestDate))) / milliSecondsPerDay + 1
})();
Insert cell
expectedDates = Array.from({length: expectedObservationCount}, (v, i) => {
const d = new Date(earliestDate);
d.setDate(d.getDate() + i);
return dateToString(d);
})
Insert cell
missingDates = expectedDates.filter(d => !observedDates.includes(d))
Insert cell
Insert cell
totalByDate = (observations) => (
groupReduce(
observations,
d => d.date,
(acc, curValue) => ({tests: acc.tests + curValue.tests, cases: acc.cases + curValue.cases}),
() => ({tests: 0, cases: 0})
)
)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
isIllinoisZipCode = z => z >= 60001 && z <= 62999
Insert cell
illinoisTotals = totalByDate(observations)
Insert cell
illinoisTestingTimeSeries = expectedDates
.map(expectedDate => {
const totals = illinoisTotals.get(expectedDate)
return {
date: new Date(expectedDate),
['Illinois tests']: totals ? totals.tests : NaN,
['Illinois cases']: totals ? totals.cases : NaN
}
})
Insert cell
illinoisPositivity = [...illinoisTotals.values()].map(d => positivity(d.tests, d.cases))
Insert cell
illinoisAveragePositivity = movingAverage(illinoisPositivity, 7)
Insert cell
illinoisPositivityTimeSeries = expectedDates
.map(expectedDate => {
const i = [...illinoisTotals.keys()].findIndex(observedDate => observedDate === expectedDate)
return {
date: new Date(expectedDate),
['Illinois positivity']: i === -1 ? NaN : illinoisPositivity[i],
['Illinois average positivity']: i === -1 ? NaN : illinoisAveragePositivity[i]
}
})
Insert cell
Insert cell
isChicagoZipCode = z => z >= 60601 && z <= 60699
Insert cell
chicagoObservations = observations
.filter(z => isChicagoZipCode(z.zipcode))
.sort((a, b) => a.date == b.date ? (a.zipcode > b.zipcode) : (a.date > b.date)) // ascending, by date, zip code within date
Insert cell
chicagoDates = new Set(chicagoObservations.map(d => d.date))
Insert cell
missingChicagoDates = expectedDates.filter(x => !chicagoDates.has(x)) // set difference
Insert cell
chicagoTotals = totalByDate(chicagoObservations)
Insert cell
chicagoTestingTimeSeries = expectedDates
.map(expectedDate => {
const totals = chicagoTotals.get(expectedDate)
return {
date: new Date(expectedDate),
['Chicago tests']: totals ? totals.tests : NaN,
['Chicago cases']: totals ? totals.cases : NaN
}
})
Insert cell
chicagoPositivity = [...chicagoTotals.values()].map(d => positivity(d.tests, d.cases))
Insert cell
chicagoAveragePositivity = movingAverage(chicagoPositivity, 7)
Insert cell
chicagoPositivityTimeSeries = expectedDates
.map(expectedDate => {
const i = [...chicagoTotals.keys()].findIndex(observedDate => observedDate === expectedDate)
return {
date: new Date(expectedDate),
['Chicago positivity']: i === -1 ? NaN : chicagoPositivity[i],
['Chicago average positivity']: i === -1 ? NaN : chicagoAveragePositivity[i]
}
})
Insert cell
Insert cell
zipCodeObservations = observations
.filter(x => x.zipcode == zipCode) // coercion
.map(({date, cases, tests}) => ({date, cases, tests})) // object destructuring to drop ZIP code property
.sort((a, b) => a.date > b.date) // ascending
Insert cell
Insert cell
missingZipCodeDates = expectedDates.filter(x => !zipCodeDates.has(x)) // set difference
Insert cell
zipCodeTestingTimeSeries = expectedDates
.map(expectedDate => {
const i = zipCodeObservations.findIndex(observation => observation.date === expectedDate)
return {
date: new Date(expectedDate),
[zipCode + ' tests']: i === -1 ? NaN : zipCodeObservations[i].tests,
[zipCode + ' cases']: i === -1 ? NaN : zipCodeObservations[i].cases
}
})
Insert cell
Insert cell
zipCodeAveragePositivity = movingAverage(zipCodePositivity, 7)
Insert cell
zipCodePositivityTimeSeries = expectedDates
.map(expectedDate => {
const i = zipCodeObservations.findIndex(observation => observation.date === expectedDate)
return {
date: new Date(expectedDate),
[zipCode + ' positivity']: i === -1 ? NaN : zipCodePositivity[i],
[zipCode + ' average positivity']: i === -1 ? NaN : zipCodeAveragePositivity[i]
}
})

Insert cell
Insert cell
positivityTimeSeries = expectedDates
.map(expectedDate => {
const illinoisPositivity = illinoisPositivityTimeSeries.find(x => dateToString(x.date) === expectedDate)
const chicagoPositivity = chicagoPositivityTimeSeries.find(x => dateToString(x.date) === expectedDate)
const zipCodePositivity = zipCodePositivityTimeSeries.find(x => dateToString(x.date) === expectedDate)
return {
'date': new Date(expectedDate),
['Illinois average positivity']: illinoisPositivity ? illinoisPositivity['Illinois average positivity'] : NaN,
['Chicago average positivity']: chicagoPositivity ? chicagoPositivity['Chicago average positivity'] : NaN,
[zipCode + ' average positivity']: zipCodePositivity ? zipCodePositivity[zipCode + ' average positivity'] : NaN
}
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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