Skip to content

Instantly share code, notes, and snippets.

@pixelthing
Last active April 29, 2025 23:26
Show Gist options
  • Save pixelthing/47a65ac75faa12457d63c763ce6d8b0b to your computer and use it in GitHub Desktop.
Save pixelthing/47a65ac75faa12457d63c763ce6d8b0b to your computer and use it in GitHub Desktop.
// JS SCRIPT TO USE WITH SCRIPTABLE APP + iOS14+ WIDGETS
//
// Requires Scriptable1.5+ and iOS14/iPadOS14
//
// Download and install Scriptableapp https://scriptable.app
// Add this script into Scriptable, then add a scriptable widget to your homescreen
// (only the smallest size has been tested), hold down on the widget, then tap "edit widget",
// select *this* script as the "Script File", and in the *Parmameter* input, paste in
// the API key you use to allow access to the data feed.
// config
let apiKey = args.widgetParameter; // the API key (set in the widget param UI) sent to URL below.
// colours (background gradient start/end and complementary text colour)
let goalStartColor = '#beed58';
let goalEndColor = '#576e23';
let goalTextColor = '#ffffff';
let midStartColor = '#ffd54f';
let midEndColor = '#bd8f03';
let midTextColor = '#000000';
let loseStartColor = '#ff8484';
let loseEndColor = '#8f3535';
let loseTextColor = '#ffffff';
// URLs
let url1 = `https://sheets.googleapis.com/v4/spreadsheets/SPREADSHEETURL/values/SHEETNAME!`;
let urlThisSoFar = `B4:B4`;
let urlThisBad = `B5:B5`;
let urlThisHourly = `E3:E101`;
let urlPrevSoFar = `B13:B13`;
let urlPrevTotal = `B14:B14`;
let urlPrevBad = `B15:B15`;
let urlPrevHourly = `M3:M101`;
// to get access to the spreadsheet, you need to create an app at
// https://console.cloud.google.com/apis/dashboard
// then add some api credentials
let url2 = `?key=${apiKey}`;
// create widget instance
let w = new ListWidget()
let data = {
thisSoFar: 0,
thisBad: 0,
thisHourly: [],
prevSoFar: 0,
prevTotal: 0,
prevBad: 0,
prevHourly: []
}
// handler if data retrieval is successful
const processSuccess = function(
jsonThisSoFar,
jsonThisBad,
jsonPrevSoFar,
jsonPrevTotal,
jsonPrevBad,
jsonThisHourly,
jsonPrevHourly
) {
const hourlyCollate = (array) => {
const output = new Array(24);
output.fill(0);
if (array && array.length) {
array.forEach(d => {
const hour = parseInt(d[0]);
output[hour]++;
});
}
return output;
}
// parse data for totals
data = {
thisSoFar: jsonThisSoFar.values[0],
thisBad: jsonThisBad.values[0],
thisHourly: hourlyCollate(jsonThisHourly.values),
prevSoFar: jsonPrevSoFar.values[0],
prevTotal: jsonPrevTotal.values[0],
prevBad: jsonPrevBad.values[0],
prevHourly: hourlyCollate(jsonPrevHourly.values)
}
console.log(data)
console.log('today ' + data.thisSoFar)
console.log('prev ' + data.prevSofar)
// format the widget
// determine colour schemes determined by how close to the goal we are
let startColor = midStartColor;
let endColor = midEndColor;
let textColor = midTextColor;
if (parseInt(data.thisSoFar) > parseInt(data.prevSoFar)) {
startColor = goalStartColor;
endColor = goalEndColor;
textColor = goalTextColor;
} else if (parseInt(data.thisSoFar) < parseInt(data.prevSoFar)) {
startColor = loseStartColor;
endColor = loseEndColor;
textColor = loseTextColor;
}
// set background (depending on context)
let gradient = new LinearGradient();
gradient.colors = [new Color(startColor), new Color(endColor)];
gradient.locations = [0.0, 1];
w.backgroundGradient = gradient;
w.addSpacer(5);
// title row
let titleStack = w.addStack();
titleStack.layoutHorizontally();
titleStack.centerAlignContent();
// title icon
// try and find an icon from https://sfsymbols.com
let extra1Icon = titleStack.addImage(SFSymbol.named('star.circle.fill').image);
extra1Icon.tintColor = new Color(textColor);
extra1Icon.imageSize = new Size(25, 25);
titleStack.addSpacer(5);
// title text: number label
let extra1Txt = titleStack.addText('Metric')
extra1Txt.textColor = new Color(textColor);
extra1Txt.font = Font.boldSystemFont(14);
w.addSpacer(5);
//Draw chart
let graphImg = columnGraph(data.thisHourly, 110, 20).getImage();
let chartImg = w.addImage(graphImg);
chartImg.resizable = false;
// big number row
let bigNumStack = w.addStack();
bigNumStack.layoutHorizontally();
bigNumStack.bottomAlignContent();
// big number: number
let bigNumTxt = bigNumStack.addText(data.thisSoFar + '')
bigNumTxt.textColor = new Color(textColor);
bigNumTxt.font = Font.lightSystemFont(40);
bigNumStack.addSpacer(3);
// big number: suffix
let bigNumSuffixStack = bigNumStack.addStack()
bigNumSuffixStack.layoutVertically();
let bigNumSuffixHorizStack = bigNumSuffixStack.addStack();
bigNumSuffixHorizStack.layoutHorizontally();
bigNumSuffixHorizStack.centerAlignContent();
let bigNumSuffix = bigNumSuffixHorizStack.addText('today')
bigNumSuffix.textColor = new Color(textColor);
bigNumSuffix.font = Font.mediumSystemFont(12);
bigNumSuffixHorizStack.addSpacer(2);
// big number: note if bad results
if (parseInt(data.thisBad) > 0) {
let bigNumBadIcon = bigNumSuffixHorizStack.addImage(SFSymbol.named('exclamationmark.triangle.fill').image);
bigNumBadIcon.tintColor = new Color(textColor,0.4);
bigNumBadIcon.imageSize = new Size(12, 12);
bigNumSuffixHorizStack.addSpacer(2);
let bigNumBadTxt = bigNumSuffixHorizStack.addText(data.thisBad + '')
bigNumBadTxt.textColor = new Color(textColor,0.4);
bigNumBadTxt.font = Font.mediumSystemFont(12);
}
bigNumSuffixStack.addSpacer(2);
// yesterday: previous day number at this point
let extra2Txt = bigNumSuffixStack.addText('yest ' + data.prevSoFar + '/' + data.prevTotal)
extra2Txt.textColor = new Color(textColor);
extra2Txt.font = Font.mediumSystemFont(12);
bigNumSuffixStack.addSpacer(7);
function columnGraph(data, width, height) {
let maxValueInArray = Math.max(...data);
console.log('max: ' + maxValueInArray)
let mean = 0
let max = maxValueInArray;
let dataLength = data.length;
let currentHour = new Date();
currentHour = currentHour.getHours();
let context = new DrawContext();
context.size = new Size(width, height);
context.opaque = false;
context.respectScreenScale = true;
for (let i = 0; i < dataLength; i++) {
let colorRect;
colorRect = textColor;
let w = width / (2 * dataLength - 1);
let h = (data[i] / max) * height;
if (!h) {
h = 2;
}
console.log(i + ' h: ' + h + '(' + data[i] + ',' + height + ',' + max + ')')
let x = (i * 2 + 1) * w;
let y = height - h;
context.setFillColor(new Color(colorRect));
if (i > currentHour - 1) {
context.setFillColor(new Color(colorRect,0.3));
}
const path = new Path();
path.addRoundedRect(new Rect(x, y, w, h), 2, 2);
context.addPath(path);
context.fillPath();
}
return context;
}
// set widget
Script.setWidget(w);
Script.complete();
}
// handler if data retrieval is UNsuccessful
const processError = function(err) {
// format error message
let errorScript = (err + '').replace('Error: ','');
// set background
let gradient = new LinearGradient();
gradient.colors = [new Color(loseStartColor), new Color(loseEndColor)];
gradient.locations = [0.0, 1];
w.backgroundGradient = gradient
// set text: title
let titleTxt = w.addText('error')
titleTxt.textColor = Color.white()
titleTxt.font = Font.mediumSystemFont(24)
// set text: message
let extra1Txt = w.addText(errorScript)
extra1Txt.textColor = Color.white()
extra1Txt.font = Font.mediumSystemFont(12)
// set widget
Script.setWidget(w);
Script.complete();
}
// run the whole thing
try {
// no API key? error out now.
if (!apiKey) {
processError('enter API key in parameter.')
console.log( 'no API key ' + apiKey )
return;
// start data requests and process them
} else {
let reqThisSoFar = new Request(url1 + urlThisSoFar + url2);
let reqThisBad = new Request(url1 + urlThisBad + url2);
let reqThisHourly = new Request(url1 + urlThisHourly + url2);
let reqPrevSoFar = new Request(url1 + urlPrevSoFar + url2);
let reqPrevTotal = new Request(url1 + urlPrevTotal + url2);
let reqPrevBad = new Request(url1 + urlPrevBad + url2);
let reqPrevHourly = new Request(url1 + urlPrevHourly + url2);
let jsonThisSoFar = await reqThisSoFar.loadJSON();
let jsonThisBad = await reqThisBad.loadJSON();
let jsonThisHourly = await reqThisHourly.loadJSON();
let jsonPrevSoFar = await reqPrevSoFar.loadJSON();
let jsonPrevTotal = await reqPrevTotal.loadJSON();
let jsonPrevBad = await reqPrevBad.loadJSON();
let jsonPrevHourly = await reqPrevHourly.loadJSON();
console.log('data fetched')
processSuccess(jsonThisSoFar, jsonThisBad, jsonPrevSoFar, jsonPrevTotal, jsonPrevBad, jsonThisHourly, jsonPrevHourly);
}
// if any data requests or parsing fails, error out here.
} catch (err) {
processError(err)
console.log( 'offline? data source down? ' + err )
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment