{
function calculateSportAverages(data) {
const maleAthletes = data.filter(d =>
d.Sex === "M" &&
d.Weight &&
d.Weight > 0 &&
d.Sport
);
const uniqueAthletes = maleAthletes.reduce((acc, current) => {
const existing = acc.find(item => item.ID === current.ID);
if (!existing) {
acc.push(current);
}
return acc;
}, []);
const sportData = d3.rollup(uniqueAthletes,
athletes => ({
avgWeight: d3.mean(athletes, d => d.Weight),
count: athletes.length,
athletes: athletes
}),
d => d.Sport,
d => d.Season
);
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();
}