Public
Edited
Sep 30, 2022
Insert cell
Insert cell
// Utility for computing moving averages from lists of data
// The raw covid numbers have weekly variation (see the chart below), so we'll use this to compute a moving average
import { computeMovingAverages } from "@kerzner/moving-average"
Insert cell
import { covidData } from "@kerzner/covid-data"
Insert cell
Insert cell
// d3.group, d3.groups, d3.rollup, d3.rollups
d3.rollups(
covidData,
(d) => d,
(d) => d.date
)
Insert cell
totals = d3
.rollups(
covidData,
(d) => d3.sum(d, (d) => d.cases),
(d) => d.date
)
.map(([d, c]) => ({ date: d, cases: c }))
Insert cell
Insert cell
{
const margin = { top: 10, right: 30, bottom: 30, left: 60 };
const width = 700 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

const svg = d3
.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

// This is the root group of the svg tree that will hold all the elements of our chart.
const root = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

const xScale = d3
.scaleTime()
.domain(d3.extent(totalWithAverages, (d) => d.date))
.range([0, width]);

// This group will hold the bottom axis
root
.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));

const yScale = d3
.scaleLinear()
.domain([0, d3.max(totalWithAverages, (d) => d.cases)])
.range([height, 0]);

root.append("g").call(d3.axisLeft(yScale));

// This line is a d3Shape, which is useful for
// generating svg paths from data.
// For more info, see the links in the lecture slides.
const line = d3
.line()
.x((d) => xScale(d.date))
.y((d) => yScale(d.cases));

root
.append("path")
.datum(totalWithAverages)
.attr("fill", "none")
.attr("opacity", 0.2)
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", line);

const averagesLine = d3
.line()
.x((d) => xScale(d.date))
.y((d) => yScale(d.average));

root
.append("path")
.datum(totalWithAverages)
.attr("fill", "none")
.attr("opacity", 1)
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", averagesLine);

return svg.node();
}
Insert cell
// With moving averages
totalWithAverages = {
const averages = computeMovingAverages(
totals.map((d) => d.cases),
7
);

return totals.map((d, i) => ({ ...d, average: averages[i] }));
}
Insert cell
statesWithAverages = {
const byState = d3.group(covidData, (d) => d.state);
for (let [key, points] of byState) {
const averages = computeMovingAverages(
points.map((d) => d.cases),
7
);
averages.forEach((a, i) => (byState.get(key)[i].average = a));
}
return byState;
}
Insert cell
Insert cell
{
const margin = { top: 10, right: 10, bottom: 30, left: 60 };
const width = 700 - margin.left - margin.right;
const height = 1500 - margin.top - margin.bottom;

const svg = d3
.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

const group = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// We'll use the y-axis to position the state labels on the side of the chart.
const yScale = d3
.scalePoint()
.domain([...statesWithAverages.keys()].sort())
.range([margin.top, height - margin.bottom]);
const yAxis = d3.axisLeft(yScale).tickSize(0).tickPadding(4);
group
.append("g")
.call(yAxis)
.call((g) => g.select(".domain").remove()); // comment-out this line to see what element it removes from the chart.

// This is a key peice of D3 magic here.
const stateGroups = group
.append("g") // this is a new group that will hold the ridge for each state
.selectAll("g") // within the new group, create a group for each state
.data(yScale.domain())
.join("g")
.attr("transform", (d) => `translate(0,${yScale(d)})`); // position the state group at the appropriate vertical position

// We'll draw the ridges using an xScale and "zScale"
// The name zScale is arbitrary; it's really a yScale, but I don't want to reuse the same name
const xDomain = d3.extent(covidData, (d) => d.date);
const xScale = d3.scaleTime().domain(xDomain).range([0, width]);

const overlap = 8; // defines how much the ridges are allowed to occlude others
const maxValue = d3.max(covidData, (d) => d.cases);
const zScale = d3
.scaleLinear()
.domain([0, maxValue])
.range([0, -(overlap * yScale.step())]);

// Create a z-scale for each state, stored in a object zScales[state]
//

// We're using a d3.area, which is another shape generator.
const area = d3
.area()
.curve(d3.curveBasis)
.defined((d) => d)
.x((d) => xScale(d.date))
.y0(0)
.y1((d) => zScale(d.average));

const line = area.lineY1();

// Notice: as we're appending paths, we're manipulating every group in the selection of stateGroups.
stateGroups
.append("path")
.datum((d) => statesWithAverages.get(d))
.attr("fill", "#ddd")
.attr("d", (d) => area(d));

stateGroups
.append("path")
.datum((d) => statesWithAverages.get(d))
.attr("fill", "none")
.attr("stroke", "black")
.attr("d", (d) => line(d));

return svg.node();
}
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