Public
Edited
Mar 29, 2023
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
map = () => {
const svg = d3.create("svg")
.attr("width", mapwidth)
.attr("height", mapheight)
.style("overflow", "visible");

svg.append("style").html(css);

svg.append("path")
.datum(statesInner)
.attr("class", "inner")
.attr("d", path);

svg.append("path")
.datum(statesOuter)
.attr("class", "outer")
.attr("d", path);

svg.selectAll(".circle")
.data(statesData)
.join("circle")
.attr("fill", colors.lighter)
.attr("r", d => r(d.barrels))
.attr("stroke", colors.darker)
.attr("transform", d => `translate(${projection(d.centroid)})`)

const annotation = svg.selectAll(".annotation")
.data(annotations)
.join("g")
.attr("class", "annotation")
.attr("transform", d => {
const mid = projection(d.centroid);
const edge = geometric.pointTranslate(mid, d.angle, r(d.barrels))
return `translate(${edge})`;
});
annotation.append("polyline")
.attr("points", d => [[0, 0], [d.x, d.y]])
.attr("stroke", "black")

annotation.append("text")
.attr("class", "bg state")
.attr("dx", d => d.dx)
.attr("dy", d => d.dy)
.attr("text-anchor", d => d.textAnchor)
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => d.state);

annotation.append("text")
.attr("class", "fg state")
.attr("dx", d => d.dx)
.attr("dy", d => d.dy)
.attr("text-anchor", d => d.textAnchor)
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => d.state);

annotation.append("text")
.attr("class", "bg value")
.attr("dx", d => d.dx)
.attr("dy", d => d.dy + 18)
.attr("text-anchor", d => d.textAnchor)
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => `${formatNumber(d.barrels)} barrels`);
annotation.append("text")
.attr("class", "fg value")
.attr("dx", d => d.dx)
.attr("dy", d => d.dy + 18)
.attr("text-anchor", d => d.textAnchor)
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => `${formatNumber(d.barrels)} barrels`);

