|
async function generateTimelineSvg() {
|
|
// Edit these variables
|
|
// Gap between Months
|
|
const gap = 10;
|
|
// Height of the Quarter Bar
|
|
const quarterRowHeight = 85;
|
|
// Height of the Row Bar
|
|
const monthRowHeight = 65;
|
|
// Gap between Quarter and Row Bars
|
|
const gapBetweenRows = 10;
|
|
// Height of the text
|
|
const textHeight = 60;
|
|
// Vertical position of the userDates
|
|
const quarterRowYPosition = 60;
|
|
// Bar radii
|
|
const barRadaii = 12;
|
|
|
|
// Follow this pattern - as few or as many as you like. Text can be what you want - like the day number or name
|
|
// The variable verticalPosition is an adjustment on where you want the icon and text to show
|
|
// The size variable is the font size for that event
|
|
// Dates are in MONTH-DAY format, so 03-10 is March 10
|
|
const userDates = [
|
|
{ date: "02-02", symbol: "🎂", text: "", verticalPosition: -106, size: 80 },
|
|
{ date: "03-29", symbol: "💍", text: "", verticalPosition: -106, size: 80 },
|
|
{ date: "06-08", symbol: "🎂", text: "", verticalPosition: -106, size: 80 },
|
|
{ date: "12-25", symbol: "🎄", text: "", verticalPosition: -106, size: 100 },
|
|
];
|
|
|
|
// Font size of the today date text
|
|
const todayTextSize = 60;
|
|
// Size of the today symbol marker
|
|
const todaySymbolSize = 30;
|
|
// Padding between today marker and text
|
|
const todayPadding = 6;
|
|
// Size of the background of the
|
|
const todayBackgroundWidth = 240;
|
|
|
|
// Don't change anything from here unless you know what you are doing
|
|
|
|
const year = new Date().getFullYear();
|
|
|
|
const isLeapYear = (year) =>
|
|
year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0);
|
|
|
|
const daysInMonth = [
|
|
31,
|
|
isLeapYear(year) ? 29 : 28,
|
|
31,
|
|
30,
|
|
31,
|
|
30,
|
|
31,
|
|
31,
|
|
30,
|
|
31,
|
|
30,
|
|
31,
|
|
];
|
|
|
|
const monthColors = [
|
|
"#6AA9FF",
|
|
"#3F85FF",
|
|
"#6AB04A",
|
|
"#B4E051",
|
|
"#F8E352",
|
|
"#FFC30F",
|
|
"#FF7F27",
|
|
"#FF4C3B",
|
|
"#DAA520",
|
|
"#6A5ACD",
|
|
"#483D8B",
|
|
"#5F9EA0",
|
|
];
|
|
|
|
const monthNames = moment.monthsShort();
|
|
|
|
const monthRowYPosition =
|
|
quarterRowYPosition + quarterRowHeight + gapBetweenRows;
|
|
|
|
const calculateDayOfYear = (dateStr) => {
|
|
const [month, day] = dateStr.split("-").map(Number);
|
|
const isLeap = year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0);
|
|
|
|
// Adjust for non-leap year if date is Feb 29
|
|
if (!isLeap && month === 2 && day === 29) {
|
|
dateStr = "2-28";
|
|
}
|
|
|
|
// Continue with day of year calculation
|
|
const newDate = new Date(`${year}-${dateStr}`);
|
|
const start = new Date(newDate.getFullYear(), 0, 0);
|
|
const diff = newDate - start;
|
|
const oneDay = 1000 * 60 * 60 * 24;
|
|
const dayOfYear = Math.floor(diff / oneDay);
|
|
|
|
return dayOfYear;
|
|
};
|
|
|
|
// Unified method to calculate xPosition for any date
|
|
const calculateXPosition = (dateStr) => {
|
|
const dayOfYear = calculateDayOfYear(dateStr);
|
|
const monthIndex = parseInt(dateStr.split("-")[0], 10) - 1; // Convert 'MM' to index
|
|
const xPosition = (dayOfYear - 1) * 10 + monthIndex * gap; // Calculate xPosition with day width and gap
|
|
return xPosition;
|
|
};
|
|
|
|
let currentX = 0;
|
|
let monthPositions = [],
|
|
quarterPositions = [],
|
|
quarterWidths = [];
|
|
|
|
daysInMonth.forEach((days, index) => {
|
|
monthPositions.push({ x: currentX, width: days * 10 });
|
|
currentX += days * 10 + gap;
|
|
if ((index + 1) % 3 === 0) {
|
|
let quarterWidth =
|
|
daysInMonth
|
|
.slice(index - 2, index + 1)
|
|
.reduce((acc, curr) => acc + curr, 0) *
|
|
10 +
|
|
2 * gap;
|
|
quarterPositions.push({
|
|
x: monthPositions[index - 2].x,
|
|
width: quarterWidth,
|
|
});
|
|
}
|
|
});
|
|
|
|
const userDatesPositions = userDates.map((date) => {
|
|
const xPosition = calculateXPosition(date.date);
|
|
const yPosition =
|
|
monthRowYPosition +
|
|
monthRowHeight +
|
|
gapBetweenRows +
|
|
date.verticalPosition;
|
|
return { ...date, x: xPosition, y: yPosition };
|
|
});
|
|
|
|
// Define gapCenterYPosition correctly
|
|
const gapCenterYPosition =
|
|
quarterRowYPosition + quarterRowHeight + gapBetweenRows / 2;
|
|
|
|
const today = new Date();
|
|
|
|
// Use for display
|
|
const todayFormatted = moment(today).format("D-MMM");
|
|
|
|
// Convert 'today' to 'MM-DD' format for calculation purposes
|
|
const todayCalculationFormat = `${today.getMonth() + 1}-${today.getDate()}`;
|
|
|
|
// Calculate xPosition using a function compatible with 'MM-DD' format
|
|
const todayXPosition = calculateXPosition(todayCalculationFormat);
|
|
const todayTextXPosition = todayXPosition - todaySymbolSize - 36;
|
|
const todayTextYPosition =
|
|
quarterRowYPosition +
|
|
quarterRowHeight +
|
|
gapBetweenRows / 2 +
|
|
todaySymbolSize -
|
|
10;
|
|
const todayBackgroundHeight = todayTextSize + todayPadding * 2;
|
|
const todayBackgroundY = todayTextYPosition - todayTextSize + todayPadding;
|
|
|
|
let quarterGradients = quarterPositions
|
|
.map((q, i) => {
|
|
const startMonthIndex = i * 3;
|
|
return `
|
|
<linearGradient id="Q${i + 1}Gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" style="stop-color:${monthColors[startMonthIndex]}" />
|
|
<stop offset="50%" style="stop-color:${monthColors[startMonthIndex + 1]}" />
|
|
<stop offset="100%" style="stop-color:${monthColors[startMonthIndex + 2]}" />
|
|
</linearGradient>
|
|
`;
|
|
})
|
|
.join("");
|
|
|
|
let filterDefinition = `
|
|
<filter id="brightness" x="0" y="0" width="100%" height="100%">
|
|
<feColorMatrix type="matrix" values="0.4 0 0 0 0
|
|
0 0.4 0 0 0
|
|
0 0 0.4 0 0
|
|
0 0 0 1 0" />
|
|
</filter>
|
|
<filter id="dropShadow" height="130%">
|
|
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
|
<feOffset dx="2" dy="2" result="offsetblur"/>
|
|
<feMerge>
|
|
<feMergeNode in="offsetblur"/>
|
|
<feMergeNode in="SourceGraphic"/>
|
|
</feMerge>
|
|
</filter>
|
|
`;
|
|
|
|
// Generate the SVG content with dynamic sizing and positioning
|
|
let svgContent = `
|
|
<svg viewBox="0 -100 ${currentX} 400" xmlns="http://www.w3.org/2000/svg">
|
|
<title>Dynamic Timeline ${year}</title>
|
|
<defs>
|
|
${filterDefinition}
|
|
${quarterGradients}
|
|
</defs>
|
|
<g filter="url(#brightness)">
|
|
${quarterPositions
|
|
.map(
|
|
(q, i) => `
|
|
<rect x="${q.x}" y="${quarterRowYPosition}" width="${q.width}" height="${quarterRowHeight}" fill="url(#Q${i + 1}Gradient)" rx="${barRadaii}" ry="${barRadaii}" />
|
|
<text x="${q.x + q.width / 2}" y="${quarterRowYPosition - 40}" fill="white" font-size="${textHeight}" text-anchor="middle">Q${i + 1}</text>
|
|
`
|
|
)
|
|
.join("")}
|
|
${monthPositions
|
|
.map(
|
|
(m, i) => `
|
|
<rect x="${m.x}" y="${monthRowYPosition}" width="${m.width}" height="${monthRowHeight}" fill="${monthColors[i]}" rx="${barRadaii}" ry="${barRadaii}" />
|
|
<text x="${m.x + m.width / 2}" y="${monthRowYPosition + monthRowHeight + 70}" fill="white" font-size="${textHeight}" text-anchor="middle">${monthNames[i]}</text>
|
|
`
|
|
)
|
|
.join("")}
|
|
</g>
|
|
<g>
|
|
${userDatesPositions
|
|
.map(
|
|
(date) => `
|
|
<text filter="url(#dropShadow)" x="${date.x}" y="${date.y}" fill="white" font-size="${date.size}" text-anchor="middle">${date.symbol}${date.text}</text>
|
|
`
|
|
)
|
|
.join("")}
|
|
<circle filter="url(#dropShadow)" cx="${todayXPosition}" cy="${gapCenterYPosition}" r="${todaySymbolSize}" stroke="white" fill="#FF00FF" stroke-width="5" />
|
|
<rect x="${todayTextXPosition - todayBackgroundWidth + todayPadding * 2 + 6}" y="${todayBackgroundY}" width="${todayBackgroundWidth}" height="${todayBackgroundHeight}" stroke-width="5" stroke="#FF00FF" fill="var(--background-primary)" rx="10" ry="10" />
|
|
<text filter="url(#dropShadow)" x="${todayTextXPosition}" y="${todayTextYPosition}" fill="white" font-size="${todayTextSize}" text-anchor="end">${todayFormatted}</text>
|
|
</g>
|
|
</svg>
|
|
`;
|
|
|
|
return svgContent;
|
|
}
|
|
|
|
module.exports = generateTimelineSvg; |