Published
Edited
Mar 17, 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
covid.sort((a, b) => a.date - b.date)
Insert cell
Insert cell
printTable(covid.slice(0,7))
Insert cell
printTable(covid.slice(-7))
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
printTable(hr_covid.slice(0,10))
Insert cell
printTable(hr_covid.slice(-10))
Insert cell
last_date = covid[covid.length-1].date
Insert cell
covid_totals = covid.filter(d => d.date >= last_date)
Insert cell
printTable(covid_totals.slice(0,10))
Insert cell
hr_totals = covid_totals.filter(d => hr_districts.has(d.district))
Insert cell
printTable(hr_totals)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
dashboard = {
// Reference - https://observablehq.com/@weiglemc/brushing-and-linking-example-with-vega-lite?collection=@weiglemc/cs-725-825-spring-2021
const selection = vl.selectSingle('Select')
.fields('district')
.bind({district: vl.menu(hr_district_list)});

// variables to get the dashboard to fit nicely
const num_first_row_charts = 2;
const first_row_hist_chart_width = width * 0.47;
const first_row_bar_chart_width = width * 0.3;
const second_row_chart_width = width - 98;
const second_row_chart_height = second_row_chart_width * 0.5;
const first_row_charts_height = second_row_chart_width * 0.125;

// brushing and linking example from https://observablehq.com/@uwdata/interaction
const brush = vl.selectInterval().encodings('x').resolve('intersect');
// First row base histogram
const top_histogram = vl.markBar()
.data(hr_covid)
.encode(
vl.x()
.fieldT("date")
.title(null),
vl.y()
.fieldQ("hospitalizations")
);

//combined histogram with total in grey and selected in blue (when selected)
const top_histogram_layer = vl.layer(
top_histogram.select(brush).encode(vl.color().value('steelblue')) // keep blue even when selected
// top_histogram.select(brush).encode(vl.color().value('lightgrey')), // turn all grey when selected
// top_histogram.transform(vl.filter(brush))) // layer selected values on top in default blue
)
.height(first_row_charts_height)
.width(first_row_hist_chart_width);
// First row horizontal bar chart
const horizontal_bar_chart = vl.markBar()
.data(hr_totals)
.encode(
vl.x()
.sum('hospitalizations')
.title(null),
vl.y()
.fieldN('district')
.title(null)
.sort(vl.sum('hospitalizations').order('descending'))
)
.title("Total Hospitalizations")
.height(first_row_charts_height)
.width(first_row_bar_chart_width);

// Second row base histogram with tooltip
const bottom_histogram = vl.markBar()
.data(hr_covid)
.encode(
vl.x()
.fieldT('date')
.title(null)
.scale({domain: brush}), // The x domain varies with the selection of the top_histogram
vl.y()
.fieldQ("hospitalizations"),
vl.tooltip(['hospitalizations', 'date']))
.height(second_row_chart_height)
.width(second_row_chart_width);
// Second row histogram layer
const bottom_histogram_layer = vl.layer(
bottom_histogram.select(selection).encode(vl.color().value('lightgrey')),
bottom_histogram.transform(vl.filter(selection)));
// place the histogram and horizontal bar chart in first row and place it with the second row chart
return vl.vconcat(vl.hconcat(top_histogram_layer, horizontal_bar_chart), bottom_histogram_layer)
.title("Hampton Roads COVID Dashboard for Hospitalizations") // Title of the Dashboard
.render();
}
Insert cell
Insert cell
Insert cell
Insert cell
myWidth = width;
Insert cell
height = 600
Insert cell
margin = ({top: 60, right: 20, bottom: 50, left: 50})
Insert cell
x = d3.scaleLinear()
.range([margin.left-50, myWidth - margin.right])

Insert cell
y = d3.scaleLinear()
.range([height+50 - margin.bottom, margin.top])
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(6))
// .call(d3.axisBottom(x).ticks(myWidth / 160))
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y).ticks(8))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
scatter1 = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height+180])
.attr("width", width-50); ;
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// set the x and y domains based on our data
x.domain(d3.extent(covid_totals, d => d[xaxis_value])).nice()
y.domain(d3.extent(covid_totals, d => d[yaxis_value])).nice()
// Reference - pan and zoom - https://observablehq.com/@nyuvis/interaction?collection=@nyuvis/guides-and-examples
// create and add axes
// setup the x-axis and label
const xAxis = d3.axisBottom(x);
const xAxisGroup = g.append("g")
.attr('transform', `translate(0, ${height})`)
.call(xAxis)
.call(g => g.selectAll('.domain'));
xAxisGroup.append('text')
.attr("x", (myWidth/2))
.attr("y", margin.bottom)
.attr("fill", "currentColor")
.attr("text-anchor", "middle")
.attr("font-size", "small")
.text(xaxis_value);
// setup the y-axis and label
const yAxis = d3.axisLeft(y);
const yAxisGroup = g.append('g')
.call(yAxis)
.call(g => g.selectAll('.domain'));
yAxisGroup.append('text')
.attr("x", -margin.left)
.attr("y", margin.top-25)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("font-size", "small")
.text(yaxis_value);

// https://observablehq.com/@weiglemc/interaction-in-d3?collection=@weiglemc/cs-725-825-spring-2021
// draw grid, based on https://observablehq.com/@nyuvis/interaction?collection=@nyuvis/guides-and-examples
const grid = g.append('g');
grid.append('rect')
.attr('width', width)
.attr("x", (margin.left-50))
.attr('height', height-50)
.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', width)
.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 => margin.top)
.attr('y2', d => height+10);
}

drawGridLines(x, y);

// https://observablehq.com/@weiglemc/interaction-in-d3?collection=@weiglemc/cs-725-825-spring-2021
// clip path hides dots that go outside when zooming
svg.append('clipPath')
.attr('id', 'border')
.append('rect')
.attr('width', width-80)
.attr('y', margin.top+10)
.attr('height', height-71)
.attr('fill', 'black');

const circlesGroup = g.append('g')
.attr('clip-path', 'url(#border)');
const radius = 5;
// draw the circles
const circles = circlesGroup.selectAll('circle')
.data(covid_totals)
.join("circle")
.attr("cx", d => x(d[xaxis_value]))
.attr("cy", d => y(d[yaxis_value]))
.attr("stroke", "black")
.attr('fill', 'steelblue') // Set the fill
.attr("r", radius)
// ********** new stuff starts here **********
.on('mouseenter', mouseEnter)
.on('mouseleave', mouseLeave);
// pan and zoom -https://observablehq.com/@nyuvis/interaction?collection=@nyuvis/guides-and-examples
const zoom = d3.zoom()
.extent([[0, 0], [width-80, height-110]])
// determine how much you can zoom out and in
.scaleExtent([1, Infinity])
.on('zoom', onZoom);
g.call(zoom);
function onZoom(event) {
// get updated scales
const xNew = event.transform.rescaleX(x);
const yNew = event.transform.rescaleY(y);
// update the position of the dots
circles.attr('cx', d => xNew(d[xaxis_value]))
.attr('cy', d => yNew(d[yaxis_value]));
// update the axes
xAxisGroup.call(xAxis.scale(xNew))
.call(g => g.selectAll('.domain'));

yAxisGroup.call(yAxis.scale(yNew))
.call(g => g.selectAll('.domain'));
// update the grid with new scales
drawGridLines(xNew, yNew);

}
// Hovering - https://observablehq.com/@nyuvis/interaction?collection=@nyuvis/guides-and-examples
// create tooltip
const tooltip = g.append('g')
.attr('visibility', 'hidden');
const tooltipHeight = 25;
const tooltipRect = tooltip.append('rect')
.attr('fill', 'grey')
.attr('rx', 5)
.attr('height', tooltipHeight);
const tooltipText = tooltip.append('text')
.attr('fill', 'white')
.attr('font-family', 'sans-serif')
.attr('font-size', 12)
.attr('dy', 2)
.attr('dx', 2)
.attr('dominant-baseline', 'hanging')
// handle hovering over a circle
function mouseEnter(event, d) {
// make the circle larger
d3.select(this)
.attr('r', radius * 2);
// update the text and get its width
tooltipText.text(d.locality + " - " + xaxis_value+ ": " + d[xaxis_value] + " , "+ yaxis_value+ ": " + d[yaxis_value]);
const labelWidth = tooltipText.node().getComputedTextLength();
// set the width of the tooltip's background rectangle
// to match the width of the label
tooltipRect.attr('width', labelWidth + 4);
// move the tooltip and make it visible
const xPos = x(d[xaxis_value]) + radius * 3;
const yPos = y(d[yaxis_value]) - tooltipHeight / 2;

tooltip.attr('transform', `translate(${xPos},${yPos})`)
.attr('visibility', 'visible');
}
// handle leaving a circle
function mouseLeave(event, d) {
// reset its size
d3.select(this)
.attr('r', radius)
// make the tooltip invisible
tooltip
.attr('visibility', 'hidden');
}
return svg.node();
}
Insert cell
Insert cell
Insert cell
scatterplot2 = d3.rollups(covid, v => [d3.sum(v, d => d.cases),d3.sum(v, d => d.hospitalizations)], d => d.date)
Insert cell
Insert cell
casesHospPerDayArr = d3.rollups(covid, v => [d3.sum(v, d => d.cases), d3.sum(v, d => d.hospitalizations)],
d => d.date)
Insert cell
Insert cell
casesHospPerDay = casesHospPerDayArr.map(obj => {let rObj= {}; rObj["date"] = obj[0]; rObj["cases"] = obj[1][0]; rObj["hospitalizations"] = obj[1][1]; return rObj;})
Insert cell
Insert cell
sorted_casesHospPerDay = casesHospPerDay.sort((a, b) => a.date - b.date)
Insert cell
Insert cell
x_line3 = d3.scaleTime() // d3.scaleUtc()
.range([margin.left + 10, width - margin.right])
Insert cell
y_line3 = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
Insert cell
xAxis_line3 = g => g
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x_line3))
Insert cell
yAxis_line3 = g => g
.attr("transform", `translate(${margin.left + 10}, 0)`)
.call(d3.axisLeft(y_line3))
Insert cell
line = d3.line()
.x(d => x_line3(d.date))
.y(d => y_line3(d.cases))
Insert cell
sorted_hr_covid = hr_covid.sort((a, b) => (a.date) - (b.date))
Insert cell
Insert cell
hrCasesByDate = {
const data = d3.rollup(sorted_hr_covid,
v => d3.sum(v, c => c.cases),
d => d.district,
d => d.date);
return Array.from(data, ([district, values]) => ({
district: district,
values: Array.from(values, ([date, cases]) => ({date: date, cases}))
}));
}
Insert cell
Insert cell
hr_health_district = new Set([health_district])
Insert cell
Insert cell
health_district_covid = hrCasesByDate.filter(d => hr_health_district.has(d.district))
Insert cell
Insert cell
selected_district_covid = health_district_covid[0].values
Insert cell
Insert cell
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height+150])
.attr("width", width-100);
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// set the x and y domains based on our data
x_line3.domain(d3.extent(selected_district_covid, d => d.date)).nice()
y_line3.domain(d3.extent(selected_district_covid, d => d.cases)).nice()
// Reference - https://observablehq.com/@nyuvis/interaction?collection=@nyuvis/guides-and-examples
// create and add axes
// setup the x-axis and label
const xAxis = d3.axisBottom(x);
const xAxisGroup = g.append("g")
.attr('transform', `translate(0, ${height})`)
.call(xAxis_line3)
.call(g => g.selectAll('.domain'));
xAxisGroup.append('text')
.attr("x", (myWidth/2))
.attr("y", margin.bottom)
.attr("fill", "currentColor")
.attr("text-anchor", "middle")
.attr("font-size", "small")
.text("Date");
const yAxis = d3.axisLeft(y);
const yAxisGroup = g.append('g')
.call(yAxis_line3)
.call(g => g.selectAll('.domain'));
yAxisGroup.append('text')
.attr("x", -margin.left)
.attr("y", margin.top-25)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("font-size", "small")
.text("Sum of Cases of " + health_district);
// draw grid, based on https://observablehq.com/@nyuvis/interaction?collection=@nyuvis/guides-and-examples
const grid = g.append('g');
grid.append('rect')
.attr('width', width-150)
.attr("x", -(margin.left -115))
.attr('height', height-117)
.attr("y", (margin.top+5))
.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', margin.left + 10)
.attr('x2', width-100)
.attr('y1', d => -50 + y(d))
.attr('y2', d => -50 + y(d));
xLines = xLines.data(x.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', d => 60 + x(d))
.attr('x2', d => 60 + x(d))
.attr('y1', d => margin.top+5)
.attr('y2', d => height - margin.bottom);
}

