Choropleth Map with Regional Coloring — D3.js

A choropleth map visualizes data by shading geographic regions (countries, states, or counties) according to a measured variable. This technique is ideal for showing regional patterns and spatial distributions, making it easy to identify areas with high or low values at a glance. The color intensity represents the data magnitude, creating an intuitive way to understand geographic variation.

Choropleth Map with Regional Coloring rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// choropleth-basic: Choropleth Map with Regional Coloring
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 88/100 | Created: 2026-09-02

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;
// `muted` isn't part of ANYPLOT_TOKENS — derive it from the theme, matching
// the Imprint semantic anchor in prompts/default-style-guide.md.
const MUTED = t.theme === "dark" ? "#A8A79F" : "#6B6A63";

// --- Data (in-memory, deterministic) ----------------------------------------
// Illustrative renewable-electricity share (%) of generation, one synthetic
// value per U.S. state. Laid out as an equal-area tile grid ("state grid
// map" cartogram) rather than literal shapefile boundaries — the renderer
// runs offline with no network/topojson access, and every state keeps the
// same visual weight regardless of its true land area, a well-established
// choropleth variant for all-state comparisons (e.g. NPR's state grid maps).
let seed = 42;
function nextRandom() {
  seed = (1103515245 * seed + 12345) % 2147483648;
  return seed / 2147483648;
}

const regions = [
  { abbr: "AK", col: -1, row: 0 }, { abbr: "HI", col: -1, row: 1 },
  { abbr: "WA", col: 0, row: 0 }, { abbr: "OR", col: 0, row: 1 }, { abbr: "CA", col: 0, row: 2 },
  { abbr: "NV", col: 0, row: 3 }, { abbr: "AZ", col: 0, row: 4 },
  { abbr: "ID", col: 1, row: 0 }, { abbr: "UT", col: 1, row: 1 }, { abbr: "NM", col: 1, row: 2 },
  { abbr: "MT", col: 2, row: 0 }, { abbr: "WY", col: 2, row: 1 }, { abbr: "CO", col: 2, row: 2 },
  { abbr: "ND", col: 3, row: 0 }, { abbr: "SD", col: 3, row: 1 }, { abbr: "NE", col: 3, row: 2 },
  { abbr: "KS", col: 3, row: 3 }, { abbr: "OK", col: 3, row: 4 }, { abbr: "TX", col: 3, row: 5 },
  { abbr: "MN", col: 4, row: 0 }, { abbr: "IA", col: 4, row: 1 }, { abbr: "MO", col: 4, row: 2 },
  { abbr: "AR", col: 4, row: 3 }, { abbr: "LA", col: 4, row: 4 },
  { abbr: "WI", col: 5, row: 0 }, { abbr: "IL", col: 5, row: 1 }, { abbr: "KY", col: 5, row: 2 },
  { abbr: "TN", col: 5, row: 3 }, { abbr: "MS", col: 5, row: 4 }, { abbr: "AL", col: 5, row: 5 },
  { abbr: "MI", col: 6, row: 0 }, { abbr: "IN", col: 6, row: 1 }, { abbr: "OH", col: 6, row: 2 },
  { abbr: "WV", col: 6, row: 3 }, { abbr: "VA", col: 6, row: 4 }, { abbr: "NC", col: 6, row: 5 },
  { abbr: "SC", col: 6, row: 6 }, { abbr: "GA", col: 6, row: 7 }, { abbr: "FL", col: 6, row: 8 },
  { abbr: "NY", col: 7, row: 0 }, { abbr: "PA", col: 7, row: 1 }, { abbr: "NJ", col: 7, row: 2 },
  { abbr: "DE", col: 7, row: 3 }, { abbr: "MD", col: 7, row: 4 },
  { abbr: "VT", col: 8, row: 0 }, { abbr: "MA", col: 8, row: 1 }, { abbr: "CT", col: 8, row: 2 },
  { abbr: "RI", col: 8, row: 3 },
  { abbr: "ME", col: 9, row: 0 }, { abbr: "NH", col: 9, row: 1 },
].map((d) => ({ ...d, value: Math.round(5 + nextRandom() * 90) }));

// Two states illustrate graceful handling of missing data (spec requirement).
const noDataStates = new Set(["WY", "SC"]);
for (const region of regions) {
  if (noDataStates.has(region.abbr)) region.value = null;
}

// Highest/lowest extremes, called out below to sharpen the story beyond the
// base color encoding and to give the map's empty southwest corner a job.
const validRegions = regions.filter((d) => d.value != null);
const maxRegion = validRegions.reduce((a, b) => (b.value > a.value ? b : a));
const minRegion = validRegions.reduce((a, b) => (b.value < a.value ? b : a));
const highlightAbbrs = new Set([maxRegion.abbr, minRegion.abbr]);

