JST --:--:--
arrow_back Widgets
Monthly Dot Grid
Monthly Dot Grid
Month progress tracker
Small Widget Scriptable
Visualizes your month as a grid of dots. Past days glow orange, future days stay white, and any day with an incomplete reminder is marked red. If today has a reminder, the dot splits — half orange, half red.
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 Small 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()