chart2 = {
const margin = {top: 40, right: 20, bottom: 30, left: 40};
const chartWidth = 180;
const chartHeight = 120;
const numCols = 4;
const numRows = 4;
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();
}