Skip to content
Snippets Groups Projects
Commit aafff544 authored by Matthias Konitzny's avatar Matthias Konitzny :fire:
Browse files

Introduced GraphObjectTypes which can be assigned to nodes to give them group...

Introduced GraphObjectTypes which can be assigned to nodes to give them group properties for color, etc.
Fixed a bug which caused randomly assigned colors to change on filter events.
parent 69a38165
No related branches found
No related tags found
1 merge request!3Master into new editor
Pipeline #56915 passed
import * as Config from "../config"; import * as Config from "../config";
interface Link { interface LinkData {
source: string; source: string;
target: string; target: string;
type?: string; type?: string;
} }
export interface LinkData { export interface Link {
source: NodeData; source: Node;
target: NodeData; target: Node;
type?: string; type?: GraphObjectType;
} }
export interface NodeData { interface NodeContent {
id: string;
name: string; name: string;
description?: string; description?: string;
icon?: string; icon?: string;
banner?: string; banner?: string;
type?: string;
video?: string; video?: string;
references?: 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 { export interface Coordinate {
...@@ -35,42 +49,52 @@ export interface Coordinate { ...@@ -35,42 +49,52 @@ export interface Coordinate {
* Basic graph data structure. * Basic graph data structure.
*/ */
export default class Graph { export default class Graph {
public nodes: NodeData[]; public nodes: Node[];
public links: LinkData[]; public links: Link[];
private idToNode: Map<string, NodeData>; public objectGroups: GraphObjectType[];
public edgeColors: Map<string, string>; public nameToObjectGroup: Map<string, GraphObjectType>;
public nodeColors: Map<string, string>; private idToNode: Map<string, Node>;
constructor(nodes: NodeData[], links: Link[]) { constructor(
this.nodes = nodes; nodes: NodeData[],
this.idToNode = new Map<string, NodeData>(); links: LinkData[],
nodes.forEach((node) => { objectGroups?: GraphObjectType[]
this.idToNode.set(node.id, node); ) {
}); 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) => { this.links = links.map((link) => {
return { return {
source: this.idToNode.get(link.source), source: this.idToNode.get(link.source),
target: this.idToNode.get(link.target), 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.updateNodeData();
this.removeFloatingNodes(); this.removeFloatingNodes();
this.mapNodeColors();
this.mapLinkColors();
} }
private resetNodeData() { private createNodes(nodes: NodeData[]) {
for (const node of this.nodes) { 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.neighbors = [];
node.links = []; node.links = [];
this.nodes.push(node);
} }
this.idToNode = new Map<string, Node>();
this.nodes.forEach((node) => {
this.idToNode.set(node.id, node);
});
} }
private removeFloatingNodes() { private removeFloatingNodes() {
...@@ -82,8 +106,6 @@ export default class Graph { ...@@ -82,8 +106,6 @@ export default class Graph {
* Creates a 'neighbors' and 'links' array for each node object. * Creates a 'neighbors' and 'links' array for each node object.
*/ */
private updateNodeData() { private updateNodeData() {
this.resetNodeData();
this.links.forEach((link) => { this.links.forEach((link) => {
const a = link.source; const a = link.source;
const b = link.target; const b = link.target;
...@@ -94,82 +116,64 @@ export default class Graph { ...@@ -94,82 +116,64 @@ export default class Graph {
}); });
} }
public node(id: string): NodeData { public node(id: string): Node {
return this.idToNode.get(id); return this.idToNode.get(id);
} }
/** private createObjectGroups(nodes: NodeData[]): GraphObjectType[] {
* Maps the colors of the color palette to the different edge types const objectGroups: GraphObjectType[] = [];
*/ const nodeClasses: string[] = [];
private mapLinkColors() { nodes.forEach((node) => nodeClasses.push(node.type));
// TODO: Legacy - is there a use-case for link types? const nodeTypes = [...new Set(nodeClasses)].map((c) => String(c));
const linkClasses = this.getLinkClasses();
for (let i = 0; i < linkClasses.length; i++) { for (let i = 0; i < nodeTypes.length; i++) {
this.edgeColors.set( objectGroups.push({
linkClasses[i], name: nodeTypes[i],
Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length] color: 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]
);
} }
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( public view(
nodeTypes: Map<string, boolean>, nodeTypes: Map<string, boolean>,
linkTypes?: Map<string, boolean> linkTypes?: Map<string, boolean>
): Graph { ): Graph {
// Filter nodes depending on type // 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 // Filter links depending on type
let links; let links;
if (linkTypes === undefined) { if (linkTypes === undefined) {
links = this.links; links = this.links;
} else { } 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 // Filter links which are connected to an invisible node
links = links.filter( 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( 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) => { links.map((link) => {
return { return {
source: link.source.id, source: link.source.id,
target: link.target.id, target: link.target.id,
type: link.type,
}; };
}) }),
this.objectGroups
); );
} }
} }
...@@ -2,9 +2,10 @@ import React, { useState } from "react"; ...@@ -2,9 +2,10 @@ import React, { useState } from "react";
import "./filtermenu.css"; import "./filtermenu.css";
import Label from "./label"; import Label from "./label";
import { GraphObjectType } from "../../../common/graph";
interface FilterMenuProps { interface FilterMenuProps {
classes: Map<string, string>; classes: Map<string, GraphObjectType>;
onVisibilityChange?: (visibility: Map<string, boolean>) => void; onVisibilityChange?: (visibility: Map<string, boolean>) => void;
} }
...@@ -47,7 +48,7 @@ function FilterMenu({ classes, onVisibilityChange }: FilterMenuProps) { ...@@ -47,7 +48,7 @@ function FilterMenu({ classes, onVisibilityChange }: FilterMenuProps) {
<Label <Label
key={cls} key={cls}
text={cls} text={cls}
color={classes.get(cls)} color={classes.get(cls).color}
width={labelWidth} width={labelWidth}
active={visibility[idx]} active={visibility[idx]}
onClick={() => handleClick(idx)} onClick={() => handleClick(idx)}
......
import React from "react"; import React from "react";
import { NodeData } from "../../../common/graph"; import { GraphObjectType, Node } from "../../../common/graph";
import FancyScrollbar from "../fancyscrollbar"; import FancyScrollbar from "../fancyscrollbar";
import Collapsible from "../collapsible"; import Collapsible from "../collapsible";
import "./neighbors.css"; import "./neighbors.css";
interface NeighborsProps { interface NeighborsProps {
neighbors: NodeData[]; neighbors: Node[];
nodeColors?: Map<string, string>; nodeColors?: Map<string, GraphObjectType>;
nodeClickedCallback?: (node: NodeData) => void; nodeClickedCallback?: (node: Node) => void;
} }
/** /**
...@@ -22,21 +22,23 @@ interface NeighborsProps { ...@@ -22,21 +22,23 @@ interface NeighborsProps {
function Neighbors({ function Neighbors({
neighbors, neighbors,
nodeClickedCallback, nodeClickedCallback,
nodeColors = new Map<string, string>(), nodeColors = new Map<string, GraphObjectType>(),
}: NeighborsProps) { }: 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 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) { for (const cls of classes) {
categories.set(cls, []); categories.set(cls, []);
} }
for (const neighbor of neighbors) { 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) { if (nodeClickedCallback) {
nodeClickedCallback(node); nodeClickedCallback(node);
} }
...@@ -51,7 +53,7 @@ function Neighbors({ ...@@ -51,7 +53,7 @@ function Neighbors({
header={cls} header={cls}
key={cls} key={cls}
heightTransition={false} heightTransition={false}
color={nodeColors.get(cls)} color={nodeColors.get(cls).color}
> >
<ul> <ul>
{categories.get(cls).map((node) => ( {categories.get(cls).map((node) => (
......
import React from "react"; import React from "react";
import "./nodeinfobar.css"; import "./nodeinfobar.css";
import { NodeData } from "../../../common/graph"; import { GraphObjectType, Node } from "../../../common/graph";
import TitleArea from "./titlearea"; import TitleArea from "./titlearea";
import FancyScrollbar from "../fancyscrollbar"; import FancyScrollbar from "../fancyscrollbar";
import MediaArea from "./mediaarea"; import MediaArea from "./mediaarea";
...@@ -9,10 +9,10 @@ import Neighbors from "./neighbors"; ...@@ -9,10 +9,10 @@ import Neighbors from "./neighbors";
interface InfoBarProps { interface InfoBarProps {
height: number; height: number;
node: NodeData; node: Node;
nodeColors?: Map<string, string>; nodeColors?: Map<string, GraphObjectType>;
onClose?: () => void; onClose?: () => void;
nodeClickedCallback?: (node: NodeData) => void; nodeClickedCallback?: (node: Node) => void;
} }
/** /**
......
...@@ -3,12 +3,12 @@ import React, { useEffect, useRef, useState } from "react"; ...@@ -3,12 +3,12 @@ import React, { useEffect, useRef, useState } from "react";
import "./searchbar.css"; import "./searchbar.css";
import searchicon from "./search_icon.svg"; import searchicon from "./search_icon.svg";
import closeicon from "./close_icon.svg"; import closeicon from "./close_icon.svg";
import { NodeData } from "../../common/graph"; import { Node } from "../../common/graph";
interface SearchBarProps { interface SearchBarProps {
minified: boolean; minified: boolean;
nodeSet: NodeData[]; nodeSet: Node[];
onSearch?: (node: NodeData) => void; onSearch?: (node: Node) => void;
nodeColors: Map<string, string>; nodeColors: Map<string, string>;
} }
...@@ -56,7 +56,7 @@ function SearchBar({ ...@@ -56,7 +56,7 @@ function SearchBar({
}) })
.slice(0, 3); .slice(0, 3);
const handleNodeClick = (node: NodeData) => { const handleNodeClick = (node: Node) => {
if (onSearch !== undefined) { if (onSearch !== undefined) {
onSearch(node); onSearch(node);
} }
...@@ -120,7 +120,9 @@ function SearchBar({ ...@@ -120,7 +120,9 @@ function SearchBar({
> >
<div <div
className={"searchbar-results-circle"} className={"searchbar-results-circle"}
style={{ backgroundColor: nodeColors.get(el.type) }} style={{
backgroundColor: el.type.color,
}}
></div> ></div>
<div>{el.name}</div> <div>{el.name}</div>
</div> </div>
......
...@@ -5,7 +5,7 @@ import PropTypes, { InferType } from "prop-types"; ...@@ -5,7 +5,7 @@ import PropTypes, { InferType } from "prop-types";
import "./display.css"; import "./display.css";
import { GraphNode, GraphRenderer } from "./renderer"; import { GraphNode, GraphRenderer } from "./renderer";
import * as Helpers from "./helpers"; import * as Helpers from "./helpers";
import Graph, { NodeData } from "../common/graph"; import Graph, { Node } from "../common/graph";
import { loadGraphJson } from "../common/datasets"; import { loadGraphJson } from "../common/datasets";
import NodeInfoBar from "./components/nodeinfo/nodeinfobar"; import NodeInfoBar from "./components/nodeinfo/nodeinfobar";
import FilterMenu from "./components/nodefilter/filtermenu"; import FilterMenu from "./components/nodefilter/filtermenu";
...@@ -69,11 +69,11 @@ class Display extends React.Component< ...@@ -69,11 +69,11 @@ class Display extends React.Component<
fetchGraph(); fetchGraph();
} }
handleNodeClicked(node: NodeData) { handleNodeClicked(node: Node) {
this.setState({ currentNode: node, nodeActive: true }); this.setState({ currentNode: node, nodeActive: true });
} }
handleNodeChangeRequest(node: NodeData) { handleNodeChangeRequest(node: Node) {
this.rendererRef.current.focusOnNode(node as GraphNode); this.rendererRef.current.focusOnNode(node as GraphNode);
this.rendererRef.current.displayNodeSelection(node as GraphNode); this.rendererRef.current.displayNodeSelection(node as GraphNode);
this.handleNodeClicked(node); this.handleNodeClicked(node);
...@@ -145,7 +145,7 @@ class Display extends React.Component< ...@@ -145,7 +145,7 @@ class Display extends React.Component<
{this.state.currentNode && ( {this.state.currentNode && (
<NodeInfoBar <NodeInfoBar
node={this.state.currentNode} node={this.state.currentNode}
nodeColors={this.state.graph.nodeColors} nodeColors={this.state.graph.nameToObjectGroup}
height={this.state.nodeActive ? this.state.height : 0} height={this.state.nodeActive ? this.state.height : 0}
onClose={() => { onClose={() => {
this.handleNodeClose(); this.handleNodeClose();
...@@ -157,7 +157,7 @@ class Display extends React.Component< ...@@ -157,7 +157,7 @@ class Display extends React.Component<
{this.state.graph && ( {this.state.graph && (
<FilterMenu <FilterMenu
classes={this.graph.nodeColors} classes={this.graph.nameToObjectGroup}
onVisibilityChange={this.handleNodeFilter} onVisibilityChange={this.handleNodeFilter}
/> />
)} )}
......
...@@ -10,9 +10,9 @@ import React from "react"; ...@@ -10,9 +10,9 @@ import React from "react";
import PropTypes, { InferType } from "prop-types"; import PropTypes, { InferType } from "prop-types";
import SpriteText from "three-spritetext"; import SpriteText from "three-spritetext";
import { Object3D, Sprite } from "three"; 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; x: number;
y: number; y: number;
z: number; z: number;
...@@ -26,7 +26,7 @@ export interface GraphNode extends NodeData { ...@@ -26,7 +26,7 @@ export interface GraphNode extends NodeData {
__threeObj: THREE.Group; __threeObj: THREE.Group;
} }
export interface GraphLink extends LinkData { export interface GraphLink extends Link {
__lineObj?: Line2; __lineObj?: Line2;
} }
...@@ -62,11 +62,15 @@ export class GraphRenderer extends React.PureComponent< ...@@ -62,11 +62,15 @@ export class GraphRenderer extends React.PureComponent<
constructor(props: InferType<typeof GraphRenderer.propTypes>) { constructor(props: InferType<typeof GraphRenderer.propTypes>) {
super(props); super(props);
this.reset();
this.forceGraph = React.createRef();
}
reset() {
this.highlightedNodes = new Set(); this.highlightedNodes = new Set();
this.highlightedLinks = new Set(); this.highlightedLinks = new Set();
this.node3dObjects = new Map<string, THREE.Group>(); this.node3dObjects = new Map<string, THREE.Group>();
this.hoverNode = null; this.hoverNode = null;
this.forceGraph = React.createRef();
} }
componentDidMount() { componentDidMount() {
...@@ -112,7 +116,7 @@ export class GraphRenderer extends React.PureComponent< ...@@ -112,7 +116,7 @@ export class GraphRenderer extends React.PureComponent<
const material = new THREE.SpriteMaterial({ const material = new THREE.SpriteMaterial({
//map: imageTexture, //map: imageTexture,
color: this.props.graph.nodeColors.get(node.type), color: node.type.color,
alphaMap: imageAlpha, alphaMap: imageAlpha,
transparent: true, transparent: true,
alphaTest: 0.2, alphaTest: 0.2,
...@@ -129,11 +133,11 @@ export class GraphRenderer extends React.PureComponent< ...@@ -129,11 +133,11 @@ export class GraphRenderer extends React.PureComponent<
return group; return group;
} }
drawLink(link: LinkData) { drawLink(link: Link) {
const colors = new Float32Array( const colors = new Float32Array(
[].concat( [].concat(
...[link.target, link.source] ...[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((color) => color.replace(/[^\d,]/g, "").split(",")) // Extract rgb() color components
.map((rgb) => rgb.map((v: string) => parseInt(v) / 255)) .map((rgb) => rgb.map((v: string) => parseInt(v) / 255))
) )
...@@ -356,6 +360,7 @@ export class GraphRenderer extends React.PureComponent< ...@@ -356,6 +360,7 @@ export class GraphRenderer extends React.PureComponent<
} }
render() { render() {
this.reset();
return ( return (
<ForceGraph3D <ForceGraph3D
ref={this.forceGraph} ref={this.forceGraph}
...@@ -365,7 +370,7 @@ export class GraphRenderer extends React.PureComponent< ...@@ -365,7 +370,7 @@ export class GraphRenderer extends React.PureComponent<
rendererConfig={{ antialias: true }} rendererConfig={{ antialias: true }}
// nodeLabel={"hidden"} // nodeLabel={"hidden"}
nodeThreeObject={(node: GraphNode) => this.drawNode(node)} nodeThreeObject={(node: GraphNode) => this.drawNode(node)}
linkThreeObject={(link: LinkData) => this.drawLink(link)} linkThreeObject={(link: Link) => this.drawLink(link)}
onNodeClick={(node: GraphNode) => this.onNodeClick(node)} onNodeClick={(node: GraphNode) => this.onNodeClick(node)}
onBackgroundClick={() => this.deselectNode()} onBackgroundClick={() => this.deselectNode()}
//d3AlphaDecay={0.1} //d3AlphaDecay={0.1}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment