Click here to Skip to main content
Click here to Skip to main content

Right Before Your Eyes

By , 13 Dec 2013
Rate this:
Please Sign up or sign in to vote.

Introduction

A rich and responsive client interface can be created using Knockout.js, Bootstrap, and Google Charts.

This is a weekly wage calculator that categorizes expenses and calculates hourly, weekly, and yearly wage estimates. It is responsive, and recalculates instantly using Knockout.js while displaying the results graphically using Google Charts. It is a SPA consisting of an HTML page that is formatted using Bootstrap.

screenshot

Background

A client wanted a way to show port truck drivers that their expenses were eating up their pay checks. Drivers spend a lot of time waiting in their cabs and on their phones. The client wanted an attention grabbing and engaging way for the drivers to discover the facts themselves. Knockout.js, Bootstrap, and JQuery provided the means and Google chart helped communicate the message.

Using the Code

The SPA page is organized logically into sections, with each section serving a separate purpose. The sections might be defined as:

  • Resources
  • Variable Definitions
  • Chart Definitions
  • HTML
  • View Model
  • Utilities

The first section of the page makes use of Content Delivery Networks (CDNs) to provide JQuery, Bootstrap, Knockout.js, and Google Charts. These script references are included in the Head section of the HTML document.

After those a style tag sets the CSS for the page. This is optional. It could reference a CSS file instead.

Knockout.js is the key to making the page responsive. When a user changes the hours worked, the amount paid, or any expense, the page recalculates. It is responsive. The values for those items are stored in variables. And those variables are tracked by Knockout.js. Variables that are tracked by Knockout are called observables. They are observed by Knockout.js.

The expense categories of the application are defined and stored in arrays. Each expense category has a name, a code, and an array of subcategory names. We have category codes to make it easier to total by category. We do not have subcategory codes because we are not doing any calculations by subcategory. We start the category codes at 2 because we are reserving category 1 for take home pay.

