Public
Edited
Jun 27
Insert cell
Insert cell
data = FileAttachment("olympics_2014_2016.csv").csv({typed: true})
Insert cell
Insert cell
{
const medals = ["Gold", "Silver", "Bronze"];
const color = d3.scaleOrdinal()
.domain(medals)
.range(["#FFD700", "#C0C0C0", "#cd7f32"]);

const filtered = data.filter(d => d.Medal !== "");

const medalCounts = d3.rollups(
filtered,
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.length
}),
d => d.Team
);

const top10 = medalCounts
.sort((a, b) => d3.descending(a[1].Total, b[1].Total))
.slice(0, 10)
.map(([team, counts]) => ({
Team: team,
...counts
}));

const stackedData = d3.stack().keys(medals)(top10);

const margin = {top: 40, right: 30, bottom: 80, left: 60},
width = 800,
height = 500;

const x = d3.scaleBand()
.domain(top10.map(d => d.Team))
.range([0, width - margin.left - margin.right])
.padding(0.2);

const y = d3.scaleLinear()
.domain([0, d3.max(top10, d => d.Gold + d.Silver + d.Bronze)])
.nice()
.range([height - margin.top - margin.bottom, 0]);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

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

g.append("g")
.attr("transform", `translate(0,${height - margin.top - margin.bottom})`)
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "rotate(-40)")
.style("text-anchor", "end");

g.append("g")
.call(d3.axisLeft(y));

const tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "#fff")
.style("padding", "8px")
.style("border", "1px solid #ccc")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font-size", "12px")
.style("box-shadow", "0px 2px 5px rgba(0,0,0,0.1)")
.style("display", "none");

