Hacking KeyLines

This page is from our blog archive

It's still useful, but it's worth searching for up-to-date information in one of our more recent blog posts.

A slightly belated happy New Year to all of you reading this blog post! We hope 2015 is a fantastic year for you all.

If you logged onto our website over the New Year weekend, you will have seen what we believe to be the finest display of Graph Visualization Fireworks the internet has ever witnessed:

The KeyLines fireworks display

In this blog post, we’ll reveal how we used the KeyLines API to create the fireworks, and give examples of how the SDK can be used to achieve some creative effects. We hope it will inspire you to look again at some of the extensive functionality you might not have used before.

  1. The concept
  2. Defining the firework object
  3. Launching the firework
  4. Banging the firework
  5. Starting the fireworks display
  6. Adding the Bang!

The concept

We started with the idea of turning the so-called ‘fireworks effect’ (too many nodes, moving too quickly) into something visually interesting.

KeyLines allows us to define the X and Y position of nodes. We can also cluster nodes in a hub and spoke arrangement. By combining these two, it’s possible to add a node cluster to a random X co-ordinate, animate it moving upwards to a random Y co-ordinate and then ungroup as the firework explodes.

With some sound effects, a black background and some colorful nodes and links, we should have a recognizable fireworks display.

Simple enough?

Defining the firework object

The first thing to do is to define our initial firework objects in a valid KeyLines format.

In order to let us easily create multiple fireworks, we will create a helper function createFirework. This function will take 3 variables: c (the colour of the firework), x (initial x co-ordinate), y (initial y co-ordinate):

function createFirework (c, x, y) {
  var nodes = [], links = [], i;
  var off = randomOff();
// the number of nodes we want in our firework (end points of the explosion)
  var numNodes = 30; 
  for (i = 0; i < numNodes; i++) {
    nodes.push({
      type: 'node',
      id: 'node' + nodeId++,
      // first node is transparent (it is the centre of the firework), other fireworks are a light colour
      c: i === 0 ? 'rgba(0,0,0,0)' : '#EBE5BB',
      // store the intended colour of the firework for when it explodes
      d: {c: c}, 
      // shrink the size of the node
      e: 0.1,
      x: x,
      y: y
    }); 
  }
  // root node is our central point in the firework
  var rootNodeId = nodes[0].id;
  // create links between all other nodes to the central node
  for (i = 1; i < numNodes; i++) {
    links.push({
      type: 'link',
      id: 'link' + linkId++,
      id1: rootNodeId,
      id2: nodes[i].id,
      off: off,
      d: {c: c}, 
      c: 'rgba(0, 0, 0, 0)'
    }); 
  }
  return {nodes: nodes, links: links};
}

One thing worth explaining at this point is the randomly generated off parameter added to every link. This offsets a curve on the link and we use this randomly to make some fireworks explode in an interesting spiral.

A firework spiral effect, created using curves of links

Launching the firework

Now that we can create firework objects, we should define a function which will animate these fireworks, launching from the ground and animating upwards. We create a function animateFirework which takes a single firework as a parameter (remember that a firework is defined as an object with a group of nodes and links).

function animateFirework (firework) {
// create a y co-ordinate of the destination for the firework before it explodes (somewhere between the ground and the top of the chart)                                                                                                                                                                       
  var y = Math.floor(Math.random() * chart.viewOptions().height);
  var afterAnim = _.map(firework.nodes, function (item) {
    return {id: item.id, y: y};
  });
  // animate the firework launching upwards
  chart.animateProperties(afterAnim, {queue: false, time: 1000}, function () {
    
// slightly nasty underscore usage to change the colour of all nodes and links in this firework to their random explosion colour
chart.setProperties(_.map(_.flatten(_.rest(firework.nodes).concat(firework.links)), function (item) {
      return {id: item.id, c: alphaColour(item.d.c, 1)};
    }));
    // we will define the bangFirework method later
    bangFirework(firework);
  });
  // also a launching sound will be played when the firework is launched from the ground (to be defined later)
  launchSound();
}

One useful option we are using in chart.animateProperties is queue: false, this allows us to perform multiple animations on the chart simultaneously.

Banging the firework

Once the cluster of nodes in our firework has reached its terminal height, we need to de-cluster them. To do this we use a new function called bangFirework:

function bangFirework (firework) {
 // evenly space out the exploded nodes in a circle
  var increment = 360 / (firework.nodes.length - 1);
// define an explosion radius
  var radius = Math.floor(Math.random()*200) + 50;
  var x, y;
// starting angle for the first node of the explosion
  var angle = Math.floor(Math.random() * 360);
  var props = [];
  var node;
// explode out each node
  for (var i = 1; i < firework.nodes.length; i++) {
    node = chart.getItem(firework.nodes[i].id);
// new x and y co-ordinates for the exploded node on a circle
    x = node.x + (radius * Math.sin(angle * Math.PI / 180));
    y = node.y + (radius * Math.cos(angle * Math.PI / 180));
// properties to be animated
    props.push({id: node.id, x: x, y: y});
    angle += increment;
    if (angle > 360) {
      angle = angle - 360;
    }
  }
// animate the explosion for this firework and then make it fade out with the killFirework function
  chart.animateProperties(props, {time: 1000, queue: false}, function () {
    killFirework(firework);
  });
  bangSound();
  chart.ping(firework.nodes[0].id, {repeat: 0, c: 'white', r: 400});
}

function killFirework (firework) {
  // succumb to gravity and fade away
  var node;
  var props = [];
  for (var i = 1; i < firework.nodes.length; i++) {
    node = chart.getItem(firework.nodes[i].id);
    props.push({id: node.id, y: node.y + 15, c: 'rgba(0,0,0,0)'});
  }
  for (var i = 0; i < firework.links.length; i++) {
    props.push({id: firework.links[i].id, c: 'rgba(0,0,0,0)'});
  }
  chart.animateProperties(props, {queue: false});
}

Starting the fireworks display

So, we’ve defined all the functions we need to create, launch and explode our fireworks.

Let’s tie it all together and generate many fireworks at once, launching them at random intervals.

We’ll limit the number of fireworks to 50, and their colors to a range of realistic firework colors. Fortunately, someone on the internet has already defined what they are. See Maggie Koerth’s infographic on “Where the colors of fireworks come from”.

var colours = [
  'rgba(220, 239, 255, 0.1)', // electric white
  'rgba(233, 127, 0, 0.1)', // orange
  'rgba(217, 0, 20, 0.1)', // bright red
  'rgba(59, 152, 224, 0.1)', // turquoise
  'rgba(79, 25, 128, 0.1)', // purple
  'rgba(200, 201, 203, 0.1)', // silver sparkle
  'rgba(66, 171, 50, 0.1)', // green
  'rgba(207, 201, 183, 0.1)', // gold
  'rgba(249, 216, 0, 0.1)' // yellow
];

We’ve also added some text at the end to wish the viewer a Happy New Year:

// store a reference to viewOptions so that we can get view co-ordinates
var viewOptions = chart.viewOptions();

  var fireworks = [];
// we are going to have 50 fireworks in total
  for (var i = 0; i < 50; i++) {
  
// create a firework with random colour, random x value and a y value at the bottom of the chart  fireworks.push(createFirework(colours[Math.floor(Math.random()*colours.length)], Math.floor(Math.random()*viewOptions.width), viewOptions.height));
  }
// load the items into KeyLines (some underscore functions to flatten the fireworks data structure)
  chart.load({type: 'LinkChart', items: _.flatten(_.map(fireworks, function (firework) { return firework.nodes.concat(firework.links); }))}, function () {
// once loaded, we will animate all of the fireworks
    animateFireworks(fireworks, function () {
      setTimeout(function () {
// show a happy new year message once the fireworks display is finished        
chart.merge([{type: 'node', fb: true, fbc: 'rgba(0,0,0,0)', x: viewOptions.width / 2, y: viewOptions.height / 2, fc: 'white', fs: '40', t: 'Happy New Year!', id: 'endMessage'}]);
      }, 2000);
    });
  });

The next function, called animateFireworks, launches an array of fireworks at scheduled times to create a display effect:

// the starting time in ms between consecutive launch of each firework
var launchRate = 1500;
// the time interval in ms by which we will slowly reduce the launchRate time
var launchRateFactor = 80;
function animateFireworks (fireworks, callback) {
  setTimeout(function () {
    var random = Math.random();
    // randomly launch 1, 2 or 3 fireworks at once
    var numToLaunch = random > 0.5 ? (random > 0.8 ? 3 : 2) : 1;
  // make sure we don’t try to launch more fireworks than exist!
    numToLaunch = numToLaunch > fireworks.length ? fireworks.length : numToLaunch;
    if (fireworks.length < 45 && fireworks.length > 25) {
     // decrease the launchRate (the interval between launching fireworks decreases)
      launchRate -= (launchRateFactor * numToLaunch);
    }
    for (var i = 0; i < numToLaunch; i++) {
      // finally call our previously defined animateFirework function on a single firework
      animateFirework(fireworks.pop());
    }
    if (fireworks.length > 0) {
     // if more fireworks exist in our array, recursively call this function
      animateFireworks(fireworks, callback);
    } else {
      callback();
    }
  }, launchRate);
}

Adding the Bang!

It wouldn’t be an authentic fireworks display without sound.

For these rockets there are 2 main sounds we want to produce: the launch sound of the rocket and then another sound for when the firework explodes in the air (the bang sound).

To do this we will use the HTML5 audio capabilities. We downloaded 2 sound files: bang.mp3 and launch.mp3 and added them to the html page:

<audio src=’sfx/bang.mp3’ class=’bang’></audio>
<audio src=’sfx/launch.mp3’ class=’launch’></audio>

Note: there is a caveat, which we crudely worked around: a single audio tag can only be played successively, you cannot play from the same audio tag simultaneously. This is a shame because we want to have multiple launch sounds/bang sounds going off at once for every firework. To get around this we simply create 10+ of each audio element on the html page. Then when we want to play a sound we can loop through the elements, playing from the next freely available audio tag as necessary.

Quite simply we store a reference to all of the audio elements (bang and launch). Then we loop through each tag to play the next sibling, this allows us to play multiple sounds at once:

  bangElems = $('.bang');
  launchElems = $('.launch');

var bangIndex = 0;
function bangSound () {
  bangElems.get(bangIndex).play();
  bangIndex = (bangIndex > bangElems.length - 2) ? 0 : bangIndex + 1;
}     
        
var launchIndex = 0;
function launchSound () {
  launchElems.get(launchIndex).play();  
  launchIndex = (launchIndex > launchElems.length - 2) ? 0 : launchIndex + 1;
}  

And we’re done! All the excitement of a live fireworks display in your favorite graph visualization tool. Surely the best way to start 2015!

Feel free to get in touch if you have any questions.

How can we help you?

Request trial

Ready to start?

Request a free trial

Learn more

Want to learn more?

Read our white papers

“case

Looking for success stories?

Read our case studies

Registered in England and Wales with Company Number 07625370 | VAT Number 113 1740 61
6-8 Hills Road, Cambridge, CB2 1JP. All material © Cambridge Intelligence 2022.
Read our Privacy Policy.