mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 08:38:32 +02:00
Add calendar heatmap display format (#1759)
* Add calendar heatmap exporter Fix #743 * Lint fixes * More lint fixes * Surface total number of entries per month in heatmap * Refactoring * More refactoring * Resolve last lint error * Unbump version * Add calendar export test scaffolding * WIP: Test debugging + scaffolding * Remove broken tests * Remove args from .vscode/launch.json * Discard changes to tests/bdd/features/format.feature * Remove extraneous vscode files * move NestedDict to utils file * run formatter * fix import error * Address lints --------- Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com> Co-authored-by: Jonathan Wren <jonathan@nowandwren.com>
This commit is contained in:
parent
2f0c5d2eb9
commit
a8bd0bcd44
4 changed files with 155 additions and 8 deletions
|
@ -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
|
||||
|
@ -14,14 +15,15 @@ from jrnl.plugins.xml_exporter import XMLExporter
|
|||
from jrnl.plugins.yaml_exporter import YAMLExporter
|
||||
|
||||
__exporters = [
|
||||
CalendarHeatmapExporter,
|
||||
DatesExporter,
|
||||
FancyExporter,
|
||||
JSONExporter,
|
||||
MarkdownExporter,
|
||||
TagExporter,
|
||||
DatesExporter,
|
||||
TextExporter,
|
||||
XMLExporter,
|
||||
YAMLExporter,
|
||||
FancyExporter,
|
||||
]
|
||||
__importers = [JRNLImporter]
|
||||
|
||||
|
|
117
jrnl/plugins/calendar_heatmap_exporter.py
Normal file
117
jrnl/plugins/calendar_heatmap_exporter.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
# 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.plugins.text_exporter import TextExporter
|
||||
from jrnl.plugins.util import get_journal_frequency_nested
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jrnl.journals import Entry
|
||||
from jrnl.journals import Journal
|
||||
from jrnl.plugins.util import NestedDict
|
||||
|
||||
|
||||
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
|
||||
hit_first_entry = False
|
||||
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
|
||||
|
||||
entries_this_month = sum(month_journaling_freq[month].values())
|
||||
if not hit_first_entry and entries_this_month > 0:
|
||||
hit_first_entry = True
|
||||
|
||||
if entries_this_month == 0 and not hit_first_entry:
|
||||
continue
|
||||
elif entries_this_month == 0:
|
||||
entry_msg = "No entries"
|
||||
elif entries_this_month == 1:
|
||||
entry_msg = "1 entry"
|
||||
else:
|
||||
entry_msg = f"{entries_this_month} entries"
|
||||
table = Table(
|
||||
title=f"{calendar.month_name[month]} {year} ({entry_msg})",
|
||||
title_style="bold green",
|
||||
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
|
||||
)
|
||||
day = str(day)
|
||||
# TODO: Make colors configurable?
|
||||
if journal_frequency_for_day == 0:
|
||||
day_label = Text(day, style="red on black")
|
||||
elif journal_frequency_for_day == 1:
|
||||
day_label = Text(day, style="black on yellow")
|
||||
elif journal_frequency_for_day == 2:
|
||||
day_label = Text(day, style="black on green")
|
||||
else:
|
||||
day_label = Text(day, 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_nested(journal)
|
||||
return cls.print_calendar_heatmap(journal_entry_date_frequency)
|
|
@ -1,10 +1,10 @@
|
|||
# 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.plugins.text_exporter import TextExporter
|
||||
from jrnl.plugins.util import get_journal_frequency_one_level
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jrnl.journals import Entry
|
||||
|
@ -24,10 +24,6 @@ class DatesExporter(TextExporter):
|
|||
@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_one_level(journal)
|
||||
result = "\n".join(f"{date}, {count}" for date, count in date_counts.items())
|
||||
return result
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from collections import Counter
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jrnl.journals import Journal
|
||||
|
||||
|
||||
class NestedDict(dict):
|
||||
"""https://stackoverflow.com/a/74873621/8740440"""
|
||||
|
||||
def __missing__(self, x):
|
||||
self[x] = NestedDict()
|
||||
return self[x]
|
||||
|
||||
|
||||
def get_tags_count(journal: "Journal") -> set[tuple[int, str]]:
|
||||
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
||||
# Astute reader: should the following line leave you as puzzled as me the first time
|
||||
|
@ -29,3 +38,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_nested(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_one_level(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
|
||||
|
|
Loading…
Add table
Reference in a new issue