Public
Edited
Oct 24, 2024
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = () => {
const wrapper = d3.create("div").attr("class", "chart-wrapper");

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

const titles = wrapper.append("div").attr("class", "chart-titles");
titles.append("div")
.attr("class", "chart-title")
.text("U.S. billion-dollar disasters, 1980-2023");

const legend = titles.append("div")
.attr("class", "chart-legend");

const legendItem = legend.selectAll(".legend-item")
.data(Object.keys(colors))
.join("div")
.attr("class", "legend-item");

legendItem.append("div")
.attr("class", "legend-swatch")
.style("background", d => colors[d]);

legendItem.append("div")
.attr("class", "legend-label")
.text(d => categoryLookup[d]);

// Scaffold
const chart = wrapper.append("div")
.attr("class", "chart");
const svg = chart.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]})`);

// Axes
g.append("g")
.attr("class", "axis y-axis")
.attr("transform", `translate(${chartwidth})`)
.call(yAxisGenerator0);

g.append("g")
.attr("class", "axis y-axis")
.attr("transform", `translate(${chartwidth})`)
.call(yAxisGenerator1);
g.append("g")
.attr("class", "axis y-axis")
.attr("transform", `translate(${chartwidth})`)
.call(yAxisGenerator2);

g.append("g")
.attr("class", "axis x-axis")
.attr("transform", `translate(0, ${chartheight})`)
.call(xAxisGenerator)
.select(".domain").raise();
// Marks
g.append("g")
.attr("class", "bars")
.selectAll(".bar")
.data(series)
.join("rect")
.attr("class", "bar")
.attr("fill", d => colors[d.category])
.attr("x", d => x(d.year))
.attr("y", d => y(d[1]))
.attr("width", d => x.bandwidth())
.attr("height", d => y(d[0]) - y(d[1]));

// Annotation
const at = width <= 600 ? [x(2023) -5 , y(28) + 54] : [x(2023) + x.bandwidth(), y(28)];
const anno = g.append("g")
.attr("class", "anno")
.attr("transform", `translate(${at})`);

anno.append("text")
.attr("class", "anno-title")
.attr("dx", 0)
.attr("y", -42)
.text("2023");

anno.append("text")
.attr("dx", 0)
.attr("y", -24)
.html(`<tspan>28 disasters</tspan><tspan x=0 dy=18>costing $94.8 billion</tspan>`);

// Source
wrapper.append("div")
.attr("class", "source")
.text("Source: National Oceanic and Atmospheric Administration");
return wrapper.node();
}
Insert cell
// See answer 1: https://www.ncei.noaa.gov/access/monitoring/dyk/billions-calculations
categoryLookup = ({
"Severe Storm": "Severe storms",
"Drought": "Droughts",
"Flooding": "Inland floods",
"Tropical Cyclone": "Hurricanes",
"Wildfire": "Wildfires",
"Freeze": "Crop freezes",
"Winter Storm": "Winter storms"
})
Insert cell
Insert cell
css = `
.chart-wrapper {
font-family: ${franklinLight}; font-size: 16px;
position: relative;
z-index: -1;
}

.chart-titles {
margin-bottom: -200px;

@media only screen and (max-width: 960px) {
margin-bottom: -60px;
}

@media only screen and (max-width: 700px) {
}

@media only screen and (max-width: 640px) {
margin-bottom: 0px;
}

.chart-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 8px;
}

.chart-legend {
max-width: 430px;

.legend-item, .legend-swatch, .legend-label {
display: inline-block;
}
.legend-item {
margin-right: 16px;

&:last-of-type {
margin-right: 0px;
}
}
.legend-swatch {
height: 11px;
margin-right: 4px;
width: 11px;
}
.legend-label {
font-size: 16px;
}
}
}

.chart {
svg {
overflow: visible;
}

.anno {
.anno-title {
font-weight: bold;
}

line {
stroke: #2a2a2a;
}
text {
paint-order: stroke fill;
stroke: white;
stroke-linejoin: round;
stroke-width: 8px;
text-anchor: end;
}
}

.axis {
.domain {
display: none;
}
.tick {
text {
font-family: ${franklinLight};
font-size: 14px;
}
}

.tick {
line {
shape-rendering: crispEdges;
stroke: #d5d5d5;
}

text {
fill: #666666;
}
}

&.x-axis {
.domain {
display: block;
stroke: #2a2a2a;
}
}
}
}

