const highlightNodes = new Set(); const highlightLinks = new Set(); let hoverNode = null; let Graph = null; let firstTick = true; const colorPallette = ['rgb(104, 169, 77)', 'rgb(102, 75, 154)', 'rgb(41, 171, 226)', 'rgb(224, 133, 35)', 'rgb(214, 207, 126)', 'rgb(239, 65, 35)', 'rgb(255, 255, 255)']; const edgeColors = {}; var graph_element = document.getElementById('3d-graph'); loadGraph(); async function loadGraph() { const dataUrl = plugin_path + 'datasets/aud1.json' const gData = await fetch(dataUrl).then(res => res.json()) Graph = ForceGraph3D({extraRenderers: [new THREE.CSS2DRenderer(), new THREE.CSS3DRenderer()]}) (document.getElementById('3d-graph')) .graphData(gData) .nodeLabel('id') .nodeAutoColorBy('group') .nodeColor(node => highlightNodes.has(node) ? node === hoverNode ? 'rgb(255,0,0,1)' : 'rgba(255,160,0,0.8)' : 'rgba(0,255,255,0.6)') .linkWidth(link => highlightLinks.has(link) ? 2 : 0.8) .onNodeClick(handleNodeClick) .onNodeHover(nodeHover) .onLinkHover(linkHover) .linkColor(linkColor) .linkOpacity(0.8) .nodeThreeObjectExtend(false) .nodeThreeObject(drawNode) .onEngineTick(loadComponents) .width(getWidth()) .height(getHeight()); //Graph.renderer().sortObjects = true; } function loadComponents() { if (firstTick) { mapEdgeColors(); updateLinks(); add_background(); createLinkOverlay(); createInfoOverlay(); add_fullscreen_mode_button(); add_fullscreen_Listener(); window.onresize = resize_graph(); firstTick = false; } } function resize_graph() { Graph.width(getWidth()); Graph.height(getHeight()); } function getWidth() { return document.getElementById('3d-graph').offsetWidth; } function getHeight() { return window.innerHeight - 200 } function mapEdgeColors() { const linkClasses = getLinkClasses() for (let i = 0; i < linkClasses.length; i++) { edgeColors[linkClasses[i]] = colorPallette[i % colorPallette.length] } } function getLinkClasses() { const linkClasses = []; Graph.graphData().links.forEach(link => linkClasses.push(link.type)); return [... new Set(linkClasses)]; } function getCanvasDivNode() { const domNode = document.getElementById('3d-graph'); return domNode.firstChild.firstChild.firstChild; } function drawNode(node) { // Draw node as label + image const nodeDiv = document.createElement('div'); group = new THREE.Group(); const labelDiv = document.createElement('div') labelDiv.textContent = node.name; labelDiv.style.color = node.color; labelDiv.className = 'node-label'; nodeDiv.appendChild(labelDiv); const cssobj = new THREE.CSS3DSprite(nodeDiv); cssobj.scale.set(0.25, 0.25, 0.25); cssobj.position.set(0, -6, 0); group.add(cssobj) // Draw node circle image const textureLoader = new THREE.TextureLoader(); const imageAlpha = textureLoader.load(plugin_path + 'datasets/images/alpha.png'); let imageTexture = null; if ('image' in node) { imageTexture = textureLoader.load(plugin_path + 'datasets/images/' + node.image); } else { imageTexture = textureLoader.load(plugin_path + 'datasets/images/default.jpg'); } const material = new THREE.SpriteMaterial({map: imageTexture, alphaMap: imageAlpha, transparent: true, alphaTest: 0.2, depthWrite:false, depthTest: false}); const sprite = new THREE.Sprite(material); sprite.renderOrder = 999; // This may not be optimal. But it allows us to render the sprite on top of everything else. if ('image' in node) { sprite.scale.set(20, 20); } else { sprite.scale.set(5, 5); } group.add(sprite) return group; } function createInfoOverlay() { const sceneNode = getCanvasDivNode(); const overlayNode = document.createElement('div'); overlayNode.id = 'infoOverlay' overlayNode.className = 'detail-view'; //overlayNode.innerText = 'Hello there!'; sceneNode.insertBefore(overlayNode, sceneNode.childNodes[2]); const close = document.createElement('div'); close.innerHTML = '<p>✖</p>'; close.id = 'infoOverlayCloseButton'; close.className = 'close-button'; overlayNode.appendChild(close); const topArea = document.createElement('div'); topArea.className = 'detail-view-top-area'; overlayNode.appendChild(topArea); const nodeImage = document.createElement('img'); nodeImage.id = 'infoOverlayImage'; nodeImage.src = plugin_path + 'datasets/images/default.jpg'; nodeImage.className = 'detail-view-image'; topArea.appendChild(nodeImage); const headline = document.createElement('h2'); headline.id = 'infoOverlayHeadline'; headline.innerText = 'Default Text'; headline.className = 'detail-view-headline'; topArea.appendChild(headline); const textArea = document.createElement('div'); textArea.className = 'detail-view-text-area'; overlayNode.appendChild(textArea); const description = document.createElement('p'); description.id = 'infoOverlayDescription'; description.innerText = 'Default Text' description.setAttribute("overflow-y", "scroll"); textArea.appendChild(description); const link_container = document.createElement('div'); const links = document.createElement('div'); const linkClasses = getLinkClasses(); link_container.id = 'link_container'; link_container.className = 'bottom container'; links.className = 'tab'; links.id = 'links'; for(i = 0; i < linkClasses.length; i++){ const linkType = linkClasses[i]; const tablink = document.createElement('button'); tablink.className = 'tablinks'; tablink.id = linkType + "_id"; tablink.innerHTML = linkType[0] + linkType[1] + linkType[2]; tablink.style = "background-color: " + edgeColors[linkClasses[i]]; tablink.setAttribute("onclick", "openTab(event, " + linkType + "_id)"); //unusual function call links.appendChild(tablink); } link_container.appendChild(links); for(i = 0; i < linkClasses.length; i++){ const linkType = linkClasses[i]; const tabcontent = document.createElement('div'); tabcontent.className = 'tabcontent'; tabcontent.id = linkType + "_id_content"; tabcontent.style = "background-color: " + edgeColors[linkClasses[i]]; link_container.appendChild(tabcontent); } overlayNode.appendChild(link_container); jQuery('#infoOverlayCloseButton').click(function () { jQuery('#infoOverlay').slideUp('fast'); }); //sceneNode.appendChild(overlayNode); } function createLinkOverlay() { const sceneNode = getCanvasDivNode(); const overlayNode = document.createElement('div'); overlayNode.className = 'link-overlay'; //overlayNode.innerText = 'Hello there!'; sceneNode.insertBefore(overlayNode, sceneNode.childNodes[2]) const linkClasses = getLinkClasses(); const chars = Math.max.apply(Math, linkClasses.map(function (c) { return c.length; })); for (let i=0; i<linkClasses.length; i++) { const relation = document.createElement('div'); relation.className = 'relation'; relation.innerHTML = "<p>" + linkClasses[i] + "</p>"; overlayNode.appendChild(relation); const colorStrip = document.createElement('div'); colorStrip.className = 'rel-container'; colorStrip.style = "background-color: " + edgeColors[linkClasses[i]] + "; width: " + 10 * chars + "px;"; relation.appendChild(colorStrip); } } function add_fullscreen_mode_button() { const sceneNode = document.getElementById('3d-graph'); const overlayNode = document.createElement('button'); overlayNode.className = 'fullscreen_button'; overlayNode.innerText = 'fullscreen'; overlayNode.addEventListener("click", fullscreen_mode); sceneNode.appendChild(overlayNode); } function fullscreen_mode(){ if(getCanvasDivNode().requestFullscreen) { getCanvasDivNode().requestFullscreen().catch(); } } function resize_canvas() { if(document.fullscreenElement == getCanvasDivNode()) { Graph.height(screen.height); Graph.width(screen.width); } else { Graph.height(window.innerHeight - 200); Graph.width(getWidth()); } } function add_fullscreen_Listener() { getCanvasDivNode().addEventListener("fullscreenchange", resize_canvas); } function add_background() { const sphereGeometry = new THREE.SphereGeometry(20000, 32, 32); //const planeGeometry = new THREE.PlaneGeometry(1000, 1000, 1, 1); const loader = new THREE.TextureLoader(); //const planeMaterial = new THREE.MeshLambertMaterial({color: 0xFF0000, side: THREE.DoubleSide}); //THREE.BackSide const planeMaterial = new THREE.MeshBasicMaterial({map: loader.load(plugin_path + 'backgrounds/background_3.jpg'), side: THREE.DoubleSide}); //THREE.BackSide const mesh = new THREE.Mesh(sphereGeometry, planeMaterial); mesh.position.set(0, 0, 0); //mesh.rotation.set(0.5 * Math.PI, 0, 0); Graph.scene().add(mesh); } function updateLinks() { const gData = Graph.graphData() // cross-link node objects gData.links.forEach(link => { const a = link.source; const b = link.target; if (!a.neighbors) a.neighbors = []; if (!b.neighbors) b.neighbors = []; a.neighbors.push(b); b.neighbors.push(a); if (!a.links) a.links = []; if (!b.links) b.links = []; a.links.push(link); b.links.push(link); }); Graph.graphData(gData) } function handleNodeClick(node) { focusOnNode(node); updateInfoOverlay(node); } function updateInfoOverlay(node) { jQuery('#infoOverlayHeadline').text(node.name); if ('image' in node) { jQuery('#infoOverlayImage').attr('src', plugin_path + 'datasets/images/' + node.image); } else { jQuery('#infoOverlayImage').attr('src', plugin_path + 'datasets/images/default.jpg'); } if ('description' in node) { jQuery('#infoOverlayDescription').text(node.description); } else { jQuery('#infoOverlayDescription').text('Default Text'); } updateTabs(node); jQuery('#infoOverlay').slideDown('fast'); } function updateTabs(node){ //tabs of the links to other nodes: //delete old nodes from tabcontent tabcontent = document.getElementsByClassName("tabcontent"); for (i = 0; i < tabcontent.length; i++) { while (tabcontent[i].firstChild){ tabcontent[i].removeChild(tabcontent[i].lastChild); } } //add new nodes to tabcontent for (i = 0; i < tabcontent.length; i++) { //for every type of link node.links.forEach(link => { //and for every link if(link.type + '_id_content' == tabcontent[i].id){ //is checked if the type is equal const linkButton = document.createElement('div'); //and if so a new element is created linkButton.className = "link img"; var node2; if(link.source == node){ node2 = link.target; } else if(link.target == node){ node2 = link.source; } linkButton.id = "linkButton_" + node2.id; const nodeID = node2.id; const linkImage = document.createElement('img'); const linkTitle = document.createElement('div'); if ('image' in node2) { linkImage.src = plugin_path + 'datasets/images/' + node2.image; } else if(!('image' in node2) && ('id' in node2)) { linkImage.src = plugin_path + 'datasets/images/default.jpg'; } linkTitle.className = "link title"; linkTitle.innerHTML = node2.name; //linkButton.appendChild(linkTitle); linkButton.appendChild(linkImage); linkButton.addEventListener('click', function (){focusOnNodeId(nodeID)}); tabcontent[i].appendChild(linkButton); } }); } } function focusOnNodeId(id) { const gData = Graph.graphData(); gData.nodes.forEach(node => { if (node.id === id) { handleNodeClick(node); } }) } function focusOnNode(node) { // Aim at node from outside it const distance = 250; const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z); Graph.cameraPosition( { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }, // new position node, // lookAt ({ x, y, z }) 1000 // ms transition duration ); } function nodeHover(node) { // no state change if ((!node && !highlightNodes.size) || (node && hoverNode === node)) return; highlightNodes.clear(); highlightLinks.clear(); if (node) { highlightNodes.add(node); node.neighbors.forEach(neighbor => highlightNodes.add(neighbor)); node.links.forEach(link => highlightLinks.add(link)); } hoverNode = node || null; updateHighlight(); } function linkHover(link) { highlightNodes.clear(); highlightLinks.clear(); if (link) { highlightLinks.add(link); highlightNodes.add(link.source); highlightNodes.add(link.target); } updateHighlight(); } function linkColor(link) { if ('type' in link) { return edgeColors[link.type] } return 'rgb(255, 255, 255)' } function updateHighlight() { // trigger update of highlighted objects in scene Graph .nodeColor(Graph.nodeColor()) .linkWidth(Graph.linkWidth()) .linkDirectionalParticles(Graph.linkDirectionalParticles()); } //is used in createInfoOverlay, as an unusual function call to open link-tabs function openTab (event, tab) { var i, tabcontent, tablinks; // Get all elements with class="tabcontent" and hide them tabcontent = document.getElementsByClassName("tabcontent"); for (i = 0; i < tabcontent.length; i++) { tabcontent[i].style.display = "none"; } // Get all elements with class="tablinks" and remove the class "active" tablinks = document.getElementsByClassName("tablinks"); for (i = 0; i < tablinks.length; i++) { tablinks[i].className = tablinks[i].className.replace(" active", ""); } // Show the current tab, and add an "active" class to the button that opened the tab document.getElementById(tab.id + "_content").style.display = "block"; event.currentTarget.className += " active"; }