# Visualización JavaScript — Contexto técnico FLUJOS **Fecha:** 2026-04-21 **Directorio:** `FLUJOS/VISUALIZACION/public/` **Backend:** `FLUJOS/BACK_BACK/FLUJOS_APP.js` --- ## Arquitectura general ``` theflows.net (nginx) ├── / → archivos estáticos desde VISUALIZACION/public/ └── /api/ → proxy a Node.js :3000 └── GET /api/data?tema=...&nodos=...&complejidad=... └── MongoDB FLUJOS_DATOS ``` El frontend es **vanilla JS sin framework**, todo en ficheros `.js` planos. Cada vista (`glob-war`, `int-sec`, `climate`, etc.) tiene su propio script de grafo 3D. --- ## Dependencias externas (CDN) ```html ``` Three.js debe cargarse ANTES de 3d-force-graph o `THREE` no estará disponible globalmente cuando el grafo intente usarlo para los Sprite. --- ## API — Endpoint único ``` GET /api/data ``` Parámetros de query: | Param | Tipo | Descripción | |---|---|---| | `tema` | string (obligatorio) | Tema principal de la consulta | | `subtematica` | string | Filtro por subtema | | `palabraClave` | string | Búsqueda `$regex` en campo `texto` | | `fechaInicio` | `YYYY-MM-DD` | Rango inicio | | `fechaFin` | `YYYY-MM-DD` | Rango fin | | `nodos` | int (máx. 500) | Límite de nodos de texto | | `complejidad` | float | Similitud mínima (%) para los enlaces | **Respuesta:** ```json { "nodes": [ { "id": "archivo.txt", "group": "subtema", "tema": "...", "content": "...", "fecha": "...", "type": "texto" }, { "id": "imagen.jpg", "group": "subtema", "tema": "...", "content": "...", "fecha": "...", "type": "imagen", "image_url": "/wiki-images/tema/img.jpg", "label": "subtema" } ], "links": [ { "source": "archivo_A.txt", "target": "archivo_B.txt", "value": 23.45 } ] } ``` El backend limita imágenes a `floor(nodosLimit / 3)` para no saturar el grafo. Prefiere `imagenes` (analizadas con Qwen) sobre `imagenes_wiki` (fallback scrapeado). --- ## Estructura de un script de grafo (patrón base) Todos los scripts (`output_glob_war.js`, `output_int_sec.js`, etc.) siguen el mismo patrón: ```javascript document.addEventListener('DOMContentLoaded', () => { const elem = document.getElementById('contenedorContainer'); // 1. Inicializar grafo const graph = ForceGraph3D()(elem) .backgroundColor('#000000') .nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id) .nodeAutoColorBy('group') .nodeVal(node => node.type === 'imagen' ? 0 : 2) // 0 = sin esfera para imágenes .linkColor(() => 'rgba(0,255,80,0.4)') .onNodeClick(node => showNodeContent(node.content)) .onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; }) .nodeThreeObject(node => { /* Sprite para imágenes */ }) .nodeThreeObjectExtend(false) .forceEngine('d3') .d3Force('charge', d3.forceManyBody().strength(-10)) .d3Force('link', d3.forceLink().distance(30).strength(1)); // 2. Fetch de datos async function getData(paramsObj = {}) { ... } // 3. Renderizar async function fetchAndRender() { ... } // 4. Evento del formulario document.getElementById('paramForm').addEventListener('submit', e => { e.preventDefault(); fetchAndRender(); }); // 5. Carga inicial fetchAndRender(); }); ``` --- ## Nodos de imagen — THREE.Sprite Los nodos de tipo `imagen` usan un Sprite de Three.js en lugar de la esfera por defecto: ```javascript const textureCache = {}; function getTexture(url) { if (!textureCache[url]) { textureCache[url] = new THREE.TextureLoader().load(url); } return textureCache[url]; } // En nodeThreeObject: .nodeThreeObject(node => { if (node.type !== 'imagen' || !node.image_url) return null; const texture = getTexture(node.image_url); texture.colorSpace = THREE.SRGBColorSpace; // corrección de color const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false }); const sprite = new THREE.Sprite(material); sprite.scale.set(14, 9, 1); // proporción 14:9 ≈ 16:9 return sprite; }) .nodeThreeObjectExtend(false) // false = reemplaza la esfera, no la extiende ``` `getTexture` cachea las texturas para no recargar la misma imagen cada vez que el grafo se actualiza. `depthWrite: false` evita artefactos de z-fighting entre sprites que se superponen. --- ## Archivos por vista | Vista | HTML | Script | |---|---|---| | Index / Home | `index.html` | ninguno (estático) | | Glob-War | `glob-war.html` | `output_glob_war_pruebas.js` | | Int-Sec | `int-sec.html` | `output_int_sec.js` | | Climate | `climate.html` | `climate.js` / `output_climate_pruebas.js` | | Eco-Corp | `eco-corp.html` | `output_eco_corp_pruebas.js` | | Popl-Up | `popl-up.html` | `output_popl_up_pruebas.js` / `output_popl_up.js` | Los ficheros `*_pruebas.js` son la versión en desarrollo activo (son los referenciados en los HTML). Los sin `_pruebas` son versiones anteriores o de referencia. --- ## Formulario de parámetros (sidebar) ```html
``` `color1` y `color2` están en el HTML pero el backend los ignora. Están pendientes de implementar para colorear nodos por grupo. --- ## Filtrado client-side de enlaces Además del filtro de `complejidad` en el backend (`porcentaje_similitud >= X`), el cliente aplica un segundo filtro eliminando enlaces cuyos nodos no existan en la respuesta: ```javascript const nodeIds = new Set(data.nodes.map(n => n.id)); data.links = data.links.filter(l => nodeIds.has(l.source) && nodeIds.has(l.target)); ``` Esto previene errores de 3d-force-graph cuando llegan enlaces a nodos que no se devolvieron por el límite. --- ## Backend Node.js — FLUJOS_APP.js **Archivo:** `FLUJOS/BACK_BACK/FLUJOS_APP.js` **Puerto:** 3000 (configurado en `.env`) **Proceso:** gestionado manualmente (falta configurar como systemd service) Rutas: ``` GET / → glob-war.html (redirige a index.html) GET /api/data → endpoint de datos GET /wiki-images/* → estático desde IMAGENES/output/wiki_images/ GET /* → archivos estáticos desde VISUALIZACION/public/ ``` La CSP actual (Helmet) permite `unsafe-inline` en scripts — pendiente de corregir (ver SEGURIDAD.md). --- ## Imágenes wiki — ruta física a URL ``` Disco: /var/www/theflows.net/flujos/FLUJOS_DATOS/IMAGENES/output/wiki_images/{tema_slug}/{archivo} Express: app.use('/wiki-images', express.static('.../wiki_images')) URL: https://theflows.net/wiki-images/{tema_slug}/{archivo} ``` El backend construye `image_url` en el API: ```javascript const WIKI_IMAGES_BASE = '/var/www/theflows.net/flujos/FLUJOS_DATOS/IMAGENES/output/wiki_images/'; const relativePath = result.image_path.replace(WIKI_IMAGES_BASE, ''); image_url = `/wiki-images/${relativePath}`; ```