Public
Edited
Oct 24, 2024
1 fork
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = () => {
const wrapper = d3.create("div");
wrapper.append("style").html(css);
// Scaffold
const chart = wrapper.selectAll(".chart")
.data(data)
.join("div")
.attr("class", "chart");
const title = chart.append("div")
.attr("class", "title")
.html(d => titles[d.prop]);
const svg = chart.append("svg");
const g = svg.append("g");

// Axes
const xaxis = g.append("g")
.attr("class", "axis x-axis");

const yaxis = g.append("g")
.attr("class", "axis y-axis");

// Marks
const bar = g.append("g")
.attr("class", "bars")
.selectAll(".bar")
.data(d => d.data)
.join("rect")
.attr("class", "bar");

const trend = g.append("polyline")
.attr("class", "trend");

// Labels
const trendLabel = g.append("text")
.attr("class", "trend-label")
.attr("dy", -4)
.text(d => d.prop === "normalized" ? "No trend" : "Increase");

const anno = g.append("g")
.attr("class", "anno")
.attr("text-anchor", d => annoData[d.prop].textAnchor);

const annoLeader = anno.append("polyline")
.attr("class", "anno-leader");

const annoYear = anno.append("text")
.attr("class", "anno-year")
.attr("y", 5)
.text(d => annoData[d.prop].year)
const annoText = anno.append("text")
.attr("class", "anno-text")
.attr("y", 23);
return Object.assign(wrapper.node(), {
resize(ww) {
// Resize: Dimensions
const cols = ww <= 560 ? 1 : 2;
const rows = data.length / cols;
const padInner = 24;
const padTotal = padInner * (cols - 1);
const pad = padTotal / cols;
const r = ww <= 400 ? 3 : 4;
const margin = { left: 51, right: 14, top: 7, bottom: 23 };
const chartMarginBottom = 24;
const basewidth = ww / cols - pad;
const width = basewidth - margin.left - margin.right;
const height = Math.max(300, basewidth * 9 / 16) - margin.top - margin.bottom;

// Resize: Scales
x.range([0, width]);
props.forEach(prop => {
Y[prop].range([height, 0]);
});

// Resize: Scaffold
chart
.style("margin-bottom", (_, i) => 1 + Math.floor(i / cols) === rows ? "0px" : `${chartMarginBottom}px`)
.style("margin-left", (_, i) => i % cols === 0 ? "0px" : `${padInner / 2}px`)
.style("margin-right", (_, i) => i % cols === (cols - 1) ? "0px" : `${padInner / 2}px`)
.style("width", `calc(${100 / cols}% - ${pad}px)`)
svg
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

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

// Resize: Axes
xaxis
.attr("transform", `translate(0, ${height})`)
.call(g => xAxisGenerator(g, height));

yaxis
.attr("transform", `translate(${width})`)
.each((d, i, e) => {
d3.select(e[i]).call(g => yAxisGenerator(g, d.prop, width, Y))
});

// Resize: Marks
bar
.attr("height", d => height - Y[d.prop](d.value))
.attr("width", x.bandwidth())
.attr("x", d => x(d.year))
.attr("y", d => Y[d.prop](d.value));

trend
.attr("points", d => {
return regressionToLine(d);
});
// Resize: Labels
const i = {
"inflation": 0.55,
"normalized": 0.67
}
trendLabel
.attr("transform", d => {
const l = regressionToLine(d);
const a = lineAngle(l);
const p = lineInterpolate(l)(i[d.prop]);
return `translate(${p}) rotate(${a})`
});
anno
.attr("transform", d => {
const { year } = annoData[d.prop]
const { value } = d.data.find(f => f.year === year);
return `translate(${x(year) + x.bandwidth() / 2}, ${Y[d.prop](value)})`;
});
annoLeader
.attr("points", d => {
const offset = annoData[d.prop].offset(ww);
if (offset < 0) {
return [[-x.bandwidth() / 2 + offset, 0], [x.bandwidth() / 2, 0]]
}
else {
return [[-x.bandwidth() / 2, 0], [x.bandwidth() / 2 + offset, 0]]
}
})
annoYear
.attr("x", d => annoOffset(d));
annoText
.attr("transform", d => `translate(${annoOffset(d)})`)
.html(d => {
return annoData[d.prop].html(ww)
})
.selectAll("tspan")
.attr("x", 0)
.attr("dy", (d, i) => i === 0 ? 0 : 16)
}
})
}
Insert cell
display.resize(width)
Insert cell
Insert cell
css = `
.chart {
display: inline-block;
}
.chart .title {
font-family: ${franklinLight};
margin-bottom: 8px;
}
.axis .domain {
display: none;
}
.axis .tick text {
fill: #666666;
font-family: ${franklinLight};
font-size: 14px;
}
.axis.x-axis .tick line {
stroke: #aaaaaa;
}
.axis.y-axis .tick line {
stroke: #d5d5d5;
}
.anno, .trend-label {
font-family: ${franklinLight};
}
.anno {
text {
fill: #2a2a2a;
font-size: 14px;
paint-order: stroke fill;
stroke: white;
stroke-linejoin: round;
stroke-width: 8px;
&.anno-year {
font-weight: bold;
}
}

polyline {
stroke: #2a2a2a;
}
}

.bar {
fill: #E69D67;
stroke: #E69D67;
}

.trend {
stroke: black;
}

.trend-label {
font-weight: bold;
paint-order: stroke fill;
stroke: white;
stroke-linejoin: round;
stroke-opacity: 0.5;
stroke-width: 4px;
text-anchor: middle;
text-transform: uppercase;
}
`
Insert cell
titles = ({
"inflation": "Adjusted for <b>inflation</b>",
"normalized": "Adjusted for <b>inflation, population and wealth</b>"
})
Insert cell
annoData = ({
"inflation": {
year: 2005,
offset: ww => -8,
textAnchor: "end",
html: ww => {
if (ww <= 480) {
return "<tspan>Six storms, including</tspan><tspan>Katrina, Wilma and Rita,</tspan><tspan>caused $147B in damage.</tspan>"
}
if (ww <= 540) {
return "<tspan>Six storms, including</tspan><tspan>Katrina, Wilma and</tspan><tspan>Rita, caused $147B</tspan><tspan> in damage.</tspan>"
}
if (ww <= 560) {
return "<tspan>Six storms, including</tspan><tspan>Katrina, Wilma and Rita,</tspan><tspan>caused $147B</tspan><tspan> in damage.</tspan>"
}
else {
return "<tspan>Six storms, including</tspan><tspan>Katrina, Wilma and Rita,</tspan><tspan>caused $147B in damage.</tspan>"
}
}
},
"normalized": {
year: 1926,
offset: ww => 8,
textAnchor: "start",
html: ww => {
if (ww <= 480) {
return "<tspan>The Great Miami Hurricane</tspan><tspan>would have cost more than</tspan><tspan>$200B if it made landfall</tspan><tspan>in 2018.</tspan>"
}
if (ww <= 540) {
return "<tspan>The Great Miami</tspan><tspan>Hurricane would</tspan><tspan>have cost $236B</tspan><tspan>if it made</tspan><tspan>landfall in</span><tspan>2018.</stpan>"
}
if (ww <= 580) {
return "<tspan>The Great Miami</tspan><tspan>Hurricane would have</tspan><tspan>cost $236B if it made</tspan><tspan>landfall in 2018.</tspan>"
}
else {
return "<tspan>The Great Miami Hurricane</tspan><tspan>would have cost more than</tspan><tspan>$200B if it made landfall</tspan><tspan>in 2018.</tspan>"
}
}
}
})
Insert cell
Insert cell
xAxisGenerator = (g, height) => {
const tickValuesAll = [...d3.range(1900, 2020, 20), 2017];
const tickValuesSmall = [1900, 1950, 2017];
const generator = d3.axisBottom(x)
.tickSize(8)
.tickValues(width <= 350 ? tickValuesSmall : width <= 480 ? tickValuesAll : width <= 640 ? tickValuesSmall : tickValuesAll)

g
.attr("transform", `translate(0, ${height})`)
.call(generator);

return g;
}
Insert cell
yAxisGenerator = (g, prop, width, Y) => {
const tickValues = {
"inflation": d3.range(0, 150e9, 30e9),
"normalized": d3.range(0, 250e9, 50e9)
};

const generator = d3.axisLeft(Y[prop])
.tickFormat(d => d > 0 ? `$${d / 1e9}B` : "$0")
.tickSize(width + 8)
.tickValues(tickValues[prop]);

g
.attr("transform", `translate(${width})`)
.call(generator);

return g;
}
Insert cell
Insert cell
x = d3.scaleBand()
.domain(d3.range(start_year, end_year + 1))
Insert cell
Y = {
const Y = {};
props.forEach(prop => {
Y[prop] = d3.scaleLinear().domain([0, d3.max(data.find(f => f.prop === prop).data, d => d.value)]);
});
return Y;
}
Insert cell
Insert cell
Insert cell
import { data_hurricanes } from "@climatelab/data-analysis-normalized-us-hurricane-losses"
Insert cell
start_year = d3.min(data_hurricanes, d => d.year)
Insert cell
end_year = d3.max(data_hurricanes, d => d.year)
Insert cell
r = d3.regressionLinear().x(d => d.year).y(d => d.value);
Insert cell
props = ["inflation", "normalized"]
Insert cell
data = props.map(prop => {
const data = data_hurricanes.map(d => ({prop, year: d.year, value: d[prop]}))
const { a, b, predict } = r(data);
const sy = Math.max(start_year, Math.round(-b / a));
return {
prop,
data,
regression: [ [sy, predict(sy)], [end_year, predict(end_year)] ]
}
})
Insert cell
Insert cell
function annoOffset(d){
const offset = annoData[d.prop].offset(width);
if (offset < 0) {
return (offset - 4) - x.bandwidth() / 2;
}
else {
return (offset + 4) + x.bandwidth() / 2;
}
}
Insert cell
function regressionToLine(d){
return [
[x(d.regression[0][0]), Y[d.prop](d.regression[0][1])],
[x(d.regression[1][0]), Y[d.prop](d.regression[1][1])],
]
}
Insert cell
lineAngle = geometric.lineAngle
Insert cell
lineInterpolate = geometric.lineInterpolate
Insert cell
Insert cell
d3 = require("d3@7", "d3-regression@1")
Insert cell
geometric = require("geometric@2")
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
Insert cell
import { toc } from "@climatelab/toc@45"
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