Creating an automatic data driven 3d slideshow

I was asked a while ago, to create a memorable and interesting way to present a large number “initiatives”, by description to a set of senior leaders. Todays post is describing the tool I built to do that.

Firstly, this isn’t really an excel problem, I solved this with html and javascript. In the javascript, I used two main libraries: D3.js and impress.js

If you’ve never met these libraries before, they’re all well worth a quick peek:

http://d3js.org/

http://impress.github.io/impress.js/#/bored

And in addition to those two big heavy hitters, I also needed an efficient way to pack the text into as small an area as possible. So I also used this: http://codeincomplete.com/posts/bin-packing/

What I wanted, was to be able to pickup data from anywhere, in a simple json format, and turn that data into a pretty and automated slide show.

You can see the demo here:

http://htmlpreview.github.io/?https://github.com/arrghh1/AutoTextSlideShow/blob/master/testImpress2.html#/step-1

So, clearly step one of writing this blog achieved: Upload code to the internet, because the internet never forgets (very much unlike my corporate network…)

Now, to understand what I did…

The summary

  1. Use D3 to create divs on the page (which impress can use as slides in a slideshow)
  2. Create a function which sizes and positions these divs
  3. send this to impress to make a slide show

The whole picture

Firstly, define my data format. I created a variable called myFullData, and it has the form:

myFullData=[
 {
 "INIT_NAME": "Branden Beach",
 "WHAT": "pede. Nsdfsd sd foijweo wetrwe wer"}, 
{
 "INIT_NAME": "Byron Clay",
 "WHAT": "facilisis vitae, orci. Phasellus dapibus"}];

Now that that’s settled, I chucked that data temporarily in a text file which is loaded in the html as a script (for reference, the plan was always to move this into an AJAX call, but the data never actually resided anywhere buy my desktop, so never got this far).

So, setting up a typical html file, and loading all the scripts I’ve already talked about:

<html>
 <head>
 <meta name="generator"
 content="HTML Tidy for HTML5 (experimental) for Windows https://github.com/w3c/tidy-html5/tree/c63cc39" />
 <meta charset="utf-8" />
 <meta name="viewport" content="width=1024" />
 <meta name="apple-mobile-web-app-capable" content="yes" />
 <link href="css/default-style.css" rel="stylesheet" />
 http://js/d3.min.js
 http://js/packer.growing.js
 http://js/PAImpressApp2_rel.js
 
 <title></title>
 </head>
 <body>

Done. Next, what goes inside the body? well, just an empty div is all that Impress requires.

 

then load the impress.js library…

 http://js/impress.js

then, very lazily, just chuck my layout code into script tags in the html body. This is where things get heavy. I’m going to chuck it all here, and edit this post later to describe how it works (or doesn’t as the case might be).

d3.select("div#impress").selectAll('.step')
     .data(myFullData)
     .enter()
     .append("div")
     .html(function(d){return "<H1>"+d.INIT_NAME +"</H1>" + d.WHAT})
     .attr("class","step");
 
 function randomScale(){
     var bob=(Math.random()+.8)*3;
     return bob;
 }
 function randomRot(){
     var bob=Math.random()
     if (bob<=0.25){
        return [0,0];
     } else if (bob<=0.5){
        return [90,1];
     } else if (bob<=0.75){
        return [180,0];
     } else {
        return [270,1];
      }
      //return [0,0];
 }
 
 function myLayout(data) {
     var nodes=data[0];
     for (i=0;i<nodes.length;i++){
         var getRand=randomRot();
         var getScale=randomScale();
         nodes[i].scale=getScale;
         nodes[i].rot=getRand[0];
         nodes[i].rotState=getRand[1];
         if (nodes[i].rotState==0){
             nodes[i].w=nodes[i].offsetWidth*nodes[i].scale;
             nodes[i].h=nodes[i].offsetHeight*nodes[i].scale;
         } else {
             nodes[i].h=nodes[i].offsetWidth*nodes[i].scale;
             nodes[i].w=nodes[i].offsetHeight*nodes[i].scale;
         } 
 }
 var packer = new GrowingPacker();
 packer.fit(nodes);
 for (i=0;i<nodes.length;i++){
     if (nodes[i].rotState==0){
         nodes[i].__data__.x=nodes[i].fit.x+0.5*nodes[i].offsetWidth*nodes[i].scale;
         nodes[i].__data__.y=nodes[i].fit.y+0.5*nodes[i].offsetHeight*nodes[i].scale;
     } else {
         nodes[i].__data__.x=nodes[i].fit.x+0.5*nodes[i].offsetHeight*nodes[i].scale;
         nodes[i].__data__.y=nodes[i].fit.y+0.5*nodes[i].offsetWidth*nodes[i].scale;
     };
     nodes[i].__data__.rot=nodes[i].rot;
     nodes[i].__data__.scale=nodes[i].scale;
 } 
 return nodes
 }
 
 var origNodes=d3.selectAll(".step");
 myLayout(origNodes);
 origNodes
     .attr('data-x',function(d){return d.x})
     .attr('data-y',function(d){return d.y})
     .attr('data-rotate',function(d){return d.rot})
     .attr('data-scale',function(d){return d.scale;});


impress().init();
 
 (function(){
     impress().next();
     setTimeout(arguments.callee, 15000);
 })();
 </script>
 
 </body>
</html>

So, what’s going on here, what was my plan, and how does it work…

Breaking it down

Firstly, I guess it helps if you have at least (like me) a basic understanding of what each of the first two libraries do.

D3, enables me to create items on the DOM of html, that are driven by data. so the first line in this code:

d3.select("div#impress").selectAll('.step')
    .data(myFullData)
    .enter()
    .append("div")
    .html(function(d){return "<H1>"+d.INIT_NAME +"</H1>" + d.WHAT})
    .attr("class","step");

Does quite a bit. Firstly, it selects the div with the ID impress, then there’s a chain of things that happen. It selectsAll items with the class .step, of which there are initially none, but the data(myFullData).enter creates some elements, which will become these –  based on the data (one for each element in the dataset).

Then it appends these items to the DOM as divs, and appends to those divs some html text based on the anonymous function which returns the two items from the element in the dataset. Once it’s done this, it then assigns the class to the newly created div, step.

If we stopped here, we’d have a page, with all the divs overlapping, but they’d all have the content that the JSON file with myFullData in it defined. I spent a lot of time here, thinking about where to go next.

Where I went next

I needed to give impress 4 attributes on the div, for it to know what to do with it. These are data-x, data-y, data-rotate, and data-scale. I decided that each element should be rotated randomly in 90º increments, and the scale should vary a little, so that there was some zooming effect. So the next bit of code was to work out how to apply those attributes.

Given, nothing had any attributes, the first thing to do was create functions that give back random values for scale, and rotation. These are pretty self explanitory, get a random number, based on that, allocate a scale and rotation.

 function randomScale(){
     var bob=(Math.random()+.8)*3;
     return bob;
 }
 function randomRot(){
     var bob=Math.random()
       if (bob<=0.25){
         return [0,0];
       } else if (bob<=0.5){
         return [90,1];
       } else if (bob<=0.75){
         return [180,0];
       } else {
         return [270,1];
 }
 //return [0,0];
 }

Note in this, that the randomRot function returns two values in an array, the second item is a flag that tells me whether to switch the height and width when doing calculations (yes, I could have just used the rotation values, but this was obviously an evolution…).

So, now what… we haven’t really changed anything have we? So, next in the code flow, is the following two lines

 var origNodes=d3.selectAll(".step");
 myLayout(origNodes);

This assigns all the divs with class step, into an object called origNodes. This becomes our new home for the ‘text nodes’ that we created earlier.

Then, we call the function myLayout on this object. My layout needs some explaining. The intent with this function is to assign the 4 variables to the data on each node (not the attributes at this point, as d3 allows us to bind data, before using the data in atrributes or values or whatever). In this case, I’m going to annotate the code…

But the summary of this is:

  1. Apply a random scale and rotation to each div
  2. Get the width and height (in the same coordinate space for each div regardless of rotation)
  3. Use the packing library to take these values and workout where the x and y of each div should be so that they’re close, but don’t overlap

 

function myLayout(data) {
   var nodes=data[0];
   for (i=0;i<nodes.length;i++){ //For every item in nodes (origNodes)
     var getRand=randomRot();    //get the array for rotation
     var getScale=randomScale(); //get the scale value
     nodes[i].scale=getScale;    //apply the scale to this node
     nodes[i].rot=getRand[0];    //apply the rotation value to this node
     nodes[i].rotState=getRand[1]; //get the width/height swap state
     if (nodes[i].rotState==0){    //if it's not rotated 90 or 270
       nodes[i].w=nodes[i].offsetWidth*nodes[i].scale; //width of node is scaled
       nodes[i].h=nodes[i].offsetHeight*nodes[i].scale; //height of node is scaled
     } else {
       nodes[i].h=nodes[i].offsetWidth*nodes[i].scale; //see, h is width and vice versa
       nodes[i].w=nodes[i].offsetHeight*nodes[i].scale;//but applies the same scale
    } 
  }
  var packer = new GrowingPacker();
  packer.fit(nodes); //now the nodes have sizes, we can pack them
  for (i=0;i<nodes.length;i++){ //for every node
     if (nodes[i].rotState==0){ // check which way round the box is and then get an x or y. remember to use the size of the box to offset so you get the top & left!
       nodes[i].__data__.x=nodes[i].fit.x+0.5*nodes[i].offsetWidth*nodes[i].scale;
       nodes[i].__data__.y=nodes[i].fit.y+0.5*nodes[i].offsetHeight*nodes[i].scale;
     } else {
       nodes[i].__data__.x=nodes[i].fit.x+0.5*nodes[i].offsetHeight*nodes[i].scale;
       nodes[i].__data__.y=nodes[i].fit.y+0.5*nodes[i].offsetWidth*nodes[i].scale;
     };
     nodes[i].__data__.rot=nodes[i].rot;
     nodes[i].__data__.scale=nodes[i].scale;
  } 
  return nodes
 }

Now that we’ve associated all the data we need, we’ve got co-ordinates that are hopefully neatly packed, we can assign these values to the four html attributes of each div.

origNodes
    .attr('data-x',function(d){return d.x})
    .attr('data-y',function(d){return d.y})
    .attr('data-rotate',function(d){return d.rot})
    .attr('data-scale',function(d){return d.scale;});

And now, we can call impress, to take these div’s and make them pretty! we call impress().init() to kick everything off, and there’s a impress().next() function that allows us to set the time between slides.

impress().init();
 
 (function(){
    impress().next();
    setTimeout(arguments.callee, 15000);
 })();

And that’s it!

I hope this walkthrough has been helpful. Feel free to take whatever I’ve done and play. Writing this up has mostly been so I don’t forget, but I’d like to hear if you found it helpful.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: