BookmarkSubscribeRSS Feed

Animating a D3.js area graph over time in SAS Visual Analytics with #D3Thursday

Started ‎11-29-2018 by
Modified ‎04-25-2019 by
Views 4,104

This #D3Thursday we will look at how we can animate a D3.js area graph over time in SAS Visual Analytics (VA).

 

Animated Area ChartAnimated Area Chart

 As with previous posts I've included sample data (Sample_Data_12.sas7bdat) for this post to help you get started. This data set is significantly larger than any we have use before, so you will have to override VA's system data limit in order to send all 50,000+ rows to our graph.System Data LimitSystem Data Limit

Handling the Data

 

The first step in creating our animation is getting our data into the right shape.

// Extract data sets from 2d array format into associative array format
DATA_SETS = [];
let dateDatum, binDatum, binTotal;
let i = 0;
let length = VA_MESSAGE.data.length;
while (i < length) {
dateDatum = {
date: VA_MESSAGE.data[i][0],
entries: []
};

while (i < length && _.isEqual(dateDatum.date, VA_MESSAGE.data[i][0])) {
binTotal = 0;
binDatum = {
bin: VA_MESSAGE.data[i][1]
};

while (i < length && binDatum.bin == VA_MESSAGE.data[i][1]) {
binDatum[VA_MESSAGE.data[i][2]] = VA_MESSAGE.data[i][3];
binTotal += VA_MESSAGE.data[i][3];
i++;
}

Y_MAXIMUM = Math.max(Y_MAXIMUM, binTotal);
dateDatum.entries.push(binDatum);
}

DATA_SETS.push(dateDatum);
}

// Sort by earliest date and lowest bin
DATA_SETS.sort(function(a, b) {
return Date.parse(a.date) - Date.parse(b.date);
});

for (let i = 0; i < DATA_SETS.length; i++) {
DATA_SETS[i].entries.sort(function(a, b) {
return a.bin - b.bin;
});
}

...

// Create data stack
STACKED_DATA = STACK(DATA_SETS[DATA_INDEX].entries);

There are two key things that are different here from previous weeks. First, we no longer have a single data set. Instead, we have an array of data sets (one for each date/time). Secondly, we must sort our data sets by date and bin in order to render our areas correctly.

 

Adding the Input Controls

Now that we have our data in the right shape, we can create the input controls we will need to control our animations.

<div class="input-container">
<div id="row1" class="row">
<span id="play-button">
<i class="fas fa-play-circle"></i>
</span>
<p id="min-date">
</p>
<input id="date-slider" class="slider" type="range" min="0" max="100" value="0" step="1">
<p id="max-date">
</p>
</div>
</div>

You have a lot of options here for what inputs you want to create, and even more options for how to style them. I decided to use FontAwesome icons to create the play button since the library is lightweight and easy to work with. You could also use a custom icon stored on your server for this, or create it with SVG graphics.

 

I also decided to use a standard HTML slider input and two paragraph tags for the date control. Again, I did this for simplicity, but you could instead use a jQuery or D3.js plug-in slider.

 

Also notice that I hard coded these HTML elements. For maximum portability, you would want your D3.js code to create these elements on your page, but this is simpler and works for our example. 

 

Binding Control Functions

Now that we have our input controls on the page, we can bind the functionality we want to each element.

// Animate when play button clicked
d3.select("#play-button").on("click", function() {
// Toggle playing boolean
PLAYING = !PLAYING;

// Update play button
d3.select("#play-button i")
.classed("fa-pause-circle", PLAYING ? true : false)
.classed("fa-play-circle", PLAYING ? false : true);

// Animate when played
if (PLAYING) {
// Reset slider if we are at end of data sets
if (DATA_INDEX == DATA_SETS.length - 1) {
d3.select("#date-slider").node().value = 0;
}

animateElements();
}
});

