Skip layout when only edges changed

This commit is contained in:
David Kaltschmidt
2015-10-28 14:05:19 +00:00
parent 7306e8fb5f
commit 47ba0ff2a4
3 changed files with 126 additions and 33 deletions

View File

@@ -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);
});
});

View File

@@ -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,

View File

@@ -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;
}