svg = {
const width = 1200, height = 500;
const positions = {
"Oceania": [150, 150],
"Asia": [225, 250],
"Africa": [300, 150],
"Americas": [450, 150],
"Europe": [375, 250],
};
const colors = {
"Africa": "#000000",
"Asia": "#FFFF00",
"Oceania": "#0000FF",
"Americas": "#FF0000",
"Europe": "#00FF00"
};
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("background-color", "#ffffff");
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.attr("font-weight", "bold")
.text("What Does Each Olympic Year Reveal About Global Equity and Representation?");
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("background", "white")
.style("border", "1px solid #ccc")
.style("padding", "6px")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font-family", "sans-serif")
.style("font-size", "12px")
.style("opacity", 0);
// Circles
const circles = svg.selectAll("circle")
.data(filteredData)
.join("circle")
.attr("cx", d => positions[d.region][0])
.attr("cy", d => positions[d.region][1] + 50) // Shift down for title space
.attr("r", d => medalScale(d.totalmedals))
.attr("stroke", d => colors[d.region])
.attr("stroke-width", d => gdpScale(d.gdp))
.attr("fill", d => colors[d.region])
.attr("fill-opacity", d => opacityScale(d));
// Tooltip interaction
circles
.on("mouseover", (event, d) => {
tooltip.transition().duration(150).style("opacity", 1);
tooltip.html(`
<strong>${d.region}</strong><br/>
Female Athletes: ${d.female}<br/>
Male Athletes: ${d.male}<br/>
Total Medals: ${d.totalmedals}<br/>
GDP: $${(+d.gdp).toLocaleString()}
`);
})
.on("mousemove", event => {
tooltip
.style("left", (event.pageX + 12) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", () => {
tooltip.transition().duration(150).style("opacity", 0);
});
// Labels
svg.selectAll("text.label")
.data(filteredData)
.join("text")
.attr("class", "label")
.attr("x", d => positions[d.region][0])
.attr("y", d => positions[d.region][1] + 50 + medalScale(d.totalmedals) + 15)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", "#333")
.text(d => d.region);
// Identify and render placeholder rings for regions with missing data
const presentRegions = new Set(filteredData.map(d => d.region));
const missingRegions = Object.keys(positions).filter(r => !presentRegions.has(r));
missingRegions.forEach(region => {
const [x, y] = positions[region];
// Draw a placeholder ring
svg.append("circle")
.attr("cx", x)
.attr("cy", y + 50)
.attr("r", 25)
.attr("stroke", "#999")
.attr("stroke-width", 2)
.attr("fill", "none");
// Label the placeholder ring
svg.append("text")
.attr("x", x)
.attr("y", y + 50 + 5)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", "#999")
.text("no data");
});
// Legend group
const legendGroup = svg.append("g")
.attr("transform", "translate(600, 150)");
// Background box
legendGroup.append("rect")
.attr("width", 300)
.attr("height", 115)
.attr("fill", "#f9f9f9")
.attr("stroke", "#ccc")
.attr("rx", 8)
.attr("ry", 8);
// Legend title
legendGroup.append("text")
.attr("x", 10)
.attr("y", 15)
.style("font-size", "14px")
.style("font-weight", "bold")
.text("Legend");
// Ring Size
legendGroup.append("circle")
.attr("cx", 15)
.attr("cy", 35)
.attr("r", 6)
.attr("stroke", "#000")
.attr("stroke-width", 2)
.attr("fill", "none");
legendGroup.append("text")
.attr("x", 30)
.attr("y", 39)
.style("font-size", "12px")
.text("Ring size: Medal Wins");
// Ring Thickness
legendGroup.append("line")
.attr("x1", 10)
.attr("y1", 60)
.attr("x2", 30)
.attr("y2", 60)
.attr("stroke", "#000")
.attr("stroke-width", 5);
legendGroup.append("text")
.attr("x", 40)
.attr("y", 63)
.style("font-size", "12px")
.text("Thickness: GDP");
// Define gradient for gender imbalance
const defs = svg.append("defs");
const gradient = defs.append("linearGradient")
.attr("id", "genderGradient")
.attr("x1", "0%")
.attr("x2", "100%");
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "#000")
.attr("stop-opacity", 0); // Transparent
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#000")
.attr("stop-opacity", 1); // Opaque
legendGroup.append("rect")
.attr("x", 10)
.attr("y", 80)
.attr("width", 100)
.attr("height", 10)
.style("fill", "url(#genderGradient)");
// Title next to gradient
legendGroup.append("text")
.attr("x", 115)
.attr("y", 89)
.style("font-size", "12px")
.text("Opacity: Gender Imbalance");
// Label below gradient
legendGroup.append("text")
.attr("x", 100)
.attr("y", 100)
.style("font-size", "10px")
.attr("text-anchor", "end")
.text("Imbalance Increases");
// Note below the graphic
svg.append("text")
.attr("x", width / 2)
.attr("y", 480) // instead of height - 10, place it just above the bottom
.attr("text-anchor", "middle")
.style("font-size", "12px")
.style("font-style", "italic")
.style("fill", "#555")
.text("If a ring is missing, it's because there was no Olympic data for that region in the selected year.");
return svg.node();
}