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
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
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
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
}
},
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>
📺 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}]
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}]
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’
}
},
💅 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>
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>
🤖 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:
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: ‘2017–03–20’, downloads: ‘3’},
{day: ‘2017–03–21’, downloads: ‘2’},
{day: ‘2017–03–22’, downloads: ‘10’},
]
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
})
}
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
},
]
})
💎 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
},
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
}
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}`)
}
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
}
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()
}
},
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}`)
}
}
}
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)