g.selectAll("g.stack")
.data(stackedData)
.join("g")
.attr("fill", d => color(d.key))
.selectAll("rect")
.data(d => d.map(v => ({...v, medalType: d.key})))
.join("rect")
.attr("x", d => x(d.data.Team))
.attr("y", d => y(d[1]))
.attr("height", d => y(d[0]) - y(d[1]))
.attr("width", x.bandwidth())
.on("mouseover", function (event, d) {
tooltip
.style("display", "block")
.html(`<strong>${d.data.Team}</strong><br>
${d.medalType === "Gold" ? "🥇 Oro: " + d.data.Gold
: d.medalType === "Silver" ? "🥈 Plata: " + d.data.Silver
: "🥉 Bronce: " + d.data.Bronze}`);
d3.select(this).attr("opacity", 0.7);
})
.on("mousemove", function (event) {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function () {
tooltip.style("display", "none");
d3.select(this).attr("opacity", 1);
});

const legend = svg.append("g")
.attr("transform", `translate(${width / 2 - 150}, 50)`);

medals.forEach((medal, i) => {
const legendRow = legend.append("g")
.attr("transform", `translate(${i * 100}, 0)`);

legendRow.append("rect")
.attr("width", 15)
.attr("height", 15)
.attr("fill", color(medal));

legendRow.append("text")
.attr("x", 20)
.attr("y", 12)
.text(medal)
.style("font-size", "12px")
.style("fill", "#333");
});

svg.append("text")
.attr("transform", `rotate(-90)`)
.attr("x", -(height / 2))
.attr("y", 15)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Cantidad de medallas");

svg.append("text")
.attr("x", width / 2)
.attr("y", height - 10)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("País");

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

{
const margin = {top: 60, right: 40, bottom: 60, left: 70},
width = 800,
height = 500;

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

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

const filtered = data.filter(d =>
d.Team === "Ireland" && d.Height && d.Weight && d.Sex
);

const sexes = ["M", "F"];
const color = d3.scaleOrdinal()
.domain(sexes)
.range(["#7FB3D5", "#F1948A"]);

const x = d3.scaleLinear()
.domain(d3.extent(filtered, d => d.Height)).nice()
.range([0, width - margin.left - margin.right]);

const y = d3.scaleLinear()
.domain(d3.extent(filtered, d => d.Weight)).nice()
.range([height - margin.top - margin.bottom, 0]);

g.append("g")
.attr("transform", `translate(0,${y.range()[0]})`)
.call(d3.axisBottom(x));
g.append("g")
.call(d3.axisLeft(y));

const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("background", "#fff")
.style("padding", "8px")
.style("border", "1px solid #ccc")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font-size", "12px")
.style("box-shadow", "0px 2px 5px rgba(0,0,0,0.1)")
.style("display", "none");

g.selectAll("circle")
.data(filtered)
.join("circle")
.attr("cx", d => x(d.Height))
.attr("cy", d => y(d.Weight))
.attr("r", 4)
.attr("fill", d => color(d.Sex))
.attr("opacity", 0.7)
.on("mouseover", function(event, d) {
tooltip.style("display", "block").html(
`<b>${d.Name}</b><br>${d.Sex === "M" ? "Hombre" : "Mujer"}<br>` +
`Altura: ${d.Height} cm<br>Peso: ${d.Weight} kg<br>Deporte: ${d.Sport}`
);
d3.select(this).attr("stroke", "#000").attr("stroke-width", 1.2);
})
.on("mousemove", function(event) {
tooltip.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY - 28}px`);
})
.on("mouseout", function() {
tooltip.style("display", "none");
d3.select(this).attr("stroke", null);
});

function linearRegression(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 sumX2 = d3.sum(data, d => Math.pow(xAccessor(d), 2));
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
const intercept = (sumY - slope * sumX) / n;
return {slope, intercept};
}

sexes.forEach(sex => {
const subset = filtered.filter(d => d.Sex === sex);
const {slope, intercept} = linearRegression(subset, d => d.Height, d => d.Weight);

const xVals = d3.extent(subset, d => d.Height);
const line = d3.line()
.x(d => x(d))
.y(d => y(slope * d + intercept));

g.append("path")
.datum(xVals)
.attr("fill", "none")
.attr("stroke", color(sex))
.attr("stroke-width", 2)
.attr("d", line);
});

svg.append("text")
.attr("x", width / 2)
.attr("y", height - 10)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Altura (cm)");

svg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -height / 2)
.attr("y", 15)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Peso (kg)");

svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Relación altura vs peso de atletas irlandeses");

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

sexes.forEach((sex, i) => {
const row = legend.append("g")
.attr("transform", `translate(0, ${i * 20})`);
row.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", color(sex));
row.append("text")
.attr("x", 18)
.attr("y", 10)
.text(sex === "M" ? "Hombres" : "Mujeres")
.style("font-size", "12px")
.attr("alignment-baseline", "middle");
});

return svg.node();
}

Insert cell
Insert cell
{
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();
}

Insert cell
Insert cell
{
const filtered = data.filter(d => d.Sex === "M" && d.Weight && d.Sport && d.Season);

const groupedSports = d3.group(filtered, d => d.Sport);

const sports = Array.from(groupedSports.entries())
.map(([sport, entries]) => {
const season = d3.median(entries, d => d.Season === "Winter" ? 0 : 1);
return {sport, season};
})
.sort((a, b) => d3.ascending(a.season, b.season) || d3.ascending(a.sport, b.sport))
.map(d => d.sport);

const seasons = ["Summer", "Winter"];
const margin = {top: 60, right: 150, bottom: 60, left: 200};
const width = 900;
const height = sports.length * 20 + margin.top + margin.bottom;

const x = d3.scaleLinear()
.domain(d3.extent(filtered, d => d.Weight)).nice()
.range([margin.left, width - margin.right]);

const y = d3.scalePoint()
.domain(sports)
.range([margin.top, height - margin.bottom])
.padding(1);

const color = d3.scaleOrdinal()
.domain(seasons)
.range(["#D4AC0D", "#3498DB"]);

const kde = kernelDensityEstimator(kernelEpanechnikov(5), x.ticks(40));

const grouped = d3.group(filtered, d => d.Sport, d => d.Season);
const densities = [];

for (const [sport, seasonsMap] of grouped.entries()) {
for (const [season, values] of seasonsMap.entries()) {
const weights = values.map(d => d.Weight);
densities.push({
sport,
season,
values: kde(weights)
});
}
}

const maxDensity = d3.max(densities, d => d3.max(d.values, v => v[1]));
const yWidth = d3.scaleLinear().domain([0, maxDensity]).range([0, 8]);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

svg.append("g")
.attr("transform", `translate(0,${margin.top})`)
.call(d3.axisTop(x).ticks(10).tickFormat(d => d + " kg"));

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(10).tickFormat(d => d + " kg"));

svg.append("g")
.attr("transform", `translate(${margin.left - 10},0)`)
.call(d3.axisLeft(y));

svg.append("g")
.selectAll("g")
.data(densities)
.join("g")
.attr("transform", d => `translate(0,${y(d.sport)})`)
.append("path")
.attr("fill", d => color(d.season))
.attr("fill-opacity", 0.4)
.attr("stroke", d => color(d.season))
.attr("stroke-width", 1.2)
.attr("d", d => d3.line()
.curve(d3.curveBasis)
(d.values.map(p => [x(p[0]), yWidth(p[1])]))
)
.clone(true)
.attr("d", d => d3.line()
.curve(d3.curveBasis)
(d.values.map(p => [x(p[0]), -yWidth(p[1])]))
);

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

seasons.forEach((s, i) => {
const g = legend.append("g").attr("transform", `translate(0, ${i * 20})`);
g.append("rect")
.attr("width", 15)
.attr("height", 15)
.attr("fill", color(s));
g.append("text")
.attr("x", 20)
.attr("y", 12)
.text(s)
.style("font-size", "12px")
.style("fill", "#333");
});

// Título
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 peso de atletas masculinos por deporte y temporada");

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();
}

Insert cell
Insert cell
{
const filtered = data.filter(d => d.NOC && d.ID && d.Season && d.Team);

const nested = d3.rollup(
filtered,
v => new Set(v.map(d => d.ID)).size,
d => d.Team,
d => d.Season
);

const countries = Array.from(nested, ([team, map]) => ({
team,
Summer: map.get("Summer") || 0,
Winter: map.get("Winter") || 0,
Total: (map.get("Summer") || 0) + (map.get("Winter") || 0)
}))
.sort((a, b) => d3.descending(a.Total, b.Total))
.slice(0, 15);

const margin = {top: 60, right: 160, bottom: 30, left: 180};
const width = 960;
const height = countries.length * 28 + margin.top + margin.bottom;

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const x = d3.scaleLinear()
.domain([0, d3.max(countries, d => Math.max(d.Summer, d.Winter))])
.nice()
.range([0, width - margin.left - margin.right]);

const y = d3.scaleBand()
.domain(countries.map(d => d.team))
.range([margin.top, height - margin.bottom])
.padding(0.2);

const color = d3.scaleOrdinal()
.domain(["Summer", "Winter"])
.range(["#D4AC0D", "#3498DB"]);

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

const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("background", "#333")
.style("color", "#fff")
.style("padding", "6px 10px")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("opacity", 0);

const showTooltip = (event, text) => {
tooltip.html(text)
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY - 20}px`)
.transition().duration(150)
.style("opacity", 1);
};

const hideTooltip = () => {
tooltip.transition().duration(150).style("opacity", 0);
};

g.selectAll("rect.summer")
.data(countries)
.join("rect")
.attr("x", 0)
.attr("y", d => y(d.team))
.attr("width", d => x(d.Summer))
.attr("height", y.bandwidth() / 2)
.attr("fill", color("Summer"))
.on("mousemove", (event, d) =>
showTooltip(event, `<strong>${d.team}</strong><br>Verano: ${d.Summer} atletas`)
)
.on("mouseout", hideTooltip);

g.selectAll("rect.winter")
.data(countries)
.join("rect")
.attr("x", 0)
.attr("y", d => y(d.team) + y.bandwidth() / 2)
.attr("width", d => x(d.Winter))
.attr("height", y.bandwidth() / 2)
.attr("fill", color("Winter"))
.on("mousemove", (event, d) =>
showTooltip(event, `<strong>${d.team}</strong><br>Invierno: ${d.Winter} atletas`)
)
.on("mouseout", hideTooltip);

g.append("g")
.call(d3.axisLeft(y).tickSize(0))
.selectAll("text")
.style("font-size", "12px")
.style("font-weight", "bold");

svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top - 10})`)
.call(d3.axisTop(x).ticks(6).tickFormat(d => d + " atletas"))
.call(g => g.select(".domain").remove());

svg.append("text")
.attr("x", width / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "bold")
.text("Top países por número de atletas en Juegos Olímpicos (Verano e Invierno)");

const legend = svg.append("g").attr("transform", `translate(${width - 130}, ${margin.top})`);
["Summer", "Winter"].forEach((s, i) => {
const row = legend.append("g").attr("transform", `translate(0, ${i * 20})`);
row.append("rect").attr("width", 12).attr("height", 12).attr("fill", color(s));
row.append("text").attr("x", 18).attr("y", 10).text(s).style("font-size", "12px");
});

return svg.node();
}

Insert cell
Insert cell
{
const invierno = data.filter(d =>
d.Season === "Winter" &&
d.Medal !== "None" &&
d.Medal !== "" &&
d.Team && d.Sport
);

const totalPorPais = d3.rollups(
invierno,
v => v.length,
d => d.Team
)
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 20)
.map(d => d[0]);

const deportes = Array.from(new Set(invierno.map(d => d.Sport)))
.sort();

const resumen = d3.rollups(
invierno,
v => v.length,
d => d.Team,
d => d.Sport
);
const flat = resumen.flatMap(([team, sports]) =>
sports.map(([sport, count]) => ({ Team: team, Sport: sport, Count: count }))
).filter(d => totalPorPais.includes(d.Team));

const margin = { top: 60, right: 230, bottom: 100, left: 140 };
const width = 1000;
const height = deportes.length * 30 + margin.top + margin.bottom;

const x = d3.scaleBand()
.domain(totalPorPais)
.range([margin.left, width - margin.right])
.padding(0.1);

const y = d3.scaleBand()
.domain(deportes)
.range([margin.top, height - margin.bottom])
.padding(0.1);

const maxCount = d3.max(flat, d => d.Count);
const r = d3.scaleSqrt()
.domain([0, maxCount])
.range([0, y.bandwidth() / 2]);

const color = d3.scaleSequential(d3.interpolateBlues)
.domain([0, maxCount]);

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))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end");

svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));

const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("background", "#333")
.style("color", "#fff")
.style("padding", "6px 10px")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("opacity", 0);

function showTip(event, d) {
tooltip.html(
`<strong>${d.Team}</strong><br/>
${d.Sport}: ${d.Count} medallas`
)
.style("left", `${event.pageX + 8}px`)
.style("top", `${event.pageY - 28}px`)
.transition().duration(100).style("opacity", 1);
}
function hideTip() {
tooltip.transition().duration(100).style("opacity", 0);
}

svg.append("g")
.selectAll("circle")
.data(flat)
.join("circle")
.attr("cx", d => x(d.Team) + x.bandwidth() / 2)
.attr("cy", d => y(d.Sport) + y.bandwidth() / 2)
.attr("r", d => r(d.Count))
.attr("fill", d => color(d.Count))
.attr("opacity", 0.8)
.on("mouseover", showTip)
.on("mouseout", hideTip);

const defs = svg.append("defs");
const grad = defs.append("linearGradient")
.attr("id", "grad")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "100%").attr("y2", "0%");
grad.append("stop").attr("offset", "0%").attr("stop-color", color(0));
grad.append("stop").attr("offset", "100%").attr("stop-color", color(maxCount));

const legendWidth = 200;
const legendHeight = 10;
const legendX = width - margin.right + 10;
const legendY = margin.top - 30;

svg.append("rect")
.attr("x", legendX)
.attr("y", legendY)
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "url(#grad)");

const legendScale = d3.scaleLinear()
.domain([0, maxCount])
.range([0, legendWidth]);

svg.append("g")
.attr("transform", `translate(${legendX}, ${legendY + legendHeight})`)
.call(d3.axisBottom(legendScale).ticks(5).tickFormat(d => d + " med"))
.selectAll("text")
.style("font-size", "10px");

svg.append("text")
.attr("x", legendX + legendWidth / 2)
.attr("y", legendY - 6)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Escala de medallas");

svg.append("text")
.attr("x", width / 2)
.attr("y", margin.top / 2)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Medallas de Invierno por país y deporte");

return svg.node();
}

Insert cell
Insert cell
{
const invierno = data.filter(d =>
d.Season === "Winter" &&
(d.Medal === "Gold" || d.Medal === "Silver" || d.Medal === "Bronze") &&
d.Team && d.Sport
);

const totalPorPais = d3.rollups(
invierno,
v => v.length,
d => d.Team
)
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 20)
.map(d => d[0]);

const deportes = d3.rollups(
invierno.filter(d => totalPorPais.includes(d.Team)),
v => v.length,
d => d.Sport
)
.sort((a, b) => d3.descending(a[1], b[1]))
.map(([sport]) => sport);

const detalle = d3.rollups(
invierno,
vs => {
const Gold = vs.filter(d => d.Medal === "Gold").length;
const Silver = vs.filter(d => d.Medal === "Silver").length;
const Bronze = vs.filter(d => d.Medal === "Bronze").length;
return { Gold, Silver, Bronze, total: Gold + Silver + Bronze };
},
d => d.Team,
d => d.Sport
);
const entries = detalle.flatMap(([team, arr]) =>
arr.map(([sport, c]) => ({ team, sport, ...c }))
).filter(d => totalPorPais.includes(d.team));

const margin = { top: 80, right: 120, bottom: 100, left: 140 };
const width = 1000;
const height = deportes.length * 30 + margin.top + margin.bottom;
const cellW = (width - margin.left - margin.right) / totalPorPais.length;
const barH = 20;

const xTeams = d3.scalePoint()
.domain(totalPorPais)
.range([margin.left + cellW/2, width - margin.right - cellW/2])
.padding(0.5);

const y = d3.scalePoint()
.domain(deportes)
.range([margin.top, height - margin.bottom])
.padding(0.5);

const maxTotal = d3.max(entries, d => d.total);
const xBar = d3.scaleLinear()
.domain([0, maxTotal])
.range([0, cellW * 0.9]);

const color = { Gold: "#FFD700", Silver: "#C0C0C0", Bronze: "#CD7F32" };

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom + 5})`)
.call(d3.axisBottom(xTeams))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.style("font-size", "10px");

svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.selectAll("text")
.style("font-size", "10px");

const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("background", "#333")
.style("color", "#fff")
.style("padding", "6px 10px")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("opacity", 0);

function showTip(event, d) {
tooltip.html(
`<strong>${d.team}</strong><br/>
${d.sport}<br/>
Total: ${d.total} medallas<br/>
🥇 ${d.Gold} · 🥈 ${d.Silver} · 🥉 ${d.Bronze}`
)
.style("left", `${event.pageX + 8}px`)
.style("top", `${event.pageY - 28}px`)
.transition().duration(100).style("opacity", 1);
}
function hideTip() {
tooltip.transition().duration(100).style("opacity", 0);
}

svg.append("g")
.selectAll("g")
.data(entries)
.join("g")
.attr("transform", d => {
const totalW = xBar(d.total);
const x0 = margin.left
+ totalPorPais.indexOf(d.team) * cellW
+ (cellW - totalW) / 2;
const y0 = y(d.sport) - barH/2;
return `translate(${x0},${y0})`;
})
.each(function(d) {
let offset = 0;
["Gold","Silver","Bronze"].forEach(type => {
const w = xBar(d[type]);
d3.select(this).append("rect")
.attr("x", offset)
.attr("y", 0)
.attr("width", w)
.attr("height", barH)
.attr("fill", color[type])
.on("mousemove", evt => showTip(evt, d))
.on("mouseout", hideTip);
offset += w;
});
});

const legendX = width - margin.right + 20;
const legendY = margin.top - 30;
const legendW = 100, legendH = 10;

svg.append("rect")
.attr("x", legendX)
.attr("y", legendY)
.attr("width", legendW)
.attr("height", legendH)
.style("fill", "none")
.style("stroke", "#000");

let lx = legendX;
["Gold","Silver","Bronze"].forEach(type => {
const segmentW = legendW / 3;
svg.append("rect")
.attr("x", lx)
.attr("y", legendY)
.attr("width", segmentW)
.attr("height", legendH)
.attr("fill", color[type]);
svg.append("text")
.attr("x", lx + segmentW/2)
.attr("y", legendY - 4)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.text(type);
lx += segmentW;
});

svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Composición de medallas por deporte y país (Top 20)");

return svg.node();
}

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