Skip to content

Instantly share code, notes, and snippets.

@brainno722
Last active February 17, 2024 11:04
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save brainno722/8499ec890d1a1f6e76e89cbfcf322044 to your computer and use it in GitHub Desktop.
Save brainno722/8499ec890d1a1f6e76e89cbfcf322044 to your computer and use it in GitHub Desktop.
Concentric circle info widgets (Small)

Concentric Circle Info Widget

Instructions

  1. Install Scriptable for iOS
  2. Copy/Paste the code into a new file
  3. Adjust colors/font
  4. Add the script to a widget (medium)
  5. It should appear similar to the screenshots

Transparent/no background

To make a "transparent" background (simulating your current background image), follow these steps:

  1. Go to Automators.fm
  2. Download (copy/paste) the scripts into the Scriptables folder
  3. Run the No Background Config script
  4. Follow the instructions on the website, but basically, insert the code into your script, changing the position as necessary (for example, this screenshot is a medium top widget, so await nobg.getSlice('small-top-left')
  5. ???
  6. Profit!
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: magic;
// Concentric circle V3
let today = new Date();
let dayNumber = Math.ceil((today - new Date(today.getFullYear(),0,1)) / 86400000);
let thisDayDate = today.getDate()
let thisMonth = today.getMonth()
let thisYear = today.getFullYear()
let daysYear = (leapYear(today.getFullYear())) ? 366 : 365;
let daysThisMonth = daysInMonth(thisMonth+1, thisYear)
const dateFormatter = new DateFormatter()
dateFormatter.dateFormat = "MMM"
const canvSize = 282;
const canvTextSize = 14;
const canvas = new DrawContext();
canvas.opaque = false
const batteryRemainColor = new Color('A8C686'); //Battery remaining color (green)
const batteryDepletedColor = new Color('E4572E'); //Battery depleted color (red)
const widgetBGColor = new Color('000'); //Widget background color
const circleTextColor = new Color('#fff'); //Widget text color
const bgCircleColor = new Color('#ccc') // bg circle color, full circle
const monthCircleColor = new Color('669BBC')
const dayCircleColor = new Color('F3A712')
const dayNCircleColor = new Color('bba420')
const canvWidth = 24; // circle thickness
const canvRadius = 120; // circle radius
canvas.size = new Size(canvSize, canvSize);
canvas.respectScreenScale = true;
const batteryLevel = Device.batteryLevel();
let monthDegree = Math.floor(((thisMonth+1)/12) * 100 * 3.6)
let dayDegree = Math.floor((thisDayDate/daysThisMonth) * 100 * 3.6)
let dayNDegree = Math.floor((dayNumber/daysYear) * 100 * 3.6)
/*
BEGIN Widget Layout
*/
let widget = new ListWidget();
widget.setPadding(0,5,1,0);
let batteryDegree = Math.floor(batteryLevel * 100 * 3.6)
makeCircle(0, batteryDepletedColor, batteryRemainColor, batteryDegree, circleTextColor)
drawMyText(
(Math.floor(batteryLevel * 100)).toString(),
circleTextColor,
258
)
let monthRadiusOffset = 27
makeCircle(monthRadiusOffset, bgCircleColor, monthCircleColor, monthDegree, circleTextColor)
drawMyText(
// dateFormatter.string(today), // Like Jan, Feb, from Scriptable dateFormatter
(thisMonth+1).toString(),
circleTextColor,
232
)
let dayRadiusOffset = 54
makeCircle(dayRadiusOffset, bgCircleColor, dayCircleColor, dayDegree, circleTextColor)
drawMyText(
thisDayDate.toString(),
circleTextColor,
205
)
// set widget
// additional script required to make transparent background image
const nobg = importModule('no-background.js')
widget.backgroundImage = await nobg.getSlice('small-top-left')
// widget.backgroundColor = widgetBGColor
// widget.backgroundImage = canvas.getImage()
widget.addImage(canvas.getImage())
Script.setWidget(widget);
widget.presentSmall();
Script.complete();
/*
END Widget Layout
*/
function makeCircle (radiusOffset, bgCircleColor, fgCircleColor, degree, txtColor) {
let ctr = new Point(canvSize / 2, canvSize / 2)
// Outer circle, usually full and in the background
CoordOffset = 0
RadiusOffset = 0
bgx = ctr.x - (canvRadius - radiusOffset);
bgy = ctr.y - (canvRadius - radiusOffset);
bgd = 2 * (canvRadius - radiusOffset);
bgr = new Rect(
bgx + CoordOffset,
bgy + CoordOffset,
bgd,
bgd
);
canvas.setStrokeColor(bgCircleColor);
canvas.setLineWidth(canvWidth);
canvas.strokeEllipse(bgr);
// Inner circle, usually filling and in the foreground
canvas.setFillColor(fgCircleColor);
for (t = 0; t < degree; t++) {
rect_x = ctr.x + (canvRadius - radiusOffset) * sinDeg(t) - canvWidth / 2;
rect_y = ctr.y - (canvRadius - radiusOffset) * cosDeg(t) - canvWidth / 2;
rect_r = new Rect(
rect_x,
rect_y,
canvWidth,
canvWidth
);
canvas.fillEllipse(rect_r);
}
}
function drawMyText(txt, txtColor, txtOffset) {
const txtRect = new Rect(
canvTextSize / 2 - 10,
txtOffset-canvTextSize / 2,
canvSize,
canvTextSize
);
canvas.setTextColor(txtColor);
canvas.setFont(Font.boldSystemFont(canvTextSize));
canvas.setTextAlignedCenter()
canvas.drawTextInRect(txt, txtRect)
}
function sinDeg(deg) {
return Math.sin((deg * Math.PI) / 180);
}
function cosDeg(deg) {
return Math.cos((deg * Math.PI) / 180);
}
function leapYear(year) {
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
// Month here is 1-indexed (January is 1, February is 2, etc). This is
// because we're using 0 as the day so that it returns the last day
// of the last month, so you have to add 1 to the month number
// so it returns the correct amount of days
function daysInMonth (month, year) {
return new Date(year, month, 0).getDate();
}
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: magic;
// Concentric circle V4
let today = new Date();
let dayNumber = Math.ceil((today - new Date(today.getFullYear(),0,1)) / 86400000);
let thisDayDate = today.getDate()
let thisMonth = today.getMonth()
let thisYear = today.getFullYear()
let daysYear = (leapYear(today.getFullYear())) ? 366 : 365;
let daysThisMonth = daysInMonth(thisMonth+1, thisYear)
const dateFormatter = new DateFormatter()
dateFormatter.dateFormat = "MMM"
const canvSize = 282;
const canvTextSize = 14;
const canvas = new DrawContext();
canvas.opaque = false
const batteryRemainColor = new Color('A8C686'); //Battery remaining color (green)
const batteryDepletedColor = new Color('E4572E'); //Battery depleted color (red)
const widgetBGColor = new Color('000'); //Widget background color
const circleTextColor = new Color('#fff'); //Widget text color
const bgCircleColor = new Color('#ccc') // bg circle color, full circle
const monthCircleColor = new Color('669BBC')
const dayCircleColor = new Color('F3A712')
const dayNCircleColor = new Color('bba420')
const canvWidth = 24; // circle thickness
const canvRadius = 120; // circle radius
canvas.size = new Size(canvSize, canvSize);
canvas.respectScreenScale = true;
const batteryLevel = Device.batteryLevel();
let monthDegree = Math.floor(((thisMonth+1)/12) * 100 * 3.6)
let dayDegree = Math.floor((thisDayDate/daysThisMonth) * 100 * 3.6)
let dayNDegree = Math.floor((dayNumber/daysYear) * 100 * 3.6)
/*
BEGIN Widget Layout
*/
let widget = new ListWidget();
widget.setPadding(0,5,1,0);
let batteryDegree = Math.floor(batteryLevel * 100 * 3.6)
makeCircle(0, batteryDepletedColor, batteryRemainColor, batteryDegree, circleTextColor)
drawMyText(
(Math.floor(batteryLevel * 100)).toString(),
circleTextColor,
258
)
let monthRadiusOffset = 27
makeCircle(monthRadiusOffset, bgCircleColor, monthCircleColor, monthDegree, circleTextColor)
drawMyText(
// dateFormatter.string(today), // Like Jan, Feb, from Scriptable dateFormatter
(thisMonth+1).toString(),
circleTextColor,
232
)
let dayRadiusOffset = 54
makeCircle(dayRadiusOffset, bgCircleColor, dayCircleColor, dayDegree, circleTextColor)
drawMyText(
thisDayDate.toString(),
circleTextColor,
205
)
/*
END Widget Layout
*/
function makeCircle (radiusOffset, bgCircleColor, fgCircleColor, degree, txtColor) {
let ctr = new Point(canvSize / 2, canvSize / 2)
// Outer circle, usually full and in the background
CoordOffset = 0
RadiusOffset = 0
bgx = ctr.x - (canvRadius - radiusOffset);
bgy = ctr.y - (canvRadius - radiusOffset);
bgd = 2 * (canvRadius - radiusOffset);
bgr = new Rect(
bgx + CoordOffset,
bgy + CoordOffset,
bgd,
bgd
);
canvas.setStrokeColor(bgCircleColor);
canvas.setLineWidth(canvWidth);
canvas.strokeEllipse(bgr);
// Inner circle, usually filling and in the foreground
canvas.setFillColor(fgCircleColor);
for (t = 0; t < degree; t++) {
rect_x = ctr.x + (canvRadius - radiusOffset) * sinDeg(t) - canvWidth / 2;
rect_y = ctr.y - (canvRadius - radiusOffset) * cosDeg(t) - canvWidth / 2;
rect_r = new Rect(
rect_x,
rect_y,
canvWidth,
canvWidth
);
canvas.fillEllipse(rect_r);
}
}
function drawMyText(txt, txtColor, txtOffset) {
const txtRect = new Rect(
canvTextSize / 2 - 10,
txtOffset - (canvTextSize / 2),
canvSize,
canvTextSize
);
canvas.setTextColor(txtColor);
canvas.setFont(Font.boldSystemFont(canvTextSize));
canvas.setTextAlignedCenter()
canvas.drawTextInRect(txt, txtRect)
}
function sinDeg(deg) {
return Math.sin((deg * Math.PI) / 180);
}
function cosDeg(deg) {
return Math.cos((deg * Math.PI) / 180);
}
function leapYear(year) {
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
// Month here is 1-indexed (January is 1, February is 2, etc). This is
// because we're using 0 as the day so that it returns the last day
// of the last month, so you have to add 1 to the month number
// so it returns the correct amount of days
function daysInMonth (month, year) {
return new Date(year, month, 0).getDate();
}
const roundedGraph = true
// roundedTemp : true|false > true (Displays the temps rounding the values (29.8 = 30 | 29.3 = 29).
// Widget Params
// Don't edit this, those are default values for debugging (location for Cupertino).
// You need to give your locations parameters through the widget params, more info below.
const widgetParams = JSON.parse((args.widgetParameter != null) ? args.widgetParameter : '{ "LAT" : "YOUR_LAT" , "LON" : "YOUR_LONG" , "LOC_NAME" : "YOUR_CITY" }')
// WEATHER API PARAMETERS !important
// API KEY, you need an Open Weather API Key
// You can get one for free at: https://home.openweathermap.org/api_keys (account needed).
const API_KEY = "YOUR_API_KEY"
// Hardcoded Location, type in your latitude/longitude values and location name
var LAT = widgetParams.LAT // 12.34
var LON = widgetParams.LON // 12.34
var LOCATION_NAME = widgetParams.LOC_NAME // "Your place"
// Set up cache. File located in the Scriptable iCloud folder
let fm = FileManager.iCloud();
// cache folder name changed to avoid conflict with the original author's script
let cachePath = fm.joinPath(fm.documentsDirectory(), "weatherCacheCC");
if(!fm.fileExists(cachePath)){
fm.createDirectory(cachePath)
}
let weatherData;
let usingCachedData = false;
let units = 'metric'
let locale = 'en'
try {
weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=minutely,alerts&units=" + units + "&lang=" + locale + "&appid=" + API_KEY).loadJSON();
fm.writeString(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON), JSON.stringify(weatherData));
}catch(e){
console.log("Offline mode")
try{
await fm.downloadFileFromiCloud(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON));
let raw = fm.readString(fm.joinPath(cachePath, "lastread"+"_"+LAT+"_"+LON));
weatherData = JSON.parse(raw);
usingCachedData = true;
}catch(e2){
console.log("Error: No offline data cached")
}
}
// 'Night' boolean for line graph and SFSymbols
var night = (today.getHours() > 17 || today.getHours() < 7)
let temp = weatherData.current.temp
const condition = weatherData.current.weather[0].id
// SFSymbol function
function symbolForCondition(cond){
let symbols = {
// Thunderstorm
"2": function(){
return "cloud.bolt.rain.fill"
},
// Drizzle
"3": function(){
return "cloud.drizzle.fill"
},
// Rain
"5": function(){
return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill"
},
// Snow
"6": function(){
return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow"
},
// Atmosphere
"7": function(){
if (cond == 781) { return "tornado" }
if (cond == 701 || cond == 741) { return "cloud.fog.fill" }
return night ? "cloud.fog.fill" : "sun.haze.fill"
},
// Clear and clouds
"8": function(){
if (cond == 800) { return night ? "moon.stars.fill" : "sun.max.fill" }
if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" }
return "cloud.fill"
}
}
// Get first condition digit.
let conditionDigit = Math.floor(cond / 100)
// Style and return the symbol.
let sfs = SFSymbol.named(symbols[conditionDigit]())
sfs.applyFont(Font.systemFont(25))
return sfs.image
}
function shouldRound(should, value){
return ((should) ? Math.round(value) : value)
}
// additional script required to make transparent background image
const nobg = importModule('no-background.js')
widget.backgroundImage = await nobg.getSlice('small-top-left')
// Draw weather icon
let weatherIconXOffset = 27
let weatherIconYOffset = 20
let weatherIconCoord = new Point(canvSize/3+weatherIconXOffset, canvSize/3+weatherIconYOffset)
canvas.drawImageAtPoint(symbolForCondition(condition), weatherIconCoord)
// Draw temperature
let temp2 = shouldRound(roundedGraph, temp)
drawMyText(temp2+'°', Color.white(), 165)
// Presents the Final Image from Canvas
widget.addImage(canvas.getImage())
Script.setWidget(widget);
widget.presentSmall();
Script.complete();
@brainno722
Copy link
Author

IF7xbdC

Copy link

ghost commented Oct 31, 2020

Hey, that looks really good - great work! Compliment!!!
Is it possible to display only the battery (possibly including the weather icon)? I tried to adapt the script a little bit, but unfortunately I'm not a programmer, so I can't do it alone :(

Another question: Is an api-key from Open Weather dangerous? So is there a risk of data abuse, hacker attacks or what data is collected from me by the website? I am just thinking about getting such an Api-Key...

Thanks for the feedback!

@brainno722
Copy link
Author

Hey, that looks really good - great work! Compliment!!!
Is it possible to display only the battery (possibly including the weather icon)? I tried to adapt the script a little bit, but unfortunately I'm not a programmer, so I can't do it alone :(

Another question: Is an api-key from Open Weather dangerous? So is there a risk of data abuse, hacker attacks or what data is collected from me by the website? I am just thinking about getting such an Api-Key...

Thanks for the feedback!

Hi,

To hide the date info (month and date) you can remove or comment out lines 56 ~ 73 (put /* before 56 and */ after line 73)

Open weather API is safe to apply. It is to prevent people from abusing the company's weather forecast system (they limit you to certain number of API calls per month in a free version, and raise that limit in the paid version)

Hope that helps

Copy link

ghost commented Oct 31, 2020

Hi,

To hide the date info (month and date) you can remove or comment out lines 56 ~ 73 (put /* before 56 and */ after line 73)

Open weather API is safe to apply. It is to prevent people from abusing the company's weather forecast system (they limit you to certain number of API calls per month in a free version, and raise that limit in the paid version)

Hope that helps

Yes, that helped me a lot. Thanks a lot!
I can use one Api Key in several widgets or? And how do I set the number of Api calls per day/month so that I do not exceed the limit?

@brainno722
Copy link
Author

I can use one Api Key in several widgets or? And how do I set the number of Api calls per day/month so that I do not exceed the limit?
If you just leave the widget to run normally, the refresh rate is decided by Apple, which is between 6-11 minutes, and that's what I do and it has not been flagged by OpenWeatherMap.

You can copy/paste the same API key to several widgets, but I did not test multiple widgets with the same API running at the same time, so I cannot tell you if it does use up the free allowance quicker.

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