DEV Community

Cover image for Let’s Build a Web App with Vue, Chart.js and an API
Jakub Juszczak
Jakub Juszczak

Posted on

Let’s Build a Web App with Vue, Chart.js and an API

Data is beautiful. And with modern technologies it is crazy easy to visualize your data and create great experiences. In this quick how to, we cover how to interact with the npm 💘 API to get download statistics of a package and generate a chart from this data with Chart.js

âš¡ Quickstart

We will build npm-stats.org and will be using following tools:

  • Vue.js with vue-router
  • Chart.js
  • vue-chartjs
  • vue-cli
  • axios

With Vue.js we will build the basic interface of the app and and routing with vue-router. And we scaffold our project with vue-cli which creates our basic project structure. For the chart generation we will be using Chart.js and as a wrapper for Vue, vue-chartjs. As we need to interact with an API, we’re using axios to make the http requests. However feel free to swap that one out with any other lib.

🔧 Install & Setup

At first we need to install vue-cli to scaffold our project. I hope you have a current version of node and npm already installed! 🙏 Even better if you have yarn installed! If not, you really should! If you don’t want, just swap out the yarn commands with the npm equivalents.

$ npm install -g vue-cli
Enter fullscreen mode Exit fullscreen mode

Then we can scaffold our project with vue-cli. If you want to can enable the unit and e2e tests, however we will not cover them.🔥 But you need to check vue-router!

$ vue init webpack npm-stats
Enter fullscreen mode Exit fullscreen mode

Then we cd in our project folder and install the dependencies with cd npm-stats && yarn install. So our basic project dependencies are installed. Now we need to add the one for our app.

$ yarn add vue-chartjs chart.js axios
Enter fullscreen mode Exit fullscreen mode

Just a quick check if everything is running with yarn run dev. Now we should see the boilerplate page of vue.

Aaaand we’re done! 👏

💪 Time to build

Just a small disclaimer here, I will not focus on the styling. I guess you’re able to make the site look good by your own 💅 so we only cover the javascript related code.
And another disclaimer, this is rather a small MVP then super clean code right now. I will refactor some of it in later stages. Like in the real world.

Components

Let’s think about what components we need. As we’re looking at the screenshot we see an input field for the package name you’re looking for and a button. Maybe a header and footer and the chart itself.

You totally could make the button and input field a component however as we don’t build a complex app, why bother? Make it simple. Make it work!

So I ended up with following components:

  • components/Footer.vue
  • components/Header.vue
  • components/LineChart.vue
  • pages/Start.vue

I will skip the Header and Footer as they only contain the logo and some links. Nothing special here. The LineChart and Start page are the important ones.

LineChart

The LineChart component will be our chart.js instance which renders the chart. We need to import the Line component and extend it. We create two props for now. One for the data which is the number of downloads and the labels which are for example the days, weeks, years.

props: {
 chartData: {
   type: Array,
   required: false
 },
 chartLabels: {
   type: Array,
   required: true
 }
},
Enter fullscreen mode Exit fullscreen mode

As we want all our charts to look the same, we define some of the Chart.js styling options in a data model which get passed as options to the renderChart() method.

And as we will have only one dataset for now, we can just build up the dataset array and bind the labels and data.

<script>
  import { Line } from 'vue-chartjs'
  export default Line.extend({
    props: {
      chartData: {
        type: Array | Object,
        required: false
      },
      chartLabels: {
        type: Array,
        required: true
      }
    },
    data () {
      return {
        options: {
          scales: {
            yAxes: [{
              ticks: {
                beginAtZero: true
              },
              gridLines: {
                display: true
              }
            }],
            xAxes: [ {
              gridLines: {
                display: false
              }
            }]
          },
          legend: {
            display: false
          },
          responsive: true,
          maintainAspectRatio: false
        }
      }
    },
    mounted () {
      this.renderChart({
        labels: this.chartLabels,
        datasets: [
          {
            label: 'downloads',
            borderColor: '#249EBF',
            pointBackgroundColor: 'white',
            borderWidth: 1,
            pointBorderColor: '#249EBF',
            backgroundColor: 'transparent',
            data: this.chartData
          }
        ]
      }, this.options)
    }
  })
</script>
Enter fullscreen mode Exit fullscreen mode

📺 Our start page

