├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .verb.md ├── LICENSE ├── examples ├── get-point.js └── simple-get.js ├── index.js ├── lib ├── calculate.js ├── get.js └── utils.js ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false 15 | 16 | [{,test/}{actual,fixtures}/**] 17 | trim_trailing_whitespace = false 18 | insert_final_newline = false 19 | 20 | [templates/**] 21 | trim_trailing_whitespace = false 22 | insert_final_newline = false 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text eol=lf 3 | 4 | # binaries 5 | *.ai binary 6 | *.psd binary 7 | *.jpg binary 8 | *.gif binary 9 | *.png binary 10 | *.jpeg binary 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.sublime-* 3 | _gh_pages 4 | bower_components 5 | node_modules 6 | npm-debug.log 7 | actual 8 | test/actual 9 | temp 10 | tmp 11 | TODO.md 12 | vendor 13 | .idea 14 | benchmark 15 | coverage 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '6' 5 | - '5' 6 | - '4' 7 | - '0.12' 8 | - '0.10' 9 | matrix: 10 | fast_finish: true 11 | allow_failures: 12 | - node_js: '4' 13 | - node_js: '0.10' 14 | - node_js: '0.12' 15 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ```js 4 | var stats = require('{%= name %}'); 5 | ``` 6 | 7 | ## API 8 | {%= apidocs("index.js") %} 9 | 10 | ## Get downloads 11 | {%= apidocs("./lib/get.js") %} 12 | 13 | ## Calculate 14 | {%= apidocs("./lib/calculate.js") %} 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016, Brian Woodward. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/get-point.js: -------------------------------------------------------------------------------- 1 | 'use string'; 2 | 3 | var stats = require('../'); 4 | 5 | // get total downloads for the last-month for micromath 6 | stats.get.point('last-month', 'micromatch', function(err, results) { 7 | if (err) return console.error(err); 8 | console.log('last-month', results); 9 | }); 10 | // last-month { downloads: 7750788, start: '2016-10-10', end: '2016-11-08', package: 'micromatch' } 11 | 12 | stats.get.allTime('micromatch', function(err, results) { 13 | if (err) return console.error(err); 14 | console.log('all-time', results); 15 | }); 16 | 17 | stats.get.lastMonth('micromatch', function(err, results) { 18 | if (err) return console.error(err); 19 | console.log('last-month', results); 20 | }); 21 | // last-month { downloads: 7750788, start: '2016-10-10', end: '2016-11-08', package: 'micromatch' } 22 | 23 | stats.get.lastWeek('micromatch', function(err, results) { 24 | if (err) return console.error(err); 25 | console.log('last-week', results); 26 | }); 27 | // last-week { downloads: 1777065, start: '2016-11-02', end: '2016-11-08', package: 'micromatch' } 28 | 29 | stats.get.lastDay('micromatch', function(err, results) { 30 | if (err) return console.error(err); 31 | console.log('last-day', results); 32 | }); 33 | // last-day { downloads: 316004, start: '2016-11-08', end: '2016-11-08', package: 'micromatch' } 34 | -------------------------------------------------------------------------------- /examples/simple-get.js: -------------------------------------------------------------------------------- 1 | 'use string'; 2 | 3 | var stats = require('../'); 4 | var start = new Date('2016-01-09'); 5 | var end = new Date('2016-01-10'); 6 | 7 | // get 2 days worth of downloads for micromatch 8 | stats.get(start, end, 'micromatch') 9 | .on('error', console.error) 10 | .on('data', function(data) { 11 | console.log(data); 12 | }) 13 | .on('end', function() { 14 | console.log('done.'); 15 | }); 16 | 17 | // { day: '2016-01-09', downloads: 53331 } 18 | // { day: '2016-01-10', downloads: 47341 } 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * download-stats 3 | * 4 | * Copyright (c) 2016, Brian Woodward. 5 | * Licensed under the MIT License. 6 | */ 7 | 8 | 'use strict'; 9 | var calc = require('./lib/calculate'); 10 | var utils = require('./lib/utils'); 11 | var get = require('./lib/get'); 12 | var stats = {}; 13 | 14 | /** 15 | * Get a range of download counts for the specified repository. 16 | * This method returns a stream of raw data 17 | * in the form of `{ day: '2016-01-10', downloads: 123456 }`. 18 | * 19 | * ```js 20 | * var start = new Date('2016-01-09'); 21 | * var end = new Date('2016-01-10'); 22 | * stats.get(start, end, 'micromatch') 23 | * .on('error', console.error) 24 | * .on('data', function(data) { 25 | * console.log(data); 26 | * }) 27 | * .on('end', function() { 28 | * console.log('done.'); 29 | * }); 30 | * // { day: '2016-01-09', downloads: 53331 } 31 | * // { day: '2016-01-10', downloads: 47341 } 32 | * ``` 33 | * 34 | * @param {Date} `start` Start date of stream. 35 | * @param {Date} `end` End date of stream. 36 | * @param {String} `repo` Repository to get downloads for. If `repo` is not passed, then all npm downloads for the day will be returned. 37 | * @return {Stream} Stream of download data. 38 | * @api public 39 | * @name get 40 | */ 41 | 42 | stats.get = get; 43 | 44 | /** 45 | * Calculate object containing methods to calculate stats on arrays of download counts. 46 | * See [calculate][#calculate] api docs for more information. 47 | * 48 | * @api public 49 | * @name calc 50 | */ 51 | 52 | stats.calc = calc; 53 | 54 | /** 55 | * Exposes `stats` 56 | */ 57 | 58 | module.exports = stats; 59 | -------------------------------------------------------------------------------- /lib/calculate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('./utils'); 4 | 5 | var calculate = module.exports = {}; 6 | 7 | /** 8 | * Group array into object where keys are groups and values are arrays. 9 | * Groups determined by provided `fn`. 10 | * 11 | * ```js 12 | * var groups = calculate.group(downloads, function(download) { 13 | * // day is formatted as '2010-12-25' 14 | * // add this download to the '2010-12' group 15 | * return download.day.substr(0, 7); 16 | * }); 17 | * ``` 18 | * @param {Array} `arr` Array of download objects 19 | * @param {Function} `fn` Function to determine group the download belongs in. 20 | * @return {String} Key to use for the group 21 | * @api public 22 | */ 23 | 24 | calculate.group = function(arr, fn) { 25 | var groups = {}; 26 | var len = arr.length, i = 0; 27 | while (len--) { 28 | var download = arr[i++]; 29 | var groupArr = utils.arrayify(fn(download)); 30 | groupArr.reduce(function(acc, group) { 31 | if (typeof group === 'string') { 32 | group = { name: group }; 33 | } 34 | acc[group.name] = acc[group.name] || group; 35 | acc[group.name].downloads = acc[group.name].downloads || []; 36 | acc[group.name].downloads.push(download); 37 | return acc; 38 | }, groups); 39 | } 40 | return groups; 41 | }; 42 | 43 | /** 44 | * Calculate the total for each group (key) in the object. 45 | * 46 | * @name group.total 47 | * @param {Object} `groups` Object created by a `group` function. 48 | * @return {Object} Object with calculated totals 49 | * @api public 50 | */ 51 | 52 | calculate.group.total = function(groups) { 53 | var res = {}; 54 | var keys = Object.keys(groups); 55 | var len = keys.length, i = 0; 56 | while (len--) { 57 | var key = keys[i++]; 58 | var group = groups[key]; 59 | if (Array.isArray(group)) { 60 | res[key] = calculate.total(group); 61 | } else { 62 | res[key] = calculate.total(group.downloads); 63 | } 64 | } 65 | return res; 66 | }; 67 | 68 | /** 69 | * Calculate the total downloads for an array of download objects. 70 | * 71 | * @param {Array} `arr` Array of download objects (must have a `.downloads` property) 72 | * @return {Number} Total of all downloads in the array. 73 | * @api public 74 | */ 75 | 76 | calculate.total = function(arr) { 77 | arr = utils.arrayify(arr); 78 | var len = arr.length, i = 0; 79 | var total = 0; 80 | while (len--) total += arr[i++].downloads || 0; 81 | return total; 82 | }; 83 | 84 | /** 85 | * Calculate the average for each group (key) in the object. 86 | * 87 | * @name group.avg 88 | * @param {Object} `groups` Object created by a `group` function. 89 | * @return {Object} Object with calculated average 90 | * @api public 91 | */ 92 | 93 | calculate.group.avg = function(groups, days) { 94 | var res = {}; 95 | var keys = Object.keys(groups); 96 | var len = keys.length, i = 0; 97 | while (len--) { 98 | var key = keys[i++]; 99 | res[key] = calculate.avg(groups[key], days); 100 | } 101 | return res; 102 | }; 103 | 104 | /** 105 | * Calculate the average downloads for an array of download objects. 106 | * 107 | * @param {Array} `arr` Array of download objects (must have a `.downloads` property) 108 | * @return {Number} Average of all downloads in the array. 109 | * @api public 110 | */ 111 | 112 | calculate.avg = function(arr, days) { 113 | arr = utils.arrayify(arr); 114 | var len = arr.length, i = 0; 115 | var total = 0; 116 | while (len--) { 117 | total += arr[i++].downloads || 0; 118 | } 119 | 120 | if (typeof days === 'undefined' || days === 0) { 121 | days = arr.length; 122 | } 123 | return total / days; 124 | }; 125 | 126 | /** 127 | * Create an array of downloads before specified day. 128 | * 129 | * @name group.before 130 | * @param {String} `day` Day specifying last day to use in group. 131 | * @param {Array} `arr` Array of downloads to check. 132 | * @return {Array} Array of downloads happened before or on specified day. 133 | * @api public 134 | */ 135 | 136 | calculate.group.before = function(day, arr) { 137 | var end = utils.format(normalizeDate(utils.moment(day))); 138 | var group = []; 139 | var len = arr.length, i = 0; 140 | while (len--) { 141 | var download = arr[i++]; 142 | if (download.day <= end) { 143 | group.push(download); 144 | } 145 | } 146 | return group; 147 | }; 148 | 149 | /** 150 | * Calculate the total downloads happening before the specified day. 151 | * 152 | * @param {String} `day` Day specifying last day to use in group. 153 | * @param {Array} `arr` Array of downloads to check. 154 | * @return {Number} Total downloads happening before or on specified day. 155 | * @api public 156 | */ 157 | 158 | calculate.before = function(day, arr) { 159 | var group = calculate.group.before(day, arr); 160 | return calculate.total(group); 161 | }; 162 | 163 | /** 164 | * Create an array of downloads for the last `X` days. 165 | * 166 | * @name group.last 167 | * @param {Number} `days` Number of days to go back. 168 | * @param {Array} `arr` Array of downloads to check. 169 | * @param {String} `init` Optional day to use as the last day to include. (Days from `init || today` - `days` to `init || today`) 170 | * @return {Array} Array of downloads for last `X` days. 171 | * @api public 172 | */ 173 | 174 | calculate.group.last = function(days, arr, init) { 175 | var today = init ? utils.moment.utc(init) : utils.moment.utc(); 176 | var start = utils.moment.utc(today); 177 | start.subtract(days, 'days') 178 | today = utils.format(today); 179 | start = utils.format(start); 180 | 181 | var group = []; 182 | var len = arr.length, i = 0; 183 | while (len--) { 184 | var download = arr[i++]; 185 | if (download.day > start && download.day <= today) { 186 | group.push(download); 187 | } 188 | } 189 | return group; 190 | }; 191 | 192 | /** 193 | * Calculate total downloads for the last `X` days. 194 | * 195 | * @name last 196 | * @param {Number} `days` Number of days to go back. 197 | * @param {Array} `arr` Array of downloads to check. 198 | * @param {String} `init` Optional day to use as the last day to include. (Days from `init || today` - `days` to `init || today`) 199 | * @return {Array} Array of downloads for last `X` days. 200 | * @api public 201 | */ 202 | 203 | calculate.last = function(days, arr, init) { 204 | var group = calculate.group.last(days, arr, init); 205 | return calculate.total(group); 206 | }; 207 | 208 | /** 209 | * Create an array of downloads for the previous `X` days. 210 | * 211 | * @name group.prev 212 | * @param {Number} `days` Number of days to go back. 213 | * @param {Array} `arr` Array of downloads to check. 214 | * @param {String} `init` Optional day to use as the prev day to include. (Days from `init || today` - `days` - `days` to `init || today` - `days`) 215 | * @return {Array} Array of downloads for prev `X` days. 216 | * @api public 217 | */ 218 | 219 | calculate.group.prev = function(days, arr, init) { 220 | var today = init ? utils.moment(init) : utils.moment(); 221 | var end = utils.moment(today); 222 | end.subtract(days, 'days'); 223 | return calculate.group.last(days, arr, end); 224 | }; 225 | 226 | /** 227 | * Calculate total downloads for the previous `X` days. 228 | * 229 | * @name prev 230 | * @param {Number} `days` Number of days to go back. 231 | * @param {Array} `arr` Array of downloads to check. 232 | * @param {String} `init` Optional day to use as the prev day to include. (Days from `init || today` - `days` - `days` to `init || today` - `days`) 233 | * @return {Array} Array of downloads for prev `X` days. 234 | * @api public 235 | */ 236 | 237 | calculate.prev = function(days, arr, init) { 238 | var group = calculate.group.prev(days, arr, init); 239 | return calculate.total(group); 240 | }; 241 | 242 | /** 243 | * Create an object of download groups by month. 244 | * 245 | * @param {Array} `arr` Array of downloads to group and total. 246 | * @return {Object} Groups with arrays of download objects 247 | * @api public 248 | */ 249 | 250 | calculate.group.monthly = function(arr) { 251 | return calculate.group(arr, function(download) { 252 | return download.day.substr(0, 7); 253 | }); 254 | }; 255 | 256 | function normalizeDate(date) { 257 | date.utc().hour(0); 258 | date.utc().minute(0); 259 | date.utc().second(0); 260 | return date; 261 | } 262 | 263 | calculate.group.window = function(days, arr, init) { 264 | var today = init ? utils.moment(init) : normalizeDate(utils.moment()); 265 | arr = calculate.group.before(today, arr); 266 | return calculate.group(arr, function(download) { 267 | var day = utils.moment.utc(download.day); 268 | var diff = day.diff(today, 'days'); 269 | var period = Math.floor((diff * -1) / days); 270 | var start = utils.moment(today); 271 | start.subtract((period + 1) * days, 'days'); 272 | return { 273 | name: period, 274 | period: utils.format(start) 275 | }; 276 | }); 277 | }; 278 | 279 | /** 280 | * Calculate total downloads grouped by month. 281 | * 282 | * @param {Array} `arr` Array of downloads to group and total. 283 | * @return {Object} Groups with total downloads calculated 284 | * @api public 285 | */ 286 | 287 | calculate.monthly = function(arr) { 288 | var months = calculate.group.monthly(arr); 289 | return calculate.group.total(months); 290 | }; 291 | 292 | /** 293 | * Create an object of download groups by month. 294 | * 295 | * @param {Array} `arr` Array of downloads to group and total. 296 | * @return {Object} Groups with arrays of download objects 297 | * @api public 298 | */ 299 | calculate.group.yearly = function(arr) { 300 | return calculate.group(arr, function(download) { 301 | return download.day.substr(0, 4); 302 | }); 303 | }; 304 | 305 | /** 306 | * Calculate total downloads grouped by year. 307 | * 308 | * @param {Array} `arr` Array of downloads to group and total. 309 | * @return {Object} Groups with total downloads calculated 310 | * @api public 311 | */ 312 | 313 | calculate.yearly = function(arr) { 314 | var years = calculate.group.yearly(arr); 315 | return calculate.group.total(years); 316 | }; 317 | -------------------------------------------------------------------------------- /lib/get.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var utils = require('./utils'); 3 | 4 | /** 5 | * Get a range of download counts for the specified repository. 6 | * This method returns a stream of raw data 7 | * in the form of `{ day: '2016-01-10', downloads: 123456 }`. 8 | * 9 | * ```js 10 | * var start = new Date('2016-01-09'); 11 | * var end = new Date('2016-01-10'); 12 | * stats.get(start, end, 'micromatch') 13 | * .on('error', console.error) 14 | * .on('data', function(data) { 15 | * console.log(data); 16 | * }) 17 | * .on('end', function() { 18 | * console.log('done.'); 19 | * }); 20 | * // { day: '2016-01-09', downloads: 53331 } 21 | * // { day: '2016-01-10', downloads: 47341 } 22 | * ``` 23 | * 24 | * @param {Date} `start` Start date of stream. 25 | * @param {Date} `end` End date of stream. 26 | * @param {String} `repo` Repository to get downloads for. If `repo` is not passed, then all npm downloads for the day will be returned. 27 | * @return {Stream} Stream of download data. 28 | * @api public 29 | */ 30 | 31 | function get(start, end, repo) { 32 | start = utils.moment.utc(start); 33 | end = utils.moment.utc(end); 34 | var current = utils.moment.utc(start); 35 | var stream = new utils.stream.Stream(); 36 | run(); 37 | return stream; 38 | 39 | function run() { 40 | process.nextTick(function() { 41 | let period = utils.moment.utc(current).add(300, 'days'); 42 | if (period.format('YYYY-MM-DD') >= end.format('YYYY-MM-DD')) { 43 | period = utils.moment.utc(end); 44 | } 45 | 46 | getPage(current, period, repo) 47 | .on('error', stream.emit.bind(stream, 'error')) 48 | .on('data', function(data) { 49 | stream.emit('data', data); 50 | }) 51 | .on('end', function() { 52 | current.add(300, 'days'); 53 | if (current.format('YYYY-MM-DD') >= end.format('YYYY-MM-DD')) { 54 | stream.emit('end'); 55 | return; 56 | } 57 | run(); 58 | }); 59 | }); 60 | } 61 | } 62 | 63 | function getPage(start, end, repo) { 64 | var stream = new utils.stream.Stream(); 65 | var url = 'https://api.npmjs.org/downloads/range/'; 66 | url += utils.format(start); 67 | url += ':' + utils.format(end); 68 | url += (repo ? '/' + repo : ''); 69 | 70 | var bulk = false; 71 | if (repo && repo.indexOf(',') > -1) { 72 | bulk = true; 73 | } 74 | 75 | process.nextTick(function() { 76 | var req = utils.https.get(options(url), function(res) { 77 | res.on('error', console.error) 78 | .pipe(utils.JSONStream.parse(bulk ? '*' : 'downloads.*')) 79 | .on('error', handleError) 80 | .on('data', function(data) { 81 | stream.emit('data', data); 82 | }) 83 | .on('end', stream.emit.bind(stream, 'end')); 84 | }); 85 | 86 | req.on('error', stream.emit.bind(stream, 'error')); 87 | }); 88 | 89 | return stream; 90 | 91 | function handleError(err) { 92 | console.error('handling error', err); 93 | if (err.message.indexOf('Invalid JSON') >= 0) { 94 | handleInvalidJSON(); 95 | return; 96 | } 97 | stream.emit('error', err); 98 | } 99 | 100 | function handleInvalidJSON() { 101 | var body = ''; 102 | utils.https.get(options(url), function(res) { 103 | res 104 | .on('error', stream.emit.bind('error')) 105 | .on('data', function(data) { 106 | body += data; 107 | }) 108 | .on('end', function() { 109 | stream.emit('error', new Error(body)); 110 | }); 111 | }); 112 | } 113 | } 114 | 115 | /** 116 | * Get a specific point (all-time, last-month, last-week, last-day) 117 | * 118 | * ```js 119 | * stats.get.period('last-day', 'micromatch', function(err, results) { 120 | * if (err) return console.error(err); 121 | * console.log(results); 122 | * }); 123 | * // { day: '2016-01-10', downloads: 47341 } 124 | * ``` 125 | * @param {String} `period` Period to retrieve downloads for. 126 | * @param {String} `repo` Repository to retrieve downloads for. 127 | * @param {Function} `cb` Callback function to get results 128 | * @api public 129 | */ 130 | 131 | get.point = function(period, repo, cb) { 132 | var url = 'https://api.npmjs.org/downloads/point/'; 133 | url += period; 134 | url += (repo ? '/' + repo : ''); 135 | 136 | var results; 137 | var req = utils.https.get(options(url), function(res) { 138 | res.once('error', console.error) 139 | .pipe(utils.JSONStream.parse()) 140 | .once('error', cb) 141 | .on('data', function(data) { 142 | results = data; 143 | }) 144 | .once('end', function() { 145 | cb(null, results); 146 | }); 147 | }); 148 | 149 | req.once('error', cb); 150 | }; 151 | 152 | /** 153 | * Get the all time total downloads for a repository. 154 | * 155 | * ```js 156 | * stats.get.allTime('micromatch', function(err, results) { 157 | * if (err) return console.error(err); 158 | * console.log(results); 159 | * }); 160 | * // { day: '2016-01-10', downloads: 47341 } 161 | * ``` 162 | * @param {String} `repo` Repository to retrieve downloads for. 163 | * @param {Function} `cb` Callback function to get results 164 | * @api public 165 | */ 166 | 167 | get.allTime = function(repo, cb) { 168 | return get.point('all-time', repo, cb); 169 | }; 170 | 171 | /** 172 | * Get the last month's total downloads for a repository. 173 | * 174 | * ```js 175 | * stats.get.lastMonth('micromatch', function(err, results) { 176 | * if (err) return console.error(err); 177 | * console.log(results); 178 | * }); 179 | * // { downloads: 7750788, start: '2016-10-10', end: '2016-11-08', package: 'micromatch' } 180 | * ``` 181 | * @param {String} `repo` Repository to retrieve downloads for. 182 | * @param {Function} `cb` Callback function to get results 183 | * @api public 184 | */ 185 | 186 | get.lastMonth = function(repo, cb) { 187 | return get.point('last-month', repo, cb); 188 | }; 189 | 190 | /** 191 | * Get the last week's total downloads for a repository. 192 | * 193 | * ```js 194 | * stats.get.lastWeek('micromatch', function(err, results) { 195 | * if (err) return console.error(err); 196 | * console.log(results); 197 | * }); 198 | * // { downloads: 1777065, start: '2016-11-02', end: '2016-11-08', package: 'micromatch' } 199 | * ``` 200 | * @param {String} `repo` Repository to retrieve downloads for. 201 | * @param {Function} `cb` Callback function to get results 202 | * @api public 203 | */ 204 | 205 | get.lastWeek = function(repo, cb) { 206 | return get.point('last-week', repo, cb); 207 | }; 208 | 209 | /** 210 | * Get the last day's total downloads for a repository. 211 | * 212 | * ```js 213 | * stats.get.lastDay('micromatch', function(err, results) { 214 | * if (err) return console.error(err); 215 | * console.log(results); 216 | * }); 217 | * // { downloads: 316004, start: '2016-11-08', end: '2016-11-08', package: 'micromatch' } 218 | * ``` 219 | * @param {String} `repo` Repository to retrieve downloads for. 220 | * @param {Function} `cb` Callback function to get results 221 | * @api public 222 | */ 223 | 224 | get.lastDay = function(repo, cb) { 225 | return get.point('last-day', repo, cb); 226 | }; 227 | 228 | function options(url) { 229 | var opts = utils.url.parse(url); 230 | opts.headers = {'User-Agent': 'https://github.com/doowb/download-stats'}; 231 | return opts; 232 | } 233 | 234 | /** 235 | * Expose `get` 236 | */ 237 | 238 | module.exports = get; 239 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies 5 | */ 6 | 7 | var utils = require('lazy-cache')(require); 8 | 9 | /** 10 | * Temporarily re-assign `require` to trick browserify and 11 | * webpack into reconizing lazy dependencies. 12 | * 13 | * This tiny bit of ugliness has the huge dual advantage of 14 | * only loading modules that are actually called at some 15 | * point in the lifecycle of the application, whilst also 16 | * allowing browserify and webpack to find modules that 17 | * are depended on but never actually called. 18 | */ 19 | 20 | var fn = require; 21 | require = utils; 22 | 23 | /** 24 | * Lazily required module dependencies 25 | */ 26 | 27 | require('JSONStream', 'JSONStream'); 28 | require('moment'); 29 | require('https'); 30 | require('stream'); 31 | require('url'); 32 | 33 | /** 34 | * Restore `require` 35 | */ 36 | 37 | require = fn; 38 | 39 | utils.arrayify = function(val) { 40 | if (!val) return []; 41 | return Array.isArray(val) ? val : [val]; 42 | }; 43 | 44 | utils.format = function (date) { 45 | if (!utils.moment.isMoment(date)) { 46 | date = utils.moment(date); 47 | } 48 | var year = date.utc().year(); 49 | var month = date.utc().month() + 1; 50 | var day = date.utc().date(); 51 | 52 | return '' + year + '-' + utils.pad(month) + '-' + utils.pad(day); 53 | }; 54 | 55 | utils.pad = function (num) { 56 | return (num < 10 ? '0' : '') + num; 57 | }; 58 | 59 | utils.formatNumber = function (num) { 60 | num = '' + num; 61 | var len = num.length; 62 | if (len <= 3) return num; 63 | var parts = len / 3; 64 | var i = len % 3; 65 | var first = '', last = ''; 66 | if (i === 0) { 67 | i = 3; 68 | } 69 | first = num.substr(0, i); 70 | last = num.substr(i); 71 | var res = first + ',' + utils.formatNumber(last); 72 | return res; 73 | }; 74 | 75 | /** 76 | * Expose `utils` modules 77 | */ 78 | 79 | module.exports = utils; 80 | 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "download-stats", 3 | "description": "Get and calculate npm download stats for npm modules.", 4 | "version": "0.3.4", 5 | "homepage": "https://github.com/doowb/download-stats", 6 | "author": "Brian Woodward (https://github.com/doowb)", 7 | "repository": "doowb/download-stats", 8 | "bugs": { 9 | "url": "https://github.com/doowb/download-stats/issues" 10 | }, 11 | "license": "MIT", 12 | "files": [ 13 | "index.js", 14 | "lib" 15 | ], 16 | "main": "index.js", 17 | "engines": { 18 | "node": ">=0.10.0" 19 | }, 20 | "scripts": { 21 | "test": "mocha" 22 | }, 23 | "dependencies": { 24 | "JSONStream": "^1.2.1", 25 | "lazy-cache": "^2.0.1", 26 | "moment": "^2.15.1" 27 | }, 28 | "devDependencies": { 29 | "gulp-format-md": "^0.1.10", 30 | "mocha": "^3.0.2" 31 | }, 32 | "keywords": [ 33 | "calculate", 34 | "calculate-downloads", 35 | "download", 36 | "download-stats", 37 | "downloads", 38 | "get", 39 | "get-downloads", 40 | "stats" 41 | ], 42 | "verb": { 43 | "toc": true, 44 | "layout": "default", 45 | "tasks": [ 46 | "readme" 47 | ], 48 | "plugins": [ 49 | "gulp-format-md" 50 | ], 51 | "related": { 52 | "list": [ 53 | { 54 | "npm-info": true 55 | } 56 | ] 57 | }, 58 | "reflinks": [ 59 | "verb", 60 | "verb-generate-readme" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # download-stats [![NPM version](https://img.shields.io/npm/v/download-stats.svg?style=flat)](https://www.npmjs.com/package/download-stats) [![NPM downloads](https://img.shields.io/npm/dm/download-stats.svg?style=flat)](https://npmjs.org/package/download-stats) [![Linux Build Status](https://img.shields.io/travis/doowb/download-stats.svg?style=flat&label=Travis)](https://travis-ci.org/doowb/download-stats) 2 | 3 | > Get and calculate npm download stats for npm modules. 4 | 5 | ## Table of Contents 6 | 7 | - [Install](#install) 8 | - [Usage](#usage) 9 | - [API](#api) 10 | - [Get downloads](#get-downloads) 11 | - [Calculate](#calculate) 12 | - [About](#about) 13 | * [Related projects](#related-projects) 14 | * [Contributing](#contributing) 15 | * [Contributors](#contributors) 16 | * [Release history](#release-history) 17 | * [Building docs](#building-docs) 18 | * [Running tests](#running-tests) 19 | * [Author](#author) 20 | * [License](#license) 21 | 22 | _(TOC generated by [verb](https://github.com/verbose/verb) using [markdown-toc](https://github.com/jonschlinkert/markdown-toc))_ 23 | 24 | ## Install 25 | 26 | Install with [npm](https://www.npmjs.com/): 27 | 28 | ```sh 29 | $ npm install --save download-stats 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```js 35 | var stats = require('download-stats'); 36 | ``` 37 | 38 | ## API 39 | 40 | ### [.get](index.js#L42) 41 | 42 | Get a range of download counts for the specified repository. This method returns a stream of raw data in the form of `{ day: '2016-01-10', downloads: 123456 }`. 43 | 44 | **Example** 45 | 46 | ```js 47 | var start = new Date('2016-01-09'); 48 | var end = new Date('2016-01-10'); 49 | stats.get(start, end, 'micromatch') 50 | .on('error', console.error) 51 | .on('data', function(data) { 52 | console.log(data); 53 | }) 54 | .on('end', function() { 55 | console.log('done.'); 56 | }); 57 | // { day: '2016-01-09', downloads: 53331 } 58 | // { day: '2016-01-10', downloads: 47341 } 59 | ``` 60 | 61 | **Params** 62 | 63 | * `start` **{Date}**: Start date of stream. 64 | * `end` **{Date}**: End date of stream. 65 | * `repo` **{String}**: Repository to get downloads for. If `repo` is not passed, then all npm downloads for the day will be returned. 66 | * `returns` **{Stream}**: Stream of download data. 67 | 68 | ### [.calc](index.js#L52) 69 | 70 | Calculate object containing methods to calculate stats on arrays of download counts. 71 | See [calculate][#calculate] api docs for more information. 72 | 73 | ## Get downloads 74 | 75 | ### [get](lib/get.js#L31) 76 | 77 | Get a range of download counts for the specified repository. This method returns a stream of raw data in the form of `{ day: '2016-01-10', downloads: 123456 }`. 78 | 79 | **Example** 80 | 81 | ```js 82 | var start = new Date('2016-01-09'); 83 | var end = new Date('2016-01-10'); 84 | stats.get(start, end, 'micromatch') 85 | .on('error', console.error) 86 | .on('data', function(data) { 87 | console.log(data); 88 | }) 89 | .on('end', function() { 90 | console.log('done.'); 91 | }); 92 | // { day: '2016-01-09', downloads: 53331 } 93 | // { day: '2016-01-10', downloads: 47341 } 94 | ``` 95 | 96 | **Params** 97 | 98 | * `start` **{Date}**: Start date of stream. 99 | * `end` **{Date}**: End date of stream. 100 | * `repo` **{String}**: Repository to get downloads for. If `repo` is not passed, then all npm downloads for the day will be returned. 101 | * `returns` **{Stream}**: Stream of download data. 102 | 103 | ### [.point](lib/get.js#L76) 104 | 105 | Get a specific point (all-time, last-month, last-week, last-day) 106 | 107 | **Example** 108 | 109 | ```js 110 | stats.get.point('last-day', 'micromatch', function(err, results) { 111 | if (err) return console.error(err); 112 | console.log(results); 113 | }); 114 | // { day: '2016-01-10', downloads: 47341 } 115 | ``` 116 | 117 | **Params** 118 | 119 | * `period` **{String}**: Period to retrieve downloads for. 120 | * `repo` **{String}**: Repository to retrieve downloads for. 121 | * `cb` **{Function}**: Callback function to get results 122 | 123 | ### [.allTime](lib/get.js#L112) 124 | 125 | Get the all time total downloads for a repository. 126 | 127 | **Example** 128 | 129 | ```js 130 | stats.get.allTime('micromatch', function(err, results) { 131 | if (err) return console.error(err); 132 | console.log(results); 133 | }); 134 | // { day: '2016-01-10', downloads: 47341 } 135 | ``` 136 | 137 | **Params** 138 | 139 | * `repo` **{String}**: Repository to retrieve downloads for. 140 | * `cb` **{Function}**: Callback function to get results 141 | 142 | ### [.lastMonth](lib/get.js#L131) 143 | 144 | Get the last month's total downloads for a repository. 145 | 146 | **Example** 147 | 148 | ```js 149 | stats.get.lastMonth('micromatch', function(err, results) { 150 | if (err) return console.error(err); 151 | console.log(results); 152 | }); 153 | // { downloads: 7750788, start: '2016-10-10', end: '2016-11-08', package: 'micromatch' } 154 | ``` 155 | 156 | **Params** 157 | 158 | * `repo` **{String}**: Repository to retrieve downloads for. 159 | * `cb` **{Function}**: Callback function to get results 160 | 161 | ### [.lastWeek](lib/get.js#L150) 162 | 163 | Get the last week's total downloads for a repository. 164 | 165 | **Example** 166 | 167 | ```js 168 | stats.get.lastWeek('micromatch', function(err, results) { 169 | if (err) return console.error(err); 170 | console.log(results); 171 | }); 172 | // { downloads: 1777065, start: '2016-11-02', end: '2016-11-08', package: 'micromatch' } 173 | ``` 174 | 175 | **Params** 176 | 177 | * `repo` **{String}**: Repository to retrieve downloads for. 178 | * `cb` **{Function}**: Callback function to get results 179 | 180 | ### [.lastDay](lib/get.js#L169) 181 | 182 | Get the last day's total downloads for a repository. 183 | 184 | **Example** 185 | 186 | ```js 187 | stats.get.lastDay('micromatch', function(err, results) { 188 | if (err) return console.error(err); 189 | console.log(results); 190 | }); 191 | // { downloads: 316004, start: '2016-11-08', end: '2016-11-08', package: 'micromatch' } 192 | ``` 193 | 194 | **Params** 195 | 196 | * `repo` **{String}**: Repository to retrieve downloads for. 197 | * `cb` **{Function}**: Callback function to get results 198 | 199 | ## Calculate 200 | 201 | ### [.group](lib/calculate.js#L24) 202 | 203 | Group array into object where keys are groups and values are arrays. Groups determined by provided `fn`. 204 | 205 | **Example** 206 | 207 | ```js 208 | var groups = calculate.group(downloads, function(download) { 209 | // day is formatted as '2010-12-25' 210 | // add this download to the '2010-12' group 211 | return download.day.substr(0, 7); 212 | }); 213 | ``` 214 | 215 | **Params** 216 | 217 | * `arr` **{Array}**: Array of download objects 218 | * `fn` **{Function}**: Function to determine group the download belongs in. 219 | * `returns` **{String}**: Key to use for the group 220 | 221 | ### [.group.total](lib/calculate.js#L52) 222 | 223 | Calculate the total for each group (key) in the object. 224 | 225 | **Params** 226 | 227 | * `groups` **{Object}**: Object created by a `group` function. 228 | * `returns` **{Object}**: Object with calculated totals 229 | 230 | ### [.total](lib/calculate.js#L76) 231 | 232 | Calculate the total downloads for an array of download objects. 233 | 234 | **Params** 235 | 236 | * `arr` **{Array}**: Array of download objects (must have a `.downloads` property) 237 | * `returns` **{Number}**: Total of all downloads in the array. 238 | 239 | ### [.group.avg](lib/calculate.js#L93) 240 | 241 | Calculate the average for each group (key) in the object. 242 | 243 | **Params** 244 | 245 | * `groups` **{Object}**: Object created by a `group` function. 246 | * `returns` **{Object}**: Object with calculated average 247 | 248 | ### [.avg](lib/calculate.js#L112) 249 | 250 | Calculate the average downloads for an array of download objects. 251 | 252 | **Params** 253 | 254 | * `arr` **{Array}**: Array of download objects (must have a `.downloads` property) 255 | * `returns` **{Number}**: Average of all downloads in the array. 256 | 257 | ### [.group.before](lib/calculate.js#L136) 258 | 259 | Create an array of downloads before specified day. 260 | 261 | **Params** 262 | 263 | * `day` **{String}**: Day specifying last day to use in group. 264 | * `arr` **{Array}**: Array of downloads to check. 265 | * `returns` **{Array}**: Array of downloads happened before or on specified day. 266 | 267 | ### [.before](lib/calculate.js#L158) 268 | 269 | Calculate the total downloads happening before the specified day. 270 | 271 | **Params** 272 | 273 | * `day` **{String}**: Day specifying last day to use in group. 274 | * `arr` **{Array}**: Array of downloads to check. 275 | * `returns` **{Number}**: Total downloads happening before or on specified day. 276 | 277 | ### [.group.last](lib/calculate.js#L174) 278 | 279 | Create an array of downloads for the last `X` days. 280 | 281 | **Params** 282 | 283 | * `days` **{Number}**: Number of days to go back. 284 | * `arr` **{Array}**: Array of downloads to check. 285 | * `init` **{String}**: Optional day to use as the last day to include. (Days from `init || today` - `days` to `init || today`) 286 | * `returns` **{Array}**: Array of downloads for last `X` days. 287 | 288 | ### [.last](lib/calculate.js#L203) 289 | 290 | Calculate total downloads for the last `X` days. 291 | 292 | **Params** 293 | 294 | * `days` **{Number}**: Number of days to go back. 295 | * `arr` **{Array}**: Array of downloads to check. 296 | * `init` **{String}**: Optional day to use as the last day to include. (Days from `init || today` - `days` to `init || today`) 297 | * `returns` **{Array}**: Array of downloads for last `X` days. 298 | 299 | ### [.group.prev](lib/calculate.js#L219) 300 | 301 | Create an array of downloads for the previous `X` days. 302 | 303 | **Params** 304 | 305 | * `days` **{Number}**: Number of days to go back. 306 | * `arr` **{Array}**: Array of downloads to check. 307 | * `init` **{String}**: Optional day to use as the prev day to include. (Days from `init || today` - `days` - `days` to `init || today` - `days`) 308 | * `returns` **{Array}**: Array of downloads for prev `X` days. 309 | 310 | ### [.prev](lib/calculate.js#L237) 311 | 312 | Calculate total downloads for the previous `X` days. 313 | 314 | **Params** 315 | 316 | * `days` **{Number}**: Number of days to go back. 317 | * `arr` **{Array}**: Array of downloads to check. 318 | * `init` **{String}**: Optional day to use as the prev day to include. (Days from `init || today` - `days` - `days` to `init || today` - `days`) 319 | * `returns` **{Array}**: Array of downloads for prev `X` days. 320 | 321 | ### [.monthly](lib/calculate.js#L250) 322 | 323 | Create an object of download groups by month. 324 | 325 | **Params** 326 | 327 | * `arr` **{Array}**: Array of downloads to group and total. 328 | * `returns` **{Object}**: Groups with arrays of download objects 329 | 330 | ### [.monthly](lib/calculate.js#L287) 331 | 332 | Calculate total downloads grouped by month. 333 | 334 | **Params** 335 | 336 | * `arr` **{Array}**: Array of downloads to group and total. 337 | * `returns` **{Object}**: Groups with total downloads calculated 338 | 339 | ### [.yearly](lib/calculate.js#L300) 340 | 341 | Create an object of download groups by month. 342 | 343 | **Params** 344 | 345 | * `arr` **{Array}**: Array of downloads to group and total. 346 | * `returns` **{Object}**: Groups with arrays of download objects 347 | 348 | ### [.yearly](lib/calculate.js#L313) 349 | 350 | Calculate total downloads grouped by year. 351 | 352 | **Params** 353 | 354 | * `arr` **{Array}**: Array of downloads to group and total. 355 | * `returns` **{Object}**: Groups with total downloads calculated 356 | 357 | ## About 358 | 359 | ### Contributing 360 | 361 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). 362 | 363 | ### Building docs 364 | 365 | _(This document was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme) (a [verb](https://github.com/verbose/verb) generator), please don't edit the readme directly. Any changes to the readme must be made in [.verb.md](.verb.md).)_ 366 | 367 | To generate the readme and API documentation with [verb](https://github.com/verbose/verb): 368 | 369 | ```sh 370 | $ npm install -g verb verb-generate-readme && verb 371 | ``` 372 | 373 | ### Running tests 374 | 375 | Install dev dependencies: 376 | 377 | ```sh 378 | $ npm install -d && npm test 379 | ``` 380 | 381 | ### Author 382 | 383 | **Brian Woodward** 384 | 385 | * [github/doowb](https://github.com/doowb) 386 | * [twitter/doowb](http://twitter.com/doowb) 387 | 388 | ### License 389 | 390 | Copyright © 2016, [Brian Woodward](https://github.com/doowb). 391 | Released under the [MIT license](LICENSE). 392 | 393 | *** 394 | 395 | _This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.2.0, on November 09, 2016._ 396 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var moment = require('moment'); 5 | var assert = require('assert'); 6 | var stats = require('./'); 7 | 8 | describe('download-stats', function() { 9 | describe('get', function() { 10 | this.timeout(10000); 11 | 12 | it('should get downloads', function(cb) { 13 | var downloads = []; 14 | stats.get('2005-01-01', new Date(), 'download-stats') 15 | .on('data', function(data) { 16 | downloads.push(data); 17 | }) 18 | .once('error', cb) 19 | .once('end', function() { 20 | try { 21 | assert(downloads.length > 0, 'expected download data'); 22 | cb(); 23 | } catch (err) { 24 | cb(err); 25 | } 26 | }); 27 | }); 28 | }); 29 | 30 | describe('calculate', function() { 31 | it('should calculate total downloads', function() { 32 | var downloads = [ 33 | { downloads: 1 }, 34 | { downloads: 2 }, 35 | { downloads: 3 } 36 | ]; 37 | assert.equal(stats.calc.total(downloads), 6); 38 | }); 39 | 40 | describe('group', function() { 41 | it('should return last X days in an array', function() { 42 | var today = moment.utc(); 43 | var start = today.clone().subtract(50, 'days'); 44 | var arr = []; 45 | while (start.isSameOrBefore(today)) { 46 | arr.push({day: start.format('YYYY-MM-DD'), downloads: 5}); 47 | start.add(1, 'days'); 48 | } 49 | 50 | var expected = arr.slice(-30); 51 | var actual = stats.calc.group.last(30, arr); 52 | assert.deepEqual(actual, expected); 53 | }); 54 | 55 | it('should return last X days before specified date in an array', function() { 56 | var arr = [ 57 | { day: '2016-11-02', downloads: 5}, 58 | { day: '2016-11-01', downloads: 5}, 59 | { day: '2016-10-31', downloads: 5}, 60 | { day: '2016-10-30', downloads: 5}, 61 | { day: '2016-10-29', downloads: 5}, 62 | { day: '2016-10-28', downloads: 5}, 63 | { day: '2016-10-27', downloads: 5}, 64 | { day: '2016-10-26', downloads: 5}, 65 | { day: '2016-10-25', downloads: 5}, 66 | ]; 67 | var expected = [ 68 | { day: '2016-10-30', downloads: 5}, 69 | { day: '2016-10-29', downloads: 5}, 70 | { day: '2016-10-28', downloads: 5}, 71 | { day: '2016-10-27', downloads: 5}, 72 | { day: '2016-10-26', downloads: 5}, 73 | ] 74 | assert.deepEqual(stats.calc.group.last(5, arr, '2016-10-30'), expected); 75 | }); 76 | 77 | it('should return prev X days in an array', function() { 78 | var today = moment.utc(); 79 | var start = today.clone().subtract(50, 'days'); 80 | var arr = []; 81 | while (start.isSameOrBefore(today)) { 82 | arr.push({day: start.format('YYYY-MM-DD'), downloads: 5}); 83 | start.add(1, 'days'); 84 | } 85 | 86 | var expected = arr.slice(arr.length - 10, arr.length - 5); 87 | // console.log(arr, expected); 88 | assert.deepEqual(stats.calc.group.prev(5, arr), expected); 89 | }); 90 | 91 | it('should return prev X days before specified date in an array', function() { 92 | var arr = [ 93 | { day: '2016-11-02', downloads: 5}, 94 | { day: '2016-11-01', downloads: 5}, 95 | { day: '2016-10-31', downloads: 5}, 96 | { day: '2016-10-30', downloads: 5}, 97 | { day: '2016-10-29', downloads: 5}, 98 | { day: '2016-10-28', downloads: 5}, 99 | { day: '2016-10-27', downloads: 5}, 100 | { day: '2016-10-26', downloads: 5}, 101 | { day: '2016-10-25', downloads: 5}, 102 | { day: '2016-10-24', downloads: 5}, 103 | { day: '2016-10-23', downloads: 5}, 104 | { day: '2016-10-22', downloads: 5}, 105 | { day: '2016-10-21', downloads: 5}, 106 | { day: '2016-10-20', downloads: 5}, 107 | { day: '2016-10-19', downloads: 5}, 108 | ]; 109 | var expected = [ 110 | { day: '2016-10-25', downloads: 5}, 111 | { day: '2016-10-24', downloads: 5}, 112 | { day: '2016-10-23', downloads: 5}, 113 | { day: '2016-10-22', downloads: 5}, 114 | { day: '2016-10-21', downloads: 5}, 115 | ] 116 | assert.deepEqual(stats.calc.group.prev(5, arr, '2016-10-30'), expected); 117 | }); 118 | }); 119 | }); 120 | }); 121 | --------------------------------------------------------------------------------