Published
Edited
Jun 26, 2021
22 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// The main function
function make_map() {
let height = 0.4 * width;

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

let svg = div
.append("svg")
.style("width", `${width}px`)
.style("height", `${height}px`)
.style("overflow", "hidden")
.style("background", "white");

// The map data is preprojected Albers Equal area for NC
let proj = d3
.geoIdentity()
.reflectY(true)
.fitSize([width, height], map_data.tracts);
let path = d3.geoPath().projection(proj);

let map = svg.append("g");
let tracts = map
.append("g")
.selectAll("path.tract")
.data(map_data.tracts.features)
.join("path")
.attr("d", path)
.attr("class", "tract")
.attr("id", (d, i) => `tract${i}`)
.attr("data-geoid", (d) => d.properties.GEOID)
.style("fill", function (d) {
return color_map.get(d.properties.GEOID);
})
.style("stroke-width", "0.2px")
.style("stroke-opacity", 0.2)
.style("stroke", "#000")
.style("stroke-linejoin", "round");
tracts
.on("mouseenter", function () {
d3.select(this).style("stroke-width", "1px").style("stroke-opacity", 1);
})
.on("mouseleave", function () {
d3.select(this)
.style("stroke-width", "0.2px")
.style("stroke-opacity", 0.2);
});
tracts.nodes().forEach((c) => {
tippy(c, {
allowHTML: true,
maxWidth: 420,
theme: "light",
content: table_map.get(c.getAttribute("data-geoid"))
});
});

let cities = map.append("g");
let city_names = cities
.selectAll("text.city")
.data(city_data)
.join("text")
.attr("class", "city")
.attr("x", (d) => proj([d.x, d.y])[0])
.attr("y", (d) => proj([d.x, d.y])[1])
.text((d) => d.city)
.style("font-family", "sans-serif")
.style("font-size", "14px")
.attr("pointer-events", "none");
let city_markers = cities
.selectAll("circle.city")
.data(city_data)
.join("circle")
.attr("class", "city")
.attr("cx", (d) => proj([d.x, d.y])[0])
.attr("cy", (d) => proj([d.x, d.y])[1])
.attr("r", 3)
.style("fill", "yellow")
.attr("pointer-events", "none");

svg.call(
d3
.zoom()
.extent([
[0, 0],
[width, height]
])
.translateExtent([
[0, 0],
[width, height]
])
.scaleExtent([1, 8])
.duration(750)
.on("zoom", function (evt) {
map.attr("transform", evt.transform);
map.selectAll(".highlight").remove();
// map
// .selectAll("path.tract")
// .style("stroke-width", `${0.2 / d3.event.transform.k}px`);
map.selectAll("circle.city").attr("r", `${3 / evt.transform.k}`);
map
.selectAll("text.city")
.style("font-size", `${14 / evt.transform.k ** 0.75}px`);
})
);

return div.node();
}
Insert cell
Insert cell
// The population data comes from the most recent five year
// American Community Survey estimates, which is 2018:
// https://www.census.gov/data/developers/data-sets/acs-5year.html
// https://api.census.gov/data/2018/acs/acs5/groups/B02001.html

raw_data = (await (await fetch(
'https://api.census.gov/data/2018/acs/acs5?get=NAME,B02001_001E,B02001_002E,B02001_003E&for=tract:*&in=state:37'
)).json()).slice(1)
Insert cell
// This map associates an HTML table with each tract GEOID
// to be shown as a tooltip.

table_map = {
let table_map = new Map();
raw_data.forEach(function([
place,
total,
white,
black,
state_fips,
county_fips,
tract_id
]) {
let div = d3.create('div');
let [tract, county, state] = place.split(', ');
tract = tract.slice(7);
div.append('h3').text(`${tract} in ${county}`);
//.style('color', 'white');
let table = div.append('table');
let head = table.append('thead').append('tr');
head.append('th').text('Race');
head.append('th').text('Population');
head.append('th').text('Percentage');
let combined_row = table.append('tr');
combined_row.append('td').text('All');
combined_row.append('td').text(`${total}`);
let white_row = table.append('tr');
white_row.append('td').text('White');
white_row.append('td').text(`${white}`);
white_row.append('td').text(`${d3.format('0.2f')((100 * white) / total)}%`);
let black_row = table.append('tr');
black_row.append('td').text('Black');
black_row.append('td').text(`${black}`);
black_row.append('td').text(`${d3.format('0.2f')((100 * black) / total)}%`);
let other_row = table.append('tr');
other_row.append('td').text('Other');
other_row.append('td').text(`${total - white - black}`);
other_row
.append('td')
.text(`${d3.format('0.2f')((100 * (total - white - black)) / total)}%`);
table_map.set(state_fips + county_fips + tract_id, div.node().innerHTML);
});
return table_map;
}
Insert cell
// Associate a color with each tract GEOID.
color_map = {
let color_map = new Map();
let max = d3.max(raw_data.map(o => parseFloat(o[1])));

raw_data.forEach(function([
place,
total,
white,
black,
state_fips,
county_fips,
tract_id
]) {
let geoid = state_fips + county_fips + tract_id;
if (total > 0) {
let population_density = density_map.get(geoid);
color_map.set(
geoid,
`rgba(${(255 * white) / total}, ${(255 * black) / total}, ${(255 *
(total - white - black)) /
total}, ${Math.pow(population_density / density_map.max, 0.25)})`
);
} else {
color_map.set(geoid, '#eee');
}
});
return color_map;
}
Insert cell
density_map = {
let density_map = new Map();
map_data.tracts.features
.map(c => c.properties)
.forEach(function(o) {
let geoid = o.GEOID;
let pop = pop_map.get(geoid).total;
let density = pop / o.ALAND;
density_map.set(geoid, density);
});
density_map.max = d3.max(Array.from(density_map.values()));
return density_map;
}
Insert cell
pop_map = {
let pop_map = new Map();
raw_data.forEach(function(o) {
pop_map.set(o[4] + o[5] + o[6], { total: o[1], white: o[2], black: o[3] });
});
return pop_map;
}
Insert cell
// Load the TopoJSON map file
map_data = {
let map_file = await FileAttachment("nc_tracts_topo.json").json();
let nc_tracts = topojson.feature(map_file, map_file.objects.nc_tracts);
return {
tracts: nc_tracts
};
}
Insert cell
// Display just a few cities
city_data = d3.csvParse(`city,lat,lon,x,y
Asheville,35.6,-82.55,-243640,3758538
Charlotte,35.2271,-80.8431,-89404,3714312
Durham,35.994,-78.8986,86782,3799379
Fayetteville,35.0527,-78.8784,89635,3694965
Raleigh,35.7796,-78.6382,110555,3775851
Wilmington,34.2104,-77.886,182013,3602891
Winston-Salem,36.0999,-80.2442,-34513,3810776`)
Insert cell
Insert cell
// tippy = require("https://unpkg.com/tippy.js@2.5.4/dist/tippy.all.min.js")

tippy = require("tippy.js@6")
Insert cell
topojson = require("topojson-client@3")
Insert cell
d3 = require('d3-selection@2', 'd3-array@2', 'd3-format@2', 'd3-geo@2', 'd3-zoom@2', 'd3-dsv@2')
Insert cell
tippy_style = html`<link rel="stylesheet" href="${await require.resolve(
`tippy.js/themes/light.css`
)}">`
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