As we have our LineChart component up and working. It’s time to build the rest. We need an input field and button to submit the package name. Then request the data and pass the data to our chart component.

So, let’s first think about what data we need and what states / data models. First of all we need a package data model, which we will use with v-model in our input field. We also want to display the name of the package as a headline. So packageName would be good. Then our two arrays for the requested data downloads and labels and as we’re requesting a time period we need to set the period. But, maybe the request goes wrong so we need errorMessage and showError. And last but not least loaded as we want to show the chart only after the request is made.

npm API

There are various endpoints to get the downloads of a package. One is for example

GET https://api.npmjs.org/downloads/point/{period}[/{package}]
Enter fullscreen mode Exit fullscreen mode

However this one gets only a point value. So the total downloads. But to draw our cool chart, we need more data. So we need the range endpoint.

GET https://api.npmjs.org/downloads/range/{period}[/{package}]
Enter fullscreen mode Exit fullscreen mode

The period can be defined as for example last-day or last-month or a specific date range 2017-01-01:2017-04-19 But to keep it simple we set the default value to last-month. Later in Part II we can then add some date input fields so the user can set a date range.

So our data models are looking like this:

data () {
 return {
  package: null,
  packageName: ‘’,
  period: ‘last-month,
  loaded: false,
  downloads: [],
  labels: [],
  showError: false,
  errorMessage: ‘Please enter a package name
 }
},
Enter fullscreen mode Exit fullscreen mode

💅 Template

Now it’s time to build up the template. We need 5 things:

  • Input field
  • Button to trigger the search
  • Error message output
  • Headline with the package name
  • Our chart.
<input
 class=”Search__input”
 @keyup.enter=”requestData”
 placeholder=”npm package name”
 type=”search” name=”search”
 v-model=”package”
 >
<button class=”Search__button” @click=”requestData”>Find</button>
<div class="error-message" v-if="showError">
  {{ errorMessage }}
</div>
<h1 class="title" v-if="loaded">{{ packageName }}</h1>
<line-chart v-if="loaded" :chart-data="downloads" :chart-labels="labels"></line-chart>
Enter fullscreen mode Exit fullscreen mode

Just ignore the css classes for now. We have our Input Field which has an keyup event on enter. So if you press enter you trigger the requestData() method. And we bind v-model to package

For the potential error we have a condition, only if showError is true we show the message. There are two types or errors that can occur. The one is, someone try to search for a package without entering any name or he’s entering a name that does not exist.

For the first case, we have our default errorMessage, for the second case we will grab the error message that comes from the request.

So our full template will look like this:

<template>
  <div class="content">
    <div class="container">
      <div class="Search__container">
        <input
          class="Search__input"
          @keyup.enter="requestData"
          placeholder="npm package name"
          type="search" name="search"
          v-model="package"
        >
        <button class="Search__button" @click="requestData">Find</button>
      </div>
      <div class="error-message" v-if="showError">
       {{ errorMessage }}
      </div>
      <hr>
      <h1 class="title" v-if="loaded">{{ packageName }}</h1>
      <div class="Chart__container" v-if="loaded">
        <div class="Chart__title">
          Downloads per Day <span>{{ period }}</span>
          <hr>
        </div>
        <div class="Chart__content">
          <line-chart v-if="loaded" :chart-data="downloads" :chart-labels="labels"></line-chart>
        </div>
      </div>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

🤖 Javascript

Now it’s time for the coding. First we will do our requestData() method. It is rather simple. We need to make a request to our endpoint and then map the data that comes in. In our response.data we have some information about the package:

response.data

Like the start data, end date, the package name and then the downloads array. However the structure for the downloads array is something like this:

downloads: [
 {day: ‘20170320, downloads: ‘3},
 {day: ‘20170321, downloads: ‘2},
 {day: ‘20170322, downloads: ‘10},
]
Enter fullscreen mode Exit fullscreen mode

But we need to separate the downloads and days, because for chart.js we need one array only with the data (downloads) and one array with the labels (day). This is an easy job for map.

requestData () {
 axios.get(`https://api.npmjs.org/downloads/range/${this.period}/${this.package}`)
 .then(response => {
   this.downloads = response.data.downloads.map(download => download.downloads)
   this.labels = response.data.downloads.map(download => download.day)
   this.packageName = response.data.package
   this.loaded = true
 })
 .catch(err => {
   this.errorMessage = err.response.data.error
   this.showError = true
 })
}
Enter fullscreen mode Exit fullscreen mode

Now if we enter a package name, like vue and hit enter, the request is made, the data mapped and the chart rendered! But, wait. You don’t see anything. Because we need to tell vue-router to set the index to our start page.

Under router/index.js we import or page and tell the router to use it

import Vue from ‘vue
import Router from ‘vue-router
import StartPage from ‘@/pages/Start
Vue.use(Router)
export default new Router({
 routes: [
   {
     path: ‘/,
     name: ‘Start,
     component: StartPage
   },
 ]
})
Enter fullscreen mode Exit fullscreen mode

💎 Polish

But, we are not done yet. We have some issues, right? First our app breaks if we don’t enter any name. And we have problems if you enter a new package and hit enter. And after an error the message does not disappear.

Well, it’s time to clean up a bit. First let’s create a new method to reset our state.

resetState () {
 this.loaded = false
 this.showError = false
},
Enter fullscreen mode Exit fullscreen mode

Which we call in our requestData() method before the axios api call. And we need a check for the package name.

if (this.package === null 
    || this.package === ‘’ 
    || this.package === ‘undefined) {
  this.showError = true
  return
}
Enter fullscreen mode Exit fullscreen mode

Now if we try to search an empty package name, we get or default errorMessage.

I know, we covered a lot, but let’s add another small cool feature. We have vue-router, but not really using it. At our root / we see the starting page with the input field. And after a search we stay at our root page. But it would be cool if we could share our link with the stats, wouldn’t it be?

So after a valid search, we add the package name to our url.

npm-stats.org/#/vue-chartjs

And if we click on that link, we need to grab the package name and use it to request our data.
Let’s create a new method to set our url

setURL () {
 history.pushState({ info: `npm-stats ${this.package}`}, this.package, `/#/${this.package}`)
 }
Enter fullscreen mode Exit fullscreen mode

We need to call this.setURL() in our response promise. Now after the request is made, we add the package name to our URL. But, if we open a new browser tab and call it, nothing happens. Because we need to tell vue-router that everything after our / will also point to the start page and define the string as a query param. Which is super easy.

In our router/index.js we just need to set another path in the routes array. We call the param package.

{
  path: ‘/:package,
  component: StartPage
}
Enter fullscreen mode Exit fullscreen mode

Now if you go to localhost:8080/#/react-vr you will get the start page. But without a chart. Because we need to grab the param and do our request with it.

Back in our Start.vue we grab the param in the mounted hook.

mounted () {
 if (this.$route.params.package) {
   this.package = this.$route.params.package
   this.requestData()
 }
},

Enter fullscreen mode Exit fullscreen mode

And thats it! Complete file:

 import axios from 'axios'
  import LineChart from '@/components/LineChart'
  export default {
    components: {
      LineChart
    },
    props: {},
    data () {
      return {
        package: null,
        packageName: '',
        period: 'last-month',
        loaded: false,
        downloads: [],
        labels: [],
        showError: false,
        errorMessage: 'Please enter a package name'
      }
    },
    mounted () {
      if (this.$route.params.package) {
        this.package = this.$route.params.package
        this.requestData()
      }
    },
    methods: {
      resetState () {
        this.loaded = false
        this.showError = false
      },
      requestData () {
        if (this.package === null || this.package === '' || this.package === 'undefined') {
          this.showError = true
          return
        }
        this.resetState()
        axios.get(`https://api.npmjs.org/downloads/range/${this.period}/${this.package}`)
          .then(response => {
            console.log(response.data)
            this.downloads = response.data.downloads.map(download => download.downloads)
            this.labels = response.data.downloads.map(download => download.day)
            this.packageName = response.data.package
            this.setURL()
            this.loaded = true
          })
          .catch(err => {
            this.errorMessage = err.response.data.error
            this.showError = true
          })
      },
      setURL () {
        history.pushState({ info: `npm-stats ${this.package}` }, this.package, `/#/${this.package}`)
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

You can check out the full source at GitHub and view the demo page at 📺 npm-stats.org

Improvements

But hey, there is still room for improvements. We could add more charts. Like monthly statistics, yearly statistics and add date fields to set the period and many more things. I will cover some of them in Part II ! So stay tuned!

Top comments (0)