Project 4B – Drawing charts on canvas

In this project, we modify previous chapter to draw different types of chart by using the canvas tag and CreateJS library. We will also animate the chart by using the TweenJS, which is part of the CreateJS suite.

Mission Checklist

  1. Animated bar chart
  2. Animated tiles
  3. Pie chart
  4. Animated pie chart

Preparation

In the chart.coffee, we have following alias declared for easier access to the CreateJS, TweenJS and the Ease library.

chart.coffee
1 # alias
2 cjs = createjs
3 Ease = cjs.Ease
4 Tween = cjs.Tween

Animated Bar Chart

There is not much difference in this project and previous project, in terms of app structure and logic flow. The main difference is in the chart.coffee where we draw different type of chart.

  1. We prepare the structure of Chart class.
    chart.coffee
     1  this.app ?= {}
     2 	
     3  # alias
     4  cjs = createjs
     5  Ease = cjs.Ease
     6 	
     7  class this.app.Chart
     8    # Entry point.
     9    constructor: (canvasId)->
    10      @stage = new cjs.Stage(canvasId)
    11      cjs.Ticker.setFPS(60);
    12      cjs.Ticker.addEventListener 'tick', @stage
    13 	
    14      utility.retinalize @stage, false
    15      @canvasWidth = utility.originalCanvasWidth
    16      @canvasHeight = utility.originalCanvasHeight    
    17      @initChart()
    18 	
    19    initChart: ->
    20      @stage.removeAllChildren()    
    21      # other init chart code later
    22 	
    23    drawChart: (value, refValue = 0) ->    
    24      # draw the chart code later
    
  2. When we init the chart, we create the rectangle shape and referencing line. They are put into this (@) scope for the drawChart to access.
    chart.coffee
     1  initChart: ->
     2      @stage.removeAllChildren()    
     3      margin = 5
     4 	
     5      @shape = new RectShape
     6        fillColor: 'ORANGERED'
     7        width: @canvasWidth - margin
     8        height: 1
     9        x: margin
    10        y: @canvasHeight
    11      @stage.addChild @shape    
    12      @refLine = new RectShape
    13        fillColor: 'RED'
    14        width: @canvasWidth - margin
    15        height: 1
    16        x: margin
    17        y: @canvasHeight
    18      @stage.addChild @refLine
    
  3. We have drew the shape in the initChart method. The drawChart method is actually used to calculate the chart dimension based on the provided value. At last, we animate the shape to the target position and dimension by using TweenJS.
    chart.coffee
     1  drawChart: (value, refValue = 0) ->    
     2      areaForEachTile = 100
     3      scaleY = value / areaForEachTile
     4      y = @canvasHeight - scaleY
     5      cjs.Tween.get(@shape).to({scaleY, y}, 400, Ease.quartOut)
     6 	
     7      refY = @canvasHeight - (refValue / areaForEachTile)
     8      cjs.Tween.get(@refLine).to({y: refY}, 400, Ease.quartOut)
     9 	
    10      # Draw on canvas
    11      @stage.update()
    
  4. We changed the drawChart method arguments. Now we need the reference value for positioning the reference line. So we update the view logic where it toggles the drawChart method.
    view.coffee
     1  this.app.handleListChange = (chart1, chart2)->
     2    # Toggle Chart 1 and 2
     3    $('input[type="radio"], input[type="checkbox"]').change ->
     4      # Chart 1
     5      value = $('input[type="radio"]:checked').val()
     6      $('.output1').text(Math.round(value))
     7      chart1.drawChart(value)      
     8      # Chart 2
     9      sum = 0
    10      $('input[type="checkbox"]:checked').each ->
    11        sum += $(this).val()*1
    12      $('.output2').text(Math.round(sum))
    13      chart2.drawChart(sum, value)
    

What just happened?

We used the TweenJS the first time. Here is the syntax:

1 craetejs.Tween.get(anyObject).to({property:newValue, }, duration, easeFunction)

