This #D3Thursday we will look at how you can use Visual Analytics (VA) selections to create a variable view stacked bar chart!
This post’s topics include:
Sometimes a single view of our chart doesn't tell the entire story of the data. A stacked bar chart is a good example of this. A stacked bar chart makes it simple to compare totals, but makes it difficult to compare values across categories. We can alleviate this issue by allowing the user to switch between a left aligned view and a center aligned view as shown above.
Currently, only one type of message is sent to the Data-Driven Content (DDC) object: the data message. That means we have to use VA selections in order to tell our chart which view to use. This has the unfortunate consequence that our chart can no longer receive selections from VA. It can, however, send selection messages if we choose to program it to do so (we haven't in this example).
Imagine we want to visualize the following data set using a stacked bar chart:
Year | Country | Population (in millions) |
1960 | Andorra | 10.6 |
1961 | Andorra | 11.2 |
. . . |
. . . |
. . . |
1960 | Belgium | 5.7 |
1961 | Belgium | 6.1 |
. . . |
. . . |
. . . |
In order to allow the user to switch between views we must add a column to indicate the views. We can make this modification easily with a SAS data step.
data YOUR_NEW_LIB.YOUR_NEW_DS;
set YOUR_ORIG_LIB.YOUR_ORIG_DS;
if mod(_N_, 2) eq 0 then View = "Center Aligned";
else View = "Left Aligned";
run;
Our new data set will look like the following:
Year | Country | Population (in millions) | View |
1960 | Andorra | 10.6 | Center Aligned |
1961 | Andorra | 11.2 | Left Aligned |
. . . |
. . . |
. . . |
. . . |
1960 | Belgium | 5.7 | Center Aligned |
1961 | Belgium | 6.1 | Left Aligned |
. . . |
. . . |
. . . |
.
. . |
Now all we have to do is create a button bar (or other VA control object) with the role View that has a linked selection to our DDC object. This will create the brush column that we use to determine which view is selected by the user.
Year | Country | Population (in millions) | View | Brush |
1960 | Andorra | 10.6 | Center Aligned | 1 |
1961 | Andorra | 11.2 | Left Aligned | 0 |
. . . |
. . . |
. . . |
. . . |
. . . |
1960 | Belgium | 5.7 | Center Aligned | 1 |
1961 | Belgium | 6.1 | Left Aligned | 0 |
. . . |
. . . |
. . . |
.
. . |
. . . |
For your convenience I've included a sample data set (Sample_Data_8.sas7bdat) as well as the SAS code (8_Sample_Data_Generator.sas) used to generate the sample data set. You can use this sample data set to try out the chart, or modify the SAS code to fit your own purposes.
Now that we have our data set prepared, we can move on to the work of accommodating multiple views in our chart. The first step is to validate the data roles we are receiving and determine which view to use.
// Validate data roles
if (
!va.contentUtil.validateRoles(messageFromVA, [
"string",
"string",
"number",
"string",
"number"
])
) {
va.messagingUtil.postInstructionalMessage(
VA_RESULT_NAME,
"D3 Variable View Stacked Bar Chart expects columns to be assigned in this order:\n" +
" 1. Y Category (string)\n" +
" 2. Group By Category (string)\n" +
" 3. Measure (number)\n" +
" 4. View (string)\n" +
" 5. View Selection (from linked selection) (number)"
);
return;
}
// Determine whether or not data should be centered based on selection
CENTERED =
(VA_MESSAGE.data[0][3] == "Center Aligned" && VA_MESSAGE.data[0][4] == 1) ||
(VA_MESSAGE.data[0][3] == "Left Aligned" && VA_MESSAGE.data[0][4] == 0);
First, notice that we are now making the brush column a required role. This means our chart will not render if the linked selection is not set up. You could make this an optional role and have a default view when the role isn't specified, but there isn't much point using this chart if you aren't supporting variable views. In that case you would be better off using the standard VA stacked bar chart.
Secondly, we are only supporting two options for views here, "Center Aligned" or "Left Aligned". You could create a graph that supports more than two views, but doing so causes a few problems.
The next step to accommodating multiple views is creating transitions for these views. By now you are quite familiar with how to create D3.js transitions so we will keep this snippet brief.
DATA_BARS.enter()
...
.attr("x", function() {
// Get previous element
const prev = getNeighborElement(this, "previousElementSibling");
if (!prev) {
// Return 0 if no previous element
return 0;
} else {
// Otherwise, use stored data to determine return
const prevD = JSON.parse(d3.select(prev).attr("data-d"));
if (CENTERED) {
// Iterate to find max from previous category, then return right edge of max
const prevArr = d3.selectAll("." + prevD.categoryNoSpace).nodes();
let max = prevArr[0];
for (let i = 1; i < prevArr.length; i++) {
if (prevArr[i].width.baseVal.value > max.width.baseVal.value) {
max = prevArr[i];
}
}
return max.x.baseVal.value + max.width.baseVal.value + 1;
} else {
// Return right edge position of previous
return prev.x.baseVal.value + prev.width.baseVal.value + 1;
}
}
});
...
The key thing to notice here is that you must check which view is selected, and then create logic for all of your transitions for each possible view. As you can imagine, supporting more than two views could quickly become cumbersome.
Finally, let's take a look at how to create dynamic axes with D3.js. The problem with how we created axes for our original bar charts in the second and third post in this series is that the same number of ticks are used no matter what the dimensions of the graph are. This can cause ticks to overlap as the graph frame is shrunk.
The solution is to tell D3.js exactly which ticks to create to ensure that they don't overlap.
Y_AXIS.enter()
...
.call(
d3
.axisLeft(Y_SCALE)
.tickValues(
getTickValues(
d3.min(DATA, function(d) {
return d.year;
}),
d3.max(DATA, function(d) {
return d.year;
}) + 1,
parseInt(CHART_HEIGHT / Y_TICK_HEIGHT),
1
)
)
.tickFormat(d3.format("d"))
.tickSizeOuter(0)
);
...
// Compute array of readable tick values between min (inclusive) and max (exclusive) of length less than count
function getTickValues(min, max, count, minInterval) {
const pattern = [5, 2, 1];
let tickValues;
let range = max - min;
let pow = Math.floor(Math.log10(range));
let p = 0;
let interval = pattern[p] * Math.pow(10, pow);
do {
tickValues = d3.range(min, max, interval);
if (p == 2) {
p = 0;
pow--;
} else {
p++;
}
interval = pattern[p] * Math.pow(10, pow);
} while (
d3.range(min, max, interval).length <= count &&
interval >= minInterval
);
return tickValues;
}
Now instead of having ticks assigned by D3.js, we determine exactly how many ticks can fit on our axis and then determine a suitable range of ticks that is within that limit.
Next post we will take a look at how we can apply the variable view chart technique to last week's radar chart!
Remember to follow these articles on the SAS Communities Library and the #D3Thursday Twitter hashtag. Comment below or Tweet your questions and comments!
Registration is now open for SAS Innovate 2025 , our biggest and most exciting global event of the year! Join us in Orlando, FL, May 6-9.
Sign up by Dec. 31 to get the 2024 rate of just $495.
Register now!
Data Literacy is for all, even absolute beginners. Jump on board with this free e-learning and boost your career prospects.