/// const ASPECT_RATIO = window.innerWidth / window.innerHeight; const CANVAS_WIDTH = window.innerWidth ?? 1000; // *0.9 for footer const CANVAS_HEIGHT = (ASPECT_RATIO > 1 ? window.innerHeight * 0.9 : CANVAS_WIDTH * ASPECT_RATIO * 0.9) ?? 500; const CHART_WIDTH = CANVAS_WIDTH * 0.8; const CHART_HEIGHT = CANVAS_HEIGHT * 0.8; const CHART_X_OFFSET = (CANVAS_WIDTH - CHART_WIDTH) / 2; const CHART_Y_OFFSET = (CANVAS_HEIGHT - CHART_HEIGHT) / 2; const STROKE_WIDTH = CHART_WIDTH / 1000; const STROKE_COLOR = "black"; const BASE_SIZE = CHART_Y_OFFSET * 0.3; const FONT_FAMILY = "quicksand, Sans-serif"; const MONTHS_NAME = { 1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December", }; const TEMP_HEXs = [ "#313695", "#4575b4", "#74add1", "#abd9e9", "#e0f3f8", "#ffffbf", "#fee090", "#fdae61", "#f46d43", "#d73027", "#a50026", ]; const TEMP_BUCKETS = TEMP_HEXs.length; const fetchData = async () => { return await (await fetch("./data.json")).json(); }; fetchData() .then((dataset) => { const BASE_TEMPERATURE = Number(dataset.baseTemperature); const MIN_TEMP = Number( ( BASE_TEMPERATURE + Number(d3.min(dataset.monthlyVariance, (d) => d.variance)) ).toFixed(3) ); const MAX_TEMP = Number( ( BASE_TEMPERATURE + Number(d3.max(dataset.monthlyVariance, (d) => d.variance)) ).toFixed(3) ); // 0.000000001 is to ensure that max temp do not yield tempBucketIndex as TEMP_BUCKET_SIZE const TEMP_BUCKET_SIZE = Number( ((MAX_TEMP - MIN_TEMP + 0.000000001) / TEMP_BUCKETS).toFixed(10) ); const getFill = (tempVariance) => { const tempBucketIndex = Math.trunc( (BASE_TEMPERATURE + tempVariance - MIN_TEMP) / TEMP_BUCKET_SIZE ); return TEMP_HEXs[tempBucketIndex]; }; const FIRST_YEAR = Number(d3.min(dataset.monthlyVariance, (d) => d.year)); const LAST_YEAR = Number(d3.max(dataset.monthlyVariance, (d) => d.year)); const FIRST_MONTH = Number(d3.min(dataset.monthlyVariance, (d) => d.month)); const LAST_MONTH = Number(d3.max(dataset.monthlyVariance, (d) => d.month)); const CELLS_WIDTH = CHART_WIDTH / (LAST_YEAR - FIRST_YEAR + 1); const CELLS_HEIGHT = CHART_HEIGHT / (LAST_MONTH - FIRST_MONTH + 1); const xPositionScale = d3 .scaleLinear() .domain([FIRST_YEAR, LAST_YEAR]) .range([0, CHART_WIDTH - CELLS_WIDTH]); const xAxisScale = d3 .scaleLinear() .domain([FIRST_YEAR - 0.5, LAST_YEAR + 0.5]) .range([0, CHART_WIDTH - CELLS_WIDTH]); const yPositionScale = d3 .scaleLinear() .domain([LAST_MONTH, FIRST_MONTH]) .range([CHART_HEIGHT - CELLS_HEIGHT, 0]); const yAxisScale = d3 .scaleLinear() .domain([LAST_MONTH + 0.5, FIRST_MONTH - 0.5]) .range([CHART_HEIGHT, 0]); const canvas = d3 .select("body") .append("svg") .attr("width", CANVAS_WIDTH) .attr("height", CANVAS_HEIGHT); canvas .append("text") .attr("id", "title") .attr("x", "50%") .attr("text-anchor", "middle") .attr("y", BASE_SIZE * 1.7) .attr("font-size", BASE_SIZE * 1.7) .text("Monthly Global Land-Surface Temperature"); canvas .append("text") .attr("id", "description") .attr("x", "50%") .attr("text-anchor", "middle") .attr("y", BASE_SIZE * 3) .attr("font-size", BASE_SIZE) .text("1753 - 2015: base temperature 8.66℃"); const mouseover = (e) => { const obj = JSON.parse(JSON.stringify(e.target.dataset)); tooltip .html( `${obj.year}, ${MONTHS_NAME[obj.month]}
Temperature: ${Number( obj.temp ).toFixed(3)}℃
Variance: ${( Number(obj.temp) - BASE_TEMPERATURE ).toFixed(3)}℃` ) .style("background-color", "#000000") .style("opacity", 1) .attr("data-year", obj.year); }; const mousemove = (e) => { tooltip.style("left", e.pageX + "px").style("top", e.pageY + "px"); }; const mouseleave = (d) => { tooltip.style("opacity", 0); }; canvas .selectAll("rect") .data(dataset.monthlyVariance) .enter() .append("rect") .attr("class", "cell") // test case expects months to be within 0-11 range and not 1-12 .attr("data-month", (d) => d.month - 1) .attr("data-year", (d) => d.year) .attr("data-temp", (d) => d.variance + BASE_TEMPERATURE) .attr("width", CELLS_WIDTH) .attr("height", CELLS_HEIGHT) .attr("x", (d) => CHART_X_OFFSET + xPositionScale(d.year)) .attr("y", (d) => CHART_Y_OFFSET + yPositionScale(d.month)) .attr("fill", (d) => getFill(d.variance)) .on("mouseover", mouseover) .on("mousemove", mousemove) .on("mouseleave", mouseleave); canvas .append("g") .attr("id", "x-axis") .style("font", BASE_SIZE + "px " + FONT_FAMILY) .attr( "transform", `translate(${CHART_X_OFFSET}, ${CHART_HEIGHT + CHART_Y_OFFSET})` ) .call(d3.axisBottom(xAxisScale).tickFormat(d3.format("d"))); canvas .append("text") .attr("id", "x-label") .attr("x", "50%") .attr("y", CHART_HEIGHT + CHART_Y_OFFSET * 1.9) .attr("text-anchor", "middle") .style("font-size", BASE_SIZE + "px") .text("Year"); canvas .append("g") .attr("id", "y-axis") .style("font", BASE_SIZE + "px " + FONT_FAMILY) .attr("transform", `translate(${CHART_X_OFFSET}, ${CHART_Y_OFFSET})`) .call( d3.axisLeft(yAxisScale).tickFormat((d) => MONTHS_NAME[d.toString()]) ); canvas .append("text") .attr("id", "y-label") .style("font-size", BASE_SIZE + "px") .attr("text-anchor", "end") .attr("y", CHART_X_OFFSET * 0.2) .attr("x", -CHART_Y_OFFSET - CHART_HEIGHT / 2) .attr("text-anchor", "middle") .attr("transform", "rotate(-90)") .text("Month"); const legend = canvas .append("g") .attr("id", "legend") .attr( "transform", `translate(${CHART_X_OFFSET + CHART_WIDTH + CHART_X_OFFSET * 0.2}, ${ CHART_Y_OFFSET + CHART_HEIGHT * 0.3 })` ); legend .selectAll("rect") .data(TEMP_HEXs.slice().reverse()) .enter() .append("rect") .attr("height", BASE_SIZE) .attr("width", BASE_SIZE) .attr("y", (d, i) => BASE_SIZE * i) .attr("fill", (d) => d); const legendScale = d3 .scaleLinear() .domain([MIN_TEMP, MAX_TEMP]) .range([TEMP_HEXs.length * BASE_SIZE, 0]); legend .append("g") .attr("id", "legend-scale-ticks") .style("font", BASE_SIZE * 0.8 + "px " + FONT_FAMILY) .attr("transform", `translate(${BASE_SIZE}, ${0})`) .call( d3 .axisRight(legendScale) .tickValues( d3.range(MIN_TEMP, MAX_TEMP + TEMP_BUCKET_SIZE, TEMP_BUCKET_SIZE) ) .tickFormat(d3.format(".1f")) ); canvas.selectAll("path").style("stroke-width", STROKE_WIDTH); canvas.selectAll("line").style("stroke-width", STROKE_WIDTH); let tooltip = d3 .select("body") .append("div") .attr("id", "tooltip") .attr("class", "tooltip") .attr("data-year", "") .style("opacity", 0) .style("position", "absolute") .style("padding", `${CHART_WIDTH / 200}px ${CHART_WIDTH / 100}px`) .style("margin", `0 ${CHART_WIDTH / 50}px`) .style("border-radius", `${CHART_WIDTH / 100}px`) .style("text-align", "center") .style("font-size", BASE_SIZE + "px"); d3.select("body") .append("footer") .style("bottom", BASE_SIZE * 0.33 + "px") .style("font-size", BASE_SIZE * 0.8 + "px") .append("p") .append("a") .attr("href", "https://radii.dev/freecodecamp-data-visualization/heatmap") .text(" Source Code & License"); }) .catch((e) => console.error("Error occurred!", e));