
Created 2025-09-12
/** * ============================================================================= * = Phases of the Moon = * ============================================================================= * * A lunar calendar visualization showing the phases of the moon throughout * a year. This example shows how to create bespoken visualizations in Recho * using various Unicode characters. * * The implementation calculates moon phases for each day of the year using * astronomical algorithms provided by [suncalc][1], then displays them in a * calendar format. The visualization shows: * * - moon phase emojis for each day of the year; * - the calendar grid with proper alignment using full-width spaces; and * - month names and day-of-week headers using circled Unicode characters. * * The rendering system constructs a character grid that accommodates the * varying lengths of month names while maintaining visual alignment. * * This is a remastered version of [Mike Bostock's implementation][2], which is * inspired by [2013 Phases of the Moon Calendar by Irwin Glusker][3]. * * Adjust the `year` parameter to view moon phases for different years! * * [1]: https://www.npmjs.com/package/suncalc * [2]: https://observablehq.com/@mbostock/phases-of-the-moon * [3]: https://www.moma.org/explore/inside_out/2012/10/16/a-paean-to-the-phases-of-the-moon/ */ const year = 2025; //➜ ╏ ╎ ⓉⓌⓇⒻⓈⓊⓂⓉⓌⓇⒻⓈⓊⓂⓉⓌⓇⒻⓈⓊⓂⓉⓌⓇⒻⓈⓊⓂⓉⓌⓇⒻⓈⓊⓂⓉⓌ ╏ //➜ ╏ January ╎ 🌑🌑🌒🌒🌒🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌖🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑 ╏ //➜ ╏ February ╎ 🌒🌒🌒🌒🌓🌓🌓🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑 ╏ //➜ ╏ March ╎ 🌑🌑🌒🌒🌒🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑 ╏ //➜ ╏ April ╎ 🌒🌒🌒🌓🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌑🌑🌑🌒 ╏ //➜ ╏ May ╎ 🌒🌒🌒🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌕🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌑🌑🌑🌑🌒🌒 ╏ //➜ ╏ June ╎ 🌒🌓🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌒🌒🌒 ╏ //➜ ╏ July ╎ 🌒🌓🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌒🌒🌒🌒🌓 ╏ //➜ ╏ August ╎ 🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓 ╏ //➜ ╏ September ╎ 🌓🌓🌔🌔🌔🌔🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓 ╏ //➜ ╏ October ╎ 🌓🌓🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓🌓🌓 ╏ //➜ ╏ November ╎ 🌔🌔🌔🌔🌕🌕🌕🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓🌓🌓 ╏ //➜ ╏ December ╎ 🌔🌔🌔🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓🌓🌔🌔 ╏ { const matrix = _.times(12, () => _.times(6 * 7, () => SPACE)); for (const day of days) { const row = day.getMonth(); const column = day.getDate() + months[row].getDay(); matrix[row][column] = getMoonEmoji(day); } // Remove the leading and trailing spaces from the header and each row. const head = Math.min(...matrix.map((xs) => xs.findIndex((x) => x !== SPACE))); const tail = Math.max(...matrix.map((xs) => xs.findLastIndex((x) => x !== SPACE))) + 1; echo(line(" ".repeat(longestLength), header.slice(head, tail)), {quote: false}); echo(matrix.map((x, i) => line(alignedMonthNames[i], x.slice(head, tail).join(""))).join("\n")); } function line(...items) { return "╏ " + items.join(" ╎ ") + " ╏"; } const header = "ⓂⓉⓌⓇⒻⓈⓊ".repeat(7); const months = d3.timeMonths(theFirstDay, theLastDay); const longestLength = monthNames.reduce((x, y) => Math.max(x, y.length), 0); const monthNames = months.map(d3.timeFormat("%B")); const alignedMonthNames = monthNames.map((n) => n.padStart(longestLength, " ")); const theFirstDay = d3.timeYear(new Date(year, 0, 1)); const theLastDay = d3.timeYear.offset(theFirstDay, 1); const days = d3.timeDays(theFirstDay, theLastDay); const SPACE = "\u3000"; // Full-width space function getMoonEmoji(date) { const index = Math.round(suncalc.getMoonIllumination(date).phase * 8); return String.fromCodePoint(0x1f311 + (index === 8 ? 0 : index)); } const suncalc = recho.require("suncalc"); const d3 = recho.require("d3"); const _ = recho.require("lodash");