diff --git a/datasets/aud1.json b/datasets/aud1.json index a55efdc2527ba30a801b1ae690ece920fff3193a..9203bc57b1e5e86f3e95245f71cec9772e048bf7 100644 --- a/datasets/aud1.json +++ b/datasets/aud1.json @@ -26,7 +26,7 @@ "id": "id6", "name": "Eulertouren", "image": "eulertouren.png", - "description": "Ein Eulerkreis ist in der Graphentheorie ein Zyklus, der alle Kanten eines Graphen genau einmal enthält. Ein offener Eulerzug (auch Eulerpfad oder Eulerweg) ist gegeben, wenn Start- und Endknoten nicht gleich sein müssen, wenn also statt eines Zyklus lediglich eine Kantenfolge verlangt wird, welche jede Kante des Graphen genau einmal enthält. Ein bekanntes Beispiel ist das Haus vom Nikolaus." + "description": "Ein Eulerkreis ist in der Graphentheorie ein Zyklus, der alle Kanten eines Graphen genau einmal enthält. Ein offener Eulerzug (auch Eulerpfad oder Eulerweg) ist gegeben, wenn Start- und Endknoten nicht gleich sein müssen, wenn also statt eines Zyklus lediglich eine Kantenfolge verlangt wird, welche jede Kante des Graphen genau einmal enthält. Ein bekanntes Beispiel ist das Haus vom Nikolaus. Dieser Text braucht noch eine Zeile, damit er gescrollt werden muss." }, { "id": "id7", diff --git a/display/graph.js b/display/graph.js index 7f975db61ddae74f302a4038e450c9799711824b..da72f0be68e505982f9f36b3878aabea8b895ba0 100644 --- a/display/graph.js +++ b/display/graph.js @@ -2,6 +2,7 @@ class Graph { constructor(dataUrl) { this.graph = null; + this.gData = null; this.highlightNodes = new Set(); this.highlightLinks = new Set(); @@ -12,15 +13,17 @@ class Graph { this.firstTick = true; this.infooverlay = null; + this.edgeTypeVisibility = {}; + this.loadGraph(dataUrl); } async loadGraph(dataUrl) { - const gData = await fetch(dataUrl).then(res => res.json()) + this.gData = await fetch(dataUrl).then(res => res.json()) this.graph = ForceGraph3D({extraRenderers: [new THREE.CSS2DRenderer(), new THREE.CSS3DRenderer()]}) (document.getElementById('3d-graph')) - .graphData(gData) + .graphData(this.gData) .nodeLabel('id') .nodeAutoColorBy('group') .nodeColor(node => this.getNodeColor(node)) @@ -46,7 +49,7 @@ class Graph { initializeModel() { if (this.firstTick) { this.mapEdgeColors(); - this.updateLinks(); + this.updateNodeData(); this.updateNodeMap(); this.addBackground(); @@ -56,6 +59,7 @@ class Graph { document.addEventListener("fullscreenchange", () => this.resize()); window.addEventListener("resize", () => this.resize()); + this.getLinkClasses().forEach(item => this.edgeTypeVisibility[item] = true); this.firstTick = false; } } @@ -125,19 +129,64 @@ class Graph { ); } - updateLinks() { + toggleLinkVisibility(type) { + if (this.edgeTypeVisibility[type]) { + this.hideLinkType(type); + } else { + this.showLinkType(type); + } + } + + hideLinkType(type) { + this.edgeTypeVisibility[type] = false; + this.updateGraphData(); + this.updateNodeData(); + this.removeFloatingNodes(); + } + + showLinkType(type) { + this.edgeTypeVisibility[type] = true; + this.updateGraphData(); + this.updateNodeData(); + this.removeFloatingNodes(); + } + + removeFloatingNodes() { + const gData = this.graph.graphData(); + const nodes = gData.nodes.filter(node => node.neighbors.length > 0); + const data = { + nodes: nodes, + links: gData.links + }; + this.graph.graphData(data); + } + + updateGraphData() { + const data = { + nodes: this.gData.nodes, + links: this.gData.links.filter(l => this.edgeTypeVisibility[l.type]) + }; + this.graph.graphData(data); + } + + resetNodeData() { + const gData = this.graph.graphData(); + for (const node of gData.nodes) { + node.neighbors = []; + node.links = []; + } + } + + updateNodeData() { const gData = this.graph.graphData(); // cross-link node objects + this.resetNodeData(); + 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); }); @@ -160,13 +209,10 @@ class Graph { } resize() { - console.log(document.fullscreenElement); if(document.fullscreenElement == getCanvasDivNode()) { - console.log("Resizing to fullscreen"); this.graph.height(screen.height); this.graph.width(screen.width); } else { - console.log("Resizing to column"); this.graph.height(window.innerHeight - 200); this.graph.width(getWidth()); } @@ -245,23 +291,21 @@ function loadComponents() { function createFullScreenButton() { - const sceneNode = document.getElementById('3d-graph'); - const overlayNode = document.createElement('button'); - overlayNode.className = 'fullscreen_button'; - overlayNode.innerText = 'fullscreen'; + const sceneNode = getCanvasDivNode(); + const overlayNode = document.createElement('div'); + overlayNode.className = 'fullscreen-button'; + overlayNode.innerHTML = '<p>⤢</p>'; overlayNode.addEventListener("click", function () { - console.log("Catched event"); - if(getCanvasDivNode().requestFullscreen) { - getCanvasDivNode().requestFullscreen().then( - () => G.resize() - ); - + if (!document.fullscreenElement) { + getCanvasDivNode().requestFullscreen(); + } else { + document.exitFullscreen(); } + G.resize(); }); sceneNode.appendChild(overlayNode); } - const dataUrl = plugin_path + 'datasets/aud1.json' G = new Graph(dataUrl); linkoverlay = new LinkOverlay(G); diff --git a/display/infooverlay.js b/display/infooverlay.js index edb8c9a15d95b92bb08f2498b253d3cc933eb2f9..3f8598f63864602777fa131e64cb753aa2b02ae5 100644 --- a/display/infooverlay.js +++ b/display/infooverlay.js @@ -3,25 +3,106 @@ class InfoOverlay { constructor(graph) { this.graph = graph; + + this.activeTabNav = null; + this.activeTabContent = null; + + this.tabContentPages = {}; } create() { + const overlayDiv = this.createOverlayMainDiv(); + this.createOverlayElements(overlayDiv); + this.createBottomMenu(overlayDiv) + + jQuery('#infoOverlayCloseButton').click(function () { + jQuery('#infoOverlay').slideUp('fast'); + }); + + } + + createBottomMenu(overlayNode) { + + const bottomContainerDiv = document.createElement('div'); + bottomContainerDiv.className = 'bottom-container'; + overlayNode.appendChild(bottomContainerDiv); + + const bottomContainerNavDiv = document.createElement('div'); + bottomContainerNavDiv.className = 'bottom-container-nav'; + bottomContainerDiv.appendChild(bottomContainerNavDiv); + + const bottomContainerLinkDiv = document.createElement('div'); + bottomContainerLinkDiv.className = 'bottom-container-links'; + bottomContainerDiv.appendChild(bottomContainerLinkDiv); + + for(const [cls, color] of Object.entries(this.graph.edgeColors)) { + + const navTab = document.createElement('div'); + navTab.className = 'bottom-container-nav-tab'; + navTab.innerText = cls.slice(0, 3); + navTab.style = "background-color: " + color; + navTab.edgeType = cls; + //tablink.setAttribute("onclick", "openTab(event, " + linkType + "_id)"); //unusual function call + bottomContainerNavDiv.appendChild(navTab); + jQuery(navTab).click(event => this.openTab(event)) + + const tabContent = document.createElement('div'); + tabContent.className = "bottom-container-tab-content"; + tabContent.id = cls + "_id_content"; + tabContent.style = "background-color: " + color; + bottomContainerLinkDiv.appendChild(tabContent); + this.tabContentPages[cls] = tabContent; + } + + this.activeTabNav = bottomContainerNavDiv.firstChild; + this.activeTabContent = bottomContainerLinkDiv.firstChild; + + this.activeTabContent.classList.add("active-tab-content"); + this.activeTabNav.classList.add("active-tab-nav"); + this.activeTabNav.innerText = this.activeTabNav.edgeType; + + } + + openTab(event) { + const navTab = event.target; + const cls = navTab.edgeType; + + this.activeTabNav.classList.remove("active-tab-nav"); + this.activeTabNav.innerText = this.activeTabNav.innerText.slice(0, 3); + navTab.classList.add("active-tab-nav"); + navTab.innerText = cls; + + this.activeTabContent.classList.remove("active-tab-content"); + this.tabContentPages[cls].classList.add("active-tab-content"); + + this.activeTabNav = navTab; + this.activeTabContent = this.tabContentPages[cls]; + } + + createOverlayMainDiv() { 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]); + return overlayNode; + } + createOverlayElements(overlayNode) { const close = document.createElement('div'); close.innerHTML = '<p>✖</p>'; close.id = 'infoOverlayCloseButton'; close.className = 'close-button'; overlayNode.appendChild(close); + const infoArea = document.createElement('div'); + infoArea.className = 'detail-view-info-area'; + overlayNode.appendChild(infoArea); + const topArea = document.createElement('div'); topArea.className = 'detail-view-top-area'; - overlayNode.appendChild(topArea); + infoArea.appendChild(topArea); const nodeImage = document.createElement('img'); nodeImage.id = 'infoOverlayImage'; @@ -37,57 +118,13 @@ class InfoOverlay { const textArea = document.createElement('div'); textArea.className = 'detail-view-text-area'; - overlayNode.appendChild(textArea); + infoArea.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 = G.getLinkClasses(); - - link_container.id = 'link_container'; - link_container.className = 'bottom container'; - links.className = 'tab'; - links.id = 'links'; - - for(let 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: " + this.graph.edgeColors[linkClasses[i]]; - tablink.setAttribute("onclick", "openTab(event, " + linkType + "_id)"); //unusual function call - links.appendChild(tablink); - - } - - link_container.appendChild(links); - - for(let 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: " + this.graph.edgeColors[linkClasses[i]]; - link_container.appendChild(tabcontent); - - } - - overlayNode.appendChild(link_container); - - jQuery('#infoOverlayCloseButton').click(function () { - jQuery('#infoOverlay').slideUp('fast'); - }); - - //sceneNode.appendChild(overlayNode); - } updateInfoOverlay(node) { @@ -109,74 +146,37 @@ class InfoOverlay { jQuery('#infoOverlay').slideDown('fast'); } - updateTabs(node){ - //tabs of the links to other nodes: - //delete old nodes from tabcontent - const tabcontent = document.getElementsByClassName("tabcontent"); - for (let i = 0; i < tabcontent.length; i++) { - while (tabcontent[i].firstChild){ - tabcontent[i].removeChild(tabcontent[i].lastChild); - } - } - //add new nodes to tabcontent - for (let 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); - - jQuery(linkButton).on('click', () => this.graph.focusOnNode(nodeID)); - tabcontent[i].appendChild(linkButton); - } - }); + clearTabContentPages() { + for (const page of Object.values(this.tabContentPages)) { + jQuery(page).empty(); } } + createReference(target) { + const linkDiv = document.createElement('div'); //and if so a new element is created + linkDiv.className = "link-img"; + if ('image' in target) { + const linkImage = document.createElement('img'); + linkImage.src = plugin_path + 'datasets/images/' + target.image; + linkDiv.appendChild(linkImage); + } -} - -//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"; + jQuery(linkDiv).on('click', () => { + this.graph.focusOnNode(target); + this.updateInfoOverlay(target); + }); + return linkDiv; } - // 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", ""); + updateTabs(node){ + this.clearTabContentPages(); + + for (const link of node.links) { + const target = link.source == node ? link.target : link.source; + const reference = this.createReference(target); + this.tabContentPages[link.type].appendChild(reference); + } } - // Show the current tab, and add an "active" class to the button that opened the tab - document.getElementById(tab.id + "_content").style.display = "flex"; - event.currentTarget.className += " active"; } \ No newline at end of file diff --git a/display/linkoverlay.js b/display/linkoverlay.js index 8dc5699ddd375a62b19c1b04a3a1dcd8585d1588..5d0f084400dc0f735e24c395a5a66ee364ac0c6b 100644 --- a/display/linkoverlay.js +++ b/display/linkoverlay.js @@ -15,17 +15,29 @@ class LinkOverlay { const linkClasses = this.graph.getLinkClasses(); const chars = Math.max.apply(Math, linkClasses.map(function (c) { return c.length; })); - for (let i=0; i<linkClasses.length; i++) { + for (const link of linkClasses) { const relation = document.createElement('div'); relation.className = 'relation'; - relation.innerHTML = "<p>" + linkClasses[i] + "</p>"; + relation.edgeType = link; + relation.innerHTML = "<p>" + link + "</p>"; + jQuery(relation).click((event) => this.toggleLinkVisibility(event)); overlayNode.appendChild(relation); const colorStrip = document.createElement('div'); colorStrip.className = 'rel-container'; - colorStrip.style = "background-color: " + this.graph.edgeColors[linkClasses[i]] + "; width: " + 10 * chars + "px;"; + colorStrip.style = "background-color: " + this.graph.edgeColors[link] + "; width: " + 10 * chars + "px;"; relation.appendChild(colorStrip); } } + toggleLinkVisibility(event) { + const target = event.currentTarget; + this.graph.toggleLinkVisibility(target.edgeType) + if (getComputedStyle(target).opacity == 1.0) { + target.style.opacity = 0.4; + } else { + target.style.opacity = 1.0; + } + } + } \ No newline at end of file diff --git a/kg-style.css b/kg-style.css index 80b1f3901522741e392b3dc6acf72ef57d16c597..3549aebf369d41669da21aeaeaf5631d97e2d2e7 100644 --- a/kg-style.css +++ b/kg-style.css @@ -2,6 +2,35 @@ display: flex; } +.detail-view-info-area::-webkit-scrollbar { + width: 6px; + background-color: #F5F5F5; +} + +.detail-view-info-area::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + background-color: #F5F5F5; +} + +.detail-view-info-area::-webkit-scrollbar-thumb { + background-color: darkgray; +} + +.bottom-container-tab-content::-webkit-scrollbar { + width: 6px; + height: 8px; + background-color: #F5F5F5; +} + +.bottom-container-tab-content::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + background-color: #F5F5F5; +} + +.bottom-container-tab-content::-webkit-scrollbar-thumb { + background-color: darkgray; +} + .node-label { font-size: 14px; color: white; @@ -18,18 +47,28 @@ pointer-events: all; z-index: 99; cursor: pointer; - right: 10px; + right: 30px; top: 5px; position: absolute; font-size: 20px; } +.fullscreen-button { + pointer-events: all; + z-index: 99; + cursor: pointer; + left: 15px; + top: 5px; + position: absolute; + font-size: 30px; +} .detail-view { position: absolute; top: 0; right: 0; - width: 250px; + width: 20%; + min-width: 250px; height: 100%; background-color: whitesmoke; padding: 10px; @@ -48,6 +87,11 @@ margin-bottom: 20px; } +.detail-view-info-area { + overflow-y: auto; + height: 100%; +} + .detail-view-top-area { position: relative; width: 100%; @@ -59,8 +103,7 @@ display: block; padding-left: 25px; padding-right: 25px; - height: 45%; - overflow-y: scroll; + padding-bottom: 130px; } .detail-view-headline { @@ -95,6 +138,10 @@ text-transform: uppercase; margin-bottom: 6px; height: 17px; + z-index: 100; + cursor: pointer; + pointer-events: all; + opacity: 1.0; } .rel-container { @@ -103,59 +150,68 @@ width: 78px; } -.tab { - overflow: hidden; - width: 200px; - background-color: #f1f1f1; -} -.tab button { - color: white; - float: left; - border: none; - outline: none; - cursor: pointer; - width: 50px; - padding: 10px 12px; - transition: 0.3s; -} -.tab button:hover { - background-color: #ddd; +.bottom-container-tab-content { + display: none; + /*padding: 6px 0px;*/ + /*border-top: none;*/ + overflow-x: auto; + /*height: 100px;*/ + height: 100%; + width: 100%; } -.tab button.active { - background-color: #ccc; + +.active-tab-nav { + color: black; + font-weight: bold; } -.tabcontent { - display: none; - padding: 6px 0px; - border-top: none; - overflow-x: scroll; - height: 100px; - width: 200px; + +.active-tab-content { + display: flex; + align-items: center; } -.bottom container { + +.bottom-container { position: absolute; bottom: 0; left: 0; - width: 200px; - padding: 0px; - margin: 0px; + width: 100%; } -.link img { - position: relative; - text-align: center; - min-width: 60px; - width: 60px; - height: 60px; - margin-left: 5px; +.bottom-container-nav { + height: 35px; + width: 100%; + top: 0; + display: flex; + flex-direction: row; + cursor: pointer; +} + +.bottom-container-links { + width: 100%; + height: 100px; + display: block; +} + + +.link-img { + min-width: 70px; + width: 70px; + height: 70px; + margin: 10px; pointer-events: all; cursor: pointer; border-radius: 50%; + overflow: hidden; + background-color: white; + display: flex; + align-items: center; +} + +.bottom-container-nav-tab { + height: 100%; + display: flex; + padding: 0 8px; + min-width: 10%; + text-align: center; + line-height: 35px; } -.link title { - font-size: 2px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} \ No newline at end of file