Remove old plot implementation; add new plot on home
This commit is contained in:
parent
63908cd837
commit
07bb604eda
15 changed files with 47 additions and 149 deletions
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
31
nummi/main/static/main/js/history_plot.js
Normal file
31
nummi/main/static/main/js/history_plot.js
Normal 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);
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
# Register your models here.
|
|
|
@ -1,6 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class PlotConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "plot"
|
|
|
@ -1 +0,0 @@
|
||||||
# Create your models here.
|
|
|
@ -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
|
|
|
@ -1 +0,0 @@
|
||||||
# Create your tests here.
|
|
|
@ -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"),
|
|
||||||
]
|
|
|
@ -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"})
|
|
Loading…
Reference in a new issue