Jakub Juszczak

@apertureless

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

Second part of building a webapp which interacts with the npm api and generate charts

In case you missed the first part, you can find it here. First of all, wow! Thanks for all the feedback and twitter messages! 💝 I never imagined to reach such a wide audience. But enough of that. Let’s get to work 💪.

⚡ Quickstart

So, we are building npm-stats.org. A small web application build with Vue.js, vue-chartjs and the npm API, to grab the download statistics of packages and generate charts based on them.

What we have done so far

A small recap of what we have build in Part 1:

  • ✅ Create a vue.js application with vue-init
  • ✅ Install dependencies and setup the vue-router
  • ✅ Create a Line Chart component with vue-chartjs
  • ✅ Make an API call to npm and render the daily downloads statistics of the last-month

This is quite a lot. In the end we had our app running. However there is always space for improvement!

What we will do today:

  • ⚙ Add settings to change the start and end period
  • 📆 Integrate an external datepicker component
  • 📈 Mutate our data to add a yearly statistics chart
  • 🔨 Refactor our methods and DRY out a bit so we can easily add more charts

⚙ Settings

Right now our default period is set to last-month but it would be awesome if we could set the startPeriod and endPeriod. This way we could inspect the statistics for a whole year or more.

For this purpose we need to add two additional input fields and data models. But for a better user experience we will pull in an external datepicker component. We don’t have to reinvent the wheel right? 👨‍🔬 And to format our date properly we will also pull in moment.js

Datepicker

📆 Install dependencies

yarn add vuejs-datepicker moment

Todos for our Start.vue:

  • Import the datepicker
  • Add two datepicker fields for the start and end period
  • Add two data models
// Start.vue

<template>

...

<datepicker placeholder="Start Date" v-model="periodStart" name="start-date"></datepicker>
<datepicker placeholder="Start Date" v-model="periodStart" name="start-date"></datepicker>

...

</template>

<script>
import axios from 'axios'
import Datepicker from 'vuejs-datepicker'
import LineChart from '@/components/LineChart'

export default {
components: {
LineChart,
Datepicker
},
data () {
return {
package: null,
packageName: '',
loaded: false,
downloads: [],
labels: [],
showError: false,
errorMessage: 'Please enter a package name',
periodStart: '',
periodEnd: new Date()
}
},

.....
}

</script>

We also removed our old period data model which was set to last-month. As the period will now be composed of periodStart and periodEnd. We also set the periodEnd to the current day. As most of the time you will only change the start date.

We need a format like this: 2017-04-18:2017-04-30. However if we now select a date we get something like this: 2017-04-17T22:00:00.000Z. A Date() with the time attributes, which we don't need. This is a nice job for moment.js.

Computing the period

For this case we have computed properties which are a pleasure to work with. To keep things a bit cleaner, we will create three properties. A formatted startDate, a formatted endDate and the composed period.

computed: {
_endDate () {
return moment(this.periodEnd).format('YYYY-MM-DD')
},
_startDate () {
return moment(this.periodStart).format('YYYY-MM-DD')
},
period () {
return this.periodStart ?
`${this._startDate}:${this._endDate}` :
'last-month'
}
},

As we want to persist the default behaviour of fetching data of the last month if no start date is set, we add our condition to the period property.

And thats it! We don’t need to change our request, as we simply replaced the content of period which we are using in our request. You can check the code on github in this feature branch

📈 More charts

Well, the chart with the daily statistics is great. But we can generate more! We have all data we need for that. We will not transform and group our data, so we can pass it to another line chart for yearly statistics. And we will refactor a bit our code.

All this data

So, our data we get from the npm api looks like this:

data: [
{day: "2017-04-18", downloads: 16280},
{day: "2017-04-19", downloads: 14280},
{day: "2017-04-20", downloads: 17280}
]

But to pass it to our chart we need two arrays, one which the labels (day) and one with the data (downloads). For the daily statistics, it was pretty easy. As we could simply use map() to get the data and labels. However now we need to do more.

  1. Format our day key to a year.
  2. For the labels, remove the duplicates so we have only the unique years
  3. Sum all the downloads in the same year.

☝ But first, it is a good time to refactor some bits of our code. We see that step 1 is to format our date to a year. And later if we want monthly statistics we need to format it to a month format and so on. So it is a good time to introduce a helper method and extract the logic from the Start.vue file.

So we create src/utils/dateFormatter.js which will help us to format our date.

import moment from 'moment'

export const dateToYear = date => moment(date).format('YYYY')
export const dateToMonth = date => moment(date).format('MMM YYYY')
export const dateToWeek = date => moment(date).format('GGGG-[W]WW')
export const dateToDay = date => moment(date).format('YYYY-MM-DD')
export const dateBeautify = date => moment(date).format('Do MMMM YYYY')

