Skip to content

Instantly share code, notes, and snippets.

@mattsson
Last active January 17, 2021 18:21
Show Gist options
  • Save mattsson/16d512c29e2939fdb69d5e2423e954a8 to your computer and use it in GitHub Desktop.
Save mattsson/16d512c29e2939fdb69d5e2423e954a8 to your computer and use it in GitHub Desktop.

This is a Scriptable iOS widget script that shows the current COVID-19 vaccination status including an estimated herd immunity date for your country of choice.

Based on marco79's great script.

To set it up for you country do the following:

  1. Change the population variable to the population in your chosen country.
  2. Change the herdImmunityPercentage if you want.
  3. Go to the list of countries, open one, tap the Raw button, copy the URL in the address bar and set it for the dataUrl variable in the script.

Take note that the script assumes each person must get 2 vaccination doses, before they are vaccinated, as is the case with the BioNTech and Moderna vaccines.

// Change the following constants to your country of choice
const population = 5837213;
const herdImmunityPercentage = 70;
// Go to https://github.com/owid/covid-19-data/tree/master/public/data/vaccinations/country_data and find the raw URL for your country
const dataUrl = "https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/vaccinations/country_data/Denmark.csv";
// -------------------------
// With caching and fallback
const cacheMinutes = 60; // 60 min
const today = new Date();
let result;
let width = 100;
const h = 5;
const neededTotalVaccinations = population * 2 * (herdImmunityPercentage / 100);
let widget = new ListWidget();
widget.setPadding(8, 10, 0, 10);
await getNumbers();
await createWidget();
Script.setWidget(widget);
Script.complete();
if (config.runsInApp) {
widget.presentSmall();
}
async function createWidget() {
widget.url = result.report_url;
const upperStack = widget.addStack();
upperStack.layoutHorizontally();
const upperTextStack = upperStack.addStack();
upperTextStack.layoutVertically();
let staticText1 = upperTextStack.addText("Vaccinations");
staticText1.font = Font.semiboldRoundedSystemFont(11);
let staticText2 = upperTextStack.addText("Given:");
staticText2.font = Font.semiboldRoundedSystemFont(11);
upperStack.addSpacer();
let logoImage = upperStack.addImage(await getImage("vac-logo.png"));
logoImage.imageSize = new Size(30, 30);
widget.addSpacer(4);
const amountPerCent = round(
100 / neededTotalVaccinations * result.cumsum_latest,
1
);
let amountText = widget.addText(
result.cumsum_latest.toLocaleString() + " (" + amountPerCent.toLocaleString() + "%)"
);
amountText.font = Font.boldSystemFont(13);
amountText.textColor = new Color("#00a86b");
amountText.minimumScaleFactor = 0.8
let description3 = widget.addText("(7D. Ø: " + calculateDailyVac().toLocaleString() + ")");
description3.font = Font.mediumSystemFont(9);
widget.addSpacer(4);
let progressStack = widget.addStack();
progressStack.layoutVertically();
let progressNumberStack = widget.addStack();
progressNumberStack.layoutHorizontally();
const progressText0 = progressNumberStack.addText("0%");
progressText0.font = Font.mediumSystemFont(8);
progressNumberStack.addSpacer();
const progressTextEnd = progressNumberStack.addText(herdImmunityPercentage + "%");
progressTextEnd.font = Font.mediumSystemFont(8);
progressStack.addImage(createProgress(result.cumsum_latest));
widget.addSpacer(7);
let calendarStack = widget.addStack();
const calendarImage = calendarStack.addImage(await getImage("calendar-icon.png"));
calendarImage.imageSize = new Size(26, 26);
calendarStack.addSpacer(6);
let calendarTextStack = calendarStack.addStack();
calendarTextStack.layoutVertically();
calendarTextStack.addSpacer(0);
// calculate date
var estimatedDate = new Date();
estimatedDate.setDate(new Date().getDate() + calculateRemainingDays());
let description = calendarTextStack.addText("Herd Immunity:");
description.font = Font.mediumSystemFont(10);
const description2 = calendarTextStack.addText(
estimatedDate.toLocaleDateString()
);
description2.font = Font.boldSystemFont(10);
widget.addSpacer(4)
const lastUpdateDate = new Date(result.date);
let lastUpdatedText = widget.addText(
"Updated: " + lastUpdateDate.toLocaleDateString()
);
lastUpdatedText.font = Font.mediumMonospacedSystemFont(8);
lastUpdatedText.textOpacity = 0.7;
lastUpdatedText.centerAlignText()
}
// get images from iCloud or download them once
async function getImage(image) {
let fm = FileManager.local();
let dir = fm.documentsDirectory();
let path = fm.joinPath(dir, image);
if (fm.fileExists(path)) {
return fm.readImage(path);
} else {
// download once
let imageUrl;
switch (image) {
case "vac-logo.png":
imageUrl = "https://i.imgur.com/ZsBNT8E.png";
break;
case "calendar-icon.png":
imageUrl = "https://i.imgur.com/Qp8CEFf.png";
break;
default:
console.log(`Sorry, couldn't find ${image}.`);
}
let req = new Request(imageUrl);
let loadedImage = await req.loadImage();
fm.writeImage(path, loadedImage);
return loadedImage;
}
}
async function getNumbers() {
// Set up the file manager.
const files = FileManager.local();
// Set up cache
const cachePath = files.joinPath(
files.cacheDirectory(),
"api-cache-covid-vaccine-numbers-mopo"
);
const cacheExists = files.fileExists(cachePath);
const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0;
// Get Data
try {
// If cache exists and it's been less than 60 minutes since last request, use cached data.
if (
cacheExists &&
today.getTime() - cacheDate.getTime() < cacheMinutes * 60 * 1000
) {
console.log("Get from Cache");
result = JSON.parse(files.readString(cachePath));
} else {
console.log("Get from API");
const req2 = new Request(
dataUrl
);
const csvString = await req2.loadString();
const csvArray = CSVToArray(csvString, ",");
// Find the last line that has content
var latestLineElements = new Array();
while (latestLineElements.length <= 1) {
latestLineElements = csvArray.pop()
};
// Find older entry
const olderLineElements = csvArray.length >= 7 ? csvArray[csvArray.length - 7] : csvArray[0];
result = {
date: latestLineElements[1],
cumsum_latest: parseInt(latestLineElements[4]),
cumsum_7_days_ago: parseInt(olderLineElements[4]),
report_url: latestLineElements[3]
};
console.log("Write Data to Cache");
try {
files.writeString(cachePath, JSON.stringify(result));
} catch (e) {
console.log("Creating Cache failed!");
console.log(e);
}
}
} catch (e) {
console.error(e);
if (cacheExists) {
console.log("Get from Cache");
result = JSON.parse(files.readString(cachePath));
} else {
console.log("No fallback to cache possible. Due to missing cache.");
}
}
}
function createProgress(currentVacNo) {
const context = new DrawContext();
context.size = new Size(width, h);
context.opaque = false;
context.respectScreenScale = true;
context.setFillColor(new Color("#d2d2d7"));
const path = new Path();
path.addRoundedRect(new Rect(0, 0, width, h), 3, 2);
context.addPath(path);
context.fillPath();
context.setFillColor(new Color("#00a86b"));
const path1 = new Path();
const path1width =
(width * currentVacNo) / neededTotalVaccinations > width
? width
: (width * currentVacNo) / neededTotalVaccinations;
path1.addRoundedRect(new Rect(0, 0, path1width, h), 3, 2);
context.addPath(path1);
context.fillPath();
return context.getImage();
}
function calculateDailyVac() {
const latestVacAmount = result.cumsum_latest;
const vacAmount7DaysAgo = result.cumsum_7_days_ago;
const dailyVacAmount = Math.round((latestVacAmount - vacAmount7DaysAgo) / 7);
return dailyVacAmount;
}
function calculateRemainingDays() {
const daysRemaining = Math.round(
(neededTotalVaccinations - result.cumsum_latest) / calculateDailyVac()
);
return daysRemaining;
}
function round(value, decimals) {
return Number(Math.round(value + "e" + decimals) + "e-" + decimals);
}
function CSVToArray(strData, strDelimiter) {
// Check to see if the delimiter is defined. If not,
// then default to comma.
strDelimiter = (strDelimiter || ",");
// Create a regular expression to parse the CSV values.
var objPattern = new RegExp(
(
// Delimiters.
"(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
// Quoted fields.
"(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
// Standard fields.
"([^\"\\" + strDelimiter + "\\r\\n]*))"
),
"gi"
);
// Create an array to hold our data. Give the array
// a default empty first row.
var arrData = [[]];
// Create an array to hold our individual pattern
// matching groups.
var arrMatches = null;
// Keep looping over the regular expression matches
// until we can no longer find a match.
while (arrMatches = objPattern.exec(strData)) {
// Get the delimiter that was found.
var strMatchedDelimiter = arrMatches[1];
// Check to see if the given delimiter has a length
// (is not the start of string) and if it matches
// field delimiter. If id does not, then we know
// that this delimiter is a row delimiter.
if (
strMatchedDelimiter.length &&
strMatchedDelimiter !== strDelimiter
) {
// Since we have reached a new row of data,
// add an empty row to our data array.
arrData.push([]);
}
var strMatchedValue;
// Now that we have our delimiter out of the way,
// let's check to see which kind of value we
// captured (quoted or unquoted).
if (arrMatches[2]) {
// We found a quoted value. When we capture
// this value, unescape any double quotes.
strMatchedValue = arrMatches[2].replace(
new RegExp("\"\"", "g"),
"\""
);
} else {
// We found a non-quoted value.
strMatchedValue = arrMatches[3];
}
// Now that we have our value string, let's add
// it to the data array.
arrData[arrData.length - 1].push(strMatchedValue);
}
// Return the parsed data.
return (arrData);
}
@Mediatros
Copy link

Got this error :
Error on line 56:25: TypeError: null is not an object (evaluating 'result.cumsum_latest.toLocaleString')

The data got malformed and there’s a 60- minute cache in the script that held onto the malformed data. Could you try again now?

All good👌🏼
Thank you very much for sharing your great script 🙏🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment