Book Image

Building Data-Driven Applications with Danfo.js

By : Rising Odegua, Stephen Oni
Book Image

Building Data-Driven Applications with Danfo.js

By: Rising Odegua, Stephen Oni

Overview of this book

Most data analysts use Python and pandas for data processing for the convenience and performance these libraries provide. However, JavaScript developers have always wanted to use machine learning in the browser as well. This book focuses on how Danfo.js brings data processing, analysis, and ML tools to JavaScript developers and how to make the most of this library to build data-driven applications. Starting with an overview of modern JavaScript, you’ll cover data analysis and transformation with Danfo.js and Dnotebook. The book then shows you how to load different datasets, combine and analyze them by performing operations such as handling missing values and string manipulations. You’ll also get to grips with data plotting, visualization, aggregation, and group operations by combining Danfo.js with Plotly. As you advance, you’ll create a no-code data analysis and handling system and create-react-app, react-table, react-chart, Draggable.js, and tailwindcss, and understand how to use TensorFlow.js and Danfo.js to build a recommendation system. Finally, you’ll build a Twitter analytics dashboard powered by Danfo.js, Next.js, node-nlp, and Twit.js. By the end of this app development book, you’ll be able to build and embed data analytics, visualization, and ML capabilities into any JavaScript app in server-side Node.js or the browser.
Table of Contents (18 chapters)
1
Section 1: The Basics
3
Section 2: Data Analysis and Manipulation with Danfo.js and Dnotebook
10
Section 3: Building Data-Driven Applications

Promises and async/await

Let's dive a bit into the world of Asynchronous functions, functions that we call now but finish later. In this section, we will see why we need Promise and async/await.

Let's start with a simple problem as shown in the following code snippet. We are given a task to update an array with a function, after 1 second of calling the function:

let syncarray = ["1", "2", "3", "4", "5"]
function addB() {
    setTimeout(() => {
        syncarray.forEach((value, index)=>{
            syncarray[index] = value + "+B"
        })
        console.log("done running")
    }, 1000)
}
addB()
console.log(syncarray);
// output 
// ["1", "2", "3", "4", "5"]
// "done running"

console.log(syncarray) is executed before the addB() function, hence we see the syncarray output before it is being updated. This is an Asynchronous behavior. One of the ways to solve this is to use a callback:

let syncarray = ["1", "2", "3", "4", "5"]
function addB(callback) {
    setTimeout(() => {
        syncarray.forEach((value, index)=>{
            syncarray[index] = value + "+B"
        })
        callback() //call the callback function here
    }, 1000)
}
addB(()=>{
  // here we can do anything with the updated syncarray 
  console.log(syncarray);  
})
// output 
// [ '1+B', '2+B', '2+B', '4+B', '5+B' ]

Using the preceding callback approach means that we always pass in callbacks in order to perform other operations on the updated syncarray function. Let's update the code a little, and this time we'll also add the string "A" to syncarray and then print out the updated array:

let syncarray = ["1", "2", "3", "4", "5"]
function addB(callback) {
    setTimeout(() => {
        syncarray.forEach((value, index) => {
            syncarray[index] = value + "+B"
        })
        callback() //call the callback function here
    }, 1000)
}
addB(() => {
    setTimeout(() => {
        syncarray.forEach((value, index) => {
            syncarray[index] = value + "+A";
        })
        console.log(syncarray);
    }, 1000)
})
// output
// [ '1+B+A', '2+B+A', '3+B+A', '4+B+A', '5+B+A' ]

The preceding code block shows a quick way of passing callback. Based on the arrow function we discussed, it can be more organized by creating a named function.

Cleaning callbacks with promises

Using callbacks quickly becomes unwieldy and can quickly descend into callback hell. One method of freeing ourselves from this is to make use of Promises. Promises makes our callbacks more organized. It gives a chainable mechanism to unify and orchestrate code that is dependent on previous functions, as you'll see in the following code block:

let syncarray = ["1", "2", "3", "4", "5"]
function addA(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            syncarray.forEach((value, index) => {
                syncarray[index] = value + "+A";
            })
            resolve()
        }, 1000);
    })
}
addA().then(() => console.log(syncarray)); 
//output
//[ '1+A', '2+A', '2+A', '4+A', '5+A' ]

In the preceding code snippet, setTimeout is wrapped inside the Promise function. A Promise is always instantiated using the following expression:

New Promise((resolve, rejection) => {
})

A Promise is either resolved or rejected. When it is resolved, then we are free to do other things, and when it is rejected, we need to handle the error.

For example, let's ensure that the following Promise is rejected:

let syncarray = ["1", "2", "3", "4", "5"]
function addA(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            syncarray.forEach((value, index) => {
                syncarray[index] = value + "+A";
            })
            let error = true;
            if (error) {
                reject("just testing promise rejection")
            }
        }, 1000);
    })
}
addA().catch(e => console.log(e)) // just testing promise rejection

And whenever we have multiple promises, we can use the .then() method to handle each one:

addA.then(doB)
     .then(doC)
     .then(doD)
     .then(doF)
     .catch(e= > console.log(e));

The use of multiple .then() methods to handle numerous promises can quickly become unwieldy. To prevent this, we can use methods such as Promise.all(), Promise.any(), and Promise.race().

The Promise.all() method takes in an array of promises to be executed, and will only resolve when all promises are fulfilled. In the following code snippet, we add another Asynchronous function to our previous example and use Promise.all() to handle them:

let syncarray = ["1", "2", "2", "4", "5"]
function addA() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            syncarray.forEach((value, index) => {
                syncarray[index] = value + "+A";
            })
            resolve()
        }, 1000);
    })
}
function addB() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            syncarray.forEach((value, index) => {
                syncarray[index] = value + "+B";
            })
            resolve()
        }, 2000);
    })
}
Promise.all([addA(), addB()])
.then(() => console.log(syncarray)); // [ '1+A+B', '2+A+B', '2+A+B', '4+A+B', '5+A+B' ]

From the output in the preceding section, you can see that each Asynchronous function gets executed in the order it was added, and the final result is the effect of both functions on the syncarray variable.

The promise.race method, on the other hand, will return as soon as any promise in the array is resolved or rejected. You can think of this as a race where each promise tries to resolve or reject first, and as soon as this happens, the race is over. To see an in-depth explanation as well as code examples, you can visit the MDN docs here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any.

And finally, the promise.any method will return on the first fulfilled promise irrespective of any other rejected promise function. If all promises are rejected, then Promise.any rejects promises by providing errors for all of them. To see an in-depth explanation as well as code examples, you can visit the MDN docs here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race.

While using promises to work with callback solves a lot of issues, there is an even better way of implementing or using them. These are called async/await functions. We'll introduce these functions and show you how to use them in the following section.

async/await

As said earlier, async/await provides a more elegant way of working with promises. It gives us the power to control how and when each promise function gets called inside a function, instead of using .then() and Promise.all().

The following code snippet shows how you can use async/await in your code:

Async function anyName() {
    await anyPromiseFunction()
         await anyPromiseFunction()
}

The preceding async function can contain as many promise functions as possible, each waiting for the other to execute before being executed. Also, note that an async function is resolved as a Promise. that is, you can only obtain the return variable of the preceding anyName function (or resolve the function) using .then() or by calling it in another async/await function:

Async function someFunction() {
    await anyPromiseFunction()
         await anotherPromiseFunction()
    return "done"
}
// To get the returned value, we can use .then()
anyName().then(value => console.log(value)) // "done"
// we can also call the function inside another Async/await function
Async function resolveAnyName() {
   const result = await anyName()
   console.log(result)
}
resolveAnyName() // "done"

With this knowledge, here is how we can rewrite the promise execution from the previous section instead of using Promise.all([addA(), addB()]):

let syncarray = ["1", "2", "2", "4", "5"] 
function addA(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            syncarray.forEach((value, index) => {
                syncarray[index] = value + "+A";
            })
            resolve()
        }, 1000);
    })
}
function addB(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            syncarray.forEach((value, index) => {
                syncarray[index] = value + "+B";
            })
            resolve()
        }, 2000);
    })
}
Async function runPromises(){ 
    await addA() 
    await  addB() 
    console.log(syncarray); 
  } 
runPromises() 
//output: [ '1+A+B', '2+A+B', '2+A+B', '4+A+B', '5+A+B' ]

You can see from the preceding output that we have the same output as when using the Promise.all syntax, but are adopting a minimal and cleaner approach.

Note

One drawback of using multiple awaits as opposed to promise.all is efficiency. Though minor, promise.all is the preferred and recommended way to handle multiple independent promises.

This thread (https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await) on Stack Overflow clearly explains why this is the recommended way to handle multiple promises.

In the next section, we'll discuss object-oriented programming (OOP) in JavaScript, and how to use ES6 classes.