Animated bar chart with CreateJS on HTML5 Canvas

A week a ago I saw really geeky video on Internet Explorer YouTube channel promoting the HTML5 game “Bored to death” made by Microsoft developers that was basing on CreateJS Suit library. That video inspired me to take alook one more at HTML5 Canvas element, that I was treating like unwanted child of changing technology due to fact i was poorly supported by most of the browsers.

Following the creators of the video I decided to use CreateJS Suit dedicated for creating games and animations basing on its components:

  • EaselJS – core component for canvas manipulation
  • TweenJS - library for tweening and animating HTML5 and Javascript
  • SoundJS – API for manipulating audio
  • LoadJS – asset  loader and coordinator

To be honest I was surprised that huge corporations like Adobe and Microsoft support this little Open Source project. Having such a good impression, my expectations was high. So I’ve started to coding and made this tutorial.

Below you have the source code and live demo:

source code link

Download the source code

live demo link

See it working in the browser

Implementation

This tutorial will strongly rely on CoffeeScript. If you are not familiar with this meta-language you can read my articles why I have given up on Javascript and how to make good Coffee or visit CoffeeScript homepage and get some basics there.

Lets start with simple HTML5 template including canvas with set width, height and id:

<!DOCTYPE html>
  <head>
    <!-- Included needed dependencies -->
    <script src='http://code.jquery.com/jquery-1.9.0.min.js'></script>
    <script src='http://code.createjs.com/easeljs-0.5.0.min.js'></script>
    <script src='http://code.createjs.com/tweenjs-0.3.0.min.js'></script>
    <!-- I will use the browser CoffeeScript compiler. In production compile it to JavaScript -->
    <script src='http://jashkenas.github.com/coffee-script/extras/coffee-script.js'></script>
  </head>
  <body style='background: #8bd3fc;'>
    <canvas id='graph' style='margin: 50px auto; display: block;' width='900' height='400' ></canvas>
    <script type='text/coffeescript'>
      'use strict'
      <!-- Our script goes here -->
    </script>
  </body>
</html>

Chart

Next we’re gonna implement our core class Chart. We are gonna work on sample JSON data grabbed form Facebook that you can download here:

Link to download file - tutorial webdesign

Download sample JSON

The implementation of AJAX request grabbing the data is shown blow:

class Chart
  _barOffset = null

  constructor: (canvas, offset) ->
    # Init stage
    @canvas = canvas
    @stage = new createjs.Stage(canvas)
    _barOffset = offset

    # Init ticker and enable mouse events
    @stage.enableMouseOver(15)
    createjs.Ticker.setFPS(30)
    @tickerListener = createjs.Ticker.addListener(@stage,false)

  # Download JSON data via AJAX
  loadData: (url) =>
    $.ajax({
      url: url,
      dataType: 'json',
      cache: true
    }).complete((data) => @parseData(data.responseText))

  parseData: (data) =>

$(->
  new Chart($("#graph")[0], 8).loadData('json/data.json')
)

Preloader

As you might notice downloading the data is pretty time taking, so let’s add Preloader class before our data will be parsed:

class Preloader
  constructor: (stage) ->
    @stage = stage

    # The preloader will be put into container
    # for easier maipulation
    @container = new createjs.Container()
    @container.x = @stage.canvas.width / 2 - 65
    @container.y  = @stage.canvas.height / 2 - 20
    return @

  initialize: =>
    for i in [0...3]
      # Draw rectangles
      rect = new createjs.Shape()
      rect.graphics.beginFill("#fff").drawRect(0,0,10,10)
      rect.x = 10+i*25
      rect.y = if i==1 then 3 else 4

      # Add Facebook-like animation to rectangles
      createjs.Tween.get(rect)
        .wait(i*200)
      createjs.Tween.get(rect, {loop:true})
        .wait(i*300)
        .to({
          scaleY: if i==1 then 1.8    else 1.3,
          scaleX: if i==1 then 1.8    else 1.3,
          y:      if i==1 then 0      else 2,
          x:      if i==1 then 7+i*25 else 9+i*25
        },700)
        .to({
          scaleY:1,
          scaleX:1,
          y: if i==1 then 3 else 4,
          x: 10+i*25
        },600)

    # Add some simple preloader text
    preloaderText = new createjs.Text(
      "Loading the data...", 
      "bold 10px Arial", 
      "#4d7a93"
    )
    preloaderText.textAlign = "center"
    preloaderText.x = 42.5
    preloaderText.y = 22
    @container.addChild(preloaderText, rect)
    return @

  addToStage: =>
    @stage.addChild(@container)
    return @

  removeFromStage: =>
    # Some funky fade out
    createjs.Tween
      .get(@container)
      .to({alpha:0}, 300)
      .call( => @stage.removeChild(@container))
    return @

