Inequality in American cities

Based on a graphic by Emily Badger and Kevin Quealy, this chart shows the change from 1980 to 2015 of the ratio of 90th-percentile wages to 10th-percentile wages, along with population, in 195 metro areas. I prefer the static display to animation.

const width = 928;
const height = 640;
const marginTop = 24;
const marginRight = 10;
const marginBottom = 34;
const marginLeft = 40;
const arrowId = DOM.uid("arrow");
const gradientIds = data.map(() => DOM.uid("gradient"));
const endColor = d3.schemeCategory10[3];
const startColor = d3.schemeCategory10[1];

const x = d3.scaleLog()
    .domain(padLog(d3.extent(data.flatMap((d) => [d.POP_1980, d.POP_2015])), 0.1))
    .rangeRound([marginLeft, width - marginRight]);

const y = d3.scaleLinear()
    .domain(padLinear(d3.extent(data.flatMap((d) => [d.R90_10_1980, d.R90_10_2015])), 0.1))
    .rangeRound([height - marginBottom, marginTop]);

function arc(x1, y1, x2, y2) {
  const r = Math.hypot(x1 - x2, y1 - y2) * 2;
  return `M${x1},${y1} A${r},${r} 0,0,1 ${x2},${y2}`;
}

function padLinear([x0, x1], k) {
  const dx = (x1 - x0) * k / 2;
  return [x0 - dx, x1 + dx];
}

function padLog(x, k) {
  return padLinear(x.map(Math.log), k).map(Math.exp);
}

const svg = d3.create("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("viewBox", [0, 0, width, height])
    .attr("style", "max-width: 100%; height: auto;");

svg.append("defs")
  .append("marker")
    .attr("id", arrowId.id)
    .attr("markerHeight", 10)
    .attr("markerWidth", 10)
    .attr("refX", 5)
    .attr("refY", 2.5)
    .attr("orient", "auto")
  .append("path")
    .attr("fill", endColor)
    .attr("d", "M0,0v5l7,-2.5Z");

svg.append("defs")
  .selectAll()
  .data(data)
  .join("linearGradient")
    .attr("id", (d, i) => gradientIds[i].id)
    .attr("gradientUnits", "userSpaceOnUse")
    .attr("x1", (d) => x(d.POP_1980))
    .attr("x2", (d) => x(d.POP_2015))
    .attr("y1", (d) => y(d.R90_10_1980))
    .attr("y2", (d) => y(d.R90_10_2015))
    .call((g) => g.append("stop").attr("stop-color", startColor).attr("stop-opacity", 0.5))
    .call((g) => g.append("stop").attr("offset", "100%").attr("stop-color", endColor));

svg.append("g")
    .attr("stroke", "currentColor")
    .attr("stroke-opacity", 0.1)
    .call((g) => g.append("g")
      .selectAll("line")
      .data(x.ticks())
      .join("line")
        .attr("x1", (d) => 0.5 + x(d))
        .attr("x2", (d) => 0.5 + x(d))
        .attr("y1", marginTop)
        .attr("y2", height - marginBottom))
    .call((g) => g.append("g")
      .selectAll("line")
      .data(y.ticks())
      .join("line")
        .attr("y1", (d) => 0.5 + y(d))
        .attr("y2", (d) => 0.5 + y(d))
        .attr("x1", marginLeft)
        .attr("x2", width - marginRight));

svg.append("g")
    .attr("transform", `translate(0,${height - marginBottom})`)
    .call(d3.axisBottom(x).ticks(width / 80, ".1s"))
    .call((g) => g.select(".domain").remove())
    .call((g) => g.append("text")
        .attr("x", width)
        .attr("y", marginBottom - 4)
        .attr("fill", "currentColor")
        .attr("text-anchor", "end")
        .text("Population →"));

svg.append("g")
    .attr("transform", `translate(${marginLeft},0)`)
    .call(d3.axisLeft(y))
    .call((g) => g.select(".domain").remove())
    .call((g) => g.append("text")
        .attr("x", -marginLeft)
        .attr("y", 10)
        .attr("fill", "currentColor")
        .attr("text-anchor", "start")
        .text("↑ Inequality"));

svg.append("g")
    .attr("fill", "none")
  .selectAll()
  .data(data)
  .join("path")
    .attr("stroke", (d, i) => gradientIds[i])
    .attr("marker-end", arrowId)
    .attr("d", (d) => arc(x(d.POP_1980), y(d.R90_10_1980), x(d.POP_2015), y(d.R90_10_2015)));

svg.append("g")
    .attr("fill", "currentColor")
  .selectAll()
  .data(data)
  .join("circle")
    .attr("r", 1.75)
    .attr("cx", (d) => x(d.POP_1980))
    .attr("cy", (d) => y(d.R90_10_1980));

svg.append("g")
    .attr("fill", "currentColor")
    .attr("text-anchor", "middle")
    .attr("font-family", "sans-serif")
    .attr("font-size", 10)
    .attr("stroke", "white")
    .attr("stroke-linejoin", "round")
    .attr("stroke-width", 4)
    .attr("paint-order", "stroke fill")
  .selectAll()
  .data(data.filter((d) => d.highlight))
  .join("text")
    .attr("dy", (d) => d.R90_10_1980 > d.R90_10_2015 ? "1.2em" : "-0.5em")
    .attr("x", (d) => x(d.POP_2015))
    .attr("y", (d) => y(d.R90_10_2015))
    .text((d) => d.nyt_display);

display(svg.node());
const data = FileAttachment("data/metros.csv").csv({typed: true}).then(display);
✎ Suggest changes to this page