Public
Edited
Jun 8
1 fork
Insert cell
Insert cell
Insert cell
mc1_graph = FileAttachment("MC1_graph.json").json()
Insert cell
graph = FileAttachment("MC1_graph.json").json()
Insert cell
graph.nodes.length // cantidad de nodos
Insert cell
graph.links.length // cantidad de relaciones (edges)
Insert cell
MC1_graph.json
X*
Y*
Color
Size
Facet X
Facet Y
Mark
Auto
Type Chart, then Shift-Enter. Ctrl-space for more options.

Insert cell
songsFolk = graph.nodes.filter(d =>
(d["Node Type"] === "Song" || d["Node Type"] === "Album") &&
d.genre === "Oceanus Folk"
)


Insert cell
Insert cell
Insert cell
tiposDeInfluencia = [
"InStyleOf",
"CoverOf",
"LyricalReferenceTo",
"InterpolatesFrom",
"DirectlySamples"
]

Insert cell
folkIDs = new Set(songsFolk.map(d => d.id)) // ahora es Set<number>

Insert cell
influenciasSalientes = graph.links.filter(d =>
folkIDs.has(d.source) && tiposDeInfluencia.includes(d["Edge Type"])
)

Insert cell
influenciasSalientes.length

/*influenciasSalientes.length = 371 indica que encontraste 371 relaciones de influencia desde canciones o álbumes de Oceanus Folk hacia otras obras.

songsFolk: 305 canciones/álbumes Oceanus Folk

folkIDs: Set de IDs de esos nodos

influenciasSalientes: 371 relaciones de influencia saliente
*/

Insert cell
influenciados = influenciasSalientes
.map(d => {
const nodoOrigen = graph.nodes.find(n => n.id === d.source);
const nodoDestino = graph.nodes.find(n => n.id === d.target);
if (!nodoOrigen || !nodoDestino) return null;

// solo si el origen es Oceanus Folk
if (nodoOrigen.genre !== "Oceanus Folk") return null;

return {
id: d.target,
genre: nodoDestino?.genre ?? "Desconocido",
year: nodoDestino?.release_date || nodoDestino?.written_date || "Desconocido"
};
})
.filter(d => d && d.genre && d.year);


Insert cell
Insert cell
influenciados1 = influenciasSalientes
.map(d => {
const nodoOrigen = graph.nodes.find(n => n.id === d.source);
const nodoDestino = graph.nodes.find(n => n.id === d.target);

if (!nodoOrigen || !nodoDestino) return null;

// Solo si el origen es Oceanus Folk y el destino NO es del mismo género
if (nodoOrigen.genre === "Oceanus Folk" && nodoDestino.genre !== "Oceanus Folk") {
return {
id: d.target,
genre: nodoDestino.genre,
year: nodoDestino.release_date || nodoDestino.written_date || "Desconocido"
};
}

return null;
})
.filter(d => d && d.genre && d.year);


Insert cell
data1
X*
Y*
Color
Size
Facet X
Facet Y
Mark
Auto
Type Chart, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Plot.plot({
marginLeft: 140, // ajusta este valor para más espacio a los nombres
y: {label: "Año", type: "band"},
x: {label: "Cantidad de obras influenciadas"},
marks: [
Plot.barX(
d3.rollups(influenciados1, v => v.length, d => d.year)
.map(([year, count]) => ({year, count}))
.sort((a, b) => a.year - b.year),
{y: "year", x: "count"}
)
]
})


//🔎 Si ves que sube progresivamente → influencia gradual
//📉 Si hay picos → influencia intermitente

Insert cell
Plot.plot({
marginLeft: 140,
height: 500,
y: {label: "Año", type: "band"},
x: {
label: "Cantidad de obras influenciadas",
grid: true
},
style: {
fontFamily: "sans-serif",
fontSize: "12px"
},
color: {
type: "linear",
scheme: "blues",
label: "Cantidad",
legend: true
},
marks: [
Plot.barX(
d3.rollups(influenciados1, v => v.length, d => d.year)
.map(function(d) { return {year: d[0], count: d[1]}; })
.sort(function(a, b) { return a.year - b.year; }),
{
x: "count",
y: "year",
fill: "count"
}
),
Plot.text(
d3.rollups(influenciados1, v => v.length, d => d.year)
.map(function(d) { return {year: d[0], count: d[1]}; })
.sort(function(a, b) { return a.year - b.year; }),
{
x: function(d) { return d.count + 2; },
y: "year",
text: function(d) { return d.count; },
fill: "black",
textAnchor: "start",
fontSize: 10
}
)
]
})

Insert cell
Plot.plot({
marginLeft: 140,
height: 500,
y: {label: "Año", type: "band"},
x: {
label: "Cantidad de obras influenciadas",
grid: true
},
color: {
type: "linear",
scheme: "blues",
label: "Cantidad",
legend: true
},
style: { fontFamily: "sans-serif", fontSize: "12px" },
marks: [
Plot.barX(
d3.rollups(influenciados1, v => v.length, d => d.year)
.map(([year, count]) => ({year, count}))
.sort((a, b) => a.year - b.year),
{x: "count", y: "year", fill: "count"}
)
]
});

Insert cell
Plot.plot({
width: 700,
height: 400,
x: {label: "Año", type: "linear"},
y: {label: "Cantidad de obras influenciadas"},
marks: [
Plot.line(
d3.rollups(influenciados1, v => v.length, d => +d.year)
.map(d => ({year: d[0], count: d[1]}))
.sort((a, b) => a.year - b.year),
{x: "year", y: "count", stroke: "#5e60ce"}
),
Plot.dot(
d3.rollups(influenciados1, v => v.length, d => +d.year)
.map(d => ({year: d[0], count: d[1]}))
.sort((a, b) => a.year - b.year),
{x: "year", y: "count", fill: "#5e60ce"}
),
Plot.text(
d3.rollups(influenciados1, v => v.length, d => +d.year)
.map(d => ({year: d[0], count: d[1]}))
.sort((a, b) => a.year - b.year),
{
x: "year",
y: d => d.count,
text: d => d.count,
dy: -8,
fontSize: 10
}
)
]
});

Insert cell
Plot.plot({
marginLeft: 140,
height: 500,
y: {label: "Año", type: "band"},
x: {label: "Cantidad (tamaño de burbuja)"},
marks: [
Plot.dot(
d3.rollups(influenciados1, v => v.length, d => d.year)
.map(d => ({year: d[0], count: d[1]}))
.sort((a, b) => a.year - b.year),
{
x: "count",
y: "year",
r: d => Math.sqrt(d.count) * 2,
fill: "#48bfe3"
}
),
Plot.text(
d3.rollups(influenciados1, v => v.length, d => d.year)
.map(d => ({year: d[0], count: d[1]}))
.sort((a, b) => a.year - b.year),
{
x: d => d.count + 2,
y: "year",
text: d => d.count,
fill: "black",
fontSize: 10,
textAnchor: "start"
}
)
]
});

Insert cell
Plot.plot({
height: 100,
width: 700,
x: {label: "Año", type: "band"},
color: {
type: "linear",
scheme: "oranges",
label: "Cantidad"
},
marks: [
Plot.cell(
d3.rollups(influenciados1, v => v.length, d => d.year)
.map(d => ({year: d[0], count: d[1]}))
.sort((a, b) => a.year - b.year),
{
x: "year",
fill: "count",
inset: 0.5,
stroke: "#fff"
}
),
Plot.text(
d3.rollups(influenciados1, v => v.length, d => d.year)
.map(d => ({year: d[0], count: d[1]}))
.sort((a, b) => a.year - b.year),
{
x: "year",
text: d => d.count,
fill: "black",
dy: 10,
fontSize: 10
}
)
]
});

Insert cell
Insert cell
Plot.plot({
marginLeft: 140, // ajusta este valor para más espacio a los nombres
x: {label: "Cantidad"},
y: {label: "Género", type: "band"},
marks: [
Plot.barX(
d3.rollups(influenciados1, v => v.length, d => d.genre)
.map(([genre, count]) => ({genre, count}))
.sort((a, b) => d3.descending(a.count, b.count)),
{x: "count", y: "genre"}
)
]
})


Insert cell
Plot.plot({
marginLeft: 140,
x: {label: "Cantidad"},
y: {label: "Género", type: "band"},
marks: [
// Barras horizontales
Plot.barX(
d3.rollups(influenciados1, v => v.length, d => d.genre)
.map(([genre, count]) => ({genre, count}))
.sort((a, b) => d3.descending(a.count, b.count)),
{x: "count", y: "genre"}
),
// Etiquetas con el número
Plot.text(
d3.rollups(influenciados1, v => v.length, d => d.genre)
.map(([genre, count]) => ({genre, count}))
.sort((a, b) => d3.descending(a.count, b.count)),
{
x: d => d.count + 1, // posición a la derecha de la barra
y: "genre",
text: d => d.count,
fill: "black",
textAnchor: "start",
fontSize: 10
}
)
]
});

Insert cell
Plot.plot({
height: 500,
marginLeft: 160,
x: {label: "Cantidad"},
y: {label: "Género", type: "band"},
color: {
type: "linear",
scheme: "plasma",
label: "Cantidad",
legend: true
},
marks: [
Plot.cell(
d3.rollups(influenciados1, v => v.length, d => d.genre)
.map(([genre, count]) => ({genre, count})),
{x: "count", y: "genre", fill: "count"}
),
Plot.text(
d3.rollups(influenciados1, v => v.length, d => d.genre)
.map(([genre, count]) => ({genre, count})),
{
x: "count",
y: "genre",
text: d => d.count,
fontSize: 10,
fill: "black",
textAnchor: "middle"
}
)
]
});

Insert cell
// Celda: data de géneros influenciados
genresTreemapData = [
{ genre: "Indie Folk", count: 100 },
{ genre: "Dream Pop", count: 15 },
{ genre: "Synthwave", count: 19 },
{ genre: "Doom Metal", count: 14 },
{ genre: "Desert Rock", count: 9 },
{ genre: "Jazz Surf Rock", count: 1 },
{ genre: "Emo/Pop Punk", count: 1 },
{ genre: "Celtic Folk", count: 1 },
{ genre: "Southern Gothic Rock", count: 4 }
]

Insert cell
// Celda: genera el treemap
{
const width = 1000;
const height = 400;

const root = d3.hierarchy({children: genresTreemapData})
.sum(d => d.count);

d3.treemap()
.size([width, height])
.padding(2)(root);

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

const color = d3.scaleOrdinal(d3.schemeTableau10);

const node = svg.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`);

node.append("rect")
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)
.attr("fill", d => color(d.data.genre));

node.append("text")
.attr("x", 4)
.attr("y", 14)
.text(d => `${d.data.genre} (${d.data.count})`)
.attr("fill", "white")
.style("font-size", "12px");

return svg.node();
}

Insert cell
Insert cell
// Tipos reales de nodos:
Array.from(new Set(graph.nodes.map(d => d["Node Type"])))
Insert cell
// Paso 1: Conseguir los IDs de obras influenciadas
obrasInfluenciadas = influenciasSalientes.map(d => d.target)
Insert cell
// Paso 2: Buscar relaciones PerformerOf sobre esas obras
performances = graph.links.filter(d =>
obrasInfluenciadas.includes(d.target) && d["Edge Type"] === "PerformerOf"
)
Insert cell
// Paso 3: Mapear performers únicos
performers = performances.map(d => {
const persona = graph.nodes.find(n => n.id === d.source && n["Node Type"] === "Person")
return persona?.stage_name || persona?.name
}).filter(Boolean)

Insert cell
// Paso 4: Contar cuántas veces aparece cada artista
conteoArtistas = d3.rollups(
performers,
v => v.length,
d => d
).map(([name, count]) => ({name, count}))
.sort((a, b) => d3.descending(a.count, b.count))

Insert cell
// Visualización: artistas más influenciados por obras de Oceanus Folk
Plot.plot({
marginLeft: 160,
x: {label: "Cantidad"},
y: {label: "Artista", type: "band"},
marks: [
Plot.barX(conteoArtistas.slice(0, 10), {x: "count", y: "name"})
]
})

Insert cell
Plot.plot({
marginLeft: 160,
x: {label: "Cantidad"},
y: {label: "Artista", type: "band"},
marks: [
Plot.barX(conteoArtistas.slice(0, 10), {x: "count", y: "name"}),

// 👉 Agregamos etiquetas con los valores
Plot.text(
conteoArtistas.slice(0, 10),
{
x: d => d.count + 0.2, // desplaza un poco a la derecha del final de la barra
y: "name",
text: d => d.count,
textAnchor: "start",
fontSize: 10,
fill: "black"
}
)
]
})

Insert cell
Insert cell
// Paso 1: Filtrar obras de Oceanus Folk posteriores a 2028
folkPostSailor = songsFolk.filter(d => +d.release_date >= 2028)

Insert cell
// Paso 1: Filtrar obras de Oceanus Folk posteriores a 2028
folkPostIDs = new Set(folkPostSailor.map(d => d.id))
Insert cell
// Paso 2: Buscar influencias entrantes sobre esas obras
influenciasEntrantes = graph.links.filter(d =>
folkPostIDs.has(d.target) &&
tiposDeInfluencia.includes(d["Edge Type"])
)

Insert cell
// Paso 3: Extraer géneros de los nodos que las influenciaron
origenes = influenciasEntrantes.map(d => {
const nodoOrigen = graph.nodes.find(n => n.id === d.source)
return {
id: d.source,
genre: nodoOrigen?.genre ?? "Desconocido",
year: nodoOrigen?.release_date || nodoOrigen?.written_date || "Desconocido"
}
}).filter(d => d.genre && d.year)

Insert cell
origenes2 = influenciasEntrantes.map(d => {
const nodoOrigen = graph.nodes.find(n => n.id === d.source);
return {
id: d.source,
genre: nodoOrigen?.genre ?? "Desconocido",
year: nodoOrigen?.release_date || nodoOrigen?.written_date || "Desconocido"
};
}).filter(d =>
d.genre &&
d.year &&
d.genre !== "Oceanus Folk" // 👈 excluimos auto-influencia
);

Insert cell
// Visualización: ¿De qué géneros se nutre el Oceanus Folk post-2028?
Plot.plot({
marginLeft: 140, // ajusta este valor para más espacio a los nombres
y: {label: "Género", type: "band"},
x: {label: "Cantidad"},
marks: [
Plot.barX(
d3.rollups(origenes2, v => v.length, d => d.genre)
.map(([genre, count]) => ({genre, count}))
.sort((a, b) => d3.descending(a.count, b.count)),
{x: "count", y: "genre"}
)
]
})

//A partir del éxito viral de Sailor Shift en 2028, el género Oceanus Folk comenzó a incorporar elementos estilísticos de géneros como Dream Pop y Space Rock. Esto indica una evolución estética hacia sonidos más atmosféricos y experimentales. Al mismo tiempo, se observa una fuerte autocontinuidad dentro del propio Oceanus Folk, señal de una escena consolidada que se retroalimenta y redefine a partir de sus propios referentes.

Insert cell

Plot.plot({
marginLeft: 160,
y: {label: "Género externo", type: "band"},
x: {label: "Cantidad de influencias"},
marks: [
Plot.barX(
d3.rollups(origenes2, v => v.length, d => d.genre)
.map(([genre, count]) => ({genre, count}))
.sort((a, b) => d3.descending(a.count, b.count)),
{x: "count", y: "genre"}
)
]
});
Insert cell
Plot.plot({
marginLeft: 160,
y: {label: "Género", type: "band"},
x: {label: "Cantidad de influencias"},
marks: [
Plot.barX(
d3.rollups(origenes, v => v.length, d => d.genre)
.map(([genre, count]) => ({genre, count}))
.sort((a, b) => d3.descending(a.count, b.count)),
{x: "count", y: "genre"}
)
]
});

Insert cell
// Paso 1: Generar los datos de entrada desde origenes
// Convertir a formato compatible para treemap
origenesTreemapData = d3.rollups(origenes2, v => v.length, d => d.genre)
.map(([genre, count]) => ({ genre, count }))
.sort((a, b) => d3.descending(a.count, b.count));

Insert cell
// Paso 2: Visualizar el treemap (desde origenesTreemapData)
{
const width = 700;
const height = 400;

const root = d3.hierarchy({children: origenesTreemapData})
.sum(d => d.count);

d3.treemap()
.size([width, height])
.padding(2)(root);

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

const color = d3.scaleOrdinal(d3.schemeTableau10);

const node = svg.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`);

node.append("rect")
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)
.attr("fill", d => color(d.data.genre));

node.append("text")
.attr("x", 4)
.attr("y", 14)
.text(d => `${d.data.genre} (${d.data.count})`)
.attr("fill", "white")
.style("font-size", "11px");

return svg.node();
}

Insert cell
{
const data = origenesTreemapData;

const root = d3.pack()
.size([600, 600])
.padding(3)(
d3.hierarchy({children: data}).sum(d => d.count)
);

const color = d3.scaleOrdinal(d3.schemeSet2);

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

const node = svg.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `translate(${d.x},${d.y})`);

node.append("circle")
.attr("r", d => d.r)
.attr("fill", d => color(d.data.genre))
.attr("stroke", "#333");

node.append("text")
.text(d => d.data.genre)
.attr("text-anchor", "middle")
.attr("dy", "0.3em")
.style("font-size", "10px")
.style("fill", "white");

function pulse() {
node.transition()
.duration(1000)
.attr("r", d => d.r * 1.1)
.transition()
.duration(1000)
.attr("r", d => d.r)
.on("end", pulse);
}

pulse();
return svg.node();
}

Insert cell
{
const data = origenesTreemapData;

const root = d3.pack()
.size([600, 600])
.padding(3)(
d3.hierarchy({children: data}).sum(d => d.count)
);

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

const color = d3.scaleOrdinal(d3.schemeSet2);

const node = svg.selectAll("circle")
.data(root.leaves())
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.r)
.attr("fill", d => color(d.data.genre))
.attr("stroke", "#333");

function pulse() {
node.transition()
.duration(1000)
.attr("r", d => d.r * 1.1)
.transition()
.duration(1000)
.attr("r", d => d.r)
.on("end", pulse);
}

pulse();

return svg.node();
}

Insert cell
{
const data = origenesTreemapData;
const width = 600, height = 600;
const radius = 250;

const angle = d3.scaleBand()
.domain(data.map(d => d.genre))
.range([0, 2 * Math.PI]);

const r = d3.scaleLinear()
.domain([0, d3.max(data, d => d.count)])
.range([40, radius]);

const color = d3.scaleOrdinal(d3.schemeSet3);

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

const group = svg.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);

// Ejes radiales
const lines = group.selectAll("line")
.data(data)
.join("line")
.attr("stroke", d => color(d.genre))
.attr("stroke-width", 3)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", d => r(d.count) * Math.cos(angle(d.genre) - Math.PI / 2))
.attr("y2", d => r(d.count) * Math.sin(angle(d.genre) - Math.PI / 2));

// Etiquetas
const labels = group.selectAll("text")
.data(data)
.join("text")
.attr("x", d => (r(d.count) + 15) * Math.cos(angle(d.genre) - Math.PI / 2))
.attr("y", d => (r(d.count) + 15) * Math.sin(angle(d.genre) - Math.PI / 2))
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.text(d => d.genre)
.style("font-size", "10px");

// Animación: rotar suavemente
let angleDeg = 0;
d3.timer(() => {
angleDeg = (angleDeg + 0.2) % 360;
group.attr("transform", `translate(${width / 2}, ${height / 2}) rotate(${angleDeg})`);
});

return svg.node();
}

Insert cell
Plot.plot({
marginLeft: 160,
x: {label: "Cantidad"},
y: {label: "Artista", type: "band"},
marks: [
Plot.dot(conteoArtistas.slice(0, 10), {
x: "count",
y: "name",
r: 6,
fill: "#5e60ce"
}),
Plot.text(conteoArtistas.slice(0, 10), {
x: d => d.count + 1,
y: "name",
text: d => d.count,
textAnchor: "start",
fontSize: 10,
fill: "black"
})
]
})

Insert cell
Plot.plot({
marginLeft: 160,
x: {label: "Cantidad"},
y: {label: "Artista", type: "band"},
marks: [
Plot.ruleX(conteoArtistas.slice(0, 10), {x1: 0, x2: "count", y: "name", stroke: "#ccc"}),
Plot.dot(conteoArtistas.slice(0, 10), {
x: "count",
y: "name",
r: 5,
fill: "#2a9d8f"
}),
Plot.text(conteoArtistas.slice(0, 10), {
x: d => d.count + 1,
y: "name",
text: d => d.count,
fill: "black",
textAnchor: "start",
fontSize: 10
})
]
})

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