import * as Config from "../config"; interface LinkData { source: string; target: string; type?: string; } export interface Link { source: Node; target: Node; type?: GraphObjectType; } interface NodeContent { name: string; description?: string; icon?: string; banner?: string; video?: string; references?: string[]; } 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 { x: number; y: number; z: number; } /** * Basic graph data structure. */ export default class Graph { 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), }; }); this.updateNodeData(); this.removeFloatingNodes(); } 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() { 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.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): Node { return this.idToNode.get(id); } 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; } 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.name)); // Filter links depending on type let links; if (linkTypes === undefined) { links = this.links; } else { 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.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.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, }; }), this.objectGroups ); } }