Public
Edited
May 2
Fork of Simple D3
Insert cell
Insert cell
data = await FileAttachment("mean_SD_by_country_by_month.csv").csv()
Insert cell
Insert cell
parsedData = data.map(d => ({
country: d.country,
month: d.Month.padStart(2, "0"),
mean: +d["mean_SD"]
}))
Insert cell
chart = {
const margin = {top: 70, right: 100, bottom: 90, left: 120};
const width = 1100;
const height = 600;

// Prepare data
const formatMonth = d3.timeFormat("%b %Y");
const parseMonth = d3.timeParse("%Y-%m-%d");

const data = parsedData.map(d => ({
...d,
fullDate: parseMonth(d.month),
label: formatMonth(parseMonth(d.month))
}));

const months = [...new Set(data.map(d => d.label))];
const countries = [...new Set(data.map(d => d.country))];

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const x = d3.scaleBand()
.domain(months)
.range([margin.left, width - margin.right])
.padding(0.05);

const y = d3.scaleBand()
.domain(countries)
.range([margin.top, height - margin.bottom])
.padding(0.05);

const color = d3.scaleSequential()
.domain(d3.extent(data, d => d.mean))
.interpolator(d3.interpolateBlues);

// Axes
const xAxis = d3.axisBottom(x)
.tickValues(months.filter((_, i) => i % 12 === 0)) // 1 tick per year for each Jan
.tickFormat(d => d);

svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(xAxis)
.selectAll("text")
.attr("transform", "rotate(90)")
.attr("x", 8)
.attr("y", -6)
.style("text-anchor", "start")
.style("font-size", "9px");

svg.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.selectAll("text")
.style("font-size", "11px");

const cells = svg.selectAll("rect.cell")
.data(data)
.join("rect")
.attr("class", "cell")
.attr("x", d => x(d.label))
.attr("y", d => y(d.country))
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.attr("fill", d => color(d.mean));

// Tooltip div
const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("background", "white")
.style("padding", "6px 10px")
.style("border", "1px solid #999")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("opacity", 0);
// Interactivity
cells
.on("mouseover", (event, d) => {
tooltip
.style("opacity", 1)
.html(`<b>${d.country}</b><br>${d.label}<br>Mean: ${d.mean}`);
d3.select(event.currentTarget)
.attr("stroke", "#333")
.attr("stroke-width", 1.2);
})
.on("mousemove", (event) => {
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY - 30}px`);
})
.on("mouseout", (event) => {
tooltip.style("opacity", 0);
d3.select(event.currentTarget).attr("stroke", null);
});


// Color legend
const legendHeight = 440;
const legendWidth = 10;

const legendScale = d3.scaleLinear()
.domain(color.domain())
.range([legendHeight, 0]);

const legendAxis = d3.axisRight(legendScale).ticks(12);

const legend = svg.append("g")
.attr("transform", `translate(${width - margin.right + 30}, ${margin.top})`);

const legendBar = d3.range(legendHeight).map(i => ({
y: i,
color: color(legendScale.invert(i))
}));

legend.selectAll("rect")
.data(legendBar)
.join("rect")
.attr("x", 0)
.attr("y", d => d.y)
.attr("width", legendWidth)
.attr("height", 1)
.attr("fill", d => d.color);

legend.append("g")
.attr("transform", `translate(${legendWidth}, 0)`)
.call(legendAxis);

// Titles
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.text("Snow Depth Dynamics by Country");

svg.append("text")
.attr("x", width / 2)
.attr("y", 50)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", "gray")
.text("Data source: ERA5 – https://www.ecmwf.int/");

return svg.node();
}

Insert cell
Insert cell
parsedData2 = data.map(d => ({
country: d.country,
month: d.Month,
mean: +d["mean_SD"]
}))
Insert cell
chart2 = {
const margin = {top: 40, right: 20, bottom: 30, left: 40};
const chartWidth = 180;
const chartHeight = 120;
const numCols = 4;
const numRows = 4;

// Sorted list of countries in descending order by total mean SD
const countryMeanSum = d3.rollup(
parsedData2,
v => d3.sum(v, d => d.mean),
d => d.country
);
const countries = Array.from(countryMeanSum.entries())
.sort((a, b) => d3.descending(a[1], b[1]))
.map(d => d[0]);
const parseDate = d3.timeParse("%Y-%m-%d");
const year = d => new Date(d.month).getFullYear();

const data = parsedData2.map(d => ({
...d,
fullDate: parseDate(d.month)
})).filter(d => d.fullDate && !isNaN(d.mean));

const xExtent = d3.extent(data, d => d.fullDate);
const yExtent = d3.extent(data, d => d.mean);
const colorExtent = d3.extent(data, d => d.fullDate.getFullYear());

const colorScale = d3.scaleSequential()
.domain(colorExtent)
.interpolator(d3.interpolateBlues);

const totalWidth = numCols * (chartWidth + margin.left + margin.right) + 70;
const totalHeight = numRows * (chartHeight + margin.top + margin.bottom) + 50;
const svg = d3.create("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);


// Tooltip
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("background", "#fff")
.style("padding", "6px 10px")
.style("border", "1px solid #999")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("opacity", 0);

countries.forEach((country, i) => {
const row = Math.floor(i / numCols);
const col = i % numCols;

const g = svg.append("g")
.attr("transform", `translate(${col * (chartWidth + margin.left + margin.right) + margin.left},
${row * (chartHeight + margin.top + margin.bottom) + margin.top + 50})`);


const countryData = data.filter(d => d.country === country);

const x = d3.scaleLinear().domain([0, 11]).range([0, chartWidth]);
const y = d3.scaleLinear().domain([0, yExtent[1]]).range([chartHeight, 0]);

// Axes
g.append("g")
.attr("transform", `translate(5, ${chartHeight})`)
.call(d3.axisBottom(x)
.tickValues(d3.range(0, 12))
.tickFormat(i => d3.timeFormat("%b")(new Date(2000, i, 1))))
.selectAll("text")
.style("font-size", "8px");

g.append("g")
.call(d3.axisLeft(y).ticks(3))
.selectAll("text")
.style("font-size", "8px");

// Dots
g.selectAll("circle")
.data(countryData)
.join("circle")
.attr("cx", d => x(d.fullDate.getMonth()) + 5)
.attr("cy", d => y(d.mean))
.attr("r", 2.5)
.attr("fill", d => colorScale(d.fullDate.getFullYear()))
.on("mouseover", (event, d) => {
tooltip
.style("opacity", 1)
.html(`<b>${d.country}</b><br>${d3.timeFormat("%b %Y")(d.fullDate)}<br>Mean: ${d.mean}`);
})
.on("mousemove", event => {
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY - 30}px`);
})
.on("mouseout", () => {
tooltip.style("opacity", 0);
});

