tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definitions

⎇ a908d8420cc8f4e2b5c4386f1f784f0977de1747 crates/apps/gioser-web/cytoscape-graph.js JavaScript · 274 lines · 8.9 KB History Who touched what raw

gioser-web: fix graph not appearing — use MutationObserver a908d8420 · Sergio ·

1/**
2 * gioser-graph.js — Grafo semántico con Cytoscape.js
3 *
4 * Detecta automáticamente elementos <gioser-graph> agregados al DOM
5 * (incluso los creados dinámicamente por el WASM) y monta el grafo.
6 *
7 * Efecto "wineandcheesemap": clic en nodo → centra + desvanece resto.
8 * Doble clic → callback de navegación (window.__gioserGraphNavigate).
9 */
10
11(function () {
12  'use strict';
13
14  const COLORS = {
15    logos:  '#d0dbff',
16    aire:   '#d0dbff',
17    nomos:  '#f59056',
18    fuego:  '#f59056',
19    kay:    '#d49873',
20    tierra: '#d49873',
21    uku:    '#6cd0f3',
22    agua:   '#6cd0f3',
23  };
24
25  function caminoColor(c) { return COLORS[c] || '#888'; }
26
27  function initGraph(container) {
28    const apiUrl = container.getAttribute('data-api-url') || 'https://api.gioser.net';
29    const onNavigate = window.__gioserGraphNavigate || null;
30
31    // Si cytoscape no ha cargado, esperar
32    if (typeof cytoscape === 'undefined') {
33      const check = setInterval(() => {
34        if (typeof cytoscape !== 'undefined') {
35          clearInterval(check);
36          initGraph(container);
37        }
38      }, 100);
39      return;
40    }
41
42    fetch(apiUrl + '/graph?limit=500')
43      .then(function (r) { return r.json(); })
44      .then(function (data) {
45        if (!data.nodes || data.nodes.length === 0) return;
46
47        var elements = [];
48
49        for (var i = 0; i < data.nodes.length; i++) {
50          var d = data.nodes[i].data;
51          if (!d.doc_id) continue;
52          var color = caminoColor(d.camino);
53          elements.push({
54            group: 'nodes',
55            data: {
56              id: d.id,
57              doc_id: d.doc_id,
58              label: d.name.length > 24 ? d.name.slice(0, 22) + '…' : d.name,
59              camino: d.camino.toUpperCase(),
60              color: color,
61            },
62          });
63        }
64
65        var nodeIds = {};
66        for (var i = 0; i < elements.length; i++) {
67          nodeIds[elements[i].data.id] = true;
68        }
69
70        for (var i = 0; i < data.edges.length; i++) {
71          var ed = data.edges[i].data;
72          if (!nodeIds[ed.source] || !nodeIds[ed.target]) continue;
73          var weight = ed.weight || 0.7;
74          elements.push({
75            group: 'edges',
76            data: {
77              id: ed.id,
78              source: ed.source,
79              target: ed.target,
80              weight: weight,
81            },
82          });
83        }
84
85        // Guardar payload completo para tooltip
86        var tipMap = {};
87        for (var i = 0; i < data.nodes.length; i++) {
88          var d = data.nodes[i].data;
89          if (d.doc_id) tipMap[d.id] = d;
90        }
91
92        var cy = cytoscape({
93          container: container,
94          elements: elements,
95          style: [
96            {
97              selector: 'edge',
98              style: {
99                'width': 'mapData(weight, 0.5, 1.0, 0.5, 4.5)',
100                'line-color': 'rgba(255,255,255,0.16)',
101                'curve-style': 'haystack',
102                'haystack-radius': 0,
103                'opacity': 0.6,
104              },
105            },
106            {
107              selector: 'node',
108              style: {
109                'shape': 'round-rectangle',
110                'width': 130,
111                'height': 34,
112                'background-color': 'data(color)',
113                'background-opacity': 0.18,
114                'border-color': 'data(color)',
115                'border-width': 1.8,
116                'border-opacity': 0.55,
117                'color': 'rgba(232,234,245,0.88)',
118                'font-size': 11,
119                'font-family': 'Inter, system-ui, sans-serif',
120                'font-weight': 500,
121                'text-valign': 'center',
122                'text-halign': 'center',
123                'label': 'data(label)',
124                'min-zoomed-font-size': 8,
125                'transition-property': 'background-opacity, border-opacity, border-width',
126                'transition-duration': 180,
127              },
128            },
129          ],
130          layout: {
131            name: 'cose',
132            animate: false,
133            idealEdgeLength: 150,
134            nodeRepulsion: 7000,
135            gravity: 0.2,
136            numIter: 800,
137            fit: true,
138            padding: 25,
139          },
140        });
141
142        // Tooltip
143        var tooltipEl = document.createElement('div');
144        tooltipEl.className = 'cy-tooltip';
145        tooltipEl.style.cssText =
146          'position:absolute;z-index:10;pointer-events:none;' +
147          'background:rgba(6,5,13,0.90);color:#e8eaf5;' +
148          'padding:6px 10px;border-radius:8px;font-size:11px;' +
149          'font-family:Inter,sans-serif;line-height:1.4;' +
150          'border:1px solid rgba(255,255,255,0.10);' +
151          'backdrop-filter:blur(8px);max-width:220px;' +
152          'opacity:0;transition:opacity 180ms ease;';
153        container.style.position = 'relative';
154        container.appendChild(tooltipEl);
155
156        cy.on('mouseover', 'node', function (ev) {
157          var n = ev.target;
158          n.style({ 'background-opacity': 0.45, 'border-opacity': 0.9, 'border-width': 2.2 });
159          var tipData = tipMap[n.id()];
160          if (tipData && tipData.preview) {
161            tooltipEl.textContent = tipData.preview.slice(0, 130);
162            tooltipEl.style.opacity = '1';
163          }
164        });
165
166        cy.on('mouseout', 'node', function (ev) {
167          var n = ev.target;
168          n.style({ 'background-opacity': 0.18, 'border-opacity': 0.55, 'border-width': 1.8 });
169          tooltipEl.style.opacity = '0';
170        });
171
172        cy.on('mousemove', function (ev) {
173          if (tooltipEl.style.opacity === '1') {
174            var pos = ev.renderedPosition || { x: 0, y: 0 };
175            tooltipEl.style.left = (pos.x + 14) + 'px';
176            tooltipEl.style.top = (pos.y - 10) + 'px';
177          }
178        });
179
180        // Click nodo: centrar + desvanecer resto
181        cy.on('click', 'node', function (ev) {
182          var node = ev.target;
183          // Vecinos
184          cy.nodes().not(node).not(node.neighborhood()).forEach(function (n) {
185            n.style({ 'opacity': 0.12 });
186          });
187          cy.edges().forEach(function (e) {
188            e.style({ 'opacity': 0.06 });
189          });
190          node.neighborhood().nodes().forEach(function (n) {
191            n.style({ 'opacity': 1 });
192          });
193          node.style({ 'opacity': 1, 'background-opacity': 0.40, 'border-opacity': 1 });
194          node.connectedEdges().forEach(function (e) {
195            e.style({ 'opacity': 0.7 });
196          });
197          cy.animate({
198            center: { eles: node },
199            zoom: 2.5,
200            duration: 350,
201          });
202        });
203
204        // Doble clic: callback de navegación
205        cy.on('dblclick', 'node', function (ev) {
206          var docId = ev.target.data('doc_id');
207          if (onNavigate && docId) onNavigate(docId);
208        });
209
210        // Clic en fondo: restaurar todo
211        cy.on('click', function (ev) {
212          if (ev.target === cy) {
213            cy.nodes().forEach(function (n) {
214              n.style({ 'opacity': 1, 'background-opacity': 0.18, 'border-opacity': 0.55, 'border-width': 1.8 });
215            });
216            cy.edges().forEach(function (e) {
217              e.style({ 'opacity': 0.6 });
218            });
219            cy.animate({ zoom: 1, pan: { x: 0, y: 0 }, duration: 300 });
220          }
221        });
222
223        // ResizeObserver para redimensionar con el contenedor
224        var ro = new ResizeObserver(function () {
225          cy.resize().fit(25);
226        });
227        ro.observe(container);
228
229        // Scroll del deck: pausar interacciones del grafo
230        var deckEl = container.closest('.deck');
231        if (deckEl) {
232          deckEl.addEventListener('scroll', function () {
233            // No hacemos nada especial, el grafo se redimensiona solo
234          });
235        }
236      })
237      .catch(function (err) {
238        console.warn('gioser-graph: error:', err);
239        container.innerHTML =
240          '<div style="padding:1rem;text-align:center;color:rgba(232,234,245,0.30);' +
241          'font-size:0.8rem;font-family:Inter,sans-serif;">' +
242          '· grafo no disponible ·</div>';
243      });
244  }
245
246  // MutationObserver: detecta <gioser-graph> agregados en cualquier momento
247  var observer = new MutationObserver(function (mutations) {
248    for (var m = 0; m < mutations.length; m++) {
249      var added = mutations[m].addedNodes;
250      for (var i = 0; i < added.length; i++) {
251        var el = added[i];
252        if (el.tagName && el.tagName.toLowerCase() === 'gioser-graph') {
253          initGraph(el);
254        }
255        // También revisar hijos
256        var graphs = el.querySelectorAll ? el.querySelectorAll('gioser-graph') : [];
257        for (var j = 0; j < graphs.length; j++) {
258          initGraph(graphs[j]);
259        }
260      }
261    }
262  });
263
264  observer.observe(document.documentElement, {
265    childList: true,
266    subtree: true,
267  });
268
269  // También inicializar los que ya existen (si el DOM ya está listo)
270  var existing = document.querySelectorAll('gioser-graph');
271  for (var i = 0; i < existing.length; i++) {
272    initGraph(existing[i]);
273  }
274})();