Animated tiles chart

  1. Similar to the last example, we prepare the chart.coffee file with the basic structure: constructor, initChart and drawChart.
    chart.coffee
     1  class this.app.Chart
     2    # Entry point.
     3    constructor: (canvasId)->
     4      @stage = new cjs.Stage(canvasId)
     5 	
     6      cjs.Ticker.setFPS(60)
     7      cjs.Ticker.addEventListener 'tick', @stage
     8 	
     9      utility.retinalize @stage, false
    10      @canvasWidth = utility.originalCanvasWidth
    11      @canvasHeight = utility.originalCanvasHeight
    12 	
    13      @initChart()
    14      @lastNumberOfTiles = 0
    15 	
    16 	
    17    initChart: ->
    18      @stage.removeAllChildren()    
    19      # Init code later
    20 	
    21 	
    22    drawChart: (value) ->  
    23      # Code later
    
  2. The initChart method will draw all the tiles in the canvas area. By default all the tiles has 0 scaling so they are not visible at the beginning.
    chart.coffee
     1  initChart: ->
     2    @stage.removeAllChildren()    
     3    tileWidth = tileHeight = 10
     4    margin = 5
     5    leadingMargin = margin + tileWidth / 2
     6 	
     7    chartArea = (@canvasWidth-margin-leadingMargin) * (@canvasHeight-margin-leadi\
     8 ngMargin)
     9    tileArea = (tileWidth + margin) * (tileHeight + margin)    
    10    @maxNumberOfTiles = Math.floor( chartArea / tileArea )    
    11    cols = Math.floor((@canvasWidth-margin) / (tileWidth + margin))
    12 	
    13    @shapes = []
    14    for i in [0...@maxNumberOfTiles]      
    15      x = leadingMargin + Math.floor(i % cols) * (tileWidth + margin)
    16      y = leadingMargin + Math.floor(i / cols) * (tileHeight + margin)
    17      shape = new RectShape
    18        fillColor: 'ORANGERED'
    19        width: tileWidth
    20        height: tileHeight      
    21        x: x
    22        y: y        
    23      @stage.addChild shape
    24      @shapes.push shape
    25 	
    26      shape.regX = tileWidth/2
    27      shape.regY = tileHeight/2
    28      shape.scaleX = shape.scaleY = 0
    
  3. The drawChart method doesn’t draw the chart. It actually toggle the scaleX and scaleY of the existing tiles. It will scale up or down the tiles based on the given value.
    chart.coffee
     1  drawChart: (value) ->  
     2    # each tile = 100K km2
     3    areaForEachTile = 100
     4    numberOfTiles = Math.floor(value / areaForEachTile)    
     5    for i in [0...@maxNumberOfTiles]
     6      if i < numberOfTiles
     7        delay = (i - @lastNumberOfTiles) * 5
     8        Tween.get(@shapes[i]).wait(delay).to({scaleX:1, scaleY:1}, 400, Ease.quar\
     9 tOut)
    10      else
    11        delay = (i - @lastNumberOfTiles) * 2
    12        Tween.get(@shapes[i]).wait(delay).to({scaleX:0, scaleY:0}, 400, Ease.quar\
    13 tOut)
    14    @lastNumberOfTiles = numberOfTiles
    
  4. In the view.coffee, we ensure the logic to toggle both chart is correct as the following. It is the same as the project 4 code.
    view.coffee
     1  this.app.handleListChange = (chart1, chart2)->
     2    # Toggle Chart 1 
     3    $('input[type="radio"]').change ->
     4      value = $('input[type="radio"]:checked').val()
     5      $('.output1').text(Math.round(value))
     6      chart1.drawChart(value)  
     7 	
     8    # Toggle Chart 2
     9    $('input[type="checkbox"]').change ->
    10      sum = 0
    11      $('input[type="checkbox"]:checked').each ->
    12        sum += $(this).val()*1
    13      $('.output2').text(Math.round(sum))
    14      chart2.drawChart(sum)
    

What just happened?

We have created a nice animated tile-based bar chart.

Static pie chart

  1. There is only 1 pie chart. We change the HTML to contain 1 chart only.
    index.html
    1  <div class='charts'>
    2    <canvas id="chart-canvas" width="150" height="150"></canvas>
    3  </div>
    
  2. We also center align the canvas.
    app.css
    1  #chart-canvas {
    2    display: block;
    3    margin: auto;
    4  }
    
  3. The pie chart is different from the bar chart we have created.
    chart.coffee
     1  class this.app.Chart
     2    # Entry point.
     3    constructor: (canvasId)->
     4      @stage = new cjs.Stage(canvasId)
     5      cjs.Ticker.setFPS(60);
     6      cjs.Ticker.addEventListener 'tick', @stage
     7 	
     8      utility.retinalize @stage, false
     9      @canvasWidth = utility.originalCanvasWidth
    10      @canvasHeight = utility.originalCanvasHeight    
    11      @initChart()    
    12    initChart: ->
    13      @stage.removeAllChildren()    
    14 	
    15    drawChart: (leftValue, rightValue) ->   
    16      x = @canvasWidth / 2
    17      y = @canvasHeight / 2
    18      r = 50
    19 	
    20      globalRotation = -90 * Math.PI / 180
    21 	
    22      percentage = rightValue / (leftValue + rightValue)
    23      splitDegree = percentage * 360
    24 	
    25      #Arc 1
    26      startAngle = 0 * Math.PI / 180 + globalRotation
    27      endAngle = splitDegree * Math.PI / 180 + globalRotation       
    28      shape = new cjs.Shape()
    29      shape.graphics
    30        .beginFill "GOLD"
    31        .moveTo(x, y)
    32        .arc(x, y, r, startAngle, endAngle)
    33        .lineTo(x, y)
    34 	
    35      @stage.addChild shape
    36 	
    37      # Arc 2
    38      startAngle = splitDegree * Math.PI / 180 + globalRotation
    39      endAngle = 360 * Math.PI / 180 + globalRotation 
    40 	
    41      shape = new cjs.Shape()
    42      shape.graphics
    43        .beginFill "ORANGERED"
    44        .moveTo(x, y)
    45        .arc(x, y, r, startAngle, endAngle)
    46        .lineTo(x, y)
    47 	
    48      @stage.addChild shape
    
  4. We changed to 1 chart, so we also change the handleListChange method to toggle the chart with both values form left and right list.
    view.coffee
     1  this.app.handleListChange = (chart)->
     2    # Toggle Chart
     3    $('input[type="radio"], input[type="checkbox"]').change ->
     4      # Left
     5      value = $('input[type="radio"]:checked').val()*1
     6      $('.output1').text(Math.round(value))
     7 	
     8      # Right
     9      sum = 0
    10      $('input[type="checkbox"]:checked').each ->
    11        sum += $(this).val()*1
    12      $('.output2').text(Math.round(sum))
    13 	
    14      # Update Chart
    15      chart.drawChart(value, sum)
    

Animated pie chart

  1. The animated chart.
    chart.coffee
     1  class this.app.Chart
     2    # Entry point.
     3    constructor: (canvasId)->
     4      @stage = new cjs.Stage(canvasId)
     5      cjs.Ticker.setFPS(60);
     6      cjs.Ticker.addEventListener 'tick', @stage
     7 	
     8      utility.retinalize @stage
     9      @canvasWidth = utility.originalCanvasWidth
    10      @canvasHeight = utility.originalCanvasHeight    
    11      @initChart()    
    12    initChart: ->    
    13      @pieData = {
    14        splitDegree: 0 
    15      }    
    16 	
    17    updateChart: (e) =>      
    18      # Code to draw arc shape later
    19 	
    20    drawChart: (leftValue, rightValue) ->       
    21      # Code to start the shape drawing later
    
  2. In the updateChart method, we clear the stage and draw the arc again based on the current splitDegree.
    chart.coffee
     1  updateChart: (e) =>      
     2    x = @canvasWidth / 2
     3    y = @canvasHeight / 2
     4    r = 50
     5    globalRotation = -90 * Math.PI / 180
     6 
     7    @stage.removeAllChildren()
     8 
     9    #Arc 1
    10    startAngle = 0 * Math.PI / 180 + globalRotation
    11    endAngle = @pieData.splitDegree * Math.PI / 180 + globalRotation       
    12    shape = new cjs.Shape()
    13    shape.graphics
    14      .beginFill "GOLD"
    15      .moveTo(x, y)
    16      .arc(x, y, r, startAngle, endAngle)
    17      .lineTo(x, y)
    18 
    19    @stage.addChild shape
    20 
    21    # Arc 2
    22    startAngle = @pieData.splitDegree * Math.PI / 180 + globalRotation
    23    endAngle = 360 * Math.PI / 180 + globalRotation 
    24 
    25    shape = new cjs.Shape()
    26    shape.graphics
    27      .beginFill "ORANGERED"
    28      .moveTo(x, y)
    29      .arc(x, y, r, startAngle, endAngle)
    30      .lineTo(x, y)
    31 
    32    @stage.addChild shape
    
  3. The splitDegree value is actually changing to create the animated effect.
    chart.coffee
    1  drawChart: (leftValue, rightValue) ->
    2        percentage = rightValue / (leftValue + rightValue)
    3    splitDegree = percentage * 360
    4 	
    5    Tween.get(@pieData).to({splitDegree}, 400, Ease.quantOut).addEventListener('c\
    6 hange', @updateChart)
    

What just happened?

We used the TweenJS to animate the pieData object. TweenJS is an independent library that the target is not necessary to be any CreateJS display object. We can provide any object and ask the TweenJS to tween any numeric property. In the change event, we know the changes happened so we can updated our canvas in our own way. In this example, we create a new pie chart based on the changing value.

Summary

Drawing chart is one of the common canvas usage.

Further challenges

We have discussed the usage of gyroscope sensor in the chapter Rain or Not. What if combine what I have learned there with the chart drawing together? Try creating an inspector as following that shows the value history of the sensor.

Rotation Inspector
Rotation Inspector

You can test the real application by using the following links with your devices.

  • Gyroscope Rotation: http://mztests.herokuapp.com/rotation/
  • Accelerometer: http://mztests.herokuapp.com/motion

Optionally, you may download the app in the Play Store.

https://play.google.com/store/apps/details?id=net.makzan.gyroinspcetor&hl=en

The rotation value ranged from -365 to +365. When we use the following chart, the rotation value shows as a history for better inspection.

Chart explanation
Chart explanation

Thanks again for reading the book. Hope you enjoy the example projects and learn to build mobile app with the CreateJS library.

If you find any questions, you’re welcome to contact me for any questions. You can find me at:

  • Website: makzan.net
  • Email: mak@makzan.net
  • Twitter: @makzan