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] 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