Published
Edited
Nov 4, 2021
Fork of Choropleth
Importers
1 star
Insert cell
Insert cell
Insert cell
chart = ChoroplethZoomable(unemployment, {
id: d => d.id,
value: d => d.rate,
scale: d3.scaleQuantize,
domain: [1, 10],
range: d3.schemeBlues[9],
title: (f, d) => `${f.properties.name}, ${statemap.get(f.id.slice(0, 2)).properties.name}\n${d?.rate}%`,
features: counties,
borders: statemesh,
width: 975,
height: 610
})
Insert cell
Insert cell
unemployment = (await FileAttachment("unemployment-x.csv").csv()).map(d => ({...d, rate: +d.rate}))
Insert cell
Insert cell
us = FileAttachment("counties-albers-10m.json").json()
Insert cell
counties = topojson.feature(us, us.objects.counties)
Insert cell
statemap = new Map(topojson.feature(us, us.objects.states).features.map(d => [d.id, d]))
Insert cell
Insert cell
statemesh = topojson.mesh(us, us.objects.states, (a, b) => a !== b)
Insert cell
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/choropleth
function ChoroplethZoomable(data, {
id = d => d.id, // given d in data, returns the feature id
value = () => undefined, // given d in data, returns the quantitative value
title, // given a feature f and possibly a datum d, returns the hover text
format, // optional format specifier for the title
scale = d3.scaleSequential, // type of color scale
domain, // [min, max] values; input of color scale
range = d3.interpolateBlues, // output of color scale
width = 640, // outer width, in pixels
height, // outer height, in pixels
projection, // a D3 projection; null for pre-projected geometry
features, // a GeoJSON feature collection
featureId = d => d.id, // given a feature, returns its id
borders, // a GeoJSON object for stroking borders
outline = projection && projection.rotate ? {type: "Sphere"} : null, // a GeoJSON object for the background
unknown = "#ccc", // fill color for missing data
fill = "white", // fill color for outline
stroke = "white", // stroke color for borders
strokeLinecap = "round", // stroke line cap for borders
strokeLinejoin = "round", // stroke line join for borders
strokeWidth, // stroke width for borders
strokeOpacity, // stroke opacity for borders
} = {}) {
// Compute values.
const N = d3.map(data, id);
const V = d3.map(data, value).map(d => d == null ? NaN : +d);
const Im = new d3.InternMap(N.map((id, i) => [id, i]));
const If = d3.map(features.features, featureId);

// Compute default domains.
if (domain === undefined) domain = d3.extent(V);

// Construct scales.
const color = scale(domain, range);
if (unknown !== undefined) color.unknown(unknown);

// Compute titles.
if (title === undefined) {
format = color.tickFormat(100, format);
title = (f, i) => `${f.properties.name}\n${format(V[i])}`;
} else if (title !== null) {
const T = title;
const O = d3.map(data, d => d);
title = (f, i) => T(f, O[i]);
}

// Compute the default height. If an outline object is specified, scale the projection to fit
// the width, and then compute the corresponding height.
if (height === undefined) {
if (outline === undefined) {
height = 400;
} else {
const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, outline)).bounds(outline);
const dy = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale(projection.scale() * (l - 1) / l).precision(0.2);
height = dy;
}
}

// Construct a path generator.
const path = d3.geoPath(projection);

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

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.on("click", reset);
const g = svg.append("g")
if (outline != null) g.append("path")
.attr("fill", fill)
.attr("stroke", "currentColor")
.attr("d", path(outline));
g.selectAll("path")
.data(features.features)
.join("path")
.attr("fill", (d, i) => color(V[Im.get(If[i])]))
.attr("d", path)
.attr("class", "countries")
.attr('vector-effect', 'non-scaling-stroke')
//.on("click", clicked)
.append("title")
.text((d, i) => title(d, Im.get(If[i])));

if (borders != null) g.append("path")
.attr("pointer-events", "none")
.attr("fill", "none")
.attr("stroke", stroke)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.attr("d", path(borders));

svg.call(zoom);
let tooltipData;
const tooltip = svg.append("g");
g.selectAll(".countries").on("touchmove mousemove", function(event, d) {
//Get the country
const transform = d3.zoomTransform(svg.node());
//Stroke the border so it stands out
d3.select(this)
.attr("stroke", "#333")
.attr("stroke-width", "1px")
.raise();

//Get current data
// console.log(d)
//const { value, type } = tooltipData.get(d.properties.name);

//Move the tooltip to the correct position
tooltip
.attr(
"transform",
`translate(${transform
.apply(d3.pointer(event, this))
.map(e => e - 60)})`
)
.call(
callout,
`id: ${d.id}\nCountry: ${d.properties.name}`
);
});

g.selectAll(".countries").on("touchend mouseleave", function() {
d3.select(this)
.attr("stroke-width", "null")
.attr("stroke", "none")
.lower();
tooltip.call(callout, null);
});
function reset() {
svg
.transition()
.duration(750)
.call(
zoom.transform,
d3.zoomIdentity,
d3.zoomTransform(svg.node()).invert([width / 2, height / 2])
);
}
function zoomed(event) {
const { transform } = event;
g.attr("transform", transform);
g.attr("stroke-width", 1 / transform.k);
// apply transform on other layers if needed
//const sqrtK = Math.sqrt(transform.k);
//circles.selectAll(".circle").attr("r", d => radius / sqrtK);
}
function updateData(newData) {
/* update color here
filtering happens in react using useState when query happens or can be added here
svg.selectAll(".countries").attr("fill", d => {
return newData.has(+d.properties.id)
});
*/
tooltipData = newData;
}
updateData(data)
return Object.assign(svg.node(), {
scales: {color},
reset,
callout,
//clicked,
zoomed,
updateData, // update data
//updateCircles // toggle data
});
}
Insert cell
callout = (g, value) => {
// Slightly altered from https://observablehq.com/@d3/line-chart-with-tooltip - ISC License
if (!value) return g.style("display", "none");

g.style("display", null)
.style("pointer-events", "none")
.style("font", "14px sans-serif");

const path = g
.selectAll("path")
.data([null])
.join("path")
.attr("fill", "white")
.attr("stroke", "black");

const text = g
.selectAll("text")
.data([null])
.join("text")
.call(text =>
text
.selectAll("tspan")
.data((value + "").split(/\n/))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i) => `${i * 1.1}em`)
.style("font-weight", (_, i) => (i ? null : "bold"))
.text(d => d)
);

const { x, y, width: w, height: h } = text.node().getBBox();

text.attr("transform", `translate(${-w / 2},${15 - y})`);
//rectangle
path.attr("d", `M${-w / 2 - 10},5H-5H${w / 2 + 10}v${h + 20}h-${w + 20}z`);
}
Insert cell
import {howto} from "@d3/example-components"
Insert cell
import {Legend} from "@d3/color-legend"
Insert cell
import { whichPolygon } from "@gallowayevan/which-polygon"
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