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

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