Build a realtime chart with VueJS and Pusher

vuejspusher-1.png

In this tutorial, we will be building an expense and income tracker line chart that updates in realtime using Pusher and VueJS.

Introduction

Data has become a very important part of our lives recently and making sense of that data is equally important. There is no point in having data if you cannot track or analyze it, especially if this data has anything to do with finances ? ? ? In this tutorial, we will be building an expense and income tracker chart, with realtime features using VueJS and Pusher. Our interactive dashboard will have a line chart that displays your income and expenses for each day. You’ll be able to add new expenses and income and see the chart update in real time.

The dashboard chart will be powered by Node.js + Express as the backend server and Vue + vue-chartjs for the frontend bootstrapped by vue-cli.

Here’s a preview of what we’ll be building:

Scaffolding the app with vue-cli

vue-cli is a simple CLI for scaffolding Vue.js projects. We’ll install vue-cli and then use it to bootstrap the app using the webpack template, with the following commands:

1npm install -g vue-cli
2    vue init webpack-simple realtime-chart-pusher

Tip: The webpack-simple template is a simple webpack + vue-loader setup for quick prototyping. You can read more about that here.

Setting up Node.js Server

Next thing to do is set up a server that will help us communicate with Pusher. I’m going to assume that both Node and npm are installed on your system. We will then install the dependencies we will be using for the Node server.

1npm install body-parser express nodemon pusher

Tip: nodemon will watch the files in the directory in which nodemon was started, and if any files change, nodemon will automatically restart your node application.

One more thing, we are going to need an entry point/file for our Node server. We can do that by creating a server.js file in the root of the app.

Pusher Setup

To implement the realtime functionality, we’ll need the power of Pusher. If you haven’t already, sign up for a Pusher account and create a new app. When your new app is created, get your app_id, keys and cluster from the Pusher dashboard.

App Setup

Now that we have a Pusher account, and have installed the dependencies needed for the Node.js backend, let’s get building.

Let’s write code for the server.js file.

1const express = require('express');
2    const path = require('path');
3    const bodyParser = require("body-parser");
4    const app = express();
5    const Pusher = require('pusher');
6
7    const pusher = new Pusher({
8        appId: 'YOUR_APP_ID',
9        key: 'YOUR_APP_KEY',
10        secret: 'YOUR_APP_SECRET',
11        cluster: 'eu',
12        encrypted: true
13    });
14
15    app.use(bodyParser.json());
16    app.use(bodyParser.urlencoded({ extended: false }));
17    app.use(express.static(path.join(__dirname + '/app')));
18
19    app.set('port', (process.env.PORT || 5000));
20
21    app.listen(app.get('port'), function() {
22        console.log('Node app is running on port', app.get('port'));
23    });

Let’s have a look at what’s happening here. We require Express, path, body-parser and Pusher and we initialized express() with app.

We use body-parser to extract the entire body portion of an incoming request stream and expose it on req.body.

Pusher is also initialized with the app credentials and cluster from your dashboard. Make sure to update that, or else the Node server will have no connection to the dashboard. Lastly, the Node server will run on the 5000 port.

Next thing to do is define our app’s route and also add mock data for the expenses and income chart. Update your server.js file with the following.

1let expensesList = {
2        data: [
3            {
4                date: "April 15th 2017",
5                expense: 100,
6                income: 4000
7            },
8            {
9                date: "April 22nd 2017",
10                expense: 500,
11                income: 2000
12            },
13            {
14                date: "April 24th 2017",
15                expense: 1000,
16                income: 2300
17            },
18            {
19                date: "April 29th 2017",
20                expense: 2000,
21                income: 1234
22            },
23            {
24                date: "May 1st 2017",
25                expense: 500,
26                income: 4180
27            },
28            {
29                date: "May 5th 2017",
30                expense: 4000,
31                income: 5000
32            },
33        ]
34    }

First, we have an expensesList object with the data containing expenses and income for particular days.

1app.get('/finances', (req,res) => {
2        res.send(expensesList);
3    });

This route simply sends the expensesList object as JSON. We use this route to get the data and display on the frontend.

1app.post('/expense/add', (req, res) => {
2        let expense = Number(req.body.expense)
3        let income = Number(req.body.income)
4        let date = req.body.date;
5
6        let newExpense  = {
7            date: date,
8            expense: expense,
9            income: income
10        };
11
12        expensesList.data.push(newExpense);
13
14        pusher.trigger('finance', 'new-expense', {
15            newExpense: expensesList
16        });
17
18        res.send({
19            success : true,
20            income: income,
21            expense: expense,
22            date: date,
23            data: expensesList
24        })
25    });

The /expense/add route sure does a lot. It’s a POST route, which means we will be expecting some incoming data (in this case, expense amount and income amount).

We then push this new income and expense to the existing one, after which we also push the updated expensesList to Pusher.

Lastly, we send a JSON as a response to the route, containing the latest income, expense, date and updated expensesList.

Your final server.js should look like this:

1const express = require('express');
2    const path = require('path');
3    const bodyParser = require("body-parser");
4    const app = express();
5    const Pusher = require('pusher');
6
7    const pusher = new Pusher({
8        appId: '338977',
9        key: '3e6b0e8f2442b34330b7',
10        secret: 'bafd22e1acf4f096c8f2',
11        cluster: 'eu',
12        encrypted: true
13    });
14
15    app.use(bodyParser.json());
16    app.use(bodyParser.urlencoded({ extended: false }));
17    app.use(express.static(path.join(__dirname + '/app')));
18
19    app.set('port', (process.env.PORT || 5000));
20
21    let expensesList = {
22        data: [
23            {
24                date: "April 15th 2017",
25                expense: 100,
26                income: 4000
27            },
28            {
29                date: "April 22nd 2017",
30                expense: 500,
31                income: 2000
32            },
33            {
34                date: "April 24th 2017",
35                expense: 1000,
36                income: 2300
37            },
38            {
39                date: "April 29th 2017",
40                expense: 2000,
41                income: 1234
42            },
43            {
44                date: "May 1st 2017",
45                expense: 500,
46                income: 4180
47            },
48            {
49                date: "May 5th 2017",
50                expense: 4000,
51                income: 5000
52            },
53        ]
54    }
55
56    app.get('/finances', (req,res) => {
57        res.send(expensesList);
58    });
59
60    app.post('/expense/add', (req, res) => {
61        let expense = Number(req.body.expense)
62        let income = Number(req.body.income)
63        let date = req.body.date;
64
65        let newExpense  = {
66            date: date,
67            expense: expense,
68            income: income
69        };
70
71        expensesList.data.push(newExpense);
72
73        pusher.trigger('finance', 'new-expense', {
74            newExpense: expensesList
75        });
76
77        res.send({
78            success : true,
79            income: income,
80            expense: expense,
81            date: date,
82            data: expensesList
83        })
84    });
85
86    app.listen(app.get('port'), function() {
87        console.log('Node app is running on port', app.get('port'));
88    });

Building the Frontend (Vue + vue-chartjs)

Most of the frontend work will be done inside the src/components folder. Navigate to that directory and you should see a Hello.vue file. You can either delete that file or rename to Home.vue as we will be needing a Home.vue file inside the components folder.

Before we get started with building the chart and displaying it, there are a couple of things we need to do. Open up the App.vue file in the src folder and replace with the following code:

1<template>
2      <div id="app">
3        <home></home>
4      </div>
5    </template>
6
7    <script>
8    import Home from './components/Home' //We are importing the Home component
9
10    export default {
11      name: 'app',
12      components: {
13        Home
14      }
15    }
16    </script>
17
18    <style>
19    #app {
20      font-family: 'Avenir', Helvetica, Arial, sans-serif;
21      -webkit-font-smoothing: antialiased;
22      -moz-osx-font-smoothing: grayscale;
23      text-align: center;
24      color: #2c3e50;
25      margin-top: 60px;
26    }
27    </style>

We essentially just replaced every instance of Hello with Home since the Hello component no longer exists.
Next, we will install vue-chartjs, momentjs, pusher-js (Pusher’s Javascript library) and axios (We’ll use axios to make API requests). and then add them to the Vue.js app.

1npm install axios vue-chartjs pusher-js moment

Once that’s done, we’ll import axios and register it globally in our app. We can do that by opening the main.js file in the src folder.

1// src/main.js
2    import Vue from 'vue'
3    import App from './App'
4    import axios from 'axios' // we import axios from installed dependencies
5
6    Vue.config.productionTip = false
7
8    Vue.use(axios) // we register axios globally
9
10    /* eslint-disable no-new */
11    new Vue({
12      el: '#app',
13      template: '<App/>',
14      components: { App }
15    })

Now that has been done, let’s create a Vue.js component that will help to display our chart. We are going to use this to specify what type of chart we want, configure its appearance and how it behaves.

We’ll then import this component into the Home.vue component and use it there. This is one of the advantages of vue-chartjs, it works by importing the base chart class, which we can then extend. Let’s go ahead and create that component. Create a new file called LineChart.vue inside the src/components/ folder, open it up and type in this code.

1<script>
2      import {Line, mixins} from 'vue-chartjs' // We specify what type of chart we want from vue-chartjs and the mixins module
3      const { reactiveProp } = mixins
4      export default Line.extend({ //We are extending the base chart class as mentioned above
5        mixins: [reactiveProp],
6        data () {
7          return {
8            options: { //Chart.js options
9              scales: {
10                yAxes: [{
11                  ticks: {
12                    beginAtZero: true
13                  },
14                  gridLines: {
15                    display: true
16                  }
17                }],
18                xAxes: [ {
19                  gridLines: {
20                    display: false
21                  }
22                }]
23              },
24              legend: {
25                display: true
26              },
27              responsive: true,
28              maintainAspectRatio: false
29            }
30          }
31        },
32        mounted () {
33          // this.chartData is created in the mixin
34          this.renderChart(this.chartData, this.options)
35        }
36      })
37    </script>

In the code block above, we imported the Line Chart from vue-chartjs and the mixins module. Chart.js ordinarily does not provide an option for an automatic update whenever a dataset changes but that can be done in vue-chartjs with the help of the following mixins:

  • reactiveProp
  • reactiveData

These mixins automatically create chartData as a prop or data and add a watcher. If data has changed, the chart will update. Read more here.

Also, the this.renderChart() function inside the mounted function is responsible for rendering the chart. this.chartData is usually an object containing the dataset needed for the chart and we’ll get that by including it as a prop in the Home.vue template, this.options contains the options object that determines the appearance and configuration of the chart.

We now have a LineChart component, but how can we see our chart and test its realtime functionality? We do that by adding the LineChart to our Home.vue component as well as subscribing to our Pusher channel via pusher-js.

Open up the Home.vue file and replace the following:

1<template>
2      <div class="hello">
3        <div class="container">
4          <div class="row">
5            <h2 class="title">Realtime Chart with Vue and Pusher</h2>
6            <h3 class="subtitle">Expense and Income Tracker</h3>
7            <!--We are using the LineChart component imported below in the script and also setting the chart-data prop to the datacollection object-->
8            <line-chart :chart-data="datacollection"></line-chart>
9          </div>
10        </div>
11        <div class="container">
12          <div class="row">
13            <form class="form" @submit.prevent="addExpenses">
14              <h4>Add New Entry</h4>
15              <div class="form-group">
16                <label>Expenses</label>
17                <input class="form-control" placeholder="How much did you spend?" type="number" v-model="expenseamount" required>
18              </div>
19              <div class="form-group">
20                <label>Income</label>
21                <input class="form-control" placeholder="How much did you earn?" type="number" v-model="incomeamount" required>
22              </div>
23              <div class="form-group">
24                <label>Date</label>
25                <input class="form-control" placeholder="Date" type="date" v-model="entrydate" required>
26              </div>
27              <div class="form-group">
28                <button class="btn btn-primary">Add New Entry</button>
29              </div>
30            </form>
31          </div>
32        </div>
33      </div>
34    </template>
35
36    <script>
37      import axios from 'axios'
38      import moment from 'moment'
39      import Pusher from 'pusher-js'
40      import LineChart from '@/components/LineChart'
41
42      const socket = new Pusher('APP_KEY', {
43        cluster: 'eu',
44        encrypted: true
45      })
46      const channel = socket.subscribe('finance')
47
48      export default {
49        name: 'home',
50        components: {LineChart},
51        data () {
52          return {
53            expense: null,
54            income: null,
55            date: null,
56            expenseamount: null,
57            incomeamount: null,
58            datacollection: null,
59            entrydate: null
60          }
61        },
62        created () {
63          this.fetchData()
64          this.fillData()
65        },
66        mounted () {
67          this.fillData()
68        },
69        methods: {
70          fillData () {
71          },
72          addExpenses () {
73          },
74          fetchData () {
75          }
76        }
77      }
78    </script>
79
80
81    <!-- Add "scoped" attribute to limit CSS to this component only -->
82    <style scoped>
83
84      .title {
85        text-align: center;
86        margin-top: 40px;
87      }
88      .subtitle {
89        text-align: center;
90      }
91      .form {
92        max-width: 600px;
93        width: 100%;
94        margin: 20px auto 0 auto;
95      }
96      .form h4 {
97        text-align: center;
98        margin-bottom: 30px;
99      }
100
101      h1, h2 {
102      font-weight: normal;
103    }
104
105    ul {
106      list-style-type: none;
107      padding: 0;
108    }
109
110    li {
111      display: inline-block;
112      margin: 0 10px;
113    }
114
115    a {
116      color: #42b983;
117    }
118    </style>

In the code block above, we imported Pusher, axios, momentjs and the newly created LineChart component and also established a connection to Pusher from our clientside Javascript. We added the line-chart component in our template and also initialized Vue along with three functions (fillData, addExpenses, fetchData) in the method object.

Let’s start with the functions and implement them.

fillData

This function gets called immediately the app is mounted and it basically makes an API request to the Node backend ( /finances) and retrieves the expensesList

1fillData () {
2        axios.get('/finances')
3          .then(response => {
4            let results = response.data.data
5
6            let dateresult = results.map(a => a.date)
7            let expenseresult = results.map(a => a.expense)
8            let incomeresult = results.map(a => a.income)
9
10            this.expense = expenseresult
11            this.income = incomeresult
12            this.date = dateresult
13
14            this.datacollection = {
15              labels: this.date,
16              datasets: [
17                {
18                  label: 'Expense',
19                  backgroundColor: '#f87979',
20                  data: this.expense
21                },
22                {
23                  label: 'Income',
24                  backgroundColor: '#5bf8bf',
25                  data: this.income
26                }
27              ]
28            }
29          })
30          .catch(error => {
31            console.log(error)
32          })
33      }

We are making a GET request to the /finances Node.js route which in turn returns the latest expensesList and we then manipulate that data with Javascript’s .map and assign it to various variables.

1## addExpenses
2    addExpenses () {
3      //We first get the new entries via the v-model we defined on the income and expense input tag
4      let expense = this.expenseamount
5      let income = this.incomeamount
6      let today = moment(this.entrydate).format('MMMM Do YYYY') //Formats the date via momentJS
7
8      //Sends a POST request to /expense/new along with the expense, income and date.
9      axios.post('/expense/add', {
10          expense: expense,
11          income: income,
12          date: today
13      })
14        .then(response => {
15          this.expenseamount = ''
16          this.incomeamount = ''
17          //We are bound to new-expense on Pusher and once it detects a change via the new entry we just submitted, we use it to build the Line Chart again.
18          channel.bind('new-expense', function(data) {
19              let results = data.newExpense.data
20
21              let dateresult = results.map(a => a.date);
22              let expenseresult = results.map(a => a.expense);
23              let incomeresult = results.map(a => a.income);
24
25              //The instance data are updated here with the latest data gotten from Pusher
26              this.expense = expenseresult
27              this.income = incomeresult
28              this.date = dateresult
29
30              //The Chart's dataset is updated with the latest data gotten from Pusher
31              this.datacollection = {
32                  labels: this.date,
33                  datasets: [
34                      {
35                          label: 'Expense Charts',
36                          backgroundColor: '#f87979',
37                          data: this.expense
38                      },
39                      {
40                          label: 'Income Charts',
41                          backgroundColor: '#5bf8bf',
42                          data: this.income
43                      }
44                  ]
45              }
46          });
47      })
48    }

The code block above simply utilizes a POST method route to /expense/add to update expensesList (Remember /expense/add route in the Node server sends the updated expensesList to the Pusher Dashboard) along with the income, expense and date data.

It then uses the data gotten from Pusher via channel.bind to build the Line Chart again and adds the new entry automatically to the Chart.

fetchData

This function gets called after the Vue instance is created and it also listens for changes to the Chart’s dataset via Pusher and automatically updates the Line Chart.

1fetchData () {
2       //We are bound to new-expense on Pusher and it listens for changes to the dataset so it can automatically rebuild the Line Chart in realtime.    
3        channel.bind('new-expense', data => {
4            let _results = data.newExpense.data
5            let dateresult = _results.map(a => a.date);
6            let expenseresult = _results.map(a => a.expense);
7            let incomeresult = _results.map(a => a.income);
8
9            //The instance data are updated here with the latest data gotten from Pusher
10            this.expense = expenseresult
11            this.income = incomeresult
12            this.date = dateresult
13
14            //The Chart's dataset is updated with the latest data gotten from Pusher
15            this.datacollection = {
16                labels: this.date,
17                datasets: [
18                    {
19                        label: 'Expense Charts',
20                        backgroundColor: '#f87979',
21                        data: this.expense
22                    },
23                    {
24                        label: 'Income Charts',
25                        backgroundColor: '#5bf8bf',
26                        data: this.income
27                    }
28                ]
29            }
30        });
31    }

Your final Home.vue file should look like this:

1<template>
2      <div class="hello">
3        <div class="container">
4          <div class="row">
5            <h2 class="title">Realtime Chart with Vue and Pusher</h2>
6            <h3 class="subtitle">Expense and Income Tracker</h3>
7            <line-chart :chart-data="datacollection"></line-chart>
8          </div>
9        </div>
10        <div class="container">
11          <div class="row">
12            <form class="form" @submit.prevent="addExpenses">
13              <h4>Add New Entry</h4>
14              <div class="form-group">
15                <label>Expenses</label>
16                <input class="form-control" placeholder="How much did you spend today?" type="number" v-model="expenseamount" required>
17              </div>
18              <div class="form-group">
19                <label>Income</label>
20                <input class="form-control" placeholder="How much did you earn today?" type="number" v-model="incomeamount" required>
21              </div>
22              <div class="form-group">
23                <button class="btn btn-primary">Add New Entry</button>
24              </div>
25            </form>
26          </div>
27        </div>
28      </div>
29    </template>
30
31    <script>
32      import axios from 'axios'
33      import moment from 'moment'
34      import Pusher from 'pusher-js'
35      import LineChart from '@/components/LineChart'
36
37      const socket = new Pusher('3e6b0e8f2442b34330b7', {
38        cluster: 'eu',
39        encrypted: true
40      })
41      const channel = socket.subscribe('finance')
42
43      export default {
44        name: 'home',
45        components: {LineChart},
46        data () {
47          return {
48            expense: null,
49            income: null,
50            date: null,
51            expenseamount: null,
52            incomeamount: null,
53            datacollection: null
54          }
55        },
56        created () {
57          this.fetchData()
58          this.fillData()
59        },
60        mounted () {
61          this.fillData()
62        },
63        methods: {
64          fillData () {
65            axios.get('/finances')
66              .then(response => {
67                let results = response.data.data
68
69                let dateresult = results.map(a => a.date)
70                let expenseresult = results.map(a => a.expense)
71                let incomeresult = results.map(a => a.income)
72
73                this.expense = expenseresult
74                this.income = incomeresult
75                this.date = dateresult
76
77                this.datacollection = {
78                  labels: this.date,
79                  datasets: [
80                    {
81                      label: 'Expense',
82                      backgroundColor: '#f87979',
83                      data: this.expense
84                    },
85                    {
86                      label: 'Income',
87                      backgroundColor: '#5bf8bf',
88                      data: this.income
89                    }
90                  ]
91                }
92              })
93              .catch(error => {
94                console.log(error)
95              })
96          },
97          addExpenses () {
98            let expense = this.expenseamount
99            let income = this.incomeamount
100            let today = moment().format('MMMM Do YYYY')
101            axios.post('/expense/add', {
102              expense: expense,
103              income: income,
104              date: today
105            })
106              .then(response => {
107                this.expenseamount = ''
108                this.incomeamount = ''
109                channel.bind('new-expense', function (data) {
110                  let results = data.newExpense.data
111
112                  let dateresult = results.map(a => a.date)
113                  let expenseresult = results.map(a => a.expense)
114                  let incomeresult = results.map(a => a.income)
115
116                  this.expense = expenseresult
117                  this.income = incomeresult
118                  this.date = dateresult
119
120                  this.datacollection = {
121                    labels: this.date,
122                    datasets: [
123                      {
124                        label: 'Expense',
125                        backgroundColor: 'transparent',
126                        pointBorderColor: '#f87979',
127                        data: this.expense
128                      },
129                      {
130                        label: 'Income',
131                        backgroundColor: 'transparent',
132                        pointBorderColor: '#5bf8bf',
133                        data: this.income
134                      }
135                    ]
136                  }
137                })
138              })
139              .catch(error => {
140                console.log(error)
141              })
142          },
143          fetchData () {
144            channel.bind('new-expense', data => {
145              let results = data.newExpense.data
146              let dateresult = results.map(a => a.date)
147              let expenseresult = results.map(a => a.expense)
148              let incomeresult = results.map(a => a.income)
149
150              this.expense = expenseresult
151              this.income = incomeresult
152              this.date = dateresult
153
154              this.datacollection = {
155                labels: this.date,
156                datasets: [
157                  {
158                    label: 'Expense Charts',
159                    backgroundColor: '#f87979',
160                    data: this.expense
161                  },
162                  {
163                    label: 'Income Charts',
164                    backgroundColor: '#5bf8bf',
165                    data: this.income
166                  }
167                ]
168              }
169            })
170          }
171        }
172      }
173    </script>
174
175
176    <!-- Add "scoped" attribute to limit CSS to this component only -->
177    <style scoped>
178
179      .title {
180        text-align: center;
181        margin-top: 40px;
182      }
183      .subtitle {
184        text-align: center;
185      }
186      .form {
187        max-width: 600px;
188        width: 100%;
189        margin: 20px auto 0 auto;
190      }
191      .form h4 {
192        text-align: center;
193        margin-bottom: 30px;
194      }
195
196      h1, h2 {
197      font-weight: normal;
198    }
199
200    ul {
201      list-style-type: none;
202      padding: 0;
203    }
204
205    li {
206      display: inline-block;
207      margin: 0 10px;
208    }
209
210    a {
211      color: #42b983;
212    }
213    </style>

One more thing!

Before we can run our app, we need to do something called API proxying. API proxying allows us to integrate our vue-cli app with a backend server (Node server in our case). This means we can run the dev server and the API backend side-by-side and let the dev server proxy all API requests to the actual backend.

We can enable API proxying by editing the dev.proxyTable option in config/index.js. You can replace with the code below.

1proxyTable: {
2      '/expense/add': {
3        target: 'http://localhost:5000',
4        changeOrigin: true
5      },
6      '/finances': {
7        target: 'http://localhost:5000',
8        changeOrigin: true
9      },
10    }

After that has been done, we are finally ready to see our app and you can run npm run dev to start the app.

That’s it! At this point, you should have a realtime chart that updates in realtime.

You can check out the live demo here or go to the code for the whole app, which is hosted on Github for your perusal.

Conclusion

We’ve seen how to build a basic Line Chart with ChartJS in Vue with the help of vue-chartjs and also added realtime features thanks to Pusher.

Then we saw how to use reactiveProps to make ChartJS update its dataset if there’s been a change in the dataset. We also saw how to use Pusher to trigger events on the server and listen for them on the client side using JS.

The use-cases of combining Vue and Pusher are numerous, another example can be seen here. If you are interested in seeing other methods in which realtime charts can be made, you can check here, and here.

Have you built anything cool with Pusher recently, a chart maybe? Let’s know in the comment below.