viewof heatmap = {
const recentYears = processedData.years.slice(-10);
const months = processedData.months;
const margin = { top: 30, right: 20, bottom: 30, left: 80 };
const cellPadding = 2;
const cellSize = 50;
const width = recentYears.length * (cellSize + cellPadding) + margin.left + margin.right;
const height = months.length * (cellSize + cellPadding) + margin.top + margin.bottom;
const avgTemperatures = d3.rollup(
processedData.data,
v => d3.mean(v, d => (+d.max_temperature + +d.min_temperature) / 2),
d => d.date.split("-")[0],
d => d.date.split("-")[1]
);
const allAvgTemps = Array.from(avgTemperatures.values()).flatMap(monthMap =>
Array.from(monthMap.values())
);
const globalMinAvgTemp = d3.min(allAvgTemps);
const globalMaxAvgTemp = d3.max(allAvgTemps);
const allDailyTemps = processedData.data.map(d => ({
maxTemp: +d.max_temperature,
minTemp: +d.min_temperature
}));
const globalMinTemp = d3.min(allDailyTemps, d => d.minTemp);
const globalMaxTemp = d3.max(allDailyTemps, d => d.maxTemp);
const colorScale = d3.scaleSequential(d3.interpolateYlOrRd).domain([globalMinAvgTemp, globalMaxAvgTemp]);
// Create SVG container
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
// Create scales for x (years) and y (months)
const xScale = d3.scaleBand()
.domain(recentYears)
.range([margin.left, width - margin.right])
.padding(0.1);
const yScale = d3.scaleBand()
.domain(months)
.range([margin.top, height - margin.bottom])
.padding(0.1);
// Adding x-axis (years)
svg.append("g")
.attr("transform", `translate(0, ${margin.top - cellPadding})`)
.call(d3.axisTop(xScale).tickFormat(d => d))
.selectAll("text")
.style("text-anchor", "middle")
.style("font-size", "10px"); // Adjust font size for smaller axes
// Adding y-axis (months)
svg.append("g")
.attr("transform", `translate(${margin.left - cellPadding},0)`)
.call(d3.axisLeft(yScale).tickFormat(d => d3.timeFormat("%B")(new Date(2000, +d - 1))))
.selectAll("text")
.style("text-anchor", "end")
.style("font-size", "10px"); // Adjust font size for smaller axes
// Creating tooltip element -- similar to Level 1, here the tooltip is intended to provide more info of that particular cell, when a user hovers over that cell.
const tooltip = html`<div style="
position: absolute;
background-color: white;
border: solid 1px #ccc;
border-radius: 5px;
padding: 10px;
font-family: sans-serif;
font-size: 12px;
pointer-events: none;
display: none;">
</div>`;
document.body.appendChild(tooltip);
// Draw heatmap cells with line charts and tooltips
svg.append("g")
.selectAll(".cell")
.data(recentYears.flatMap(year =>
months.map(month => {
const avgTemp = avgTemperatures.get(year)?.get(month);
const dailyTemps = processedData.data.filter(d => d.date.startsWith(`${year}-${month}`)) || [];
return avgTemp !== null && avgTemp !== undefined
? { year, month, avgTemp, dailyTemps } // Include only valid data
: null; // Exclude invalid data
}).filter(d => d !== null) // Filter out null values
))
.join("g")
.attr("class", "cell")
.attr("transform", d => `translate(${xScale(d.year)}, ${yScale(d.month)})`)
.each(function(d) {
const avgTemp = d.avgTemp;
const dailyTemps = d.dailyTemps.map(t => ({
day: +t.date.split("-")[2],
maxTemp: +t.max_temperature,
minTemp: +t.min_temperature
}));
// Base rectangle for heatmap color coding based on average temperature (global scaling)
d3.select(this).append("rect")
.attr("width", xScale.bandwidth())
.attr("height", yScale.bandwidth())
.attr("fill", avgTemp !== undefined ? colorScale(avgTemp) : "#ccc")
.on("mouseover", (event) => {
tooltip.style.display = "block";
tooltip.style.left = `${event.pageX + 10}px`;
tooltip.style.top = `${event.pageY}px`;
tooltip.innerHTML = `
<strong>Max Temp:</strong> ${d3.max(dailyTemps, t => t.maxTemp)?.toFixed(2)}°C<br>
<strong>Min Temp:</strong> ${d3.min(dailyTemps, t => t.minTemp)?.toFixed(2)}°C
`;
})
.on("mouseout", () => {
tooltip.style.display = "none";
});
if (dailyTemps.length === 0) return; // Skip empty cells
// Scales for mini line chart within each cell using global temperature range
const dayScale = d3.scaleLinear()
.domain([1, dailyTemps.length])
.range([0, xScale.bandwidth()]);
const tempScaleGlobal = d3.scaleLinear()
.domain([globalMinTemp, globalMaxTemp]) // Global temperature range
.range([yScale.bandwidth(), 0]);
// Line generators for max and min temperatures
const lineMaxTemp = d3.line()
.x(t => dayScale(t.day))
.y(t => tempScaleGlobal(t.maxTemp));
const lineMinTemp = d3.line()
.x(t => dayScale(t.day))
.y(t => tempScaleGlobal(t.minTemp));
// Overlay mini line chart
const cellSvg = d3.select(this).append("svg")
.attr("width", xScale.bandwidth())
.attr("height", yScale.bandwidth());
// Draw max temperature line
cellSvg.append("path")
.datum(dailyTemps)
.attr("d", lineMaxTemp)
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", "1"); // Reduced stroke width
// Draw min temperature line
cellSvg.append("path")
.datum(dailyTemps)
.attr("d", lineMinTemp)
.attr("fill", "none")
.attr("stroke", "blue")
.attr("stroke-width", "1"); // Reduced stroke width
});
return svg.node();
}