Build a live dashboard with Python

  • Neo Ighodaro
June 29th, 2018
You will need Python 3+ installed on your machine. A basic knowledge of Python and Flask will be helpful.

Introduction

In the past, if we needed to build a web platform that keeps track of user actions and displays updates accordingly, say on the admin dashboard, we will have to refresh the dashboard from time to time — usually intuitively — to check for new updates.

Today, however, we can build a fully interactive web application and have the updates served to us in realtime. In this tutorial, we will build an interactive website with a dashboard that displays updates on user actions in realtime. Here is what the final application will look like:

The image above shows two browser windows, the window on the left shows a user performing three actions:

  • The user places a new order.
  • The user sends a new message.
  • The user adds a new customer.

The window on the right shows an admin dashboard that updates in realtime based on the user’s interaction. The realtime update in this application is powered by Pusher.

For the sake of this article, we will build the backend server using a Python framework called Flask. For the frontend, we will use JavaScript to send HTTP requests and communicate with the backend API.

The source code for this tutorial is available here on GitHub.

Prerequisites

To follow along with this tutorial, a basic knowledge of Python, Flask, and JavaScript (ES6 syntax) is required. You will also need the following installed:

  1. Python (>= v3.x).
  2. virtualenv.
  3. Flask.

Virtualenv is great for creating isolated Python environments, so we can install dependencies in an isolated environment, and not pollute our global packages directory.

Let’s install virtualenv with this command:

    $ pip install virtualenv

Setting up the app environment

Let’s create our project folder, and activate a virtual environment within it:

    $ mkdir pusher-python-realtime-dashboard
    $ cd pusher-python-realtime-dashboard
    $ virtualenv .venv
    $ source .venv/bin/activate # Linux based systems
    $ \path\to\env\Scripts\activate # Windows users

Now that we have the virtual environment setup, we can install Flask with this command:

    $ pip install flask

We need to install the Pusher library as we will need that for realtime updates.

Setting up Pusher

The first step will be to get a Pusher Channels application. We will need the application credentials for our realtime features to work.

Go to the Pusher website and create an account. After creating an account, you should create a new application. Follow the application creation wizard and then you should be given your application credentials, we will use this later in the article.

We also need to install the Pusher Python Library to send events to Pusher. Install this using the command below:

    $ pip install pusher

File and folder structure

We don’t need to create so many files and folders for this application since it’s a simple one. Here’s the file/folder structure:

    ├── pusher-python-realtime-dashboard
           ├── app.py
           ├── static
           └── templates

The static folder will contain the static files to be used as is defined by Flask standards. The templates folder will contain the HTML templates. In our application, app.py is the main entry point and will contain our server-side code.

We will go ahead and create the app.py and then the static and templates folders.

Building the backend

Let’s open the app.py file and start writing the backend code that will handle the incoming HTTP requests. In this file, we are going to register five routes and their respective handler functions. The / and /dashboard routes will render the website and admin dashboard pages respectively. We will create these pages shortly.

We will define three more routes: /orders, /message and /customer. These will serve as API endpoints. These endpoints will be responsible for processing the POST requests that will be coming from our frontend and receiving user data.

We will also create a fresh Pusher instance and use it to broadcast data through three channels, one for each of the three possible user operations:

  • Place an order
  • Send a message
  • Register a new customer

Open the app.py file and paste the following code:

    from flask import Flask, render_template, request
    from pusher import Pusher

    app = Flask(__name__)

    # configure pusher object
    pusher = Pusher(
    app_id='PUSHER_APP_ID',
    key='PUSHER_APP_KEY',
    secret='PUSHER_APP_SECRET',
    cluster='PUSHER_APP_CLUSTER',
    ssl=True)

    @app.route('/')
    def index():
        return render_template('index.html')

    @app.route('/dashboard')
    def dashboard():
        return render_template('dashboard.html')

    @app.route('/orders', methods=['POST'])
    def order():
        data = request.form
        pusher.trigger(u'order', u'place', {
            u'units': data['units']
        })
        return "units logged"

    @app.route('/message', methods=['POST'])
    def message():
        data = request.form
        pusher.trigger(u'message', u'send', {
            u'name': data['name'],
            u'message': data['message']
        })
        return "message sent"

    @app.route('/customer', methods=['POST'])
    def customer():
        data = request.form
        pusher.trigger(u'customer', u'add', {
            u'name': data['name'],
            u'position': data['position'],
            u'office': data['office'],
            u'age': data['age'],
            u'salary': data['salary'],
        })
        return "customer added"

    if __name__ == '__main__':
        app.run(debug=True)

