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";
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;
}