mirror of
https://github.com/yldio/copilot.git
synced 2024-11-11 05:40:11 +02:00
Adjust animation on topology
This commit is contained in:
parent
43569aeb43
commit
705aa32e73
@ -24,6 +24,7 @@
|
|||||||
"lodash.first": "^3.0.0",
|
"lodash.first": "^3.0.0",
|
||||||
"lodash.flatten": "^4.4.0",
|
"lodash.flatten": "^4.4.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.isfunction": "^3.0.8",
|
"lodash.isfunction": "^3.0.8",
|
||||||
"lodash.isstring": "^4.0.1",
|
"lodash.isstring": "^4.0.1",
|
||||||
"lodash.isundefined": "^3.0.1",
|
"lodash.isundefined": "^3.0.1",
|
||||||
|
130
ui/src/components/topology/graph-simulation.js
Normal file
130
ui/src/components/topology/graph-simulation.js
Normal file
@ -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();
|
||||||
|
})
|
||||||
|
*/
|
@ -19,11 +19,6 @@ const StyledForm = styled.form`
|
|||||||
margin: 5px;
|
margin: 5px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const nodeSize = {
|
|
||||||
width: 180,
|
|
||||||
height: 156
|
|
||||||
};
|
|
||||||
|
|
||||||
class StoryHelper extends React.Component {
|
class StoryHelper extends React.Component {
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
@ -100,7 +95,7 @@ class StoryHelper extends React.Component {
|
|||||||
</Select>
|
</Select>
|
||||||
<Input name='Add service' type='submit' />
|
<Input name='Add service' type='submit' />
|
||||||
</StyledForm>
|
</StyledForm>
|
||||||
<TopologyGraph data={data} nodeSize={nodeSize} />
|
<TopologyGraph data={data} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const d3 = require('d3');
|
|
||||||
const Styled = require('styled-components');
|
const Styled = require('styled-components');
|
||||||
|
const GraphSimulation = require('./graph-simulation');
|
||||||
const GraphNode = require('./graph-node');
|
const GraphNode = require('./graph-node');
|
||||||
const GraphLink = require('./graph-link');
|
const GraphLink = require('./graph-link');
|
||||||
|
|
||||||
@ -8,27 +8,25 @@ const {
|
|||||||
default: styled
|
default: styled
|
||||||
} = Styled;
|
} = Styled;
|
||||||
|
|
||||||
|
const {
|
||||||
|
createSimulation,
|
||||||
|
updateSimulation
|
||||||
|
} = GraphSimulation;
|
||||||
|
|
||||||
const StyledSvg = styled.svg`
|
const StyledSvg = styled.svg`
|
||||||
width: 1024px;
|
width: 1024px;
|
||||||
height: 860px;
|
height: 860px;
|
||||||
border: 1px solid #ff0000;
|
border: 1px solid #ff0000;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/*const nodeSize = {
|
const nodeSize = {
|
||||||
width: 180,
|
width: 180,
|
||||||
height: 156
|
height: 156
|
||||||
};*/
|
};
|
||||||
|
|
||||||
const mapData = (data, withIndex=false) => {
|
const svgSize = {
|
||||||
return data.map((d, index) => {
|
width: 1024,
|
||||||
const r = {
|
height: 860
|
||||||
...d
|
|
||||||
};
|
|
||||||
if(withIndex) {
|
|
||||||
r.index = index;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class TopologyGraph extends React.Component {
|
class TopologyGraph extends React.Component {
|
||||||
@ -36,64 +34,101 @@ class TopologyGraph extends React.Component {
|
|||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
nodes,
|
||||||
nodeSize
|
links
|
||||||
} = this.props;
|
} = this.props.data;
|
||||||
|
|
||||||
this.setState(
|
const simulation = createSimulation(
|
||||||
this.createSimulation(data, nodeSize)
|
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) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const {
|
// either, we'll have more services
|
||||||
data,
|
// or, we'll have less services
|
||||||
nodeSize
|
// or, data of services had changed =>
|
||||||
} = nextProps;
|
// 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
|
||||||
this.setState(
|
// try freezing exisiting ones... then adding another
|
||||||
this.createSimulation(data, nodeSize)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createSimulation(data, nodeSize) {
|
|
||||||
|
|
||||||
const dataNodes = mapData(data.nodes, true);
|
|
||||||
const dataLinks = mapData(data.links);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
width,
|
nodes: nextNodes,
|
||||||
height
|
links: nextLinks
|
||||||
} = nodeSize;
|
} = nextProps.data;
|
||||||
const nodeRadius = Math.round(Math.sqrt(width*width + height*height)/2);
|
|
||||||
// const linkDistance = nodeRadius*2 + 20;
|
const {
|
||||||
// console.log('nodeRadius = ', nodeRadius);
|
nodes,
|
||||||
// console.log('linkDistance = ', linkDistance);
|
links
|
||||||
const simulation = d3.forceSimulation(dataNodes)
|
} = this.props.data;
|
||||||
.force('charge', d3.forceManyBody())
|
|
||||||
.force('link', d3.forceLink(dataLinks)
|
// this is tmp for the compare above
|
||||||
/*.distance(() => linkDistance)*/
|
if(nextNodes.length !== nodes.length || nextLinks.length !== links.length) {
|
||||||
.id(d => d.id))
|
const simulation = this.state.simulation;
|
||||||
.force('collide', d3.forceCollide(nodeRadius))
|
const nextSimulation = updateSimulation(
|
||||||
.force('center', d3.forceCenter(1024/2, 860/2))
|
simulation,
|
||||||
.on('tick', () => {
|
nodes,
|
||||||
// console.log('SIMULATION TICK');
|
links,
|
||||||
this.forceUpdate();
|
nextNodes,
|
||||||
})
|
nextLinks,
|
||||||
.on('end', () => {
|
nodeSize,
|
||||||
// console.log('SIMULATION END');
|
svgSize,
|
||||||
// this.forceUpdate();
|
() => this.forceUpdate(),
|
||||||
|
() => this.forceUpdate()
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
simulation: nextSimulation
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
const n = Math.ceil(
|
||||||
dataNodes,
|
Math.log(
|
||||||
dataLinks,
|
nextSimulation.alphaMin()) / Math.log(
|
||||||
simulation
|
1 - nextSimulation.alphaDecay())) - 200;
|
||||||
};
|
for (var i = 0; i < n; ++i) {
|
||||||
|
nextSimulation.tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNodes(nodeSize) {
|
render() {
|
||||||
return this.state.dataNodes.map((n, index) => (
|
|
||||||
|
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) => (
|
||||||
<GraphNode
|
<GraphNode
|
||||||
key={index}
|
key={index}
|
||||||
data={n}
|
data={n}
|
||||||
@ -101,10 +136,8 @@ class TopologyGraph extends React.Component {
|
|||||||
size={nodeSize}
|
size={nodeSize}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
|
||||||
|
|
||||||
renderLinks(nodeSize) {
|
const renderedLinks = linksData.map((l, index) => (
|
||||||
return this.state.dataLinks.map((l, index) => (
|
|
||||||
<GraphLink
|
<GraphLink
|
||||||
key={index}
|
key={index}
|
||||||
data={l}
|
data={l}
|
||||||
@ -112,20 +145,14 @@ class TopologyGraph extends React.Component {
|
|||||||
nodeSize={nodeSize}
|
nodeSize={nodeSize}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
nodeSize
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSvg>
|
<StyledSvg>
|
||||||
<g>
|
<g>
|
||||||
{this.renderNodes(nodeSize)}
|
{renderedNodes}
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
{this.renderLinks(nodeSize)}
|
{renderedLinks}
|
||||||
</g>
|
</g>
|
||||||
</StyledSvg>
|
</StyledSvg>
|
||||||
);
|
);
|
||||||
@ -136,10 +163,6 @@ TopologyGraph.propTypes = {
|
|||||||
data: React.PropTypes.shape({
|
data: React.PropTypes.shape({
|
||||||
nodes: React.PropTypes.array,
|
nodes: React.PropTypes.array,
|
||||||
links: React.PropTypes.array
|
links: React.PropTypes.array
|
||||||
}),
|
|
||||||
nodeSize: React.PropTypes.shape({
|
|
||||||
width: React.PropTypes.number,
|
|
||||||
height: React.PropTypes.number
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user