Skip to content

Commit eee8df7

Browse files
authored
Merge pull request OSSPhilippines#36 from jkga/feature/gfx
feat: Added chart for history per country
2 parents 8bd01db + 6f0bc9f commit eee8df7

File tree

7 files changed

+1041
-4
lines changed

7 files changed

+1041
-4
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
2-
.now
2+
.now
3+
.DS_Store

app.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const express = require('express'),
44
util = require('./bin/util'),
55
axios = require('axios'),
66
covid19 = require('./lib/cli'),
7+
covid19GFX = require('./lib/cli/gfx'),
78
pkg = require('./package.json'), // package.json info
89
apiBaseURL = "https://corona.lmao.ninja", // NovelCOVID API
910
port = process.env.port || 7070; // set port
@@ -128,6 +129,30 @@ app.get('/history/:country/:chartType(cases|deaths)?', async (req, res, next) =>
128129
return next();
129130
});
130131

132+
133+
// historical chart by country
134+
app.get('/history/charts/:country', async (req, res, next) => {
135+
const userAgent = req.headers['user-agent'],
136+
countryData = req.params.country,
137+
chartType = req.params.chartType || 'cases',
138+
summary = await axios.get(`${apiBaseURL}/countries/${countryData}`),
139+
history = await axios.get(`${apiBaseURL}/v2/historical/${summary.data.country}?lastdays=all`),
140+
s = summary.data,
141+
h = history.data;
142+
143+
if (util.isCommandline(userAgent)) {
144+
covid19GFX.historyCountryTracker(
145+
req, res,
146+
s.country, s.cases, s.todayCases,
147+
s.deaths, s.todayDeaths, s.recovered,
148+
s.active, s.critical, s.casesPerOneMillion,
149+
s.updated, h, chartType, s.countryInfo
150+
)
151+
return null;
152+
}
153+
return next();
154+
});
155+
131156
app.get('*', (req, res) => res.send(`
132157
Welcome to COVID-19 Tracker CLI v${pkg.version} by Waren Gonzaga\n
133158
Please visit: https://warengonza.ga/covid19-tracker-cli

lib/cli/gfx/index.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
const
2+
blessed = require('blessed'),
3+
pkg = require('../../../package.json'),
4+
say = require('../../sayings/threads.json'),
5+
serverUtil = require('./server-util')
6+
template = require ('./template.js'),
7+
ts = Date.now(),
8+
date_ob = new Date(ts),
9+
date = date_ob.getDate(),
10+
month = date_ob.getMonth() + 1,
11+
year = date_ob.getFullYear(),
12+
currentdate = month + "/" + date + "/" + year,
13+
space = ' ',
14+
br = '\n',
15+
header = '`COVID19-TRACKER-CLI (v'+pkg.version+')`',
16+
tagline = '*A curl-based command line tracker for Novel Coronavirus or COVID-19 pandemic.*',
17+
source = 'Source: https://www.worldometers.info/coronavirus/',
18+
repo = 'Code: https://github.com/warengonzaga/covid19-tracker-cli',
19+
bmcurl = 'warengonza.ga/coffee4dev',
20+
twitterhandle = '@warengonzaga',
21+
twitterhashtag = '#covid19trackercli',
22+
gcashNum = '+639176462753',
23+
ansiBMC = '`(Buy Me A Coffee)` '+bmcurl,
24+
ansiTwitter = twitterhandle+space+twitterhashtag,
25+
ansiGCash = '(GCash) '+gcashNum;
26+
27+
28+
// random sayings
29+
const randomSay = () => {
30+
let random = Math.floor(Math.random() * say.length);
31+
return say[random];
32+
};
33+
34+
const patchBlessed = () => {
35+
blessed.Program.prototype.listem = function() {}
36+
process.on = function() {}
37+
};
38+
39+
patchBlessed();
40+
41+
exports.historyCountryTracker = (req, res, n, c, tC, d, tD, r, a, cl, cPOM, u, h, chartType, countryInfo) => {
42+
const name = n, cases = c, todayCases = tC,
43+
deaths = d, todayDeaths = tD, recovered = r,
44+
active = a, critical = cl, casesPerOneMillion = cPOM,
45+
mortalityPercentage = (d/c)*100, recoveredPercentage = (r/c)*100,
46+
asof = new Date(u),
47+
dates = Object.keys(h.timeline[chartType]),
48+
from = dates[0],
49+
to = dates[dates.length - 1],
50+
tableFooter = randomSay(),
51+
defaultfooter = ansiBMC+ansiTwitter+br+br,
52+
specialfooter = ansiGCash+br+ansiBMC+ansiTwitter+br+br,
53+
defaultHeader = header+br+tagline,
54+
footer = (n.toLowerCase() == 'philippines') ? tableFooter+br+specialfooter+br+source+br+repo : tableFooter+br+defaultfooter+br+source+br+repo;
55+
56+
// load template with data
57+
// serverUtil.loadTemplate(template, {jsonData}, callback)
58+
serverUtil.loadTemplate(template, {
59+
name,
60+
cases,
61+
deaths,
62+
recovered,
63+
active,
64+
casesPerOneMillion,
65+
todayCases,
66+
todayDeaths,
67+
critical,
68+
mortalityPercentage,
69+
recoveredPercentage,
70+
countryInfo,
71+
from,
72+
to,
73+
currentdate,
74+
asof,
75+
h,
76+
defaultHeader,
77+
footer,
78+
}, (screen) => {
79+
res.send(screen+'\r\n'+'\033[?25h')
80+
})
81+
82+
};
83+

lib/cli/gfx/server-util.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/** Modified script from blessed-contrib server-util */
2+
const blessed = require('blessed'),
3+
contrib = require('blessed-contrib'),
4+
Stream = require('stream');
5+
6+
function OutputBuffer(options) {
7+
this.isTTY = true;
8+
this.columns = options.cols;
9+
this.rows = options.rows;
10+
this.write = function(s) {
11+
s = s.replace('\x1b8', '');
12+
options.stream.write(s)
13+
};
14+
this.on = function() {};
15+
}
16+
17+
function InputBuffer() {
18+
this.isTTY = true;
19+
this.isRaw = true;
20+
this.emit = function() {};
21+
this.setRawMode = function() {};
22+
this.resume = function() {};
23+
this.pause = function() {};
24+
this.on = function() {};
25+
}
26+
27+
createScreen = (opt = {}) => {
28+
const output = new OutputBuffer({stream: opt.stream, cols: 250, rows: 50});
29+
const input = new InputBuffer(); //required to run under forever since it replaces stdin to non-tty
30+
const program = blessed.program({output: output, input: input});
31+
32+
let screen = blessed.screen({program: program});
33+
return screen
34+
}
35+
36+
loadTemplate = (gridTemplateClass, json, callback) => {
37+
38+
blessed.Screen.global = null
39+
blessed.Program.global = null
40+
41+
const
42+
customStream = new Stream.Transform()
43+
screen = createScreen({stream: customStream}),
44+
grid = new contrib.grid({rows: 12, cols: 14, screen: screen});
45+
46+
let result = []
47+
48+
if(!gridTemplateClass) throw new Error('No template loaded')
49+
50+
// parse template
51+
gridTemplateClass.load(grid, json)
52+
53+
customStream._transform = function (chunk,encoding,done) {
54+
result.push(chunk.toString())
55+
done()
56+
}
57+
58+
customStream._final = () => {
59+
callback(result.join(''))
60+
}
61+
62+
customStream.on('error', (e) => {
63+
// do nothing here
64+
})
65+
66+
screen.render()
67+
68+
setTimeout(() => {
69+
customStream.end()
70+
}, 1000)
71+
}
72+
73+
module.exports = {
74+
createScreen,
75+
loadTemplate,
76+
}

lib/cli/gfx/template.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
const
2+
contrib = require('blessed-contrib'),
3+
chalk = require('chalk');
4+
5+
exports.load = (grid, data) => {
6+
7+
let markdown = grid.set(0, 0, 2, 2, contrib.markdown,{
8+
style: {
9+
border: {
10+
fg: 'black'
11+
}
12+
}
13+
})
14+
markdown.setMarkdown(data.defaultHeader)
15+
16+
let table = grid.set(0, 2, 2, 7, contrib.table,
17+
{ keys: true
18+
, fg: 'white'
19+
, selectedFg: 'white'
20+
, selectedBg: 'blue'
21+
, interactive: false
22+
, label: 'Historical data as of '+data.asof.toLocaleString()+' [Date:'+data.currentdate+']'
23+
, width: '30%'
24+
, height: '30%'
25+
, border: {type: "line", fg: "cyan"}
26+
, columnSpacing: 10 //in chars
27+
, columnWidth: [1,16, 12, 12, 12, 18],
28+
style: {
29+
border: {
30+
fg: 'white'
31+
}
32+
}})
33+
34+
table.setData(
35+
{ headers: ['','Cases', 'Deaths', 'Recovered', 'Active', 'Cases/Million']
36+
, data:
37+
[
38+
['',data.cases, data.deaths, data.recovered, data.active, data.casesPerOneMillion],
39+
['','Today Cases', 'Today Deaths', 'Critical', 'Mortality %', 'Recovery %'],
40+
['',data.todayCases, data.todayDeaths, data.critical, data.mortalityPercentage, data.recoveredPercentage]
41+
]
42+
})
43+
44+
let map = grid.set(2, 0, 3, 4, contrib.map,{
45+
style: {
46+
border: {
47+
fg: 'black'
48+
},
49+
shapeColor: 'yellow'
50+
},
51+
})
52+
53+
map.addMarker({"lon" : data.countryInfo.long, "lat" : data.countryInfo.lat, color: 'magenta', char: '\u24E7'+` ${data.name}`})
54+
55+
let bar = grid.set(2, 4, 3, 3, contrib.bar,{
56+
barBgColor: 'red',
57+
barWidth: 7,
58+
barSpacing: 5,
59+
xOffset: 3,
60+
maxHeight: 9,
61+
style: {
62+
border: {
63+
fg: 'white'
64+
}
65+
}})
66+
67+
screen.append(bar) //must append before setting data
68+
bar.setData({ titles: ['Cases', 'Deaths', 'Recovered', 'Active'], data: [data.cases, data.deaths, data.recovered, data.active]})
69+
70+
let donut = grid.set(2, 7, 3, 2, contrib.donut,{
71+
label: 'Mortality Rate',
72+
radius: 8,
73+
arcWidth: 3,
74+
remainColor: 'black',
75+
yPadding: 2,
76+
style: {
77+
border: {
78+
fg: 'red'
79+
}
80+
},
81+
data: [
82+
{percent: data.mortalityPercentage, label: 'Mortality', color: 'red'},
83+
{percent: data.recoveredPercentage, label: 'Recovery', color: 'green'}
84+
]
85+
});
86+
87+
let line = grid.set(5, 0, 4, 9, contrib.line,
88+
{ style:
89+
{ line: "yellow"
90+
, text: "green"
91+
, baseline: "black",
92+
border: {
93+
fg: 'black'
94+
}
95+
}
96+
, xLabelPadding: 3
97+
, xPadding: 5
98+
, showLegend: true
99+
, wholeNumbersOnly: true //true=do not show fraction in y axis
100+
, label: `Cases from ${data.from} to ${data.to}`})
101+
102+
let series1 = {
103+
title: 'Cases',
104+
x: Object.keys(data.h.timeline.cases),
105+
y: Object.values(data.h.timeline.cases),
106+
style: {
107+
line: 'blue'
108+
}
109+
}
110+
let series2 = {
111+
title: 'Deaths',
112+
x: Object.keys(data.h.timeline.deaths),
113+
y: Object.values(data.h.timeline.deaths),
114+
style: {
115+
line: 'red'
116+
}
117+
}
118+
119+
let series3 = {
120+
title: 'Recovered',
121+
x: Object.keys(data.h.timeline.recovered),
122+
y: Object.values(data.h.timeline.recovered),
123+
style: {
124+
line: 'green'
125+
}
126+
}
127+
128+
screen.append(line) //must append before setting data
129+
line.setData([series1, series2, series3])
130+
131+
let markdownFooter = grid.set(8, 0, 2, 9, contrib.markdown,{
132+
style: {
133+
border: {
134+
fg: 'black'
135+
}
136+
}
137+
})
138+
markdownFooter.setMarkdown(data.footer)
139+
140+
return grid
141+
}

0 commit comments

Comments
 (0)