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

Add support for HyperEdges

parent efa1eda1
......@@ -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": {
......
......@@ -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,65 +357,87 @@ export default class GraphEditor extends Component {
nodeIdsWithChildren[childData.parent] = true;
}
Object.keys(childData.sets)
.forEach((setName) => {
if (!childData.sets[setName] || activeFilters[`sets$${setName}`]) {
return;
}
childData.sets[setName].forEach((setMemberData) => {
const edgeId = `${id}$${setName}$${setMemberData.id}`;
const edgeData = {
data: {
id: edgeId,
source: id,
target: setMemberData.id,
label: setName,
memberAttrs: setMemberData.memberAttrs,
},
classes: `set-member ${activeSelection.includes(edgeId)
? 'in-active-selection' : ''}`,
};
if (setMemberData.label !== null) {
edgeData.data.label = setMemberData.label;
}
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})`,
},
});
}
}
Object.keys(childData.pointers)
.forEach((pName) => {
if (!childData.pointers[pName] || activeFilters[`pointers$${pName}`]) {
if (createPointer && createPointer.target === id) {
cytoData.classes += ' valid-pointer-target';
}
Object.keys(childData.sets)
.forEach((setName) => {
if (!childData.sets[setName] || activeFilters[`sets$${setName}`]) {
return;
}
childData.sets[setName].forEach((setMemberData) => {
if (!targetsExist(id, setMemberData.id)) {
return;
}
const edgeId = `${id}$${pName}$${childData.pointers[pName]}`;
const edgeId = `${id}$${setName}$${setMemberData.id}`;
const edgeData = {
data: {
id: edgeId,
source: id,
target: childData.pointers[pName],
label: pName,
target: setMemberData.id,
label: setName,
memberAttrs: setMemberData.memberAttrs,
},
classes: `${pName === 'base' ? 'base-pointer' : 'pointer'}\
${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
classes: `set-member ${activeSelection.includes(edgeId)
? 'in-active-selection' : ''}`,
};
result.elements.edges.push(edgeData);
if (setMemberData.label !== null) {
edgeData.data.label = setMemberData.label;
}
if (hasEdgeTargets(id, setMemberData.id)) {
result.hyperEdges.push(edgeData);
hyperTargets[id] = true;
hyperTargets[setMemberData.id] = true;
} else {
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})`,
Object.keys(childData.pointers)
.forEach((pName) => {
const targetId = childData.pointers[pName];
if (!targetId || activeFilters[`pointers$${pName}`] || !targetsExist(id, targetId) ||
(isGmeConnection && (pName === 'src' || pName === 'dst'))) {
return;
}
const edgeId = `${id}$${pName}$${targetId}`;
const edgeData = {
data: {
id: edgeId,
source: id,
target: targetId,
label: pName,
},
});
}
}
classes: `${pName === 'base' ? 'base-pointer' : 'pointer'}\
${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
};
if (hasEdgeTargets(id, targetId)) {
result.hyperEdges.push(edgeData);
hyperTargets[id] = true;
hyperTargets[targetId] = true;
} else {
edges.push(edgeData);
}
});
});
Object.keys(nodeIdsWithChildren)
......@@ -379,6 +445,15 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
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()]);
});
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;
......@@ -629,7 +705,7 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
onClose={() => {
this.setState({showNodeMenu: null});
}}
createPointer = {this.startNewPointer}
createPointer={this.startNewPointer}
setActiveNode={setActiveNode}
/>) : null}
......
......@@ -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) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment