diff --git a/ui/package.json b/ui/package.json
index 5d365175..79bbe0d2 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -24,6 +24,7 @@
"lodash.first": "^3.0.0",
"lodash.flatten": "^4.4.0",
"lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
"lodash.isfunction": "^3.0.8",
"lodash.isstring": "^4.0.1",
"lodash.isundefined": "^3.0.1",
diff --git a/ui/src/components/topology/graph-simulation.js b/ui/src/components/topology/graph-simulation.js
new file mode 100644
index 00000000..51c06029
--- /dev/null
+++ b/ui/src/components/topology/graph-simulation.js
@@ -0,0 +1,130 @@
+const d3 = require('d3');
+
+const hypotenuse = (a, b) =>
+ Math.sqrt(a*a + b*b);
+
+const rectRadius = (size) => {
+
+ const {
+ width,
+ height
+ } = size;
+
+ return Math.round(hypotenuse(width, height)/2);
+};
+
+const createSimulation = (
+ nodes,
+ links,
+ nodeSize,
+ svgSize,
+ onTick,
+ onEnd
+) => {
+ // This is not going to work given that as well as the d3 layout stuff, other things might be at play too
+ // We should pass two objects to the components - one for positioning and one for data
+ const mappedNodes = nodes.map((node, index) => ({
+ id: node.id,
+ index: index
+ }));
+ const mappedLinks = links.map((link, index) => ({
+ ...link
+ }));
+
+ const {
+ width,
+ height
+ } = svgSize;
+
+ const nodeRadius = rectRadius(nodeSize);
+
+ return d3.forceSimulation(mappedNodes)
+ .force('link', d3.forceLink(mappedLinks).id(d => d.id))
+ .force('collide', d3.forceCollide(nodeRadius))
+ .force('center', d3.forceCenter(width/2, height/2))
+ .on('tick', onTick)
+ .on('end', onEnd);
+};
+
+ // TODO we need to kill the previous simulation
+const updateSimulation = (
+ simulation,
+ nodes,
+ links,
+ nextNodes,
+ nextLinks,
+ nodeSize,
+ svgSize,
+ onTick,
+ onEnd
+) => {
+ // want to copy all the existing nodes that we still need and freeze them
+ // want to copy all the existing links we still need
+ // if we have any new nodes / links, we should add them
+ // this is going to be messy!!! maybe not so much!!! :D <3
+ const mappedNodes = nextNodes.map((nextNode, index) => {
+ const node = nodes.reduce((acc, n, i) =>
+ nextNode.id === n.id ? n : acc ? null : acc);
+ return node ? {
+ id: node.id,
+ fx: node.x,
+ fy: node.y,
+ index: index
+ } : {
+ id: nextNode.id,
+ index: index
+ };
+ });
+
+ const mappedLinks = nextLinks.map((nextLink, index) => {
+ const link = links.reduce((acc, l, i) =>
+ nextLink.source === l.source && nextLink.target === l.target ?
+ l : acc ? null : acc);
+ return link ? {
+ ...link
+ } : {
+ ...nextLink
+ };
+ });
+
+ const {
+ width,
+ height
+ } = svgSize;
+
+ const nodeRadius = rectRadius(nodeSize);
+
+ return d3.forceSimulation(mappedNodes)
+ .force('link', d3.forceLink(mappedLinks).id(d => d.id))
+ .force('collide', d3.forceCollide(nodeRadius))
+ .force('center', d3.forceCenter(width/2, height/2))
+ .on('tick', onTick)
+ .on('end', onEnd);
+};
+
+module.exports = {
+ createSimulation,
+ updateSimulation
+};
+
+/*
+const simulation = d3.forceSimulation(dataNodes)
+ // .alpha(1).alphaDecay(0.1)
+ // .force('charge', d3.forceManyBody())
+ .force('link', d3.forceLink(dataLinks)
+ //.distance(() => linkDistance)
+ .id(d => d.id))
+ .force('collide', d3.forceCollide(nodeRadius))
+ .force('center', d3.forceCenter(1024/2, 860/2))
+ .on('tick', () => {
+ console.log('SIMULATION TICK');
+ console.log('tickCounter = ', tickCounter);
+ tickCounter++;
+ this.forceUpdate();
+ })
+ .on('end', () => {
+ console.log('SIMULATION END');
+ console.log('tickCounter = ', tickCounter);
+ // this.forceUpdate();
+ })
+*/
diff --git a/ui/src/components/topology/story-helper.js b/ui/src/components/topology/story-helper.js
index 3775de36..138e1cec 100644
--- a/ui/src/components/topology/story-helper.js
+++ b/ui/src/components/topology/story-helper.js
@@ -19,11 +19,6 @@ const StyledForm = styled.form`
margin: 5px;
`;
-const nodeSize = {
- width: 180,
- height: 156
-};
-
class StoryHelper extends React.Component {
constructor(props){
@@ -100,7 +95,7 @@ class StoryHelper extends React.Component {
-
+
);
}
diff --git a/ui/src/components/topology/topology-graph.js b/ui/src/components/topology/topology-graph.js
index 09d77787..8387ccbe 100644
--- a/ui/src/components/topology/topology-graph.js
+++ b/ui/src/components/topology/topology-graph.js
@@ -1,6 +1,6 @@
const React = require('react');
-const d3 = require('d3');
const Styled = require('styled-components');
+const GraphSimulation = require('./graph-simulation');
const GraphNode = require('./graph-node');
const GraphLink = require('./graph-link');
@@ -8,27 +8,25 @@ const {
default: styled
} = Styled;
+const {
+ createSimulation,
+ updateSimulation
+} = GraphSimulation;
+
const StyledSvg = styled.svg`
width: 1024px;
height: 860px;
border: 1px solid #ff0000;
`;
-/*const nodeSize = {
+const nodeSize = {
width: 180,
height: 156
-};*/
+};
-const mapData = (data, withIndex=false) => {
- return data.map((d, index) => {
- const r = {
- ...d
- };
- if(withIndex) {
- r.index = index;
- }
- return r;
- });
+const svgSize = {
+ width: 1024,
+ height: 860
};
class TopologyGraph extends React.Component {
@@ -36,64 +34,101 @@ class TopologyGraph extends React.Component {
componentWillMount() {
const {
- data,
- nodeSize
- } = this.props;
+ nodes,
+ links
+ } = this.props.data;
- this.setState(
- this.createSimulation(data, nodeSize)
+ const simulation = createSimulation(
+ nodes,
+ links,
+ nodeSize,
+ svgSize,
+ () => this.forceUpdate(),
+ () => this.forceUpdate()
);
+
+ const n = Math.ceil(
+ Math.log(
+ simulation.alphaMin()) / Math.log(
+ 1 - simulation.alphaDecay()));
+ for (var i = 0; i < n; ++i) {
+ simulation.tick();
+ }
+
+ this.setState({
+ simulation: simulation
+ });
}
componentWillReceiveProps(nextProps) {
- const {
- data,
- nodeSize
- } = nextProps;
-
- this.setState(
- this.createSimulation(data, nodeSize)
- );
- }
-
- createSimulation(data, nodeSize) {
-
- const dataNodes = mapData(data.nodes, true);
- const dataLinks = mapData(data.links);
+ // either, we'll have more services
+ // or, we'll have less services
+ // or, data of services had changed =>
+ // do shallow check on objects and links, if no change, don't do rerender
+ // otherwise, redo them bitches = by what I mean to update the simulation
+ // try freezing exisiting ones... then adding another
const {
- width,
- height
- } = nodeSize;
- const nodeRadius = Math.round(Math.sqrt(width*width + height*height)/2);
- // const linkDistance = nodeRadius*2 + 20;
- // console.log('nodeRadius = ', nodeRadius);
- // console.log('linkDistance = ', linkDistance);
- const simulation = d3.forceSimulation(dataNodes)
- .force('charge', d3.forceManyBody())
- .force('link', d3.forceLink(dataLinks)
- /*.distance(() => linkDistance)*/
- .id(d => d.id))
- .force('collide', d3.forceCollide(nodeRadius))
- .force('center', d3.forceCenter(1024/2, 860/2))
- .on('tick', () => {
- // console.log('SIMULATION TICK');
- this.forceUpdate();
- })
- .on('end', () => {
- // console.log('SIMULATION END');
- // this.forceUpdate();
+ nodes: nextNodes,
+ links: nextLinks
+ } = nextProps.data;
+
+ const {
+ nodes,
+ links
+ } = this.props.data;
+
+ // this is tmp for the compare above
+ if(nextNodes.length !== nodes.length || nextLinks.length !== links.length) {
+ const simulation = this.state.simulation;
+ const nextSimulation = updateSimulation(
+ simulation,
+ nodes,
+ links,
+ nextNodes,
+ nextLinks,
+ nodeSize,
+ svgSize,
+ () => this.forceUpdate(),
+ () => this.forceUpdate()
+ );
+ this.setState({
+ simulation: nextSimulation
});
- return {
- dataNodes,
- dataLinks,
- simulation
- };
+ const n = Math.ceil(
+ Math.log(
+ nextSimulation.alphaMin()) / Math.log(
+ 1 - nextSimulation.alphaDecay())) - 200;
+ for (var i = 0; i < n; ++i) {
+ nextSimulation.tick();
+ }
+ }
}
- renderNodes(nodeSize) {
- return this.state.dataNodes.map((n, index) => (
+ render() {
+
+ const {
+ nodes,
+ links
+ } = this.props.data;
+
+ const simulationNodes = this.state.simulation.nodes();
+
+ const nodesData = nodes.map((node, index) => ({
+ ...node,
+ ...simulationNodes.reduce((acc, simNode, index) =>
+ simNode.id === node.id ? simNode : acc)
+ }));
+
+ const linksData = links.map((link, index) => ({
+ source: simulationNodes.reduce((acc, simNode, index) =>
+ simNode.id === link.source ? simNode : acc),
+ target: simulationNodes.reduce((acc, simNode, index) =>
+ simNode.id === link.target ? simNode : acc)
+ }));
+
+ const renderedNodes = nodesData.map((n, index) => (
));
- }
- renderLinks(nodeSize) {
- return this.state.dataLinks.map((l, index) => (
+ const renderedLinks = linksData.map((l, index) => (
));
- }
-
- render() {
- const {
- nodeSize
- } = this.props;
return (
- {this.renderNodes(nodeSize)}
+ {renderedNodes}
- {this.renderLinks(nodeSize)}
+ {renderedLinks}
);
@@ -136,10 +163,6 @@ TopologyGraph.propTypes = {
data: React.PropTypes.shape({
nodes: React.PropTypes.array,
links: React.PropTypes.array
- }),
- nodeSize: React.PropTypes.shape({
- width: React.PropTypes.number,
- height: React.PropTypes.number
})
};