Skip to content
Snippets Groups Projects
graph.js 11.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • import ManagedData from "./manageddata";
    
    import { PLUGIN_PATH, COLOR_PALETTE } from "../../config";
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
    const LINK_NAME_CONNECTOR = "";
    
    
    export const NODE_LABEL = "name";
    export const NODE_ID = "id";
    
    export const NODE_TYPE = "type";
    
    export const NODE_DESCRIPTION = "description";
    export const NODE_IMAGE = "image";
    
    export const NODE_REFERENCES = "infoLinks";
    export const NODE_VIDEO = "video";
    export const NODE_DETAIL_IMAGE = "infoImage";
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
    export const LINK_SOURCE = "source";
    export const LINK_TARGET = "target";
    export const LINK_TYPE = "type";
    export const LINK_PARTICLE_COUNT = 4;
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
    export const GRAPH_NODES = "nodes";
    export const GRAPH_LINKS = "links";
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
    export const IMAGE_SIZE = 12;
    export const IMAGE_SRC = PLUGIN_PATH + "datasets/images/";
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
    export const LINK_PARAMS = [];
    
    export const NODE_PARAMS = [
        NODE_ID,
        NODE_LABEL,
        NODE_IMAGE,
        NODE_DESCRIPTION,
        NODE_REFERENCES,
    
        NODE_DETAIL_IMAGE,
    
    export const LINK_SIM_PARAMS = ["index"];
    export 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
    
    
    export const JSON_CONFIG = PLUGIN_PATH + "datasets/aud1v2.json";
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
    export const STOP_PHYSICS_DELAY = 5000; // ms
    
    export class Graph extends ManagedData {
    
    Maximilian Giller's avatar
    Maximilian Giller committed
        constructor(data) {
            super(Graph.addIdentifiers(data));
    
            this.calculateNodeTypes();
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.onChangeCallbacks = [];
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            this.physicsDelay = STOP_PHYSICS_DELAY;
    
            this.physicsStopTimeoutId = undefined;
            this.graphRenderer = undefined;
    
    Maximilian Giller's avatar
    Maximilian Giller committed
        }
    
    
        setRenderer(graphRenderer) {
            this.graphRenderer = graphRenderer;
        }
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
    
        restartSimulation() {
            if (this.physicsStopTimeoutId !== undefined) {
                clearTimeout(this.physicsStopTimeoutId);
            }
    
    
            if (this.graphRenderer !== undefined) {
    
                this.data = Graph.addIdentifiers(this.getCleanData(this.data, false));
                this.triggerOnChange();
    
            }
    
            // Deactivate physics after a short delay
            this.physicsStopTimeoutId = setTimeout(() => {
                this.stopPhysics();
    
                this.storeCurrentData("Physics stopped", false);
    
                this.physicsStopTimeoutId = undefined;
            }, this.physicsDelay);
    
    Maximilian Giller's avatar
    Maximilian Giller committed
        }
    
        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
    
    
        /**
         * Based on the function from the 3d-graph code from @JoschaRode
         */
    
        calculateNodeTypes() {
            const nodeClasses = [];
    
            this.data[GRAPH_NODES].forEach((node) =>
                nodeClasses.push(node[NODE_TYPE])
    
            this.nodeTypes = [...new Set(nodeClasses)];
    
        getNodeColor(node) {
            return this.getTypeColor(node[NODE_TYPE]);
    
        getTypeColor(typeClass) {
            var classIndex = this.nodeTypes.indexOf(typeClass);
    
            if (classIndex <= -1) {
                return 'black';
            }
    
    
            return COLOR_PALETTE[classIndex % COLOR_PALETTE.length];
        }
    
    
    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
    
    
        changeDetails(selectionDetails) {
            if (selectionDetails.node === true) {
    
                this.changeNodeDetails(selectionDetails[NODE_ID], selectionDetails);
    
            } else if (selectionDetails.link === true) {
                this.changeLinkDetails(
                    selectionDetails[LINK_SOURCE][NODE_ID],
                    selectionDetails[LINK_TARGET][NODE_ID],
                    selectionDetails
                );
            }
        }
    
        changeNodeDetails(nodeId, newDetails) {
            var nodes = this.data[GRAPH_NODES];
            for (var i = 0; i < nodes.length; i++) {
                // Is relevant node?
                if (nodes[i][NODE_ID] !== nodeId) {
                    continue; // No
                }
    
    
                nodes[i] = Object.assign(nodes[i], newDetails);
    
                // All done
    
                this.storeCurrentData("Changed node details");
    
                return;
            }
        }
    
        changeLinkDetails(sourceId, targetId, newDetails) {
            var links = this.data[GRAPH_LINKS];
            for (var i = 0; i < links.length; i++) {
                // Is relevant link?
                if (
                    links[i][LINK_SOURCE][NODE_ID] !== sourceId ||
                    links[i][LINK_TARGET][NODE_ID] !== targetId
                ) {
                    continue; // No
                }
    
    
                links[i] = Object.assign(links[i], newDetails);
    
                // All done
    
                this.storeCurrentData("Changed link details");
    
    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;
                }
    
    
                this.addLink(sourceId, targetId);
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            });
    
        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 = {};
    
    
    Maximilian Giller's avatar
    Maximilian Giller committed
            // Assuming that all nodes are valid, there are two possible formats
            // 1. source and target are node objects
            if (link[LINK_SOURCE][NODE_ID] !== undefined) {
                // 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];
            } else {
                // 2. source and target are just node ids
                cleanLink[LINK_SOURCE] = link[LINK_SOURCE];
                cleanLink[LINK_TARGET] = link[LINK_TARGET];
            }
    
    Maximilian Giller's avatar
    Maximilian Giller committed
    
            // 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;
    
    
        static toStr(item) {
            if (item === undefined) {
                return "UNDEFINED";
            }
    
            if (item.node) {
    
                return item[NODE_LABEL];
    
            } else if (item.link) {
                return (
    
                    Graph.toStr(item[LINK_SOURCE]) +
    
                    LINK_NAME_CONNECTOR +
    
                    Graph.toStr(item[LINK_TARGET])
    
                );
            } else {
                return "UNDEFINED";
            }
        }