And in our Start.vue we can now remove the moment import and import our helper modules and replace the moment statements with them. (In our _startPeriod and _endPeriod. And we add a new computed property to which will replace the period in our chart container.

import { dateToYear, dateToDay, dateBeautify } from '../utils/dateFormatter'

computed: {
_endDate () {
return dateToDay(this.periodEnd)
},
_startDate () {
return dateToDay(this.periodStart)
},
period () {
return this.periodStart ? `${this._startDate}:${this._endDate}` : 'last-month'
},
formattedPeriod () {
return this.periodStart ? `${dateBeautify(this._startDate)} - ${dateBeautify(this._endDate)}` : 'last-month'
}
},
Chart with formattedPeriod in title

Time to transform

Now we create three new data models rawData which will hold our downloads data we get from the api call, downloadsYear: [] and labelsYear: []. And we create a new method called formatYear() .

In our axios request promise we then assign the data.downloads to rawData and call formatYear().

axios.get(`https://api.npmjs.org/downloads/range/${this.period}/${this.package}`)
.then(response => {
this.rawData = response.data.downloads // 🆕
this.downloads = response.data.downloads.map(entry => entry.downloads)
this.labels = response.data.downloads.map(entry => entry.day)
this.packageName = response.data.package
this.formatYear() // 🆕
this.setURL()
this.loaded = true

})

Now we will create two additional helper methods. Because we want different data transformations in the future. Like weekly stats and monthly. And to keep our component clean we extract those into a separate file.

So we create our src/utils/downloadFormatter.js which will contain two methods:

  1. removeDuplicate (a, b) {..}
  2. getDownloadsPerYear(data) {…}

In our Start.vue we import both modules and now we can use them in our formatYear() method.

formatYear () {
this.labelsYear = this.rawData
.map(entry => dateToYear(entry.day))
.reduce(removeDuplicate, [])
this.downloadsYear = getDownloadsPerYear(this.rawData)
},

Now thats a bit tricky now. Normally I write down my methods and clean then up later. However I hope you can follow me here.

First we need to get the years, right? So we use again map() to get the day key like we did in the request for our daily statistics. But now we format it with our dateToYear() helper. So now our data will look like this:

data: [
{day: "2017", downloads: 16280},
{day: "2017", downloads: 14280},
{day: "2017", downloads: 17280}
]

And now we will use reduce() to remove the duplicated years. Because our labels array will have only unique years. And as we may use this more then once, we extracted it into our downloadFormatter.js

In our downloadFormatter.js we now finish our removeDuplicate function.

export function removeDuplicate (a, b) {
if (a.indexOf(b) < 0) {
a.push(b)
}
return a
}

Now our this.labelYear array will contain only unique years. And it's time to group and transform our data. For this we will again use map() and reduce() and filter().

The rawData looks like this:

data: [
{day: "2017-04-18", downloads: 16280},
{day: "2017-04-19", downloads: 14280},
{day: "2017-04-20", downloads: 17280}
]

So first we need to find the unique dates again. Which we do with reduce() then we chain a map() to it and return an object with the date and the downloads which are in that date. Thats why we use filter() there and our dateToYear() helper.

However our data will look now like this:

First reduce

But as we don’t need the date now, we map it to only the downloads and then use reduce() to sum it up. Now our object will contain the sum of all downloads of the year. However it is nested and we still have the date key.

After map and second reduce

But a last map() will fix this.

export const getDownloadsPerYear = (data) => {
// Find unique dates
return data.reduce((date, current) => {
if (date.indexOf(dateToYear(current.day)) < 0) {
date.push(dateToYear(current.day))
}
return date
}, [])
.map((date) => {
return {
date: date,
downloads: data.filter(el => dateToYear(el.day) === date)
.map(el => el.downloads)
.reduce((total, download) => total + download)
}
})
.map(element => element.downloads)
}

Chart time

Now we have our transformation done and can copy the template for our first chart and pass in the downloadsYear props.

Downloads per Year Chart

You can check the source up to this point at this feature branch

🔨 Refactor and more charts

As we now finished our data transformer it is pretty easy to add new charts. Like weekly statistics and monthly. We could duplicate our getDownloadsPerYear() method and change our dateToYear() helper to dateToMonth(). But, this is not very DRY. Because we end up with multiple methods which are doing pretty much the same.

So let’s refactor our specific getDownloadsPerYear() method to a more general one. The only thing, that would change it our dateFormatter helper. So we should make it an argument you can pass to our method.

  1. Rename our method to something more general like groupData
  2. Add a second argument which will be our helper function
  3. ???
  4. Profit!
export const groupData = (data, dateFormatter) => {
return data.reduce((date, current) => {
if (date.indexOf(dateFormatter(current.day)) < 0) {
date.push(dateFormatter(current.day))
}
return date
}, [])
.map((date) => {
return {
date: date,
downloads: data.filter(el => dateFormatter(el.day) === date)
.map(el => el.downloads)
.reduce((total, download) => total + download)
}
})
.map(element => element.downloads)
}

In our Start.vue we now need to pass our helper function.

this.downloadsYear = groupData(this.rawData, dateToYear)

Now we can do this for our monthly and weekly data too, with only swapping out the dateToYear

You can find the source up to this point in this feature branch with some additions like a loading indicator and a new fetch of the data if the datepicker value changed, which I did not covered here.

I hope you enjoyed it and learned something. Feel free to leave me feedback! ✌
You can follow me on twitter and github.

More by Jakub Juszczak

Topics of interest

More Related Stories