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