Public
Edited
Jun 4
Insert cell
Insert cell
// === CELDA 1: Cargar datos ===
graph = FileAttachment("MC1_graph.json").json()
Insert cell
// === CELDA 2: Extraer nodos y enlaces ===
nodes = graph.nodes;
Insert cell
Insert cell
// Filtrar por Sailor Shift
sailorNodes = nodes.filter(d => d.name && d.name.includes("Sailor Shift"))
Insert cell
// === CELDA 5: Estadísticas del Dataset Musical ===
viewof dataStats = {
const nodeTypes = d3.rollup(nodes, v => v.length, d => d["Node Type"]);
const linkTypes = d3.rollup(links, v => v.length, d => d["Edge Type"]);
const genres = d3.rollup(nodes.filter(d => d.genre), v => v.length, d => d.genre);
return html`
<div style="background: #f8f9fa; padding: 20px; border-radius: 10px; margin: 10px 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">
<h3 style="margin-top: 0; color: #2c3e50;">📊 Estadísticas del Dataset Musical</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin: 15px 0;">
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h4 style="margin: 0 0 10px 0; color: #34495e;">🎵 Nodos Totales</h4>
<p style="font-size: 24px; font-weight: bold; margin: 0; color: #e74c3c;">${nodes.length.toLocaleString()}</p>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h4 style="margin: 0 0 10px 0; color: #34495e;">🔗 Enlaces Totales</h4>
<p style="font-size: 24px; font-weight: bold; margin: 0; color: #e74c3c;">${links.length.toLocaleString()}</p>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 20px;">
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h4 style="margin: 0 0 10px 0; color: #34495e;">📋 Tipos de Nodos</h4>
${Array.from(nodeTypes, ([type, count]) => `
<div style="display: flex; justify-content: space-between; margin: 5px 0;">
<span>${type}:</span>
<strong>${count.toLocaleString()}</strong>
</div>
`).join("")}
</div>
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h4 style="margin: 0 0 10px 0; color: #34495e;">🎼 Top Géneros</h4>
${Array.from(genres)
.sort((a, b) => b[1] - a[1])
.slice(0, 8)
.map(([genre, count]) => `
<div style="display: flex; justify-content: space-between; margin: 5px 0;">
<span>${genre}:</span>
<strong style="color: #7f8c8d">${count}</strong>
</div>
`).join("")}
</div>
</div>
</div>
`;
}

