Created
February 1, 2021 22:42
-
-
Save kaminski-tomasz/b5a97766161a11fb822743c889e985b0 to your computer and use it in GitHub Desktop.
Node.js script that downloads COVID cases for all countries (confirmed/deaths/recovered) and transforms it to tabular form.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const superagent = require("superagent"); | |
const ExcelJS = require('exceljs'); | |
const CORONA_API = 'https://coronavirus-tracker-api.herokuapp.com'; | |
/** | |
* @typedef {{ | |
* id: number, | |
* name: string, | |
* }} Country | |
* @typedef {{ | |
* id: number, | |
* timelines: { | |
* confirmed: Object.<string, number>, | |
* deaths: Object.<string, number>, | |
* recovered: Object.<string, number>, | |
* } | |
* }} CountryTimeline | |
*/ | |
/** | |
* @typedef {{ | |
* id: number, | |
* name: string, | |
* }} Country | |
* @typedef {{ | |
* id: number, | |
* timelines: { | |
* confirmed: Object.<string, number>, | |
* deaths: Object.<string, number>, | |
* recovered: Object.<string, number>, | |
* } | |
* }} CountryTimeline | |
*/ | |
/** | |
* @param {Country[]} countries | |
* @param {CountryTimeline[]} countryTimelines | |
* @returns {{ | |
* confirmed: {columns: any[], rows: any[][]}, | |
* deaths: {columns: any[], rows: any[][]}, | |
* recovered: {columns: any[], rows: any[][]}, | |
* }} excel rows | |
*/ | |
function transformTimelines(countries, countryTimelines) { | |
const sheetColumnDates = { | |
confirmed: new Set(), | |
deaths: new Set(), | |
recovered: new Set(), | |
}; | |
const rows = { | |
confirmed: [], | |
deaths: [], | |
recovered: [], | |
} | |
// columns | |
countryTimelines.forEach(countryTimeline => { | |
Object.keys(countryTimeline.timelines).forEach(timelineName => { | |
Object.keys(countryTimeline.timelines[timelineName]) | |
.forEach(date => sheetColumnDates[timelineName].add(date)); | |
}); | |
}); | |
// rows | |
countryTimelines.forEach(countryTimeline => { | |
const countryName = countries.find(country => country.id === countryTimeline.id).name; | |
Object.keys(countryTimeline.timelines).forEach(timelineName => { | |
const timeline = countryTimeline.timelines[timelineName]; | |
if (Object.keys(timeline).length === 0 ) { | |
return; | |
} | |
let dailyCases = []; | |
const countryTimelineDates = new Set(Object.keys(timeline)); | |
const allTimelineDates = [...sheetColumnDates[timelineName]]; | |
allTimelineDates.forEach(timelineDate => { | |
dailyCases.push(countryTimelineDates.has(timelineDate)? timeline[timelineDate]: null); | |
}); | |
rows[timelineName].push([countryName, ...dailyCases]); | |
}); | |
}); | |
/** | |
* @param {Set.<string>} dates | |
*/ | |
function transformDatesToColumns(dates) { | |
const datesSorted = [...dates].map(d => new Date(d)); | |
datesSorted.sort((d1,d2) => d1.getTime() - d2.getTime()); | |
return datesSorted.map(date => ( | |
{header: date, width: 15} | |
)); | |
} | |
return { | |
confirmed: { | |
columns: [ | |
{header: "Country", width: 30}, | |
...transformDatesToColumns(sheetColumnDates.confirmed), | |
], | |
rows: [ | |
...rows.confirmed, | |
] | |
}, | |
deaths: { | |
columns: [ | |
{header: "Country", width: 30}, | |
...transformDatesToColumns(sheetColumnDates.deaths), | |
], | |
rows: [ | |
...rows.deaths | |
] | |
}, | |
recovered: { | |
columns: [ | |
{header: "Country", width: 30}, | |
...transformDatesToColumns(sheetColumnDates.recovered), | |
], | |
rows: [ | |
...rows.recovered | |
] | |
}, | |
}; | |
} | |
/** | |
* @returns {Promise<Country[]>} | |
*/ | |
async function fetchCountries() { | |
return superagent.get(`${CORONA_API}/v2/locations`) | |
.then(res => res.body.locations.map(location => ({ | |
id: location.id, | |
name: location.country + (location.province ? ` (${location.province})` : '') | |
}))); | |
} | |
/** | |
* @param {number} countryId | |
* @returns {Promise<CountryTimeline[]>} | |
*/ | |
async function fetchCountryTimelines(countryId) { | |
return superagent.get(`${CORONA_API}/v2/locations/${countryId}`) | |
.then(res => ({ | |
id: res.body.location.id, | |
timelines: { | |
confirmed: res.body.location.timelines.confirmed.timeline, | |
deaths: res.body.location.timelines.deaths.timeline, | |
recovered: res.body.location.timelines.recovered.timeline, | |
} | |
})); | |
} | |
/** | |
* @returns {Promise<{countries: Country[], countryTimelines: CountryTimeline[]}>} | |
*/ | |
async function fetchTimelineData() { | |
try { | |
const countries = await fetchCountries(); | |
console.log('countries fetched'); | |
const countryIds = countries.map(country => country.id); | |
const countryTimelines = await (Promise.all( | |
countryIds.map(countryId => fetchCountryTimelines(countryId))) | |
); | |
console.log('country timelines fetched'); | |
return { | |
countries: countries, | |
countryTimelines: countryTimelines | |
}; | |
} catch (e) { | |
throw new Error(e); | |
} | |
} | |
fetchTimelineData().then(timelineData => { | |
const workbook = new ExcelJS.Workbook(); | |
const sheets = { | |
confirmed: "Confirmed cases", | |
deaths: "Death cases", | |
recovered: "Recovery cases", | |
}; | |
const excelData = transformTimelines(timelineData.countries, timelineData.countryTimelines); | |
const options = {views: [{state: 'frozen', xSplit: 1, ySplit: 1}]}; | |
Object.keys(sheets).forEach(sheetName => { | |
const worksheet = workbook.addWorksheet(sheets[sheetName], options); | |
worksheet.columns = excelData[sheetName].columns; | |
worksheet.addRows(excelData[sheetName].rows) | |
}); | |
workbook.xlsx.writeFile("covid-cases.xlsx"); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment