diff --git a/src/display/components/nodefilter/classlabel.css b/src/display/components/nodefilter/classlabel.css new file mode 100644 index 0000000000000000000000000000000000000000..8d4ff55046ec6d5780eb14f5e61e1954dca520f7 --- /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 0000000000000000000000000000000000000000..f0cc9cbdcaa96f91805a58e9e59a1a2a66b8f38d --- /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 0000000000000000000000000000000000000000..015e6b42667268c13446b06d6ea683b9bb5cdeef --- /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 0000000000000000000000000000000000000000..bbd2549ece13345606501953690b8f93352640e7 --- /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 f6fa0557cd63f63bdf2c1475bdac755dbde43081..196af78d95787d2ff71d92b97fc8b3825ae7aec0 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 bf0ed0d12e35f47129dd435519af1ca25390c5a5..d802622f55a19be913e5133fc7046550c2778fa4 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 861e240e4e0e86c2ca2ee3deb1e7a167b642a6f2..934305da9149ab29319932a1e5ed551a6303a2f0 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)),