Putting it all together - sequencing animations

Time to put what we've learned to good use. Let's stop creating toys for a second and instead, create an actual chart.

We're going to use a dataset of UK prison population from 1900 to 2015, which is available at uk_prison_population_1900-2015.csv in this data/ folder of the book's repository. I used this data to create a similar series of charts for The Times in 2016, with my team trying out several variations similar to the charts you're about to create, before arriving at the style of interaction you'll see in the following section, "Interacting with the user". 

In chapter5/index.js, comment everything out and add the following at the top:

import prisonChart from './prisonChart';
(async (enabled) => {
if (!enabled) return;
await prisonChart.init();
const data = prisonChart.data;
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const randomChart = () => {
try {
const from = getRandomInt(0, data.length - 1);
const to = getRandomInt(from, data.length - 1);
prisonChart.update(data.slice(from, to));
} catch (e) {
console.error(e);
}
};
prisonChart.update(data);
setInterval(randomChart, 5000);
})(true);

We will create a chart that randomly displays a subset of our data, animating between each state. You wouldn't do this in production, but setting up tests like this can be useful to ensure that your animations will work no matter what you throw at them. We haven't written prisonChart.init() yet; we'll do so shortly.

Create a new file in lib/chapter5/, called prisonChart.js, and add the following:

import * as d3 from 'd3'; 
import { csvParse } from 'd3-dsv';
import chartFactory from '../common';

const prisonChart = chartFactory({
margin: { left: 50, right: 0, top: 50, bottom: 50 },
padding: 20,
transitionSpeed: 500,
});

We instantiate a new chart using our good old chartFactory function, setting a few margins and default values.

Next, we need to define some methods on our new chart object:

prisonChart.resolveData = async function resolveData() { 
return csvParse(await (await fetch('data/uk_prison_data_1900-2015.csv')).text());
};

Here, we create a new asynchronous method to resolve our data. All this does is grab the file as a text string and then parse it using d3-dsv.

Next, we create another async function to do things like initializing scales. We make this asynchronous so that we can await the promise created by resolveData():

prisonChart.init = async function init() { 
this.data = this.data || await this.resolveData();
this.innerHeight = () =>
this.height - (this.margin.bottom + this.margin.top + this.padding);
this.innerWidth = () =>
this.width - (this.margin.right + this.margin.left + this.padding);

this.x = d3.scaleBand()
.range([0, this.innerWidth()])
.padding(0.2);

this.y = d3.scaleLinear()
.range([this.innerHeight(), 0]);

this.x.domain(this.data.map(d => d.year));
this.y.domain([0, d3.max(this.data, d => Number(d.total))]);

this.xAxis = d3.axisBottom().scale(this.x)
.tickValues(this.x.domain().filter((d, i) => !(i % 5)));
this.yAxis = d3.axisLeft().scale(this.y);

this.xAxisElement = this.container.append('g')
.classed('axis x', true)
.attr('transform', `translate(0, ${this.innerHeight()})`)
.call(this.xAxis);

this.yAxisElement = this.container.append('g')
.classed('axis y', true)
.call(this.yAxis);

this.barsContainer = this.container.append('g')
.classed('bars', true);
};

You may have noted that we use both the full not-fat-arrow function keyword here, and use the this keyword quite a lot. Using fat-arrow sets the this context to the parent block, which we don't want as we're wanting to set internal properties of our prisonChart object. This will come in handy later as we extend our chart to do new and different things.

Essentially, all we do in the preceding code is set up the scales and axes and assign them to our object's internal state using its this object. We also create a container to put our bars in, which we assign to this.barsContainer. Most notably, we use our resolveData function to get our data for us, which we assign to the data property of prisonChart.

We will do something different and create a whole method to update data in our chart. When we do more interesting things with interactivity later on in the chapter, we'll rely on this to change our chart's state.

I will go through this one step at a time as it expands upon what we learned about data joints earlier in the book while adding transitions. First, create a function and assign it to prisonChart, like so:

prisonChart.update = function update(_data) { 
const data = _data || this.data;
const TRANSITION_SPEED = this.transitionSpeed;
};

All we do here is set a constant for our transitions, which we'll use throughout. We have a local data variable that we populate with either the subset of the data passed through as the argument or, if that's undefined, we grab the data we set to the object's internal data property in the init() function.

Continuing to add to that our update() function, insert the following lines:

// Update 
const bars = d3.select('.bars').selectAll('.bar');
const barsJoin = bars.data(data, d => d.year);

// Update scales
this.x.domain(data.map(d => +d.year));
this.xAxis.tickValues(this.x.domain()
.filter((d, i, a) => (a.length > 10 ? !(i % 5) : true)));

