273 lines
8.0 KiB
JavaScript
273 lines
8.0 KiB
JavaScript
|
/// <reference path="./d3.js" />
|
||
|
|
||
|
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]}<br/>Temperature: ${Number(
|
||
|
obj.temp
|
||
|
).toFixed(3)}℃<br/>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));
|