With that done we can init our Preloader in constructor of Chart class:

  # Init preloader
    @preloader = new Preloader(@stage).initialize().addToStage()
createjs bar chart preloader tutorial

Now our Preloader is working in browser.

Its time to parse our data. We are gonna do this by implementing parseData method in Chart class function and right after that lets remove our Preloder to prepare the canvas to display the chart data:

class Chart
  # Contains list of all bars
  _bars = []
  _barOffset = null
  _barWidth = null

  constructor: (canvas, offset) ->
    ...

  # Download data via AJAX
  loadData: (url) =>
    ...

  # Parse downloaded HTML to extract the data
  parseData: (data) =>
    data = JSON.parse(data)
    for item, index in data.items
      # Here we will push our bars 
      # to _bars array for easier modifications
    
    # Remove preloader and draw the graph
    @preloader.removeFromStage()

Axis

Lest begin with coding the background of chart by displaying axis attached directly to the Stage

class Chart
  ...

  # Parse downloaded HTML to extract the data
  parseData: (data) =>
    ...
    @drawBackground()

  # Draw background lines and labels
  drawBackground: ->
    for i in [0..5]
      # Create dotted lines in background
      dotLine = new createjs.Shape()
      dotLine.graphics.beginFill('#fff')
      for j in [0...100]
        x = (j / 100 * (@canvas.width)) + 35
        y = @canvas.height - ~~(((@canvas.height / 5) * (5-i)) + 65)
        dotLine.graphics.drawCircle(x,y,1)
        dotLine.graphics.closePath()

      # Create percentage on left side
      lineLabel = new createjs.Text((5-i)*25 + '%', "bold 10px Arial", "#033a59")
      lineLabel.textAlign = "right"
      lineLabel.x = 30
      lineLabel.y = @canvas.height - ~~(((@canvas.height / 5) * (5-i)) + 72)
      @stage.addChildAt(dotLine, 1)
      @stage.addChildAt(lineLabel, 1)
Now after parsing the data our browser shows us very simple percentage axis

After parsing the data, browser shows us very simple percentage axis

Bars

We are moving to the most difficult part: rendering the bars. It requires modifying Chart class and Bar class as shown below:

