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.
- Building the app layout with flex
- Listing the countries data
- Basic list selection and calculation
- Styling the radio and checkbox list
- Drawing the chart
- Adding the info panel and global app style
Preparing the project
Before we get started the project, let’s prepare several files. They are:
- Gulpfile.coffee
- retinalize.coffee
- shape.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.
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.
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.
- In the
#appin HTML, we add the following elements.index.html 1<divid='app'class='container vertical'>2<divclass='charts container'>3<div><canvaswidth="300"height="150"></canvas></div>4<div><canvaswidth="300"height="150"></canvas></div>5</div>6<divclass='container'>7<divclass='list container vertical'>8<pclass='description'>Area:<spanclass='output1'>0</span>K km<sup>2</su\9p></p>10<ulid='countries-on-left'>11<li>ListItem</li>12<!--lotsoflisttimes-->13<li>ListItem</li>14</ul>15</div>16<divclass='list container vertical'>17<pclass='description'>Area:<spanclass='output2'>0</span>K km<sup>2</su\18p></p>19<ulid='countries-on-right'>20<li>ListItem</li>21<!--lotsoflisttimes-->22<li>ListItem</li>23</ul>24</div>25</div>26</div> - The minimal flex-based layout.
app.css 1/* Minimal flex grid */2.container{3display:flex;4}5.container.vertical{6flex-direction:column;7}8.container>*{9flex:11auto;10border:1pxsolidgreen;/* debug */11}12.container.shrink{13flex:01auto;14} - 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 */23html,body{4width:100%;5height:100%;6}7#app{8width:100%;9height:100%;10background:IVORY;11} - 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 */2canvas{3max-width:100%;4min-height:150px;5}67.charts{8min-height:150px;9}1011/* Area Description */12p.description{13min-height:30px;14} - Finally, we make the long list
overflow:scrolland enable the momentum scrolling.app.css 1/* List */2ul{3overflow-x:hidden;4overflow-y:scroll;5-webkit-overflow-scrolling:touch;6list-style:none;7padding: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.
- 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
ulelements into the following.index.html 1<ulid='countries-on-left'>2<liclass='template'><label><inputtype='radio'name='target-country'><spanc\3lass='name'>China</span></label></li>4</ul>5...6<ulid='countries-on-right'>7<liclass='template'><label><inputtype='checkbox'name='countries[]'><spanc\8lass='name'>China</span></label></li>9</ul> - 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 1this.app?={}2this.app.data?={}34#Areaofcommoncountries5#Sourcefromhttp://simple.wikipedia.org/wiki/List_of_countries_by_area6this.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... - We have the data now, the next step is to render the data into the HTML list.
view.coffee 1this.app?={}23this.app.renderList=->4#List5template=$('#countries-on-left').find('.template')6countriesOnLeft=$('#countries-on-left')7forcountryinapp.data.areaOfCountries8clone=template.clone().removeClass('template')9clone.find('input[type="radio"]').val(country.area)10clone.find('.name').text(country.name)11countriesOnLeft.appendclone12#Removetemplateaftercloningdone.13template.remove()1415template=$('#countries-on-right').find('.template')16countriesOnLeft=$('#countries-on-right')17forcountryinapp.data.areaOfCountries18clone=template.clone().removeClass('template')19clone.find('input[type="checkbox"]').val(country.area)20clone.find('.name').text(country.name)21countriesOnLeft.appendclone22#Removetemplateaftercloningdone.23template.remove() - At last, we render the list in the
app.coffee.app.coffee 1this.app?={}23this.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.
- In the
view.coffee, we add a function that check the input changes and display the calculation.view.coffee 1this.app.handleListChange=->2#ToggleChart13$('input[type="radio"]').change->4value=$('input[type="radio"]:checked').val()5$('.output1').text(Math.round(value))678#ToggleChart29$('input[type="checkbox"]').change->10sum=011$('input[type="checkbox"]:checked').each->12sum+=$(this).val()*113$('.output2').text(Math.round(sum)) - In the
app.coffeefile, we register the input changes handling by calling the function by the end of the logic.app.coffee 1this.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
- All the customization are done is CSS. Add the following style in the
app.cssfile.app.css 1/* Styling Radio and Checkbox */2input[type='radio'],3input[type='checkbox']{4display:none;5}67input[type='radio']+.name,8input[type='checkbox']+.name{9display:block;10font-size:1rem;11padding:1rem.5rem;12padding-left:2rem;13border:1pxsolidtransparent;14border-left:0;15border-right:0;16transition:all.3sease-out;17}1819input[type='radio']:checked+.name,20input[type='checkbox']:checked+.name{21border-color:DARKORANGE;22}2324/* Radio specific */25input[type='radio']+.name{26background:url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/radio.svg)\2710px50%no-repeat;28background-size:16px;29}30input[type='radio']:checked+.name{31background:SNOWurl(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/radio\32-checked.svg)10px50%no-repeat;33background-size:16px;34}3536/* Checkbox specific */37input[type='checkbox']:checked+.name{38background:SNOWurl(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/check\39ed.svg)10px50%no-repeat;40background-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
- We add the
#chart1-canvasand#chart2-canvasid to the canvas, so that our JavaScript logic can reference them.index.html 1<divclass='charts container'>2<div><canvasid="chart1-canvas"width="300"height="150"></canvas></div>3<div><canvasid="chart2-canvas"width="300"height="150"></canvas></div>4</div> - The chart logic is encapsulated into the
chart.coffeefile.chart.coffee 1this.app?={}23#alias4cjs=createjs56classthis.app.Chart7#Entrypoint.8constructor:(canvasId)->9@stage=newcjs.Stage(canvasId)1011utility.retinalize@stage,false12@canvasWidth=utility.originalCanvasWidth1314drawChart:(value)->15@stage.removeAllChildren()16#eachtile=10Kkm217areaForEachTile=10018tileWidth=tileHeight=1019margin=520numberOfTiles=value/areaForEachTile21cols=Math.floor((@canvasWidth-margin)/(tileWidth+margin))2223foriin[0...numberOfTiles]24x=margin+Math.floor(i%cols)*(tileWidth+margin)25y=margin+Math.floor(i/cols)*(tileHeight+margin)26shape=newRectShape27fillColor:'ORANGERED'28width:tileWidth29height:tileHeight30x:x31y:y32@stage.addChildshape3334#Drawoncanvas35@stage.update() - In the
handleListChangefunction in view, we update the code to call the chart to draw the value.view.coffee 1this.app.handleListChange=(chart1,chart2)->2#ToggleChart13$('input[type="radio"]').change->4value=$('input[type="radio"]:checked').val()5$('.output1').text(Math.round(value))6chart1.drawChart(value)78#ToggleChart29$('input[type="checkbox"]').change->10sum=011$('input[type="checkbox"]:checked').each->12sum+=$(this).val()*113$('.output2').text(Math.round(sum))14chart2.drawChart(sum) - Finally, we create the chart instance and tell the view to draw the chart after any input changes.
app.coffee 1chart1=newapp.Chart("chart1-canvas")2chart2=newapp.Chart("chart2-canvas")34this.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
- We add an
#info-btnthat trigger the info panel.index.html 1<divid="info-btn">2<ahref="#info"><span>Info</span></a>3</div> - The
#info-panelcontains basic content.index.html 1<divid='info-panel'>2<h1>CountriesArea</h1>3<p>Thistoolletyoucomparetheareaofsomecountries.</p>4<p>Selectcountriesonbothlistandcomparethem.Eachtileinthechartis\5100Kkm<sup>2</sup></p>6<pclass='more-space'>Tapanywheretobegin.</p>7<p><small>Note:Weonlylistseveralcountriesinthisdemo.<br>Thesourcei\8sfrom<ahref="http://simple.wikipedia.org/wiki/List_of_countries_by_area">wiki\9pedia</a>.</small></p>10</div> - The
info-btnsits on the top right corner.app.css 1/* Info Button */2#info-btn{3position:absolute;4top:0;5right:0;6}7#info-btna{8width:44px;9height:44px;10display:block;11background:url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/15649/info.svg);\1213}14#info-btnaspan{15display:none;16} - The
#info-panelis full screen that rotate into the view in 3D.app.css 1/* Info panel */2.hidden{3transform:rotateY(-90deg);4}5.show{6transform:rotateY(0);7}8#info-panel{9position:absolute;10top:0;11left:0;12width:100%;13height:100%;14background:ORANGERED;15color:white;1617transform-origin:00;18transition:all.3sease-out;1920display:flex;21flex-direction:column;22justify-content:center;23align-items:center;24text-align:center;25}26#info-panela{27color:white;28}29#info-panelp{30margin:.5em;31}32p.more-space{33margin:2em;34} - For the rotate 3D effect, we add the
perspectiveto body. We also fine tune the global style here.app.css 1body{2perspective:700px;3font-family:Verdana,sans-serif;4font-size:12px;56padding:5px;7background:ORANGERED;8} - Finally, make sure we have removed the debugging style.
app.css 1.container>*{2border:1pxsolidgreen;/* 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.