drawGridLines(x, y);

// clip path hides dots that go outside of the vis when zooming
svg.append('clipPath')
.attr('id', 'border')
.append('rect')
.attr('width', width-150)
.attr("x", -(margin.left -170))
.attr("y", (margin.top+70))
.attr('height', height-120)
.attr('fill', 'white');
// draw the line
const lines = svg.append('path')
.datum(selected_district_covid)
.style('fill', 'none')
.style('stroke', 'red')
.style('stroke-width', '2')
.attr('d', line)
.attr('clip-path', 'url(#border)'); //clip path
// Zoom - based on https://observablehq.com/@nyuvis/interaction?collection=@nyuvis/guides-and-examples
const zoom = d3.zoom()
.extent([[0, 0], [width, height]])
.scaleExtent([1, Infinity])
.on('zoom', onZoom);
g.call(zoom);
// tooltips
// source - https://observablehq.com/@d3/line-chart-with-tooltip
const tooltip = g.append("g");
g.on("touchmove mousemove", function(event) {
const {date, value} = bisect(d3.pointer(event, this)[0]);
tooltip
.attr("transform", `translate(${x_line3(value.date)},${y_line3(value.cases)})`)
.call(callout, `Date: ${value.date} - Cases: ${value.cases}`);
});
g.on("touchend mouseleave", () => tooltip.call(callout, null));
// zoom event funtion
function onZoom(event) {
// get updated scales
const xNew = event.transform.rescaleX(x_line3);
const yNew = event.transform.rescaleY(y_line3);
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'));

yAxisGroup.call(yAxis.scale(yNew))
.call(g => g.selectAll('.domain'));
// update the grid
drawGridLines(xNew,yNew);

// update the tooltips
g.on("touchmove mousemove", function(event) {
const {date, value} = bisect(d3.pointer(event, this)[0]);
tooltip
.attr("transform", `translate(${xNew(value.date)},${yNew(value.cases)})`)
.call(callout, `Date: ${value.date} - Cases: ${value.cases}`);
});

g.on("touchend mouseleave", () => tooltip.call(callout, null));
}
return svg.node();
}
Insert cell
bisect = {
const bisect = d3.bisector(d => d.date).left;
return mx => {

const date = x.invert(mx);
const index = bisect(selected_district_covid, date, 1);
const a = selected_district_covid[index - 1];
const b = selected_district_covid[index];
return b && (date - a.date > b.date - date) ? b : a;
};
}
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