Add calendar heatmap exporter

Fix #743
This commit is contained in:
Aaron Lichtman 2023-06-21 06:18:17 -07:00
parent 2a05aad0e9
commit c9e24e5d3b
No known key found for this signature in database
GPG key ID: D046D019DC745EDA
6 changed files with 150 additions and 7 deletions

View file

@ -0,0 +1,7 @@
"""https://stackoverflow.com/a/74873621/8740440"""
class NestedDict(dict):
def __missing__(self, x):
self[x] = NestedDict()
return self[x]

View file

@ -0,0 +1 @@
from .NestedDict import NestedDict

View file

@ -3,6 +3,7 @@
from typing import Type from typing import Type
from jrnl.plugins.calendar_heatmap_exporter import CalendarHeatmapExporter
from jrnl.plugins.dates_exporter import DatesExporter from jrnl.plugins.dates_exporter import DatesExporter
from jrnl.plugins.fancy_exporter import FancyExporter from jrnl.plugins.fancy_exporter import FancyExporter
from jrnl.plugins.jrnl_importer import JRNLImporter from jrnl.plugins.jrnl_importer import JRNLImporter
@ -14,14 +15,15 @@ from jrnl.plugins.xml_exporter import XMLExporter
from jrnl.plugins.yaml_exporter import YAMLExporter from jrnl.plugins.yaml_exporter import YAMLExporter
__exporters = [ __exporters = [
CalendarHeatmapExporter,
DatesExporter,
FancyExporter,
JSONExporter, JSONExporter,
MarkdownExporter, MarkdownExporter,
TagExporter, TagExporter,
DatesExporter,
TextExporter, TextExporter,
XMLExporter, XMLExporter,
YAMLExporter, YAMLExporter,
FancyExporter,
] ]
__importers = [JRNLImporter] __importers = [JRNLImporter]

View file

@ -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)

View file

@ -5,6 +5,7 @@ from collections import Counter
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from jrnl.plugins.text_exporter import TextExporter from jrnl.plugins.text_exporter import TextExporter
from jrnl.plugins.util import get_journal_frequency_as_str
if TYPE_CHECKING: if TYPE_CHECKING:
from jrnl.journals import Entry from jrnl.journals import Entry
@ -24,10 +25,6 @@ class DatesExporter(TextExporter):
@classmethod @classmethod
def export_journal(cls, journal: "Journal") -> str: def export_journal(cls, journal: "Journal") -> str:
"""Returns dates and their frequencies for an entire journal.""" """Returns dates and their frequencies for an entire journal."""
date_counts = Counter() date_counts = get_journal_frequency_as_str(journal)
for entry in journal.entries:
# entry.date.date() gets date without time
date = str(entry.date.date())
date_counts[date] += 1
result = "\n".join(f"{date}, {count}" for date, count in date_counts.items()) result = "\n".join(f"{date}, {count}" for date, count in date_counts.items())
return result return result

View file

@ -1,8 +1,11 @@
# Copyright © 2012-2023 jrnl contributors # Copyright © 2012-2023 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html # License: https://www.gnu.org/licenses/gpl-3.0.html
from collections import Counter
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from jrnl.datatypes import NestedDict
if TYPE_CHECKING: if TYPE_CHECKING:
from jrnl.journals import Journal from jrnl.journals import Journal
@ -28,3 +31,26 @@ def oxford_list(lst: list) -> str:
return lst[0] + " or " + lst[1] return lst[0] + " or " + lst[1]
else: else:
return ", ".join(lst[:-1]) + ", or " + lst[-1] 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