├── .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 --------------------------------------------------------------------------------