I made a yearly calendar based on the Heatmap calendar plugin

Long story short, since I’m a frequent traveler I need to check the dates quickly during the year I’m travelling. I tried to find any good ways to visualise the whole year calendar, but there’s not many options out there. I found the heatmap calendar plugin did something close to my needs, so I adjusted it. This is what it looks like:

My skills of java script are close to zero, so probably this idea could be pushed further. If anyone want to test it or reuse it

main.zip (4.1 KB)

2 Likes

I forgot to add it requires installing Dataview plugin to enable the javascript queries and for the trip files are stored in a Folder named ‘Trips’ and a basic template includes the following metadata:
-–
destination: Portugal
startDate: 2023-01-27
endDate: 2023-01-31
color: green
code: 2
-–

To create the calendar, just create a page with the following content:

```dataviewjs
dv.span(“”) /* optional */
const calendarData = {
year: 2023, // (optional) defaults to current year
colors: { // (optional) defaults to green
gray: [“#dbdbdb”, “#69a3ff”, “#428bff”, “#1872ff”, “#0058e2”],
blue: [“#8cb9ff”, “#69a3ff”, “#428bff”, “#1872ff”, “#0058e2”], // first entry is considered default if supplied
green: [“#c6e48b”, “#7bc96f”, “#49af5d”, “#2e8840”, “#196127”],
red: [“#ff9e82”, “#ff7b55”, “#ff4d1a”, “#e73400”, “#bd2a00”],
orange: [“#ffa244”, “#fd7f00”, “#dd6f00”, “#bf6000”, “#9b4e00”],
pink: [“#ff96cb”, “#ff70b8”, “#ff3a9d”, “#ee0077”, “#c30062”],
orangeToRed: [“#ffdf04”, “#ffbe04”, “#ff9a03”, “#ff6d02”, “#ff2c01”]
},
showCurrentDayBorder: true, // (optional) defaults to true
defaultEntryIntensity: 4, // (optional) defaults to 4
intensityScaleStart: 10, // (optional) defaults to lowest value passed to entries.intensity
intensityScaleEnd: 100, // (optional) defaults to highest value passed to entries.intensity
entries: [], // (required) populated in the DataviewJS loop below
}

//DataviewJS loop
for (let page of dv.pages(‘“Trips”’).where(p => p.code)) {
//dv.span(“
” + Date(page.inbound)) // uncomment for troubleshooting
const startDate = new Date(page.startDate);
const endDate = new Date(page.endDate);
let currentDate = startDate;
const oneDayInMillis = 24 * 60 * 60 * 1000;
while (currentDate <= endDate) {
const day = new Date(currentDate.getTime() + oneDayInMillis);
calendarData.entries.push({
date: day.toISOString().split(‘T’)[0],
intensity: page.code,
color: page.color,
});

    currentDate.setDate(currentDate.getDate() + 1);
}

}

renderHeatmapCalendar(this.container, calendarData)
```

1 Like

Something didn’t paste right with the javascript snippet. Maybe re-attach as a text file or embed as a code block? Looks very cool!

Hi @p3d2 - you might also like to have a look at this one, which was created for visualising travel events over the year:

3 Likes

I was able to almost recreate this with the heatmap-calendar github and your helpful code, but I can’t figure out how to change the colors of the months and get the numbers inside the cells. Something to do with content inside the calendarData.entries.push, but I can’t figure it out from there… Any tips?

I doubt this will get replies given the original post was a long time ago, but I wanted to update to say that I figured out the alternating months with the snippet on the heatmap github. Still searching for how to figure out the numbers…

I’ve been working on my own take on this idea, and here is the result:

It just got approved, so you can use it right now !

Hey! I’ve been banging my head with Copilot for two days but I finally got it to flip the layout to horizontal. I see it’s an open issue in the GitHub. I know nothing of JS and only a pinch of CSS so I won’t be presumptuous enough to fork.

Here’s what copilot did, maybe it’ll help you!

main.js:


var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// main.ts
var main_exports = {};
__export(main_exports, {
  default: () => EveryDayCalendar
});
module.exports = __toCommonJS(main_exports);
var import_obsidian = require("obsidian");
var EveryDayCalendar = class extends import_obsidian.Plugin {
  async onload() {
    window.everyDayCalendar = (el, year, fromDays, extraParams = null) => {
      const monthsInYear = 12;
      const daysInMonth = (month) => new Date(Date.UTC(year, month, 0)).getUTCDate();
      
      const additionalClasses = (extraParams?.additionalClasses) ?? [];

      const outerDiv = createDiv({
        cls: additionalClasses.concat(["every-day-calendar", "outermost"]),
        parent: el
      });

      createDiv({
        cls: additionalClasses.concat(["every-day-calendar", "year"]),
        parent: outerDiv,
        text: `${year}`
      });

      const boxesDiv = createDiv({
        cls: additionalClasses.concat(["every-day-calendar", "boxes"]),
        parent: outerDiv
      });

      for (let month = 1; month <= monthsInYear; month++) {
        const daysInThisMonth = daysInMonth(month);

        for (let day = 1; day <= daysInThisMonth; day++) {
          const value = fromDays(new Date(Date.UTC(year, month - 1, day)));
          createSpan({
            cls: additionalClasses.concat(["every-day-calendar", "box"]),
            parent: boxesDiv,
            attr: { value, month, day } // Added day attribute for correct grid placement
          });
        }
      }
    };
  }
  onunload() {}
};

styles.css:

/* Adjusted layout for a horizontal heatmap with 12 rows (months) and 31 columns (days) */

.every-day-calendar {
    --grid-gap: 0.3vh;
    --height: auto;
    --width: auto;
    --num-months: 12 !important;
    --num-days: 31 !important;
    --box-aspect-ratio: calc(var(--num-days) / var(--num-months)) !important;
    --sum-box-heights: calc(var(--height) - (var(--num-months) - 1) * var(--grid-gap)) !important;
    --sum-box-widths: calc(var(--sum-box-heights) * var(--box-aspect-ratio)) !important;
}

.every-day-calendar.outermost {
    display: grid;
    grid-template-rows: auto 1fr;
    gap: var(--grid-gap);
    width: 100%;
    height: auto;
}

.every-day-calendar.boxes {
    display: grid;
    grid-template-columns: repeat(var(--num-days), 1fr);
    grid-template-rows: repeat(var(--num-months), 1fr);
    grid-gap: var(--grid-gap);
    width: 100%;
}

.every-day-calendar.box {
    display: flex;
    flex-direction: row;
    color: black;
    background-color: #FF00FF;
    border-radius: 25%;
    aspect-ratio: 1;
}

.every-day-calendar.box[value="0"] {
    background-color: var(--background-secondary);
}

.every-day-calendar.box[value="1"] {
    /* background-color: rgba(var(--background-modifier-border), 0.4); */
    background-color: #A0E7E5;
}

.every-day-calendar.box[value="2"] {
    /* background-color: rgba(var(--text-accent), 0.7); */
    background-color: #40E0D0;
}

.every-day-calendar.box[value="3"] {
    /* background-color: var(--text-accent); */
    background-color: #008080;
}

/* Assign rows dynamically based on the month attribute */
.every-day-calendar.box[month="1"] { grid-row: 1; }
.every-day-calendar.box[month="2"] { grid-row: 2; }
.every-day-calendar.box[month="3"] { grid-row: 3; }
.every-day-calendar.box[month="4"] { grid-row: 4; }
.every-day-calendar.box[month="5"] { grid-row: 5; }
.every-day-calendar.box[month="6"] { grid-row: 6; }
.every-day-calendar.box[month="7"] { grid-row: 7; }
.every-day-calendar.box[month="8"] { grid-row: 8; }
.every-day-calendar.box[month="9"] { grid-row: 9; }
.every-day-calendar.box[month="10"] { grid-row: 10; }
.every-day-calendar.box[month="11"] { grid-row: 11; }
.every-day-calendar.box[month="12"] { grid-row: 12; }

/* Assign columns dynamically based on the day attribute */
.every-day-calendar.box[day="1"] { grid-column: 1; }
.every-day-calendar.box[day="2"] { grid-column: 2; }
.every-day-calendar.box[day="3"] { grid-column: 3; }
.every-day-calendar.box[day="4"] { grid-column: 4; }
.every-day-calendar.box[day="5"] { grid-column: 5; }
.every-day-calendar.box[day="6"] { grid-column: 6; }
.every-day-calendar.box[day="7"] { grid-column: 7; }
.every-day-calendar.box[day="8"] { grid-column: 8; }
.every-day-calendar.box[day="9"] { grid-column: 9; }
.every-day-calendar.box[day="10"] { grid-column: 10; }
.every-day-calendar.box[day="11"] { grid-column: 11; }
.every-day-calendar.box[day="12"] { grid-column: 12; }
.every-day-calendar.box[day="13"] { grid-column: 13; }
.every-day-calendar.box[day="14"] { grid-column: 14; }
.every-day-calendar.box[day="15"] { grid-column: 15; }
.every-day-calendar.box[day="16"] { grid-column: 16; }
.every-day-calendar.box[day="17"] { grid-column: 17; }
.every-day-calendar.box[day="18"] { grid-column: 18; }
.every-day-calendar.box[day="19"] { grid-column: 19; }
.every-day-calendar.box[day="20"] { grid-column: 20; }
.every-day-calendar.box[day="21"] { grid-column: 21; }
.every-day-calendar.box[day="22"] { grid-column: 22; }
.every-day-calendar.box[day="23"] { grid-column: 23; }
.every-day-calendar.box[day="24"] { grid-column: 24; }
.every-day-calendar.box[day="25"] { grid-column: 25; }
.every-day-calendar.box[day="26"] { grid-column: 26; }
.every-day-calendar.box[day="27"] { grid-column: 27; }
.every-day-calendar.box[day="28"] { grid-column: 28; }
.every-day-calendar.box[day="29"] { grid-column: 29; }
.every-day-calendar.box[day="30"] { grid-column: 30; }
.every-day-calendar.box[day="31"] { grid-column: 31; }

I’m sorry I massacred your plugin, but I really have been looking for something this good since forever and I wanted to waste some hours. Thank you for your work!

PS: Yes I’ve been stalking the github these days, yes I saw you JUST opened a PR and came here to hound you instead.
PS2: I tried opacity with the colors but obsidian wasn’t playing. So I just put a hardcoded palette that looks good with my theme. It’s nothing important.

I’m glad you enjoyed my plugin !

If I understood correctly, this only does horizontal ?

If you can’t do opacity, maybe color interpolation will work, since the color of the background is --background-primary no matter the theme (source)

PS: I don’t have notifications on here, you are much more likely to reach me through github, especially for things related to an open issue !

Yup, just flipped it and took out the JFM…D month labels cause they acted funcky when flipped.

End result with the two days logged:

The dataviewjs query looks like

const property = "property";
const defaultValue = 0;

everyDayCalendar(this.container, 2025, d => {
    const name = d.toISOString().substring(0, 10);
    const page = dv.page(name);
    if (page && page[property] !== undefined) {
        return page[property];
    } else {
        return defaultValue;
    }
});