import * as Config from "../config"; interface Link { source: string; target: string; type?: string; } export interface LinkData { source: NodeData; target: NodeData; type?: string; } export interface NodeData { id: string; name: string; description?: string; icon?: string; banner?: string; type?: string; video?: string; references?: string[]; neighbors: NodeData[]; links: LinkData[]; } export interface Coordinate { x: number; y: number; z: number; } /** * 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); }); 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) { node.neighbors = []; node.links = []; } } private removeFloatingNodes() { this.nodes = this.nodes.filter((node) => node.neighbors.length > 0); } /** * Updates the graph data structure to contain additional values. * 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; a.neighbors.push(b); b.neighbors.push(a); a.links.push(link); b.links.push(link); }); } public node(id: string): NodeData { 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] ); } } /** * 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)); // Filter links depending on type let links; if (linkTypes === undefined) { links = this.links; } else { links = this.links.filter((l) => linkTypes.get(l.type)); } // Filter links which are connected to an invisible node links = links.filter( (l) => nodeTypes.get(l.source.type) && nodeTypes.get(l.target.type) ); return new Graph( nodes, links.map((link) => { return { source: link.source.id, target: link.target.id, type: link.type, }; }) ); } }