Reorganize POCs and demos under POCS/ folder

- 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>
This commit is contained in:
CAPITANSITO 2026-04-01 01:22:27 +02:00
parent 932e8e80db
commit 25953376cd
15 changed files with 172 additions and 0 deletions

View file

@ -0,0 +1,301 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>FLUJOS — Demo: Image Nodes</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/",
"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 #ff4500; color: #ff4500;
opacity: 0.8; letter-spacing: 2px;
}
#info-panel {
position: fixed; bottom: 30px; right: 20px; z-index: 10;
width: 300px;
background: rgba(0,0,0,0.9);
border: 1px solid #333;
display: none;
overflow: hidden;
}
#info-panel .panel-img {
width: 100%; height: 120px;
object-fit: cover; display: block;
opacity: 0.8;
}
#info-panel .panel-img-placeholder {
width: 100%; height: 120px;
background: #111;
display: flex; align-items: center; justify-content: center;
font-size: 2em; color: #333;
}
#info-panel .panel-body { padding: 12px 16px; }
#info-panel h3 { font-size: 0.85em; color: #fff; margin-bottom: 6px; }
#info-panel .group-badge {
display: inline-block; padding: 2px 8px;
font-size: 0.65em; letter-spacing: 2px; margin-bottom: 8px;
}
#info-panel p { font-size: 0.72em; color: #888; line-height: 1.5; }
#info-panel .close {
position: absolute; top: 8px; right: 10px; z-index: 1;
cursor: pointer; color: #fff; font-size: 0.8em;
background: rgba(0,0,0,0.6); border: none; font-family: inherit;
padding: 2px 6px;
}
#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;
}
#legend {
position: fixed; bottom: 30px; left: 20px; z-index: 10;
background: rgba(0,0,0,0.8); border: 1px solid #222;
padding: 12px 16px;
}
#legend h4 { font-size: 0.6em; color: #444; margin-bottom: 8px; letter-spacing: 2px; }
.legend-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.legend-img { width: 20px; height: 20px; object-fit: cover; opacity: 0.7; }
.legend-sphere { width: 10px; height: 10px; border-radius: 50%; }
.legend-label { font-size: 0.62em; color: #666; }
</style>
</head>
<body>
<div id="graph"></div>
<div id="header">
<h1>FLUJOS</h1>
<span class="tag">IMAGE NODES</span>
<span class="tag">THREE.Sprite + TextureLoader</span>
</div>
<div id="info-panel">
<button class="close" onclick="document.getElementById('info-panel').style.display='none'"></button>
<img id="panel-img" class="panel-img" src="" alt="" style="display:none">
<div id="panel-placeholder" class="panel-img-placeholder"></div>
<div class="panel-body">
<h3 id="node-title"></h3>
<span class="group-badge" id="node-group"></span>
<p id="node-content"></p>
</div>
</div>
<div id="legend">
<h4>NODOS CON IMAGEN</h4>
<div class="legend-row">
<img class="legend-img" src="/images/flujos_logo.png" alt="">
<span class="legend-label">FLUJOS (core)</span>
</div>
<div class="legend-row">
<img class="legend-img" src="/images/flujos3.jpg" alt="">
<span class="legend-label">Cambio Climático</span>
</div>
<div class="legend-row">
<img class="legend-img" src="/images/journalist_fondo.jpg" alt="">
<span class="legend-label">Libertad de Prensa</span>
</div>
<h4 style="margin-top:8px">NODOS ESFERA</h4>
<div class="legend-row">
<div class="legend-sphere" style="background:#ff69b4"></div>
<span class="legend-label">security</span>
</div>
<div class="legend-row">
<div class="legend-sphere" style="background:#ffdc00"></div>
<span class="legend-label">corporate</span>
</div>
<div class="legend-row">
<div class="legend-sphere" style="background:#4488ff"></div>
<span class="legend-label">politics</span>
</div>
<div class="legend-row">
<div class="legend-sphere" style="background:#cc44ff"></div>
<span class="legend-label">data</span>
</div>
</div>
<div id="technique-label">
nodeThreeObject(node =&gt; Sprite)<br>
new THREE.TextureLoader().load(img)<br>
new THREE.SpriteMaterial({ map })<br>
— THREE.js Sprite billboard —
</div>
<script type="module">
import ForceGraph3D from '3d-force-graph';
import * as THREE from 'three';
// Nodes with `img` get rendered as image sprites.
// Nodes without `img` fall back to a colored sphere (default).
const GROUP_COLORS = {
core: '#39ff14',
climate: '#ff4500',
security: '#ff69b4',
journalism: '#00fff2',
corporate: '#ffdc00',
politics: '#4488ff',
data: '#cc44ff'
};
// Images are served from the project's /images/ folder
const MOCK_DATA = {
nodes: [
{ id: 'FLUJOS', group: 'core', img: '/images/flujos_logo.png', size: 22, content: 'Sistema de visualización de flujos de información global' },
{ id: 'Cambio Climático', group: 'climate', img: '/images/flujos3.jpg', size: 18, content: 'Crisis climática y sus efectos sociopolíticos' },
{ id: 'Seguridad Intl.', group: 'security', img: '/images/flujos4.jpg', size: 16, content: 'Geopolítica y conflictos armados globales' },
{ id: 'Libertad de Prensa', group: 'journalism', img: '/images/journalist_fondo.jpg', size: 16, content: 'Estado global de la libertad periodística' },
{ id: 'Eco-Corporativo', group: 'corporate', img: '/images/flujos7.jpg', size: 16, content: 'Poder corporativo e influencia política' },
{ id: 'Populismo', group: 'politics', img: '/images/flujos8.jpg', size: 16, content: 'Auge de movimientos populistas globales' },
{ id: 'Wikipedia', group: 'data', img: '/images/flujos_logo3.png', size: 14, content: 'Enciclopedia libre como fuente de datos' },
// Leaf nodes — rendered as default colored spheres
{ id: 'Emisiones CO₂', group: 'climate', content: 'Emisiones industriales de CO₂' },
{ id: 'Energía Renovable', group: 'climate', content: 'Transición a fuentes sostenibles' },
{ id: 'Pérdida Biodiversidad',group: 'climate', content: 'Extinción de especies y ecosistemas' },
{ id: 'Ciberseguridad', group: 'security', content: 'Ataques cibernéticos estatales' },
{ id: 'Vigilancia Masiva', group: 'security', content: 'Programas de espionaje gubernamental' },
{ id: 'Privacidad Datos', group: 'security', content: 'Derechos digitales y datos personales' },
{ id: 'Desinformación', group: 'journalism', content: 'Fake news y manipulación informativa' },
{ id: 'Whistleblowers', group: 'journalism', content: 'Snowden, Assange, Panama Papers...' },
{ id: 'Big Tech', group: 'corporate', content: 'Monopolios: Google, Meta, Amazon...' },
{ id: 'Paraísos Fiscales', group: 'corporate', content: 'Evasión fiscal corporativa offshore' },
{ id: 'Lobbying', group: 'corporate', content: 'Grupos de presión legislativa' },
{ id: 'Elecciones', group: 'politics', content: 'Procesos electorales e interferencia' },
{ id: 'Migración', group: 'politics', content: 'Crisis migratoria y fronteras' },
{ id: 'Extremismo', group: 'politics', content: 'Radicalización y movimientos extremistas' },
{ id: 'Redes Sociales', group: 'data', content: 'Plataformas sociales como vectores' },
{ id: 'IA & Algoritmos', group: 'data', content: 'IA, sesgos y control algorítmico' },
{ id: 'Torrents & P2P', group: 'data', content: 'Distribución descentralizada de info' },
],
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: 'Energía Renovable', target: 'Big Tech', value: 65 },
{ 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: 'Big Tech', value: 75 },
{ 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: 'Desinformación', target: 'Extremismo', value: 80 },
{ 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 },
{ source: 'Wikipedia', target: 'Desinformación', value: 72 },
]
};
const textureCache = {};
const loader = new THREE.TextureLoader();
function getTexture(url) {
if (!textureCache[url]) {
const tex = loader.load(url);
tex.colorSpace = THREE.SRGBColorSpace;
textureCache[url] = tex;
}
return textureCache[url];
}
const elem = document.getElementById('graph');
const Graph = ForceGraph3D()(elem)
.backgroundColor('#000000')
.nodeLabel(node => `<span style="font-family:Fira Code,monospace;color:${GROUP_COLORS[node.group]};font-size:12px">${node.id}</span>`)
.nodeColor(node => GROUP_COLORS[node.group] || '#ffffff')
.nodeVal(node => node.size ? node.size / 3 : 1.2)
.linkColor(() => 'rgba(57,255,20,0.25)')
.linkWidth(link => (link.value || 50) / 55)
.nodeThreeObject(node => {
if (!node.img) return null; // use default sphere for leaf nodes
const texture = getTexture(node.img);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(material);
const s = node.size || 12;
sprite.scale.set(s, s);
return sprite;
})
.onNodeClick(node => {
const panel = document.getElementById('info-panel');
document.getElementById('node-title').textContent = node.id;
const badge = document.getElementById('node-group');
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('node-content').textContent = node.content || '';
const imgEl = document.getElementById('panel-img');
const placeholder = document.getElementById('panel-placeholder');
if (node.img) {
imgEl.src = node.img;
imgEl.style.display = 'block';
placeholder.style.display = 'none';
} else {
imgEl.style.display = 'none';
placeholder.style.display = 'flex';
}
panel.style.display = 'block';
})
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
.graphData(MOCK_DATA);
Graph.d3Force('charge').strength(-200);
setTimeout(() => Graph.zoomToFit(600, 80), 1500);
window.addEventListener('resize', () => {
Graph.width(elem.clientWidth).height(elem.clientHeight);
});
</script>
</body>
</html>

View file

@ -0,0 +1,346 @@
<!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 =&gt; 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>

View file

@ -0,0 +1,242 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>FLUJOS — Demo: Text Nodes</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 #39ff14; color: #39ff14;
opacity: 0.7; letter-spacing: 2px;
}
#info-panel {
position: fixed; bottom: 30px; right: 20px; z-index: 10;
width: 280px;
background: rgba(0,0,0,0.85);
border: 1px solid #39ff14;
padding: 16px;
display: none;
}
#info-panel h3 { font-size: 0.9em; color: #39ff14; margin-bottom: 8px; }
#info-panel .group-badge {
display: inline-block; padding: 2px 8px;
font-size: 0.7em; letter-spacing: 2px;
margin-bottom: 10px;
}
#info-panel p { font-size: 0.75em; color: #aaa; line-height: 1.5; }
#info-panel .close {
position: absolute; top: 8px; right: 10px;
cursor: pointer; color: #39ff14; font-size: 0.8em;
background: none; border: none; font-family: inherit;
}
#legend {
position: fixed; bottom: 30px; left: 20px; z-index: 10;
background: rgba(0,0,0,0.8); border: 1px solid #333;
padding: 12px 16px;
}
#legend h4 { font-size: 0.65em; color: #555; margin-bottom: 8px; letter-spacing: 2px; }
.legend-item { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; }
.legend-label { font-size: 0.65em; color: #888; }
#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;
}
</style>
</head>
<body>
<div id="graph"></div>
<div id="header">
<h1>FLUJOS</h1>
<span class="tag">TEXT NODES</span>
<span class="tag">three-spritetext</span>
</div>
<div id="info-panel">
<button class="close" onclick="document.getElementById('info-panel').style.display='none'"></button>
<h3 id="node-title"></h3>
<span class="group-badge" id="node-group"></span>
<p id="node-content"></p>
</div>
<div id="legend">
<h4>GRUPOS</h4>
<div class="legend-item"><div class="legend-dot" style="background:#39ff14"></div><span class="legend-label">core</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#ff4500"></div><span class="legend-label">climate</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#ff69b4"></div><span class="legend-label">security</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#00fff2"></div><span class="legend-label">journalism</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#ffdc00"></div><span class="legend-label">corporate</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#4488ff"></div><span class="legend-label">politics</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#cc44ff"></div><span class="legend-label">data</span></div>
</div>
<div id="technique-label">
nodeThreeObject(node =&gt; SpriteText)<br>
nodeThreeObjectExtend(true)<br>
— three-spritetext —
</div>
<script type="module">
import ForceGraph3D from '3d-force-graph';
import SpriteText from 'three-spritetext';
const GROUP_COLORS = {
core: '#39ff14',
climate: '#ff4500',
security: '#ff69b4',
journalism: '#00fff2',
corporate: '#ffdc00',
politics: '#4488ff',
data: '#cc44ff'
};
const MOCK_DATA = {
nodes: [
{ id: 'FLUJOS', group: 'core', content: 'Sistema de visualización de flujos de información global' },
{ id: 'Cambio Climático', group: 'climate', content: 'Crisis climática y sus efectos sociopolíticos a escala global' },
{ id: 'Emisiones CO₂', group: 'climate', content: 'Emisiones de dióxido de carbono por sector industrial' },
{ id: 'Energía Renovable', group: 'climate', content: 'Transición energética hacia fuentes sostenibles' },
{ id: 'Pérdida Biodiversidad',group: 'climate', content: 'Extinción masiva de especies y colapso de ecosistemas' },
{ id: 'Seguridad Intl.', group: 'security', content: 'Geopolítica, conflictos armados y alianzas militares' },
{ id: 'Ciberseguridad', group: 'security', content: 'Ataques cibernéticos estatales y crimen organizado digital' },
{ id: 'Vigilancia Masiva', group: 'security', content: 'Programas de espionaje gubernamental (NSA, GCHQ...)' },
{ id: 'Privacidad Datos', group: 'security', content: 'Derechos digitales y protección de datos personales' },
{ id: 'Libertad de Prensa', group: 'journalism', content: 'Estado global de la libertad periodística' },
{ id: 'Desinformación', group: 'journalism', content: 'Fake news, propaganda y manipulación informativa' },
{ id: 'Whistleblowers', group: 'journalism', content: 'Filtraciones: Snowden, Assange, Panama Papers...' },
{ id: 'Periodismo de Datos', group: 'journalism', content: 'Investigación basada en datos abiertos y scraping' },
{ id: 'Eco-Corporativo', group: 'corporate', content: 'Poder corporativo y su influencia en política' },
{ id: 'Big Tech', group: 'corporate', content: 'Monopolios: Google, Meta, Amazon, Apple, Microsoft' },
{ id: 'Paraísos Fiscales', group: 'corporate', content: 'Evasión fiscal corporativa offshore' },
{ id: 'Lobbying', group: 'corporate', content: 'Grupos de presión e influencia legislativa' },
{ id: 'Populismo', group: 'politics', content: 'Auge de movimientos populistas globales' },
{ id: 'Elecciones', group: 'politics', content: 'Procesos electorales e interferencia exterior' },
{ id: 'Migración', group: 'politics', content: 'Crisis migratoria y políticas de fronteras' },
{ id: 'Extremismo', group: 'politics', content: 'Radicalización online y movimientos extremistas' },
{ id: 'Wikipedia', group: 'data', content: 'Enciclopedia libre como fuente de datos estructurados' },
{ id: 'Redes Sociales', group: 'data', content: 'Plataformas sociales como vectores de información' },
{ id: 'IA & Algoritmos', group: 'data', content: 'Inteligencia artificial, sesgos y control algorítmico' },
{ id: 'Torrents & P2P', group: 'data', content: 'Redes de distribución descentralizada de información' },
],
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: 'Energía Renovable', target: 'Big Tech', value: 65 },
{ 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: 'Big Tech', value: 75 },
{ 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: 'Libertad de Prensa',target: 'Periodismo de Datos', value: 82 },
{ source: 'Desinformación', target: 'Redes Sociales', value: 90 },
{ source: 'Desinformación', target: 'Elecciones', value: 85 },
{ source: 'Desinformación', target: 'Extremismo', value: 80 },
{ source: 'Whistleblowers', target: 'Ciberseguridad', value: 72 },
{ source: 'Periodismo de Datos',target:'Wikipedia', value: 80 },
{ 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: 'Privacidad Datos', value: 85 },
{ 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 Graph = ForceGraph3D()(elem)
.backgroundColor('#000000')
.nodeLabel(node => `<span style="font-family:Fira Code,monospace;color:${GROUP_COLORS[node.group]}">${node.id}</span>`)
.nodeColor(node => GROUP_COLORS[node.group] || '#ffffff')
.nodeVal(node => node.group === 'core' ? 4 : 1.5)
.linkColor(link => {
const val = link.value || 50;
const alpha = Math.round((val / 100) * 200).toString(16).padStart(2, '0');
return `#39ff14${alpha}`;
})
.linkOpacity(0.4)
.linkWidth(link => (link.value || 50) / 60)
.nodeThreeObject(node => {
const sprite = new SpriteText(node.id);
sprite.material.depthWrite = false;
sprite.color = GROUP_COLORS[node.group] || '#ffffff';
sprite.textHeight = node.group === 'core' ? 5 : 3.5;
sprite.backgroundColor = 'rgba(0,0,0,0.4)';
sprite.padding = 2;
return sprite;
})
.nodeThreeObjectExtend(true)
.onNodeClick(node => {
const panel = document.getElementById('info-panel');
document.getElementById('node-title').textContent = node.id;
const badge = document.getElementById('node-group');
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('node-content').textContent = node.content || '';
panel.style.display = 'block';
})
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
.graphData(MOCK_DATA);
Graph.d3Force('charge').strength(-180);
setTimeout(() => Graph.zoomToFit(600, 80), 1200);
window.addEventListener('resize', () => {
Graph.width(elem.clientWidth).height(elem.clientHeight);
});
</script>
</body>
</html>

View file

@ -0,0 +1,385 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>FLUJOS — Demo: Wikipedia Images</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/",
"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.95) 0%, transparent 100%);
display: flex; align-items: center; gap: 14px;
}
#header h1 { font-size: 1em; color: #39ff14; letter-spacing: 4px; text-shadow: 0 0 10px #39ff14; }
.tag { font-size: 0.6em; padding: 3px 8px; border: 1px solid #ff4500; color: #ff4500; letter-spacing: 2px; }
.tag2 { font-size: 0.6em; padding: 3px 8px; border: 1px solid #555; color: #555; letter-spacing: 2px; }
/* Panel de detalle */
#detail {
position: fixed; bottom: 0; right: 0; z-index: 10;
width: 340px;
background: rgba(0,0,0,0.95);
border-left: 1px solid #222;
border-top: 1px solid #222;
display: none;
flex-direction: column;
max-height: 60vh;
}
#detail img {
width: 100%; max-height: 200px;
object-fit: cover; display: block;
}
#detail .detail-body { padding: 14px 16px; overflow-y: auto; }
#detail h3 { font-size: 0.8em; color: #fff; margin-bottom: 6px; line-height: 1.4; }
#detail .badge {
display: inline-block; font-size: 0.6em; padding: 2px 8px;
border: 1px solid #ff4500; color: #ff4500; letter-spacing: 2px;
margin-bottom: 10px;
}
#detail p { font-size: 0.7em; color: #777; line-height: 1.6; margin-bottom: 8px; }
#detail .meta { font-size: 0.62em; color: #444; line-height: 1.8; }
#detail .meta span { color: #666; }
#detail .close {
position: absolute; top: 8px; right: 10px;
background: rgba(0,0,0,0.7); border: none;
color: #555; font-family: inherit; font-size: 0.8em;
cursor: pointer; padding: 2px 6px; z-index: 1;
}
#detail .close:hover { color: #fff; }
/* Stats */
#stats {
position: fixed; bottom: 20px; left: 20px; z-index: 10;
font-size: 0.62em; color: #333; line-height: 2;
}
#stats span { color: #555; }
/* Tooltip custom */
.node-tooltip {
position: fixed; z-index: 20; pointer-events: none;
background: rgba(0,0,0,0.9); border: 1px solid #333;
padding: 6px 10px; font-size: 0.7em; color: #fff;
max-width: 180px; display: none;
}
</style>
</head>
<body>
<div id="graph"></div>
<div id="header">
<h1>FLUJOS</h1>
<span class="tag">CAMBIO CLIMÁTICO</span>
<span class="tag2">Wikipedia Images · 15 nodos</span>
</div>
<div id="detail">
<button class="close" onclick="document.getElementById('detail').style.display='none'"></button>
<img id="d-img" src="" alt="">
<div class="detail-body">
<h3 id="d-title"></h3>
<span class="badge">CAMBIO CLIMÁTICO</span>
<p id="d-desc"></p>
<div class="meta">
<div>ARTÍCULO &nbsp;<span id="d-articulo"></span></div>
<div>LICENCIA &nbsp;<span id="d-licencia"></span></div>
<div>AUTOR &nbsp;&nbsp;&nbsp;<span id="d-autor"></span></div>
<div>TAMAÑO &nbsp;&nbsp;<span id="d-size"></span></div>
</div>
</div>
</div>
<div id="stats">
<div>NODOS &nbsp;<span>15</span></div>
<div>TEMA &nbsp;&nbsp;<span>cambio climático</span></div>
<div>FUENTE <span>Wikipedia / Wikimedia Commons</span></div>
</div>
<div id="node-tooltip" class="node-tooltip"></div>
<script type="module">
import ForceGraph3D from '3d-force-graph';
import * as THREE from 'three';
// ── Datos: imágenes de Wikipedia sobre cambio climático ─────────────────────
const WIKI_NODES = [
{
id: "cambio_climático_000",
archivo: "cambio_climático_000.jpg",
img: "/images/wiki/cambio_climático_000.jpg",
titulo: "Vista aérea del hielo en Nunavut",
articulo: "Cambio climático",
descripcion: "Vista aérea del borde del hielo en Nunavut, Canadá. Representa el deshielo polar como consecuencia del cambio climático.",
autor: "Doc Searls",
licencia: "CC BY-SA 2.0",
width: 3504, height: 2336,
grupo: "fotografia",
},
{
id: "cambio_climático_001",
archivo: "cambio_climático_001.png",
img: "/images/wiki/cambio_climático_001.png",
titulo: "Variaciones históricas de CO₂",
articulo: "Cambio climático",
descripcion: "Historia y futuro de la concentración de CO₂ en la atmósfera. Escala logarítmica mostrando los últimos 100 millones de años.",
autor: "Hannes Grobe",
licencia: "CC BY-SA 2.5",
width: 1155, height: 806,
grupo: "grafico",
},
{
id: "cambio_climático_002",
archivo: "cambio_climático_002.png",
img: "/images/wiki/cambio_climático_002.png",
titulo: "Factores del cambio climático",
articulo: "Cambio climático",
descripcion: "Esquema ilustrativo de los principales factores que afectan al cambio climático.",
autor: "Medium69 / Ortisa",
licencia: "CC BY-SA 4.0",
width: 600, height: 582,
grupo: "esquema",
},
{
id: "cambio_climático_003",
archivo: "cambio_climático_003.jpg",
img: "/images/wiki/cambio_climático_003.jpg",
titulo: "Consenso científico sobre el clima",
articulo: "Cambio climático",
descripcion: "Gráfico de Cook et al. (2016) ilustrando los resultados de siete estudios de consenso climático.",
autor: "Skeptical Science",
licencia: "CC BY-SA 3.0",
width: 1920, height: 1080,
grupo: "grafico",
},
{
id: "cambio_climático_004",
archivo: "cambio_climático_004.png",
img: "/images/wiki/cambio_climático_004.png",
titulo: "Emisiones CO₂ y temperatura París",
articulo: "Cambio climático",
descripcion: "Emisiones globales de CO₂ y resultados probabilísticos de temperatura según los anuncios previos a la conferencia de París.",
autor: "Jae Edmonds / PNNL",
licencia: "Public domain",
width: 1920, height: 806,
grupo: "grafico",
},
{
id: "cambio_climático_005",
archivo: "cambio_climático_005.png",
img: "/images/wiki/cambio_climático_005.png",
titulo: "Escenarios de emisiones futuras",
articulo: "Cambio climático",
descripcion: "Proyecciones de emisiones globales de gases de efecto invernadero según distintos escenarios de política climática.",
autor: "Hannah Ritchie y Max Roser",
licencia: "CC BY 4.0",
width: 2041, height: 1422,
grupo: "grafico",
},
{
id: "cambio_climático_006",
archivo: "cambio_climático_006.png",
img: "/images/wiki/cambio_climático_006.png",
titulo: "Gases efecto invernadero por sector",
articulo: "Cambio climático",
descripcion: "Emisión de gases de efecto invernadero desglosada por sector económico.",
autor: "Robert A. Rohde / Rojasyesid",
licencia: "CC BY-SA 3.0",
width: 617, height: 584,
grupo: "grafico",
},
{
id: "cambio_climático_007",
archivo: "cambio_climático_007.jpg",
img: "/images/wiki/cambio_climático_007.jpg",
titulo: "Temperatura de la Corriente del Golfo",
articulo: "Cambio climático",
descripcion: "Imagen en falso color de la temperatura de la Corriente del Golfo. La corriente cálida es visible contra las aguas más frías circundantes.",
autor: "NASA / MODIS Ocean Group",
licencia: "Public domain",
width: 538, height: 566,
grupo: "satelite",
},
{
id: "cambio_climático_008",
archivo: "cambio_climático_008.png",
img: "/images/wiki/cambio_climático_008.png",
titulo: "Mapa de Pangea",
articulo: "Cambio climático",
descripcion: "Mapa del supercontinente Pangea, relevante para entender la historia climática de la Tierra a escala geológica.",
autor: "en:User:Kieff / user:tsca",
licencia: "CC BY-SA 3.0",
width: 772, height: 869,
grupo: "esquema",
},
{
id: "cambio_climático_009",
archivo: "cambio_climático_009.jpg",
img: "/images/wiki/cambio_climático_009.jpg",
titulo: "Precesión y estaciones",
articulo: "Cambio climático",
descripcion: "Diagrama de la precesión de la Tierra y su efecto sobre las estaciones, factor clave en los ciclos climáticos naturales.",
autor: "Wikimedia Commons",
licencia: "CC BY-SA 3.0",
width: 593, height: 445,
grupo: "esquema",
},
{
id: "cambio_climático_010",
archivo: "cambio_climático_010.png",
img: "/images/wiki/cambio_climático_010.png",
titulo: "Proyecciones nivel del mar",
articulo: "Cambio climático",
descripcion: "Proyecciones del aumento del nivel medio del mar según Parris et al. (2012), traducidas al español.",
autor: "Enescot / Hiperfelix",
licencia: "CC0",
width: 1355, height: 761,
grupo: "grafico",
},
{
id: "cambio_climático_011",
archivo: "cambio_climático_011.jpg",
img: "/images/wiki/cambio_climático_011.jpg",
titulo: "Palma de aceite en Riau, Sumatra",
articulo: "Cambio climático",
descripcion: "Concesión de palma de aceite en Riau, Sumatra (Indonesia). La deforestación tropical es un factor relevante en el cambio climático.",
autor: "Hayden / Flickr",
licencia: "CC BY 2.0",
width: 2048, height: 1365,
grupo: "fotografia",
},
{
id: "cambio_climático_012",
archivo: "cambio_climático_012.png",
img: "/images/wiki/cambio_climático_012.png",
titulo: "Sistema climático terrestre",
articulo: "Cambio climático",
descripcion: "Diagrama del sistema climático terrestre mostrando las interacciones entre atmósfera, hidrosfera, criosfera y biosfera.",
autor: "Martín, Rodrigo",
licencia: "CC BY-SA 3.0",
width: 1164, height: 612,
grupo: "esquema",
},
{
id: "cambio_climático_013",
archivo: "cambio_climático_013.png",
img: "/images/wiki/cambio_climático_013.png",
titulo: "Ciclos solares e irradiancia",
articulo: "Cambio climático",
descripcion: "Los últimos tres ciclos solares medidos en irradiancia solar, manchas solares, actividad de erupciones y flujo de radio.",
autor: "Robert A. Rohde",
licencia: "CC BY-SA 3.0",
width: 700, height: 466,
grupo: "grafico",
},
{
id: "cambio_climático_014",
archivo: "cambio_climático_014.jpg",
img: "/images/wiki/cambio_climático_014.jpg",
titulo: "Activismo climático juvenil (TedX)",
articulo: "Cambio climático e infancia",
descripcion: "Aayan Aggarwal durante su charla TedX sobre cambio climático e infancia en el NMIMS de Shirpur.",
autor: "Dk4588",
licencia: "CC BY-SA 4.0",
width: 791, height: 640,
grupo: "fotografia",
},
];
// ── Conexiones temáticas entre imágenes ─────────────────────────────────────
const GRUPO_COLORS = {
fotografia: '#ff4500',
grafico: '#39ff14',
esquema: '#00fff2',
satelite: '#cc44ff',
};
// Conectar imágenes del mismo grupo y artículo
const links = [];
for (let i = 0; i < WIKI_NODES.length; i++) {
for (let j = i + 1; j < WIKI_NODES.length; j++) {
const a = WIKI_NODES[i], b = WIKI_NODES[j];
if (a.articulo === b.articulo) {
links.push({ source: a.id, target: b.id, value: a.grupo === b.grupo ? 80 : 40 });
}
}
}
const graphData = { nodes: WIKI_NODES, links };
// ── Render ───────────────────────────────────────────────────────────────────
const elem = document.getElementById('graph');
const textureCache = {};
const loader = new THREE.TextureLoader();
function getTexture(url) {
if (!textureCache[url]) {
const t = loader.load(url);
t.colorSpace = THREE.SRGBColorSpace;
textureCache[url] = t;
}
return textureCache[url];
}
const Graph = ForceGraph3D()(elem)
.backgroundColor('#000000')
.nodeLabel(node => `<div style="font-family:Fira Code,monospace;font-size:11px;color:${GRUPO_COLORS[node.grupo]};background:rgba(0,0,0,0.8);padding:4px 8px;border:1px solid ${GRUPO_COLORS[node.grupo]}33">${node.titulo}</div>`)
.nodeColor(node => GRUPO_COLORS[node.grupo] || '#fff')
.nodeVal(node => {
const area = node.width * node.height;
return Math.max(1.5, Math.min(6, area / 500000));
})
.linkColor(link => {
const val = link.value || 40;
return val > 60 ? 'rgba(57,255,20,0.4)' : 'rgba(57,255,20,0.15)';
})
.linkWidth(link => link.value > 60 ? 1.2 : 0.4)
.nodeThreeObject(node => {
const texture = getTexture(node.img);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(material);
// Escalar manteniendo aspect ratio aproximado
const ratio = node.width / node.height;
const baseSize = 14;
sprite.scale.set(baseSize * Math.min(ratio, 1.6), baseSize / Math.max(ratio, 0.6));
return sprite;
})
.onNodeClick(node => {
const panel = document.getElementById('detail');
document.getElementById('d-img').src = node.img;
document.getElementById('d-title').textContent = node.titulo;
document.getElementById('d-desc').textContent = node.descripcion;
document.getElementById('d-articulo').textContent = node.articulo;
document.getElementById('d-licencia').textContent = node.licencia;
document.getElementById('d-autor').textContent = node.autor;
document.getElementById('d-size').textContent = `${node.width}×${node.height}px`;
panel.style.display = 'flex';
})
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
.graphData(graphData);
Graph.d3Force('charge').strength(-250);
setTimeout(() => Graph.zoomToFit(800, 80), 1500);
window.addEventListener('resize', () => {
Graph.width(elem.clientWidth).height(elem.clientHeight);
});
</script>
</body>
</html>

View file

@ -0,0 +1,2 @@
*
!.gitignore