Skip to content

Instantly share code, notes, and snippets.

@ms3056
Last active September 8, 2024 09:02
Show Gist options
  • Save ms3056/ebb6a7e175dcfd47f414a70addbd18c7 to your computer and use it in GitHub Desktop.
Save ms3056/ebb6a7e175dcfd47f414a70addbd18c7 to your computer and use it in GitHub Desktop.
Timeline template

image

Version History

  • V1 - initial release
  • V2: cleaned up the code a little, removed the black background behind the current day text as it wasn't precise. I left the code in there - just uncomment it. Removed an indent at the end to prevent breaking the SVG - thanks q on Discord!

Note

  • Reading View is BROKEN - wait for the plugin (assuming no surprises await me there). TBD - (when it gets done)™
  • Live Preview works as it should
  • Adjustments can be made at the top of the file
  • Adjust the bar heights, gaps between months, gap between quarter and month bar, add user defined events
  • You can change the month colors but note the quarters are a linear gradient of the colors of the months - this is hard coded

Requirements

  • Templater

Setup

  • Make sure Templater can see your files image

Usage - Template

  • Insert into your template using <% tp.user.Timeline() %>

Usage - Direct in note

  • Insert into your note <% tp.user.Timeline() %> and then click Alt+R
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
const userDates = [
{
date: "03-14",
symbol: "⛩️",
text: "14",
verticalPosition: -110,
size: 44,
},
{
date: "03-18",
symbol: "🍤",
text: "18",
verticalPosition: -186,
size: 44,
},
{
date: "03-22",
symbol: "🌸",
text: "22",
verticalPosition: -30,
size: 44,
},
{
date: "03-29",
symbol: "⛩️",
text: "29",
verticalPosition: -110,
size: 44,
},
{
date: "04-01",
symbol: "🍤",
text: "01",
verticalPosition: -186,
size: 44,
},
{
date: "04-25",
symbol: "⛩️",
text: "25",
verticalPosition: -110,
size: 44,
},
{
date: "05-08",
symbol: "🍤",
text: "08",
verticalPosition: -110,
size: 44,
},
{
date: "06-20",
symbol: "💍",
text: "20",
verticalPosition: -30,
size: 44,
},
{
date: "11-15",
symbol: "🎂",
text: "15",
verticalPosition: -30,
size: 44,
},
{ date: "12-25", symbol: "🎄", text: "", verticalPosition: -30, size: 100 },
];
// Font size of the today date text
const todayTextSize = 46;
// Size of the today symbol marker
const todaySymbolSize = 20;
// Padding between today marker and text
const todayPadding = 4;
// 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("DD-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 - todayTextSize / 3;
const todayTextYPosition =
quarterRowYPosition +
quarterRowHeight +
gapBetweenRows / 2 +
todaySymbolSize -
10;
const adjustedTodayTextYPosition = todayTextYPosition + todayTextSize / 14;
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>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
`;
// Generate the SVG content with dynamic sizing and positioning
let svgContent = `
<svg viewBox="0 -100 ${currentX + 20} 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" />
<!-- I have removed the black rectangle behind today's date -->
<text filter="url(#dropShadow)" x="${todayTextXPosition}" y="${adjustedTodayTextYPosition}" fill="#FF00FF" font-size="${todayTextSize}" font-weight="bold" text-anchor="end">${todayFormatted}</text>
</g>
</svg>
`;
return svgContent;
}
module.exports = generateTimelineSvg;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment