Published
Edited
Jan 5, 2022
1 fork
1 star
Also listed in…
hex maps
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function make_map() {
let max_size = 1200;
let break_size = 640; // Use alternative tooltip when tless than this size.
let map_width = width < max_size ? width : max_size;
let height = 0.575 * map_width;

let div = d3
.create("div")
.style("width", `${map_width}px`)
.style("height", `${height}px`)
.style("overflow", "hidden")
.style("position", "relative");

let svg = div
.append("svg")
.style("overflow", "hidden")
.attr("viewBox", [0, 0, map_width, height]);

let proj = d3
.geoIdentity()
.reflectY(true)
.fitSize([map_width, height], stateTiles);
let path = d3.geoPath().projection(proj);

let map = svg.append("g");
let density_map = new Map();
map
.selectAll("path.tile")
.data(stateTiles.features)
.join("path")
.attr("class", "tile")
.attr("data-fips", (o) => o.properties.fips)
.attr("data-name", (o) => o.properties.name)
.attr("d", path)
.attr("fill", function (d) {
let this_covid_data = covid_data.filter(
(o) => o.fips == d.properties.fips
)[0];
let rolling_mean = d3.mean(
this_covid_data.actualsTimeseries.slice(-14).map((o) => o.newCases)
);
let population = this_covid_data.population;
let density = rolling_mean / population;
density_map.set(d.properties.fips, density);
return d3.interpolateReds((density / max_case_density) ** 1);
})
.attr("data-density", (o) => density_map.get(o.properties.fips))

.attr("stroke-width", "0.8px")
.attr("stroke", function (d) {
return "#000";
})
.attr("stroke-linejoin", "round");

map
.selectAll("text")
.data(stateTiles.features)
.join("text")
.text((o) => o.properties.abbr)
.attr("font-size", `${0.022 * map_width}px`)
.attr("x", function (o) {
return path.centroid(o)[0];
})
.attr("text-anchor", "middle")
.attr("y", function (o) {
return path.centroid(o)[1];
})
.attr("dy", 0.008 * map_width)
.attr("pointer-events", "none");

if (map_width > break_size) {
// Use Tippy, if big enough
map
.selectAll("path.tile")
.nodes()
.forEach(function (t) {
let graph = make_graph(t.getAttribute("data-fips"), {
parameter: "newCases",
roll_by: 14
});
let content = html`<div><span style="font-weight:bold">${t.getAttribute(
"data-name"
)}</span>: Approximately ${Math.round(
100000 * t.getAttribute("data-density")
)} new cases per 100,000</div>${graph}`;
tippy(t, {
content: content,
theme: "light",
maxWidth: 500
});
});
} else {
// Overlay a transparent DIV, if smaller
map
.selectAll("path.tile")
.on("pointerenter", function (evt, d) {
let fips = d.properties.fips;
let graph_width = 0.96 * map_width;
let graph_height = 0.96 * height;
let graph_div = div
.append("div")
.attr("id", `graph${fips}`)
.style("width", `${graph_width}px`)
.style("height", `${graph_height}px`)
.style("position", "absolute")
.style("top", `${0.02 * graph_height}px`)
.style("left", `${0.02 * graph_width}px`)
.style("pointer-events", "none");
div
.append("div")
.attr("class", "close")
.style("width", "20px")
.style("height", "20px")
.style("background-color", "#eee")
.style("border", "solid 1px black")
.style("position", "absolute")
.style("left", "0px")
.style("top", "0px")
.style("text-align", "center")
.text("X")
.on("pointerdown", function (evt) {
evt.stopPropagation();
div.selectAll(".close").remove();
graph_div.remove();
});

graph_div.append(() =>
make_graph(fips, {
graph_width: graph_width,
graph_height: graph_height,
roll_by: 14
})
);
})
.on("mouseout", function (evt, d) {
let fips = d.properties.fips;
div.select(`#graph${fips}`).remove();
});
}

return div.node();
}
Insert cell
function make_graph(fips, opts = {}) {
let {
show_axes = true,
graph_width = 450, // 0.7 * width,
graph_height = 250, // 0.7 * width,
margin = { left: 50, right: 15, bottom: 25 },
strokeOpacity = 1,
roll_by = 1,
background_opacity = 0.7
} = opts;

let this_covid_data = covid_data.filter(o => o.fips == fips)[0];
let population = this_covid_data.population;
this_covid_data = this_covid_data.actualsTimeseries.filter(o => o.newCases);
let values = this_covid_data.map(o => o.newCases);
let rolled = rolling_sum(values, roll_by);
let y_max = (100000 * d3.max(rolled)) / (roll_by * population);
this_covid_data.map((o, i) => (o.rolled = rolled[i]));
this_covid_data = this_covid_data.slice(roll_by - 1);

let x_scale = d3
.scaleTime()
.domain([this_covid_data[0].date, this_covid_data.slice(-1)[0].date])
.range([margin.left, graph_width - margin.right]);
let y_scale = d3
.scaleLinear()
.domain([0, y_max])
.range([graph_height - margin.bottom, 0]);
let pts_to_path = d3
.line()
.x(d => x_scale(d.date))
.y(d => y_scale((100000 * d.rolled) / (roll_by * population)));
let svg = d3
.create("svg")
.attr("width", graph_width)
.attr("height", graph_height);

svg
.append("rect")
.attr("width", graph_width)
.attr("height", graph_height)
.attr("fill", "white")
.attr("fill-opacity", background_opacity);
svg
.append("path")
.attr("d", pts_to_path(this_covid_data))
.style("stroke", "black")
.style("stroke-opacity", strokeOpacity)
.style("stroke-width", 2)
.style("stroke-linejoin", "round")
.style("fill", "none");

if (show_axes) {
svg
.append("g")
.attr("transform", `translate(0,${graph_height - margin.bottom})`)
.call(
d3
.axisBottom(x_scale)
.ticks(6)
.tickFormat(d3.timeFormat("%b %y"))
);
svg
.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y_scale));
}

return svg.node();
}
Insert cell
// Modfied from https://observablehq.com/@d3/moving-average
function rolling_sum(values, N) {
let i = 0;
let sum = 0;
let sums = new Float64Array(values.length).fill(NaN);
for (let n = Math.min(N - 1, values.length); i < n; ++i) {
sum += values[i];
}
for (let n = values.length; i < n; ++i) {
sum += values[i];
sums[i] = sum;
sum -= values[i - N + 1];
}
return sums;
}
Insert cell
max_vaccination_density = d3.max(
covid_data.map(o => o.actuals.vaccinationsInitiated / o.population)
)
Insert cell
max_case_density = d3.max(
covid_data.map(
o =>
d3.mean(o.actualsTimeseries.slice(-14).map(o => o.newCases)) /
o.population
)
)
Insert cell
covid_data = {
let parseDate = d3.utcParse("%Y-%m-%d");
return (await (await fetch(
'https://api.covidactnow.org/v2/states.timeseries.json?apiKey=b496b11f1f5347aa88bc5573e536eef8'
)).json()).map(function(o) {
o.actualsTimeseries.forEach(d => (d.date = parseDate(d.date)));
return o;
});
}
Insert cell
stateTiles = {
let stateTiles = await FileAttachment("stateTiles2.json").json();
let tiles = topojson.feature(stateTiles, stateTiles.objects.tiles);
return tiles;
}
Insert cell
style = html`<link style="display: none" rel="stylesheet" href="${await require.resolve(
`tippy.js/themes/light.css`
)}">`
Insert cell
import { Radio } from '@observablehq/inputs'
Insert cell
tippy = require("tippy.js@6")
Insert cell
topojson = require('topojson-client@3')
Insert cell
import { legend } from "@d3/color-legend"
Insert cell
d3 = require('d3@6')
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