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

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