// Copyright 2021 Observable, Inc. // Released under the ISC license. // https://observablehq.com/@d3/choropleth function Choropleth({ 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 duration = 0, // duration for transitions } = {}) { // Compute feture map const If = d3.map(features.features, featureId); // 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); // Update DOM const svg = d3.create("svg") .classed('choropleth', true) .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .attr("style", "width: 100%; height: auto; height: intrinsic;"); svg.append("g") .classed("data-group", true); if (outline != null) svg.append("path") .classed('hidden', false) .attr("fill", fill) .attr("stroke", "currentColor") .attr("d", path(outline)); if (borders != null) svg.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)); // Object facet to update visualization let obj = { node: svg.node(), update: function (data) { const dataDomain = domain(data); const color = scale(dataDomain, range); if (color.unknown && unknown !== undefined) color.unknown(unknown); // Update DOM const svg = d3.select("svg.choropleth"); svg.select("g.data-group") .selectAll("path") .data(features.features) .join( (enter) => { return enter.append("path") .attr("fill", (d, i) => color(value(data[d.id]))) .attr("d", path) .append("title") .text((d, i) => title(d, data[d.id])); }, (update) => { update .transition() .duration(duration) .attr("fill", (d, i) => color(data[d.id].sevenDayMean)); return update .select("title") .text((d, i) => title(d, data[d.id])); }); Object.assign(svg.node(), {scales: {color}}); } } return obj; }