Skip to content

Commit

Permalink
Add calendar heatmap exporter
Browse files Browse the repository at this point in the history
Fix #743
  • Loading branch information
alichtman committed Jun 21, 2023
1 parent 2a05aad commit c9e24e5
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 7 deletions.
7 changes: 7 additions & 0 deletions jrnl/datatypes/NestedDict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""https://stackoverflow.com/a/74873621/8740440"""


class NestedDict(dict):
def __missing__(self, x):
self[x] = NestedDict()
return self[x]
1 change: 1 addition & 0 deletions jrnl/datatypes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .NestedDict import NestedDict
6 changes: 4 additions & 2 deletions jrnl/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from typing import Type

from jrnl.plugins.calendar_heatmap_exporter import CalendarHeatmapExporter
from jrnl.plugins.dates_exporter import DatesExporter
from jrnl.plugins.fancy_exporter import FancyExporter
from jrnl.plugins.jrnl_importer import JRNLImporter
Expand All @@ -14,14 +15,15 @@
from jrnl.plugins.yaml_exporter import YAMLExporter

__exporters = [
CalendarHeatmapExporter,
DatesExporter,
FancyExporter,
JSONExporter,
MarkdownExporter,
TagExporter,
DatesExporter,
TextExporter,
XMLExporter,
YAMLExporter,
FancyExporter,
]
__importers = [JRNLImporter]

Expand Down
110 changes: 110 additions & 0 deletions jrnl/plugins/calendar_heatmap_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Copyright © 2012-2023 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html

import calendar
from datetime import datetime
from typing import TYPE_CHECKING

from rich import box
from rich.align import Align
from rich.columns import Columns
from rich.console import Console
from rich.table import Table
from rich.text import Text

from jrnl.datatypes import NestedDict
from jrnl.plugins.text_exporter import TextExporter
from jrnl.plugins.util import get_journal_frequency_as_dict

if TYPE_CHECKING:
from jrnl.journals import Entry
from jrnl.journals import Journal


class CalendarHeatmapExporter(TextExporter):
"""This Exporter displays a calendar heatmap of the journaling frequency."""

names = ["calendar", "heatmap"]
extension = "cal"

@classmethod
def export_entry(cls, entry: "Entry"):
raise NotImplementedError

@classmethod
def print_calendar_heatmap(cls, journal_frequency: NestedDict) -> str:
"""Returns a string representation of the calendar heatmap."""
console = Console()
cal = calendar.Calendar()
curr_year = datetime.now().year
curr_month = datetime.now().month
curr_day = datetime.now().day
with console.capture() as capture:
for year, month_journaling_freq in journal_frequency.items():
year_calendar = []
for month in range(1, 13):
if month > curr_month and year == curr_year:
break
table = Table(
title=f"{calendar.month_name[month]} {year}",
style="white",
box=box.SIMPLE_HEAVY,
padding=0,
)

for week_day in cal.iterweekdays():
table.add_column(
"{:.3}".format(calendar.day_name[week_day]), justify="right"
)

month_days = cal.monthdayscalendar(year, month)
for weekdays in month_days:
days = []
for _, day in enumerate(weekdays):
if day == 0: # Not a part of this month, just filler.
day_label = Text(str(day or ""), style="white")
elif (
day > curr_day
and month == curr_month
and year == curr_year
):
break
else:
journal_frequency_for_day = (
month_journaling_freq[month][day] or 0
)
# TODO: Make colors configurable?
if journal_frequency_for_day == 0:
day_label = Text(
str(day or ""), style="red on black"
)
elif journal_frequency_for_day == 1:
day_label = Text(
str(day or ""), style="black on yellow"
)
elif journal_frequency_for_day == 2:
day_label = Text(
str(day or ""), style="black on green"
)
else:
day_label = Text(
str(day or ""), style="black on white"
)

days.append(day_label)
table.add_row(*days)

year_calendar.append(Align.center(table))

# Print year header line
console.rule(str(year))
console.print()
# Print calendar
console.print(Columns(year_calendar, padding=1, expand=True))
return capture.get()

@classmethod
def export_journal(cls, journal: "Journal"):
"""Returns dates and their frequencies for an entire journal."""
journal_entry_date_frequency = get_journal_frequency_as_dict(journal)
return cls.print_calendar_heatmap(journal_entry_date_frequency)
7 changes: 2 additions & 5 deletions jrnl/plugins/dates_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import TYPE_CHECKING

from jrnl.plugins.text_exporter import TextExporter
from jrnl.plugins.util import get_journal_frequency_as_str

if TYPE_CHECKING:
from jrnl.journals import Entry
Expand All @@ -24,10 +25,6 @@ def export_entry(cls, entry: "Entry"):
@classmethod
def export_journal(cls, journal: "Journal") -> str:
"""Returns dates and their frequencies for an entire journal."""
date_counts = Counter()
for entry in journal.entries:
# entry.date.date() gets date without time
date = str(entry.date.date())
date_counts[date] += 1
date_counts = get_journal_frequency_as_str(journal)
result = "\n".join(f"{date}, {count}" for date, count in date_counts.items())
return result
26 changes: 26 additions & 0 deletions jrnl/plugins/util.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Copyright © 2012-2023 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html

from collections import Counter
from typing import TYPE_CHECKING

from jrnl.datatypes import NestedDict

if TYPE_CHECKING:
from jrnl.journals import Journal

Expand All @@ -28,3 +31,26 @@ def oxford_list(lst: list) -> str:
return lst[0] + " or " + lst[1]
else:
return ", ".join(lst[:-1]) + ", or " + lst[-1]


def get_journal_frequency_as_dict(journal: "Journal") -> NestedDict:
"""Returns a NestedDict of the form {year: {month: {day: count}}}"""
journal_frequency = NestedDict()
for entry in journal.entries:
date = entry.date.date()
if date.day in journal_frequency[date.year][date.month]:
journal_frequency[date.year][date.month][date.day] += 1
else:
journal_frequency[date.year][date.month][date.day] = 1

return journal_frequency


def get_journal_frequency_as_str(journal: "Journal") -> Counter:
"""Returns a Counter of the form {date (YYYY-MM-DD): count}"""
date_counts = Counter()
for entry in journal.entries:
# entry.date.date() gets date without time
date = str(entry.date.date())
date_counts[date] += 1
return date_counts

0 comments on commit c9e24e5

Please sign in to comment.