var expenseCategories = [
{
    "expenseSubCategories": [
      {
        "name": "Truck"
      },
      {
        "name": "Yard Space"
      },
      {
        "name": "Parking"
      },
      {
        "name": "Radio"
      },
      {
        "name": "Other"
     } 
    ], "name": "Lease and Rent",
    "categoryCode": 2
},  

Expenses can be weekly, monthly, or yearly. We need to be able to convert the expense to weekly and yearly amounts. We create an array to store the time periods and their coefficients.

We then define the charts. We do this here because they are referenced in our HTML and must be defined first. In this application, we have two gauge charts and a pie graph. The pie graph records the expenses by category and the gauges measure hourly and yearly wages.

Google Chart downloads chart building information depending on the type of chart. Pie charts use the “corechart” package. Once loaded, it runs a callback. We will have the callback draw our chart. Charts use Data Tables to store data. And we will use arrays to populate the chart data Data Tables. We define our chart data before we draw our chart. Our chart data array is named “expenseCategoryChartData”. Notice how the first item in the array stores the column headings for our data.

var expenseCategoryChartData = [['Category', 'Count'],
['Take Home',takeHome],['Lease and Rent', leaseExpense],
['Maintenance', maintenanceExpense],['Fuel', fuelExpense],
['Insurance', insuranceExpense],['Fees and Fines', feeExpense]];

google.load("visualization", "1", { packages: ["corechart"] });

google.setOnLoadCallback(drawCategoryChart);

function drawCategoryChart() {
var pieData = google.visualization.arrayToDataTable(expenseCategoryChartData);
var pieOptions = {
backgroundColor: "#BE6527", //Note: set to match page css
chartArea: {left:"10%",top:"5%",  
width:"70%",height:"25%"},
legend: {position: 'left'},
slices: [ {color: '#168E10', offset: 0.2 },{color: '#C5161F'},{color: '#FFA500'},
{color: '#499DDA'},{color: '#88108E'},
{color: '#10558E'}] //Note: set to match page color scheme
};
var pieChart = new google.visualization.PieChart
    (document.getElementById('chartCategories'));
pieChart.draw(pieData, pieOptions);
}

We begin our chart drawing function by converting our array into a Data Table variable “pieData”. We also define various options for our pie chart. We have made the rectangular chart background color the same as the page background to make it blend in to the page. And we have set a color scheme for the chart colors to complement the background color.

We define the pie chart and provide a parameter that is the item id for the chart on the HTML page, in this case “chartCategories”. Next, we provide the data and options and draw the chart.

We follow the same procedure for our two gauge charts as well.

Next is our HTML. Here is where we begin to make use of Knockout and add the markup that will help give us responsiveness.

We will begin with 2 Div markers. The first is just a name. The second defines a container.

<div class='liveExample'> 
<div class="container">

<div class="row">
<table width='100%'>
    <td width='60%' >
    <form role="form">
    <fieldset>
        <table width='100%'>
        <thead>
        <tr><div class="col-md-6"><th >
        <h3>Weekly Pay</h3></th></div></tr>
        <tr>
                <div class="col-md-1">
                <th class='quantity' >Hours Worked</th></div>                
                <div class="col-md-1">
                <th class='price' >Paid</th></div>
                <div class="col-md-1">
                <th class='price text-right'  >Per Hour</th></div>
            </tr>
        </thead>
        <tbody>
            <tr>
                <div class="col-md-1"><td class='quantity' >
                <input  data-bind="value: hoursWorked" 
                class="form-control"  /></td></div>
                <div class="col-md-1"><td class='price' >
                <input  data-bind="value: amountPaid" class="
                form-control" /></td></div>                
                <div class="col-md-1"><td class='price text-right'  >
                <label  data-bind="text: formatCurrency(grossPerHour())"  >
                </label></td></div>
            </tr>
        </tbody>
        </table>

The “row” class is Bootstrap and describes the standard way Bootstrap handles rows across multiple devices and orientations. The form tag with role=form and the fieldset tags are used by Knockout. Here we use tables as a way to divide the presentation of the page into 4 sections. The upper left is where hours and pay are input. To the right is where the calculated totals are displayed. Below those are where individual expenses are added and displayed. And below that is where the charts and graphs are displayed.

The “col-” classes in the HTML define the widths using Bootstrap CSS. However, the “price” and “quantity” classes are used by Knockout. Those classes help tell Knockout what sort of numbers and formatting are needed by those items. It is good practice to also use those classes on the headings. Also note that the data entry fields are class “form-control”.

Knockout likes tables and makes use of “thead” and “tbody” tags. We are using “text-right” from Bootstrap to right align what we display. The “data-bind” elements within tags identify those items as observables by Knockout. For instance, the variable “grossThisPeriod” will be recalculated if our hours or pay change. The value is then formatted by a function.

The Expenses section of the document is more involved. Here, we have a select for categories and a dependent select for subcategories.

<tbody  data-bind='foreach: expenseItemLines' >
    <tr>
        <td  ><select data-bind="options: expenseCategories, 
        optionsText: 'name', optionsCaption: 'Select...', 
        value: expenseCategory" > </select></td>
        <td data-bind="with: expenseCategory">
            <select data-bind="options: expenseSubCategories, 
            optionsText: 'name', optionsCaption: 'Select...', 
            value: $parent.expenseSubCategory "> </select></td>
        <td class='price' ><input  data-bind="value: 
        expenseAmount" class="form-control" /></td>                 
        <td ><select  data-bind="options: expensePeriods, 
        optionsText: 'expensePeriodName', 
        value: expensePeriod" ></select></td>
<div class="col-md-1"><td  class='price    text-right' 
data-bind="text: 
formatCurrency(expenseSubtotal())" ></td></div> 
        <td class='text-right' ><a href='#' 
        data-bind='click: removeExpenseLine'>Remove</a></td>
    </tr>
</tbody>

Most importantly, we use just one row for our table and have Knockout take charge of duplicating those rows to allow us to add expenses. We can also remove those expenses. And Knockout recalculates everything for us.

Each row in our table will display a line item expense. We are naming the collection of them “expenseItemLines” and use the tbody tag to data-bind them. The foreach indicates that we will be able to iterate over the collection.

The first select is fairly straightforward. It utilizes the array we defined at the top of our document and returns the category code. The dependent select is very similar. The “with: “ keyword establishes the dependence. And the “$parent.” in the value section provides navigation.

We have a form-control input, another select to get the expense period coefficient, the calculated weekly amount of the expense, and finally a link to a function to remove the line item expense.

Below the row definition is a button to add a blank row to expenseItemLines and a field to display the total expenses for the week.

And finally at the end of the HTML, we have the layout and ids for the charts.

So far, we have defined things and done layout. Now comes functionality.

Our application functionality comes from a Knockout view model that we are calling “appViewModel”. The view model defines the observables and their behavior. In our application, we also have line item expenses. We create a model within our view model for those, called “expenseItemLine”.

function AppViewModel(data) {
    this.hoursWorked = ko.observable();
    this.amountPaid = ko.observable();
    this.grossPerHour = ko.computed(function() {
        if (this.hoursWorked() > 0 && this.amountPaid() > 0) {
            var amount = (this.grossPerPeriod() / this.hoursWorked());
            return amount;    
        }
        else {
            return 0;
        }
    }, this
    );

We begin our View model by defining our inputs as Knockout observables. We next define the variables that depend on our inputs. We want to make sure there is data for our calculations so we have added validation checks to make sure we do not do things like divide by zero. And we return zero if there is no data. This is helpful for Google chart which generates errors if fed bad data.

Next, we have chosen to define our array of expense line items. We use the observable grandTotal with a callback function that calculates our expenses. We begin by resetting our totals, otherwise they will just grow and grow. We use a JQuery $.each to iterate over our expense array. We do a check to make sure there is a category to assign the expense, and that there is an amount. We then call a function to add to the category totals that populate the array that our pie chart uses for data. After iterating through the expenses, we redraw the charts and display the total.

Now that we have gross hours and pay and the total expense amount for the period, we can perform the net calculations. Again we will perform validation checks to make sure there is data or return 0 if there is not. We reset totals and redraw charts as needed.

We need methods for our view model. We need to be able to add an expense and remove an expense so we create functions for those. We also define a variable to model our expenseItemLine. It is akin to a data record and we define observables for each field. The subtotal field is calculated so we perform validation checks and perform the calculation which we return.

self.expenseItemLines = ko.observableArray
    ([new expenseItemLine()]); // Put one line in by default

self.grandTotal = ko.computed(function() {
    resetExpenseCategoryTotals();
    var total = 0;
    $.each(self.expenseItemLines(), function() { 
        if (this.expenseCategory() != null)
        {
            var selectedCategory = this.expenseCategory().categoryCode;
            var expenseAmountThisPeriod = this.expenseSubtotal();
            if (expenseAmountThisPeriod > 0) {
                updateExpenseCategoryTotal
                (selectedCategory, expenseAmountThisPeriod);
            }
        }
        total += this.expenseSubtotal() 
    })
    redrawCharts();
    return total;
});

this.netPerPeriod = ko.computed(function() {
    if (this.amountPaid() > 0)
    {
        var netPeriod = (this.amountPaid() - self.grandTotal());
        resetTakeHomeCategoryTotal();
        updateExpenseCategoryTotal(1, netPeriod);
        return netPeriod;    
    }
    else
    {
        return 0;
    }
}, this);

We want the expense subcategory select to depend on the category selection. To achieve this, we subscribe the subcategory to the category. Now a change in category changes the subcategory options.

Knockout's applyBindings instantiates our view model.

After the Knockout code, we have some utilities that reset our totals that we use for chart data and redraws our charts. These are specific to our application.

The last function is very helpful because Google Chart sometimes likes to reduce a chart's size with a page refresh. Without that function, if you keep refreshing your page, the charts will shrink until they cannot be read. The function resets their size to normal.

$(window).resize(function(){
        redrawCharts();
});

Points of Interest

This is a stand alone SPA but Knockout and the other technologies could also be used to make AJAX calls and update or be updated by a database. Hopefully, this project showed both the ease and advantages of using responsiveness from products like Knockout.js, Bootstrap, Jquery, and Google Charts.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

dgDavidGreene

United States United States
David Greene is an application developer in Southern California.

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web01 | 2.8.140421.2 | Last Updated 13 Dec 2013
Article Copyright 2013 by dgDavidGreene
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid