COS30045/script/script.js
2025-04-26 08:16:15 +10:00

254 lines
No EOL
8.3 KiB
JavaScript

const svg = d3.select("svg"),
margin = {top: 80, right: 30, bottom: 150, left: 60},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
const x0 = d3.scaleBand().rangeRound([0, width]).paddingInner(0.1);
const x1 = d3.scaleBand().padding(0.05);
const y = d3.scaleLinear().rangeRound([height, 0]);
const educationLevels = [
"Pre-primary, primary and lower secondary education",
"Upper secondary and post-secondary non-tertiary, all programmes",
"Tertiary education"
];
const color = d3.scaleOrdinal()
.domain(educationLevels)
.range(["#4E79A7", "#F28E2B", "#59A14F"]);
const tooltip = d3.select("body").append("div").attr("class", "tooltip");
const chartTitle1 = svg.append("text")
.attr("x", +svg.attr("width") / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("font-weight", "bold");
const chartTitle2 = svg.append("text")
.attr("x", +svg.attr("width") / 2)
.attr("y", 55)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("fill", "#666");
const noDataText = g.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("fill", "#999")
.style("display", "none")
.text("No data available for selected filters");
// Dark mode toggle
const darkModeToggle = document.createElement('button');
darkModeToggle.textContent = 'Toggle Dark Mode';
Object.assign(darkModeToggle.style, {
margin: '5px',
padding: '8px 12px',
fontSize: '14px',
cursor: 'pointer',
border: 'none',
borderRadius: '4px',
backgroundColor: '#333',
color: '#fff'
});
document.querySelector('.controls').appendChild(darkModeToggle);
let darkMode = false;
darkModeToggle.addEventListener('click', () => {
darkMode = !darkMode;
document.body.style.backgroundColor = darkMode ? '#121212' : '#fff';
document.body.style.color = darkMode ? '#eee' : '#000';
darkModeToggle.style.backgroundColor = darkMode ? '#eee' : '#333';
darkModeToggle.style.color = darkMode ? '#000' : '#fff';
tooltip.style("background-color", darkMode ? "#222" : "#fff")
.style("color", darkMode ? "#eee" : "#000");
g.selectAll("text").style("fill", darkMode ? "#eee" : "#000");
});
d3.csv("PHSwithContinent.csv").then(function(data) {
const allData = data;
const continents = Array.from(new Set(data.map(d => d.CONTINENT))).sort();
continents.forEach(continent => {
d3.select("#continentSelect")
.append("option")
.attr("value", continent)
.text(continent);
});
educationLevels.forEach(level => {
d3.select("#incomeSelect")
.append("option")
.attr("value", level)
.text(level);
});
const legend = d3.select("#legend");
color.domain().forEach(d => {
const item = legend.append("div").attr("class", "legend-item");
item.append("div")
.attr("class", "legend-color")
.style("background-color", color(d));
item.append("span").text(d);
});
function updateChart() {
const selectedContinent = d3.select("#continentSelect").property("value");
const selectedSex = d3.select("#sexSelect").property("value");
const selectedEdu = d3.select("#incomeSelect").property("value");
// Two-line title
let title1 = "Self-Reported Health";
let title2 = "";
if (selectedContinent !== "All") title2 += `${selectedContinent}`;
if (selectedSex !== "All") title2 += (title2 ? ", " : "") + selectedSex;
if (selectedEdu !== "All") title2 += (title2 ? ", " : "") + selectedEdu;
chartTitle1.text(title1);
chartTitle2.text(title2);
let filteredData = allData.filter(d =>
(selectedContinent === "All" || d.CONTINENT === selectedContinent) &&
(selectedSex === "All" || d.Sex === selectedSex) &&
(selectedEdu === "All" || d["Socio-economic status"] === selectedEdu)
);
filteredData = filteredData.filter(d =>
educationLevels.includes(d["Socio-economic status"]) &&
!isNaN(parseFloat(d.OBS_VALUE))
);
const countries = Array.from(new Set(filteredData.map(d => d["Reference area"])));
x0.domain(countries);
x1.domain(educationLevels).rangeRound([0, x0.bandwidth()]);
y.domain([0, 100]);
g.selectAll("*").remove();
if (filteredData.length === 0) {
noDataText.style("display", null);
return;
} else {
noDataText.style("display", "none");
}
const groupedData = filteredData.reduce((acc, d) => {
const found = acc.find(v => v.Country === d["Reference area"]);
const value = parseFloat(d.OBS_VALUE);
const entry = { ...d, value };
if (found) found.values.push(entry);
else acc.push({ Country: d["Reference area"], values: [entry] });
return acc;
}, []);
const countryGroups = g.append("g")
.selectAll("g")
.data(groupedData)
.join("g")
.attr("transform", d => `translate(${x0(d.Country)},0)`)
.attr("class", "country-group");
const bars = countryGroups.selectAll("rect")
.data(d => d.values)
.join("rect")
.attr("x", d => x1(d["Socio-economic status"]))
.attr("width", x1.bandwidth())
.attr("fill", d => color(d["Socio-economic status"]))
.attr("y", height)
.attr("height", 0)
.attr("class", "bar")
.on("mouseover", function(event, d) {
tooltip.html(`<strong>${d["Reference area"]}</strong><br/>${d["Socio-economic status"]}: ${d.OBS_VALUE}%`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 40) + "px")
.transition().duration(300)
.style("opacity", 1)
.style("transform", "translateY(-10px)");
d3.select(this.parentNode).selectAll("rect").style("opacity", 1);
g.selectAll(".country-group").filter(gd => gd.Country !== d["Reference area"])
.selectAll("rect").style("opacity", 0.2);
})
.on("mouseout", function() {
tooltip.transition().duration(500)
.style("opacity", 0)
.style("transform", "translateY(0px)");
g.selectAll("rect").style("opacity", 0.8);
});
bars.transition()
.duration(800)
.ease(d3.easeCubicOut)
.attr("y", d => y(d.value))
.attr("height", d => height - y(d.value));
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x0))
.selectAll("text")
.attr("transform", "rotate(45)")
.style("text-anchor", "start");
g.append("g")
.call(d3.axisLeft(y).ticks(10));
// Annotate lowest
if (groupedData.length > 0) {
const flat = groupedData.flatMap(g => g.values);
const lowest = flat.reduce((min, d) => d.value < min.value ? d : min, flat[0]);
const barX = x0(lowest["Reference area"]) + x0.bandwidth() / 2;
const barY = y(lowest.value);
g.append("rect")
.attr("x", barX - 50)
.attr("y", barY - 30)
.attr("width", 100)
.attr("height", 18)
.attr("rx", 4)
.attr("fill", darkMode ? "#222" : "#fff")
.attr("opacity", 0.85);
g.append("text")
.attr("x", barX)
.attr("y", barY - 17)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.style("font-weight", "bold")
.style("fill", darkMode ? "#fff" : "red")
.text(`Lowest: ${lowest["Reference area"]}`);
}
}
updateChart();
d3.select("#loading").classed("hidden", true);
d3.selectAll("select").on("change", updateChart);
d3.select("#resetButton").on("click", function() {
d3.select("#continentSelect").property("value", "All");
d3.select("#sexSelect").property("value", "All");
d3.select("#incomeSelect").property("value", "All");
updateChart();
});
d3.select("#downloadButton").on("click", function() {
const c = d3.select("#continentSelect").property("value");
const s = d3.select("#sexSelect").property("value");
const e = d3.select("#incomeSelect").property("value");
const parts = [
"education_health",
c !== "All" ? c : null,
s !== "All" ? s : null,
e !== "All" ? e.replace(/[^a-zA-Z0-9]+/g, "_").toLowerCase() : null
].filter(Boolean);
const filename = parts.join("_") + ".png";
saveSvgAsPng.saveSvgAsPng(document.querySelector("svg"), filename);
});
});