Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A Scriptable widget that shows the amount of people who have received the corona vaccination in Austria
/**************************
* Bitte ab hier kopieren *
**************************/
// Scriptable Widget um die aktuellen Impfzahlen darzustellen
//
// Berechnet auch ein Datum ab dem eine Herdenimmunität gegeben wäre
// (Der Mensch hat keine lebenslange Immunität vor Coronaviren, deshalb wird es
// keine Herdenimmunität geben)
//
// Quelle: https://gist.github.com/schl3ck/570422d5f6ce4595c05fca951da067e5
// Mit Caching und Fallback
const cacheMinutes = 60; // 60 min
const today = new Date();
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
// nur 70% werden für eine Herdenimmunität gebraucht
const herdImmunityFactor = 0.7;
let result;
/**
* @type { { nCitizens: number,
* today: {
* partiallyImmunized: number,
* fullyImmunized: number,
* },
* weekAgo: {
* partiallyImmunized: number,
* fullyImmunized: number,
* },
* daysBetweenLatest7DaysAgo: number,
* date: Date,
* } }
*/
let resultAt;
let width = 100;
const h = 5;
const colors = {
partiallyImmunizedGrayBG: new Color("#ecb100"),
partiallyImmunizedWhiteBG: new Color("#d29e00"),
fullyImmunized: new Color("#00a86b"),
};
let widget = new ListWidget();
widget.setPadding(4, 10, 0, 10);
await getNumbers();
await createWidget();
Script.setWidget(widget);
Script.complete();
if (config.runsInApp) {
widget.presentSmall();
}
async function createWidget() {
const upperStack = widget.addStack();
upperStack.layoutHorizontally();
const upperTextStack = upperStack.addStack();
upperTextStack.layoutVertically();
let staticText1 = upperTextStack.addText("Geimpfte");
staticText1.font = Font.semiboldRoundedSystemFont(11);
let staticText2 = upperTextStack.addText("Personen");
staticText2.font = Font.semiboldRoundedSystemFont(11);
upperStack.addSpacer();
let logoImage = upperStack.addImage(await getImage("vac-logo.png"));
logoImage.imageSize = new Size(30, 30);
widget.addSpacer(2);
const partiallyImmunized = {
title: "Teil",
color: colors.partiallyImmunizedWhiteBG,
today: resultAt.today.partiallyImmunized,
weekAgo: resultAt.weekAgo.partiallyImmunized,
};
const fullyImmunized = {
title: "Voll",
color: colors.fullyImmunized,
today: resultAt.today.fullyImmunized,
weekAgo: resultAt.weekAgo.fullyImmunized,
};
for (const data of [partiallyImmunized, fullyImmunized]) {
const amountPerCent = round((100 / resultAt.nCitizens) * data.today, 2);
const titleDataStack = widget.addStack();
titleDataStack.layoutHorizontally();
const dataTitle = titleDataStack.addText(data.title + ":");
dataTitle.font = Font.boldSystemFont(10);
// dataTitle.minimumScaleFactor = 0.8;
titleDataStack.addSpacer(2);
const dataStack = titleDataStack.addStack();
dataStack.layoutVertically();
let amountText = dataStack.addText(
`${data.today.toLocaleString()} (${amountPerCent.toLocaleString()}%)`,
);
amountText.font = Font.boldSystemFont(11);
amountText.textColor = data.color;
amountText.minimumScaleFactor = 0.8;
let description3 = dataStack.addText(
`(${resultAt.daysBetweenLatest7DaysAgo}T. Ø: ${calculateDailyVac(
data.today,
data.weekAgo,
).toLocaleString()} / Tag)`,
);
description3.font = Font.mediumSystemFont(9);
}
widget.addSpacer(4);
let progressStack = widget.addStack();
progressStack.layoutVertically();
progressStack.addImage(createProgress());
let progressNumberStack = widget.addStack();
progressNumberStack.layoutHorizontally();
const progressText0 = progressNumberStack.addText("0%");
progressText0.font = Font.mediumSystemFont(8);
progressNumberStack.addSpacer();
const progressText70 = progressNumberStack.addText("70%");
progressText70.font = Font.mediumSystemFont(8);
widget.addSpacer(4);
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(
resultAt.today.fullyImmunized,
resultAt.weekAgo.fullyImmunized,
),
);
let description = calendarTextStack.addText('"Herdenimmunität":');
description.font = Font.mediumSystemFont(9);
const description2 = calendarTextStack.addText(
estimatedDate.toLocaleDateString(),
);
description2.font = Font.boldSystemFont(9);
widget.addSpacer(2);
const lastUpdateDate = new Date(resultAt.date);
let lastUpdatedText = widget.addText(
"Stand: " + 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.joinPath(fm.documentsDirectory(), "vaccination-stats-images");
fm.createDirectory(dir, true);
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(),
"vaccination-stats-data.csv",
);
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 = files.readString(cachePath);
} else {
console.log("Get from API");
const req2 = new Request(
"https://info.gesundheitsministerium.gv.at/data/timeline-eimpfpass.csv",
);
result = await req2.loadString();
console.log("Write Data to Cache");
try {
files.writeString(cachePath, result);
} catch (e) {
console.log("Creating Cache failed!");
console.log(e);
}
}
} catch (e) {
console.error(e);
if (cacheExists) {
console.log("Get from Cache");
result = files.readString(cachePath);
} else {
console.log("No fallback to cache possible. Due to missing cache.");
}
}
parseCSV(result);
}
/** @param {string} csv */
function parseCSV(csv) {
const data = [];
let names = [];
for (const line of csv.split("\n")) {
const tokens = line.split(";");
if (names.length === 0) {
names = tokens;
} else {
data.push(
Object.fromEntries(tokens.map((token, i) => [names[i], token])),
);
}
}
let latest, latestWeekAgo;
for (let i = data.length - 1; i >= 0; i--) {
const item = data[i];
if (item.BundeslandID !== "10") continue;
item.Datum = new Date(item.Datum);
if (!latest && item.Datum <= today) {
latest = item;
}
if (!latestWeekAgo && item.Datum <= weekAgo) {
latestWeekAgo = item;
}
if (latest && latestWeekAgo) break;
}
resultAt = {
nCitizens: latest["Bevölkerung"],
today: {
partiallyImmunized: parseInt(latest.Teilgeimpfte),
fullyImmunized: parseInt(latest.Vollimmunisierte),
},
weekAgo: {
partiallyImmunized: parseInt(latestWeekAgo.Teilgeimpfte),
fullyImmunized: parseInt(latestWeekAgo.Vollimmunisierte),
},
daysBetweenLatest7DaysAgo: Math.round(
(latest.Datum.getTime() - latestWeekAgo.Datum.getTime()) / 86400000, // 1000 * 3600 * 24 = 86400000
),
date: latest.Datum,
};
}
function createProgress() {
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(colors.partiallyImmunizedGrayBG);
const path1 = new Path();
const path1width = Math.max(
Math.min(
(width * resultAt.today.partiallyImmunized)
/ (resultAt.nCitizens * herdImmunityFactor),
width,
),
6,
);
path1.addRoundedRect(new Rect(0, 0, path1width, h), 3, 2);
context.addPath(path1);
context.fillPath();
context.setFillColor(colors.fullyImmunized);
const path2 = new Path();
const path2width = Math.max(
Math.min(
(width * resultAt.today.fullyImmunized)
/ (resultAt.nCitizens * herdImmunityFactor),
width,
),
2,
);
path2.addRoundedRect(new Rect(0, 0, path2width, h), 3, 2);
context.addPath(path2);
context.fillPath();
return context.getImage();
}
/**
* @param {number} today Number of people vaccined today
* @param {number} weekAgo Number of people vaccined a week ago
*/
function calculateDailyVac(today, weekAgo) {
const dailyVacAmount = Math.round(
(today - weekAgo) / resultAt.daysBetweenLatest7DaysAgo,
);
return dailyVacAmount;
}
/**
* @param {number} today Number of people vaccined today
* @param {number} weekAgo Number of people vaccined a week ago
*/
function calculateRemainingDays(today, weekAgo) {
const daysRemaining = Math.round(
(resultAt.nCitizens * herdImmunityFactor - resultAt.today.fullyImmunized)
/ calculateDailyVac(today, weekAgo),
);
return daysRemaining;
}
function round(value, decimals) {
return parseFloat(
Math.round(value * parseFloat("1e" + decimals))
* parseFloat("1e-" + decimals),
);
}
/***************************
* Bitte bis hier kopieren *
***************************/
@martinellli

This comment has been minimized.

Copy link

@martinellli martinellli commented Apr 16, 2021

Sehr gutes Script, ist täglich im Einsatz...😉
Schön wäre es, wenn die doch sehr großen Zahlen Tausenderpunkte hätten. Es würde die Lesbarkeit steigern.

Gruß
tm

@schl3ck

This comment has been minimized.

Copy link
Owner Author

@schl3ck schl3ck commented Apr 18, 2021

@martinellli Vielen Dank!

Hab das Skript aktualisiert. Die Zahlen werden mit der aktuellen Version nun mit dem systemweit eingestellten Format formatiert.

@martinellli

This comment has been minimized.

Copy link

@martinellli martinellli commented Apr 18, 2021

Jetzt ist es noch besser; danke für die schnelle Umsetzung! 👍

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