Public
Edited
Mar 29, 2023
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = () => {
const wrapper = d3.create("div")
.style("font-family", franklinLight);

wrapper.append("div")
.text("Cumulative approvals of applications for permits to drill on federal land")
const svg = wrapper.append("svg")
.attr("width", chartwidth + margin.left + margin.right)
.attr("height", chartheight + margin.top + margin.bottom);

const g = svg.append("g")
.attr("transform", `translate(${[margin.left, margin.top]})`);

const xaxis = g.append("g")
.attr("transform", `translate(0, ${chartheight})`)
.call(
d3.axisBottom(x)
.tickFormat(d => d ? d : "Inauguration")
.tickSize(10)
.tickValues([...d3.range(0, 800, 200), lastBiden])
);

xaxis.select(".domain").remove();
const xticks = xaxis.selectAll(".tick");
xticks.select("line")
.attr("stroke", "#808080");
xticks.select("text")
.attr("fill", "#494949")
.attr("font-family", franklinLight)
.attr("font-size", 14)
.attr("text-anchor", (d, i) => i === 0 ? "start" : "middle")
.attr("x", (d, i) => i === 0 ? -4 : 0);

g.append("text")
.attr("font-size", 16)
.attr("x", -4)
.attr("y", chartheight + 44)
.text("Number of days in office →")

const yaxis = g.append("g")
.attr("transform", `translate(${chartwidth})`)
.call(
d3.axisLeft(y)
.tickSize(chartwidth + 10)
);

yaxis.select(".domain").remove();
const yticks = yaxis.selectAll(".tick");

yticks.select("line")
.attr("stroke", "#d5d5d5")
yticks.select("text")
.attr("fill", "#494949")
.attr("font-family", franklinLight)
.attr("font-size", 14)
g.selectAll(".line")
.data(adminsPermits)
.join("path")
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", d => colors[d.admin])
.attr("stroke-width", 2)
.attr("d", d => line(d.data));

const lineLabels = g.selectAll(".line-label")
.data(adminsPermits)
.join("g")
.attr("class", "line-label")
.attr("font-family", franklinBold)
.attr("font-size", 18)
.attr("text-anchor", d => d.admin === "Biden" ? "end" : "start")
.style("text-transform", "uppercase")
.attr("transform", d => {
const mid = d.data[Math.floor(lastBiden / 2)];
const lx = x(mid.admin_days);
const ly = y(mid.cumsum) + (d.admin === "Trump" ? 12 : 0);
const h = 8 * (d.admin === "Biden" ? -1 : 1);
const v = h * (chartheight / chartwidth);
return `translate(${[lx + h, ly + v]})`
});

lineLabels.append("text")
.attr("fill", "white")
.attr("stroke", "white")
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round")
.attr("stroke-width", 8)
.text(d => d.admin)
lineLabels.append("text")
.attr("fill", d => colors[d.admin])
.text(d => d.admin);

const endLabels = g.selectAll(".end-label")
.data(adminsPermits)
.join("g")
.attr("class", "end-label")
.attr("transform", d => {
const end = d.data[d.data.length - 1];
return `translate(${[x(end.admin_days), y(end.cumsum)]})`
});

endLabels.append("circle")
.attr("fill", d => colors[d.admin])
.attr("r", 4)
.attr("stroke", "white");

endLabels.append("text")
.attr("dy", d => 7 * (d.admin === "Biden" ? -1 : 1))
.attr("fill", d => colors[d.admin])
.attr("font-family", franklinLight)
.attr("font-size", 16)
.attr("x", 7)
.attr("y", 3)
.text(d => d3.format(",")(d.data[d.data.length - 1].cumsum))
return wrapper.node();
}
Insert cell
note = (text) => d3.create("div")
.style("color", "#666")
.style("font-family", franklinLight)
.style("font-size", "14px")
.text(text)
.node()
Insert cell
Insert cell
colors = ({
"Biden": "#a54c24",
"Trump": "#ee9e69"
})
Insert cell
Insert cell
line = d3.line()
.x(d => x(d.admin_days))
.y(d => y(d.cumsum))
.curve(d3.curveStep)
Insert cell
Insert cell
x = d3.scaleLinear()
.domain(d3.extent(adminsFlat, d => d.admin_days))
.range([0, chartwidth])
Insert cell
y = d3.scaleLinear()
.domain([0, d3.max(adminsFlat, d => d.cumsum)])
.range([chartheight, 0])
Insert cell
Insert cell
margin = ({left: 49, right: 47, top: 16, bottom: 48})
Insert cell
basewidth = Math.min(width, 640)
Insert cell
chartwidth = basewidth - margin.left - margin.right
Insert cell
chartheight = Math.max(300, chartwidth * 9 / 16)
Insert cell
Insert cell
// From the Bureau of Land Management: https://reports.blm.gov/report/AFMSS/81/Approved-APDs-Report-Federal
approvedFederalApds = FileAttachment("Approved Federal APDs_20170120-20230327.csv").csv()
Insert cell
// Parse the BLM's file
approvedFederalApdsParsed = {
// Remove sum rows
const data = approvedFederalApds.filter(d => d["Lease Type"] === "FED")

// Convert column names to snake case
const keys = Object.keys(data[0]);
const cols = keys.map(toSnakeCase);

// Start of admins
const bidenStart = new Date("2021-01-20");
const trumpStart = new Date("2017-01-20");

// Add dates
return data
.map(d => {
const o = {};
keys.forEach((key, i) => {
const col = cols[i];
const val = d[key];
if (key) o[col] = val;
if (col.endsWith("_date")) {
o[`${col}time`] = parseDate(val);
}
});

// Admin
if (o.apd_approval_datetime.getTime() >= bidenStart.getTime()) {
o.admin = "Biden";
o.admin_days = moment(o.apd_approval_datetime).diff(bidenStart, "days");
}
else {
o.admin = "Trump";
o.admin_days = moment(o.apd_approval_datetime).diff(trumpStart, "days");
}
return o;
})
.sort((a, b) => d3.descending(a.apd_approval_datetime, b.apd_approval_datetime))
}
Insert cell
approvedFederalApdsParsed.filter(d => d.apd_approval_date === "1/20/2021")
Insert cell
approvedFederalApdsParsed.filter(d => d.apd_approval_date === "1/21/2021")
Insert cell
// Get the number of Biden's most recent day
lastBiden = d3.max(approvedFederalApdsParsed, d => d.admin === "Trump" ? 0 : d.admin_days)
Insert cell
// Filter days equal to or ealier than Biden's most recent day
// so we make an even comparison between administrations
admins = d3.groups(approvedFederalApdsParsed.filter(d => d.admin_days <= lastBiden), d => d.admin)
Insert cell
// Within each administration, calculate the cumulative sum of permits by day
adminsPermits = admins
.map(([admin, entries]) => {
const data = [];
let cumsum = 0;
d3
.range(lastBiden + 1)
.map(admin_days => {
const matches = entries.filter(d => d.admin_days === admin_days);
const approvals = matches.length;
cumsum += approvals;
data.push({
admin_days,
cumsum
});
});
return {
admin,
data
}
})
.sort((a, b) => d3.descending(a.admin, b.admin))
Insert cell
// Flatten the grouped admin data for use with scales
adminsFlat = adminsPermits.map(d => d.data).flat()
Insert cell
Insert cell
// A utility function to parse "mm/dd/yyyy" date strings
parseDate = string => {
const p = n => n < 10 ? `0${n}` : n.toString();
const [mm, dd, yyyy] = string.split("/").map(Number);
if (!yyyy || !mm || !dd) return null;
return new Date(`${yyyy}-${p(mm)}-${p(dd)}`);
}
Insert cell
// Snake case converts difficult column names
toSnakeCase = string => {
return string
.toLowerCase()
.replace(/\s+/g, "_") // Replace spaces with _
.replace(/[^\w\_]+/g, "") // Remove all non-word chars
.replace(/\_\_+/g, "_") // Replace multiple _ with single _
.replace(/^_+/, "") // Trim _ from start of text
.replace(/_+$/, ""); // Trim _ from end of text
}
Insert cell
Insert cell
import { toc } from "@harrystevens/toc"
Insert cell
import {
franklinLight,
franklinBold
} from "1dec0e3505bd3624"
Insert cell
moment = require('moment@2.29.4/moment.js')
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