FLUJOS/INFO/DOCS/CONTEXT/VISUALIZACION_JS.md
CAPITANSITO 954f47996f refactor: reorganizar docs y pocs bajo INFO/
- docs/ → INFO/DOCS/CONTEXT/ (documentación técnica en markdown)
- FLUJOS/DOCS/ + FLUJOS_DATOS/DOCS/ → INFO/DOCS/ (txts de arquitectura)
- POCS/ → INFO/POCS/ (pruebas de concepto)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 23:49:33 +02:00

7.5 KiB

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)

<!-- D3.js v6 — simulación de fuerzas -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

<!-- Three.js r168 — renderer WebGL, Sprite, TextureLoader -->
<script src="https://unpkg.com/three@0.168/build/three.min.js"></script>

<!-- 3d-force-graph — wrapper de Three.js para grafos de fuerza 3D -->
<script src="https://unpkg.com/3d-force-graph"></script>

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:

{
  "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:

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:

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)

<form id="paramForm">
  <input type="date"   id="fecha_inicio">
  <input type="date"   id="fecha_fin">
  <input type="number" id="nodos"      value="100">
  <input type="range"  id="complejidad" min="1" max="40" value="20">
  <input type="text"   id="param1">     <!-- palabraClave -->
  <input type="color"  id="color1">     <!-- sin uso en backend aún -->
  <input type="text"   id="param2">     <!-- subtematica -->
  <input type="color"  id="color2">     <!-- sin uso en backend aún -->
  <input type="submit" value="Aplicar">
</form>

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:

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:

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}`;