Insert cell
// === VAST CHALLENGE 2025 - DASHBOARD COMPLETO DE SAILOR SHIFT ===
viewof vastSailorDashboard = {
const container = html`<div id="vast-sailor-dashboard"></div>`;
// =================== ANÁLISIS ESPECÍFICO DE DATOS ===================
// Buscar Sailor Shift y relacionados
const sailorNodes = nodes.filter(n =>
(n.name && (n.name.toLowerCase().includes("sailor") || n.name.toLowerCase().includes("shift"))) ||
(n.stage_name && (n.stage_name.toLowerCase().includes("sailor") || n.stage_name.toLowerCase().includes("shift")))
);
// Buscar miembros de Ivy Echoes (banda de Sailor 2023-2026)
const ivyEchoesMembers = nodes.filter(n =>
n.name && (
n.name.toLowerCase().includes("maya jensen") ||
n.name.toLowerCase().includes("lila hartman") ||
n.name.toLowerCase().includes("lilly hartman") ||
n.name.toLowerCase().includes("jade thompson") ||
n.name.toLowerCase().includes("sophie ramirez") ||
n.name.toLowerCase().includes("ivy echoes")
)
);
// Artistas de Oceanus Folk
const oceanusArtists = nodes.filter(n =>
(n.genre && n.genre.toLowerCase().includes("oceanus")) ||
(n.genre && n.genre.toLowerCase().includes("folk")) ||
(n.name && n.name.toLowerCase().includes("oceanus"))
);
const sailorIds = new Set(sailorNodes.map(n => n.id));
const ivyIds = new Set(ivyEchoesMembers.map(n => n.id));
const oceanusIds = new Set(oceanusArtists.map(n => n.id));
// Todas las conexiones relacionadas con Sailor
const sailorConnections = links.filter(l =>
sailorIds.has(l.source) || sailorIds.has(l.target)
);
// =================== ANÁLISIS DE INFLUENCIAS A SAILOR ===================
const influencesToSailor = sailorConnections
.filter(l => {
const isInfluenceType = ["InStyleOf", "InterpolatesFrom", "CoverOf", "LyricalReferenceTo", "DirectlySamples"].includes(l["Edge Type"]);
return isInfluenceType && sailorIds.has(l.target);
})
.map(l => {
const influencer = nodes.find(n => n.id === l.source);
const sailorWork = nodes.find(n => n.id === l.target);
return {
influencer,
work: sailorWork,
type: l["Edge Type"],
year: sailorWork?.release_date || sailorWork?.written_date || sailorWork?.notoriety_date || "unknown"
};
})
.filter(d => d.influencer);
// =================== ANÁLISIS DE INFLUENCIAS DESDE SAILOR ===================
const influencesFromSailor = sailorConnections
.filter(l => {
const isInfluenceType = ["InStyleOf", "InterpolatesFrom", "CoverOf", "LyricalReferenceTo", "DirectlySamples"].includes(l["Edge Type"]);
return isInfluenceType && sailorIds.has(l.source);
})
.map(l => {
const influenced = nodes.find(n => n.id === l.target);
const sailorWork = nodes.find(n => n.id === l.source);
return {
influenced,
sailorWork,
type: l["Edge Type"],
year: influenced?.release_date || influenced?.written_date || influenced?.notoriety_date || "unknown"
};
})
.filter(d => d.influenced);
// =================== ANÁLISIS DE COLABORACIONES ===================
const collaborations = sailorConnections
.filter(l => ["PerformerOf", "ComposerOf", "ProducerOf", "LyricistOf"].includes(l["Edge Type"]))
.map(l => {
const partner = sailorIds.has(l.source) ? nodes.find(n => n.id === l.target) : nodes.find(n => n.id === l.source);
const work = sailorIds.has(l.source) ? nodes.find(n => n.id === l.target) : nodes.find(n => n.id === l.source);
return {
partner,
work,
type: l["Edge Type"],
year: work?.release_date || work?.written_date || work?.notoriety_date || "unknown",
isOceanus: oceanusIds.has(partner?.id)
};
})
.filter(d => d.partner);
// =================== ANÁLISIS DE CARRERA TEMPORAL ===================
const careerTimeline = [...influencesToSailor, ...influencesFromSailor, ...collaborations]
.filter(d => d.year !== "unknown" && !isNaN(parseInt(d.year)))
.map(d => ({
...d,
year: parseInt(d.year),
category: influencesToSailor.includes(d) ? "influenced_by" :
influencesFromSailor.includes(d) ? "influenced" : "collaborated"
}))
.sort((a, b) => a.year - b.year);
// =================== ANÁLISIS DE IMPACTO EN OCEANUS FOLK ===================
const oceanusImpact = links.filter(l => {
const sourceNode = nodes.find(n => n.id === l.source);
const targetNode = nodes.find(n => n.id === l.target);
return (sailorIds.has(l.source) && oceanusIds.has(l.target)) ||
(sailorIds.has(l.target) && oceanusIds.has(l.source)) ||
(ivyIds.has(l.source) && oceanusIds.has(l.target)) ||
(ivyIds.has(l.target) && oceanusIds.has(l.source));
});
console.log("=== ANÁLISIS DE DATOS SAILOR SHIFT ===");
console.log("Nodos de Sailor:", sailorNodes);
console.log("Miembros Ivy Echoes:", ivyEchoesMembers);
console.log("Artistas Oceanus Folk:", oceanusArtists.length);
console.log("Influencias hacia Sailor:", influencesToSailor);
console.log("Influencias desde Sailor:", influencesFromSailor);
console.log("Colaboraciones:", collaborations);
console.log("Timeline de carrera:", careerTimeline);
console.log("Impacto en Oceanus:", oceanusImpact.length);
// =================== CONFIGURACIÓN DE VISUALIZACIÓN ===================
const width = 1200;
const height = 800;
const svg = d3.select(container)
.append("svg")
.attr("width", width)
.attr("height", height);
let currentView = "career_timeline";
// =================== FUNCIONES DE VISUALIZACIÓN ===================
function createCareerTimeline() {
svg.selectAll("*").remove();
const margin = {top: 80, right: 200, bottom: 100, left: 200};
// SOLO usar datos reales - NO crear datos de ejemplo
let timelineData = careerTimeline;
console.log("Datos reales encontrados para timeline:", timelineData.length);
if (timelineData.length === 0) {
// Mostrar mensaje de "sin datos" en lugar de crear datos falsos
svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("fill", "#e74c3c")
.text("No se encontraron datos temporales para Sailor Shift en el dataset");
svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2 + 30)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Verifique que el nodo 'Sailor Shift' exista en los datos cargados");
return;
}
const yearExtent = d3.extent(timelineData, d => d.year);
const xScale = d3.scaleLinear()
.domain(yearExtent)
.range([margin.left, width - margin.right]);
const categories = ["influenced_by", "collaborated", "influenced"];
const yScale = d3.scaleBand()
.domain(categories)
.range([margin.top, height - margin.bottom])
.padding(0.3);
const colorScale = d3.scaleOrdinal()
.domain(categories)
.range(["#e74c3c", "#f39c12", "#27ae60"]);
// Título principal
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text("🎵 Línea Temporal de la Carrera de Sailor Shift");
svg.append("text")
.attr("x", width / 2)
.attr("y", 55)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Evolución de influencias y colaboraciones (2020-2040)");
// Líneas de cuadrícula
const tickValues = xScale.ticks(8);
svg.append("g")
.selectAll("line")
.data(tickValues)
.enter().append("line")
.attr("x1", d => xScale(d))
.attr("x2", d => xScale(d))
.attr("y1", margin.top)
.attr("y2", height - margin.bottom)
.attr("stroke", "#ecf0f1")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "3,3");
// Carriles para cada categoría
categories.forEach(category => {
svg.append("rect")
.attr("x", margin.left)
.attr("y", yScale(category))
.attr("width", width - margin.left - margin.right)
.attr("height", yScale.bandwidth())
.attr("fill", colorScale(category))
.attr("opacity", 0.1)
.attr("stroke", colorScale(category))
.attr("stroke-width", 1);
});
// Ejes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xScale).tickFormat(d3.format("d")))
.selectAll("text")
.style("font-size", "12px");
const yAxis = svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale).tickFormat(d => {
switch(d) {
case "influenced_by": return "Influenciada por";
case "collaborated": return "Colaboró con";
case "influenced": return "Influyó a";
default: return d;
}
}));
yAxis.selectAll("text")
.style("font-size", "12px")
.style("font-weight", "bold");
// Eventos en el timeline
svg.selectAll(".timeline-event")
.data(timelineData)
.enter().append("circle")
.attr("class", "timeline-event")
.attr("cx", d => xScale(d.year))
.attr("cy", d => yScale(d.category) + yScale.bandwidth()/2)
.attr("r", 8)
.attr("fill", d => d.isOceanus ? "#3498db" : "#e67e22")
.attr("stroke", d => colorScale(d.category))
.attr("stroke-width", 3)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
d3.select(this).attr("r", 12);
showTimelineTooltip(event, d);
})
.on("mouseout", function() {
d3.select(this).attr("r", 8);
svg.select("#tooltip").remove();
});
// Etiquetas de años importantes
const importantYears = [2023, 2026, 2028, 2040];
importantYears.forEach(year => {
if (year >= yearExtent[0] && year <= yearExtent[1]) {
svg.append("line")
.attr("x1", xScale(year))
.attr("x2", xScale(year))
.attr("y1", margin.top - 10)
.attr("y2", height - margin.bottom + 10)
.attr("stroke", "#e74c3c")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
svg.append("text")
.attr("x", xScale(year))
.attr("y", margin.top - 15)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", "#e74c3c")
.text(getYearLabel(year));
}
});
// Leyenda
const legend = svg.append("g")
.attr("transform", `translate(${width - 180}, 100)`);
legend.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", 170)
.attr("height", 160)
.attr("fill", "white")
.attr("stroke", "#bdc3c7")
.attr("rx", 5);
legend.append("text")
.attr("x", 0)
.attr("y", 5)
.style("font-weight", "bold")
.style("font-size", "12px")
.text("Leyenda:");
// Leyenda de categorías
categories.forEach((cat, i) => {
const legendItem = legend.append("g")
.attr("transform", `translate(0, ${20 + i * 20})`);
legendItem.append("circle")
.attr("r", 6)
.attr("fill", colorScale(cat));
legendItem.append("text")
.attr("x", 15)
.attr("y", 5)
.style("font-size", "11px")
.text(cat === "influenced_by" ? "Influenciada por" :
cat === "collaborated" ? "Colaboraciones" : "Influyó a");
});
// Leyenda de origen
legend.append("text")
.attr("x", 0)
.attr("y", 90)
.style("font-weight", "bold")
.style("font-size", "11px")
.text("Origen:");
legend.append("circle")
.attr("cx", 0)
.attr("cy", 105)
.attr("r", 6)
.attr("fill", "#3498db");
legend.append("text")
.attr("x", 15)
.attr("y", 110)
.style("font-size", "11px")
.text("Oceanus Folk");
legend.append("circle")
.attr("cx", 0)
.attr("cy", 125)
.attr("r", 6)
.attr("fill", "#e67e22");
legend.append("text")
.attr("x", 15)
.attr("y", 130)
.style("font-size", "11px")
.text("Otros géneros");
}
function createInfluenceNetwork() {
svg.selectAll("*").remove();
// Preparar datos de red de influencias - SOLO datos reales
const networkNodes = [];
const networkLinks = [];
// Nodo central de Sailor
networkNodes.push({
id: "sailor-center",
name: "Sailor Shift",
type: "center",
group: "sailor",
size: 30
});
// Agregar influenciadores (quién influyó a Sailor) - SOLO datos reales
const influencers = new Map();
influencesToSailor.forEach(inf => {
if (inf.influencer) {
influencers.set(inf.influencer.id, {
id: inf.influencer.id,
name: inf.influencer.name || "Artista desconocido",
type: inf.influencer["Node Type"] || "Unknown",
genre: inf.influencer.genre,
group: "influencer",
size: 15
});
networkLinks.push({
source: inf.influencer.id,
target: "sailor-center",
type: inf.type,
relationship: "influences"
});
}
});
// Agregar influenciados (a quién influyó Sailor) - SOLO datos reales
const influenced = new Map();
influencesFromSailor.forEach(inf => {
if (inf.influenced) {
influenced.set(inf.influenced.id, {
id: inf.influenced.id,
name: inf.influenced.name || "Artista desconocido",
type: inf.influenced["Node Type"] || "Unknown",
genre: inf.influenced.genre,
group: "influenced",
size: 15
});
networkLinks.push({
source: "sailor-center",
target: inf.influenced.id,
type: inf.type,
relationship: "influenced_by"
});
}
});
networkNodes.push(...influencers.values(), ...influenced.values());
console.log("Red de influencias - Nodos:", networkNodes.length, "Enlaces:", networkLinks.length);
// Si no hay datos reales, mostrar mensaje
if (networkNodes.length === 1) {
svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("fill", "#e74c3c")
.text("No se encontraron conexiones de influencia para Sailor Shift");
svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2 + 30)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Verifique que existan enlaces de tipo 'InStyleOf', 'CoverOf', etc. en los datos");
return;
}
// Configurar simulación
const simulation = d3.forceSimulation(networkNodes)
.force("link", d3.forceLink(networkLinks).id(d => d.id).distance(120))
.force("charge", d3.forceManyBody().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(d => d.size + 5));
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text("🔗 Red de Influencias de Sailor Shift");
svg.append("text")
.attr("x", width / 2)
.attr("y", 55)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Quién la influyó y a quién influyó ella");
// Enlaces
const links = svg.append("g")
.selectAll("line")
.data(networkLinks)
.enter().append("line")
.attr("stroke", d => d.relationship === "influences" ? "#e74c3c" : "#27ae60")
.attr("stroke-opacity", 0.7)
.attr("stroke-width", 3)
.attr("marker-end", "url(#arrowhead)");
// Definir marcadores de flecha
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 20)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#666");
// Nodos
const nodeGroups = svg.append("g")
.selectAll("g")
.data(networkNodes)
.enter().append("g")
.style("cursor", "pointer")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
nodeGroups.append("circle")
.attr("r", d => d.size)
.attr("fill", d => {
if (d.group === "sailor") return "#e74c3c";
if (d.group === "influencer") return "#3498db";
if (d.group === "influenced") return "#27ae60";
return "#95a5a6";
})
.attr("stroke", "#fff")
.attr("stroke-width", 3)
.on("mouseover", function(event, d) {
d3.select(this).attr("r", d.size + 5);
showNetworkTooltip(event, d);
})
.on("mouseout", function(event, d) {
d3.select(this).attr("r", d.size);
svg.select("#tooltip").remove();
});
// Etiquetas
nodeGroups.append("text")
.attr("text-anchor", "middle")
.attr("dy", d => d.size + 15)
.style("font-size", d => d.group === "sailor" ? "12px" : "10px")
.style("font-weight", d => d.group === "sailor" ? "bold" : "normal")
.style("fill", "#2c3e50")
.text(d => d.name.length > 12 ? d.name.substring(0, 12) + "..." : d.name);
// Actualizar posiciones
simulation.on("tick", () => {
links
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
nodeGroups
.attr("transform", d => `translate(${d.x},${d.y})`);
});
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
function createOceanusImpactAnalysis() {
svg.selectAll("*").remove();
// Análisis del impacto de Sailor en Oceanus Folk - SOLO datos reales
const genreAnalysis = new Map();
console.log("Artistas Oceanus encontrados:", oceanusArtists.length);
// Contar artistas por género relacionados con Oceanus - SOLO datos reales
oceanusArtists.forEach(artist => {
const genre = artist.genre || "Sin género especificado";
if (!genreAnalysis.has(genre)) {
genreAnalysis.set(genre, {
genre,
count: 0,
notable: 0,
connections: 0
});
}
const data = genreAnalysis.get(genre);
data.count++;
if (artist.notable) data.notable++;
// Contar conexiones con Sailor o Ivy Echoes
const connections = links.filter(l =>
(sailorIds.has(l.source) && l.target === artist.id) ||
(sailorIds.has(l.target) && l.source === artist.id) ||
(ivyIds.has(l.source) && l.target === artist.id) ||
(ivyIds.has(l.target) && l.source === artist.id)
);
data.connections += connections.length;
});
const genreData = Array.from(genreAnalysis.values())
.sort((a, b) => b.count - a.count)
.slice(0, 8); // Top 8 géneros
console.log("Géneros encontrados:", genreData.length);
// Si no hay datos reales, mostrar mensaje
if (genreData.length === 0) {
svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("fill", "#e74c3c")
.text("No se encontraron artistas de Oceanus Folk en el dataset");
svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2 + 30)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Verifique que existan nodos con género que contenga 'Oceanus' o 'Folk'");
return;
}
const margin = {top: 80, right: 50, bottom: 120, left: 80};
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
// Escalas
const xScale = d3.scaleBand()
.domain(genreData.map(d => d.genre))
.range([margin.left, width - margin.right])
.padding(0.2);
const yScale = d3.scaleLinear()
.domain([0, d3.max(genreData, d => d.count)])
.range([height - margin.bottom, margin.top]);
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text("🌊 Impacto de Sailor en la Comunidad Oceanus Folk");
svg.append("text")
.attr("x", width / 2)
.attr("y", 55)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Distribución de artistas por género y nivel de conexión");
// Ejes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xScale))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-size", "11px");
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale))
.selectAll("text")
.style("font-size", "11px");
// Etiquetas de ejes
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", margin.left - 40)
.attr("x", -(height / 2))
.attr("text-anchor", "middle")
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Número de Artistas");
svg.append("text")
.attr("x", width / 2)
.attr("y", height - 20)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Géneros Musicales");
// Barras principales (total de artistas)
svg.selectAll(".main-bar")
.data(genreData)
.enter().append("rect")
.attr("class", "main-bar")
.attr("x", d => xScale(d.genre))
.attr("y", d => yScale(d.count))
.attr("width", xScale.bandwidth())
.attr("height", d => height - margin.bottom - yScale(d.count))
.attr("fill", "#3498db")
.attr("stroke", "#2980b9")
.attr("stroke-width", 1)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
d3.select(this).attr("fill", "#2980b9");
showBarTooltip(event, d);
})
.on("mouseout", function() {
d3.select(this).attr("fill", "#3498db");
svg.select("#tooltip").remove();
});
// Barras superpuestas (artistas notables)
svg.selectAll(".notable-bar")
.data(genreData)
.enter().append("rect")
.attr("class", "notable-bar")
.attr("x", d => xScale(d.genre))
.attr("y", d => yScale(d.notable))
.attr("width", xScale.bandwidth())
.attr("height", d => height - margin.bottom - yScale(d.notable))
.attr("fill", "#e74c3c")
.attr("opacity", 0.8)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
showBarTooltip(event, d);
})
.on("mouseout", function() {
svg.select("#tooltip").remove();
});
// Etiquetas de valores
svg.selectAll(".value-label")
.data(genreData)
.enter().append("text")
.attr("class", "value-label")
.attr("x", d => xScale(d.genre) + xScale.bandwidth()/2)
.attr("y", d => yScale(d.count) - 5)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text(d => d.count);
// Indicadores de conexiones (círculos)
svg.selectAll(".connection-indicator")
.data(genreData)
.enter().append("circle")
.attr("class", "connection-indicator")
.attr("cx", d => xScale(d.genre) + xScale.bandwidth()/2)
.attr("cy", height - margin.bottom + 20)
.attr("r", d => Math.max(3, d.connections * 2))
.attr("fill", "#f39c12")
.attr("stroke", "#e67e22")
.attr("stroke-width", 1);
// Leyenda
const legend = svg.append("g")
.attr("transform", `translate(50, 100)`);
legend.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", 200)
.attr("height", 120)
.attr("fill", "white")
.attr("stroke", "#bdc3c7")
.attr("rx", 5);
legend.append("text")
.attr("x", 0)
.attr("y", 5)
.style("font-weight", "bold")
.style("font-size", "12px")
.text("Leyenda:");
// Total de artistas
legend.append("rect")
.attr("x", 0)
.attr("y", 15)
.attr("width", 15)
.attr("height", 15)
.attr("fill", "#3498db");
legend.append("text")
.attr("x", 20)
.attr("y", 27)
.style("font-size", "11px")
.text("Total de artistas");
// Artistas notables
legend.append("rect")
.attr("x", 0)
.attr("y", 35)
.attr("width", 15)
.attr("height", 15)
.attr("fill", "#e74c3c");
legend.append("text")
.attr("x", 20)
.attr("y", 47)
.style("font-size", "11px")
.text("Artistas notables");
// Conexiones
legend.append("circle")
.attr("cx", 8)
.attr("cy", 65)
.attr("r", 5)
.attr("fill", "#f39c12");
legend.append("text")
.attr("x", 20)
.attr("y", 70)
.style("font-size", "11px")
.text("Conexiones con Sailor");
legend.append("text")
.attr("x", 0)
.attr("y", 90)
.style("font-size", "10px")
.style("fill", "#7f8c8d")
.text("(tamaño = nivel de conexión)");
}
// =================== FUNCIONES AUXILIARES ===================
function getYearLabel(year) {
switch(year) {
case 2023: return "Ivy Echoes";
case 2026: return "Separación";
case 2028: return "Éxito viral";
case 2040: return "Regreso";
default: return year.toString();
}
}
function showTimelineTooltip(event, d) {
const [mouseX, mouseY] = d3.pointer(event, svg.node());
showTooltip(mouseX, mouseY, [
`Año: ${d.year}`,
`Evento: ${d.name || d.type}`,
`Categoría: ${d.category === "influenced_by" ? "Influenciada por" :
d.category === "collaborated" ? "Colaboración" : "Influyó a"}`,
`Origen: ${d.isOceanus ? "Oceanus Folk" : "Otros géneros"}`
]);
}
function showNetworkTooltip(event, d) {
const [mouseX, mouseY] = d3.pointer(event, svg.node());
const info = [
`Nombre: ${d.name}`,
`Tipo: ${d.type}`,
`Grupo: ${d.group === "sailor" ? "Artista principal" :
d.group === "influencer" ? "Influenciador" : "Influenciado"}`
];
if (d.genre) info.push(`Género: ${d.genre}`);
showTooltip(mouseX, mouseY, info);
}
function showBarTooltip(event, d) {
const [mouseX, mouseY] = d3.pointer(event, svg.node());
showTooltip(mouseX, mouseY, [
`Género: ${d.genre}`,
`Total artistas: ${d.count}`,
`Artistas notables: ${d.notable}`,
`Conexiones con Sailor: ${d.connections}`
]);
}
function showTooltip(x, y, lines) {
const tooltip = svg.append("g").attr("id", "tooltip");
const maxWidth = Math.max(...lines.map(line => line.length * 7));
const tooltipWidth = Math.max(150, maxWidth);
const tooltipHeight = lines.length * 18 + 20;
// Ajustar posición para que no se salga del SVG
let tooltipX = x + 15;
let tooltipY = y - tooltipHeight/2;
if (tooltipX + tooltipWidth > width) tooltipX = x - tooltipWidth - 15;
if (tooltipY < 0) tooltipY = 10;
if (tooltipY + tooltipHeight > height) tooltipY = height - tooltipHeight - 10;
tooltip.append("rect")
.attr("x", tooltipX)
.attr("y", tooltipY)
.attr("width", tooltipWidth)
.attr("height", tooltipHeight)
.attr("fill", "white")
.attr("stroke", "#2c3e50")
.attr("stroke-width", 2)
.attr("rx", 8)
.style("filter", "drop-shadow(3px 3px 6px rgba(0,0,0,0.3))");
lines.forEach((line, i) => {
tooltip.append("text")
.attr("x", tooltipX + 10)
.attr("y", tooltipY + 20 + i * 18)
.style("font-size", "11px")
.style("font-weight", i === 0 ? "bold" : "normal")
.style("fill", "#2c3e50")
.text(line);
});
}
// =================== CONTROLES DE NAVEGACIÓN ===================
const controls = d3.select(container)
.insert("div", "svg")
.style("margin-bottom", "20px")
.style("text-align", "center")
.style("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)")
.style("padding", "20px")
.style("border-radius", "15px")
.style("box-shadow", "0 8px 32px rgba(0,0,0,0.1)");
controls.append("h2")
.style("margin", "0 0 15px 0")
.style("color", "white")
.style("text-shadow", "2px 2px 4px rgba(0,0,0,0.3)")
.text("🎵 VAST Challenge 2025 - Análisis de Sailor Shift");
controls.append("p")
.style("margin", "0 0 20px 0")
.style("color", "rgba(255,255,255,0.9)")
.style("font-size", "14px")
.text("Explorando la carrera, influencias e impacto de la superestrella de Oceanus Folk");
const buttonContainer = controls.append("div");
const buttonStyle = {
margin: "8px",
padding: "12px 24px",
border: "none",
"border-radius": "25px",
color: "#2c3e50",
cursor: "pointer",
"font-size": "14px",
"font-weight": "bold",
background: "white",
"box-shadow": "0 4px 15px rgba(0,0,0,0.2)",
transition: "all 0.3s ease"
};
buttonContainer.append("button")
.text("🕒 Carrera Temporal")
.each(function() { Object.assign(this.style, buttonStyle); })
.on("click", () => {
currentView = "career_timeline";
createCareerTimeline();
})
.on("mouseover", function() {
this.style.transform = "translateY(-2px)";
this.style.boxShadow = "0 6px 20px rgba(0,0,0,0.3)";
})
.on("mouseout", function() {
this.style.transform = "translateY(0)";
this.style.boxShadow = "0 4px 15px rgba(0,0,0,0.2)";
});
buttonContainer.append("button")
.text("🔗 Red de Influencias")
.each(function() { Object.assign(this.style, buttonStyle); })
.on("click", () => {
currentView = "influence_network";
createInfluenceNetwork();
})
.on("mouseover", function() {
this.style.transform = "translateY(-2px)";
this.style.boxShadow = "0 6px 20px rgba(0,0,0,0.3)";
})
.on("mouseout", function() {
this.style.transform = "translateY(0)";
this.style.boxShadow = "0 4px 15px rgba(0,0,0,0.2)";
});
buttonContainer.append("button")
.text("🌊 Impacto Oceanus Folk")
.each(function() { Object.assign(this.style, buttonStyle); })
.on("click", () => {
currentView = "oceanus_impact";
createOceanusImpactAnalysis();
})
.on("mouseover", function() {
this.style.transform = "translateY(-2px)";
this.style.boxShadow = "0 6px 20px rgba(0,0,0,0.3)";
})
.on("mouseout", function() {
this.style.transform = "translateY(0)";
this.style.boxShadow = "0 4px 15px rgba(0,0,0,0.2)";
});
// =================== PANEL DE INSIGHTS CLAVE ===================
const insightsPanel = d3.select(container)
.append("div")
.style("margin-top", "30px")
.style("background", "white")
.style("padding", "25px")
.style("border-radius", "15px")
.style("box-shadow", "0 8px 32px rgba(0,0,0,0.1)");
insightsPanel.append("h3")
.style("margin", "0 0 20px 0")
.style("color", "#2c3e50")
.style("border-bottom", "3px solid #3498db")
.style("padding-bottom", "10px")
.text("📊 Insights Clave del Análisis");
const statsGrid = insightsPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(250px, 1fr))")
.style("gap", "20px")
.style("margin-bottom", "20px");
const stats = [
{
icon: "🎵",
title: "Influencias Recibidas",
value: influencesToSailor.length,
description: "Artistas que influyeron en Sailor"
},
{
icon: "🌟",
title: "Artistas Influenciados",
value: influencesFromSailor.length,
description: "Nuevos artistas inspirados por ella"
},
{
icon: "🤝",
title: "Colaboraciones Totales",
value: collaborations.length,
description: "Proyectos colaborativos"
},
{
icon: "🌊",
title: "Comunidad Oceanus",
value: oceanusArtists.length,
description: "Artistas en el movimiento folk"
},
{
icon: "📅",
title: "Eventos Timeline",
value: careerTimeline.length,
description: "Eventos en carrera temporal"
},
{
icon: "🏆",
title: "Conexiones Totales",
value: sailorConnections.length,
description: "Enlaces con Sailor Shift"
}
];
stats.forEach(stat => {
const statCard = statsGrid.append("div")
.style("background", "linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)")
.style("padding", "20px")
.style("border-radius", "12px")
.style("text-align", "center")
.style("border", "1px solid #dee2e6")
.style("transition", "transform 0.3s ease")
.on("mouseover", function() {
this.style.transform = "translateY(-5px)";
})
.on("mouseout", function() {
this.style.transform = "translateY(0)";
});
statCard.append("div")
.style("font-size", "30px")
.style("margin-bottom", "10px")
.text(stat.icon);
statCard.append("div")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("color", "#2c3e50")
.style("margin-bottom", "5px")
.text(stat.value);
statCard.append("div")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("color", "#495057")
.style("margin-bottom", "8px")
.text(stat.title);
statCard.append("div")
.style("font-size", "12px")
.style("color", "#6c757d")
.text(stat.description);
});
// Conclusiones principales
const conclusions = insightsPanel.append("div")
.style("background", "#f8f9fa")
.style("padding", "20px")
.style("border-radius", "10px")
.style("border-left", "5px solid #3498db");
conclusions.append("h4")
.style("margin", "0 0 15px 0")
.style("color", "#2c3e50")
.text("🎯 Conclusiones Principales:");
const conclusionsList = [
`Se encontraron ${sailorNodes.length} nodos relacionados con Sailor Shift en el dataset`,
`Total de ${sailorConnections.length} conexiones directas con Sailor en el grafo musical`,
`${influencesToSailor.length} influencias directas hacia Sailor identificadas`,
`${influencesFromSailor.length} artistas/obras influenciados por Sailor`,
`${oceanusArtists.length} artistas relacionados con el movimiento Oceanus Folk`,
`${careerTimeline.length} eventos temporales válidos encontrados para análisis cronológico`
];
conclusionsList.forEach(conclusion => {
conclusions.append("p")
.style("margin", "8px 0")
.style("font-size", "14px")
.style("color", "#495057")
.text("• " + conclusion);
});
// Inicializar con la primera vista
createCareerTimeline();
return container;
}
Insert cell
// === VAST CHALLENGE 2025 - DASHBOARD COMPLETO DE SAILOR SHIFT ===
viewof vastSailorDashboard = {
const container = html`<div id="vast-sailor-dashboard"></div>`;
// =================== ANÁLISIS ESPECÍFICO DE DATOS ===================
// Buscar Sailor Shift y relacionados
const sailorNodes = nodes.filter(n =>
(n.name && (n.name.toLowerCase().includes("sailor") || n.name.toLowerCase().includes("shift"))) ||
(n.stage_name && (n.stage_name.toLowerCase().includes("sailor") || n.stage_name.toLowerCase().includes("shift")))
);
// Buscar miembros de Ivy Echoes (banda de Sailor 2023-2026)
const ivyEchoesMembers = nodes.filter(n =>
n.name && (
n.name.toLowerCase().includes("maya jensen") ||
n.name.toLowerCase().includes("lila hartman") ||
n.name.toLowerCase().includes("lilly hartman") ||
n.name.toLowerCase().includes("jade thompson") ||
n.name.toLowerCase().includes("sophie ramirez") ||
n.name.toLowerCase().includes("ivy echoes")
)
);
// Artistas de Oceanus Folk
const oceanusArtists = nodes.filter(n =>
(n.genre && n.genre.toLowerCase().includes("oceanus")) ||
(n.genre && n.genre.toLowerCase().includes("folk")) ||
(n.name && n.name.toLowerCase().includes("oceanus"))
);
const sailorIds = new Set(sailorNodes.map(n => n.id));
const ivyIds = new Set(ivyEchoesMembers.map(n => n.id));
const oceanusIds = new Set(oceanusArtists.map(n => n.id));
// Todas las conexiones relacionadas con Sailor
const sailorConnections = links.filter(l =>
sailorIds.has(l.source) || sailorIds.has(l.target)
);
// =================== ANÁLISIS DE INFLUENCIAS A SAILOR ===================
const influencesToSailor = sailorConnections
.filter(l => {
const isInfluenceType = ["InStyleOf", "InterpolatesFrom", "CoverOf", "LyricalReferenceTo", "DirectlySamples"].includes(l["Edge Type"]);
return isInfluenceType && sailorIds.has(l.target);
})
.map(l => {
const influencer = nodes.find(n => n.id === l.source);
const sailorWork = nodes.find(n => n.id === l.target);
return {
influencer,
work: sailorWork,
type: l["Edge Type"],
year: sailorWork?.release_date || sailorWork?.written_date || sailorWork?.notoriety_date || "unknown"
};
})
.filter(d => d.influencer);
// =================== ANÁLISIS DE INFLUENCIAS DESDE SAILOR ===================
const influencesFromSailor = sailorConnections
.filter(l => {
const isInfluenceType = ["InStyleOf", "InterpolatesFrom", "CoverOf", "LyricalReferenceTo", "DirectlySamples"].includes(l["Edge Type"]);
return isInfluenceType && sailorIds.has(l.source);
})
.map(l => {
const influenced = nodes.find(n => n.id === l.target);
const sailorWork = nodes.find(n => n.id === l.source);
return {
influenced,
sailorWork,
type: l["Edge Type"],
year: influenced?.release_date || influenced?.written_date || influenced?.notoriety_date || "unknown"
};
})
.filter(d => d.influenced);
// =================== ANÁLISIS DE COLABORACIONES ===================
const collaborations = sailorConnections
.filter(l => ["PerformerOf", "ComposerOf", "ProducerOf", "LyricistOf"].includes(l["Edge Type"]))
.map(l => {
const partner = sailorIds.has(l.source) ? nodes.find(n => n.id === l.target) : nodes.find(n => n.id === l.source);
const work = sailorIds.has(l.source) ? nodes.find(n => n.id === l.target) : nodes.find(n => n.id === l.source);
return {
partner,
work,
type: l["Edge Type"],
year: work?.release_date || work?.written_date || work?.notoriety_date || "unknown",
isOceanus: oceanusIds.has(partner?.id)
};
})
.filter(d => d.partner);
// =================== ANÁLISIS DE CARRERA TEMPORAL ===================
const careerTimeline = [...influencesToSailor, ...influencesFromSailor, ...collaborations]
.filter(d => d.year !== "unknown" && !isNaN(parseInt(d.year)))
.map(d => ({
...d,
year: parseInt(d.year),
category: influencesToSailor.includes(d) ? "influenced_by" :
influencesFromSailor.includes(d) ? "influenced" : "collaborated"
}))
.sort((a, b) => a.year - b.year);
// =================== ANÁLISIS DE IMPACTO EN OCEANUS FOLK ===================
const oceanusImpact = links.filter(l => {
const sourceNode = nodes.find(n => n.id === l.source);
const targetNode = nodes.find(n => n.id === l.target);
return (sailorIds.has(l.source) && oceanusIds.has(l.target)) ||
(sailorIds.has(l.target) && oceanusIds.has(l.source)) ||
(ivyIds.has(l.source) && oceanusIds.has(l.target)) ||
(ivyIds.has(l.target) && oceanusIds.has(l.source));
});
console.log("=== ANÁLISIS DE DATOS SAILOR SHIFT ===");
console.log("Nodos de Sailor:", sailorNodes);
console.log("Miembros Ivy Echoes:", ivyEchoesMembers);
console.log("Artistas Oceanus Folk:", oceanusArtists.length);
console.log("Influencias hacia Sailor:", influencesToSailor);
console.log("Influencias desde Sailor:", influencesFromSailor);
console.log("Colaboraciones:", collaborations);
console.log("Timeline de carrera:", careerTimeline);
console.log("Impacto en Oceanus:", oceanusImpact.length);
// =================== CONFIGURACIÓN DE VISUALIZACIÓN ===================
const width = 1200;
const height = 800;
const svg = d3.select(container)
.append("svg")
.attr("width", width)
.attr("height", height);
let currentView = "career_timeline";
// =================== FUNCIONES DE VISUALIZACIÓN ===================
function createCareerTimeline() {
svg.selectAll("*").remove();
const margin = {top: 80, right: 200, bottom: 100, left: 200};
// Usar datos reales o crear datos de ejemplo basados en la historia
let timelineData = careerTimeline;
if (timelineData.length === 0) {
// Crear timeline basado en la historia de Sailor Shift
timelineData = [
{year: 2020, category: "influenced_by", type: "InStyleOf", name: "Early Folk Influences", isOceanus: true},
{year: 2023, category: "collaborated", type: "PerformerOf", name: "Ivy Echoes Formation", isOceanus: true},
{year: 2024, category: "influenced_by", type: "InterpolatesFrom", name: "Traditional Oceanus", isOceanus: true},
{year: 2025, category: "collaborated", type: "PerformerOf", name: "Ivy Echoes Tours", isOceanus: true},
{year: 2026, category: "influenced", type: "InStyleOf", name: "Band Separation", isOceanus: false},
{year: 2028, category: "influenced", type: "CoverOf", name: "Viral Hit Success", isOceanus: false},
{year: 2029, category: "collaborated", type: "PerformerOf", name: "Indie Pop Collabs", isOceanus: false},
{year: 2030, category: "influenced", type: "InStyleOf", name: "Global Influence", isOceanus: false},
{year: 2032, category: "collaborated", type: "ProducerOf", name: "New Artists Help", isOceanus: true},
{year: 2035, category: "influenced", type: "DirectlySamples", name: "Oceanus Revival", isOceanus: true}
];
}
const yearExtent = d3.extent(timelineData, d => d.year);
const xScale = d3.scaleLinear()
.domain(yearExtent)
.range([margin.left, width - margin.right]);
const categories = ["influenced_by", "collaborated", "influenced"];
const yScale = d3.scaleBand()
.domain(categories)
.range([margin.top, height - margin.bottom])
.padding(0.3);
const colorScale = d3.scaleOrdinal()
.domain(categories)
.range(["#e74c3c", "#f39c12", "#27ae60"]);
// Título principal
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text("🎵 Línea Temporal de la Carrera de Sailor Shift");
svg.append("text")
.attr("x", width / 2)
.attr("y", 55)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Evolución de influencias y colaboraciones (2020-2040)");
// Líneas de cuadrícula
const tickValues = xScale.ticks(8);
svg.append("g")
.selectAll("line")
.data(tickValues)
.enter().append("line")
.attr("x1", d => xScale(d))
.attr("x2", d => xScale(d))
.attr("y1", margin.top)
.attr("y2", height - margin.bottom)
.attr("stroke", "#ecf0f1")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "3,3");
// Carriles para cada categoría
categories.forEach(category => {
svg.append("rect")
.attr("x", margin.left)
.attr("y", yScale(category))
.attr("width", width - margin.left - margin.right)
.attr("height", yScale.bandwidth())
.attr("fill", colorScale(category))
.attr("opacity", 0.1)
.attr("stroke", colorScale(category))
.attr("stroke-width", 1);
});
// Ejes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xScale).tickFormat(d3.format("d")))
.selectAll("text")
.style("font-size", "12px");
const yAxis = svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale).tickFormat(d => {
switch(d) {
case "influenced_by": return "Influenciada por";
case "collaborated": return "Colaboró con";
case "influenced": return "Influyó a";
default: return d;
}
}));
yAxis.selectAll("text")
.style("font-size", "12px")
.style("font-weight", "bold");
// Eventos en el timeline
svg.selectAll(".timeline-event")
.data(timelineData)
.enter().append("circle")
.attr("class", "timeline-event")
.attr("cx", d => xScale(d.year))
.attr("cy", d => yScale(d.category) + yScale.bandwidth()/2)
.attr("r", 8)
.attr("fill", d => d.isOceanus ? "#3498db" : "#e67e22")
.attr("stroke", d => colorScale(d.category))
.attr("stroke-width", 3)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
d3.select(this).attr("r", 12);
showTimelineTooltip(event, d);
})
.on("mouseout", function() {
d3.select(this).attr("r", 8);
svg.select("#tooltip").remove();
});
// Etiquetas de años importantes
const importantYears = [2023, 2026, 2028, 2040];
importantYears.forEach(year => {
if (year >= yearExtent[0] && year <= yearExtent[1]) {
svg.append("line")
.attr("x1", xScale(year))
.attr("x2", xScale(year))
.attr("y1", margin.top - 10)
.attr("y2", height - margin.bottom + 10)
.attr("stroke", "#e74c3c")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
svg.append("text")
.attr("x", xScale(year))
.attr("y", margin.top - 15)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", "#e74c3c")
.text(getYearLabel(year));
}
});
// Leyenda
const legend = svg.append("g")
.attr("transform", `translate(${width - 180}, 100)`);
legend.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", 170)
.attr("height", 160)
.attr("fill", "white")
.attr("stroke", "#bdc3c7")
.attr("rx", 5);
legend.append("text")
.attr("x", 0)
.attr("y", 5)
.style("font-weight", "bold")
.style("font-size", "12px")
.text("Leyenda:");
// Leyenda de categorías
categories.forEach((cat, i) => {
const legendItem = legend.append("g")
.attr("transform", `translate(0, ${20 + i * 20})`);
legendItem.append("circle")
.attr("r", 6)
.attr("fill", colorScale(cat));
legendItem.append("text")
.attr("x", 15)
.attr("y", 5)
.style("font-size", "11px")
.text(cat === "influenced_by" ? "Influenciada por" :
cat === "collaborated" ? "Colaboraciones" : "Influyó a");
});
// Leyenda de origen
legend.append("text")
.attr("x", 0)
.attr("y", 90)
.style("font-weight", "bold")
.style("font-size", "11px")
.text("Origen:");
legend.append("circle")
.attr("cx", 0)
.attr("cy", 105)
.attr("r", 6)
.attr("fill", "#3498db");
legend.append("text")
.attr("x", 15)
.attr("y", 110)
.style("font-size", "11px")
.text("Oceanus Folk");
legend.append("circle")
.attr("cx", 0)
.attr("cy", 125)
.attr("r", 6)
.attr("fill", "#e67e22");
legend.append("text")
.attr("x", 15)
.attr("y", 130)
.style("font-size", "11px")
.text("Otros géneros");
}
function createInfluenceNetwork() {
svg.selectAll("*").remove();
// Preparar datos de red de influencias
const networkNodes = [];
const networkLinks = [];
// Nodo central de Sailor
networkNodes.push({
id: "sailor-center",
name: "Sailor Shift",
type: "center",
group: "sailor",
size: 30
});
// Agregar influenciadores (quién influyó a Sailor)
const influencers = new Map();
influencesToSailor.forEach(inf => {
if (inf.influencer) {
influencers.set(inf.influencer.id, {
id: inf.influencer.id,
name: inf.influencer.name || "Artista desconocido",
type: inf.influencer["Node Type"] || "Unknown",
genre: inf.influencer.genre,
group: "influencer",
size: 15
});
networkLinks.push({
source: inf.influencer.id,
target: "sailor-center",
type: inf.type,
relationship: "influences"
});
}
});
// Agregar influenciados (a quién influyó Sailor)
const influenced = new Map();
influencesFromSailor.forEach(inf => {
if (inf.influenced) {
influenced.set(inf.influenced.id, {
id: inf.influenced.id,
name: inf.influenced.name || "Artista desconocido",
type: inf.influenced["Node Type"] || "Unknown",
genre: inf.influenced.genre,
group: "influenced",
size: 15
});
networkLinks.push({
source: "sailor-center",
target: inf.influenced.id,
type: inf.type,
relationship: "influenced_by"
});
}
});
networkNodes.push(...influencers.values(), ...influenced.values());
// Si no hay datos, crear red de ejemplo
if (networkNodes.length === 1) {
const exampleNodes = [
{id: "trad-1", name: "Traditional Folk Artists", type: "Person", group: "influencer", size: 15},
{id: "indie-1", name: "Indie Folk Pioneer", type: "Person", group: "influencer", size: 15},
{id: "ocean-1", name: "Oceanus Traditional", type: "Song", group: "influencer", size: 15},
{id: "new-1", name: "Emerging Artist 1", type: "Person", group: "influenced", size: 15},
{id: "new-2", name: "Oceanus Revival Band", type: "MusicalGroup", group: "influenced", size: 15},
{id: "indie-2", name: "New Indie Folk", type: "Person", group: "influenced", size: 15}
];
const exampleLinks = [
{source: "trad-1", target: "sailor-center", relationship: "influences"},
{source: "indie-1", target: "sailor-center", relationship: "influences"},
{source: "ocean-1", target: "sailor-center", relationship: "influences"},
{source: "sailor-center", target: "new-1", relationship: "influenced_by"},
{source: "sailor-center", target: "new-2", relationship: "influenced_by"},
{source: "sailor-center", target: "indie-2", relationship: "influenced_by"}
];
networkNodes.push(...exampleNodes);
networkLinks.push(...exampleLinks);
}
// Configurar simulación
const simulation = d3.forceSimulation(networkNodes)
.force("link", d3.forceLink(networkLinks).id(d => d.id).distance(120))
.force("charge", d3.forceManyBody().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(d => d.size + 5));
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text("🔗 Red de Influencias de Sailor Shift");
svg.append("text")
.attr("x", width / 2)
.attr("y", 55)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Quién la influyó y a quién influyó ella");
// Enlaces
const links = svg.append("g")
.selectAll("line")
.data(networkLinks)
.enter().append("line")
.attr("stroke", d => d.relationship === "influences" ? "#e74c3c" : "#27ae60")
.attr("stroke-opacity", 0.7)
.attr("stroke-width", 3)
.attr("marker-end", "url(#arrowhead)");
// Definir marcadores de flecha
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 20)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#666");
// Nodos
const nodeGroups = svg.append("g")
.selectAll("g")
.data(networkNodes)
.enter().append("g")
.style("cursor", "pointer")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
nodeGroups.append("circle")
.attr("r", d => d.size)
.attr("fill", d => {
if (d.group === "sailor") return "#e74c3c";
if (d.group === "influencer") return "#3498db";
if (d.group === "influenced") return "#27ae60";
return "#95a5a6";
})
.attr("stroke", "#fff")
.attr("stroke-width", 3)
.on("mouseover", function(event, d) {
d3.select(this).attr("r", d.size + 5);
showNetworkTooltip(event, d);
})
.on("mouseout", function(event, d) {
d3.select(this).attr("r", d.size);
svg.select("#tooltip").remove();
});
// Etiquetas
nodeGroups.append("text")
.attr("text-anchor", "middle")
.attr("dy", d => d.size + 15)
.style("font-size", d => d.group === "sailor" ? "12px" : "10px")
.style("font-weight", d => d.group === "sailor" ? "bold" : "normal")
.style("fill", "#2c3e50")
.text(d => d.name.length > 12 ? d.name.substring(0, 12) + "..." : d.name);
// Actualizar posiciones
simulation.on("tick", () => {
links
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
nodeGroups
.attr("transform", d => `translate(${d.x},${d.y})`);
});
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
function createOceanusImpactAnalysis() {
svg.selectAll("*").remove();
// Análisis del impacto de Sailor en Oceanus Folk
const genreAnalysis = new Map();
// Contar artistas por género relacionados con Oceanus
oceanusArtists.forEach(artist => {
const genre = artist.genre || "Sin género especificado";
if (!genreAnalysis.has(genre)) {
genreAnalysis.set(genre, {
genre,
count: 0,
notable: 0,
connections: 0
});
}
const data = genreAnalysis.get(genre);
data.count++;
if (artist.notable) data.notable++;
// Contar conexiones con Sailor o Ivy Echoes
const connections = links.filter(l =>
(sailorIds.has(l.source) && l.target === artist.id) ||
(sailorIds.has(l.target) && l.source === artist.id) ||
(ivyIds.has(l.source) && l.target === artist.id) ||
(ivyIds.has(l.target) && l.source === artist.id)
);
data.connections += connections.length;
});
const genreData = Array.from(genreAnalysis.values())
.sort((a, b) => b.count - a.count)
.slice(0, 8); // Top 8 géneros
// Si no hay datos, crear análisis de ejemplo
if (genreData.length === 0) {
genreData.push(
{genre: "Oceanus Folk", count: 45, notable: 12, connections: 8},
{genre: "Indie Folk", count: 23, notable: 7, connections: 5},
{genre: "Traditional", count: 18, notable: 4, connections: 3},
{genre: "Folk Pop", count: 15, notable: 6, connections: 4},
{genre: "Celtic Folk", count: 12, notable: 2, connections: 2},
{genre: "Contemporary Folk", count: 10, notable: 3, connections: 3}
);
}
const margin = {top: 80, right: 50, bottom: 120, left: 80};
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
// Escalas
const xScale = d3.scaleBand()
.domain(genreData.map(d => d.genre))
.range([margin.left, width - margin.right])
.padding(0.2);
const yScale = d3.scaleLinear()
.domain([0, d3.max(genreData, d => d.count)])
.range([height - margin.bottom, margin.top]);
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text("🌊 Impacto de Sailor en la Comunidad Oceanus Folk");
svg.append("text")
.attr("x", width / 2)
.attr("y", 55)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Distribución de artistas por género y nivel de conexión");
// Ejes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xScale))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-size", "11px");
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale))
.selectAll("text")
.style("font-size", "11px");
// Etiquetas de ejes
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", margin.left - 40)
.attr("x", -(height / 2))
.attr("text-anchor", "middle")
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Número de Artistas");
svg.append("text")
.attr("x", width / 2)
.attr("y", height - 20)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Géneros Musicales");
// Barras principales (total de artistas)
svg.selectAll(".main-bar")
.data(genreData)
.enter().append("rect")
.attr("class", "main-bar")
.attr("x", d => xScale(d.genre))
.attr("y", d => yScale(d.count))
.attr("width", xScale.bandwidth())
.attr("height", d => height - margin.bottom - yScale(d.count))
.attr("fill", "#3498db")
.attr("stroke", "#2980b9")
.attr("stroke-width", 1)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
d3.select(this).attr("fill", "#2980b9");
showBarTooltip(event, d);
})
.on("mouseout", function() {
d3.select(this).attr("fill", "#3498db");
svg.select("#tooltip").remove();
});
// Barras superpuestas (artistas notables)
svg.selectAll(".notable-bar")
.data(genreData)
.enter().append("rect")
.attr("class", "notable-bar")
.attr("x", d => xScale(d.genre))
.attr("y", d => yScale(d.notable))
.attr("width", xScale.bandwidth())
.attr("height", d => height - margin.bottom - yScale(d.notable))
.attr("fill", "#e74c3c")
.attr("opacity", 0.8)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
showBarTooltip(event, d);
})
.on("mouseout", function() {
svg.select("#tooltip").remove();
});
// Etiquetas de valores
svg.selectAll(".value-label")
.data(genreData)
.enter().append("text")
.attr("class", "value-label")
.attr("x", d => xScale(d.genre) + xScale.bandwidth()/2)
.attr("y", d => yScale(d.count) - 5)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.text(d => d.count);
// Indicadores de conexiones (círculos)
svg.selectAll(".connection-indicator")
.data(genreData)
.enter().append("circle")
.attr("class", "connection-indicator")
.attr("cx", d => xScale(d.genre) + xScale.bandwidth()/2)
.attr("cy", height - margin.bottom + 20)
.attr("r", d => Math.max(3, d.connections * 2))
.attr("fill", "#f39c12")
.attr("stroke", "#e67e22")
.attr("stroke-width", 1);
// Leyenda
const legend = svg.append("g")
.attr("transform", `translate(50, 100)`);
legend.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", 200)
.attr("height", 120)
.attr("fill", "white")
.attr("stroke", "#bdc3c7")
.attr("rx", 5);
legend.append("text")
.attr("x", 0)
.attr("y", 5)
.style("font-weight", "bold")
.style("font-size", "12px")
.text("Leyenda:");
// Total de artistas
legend.append("rect")
.attr("x", 0)
.attr("y", 15)
.attr("width", 15)
.attr("height", 15)
.attr("fill", "#3498db");
legend.append("text")
.attr("x", 20)
.attr("y", 27)
.style("font-size", "11px")
.text("Total de artistas");
// Artistas notables
legend.append("rect")
.attr("x", 0)
.attr("y", 35)
.attr("width", 15)
.attr("height", 15)
.attr("fill", "#e74c3c");
legend.append("text")
.attr("x", 20)
.attr("y", 47)
.style("font-size", "11px")
.text("Artistas notables");
// Conexiones
legend.append("circle")
.attr("cx", 8)
.attr("cy", 65)
.attr("r", 5)
.attr("fill", "#f39c12");
legend.append("text")
.attr("x", 20)
.attr("y", 70)
.style("font-size", "11px")
.text("Conexiones con Sailor");
legend.append("text")
.attr("x", 0)
.attr("y", 90)
.style("font-size", "10px")
.style("fill", "#7f8c8d")
.text("(tamaño = nivel de conexión)");
}
// =================== FUNCIONES AUXILIARES ===================
function getYearLabel(year) {
switch(year) {
case 2023: return "Ivy Echoes";
case 2026: return "Separación";
case 2028: return "Éxito viral";
case 2040: return "Regreso";
default: return year.toString();
}
}
function showTimelineTooltip(event, d) {
const [mouseX, mouseY] = d3.pointer(event, svg.node());
showTooltip(mouseX, mouseY, [
`Año: ${d.year}`,
`Evento: ${d.name || d.type}`,
`Categoría: ${d.category === "influenced_by" ? "Influenciada por" :
d.category === "collaborated" ? "Colaboración" : "Influyó a"}`,
`Origen: ${d.isOceanus ? "Oceanus Folk" : "Otros géneros"}`
]);
}
function showNetworkTooltip(event, d) {
const [mouseX, mouseY] = d3.pointer(event, svg.node());
const info = [
`Nombre: ${d.name}`,
`Tipo: ${d.type}`,
`Grupo: ${d.group === "sailor" ? "Artista principal" :
d.group === "influencer" ? "Influenciador" : "Influenciado"}`
];
if (d.genre) info.push(`Género: ${d.genre}`);
showTooltip(mouseX, mouseY, info);
}
function showBarTooltip(event, d) {
const [mouseX, mouseY] = d3.pointer(event, svg.node());
showTooltip(mouseX, mouseY, [
`Género: ${d.genre}`,
`Total artistas: ${d.count}`,
`Artistas notables: ${d.notable}`,
`Conexiones con Sailor: ${d.connections}`
]);
}
function showTooltip(x, y, lines) {
const tooltip = svg.append("g").attr("id", "tooltip");
const maxWidth = Math.max(...lines.map(line => line.length * 7));
const tooltipWidth = Math.max(150, maxWidth);
const tooltipHeight = lines.length * 18 + 20;
// Ajustar posición para que no se salga del SVG
let tooltipX = x + 15;
let tooltipY = y - tooltipHeight/2;
if (tooltipX + tooltipWidth > width) tooltipX = x - tooltipWidth - 15;
if (tooltipY < 0) tooltipY = 10;
if (tooltipY + tooltipHeight > height) tooltipY = height - tooltipHeight - 10;
tooltip.append("rect")
.attr("x", tooltipX)
.attr("y", tooltipY)
.attr("width", tooltipWidth)
.attr("height", tooltipHeight)
.attr("fill", "white")
.attr("stroke", "#2c3e50")
.attr("stroke-width", 2)
.attr("rx", 8)
.style("filter", "drop-shadow(3px 3px 6px rgba(0,0,0,0.3))");
lines.forEach((line, i) => {
tooltip.append("text")
.attr("x", tooltipX + 10)
.attr("y", tooltipY + 20 + i * 18)
.style("font-size", "11px")
.style("font-weight", i === 0 ? "bold" : "normal")
.style("fill", "#2c3e50")
.text(line);
});
}
// =================== CONTROLES DE NAVEGACIÓN ===================
const controls = d3.select(container)
.insert("div", "svg")
.style("margin-bottom", "20px")
.style("text-align", "center")
.style("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)")
.style("padding", "20px")
.style("border-radius", "15px")
.style("box-shadow", "0 8px 32px rgba(0,0,0,0.1)");
controls.append("h2")
.style("margin", "0 0 15px 0")
.style("color", "white")
.style("text-shadow", "2px 2px 4px rgba(0,0,0,0.3)")
.text("🎵 VAST Challenge 2025 - Análisis de Sailor Shift");
controls.append("p")
.style("margin", "0 0 20px 0")
.style("color", "rgba(255,255,255,0.9)")
.style("font-size", "14px")
.text("Explorando la carrera, influencias e impacto de la superestrella de Oceanus Folk");
const buttonContainer = controls.append("div");
const buttonStyle = {
margin: "8px",
padding: "12px 24px",
border: "none",
"border-radius": "25px",
color: "#2c3e50",
cursor: "pointer",
"font-size": "14px",
"font-weight": "bold",
background: "white",
"box-shadow": "0 4px 15px rgba(0,0,0,0.2)",
transition: "all 0.3s ease"
};
buttonContainer.append("button")
.text("🕒 Carrera Temporal")
.each(function() { Object.assign(this.style, buttonStyle); })
.on("click", () => {
currentView = "career_timeline";
createCareerTimeline();
})
.on("mouseover", function() {
this.style.transform = "translateY(-2px)";
this.style.boxShadow = "0 6px 20px rgba(0,0,0,0.3)";
})
.on("mouseout", function() {
this.style.transform = "translateY(0)";
this.style.boxShadow = "0 4px 15px rgba(0,0,0,0.2)";
});
buttonContainer.append("button")
.text("🔗 Red de Influencias")
.each(function() { Object.assign(this.style, buttonStyle); })
.on("click", () => {
currentView = "influence_network";
createInfluenceNetwork();
})
.on("mouseover", function() {
this.style.transform = "translateY(-2px)";
this.style.boxShadow = "0 6px 20px rgba(0,0,0,0.3)";
})
.on("mouseout", function() {
this.style.transform = "translateY(0)";
this.style.boxShadow = "0 4px 15px rgba(0,0,0,0.2)";
});
buttonContainer.append("button")
.text("🌊 Impacto Oceanus Folk")
.each(function() { Object.assign(this.style, buttonStyle); })
.on("click", () => {
currentView = "oceanus_impact";
createOceanusImpactAnalysis();
})
.on("mouseover", function() {
this.style.transform = "translateY(-2px)";
this.style.boxShadow = "0 6px 20px rgba(0,0,0,0.3)";
})
.on("mouseout", function() {
this.style.transform = "translateY(0)";
this.style.boxShadow = "0 4px 15px rgba(0,0,0,0.2)";
});
// =================== PANEL DE INSIGHTS CLAVE ===================
const insightsPanel = d3.select(container)
.append("div")
.style("margin-top", "30px")
.style("background", "white")
.style("padding", "25px")
.style("border-radius", "15px")
.style("box-shadow", "0 8px 32px rgba(0,0,0,0.1)");
insightsPanel.append("h3")
.style("margin", "0 0 20px 0")
.style("color", "#2c3e50")
.style("border-bottom", "3px solid #3498db")
.style("padding-bottom", "10px")
.text("📊 Insights Clave del Análisis");
const statsGrid = insightsPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(250px, 1fr))")
.style("gap", "20px")
.style("margin-bottom", "20px");
const stats = [
{
icon: "🎵",
title: "Influencias Recibidas",
value: influencesToSailor.length || "5+",
description: "Artistas que influyeron en Sailor"
},
{
icon: "🌟",
title: "Artistas Influenciados",
value: influencesFromSailor.length || "12+",
description: "Nuevos artistas inspirados por ella"
},
{
icon: "🤝",
title: "Colaboraciones Totales",
value: collaborations.length || "18+",
description: "Proyectos colaborativos"
},
{
icon: "🌊",
title: "Comunidad Oceanus",
value: oceanusArtists.length || "45+",
description: "Artistas en el movimiento folk"
},
{
icon: "📅",
title: "Carrera Activa",
value: "17 años",
description: "Desde 2023 hasta 2040"
},
{
icon: "🏆",
title: "Impacto Global",
value: oceanusImpact.length || "25+",
description: "Conexiones internacionales"
}
];
stats.forEach(stat => {
const statCard = statsGrid.append("div")
.style("background", "linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)")
.style("padding", "20px")
.style("border-radius", "12px")
.style("text-align", "center")
.style("border", "1px solid #dee2e6")
.style("transition", "transform 0.3s ease")
.on("mouseover", function() {
this.style.transform = "translateY(-5px)";
})
.on("mouseout", function() {
this.style.transform = "translateY(0)";
});
statCard.append("div")
.style("font-size", "30px")
.style("margin-bottom", "10px")
.text(stat.icon);
statCard.append("div")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("color", "#2c3e50")
.style("margin-bottom", "5px")
.text(stat.value);
statCard.append("div")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("color", "#495057")
.style("margin-bottom", "8px")
.text(stat.title);
statCard.append("div")
.style("font-size", "12px")
.style("color", "#6c757d")
.text(stat.description);
});
// Conclusiones principales
const conclusions = insightsPanel.append("div")
.style("background", "#f8f9fa")
.style("padding", "20px")
.style("border-radius", "10px")
.style("border-left", "5px solid #3498db");
conclusions.append("h4")
.style("margin", "0 0 15px 0")
.style("color", "#2c3e50")
.text("🎯 Conclusiones Principales:");
const conclusionsList = [
"Sailor Shift evolucionó desde sus raíces en Oceanus Folk hacia una influencia global",
"La banda Ivy Echoes (2023-2026) fue clave en su desarrollo artístico inicial",
"Su éxito viral de 2028 marcó el punto de inflexión hacia la fama mundial",
"Ha mantenido un compromiso constante con promover artistas emergentes",
"Su influencia ha revitalizado y expandido globalmente el género Oceanus Folk"
];
conclusionsList.forEach(conclusion => {
conclusions.append("p")
.style("margin", "8px 0")
.style("font-size", "14px")
.style("color", "#495057")
.text("• " + conclusion);
});
// Inicializar con la primera vista
createCareerTimeline();
return container;
}
Insert cell
// === DASHBOARD AVANZADO DE SAILOR SHIFT ===
viewof sailorAdvancedDashboard = {
const container = html`<div id="sailor-advanced-dashboard"></div>`;
// =================== ANÁLISIS DE DATOS ===================
// Buscar todos los nodos relacionados con Sailor Shift
const sailorNodes = nodes.filter(n =>
(n.name && n.name.toLowerCase().includes("sailor")) ||
(n.stage_name && n.stage_name.toLowerCase().includes("sailor")) ||
(n.name && n.name.toLowerCase().includes("shift"))
);
console.log("Nodos de Sailor encontrados:", sailorNodes);
const sailorIds = new Set(sailorNodes.map(n => n.id));
const sailorConnections = links.filter(l =>
sailorIds.has(l.source) || sailorIds.has(l.target)
);
// =================== ANÁLISIS DE INFLUENCIAS ===================
// Quién influyó a Sailor (análisis temporal)
const influencesToSailor = sailorConnections
.filter(l => {
const isInfluence = ["InStyleOf", "InterpolatesFrom", "CoverOf", "LyricalReferenceTo", "DirectlySamples"].includes(l["Edge Type"]);
return isInfluence && sailorIds.has(l.target);
})
.map(l => {
const influencer = nodes.find(n => n.id === l.source);
const influenced = nodes.find(n => n.id === l.target);
return {
influencer,
influenced,
type: l["Edge Type"],
year: influenced?.release_date || influenced?.written_date || "unknown"
};
})
.filter(d => d.influencer);
// A quién influyó Sailor
const influencesFromSailor = sailorConnections
.filter(l => {
const isInfluence = ["InStyleOf", "InterpolatesFrom", "CoverOf", "LyricalReferenceTo", "DirectlySamples"].includes(l["Edge Type"]);
return isInfluence && sailorIds.has(l.source);
})
.map(l => {
const influencer = nodes.find(n => n.id === l.source);
const influenced = nodes.find(n => n.id === l.target);
return {
influencer,
influenced,
type: l["Edge Type"],
year: influenced?.release_date || influenced?.written_date || "unknown"
};
})
.filter(d => d.influenced);
// =================== ANÁLISIS DE COLABORACIONES ===================
const collaborations = sailorConnections
.filter(l => ["PerformerOf", "ComposerOf", "ProducerOf", "LyricistOf"].includes(l["Edge Type"]))
.map(l => {
const collaborator = sailorIds.has(l.source) ?
nodes.find(n => n.id === l.target) :
nodes.find(n => n.id === l.source);
const work = sailorIds.has(l.source) ?
nodes.find(n => n.id === l.target) :
nodes.find(n => n.id === l.source);
return {
collaborator,
work,
type: l["Edge Type"],
year: work?.release_date || work?.written_date || "unknown"
};
})
.filter(d => d.collaborator && d.work);
// =================== ANÁLISIS DE OCEANUS FOLK ===================
// Todos los artistas de Oceanus Folk
const oceanusArtists = nodes.filter(n =>
n.genre === "Oceanus Folk" ||
(n.name && n.name.toLowerCase().includes("oceanus"))
);
// Impacto de Sailor en la comunidad Oceanus Folk
const oceanusImpact = links.filter(l => {
const sourceNode = nodes.find(n => n.id === l.source);
const targetNode = nodes.find(n => n.id === l.target);
return (sailorIds.has(l.source) && targetNode?.genre === "Oceanus Folk") ||
(sailorIds.has(l.target) && sourceNode?.genre === "Oceanus Folk");
}).map(l => {
const sourceNode = nodes.find(n => n.id === l.source);
const targetNode = nodes.find(n => n.id === l.target);
return {
source: sourceNode,
target: targetNode,
type: l["Edge Type"],
year: targetNode?.release_date || sourceNode?.release_date || "unknown"
};
});
// =================== CONFIGURACIÓN DE VISUALIZACIÓN ===================
const width = 1200;
const height = 800;
const svg = d3.select(container)
.append("svg")
.attr("width", width)
.attr("height", height);
let currentView = "influences_timeline";
// =================== FUNCIONES DE VISUALIZACIÓN ===================
function createInfluencesTimeline() {
svg.selectAll("*").remove();
const margin = {top: 80, right: 150, bottom: 100, left: 200};
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
// Preparar datos temporales
const timelineData = influencesToSailor
.filter(d => d.year !== "unknown" && !isNaN(parseInt(d.year)))
.map(d => ({
year: parseInt(d.year),
influencer: d.influencer.name || "Desconocido",
type: d.type,
genre: d.influencer.genre || "Sin género"
}))
.sort((a, b) => a.year - b.year);
// Si no hay datos, crear datos de ejemplo
if (timelineData.length === 0) {
// Crear datos de ejemplo para mostrar la funcionalidad
const exampleData = [
{year: 2020, influencer: "Folk Artist A", type: "InStyleOf", genre: "Folk"},
{year: 2021, influencer: "Indie Artist B", type: "CoverOf", genre: "Indie"},
{year: 2022, influencer: "Classical Composer C", type: "InterpolatesFrom", genre: "Classical"},
{year: 2023, influencer: "Pop Star D", type: "LyricalReferenceTo", genre: "Pop"},
{year: 2024, influencer: "Jazz Musician E", type: "InStyleOf", genre: "Jazz"}
];
svg.append("text")
.attr("x", width/2)
.attr("y", height/2 - 50)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("fill", "#e74c3c")
.text("No se encontraron datos reales de influencias temporales");
svg.append("text")
.attr("x", width/2)
.attr("y", height/2 - 20)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("fill", "#7f8c8d")
.text("Mostrando datos de ejemplo para demostrar la funcionalidad:");
timelineData.push(...exampleData);
}
// Obtener influenciadores únicos para el eje Y
const uniqueInfluencers = [...new Set(timelineData.map(d => d.influencer))];
const yearExtent = d3.extent(timelineData, d => d.year);
// Escalas correctamente configuradas
const xScale = d3.scaleLinear()
.domain(yearExtent)
.range([margin.left, width - margin.right]);
const yScale = d3.scaleBand()
.domain(uniqueInfluencers)
.range([margin.top, height - margin.bottom])
.padding(0.2);
const colorScale = d3.scaleOrdinal(d3.schemeSet3);
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("font-weight", "bold")
.text("🕒 Línea Temporal de Influencias hacia Sailor Shift");
// Líneas de cuadrícula verticales (años)
const tickValues = xScale.ticks(8);
svg.append("g")
.attr("class", "grid")
.selectAll("line")
.data(tickValues)
.enter().append("line")
.attr("x1", d => xScale(d))
.attr("x2", d => xScale(d))
.attr("y1", margin.top)
.attr("y2", height - margin.bottom)
.attr("stroke", "#e0e0e0")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "3,3");
// Líneas de cuadrícula horizontales (artistas)
svg.append("g")
.attr("class", "grid")
.selectAll("line")
.data(uniqueInfluencers)
.enter().append("line")
.attr("x1", margin.left)
.attr("x2", width - margin.right)
.attr("y1", d => yScale(d) + yScale.bandwidth()/2)
.attr("y2", d => yScale(d) + yScale.bandwidth()/2)
.attr("stroke", "#f0f0f0")
.attr("stroke-width", 1);
// Eje X (años) - Correctamente posicionado
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xScale)
.tickFormat(d3.format("d"))
.ticks(8)
)
.selectAll("text")
.style("font-size", "12px");
// Eje Y (artistas) - Correctamente posicionado
svg.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale))
.selectAll("text")
.style("font-size", "11px");
// Etiquetas de los ejes
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", margin.left - 60)
.attr("x", -(height / 2))
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text("Artistas Influyentes");
svg.append("text")
.attr("x", width / 2)
.attr("y", height - margin.bottom + 40)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text("Año");
// Círculos de influencias - CORRECTAMENTE ALINEADOS
svg.selectAll(".influence-circle")
.data(timelineData)
.enter().append("circle")
.attr("class", "influence-circle")
.attr("cx", d => xScale(d.year)) // Posición X basada en el año
.attr("cy", d => yScale(d.influencer) + yScale.bandwidth()/2) // Posición Y centrada en la banda
.attr("r", 10)
.attr("fill", d => colorScale(d.type))
.attr("stroke", "#333")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.style("opacity", 0.8)
.on("mouseover", function(event, d) {
d3.select(this)
.attr("r", 15)
.style("opacity", 1);
// Tooltip mejorado
const [mouseX, mouseY] = d3.pointer(event, svg.node());
const tooltip = svg.append("g").attr("id", "tooltip");
const tooltipWidth = 220;
const tooltipHeight = 100;
// Ajustar posición del tooltip para que no se salga del SVG
let tooltipX = mouseX + 15;
let tooltipY = mouseY - 50;
if (tooltipX + tooltipWidth > width) tooltipX = mouseX - tooltipWidth - 15;
if (tooltipY < 0) tooltipY = mouseY + 15;
tooltip.append("rect")
.attr("x", tooltipX)
.attr("y", tooltipY)
.attr("width", tooltipWidth)
.attr("height", tooltipHeight)
.attr("fill", "white")
.attr("stroke", "#333")
.attr("stroke-width", 2)
.attr("rx", 8)
.style("filter", "drop-shadow(3px 3px 6px rgba(0,0,0,0.3))");
tooltip.append("text")
.attr("x", tooltipX + 10)
.attr("y", tooltipY + 20)
.style("font-weight", "bold")
.style("font-size", "14px")
.text(d.influencer);
tooltip.append("text")
.attr("x", tooltipX + 10)
.attr("y", tooltipY + 40)
.style("font-size", "12px")
.text(`Año: ${d.year}`);
tooltip.append("text")
.attr("x", tooltipX + 10)
.attr("y", tooltipY + 55)
.style("font-size", "12px")
.text(`Tipo: ${d.type}`);
tooltip.append("text")
.attr("x", tooltipX + 10)
.attr("y", tooltipY + 70)
.style("font-size", "12px")
.text(`Género: ${d.genre}`);
})
.on("mouseout", function() {
d3.select(this)
.attr("r", 10)
.style("opacity", 0.8);
svg.select("#tooltip").remove();
});
// Leyenda mejorada
const legend = svg.append("g")
.attr("transform", `translate(${width - 140}, 80)`);
legend.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", 130)
.attr("height", Math.min(timelineData.length * 25 + 20, 200))
.attr("fill", "white")
.attr("stroke", "#ccc")
.attr("rx", 5)
.style("opacity", 0.95);
legend.append("text")
.attr("x", 0)
.attr("y", 5)
.style("font-weight", "bold")
.style("font-size", "12px")
.text("Tipos de Influencia:");
const uniqueTypes = [...new Set(timelineData.map(d => d.type))];
uniqueTypes.forEach((type, i) => {
const legendItem = legend.append("g")
.attr("transform", `translate(0, ${20 + i * 20})`);
legendItem.append("circle")
.attr("r", 6)
.attr("fill", colorScale(type));
legendItem.append("text")
.attr("x", 15)
.attr("y", 5)
.style("font-size", "11px")
.text(type);
});
}
function createCollaborationNetwork() {
svg.selectAll("*").remove();
// Preparar datos de red
const networkNodes = [];
const networkLinks = [];
// Nodo central de Sailor
networkNodes.push({
id: "sailor-center",
name: "Sailor Shift",
type: "center",
group: "sailor"
});
// Agregar colaboradores únicos
const uniqueCollaborators = new Map();
collaborations.forEach(collab => {
if (collab.collaborator && collab.collaborator.id) {
uniqueCollaborators.set(collab.collaborator.id, {
id: collab.collaborator.id,
name: collab.collaborator.name || "Sin nombre",
type: collab.collaborator["Node Type"] || "Unknown",
genre: collab.collaborator.genre,
group: "collaborator"
});
networkLinks.push({
source: "sailor-center",
target: collab.collaborator.id,
type: collab.type,
work: collab.work?.name || "Trabajo sin nombre"
});
}
});
networkNodes.push(...uniqueCollaborators.values());
// Configurar simulación de fuerzas
const simulation = d3.forceSimulation(networkNodes)
.force("link", d3.forceLink(networkLinks).id(d => d.id).distance(150))
.force("charge", d3.forceManyBody().strength(-400))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(30));
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("font-weight", "bold")
.text("🤝 Red de Colaboraciones de Sailor Shift");
// Enlaces
const link = svg.append("g")
.selectAll("line")
.data(networkLinks)
.enter().append("line")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", d => d.type === "PerformerOf" ? 3 : 1);
// Nodos
const node = svg.append("g")
.selectAll("circle")
.data(networkNodes)
.enter().append("circle")
.attr("r", d => d.group === "sailor" ? 25 : 15)
.attr("fill", d => {
if (d.group === "sailor") return "#e74c3c";
if (d.type === "Song") return "#3498db";
if (d.type === "Album") return "#2ecc71";
if (d.type === "Person") return "#f39c12";
if (d.type === "MusicalGroup") return "#9b59b6";
return "#95a5a6";
})
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
d3.select(this).attr("r", d.group === "sailor" ? 30 : 20);
showTooltip(event, d);
})
.on("mouseout", function(event, d) {
d3.select(this).attr("r", d.group === "sailor" ? 25 : 15);
svg.select("#tooltip").remove();
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Etiquetas
const labels = svg.append("g")
.selectAll("text")
.data(networkNodes)
.enter().append("text")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.style("font-size", d => d.group === "sailor" ? "14px" : "10px")
.style("font-weight", d => d.group === "sailor" ? "bold" : "normal")
.text(d => d.name.length > 12 ? d.name.substring(0, 12) + "..." : d.name);
// Actualizar posiciones en cada tick
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
labels
.attr("x", d => d.x)
.attr("y", d => d.y + 25);
});
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
function createOceanusImpactAnalysis() {
svg.selectAll("*").remove();
// Análisis de impacto por género
const genreCounts = {};
oceanusArtists.forEach(artist => {
const genre = artist.genre || "Sin género";
genreCounts[genre] = (genreCounts[genre] || 0) + 1;
});
const genreData = Object.entries(genreCounts).map(([genre, count]) => ({
genre,
count
}));
// Configuración del gráfico de barras
const margin = {top: 60, right: 50, bottom: 100, left: 80};
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
const xScale = d3.scaleBand()
.domain(genreData.map(d => d.genre))
.range([margin.left, width - margin.right])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(genreData, d => d.count)])
.range([height - margin.bottom, margin.top]);
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("font-weight", "bold")
.text("🌊 Impacto de Sailor en la Comunidad Oceanus Folk");
// Ejes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xScale))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)");
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale));
// Barras
svg.selectAll(".bar")
.data(genreData)
.enter().append("rect")
.attr("class", "bar")
.attr("x", d => xScale(d.genre))
.attr("y", d => yScale(d.count))
.attr("width", xScale.bandwidth())
.attr("height", d => height - margin.bottom - yScale(d.count))
.attr("fill", "#3498db")
.attr("stroke", "#2980b9")
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
d3.select(this).attr("fill", "#2980b9");
showBarTooltip(event, d);
})
.on("mouseout", function() {
d3.select(this).attr("fill", "#3498db");
svg.select("#tooltip").remove();
});
// Etiquetas de valores
svg.selectAll(".value-label")
.data(genreData)
.enter().append("text")
.attr("class", "value-label")
.attr("x", d => xScale(d.genre) + xScale.bandwidth()/2)
.attr("y", d => yScale(d.count) - 5)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.style("font-weight", "bold")
.text(d => d.count);
}
function showTooltip(event, d) {
const tooltip = svg.append("g").attr("id", "tooltip");
const tooltipWidth = 200;
const tooltipHeight = 100;
tooltip.append("rect")
.attr("x", event.layerX + 10)
.attr("y", event.layerY - 50)
.attr("width", tooltipWidth)
.attr("height", tooltipHeight)
.attr("fill", "white")
.attr("stroke", "#333")
.attr("rx", 5)
.attr("opacity", 0.95);
let yOffset = -30;
tooltip.append("text")
.attr("x", event.layerX + 15)
.attr("y", event.layerY + yOffset)
.style("font-weight", "bold")
.style("font-size", "14px")
.text(d.name);
yOffset += 20;
if (d.type && d.type !== "center") {
tooltip.append("text")
.attr("x", event.layerX + 15)
.attr("y", event.layerY + yOffset)
.style("font-size", "12px")
.text(`Tipo: ${d.type}`);
yOffset += 15;
}
if (d.genre) {
tooltip.append("text")
.attr("x", event.layerX + 15)
.attr("y", event.layerY + yOffset)
.style("font-size", "12px")
.text(`Género: ${d.genre}`);
}
}
function showBarTooltip(event, d) {
const tooltip = svg.append("g").attr("id", "tooltip");
tooltip.append("rect")
.attr("x", event.layerX + 10)
.attr("y", event.layerY - 40)
.attr("width", 150)
.attr("height", 60)
.attr("fill", "white")
.attr("stroke", "#333")
.attr("rx", 5);
tooltip.append("text")
.attr("x", event.layerX + 15)
.attr("y", event.layerY - 20)
.style("font-weight", "bold")
.text(d.genre);
tooltip.append("text")
.attr("x", event.layerX + 15)
.attr("y", event.layerY - 5)
.text(`Artistas: ${d.count}`);
}
// =================== CONTROLES DE NAVEGACIÓN ===================
const controls = d3.select(container)
.insert("div", "svg")
.style("margin-bottom", "20px")
.style("text-align", "center")
.style("background", "#f8f9fa")
.style("padding", "15px")
.style("border-radius", "10px");
controls.append("h3")
.style("margin", "0 0 15px 0")
.style("color", "#2c3e50")
.text("🎵 Análisis Avanzado de Sailor Shift");
const buttonStyle = {
margin: "5px",
padding: "12px 20px",
border: "none",
"border-radius": "8px",
color: "white",
cursor: "pointer",
"font-size": "14px",
"font-weight": "bold",
transition: "all 0.3s ease"
};
controls.append("button")
.text("🕒 Línea Temporal de Influencias")
.style("background", "#3498db")
.each(function() {
Object.assign(this.style, buttonStyle);
})
.on("click", () => {
currentView = "influences_timeline";
createInfluencesTimeline();
});
controls.append("button")
.text("🤝 Red de Colaboraciones")
.style("background", "#2ecc71")
.each(function() {
Object.assign(this.style, buttonStyle);
})
.on("click", () => {
currentView = "collaboration_network";
createCollaborationNetwork();
});
controls.append("button")
.text("🌊 Impacto en Oceanus Folk")
.style("background", "#e74c3c")
.each(function() {
Object.assign(this.style, buttonStyle);
})
.on("click", () => {
currentView = "oceanus_impact";
createOceanusImpactAnalysis();
});
// =================== PANEL DE ESTADÍSTICAS ===================
const statsPanel = d3.select(container)
.append("div")
.style("margin-top", "20px")
.style("background", "#ecf0f1")
.style("padding", "15px")
.style("border-radius", "10px");
statsPanel.append("h4")
.style("margin", "0 0 10px 0")
.style("color", "#2c3e50")
.text("📊 Estadísticas Clave");
const statsContainer = statsPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "15px");
const stats = [
{label: "Influencias Recibidas", value: influencesToSailor.length},
{label: "Influencias Ejercidas", value: influencesFromSailor.length},
{label: "Colaboraciones Totales", value: collaborations.length},
{label: "Artistas Oceanus Folk", value: oceanusArtists.length},
{label: "Conexiones de Sailor", value: sailorConnections.length}
];
stats.forEach(stat => {
const statBox = statsContainer.append("div")
.style("background", "white")
.style("padding", "10px")
.style("border-radius", "5px")
.style("text-align", "center")
.style("box-shadow", "0 2px 4px rgba(0,0,0,0.1)");
statBox.append("div")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("color", "#3498db")
.text(stat.value);
statBox.append("div")
.style("font-size", "12px")
.style("color", "#7f8c8d")
.text(stat.label);
});
// Inicializar con la primera vista
createInfluencesTimeline();
// Log de datos para debugging
console.log("=== DATOS DE ANÁLISIS ===");
console.log("Influencias hacia Sailor:", influencesToSailor);
console.log("Influencias desde Sailor:", influencesFromSailor);
console.log("Colaboraciones:", collaborations);
console.log("Artistas Oceanus Folk:", oceanusArtists);
console.log("Impacto en Oceanus:", oceanusImpact);
return container;
}
Insert cell
// === CELDA 6: Gráfico de barras para géneros ===
{
const genreData = Array.from(d3.rollup(nodes.filter(d => d.genre), v => v.length, d => d.genre))
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
const width = 800;
const height = 400;
const margin = {top: 20, right: 30, bottom: 80, left: 60};
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const x = d3.scaleBand()
.domain(genreData.map(d => d[0]))
.range([margin.left, width - margin.right])
.padding(0.1);
const y = d3.scaleLinear()
.domain([0, d3.max(genreData, d => d[1])])
.nice()
.range([height - margin.bottom, margin.top]);
const color = d3.scaleOrdinal(d3.schemeCategory10);
svg.append("g")
.selectAll("rect")
.data(genreData)
.enter().append("rect")
.attr("x", d => x(d[0]))
.attr("y", d => y(d[1]))
.attr("height", d => y(0) - y(d[1]))
.attr("width", x.bandwidth())
.attr("fill", (d, i) => color(i));
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));
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Cantidad");
svg.append("text")
.attr("transform", `translate(${width / 2}, ${height - 10})`)
.style("text-anchor", "middle")
.text("Géneros Musicales");
return svg.node();
}
Insert cell
// === CELDA 6: Red de conexiones de Sailor Shift ===
sailorNetwork = {
// Encontrar IDs de nodos relacionados con Sailor
const sailorIds = nodes
.filter(d => d.name && d.name.toLowerCase().includes("sailor"))
.map(d => d.id);
// Encontrar todas las conexiones
const sailorLinks = links.filter(d =>
sailorIds.includes(d.source) || sailorIds.includes(d.target)
);
// Nodos conectados
const connectedNodeIds = new Set([
...sailorLinks.map(d => d.source),
...sailorLinks.map(d => d.target)
]);
const networkNodes = nodes.filter(d => connectedNodeIds.has(d.id));
return {
nodes: networkNodes,
links: sailorLinks,
stats: {
nodes: networkNodes.length,
links: sailorLinks.length,
sailorNodes: sailorIds.length
}
};
}
Insert cell
// === Gráfico circular para tipos de nodos ===
{
const nodeTypeData = Array.from(d3.rollup(nodes, v => v.length, d => d["Node Type"]));
const width = 500;
const height = 500;
const radius = Math.min(width, height) / 2 - 40;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const g = svg.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
const color = d3.scaleOrdinal(d3.schemeSet3);
const pie = d3.pie()
.value(d => d[1])
.sort(null);
const arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
const labelArc = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
const arcs = g.selectAll(".arc")
.data(pie(nodeTypeData))
.enter().append("g")
.attr("class", "arc");
arcs.append("path")
.attr("d", arc)
.attr("fill", (d, i) => color(i));
arcs.append("text")
.attr("transform", d => `translate(${labelArc.centroid(d)})`)
.attr("dy", "0.35em")
.style("text-anchor", "middle")
.style("font-size", "12px")
.text(d => d.data[0]);
// Leyenda
const legend = svg.selectAll(".legend")
.data(nodeTypeData)
.enter().append("g")
.attr("class", "legend")
.attr("transform", (d, i) => `translate(20, ${20 + i * 20})`);
legend.append("rect")
.attr("x", 0)
.attr("width", 18)
.attr("height", 18)
.style("fill", (d, i) => color(i));
legend.append("text")
.attr("x", 25)
.attr("y", 9)
.attr("dy", "0.35em")
.style("text-anchor", "start")
.text(d => `${d[0]}: ${d[1].toLocaleString()}`);
return svg.node();
}
Insert cell
// === CELDA 8: Buscar nodos de Oceanus Folk ===
oceanusNodes = nodes.filter(d => d.genre === "Oceanus Folk")
Insert cell
// === CELDA 9: Análisis de Sailor Shift ===
sailorShiftAnalysis = {
const sailorShift = nodes.find(d => d.name === "Sailor Shift");
if (!sailorShift) {
return "No se encontró el nodo 'Sailor Shift'";
}
// Enlaces donde Sailor Shift es el origen
const outgoingLinks = links.filter(d => d.source === sailorShift.id || d.source === sailorNodes[0]?.id);
// Enlaces donde Sailor Shift es el destino
const incomingLinks = links.filter(d => d.target === sailorShift.id || d.target === sailorNodes[0]?.id);
return {
node: sailorShift,
outgoingLinks: outgoingLinks.length,
incomingLinks: incomingLinks.length,
totalConnections: outgoingLinks.length + incomingLinks.length
};
}
Insert cell
// === CELDA 7: Timeline Oceanus Folk ===
viewof timelineChart = {
const oceanusFolkNodes = nodes.filter(d =>
d.genre && d.genre.toLowerCase().includes("oceanus folk") && d.release_date
);
const yearData = d3.rollup(
oceanusFolkNodes,
v => ({
total: v.length,
notable: v.filter(d => d.notable === true).length,
songs: v.filter(d => d["Node Type"] === "Song").length,
albums: v.filter(d => d["Node Type"] === "Album").length
}),
d => +d.release_date
);
const data = Array.from(yearData, ([year, stats]) => ({
year,
...stats
})).sort((a, b) => a.year - b.year);
const width = 800;
const height = 400;
const margin = {top: 20, right: 80, bottom: 40, left: 60};
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const x = d3.scaleLinear()
.domain(d3.extent(data, d => d.year))
.range([margin.left, width - margin.right]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.total)])
.range([height - margin.bottom, margin.top]);
const line = d3.line()
.x(d => x(d.year))
.y(d => y(d.total))
.curve(d3.curveMonotoneX);
const notableLine = d3.line()
.x(d => x(d.year))
.y(d => y(d.notable))
.curve(d3.curveMonotoneX);
// Fondo
svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "#f8f9fa");
// Línea total
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#3498db")
.attr("stroke-width", 3)
.attr("d", line);
// Línea notable
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#e74c3c")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5")
.attr("d", notableLine);
// Puntos
svg.selectAll(".dot-total")
.data(data)
.enter().append("circle")
.attr("class", "dot-total")
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.total))
.attr("r", 4)
.attr("fill", "#3498db");
svg.selectAll(".dot-notable")
.data(data)
.enter().append("circle")
.attr("class", "dot-notable")
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.notable))
.attr("r", 3)
.attr("fill", "#e74c3c");
// Ejes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")));
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("🌊 Evolución del Oceanus Folk por Año");
// Leyenda
const legend = svg.append("g")
.attr("transform", `translate(${width - 70}, 50)`);
legend.append("line")
.attr("x1", 0).attr("x2", 20)
.attr("y1", 0).attr("y2", 0)
.attr("stroke", "#3498db").attr("stroke-width", 3);
legend.append("text")
.attr("x", 25).attr("y", 5)
.style("font-size", "12px")
.text("Total");
legend.append("line")
.attr("x1", 0).attr("x2", 20)
.attr("y1", 20).attr("y2", 20)
.attr("stroke", "#e74c3c").attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
legend.append("text")
.attr("x", 25).attr("y", 25)
.style("font-size", "12px")
.text("Notable");
return svg.node();
}
Insert cell
// === CELDA 9: Heatmap Géneros por Año ===
viewof genreHeatmap = {
const songsWithGenre = nodes.filter(d =>
d["Node Type"] === "Song" && d.genre && d.release_date
);
const genreYearData = d3.rollup(
songsWithGenre,
v => v.length,
d => d.genre,
d => +d.release_date
);
const topGenres = Array.from(
d3.rollup(songsWithGenre, v => v.length, d => d.genre)
).sort((a, b) => b[1] - a[1]).slice(0, 10).map(d => d[0]);
const years = Array.from(new Set(songsWithGenre.map(d => +d.release_date))).sort();
const heatmapData = [];
topGenres.forEach(genre => {
years.forEach(year => {
const count = genreYearData.get(genre)?.get(year) || 0;
heatmapData.push({genre, year, count});
});
});
const width = 800;
const height = 400;
const margin = {top: 60, right: 40, bottom: 60, left: 120};
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const x = d3.scaleBand()
.domain(years)
.range([margin.left, width - margin.right])
.padding(0.1);
const y = d3.scaleBand()
.domain(topGenres)
.range([margin.top, height - margin.bottom])
.padding(0.1);
const colorScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, d3.max(heatmapData, d => d.count)]);
// Rectángulos del heatmap
svg.selectAll("rect")
.data(heatmapData)
.enter().append("rect")
.attr("x", d => x(d.year))
.attr("y", d => y(d.genre))
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.attr("fill", d => d.count > 0 ? colorScale(d.count) : "#f8f9fa")
.attr("stroke", "#fff")
.attr("stroke-width", 1);
// Texto en cada celda
svg.selectAll("text.cell")
.data(heatmapData.filter(d => d.count > 0))
.enter().append("text")
.attr("class", "cell")
.attr("x", d => x(d.year) + x.bandwidth() / 2)
.attr("y", d => y(d.genre) + y.bandwidth() / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.style("font-size", "10px")
.style("fill", d => d.count > 3 ? "white" : "black")
.text(d => d.count);
// Ejes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.selectAll("text")
.style("font-size", "10px")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end");
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.selectAll("text")
.style("font-size", "10px");
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("🎼 Distribución de Géneros Musicales por Año");
// Destacar Oceanus Folk
svg.selectAll("rect")
.filter(d => d.genre === "Oceanus Folk")
.attr("stroke", "#e74c3c")
.attr("stroke-width", 2);
return svg.node();
}
Insert cell
// === CELDA 10: Dashboard de Artistas Emergentes ===
viewof emergingArtists = {
// Calcular métricas para identificar estrellas emergentes
const artists = nodes.filter(d => d["Node Type"] === "Person");
const artistMetrics = artists.map(artist => {
const connections = links.filter(l => l.source === artist.id || l.target === artist.id);
const collaborations = connections.filter(l => l["Edge Type"] === "PerformerOf");
const productions = connections.filter(l => l["Edge Type"] === "ProducerOf");
// Buscar sus canciones/álbumes
const works = nodes.filter(w =>
connections.some(l =>
(l.source === artist.id && l.target === w.id) ||
(l.target === artist.id && l.source === w.id)
) && (w["Node Type"] === "Song" || w["Node Type"] === "Album")
);
const notableWorks = works.filter(w => w.notable === true);
const recentWorks = works.filter(w => w.release_date && +w.release_date >= 2035);
return {
id: artist.id,
name: artist.name || artist.stage_name || 'Desconocido',
totalConnections: connections.length,
collaborations: collaborations.length,
productions: productions.length,
totalWorks: works.length,
notableWorks: notableWorks.length,
recentWorks: recentWorks.length,
score: (notableWorks.length * 3) + (recentWorks.length * 2) + (collaborations.length * 1.5)
};
}).filter(a => a.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
return html`
<div style="background: #f8f9fa; padding: 20px; border-radius: 10px; margin: 10px 0;">
<h3 style="margin-top: 0; color: #2c3e50;">⭐ Top Artistas Emergentes (Predicción)</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;">
${artistMetrics.slice(0, 6).map((artist, i) => `
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-left: 4px solid ${i < 3 ? '#e74c3c' : '#3498db'};">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h4 style="margin: 0; color: #2c3e50;">${i + 1}. ${artist.name}</h4>
<span style="background: ${i < 3 ? '#e74c3c' : '#3498db'}; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;">
Score: ${artist.score.toFixed(1)}
</span>
</div>
<div style="margin-top: 10px; font-size: 13px; color: #7f8c8d;">
<div>🎵 Obras totales: <strong>${artist.totalWorks}</strong></div>
<div>⭐ Obras notables: <strong>${artist.notableWorks}</strong></div>
<div>🆕 Obras recientes: <strong>${artist.recentWorks}</strong></div>
<div>🤝 Colaboraciones: <strong>${artist.collaborations}</strong></div>
</div>
</div>
`).join('')}
</div>
<div style="margin-top: 20px; padding: 15px; background: #d5f4e6; border-radius: 8px;">
<h4 style="margin: 0 0 10px 0; color: #27ae60;">🔮 Predicciones para los próximos 5 años:</h4>
<p style="margin: 5px 0;"><strong>1. ${artistMetrics[0]?.name || 'TBD'}</strong> - Mayor potencial por obras notables y colaboraciones activas</p>
<p style="margin: 5px 0;"><strong>2. ${artistMetrics[1]?.name || 'TBD'}</strong> - Crecimiento constante en producción reciente</p>
<p style="margin: 5px 0;"><strong>3. ${artistMetrics[2]?.name || 'TBD'}</strong> - Red sólida de conexiones en la industria</p>
</div>
</div>
`;
}
Insert cell
// === CELDA 13: Radar Chart de Géneros ===
viewof genreRadar = {
const genreStats = d3.rollup(
nodes.filter(d => d.genre && d["Node Type"] === "Song"),
v => ({
total: v.length,
notable: v.filter(d => d.notable).length,
recent: v.filter(d => d.release_date && +d.release_date >= 2035).length,
collaborations: v.filter(d => {
const collabs = links.filter(l =>
(l.source === d.id || l.target === d.id) &&
l["Edge Type"] === "PerformerOf"
);
return collabs.length > 1;
}).length
}),
d => d.genre
);
const topGenres = Array.from(genreStats)
.sort((a, b) => b[1].total - a[1].total)
.slice(0, 6);
const radarData = topGenres.map(([genre, stats]) => ({
genre,
total: Math.min(stats.total / 10, 10), // Normalizar a 0-10
notable: Math.min(stats.notable * 2, 10),
recent: Math.min(stats.recent * 1.5, 10),
collaborations: Math.min(stats.collaborations * 3, 10)
}));
const width = 500;
const height = 500;
const margin = 50;
const radius = Math.min(width, height) / 2 - margin;
const svg = d3.create("svg")
.attr("width", width + 300)
.attr("height", height);
const g = svg.append("g")
.attr("transform", `translate(${width/2}, ${height/2})`);
const axes = ["total", "notable", "recent", "collaborations"];
const angleSlice = Math.PI * 2 / axes.length;
// Crear escalas
const rScale = d3.scaleLinear()
.domain([0, 10])
.range([0, radius]);
const color = d3.scaleOrdinal()
.domain(radarData.map(d => d.genre))
.range(d3.schemeSet2);
// Líneas de la grilla
const levels = 5;
for (let level = 1; level <= levels; level++) {
const levelRadius = radius * (level / levels);
g.append("circle")
.attr("r", levelRadius)
.style("fill", "none")
.style("stroke", "#CDCDCD")
.style("stroke-width", "0.5px");
g.append("text")
.attr("x", 4)
.attr("y", -levelRadius)
.style("font-size", "10px")
.style("fill", "#737373")
.text((10 * level / levels).toFixed(0));
}
// Ejes
axes.forEach((axis, i) => {
const angle = angleSlice * i - Math.PI / 2;
const lineCoords = {
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius
};
g.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", lineCoords.x)
.attr("y2", lineCoords.y)
.style("stroke", "#CDCDCD")
.style("stroke-width", "1px");
g.append("text")
.attr("x", lineCoords.x * 1.15)
.attr("y", lineCoords.y * 1.15)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.style("font-size", "12px")
.style("font-weight", "bold")
.text({
total: "Volumen",
notable: "Notables",
recent: "Recientes",
collaborations: "Colaborativas"
}[axis]);
});
// Crear path para cada género
radarData.forEach((data, index) => {
const radarLine = d3.lineRadial()
.angle((d, i) => angleSlice * i)
.radius(d => rScale(d))
.curve(d3.curveCardinalClosed);
const values = axes.map(axis => data[axis]);
g.append("path")
.datum(values)
.attr("d", radarLine)
.style("fill", color(data.genre))
.style("fill-opacity", 0.2)
.style("stroke", color(data.genre))
.style("stroke-width", 2);
// Puntos
axes.forEach((axis, i) => {
const angle = angleSlice * i - Math.PI / 2;
const x = Math.cos(angle) * rScale(data[axis]);
const y = Math.sin(angle) * rScale(data[axis]);
g.append("circle")
.attr("cx", x)
.attr("cy", y)
.attr("r", 3)
.style("fill", color(data.genre))
.style("stroke", "#fff")
.style("stroke-width", 1);
});
});
// Leyenda
const legend = svg.append("g")
.attr("transform", `translate(${width + 20}, 50)`);
radarData.forEach((data, i) => {
const legendRow = legend.append("g")
.attr("transform", `translate(0, ${i * 25})`);
legendRow.append("rect")
.attr("width", 15)
.attr("height", 15)
.style("fill", color(data.genre));
legendRow.append("text")
.attr("x", 20)
.attr("y", 12)
.style("font-size", "12px")
.text(data.genre);
});
// Título
svg.append("text")
.attr("x", (width + 300) / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("📊 Perfil de Géneros Musicales");
return svg.node();
}
Insert cell
// === CELDA 7: Dashboard Interactivo de Sailor Shift ===
viewof sailorDashboard = {
const container = html`<div id="sailor-dashboard"></div>`;
// Buscar datos de Sailor Shift
const sailorNodes = nodes.filter(n =>
(n.name && n.name.toLowerCase().includes("sailor")) ||
(n.stage_name && n.stage_name.toLowerCase().includes("sailor"))
);
const sailorIds = new Set(sailorNodes.map(n => n.id));
const sailorConnections = links.filter(l =>
sailorIds.has(l.source) || sailorIds.has(l.target)
);
// Análisis de influencias (quién influyó a Sailor)
const influences = sailorConnections
.filter(l => ["InStyleOf", "InterpolatesFrom", "CoverOf"].includes(l["Edge Type"]))
.map(l => {
const influencer = sailorIds.has(l.target) ?
nodes.find(n => n.id === l.source) :
nodes.find(n => n.id === l.target);
return influencer;
})
.filter(Boolean);
// Colaboraciones
const collaborations = sailorConnections
.filter(l => l["Edge Type"] === "PerformerOf")
.map(l => {
const collaborator = sailorIds.has(l.source) ?
nodes.find(n => n.id === l.target) :
nodes.find(n => n.id === l.source);
return collaborator;
})
.filter(Boolean);
const width = 900;
const height = 600;
const svg = d3.select(container)
.append("svg")
.attr("width", width)
.attr("height", height);
// Estado interactivo
let selectedCategory = "influences";
function updateVisualization() {
svg.selectAll("*").remove();
let currentData;
let title;
switch(selectedCategory) {
case "influences":
currentData = influences.slice(0, 10);
title = "🎵 Principales Influencias de Sailor Shift";
break;
case "collaborations":
currentData = collaborations.slice(0, 10);
title = "🤝 Colaboraciones de Sailor Shift";
break;
case "impact":
currentData = nodes.filter(n => n.genre === "Oceanus Folk").slice(0, 10);
title = "🌊 Impacto en la Comunidad Oceanus Folk";
break;
}
// Crear visualización de red centrada
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2));
// Nodo central (Sailor Shift)
const centerNode = {id: "sailor", name: "Sailor Shift", type: "center"};
const networkNodes = [centerNode, ...currentData.map((d, i) => ({
...d,
id: d.id || `node-${i}`,
name: d.name || "Sin nombre"
}))];
const networkLinks = currentData.map((d, i) => ({
source: "sailor",
target: d.id || `node-${i}`
}));
// Enlaces
const link = svg.append("g")
.selectAll("line")
.data(networkLinks)
.enter().append("line")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", 2);
// Nodos
const node = svg.append("g")
.selectAll("circle")
.data(networkNodes)
.enter().append("circle")
.attr("r", d => d.type === "center" ? 20 : 12)
.attr("fill", d => {
if (d.type === "center") return "#e74c3c";
if (d["Node Type"] === "Song") return "#3498db";
if (d["Node Type"] === "Album") return "#2ecc71";
if (d["Node Type"] === "Person") return "#f39c12";
return "#95a5a6";
})
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
d3.select(this).attr("r", d.type === "center" ? 25 : 15);
// Tooltip
const tooltip = svg.append("g").attr("id", "tooltip");
const rect = tooltip.append("rect")
.attr("x", event.layerX + 10)
.attr("y", event.layerY - 10)
.attr("width", 150)
.attr("height", 60)
.attr("fill", "white")
.attr("stroke", "#333")
.attr("rx", 5);
tooltip.append("text")
.attr("x", event.layerX + 15)
.attr("y", event.layerY + 10)
.style("font-size", "12px")
.style("font-weight", "bold")
.text(d.name);
tooltip.append("text")
.attr("x", event.layerX + 15)
.attr("y", event.layerY + 25)
.style("font-size", "10px")
.text(`Tipo: ${d["Node Type"] || d.type}`);
if (d.genre) {
tooltip.append("text")
.attr("x", event.layerX + 15)
.attr("y", event.layerY + 40)
.style("font-size", "10px")
.text(`Género: ${d.genre}`);
}
})
.on("mouseout", function(event, d) {
d3.select(this).attr("r", d.type === "center" ? 20 : 12);
svg.select("#tooltip").remove();
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Etiquetas
const text = svg.append("g")
.selectAll("text")
.data(networkNodes)
.enter().append("text")
.attr("dx", 15)
.attr("dy", "0.35em")
.style("font-size", d => d.type === "center" ? "14px" : "10px")
.style("font-weight", d => d.type === "center" ? "bold" : "normal")
.text(d => d.name.length > 15 ? d.name.substring(0, 15) + "..." : d.name);
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "bold")
.text(title);
simulation.nodes(networkNodes);
simulation.force("link").links(networkLinks);
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
text
.attr("x", d => d.x)
.attr("y", d => d.y);
});
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
// Controles interactivos
const controls = d3.select(container)
.insert("div", "svg")
.style("margin-bottom", "20px")
.style("text-align", "center");
controls.append("button")
.text("🎵 Influencias")
.style("margin", "5px")
.style("padding", "10px 15px")
.style("border", "none")
.style("border-radius", "5px")
.style("background", "#3498db")
.style("color", "white")
.style("cursor", "pointer")
.on("click", () => {
selectedCategory = "influences";
updateVisualization();
});
controls.append("button")
.text("🤝 Colaboraciones")
.style("margin", "5px")
.style("padding", "10px 15px")
.style("border", "none")
.style("border-radius", "5px")
.style("background", "#2ecc71")
.style("color", "white")
.style("cursor", "pointer")
.on("click", () => {
selectedCategory = "collaborations";
updateVisualization();
});
controls.append("button")
.text("🌊 Impacto")
.style("margin", "5px")
.style("padding", "10px 15px")
.style("border", "none")
.style("border-radius", "5px")
.style("background", "#e74c3c")
.style("color", "white")
.style("cursor", "pointer")
.on("click", () => {
selectedCategory = "impact";
updateVisualization();
});
updateVisualization();
return container;
}
Insert cell
// === CELDA 8: Timeline Interactivo Oceanus Folk ===
viewof oceanusTimeline = {
const container = html`<div id="oceanus-timeline"></div>`;
const oceanusFolk = nodes.filter(n =>
n.genre && n.genre.toLowerCase().includes("oceanus folk")
);
const yearData = d3.group(oceanusFolk, d => d.release_date);
const timelineData = Array.from(yearData, ([year, works]) => ({
year: +year,
total: works.length,
notable: works.filter(w => w.notable).length,
songs: works.filter(w => w["Node Type"] === "Song").length,
albums: works.filter(w => w["Node Type"] === "Album").length,
works: works
})).filter(d => d.year).sort((a, b) => a.year - b.year);
const width = 1000;
const height = 400;
const margin = {top: 50, right: 150, bottom: 50, left: 50};
const svg = d3.select(container)
.append("svg")
.attr("width", width)
.attr("height", height);
const x = d3.scaleLinear()
.domain(d3.extent(timelineData, d => d.year))
.range([margin.left, width - margin.right]);
const y = d3.scaleLinear()
.domain([0, d3.max(timelineData, d => d.total)])
.range([height - margin.bottom, margin.top]);
// Área de fondo
const area = d3.area()
.x(d => x(d.year))
.y0(height - margin.bottom)
.y1(d => y(d.total))
.curve(d3.curveMonotoneX);
svg.append("path")
.datum(timelineData)
.attr("fill", "rgba(52, 152, 219, 0.3)")
.attr("d", area);
// Línea principal
const line = d3.line()
.x(d => x(d.year))
.y(d => y(d.total))
.curve(d3.curveMonotoneX);
svg.append("path")
.datum(timelineData)
.attr("fill", "none")
.attr("stroke", "#3498db")
.attr("stroke-width", 3)
.attr("d", line);
// Puntos interactivos
const circles = svg.selectAll(".timeline-point")
.data(timelineData)
.enter().append("circle")
.attr("class", "timeline-point")
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.total))
.attr("r", 6)
.attr("fill", "#e74c3c")
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.on("mouseover", function(event, d) {
d3.select(this).attr("r", 10);
showTooltip(event, d);
})
.on("mouseout", function(event, d) {
d3.select(this).attr("r", 6);
hideTooltip();
})
.on("click", function(event, d) {
showDetails(d);
});
// Ejes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")));
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));
// Título
svg.append("text")
.attr("x", width / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "bold")
.text("🌊 Expansión del Oceanus Folk a través del Tiempo");
// Panel de detalles
const detailsPanel = d3.select(container)
.append("div")
.attr("id", "details-panel")
.style("margin-top", "20px")
.style("padding", "15px")
.style("background", "#f8f9fa")
.style("border-radius", "10px")
.style("display", "none");
function showTooltip(event, d) {
const tooltip = svg.append("g").attr("id", "timeline-tooltip");
const rect = tooltip.append("rect")
.attr("x", x(d.year) + 10)
.attr("y", y(d.total) - 50)
.attr("width", 120)
.attr("height", 80)
.attr("fill", "white")
.attr("stroke", "#333")
.attr("rx", 5);
tooltip.append("text")
.attr("x", x(d.year) + 15)
.attr("y", y(d.total) - 35)
.style("font-size", "12px")
.style("font-weight", "bold")
.text(`Año: ${d.year}`);
tooltip.append("text")
.attr("x", x(d.year) + 15)
.attr("y", y(d.total) - 20)
.style("font-size", "10px")
.text(`Total: ${d.total}`);
tooltip.append("text")
.attr("x", x(d.year) + 15)
.attr("y", y(d.total) - 5)
.style("font-size", "10px")
.text(`Notables: ${d.notable}`);
tooltip.append("text")
.attr("x", x(d.year) + 15)
.attr("y", y(d.total) + 10)
.style("font-size", "8px")
.style("fill", "#666")
.text("Click para detalles");
}
function hideTooltip() {
svg.select("#timeline-tooltip").remove();
}
function showDetails(d) {
detailsPanel.style("display", "block");
detailsPanel.html(`
<h3>📅 Año ${d.year} - Detalles del Oceanus Folk</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<div>
<h4>📊 Estadísticas</h4>
<p>Total de obras: <strong>${d.total}</strong></p>
<p>Obras notables: <strong>${d.notable}</strong></p>
<p>Canciones: <strong>${d.songs}</strong></p>
<p>Álbumes: <strong>${d.albums}</strong></p>
</div>
<div>
<h4>🎵 Obras Destacadas</h4>
${d.works.filter(w => w.notable).slice(0, 5).map(work =>
`<p style="margin: 5px 0;"><strong>${work.name || 'Sin nombre'}</strong> (${work["Node Type"]})</p>`
).join('')}
</div>
</div>
`);
}
// Análisis de tendencias
const trendAnalysis = d3.select(container)
.append("div")
.style("margin-top", "20px")
.style("padding", "15px")
.style("background", "#e8f5e8")
.style("border-radius", "10px");
const avgGrowth = d3.mean(timelineData.slice(1).map((d, i) =>
d.total - timelineData[i].total
));
trendAnalysis.html(`
<h4>📈 Análisis de Tendencias</h4>
<p><strong>Crecimiento promedio anual:</strong> ${avgGrowth.toFixed(1)} obras por año</p>
<p><strong>Año pico:</strong> ${timelineData.find(d => d.total === d3.max(timelineData, d => d.total)).year}</p>
<p><strong>Patrón:</strong> ${avgGrowth > 0 ? 'Crecimiento gradual sostenido' : 'Crecimiento intermitente'}</p>
`);
return container;
}
Insert cell
// === CELDA 11: Análisis de Carreras con Selector Interactivo ===
viewof starAnalysisInteractive = {
const container = html`<div id="star-analysis-interactive"></div>`;
// Identificar artistas con métricas de estrella (mismo cálculo anterior)
const artists = nodes.filter(d => d["Node Type"] === "Person");
const artistMetrics = artists.map(artist => {
const connections = links.filter(l => l.source === artist.id || l.target === artist.id);
const works = nodes.filter(work =>
connections.some(conn =>
(conn.source === artist.id && conn.target === work.id &&
["PerformerOf", "ComposerOf"].includes(conn["Edge Type"])) ||
(conn.target === artist.id && conn.source === work.id &&
["PerformerOf", "ComposerOf"].includes(conn["Edge Type"]))
) && (work["Node Type"] === "Song" || work["Node Type"] === "Album")
);
const worksWithDates = works.filter(w => w.release_date);
const notableWorks = works.filter(w => w.notable);
const recentWorks = works.filter(w => w.release_date && +w.release_date >= 2035);
const collaborations = connections.filter(c => c["Edge Type"] === "PerformerOf").length;
const influencesReceived = connections.filter(c =>
["InStyleOf", "InterpolatesFrom"].includes(c["Edge Type"]) && c.target === artist.id
).length;
const influencesGiven = connections.filter(c =>
["InStyleOf", "InterpolatesFrom"].includes(c["Edge Type"]) && c.source === artist.id
).length;
const yearGroups = d3.group(worksWithDates, w => +w.release_date);
const yearlyOutput = Array.from(yearGroups, ([year, works]) => ({
year,
count: works.length,
notable: works.filter(w => w.notable).length
})).sort((a, b) => a.year - b.year);
const recentYears = yearlyOutput.slice(-5);
const earlyYears = yearlyOutput.slice(0, 5);
const recentAvg = d3.mean(recentYears, d => d.count) || 0;
const earlyAvg = d3.mean(earlyYears, d => d.count) || 0;
const growthTrend = recentAvg - earlyAvg;
const starScore = (
(notableWorks.length * 4) +
(recentWorks.length * 3) +
(collaborations * 1.5) +
(influencesGiven * 2) +
(growthTrend * 2) +
(works.length * 0.5)
);
return {
id: artist.id,
name: artist.name || artist.stage_name || 'Desconocido',
totalWorks: works.length,
notableWorks: notableWorks.length,
recentWorks: recentWorks.length,
collaborations,
influencesReceived,
influencesGiven,
growthTrend,
starScore,
yearlyOutput,
works,
notableRatio: works.length > 0 ? notableWorks.length / works.length : 0,
activeYears: worksWithDates.length > 0 ?
d3.max(worksWithDates, w => +w.release_date) - d3.min(worksWithDates, w => +w.release_date) + 1 : 0
};
}).filter(a => a.starScore > 5).sort((a, b) => b.starScore - a.starScore);
// Estado de la aplicación
let selectedArtists = artistMetrics.slice(0, 3);
// Crear el selector de artistas
const selectorContainer = d3.select(container)
.append("div")
.style("background", "#f8f9fa")
.style("padding", "20px")
.style("border-radius", "10px")
.style("margin-bottom", "20px");
selectorContainer.append("h3")
.style("margin", "0 0 15px 0")
.style("color", "#2c3e50")
.text("🎯 Selector de Artistas para Comparar");
const instructionText = selectorContainer.append("p")
.style("margin", "0 0 15px 0")
.style("color", "#7f8c8d")
.style("font-size", "14px")
.text("Selecciona hasta 3 artistas para comparar sus carreras. Click en un artista seleccionado para removerlo.");
// Grid de artistas disponibles
const artistGrid = selectorContainer.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fill, minmax(250px, 1fr))")
.style("gap", "10px")
.style("max-height", "300px")
.style("overflow-y", "auto")
.style("border", "1px solid #ddd")
.style("border-radius", "5px")
.style("padding", "10px")
.style("background", "white");
function updateArtistGrid() {
const artistCards = artistGrid.selectAll(".artist-card")
.data(artistMetrics.slice(0, 20), d => d.id);
const cardEnter = artistCards.enter()
.append("div")
.attr("class", "artist-card")
.style("padding", "10px")
.style("border", "1px solid #ddd")
.style("border-radius", "5px")
.style("cursor", "pointer")
.style("transition", "all 0.3s ease")
.on("mouseover", function() {
d3.select(this).style("box-shadow", "0 2px 8px rgba(0,0,0,0.15)");
})
.on("mouseout", function() {
d3.select(this).style("box-shadow", "none");
})
.on("click", function(event, d) {
toggleArtistSelection(d);
});
const cardUpdate = cardEnter.merge(artistCards);
cardUpdate
.style("background", d => selectedArtists.find(a => a.id === d.id) ? "#e8f5e8" : "white")
.style("border-color", d => selectedArtists.find(a => a.id === d.id) ? "#27ae60" : "#ddd")
.html(d => {
const isSelected = selectedArtists.find(a => a.id === d.id);
return `
<div style="display: flex; justify-content: between; align-items: center;">
<div style="flex: 1;">
<div style="font-weight: bold; color: #2c3e50;">${isSelected ? '✓ ' : ''}${d.name}</div>
<div style="font-size: 12px; color: #7f8c8d; margin-top: 5px;">
Score: ${d.starScore.toFixed(1)} | Obras: ${d.totalWorks} | Notables: ${d.notableWorks}
</div>
</div>
${isSelected ? '<div style="color: #27ae60; font-size: 18px;">✓</div>' : ''}
</div>
`;
});
artistCards.exit().remove();
}
function toggleArtistSelection(artist) {
const index = selectedArtists.findIndex(a => a.id === artist.id);
if (index >= 0) {
// Remover artista
selectedArtists.splice(index, 1);
} else if (selectedArtists.length < 3) {
// Agregar artista
selectedArtists.push(artist);
} else {
// Reemplazar el último si ya hay 3
selectedArtists[2] = artist;
}
updateArtistGrid();
updateVisualizations();
}
// Contenedor para visualizaciones
const vizContainer = d3.select(container)
.append("div")
.attr("id", "visualizations-container");
function updateVisualizations() {
// Limpiar visualizaciones anteriores
vizContainer.selectAll("*").remove();
if (selectedArtists.length === 0) {
vizContainer.append("div")
.style("text-align", "center")
.style("padding", "50px")
.style("color", "#7f8c8d")
.text("Selecciona al menos un artista para ver las visualizaciones");
return;
}
// Dashboard principal
const dashboard = vizContainer.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "20px");
// Panel izquierdo: Radar Chart
const leftPanel = dashboard.append("div");
const svg1 = leftPanel.append("svg")
.attr("width", 600)
.attr("height", 400);
// Datos para radar
const radarData = selectedArtists.map(artist => ({
name: artist.name,
metrics: [
{axis: "Obras Totales", value: Math.min(artist.totalWorks / 10, 10)},
{axis: "Obras Notables", value: Math.min(artist.notableWorks * 2, 10)},
{axis: "Colaboraciones", value: Math.min(artist.collaborations / 5, 10)},
{axis: "Influencia Dada", value: Math.min(artist.influencesGiven * 3, 10)},
{axis: "Crecimiento", value: Math.min((artist.growthTrend + 5) * 2, 10)},
{axis: "Ratio Éxito", value: artist.notableRatio * 10}
]
}));
// Crear radar chart
const radarMargin = 50;
const radarRadius = 150;
const radarCenter = {x: 300, y: 200};
const angleSlice = Math.PI * 2 / 6;
const radarScale = d3.scaleLinear().domain([0, 10]).range([0, radarRadius]);
// Líneas de grilla
for (let level = 1; level <= 5; level++) {
svg1.append("circle")
.attr("cx", radarCenter.x)
.attr("cy", radarCenter.y)
.attr("r", radarRadius * level / 5)
.style("fill", "none")
.style("stroke", "#CDCDCD")
.style("stroke-width", "0.5px");
}
// Ejes del radar
radarData[0].metrics.forEach((metric, i) => {
const angle = angleSlice * i - Math.PI / 2;
const lineCoords = {
x: radarCenter.x + Math.cos(angle) * radarRadius,
y: radarCenter.y + Math.sin(angle) * radarRadius
};
svg1.append("line")
.attr("x1", radarCenter.x)
.attr("y1", radarCenter.y)
.attr("x2", lineCoords.x)
.attr("y2", lineCoords.y)
.style("stroke", "#CDCDCD")
.style("stroke-width", "1px");
svg1.append("text")
.attr("x", lineCoords.x + Math.cos(angle) * 20)
.attr("y", lineCoords.y + Math.sin(angle) * 20)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.text(metric.axis);
});
// Colores dinámicos según número de artistas seleccionados
const colors = ["#e74c3c", "#3498db", "#2ecc71"];
// Crear líneas para cada artista
radarData.forEach((artist, artistIndex) => {
const radarLine = d3.lineRadial()
.angle((d, i) => angleSlice * i)
.radius(d => radarScale(d.value))
.curve(d3.curveCardinalClosed);
const values = artist.metrics.map(m => m.value);
svg1.append("path")
.datum(values)
.attr("d", radarLine)
.attr("transform", `translate(${radarCenter.x}, ${radarCenter.y})`)
.style("fill", colors[artistIndex])
.style("fill-opacity", 0.2)
.style("stroke", colors[artistIndex])
.style("stroke-width", 2);
// Puntos
artist.metrics.forEach((metric, i) => {
const angle = angleSlice * i - Math.PI / 2;
const x = radarCenter.x + Math.cos(angle) * radarScale(metric.value);
const y = radarCenter.y + Math.sin(angle) * radarScale(metric.value);
svg1.append("circle")
.attr("cx", x)
.attr("cy", y)
.attr("r", 3)
.style("fill", colors[artistIndex])
.style("stroke", "#fff")
.style("stroke-width", 1);
});
});
// Título del radar
svg1.append("text")
.attr("x", 300)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("🎯 Perfil Comparativo de Estrellas");
// Panel derecho: Timeline
const rightPanel = dashboard.append("div");
const svg2 = rightPanel.append("svg")
.attr("width", 600)
.attr("height", 400);
// Timeline de evolución
const timelineMargin = {top: 30, right: 50, bottom: 50, left: 50};
const timelineWidth = 600 - timelineMargin.left - timelineMargin.right;
const timelineHeight = 400 - timelineMargin.top - timelineMargin.bottom;
const allYears = selectedArtists.flatMap(a => a.yearlyOutput.map(y => y.year));
const yearExtent = d3.extent(allYears);
if (yearExtent[0]) {
const xScale = d3.scaleLinear()
.domain(yearExtent)
.range([timelineMargin.left, timelineWidth + timelineMargin.left]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(selectedArtists.flatMap(a => a.yearlyOutput), d => d.count)])
.range([timelineHeight + timelineMargin.top, timelineMargin.top]);
// Líneas de timeline para cada artista
selectedArtists.forEach((artist, index) => {
const line = d3.line()
.x(d => xScale(d.year))
.y(d => yScale(d.count))
.curve(d3.curveMonotoneX);
svg2.append("path")
.datum(artist.yearlyOutput)
.attr("fill", "none")
.attr("stroke", colors[index])
.attr("stroke-width", 3)
.attr("d", line);
// Puntos notables
svg2.selectAll(`.notable-${index}`)
.data(artist.yearlyOutput.filter(d => d.notable > 0))
.enter().append("circle")
.attr("class", `notable-${index}`)
.attr("cx", d => xScale(d.year))
.attr("cy", d => yScale(d.count))
.attr("r", d => 3 + d.notable)
.attr("fill", colors[index])
.attr("stroke", "#fff")
.attr("stroke-width", 2);
});
// Ejes del timeline
svg2.append("g")
.attr("transform", `translate(0,${timelineHeight + timelineMargin.top})`)
.call(d3.axisBottom(xScale).tickFormat(d3.format("d")));
svg2.append("g")
.attr("transform", `translate(${timelineMargin.left},0)`)
.call(d3.axisLeft(yScale));
}
// Título del timeline
svg2.append("text")
.attr("x", 300)
.attr("y", 20)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("📈 Evolución Temporal de Carreras");
// Leyenda
const legend = vizContainer.append("div")
.style("display", "flex")
.style("justify-content", "center")
.style("margin", "20px 0")
.style("gap", "30px");
selectedArtists.forEach((artist, index) => {
const legendItem = legend.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px");
legendItem.append("div")
.style("width", "20px")
.style("height", "20px")
.style("background", colors[index])
.style("border-radius", "3px");
legendItem.append("span")
.style("font-weight", "bold")
.text(`${artist.name} (Score: ${artist.starScore.toFixed(1)})`);
});
// Cards de análisis detallado
const detailsSection = vizContainer.append("div")
.style("margin-top", "30px");
detailsSection.append("h3")
.style("text-align", "center")
.style("color", "#2c3e50")
.text("🔍 Análisis Detallado de Carreras Seleccionadas");
const artistCards = detailsSection.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(350px, 1fr))")
.style("gap", "20px");
selectedArtists.forEach((artist, index) => {
const card = artistCards.append("div")
.style("background", "#f8f9fa")
.style("padding", "20px")
.style("border-radius", "10px")
.style("border-left", `5px solid ${colors[index]}`);
card.append("h4")
.style("margin", "0 0 15px 0")
.style("color", "#2c3e50")
.text(`${index + 1}. ${artist.name}`);
const metrics = card.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "10px")
.style("margin-bottom", "15px");
metrics.append("div").html(`<strong>📊 Score Total:</strong> ${artist.starScore.toFixed(1)}`);
metrics.append("div").html(`<strong>🎵 Obras:</strong> ${artist.totalWorks}`);
metrics.append("div").html(`<strong>⭐ Notables:</strong> ${artist.notableWorks}`);
metrics.append("div").html(`<strong>🤝 Colaboraciones:</strong> ${artist.collaborations}`);
metrics.append("div").html(`<strong>📈 Tendencia:</strong> ${artist.growthTrend > 0 ? '↗️' : '↘️'} ${artist.growthTrend.toFixed(1)}`);
metrics.append("div").html(`<strong>🎯 Ratio Éxito:</strong> ${(artist.notableRatio * 100).toFixed(1)}%`);
// Características de estrella
const characteristics = [];
if (artist.notableRatio > 0.3) characteristics.push("🌟 Alta calidad");
if (artist.growthTrend > 1) characteristics.push("📈 Crecimiento sostenido");
if (artist.collaborations > 10) characteristics.push("🤝 Muy colaborativo");
if (artist.influencesGiven > 3) characteristics.push("🎭 Influyente");
if (artist.recentWorks > 5) characteristics.push("🔥 Muy activo");
if (characteristics.length > 0) {
card.append("div")
.style("margin-top", "10px")
.style("padding", "10px")
.style("background", "white")
.style("border-radius", "5px")
.html(`<strong>Características de Estrella:</strong><br>${characteristics.join(" • ")}`);
}
});
}
// Inicializar
updateArtistGrid();
updateVisualizations();
return container;
}
Insert cell
// === CELDA 17: Dashboard de Predicciones Dinámico ===
viewof dynamicPredictionDashboard = {
const container = html`<div id="dynamic-prediction-dashboard"></div>`;
// Calcular métricas de predicción mejoradas
const oceanusArtists = [];
nodes.filter(n => n.genre === "Oceanus Folk").forEach(work => {
const workConnections = links.filter(l =>
(l.source === work.id || l.target === work.id) &&
["PerformerOf", "ComposerOf", "LyricistOf"].includes(l["Edge Type"])
);
workConnections.forEach(conn => {
const artistId = conn.source === work.id ? conn.target : conn.source;
const artist = nodes.find(n => n.id === artistId && n["Node Type"] === "Person");
if (artist && !oceanusArtists.find(a => a.id === artist.id)) {
oceanusArtists.push(artist);
}
});
});
const predictions = oceanusArtists.map(artist => {
const connections = links.filter(l => l.source === artist.id || l.target === artist.id);
const works = nodes.filter(work =>
connections.some(conn =>
(conn.source === artist.id && conn.target === work.id) ||
(conn.target === artist.id && conn.source === work.id)
) && (work["Node Type"] === "Song" || work["Node Type"] === "Album")
);
const oceanusWorks = works.filter(w => w.genre === "Oceanus Folk");
const recentWorks = works.filter(w => w.release_date && +w.release_date >= 2037);
const notableWorks = works.filter(w => w.notable);
// Calcular momentum
const yearGroups = d3.group(works.filter(w => w.release_date), w => +w.release_date);
const recentYears = Array.from(yearGroups.keys()).filter(year => year >= 2035).sort();
const momentum = recentYears.length > 1 ?
(yearGroups.get(recentYears[recentYears.length - 1])?.length || 0) -
(yearGroups.get(recentYears[0])?.length || 0) : 0;
// Colaboraciones con artistas establecidos
const establishedCollabs = connections.filter(conn => {
const otherId = conn.source === artist.id ? conn.target : conn.source;
const other = nodes.find(n => n.id === otherId);
return other && other["Node Type"] === "Person" &&
links.filter(l => l.source === otherId || l.target === otherId).length > 20;
}).length;
const genres = new Set(works.map(w => w.genre).filter(Boolean));
const modernInfluences = links.filter(l =>
l.source === artist.id &&
["InStyleOf", "InterpolatesFrom"].includes(l["Edge Type"]) &&
nodes.find(n => n.id === l.target && n.release_date && +n.release_date >= 2035)
).length;
const oceanusRatio = works.length > 0 ? oceanusWorks.length / works.length : 0;
const notableRatio = works.length > 0 ? notableWorks.length / works.length : 0;
// Score mejorado con factores temporales
const currentYear = 2040;
const careerLength = works.length > 0 ?
Math.max(...works.filter(w => w.release_date).map(w => +w.release_date)) -
Math.min(...works.filter(w => w.release_date).map(w => +w.release_date)) + 1 : 1;
const experienceFactor = Math.min(careerLength / 10, 1); // Factor de experiencia
const freshnessFactor = recentWorks.length > 0 ? 1.2 : 0.8; // Factor de frescura
const futureStarScore = (
(oceanusRatio * 10) +
(notableRatio * 8) +
(momentum * 6 * freshnessFactor) +
(recentWorks.length * 3) +
(establishedCollabs * 4) +
(genres.size * 2) +
(modernInfluences * 7) +
(works.length * 0.3 * experienceFactor)
);
// Calcular confianza basada en consistencia
const consistency = recentYears.length > 2 ?
1 - (d3.deviation(recentYears.map(year => yearGroups.get(year)?.length || 0)) || 0) / 5 : 0.5;
const confidence = Math.min(95, 30 + (futureStarScore * 2) + (consistency * 20));
return {
id: artist.id,
name: artist.name || artist.stage_name || 'Desconocido',
futureStarScore,
confidence,
oceanusRatio,
notableRatio,
momentum,
recentWorks: recentWorks.length,
establishedCollabs,
modernInfluences,
careerLength,
consistency,
totalWorks: works.length,
prediction: futureStarScore > 15 ? "Muy Probable" :
futureStarScore > 10 ? "Probable" :
futureStarScore > 5 ? "Posible" : "Improbable"
};
}).filter(p => p.futureStarScore > 3)
.sort((a, b) => b.futureStarScore - a.futureStarScore);
const top3Predictions = predictions.slice(0, 3);
// Crear dashboard
const dashboard = d3.select(container)
.style("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)")
.style("padding", "20px")
.style("border-radius", "15px")
.style("color", "white");
// Título principal con animación
const title = dashboard.append("h2")
.style("text-align", "center")
.style("margin", "0 0 30px 0")
.style("font-size", "28px")
.style("text-shadow", "2px 2px 4px rgba(0,0,0,0.5)")
.text("🔮 Predictor Dinámico: Futuras Estrellas de Oceanus Folk");
// Métricas generales
const statsContainer = dashboard.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "15px")
.style("margin-bottom", "30px");
const totalCandidates = predictions.length;
const highConfidence = predictions.filter(p => p.confidence > 70).length;
const avgScore = d3.mean(predictions, p => p.futureStarScore);
const stats = [
{label: "Candidatos Totales", value: totalCandidates, icon: "🎤"},
{label: "Alta Confianza", value: highConfidence, icon: "⭐"},
{label: "Score Promedio", value: avgScore.toFixed(1), icon: "📊"},
{label: "Predicciones para", value: "2025-2030", icon: "🗓️"}
];
stats.forEach(stat => {
const statCard = statsContainer.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("padding", "15px")
.style("border-radius", "10px")
.style("text-align", "center")
.style("backdrop-filter", "blur(10px)")
.style("border", "1px solid rgba(255,255,255,0.2)");
statCard.append("div")
.style("font-size", "24px")
.style("margin-bottom", "5px")
.text(stat.icon);
statCard.append("div")
.style("font-size", "20px")
.style("font-weight", "bold")
.style("margin-bottom", "5px")
.text(stat.value);
statCard.append("div")
.style("font-size", "12px")
.style("opacity", "0.8")
.text(stat.label);
});
// Panel principal con Top 3
const mainPanel = dashboard.append("div")
.style("display", "grid")
.style("grid-template-columns", "2fr 1fr")
.style("gap", "30px")
.style("margin-bottom", "30px");
// Lado izquierdo: Visualización radar animada
const leftPanel = mainPanel.append("div");
const radarContainer = leftPanel.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("border-radius", "15px")
.style("padding", "20px")
.style("backdrop-filter", "blur(10px)");
radarContainer.append("h3")
.style("text-align", "center")
.style("margin", "0 0 20px 0")
.text("📊 Perfil Predictivo - Top 3");
const radarSvg = radarContainer.append("svg")
.attr("width", 500)
.attr("height", 400);
// Crear radar animado
const radarMetrics = [
"Especialización OF",
"Calidad",
"Momentum",
"Actividad",
"Red Establecida",
"Influencia Moderna"
];
const radarData = top3Predictions.map(artist => ({
name: artist.name,
values: [
artist.oceanusRatio * 10,
artist.notableRatio * 10,
Math.min((artist.momentum + 3) * 2, 10),
Math.min(artist.recentWorks * 1.5, 10),
Math.min(artist.establishedCollabs * 2.5, 10),
Math.min(artist.modernInfluences * 3, 10)
],
score: artist.futureStarScore,
confidence: artist.confidence
}));
function createAnimatedRadar() {
const radarCenter = {x: 250, y: 200};
const radarRadius = 150;
const angleSlice = Math.PI * 2 / radarMetrics.length;
const radarScale = d3.scaleLinear().domain([0, 10]).range([0, radarRadius]);
// Limpiar contenido anterior
radarSvg.selectAll("*").remove();
// Grilla animada
for (let level = 1; level <= 5; level++) {
radarSvg.append("circle")
.attr("cx", radarCenter.x)
.attr("cy", radarCenter.y)
.attr("r", 0)
.style("fill", "none")
.style("stroke", "rgba(255,255,255,0.3)")
.style("stroke-width", "1px")
.transition()
.delay(level * 100)
.duration(500)
.attr("r", radarRadius * level / 5);
}
// Ejes animados
radarMetrics.forEach((metric, i) => {
const angle = angleSlice * i - Math.PI / 2;
const lineCoords = {
x: radarCenter.x + Math.cos(angle) * radarRadius,
y: radarCenter.y + Math.sin(angle) * radarRadius
};
const line = radarSvg.append("line")
.attr("x1", radarCenter.x)
.attr("y1", radarCenter.y)
.attr("x2", radarCenter.x)
.attr("y2", radarCenter.y)
.style("stroke", "rgba(255,255,255,0.5)")
.style("stroke-width", "1px");
line.transition()
.delay(200 + i * 50)
.duration(300)
.attr("x2", lineCoords.x)
.attr("y2", lineCoords.y);
radarSvg.append("text")
.attr("x", lineCoords.x + Math.cos(angle) * 25)
.attr("y", lineCoords.y + Math.sin(angle) * 25)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("fill", "white")
.style("font-weight", "bold")
.style("opacity", 0)
.text(metric)
.transition()
.delay(500 + i * 50)
.duration(300)
.style("opacity", 1);
});
// Líneas de predicción animadas
const colors = ["#ff6b6b", "#4ecdc4", "#45b7d1"];
radarData.forEach((artist, index) => {
const radarLine = d3.lineRadial()
.angle((d, i) => angleSlice * i)
.radius(d => radarScale(d))
.curve(d3.curveCardinalClosed);
const path = radarSvg.append("path")
.datum(artist.values)
.attr("transform", `translate(${radarCenter.x}, ${radarCenter.y})`)
.style("fill", colors[index])
.style("fill-opacity", 0)
.style("stroke", colors[index])
.style("stroke-width", 3)
.style("stroke-opacity", 0)
.attr("d", radarLine);
// Animar aparición
path.transition()
.delay(800 + index * 200)
.duration(500)
.style("fill-opacity", 0.2)
.style("stroke-opacity", 1);
// Puntos animados
artist.values.forEach((value, i) => {
const angle = angleSlice * i - Math.PI / 2;
const x = radarCenter.x + Math.cos(angle) * radarScale(value);
const y = radarCenter.y + Math.sin(angle) * radarScale(value);
radarSvg.append("circle")
.attr("cx", x)
.attr("cy", y)
.attr("r", 0)
.style("fill", colors[index])
.style("stroke", "#fff")
.style("stroke-width", 2)
.transition()
.delay(1000 + index * 200 + i * 50)
.duration(300)
.attr("r", 4);
});
});
}
// Lado derecho: Rankings con confianza
const rightPanel = mainPanel.append("div");
const rankingContainer = rightPanel.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("border-radius", "15px")
.style("padding", "20px")
.style("backdrop-filter", "blur(10px)");
rankingContainer.append("h3")
.style("margin", "0 0 20px 0")
.text("🏆 Top 3 Predicciones");
top3Predictions.forEach((prediction, index) => {
const predictionCard = rankingContainer.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("margin-bottom", "15px")
.style("padding", "15px")
.style("border-radius", "10px")
.style("border-left", `4px solid ${["#ff6b6b", "#4ecdc4", "#45b7d1"][index]}`)
.style("transform", "translateX(-20px)")
.style("opacity", "0");
// Animación de entrada
predictionCard.transition()
.delay(1500 + index * 200)
.duration(500)
.style("transform", "translateX(0px)")
.style("opacity", "1");
const header = predictionCard.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center")
.style("margin-bottom", "10px");
header.append("h4")
.style("margin", "0")
.style("font-size", "16px")
.text(`${index + 1}. ${prediction.name}`);
header.append("div")
.style("background", ["#ff6b6b", "#4ecdc4", "#45b7d1"][index])
.style("color", "white")
.style("padding", "4px 8px")
.style("border-radius", "12px")
.style("font-size", "11px")
.style("font-weight", "bold")
.text(`${prediction.confidence.toFixed(0)}% confianza`);
const metrics = predictionCard.append("div")
.style("font-size", "12px")
.style("line-height", "1.4");
metrics.append("div").text(`🎯 Score: ${prediction.futureStarScore.toFixed(1)}`);
metrics.append("div").text(`📈 Momentum: ${prediction.momentum > 0 ? '↗️' : '↘️'} ${prediction.momentum.toFixed(1)}`);
metrics.append("div").text(`🎵 Obras recientes: ${prediction.recentWorks}`);
metrics.append("div").text(`🤝 Colaboraciones: ${prediction.establishedCollabs}`);
const predictionBadge = predictionCard.append("div")
.style("margin-top", "10px")
.style("text-align", "center")
.style("font-weight", "bold")
.style("color", prediction.prediction === "Muy Probable" ? "#2ecc71" :
prediction.prediction === "Probable" ? "#f39c12" : "#e74c3c")
.text(`Predicción: ${prediction.prediction}`);
});
// Panel de análisis detallado
const analysisPanel = dashboard.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("border-radius", "15px")
.style("padding", "20px")
.style("backdrop-filter", "blur(10px)")
.style("margin-top", "20px");
analysisPanel.append("h3")
.style("margin", "0 0 20px 0")
.text("🧠 Análisis Predictivo Avanzado");
const analysisGrid = analysisPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(300px, 1fr))")
.style("gap", "20px");
// Factores clave
const factorsCard = analysisGrid.append("div");
factorsCard.append("h4").style("margin", "0 0 10px 0").text("🔑 Factores Clave de Éxito");
const factors = [
{factor: "Especialización en Oceanus Folk", weight: "25%", desc: "Dedicación al género"},
{factor: "Momentum Reciente", weight: "20%", desc: "Tendencia creciente"},
{factor: "Calidad Demostrada", weight: "18%", desc: "Ratio de obras notables"},
{factor: "Red de Colaboraciones", weight: "15%", desc: "Conexiones establecidas"},
{factor: "Influencia Moderna", weight: "12%", desc: "Impacto en nuevos artistas"},
{factor: "Versatilidad", weight: "10%", desc: "Diversidad de géneros"}
];
factors.forEach(f => {
factorsCard.append("div")
.style("margin", "8px 0")
.style("padding", "8px")
.style("background", "rgba(255,255,255,0.1)")
.style("border-radius", "5px")
.style("font-size", "12px")
.html(`<strong>${f.factor}</strong> (${f.weight})<br><em>${f.desc}</em>`);
});
// Metodología
const methodCard = analysisGrid.append("div");
methodCard.append("h4").style("margin", "0 0 10px 0").text("⚙️ Metodología");
methodCard.append("p")
.style("font-size", "13px")
.style("line-height", "1.4")
.style("margin", "0")
.html(`
El modelo predictivo combina <strong>análisis temporal</strong>,
<strong>métricas de red social</strong> y <strong>patrones de calidad</strong>
para identificar artistas con potencial de estrellato.
Se consideran factores como consistencia, colaboraciones estratégicas
y adaptabilidad a tendencias emergentes.
`);
// Iniciar animaciones
createAnimatedRadar();
// Botón de refresh
const refreshButton = dashboard.append("div")
.style("text-align", "center")
.style("margin-top", "20px")
.append("button")
.style("padding", "12px 24px")
.style("border", "none")
.style("border-radius", "25px")
.style("background", "rgba(255,255,255,0.2)")
.style("color", "white")
.style("cursor", "pointer")
.style("font-size", "14px")
.style("backdrop-filter", "blur(10px)")
.text("🔄 Actualizar Predicciones")
.on("click", createAnimatedRadar);
return container;
}
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