diff --git a/scriptable/Impfdashboard.scriptable b/scriptable/Impfdashboard.scriptable index 84947c3..a6f3334 100644 --- a/scriptable/Impfdashboard.scriptable +++ b/scriptable/Impfdashboard.scriptable @@ -1 +1 @@ -{"always_run_in_app":false,"icon":{"color":"blue","glyph":"syringe"},"name":"Impfdashboard","script":"","share_sheet_inputs":[]} \ No newline at end of file +{"always_run_in_app":false,"icon":{"color":"blue","glyph":"syringe"},"name":"Impfdashboard","script":"// Impfdashboard Widget\n//\n// MIT License\n//\n// Copyright (c) 2021 Christian Lobach\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\nconst localizedStrings = {\n 'first.vaccinations': {\n de: 'Mindestens\\nErstgeimpfte',\n en: 'At least first\\nvaccinations',\n },\n 'cumulative.doses': {\n de: 'Verabreichte\\nImpfdosen',\n en: 'Administered\\ndoses',\n },\n 'fully.vaccinated': {\n de: 'Vollständig\\nGeimpfte',\n en: 'Fully\\nvaccinated',\n },\n\n};\n\nconst widget = new ListWidget();\nconst { locale, language } = Device;\nconst horizontalPadding = 12;\nconst verticalPadding = 8;\n\nconfig.widgetFamily ||= 'large';\n\nconst titleFont = Font.mediumSystemFont(config.widgetFamily == 'large' ? 12 : 10);\nconst valueFont = Font.mediumSystemFont(config.widgetFamily == 'large' ? 20 : 16);\n\nconst lightBlue = new Color('#9DCCE5');\nconst darkBlue = new Color('#3392C5');\n\nconst dateFormatter = new DateFormatter();\ndateFormatter.dateFormat = 'E';\n\nwidget.setPadding(verticalPadding, horizontalPadding, verticalPadding, horizontalPadding);\nwidget.url = 'https://impfdashboard.de';\n\n\nlet data = await loadData();\n\nconst headerStack = widget.addStack();\nconst header = headerStack.addText('💉 Impfdashboard'.toUpperCase());\nheader.font = Font.mediumSystemFont(10);\n\nwidget.addSpacer(4);\n\nbuildLayout(widget);\n\nScript.setWidget(widget);\nScript.complete();\nwidget.presentLarge();\n\nfunction buildLayout(widget) {\n const chart = createChart(data.dosesLastWeeks, config.widgetFamily);\n\n switch (config.widgetFamily) {\n case 'large':\n const largeOuterVStack = widget.addStack();\n largeOuterVStack.layoutVertically();\n largeOuterVStack.centerAlignContent();\n largeOuterVStack.addSpacer();\n const largeHStack = largeOuterVStack.addStack();\n largeHStack.setPadding(0, 16, 0, 16);\n createStack(largeHStack, data.firstVaccinations);\n largeHStack.addSpacer();\n createStack(largeHStack, data.dosesCumulative);\n largeHStack.addSpacer();\n createStack(largeHStack, data.fullVaccinations);\n largeOuterVStack.addSpacer();\n largeOuterVStack.addImage(chart).centerAlignImage();\n\n const largePadded = largeOuterVStack.addStack();\n largePadded.setPadding(16, 0, 16, 0);\n largePadded.addSpacer();\n createStack(largePadded, data.dosesToday, true, true);\n largePadded.addSpacer();\n\n break;\n case 'medium':\n const outerHStack = widget.addStack();\n\n const vStack = outerHStack.addStack();\n vStack.setPadding(verticalPadding, 0, verticalPadding, 0);\n vStack.layoutVertically();\n createStack(vStack, data.firstVaccinations);\n vStack.addSpacer(verticalPadding);\n createStack(vStack, data.fullVaccinations);\n outerHStack.addSpacer(horizontalPadding);\n const secondaryStack = outerHStack.addStack();\n secondaryStack.layoutVertically();\n\n secondaryStack.addImage(chart);\n secondaryStack.addSpacer();\n const mediumPadded = secondaryStack.addStack();\n mediumPadded.addSpacer();\n createStack(mediumPadded, data.dosesToday, true, true);\n mediumPadded.addSpacer();\n\n break;\n default:\n const outerVStack = widget.addStack();\n outerVStack.layoutVertically();\n outerVStack.centerAlignContent();\n const hStack = outerVStack.addStack();\n createStack(hStack, data.firstVaccinations);\n hStack.addSpacer();\n createStack(hStack, data.fullVaccinations);\n outerVStack.addImage(chart);\n\n const padded = outerVStack.addStack();\n padded.addSpacer();\n createStack(padded, data.dosesToday, true, true);\n padded.addSpacer();\n break;\n }\n}\n\nfunction createStack(superView, data, inverse = false, centerAlignText = false) {\n const vStack = superView.addStack();\n vStack.layoutVertically();\n if (centerAlignText) {\n vStack.centerAlignContent();\n }\n if (inverse == false) {\n const title = vStack.addText(data.title);\n title.font = titleFont;\n if (centerAlignText) {\n title.centerAlignText();\n }\n }\n const value = vStack.addText(data.stringValue);\n value.font = valueFont;\n value.textColor = darkBlue;\n if (centerAlignText) {\n value.centerAlignText();\n }\n if (inverse) {\n const title = vStack.addText(data.title);\n title.font = titleFont;\n if (centerAlignText) {\n title.centerAlignText();\n }\n }\n\n return vStack;\n}\n\nasync function loadData() {\n const url = 'https://github.com/DerLobi/impfdashboard-scriptable-widget/raw/main/data/data.json';\n const request = new Request(url);\n const records = await request.loadJSON();\n\n const lastRecord = records[records.length - 1];\n\n const data = {\n firstVaccinations: {\n title: localized('first.vaccinations'),\n stringValue: lastRecord.impf_quote_erst.toLocaleString(locale, {\n style: 'percent',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }),\n },\n fullVaccinations: {\n title: localized('fully.vaccinated'),\n stringValue: lastRecord.impf_quote_voll.toLocaleString(locale, {\n style: 'percent',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }),\n },\n dosesCumulative: {\n title: localized('cumulative.doses'),\n stringValue: new Intl.NumberFormat(locale, { notation: 'compact', compactDisplay: 'short', maximumFractionDigits: 1 }).format(lastRecord.dosen_kumulativ),\n },\n dosesToday: {\n title: new Date(lastRecord.date).toLocaleString(locale, {\n weekday: 'long',\n year: '2-digit',\n month: '2-digit',\n day: '2-digit',\n }),\n stringValue: `+${lastRecord.dosen_differenz_zum_vortag.toLocaleString()}`,\n },\n dosesLastWeeks: records.map((record) => ({\n date: record.date,\n amount: record.dosen_differenz_zum_vortag,\n })),\n };\n\n return data;\n}\n\nfunction createChart(data, widgetFamily) {\n let size = new Size(400, 120);\n let dataSeries = data;\n\n switch (widgetFamily) {\n case 'large':\n size = new Size(800, 400);\n break;\n case 'medium':\n size = new Size(640, 240);\n dataSeries = data.slice(data.length - 7, data.length);\n break;\n default:\n dataSeries = data.slice(data.length - 7, data.length);\n break;\n }\n\n const ctx = new DrawContext();\n ctx.opaque = false;\n ctx.respectScreenScale = true;\n ctx.size = size;\n\n const sorted = [...dataSeries];\n sorted.sort((lhs, rhs) => rhs.amount - lhs.amount);\n const maximum = sorted[0].amount;\n\n const textHeight = 20;\n const availableHeight = size.height - textHeight;\n const spacing = 4;\n const barWidth = (size.width - ((dataSeries.length - 1) * spacing)) / dataSeries.length;\n\n ctx.setFont(Font.mediumSystemFont(18));\n ctx.setTextColor(Color.gray());\n ctx.setTextAlignedCenter();\n\n for (i = 0; i < dataSeries.length; i++) {\n const day = dataSeries[i];\n const path = new Path();\n const x = (i * spacing + i * barWidth);\n const value = day.amount;\n const heightFactor = value / maximum;\n const barHeight = heightFactor * availableHeight;\n const rect = new Rect(x, size.height - barHeight, barWidth, barHeight);\n path.addRoundedRect(rect, 4, 4);\n ctx.addPath(path);\n\n if (i == dataSeries.length - 1) {\n ctx.setFillColor(darkBlue);\n } else {\n ctx.setFillColor(lightBlue);\n }\n\n ctx.fillPath();\n\n const textRect = new Rect(x, size.height - barHeight - textHeight - 2, barWidth, textHeight);\n const date = new Date(day.date);\n const formattedDate = dateFormatter.string(date);\n\n ctx.drawTextInRect(dateFormatter.string(date), textRect);\n }\n\n const image = ctx.getImage();\n\n return image;\n}\n\nfunction localized(key) {\n if (localizedStrings[key] == null) {\n return key;\n }\n return localizedStrings[key][language()] || localizedStrings[key].en;\n}\n","share_sheet_inputs":[]} \ No newline at end of file