Skip to content
Snippets Groups Projects
graph.js 7.91 KiB
Newer Older
  • Learn to ignore specific revisions
  • Maximilian Giller's avatar
    Maximilian Giller committed
    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/";
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    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
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    const JSON_CONFIG = PLUGIN_PATH + "datasets/aud1.json";
    
    const STOP_PHYSICS_DELAY = 5000; // ms
    
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    class Graph extends ManagedData {
        constructor(data) {
            super(Graph.addIdentifiers(data));
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.onChangeCallbacks = [];
        }
    
        triggerOnChange() {
            this.onChangeCallbacks.forEach((fn) => fn(this.data));
        }
    
        onRedo() {
            this.triggerOnChange();
        }
    
        onUndo() {
            this.triggerOnChange();
        }
    
    
        storableData(data) {
    
            return this.getCleanData(data, true);
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
        deleteNode(nodeId) {
            // Delete node from nodes
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.data[GRAPH_NODES] = this.data[GRAPH_NODES].filter(
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                (n) => n[NODE_ID] !== nodeId
            );
    
            // Delete links with node
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.data[GRAPH_LINKS] = this.data[GRAPH_LINKS].filter(
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                (l) =>
                    l[LINK_SOURCE][NODE_ID] !== nodeId &&
                    l[LINK_TARGET][NODE_ID] !== nodeId
            );
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
            this.storeCurrentData("Deleted node with id [" + nodeId + "]");
        }
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
        stopPhysics() {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.data[GRAPH_NODES].forEach((n) => {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                n.fx = n.x;
                n.fy = n.y;
            });
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
    Maximilian Giller's avatar
    Maximilian Giller committed
        static addIdentifiers(data) {
            data[GRAPH_NODES].forEach((n) => {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                n.node = true;
                n.link = false;
            });
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            data[GRAPH_LINKS].forEach((l) => {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                l.node = false;
                l.link = true;
            });
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
            return data;
        }
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
        deleteLink(sourceId, targetId) {
            // Only keep links, of one of the nodes is different
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.data[GRAPH_LINKS] = this.data[GRAPH_LINKS].filter(
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                (l) =>
                    l[LINK_SOURCE][NODE_ID] !== sourceId ||
                    l[LINK_TARGET][NODE_ID] !== targetId
            );
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
            this.storeCurrentData(
                "Deleted link connecting [" + sourceId + "] with [" + targetId + "]"
            );
        }
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
        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]
            );
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
        existsLink(sourceId, targetId) {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            const links = this.data[GRAPH_LINKS];
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
            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;
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
        connectNodes(sourceId, targetIds) {
            targetIds.forEach((targetId) => {
                if (
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                    this.existsLink(sourceId, targetId) ||
                    this.existsLink(targetId, sourceId)
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                ) {
                    return;
                }
    
                var link = {};
    
                link[LINK_SOURCE] = sourceId;
                link[LINK_TARGET] = targetId;
    
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                this.data[GRAPH_LINKS].push(link);
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            });
    
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.storeCurrentData(
                "Created link connecting [" +
                    sourceId +
                    "] with [" +
                    targetIds.join() +
                    "]"
            );
        }
    
    
        getCleanData(data = undefined, simulationParameters = false) {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            if (data === undefined) {
                data = this.data;
            }
    
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            var cleanData = {};
            cleanData[GRAPH_LINKS] = [];
            cleanData[GRAPH_NODES] = [];
    
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            data[GRAPH_LINKS].forEach((link) =>
    
                cleanData[GRAPH_LINKS].push(this.getCleanLink(link, simulationParameters))
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            );
    
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            data[GRAPH_NODES].forEach((node) =>
    
                cleanData[GRAPH_NODES].push(this.getCleanNode(node, simulationParameters))
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            );
    
            return cleanData;
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
        getCleanNode(node, simulationParameters) {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            var cleanNode = {};
    
            NODE_PARAMS.forEach((param) => {
                cleanNode[param] = node[param];
            });
    
    
            if (simulationParameters) {
                NODE_SIM_PARAMS.forEach((param) => {
                    cleanNode[param] = node[param];
                });
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            return cleanNode;
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
        getCleanLink(link, simulationParameters) {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            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];
                });
            }
    
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            return cleanLink;
    
    
        existsNodeId(nodeId) {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            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 {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                id = this.getRandomString();
            } while (this.existsNodeId(id));
    
            return id;
    
    
        getRandomString(length = 8) {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            // 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;
    
    Maximilian Giller's avatar
    Maximilian Giller committed
        addLink(sourceId, targetId, linkDetails = {}) {
            // Copy params
            var newLink = linkDetails;
    
            // Make sure the IDs exist
            if (
                sourceId === undefined ||
                targetId === undefined ||
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                this.existsNodeId(sourceId) === false ||
                this.existsNodeId(targetId) === false
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            ) {
                return;
            }
    
            // Make sure the link is unique
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            if (this.existsLink(sourceId, targetId)) {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                return;
            }
    
            newLink[LINK_SOURCE] = sourceId;
            newLink[LINK_TARGET] = targetId;
    
            // Basic node properties
            newLink.link = true;
            newLink.node = false;
    
            // Add node
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.data[GRAPH_LINKS].push(newLink);
            this.triggerOnChange();
    
            this.storeCurrentData(
                "Added custom link connecting [" +
                    sourceId +
                    "] with [" +
                    targetId +
                    "]"
            );
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
            return newLink;
    
        addNode(nodeDetails) {
            // Copy params
            var newNode = nodeDetails;
    
            // Make sure the ID is set and unique
            if (newNode[NODE_ID] === undefined) {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                newNode[NODE_ID] = this.getUnusedNodeId();
            } else if (this.existsNodeId(newNode[NODE_ID])) {
    
                return;
            }
    
            // Basic node properties
            newNode.node = true;
            newNode.link = false;
    
            // Add node
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.data[GRAPH_NODES].push(newNode);
            this.triggerOnChange();
    
            this.storeCurrentData(
                "Added custom node with id [" + newNode[NODE_ID] + "]"
            );
    
    
            return newNode;