Project 4 – Countries Area

In this project, we create an app that draws a chart by using canvas and CreateJS.

This example demonstrated both multiple select list and single item select list.

The multiple select list is done via check box with array as name. The single item selection list is done by using the radio buttons.

The beauty of using these two basic elements is that it works perfect without any css and Javascript. The css and Javascript is here to enhance the select list. But the core thing still works without these enhancement.

Why is this project awesome?

This project you will learn tobmake momentum list. Learn to customize the radio and checkbox. Learn to draw basic chart using the create js library. Also learn to use flex box to create the entire app layout.

The project is divided into the following steps.

  1. Building the app layout with flex
  2. Listing the countries data
  3. Basic list selection and calculation
  4. Styling the radio and checkbox list
  5. Drawing the chart
  6. Adding the info panel and global app style

Preparing the project

Before we get started the project, let’s prepare several files. They are:

  1. Gulpfile.coffee
  2. retinalize.coffee
  3. shape.coffee
Gulpfile.coffee
 1 gulp.task 'js', ->
 2   gulp.src [
 3     './app/scripts/data.coffee'
 4     './app/scripts/retinalize.coffee'
 5     './app/scripts/shape.coffee'
 6     './app/scripts/chart.coffee'
 7     './app/scripts/view.coffee'
 8     './app/scripts/app.coffee'
 9   ]
10   .pipe coffee()
11   .pipe concat 'app.js'
12   .pipe gulp.dest './app/scripts/'

We make the retinalize class is ready.

 1 this.utility ?= {}
 2 
 3 this.utility.retinalize = (stage, updateCSS=true) ->
 4   canvas = stage.canvas
 5   utility.originalCanvasWidth = canvas.width
 6   utility.originalCanvasHeight = canvas.height
 7 
 8   return unless window.devicePixelRatio
 9 
10   ratio = window.devicePixelRatio      
11 
12   height = canvas.getAttribute('height')
13   width = canvas.getAttribute('width')
14 
15   canvas.setAttribute 'width', Math.round( width * ratio )
16   canvas.setAttribute 'height', Math.round( height * ratio )
17 
18   if updateCSS
19     canvas.style.width = width+"px"
20     canvas.style.height = height+"px"
21 
22   stage.scaleX = stage.scaleY = ratio

Then we have another utility class that draws rectangle shape.

shape.coffee
 1 # alias
 2 cjs = createjs
 3 
 4 class this.DefaultShape extends cjs.Shape
 5   constructor: (@options={}) ->
 6     super()
 7     @initialize()
 8     @applyOptions()
 9   applyOptions: ->
10     @options.fillColor ?= null
11     @options.strokeColor ?= null
12     @options.strokeWidth ?= 1
13     @options.width ?= 100
14     @options.height ?= 100
15     @options.x ?= 0
16     @options.y ?= 0
17     return @options
18 
19 # Shapes
20 # options:
21 # width, height, fillColor, strokeColor, strokeWidth
22 class this.RectShape extends this.DefaultShape
23   constructor: (options={}) ->
24     super(options)        
25     @graphics
26       .setStrokeStyle @options.strokeWidth
27       .beginFill @options.fillColor
28       .beginStroke @options.strokeColor
29       .drawRect 0, 0, @options.width, @options.height
30     @x = @options.x
31     @y = @options.y

We’ll need the RectShape when we draw the chart in canvas.

There is basic CSS reset in the app.css file.

app.css
1 /* Basic reset */
2 html, body, p, ul, li, h1, h2, h3, div {
3   margin: 0;
4   padding: 0;
5 }
6 
7 * {
8   box-sizing: border-box;
9 }

Step 1 – Building the app layout with flex

In this task, we build the basic layout by using the flex box. We will create our own minimal flex layout styles. Every elements with class .container will treat as flex display. All their children would have flex:1 1 auto by default, unless .shrink class presents.

Time for Actions

