Public
Edited
Jun 30
Insert cell
Insert cell
data = FileAttachment("olympics_2014_2016.csv").csv({typed: true})
Insert cell
Insert cell
medalsByCountry = d3.rollup(data,
v => ({
Gold: v.filter(d => d.Medal === "Gold").length,
Silver: v.filter(d => d.Medal === "Silver").length,
Bronze: v.filter(d => d.Medal === "Bronze").length,
Total: v.filter(d => d.Medal !== null && d.Medal !== "None").length
}),
d => d.Team
);
Insert cell
medalData = Array.from(medalsByCountry, ([country, medals]) => ({
country,
...medals
}))
.filter(d => d.Total > 0)
.sort((a, b) => b.Total - a.Total)
.slice(0, 10);
Insert cell
{
const margin = { top: 20, right: 120, bottom: 40, left: 150 };
const width = 800 - margin.left - margin.right;
const height = 500 - margin.bottom - margin.top;

// Crear SVG y grupo contenedor
const svg = d3.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// Escalas
const xScale = d3.scaleLinear()
.domain([0, d3.max(medalData, d => d.Total)])
.range([0, width]);

const yScale = d3.scaleBand()
.domain(medalData.map(d => d.country))
.range([0, height])
.padding(0.1);

// Generador de pilas
const stack = d3.stack().keys(["Gold", "Silver", "Bronze"]);
const stackedData = stack(medalData);

// Colores de las medallas
const colorScale = d3.scaleOrdinal()
.domain(["Gold", "Silver", "Bronze"])
.range(["#FFD700", "#C0C0C0", "#CD7F32"]);

// Dibujar barras apiladas
g.selectAll(".medal-group")
.data(stackedData)
.join("g")
.attr("class", "medal-group")
.attr("fill", d => colorScale(d.key))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", d => xScale(d[0]))
.attr("y", d => yScale(d.data.country))
.attr("width", d => xScale(d[1]) - xScale(d[0]))
.attr("height", yScale.bandwidth())
.on("mouseover", function(event, d) {
const medalType = d3.select(this.parentNode).datum().key;
const value = d[1] - d[0];

d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.8)")
.style("color", "white")
.style("padding", "10px")
.style("border-radius", "5px")
.style("pointer-events", "none")
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px")
.html(`${d.data.country}<br/>${medalType}: ${value}`)
.transition().duration(200).style("opacity", 1);
})
.on("mouseout", () => d3.selectAll(".tooltip").remove());

// Eje X
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale).tickFormat(d3.format("d")));

// Eje Y
g.append("g").call(d3.axisLeft(yScale));

// Etiquetas de ejes
g.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 10)
.attr("x", -height / 2)
.attr("dy", "-1em")
.style("text-anchor", "middle")
.style("font-size", "12px")
.text("País");

g.append("text")
.attr("transform", `translate(${width / 2}, ${height + margin.bottom - 5})`)
.style("text-anchor", "middle")
.style("font-size", "12px")
.text("Número de Medallas");

// Título
svg.append("text")
.attr("x", (width + margin.left + margin.right) / 2)
.attr("y", margin.top / 1.5)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Top 10 Países por Medallas Olímpicas");

// Leyenda
const legend = svg.append("g")
.attr("transform", `translate(${width + margin.left + 20}, ${margin.top + 20})`);

const medalTypes = ["Gold", "Silver", "Bronze"];
legend.selectAll(".legend-item")
.data(medalTypes)
.join("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(0, ${i * 25})`)
.call(g => {
g.append("rect")
.attr("width", 18)
.attr("height", 18)
.attr("fill", d => colorScale(d));
g.append("text")
.attr("x", 25)
.attr("y", 9)
.attr("dy", "0.35em")
.style("font-size", "12px")
.text(d => d === "Gold" ? "Oro" : d === "Silver" ? "Plata" : "Bronce");
});

return svg.node(); // <- lo que se renderiza en Observable
}
Insert cell
Insert cell
//tu código

function filterIrelandAthletes(data) {
return data
.filter(d => d.Team === "Ireland" && d.Height && d.Weight && d.Height > 0 && d.Weight > 0)
.reduce((acc, current) => {
const existing = acc.find(item => item.ID === current.ID);
if (!existing) {
acc.push(current);
}
return acc;
}, []);
}

Insert cell
irelandData = filterIrelandAthletes(data);
Insert cell
maleAthletes = irelandData.filter(d => d.Sex === "M");

Insert cell
femaleAthletes = irelandData.filter(d => d.Sex === "F");
Insert cell
function calculateTrendLine(data, xAccessor, yAccessor) {
const n = data.length;
const sumX = d3.sum(data, xAccessor);
const sumY = d3.sum(data, yAccessor);
const sumXY = d3.sum(data, d => xAccessor(d) * yAccessor(d));
const sumXX = d3.sum(data, d => xAccessor(d) * xAccessor(d));
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
const intercept = (sumY - slope * sumX) / n;
return { slope, intercept };
}
Insert cell
function generateTrendLinePoints(trendLine, xDomain) {
return [
{ x: xDomain[0], y: trendLine.slope * xDomain[0] + trendLine.intercept },
{ x: xDomain[1], y: trendLine.slope * xDomain[1] + trendLine.intercept }
];
}
Insert cell
{ const margin = {top: 40, right: 170, bottom: 60, left: 70};
const width = 1000 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;

// Crear SVG
const svg = d3.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// Escalas
const xScale = d3.scaleLinear()
.domain(d3.extent(irelandData, d => d.Height)).nice()
.range([0, width]);

const yScale = d3.scaleLinear()
.domain(d3.extent(irelandData, d => d.Weight)).nice()
.range([height, 0]);

// Escala de colores por género
const colorScale = d3.scaleOrdinal()
.domain(["M", "F"])
.range(["#1f77b4", "#ff7f0e"]);

// Calcular líneas de tendencia
const maleTrendLine = maleAthletes.length > 1 ?
calculateTrendLine(maleAthletes, d => d.Height, d => d.Weight) : null;
const femaleTrendLine = femaleAthletes.length > 1 ?
calculateTrendLine(femaleAthletes, d => d.Height, d => d.Weight) : null;

// Crear grid
g.append("g")
.attr("class", "grid")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat("")
)
.style("stroke-dasharray", "3,3")
.style("opacity", 0.3);

g.append("g")
.attr("class", "grid")
.call(d3.axisLeft(yScale)
.tickSize(-width)
.tickFormat("")
)
.style("stroke-dasharray", "3,3")
.style("opacity", 0.3);

// Agregar líneas de tendencia
if (maleTrendLine) {
const maleLinePoints = generateTrendLinePoints(maleTrendLine, xScale.domain());
g.append("line")
.attr("class", "trend-line-male")
.attr("x1", xScale(maleLinePoints[0].x))
.attr("y1", yScale(maleLinePoints[0].y))
.attr("x2", xScale(maleLinePoints[1].x))
.attr("y2", yScale(maleLinePoints[1].y))
.style("stroke", colorScale("M"))
.style("stroke-width", 2)
.style("stroke-dasharray", "5,5")
.style("opacity", 0.8);
}

