diff --git a/app/components/architecture-visualisation-cytoscape/component.js b/app/components/architecture-visualisation-cytoscape/component.js index b9e8b24ccf828ba9909f8acf9a2313d91cb56d3d..49002fd47bceec8e6482c504ff0c5760df1e5926 100644 --- a/app/components/architecture-visualisation-cytoscape/component.js +++ b/app/components/architecture-visualisation-cytoscape/component.js @@ -5,7 +5,7 @@ import cytoscapeStyle from './style'; import _ from 'npm:lodash'; import coseBilkent from 'npm:cytoscape-cose-bilkent'; -const { Component, inject, observer, on } = Ember; +const { Component, inject, observer, on, computed } = Ember; coseBilkent(cytoscape); // register @@ -37,6 +37,37 @@ export default Component.extend({ visualisationEvents.off('resizeEnd', resizeListener); }); }, + layoutOptions: computed('visualisationSettings.layoutAlgorithm', function() { + return { + name: this.get('visualisationSettings.layoutAlgorithm'), + randomize: false, // kose-bilkent will randomize node positions + // maxSimulationTime: 1000, + // padding: 6, + // ungrabifyWhileSimulating: false, + + // webcola options + // infinite: false // blocks all interaction + refresh: 4, // fast animation + avoidOverlap: true, + edgeLength: 250, // should be at least two times the diagonal of a block, blocks are 100x60, therefore around 2*116 + unconstrIter: 1, // unconstrained initial layout iterations + userConstIter: 0, // initial layout iterations with user-specified constraints - we don't have any user constraints + allConstIter: 1, // initial layout iterations with all constraints including non-overlap + infinite: true, + fit: false + }; + }), + + /** + * Since not every layout algorthim supports the reuse of the same cytoscape instance, we need to filter them. + * Currently cose-bilkent and cose require a complete rerender + * @property + * @return {Boolean} + * @readonly + */ + layoutSupportsReuse: computed('visualisationSettings.layoutAlgorithm', function() { + return ['cose-bilkent', 'cose'].indexOf(this.get('visualisationSettings.layoutAlgorithm')) < 0; + }), /** * Observer method that renders the visualisation in a canvas using Cytoscape * @method renderGraph @@ -44,58 +75,119 @@ export default Component.extend({ renderGraph: on('didInsertElement', observer('visualisationSettings.{layoutAlgorithm,theme}', 'graph', function() { this.debug('graph', this.get('visualisationSettings.theme'), this.get('visualisationSettings.layoutAlgorithm'), this.get('graph')); - // do not use this.set('rendering') since it would trigger rendering updates within didInsertElement - this._rendering = cytoscape({ - container: this.element, - - boxSelectionEnabled: false, - autounselectify: true, + // if(!this._rendering) { // FIXME: does not handle layout/theme changes + // do not use this.set('rendering') since it would trigger rendering updates within didInsertElement + const elements = _.cloneDeep(this.get('graph')); + const stylesheet = cytoscapeStyle(this.get('visualisationSettings.themeStyle')); + if(!this._rendering || !this.get('layoutSupportsReuse')) { + if(this._rendering) { + // this._rendering.destroy(); // remove previous listeners TODO: requires https://github.com/cytoscape/cytoscape.js-cola/issues/15 + } + this._rendering = cytoscape({ + container: this.element, - style: cytoscapeStyle(this.get('visualisationSettings.themeStyle')), + boxSelectionEnabled: false, + autounselectify: true, - elements: _.cloneDeep(this.get('graph')), + style: stylesheet, - layout: { - name: this.get('visualisationSettings.layoutAlgorithm'), - randomize: false, // kose-bilkent will randomize node positions - // maxSimulationTime: 1000, - // padding: 6, - // ungrabifyWhileSimulating: true, - // infinite: false + elements: elements, - // webcola options - refresh: 5, // fast animation - avoidOverlap: true, - edgeLength: 250, // should be at least two times the diagonal of a block, blocks are 100x60, therefore around 2*116 - unconstrIter: 100, // unconstrained initial layout iterations - userConstIter: 0, // initial layout iterations with user-specified constraints - we don't have any user constraints - allConstIter: 10 // initial layout iterations with all constraints including non-overlap - } - }); + layout: this.get('layoutOptions') + }); + const isCola = this.get('layoutOptions.name') === 'cola'; - this._rendering.on('click', (event) => { - const target = event.cyTarget; - const data = target && target.data && target.data(); - - this.set('_clickedElement', target); - if(data && data.id) { - this.debug('clicked on element in graph', data, event); - const action = this.get('select'); - if(action) { - action(data); - } else { - this.debug('select action not set, ignoring click'); + if(isCola) { + this._rendering.on('layoutready', () => { + this.debug('layoutready, using fit'); + this._rendering.fit(); + }); } + + this._rendering.on('click', (event) => { + const target = event.cyTarget; + const data = target && target.data && target.data(); + + this.set('_clickedElement', target); + if(data && data.id) { + this.debug('clicked on element in graph', data, event); + const action = this.get('select'); + if(action) { + action(data); + } else { + this.debug('select action not set, ignoring click'); + } + } else { + this.debug('clicked on non-selectable entity', event); + } + }); } else { - this.debug('clicked on non-selectable entity', event); + const cy = this._rendering; + cy.batch(() => { + cy.style(stylesheet); + this.applyDiff(cy, elements); + }); + if(this._layout) { + this._layout.stop(); // may still be running, e.g. cola uses infinite mode + } + const layout = cy.makeLayout(this.get('layoutOptions')); + this._layout = layout; + layout.run(); } - }); - // just for development purposes - TODO: remove window.cy = cytoscape; + window.layout = this._layout; window.cytoscape = this._rendering; })), + + applyDiff(cytoscapeInstance, newElements) { + cytoscapeInstance.batch(() => { + const renderedElementsById = {}; // stores cytoscape node/edge instances by id + const updatedElementsById = {}; // stores all elements that will have to be updated/added + + newElements.forEach((element) => { + const id = element.data.id; + updatedElementsById[id] = element; + }); + + const renderedElements = newElements + .map((element) => { + const id = element.data.id; + const existingElement = cytoscapeInstance.getElementById(id); + return existingElement; + }) + .filter((renderedElement) => !!renderedElement); // null if not in cytoscape + + renderedElements.forEach((renderedElement) => renderedElementsById[renderedElement.id()] = renderedElement); + + // existing update existing nodes/edges + renderedElements + .map((renderedElement) => { + const id = renderedElement.id(); + const updatedElement = renderedElementsById[id]; + renderedElement.data(updatedElement.data); + }); + + + // remove unnecessary. Do this before adding new ones, otherwise they would new elements will be directly removed + cytoscapeInstance.elements() + .filterFn((el) => !renderedElementsById[el.id()]) // elements that are no longer in source data + .forEach((el) => el.remove()); + + // add new + newElements + .filter((element) => !renderedElementsById[element.data.id]) // should not have rendered counterpart + .forEach((element) => { + element.position = { // new nodes require an initial position + x: Math.random()* 10, + y: Math.random()* 10 + }; + cytoscapeInstance.add(element); + }); + }); + }, + resize() { if(this._rendering) { this._rendering.resize(); diff --git a/app/services/architecture-graphing-service.js b/app/services/architecture-graphing-service.js index 3d0ae8bfeff77163fc1b5e528189450c982ba18b..f8a022eeeeb0708711da6f6b59742b065de4e39e 100644 --- a/app/services/architecture-graphing-service.js +++ b/app/services/architecture-graphing-service.js @@ -20,15 +20,12 @@ export default Ember.Service.extend({ // services not used in current view const {services, /* serviceInstances,*/ communications} = prepared; - - var network = { - nodes: [], - edges: [] - }; + const elements = []; services.forEach(data => { data.label = data.name; - network.nodes.push({ + elements.push({ + group: 'nodes', data: data }); }); @@ -55,12 +52,13 @@ export default Ember.Service.extend({ //TODO Normalize - network.edges.push({ + elements.push({ + group: 'edges', data, classes: data.status || '' }); }); - return network; + return elements; } }); diff --git a/app/services/deployment-graphing-service.js b/app/services/deployment-graphing-service.js index 60335c37367b6631ba254d44e57f201615fa9395..ec649c442942891dca1a036916fff971bda3e6c7 100644 --- a/app/services/deployment-graphing-service.js +++ b/app/services/deployment-graphing-service.js @@ -22,16 +22,13 @@ export default Ember.Service.extend({ // services not used in current view const {serviceInstances, communicationInstances, nodeGroups, nodes} = prepared; - - var network = { - nodes: [], - edges: [] - }; + const elements = []; nodeGroups.forEach(data => { data.label = data.name; - network.nodes.push({ + elements.push({ + group: 'nodes', data: data }); }); @@ -40,7 +37,8 @@ export default Ember.Service.extend({ data.label = data.name; data.parent = data.nodeGroupId; - network.nodes.push({ + elements.push({ + group: 'nodes', data: data, classes: data.status || '' }); @@ -50,8 +48,10 @@ export default Ember.Service.extend({ data.label = data.name; data.parent = data.nodeId; - network.nodes.push({ + elements.push({ + group: 'nodes', data: data, + parent: data.parent, classes: data.status || '' }); }); @@ -62,12 +62,13 @@ export default Ember.Service.extend({ data.technology = this.get('store').peekRecord('communication', data.communicationId).get('technology'); data.label = data.technology; - network.edges.push({ + elements.push({ + group: 'edges', data, classes: data.status || '' }); }); - return network; + return elements; } }); diff --git a/package.json b/package.json index bf6298b30166e59b6b990e8f94d3de23f1ccc09e..307e195cea5e33b4e6f43c6605a0c9130e1b0e20 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ }, "dependencies": { "cytoscape": "^2.7.3", - "cytoscape-cola": "git+https://github.com/EyMaddis/cytoscape.js-cola.git#5c0957e60726def6d7e849956939079c5a491ba2", + "cytoscape-cola": "https://github.com/cytoscape/cytoscape.js-cola.git#8436961cd4ac1899a2a429d6c99dc2a140adcf0e", "cytoscape-cose-bilkent": "^1.3.6", "ember-cli": "^2.7.0", "yuidocjs": "^0.10.1"