diff --git a/src/common/graph.ts b/src/common/graph.ts index d515c4fd5900b0fc16df3fdbc2f9e6a545aa1e8a..4ed5997d679bcad99fb48afc774b6ae3e9ad6069 100644 --- a/src/common/graph.ts +++ b/src/common/graph.ts @@ -1,28 +1,42 @@ import * as Config from "../config"; -interface Link { +interface LinkData { source: string; target: string; type?: string; } -export interface LinkData { - source: NodeData; - target: NodeData; - type?: string; +export interface Link { + source: Node; + target: Node; + type?: GraphObjectType; } -export interface NodeData { - id: string; +interface NodeContent { name: string; description?: string; icon?: string; banner?: string; - type?: string; video?: string; references?: string[]; - neighbors: NodeData[]; - links: LinkData[]; +} + +interface NodeData extends NodeContent { + id: string; + type?: string; +} + +export interface Node extends NodeContent { + id: string; + type: GraphObjectType; + + neighbors: Node[]; + links: Link[]; +} + +export interface GraphObjectType { + name: string; + color?: string; } export interface Coordinate { @@ -35,42 +49,52 @@ export interface Coordinate { * Basic graph data structure. */ export default class Graph { - public nodes: NodeData[]; - public links: LinkData[]; - private idToNode: Map<string, NodeData>; - public edgeColors: Map<string, string>; - public nodeColors: Map<string, string>; - - constructor(nodes: NodeData[], links: Link[]) { - this.nodes = nodes; - this.idToNode = new Map<string, NodeData>(); - nodes.forEach((node) => { - this.idToNode.set(node.id, node); - }); + public nodes: Node[]; + public links: Link[]; + public objectGroups: GraphObjectType[]; + public nameToObjectGroup: Map<string, GraphObjectType>; + private idToNode: Map<string, Node>; + + constructor( + nodes: NodeData[], + links: LinkData[], + objectGroups?: GraphObjectType[] + ) { + this.objectGroups = objectGroups ?? this.createObjectGroups(nodes); + + this.nameToObjectGroup = new Map<string, GraphObjectType>(); + this.objectGroups.forEach((group) => + this.nameToObjectGroup.set(group.name, group) + ); + + this.createNodes(nodes); this.links = links.map((link) => { return { source: this.idToNode.get(link.source), target: this.idToNode.get(link.target), - type: link.type, }; }); - this.edgeColors = new Map<string, string>(); - this.nodeColors = new Map<string, string>(); - - this.resetNodeData(); this.updateNodeData(); this.removeFloatingNodes(); - this.mapNodeColors(); - this.mapLinkColors(); } - private resetNodeData() { - for (const node of this.nodes) { + private createNodes(nodes: NodeData[]) { + this.nodes = []; + for (const nodeData of nodes) { + const { type, ...nodeVars } = nodeData; + const node = { ...nodeVars } as Node; + node.type = this.nameToObjectGroup.get(type); node.neighbors = []; node.links = []; + this.nodes.push(node); } + + this.idToNode = new Map<string, Node>(); + this.nodes.forEach((node) => { + this.idToNode.set(node.id, node); + }); } private removeFloatingNodes() { @@ -82,8 +106,6 @@ export default class Graph { * Creates a 'neighbors' and 'links' array for each node object. */ private updateNodeData() { - this.resetNodeData(); - this.links.forEach((link) => { const a = link.source; const b = link.target; @@ -94,82 +116,64 @@ export default class Graph { }); } - public node(id: string): NodeData { + public node(id: string): Node { return this.idToNode.get(id); } - /** - * Maps the colors of the color palette to the different edge types - */ - private mapLinkColors() { - // TODO: Legacy - is there a use-case for link types? - const linkClasses = this.getLinkClasses(); - for (let i = 0; i < linkClasses.length; i++) { - this.edgeColors.set( - linkClasses[i], - Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length] - ); - } - } - - /** - * Maps the colors of the color palette to the different edge types - */ - private mapNodeColors() { - const nodeClasses = this.getNodeClasses(); - for (let i = 0; i < nodeClasses.length; i++) { - this.nodeColors.set( - nodeClasses[i], - Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length] - ); + private createObjectGroups(nodes: NodeData[]): GraphObjectType[] { + const objectGroups: GraphObjectType[] = []; + const nodeClasses: string[] = []; + nodes.forEach((node) => nodeClasses.push(node.type)); + const nodeTypes = [...new Set(nodeClasses)].map((c) => String(c)); + + for (let i = 0; i < nodeTypes.length; i++) { + objectGroups.push({ + name: nodeTypes[i], + color: Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length], + }); } + return objectGroups; } - /** - * Returns an array containing the different edge types of the graph. - * @returns {*[]} - */ - public getLinkClasses(): string[] { - const linkClasses: string[] = []; - - this.links.forEach((link) => linkClasses.push(link.type)); - return [...new Set(linkClasses)].map((c) => String(c)); - } - - public getNodeClasses(): string[] { - const nodeClasses: string[] = []; - 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> ): Graph { // Filter nodes depending on type - const nodes = this.nodes.filter((l) => nodeTypes.get(l.type)); + const nodes = this.nodes.filter((l) => nodeTypes.get(l.type.name)); // Filter links depending on type let links; if (linkTypes === undefined) { links = this.links; } else { - links = this.links.filter((l) => linkTypes.get(l.type)); + links = this.links.filter((l) => linkTypes.get(l.type.name)); } // Filter links which are connected to an invisible node links = links.filter( - (l) => nodeTypes.get(l.source.type) && nodeTypes.get(l.target.type) + (l) => + nodeTypes.get(l.source.type.name) && + nodeTypes.get(l.target.type.name) ); + // Convert to data objects and create new graph. + // Using spread syntax to simplify object copying. return new Graph( - nodes, + nodes.map((node) => { + // eslint-disable-next-line no-unused-vars + const { type, neighbors, links, ...nodeVars } = node; + const nodeData = { ...nodeVars } as NodeData; + nodeData.type = type.name; + return nodeData; + }), links.map((link) => { return { source: link.source.id, target: link.target.id, - type: link.type, }; - }) + }), + this.objectGroups ); } } diff --git a/src/display/components/nodefilter/filtermenu.tsx b/src/display/components/nodefilter/filtermenu.tsx index a78fc93a35e695a30523ffdf11386c54af48341c..f38c8affb945dd898768c0a4f0e7c05ed93d9f44 100644 --- a/src/display/components/nodefilter/filtermenu.tsx +++ b/src/display/components/nodefilter/filtermenu.tsx @@ -2,9 +2,10 @@ import React, { useState } from "react"; import "./filtermenu.css"; import Label from "./label"; +import { GraphObjectType } from "../../../common/graph"; interface FilterMenuProps { - classes: Map<string, string>; + classes: Map<string, GraphObjectType>; onVisibilityChange?: (visibility: Map<string, boolean>) => void; } @@ -47,7 +48,7 @@ function FilterMenu({ classes, onVisibilityChange }: FilterMenuProps) { <Label key={cls} text={cls} - color={classes.get(cls)} + color={classes.get(cls).color} width={labelWidth} active={visibility[idx]} onClick={() => handleClick(idx)} diff --git a/src/display/components/nodeinfo/neighbors.tsx b/src/display/components/nodeinfo/neighbors.tsx index b7f713ec0c1e237e711b03a2845144daac357c42..5a899cc6a066bdf9c4ccbdf169eac197d8125b4e 100644 --- a/src/display/components/nodeinfo/neighbors.tsx +++ b/src/display/components/nodeinfo/neighbors.tsx @@ -1,15 +1,15 @@ import React from "react"; -import { NodeData } from "../../../common/graph"; +import { GraphObjectType, Node } from "../../../common/graph"; import FancyScrollbar from "../fancyscrollbar"; import Collapsible from "../collapsible"; import "./neighbors.css"; interface NeighborsProps { - neighbors: NodeData[]; - nodeColors?: Map<string, string>; - nodeClickedCallback?: (node: NodeData) => void; + neighbors: Node[]; + nodeColors?: Map<string, GraphObjectType>; + nodeClickedCallback?: (node: Node) => void; } /** @@ -22,21 +22,23 @@ interface NeighborsProps { function Neighbors({ neighbors, nodeClickedCallback, - nodeColors = new Map<string, string>(), + nodeColors = new Map<string, GraphObjectType>(), }: NeighborsProps) { - const classes = [...new Set<string>(neighbors.map((node) => node.type))]; + const classes = [ + ...new Set<string>(neighbors.map((node) => node.type.name)), + ]; classes.sort(); // Sort classes to get a constant order of the node type tabs - const categories = new Map<string, Array<NodeData>>(); + const categories = new Map<string, Array<Node>>(); for (const cls of classes) { categories.set(cls, []); } for (const neighbor of neighbors) { - categories.get(neighbor.type).push(neighbor); + categories.get(neighbor.type.name).push(neighbor); } - const handleNodeClick = (node: NodeData) => { + const handleNodeClick = (node: Node) => { if (nodeClickedCallback) { nodeClickedCallback(node); } @@ -51,7 +53,7 @@ function Neighbors({ header={cls} key={cls} heightTransition={false} - color={nodeColors.get(cls)} + color={nodeColors.get(cls).color} > <ul> {categories.get(cls).map((node) => ( diff --git a/src/display/components/nodeinfo/nodeinfobar.tsx b/src/display/components/nodeinfo/nodeinfobar.tsx index 9d98adda3e33ac859d861a242700ad8682c9131c..4965e738d90c1df5985d6912d64b04178bfc8d54 100644 --- a/src/display/components/nodeinfo/nodeinfobar.tsx +++ b/src/display/components/nodeinfo/nodeinfobar.tsx @@ -1,7 +1,7 @@ import React from "react"; import "./nodeinfobar.css"; -import { NodeData } from "../../../common/graph"; +import { GraphObjectType, Node } from "../../../common/graph"; import TitleArea from "./titlearea"; import FancyScrollbar from "../fancyscrollbar"; import MediaArea from "./mediaarea"; @@ -9,10 +9,10 @@ import Neighbors from "./neighbors"; interface InfoBarProps { height: number; - node: NodeData; - nodeColors?: Map<string, string>; + node: Node; + nodeColors?: Map<string, GraphObjectType>; onClose?: () => void; - nodeClickedCallback?: (node: NodeData) => void; + nodeClickedCallback?: (node: Node) => void; } /** diff --git a/src/display/components/searchbar.tsx b/src/display/components/searchbar.tsx index 9cfca31d869ab6089ce56f027b3a762fe6e73568..d1ee95ed29853234c5c2d4cfa389c8b737138f47 100644 --- a/src/display/components/searchbar.tsx +++ b/src/display/components/searchbar.tsx @@ -3,12 +3,12 @@ import React, { useEffect, useRef, useState } from "react"; import "./searchbar.css"; import searchicon from "./search_icon.svg"; import closeicon from "./close_icon.svg"; -import { NodeData } from "../../common/graph"; +import { Node } from "../../common/graph"; interface SearchBarProps { minified: boolean; - nodeSet: NodeData[]; - onSearch?: (node: NodeData) => void; + nodeSet: Node[]; + onSearch?: (node: Node) => void; nodeColors: Map<string, string>; } @@ -56,7 +56,7 @@ function SearchBar({ }) .slice(0, 3); - const handleNodeClick = (node: NodeData) => { + const handleNodeClick = (node: Node) => { if (onSearch !== undefined) { onSearch(node); } @@ -120,7 +120,9 @@ function SearchBar({ > <div className={"searchbar-results-circle"} - style={{ backgroundColor: nodeColors.get(el.type) }} + style={{ + backgroundColor: el.type.color, + }} ></div> <div>{el.name}</div> </div> diff --git a/src/display/display.tsx b/src/display/display.tsx index 13ec325f2f50e42f7e93aeecb4c175cc0bc4deb8..3b5c52a833c63cda53474d25d9140b7a5bd0c412 100644 --- a/src/display/display.tsx +++ b/src/display/display.tsx @@ -5,7 +5,7 @@ import PropTypes, { InferType } from "prop-types"; import "./display.css"; import { GraphNode, GraphRenderer } from "./renderer"; import * as Helpers from "./helpers"; -import Graph, { NodeData } from "../common/graph"; +import Graph, { Node } from "../common/graph"; import { loadGraphJson } from "../common/datasets"; import NodeInfoBar from "./components/nodeinfo/nodeinfobar"; import FilterMenu from "./components/nodefilter/filtermenu"; @@ -69,11 +69,11 @@ class Display extends React.Component< fetchGraph(); } - handleNodeClicked(node: NodeData) { + handleNodeClicked(node: Node) { this.setState({ currentNode: node, nodeActive: true }); } - handleNodeChangeRequest(node: NodeData) { + handleNodeChangeRequest(node: Node) { this.rendererRef.current.focusOnNode(node as GraphNode); this.rendererRef.current.displayNodeSelection(node as GraphNode); this.handleNodeClicked(node); @@ -145,7 +145,7 @@ class Display extends React.Component< {this.state.currentNode && ( <NodeInfoBar node={this.state.currentNode} - nodeColors={this.state.graph.nodeColors} + nodeColors={this.state.graph.nameToObjectGroup} height={this.state.nodeActive ? this.state.height : 0} onClose={() => { this.handleNodeClose(); @@ -157,7 +157,7 @@ class Display extends React.Component< {this.state.graph && ( <FilterMenu - classes={this.graph.nodeColors} + classes={this.graph.nameToObjectGroup} onVisibilityChange={this.handleNodeFilter} /> )} diff --git a/src/display/renderer.tsx b/src/display/renderer.tsx index a1ac0494ec01e0f002013978dc743b51b2d1367b..382e0ef3809ce7fc115220ffafe6123c8ccd91a3 100644 --- a/src/display/renderer.tsx +++ b/src/display/renderer.tsx @@ -10,9 +10,9 @@ import React from "react"; import PropTypes, { InferType } from "prop-types"; import SpriteText from "three-spritetext"; import { Object3D, Sprite } from "three"; -import Graph, { Coordinate, LinkData, NodeData } from "../common/graph"; +import Graph, { Coordinate, Link, Node } from "../common/graph"; -export interface GraphNode extends NodeData { +export interface GraphNode extends Node { x: number; y: number; z: number; @@ -26,7 +26,7 @@ export interface GraphNode extends NodeData { __threeObj: THREE.Group; } -export interface GraphLink extends LinkData { +export interface GraphLink extends Link { __lineObj?: Line2; } @@ -62,11 +62,15 @@ export class GraphRenderer extends React.PureComponent< constructor(props: InferType<typeof GraphRenderer.propTypes>) { super(props); + this.reset(); + this.forceGraph = React.createRef(); + } + + reset() { this.highlightedNodes = new Set(); this.highlightedLinks = new Set(); this.node3dObjects = new Map<string, THREE.Group>(); this.hoverNode = null; - this.forceGraph = React.createRef(); } componentDidMount() { @@ -112,7 +116,7 @@ export class GraphRenderer extends React.PureComponent< const material = new THREE.SpriteMaterial({ //map: imageTexture, - color: this.props.graph.nodeColors.get(node.type), + color: node.type.color, alphaMap: imageAlpha, transparent: true, alphaTest: 0.2, @@ -129,11 +133,11 @@ export class GraphRenderer extends React.PureComponent< return group; } - drawLink(link: LinkData) { + drawLink(link: Link) { const colors = new Float32Array( [].concat( ...[link.target, link.source] - .map((node) => this.props.graph.nodeColors.get(node.type)) + .map((node) => node.type.color) .map((color) => color.replace(/[^\d,]/g, "").split(",")) // Extract rgb() color components .map((rgb) => rgb.map((v: string) => parseInt(v) / 255)) ) @@ -356,6 +360,7 @@ export class GraphRenderer extends React.PureComponent< } render() { + this.reset(); return ( <ForceGraph3D ref={this.forceGraph} @@ -365,7 +370,7 @@ export class GraphRenderer extends React.PureComponent< rendererConfig={{ antialias: true }} // nodeLabel={"hidden"} nodeThreeObject={(node: GraphNode) => this.drawNode(node)} - linkThreeObject={(link: LinkData) => this.drawLink(link)} + linkThreeObject={(link: Link) => this.drawLink(link)} onNodeClick={(node: GraphNode) => this.onNodeClick(node)} onBackgroundClick={() => this.deselectNode()} //d3AlphaDecay={0.1}