draft chart
This commit is contained in:
parent
575a0a436e
commit
297b3ca70b
7 changed files with 3251 additions and 127 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:8080",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
117
CSS/style.css
117
CSS/style.css
|
|
@ -1,58 +1,69 @@
|
|||
/* General Reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Body Styling */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
background-color: #f4f4f4;
|
||||
color: #333;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Header Styling */
|
||||
header {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Navigation Styling */
|
||||
nav {
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
margin: 0 10px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.bar {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.bar:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.axis-label {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: #007BFF;
|
||||
}
|
||||
|
||||
/* Main Content Styling */
|
||||
main {
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Footer Styling */
|
||||
footer {
|
||||
text-align: center;
|
||||
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;
|
||||
padding: 10px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// Basic D3 Script
|
||||
|
||||
// Create an SVG container
|
||||
const svg = d3.select("body")
|
||||
.append("svg")
|
||||
.attr("width", 500)
|
||||
.attr("height", 500);
|
||||
|
||||
// Add a circle to the SVG
|
||||
svg.append("circle")
|
||||
.attr("cx", 250)
|
||||
.attr("cy", 250)
|
||||
.attr("r", 50)
|
||||
.attr("fill", "blue");
|
||||
|
||||
// Add a rectangle to the SVG
|
||||
svg.append("rect")
|
||||
.attr("x", 200)
|
||||
.attr("y", 300)
|
||||
.attr("width", 100)
|
||||
.attr("height", 50)
|
||||
.attr("fill", "green");
|
||||
|
||||
// Add some text to the SVG
|
||||
svg.append("text")
|
||||
.attr("x", 250)
|
||||
.attr("y", 100)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("font-size", "20px")
|
||||
.attr("fill", "black")
|
||||
.text("Hello, D3!");
|
||||
2881
PHSwithContinent.csv
Executable file
2881
PHSwithContinent.csv
Executable file
File diff suppressed because it is too large
Load diff
78
index.html
78
index.html
|
|
@ -1,49 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Basic Webpage with D3</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
header, footer {
|
||||
background-color: #f4f4f4;
|
||||
text-align: center;
|
||||
padding: 1em 0;
|
||||
}
|
||||
main {
|
||||
padding: 20px;
|
||||
}
|
||||
#d3-content {
|
||||
margin: 20px auto;
|
||||
width: 80%;
|
||||
height: 400px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<title>Health Perception by Education Level</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/save-svg-as-png/1.4.17/saveSvgAsPng.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Welcome to My D3 Webpage</h1>
|
||||
</header>
|
||||
<main>
|
||||
<h2>D3 Content Area</h2>
|
||||
<div id="d3-content"></div>
|
||||
</main>
|
||||
<footer>
|
||||
<p>© 2023 My Website</p>
|
||||
</footer>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script>
|
||||
// D3 content will go here
|
||||
const svg = d3.select("#d3-content")
|
||||
.append("svg")
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%");
|
||||
</script>
|
||||
<h2>How Education Level Shapes Perceived Health</h2>
|
||||
|
||||
<div class="controls">
|
||||
<label for="continentSelect">Continent:</label>
|
||||
<select id="continentSelect">
|
||||
<option value="All">All</option>
|
||||
</select>
|
||||
|
||||
<label for="sexSelect">Sex:</label>
|
||||
<select id="sexSelect">
|
||||
<option value="All">All</option>
|
||||
<option value="Male">Male</option>
|
||||
<option value="Female">Female</option>
|
||||
</select>
|
||||
|
||||
<label for="incomeSelect">Education Level:</label>
|
||||
<select id="incomeSelect">
|
||||
<option value="All">All</option>
|
||||
</select>
|
||||
|
||||
<button id="resetButton">Reset Filters</button>
|
||||
<button id="downloadButton">Download Chart as PNG</button>
|
||||
</div>
|
||||
|
||||
<div id="loading">Loading chart, please wait...</div>
|
||||
|
||||
<svg width="960" height="600"></svg>
|
||||
|
||||
<div class="legend" id="legend"></div>
|
||||
|
||||
<script src="script/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
254
script/script.js
Normal file
254
script/script.js
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue