Public
Edited
Jan 28, 2024
3 forks
27 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
legend = () => html`
<div style="width: ${mapwidth}px">
<div style="display: table; margin: 0 auto">
<div style="display: inline-block">${legendSize()}</div>
<div style="display: inline-block; margin-left: 40px;">${legendChange()}</div>
</div>
</div>
`
Insert cell
legendSize = () => {
const data = [2, 4, 6, 8, 10, 12];
const rectPad = 6.5;
const margin = {left: 1, right: 0, top: 20, bottom: 20};
const width = Math.max(78, d3.sum(data) + rectPad * (data.length - 1) + 2) - margin.left - margin.right;
const height = data[data.length - 1];

const svg = d3.create("svg")
.attr("font-family", franklinLight)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("overflow", "visible");

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

const title = g.append("g")
.attr("transform", `translate(0, -6)`);

title.append("text")
.text("Abundance")
.attr("font-size", 16);

g.selectAll(".rect")
.data(data)
.join("rect")
.attr("fill", "#626262")
.attr("x", (d, i) => rectPad * i + d3.sum(data.filter((_, i0) => i0 < i)))
.attr("y", d => height - d)
.attr("width", d => d)
.attr("height", d => d);
g.append("text")
.attr("font-size", 14)
.attr("y", height + 14)
.text("More →");

return svg.node();
}
Insert cell
legendChange = () => {
const margin = {left: 0, right: 0, top: 20, bottom: 20};
const width = 300 - margin.left - margin.right;
const height = 12;

const data = d3
.range(-40, 50, 10)
.map(d => ({ value: d, abd_trend: d < 0 ? d + 1 : d, abd_ppy_nonzero: d ? 1 : 0 }))
.filter(d => d.value)

const svg = d3.create("svg")
.attr("font-family", franklinLight)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

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

const title = g.append("g")
.attr("transform", `translate(0, -6)`);
title.append("text")
.attr("font-size", 16)
.attr("text-anchor", "middle")
.attr("x", width / 2)
.text("Change, 2012-2022");

const tick = g.selectAll(".tick")
.data(data)
.join("g")
.attr("class", "tick")
.attr("transform", (d, i) => `translate(${i / (data.length) * width})`)

tick.append("rect")
.attr("fill", color)
.attr("height", height)
.attr("width", width / data.length);

tick.filter((d, i, e) => i && i < e.length - 1)
.append("text")
.attr("font-size", 14)
.attr("text-anchor", "middle")
.attr("x", d => d.value < 0 ? 0 : width / data.length * (d.value ? 1 : 0.5))
.attr("y", height + 14)
.text((d, i) => d.value ? `${d.value > 0 ? "+" : "-"}${Math.abs(d.value)}${i ? "" : "%"}` : "Uncertain")

return svg.node();
}
Insert cell
map = () => {
const wrapper = d3.create("div")
.style("width", `${mapwidth}px`)
.style("height", `${mapheight}px`)
.style("position", "relative");

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

const canvas = wrapper.append("canvas")
.style("position", "absolute");
canvas.node().width = mapwidth;
canvas.node().height = mapheight;
const context = canvas.node().getContext("2d");
const path = d3.geoPath(projection, context);

// Draw the basemap
context.beginPath();
path(statesOuter);
context.fillStyle = scheme.rangeOuter;
context.fill();

// Clip the rest
context.beginPath();
path(statesOuter);
context.clip();

// Draw the range
range.features.forEach(feature => {
context.beginPath();
path(feature);
context.fillStyle = scheme.rangeInner;
context.fill();
});

context.beginPath();
path(statesInner);
context.strokeStyle = scheme.statesInner;
context.lineJoin = "round";
context.lineWidth = 0.5;
context.stroke();

data.forEach(d => {
context.beginPath();
path(square(d));
context.fillStyle = color(d);
context.fill();
});

path.context(null);
const svg = wrapper.append("svg")
.style("position", "absolute")
.attr("width", mapwidth)
.attr("height", mapheight);

svg.append("path")
.datum(statesOuter)
.attr("d", path)
.attr("fill", "none")
.attr("stroke", scheme.statesOuter)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 0.5);
const citiesG = svg.selectAll(".city")
.data(cities)
.join("g")
.attr("class", d => `city ${d.pos} ${toSlugCase(d.name)}`)
.attr("transform", d => `translate(${projection([d.lon, d.lat])})`);

citiesG.append("circle")
.attr("class", "circle-bg")
.attr("r", 3);
citiesG.append("circle")
.attr("r", 3);

citiesG.append("text")
.attr("class", "bg bg-0")
.text(d => d.name);

citiesG.append("text")
.attr("class", "bg bg-1")
.text(d => d.name);