class Bar
  _front = null
  _label = null
  _desc = null
  _offset = null

  constructor: (value, desc, barNumber, offset, stage) ->
    @value = value
    @desc = desc
    @stage = stage
    @barNumber = barNumber
    _offset = offset

    @barContainer = new createjs.Container()
    @barContainer.y = @stage.height - 39
    return @

  initialize: (barMaxHeight, barWidth) =>
    # Create the front of the bar
    _front = new createjs.Shape()
    # Set scaleY to 0 to make them 'grow', by animating this property
    _front.scaleY = 0
    _front.x = @barNumber * (barWidth + _offset)
    _front.y = 0
    _front.graphics
      .beginFill('#fff')
      .drawRect(0, 0, barWidth, ~~(barMaxHeight*(@value / 100)*-1))
      .beginFill(createjs.Graphics.getHSL(180-@value*3, 100, 43))
      #color by value (0 -> red, 40 -> yellow, 80 -> green)
      .drawRect(0, ~~(barMaxHeight*(@value / 100)*-1) - 4, barWidth, 4)
      .closePath()

    # Create the label of the bar
    _label = new createjs.Text(@value + "%", "bold 10px Arial", "#033a59");
    _label.textAlign = "center";
    _label.rotation = -90
    _label.x = @barNumber * (barWidth + _offset) - 2
    _label.y = -25
    _label.alpha = 0
    # Tween it to grow with bars at initialization
    createjs.Tween.get(_label)
      .wait(@barNumber * 200 + 500)
      .to({y:(-(barMaxHeight)*(@value / 100)) - 30}, 1200)

    # Create the desc of the bar
    _desc = new createjs.Text(@desc, "bold 10px Arial", "#4d7a93")
    _desc.textAlign = "right"
    _desc.rotation = -45
    _desc.x = @barNumber * (barWidth + _offset) - 5
    _desc.y = -5
    _desc.alpha = 0
    createjs.Tween.get(_desc)
      .wait(@barNumber * 200 + 500)
      .to({alpha:1}, 1200)


    # Add children keeping order
    @barContainer.addChildAt(_front, 0)
    @barContainer.addChildAt(_label, 1)
    @barContainer.addChildAt(_desc,  2)

  addToStage: () ->
    @stage.addChild(@barContainer)
    return @

To make modifications easier bars will be stored inside the Container:

class Chart
  ...

  constructor: (canvas, offset) ->
    ...

    # Init bar conatainer
    @container = new createjs.Container()
    @container.x = 45
    @container.y = 0
    @container.height = @stage.canvas.height - 25
    @container.width = @stage.canvas.width - 45
    @stage.addChild(@container)

  # Parse downloaded HTML to extract the data
  parseData: (data) =>
    data = JSON.parse(data)
    for item, index in data.items
      # Populate the bar array with data
      _bars.push(new Bar(item.value, item.desc, index, _barOffset, @container))

    _barWidth = ((@container.width - ((_bars.length - 1) *_barOffset)) / _bars.length)

    # Remove preloader and draw the graph
    @preloader.removeFromStage()
    @drawBackground()
    @drawBars()

  # Draw background lines and labels
  drawBackground: ->
    ...

  drawBars: ->
    for bar, i in _bars
      bar
        .initialize(@container.height, _barWidth)
        .addToStage()

Bar animations

The last thing that has to be done is the animation of the bars. Lets implement the animate function, add scaleY property, onMouseOver and onMouseOut events:

class Bar
  ...

  initialize: (barMaxHeight, barWidth) =>
    # Set scaleY to 0 to make them 'grow', by animating this property
    _front.scaleY = 0

    # Bind hover events to display bars label
    @barContainer.onMouseOver = (e) =>
      children = e.target.children
      # Dont tween if already animated
      if(!children[1].hasActiveTweens)
        createjs.Tween.get(children[1])
          .to({alpha:1}, 400)

    @barContainer.onMouseOut = (e) =>
      label = e.target.children[1]
      createjs.Tween.get(label)
        .to({alpha:0}, 400)
    return @

  animate: ->
    createjs.Tween.get(_front)
      .wait(@barNumber*200+500)
      .to({scaleY:1},1200)
    return @

Finally lets fire our animation:

class Chart
  ...

  drawBars: ->
    for bar, i in _bars
      bar
        .initialize(@container.height, _barWidth)
        .addToStage()
        .animate()
createjs bar chart tutorial final effect preview

Everything is working now! Silky and smooth.

Conclusions

There’s just a few things i would like to mention as my conclusions. It’s still very hard to work with HTML5 canvas. It requires lots of testing almost non-debugable code and to come to satisfying effects tremendous amount time is required. If I had an option to choose between Flash and HTML5 canvas, I’d still choose Flash because he would save most of my time with much easier debugging and guarantee the cross-browser compatibility.

If you read it, you should share it: