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

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