citiesG.append("text")
.attr("class", "fg")
.text(d => d.name);
return wrapper.node();
}
Insert cell
Insert cell
css = `
.city circle {
fill: none;
stroke: black;
}

.city.ne text {
transform: translate(4px, -4px);
}
.city.se text {
transform: translate(4px, 13px);
}
.city.e text {
transform: translate(6px, 4.5px);
}

.city.boston {
display: ${width <= 480 ? "none" : "block"};
}
.city.boston text {
text-anchor: end;
transform: translate(-4px, -4px);
}

.city.chicago text {
text-anchor: start;
transform: translate(4px, 13px);
}

.city.miami text {
fill: black;
}

.city.miami text.bg {
fill: white;
stroke: white;
}

.city.minneapolis {
display: ${width <= 480 ? "none" : "block"};
}

.city.new-york text {
text-anchor: end;
transform: translate(-4px, -4px);
}

.city.philadelphia {
display: none;
}

.city.seattle text {
transform: translate(4px, ${width <= 400 ? "13px" : "-4px"});
}

.city.st-louis {
display: none;
}

.city.washington-dc text {
text-anchor: ${width <= 600 ? "middle" : "end"};
transform: ${width <= 600 ? "translate(0, 16px)" : "translate(-4px, 13px)"};
}

.city circle {
fill: none;
stroke: black;
}

.city circle.circle-bg {
stroke: white;
stroke-opacity: 0.8;
stroke-width: 3px;
}

.city text {
fill: black;
font-size: ${width <= 480 ? "14px" : "16px"};
font-family: ${franklinLight};
}

.city text.bg {
fill: white;
stroke: white;
}

.city text.bg-0 {
stroke-width: 4px;
stroke-opacity: 0.4;
}

.city text.bg-1 {
stroke-width: 2px;
stroke-opacity: 0.8;
}
`
Insert cell
scheme = ({
rangeInner: "#fff",
rangeOuter: "#f5f5f5",
statesInner: "#d5d5d5",
statesOuter: "#aaaaaa",
uncertain: "#f1e7da",
})
Insert cell
colors = ["#630700", "#9d3c24", "#d0714f", "#ffab85", "#85c7f9", "#4994cc", "#1d629b", "#003466"]
Insert cell
Insert cell
projection = d3.geoAlbers()
.fitSize([mapwidth, mapheight], statesOuter)
Insert cell
path = d3.geoPath(projection)
Insert cell
square = geoSquare()
.center(d => [d.longitude, d.latitude])
.area(d => Math.pow(radius(d.abd) * 2, 2));
Insert cell
cities = FileAttachment("cities.json").json()
Insert cell
statesInner = topojson.mesh(statesTopo, statesTopo.objects.ne_50m_admin_1_states_provinces_lakes, (a, b) => a !== b)
Insert cell
statesOuter = topojson.mesh(statesTopo, statesTopo.objects.ne_50m_admin_1_states_provinces_lakes, (a, b) => a === b)
Insert cell
statesTopo = FileAttachment("conusTopo.json").json()
Insert cell
usOuter = FileAttachment("conusOutline.geojson").json()
Insert cell
Insert cell
mapwidth = Math.min(width, 1000)
Insert cell
mapheight = mapwidth * 0.631
Insert cell
Insert cell
color = d => {
if (d.abd_trend <= -30) return colors[0];
else if (d.abd_trend > -30 && d.abd_trend <= -20) return colors[1];
else if (d.abd_trend > -20 && d.abd_trend <= -10) return colors[2];
else if (d.abd_trend > -10 && d.abd_trend <= 0) return colors[3];
else if (d.abd_trend > 0 && d.abd_trend <= 10) return colors[4];
else if (d.abd_trend > 10 && d.abd_trend <= 20) return colors[5];
else if (d.abd_trend > 20 && d.abd_trend <= 30) return colors[6];
else if (d.abd_trend > 30) return colors[7];
}
Insert cell
radius = d3.scaleSqrt([0, d3.max(data, d => d.abd)], [0, 32 / 2]) // each grid cell is 27km x 27km, and we let big ones overlap a little
Insert cell
Insert cell
// This CSV file was converted from a GPKG file. You can get your own API to download files at eBird.
data = (await FileAttachment("amecro_breeding_ebird-trends_2022.csv")
.csv({ typed: true }))
.filter(d => {
// If either the centroid or bound points are in the U.S., keep the cell
// Circumference of Earth: https://en.wikipedia.org/wiki/Earth%27s_circumference
// Latitude to longitude: https://stackoverflow.com/questions/13861616/drawing-a-square-around-a-lat-long-point
// Each cell side is 27km
const sideDegLat = (27 * 0.5) / 111.1329527778; // circumference of Earth in km / 360 (degrees)
const sideDegLon = sideDegLat / Math.cos(d.latitude / 180 * Math.PI);
const nw = [
d.longitude - sideDegLon,
d.latitude - sideDegLat
]
const se = [
d.longitude + sideDegLon,
d.latitude + sideDegLat
];
return d3.geoContains(usOuter, nw) || d3.geoContains(usOuter, se) || d3.geoContains(usOuter, [d.longitude, d.latitude])
});
Insert cell
seasons = d3.groups(data, d => d.season).map(d => d[0])
Insert cell
// This GeoJSON file was converted from a GPKG file. You can get your own API to download files at eBird.
range = {
const json = await FileAttachment("amecro_range_2022.json").json();
// Only use the season of the trends data
return {
type: "FeatureCollection",
features: json.features.filter(({properties}) => seasons.includes(properties.season))
}
}
Insert cell
Insert cell
// Function to convert a string to slug case
function toSlugCase(x) {
return x.toString().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 { franklinLight } from "@climatelab/fonts@44"
Insert cell
import { geoSquare } from "@climatelab/geosquare@267"
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