chart = {
const width = 900;
const height = 450;
const marginTop = 20;
const marginRight = 40;
const marginBottom = 34;
const marginLeft = 60;
const yearStep = 1;
const yearMin = d3.min(data, d => d.year);
const yearMax = d3.max(data, d => d.year);
const maxValue = d3.max(data, d => d.value);
const x = d3.scaleLinear()
.domain([-maxValue, maxValue])
.range([marginLeft, width - marginRight]);
const y = d3.scaleBand()
.domain(Array.from(d3.group(data, d => d.age).keys()).sort(d3.ascending))
.range([height - marginBottom, marginTop])
.paddingInner(0);
const rawGenerationMin = d3.min(data, d => d.year - d.age);
const rawGenerationMax = d3.max(data, d => d.year - d.age);
const groupSize = 20;
const groupMin = Math.floor(rawGenerationMin / groupSize) * groupSize;
const groupMax = Math.ceil(rawGenerationMax / groupSize) * groupSize;
// Escalas de color usando d3.scaleSequential y los interpoladores
const colorMale = d3.scaleSequential()
.domain([groupMin, groupMax])
.interpolator(d3.interpolateViridis);
const colorFemale = d3.scaleSequential()
.domain([groupMin, groupMax])
.interpolator(d3.interpolateInferno);
// Crear el contenedor SVG.
const svg = d3.create("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("width", width)
.attr("height", height)
.attr("style", `max-width: 100%; height: auto;`);
// Añadir los ejes.
// Eje Y (Edad) a la izquierda (principal)
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y)
.tickValues(d3.ticks(...d3.extent(data, d => d.age), height / 40))
.tickSizeOuter(0))
.call(g => g.append("text")
.attr("x", -marginLeft + 10)
.attr("y", marginTop - 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("font-size", "1.1em")
.text("Edad →"));
// Eje Y secundario para la "Generación" (derecha)
const yAxisRight = svg.append("g")
.attr("transform", `translate(${width - marginRight},0)`);
yAxisRight.append("text")
.attr("x", marginRight - 10)
.attr("y", marginTop - 10)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.attr("font-size", "1.1em")
.text("Generación →");
// Eje X (Población) en la parte inferior
const xAxisGroup = svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x)
.ticks(width / 80, "s")
.tickFormat(d => Math.abs(d)))
.call(g => g.select(".domain").remove());
xAxisGroup.append("text")
.attr("x", width - marginRight)
.attr("y", marginBottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.attr("font-size", "1.1em")
.text("Población →");
// Línea central en el 0 de la población (vertical)
svg.append("line")
.attr("x1", x(0))
.attr("x2", x(0))
.attr("y1", marginTop)
.attr("y2", height - marginBottom)
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.3)
.attr("stroke-dasharray", "2,2");
// Crear la etiqueta del año dinámico
const yearLabel = svg.append("text")
.attr("x", width - marginRight - 80)
.attr("y", marginTop + 40)
.attr("fill", "gray")
.attr("text-anchor", "end")
.attr("font-size", "2em")
.attr("font-weight", "bold");
// Grupo para las líneas de cuadrícula horizontales
const gridHorizontal = svg.append("g")
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
.attr("stroke-dasharray", "2,2");
// Grupo para las líneas de cuadrícula verticales
const gridVertical = svg.append("g")
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
.attr("stroke-dasharray", "2,2");
const group = svg.append("g");
let rect = group.selectAll("rect");
return Object.assign(svg.node(), {
update(year) {
const t = svg.transition()
.ease(d3.easeLinear)
.duration(delay);
rect = rect
.data(data.filter(d => d.year === year), d => `${d.sex}:${d.year - d.age}`)
.join(
enter => enter.append("rect")
.style("mix-blend-mode", "darken")
.attr("fill", d => {
const generation = year - d.age;
const generationGroup = Math.floor(generation / groupSize) * groupSize;
return d.sex === "M" ? colorMale(generationGroup) : colorFemale(generationGroup);
})
.attr("y", d => y(d.age))
.attr("height", y.bandwidth())
.attr("x", d => x(0))
.attr("width", 0),
update => update,
exit => exit.call(rect => rect.transition(t).remove()
.attr("x", d => x(0))
.attr("width", 0))
);
rect.transition(t)
.attr("y", d => y(d.age))
.attr("height", y.bandwidth())
.attr("x", d => d.sex === "M" ? x(-d.value) : x(0))
.attr("width", d => x(d.value) - x(0))
.attr("fill", d => {
const generation = year - d.age;
const generationGroup = Math.floor(generation / groupSize) * groupSize;
return d.sex === "M" ? colorMale(generationGroup) : colorFemale(generationGroup);
});
// Calcular los tickValues para las generaciones que terminan en cero para grid horizontal
const allAges = Array.from(d3.group(data, d => d.age).keys()).sort(d3.ascending);
const generationTickValues = allAges.filter(age => (year - age) % 20 === 0);
// Actualizar el eje Y secundario (Generación)
yAxisRight.call(d3.axisRight(y)
.tickValues(generationTickValues)
.tickFormat(d => year - d))
.call(g => g.select(".domain").remove());
// Actualizar las líneas de cuadrícula horizontales
gridHorizontal.selectAll("line")
.data(generationTickValues, d => d)
.join(
enter => enter.append("line")
.attr("y1", d => y(d) + y.bandwidth() / 2)
.attr("y2", d => y(d) + y.bandwidth() / 2)
.attr("x1", marginLeft)
.attr("x2", width - marginRight)
.attr("stroke-opacity", 0),
update => update,
exit => exit.call(line => line.transition(t)
.attr("stroke-opacity", 0)
.remove())
)
.transition(t)
.attr("y1", d => y(d) + y.bandwidth() / 2)
.attr("y2", d => y(d) + y.bandwidth() / 2)
.attr("x1", marginLeft)
.attr("x2", width - marginRight)
.attr("stroke-opacity", 0.1);
// Obtener los valores de los ticks del eje X (población)
const populationTickValues = x.ticks(width / 80);
// Actualizar las líneas de cuadrícula verticales
gridVertical.selectAll("line")
.data(populationTickValues, d => d)
.join(
enter => enter.append("line")
.attr("x1", d => x(d))
.attr("x2", d => x(d))
.attr("y1", marginTop)
.attr("y2", height - marginBottom)
.attr("stroke-opacity", 0),
update => update,
exit => exit.call(line => line.transition(t)
.attr("stroke-opacity", 0)
.remove())
)
.transition(t)
.attr("x1", d => x(d))
.attr("x2", d => x(d))
.attr("y1", marginTop)
.attr("y2", height - marginBottom)
.attr("stroke-opacity", 0.1);
// Actualizar la etiqueta del año dinámico
yearLabel.text(year);
},
scales: {
sexLabels: {
"M": "Hombres",
"F": "Mujeres"
},
colorMale,
colorFemale
}
});
}