// Call this.xAxis after double the TRANSITION_SPEED timeout value
d3.timeout(this.xAxis.bind(this, this.xAxisElement), TRANSITION_SPEED * 2);

Here, we select our .bars container, and then all the items with class .bar inside it, which we save as our bars local variable. We then join our new dataset to that, which we assign to barsJoin. Note how we use the second argument in selection.data() to tell D3 which pieces of data correspond to which array element; normally, D3 keys objects by array index, but if we're adding and subtracting elements to our data array, we can't rely on that. We update our x scale's domain to be all the years supplied in the dataset, and set the x axis to display every fifth tick value if the number of items in the array is greater than 10. We then set a timer to fire once after waiting twice the value of our transition speed constant, which causes our x axis to update (if confused, running this.axisElement.call(this.xAxis) would update the axis immediately, while this.xAxis.bind(this, this.xAxisElement) returns a function that updates our x axis when it is run--in this way, we leave d3.timeout to run the function once the timeout function fires).

Continuing to add to prisonChart.update():

  // Remove 
barsJoin.exit()
.transition()
.duration(TRANSITION_SPEED)
.attr('height', 0)
.attr('y', this.y(0))
.remove();

Here, we take our new data and join and set up its exit() state. We set up a new transition, which we set to the length of our transition speed constant. We then tell it to transition each bar's height to zero as well as move its y value to the chart x axis. This will cause all of our bars to shrink towards the x axis as they're leaving the scene.

Next, we set our enter() state:

  const newBars = barsJoin.enter() // Enter 
.append('rect')
.attr('x', d => this.x(+d.year))
.classed('bar', true);

This sets a new local variable to the new bars that were created due to the data join. We set their x value straightaway and give them a .bar class.

Here's the last bit of prisonChart.update(), which handles both the new bars and the remaining old bars:

  barsJoin.merge(newBars) // Update 
.transition()
.duration(TRANSITION_SPEED)
.attr('height', 0)
.attr('y', this.y(0))
.transition()
.attr('x', d => this.x(+d.year))
.attr('width', this.x.bandwidth())
.transition()
.attr('x', d => this.x(+d.year))
.attr('y', d => this.y(+d.total))
.attr('height', d => this.innerHeight() - this.y(+d.total));

Here, we use D3 v4's new merge() function to update our existing bars. We take the selection of our old bars and merge it with the selection of our new bars and then run operations on all of them. First, we initiate a transition lasting one transition speed interval, which sets all of our bars to a height of zero and a scaled y value of zero; this is exactly what we did while removing bars. Then, we set another transition lasting the default of 250 ms, which moves all the bars to their new location and sets their width appropriately, given their size. A final transition after that one sets each bar to its proper height.

Refresh and you'll have a hyperactive, rapidly-refreshing chart!:

The bars grow, the data changes, the bars shrink, the axes update, and the bars grow again. Splendid!

That does it for animation using D3 transitions. Next, we'll be looking at adding interactions using the various behaviors that come with D3.

What are the other ways to animate SVG?
So glad you asked!
In the beginning, there was SMIL or Synchronized Multimedia Integration Language. To use SMIL, you use <animate> tags, which you put in your SVG markup. If this sounds gross already, it is. Luckily, Chrome 45 depreciated it, which means you never have to worry about learning it. For the morbidly curious, refer to https://mdn.io/SVG_animation_with_SMIL.
Another way of doing transitions is using CSS transitions. For simple animations, these are terrific as they are able to use your graphics card GPU to render things, ensuring that they don't block and thus improving performance. In fact, I used these for the last example in the previous edition of the book. However, they don't work with canvas and are difficult to sequence when you're also using d3-transition in your project, which means that they're probably best left to things such as animating hover states on buttons and other UI-related animations.

For more information, visit: https://developer.mozilla.org/Web/CSS/CSS_Transitions/Using_CSS_transitions.
In the future, another alternative to animate using JavaScript will be the Web Animation API. It's currently not supported by anything from Microsoft or Apple, but it's been in Chrome since version 36 and Firefox since version 41. It has the nicest syntax, is all based on JavaScript, and works well with D3. Here's an example:

d3.selectAll('.bar').each(function(d, i){
  this.animate([
    {transform: &grave;translate(${x(i)}, ${y(d)})&grave;}
  ],{
    duration: 1000,
    iterations: 5,
    delay: 100
  });
});

Alas! As always, web development is a toy chest full of things you can't reliably use just quite yet. If you want to play with it while it's still being standardized, check out the fantastic polyfill at https://github.com/web-animations/web-animations-js that enables the use of Web Animations in most modern browsers. Again, this is all really early days; if you use the Web Animations API with D3 in a project, drop me a line and let me know how it went!
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset