code icon Code

Render Life Grid SVG

Generate an SVG visualization showing life as a grid of weeks

Source Code

import fs from "fs";
import path from "path";

const [birthdate, outputDir, lifeExpectancyArg = "90"] = process.argv.slice(2);

if (!birthdate || !outputDir) {
  console.error("Error: birthdate and outputDir are required");
  process.exit(1);
}

const lifeExpectancy = parseInt(lifeExpectancyArg, 10);
const WEEKS_PER_YEAR = 52;
const TOTAL_WEEKS = lifeExpectancy * WEEKS_PER_YEAR;

// Parse birthdate and calculate weeks lived
const birth = new Date(birthdate);
if (isNaN(birth.getTime())) {
  console.error("Error: Invalid birthdate format. Use YYYY-MM-DD");
  process.exit(1);
}

const now = new Date();
const ageMs = now.getTime() - birth.getTime();
const weeksLived = Math.min(
  Math.floor(ageMs / (7 * 24 * 60 * 60 * 1000)),
  TOTAL_WEEKS
);

// SVG configuration - minimalist aesthetic
const cellSize = 8;
const cellGap = 2;
const padding = 40;
const labelPadding = 30;

const gridWidth = WEEKS_PER_YEAR * (cellSize + cellGap) - cellGap;
const gridHeight = lifeExpectancy * (cellSize + cellGap) - cellGap;
const legendHeight = 50;

const svgWidth = gridWidth + padding * 2 + labelPadding;
const svgHeight = gridHeight + padding * 2 + labelPadding + legendHeight;

// Life phases
const CHILDHOOD_END = 18;
const RETIREMENT_START = 65;

// Muted color palette (following skill color guidelines)
const colors = {
  background: "#faf9f7",
  childhood: "hsl(210 45% 80%)",      // lighter blue
  adulthood: "hsl(220 35% 65%)",      // standard blue
  currentWeek: "hsl(0 55% 55%)",      // red
  remaining: "hsl(220 15% 90%)",      // light gray
  retirement: "hsl(220 10% 75%)",     // darker gray
  text: "hsl(220 10% 40%)",
  labelText: "hsl(220 10% 60%)",
};

console.log("Generating life grid SVG...");
console.log(`  Weeks lived: ${weeksLived.toLocaleString()}`);
console.log(`  Total weeks: ${TOTAL_WEEKS.toLocaleString()}`);

// Build SVG
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${svgHeight}" width="${svgWidth}" height="${svgHeight}">
  <style>
    .title { font-family: system-ui, -apple-system, sans-serif; font-size: 14px; fill: ${colors.text}; }
    .label { font-family: system-ui, -apple-system, sans-serif; font-size: 9px; fill: ${colors.labelText}; }
    .legend-label { font-family: system-ui, -apple-system, sans-serif; font-size: 10px; fill: ${colors.labelText}; }
    .childhood { fill: ${colors.childhood}; }
    .adulthood { fill: ${colors.adulthood}; }
    .current { fill: ${colors.currentWeek}; }
    .remaining { fill: ${colors.remaining}; }
    .retirement { fill: ${colors.retirement}; }
  </style>
  <rect width="100%" height="100%" fill="${colors.background}"/>
  <text x="${padding + labelPadding}" y="${padding - 15}" class="title">Your life in weeks</text>
`;

// Add decade labels on the left
for (let decade = 0; decade <= lifeExpectancy; decade += 10) {
  const y = padding + labelPadding + decade * (cellSize + cellGap) + cellSize / 2;
  svg += `  <text x="${padding}" y="${y}" class="label" text-anchor="end" dominant-baseline="middle">${decade}</text>\n`;
}

// Add week labels on top (every 13 weeks = quarter)
for (let week = 0; week < WEEKS_PER_YEAR; week += 13) {
  const x = padding + labelPadding + week * (cellSize + cellGap) + cellSize / 2;
  svg += `  <text x="${x}" y="${padding + labelPadding - 8}" class="label" text-anchor="middle">${week + 1}</text>\n`;
}

// Generate grid cells
let weekCount = 0;
const currentWeekIndex = weeksLived - 1;

for (let year = 0; year < lifeExpectancy; year++) {
  for (let week = 0; week < WEEKS_PER_YEAR; week++) {
    const x = padding + labelPadding + week * (cellSize + cellGap);
    const y = padding + labelPadding + year * (cellSize + cellGap);
    const isLived = weekCount < weeksLived;
    const isCurrent = weekCount === currentWeekIndex;

    let className;
    if (isCurrent) {
      className = "current";
    } else if (isLived) {
      className = year < CHILDHOOD_END ? "childhood" : "adulthood";
    } else {
      className = year >= RETIREMENT_START ? "retirement" : "remaining";
    }

    svg += `  <rect x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" class="${className}" rx="1"/>\n`;
    weekCount++;
  }
}

// Add legend
const legendY = padding + labelPadding + gridHeight + 25;
const legendItems = [
  { label: "Childhood", className: "childhood" },
  { label: "Adulthood", className: "adulthood" },
  { label: "This week", className: "current" },
  { label: "Remaining", className: "remaining" },
  { label: "Retirement", className: "retirement" },
];

let legendX = padding + labelPadding;
for (const item of legendItems) {
  svg += `  <rect x="${legendX}" y="${legendY}" width="${cellSize}" height="${cellSize}" class="${item.className}" rx="1"/>\n`;
  svg += `  <text x="${legendX + cellSize + 5}" y="${legendY + cellSize - 1}" class="legend-label">${item.label}</text>\n`;
  legendX += 85;
}

svg += "</svg>";

// Ensure output directory exists
fs.mkdirSync(outputDir, { recursive: true });

// Generate filename with timestamp
const timestamp = Date.now();
const outputPath = path.join(outputDir, `life-grid-${timestamp}.svg`);
fs.writeFileSync(outputPath, svg);

const stats = fs.statSync(outputPath);

console.log(`\n✓ Generated life grid visualization`);
console.log(`  Dimensions: ${svgWidth}x${svgHeight}px`);
console.log(`  File size: ${(stats.size / 1024).toFixed(1)} KB`);
console.log(`  Saved to: ${outputPath}`);

console.log(
  JSON.stringify({
    success: true,
    path: outputPath,
    weeksLived,
    totalWeeks: TOTAL_WEEKS,
    dimensions: { width: svgWidth, height: svgHeight },
  })
);