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 },
})
);