├── .gitignore
├── COVID-19 Dashboard.js
├── COVID-19 Global Incidence & Vaccination.js
├── COVID-19 Global Incidence.js
├── COVID-19 Global Vaccination.js
├── COVID-19 Local Incidence.js
├── README.md
└── Screenshots
├── COVID-19 Dashboard.png
├── COVID-19 Global Incidence & Vaccination.png
├── COVID-19 Global Incidence.png
├── COVID-19 Global Vaccination.png
└── COVID-19 Local Incidence.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Scripts by others
2 | ScriptDude.js
3 | MVG Abfahrtsmonitor.js
4 | NHL MyTeam.js
5 | Klopapier.js
6 | Inzidenz-Widget.js
7 | Inzidenz Debug.js
8 | Impfungs-Status.js
9 | Impfung Vorlage.js
10 | Ford Focus.js
11 | COVID Stats.js
12 | COVID Standard Time.js
13 |
14 | # Created by https://www.toptal.com/developers/gitignore/api/macos
15 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos
16 |
17 | ### macOS ###
18 | # General
19 | .DS_Store
20 | .AppleDouble
21 | .LSOverride
22 |
23 | # Icon must end with two \r
24 | Icon
25 |
26 |
27 | # Thumbnails
28 | ._*
29 |
30 | # Files that might appear in the root of a volume
31 | .DocumentRevisions-V100
32 | .fseventsd
33 | .Spotlight-V100
34 | .TemporaryItems
35 | .Trash
36 | .Trashes
37 | .VolumeIcon.icns
38 | .com.apple.timemachine.donotpresent
39 |
40 | # Directories potentially created on remote AFP share
41 | .AppleDB
42 | .AppleDesktop
43 | Network Trash Folder
44 | Temporary Items
45 | .apdisk
46 |
47 | # End of https://www.toptal.com/developers/gitignore/api/macos
--------------------------------------------------------------------------------
/COVID-19 Dashboard.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: deep-green; icon-glyph: syringe;
4 |
5 | ////////////////////////////////////////////////
6 | // Debug ///////////////////////////////////////
7 | ////////////////////////////////////////////////
8 | let debug = false
9 |
10 | // Fine tune Debug Mode by modifying specific variables below
11 | var logCache = true
12 | var logCacheUpdateStatus = true
13 | var logURLs = true
14 | var temporaryLogging = true // if (temporaryLogging) { console.log("") }
15 |
16 | ////////////////////////////////////////////////
17 | // Configuration ///////////////////////////////
18 | ////////////////////////////////////////////////
19 | let cacheInvalidationInMinutes = 60
20 |
21 | let smallWidgetWidth = 121
22 | let padding = 14
23 | let barWidth = smallWidgetWidth - 2 * padding
24 | let barHeight = 3
25 |
26 | let showFirstAndSecondVaccinationOnProgressBar = true
27 |
28 | // Global Configuration ////////////////////////
29 | let country = {
30 | germany: "DEU",
31 | canada: "CAN",
32 | usa: "USA"
33 | }
34 |
35 | let flag = {
36 | "DEU": "🇩🇪",
37 | "CAN": "🇨🇦",
38 | "USA": "🇺🇸"
39 | }
40 |
41 | // Local Configuration /////////////////////////
42 | let location = {
43 | kissing: "FDB",
44 | augsburg: "A",
45 | munich: "M",
46 | freilassing: "BGL"
47 | }
48 |
49 | let coordinates = {
50 | "FDB": "48.294,10.969",
51 | "A": "48.366,10.898",
52 | "M": "48.135,11.613",
53 | "BGL": "47.835,12.970"
54 | }
55 |
56 | let name = {
57 | "FDB": "Kissing",
58 | "A": "Augsburg",
59 | "M": "München",
60 | "BGL": "Freilassing"
61 | }
62 |
63 | ////////////////////////////////////////////////
64 | // Disable Debug Logs in Production ////////////
65 | ////////////////////////////////////////////////
66 |
67 | if (!debug) {
68 | logCache = false
69 | logCacheUpdateStatus = false
70 | logURLs = false
71 | temporaryLogging = false
72 | }
73 |
74 | ////////////////////////////////////////////////
75 | // Data ////////////////////////////////////////
76 | ////////////////////////////////////////////////
77 | let today = new Date()
78 |
79 | let formatter = new DateFormatter()
80 | formatter.locale = "en"
81 | formatter.dateFormat = "MMM d"
82 |
83 | // Vaccination Data ////////////////////////////
84 | let vaccinationResponseMemoryCache
85 | let vaccinationData = {}
86 |
87 | await loadVaccinationData(country.germany)
88 | await loadVaccinationData(country.canada)
89 | await loadVaccinationData(country.usa)
90 |
91 | // Global Case Data ////////////////////////////
92 | let globalCaseData = {}
93 |
94 | await loadGlobalCaseData(country.germany)
95 | await loadGlobalCaseData(country.canada)
96 | await loadGlobalCaseData(country.usa)
97 |
98 | // Local Case Data /////////////////////////////
99 | let localCaseData = {}
100 | let localHistoryData = {}
101 |
102 | await loadLocalCaseData(location.kissing)
103 | await loadLocalCaseData(location.augsburg)
104 | await loadLocalCaseData(location.munich)
105 | await loadLocalCaseData(location.freilassing)
106 |
107 | await loadLocalHistoryData(location.kissing)
108 | await loadLocalHistoryData(location.augsburg)
109 | await loadLocalHistoryData(location.munich)
110 | await loadLocalHistoryData(location.freilassing)
111 |
112 | ////////////////////////////////////////////////
113 | // Debug Execution - DO NOT MODIFY /////////////
114 | ////////////////////////////////////////////////
115 |
116 | printCache()
117 |
118 | ////////////////////////////////////////////////
119 | // Widget //////////////////////////////////////
120 | ////////////////////////////////////////////////
121 | let widget = new ListWidget()
122 | widget.setPadding(padding, padding, padding, padding)
123 | await createWidget()
124 |
125 | ////////////////////////////////////////////////
126 | // Script //////////////////////////////////////
127 | ////////////////////////////////////////////////
128 | Script.setWidget(widget)
129 | Script.complete()
130 | if (config.runsInApp) {
131 | widget.presentMedium()
132 | }
133 |
134 | ////////////////////////////////////////////////
135 | // Widget Creation /////////////////////////////
136 | ////////////////////////////////////////////////
137 | async function createWidget() {
138 | let canvas = widget.addStack()
139 | canvas.layoutVertically()
140 | displayTitle(canvas)
141 | canvas.addSpacer()
142 | displayContent(canvas)
143 | canvas.addSpacer()
144 | displayFooter(canvas)
145 | }
146 |
147 | // Title ///////////////////////////////////////
148 | function displayTitle(canvas) {
149 | let title = canvas.addText("COVID-19 Dashboard".toUpperCase())
150 | title.font = Font.semiboldRoundedSystemFont(13)
151 | title.textColor = Color.dynamic(Color.darkGray(), Color.lightGray())
152 | }
153 |
154 | // Content /////////////////////////////////////
155 | function displayContent(canvas) {
156 | let contentContainer = canvas.addStack()
157 | contentContainer.layoutHorizontally()
158 | let localContainer = contentContainer.addStack()
159 | localContainer.layoutVertically()
160 | displayLocalData(localContainer)
161 | contentContainer.addSpacer(padding)
162 | let globalContainer = contentContainer.addStack()
163 | globalContainer.size = new Size(smallWidgetWidth, -1)
164 | globalContainer.layoutVertically()
165 | displayGlobalData(globalContainer)
166 | }
167 |
168 | // Local Data //////////////////////////////////
169 | function displayLocalData(canvas) {
170 | displayPrimaryRegion(canvas, location.kissing)
171 | canvas.addSpacer(2)
172 | displaySecondaryRegionContainer(canvas, location.augsburg, location.munich, location.freilassing)
173 | }
174 |
175 | // Primary Region //////////////////////////////
176 | function displayPrimaryRegion(canvas, location) {
177 | let incidenceValue = localCaseData[location].cases7_per_100k.toFixed(1)
178 | let primaryLocationContainer = canvas.addStack()
179 | primaryLocationContainer.layoutHorizontally()
180 |
181 | let nameContainer = primaryLocationContainer.addStack()
182 | nameContainer.layoutVertically()
183 | let locationLabel = nameContainer.addText(name[location])
184 | locationLabel.font = Font.mediumRoundedSystemFont(13)
185 | let tendencyContainer = nameContainer.addStack()
186 | tendencyContainer.layoutHorizontally()
187 | tendencyContainer.addSpacer(10)
188 | let tendencyLabel = tendencyContainer.addText(getLocalTendency(location))
189 | tendencyLabel.font = Font.boldRoundedSystemFont(30)
190 | tendencyLabel.textColor = incidenceColor(incidenceValue)
191 | tendencyContainer.addSpacer(10)
192 |
193 | primaryLocationContainer.addSpacer()
194 |
195 | let incidenceContainer = primaryLocationContainer.addStack()
196 | incidenceContainer.layoutVertically()
197 | incidenceContainer.addSpacer()
198 | let incidenceLabel = incidenceContainer.addText(incidenceValue)
199 | incidenceLabel.font = Font.mediumRoundedSystemFont(40)
200 | incidenceLabel.textColor = incidenceColor(incidenceValue)
201 | }
202 |
203 | // Secondary Region Container //////////////////
204 | function displaySecondaryRegionContainer(canvas, location1, location2, location3) {
205 | let container = canvas.addStack()
206 | displaySecondaryRegion(container, location1)
207 | container.addSpacer()
208 | displaySecondaryRegion(container, location2)
209 | container.addSpacer()
210 | displaySecondaryRegion(container, location3)
211 | }
212 |
213 | // Secondary Region ////////////////////////////
214 | function displaySecondaryRegion(canvas, location) {
215 | let container = canvas.addStack()
216 | container.layoutVertically()
217 | let locationLabel = container.addText(name[location])
218 | locationLabel.font = Font.mediumRoundedSystemFont(10)
219 | locationLabel.textColor = Color.dynamic(Color.darkGray(), Color.lightGray())
220 | container.addSpacer(2)
221 | let incidenceValue = localCaseData[location].cases7_per_100k.toFixed(1)
222 | let incidenceLabel = container.addText(incidenceValue + " " + getLocalTendency(location))
223 | incidenceLabel.font = Font.semiboldRoundedSystemFont(10)
224 | incidenceLabel.textColor = incidenceColor(incidenceValue)
225 | }
226 |
227 | // Global Data /////////////////////////////////
228 | function displayGlobalData(canvas) {
229 | displayCountry(canvas, country.germany)
230 | canvas.addSpacer()
231 | displayCountry(canvas, country.canada)
232 | canvas.addSpacer()
233 | displayCountry(canvas, country.usa)
234 | canvas.addSpacer(2)
235 | }
236 |
237 | // Content Row /////////////////////////////////
238 | function displayCountry(canvas, country) {
239 | displayInformation(canvas, country)
240 | canvas.addSpacer(2)
241 | displayProgressBar(canvas, country)
242 | }
243 |
244 | // Country Data ////////////////////////////////
245 | function displayInformation(canvas, country) {
246 | let informationContainer = canvas.addStack()
247 | informationContainer.layoutHorizontally()
248 | displayFlag(informationContainer, country)
249 | informationContainer.addSpacer()
250 | displayIncidence(informationContainer, country)
251 | displayPercentage(informationContainer, country)
252 | }
253 |
254 | // Flag ////////////////////////////////////////
255 | function displayFlag(canvas, country) {
256 | let flagLabel = canvas.addText(flag[country])
257 | flagLabel.font = Font.regularRoundedSystemFont(13)
258 | }
259 |
260 | // 7-Day Incidence /////////////////////////////
261 | function displayIncidence(canvas, country) {
262 | let smallLabelContainer = canvas.addStack()
263 | smallLabelContainer.layoutVertically()
264 | smallLabelContainer.addSpacer(2)
265 | let incidenceValue = get7DayIncidence(country).toFixed(1)
266 | let incidenceLabel = smallLabelContainer.addText(incidenceValue + " " + getTendency(country))
267 | incidenceLabel.font = Font.semiboldRoundedSystemFont(10)
268 | incidenceLabel.textColor = incidenceColor(incidenceValue)
269 | }
270 |
271 | // Total Vaccination Percentage ////////////////
272 | function displayPercentage(canvas, country) {
273 | let percentageContainer = canvas.addStack()
274 | percentageContainer.size = new Size(50, 0)
275 | percentageContainer.layoutHorizontally()
276 | percentageContainer.addSpacer()
277 | let vaccinationPercentage = vaccinationData[country].people_fully_vaccinated_per_hundred
278 | let percentageLabel = percentageContainer.addText(vaccinationPercentage.toFixed(1) + "%")
279 | percentageLabel.font = Font.mediumRoundedSystemFont(13)
280 | percentageLabel.minimumScaleFactor = 0.8
281 | percentageLabel.lineLimit = 1
282 | }
283 |
284 | // Vaccination Progress Bar ////////////////////
285 | function displayProgressBar(canvas, country) {
286 | let firstVaccinationPercentage = vaccinationData[country].people_vaccinated_per_hundred
287 | let vaccinationPercentage = vaccinationData[country].people_fully_vaccinated_per_hundred
288 | let progressBar = canvas.addImage(drawProgressBar(firstVaccinationPercentage, vaccinationPercentage))
289 | progressBar.cornerRadius = barHeight / 2
290 | }
291 |
292 | // Progress Bar Creation ///////////////////////
293 | function drawProgressBar(firstVaccinationPercentage, fullVaccinationPercentage) {
294 | // Total Vaccination Target in Percent
295 | let target = {
296 | good: 60,
297 | perfect: 70
298 | }
299 |
300 | // Drawing Canvas
301 | let canvas = new DrawContext()
302 | canvas.size = new Size(barWidth, barHeight)
303 | canvas.opaque = false
304 | canvas.respectScreenScale = true
305 |
306 | // Bar Container
307 | canvas.setFillColor(Color.dynamic(Color.darkGray(), Color.lightGray()))
308 | let bar = new Path()
309 | let backgroundRect = new Rect(0, 0, barWidth, barHeight)
310 | bar.addRect(backgroundRect)
311 | canvas.addPath(bar)
312 | canvas.fillPath()
313 |
314 | if (showFirstAndSecondVaccinationOnProgressBar) {
315 | // Progress Bar Color for first vaccination
316 | let firstVaccinationColor = Color.dynamic(Color.lightGray(), Color.darkGray())
317 |
318 | // First Vaccination Progress Bar
319 | canvas.setFillColor(firstVaccinationColor)
320 | let firstVaccinationProgress = new Path()
321 | let firstVaccinationQuotient = firstVaccinationPercentage / 100
322 | let firstVaccinationProgressWidth = Math.min(barWidth, barWidth * firstVaccinationQuotient) // Makes breaking the scale impossible although barWidth * quotient should suffice
323 | firstVaccinationProgress.addRect(new Rect(0, 0, firstVaccinationProgressWidth, barHeight))
324 | canvas.addPath(firstVaccinationProgress)
325 | canvas.fillPath()
326 | }
327 |
328 | // Progress Bar Color depending on vaccination status
329 | let color
330 | if (fullVaccinationPercentage >= target.perfect) {
331 | color = Color.green()
332 | } else if (fullVaccinationPercentage >= target.good) {
333 | color = Color.orange()
334 | } else {
335 | color = Color.red()
336 | }
337 |
338 | // Progress Bar
339 | canvas.setFillColor(color)
340 | let progress = new Path()
341 | let quotient = fullVaccinationPercentage / 100
342 | let progressWidth = Math.min(barWidth, barWidth * quotient) // Makes breaking the scale impossible although barWidth * quotient should suffice
343 | progress.addRect(new Rect(0, 0, progressWidth, barHeight))
344 | canvas.addPath(progress)
345 | canvas.fillPath()
346 |
347 | return canvas.getImage()
348 | }
349 |
350 | // Footer //////////////////////////////////////
351 | function displayFooter(canvas) {
352 | let updateDictionary = getUpdateDictionary()
353 |
354 | let sortedUpdates = Object.keys(updateDictionary).sort().reverse() // newest first
355 | let updateInfoArray = sortedUpdates.map(k => relativeTimestamp(new Date(k)) + " (" + updateDictionary[k].join(', ') + ")")
356 | let updateInfoText = updateInfoArray.join(', ')
357 |
358 | let lastUpdateLabel = canvas.addText("Last Update: " + updateInfoText)
359 | lastUpdateLabel.font = Font.mediumRoundedSystemFont(10)
360 | lastUpdateLabel.textColor = Color.dynamic(Color.lightGray(), Color.darkGray())
361 | }
362 |
363 | function getUpdateDictionary() {
364 | let updateFormatter = new DateFormatter()
365 | updateFormatter.locale = "en"
366 | updateFormatter.dateFormat = "yyyy-MM-dd"
367 |
368 | let updateDict = {}
369 |
370 | let rkiUpdates = [getLastRKIUpdate(location.kissing), getLastRKIUpdate(location.augsburg), getLastRKIUpdate(location.munich), getLastRKIUpdate(location.freilassing)]
371 | let oldestLocalCasesUpdate = rkiUpdates.sort().reverse()[0]
372 | if (!updateDict[updateFormatter.string(oldestLocalCasesUpdate)]) {
373 | updateDict[updateFormatter.string(oldestLocalCasesUpdate)] = []
374 | }
375 | updateDict[updateFormatter.string(oldestLocalCasesUpdate)].push("RKI")
376 |
377 | let jhuUpdates = [getLastJHUUpdate(country.germany), getLastJHUUpdate(country.canada), getLastJHUUpdate(country.usa)]
378 | let oldestGlobalCasesUpdate = jhuUpdates.sort().reverse()[0]
379 | if (!updateDict[updateFormatter.string(oldestGlobalCasesUpdate)]) {
380 | updateDict[updateFormatter.string(oldestGlobalCasesUpdate)] = []
381 | }
382 | updateDict[updateFormatter.string(oldestGlobalCasesUpdate)].push("JHU")
383 |
384 | let owidUpdates = [getLastOWIDUpdate(country.germany), getLastOWIDUpdate(country.canada), getLastOWIDUpdate(country.usa)]
385 | let oldestGlobalVaccinationsUpdate = owidUpdates.sort().reverse()[0]
386 | if (!updateDict[updateFormatter.string(oldestGlobalVaccinationsUpdate)]) {
387 | updateDict[updateFormatter.string(oldestGlobalVaccinationsUpdate)] = []
388 | }
389 | updateDict[updateFormatter.string(oldestGlobalVaccinationsUpdate)].push("OWID")
390 |
391 | return updateDict
392 | }
393 |
394 | ////////////////////////////////////////////////
395 | // Calculations ////////////////////////////////
396 | ////////////////////////////////////////////////
397 | function incidenceColor(incidenceValue) {
398 | let color
399 | if (incidenceValue < 35) {
400 | color = Color.green()
401 | } else if (incidenceValue < 50) {
402 | color = Color.yellow()
403 | } else if (incidenceValue < 100) {
404 | color = Color.dynamic(new Color("e74300"), new Color("e64400"))
405 | } else {
406 | color = Color.dynamic(new Color("9e000a"), new Color("b61116")) // #ce2222
407 | }
408 | return color
409 | }
410 |
411 | function get7DayIncidence(country, requestedDate) {
412 | // Start Index = Date Difference to Today (defaults to today)
413 | let startIndex = requestedDate ? daysBetween(requestedDate, today) : 0
414 |
415 | // Sum up daily new cases for the 7 days from the requested date (or today if none specified)
416 | let newWeeklyCases = globalCaseData[country].cases.slice(startIndex, startIndex + 7).reduce(sum, 0)
417 | let population = vaccinationData[country].population
418 | return 100_000 * (newWeeklyCases / population)
419 | }
420 |
421 | function getTendency(country, accuracy, longTimeAccuracy) {
422 | let tendencyIndicator = {
423 | falling: "↘",
424 | steady: "→",
425 | rising: "↗"
426 | }
427 |
428 | let yesterday = new Date()
429 | yesterday.setDate(today.getDate() - 1)
430 |
431 | let lastWeek = new Date()
432 | lastWeek.setDate(today.getDate() - 7)
433 |
434 | let incidenceToday = get7DayIncidence(country, today)
435 | let incidenceYesterday = get7DayIncidence(country, yesterday)
436 | let incidenceLastWeek = get7DayIncidence(country, lastWeek)
437 | let incidenceDifference = incidenceToday - incidenceYesterday
438 | let longTermIncidenceDifference = incidenceToday - incidenceLastWeek
439 |
440 | // The short term tendency is deemed steady if it differs less than the requested accuracy (default: 5)
441 | let steadyRange = accuracy ?? 5
442 | // The long term tendency is deemed steady if it differs less than the requested long term accuracy (default: 10)
443 | let longTermSteadyRange = longTimeAccuracy ?? 10
444 |
445 | // The short term tendency is the primary return value. If short term is steady, the long term tendency will be returned, if it is similar to the short term tendency.
446 | let tendency
447 | if (incidenceDifference < -steadyRange) {
448 | tendency = tendencyIndicator.falling
449 | } else if (incidenceDifference > steadyRange) {
450 | tendency = tendencyIndicator.rising
451 | } else if (incidenceDifference <= 0 && longTermIncidenceDifference < -longTermSteadyRange) {
452 | tendency = tendencyIndicator.falling
453 | } else if (incidenceDifference >= 0 && longTermIncidenceDifference > longTermSteadyRange) {
454 | tendency = tendencyIndicator.rising
455 | } else {
456 | tendency = tendencyIndicator.steady
457 | }
458 |
459 | return tendency
460 | }
461 |
462 | function getLocal7DayIncidence(location, requestedDate) {
463 | // Start Index = Date Difference to Today (defaults to today)
464 | let startIndex = requestedDate ? daysBetween(requestedDate, today) : 0
465 |
466 | // Sum up daily new cases for the 7 days from the requested date (or today if none specified)
467 | let newWeeklyCasesArray = localHistoryData[location].cases.slice(startIndex, startIndex + 7)
468 | let newWeeklyCases = localHistoryData[location].cases.slice(startIndex, startIndex + 7).reduce(sum, 0)
469 | let population = localCaseData[location].EWZ
470 | return 100_000 * (newWeeklyCases / population)
471 | }
472 |
473 | function getLocalTendency(location, accuracy, longTimeAccuracy) {
474 | let tendencyIndicator = {
475 | falling: "↘",
476 | steady: "→",
477 | rising: "↗"
478 | }
479 |
480 | let yesterday = new Date()
481 | yesterday.setDate(today.getDate() - 1)
482 |
483 | let lastWeek = new Date()
484 | lastWeek.setDate(today.getDate() - 7)
485 |
486 | let incidenceToday = getLocal7DayIncidence(location, today)
487 | let incidenceYesterday = getLocal7DayIncidence(location, yesterday)
488 | let incidenceLastWeek = getLocal7DayIncidence(location, lastWeek)
489 | let incidenceDifference = incidenceToday - incidenceYesterday
490 | let longTermIncidenceDifference = incidenceToday - incidenceLastWeek
491 |
492 | // The short term tendency is deemed steady if it differs less than the requested accuracy (default: 5)
493 | let steadyRange = accuracy ?? 5
494 | // The long term tendency is deemed steady if it differs less than the requested long term accuracy (default: 10)
495 | let longTermSteadyRange = longTimeAccuracy ?? 10
496 |
497 | // The short term tendency is the primary return value. If short term is steady, the long term tendency will be returned, if it is similar to the short term tendency.
498 | let tendency
499 | if (incidenceDifference < -steadyRange) {
500 | tendency = tendencyIndicator.falling
501 | } else if (incidenceDifference > steadyRange) {
502 | tendency = tendencyIndicator.rising
503 | } else if (incidenceDifference <= 0 && longTermIncidenceDifference < -longTermSteadyRange) {
504 | tendency = tendencyIndicator.falling
505 | } else if (incidenceDifference >= 0 && longTermIncidenceDifference > longTermSteadyRange) {
506 | tendency = tendencyIndicator.rising
507 | } else {
508 | tendency = tendencyIndicator.steady
509 | }
510 |
511 | return tendency
512 | }
513 |
514 | function getCoordinates(location) {
515 | let coordinatesString = coordinates[location]
516 | let splitCoordinates = coordinatesString.split(",").map(parseFloat)
517 | return { latitude: splitCoordinates[0], longitude: splitCoordinates[1] }
518 | }
519 |
520 | function getRKIDateString(addDays) {
521 | addDays = addDays || 0
522 | return new Date(Date.now() + addDays * 24 * 60 * 60 * 1000).toISOString().substring(0, 10)
523 | }
524 |
525 | function getLastRKIUpdate(location) {
526 | let lastUpdate = new Date(localHistoryData[location].last_updated_date)
527 | // Since incidence is always determined by looking at cases from the previous day, we add 1 day here.
528 | lastUpdate.setDate(lastUpdate.getDate() + 1)
529 | // If data gets reported before midnight, the last update should still be today instead of tomorrow.
530 | return lastUpdate.getTime() > today.getTime() ? today : lastUpdate
531 | }
532 |
533 | function getLastJHUUpdate(country) {
534 | let lastUpdate = new Date(globalCaseData[country].last_updated_date)
535 | // Since incidence is always determined by looking at cases from the previous day, we add 1 day here.
536 | lastUpdate.setDate(lastUpdate.getDate() + 1)
537 | // If data gets reported before midnight in our time zone, the last update should still show today instead of tomorrow.
538 | return lastUpdate.getTime() > today.getTime() ? today : lastUpdate
539 | }
540 |
541 | function getLastOWIDUpdate(country) {
542 | let lastUpdate = new Date(vaccinationData[country].last_updated_date)
543 | // Since vaccinations are always reported at the end of the day, we add 1 day here (data from yesterday = last update today)
544 | lastUpdate.setDate(lastUpdate.getDate() + 1)
545 | // If data gets reported before midnight, the last update should still be today instead of tomorrow.
546 | return lastUpdate.getTime() > today.getTime() ? today : lastUpdate
547 | }
548 |
549 | function relativeTimestamp(date) {
550 | let yesterday = new Date()
551 | yesterday.setDate(yesterday.getDate() - 1)
552 |
553 | switch (formatter.string(date)) {
554 | case formatter.string(today):
555 | return "Today"
556 | case formatter.string(yesterday):
557 | return "Yesterday"
558 | default:
559 | return formatter.string(date)
560 | }
561 | }
562 |
563 | function sum(a, b) {
564 | return a + b
565 | }
566 |
567 | ////////////////////////////////////////////////
568 | // Networking //////////////////////////////////
569 | ////////////////////////////////////////////////
570 | async function loadVaccinationData(country) {
571 | let files = FileManager.local()
572 | let cacheName = debug ? ("debug-api-cache-ourworldindata-latest-" + country) : ("api-cache-ourworldindata-latest-" + country)
573 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
574 | let cacheExists = files.fileExists(cachePath)
575 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
576 |
577 | try {
578 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
579 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
580 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Using cached Data") }
581 | vaccinationData[country] = JSON.parse(files.readString(cachePath))
582 | } else {
583 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Updating cached Data") }
584 | if (logURLs) { console.log("\nURL: Vaccination " + country) }
585 | if (logURLs) { console.log('https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/latest/owid-covid-latest.json') }
586 | if (vaccinationResponseMemoryCache) {
587 | vaccinationData[country] = vaccinationResponseMemoryCache[country]
588 | } else {
589 | let response = await new Request('https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/latest/owid-covid-latest.json').loadJSON()
590 | vaccinationData[country] = response[country]
591 | }
592 | files.writeString(cachePath, JSON.stringify(vaccinationData[country]))
593 | }
594 | } catch (error) {
595 | console.error(error)
596 | if (cacheExists) {
597 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Loading new Data failed, using cached as fallback") }
598 | vaccinationData[country] = JSON.parse(files.readString(cachePath))
599 | } else {
600 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Loading new Data failed and no Cache found") }
601 | }
602 | }
603 | }
604 |
605 | async function loadGlobalCaseData(country) {
606 | let files = FileManager.local()
607 | let cacheName = debug ? ("debug-api-cache-global-cases-" + country) : ("api-cache-global-cases-" + country)
608 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
609 | let cacheExists = files.fileExists(cachePath)
610 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
611 |
612 | try {
613 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
614 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
615 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Using cached Data") }
616 | globalCaseData[country] = JSON.parse(files.readString(cachePath))
617 | } else {
618 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Updating cached Data") }
619 | if (logURLs) { console.log("\nURL: Cases " + country) }
620 | if (logURLs) { console.log('https://corona.lmao.ninja/v2/historical/' + country + '?lastdays=40') }
621 | let response = await new Request('https://corona.lmao.ninja/v2/historical/' + country + '?lastdays=40').loadJSON()
622 |
623 | let activeCases = {}
624 | let dates = []
625 | for (var entry in response.timeline.cases) {
626 | let date = new Date(entry)
627 | dates.push(date.getTime())
628 | activeCases[date.getTime()] = response.timeline.cases[entry]
629 | }
630 |
631 | let sortedKeys = dates.sort().reverse()
632 | globalCaseData[country] = {}
633 | globalCaseData[country]["cases"] = sortedKeys.map(date => activeCases[date] - activeCases[date - 24 * 60 * 60 * 1000]).slice(0,-1)
634 |
635 | // Add Last Update of JHU Data to Dictionary
636 | let lastJHUDataUpdate = treatAsUTC(new Date(parseInt(sortedKeys[0]))).toISOString()
637 | globalCaseData[country]["last_updated_date"] = lastJHUDataUpdate
638 | files.writeString(cachePath, JSON.stringify(globalCaseData[country]))
639 | }
640 | } catch (error) {
641 | console.error(error)
642 | if (cacheExists) {
643 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Loading new Data failed, using cached as fallback") }
644 | globalCaseData[country] = JSON.parse(files.readString(cachePath))
645 | } else {
646 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Loading new Data failed and no Cache found") }
647 | }
648 | }
649 | }
650 |
651 | async function loadLocalCaseData(location) {
652 | let files = FileManager.local()
653 | let cacheName = debug ? ("debug-api-cache-local-cases-" + location) : ("api-cache-local-cases-" + location)
654 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
655 | let cacheExists = files.fileExists(cachePath)
656 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
657 |
658 | try {
659 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
660 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
661 | if (logCacheUpdateStatus) { console.log(location + " Case Data: Using cached Data") }
662 | localCaseData[location] = JSON.parse(files.readString(cachePath))
663 | } else {
664 | if (logCacheUpdateStatus) { console.log(location + " Case Data: Updating cached Data") }
665 | let coordinates = getCoordinates(location)
666 | if (logURLs) { console.log("\nURL: Cases " + name[location]) }
667 | if (logURLs) { console.log('https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=RS,GEN,cases7_per_100k,EWZ&geometry=' + coordinates.longitude.toFixed(3) + '%2C' + coordinates.latitude.toFixed(3) + '&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnGeometry=false&outSR=4326&f=json') }
668 | let response = await new Request('https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=RS,GEN,cases7_per_100k,EWZ&geometry=' + coordinates.longitude.toFixed(3) + '%2C' + coordinates.latitude.toFixed(3) + '&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnGeometry=false&outSR=4326&f=json').loadJSON()
669 | localCaseData[location] = response.features[0].attributes
670 | files.writeString(cachePath, JSON.stringify(localCaseData[location]))
671 | }
672 | } catch (error) {
673 | console.error(error)
674 | if (cacheExists) {
675 | if (logCacheUpdateStatus) { console.log(location + " Case Data: Loading new Data failed, using cached as fallback") }
676 | localCaseData[location] = JSON.parse(files.readString(cachePath))
677 | } else {
678 | if (logCacheUpdateStatus) { console.log(location + " Case Data: Loading new Data failed and no Cache found") }
679 | }
680 | }
681 | }
682 |
683 | async function loadLocalHistoryData(location) {
684 | let files = FileManager.local()
685 | let cacheName = debug ? ("debug-api-cache-local-history-" + location) : ("api-cache-local-history-" + location)
686 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
687 | let cacheExists = files.fileExists(cachePath)
688 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
689 |
690 | try {
691 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
692 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
693 | if (logCacheUpdateStatus) { console.log(location + " History Data: Using cached Data") }
694 | localHistoryData[location] = JSON.parse(files.readString(cachePath))
695 | } else {
696 | if (logCacheUpdateStatus) { console.log(location + " History Data: Updating cached Data") }
697 | if (logURLs) { console.log("\nURL: History " + name[location]) }
698 | if (logURLs) { console.log('https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=IdLandkreis%20%3D%20%27' + localCaseData[location].RS + '%27%20AND%20Meldedatum%20%3E%3D%20TIMESTAMP%20%27' + getRKIDateString(-15) + '%2000%3A00%3A00%27%20AND%20Meldedatum%20%3C%3D%20TIMESTAMP%20%27' + getRKIDateString(1) + '%2000%3A00%3A00%27&outFields=Landkreis,Meldedatum,AnzahlFall&outSR=4326&f=json') }
699 | let response = await new Request('https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=IdLandkreis%20%3D%20%27' + localCaseData[location].RS + '%27%20AND%20Meldedatum%20%3E%3D%20TIMESTAMP%20%27' + getRKIDateString(-15) + '%2000%3A00%3A00%27%20AND%20Meldedatum%20%3C%3D%20TIMESTAMP%20%27' + getRKIDateString(1) + '%2000%3A00%3A00%27&outFields=Landkreis,Meldedatum,AnzahlFall&outSR=4326&f=json').loadJSON()
700 | // The response contains multiple entries per day. This sums them up and creates a new dictionary with each days new cases as values and the corresponding UNIX timestamp as keys.
701 | let aggregate = response.features.map(f => f.attributes).reduce((dict, feature) => {
702 | dict[feature["Meldedatum"]] = (dict[feature["Meldedatum"]]|0) + feature["AnzahlFall"]
703 | return dict
704 | }, {})
705 | let sortedKeys = Object.keys(aggregate).sort().reverse()
706 | // Local History Data is now being sorted by keys (Timestamps) and put into a sorted array (newest day first).
707 | localHistoryData[location] = {}
708 | localHistoryData[location]["cases"] = sortedKeys.map(k => aggregate[k])
709 |
710 | // Add Last Update of RKI Data to Dictionary
711 | let lastRKIDataUpdate = new Date(parseInt(sortedKeys[0])).toISOString()
712 | localHistoryData[location]["last_updated_date"] = lastRKIDataUpdate
713 | files.writeString(cachePath, JSON.stringify(localHistoryData[location]))
714 | }
715 | } catch (error) {
716 | console.error(error)
717 | if (cacheExists) {
718 | if (logCacheUpdateStatus) { console.log(location + " History Data: Loading new Data failed, using cached as fallback") }
719 | localHistoryData[location] = JSON.parse(files.readString(cachePath))
720 | } else {
721 | if (logCacheUpdateStatus) { console.log(location + " History Data: Loading new Data failed and no Cache found") }
722 | }
723 | }
724 | }
725 |
726 | ////////////////////////////////////////////////
727 | // Date Calculation ////////////////////////////
728 | ////////////////////////////////////////////////
729 | // --> see stackoverflow.com/a/11252167/6333824
730 |
731 | function treatAsUTC(date) {
732 | var result = new Date(date)
733 | result.setMinutes(result.getMinutes() - result.getTimezoneOffset())
734 | return result
735 | }
736 |
737 | function daysBetween(startDate, endDate) {
738 | var millisecondsPerDay = 24 * 60 * 60 * 1000
739 | return Math.round((treatAsUTC(endDate) - treatAsUTC(startDate)) / millisecondsPerDay)
740 | }
741 |
742 | ////////////////////////////////////////////////
743 | // Debug ///////////////////////////////////////
744 | ////////////////////////////////////////////////
745 |
746 | function printCache() {
747 | if (logCache) {
748 | console.log("\n\n**Global Vaccination Data**\n")
749 | console.log(JSON.stringify(vaccinationData, null, 2))
750 | console.log("\n\n**Global Cases Data**\n")
751 | console.log(JSON.stringify(globalCaseData, null, 2))
752 | console.log("\n\n**Local Cases Data**\n")
753 | console.log(JSON.stringify(localCaseData, null, 2))
754 | console.log("\n\n**Local History Data**\n")
755 | console.log(JSON.stringify(localHistoryData, null, 2))
756 | }
757 | }
758 |
759 |
760 |
761 |
762 | ////////////////////////////////////////////////
763 | // Author: Benno Kress /////////////////////////
764 | // Original: Benno Kress ///////////////////////
765 | // github.com/bennokress/Scriptable-Scripts ////
766 | // Please copy every line! /////////////////////
767 | ////////////////////////////////////////////////
--------------------------------------------------------------------------------
/COVID-19 Global Incidence & Vaccination.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: deep-green; icon-glyph: syringe;
4 |
5 | ////////////////////////////////////////////////
6 | // Debug ///////////////////////////////////////
7 | ////////////////////////////////////////////////
8 | let debug = false
9 |
10 | // Fine tune Debug Mode by modifying specific variables below
11 | var logCache = true
12 | var logCacheUpdateStatus = true
13 | var logURLs = true
14 | var temporaryLogging = true // if (temporaryLogging) { console.log("") }
15 |
16 | ////////////////////////////////////////////////
17 | // Configuration ///////////////////////////////
18 | ////////////////////////////////////////////////
19 | let cacheInvalidationInMinutes = 60
20 |
21 | let smallWidgetWidth = 121
22 | let padding = 14
23 | let barWidth = smallWidgetWidth - 2 * padding
24 | let barHeight = 3
25 |
26 | let showFirstAndSecondVaccinationOnProgressBar = true
27 |
28 | let country = {
29 | germany: "DEU",
30 | canada: "CAN",
31 | usa: "USA"
32 | }
33 |
34 | let flag = {
35 | "DEU": "🇩🇪",
36 | "CAN": "🇨🇦",
37 | "USA": "🇺🇸"
38 | }
39 |
40 | ////////////////////////////////////////////////
41 | // Disable Debug Logs in Production ////////////
42 | ////////////////////////////////////////////////
43 |
44 | if (!debug) {
45 | logCache = false
46 | logCacheUpdateStatus = false
47 | logURLs = false
48 | temporaryLogging = false
49 | }
50 |
51 | ////////////////////////////////////////////////
52 | // Data ////////////////////////////////////////
53 | ////////////////////////////////////////////////
54 | let today = new Date()
55 |
56 | let formatter = new DateFormatter()
57 | formatter.locale = "en"
58 | formatter.dateFormat = "MMM d"
59 |
60 | // Vaccination Data ////////////////////////////
61 | let vaccinationResponseMemoryCache
62 | let vaccinationData = {}
63 |
64 | await loadVaccinationData(country.germany)
65 | await loadVaccinationData(country.canada)
66 | await loadVaccinationData(country.usa)
67 |
68 | // Global Case Data ////////////////////////////
69 | let globalCaseData = {}
70 |
71 | await loadGlobalCaseData(country.germany)
72 | await loadGlobalCaseData(country.canada)
73 | await loadGlobalCaseData(country.usa)
74 |
75 | ////////////////////////////////////////////////
76 | // Debug Execution - DO NOT MODIFY /////////////
77 | ////////////////////////////////////////////////
78 |
79 | printCache()
80 |
81 | ////////////////////////////////////////////////
82 | // Widget //////////////////////////////////////
83 | ////////////////////////////////////////////////
84 | let widget = new ListWidget()
85 | widget.setPadding(padding, padding, padding, padding)
86 | await createWidget()
87 |
88 | ////////////////////////////////////////////////
89 | // Script //////////////////////////////////////
90 | ////////////////////////////////////////////////
91 | Script.setWidget(widget)
92 | Script.complete()
93 | if (config.runsInApp) {
94 | widget.presentSmall()
95 | }
96 |
97 | ////////////////////////////////////////////////
98 | // Widget Creation /////////////////////////////
99 | ////////////////////////////////////////////////
100 | async function createWidget() {
101 | let canvas = widget.addStack()
102 | canvas.layoutVertically()
103 | displayTitle(canvas)
104 | canvas.addSpacer()
105 | displayContent(canvas)
106 | canvas.addSpacer()
107 | displayFooter(canvas)
108 | }
109 |
110 | // Title ///////////////////////////////////////
111 | function displayTitle(canvas) {
112 | let title = canvas.addText("COVID-19".toUpperCase())
113 | title.font = Font.semiboldRoundedSystemFont(13)
114 | title.textColor = Color.dynamic(Color.darkGray(), Color.lightGray())
115 | }
116 |
117 | // Content /////////////////////////////////////
118 | function displayContent(canvas) {
119 | displayCountry(canvas, country.germany)
120 | canvas.addSpacer()
121 | displayCountry(canvas, country.canada)
122 | canvas.addSpacer()
123 | displayCountry(canvas, country.usa)
124 | }
125 |
126 | // Content Row /////////////////////////////////
127 | function displayCountry(canvas, country) {
128 | displayInformation(canvas, country)
129 | canvas.addSpacer(2)
130 | displayProgressBar(canvas, country)
131 | }
132 |
133 | // Country Data ////////////////////////////////
134 | function displayInformation(canvas, country) {
135 | let informationContainer = canvas.addStack()
136 | informationContainer.layoutHorizontally()
137 | displayFlag(informationContainer, country)
138 | informationContainer.addSpacer()
139 | displayIncidence(informationContainer, country)
140 | displayPercentage(informationContainer, country)
141 | }
142 |
143 | // Flag ////////////////////////////////////////
144 | function displayFlag(canvas, country) {
145 | let flagLabel = canvas.addText(flag[country])
146 | flagLabel.font = Font.regularRoundedSystemFont(13)
147 | }
148 |
149 | // 7-Day Incidence /////////////////////////////
150 | function displayIncidence(canvas, country) {
151 | let smallLabelContainer = canvas.addStack()
152 | smallLabelContainer.layoutVertically()
153 | smallLabelContainer.addSpacer(2)
154 | let incidenceValue = get7DayIncidence(country).toFixed(1)
155 | let incidenceLabel = smallLabelContainer.addText(incidenceValue + " " + getTendency(country))
156 | incidenceLabel.font = Font.semiboldRoundedSystemFont(10)
157 | incidenceLabel.textColor = incidenceColor(incidenceValue)
158 | }
159 |
160 | // Total Vaccination Percentage ////////////////
161 | function displayPercentage(canvas, country) {
162 | let percentageContainer = canvas.addStack()
163 | percentageContainer.size = new Size(50, 0)
164 | percentageContainer.layoutHorizontally()
165 | percentageContainer.addSpacer()
166 | let vaccinationPercentage = vaccinationData[country].people_fully_vaccinated_per_hundred
167 | let percentageLabel = percentageContainer.addText(vaccinationPercentage.toFixed(1) + "%")
168 | percentageLabel.font = Font.mediumRoundedSystemFont(13)
169 | percentageLabel.minimumScaleFactor = 0.8
170 | percentageLabel.lineLimit = 1
171 | }
172 |
173 | // Vaccination Progress Bar ////////////////////
174 | function displayProgressBar(canvas, country) {
175 | let firstVaccinationPercentage = vaccinationData[country].people_vaccinated_per_hundred
176 | let vaccinationPercentage = vaccinationData[country].people_fully_vaccinated_per_hundred
177 | let progressBar = canvas.addImage(drawProgressBar(firstVaccinationPercentage, vaccinationPercentage))
178 | progressBar.cornerRadius = barHeight / 2
179 | }
180 |
181 | // Progress Bar Creation ///////////////////////
182 | function drawProgressBar(firstVaccinationPercentage, fullVaccinationPercentage) {
183 | // Total Vaccination Target in Percent
184 | let target = {
185 | good: 60,
186 | perfect: 70
187 | }
188 |
189 | // Drawing Canvas
190 | let canvas = new DrawContext()
191 | canvas.size = new Size(barWidth, barHeight)
192 | canvas.opaque = false
193 | canvas.respectScreenScale = true
194 |
195 | // Bar Container
196 | canvas.setFillColor(Color.dynamic(Color.darkGray(), Color.lightGray()))
197 | let bar = new Path()
198 | let backgroundRect = new Rect(0, 0, barWidth, barHeight)
199 | bar.addRect(backgroundRect)
200 | canvas.addPath(bar)
201 | canvas.fillPath()
202 |
203 | if (showFirstAndSecondVaccinationOnProgressBar) {
204 | // Progress Bar Color for first vaccination
205 | let firstVaccinationColor = Color.dynamic(Color.lightGray(), Color.darkGray())
206 |
207 | // First Vaccination Progress Bar
208 | canvas.setFillColor(firstVaccinationColor)
209 | let firstVaccinationProgress = new Path()
210 | let firstVaccinationQuotient = firstVaccinationPercentage / 100
211 | let firstVaccinationProgressWidth = Math.min(barWidth, barWidth * firstVaccinationQuotient) // Makes breaking the scale impossible although barWidth * quotient should suffice
212 | firstVaccinationProgress.addRect(new Rect(0, 0, firstVaccinationProgressWidth, barHeight))
213 | canvas.addPath(firstVaccinationProgress)
214 | canvas.fillPath()
215 | }
216 |
217 | // Progress Bar Color depending on vaccination status
218 | let color
219 | if (fullVaccinationPercentage >= target.perfect) {
220 | color = Color.green()
221 | } else if (fullVaccinationPercentage >= target.good) {
222 | color = Color.orange()
223 | } else {
224 | color = Color.red()
225 | }
226 |
227 | // Progress Bar
228 | canvas.setFillColor(color)
229 | let progress = new Path()
230 | let quotient = fullVaccinationPercentage / 100
231 | let progressWidth = Math.min(barWidth, barWidth * quotient) // Makes breaking the scale impossible although barWidth * quotient should suffice
232 | progress.addRect(new Rect(0, 0, progressWidth, barHeight))
233 | canvas.addPath(progress)
234 | canvas.fillPath()
235 |
236 | return canvas.getImage()
237 | }
238 |
239 | // Footer //////////////////////////////////////
240 | function displayFooter(canvas) {
241 | let updateDictionary = getUpdateDictionary()
242 |
243 | let oldestUpdate = Object.keys(updateDictionary).sort()[0] // only oldest
244 | let updateInfoText = relativeTimestamp(new Date(oldestUpdate)) + " (" + updateDictionary[oldestUpdate].join(', ') + ")"
245 |
246 | let lastUpdateLabel = canvas.addText(updateInfoText)
247 | lastUpdateLabel.font = Font.mediumRoundedSystemFont(10)
248 | lastUpdateLabel.textColor = Color.dynamic(Color.lightGray(), Color.darkGray())
249 | }
250 |
251 | function getUpdateDictionary() {
252 | let updateFormatter = new DateFormatter()
253 | updateFormatter.locale = "en"
254 | updateFormatter.dateFormat = "yyyy-MM-dd"
255 |
256 | let updateDict = {}
257 |
258 | let jhuUpdates = [getLastJHUUpdate(country.germany), getLastJHUUpdate(country.canada), getLastJHUUpdate(country.usa)]
259 | let oldestGlobalCasesUpdate = jhuUpdates.sort().reverse()[0]
260 | if (!updateDict[updateFormatter.string(oldestGlobalCasesUpdate)]) {
261 | updateDict[updateFormatter.string(oldestGlobalCasesUpdate)] = []
262 | }
263 | updateDict[updateFormatter.string(oldestGlobalCasesUpdate)].push("JHU")
264 |
265 | let owidUpdates = [getLastOWIDUpdate(country.germany), getLastOWIDUpdate(country.canada), getLastOWIDUpdate(country.usa)]
266 | let oldestGlobalVaccinationsUpdate = owidUpdates.sort().reverse()[0]
267 | if (!updateDict[updateFormatter.string(oldestGlobalVaccinationsUpdate)]) {
268 | updateDict[updateFormatter.string(oldestGlobalVaccinationsUpdate)] = []
269 | }
270 | updateDict[updateFormatter.string(oldestGlobalVaccinationsUpdate)].push("OWID")
271 |
272 | return updateDict
273 | }
274 |
275 | ////////////////////////////////////////////////
276 | // Calculations ////////////////////////////////
277 | ////////////////////////////////////////////////
278 | function incidenceColor(incidenceValue) {
279 | let color
280 | if (incidenceValue < 35) {
281 | color = Color.green()
282 | } else if (incidenceValue < 50) {
283 | color = Color.yellow()
284 | } else if (incidenceValue < 100) {
285 | color = Color.dynamic(new Color("e74300"), new Color("e64400"))
286 | } else {
287 | color = Color.dynamic(new Color("9e000a"), new Color("b61116")) // #ce2222
288 | }
289 | return color
290 | }
291 |
292 | function get7DayIncidence(country, requestedDate) {
293 | // Start Index = Date Difference to Today (defaults to today)
294 | let startIndex = requestedDate ? daysBetween(requestedDate, today) : 0
295 |
296 | // Sum up daily new cases for the 7 days from the requested date (or today if none specified)
297 | let newWeeklyCases = globalCaseData[country].cases.slice(startIndex, startIndex + 7).reduce(sum, 0)
298 | let population = vaccinationData[country].population
299 | return 100_000 * (newWeeklyCases / population)
300 | }
301 |
302 | function getTendency(country, accuracy, longTimeAccuracy) {
303 | let tendencyIndicator = {
304 | falling: "↘",
305 | steady: "→",
306 | rising: "↗"
307 | }
308 |
309 | let yesterday = new Date()
310 | yesterday.setDate(today.getDate() - 1)
311 |
312 | let lastWeek = new Date()
313 | lastWeek.setDate(today.getDate() - 7)
314 |
315 | let incidenceToday = get7DayIncidence(country, today)
316 | let incidenceYesterday = get7DayIncidence(country, yesterday)
317 | let incidenceLastWeek = get7DayIncidence(country, lastWeek)
318 | let incidenceDifference = incidenceToday - incidenceYesterday
319 | let longTermIncidenceDifference = incidenceToday - incidenceLastWeek
320 |
321 | // The short term tendency is deemed steady if it differs less than the requested accuracy (default: 5)
322 | let steadyRange = accuracy ?? 5
323 | // The long term tendency is deemed steady if it differs less than the requested long term accuracy (default: 10)
324 | let longTermSteadyRange = longTimeAccuracy ?? 10
325 |
326 | // The short term tendency is the primary return value. If short term is steady, the long term tendency will be returned, if it is similar to the short term tendency.
327 | let tendency
328 | if (incidenceDifference < -steadyRange) {
329 | tendency = tendencyIndicator.falling
330 | } else if (incidenceDifference > steadyRange) {
331 | tendency = tendencyIndicator.rising
332 | } else if (incidenceDifference <= 0 && longTermIncidenceDifference < -longTermSteadyRange) {
333 | tendency = tendencyIndicator.falling
334 | } else if (incidenceDifference >= 0 && longTermIncidenceDifference > longTermSteadyRange) {
335 | tendency = tendencyIndicator.rising
336 | } else {
337 | tendency = tendencyIndicator.steady
338 | }
339 |
340 | return tendency
341 | }
342 |
343 | function getLastJHUUpdate(country) {
344 | let lastUpdate = new Date(globalCaseData[country].last_updated_date)
345 | // Since incidence is always determined by looking at cases from the previous day, we add 1 day here.
346 | lastUpdate.setDate(lastUpdate.getDate() + 1)
347 | // If data gets reported before midnight in our time zone, the last update should still show today instead of tomorrow.
348 | return lastUpdate.getTime() > today.getTime() ? today : lastUpdate
349 | }
350 |
351 | function getLastOWIDUpdate(country) {
352 | let lastUpdate = new Date(vaccinationData[country].last_updated_date)
353 | // Since vaccinations are always reported at the end of the day, we add 1 day here (data from yesterday = last update today)
354 | lastUpdate.setDate(lastUpdate.getDate() + 1)
355 | // If data gets reported before midnight, the last update should still be today instead of tomorrow.
356 | return lastUpdate.getTime() > today.getTime() ? today : lastUpdate
357 | }
358 |
359 | function relativeTimestamp(date) {
360 | let yesterday = new Date()
361 | yesterday.setDate(yesterday.getDate() - 1)
362 |
363 | switch (formatter.string(date)) {
364 | case formatter.string(today):
365 | return "Today"
366 | case formatter.string(yesterday):
367 | return "Yesterday"
368 | default:
369 | return formatter.string(date)
370 | }
371 | }
372 |
373 | function sum(a, b) {
374 | return a + b
375 | }
376 |
377 | ////////////////////////////////////////////////
378 | // Networking //////////////////////////////////
379 | ////////////////////////////////////////////////
380 | async function loadVaccinationData(country) {
381 | let files = FileManager.local()
382 | let cacheName = debug ? ("debug-api-cache-ourworldindata-latest-" + country) : ("api-cache-ourworldindata-latest-" + country)
383 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
384 | let cacheExists = files.fileExists(cachePath)
385 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
386 |
387 | try {
388 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
389 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
390 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Using cached Data") }
391 | vaccinationData[country] = JSON.parse(files.readString(cachePath))
392 | } else {
393 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Updating cached Data") }
394 | if (logURLs) { console.log("\nURL: Vaccination " + country) }
395 | if (logURLs) { console.log('https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/latest/owid-covid-latest.json') }
396 | if (vaccinationResponseMemoryCache) {
397 | vaccinationData[country] = vaccinationResponseMemoryCache[country]
398 | } else {
399 | let response = await new Request('https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/latest/owid-covid-latest.json').loadJSON()
400 | vaccinationData[country] = response[country]
401 | }
402 | files.writeString(cachePath, JSON.stringify(vaccinationData[country]))
403 | }
404 | } catch (error) {
405 | console.error(error)
406 | if (cacheExists) {
407 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Loading new Data failed, using cached as fallback") }
408 | vaccinationData[country] = JSON.parse(files.readString(cachePath))
409 | } else {
410 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Loading new Data failed and no Cache found") }
411 | }
412 | }
413 | }
414 |
415 | async function loadGlobalCaseData(country) {
416 | let files = FileManager.local()
417 | let cacheName = debug ? ("debug-api-cache-global-cases-" + country) : ("api-cache-global-cases-" + country)
418 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
419 | let cacheExists = files.fileExists(cachePath)
420 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
421 |
422 | try {
423 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
424 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
425 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Using cached Data") }
426 | globalCaseData[country] = JSON.parse(files.readString(cachePath))
427 | } else {
428 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Updating cached Data") }
429 | if (logURLs) { console.log("\nURL: Cases " + country) }
430 | if (logURLs) { console.log('https://corona.lmao.ninja/v2/historical/' + country + '?lastdays=40') }
431 | let response = await new Request('https://corona.lmao.ninja/v2/historical/' + country + '?lastdays=40').loadJSON()
432 |
433 | let activeCases = {}
434 | let dates = []
435 | for (var entry in response.timeline.cases) {
436 | let date = new Date(entry)
437 | dates.push(date.getTime())
438 | activeCases[date.getTime()] = response.timeline.cases[entry]
439 | }
440 |
441 | let sortedKeys = dates.sort().reverse()
442 | globalCaseData[country] = {}
443 | globalCaseData[country]["cases"] = sortedKeys.map(date => activeCases[date] - activeCases[date - 24 * 60 * 60 * 1000]).slice(0,-1)
444 |
445 | // Add Last Update of JHU Data to Dictionary
446 | let lastJHUDataUpdate = treatAsUTC(new Date(parseInt(sortedKeys[0]))).toISOString()
447 | globalCaseData[country]["last_updated_date"] = lastJHUDataUpdate
448 | files.writeString(cachePath, JSON.stringify(globalCaseData[country]))
449 | }
450 | } catch (error) {
451 | console.error(error)
452 | if (cacheExists) {
453 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Loading new Data failed, using cached as fallback") }
454 | globalCaseData[country] = JSON.parse(files.readString(cachePath))
455 | } else {
456 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Loading new Data failed and no Cache found") }
457 | }
458 | }
459 | }
460 |
461 | ////////////////////////////////////////////////
462 | // Date Calculation ////////////////////////////
463 | ////////////////////////////////////////////////
464 | // --> see stackoverflow.com/a/11252167/6333824
465 |
466 | function treatAsUTC(date) {
467 | var result = new Date(date)
468 | result.setMinutes(result.getMinutes() - result.getTimezoneOffset())
469 | return result
470 | }
471 |
472 | function daysBetween(startDate, endDate) {
473 | var millisecondsPerDay = 24 * 60 * 60 * 1000
474 | return Math.round((treatAsUTC(endDate) - treatAsUTC(startDate)) / millisecondsPerDay)
475 | }
476 |
477 | ////////////////////////////////////////////////
478 | // Debug ///////////////////////////////////////
479 | ////////////////////////////////////////////////
480 |
481 | function printCache() {
482 | if (logCache) {
483 | console.log("\n\n**Global Vaccination Data**\n")
484 | console.log(JSON.stringify(vaccinationData, null, 2))
485 | console.log("\n\n**Global Cases Data**\n")
486 | console.log(JSON.stringify(globalCaseData, null, 2))
487 | }
488 | }
489 |
490 |
491 |
492 |
493 | ////////////////////////////////////////////////
494 | // Author: Benno Kress /////////////////////////
495 | // Original: Benno Kress ///////////////////////
496 | // github.com/bennokress/Scriptable-Scripts ////
497 | // Please copy every line! /////////////////////
498 | ////////////////////////////////////////////////
--------------------------------------------------------------------------------
/COVID-19 Global Incidence.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: deep-green; icon-glyph: chart-line;
4 |
5 | ////////////////////////////////////////////////
6 | // Debug ///////////////////////////////////////
7 | ////////////////////////////////////////////////
8 | let debug = false
9 |
10 | // Fine tune Debug Mode by modifying specific variables below
11 | var logCache = true
12 | var logCacheUpdateStatus = true
13 | var logURLs = true
14 | var temporaryLogging = true // if (temporaryLogging) { console.log("") }
15 |
16 | ////////////////////////////////////////////////
17 | // Configuration ///////////////////////////////
18 | ////////////////////////////////////////////////
19 | let cacheInvalidationInMinutes = 60
20 | let padding = 14
21 |
22 | let country = {
23 | germany: "DEU",
24 | canada: "CAN",
25 | usa: "USA"
26 | }
27 |
28 | let flag = {
29 | "DEU": "🇩🇪",
30 | "CAN": "🇨🇦",
31 | "USA": "🇺🇸"
32 | }
33 |
34 | let name = {
35 | "DEU": "Germany",
36 | "CAN": "Canada",
37 | "USA": "USA"
38 | }
39 |
40 | ////////////////////////////////////////////////
41 | // Disable Debug Logs in Production ////////////
42 | ////////////////////////////////////////////////
43 |
44 | if (!debug) {
45 | logCache = false
46 | logCacheUpdateStatus = false
47 | logURLs = false
48 | temporaryLogging = false
49 | }
50 |
51 | ////////////////////////////////////////////////
52 | // Data ////////////////////////////////////////
53 | ////////////////////////////////////////////////
54 | let today = new Date()
55 |
56 | let formatter = new DateFormatter()
57 | formatter.locale = "en"
58 | formatter.dateFormat = "MMM d"
59 |
60 | // Vaccination Data ////////////////////////////
61 | let vaccinationResponseMemoryCache
62 | let vaccinationData = {}
63 |
64 | await loadVaccinationData(country.germany)
65 | await loadVaccinationData(country.canada)
66 | await loadVaccinationData(country.usa)
67 |
68 | // Global Case Data ////////////////////////////
69 | let globalCaseData = {}
70 |
71 | await loadGlobalCaseData(country.germany)
72 | await loadGlobalCaseData(country.canada)
73 | await loadGlobalCaseData(country.usa)
74 |
75 | ////////////////////////////////////////////////
76 | // Debug Execution - DO NOT MODIFY /////////////
77 | ////////////////////////////////////////////////
78 |
79 | printCache()
80 |
81 | ////////////////////////////////////////////////
82 | // Widget //////////////////////////////////////
83 | ////////////////////////////////////////////////
84 | let widget = new ListWidget()
85 | widget.setPadding(padding, padding, padding, padding)
86 | await createWidget()
87 |
88 | ////////////////////////////////////////////////
89 | // Script //////////////////////////////////////
90 | ////////////////////////////////////////////////
91 | Script.setWidget(widget)
92 | Script.complete()
93 | if (config.runsInApp) {
94 | widget.presentSmall()
95 | }
96 |
97 | ////////////////////////////////////////////////
98 | // Widget Creation /////////////////////////////
99 | ////////////////////////////////////////////////
100 | async function createWidget() {
101 | let canvas = widget.addStack()
102 | canvas.layoutVertically()
103 | displayTitle(canvas)
104 | canvas.addSpacer()
105 | displayContent(canvas)
106 | canvas.addSpacer()
107 | displayFooter(canvas)
108 | }
109 |
110 | // Title ///////////////////////////////////////
111 | function displayTitle(canvas) {
112 | let title = canvas.addText("JHU Incidence".toUpperCase())
113 | title.font = Font.semiboldRoundedSystemFont(13)
114 | title.textColor = Color.dynamic(Color.darkGray(), Color.lightGray())
115 | }
116 |
117 | // Content /////////////////////////////////////
118 | function displayContent(canvas) {
119 | displayPrimaryRegion(canvas, country.germany)
120 | canvas.addSpacer(2)
121 | displaySecondaryRegionContainer(canvas, country.canada, country.usa)
122 | }
123 |
124 | // Primary Region //////////////////////////////
125 | function displayPrimaryRegion(canvas, country) {
126 | let incidenceValue = get7DayIncidence(country).toFixed(1)
127 |
128 | let locationLabel = canvas.addText(flag[country] + " " + name[country])
129 | locationLabel.font = Font.mediumRoundedSystemFont(13)
130 |
131 | let incidenceContainer = canvas.addStack()
132 | incidenceContainer.layoutHorizontally()
133 |
134 | incidenceContainer.addSpacer(10)
135 | let tendencyLabel = incidenceContainer.addText(getTendency(country))
136 | tendencyLabel.font = Font.mediumRoundedSystemFont(30)
137 | tendencyLabel.textColor = incidenceColor(incidenceValue)
138 | incidenceContainer.addSpacer()
139 | let incidenceLabel = incidenceContainer.addText(incidenceValue)
140 | incidenceLabel.font = Font.mediumRoundedSystemFont(30)
141 | incidenceLabel.textColor = incidenceColor(incidenceValue)
142 | }
143 |
144 | // Secondary Region Container //////////////////
145 | function displaySecondaryRegionContainer(canvas, country1, country2) {
146 | let container = canvas.addStack()
147 | displaySecondaryRegion(container, country1)
148 | container.addSpacer()
149 | displaySecondaryRegion(container, country2)
150 | }
151 |
152 | // Secondary Region ////////////////////////////
153 | function displaySecondaryRegion(canvas, country) {
154 | let container = canvas.addStack()
155 | container.layoutVertically()
156 | let locationLabel = container.addText(flag[country] + " " + name[country])
157 | locationLabel.font = Font.mediumRoundedSystemFont(10)
158 | locationLabel.textColor = Color.dynamic(Color.darkGray(), Color.lightGray())
159 | container.addSpacer(2)
160 | let incidenceValue = get7DayIncidence(country).toFixed(1)
161 | let incidenceLabel = container.addText(incidenceValue + " " + getTendency(country))
162 | incidenceLabel.font = Font.semiboldRoundedSystemFont(10)
163 | incidenceLabel.textColor = incidenceColor(incidenceValue)
164 | }
165 |
166 | // Footer //////////////////////////////////////
167 | function displayFooter(canvas) {
168 | let updateDictionary = getUpdateDictionary()
169 |
170 | let sortedUpdates = Object.keys(updateDictionary).sort().reverse() // newest first
171 | let updateInfoArray = sortedUpdates.map(k => relativeTimestamp(new Date(k)))
172 | let updateInfoText = updateInfoArray.join(', ')
173 |
174 | let lastUpdateLabel = canvas.addText("Last Update: " + updateInfoText)
175 | lastUpdateLabel.font = Font.mediumRoundedSystemFont(10)
176 | lastUpdateLabel.textColor = Color.dynamic(Color.lightGray(), Color.darkGray())
177 | }
178 |
179 | function getUpdateDictionary() {
180 | let updateFormatter = new DateFormatter()
181 | updateFormatter.locale = "en"
182 | updateFormatter.dateFormat = "yyyy-MM-dd"
183 |
184 | let updateDict = {}
185 |
186 | let jhuUpdates = [getLastJHUUpdate(country.germany), getLastJHUUpdate(country.canada), getLastJHUUpdate(country.usa)]
187 | let oldestGlobalCasesUpdate = jhuUpdates.sort().reverse()[0]
188 | if (!updateDict[updateFormatter.string(oldestGlobalCasesUpdate)]) {
189 | updateDict[updateFormatter.string(oldestGlobalCasesUpdate)] = []
190 | }
191 | updateDict[updateFormatter.string(oldestGlobalCasesUpdate)].push("JHU")
192 |
193 | return updateDict
194 | }
195 |
196 | ////////////////////////////////////////////////
197 | // Calculations ////////////////////////////////
198 | ////////////////////////////////////////////////
199 | function incidenceColor(incidenceValue) {
200 | let color
201 | if (incidenceValue < 35) {
202 | color = Color.green()
203 | } else if (incidenceValue < 50) {
204 | color = Color.yellow()
205 | } else if (incidenceValue < 100) {
206 | color = Color.dynamic(new Color("e74300"), new Color("e64400"))
207 | } else {
208 | color = Color.dynamic(new Color("9e000a"), new Color("b61116")) // #ce2222
209 | }
210 | return color
211 | }
212 |
213 | function get7DayIncidence(country, requestedDate) {
214 | // Start Index = Date Difference to Today (defaults to today)
215 | let startIndex = requestedDate ? daysBetween(requestedDate, today) : 0
216 |
217 | // Sum up daily new cases for the 7 days from the requested date (or today if none specified)
218 | let newWeeklyCases = globalCaseData[country].cases.slice(startIndex, startIndex + 7).reduce(sum, 0)
219 | let population = vaccinationData[country].population
220 | return 100_000 * (newWeeklyCases / population)
221 | }
222 |
223 | function getTendency(country, accuracy, longTimeAccuracy) {
224 | let tendencyIndicator = {
225 | falling: "↘",
226 | steady: "→",
227 | rising: "↗"
228 | }
229 |
230 | let yesterday = new Date()
231 | yesterday.setDate(today.getDate() - 1)
232 |
233 | let lastWeek = new Date()
234 | lastWeek.setDate(today.getDate() - 7)
235 |
236 | let incidenceToday = get7DayIncidence(country, today)
237 | let incidenceYesterday = get7DayIncidence(country, yesterday)
238 | let incidenceLastWeek = get7DayIncidence(country, lastWeek)
239 | let incidenceDifference = incidenceToday - incidenceYesterday
240 | let longTermIncidenceDifference = incidenceToday - incidenceLastWeek
241 |
242 | // The short term tendency is deemed steady if it differs less than the requested accuracy (default: 5)
243 | let steadyRange = accuracy ?? 5
244 | // The long term tendency is deemed steady if it differs less than the requested long term accuracy (default: 10)
245 | let longTermSteadyRange = longTimeAccuracy ?? 10
246 |
247 | // The short term tendency is the primary return value. If short term is steady, the long term tendency will be returned, if it is similar to the short term tendency.
248 | let tendency
249 | if (incidenceDifference < -steadyRange) {
250 | tendency = tendencyIndicator.falling
251 | } else if (incidenceDifference > steadyRange) {
252 | tendency = tendencyIndicator.rising
253 | } else if (incidenceDifference <= 0 && longTermIncidenceDifference < -longTermSteadyRange) {
254 | tendency = tendencyIndicator.falling
255 | } else if (incidenceDifference >= 0 && longTermIncidenceDifference > longTermSteadyRange) {
256 | tendency = tendencyIndicator.rising
257 | } else {
258 | tendency = tendencyIndicator.steady
259 | }
260 |
261 | return tendency
262 | }
263 |
264 | function getLastJHUUpdate(country) {
265 | let lastUpdate = new Date(globalCaseData[country].last_updated_date)
266 | // Since incidence is always determined by looking at cases from the previous day, we add 1 day here.
267 | lastUpdate.setDate(lastUpdate.getDate() + 1)
268 | // If data gets reported before midnight in our time zone, the last update should still show today instead of tomorrow.
269 | return lastUpdate.getTime() > today.getTime() ? today : lastUpdate
270 | }
271 |
272 | function relativeTimestamp(date) {
273 | let yesterday = new Date()
274 | yesterday.setDate(today.getDate() - 1)
275 |
276 | switch (formatter.string(date)) {
277 | case formatter.string(today):
278 | return "Today"
279 | case formatter.string(yesterday):
280 | return "Yesterday"
281 | default:
282 | return formatter.string(date)
283 | }
284 | }
285 |
286 | function sum(a, b) {
287 | return a + b
288 | }
289 |
290 | ////////////////////////////////////////////////
291 | // Networking //////////////////////////////////
292 | ////////////////////////////////////////////////
293 | async function loadVaccinationData(country) {
294 | let files = FileManager.local()
295 | let cacheName = debug ? ("debug-api-cache-ourworldindata-latest-" + country) : ("api-cache-ourworldindata-latest-" + country)
296 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
297 | let cacheExists = files.fileExists(cachePath)
298 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
299 |
300 | try {
301 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
302 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
303 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Using cached Data") }
304 | vaccinationData[country] = JSON.parse(files.readString(cachePath))
305 | } else {
306 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Updating cached Data") }
307 | if (logURLs) { console.log("\nURL: Vaccination " + country) }
308 | if (logURLs) { console.log('https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/latest/owid-covid-latest.json') }
309 | if (vaccinationResponseMemoryCache) {
310 | vaccinationData[country] = vaccinationResponseMemoryCache[country]
311 | } else {
312 | let response = await new Request('https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/latest/owid-covid-latest.json').loadJSON()
313 | vaccinationData[country] = response[country]
314 | }
315 | files.writeString(cachePath, JSON.stringify(vaccinationData[country]))
316 | }
317 | } catch (error) {
318 | console.error(error)
319 | if (cacheExists) {
320 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Loading new Data failed, using cached as fallback") }
321 | vaccinationData[country] = JSON.parse(files.readString(cachePath))
322 | } else {
323 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Loading new Data failed and no Cache found") }
324 | }
325 | }
326 | }
327 |
328 | async function loadGlobalCaseData(country) {
329 | let files = FileManager.local()
330 | let cacheName = debug ? ("debug-api-cache-global-cases-" + country) : ("api-cache-global-cases-" + country)
331 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
332 | let cacheExists = files.fileExists(cachePath)
333 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
334 |
335 | try {
336 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
337 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
338 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Using cached Data") }
339 | globalCaseData[country] = JSON.parse(files.readString(cachePath))
340 | } else {
341 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Updating cached Data") }
342 | if (logURLs) { console.log("\nURL: Cases " + country) }
343 | if (logURLs) { console.log('https://corona.lmao.ninja/v2/historical/' + country + '?lastdays=40') }
344 | let response = await new Request('https://corona.lmao.ninja/v2/historical/' + country + '?lastdays=40').loadJSON()
345 |
346 | let activeCases = {}
347 | let dates = []
348 | for (var entry in response.timeline.cases) {
349 | let date = new Date(entry)
350 | dates.push(date.getTime())
351 | activeCases[date.getTime()] = response.timeline.cases[entry]
352 | }
353 |
354 | let sortedKeys = dates.sort().reverse()
355 | globalCaseData[country] = {}
356 | globalCaseData[country]["cases"] = sortedKeys.map(date => activeCases[date] - activeCases[date - 24 * 60 * 60 * 1000]).slice(0,-1)
357 |
358 | // Add Last Update of JHU Data to Dictionary
359 | let lastJHUDataUpdate = treatAsUTC(new Date(parseInt(sortedKeys[0]))).toISOString()
360 | globalCaseData[country]["last_updated_date"] = lastJHUDataUpdate
361 | files.writeString(cachePath, JSON.stringify(globalCaseData[country]))
362 | }
363 | } catch (error) {
364 | console.error(error)
365 | if (cacheExists) {
366 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Loading new Data failed, using cached as fallback") }
367 | globalCaseData[country] = JSON.parse(files.readString(cachePath))
368 | } else {
369 | if (logCacheUpdateStatus) { console.log(country + " Case Data: Loading new Data failed and no Cache found") }
370 | }
371 | }
372 | }
373 |
374 | ////////////////////////////////////////////////
375 | // Date Calculation ////////////////////////////
376 | ////////////////////////////////////////////////
377 | // --> see stackoverflow.com/a/11252167/6333824
378 |
379 | function treatAsUTC(date) {
380 | var result = new Date(date)
381 | result.setMinutes(result.getMinutes() - result.getTimezoneOffset())
382 | return result
383 | }
384 |
385 | function daysBetween(startDate, endDate) {
386 | var millisecondsPerDay = 24 * 60 * 60 * 1000
387 | return Math.round((treatAsUTC(endDate) - treatAsUTC(startDate)) / millisecondsPerDay)
388 | }
389 |
390 | ////////////////////////////////////////////////
391 | // Debug ///////////////////////////////////////
392 | ////////////////////////////////////////////////
393 |
394 | function printCache() {
395 | if (logCache) {
396 | console.log("\n\n**Global Vaccination Data**\n")
397 | console.log(JSON.stringify(vaccinationData, null, 2))
398 | console.log("\n\n**Global Cases Data**\n")
399 | console.log(JSON.stringify(globalCaseData, null, 2))
400 | }
401 | }
402 |
403 |
404 |
405 |
406 | ////////////////////////////////////////////////
407 | // Author: Benno Kress /////////////////////////
408 | // Original: Benno Kress ///////////////////////
409 | // github.com/bennokress/Scriptable-Scripts ////
410 | // Please copy every line! /////////////////////
411 | ////////////////////////////////////////////////
--------------------------------------------------------------------------------
/COVID-19 Global Vaccination.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: deep-green; icon-glyph: syringe;
4 |
5 | ////////////////////////////////////////////////
6 | // Debug ///////////////////////////////////////
7 | ////////////////////////////////////////////////
8 | let debug = false
9 |
10 | // Fine tune Debug Mode by modifying specific variables below
11 | var logCache = true
12 | var logCacheUpdateStatus = true
13 | var logURLs = true
14 | var temporaryLogging = true // if (temporaryLogging) { console.log("") }
15 |
16 | ////////////////////////////////////////////////
17 | // Configuration ///////////////////////////////
18 | ////////////////////////////////////////////////
19 | let cacheInvalidationInMinutes = 60
20 |
21 | let smallWidgetWidth = 121
22 | let padding = 14
23 | let barWidth = smallWidgetWidth - 2 * padding
24 | let barHeight = 3
25 |
26 | let showFirstAndSecondVaccinationOnProgressBar = true
27 |
28 | let country = {
29 | germany: "DEU",
30 | canada: "CAN",
31 | usa: "USA"
32 | }
33 |
34 | let flag = {
35 | "DEU": "🇩🇪",
36 | "CAN": "🇨🇦",
37 | "USA": "🇺🇸"
38 | }
39 |
40 | ////////////////////////////////////////////////
41 | // Disable Debug Logs in Production ////////////
42 | ////////////////////////////////////////////////
43 |
44 | if (!debug) {
45 | logCache = false
46 | logCacheUpdateStatus = false
47 | logURLs = false
48 | temporaryLogging = false
49 | }
50 |
51 | ////////////////////////////////////////////////
52 | // Data ////////////////////////////////////////
53 | ////////////////////////////////////////////////
54 | let today = new Date()
55 |
56 | let formatter = new DateFormatter()
57 | formatter.locale = "en"
58 | formatter.dateFormat = "MMM d"
59 |
60 | // Vaccination Data ////////////////////////////
61 | let vaccinationResponseMemoryCache
62 | let vaccinationData = {}
63 |
64 | await loadVaccinationData(country.germany)
65 | await loadVaccinationData(country.canada)
66 | await loadVaccinationData(country.usa)
67 |
68 | ////////////////////////////////////////////////
69 | // Debug Execution - DO NOT MODIFY /////////////
70 | ////////////////////////////////////////////////
71 |
72 | printCache()
73 |
74 | ////////////////////////////////////////////////
75 | // Widget //////////////////////////////////////
76 | ////////////////////////////////////////////////
77 | let widget = new ListWidget()
78 | widget.setPadding(padding, padding, padding, padding)
79 | await createWidget()
80 |
81 | ////////////////////////////////////////////////
82 | // Script //////////////////////////////////////
83 | ////////////////////////////////////////////////
84 | Script.setWidget(widget)
85 | Script.complete()
86 | if (config.runsInApp) {
87 | widget.presentSmall()
88 | }
89 |
90 | ////////////////////////////////////////////////
91 | // Widget Creation /////////////////////////////
92 | ////////////////////////////////////////////////
93 | async function createWidget() {
94 | let canvas = widget.addStack()
95 | canvas.layoutVertically()
96 | displayTitle(canvas)
97 | canvas.addSpacer()
98 | displayContent(canvas)
99 | canvas.addSpacer()
100 | displayFooter(canvas)
101 | }
102 |
103 | // Title ///////////////////////////////////////
104 | function displayTitle(canvas) {
105 | let title = canvas.addText("Vaccination".toUpperCase())
106 | title.font = Font.semiboldRoundedSystemFont(13)
107 | title.textColor = Color.dynamic(Color.darkGray(), Color.lightGray())
108 | }
109 |
110 | // Content /////////////////////////////////////
111 | function displayContent(canvas) {
112 | displayCountry(canvas, country.germany)
113 | canvas.addSpacer()
114 | displayCountry(canvas, country.canada)
115 | canvas.addSpacer()
116 | displayCountry(canvas, country.usa)
117 | }
118 |
119 | // Content Row /////////////////////////////////
120 | function displayCountry(canvas, country) {
121 | displayInformation(canvas, country)
122 | canvas.addSpacer(2)
123 | displayProgressBar(canvas, country)
124 | }
125 |
126 | // Country Data ////////////////////////////////
127 | function displayInformation(canvas, country) {
128 | let informationContainer = canvas.addStack()
129 | informationContainer.layoutHorizontally()
130 | displayFlag(informationContainer, country)
131 | informationContainer.addSpacer()
132 | displayNewVaccinations(informationContainer, country)
133 | displayPercentage(informationContainer, country)
134 | }
135 |
136 | // Flag ////////////////////////////////////////
137 | function displayFlag(canvas, country) {
138 | let flagLabel = canvas.addText(flag[country])
139 | flagLabel.font = Font.regularRoundedSystemFont(13)
140 | }
141 |
142 | // New Vaccinations ////////////////////////////
143 | function displayNewVaccinations(canvas, country) {
144 | let smallLabelContainer = canvas.addStack()
145 | smallLabelContainer.layoutVertically()
146 | smallLabelContainer.addSpacer(3)
147 | let numberFormatter = new Intl.NumberFormat('en', { style: 'decimal', useGrouping: true })
148 | let newVaccinations = vaccinationData[country].new_vaccinations
149 | let vaccinationLabel = smallLabelContainer.addText(" + " + numberFormatter.format(newVaccinations).replaceAll(",", "."))
150 | vaccinationLabel.textColor = Color.dynamic(Color.darkGray(), Color.lightGray())
151 | vaccinationLabel.font = Font.regularRoundedSystemFont(8)
152 | }
153 |
154 | // Total Vaccination Percentage ////////////////
155 | function displayPercentage(canvas, country) {
156 | let percentageContainer = canvas.addStack()
157 | percentageContainer.size = new Size(50, 0)
158 | percentageContainer.layoutHorizontally()
159 | percentageContainer.addSpacer()
160 | let vaccinationPercentage = vaccinationData[country].people_fully_vaccinated_per_hundred
161 | let percentageLabel = percentageContainer.addText(vaccinationPercentage.toFixed(1) + "%")
162 | percentageLabel.font = Font.mediumRoundedSystemFont(13)
163 | percentageLabel.minimumScaleFactor = 0.8
164 | percentageLabel.lineLimit = 1
165 | }
166 |
167 | // Vaccination Progress Bar ////////////////////
168 | function displayProgressBar(canvas, country) {
169 | let firstVaccinationPercentage = vaccinationData[country].people_vaccinated_per_hundred
170 | let vaccinationPercentage = vaccinationData[country].people_fully_vaccinated_per_hundred
171 | let progressBar = canvas.addImage(drawProgressBar(firstVaccinationPercentage, vaccinationPercentage))
172 | progressBar.cornerRadius = barHeight / 2
173 | }
174 |
175 | // Progress Bar Creation ///////////////////////
176 | function drawProgressBar(firstVaccinationPercentage, fullVaccinationPercentage) {
177 | // Total Vaccination Target in Percent
178 | let target = {
179 | good: 60,
180 | perfect: 70
181 | }
182 |
183 | // Drawing Canvas
184 | let canvas = new DrawContext()
185 | canvas.size = new Size(barWidth, barHeight)
186 | canvas.opaque = false
187 | canvas.respectScreenScale = true
188 |
189 | // Bar Container
190 | canvas.setFillColor(Color.dynamic(Color.darkGray(), Color.lightGray()))
191 | let bar = new Path()
192 | let backgroundRect = new Rect(0, 0, barWidth, barHeight)
193 | bar.addRect(backgroundRect)
194 | canvas.addPath(bar)
195 | canvas.fillPath()
196 |
197 | if (showFirstAndSecondVaccinationOnProgressBar) {
198 | // Progress Bar Color for first vaccination
199 | let firstVaccinationColor = Color.dynamic(Color.lightGray(), Color.darkGray())
200 |
201 | // First Vaccination Progress Bar
202 | canvas.setFillColor(firstVaccinationColor)
203 | let firstVaccinationProgress = new Path()
204 | let firstVaccinationQuotient = firstVaccinationPercentage / 100
205 | let firstVaccinationProgressWidth = Math.min(barWidth, barWidth * firstVaccinationQuotient) // Makes breaking the scale impossible although barWidth * quotient should suffice
206 | firstVaccinationProgress.addRect(new Rect(0, 0, firstVaccinationProgressWidth, barHeight))
207 | canvas.addPath(firstVaccinationProgress)
208 | canvas.fillPath()
209 | }
210 |
211 | // Progress Bar Color depending on vaccination status
212 | let color
213 | if (fullVaccinationPercentage >= target.perfect) {
214 | color = Color.green()
215 | } else if (fullVaccinationPercentage >= target.good) {
216 | color = Color.orange()
217 | } else {
218 | color = Color.red()
219 | }
220 |
221 | // Progress Bar
222 | canvas.setFillColor(color)
223 | let progress = new Path()
224 | let quotient = fullVaccinationPercentage / 100
225 | let progressWidth = Math.min(barWidth, barWidth * quotient) // Makes breaking the scale impossible although barWidth * quotient should suffice
226 | progress.addRect(new Rect(0, 0, progressWidth, barHeight))
227 | canvas.addPath(progress)
228 | canvas.fillPath()
229 |
230 | return canvas.getImage()
231 | }
232 |
233 | // Footer //////////////////////////////////////
234 | function displayFooter(canvas) {
235 | let updateDictionary = getUpdateDictionary()
236 |
237 | let sortedUpdates = Object.keys(updateDictionary).sort().reverse() // newest first
238 | let updateInfoArray = sortedUpdates.map(k => relativeTimestamp(new Date(k)))
239 | let updateInfoText = updateInfoArray.join(', ')
240 |
241 | let lastUpdateLabel = canvas.addText("Last Update: " + updateInfoText)
242 | lastUpdateLabel.font = Font.mediumRoundedSystemFont(10)
243 | lastUpdateLabel.textColor = Color.dynamic(Color.lightGray(), Color.darkGray())
244 | }
245 |
246 | function getUpdateDictionary() {
247 | let updateFormatter = new DateFormatter()
248 | updateFormatter.locale = "en"
249 | updateFormatter.dateFormat = "yyyy-MM-dd"
250 |
251 | let updateDict = {}
252 |
253 | let owidUpdates = [getLastOWIDUpdate(country.germany), getLastOWIDUpdate(country.canada), getLastOWIDUpdate(country.usa)]
254 | let oldestGlobalVaccinationsUpdate = owidUpdates.sort().reverse()[0]
255 | if (!updateDict[updateFormatter.string(oldestGlobalVaccinationsUpdate)]) {
256 | updateDict[updateFormatter.string(oldestGlobalVaccinationsUpdate)] = []
257 | }
258 | updateDict[updateFormatter.string(oldestGlobalVaccinationsUpdate)].push("OWID")
259 |
260 | return updateDict
261 | }
262 |
263 | ////////////////////////////////////////////////
264 | // Calculations ////////////////////////////////
265 | ////////////////////////////////////////////////
266 | function getLastOWIDUpdate(country) {
267 | let lastUpdate = new Date(vaccinationData[country].last_updated_date)
268 | // Since vaccinations are always reported at the end of the day, we add 1 day here (data from yesterday = last update today)
269 | lastUpdate.setDate(lastUpdate.getDate() + 1)
270 | // If data gets reported before midnight, the last update should still be today instead of tomorrow.
271 | return lastUpdate.getTime() > today.getTime() ? today : lastUpdate
272 | }
273 |
274 | function relativeTimestamp(date) {
275 | let yesterday = new Date()
276 | yesterday.setDate(today.getDate() - 1)
277 |
278 | switch (formatter.string(date)) {
279 | case formatter.string(today):
280 | return "Today"
281 | case formatter.string(yesterday):
282 | return "Yesterday"
283 | default:
284 | return formatter.string(date)
285 | }
286 | }
287 |
288 | function sum(a, b) {
289 | return a + b
290 | }
291 |
292 | ////////////////////////////////////////////////
293 | // Networking //////////////////////////////////
294 | ////////////////////////////////////////////////
295 | async function loadVaccinationData(country) {
296 | let files = FileManager.local()
297 | let cacheName = debug ? ("debug-api-cache-ourworldindata-latest-" + country) : ("api-cache-ourworldindata-latest-" + country)
298 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
299 | let cacheExists = files.fileExists(cachePath)
300 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
301 |
302 | try {
303 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
304 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
305 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Using cached Data") }
306 | vaccinationData[country] = JSON.parse(files.readString(cachePath))
307 | } else {
308 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Updating cached Data") }
309 | if (logURLs) { console.log("\nURL: Vaccination " + country) }
310 | if (logURLs) { console.log('https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/latest/owid-covid-latest.json') }
311 | if (vaccinationResponseMemoryCache) {
312 | vaccinationData[country] = vaccinationResponseMemoryCache[country]
313 | } else {
314 | let response = await new Request('https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/latest/owid-covid-latest.json').loadJSON()
315 | vaccinationData[country] = response[country]
316 | }
317 | files.writeString(cachePath, JSON.stringify(vaccinationData[country]))
318 | }
319 | } catch (error) {
320 | console.error(error)
321 | if (cacheExists) {
322 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Loading new Data failed, using cached as fallback") }
323 | vaccinationData[country] = JSON.parse(files.readString(cachePath))
324 | } else {
325 | if (logCacheUpdateStatus) { console.log(country + " Vaccination Data: Loading new Data failed and no Cache found") }
326 | }
327 | }
328 | }
329 |
330 | ////////////////////////////////////////////////
331 | // Date Calculation ////////////////////////////
332 | ////////////////////////////////////////////////
333 | // --> see stackoverflow.com/a/11252167/6333824
334 |
335 | function treatAsUTC(date) {
336 | var result = new Date(date)
337 | result.setMinutes(result.getMinutes() - result.getTimezoneOffset())
338 | return result
339 | }
340 |
341 | function daysBetween(startDate, endDate) {
342 | var millisecondsPerDay = 24 * 60 * 60 * 1000
343 | return Math.round((treatAsUTC(endDate) - treatAsUTC(startDate)) / millisecondsPerDay)
344 | }
345 |
346 | ////////////////////////////////////////////////
347 | // Debug ///////////////////////////////////////
348 | ////////////////////////////////////////////////
349 |
350 | function printCache() {
351 | if (logCache) {
352 | console.log("\n\n**Global Vaccination Data**\n")
353 | console.log(JSON.stringify(vaccinationData, null, 2))
354 | }
355 | }
356 |
357 |
358 |
359 |
360 | ////////////////////////////////////////////////
361 | // Author: Benno Kress /////////////////////////
362 | // Original: Benno Kress ///////////////////////
363 | // github.com/bennokress/Scriptable-Scripts ////
364 | // Please copy every line! /////////////////////
365 | ////////////////////////////////////////////////
--------------------------------------------------------------------------------
/COVID-19 Local Incidence.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: deep-green; icon-glyph: chart-line;
4 |
5 | ////////////////////////////////////////////////
6 | // Debug ///////////////////////////////////////
7 | ////////////////////////////////////////////////
8 | let debug = false
9 |
10 | // Fine tune Debug Mode by modifying specific variables below
11 | var logCache = true
12 | var logCacheUpdateStatus = true
13 | var logURLs = true
14 | var temporaryLogging = true // if (temporaryLogging) { console.log("") }
15 |
16 | ////////////////////////////////////////////////
17 | // Configuration ///////////////////////////////
18 | ////////////////////////////////////////////////
19 | let cacheInvalidationInMinutes = 60
20 | let padding = 14
21 |
22 | let formatter = new DateFormatter()
23 | formatter.locale = "en"
24 | formatter.dateFormat = "MMM d"
25 |
26 | // Local Configuration /////////////////////////
27 | let location = {
28 | kissing: "FDB",
29 | augsburg: "A",
30 | munich: "M",
31 | freilassing: "BGL"
32 | }
33 |
34 | let coordinates = {
35 | "FDB": "48.294,10.969",
36 | "A": "48.366,10.898",
37 | "M": "48.135,11.613",
38 | "BGL": "47.835,12.970"
39 | }
40 |
41 | let name = {
42 | "FDB": "Kissing",
43 | "A": "Augsburg",
44 | "M": "München",
45 | "BGL": "Freilassing"
46 | }
47 |
48 | ////////////////////////////////////////////////
49 | // Disable Debug Logs in Production ////////////
50 | ////////////////////////////////////////////////
51 |
52 | if (!debug) {
53 | logCache = false
54 | logCacheUpdateStatus = false
55 | logURLs = false
56 | temporaryLogging = false
57 | }
58 |
59 | ////////////////////////////////////////////////
60 | // Data ////////////////////////////////////////
61 | ////////////////////////////////////////////////
62 | let today = new Date()
63 |
64 | // Local Case Data /////////////////////////////
65 | let localCaseData = {}
66 | let localHistoryData = {}
67 |
68 | await loadLocalCaseData(location.kissing)
69 | await loadLocalCaseData(location.augsburg)
70 | await loadLocalCaseData(location.munich)
71 | await loadLocalCaseData(location.freilassing)
72 |
73 | await loadLocalHistoryData(location.kissing)
74 | await loadLocalHistoryData(location.augsburg)
75 | await loadLocalHistoryData(location.munich)
76 | await loadLocalHistoryData(location.freilassing)
77 |
78 | ////////////////////////////////////////////////
79 | // Debug Execution - DO NOT MODIFY /////////////
80 | ////////////////////////////////////////////////
81 |
82 | printCache()
83 |
84 | ////////////////////////////////////////////////
85 | // Widget //////////////////////////////////////
86 | ////////////////////////////////////////////////
87 | let widget = new ListWidget()
88 | widget.setPadding(padding, padding, padding, padding)
89 | await createWidget()
90 |
91 | ////////////////////////////////////////////////
92 | // Script //////////////////////////////////////
93 | ////////////////////////////////////////////////
94 | Script.setWidget(widget)
95 | Script.complete()
96 | if (config.runsInApp) {
97 | widget.presentSmall()
98 | }
99 |
100 | ////////////////////////////////////////////////
101 | // Widget Creation /////////////////////////////
102 | ////////////////////////////////////////////////
103 | async function createWidget() {
104 | let canvas = widget.addStack()
105 | canvas.layoutVertically()
106 | displayTitle(canvas)
107 | canvas.addSpacer()
108 | displayContent(canvas)
109 | canvas.addSpacer()
110 | displayFooter(canvas)
111 | }
112 |
113 | // Title ///////////////////////////////////////
114 | function displayTitle(canvas) {
115 | let title = canvas.addText("RKI Incidence".toUpperCase())
116 | title.font = Font.semiboldRoundedSystemFont(13)
117 | title.textColor = Color.dynamic(Color.darkGray(), Color.lightGray())
118 | }
119 |
120 | // Content /////////////////////////////////////
121 | function displayContent(canvas) {
122 | displayPrimaryRegion(canvas, location.kissing)
123 | canvas.addSpacer(2)
124 | displaySecondaryRegionContainer(canvas, location.augsburg, location.freilassing)
125 | }
126 |
127 | // Primary Region //////////////////////////////
128 | function displayPrimaryRegion(canvas, location) {
129 | let incidenceValue = localCaseData[location].cases7_per_100k.toFixed(1)
130 |
131 | let locationLabel = canvas.addText(name[location])
132 | locationLabel.font = Font.mediumRoundedSystemFont(13)
133 |
134 | let incidenceContainer = canvas.addStack()
135 | incidenceContainer.layoutHorizontally()
136 |
137 | incidenceContainer.addSpacer(10)
138 | let tendencyLabel = incidenceContainer.addText(getLocalTendency(location))
139 | tendencyLabel.font = Font.mediumRoundedSystemFont(30)
140 | tendencyLabel.textColor = incidenceColor(incidenceValue)
141 | incidenceContainer.addSpacer()
142 | let incidenceLabel = incidenceContainer.addText(incidenceValue)
143 | incidenceLabel.font = Font.mediumRoundedSystemFont(30)
144 | incidenceLabel.textColor = incidenceColor(incidenceValue)
145 | }
146 |
147 | // Secondary Region Container //////////////////
148 | function displaySecondaryRegionContainer(canvas, location1, location2) {
149 | let container = canvas.addStack()
150 | displaySecondaryRegion(container, location1)
151 | container.addSpacer()
152 | displaySecondaryRegion(container, location2)
153 | }
154 |
155 | // Secondary Region ////////////////////////////
156 | function displaySecondaryRegion(canvas, location) {
157 | let container = canvas.addStack()
158 | container.layoutVertically()
159 | let locationLabel = container.addText(name[location])
160 | locationLabel.font = Font.mediumRoundedSystemFont(10)
161 | locationLabel.textColor = Color.dynamic(Color.darkGray(), Color.lightGray())
162 | container.addSpacer(2)
163 | let incidenceValue = localCaseData[location].cases7_per_100k.toFixed(1)
164 | let incidenceLabel = container.addText(incidenceValue + " " + getLocalTendency(location))
165 | incidenceLabel.font = Font.semiboldRoundedSystemFont(10)
166 | incidenceLabel.textColor = incidenceColor(incidenceValue)
167 | }
168 |
169 | // Footer //////////////////////////////////////
170 | function displayFooter(canvas) {
171 | let updateDictionary = getUpdateDictionary()
172 |
173 | let sortedUpdates = Object.keys(updateDictionary).sort().reverse() // newest first
174 | let updateInfoArray = sortedUpdates.map(k => relativeTimestamp(new Date(k)))
175 | let updateInfoText = updateInfoArray.join(', ')
176 |
177 | let lastUpdateLabel = canvas.addText("Last Update: " + updateInfoText)
178 | lastUpdateLabel.font = Font.mediumRoundedSystemFont(10)
179 | lastUpdateLabel.textColor = Color.dynamic(Color.lightGray(), Color.darkGray())
180 | }
181 |
182 | function getUpdateDictionary() {
183 | let updateFormatter = new DateFormatter()
184 | updateFormatter.locale = "en"
185 | updateFormatter.dateFormat = "yyyy-MM-dd"
186 |
187 | let updateDict = {}
188 |
189 | let rkiUpdates = [getLastRKIUpdate(location.kissing), getLastRKIUpdate(location.augsburg), getLastRKIUpdate(location.munich), getLastRKIUpdate(location.freilassing)]
190 | let oldestLocalCasesUpdate = rkiUpdates.sort().reverse()[0]
191 | if (!updateDict[updateFormatter.string(oldestLocalCasesUpdate)]) {
192 | updateDict[updateFormatter.string(oldestLocalCasesUpdate)] = []
193 | }
194 | updateDict[updateFormatter.string(oldestLocalCasesUpdate)].push("RKI")
195 |
196 | return updateDict
197 | }
198 |
199 | ////////////////////////////////////////////////
200 | // Calculations ////////////////////////////////
201 | ////////////////////////////////////////////////
202 | function incidenceColor(incidenceValue) {
203 | let color
204 | if (incidenceValue < 35) {
205 | color = Color.green()
206 | } else if (incidenceValue < 50) {
207 | color = Color.yellow()
208 | } else if (incidenceValue < 100) {
209 | color = Color.dynamic(new Color("e74300"), new Color("e64400"))
210 | } else {
211 | color = Color.dynamic(new Color("9e000a"), new Color("b61116")) // #ce2222
212 | }
213 | return color
214 | }
215 |
216 | function getLocal7DayIncidence(location, requestedDate) {
217 | // Start Index = Date Difference to Today (defaults to today)
218 | let startIndex = requestedDate ? daysBetween(requestedDate, today) : 0
219 |
220 | // Sum up daily new cases for the 7 days from the requested date (or today if none specified)
221 | let newWeeklyCases = localHistoryData[location].cases.slice(startIndex, startIndex + 7).reduce(sum, 0)
222 | let population = localCaseData[location].EWZ
223 | return 100_000 * (newWeeklyCases / population)
224 | }
225 |
226 | function getLocalTendency(location, accuracy, longTimeAccuracy) {
227 | let tendencyIndicator = {
228 | falling: "↘",
229 | steady: "→",
230 | rising: "↗"
231 | }
232 |
233 | let yesterday = new Date()
234 | yesterday.setDate(today.getDate() - 1)
235 |
236 | let lastWeek = new Date()
237 | lastWeek.setDate(today.getDate() - 7)
238 |
239 | let incidenceToday = getLocal7DayIncidence(location, today)
240 | let incidenceYesterday = getLocal7DayIncidence(location, yesterday)
241 | let incidenceLastWeek = getLocal7DayIncidence(location, lastWeek)
242 | let incidenceDifference = incidenceToday - incidenceYesterday
243 | let longTermIncidenceDifference = incidenceToday - incidenceLastWeek
244 |
245 | // The short term tendency is deemed steady if it differs less than the requested accuracy (default: 5)
246 | let steadyRange = accuracy ?? 5
247 | // The long term tendency is deemed steady if it differs less than the requested long term accuracy (default: 10)
248 | let longTermSteadyRange = longTimeAccuracy ?? 10
249 |
250 | // The short term tendency is the primary return value. If short term is steady, the long term tendency will be returned, if it is similar to the short term tendency.
251 | let tendency
252 | if (incidenceDifference < -steadyRange) {
253 | tendency = tendencyIndicator.falling
254 | } else if (incidenceDifference > steadyRange) {
255 | tendency = tendencyIndicator.rising
256 | } else if (incidenceDifference <= 0 && longTermIncidenceDifference < -longTermSteadyRange) {
257 | tendency = tendencyIndicator.falling
258 | } else if (incidenceDifference >= 0 && longTermIncidenceDifference > longTermSteadyRange) {
259 | tendency = tendencyIndicator.rising
260 | } else {
261 | tendency = tendencyIndicator.steady
262 | }
263 |
264 | return tendency
265 | }
266 |
267 | function getCoordinates(location) {
268 | let coordinatesString = coordinates[location]
269 | let splitCoordinates = coordinatesString.split(",").map(parseFloat)
270 | return { latitude: splitCoordinates[0], longitude: splitCoordinates[1] }
271 | }
272 |
273 | function getRKIDateString(addDays) {
274 | addDays = addDays || 0
275 | return new Date(Date.now() + addDays * 24 * 60 * 60 * 1000).toISOString().substring(0, 10)
276 | }
277 |
278 | function getLastRKIUpdate(location) {
279 | let lastUpdate = new Date(localHistoryData[location].last_updated_date)
280 | // Since incidence is always determined by looking at cases from the previous day, we add 1 day here.
281 | lastUpdate.setDate(lastUpdate.getDate() + 1)
282 | // If data gets reported before midnight, the last update should still be today instead of tomorrow.
283 | return lastUpdate.getTime() > today.getTime() ? today : lastUpdate
284 | }
285 |
286 | function relativeTimestamp(date) {
287 | let yesterday = new Date()
288 | yesterday.setDate(today.getDate() - 1)
289 |
290 | switch (formatter.string(date)) {
291 | case formatter.string(today):
292 | return "Today"
293 | case formatter.string(yesterday):
294 | return "Yesterday"
295 | default:
296 | return formatter.string(date)
297 | }
298 | }
299 |
300 | function sum(a, b) {
301 | return a + b
302 | }
303 |
304 | ////////////////////////////////////////////////
305 | // Networking //////////////////////////////////
306 | ////////////////////////////////////////////////
307 | async function loadLocalCaseData(location) {
308 | let files = FileManager.local()
309 | let cacheName = debug ? ("debug-api-cache-local-cases-" + location) : ("api-cache-local-cases-" + location)
310 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
311 | let cacheExists = files.fileExists(cachePath)
312 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
313 |
314 | try {
315 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
316 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
317 | if (logCacheUpdateStatus) { console.log(location + " Case Data: Using cached Data") }
318 | localCaseData[location] = JSON.parse(files.readString(cachePath))
319 | } else {
320 | if (logCacheUpdateStatus) { console.log(location + " Case Data: Updating cached Data") }
321 | let coordinates = getCoordinates(location)
322 | if (logURLs) { console.log("\nURL: Cases " + name[location]) }
323 | if (logURLs) { console.log('https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=RS,GEN,cases7_per_100k,EWZ&geometry=' + coordinates.longitude.toFixed(3) + '%2C' + coordinates.latitude.toFixed(3) + '&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnGeometry=false&outSR=4326&f=json') }
324 | let response = await new Request('https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=RS,GEN,cases7_per_100k,EWZ&geometry=' + coordinates.longitude.toFixed(3) + '%2C' + coordinates.latitude.toFixed(3) + '&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnGeometry=false&outSR=4326&f=json').loadJSON()
325 | localCaseData[location] = response.features[0].attributes
326 | files.writeString(cachePath, JSON.stringify(localCaseData[location]))
327 | }
328 | } catch (error) {
329 | console.error(error)
330 | if (cacheExists) {
331 | if (logCacheUpdateStatus) { console.log(location + " Case Data: Loading new Data failed, using cached as fallback") }
332 | localCaseData[location] = JSON.parse(files.readString(cachePath))
333 | } else {
334 | if (logCacheUpdateStatus) { console.log(location + " Case Data: Loading new Data failed and no Cache found") }
335 | }
336 | }
337 | }
338 |
339 | async function loadLocalHistoryData(location) {
340 | let files = FileManager.local()
341 | let cacheName = debug ? ("debug-api-cache-local-history-" + location) : ("api-cache-local-history-" + location)
342 | let cachePath = files.joinPath(files.cacheDirectory(), cacheName)
343 | let cacheExists = files.fileExists(cachePath)
344 | let cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
345 |
346 | try {
347 | // Use Cache if available and last updated within specified `cacheInvalidationInMinutes
348 | if (!debug && cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheInvalidationInMinutes * 60 * 1000)) {
349 | if (logCacheUpdateStatus) { console.log(location + " History Data: Using cached Data") }
350 | localHistoryData[location] = JSON.parse(files.readString(cachePath))
351 | } else {
352 | if (logCacheUpdateStatus) { console.log(location + " History Data: Updating cached Data") }
353 | if (logURLs) { console.log("\nURL: History " + name[location]) }
354 | if (logURLs) { console.log('https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=IdLandkreis%20%3D%20%27' + localCaseData[location].RS + '%27%20AND%20Meldedatum%20%3E%3D%20TIMESTAMP%20%27' + getRKIDateString(-15) + '%2000%3A00%3A00%27%20AND%20Meldedatum%20%3C%3D%20TIMESTAMP%20%27' + getRKIDateString(1) + '%2000%3A00%3A00%27&outFields=Landkreis,Meldedatum,AnzahlFall&outSR=4326&f=json') }
355 | let response = await new Request('https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=IdLandkreis%20%3D%20%27' + localCaseData[location].RS + '%27%20AND%20Meldedatum%20%3E%3D%20TIMESTAMP%20%27' + getRKIDateString(-15) + '%2000%3A00%3A00%27%20AND%20Meldedatum%20%3C%3D%20TIMESTAMP%20%27' + getRKIDateString(1) + '%2000%3A00%3A00%27&outFields=Landkreis,Meldedatum,AnzahlFall&outSR=4326&f=json').loadJSON()
356 | // The response contains multiple entries per day. This sums them up and creates a new dictionary with each days new cases as values and the corresponding UNIX timestamp as keys.
357 | let aggregate = response.features.map(f => f.attributes).reduce((dict, feature) => {
358 | dict[feature["Meldedatum"]] = (dict[feature["Meldedatum"]]|0) + feature["AnzahlFall"]
359 | return dict
360 | }, {})
361 | let sortedKeys = Object.keys(aggregate).sort().reverse()
362 | // Local History Data is now being sorted by keys (Timestamps) and put into a sorted array (newest day first).
363 | localHistoryData[location] = {}
364 | localHistoryData[location]["cases"] = sortedKeys.map(k => aggregate[k])
365 |
366 | // Add Last Update of RKI Data to Dictionary
367 | let lastRKIDataUpdate = new Date(parseInt(sortedKeys[0])).toISOString()
368 | localHistoryData[location]["last_updated_date"] = lastRKIDataUpdate
369 | files.writeString(cachePath, JSON.stringify(localHistoryData[location]))
370 | }
371 | } catch (error) {
372 | console.error(error)
373 | if (cacheExists) {
374 | if (logCacheUpdateStatus) { console.log(location + " History Data: Loading new Data failed, using cached as fallback") }
375 | localHistoryData[location] = JSON.parse(files.readString(cachePath))
376 | } else {
377 | if (logCacheUpdateStatus) { console.log(location + " History Data: Loading new Data failed and no Cache found") }
378 | }
379 | }
380 | }
381 |
382 | ////////////////////////////////////////////////
383 | // Date Calculation ////////////////////////////
384 | ////////////////////////////////////////////////
385 | // --> see stackoverflow.com/a/11252167/6333824
386 |
387 | function treatAsUTC(date) {
388 | var result = new Date(date)
389 | result.setMinutes(result.getMinutes() - result.getTimezoneOffset())
390 | return result
391 | }
392 |
393 | function daysBetween(startDate, endDate) {
394 | var millisecondsPerDay = 24 * 60 * 60 * 1000
395 | return Math.round((treatAsUTC(endDate) - treatAsUTC(startDate)) / millisecondsPerDay)
396 | }
397 |
398 | ////////////////////////////////////////////////
399 | // Debug ///////////////////////////////////////
400 | ////////////////////////////////////////////////
401 |
402 | function printCache() {
403 | if (logCache) {
404 | console.log("\n\n**Local Cases Data**\n")
405 | console.log(JSON.stringify(localCaseData, null, 2))
406 | console.log("\n\n**Local History Data**\n")
407 | console.log(JSON.stringify(localHistoryData, null, 2))
408 | }
409 | }
410 |
411 |
412 |
413 |
414 | ////////////////////////////////////////////////
415 | // Author: Benno Kress /////////////////////////
416 | // Original: Benno Kress ///////////////////////
417 | // github.com/bennokress/Scriptable-Scripts ////
418 | // Please copy every line! /////////////////////
419 | ////////////////////////////////////////////////
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scriptable Scripts
2 |
3 | My Scriptable Creations in one place. Please let me know if you see possibilities for improvement. Since I'm no JavaScript developer, I'm sure not everything is perfect with my code …
4 |
5 | ## COVID-19 Global Vaccination
6 | Small Widget showing vaccination data for 3 countries. Data comes from the [Github Repo](https://github.com/owid/covid-19-data/tree/master/public/data) of [Our World in Data](https://ourworldindata.org/coronavirus-data-explorer?zoomToSelection=true&country=DEU~CAN~USA®ion=World&vaccinationsMetric=true&interval=total&aligned=true&perCapita=true&smoothing=0&pickerMetric=location&pickerSort=asc) and the widget displays the vaccination progress of the population from Germany, Canada and the US. Displayed is the progress in percent and as a progress bar as well as the total of new vaccinations in one day.
7 |
8 |
9 | ## COVID-19 Global Incidence
10 | Small Widget showing the 7-Day incidence data for 3 countries as used in Germany: the sum of all new cases in the last 7 days divided by population and multiplied by 100.000. The arrow indicates a significant tendency compared to the day before (difference > 5) or if the difference is less than 5 then the week before (stable indicator if the difference there is less then 10 or contrary to the tendency compared to the day before). Data comes from the [Johns Hopkins University](https://coronavirus.jhu.edu/region) via the [COVID-19 API](https://covid19-api.org/).
11 |
12 |
13 | ## COVID-19 Global Incidence & Vaccination
14 | This is a combination of my [Global Vaccination](https://github.com/bennokress/Scriptable-Scripts#covid-19-global-vaccination) and [Global Incidence](https://github.com/bennokress/Scriptable-Scripts#covid-19-global-incidence) widgets.
15 |
16 |
17 | ## COVID-19 Local Incidence (Germany)
18 | Small Widget showing the 7-Day incidence data for 3 regions (Landkreise). Data comes from the [API]() of the [Robert Koch Institut](https://experience.arcgis.com/experience/478220a4c454480e823b17327b2bf1d4).
19 |
20 |
21 | ## COVID-19 Dashboard
22 | Medium Widget showing a combination of my [Local Incidence](https://github.com/bennokress/Scriptable-Scripts#covid-19-local-incidence-germany), [Global Vaccination](https://github.com/bennokress/Scriptable-Scripts#covid-19-global-vaccination) and [Global Incidence](https://github.com/bennokress/Scriptable-Scripts#covid-19-global-incidence) widgets.
23 |
24 |
--------------------------------------------------------------------------------
/Screenshots/COVID-19 Dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bennokress/Scriptable-Scripts/4215596e608e4d8a9a4e3be79fa87afab27cd455/Screenshots/COVID-19 Dashboard.png
--------------------------------------------------------------------------------
/Screenshots/COVID-19 Global Incidence & Vaccination.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bennokress/Scriptable-Scripts/4215596e608e4d8a9a4e3be79fa87afab27cd455/Screenshots/COVID-19 Global Incidence & Vaccination.png
--------------------------------------------------------------------------------
/Screenshots/COVID-19 Global Incidence.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bennokress/Scriptable-Scripts/4215596e608e4d8a9a4e3be79fa87afab27cd455/Screenshots/COVID-19 Global Incidence.png
--------------------------------------------------------------------------------
/Screenshots/COVID-19 Global Vaccination.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bennokress/Scriptable-Scripts/4215596e608e4d8a9a4e3be79fa87afab27cd455/Screenshots/COVID-19 Global Vaccination.png
--------------------------------------------------------------------------------
/Screenshots/COVID-19 Local Incidence.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bennokress/Scriptable-Scripts/4215596e608e4d8a9a4e3be79fa87afab27cd455/Screenshots/COVID-19 Local Incidence.png
--------------------------------------------------------------------------------