Become a member
to unlock all features

Level Up!

Access all courses & lessons on egghead today and lock-in your price for life.


    Creating a D3 Force Layout in React

    Josh BlackJosh Black

    Learn how to leverage d3's layout module to create a Force Layout inside of React. We'll take a look at React's lifecycle methods, using them to bootstrap d3's force layout in order to render our visualization.



    Become a Member to view code

    You must be a Member to view code

    Access all courses and lessons, track your progress, gain confidence and expertise.

    Become a Member
    and unlock code for this lesson




    We're going to recreate this force-directed graph layout here inside of React using D3. You can see in the example that they're just using JavaScript and plain D3 here to render out that visualization up above, but what we're going to do is actually grab the data and render it inside of this JSBin file that I have set up.

    Here, I just have a div with an ID of root that our React application is going to render into. I'm also including React, ReactDOM, and D3. Finally, here at the bottom, I have a variable called data, which has two keys, nodes and links.

    Nodes are just characters that appear in the movie. They have a name and a group that they're associated with. Then, links correspond to when two characters actually appear in a scene together.

    There's a source value and a target value, which correspond to the actual character. Then, the value here for a link actually corresponds to when two characters appear in a scene together.

    Now that we have our HTML set up, let's go ahead and create our first React component. Here, our React component's going to be called forceLayout, and it's going to extend from the React.component base class. Our render method is just going to initially grab the width and the height off of the props that are passed into it.

    We'll also make a style object that's just going to hold on to the width and height information and also include a border, which is just an off-black value.

    What render is going to return is actually a single DOM node that's a div, and it's going to have that style object on it. It's actually going to contain a ref called mountPoint, which D3 is going to use to render into later on.

    This component is a container component, which means that this div is going to contain all of the rendering done by D3, which we'll manage in the componentDidMount lifecycle method.

    Now, we can go ahead and actually leverage the render method on ReactDOM to render out our force layout application to the DOM.

    We'll just go and initialize the force layout component with the width and the height prop, and we'll actually use document.querySelector to target that root node that I made earlier inside of our HTML for our React application to render into.

    If we go and check this out in the output, we can see that our container component was rendered properly, so we're good to go to jump into the componentDidMount lifecycle method.

    ComponentDidMount is a lifecycle method that means that our DOM node has been placed inside of our DOM so we can actually work with it. More importantly, we can use D3 to hook into that mount point to actually render.

    We'll start out by grabbing width and height, once again, off of props. Then we're going to use the D3 force layout to actually drive the algorithmic behavior of our visualization. Just to give you an idea of what the force layout module actually gives us, I'll go ahead and log out the keys here.

    We can see that the force layout module really is just a collection of configuration options, like link distance or friction. We can also call things like drag, which will implement that node drag behavior. We can even listen to events using the on method, as well, like onTick.

    Now that we know that this layout can be configured, we'll go ahead and copy the properties from that example from before like charge and link distance. We'll also go ahead and add the size attribute for it, which will be based off of the props of our component. Finally, we'll include the nodes and the links from our data variable.

    Data.nodes here is just a collection, once again, of all the characters that we have. If we go and log it out to the console, we can see that each object here just has a name and a group. Links are similar in that they have the source value, a target value, and then an actual value.

    We can actually bootstrap our force layout by calling force.start. What's important is that we specify some kind of handler for the tick event. What the tick event allows us to do is hook into whenever the layout wants to update itself.

    If I go and log out the current time whenever the tick event is called and we can see it out here in the console, you can see that it's called almost 60 times a second, which is the ideal frame rate for our animation here inside of our visualization.

    What's even more important to realize is that our nodes and our links are also changing over time. Here, I'll go and log out just the first node in our entry of nodes. You can see that we have an X- and a Y-value changing, as well as a previous X- and a previous Y-value changing.

    Links are the same way, except they have a source and a target node, which also have the X- and the Y-values changing. They also have their consistent value that's present in every single link.

    Now we can go ahead and start creating our SVG container that D3 is going to render into. We can use D3 select method and pass in our mountPoint ref. It's actually going to go in and target the DOM node that our component is wrapping.

    We can go ahead and append an SVG element inside of this div and apply a couple of attributes such as width and height.

    If we go and check out what this looks like in our output, we can see that our container node is being rendered, but if we go and inspect inside of there, our SVG element is also being rendered inside of div. This SVG element is actually managed by D3, and we can use it to render our nodes and links.

    Now, let's go ahead and get started on our nodes. Nodes are going to be represented as circle SVG elements, and they're going to correspond to each of our nodes that are present in that data object.

    We can use D3's enter method and, for each node, actually append an SVG element of the type circle. For each circle, we're going to specify an attribute called radius. We're also going to specify some style attributes like stroke and stroke width.

    We also want a dynamic fill style for each node that's going to be based off of its group. This callback function here for fill will actually take a specific data node. Just to give an idea of what that looks like, let's go ahead and log out just one node from our data.nodes object.

    What this is going to look like, once again, is the object has an X- and Y-value, and it also has a unique group identifier, which is just a number. We can actually use one of D3's ordinal scales called category 20, which his just going to generate a bunch of colors, 20 specifically, that are going to correspond to some kind of entity that we have defined in our data.

    We can go and define that color function using D3.scale.category20. For each data element in this callback, we can just call it d, and we can access its group by calling d.group. Finally, we can actually get the color for the fill by calling the color function and passing in the group as the parameter.

    When we go and actually render what we have here to the output, we can see that we have these nodes that are all stuck up in the corner, and that's because we haven't applied any positioning to them yet. We can apply the positioning inside of this tick method.

    These circles are just going to take two attributes, cx and cy, and they're going to be based off of each data point's X-value and each data point's Y-value. We're going to update this on each tick event. If we go and render this in the output, we can see that all of our nodes are actually being rendered now, since we're updating their cx and their cy values.

    We can actually go and render our links in a similar way, but instead of using circles, we're going to use line SVG elements.

    For each link in our data.links array, we're just going to append a single line and give it some style attributes such as stroke. We're also going to give it a stroke opacity, and then, finally, we're going to give it a dynamic stroke width value. This is just going to be based off of the value of that specific link.

    Now, inside of our tick event, we can go and update the line positioning the same way we did for node, but instead of having an X- and a Y-value, each individual link actually has two objects, a source and a target object. Each one individually has an X and Y positioning.

    We can go and actually leverage source and target to actually position out our line SVG elements.

    Let's go ahead and work inside of that tick method, and for each link, we're going to update the X1 attribute. It's going to correspond to the specific data point source, the X-value. We'll also update the Y1 positioning in the same way, except we'll use the Y property on the source value.

    Then, for X2 and Y2, we'll do something similar, but instead of using source, we're just going to refer to target.

    Now that we have the positioning of our links being specified, when we go and run this and see in the output, we can see that the lines and the circle SVG elements are being rendered. They correspond to the nodes and the links that are specified on our data object.

    However, our drag behavior doesn't work like it did in that example that I showed at the beginning of the video, so let's go ahead and change that.

    Thankfully, that force layout that we had before actually has a drag method. For the nodes, all we have to do is call .call on the node object here and just pass in force.drag. This is going to hook in all the different event listeners that we need to for the drag behavior to actually work. We go and run that again, and the drag behavior works exactly as we expect.