// --- Layout -------------------------------------------------------------
const margin = { top: 150, right: 70, bottom: 40, left: 70 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

const x = d3.scaleBand().domain(d3.range(-1, 10)).range([0, iw]).padding(0.1);
const y = d3.scaleBand().domain(d3.range(0, 9)).range([0, ih]).padding(0.1);

const valueExtent = d3.extent(regions.filter((d) => d.value != null), (d) => d.value);
const color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain(valueExtent);

// Pick dark or light label ink from the tile's own fill luminance, independent
// of page theme — a pale tile needs dark text even on the dark surface.
function labelInk(hex) {
  const c = d3.rgb(hex);
  const luminance = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
  return luminance > 150 ? "#1A1A17" : "#F0EFE8";
}

// --- SVG mount ----------------------------------------------------------
const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

// --- Tiles ----------------------------------------------------------------
const tile = g
  .selectAll("g.tile")
  .data(regions)
  .join("g")
  .attr("class", "tile")
  .attr("transform", (d) => `translate(${x(d.col)},${y(d.row)})`);

tile
  .append("rect")
  .attr("width", x.bandwidth())
  .attr("height", y.bandwidth())
  .attr("rx", 4)
  .attr("fill", (d) => (d.value == null ? MUTED : color(d.value)))
  .attr("stroke", (d) => (highlightAbbrs.has(d.abbr) ? t.ink : t.pageBg))
  .attr("stroke-width", (d) => (highlightAbbrs.has(d.abbr) ? 4 : 3));

tile
  .append("text")
  .attr("x", x.bandwidth() / 2)
  .attr("y", y.bandwidth() / 2)
  .attr("text-anchor", "middle")
  .attr("dominant-baseline", "central")
  .style("font-size", "16px")
  .style("font-weight", "600")
  .attr("fill", (d) => labelInk(d.value == null ? MUTED : color(d.value)))
  .text((d) => d.abbr);

// --- Highest/lowest callout --------------------------------------------------
// The tile-grid's true US shape leaves cols -1..2 empty below row 5 (nothing
// west of Texas reaches that far south) — use that void for a data-storytelling
// callout instead of leaving it dead space, and echo the ink-stroke swatches
// against the two highlighted tiles above.
const calloutX = x(-1);
const calloutRowY = y(5);
const calloutEntries = [
  { region: maxRegion, label: "highest" },
  { region: minRegion, label: "lowest" },
];
g.append("text")
  .attr("x", calloutX)
  .attr("y", calloutRowY + 14)
  .attr("fill", t.inkSoft)
  .style("font-size", "14px")
  .style("font-weight", "600")
  .text("Highest & lowest share");
calloutEntries.forEach((entry, i) => {
  const rowY = calloutRowY + 40 + i * 30;
  g.append("rect")
    .attr("x", calloutX)
    .attr("y", rowY)
    .attr("width", 16)
    .attr("height", 16)
    .attr("rx", 3)
    .attr("fill", "none")
    .attr("stroke", t.ink)
    .attr("stroke-width", 3);
  g.append("text")
    .attr("x", calloutX + 24)
    .attr("y", rowY + 13)
    .attr("fill", t.inkSoft)
    .style("font-size", "14px")
    .text(`${entry.region.abbr} — ${entry.region.value}% (${entry.label})`);
});

// --- Title ------------------------------------------------------------------
const title = "Renewable Electricity Share · choropleth-basic · javascript · d3 · anyplot.ai";
// Default bumped from the style-guide's 22px baseline: at the formula's 19px
// result this title measured only ~42% of canvas width (below the ~50-70%
// comfort range) per AI review — 30px recalibrates for this title's actual
// rendered character width while the 67-char linear taper still guards long
// titles from overflow.
const titleFontSize = Math.round(30 * Math.min(1, 67 / title.length));
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 50)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", `${titleFontSize}px`)
  .style("font-weight", "600")
  .text(title);

// --- Legend (sequential gradient + no-data swatch) --------------------------
const legendWidth = 260;
const legendHeight = 18;
const noDataSwatchGap = 20;
const noDataLabelWidth = 60;
const legendTotalWidth = legendWidth + noDataSwatchGap + legendHeight + 8 + noDataLabelWidth;
const legendX = width - 20 - legendTotalWidth;
const legendY = 95;

const gradientId = "choropleth-legend-gradient";
const gradient = svg
  .append("defs")
  .append("linearGradient")
  .attr("id", gradientId)
  .attr("x1", "0%")
  .attr("x2", "100%")
  .attr("y1", "0%")
  .attr("y2", "0%");
d3.range(0, 1.001, 0.1).forEach((stop) => {
  gradient
    .append("stop")
    .attr("offset", `${stop * 100}%`)
    .attr("stop-color", color(valueExtent[0] + stop * (valueExtent[1] - valueExtent[0])));
});

svg
  .append("text")
  .attr("x", legendX)
  .attr("y", legendY - 12)
  .attr("fill", t.inkSoft)
  .style("font-size", "14px")
  .text("Renewable electricity share (%)");

svg
  .append("rect")
  .attr("x", legendX)
  .attr("y", legendY)
  .attr("width", legendWidth)
  .attr("height", legendHeight)
  .attr("fill", `url(#${gradientId})`);

svg
  .append("text")
  .attr("x", legendX)
  .attr("y", legendY + legendHeight + 20)
  .attr("fill", t.inkSoft)
  .style("font-size", "13px")
  .text(`${valueExtent[0]}%`);

svg
  .append("text")
  .attr("x", legendX + legendWidth)
  .attr("y", legendY + legendHeight + 20)
  .attr("text-anchor", "end")
  .attr("fill", t.inkSoft)
  .style("font-size", "13px")
  .text(`${valueExtent[1]}%`);

svg
  .append("rect")
  .attr("x", legendX + legendWidth + noDataSwatchGap)
  .attr("y", legendY)
  .attr("width", legendHeight)
  .attr("height", legendHeight)
  .attr("rx", 3)
  .attr("fill", MUTED);

svg
  .append("text")
  .attr("x", legendX + legendWidth + noDataSwatchGap + legendHeight + 8)
  .attr("y", legendY + legendHeight - 4)
  .attr("fill", t.inkSoft)
  .style("font-size", "13px")
  .text("No data");

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/choropleth-basic/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "choropleth-basic",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/choropleth-basic/javascript/d3",
  "hub": "https://anyplot.ai/choropleth-basic",
  "code_json": "https://api.anyplot.ai/specs/choropleth-basic/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/choropleth-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/choropleth-basic/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/choropleth-basic/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/choropleth-basic/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/choropleth-basic/javascript/d3/plot-dark.html",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Choropleth Map with Regional Coloring on anyplot.ai.

Other implementations