Add draggability to topology graph nodes

This commit is contained in:
JUDIT GRESKOVITS 2017-02-15 10:44:16 +00:00
parent 705aa32e73
commit 14a5433a70
4 changed files with 151 additions and 60 deletions

View File

@ -106,7 +106,7 @@ const GraphLink = ({
const sourceAngle = getAngleFromPoints(source, target); const sourceAngle = getAngleFromPoints(source, target);
const sourcePosition = getPosition(sourceAngle, positions, source); const sourcePosition = getPosition(sourceAngle, positions, source);
const targetAngle = getAngleFromPoints(target, source); const targetAngle = getAngleFromPoints(target, source);
const targetPosition = getPosition(targetAngle, positions, target, true); const targetPosition = getPosition(targetAngle, positions, target); //, true);
const arrowAngle = getAngleFromPoints(sourcePosition, targetPosition); const arrowAngle = getAngleFromPoints(sourcePosition, targetPosition);
return ( return (

View File

@ -45,7 +45,8 @@ const HeartCircle = styled.circle`
const GraphNode = ({ const GraphNode = ({
data, data,
size size,
onDragStart
}) => { }) => {
const { const {
@ -79,9 +80,17 @@ const GraphNode = ({
}; };
// const titleBbBox = {x:100, y: 30 - halfHeight}; // const titleBbBox = {x:100, y: 30 - halfHeight};
const onStart = (evt) => {
evt.preventDefault();
onDragStart(evt, data.id);
};
return ( return (
<g transform={`translate(${data.x}, ${data.y})`}> <g
transform={`translate(${data.x}, ${data.y})`}
onMouseDown={onStart}
onTouchStart={onStart}
>
<StyledShadowRect <StyledShadowRect
x={-halfWidth} x={-halfWidth}
y={3-halfHeight} y={3-halfHeight}
@ -133,7 +142,8 @@ const GraphNode = ({
GraphNode.propTypes = { GraphNode.propTypes = {
data: React.PropTypes.object.isRequired, data: React.PropTypes.object.isRequired,
size: PropTypes.Size, onDragStart: React.PropTypes.func,
size: PropTypes.Size
}; };
module.exports = GraphNode; module.exports = GraphNode;

View File

@ -38,37 +38,39 @@ const createSimulation = (
const nodeRadius = rectRadius(nodeSize); const nodeRadius = rectRadius(nodeSize);
return d3.forceSimulation(mappedNodes) return ({
simulation: d3.forceSimulation(mappedNodes)
.force('link', d3.forceLink(mappedLinks).id(d => d.id)) .force('link', d3.forceLink(mappedLinks).id(d => d.id))
.force('collide', d3.forceCollide(nodeRadius)) .force('collide', d3.forceCollide(nodeRadius))
.force('center', d3.forceCenter(width/2, height/2)) .force('center', d3.forceCenter(width/2, height/2))
.on('tick', onTick) .on('tick', onTick)
.on('end', onEnd); .on('end', onEnd),
nodes: mappedNodes,
links: mappedLinks
});
}; };
// TODO we need to kill the previous simulation // TODO we need to kill the previous simulation
const updateSimulation = ( const updateSimulation = (
simulation, simulation,
nodes,
links,
nextNodes, nextNodes,
nextLinks, nextLinks,
simNodes,
simLinks,
nodeSize, nodeSize,
svgSize, svgSize,
onTick, onTick,
onEnd 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 mappedNodes = nextNodes.map((nextNode, index) => {
const node = nodes.reduce((acc, n, i) => const simNode = simNodes.reduce((acc, n, i) => {
nextNode.id === n.id ? n : acc ? null : acc); return nextNode.id === n.id ? n : acc;
return node ? { }, null);
id: node.id,
fx: node.x, return simNode ? {
fy: node.y, id: simNode.id,
// fx: simNode.x,
// fy: simNode.y,
index: index index: index
} : { } : {
id: nextNode.id, id: nextNode.id,
@ -77,11 +79,12 @@ const updateSimulation = (
}); });
const mappedLinks = nextLinks.map((nextLink, index) => { const mappedLinks = nextLinks.map((nextLink, index) => {
const link = links.reduce((acc, l, i) => const simLink = simLinks.reduce((acc, l, i) => {
nextLink.source === l.source && nextLink.target === l.target ? return nextLink.source === l.source && nextLink.target === l.target ?
l : acc ? null : acc); l : acc;
return link ? { }, {});
...link return simLink ? {
...simLink
} : { } : {
...nextLink ...nextLink
}; };
@ -94,12 +97,16 @@ const updateSimulation = (
const nodeRadius = rectRadius(nodeSize); const nodeRadius = rectRadius(nodeSize);
return d3.forceSimulation(mappedNodes) return ({
simulation: d3.forceSimulation(mappedNodes)
.force('link', d3.forceLink(mappedLinks).id(d => d.id)) .force('link', d3.forceLink(mappedLinks).id(d => d.id))
.force('collide', d3.forceCollide(nodeRadius)) .force('collide', d3.forceCollide(nodeRadius))
.force('center', d3.forceCenter(width/2, height/2)) .force('center', d3.forceCenter(width/2, height/2))
.on('tick', onTick) .on('tick', onTick)
.on('end', onEnd); .on('end', onEnd),
nodes: mappedNodes,
links: mappedLinks
});
}; };
module.exports = { module.exports = {

View File

@ -29,6 +29,12 @@ const svgSize = {
height: 860 height: 860
}; };
let dragInfo = {
dragging: false,
nodeId: null,
position: null
};
class TopologyGraph extends React.Component { class TopologyGraph extends React.Component {
componentWillMount() { componentWillMount() {
@ -38,15 +44,17 @@ class TopologyGraph extends React.Component {
links links
} = this.props.data; } = this.props.data;
const simulation = createSimulation( const simulationData = createSimulation(
nodes, nodes,
links, links,
nodeSize, nodeSize,
svgSize, svgSize//,
() => this.forceUpdate(), //() => this.forceUpdate(),
() => this.forceUpdate() //() => this.forceUpdate()
); );
const simulation = simulationData.simulation;
const n = Math.ceil( const n = Math.ceil(
Math.log( Math.log(
simulation.alphaMin()) / Math.log( simulation.alphaMin()) / Math.log(
@ -55,9 +63,7 @@ class TopologyGraph extends React.Component {
simulation.tick(); simulation.tick();
} }
this.setState({ this.setState(simulationData);
simulation: simulation
});
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -68,33 +74,34 @@ class TopologyGraph extends React.Component {
// otherwise, redo them bitches = by what I mean to update the simulation // otherwise, redo them bitches = by what I mean to update the simulation
// try freezing exisiting ones... then adding another // try freezing exisiting ones... then adding another
const {
nodes: simNodes,
links: simLinks
} = this.state;
const { const {
nodes: nextNodes, nodes: nextNodes,
links: nextLinks links: nextLinks
} = nextProps.data; } = nextProps.data;
const {
nodes,
links
} = this.props.data;
// this is tmp for the compare above // this is tmp for the compare above
if(nextNodes.length !== nodes.length || nextLinks.length !== links.length) { if(nextNodes.length !== simNodes.length ||
nextLinks.length !== simLinks.length) {
const simulation = this.state.simulation; const simulation = this.state.simulation;
const nextSimulation = updateSimulation( const nextSimulationData = updateSimulation(
simulation, simulation,
nodes,
links,
nextNodes, nextNodes,
nextLinks, nextLinks,
simNodes,
simLinks,
nodeSize, nodeSize,
svgSize, svgSize,
() => this.forceUpdate(), () => this.forceUpdate(),
() => this.forceUpdate() () => this.forceUpdate()
); );
this.setState({
simulation: nextSimulation const nextSimulation = nextSimulationData.simulation;
}); // console.log('nextSimulationData.nodes = ', nextSimulationData.nodes);
const n = Math.ceil( const n = Math.ceil(
Math.log( Math.log(
@ -103,6 +110,13 @@ class TopologyGraph extends React.Component {
for (var i = 0; i < n; ++i) { for (var i = 0; i < n; ++i) {
nextSimulation.tick(); nextSimulation.tick();
} }
/*this.state.simulation.nodes().forEach((node, index) => {
delete node.fx;
delete node.fy;
});*/
this.setState(nextSimulationData);
} }
} }
@ -113,27 +127,81 @@ class TopologyGraph extends React.Component {
links links
} = this.props.data; } = this.props.data;
const simulationNodes = this.state.simulation.nodes(); const simulationNodes = this.state.nodes;
const simulationNode = (nodeId) =>
simulationNodes.reduce((acc, simNode, index) => {
return simNode.id === nodeId ? simNode : acc;
}, {});
const nodesData = nodes.map((node, index) => ({ const nodesData = nodes.map((node, index) => ({
...node, ...node,
...simulationNodes.reduce((acc, simNode, index) => ...simulationNode(node.id)
simNode.id === node.id ? simNode : acc)
})); }));
const linksData = links.map((link, index) => ({ const linksData = links.map((link, index) => ({
source: simulationNodes.reduce((acc, simNode, index) => source: simulationNode(link.source),
simNode.id === link.source ? simNode : acc), target: simulationNode(link.target)
target: simulationNodes.reduce((acc, simNode, index) =>
simNode.id === link.target ? simNode : acc)
})); }));
const onDragStart = (evt, nodeId) => {
// it's this node's position that we'll need to update
dragInfo.dragging = true;
dragInfo.nodeId = nodeId;
dragInfo.position = {
x: evt.clientX,
y: evt.clientY
};
};
const onDragMove = (evt) => {
if(dragInfo.dragging) {
const offset = {
x: evt.clientX - dragInfo.position.x,
y: evt.clientY - dragInfo.position.y
};
const dragNodes = simulationNodes.map((simNode, index) => {
if(simNode.id === dragInfo.nodeId) {
return ({
...simNode,
x: simNode.x + offset.x,
y: simNode.y + offset.y,
});
}
return ({
...simNode
});
});
this.setState({
nodes: dragNodes
});
dragInfo.position = {
x: evt.clientX,
y: evt.clientY
};
}
};
const onDragEnd = (evt) => {
dragInfo = {
dragging: false,
nodeId: null,
position: null
};
};
const renderedNodes = nodesData.map((n, index) => ( const renderedNodes = nodesData.map((n, index) => (
<GraphNode <GraphNode
key={index} key={index}
data={n} data={n}
index={index} index={index}
size={nodeSize} size={nodeSize}
onDragStart={onDragStart}
/> />
)); ));
@ -147,7 +215,13 @@ class TopologyGraph extends React.Component {
)); ));
return ( return (
<StyledSvg> <StyledSvg
onMouseMove={onDragMove}
onTouchMove={onDragMove}
onMouseUp={onDragEnd}
onTouchEnd={onDragEnd}
onTouchCancel={onDragEnd}
>
<g> <g>
{renderedNodes} {renderedNodes}
</g> </g>