diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5cf946d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "all" +} diff --git a/app.js b/app.js index 93a0f90..cb0b11d 100644 --- a/app.js +++ b/app.js @@ -1,110 +1,248 @@ const express = require('express'); +const helmet = require('helmet'); const morgan = require('morgan'); - -const { getCountryTable, getJSONData, getJSONDataForCountry } = require('./lib/byCountry'); -const { getCompleteTable } = require('./lib/corona'); -const { lookupCountry } = require('./lib/helpers'); +const chalk = require('chalk'); + +const { + getCountryTable, + getJSONData, + getJSONDataForCountry, +} = require('./lib/byCountry'); +const { getCompleteTable, getGraph } = require('./lib/corona'); +const { lookupCountry, htmlTemplate, footer } = require('./lib/helpers'); const { getLiveUpdates } = require('./lib/reddit.js'); +const { getWorldoMetersTable } = require('./lib/worldoMeters.js'); +const { getUsaStats } = require('./lib/country/us.js'); +const { + helpContent, + countryNotFound, + stateCountryNotFound, +} = require('./lib/constants'); const app = express(); const port = process.env.PORT || 3001; const IS_CURL_RE = /\bcurl\b/im; -function errorHandler(error, res) { +function errorHandler(error, req, res) { console.error(error); - return res.send(` + const body = ` I am sorry. Something went wrong. Please report it\n ${error.message} - `); + ${footer(new Date())} + `; + if (req.isCurl) { + return body; + } + return res.status(500).send(htmlTemplate(body)); } app.set('json escape', true); -app.use(morgan(':remote-addr :remote-user :method :url :status :res[content-length] - :response-time ms')); +app.use( + helmet({ + dnsPrefetchControl: false, + frameguard: { + action: 'deny', + }, + }), +); + +app.use( + helmet.hsts({ + force: true, + includeSubDomains: true, + maxAge: 63072000, // 2 years + preload: true, + }), +); + +app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' })); + +app.use( + morgan( + ':remote-addr :remote-user :method :url :status :res[content-length] - :response-time ms', + ), +); +app.use('/favicon.ico', express.static('./favicon.ico')); + app.use((req, res, next) => { - res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Cache-Control', 's-maxage=600'); + req.isCurl = IS_CURL_RE.test(req.headers['user-agent']); next(); }); app.get('/', (req, res) => { - const isCurl = IS_CURL_RE.test(req.headers['user-agent']); + const isCurl = req.isCurl; const format = req.query.format ? req.query.format : ''; const minimal = req.query.minimal === 'true'; const emojis = req.query.emojis === 'true'; const top = req.query.top ? Number(req.query.top) : 1000; + const source = req.query.source ? Number(req.query.source) : 2; - if (format.toLowerCase() === 'json') { - return getJSONData().then(result => { - return res.json(result); - }).catch(error => errorHandler(error, res)); + if (source === 1) { + if (format.toLowerCase() === 'json') { + return getJSONData() + .then((result) => { + return res.json(result); + }) + .catch((error) => errorHandler(error, req, res)); + } + + return getCompleteTable({ isCurl, emojis, minimal, top }) + .then((result) => { + return res.send(result); + }) + .catch((error) => errorHandler(error, req, res)); } - return getCompleteTable({ isCurl, emojis, minimal, top }) - .then(result => { + return getWorldoMetersTable({ isCurl, emojis, minimal, top, format }) + .then((result) => { return res.send(result); - }).catch(error => errorHandler(error, res)); + }) + .catch((error) => errorHandler(error, req, res)); }); app.get('/updates', (req, res) => { - const isCurl = IS_CURL_RE.test(req.headers['user-agent']); + const isCurl = req.isCurl; const format = req.query.format ? req.query.format : ''; if (format.toLowerCase() === 'json') { - return getLiveUpdates({ json: true, isCurl }).then(result => { - return res.json(result); - }).catch(error => errorHandler(error, res)); + return getLiveUpdates({ json: true, isCurl }) + .then((result) => { + return res.json(result); + }) + .catch((error) => errorHandler(error, req, res)); } - return getLiveUpdates({ json: false, isCurl }).then(result => { - return res.send(result); - }).catch(error => errorHandler(error, res)); + return getLiveUpdates({ json: false, isCurl }) + .then((result) => { + return res.send(result); + }) + .catch((error) => errorHandler(error, req, res)); +}); + +app.get(['/:country/graph', '/graph'], (req, res) => { + const { country } = req.params; + const isCurl = req.isCurl; + if (!country) { + return getGraph({ isCurl }) + .then((result) => res.send(result)) + .catch((error) => errorHandler(error, req, res)); + } + const lookupObj = lookupCountry(country); + + if (!lookupObj) { + return res.status(404).send(countryNotFound(isCurl)); + } + return getGraph({ countryCode: lookupObj.iso2, isCurl }) + .then((result) => res.send(result)) + .catch((error) => errorHandler(error, req, res)); +}); + +app.get('/help', (req, res) => { + const isCurl = req.isCurl; + if (!isCurl) { + return res.send(htmlTemplate(helpContent)); + } + return res.send(chalk.green(helpContent)); +}); + +app.get('/states/:country', (req, res) => { + const { country } = req.params; + const isCurl = req.isCurl; + const format = req.query.format ? req.query.format : ''; + const minimal = req.query.minimal === 'true'; + const top = req.query.top ? Number(req.query.top) : 1000; + + const lookupObj = lookupCountry(country); + + if (!lookupObj) { + return res.status(404).send(stateCountryNotFound(isCurl)); + } + if (lookupObj.iso2 === 'US') { + return getUsaStats({ isCurl, minimal, top, format }) + .then((result) => { + return res.send(result); + }) + .catch((error) => errorHandler(error, req, res)); + } }); app.get('/:country', (req, res) => { const { country } = req.params; - const isCurl = IS_CURL_RE.test(req.headers['user-agent']); + const isCurl = req.isCurl; const format = req.query.format ? req.query.format : ''; const minimal = req.query.minimal === 'true'; const emojis = req.query.emojis === 'true'; + const source = req.query.source ? Number(req.query.source) : 2; - if (!country || country.toUpperCase() === 'ALL') { + if (!country || country.toUpperCase() === 'ALL' || country.includes(',')) { if (format.toLowerCase() === 'json') { - return getJSONData().then(result => { - return res.json(result); - }).catch(error => errorHandler(error, res)); + return getWorldoMetersTable({ + countryCode: country, + isCurl, + emojis, + minimal, + format, + }) + .then((result) => { + return res.json(result); + }) + .catch((error) => errorHandler(error, req, res)); } - return getCompleteTable({ isCurl, emojis, minimal }) - .then(result => { + return getWorldoMetersTable({ + countryCode: country, + isCurl, + emojis, + minimal, + }) + .then((result) => { return res.send(result); - }).catch(error => errorHandler(error, res)); + }) + .catch((error) => errorHandler(error, req, res)); + } + if (source === 1) { + const lookupObj = lookupCountry(country); + + if (!lookupObj) { + return res.status(404).send(countryNotFound(isCurl)); + } + const { iso2 } = lookupObj; + + if (format.toLowerCase() === 'json') { + return getJSONDataForCountry(iso2) + .then((result) => { + return res.json(result); + }) + .catch((error) => errorHandler(error, req, res)); + } + return getCountryTable({ countryCode: iso2, isCurl, emojis, minimal }) + .then((result) => { + return res.send(result); + }) + .catch((error) => errorHandler(error, req, res)); } const lookupObj = lookupCountry(country); if (!lookupObj) { - return res.send(` - Country not found. - Try the full country name or country code. - Example: - - /UK: for United Kingdom - - /US: for United States of America. - - /Italy: for Italy. - `); + return res.status(404).send(countryNotFound(isCurl)); } const { iso2 } = lookupObj; - if (format.toLowerCase() === 'json') { - return getJSONDataForCountry(iso2).then(result => { - return res.json(result); - }).catch(error => errorHandler(error, res)); - } - - return getCountryTable({ countryCode: iso2, isCurl, emojis, minimal }) - .then(result => { + return getWorldoMetersTable({ + countryCode: iso2, + isCurl, + emojis, + minimal, + format, + }) + .then((result) => { return res.send(result); - }).catch(error => errorHandler(error, res)); + }) + .catch((error) => errorHandler(error, req, res)); }); app.listen(port, () => console.log(`Running on ${port}`)); diff --git a/bin/index.js b/bin/index.js index f6c7d8a..624d7f1 100755 --- a/bin/index.js +++ b/bin/index.js @@ -3,9 +3,11 @@ require('yargonaut').style('green'); const yargs = require('yargs'); const chalk = require('chalk'); -const { getCompleteTable } = require('../lib/corona'); +const { getCompleteTable, getGraph } = require('../lib/corona'); const { getCountryTable } = require('../lib/byCountry'); +const { getWorldoMetersTable } = require('../lib/worldoMeters'); const { lookupCountry } = require('../lib/helpers'); +const { getUsaStats } = require('../lib/country/us'); const { argv } = yargs .command('$0 [country]', 'Tool to track COVID-19 statistics from terminal', yargs => @@ -34,6 +36,12 @@ const { argv } = yargs }) ) .options({ + s: { + alias: 'source', + describe: 'fetch data from other source', + default: 2, + type: 'int' + }, e: { alias: 'emojis', describe: 'Show emojis in table', @@ -55,16 +63,55 @@ const { argv } = yargs alias: 'top', describe: 'Filter table by rank', type: 'int' + }, + g: { + alias: 'graph', + describe: 'Get graph', + type: 'boolean', + default: false, + }, + st: { + alias: 'states', + describe: 'Get state level data of country ', + type: 'string', } }) .strict() .help('help'); -const { emojis, country, minimal, top } = argv; -( - country === 'ALL' - ? getCompleteTable({ emojis, minimal, top }) - : getCountryTable({ countryCode: country, emojis, minimal }) -) - .then(console.log) - .catch(console.error); +argv.countryCode = argv.country; +if (argv.states) { + const country = lookupCountry(argv.states); + if (!country) { + let error = `Country '${argv.states}' not found.\n`; + error += 'Try full country name or country code.\n'; + error += 'Ex:\n'; + error += '- UK: for United Kingdom \n'; + error += '- US: for United States of America.\n'; + error += '- Italy: for Italy.\n'; + throw new Error(chalk.red.bold(error)); + } + argv.countryCode = country.iso2; + if (argv.countryCode === 'US') { + getUsaStats(argv).then(result => { + console.log(result); + process.exit(1); + }).catch(error => { + console.error(error); + process.exit(0); + }); + } +} + +if (argv.source === 1) { + ( + argv.country === 'ALL' + ? getCompleteTable(argv) + : getCountryTable(argv) + ).then(console.log).catch(console.error); +} +else if (argv.graph === true) { + getGraph(argv).then(console.log).catch(console.error); +} else { + getWorldoMetersTable(argv).then(console.log).catch(console.error); +} diff --git a/changelog.md b/changelog.md index 04e3ae6..2aa9b5f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,21 +1,38 @@ # Changelog +## Version 0.9.1 + +* Added US states api `corona --states=US` + +## Version 0.9.0 + +* Changed default source to worldoMeters. i.e source 2 is now default + +## Version 0.8.0 + +* Added confirmed cases graphs `corona -g` or `corona italy -g` + +## Version 0.7.0 + +* Added new source to fetch realtime data. `corona --source=2` +* Code refactored and some bug fixes. + ## Version 0.6.0 -* Added filter to show top N countries. ``corona --top=20`` +* Added filter to show top N countries. `corona --top=20` ## Version 0.5.0 -* Added minimal / comapct table command. ``corona --minimal`` +* Added minimal / comapct table command. `corona --minimal` * Added world total stats at the bottom of the table too. * Refactor: moved table formatting functions to helpers. * Added total stats object when using `?format=json` ## Version 0.4.0 -* Added country filter. Ex: ``corona Italy`` -* Added command to show emojis. Ex: ``corona --emojis`` -* Added command to disable colors using. Ex: ``corona --color=false`` +* Added country filter. Ex: `corona Italy` +* Added command to show emojis. Ex: `corona --emojis` +* Added command to disable colors using. Ex: `corona --color=false` ## Version 0.2.0 @@ -23,4 +40,4 @@ ## Version 0.1.0 -* Lauched command `corona` \ No newline at end of file +* Lauched command `corona` diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..804a1ac Binary files /dev/null and b/favicon.ico differ diff --git a/lib/api.js b/lib/api.js index c814652..d2fb154 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,23 +1,128 @@ +/** + * Data Source File + * 1. Fetch data from source. + * 2. Process data. + * 3. Add to cache and return + */ + const NodeCache = require('node-cache'); const axios = require('axios'); const myCache = new NodeCache({ stdTTL: 100, checkperiod: 600 }); -const CORONA_ALL_KEY = 'coronaAll'; +/** + * John Hopkins Univ. data source API. + * The data needs to be flattened. + * @todo Move the data processing from `view` files to this `data` file. + * @returns {Object} JHUDataObject + */ exports.getCoronaData = async () => { - const coronaCache = myCache.get(CORONA_ALL_KEY); + const CORONA_ALL_KEY = 'coronaAll'; + const cache = myCache.get(CORONA_ALL_KEY); - if (coronaCache) { - return coronaCache; + if (cache) { + console.log('cache', CORONA_ALL_KEY); + return cache; } - - const result = await axios('https://coronavirus-tracker-api.herokuapp.com/all'); + const result = await axios( + 'https://coronavirus-tracker-api.herokuapp.com/all', + ); if (!result || !result.data) { throw new Error('Source API failure.'); } - myCache.set(CORONA_ALL_KEY, result.data, 60 * 15); - return result.data; }; + +/** + * Fetch Worldometers Data. + * As JHU data updates once a day, this was added. + * This API scrapes data from `https://www.worldometers.info/coronavirus/` + * and updates very frequenly. + * */ +exports.getWorldoMetersData = async (countryCode = 'ALL') => { + const key = `worldMetersData_${countryCode}`; + const cache = myCache.get(key); + + if (cache) { + console.log('cache', key); + return cache; + } + const result = await axios( + 'https://disease.sh/v2/countries?sort=cases', + ); + if (!result || !result.data) { + throw new Error('WorldoMeters Source API failure'); + } + + const worldStats = result.data.reduce( + (acc, countryObj) => { + acc.cases += countryObj.cases; + acc.todayCases += countryObj.todayCases; + acc.deaths += countryObj.deaths; + acc.todayDeaths += countryObj.todayDeaths; + acc.recovered += countryObj.recovered; + acc.active += countryObj.active; + acc.critical += countryObj.critical; + return acc; + }, + { + country: 'World', + countryCode: 'World', + cases: 0, + todayCases: 0, + deaths: 0, + todayDeaths: 0, + recovered: 0, + active: 0, + critical: 0, + }, + ); + + result.data.forEach((obj) => { + obj.confirmed = obj.cases; + obj.countryCode = obj.countryInfo.iso2 || ''; + }); + worldStats.casesPerOneMillion = (worldStats.cases / 7794).toFixed(2); + worldStats.confirmed = worldStats.cases; + let finalData = result.data; + if (countryCode && countryCode !== 'ALL') { + // extra filter to cater for trailing comma, ie /gb, + finalData = finalData.filter((obj) => countryCode.toLowerCase().split(',').filter((obj) => obj.length > 1).includes(obj.countryCode.toLowerCase())); + } + const returnObj = { data: finalData, worldStats }; + + myCache.set(key, returnObj, 60 * 15); + return returnObj; +}; + +exports.usaStats = async () => { + const key = 'usaStats'; + const cache = myCache.get(key); + + if (cache) { + console.log('cache', key); + return cache; + } + const result = await axios('https://disease.sh/v2/states'); + if (!result || !result.data) { + throw new Error('usa stats API failure'); + } + return result; +}; + +// exports.bingApi = async (countryCode = 'ALL') => { +// const key = 'bingData'; +// const cache = myCache.get(key); + +// if (cache) { +// console.log('cache', key); +// return cache; +// } +// const result = await axios('https://bing.com/covid/data'); +// if (!result || !result.data) { +// throw new Error('bing api faliure'); +// } +// return result; +// }; diff --git a/lib/byCountry.js b/lib/byCountry.js index 7f50d05..8e7e87e 100644 --- a/lib/byCountry.js +++ b/lib/byCountry.js @@ -2,14 +2,12 @@ const Table = require('cli-table3'); const _ = require('lodash'); const helpers = require('./helpers'); const api = require('./api'); + const { extraStats, getConfirmed, - getActive, getDeaths, - getRecovered, getMortalityPer, - getRecoveredPer, getEmoji, getOneDayChange, getOneWeekChange, @@ -18,11 +16,10 @@ const { htmlTemplate, } = require('./helpers'); -function getDataByState(confirmed, deaths, recovered) { +function getDataByState(confirmed, deaths) { const countryMap = {}; const lastUpdated = confirmed.last_updated; const confirmedMap = _.keyBy(confirmed.locations, (i) => i.country + i.province); - const recoveredMap = _.keyBy(recovered.locations, (i) => i.country + i.province); const deathsMap = _.keyBy(deaths.locations, (i) => i.country + i.province); confirmed.locations.forEach(obj => { const countryName = obj.country; @@ -34,10 +31,8 @@ function getDataByState(confirmed, deaths, recovered) { province: provinceName, countryCode: obj.country_code, confirmed: confirmedMap[mapKey].latest, - recovered: recoveredMap[mapKey].latest, deaths: deathsMap[mapKey].latest, confirmedByDay: helpers.historyObjToArr(confirmedMap[mapKey].history), - recoveredByDay: helpers.historyObjToArr(recoveredMap[mapKey].history), deathsByDay: helpers.historyObjToArr(deathsMap[mapKey].history), lastUpdated, }; @@ -51,8 +46,8 @@ function getDataByState(confirmed, deaths, recovered) { exports.getJSONData = async () => { const data = await api.getCoronaData(); - const { confirmed, deaths, recovered } = data; - const countryData = getDataByState(confirmed, deaths, recovered); + const { confirmed, deaths } = data; + const countryData = getDataByState(confirmed, deaths); const totalStats = getTotalStats(countryData); totalStats.country = 'World'; return countryData.concat(totalStats); @@ -60,8 +55,8 @@ exports.getJSONData = async () => { exports.getJSONDataForCountry = async (countryCode) => { const data = await api.getCoronaData(); - const { confirmed, deaths, recovered } = data; - const countryData = getDataByState(confirmed, deaths, recovered) + const { confirmed, deaths } = data; + const countryData = getDataByState(confirmed, deaths) .filter(obj => obj.countryCode === countryCode); return countryData; }; @@ -78,8 +73,8 @@ exports.getCountryTable = async ({ style: helpers.getTableStyles(minimal), }); const data = await api.getCoronaData(); - const { confirmed, deaths, recovered } = data; - const countryData = getDataByState(confirmed, deaths, recovered) + const { confirmed, deaths } = data; + const countryData = getDataByState(confirmed, deaths) .filter(obj => obj.countryCode === countryCode); if (countryData.length === 0) { @@ -91,11 +86,8 @@ exports.getCountryTable = async ({ [countryData[0].country]: [ 'Total', getConfirmed(totalStats.confirmed), - getRecovered(totalStats.recovered), getDeaths(totalStats.deaths), - getActive(totalStats.active), getMortalityPer(totalStats.mortalityPer), - getRecoveredPer(totalStats.recoveredPer), getOneDayChange(totalStats), getOneWeekChange(totalStats), ] @@ -108,11 +100,8 @@ exports.getCountryTable = async ({ const values = [ cd.province, getConfirmed(cd.confirmed), - getRecovered(cd.recovered), getDeaths(cd.deaths), - getActive(cd.active), getMortalityPer(cd.mortalityPer), - getRecoveredPer(cd.recoveredPer), getOneDayChange(cd), getOneWeekChange(cd), ...(emojis ? [countryEmoji] : []) diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..028316a --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,127 @@ +const { htmlTemplate, footer } = require('./helpers'); + +exports.helpContent = ` + +/$$ /$$ /$$$$$$$$ /$$ /$$$$$$$ +| $$ | $$| $$_____/| $$ | $$____$$ +| $$ | $$| $$ | $$ | $$ $$ +| $$$$$$$$| $$$$$ | $$ | $$$$$$$/ +| $$__ $$| $$__/ | $$ | $$____/ +| $$ | $$| $$ | $$ | $$ +| $$ | $$| $$$$$$$$| $$$$$$$$| $$ +|__/ |__/|________/|________/|__/ + +--------------------------------------------------------------------------------- + +# Source 1 stats - updated once a day from John Hopkins University +https://corona-stats.online + +--------------------------------------------------------------------------------- + +(DEFAULT SOURCE) +# Source 2 stats - updated every 15 minutes from worldometers.info +https://corona-stats.online?source=2 + +--------------------------------------------------------------------------------- + +# Country wise stats + +## Format: +https://corona-stats.online/[countryCode] +https://corona-stats.online/[countryName] + +## Example: From source 1 +https://corona-stats.online/Italy?source=1 +https://corona-stats.online/UK?source=1 + +## Example: From source 2 (DEFAULT) +https://corona-stats.online/italy +https://corona-stats.online/italy?source=2 +https://corona-stats.online/UK?source=2 +https://corona-stats.online/UK + +--------------------------------------------------------------------------------- + +# State wise api (Only for US as of now) + +## Format: +https://corona-stats.online/states/[countryCode] +https://corona-stats.online/states/[countryName] + +## Example: From source 1 +https://corona-stats.online/us +https://corona-stats.online/USA?format=json +https://corona-stats.online/USA?minimal=true + +--------------------------------------------------------------------------------- + +# Minimal Mode - remove the borders and padding from table + +## Example: +https://corona-stats.online?minimal=true +https://corona-stats.online/Italy?minimal=true (with country filter) +https://corona-stats.online?minimal=true&source=1 (with source) +https://corona-stats.online/uk?source=2&minimal=true (with source and country) + +--------------------------------------------------------------------------------- + +# Get data as JSON - Add ?format=json + +## Example: +https://corona-stats.online?format=json +https://corona-stats.online/Italy?format=json (with country filter) +https://corona-stats.online/?source=2&format=json (with source) +https://corona-stats.online/uk?source=2&format=json (with source and country) + +--------------------------------------------------------------------------------- + +# Get top N countries - Add ?top=N + +## Example: +https://corona-stats.online?top=25 +https://corona-stats.online?source=1&top=10 (with source) +https://corona-stats.online/uk?minimal=true&top=20 (with minimal) + + +--------------------------------------------------------------------------------- + +# Confirmed Cases Graph (WIP) + +## Format: +https://corona-stats.online/[countryName]/graph +https://corona-stats.online/[countryCode]/graph + +## Example: +https://corona-stats.online/italy/graph +https://corona-stats.online/china/graph + + +------------- Any issues or feedback - Hit me up on twitter @ekrysis -------------- + +`; + +exports.countryNotFound = (isCurl) => { + const body = ` + Country not found. + Try the full country name or country code. + Example: + - /UK: for United Kingdom + - /US: for United States of America. + - /Italy: for Italy. + + ${footer(new Date)} + `; + return isCurl ? body : htmlTemplate(body); +}; + +exports.stateCountryNotFound = (isCurl) => { + const body = ` + State wise api is only available for: + - US + Try: + /US or /USA + + ${footer(new Date)} + `; + return isCurl ? body : htmlTemplate(body); +}; \ No newline at end of file diff --git a/lib/corona.js b/lib/corona.js index 48c3ece..be1cafa 100644 --- a/lib/corona.js +++ b/lib/corona.js @@ -1,17 +1,16 @@ const Table = require('cli-table3'); const _ = require('lodash'); const helpers = require('./helpers'); +const asciichart = require('asciichart'); const api = require('./api'); +const chalk = require('chalk'); const { extraStats, getCountry, getConfirmed, - getActive, getDeaths, - getRecovered, getMortalityPer, - getRecoveredPer, getEmoji, getOneDayChange, getOneWeekChange, @@ -20,11 +19,10 @@ const { htmlTemplate, } = require('./helpers'); -function getDataByCountry(confirmed, deaths, recovered) { +function getDataByCountry(confirmed, deaths) { const countryMap = {}; const lastUpdated = confirmed.last_updated; const confirmedMap = _.keyBy(confirmed.locations, (i) => i.country + i.province); - const recoveredMap = _.keyBy(recovered.locations, (i) => i.country + i.province); const deathsMap = _.keyBy(deaths.locations, (i) => i.country + i.province); confirmed.locations.forEach(obj => { const countryName = obj.country; @@ -35,25 +33,18 @@ function getDataByCountry(confirmed, deaths, recovered) { country: countryName, countryCode: obj.country_code, confirmed: confirmedMap[mapKey].latest, - recovered: recoveredMap[mapKey].latest, deaths: deathsMap[mapKey].latest, confirmedByDay: helpers.historyObjToArr(confirmedMap[mapKey].history), - recoveredByDay: helpers.historyObjToArr(recoveredMap[mapKey].history), deathsByDay: helpers.historyObjToArr(deathsMap[mapKey].history), lastUpdated, }; } else { countryMap[countryName].confirmed += confirmedMap[mapKey].latest; - countryMap[countryName].recovered += recoveredMap[mapKey].latest; countryMap[countryName].deaths += deathsMap[mapKey].latest; countryMap[countryName].confirmedByDay = helpers.addArr( countryMap[countryName].confirmedByDay, helpers.historyObjToArr(confirmedMap[mapKey].history) ); - countryMap[countryName].recoveredByDay = helpers.addArr( - countryMap[countryName].recoveredByDay, - helpers.historyObjToArr(recoveredMap[mapKey].history) - ); countryMap[countryName].deathsByDay = helpers.addArr( countryMap[countryName].deathsByDay, helpers.historyObjToArr(deathsMap[mapKey].history) @@ -66,6 +57,38 @@ function getDataByCountry(confirmed, deaths, recovered) { return _.sortBy(countryArr, (o) => -o.confirmed); } +exports.getGraph = async ({ countryCode = 'ALL', isCurl = true}) => { + const data = await api.getCoronaData(); + const { confirmed, deaths, } = data; + const countryData = getDataByCountry(confirmed, deaths,); + const worldStats = getTotalStats(countryData); + worldStats.countryCode = 'ALL'; + worldStats.countryName = 'World'; + countryData.push(worldStats); + + // const graphLength = ' '; + const graphLength = ' Confirmed Cases Graph '; + const padding = ' '; + const graphConfig = { + height: 30, + offset: 2, + padding, + }; + const singleCountryData = countryData.filter(obj => obj.countryCode === countryCode); + const { confirmedByDay } = singleCountryData[0]; + + const confirmedGraph = asciichart.plot(confirmedByDay, graphConfig); + const body = chalk.greenBright(confirmedGraph) + + chalk.cyanBright('\n\n' +padding + '22 Feb' + graphLength + 'Today') + '\n'; + + if (!isCurl) { + return htmlTemplate(body); + } + return body; + + +}; + exports.getCompleteTable = async ({ isCurl = true, emojis = false, @@ -78,34 +101,28 @@ exports.getCompleteTable = async ({ style: helpers.getTableStyles(minimal), }); const data = await api.getCoronaData(); - const { confirmed, deaths, recovered } = data; - const countryData = getDataByCountry(confirmed, deaths, recovered); + const { confirmed, deaths, } = data; + const countryData = getDataByCountry(confirmed, deaths); const worldStats = getTotalStats(countryData); - table.push({ - '': [ - 'World', - getConfirmed(worldStats.confirmed), - getRecovered(worldStats.recovered), - getDeaths(worldStats.deaths), - getActive(worldStats.active), - getMortalityPer(worldStats.mortalityPer), - getRecoveredPer(worldStats.recoveredPer), - getOneDayChange(worldStats), - getOneWeekChange(worldStats), - ...(emojis ? ['🌎'] : []) - ] - }); + const worldRow = [ + 'World', + getConfirmed(worldStats.confirmed), + getDeaths(worldStats.deaths), + getMortalityPer(worldStats.mortalityPer), + getOneDayChange(worldStats), + getOneWeekChange(worldStats), + ...(emojis ? ['🌎'] : []) + ]; + + table.push({ '': worldRow }); let rank = 1; countryData.some(cd => { const countryEmoji = getEmoji(cd.countryCode); const values = [ getCountry(`${cd.country} (${cd.countryCode})`), getConfirmed(cd.confirmed), - getRecovered(cd.recovered), getDeaths(cd.deaths), - getActive(cd.active), getMortalityPer(cd.mortalityPer), - getRecoveredPer(cd.recoveredPer), getOneDayChange(cd), getOneWeekChange(cd), ...(emojis ? [countryEmoji] : []) @@ -113,20 +130,7 @@ exports.getCompleteTable = async ({ table.push({ [rank++]: values }); return rank === top + 1; }); - table.push({ - '': [ - 'World', - getConfirmed(worldStats.confirmed), - getRecovered(worldStats.recovered), - getDeaths(worldStats.deaths), - getActive(worldStats.active), - getMortalityPer(worldStats.mortalityPer), - getRecoveredPer(worldStats.recoveredPer), - getOneDayChange(worldStats), - getOneWeekChange(worldStats), - ...(emojis ? ['🌎'] : []) - ] - }); + table.push({ '': worldRow }); const { lastUpdated } = countryData[0]; const ret = table.toString() + footer(lastUpdated); diff --git a/lib/country/us.js b/lib/country/us.js new file mode 100644 index 0000000..60e5b4d --- /dev/null +++ b/lib/country/us.js @@ -0,0 +1,57 @@ +const Table = require('cli-table3'); +const chalk = require('chalk'); +const helpers = require('../helpers'); +const api = require('../api'); +const { cFormatter } = helpers; + + +const getUsaStatsHeaders = (emojis, secondColumnName) => { + const head = [ + 'Rank', + secondColumnName, + `Total Cases ${emojis ? ' ✅' : ''}`, + 'New Cases ▲', + `Total Deaths${emojis ? ' 😞' : ''}`, + `New Deaths ▲${emojis ? ' 😞' : ''}`, + `Active${emojis ? ' 😷' : ''}`, + + ]; + return head; +}; + + +exports.getUsaStats = async ({ + isCurl = true, + minimal = false, + top = 1000, + format, +}) => { + const secondColumnName = 'US States'; + const table = new Table({ + head: getUsaStatsHeaders(null, secondColumnName), + chars: helpers.getTableBorders(minimal), + style: helpers.getTableStyles(minimal), + }); + const { data } = await api.usaStats(); + if (format === 'json') { + return { data }; + } + + let rank = 1; + data.some(cd => { + const values = [ + cFormatter(cd.state , chalk.cyanBright), + cFormatter(cd.cases, chalk.green, 'right', true), + cFormatter(cd.todayCases, chalk.cyanBright, 'right', true, ' ▲'), + cFormatter(cd.deaths, chalk.whiteBright, 'right', true), + cFormatter(cd.todayDeaths, chalk.redBright, 'right', true, ' ▲'), + cFormatter(cd.active, chalk.blueBright , 'right', true), + ]; + table.push({ [rank++]: values }); + return rank === top + 1; + }); + + const lastUpdated = new Date(); + const ret = table.toString() + helpers.footer(lastUpdated); + return isCurl ? ret : helpers.htmlTemplate(ret); +}; \ No newline at end of file diff --git a/lib/helpers.js b/lib/helpers.js index 50cd703..566986f 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -15,7 +15,6 @@ e.getState = (state) => { if (state) { return chalk.red(state); } - return chalk.red('ALL'); }; @@ -43,7 +42,7 @@ e.getDeaths = (deaths) => { e.getActive = (active) => { return { content: chalk.magentaBright(h(active)), - hAlign: 'right' + hAlign: 'right', }; }; @@ -61,13 +60,15 @@ e.getEmoji = (countryCode) => { if (countryCode && emojiFlags.countryCode(countryCode)) { return emojiFlags.countryCode(countryCode).emoji; } - return ''; }; -e.calActive = ({ confirmed, recovered, deaths }) => confirmed - (recovered + deaths); -e.calMortalityPer = ({ confirmed, deaths }) => ((deaths / confirmed) * 100).toFixed(2); -e.calRecoveredPer = ({ confirmed, recovered }) => ((recovered / confirmed) * 100).toFixed(2); +e.calActive = ({ confirmed, recovered, deaths }) => + confirmed - (recovered + deaths); +e.calMortalityPer = ({ confirmed, deaths }) => + ((deaths / confirmed) * 100).toFixed(2); +e.calRecoveredPer = ({ confirmed, recovered }) => + ((recovered / confirmed) * 100).toFixed(2); /** historyObj = { @@ -80,11 +81,11 @@ e.calRecoveredPer = ({ confirmed, recovered }) => ((recovered / confirmed) * 100 */ e.historyObjToArr = (historyObj) => { const sortedTimestampArr = _.sortBy( - Object.keys(historyObj).map(date => new Date(date).getTime()), - Number + Object.keys(historyObj).map((date) => new Date(date).getTime()), + Number, ); - return sortedTimestampArr.map(timestamp => { + return sortedTimestampArr.map((timestamp) => { const dateFormatted = moment(timestamp).format('M/D/YY'); return historyObj[dateFormatted]; }); @@ -94,7 +95,7 @@ e.historyObjToArr = (historyObj) => { * Given both arr1 and arr2 has same number of elements * Returns -> sum[n] = arr1[n] + arr2[n] * -*/ + */ e.addArr = (arr1, arr2) => { if (arr1.length === 0) { return arr2; @@ -128,45 +129,50 @@ e.getOneWeekChange = ({ confirmedByDay }) => { }; e.getTotalStats = (countryData) => { - const worldStats = countryData.reduce((acc, countryObj) => { - acc.confirmed += countryObj.confirmed; - acc.recovered += countryObj.recovered; - acc.deaths += countryObj.deaths; - acc.confirmedByDay = e.addArr(acc.confirmedByDay, countryObj.confirmedByDay); - acc.recoveredByDay = e.addArr(acc.recoveredByDay, countryObj.recoveredByDay); - acc.deathsByDay = e.addArr(acc.deathsByDay, countryObj.deathsByDay); - return acc; - }, { - confirmed: 0, - recovered: 0, - deaths: 0, - confirmedByDay: [], - recoveredByDay: [], - deathsByDay: [], - }); + const worldStats = countryData.reduce( + (acc, countryObj) => { + acc.confirmed += countryObj.confirmed; + acc.deaths += countryObj.deaths; + acc.confirmedByDay = e.addArr( + acc.confirmedByDay, + countryObj.confirmedByDay, + ); + acc.deathsByDay = e.addArr(acc.deathsByDay, countryObj.deathsByDay); + return acc; + }, + { + confirmed: 0, + deaths: 0, + confirmedByDay: [], + deathsByDay: [], + }, + ); - worldStats.active = e.calActive(worldStats); - worldStats.recoveredPer = e.calRecoveredPer(worldStats); worldStats.mortalityPer = e.calMortalityPer(worldStats); - return worldStats; }; -e.countryUpperCase = country => { +e.countryUpperCase = (country) => { if (country.length > 2) { - return country.toLowerCase().split(/\s+/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); + return country + .toLowerCase() + .split(/\s+/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); } return country; }; -e.lookupCountry = country => { +e.lookupCountry = (country) => { country = e.countryUpperCase(country); try { - return lookup.byIso(country) - || lookup.byFips(country) - || lookup.byCountry(country); + return ( + lookup.byIso(country) || + lookup.byFips(country) || + lookup.byCountry(country) + ); } catch (error) { return lookup.byFips(country) || lookup.byCountry(country); } @@ -174,34 +180,72 @@ e.lookupCountry = country => { e.footer = (lastUpdated) => ` -Stay safe. Stay inside. - -Code: https://github.com/sagarkarira/coronavirus-tracker-cli -Twitter: https://twitter.com/ekrysis - -Last Updated on: ${moment(lastUpdated).utc().format('DD-MMM-YYYY HH:MM')} UTC - +${chalk.greenBright('Code')}: ${chalk.blueBright( + 'https://github.com/sagarkarira/coronavirus-tracker-cli', +)} +${chalk.greenBright('Twitter')}: ${chalk.blueBright( + 'https://twitter.com/ekrysis', +)} + +${chalk.magentaBright('Last Updated on:')} ${moment(lastUpdated) + .utc() + .format('DD-MMM-YYYY HH:MM')} UTC + +${chalk.red.bold('US STATES API')}: ${chalk.blueBright( + 'https://corona-stats.online/states/us', +)} +${chalk.red.bold('HELP')}: ${chalk.blueBright( + 'https://corona-stats.online/help', +)} +${chalk.red.bold('SPONSORED BY')}: ${chalk.blueBright('Vercel')} +${chalk.greenBright( + 'Checkout fun new side project I am working on', +)}: ${chalk.bold.blueBright( + 'https://messagink.com', +)} `; -e.getTableBorders = minimal => { +e.getTableBorders = (minimal) => { if (minimal) { return { - 'top': '', 'top-mid': '', 'top-left': '', 'top-right': '', - 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '', - 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '', - 'right': '', 'right-mid': '', 'middle': ' ' + top: '', + 'top-mid': '', + 'top-left': '', + 'top-right': '', + bottom: '', + 'bottom-mid': '', + 'bottom-left': '', + 'bottom-right': '', + left: '', + 'left-mid': '', + mid: '', + 'mid-mid': '', + right: '', + 'right-mid': '', + middle: ' ', }; } return { - 'top': '═', 'top-mid': '╤', 'top-left': '╔', 'top-right': '╗', - 'bottom': '═', 'bottom-mid': '╧', 'bottom-left': '╚', 'bottom-right': '╝', - 'left': '║', 'left-mid': '╟', 'mid': '─', 'mid-mid': '┼', - 'right': '║', 'right-mid': '╢', 'middle': '│', + top: '═', + 'top-mid': '╤', + 'top-left': '╔', + 'top-right': '╗', + bottom: '═', + 'bottom-mid': '╧', + 'bottom-left': '╚', + 'bottom-right': '╝', + left: '║', + 'left-mid': '╟', + mid: '─', + 'mid-mid': '┼', + right: '║', + 'right-mid': '╢', + middle: '│', }; }; -e.getTableStyles = minimal => { +e.getTableStyles = (minimal) => { if (minimal) { return { 'padding-left': 0, 'padding-right': 0 }; } @@ -212,11 +256,8 @@ e.getTableHeaders = (emojis, secondColumnName) => { 'Rank', secondColumnName, `Confirmed ${emojis ? ' ✅' : ''}`, - `Recovered${emojis ? ' 😀' : ''}`, `Deaths${emojis ? ' 😞' : ''}`, - `Active${emojis ? ' 😷' : ''}`, - 'Mortality %', - 'Recovered %', + 'CFR %', '1 Day ▲', '1 Week ▲', ...(emojis ? ['🏳'] : []), @@ -224,13 +265,27 @@ e.getTableHeaders = (emojis, secondColumnName) => { return head; }; +e.getTableHeadersV2 = (emojis, secondColumnName) => { + const head = [ + 'Rank', + secondColumnName, + `Total Cases ${emojis ? ' ✅' : ''}`, + 'New Cases ▲', + `Total Deaths${emojis ? ' 😞' : ''}`, + `New Deaths ▲${emojis ? ' 😞' : ''}`, + `Recovered${emojis ? ' 😀' : ''}`, + `Active${emojis ? ' 😷' : ''}`, + 'Critical', + 'Cases / 1M pop', + ...(emojis ? ['🏳'] : []), + ]; + return head; +}; e.extraStats = (dataArr) => { - return dataArr.map(obj => { + return dataArr.map((obj) => { return { ...obj, - active: e.calActive(obj), mortalityPer: e.calMortalityPer(obj), - recoveredPer: e.calRecoveredPer(obj) }; }); }; @@ -243,21 +298,59 @@ e.htmlTemplate = (body) => {
${body}
+ ${body}
+