Skip to content

Commit 5453dc3

Browse files
committed
feat: Added non-interactive chart for history per country
1 parent a754e94 commit 5453dc3

File tree

7 files changed

+1019
-4
lines changed

7 files changed

+1019
-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: 26 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,31 @@ 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+
152+
return null;
153+
}
154+
return next();
155+
});
156+
131157
app.get('*', (req, res) => res.send(`
132158
Welcome to COVID-19 Tracker CLI v${pkg.version} by Waren Gonzaga\n
133159
Please visit: https://warengonza.ga/covid19-tracker-cli

lib/cli/gfx/index.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
const
2+
blessed = require('blessed'),
3+
fs = require('fs'),
4+
contrib = require('blessed-contrib'),
5+
present = require('./presenter'),
6+
pkg = require('../../../package.json'),
7+
xml2js = require('xml2js'),
8+
say = require('../../sayings/threads.json'),
9+
ts = Date.now(),
10+
date_ob = new Date(ts),
11+
date = date_ob.getDate(),
12+
month = date_ob.getMonth() + 1,
13+
year = date_ob.getFullYear(),
14+
currentdate = month + "/" + date + "/" + year,
15+
space = ' ',
16+
br = '\n',
17+
header = '`COVID19-TRACKER-CLI (v'+pkg.version+')`',
18+
tagline = '*A curl-based command line tracker for Novel Coronavirus or COVID-19 pandemic.*',
19+
source = 'Source: https://www.worldometers.info/coronavirus/',
20+
repo = 'Code: https://github.com/warengonzaga/covid19-tracker-cli',
21+
bmcurl = 'warengonza.ga/coffee4dev',
22+
twitterhandle = '@warengonzaga',
23+
twitterhashtag = '#covid19trackercli',
24+
gcashNum = '+639176462753',
25+
ansiBMC = '`(Buy Me A Coffee)` '+bmcurl,
26+
ansiTwitter = twitterhandle+space+twitterhashtag,
27+
ansiGCash = '(GCash) '+gcashNum,
28+
XMLTemplate = '/template.xml';
29+
30+
let cachedTemplate = null
31+
32+
// random sayings
33+
const randomSay = () => {
34+
let random = Math.floor(Math.random() * say.length);
35+
return say[random];
36+
};
37+
38+
const patchBlessed = () => {
39+
blessed.Program.prototype.listem = function() {}
40+
process.on = function() {}
41+
};
42+
43+
patchBlessed();
44+
45+
46+
exports.historyCountryTracker = (req, res, n, c, tC, d, tD, r, a, cl, cPOM, u, h, chartType, countryInfo) => {
47+
const name = n, cases = c, todayCases = tC,
48+
deaths = d, todayDeaths = tD, recovered = r,
49+
active = a, critical = cl, casesPerOneMillion = cPOM,
50+
mortalityPercentage = (d/c)*100, recoveredPercentage = (r/c)*100,
51+
asof = new Date(u),
52+
dates = Object.keys(h.timeline[chartType]),
53+
from = dates[0],
54+
to = dates[dates.length - 1],
55+
parser = new xml2js.Parser(),
56+
builder = new xml2js.Builder(),
57+
tableFooter = randomSay(),
58+
defaultfooter = ansiBMC+ansiTwitter+br+br,
59+
specialfooter = ansiGCash+br+ansiBMC+ansiTwitter+br+br;
60+
61+
const parseXML = (data) => {
62+
parser.parseString(data, function (err, result) {
63+
64+
const casesDataTable =
65+
'\n' +
66+
` ,${cases},${deaths},${recovered},${active},${casesPerOneMillion}\n` +
67+
' ,Today Cases,Today Deaths,Critical,Mortality %,Recovery %\n' +
68+
` ,${todayCases},${todayDeaths},${critical},${mortalityPercentage},${recoveredPercentage}\n`;
69+
70+
// header
71+
result.document.page[0].item[0].markdown[0].markdown[0] = header+br+tagline
72+
73+
// map
74+
result.document.page[0].item[1].map[0].markers[0].m[0].$ = {
75+
lat: countryInfo.lat,
76+
lon: countryInfo.long,
77+
char: '\u24E7'+` ${name}`,
78+
color: 'magenta',
79+
};
80+
81+
// Doughnut/donut
82+
result.document.page[0].item[4].donut[0].data[0].m[0].$={
83+
color: 'red',
84+
percent: parseFloat(mortalityPercentage).toFixed(2),
85+
label: 'Mortality',
86+
};
87+
88+
result.document.page[0].item[4].donut[0].data[0].m[1].$={
89+
color: 'green',
90+
percent: parseFloat(recoveredPercentage).toFixed(2),
91+
label: 'Recovery',
92+
};
93+
94+
// bar graph
95+
result.document.page[0].item[3].bar[0].$['data-data'] = `${cases},${deaths},${recovered},${active}`;
96+
97+
// line graph
98+
result.document.page[0].item[5].line[0].label = `Cases from ${from} to ${to}`
99+
result.document.page[0].item[5].line[0].data[0].m[0].$ = {
100+
title: 'Cases',
101+
'style-line': 'blue',
102+
x:Object.keys(h.timeline.cases).join(','),
103+
y:Object.values(h.timeline.cases).join(','),
104+
};
105+
106+
result.document.page[0].item[5].line[0].data[0].m[1].$ = {
107+
title: 'Deaths',
108+
'style-line': 'red',
109+
x:Object.keys(h.timeline.deaths).join(','),
110+
y:Object.values(h.timeline.deaths).join(','),
111+
};
112+
113+
result.document.page[0].item[5].line[0].data[0].m[2].$ = {
114+
title: 'Recovered',
115+
'style-line': 'green',
116+
x:Object.keys(h.timeline.recovered).join(','),
117+
y:Object.values(h.timeline.recovered).join(','),
118+
};
119+
120+
// Historical data table
121+
result.document.page[0].item[2].table[0].label = 'Historical data as of '+asof.toLocaleString()+' [Date:'+currentdate+']'
122+
result.document.page[0].item[2].table[0]['data-data'][0] = casesDataTable;
123+
124+
// footer
125+
result.document.page[0].item[6].markdown[0].markdown[0] = (n.toLowerCase() == 'philippines') ? tableFooter+br+specialfooter+br+source+br+repo : tableFooter+br+defaultfooter+br+source+br+repo;
126+
127+
present(req, res, builder.buildObject(result), function(err) {
128+
if (err) console.log(new Error().stack);
129+
if (err) return contrib.serverError(req, res, err);
130+
});
131+
132+
});
133+
}
134+
135+
const readXML = () => {
136+
fs.readFile(__dirname + XMLTemplate, (err, data) => {
137+
cachedTemplate = data
138+
parseXML(data)
139+
});
140+
}
141+
142+
// read XML template from file or cache
143+
if(!cachedTemplate) return readXML ()
144+
145+
try {
146+
return parseXML(new Buffer.from(cachedTemplate.toString()))
147+
} catch (e) {
148+
return readXML ()
149+
}
150+
151+
152+
};

lib/cli/gfx/presenter.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const parse = require('xml2js').parseString,
2+
Viewer = require('wopr/lib/document-viewer'),
3+
blessed = require('blessed'),
4+
contrib = require('blessed-contrib');
5+
6+
7+
const present = (req, res, body, cba) => {
8+
9+
blessed.Screen.global = null
10+
blessed.Program.global = null
11+
12+
if (!body || body=="") {
13+
return cba("You must upload the document to present as the POST body")
14+
}
15+
16+
parse(body, function (err, doc) {
17+
try {
18+
19+
if (err) {
20+
return cba("Document xml is not valid: " + err)
21+
}
22+
23+
if (!doc || !doc.document) return cba("document not valid or has no pages")
24+
if (!doc.document.page || doc.document.page.length==0) return cba("document must have at least one page")
25+
26+
var screen = contrib.createScreen(req, res)
27+
if (screen==null) return
28+
29+
viewer = new Viewer(doc.document, screen)
30+
var err = viewer.renderPage(0, '\u2800')
31+
if (err!==null) {
32+
clean(screen)
33+
return cba(err)
34+
}
35+
36+
//note the setTimeout is necessary even if delay is 0
37+
setTimeout(function() {
38+
//restore cursor
39+
res.end('\033[?25h')
40+
clean(screen)
41+
return cba()
42+
}, '' ? 5000:0)
43+
}
44+
45+
catch (e) {
46+
return cba(e)
47+
}
48+
})
49+
}
50+
51+
function clean(screen) {
52+
//TODO this code is very sensitive to blessed versions, need to check right version/usage
53+
//screen.program.destroy()
54+
//screen.destroy()
55+
}
56+
57+
module.exports = present

lib/cli/gfx/template.xml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
<document>
3+
<page>
4+
<item col="0" row="0" colSpan="2" rowSpan="2">
5+
<markdown style-paragraph="chalk.white" style-strong="chalk.cyan.underline" style-em="chalk.green" border-type="line" border-fg="gray">
6+
<markdown></markdown>
7+
</markdown>
8+
</item>
9+
10+
<item col="0" row="2" colSpan="4" rowSpan="3">
11+
<map style-shapeColor="yellow">
12+
<markers>
13+
<m lat="37.5000" lon="-79.0000" color="magenta" char='-x-'/>
14+
</markers>
15+
</map>
16+
</item>
17+
18+
<item col="2" row="0" colSpan="7" rowSpan="2">
19+
<table fg="white" width="30%" height="30%" border-type="line" border-fg="gray" columnSpacing="5" columnWidth="1,15,15,15,15,15" data-headers=",Cases,Deaths,Recovered, Active,Cases/Million" interactive="false" label="Historical chart as of 4/1/2020, 9:15:56 AM Date: [4/1/2020]">
20+
<data-data>
21+
,1,02:17,21,123,3453
22+
,Today Cases,Today Deaths,Critical,Mortality %,Recovery %
23+
,1,02:17,21,123,3453
24+
</data-data>
25+
</table>
26+
</item>
27+
28+
<item col="4" row="2" colSpan="3" rowSpan="3">
29+
<bar barWidth="9" barSpacing="3" xOffset="1" maxHeight="5" height="100%" border-type="line" border-fg="gray" data-titles="Cases,Deaths,Recovered,Active" data-data="2,4,2,5" />
30+
</item>
31+
32+
<item col="7" row="2" colSpan="2" rowSpan="3">
33+
<donut radius="8" archWidth='2' yPadding='4' border-type="line" border-fg="gray" label="Mortality Rate" xOffset='1' spacing='0'>
34+
<data>
35+
<m color="red" percent="40" label="mort" />
36+
<m color="yellow" percent="40" label="recov" />
37+
</data>
38+
</donut>
39+
</item>
40+
41+
<item col="0" row="5" colSpan="9" rowSpan="4">
42+
<line xPadding="0" showLegend="true" legend-width="14" border-type="line" border-fg="gray" xLabelPadding='1' showNthLabel='4' label='History Timeline'>
43+
<data>
44+
<m title="Cases" style-line="red" x="" y=""/>
45+
<m title="Deaths" style-line="red" x="" y=""/>
46+
<m title="Recovered" style-line="green" x="" y=""/>
47+
</data>
48+
</line>
49+
</item>
50+
51+
<item col="0" row="9" colSpan="9" rowSpan="2">
52+
<markdown style-paragraph="chalk.white" style-strong="chalk.cyan.underline" style-em="chalk.green" border-type="line" border-fg="gray">
53+
<markdown></markdown>
54+
</markdown>
55+
</item>
56+
</page>
57+
58+
59+
</document>

0 commit comments

Comments
 (0)