ABeamer has its own charts component, but it can also be used to create frame-by-frame animations of another chart and map libraries.
D3.js has been widely used to create interactive animations for data science, but since its animation engine it’s mainly designed to be used interactively, if want you to generate the animation frames to generate animated gifs or movies by using an video capture application, you can’t guarantee the output quality nor guarantee that the animation is timely captured.
But by using ABeamer on top of D3.js, you can generate high-resolution frames at a selected frame rate.
To proof the concept, we will use an d3 datamap animated with ABeamer, and from that animation generate the frame sequence and an animated GIF.

Step-by-step tutorial

Final result on Codepen

Just follow the following steps:

  • If ABeamer isn’t install, read here how to install it.

  • Create ABeamer project named d3-datamap:

abeamer create d3-datamap --width 640 --height 360

HTML

  • Add the d3, topojson and datamap script files to index.html before abeamer.min.js.
  <script src="https://www.abeamer.com/files/by-date/2018/10/d3-datamap/lib/d3.min.js"></script>
  <script src="https://www.abeamer.com/files/by-date/2018/10/d3-datamap/lib/topojson.min.js"></script>
  <script src="https://www.abeamer.com/files/by-date/2018/10/d3-datamap/lib/datamaps.world.min.js"></script>
  • Add the datamap container inside scene1:

(The story is ABeamer root element, and supports multiple scenes just like a theater play)

<div class="abeamer-story" id=story>
  <div class="abeamer-scene" id=scene1>
      <div id="map"></div>
      <h1>GDP Per Capita</h1>
  </div>
</div>

Stylesheet

  • Add basic style to the css/main.scss file:
// the map will occupy the whole frame
#map {
  position: absolute;
  width: $abeamer-width + px;
  height: $abeamer-height + px;
}

h1 {
  z-index: 10;
  letter-spacing: 0px;
  position: relative;
  margin: 9px;
  text-decoration: underline;
  font-size: 36px;
  font-weight: bold;
  color: #424242;
}

JavaScript

The following code is written in TypeScript, but you can use pure JavaScript.

  • Load your data.

Since d3 datamap doesn’t supports country names, only ISO Country Codes, the first step is to load a list of Country Names and ISO Country Codes, and create JavaScript map of ISO alpha-3 per country.
In the second step, builds a JavaScript array with a list of ISO alpha-3 Country codes and GDP per Capita, and calls dataLoaded with that data.

d3.json('https://www.abeamer.com/files/by-date/2018/10/d3-datamap/data-cors/iso-codes.json', (isoData) => {

    // populate iso3 per country
    const iso3perCountry = {};
    isoData.forEach(item => {
      iso3perCountry[item.country] = item.iso3;
    });

    d3.json('https://www.abeamer.com/files/by-date/2018/10/d3-datamap/data-cors/gdp-ppp.json', (gdpData) => {

      // load GDP PER CAPITA per COUNTRY
      const gdpPPPerCountry = gdpData.map(item => {
        const iso3Code = iso3perCountry[item.country];
        if (!iso3Code) {
          throw `Unknown ISO alpha-3 for ${item.country}`;
        }
        return { iso3Code, gdpPPP: item.gdpPPP };
      });
      dataLoaded(gdpPPPerCountry);
    });
  });
  • Place the animation and render command inside dataLoaded.

ABeamer uses addAnimations and addStills to build the animation pipeline,
and story.render generates frame-by-frame.

  function dataLoaded(gdpPPPerCountry) {

    const scene = story.scenes[0];
    scene.addStills('2s');

    story.render(story.bestPlaySpeed());
  }
  • Create a d3 datamap with bubbles as usual.

Disable all d3 animations since these animations aren’t controlled by ABeamer. In this case, set animate: false in the bubblesConfig.

  map = new Datamap({
      element: document.getElementById('map'),
      fills: {
        bubbleColor: '#306596',
        defaultFill: '#dddddd',
      },
      setProjection: (element) => {
        const projection = d3.geo.mercator()
          .scale(100)
          .translate([element.offsetWidth / 2, element.offsetHeight / 2]);

        const path = d3.geo.path().projection(projection);
        return { path, projection };
      },
      bubblesConfig: {
        // disable bubbles animation since, we will use only ABeamer animations
        animate: false,
        borderWidth: 1,
      },
    });

JavaScript - Bubbles Animation

Since d3 isn’t a DOM element, we need to use ABeamer VirtualAnimator facility to animate a d3 datamap.
Our first goal is to animate the bubbles, to archive this process we derive from ABeamer.SimpleVirtualAnimator since it allow us to execute animateProps once per frame, unlike ABeamer.VirtualAnimator which allows to call per property change.

  • First, we prepare the data:
  gdpPPPerCountry.sort((a, b) => a.gdpPPP > b.gdpPPP ? -1 : 1);
  const maxGdpPPP = gdpPPPerCountry[0].gdpPPP;
  const maxRadius = 20;
  • Second, we create the MapAnimator with the method animateProps:
  class MapAnimator extends ABeamer.SimpleVirtualAnimator {

    animateProps(): void {

      map.bubbles(
        gdpPPPerCountry.map(item => {
          return {
            radius: parseFloat(this.props.t) * (item.gdpPPP / maxGdpPPP) * maxRadius,
            centered: item.iso3Code,
            fillKey: 'bubbleColor',
          };
        }));
    }
}
  • Third, we add the MapAnimator to the story:

The selector will be used animate the properties.

  const ca = new MapAnimator();
  ca.selector = 'map-fx';
  story.addVirtualAnimator(ca);
  • Forth, we add an animation to the story:
  scene.addAnimations([{
    selector: '%map-fx',  // must match ca.selector with `%` prefix
    duration: `2s`,
    props: [{
      prop: 't',
      valueStart: 0,
    }],
  }])

With these 4 steps, ABeamer will execute MapAnimator.animateProps once per frame, the number of frames is defined on ABeamer.createStory for 2 seconds, iterating the property t from 0 (defined by valueStart) to 1 (the end value is 1 by default).


JavaScript - Zoom Animation

The 2nd part of the animation it’s to zoom to Europe.
d3.js allows zoom by using attr with scale.
To add this animation with need the new iterator zoom running from 1 to 5.
Before the zoom animation starts, the zoom value will be 0.

  • First, change the animation code to incorporate the zoom animation:
class MapAnimator extends ABeamer.SimpleVirtualAnimator {

      // this method will be called one for each frame
      // this.prop.t goes from 0 to 1.
      animateProps(): void {

        const zoom = ca.props.z;

        if (zoom < 1) {
          // animate bubbles
          map.bubbles(
            gdpPPPerCountry.map(item => {
              return {
                radius: parseFloat(this.props.t) * (item.gdpPPP / maxGdpPPP) * maxRadius,
                centered: item.iso3Code,
                fillKey: 'bubbleColor',
              };
            }));

        } else {
          // animate zoom
          const svg = map.svg;
          svg.attr('transform', `translate(0, ${zoom * 100}), scale(${zoom})`);
          svg.selectAll('circle').style('stroke-width', `${1 / zoom}px`);
          svg.selectAll('path').style('stroke-width', `${1 / zoom}px`);
        }
      }
    }
  • Second, change the initialization of VirtualAnimator code to reset the zoom property:
  const ca = new MapAnimator();
  ca.selector = 'map-fx';
  ca.props.z = 0;
  story.addVirtualAnimator(ca);
  • Third, change scene animations to incorporate zoom property animation:
   scene
      .addAnimations([{
        selector: '%map-fx',
        duration: `2s`,
        props: [{
          prop: 't',
          valueStart: 0,
        }],
      }])
      // add still frames
      .addStills('2s')
      .addAnimations([{
        selector: '%map-fx',
        duration: `2s`,
        props: [{
          prop: 'z',
          valueStart: 1,
          value: 5,
        }],
      }])
      // add still frames
      .addStills('2s');

With these 3 extra steps, once the bubble animation finishes, it waits for 2s and then animates the zoom for another 2s.