viewof dynamicPredictionDashboard = {
const container = html`<div id="dynamic-prediction-dashboard"></div>`;
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);
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;
}