if (femaleTrendLine) {
const femaleLinePoints = generateTrendLinePoints(femaleTrendLine, xScale.domain());
g.append("line")
.attr("class", "trend-line-female")
.attr("x1", xScale(femaleLinePoints[0].x))
.attr("y1", yScale(femaleLinePoints[0].y))
.attr("x2", xScale(femaleLinePoints[1].x))
.attr("y2", yScale(femaleLinePoints[1].y))
.style("stroke", colorScale("F"))
.style("stroke-width", 2)
.style("stroke-dasharray", "5,5")
.style("opacity", 0.8);
}

// Crear círculos para cada atleta
g.selectAll(".athlete-circle")
.data(irelandData)
.enter().append("circle")
.attr("class", "athlete-circle")
.attr("cx", d => xScale(d.Height))
.attr("cy", d => yScale(d.Weight))
.attr("r", 6)
.attr("fill", d => colorScale(d.Sex))
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.style("opacity", 0.8)
.on("mouseover", function(event, d) {
// Resaltar el punto
d3.select(this)
.transition().duration(150)
.attr("r", 8)
.style("opacity", 1);

// Crear tooltip
const tooltip = d3.select("body").selectAll(".tooltip").data([0]);
const tooltipEnter = tooltip.enter().append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.9)")
.style("color", "white")
.style("padding", "12px")
.style("border-radius", "8px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("opacity", 0)
.style("box-shadow", "0 4px 8px rgba(0,0,0,0.3)");

const tooltipUpdate = tooltipEnter.merge(tooltip);
tooltipUpdate.transition().duration(200).style("opacity", 1);
tooltipUpdate.html(`
<strong>${d.Name}</strong><br/>
Altura: ${d.Height} cm<br/>
Peso: ${d.Weight} kg<br/>
Género: ${d.Sex === 'M' ? 'Masculino' : 'Femenino'}<br/>
Deporte: ${d.Sport || 'N/A'}
`)
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 15) + "px");
})
.on("mouseout", function(event, d) {
// Restaurar el punto
d3.select(this)
.transition().duration(150)
.attr("r", 6)
.style("opacity", 0.8);

// Remover tooltip
d3.select("body").selectAll(".tooltip")
.transition().duration(200)
.style("opacity", 0)
.remove();
});

// Ejes
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale))
.style("font-size", "12px");

g.append("g")
.call(d3.axisLeft(yScale))
.style("font-size", "12px");

