BookmarkSubscribeRSS Feed

Visualizing nested data with a sunburst chart in SAS Visual Analytics with #D3Thursday

Started ‎11-08-2018 by
Modified ‎03-25-2019 by
Views 5,570

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.

Sunburst ChartSunburst 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.

 

Handling Nested Data

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.

 

Nesting our Data 

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!

 

Creating our Data Arcs

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().

 

Additional Resources

Next Post

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!

 

 D3Thursday Logo.png

Comments

Nice post Ryan! Keep them coming. 

Version history
Last update:
‎03-25-2019 10:03 AM
Updated by:
Contributors

sas-innovate-2024.png

Available on demand!

Missed SAS Innovate Las Vegas? Watch all the action for free! View the keynotes, general sessions and 22 breakouts on demand.

 

Register now!

Free course: Data Literacy Essentials

Data Literacy is for all, even absolute beginners. Jump on board with this free e-learning  and boost your career prospects.

Get Started

Article Labels
Article Tags