Published
Edited
Mar 11, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
covid = d3.csvParse(await FileAttachment("VDH-COVID-19-PublicUseDataset-Cases.csv").text(), function(d) {
return {
date: d3.timeParse("%x")(d["Report Date"]),
fips: d.FIPS,
locality: d.Locality,
district: d["VDH Health District"],
cases: +d["Total Cases"],
hospitalizations: +d.Hospitalizations,
deaths: +d.Deaths
};
});
Insert cell
Insert cell
covid.sort(function (a, b) {
return a.date - b.date;
});
Insert cell
Insert cell
printTable(covid.slice(0,5))
Insert cell
printTable(covid.slice(-5))
Insert cell
Insert cell
hr_districts = new Set(["Western Tidewater", "Chesapeake", "Virginia Beach", "Norfolk", "Portsmouth", "Hampton", "Peninsula"])
Insert cell
hr_covid = covid.filter(d => hr_districts.has(d.district))
Insert cell
Insert cell
Insert cell
last_date = covid[covid.length-1].date
Insert cell
covid_totals = covid.filter(d => d.date >= last_date)
Insert cell
hr_totals = covid_totals.filter(d => hr_districts.has(d.district))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
margin = ({top: 10, right: 10, bottom: 50, left: 100});
Insert cell
Insert cell
visHeight = 510 - margin.top - margin.bottom;
Insert cell
visWidth = width - margin.left - margin.right;
Insert cell
Insert cell
xscale = d3.scaleTime()
.domain([d3.min(covid_cases_objects, d => d.date),
d3.max(covid_totals, d => d.date)])
.range([margin.left, visWidth - margin.right]);
Insert cell
Insert cell
yscale = d3.scaleLinear()
.domain([0, d3.max(covid_cases_objects, d => d.cases)])
.range([visHeight - margin.bottom, margin.top]);
Insert cell
Insert cell
yAxis = d3.axisLeft(yscale);
Insert cell
Insert cell
xAxis = d3.axisBottom(xscale);
Insert cell
Insert cell
single_district = new Set([selected_district])
Insert cell
single_district_covid = covid.filter(d => single_district.has(d.district))
Insert cell
Insert cell
covid_cases_rollup = d3.rollups(single_district_covid, v => d3.sum(v, d => d.cases), d => d.date);
Insert cell
covid_cases_objects = covid_cases_rollup.map(obj => { let rObj= {}; rObj["date"] = obj[0]; rObj["cases"] = obj[1]; return rObj;})
Insert cell
Insert cell
Insert cell
{
// ----------------------------------------------
// Create the SVG *******************************
const svg = d3.create('svg')
.attr('width', visWidth + margin.left + margin.right)
.attr('height', visHeight + margin.top + margin.bottom);

const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// ----------------------------------------------
// ADD THE AXES *********************************
const xAxisGroup = g.append("g")
.attr('transform', `translate(0, ${visHeight})`)
.call(xAxis)
.call(g => g.selectAll('.domain').remove());
xAxisGroup.append('text') // Add the x axis label
.attr('x', visWidth / 2)
.attr('y', 45)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text('date'); // The axis label is the chosen attribute
const yAxisGroup = g.append('g')
.call(yAxis)
.call(g => g.selectAll('.domain').remove());
yAxisGroup.append('text') // Add the y axis label
.attr('x', -60)
.attr('y', visHeight / 2)
.attr('fill', 'black')
.text('cases'); // The axis label is the chosen attribute
// ----------------------------------------------
// DRAW THE GRID ********************************
const grid = g.append('g');
grid.append('rect')
.attr('width', visWidth)
.attr('height', visHeight)
.attr('fill', 'white');

let yLines = grid.append('g')
.selectAll('line');

let xLines = grid.append('g')
.selectAll('line');

function drawGridLines(x, y) {
yLines = yLines.data(y.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', visWidth)
.attr('y1', d => 0.5 + y(d))
.attr('y2', d => 0.5 + y(d));

xLines = xLines.data(x.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', d => 0.5 + x(d))
.attr('x2', d => 0.5 + x(d))
.attr('y1', d => 0)
.attr('y2', d => visHeight);
}

drawGridLines(xscale, yscale);
// ----------------------------------------------
// DRAW THE LINE ******************************
svg.append('clipPath')
.attr('id', 'border')
.append('rect')
.attr('width', visWidth)
.attr('height', visHeight)
.attr('fill', 'white');
const color = d3.scaleOrdinal()
.domain(covid_cases_objects)
.range(d3.schemeTableau10);
const line = d3.line()
.x(d => xscale(d.date))
.y(d => yscale(d.cases))
const lines = g.append('path')
.attr('stroke', 'steelblue')
.datum(covid_cases_objects)
.attr('clip-path', 'url(#border)')
.attr('fill', 'none')
.attr('stroke-width', 3)
.attr('d', line);
const zoom = d3.zoom()
.extent([[0, 0], [visWidth, visHeight]])
// Determine how much you can zoom out and in
// 1 is the factor by which you can zoom out, smaller number means zoom
// out more, 1 means you can't zoom out more than the default zoom.
// 10 is the factor by which you can zoom in, larger number means you can
// zoom in more.
.scaleExtent([1, 10])
.on('zoom', onZoom);
g.call(zoom);
// ----------------------------------------------
// TOOLTIP SECTION ******************************
const tooltip = g.append("g");
const bisect = d3.bisector(d => d.date).left;
function setTooltips(x, y) {
g.on("touchmove mousemove", function(event) {
const invertedDate = x.invert(d3.pointer(event, this)[0]);
const index = bisect(covid_cases_objects, invertedDate, 1);
const a = covid_cases_objects[index - 1];
const b = covid_cases_objects[index];
const values = (invertedDate - a.date > b.date - invertedDate) ? b : a;
lines.attr('stroke-width', 5);

tooltip
.attr("transform", `translate(${x(values.date)},${y(values.cases)})`)
.call(callout, `Cases: ${values.cases}\nDate: ${values.date}`);
});

g.on("touchend mouseleave", function(event) {

tooltip.call(callout, null)
lines.attr('stroke-width', 2);

});
}
setTooltips(xscale, yscale);
// END TOOLTIP SECTION **************************
// ----------------------------------------------
function onZoom(event) {
// get updated scales
const xNew = event.transform.rescaleX(xscale);
const yNew = event.transform.rescaleY(yscale);
const line = d3.line()
.x(d => xNew(d.date))
.y(d => yNew(d.cases))
// update the position of the lines
lines.attr('d', line);
// update the axes
xAxisGroup.call(xAxis.scale(xNew))
.call(g => g.selectAll('.domain').remove());

yAxisGroup.call(yAxis.scale(yNew))
.call(g => g.selectAll('.domain').remove());
// update the grid
drawGridLines(xNew, yNew);
// update the tooltips
setTooltips(xNew, yNew);
}
return svg.node();
}
Insert cell
Insert cell
callout = (g, value) => {
if (!value) return g.style("display", "none");

g
.style("display", null)
.style("pointer-events", "none")
.style("font", "10px sans-serif");

const path = g.selectAll("path")
.data([null])
.join("path")
.attr("fill", "white")
.attr("stroke", "black");

const text = g.selectAll("text")
.data([null])
.join("text")
.call(text => text
.selectAll("tspan")
.data((value + "").split(/\n/))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i) => `${i * 1.1}em`)
.style("font-weight", (_, i) => i ? null : "bold")
.text(d => d));

const {x, y, width: w, height: h} = text.node().getBBox();

text.attr("transform", `translate(${-w / 2},${15 - y})`);
path.attr("d", `M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`);
}
Insert cell
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
import { vl } from "@vega/vega-lite-api"
Insert cell
import {printTable} from '@uwdata/data-utilities'
Insert cell
import {Select} from "@observablehq/inputs"
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