.source {
color: #666666;
font-size: 14px;

a {
color: #666666;
}
}
`
Insert cell
colors = ({
"Severe Storm": "#39577C",
"Tropical Cyclone": "#6E86AA",
"Flooding": "#98B2CE",
"Drought": "#BF6136",
"Winter Storm": "#E69D67",
"Wildfire": "#F7CF96",
"Freeze": "#F9EFDD"
})
Insert cell
Insert cell
xAxisGenerator = d3.axisBottom(x)
.tickSizeInner(8)
.tickSizeOuter(0)
.tickValues(d3.range(1980, 2030, 10))
Insert cell
yAxisGenerator0 = d3.axisLeft(y)
.tickSize(chartwidth + 8)
.tickValues([0, 5, 10])
Insert cell
yAxisGenerator1 = d3.axisLeft(y)
.tickSize(width <= 640 ? chartwidth + 8 : chartwidth - x(2008))
.tickValues([15, 20]);
Insert cell
yAxisGenerator2 = d3.axisLeft(y)
.tickSize(width <= 640 ? chartwidth + 8 : chartwidth - x(2021))
.tickValues([25]);
Insert cell
Insert cell
x = d3.scaleBand().domain(years).paddingInner(0.1).range([0, chartwidth])
Insert cell
y = d3.scaleLinear().domain([0, d3.max(series, d => d[1])]).range([chartheight, 0])
Insert cell
Insert cell
margin = ({ left: 27, right: 0, top: 0, bottom: 23 })
Insert cell
chartwidth = width - margin.left - margin.right
Insert cell
minheight = 300
Insert cell
maxheight = 500
Insert cell
chartheight = Math.max(Math.min(width * 9 / 16, maxheight), minheight) - margin.top - margin.bottom
Insert cell
Insert cell
// Load the CSV file as text, remove the information lines, and parse it
events = d3
.csvParse(
(await FileAttachment("events-US-1980-2024.csv").text())
.split("\n")
.slice(2)
.join("\n")
)
.map(parseRow) // See below for the function to parse the data
Insert cell
// An array of years to show
years = d3.range(1980, 2024) // exclude most recent year because it is incomplete
Insert cell
// Categories to show
categories = d3.groups(events, d => d.category).map(d => d[0])
Insert cell
// Group the data by year and category
data = d3
.cross(
years,
categories
)
.map(([year, category]) => {
const entries = events.filter(f => f.end_year === year && f.category === category);
return {
year,
category,
count: entries.length
}
})
Insert cell
// A configurable function to convert the data to a stack
stack = d3.stack()
.keys(d3.union(data.map(d => d.category)))
.order(d3.stackOrderDescending)
.value(([, group], key) => group.get(key).count)
Insert cell
// The stacked data
series = stack(d3.index(data, d => d.year, d => d.category))
.map(d => d.map(d0 => {
// Easier coloring
d0.category = d.key;

// In case you wanted to do tooltips or labeling
const v = d0.data[1].get(d0.category);
d0.count = v.count;
d0.year = v.year;
return d0;
}))
.flat(); // No need to nest these in the DOM
Insert cell
// Parse the input CSV rows
parseRow = (row) => {
const o = {};
o.name = row.Name.split(" (")[0];
o.category = row.Disaster;

const sd = row["Begin Date"];
const sy = sd.slice(0, 4);
const sm = sd.slice(4, 6);
o.start_date = `${sy}-${sm}-${sd.slice(6, 8)}`;

const ed = row["End Date"];
const ey = ed.slice(0, 4);
const em = ed.slice(4, 6);
o.end_date = `${ey}-${em}-${ed.slice(6, 8)}`;

o.start_year = +sy;
o.end_year = +ey;

o.start_month = +sm;
o.end_month = +em;
o.cost_adjusted = +row["CPI-Adjusted Cost"] * 1e6;
o.cost_unadjusted = +row["Unadjusted Cost"] * 1e6;

o.deaths = +row["Deaths"];

return o;
}
Insert cell
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
Insert cell
import { toc } from "@climatelab/toc@44"
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