From 09f5d90febff4ac58ca00414fbdd160f23b934a5 Mon Sep 17 00:00:00 2001
From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de>
Date: Tue, 19 Jul 2022 13:50:57 +0200
Subject: [PATCH] Partially re-implemented filter menu

---
 .../components/nodefilter/classlabel.css      | 25 +++++++++
 .../components/nodefilter/classlabel.tsx      | 42 +++++++++++++++
 .../components/nodefilter/filtermenu.css      | 13 +++++
 .../components/nodefilter/filtermenu.tsx      | 51 +++++++++++++++++++
 src/display/display.css                       | 39 --------------
 src/display/display.tsx                       | 19 ++++++-
 src/display/graph.ts                          | 10 ++--
 7 files changed, 155 insertions(+), 44 deletions(-)
 create mode 100644 src/display/components/nodefilter/classlabel.css
 create mode 100644 src/display/components/nodefilter/classlabel.tsx
 create mode 100644 src/display/components/nodefilter/filtermenu.css
 create mode 100644 src/display/components/nodefilter/filtermenu.tsx

diff --git a/src/display/components/nodefilter/classlabel.css b/src/display/components/nodefilter/classlabel.css
new file mode 100644
index 0000000..8d4ff55
--- /dev/null
+++ b/src/display/components/nodefilter/classlabel.css
@@ -0,0 +1,25 @@
+.filter-class-label {
+    display: inline-block;
+    padding-right: 5px;
+    color: #fff;
+    font-size: 13px;
+    line-height: 20px;
+    font-family: CuratorRegular, Helvetica Neue, Helvetica, Arial, sans-serif;
+    text-transform: uppercase;
+    margin-bottom: 6px;
+    height: 17px;
+    z-index: 100;
+    cursor: pointer;
+    pointer-events: all;
+    opacity: 1;
+}
+
+.filter-class-label p {
+    margin: 0
+}
+
+.filter-class-label-color-strip {
+    display: block;
+    height: 2px;
+    width: 78px;
+}
\ No newline at end of file
diff --git a/src/display/components/nodefilter/classlabel.tsx b/src/display/components/nodefilter/classlabel.tsx
new file mode 100644
index 0000000..f0cc9cb
--- /dev/null
+++ b/src/display/components/nodefilter/classlabel.tsx
@@ -0,0 +1,42 @@
+import React, { useState } from "react";
+
+import "./classlabel.css";
+
+interface ClassLabelProps {
+    type: string;
+    color: string;
+    width: number;
+    visible?: boolean;
+    onClick?: (type: string) => void;
+}
+
+function ClassLabel({
+    type,
+    color,
+    width,
+    onClick,
+    visible = true,
+}: ClassLabelProps) {
+    const handleClick = () => {
+        if (onClick) {
+            onClick(type);
+        }
+    };
+
+    const opacity = visible ? 1.0 : 0.4;
+    return (
+        <div
+            className={"filter-class-label"}
+            style={{ opacity }}
+            onClick={handleClick}
+        >
+            <p>{type}</p>
+            <div
+                className={"filter-class-label-color-strip"}
+                style={{ width: width + "px", backgroundColor: color }}
+            ></div>
+        </div>
+    );
+}
+
+export default ClassLabel;
diff --git a/src/display/components/nodefilter/filtermenu.css b/src/display/components/nodefilter/filtermenu.css
new file mode 100644
index 0000000..015e6b4
--- /dev/null
+++ b/src/display/components/nodefilter/filtermenu.css
@@ -0,0 +1,13 @@
+.filter-menu {
+    position: absolute;
+    bottom: 0;
+    pointer-events: none;
+    width: 350px;
+    max-width: 400px;
+    padding: 10px;
+    left: 0;
+    display: block;
+    background-color: rgba(0, 0, 0, 0.6);
+    z-index: 1;
+}
+
diff --git a/src/display/components/nodefilter/filtermenu.tsx b/src/display/components/nodefilter/filtermenu.tsx
new file mode 100644
index 0000000..bbd2549
--- /dev/null
+++ b/src/display/components/nodefilter/filtermenu.tsx
@@ -0,0 +1,51 @@
+import React, { useState } from "react";
+
+import "./filtermenu.css";
+import ClassLabel from "./classlabel";
+
+interface FilterMenuProps {
+    classes: Map<string, string>;
+    onVisibilityChange?: (visibility: Map<string, boolean>) => void;
+}
+
+function FilterMenu({ classes, onVisibilityChange }: FilterMenuProps) {
+    const classList = [...classes.keys()];
+    const chars = Math.max(
+        ...classList.map(function (c: string) {
+            return c.length;
+        })
+    );
+    const [visibility, setVisibility] = useState(
+        new Array(classList.length).fill(true)
+    );
+
+    const handleClick = (idx: number) => {
+        const vis = [...visibility];
+        vis[idx] = !vis[idx];
+        setVisibility(vis);
+        if (onVisibilityChange !== undefined) {
+            onVisibilityChange(
+                new Map<string, boolean>(
+                    classList.map((cls, idx) => [cls, vis[idx]])
+                )
+            );
+        }
+    };
+
+    return (
+        <div className={"filter-menu"}>
+            {classList.map((cls: string, idx) => (
+                <ClassLabel
+                    key={cls}
+                    type={cls}
+                    color={classes.get(cls)}
+                    width={10 * chars}
+                    visible={visibility[idx]}
+                    onClick={() => handleClick(idx)}
+                />
+            ))}
+        </div>
+    );
+}
+
+export default FilterMenu;
diff --git a/src/display/display.css b/src/display/display.css
index f6fa055..196af78 100644
--- a/src/display/display.css
+++ b/src/display/display.css
@@ -153,45 +153,6 @@
     width: 100%;
 }
 
-.link-overlay {
-    position: absolute;
-    bottom: 0;
-    pointer-events: none;
-    width: 350px;
-    max-width: 400px;
-    padding: 10px;
-    left: 0;
-    display: block;
-    background-color: rgba(0, 0, 0, 0.6);
-    z-index: 1;
-}
-
-.relation {
-    display: inline-block;
-    padding-right: 5px;
-    color: #fff;
-    font-size: 13px;
-    line-height: 20px;
-    font-family: CuratorRegular, Helvetica Neue, Helvetica, Arial, sans-serif;
-    text-transform: uppercase;
-    margin-bottom: 6px;
-    height: 17px;
-    z-index: 100;
-    cursor: pointer;
-    pointer-events: all;
-    opacity: 1;
-}
-
-.relation p {
-    margin: 0
-}
-
-.rel-container {
-    display: block;
-    height: 2px;
-    width: 78px;
-}
-
 /*New Section */
 
 .neighbor-collapsible-title {
diff --git a/src/display/display.tsx b/src/display/display.tsx
index bf0ed0d..d802622 100644
--- a/src/display/display.tsx
+++ b/src/display/display.tsx
@@ -7,6 +7,7 @@ import * as Helpers from "./helpers";
 import Graph, { NodeData } from "./graph";
 import { loadGraphJson } from "../datasets";
 import NodeInfoBar from "./components/nodeinfo/nodeinfobar";
+import FilterMenu from "./components/nodefilter/filtermenu";
 
 class Display extends React.Component<
     InferType<typeof Display.propTypes>,
@@ -17,6 +18,7 @@ class Display extends React.Component<
 
     fullscreenRef: React.RefObject<HTMLDivElement>;
     rendererRef: React.RefObject<GraphRenderer>;
+    graph: Graph;
 
     static propTypes = {
         spaceId: PropTypes.string.isRequired,
@@ -44,6 +46,7 @@ class Display extends React.Component<
         this.handleNodeClicked = this.handleNodeClicked.bind(this);
         this.handleNodeClose = this.handleNodeClose.bind(this);
         this.handleNodeChangeRequest = this.handleNodeChangeRequest.bind(this);
+        this.handleNodeFilter = this.handleNodeFilter.bind(this);
     }
 
     componentDidMount() {
@@ -54,8 +57,8 @@ class Display extends React.Component<
 
         const fetchGraph = async () => {
             const graphData = await loadGraphJson(this.props.spaceId);
-            const graph = new Graph(graphData.nodes, graphData.links);
-            this.setState({ graph: graph });
+            this.graph = new Graph(graphData.nodes, graphData.links);
+            this.setState({ graph: this.graph });
         };
         fetchGraph();
     }
@@ -73,6 +76,11 @@ class Display extends React.Component<
         this.setState({ nodeActive: false });
     }
 
+    handleNodeFilter(visibility: Map<string, boolean>) {
+        const graph = this.graph.view(visibility);
+        this.setState({ graph: graph });
+    }
+
     toggleFullscreen() {
         if (screenfull.isEnabled) {
             if (!screenfull.isFullscreen) {
@@ -121,6 +129,13 @@ class Display extends React.Component<
                     ></NodeInfoBar>
                 )}
 
+                {this.state.graph && (
+                    <FilterMenu
+                        classes={this.state.graph.nodeColors}
+                        onVisibilityChange={this.handleNodeFilter}
+                    />
+                )}
+
                 <div id="3d-graph">
                     {this.state.graph && (
                         <GraphRenderer
diff --git a/src/display/graph.ts b/src/display/graph.ts
index 861e240..934305d 100644
--- a/src/display/graph.ts
+++ b/src/display/graph.ts
@@ -133,12 +133,16 @@ export default class Graph {
         this.nodes.forEach((node) => nodeClasses.push(node.type));
         return [...new Set(nodeClasses)].map((c) => String(c));
     }
-
     public view(
         nodeTypes: Map<string, boolean>,
-        linkTypes: Map<string, boolean>
+        linkTypes?: Map<string, boolean>
     ): Graph {
-        const links = this.links.filter((l) => linkTypes.get(l.type));
+        let links;
+        if (linkTypes === undefined) {
+            links = this.links;
+        } else {
+            links = this.links.filter((l) => linkTypes.get(l.type));
+        }
 
         return new Graph(
             this.nodes.filter((l) => nodeTypes.get(l.type)),
-- 
GitLab