Commit dedf696b authored by Patrik Meijer's avatar Patrik Meijer
Browse files

Add support for HyperEdges

parent efa1eda1
Loading
Loading
Loading
Loading
+10 −5
Original line number Diff line number Diff line
@@ -2439,9 +2439,9 @@
      "dev": true
    },
    "cytoscape": {
      "version": "3.2.12",
      "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.2.12.tgz",
      "integrity": "sha512-epagPckuFpLO2hiKgKMXmCFBFM2K7XbJhYDQj8fH3riiEH+yFXpLYA30jV18TEbVcsq2c58ETOGnHE2YzSBr/Q==",
      "version": "3.4.1",
      "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.4.1.tgz",
      "integrity": "sha512-oHbpo01yd4SB3TjOc/EU4C66TmauROo2+4tKtpLyFnk+/mu5R6OIlARt6OFSWrgoa1AB2f4wrcU0UnBRqhGNNw==",
      "requires": {
        "heap": "^0.2.6",
        "lodash.debounce": "^4.0.8"
@@ -2460,6 +2460,11 @@
        "dagre": "^0.8.2"
      }
    },
    "cytoscape-edge-connections": {
      "version": "0.3.3",
      "resolved": "https://registry.npmjs.org/cytoscape-edge-connections/-/cytoscape-edge-connections-0.3.3.tgz",
      "integrity": "sha512-e6W3nFsyxS9XvpFx5OcLHiT3LlF1dQcUdfxVStYdXFhaFVas9u7xndhiPwAE7GXyjIyh+3urVwvFO8XFakB9Qw=="
    },
    "dagre": {
      "version": "0.8.4",
      "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.4.tgz",
@@ -4356,7 +4361,7 @@
    },
    "http-errors": {
      "version": "1.6.3",
      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
      "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
      "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
      "dev": true,
      "requires": {
@@ -4385,7 +4390,7 @@
    },
    "http-proxy-middleware": {
      "version": "0.18.0",
      "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
      "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
      "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==",
      "dev": true,
      "requires": {
+2 −1
Original line number Diff line number Diff line
@@ -23,9 +23,10 @@
    "@material-ui/core": "3.1.0",
    "@material-ui/icons": "3.0.1",
    "blockies": "0.0.2",
    "cytoscape": "3.2.12",
    "cytoscape": "3.4.1",
    "cytoscape-cose-bilkent": "^4.0.0",
    "cytoscape-dagre": "^2.2.2",
    "cytoscape-edge-connections": "^0.3.3",
    "immutability-helper": "^2.8.1",
    "react-color": "^2.14.1",
    "react-samy-svg": "^3.0.1",
+139 −63
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ const CONSTANTS = {

const DEFAULT_STYLES = [
    {
        selector: 'node[hasChildren]',
        selector: 'node.gme-node[hasChildren]',
        style: {
            content: 'data(label)',
            // http://js.cytoscape.org/#style/background-image
@@ -36,7 +36,7 @@ const DEFAULT_STYLES = [
        },
    },
    {
        selector: 'node[^hasChildren]',
        selector: 'node.gme-node[^hasChildren]',
        style: {
            content: 'data(label)',
            // http://js.cytoscape.org/#style/background-image
@@ -44,20 +44,31 @@ const DEFAULT_STYLES = [
            'background-height': '80%',
        },
    },
    {
        selector: 'node.aux-node',
        css: {
            width: 6,
            height: 6,
        },
    },
    {
        selector: 'edge.pointer',
        style: {
            content: 'data(label)',
            'line-color': 'rgb(0,0,255)',
            'curve-style': 'bezier',
            'target-arrow-color': 'rgb(0,0,255)',
            'target-arrow-shape': 'open',
            'target-arrow-shape': 'vee',
        },
    },
    {
        selector: 'edge.set-member',
        style: {
            content: 'data(label)',
            'curve-style': 'bezier',
            'line-color': 'rgb(255,0,255)',
            'target-arrow-color': 'rgb(255,0,255)',
            'target-arrow-shape': 'vee',
        },
    },
    {
@@ -65,6 +76,7 @@ const DEFAULT_STYLES = [
        style: {
            width: 1,
            'line-color': 'rgb(255,0,0)',
            'curve-style': 'bezier',
            'target-arrow-fill': 'hollow',
            'target-arrow-color': 'rgb(255,0,0)',
            'target-arrow-shape': 'triangle',
@@ -245,8 +257,13 @@ export default class GraphEditor extends Component {
                edges: [],
            },
            style: [],
            hyperEdges: [],
        };

        // This will be put in either elements.edges (regular ones) or in hyperEdges.
        const edges = [];
        const hyperTargets = {};

        const nodeMap = {};
        const nodeIdsWithChildren = {};

@@ -263,6 +280,28 @@ export default class GraphEditor extends Component {
                || parentNode.registries.position));
        };

        const isRenderedAsGmeConnection = (id) => {
            return nodes[id] &&
                typeof nodes[id].pointers.src === 'string' &&
                typeof nodes[id].pointers.dst === 'string' &&
                targetsExist(nodes[id].pointers.src, nodes[id].pointers.dst);
        }

        const targetsExist = (src, dst) => {
            return nodes[src] && nodes[dst] && src !== activeNode && dst !== activeNode;
        };

        const hasEdgeTargets = (src, dst) => {
            function isEdge(target) {
                return nodes[target].pointers &&
                    typeof nodes[target].pointers.src === 'string' &&
                    typeof nodes[target].pointers.dst === 'string' &&
                    targetsExist(nodes[target].pointers.src, nodes[target].pointers.dst);
            }

            return isEdge(src) || isEdge(dst);
        };

        Object.keys(nodes)
            .forEach((id) => {
                if (id === activeNode) {
@@ -270,21 +309,30 @@ export default class GraphEditor extends Component {
                }

                const childData = nodes[id];
                const isGmeConnection = isRenderedAsGmeConnection(id);

                if (activeFilters[`nodes$${childData.metaType}`]) {
                    return;
                }

                if (typeof childData.pointers.src === 'string' && typeof childData.pointers.dst === 'string') {
                    result.elements.edges.push({
                if (isGmeConnection) {
                    const edgeData = {
                        data: {
                            id,
                            label: childData.attributes.name,
                            source: childData.pointers.src,
                            target: childData.pointers.dst,
                            label: childData.attributes.name,
                        },
                        classes: `${activeSelection.includes(id) ? 'in-active-selection ' : ''}gme-connection`,
                    });
                    };

                    if (hasEdgeTargets(childData.pointers.src, childData.pointers.dst)) {
                        result.hyperEdges.push(edgeData);
                        hyperTargets[childData.pointers.src] = true;
                        hyperTargets[childData.pointers.dst] = true;
                    } else {
                        edges.push(edgeData);
                    }
                } else {
                    const cytoData = {
                        data: {
@@ -298,13 +346,9 @@ export default class GraphEditor extends Component {
                        },
                        position: getPosition(id),
                        grabbable: !readOnly,
                        classes: `${activeSelection.includes(id) ? 'in-active-selection' : ''}`,
                        classes: `gme-node${activeSelection.includes(id) ? ' in-active-selection' : ''}`,
                    };

                    if (createPointer && createPointer.target === id) {
                        cytoData.classes += ' valid-pointer-target';
                    }

                    result.elements.nodes.push(cytoData);

                    // Keep track of the containers s.t. they cannot be grabbed.
@@ -313,6 +357,21 @@ export default class GraphEditor extends Component {
                        nodeIdsWithChildren[childData.parent] = true;
                    }

                    // Use the images defined for the node.
                    if (childData.registries.SVGIcon && childData.registries.SVGIcon.indexOf('<') === -1) {
                        result.style.push({
                            selector: `node[id = "${id}"]`,
                            style: {
                                'background-image': `url(/assets/DecoratorSVG/${childData.registries.SVGIcon})`,
                            },
                        });
                    }
                }

                if (createPointer && createPointer.target === id) {
                    cytoData.classes += ' valid-pointer-target';
                }

                Object.keys(childData.sets)
                    .forEach((setName) => {
                        if (!childData.sets[setName] || activeFilters[`sets$${setName}`]) {
@@ -320,6 +379,10 @@ export default class GraphEditor extends Component {
                        }

                        childData.sets[setName].forEach((setMemberData) => {
                            if (!targetsExist(id, setMemberData.id)) {
                                return;
                            }

                            const edgeId = `${id}$${setName}$${setMemberData.id}`;
                            const edgeData = {
                                data: {
@@ -337,48 +400,60 @@ export default class GraphEditor extends Component {
                                edgeData.data.label = setMemberData.label;
                            }

                                result.elements.edges.push(edgeData);
                            if (hasEdgeTargets(id, setMemberData.id)) {
                                result.hyperEdges.push(edgeData);
                                hyperTargets[id] = true;
                                hyperTargets[setMemberData.id] = true;
                            } else {
                                edges.push(edgeData);
                            }
                        });
                    });

                Object.keys(childData.pointers)
                    .forEach((pName) => {
                            if (!childData.pointers[pName] || activeFilters[`pointers$${pName}`]) {
                        const targetId = childData.pointers[pName];
                        if (!targetId || activeFilters[`pointers$${pName}`] || !targetsExist(id, targetId) ||
                            (isGmeConnection && (pName === 'src' || pName === 'dst'))) {
                            return;
                        }

                            const edgeId = `${id}$${pName}$${childData.pointers[pName]}`;
                        const edgeId = `${id}$${pName}$${targetId}`;
                        const edgeData = {
                            data: {
                                id: edgeId,
                                source: id,
                                    target: childData.pointers[pName],
                                target: targetId,
                                label: pName,
                            },
                            classes: `${pName === 'base' ? 'base-pointer' : 'pointer'}\
${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
                        };

                            result.elements.edges.push(edgeData);
                        });

                    // Use the images defined for the node.
                    if (childData.registries.SVGIcon && childData.registries.SVGIcon.indexOf('<') === -1) {
                        result.style.push({
                            selector: `node[id = "${id}"]`,
                            style: {
                                'background-image': `url(/assets/DecoratorSVG/${childData.registries.SVGIcon})`,
                            },
                        });
                    }
                        if (hasEdgeTargets(id, targetId)) {
                            result.hyperEdges.push(edgeData);
                            hyperTargets[id] = true;
                            hyperTargets[targetId] = true;
                        } else {
                            edges.push(edgeData);
                        }
                    });
            });

        Object.keys(nodeIdsWithChildren)
            .forEach((id) => {
                nodeMap[id].data.hasChildren = true;
            });

        // Resolve edges that need to be hyperEdges
        edges.forEach((edgeData) => {
            if (hyperTargets[edgeData.data.id]) {
                result.hyperEdges.push(edgeData);
            } else {
                result.elements.edges.push(edgeData);
            }
        })

        return result;
    }

@@ -450,7 +525,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,

        this.cy.on('free', ({target}) => {
            const {setActiveSelection} = this.props;
            if (typeof target.id === 'function') {
            if (typeof target.id === 'function' && target.hasClass('aux-node') === false) {
                // console.log('free', cyNode.id(), JSON.stringify(this.reposition));
                this.storePosition(target.id());
                setTimeout(() => {
@@ -471,7 +546,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
                if (createPointer && createPointer.target === e.target.id()) {
                    this.setState({createPointer: null});
                    gmeClient.setPointer(createPointer.nodeId, createPointer.ptrName, createPointer.target);
                } else {
                } else if (e.target.hasClass('aux-node') === false) {
                    // console.log('vclick', e.target.id(), JSON.stringify(this.reposition));
                    this.setState({
                        showNodeMenu: {
@@ -484,11 +559,11 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
                        },
                        createPointer: null,
                    });
                }

                    setTimeout(() => {
                        setActiveSelection([e.target.id()]);
                    });
                }
            } else {
                setActiveSelection([]);
            }
@@ -608,6 +683,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
                    width={width}
                    height={height}
                    elements={cytoData.elements}
                    hyperEdges={cytoData.hyperEdges}
                    cyRef={(cy) => {
                        if (!this.cy) {
                            this.cy = cy;
+47 −35
Original line number Diff line number Diff line
@@ -8,19 +8,20 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';

import cytoscape from 'cytoscape';
import edgeConnections from 'cytoscape-edge-connections';
import coseBilkent from 'cytoscape-cose-bilkent';
import dagre from 'cytoscape-dagre';


cytoscape.use(edgeConnections);
cytoscape.use(coseBilkent);

// cytoscape.use(cycola);
cytoscape.use(dagre);

export default class ReactCytoscape extends Component {
    static propTypes = {
        cyRef: PropTypes.func.isRequired,
        elements: PropTypes.object.isRequired,
        hyperEdges: PropTypes.arrayOf(PropTypes.object).isRequired,
        width: PropTypes.number.isRequired,
        height: PropTypes.number.isRequired,
        containerID: PropTypes.string,
@@ -34,37 +35,44 @@ export default class ReactCytoscape extends Component {
        layout: {name: 'cola'},
        cytoscapeOptions: {},
        style: [
            {
                selector: 'node',
                css: {
                    content: function elemRender(ele) {
                        return ele.data('label') || ele.data('id');
                    },
                    'text-valign': 'center',
                    'text-halign': 'center',
                },
            },
            {
                selector: '$node > node',
                css: {
                    'padding-top': '10px',
                    'padding-left': '10px',
                    'padding-bottom': '10px',
                    'padding-right': '10px',
                    'text-valign': 'top',
                    'text-halign': 'center',
                    'background-color': '#bbb',
                },
            },
            {
                selector: ':selected',
                css: {
                    'background-color': 'black',
                    'line-color': 'black',
                    'target-arrow-color': 'black',
                    'source-arrow-color': 'black',
                },
            },
            // {
            //     selector: 'node.gme-node',
            //     css: {
            //         content: function elemRender(ele) {
            //             return ele.data('label') || ele.data('id');
            //         },
            //         'text-valign': 'center',
            //         'text-halign': 'center',
            //     },
            // },
            // {
            //     selector: '$node > node',
            //     css: {
            //         'padding-top': '10px',
            //         'padding-left': '10px',
            //         'padding-bottom': '10px',
            //         'padding-right': '10px',
            //         'text-valign': 'top',
            //         'text-halign': 'center',
            //         'background-color': '#bbb',
            //     },
            // },
            // {
            //     selector: ':selected',
            //     css: {
            //         'background-color': 'black',
            //         'line-color': 'black',
            //         'target-arrow-color': 'black',
            //         'source-arrow-color': 'black',
            //     },
            // },
            // {
            //     selector: 'node.aux-node',
            //     css: {
            //         width: '3px',
            //         height: '3px',
            //     },
            // },
        ],
    };

@@ -80,8 +88,9 @@ export default class ReactCytoscape extends Component {
        }, this.props.cytoscapeOptions);

        this.cy = cytoscape(opts);
        // this.cy.layout({name: 'cose-bilkent', coseBilkentOptions});
        this.edgeConnHandler = this.cy.edgeConnections();
        this.cy.json({elements: this.props.elements});
        this.edgeConnHandler.addEdges(this.props.hyperEdges);

        if (this.props.cyRef) {
            this.props.cyRef(this.cy, this.container);
@@ -93,14 +102,17 @@ export default class ReactCytoscape extends Component {
    componentWillReceiveProps(nextProps) {
        const {
            elements,
            hyperEdges,
            width,
            height,
            style,
        } = this.props;

        // TODO: Consider making more fine-grained updates here instead.
        if (JSON.stringify(elements) !== JSON.stringify(nextProps.elements)) {
        if (JSON.stringify(elements) !== JSON.stringify(nextProps.elements) ||
            JSON.stringify(hyperEdges) !== JSON.stringify(nextProps.hyperEdges)) {
            this.cy.json({elements: nextProps.elements});
            this.edgeConnHandler.addEdges(nextProps.hyperEdges);
        }

        if (width !== nextProps.width || height !== nextProps.height) {