diff --git a/client/app/scripts/charts/nodes-chart-elements.js b/client/app/scripts/charts/nodes-chart-elements.js
index 7d921f0a8..19b1aa996 100644
--- a/client/app/scripts/charts/nodes-chart-elements.js
+++ b/client/app/scripts/charts/nodes-chart-elements.js
@@ -1,6 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
+import { completeNodesSelector } from '../selectors/chartSelectors';
import NodesChartEdges from './nodes-chart-edges';
import NodesChartNodes from './nodes-chart-nodes';
@@ -9,14 +10,24 @@ class NodesChartElements extends React.Component {
const props = this.props;
return (
-
-
);
}
}
-export default connect()(NodesChartElements);
+function mapStateToProps(state, props) {
+ return {
+ completeNodes: completeNodesSelector(state, props)
+ };
+}
+
+export default connect(mapStateToProps)(NodesChartElements);
diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js
index fdce656aa..ca36943af 100644
--- a/client/app/scripts/charts/nodes-chart.js
+++ b/client/app/scripts/charts/nodes-chart.js
@@ -24,22 +24,6 @@ const radiusDensity = d3.scale.threshold()
.range([2.5, 3.5, 3]);
-function toNodes(nodes) {
- return nodes.map((node, id) => makeMap({
- id,
- label: node.get('label'),
- pseudo: node.get('pseudo'),
- subLabel: node.get('label_minor'),
- nodeCount: node.get('node_count'),
- metrics: node.get('metrics'),
- rank: node.get('rank'),
- shape: node.get('shape'),
- stack: node.get('stack'),
- networks: node.get('networks'),
- }));
-}
-
-
function identityPresevingMerge(a, b) {
//
// merge two maps, if the values are equal return the old value to preserve (a === a')
@@ -51,14 +35,6 @@ function identityPresevingMerge(a, b) {
}
-function mergeDeepIfExists(mapA, mapB) {
- //
- // Does a deep merge on any key that exists in the first map
- //
- return mapA.map((v, k) => v.mergeDeep(mapB.get(k)));
-}
-
-
function getLayoutNodes(nodes) {
return nodes.map(n => makeMap({
id: n.get('id'),
@@ -97,12 +73,13 @@ function initEdges(nodes) {
}
-function getNodeScale(nodes, width, height) {
+function getNodeScale(nodesCount, width, height) {
+ console.log(nodesCount, width, height);
const expanse = Math.min(height, width);
const nodeSize = expanse / 3; // single node should fill a third of the screen
const maxNodeSize = Math.min(MAX_NODE_SIZE, expanse / 10);
const normalizedNodeSize = Math.max(MIN_NODE_SIZE,
- Math.min(nodeSize / Math.sqrt(nodes.size), maxNodeSize));
+ Math.min(nodeSize / Math.sqrt(nodesCount), maxNodeSize));
return d3.scale.linear().range([0, normalizedNodeSize]);
}
@@ -110,7 +87,7 @@ function getNodeScale(nodes, width, height) {
function updateLayout({width, height, margins, topologyId, topologyOptions, forceRelayout,
nodes }) {
- const nodeScale = getNodeScale(nodes, width, height);
+ const nodeScale = getNodeScale(nodes.size, width, height);
const edges = initEdges(nodes);
const options = {
@@ -119,7 +96,7 @@ function updateLayout({width, height, margins, topologyId, topologyOptions, forc
margins: margins.toJS(),
forceRelayout,
topologyId,
- topologyOptions: topologyOptions.toJS(),
+ topologyOptions: (topologyOptions && topologyOptions.toJS()),
scale: nodeScale,
};
@@ -128,20 +105,21 @@ function updateLayout({width, height, margins, topologyId, topologyOptions, forc
log(`graph layout took ${timedLayouter.time}ms`);
- // extract coords and save for restore
const layoutNodes = graph.nodes.map(node => makeMap({
x: node.get('x'),
- px: node.get('x'),
y: node.get('y'),
+ // extract coords and save for restore
+ px: node.get('x'),
py: node.get('y')
}));
const layoutEdges = graph.edges
.map(edge => edge.set('ppoints', edge.get('points')));
- return { layoutNodes, layoutEdges, graph, nodeScale };
+ return { layoutNodes, layoutEdges, width: graph.width, height: graph.height };
}
+
class NodesChart extends React.Component {
constructor(props, context) {
@@ -244,6 +222,7 @@ class NodesChart extends React.Component {
const translate = [panTranslateX, panTranslateY];
const transform = `translate(${translate}) scale(${scale})`;
const svgClassNames = this.props.isEmpty ? 'hide' : '';
+ console.log('nodes-chart.render');
return (
@@ -338,7 +317,7 @@ class NodesChart extends React.Component {
});
// auto-scale node size for selected nodes
- const selectedNodeScale = getNodeScale(adjacentNodes, state.width, state.height);
+ const selectedNodeScale = getNodeScale(adjacentNodes.size, state.width, state.height);
return {
selectedNodeScale,
@@ -375,11 +354,6 @@ class NodesChart extends React.Component {
};
}
- const nextState = { nodes: toNodes(props.nodes) };
-
- //
- // pull this out into redux.
- //
const layoutInput = identityPresevingMerge(state.layoutInput, {
width: state.width,
height: state.height,
@@ -391,34 +365,32 @@ class NodesChart extends React.Component {
});
// layout input hasn't changed.
- // TODO: move this out into reselect
- if (state.layoutInput !== layoutInput) {
- nextState.layoutInput = layoutInput;
-
- const { layoutNodes, layoutEdges, graph, nodeScale } = updateLayout(layoutInput.toObject());
- //
- // adjust layout based on viewport
- const xFactor = (state.width - props.margins.left - props.margins.right) / graph.width;
- const yFactor = state.height / graph.height;
- const zoomFactor = Math.min(xFactor, yFactor);
- let zoomScale = state.scale;
-
- if (this.zoom && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
- zoomScale = zoomFactor;
- // saving in d3's behavior cache
- this.zoom.scale(zoomFactor);
- }
-
- nextState.scale = zoomScale;
- nextState.edges = layoutEdges;
- nextState.nodeScale = nodeScale;
- nextState.layoutNodes = layoutNodes;
+ // TODO: move this out into reselect (relies on `state` a bit right now which makes it tricky)
+ if (state.layoutInput === layoutInput) {
+ return {};
}
- nextState.nodes = mergeDeepIfExists(nextState.nodes,
- (nextState.layoutNodes || state.layoutNodes));
+ const { layoutNodes, layoutEdges, width, height } = updateLayout(layoutInput.toObject());
+ //
+ // adjust layout based on viewport
+ const xFactor = (state.width - props.margins.left - props.margins.right) / width;
+ const yFactor = state.height / height;
+ const zoomFactor = Math.min(xFactor, yFactor);
+ let zoomScale = state.scale;
- return nextState;
+ if (this.zoom && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
+ zoomScale = zoomFactor;
+ // saving in d3's behavior cache
+ this.zoom.scale(zoomFactor);
+ }
+
+ return {
+ layoutInput,
+ scale: zoomScale,
+ nodes: layoutNodes,
+ edges: layoutEdges,
+ nodeScale: getNodeScale(props.nodes.size, state.width, state.height),
+ };
}
zoomed() {
@@ -436,6 +408,7 @@ class NodesChart extends React.Component {
}
}
+
function mapStateToProps(state) {
return {
adjacentNodes: getAdjacentNodes(state),
@@ -446,6 +419,7 @@ function mapStateToProps(state) {
};
}
+
export default connect(
mapStateToProps,
{ clickBackground }
diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js
index e9632af3c..c96a9f22e 100644
--- a/client/app/scripts/components/nodes.js
+++ b/client/app/scripts/components/nodes.js
@@ -8,6 +8,7 @@ import { DelayedShow } from '../utils/delayed-show';
import { Loading, getNodeType } from './loading';
import { isTopologyEmpty } from '../utils/topology-utils';
import { CANVAS_MARGINS } from '../constants/styles';
+import { nodesSelector } from '../selectors/chartSelectors';
const navbarHeight = 194;
const marginTop = 0;
@@ -71,6 +72,8 @@ class Nodes extends React.Component {
currentTopology } = this.props;
const layoutPrecision = getLayoutPrecision(nodes.size);
+ console.log('nodes.render');
+
return (
@@ -113,7 +116,7 @@ function mapStateToProps(state) {
return {
currentTopology: state.get('currentTopology'),
gridMode: state.get('gridMode'),
- nodes: state.get('nodes').filter(node => !node.get('filtered')),
+ nodes: nodesSelector(state),
nodesLoaded: state.get('nodesLoaded'),
topologies: state.get('topologies'),
topologiesLoaded: state.get('topologiesLoaded'),
diff --git a/client/app/scripts/selectors/chartSelectors.js b/client/app/scripts/selectors/chartSelectors.js
new file mode 100644
index 000000000..9ca929b53
--- /dev/null
+++ b/client/app/scripts/selectors/chartSelectors.js
@@ -0,0 +1,61 @@
+import { createSelector } from 'reselect';
+import { Map as makeMap } from 'immutable';
+
+
+const allNodesSelector = state => state.get('nodes');
+
+
+export const nodesSelector = createSelector(
+ allNodesSelector,
+ (allNodes) => allNodes.filter(node => !node.get('filtered'))
+);
+
+
+export const nodeAdjacenciesSelector = createSelector(
+ nodesSelector,
+ (nodes) => nodes.map(n => makeMap({
+ id: n.get('id'),
+ adjacency: n.get('adjacency'),
+ }))
+);
+
+
+export const layoutNodesSelector = (_, props) => props.layoutNodes;
+
+
+export const dataNodesSelector = createSelector(
+ nodesSelector,
+ (nodes) => nodes.map((node, id) => makeMap({
+ id,
+ label: node.get('label'),
+ pseudo: node.get('pseudo'),
+ subLabel: node.get('label_minor'),
+ nodeCount: node.get('node_count'),
+ metrics: node.get('metrics'),
+ rank: node.get('rank'),
+ shape: node.get('shape'),
+ stack: node.get('stack'),
+ networks: node.get('networks'),
+ }))
+);
+
+
+function mergeDeepIfExists(mapA, mapB) {
+ //
+ // Does a deep merge on any key that exists in the first map
+ //
+ return mapA.map((v, k) => v.mergeDeep(mapB.get(k)));
+}
+
+
+export const completeNodesSelector = createSelector(
+ layoutNodesSelector,
+ dataNodesSelector,
+ (layoutNodes, dataNodes) => {
+ if (!layoutNodes || layoutNodes.size === 0) {
+ return makeMap();
+ }
+
+ return mergeDeepIfExists(dataNodes, layoutNodes);
+ }
+);
diff --git a/client/package.json b/client/package.json
index f2d2a888e..207f383e1 100644
--- a/client/package.json
+++ b/client/package.json
@@ -29,6 +29,7 @@
"redux-logger": "2.6.1",
"redux-thunk": "2.0.1",
"reqwest": "~2.0.5",
+ "reselect": "^2.5.3",
"timely": "0.1.0",
"whatwg-fetch": "0.11.0"
},