Skip to main content Skip to footer

Building a Business Intelligence Progressive Web Application (Source Code Included)

This article was originally published on CodeProject - 8/09/18.

Progressive web applications (PWAs) are practical alternatives to native mobile apps. A progressive web application is a regular web app that behaves like a native application. They can be easily installed, open quickly, and can operate without a network connection. Upgrading existing web apps to PWAs is simple and can eliminate the need to develop Android and iOS-specific apps.

In another article, we discuss the advantages of PWAs. From avoiding marketplace fees to eliminating overhead, progressive web apps benefit your business in multiple ways.

Progressive web apps start as regular modern web apps. If your web app is responsive, touch-aware, and efficient, you can turn it into a progressive web application. We created a MyBI sample app to show how easy it can be to build progressive web apps using Wijmo. In this article, we describe how we implemented MyBI as a regular web app, and then show how we upgraded it to a progressive web app.

Download Wijmo Today!

MyBI: Built in JavaScript

We chose to build our PWA in JavaScript. We could have used Angular, React, or Vue, but the application is simple enough that we decided to go with plain JavaScript to keep the application as small as possible. Our app is only 20k (unminified) and adding any framework to it would represent a huge increase in size.

The app downloads monthly sales data from a server, consolidates it, and shows cards with dashboard-style summary information about sales volumes and revenue. Users may select individual regions and products or look at aggregate data. When a selection is made, the cards are updated.

This is what the app looks like on the desktop and on a phone:

MyBI: Built in JavaScript

MyBI: Built in JavaScript

The layout has a fixed header with the app title and an “options” button on the right, a content area with four cards that show various types of sales information, and a footer with shortcut buttons that allow users to navigate directly to specific cards. All layout is done with flex elements, so it’s naturally responsive. Except for the Wijmo logo, all images are svg’s from the Material Icons page, a great source for high-quality icons.

The Data Source

The MyBI application gets data from a Google Sheet published to the Web as a CSV file. The data contains monthly sales and revenues divided among regions and products. We decided to use Google Sheets and CSV because that makes it easy to update and load the data. A manager can open the sheet in his browser, update the data, and it becomes immediately available to the app. There’s no need to save or export anything, and the CSV format is compact and easy to parse on the client-side.

When the application starts, it loads the data from local storage first, then from the web. This enables quick startup times and real-time updates. The app assigns the data to a Model class that filters and summarizes it, so it can be shown to users.

The Model class has the following members:

Model Input

  • rawData: CSV string with data from the server
  • selectedRegion: Name of the selected region, empty to select all regions
  • selectedProduct: Name of the selected product, empty to select all products

Model Output

  • regions: CollectionView with all regions
  • products: CollectionView with all products
  • filteredView: CollectionView with consolidated and filtered data
  • groupedView: CollectionView with consolidated/filtered/grouped data

The model filters and summarizes data according to the current selection using the CollectionView class and some JavaScript. The app is simple enough that we didn’t need to use full-scale OLAP tools like Wijmo’s OLAP module.

The application also has an App class that instantiates a Model and uses it to create cards that make up the UI.

Cards

  • Selection contains two combo boxes that allow the user to select the region and country. It also contains buttons to navigate to the other cards in case they’re off screen.
  • Current Period contains radial gauges that show the units sold and revenue for the current period. The gauges include ranges that represent ‘target zones’ so users can see the current values.
  • Trends contains charts showing units sold and revenue over time. The charts include trend lines that show whether the values are trending up or down.
  • Details contains a grid that displays units sold and revenue per month. The grid includes spark-bars for the numerical values. It acts as a hybrid between a grid and a chart.

In addition to creating the cards, the App class uses localStorage to persist the application state. If you select a region and a product, the selection will be restored the next time you run the app.

Screen Layout and Behavior

The screen layout is built with responsive flex-box divs. It has a header element with the application title, a content element where the cards are displayed, and a footer element with navigation buttons.

On phones and other small screens, the content element shows one card at a time, and users may use swipe gestures to switch cards. We used CSS Scroll Snap Points to achieve an effect that native apps have.

Here’s a simple demo of CSS Scroll Snap Points. This fiddle should work on all modern browsers except Chrome version 67. The next version of Chrome already supports CSS Scroll Snap Points.

How to Turn A Modern Web Application into A Progressive Web App

Two items are required in order turn a modern web application into a PWA:

  • A manifest file, so the browser knows this is a progressive app, and
  • A service worker, so it works in disconnected scenarios

Step 1: Add the Manifest File

The easiest way to create a manifest file is by using a manifest generator app.

Before you run this app, you’ll need a 512 by 512-pixel image to be used for the app’s icon. Round icons display the best. The manifest generator will let you enter some basic information about the app, including its name, short name, screen mode, and theme colors. Click the “ICON” button to upload the image, then click “GENERATE ZIP” button. The manifest generator will create a zip file with the manifest.json file and a folder with scaled versions of the image:

MyBI: Built in JavaScript

Copy the manifest.json file to the app’s root folder and copy the images to the app’s images or resources folder. Make sure the image paths in the manifest file are correct. In our case, we placed the icons in a resources/icons folder, so the paths look like “resources/icons/icon-72x72.png” etc.

Add a reference to the manifest file to the app’s main HTML page:

<!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">

    <!-- SEO -->
    <title>MyBI | Business Intelligence PWA with Wijmo</title>
    <meta name="description" content="A BI Progressive Web App with Wijmo.">

    <!-- PWA -->
    <link rel="manifest" href="manifest.json">
    <meta name="theme-color" content="#fff">
    <script>
        // create service worker (to work off-line)
        if ('serviceWorker' in navigator) {
            try {
                navigator.serviceWorker.register('../serviceWorker.js');
                console.log('serviceWorker registered');
            } catch (error) {
                console.log('serviceWorker failed to register');
            }
        }
    </script>

Step 2: Add a Service Worker

Start by creating a “serviceWorker.js” file in the app’s root folder. Before adding any content to the service worker file, add it to the app by editing the app’s main HTML page:

// serviceWorker.js

// initialize cache when app is installed
self.addEventListener('install', async e => {
    const cache = await caches.open('mybi-static');
    await cache.addAll([
        './',
        './styles/app.css',
        './scripts/app.js',
        './scripts/vendor/wijmo.theme.material.min.css',
        './scripts/vendor/wijmo-bundle.min.js',
        './resources/home.svg',
        './resources/current.svg',
        './resources/trends.svg',
        './resources/details.svg',
        './resources/options.svg',
        './resources/wijmo-logo.png'
    ]);
});

Notice that the script will add the service worker only if the browser supports it. The application will still run as a regular web app on browsers that do not support PWAs (check out the status of browser support for PWAs).

Next, implement the serviceWorker.js file. This will only be executed on modern browsers, so you can use ES2017 features like async, await, and fetch.

Our serviceWorker starts by initializing a static cache when the app is installed:

// serviceWorker.js

// initialize cache when app is installed
self.addEventListener('install', async e => {
    const cache = await caches.open('mybi-static');
    await cache.addAll([
        './',
        './styles/app.css',
        './scripts/app.js',
        './scripts/vendor/wijmo.theme.material.min.css',
        './scripts/vendor/wijmo-bundle.min.js',
        './resources/home.svg',
        './resources/current.svg',
        './resources/trends.svg',
        './resources/details.svg',
        './resources/options.svg',
        './resources/wijmo-logo.png'
    ]);
});

The “self” property returns a reference to the worker.

The code handles the “install” event to open a static cache and adds all our static assets to it. Next time the app starts, all the required assets will be available locally. The app will run instantly without an internet connection.

Next, we must handle “fetch” events to return the assets from the static cache and the data from the network. Items retrieved from the network are stored in a dynamic cache to be used in future sessions in case a network connection is not available:

// intercept fetch requests sent from app to network
self.addEventListener('fetch', e => {
    const req = e.request;
    const url = new URL(req.url);
    e.respondWith(url.origin === req.origin
        ? cacheFirst(req)
        : networkFirst(req));
});

// fetch from cache if possible, fall back on network
async function cacheFirst(req) {
    const cachedResponse = await caches.match(req);
    return cachedResponse || fetch(req);
}

// fetch from network if possible, fall back on cache
async function networkFirst(req) {
    const cache = await caches.open('mybi-dynamic');
    try {
        const res = await fetch(req);
        cache.put(req, res.clone());
        return res;
    } catch (error) {
        return await cache.match(req);
    }
}

This simple implementation accomplishes two things:

  1. The application assets will be downloaded from the network only when the application is installed. After that, they will be fetched from the static cache.
  2. The application data will be retrieved from the network if a connection is available (so we can get the most recent data) and will fall back on a dynamic cache then the app is not connected.

Handling requests is the most important and complex task of the serviceWorker. You can choose from many possible strategies, each with their own implications and trade-offs.

Our MyBI application is now a fully-fledged progressive web application! You can check this by opening the Developer Tools window in Chrome and selecting the “Application” tab:

MyBI: Built in JavaScript

From this page you can inspect the application manifest, install it on the home screen, and inspect/debug your service worker and application storage, including the cache storage.

Now that we have a brand new PWA, it’s time to use Lighthouse to audit the app and make sure it is working well. To start a Lighthouse audit, open the Developer Tools window in Chrome and select the “Audit” tab:

MyBI: Built in JavaScript

We are interested in the Progressive Web Application Audit, but it’s always a good idea to check everything, just in case. The audit takes a minute or so to run, and displays the results in a comprehensive report:

MyBI: Built in JavaScript

All gauges are green, and the scores are high!
It’s important to look at the details for each audit since they include suggestions for improving the app. Some suggestions are trivial and can be addressed easily (like forgetting to add a meta tag with a description of the app, which is important for SEO).

Performance Audit

The app got an 81 on the performance audit, which is good. Unlike the other scores, this one may change if you run the audit again, because of varying network performance.
Our initial scores were lower than this. We improved our score by adding common performance optimizations such as minification, bundling, and gzip compression. We also added a ‘defer’ attribute to all script tags to reduce the time required to deliver the first meaningful paint.

Progressive Web App Audit

The app scored a perfect 100 on the progressive web app audit, which includes 11 tests:

MyBI: Built in JavaScript

The manifest and service worker files we added handle the main PWA requirements.
Additional requirements are related to performance, responsive design, and security (use HTTPS). We see these as requirements for modern web apps regardless of being PWA’s.

Next Steps with our MyBI App

When we started designing the MyBI app, we wanted to create at least two versions: a PWA using Wijmo and a native mobile app using Xamarin. We just finished the Wijmo version. The Xamarin version should be easy to build, as we’ll use the same data and application logic and similar IO components.

We hope you enjoyed this quick intro to PWAs. Feel free to leave a comment below if you have any questions, requests, or suggestions!

Download Now!

Bernardo de Castilho

comments powered by Disqus