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(`${d["Reference area"]}
${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); }); });