# Arc diagrams in d3
As this is my first d3 creation, this is in no way meant to be a well-thought-out
tutorial. I came across a part of my dataset that inspired an arc diagram, and had
trouble finding a way to incoorporate gradients,
clickability, and
varying svg arc-widths,
and felt like sharing after figuring it out.
In the graphic below, each letter's node highlights and recolors all emanating edges upon a
click-event.
### Motivations
In this dataset, each subject can qualify for many categories (A - I), and we anticipate that
qualifying for a certain category changes the probability of qualifying for another.
The larger question at hand is which categories show up together at higher rates than expected.
Now an arc diagram arguably isn't
a good way to think about this quantitatively, especially since the total population
for each category (loosely represented by the size of the node) is so variable,
but a visual representation won't hurt. The first half of this post will be dedicated to the
mechanics behind the visual, and the second half to the actual problem at hand.
SVG gradients were in theory a work-around to the usual messiness of arc diagrams, which usually
rely on tuning the transparency or applying a (single) color to the arc. As for the dome-y hemisphere look,
I find chord diagrams (the alternative) difficult to read and analyze, even at peak interactivity.
### Cleaning the data
The original dataset (top) is cleaned using R to its plottable form (bottom).
The number of occurrences of each *pair* of letters are counted and sorted alphabetically.
I will also keep track of the number of solo occurrences of each letter, for later on.
| subject | categories |
| :-----: | :--------- |
| id1 | A,D,F |
| id2 | G |
| id3 | A,E,F,I |
| ••• | ••• |
| pairing | count |
| :-----: | ---------: |
| AB | 2,795 |
| AC | 10,812 |
| ••• | ••• |
| HI | 406 |
The javascript data arrays/objects look like this:
var data = [
{code1: 'A', code2: 'B', count: 2795},
{code1: 'A', code2: 'C', count: 10812},
...
{code1: 'H', code2: 'I', count: 406}
];
var palette = {
"A": "#303359",
"B": "#8B8BAE",
"C": "#6AB3BF",
"D": "#DC996A",
"E": "#A84D60",
"F": "#D8CA26",
"G": "#D6A3B7",
"H": "#77A289",
"I": "#A45A36"
};
### The loopiness of it all
This project unfortunately defaults to `for` loops instead of employing
the elegant scheme of `data()`, `enter()`, and `append()` in d3. The main issue
I faced was indexing the pairwise gradients that needed to be created based on
the beginning and ending node's position (please reach out if you know of a better way!).
### Creating data-based gradients
Let's begin! I'll start with the gradients, as this was what gave me the most trouble.
The idea is to have some sort of "library" of gradients (called `defs`), one for every arc, by attaching a
corresponding `id`. (In this case, the datum index is sufficient in this graphic as the dataset
is never re-ordered, but a key value would be more robust).
I followed
Sirius Strebe's article,
and adjusted the offsets of the "start" and "end" points of the gradients (from 0-100% to 20-80%).
This results in a gradient that remains a solid color for longer on both ends. The (x1, y1) and (x2, y2)
are set to (0%, 100%) and (100%, 100%), to create a gradient that shades only in the x-direction (i.e., stays 100%
the color it is set to in the y-axis). Finally, `stop-colors` are set to each arc's `code1` and `code2`
values (the two end nodes).
var defs = svg.append("defs");
for (i = 0; i < data.length; i++) {
var d = data[i];
var gradient = defs.append("linearGradient")
.attr("id", "svgGradient" + i)
.attr("x1", "0%")
.attr("x2", "100%")
.attr("y1", "100%")
.attr("y2", "100%");
gradient.append("stop")
.attr("class", "start")
.attr("offset", "20%")
.attr("stop-color", palette_concern[d.code1])
.attr("stop-opacity", 1);
gradient.append("stop")
.attr("class", "end")
.attr("offset", "80%")
.attr("stop-color", palette_concern[d.code2])
.attr("stop-opacity", 1);
}
### Drawing the arcs
With defined gradients, we can start drawing the arcs by appending a path for every row in the data.
`x1` and `x2` are retrieved with an
ordinal scale,
and the width of the arc is set proportional to the observed counts. An intermediate `r_avg` variable
is created to center the arcs to their ordinal scale's positioning. The arc's position in the svg is
determined by its center (the midpoint of `x1` and `x2`).
The fill is then set to the url gradient we created earlier.
for (i = 0; i < data.length; i++) {
var d = data[i];
var x1 = scale_x(d.code1),
x2 = scale_x(d.code2);
var width = scale_width(d.count),
r_avg = (x2 - x1) / 2.0,
r_inner = r_avg - width/2.0,
r_outer = r_avg + width/2.0 ;
var arc = d3.arc()
.innerRadius(r_inner)
.outerRadius(r_outer)
.startAngle(-Math.PI/2)
.endAngle(Math.PI/2);
var move_x = x1 + (x2 - x1)/2.0
var move_y = baseline_height;
svg.append("path")
.attr("class", "arcs" + " " + d.code1 + " " + d.code2)
.attr("d", arc)
.attr("fill", "url(#svgGradient" + i + ")")
.attr('transform', "translate(" + move_x + "," + move_y + ")");
}
### Drawing clickable nodes
We want to bind the click-events to the nodes, so we first draw some circles, scaling the radius by the count.
var data_node_counts = [
{code: 'A', count: 4800},
{code: 'B', count: 2560},
{code: 'C', count: 26562},
{code: 'D', count: 3892},
{code: 'E', count: 13691},
{code: 'F', count: 29841},
{code: 'G', count: 9981},
{code: 'H', count: 71312},
{code: 'I', count: 1272},
]
var nodes = svg
.selectAll("circle")
.data(data_node_counts)
.enter()
.append("circle")
.attr("class", "nodes")
.attr("id", d => d.code)
.attr("cx", function(d){ return scale_x(d.code); })
.attr("cy", baseline_height)
.attr("r", function(d) { return scale_r(d.count); })
.style("fill", function(d) { return palette_concern[d.code]; })
The clickable behavior is a little more challenging. First, we turn all arcs and nodes grey.
To highlight all arcs belonging to the selected node, we retrieve the node's value and color.
If you noticed earlier, when drawing the nodes, each one was assigned an `id` so that we could
color that node upon selection. When drawing the arcs, each one was assigned two `class`es:
one for each of its nodes. This way, we can select multiple arcs (hence class) for each unique node (hence id)
upon a click-event.
The node and arcs are re-colored, and the selected arcs are raised to the front.
nodes.on("click", function() {
// ------- turn everything grey
svg.selectAll(".arcs").attr("fill", "#dfdfdf")
svg.selectAll(".nodes").style("fill", "#dfdfdf")
// ------- Get associated data from selected node
var code_selected = d3.select(this).data()[0].code,
color_selected = palette_concern[code_selected];
// ------- select node
id_selected = "#" + code_selected
svg.selectAll(id_selected)
.style("fill", color_selected)
// ------- select arcs
class_selected = "." + code_selected
svg.selectAll(class_selected)
.attr("fill", color_selected)
.raise()
})
### How to exit?
We're not done yet! We want to be able to return to gradient-mode when clicking outside of the svg.
The trick is to draw an invisible rectangle *behind* everything, and then add an event-handler.
We can use the same selection scheme as above.
bg_rect.on("click", function() {
// change nodes back to original colors
for (j = 0; j < Object.keys(class_concern).length + 1; j++) {
svg.select("#" + class_concern[j])
.style("fill", palette_concern[j])
}
// select arc that matches datum, change its fill back to original gradient
for (i = 0; i < data.length; i++) {
var d = data[i];
svg.select(".arcs" + "." + class_concern[d.code1] + "." + class_concern[d.code2])
.attr("fill", "url(#svgGradient" + i + ")")
}
})
And that's it!