Let’s follow the steps to create the app layout by using flex box.

  1. In the #app in HTML, we add the following elements.
    index.html
     1  <div id='app' class='container vertical'>
     2    <div class='charts container'>
     3      <div><canvas width="300" height="150"></canvas></div>
     4      <div><canvas width="300" height="150"></canvas></div>
     5    </div>  
     6    <div class='container'>
     7      <div class='list container vertical'>  
     8        <p class='description'>Area: <span class='output1'>0</span>K km<sup>2</su\
     9 p></p>
    10        <ul id='countries-on-left'>
    11          <li>List Item</li>
    12          <!-- lots of list times -->
    13          <li>List Item</li>
    14        </ul>
    15      </div>
    16      <div class='list container vertical'>      
    17        <p class='description'>Area: <span class='output2'>0</span>K km<sup>2</su\
    18 p></p>
    19        <ul id='countries-on-right'>
    20          <li>List Item</li>
    21          <!-- lots of list times -->
    22          <li>List Item</li>
    23        </ul>
    24      </div>  
    25    </div>
    26  </div>
    
  2. The minimal flex-based layout.
    app.css
     1  /* Minimal flex grid */
     2  .container {
     3    display: flex;
     4  }
     5  .container.vertical {
     6    flex-direction: column;
     7  }
     8  .container > * {
     9    flex: 1 1 auto;
    10    border: 1px solid green; /* debug */
    11  }
    12  .container .shrink {
    13    flex: 0 1 auto;
    14  }
    
  3. For the flex to work perfectly, we give a width and height to the container, which is the HTML and body element in this case.
    app.css
     1  /* Global */
     2 	
     3  html, body {  
     4    width: 100%;
     5    height: 100%;
     6  }
     7  #app {
     8    width: 100%;
     9    height: 100%;
    10    background: IVORY;
    11  }
    
  4. The flex layout will change the dimension on the children elements. We can specific a minimal width and height so the flex layout will keep a minimal space for the elements.
    app.css
     1  /* Canvas */
     2  canvas {
     3    max-width: 100%;
     4    min-height: 150px;
     5  }
     6 	
     7  .charts {
     8    min-height: 150px;
     9  }
    10 	
    11  /* Area Description */
    12  p.description {
    13    min-height: 30px;
    14  }
    
  5. Finally, we make the long list overflow:scroll and enable the momentum scrolling.
    app.css
    1  /* List */
    2  ul {
    3    overflow-x: hidden;
    4    overflow-y: scroll;
    5    -webkit-overflow-scrolling: touch;
    6    list-style: none;
    7    padding: 5px;
    8  }
    

We created a minimal flex based layout.

For every .container class, we display the children as flex items.

The children inside the container has flex: 1 1 auto by default.

 1 .container
 2 +----------------------------------------------------+
 3 | .container               .container                |
 4 | +----------------------+ +-----------------------+ |
 5 | |                      | |                       | |
 6 | |                      | |                       | |
 7 | |                      | |                       | |
 8 | |                      | |                       | |
 9 | |                      | |                       | |
10 | +----------------------+ +-----------------------+ |
11 |                                                    |
12 +----------------------------------------------------+
13 |                                                    |
14 |                                                    |
15 |                                                    |        .container {
16 |                                                    |          display: flex;
17 |                                                    |        }
18 |                                                    |
19 |                                                    |
20 
21 |                                                    |
22 +----------------------------------------------------+

When the content exceeds the DOM container, we can use overflow scroll to make the content scroll inside the container. But this scroll wont have the momentum scrolling which common in the touch device. We need to add he the webkit scrolling to enable the momentum scrolling.

Step 2 – Listing the countries data

In this task, we obtain the list of countries areas and render them into the 2 lists we create in last step.

  1. In last task, we created a long list to test the layout. Now we don’t need that list anymore because we are rendering the list dynamically. Replace the 2 ul elements into the following.
    index.html
    1  <ul id='countries-on-left'>
    2    <li class='template'><label><input type='radio' name='target-country'><span c\
    3 lass='name'>China</span></label></li>
    4  </ul>
    5  ...
    6  <ul id='countries-on-right'>
    7    <li class='template'><label><input type='checkbox' name='countries[]'><span c\
    8 lass='name'>China</span></label></li>
    9  </ul>
    
  2. The country data is obtained from Wikipedia. We put them into an array of object. For each object, we store the country name and the area.
    data.coffee
     1  this.app ?= {}
     2  this.app.data ?= {}
     3 	
     4  # Area of common countries
     5  # Source from http://simple.wikipedia.org/wiki/List_of_countries_by_area
     6  this.app.data.areaOfCountries = [
     7    {name: 'China', area: 9651.747}
     8    {name: 'Russia', area: 17098.242}
     9    {name: 'Canada', area: 9889.000}
    10    {name: 'USA', area: 9826.675}
    11    {name: 'Australia', area:9596.691}
    12    ...
    
  3. We have the data now, the next step is to render the data into the HTML list.
    view.coffee
     1  this.app ?= {}
     2 	
     3  this.app.renderList = ->
     4    # List
     5    template = $('#countries-on-left').find('.template')
     6    countriesOnLeft = $('#countries-on-left')
     7    for country in app.data.areaOfCountries  
     8      clone = template.clone().removeClass('template')
     9      clone.find('input[type="radio"]').val(country.area)
    10      clone.find('.name').text(country.name)
    11      countriesOnLeft.append clone
    12    # Remove template after cloning done.
    13    template.remove()
    14 	
    15    template = $('#countries-on-right').find('.template')
    16    countriesOnLeft = $('#countries-on-right')
    17    for country in app.data.areaOfCountries  
    18      clone = template.clone().removeClass('template')
    19      clone.find('input[type="checkbox"]').val(country.area)
    20      clone.find('.name').text(country.name)
    21      countriesOnLeft.append clone
    22    # Remove template after cloning done.
    23    template.remove()
    
  4. At last, we render the list in the app.coffee.
    app.coffee
    1  this.app ?= {}
    2 	
    3  this.app.renderList()
    

What just happened?

Some JavaScript tutorial may show you to render HTML directly inside the JavaScript.

I prefer using the template approach where the template element of the radio list item and checkbox list item are defined inside the HTML. When I need it, I clone the template and update the data inside.

In this example, we use the template at the initial stage and we don’t need it after the setup logic, so we can remove the clone after the list creation. In some projects, we may need the template at unknown time after the project is setup. In this case, we can hide all the template elements by using .template{display:none}.

Step 3 – Basic list selection and calculation

In this task, we handle the radio and checkboxes clicking and calculate the sum of area of selected countries.

Time for Action

Let’s follow the steps to handle the radio and checkbox selection.

  1. In the view.coffee, we add a function that check the input changes and display the calculation.
    view.coffee
     1  this.app.handleListChange = ->
     2    # Toggle Chart 1 
     3    $('input[type="radio"]').change ->
     4      value = $('input[type="radio"]:checked').val()
     5      $('.output1').text(Math.round(value))
     6 	
     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))
    
  2. In the app.coffee file, we register the input changes handling by calling the function by the end of the logic.
    app.coffee
    1  this.app.handleListChange()
    

