From 0940904cd8dd651bec8b9acde927a79197e18c7e Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart" <git@edgarpierre.fr>
Date: Tue, 2 Jan 2024 10:58:30 +0100
Subject: [PATCH] Refactor History plot

---
 nummi/history/templates/history/plot.html | 94 ++++++-----------------
 nummi/history/utils.py                    | 38 ++++-----
 nummi/main/static/main/css/plot.css       | 34 ++++----
 nummi/main/templatetags/main_extras.py    | 69 ++++++++++++++++-
 4 files changed, 130 insertions(+), 105 deletions(-)

diff --git a/nummi/history/templates/history/plot.html b/nummi/history/templates/history/plot.html
index 399bc4e..8d9bad4 100644
--- a/nummi/history/templates/history/plot.html
+++ b/nummi/history/templates/history/plot.html
@@ -18,57 +18,23 @@
             </tr>
         </thead>
         <tbody>
-            {% for y in history.years reversed %}
-                {% for date in y.d reversed %}
-                    {% ifchanged %}
-                        {% if date %}
-                            <tr {% if not date.month.month|divisibleby:"2" %}class="even"{% endif %}>
-                                <td class="icon">
-                                    <span class="ri-{% if date.sum > 0 %}arrow-up-s-line green{% elif date.sum < 0 %}arrow-down-s-line red{% endif %}"></span>
-                                </td>
-                                <th class="date" scope="row">
-                                    {% if date.has_transactions %}
-                                        {% if account %}
-                                            <a href="{% url "account_transaction_month" account=account.pk year=date.month.year month=date.month.month %}">{{ date.month|date:"Y-m" }}</a>
-                                        {% elif category %}
-                                            <a href="{% url "category_transaction_month" category=category.pk year=date.month.year month=date.month.month %}">{{ date.month|date:"Y-m" }}</a>
-                                        {% else %}
-                                            <a href="{% url "transaction_month" year=date.month.year month=date.month.month %}">{{ date.month|date:"Y-m" }}</a>
-                                        {% endif %}
-                                    {% else %}
-                                        {{ date.month|date:"Y-m" }}
-                                    {% endif %}
-                                </th>
-                                <td class="value">{{ date.sum_m|pmrvalue }}</td>
-                                <td class="bar m">
-                                    {% if date.sum_m %}
-                                        <div style="width: {% widthratio date.sum_m history.max -100 %}%"></div>
-                                    {% endif %}
-                                    {% if date.sum < 0 %}
-                                        <div class="tot" style="width:{% widthratio date.sum history.max -100 %}%">
-                                            <span>{{ date.sum|pmrvalue }}</span>
-                                        </div>
-                                    {% endif %}
-                                </td>
-                                <td class="bar p">
-                                    {% if date.sum_p %}
-                                        <div style="width: {% widthratio date.sum_p history.max 100 %}%"></div>
-                                    {% endif %}
-                                    {% if date.sum > 0 %}
-                                        <div class="tot" style="width:{% widthratio date.sum history.max 100 %}%">
-                                            <span>{{ date.sum|pmrvalue }}</span>
-                                        </div>
-                                    {% endif %}
-                                </td>
-                                <td class="value">{{ date.sum_p|pmrvalue }}</td>
-                            </tr>
-                        {% else %}
-                            <tr class="empty">
-                                <td colspan="6" class="empty"></td>
-                            </tr>
-                        {% endif %}
-                    {% endifchanged %}
-                {% endfor %}
+            {% for date in history.data reversed %}
+                {% ifchanged %}
+                    {% if date.sum_m or date.sum_p %}
+                        <tr {% if not date.month.month|divisibleby:"2" %}class="even"{% endif %}>
+                            <td class="icon">{% up_down_icon date.sum %}</td>
+                            <th class="date" scope="row">{% month_url date.month account=account category=category %}</th>
+                            <td class="value">{{ date.sum_m|pmrvalue }}</td>
+                            <td class="bar m">{% plot_bar date.sum date.sum_m history.max.pm %}</td>
+                            <td class="bar p">{% plot_bar date.sum date.sum_p history.max.pm %}</td>
+                            <td class="value">{{ date.sum_p|pmrvalue }}</td>
+                        </tr>
+                    {% else %}
+                        <tr class="empty">
+                            <td colspan="6" class="empty"></td>
+                        </tr>
+                    {% endif %}
+                {% endifchanged %}
             {% endfor %}
         </tbody>
     </table>
@@ -78,36 +44,26 @@
         <thead>
             <tr>
                 <th scope="col">{% translate "Year" %}</th>
-                <th scope="col">01</th>
-                <th scope="col">02</th>
-                <th scope="col">03</th>
-                <th scope="col">04</th>
-                <th scope="col">05</th>
-                <th scope="col">06</th>
-                <th scope="col">07</th>
-                <th scope="col">08</th>
-                <th scope="col">09</th>
-                <th scope="col">10</th>
-                <th scope="col">11</th>
-                <th scope="col">12</th>
+                {% calendar_head %}
             </tr>
         </thead>
         <tbody>
-            {% for year in history.years reversed %}
+            {% regroup history.data by month.year as years_list %}
+            {% for y, year in years_list reversed %}
                 <tr>
-                    <th>{{ year.y }}</th>
-                    {% for m in year.d %}
+                    <th>{{ y }}</th>
+                    {% for m in year %}
                         {% if forloop.parentloop.last and forloop.first %}
-                            {% for _ in history.offset.0 %}<td></td>{% endfor %}
+                        {% empty_calendar_cells_start m.month.month %}
                         {% endif %}
                         {% if m %}
                             <td class="{% if m.sum > 0 %}p{% else %}m{% endif %}"
-                                style="opacity: {% opacity m.sum history.years_max %}"></td>
+                                style="opacity: {% opacity m.sum history.max.sum %}"></td>
                         {% else %}
                             <td></td>
                         {% endif %}
                         {% if forloop.parentloop.first and forloop.last %}
-                            {% for _ in history.offset.1 %}<td></td>{% endfor %}
+                        {% empty_calendar_cells_end m.month.month %}
                         {% endif %}
                     {% endfor %}
                 </tr>
diff --git a/nummi/history/utils.py b/nummi/history/utils.py
index b15e792..423d34e 100644
--- a/nummi/history/utils.py
+++ b/nummi/history/utils.py
@@ -1,6 +1,6 @@
 import datetime
 
-from django.db.models import Func, Max, Min, Q, Sum, Value
+from django.db.models import Func, Max, Min, Q, Sum
 from django.db.models.functions import Abs, TruncMonth
 
 
@@ -23,34 +23,30 @@ def history(transaction_set):
         sum_p=Sum("value", filter=Q(value__gt=0)),
         sum_m=Sum("value", filter=Q(value__lt=0)),
         sum=Sum("value"),
-        has_transactions=Value(1),
     ).order_by("-month")
 
     _data = [
-        {
-            "y": y,
-            "d": [
-                _history.filter(month=datetime.date(y, m + 1, 1)).first()
-                for m in range(
-                    _first_month.month - 1 if _first_month.year == y else 0,
-                    _last_month.month if _last_month.year == y else 12,
-                )
-            ],
-        }
+        _history.filter(month=datetime.date(y, m + 1, 1)).first()
+        or {"month": datetime.date(y, m + 1, 1), "sum": 0}
         for y in range(
             _first_month.year,
             _last_month.year + 1,
         )
+        for m in range(
+            _first_month.month - 1 if _first_month.year == y else 0,
+            _last_month.month if _last_month.year == y else 12,
+        )
     ]
 
     return {
-        "years": _data,
-        "offset": [range(_first_month.month - 1), range(12 - _last_month.month)],
-        "max": max(
-            _history.aggregate(
-                max=Max("sum_p", default=0),
-                min=-Min("sum_m", default=0),
-            ).values(),
-        ),
-        "years_max": _history.aggregate(max=Max(Abs("sum")))["max"],
+        "data": _data,
+        "max": {
+            "pm": max(
+                _history.aggregate(
+                    max=Max("sum_p", default=0),
+                    min=-Min("sum_m", default=0),
+                ).values(),
+            ),
+            "sum": _history.aggregate(max=Max(Abs("sum")))["max"],
+        },
     }
diff --git a/nummi/main/static/main/css/plot.css b/nummi/main/static/main/css/plot.css
index 631d3c4..0ec7189 100644
--- a/nummi/main/static/main/css/plot.css
+++ b/nummi/main/static/main/css/plot.css
@@ -65,12 +65,17 @@ table.full-width col.bar {
       }
     }
   }
-  tr.empty {
-    height: 0.5rem;
-  }
-  tr.even {
-    background: #eeeeff;
+
+  tbody tr {
+    background: initial;
+    &.empty {
+      height: 0.5rem;
+    }
+    &.even {
+      background: #eeeeff;
+    }
   }
+
   tfoot {
     background: var(--bg-01);
   }
@@ -78,22 +83,23 @@ table.full-width col.bar {
 
 .calendar {
   margin-top: var(--gap);
+  font-feature-settings: var(--num);
 
-  tbody tr {
-    background: initial;
-  }
   .p {
     background: var(--green);
   }
   .m {
     background: var(--red);
   }
-  tbody tr {
-    &:not(:last-child) {
-      border-bottom: none;
-    }
-    &:not(:first-child) {
-      border-top: none;
+  table {
+    tbody tr {
+      background: initial;
+      &:not(:last-child) {
+        border-bottom: none;
+      }
+      &:not(:first-child) {
+        border-top: none;
+      }
     }
   }
 }
diff --git a/nummi/main/templatetags/main_extras.py b/nummi/main/templatetags/main_extras.py
index 670bda9..8fca38e 100644
--- a/nummi/main/templatetags/main_extras.py
+++ b/nummi/main/templatetags/main_extras.py
@@ -1,7 +1,10 @@
+import datetime
 import math
 
 from django import template
+from django.template.defaultfilters import date
 from django.templatetags.static import static
+from django.urls import reverse
 from django.utils import formats
 from django.utils.safestring import mark_safe
 
@@ -73,4 +76,68 @@ def css(href):
 
 @register.simple_tag
 def opacity(v, vmax):
-    return f"{math.sin(math.fabs(v/vmax)*math.pi/2):.2f}"
+    return f"{math.sin(math.fabs(v/vmax)*math.pi/2):.3f}"
+
+
+@register.simple_tag
+def empty_calendar_cells(n):
+    return mark_safe(n * "<td></td>")
+
+
+@register.simple_tag
+def empty_calendar_cells_start(n):
+    return empty_calendar_cells(n - 1)
+
+
+@register.simple_tag
+def empty_calendar_cells_end(n):
+    return empty_calendar_cells(12 - n)
+
+
+@register.simple_tag
+def up_down_icon(val):
+    if val > 0:
+        return mark_safe("""<span class="ri-arrow-up-s-line green"></span>""")
+    elif val < 0:
+        return mark_safe("""<span class="ri-arrow-down-s-line red"></span>""")
+
+
+@register.simple_tag
+def month_url(month, account=None, category=None):
+    url_name = "transaction_month"
+    url_params = {"year": month.year, "month": month.month}
+
+    if account:
+        url_name = "account_" + url_name
+        url_params |= {"account": account.pk}
+    elif category:
+        url_name = "category_" + url_name
+        url_params |= {"category": category.pk}
+
+    url = reverse(url_name, kwargs=url_params)
+    return mark_safe(f"""<a href="{url}">{ date(month, "Y-m") }</a>""")
+
+
+@register.simple_tag
+def plot_bar(s, sum_pm, s_max):
+    _res = ""
+
+    if sum_pm:
+        _w = abs(sum_pm / s_max)
+        _res += f"""<div style="width: {_w:.1%}"></div>"""
+    if sum_pm is not None and s * sum_pm > 0:
+        _w = abs(s / s_max)
+        _res += (
+            f"""<div class="tot" style="width: {_w:.1%}">"""
+            f"""<span>{pmrvalue(s)}</span></div>"""
+        )
+
+    return mark_safe(_res)
+
+
+@register.simple_tag
+def calendar_head():
+    months = (datetime.date(1, m + 1, 1) for m in range(12))
+    th = (f"""<th>{date(month, "m")}</th>""" for month in months)
+
+    return mark_safe("".join(th))
