
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");