Skip to content

Instantly share code, notes, and snippets.

@BXYMartin
Created March 15, 2025 14:33
Show Gist options
  • Select an option

  • Save BXYMartin/93927ea6a99902d44c76edab0f99af1f to your computer and use it in GitHub Desktop.

Select an option

Save BXYMartin/93927ea6a99902d44c76edab0f99af1f to your computer and use it in GitHub Desktop.
Scriptable widget to monitor Transport for London (TFL) status
const API_URL = "https://api.tfl.gov.uk/line/mode/tube/status";
// Define official TfL colors
const LINE_COLORS = {
"Bakerloo": "#B36305",
"Central": "#E32017",
"Circle": "#FFD300",
"District": "#00782A",
"Hammersmith & City": "#F3A9BB",
"Jubilee": "#A0A5A9",
"Metropolitan": "#9B0056",
"Northern": "#000000",
"Piccadilly": "#003688",
"Victoria": "#0098D4",
"Waterloo & City": "#95CDBA"
};
// Define status severity colors
const STATUS_COLORS = {
10: Color.green(), // Good Service
9: Color.yellow(), // Minor Delays
8: Color.yellow(), // Bus Service
7: Color.yellow(), // Reduced Service
6: Color.red(), // Severe Delays
5: Color.orange(), // Part Closure
4: Color.red(), // Planned Closure
3: Color.red(), // Suspended
2: Color.red(), // Service Closed
1: Color.red(), // Closed
0: Color.red(), // Special Service
};
// Fetch Tube status from TfL API
async function fetchTubeStatus() {
try {
let req = new Request(API_URL);
let data = await req.loadJSON();
let tubeStatus = data.map(line => ({
name: line.name,
status: line.lineStatuses[0]?.statusSeverityDescription || "Unknown",
severity: line.lineStatuses[0]?.statusSeverity || 10,
color: LINE_COLORS[line.name] || "#AAAAAA"
}));
// Prioritize Central and Piccadilly lines first, then sort by severity
tubeStatus.sort((a, b) => {
if (a.name === "Central" || a.name === "Piccadilly") return -1;
if (b.name === "Central" || b.name === "Piccadilly") return 1;
return a.severity - b.severity; // Higher severity first
});
return tubeStatus;
} catch (error) {
console.error("Error fetching TfL data:", error);
return [];
}
}
// Create a capsule-styled label with a rectangle at the bottom
function createCapsule(stack, text, bgColor, textColor = Color.white(), bottomColor = null) {
let capsule = stack.addStack();
capsule.layoutHorizontally(); // Ensure vertical stacking
capsule.setPadding(3, 6, 3, 6);
capsule.backgroundColor = bgColor;
capsule.cornerRadius = 10;
capsule.size = new Size(60, 0);
// If bottomColor is provided, add a rectangle at the bottom
if (bottomColor) {
// Create a hard transition gradient
let gradient = new LinearGradient();
gradient.colors = [bgColor, bgColor, bottomColor, bottomColor, bgColor, bgColor]; // Hard transition
gradient.locations = [0.0, 0.05, 0.051, 0.1, 0.11, 1.0]; // Sharp color change in the middle
gradient.startPoint = new Point(0, 0);
gradient.endPoint = new Point(1, 1);
capsule.backgroundGradient = gradient;
}
let label = capsule.addText(text);
label.font = Font.boldSystemFont(8);
label.textColor = textColor;
label.lineLimit = 1;
label.minimumScaleFactor = 1.0;
}
function createText(stack, text, textColor = Color.white()) {
let label = stack.addText(text);
label.font = Font.boldSystemFont(10);
label.textColor = textColor;
label.lineLimit = 1;
label.minimumScaleFactor = 1.0;
}
// Build widget UI
async function createWidget(size) {
let tubeStatus = await fetchTubeStatus();
let widget = new ListWidget();
widget.setPadding(18, 18, 18, 18);
widget.backgroundColor = new Color("#323E48"); // Official TfL blue
// Widget title
widget.addSpacer();
headerStack = widget.addStack();
headerStack.layoutHorizontally();
let title = headerStack.addText(size == "small" ? "Status" : "London Tubes Status");
title.font = Font.boldSystemFont(12);
title.textColor = Color.white();
headerStack.addSpacer();
let footer = headerStack.addText(new Date().toLocaleTimeString());
footer.rightAlignText();
footer.font = Font.systemFont(12);
footer.textColor = Color.white();
footer.textOpacity = 0.8;
widget.addSpacer();
// Define layout based on widget size
let maxLines = size == "small" ? 10 : config.widgetFamily == "medium" ? 10 : 16;
let displayedLines = tubeStatus.slice(0, maxLines);
let columnStack = widget.addStack();
columnStack.layoutHorizontally();
let leftColumn = columnStack.addStack();
leftColumn.layoutVertically();
columnStack.addSpacer(size == "small" ?5 : 10);
let rightColumn = columnStack.addStack();
rightColumn.layoutVertically();
displayedLines.forEach((line, index) => {
let parentStack = index % 2 === 0 ? leftColumn : rightColumn;
let row = parentStack.addStack();
row.layoutHorizontally();
row.centerAlignContent();
// Line name capsule
if (size == "small") {
createCapsule(row, line.name, STATUS_COLORS[line.severity] || Color.white(), Color.white(), new Color(line.color));
}
else {
createCapsule(row, line.name, new Color(line.color));
row.addSpacer();
// Status capsule with severity color
createText(row, line.status, STATUS_COLORS[line.severity] || Color.white());
}
parentStack.addSpacer(5);
});
// Refresh timestamp
widget.addSpacer();
return widget;
}
// Determine widget size
let widget = await createWidget(config.widgetFamily || "small");
if (!config.runsInWidget) {
await widget.presentSmall();
// await widget.presentMedium();
}
const now = new Date();
const nextUpdate = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
Math.ceil(now.getMinutes() / 30) * 30,
5,
0
);
widget.refreshAfterDate = nextUpdate;
Script.setWidget(widget);
Script.complete();
@BXYMartin
Copy link
Copy Markdown
Author

IMG_7162
IMG_7163

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