Public
Edited
Oct 24, 2024
1 star
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");
return Object.assign(wrapper.node(), {
resize(ww) {
// Resize: Dimensions
const cols = ww <= 480 ? 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: 38, right: 14, top: 7, bottom: 23 }
const chartMarginBottom = 24;
const basewidth = ww / cols - pad;
const width = basewidth - margin.left - margin.right;
const maxHeight = ww <= 480 ? 100 : 250;
const height = Math.min(basewidth * 9 / 16, maxHeight) - 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));
}
})
}
Insert cell
display.resize(width)
Insert cell
Insert cell
css = `
.chart {
display: inline-block;
}
.chart .title {
font-family: ${franklinLight};
margin-bottom: 8px;
}
.chart svg {
overflow: visible;
}
.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;
}

.bar {
fill: #6E86AA;
stroke: #6E86AA;
}
`
Insert cell
titles = ({
"count": `<b>Hurricanes</b> making landfall`,
"count_major": `<b>Major hurricanes</b> making landfall`,
"population": `<b>Population</b> in coastal counties`,
"wealth": `<b>Wealth</b> in coastal counties`
})
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 = {
"count": [0, 3, 6],
"count_major": [0, 2, 4],
"population": [0, 30e6, 60e6],
"wealth": [0, 5e12, 10e12]
};

const tickFormat = {
"count": d => d,
"count_major": d => d,
"population": d => d > 0 ? `${d / 1e6}M` : "0",
"wealth": d => d > 0 ? `$${d / 1e12}T` : "$0"
}

const ts = {
"count": 27,
"count_major": 26,
"population": 8,
"wealth": 6
}
const generator = d3.axisLeft(Y[prop])
.tickFormat(tickFormat[prop])
.tickSize(width + ts[prop])
.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 domains = {
"count": [0, 6],
"count_major": [0, 4],
"population": [0, 60e6],
"wealth": [0, 10e12]
}
const Y = {};
props.forEach(prop => {
Y[prop] = d3.scaleLinear().domain(domains[prop]);
});
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 = ["count", "count_major", "population", "wealth"]
Insert cell
data = props.map(prop => {
const data = data_hurricanes.map(d => ({prop, year: d.year, value: d[prop]}))
return {
prop,
data
}
})
Insert cell
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