From 07bb604eda4c2c988b9d0c03c1863657ddab9303 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sun, 16 Apr 2023 17:26:43 +0200 Subject: [PATCH] Remove old plot implementation; add new plot on home --- nummi/api/views.py | 17 ++- nummi/main/static/main/js/history_plot.js | 31 ++++++ nummi/main/templates/main/category_form.html | 2 - nummi/main/templates/main/index.html | 2 + nummi/main/views.py | 4 +- nummi/nummi/urls.py | 1 - nummi/plot/__init__.py | 0 nummi/plot/admin.py | 1 - nummi/plot/apps.py | 6 -- nummi/plot/migrations/__init__.py | 0 nummi/plot/models.py | 1 - nummi/plot/nummi.mplstyle | 13 --- nummi/plot/tests.py | 1 - nummi/plot/urls.py | 9 -- nummi/plot/views.py | 108 ------------------- 15 files changed, 47 insertions(+), 149 deletions(-) create mode 100644 nummi/main/static/main/js/history_plot.js delete mode 100644 nummi/plot/__init__.py delete mode 100644 nummi/plot/admin.py delete mode 100644 nummi/plot/apps.py delete mode 100644 nummi/plot/migrations/__init__.py delete mode 100644 nummi/plot/models.py delete mode 100644 nummi/plot/nummi.mplstyle delete mode 100644 nummi/plot/tests.py delete mode 100644 nummi/plot/urls.py delete mode 100644 nummi/plot/views.py diff --git a/nummi/api/views.py b/nummi/api/views.py index 876d5fd..2d46911 100644 --- a/nummi/api/views.py +++ b/nummi/api/views.py @@ -1,5 +1,5 @@ -from django.db.models import Sum -from django.db.models.functions import ExtractQuarter, ExtractYear +from django.db.models import Sum, FloatField, Q +from django.db.models.functions import TruncMonth from django.http import JsonResponse from django.views import View from django.views.generic.list import MultipleObjectMixin @@ -44,8 +44,17 @@ class HistoryView(UserMixin, MultipleObjectMixin, View): { "data": list( self.get_queryset() - .values("category__name", quarter=ExtractQuarter("date"), year=ExtractYear("date")) - .annotate(Sum("value")) + .values(month=TruncMonth("date")) + .annotate( + sum_p=Sum( + "value", output_field=FloatField(), filter=Q(value__gt=0) + ), + sum_m=Sum( + "value", output_field=FloatField(), filter=Q(value__lt=0) + ), + sum=Sum("value", output_field=FloatField()), + ) + .order_by("month") ) } ) diff --git a/nummi/main/static/main/js/history_plot.js b/nummi/main/static/main/js/history_plot.js new file mode 100644 index 0000000..ddec5bd --- /dev/null +++ b/nummi/main/static/main/js/history_plot.js @@ -0,0 +1,31 @@ +import {json} from "https://cdn.skypack.dev/d3-fetch@3"; +import {timeParse} from "https://cdn.skypack.dev/d3-time-format@4"; +import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm"; + +const history = await json("/api/history"); +const parseDate = timeParse("%Y-%m-%d"); +console.log(history); +history["data"].forEach((d) => { + d.month = parseDate(d.month); +}); +const data = history["data"]; + +let history_plot = Plot.plot({ + x: { + round: true, + label: "Month", + grid: true, + }, + y: { + grid: true, + }, + marks: [ + Plot.areaY(data, {x: "month", y: "sum_p", interval: "month", curve: "bump-x", fill: "#aaccff"}), + Plot.areaY(data, {x: "month", y: "sum_m", interval: "month", curve: "bump-x", fill: "#ffccaa"}), + Plot.ruleY([0]), + Plot.lineY(data, {x: "month", y: "sum", interval: "month", curve: "bump-x", marker: "circle", strokeWidth: 2}), + ], + insetTop: 20, + insetBottom: 20, +}); +document.querySelector("#history_plot").append(history_plot); diff --git a/nummi/main/templates/main/category_form.html b/nummi/main/templates/main/category_form.html index b2c04cf..a3b327a 100644 --- a/nummi/main/templates/main/category_form.html +++ b/nummi/main/templates/main/category_form.html @@ -20,8 +20,6 @@ {{ form }} {% if form.instance.transactions %} - Graph representing value over time

{% translate "Transactions" %}

{% include "main/table/transaction.html" %} {% endif %} diff --git a/nummi/main/templates/main/index.html b/nummi/main/templates/main/index.html index 503d0fe..2dc4d85 100644 --- a/nummi/main/templates/main/index.html +++ b/nummi/main/templates/main/index.html @@ -10,6 +10,7 @@ + {% endblock %} {% block body %} {% if accounts %} @@ -35,6 +36,7 @@ {% endfor %} {% endspaceless %} +
{% endif %} {% if snapshots %}

{% translate "Snapshots" %}