return svg.node()
}
Insert cell
mapwillow = () => {
const height = 28;
const radius = r(willow);
const svg = d3.create("svg")
.attr("width", radius * 2 + (width <= 768 ? 180 : 204))
.attr("height", height + radius + 1)
.style("display", "table")
.style("margin", "0px auto 40px")
.style("overflow", "visible");

svg.append("style").html(css);
const g = svg.append("g");

g.append("path")
.attr("d", polylineRound([[radius, height - radius], [radius, 11], [21, 11]], ((height - radius) - 11) / (Math.PI / 2)))
.attr("fill", "none")
.attr("stroke", "black");

g.append("circle")
.attr("cx", radius)
.attr("cy", height)
.attr("fill", colors.lighter)
.attr("stroke", colors.darker)
.attr("r", radius);

const anno = g.append("g")
.attr("class", "annotation")
.attr("transform", `translate(${[radius, 14]})`);

anno.append("text")
.attr("class", "state")
.attr("x", 20)
.attr("y", 4)
.text("Willow");
anno.append("text")
.attr("class", "value")
.attr("x", 20)
.attr("y", 20)
.text(`${willow / 1e6} million barrels per year`);

return svg.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
css = `
.legend {
display: table;
font-family: ${franklinLight};
margin: 0 auto;
}

.legend .title {
font-size: 16px;
font-weight: bold;
}

.legend text {
font-family: ${franklinLight};
font-size: 14px;
}

.inner {
fill: none;
stroke: #ccc;
stroke-linejoin: round;
}

.outer {
fill: none;
stroke: #888;
stroke-linejoin: round;
}

.annotation text {
font-family: ${franklinLight};
}
.annotation text.bg {
fill: white;
stroke: white;
stroke-linejoin: round;
stroke-opacity: 0.8;
stroke-width: 6px;
}

.annotation text.state {
font-family: ${postoni};
font-size: 24px;
font-weight: bold;
}

.annotation text.value {
fill: ${colors.darker};
font-size: 16px;
}
`
Insert cell
colors = ({
darker: "#a54c24",
lighter: "#ee9e69"
})
Insert cell
formatNumber = barrels => {
return (
barrels >= 1e9 ? `${(barrels / 1e9).toFixed(1)} billion` :
barrels >= 1e7 ? `${(barrels / 1e6).toFixed(0)} million` :
barrels >= 1e6 ? `${(barrels / 1e6).toFixed(1)} million` :
barrels >= 1e4 ? `${(barrels / 1e3).toFixed(0)} thousand` :
d3.format(",")(barrels)
)
}
Insert cell
r = d3.scaleSqrt()
.domain([0, d3.max(statesData, d => d.barrels)])
.range([0, Math.sqrt(mapwidth * mapheight) / 15])
Insert cell
Insert cell
projection = d3.geoAlbersUsa()
.fitSize([mapwidth, mapheight], statesOuter)
Insert cell
path = d3.geoPath(projection)
Insert cell
Insert cell
mapwidth = width
Insert cell
mapheight = mapwidth * 0.605
Insert cell
Insert cell
// Downloaded from https://www.eia.gov/dnav/pet/pet_crd_crpdn_adc_mbbl_m.htm
eia = FileAttachment("crude-oil-production_20230228.csv").csv()
Insert cell
// A lookup file for U.S. states
statesLookup = FileAttachment("states.csv").csv()
Insert cell
// From Natural Earth
statesTopo = FileAttachment("ne_50m_admin_1_states_provinces_lakes.json").json()
Insert cell
// Get a list of states
states = Object
.keys(eia[0])
.filter(d => !d.includes("PADD") && !d.includes("U.S.") && !d.includes("North Slope") && !d.includes("South Field") && !d.includes("Federal Offshore") && d !== "Date")
.map(d => d.split(" Field")[0])
Insert cell
// Filter data for months for 2022
data2022 = eia.filter(d => d.Date.includes("-2022"))
Insert cell
// Create data by state for 2022
data = d3
.cross([["2022", data2022]], states)
.map(([[y, entries], state]) => {
const values = entries.map(d => d[`${state} Field Production of Crude Oil (Thousand Barrels)`] * 1e3);
const year = +y;
return {
year,
state,
barrels: d3.sum(values)
}
})
.filter(d => d.year >= 1993)
Insert cell
// Join the tabular and geospatial data
statesData = topojson
.feature(statesTopo, statesTopo.objects.ne_50m_admin_1_states_provinces_lakes)
.features
.map(d => {
const { properties } = d;
const match = statesLookup.find(d => d.state_fips === properties.fips.replace("US", ""));
const state = match.state_name;
const entries = data.filter(d0 => d0.state === state);
return {
state,
state_fips: match.state_fips,
centroid: state === "Alaska" ? [-150.5, 65.5] : state === "California" ? [-120.5, 37.5] : state === "Florida" ? [-82, 29.4] : state === "Louisiana" ? [-92.6, 31.2] : d3.geoCentroid(d),
barrels: d3.sum(entries, d0 => d0.barrels)
}
})
.sort((a, b) => d3.descending(a.barrels, b.barrels))
Insert cell
// Make annotations and join them to the data
annotations = [
{
state: "Alaska",
angle: -90,
textAnchor: "middle",
x: 0,
y: -5,
dx: 0,
dy: -20
},
{
state: "California",
angle: -90,
textAnchor: "middle",
x: 0,
y: -5,
dx: 0,
dy: -20
},
{
state: "New Mexico",
angle: -90,
textAnchor: "middle",
x: 0,
y: -5,
dx: 0,
dy: -20
},
{
state: "North Dakota",
angle: -90,
textAnchor: "middle",
x: 0,
y: -5,
dx: 0,
dy: -20
},
{
state: "Texas",
angle: 90,
textAnchor: "middle",
x: 0,
y: 5,
dx: 0,
dy: 18
}
]
.map(d => Object.assign(d, statesData.find(d0 => d0.state === d.state)))
Insert cell
// A GeoJSON object of the inner lines of the U.S. states
statesInner = topojson.mesh(statesTopo, statesTopo.objects.ne_50m_admin_1_states_provinces_lakes, (a, b) => a !== b)
Insert cell
// A GeoJSON object of the outer lines of the U.S. states
statesOuter = topojson.mesh(statesTopo, statesTopo.objects.ne_50m_admin_1_states_provinces_lakes, (a, b) => a === b)
Insert cell
// Willow's annual proudction
willow = 576e6 / 30
Insert cell
Insert cell
import { polylineRound } from "@harrystevens/rounded-polyline"
Insert cell
import { toc } from "@harrystevens/toc"
Insert cell
import {
franklinLight,
postoni
} from "1dec0e3505bd3624"
Insert cell
geometric = require("geometric@2")
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