Public
Edited
Jun 4
Insert cell
Insert cell
Importamos la BD
Insert cell
data = await FileAttachment("BD Consolidada (EJ1)@1.xlsx").xlsx()
Insert cell
sheets = data.sheet("Consolidated")
Insert cell
// Como se importa incorrectamente el header de las columnas como una fila más, elimino esa fila y dejo a los nombres de las
// columnas como las letras
// (*) Por supuesto que intenté importarlas correctamente o cambiarles el nombre pero no pude

rawSongs = sheets.slice(1)
Insert cell
rawSongs
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
songs = rawSongs
.filter(d => d.D != null && +d.D !== 0)
.map(d => ({
...d,
releaseDate: new Date(+d.D, 0, 1),
// notorietyDate: (+d.D)
// ? new Date(+d.D, 0, 1)
// : null,
notable: d.C === true
}))

Insert cell
d3 = require("d3@7")
Insert cell
minTs = d3.min(songs, d => d.releaseDate).getTime()
Insert cell
maxTs = d3.max(songs, d => d.releaseDate).getTime()
Insert cell

// Creamos un tooltip para distinguir el nombre de la canción

tooltip = {
// En Observable, esto crea un elemento <div id="tooltip"> con los estilos iniciales.
const div = html`<div id="tooltip" style="
position: absolute;
pointer-events: none;
background: rgba(0,0,0,0.7);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
line-height: 1.2em;
opacity: 0;
transition: opacity 0.1s;
"></div>`;
document.body.append(div);
return div;
}


Insert cell
// Agrupamos la cantidad de canciones por año (de esta manera, generamos un stack donde cada año tiene
// asignadas la cantidad de canciones publicadas en ese mismo año)
// Luego iteramos sobre cada año y cada canción recibe un índice según el orden en el que aparece

stackByYear = {
const byYear = new Map();
for (const song of songs) {
const year = song.releaseDate.getFullYear();
if (!byYear.has(year)) byYear.set(year, []);
byYear.get(year).push(song);
}
for (const [year, list] of byYear) {
list.forEach((songObj, i) => {
songObj.stackIndex = i;
});
}
return byYear;
}

Insert cell
// La idea es que cada canción en el gráfico tenga una posición horizontal segun el año y una
// vertical (de esta manera, las canciones del mismo año no se superponen, sino que se
// apilan)
// Ya que la BD original no tiene fechas específicas para la publicación de una canción,
// sino que tan solo su año, no nos importa el orden y ordenamos por órden de ocurrencia
// De esta manera, cuando aparezcan apiladas no nos importa el orden

// computeDy = (stackIndex, spacing) => {
// const pair = Math.floor(stackIndex / 2) + 1;
// const sign = (stackIndex % 2 === 0 ? -1 : +1);
// return sign * pair * spacing;
// }

Insert cell
// Definimos un "map" para asociar cada categoría de la columna "E" a un color y/o nivel de opacidad
// (*) Intentamos hacerlo con un diccionario pero no funcionó

colorByOrigin = new Map([
["Sailor Shift", "rgba(255, 215, 0, 1.0)"],
["Maya Jensen", "rgba(255, 182, 193, 0.4)"],
["Sophie Ramirez", "rgba(255, 182, 193, 0.4)"],
["Ivy Echos", "rgba(255, 182, 193, 1.0)"],
["IE Inspired On", "rgba(220, 20, 60, 1.0)"],
["IE Inspired By", "rgba( 0, 128, 0, 1.0)"],
["Others Work IE Inspirated On", "rgba(220, 20, 60, 0.4)"],
["Others Work IE Inspired By", "rgba( 0, 128, 0, 0.4)"],
["IE Contributors Other Work", "rgba(255, 140, 0, 0.4)"]
]);
Insert cell
originLabel = new Map([
["Sailor Shift", "Sailor Shift"],
["Maya Jensen", "Maya Jensen"],
["Sophie Ramirez", "Sophie Ramirez"],
["Ivy Echos", "Ivy Echo"],
["IE Inspired On", "Inspired on Ivy Echo or Sailor Shift"],
["IE Inspired By", "Inspired by Ivy Echo or Sailor Shift"],
["Others Work IE Inspirated On","Produced by someone IE or SS got inspired from"],
["Others Work IE Inspired By", "Produced by someone IE or SS inspired"],
["IE Contributors Other Work", "Produced by someone IE or SS contributed with"]
]);

Insert cell
specialReleases = new Map([
["The Kelp Forest Canticles", "Ivy Echo's first album (notable) is released"],
["Sunken Bell Song", "Ivy Echo's first song is released (not notable)"],
["Salt-Kissed Rhymes", "Ivy Echo's first notable (and only one) song is released"],
["High Tide Heartbeat", "Sailor Shift's first song is released (not notable)"],
["Electric Eel Love", "Sailor Shift's first notable song is released"],
["Seashell Serenade", "Sailor Shift's first song to reach notoriety is released"],
["Tidal Pop Waves", "Sailor Shift's notability albums' streak begins"],
["Ballads for the Low Tide", "Maya Jensen´s first notable album is released"],
["MJ Tidal Whisper", "Maya Jensen's first song to reach notoriety"],
["Anchored Love", "Sophie Ramirez's first (and only registered) produced song is released"]
]);

Insert cell
viewof songSlider = Inputs.range([minTs, maxTs], {
label: "Mostrar canciones hasta (año)",
value: minTs,
step: 1000 * 60 * 60 * 24 * 365,
format: d => d3.utcFormat("%Y")(new Date(d))
})
Insert cell
// 3) Dibujamos la línea de tiempo con burbujas “stackeadas” por año
timeline = {
const width = 1000;
const height = 600;
const margin = { top: 70, right: 25, bottom: 35, left: 60 };
const centerY = height / 2;

// 3a) Generamos el SVG
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("width", "100%");

// 3b) Escala de tiempo (eje X)
const x = d3.scaleTime()
.domain(d3.extent(songs, d => d.releaseDate))
.range([margin.left, width - margin.right - 10]);

// 3c) Ya no usamos d3.axisBottom. En su lugar:

// 3.c.1) Dibujamos la línea horizontal base del timeline (justo en centerY)
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);
// 3.c.2) Obtenemos las fechas extremas (Date) de nuestro dominio:
const [minDate, maxDate] = d3.extent(songs, d => d.releaseDate);
// 3.c.3) Colocamos la etiqueta “1975” en la posición x(minDate) + un pequeño ajuste si es necesario:
svg.append("text")
.attr("x", x(minDate) - 45)
.attr("y", centerY + 20) // 15 px por debajo de la línea central (ajústalo si la quieres arriba o más abajo)
.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();
}

Insert cell
// ------------------------------------------
// 1) Preprocesar: asignar a cada d.stackIndex
// ------------------------------------------
{
// 1a) Filtramos las canciones que ya vamos a mostrar (hasta songSlider)
const cutoffDate = new Date(songSlider);
const shownSongs = songs.filter(d => d.releaseDate <= cutoffDate);

// 1b) Agrupamos shownSongs por año (solo año, no la fecha completa)
// d.releaseDate.getFullYear() devuelve el año numérico, p.ej. 2033
const byYear = d3.group(shownSongs, d => d.releaseDate.getFullYear());

// 1c) Para cada grupo (cada año), recorremos sus elementos y le asignamos
// un índice secuencial (0,1,2,3...) en el orden en que estén
byYear.forEach((arrayDeCancionesDeUnAno) => {
arrayDeCancionesDeUnAno.forEach((d, i) => {
d.stackIndex = i;
});
});

// 1d) Ahora `shownSongs` tiene la propiedad `stackIndex` en cada elemento
// de manera que todas las canciones que comparten año X tendrán
// `d.stackIndex` = 0,1,2,... según cuántas haya.
// Retornamos mostradoSongs para alimentar la celda siguiente.
shownSongs;
}

Insert cell
// --------------------------------------------------
// 2) Definir la función computeDy(stackIndex, spacing)
// que calcula el desplazamiento vertical a partir
// del índice de “stack” y un espaciado fijo
// --------------------------------------------------
computeDy = (stackIndex, spacing) => {
// Alternamos arriba/abajo de la siguiente manera:
// stackIndex = 0 → desplaza +spacing (por ejemplo, 12px)
// stackIndex = 1 → desplaza –spacing
// stackIndex = 2 → desplaza +2*spacing
// stackIndex = 3 → desplaza –2*spacing
// stackIndex = 4 → desplaza +3*spacing
// stackIndex = 5 → desplaza –3*spacing
//
// Fórmula: el “nivel” será Math.floor((stackIndex+1)/2),
// y luego signo positivo si stackIndex par,
// signo negativo si stackIndex impar.
const level = Math.floor((stackIndex + 1) / 2);
return (stackIndex % 2 === 0 ? -1 : 1) * (level * spacing);
};

Insert cell
// --------------------------------------------------
// 3) Dibujar el timeline con las burbujas “stackeadas”
// --------------------------------------------------
timeline_2 = {
const width = 1500;
const height = 1000;
const margin = { top: 70, right: 20, bottom: 35, left: 60 };
const centerY = height / 2;

// 3a) Creamos el SVG
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("width", "100%");

// 3b) Escala de tiempo (eje X)
const x = d3.scaleTime()
.domain(d3.extent(songs, d => d.releaseDate))
.range([margin.left, width - margin.right]);

// 3c) Dibujamos el eje X
svg.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(
d3.axisBottom(x)
.ticks(d3.timeYear.every(5))
.tickFormat(d3.timeFormat("%Y"))
.tickSizeOuter(0)
)
.selectAll("text")
.attr("transform", "rotate(-45)")
.attr("dx", "-0.5em")
.attr("dy", "0.25em")
.attr("text-anchor", "end");

// 3d) Línea horizontal central
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) Filtramos de nuevo las canciones que podemos mostrar
const cutoffDate = new Date(songSlider);
const shownSongs = songs.filter(d => d.releaseDate <= cutoffDate);

// 3f) Volvemos a aplicar computeDy pero ahora los d.stackIndex YA están asignados
const spacing = 20; // 12 píxeles por nivel de apilamiento (ajústalo a tu gusto)

// 3g) Dibujamos cada burbuja, usando d.stackIndex para saber cuánto subir o bajar
const bubbles = svg.append("g")
.selectAll("circle.bubble")
.data(shownSongs)
.join("circle")
.attr("class", "bubble")
.attr("cx", d => x(d.releaseDate))
.attr("cy", d => centerY + computeDy(d.stackIndex, spacing))
.attr("r", d => d.notable ? 12 : 7)
.attr("fill", d => {
// Usamos el Map colorByOrigin para asignar colores según d.E (origin)
return colorByOrigin.get(d.E) || "rgba(128, 128, 128, 0.5)";
})
.attr("stroke", "#fff")
.attr("stroke-width", 0.5);

// 3h) El tooltip sigue igual que antes
bubbles
.on("mouseover", (event, d) => {
const [px, py] = [event.pageX, event.pageY];
const originDesc = originLabel.get(d.E) || "(Origen desconocido)";
const type = d.notable ? "notable" : "no-notable";
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();
}

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