Skip to content

Instantly share code, notes, and snippets.

@mikepayne02
Last active April 1, 2025 08:43
Show Gist options
  • Save mikepayne02/cd0c0b628560079b46c9dffce9157495 to your computer and use it in GitHub Desktop.
Save mikepayne02/cd0c0b628560079b46c9dffce9157495 to your computer and use it in GitHub Desktop.
iOS Grafana widgets using Scriptable and grafana-image-renderer

Grafana iOS widgets

Observability on your home screen

Panel snapshots are generated server-side with grafana-image-renderer. This script fetches the image and displays it in a widget. Home screen, lock screen, and apple watch widget types are supported. This is with the exception of the inline lock screen widget because there isn't enough vertical resolution to display anything useful.

Server Setup

  1. Install Docker
  2. Download docker-compose.yaml and place it in a folder
$ mkdir grafana && cd grafana && wget https://gist.githubusercontent.com/mikepayne02/cd0c0b628560079b46c9dffce9157495/raw/docker-compose.yml
  1. Bring up Grafana stack
$ docker compose up -d

Grafana Setup

  1. Create a service account in your Grafana instance.
  2. Create a dashboard to store the panels or use an existing one. For the rendered image to display seamlessly in the widget, turn on Transparent background under Panel Options. Clear out the title as well because one is displayed via the script using the system font.

Device Configuration

  1. Download Scriptable
  2. Long press on raw link for the Grafana.scriptable file below and select Download Linked File to save the script. Open it from the Safari downloads drawer and use the share sheet to import it into Scriptable.
  3. Click the share icon in the top right corner of any Grafana panel. You will be presented with a URL directly linking to the chosen panel. Use this information to update all the configuration variables at the top of the script.
  4. Find your corresponding device resolution on Apple Developer Docs and set the deviceSize variable. For most retina devices, divide the pixel dimensions by 3 to get pts. Forolder, non-retina devices, divide by 2.
  5. Add a widget to your home screen or lock screen, and select the script "Grafana"
  6. Fill in the Parameter field using this format:
<panelId>,<from>,<to>,<title>,<alignment: l|c|r>

The default config is 1,now-30m,now,Grafana,c

Dark/light mode

Unfortunately, Scriptable widgets do not support automatic dark/light mode switching outside of the native widget background and text colors. To change the color scheme, edit the darkMode variable in the configuration section of the script. Set it to true for dark mode and false for light mode.

services:
grafana:
container_name: grafana
image: grafana/grafana
restart: unless-stopped
volumes:
- grafana:/var/lib/grafana
environment:
GF_RENDERING_SERVER_URL: "http://renderer:8081/render"
GF_RENDERING_CALLBACK_URL: "http://grafana:3000/"
RENDERING_MODE: "reusable"
GF_LOG_FILTERS: "rendering:debug"
ENABLE_METRICS: true
renderer:
container_name: grafana_image_renderer
image: grafana/grafana-image-renderer:latest
depends_on:
- grafana
environment:
BROWSER_TZ: "${TZ}"
volumes:
grafana:
{
"always_run_in_app" : false,
"icon" : {
"color" : "orange",
"glyph" : "chart-area"
},
"name" : "Grafana",
"script" : "\/\/ Configuration\nconst url=\"http:\/\/grafana.example.com\",\ntoken=\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\norgId=1,\ndashboardName=\"widgets\",\ndashboardId=\"xxxxxxxxxxxxxx\",\ntz=\"America\/Chicago\",\ndarkMode=true,\ndeviceSize=\"393x852\";\n\nif(config.runsInWidget)return;\nconst e=config.runsInAccessoryWidget,n={small:\"s\",medium:\"m\",large:\"l\",extraLarge:\"x\",accessoryRectangular:\"r\",accessoryCircular:\"c\"}[config.widgetFamily]||\"m\",i=args.widgetParameter?.split(\",\")||[],w=i[0]||\"1\",S=i[1]||\"now-30m\",z=i[2]||\"now\",x=i[3]||\"Grafana\",t=i[4]||\"c\",c=Font.mediumSystemFont(12),r={s:new Size(474,474),m:new Size(500,210),l:new Size(500,500),x:new Size(500,500),r:new Size(480,216),c:new Size(216,216)},a={\"430x932\":{s:new Size(170,170),m:new Size(364,170),l:new Size(364,382),r:new Size(172,76),c:new Size(76,76)},\"428x926\":{s:new Size(170,170),m:new Size(364,170),l:new Size(364,382),r:new Size(172,76),c:new Size(76,76)},\"414x896\":{s:new Size(169,169),m:new Size(360,169),l:new Size(360,379),r:new Size(160,72),c:new Size(76,76)},\"414x736\":{s:new Size(159,159),m:new Size(348,157),l:new Size(348,357),r:new Size(170,76),c:new Size(76,76)},\"393x852\":{s:new Size(158,158),m:new Size(338,158),l:new Size(338,354),r:new Size(160,72),c:new Size(72,72)},\"390x844\":{s:new Size(158,158),m:new Size(338,158),l:new Size(338,354),r:new Size(160,72),c:new Size(72,72)},\"375x812\":{s:new Size(155,155),m:new Size(329,155),l:new Size(329,345),r:new Size(157,72),c:new Size(72,72)},\"375x667\":{s:new Size(148,148),m:new Size(321,148),l:new Size(321,324),r:new Size(153,68),c:new Size(68,68)},\"360x780\":{s:new Size(155,155),m:new Size(329,155),l:new Size(329,345),r:new Size(157,72),c:new Size(72,72)},\"320x568\":{s:new Size(141,141),m:new Size(292,141),l:new Size(292,311)},\"768x1024\":{s:new Size(141,141),m:new Size(305.5,141),l:new Size(305.5,305.5),x:new Size(634.5,305.5)},\"744x1133\":{s:new Size(141,141),m:new Size(305.5,141),l:new Size(305.5,305.5),x:new Size(634.5,305.5)},\"810x1080\":{s:new Size(146,146),m:new Size(320.5,146),l:new Size(320.5,320.5),x:new Size(669,320.5)},\"820x1180\":{s:new Size(155,155),m:new Size(342,155),l:new Size(342,342),x:new Size(715.5,342)},\"834x1112\":{s:new Size(150,150),m:new Size(327.5,150),l:new Size(327.5,327.5),x:new Size(682,327.5)},\"834x1194\":{s:new Size(155,155),m:new Size(342,155),l:new Size(342,342),x:new Size(715.5,342)},\"954x1373\":{s:new Size(162,162),m:new Size(350,162),l:new Size(350,350),x:new Size(726,350)},\"970x1389\":{s:new Size(162,162),m:new Size(350,162),l:new Size(350,350),x:new Size(726,350)},\"1024x1366\":{s:new Size(170,170),m:new Size(378.5,170),l:new Size(378.5,378.5),x:new Size(795,378.5)},\"1192x1590\":{s:new Size(188,188),m:new Size(412,188),l:new Size(412,412),x:new Size(860,412)},\"40mm\":{c:new Size(152,69.5)},\"41mm\":{c:new Size(165,72.5)},\"44mm\":{c:new Size(173,76.5)},\"45mm\":{c:new Size(184,80.5)},\"49mm\":{c:new Size(191,81.5)}},o=new ListWidget;function s(e,n){o.addSpacer(5);const i=o.addStack();i.size=new Size(d.width,0),i.setPadding(5,0,0,0),\"l\"==n&&i.addSpacer(15),\"r\"==n&&i.addSpacer(null);const w=i.addText(e);w.font=c,w.textColor=new Color(darkMode?\"#ccccdc\":\"#24292e\"),\"l\"==n&&i.addSpacer(null),\"r\"==n&&i.addSpacer(15)}o.setPadding(0,0,0,0);const d=a[deviceSize][n],l=r[n],m=e||darkMode?\"dark\":\"light\";o.backgroundColor=new Color(darkMode?\"#111216\":\"#F4F5F5\"),o.url=`${url}\/d\/${dashboardId}\/${dashboardName}?orgId=${orgId}&viewPanel=panel-${w}&from=${S}&to=${z}&kiosk`;const g=`${url}\/render\/d-solo\/${dashboardId}\/${dashboardName}?orgId=${orgId}&panelId=${w}&from=${S}&to=${z}&width=${l.width}&height=${l.height}&theme=${m}&tz=${tz}`,h=new Request(g);h.timeoutInterval=20,h.headers={Authorization:`Bearer ${token}`},await h.loadImage().then((i=>{e||s(x,t);const w=o.addStack();w.size=new Size(d.width,e?d.height:0);const S=w.addImage(i);\"r\"==n&&(S.cornerRadius=10),\"c\"==n&&(S.cornerRadius=100)})).catch((i=>{if(e){const e=o.addText(`${\"c\"==n?\"Error\":i}`);e.font=c,e.centerAlignText()}else{s(x,\"c\"),o.addSpacer(25);const e=o.addStack();e.size=new Size(d.width,30);const n=e.addText(i.toString());n.font=c,n.textColor=new Color(\"#FF0000\"),n.centerAlignText()}})),Script.setWidget(o),Script.complete();",
"share_sheet_inputs" : [
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment