UNPKG

mgraph.graph

Version:

Modern graph data structure for JavaScript – a refactoring of ngraph.graph using mgraph.events

387 lines (318 loc) 11.1 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.createGraph = factory()); })(this, (function () { 'use strict'; // index.js function eventify(subject) { if (!subject) { throw new Error('Eventify cannot use a falsy object as events subject'); } for (const prop of ['on', 'off', 'fire']) { if (Object.prototype.hasOwnProperty.call(subject, prop)) { throw new Error(`Subject already has property '${prop}'`); } } // Use a Map to store event listeners: const events = new Map(); subject.on = function (eventName, callback, ctx) { if (typeof callback !== 'function') { throw new Error('Callback is expected to be a function'); } if (!events.has(eventName)) { events.set(eventName, []); } events.get(eventName).push({ callback, ctx }); return subject; }; subject.off = function (eventName, callback) { // Remove all events if eventName is undefined: if (eventName === undefined) { events.clear(); return subject; } if (events.has(eventName)) { // Remove all handlers for this event if callback is not a function: if (typeof callback !== 'function') { events.delete(eventName); } else { const filtered = events.get(eventName).filter( handler => handler.callback !== callback ); if (filtered.length) { events.set(eventName, filtered); } else { events.delete(eventName); } } } return subject; }; subject.fire = function (eventName, ...args) { const handlers = events.get(eventName); if (handlers) { for (const { callback, ctx } of handlers) { callback.apply(ctx, args); } } return subject; }; return subject; } // index.js /** * mgraph.graph – A graph data structure for JavaScript. * Modern refactoring of ngraph.graph using mgraph.events. */ function createGraph(options = {}) { // Support legacy option; prefer using { multigraph: true }. if ('uniqueLinkId' in options) { console.warn( 'mgraph.graph: `uniqueLinkId` is deprecated. Use `multigraph` instead.' ); options.multigraph = options.uniqueLinkId; } if (options.multigraph === undefined) options.multigraph = false; if (typeof Map !== 'function') { throw new Error('mgraph.graph requires Map to be defined. Please polyfill it.'); } const nodes = new Map(); // nodeId => Node const links = new Map(); // linkId => Link const multiEdges = {}; let suspendEvents = 0; const changes = []; let recordLinkChange = noop; let recordNodeChange = noop; let enterModification = noop; let exitModification = noop; // Depending on options create single or unique links: const createLink = options.multigraph ? createUniqueLink : createSingleLink; const graph = { version: 1.0, addNode, addLink, removeLink, removeNode, getNode, hasNode, getLink, hasLink, getNodeCount, getNodesCount: getNodeCount, getLinkCount, getLinksCount: getLinkCount, getLinks, forEachNode, forEachLink, forEachLinkedNode, beginUpdate: enterModificationReal, endUpdate: exitModificationReal, clear }; // Add event methods (on, off, fire) from mgraph.events: eventify(graph); monitorSubscribers(); return graph; function monitorSubscribers() { const realOn = graph.on; graph.on = function (...args) { // Switch on event recording once someone subscribes: graph.beginUpdate = enterModification = enterModificationReal; graph.endUpdate = exitModification = exitModificationReal; recordLinkChange = recordLinkChangeReal; recordNodeChange = recordNodeChangeReal; graph.on = realOn; return realOn.apply(graph, args); }; } function recordLinkChangeReal(link, changeType) { changes.push({ link, changeType }); } function recordNodeChangeReal(node, changeType) { changes.push({ node, changeType }); } function addNode(nodeId, data) { if (nodeId === undefined) throw new Error('Invalid node identifier'); enterModification(); let node = getNode(nodeId); if (!node) { node = new Node(nodeId, data); nodes.set(nodeId, node); recordNodeChange(node, 'add'); } else { node.data = data; recordNodeChange(node, 'update'); } exitModification(); return node; } function getNode(nodeId) { return nodes.get(nodeId); } function hasNode(nodeId) { return nodes.has(nodeId); } function removeNode(nodeId) { const node = getNode(nodeId); if (!node) return false; enterModification(); // Remove all links connected to this node if (node.links) { // Create a copy of the links set to avoid modification during iteration const linksToRemove = Array.from(node.links); linksToRemove.forEach(link => removeLinkInstance(link)); } nodes.delete(nodeId); recordNodeChange(node, 'remove'); exitModification(); return true; } function addLink(fromId, toId, data) { enterModification(); const fromNode = getNode(fromId) || addNode(fromId); const toNode = getNode(toId) || addNode(toId); const link = createLink(fromId, toId, data); const isUpdate = links.has(link.id); links.set(link.id, link); addLinkToNode(fromNode, link); if (fromId !== toId) addLinkToNode(toNode, link); recordLinkChange(link, isUpdate ? 'update' : 'add'); exitModification(); return link; } function createSingleLink(fromId, toId, data) { const linkId = makeLinkId(fromId, toId); const prevLink = links.get(linkId); if (prevLink) { prevLink.data = data; return prevLink; } return new Link(fromId, toId, data, linkId); } function createUniqueLink(fromId, toId, data) { let linkId = makeLinkId(fromId, toId); const isMultiEdge = Object.prototype.hasOwnProperty.call(multiEdges, linkId); if (isMultiEdge || getLink(fromId, toId)) { if (!isMultiEdge) multiEdges[linkId] = 0; const suffix = '@' + (++multiEdges[linkId]); linkId = makeLinkId(fromId + suffix, toId + suffix); } return new Link(fromId, toId, data, linkId); } function getNodeCount() { return nodes.size; } function getLinkCount() { return links.size; } function getLinks(nodeId) { const node = getNode(nodeId); return node ? node.links : null; } function removeLink(link, toNodeId) { if (toNodeId !== undefined) { // Called as removeLink(fromId, toId) link = getLink(link, toNodeId); } return removeLinkInstance(link); } function removeLinkInstance(link) { if (!link || !links.has(link.id)) return false; enterModification(); links.delete(link.id); const fromNode = getNode(link.fromId); const toNode = getNode(link.toId); if (fromNode && fromNode.links) fromNode.links.delete(link); if (toNode && toNode.links && fromNode !== toNode) toNode.links.delete(link); recordLinkChange(link, 'remove'); exitModification(); return true; } function getLink(fromNodeId, toNodeId) { if (fromNodeId === undefined || toNodeId === undefined) return undefined; return links.get(makeLinkId(fromNodeId, toNodeId)); } function hasLink(fromNodeId, toNodeId) { if (fromNodeId === undefined || toNodeId === undefined) return false; return links.has(makeLinkId(fromNodeId, toNodeId)); } function clear() { enterModification(); // Clear all nodes and links const nodeIds = Array.from(nodes.keys()); nodeIds.forEach(nodeId => removeNode(nodeId)); exitModification(); } function forEachLink(callback) { if (typeof callback !== 'function') return; for (const link of links.values()) { if (callback(link)) return true; } return false; } function forEachLinkedNode(nodeId, callback, oriented) { const node = getNode(nodeId); if (!node || !node.links || typeof callback !== 'function') return; return oriented ? forEachOrientedLink(node.links, nodeId, callback) : forEachNonOrientedLink(node.links, nodeId, callback); } function forEachNonOrientedLink(linksSet, nodeId, callback) { for (const link of linksSet.values()) { const linkedNodeId = link.fromId === nodeId ? link.toId : link.fromId; const linkedNode = nodes.get(linkedNodeId); if (linkedNode && callback(linkedNode, link)) return true; } return false; } function forEachOrientedLink(linksSet, nodeId, callback) { for (const link of linksSet.values()) { if (link.fromId === nodeId) { const linkedNode = nodes.get(link.toId); if (linkedNode && callback(linkedNode, link)) return true; } } return false; } function forEachNode(callback) { if (typeof callback !== 'function') return; for (const node of nodes.values()) { if (callback(node)) return true; } return false; } function enterModificationReal() { suspendEvents++; } function exitModificationReal() { suspendEvents--; if (suspendEvents === 0 && changes.length > 0) { graph.fire('changed', changes); changes.length = 0; } } function noop() {} } function Node(id, data) { this.id = id; this.links = null; this.data = data; } function addLinkToNode(node, link) { if (node.links) { node.links.add(link); } else { node.links = new Set([link]); } } function Link(fromId, toId, data, id) { this.fromId = fromId; this.toId = toId; this.data = data; this.id = id; } function makeLinkId(fromId, toId) { return fromId.toString() + '👉' + toId.toString(); } return createGraph; }));