// Etiquetas de los ejes
g.append("text")
.attr("transform", `translate(${width / 2}, ${height + 45})`)
.style("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text("Altura (cm)");

g.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left + 20)
.attr("x", 0 - (height / 2))
.style("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text("Peso (kg)");

// Título
svg.append("text")
.attr("x", (width + margin.left + margin.right) / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "bold")
.text("Relación Peso-Altura: Atletas Irlandeses");

// Leyenda
const legend = svg.append("g")
.attr("transform", `translate(${width + margin.left + 20}, ${margin.top + 50})`);

const genderData = [
{gender: "M", label: "Masculino", count: maleAthletes.length},
{gender: "F", label: "Femenino", count: femaleAthletes.length}
];

const legendItem = legend.selectAll(".legend-item")
.data(genderData)
.enter().append("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(0, ${i * 30})`);

legendItem.append("circle")
.attr("cx", 10)
.attr("cy", 0)
.attr("r", 6)
.attr("fill", d => colorScale(d.gender))
.attr("stroke", "white")
.attr("stroke-width", 1.5);

legendItem.append("text")
.attr("x", 25)
.attr("y", 0)
.attr("dy", "0.35em")
.style("font-size", "12px")
.text(d => `${d.label} (${d.count})`);

// Líneas de tendencia en la leyenda
if (maleTrendLine || femaleTrendLine) {
legend.append("text")
.attr("x", 0)
.attr("y", 80)
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Líneas de tendencia:");

if (maleTrendLine) {
legend.append("line")
.attr("x1", 10)
.attr("y1", 100)
.attr("x2", 40)
.attr("y2", 100)
.style("stroke", colorScale("M"))
.style("stroke-width", 2)
.style("stroke-dasharray", "5,5");

legend.append("text")
.attr("x", 45)
.attr("y", 100)
.attr("dy", "0.35em")
.style("font-size", "11px")
.text("Masculino");
}

if (femaleTrendLine) {
legend.append("line")
.attr("x1", 10)
.attr("y1", 120)
.attr("x2", 40)
.attr("y2", 120)
.style("stroke", colorScale("F"))
.style("stroke-width", 2)
.style("stroke-dasharray", "5,5");

legend.append("text")
.attr("x", 45)
.attr("y", 120)
.attr("dy", "0.35em")
.style("font-size", "11px")
.text("Femenino");
}
}

// Estadísticas en la leyenda
const stats = svg.append("g")
.attr("transform", `translate(${width + margin.left + 20}, ${margin.top + 200})`);

stats.append("text")
.attr("x", 0)
.attr("y", 0)
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Estadísticas:");

const maleStats = {
avgHeight: d3.mean(maleAthletes, d => d.Height),
avgWeight: d3.mean(maleAthletes, d => d.Weight)
};

const femaleStats = {
avgHeight: d3.mean(femaleAthletes, d => d.Height),
avgWeight: d3.mean(femaleAthletes, d => d.Weight)
};

if (maleStats.avgHeight) {
stats.append("text")
.attr("x", 0)
.attr("y", 20)
.style("font-size", "11px")
.text(`♂ Promedio: ${maleStats.avgHeight.toFixed(1)}cm, ${maleStats.avgWeight.toFixed(1)}kg`);
}

if (femaleStats.avgHeight) {
stats.append("text")
.attr("x", 0)
.attr("y", 35)
.style("font-size", "11px")
.text(`♀ Promedio: ${femaleStats.avgHeight.toFixed(1)}cm, ${femaleStats.avgWeight.toFixed(1)}kg`);
}

return svg.node();
}
Insert cell
Insert cell
//tu código

function filterBySeason(data, season) {
return data
.filter(d => d.Season === season && d.Height && d.Height > 0)
.reduce((acc, current) => {
// Eliminar duplicados basado en ID del atleta
const existing = acc.find(item => item.ID === current.ID);
if (!existing) {
acc.push(current);
}
return acc;
}, [])
.map(d => d.Height);
}
Insert cell
function kernelDensityEstimator(kernel, X) {
return function(V) {
return X.map(function(x) {
return [x, d3.mean(V, function(v) { return kernel(x - v); })];
});
};
}
Insert cell
function kernelEpanechnikov(k) {
return function(v) {
return Math.abs(v /= k) <= 1 ? 0.75 * (1 - v * v) / k : 0;
};
}

Insert cell
function calculateStats(data) {
const sorted = data.sort(d3.ascending);
return {
mean: d3.mean(data),
median: d3.quantile(sorted, 0.5),
q1: d3.quantile(sorted, 0.25),
q3: d3.quantile(sorted, 0.75),
min: d3.min(data),
max: d3.max(data),
std: d3.deviation(data),
count: data.length
};
}
Insert cell
summerHeights = filterBySeason(data, "Summer");
Insert cell
winterHeights = filterBySeason(data, "Winter")
Insert cell
summerStats = calculateStats(summerHeights);
Insert cell
winterStats = calculateStats(winterHeights);
Insert cell
{const margin = {top: 40, right: 150, bottom: 60, left: 70};
const width = 900 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;

// Crear SVG
const svg = d3.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// Dominio combinado para altura
const heightDomain = d3.extent([...summerHeights, ...winterHeights]);
const heightRange = d3.range(heightDomain[0], heightDomain[1] + 1, 1);

// Escalas
const xScale = d3.scaleLinear()
.domain(heightDomain).nice()
.range([0, width]);

// Calcular densidades
const bandwidth = 3; // Ancho de banda para el kernel
const kde = kernelDensityEstimator(kernelEpanechnikov(bandwidth), heightRange);
const summerDensity = kde(summerHeights);
const winterDensity = kde(winterHeights);

// Escala Y basada en la densidad máxima
const maxDensity = d3.max([...summerDensity, ...winterDensity], d => d[1]);
const yScale = d3.scaleLinear()
.domain([0, maxDensity]).nice()
.range([height, 0]);

// Colores para las temporadas
const colorScale = d3.scaleOrdinal()
.domain(["Summer", "Winter"])
.range(["#e74c3c", "#3498db"]);

// Crear áreas para las curvas de densidad
const area = d3.area()
.x(d => xScale(d[0]))
.y0(height)
.y1(d => yScale(d[1]))
.curve(d3.curveBasis);

const line = d3.line()
.x(d => xScale(d[0]))
.y(d => yScale(d[1]))
.curve(d3.curveBasis);

// Crear gradientes para las áreas
const defs = svg.append("defs");

const summerGradient = defs.append("linearGradient")
.attr("id", "summerGradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0).attr("y1", height)
.attr("x2", 0).attr("y2", 0);

summerGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", colorScale("Summer"))
.attr("stop-opacity", 0.6);

summerGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", colorScale("Summer"))
.attr("stop-opacity", 0.1);

const winterGradient = defs.append("linearGradient")
.attr("id", "winterGradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0).attr("y1", height)
.attr("x2", 0).attr("y2", 0);

winterGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", colorScale("Winter"))
.attr("stop-opacity", 0.6);

winterGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", colorScale("Winter"))
.attr("stop-opacity", 0.1);

// Grid de referencia
g.append("g")
.attr("class", "grid")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat("")
)
.style("stroke-dasharray", "2,2")
.style("opacity", 0.3);

// Área de verano
g.append("path")
.datum(summerDensity)
.attr("class", "area-summer")
.attr("d", area)
.style("fill", "url(#summerGradient)")
.style("opacity", 0.7);

// Área de invierno
g.append("path")
.datum(winterDensity)
.attr("class", "area-winter")
.attr("d", area)
.style("fill", "url(#winterGradient)")
.style("opacity", 0.7);

// Líneas de contorno
g.append("path")
.datum(summerDensity)
.attr("class", "line-summer")
.attr("d", line)
.style("fill", "none")
.style("stroke", colorScale("Summer"))
.style("stroke-width", 2.5)
.style("opacity", 0.9);

g.append("path")
.datum(winterDensity)
.attr("class", "line-winter")
.attr("d", line)
.style("fill", "none")
.style("stroke", colorScale("Winter"))
.style("stroke-width", 2.5)
.style("opacity", 0.9);

// Líneas de media
g.append("line")
.attr("class", "mean-summer")
.attr("x1", xScale(summerStats.mean))
.attr("x2", xScale(summerStats.mean))
.attr("y1", 0)
.attr("y2", height)
.style("stroke", colorScale("Summer"))
.style("stroke-width", 2)
.style("stroke-dasharray", "8,4")
.style("opacity", 0.8);

g.append("line")
.attr("class", "mean-winter")
.attr("x1", xScale(winterStats.mean))
.attr("x2", xScale(winterStats.mean))
.attr("y1", 0)
.attr("y2", height)
.style("stroke", colorScale("Winter"))
.style("stroke-width", 2)
.style("stroke-dasharray", "8,4")
.style("opacity", 0.8);

// Líneas de mediana
g.append("line")
.attr("class", "median-summer")
.attr("x1", xScale(summerStats.median))
.attr("x2", xScale(summerStats.median))
.attr("y1", 0)
.attr("y2", height)
.style("stroke", colorScale("Summer"))
.style("stroke-width", 1.5)
.style("stroke-dasharray", "4,2")
.style("opacity", 0.6);

g.append("line")
.attr("class", "median-winter")
.attr("x1", xScale(winterStats.median))
.attr("x2", xScale(winterStats.median))
.attr("y1", 0)
.attr("y2", height)
.style("stroke", colorScale("Winter"))
.style("stroke-width", 1.5)
.style("stroke-dasharray", "4,2")
.style("opacity", 0.6);

// Ejes
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale))
.style("font-size", "12px");

g.append("g")
.call(d3.axisLeft(yScale).tickFormat(d3.format(".3f")))
.style("font-size", "12px");

// Etiquetas de los ejes
g.append("text")
.attr("transform", `translate(${width / 2}, ${height + 45})`)
.style("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text("Altura (cm)");

g.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left + 20)
.attr("x", 0 - (height / 2))
.style("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text("Densidad");

// Título
svg.append("text")
.attr("x", (width + margin.left + margin.right) / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "bold")
.text("Distribución de Alturas: Juegos Olímpicos de Verano vs Invierno");

// Leyenda
const legend = svg.append("g")
.attr("transform", `translate(${width + margin.left + 20}, ${margin.top + 50})`);

// Elementos de la leyenda
const legendData = [
{season: "Summer", label: "Verano", stats: summerStats},
{season: "Winter", label: "Invierno", stats: winterStats}
];

const legendItem = legend.selectAll(".legend-item")
.data(legendData)
.enter().append("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(0, ${i * 120})`);

// Título de temporada
legendItem.append("text")
.attr("x", 0)
.attr("y", 0)
.style("font-size", "14px")
.style("font-weight", "bold")
.style("fill", d => colorScale(d.season))
.text(d => `${d.label} (n=${d.stats.count.toLocaleString()})`);

// Muestra de color
legendItem.append("rect")
.attr("x", 0)
.attr("y", 10)
.attr("width", 20)
.attr("height", 12)
.style("fill", d => colorScale(d.season))
.style("opacity", 0.7);

// Estadísticas
legendItem.append("text")
.attr("x", 0)
.attr("y", 35)
.style("font-size", "11px")
.text(d => `Media: ${d.stats.mean.toFixed(1)} cm`);

legendItem.append("text")
.attr("x", 0)
.attr("y", 50)
.style("font-size", "11px")
.text(d => `Mediana: ${d.stats.median.toFixed(1)} cm`);

legendItem.append("text")
.attr("x", 0)
.attr("y", 65)
.style("font-size", "11px")
.text(d => `Desv. Est.: ${d.stats.std.toFixed(1)} cm`);

legendItem.append("text")
.attr("x", 0)
.attr("y", 80)
.style("font-size", "11px")
.text(d => `Rango: ${d.stats.min}-${d.stats.max} cm`);

// Leyenda de líneas
const lineTypes = legend.append("g")
.attr("transform", "translate(0, 280)");

lineTypes.append("text")
.attr("x", 0)
.attr("y", 0)
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Líneas de referencia:");

// Media
lineTypes.append("line")
.attr("x1", 0)
.attr("y1", 20)
.attr("x2", 25)
.attr("y2", 20)
.style("stroke", "#666")
.style("stroke-width", 2)
.style("stroke-dasharray", "8,4");

lineTypes.append("text")
.attr("x", 30)
.attr("y", 20)
.attr("dy", "0.35em")
.style("font-size", "11px")
.text("Media");

// Mediana
lineTypes.append("line")
.attr("x1", 0)
.attr("y1", 40)
.attr("x2", 25)
.attr("y2", 40)
.style("stroke", "#666")
.style("stroke-width", 1.5)
.style("stroke-dasharray", "4,2");

lineTypes.append("text")
.attr("x", 30)
.attr("y", 40)
.attr("dy", "0.35em")
.style("font-size", "11px")
.text("Mediana");

// Interactividad para resaltar curvas
g.selectAll(".area-summer, .line-summer")
.on("mouseover", function() {
g.selectAll(".area-winter, .line-winter").style("opacity", 0.3);
g.selectAll(".area-summer, .line-summer").style("opacity", 1);
})
.on("mouseout", function() {
g.selectAll(".area-winter, .line-winter").style("opacity", 0.7);
g.selectAll(".area-summer, .line-summer").style("opacity", 0.7);
});

g.selectAll(".area-winter, .line-winter")
.on("mouseover", function() {
g.selectAll(".area-summer, .line-summer").style("opacity", 0.3);
g.selectAll(".area-winter, .line-winter").style("opacity", 1);
})
.on("mouseout", function() {
g.selectAll(".area-summer, .line-summer").style("opacity", 0.7);
g.selectAll(".area-winter, .line-winter").style("opacity", 0.7);
});

return svg.node();
}
Insert cell
Insert cell
//tu código


{
// Función para filtrar atletas masculinos y calcular promedios por deporte
function calculateSportAverages(data) {
// Filtrar atletas masculinos con peso válido
const maleAthletes = data.filter(d =>
d.Sex === "M" &&
d.Weight &&
d.Weight > 0 &&
d.Sport
);

// Eliminar duplicados por atleta (mismo ID)
const uniqueAthletes = maleAthletes.reduce((acc, current) => {
const existing = acc.find(item => item.ID === current.ID);
if (!existing) {
acc.push(current);
}
return acc;
}, []);

// Agrupar por deporte y temporada
const sportData = d3.rollup(uniqueAthletes,
athletes => ({
avgWeight: d3.mean(athletes, d => d.Weight),
count: athletes.length,
athletes: athletes
}),
d => d.Sport,
d => d.Season
);

// Convertir a formato útil
const results = [];
for (const [sport, seasons] of sportData) {
const summer = seasons.get("Summer");
const winter = seasons.get("Winter");
// Solo incluir deportes con al menos 5 atletas en alguna temporada
if ((summer && summer.count >= 5) || (winter && winter.count >= 5)) {
results.push({
sport,
summer: summer ? {
weight: summer.avgWeight,
count: summer.count
} : null,
winter: winter ? {
weight: winter.avgWeight,
count: winter.count
} : null
});
}
}

return results.sort((a, b) => {
const aMax = Math.max(a.summer?.weight || 0, a.winter?.weight || 0);
const bMax = Math.max(b.summer?.weight || 0, b.winter?.weight || 0);
return bMax - aMax;
});
}

// Función para crear efectos de glow
function createGlowFilter(defs, id, color) {
const filter = defs.append("filter")
.attr("id", id)
.attr("x", "-50%")
.attr("y", "-50%")
.attr("width", "200%")
.attr("height", "200%");

filter.append("feGaussianBlur")
.attr("stdDeviation", "3")
.attr("result", "coloredBlur");

const feMerge = filter.append("feMerge");
feMerge.append("feMergeNode").attr("in", "coloredBlur");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
}

const sportAverages = calculateSportAverages(data);

// Configuración del gráfico
const margin = {top: 60, right: 200, bottom: 80, left: 200};
const width = 1200 - margin.left - margin.right;
const height = Math.max(600, sportAverages.length * 35) - margin.top - margin.bottom;

// Crear SVG
const svg = d3.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)");

const defs = svg.append("defs");

// Crear filtros de glow
createGlowFilter(defs, "glow-summer", "#ff6b6b");
createGlowFilter(defs, "glow-winter", "#4ecdc4");

// Gradientes para las esferas
const summerGradient = defs.append("radialGradient")
.attr("id", "summerSphere")
.attr("cx", "30%")
.attr("cy", "30%");
summerGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "#ff9999");
summerGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#ff4757");

const winterGradient = defs.append("radialGradient")
.attr("id", "winterSphere")
.attr("cx", "30%")
.attr("cy", "30%");
winterGradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "#7bed9f");
winterGradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#2ed573");

