mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 01:31:17 +00:00
Simplified layout steps for singles and same-ranks
This commit is contained in:
@@ -35,19 +35,34 @@ describe('NodesLayout', () => {
|
||||
'n2-n4': {id: 'n2-n4', source: 'n2', target: 'n4'}
|
||||
})
|
||||
},
|
||||
addNode15: {
|
||||
rank4: {
|
||||
nodes: fromJS({
|
||||
n1: {id: 'n1'},
|
||||
n2: {id: 'n2'},
|
||||
n3: {id: 'n3'},
|
||||
n4: {id: 'n4'},
|
||||
n5: {id: 'n5'}
|
||||
n1: {id: 'n1', rank: 'A'},
|
||||
n2: {id: 'n2', rank: 'A'},
|
||||
n3: {id: 'n3', rank: 'B'},
|
||||
n4: {id: 'n4', rank: 'B'}
|
||||
}),
|
||||
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'}
|
||||
})
|
||||
},
|
||||
rank6: {
|
||||
nodes: fromJS({
|
||||
n1: {id: 'n1', rank: 'A'},
|
||||
n2: {id: 'n2', rank: 'A'},
|
||||
n3: {id: 'n3', rank: 'B'},
|
||||
n4: {id: 'n4', rank: 'B'},
|
||||
n5: {id: 'n5', rank: 'A'},
|
||||
n6: {id: 'n6', rank: 'B'},
|
||||
}),
|
||||
edges: fromJS({
|
||||
'n1-n3': {id: 'n1-n3', source: 'n1', target: 'n3'},
|
||||
'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'},
|
||||
'n1-n5': {id: 'n1-n5', source: 'n1', target: 'n5'},
|
||||
'n2-n4': {id: 'n2-n4', source: 'n2', target: 'n4'}
|
||||
'n2-n4': {id: 'n2-n4', source: 'n2', target: 'n4'},
|
||||
'n2-n6': {id: 'n2-n6', source: 'n2', target: 'n6'},
|
||||
})
|
||||
},
|
||||
removeEdge24: {
|
||||
@@ -149,6 +164,54 @@ describe('NodesLayout', () => {
|
||||
expect(hasUnseen).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shifts layouts to center', () => {
|
||||
let xMin;
|
||||
let xMax;
|
||||
let yMin;
|
||||
let yMax;
|
||||
let xCenter;
|
||||
let yCenter;
|
||||
|
||||
// make sure initial layout is centered
|
||||
const original = NodesLayout.doLayout(
|
||||
nodeSets.initial4.nodes,
|
||||
nodeSets.initial4.edges
|
||||
);
|
||||
xMin = original.nodes.minBy(n => n.get('x'));
|
||||
xMax = original.nodes.maxBy(n => n.get('x'));
|
||||
yMin = original.nodes.minBy(n => n.get('y'));
|
||||
yMax = original.nodes.maxBy(n => n.get('y'));
|
||||
xCenter = (xMin.get('x') + xMax.get('x')) / 2;
|
||||
yCenter = (yMin.get('y') + yMax.get('y')) / 2;
|
||||
expect(xCenter).toEqual(NodesLayout.DEFAULT_WIDTH / 2);
|
||||
expect(yCenter).toEqual(NodesLayout.DEFAULT_HEIGHT / 2);
|
||||
|
||||
// make sure re-running is idempotent
|
||||
const rerun = NodesLayout.shiftLayoutToCenter(original);
|
||||
xMin = rerun.nodes.minBy(n => n.get('x'));
|
||||
xMax = rerun.nodes.maxBy(n => n.get('x'));
|
||||
yMin = rerun.nodes.minBy(n => n.get('y'));
|
||||
yMax = rerun.nodes.maxBy(n => n.get('y'));
|
||||
xCenter = (xMin.get('x') + xMax.get('x')) / 2;
|
||||
yCenter = (yMin.get('y') + yMax.get('y')) / 2;
|
||||
expect(xCenter).toEqual(NodesLayout.DEFAULT_WIDTH / 2);
|
||||
expect(yCenter).toEqual(NodesLayout.DEFAULT_HEIGHT / 2);
|
||||
|
||||
// shift after window was resized
|
||||
const shifted = NodesLayout.shiftLayoutToCenter(original, {
|
||||
width: 128,
|
||||
height: 256
|
||||
});
|
||||
xMin = shifted.nodes.minBy(n => n.get('x'));
|
||||
xMax = shifted.nodes.maxBy(n => n.get('x'));
|
||||
yMin = shifted.nodes.minBy(n => n.get('y'));
|
||||
yMax = shifted.nodes.maxBy(n => n.get('y'));
|
||||
xCenter = (xMin.get('x') + xMax.get('x')) / 2;
|
||||
yCenter = (yMin.get('y') + yMax.get('y')) / 2;
|
||||
expect(xCenter).toEqual(128 / 2);
|
||||
expect(yCenter).toEqual(256 / 2);
|
||||
});
|
||||
|
||||
it('lays out initial nodeset in a rectangle', () => {
|
||||
const result = NodesLayout.doLayout(
|
||||
nodeSets.initial4.nodes,
|
||||
@@ -351,28 +414,37 @@ describe('NodesLayout', () => {
|
||||
expect(nodes.n1.x).toBeLessThan(nodes.n5.x);
|
||||
expect(nodes.n1.x).toBeLessThan(nodes.n6.x);
|
||||
});
|
||||
//
|
||||
// it('adds a new node to existing layout in a line', () => {
|
||||
// let result = NodesLayout.doLayout(
|
||||
// nodeSets.initial4.nodes,
|
||||
// nodeSets.initial4.edges);
|
||||
//
|
||||
// nodes = result.nodes.toJS();
|
||||
//
|
||||
// coords = getNodeCoordinates(result.nodes);
|
||||
// options.cachedLayout = result;
|
||||
// options.nodeCache = options.nodeCache.merge(result.nodes);
|
||||
// options.edgeCache = options.edgeCache.merge(result.edge);
|
||||
//
|
||||
// result = NodesLayout.doLayout(
|
||||
// nodeSets.addNode15.nodes,
|
||||
// nodeSets.addNode15.edges,
|
||||
// options
|
||||
// );
|
||||
//
|
||||
// nodes = result.nodes.toJS();
|
||||
//
|
||||
// expect(nodes.n1.x).toBeGreaterThan(nodes.n5.x);
|
||||
// expect(nodes.n1.y).toEqual(nodes.n5.y);
|
||||
// });
|
||||
|
||||
it('adds a new node to existing layout in a line', () => {
|
||||
let result = NodesLayout.doLayout(
|
||||
nodeSets.rank4.nodes,
|
||||
nodeSets.rank4.edges,
|
||||
{ noCache: true }
|
||||
);
|
||||
|
||||
nodes = result.nodes.toJS();
|
||||
|
||||
coords = getNodeCoordinates(result.nodes);
|
||||
options.cachedLayout = result;
|
||||
options.nodeCache = options.nodeCache.merge(result.nodes);
|
||||
options.edgeCache = options.edgeCache.merge(result.edge);
|
||||
|
||||
expect(NodesLayout.hasNewNodesOfExistingRank(
|
||||
nodeSets.rank6.nodes,
|
||||
nodeSets.rank6.edges,
|
||||
result.nodes)).toBeTruthy();
|
||||
|
||||
result = NodesLayout.doLayout(
|
||||
nodeSets.rank6.nodes,
|
||||
nodeSets.rank6.edges,
|
||||
options
|
||||
);
|
||||
|
||||
nodes = result.nodes.toJS();
|
||||
|
||||
expect(nodes.n5.x).toBeGreaterThan(nodes.n1.x);
|
||||
expect(nodes.n5.y).toEqual(nodes.n1.y);
|
||||
expect(nodes.n6.x).toBeGreaterThan(nodes.n3.x);
|
||||
expect(nodes.n6.y).toEqual(nodes.n3.y);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,9 @@ import { buildTopologyCacheId, updateNodeDegrees } from '../utils/topology-utils
|
||||
const log = debug('scope:nodes-layout');
|
||||
|
||||
const topologyCaches = {};
|
||||
const DEFAULT_WIDTH = 800;
|
||||
const DEFAULT_MARGINS = {top: 0, left: 0};
|
||||
export const DEFAULT_WIDTH = 800;
|
||||
export const DEFAULT_HEIGHT = DEFAULT_WIDTH / 2;
|
||||
export const DEFAULT_MARGINS = {top: 0, left: 0};
|
||||
const DEFAULT_SCALE = val => val * 2;
|
||||
const NODE_SIZE_FACTOR = 1;
|
||||
const NODE_SEPARATION_FACTOR = 2.0;
|
||||
@@ -127,11 +128,63 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
|
||||
};
|
||||
}
|
||||
|
||||
export function doLayoutNewNodesOfExistingRank(layout, immNodes, immEdges, opts) {
|
||||
console.log(opts);
|
||||
/**
|
||||
* Adds `points` array to edge based on location of source and target
|
||||
* @param {Map} edge new edge
|
||||
* @param {Map} nodeCache all nodes
|
||||
* @returns {Map} modified edge
|
||||
*/
|
||||
function setSimpleEdgePoints(edge, nodeCache) {
|
||||
const source = nodeCache.get(edge.get('source'));
|
||||
const target = nodeCache.get(edge.get('target'));
|
||||
return edge.set('points', fromJS([
|
||||
{x: source.get('x'), y: source.get('y')},
|
||||
{x: target.get('x'), y: target.get('y')}
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout nodes that have rank that already exists.
|
||||
* Relies on only nodes being added that have a connection to an existing node
|
||||
* while having a rank of an existing node. They will be laid out in the same
|
||||
* line as the latter, with a direct connection between the existing and the new node.
|
||||
* @param {object} layout Layout with nodes and edges
|
||||
* @param {Map} nodeCache previous nodes
|
||||
* @param {object} opts Options
|
||||
* @return {object} new layout object
|
||||
*/
|
||||
export function doLayoutNewNodesOfExistingRank(layout, nodeCache, opts) {
|
||||
const result = Object.assign({}, layout);
|
||||
const options = opts || {};
|
||||
const scale = options.scale || DEFAULT_SCALE;
|
||||
const nodesep = scale(NODE_SEPARATION_FACTOR);
|
||||
const nodeWidth = scale(NODE_SIZE_FACTOR);
|
||||
|
||||
// determine new nodes
|
||||
// layout new nodes
|
||||
// return layout
|
||||
const oldNodes = ImmSet.fromKeys(nodeCache);
|
||||
const newNodes = ImmSet.fromKeys(layout.nodes.filter(n => n.get('degree') > 0))
|
||||
.subtract(oldNodes);
|
||||
result.nodes = layout.nodes.map(n => {
|
||||
if (newNodes.contains(n.get('id'))) {
|
||||
const nodesSameRank = nodeCache.filter(nn => nn.get('rank') === n.get('rank'));
|
||||
if (nodesSameRank.size > 0) {
|
||||
const y = nodesSameRank.first().get('y');
|
||||
const x = nodesSameRank.maxBy(nn => nn.get('x')).get('x') + nodesep + nodeWidth;
|
||||
return n.merge({ x, y });
|
||||
}
|
||||
return n;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
|
||||
result.edges = layout.edges.map(edge => {
|
||||
if (!edge.has('points')) {
|
||||
return setSimpleEdgePoints(edge, layout.nodes);
|
||||
}
|
||||
return edge;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,53 +276,44 @@ function layoutSingleNodes(layout, opts) {
|
||||
* @param {Object} opts Options with width and margins
|
||||
* @return {Object} modified layout
|
||||
*/
|
||||
function shiftLayoutToCenter(layout, opts) {
|
||||
export function shiftLayoutToCenter(layout, opts) {
|
||||
const result = Object.assign({}, layout);
|
||||
const options = opts || {};
|
||||
const margins = options.margins || DEFAULT_MARGINS;
|
||||
const width = options.width || DEFAULT_WIDTH;
|
||||
const height = options.height || width / 2;
|
||||
const height = options.height || DEFAULT_HEIGHT;
|
||||
|
||||
let offsetX = 0 + margins.left;
|
||||
let offsetY = 0 + margins.top;
|
||||
|
||||
if (layout.width < width) {
|
||||
offsetX = (width - layout.width) / 2 + margins.left;
|
||||
const xMin = layout.nodes.minBy(n => n.get('x'));
|
||||
const xMax = layout.nodes.maxBy(n => n.get('x'));
|
||||
offsetX = (width - (xMin.get('x') + xMax.get('x'))) / 2 + margins.left;
|
||||
}
|
||||
if (layout.height < height) {
|
||||
offsetY = (height - layout.height) / 2 + margins.top;
|
||||
const yMin = layout.nodes.minBy(n => n.get('y'));
|
||||
const yMax = layout.nodes.maxBy(n => n.get('y'));
|
||||
offsetY = (height - (yMin.get('y') + yMax.get('y'))) / 2 + margins.top;
|
||||
}
|
||||
|
||||
result.nodes = layout.nodes.map(node => node.merge({
|
||||
x: node.get('x') + offsetX,
|
||||
y: node.get('y') + offsetY
|
||||
}));
|
||||
if (offsetX || offsetY) {
|
||||
result.nodes = layout.nodes.map(node => node.merge({
|
||||
x: node.get('x') + offsetX,
|
||||
y: node.get('y') + offsetY
|
||||
}));
|
||||
|
||||
result.edges = layout.edges.map(edge => edge.update('points',
|
||||
points => points.map(point => point.merge({
|
||||
x: point.get('x') + offsetX,
|
||||
y: point.get('y') + offsetY
|
||||
}))
|
||||
));
|
||||
result.edges = layout.edges.map(edge => edge.update('points',
|
||||
points => points.map(point => point.merge({
|
||||
x: point.get('x') + offsetX,
|
||||
y: point.get('y') + offsetY
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `points` array to edge based on location of source and target
|
||||
* @param {Map} edge new edge
|
||||
* @param {Map} nodeCache all nodes
|
||||
* @returns {Map} modified edge
|
||||
*/
|
||||
function setSimpleEdgePoints(edge, nodeCache) {
|
||||
const source = nodeCache.get(edge.get('source'));
|
||||
const target = nodeCache.get(edge.get('target'));
|
||||
return edge.set('points', fromJS([
|
||||
{x: source.get('x'), y: source.get('y')},
|
||||
{x: target.get('x'), y: target.get('y')}
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if nodes were added between node sets
|
||||
* @param {Map} nodes new Map of nodes
|
||||
@@ -303,11 +347,25 @@ function hasNewSingleNode(nodes, cache) {
|
||||
* Determine if all new nodes are of existing ranks
|
||||
* Requires cached nodes (implies a previous layout run).
|
||||
* @param {Map} nodes new Map of nodes
|
||||
* @param {Map} edges new Map of edges
|
||||
* @param {Map} cache old Map of nodes
|
||||
* @return {Boolean} True if all new nodes have a rank that already exists
|
||||
*/
|
||||
function hasNewNodesOfExistingRank(nodes, cache) {
|
||||
return false && nodes && cache;
|
||||
export function hasNewNodesOfExistingRank(nodes, edges, cache) {
|
||||
const oldNodes = ImmSet.fromKeys(cache);
|
||||
const newNodes = ImmSet.fromKeys(nodes).subtract(oldNodes);
|
||||
|
||||
// if new there are edges that connect 2 new nodes, need a full layout
|
||||
const bothNodesNew = edges.find(edge => newNodes.contains(edge.get('source'))
|
||||
&& newNodes.contains(edge.get('target')));
|
||||
if (bothNodesNew) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldRanks = cache.filter(n => n.get('rank')).map(n => n.get('rank')).toSet();
|
||||
const hasNewNodesOfExistingRankOrSingle = newNodes.every(key => nodes.getIn([key, 'degree']) === 0
|
||||
|| oldRanks.contains(nodes.getIn([key, 'rank'])));
|
||||
return oldNodes.size > 0 && hasNewNodesOfExistingRankOrSingle;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,8 +415,10 @@ function copyLayoutProperties(layout, nodeCache, edgeCache) {
|
||||
if (edgeCache.has(edge.get('id'))
|
||||
&& hasSameEndpoints(edgeCache.get(edge.get('id')), result.nodes)) {
|
||||
return edge.merge(edgeCache.get(edge.get('id')));
|
||||
} else if (nodeCache.get(edge.get('source')) && nodeCache.get(edge.get('target'))) {
|
||||
return setSimpleEdgePoints(edge, nodeCache);
|
||||
}
|
||||
return setSimpleEdgePoints(edge, nodeCache);
|
||||
return edge;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -406,13 +466,13 @@ export function doLayout(immNodes, immEdges, opts) {
|
||||
log('skip layout, only 0-degree node(s) added');
|
||||
layout = cloneLayout(cachedLayout, nodesWithDegrees, immEdges);
|
||||
layout = copyLayoutProperties(layout, nodeCache, edgeCache);
|
||||
} else if (useCache && hasNewNodesOfExistingRank(nodesWithDegrees, nodeCache)) {
|
||||
} else if (useCache && hasNewNodesOfExistingRank(nodesWithDegrees, immEdges, nodeCache)) {
|
||||
// special case: few new nodes were added, no need for layout run,
|
||||
// they will inserted according to ranks
|
||||
log('skip layout, used rank-based insertion');
|
||||
layout = cloneLayout(cachedLayout, nodesWithDegrees, immEdges);
|
||||
layout = copyLayoutProperties(layout, nodeCache, edgeCache);
|
||||
layout = doLayoutNewNodesOfExistingRank(layout, nodesWithDegrees, immEdges);
|
||||
layout = doLayoutNewNodesOfExistingRank(layout, nodeCache, opts);
|
||||
} else {
|
||||
const graph = cache.graph;
|
||||
layout = runLayoutEngine(graph, nodesWithDegrees, immEdges, opts);
|
||||
@@ -420,6 +480,7 @@ export function doLayout(immNodes, immEdges, opts) {
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
||||
layout = layoutSingleNodes(layout, opts);
|
||||
layout = shiftLayoutToCenter(layout, opts);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user