From ee61ff7143623665d7454cc67f1e1dbbd8a287c5 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 27 Sep 2016 09:24:38 -0700 Subject: [PATCH 1/9] Test and debugbar tools to simulate single nodes appearing --- .../charts/__tests__/node-layout-test.js | 89 +++++++++++++++++++ client/app/scripts/charts/nodes-layout.js | 18 +++- .../app/scripts/components/debug-toolbar.js | 72 ++++++++++++++- 3 files changed, 175 insertions(+), 4 deletions(-) diff --git a/client/app/scripts/charts/__tests__/node-layout-test.js b/client/app/scripts/charts/__tests__/node-layout-test.js index 7baa0d33e8..8f8c7b969b 100644 --- a/client/app/scripts/charts/__tests__/node-layout-test.js +++ b/client/app/scripts/charts/__tests__/node-layout-test.js @@ -35,6 +35,21 @@ describe('NodesLayout', () => { 'n2-n4': {id: 'n2-n4', source: 'n2', target: 'n4'} }) }, + addNode15: { + nodes: fromJS({ + n1: {id: 'n1'}, + n2: {id: 'n2'}, + n3: {id: 'n3'}, + n4: {id: 'n4'}, + n5: {id: 'n5'} + }), + 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'} + }) + }, removeEdge24: { nodes: fromJS({ n1: {id: 'n1'}, @@ -86,6 +101,19 @@ describe('NodesLayout', () => { edges: fromJS({ 'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'} }) + }, + singlePortrait6: { + nodes: fromJS({ + n1: {id: 'n1'}, + n2: {id: 'n2'}, + n3: {id: 'n3'}, + n4: {id: 'n4'}, + n5: {id: 'n5'}, + n6: {id: 'n6'} + }), + edges: fromJS({ + 'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'} + }) } }; @@ -282,4 +310,65 @@ describe('NodesLayout', () => { expect(nodes.n1.x).toBeLessThan(nodes.n5.x); expect(nodes.n2.x).toEqual(nodes.n5.x); }); + + it('renders an additional single node in single nodes group', () => { + let result = NodesLayout.doLayout( + nodeSets.singlePortrait.nodes, + nodeSets.singlePortrait.edges); + + nodes = result.nodes.toJS(); + + // first square row on same level as top-most other node + expect(nodes.n1.y).toEqual(nodes.n2.y); + expect(nodes.n1.y).toEqual(nodes.n3.y); + expect(nodes.n4.y).toEqual(nodes.n5.y); + + // all singles right to other nodes + expect(nodes.n1.x).toEqual(nodes.n4.x); + expect(nodes.n1.x).toBeLessThan(nodes.n2.x); + expect(nodes.n1.x).toBeLessThan(nodes.n3.x); + expect(nodes.n1.x).toBeLessThan(nodes.n5.x); + expect(nodes.n2.x).toEqual(nodes.n5.x); + + options.cachedLayout = result; + options.nodeCache = options.nodeCache.merge(result.nodes); + options.edgeCache = options.edgeCache.merge(result.edge); + + result = NodesLayout.doLayout( + nodeSets.singlePortrait6.nodes, + nodeSets.singlePortrait6.edges, + options + ); + + nodes = result.nodes.toJS(); + + expect(nodes.n1.x).toBeLessThan(nodes.n2.x); + expect(nodes.n1.x).toBeLessThan(nodes.n3.x); + 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); + }); }); diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index 116e35f4dd..e41b5a9b8e 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -274,6 +274,13 @@ export function hasUnseenNodes(nodes, cache) { return hasUnseen; } +function hasNewSingleNode(nodes, cache) { + return (ImmSet + .fromKeys(nodes) + .subtract(ImmSet.fromKeys(cache)) + .every(key => nodes.getIn([key, 'degree']) === 0)); +} + /** * Determine if edge has same endpoints in new nodes as well as in the nodeCache * @param {Map} edge Edge with source and target @@ -364,9 +371,14 @@ export function doLayout(immNodes, immEdges, opts) { } else { const graph = cache.graph; const nodesWithDegrees = updateNodeDegrees(immNodes, immEdges); - layout = runLayoutEngine(graph, nodesWithDegrees, immEdges, opts); - if (!layout) { - return layout; + if (hasNewSingleNode(nodesWithDegrees, nodeCache)) { + layout = cloneLayout(cachedLayout, immNodes, immEdges); + layout = copyLayoutProperties(layout, nodeCache, edgeCache); + } else { + layout = runLayoutEngine(graph, nodesWithDegrees, immEdges, opts); + if (!layout) { + return layout; + } } layout = layoutSingleNodes(layout, opts); layout = shiftLayoutToCenter(layout, opts); diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 1888d9fda0..34e771cca6 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -4,7 +4,7 @@ import d3 from 'd3'; import _ from 'lodash'; import Perf from 'react-addons-perf'; import { connect } from 'react-redux'; -import { fromJS } from 'immutable'; +import { fromJS, Set as makeSet } from 'immutable'; import debug from 'debug'; const log = debug('scope:debug-panel'); @@ -160,6 +160,10 @@ class DebugToolbar extends React.Component { this.onChange = this.onChange.bind(this); this.toggleColors = this.toggleColors.bind(this); this.addNodes = this.addNodes.bind(this); + this.intermittendTimer = null; + this.intermittendNodes = makeSet(); + this.shortLivedTimer = null; + this.shortLivedNodes = makeSet(); this.state = { nodesToAdd: 30, showColors: false @@ -197,6 +201,66 @@ class DebugToolbar extends React.Component { })); } + setIntermittend() { + // simulate epheremal nodes + if (this.intermittendTimer) { + clearInterval(this.intermittendTimer); + this.intermittendTimer = null; + } else { + this.intermittendTimer = setInterval(() => { + // add new node + this.addNodes(1); + + // remove random node + const ns = this.props.nodes; + const nodeNames = ns.keySeq().toJS(); + const randomNode = _.sample(nodeNames); + this.asyncDispatch(receiveNodesDelta({ + remove: [randomNode] + })); + }, 1000); + } + } + + setShortLived() { + // simulate nodes with same ID popping in and out + if (this.shortLivedTimer) { + clearInterval(this.shortLivedTimer); + this.shortLivedTimer = null; + } else { + this.shortLivedTimer = setInterval(() => { + // filter random node + const ns = this.props.nodes; + const nodeNames = ns.keySeq().toJS(); + const randomNode = _.sample(nodeNames); + if (randomNode) { + let nextNodes = ns.setIn([randomNode, 'filtered'], true); + this.shortLivedNodes = this.shortLivedNodes.add(randomNode); + // bring nodes back after a bit + if (this.shortLivedNodes.size > 5) { + const returningNode = this.shortLivedNodes.first(); + this.shortLivedNodes = this.shortLivedNodes.rest(); + nextNodes = nextNodes.setIn([returningNode, 'filtered'], false); + } + this.asyncDispatch(setAppState(state => state.set('nodes', nextNodes))); + } + }, 1000); + } + } + + updateAdjacencies() { + const ns = this.props.nodes; + const nodeNames = ns.keySeq().toJS(); + this.asyncDispatch(receiveNodesDelta({ + add: this._addNodes(7), + update: sample(nodeNames).map(n => ({ + id: n, + adjacency: sample(nodeNames), + }), nodeNames.length), + remove: this._removeNode(), + })); + } + _addNodes(n, prefix = 'zing') { const ns = this.props.nodes; const nodeNames = ns.keySeq().toJS(); @@ -303,6 +367,12 @@ class DebugToolbar extends React.Component { +
+ + + +
+
From 90c7659526cd96c9795186883ff7f7e54cc67ff1 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 27 Sep 2016 17:06:30 -0700 Subject: [PATCH 2/9] Dont relayout for new unconnected nodes --- .../charts/__tests__/node-layout-test.js | 56 ++++++++-------- client/app/scripts/charts/nodes-layout.js | 64 +++++++++++++++---- 2 files changed, 82 insertions(+), 38 deletions(-) diff --git a/client/app/scripts/charts/__tests__/node-layout-test.js b/client/app/scripts/charts/__tests__/node-layout-test.js index 8f8c7b969b..f55b612037 100644 --- a/client/app/scripts/charts/__tests__/node-layout-test.js +++ b/client/app/scripts/charts/__tests__/node-layout-test.js @@ -294,7 +294,9 @@ describe('NodesLayout', () => { it('renders single nodes next to portrait graph', () => { const result = NodesLayout.doLayout( nodeSets.singlePortrait.nodes, - nodeSets.singlePortrait.edges); + nodeSets.singlePortrait.edges, + { noCache: true } + ); nodes = result.nodes.toJS(); @@ -314,7 +316,9 @@ describe('NodesLayout', () => { it('renders an additional single node in single nodes group', () => { let result = NodesLayout.doLayout( nodeSets.singlePortrait.nodes, - nodeSets.singlePortrait.edges); + nodeSets.singlePortrait.edges, + { noCache: true } + ); nodes = result.nodes.toJS(); @@ -347,28 +351,28 @@ 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.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); + // }); }); diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index e41b5a9b8e..bb2d803bd4 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -118,6 +118,8 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) { // return object with the width and height of layout return { + graphWidth: layout.width, + graphHeight: layout.height, width: layout.width, height: layout.height, nodes, @@ -125,6 +127,13 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) { }; } +export function doLayoutNewNodesOfExistingRank(layout, immNodes, immEdges, opts) { + console.log(opts); + // determine new nodes + // layout new nodes + // return layout +} + /** * Add coordinates to 0-degree nodes using a square layout * Depending on the previous layout run's graph aspect ratio, the square will be @@ -142,7 +151,9 @@ function layoutSingleNodes(layout, opts) { const nodesep = scale(NODE_SEPARATION_FACTOR); const nodeWidth = scale(NODE_SIZE_FACTOR); const nodeHeight = scale(NODE_SIZE_FACTOR); - const aspectRatio = layout.height ? layout.width / layout.height : 1; + const graphHeight = layout.graphHeight || layout.height; + const graphWidth = layout.graphWidth || layout.width; + const aspectRatio = graphHeight ? graphWidth / graphHeight : 1; let nodes = layout.nodes; @@ -274,11 +285,29 @@ export function hasUnseenNodes(nodes, cache) { return hasUnseen; } +/** + * Determine if all new nodes are 0-degree nodes + * Requires cached nodes (implies a previous layout run). + * @param {Map} nodes new Map of nodes + * @param {Map} cache old Map of nodes + * @return {Boolean} True if all new nodes are 0-nodes + */ function hasNewSingleNode(nodes, cache) { - return (ImmSet - .fromKeys(nodes) - .subtract(ImmSet.fromKeys(cache)) - .every(key => nodes.getIn([key, 'degree']) === 0)); + const oldNodes = ImmSet.fromKeys(cache); + const newNodes = ImmSet.fromKeys(nodes).subtract(oldNodes); + const hasNewSingleNodes = newNodes.every(key => nodes.getIn([key, 'degree']) === 0); + return oldNodes.size > 0 && hasNewSingleNodes; +} + +/** + * 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} 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; } /** @@ -322,7 +351,8 @@ function cloneLayout(layout, nodes, edges) { */ function copyLayoutProperties(layout, nodeCache, edgeCache) { const result = Object.assign({}, layout); - result.nodes = layout.nodes.map(node => node.merge(nodeCache.get(node.get('id')))); + result.nodes = layout.nodes.map(node => (nodeCache.has(node.get('id')) + ? node.merge(nodeCache.get(node.get('id'))) : node)); result.edges = layout.edges.map(edge => { if (edgeCache.has(edge.get('id')) && hasSameEndpoints(edgeCache.get(edge.get('id')), result.nodes)) { @@ -347,7 +377,7 @@ export function doLayout(immNodes, immEdges, opts) { const cacheId = buildTopologyCacheId(options.topologyId, options.topologyOptions); // one engine and node and edge caches per topology, to keep renderings similar - if (!topologyCaches[cacheId]) { + if (options.noCache || !topologyCaches[cacheId]) { topologyCaches[cacheId] = { nodeCache: makeMap(), edgeCache: makeMap(), @@ -359,22 +389,32 @@ export function doLayout(immNodes, immEdges, opts) { const cachedLayout = options.cachedLayout || cache.cachedLayout; const nodeCache = options.nodeCache || cache.nodeCache; const edgeCache = options.edgeCache || cache.edgeCache; + const useCache = !options.forceRelayout && cachedLayout && nodeCache && edgeCache; let layout; ++layoutRuns; - if (!options.forceRelayout && cachedLayout && nodeCache && edgeCache - && !hasUnseenNodes(immNodes, nodeCache)) { + if (useCache && !hasUnseenNodes(immNodes, nodeCache)) { log('skip layout, trivial adjustment', ++layoutRunsTrivial, layoutRuns); layout = cloneLayout(cachedLayout, immNodes, immEdges); // copy old properties, works also if nodes get re-added layout = copyLayoutProperties(layout, nodeCache, edgeCache); } else { - const graph = cache.graph; const nodesWithDegrees = updateNodeDegrees(immNodes, immEdges); - if (hasNewSingleNode(nodesWithDegrees, nodeCache)) { - layout = cloneLayout(cachedLayout, immNodes, immEdges); + if (useCache && hasNewSingleNode(nodesWithDegrees, nodeCache)) { + // special case: new nodes are 0-degree nodes, no need for layout run, + // they will be layed out further below + 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)) { + // 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); } else { + const graph = cache.graph; layout = runLayoutEngine(graph, nodesWithDegrees, immEdges, opts); if (!layout) { return layout; From d131e9903935e0aa63177de25915621bea17e48c Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 7 Nov 2016 17:36:41 +0100 Subject: [PATCH 3/9] Simplified layout steps for singles and same-ranks --- .../charts/__tests__/node-layout-test.js | 134 ++++++++++++---- client/app/scripts/charts/nodes-layout.js | 143 +++++++++++++----- 2 files changed, 205 insertions(+), 72 deletions(-) diff --git a/client/app/scripts/charts/__tests__/node-layout-test.js b/client/app/scripts/charts/__tests__/node-layout-test.js index f55b612037..9668705cdf 100644 --- a/client/app/scripts/charts/__tests__/node-layout-test.js +++ b/client/app/scripts/charts/__tests__/node-layout-test.js @@ -35,21 +35,36 @@ 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'}, - 'n1-n5': {id: 'n1-n5', source: 'n1', target: 'n5'}, '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-n6': {id: 'n2-n6', source: 'n2', target: 'n6'}, + }) + }, removeEdge24: { nodes: fromJS({ n1: {id: 'n1'}, @@ -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); + }); }); diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index bb2d803bd4..785cda483e 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -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 - })); - - 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 - })) - )); + 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 + })) + )); + } 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); } From afe177cdba8947d997ccd679b8c7951ba161363c Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 14 Nov 2016 17:16:46 +0100 Subject: [PATCH 4/9] Support for feature flags --- .../utils/__tests__/feature-utils-test.js | 30 +++++++++++++++++++ client/app/scripts/utils/feature-utils.js | 18 +++++++++++ client/app/scripts/utils/storage-utils.js | 25 +++++++++++++++- client/package.json | 4 +++ client/test/support/localStorage.js | 17 +++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 client/app/scripts/utils/__tests__/feature-utils-test.js create mode 100644 client/app/scripts/utils/feature-utils.js create mode 100644 client/test/support/localStorage.js diff --git a/client/app/scripts/utils/__tests__/feature-utils-test.js b/client/app/scripts/utils/__tests__/feature-utils-test.js new file mode 100644 index 0000000000..2395236e47 --- /dev/null +++ b/client/app/scripts/utils/__tests__/feature-utils-test.js @@ -0,0 +1,30 @@ + +const FU = require('../feature-utils'); + +describe('FeatureUtils', () => { + const FEATURE_X_KEY = 'my feature 1'; + const FEATURE_Y_KEY = 'my feature 2'; + + beforeEach(() => { + FU.clearFeatures(); + }); + + describe('Setting of features', () => { + it('should not have any features by default', () => { + expect(FU.featureIsEnabled(FEATURE_X_KEY)).toBeFalsy(); + expect(FU.featureIsEnabled(FEATURE_Y_KEY)).toBeFalsy(); + }); + + it('should work with enabling one feature', () => { + let success; + expect(FU.featureIsEnabled(FEATURE_X_KEY)).toBeFalsy(); + success = FU.setFeature(FEATURE_X_KEY, true); + expect(success).toBeTruthy(); + expect(FU.featureIsEnabled(FEATURE_X_KEY)).toBeTruthy(); + expect(FU.featureIsEnabled(FEATURE_Y_KEY)).toBeFalsy(); + success = FU.setFeature(FEATURE_X_KEY, false); + expect(success).toBeTruthy(); + expect(FU.featureIsEnabled(FEATURE_X_KEY)).toBeFalsy(); + }); + }); +}); diff --git a/client/app/scripts/utils/feature-utils.js b/client/app/scripts/utils/feature-utils.js new file mode 100644 index 0000000000..abd4ae3b5d --- /dev/null +++ b/client/app/scripts/utils/feature-utils.js @@ -0,0 +1,18 @@ +import { storageGetObject, storageSetObject } from './storage-utils'; + +const STORAGE_FEATURE_KEY = 'scopeFeatureFlags'; + +export function featureIsEnabled(feature) { + const features = storageGetObject(STORAGE_FEATURE_KEY, {}); + return features[feature]; +} + +export function setFeature(feature, isEnabled) { + const features = storageGetObject(STORAGE_FEATURE_KEY, {}); + features[feature] = isEnabled; + return storageSetObject(STORAGE_FEATURE_KEY, features); +} + +export function clearFeatures() { + return storageSetObject(STORAGE_FEATURE_KEY, {}); +} diff --git a/client/app/scripts/utils/storage-utils.js b/client/app/scripts/utils/storage-utils.js index 0ff92ff99f..1556fa4038 100644 --- a/client/app/scripts/utils/storage-utils.js +++ b/client/app/scripts/utils/storage-utils.js @@ -6,7 +6,7 @@ const log = debug('scope:storage-utils'); const storage = typeof(Storage) !== 'undefined' ? window.localStorage : null; export function storageGet(key, defaultValue) { - if (storage && storage[key] !== undefined) { + if (storage && storage.getItem(key) !== undefined) { return storage.getItem(key); } return defaultValue; @@ -16,8 +16,31 @@ export function storageSet(key, value) { if (storage) { try { storage.setItem(key, value); + return true; } catch (e) { log('Error storing value in storage. Maybe full? Could not store key.', key); } } + return false; +} + +export function storageGetObject(key, defaultValue) { + const value = storageGet(key); + if (value) { + try { + return JSON.parse(value); + } catch (e) { + log('Error getting object for key.', key); + } + } + return defaultValue; +} + +export function storageSetObject(key, obj) { + try { + return storageSet(key, JSON.stringify(obj)); + } catch (e) { + log('Error encoding object for key', key); + } + return false; } diff --git a/client/package.json b/client/package.json index c5505b938e..549ff7390a 100644 --- a/client/package.json +++ b/client/package.json @@ -90,6 +90,10 @@ }, "jest": { "transform": {".*": "/node_modules/babel-jest"}, + "scriptPreprocessor": "/node_modules/babel-jest", + "setupFiles": [ + "/test/support/localStorage.js" + ], "testPathDirs": [ "/app/scripts" ], diff --git a/client/test/support/localStorage.js b/client/test/support/localStorage.js new file mode 100644 index 0000000000..d153bdf8ab --- /dev/null +++ b/client/test/support/localStorage.js @@ -0,0 +1,17 @@ +const localStorageMock = (function() { + let store = {}; + return { + store, + getItem: function(key) { + return store[key]; + }, + setItem: function(key, value) { + store[key] = value.toString(); + }, + clear: function() { + store = {}; + } + }; +})(); +Object.defineProperty(window, 'Storage', { value: localStorageMock }); +Object.defineProperty(window, 'localStorage', { value: localStorageMock }); From faa8ee098cd7886ded27af584b98ba94bf85a39c Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 15 Nov 2016 11:35:35 +0100 Subject: [PATCH 5/9] Adapted feature check to allow only some features --- .../utils/__tests__/feature-utils-test.js | 15 ++++++- client/app/scripts/utils/feature-utils.js | 41 ++++++++++++++----- client/test/support/localStorage.js | 2 +- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/client/app/scripts/utils/__tests__/feature-utils-test.js b/client/app/scripts/utils/__tests__/feature-utils-test.js index 2395236e47..f0ee39296a 100644 --- a/client/app/scripts/utils/__tests__/feature-utils-test.js +++ b/client/app/scripts/utils/__tests__/feature-utils-test.js @@ -6,7 +6,8 @@ describe('FeatureUtils', () => { const FEATURE_Y_KEY = 'my feature 2'; beforeEach(() => { - FU.clearFeatures(); + FU.setFeature(FEATURE_X_KEY, false); + FU.setFeature(FEATURE_Y_KEY, false); }); describe('Setting of features', () => { @@ -26,5 +27,17 @@ describe('FeatureUtils', () => { expect(success).toBeTruthy(); expect(FU.featureIsEnabled(FEATURE_X_KEY)).toBeFalsy(); }); + + it('should allow for either feature', () => { + let success; + expect(FU.featureIsEnabledAny(FEATURE_X_KEY, FEATURE_Y_KEY)).toBeFalsy(); + success = FU.setFeature(FEATURE_X_KEY, true); + expect(success).toBeTruthy(); + expect(FU.featureIsEnabledAny(FEATURE_X_KEY, FEATURE_Y_KEY)).toBeTruthy(); + success = FU.setFeature(FEATURE_X_KEY, false); + success = FU.setFeature(FEATURE_Y_KEY, true); + expect(success).toBeTruthy(); + expect(FU.featureIsEnabledAny(FEATURE_X_KEY, FEATURE_Y_KEY)).toBeTruthy(); + }); }); }); diff --git a/client/app/scripts/utils/feature-utils.js b/client/app/scripts/utils/feature-utils.js index abd4ae3b5d..91c2fccfed 100644 --- a/client/app/scripts/utils/feature-utils.js +++ b/client/app/scripts/utils/feature-utils.js @@ -1,18 +1,39 @@ -import { storageGetObject, storageSetObject } from './storage-utils'; +import { storageGet, storageSet } from './storage-utils'; -const STORAGE_FEATURE_KEY = 'scopeFeatureFlags'; +// prefix for all feature flags +const STORAGE_KEY_PREFIX = 'scope-experimental-'; +const getKey = key => `${STORAGE_KEY_PREFIX}${key}`; + +/** + * Returns true if `feature` is enabled + * + * Features can be enabled either via calling `setFeature()` or by setting + * `localStorage.scope-experimental-featureName = true` in the console. + * @param {String} feature Feature name, ideally one word or hyphenated + * @return {Boolean} True if feature is enabled + */ export function featureIsEnabled(feature) { - const features = storageGetObject(STORAGE_FEATURE_KEY, {}); - return features[feature]; + return storageGet(getKey(feature)); } -export function setFeature(feature, isEnabled) { - const features = storageGetObject(STORAGE_FEATURE_KEY, {}); - features[feature] = isEnabled; - return storageSetObject(STORAGE_FEATURE_KEY, features); +/** + * Returns true if any of the features given as arguments are enabled. + * + * Useful if features are hierarchical, e.g.: + * `featureIsEnabledAny('superFeature', 'subFeature')` + * @param {String} args Feature names + * @return {Boolean} True if any of the features are enabled + */ +export function featureIsEnabledAny(...args) { + return Array.prototype.some.call(args, feature => featureIsEnabled(feature)); } -export function clearFeatures() { - return storageSetObject(STORAGE_FEATURE_KEY, {}); +/** + * Set true/false if a feature is enabled. + * @param {String} feature Feature name + * @param {Boolean} isEnabled true/false + */ +export function setFeature(feature, isEnabled) { + return storageSet(getKey(feature), isEnabled); } diff --git a/client/test/support/localStorage.js b/client/test/support/localStorage.js index d153bdf8ab..50cf4b680b 100644 --- a/client/test/support/localStorage.js +++ b/client/test/support/localStorage.js @@ -6,7 +6,7 @@ const localStorageMock = (function() { return store[key]; }, setItem: function(key, value) { - store[key] = value.toString(); + store[key] = value; }, clear: function() { store = {}; From aa7b85b5f0e3650b99a2df6efb1a94c89c599c83 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 15 Nov 2016 11:41:25 +0100 Subject: [PATCH 6/9] Use feature flags in nodes-layout --- client/app/scripts/charts/nodes-layout.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index 785cda483e..e972efca8b 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -3,6 +3,7 @@ import debug from 'debug'; import { fromJS, Map as makeMap, Set as ImmSet } from 'immutable'; import { EDGE_ID_SEPARATOR } from '../constants/naming'; +import { featureIsEnabledAny } from '../utils/feature-utils'; import { buildTopologyCacheId, updateNodeDegrees } from '../utils/topology-utils'; const log = debug('scope:nodes-layout'); @@ -460,13 +461,17 @@ export function doLayout(immNodes, immEdges, opts) { layout = copyLayoutProperties(layout, nodeCache, edgeCache); } else { const nodesWithDegrees = updateNodeDegrees(immNodes, immEdges); - if (useCache && hasNewSingleNode(nodesWithDegrees, nodeCache)) { + if (useCache + && featureIsEnabledAny('layout-dance', 'layout-dance-single') + && hasNewSingleNode(nodesWithDegrees, nodeCache)) { // special case: new nodes are 0-degree nodes, no need for layout run, // they will be layed out further below log('skip layout, only 0-degree node(s) added'); layout = cloneLayout(cachedLayout, nodesWithDegrees, immEdges); layout = copyLayoutProperties(layout, nodeCache, edgeCache); - } else if (useCache && hasNewNodesOfExistingRank(nodesWithDegrees, immEdges, nodeCache)) { + } else if (useCache + && featureIsEnabledAny('layout-dance', 'layout-dance-rank') + && 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'); From beed39f13ebc9ed06460ecc61460e538f69cc19a Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 15 Nov 2016 11:50:16 +0100 Subject: [PATCH 7/9] Add feature flags to test --- client/app/scripts/charts/__tests__/node-layout-test.js | 6 ++++++ client/app/scripts/utils/feature-utils.js | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/app/scripts/charts/__tests__/node-layout-test.js b/client/app/scripts/charts/__tests__/node-layout-test.js index 9668705cdf..72f0467c53 100644 --- a/client/app/scripts/charts/__tests__/node-layout-test.js +++ b/client/app/scripts/charts/__tests__/node-layout-test.js @@ -133,6 +133,9 @@ describe('NodesLayout', () => { }; beforeEach(() => { + // clear feature flags + window.localStorage.clear(); + options = { nodeCache: makeMap(), edgeCache: makeMap() @@ -416,6 +419,9 @@ describe('NodesLayout', () => { }); it('adds a new node to existing layout in a line', () => { + // feature flag + window.localStorage.setItem('scope-experimental:layout-dance', true); + let result = NodesLayout.doLayout( nodeSets.rank4.nodes, nodeSets.rank4.edges, diff --git a/client/app/scripts/utils/feature-utils.js b/client/app/scripts/utils/feature-utils.js index 91c2fccfed..0e6a9681c4 100644 --- a/client/app/scripts/utils/feature-utils.js +++ b/client/app/scripts/utils/feature-utils.js @@ -1,7 +1,7 @@ import { storageGet, storageSet } from './storage-utils'; // prefix for all feature flags -const STORAGE_KEY_PREFIX = 'scope-experimental-'; +const STORAGE_KEY_PREFIX = 'scope-experimental:'; const getKey = key => `${STORAGE_KEY_PREFIX}${key}`; @@ -9,7 +9,7 @@ const getKey = key => `${STORAGE_KEY_PREFIX}${key}`; * Returns true if `feature` is enabled * * Features can be enabled either via calling `setFeature()` or by setting - * `localStorage.scope-experimental-featureName = true` in the console. + * `localStorage.scope-experimental:featureName = true` in the console. * @param {String} feature Feature name, ideally one word or hyphenated * @return {Boolean} True if feature is enabled */ From 79a9598d0f26c0744ec69d8e8451a93de4ddecda Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Mon, 21 Nov 2016 11:59:30 +0100 Subject: [PATCH 8/9] Change spelling for eslint --- client/app/scripts/charts/nodes-layout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index e972efca8b..670671777b 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -465,7 +465,7 @@ export function doLayout(immNodes, immEdges, opts) { && featureIsEnabledAny('layout-dance', 'layout-dance-single') && hasNewSingleNode(nodesWithDegrees, nodeCache)) { // special case: new nodes are 0-degree nodes, no need for layout run, - // they will be layed out further below + // they will be laid out further below log('skip layout, only 0-degree node(s) added'); layout = cloneLayout(cachedLayout, nodesWithDegrees, immEdges); layout = copyLayoutProperties(layout, nodeCache, edgeCache); From 90dbf12a7b36ec75e08df5e9123382d4ada08464 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 24 Nov 2016 17:05:38 +0100 Subject: [PATCH 9/9] Review feedback --- .../app/scripts/components/debug-toolbar.js | 29 +++++-------------- client/package.json | 1 - 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 34e771cca6..14d5eaa74b 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -160,8 +160,8 @@ class DebugToolbar extends React.Component { this.onChange = this.onChange.bind(this); this.toggleColors = this.toggleColors.bind(this); this.addNodes = this.addNodes.bind(this); - this.intermittendTimer = null; - this.intermittendNodes = makeSet(); + this.intermittentTimer = null; + this.intermittentNodes = makeSet(); this.shortLivedTimer = null; this.shortLivedNodes = makeSet(); this.state = { @@ -188,26 +188,13 @@ class DebugToolbar extends React.Component { this.asyncDispatch(setAppState(state => state.set('topologiesLoaded', !loading))); } - updateAdjacencies() { - const ns = this.props.nodes; - const nodeNames = ns.keySeq().toJS(); - this.asyncDispatch(receiveNodesDelta({ - add: this._addNodes(7), - update: sample(nodeNames).map(n => ({ - id: n, - adjacency: sample(nodeNames), - }), nodeNames.length), - remove: this._removeNode(), - })); - } - - setIntermittend() { + setIntermittent() { // simulate epheremal nodes - if (this.intermittendTimer) { - clearInterval(this.intermittendTimer); - this.intermittendTimer = null; + if (this.intermittentTimer) { + clearInterval(this.intermittentTimer); + this.intermittentTimer = null; } else { - this.intermittendTimer = setInterval(() => { + this.intermittentTimer = setInterval(() => { // add new node this.addNodes(1); @@ -370,7 +357,7 @@ class DebugToolbar extends React.Component {
- +
diff --git a/client/package.json b/client/package.json index 549ff7390a..6fc40c90bc 100644 --- a/client/package.json +++ b/client/package.json @@ -90,7 +90,6 @@ }, "jest": { "transform": {".*": "/node_modules/babel-jest"}, - "scriptPreprocessor": "/node_modules/babel-jest", "setupFiles": [ "/test/support/localStorage.js" ],