Published unlisted
Edited
Sep 9, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
above_tabs = {
let blocks = text_blocks.filter(o => o.Section == "Above tabs");
let md_string = blocks[0].Text;
return md`${md_string}`;
}
Insert cell
Insert cell
// Reads text blocks from the SC-COSMO GitHub repository and prints
// them out nicely formatted here.

about = {
let md_string = "";
let blocks = text_blocks.filter((o) => o.Page == "About");
blocks.forEach(function (b, i) {
if (b.Section.length > 4 && b.Section.slice(0, 5) == "Below") {
if (b.Style == "Text") {
md_string = md_string + b.Text + "\n\n";
} else if (b.Style == "Header") {
md_string = md_string + "## " + b.Text + "\n\n";
}
}
});

return html`${md`${md_string}`}
<hr style="border-top: 0.3px" />
<div style="font-size: 70%">
Website visualizations built on <a href="https://observablehq.com" target="_blank">Observable</a> by <a href="https://wncviz.com" target="_blank">WNC Viz</a>.
</div>
`;
}
Insert cell
Insert cell
Insert cell
// Makes the four different hex maps for the four different racial groups.
// Shades the map and sets the tooltip depending on whether we want projected
// coverage date or the current coverage.

function make_hex_map(racial_group, opts = {}) {
let max_size = 768;
let map_width = width < max_size ? width : 900;
let height = 0.575 * map_width;
let div = d3.create("div").style("width", "90%").style("overflow", "hidden");

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 filtered_attainment_projections = attainment_projections.filter(
(o) => o.race_grp == racial_group
);
let map = svg.append("g");
let tiles = map.append("g");
tiles
.selectAll("path.tile")
.data(stateTiles.features)
.join("path")
.attr("class", "tile")
.attr("d", path)
.attr("fill", function (d) {
if (shade_by == "Projected date to reach 80% coverage") {
let this_data = filtered_attainment_projections.filter(
(o) => o.state == d.properties.abbr
);
let this_attainment_projection = this_data[0].date_80;
let idx = date_labels_for_hexmaps.indexOf(this_attainment_projection);
if (idx == -1) {
return "gray";
} else {
return color_scheme[idx];
}
} else {
return coverage_shade(d.properties.abbr, racial_group);
}
})
.attr("stroke-width", "0.8px")
.attr("stroke", "#fff")
.attr("stroke-linejoin", "round")
.on("pointerenter", function (evt, d) {
let tile = d3
.select(this)
.attr("stroke-width", "5px")
.attr("stroke", "#000")
.raise();
let tip_content = `<h3 style="text-decoration:underline; padding-bottom:5px">${d.properties.name}</h3>`;
if (shade_by == "Projected date to reach 80% coverage") {
tip_content =
tip_content +
`<div>Projected 80% coverage date:</div>
<table>`;
attainment_projections
.filter((o) => o.state_name == d.properties.name)
.sort((g, h) => g.race_grp > h.race_grp)
.forEach(function (o) {
tip_content =
tip_content +
`<tr><th style="text-align:left; padding-right: 3px">${
o.race_grp
}</th><td style="text-align:left">${
o.date_80 == "NR" ? "Data not reported" : o.date_80
}</td></tr>`;
});
tip_content = tip_content + "</table>";
} else {
let info = get_state_values(d.properties.abbr, Date.now());
tip_content =
tip_content +
`<div>Current coverage:</div>
<table>`;

race_grps.forEach(function (grp) {
let value = info.filter((o) => o.race_grp == grp);
if (value.length > 0) {
value = Math.round(value[0].value);
if (value > 90) {
value = "90%+";
} else {
value = `${value}%`;
}
} else {
value = "Data not reported";
}
tip_content =
tip_content +
`<tr>
<th style="text-align:left; padding-right: 3px">${grp}</th>
<td style="text-align:left">${value}</td>
</tr>
`;
});
tip_content = tip_content + "</table>";
}
tippy(tile.node(), {
allowHTML: true,
maxWidth: 420,
theme: "light",
content: tip_content
});
})
.on("mouseleave", function () {
d3.select(this).attr("stroke-width", "0.8px").attr("stroke", "#fff");
});
map
.selectAll("text")
.data(stateTiles.features)
.join("text")
.text((o) => o.properties.abbr)
.style("fill", "black")
.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");

let download_button = DOM.download(
() =>
serialize(html`
<h1>${shade_by} of 1+ dose among ages 12+: ${racial_group}</h1>
<div style = "width:800px">
<div>
${svg.node().outerHTML}
</div>
<div style="width: 320px; margin:30px auto;">
${color_legend_for_maps.cloneNode(true).outerHTML}
</div>
<div style="font-size:80%">
Downloaded on ${d3.timeFormat("%a, %b %e, %Y")(
Date.now()
)} from vax-equity-tracker.org.
</div>
</div>
`),
"HexMap",
"Download SVG"
);

d3.select(download_button)
.style("margin-left", "10px")
.style("margin-top", "5px")
.select("button")
.style("font-size", "10px");
div.append(() => download_button);
return div.node();
}
Insert cell
// The top level make_timeseries. Just calls one of the sub-functions
// depending on the metric

