Remove old plot implementation; add new plot on home

This commit is contained in:
Edgar P. Burkhart 2023-04-16 17:26:43 +02:00
parent 63908cd837
commit 07bb604eda
Signed by: edpibu
GPG key ID: 9833D3C5A25BD227
15 changed files with 47 additions and 149 deletions

View file

@ -1,5 +1,5 @@
from django.db.models import Sum from django.db.models import Sum, FloatField, Q
from django.db.models.functions import ExtractQuarter, ExtractYear from django.db.models.functions import TruncMonth
from django.http import JsonResponse from django.http import JsonResponse
from django.views import View from django.views import View
from django.views.generic.list import MultipleObjectMixin from django.views.generic.list import MultipleObjectMixin
@ -44,8 +44,17 @@ class HistoryView(UserMixin, MultipleObjectMixin, View):
{ {
"data": list( "data": list(
self.get_queryset() self.get_queryset()
.values("category__name", quarter=ExtractQuarter("date"), year=ExtractYear("date")) .values(month=TruncMonth("date"))
.annotate(Sum("value")) .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")
) )
} }
) )

View file

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

View file

@ -20,8 +20,6 @@
{{ form }} {{ form }}
</form> </form>
{% if form.instance.transactions %} {% if form.instance.transactions %}
<img src="{% url "plot-category" form.instance.id %}"
alt="Graph representing value over time"/>
<h2>{% translate "Transactions" %}</h2> <h2>{% translate "Transactions" %}</h2>
{% include "main/table/transaction.html" %} {% include "main/table/transaction.html" %}
{% endif %} {% endif %}

View file

@ -10,6 +10,7 @@
<link rel="stylesheet" <link rel="stylesheet"
href="{% static 'main/css/table.css' %}" href="{% static 'main/css/table.css' %}"
type="text/css"/> type="text/css"/>
<script src="{% static 'main/js/history_plot.js' %}" type="module"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% if accounts %} {% if accounts %}
@ -35,6 +36,7 @@
{% endfor %} {% endfor %}
</div> </div>
{% endspaceless %} {% endspaceless %}
<div id="history_plot"></div>
{% endif %} {% endif %}
{% if snapshots %} {% if snapshots %}
<h2>{% translate "Snapshots" %}</h2> <h2>{% translate "Snapshots" %}</h2>

View file

@ -233,9 +233,7 @@ class SnapshotUpdateView(NummiUpdateView):
) )
if _transactions: if _transactions:
data["categories"] = ( data["categories"] = (
_transactions.values( _transactions.values("category", "category__name", "category__icon")
"category", "category__name", "category__icon"
)
.annotate( .annotate(
sum=models.Sum("value"), sum=models.Sum("value"),
sum_m=models.Sum("value", filter=models.Q(value__lt=0)), sum_m=models.Sum("value", filter=models.Q(value__lt=0)),

View file

@ -19,7 +19,6 @@ from django.urls import include, path
urlpatterns = i18n_patterns( urlpatterns = i18n_patterns(
path("", include("main.urls")), path("", include("main.urls")),
path("plot/", include("plot.urls")),
path("api/", include("api.urls")), path("api/", include("api.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
prefix_default_language=False, prefix_default_language=False,

View file

View file

@ -1 +0,0 @@
# Register your models here.

View file

@ -1,6 +0,0 @@
from django.apps import AppConfig
class PlotConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "plot"

View file

@ -1 +0,0 @@
# Create your models here.

View file

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

View file

@ -1 +0,0 @@
# Create your tests here.

View file

@ -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/<uuid>", views.category, name="plot-category"),
]

View file

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