timeline = {
const width = 1000;
const height = 600;
const margin = { top: 70, right: 25, bottom: 35, left: 60 };
const centerY = height / 2;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("width", "100%");
const x = d3.scaleTime()
.domain(d3.extent(songs, d => d.releaseDate))
.range([margin.left, width - margin.right - 10]);
svg.append("line")
.attr("x1", margin.left)
.attr("x2", width - margin.right)
.attr("y1", centerY)
.attr("y2", centerY)
.attr("stroke", "#333")
.attr("stroke-width", 2);
const [minDate, maxDate] = d3.extent(songs, d => d.releaseDate);
svg.append("text")
.attr("x", x(minDate) - 45)
.attr("y", centerY + 20)
.attr("fill", "#333")
.attr("font-size", 15)
.attr("font-weight", "bold") // ← aquí ponemos negrita
.attr("text-anchor", "start") // ancla el texto a la izquierda del punto
.attr("transform", `rotate(0, ${x(minDate)}, ${centerY + 15})`)
.text(d3.timeFormat("%Y")(minDate));
// 3.c.4) Colocamos la etiqueta “2040” en la posición x(maxDate):
svg.append("text")
.attr("x", x(maxDate) + 30)
.attr("y", centerY + 20) // misma vertical que en el paso anterior
.attr("fill", "#333")
.attr("font-size", 15)
.attr("font-weight", "bold") // ← aquí ponemos negrita
.attr("text-anchor", "end") // ancla el texto a la derecha del punto
.attr("transform", `rotate(0, ${x(maxDate)}, ${centerY + 15})`)
.text(d3.timeFormat("%Y")(maxDate));
// 3d) Línea horizontal base del timeline
svg.append("line")
.attr("x1", margin.left)
.attr("x2", width - margin.right)
.attr("y1", centerY)
.attr("y2", centerY)
.attr("stroke", "#333")
.attr("stroke-width", 2);
// 3e) Elegimos el “cutoffDate” según el slider
const cutoffDate = new Date(songSlider);
const shownSongs = songs.filter(d => d.releaseDate <= cutoffDate);
// Buscar si hay alguna canción especial lanzada hasta ahora
const specialSong = shownSongs.find(d => specialReleases.has(d.A));
const specialMessage = specialSong ? specialReleases.get(specialSong.A) : null;
// 3f) Definimos un valor “spacing” (en píxeles) para separar cada par
// por ejemplo, 12 px entre cada par. Ajusta según te guste.
const spacing = 12;
// 3g) Dibujamos cada burbuja, pero calculamos `dy` según stackIndex
const bubbles = svg.append("g")
.selectAll("circle.bubble")
.data(shownSongs)
.join("circle")
.attr("class", "bubble")
.attr("cx", d => x(d.releaseDate))
// Ahora el `cy` = centerY + computeDy(d.stackIndex, spacing)
.attr("cy", d => centerY + computeDy(d.stackIndex, spacing))
.attr("r", d => d.notable ? 12 : 7)
.attr("fill", d => colorByOrigin.get(d.E) || "rgba(128, 128, 128, 0.5)")
.attr("stroke", "#fff")
.attr("stroke-width", 0.5);
// // 3) Limpia flechas y textos anteriores
// svg.selectAll(".special-arrow").remove();
// svg.selectAll(".special-label").remove();
// 4) Filtra las canciones especiales visibles
const specialSongs = shownSongs.filter(d => specialReleases.has(d.A));
// Obtener los mensajes, sin duplicados y ordenados (opcional)
const specialMessages = specialSongs.map(d => specialReleases.get(d.A));
// Eliminar mensajes anteriores
svg.selectAll(".special-message").remove();
const marginTop = 10;
const marginLeft = 10;
const lineHeight = 15;
specialMessages.forEach((msg, i) => {
svg.append("text")
.attr("class", "special-message")
.attr("x", marginLeft)
.attr("y", marginTop + i * lineHeight)
.attr("fill", "black")
// .attr("font-weight")
.attr("font-size", 10)
.text(msg);
});
// 3h) Volvemos a ligar el tooltip por si quieres mantenerlo
bubbles
.on("mouseover", (event, d) => {
const [px, py] = [event.pageX, event.pageY];
// 1) Obtenemos la descripción ampliada según d.origin
const originDesc = originLabel.get(d.E) || "(Origen desconocido)";
// 2) Obtener si es song o album (en minúsculas para mejor lectura)
const type = d.B ? d.B : "item";
// 3) Construir el texto completo del tooltip
const tooltipHTML = `${d.A} ; ${originDesc} (${type})`;
d3.select("#tooltip")
.style("opacity", 1)
.style("left", (px + 8) + "px")
.style("top", (py + 8) + "px")
.html(tooltipHTML);
})
.on("mousemove", (event, d) => {
const [px, py] = [event.pageX, event.pageY];
d3.select("#tooltip")
.style("left", (px + 8) + "px")
.style("top", (py + 8) + "px");
})
.on("mouseout", () => {
d3.select("#tooltip").style("opacity", 0);
});
return svg.node();
}