I've been trying to make a draggable d3 force layout in React for a while now. React has to be able to interact with the nodes in the graph. For example, when you click on a node, React should be able to return the node's id onClick.
I made 4 ponents according to one of Shirley Wu's examples. An App ponent that holds the graph data in it's state and renders the Graph ponent. The graph ponent renders a Node and a Link ponent. This way, the clickable nodes part worked out.
When the page renders, the nodes will be draggable only for a few seconds though. Immediately after rendering the page you can drag nodes, then suddenly, the node being dragged stops in one position pletely. At this point the other nodes cannot be dragged anymore either. I expected to be able to drag the nodes at all times.
I could find a few hints online about creating a canvas behind the graph, setting fill and pointer-events. There are also many discussions about letting or d3 or React do the rendering and calculations. I tried playing with all of React's lifecycle methods, but I can't get it to work.
You can find a live sample over here:
Remember, the circles will be clickable only for a few seconds. Then they'll stay put in the same place. The behavior is the same in all browsers and after every page refresh. When you log the drag function, you'll see that it does assign new coordinates when dragging, the circle won't be displayed in it's new position though.
I'm very eager to learn about the cause of this problem and it would be very cool if you could maybe even propose a solution.
App.js
class App extends React.Component {
constructor(props){
super(props)
this.state = {
data : {"nodes":
[
{"name": "fruit", "id": 1},
{"name": "apple", "id": 2},
{"name": "orange", "id": 3},
{"name": "banana", "id": 4}
],
"links":
[
{"source": 1, "target": 2},
{"source": 1, "target": 3}
]
}
}
}
render() {
return (
<div className="graphContainer">
<Graph data={this.state.data} />
</div>
)
}
}
class Graph extends React.Component {
ponentDidMount() {
this.d3Graph = d3.select(ReactDOM.findDOMNode(this));
var force = d3.forceSimulation(this.props.data.nodes);
force.on('tick', () => {
force
.force("charge", d3.forceManyBody().strength(-50))
.force("link", d3.forceLink(this.props.data.links).distance(90))
.force("center", d3.forceCenter().x(width / 2).y(height / 2))
.force("collide", d3.forceCollide([5]).iterations([5]))
const node = d3.selectAll('g')
.call(drag)
this.d3Graph.call(updateGraph)
});
}
render() {
var nodes = this.props.data.nodes.map( (node) => {
return (
<Node
data={node}
name={node.name}
key={node.id}
/>);
});
var links = this.props.data.links.map( (link,i) => {
return (
<Link
key={link.target+i}
data={link}
/>);
});
return (
<svg className="graph" width={width} height={height}>
<g>
{nodes}
</g>
<g>
{links}
</g>
</svg>
);
}
}
Node.js
class Node extends React.Component {
ponentDidMount() {
this.d3Node = d3.select(ReactDOM.findDOMNode(this))
.datum(this.props.data)
.call(enterNode)
}
ponentDidUpdate() {
this.d3Node.datum(this.props.data)
.call(updateNode)
}
handle(e){
console.log(this.props.data.id + ' been clicked')
}
render() {
return (
<g className='node'>
<circle ref="dragMe" onClick={this.handle.bind(this)}/>
<text>{this.props.data.name}</text>
</g>
);
}
}
Link.js
class Link extends React.Component {
ponentDidMount() {
this.d3Link = d3.select(ReactDOM.findDOMNode(this))
.datum(this.props.data)
.call(enterLink);
}
ponentDidUpdate() {
this.d3Link.datum(this.props.data)
.call(updateLink);
}
render() {
return (
<line className='link' />
);
}
}
D3Functions.js
const width = 1080;
const height = 250;
const color = d3.scaleOrdinal(d3.schemeCategory10);
const force = d3.forceSimulation();
const drag = () => {
d3.selectAll('g')
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragging)
.on("end", dragEnded));
};
function dragStarted(d) {
if (!d3.event.active) force.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
}
function dragging(d) {
d.fx = d3.event.x
d.fy = d3.event.y
}
function dragEnded(d) {
if (!d3.event.active) force.alphaTarget(0)
d.fx = null
d.fy = null
}
const enterNode = (selection) => {
selection.select('circle')
.attr("r", 30)
.style("fill", function(d) { return color(d.name) })
selection.select('text')
.attr("dy", ".35em")
.style("transform", "translateX(-50%,-50%")
};
const updateNode = (selection) => {
selection.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")")
};
const enterLink = (selection) => {
selection.attr("stroke-width", 2)
.style("stroke","yellow")
.style("opacity",".2")
};
const updateLink = (selection) => {
selection
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
};
const updateGraph = (selection) => {
selection.selectAll('.node')
.call(updateNode)
.call(drag);
selection.selectAll('.link')
.call(updateLink);
};
I've been trying to make a draggable d3 force layout in React for a while now. React has to be able to interact with the nodes in the graph. For example, when you click on a node, React should be able to return the node's id onClick.
I made 4 ponents according to one of Shirley Wu's examples. An App ponent that holds the graph data in it's state and renders the Graph ponent. The graph ponent renders a Node and a Link ponent. This way, the clickable nodes part worked out.
When the page renders, the nodes will be draggable only for a few seconds though. Immediately after rendering the page you can drag nodes, then suddenly, the node being dragged stops in one position pletely. At this point the other nodes cannot be dragged anymore either. I expected to be able to drag the nodes at all times.
I could find a few hints online about creating a canvas behind the graph, setting fill and pointer-events. There are also many discussions about letting or d3 or React do the rendering and calculations. I tried playing with all of React's lifecycle methods, but I can't get it to work.
You can find a live sample over here: https://codepen.io/vialito/pen/WMKwEr
Remember, the circles will be clickable only for a few seconds. Then they'll stay put in the same place. The behavior is the same in all browsers and after every page refresh. When you log the drag function, you'll see that it does assign new coordinates when dragging, the circle won't be displayed in it's new position though.
I'm very eager to learn about the cause of this problem and it would be very cool if you could maybe even propose a solution.
App.js
class App extends React.Component {
constructor(props){
super(props)
this.state = {
data : {"nodes":
[
{"name": "fruit", "id": 1},
{"name": "apple", "id": 2},
{"name": "orange", "id": 3},
{"name": "banana", "id": 4}
],
"links":
[
{"source": 1, "target": 2},
{"source": 1, "target": 3}
]
}
}
}
render() {
return (
<div className="graphContainer">
<Graph data={this.state.data} />
</div>
)
}
}
class Graph extends React.Component {
ponentDidMount() {
this.d3Graph = d3.select(ReactDOM.findDOMNode(this));
var force = d3.forceSimulation(this.props.data.nodes);
force.on('tick', () => {
force
.force("charge", d3.forceManyBody().strength(-50))
.force("link", d3.forceLink(this.props.data.links).distance(90))
.force("center", d3.forceCenter().x(width / 2).y(height / 2))
.force("collide", d3.forceCollide([5]).iterations([5]))
const node = d3.selectAll('g')
.call(drag)
this.d3Graph.call(updateGraph)
});
}
render() {
var nodes = this.props.data.nodes.map( (node) => {
return (
<Node
data={node}
name={node.name}
key={node.id}
/>);
});
var links = this.props.data.links.map( (link,i) => {
return (
<Link
key={link.target+i}
data={link}
/>);
});
return (
<svg className="graph" width={width} height={height}>
<g>
{nodes}
</g>
<g>
{links}
</g>
</svg>
);
}
}
Node.js
class Node extends React.Component {
ponentDidMount() {
this.d3Node = d3.select(ReactDOM.findDOMNode(this))
.datum(this.props.data)
.call(enterNode)
}
ponentDidUpdate() {
this.d3Node.datum(this.props.data)
.call(updateNode)
}
handle(e){
console.log(this.props.data.id + ' been clicked')
}
render() {
return (
<g className='node'>
<circle ref="dragMe" onClick={this.handle.bind(this)}/>
<text>{this.props.data.name}</text>
</g>
);
}
}
Link.js
class Link extends React.Component {
ponentDidMount() {
this.d3Link = d3.select(ReactDOM.findDOMNode(this))
.datum(this.props.data)
.call(enterLink);
}
ponentDidUpdate() {
this.d3Link.datum(this.props.data)
.call(updateLink);
}
render() {
return (
<line className='link' />
);
}
}
D3Functions.js
const width = 1080;
const height = 250;
const color = d3.scaleOrdinal(d3.schemeCategory10);
const force = d3.forceSimulation();
const drag = () => {
d3.selectAll('g')
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragging)
.on("end", dragEnded));
};
function dragStarted(d) {
if (!d3.event.active) force.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
}
function dragging(d) {
d.fx = d3.event.x
d.fy = d3.event.y
}
function dragEnded(d) {
if (!d3.event.active) force.alphaTarget(0)
d.fx = null
d.fy = null
}
const enterNode = (selection) => {
selection.select('circle')
.attr("r", 30)
.style("fill", function(d) { return color(d.name) })
selection.select('text')
.attr("dy", ".35em")
.style("transform", "translateX(-50%,-50%")
};
const updateNode = (selection) => {
selection.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")")
};
const enterLink = (selection) => {
selection.attr("stroke-width", 2)
.style("stroke","yellow")
.style("opacity",".2")
};
const updateLink = (selection) => {
selection
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
};
const updateGraph = (selection) => {
selection.selectAll('.node')
.call(updateNode)
.call(drag);
selection.selectAll('.link')
.call(updateLink);
};
Share
Improve this question
edited Feb 25, 2018 at 8:51
Vialito
asked Feb 23, 2018 at 5:01
VialitoVialito
5238 silver badges29 bronze badges
1
-
1
I took a look at your question, it seems like the simulation is not started again after it's initial rendering. What makes it a bit plicated to debug is that you're rendering DOM elements with both React and D3. Your on
tick
function also always sets the simulation forces over and over again (you could do that once, when you initialise the force). – pasql Commented Feb 25, 2018 at 11:32
1 Answer
Reset to default 7 +100You define force simulation twice in your code. First time - string 7 in your codepen and second time - string 113. Your dragStarted
and dragEnded
functions (which are defined globally) use force simulation from string 7, but it not specified (you did not pass nodes, links and other params to it).
You should move these function into the method when you define and specify your force simulation so ponentDidMount
method for Graph
ponent should look like this (you should also rewrite your tick
handler function, and set force params only once (now you do it on each tick), check my fork of your pen):
ponentDidMount() {
this.d3Graph = d3.select(ReactDOM.findDOMNode(this));
var force = d3.forceSimulation(this.props.data.nodes)
.force("charge", d3.forceManyBody().strength(-50))
.force("link", d3.forceLink(this.props.data.links).distance(90))
.force("center", d3.forceCenter().x(width / 2).y(height / 2))
.force("collide", d3.forceCollide([5]).iterations([5]))
function dragStarted(d) {
if (!d3.event.active) force.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
}
function dragging(d) {
d.fx = d3.event.x
d.fy = d3.event.y
}
function dragEnded(d) {
if (!d3.event.active) force.alphaTarget(0)
d.fx = null
d.fy = null
}
const node = d3.selectAll('g.node')
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragging)
.on("end", dragEnded)
);
force.on('tick', () => {
this.d3Graph.call(updateGraph)
});
}