- Move BACK_BACK/ → POCS/BACK_BACK/ (image pipeline scripts) - Move VISUALIZACION/ → POCS/VISUALIZACION/ (demos + static assets) - No path changes needed: ../VISUALIZACION/public still resolves correctly from POCS/BACK_BACK/FLUJOS_APP_PRUEBAS.js - Add FLUJOS_DATOS/DOCS/extraer_info_bbdd.txt (DB state snapshot + commands) FLUJOS/ and FLUJOS_DATOS/ untouched (production). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
346 lines
16 KiB
HTML
346 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>FLUJOS — Demo: Mixed Nodes (HTML Cards)</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://esm.sh/three@0.168",
|
|
"three/": "https://esm.sh/three@0.168/",
|
|
"three-spritetext": "https://esm.sh/three-spritetext@1.9?external=three",
|
|
"3d-force-graph": "https://esm.sh/3d-force-graph@1.73?external=three"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Fira Code', monospace; }
|
|
body { background: #000; color: #39ff14; overflow: hidden; }
|
|
#graph { position: fixed; top: 0; left: 0; width: 100%; height: 100%; }
|
|
|
|
#header {
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 10;
|
|
padding: 10px 20px;
|
|
background: linear-gradient(180deg, rgba(0,0,0,0.9) 0%, transparent 100%);
|
|
display: flex; align-items: center; gap: 16px;
|
|
}
|
|
#header h1 { font-size: 1.1em; color: #39ff14; letter-spacing: 3px; text-shadow: 0 0 10px #39ff14; }
|
|
#header .tag {
|
|
font-size: 0.65em; padding: 3px 8px;
|
|
border: 1px solid #cc44ff; color: #cc44ff;
|
|
opacity: 0.8; letter-spacing: 2px;
|
|
}
|
|
|
|
/* CSS2D node cards — these are real DOM elements positioned by THREE.js */
|
|
.node-card {
|
|
background: rgba(0,0,0,0.85);
|
|
border: 1px solid var(--color, #39ff14);
|
|
padding: 6px 10px;
|
|
max-width: 160px;
|
|
cursor: pointer;
|
|
pointer-events: auto;
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
user-select: none;
|
|
}
|
|
.node-card:hover {
|
|
box-shadow: 0 0 12px var(--color, #39ff14);
|
|
}
|
|
.node-card .card-title {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
letter-spacing: 0.5px;
|
|
line-height: 1.3;
|
|
margin-bottom: 3px;
|
|
}
|
|
.node-card .card-badge {
|
|
display: inline-block;
|
|
font-size: 8px;
|
|
padding: 1px 5px;
|
|
letter-spacing: 1px;
|
|
background: var(--color-bg, rgba(57,255,20,0.1));
|
|
color: var(--color, #39ff14);
|
|
border: 1px solid var(--color, #39ff14);
|
|
margin-bottom: 3px;
|
|
}
|
|
.node-card .card-source {
|
|
font-size: 8px;
|
|
color: #555;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
/* Small leaf node labels */
|
|
.node-label-small {
|
|
font-size: 9px;
|
|
color: var(--color, #666);
|
|
background: rgba(0,0,0,0.7);
|
|
padding: 2px 5px;
|
|
pointer-events: none;
|
|
white-space: nowrap;
|
|
user-select: none;
|
|
}
|
|
|
|
#technique-label {
|
|
position: fixed; top: 50px; right: 20px; z-index: 10;
|
|
font-size: 0.6em; color: #333; letter-spacing: 1px;
|
|
text-align: right; line-height: 1.8;
|
|
}
|
|
|
|
#controls {
|
|
position: fixed; bottom: 30px; left: 20px; z-index: 10;
|
|
background: rgba(0,0,0,0.8); border: 1px solid #222;
|
|
padding: 12px 16px;
|
|
}
|
|
#controls h4 { font-size: 0.6em; color: #444; margin-bottom: 8px; letter-spacing: 2px; }
|
|
.ctrl-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
.ctrl-row label { font-size: 0.65em; color: #666; }
|
|
.ctrl-row input[type=range] { width: 100px; accent-color: #39ff14; }
|
|
.ctrl-row span { font-size: 0.6em; color: #444; width: 30px; }
|
|
|
|
#selected-info {
|
|
position: fixed; bottom: 30px; right: 20px; z-index: 10;
|
|
width: 260px;
|
|
background: rgba(0,0,0,0.9);
|
|
border: 1px solid #222;
|
|
padding: 14px;
|
|
display: none;
|
|
}
|
|
#selected-info h3 { font-size: 0.8em; color: #fff; margin-bottom: 6px; }
|
|
#selected-info .badge { font-size: 0.65em; padding: 2px 8px; margin-bottom: 8px; display: inline-block; }
|
|
#selected-info p { font-size: 0.7em; color: #888; line-height: 1.5; margin-bottom: 6px; }
|
|
#selected-info .meta { font-size: 0.65em; color: #555; }
|
|
#selected-info .close {
|
|
position: absolute; top: 8px; right: 10px;
|
|
cursor: pointer; color: #555; font-size: 0.8em;
|
|
background: none; border: none; font-family: inherit;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="graph"></div>
|
|
|
|
<div id="header">
|
|
<h1>FLUJOS</h1>
|
|
<span class="tag">HTML NODES</span>
|
|
<span class="tag">CSS2DRenderer</span>
|
|
</div>
|
|
|
|
<div id="technique-label">
|
|
CSS2DRenderer + CSS2DObject<br>
|
|
nodeThreeObject(node => CSS2DObject(div))<br>
|
|
nodeThreeObjectExtend(true)<br>
|
|
— Real DOM elements in 3D space —
|
|
</div>
|
|
|
|
<div id="controls">
|
|
<h4>VISUALIZACIÓN</h4>
|
|
<div class="ctrl-row">
|
|
<label>Umbral similitud</label>
|
|
<input type="range" id="umbral" min="0" max="95" value="0" step="5">
|
|
<span id="umbral-val">0%</span>
|
|
</div>
|
|
<div class="ctrl-row">
|
|
<label>Carga D3</label>
|
|
<input type="range" id="charge" min="-400" max="-50" value="-200" step="10">
|
|
<span id="charge-val">-200</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="selected-info">
|
|
<button class="close" onclick="document.getElementById('selected-info').style.display='none'">✕</button>
|
|
<h3 id="sel-title"></h3>
|
|
<span class="badge" id="sel-badge"></span>
|
|
<p id="sel-content"></p>
|
|
<div class="meta" id="sel-meta"></div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
import ForceGraph3D from '3d-force-graph';
|
|
import { CSS2DRenderer, CSS2DObject } from 'https://esm.sh/three@0.168/examples/jsm/renderers/CSS2DRenderer.js';
|
|
|
|
const GROUP_COLORS = {
|
|
core: '#39ff14',
|
|
climate: '#ff4500',
|
|
security: '#ff69b4',
|
|
journalism: '#00fff2',
|
|
corporate: '#ffdc00',
|
|
politics: '#4488ff',
|
|
data: '#cc44ff'
|
|
};
|
|
|
|
// `card: true` = rendered as HTML card (CSS2DObject)
|
|
// `card: false` = rendered as colored sphere with small label
|
|
const MOCK_DATA = {
|
|
nodes: [
|
|
// Hub nodes — full HTML cards
|
|
{ id: 'FLUJOS', group: 'core', card: true, source: 'SISTEMA', connections: 7, content: 'Plataforma de análisis y visualización de flujos informativos globales' },
|
|
{ id: 'Cambio Climático', group: 'climate', card: true, source: 'Wikipedia', connections: 5, content: 'Crisis climática: emisiones, biodiversidad, energía' },
|
|
{ id: 'Seguridad Intl.', group: 'security', card: true, source: 'Noticias', connections: 4, content: 'Geopolítica, conflictos armados y espionaje global' },
|
|
{ id: 'Libertad de Prensa', group: 'journalism', card: true, source: 'Wikipedia', connections: 4, content: 'Periodismo, desinformación y denunciantes' },
|
|
{ id: 'Eco-Corporativo', group: 'corporate', card: true, source: 'Noticias', connections: 4, content: 'Poder corporativo, lobbying y paraísos fiscales' },
|
|
{ id: 'Populismo', group: 'politics', card: true, source: 'Noticias', connections: 3, content: 'Movimientos populistas, elecciones y extremismo' },
|
|
{ id: 'Wikipedia', group: 'data', card: true, source: 'Wikipedia', connections: 3, content: 'Fuente de datos abiertos y verificación factual' },
|
|
{ id: 'Torrents & P2P', group: 'data', card: true, source: 'Torrents', connections: 2, content: 'Distribución descentralizada de información' },
|
|
// Leaf nodes — small labels on spheres
|
|
{ id: 'Emisiones CO₂', group: 'climate', card: false, source: 'Wikipedia' },
|
|
{ id: 'Energía Renovable', group: 'climate', card: false, source: 'Wikipedia' },
|
|
{ id: 'Pérdida Biodiversidad',group: 'climate', card: false, source: 'Wikipedia' },
|
|
{ id: 'Ciberseguridad', group: 'security', card: false, source: 'Noticias' },
|
|
{ id: 'Vigilancia Masiva', group: 'security', card: false, source: 'Noticias' },
|
|
{ id: 'Privacidad Datos', group: 'security', card: false, source: 'Wikipedia' },
|
|
{ id: 'Desinformación', group: 'journalism', card: false, source: 'Noticias' },
|
|
{ id: 'Whistleblowers', group: 'journalism', card: false, source: 'Wikipedia' },
|
|
{ id: 'Big Tech', group: 'corporate', card: false, source: 'Noticias' },
|
|
{ id: 'Paraísos Fiscales', group: 'corporate', card: false, source: 'Wikipedia' },
|
|
{ id: 'Lobbying', group: 'corporate', card: false, source: 'Noticias' },
|
|
{ id: 'Elecciones', group: 'politics', card: false, source: 'Noticias' },
|
|
{ id: 'Migración', group: 'politics', card: false, source: 'Noticias' },
|
|
{ id: 'Extremismo', group: 'politics', card: false, source: 'Noticias' },
|
|
{ id: 'Redes Sociales', group: 'data', card: false, source: 'Wikipedia' },
|
|
{ id: 'IA & Algoritmos', group: 'data', card: false, source: 'Noticias' },
|
|
],
|
|
links: [
|
|
{ source: 'FLUJOS', target: 'Cambio Climático', value: 90 },
|
|
{ source: 'FLUJOS', target: 'Seguridad Intl.', value: 85 },
|
|
{ source: 'FLUJOS', target: 'Libertad de Prensa', value: 88 },
|
|
{ source: 'FLUJOS', target: 'Eco-Corporativo', value: 87 },
|
|
{ source: 'FLUJOS', target: 'Populismo', value: 82 },
|
|
{ source: 'FLUJOS', target: 'Wikipedia', value: 95 },
|
|
{ source: 'FLUJOS', target: 'Torrents & P2P', value: 88 },
|
|
{ source: 'Cambio Climático', target: 'Emisiones CO₂', value: 92 },
|
|
{ source: 'Cambio Climático', target: 'Energía Renovable', value: 88 },
|
|
{ source: 'Cambio Climático', target: 'Pérdida Biodiversidad',value: 85 },
|
|
{ source: 'Emisiones CO₂', target: 'Eco-Corporativo', value: 78 },
|
|
{ source: 'Seguridad Intl.', target: 'Ciberseguridad', value: 88 },
|
|
{ source: 'Seguridad Intl.', target: 'Vigilancia Masiva', value: 85 },
|
|
{ source: 'Seguridad Intl.', target: 'Migración', value: 75 },
|
|
{ source: 'Ciberseguridad', target: 'Privacidad Datos', value: 90 },
|
|
{ source: 'Vigilancia Masiva', target: 'Privacidad Datos', value: 92 },
|
|
{ source: 'Vigilancia Masiva', target: 'IA & Algoritmos', value: 82 },
|
|
{ source: 'Libertad de Prensa',target: 'Desinformación', value: 88 },
|
|
{ source: 'Libertad de Prensa',target: 'Whistleblowers', value: 85 },
|
|
{ source: 'Desinformación', target: 'Redes Sociales', value: 90 },
|
|
{ source: 'Desinformación', target: 'Elecciones', value: 85 },
|
|
{ source: 'Eco-Corporativo', target: 'Big Tech', value: 85 },
|
|
{ source: 'Eco-Corporativo', target: 'Paraísos Fiscales', value: 88 },
|
|
{ source: 'Eco-Corporativo', target: 'Lobbying', value: 90 },
|
|
{ source: 'Big Tech', target: 'IA & Algoritmos', value: 88 },
|
|
{ source: 'Big Tech', target: 'Redes Sociales', value: 92 },
|
|
{ source: 'Lobbying', target: 'Elecciones', value: 78 },
|
|
{ source: 'Populismo', target: 'Elecciones', value: 88 },
|
|
{ source: 'Populismo', target: 'Migración', value: 85 },
|
|
{ source: 'Populismo', target: 'Extremismo', value: 82 },
|
|
{ source: 'Extremismo', target: 'Redes Sociales', value: 78 },
|
|
{ source: 'IA & Algoritmos', target: 'Redes Sociales', value: 85 },
|
|
]
|
|
};
|
|
|
|
const elem = document.getElementById('graph');
|
|
const css2dRenderer = new CSS2DRenderer();
|
|
css2dRenderer.setSize(window.innerWidth, window.innerHeight);
|
|
css2dRenderer.domElement.style.position = 'absolute';
|
|
css2dRenderer.domElement.style.top = '0';
|
|
css2dRenderer.domElement.style.left = '0';
|
|
css2dRenderer.domElement.style.pointerEvents = 'none';
|
|
elem.appendChild(css2dRenderer.domElement);
|
|
|
|
function showNodeInfo(node) {
|
|
const panel = document.getElementById('selected-info');
|
|
document.getElementById('sel-title').textContent = node.id;
|
|
const badge = document.getElementById('sel-badge');
|
|
badge.textContent = node.group.toUpperCase();
|
|
badge.style.background = GROUP_COLORS[node.group] + '22';
|
|
badge.style.border = `1px solid ${GROUP_COLORS[node.group]}`;
|
|
badge.style.color = GROUP_COLORS[node.group];
|
|
document.getElementById('sel-content').textContent = node.content || node.id;
|
|
document.getElementById('sel-meta').textContent =
|
|
`FUENTE: ${node.source || '—'}` + (node.connections ? ` | CONEXIONES: ${node.connections}` : '');
|
|
panel.style.display = 'block';
|
|
}
|
|
|
|
const Graph = ForceGraph3D({ extraRenderers: [css2dRenderer] })(elem)
|
|
.backgroundColor('#000000')
|
|
.nodeLabel(() => '') // disable default tooltip — we use CSS2D labels
|
|
.nodeColor(node => GROUP_COLORS[node.group] || '#ffffff')
|
|
.nodeVal(node => node.card ? 3 : 1)
|
|
.nodeOpacity(node => node.card ? 0 : 0.85) // hide sphere for card nodes
|
|
.linkColor(() => 'rgba(57,255,20,0.2)')
|
|
.linkWidth(link => (link.value || 50) / 60)
|
|
.nodeThreeObject(node => {
|
|
const color = GROUP_COLORS[node.group] || '#ffffff';
|
|
|
|
if (node.card) {
|
|
// Full HTML card as CSS2D object
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'node-card';
|
|
wrapper.style.setProperty('--color', color);
|
|
wrapper.style.setProperty('--color-bg', color + '15');
|
|
wrapper.style.borderColor = color;
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'card-title';
|
|
title.textContent = node.id;
|
|
|
|
const badge = document.createElement('div');
|
|
badge.className = 'card-badge';
|
|
badge.textContent = node.group.toUpperCase();
|
|
|
|
const source = document.createElement('div');
|
|
source.className = 'card-source';
|
|
source.textContent = `↗ ${node.source || '—'}${node.connections ? ' · ' + node.connections + ' conn.' : ''}`;
|
|
|
|
wrapper.appendChild(title);
|
|
wrapper.appendChild(badge);
|
|
wrapper.appendChild(source);
|
|
|
|
wrapper.addEventListener('click', () => showNodeInfo(node));
|
|
|
|
return new CSS2DObject(wrapper);
|
|
} else {
|
|
// Small text label for leaf nodes
|
|
const label = document.createElement('div');
|
|
label.className = 'node-label-small';
|
|
label.textContent = node.id;
|
|
label.style.setProperty('--color', color);
|
|
return new CSS2DObject(label);
|
|
}
|
|
})
|
|
.nodeThreeObjectExtend(true)
|
|
.onNodeClick(node => showNodeInfo(node))
|
|
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
|
.graphData(MOCK_DATA);
|
|
|
|
Graph.d3Force('charge').strength(-200);
|
|
setTimeout(() => Graph.zoomToFit(800, 100), 1500);
|
|
|
|
// Controls
|
|
const umbralSlider = document.getElementById('umbral');
|
|
const umbralVal = document.getElementById('umbral-val');
|
|
umbralSlider.addEventListener('input', () => {
|
|
const v = parseInt(umbralSlider.value);
|
|
umbralVal.textContent = v + '%';
|
|
const allLinks = MOCK_DATA.links;
|
|
const filtered = v > 0 ? allLinks.filter(l => (l.value || 0) >= v) : allLinks;
|
|
Graph.graphData({ nodes: MOCK_DATA.nodes, links: filtered });
|
|
});
|
|
|
|
const chargeSlider = document.getElementById('charge');
|
|
const chargeVal = document.getElementById('charge-val');
|
|
chargeSlider.addEventListener('input', () => {
|
|
const v = parseInt(chargeSlider.value);
|
|
chargeVal.textContent = v;
|
|
Graph.d3Force('charge').strength(v);
|
|
Graph.d3ReheatSimulation();
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
Graph.width(elem.clientWidth).height(elem.clientHeight);
|
|
css2dRenderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|