This #D3Thursday we will look at how we can visualize our nested data within SAS Visual Analytics (VA) using a D3.js sunburst chart!
Like last post, there isn't much new material to cover here since our sunburst chart is an extension of the pie chart we made previously! All we will cover today is how to handle and process nested data to create our sunburst chart.
As with previous posts I've included sample data (Sample_Data_10.sas7bdat) for this post to help you get started. Do note that, unlike previous posts, the sunburst chart doesn't require any special modifications to your data set. Any nested data will work in this visualization (like SAShelp_Baseball.sas7bdat). The sample data mentioned above is only included so that you can see how the graph works with data that isn't uniformly nested.
Before we get started, I should explain what I mean by nested data. As a SAS user, you may be unfamiliar with nested data since SAS always uses flattened data sets like the following:
Category 1 | Category 2 | Value |
A | Apple | 27 |
A | Apricot | 32 |
B | Banana | 68 |
B | Blueberry | 15 |
Notice that Category 2 is a sub-category of Category 1. Instead of representing this data in a flat table as shown above, we could also use a nest that shows the dimensionality of our data:
{
"id": "Root",
"depth": 0,
"value": 142,
"children": [
{
"id": "A",
"depth": 1,
"value": 59,
"children": [
{
"id": "Apple",
"depth": 2,
"value": 27
},
{
"id": "Apricot",
"depth": 2,
"value": 32
}
]
},
{
"id": "B",
"depth": 1,
"value": 83,
"children": [
{
"id": "Banana",
"depth": 2,
"value": 68
},
{
"id": "Blueberry",
"depth": 2,
"value": 15
}
]
}
]
}
This representation of the data, while less compact, shows the hierarchy between categories. This is exactly what we need while visualizing hierarchical data with our sunburst chart.
D3.js does have functions to turn a flat data set (like what we receive from VA) to a nested data set, but there is one issue. We want to support non-uniformly nested data, which would require writing a custom roll up function for the d3.nest function to call. Instead, I opted to create a custom recursive function to handle this data processing:
DATA = nestLayer(
{
id: "Root",
depth: 0,
children: []
}
);
...
function nestLayer(nestedData) {
const data = VA_MESSAGE.data;
const pushedCategories = {};
const catIndex = nestedData.depth + 1;
let dataId, testId, datum;
let color = nestedData.color;
let initColor;
// Iterate over all data elements
for (let i = 0; i < data.length; i++) {
// Create id for data element
dataId = "";
for (let j = 1; j <= catIndex; j++) {
dataId += j == 1 ? "" : "-";
dataId += data[i][j].replace(/ /g, "_");
}
// Create test id to compare data id
testId = nestedData.id + "-" + data[i][catIndex].replace(/ /g, "_");
// Initialize color / iterate to new version
initColor = FILL[dataId] ? FILL[dataId] : COLOR_SCALE(dataId);
color = catIndex == 1 ? d3.color(initColor).darker(0.5) : color;
if (
catIndex == 1 || // If category is top level or...
dataId == testId // element should be a child of nestedData
) {
if (
catIndex == data[i].length - 1 || // If category is last level or...
data[i][catIndex + 1] == MISSING // next category is missing
) {
// Push on data element as leaf
color = color.brighter(0.25);
datum = {
id: dataId,
label: data[i][catIndex],
color: color,
value: data[i][0]
};
nestedData.children.push(datum);
} else if (!pushedCategories[data[i][catIndex]]) { // Otherwise if we haven't recursed for this category...
// Push result of recursing to data elements children
pushedCategories[data[i][catIndex]] = true;
color = color.brighter(0.25);
nestedData.children.push(
nestLayer({
id: dataId,
label: data[i][catIndex],
color: color,
depth: nestedData.depth + 1,
children: []
})
);
}
}
}
return nestedData;
}
Notice that we are only including the value attribute at the leaf level. That is because we will need to convert our nested data structure into a d3.hierarchy and can calculate these values at that step.
// Create d3 hierarchy from nested data
ROOT = d3.hierarchy(DATA).sum(function(d)
return d.value;
});
// Create partition for nested data
PARTITION = d3.partition().size([2 * Math.PI, RADIUS * RADIUS]);
// Apply partition to root
PARTITION(ROOT);
Here are the last few steps of preparing our data. First, we do the conversion mentioned above to get our data into the format that D3.js expects and calculate values for non-leaf nodes. Next, we create a partition that defines our space. In the case of a sunburst chart, we are working with polar coordinates so our partition will be in terms of angle and radius length. Finally, we apply this partition to our hierarchy. This partition will iterate over all of the nodes in our hierarchy and assign them a start angle, end angle, start radius, and end radius using their values and positions in the hierarchy.
For more information on how D3.js hierarchies and partitions work, checkout the D3.js API documentation linked below!
With our data processed, the hard work is out of the way! All that's left is to setup our arc generators and start making path elements!
ARC = d3
.arc()
.startAngle(function(d) {
return d.x0;
})
.endAngle(function(d) {
return d.x1;
})
.innerRadius(function(d) {
return Math.sqrt(d.y0);
})
.outerRadius(function(d) {
return Math.sqrt(d.y1);
});
...
DATA_ARCS = G_CHART_AREA.selectAll(".data-arc").data(
ROOT.descendants(),
function(d) {
return d.data.id;
}
);
DATA_ARCS.enter()
...
.attr("d", ARC);
Notice that when we create our arc generators, we take the square root of the radius values assigned by our partition function. That is because we set our partition to have a size of RADIUS*RADIUS. The reason we did this instead of just using a partition of size RADIUS is that we want our arcs to be thinner than our partition will create by default.
Also notice that we can't just use our ROOT as the data for our data-join. Data-joins always use arrays as their data input so we must flatten our hierarchy back out by calling ROOT.descendants().
Next post we will learn how to create a stream graph with built in zoom and brush events!
Remember to follow these articles on the SAS Communities Library and the #D3Thursday Twitter hashtag. Comment below or Tweet your questions and comments!
Nice post Ryan! Keep them coming.
SAS Innovate 2025 is scheduled for May 6-9 in Orlando, FL. Sign up to be first to learn about the agenda and registration!
Data Literacy is for all, even absolute beginners. Jump on board with this free e-learning and boost your career prospects.