feat: mejoras de navegación y visualización en sub-páginas
- sub-nav.css: barra de navegación compartida con botones estilo home (fondos por sección, texto negro, borde negro, hover neón, fix WebGL z-index) - Móvil: panel de detalle ocupa 50% inferior, grafo permanece visible arriba - Imágenes de nodo duplicadas en tamaño (160×104), detail-img a 28vh - output_*.js: función showEgoGraph — filtra el grafo al ego-network del nodo seleccionado; botón "Ver solo conexiones" (solo si hay relaciones); botón flotante "← Volver al grafo completo" - int-sec.js: eliminado makeTextSprite, igualado al resto (nulos para no-imagen) - Eliminado footer de los 5 sub-HTML - image_analyzer.py: cuantización int4 (NF4) para Qwen3-VL-8B → 6.4 GB VRAM Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
778db90d78
commit
9b67e2915b
15 changed files with 2070 additions and 1083 deletions
|
|
@ -19,10 +19,10 @@ app.use(
|
|||
helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", 'https://unpkg.com', 'https://cdnjs.cloudflare.com'],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net', 'https://esm.sh', 'https://unpkg.com', 'https://cdnjs.cloudflare.com'],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
||||
imgSrc: ["'self'", 'data:', 'blob:'],
|
||||
connectSrc: ["'self'"],
|
||||
connectSrc: ["'self'", 'https://esm.sh'],
|
||||
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
|
|
@ -150,10 +150,8 @@ app.get('/api/data', async (req, res) => {
|
|||
if (subtematica) nodesQuery.subtema = subtematica;
|
||||
if (regexKw) nodesQuery.texto = { $regex: regexKw, $options: 'i' };
|
||||
if (fechaGte) nodesQuery.fecha = { $gte: fechaGte, $lte: fechaLte };
|
||||
// Ejecutar la consulta y obtener los resultados de las colecciones
|
||||
console.log('Ejecutando consulta para nodos en colecciones...');
|
||||
// Límite de imágenes: un tercio del total para no saturar el grafo
|
||||
const imagenesLimit = Math.floor(nodosLimit / 3);
|
||||
// Imágenes: proporcional a nodosLimit (1/3), mínimo 3 para siempre mostrar algo
|
||||
const imagenesLimit = Math.max(3, Math.floor(nodosLimit / 3));
|
||||
|
||||
const [wikipediaNodes, noticiasNodes, torrentsNodes, imagenesNodes, imagenesWikiNodes] = await Promise.all([
|
||||
wikipediaCollection.find(nodesQuery).limit(nodosLimit).toArray(),
|
||||
|
|
@ -162,15 +160,11 @@ app.get('/api/data', async (req, res) => {
|
|||
imagenesCollection.find(nodesQuery).limit(imagenesLimit).toArray(),
|
||||
imagenesWikiCollection.find(nodesQuery).limit(imagenesLimit).toArray(),
|
||||
]);
|
||||
console.log('Nodos Wikipedia:', wikipediaNodes.length, '| Noticias:', noticiasNodes.length,
|
||||
'| Torrents:', torrentsNodes.length, '| Imágenes:', imagenesNodes.length,
|
||||
'| Imágenes Wiki:', imagenesWikiNodes.length);
|
||||
|
||||
// Preferir imagenes analizadas (con keywords); si no hay, usar imagenes_wiki
|
||||
const imgNodes = imagenesNodes.length > 0 ? imagenesNodes : imagenesWikiNodes;
|
||||
|
||||
const nodes = [...wikipediaNodes, ...noticiasNodes, ...torrentsNodes];
|
||||
console.log('Total nodos texto:', nodes.length, '| Nodos imagen:', imgNodes.length);
|
||||
|
||||
// Nodos de texto
|
||||
const formattedNodes = nodes.map((result) => ({
|
||||
|
|
@ -179,21 +173,28 @@ app.get('/api/data', async (req, res) => {
|
|||
tema: result.tema || 'sin tema',
|
||||
content: result.texto || '',
|
||||
fecha: result.fecha || '',
|
||||
fuente: result.fuente || result.autor || result.source || '',
|
||||
type: 'texto',
|
||||
}));
|
||||
|
||||
// Nodos de imagen — image_url construida desde image_path absoluto
|
||||
const WIKI_IMAGES_BASE = '/var/www/theflows.net/flujos/FLUJOS_DATOS/IMAGENES/output/wiki_images/';
|
||||
const imgFormattedNodes = imgNodes.map((result) => {
|
||||
const relativePath = result.image_path
|
||||
? result.image_path.replace(WIKI_IMAGES_BASE, '')
|
||||
: null;
|
||||
let relativePath = null;
|
||||
if (result.image_path && result.image_path.startsWith(WIKI_IMAGES_BASE)) {
|
||||
const rel = result.image_path.slice(WIKI_IMAGES_BASE.length);
|
||||
// Reject paths with traversal sequences or absolute paths
|
||||
if (rel && !rel.includes('..') && !rel.startsWith('/')) {
|
||||
relativePath = rel;
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: result.archivo.trim(),
|
||||
group: result.subtema || result.tema || 'imagen',
|
||||
tema: result.tema || 'sin tema',
|
||||
content: result.texto || result.descripcion_wiki || '',
|
||||
fecha: result.fecha || '',
|
||||
fuente: result.fuente || result.autor || result.source || '',
|
||||
type: 'imagen',
|
||||
image_url: relativePath ? `/wiki-images/${relativePath}` : null,
|
||||
label: result.subtema || result.tema || result.archivo,
|
||||
|
|
@ -201,47 +202,186 @@ app.get('/api/data', async (req, res) => {
|
|||
}).filter(n => n.image_url);
|
||||
|
||||
const allNodes = [...formattedNodes, ...imgFormattedNodes];
|
||||
const formattedNodes_final = allNodes; // alias para claridad
|
||||
const formattedNodes_final = allNodes;
|
||||
|
||||
// Obtener los IDs de todos los nodos (texto + imagen)
|
||||
const nodeIds = formattedNodes_final.map(node => node.id);
|
||||
console.log('IDs de nodos:', nodeIds);
|
||||
|
||||
// Construir la consulta para obtener los enlaces relacionados con los nodos obtenidos
|
||||
// Links texto-texto: filtro por complejidad del usuario.
|
||||
// Links imagen-texto (source1_type='imagen'): umbral fijo de 3% porque la similitud
|
||||
// TF-IDF keyword↔texto es estructuralmente baja (~3-10%) y quedarían siempre ocultos.
|
||||
const IMG_UMBRAL = 3.0;
|
||||
const linksQuery = {
|
||||
porcentaje_similitud: { $gte: porcentajeSimilitudMin },
|
||||
noticia1: { $in: nodeIds },
|
||||
noticia2: { $in: nodeIds },
|
||||
$or: [
|
||||
{ porcentaje_similitud: { $gte: porcentajeSimilitudMin } },
|
||||
{ porcentaje_similitud: { $gte: IMG_UMBRAL }, source1_type: 'imagen' },
|
||||
],
|
||||
};
|
||||
console.log('Consulta para enlaces:', linksQuery);
|
||||
|
||||
// Ejecutar la consulta y obtener los enlaces
|
||||
console.log('Ejecutando consulta para enlaces...');
|
||||
const links = await comparacionesCollection.find(linksQuery).toArray();
|
||||
console.log('Enlaces obtenidos:', links.length);
|
||||
|
||||
// Formatear los enlaces sin normalizar los IDs
|
||||
const formattedLinks = links.map((result) => ({
|
||||
// Limitar links por nodo: cada nodo muestra solo sus top MAX conexiones más fuertes.
|
||||
// Sin esto, los 100 nodos wikipedia del mismo tema se conectan todos entre sí
|
||||
// (>8000 links) y forman una bola densa inmanejable.
|
||||
const MAX_LINKS_PER_NODE = 8;
|
||||
links.sort((a, b) => b.porcentaje_similitud - a.porcentaje_similitud);
|
||||
const nodeDegree = new Map();
|
||||
const selectedLinks = [];
|
||||
for (const link of links) {
|
||||
const n1 = link.noticia1.trim();
|
||||
const n2 = link.noticia2.trim();
|
||||
const d1 = nodeDegree.get(n1) || 0;
|
||||
const d2 = nodeDegree.get(n2) || 0;
|
||||
if (d1 < MAX_LINKS_PER_NODE && d2 < MAX_LINKS_PER_NODE) {
|
||||
selectedLinks.push(link);
|
||||
nodeDegree.set(n1, d1 + 1);
|
||||
nodeDegree.set(n2, d2 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const formattedLinks = selectedLinks.map((result) => ({
|
||||
source: result.noticia1.trim(),
|
||||
target: result.noticia2.trim(),
|
||||
value: result.porcentaje_similitud,
|
||||
}));
|
||||
|
||||
// Enviar los datos al cliente en formato JSON
|
||||
res.json({ nodes: formattedNodes_final, links: formattedLinks });
|
||||
console.log('Datos enviados al cliente');
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos:', error);
|
||||
res.status(500).json({ error: 'Error al obtener datos' });
|
||||
}
|
||||
});
|
||||
|
||||
// ── STATS: cálculo pesado → MongoDB, endpoint solo lee ─────────────────────────
|
||||
const STATS_TEMAS = ['guerra global','inteligencia y seguridad','cambio climático','demografía y sociedad','economía y corporaciones'];
|
||||
const STATS_TEXT_COLS = ['wikipedia','noticias','torrents','imagenes_wiki'];
|
||||
|
||||
async function computeAndSaveStats() {
|
||||
if (!db) return;
|
||||
console.log('[stats] Calculando stats...');
|
||||
try {
|
||||
const [wikipedia, noticias, torrents, imagenes, imagenes_wiki, comparaciones,
|
||||
comp_imagen_total] = await Promise.all([
|
||||
db.collection('wikipedia').countDocuments({}),
|
||||
db.collection('noticias').countDocuments({}),
|
||||
db.collection('torrents').countDocuments({}),
|
||||
db.collection('imagenes').countDocuments({}),
|
||||
db.collection('imagenes_wiki').countDocuments({}),
|
||||
db.collection('comparaciones').estimatedDocumentCount(),
|
||||
db.collection('comparaciones').countDocuments({ source1_type: 'imagen' }),
|
||||
]);
|
||||
|
||||
const por_tema = {};
|
||||
await Promise.all(STATS_TEMAS.map(async tema => {
|
||||
const counts = await Promise.all(
|
||||
STATS_TEXT_COLS.map(col => db.collection(col).countDocuments({ tema }))
|
||||
);
|
||||
por_tema[tema] = {};
|
||||
STATS_TEXT_COLS.forEach((col, i) => { por_tema[tema][col] = counts[i]; });
|
||||
}));
|
||||
|
||||
const subtemaRaw = await db.collection('wikipedia').aggregate([
|
||||
{ $match: { tema: { $in: STATS_TEMAS } } },
|
||||
{ $group: { _id: { tema: '$tema', subtema: '$subtema' }, n: { $sum: 1 } } },
|
||||
{ $sort: { n: -1 } },
|
||||
]).toArray();
|
||||
const subtemas = {};
|
||||
for (const doc of subtemaRaw) {
|
||||
const { tema, subtema } = doc._id;
|
||||
if (!subtemas[tema]) subtemas[tema] = [];
|
||||
if (subtemas[tema].length < 10)
|
||||
subtemas[tema].push({ subtema: subtema || 'sin subtema', n: doc.n });
|
||||
}
|
||||
|
||||
const notSubRaw = await db.collection('noticias').aggregate([
|
||||
{ $match: { tema: { $in: STATS_TEMAS } } },
|
||||
{ $group: { _id: { tema: '$tema', subtema: '$subtema' }, n: { $sum: 1 } } },
|
||||
{ $sort: { n: -1 } },
|
||||
]).toArray();
|
||||
const subtemas_noticias = {};
|
||||
for (const doc of notSubRaw) {
|
||||
const { tema, subtema } = doc._id;
|
||||
if (!subtemas_noticias[tema]) subtemas_noticias[tema] = [];
|
||||
if (subtemas_noticias[tema].length < 8)
|
||||
subtemas_noticias[tema].push({ subtema: subtema || 'sin subtema', n: doc.n });
|
||||
}
|
||||
|
||||
const compImgRaw = await db.collection('comparaciones').aggregate([
|
||||
{ $match: { source1_type: 'imagen' } },
|
||||
{ $group: {
|
||||
_id: '$tema',
|
||||
count: { $sum: 1 },
|
||||
avg_sim: { $avg: '$porcentaje_similitud' },
|
||||
max_sim: { $max: '$porcentaje_similitud' },
|
||||
}},
|
||||
]).toArray();
|
||||
const comp_imagen = { total: comp_imagen_total, por_tema: {} };
|
||||
for (const d of compImgRaw) {
|
||||
comp_imagen.por_tema[d._id] = {
|
||||
count: d.count,
|
||||
avg_sim: Math.round(d.avg_sim * 10) / 10,
|
||||
max_sim: Math.round(d.max_sim * 10) / 10,
|
||||
};
|
||||
}
|
||||
|
||||
// $sample sin filtro — 99.94% de docs son texto-texto
|
||||
const simRaw = await db.collection('comparaciones').aggregate([
|
||||
{ $sample: { size: 3000 } },
|
||||
{ $group: { _id: null, avg_sim: { $avg: '$porcentaje_similitud' }, max_sim: { $max: '$porcentaje_similitud' } } },
|
||||
]).toArray();
|
||||
const comp_texto = simRaw[0]
|
||||
? { avg_sim: Math.round(simRaw[0].avg_sim * 10) / 10, max_sim: Math.round(simRaw[0].max_sim * 10) / 10 }
|
||||
: { avg_sim: 0, max_sim: 0 };
|
||||
|
||||
const snapshot = {
|
||||
_id: 'stats',
|
||||
computed_at: new Date(),
|
||||
totales: { wikipedia, noticias, torrents, imagenes, imagenes_wiki, comparaciones },
|
||||
por_tema, subtemas, subtemas_noticias, comp_imagen, comp_texto,
|
||||
};
|
||||
|
||||
await db.collection('stats_cache').replaceOne({ _id: 'stats' }, snapshot, { upsert: true });
|
||||
console.log('[stats] Stats guardadas en MongoDB');
|
||||
} catch (e) {
|
||||
console.error('[stats] Error calculando stats:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Calcular al arrancar (si no hay cache o tiene más de 6h) y luego cada 6h
|
||||
async function initStats() {
|
||||
if (!db) return;
|
||||
const cached = await db.collection('stats_cache').findOne({ _id: 'stats' });
|
||||
const sixHours = 6 * 60 * 60 * 1000;
|
||||
if (!cached || !cached.computed_at || Date.now() - new Date(cached.computed_at).getTime() > sixHours) {
|
||||
computeAndSaveStats(); // async, no bloquea el arranque
|
||||
}
|
||||
setInterval(computeAndSaveStats, sixHours);
|
||||
}
|
||||
|
||||
app.get('/api/stats', async (req, res) => {
|
||||
try {
|
||||
if (!db) return res.status(500).json({ error: 'Sin conexión a BD' });
|
||||
const cached = await db.collection('stats_cache').findOne({ _id: 'stats' });
|
||||
if (!cached) return res.status(503).json({ error: 'Stats aún calculándose, intenta en unos minutos' });
|
||||
const { _id, ...data } = cached;
|
||||
res.json(data);
|
||||
} catch (e) {
|
||||
console.error('Error /api/stats:', e);
|
||||
res.status(500).json({ error: 'Error al obtener estadísticas' });
|
||||
}
|
||||
});
|
||||
|
||||
// **Luego, definir las rutas de las páginas principales**
|
||||
// Rutas para las páginas principales
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/index.html'));
|
||||
});
|
||||
|
||||
app.get('/stats.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/stats.html'));
|
||||
});
|
||||
|
||||
app.get('/climate.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/climate.html'));
|
||||
});
|
||||
|
|
@ -287,6 +427,7 @@ async function connectToMongoDB() {
|
|||
await mongoClient.connect();
|
||||
db = mongoClient.db(dbName);
|
||||
console.log('Conectado a MongoDB');
|
||||
initStats();
|
||||
} catch (error) {
|
||||
console.error('Error al conectar a MongoDB:', error);
|
||||
process.exit(1); // Salir de la aplicación si no se puede conectar
|
||||
|
|
|
|||
|
|
@ -4,43 +4,120 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>CLIMATE</title>
|
||||
<link rel="stylesheet" href="climate.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
<!-- Fuentes de Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Cargar 3d-force-graph (incluye Three.js) -->
|
||||
<!-- CORRECTO: d3 primero, force-graph después -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
<script src="https://unpkg.com/3d-force-graph"></script>
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<ul class="nav-links">
|
||||
<li><a href="climate.html" class="climate">CLIMATE</a></li>
|
||||
</ul>
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC</a>
|
||||
</div>
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('cambio climático');return false;">Cambio Climático</a>
|
||||
<a href="#" onclick="setSubtema('desastres naturales');return false;">Desastres Naturales</a>
|
||||
<a href="#" onclick="setSubtema('conservación');return false;">Conservación</a>
|
||||
<a href="#" onclick="setSubtema('energía renovable');return false;">Energía Renovable</a>
|
||||
<a href="#" onclick="setSubtema('contaminacion');return false;">Escasez de Agua</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-item">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div id="climateContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="split-screen">
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="climateContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div id="detailPanel">
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
<div id="sidebar">
|
||||
|
|
@ -54,22 +131,23 @@
|
|||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100">
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">Complejidad:</label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Búsqueda por palabra:</label>
|
||||
<input type="text" id="param1" name="param1">
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: glaciares, CO2...">
|
||||
|
||||
<label for="color1">Color 1:</label>
|
||||
<input type="color" id="color1" name="color1">
|
||||
|
||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
||||
<input type="text" id="param2" name="param2">
|
||||
|
||||
<label for="color2">Color 2:</label>
|
||||
<input type="color" id="color2" name="color2">
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="conservación">conservación</option>
|
||||
<option value="cambio climático">cambio climático</option>
|
||||
<option value="energía renovable">energía renovable</option>
|
||||
<option value="desastres naturales">desastres naturales</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
|
|
@ -77,11 +155,7 @@
|
|||
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<footer>
|
||||
<p><a href="#">GitHub</a> | <a href="#">Telegram</a> | <a href="#">Email</a> | <a href="#">Web de Tor</a></p>
|
||||
</footer>
|
||||
|
||||
<!-- Movemos la inclusión del script aquí, al final del body -->
|
||||
<script src="output_climate_pruebas.js"></script>
|
||||
<script type="module" src="output_climate_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Economía y Corporaciones</title>
|
||||
<link rel="stylesheet" href="eco-corp.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
<!-- Fuentes de Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
|
@ -11,32 +12,111 @@
|
|||
|
||||
<!-- Cargar D3.js -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
<!-- Cargar 3d-force-graph (incluye Three.js) -->
|
||||
<script src="https://unpkg.com/3d-force-graph"></script>
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<ul class="nav-links">
|
||||
<li><a href="eco-corp.html" class="eco-corp">Economía y Corporaciones</a></li>
|
||||
</ul>
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE</a>
|
||||
</div>
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('economía global');return false;">Economía Global</a>
|
||||
<a href="#" onclick="setSubtema('corporaciones multinacionales');return false;">Corporaciones Multinacionales</a>
|
||||
<a href="#" onclick="setSubtema('comercio internacional');return false;">Comercio Internacional</a>
|
||||
<a href="#" onclick="setSubtema('organismos financieros');return false;">Organismos Financieros</a>
|
||||
<a href="#" onclick="setSubtema('desigualdad económica');return false;">Desigualdad Económica</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-item">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div id="ecoCorpContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="split-screen">
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="ecoCorpContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div id="detailPanel">
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
|
@ -50,16 +130,24 @@
|
|||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100">
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">Complejidad:</label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Búsqueda por palabra:</label>
|
||||
<input type="text" id="param1" name="param1">
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: FMI, Amazon...">
|
||||
|
||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
||||
<input type="text" id="param2" name="param2">
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="comercio internacional">comercio internacional</option>
|
||||
<option value="economía global">economía global</option>
|
||||
<option value="desigualdad económica">desigualdad económica</option>
|
||||
<option value="corporaciones multinacionales">corporaciones multinacionales</option>
|
||||
<option value="organismos financieros">organismos financieros</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
|
|
@ -67,11 +155,7 @@
|
|||
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<footer>
|
||||
<p><a href="#">GitHub</a> | <a href="#">Telegram</a> | <a href="#">Email</a> | <a href="#">Web de Tor</a></p>
|
||||
</footer>
|
||||
|
||||
<!-- Incluir tu script al final del body -->
|
||||
<script src="output_eco_corp_pruebas.js"></script>
|
||||
<script type="module" src="output_eco_corp_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -4,41 +4,117 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Glob-War</title>
|
||||
<link rel="stylesheet" href="glob-war.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
<!-- Fuentes de Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Cargar D3.js -->
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
<!-- Three.js (necesario para Sprite/TextureLoader en nodos imagen) -->
|
||||
<script src="https://unpkg.com/three@0.168/build/three.min.js"></script>
|
||||
<!-- Cargar 3d-force-graph -->
|
||||
<script src="https://unpkg.com/3d-force-graph"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<ul class="nav-links">
|
||||
<li><a href="glob-war.html" class="glob-war">GLOB-WAR</a></li>
|
||||
</ul>
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('conflictos internacionales');return false;">Conflictos Internacionales</a>
|
||||
<a href="#" onclick="setSubtema('guerras civiles');return false;">Guerras Civiles</a>
|
||||
<a href="#" onclick="setSubtema('terrorismo');return false;">Terrorismo</a>
|
||||
<a href="#" onclick="setSubtema('armas');return false;">Armas</a>
|
||||
<a href="#" onclick="setSubtema('alianzas militares');return false;">Alianzas Militares</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-item">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div id="globWarContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="split-screen">
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="globWarContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div id="detailPanel">
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
|
@ -52,22 +128,24 @@
|
|||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100">
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">Complejidad:</label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Búsqueda por palabra:</label>
|
||||
<input type="text" id="param1" name="param1">
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: misiles, OTAN...">
|
||||
|
||||
<label for="color1">Color 1:</label>
|
||||
<input type="color" id="color1" name="color1">
|
||||
|
||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
||||
<input type="text" id="param2" name="param2">
|
||||
|
||||
<label for="color2">Color 2:</label>
|
||||
<input type="color" id="color2" name="color2">
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="armas">armas</option>
|
||||
<option value="terrorismo">terrorismo</option>
|
||||
<option value="guerras civiles">guerras civiles</option>
|
||||
<option value="conflictos internacionales">conflictos internacionales</option>
|
||||
<option value="alianzas militares">alianzas militares</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
|
|
@ -75,11 +153,7 @@
|
|||
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<footer>
|
||||
<p><a href="#">GitHub</a> | <a href="#">Telegram</a> | <a href="#">Email</a> | <a href="#">Web de Tor</a></p>
|
||||
</footer>
|
||||
|
||||
<!-- Incluir tu script al final del body -->
|
||||
<script src="output_glob_war_pruebas.js"></script>
|
||||
<script type="module" src="output_glob_war_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@
|
|||
<header>
|
||||
<div class="header-content">
|
||||
<h1 class="title">UP-LEAKS</h1>
|
||||
<div class="header-buttons">
|
||||
<div class="header-buttons header-buttons--left">
|
||||
<a href="/coconews/" class="small-button coconews-btn">COCONEWS</a>
|
||||
<a href="journalist.html" class="small-button">Journalist</a>
|
||||
<a href="stats.html" class="small-button stats-btn">STATS</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Inteligencia y Seguridad</title>
|
||||
<link rel="stylesheet" href="int-sec.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
<!-- Fuentes de Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
|
@ -11,19 +12,77 @@
|
|||
|
||||
<!-- Cargar D3.js -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
<!-- Cargar 3d-force-graph (incluye Three.js) -->
|
||||
<script src="https://unpkg.com/3d-force-graph"></script>
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<ul class="nav-links">
|
||||
<li><a href="int-sec.html" class="int-sec">Inteligencia y Seguridad</a></li>
|
||||
</ul>
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR</a>
|
||||
</div>
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('inteligencia');return false;">Inteligencia</a>
|
||||
<a href="#" onclick="setSubtema('ciberseguridad');return false;">Ciberseguridad</a>
|
||||
<a href="#" onclick="setSubtema('espionaje');return false;">Espionaje</a>
|
||||
<a href="#" onclick="setSubtema('seguridad nacional');return false;">Seguridad Nacional</a>
|
||||
<a href="#" onclick="setSubtema('contraterrorismo');return false;">Contraterrorismo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-item">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="split-screen">
|
||||
<!-- PANEL IZQUIERDO: el grafo -->
|
||||
<div id="graphPanel">
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="intSecContainer"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos.jpg">
|
||||
|
|
@ -43,7 +102,23 @@
|
|||
|
||||
<!-- PANEL DERECHO: detalle de la noticia -->
|
||||
<div id="detailPanel">
|
||||
<p class="placeholder">Haz click en un nodo para ver aquí la noticia completa.</p>
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
|
@ -57,16 +132,24 @@
|
|||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100">
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">Complejidad:</label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Búsqueda por palabra:</label>
|
||||
<input type="text" id="param1" name="param1">
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: CIA, GCHQ...">
|
||||
|
||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
||||
<input type="text" id="param2" name="param2">
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="inteligencia">inteligencia</option>
|
||||
<option value="seguridad nacional">seguridad nacional</option>
|
||||
<option value="espionaje">espionaje</option>
|
||||
<option value="ciberseguridad">ciberseguridad</option>
|
||||
<option value="contraterrorismo">contraterrorismo</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
|
|
@ -74,16 +157,7 @@
|
|||
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
<a href="#">GitHub</a> |
|
||||
<a href="#">Telegram</a> |
|
||||
<a href="#">Email</a> |
|
||||
<a href="#">Web de Tor</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<!-- Incluir tu script al final del body -->
|
||||
<script src="output_int_sec.js"></script>
|
||||
<script type="module" src="output_int_sec.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,50 @@
|
|||
// output_climate_pruebas.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
// Obtener el contenedor del gráfico
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('climateContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
|
||||
// Inicializar el gráfico 3D
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel('id')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(1)
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => '#42FF00')
|
||||
.onNodeClick(node => showNodeContent(node.content))
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.forceEngine('d3')
|
||||
//.d3Force('charge', d3.forceManyBody().strength(-300))
|
||||
//.d3Force('link', d3.forceLink().distance(300).strength(1));
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
// Centrado automático del gráfico
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
const width = elem.clientWidth;
|
||||
const height = elem.clientHeight;
|
||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
|
|
@ -32,95 +53,175 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
centerGraph();
|
||||
});
|
||||
|
||||
// Mostrar contenido del nodo
|
||||
function showNodeContent(content) {
|
||||
console.log('Contenido del nodo:', content);
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
// Función para obtener datos del servidor
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.className = 'detail-img';
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Establecer el tema fijo
|
||||
params.append('tema', 'cambio climático');
|
||||
|
||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
url += `?${params.toString()}`;
|
||||
console.log('🔎 Fetch URL:', url);
|
||||
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
console.log('Datos recibidos del servidor:', data);
|
||||
|
||||
// Filtrar enlaces inválidos
|
||||
const nodeIds = new Set(data.nodes.map(node => node.id));
|
||||
data.links = data.links.filter(link => nodeIds.has(link.source) && nodeIds.has(link.target));
|
||||
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos del servidor:', error);
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
||||
async function fetchAndRender() {
|
||||
const subtematica = document.getElementById('param2').value;
|
||||
const palabraClave = document.getElementById('param1').value;
|
||||
const fechaInicio = document.getElementById('fecha_inicio').value;
|
||||
const fechaFin = document.getElementById('fecha_fin').value;
|
||||
const nodos = document.getElementById('nodos').value;
|
||||
const complejidad = document.getElementById('complejidad').value;
|
||||
|
||||
// Usamos 'complejidad' como porcentaje de similitud mínima (umbral)
|
||||
const paramsObj = {
|
||||
subtematica,
|
||||
palabraClave,
|
||||
fechaInicio,
|
||||
fechaFin,
|
||||
nodos,
|
||||
complejidad // enviado al backend y usado de umbral en cliente
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
|
||||
// Parsear complejidad a número
|
||||
const umbralPct = parseFloat(complejidad) || 0;
|
||||
console.log(`Aplicando umbral de similitud: ${umbralPct}%`);
|
||||
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
|
||||
// Filtrar enlaces por umbral de similitud
|
||||
let filteredLinks = graphData.links;
|
||||
if (umbralPct > 0) {
|
||||
filteredLinks = filteredLinks.filter(link => Number(link.value) >= umbralPct);
|
||||
console.log(`Enlaces tras aplicar umbral: ${filteredLinks.length}`);
|
||||
}
|
||||
|
||||
graph.graphData({ nodes: graphData.nodes, links: filteredLinks });
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
// Escuchar evento submit del formulario
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
fetchAndRender();
|
||||
});
|
||||
|
||||
// Cargar gráfico inicial
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,50 @@
|
|||
// output_eco_corp.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
// Obtener el contenedor del gráfico
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('ecoCorpContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
|
||||
// Inicializar el gráfico 3D
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel('id')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(1)
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => 'yellow')
|
||||
.onNodeClick(node => showNodeContent(node.content))
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.forceEngine('d3')
|
||||
//.d3Force('charge', d3.forceManyBody().strength(-300))
|
||||
//.d3Force('link', d3.forceLink().distance(300).strength(1));
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
// Centrado automático del gráfico
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
const width = elem.clientWidth;
|
||||
const height = elem.clientHeight;
|
||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
|
|
@ -32,89 +53,175 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
centerGraph();
|
||||
});
|
||||
|
||||
// Mostrar contenido del nodo
|
||||
function showNodeContent(content) {
|
||||
console.log('Contenido del nodo:', content);
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
// Función para obtener datos del servidor
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.className = 'detail-img';
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Establecer el tema fijo
|
||||
params.append('tema', 'economía y corporaciones');
|
||||
|
||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
url += `?${params.toString()}`;
|
||||
console.log('🔎 Fetch URL:', url);
|
||||
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
console.log('Datos recibidos del servidor:', data);
|
||||
|
||||
// Filtrar enlaces inválidos
|
||||
const nodeIds = new Set(data.nodes.map(node => node.id));
|
||||
data.links = data.links.filter(link => nodeIds.has(link.source) && nodeIds.has(link.target));
|
||||
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos del servidor:', error);
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
||||
async function fetchAndRender() {
|
||||
const subtematica = document.getElementById('param2').value;
|
||||
const palabraClave = document.getElementById('param1').value;
|
||||
const fechaInicio = document.getElementById('fecha_inicio').value;
|
||||
const fechaFin = document.getElementById('fecha_fin').value;
|
||||
const nodos = document.getElementById('nodos').value;
|
||||
const complejidad = document.getElementById('complejidad').value;
|
||||
|
||||
// Usamos 'complejidad' como porcentaje de similitud mínima (umbral)
|
||||
const paramsObj = {
|
||||
subtematica,
|
||||
palabraClave,
|
||||
fechaInicio,
|
||||
fechaFin,
|
||||
nodos,
|
||||
complejidad // enviado al backend y usado de umbral en cliente
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
|
||||
// Parsear complejidad a número
|
||||
const umbralPct = parseFloat(complejidad) || 0;
|
||||
console.log(`Aplicando umbral de similitud: ${umbralPct}%`);
|
||||
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
|
||||
// Filtrar enlaces por umbral de similitud
|
||||
let filteredLinks = graphData.links;
|
||||
if (umbralPct > 0) {
|
||||
filteredLinks = filteredLinks.filter(link => Number(link.value) >= umbralPct);
|
||||
console.log(`Enlaces tras aplicar umbral: ${filteredLinks.length}`);
|
||||
}
|
||||
|
||||
graph.graphData({ nodes: graphData.nodes, links: filteredLinks });
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
// Escuchar evento submit del formulario
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
fetchAndRender();
|
||||
});
|
||||
|
||||
// Cargar gráfico inicial
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,29 +1,50 @@
|
|||
// output_glob_war.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
// Obtener el contenedor del gráfico
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('globWarContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
|
||||
// Inicializar el gráfico 3D
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel('id')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(1)
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => 'red')
|
||||
.onNodeClick(node => showNodeContent(node.content))
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.forceEngine('d3')
|
||||
//.d3Force('charge', d3.forceManyBody().strength(-300))
|
||||
//.d3Force('link', d3.forceLink().distance(300).strength(1));
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
// Centrado automático del gráfico
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
const width = elem.clientWidth;
|
||||
const height = elem.clientHeight;
|
||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
|
|
@ -32,89 +53,177 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
centerGraph();
|
||||
});
|
||||
|
||||
// Mostrar contenido del nodo
|
||||
function showNodeContent(content) {
|
||||
console.log('Contenido del nodo:', content);
|
||||
// --- Detail panel helpers ---
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
// Función para obtener datos del servidor
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.className = 'detail-img';
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
// --- Data fetch ---
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Establecer el tema fijo
|
||||
params.append('tema', 'guerra global');
|
||||
|
||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
url += `?${params.toString()}`;
|
||||
console.log('🔎 Fetch URL:', url);
|
||||
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
console.log('Datos recibidos del servidor:', data);
|
||||
|
||||
// Filtrar enlaces inválidos
|
||||
const nodeIds = new Set(data.nodes.map(node => node.id));
|
||||
data.links = data.links.filter(link => nodeIds.has(link.source) && nodeIds.has(link.target));
|
||||
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos del servidor:', error);
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
||||
async function fetchAndRender() {
|
||||
const subtematica = document.getElementById('param2').value;
|
||||
const palabraClave = document.getElementById('param1').value;
|
||||
const fechaInicio = document.getElementById('fecha_inicio').value;
|
||||
const fechaFin = document.getElementById('fecha_fin').value;
|
||||
const nodos = document.getElementById('nodos').value;
|
||||
const complejidad = document.getElementById('complejidad').value;
|
||||
|
||||
// Usamos 'complejidad' como porcentaje de similitud mínima (umbral)
|
||||
const paramsObj = {
|
||||
subtematica,
|
||||
palabraClave,
|
||||
fechaInicio,
|
||||
fechaFin,
|
||||
nodos,
|
||||
complejidad // enviado al backend y usado de umbral en cliente
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
|
||||
// Parsear complejidad a número
|
||||
const umbralPct = parseFloat(complejidad) || 0;
|
||||
console.log(`Aplicando umbral de similitud: ${umbralPct}%`);
|
||||
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
|
||||
// Filtrar enlaces por umbral de similitud
|
||||
let filteredLinks = graphData.links;
|
||||
if (umbralPct > 0) {
|
||||
filteredLinks = filteredLinks.filter(link => Number(link.value) >= umbralPct);
|
||||
console.log(`Enlaces tras aplicar umbral: ${filteredLinks.length}`);
|
||||
}
|
||||
|
||||
graph.graphData({ nodes: graphData.nodes, links: filteredLinks });
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
// Escuchar evento submit del formulario
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
fetchAndRender();
|
||||
});
|
||||
|
||||
// Cargar gráfico inicial
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,78 +1,50 @@
|
|||
// output_int_sec.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
// Obtener el contenedor del gráfico
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('intSecContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
const detailPanel = document.getElementById('detailPanel');
|
||||
|
||||
// --- Etiqueta de texto con CanvasTexture (sin SpriteText externo) ---
|
||||
function makeTextSprite(text) {
|
||||
if (!window.THREE) {
|
||||
console.warn('THREE no está disponible; omito etiquetas.');
|
||||
return undefined;
|
||||
}
|
||||
const pad = 16; // más padding para nitidez
|
||||
const font = 'bold 36px Fira Code, monospace'; // fuente más grande
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
// 1) preparar canvas al tamaño del texto
|
||||
const c = document.createElement('canvas');
|
||||
const ctx = c.getContext('2d');
|
||||
ctx.font = font;
|
||||
const w = Math.ceil(ctx.measureText(text).width) + pad * 2;
|
||||
const h = 50 + pad * 2;
|
||||
c.width = w; c.height = h;
|
||||
|
||||
// 2) dibujar texto
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.98)';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(text, pad, h / 2);
|
||||
|
||||
// 3) textura -> sprite
|
||||
const tex = new THREE.CanvasTexture(c);
|
||||
tex.needsUpdate = true;
|
||||
|
||||
const mat = new THREE.SpriteMaterial({
|
||||
map: tex,
|
||||
transparent: true,
|
||||
depthTest: false, // que no lo tape la esfera
|
||||
depthWrite: false // que no escriba en el z-buffer
|
||||
});
|
||||
const spr = new THREE.Sprite(mat);
|
||||
|
||||
// tamaño del texto en “mundo”
|
||||
const k = 0.22; // ajusta tamaño del texto (↑ más grande, ↓ más pequeño)
|
||||
spr.scale.set(w * k, h * k, 1);
|
||||
|
||||
// elevar el texto sobre el nodo
|
||||
spr.position.y = 8; // sube/baja si lo ves muy pegado
|
||||
|
||||
return spr;
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
// Inicializar el gráfico 3D
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel('id')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(1)
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => 'blue')
|
||||
// Texto fijo sobre cada nodo (usando CanvasTexture)
|
||||
.nodeThreeObject(n => makeTextSprite(n.id))
|
||||
.nodeThreeObjectExtend(true) // mantiene esfera + texto
|
||||
.onNodeClick(node => showNodeContent(node.content))
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
// .d3Force('charge', d3.forceManyBody().strength(-400))
|
||||
// .d3Force('link', d3.forceLink().distance(300).strength(1))
|
||||
|
||||
// Centrado automático del gráfico
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
const width = elem.clientWidth;
|
||||
const height = elem.clientHeight;
|
||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
|
|
@ -81,101 +53,175 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
centerGraph();
|
||||
});
|
||||
|
||||
// Mostrar contenido del nodo
|
||||
function showNodeContent(content) {
|
||||
if (!detailPanel) {
|
||||
console.log('Contenido del nodo:', content);
|
||||
return;
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const h2 = document.createElement('h2');
|
||||
h2.style.cssText = 'margin:0 0 8px';
|
||||
h2.textContent = 'Detalle';
|
||||
const pre = document.createElement('pre');
|
||||
pre.style.cssText = 'white-space:pre-wrap; line-height:1.35; font-family:\'Fira Code\', monospace; font-size:14px; color:#e5e5e5;';
|
||||
pre.textContent = content || 'No hay contenido disponible.';
|
||||
detailPanel.replaceChildren(h2, pre);
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.add('show-detail');
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
// Función para obtener datos del servidor
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.className = 'detail-img';
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Establecer el tema fijo
|
||||
params.append('tema', 'inteligencia y seguridad');
|
||||
|
||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
url += `?${params.toString()}`;
|
||||
console.log('🔎 Fetch URL:', url);
|
||||
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
console.log('Datos recibidos del servidor:', data);
|
||||
|
||||
// Filtrar enlaces inválidos
|
||||
const nodeIds = new Set(data.nodes.map(node => node.id));
|
||||
data.links = data.links.filter(link => nodeIds.has(link.source) && nodeIds.has(link.target));
|
||||
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos del servidor:', error);
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
||||
async function fetchAndRender() {
|
||||
const subtematica = document.getElementById('param2').value;
|
||||
const palabraClave = document.getElementById('param1').value;
|
||||
const fechaInicio = document.getElementById('fecha_inicio').value;
|
||||
const fechaFin = document.getElementById('fecha_fin').value;
|
||||
const nodos = document.getElementById('nodos').value;
|
||||
const complejidad = document.getElementById('complejidad').value;
|
||||
|
||||
// Usamos 'complejidad' como porcentaje de similitud mínima (umbral)
|
||||
const paramsObj = {
|
||||
subtematica,
|
||||
palabraClave,
|
||||
fechaInicio,
|
||||
fechaFin,
|
||||
nodos,
|
||||
complejidad // enviado al backend y usado de umbral en cliente
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
|
||||
// Parsear complejidad a número
|
||||
const umbralPct = parseFloat(complejidad) || 0;
|
||||
console.log(`Aplicando umbral de similitud: ${umbralPct}%`);
|
||||
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
|
||||
// Filtrar enlaces por umbral de similitud
|
||||
let filteredLinks = graphData.links;
|
||||
if (umbralPct > 0) {
|
||||
filteredLinks = filteredLinks.filter(link => Number(link.value) >= umbralPct);
|
||||
console.log(`Enlaces tras aplicar umbral: ${filteredLinks.length}`);
|
||||
}
|
||||
|
||||
graph.graphData({ nodes: graphData.nodes, links: filteredLinks });
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
// Escuchar evento submit del formulario
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
fetchAndRender();
|
||||
});
|
||||
|
||||
// Cargar gráfico inicial
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,29 +1,50 @@
|
|||
// output_popl_up_pruebas.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
// Obtener el contenedor del gráfico
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('poplUpContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
|
||||
// Inicializar el gráfico 3D
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel('id')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(1)
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => 'orange')
|
||||
.onNodeClick(node => showNodeContent(node.content))
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.forceEngine('d3')
|
||||
//.d3Force('charge', d3.forceManyBody().strength(-300))
|
||||
//.d3Force('link', d3.forceLink().distance(300).strength(1));
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
// Centrado automático del gráfico
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
const width = elem.clientWidth;
|
||||
const height = elem.clientHeight;
|
||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
|
|
@ -32,98 +53,175 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
centerGraph();
|
||||
});
|
||||
|
||||
// Mostrar contenido del nodo
|
||||
function showNodeContent(content) {
|
||||
console.log('Contenido del nodo:', content);
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
// Función para obtener datos del servidor
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.className = 'detail-img';
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Establecer el tema fijo
|
||||
params.append('tema', 'demografía y sociedad');
|
||||
|
||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
url += `?${params.toString()}`;
|
||||
console.log('🔎 Fetch URL:', url);
|
||||
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
console.log('Datos recibidos del servidor:', data);
|
||||
|
||||
// Filtrar enlaces inválidos
|
||||
const nodeIds = new Set(data.nodes.map(node => node.id));
|
||||
data.links = data.links.filter(link => nodeIds.has(link.source) && nodeIds.has(link.target));
|
||||
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos del servidor:', error);
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
||||
async function fetchAndRender() {
|
||||
const subtematica = document.getElementById('param2').value;
|
||||
const palabraClave = document.getElementById('param1').value;
|
||||
const fechaInicio = document.getElementById('fecha_inicio').value;
|
||||
const fechaFin = document.getElementById('fecha_fin').value;
|
||||
const nodos = document.getElementById('nodos').value;
|
||||
const complejidad = document.getElementById('complejidad').value;
|
||||
|
||||
// Usamos 'complejidad' como porcentaje de similitud mínima (umbral)
|
||||
const paramsObj = {
|
||||
subtematica,
|
||||
palabraClave,
|
||||
fechaInicio,
|
||||
fechaFin,
|
||||
nodos,
|
||||
complejidad // enviado al backend y usado de umbral en cliente
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
|
||||
// Parsear complejidad a número
|
||||
const umbralPct = parseFloat(complejidad) || 0;
|
||||
console.log(`Aplicando umbral de similitud: ${umbralPct}%`);
|
||||
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
|
||||
// Filtrar enlaces por umbral de similitud
|
||||
let filteredLinks = graphData.links;
|
||||
if (umbralPct > 0) {
|
||||
filteredLinks = filteredLinks.filter(link => Number(link.value) >= umbralPct);
|
||||
console.log(`Enlaces tras aplicar umbral: ${filteredLinks.length}`);
|
||||
}
|
||||
|
||||
graph.graphData({ nodes: graphData.nodes, links: filteredLinks });
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
// Escuchar evento submit del formulario
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
fetchAndRender();
|
||||
});
|
||||
|
||||
// Cargar gráfico inicial
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Demografía y Sociedad</title>
|
||||
<link rel="stylesheet" href="popl-up.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
|
||||
<!-- Fuentes -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
|
|
@ -12,37 +13,114 @@
|
|||
|
||||
<!-- Librerías necesarias -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
<script src="https://unpkg.com/3d-force-graph"></script>
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Navegación -->
|
||||
<nav>
|
||||
<ul class="nav-links">
|
||||
<li><a href="popl-up.html" class="popl-up">Demografía y Sociedad</a></li>
|
||||
</ul>
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP</a>
|
||||
</div>
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('sobrepoblación');return false;">Sobrepoblación</a>
|
||||
<a href="#" onclick="setSubtema('enfermedades');return false;">COVID / Enfermedades</a>
|
||||
<a href="#" onclick="setSubtema('migraciones');return false;">Migraciones</a>
|
||||
<a href="#" onclick="setSubtema('urbanización');return false;">Urbanización</a>
|
||||
<a href="#" onclick="setSubtema('distribucion_edad');return false;">Despoblación Rural</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Contenedor principal -->
|
||||
<main>
|
||||
<div id="poplUpContainer" style="position: absolute; width: 100%; height: 100%; z-index: 0;"></div>
|
||||
|
||||
<!-- Fondo animado -->
|
||||
<div class="background">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<main class="split-screen">
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="poplUpContainer" style="position: absolute; width: 100%; height: 100%; z-index: 0;"></div>
|
||||
<!-- Fondo animado -->
|
||||
<div class="background">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
const fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</div>
|
||||
<div id="detailPanel">
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
const fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Barra lateral de filtros -->
|
||||
|
|
@ -56,16 +134,23 @@
|
|||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100">
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">Complejidad:</label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Búsqueda por palabra:</label>
|
||||
<input type="text" id="param1" name="param1">
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: migración, pandemia...">
|
||||
|
||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
||||
<input type="text" id="param2" name="param2">
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="enfermedades">enfermedades</option>
|
||||
<option value="urbanización">urbanización</option>
|
||||
<option value="migraciones">migraciones</option>
|
||||
<option value="sobrepoblación">sobrepoblación</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
|
|
@ -74,17 +159,7 @@
|
|||
<!-- Botón para colapsar la barra -->
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>
|
||||
<a href="#">GitHub</a> |
|
||||
<a href="#">Telegram</a> |
|
||||
<a href="#">Email</a> |
|
||||
<a href="#">Web de Tor</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<!-- Script principal -->
|
||||
<script src="output_popl_up_pruebas.js"></script>
|
||||
<script type="module" src="output_popl_up_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -42,19 +42,35 @@ header {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
position: absolute;
|
||||
top: 70%;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
right: 14px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.header-buttons--left {
|
||||
right: auto;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.stats-btn {
|
||||
border-color: #39ff14;
|
||||
color: #39ff14;
|
||||
}
|
||||
|
||||
.stats-btn:hover {
|
||||
background-color: #39ff14;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
margin-left: 5px;
|
||||
margin-right: 3px;
|
||||
|
|
@ -495,501 +511,192 @@ footer p {
|
|||
padding: 12px 20px;
|
||||
}
|
||||
}
|
||||
/* Para teléfonos de 768px de ancho en portrait */
|
||||
@media screen and (max-width: 768px) and (orientation: portrait) {
|
||||
.background a {
|
||||
width: 90%;
|
||||
}
|
||||
/* ============================================================
|
||||
MÓVIL — Portrait (≤ 1024px)
|
||||
============================================================ */
|
||||
|
||||
.button-column {
|
||||
width: 90%;
|
||||
gap: 10px;
|
||||
}
|
||||
@media screen and (max-width: 1024px) and (orientation: portrait) {
|
||||
header { height: auto; padding: 12px 10px; }
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.6rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.title { font-size: 3.2rem; }
|
||||
|
||||
.title {
|
||||
font-size: 3rem;
|
||||
.header-buttons {
|
||||
position: static;
|
||||
transform: none;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
.header-buttons--left { position: static; }
|
||||
|
||||
.small-button {
|
||||
font-size: 0.3rem;
|
||||
padding: 2px 2px;
|
||||
font-size: 0.72rem;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
/* Nav: fila horizontal scrollable de botones compactos */
|
||||
nav {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
padding: 6px 8px;
|
||||
overflow-x: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.nav-links {
|
||||
gap: 2em; /* Reduce el espacio entre los enlaces del nav */
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.nav-links li { flex-shrink: 0; }
|
||||
.nav-links a {
|
||||
width: auto;
|
||||
height: auto;
|
||||
font-size: 0.78rem;
|
||||
padding: 8px 14px;
|
||||
border-width: 2px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.8rem;
|
||||
padding: 8px 16px;
|
||||
/* Layout de imágenes: 2 columnas en lugar de 5 */
|
||||
.background {
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.background a {
|
||||
width: 50%;
|
||||
height: 30vw;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* Botones overlay: rejilla 2×3 */
|
||||
.button-overlay {
|
||||
position: static;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
padding: 14px 12px 60px;
|
||||
background: rgba(0,0,0,0.75);
|
||||
height: auto;
|
||||
top: auto;
|
||||
}
|
||||
.button-column {
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
.overlay-button {
|
||||
font-size: 0.75rem;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Para teléfonos de 576px de ancho en portrait */
|
||||
@media screen and (max-width: 576px) and (orientation: portrait) {
|
||||
.background a {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 95%;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.3rem;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 4rem;
|
||||
}
|
||||
/* ≤ 600px portrait */
|
||||
@media screen and (max-width: 600px) and (orientation: portrait) {
|
||||
.title { font-size: 2.2rem; }
|
||||
|
||||
.small-button {
|
||||
font-size: 0.3rem;
|
||||
font-size: 0.65rem;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.68rem;
|
||||
padding: 7px 11px;
|
||||
}
|
||||
|
||||
.background a {
|
||||
width: 50%;
|
||||
height: 28vw;
|
||||
}
|
||||
|
||||
.button-overlay {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
padding: 10px 8px 56px;
|
||||
}
|
||||
.overlay-button {
|
||||
font-size: 0.68rem;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ≤ 420px portrait */
|
||||
@media screen and (max-width: 420px) and (orientation: portrait) {
|
||||
.title { font-size: 1.8rem; }
|
||||
|
||||
.header-content { flex-direction: column; align-items: flex-start; gap: 6px; }
|
||||
|
||||
.small-button {
|
||||
font-size: 0.6rem;
|
||||
padding: 4px 7px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
gap: 1.5em;
|
||||
.nav-links a {
|
||||
font-size: 0.62rem;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.4rem;
|
||||
padding: 7px 14px;
|
||||
.background a {
|
||||
width: 50%;
|
||||
height: 26vw;
|
||||
}
|
||||
|
||||
.button-overlay {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
.overlay-button {
|
||||
font-size: 0.62rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Para teléfonos de 411px de ancho en portrait */
|
||||
@media screen and (max-width: 411px) and (orientation: portrait) {
|
||||
.background a {
|
||||
width: 100%;
|
||||
}
|
||||
/* ============================================================
|
||||
MÓVIL — Landscape (altura ≤ 500px)
|
||||
============================================================ */
|
||||
|
||||
.button-column {
|
||||
width: 100%;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.4rem;
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
@media screen and (max-width: 900px) and (orientation: landscape) and (max-height: 500px) {
|
||||
header { padding: 6px 10px; }
|
||||
.title { font-size: 1.6rem; }
|
||||
|
||||
.small-button {
|
||||
font-size: 0.3rem;
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.6rem;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Para teléfonos de 375px de ancho en portrait */
|
||||
@media screen and (max-width: 375px) and (orientation: portrait) {
|
||||
.background a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 100%;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.35rem;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.35rem;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.5rem;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Para teléfonos de 320px de ancho en portrait */
|
||||
@media screen and (max-width: 320px) and (orientation: portrait) {
|
||||
.background a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 100%;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.3rem;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.3rem;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.4rem;
|
||||
font-size: 0.65rem;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* For devices with smaller screens (e.g., 600px - 800px width) */
|
||||
@media screen and (min-width: 600px) and (max-width: 800px) and (max-height: 400px) and (orientation: landscape) {
|
||||
.background {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
nav { margin-top: 4px; padding: 4px 8px; overflow-x: auto; justify-content: flex-start; }
|
||||
.nav-links { flex-wrap: nowrap; gap: 6px; }
|
||||
.nav-links a {
|
||||
width: auto; height: auto;
|
||||
font-size: 0.65rem;
|
||||
padding: 6px 10px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.background { height: 100vh; flex-wrap: nowrap; overflow-x: auto; }
|
||||
.background a { width: 20%; height: 100%; flex-shrink: 0; }
|
||||
|
||||
.button-overlay {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
padding-bottom: 0px;
|
||||
align-items: center;
|
||||
top: 60px;
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 15%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.button-column { width: 18%; gap: 4px; }
|
||||
.overlay-button {
|
||||
font-size: 0.3rem;
|
||||
padding: 2px 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.3rem;
|
||||
padding: 2px 2px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.3rem; /* Reduced font size */
|
||||
padding: 15px 30px; /* Adjusted padding */
|
||||
font-size: 0.6rem;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* For medium-sized devices (e.g., 800px - 1000px width) */
|
||||
@media screen and (min-width: 800px) and (max-width: 1000px) and (max-height: 450px) and (orientation: landscape) {
|
||||
.background {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.button-overlay {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 16%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.35rem;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.35rem;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.3rem; /* Reduced font size */
|
||||
padding: 15px 30px; /* Adjusted padding */
|
||||
}
|
||||
}
|
||||
|
||||
/* For larger devices (e.g., 1000px - 1200px width) */
|
||||
@media screen and (min-width: 1000px) and (max-width: 1200px) and (max-height: 500px) and (orientation: landscape) {
|
||||
.background {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.button-overlay {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 17%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.4rem;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.35rem;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.3rem; /* Reduced font size */
|
||||
padding: 15px 30px; /* Adjusted padding */
|
||||
}
|
||||
}
|
||||
|
||||
/* For even larger devices (e.g., 1200px - 1500px width) */
|
||||
@media screen and (min-width: 1200px) and (max-width: 1500px) and (max-height: 700px) and (orientation: landscape) {
|
||||
.background {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.button-overlay {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 18%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.45rem;
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.35rem;
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.3rem; /* Reduced font size */
|
||||
padding: 15px 30px; /* Adjusted padding */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 800px) and (orientation: landscape) {
|
||||
.background a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 20%;
|
||||
gap: 5px;
|
||||
margin-top: 40px; /* Push buttons down to avoid navbar */
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.35rem;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.35rem;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.35rem;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* For devices with width around 700px */
|
||||
@media screen and (max-width: 700px) and (orientation: landscape) {
|
||||
.background a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 25%;
|
||||
gap: 5px;
|
||||
margin-top: 50px; /* Further adjustment */
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.3rem;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.3rem;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.3rem;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* For smaller devices with width around 600px */
|
||||
@media screen and (max-width: 600px) and (orientation: landscape) {
|
||||
.background a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 30%;
|
||||
gap: 5px;
|
||||
margin-top: 60px; /* Ensures the buttons are well below the navbar */
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.4rem;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.5rem;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.5rem;
|
||||
padding: 3px 5px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Landscape mode for screens up to 480px wide */
|
||||
@media screen and (max-width: 480px) and (orientation: landscape) {
|
||||
.button-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button-column {
|
||||
width: 28%;
|
||||
gap: 2px;
|
||||
margin-top:150px;
|
||||
}
|
||||
|
||||
.overlay-button {
|
||||
font-size: 0.45rem;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
font-size: 0.45rem;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.45rem;
|
||||
padding: 3px 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
/* landscape muy pequeño (≤ 600px ancho) */
|
||||
@media screen and (max-width: 600px) and (orientation: landscape) and (max-height: 400px) {
|
||||
.title { font-size: 1.2rem; }
|
||||
.small-button { font-size: 0.6rem; padding: 3px 6px; }
|
||||
.nav-links a { font-size: 0.6rem; padding: 5px 8px; }
|
||||
.button-column { width: 28%; }
|
||||
.overlay-button { font-size: 0.58rem; padding: 4px 6px; }
|
||||
}
|
||||
|
||||
|
|
|
|||
278
FLUJOS/VISUALIZACION/public/sub-nav.css
Normal file
278
FLUJOS/VISUALIZACION/public/sub-nav.css
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
/* ============================================================
|
||||
SUB-NAV — Barra de navegación compacta, estética = home
|
||||
============================================================ */
|
||||
|
||||
/* ── Contenedor fijo ─────────────────────────────────────── */
|
||||
.top-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transform: translateZ(0); /* capa GPU propia — sobre el canvas WebGL */
|
||||
z-index: 9999;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
nav.top-nav {
|
||||
display: block !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
justify-content: unset;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* ── Fila centrada ───────────────────────────────────────── */
|
||||
.section-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
width: 100%;
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
/* Sin overflow en desktop → dropdown no queda recortado en Y */
|
||||
}
|
||||
|
||||
/* ── Wrapper ─────────────────────────────────────────────── */
|
||||
.section-item {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Botón — texto NEGRO sobre fondo de sección ──────────── */
|
||||
.section-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
/* Desktop grande — mismo "peso visual" que el home */
|
||||
padding: 14px 34px;
|
||||
font-size: 1.05em;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
color: #000000; /* letra negra — legible sobre cualquier fondo */
|
||||
border: 3px solid black; /* reborde negro siempre */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: color 0.35s ease-in, transform 0.3s ease, box-shadow 0.3s ease;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
/* mismo text-shadow del home para profundidad */
|
||||
text-shadow: 1px 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.section-btn:hover {
|
||||
transform: scale(1.06);
|
||||
border: 3px solid black;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
font-size: 0.75em;
|
||||
line-height: 1;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
/* ── Fondos por sección — exactos al home ────────────────── */
|
||||
/* Solo background y border-color; el texto queda en #000000 */
|
||||
.glob-war-btn { background-color: red; }
|
||||
.int-sec-btn { background-color: darkblue; color: #aad4ff; } /* azul oscuro → letra clara */
|
||||
.climate-btn { background-color: lightgreen; }
|
||||
.eco-corp-btn { background-color: yellow; }
|
||||
.popl-up-btn { background-color: orange; }
|
||||
|
||||
/* ── Hover — colores neón iguales al home ────────────────── */
|
||||
.glob-war-btn:hover { color: #39ff14; }
|
||||
.int-sec-btn:hover { color: #ff69b4; }
|
||||
.climate-btn:hover { color: #ff4500; }
|
||||
.eco-corp-btn:hover { color: #00fff2; }
|
||||
.popl-up-btn:hover { color: #0066ff; }
|
||||
|
||||
/* ── Botón activo: borde brillante ──────────────────────── */
|
||||
.section-item.current .section-btn {
|
||||
box-shadow: 0 0 12px 3px rgba(0,0,0,0.5), 0 0 0 2px rgba(255,255,255,0.4);
|
||||
}
|
||||
.section-item.current:hover .dropdown-arrow,
|
||||
.section-item.current.open .dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* ── Dropdown ────────────────────────────────────────────── */
|
||||
.section-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateZ(0);
|
||||
min-width: 200px;
|
||||
background: rgba(4, 4, 4, 0.98);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
padding: 6px 0;
|
||||
z-index: 99999;
|
||||
box-shadow: 0 10px 32px rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
/* Puente para no perder hover al moverse al dropdown */
|
||||
.section-item.current::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: -10px;
|
||||
right: -10px;
|
||||
height: 14px;
|
||||
z-index: 99998;
|
||||
}
|
||||
|
||||
.section-item.current:hover .section-dropdown,
|
||||
.section-item.current.open .section-dropdown {
|
||||
display: block;
|
||||
animation: dropFade 0.14s ease;
|
||||
}
|
||||
|
||||
@keyframes dropFade {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(-5px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
.section-dropdown a {
|
||||
display: block;
|
||||
padding: 8px 18px;
|
||||
font-size: 0.75em;
|
||||
font-family: 'Fira Code', monospace;
|
||||
color: #ccc;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.section-dropdown a:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
MÓVIL — Panel de detalle en la MITAD INFERIOR (≤ 768px)
|
||||
Sustituye el "display:none" del graphPanel por layout vertical
|
||||
================================================================ */
|
||||
@media (max-width: 768px) {
|
||||
/* Columna vertical: grafo arriba, detalle abajo */
|
||||
main.split-screen.show-detail {
|
||||
flex-direction: column !important;
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
/* Grafo ocupa la mitad superior — NO se oculta */
|
||||
main.split-screen.show-detail #graphPanel {
|
||||
display: block !important;
|
||||
flex: none !important;
|
||||
width: 100% !important;
|
||||
height: 50vh !important;
|
||||
min-height: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Panel de detalle ocupa la mitad inferior */
|
||||
main.split-screen.show-detail #detailPanel {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
flex: none !important;
|
||||
height: 50vh !important;
|
||||
overflow-y: auto;
|
||||
border-top: 2px solid rgba(57, 255, 20, 0.4);
|
||||
}
|
||||
|
||||
/* El contenido del panel se muestra siempre cuando está abierto */
|
||||
main.split-screen.show-detail #detailContent {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Detail image — larger display ──────────────────────── */
|
||||
.detail-img {
|
||||
width: 100%;
|
||||
max-height: 28vh;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* ── Ver conexiones button ───────────────────────────────── */
|
||||
#verConexionesBtn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
padding: 10px 0;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border: 1px solid rgba(255,255,255,0.25);
|
||||
border-radius: 5px;
|
||||
color: #aaffaa;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.82em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
#verConexionesBtn:hover {
|
||||
background: rgba(255,255,255,0.18);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Volver al grafo completo — floating overlay ─────────── */
|
||||
#volverGrafoBtn {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9990;
|
||||
padding: 7px 20px;
|
||||
background: rgba(0,0,0,0.82);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.82em;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#volverGrafoBtn.visible { display: block; }
|
||||
|
||||
/* ── Móvil — nav más pequeño ≤ 800px ────────────────────── */
|
||||
@media (max-width: 800px) {
|
||||
.section-buttons {
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
gap: 8px;
|
||||
}
|
||||
.section-buttons::-webkit-scrollbar { display: none; }
|
||||
|
||||
.section-btn {
|
||||
padding: 7px 14px;
|
||||
font-size: 0.72em;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
/* En móvil con overflow-x el dropdown escapa al contenedor → fixed */
|
||||
.section-dropdown {
|
||||
position: fixed !important;
|
||||
left: 50% !important;
|
||||
top: 52px !important;
|
||||
transform: translateX(-50%) translateZ(0) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.section-btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 0.65em;
|
||||
}
|
||||
.section-dropdown { min-width: 160px; }
|
||||
.section-dropdown a { font-size: 0.7em; padding: 7px 14px; }
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ from pathlib import Path
|
|||
|
||||
import torch
|
||||
from PIL import Image
|
||||
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
|
||||
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor, BitsAndBytesConfig
|
||||
|
||||
# ── Configuración ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -33,10 +33,9 @@ CACHE_DIR = os.getenv("HF_HOME", "/var/www/theflows.net/flujos/FLUJOS_DATOS/IMAG
|
|||
|
||||
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
|
||||
|
||||
# RAM por imagen en batch (aprox): ~500MB activaciones encoder
|
||||
# Modelo base bfloat16: ~16GB
|
||||
# Batch de 4: ~18GB total → seguro con 64GB
|
||||
DEFAULT_BATCH_SIZE = 4
|
||||
# int4 via bitsandbytes: modelo ocupa ~4-5GB VRAM en lugar de ~16GB bfloat16
|
||||
# RTX 3060 12GB → sobra VRAM para activaciones
|
||||
DEFAULT_BATCH_SIZE = 1 # batch 1 para seguridad con 12GB
|
||||
|
||||
KEYWORD_PROMPT = """Analiza esta imagen en detalle.
|
||||
Devuelve ÚNICAMENTE un objeto JSON válido con esta estructura exacta, sin texto adicional:
|
||||
|
|
@ -73,17 +72,35 @@ class ImageAnalyzer:
|
|||
print(f"[ImageAnalyzer] Cargando modelo {self.model_id}...")
|
||||
print(f"[ImageAnalyzer] Cache: {CACHE_DIR}")
|
||||
|
||||
self._model = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||
self.model_id,
|
||||
torch_dtype=torch.bfloat16,
|
||||
device_map="cpu",
|
||||
cache_dir=CACHE_DIR,
|
||||
)
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
print(f"[ImageAnalyzer] Dispositivo: {device}")
|
||||
|
||||
if device == "cuda":
|
||||
bnb_config = BitsAndBytesConfig(
|
||||
load_in_4bit=True,
|
||||
bnb_4bit_compute_dtype=torch.bfloat16,
|
||||
bnb_4bit_use_double_quant=True,
|
||||
bnb_4bit_quant_type="nf4",
|
||||
)
|
||||
self._model = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||
self.model_id,
|
||||
quantization_config=bnb_config,
|
||||
device_map="auto",
|
||||
cache_dir=CACHE_DIR,
|
||||
)
|
||||
else:
|
||||
self._model = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||
self.model_id,
|
||||
torch_dtype=torch.bfloat16,
|
||||
device_map="cpu",
|
||||
cache_dir=CACHE_DIR,
|
||||
)
|
||||
|
||||
self._processor = AutoProcessor.from_pretrained(
|
||||
self.model_id,
|
||||
cache_dir=CACHE_DIR,
|
||||
)
|
||||
print("[ImageAnalyzer] Modelo cargado.")
|
||||
print("[ImageAnalyzer] Modelo cargado (int4 cuantizado).")
|
||||
|
||||
# ── Opción 3: Resume — obtener archivos ya analizados en MongoDB ───────────
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue