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