function make_timeseries_graph() {
if (timeseries_view == "All states by race/ethnicity") {
return make_coverage_graphs_by_state(racial_group, state_selector.abbr);
} else if (timeseries_view == "All racial/ethnic groups by state") {
return make_racial_coverage_comparison_graph(state_selector.abbr);
}
}
Insert cell
// Generates the default view of all 50 states with one highlighted.

function make_coverage_graphs_by_state(racial_group, state) {
let max_size = 700;
let graph_width = width < max_size ? width : max_size;
let graph_height = 0.7 * graph_width;
let margin = { left: 30, right: 30, bottom: 25, top: 20 };
let strokeOpacity = 0.2;

let svg = d3
.create("svg")
.attr("width", graph_width)
.attr("height", graph_height)
.attr("viewBox", [0, 0, graph_width, graph_height]);

let x_scale = d3
.scaleTime()
.domain([coverage_data.start_date, coverage_data.end_date])
.range([margin.left, graph_width - margin.right]);
x_scale.ticks(2);
let y_scale = d3
.scaleLinear()
.domain([0, 90])
.range([graph_height - margin.bottom, margin.top]);
let pts_to_path = d3
.line()
.defined((d) => !isNaN(d.vaccinated_pct_12_out) && !d.project)
.x((d) => x_scale(d.Date))
.y((d) => y_scale(d.vaccinated_pct_12_out));
let pts_to_projected_path = d3
.line()
.defined((d) => !isNaN(d.vaccinated_pct_12_out) && d.project)
.x((d) => x_scale(d.Date))
.y((d) => y_scale(d.vaccinated_pct_12_out));
let dateFormat = d3.timeFormat("%b %e");

let coverage_graph = svg.append("g");
coverage_graph
.append("line")
.attr("x1", x_scale(coverage_data.start_date) + 5)
.attr("x2", x_scale(coverage_data.end_date))
.attr("y1", y_scale(80))
.attr("y2", y_scale(80))
.style("stroke", "black");

try {
let attainment_date = get_attainment_date(state, racial_group, 80);
let attainment_position = x_scale(attainment_date);
coverage_graph
.append("circle")
.attr("cx", attainment_position)
.attr("cy", y_scale(80))
.attr("r", 0.005 * graph_width)
.attr("fill", "black");
coverage_graph
.append("text")
.attr("class", "default")
.attr("x", attainment_position)
.attr("y", y_scale(80))
.attr("dx", 5)
.attr("dy", 15)
.attr("fill", "black")
.text(dateFormat(attainment_date));
coverage_graph
.append("text")
.attr("class", "state_name_selected")
.attr("x", 0.05 * graph_width)
.attr("y", 0.04 * graph_height)
.style("font-size", `${0.03 * graph_width}px`)
.style("fill", "blue")
.text(state_selector.name);
} catch {
coverage_graph
.append("text")
.attr("class", "state_name_selected")
.attr("x", 0.05 * graph_width)
.attr("y", 0.04 * graph_height)
.style("font-size", `${0.03 * graph_width}px`)
.style("fill", "blue")
.text(`${state_selector.name}`);
}

coverage_data.state_groups.forEach(function (v, k) {
let this_data = v.get(racial_group);
// Exclude missing states
if (this_data) {
coverage_graph
.append("path")
.attr("d", pts_to_path(this_data))
.attr("class", k == state ? "" : `state ${k.replace(/ /g, "")}`)
.style("stroke", k == state ? "blue" : "black")
.style("stroke-opacity", k == state ? 1 : 0.2)
.style("stroke-width", k == state ? "4px" : "0.75px")
.style("stroke-linejoin", "round")
.style("fill", "none");
coverage_graph
.append("path")
.attr("d", pts_to_projected_path(this_data))
.attr("class", k == state ? "" : `state ${k.replace(/ /g, "")}`)
.style("stroke", k == state ? "blue" : "black")
.style("stroke-opacity", k == state ? 1 : 0.2)
.style("stroke-width", k == state ? "4px" : "0.75px")
.style("stroke-linejoin", "round")
.style("fill", "none")
.attr("stroke-dasharray", "5 8");
}
});

let closest;
svg
.on("pointerenter", function () {
coverage_graph.select("text.default").attr("opacity", 0);
})
.on("mousemove", function (evt) {
let x = d3.pointer(evt)[0];
let y = d3.pointer(evt)[1];
let day = x_scale.invert(x);
let p = y_scale.invert(y);

closest = get_closest_state_value(day, racial_group, p);
coverage_graph
.selectAll("path.state")
.style("stroke-opacity", 0.2)
.style("stroke-width", "0.75px");
coverage_graph.selectAll("text.state_name").remove();
coverage_graph.selectAll("text.date").remove();
coverage_graph.selectAll("circle.temp").remove();
if (closest.err < 15) {
coverage_graph
.selectAll(`path.${closest.state}`)
.style("stroke-opacity", 1)
.style("stroke-width", "1.5px");
coverage_graph
.append("text")
.attr("class", "state_name")
.attr("x", margin.left + 0.02 * graph_width)
.attr("y", margin.top + 0.05 * graph_height)
.style("font-size", `${0.025 * graph_width}px`)
.text(fips_codes.filter((o) => o.abbr == closest.state)[0].name);
try {
let attainment_date = get_attainment_date(
closest.state,
racial_group,
80
);
let attainment_position = x_scale(attainment_date);
coverage_graph
.append("circle")
.attr("class", "temp")
.attr("cx", attainment_position)
.attr("cy", y_scale(80))
.attr("r", 0.005 * graph_width)
.attr("fill", "black");
coverage_graph
.append("text")
.attr("class", "date")
.attr("x", attainment_position)
.attr("y", y_scale(80))
.attr("dx", 5)
.attr("dy", 15)
.attr("fill", "black")
.style("stroke", "white")
.style("fill", "black")
.style("paint-order", "stroke fill")
.text(dateFormat(attainment_date));
} catch {
("pass");
}
}
})
.on("mouseleave", function () {
coverage_graph.selectAll("circle.temp").remove();
coverage_graph.selectAll("text.state_name").remove();
coverage_graph.selectAll("text.date").remove();
coverage_graph
.selectAll("path.state")
.style("stroke-opacity", 0.2)
.style("stroke-width", "0.75px");
coverage_graph.select("text.default").attr("opacity", 1);
});

coverage_graph
.append("g")
.style("font", "14px times")
.attr("transform", `translate(0,${graph_height - margin.bottom})`)
.call(
d3
.axisBottom(x_scale)
.ticks(d3.timeMonth.every(2))
.tickFormat(d3.timeFormat("%b %e %Y"))
);
coverage_graph
.append("g")
.style("font", "14px times")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y_scale));

