| 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") |
| |
| |
| 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 = "#3b82f6" |
| if "PubMed" in fuente: color = "#10b981" |
| elif "ArXiv" in fuente: color = "#f59e0b" |
| elif "Semantic" in fuente: color = "#8b5cf6" |
| elif "Crossref" in fuente: color = "#ef4444" |
| |
| |
| 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("'", "'").replace('"', """) |
|
|
| nodes.append({ |
| "id": node_id, |
| "label": cite, |
| "title": f"<b>{cite}</b><br>{titulo[:80]}...", |
| "color": color, |
| "value": 10, |
| "group": fuente, |
| "paperData": safe_row |
| }) |
|
|
| |
| 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: |
| 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 |
|
|
| 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(/"/g, '"').replace(/'/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 |
|
|