From d2e2da58e911d2ffa9f861cdecb48c3f647a6c03 Mon Sep 17 00:00:00 2001 From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de> Date: Wed, 29 Sep 2021 18:09:32 +0200 Subject: [PATCH] Filtering the nodes now also toggles the available tabs in the infoOverlay on and off. --- display/graph.js | 80 ++++++++++++++----- .../{linkselection.js => filteroverlay.js} | 37 ++++++--- display/overlays/neighbors.js | 62 +++++++++++--- display/overlays/nodeinfo.js | 7 +- kg-style.css | 4 + 5 files changed, 147 insertions(+), 43 deletions(-) rename display/overlays/{linkselection.js => filteroverlay.js} (57%) diff --git a/display/graph.js b/display/graph.js index e54a14f..e24ce44 100644 --- a/display/graph.js +++ b/display/graph.js @@ -2,7 +2,7 @@ import * as Config from "../config"; import * as Helpers from "./helpers"; import { loadGraphJson } from "../datasets/datasets"; import { NodeInfoOverlay } from "./overlays/nodeinfo"; -import { LinkSelectionOverlay } from "./overlays/linkselection"; +import { FilterOverlay } from "./overlays/filteroverlay"; import * as THREE from "three"; import ForceGraph3D from "3d-force-graph"; @@ -33,9 +33,10 @@ class Graph { this.idToNode = {}; this.firstTick = true; - this.infooverlay = null; + this.infoOverlay = null; this.edgeTypeVisibility = {}; + this.nodeTypeVisibility = {}; this.loadGraph(spaceId); } @@ -59,7 +60,7 @@ class Graph { .linkWidth((link) => this.getLinkWidth(link)) .onNodeClick((node) => { this.focusOnNode(node); - this.infooverlay.updateInfoOverlay(node); + this.infoOverlay.updateInfoOverlay(node); }) .onNodeHover((node) => { this.onNodeHover(node); @@ -101,9 +102,13 @@ class Graph { document.addEventListener("fullscreenchange", () => this.resize()); window.addEventListener("resize", () => this.resize()); + // Initialize visibility states this.getLinkClasses().forEach( (item) => (this.edgeTypeVisibility[item] = true) ); + this.getNodeClasses().forEach( + (item) => (this.nodeTypeVisibility[item] = true) + ); this.firstTick = false; } } @@ -150,7 +155,7 @@ class Graph { this.graph .graphData() .nodes.forEach((node) => nodeClasses.push(node.type)); - return [...new Set(nodeClasses)]; + return [...new Set(nodeClasses)].map((c) => String(c)); } onNodeHover(node) { @@ -215,18 +220,39 @@ class Graph { } } - hideLinkType(type) { - this.edgeTypeVisibility[type] = false; + toggleNodeVisibility(type) { + if (this.nodeTypeVisibility[type]) { + this.hideNodeType(type); + } else { + this.showNodeType(type); + } + } + + updateVisibility() { this.updateGraphData(); + this.removeFloatingLinks(); this.updateNodeData(); this.removeFloatingNodes(); } + hideLinkType(type) { + this.edgeTypeVisibility[type] = false; + this.updateVisibility(); + } + showLinkType(type) { this.edgeTypeVisibility[type] = true; - this.updateGraphData(); - this.updateNodeData(); - this.removeFloatingNodes(); + this.updateVisibility(); + } + + hideNodeType(type) { + this.nodeTypeVisibility[type] = false; + this.updateVisibility(); + } + + showNodeType(type) { + this.nodeTypeVisibility[type] = true; + this.updateVisibility(); } removeFloatingNodes() { @@ -239,9 +265,25 @@ class Graph { this.graph.graphData(data); } + removeFloatingLinks() { + const gData = this.graph.graphData(); + const links = gData.links.filter( + (link) => + this.nodeTypeVisibility[link.target.type] & + this.nodeTypeVisibility[link.source.type] + ); + const data = { + nodes: gData.nodes, + links: links, + }; + this.graph.graphData(data); + } + updateGraphData() { const data = { - nodes: this.gData.nodes, + nodes: this.gData.nodes.filter( + (l) => this.nodeTypeVisibility[l.type] + ), links: this.gData.links.filter( (l) => this.edgeTypeVisibility[l.type] ), @@ -450,8 +492,10 @@ class Graph { } function loadComponents() { - linkoverlay.create(); - infooverlay.create(); + filterOverlay.create(); + infoOverlay.create(); + filterOverlay.filterChangedCallback = (cls) => + infoOverlay.bottomMenu.toggleTabVisibility(cls); createFullScreenButton(); } @@ -471,14 +515,14 @@ function createFullScreenButton() { sceneNode.appendChild(overlayNode); } -var G; -var linkoverlay; -var infooverlay; +let G = null; +let filterOverlay = null; +let infoOverlay = null; // Only execute, if corresponding dom is present if (document.getElementById("3d-graph") !== null) { G = new Graph(space_id); // space_id defined globaly through knowledge-space.php - linkoverlay = new LinkSelectionOverlay(G); - infooverlay = new NodeInfoOverlay(G); - G.infooverlay = infooverlay; + filterOverlay = new FilterOverlay(G, "node"); + infoOverlay = new NodeInfoOverlay(G); + G.infoOverlay = infoOverlay; } diff --git a/display/overlays/linkselection.js b/display/overlays/filteroverlay.js similarity index 57% rename from display/overlays/linkselection.js rename to display/overlays/filteroverlay.js index fb43bc9..76db39e 100644 --- a/display/overlays/linkselection.js +++ b/display/overlays/filteroverlay.js @@ -1,19 +1,22 @@ import * as Helpers from "../helpers"; import jQuery from "jquery"; -export { LinkSelectionOverlay }; +export { FilterOverlay }; /** - * Represents an overlay showing the meaning of different edge colors. - * Offers the ability to toggle certain edge types. + * Represents an overlay showing the meaning of different link/node colors. + * Offers the ability to toggle certain types. */ -class LinkSelectionOverlay { +class FilterOverlay { /** - * Creates the link overlay for a given graph object. + * Creates the overlay for a given graph object. * @param {Graph} graph The graph object. + * @param {String} type The selection type. Can be "link" or "node". */ - constructor(graph) { + constructor(graph, type = "link") { this.graph = graph; + this.type = type; + this.filterChangedCallback = (type) => void type; } /** @@ -26,20 +29,23 @@ class LinkSelectionOverlay { overlayNode.className = "link-overlay"; sceneNode.insertBefore(overlayNode, sceneNode.childNodes[2]); - const linkClasses = this.graph.getLinkClasses(); + const classes = + this.type === "link" + ? this.graph.getLinkClasses() + : this.graph.getNodeClasses(); const chars = Math.max.apply( Math, - linkClasses.map(function (c) { + classes.map(function (c) { return c.length; }) ); - for (const link of linkClasses) { + for (const link of classes) { const relation = Helpers.createDiv("relation", overlayNode, { - edgeType: link, // Attach the link type to the relation div object + type: link, // Attach the link type to the relation div object innerHTML: "<p>" + link + "</p>", }); - jQuery(relation).click((event) => this.toggleLinkVisibility(event)); + jQuery(relation).click((event) => this.toggleVisibility(event)); const colorStrip = Helpers.createDiv("rel-container", relation); colorStrip.style.backgroundColor = this.graph.edgeColors[link]; @@ -52,9 +58,14 @@ class LinkSelectionOverlay { * Toggles the visibility of certain edge types. * @param {MouseEvent} event */ - toggleLinkVisibility(event) { + toggleVisibility(event) { const target = event.currentTarget; - this.graph.toggleLinkVisibility(target.edgeType); + if (this.type === "link") { + this.graph.toggleLinkVisibility(target.type); + } else { + this.graph.toggleNodeVisibility(target.type); + } + this.filterChangedCallback(target.type); if (getComputedStyle(target).opacity == 1.0) { target.style.opacity = 0.4; } else { diff --git a/display/overlays/neighbors.js b/display/overlays/neighbors.js index d7b253f..7e641b8 100644 --- a/display/overlays/neighbors.js +++ b/display/overlays/neighbors.js @@ -9,15 +9,17 @@ export { NodeNeighborOverlay }; * that connects them. */ class NodeNeighborOverlay { - constructor(graph, parentNode, infoOverlay) { + constructor(graph, parentNode, infoOverlay, type = "link") { this.graph = graph; this.parentNode = parentNode; this.infoOverlay = infoOverlay; + this.type = type; this.activeTabNav = null; // The currently selected tab handle this.activeTabContent = null; // The currently selected tab content this.tabContentPages = {}; + this.tabNavHandles = {}; } /** @@ -38,15 +40,20 @@ class NodeNeighborOverlay { bottomContainerDiv ); - for (const [cls, color] of Object.entries(this.graph.edgeColors)) { + const colors = + this.type === "link" + ? this.graph.edgeColors + : this.graph.nodeColors; + for (const [cls, color] of Object.entries(colors)) { const navTab = Helpers.createDiv( "bottom-container-nav-tab", bottomContainerNavDiv ); navTab.innerText = cls.slice(0, 3); navTab.style.backgroundColor = color; - navTab.edgeType = cls; // Attach the edge type to the DOM object to retrieve it during click events - jQuery(navTab).click((event) => this.openTab(event)); + navTab.type = cls; // Attach the edge type to the DOM object to retrieve it during click events + jQuery(navTab).click((event) => this.openTabFromEvent(event)); + this.tabNavHandles[cls] = navTab; const tabContent = Helpers.createDiv( "bottom-container-tab-content", @@ -69,29 +76,58 @@ class NodeNeighborOverlay { this.activeTabContent.classList.add("active-tab-content"); this.activeTabNav.classList.add("active-tab-nav"); - this.activeTabNav.innerText = this.activeTabNav.edgeType; + this.activeTabNav.innerText = this.activeTabNav.type; } /** * Click event handler for the tab headers of the bottom menu. * @param event */ - openTab(event) { + openTabFromEvent(event) { const navTab = event.target; - const cls = navTab.edgeType; + const cls = navTab.type; + this.openTab(cls); + } + openTab(cls) { 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.tabNavHandles[cls].classList.add("active-tab-nav"); + this.tabNavHandles[cls].innerText = cls; this.activeTabContent.classList.remove("active-tab-content"); this.tabContentPages[cls].classList.add("active-tab-content"); - this.activeTabNav = navTab; + this.activeTabNav = this.tabNavHandles[cls]; this.activeTabContent = this.tabContentPages[cls]; } + toggleTabVisibility(cls) { + this.tabNavHandles[cls].classList.toggle("bottom-container-nav-tab"); + this.tabNavHandles[cls].classList.toggle("hidden-tab"); + this.tabContentPages[cls].classList.toggle( + "bottom-container-tab-content" + ); + this.tabContentPages[cls].classList.toggle("hidden-tab"); + + // If the tab gets hidden and is the active tab, search for an alternative nav tab to become the new active tab. + if (this.tabContentPages[cls].classList.contains("hidden-tab")) { + if (this.tabNavHandles[cls].classList.contains("active-tab-nav")) { + for (const tab of Object.values(this.tabNavHandles)) { + if (!tab.classList.contains("hidden-tab")) { + this.openTab(tab.type); + break; + } + } + } + } else { + // If all tabs are hidden, the new tab should become the active tab. + if (this.activeTabNav.classList.contains("hidden-tab")) { + this.openTab(cls); + } + } + } + /** * Clears the images from all tab content pages. */ @@ -134,7 +170,11 @@ class NodeNeighborOverlay { 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); + if (this.type === "link") { + this.tabContentPages[link.type].appendChild(reference); + } else { + this.tabContentPages[target.type].appendChild(reference); + } } } } diff --git a/display/overlays/nodeinfo.js b/display/overlays/nodeinfo.js index 1c97533..e94bba4 100644 --- a/display/overlays/nodeinfo.js +++ b/display/overlays/nodeinfo.js @@ -26,7 +26,12 @@ class NodeInfoOverlay { create() { const overlayDiv = this.createOverlayMainDiv(); this.createOverlayElements(overlayDiv); - this.bottomMenu = new NodeNeighborOverlay(this.graph, overlayDiv, this); + this.bottomMenu = new NodeNeighborOverlay( + this.graph, + overlayDiv, + this, + "node" + ); this.bottomMenu.create(); jQuery("#infoOverlayCloseButton").click(function () { diff --git a/kg-style.css b/kg-style.css index 58f0b45..6ed2ff8 100644 --- a/kg-style.css +++ b/kg-style.css @@ -169,6 +169,10 @@ align-items: center; } +.hidden-tab { + display: none; +} + .bottom-container { position: absolute; bottom: 0; -- GitLab