Public
Edited
Jun 6, 2024
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")
.text(d => `${d.name}, ${d.country}`);

const subtitle = chart.append("div")
.attr("class", "subtitle")
.text(d => `${+(d.pop / 1e6).toFixed(1)} million people`)
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 averageLine = g.append("line").attr("class", "average-line");
const circle = g.selectAll(".circle")
.data(d => d.data)
.join("circle")
.attr("class", "circle")
.attr("fill", d => color(d.danger))
.attr("stroke", d => d3.color(color(d.danger)).darker(0.3));

const averageText = g.append("g")
.attr("class", "average-text");
const averageLabel = averageText.append("text").attr("class", "average-label").text("Average");
const averageValue = averageText.append("text").attr("class", "average-value").text(d => `${Math.round(d.average)} days`);

return Object.assign(wrapper.node(), {
resize(ww) {
// Resize: Dimensions
const cols = ww <= 900 ? 2 : 3
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: 29, right: 17, top: 5, bottom: 24};
const chartMarginBottom = 24;
const basewidth = ww / cols - pad;
const width = basewidth - margin.left - margin.right;
const height = Math.max(200, basewidth * 9 / 16) - margin.top - margin.bottom;
const topOffset = 96;

// Resize: Scales
x.range([0, width]);
y.range([height, 0]);

// Resize: Axes

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

yaxisGenerator.tickSize(width + 10)
yaxis
.attr("transform", `translate(${width})`)
.call(yaxisGenerator);

const ytick = yaxis.select(".tick:last-of-type");
ytick.select("line").attr("x2", -yaxisGenerator.tickSize() + topOffset)
ytick.select("text").attr("x", -yaxisGenerator.tickSize() - yaxisGenerator.tickPadding() + topOffset)

// 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: Marks
averageText
.attr("transform", d => `translate(${[-margin.left, y(d.average)]})`);
averageLine
.attr("transform", d => `translate(${[-margin.left, y(d.average)]})`)
.attr("x2", margin.left + x(2024));
circle
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.danger))
.attr("r", r);
}
})
}
Insert cell
display.resize(width)
Insert cell
Insert cell
css = `
.chart {
display: inline-block;
}
.chart .title {
font-family: ${franklinLight};
font-size: 18px;
font-weight: bold;
margin-bottom: -2px;
}
.chart .subtitle {
font-family: ${franklinLight};
font-size: 16px;
margin-bottom: 16px;
}
.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;
}

.average-line {
shape-rendering: crispEdges;
stroke: #aaaaaa;
}

.average-text {
fill: #666666;
font-family: ${franklinLight};
font-size: 14px;
paint-order: stroke fill;
stroke: white;
stroke-width: 2px;
}

.average-text .average-label {
transform: translateY(-20px);
}
.average-text .average-value {
font-weight: bold;
transform: translateY(-4px);
}
`
Insert cell
palette = ["#ffdbcc", "#ff824d", "#cc3d00", "#4d1700"]
Insert cell
interpolator = interpolatePalette(palette)
Insert cell
color = d3.scaleSequential([1, 120], interpolator)
Insert cell
Insert cell
xaxisGenerator = d3.axisBottom(x)
.tickFormat(d => d)
.tickSize(10)
.tickValues(x.domain())
Insert cell
yaxisGenerator = d3.axisLeft(y)
.tickFormat((d, i, e) => `${d}${i === e.length - 1 ? " dangerous days" : ""}`)
.tickValues(y.domain())
Insert cell
Insert cell
x = d3.scaleLinear()
.domain([1979, 2024])
Insert cell
y = d3.scaleLinear()
.domain([0, 90])
Insert cell
Insert cell
history = FileAttachment("history@1.json").json()
Insert cell
cityCount = history.filter(d => d.pop >= 1e6 && d.average >= 1)
Insert cell
data = ["Lagos, Nigeria", "Bangkok, Thailand", "Mumbai, India", "Ho Chi Minh City, Vietnam", "Luanda, Angola", "Karachi, Pakistan"]
.map(name => history.find(f => `${f.name}, ${f.country}` === name))
.sort((a, b) => d3.descending(a.this_year, b.this_year))
Insert cell
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
Insert cell
import { interpolatePalette } from "@climatelab/roll-your-own-color-palette-interpolator@346";
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