A d3 Basic Chord Diagram Template

Chord Diagram

Prepare your data as a CSV file

A chord diagram represents a relationship between two datasets. The way this template is designed, we established a format that works with data in the exactly following format:

Data for chord diagram

The headers across the top of the spreadsheet will be the source data in the diagram, and will be displayed along the left side of the circle. The data in the first column of the spreadsheet will be the target data in the digram, and will be listed at the right side of the circle.

Because D3 brings in data as rows and doesn’t discern any column from another, it’s important to tell D3 the name of the first column in the code below. See the comments in the code.

NOTE: This example uses nutritiondata.csv, which needs to be in the same folder as your .html file. This project will only work when launched from a web server.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Chord Diagram</title>
    <style type="text/css">

    path{
        stroke: black;
        stroke-width: .25px;
    }
    path.fade{
        display: none;
    }
    </style>
</head>
<body>

<script src="http://d3js.org/d3.v5.min.js"></script>
<script>

var margin      = {top: 10, right: 10, bottom: 10, left: 10},
    width       = 600 - margin.left - margin.right,
    height      = 600 - margin.top  - margin.bottom,
    innerRadius = Math.min(width, height) * .35, //35% of smallest measurement
    outerRadius = innerRadius * 1.1; //110% of innerradius

var svg = d3.select("body").append("svg")
    .attr("width",  width  + margin.left + margin.right)
    .attr("height", height + margin.top  + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .append("g")
    .attr("class", "chordgraph")
    .attr("transform", "translate(" + width/2 + "," + height/2 + ")");

d3.csv("nutritiondata.csv").then(function(d){

    /*
     * IMPORTANT! Specify your first column of data here (see example data)
     *
     */
    var firstColumn = "first_column";

    //store column names
    var fc = d.map(function(d){ return d[firstColumn]; }),
        fo = fc.slice(0),
        maxtrix_size = (Object.keys(d[0]).length - 1) + fc.length,
        matrix  = [];

    //Create an empty square matrix of zero placeholders, the size of the data
    for(var i=0; i < maxtrix_size; i++){
        matrix.push(new Array(maxtrix_size+1).join('0').split('').map(parseFloat));
    }

    //go through the data and convert all to numbers except "first_column"
    for(var i=0; i < d.length; i++){

        var j = d.length;//counter

        for(var prop in d[i]){
            if(prop != firstColumn){
                fc.push(prop);
                matrix[i][j] = +d[i][prop];
                matrix[j][i] = +d[i][prop];
                j++;
            }
        }
    }
  
    //set color scale. More color options: https://github.com/d3/d3-scale-chromatic
    var color = d3.scaleOrdinal(d3.schemeCategory10)
  
    //d3 chord generator
    var chord = d3.chord()
        .padAngle(0.01)
        .sortSubgroups(d3.descending);
  
    //apply the matrix
    var chords = chord(matrix);
  
    //each ribbon generator
    var ribbon = d3.ribbon()
        .radius(innerRadius);
  
    //outer rim arc
    var arc = d3.arc()
        .innerRadius(innerRadius)
        .outerRadius(innerRadius + 20);

    //add each of the groupings for outer rim arcs
    var group = svg.append("g")
        .selectAll("g")
        .data(chords.groups)
        .enter()
        .append("g");
  
    //add each outer rim arc path
    group.append("path")
        .attr("fill", function(d){ return (d.index+1) > fo.length ? color(d.index): "#ccc"; })
        .attr("stroke", function(d){ return color(d.index); })
        .attr("d", arc)
        .style("cursor", "pointer")
        .on("mouseover", function(d, i){
            ribbons.classed("fade", function(d){
                return d.source.index != i && d.target.index != i;
            });
        });
  
    //add each ribbon
    var ribbons = svg.append("g")
      .attr("fill-opacity", 0.67)
      .selectAll("path")
      .data(chords)
      .enter()
      .append("path")
      .attr("d", ribbon)
      .attr("fill", function(d){ return color(d.target.index); })
      .attr("stroke", function(d){ return d3.rgb(color(d.target.index)).darker(); });
  
    //add the text labels
    group.append("text")
      .each(function(d){ return d.angle = (d.startAngle + d.endAngle) /2; })
      .attr("dy", ".35em")
      .attr("class", "text")
      .style("pointer-events","none")
      .attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : "start"; })
      .attr("transform", function(d,i){
          console.log(fc[i], d);
          //rotate each label around the circle           
          return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" + 
                 "translate(" + (outerRadius + 10) + ")" +
                 (d.angle > Math.PI ? "rotate(180)" : "");

      })
      .text(function(d,i){
          //set the text content
          return fc[i];
      })
      .style("font-family","sans-serif")
      .style("font-size","10px");
});
</script>
</body>
</html>

Background info on this type of chord diagram

Chord diagrams typically use a square matrix to show relationships between two sets of data. Using a square matrix is technically possible to also specify different quantities depending on the direction of the data in a square matrix. However, square matrices are difficult for many people to understand, and also can be difficult to visually discern in a graph like a chord diagram.

More information about the structure of a Chord Diagram can be found here. This is useful to understanding the nature of how this type of graphic works.

In this template, I decided to create a boilerplate matrix that falls under very specific constraints:

  1. Each chord has only one quantity associated with it.
  2. The data isn’t a true many-to-many relationship. Instead, half of the circle is source data, and the other half is target data.

If our data was a 3x3 relationship (ABC -> DEF), the matrix would look like this in chart form:

  A B C D E F
A 0 0 0 10 20 30
B 0 0 0 40 50 60
C 0 0 0 70 80 90
D 10 40 70 0 0 0
E 20
50 80 0 0 0
F 30 60 90 0 0 0

For example, notice that the value for an A -> E relationship is the same value as E -> A in the chart above.

This deviation from the norm is intentional, and only specific to the template we setup here. Most typical chord diagrams have varying quantities depending on the direction of the relationship.

As a two-dimensional array, we can represent the above values in the following manner:

var matrix = [
    [0,   0,  0,  10,  20,  30],
    [0,   0,  0,  40,  50,  60],
    [0,   0,  0,  70,  80,  90],
    [10, 40, 70,   0,   0,   0],
    [20, 50, 80,   0,   0,   0],
    [30, 60, 90,   0,   0,   0]
];

It would be possible to modify the code to provide your own matrix instead of using a CSV. However, you would need to remove the for loops which convert the .csv data to this format of matrix.