diff --git a/client/app/scripts/charts/__tests__/node-layout-test.js b/client/app/scripts/charts/__tests__/node-layout-test.js index 04a308bb3..f76f7ce00 100644 --- a/client/app/scripts/charts/__tests__/node-layout-test.js +++ b/client/app/scripts/charts/__tests__/node-layout-test.js @@ -1,7 +1,7 @@ jest.dontMock('../nodes-layout'); jest.dontMock('../../constants/naming'); // edge naming: 'source-target' -import { fromJS } from 'immutable'; +import { fromJS, is } from 'immutable'; describe('NodesLayout', () => { const NodesLayout = require('../nodes-layout'); @@ -21,36 +21,36 @@ describe('NodesLayout', () => { const nodeSets = { initial4: { - nodes: { + nodes: fromJS({ n1: {id: 'n1'}, n2: {id: 'n2'}, n3: {id: 'n3'}, n4: {id: 'n4'} - }, - edges: { + }), + edges: fromJS({ 'n1-n3': {id: 'n1-n3', source: 'n1', target: 'n3'}, 'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'}, 'n2-n4': {id: 'n2-n4', source: 'n2', target: 'n4'} - } + }) }, removeEdge24: { - nodes: { + nodes: fromJS({ n1: {id: 'n1'}, n2: {id: 'n2'}, n3: {id: 'n3'}, n4: {id: 'n4'} - }, - edges: { + }), + edges: fromJS({ 'n1-n3': {id: 'n1-n3', source: 'n1', target: 'n3'}, 'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'} - } + }) } }; it('lays out initial nodeset in a rectangle', () => { const result = NodesLayout.doLayout( - fromJS(nodeSets.initial4.nodes), - fromJS(nodeSets.initial4.edges)); + nodeSets.initial4.nodes, + nodeSets.initial4.edges); // console.log('initial', result.get('nodes')); nodes = result.nodes.toJS(); @@ -62,23 +62,66 @@ describe('NodesLayout', () => { expect(nodes.n3.y).toEqual(nodes.n4.y); }); - // it('keeps nodes in rectangle after removing one edge', () => { - // history = [{ - // nodes: nodeSets.initial4.nodes, - // edges: nodeSets.initial4.edges - // }]; - // nodes = nodeSets.removeEdge24.nodes; - // edges = nodeSets.removeEdge24.edges; - // NodesLayout.doLayout(nodes, edges, {history}); - // console.log('remove 1 edge', nodes); - // - // expect(nodes.n1.x).toBeLessThan(nodes.n2.x); - // expect(nodes.n1.y).toEqual(nodes.n2.y); - // expect(nodes.n1.x).toEqual(nodes.n3.x); - // expect(nodes.n1.y).toBeLessThan(nodes.n3.y); - // expect(nodes.n3.x).toBeLessThan(nodes.n4.x); - // expect(nodes.n3.y).toEqual(nodes.n4.y); - // - // }); + it('keeps nodes in rectangle after removing one edge', () => { + let result = NodesLayout.doLayout( + nodeSets.initial4.nodes, + nodeSets.initial4.edges); + history = [{ + nodes: result.nodes, + edges: result.edges + }]; + result = NodesLayout.doLayout( + nodeSets.removeEdge24.nodes, + nodeSets.removeEdge24.edges, + {history} + ); + nodes = result.nodes.toJS(); + // console.log('remove 1 edge', nodes, result); + + expect(nodes.n1.x).toBeLessThan(nodes.n2.x); + expect(nodes.n1.y).toEqual(nodes.n2.y); + expect(nodes.n1.x).toEqual(nodes.n3.x); + expect(nodes.n1.y).toBeLessThan(nodes.n3.y); + expect(nodes.n3.x).toBeLessThan(nodes.n4.x); + expect(nodes.n3.y).toEqual(nodes.n4.y); + }); + + it('keeps nodes in rectangle after removed edge reappears', () => { + let result = NodesLayout.doLayout( + nodeSets.initial4.nodes, + nodeSets.initial4.edges); + + history = [{ + nodes: result.nodes, + edges: result.edges + }]; + result = NodesLayout.doLayout( + nodeSets.removeEdge24.nodes, + nodeSets.removeEdge24.edges, + {history} + ); + + history = [{ + nodes: result.nodes, + edges: result.edges + }]; + result = NodesLayout.doLayout( + nodeSets.initial4.nodes, + nodeSets.initial4.edges, + {history} + ); + + nodes = result.nodes.toJS(); + // console.log('re-add 1 edge', nodes, result); + + expect(nodes.n1.x).toBeLessThan(nodes.n2.x); + expect(nodes.n1.y).toEqual(nodes.n2.y); + expect(nodes.n1.x).toEqual(nodes.n3.x); + expect(nodes.n1.y).toBeLessThan(nodes.n3.y); + expect(nodes.n3.x).toBeLessThan(nodes.n4.x); + expect(nodes.n3.y).toEqual(nodes.n4.y); + }); + + }); diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index a0c98336f..7bc093227 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -31,6 +31,7 @@ const NodesChart = React.createClass({ return { nodes: makeMap(), edges: makeMap(), + history: [], nodeScale: d3.scale.linear(), shiftTranslate: [0, 0], panTranslate: [0, 0], @@ -453,7 +454,8 @@ const NodesChart = React.createClass({ height: props.height, scale: nodeScale, margins: MARGINS, - topologyId: this.props.topologyId + topologyId: this.props.topologyId, + history: state.history }; const timedLayouter = timely(NodesLayout.doLayout); @@ -492,6 +494,7 @@ const NodesChart = React.createClass({ } return { + history: [graph], nodes: stateNodes, edges: stateEdges, nodeScale: nodeScale, diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index 91ee6061c..686973b4d 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -1,5 +1,6 @@ const dagre = require('dagre'); const debug = require('debug')('scope:nodes-layout'); +const ImmSet = require('immutable').Set; const Naming = require('../constants/naming'); const MAX_NODES = 100; @@ -118,6 +119,38 @@ function runLayoutEngine(imNodes, imEdges, opts) { return layout; } +function doLayoutEdges(nodes, edges, previousLayout) { + const previousEdges = previousLayout.edges; + + // remove old edges + let layoutEdges = previousEdges.filter(edge => { + return edges.has(edge.get('id')); + }); + + // add new edges with points from source and target + let source; + let target; + let layoutEdge; + edges.forEach(edge => { + if (!layoutEdges.has(edge.get('id'))) { + source = nodes.get(edge.get('source')); + target = nodes.get(edge.get('target')); + layoutEdge = edge.set('points', [ + {x: source.get('x'), y: source.get('y')}, + {x: target.get('x'), y: target.get('y')} + ]); + layoutEdges = layoutEdges.set(layoutEdge.get('id'), layoutEdge); + } + }); + + previousLayout.edges = layoutEdges; + return previousLayout; +} + +function hasSameNodes(nodes, prevNodes) { + return ImmSet.fromKeys(nodes).equals(ImmSet.fromKeys(prevNodes)); +} + /** * Layout of nodes and edges * @param {Map} nodes All nodes @@ -126,8 +159,22 @@ function runLayoutEngine(imNodes, imEdges, opts) { * @return {object} graph object with nodes, edges, dimensions */ export function doLayout(nodes, edges, opts) { - // const options = opts || {}; - // const history = options.history || []; + const options = opts || {}; + const history = options.history || []; + const previous = history.pop(); + let layout; - return runLayoutEngine(nodes, edges, opts); + if (previous) { + // add/remove edges if nodes are the same + if (hasSameNodes(previous.nodes, nodes)) { + debug('skip layout, only edges changed', edges.size, previous.edges.size); + layout = doLayoutEdges(nodes, edges, previous); + } + } + + if (layout === undefined) { + layout = runLayoutEngine(nodes, edges, opts); + } + + return layout; }