chart = {
const width = 960
const height = 720
const mapOffset = 60
const svg = d3.create("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.style("width", "100%")
.style("cursor", "pointer")
// add title and subtitle
svg.append("text")
.attr("x", width / 2)
.attr("y", 35)
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("font-weight", "bold")
.text("Agricultural Land Change, 2017–2022")
svg.append("text")
.attr("x", width / 2)
.attr("y", 60)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("fill", "#555")
.text("% change in total farm acreage — almost all states lost acreage in just 5 years")
svg.append("text")
.attr("x", width / 2)
.attr("y", 80)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "bold")
.style("fill", "#555")
.text("Click a state to explore counties")
// --- Zoom behavior ---
const zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", (event) => {
mapG.selectAll("g").attr("transform", event.transform)
})
svg.call(zoom)
const mapG = svg.append("g")
.attr("id", "map-group")
.attr("transform", `translate(0, ${mapOffset})`)
// --- State layer ---
mapG.append("g")
.attr("id", "state-layer")
.selectAll("path")
.data(states.features)
.join("path")
.attr("d", path)
.attr("fill", d => {
const row = stateMap.get(d.id)
return row ? colorScale(row.perc_baseline) : "#ccc"
})
.attr("stroke", "#333")
.attr("stroke-width", 0.5)
.on("click", function(event, d) {
event.stopPropagation()
const [[x0, y0], [x1, y1]] = path.bounds(d)
const scale = Math.min(8, 0.7 / Math.max((x1 - x0) / width, (y1 - y0) / height))
const tx = width / 2 - scale * (x0 + x1) / 2
const ty = height / 2 - scale * (y0 + y1) / 2
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity.translate(tx, ty).scale(scale)
)
mapG.select("#state-layer").selectAll("path")
.transition().duration(750)
.attr("fill", s => s.id === d.id ? "none" : "#ddd")
.attr("stroke", s => s.id === d.id ? "none" : "#fff")
mapG.select("#state-borders")
.transition().duration(750)
.attr("stroke-opacity", 1)
// Fade title out on zoom
svg.selectAll("text")
.transition().duration(750)
.attr("opacity", 0)
const stateFips = d.id
const stateCounties = counties.features
.filter(c => c.id.slice(0, 2) === stateFips)
mapG.selectAll(".county-layer").remove()
mapG.insert("g", "#state-layer")
.attr("class", "county-layer")
.selectAll("path")
.data(stateCounties)
.join("path")
.attr("d", path)
.attr("fill", c => {
const row = countyMap.get(c.id)
return row ? colorScale(row.perc_baseline) : "#ccc"
})
.attr("stroke", "#333")
.attr("stroke-width", 0.3)
})
// --- State borders ---
mapG.append("g")
.attr("id", "state-borders-g")
.append("path")
.attr("id", "state-borders")
.datum(topojson.mesh(us, us.objects.states, (a, b) => a !== b))
.attr("fill", "none")
.attr("stroke", "#333")
.attr("stroke-width", 1)
.attr("d", path)
mapG.append("g")
.attr("id", "nation-outline")
.append("path")
.datum(topojson.feature(us, us.objects.nation))
.attr("fill", "none")
.attr("stroke", "#333")
.attr("stroke-width", 1.5)
.attr("d", path)
// --- Reset on background click ---
svg.on("click", function() {
mapG.selectAll(".county-layer").remove()
mapG.select("#state-layer").selectAll("path")
.transition().duration(750)
.attr("fill", d => {
const row = stateMap.get(d.id)
return row ? colorScale(row.perc_baseline) : "#ccc"
})
.attr("stroke", "#333")
mapG.select("#state-borders")
.transition().duration(750)
.attr("stroke-opacity", 1)
// Fade title back in on reset
svg.selectAll("text")
.transition().duration(750)
.attr("opacity", 1)
svg.transition().duration(750)
.call(zoom.transform, d3.zoomIdentity)
})
return svg.node()
}legend = {
const lWidth = 360
const lHeight = 80
const svg = d3.create("svg")
.attr("width", lWidth)
.attr("height", lHeight)
const defs = svg.append("defs")
const grad = defs.append("linearGradient")
.attr("id", "legend-gradient")
.attr("x1", "0%").attr("x2", "100%")
grad.selectAll("stop")
.data([
{offset: "0%", color: "#d73027"},
{offset: "50%", color: "#ffffff"},
{offset: "100%", color: "#1a9850"}
])
.join("stop")
.attr("offset", d => d.offset)
.attr("stop-color", d => d.color)
// Gradient bar
svg.append("rect")
.attr("x", 30)
.attr("y", 30)
.attr("width", lWidth - 60)
.attr("height", 16)
.style("fill", "url(#legend-gradient)")
.attr("stroke", "#999")
.attr("stroke-width", 0.5)
// Tick marks at -10, 0, +10
const scale = d3.scaleLinear()
.domain([-10, 10])
.range([30, lWidth - 30])
const ticks = [-10, 0, 10]
ticks.forEach(val => {
svg.append("line")
.attr("x1", scale(val)).attr("x2", scale(val))
.attr("y1", 46).attr("y2", 52)
.attr("stroke", "#666")
.attr("stroke-width", 1)
svg.append("text")
.attr("x", scale(val))
.attr("y", 64)
.attr("text-anchor", "middle")
.style("font-size", "11px")
.style("fill", "#444")
.text(val === 0 ? "0%" : val > 0 ? `+${val}%` : `${val}%`)
})
// Caption
svg.append("text")
.attr("x", lWidth / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.style("font-size", "15px")
.style("fill", "#888")
.text("% change (capped at ±10%)")
return svg.node()
}tooltipSetup = {
// Create tooltip div
const tooltip = d3.select("body")
.append("div")
.attr("class", "map-tooltip")
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.75)")
.style("color", "#fff")
.style("padding", "6px 10px")
.style("border-radius", "4px")
.style("font-size", "13px")
.style("pointer-events", "none")
.style("opacity", 0)
const svg = d3.select(chart)
// --- State tooltips ---
svg.select("#state-layer").selectAll("path")
.on("mouseover", function(event, d) {
const row = stateMap.get(d.id)
if (!row) return
tooltip
.style("opacity", 1)
.html(`<strong>${row.StateName}</strong><br>${row.perc_baseline.toFixed(1)}% change`)
})
.on("mousemove", function(event) {
tooltip
.style("left", (event.pageX + 12) + "px")
.style("top", (event.pageY - 28) + "px")
})
.on("mouseout", function() {
tooltip.style("opacity", 0)
})
// --- County tooltips (delegated to svg, catches dynamically added counties) ---
svg.on("mouseover.tip", function(event) {
const el = event.target
if (!el.__data__ || !el.__data__.id) return
const c = el.__data__
if (c.id.length !== 5) return
const row = countyMap.get(c.id)
// No data — hide tooltip and reset fill to grey
if (!row || row.perc_baseline == null) {
tooltip
.style("opacity", 1)
.html(`<strong>${row ? row.CountyName + ", " + row.StateName : "Unknown County"}</strong><br>Data Unavailable`)
return
}
tooltip
.style("opacity", 1)
.html(`<strong>${row.CountyName}, ${row.StateName}</strong><br>${row.perc_baseline.toFixed(1)}% change`)
})
svg.on("mousemove.tip", function(event) {
tooltip
.style("left", (event.pageX + 12) + "px")
.style("top", (event.pageY - 28) + "px")
})
svg.on("mouseout.tip", function(event) {
if (!event.target.__data__) return
tooltip.style("opacity", 0)
})
return null
}
Data: USDA National Agricultural Statistics Service (NASS) Census of Agriculture, 2017 and 2022.
Note: Total farm acres calculated as number of farms x average size at county level, aggregated for state level.
Viz: Louis M Penrod | #30DayChartChallenge