Unlisted
Edited
Mar 19
Insert cell
Insert cell
d3 = require("d3@7") //Import D3.js package
Insert cell
data = FileAttachment("temperature_daily.csv").csv() //CSV file is uploaded in Observable as an attachment and passed into dataframe
Insert cell
// Here we process the data using multiple transformations
// we use the d3.rollup() to aggregate the data and calculate maximum and minimum temperatures per year and month
processedData = {
const aggregated = d3.rollup(
data,
v => ({
maxTemp: d3.max(v, d => +d.max_temperature),
minTemp: d3.min(v, d => +d.min_temperature)
}),
d => d.date.split("-")[0], // Year
d => d.date.split("-")[1] // Month
);

return {
years: Array.from(aggregated.keys()),
months: ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"],
aggregated,
data // Include original daily temperature records here - we need this for the line graphs which represent daily max and min temperatures
};
}
Insert cell
// Here, we create the heatmap (as Level 1) but with a different scaling

viewof heatmap = {
const recentYears = processedData.years.slice(-10); // Filter last 10 years of data
const months = processedData.months;

const margin = { top: 30, right: 20, bottom: 30, left: 80 };
const cellPadding = 2; // Space between cells
const cellSize = 50;
const width = recentYears.length * (cellSize + cellPadding) + margin.left + margin.right;
const height = months.length * (cellSize + cellPadding) + margin.top + margin.bottom;

// Calculating average temperatures for each year and month
const avgTemperatures = d3.rollup(
processedData.data,
v => d3.mean(v, d => (+d.max_temperature + +d.min_temperature) / 2),
d => d.date.split("-")[0], // Year
d => d.date.split("-")[1] // Month
);

// Calculating global minimum and maximum average temperatures -- this is done to decide the color for the heatmap cells
const allAvgTemps = Array.from(avgTemperatures.values()).flatMap(monthMap =>
Array.from(monthMap.values())
);
const globalMinAvgTemp = d3.min(allAvgTemps);
const globalMaxAvgTemp = d3.max(allAvgTemps);

// Calculate global minimum and maximum daily temperatures -- this is done to scale the line graphs inside each cell
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);

// Color scale for average temperatures (global)
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();
}

Insert cell
legend = {
const legendWidth = 70; // Width of the gradient bar
const legendHeight = 400; // Height of the gradient bar
const gradientHeight = 250; // Height of the actual gradient
const minTemp = 0; // Minimum temperature
const maxTemp = 40; // Maximum temperature

// Create an SVG container for the legend
const svg = d3.create("svg")
.attr("width", legendWidth + 300) // Add space for labels
.attr("height", legendHeight);

// Defining linear gradient for the legend
const defs = svg.append("defs");
const gradientId = "legendGradient";

const linearGradient = defs.append("linearGradient")
.attr("id", gradientId)
.attr("x1", "0%")
.attr("x2", "0%")
.attr("y1", "0%")
.attr("y2", "100%");

linearGradient.selectAll("stop")
.data(d3.range(0, 1.01, 0.1).map(d => ({
offset: `${d * 100}%`,
color: d3.interpolateYlOrRd(d)
})))
.enter()
.append("stop")
.attr("offset", d => d.offset)
.attr("stop-color", d => d.color);

// Drawing the gradient bar
svg.append("rect")
.attr("x", legendWidth / 4)
.attr("y", (legendHeight - gradientHeight) / 2)
.attr("width", legendWidth / 2)
.attr("height", gradientHeight)
.style("fill", `url(#${gradientId})`);

// Adding labels for min and max temperature values
svg.append("text")
.attr("x", legendWidth / 4 + legendWidth / 2 + 10) // Position to the right of the gradient
.attr("y", (legendHeight - gradientHeight) / 2 + gradientHeight)
.style("font-family", "sans-serif")
.style("font-size", "12px")
.style("text-anchor", "start")
.text(`${maxTemp}° Celsius`);

svg.append("text")
.attr("x", legendWidth / 4 + legendWidth / 2 + 10) // Position to the right of the gradient
.attr("y", (legendHeight - gradientHeight) / 2)
.style("font-family", "sans-serif")
.style("font-size", "12px")
.style("text-anchor", "start")
.text(`${minTemp}° Celsius`);

// Adding title for the legend
svg.append("text")
.attr("x", legendWidth / 4 + legendWidth / 2 + 10)
.attr("y", (legendHeight - gradientHeight) / 2 - 20)
.style("font-family", "sans-serif")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("text-anchor", "start")
.text(`Temperature Scale`);

// Adding line graph information
svg.append("line") // Line for max temperature
.attr("x1", legendWidth / 4 + legendWidth / 2 + 10)
.attr("x2", legendWidth / 4 + legendWidth / 2 + 30)
.attr("y1", (legendHeight - gradientHeight) / 2 + gradientHeight + 30)
.attr("y2", (legendHeight - gradientHeight) / 2 + gradientHeight + 30)
.style("stroke-width", "2px")
.style("stroke", "red");

svg.append("text") // Label for max temperature line
.attr("x", legendWidth / 4 + legendWidth / 2 + 35)
.attr("y", (legendHeight - gradientHeight) / 2 + gradientHeight + 32)
.style("font-family", "sans-serif")
.style("font-size", "12px")
.style("alignment-baseline", "middle")
.text(`Max Temperature`);

svg.append("line") // Line for min temperature
.attr("x1", legendWidth / 4 + legendWidth / 2 + 10)
.attr("x2", legendWidth / 4 + legendWidth / 2 + 30)
.attr("y1", (legendHeight - gradientHeight) / 2 + gradientHeight + 50)
.attr("y2", (legendHeight - gradientHeight) / 2 + gradientHeight + 50)
.style("stroke-width", "2px")
.style("stroke", "blue");

svg.append("text") // Label for min temperature line
.attr("x", legendWidth / 4 + legendWidth / 2 + 35)
.attr("y", (legendHeight - gradientHeight) / 2 + gradientHeight + 52)
.style("font-family", "sans-serif")
.style("font-size", "12px")
.style("alignment-baseline", "middle")
.text(`Min Temperature`);

return svg.node();
}

Insert cell
finalOutput = {
const container = html`<div style="display: flex; flex-direction: row; align-items: center; margin: 20px 0;"></div>`;
// Insert the heatmap
container.appendChild(viewof heatmap);
// Append the heatmap (with legend) in the same container.
container.appendChild(legend);
return container;
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more