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({
|
helmet.contentSecurityPolicy({
|
||||||
directives: {
|
directives: {
|
||||||
defaultSrc: ["'self'"],
|
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'],
|
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
||||||
imgSrc: ["'self'", 'data:', 'blob:'],
|
imgSrc: ["'self'", 'data:', 'blob:'],
|
||||||
connectSrc: ["'self'"],
|
connectSrc: ["'self'", 'https://esm.sh'],
|
||||||
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
|
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
|
||||||
frameAncestors: ["'none'"],
|
frameAncestors: ["'none'"],
|
||||||
objectSrc: ["'none'"],
|
objectSrc: ["'none'"],
|
||||||
|
|
@ -150,10 +150,8 @@ app.get('/api/data', async (req, res) => {
|
||||||
if (subtematica) nodesQuery.subtema = subtematica;
|
if (subtematica) nodesQuery.subtema = subtematica;
|
||||||
if (regexKw) nodesQuery.texto = { $regex: regexKw, $options: 'i' };
|
if (regexKw) nodesQuery.texto = { $regex: regexKw, $options: 'i' };
|
||||||
if (fechaGte) nodesQuery.fecha = { $gte: fechaGte, $lte: fechaLte };
|
if (fechaGte) nodesQuery.fecha = { $gte: fechaGte, $lte: fechaLte };
|
||||||
// Ejecutar la consulta y obtener los resultados de las colecciones
|
// Imágenes: proporcional a nodosLimit (1/3), mínimo 3 para siempre mostrar algo
|
||||||
console.log('Ejecutando consulta para nodos en colecciones...');
|
const imagenesLimit = Math.max(3, Math.floor(nodosLimit / 3));
|
||||||
// Límite de imágenes: un tercio del total para no saturar el grafo
|
|
||||||
const imagenesLimit = Math.floor(nodosLimit / 3);
|
|
||||||
|
|
||||||
const [wikipediaNodes, noticiasNodes, torrentsNodes, imagenesNodes, imagenesWikiNodes] = await Promise.all([
|
const [wikipediaNodes, noticiasNodes, torrentsNodes, imagenesNodes, imagenesWikiNodes] = await Promise.all([
|
||||||
wikipediaCollection.find(nodesQuery).limit(nodosLimit).toArray(),
|
wikipediaCollection.find(nodesQuery).limit(nodosLimit).toArray(),
|
||||||
|
|
@ -162,15 +160,11 @@ app.get('/api/data', async (req, res) => {
|
||||||
imagenesCollection.find(nodesQuery).limit(imagenesLimit).toArray(),
|
imagenesCollection.find(nodesQuery).limit(imagenesLimit).toArray(),
|
||||||
imagenesWikiCollection.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
|
// Preferir imagenes analizadas (con keywords); si no hay, usar imagenes_wiki
|
||||||
const imgNodes = imagenesNodes.length > 0 ? imagenesNodes : imagenesWikiNodes;
|
const imgNodes = imagenesNodes.length > 0 ? imagenesNodes : imagenesWikiNodes;
|
||||||
|
|
||||||
const nodes = [...wikipediaNodes, ...noticiasNodes, ...torrentsNodes];
|
const nodes = [...wikipediaNodes, ...noticiasNodes, ...torrentsNodes];
|
||||||
console.log('Total nodos texto:', nodes.length, '| Nodos imagen:', imgNodes.length);
|
|
||||||
|
|
||||||
// Nodos de texto
|
// Nodos de texto
|
||||||
const formattedNodes = nodes.map((result) => ({
|
const formattedNodes = nodes.map((result) => ({
|
||||||
|
|
@ -179,21 +173,28 @@ app.get('/api/data', async (req, res) => {
|
||||||
tema: result.tema || 'sin tema',
|
tema: result.tema || 'sin tema',
|
||||||
content: result.texto || '',
|
content: result.texto || '',
|
||||||
fecha: result.fecha || '',
|
fecha: result.fecha || '',
|
||||||
|
fuente: result.fuente || result.autor || result.source || '',
|
||||||
type: 'texto',
|
type: 'texto',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Nodos de imagen — image_url construida desde image_path absoluto
|
// 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 WIKI_IMAGES_BASE = '/var/www/theflows.net/flujos/FLUJOS_DATOS/IMAGENES/output/wiki_images/';
|
||||||
const imgFormattedNodes = imgNodes.map((result) => {
|
const imgFormattedNodes = imgNodes.map((result) => {
|
||||||
const relativePath = result.image_path
|
let relativePath = null;
|
||||||
? result.image_path.replace(WIKI_IMAGES_BASE, '')
|
if (result.image_path && result.image_path.startsWith(WIKI_IMAGES_BASE)) {
|
||||||
: null;
|
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 {
|
return {
|
||||||
id: result.archivo.trim(),
|
id: result.archivo.trim(),
|
||||||
group: result.subtema || result.tema || 'imagen',
|
group: result.subtema || result.tema || 'imagen',
|
||||||
tema: result.tema || 'sin tema',
|
tema: result.tema || 'sin tema',
|
||||||
content: result.texto || result.descripcion_wiki || '',
|
content: result.texto || result.descripcion_wiki || '',
|
||||||
fecha: result.fecha || '',
|
fecha: result.fecha || '',
|
||||||
|
fuente: result.fuente || result.autor || result.source || '',
|
||||||
type: 'imagen',
|
type: 'imagen',
|
||||||
image_url: relativePath ? `/wiki-images/${relativePath}` : null,
|
image_url: relativePath ? `/wiki-images/${relativePath}` : null,
|
||||||
label: result.subtema || result.tema || result.archivo,
|
label: result.subtema || result.tema || result.archivo,
|
||||||
|
|
@ -201,47 +202,186 @@ app.get('/api/data', async (req, res) => {
|
||||||
}).filter(n => n.image_url);
|
}).filter(n => n.image_url);
|
||||||
|
|
||||||
const allNodes = [...formattedNodes, ...imgFormattedNodes];
|
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);
|
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 = {
|
const linksQuery = {
|
||||||
porcentaje_similitud: { $gte: porcentajeSimilitudMin },
|
|
||||||
noticia1: { $in: nodeIds },
|
noticia1: { $in: nodeIds },
|
||||||
noticia2: { $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();
|
const links = await comparacionesCollection.find(linksQuery).toArray();
|
||||||
console.log('Enlaces obtenidos:', links.length);
|
|
||||||
|
|
||||||
// Formatear los enlaces sin normalizar los IDs
|
// Limitar links por nodo: cada nodo muestra solo sus top MAX conexiones más fuertes.
|
||||||
const formattedLinks = links.map((result) => ({
|
// 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(),
|
source: result.noticia1.trim(),
|
||||||
target: result.noticia2.trim(),
|
target: result.noticia2.trim(),
|
||||||
value: result.porcentaje_similitud,
|
value: result.porcentaje_similitud,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Enviar los datos al cliente en formato JSON
|
|
||||||
res.json({ nodes: formattedNodes_final, links: formattedLinks });
|
res.json({ nodes: formattedNodes_final, links: formattedLinks });
|
||||||
console.log('Datos enviados al cliente');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al obtener datos:', error);
|
console.error('Error al obtener datos:', error);
|
||||||
res.status(500).json({ error: 'Error al obtener datos' });
|
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**
|
// **Luego, definir las rutas de las páginas principales**
|
||||||
// Rutas para las páginas principales
|
// Rutas para las páginas principales
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/index.html'));
|
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) => {
|
app.get('/climate.html', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/climate.html'));
|
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/climate.html'));
|
||||||
});
|
});
|
||||||
|
|
@ -287,6 +427,7 @@ async function connectToMongoDB() {
|
||||||
await mongoClient.connect();
|
await mongoClient.connect();
|
||||||
db = mongoClient.db(dbName);
|
db = mongoClient.db(dbName);
|
||||||
console.log('Conectado a MongoDB');
|
console.log('Conectado a MongoDB');
|
||||||
|
initStats();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al conectar a MongoDB:', error);
|
console.error('Error al conectar a MongoDB:', error);
|
||||||
process.exit(1); // Salir de la aplicación si no se puede conectar
|
process.exit(1); // Salir de la aplicación si no se puede conectar
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,85 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>CLIMATE</title>
|
<title>CLIMATE</title>
|
||||||
<link rel="stylesheet" href="climate.css">
|
<link rel="stylesheet" href="climate.css">
|
||||||
|
<link rel="stylesheet" href="sub-nav.css">
|
||||||
<!-- Fuentes de Google Fonts -->
|
<!-- Fuentes de Google Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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://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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav>
|
<nav class="top-nav">
|
||||||
<ul class="nav-links">
|
<div class="section-buttons">
|
||||||
<li><a href="climate.html" class="climate">CLIMATE</a></li>
|
|
||||||
</ul>
|
<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>
|
</nav>
|
||||||
|
|
||||||
<main>
|
<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 id="climateContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||||
<div class="background">
|
<div class="background">
|
||||||
<img src="/images/flujos6.jpg">
|
<img src="/images/flujos6.jpg">
|
||||||
|
|
@ -39,8 +98,26 @@
|
||||||
}, 1000);
|
}, 1000);
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</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>
|
</main>
|
||||||
|
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
|
|
@ -54,22 +131,23 @@
|
||||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||||
|
|
||||||
<label for="nodos">Nodos:</label>
|
<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>
|
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
<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>
|
<label for="param1">Palabra clave:</label>
|
||||||
<input type="text" id="param1" name="param1">
|
<input type="text" id="param1" name="param1" placeholder="ej: glaciares, CO2...">
|
||||||
|
|
||||||
<label for="color1">Color 1:</label>
|
<label for="param2">Subtematica:</label>
|
||||||
<input type="color" id="color1" name="color1">
|
<select id="param2" name="param2">
|
||||||
|
<option value="">— Todas —</option>
|
||||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
<option value="conservación">conservación</option>
|
||||||
<input type="text" id="param2" name="param2">
|
<option value="cambio climático">cambio climático</option>
|
||||||
|
<option value="energía renovable">energía renovable</option>
|
||||||
<label for="color2">Color 2:</label>
|
<option value="desastres naturales">desastres naturales</option>
|
||||||
<input type="color" id="color2" name="color2">
|
</select>
|
||||||
|
|
||||||
<input type="submit" value="Aplicar">
|
<input type="submit" value="Aplicar">
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -77,11 +155,7 @@
|
||||||
|
|
||||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
<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 -->
|
<!-- 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Economía y Corporaciones</title>
|
<title>Economía y Corporaciones</title>
|
||||||
<link rel="stylesheet" href="eco-corp.css">
|
<link rel="stylesheet" href="eco-corp.css">
|
||||||
|
<link rel="stylesheet" href="sub-nav.css">
|
||||||
<!-- Fuentes de Google Fonts -->
|
<!-- Fuentes de Google Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|
@ -11,18 +12,77 @@
|
||||||
|
|
||||||
<!-- Cargar D3.js -->
|
<!-- Cargar D3.js -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||||
<!-- Cargar 3d-force-graph (incluye Three.js) -->
|
<script type="importmap">
|
||||||
<script src="https://unpkg.com/3d-force-graph"></script>
|
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav>
|
<nav class="top-nav">
|
||||||
<ul class="nav-links">
|
<div class="section-buttons">
|
||||||
<li><a href="eco-corp.html" class="eco-corp">Economía y Corporaciones</a></li>
|
|
||||||
</ul>
|
<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>
|
</nav>
|
||||||
|
|
||||||
<main>
|
<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 id="ecoCorpContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||||
<div class="background">
|
<div class="background">
|
||||||
<img src="/images/flujos.jpg">
|
<img src="/images/flujos.jpg">
|
||||||
|
|
@ -38,6 +98,26 @@
|
||||||
}, 1000);
|
}, 1000);
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</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>
|
</main>
|
||||||
|
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
|
|
@ -50,16 +130,24 @@
|
||||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||||
|
|
||||||
<label for="nodos">Nodos:</label>
|
<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>
|
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
<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>
|
<label for="param1">Palabra clave:</label>
|
||||||
<input type="text" id="param1" name="param1">
|
<input type="text" id="param1" name="param1" placeholder="ej: FMI, Amazon...">
|
||||||
|
|
||||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
<label for="param2">Subtematica:</label>
|
||||||
<input type="text" id="param2" name="param2">
|
<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">
|
<input type="submit" value="Aplicar">
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -67,11 +155,7 @@
|
||||||
|
|
||||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
<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 -->
|
<!-- 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -4,27 +4,83 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Glob-War</title>
|
<title>Glob-War</title>
|
||||||
<link rel="stylesheet" href="glob-war.css">
|
<link rel="stylesheet" href="glob-war.css">
|
||||||
|
<link rel="stylesheet" href="sub-nav.css">
|
||||||
<!-- Fuentes de Google Fonts -->
|
<!-- Fuentes de Google Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav>
|
<nav class="top-nav">
|
||||||
<ul class="nav-links">
|
<div class="section-buttons">
|
||||||
<li><a href="glob-war.html" class="glob-war">GLOB-WAR</a></li>
|
|
||||||
</ul>
|
<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>
|
</nav>
|
||||||
|
|
||||||
<main>
|
<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 id="globWarContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||||
<div class="background">
|
<div class="background">
|
||||||
<img src="/images/flujos.jpg">
|
<img src="/images/flujos.jpg">
|
||||||
|
|
@ -40,6 +96,26 @@
|
||||||
}, 1000);
|
}, 1000);
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</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>
|
</main>
|
||||||
|
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
|
|
@ -52,22 +128,24 @@
|
||||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||||
|
|
||||||
<label for="nodos">Nodos:</label>
|
<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>
|
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
<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>
|
<label for="param1">Palabra clave:</label>
|
||||||
<input type="text" id="param1" name="param1">
|
<input type="text" id="param1" name="param1" placeholder="ej: misiles, OTAN...">
|
||||||
|
|
||||||
<label for="color1">Color 1:</label>
|
<label for="param2">Subtematica:</label>
|
||||||
<input type="color" id="color1" name="color1">
|
<select id="param2" name="param2">
|
||||||
|
<option value="">— Todas —</option>
|
||||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
<option value="armas">armas</option>
|
||||||
<input type="text" id="param2" name="param2">
|
<option value="terrorismo">terrorismo</option>
|
||||||
|
<option value="guerras civiles">guerras civiles</option>
|
||||||
<label for="color2">Color 2:</label>
|
<option value="conflictos internacionales">conflictos internacionales</option>
|
||||||
<input type="color" id="color2" name="color2">
|
<option value="alianzas militares">alianzas militares</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<input type="submit" value="Aplicar">
|
<input type="submit" value="Aplicar">
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -75,11 +153,7 @@
|
||||||
|
|
||||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
<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 -->
|
<!-- 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@
|
||||||
<header>
|
<header>
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1 class="title">UP-LEAKS</h1>
|
<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="journalist.html" class="small-button">Journalist</a>
|
||||||
|
<a href="stats.html" class="small-button stats-btn">STATS</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Inteligencia y Seguridad</title>
|
<title>Inteligencia y Seguridad</title>
|
||||||
<link rel="stylesheet" href="int-sec.css">
|
<link rel="stylesheet" href="int-sec.css">
|
||||||
|
<link rel="stylesheet" href="sub-nav.css">
|
||||||
<!-- Fuentes de Google Fonts -->
|
<!-- Fuentes de Google Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|
@ -11,19 +12,77 @@
|
||||||
|
|
||||||
<!-- Cargar D3.js -->
|
<!-- Cargar D3.js -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||||
<!-- Cargar 3d-force-graph (incluye Three.js) -->
|
<script type="importmap">
|
||||||
<script src="https://unpkg.com/3d-force-graph"></script>
|
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav class="top-nav">
|
||||||
<ul class="nav-links">
|
<div class="section-buttons">
|
||||||
<li><a href="int-sec.html" class="int-sec">Inteligencia y Seguridad</a></li>
|
|
||||||
</ul>
|
<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>
|
</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">
|
<main class="split-screen">
|
||||||
<!-- PANEL IZQUIERDO: el grafo -->
|
<!-- 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 id="intSecContainer"></div>
|
||||||
<div class="background">
|
<div class="background">
|
||||||
<img src="/images/flujos.jpg">
|
<img src="/images/flujos.jpg">
|
||||||
|
|
@ -43,7 +102,23 @@
|
||||||
|
|
||||||
<!-- PANEL DERECHO: detalle de la noticia -->
|
<!-- PANEL DERECHO: detalle de la noticia -->
|
||||||
<div id="detailPanel">
|
<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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
@ -57,16 +132,24 @@
|
||||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||||
|
|
||||||
<label for="nodos">Nodos:</label>
|
<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>
|
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
<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>
|
<label for="param1">Palabra clave:</label>
|
||||||
<input type="text" id="param1" name="param1">
|
<input type="text" id="param1" name="param1" placeholder="ej: CIA, GCHQ...">
|
||||||
|
|
||||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
<label for="param2">Subtematica:</label>
|
||||||
<input type="text" id="param2" name="param2">
|
<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">
|
<input type="submit" value="Aplicar">
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -74,16 +157,7 @@
|
||||||
|
|
||||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
<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 -->
|
<!-- Incluir tu script al final del body -->
|
||||||
<script src="output_int_sec.js"></script>
|
<script type="module" src="output_int_sec.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,50 @@
|
||||||
// output_climate_pruebas.js
|
// 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const elem = document.getElementById('climateContainer');
|
const elem = document.getElementById('climateContainer');
|
||||||
const form = document.getElementById('paramForm');
|
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)
|
const graph = ForceGraph3D()(elem)
|
||||||
.backgroundColor('#000000')
|
.backgroundColor('#000000')
|
||||||
.nodeLabel('id')
|
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||||
.nodeAutoColorBy('group')
|
.nodeAutoColorBy('group')
|
||||||
.nodeVal(1)
|
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||||
.linkColor(() => '#42FF00')
|
.linkColor(() => '#42FF00')
|
||||||
.onNodeClick(node => showNodeContent(node.content))
|
.onNodeClick(node => showNodeDetail(node))
|
||||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||||
.forceEngine('d3')
|
.nodeThreeObject(node => {
|
||||||
//.d3Force('charge', d3.forceManyBody().strength(-300))
|
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||||
//.d3Force('link', d3.forceLink().distance(300).strength(1));
|
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() {
|
function centerGraph() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const width = elem.clientWidth;
|
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||||
const height = elem.clientHeight;
|
|
||||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
|
|
@ -32,95 +53,175 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
centerGraph();
|
centerGraph();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mostrar contenido del nodo
|
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||||
function showNodeContent(content) {
|
function formatDate(fecha) {
|
||||||
console.log('Contenido del nodo:', content);
|
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 = {}) {
|
async function getData(paramsObj = {}) {
|
||||||
try {
|
try {
|
||||||
let url = '/api/data';
|
let url = '/api/data';
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
// Establecer el tema fijo
|
|
||||||
params.append('tema', 'cambio climático');
|
params.append('tema', 'cambio climático');
|
||||||
|
|
||||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
|
||||||
for (const key in paramsObj) {
|
for (const key in paramsObj) {
|
||||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||||
params.append(key, paramsObj[key]);
|
params.append(key, paramsObj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
url += `?${params.toString()}`;
|
url += `?${params.toString()}`;
|
||||||
console.log('🔎 Fetch URL:', url);
|
console.log('Fetch URL:', url);
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error(response.statusText);
|
if (!response.ok) throw new Error(response.statusText);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Datos recibidos del servidor:', data);
|
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||||
|
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||||
// 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));
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al obtener datos del servidor:', error);
|
console.error('Error al obtener datos:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
|
||||||
async function fetchAndRender() {
|
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 = {
|
const paramsObj = {
|
||||||
subtematica,
|
subtematica: document.getElementById('param2').value,
|
||||||
palabraClave,
|
palabraClave: document.getElementById('param1').value,
|
||||||
fechaInicio,
|
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||||
fechaFin,
|
fechaFin: document.getElementById('fecha_fin').value,
|
||||||
nodos,
|
nodos: document.getElementById('nodos').value,
|
||||||
complejidad // enviado al backend y usado de umbral en cliente
|
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);
|
const graphData = await getData(paramsObj);
|
||||||
if (!graphData) return;
|
if (!graphData) return;
|
||||||
|
lastGraphData = graphData;
|
||||||
// Filtrar enlaces por umbral de similitud
|
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||||
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 });
|
|
||||||
centerGraph();
|
centerGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escuchar evento submit del formulario
|
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||||
form.addEventListener('submit', event => {
|
|
||||||
event.preventDefault();
|
|
||||||
fetchAndRender();
|
fetchAndRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cargar gráfico inicial
|
|
||||||
fetchAndRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,50 @@
|
||||||
// output_eco_corp.js
|
// 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const elem = document.getElementById('ecoCorpContainer');
|
const elem = document.getElementById('ecoCorpContainer');
|
||||||
const form = document.getElementById('paramForm');
|
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)
|
const graph = ForceGraph3D()(elem)
|
||||||
.backgroundColor('#000000')
|
.backgroundColor('#000000')
|
||||||
.nodeLabel('id')
|
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||||
.nodeAutoColorBy('group')
|
.nodeAutoColorBy('group')
|
||||||
.nodeVal(1)
|
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||||
.linkColor(() => 'yellow')
|
.linkColor(() => 'yellow')
|
||||||
.onNodeClick(node => showNodeContent(node.content))
|
.onNodeClick(node => showNodeDetail(node))
|
||||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||||
.forceEngine('d3')
|
.nodeThreeObject(node => {
|
||||||
//.d3Force('charge', d3.forceManyBody().strength(-300))
|
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||||
//.d3Force('link', d3.forceLink().distance(300).strength(1));
|
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() {
|
function centerGraph() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const width = elem.clientWidth;
|
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||||
const height = elem.clientHeight;
|
|
||||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
|
|
@ -32,89 +53,175 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
centerGraph();
|
centerGraph();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mostrar contenido del nodo
|
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||||
function showNodeContent(content) {
|
function formatDate(fecha) {
|
||||||
console.log('Contenido del nodo:', content);
|
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 = {}) {
|
async function getData(paramsObj = {}) {
|
||||||
try {
|
try {
|
||||||
let url = '/api/data';
|
let url = '/api/data';
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
// Establecer el tema fijo
|
|
||||||
params.append('tema', 'economía y corporaciones');
|
params.append('tema', 'economía y corporaciones');
|
||||||
|
|
||||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
|
||||||
for (const key in paramsObj) {
|
for (const key in paramsObj) {
|
||||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||||
params.append(key, paramsObj[key]);
|
params.append(key, paramsObj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
url += `?${params.toString()}`;
|
url += `?${params.toString()}`;
|
||||||
console.log('🔎 Fetch URL:', url);
|
console.log('Fetch URL:', url);
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error(response.statusText);
|
if (!response.ok) throw new Error(response.statusText);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Datos recibidos del servidor:', data);
|
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||||
|
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||||
// 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));
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al obtener datos del servidor:', error);
|
console.error('Error al obtener datos:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
|
||||||
async function fetchAndRender() {
|
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 = {
|
const paramsObj = {
|
||||||
subtematica,
|
subtematica: document.getElementById('param2').value,
|
||||||
palabraClave,
|
palabraClave: document.getElementById('param1').value,
|
||||||
fechaInicio,
|
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||||
fechaFin,
|
fechaFin: document.getElementById('fecha_fin').value,
|
||||||
nodos,
|
nodos: document.getElementById('nodos').value,
|
||||||
complejidad // enviado al backend y usado de umbral en cliente
|
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);
|
const graphData = await getData(paramsObj);
|
||||||
if (!graphData) return;
|
if (!graphData) return;
|
||||||
|
lastGraphData = graphData;
|
||||||
// Filtrar enlaces por umbral de similitud
|
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||||
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 });
|
|
||||||
centerGraph();
|
centerGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escuchar evento submit del formulario
|
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||||
form.addEventListener('submit', event => {
|
|
||||||
event.preventDefault();
|
|
||||||
fetchAndRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cargar gráfico inicial
|
|
||||||
fetchAndRender();
|
fetchAndRender();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,50 @@
|
||||||
// output_glob_war.js
|
// 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const elem = document.getElementById('globWarContainer');
|
const elem = document.getElementById('globWarContainer');
|
||||||
const form = document.getElementById('paramForm');
|
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)
|
const graph = ForceGraph3D()(elem)
|
||||||
.backgroundColor('#000000')
|
.backgroundColor('#000000')
|
||||||
.nodeLabel('id')
|
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||||
.nodeAutoColorBy('group')
|
.nodeAutoColorBy('group')
|
||||||
.nodeVal(1)
|
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||||
.linkColor(() => 'red')
|
.linkColor(() => 'red')
|
||||||
.onNodeClick(node => showNodeContent(node.content))
|
.onNodeClick(node => showNodeDetail(node))
|
||||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||||
.forceEngine('d3')
|
.nodeThreeObject(node => {
|
||||||
//.d3Force('charge', d3.forceManyBody().strength(-300))
|
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||||
//.d3Force('link', d3.forceLink().distance(300).strength(1));
|
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() {
|
function centerGraph() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const width = elem.clientWidth;
|
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||||
const height = elem.clientHeight;
|
|
||||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
|
|
@ -32,89 +53,177 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
centerGraph();
|
centerGraph();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mostrar contenido del nodo
|
// --- Detail panel helpers ---
|
||||||
function showNodeContent(content) {
|
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||||
console.log('Contenido del nodo:', content);
|
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 = {}) {
|
async function getData(paramsObj = {}) {
|
||||||
try {
|
try {
|
||||||
let url = '/api/data';
|
let url = '/api/data';
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
// Establecer el tema fijo
|
|
||||||
params.append('tema', 'guerra global');
|
params.append('tema', 'guerra global');
|
||||||
|
|
||||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
|
||||||
for (const key in paramsObj) {
|
for (const key in paramsObj) {
|
||||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||||
params.append(key, paramsObj[key]);
|
params.append(key, paramsObj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
url += `?${params.toString()}`;
|
url += `?${params.toString()}`;
|
||||||
console.log('🔎 Fetch URL:', url);
|
console.log('Fetch URL:', url);
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error(response.statusText);
|
if (!response.ok) throw new Error(response.statusText);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Datos recibidos del servidor:', data);
|
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||||
|
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||||
// 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));
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al obtener datos del servidor:', error);
|
console.error('Error al obtener datos:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
|
||||||
async function fetchAndRender() {
|
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 = {
|
const paramsObj = {
|
||||||
subtematica,
|
subtematica: document.getElementById('param2').value,
|
||||||
palabraClave,
|
palabraClave: document.getElementById('param1').value,
|
||||||
fechaInicio,
|
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||||
fechaFin,
|
fechaFin: document.getElementById('fecha_fin').value,
|
||||||
nodos,
|
nodos: document.getElementById('nodos').value,
|
||||||
complejidad // enviado al backend y usado de umbral en cliente
|
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);
|
const graphData = await getData(paramsObj);
|
||||||
if (!graphData) return;
|
if (!graphData) return;
|
||||||
|
lastGraphData = graphData;
|
||||||
// Filtrar enlaces por umbral de similitud
|
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||||
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 });
|
|
||||||
centerGraph();
|
centerGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escuchar evento submit del formulario
|
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||||
form.addEventListener('submit', event => {
|
|
||||||
event.preventDefault();
|
|
||||||
fetchAndRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cargar gráfico inicial
|
|
||||||
fetchAndRender();
|
fetchAndRender();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,50 @@
|
||||||
// output_int_sec.js
|
// 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const elem = document.getElementById('intSecContainer');
|
const elem = document.getElementById('intSecContainer');
|
||||||
const form = document.getElementById('paramForm');
|
const form = document.getElementById('paramForm');
|
||||||
const detailPanel = document.getElementById('detailPanel');
|
|
||||||
|
|
||||||
// --- Etiqueta de texto con CanvasTexture (sin SpriteText externo) ---
|
let lastGraphData = { nodes: [], links: [] };
|
||||||
function makeTextSprite(text) {
|
let fullGraphData = null;
|
||||||
if (!window.THREE) {
|
let currentRelatedNodes = [];
|
||||||
console.warn('THREE no está disponible; omito etiquetas.');
|
let currentRelatedIdx = 0;
|
||||||
return undefined;
|
let currentNode = null;
|
||||||
}
|
|
||||||
const pad = 16; // más padding para nitidez
|
|
||||||
const font = 'bold 36px Fira Code, monospace'; // fuente más grande
|
|
||||||
|
|
||||||
// 1) preparar canvas al tamaño del texto
|
const textureCache = {};
|
||||||
const c = document.createElement('canvas');
|
function getTexture(url) {
|
||||||
const ctx = c.getContext('2d');
|
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||||
ctx.font = font;
|
return textureCache[url];
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inicializar el gráfico 3D
|
|
||||||
const graph = ForceGraph3D()(elem)
|
const graph = ForceGraph3D()(elem)
|
||||||
.backgroundColor('#000000')
|
.backgroundColor('#000000')
|
||||||
.nodeLabel('id')
|
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||||
.nodeAutoColorBy('group')
|
.nodeAutoColorBy('group')
|
||||||
.nodeVal(1)
|
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||||
.linkColor(() => 'blue')
|
.linkColor(() => 'blue')
|
||||||
// Texto fijo sobre cada nodo (usando CanvasTexture)
|
.onNodeClick(node => showNodeDetail(node))
|
||||||
.nodeThreeObject(n => makeTextSprite(n.id))
|
|
||||||
.nodeThreeObjectExtend(true) // mantiene esfera + texto
|
|
||||||
.onNodeClick(node => showNodeContent(node.content))
|
|
||||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
.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');
|
.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() {
|
function centerGraph() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const width = elem.clientWidth;
|
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||||
const height = elem.clientHeight;
|
|
||||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
|
|
@ -81,101 +53,175 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
centerGraph();
|
centerGraph();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mostrar contenido del nodo
|
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||||
function showNodeContent(content) {
|
function formatDate(fecha) {
|
||||||
if (!detailPanel) {
|
if (!fecha) return '';
|
||||||
console.log('Contenido del nodo:', content);
|
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const h2 = document.createElement('h2');
|
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||||
h2.style.cssText = 'margin:0 0 8px';
|
|
||||||
h2.textContent = 'Detalle';
|
function getRelated(node) {
|
||||||
const pre = document.createElement('pre');
|
const nid = node.id;
|
||||||
pre.style.cssText = 'white-space:pre-wrap; line-height:1.35; font-family:\'Fira Code\', monospace; font-size:14px; color:#e5e5e5;';
|
const relIds = new Set();
|
||||||
pre.textContent = content || 'No hay contenido disponible.';
|
for (const lk of lastGraphData.links) {
|
||||||
detailPanel.replaceChildren(h2, pre);
|
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||||
const split = document.querySelector('main.split-screen');
|
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||||
if (split) split.classList.add('show-detail');
|
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 = {}) {
|
async function getData(paramsObj = {}) {
|
||||||
try {
|
try {
|
||||||
let url = '/api/data';
|
let url = '/api/data';
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
// Establecer el tema fijo
|
|
||||||
params.append('tema', 'inteligencia y seguridad');
|
params.append('tema', 'inteligencia y seguridad');
|
||||||
|
|
||||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
|
||||||
for (const key in paramsObj) {
|
for (const key in paramsObj) {
|
||||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||||
params.append(key, paramsObj[key]);
|
params.append(key, paramsObj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
url += `?${params.toString()}`;
|
url += `?${params.toString()}`;
|
||||||
console.log('🔎 Fetch URL:', url);
|
console.log('Fetch URL:', url);
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error(response.statusText);
|
if (!response.ok) throw new Error(response.statusText);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Datos recibidos del servidor:', data);
|
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||||
|
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||||
// 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));
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al obtener datos del servidor:', error);
|
console.error('Error al obtener datos:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
|
||||||
async function fetchAndRender() {
|
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 = {
|
const paramsObj = {
|
||||||
subtematica,
|
subtematica: document.getElementById('param2').value,
|
||||||
palabraClave,
|
palabraClave: document.getElementById('param1').value,
|
||||||
fechaInicio,
|
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||||
fechaFin,
|
fechaFin: document.getElementById('fecha_fin').value,
|
||||||
nodos,
|
nodos: document.getElementById('nodos').value,
|
||||||
complejidad // enviado al backend y usado de umbral en cliente
|
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);
|
const graphData = await getData(paramsObj);
|
||||||
if (!graphData) return;
|
if (!graphData) return;
|
||||||
|
lastGraphData = graphData;
|
||||||
// Filtrar enlaces por umbral de similitud
|
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||||
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 });
|
|
||||||
centerGraph();
|
centerGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escuchar evento submit del formulario
|
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||||
form.addEventListener('submit', event => {
|
|
||||||
event.preventDefault();
|
|
||||||
fetchAndRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cargar gráfico inicial
|
|
||||||
fetchAndRender();
|
fetchAndRender();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,50 @@
|
||||||
// output_popl_up_pruebas.js
|
// 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const elem = document.getElementById('poplUpContainer');
|
const elem = document.getElementById('poplUpContainer');
|
||||||
const form = document.getElementById('paramForm');
|
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)
|
const graph = ForceGraph3D()(elem)
|
||||||
.backgroundColor('#000000')
|
.backgroundColor('#000000')
|
||||||
.nodeLabel('id')
|
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||||
.nodeAutoColorBy('group')
|
.nodeAutoColorBy('group')
|
||||||
.nodeVal(1)
|
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||||
.linkColor(() => 'orange')
|
.linkColor(() => 'orange')
|
||||||
.onNodeClick(node => showNodeContent(node.content))
|
.onNodeClick(node => showNodeDetail(node))
|
||||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||||
.forceEngine('d3')
|
.nodeThreeObject(node => {
|
||||||
//.d3Force('charge', d3.forceManyBody().strength(-300))
|
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||||
//.d3Force('link', d3.forceLink().distance(300).strength(1));
|
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() {
|
function centerGraph() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const width = elem.clientWidth;
|
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||||
const height = elem.clientHeight;
|
|
||||||
graph.zoomToFit(400, Math.min(width, height) * 0.1);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
|
|
@ -32,98 +53,175 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
centerGraph();
|
centerGraph();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mostrar contenido del nodo
|
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||||
function showNodeContent(content) {
|
function formatDate(fecha) {
|
||||||
console.log('Contenido del nodo:', content);
|
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 = {}) {
|
async function getData(paramsObj = {}) {
|
||||||
try {
|
try {
|
||||||
let url = '/api/data';
|
let url = '/api/data';
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
// Establecer el tema fijo
|
|
||||||
params.append('tema', 'demografía y sociedad');
|
params.append('tema', 'demografía y sociedad');
|
||||||
|
|
||||||
// Agregar parámetros del formulario, incluyendo complejidad como umbral
|
|
||||||
for (const key in paramsObj) {
|
for (const key in paramsObj) {
|
||||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||||
params.append(key, paramsObj[key]);
|
params.append(key, paramsObj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
url += `?${params.toString()}`;
|
url += `?${params.toString()}`;
|
||||||
console.log('🔎 Fetch URL:', url);
|
console.log('Fetch URL:', url);
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error(response.statusText);
|
if (!response.ok) throw new Error(response.statusText);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Datos recibidos del servidor:', data);
|
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||||
|
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||||
// 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));
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al obtener datos del servidor:', error);
|
console.error('Error al obtener datos:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función principal: fetch, filtrar por complejidad/umbral y renderizar
|
|
||||||
async function fetchAndRender() {
|
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 = {
|
const paramsObj = {
|
||||||
subtematica,
|
subtematica: document.getElementById('param2').value,
|
||||||
palabraClave,
|
palabraClave: document.getElementById('param1').value,
|
||||||
fechaInicio,
|
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||||
fechaFin,
|
fechaFin: document.getElementById('fecha_fin').value,
|
||||||
nodos,
|
nodos: document.getElementById('nodos').value,
|
||||||
complejidad // enviado al backend y usado de umbral en cliente
|
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);
|
const graphData = await getData(paramsObj);
|
||||||
if (!graphData) return;
|
if (!graphData) return;
|
||||||
|
lastGraphData = graphData;
|
||||||
// Filtrar enlaces por umbral de similitud
|
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||||
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 });
|
|
||||||
centerGraph();
|
centerGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escuchar evento submit del formulario
|
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||||
form.addEventListener('submit', event => {
|
|
||||||
event.preventDefault();
|
|
||||||
fetchAndRender();
|
fetchAndRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cargar gráfico inicial
|
|
||||||
fetchAndRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Demografía y Sociedad</title>
|
<title>Demografía y Sociedad</title>
|
||||||
<link rel="stylesheet" href="popl-up.css">
|
<link rel="stylesheet" href="popl-up.css">
|
||||||
|
<link rel="stylesheet" href="sub-nav.css">
|
||||||
|
|
||||||
<!-- Fuentes -->
|
<!-- Fuentes -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
|
@ -12,21 +13,79 @@
|
||||||
|
|
||||||
<!-- Librerías necesarias -->
|
<!-- Librerías necesarias -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Navegación -->
|
<!-- Navegación -->
|
||||||
<nav>
|
<nav class="top-nav">
|
||||||
<ul class="nav-links">
|
<div class="section-buttons">
|
||||||
<li><a href="popl-up.html" class="popl-up">Demografía y Sociedad</a></li>
|
|
||||||
</ul>
|
<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>
|
</nav>
|
||||||
|
|
||||||
<!-- Contenedor principal -->
|
<script>
|
||||||
<main>
|
function setSubtema(val) {
|
||||||
<div id="poplUpContainer" style="position: absolute; width: 100%; height: 100%; z-index: 0;"></div>
|
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 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 -->
|
<!-- Fondo animado -->
|
||||||
<div class="background">
|
<div class="background">
|
||||||
<img src="/images/flujos3.jpg">
|
<img src="/images/flujos3.jpg">
|
||||||
|
|
@ -35,7 +94,6 @@
|
||||||
<img src="/images/flujos3.jpg">
|
<img src="/images/flujos3.jpg">
|
||||||
<img src="/images/flujos3.jpg">
|
<img src="/images/flujos3.jpg">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const fondo = document.querySelector('.background');
|
const fondo = document.querySelector('.background');
|
||||||
|
|
@ -43,6 +101,26 @@
|
||||||
fondo.style.pointerEvents = 'none';
|
fondo.style.pointerEvents = 'none';
|
||||||
}, 1000);
|
}, 1000);
|
||||||
</script>
|
</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>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Barra lateral de filtros -->
|
<!-- Barra lateral de filtros -->
|
||||||
|
|
@ -56,16 +134,23 @@
|
||||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||||
|
|
||||||
<label for="nodos">Nodos:</label>
|
<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>
|
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||||
<input type="range" id="complejidad" name="complejidad" min="1" max="40" value="20">
|
<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>
|
<label for="param1">Palabra clave:</label>
|
||||||
<input type="text" id="param1" name="param1">
|
<input type="text" id="param1" name="param1" placeholder="ej: migración, pandemia...">
|
||||||
|
|
||||||
<label for="param2">Búsqueda por temática personalizada:</label>
|
<label for="param2">Subtematica:</label>
|
||||||
<input type="text" id="param2" name="param2">
|
<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">
|
<input type="submit" value="Aplicar">
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -74,17 +159,7 @@
|
||||||
<!-- Botón para colapsar la barra -->
|
<!-- Botón para colapsar la barra -->
|
||||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
<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 principal -->
|
||||||
<script src="output_popl_up_pruebas.js"></script>
|
<script type="module" src="output_popl_up_pruebas.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -42,19 +42,35 @@ header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 1200px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-buttons {
|
.header-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 70%;
|
top: 50%;
|
||||||
right: 10px;
|
right: 14px;
|
||||||
transform: translateY(-50%);
|
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 {
|
.small-button {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
|
|
@ -495,501 +511,192 @@ footer p {
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Para teléfonos de 768px de ancho en portrait */
|
/* ============================================================
|
||||||
@media screen and (max-width: 768px) and (orientation: portrait) {
|
MÓVIL — Portrait (≤ 1024px)
|
||||||
.background a {
|
============================================================ */
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-column {
|
@media screen and (max-width: 1024px) and (orientation: portrait) {
|
||||||
width: 90%;
|
header { height: auto; padding: 12px 10px; }
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-button {
|
.title { font-size: 3.2rem; }
|
||||||
font-size: 0.6rem;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
.header-buttons {
|
||||||
font-size: 3rem;
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
margin-top: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
.header-buttons--left { position: static; }
|
||||||
|
|
||||||
.small-button {
|
.small-button {
|
||||||
font-size: 0.3rem;
|
font-size: 0.72rem;
|
||||||
padding: 2px 2px;
|
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 {
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ≤ 600px portrait */
|
||||||
|
@media screen and (max-width: 600px) and (orientation: portrait) {
|
||||||
|
.title { font-size: 2.2rem; }
|
||||||
|
|
||||||
|
.small-button {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a {
|
.nav-links a {
|
||||||
font-size: 0.8rem;
|
font-size: 0.68rem;
|
||||||
padding: 8px 16px;
|
padding: 7px 11px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Para teléfonos de 576px de ancho en portrait */
|
|
||||||
@media screen and (max-width: 576px) and (orientation: portrait) {
|
|
||||||
.background a {
|
.background a {
|
||||||
width: 95%;
|
width: 50%;
|
||||||
|
height: 28vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-column {
|
.button-overlay {
|
||||||
width: 95%;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 30px;
|
gap: 8px;
|
||||||
|
padding: 10px 8px 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-button {
|
.overlay-button {
|
||||||
font-size: 0.3rem;
|
font-size: 0.68rem;
|
||||||
padding: 5px 7px;
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
/* ≤ 420px portrait */
|
||||||
font-size: 4rem;
|
@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 {
|
.small-button {
|
||||||
font-size: 0.3rem;
|
font-size: 0.6rem;
|
||||||
padding: 4px 7px;
|
padding: 4px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links {
|
|
||||||
gap: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a {
|
.nav-links a {
|
||||||
font-size: 0.4rem;
|
font-size: 0.62rem;
|
||||||
padding: 7px 14px;
|
padding: 6px 10px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Para teléfonos de 411px de ancho en portrait */
|
|
||||||
@media screen and (max-width: 411px) and (orientation: portrait) {
|
|
||||||
.background a {
|
.background a {
|
||||||
width: 100%;
|
width: 50%;
|
||||||
|
height: 26vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-column {
|
.button-overlay {
|
||||||
width: 100%;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 5px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-button {
|
.overlay-button {
|
||||||
font-size: 0.4rem;
|
font-size: 0.62rem;
|
||||||
padding: 4px 5px;
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
/* ============================================================
|
||||||
font-size: 1.5rem;
|
MÓVIL — Landscape (altura ≤ 500px)
|
||||||
}
|
============================================================ */
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) and (orientation: landscape) and (max-height: 500px) {
|
||||||
|
header { padding: 6px 10px; }
|
||||||
|
.title { font-size: 1.6rem; }
|
||||||
|
|
||||||
.small-button {
|
.small-button {
|
||||||
font-size: 0.3rem;
|
font-size: 0.65rem;
|
||||||
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;
|
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
/* For devices with smaller screens (e.g., 600px - 800px width) */
|
.background a { width: 20%; height: 100%; flex-shrink: 0; }
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-overlay {
|
.button-overlay {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
height: 100%;
|
top: 60px;
|
||||||
padding-bottom: 0px;
|
height: calc(100% - 60px);
|
||||||
}
|
}
|
||||||
|
.button-column { width: 18%; gap: 4px; }
|
||||||
.button-column {
|
|
||||||
width: 15%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-button {
|
.overlay-button {
|
||||||
font-size: 0.3rem;
|
font-size: 0.6rem;
|
||||||
padding: 2px 2px;
|
padding: 5px 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.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 */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For medium-sized devices (e.g., 800px - 1000px width) */
|
/* landscape muy pequeño (≤ 600px ancho) */
|
||||||
@media screen and (min-width: 800px) and (max-width: 1000px) and (max-height: 450px) and (orientation: landscape) {
|
@media screen and (max-width: 600px) and (orientation: landscape) and (max-height: 400px) {
|
||||||
.background {
|
.title { font-size: 1.2rem; }
|
||||||
display: flex;
|
.small-button { font-size: 0.6rem; padding: 3px 6px; }
|
||||||
justify-content: center;
|
.nav-links a { font-size: 0.6rem; padding: 5px 8px; }
|
||||||
height: 100vh;
|
.button-column { width: 28%; }
|
||||||
width: 100%;
|
.overlay-button { font-size: 0.58rem; padding: 4px 6px; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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
|
import torch
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
|
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor, BitsAndBytesConfig
|
||||||
|
|
||||||
# ── Configuración ──────────────────────────────────────────────────────────────
|
# ── 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"}
|
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
|
||||||
|
|
||||||
# RAM por imagen en batch (aprox): ~500MB activaciones encoder
|
# int4 via bitsandbytes: modelo ocupa ~4-5GB VRAM en lugar de ~16GB bfloat16
|
||||||
# Modelo base bfloat16: ~16GB
|
# RTX 3060 12GB → sobra VRAM para activaciones
|
||||||
# Batch de 4: ~18GB total → seguro con 64GB
|
DEFAULT_BATCH_SIZE = 1 # batch 1 para seguridad con 12GB
|
||||||
DEFAULT_BATCH_SIZE = 4
|
|
||||||
|
|
||||||
KEYWORD_PROMPT = """Analiza esta imagen en detalle.
|
KEYWORD_PROMPT = """Analiza esta imagen en detalle.
|
||||||
Devuelve ÚNICAMENTE un objeto JSON válido con esta estructura exacta, sin texto adicional:
|
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] Cargando modelo {self.model_id}...")
|
||||||
print(f"[ImageAnalyzer] Cache: {CACHE_DIR}")
|
print(f"[ImageAnalyzer] Cache: {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 = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||||
self.model_id,
|
self.model_id,
|
||||||
torch_dtype=torch.bfloat16,
|
torch_dtype=torch.bfloat16,
|
||||||
device_map="cpu",
|
device_map="cpu",
|
||||||
cache_dir=CACHE_DIR,
|
cache_dir=CACHE_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._processor = AutoProcessor.from_pretrained(
|
self._processor = AutoProcessor.from_pretrained(
|
||||||
self.model_id,
|
self.model_id,
|
||||||
cache_dir=CACHE_DIR,
|
cache_dir=CACHE_DIR,
|
||||||
)
|
)
|
||||||
print("[ImageAnalyzer] Modelo cargado.")
|
print("[ImageAnalyzer] Modelo cargado (int4 cuantizado).")
|
||||||
|
|
||||||
# ── Opción 3: Resume — obtener archivos ya analizados en MongoDB ───────────
|
# ── Opción 3: Resume — obtener archivos ya analizados en MongoDB ───────────
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue