Public
Edited
May 4
1 star
Insert cell
Insert cell
data = FileAttachment("olympicdata2@1.csv").csv({ typed: true })
Insert cell
viewofyear = Inputs.range([1994,2016], {
step: 2,
label: "Year",
value: 1992,
format: d3.format("d")
})
Insert cell
year = Generators.input(viewofyear)
Insert cell
filteredData = data.filter(d => d.year === year)
Insert cell
// Create scales
medalScale = d3.scaleSqrt()
.domain(d3.extent(data, d => d.totalmedals))
.range([10, 100])
Insert cell
gdpScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.gdp))
.range([2, 20])
Insert cell
opacityScale = d => {
const total = d.female + d.male
if (total === 0) return 0 // handle missing data
const femalePct = d.female / total
return Math.abs(femalePct - 0.5) * 2 // Range: 0–1
}
Insert cell
svg = {
const width = 1200, height = 500; // Increased height to allow room for the title

const positions = {
"Oceania": [150, 150],
"Asia": [225, 250],
"Africa": [300, 150],
"Americas": [450, 150],
"Europe": [375, 250],
};

const colors = {
"Africa": "#000000", // Black
"Asia": "#FFFF00", // Yellow
"Oceania": "#0000FF", // Blue
"Americas": "#FF0000", // Red
"Europe": "#00FF00" // Green
};

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("background-color", "#ffffff");

// Add title
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?");

// Tooltip setup
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();
}
Insert cell
final = html`<div style="display: flex; flex-direction: column; align-items: center; gap: 20px;">
${viewofyear}
${svg}
</div>`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more