diff --git a/nummi/main/views.py b/nummi/main/views.py index 9b078d7..b981fa8 100644 --- a/nummi/main/views.py +++ b/nummi/main/views.py @@ -233,9 +233,7 @@ class SnapshotUpdateView(NummiUpdateView): ) if _transactions: data["categories"] = ( - _transactions.values( - "category", "category__name", "category__icon" - ) + _transactions.values("category", "category__name", "category__icon") .annotate( sum=models.Sum("value"), sum_m=models.Sum("value", filter=models.Q(value__lt=0)), diff --git a/nummi/nummi/urls.py b/nummi/nummi/urls.py index a888c80..372b4cb 100644 --- a/nummi/nummi/urls.py +++ b/nummi/nummi/urls.py @@ -19,7 +19,6 @@ from django.urls import include, path urlpatterns = i18n_patterns( path("", include("main.urls")), - path("plot/", include("plot.urls")), path("api/", include("api.urls")), path("admin/", admin.site.urls), prefix_default_language=False, diff --git a/nummi/plot/__init__.py b/nummi/plot/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nummi/plot/admin.py b/nummi/plot/admin.py deleted file mode 100644 index 846f6b4..0000000 --- a/nummi/plot/admin.py +++ /dev/null @@ -1 +0,0 @@ -# Register your models here. diff --git a/nummi/plot/apps.py b/nummi/plot/apps.py deleted file mode 100644 index 1f7a601..0000000 --- a/nummi/plot/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class PlotConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "plot" diff --git a/nummi/plot/migrations/__init__.py b/nummi/plot/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nummi/plot/models.py b/nummi/plot/models.py deleted file mode 100644 index 6b20219..0000000 --- a/nummi/plot/models.py +++ /dev/null @@ -1 +0,0 @@ -# Create your models here. diff --git a/nummi/plot/nummi.mplstyle b/nummi/plot/nummi.mplstyle deleted file mode 100644 index b49262b..0000000 --- a/nummi/plot/nummi.mplstyle +++ /dev/null @@ -1,13 +0,0 @@ -font.family: Inter - -lines.linewidth: 2 - -figure.autolayout: True -figure.figsize: 8, 4 -figure.dpi: 300 - -axes.prop_cycle: cycler('color', ["66cc66", "338033", "99ff99", "802653", "cc6699"]) -axes.axisbelow: True -axes.grid: True - -svg.fonttype: none diff --git a/nummi/plot/tests.py b/nummi/plot/tests.py deleted file mode 100644 index a39b155..0000000 --- a/nummi/plot/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/nummi/plot/urls.py b/nummi/plot/urls.py deleted file mode 100644 index 1412ba8..0000000 --- a/nummi/plot/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path - -from . import views - -urlpatterns = [ - path("timeline", views.timeline, name="plot-timeline"), - path("categories", views.categories, name="plot-categories"), - path("category/", views.category, name="plot-category"), -] diff --git a/nummi/plot/views.py b/nummi/plot/views.py deleted file mode 100644 index 46ebfa5..0000000 --- a/nummi/plot/views.py +++ /dev/null @@ -1,108 +0,0 @@ -import io - -import matplotlib -import matplotlib.pyplot as plt -from django.contrib.auth.decorators import login_required -from django.db import models -from django.http import HttpResponse -from django.shortcuts import get_object_or_404 -from django.utils.translation import gettext as _ -from matplotlib import dates as mdates - -from main.models import Category, Snapshot, Transaction - -matplotlib.use("Agg") -plt.style.use("./plot/nummi.mplstyle") - - -@login_required -def timeline(request): - _snapshots = Snapshot.objects.all() - - fig, ax = plt.subplots() - ax.step( - [s.date for s in _snapshots], - [s.value for s in _snapshots], - where="post", - ) - ax.set(ylabel=_("Snapshots"), ylim=0) - ax.xaxis.set_major_formatter( - mdates.ConciseDateFormatter(ax.xaxis.get_major_locator()) - ) - ax.autoscale(True, "x", True) - - _io = io.StringIO() - - fig.savefig(_io, format="svg") - - return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"}) - - -@login_required -def categories(request): - _categories = Category.objects.filter(budget=True) - - fig, ax = plt.subplots(figsize=(8, _categories.count() / 4)) - ax.barh( - [str(c) for c in _categories][::-1], - [ - Transaction.objects.filter(category=c).aggregate(sum=models.Sum("value"))[ - "sum" - ] - for c in _categories - ][::-1], - ) - - _io = io.StringIO() - - fig.savefig(_io, format="svg") - - return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"}) - - -@login_required -def category(request, uuid): - _category = get_object_or_404(Category, id=uuid) - _values_p = ( - Transaction.objects.filter(category=_category) - .filter(value__gt=0) - .annotate(m=models.functions.TruncMonth("date")) - .values("m") - .annotate(sum=models.Sum("value")) - .order_by("m") - ) - _values_m = ( - Transaction.objects.filter(category=_category) - .filter(value__lt=0) - .annotate(m=models.functions.TruncMonth("date")) - .values("m") - .annotate(sum=models.Sum("value")) - .order_by("m") - ) - - fig, ax = plt.subplots() - ax.bar( - [v["m"] for v in _values_p], - [v["sum"] for v in _values_p], - width=12, - color="#66cc66", - ) - ax.bar( - [v["m"] for v in _values_m], - [v["sum"] for v in _values_m], - width=12, - color="#cc6699", - ) - ax.xaxis.set_major_formatter( - mdates.ConciseDateFormatter(ax.xaxis.get_major_locator()) - ) - ax.autoscale(True, "x", True) - _ym, _yp = ax.get_ylim() - ax.set(ylim=(min(_ym, 0), max(_yp, 0))) - ax.set(ylabel=f"{_category.name} (€)") - - _io = io.StringIO() - - fig.savefig(_io, format="svg") - - return HttpResponse(_io.getvalue(), headers={"Content-Type": "image/svg+xml"})