From 17f63a4fc8ec8d0543c97e7ac137fe6c83bdb57c Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Sun, 27 Apr 2025 08:25:45 +1000 Subject: [PATCH 1/7] added dot plot and heat map --- index.html | 11 +- script/script.js | 408 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 324 insertions(+), 95 deletions(-) diff --git a/index.html b/index.html index d94c17b..949d24f 100644 --- a/index.html +++ b/index.html @@ -28,13 +28,22 @@ + + + + +
Loading chart, please wait...
- +
diff --git a/script/script.js b/script/script.js index 1c72b72..f1c6e38 100644 --- a/script/script.js +++ b/script/script.js @@ -1,5 +1,5 @@ const svg = d3.select("svg"), - margin = {top: 80, right: 30, bottom: 150, left: 60}, + margin = {top: 80, right: 30, bottom: 150, left: 160}, 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})`); @@ -43,7 +43,7 @@ const noDataText = g.append("text") .style("display", "none") .text("No data available for selected filters"); -// Dark mode toggle +// Dark Mode toggle button (unchanged) const darkModeToggle = document.createElement('button'); darkModeToggle.textContent = 'Toggle Dark Mode'; Object.assign(darkModeToggle.style, { @@ -101,8 +101,10 @@ d3.csv("PHSwithContinent.csv").then(function(data) { const selectedContinent = d3.select("#continentSelect").property("value"); const selectedSex = d3.select("#sexSelect").property("value"); const selectedEdu = d3.select("#incomeSelect").property("value"); + const selectedChart = d3.select("#chartTypeSelect").property("value"); + + updateLegend(selectedChart); - // Two-line title let title1 = "Self-Reported Health"; let title2 = ""; if (selectedContinent !== "All") title2 += `${selectedContinent}`; @@ -123,13 +125,7 @@ d3.csv("PHSwithContinent.csv").then(function(data) { !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; @@ -137,90 +133,13 @@ d3.csv("PHSwithContinent.csv").then(function(data) { 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"]}`); + // 🆕 Decide which chart type to draw + if (selectedChart === "grouped") { + drawGrouped(filteredData); + } else if (selectedChart === "dot") { + drawDotPlot(filteredData); + } else if (selectedChart === "heatmap") { + drawHeatmap(filteredData); } } @@ -233,6 +152,7 @@ d3.csv("PHSwithContinent.csv").then(function(data) { d3.select("#continentSelect").property("value", "All"); d3.select("#sexSelect").property("value", "All"); d3.select("#incomeSelect").property("value", "All"); + d3.select("#chartTypeSelect").property("value", "grouped"); // 🆕 Reset chart type too updateChart(); }); @@ -251,4 +171,304 @@ d3.csv("PHSwithContinent.csv").then(function(data) { const filename = parts.join("_") + ".png"; saveSvgAsPng.saveSvgAsPng(document.querySelector("svg"), filename); }); -}); \ No newline at end of file +}); + +function updateLegend(chartType) { + const legend = d3.select("#legend"); + legend.html(""); // Clear existing + + if (chartType === "heatmap") { + // Create a wrapper for gradient + labels + const wrapper = legend.append("div") + .style("display", "flex") + .style("flex-direction", "column") + .style("align-items", "center") + .style("margin-top", "10px"); + + // Gradient bar + wrapper.append("div") + .style("width", "300px") + .style("height", "20px") + .style("background", "linear-gradient(to right, #edf8b1, #2c7fb8)") + .style("margin-bottom", "5px"); + + // Tick labels (0% — 50% — 100%) + const ticks = wrapper.append("div") + .style("width", "300px") + .style("display", "flex") + .style("justify-content", "space-between") + .style("font-size", "12px"); + + ticks.html('0%50%100%'); + + } else { + // Grouped/Dot Chart — categorical legend + educationLevels.forEach(level => { + const item = legend.append("div").attr("class", "legend-item"); + item.append("div") + .attr("class", "legend-color") + .style("background-color", color(level)); + item.append("span").text(level); + }); + } +} + +function drawGrouped(data) { + const countries = Array.from(new Set(data.map(d => d["Reference area"]))); + x0.domain(countries); + x1.domain(educationLevels).rangeRound([0, x0.bandwidth()]); + y.domain([0, 100]); + + const groupedData = data.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"); + + 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) + .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)"); + }) + .on("mouseout", function() { + tooltip.transition().duration(500) + .style("opacity", 0) + .style("transform", "translateY(0px)"); + }) + .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)); + + // 📌 Smart Annotate lowest bar + 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"]) + x1(lowest["Socio-economic status"]) + x1.bandwidth() / 2; + const barY = y(lowest.value); + + const labelText = `Lowest: ${lowest["Reference area"]}`; + + const tempText = g.append("text") + .attr("x", -9999) // Place it offscreen first + .attr("y", -9999) + .style("font-size", "12px") + .style("font-weight", "bold") + .text(labelText); + + const textWidth = tempText.node().getBBox().width; + tempText.remove(); // Cleanup + + // Background rectangle scaled to text size + g.append("rect") + .attr("x", barX - textWidth / 2 - 6) + .attr("y", barY - 28) + .attr("width", textWidth + 12) + .attr("height", 20) + .attr("rx", 4) + .attr("fill", darkMode ? "#222" : "#fff") + .attr("stroke", darkMode ? "#aaa" : "#999") + .attr("stroke-width", 0.5) + .attr("opacity", 0.9); + + // Final visible text + g.append("text") + .attr("x", barX) + .attr("y", barY - 14) + .attr("text-anchor", "middle") + .style("font-size", "12px") + .style("font-weight", "bold") + .style("fill", darkMode ? "#eee" : "red") + .text(labelText); + } +} + +function drawDotPlot(data) { + const x = d3.scaleLinear() + .domain([0, 100]) + .range([0, width]); + + const y = d3.scaleBand() + .domain([...new Set(data.map(d => d["Reference area"]))]) + .range([0, height]) + .padding(0.3); + + const groupedData = data.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(0,${y(d.Country)})`) + .attr("class", "country-group"); + + countryGroups.selectAll("circle") + .data(d => d.values) + .join("circle") + .attr("cy", d => 0) + .attr("cx", 0) + .attr("r", 0) + .attr("fill", d => color(d["Socio-economic status"])) + .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)"); + }) + .on("mouseout", function() { + tooltip.transition().duration(500) + .style("opacity", 0) + .style("transform", "translateY(0px)"); + }) + .transition() + .duration(800) + .ease(d3.easeCubicOut) + .attr("cx", d => x(d.value)) + .attr("r", 6); + + g.append("g") + .attr("transform", `translate(0,${height})`) + .call(d3.axisBottom(x).ticks(10)); + + g.append("g") + .call(d3.axisLeft(y)); + + // Add X-axis label + g.append("text") + .attr("x", width / 2) + .attr("y", height + 50) + .attr("text-anchor", "middle") + .style("font-size", "14px") + .style("fill", darkMode ? "#fff" : "#000") + .text("Percentage Reporting Good Health (%)"); + + // 📌 Smart annotate lowest dot + 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 dotX = x(lowest.value); + const dotY = y(lowest["Reference area"]); + + const labelText = `Lowest: ${lowest["Reference area"]}`; + + const tempText = g.append("text") + .attr("x", -9999) + .attr("y", -9999) + .style("font-size", "12px") + .style("font-weight", "bold") + .text(labelText); + + const textWidth = tempText.node().getBBox().width; + tempText.remove(); + + g.append("rect") + .attr("x", dotX - textWidth / 2 - 6) + .attr("y", dotY - 28) + .attr("width", textWidth + 12) + .attr("height", 20) + .attr("rx", 4) + .attr("fill", darkMode ? "#222" : "#fff") + .attr("stroke", darkMode ? "#aaa" : "#999") + .attr("stroke-width", 0.5) + .attr("opacity", 0.9); + + g.append("text") + .attr("x", dotX) + .attr("y", dotY - 14) + .attr("text-anchor", "middle") + .style("font-size", "12px") + .style("font-weight", "bold") + .style("fill", darkMode ? "#eee" : "red") + .text(labelText); + } +} + +function drawHeatmap(data) { + const countries = [...new Set(data.map(d => d["Reference area"]))]; + + const x = d3.scaleBand() + .domain(educationLevels) + .range([0, width]) + .padding(0.05); + + const y = d3.scaleBand() + .domain(countries) + .range([0, height]) + .padding(0.05); + + const colorScale = d3.scaleSequential() + .interpolator(d3.interpolateYlGnBu) + .domain([0, 100]); + + g.selectAll() + .data(data) + .join("rect") + .attr("x", d => x(d["Socio-economic status"])) + .attr("y", d => y(d["Reference area"])) + .attr("width", x.bandwidth()) + .attr("height", y.bandwidth()) + .attr("fill", d => colorScale(parseFloat(d.OBS_VALUE))) + .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)"); + }) + .on("mouseout", function() { + tooltip.transition().duration(500) + .style("opacity", 0) + .style("transform", "translateY(0px)"); + }); + + g.append("g") + .attr("transform", `translate(0,${height})`) + .call(d3.axisBottom(x)); + + g.append("g") + .call(d3.axisLeft(y)); +} \ No newline at end of file From e10e838745146db18e6020f2432415ad7d524e07 Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Sun, 27 Apr 2025 10:21:41 +1000 Subject: [PATCH 2/7] fixed darkmode and centred headings --- CSS/style.css | 7 +++++++ index.html | 2 +- script/script.js | 25 ++++++++++++++++++------- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CSS/style.css b/CSS/style.css index aeb1de8..b19cbfe 100644 --- a/CSS/style.css +++ b/CSS/style.css @@ -66,4 +66,11 @@ body { } .hidden { display: none; + } + .page-title { + text-align: center; + margin-top: 20px; + margin-bottom: 10px; + font-size: 28px; + font-weight: bold; } \ No newline at end of file diff --git a/index.html b/index.html index 949d24f..84acc3b 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ -

How Education Level Shapes Perceived Health

+

How Education Level Shapes Perceived Health

diff --git a/script/script.js b/script/script.js index f1c6e38..38c724b 100644 --- a/script/script.js +++ b/script/script.js @@ -63,11 +63,21 @@ darkModeToggle.addEventListener('click', () => { darkMode = !darkMode; document.body.style.backgroundColor = darkMode ? '#121212' : '#fff'; document.body.style.color = darkMode ? '#eee' : '#000'; + chartTitle1.style("fill", darkMode ? "#eee" : "#000"); + chartTitle2.style("fill", darkMode ? "#ccc" : "#666"); 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"); + + // 🆕 THESE MUST BE INSIDE the event listener block + g.selectAll(".lowest-annotation") + .style("fill", darkMode ? "#eee" : "red"); + + g.selectAll(".lowest-box") + .attr("fill", darkMode ? "#222" : "#fff") + .attr("stroke", darkMode ? "#aaa" : "#999"); }); d3.csv("PHSwithContinent.csv").then(function(data) { @@ -272,7 +282,7 @@ function drawGrouped(data) { g.append("g") .call(d3.axisLeft(y).ticks(10)); - // 📌 Smart Annotate lowest bar + // 📌 Annotate lowest bar with dynamic box 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]); @@ -283,17 +293,17 @@ function drawGrouped(data) { const labelText = `Lowest: ${lowest["Reference area"]}`; const tempText = g.append("text") - .attr("x", -9999) // Place it offscreen first + .attr("x", -9999) .attr("y", -9999) .style("font-size", "12px") .style("font-weight", "bold") .text(labelText); const textWidth = tempText.node().getBBox().width; - tempText.remove(); // Cleanup + tempText.remove(); - // Background rectangle scaled to text size g.append("rect") + .attr("class", "lowest-box") .attr("x", barX - textWidth / 2 - 6) .attr("y", barY - 28) .attr("width", textWidth + 12) @@ -304,8 +314,8 @@ function drawGrouped(data) { .attr("stroke-width", 0.5) .attr("opacity", 0.9); - // Final visible text g.append("text") + .attr("class", "lowest-annotation") .attr("x", barX) .attr("y", barY - 14) .attr("text-anchor", "middle") @@ -375,7 +385,6 @@ function drawDotPlot(data) { g.append("g") .call(d3.axisLeft(y)); - // Add X-axis label g.append("text") .attr("x", width / 2) .attr("y", height + 50) @@ -384,7 +393,7 @@ function drawDotPlot(data) { .style("fill", darkMode ? "#fff" : "#000") .text("Percentage Reporting Good Health (%)"); - // 📌 Smart annotate lowest dot + // 📌 Annotate lowest dot with dynamic box 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]); @@ -405,6 +414,7 @@ function drawDotPlot(data) { tempText.remove(); g.append("rect") + .attr("class", "lowest-box") .attr("x", dotX - textWidth / 2 - 6) .attr("y", dotY - 28) .attr("width", textWidth + 12) @@ -416,6 +426,7 @@ function drawDotPlot(data) { .attr("opacity", 0.9); g.append("text") + .attr("class", "lowest-annotation") .attr("x", dotX) .attr("y", dotY - 14) .attr("text-anchor", "middle") From 93ecc88adea650ef41cd3989c6224b71674e6121 Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Sun, 27 Apr 2025 18:21:39 +1000 Subject: [PATCH 3/7] fixed export png function --- index.html | 2 +- script/script.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 84acc3b..d31d7c6 100644 --- a/index.html +++ b/index.html @@ -43,7 +43,7 @@
Loading chart, please wait...
- +
diff --git a/script/script.js b/script/script.js index 38c724b..8edb336 100644 --- a/script/script.js +++ b/script/script.js @@ -170,16 +170,17 @@ d3.csv("PHSwithContinent.csv").then(function(data) { 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); + + saveSvgAsPng(document.getElementById("chart"), filename); }); }); From b578c5359418ebbba8c0d55f3bfc6ee7de641380 Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Mon, 28 Apr 2025 15:33:26 +1000 Subject: [PATCH 4/7] fixed code commenting --- CSS/style.css | 171 ++++++++++++++++++++++++++--------------------- index.html | 18 ++++- script/script.js | 92 ++++++++++++++++--------- 3 files changed, 173 insertions(+), 108 deletions(-) diff --git a/CSS/style.css b/CSS/style.css index b19cbfe..855e082 100644 --- a/CSS/style.css +++ b/CSS/style.css @@ -1,76 +1,97 @@ +/* Base body styles */ body { - font-family: sans-serif; - } - .bar { - opacity: 0.8; - transition: opacity 0.3s; - } - .bar:hover { - opacity: 1; - } - .axis-label { - font-size: 14px; - font-weight: bold; - } - .tooltip { - position: absolute; - padding: 6px; - background: #fff; - border: 1px solid #ccc; - border-radius: 5px; - box-shadow: 0px 2px 8px rgba(0,0,0,0.2); - font-size: 12px; - pointer-events: none; - opacity: 0; - transition: all 0.3s ease; - } - .controls { - margin-bottom: 20px; - } - select { - margin-right: 10px; - padding: 5px; - } - button { - margin: 5px; - padding: 8px 12px; - font-size: 14px; - cursor: pointer; - background-color: #4E79A7; - color: white; - border: none; - border-radius: 4px; - } - button:hover { - background-color: #3a5f8c; - } - .legend { - margin-top: 20px; - } - .legend-item { - display: inline-block; - margin-right: 20px; - font-size: 14px; - } - .legend-color { - width: 12px; - height: 12px; - display: inline-block; - margin-right: 5px; - vertical-align: middle; - } - #loading { - font-size: 18px; - text-align: center; - margin: 20px; - } - .hidden { - display: none; - } - .page-title { - text-align: center; - margin-top: 20px; - margin-bottom: 10px; - font-size: 28px; - font-weight: bold; - } \ No newline at end of file + font-family: sans-serif; +} + +/* Chart bars */ +.bar { + opacity: 0.8; + transition: opacity 0.3s; +} +.bar:hover { + opacity: 1; +} + +/* Axis labels */ +.axis-label { + font-size: 14px; + font-weight: bold; +} + +/* Tooltip styles */ +.tooltip { + position: absolute; + padding: 6px; + background: #fff; + border: 1px solid #ccc; + border-radius: 5px; + box-shadow: 0px 2px 8px rgba(0,0,0,0.2); + font-size: 12px; + pointer-events: none; + opacity: 0; + transition: all 0.3s ease; +} + +/* Controls section (dropdowns and buttons) */ +.controls { + margin-bottom: 20px; +} + +/* Dropdown (select) styling */ +select { + margin-right: 10px; + padding: 5px; +} + +/* Buttons styling */ +button { + margin: 5px; + padding: 8px 12px; + font-size: 14px; + cursor: pointer; + background-color: #4E79A7; + color: white; + border: none; + border-radius: 4px; +} +button:hover { + background-color: #3a5f8c; +} + +/* Legend area */ +.legend { + margin-top: 20px; +} +.legend-item { + display: inline-block; + margin-right: 20px; + font-size: 14px; +} +.legend-color { + width: 12px; + height: 12px; + display: inline-block; + margin-right: 5px; + vertical-align: middle; +} + +/* Loading message */ +#loading { + font-size: 18px; + text-align: center; + margin: 20px; +} + +/* Hidden class (for hiding elements like loading spinner) */ +.hidden { + display: none; +} + +/* Main page title */ +.page-title { + text-align: center; + margin-top: 20px; + margin-bottom: 10px; + font-size: 28px; + font-weight: bold; +} \ No newline at end of file diff --git a/index.html b/index.html index d31d7c6..6ad0bfb 100644 --- a/index.html +++ b/index.html @@ -3,13 +3,22 @@ Health Perception by Education Level + + + + + + + +

How Education Level Shapes Perceived Health

+
+
- - +
Loading chart, please wait...
+ +
+ - + \ No newline at end of file diff --git a/script/script.js b/script/script.js index 8edb336..6084930 100644 --- a/script/script.js +++ b/script/script.js @@ -1,25 +1,31 @@ +// SVG setup const svg = d3.select("svg"), margin = {top: 80, right: 30, bottom: 150, left: 160}, 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})`); +// Scales for grouped bar and dot plots const x0 = d3.scaleBand().rangeRound([0, width]).paddingInner(0.1); const x1 = d3.scaleBand().padding(0.05); const y = d3.scaleLinear().rangeRound([height, 0]); +// Education levels for legend and grouped charts const educationLevels = [ "Pre-primary, primary and lower secondary education", "Upper secondary and post-secondary non-tertiary, all programmes", "Tertiary education" ]; +// Color scale for education levels const color = d3.scaleOrdinal() .domain(educationLevels) .range(["#4E79A7", "#F28E2B", "#59A14F"]); +// Tooltip div const tooltip = d3.select("body").append("div").attr("class", "tooltip"); +// Chart titles const chartTitle1 = svg.append("text") .attr("x", +svg.attr("width") / 2) .attr("y", 30) @@ -34,6 +40,7 @@ const chartTitle2 = svg.append("text") .style("font-size", "18px") .style("fill", "#666"); +// "No Data" placeholder text const noDataText = g.append("text") .attr("x", width / 2) .attr("y", height / 2) @@ -43,7 +50,7 @@ const noDataText = g.append("text") .style("display", "none") .text("No data available for selected filters"); -// Dark Mode toggle button (unchanged) +// Dark mode toggle button const darkModeToggle = document.createElement('button'); darkModeToggle.textContent = 'Toggle Dark Mode'; Object.assign(darkModeToggle.style, { @@ -58,6 +65,7 @@ Object.assign(darkModeToggle.style, { }); document.querySelector('.controls').appendChild(darkModeToggle); +// Dark mode logic let darkMode = false; darkModeToggle.addEventListener('click', () => { darkMode = !darkMode; @@ -70,19 +78,15 @@ darkModeToggle.addEventListener('click', () => { tooltip.style("background-color", darkMode ? "#222" : "#fff") .style("color", darkMode ? "#eee" : "#000"); g.selectAll("text").style("fill", darkMode ? "#eee" : "#000"); - - // 🆕 THESE MUST BE INSIDE the event listener block - g.selectAll(".lowest-annotation") - .style("fill", darkMode ? "#eee" : "red"); - - g.selectAll(".lowest-box") - .attr("fill", darkMode ? "#222" : "#fff") - .attr("stroke", darkMode ? "#aaa" : "#999"); + g.selectAll(".lowest-annotation").style("fill", darkMode ? "#eee" : "red"); + g.selectAll(".lowest-box").attr("fill", darkMode ? "#222" : "#fff").attr("stroke", darkMode ? "#aaa" : "#999"); }); +// Load data d3.csv("PHSwithContinent.csv").then(function(data) { const allData = data; + // Populate continent and education dropdowns const continents = Array.from(new Set(data.map(d => d.CONTINENT))).sort(); continents.forEach(continent => { d3.select("#continentSelect") @@ -98,6 +102,7 @@ d3.csv("PHSwithContinent.csv").then(function(data) { .text(level); }); + // Setup static legend (initial) const legend = d3.select("#legend"); color.domain().forEach(d => { const item = legend.append("div").attr("class", "legend-item"); @@ -107,6 +112,7 @@ d3.csv("PHSwithContinent.csv").then(function(data) { item.append("span").text(d); }); + // Update the chart function updateChart() { const selectedContinent = d3.select("#continentSelect").property("value"); const selectedSex = d3.select("#sexSelect").property("value"); @@ -115,26 +121,26 @@ d3.csv("PHSwithContinent.csv").then(function(data) { updateLegend(selectedChart); + // Update chart titles 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); + // Filter data based on selections 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 => + ).filter(d => educationLevels.includes(d["Socio-economic status"]) && !isNaN(parseFloat(d.OBS_VALUE)) ); + // Clear and redraw g.selectAll("*").remove(); if (filteredData.length === 0) { noDataText.style("display", null); @@ -143,7 +149,6 @@ d3.csv("PHSwithContinent.csv").then(function(data) { noDataText.style("display", "none"); } - // 🆕 Decide which chart type to draw if (selectedChart === "grouped") { drawGrouped(filteredData); } else if (selectedChart === "dot") { @@ -156,64 +161,65 @@ d3.csv("PHSwithContinent.csv").then(function(data) { updateChart(); d3.select("#loading").classed("hidden", true); + // Attach event listeners 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"); - d3.select("#chartTypeSelect").property("value", "grouped"); // 🆕 Reset chart type too + d3.select("#chartTypeSelect").property("value", "grouped"); updateChart(); }); + // Download chart as PNG 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(document.getElementById("chart"), filename); + + saveSvgAsPng(document.getElementById("chart"), { + filename: filename, + scale: 2, + backgroundColor: darkMode ? "#121212" : "#ffffff" + }); }); }); +// Update legend depending on chart type function updateLegend(chartType) { const legend = d3.select("#legend"); legend.html(""); // Clear existing if (chartType === "heatmap") { - // Create a wrapper for gradient + labels const wrapper = legend.append("div") .style("display", "flex") .style("flex-direction", "column") .style("align-items", "center") .style("margin-top", "10px"); - // Gradient bar wrapper.append("div") .style("width", "300px") .style("height", "20px") .style("background", "linear-gradient(to right, #edf8b1, #2c7fb8)") .style("margin-bottom", "5px"); - // Tick labels (0% — 50% — 100%) - const ticks = wrapper.append("div") + wrapper.append("div") .style("width", "300px") .style("display", "flex") .style("justify-content", "space-between") - .style("font-size", "12px"); - - ticks.html('0%50%100%'); + .style("font-size", "12px") + .html('0%50%100%'); } else { - // Grouped/Dot Chart — categorical legend educationLevels.forEach(level => { const item = legend.append("div").attr("class", "legend-item"); item.append("div") @@ -225,11 +231,13 @@ function updateLegend(chartType) { } function drawGrouped(data) { + // Setup x0, x1, and y domains const countries = Array.from(new Set(data.map(d => d["Reference area"]))); x0.domain(countries); x1.domain(educationLevels).rangeRound([0, x0.bandwidth()]); y.domain([0, 100]); + // Group data by country const groupedData = data.reduce((acc, d) => { const found = acc.find(v => v.Country === d["Reference area"]); const value = parseFloat(d.OBS_VALUE); @@ -239,6 +247,7 @@ function drawGrouped(data) { return acc; }, []); + // Draw grouped bars const countryGroups = g.append("g") .selectAll("g") .data(groupedData) @@ -255,6 +264,7 @@ function drawGrouped(data) { .attr("y", height) .attr("height", 0) .on("mouseover", function(event, d) { + // Show tooltip tooltip.html(`${d["Reference area"]}
${d["Socio-economic status"]}
${d.OBS_VALUE}%`) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 40) + "px") @@ -263,6 +273,7 @@ function drawGrouped(data) { .style("transform", "translateY(-10px)"); }) .on("mouseout", function() { + // Hide tooltip tooltip.transition().duration(500) .style("opacity", 0) .style("transform", "translateY(0px)"); @@ -273,6 +284,7 @@ function drawGrouped(data) { .attr("y", d => y(d.value)) .attr("height", d => height - y(d.value)); + // Add X axis g.append("g") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x0)) @@ -280,10 +292,11 @@ function drawGrouped(data) { .attr("transform", "rotate(45)") .style("text-anchor", "start"); + // Add Y axis g.append("g") .call(d3.axisLeft(y).ticks(10)); - // 📌 Annotate lowest bar with dynamic box + // Annotate lowest bar 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]); @@ -293,6 +306,7 @@ function drawGrouped(data) { const labelText = `Lowest: ${lowest["Reference area"]}`; + // Measure text width const tempText = g.append("text") .attr("x", -9999) .attr("y", -9999) @@ -303,6 +317,7 @@ function drawGrouped(data) { const textWidth = tempText.node().getBBox().width; tempText.remove(); + // Draw annotation background box g.append("rect") .attr("class", "lowest-box") .attr("x", barX - textWidth / 2 - 6) @@ -315,6 +330,7 @@ function drawGrouped(data) { .attr("stroke-width", 0.5) .attr("opacity", 0.9); + // Draw annotation text g.append("text") .attr("class", "lowest-annotation") .attr("x", barX) @@ -328,6 +344,7 @@ function drawGrouped(data) { } function drawDotPlot(data) { + // Setup x and y scales const x = d3.scaleLinear() .domain([0, 100]) .range([0, width]); @@ -337,6 +354,7 @@ function drawDotPlot(data) { .range([0, height]) .padding(0.3); + // Group data by country const groupedData = data.reduce((acc, d) => { const found = acc.find(v => v.Country === d["Reference area"]); const value = parseFloat(d.OBS_VALUE); @@ -346,6 +364,7 @@ function drawDotPlot(data) { return acc; }, []); + // Draw dot points const countryGroups = g.append("g") .selectAll("g") .data(groupedData) @@ -361,6 +380,7 @@ function drawDotPlot(data) { .attr("r", 0) .attr("fill", d => color(d["Socio-economic status"])) .on("mouseover", function(event, d) { + // Show tooltip tooltip.html(`${d["Reference area"]}
${d["Socio-economic status"]}
${d.OBS_VALUE}%`) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 40) + "px") @@ -369,6 +389,7 @@ function drawDotPlot(data) { .style("transform", "translateY(-10px)"); }) .on("mouseout", function() { + // Hide tooltip tooltip.transition().duration(500) .style("opacity", 0) .style("transform", "translateY(0px)"); @@ -379,6 +400,7 @@ function drawDotPlot(data) { .attr("cx", d => x(d.value)) .attr("r", 6); + // Add axes g.append("g") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x).ticks(10)); @@ -386,6 +408,7 @@ function drawDotPlot(data) { g.append("g") .call(d3.axisLeft(y)); + // Add x-axis label g.append("text") .attr("x", width / 2) .attr("y", height + 50) @@ -394,7 +417,7 @@ function drawDotPlot(data) { .style("fill", darkMode ? "#fff" : "#000") .text("Percentage Reporting Good Health (%)"); - // 📌 Annotate lowest dot with dynamic box + // Annotate lowest dot 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]); @@ -404,6 +427,7 @@ function drawDotPlot(data) { const labelText = `Lowest: ${lowest["Reference area"]}`; + // Measure text width const tempText = g.append("text") .attr("x", -9999) .attr("y", -9999) @@ -414,6 +438,7 @@ function drawDotPlot(data) { const textWidth = tempText.node().getBBox().width; tempText.remove(); + // Draw annotation background box g.append("rect") .attr("class", "lowest-box") .attr("x", dotX - textWidth / 2 - 6) @@ -426,6 +451,7 @@ function drawDotPlot(data) { .attr("stroke-width", 0.5) .attr("opacity", 0.9); + // Draw annotation text g.append("text") .attr("class", "lowest-annotation") .attr("x", dotX) @@ -439,6 +465,7 @@ function drawDotPlot(data) { } function drawHeatmap(data) { + // Setup x and y scales for heatmap const countries = [...new Set(data.map(d => d["Reference area"]))]; const x = d3.scaleBand() @@ -451,10 +478,12 @@ function drawHeatmap(data) { .range([0, height]) .padding(0.05); + // Color scale for heatmap (sequential) const colorScale = d3.scaleSequential() .interpolator(d3.interpolateYlGnBu) .domain([0, 100]); + // Draw heatmap squares g.selectAll() .data(data) .join("rect") @@ -464,6 +493,7 @@ function drawHeatmap(data) { .attr("height", y.bandwidth()) .attr("fill", d => colorScale(parseFloat(d.OBS_VALUE))) .on("mouseover", function(event, d) { + // Show tooltip tooltip.html(`${d["Reference area"]}
${d["Socio-economic status"]}: ${d.OBS_VALUE}%`) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 40) + "px") @@ -472,11 +502,13 @@ function drawHeatmap(data) { .style("transform", "translateY(-10px)"); }) .on("mouseout", function() { + // Hide tooltip tooltip.transition().duration(500) .style("opacity", 0) .style("transform", "translateY(0px)"); }); + // Add axes g.append("g") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x)); From b4642ce57a8774a5ff42a221d3b6d6663a2c7712 Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Tue, 29 Apr 2025 19:19:45 +1000 Subject: [PATCH 5/7] fixed download function --- script/script.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/script/script.js b/script/script.js index 6084930..aa8c4bd 100644 --- a/script/script.js +++ b/script/script.js @@ -171,29 +171,26 @@ d3.csv("PHSwithContinent.csv").then(function(data) { updateChart(); }); - // Download chart as PNG 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(document.getElementById("chart"), { - filename: filename, + + saveSvgAsPng(document.getElementById("chart"), filename, { // <-- filename second, options third scale: 2, backgroundColor: darkMode ? "#121212" : "#ffffff" }); }); }); - // Update legend depending on chart type function updateLegend(chartType) { const legend = d3.select("#legend"); From 9e3190d75984a04d6780097dbe8d7264fa3bc97b Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Tue, 29 Apr 2025 19:25:11 +1000 Subject: [PATCH 6/7] Update script.js --- script/script.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/script/script.js b/script/script.js index aa8c4bd..0d934e6 100644 --- a/script/script.js +++ b/script/script.js @@ -175,9 +175,11 @@ d3.csv("PHSwithContinent.csv").then(function(data) { const c = d3.select("#continentSelect").property("value"); const s = d3.select("#sexSelect").property("value"); const e = d3.select("#incomeSelect").property("value"); + const chartType = d3.select("#chartTypeSelect").property("value"); // 🆕 Get selected chart type const parts = [ "education_health", + chartType, // 🆕 Add chart type into the filename c !== "All" ? c : null, s !== "All" ? s : null, e !== "All" ? e.replace(/[^a-zA-Z0-9]+/g, "_").toLowerCase() : null @@ -185,7 +187,7 @@ d3.csv("PHSwithContinent.csv").then(function(data) { const filename = parts.join("_") + ".png"; - saveSvgAsPng(document.getElementById("chart"), filename, { // <-- filename second, options third + saveSvgAsPng(document.getElementById("chart"), filename, { scale: 2, backgroundColor: darkMode ? "#121212" : "#ffffff" }); From 54613b7c9a5b704e4ed6eb6fbd1f6c9e214f0189 Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Tue, 29 Apr 2025 19:25:52 +1000 Subject: [PATCH 7/7] Update script.js --- script/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/script.js b/script/script.js index 0d934e6..feb4a39 100644 --- a/script/script.js +++ b/script/script.js @@ -175,11 +175,11 @@ d3.csv("PHSwithContinent.csv").then(function(data) { const c = d3.select("#continentSelect").property("value"); const s = d3.select("#sexSelect").property("value"); const e = d3.select("#incomeSelect").property("value"); - const chartType = d3.select("#chartTypeSelect").property("value"); // 🆕 Get selected chart type + const chartType = d3.select("#chartTypeSelect").property("value"); //Get selected chart type const parts = [ "education_health", - chartType, // 🆕 Add chart type into the filename + chartType, //Add chart type into the filename c !== "All" ? c : null, s !== "All" ? s : null, e !== "All" ? e.replace(/[^a-zA-Z0-9]+/g, "_").toLowerCase() : null