Chord Diagrams in D3
Chord diagrams are a graphical method of displaying relationships within a square matrix. Usually, the matrix shows a transfer from one state to another state.
Chord Chart Output
Data Table
This data is randomly generated by the script.
In our example data set, items started in State 1 and went to State 3.
Along the diagonal, we see values that stayed in a single state. In our example data set, items started in State 2 and remained in State 2.
The Code
First, our data matrix must be chord-ified. Fortunately, D3 makes this quite easy with the d3.chord
function.
// Chord-ify the data set.
let chord = d3.chord().padAngle(deg2rad(1));
let chords = chord(data);
Let’s define our svg
object.
let svg = d3.select("#canvas");
svg.attr("width", width)
.attr("height", height)
.attr("font-size", fontSize)
.attr("font-family", fontFamily);
The d3.chord
, d3.arc
, and d3.ribbon
functions will create graphical elements centered at the point (0, 0). We need to apply a transform so graphic will appear in the middle of our SVG.
// Define the view window for the chort chart.
let gView = svg
.append("g")
.classed("view", true)
.attr("transform", `translate(${width / 2}, ${height / 2})`);
We have two primary groups to concern ourself with. We have arcs and we have ribbons.
// Create a wrapping group for the chord groups (arcs).
let gGroups = gView
.selectAll("g.group")
.data(chords.groups)
.join("g")
.classed("group", true);
// Create a wrapping group for the chord groups.
let gChords = gView
.selectAll("g.chord")
.data(chords)
.join("g")
.classed("chord", true);
We use our arc generator function to draw the arcs.
// Generator function for the outer arcs.
let arc = d3
.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
// Create a path, using the arc generator function, for each
// group in the data set.
gGroups
.append("path")
.attr("fill", d => color(d.index, numCategories))
.attr("stroke", d => d3.rgb(color(d.index, numCategories)).darker())
.attr("stroke-width", 1)
.attr("d", arc)
.append("title")
.text(d => label(d.index));
Let’s add some tick marks to the arcs.
/**
* Function to generate the tick marks for a single arc.
*
* @param {object} data
* @param {number} step
*/
function ticks(data, step) {
let k = (data.endAngle - data.startAngle) / data.value;
return d3.range(0, data.value, step).map(x => {
return {
value: x,
angle: x * k + data.startAngle
};
});
}
// Create a group for each small tick mark.
let gTicks = gGroups
.selectAll("g.tick")
.data(d => ticks(d, smallTick))
.join("g")
.classed("tick", true)
.attr("transform", d => `rotate(${rad2deg(d.angle) - 90}) translate(${outerRadius}, 0)`);
// Create a tick for each tick mark.
gTicks
.append("line")
.attr("x1", 0)
.attr("x2", tickSize)
.attr("stroke", "black")
.attr("stroke-width", 1);
// Create a text element for large tick mark.
gTicks
.append("text")
.filter(d => d.value % largeTick === 0)
.attr("x", tickSize + 2)
.attr("dy", "0.35em")
.attr("transform", d => (d.angle < Math.PI ? "rotate(0) translate(0)" : "rotate(180) translate(-16, 0)"))
.attr("text-anchor", d => (d.angle < Math.PI ? "start" : "end"))
.text(d => format(d.value));
Finally, let’s draw the ribbons.
// Generator function for the inner chords.
let ribbon = d3.ribbon().radius(innerRadius);
// Create a path, using the ribbon generator function, for each
// path in the data set.
gChords
.append("path")
.attr("d", ribbon)
.attr("fill", d => color(d.target.index, numCategories))
.attr("opacity", 0.5)
.attr("stroke", d => d3.rgb(color(d.target.index, numCategories)).darker())
.append("title")
.text(d => `${label(d.source.index)} ${arrow} ${label(d.target.index)}`);
The complete code for this example is available in Github.