JST --:--:--
arrow_back Widgets
Days Left in Year
Days Left in Year
Year progress at a glance
Medium Widget Scriptable
Shows exactly how many days remain in the year, alongside your current day-of-year, percentage done, and a full-width progress bar. Styled in cream and burnt orange.
How to install
1
Download Scriptable from the App Store. It is free.
2
Tap Copy on the right, open Scriptable, tap +, paste the code, and tap Done.
3
Long press your home screen, add a Scriptable widget at Medium size, and pick this script.
Drag me!
// ===== CONFIG =====

const DOT_SIZE = 15
const DOT_SPACING_X = 6
const DOT_SPACING_Y = 6

const COLOR_DONE = "#ff7a00"
const COLOR_LEFT = "#ffffff"
const COLOR_REMINDER = "#ff0000"
const BACKGROUND = "#000000"

// ==================

let today = new Date()
let day = today.getDate()
let year = today.getFullYear()
let month = today.getMonth()

let daysInMonth = new Date(year, month + 1, 0).getDate()

// 🔥 FETCH REMINDERS
let calendars = await Calendar.forReminders()
let reminders = await Reminder.allIncomplete(calendars)

let reminderDays = new Set()

for (let r of reminders) {
  if (!r.dueDate) continue
  let d = new Date(r.dueDate)
  if (d.getMonth() !== month || d.getFullYear() !== year) continue
  if (r.recurrenceRules) {
    let hasDaily = r.recurrenceRules.some(rule => rule.frequency === "daily")
    if (hasDaily) continue
  }
  reminderDays.add(d.getDate())
}

// ==================

function makeSplitDot(size, colorL, colorR) {
  let scale = 4
  let s = size * scale
  let ctx = new DrawContext()
  ctx.size = new Size(s, s)
  ctx.opaque = false
  ctx.respectScreenScale = false

  let cx = s / 2
  let cy = s / 2
  let r = s / 2

  for (let x = 0; x < s; x++) {
    let dx = x - cx + 0.5
    if (Math.abs(dx) > r) continue
    let halfH = Math.sqrt(r * r - dx * dx)
    let top = cy - halfH
    let height = halfH * 2
    let col = (x < s / 2) ? new Color(colorL) : new Color(colorR)
    ctx.setFillColor(col)
    ctx.fillRect(new Rect(x, top, 1, height))
  }

  return ctx.getImage()
}

// ==================

let widget = new ListWidget()
widget.backgroundColor = new Color(BACKGROUND)

widget.addSpacer()

let container = widget.addStack()
container.layoutVertically()
container.centerAlignContent()

let rows = Math.ceil(daysInMonth / 7)

for (let r = 0; r < rows; r++) {

  let row = container.addStack()
  row.layoutHorizontally()
  row.centerAlignContent()

  for (let c = 0; c < 7; c++) {

    let index = r * 7 + c + 1
    if (index > daysInMonth) break

    if (reminderDays.has(index) && index === day) {
      let img = makeSplitDot(DOT_SIZE, COLOR_DONE, COLOR_REMINDER)
      let dotImg = row.addImage(img)
      dotImg.imageSize = new Size(DOT_SIZE * 0.80, DOT_SIZE * 0.80)

    } else if (reminderDays.has(index) && index > day) {
      let dot = row.addText("●")
      dot.textColor = new Color(COLOR_REMINDER)
      dot.font = Font.systemFont(DOT_SIZE)

    } else if (index <= day) {
      let dot = row.addText("●")
      dot.textColor = new Color(COLOR_DONE)
      dot.font = Font.systemFont(DOT_SIZE)

    } else {
      let dot = row.addText("●")
      dot.textColor = new Color(COLOR_LEFT)
      dot.font = Font.systemFont(DOT_SIZE)
    }

    row.addSpacer(DOT_SPACING_X)
  }

  container.addSpacer(DOT_SPACING_Y)
}

widget.addSpacer()

Script.setWidget(widget)
Script.complete()''',
    },
    {
        'id': 'days-left-year',
        'name': 'Days Left in Year',
        'tagline': 'Year progress at a glance',
        'description': 'Shows exactly how many days remain in the year, alongside your current day-of-year, percentage done, and a full-width progress bar. Styled in cream and burnt orange.',
        'size': 'Medium',
        'accent': '#C8621A',
        'preview': 'https://res.cloudinary.com/dpt9t6vvg/image/upload/widgets/days-left-year.png',
        'code': r'''// Days Left in Year Widget — Medium Size

const now = new Date();
const year = now.getFullYear();
const endOfYear = new Date(year, 11, 31); // Dec 31

// Calculate days
const msPerDay = 1000 * 60 * 60 * 24;
const daysLeft = Math.ceil((endOfYear - now) / msPerDay);
const dayOfYear = Math.floor((now - new Date(year, 0, 1)) / msPerDay) + 1;
const totalDays = ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) ? 366 : 365;
const progress = dayOfYear / totalDays;
const percentDone = Math.round(progress * 100);

// Month name
const monthNames = ["January","February","March","April","May","June","July","August","September","October","November","December"];
const monthName = monthNames[now.getMonth()];
const dateStr = `${monthName} ${now.getDate()}, ${year}`;

// Colors
const bg = new Color("#F5F0E8");          // Cream
const accentColor = new Color("#C8621A"); // Brownish orange
const textColor = new Color("#1C1C1C");   // Matte black
const subColor = new Color("#4A4A4A");    // Soft matte black
const trackColor = new Color("#DDD8CE");  // Cream track

const widget = new ListWidget();
widget.backgroundColor = bg;
widget.setPadding(16, 18, 16, 18);

// Top row: header label + date
const topRow = widget.addStack();
topRow.layoutHorizontally();
topRow.centerAlignContent();

const headerLabel = topRow.addText("📅  DAYS LEFT IN YEAR");
headerLabel.font = Font.boldMonospacedSystemFont(10);
headerLabel.textColor = subColor;

topRow.addSpacer();

const dateLabel = topRow.addText(dateStr);
dateLabel.font = Font.systemFont(10);
dateLabel.textColor = subColor;

widget.addSpacer(8);

// Main row: big number on left, stats on right
const mainRow = widget.addStack();
mainRow.layoutHorizontally();
mainRow.centerAlignContent();

// Left: big number
const leftStack = mainRow.addStack();
leftStack.layoutVertically();

const numText = leftStack.addText(String(daysLeft));
numText.font = Font.boldSystemFont(64);
numText.textColor = accentColor;
numText.minimumScaleFactor = 0.5;

const daysLabel = leftStack.addText("days remaining");
daysLabel.font = Font.systemFont(11);
daysLabel.textColor = subColor;

mainRow.addSpacer();

// Right: stats column
const rightStack = mainRow.addStack();
rightStack.layoutVertically();

// Day of year
const stat1 = rightStack.addText(`Day ${dayOfYear}`);
stat1.font = Font.boldSystemFont(20);
stat1.textColor = textColor;
stat1.rightAlignText();

const stat1sub = rightStack.addText(`of ${totalDays} days`);
stat1sub.font = Font.systemFont(10);
stat1sub.textColor = subColor;
stat1sub.rightAlignText();

rightStack.addSpacer(10);

// Percent done
const stat2 = rightStack.addText(`${percentDone}% done`);
stat2.font = Font.boldSystemFont(16);
stat2.textColor = accentColor;
stat2.rightAlignText();

const stat2sub = rightStack.addText(`${100 - percentDone}% to go`);
stat2sub.font = Font.systemFont(10);
stat2sub.textColor = subColor;
stat2sub.rightAlignText();

widget.addSpacer(10);

// Progress bar (full width)
const barBg = widget.addStack();
barBg.layoutHorizontally();
barBg.backgroundColor = trackColor;
barBg.cornerRadius = 5;
barBg.size = new Size(0, 8);

const filled = barBg.addStack();
filled.backgroundColor = accentColor;
filled.cornerRadius = 5;
const filledWidth = Math.max(6, Math.round(progress * 300));
filled.size = new Size(filledWidth, 8);

// Refresh every hour
widget.refreshAfterDate = new Date(Date.now() + 60 * 60 * 1000);

if (config.runsInWidget) {
  Script.setWidget(widget);
} else {
  widget.presentMedium();
}

Script.complete();