const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// Escalas
const yScale = d3.scaleBand()
.domain(sportAverages.map(d => d.sport))
.range([0, height])
.padding(0.3);

const maxWeight = d3.max(sportAverages, d =>
Math.max(d.summer?.weight || 0, d.winter?.weight || 0)
);

const xScale = d3.scaleLinear()
.domain([0, maxWeight * 1.1])
.range([0, width]);

// Colores
const summerColor = "#ff6b6b";
const winterColor = "#4ecdc4";

// Crear fondo del gráfico
g.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "rgba(255,255,255,0.05)")
.attr("rx", 15);

const xTicks = xScale.ticks(8);
g.selectAll(".grid-line")
.data(xTicks)
.enter().append("line")
.attr("class", "grid-line")
.attr("x1", d => xScale(d))
.attr("x2", d => xScale(d))
.attr("y1", 0)
.attr("y2", height)
.style("stroke", "rgba(255,255,255,0.1)")
.style("stroke-width", 1);


const connections = g.selectAll(".connection")
.data(sportAverages.filter(d => d.summer && d.winter))
.enter().append("line")
.attr("class", "connection")
.attr("x1", d => xScale(Math.min(d.summer.weight, d.winter.weight)))
.attr("x2", d => xScale(Math.max(d.summer.weight, d.winter.weight)))
.attr("y1", d => yScale(d.sport) + yScale.bandwidth() / 2)
.attr("y2", d => yScale(d.sport) + yScale.bandwidth() / 2)
.style("stroke", "rgba(255,255,255,0.6)")
.style("stroke-width", 4)
.style("stroke-linecap", "round");

// Crear círculos para verano
const summerCircles = g.selectAll(".summer-circle")
.data(sportAverages.filter(d => d.summer))
.enter().append("circle")
.attr("class", "summer-circle")
.attr("cx", d => xScale(d.summer.weight))
.attr("cy", d => yScale(d.sport) + yScale.bandwidth() / 2)
.attr("r", 0)
.attr("fill", "url(#summerSphere)")
.style("filter", "url(#glow-summer)")
.style("cursor", "pointer");

// Crear círculos para invierno
const winterCircles = g.selectAll(".winter-circle")
.data(sportAverages.filter(d => d.winter))
.enter().append("circle")
.attr("class", "winter-circle")
.attr("cx", d => xScale(d.winter.weight))
.attr("cy", d => yScale(d.sport) + yScale.bandwidth() / 2)
.attr("r", 0)
.attr("fill", "url(#winterSphere)")
.style("filter", "url(#glow-winter)")
.style("cursor", "pointer");

// Animación de entrada
summerCircles.transition()
.duration(1000)
.delay((d, i) => i * 50)
.attr("r", d => Math.sqrt(d.summer.count) + 8);

winterCircles.transition()
.duration(1000)
.delay((d, i) => i * 50 + 25)
.attr("r", d => Math.sqrt(d.winter.count) + 8);


const sportLabels = g.selectAll(".sport-label")
.data(sportAverages)
.enter().append("text")
.attr("class", "sport-label")
.attr("x", -10)
.attr("y", d => yScale(d.sport) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.style("text-anchor", "end")
.style("font-family", "'Orbitron', monospace")
.style("font-size", "12px")
.style("font-weight", "500")
.style("fill", "white")
.style("text-shadow", "2px 2px 4px rgba(0,0,0,0.5)")
.text(d => d.sport);

// Etiquetas de peso en los círculos
g.selectAll(".summer-label")
.data(sportAverages.filter(d => d.summer))
.enter().append("text")
.attr("class", "summer-label")
.attr("x", d => xScale(d.summer.weight))
.attr("y", d => yScale(d.sport) + yScale.bandwidth() / 2 - 20)
.style("text-anchor", "middle")
.style("font-family", "'Orbitron', monospace")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", summerColor)
.style("text-shadow", "1px 1px 2px rgba(0,0,0,0.8)")
.text(d => `${d.summer.weight.toFixed(1)}kg`);

g.selectAll(".winter-label")
.data(sportAverages.filter(d => d.winter))
.enter().append("text")
.attr("class", "winter-label")
.attr("x", d => xScale(d.winter.weight))
.attr("y", d => yScale(d.sport) + yScale.bandwidth() / 2 + 35)
.style("text-anchor", "middle")
.style("font-family", "'Orbitron', monospace")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", winterColor)
.style("text-shadow", "1px 1px 2px rgba(0,0,0,0.8)")
.text(d => `${d.winter.weight.toFixed(1)}kg`);

// Eje X con estilo
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale).tickFormat(d => `${d}kg`))
.style("color", "white");

g.selectAll(".tick text")
.style("font-family", "'Orbitron', monospace")
.style("font-size", "11px");

// Título principal
svg.append("text")
.attr("x", (width + margin.left + margin.right) / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-family", "'Orbitron', monospace")
.style("font-size", "20px")
.style("font-weight", "bold")
.style("fill", "white")
.style("text-shadow", "2px 2px 4px rgba(0,0,0,0.5)")
.text("PESOS PROMEDIO: ATLETAS MASCULINOS POR DEPORTE");

// Subtítulo
svg.append("text")
.attr("x", (width + margin.left + margin.right) / 2)
.attr("y", 50)
.attr("text-anchor", "middle")
.style("font-family", "'Orbitron', monospace")
.style("font-size", "12px")
.style("fill", "rgba(255,255,255,0.8)")
.text("Comparación entre Juegos de Verano e Invierno");

const legend = svg.append("g")
.attr("transform", `translate(${width + margin.left + 30}, ${margin.top + 100})`);

// Fondo de la leyenda
legend.append("rect")
.attr("x", -15)
.attr("y", -15)
.attr("width", 160)
.attr("height", 120)
.attr("fill", "rgba(0,0,0,0.3)")
.attr("rx", 10)
.style("backdrop-filter", "blur(10px)");

legend.append("text")
.attr("x", 0)
.attr("y", 0)
.style("font-family", "'Orbitron', monospace")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("fill", "white")
.text("TEMPORADAS");

// Elementos de leyenda
const legendSummer = legend.append("g").attr("transform", "translate(0, 25)");
legendSummer.append("circle")
.attr("cx", 10)
.attr("cy", 0)
.attr("r", 8)
.attr("fill", "url(#summerSphere)")
.style("filter", "url(#glow-summer)");

legendSummer.append("text")
.attr("x", 25)
.attr("y", 0)
.attr("dy", "0.35em")
.style("font-family", "'Orbitron', monospace")
.style("font-size", "12px")
.style("fill", "white")
.text("Verano");

const legendWinter = legend.append("g").attr("transform", "translate(0, 50)");
legendWinter.append("circle")
.attr("cx", 10)
.attr("cy", 0)
.attr("r", 8)
.attr("fill", "url(#winterSphere)")
.style("filter", "url(#glow-winter)");

legendWinter.append("text")
.attr("x", 25)
.attr("y", 0)
.attr("dy", "0.35em")
.style("font-family", "'Orbitron', monospace")
.style("font-size", "12px")
.style("fill", "white")
.text("Invierno");

legend.append("text")
.attr("x", 0)
.attr("y", 80)
.style("font-family", "'Orbitron', monospace")
.style("font-size", "10px")
.style("fill", "rgba(255,255,255,0.7)")
.text("Tamaño = Nº atletas");

// Interactividad
function addInteractivity() {
svg.selectAll(".summer-circle, .winter-circle")
.on("mouseover", function(event, d) {
const season = d3.select(this).attr("class").includes("summer") ? "summer" : "winter";
const data = season === "summer" ? d.summer : d.winter;
d3.select(this)
.transition().duration(200)
.attr("r", d => (Math.sqrt(data.count) + 8) * 1.3);

const tooltip = d3.select("body").append("div")
.attr("class", "futuristic-tooltip")
.style("position", "absolute")
.style("background", "linear-gradient(135deg, rgba(0,0,0,0.9), rgba(30,30,30,0.9))")
.style("color", "white")
.style("padding", "15px")
.style("border-radius", "10px")
.style("border", `2px solid ${season === "summer" ? summerColor : winterColor}`)
.style("font-family", "'Orbitron', monospace")
.style("font-size", "12px")
.style("box-shadow", `0 0 20px ${season === "summer" ? summerColor : winterColor}`)
.style("pointer-events", "none")
.style("opacity", 0);

tooltip.transition().duration(200).style("opacity", 1);
tooltip.html(`
<div style="border-bottom: 1px solid ${season === "summer" ? summerColor : winterColor}; margin-bottom: 8px; padding-bottom: 5px;">
<strong>${d.sport}</strong> - ${season === "summer" ? "VERANO" : "INVIERNO"}
</div>
<div>Peso promedio: <span style="color: ${season === "summer" ? summerColor : winterColor};">${data.weight.toFixed(1)} kg</span></div>
<div>Atletas: <span style="color: ${season === "summer" ? summerColor : winterColor};">${data.count}</span></div>
`)
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 15) + "px");
})
.on("mouseout", function(event, d) {
const season = d3.select(this).attr("class").includes("summer") ? "summer" : "winter";
const data = season === "summer" ? d.summer : d.winter;
d3.select(this)
.transition().duration(200)
.attr("r", Math.sqrt(data.count) + 8);

d3.selectAll(".futuristic-tooltip")
.transition().duration(200)
.style("opacity", 0)
.remove();
});
}

setTimeout(addInteractivity, 2000);

// Etiqueta del eje X
g.append("text")
.attr("transform", `translate(${width / 2}, ${height + 60})`)
.style("text-anchor", "middle")
.style("font-family", "'Orbitron', monospace")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("fill", "white")
.style("text-shadow", "2px 2px 4px rgba(0,0,0,0.5)")
.text("PESO PROMEDIO (kg)");

return svg.node();
}
Insert cell
Insert cell
//tu código


{
// Función para contar atletas únicos por país y temporada
function countAthletesByCountry(data) {
// Agrupar por país y temporada, contando atletas únicos (por ID)
const countryData = d3.rollup(data,
athletes => new Set(athletes.map(d => d.ID)).size, // Contar IDs únicos
d => d.Team,
d => d.Season
);

// Convertir a formato útil
const results = [];
for (const [country, seasons] of countryData) {
const summer = seasons.get("Summer") || 0;
const winter = seasons.get("Winter") || 0;
// Solo incluir países con al menos 10 atletas en total
if (summer + winter >= 10) {
results.push({
country,
summer,
winter,
total: summer + winter
});
}
}

return results.sort((a, b) => b.total - a.total);
}

// Función para crear gradiente de fondo colorido
function createBackgroundGradient(defs) {
const gradient = defs.append("linearGradient")
.attr("id", "backgroundGradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "100%");

// Colores del gradiente (inspirado en el gráfico de referencia)
const colors = [
{offset: "0%", color: "#ff9a9e"},
{offset: "25%", color: "#fecfef"},
{offset: "50%", color: "#fecfef"},
{offset: "75%", color: "#a8edea"},
{offset: "100%", color: "#667eea"}
];

colors.forEach(c => {
gradient.append("stop")
.attr("offset", c.offset)
.attr("stop-color", c.color)
.attr("stop-opacity", 0.8);
});
}

const countryAthletes = countAthletesByCountry(data);

// Configuración del gráfico
const margin = {top: 60, right: 100, bottom: 80, left: 100};
const width = 800 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;

// Crear SVG
const svg = d3.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

const defs = svg.append("defs");
createBackgroundGradient(defs);

// Fondo con gradiente
svg.append("rect")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("fill", "url(#backgroundGradient)");

const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// Escalas
const maxAthletes = Math.max(
d3.max(countryAthletes, d => d.summer),
d3.max(countryAthletes, d => d.winter)
);

const xScale = d3.scaleLinear()
.domain([0, maxAthletes * 1.05])
.range([0, width]);

const yScale = d3.scaleLinear()
.domain([0, maxAthletes * 1.05])
.range([height, 0]);

// Escala de tamaño para los puntos (basada en total de atletas)
const sizeScale = d3.scaleSqrt()
.domain([0, d3.max(countryAthletes, d => d.total)])
.range([3, 12]);

// Grid de referencia sutil
const xTicks = xScale.ticks(8);
const yTicks = yScale.ticks(8);

g.selectAll(".grid-x")
.data(xTicks)
.enter().append("line")
.attr("class", "grid-x")
.attr("x1", d => xScale(d))
.attr("x2", d => xScale(d))
.attr("y1", 0)
.attr("y2", height)
.style("stroke", "rgba(255,255,255,0.3)")
.style("stroke-width", 0.5);

g.selectAll(".grid-y")
.data(yTicks)
.enter().append("line")
.attr("class", "grid-y")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", d => yScale(d))
.attr("y2", d => yScale(d))
.style("stroke", "rgba(255,255,255,0.3)")
.style("stroke-width", 0.5);

// Línea diagonal de referencia (donde summer = winter)
const diagonalLength = Math.min(xScale(maxAthletes), yScale(0));
g.append("line")
.attr("class", "diagonal-reference")
.attr("x1", 0)
.attr("y1", height)
.attr("x2", diagonalLength)
.attr("y2", height - diagonalLength)
.style("stroke", "rgba(0,0,0,0.4)")
.style("stroke-width", 2)
.style("stroke-dasharray", "8,4");

// Crear círculos para cada país
const circles = g.selectAll(".country-circle")
.data(countryAthletes)
.enter().append("circle")
.attr("class", "country-circle")
.attr("cx", d => xScale(d.winter))
.attr("cy", d => yScale(d.summer))
.attr("r", d => sizeScale(d.total))
.attr("fill", "rgba(70, 130, 180, 0.7)")
.attr("stroke", "white")
.attr("stroke-width", 2)
.style("cursor", "pointer");

// Etiquetas para países destacados (top 10)
const topCountries = countryAthletes.slice(0, 10);
const labels = g.selectAll(".country-label")
.data(topCountries)
.enter().append("text")
.attr("class", "country-label")
.attr("x", d => xScale(d.winter))
.attr("y", d => yScale(d.summer))
.attr("dx", d => sizeScale(d.total) + 8)
.attr("dy", "0.35em")
.style("font-family", "Arial, sans-serif")
.style("font-size", "12px")
.style("font-weight", "600")
.style("fill", "#2c3e50")
.style("text-shadow", "1px 1px 2px rgba(255,255,255,0.8)")
.text(d => d.country);

// Ejes
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale))
.style("color", "#2c3e50");

g.append("g")
.call(d3.axisLeft(yScale))
.style("color", "#2c3e50");

// Estilo de los ejes
g.selectAll(".tick text")
.style("font-family", "Arial, sans-serif")
.style("font-size", "11px")
.style("font-weight", "500");

g.selectAll(".domain")
.style("stroke", "#2c3e50")
.style("stroke-width", 2);

// Etiquetas de los ejes
g.append("text")
.attr("transform", `translate(${width / 2}, ${height + 60})`)
.style("text-anchor", "middle")
.style("font-family", "Arial, sans-serif")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text("# of Total Winter Athletes");

g.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left + 30)
.attr("x", 0 - (height / 2))
.style("text-anchor", "middle")
.style("font-family", "Arial, sans-serif")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text("# of Total Summer Athletes");

// Interactividad
circles
.on("mouseover", function(event, d) {
// Resaltar el círculo
d3.select(this)
.transition().duration(200)
.attr("r", sizeScale(d.total) * 1.3)
.attr("fill", "rgba(231, 76, 60, 0.8)");

// Crear tooltip
const tooltip = d3.select("body").append("div")
.attr("class", "country-tooltip")
.style("position", "absolute")
.style("background", "rgba(255,255,255,0.95)")
.style("color", "#2c3e50")
.style("padding", "12px")
.style("border-radius", "8px")
.style("border", "2px solid #3498db")
.style("font-family", "Arial, sans-serif")
.style("font-size", "13px")
.style("box-shadow", "0 4px 12px rgba(0,0,0,0.2)")
.style("pointer-events", "none")
.style("opacity", 0);

tooltip.transition().duration(200).style("opacity", 1);
// Determinar posición relativa a la línea diagonal
const winterVsSummer = d.winter > d.summer ? "más atletas en invierno" :
d.summer > d.winter ? "más atletas en verano" :
"equilibrado entre temporadas";

tooltip.html(`
<div style="border-bottom: 1px solid #3498db; margin-bottom: 8px; padding-bottom: 5px;">
<strong>${d.country}</strong>
</div>
<div><strong>Verano:</strong> ${d.summer.toLocaleString()} atletas</div>
<div><strong>Invierno:</strong> ${d.winter.toLocaleString()} atletas</div>
<div><strong>Total:</strong> ${d.total.toLocaleString()} atletas</div>
<div style="margin-top: 5px; font-style: italic; color: #7f8c8d;">
${winterVsSummer}
</div>
`)
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 15) + "px");
})
.on("mouseout", function(event, d) {
// Restaurar el círculo
d3.select(this)
.transition().duration(200)
.attr("r", sizeScale(d.total))
.attr("fill", "rgba(70, 130, 180, 0.7)");

// Remover tooltip
d3.selectAll(".country-tooltip")
.transition().duration(200)
.style("opacity", 0)
.remove();
});

// Información sobre la línea diagonal
g.append("text")
.attr("x", xScale(maxAthletes * 0.6))
.attr("y", yScale(maxAthletes * 0.5))
.attr("transform", `rotate(-45, ${xScale(maxAthletes * 0.6)}, ${yScale(maxAthletes * 0.5)})`)
.style("text-anchor", "middle")
.style("font-family", "Arial, sans-serif")
.style("font-size", "10px")
.style("font-style", "italic")
.style("fill", "rgba(0,0,0,0.6)")


// Leyenda del tamaño
const legend = svg.append("g")
.attr("transform", `translate(${width + margin.left - 80}, ${margin.top + 20})`);

legend.append("text")
.attr("x", 0)
.attr("y", 0)
.style("font-family", "Arial, sans-serif")
.style("font-size", "12px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text("Total de Atletas");

const sampleSizes = [10, 100, 300, 500, 700];
sampleSizes.forEach((size, i) => {
const y = 20 + i * 25;
legend.append("circle")
.attr("cx", 10)
.attr("cy", y)
.attr("r", sizeScale(size))
.attr("fill", "rgba(70, 130, 180, 0.7)")
.attr("stroke", "white")
.attr("stroke-width", 1);

legend.append("text")
.attr("x", 25)
.attr("y", y)
.attr("dy", "0.35em")
.style("font-family", "Arial, sans-serif")
.style("font-size", "10px")
.style("fill", "#2c3e50")
.text(size.toLocaleString());
});

// Estadísticas generales
const stats = svg.append("g")
.attr("transform", `translate(20, ${height + margin.top + 20})`);

const totalCountries = countryAthletes.length;
const summerDominant = countryAthletes.filter(d => d.summer > d.winter).length;
const winterDominant = countryAthletes.filter(d => d.winter > d.summer).length;

stats.append("text")
.attr("x", 300)
.attr("y", 15)
.style("font-family", "Arial, sans-serif")
.style("font-size", "11px")
.style("fill", "#2c3e50")
.text(`${totalCountries} países con 10+ atletas | ${summerDominant} más activos en verano | ${winterDominant} más activos en invierno`);

return svg.node();
}
Insert cell
Insert cell


{
// Función para procesar medallas de deportes de invierno
function processWinterMedals(data) {
// Filtrar solo deportes de invierno con medallas
const winterMedals = data.filter(d =>
d.Season === "Winter" &&
d.Medal &&
d.Medal !== "None" &&
d.Medal !== null &&
d.Team &&
d.Sport
);

// Agrupar por país y deporte, contando medallas por tipo
const medalData = d3.rollup(winterMedals,
medals => ({
Gold: medals.filter(d => d.Medal === "Gold").length,
Silver: medals.filter(d => d.Medal === "Silver").length,
Bronze: medals.filter(d => d.Medal === "Bronze").length,
Total: medals.length
}),
d => d.Team,
d => d.Sport
);

const countryTotals = new Map();
const sportSet = new Set();
for (const [country, sports] of medalData) {
let totalMedals = 0;
for (const [sport, medals] of sports) {
totalMedals += medals.Total;
sportSet.add(sport);
}
countryTotals.set(country, totalMedals);
}

// Obtener top 20 países por total de medallas
const top20Countries = Array.from(countryTotals.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(d => d[0]);

// Crear matriz de datos
const results = [];
const allSports = Array.from(sportSet).sort();

top20Countries.forEach(country => {
const countrySports = medalData.get(country);
allSports.forEach(sport => {
const medals = countrySports?.get(sport) || {Gold: 0, Silver: 0, Bronze: 0, Total: 0};
if (medals.Total > 0) {
results.push({
country,
sport,
...medals
});
}
});
});

return {
data: results,
countries: top20Countries,
sports: allSports
};
}

function createGradients(defs) {
// Gradiente de fondo invernal
const winterGradient = defs.append("radialGradient")
.attr("id", "winterBackground")
.attr("cx", "50%")
.attr("cy", "50%")
.attr("r", "50%");
winterGradient.append("stop").attr("offset", "0%").attr("stop-color", "#f8fbff");
winterGradient.append("stop").attr("offset", "100%").attr("stop-color", "#e8f4fd");
}

const processedData = processWinterMedals(data);
const { data: medalData, countries, sports } = processedData;

// Configuración del gráfico
const width = 1200;
const height = 800;
const margin = 100;
const radius = 250;
const centerX = width / 2 + 100;
const centerY = height / 2 + 50;
// Crear SVG
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("background", "url(#winterBackground)");

const defs = svg.append("defs");
createGradients(defs);

// Crear filtro de glow
const glowFilter = defs.append("filter")
.attr("id", "glow")
.attr("x", "-50%")
.attr("y", "-50%")
.attr("width", "200%")
.attr("height", "200%");

glowFilter.append("feGaussianBlur")
.attr("stdDeviation", "3")
.attr("result", "coloredBlur");

const feMerge = glowFilter.append("feMerge");
feMerge.append("feMergeNode").attr("in", "coloredBlur");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");

const g = svg.append("g")
.attr("transform", `translate(${centerX},${centerY})`);

// Escalas
const angleScale = d3.scaleBand()
.domain(sports)
.range([0, 2 * Math.PI])
.padding(0.05);

const maxMedals = d3.max(medalData, d => d.Total);
// Escala radial: las medallas determinan qué tan lejos del centro está
const radiusScale = d3.scaleLinear()
.domain([0, maxMedals])
.range([40, radius - 20]);

// Escala de colores por país
const countryColors = [
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf",
"#aec7e8", "#ffbb78", "#98df8a", "#ff9896", "#c5b0d5",
"#c49c94", "#f7b6d3", "#c7c7c7", "#dbdb8d", "#9edae5"
];
const countryColorScale = d3.scaleOrdinal()
.domain(countries)
.range(countryColors);

// Dibujar círculos concéntricos como referencia
const medalTicks = [1, 5, 10, 15, 20, 25];
medalTicks.forEach(tick => {
if (tick <= maxMedals) {
const r = radiusScale(tick);
g.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", r)
.style("fill", "none")
.style("stroke", "rgba(120,144,156,0.2)")
.style("stroke-width", 1)
.style("stroke-dasharray", "2,2");


}
});

// Dibujar líneas radiales (deportes)
sports.forEach(sport => {
const angle = angleScale(sport) + angleScale.bandwidth() / 2;
const x1 = Math.cos(angle - Math.PI/2) * 30;
const y1 = Math.sin(angle - Math.PI/2) * 30;
const x2 = Math.cos(angle - Math.PI/2) * radius;
const y2 = Math.sin(angle - Math.PI/2) * radius;

g.append("line")
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.style("stroke", "rgba(120,144,156,0.15)")
.style("stroke-width", 0.5);
});

// Crear las barras radiales
medalData.forEach(d => {
const angle = angleScale(d.sport) + angleScale.bandwidth() / 2;
const innerRadius = 30;
const outerRadius = radiusScale(d.Total);
const barWidth = angleScale.bandwidth() * 0.8;

const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(angle - barWidth/2)
.endAngle(angle + barWidth/2)
.cornerRadius(1);

g.append("path")
.datum(d)
.attr("class", "medal-bar")
.attr("d", arc)
.style("fill", countryColorScale(d.country))
.style("stroke", "white")
.style("stroke-width", 1)
.style("opacity", 0.8)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
// Resaltar barra
d3.select(this)
.transition().duration(200)
.style("opacity", 1)
.style("filter", "url(#glow)")
.style("stroke-width", 2);

// Tooltip
const tooltip = d3.select("body").append("div")
.attr("class", "radar-tooltip")
.style("position", "absolute")
.style("background", "linear-gradient(135deg, rgba(255,255,255,0.98), rgba(248,251,255,0.98))")
.style("color", "#263238")
.style("padding", "16px")
.style("border-radius", "12px")
.style("border", `2px solid ${countryColorScale(d.country)}`)
.style("font-family", "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif")
.style("font-size", "13px")
.style("box-shadow", "0 8px 24px rgba(0,0,0,0.15)")
.style("pointer-events", "none")
.style("opacity", 0)
.style("backdrop-filter", "blur(10px)");

tooltip.transition().duration(300).style("opacity", 1);
const medalPercentages = {
gold: Math.round((d.Gold / d.Total) * 100),
silver: Math.round((d.Silver / d.Total) * 100),
bronze: Math.round((d.Bronze / d.Total) * 100)
};

tooltip.html(`
<div style="border-bottom: 2px solid ${countryColorScale(d.country)}; margin-bottom: 10px; padding-bottom: 6px;">
<strong style="font-size: 15px;" data-country="${d.country}">${d.country}</strong>
<div style="font-size: 12px; color: #546e7a; margin-top: 2px;">${d.sport}</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 8px;">
<div style="text-align: center; padding: 6px; background: rgba(255, 215, 0, 0.1); border-radius: 6px;">
<div style="font-weight: bold; color: #ff8f00; font-size: 14px;">🥇 ${d.Gold}</div>
<div style="font-size: 10px; color: #666;">${medalPercentages.gold}%</div>
</div>
<div style="text-align: center; padding: 6px; background: rgba(192, 192, 192, 0.1); border-radius: 6px;">
<div style="font-weight: bold; color: #757575; font-size: 14px;">🥈 ${d.Silver}</div>
<div style="font-size: 10px; color: #666;">${medalPercentages.silver}%</div>
</div>
<div style="text-align: center; padding: 6px; background: rgba(205, 127, 50, 0.1); border-radius: 6px;">
<div style="font-weight: bold; color: #8d6e63; font-size: 14px;">🥉 ${d.Bronze}</div>
<div style="font-size: 10px; color: #666;">${medalPercentages.bronze}%</div>
</div>
</div>
<div style="text-align: center; font-weight: bold; font-size: 15px;" data-country="${d.country}">
Total: ${d.Total} medal${d.Total !== 1 ? 's' : ''}
</div>
`)
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 15) + "px");

// Aplicar color del país al texto
tooltip.selectAll("[data-country]")
.style("color", countryColorScale(d.country));
})
.on("mouseout", function() {
d3.select(this)
.transition().duration(200)
.style("opacity", 0.8)
.style("filter", "none")
.style("stroke-width", 1);

d3.selectAll(".radar-tooltip")
.transition().duration(200)
.style("opacity", 0)
.remove();
});
});

// Etiquetas de deportes (alrededor del perímetro)
const sportLabels = g.selectAll(".sport-label")
.data(sports)
.enter().append("text")
.attr("class", "sport-label")
.attr("transform", d => {
const angle = angleScale(d) + angleScale.bandwidth() / 2;
const r = radius + 30;
const x = Math.cos(angle - Math.PI/2) * r;
const y = Math.sin(angle - Math.PI/2) * r;
const rotation = (angle * 180 / Math.PI - 90);
return `translate(${x},${y}) rotate(${rotation > 90 ? rotation + 180 : rotation})`;
})
.style("text-anchor", d => {
const angle = angleScale(d) + angleScale.bandwidth() / 2;
const rotation = (angle * 180 / Math.PI - 90);
return rotation > 90 ? "end" : "start";
})
.style("alignment-baseline", "middle")
.style("font-family", "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif")
.style("font-size", "11px")
.style("font-weight", "500")
.style("fill", "#455a64")
.style("text-shadow", "1px 1px 3px rgba(255,255,255,0.9)")
.text(d => d.replace(/([A-Z])/g, ' $1').trim());

// Leyenda de países
const legend = svg.append("g")
.attr("transform", `translate(50, 80)`);

legend.append("rect")
.attr("x", -15)
.attr("y", -15)
.attr("width", 200)
.attr("height", Math.min(countries.length * 18 + 30, 500))
.attr("fill", "rgba(255,255,255,0.95)")
.attr("rx", 10)
.style("stroke", "#90a4ae")
.style("stroke-width", 1)
.style("filter", "url(#glow)");

legend.append("text")
.attr("x", 0)
.attr("y", 5)
.style("font-family", "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("fill", "#37474f")
.text("Top 20 Países");

// Elementos de la leyenda
const legendItems = legend.selectAll(".legend-item")
.data(countries.slice(0, 15)) // Mostrar solo los primeros 15 para que quepan
.enter().append("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(0, ${25 + i * 18})`);

legendItems.append("rect")
.attr("x", 0)
.attr("y", -6)
.attr("width", 12)
.attr("height", 12)
.attr("fill", d => countryColorScale(d))
.attr("stroke", "white")
.attr("stroke-width", 1)
.attr("rx", 2);

legendItems.append("text")
.attr("x", 18)
.attr("y", 0)
.attr("dy", "0.35em")
.style("font-family", "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif")
.style("font-size", "11px")
.style("fill", "#37474f")
.text(d => d);

// Título principal
svg.append("text")
.attr("x", width / 2)
.attr("y", 35)
.style("text-anchor", "middle")
.style("font-family", "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif")
.style("font-size", "20px")
.style("font-weight", "bold")
.style("fill", "#1565c0")
.style("text-shadow", "2px 2px 4px rgba(255,255,255,0.8)")
.text("Medallas por País y Deporte - Juegos Olímpicos de Invierno");



return svg.node();
}
Insert cell
Insert cell
//tu código
// el tooltip soluciona tambien este task :)
Insert cell
Insert cell
d3 = require('d3@7')
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