return svg.node();
}
Insert cell
// Generates the graphs of the different racial groups for a single selected state.

function make_racial_coverage_comparison_graph(state, opts = {}) {
let {
max_size = 768,
graph_width = 0.9 * (width < max_size ? width : max_size),
graph_height = 0.7 * graph_width,
margin = { left: 50, right: 15, bottom: 25, top: 10 },
group_to_highlight = null
} = opts;

let svg = d3
.create("svg")
.attr("width", graph_width)
.attr("height", graph_height);

let x_scale = d3
.scaleTime()
.domain([coverage_data.start_date, coverage_data.end_date])
.range([margin.left, graph_width - margin.right]);
let y_scale = d3
.scaleLinear()
.domain([0, 90])
.range([graph_height - margin.bottom, margin.top]);
let pts_to_path = d3
.line()
.defined((d) => !isNaN(d.vaccinated_pct_12_out) && !d.project)
.x((d) => x_scale(d.Date))
.y((d) => y_scale(d.vaccinated_pct_12_out));
let pts_to_projected_path = d3
.line()
.defined((d) => !isNaN(d.vaccinated_pct_12_out) && d.project)
.x((d) => x_scale(d.Date))
.y((d) => y_scale(d.vaccinated_pct_12_out));

let data_map = coverage_data.state_groups.get(state);
data_map.forEach(function (data, group) {
let group_stroke, group_stroke_width;
if (group_to_highlight) {
group_stroke = "black";
if (group == group_to_highlight) {
group_stroke_width = 4;
} else {
group_stroke_width = 0.5;
}
} else {
group_stroke = color(group);
group_stroke_width = 4;
}
if (race_grps.indexOf(group) > -1) {
svg
.append("path")
.attr("d", pts_to_path(data))
.style("stroke", group_stroke)
.style("stroke-width", group_stroke_width)
.style("fill", "none");
svg
.append("path")
.attr("d", pts_to_projected_path(data))
.style("stroke", group_stroke)
.style("stroke-width", group_stroke_width)
.attr("stroke-dasharray", "5 8")
.style("fill", "none");
}
});

let dateFormat = d3.timeFormat("%b %e");
let vline;
svg
.on("mouseenter", function () {
svg.selectAll(".line70").attr("opacity", 0);
vline = svg
.append("line")
.attr("class", "temp")
.attr("stroke", "black")
.attr("y1", 0)
.attr("y2", graph_height);
race_grps.forEach(function (s) {
svg
.append("circle")
.attr("class", "temp")
.attr("id", `point_${s}`)
.attr("r", 5)
.attr("fill", "black");
svg
.append("text")
.attr("class", "temp")
.attr("id", `value_${s}`)
.attr("fill", "black");
});
})
.on("mousemove", function (evt) {
let x = d3.pointer(evt)[0];
vline.attr("x1", x).attr("x2", x);
let t = x_scale.invert(x).getTime();
let values = get_state_values(state_selector.abbr, t);
values.forEach(function (o) {
let y = d3.max([y_scale(o.value), y_scale(89)]);
let cy = d3.max([y_scale(o.value), y_scale(92)]);
svg.select(`#point_${o.race_grp}`).attr("cx", x).attr("cy", cy);
svg
.select(`#value_${o.race_grp}`)
.attr("x", x)
.attr("y", y)
.attr("text-anchor", "end")
.attr("alignment-baseline", "text-top")
.text(`${o.value > 90 ? "90%+" : Math.round(o.value) + "%"}`);
});
})
.on("mouseleave", function () {
svg.selectAll(".line70").attr("opacity", 1);
svg.selectAll(".temp").remove();
});

svg
.append("g")
.style("font", "14px times")
.attr("transform", `translate(0,${graph_height - margin.bottom})`)
.call(
d3
.axisBottom(x_scale)
.ticks(d3.timeMonth.every(2))
.tickFormat(d3.timeFormat("%b %e %Y"))
);

svg
.append("g")
.style("font", "14px times")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y_scale));

return svg.node();
}
Insert cell
Insert cell
function get_state_values(state_abbr, time) {
let state_data = coverage_data.state_groups.get(state_abbr);

let results = [];
race_grps.forEach(function (race_grp) {
let this_data = state_data.get(race_grp);
if (this_data) {
let these_dates = this_data.map((o) => o.Date);
try {
let idx = d3.bisect(these_dates, time);
let t1 = these_dates[idx - 1].getTime();
let t2 = these_dates[idx].getTime();
let v1 = this_data[idx - 1].vaccinated_pct_12_out;
let v2 = this_data[idx].vaccinated_pct_12_out;
let v = v1 + ((v2 - v1) * (time - t1)) / (t2 - t1);
results.push({ race_grp: race_grp, value: v });
} catch {
("pass");
}
}
});

return results;
}
Insert cell
function get_closest_state_value(time, race, p) {
let results = [];
for (let [state_abbr, state_data] of coverage_data.state_groups) {
let this_data = state_data.get(race);
if (this_data) {
let these_dates = this_data.map(o => o.Date);
try {
let idx = d3.bisect(these_dates, time);
let t1 = these_dates[idx - 1].getTime();
let t2 = these_dates[idx].getTime();
let v1 = this_data[idx - 1].vaccinated_pct_12_out;
let v2 = this_data[idx].vaccinated_pct_12_out;
let v = v1 + ((v2 - v1) * (time.getTime() - t1)) / (t2 - t1);
results.push({
time: time,
value: v,
err: Math.abs(v - p),
state: state_abbr
});
} catch {
("pass");
}
}
}
return results.sort((o1, o2) => o1.err - o2.err)[0];
}
Insert cell
function get_attainment_date(state, race_grp, p) {
let this_data = coverage_data.state_groups.get(state).get(race_grp);
let idx = d3.bisect(this_data.map(o => o.vaccinated_pct_12_out), p);
let t1 = this_data[idx - 1].Date.getTime();
let t2 = this_data[idx].Date.getTime();
let v1 = this_data[idx - 1].vaccinated_pct_12_out;
let v2 = this_data[idx].vaccinated_pct_12_out;
let t = t1 + Math.abs(((t2 - t1) * (p - v1)) / (v2 - v1));
let d = new Date(t);

return d;
}
Insert cell
function coverage_shade(state, grp) {
let info = get_state_values(state, Date.now()).filter(o => o.race_grp == grp);
if (info.length == 1) {
info = info[0];
} else {
info = false;
}
// let info = coverage_data.state_groups.get(state).get(grp);
if (info) {
return d3.interpolateGreens(info.value / 100);
} else {
return "gray";
}
}
Insert cell
serialize = {
const xmlns = "http://www.w3.org/2000/xmlns/";
const xlinkns = "http://www.w3.org/1999/xlink";
const svgns = "http://www.w3.org/2000/svg";
return function serialize(svg) {
svg = svg.cloneNode(true);
const fragment = window.location.href + "#";
const walker = document.createTreeWalker(svg, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
for (const attr of walker.currentNode.attributes) {
if (attr.value.includes(fragment)) {
attr.value = attr.value.replace(fragment, "#");
}
}
}
svg.setAttributeNS(xmlns, "xmlns", svgns);
svg.setAttributeNS(xmlns, "xmlns:xlink", xlinkns);
const serializer = new window.XMLSerializer();
const string = serializer.serializeToString(svg);
return new Blob([string], { type: "image/svg+xml" });
};
}
Insert cell
Insert cell
attainment_objective = 80
Insert cell
cutoff_time = d3.timeParse("%b. %Y, %d")("Aug. 2021, 10").getTime()
// cutoff_time = Date.now()
Insert cell
race_grps = ["All", "Asian", "Black", "Hispanic", "White"]
Insert cell
color_scheme = d3.schemeRdBu[date_labels_for_hexmaps.length].map(
(_, i, a) => a[date_labels_for_hexmaps.length - 1 - i]
)
Insert cell
color = d3.scaleOrdinal(race_grps, [
"#767676",
"#6399AC",
"#e5a825",
"#c42e31",
"#832543"
])
Insert cell
date_labels_for_legend = date_labels_for_hexmaps.map(function (s) {
if (s.length > 2 && s.slice(-3) == "80%") {
return "Reached";
} else if (s.length > 4 && s.slice(-5) == "later") {
return "Later";
} else {
return s;
}
})
Insert cell
date_labels_for_hexmaps = {
let dates = [...new Set(attainment_projections.map((o) => o.date_80))]
.filter((s) => s != "NR")
.filter(
(s) =>
s == "Reached 80%" ||
(s.length > 7 && s.slice(-8) == "or later") ||
d3
.timeParse("%b. %Y, %d")(s + ", 30")
.getTime() > cutoff_time
);
dates.sort(function (a, b) {
if (a == "Reached 80%") {
return -1;
} else if (b == "Reached 80%") {
return 1;
} else if (b.length > 7 && b.slice(-8) == "or later") {
return -1;
} else if (a.length > 7 && a.slice(-8) == "or later") {
return 1;
} else {
if (parseTime(a).getTime() < parseTime(b).getTime()) {
return -1;
} else {
return 1;
}
}
});
return dates;
}
Insert cell
parseTime = d3.timeParse("%b. %Y")
Insert cell
Insert cell
coverage_data = {
let input_data = d3
.csvParse(
await (
await fetch(
"https://raw.githubusercontent.com/SC-COSMO/vax_disparities_dashboard/main/output/coverage_time_series.csv"
)
).text(),
d3.autoType
)
.filter((o) => o.vaccinated_pct_12_out != "NR");

let start_date = input_data[0].Date;
let end_date = input_data.slice(-1)[0].Date;
let output = { start_date: start_date, end_date: end_date };
let state_groups = d3.group(input_data, (s) => s.state);
let coverage_data_by_state = new Map();
state_groups.forEach((v, k) =>
coverage_data_by_state.set(
k,
d3.group(v, (d) => d.race_grp)
)
);
output.state_groups = coverage_data_by_state;

return output;
}
Insert cell
attainment_projections = d3.csvParse(
await (
await fetch(
"https://raw.githubusercontent.com/SC-COSMO/vax_disparities_dashboard/main/output/map_date_80.csv"
)
).text()
)

Insert cell
fips_codes = {
let fips_codes = (await FileAttachment("FIPSCodes.csv").csv()).slice(0, 51);
fips_codes.unshift({ abbr: "US", name: "United States" });
return fips_codes;
}
Insert cell
stateTiles = {
let stateTiles = await FileAttachment("stateTiles2.json").json();
let tiles = topojson.feature(stateTiles, stateTiles.objects.tiles);
return tiles;
}
Insert cell
text_blocks = d3.csvParse(
await (
await fetch(
"https://raw.githubusercontent.com/SC-COSMO/vax_disparities_dashboard/main/dashboard/text.csv"
)
).text()
)
Insert cell
Insert cell
tippy_style = html`<link rel="stylesheet" href="${await require.resolve(
`tippy.js/themes/light.css`
)}">`
Insert cell
tippy = require("tippy.js@6")
Insert cell
topojson = require("topojson-client@3")
Insert cell
import { legend, swatches } from "@d3/color-legend"
Insert cell
d3 = require("d3@7")
// d3 = require('d3-selection@3', 'd3-transition@3')
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