In the code above, we imported the required modules and objects, then initialized a Flask app. Next, we initialized and configure Pusher and also registered the routes and their associated handler functions.

Replace the PUSHER_APP_* keys with the values on your Pusher dashboard.

With the pusher object instantiated, we can trigger events on whatever channels we define. In the handler functions of the /orders, /message and /customer routes, we trigger events on three channels. The trigger method has the following syntax:

    pusher.trigger("a_channel", "an_event", {key: "data to pass with event"})

You can find the docs for the Pusher Python library here, to get more information on configuring and using Pusher in Python.

As we already discussed, the / and /dashboard routes will render the index.html and dashboard.html templates so we need to create these files and write the code to define the frontend layout. In the next step, we will create the app view and use the frontend to communicate with the Python backend.

Setting up the app view

We need to create two files in the templates directory. These files will be named index.html and dashboard.html, this is where the view for our code will live. When we visit our application’s root address, the index.html page will be rendered as the homepage. When we visit the [/dashboard](http://127.0.0.1:5000/dashboard) address, the dashboard.html file will be rendered on the browser.

In the ./templates/index.html file, you can paste this code:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Pusher Python Realtime Dashboard</title>
    </head>
    <body>
        <form method="post" action="/orders">
            <h3>Place a new order</h3>
            <input type="number" name="units" placeholder="units"><br>
            <input type="submit" name="Submit">
        </form>
        <form method="post" action="/message">
            <h3>Send a new message</h3>
            <input type="text" name="name" placeholder="name here"><br>
            <textarea  name="message" placeholder="message here"></textarea><br>
            <input type="submit" name="Submit">
        </form>
        <form method="post" action="/customer">
            <h3>Add new customer</h3>
            <input type="text" name="name" placeholder="name here"><br>
            <input type="text" name="position" placeholder="position here"><br>
            <input type="text" name="office" placeholder="office here"><br>
            <input type="number" name="age" placeholder="age here"><br>
            <input type="text" name="salary" placeholder="salary here"><br>
            <input type="submit" name="Submit">
        </form>
    </body>
    </html>

In the markup above, we created three forms with the POST method and defined their actions. Whenever each of these forms is submitted, user data is sent to the Python backend server that we defined in the previous step.

Before we write the code for dashboard-single.html and dashboard files, we will pull in some CSS and JS from https://startbootstrap.com. Go to the URL and click Download. Unzip the file and copy the css and js directories into the static directory of our project. Now, let’s continue building the frontend of our application.

Open the ./templates/dashboard.html file and paste the following:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
      <meta name="description" content="">
      <meta name="author" content="">
      <title>SB Admin - Start Bootstrap Template</title>
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
      <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
      <link href="https://cdn.datatables.net/1.10.16/css/dataTables.bootstrap4.min.css" rel="stylesheet">
      <link href="{{ url_for('static', filename='css/sb-admin.css') }}" rel="stylesheet">
    </head>
    <body class="fixed-nav sticky-footer bg-dark" id="page-top">
      <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top" id="mainNav">
        <a class="navbar-brand" href="index.html">Start Bootstrap</a>
        <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarResponsive">
          <ul class="navbar-nav navbar-sidenav" id="exampleAccordion">
            <li class="nav-item" data-toggle="tooltip" data-placement="right" title="Dashboard">
              <a class="nav-link" href="/dashboard">
                <i class="fa fa-fw fa-dashboard"></i>
                <span class="nav-link-text">Dashboard</span>
              </a>
            </li>
          </ul>
        </div>
      </nav>
      <div class="content-wrapper">
        <div class="container-fluid">
          <ol class="breadcrumb">
            <li class="breadcrumb-item">
              <a href="#">Dashboard</a>
            </li>
            <li class="breadcrumb-item active">My Dashboard</li>
          </ol>
          <div class="row">
            <div class="col-xl-3 col-sm-6 mb-3">
              <div class="card text-white bg-primary o-hidden h-100">
                <div class="card-body">
                  <div class="card-body-icon">
                    <i class="fa fa-fw fa-comments"></i>
                  </div>
                  <div class="mr-5"><span id="message-count">26</span> New Messages!</div>
                </div>
                <a class="card-footer text-white clearfix small z-1" href="#">
                  <span class="float-left">View Details</span>
                  <span class="float-right">
                    <i class="fa fa-angle-right"></i>
                  </span>
                </a>
              </div>
            </div>
            <div class="col-xl-3 col-sm-6 mb-3">
              <div class="card text-white bg-warning o-hidden h-100">
                <div class="card-body">
                  <div class="card-body-icon">
                    <i class="fa fa-fw fa-list"></i>
                  </div>
                  <div class="mr-5">11 New Tasks!</div>
                </div>
                <a class="card-footer text-white clearfix small z-1" href="#">
                  <span class="float-left">View Details</span>
                  <span class="float-right">
                    <i class="fa fa-angle-right"></i>
                  </span>
                </a>
              </div>
            </div>
            <div class="col-xl-3 col-sm-6 mb-3">
              <div class="card text-white bg-success o-hidden h-100">
                <div class="card-body">
                  <div class="card-body-icon">
                    <i class="fa fa-fw fa-shopping-cart"></i>
                  </div>
                  <div class="mr-5"><span id="order-count">123</span> New Orders!</div>
                </div>
                <a class="card-footer text-white clearfix small z-1" href="#">
                  <span class="float-left">View Details</span>
                  <span class="float-right">
                    <i class="fa fa-angle-right"></i>
                  </span>
                </a>
              </div>
            </div>
            <div class="col-xl-3 col-sm-6 mb-3">
              <div class="card text-white bg-danger o-hidden h-100">
                <div class="card-body">
                  <div class="card-body-icon">
                    <i class="fa fa-fw fa-support"></i>
                  </div>
                  <div class="mr-5">13 New Tickets!</div>
                </div>
                <a class="card-footer text-white clearfix small z-1" href="#">
                  <span class="float-left">View Details</span>
                  <span class="float-right">
                    <i class="fa fa-angle-right"></i>
                  </span>
                </a>
              </div>
            </div>
          </div>
          <div class="row">
            <div class="col-lg-8">
              <div class="card mb-3">
                <div class="card-header">
                  <i class="fa fa-bar-chart"></i> Revenue Chart</div>
                <div class="card-body">
                  <div class="row">
                    <div class="col-sm-8 my-auto">
                      <canvas id="myBarChart" width="100" height="50"></canvas>
                    </div>
                    <div class="col-sm-4 text-center my-auto">
                      <div class="h4 mb-0 text-primary">$34,693</div>
                      <div class="small text-muted">YTD Revenue</div>
                      <hr>
                      <div class="h4 mb-0 text-warning">$18,474</div>
                      <div class="small text-muted">YTD Expenses</div>
                      <hr>
                      <div class="h4 mb-0 text-success">$16,219</div>
                      <div class="small text-muted">YTD Margin</div>
                    </div>
                  </div>
                </div>
                <div class="card-footer small text-muted">Updated yesterday at 11:59 PM</div>
              </div>
            </div>
            <div class="col-lg-4">
              <!-- Example Notifications Card-->
              <div class="card mb-3">
                <div class="card-header">
                  <i class="fa fa-bell-o"></i> Message Feed</div>
                <div class="list-group list-group-flush small">
                  <div id="message-box">
                    <a class="list-group-item list-group-item-action" href="#">
                      <div class="media">
                        <img class="d-flex mr-3 rounded-circle" src="http://placehold.it/45x45" alt="">
                        <div class="media-body">
                          <strong>Jeffery Wellings</strong>added a new photo to the album
                          <strong>Beach</strong>.
                          <div class="text-muted smaller">Today at 4:31 PM - 1hr ago</div>
                        </div>
                      </div>
                    </a>
                    <a class="list-group-item list-group-item-action" href="#">
                      <div class="media">
                        <img class="d-flex mr-3 rounded-circle" src="http://placehold.it/45x45" alt="">
                        <div class="media-body">
                          <i class="fa fa-code-fork"></i>
                          <strong>Monica Dennis</strong>forked the
                          <strong>startbootstrap-sb-admin</strong>repository on
                          <strong>GitHub</strong>.
                          <div class="text-muted smaller">Today at 3:54 PM - 2hrs ago</div>
                        </div>
                      </div>
                    </a>
                  </div>
                  <a class="list-group-item list-group-item-action" href="#">View all activity...</a>
                </div>
                <div class="card-footer small text-muted">Updated yesterday at 11:59 PM</div>
              </div>
            </div>
          </div>
          <!-- Example DataTables Card-->
          <div class="card mb-3">
            <div class="card-header">
              <i class="fa fa-table"></i> Customer Order Record</div>
            <div class="card-body">
              <div class="table-responsive">
                <table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
                  <thead>
                    <tr>
                      <th>Name</th>
                      <th>Position</th>
                      <th>Office</th>
                      <th>Age</th>
                      <th>Start date</th>
                      <th>Salary</th>
                    </tr>
                  </thead>
                  <tfoot>
                    <tr>
                      <th>Name</th>
                      <th>Position</th>
                      <th>Office</th>
                      <th>Age</th>
                      <th>Start date</th>
                      <th>Salary</th>
                    </tr>
                  </tfoot>
                  <tbody id="customer-table">
                    <tr>
                      <td>Cedric Kelly</td>
                      <td>Senior Javascript Developer</td>
                      <td>Edinburgh</td>
                      <td>22</td>
                      <td>2012/03/29</td>
                      <td>$433,060</td>
                    </tr>
                    <tr>
                      <td>Airi Satou</td>
                      <td>Accountant</td>
                      <td>Tokyo</td>
                      <td>33</td>
                      <td>2008/11/28</td>
                      <td>$162,700</td>
                    </tr>
                  </tbody>
                </table>
              </div>
            </div>
            <div class="card-footer small text-muted">Updated yesterday at 11:59 PM</div>
          </div>
        </div>
        <footer class="sticky-footer">
          <div class="container">
            <div class="text-center">
              <small>Copyright © Your Website 2018</small>
            </div>
          </div>
        </footer>
        <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
            crossorigin="anonymous"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
        <!-- Page level plugin JavaScript-->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
        <script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js"></script>
        <script src="https://cdn.datatables.net/1.10.16/js/dataTables.bootstrap4.min.js"></script>
        <script src="https://js.pusher.com/4.0/pusher.min.js"></script>
        <script src="{{ url_for('static', filename='js/customer.js') }}"></script>
        <script src="{{ url_for('static', filename='js/order.js') }}"></script>
        <script src="{{ url_for('static', filename='js/message.js') }}"></script>
      </div>
    </body>
    </html>

In the code above, we imported the JQuery and the JavaScript Pusher library and written the markup to define the layout for the home and dashboard pages, In the next step, we will create the JavaScript files that will handle the realtime updates.

Communicating with the backend

Create a new folder called js in the static directory and populate it with three new files:

  • order.js — in this file, we will subscribe to the order channel and update the admin dashboard in realtime whenever a new order is placed.
  • message.js — in this file, we will subscribe to the message channel and update the admin dashboard in realtime whenever a new message is sent.
  • customer.js — in this file, we will subscribe to the customer channel and update the admin dashboard in realtime whenever a new customer is registered.

In the ./static/js/order.js file, we can paste the following:

    Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';

    Chart.defaults.global.defaultFontColor = '#292b2c';

    var ctx = document.getElementById("myBarChart");
    var myLineChart = new Chart(ctx, {
      type: 'bar',
      data: {
        labels: ["February", "March", "April", "May", "June", "July"],
        datasets: [{
          label: "Revenue",
          backgroundColor: "rgba(2,117,216,1)",
          borderColor: "rgba(2,117,216,1)",
          data: [5312, 6251, 7841, 9821, 14984, 0],
        }],
      },
      options: {
        scales: {
          xAxes: [{
            time: {
              unit: 'month'
            },
            gridLines: {
              display: false
            },
            ticks: {
              maxTicksLimit: 6
            }
          }],
        },
        legend: {
          display: false
        }
      }
    });

    // Configure Pusher instance
    const pusher = new Pusher('PUSHER_APP_KEY', {
        cluster: 'PUSHER_APP_CLUSTER',
        encrypted: true
    });

    // Subscribe to poll trigger
    var orderChannel = pusher.subscribe('order');

    // Listen to 'order placed' event
    var order = document.getElementById('order-count')
    orderChannel.bind('place', function(data) {
      myLineChart.data.datasets.forEach((dataset) => {
          dataset.data.fill(parseInt(data.units),-1);
      });
      myLineChart.update();
      order.innerText = parseInt(order.innerText)+1
    });

Replace the PUSHER_APP_* keys with the keys on your Pusher dashboard.

In the code above, first, we targeted the bar chart on the dashboard page using the ID myBarChart, then we initialized its data object. Next, we configured a Pusher instance to communicate with the Pusher service. We register a listener, on the place event, and listen to the events Pusher sends.

Next, open the ./static/js/message.js file and paste in this code:

    $(document).ready(function () {
      $('.navbar-sidenav [data-toggle="tooltip"]').tooltip({
        template: '<div class="tooltip navbar-sidenav-tooltip" role="tooltip" style="pointer-events: none;"><div class="arrow"></div><div class="tooltip-inner"></div></div>'
      })

      $('[data-toggle="tooltip"]').tooltip()

      var messageChannel = pusher.subscribe('message');
      messageChannel.bind('send', function(data) {
        var message = document.getElementById('message-count')
        var date = new Date();
        var toAppend = document.createElement('a')
        toAppend.classList.add('list-group-item', 'list-group-item-action')
        toAppend.href = '#'
        document.getElementById('message-box').appendChild(toAppend)
        toAppend.innerHTML ='<div class="media">'+
                        '<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/45x45" alt="">'+
                        '<div class="media-body">'+
                          `<strong>${data.name}</strong> posted a new message `+
                          `<em>${data.message}</em>.`+
                          `<div class="text-muted smaller">Today at ${date.getHours()} : ${date.getMinutes()}</div>`+
                        '</div>'+
                      '</div>'

        message.innerText = parseInt(message.innerText)+1
      });
    });

As we did before, here bind to the sent event and listen for updates from Pusher, whenever there is an update, we display it on the admin dashboard.

Lastly, open the ./static/js/customer.js file and paste in this code:

    $(document).ready(function(){
      var dataTable = $("#dataTable").DataTable()
      var customerChannel = pusher.subscribe('customer');
      customerChannel.bind('add', function(data) {
      var date = new Date();
      dataTable.row.add([
          data.name,
          data.position,
          data.office,
          data.age,
          `${date.getFullYear()}/${date.getMonth()}/${date.getDay()}`,
          data.salary
        ]).draw( false );
      });
    });

In the above code, we subscribe to the customer channel and bind to the add event so that we can update the dashboard in realtime whenever a new customer is registered.

We are done building! We can run the application using this command:

    $ flask run

Now if we visit 127.0.0.1:5000 and 127.0.0.1:5000/dashboard we should see our app:

Conclusion

In this tutorial, we have learned how to build a Python Flask project from the scratch and inplement realtime functionality using Pusher and JavaScript. The entire code for this tutorial is available on GitHub.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.