HTML/CSS only plot implemented for history on homepage
This commit is contained in:
parent
ee7e6e60a7
commit
eee7131715
16 changed files with 155 additions and 147 deletions
79
nummi/main/static/main/css/plot.css
Normal file
79
nummi/main/static/main/css/plot.css
Normal file
|
@ -0,0 +1,79 @@
|
|||
.plot table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.plot tr {
|
||||
padding-bottom: .5rem;
|
||||
}
|
||||
.plot th {text-align: center}
|
||||
.plot th.r {text-align: right}
|
||||
.plot th.l {text-align: left}
|
||||
|
||||
.plot td, .plot th, .plot td.bar div {
|
||||
position: relative;
|
||||
height: 2rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
.plot td, .plot th {
|
||||
padding: .5rem var(--gap);
|
||||
}
|
||||
|
||||
.plot td:not(.bar), .plot tbody th {
|
||||
width: 8rem;
|
||||
}
|
||||
.plot td.bar {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
.plot td.bar div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.plot td.m {
|
||||
text-align: right;
|
||||
}
|
||||
.plot td.value {
|
||||
padding: 0 var(--gap);
|
||||
font-feature-settings: var(--num);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.plot td.bar div:not(.tot) {
|
||||
width: 0;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
.plot td.bar.p div {
|
||||
left: 0;
|
||||
border-radius: 0 var(--radius) var(--radius) 0;
|
||||
}
|
||||
.plot td.bar.m div {
|
||||
right: 0;
|
||||
border-radius: var(--radius) 0 0 var(--radius);
|
||||
}
|
||||
.plot td.bar.m div:not(.tot) {
|
||||
background: var(--red-1);
|
||||
}
|
||||
.plot td.bar.p div:not(.tot) {
|
||||
background: var(--green-1);
|
||||
}
|
||||
|
||||
.plot td.bar div.tot {
|
||||
z-index: 10;
|
||||
height: .5rem;
|
||||
background: black;
|
||||
}
|
||||
.plot td.bar div.tot span {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
margin: 0 var(--gap);
|
||||
font-weight: 650;
|
||||
top: .5rem;
|
||||
line-height: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-feature-settings: var(--num);
|
||||
}
|
||||
.plot td.bar.p div.tot span {left: 0}
|
||||
.plot td.bar.m div.tot span {right: 0}
|
|
@ -21,8 +21,6 @@
|
|||
{{ form }}
|
||||
</form>
|
||||
{% if form.instance.transactions %}
|
||||
<img src="{% url "plot-category" form.instance.id %}"
|
||||
alt="Graph representing value over time"/>
|
||||
<h2>{% translate "Transactions" %}</h2>
|
||||
{% include "main/table/transaction.html" %}
|
||||
{% endif %}
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
<link rel="stylesheet"
|
||||
href="{% static 'main/css/table.css' %}"
|
||||
type="text/css"/>
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'main/css/plot.css' %}"
|
||||
type="text/css"/>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
{% if accounts %}
|
||||
|
@ -25,6 +28,8 @@
|
|||
{% if transactions %}
|
||||
<h2>{% translate "Transactions" %}</h2>
|
||||
{% include "main/table/transaction.html" %}
|
||||
<h2>{% translate "History" %}</h2>
|
||||
{% include "main/plot/history.html" %}
|
||||
{% endif %}
|
||||
{% if categories %}
|
||||
<h2>{% translate "Categories" %}</h2>
|
||||
|
|
40
nummi/main/templates/main/plot/history.html
Normal file
40
nummi/main/templates/main/plot/history.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% load main_extras %}
|
||||
{% load i18n %}
|
||||
<div class="plot">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% translate "Month" %}</th>
|
||||
<th class="l" scope="col" colspan="2">{% translate "Expenses" %}</th>
|
||||
<th class="r" scope="col" colspan="2">{% translate "Income" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% spaceless %}
|
||||
{% for date in history.data %}
|
||||
<tr>
|
||||
<th scope="row">{{ date.month|date:"M y" }}</th>
|
||||
<td class="value">{{ date.sum_m|pmrvalue }}</td>
|
||||
<td class="bar m">
|
||||
<div style="width: {% widthratio date.sum_m history.max -100 %}%"></div>
|
||||
{% 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">
|
||||
<div style="width: {% widthratio date.sum_p history.max 100 %}%"></div>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
{% endspaceless %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -40,7 +40,7 @@
|
|||
<i class="fa fa-{{ cat.category__icon }}"></i>
|
||||
<a href="{% url 'category' cat.category %}">{{ cat.category__name }}</a>
|
||||
{% else %}
|
||||
<i class="fa fa-wallet"></i></i>
|
||||
<i class="fa fa-wallet"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="value left">{{ cat.sum_m|pmvalue }}</div>
|
||||
|
|
|
@ -6,12 +6,12 @@ register = template.Library()
|
|||
|
||||
|
||||
@register.filter
|
||||
def value(val, pm=False):
|
||||
def value(val, pm=False, r=2):
|
||||
if not val:
|
||||
return mark_safe("–")
|
||||
_prefix = ""
|
||||
_suffix = " €"
|
||||
_val = formats.number_format(val, 2, use_l10n=True, force_grouping=True)
|
||||
_val = formats.number_format(val, r, use_l10n=True, force_grouping=True)
|
||||
|
||||
if val > 0:
|
||||
if pm:
|
||||
|
@ -28,6 +28,11 @@ def pmvalue(val):
|
|||
return value(val, True)
|
||||
|
||||
|
||||
@register.filter
|
||||
def pmrvalue(val):
|
||||
return value(val, True, r=0)
|
||||
|
||||
|
||||
@register.inclusion_tag("main/tag/form_buttons.html")
|
||||
def form_buttons(instance):
|
||||
return {
|
||||
|
|
|
@ -41,12 +41,34 @@ class IndexView(LoginRequiredMixin, TemplateView):
|
|||
_max = 8
|
||||
_transactions = Transaction.objects.filter(user=self.request.user)
|
||||
_snapshots = Snapshot.objects.filter(user=self.request.user)
|
||||
_history = (
|
||||
_transactions.filter(category__budget=True)
|
||||
.values(month=models.functions.TruncMonth("date"))
|
||||
.annotate(
|
||||
sum_p=models.Sum("value", filter=models.Q(value__gt=0)),
|
||||
sum_m=models.Sum("value", filter=models.Q(value__lt=0)),
|
||||
sum=models.Sum("value"),
|
||||
)
|
||||
.order_by("-month")
|
||||
)
|
||||
|
||||
res = {
|
||||
"accounts": Account.objects.filter(user=self.request.user),
|
||||
"transactions": _transactions[:_max],
|
||||
"categories": Category.objects.filter(user=self.request.user),
|
||||
"snapshots": _snapshots[:_max],
|
||||
"history": {
|
||||
"data": _history,
|
||||
"max": max(
|
||||
map(
|
||||
lambda x: abs(x) if x else 0,
|
||||
_history.aggregate(
|
||||
max=models.Max("sum_p"),
|
||||
min=models.Min("sum_m"),
|
||||
).values(),
|
||||
)
|
||||
),
|
||||
},
|
||||
}
|
||||
if _transactions.count() > _max:
|
||||
res["transactions_url"] = reverse_lazy("transactions")
|
||||
|
@ -233,9 +255,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)),
|
||||
|
|
|
@ -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