Published
Edited
Aug 26, 2022
3 forks
Importers
16 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof testWithCountiesData = drawChoroplethMap({
data: countiesData,
colorScale,
tooltipContents,
zoomEnabled: zoomToggle === "zoomEnabled"
})
Insert cell
testWithStatesData = drawChoroplethMap({
data: statesData,
colorScale,
tooltipContents,
adminLevel: "states",
zoomEnabled: zoomToggle === "zoomEnabled"
})
Insert cell
testMoreOptions = drawChoroplethMap({
width: 450,
// height: 300,
zoomEnabled: false,
borderEnabled: false,
adminLevel: "states",
backgroundColor: "#333"
})
Insert cell
colorScale = d3
.scaleQuantize()
.domain([0, 0.8])
.range(d3.schemeRdPu[7])
Insert cell
tooltipContents = datum => {
const { id, value } = datum;
return `${id}: ${d3.format(".2f")(value)}`;
}
Insert cell
Insert cell
drawChoroplethMap = (options = {}) => {
const adminLevel = allowedAdminLevels.has(options.adminLevel)
? options.adminLevel
: "counties";
const data = options.data || [];
const accessor =
options.accessor ||
function (d) {
return d.value;
};
const colorScale = options.colorScale || defaultColorScale;
const colorNoData = options.colorNoData || "#e2e2e2";
const name = options.name
? options.name.replace(/,/gi, "").replace(/ /gi, "")
: DOM.uid().id;
const cssScoped = `map-${name}-${adminLevel}-container`;
const tooltipContents = options.tooltipContents;
const width = options.width || mapConfig.width;
const height = options.height || Math.floor((width * 3) / 5);
const zoomEnabled = options.zoomEnabled !== false ? true : false;
const borderEnabled = options.borderEnabled !== false ? true : false;
const borderColorStates = options.borderColorStates || "#fff";
const borderColorCounties = options.borderColorCounties || "#e2e2e2";
const backgroundColor = options.backgroundColor || "#fff";

// for making the map a "viewof" cell
const initialState = {
selected: {},
hovered: {}
};

const geoData =
adminLevel === "counties" ? countiesGeoByFips : statesGeoByFips;

const zoom = d3.zoom().scaleExtent([1, 8]).on("zoom", zoomed);

const container = html`<div class="${cssScoped}">
<style>
.${cssScoped} {
position: relative;
width: ${width}px;
height: ${height}px;
background-color: ${backgroundColor};
}
.${cssScoped} .tooltip {
display: none;
position: fixed;
font-size: 14px;
font-family: sans-serif, Arial;
padding: 6px;
background-color: #fff;
border: 1px solid #333;
pointer-events: none;
}
.${cssScoped} .zoom-btns {
position: absolute;
bottom: 24px;
right: 24px;
}
.${cssScoped} .zoom-btns button {
display: ${zoomEnabled ? "block;" : "none;"}
width: 25px;
height: 25px;
background-color: #fff;
border: 1px solid #222;
padding: 6px;
margin: 0;
font-weight: bold;
font-family: serif;
font-size: 18px;
line-height: 1px;
cursor: pointer;
}
.${cssScoped} .zoom-btns button:first-of-type {
border-bottom: none;
}
.${cssScoped} .zoom-btns button:hover {
background-color: #e2e2e2;
}
</style>
<div class="zoom-btns">
<button aria-label="zoom in" class="zoom-in">+</button>
<button aria-label="zoom out" class="zoom-out">–</button>
</div>
</div>`;

const svg = d3
.select(container)
.append("svg")
.attr("viewBox", `0 0 ${mapConfig.width} ${mapConfig.height}`);

const g = svg.append("g");

const divTooltip = d3
.select(container)
.append("div")
.classed("tooltip", true);

const tooltip = new Tooltip(divTooltip);

const btnZoomIn = d3
.select(container)
.select("button.zoom-in")
.on("click", handleZoomIn);

const btnZoomOut = d3
.select(container)
.select("button.zoom-out")
.on("click", handleZoomOut);

g.append("g")
.attr("id", "nation-boundary")
.attr("stroke-width", 0.8)
.append("path")
.attr("vector-effect", "non-scaling-stroke")
.attr("d", path(topojson.mesh(us, us.objects.nation)))
.attr("fill", "#eee")
.attr("stroke", "#888");

g.append("g")
.attr("id", `${adminLevel}-data`)
.selectAll("path")
.data(data, (d) => d.id)
.join("path")
.attr("vector-effect", "non-scaling-stroke")
.attr("d", (d) => path(geoData.get(d.id)))
.attr("fill", (d) => {
const value = accessor(d);
if (value === null || value === undefined) {
return colorNoData;
}
return colorScale(value);
})
.on("mouseover", ({ currentTarget }, d) => {
d3.select(currentTarget)
.attr("stroke", "yellow")
.attr("stroke-width", 2)
.raise();
if (tooltipContents) tooltip.display(d, tooltipContents);
dispatch("hovered", d);
})
.on("mouseout", ({ currentTarget }) => {
d3.select(currentTarget).attr("stroke", "none").attr("stroke-width", 0);
if (tooltipContents) tooltip.hide();
})
.on("mousemove", (event) => {
if (tooltipContents) tooltip.move(event);
})
.on("click", (event, d) => {
dispatch("selected", d);
});

if (adminLevel === "counties") {
g.append("g")
.attr("id", "county-boundaries")
.attr("stroke-width", 0.5)
.append("path")
.attr(
"d",
path(
topojson.mesh(
us,
us.objects.counties,
(a, b) => a !== b && ((a.id / 1000) | 0) === ((b.id / 1000) | 0)
)
)
)
.attr("fill", "none")
.attr("stroke", borderColorCounties);
}

g.append("g")
.attr("id", "state-boundaries")
.attr("stroke-width", 0.7)
.append("path")
.attr("vector-effect", "non-scaling-stroke")
.attr("d", path(topojson.mesh(us, us.objects.states)))
.attr("fill", "none")
.attr("stroke", borderColorStates);

if (borderEnabled) {
svg
.append("rect")
.attr("width", mapConfig.width)
.attr("height", mapConfig.height)
.attr("fill", "none")
.attr("stroke", "#222")
.attr("stroke-width", 2);
}

// Helper functions that need access to local variables in this cell
function zoomed({ transform }) {
g.attr("transform", transform);
g.selectAll("g").attr("stroke-width", 1 / transform.k);
}

function handleZoomIn(event) {
svg.transition().duration(500).call(zoom.scaleBy, 1.74);
}

function handleZoomOut(event) {
svg.transition().duration(500).call(zoom.scaleBy, 0.5);
}

// for when the map is a "viewof" cell
function dispatch(key, value) {
container.value = { ...container.value, ...{ [key]: value } };
container.dispatchEvent(new CustomEvent("input"));
}

if (zoomEnabled) svg.call(zoom);
container.value = { ...initialState };

return container;
}
Insert cell
mapConfig = ({
padding: 24,
height: Math.floor((width * 3) / 5),
width
})
Insert cell
defaultColorScale = d3.scaleQuantize().domain([0, 1]).range(d3.schemeRdPu[5])
Insert cell
allowedAdminLevels = new Set(["counties", "states"])
Insert cell
path = d3.geoPath(projection)
Insert cell
projection = d3
.geoAlbersUsa()
.fitExtent(
[
[mapConfig.padding, mapConfig.padding],
[
mapConfig.width - mapConfig.padding,
mapConfig.height - mapConfig.padding
]
],
countiesGeo
)
Insert cell
format = d3.format(".2f")
Insert cell
countiesData = Array.from(testData.entries()).map(([id, values]) => ({
id,
value: values.HD01_VD13 / values.HD01_VD01
}))
Insert cell
countiesGeoByFips = new Map(countiesGeo.features.map(d => [d.id, d]))
Insert cell
countiesGeo = topojson.feature(us, "counties")
Insert cell
statesData = {
const data = Array.from(statesGeoByFips.keys()).map(id => ({
id,
value: +format(Math.random())
}));
data.find(d => d.id === "48").value = null; // test no data color
return data;
}
Insert cell
statesGeoByFips = new Map(statesGeo.features.map(d => [d.id, d]))
Insert cell
statesGeo = topojson.feature(us, "states")
Insert cell
us = d3.json("https://cdn.jsdelivr.net/npm/us-atlas@3/counties-10m.json")
Insert cell
topojson = require("topojson-client@3")
Insert cell
Insert cell
import { checkbox } from "@jashkenas/inputs"
Insert cell
import { acs as testData } from "@clhenrick/u-s-broadband-internet-access"
Insert cell
import {
countyFipsToName,
countyNameToFips,
stateNameToFips,
stateFipsToName
} from "@clhenrick/us-census-constants"
Insert cell
import { Tooltip } from "@clhenrick/tooltip-component"
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