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 . 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 💪. here ⚡ Quickstart So, we are building . A small web application build with , and the API, to grab the download statistics of packages and generate charts based on them. npm-stats.org Vue.js vue-chartjs npm 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 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. last-month 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 data model which was set to . As the period will now be composed of and . We also set the to the current day. As most of the time you will only change the start date. period last-month periodStart periodEnd periodEnd We need a format like this: . However if we now select a date we get something like this: . A with the time attributes, which we don't need. This is a nice job for . 2017-04-18:2017-04-30 2017-04-17T22:00:00.000Z Date() moment.js Computing the period For this case we have 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 properties 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 property. period And thats it! We don’t need to change our request, as we simply replaced the content of which we are using in our request. You can check the code on github in this period 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 ( ) and one with the data ( ). For the daily statistics, it was pretty easy. As we could simply use to get the data and labels. However now we need to do more. day downloads map() Format our key to a year. day For the labels, remove the duplicates so we have only the unique years 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 file. Start.vue So we create which will help us to format our date. src/utils/dateFormatter.js 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 we can now remove the import and import our helper modules and replace the moment statements with them. (In our and . And we add a new computed property to which will replace the in our chart container. Start.vue moment _startPeriod _endPeriod period 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 which will hold our downloads data we get from the api call, and . And we create a new method called . rawData downloadsYear: [] labelsYear: [] formatYear() In our axios request promise we then assign the data.downloads to and call . rawData 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 which will contain two methods: src/utils/downloadFormatter.js removeDuplicate (a, b) {..} getDownloadsPerYear(data) {…} In our we import both modules and now we can use them in our method. Start.vue formatYear() 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 to get the key like we did in the request for our daily statistics. But now we format it with our helper. So now our data will look like this: map() day dateToYear() data: [{day: "2017", downloads: 16280},{day: "2017", downloads: 14280},{day: "2017", downloads: 17280}] And now we will use 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 reduce() downloadFormatter.js In our we now finish our removeDuplicate function. downloadFormatter.js export function removeDuplicate (a, b) { if (a.indexOf(b) < 0) { a.push(b) } return a } Now our array will contain only unique years. And it's time to group and transform our data. For this we will again use and and . this.labelYear map() reduce() filter() The looks like this: rawData 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 then we chain a to it and return an object with the date and the downloads which are in that date. Thats why we use there and our helper. reduce() map() filter() dateToYear() However our data will look now like this: First reduce But as we don’t need the date now, we it to only the downloads and then use 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. map reduce() After map and second reduce But a last will fix this. map() 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 props. downloadsYear 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 method and change our helper to . But, this is not very DRY. Because we end up with multiple methods which are doing pretty much the same. getDownloadsPerYear() dateToYear() dateToMonth() So let’s refactor our specific 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. getDownloadsPerYear() Rename our method to something more general like groupData Add a second argument which will be our helper function ??? 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 we now need to pass our helper function. Start.vue 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 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. feature branch I hope you enjoyed it and learned something. Feel free to leave me feedback! ✌ You can follow me on and . twitter github
Share Your Thoughts