What just happened?

We handled the checkbox and radio selection.

The logic is based on the pseudo class :checked to get the HTML element of the checked input.

Step 4 – Styling the radio and checkbox list

In this project, we customize the radio and checkbox styles.

Time for Action

  1. All the customization are done is CSS. Add the following style in the app.css file.
    app.css
     1  /* Styling Radio and Checkbox */
     2  input[type='radio'],
     3  input[type='checkbox']{
     4    display: none;    
     5  }
     6 	
     7  input[type='radio'] + .name,
     8  input[type='checkbox'] + .name{
     9    display: block;
    10    font-size: 1rem;  
    11    padding: 1rem .5rem;
    12    padding-left: 2rem;
    13    border: 1px solid transparent;
    14    border-left: 0;
    15    border-right: 0;
    16    transition: all .3s ease-out;  
    17  }
    18 	
    19  input[type='radio']:checked + .name,
    20  input[type='checkbox']:checked + .name{
    21    border-color:DARKORANGE ;    
    22  }
    23 	
    24  /* Radio specific */
    25  input[type='radio'] + .name {
    26    background: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/radio.svg)\
    27  10px 50% no-repeat; 
    28    background-size: 16px;
    29  }
    30  input[type='radio']:checked + .name {
    31    background: SNOW url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/radio\
    32 -checked.svg) 10px 50% no-repeat;
    33    background-size: 16px;
    34  }
    35 	
    36  /* Checkbox specific */
    37  input[type='checkbox']:checked+.name {
    38    background: SNOW url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/check\
    39 ed.svg) 10px 50% no-repeat;
    40    background-size: 16px;
    41  }
    

What just happened?

We customize the radio and checkbooks by hiding the default browser rendered radio and checkbooks button. Then we use the label to customize the button graphic

The label works because clicking in the label is identical to clicking on the input. That means when we click on the label, we are toggling the real radio boxes and check boxes.

So we can define the :checked style in the css where we rely to customize our graphics

We used the SVG format for the checkbox and radio box graphics. We could use png. The reason we use SVG is because the vector format scale better on the retina display and look sharper than using png format.

Step 5 – Drawing the chart

In this task, we draw the chart by using canvas.

Time for Action

  1. We add the #chart1-canvas and #chart2-canvas id to the canvas, so that our JavaScript logic can reference them.
    index.html
    1  <div class='charts container'>
    2    <div><canvas id="chart1-canvas" width="300" height="150"></canvas></div>
    3    <div><canvas id="chart2-canvas" width="300" height="150"></canvas></div>
    4  </div> 
    
  2. The chart logic is encapsulated into the chart.coffee file.
    chart.coffee
     1  this.app ?= {}
     2 	
     3  # alias
     4  cjs = createjs
     5 	
     6  class this.app.Chart
     7    # Entry point.
     8    constructor: (canvasId)->
     9      @stage = new cjs.Stage(canvasId)
    10 	
    11      utility.retinalize @stage, false
    12      @canvasWidth = utility.originalCanvasWidth
    13 	
    14    drawChart: (value) ->    
    15      @stage.removeAllChildren()
    16      # each tile = 10K km2
    17      areaForEachTile = 100
    18      tileWidth = tileHeight = 10
    19      margin = 5
    20      numberOfTiles = value / areaForEachTile
    21      cols = Math.floor((@canvasWidth-margin) / (tileWidth + margin))
    22 	
    23      for i in [0...numberOfTiles]      
    24        x = margin + Math.floor(i % cols) * (tileWidth + margin)
    25        y = margin + Math.floor(i / cols) * (tileHeight + margin)
    26        shape = new RectShape
    27          fillColor: 'ORANGERED'
    28          width: tileWidth
    29          height: tileHeight      
    30          x: x
    31          y: y
    32        @stage.addChild shape
    33 	
    34      # Draw on canvas
    35      @stage.update()
    
  3. In the handleListChange function in view, we update the code to call the chart to draw the value.
    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)
    
  4. Finally, we create the chart instance and tell the view to draw the chart after any input changes.
    app.coffee
    1  chart1 = new app.Chart("chart1-canvas")
    2  chart2 = new app.Chart("chart2-canvas")
    3 	
    4  this.app.handleListChange(chart1, chart2)
    

    <—————-+ canvas_width +————————————>
    +——————————————————————–+ | | | +–+ +–+ +–+ +–+ +–+ +–+ +–+ +–+ +–+ +–+ +–+ | | | | | | | | | | | | | | | | | | | | | | | | | | +–+ +–+ +–+ +–+ +–+ +–+ +–+ +–+ +–+ +–+ +–+ | | | | | tile_width | | | | | + | | margin | | | | |   | | | | | | | tiles_per_row = (canvas_width - margin) / (tile_width + margin) | | | | | | x = index % tiles_per_row | | | | y = Math.floor(index / tiles_per_row) | | | | | | | | | +——————————————————————–+

Step 6 – Adding the info panel and global app style

In the last step, we add an information page and fine tune the global styles.

The info panel is presented by CSS 3d that rotate in from the left screen.

Time for Action

  1. We add an #info-btn that trigger the info panel.
    index.html
    1  <div id="info-btn">
    2    <a href="#info"><span>Info</span></a>
    3  </div>
    
  2. The #info-panel contains basic content.
    index.html
     1  <div id='info-panel'>
     2    <h1>Countries Area</h1>
     3    <p>This tool let you compare the area of some countries.</p>
     4    <p>Select countries on both list and compare them. Each tile in the chart is \
     5 100K km<sup>2</sup></p>
     6    <p class='more-space'>Tap anywhere to begin.</p>
     7    <p><small>Note: We only list several countries in this demo. <br>The source i\
     8 s from <a href="http://simple.wikipedia.org/wiki/List_of_countries_by_area">wiki\
     9 pedia</a>.</small></p>
    10  </div>
    
  3. The info-btn sits on the top right corner.
    app.css
     1  /* Info Button */
     2  #info-btn {
     3    position: absolute;
     4    top: 0;
     5    right: 0;
     6  }
     7  #info-btn a{
     8    width: 44px;
     9    height: 44px;
    10    display: block;  
    11    background: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/info.svg);\
    12       
    13  }
    14  #info-btn a span {   
    15    display: none;
    16  }
    
  4. The #info-panel is full screen that rotate into the view in 3D.
    app.css
     1  /* Info panel */
     2  .hidden {
     3    transform: rotateY(-90deg);
     4  }
     5  .show {
     6    transform: rotateY(0);
     7  }
     8  #info-panel {
     9    position: absolute;
    10    top: 0;
    11    left: 0;
    12    width: 100%;
    13    height: 100%;
    14    background: ORANGERED;
    15    color: white;
    16 	
    17    transform-origin: 0 0;
    18    transition: all .3s ease-out;
    19 	
    20    display: flex;
    21    flex-direction: column;
    22    justify-content: center;
    23    align-items: center;  
    24    text-align: center;
    25  }
    26  #info-panel a {
    27    color: white;  
    28  }
    29  #info-panel p {
    30    margin: .5em;    
    31  }
    32  p.more-space {
    33    margin: 2em;
    34  }
    
  5. For the rotate 3D effect, we add the perspective to body. We also fine tune the global style here.
    app.css
    1  body {
    2    perspective: 700px;
    3    font-family: Verdana, sans-serif;
    4    font-size: 12px;
    5 	
    6    padding: 5px;
    7    background: ORANGERED;
    8  }
    
  6. Finally, make sure we have removed the debugging style.
    app.css
    1  .container > * {
    2    border: 1px solid green; /* debug */
    3  }
    

What just happened?

We have created a panel transition by using CSS 3D effects.

Summary

We learnt a lot in the chapter. We created a simple app that let user select countries and display their area in a tile bar chart. In conclusion, after reading this chapter, you should be able to:

  • Build app layout by using CSS flex box.
  • Draw basic chart by using canvas and CreateJS library.