Life calendar for Obsidian (Dataview): one row per year of life, one square per week

// Life calendar: one row per year of life, one square per week. Needs the Dataview plugin with "Enable JavaScript Queries" turned on.

// Source: https://gist.github.com/churnish/69a96c57442eae936ed5589d51860f79

// ===== Fill in your own details =====

const BIRTH_DATE = "1993-03-30"; // YYYY-MM-DD

// Key life moments, one per line: the date as YYYY-MM-DD, a space, then a short name.
const KEY_EVENTS = `
1999-09-01 Started school
2011-06-20 Graduated high school
2016-08-01 First job
`;

// ===== Optional =====

// Age ranges (inclusive), each shaded in its own color.
const PERIODS = [
  { from: 0, to: 10, color: "#a9c7f2" },
  { from: 11, to: 20, color: "#b7dcae" },
  { from: 21, to: 30, color: "#f6c9a3" },
  { from: 31, to: 40, color: "#cdb6ed" },
];
// Ages that get an empty gap above their row.
const GAP_BEFORE_AGES = [];
// Key-moment squares keep this percent of their range's color and mix the rest with black; lower is darker.
const EVENT_COLOR_SHARE = 80;
// Number of rows; the last one is age MAX_AGE - 1.
const MAX_AGE = 100;

// ===== No need to edit below =====

const WEEKS_PER_YEAR = 52;
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;

// A round trip through Date rejects impossible dates such as 2015-02-30, which the pattern alone would accept.
function parseDate(text) {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text.trim());
  if (!match) return null;
  const [year, month, day] = match.slice(1).map(Number);
  const date = new Date(year, month - 1, day);
  return date.getMonth() === month - 1 && date.getDate() === day ? date : null;
}

// Each row is one year of life, so the current row starts at the last birthday; counting 52-week blocks from birth would drift about a day per row.
function getLifeProgress(birth, now) {
  let years = now.getFullYear() - birth.getFullYear();
  let lastBirthday = new Date(birth.getFullYear() + years, birth.getMonth(), birth.getDate());
  if (lastBirthday > now) {
    years--;
    lastBirthday = new Date(birth.getFullYear() + years, birth.getMonth(), birth.getDate());
  }
  const weeks = Math.min(WEEKS_PER_YEAR, Math.floor((now - lastBirthday) / MS_PER_WEEK));
  return { years, weeks };
}

// Problems with the filled-in values are listed above the calendar, since whoever edits them may not read code and would otherwise get a silently wrong calendar.
const problems = [];
const today = new Date();
const birth = BIRTH_DATE.trim() ? parseDate(BIRTH_DATE) : null;
if (BIRTH_DATE.trim() && !birth) problems.push(`Birth date "${BIRTH_DATE}" is not a real date in YYYY-MM-DD format.`);
if (birth && birth > today) problems.push(`Birth date ${BIRTH_DATE} is in the future.`);
const progress = birth && birth <= today ? getLifeProgress(birth, today) : { years: 0, weeks: 0 };

const eventLines = KEY_EVENTS.split("\n").map((line) => line.trim()).filter(Boolean);
if (!BIRTH_DATE.trim() && eventLines.length) problems.push("Fill in BIRTH_DATE to place the key events.");
const placedEvents = [];
for (const line of eventLines) {
  const [, dateText = "", label = ""] = /^(\S+)\s+(.+)$/.exec(line) ?? [];
  const date = parseDate(dateText);
  if (!date) {
    problems.push(`Key event "${line}" needs a YYYY-MM-DD date, a space, then a name.`);
    continue;
  }
  if (!birth) continue;
  if (date < birth) {
    problems.push(`Key event "${line}" is before the birth date.`);
    continue;
  }
  const { years, weeks } = getLifeProgress(birth, date);
  if (years >= MAX_AGE) {
    problems.push(`Key event "${line}" is past age ${MAX_AGE - 1}.`);
    continue;
  }
  placedEvents.push({ date: dateText, label, age: years, week: Math.min(weeks, WEEKS_PER_YEAR - 1) });
}
placedEvents.sort((a, b) => a.date.localeCompare(b.date));

const eventsByCell = new Map();
for (const event of placedEvents) {
  const key = `${event.age}-${event.week}`;
  eventsByCell.set(key, [...(eventsByCell.get(key) ?? []), event]);
}

// Obsidian keeps the 
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论