{
const filtered = data.filter(d => d.Height && d.Sex && d.Season);
const seasons = ["Summer", "Winter"];
const sexes = ["M", "F"];
const margin = {top: 80, right: 30, bottom: 60, left: 60};
const width = 800;
const height = 500;
const x = d3.scaleBand()
.domain(seasons.flatMap(s => sexes.map(sex => s + "-" + sex)))
.range([margin.left, width - margin.right])
.padding(0.5);
const y = d3.scaleLinear()
.domain(d3.extent(filtered, d => d.Height))
.nice()
.range([height - margin.bottom, margin.top]);
const color = d3.scaleOrdinal()
.domain(sexes)
.range(["#7FB3D5", "#F1948A"]);
const kde = kernelDensityEstimator(kernelEpanechnikov(5), y.ticks(40));
const grouped = d3.group(filtered, d => d.Season, d => d.Sex);
const densities = [];
for (const [season, sexGroup] of grouped.entries()) {
for (const [sex, values] of sexGroup.entries()) {
const heights = values.map(d => d.Height);
densities.push({
key: season + "-" + sex,
sex,
season,
values: kde(heights),
raw: values
});
}
}
const maxDensity = d3.max(densities, d => d3.max(d.values, v => v[1]));
const xWidth = d3.scaleLinear().domain([0, maxDensity]).range([0, x.bandwidth() / 2]);
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d => d.split("-")[0])) // Solo mostrar temporada
.selectAll("text")
.attr("font-size", "12px");
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Distribución de altura por sexo y temporada olímpica");
svg.append("text")
.attr("x", width / 2)
.attr("y", height - 15)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Temporada");
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -height / 2)
.attr("y", 15)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Altura (cm)");
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("background", "#fff")
.style("padding", "6px")
.style("border", "1px solid #aaa")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("display", "none");
svg.append("g")
.selectAll("path")
.data(densities)
.join("path")
.attr("transform", d => `translate(${x(d.key)},0)`)
.attr("fill", d => color(d.sex))
.attr("fill-opacity", 0.3)
.attr("stroke", d => color(d.sex))
.attr("stroke-width", 1.5)
.attr("d", d => d3.line()
.curve(d3.curveBasis)
(d.values.map(v => [xWidth(v[1]), y(v[0])]))
)
.clone(true)
.attr("d", d => d3.line()
.curve(d3.curveBasis)
(d.values.map(v => [-xWidth(v[1]), y(v[0])]))
);
svg.append("g")
.selectAll("circle")
.data(densities.flatMap(d => d.raw.map(p => ({...p, key: d.key}))))
.join("circle")
.attr("cx", d => x(d.key) + (Math.random() - 0.5) * 10)
.attr("cy", d => y(d.Height))
.attr("r", 2.2)
.attr("fill", d => color(d.Sex))
.attr("fill-opacity", 0.7)
.on("mouseover", function (event, d) {
tooltip
.html(`<strong>${d.Name}</strong><br/>${d.Height} cm<br/>${d.Sex === "M" ? "Hombre" : "Mujer"}, ${d.Season}`)
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY - 30 + "px")
.style("display", "block");
})
.on("mouseout", () => tooltip.style("display", "none"));
const legend = svg.append("g")
.attr("transform", `translate(${width - 150},${margin.top - 40})`);
sexes.forEach((sex, i) => {
const row = legend.append("g")
.attr("transform", `translate(0, ${i * 20})`);
row.append("rect")
.attr("width", 15)
.attr("height", 15)
.attr("fill", color(sex));
row.append("text")
.attr("x", 20)
.attr("y", 12)
.text(sex === "M" ? "Hombres" : "Mujeres")
.attr("font-size", "12px")
.attr("fill", "#333");
});
function kernelDensityEstimator(kernel, X) {
return function(V) {
return X.map(x => [x, d3.mean(V, v => kernel(x - v))]);
};
}
function kernelEpanechnikov(k) {
return v => Math.abs(v /= k) <= 1 ? 0.75 * (1 - v * v) / k : 0;
}
return svg.node();
}