// Redraw when date slider moved
d3.select("#date-slider").on("input", function() {
// Update data index and data set, stop playing
DATA_INDEX = parseInt(this.value);
STACKED_DATA = STACK(DATA_SETS[DATA_INDEX].entries);
PLAYING = false;

// Update progress bar on slider
d3.select(this)
.attr("value", this.value)
.style("background-image", function() {
const middle =
(parseInt(this.value) - parseInt(this.min)) /
(parseInt(this.max) - parseInt(this.min));

return (
"-webkit-gradient(linear, left top, right top, " +
"color-stop(" +
middle +
", #777777), " +
"color-stop(" +
middle +
", #d3d3d3)" +
")"
);
});

// Change play button to pause
d3.select("#play-button i")
.classed("fa-pause-circle", false)
.classed("fa-play-circle", true);

// Redraw selected data set
drawElements();
});

 

Animating

Finally, we can get around to the business of writing the animateElements function. We could have simply used our existing updateElements function to handle the animation, but I prefer to keep it separate for simplicity. The updateElements function has to handle changes to many elements (the scales, axes, legend, etc.) that won't change while animating over time. Instead of reusing our complicated update function, I've elected to create a more compact animateElements function.

// Redraw elements changed over time
function animateElements() {
// Iterate to next dataset
DATA_INDEX = DATA_INDEX >= DATA_SETS.length - 1 ? 0 : DATA_INDEX + 1;
STACKED_DATA = STACK(DATA_SETS[DATA_INDEX].entries);

// Update date slider
d3.select("#date-slider")
.style("background-image", function() {
const middle =
(parseInt(this.value) + parseInt(this.step) - parseInt(this.min)) /
(parseInt(this.max) - parseInt(this.min));

return (
"-webkit-gradient(linear, left top, right top, " +
"color-stop(" +
middle +
", #777777), " +
"color-stop(" +
middle +
", #d3d3d3)" +
")"
);
})
.node()
.stepUp();

// Update date texts
DATE_TEXT = SVG.selectAll(".date-text").data([DATA_SETS[DATA_INDEX].date]);

DATE_TEXT.text(function(d) {
return DATE_FORMAT(d);
});

// Update data paths
DATA_PATHS = G_CHART_AREA.selectAll(".data-path").data(
STACKED_DATA,
function(d) {
return d.key;
}
);

DATA_PATHS.transition()
.duration(ANIMATION_TRANS_TIME)
.attr("d", AREA);

// If playing and not at end of data sets, set timer to animate again
if (PLAYING && DATA_INDEX < DATA_SETS.length - 1) {
setTimeout(animateElements, ANIMATION_TRANS_TIME);
}
// Otherwise, pause
else if (DATA_INDEX == DATA_SETS.length - 1) {
PLAYING = false;

d3.select("#play-button i")
.classed("fa-pause-circle", false)
.classed("fa-play-circle", true);
}
}

The one thing worth noting here is that we aren't actually doing any interpolating on the date text element. Since our data set spans such a large range of dates, interpolating across all date values becomes dizzying. If, however, you were animating over a smaller number of dates that were close together you may want to interpolate the text to smooth out the animation.

 

Controlling Animation Speed

As a bonus, we will add another input slider to allow the user to control the speed of the animation.

<div class="input-container">
...
<div id="row2" class="row">
<p id="slow-text">Slow</p>
<input id="speed-slider" class="slider" type="range" min="20" max="500" value="100">
<p id="fast-text">Fast</p>
</div>
</div>

...

// Update animation time when speed slider moved
d3.select("#speed-slider").on("input", function() {
ANIMATION_TRANS_TIME = parseInt(this.value);
});

Giving the user the ability to control the animation speed is a great idea since we don't know how many dates we will be animating over or what length of time is between them. Here the user can select any animation duration between 20 and 500 milliseconds, which should be sufficient for most situations. 

 

Additional Resources

The Future of #D3Thursday

Unfortunately, this post marks the end of the 12 planned #D3Thursday posts. Don't fret though, as I'm not going anywhere and will be continuing to work on awesome third-party visualizations to use in VA. Stay posted for additional #D3Thursday content and be sure to reach out with any ideas you have visualizations or interesting data you think deserve a post!

 

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

Awesome ! Tank-s 

Version history
Last update:
‎04-25-2019 09:51 AM
Updated by:
Contributors

SAS Innovate 2025: Call for Content

Are you ready for the spotlight? We're accepting content ideas for SAS Innovate 2025 to be held May 6-9 in Orlando, FL. The call is open until September 25. Read more here about why you should contribute and what is in it for you!

Submit your idea!

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