// Country title
g.append("text")
.attr("x", chartWidth / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.style("font-size", "10px")
.text(country);
});

// Color legend
const legendHeight = 700;
const legendWidth = 10;
const legendScale = d3.scaleLinear().domain(colorExtent).range([legendHeight, 0]);

const legendAxis = d3.axisRight(legendScale)
.tickFormat(d3.format("d"))
.ticks(6);

const legend = svg.append("g")
.attr("transform", `translate(${svg.attr("width") - 50}, 80)`);

const legendBar = d3.range(legendHeight).map(i => ({
y: i,
color: colorScale(legendScale.invert(i))
}));

legend.selectAll("rect")
.data(legendBar)
.join("rect")
.attr("x", 0)
.attr("y", d => d.y)
.attr("width", legendWidth)
.attr("height", 1)
.attr("fill", d => d.color);

legend.append("g")
.attr("transform", `translate(${legendWidth}, 0)`)
.call(legendAxis)
.selectAll("text")
.style("font-size", "9px");

// Titles
svg.append("text")
.attr("x", svg.attr("width") / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.text("Snow Depth Dynamics by Country");

svg.append("text")
.attr("x", svg.attr("width") / 2)
.attr("y", 40)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", "gray")
.text("Data source: ERA5 – https://www.ecmwf.int/");


return svg.node();
}
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