|
12 | 12 | <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> |
13 | 13 | <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
14 | 14 | <script src="https://unpkg.com/@tailwindcss/browser@4"></script> |
15 | | - <script src="./data.js"></script> |
| 15 | + <!-- <script src="./data.js"></script> --> |
16 | 16 | <style type="text/tailwindcss"> |
17 | 17 | @theme { |
18 | 18 | --color-primary-50: #faf5ff; |
|
32 | 32 | // API base path |
33 | 33 | const API_BASE = window.location.origin; |
34 | 34 |
|
35 | | - // Local data testing helper |
36 | | - // To validate with local data: |
37 | | - // 2) Set USE_LOCAL_DATA to true below |
38 | | - const USE_LOCAL_DATA = true; // default off |
39 | | - |
40 | 35 | // Configuration constants |
41 | 36 | const COLORS = [ |
42 | 37 | "#F6BD16", |
|
113 | 108 |
|
114 | 109 | // API call functions |
115 | 110 | const fetchDatabaseInfo = async () => { |
116 | | - if (USE_LOCAL_DATA && typeof datas !== "undefined") { |
| 111 | + if (typeof datas !== "undefined") { |
117 | 112 | return Promise.resolve(datas.database); |
118 | 113 | } |
119 | 114 | const response = await fetch(`${API_BASE}/api/database/info`); |
|
127 | 122 | sortBy, |
128 | 123 | sortOrder |
129 | 124 | ) => { |
130 | | - if (USE_LOCAL_DATA && typeof datas !== "undefined") { |
| 125 | + if (typeof datas !== "undefined") { |
131 | 126 | const toLower = (v) => (v ?? "").toString().toLowerCase(); |
132 | 127 | let list = Array.isArray(datas.vertices) ? datas.vertices : []; |
133 | 128 |
|
|
176 | 171 | }; |
177 | 172 |
|
178 | 173 | const fetchGraphData = async (vertexId) => { |
179 | | - if (USE_LOCAL_DATA && typeof datas !== "undefined") { |
| 174 | + if (typeof datas !== "undefined") { |
180 | 175 | const entry = datas.graphs[vertexId]; |
181 | 176 | if (entry && entry.vertices && entry.edges) { |
182 | 177 | return Promise.resolve({ |
|
360 | 355 | connectedNodes.has(node.id) |
361 | 356 | ); |
362 | 357 | } else { |
363 | | - // Hyper mode: use bubble-sets plugin |
| 358 | + // Hyper mode: render 2-node entries as normal edges, others as bubble-sets |
| 359 | + const edgeSet = new Set(); |
364 | 360 | edgeEntries.forEach(([key, edge], i) => { |
365 | 361 | const nodes = key.split(EDGE_SEPARATOR); |
366 | 362 |
|
| 363 | + if (nodes.length === 2) { |
| 364 | + const [a, b] = nodes; |
| 365 | + const edgeId = a < b ? `${a}-${b}` : `${b}-${a}`; |
| 366 | + if (!edgeSet.has(edgeId)) { |
| 367 | + edgeSet.add(edgeId); |
| 368 | + hyperData.edges.push({ |
| 369 | + id: edgeId, |
| 370 | + source: a, |
| 371 | + target: b, |
| 372 | + ...edge, |
| 373 | + }); |
| 374 | + } |
| 375 | + return; |
| 376 | + } |
| 377 | + |
367 | 378 | plugins.push({ |
368 | 379 | key: `bubble-sets-${key}`, |
369 | 380 | type: "bubble-sets", |
|
380 | 391 | members: nodes, |
381 | 392 | }); |
382 | 393 | }); |
| 394 | + |
| 395 | + // Assign cluster by hyperEdges for layout grouping |
| 396 | + // Pick the heaviest hyperedge containing the node as its primary cluster |
| 397 | + const nodeIdToCandidateClusters = new Map(); |
| 398 | + hyperData.hyperEdges.forEach((he) => { |
| 399 | + const weight = |
| 400 | + he.weight || |
| 401 | + (Array.isArray(he.members) ? he.members.length : 1); |
| 402 | + (he.members || []).forEach((m) => { |
| 403 | + const list = nodeIdToCandidateClusters.get(m) || []; |
| 404 | + list.push({ id: he.id, weight }); |
| 405 | + nodeIdToCandidateClusters.set(m, list); |
| 406 | + }); |
| 407 | + }); |
| 408 | + hyperData.nodes = hyperData.nodes.map((n) => { |
| 409 | + const candidates = nodeIdToCandidateClusters.get(n.id) || []; |
| 410 | + if (candidates.length === 0) return n; |
| 411 | + const primary = candidates.reduce((best, cur) => |
| 412 | + cur.weight > best.weight ? cur : best |
| 413 | + ); |
| 414 | + return { ...n, cluster: primary.id }; |
| 415 | + }); |
383 | 416 | } |
384 | 417 |
|
385 | 418 | // Add tooltip plugin |
|
427 | 460 | node: { |
428 | 461 | palette: { field: "cluster" }, |
429 | 462 | style: { |
430 | | - size: isGraph ? 20 : 25, |
| 463 | + size: hyperData.nodes.length > LAYOUT_THRESHOLD ? 15 : 20, |
431 | 464 | labelText: (d) => d.id, |
432 | 465 | fill: (d) => getNodeColor(d, selectedVertex, entityTypeColors), |
433 | 466 | }, |
434 | 467 | }, |
435 | 468 | edge: { |
436 | 469 | style: { |
437 | 470 | size: isGraph ? 3 : 2, |
438 | | - stroke: isGraph ? "#a68fff" : undefined, |
439 | | - lineWidth: isGraph ? 2 : undefined, |
| 471 | + stroke: "#a68fff", |
| 472 | + lineWidth: 1, |
440 | 473 | }, |
441 | 474 | }, |
442 | 475 | layout: { |
|
446 | 479 | : "force", |
447 | 480 | clustering: !isGraph, |
448 | 481 | preventOverlap: true, |
449 | | - nodeClusterBy: isGraph ? undefined : "entity_type", |
| 482 | + nodeClusterBy: isGraph ? undefined : "cluster", |
450 | 483 | gravity: 20, |
451 | 484 | linkDistance: isGraph ? 100 : 150, |
452 | 485 | }, |
|
0 commit comments