Public
Edited
Oct 10, 2023
1 fork
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function make_map() {
let div = d3.create("div").style("width", `${w}px`).style("height", `${h}px`);
let svg = div.append("svg").attr("width", w).attr("height", h);
let map = svg.append("g");

let projection = d3
.geoConicEqualArea()
.parallels([29.5, 45.5])
.rotate([90, 0])
.fitSize([w - 10, h], rect);
let path = d3.geoPath().projection(projection);

map
.append("path")
.attr("d", path(contiguous_states))
.attr("fill", "lightgray")
.attr("stroke", "white")
.attr("stroke-width", 1);

let strokeWidth = w / 1100;
let xy;
map
.selectAll("circle")
.data(grouped)
.join("circle")
.attr("id", (g) => g[0])
.attr("cx", function (g) {
let schools = g[1];
let lat = d3.mean(schools.map((o) => o.latitude));
let lon = d3.mean(schools.map((o) => o.longitude));
xy = projection([lon, lat]);
return xy[0];
})
.attr("cy", function (g) {
let schools = g[1];
let lat = d3.mean(schools.map((o) => o.latitude));
let lon = d3.mean(schools.map((o) => o.longitude));
xy = projection([lon, lat]);
return xy[1];
})
.attr("r", (g) => (3 * Math.sqrt(g[1].length) * w) / 1100)
.attr("fill", "#a00")
.attr("stroke", "#000")
.attr("stroke-width", strokeWidth)
.attr("fill-opacity", 0.3)
.on("pointerenter", function () {
d3.select(this).attr("stroke-width", 2 * strokeWidth);
})
.on("pointerleave", function () {
d3.select(this).attr("stroke-width", strokeWidth);
})
.nodes()
.forEach(function (c) {
let content = get_tip(c.id);
tippy(c, {
content: content,
theme: "light",
allowHTML: true,
interactive: true,
appendTo: () => div.node() // document.body,
});
});

svg.call(
d3
.zoom()
.scaleExtent([0.5, 8])
.duration(500)
.on("zoom", function (evt) {
map.attr("transform", evt.transform);
})
);

return div.node();
}
Insert cell
make_table = () =>
Inputs.table(
hbcu_data.map(function (o) {
let clone = {};
clone.School = o["Institution Name"];
clone.City = o["City location of institution (HD2020)"];
clone.State = o["State abbreviation (HD2020)"];
clone.Sector =
o["Sector of institution (HD2020)"] % 2 == 0 ? "Private" : "Public";

let car_class = parseInt(
o["Carnegie Classification 2018: Basic (HD2020)"]
);
if (car_class < 10) {
car_class = "Associate's";
} else if (18 <= car_class && car_class <= 20) {
car_class = "Master's";
} else if (21 <= car_class && car_class <= 23) {
car_class = "Baccalaureate";
} else if (car_class == 16) {
car_class = "Doctoral";
} else if (car_class == 24) {
car_class = "Religious";
} else if (car_class == 25) {
car_class = "Medical";
} else if (car_class == 31) {
car_class = "Law";
}
clone["Highest degree type"] = car_class;

let religious_affiliation = o["Religious affiliation (IC2020)"];
if (religious_affiliation == 51) {
religious_affiliation = "African Methodist Episcopal Church";
} else if (religious_affiliation == 24) {
religious_affiliation = "African Methodist Episcopal Zion Church";
} else if (religious_affiliation == 52) {
religious_affiliation = "American Baptist Church";
} else if (religious_affiliation == 54) {
religious_affiliation = "Baptist Church";
} else if (religious_affiliation == 61) {
religious_affiliation = "Disciples of Christ";
} else if (religious_affiliation == 55) {
religious_affiliation = "Christian Methodist Episcopal Church";
} else if (religious_affiliation == 74) {
religious_affiliation = "sChurches of Christ";
} else if (religious_affiliation == 42) {
religious_affiliation = "Interdenominational";
} else if (religious_affiliation == 78) {
religious_affiliation = "Multiple Protestant Denominational";
} else if (religious_affiliation == 66) {
religious_affiliation = "Presbyterian Church ";
} else if (religious_affiliation == 73) {
religious_affiliation = "Protestant Church";
} else if (religious_affiliation == 30) {
religious_affiliation = "Roman Catholic Church";
} else if (religious_affiliation == 95) {
religious_affiliation = "Seventh Day Adventist Church";
} else if (religious_affiliation == 76) {
religious_affiliation = "United Church of Christ";
} else if (religious_affiliation == 71) {
religious_affiliation = "United Methodist Church";
} else if (religious_affiliation == -2) {
religious_affiliation = "None";
}
clone["Religious affiliation"] = religious_affiliation;

clone["Student body"] = parseInt(
o["Grand total (EF2020A All students total)"]
);
clone["Percent African American"] = Math.round(
(100 *
parseInt(
o["Black or African American total (EF2020A All students total)"]
)) /
parseInt(o["Grand total (EF2020A All students total)"])
);

clone.Link = o["Institution's internet website address (HD2020)"];

return clone;
}),
{
height: h,
rows: 50,
format: {
Link: (link) => html`<a href="http://${link}" target="_blank">link</a>`,
"Percent African American": (p) => `${p}%`
}
}
)
Insert cell
hbcu_data
Insert cell
function make_time_series_graphs(variable, opts = {}) {
let {
max_size = 1100,
margin = { left: 70, right: 30, bottom: 25, top: 20 },
opacity = 0.2,
interactive = true,
top5 = true
} = opts;

let data = get_single_value(variable, hbcu_data);
let max = data.max;
if (top5) {
data = d3.sort(data, (a) => a.slice(-1)[0].value).slice(-5);
data.max = max;
}

let graph_width = width < max_size ? width : max_size;
let graph_height = 0.625 * graph_width;
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([new Date("2010"), new Date("2020")])
.range([margin.left, graph_width - margin.right]);
let y_scale = d3
.scaleLinear()
.domain([0, data.max])
.range([graph_height - margin.bottom, margin.top]);
let pts_to_path = d3
.line()
.x((d) => x_scale(d.year))
.y((d) => y_scale(d.value));

let axes = svg.append("g");
let x_axis = d3.axisBottom(x_scale);
axes
.append("g")
.attr("transform", `translate(0,${graph_height - margin.bottom})`)
.call(x_axis.tickSizeOuter(0));
let y_axis = d3.axisLeft(y_scale);
axes
.append("g")
.attr("transform", `translate(${margin.left})`)
.call(y_axis.tickSizeOuter(0));

let time_series_plots = svg.append("g");
let text_group = svg
.append("text")
.attr("x", 1.3 * margin.left)
.attr("y", 1.5 * margin.top)
.attr("font-size", 16)
.attr("text-anchor", "start")
.attr("fill", "steelblue")
.text("Avg highlighted");
data.forEach(function (d) {
time_series_plots
.append("path")
.attr("d", pts_to_path(d))
.attr("class", "time_series")
.attr("stroke", "black")
.attr("id", `uid${d[0].UnitID}`)
.attr("stroke-opacity", opacity)
.attr("stroke-width", 0.7)
.attr("stroke-linejoin", "round")
.attr("fill", "none");
});

let get_mean_values = (date) =>
d3.mean(
data
.map(function (d) {
let dd = d.filter((o) => o.year.getTime() == date.getTime());
return dd;
})
.filter((a) => a.length > 0)
.map((a) => a[0].value)
);

let means = d3
.range(2010, 2021)
.map((d) => new Date(d.toString()))
.map((d) => ({ value: get_mean_values(d), year: d }));

time_series_plots
.append("path")
.attr("d", pts_to_path(means))
.attr("class", "avg_time_series")
.attr("stroke", "steelblue")
.attr("id", "avg")
.attr("stroke-opacity", 1)
.attr("stroke-width", 3)
.attr("stroke-linejoin", "round")
.attr("fill", "none");

svg
.on("touchmove", (e) => e.preventDefault()) // prevent scrolling
.on("pointermove", function (evt) {
let p = d3.pointer(evt);
let x = p[0];
let y = p[1];
let day = x_scale.invert(x);
let value = y_scale.invert(y);
let closest = get_closest(day, value, data);
if (closest) {
time_series_plots
.selectAll("path.time_series")
.attr("stroke-opacity", opacity)
.attr("stroke-width", 0.7)
.attr("stroke", "black");
time_series_plots
.select(`#uid${closest.uid}`)
.attr("stroke-opacity", 1)
.attr("stroke-width", 6)
.attr("stroke", "steelblue")
.raise();

text_group.text(
hbcu_data.filter((o) => o.UnitID == closest.uid)[0][
"Institution Name"
]
);
}
})
.on("pointerleave", function () {
time_series_plots
.selectAll("path.time_series")
.attr("stroke-opacity", opacity)
.attr("stroke-width", 0.7)
.attr("stroke", "black");
time_series_plots
.select("#avg")
.attr("stroke-opacity", 1)
.attr("stroke-width", 3)
.attr("stroke", "steelblue")
.raise();
text_group.text("Avg highlighted");
});

return svg.node();
}
Insert cell
Insert cell
function get_closest(time, value, data) {
let results = [];

data.forEach(function (d) {
let these_dates = d.map((o) => o.year);
try {
let idx = d3.bisect(these_dates, time);
let t1 = these_dates[idx - 1].getTime();
let t2 = these_dates[idx].getTime();
let p1 = d[idx - 1].value;
let p2 = d[idx].value;
let p = p1 + ((p2 - p1) * (time.getTime() - t1)) / (t2 - t1);
results.push({
time: time,
value: p,
err: Math.abs(p - value),
uid: d[0].UnitID,
});
} catch {
("pass");
}
});
results.sort((o1, o2) => o1.err - o2.err);
return results[0];
}
Insert cell
function get_tip(city_name) {
let group = grouped.get(city_name);
let state = group[0]["State abbreviation (HD2020)"];
let tip = `<h3>${city_name}, ${state}</h3><ul>`;
group.forEach(function (o) {
let pub_pri =
o["Sector of institution (HD2020)"] % 2 == 0 ? "private" : "public";
let enrollment = o["Grand total (EF2020A All students total)"];
let percentage_black =
parseInt(
o["Black or African American total (EF2020A All students total)"]
) / parseInt(o["Grand total (EF2020A All students total)"]);
let car_class = parseInt(o["Carnegie Classification 2018: Basic (HD2020)"]);
if (car_class < 10) {
car_class = "associate's";
} else if (18 <= car_class && car_class <= 20) {
car_class = "master's";
} else if (21 <= car_class && car_class <= 23) {
car_class = "baccalaureate";
} else if (car_class == 16) {
car_class = "doctoral";
} else if (car_class == 24) {
car_class = "religious";
} else if (car_class == 25) {
car_class = "medical";
} else if (car_class == 31) {
car_class = "law";
}
let religious_affiliation = o["Religious affiliation (IC2020)"];
if (religious_affiliation == 51) {
religious_affiliation =
"school affiliated with the African Methodist Episcopal Church";
} else if (religious_affiliation == 24) {
religious_affiliation =
"school affiliated with the African Methodist Episcopal Zion Church";
} else if (religious_affiliation == 52) {
religious_affiliation =
"school affiliated with the American Baptist Church";
} else if (religious_affiliation == 54) {
religious_affiliation = "school affiliated with the Baptist Church";
} else if (religious_affiliation == 61) {
religious_affiliation = "school affiliated with the Disciples of Christ";
} else if (religious_affiliation == 55) {
religious_affiliation =
"school affiliated with the Christian Methodist Episcopal Church";
} else if (religious_affiliation == 74) {
religious_affiliation = "school affiliated with the Churches of Christ";
} else if (religious_affiliation == 42) {
religious_affiliation = "interdenominational school";
} else if (religious_affiliation == 78) {
religious_affiliation = "multiple protestant denominational school";
} else if (religious_affiliation == 66) {
religious_affiliation = "school affiliated with the Presbyterian Church ";
} else if (religious_affiliation == 73) {
religious_affiliation = "school affiliated with the Protestant Church";
} else if (religious_affiliation == 30) {
religious_affiliation =
"school affiliated with the Roman Catholic Church";
} else if (religious_affiliation == 95) {
religious_affiliation =
"school affiliated with the Seventh Day Adventist Church";
} else if (religious_affiliation == 76) {
religious_affiliation =
"school affiliated with the United Church of Christ";
} else if (religious_affiliation == 71) {
religious_affiliation =
"school affiliated with the United Methodist Church";
} else if (religious_affiliation == -2) {
religious_affiliation = "school";
}
console.log(o["Institution's internet website address (HD2020)"]);
tip =
tip +
`<li><span style="font-weight: bold"><a target="_blank" href="http://${
o["Institution's internet website address (HD2020)"]
}">${
o["Institution Name"]
}</a></span> <br /> <div style="font-style:italic; margin: 3px;">A ${pub_pri}, ${car_class} ${religious_affiliation} with ${enrollment} students, ${d3.format(
"0.0%"
)(percentage_black)} of whom are African American.</div></li>`;
});
tip = tip + "</ul>";
return tip;
}
Insert cell
function get_single_value(column_start, data) {
let columns = Object.keys(data[0]).filter(
(s) => s.slice(0, column_start.length) == column_start
);
let td = data.map(function (d) {
let row = columns.map((key) => ({
year: new Date(key.match(find_year_re)[0].slice(-4)),
value: parseFloat(d[key]),
UnitID: d.UnitID
}));
row = d3.sort(row, (o) => o.year);
return row;
});

// Figure out which data is incomplete
// let incomplete = td.filter(
// (a) => a.map((o) => isNaN(o.value)).indexOf(true) > -1
// );
// incomplete.forEach(function (o) {
// let name = hbcu_data.filter((oo) => o[0].UnitID == oo.UnitID);
// if (name.length > 0) {
// name = name[0]["Institution Name"];
// console.log(name);
// }
// });

td = td.filter((a) => a.map((o) => isNaN(o.value)).indexOf(true) == -1);
let all_values = td.map((o) => o.map((oo) => oo.value)).flat();
let [min, max] = d3.extent(all_values);
td.min = min;
td.max = max;

return td;
}
Insert cell
find_year_re = /\([A-Z]{2,}20\d{2}/
Insert cell
h = 1.2 * rect.features[0].properties.aspect * w
Insert cell
w = width < 1100 ? width : 1100
Insert cell
Insert cell
// A bounding rectangle
rect = {
let xmax = -74;
let xmin = -101;
let ymin = 24;
let ymax = 41;
let aspect = (ymax - ymin) / (xmax - xmin);
return {
type: "FeatureCollection",

features: [
{
type: "Feature",
properties: { aspect: aspect },
geometry: {
type: "LineString",
coordinates: [
[xmin, ymin],
[xmax, ymax]
]
}
}
]
};
}
Insert cell
contiguous_states = {
let contiguous_states = await FileAttachment("contiguous_states.json").json();
contiguous_states = topojson.feature(
contiguous_states,
contiguous_states.objects.contiguous_states
);
return contiguous_states;
}
Insert cell
hbcu_data = {
let hbcu_data = await FileAttachment("hbcu3@1.csv").csv();
hbcu_data.forEach(function (o) {
o.latitude = parseFloat(o["Latitude location of institution (HD2020)"]);
o.longitude = parseFloat(o["Longitude location of institution (HD2020)"]);

let city = o["City location of institution (HD2020)"];
if (city == "Huntsville" || city == "Normal") {
city = "Huntsville - Normal";
} else if (city == "Fairfield" || city == "Birmingham") {
city = "Fairfield - Birmingham";
} else if (city == "Little Rock" || city == "N Little Rock") {
city = "Little Rock";
}
o.city = city;
});
return hbcu_data;
}
Insert cell
grouped = d3.group(hbcu_data, (o) => o.city)
Insert cell
FileAttachment("hbcu3@1.csv").csv()
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

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