letxinet / modules /graph_module.py
C2MV's picture
Initial upload for Build Small Hackathon
68fb5e2 verified
Raw
History Blame Contribute Delete
11.3 kB
import pandas as pd
import json
def generate_interactive_graph(docs_df):
if docs_df is None or docs_df.empty:
return '''<div style="text-align:center; padding:40px; color:#6b7280; background:var(--glass, rgba(17,24,39,0.4)); border-radius:12px; border:1px solid rgba(255,255,255,0.05);">
<div style="font-size:36px; margin-bottom:12px;">🌐</div>
<div style="font-weight:600;">Sin datos para el grafo</div>
<div style="font-size:12px; opacity:0.7;">Ejecuta una búsqueda primero.</div>
</div>'''
try:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
has_sklearn = True
except ImportError:
has_sklearn = False
nodes = []
edges = []
docs = docs_df.to_dict(orient="records")
# Create Nodes
for i, row in enumerate(docs):
node_id = i + 1
titulo = str(row.get("Título", ""))
fuente = str(row.get("Fuente", ""))
autores = str(row.get("Autores", ""))
año = str(row.get("Año", ""))
grade = str(row.get("GRADE", ""))
# Color based on source
color = "#3b82f6" # default blue
if "PubMed" in fuente: color = "#10b981" # green
elif "ArXiv" in fuente: color = "#f59e0b" # orange
elif "Semantic" in fuente: color = "#8b5cf6" # purple
elif "Crossref" in fuente: color = "#ef4444" # red
# Format label (APA style short)
parts = [a.strip() for a in autores.split(",")]
surnames = [p.split()[-1] for p in parts if p and "..." not in p]
if len(surnames) == 1: cite = f"{surnames[0]} ({año})"
elif len(surnames) == 2: cite = f"{surnames[0]} y {surnames[1]} ({año})"
elif len(surnames) > 2: cite = f"{surnames[0]} et al. ({año})"
else: cite = f"Doc {node_id}"
safe_row = json.dumps(row, ensure_ascii=False).replace("'", "&#39;").replace('"', "&quot;")
nodes.append({
"id": node_id,
"label": cite,
"title": f"<b>{cite}</b><br>{titulo[:80]}...",
"color": color,
"value": 10, # default size
"group": fuente,
"paperData": safe_row
})
# Create Edges using TF-IDF similarity on titles
if len(docs) > 1 and has_sklearn:
titles = [str(r.get("Título", "")) for r in docs]
vectorizer = TfidfVectorizer(stop_words='english')
try:
tfidf_matrix = vectorizer.fit_transform(titles)
sim_matrix = cosine_similarity(tfidf_matrix)
for i in range(len(docs)):
for j in range(i+1, len(docs)):
sim = sim_matrix[i][j]
if sim > 0.2: # Threshold for similarity
edges.append({
"from": i + 1,
"to": j + 1,
"value": float(sim),
"title": f"Similitud: {sim:.2f}"
})
nodes[i]["value"] += (sim * 5)
nodes[j]["value"] += (sim * 5)
except Exception:
pass # fallback if tfidf fails
nodes_json = json.dumps(nodes)
edges_json = json.dumps(edges)
html = f"""
<div style="position:relative; width:100%; height:650px; background:var(--bg, #0f172a); border-radius:12px; overflow:hidden; border:1px solid var(--glass-border, rgba(255,255,255,0.08));">
<!-- Controls -->
<div style="position:absolute; top:16px; left:16px; z-index:10; background:rgba(15,23,42,0.8); backdrop-filter:blur(8px); padding:8px; border-radius:8px; border:1px solid rgba(255,255,255,0.1); display:flex; gap:8px;">
<button onclick="window.network.fit()" style="background:var(--primary, #8b5cf6); color:white; border:none; padding:6px 12px; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:0.2s;" onmouseover="this.style.opacity=0.8" onmouseout="this.style.opacity=1">🔍 Centrar Grafo</button>
</div>
<!-- Network Container -->
<div id="mynetwork" style="width:100%; height:100%;"></div>
<!-- Side Panel -->
<div id="graph-sidepanel" style="position:absolute; top:0; right:-400px; width:350px; height:100%; background:rgba(15,23,42,0.95); backdrop-filter:blur(16px); border-left:1px solid rgba(255,255,255,0.1); transition:right 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow:-10px 0 30px rgba(0,0,0,0.5); z-index:20; display:flex; flex-direction:column;">
<div style="padding:16px; border-bottom:1px solid rgba(255,255,255,0.1); display:flex; justify-content:space-between; align-items:center;">
<h3 style="margin:0; font-size:16px; font-weight:600; color:white;">Detalles del Documento</h3>
<button onclick="document.getElementById('graph-sidepanel').style.right='-400px'" style="background:transparent; border:none; color:#9ca3af; cursor:pointer; font-size:24px; display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:16px; transition:0.2s;" onmouseover="this.style.background='rgba(255,255,255,0.1)'" onmouseout="this.style.background='transparent'">×</button>
</div>
<div id="graph-panel-content" style="padding:20px; overflow-y:auto; flex-grow:1; color:#e2e8f0; font-size:13px; line-height:1.6;">
Selecciona un nodo para ver los detalles.
</div>
</div>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<script type="text/javascript">
// Use setTimeout to ensure DOM is ready in Gradio
setTimeout(function() {{
if(typeof vis === 'undefined') return;
var nodes = new vis.DataSet({nodes_json});
var edges = new vis.DataSet({edges_json});
var container = document.getElementById('mynetwork');
var data = {{ nodes: nodes, edges: edges }};
var options = {{
nodes: {{
shape: 'dot',
scaling: {{ min: 10, max: 35 }},
font: {{ color: '#e2e8f0', size: 12, face: 'system-ui, -apple-system, sans-serif' }},
borderWidth: 2,
shadow: {{ enabled: true, color: 'rgba(0,0,0,0.4)', size: 10 }}
}},
edges: {{
color: {{ color: 'rgba(148, 163, 184, 0.2)', highlight: '#8b5cf6' }},
smooth: {{ type: 'continuous' }}
}},
physics: {{
barnesHut: {{ gravitationalConstant: -2000, centralGravity: 0.2, springLength: 150 }},
stabilization: {{ iterations: 150 }}
}},
interaction: {{ hover: true, tooltipDelay: 200, zoomView: true, dragView: true }}
}};
window.network = new vis.Network(container, data, options);
window.network.on("click", function (params) {{
if (params.nodes.length > 0) {{
var nodeId = params.nodes[0];
var node = nodes.get(nodeId);
if(node && node.paperData) {{
var paper = JSON.parse(node.paperData.replace(/&quot;/g, '"').replace(/&#39;/g, "'"));
var panel = document.getElementById('graph-sidepanel');
var content = document.getElementById('graph-panel-content');
var html = '<div style="margin-bottom:16px; display:flex; gap:8px;">';
html += '<span style="background:rgba(255,255,255,0.1); padding:4px 10px; border-radius:12px; font-size:11px; font-weight:600;">' + paper.Fuente + '</span>';
if(paper.GRADE) {{
var gradeColor = "#6b7280";
var gradeLvl = paper.GRADE.split(" - ")[0].trim().toUpperCase();
if(gradeLvl === "HIGH") gradeColor = "#10b981";
else if(gradeLvl === "MODERATE") gradeColor = "#f59e0b";
else if(gradeLvl === "LOW" || gradeLvl === "VERY LOW") gradeColor = "#ef4444";
html += '<span style="background:'+gradeColor+'15; color:'+gradeColor+'; border:1px solid '+gradeColor+'40; padding:4px 10px; border-radius:12px; font-size:11px; font-weight:600;">' + paper.GRADE + '</span>';
}}
html += '</div>';
html += '<h4 style="margin:0 0 16px 0; color:white; font-size:16px; line-height:1.4;">' + paper.Título + '</h4>';
html += '<p style="color:#94a3b8; margin-bottom:8px;"><b>Autores:</b> ' + paper.Autores + '</p>';
html += '<p style="color:#94a3b8; margin-bottom:24px;"><b>Año:</b> ' + paper.Año + '</p>';
if(paper.DOI) html += '<a href="https://doi.org/' + paper.DOI + '" target="_blank" style="display:inline-block; background:rgba(59,130,246,0.1); color:#3b82f6; border:1px solid rgba(59,130,246,0.2); padding:8px 16px; border-radius:8px; text-decoration:none; margin-bottom:8px; width:100%; text-align:center; box-sizing:border-box; transition:0.2s;" onmouseover="this.style.background=\'rgba(59,130,246,0.2)\'" onmouseout="this.style.background=\'rgba(59,130,246,0.1)\'">🔗 Ver en DOI</a>';
if(paper['PDF URL']) html += '<a href="' + paper['PDF URL'] + '" target="_blank" style="display:inline-block; background:rgba(239,68,68,0.1); color:#ef4444; border:1px solid rgba(239,68,68,0.2); padding:8px 16px; border-radius:8px; text-decoration:none; margin-bottom:8px; width:100%; text-align:center; box-sizing:border-box; transition:0.2s;" onmouseover="this.style.background=\'rgba(239,68,68,0.2)\'" onmouseout="this.style.background=\'rgba(239,68,68,0.1)\'">📄 Abrir PDF</a>';
content.innerHTML = html;
panel.style.right = '0';
}}
}} else {{
document.getElementById('graph-sidepanel').style.right = '-400px';
}}
}});
// Close panel on empty click
container.addEventListener("click", function(e) {{
if(window.network.getSelection().nodes.length === 0) {{
document.getElementById('graph-sidepanel').style.right = '-400px';
}}
}});
}}, 500);
</script>
</div>
"""
return html