const NODE_LABEL = "name"; const NODE_ID = "id"; const NODE_GROUP = "group"; const NODE_DESCRIPTION = "description"; const NODE_IMAGE = "image"; const LINK_SOURCE = "source"; const LINK_TARGET = "target"; const LINK_TYPE = "type"; const LINK_PARTICLE_COUNT = 4; const GRAPH_NODES = "nodes"; const GRAPH_LINKS = "links"; const IMAGE_SIZE = 12; const IMAGE_SRC = PLUGIN_PATH + "datasets/images/"; const LINK_PARAMS = [LINK_TYPE]; const NODE_PARAMS = [NODE_ID, NODE_LABEL, NODE_IMAGE, NODE_DESCRIPTION]; const LINK_SIM_PARAMS = ["index"]; const NODE_SIM_PARAMS = ["index", "x", "y", "vx", "vy", "fx", "fy"]; // Based on https://github.com/d3/d3-force#simulation_nodes const JSON_CONFIG = PLUGIN_PATH + "datasets/aud1.json"; const STOP_PHYSICS_DELAY = 5000; // ms class Graph extends ManagedData { constructor(data) { super(Graph.addIdentifiers(data)); this.onChangeCallbacks = []; } triggerOnChange() { this.onChangeCallbacks.forEach((fn) => fn(this.data)); } onRedo() { this.triggerOnChange(); } onUndo() { this.triggerOnChange(); } storableData(data) { return this.getCleanData(data, true); } deleteNode(nodeId) { // Delete node from nodes this.data[GRAPH_NODES] = this.data[GRAPH_NODES].filter( (n) => n[NODE_ID] !== nodeId ); // Delete links with node this.data[GRAPH_LINKS] = this.data[GRAPH_LINKS].filter( (l) => l[LINK_SOURCE][NODE_ID] !== nodeId && l[LINK_TARGET][NODE_ID] !== nodeId ); this.storeCurrentData("Deleted node with id [" + nodeId + "]"); } stopPhysics() { this.data[GRAPH_NODES].forEach((n) => { n.fx = n.x; n.fy = n.y; }); } static addIdentifiers(data) { data[GRAPH_NODES].forEach((n) => { n.node = true; n.link = false; }); data[GRAPH_LINKS].forEach((l) => { l.node = false; l.link = true; }); return data; } deleteLink(sourceId, targetId) { // Only keep links, of one of the nodes is different this.data[GRAPH_LINKS] = this.data[GRAPH_LINKS].filter( (l) => l[LINK_SOURCE][NODE_ID] !== sourceId || l[LINK_TARGET][NODE_ID] !== targetId ); this.storeCurrentData( "Deleted link connecting [" + sourceId + "] with [" + targetId + "]" ); } isLinkOnNode(link, node) { if (link === undefined || node === undefined) { return false; } if (link.link !== true || node.node !== true) { return false; } return ( link[LINK_SOURCE][NODE_ID] === node[NODE_ID] || link[LINK_TARGET][NODE_ID] === node[NODE_ID] ); } existsLink(sourceId, targetId) { const links = this.data[GRAPH_LINKS]; for (var i = 0; i < links.length; i++) { var link = links[i]; if ( link[LINK_SOURCE][NODE_ID] === sourceId && link[LINK_TARGET][NODE_ID] === targetId ) { return true; } } return false; } connectNodes(sourceId, targetIds) { targetIds.forEach((targetId) => { if ( this.existsLink(sourceId, targetId) || this.existsLink(targetId, sourceId) ) { return; } var link = {}; link[LINK_SOURCE] = sourceId; link[LINK_TARGET] = targetId; this.data[GRAPH_LINKS].push(link); }); this.storeCurrentData( "Created link connecting [" + sourceId + "] with [" + targetIds.join() + "]" ); } getCleanData(data = undefined, simulationParameters = false) { if (data === undefined) { data = this.data; } var cleanData = {}; cleanData[GRAPH_LINKS] = []; cleanData[GRAPH_NODES] = []; data[GRAPH_LINKS].forEach((link) => cleanData[GRAPH_LINKS].push(this.getCleanLink(link, simulationParameters)) ); data[GRAPH_NODES].forEach((node) => cleanData[GRAPH_NODES].push(this.getCleanNode(node, simulationParameters)) ); return cleanData; } getCleanNode(node, simulationParameters) { var cleanNode = {}; NODE_PARAMS.forEach((param) => { cleanNode[param] = node[param]; }); if (simulationParameters) { NODE_SIM_PARAMS.forEach((param) => { cleanNode[param] = node[param]; }); } return cleanNode; } getCleanLink(link, simulationParameters) { var cleanLink = {}; // Source and target nodes // Node ids will be converted to complete node objects on running graphs, gotta convert back cleanLink[LINK_SOURCE] = link[LINK_SOURCE][NODE_ID]; cleanLink[LINK_TARGET] = link[LINK_TARGET][NODE_ID]; // Other parameters LINK_PARAMS.forEach((param) => { cleanLink[param] = link[param]; }); if (simulationParameters) { LINK_SIM_PARAMS.forEach((param) => { cleanLink[param] = link[param]; }); } return cleanLink; } existsNodeId(nodeId) { var nodes = this.data[GRAPH_NODES]; for (var i = 0; i < nodes.length; i++) { if (nodes[i][NODE_ID] === nodeId) { return true; } } return false; } getUnusedNodeId() { var id; do { id = this.getRandomString(); } while (this.existsNodeId(id)); return id; } getRandomString(length = 8) { // Move to global helpers // Based on: https://stackoverflow.com/a/1349426/7376120 var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var charactersLength = characters.length; var result = ""; for (var i = 0; i < length; i++) { result += characters.charAt( Math.floor(Math.random() * charactersLength) ); } return result; } addLink(sourceId, targetId, linkDetails = {}) { // Copy params var newLink = linkDetails; // Make sure the IDs exist if ( sourceId === undefined || targetId === undefined || this.existsNodeId(sourceId) === false || this.existsNodeId(targetId) === false ) { return; } // Make sure the link is unique if (this.existsLink(sourceId, targetId)) { return; } newLink[LINK_SOURCE] = sourceId; newLink[LINK_TARGET] = targetId; // Basic node properties newLink.link = true; newLink.node = false; // Add node this.data[GRAPH_LINKS].push(newLink); this.triggerOnChange(); this.storeCurrentData( "Added custom link connecting [" + sourceId + "] with [" + targetId + "]" ); return newLink; } addNode(nodeDetails) { // Copy params var newNode = nodeDetails; // Make sure the ID is set and unique if (newNode[NODE_ID] === undefined) { newNode[NODE_ID] = this.getUnusedNodeId(); } else if (this.existsNodeId(newNode[NODE_ID])) { return; } // Basic node properties newNode.node = true; newNode.link = false; // Add node this.data[GRAPH_NODES].push(newNode); this.triggerOnChange(); this.storeCurrentData( "Added custom node with id [" + newNode[NODE_ID] + "]" ); return newNode; } }