# CS465 - Lab 9 Chord diagram layout

# Objectives

  • Get more practice working with D3 layout and shape tools

# Starting the lab

Go to Github Classroom and grab the starter code for the lab.

# Building a chord diagram

In class, I showed you a chord diagram as a technique for visualizing small graphs. Today, you will build one.

While the visualization type itself will be new to you, this lab will mostly serve to reenforce the patterns that we have been seeing before.

# "Chording" our data

When you open the starter code, you will see that this week I didn't give you an actual data set, we are just working with an arbitrary weighted and directed graph.

The data is stored in an adjacency matrix. As you hopefully recall, each cell in the matrix represents a directed link between the variables placed on the corresponding row and column for the cell. The value in the cell is the weight of the edge from column variable to row variable.

I've given you some code that puts all of the values from the adjacency matrix into a table, and allows us to edit the values (you will see that changing values in the table calls the update function). You don't have to worry about this.

We need to transform the matrix data into a form that we can work with. For this, we will use d3.chord. This follows the same pattern we saw with d3.hierarchy. We call d3.chord() to create the layout function, then we call some additional functions on it to configure the behavior. This returns a function which we can then use on our data. In this case, the returned data will be an array of objects with two properties: 'source' and 'target'. Each one represents one edge in the graph. Each endpoint is a property of the following form (from the documentation):

  • startAngle - the start angle in radians
  • endAngle - the end angle in radians
  • value - the flow value matrix[i][j]
  • index - the node index i
  • subindex - the node index j

The important part of this for us is the startAngle and endAngle. This tells us the portion of the circle that this end of the chord occupies. The chord layout has taken all of the weights and figured out which percentage of the overall circumference of the circle this particular weight should occupy. It has also figured out how to arrange the chords around the circle. So, the interval between startAngle and endAngle belongs to this end of the chord.

The returned data also includes a property called groups. This is an array of the primary "groups" (variables) of the data. So we will have one of these for every row of the matrix. These have this form (again from the documentation):

  • startAngle - the start angle in radians
  • endAngle - the end angle in radians
  • value - the total outgoing flow value for node i
  • index - the node index i

This provides us with the end points of the chords. Each variable is allocated startAngle to endAngle of the circle based on the combined weight of all of its flows.

Here is the process:

Step one Create the layout generator function. Find the place in the code that says "initialize layout generators here". Call d3.chord() and configure it with .sortSubgroups(d3.descending) and .padAngle(0.05);. Store the result in a variable called chord_function. Sorting the subgroups just orders the chords within each arc segment by their weights. The padding angle gives us some space between the outer arcs so we can better differentiate between the different variables.

Step two Call the new function on the adjacency_matrix. Do this inside of the update_chords function at the top and store the result in a variable called chord_data.

Step three Print this out and take a look at the data.

# Drawing arcs

Time to actually draw something. We will start with the arcs around the periphery of the chart. To help us out with this, we will use one of D3's shape tools: d3.arc. Like the other shapes that we have looked at, the arc tool will generate drawing instructions to create arcs from paths. Yes, it can be used to make pie charts and donut plots. In truth, we are essentially creating a donut plot right now.

To draw an arc, the shape tool needs four pieces of information:

  • the start angle - where to start the arc on the circle
  • the end angle - where to end the arc around the circle
  • the inner radius - how far out from the center of the circle to place the inner edge of the arc
  • the outer radius - how far out from the center to place the outer edge

It will look at the data passed to it for these in the properties startAngle, endAngle, innerRadius and outerRadius. We can also set defaults for the generator to be used when those properties are missing. Conveniently, our data has two of those properties...

Let's draw the arcs. You will see that I have already given you a variable called vis which is a selection of the SVG element. I've also added a transformation so that the origin is in the middle of the SVG, which will make drawing the circle easier. In addition, I've given you two constants: INNER_RADIUS, and OUTER_RADIUS, whose purpose should be clear.

Step one Create the arc function. Call d3.arc() to create the function. Then configure it with .innerRadius(INNER_RADIUS) and .outerRadius(OUTER_RADIUS). Do this right after the definition of the chord_function and call this one arc_function.

Step two Create the arcs. Do the standard enter/exit pattern in the update_chords function. The objects you will create are paths and you will bind them to chord_data.groups (we want one arc per variable). For the enter selection, append a "path", provide it with a class name and set the fill color. I've given you an array of colors, so your fill color can just be (d)=>COLORS[d.index].

Step three Merge the enter and update selections.

Step four Set the "d" attribute for the path for the merged set. For the value, just pass it arc_function -- this will take the object bound to the path and generate the drawing instructions. The reason to set this on the merged set is that we want this to respond to changes in the matrix. When the values in the matrix change, the weight of each variable will also change and we will need to recalculate the arcs.

Step five Try it out. You should see a donut plot. When you change the values in the matrix, it should update the arcs.

# Draw the chords

Drawing the chords will follow much the same pattern. To draw the chords, we will use d3.ribbon. This is a shape generator made specifically for drawing the objects generated by the chord function. As such, it expects an object with properties source and target, where each of those has a startAngle and endAngle. The only other thing it needs to draw the ribbons is the radius (how far out from the center to end the ribbons). This can be included on the objects or configured with a default.

Step one Create the ribbon generator. Call d3.ribbon() to create the generator function. We will configure it with .radius(INNER_RADIUS). Put this right after the arc_function and call it ribbon_function.

Step two Create the ribbons. This follows exactly the same pattern as the arcs. Do the data binding and handle the exit selection. For the enter selection, append a "path" and color it in the same way. For the color, set it based on the target.index (if you want to get fancy, you could set it to whichever side has the larger flow, though the sorting has already done a measure of this). To make it easier to read, I suggest setting fill-opacity to something less than 1 and setting the stroke to black. Then merge the enter and update selections, and set the "d" property using the ribbon_function

Step three Test away.

# Improve the interaction

Our simple chord diagram isn't too hard to read, but it can get more difficult as we add more variables. A standard technique is to allow the user to click on the arc segments to show just the flows into or out of that node.

To support this, you are going to add an event handler to the arcs.

Step one Create a new variable. We want to keep track of what is current selected (if anything). Create a variable called current_index up under the constants at the top of the script and set it to null (no selection).

Step two Add a click event handler to the arcs. You should do this to the enter selection for the arcs since it only ever has to be done once.

Step three Set current_index. Write a conditional that sets current_index to the index of the current arc. If current_index was already this arc's index, set it to null (i.e., clearing the selection).

Step four Conditionally set the visibility based on the selection. We are going to use a different technique to show selection this time. Select all of the chords set their visibility style. The visibility style can be "visible" or "hidden". For your accessor function, return "hidden" if the current_index isn't null and the index doesn't match, otherwise return "visible".

Step five Test, test, test.

Step six? A lot of the values in here may seem a bit magic. Try playing around with the radii and other configurations to get a better feel for how they work. (when you hand it in, make sure to set the values back or note what you changed so I don't just think you did it wrong...)

Last Updated: 11/19/2018, 1:01:50 PM