Compare commits

...

3 Commits

Author SHA1 Message Date
Herculino Trotta 7645153f77 feat(dca): remove ticks from price chart 2025-09-20 11:05:56 -03:00
Herculino Trotta 9dce5e9efe feat(networth): add a chart with the currency difference between each month 2025-09-20 11:02:23 -03:00
Herculino Trotta bb3cc5da6c fix(login): use full dynamic height 2025-09-20 01:40:22 -03:00
6 changed files with 456 additions and 310 deletions
@@ -182,3 +182,29 @@ def calculate_historical_account_balance(queryset):
historical_account_balance[date_filter(end_date, "b Y")] = month_data historical_account_balance[date_filter(end_date, "b Y")] = month_data
return historical_account_balance return historical_account_balance
def calculate_monthly_net_worth_difference(historical_net_worth):
diff_dict = OrderedDict()
if not historical_net_worth:
return diff_dict
# Get all currencies
currencies = set()
for data in historical_net_worth.values():
currencies.update(data.keys())
# Initialize prev_values for all currencies
prev_values = {currency: Decimal("0.00") for currency in currencies}
for month, values in historical_net_worth.items():
diff_values = {}
for currency in sorted(list(currencies)):
current_val = values.get(currency, Decimal("0.00"))
prev_val = prev_values.get(currency, Decimal("0.00"))
diff_values[currency] = current_val - prev_val
diff_dict[month] = diff_values
prev_values = values.copy()
return diff_dict
+34
View File
@@ -8,6 +8,7 @@ from django.views.decorators.http import require_http_methods
from apps.net_worth.utils.calculate_net_worth import ( from apps.net_worth.utils.calculate_net_worth import (
calculate_historical_currency_net_worth, calculate_historical_currency_net_worth,
calculate_historical_account_balance, calculate_historical_account_balance,
calculate_monthly_net_worth_difference,
) )
from apps.transactions.models import Transaction from apps.transactions.models import Transaction
from apps.transactions.utils.calculations import ( from apps.transactions.utils.calculations import (
@@ -96,6 +97,38 @@ def net_worth(request):
chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder) chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder)
monthly_difference_data = calculate_monthly_net_worth_difference(
historical_net_worth=historical_currency_net_worth
)
diff_labels = (
list(monthly_difference_data.keys()) if monthly_difference_data else []
)
diff_currencies = (
list(monthly_difference_data[diff_labels[0]].keys())
if monthly_difference_data and diff_labels
else []
)
diff_datasets = []
for i, currency in enumerate(diff_currencies):
data = [
float(month_data.get(currency, 0))
for month_data in monthly_difference_data.values()
]
diff_datasets.append(
{
"label": currency,
"data": data,
"borderWidth": 3,
}
)
chart_data_monthly_difference = {"labels": diff_labels, "datasets": diff_datasets}
chart_data_monthly_difference_json = json.dumps(
chart_data_monthly_difference, cls=DjangoJSONEncoder
)
historical_account_balance = calculate_historical_account_balance( historical_account_balance = calculate_historical_account_balance(
queryset=transactions_account_queryset queryset=transactions_account_queryset
) )
@@ -140,6 +173,7 @@ def net_worth(request):
"chart_data_accounts_json": chart_data_accounts_json, "chart_data_accounts_json": chart_data_accounts_json,
"accounts": accounts, "accounts": accounts,
"type": view_type, "type": view_type,
"chart_data_monthly_difference_json": chart_data_monthly_difference_json,
}, },
) )
+1 -1
View File
@@ -3,7 +3,7 @@
{% if icon %}<i class="{{ icon }}"></i>{% else %}<span class="fw-bold">{{ title.0 }}</span>{% endif %} {% if icon %}<i class="{{ icon }}"></i>{% else %}<span class="fw-bold">{{ title.0 }}</span>{% endif %}
</div> </div>
<div class="card-body"> <div class="card-body">
<h5 class="tw:text-{{ color }}-400 fw-bold tw:mr-[50px]" {{ attrs }}>{{ title }}{% if help_text %}<c-ui.help-icon :content="help_text" icon=""></c-ui.help-icon>{% endif %}</h5> <h5 class="tw:text-{{ color }}-400 fw-bold tw:mr-[50px] {{ title_css_classes }}" {{ attrs }}>{{ title }}{% if help_text %}<c-ui.help-icon :content="help_text" icon=""></c-ui.help-icon>{% endif %}</h5>
{{ slot }} {{ slot }}
</div> </div>
</div> </div>
@@ -354,6 +354,7 @@
display: false, display: false,
}, },
ticks: { ticks: {
display: false,
format: { maximumFractionDigits: 40, minimumFractionDigits: 1 } format: { maximumFractionDigits: 40, minimumFractionDigits: 1 }
} }
}, },
@@ -369,6 +370,7 @@
display: false, display: false,
}, },
ticks: { ticks: {
display: false,
format: { maximumFractionDigits: 40, minimumFractionDigits: 1 } format: { maximumFractionDigits: 40, minimumFractionDigits: 1 }
} }
} }
+92 -8
View File
@@ -9,7 +9,8 @@
{% block title %}{% if type == "current" %}{% translate 'Current Net Worth' %}{% else %}{% translate 'Projected Net Worth' %}{% endif %}{% endblock %} {% block title %}{% if type == "current" %}{% translate 'Current Net Worth' %}{% else %}{% translate 'Projected Net Worth' %}{% endif %}{% endblock %}
{% block content %} {% block content %}
<div hx-trigger="every 60m, updated from:window" hx-include="#view-type" class="show-loading" hx-get="" hx-target="body"> <div hx-trigger="every 60m, updated from:window" hx-include="#view-type" class="show-loading" hx-get=""
hx-target="body">
<div class="h-100 text-center mb-4 pt-2"> <div class="h-100 text-center mb-4 pt-2">
<div class="btn-group gap-3" role="group" id="view-type" _="on change trigger updated"> <div class="btn-group gap-3" role="group" id="view-type" _="on change trigger updated">
<input type="radio" class="btn-check" <input type="radio" class="btn-check"
@@ -32,17 +33,19 @@
class="fa-solid fa-rocket fa-fw me-2"></i>{% trans 'Projected' %}</label> class="fa-solid fa-rocket fa-fw me-2"></i>{% trans 'Projected' %}</label>
</div> </div>
</div> </div>
<div class="container px-md-3 py-3" _="init call initializeAccountChart() then initializeCurrencyChart() end"> <div class="container px-md-3 py-3"
_="init call initializeAccountChart() then initializeCurrencyChart() then initializeMonthlyDifferenceChart() end">
<div class="row gx-xl-4 gy-3 mb-4"> <div class="row gx-xl-4 gy-3 mb-4">
<div class="col-12 col-xl-5"> <div class="col-12 col-xl-5">
<div class="row row-cols-1 g-4"> <div class="row row-cols-1 g-4">
<div class="col"> <div class="col">
<c-ui.info-card color="yellow" icon="fa-solid fa-coins" title="{% trans 'By currency' %}" <c-ui.info-card color="yellow" icon="fa-solid fa-coins" title="{% trans 'By currency' %}"
title_css_classes="tw:cursor-pointer"
_="on click showAllDatasetsCurrency()"> _="on click showAllDatasetsCurrency()">
{% for currency in currency_net_worth.values %} {% for currency in currency_net_worth.values %}
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100"> <div class="d-flex align-items-baseline w-100">
<div class="currency-name text-start font-monospace tw:text-gray-300" <div class="currency-name text-start font-monospace tw:text-gray-300 tw:cursor-pointer"
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')"> _="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')">
{{ currency.currency.name }} {{ currency.currency.name }}
</div> </div>
@@ -91,10 +94,31 @@
</div> </div>
</div> </div>
<div class="col-12 col-xl-7"> <div class="col-12 col-xl-7">
<div class="chart-container position-relative tw:min-h-[40vh] tw:h-full"> <ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="evolution-tab" data-bs-toggle="tab"
data-bs-target="#evolution-tab-pane" type="button" role="tab" aria-controls="evolution-tab-pane"
aria-selected="true">{% trans 'Evolution' %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="diff-tab" data-bs-toggle="tab" data-bs-target="#diff-tab-pane" type="button"
role="tab" aria-controls="diff-tab-pane" aria-selected="false">{% trans 'Difference' %}</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="evolution-tab-pane" role="tabpanel"
aria-labelledby="evolution-tab" tabindex="0">
<div class="chart-container position-relative tw:min-h-[40vh] tw:h-full tw:w-full">
<canvas id="currencyBalanceChart"></canvas> <canvas id="currencyBalanceChart"></canvas>
</div> </div>
</div> </div>
<div class="tab-pane fade" id="diff-tab-pane" role="tabpanel" aria-labelledby="diff-tab" tabindex="0">
<div class="chart-container position-relative tw:min-h-[40vh] tw:h-full tw:w-full">
<canvas id="monthlyDifferenceChart"></canvas>
</div>
</div>
</div>
</div>
</div> </div>
<hr> <hr>
<div class="row gx-xl-4 gy-3 mt-4"> <div class="row gx-xl-4 gy-3 mt-4">
@@ -102,6 +126,7 @@
<div class="row row-cols-1 g-4"> <div class="row row-cols-1 g-4">
<div class="col"> <div class="col">
<c-ui.info-card color="blue" icon="fa-solid fa-wallet" title="{% trans 'By account' %}" <c-ui.info-card color="blue" icon="fa-solid fa-wallet" title="{% trans 'By account' %}"
title_css_classes="tw:cursor-pointer"
_="on click showAllDatasetsAccount()"> _="on click showAllDatasetsAccount()">
{% regroup account_net_worth.values by account.group as account_data %} {% regroup account_net_worth.values by account.group as account_data %}
{% for data in account_data %} {% for data in account_data %}
@@ -115,7 +140,7 @@
{% for account in data.list %} {% for account in data.list %}
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100"> <div class="d-flex align-items-baseline w-100">
<div class="account-name text-start font-monospace tw:text-gray-300" <div class="account-name text-start font-monospace tw:text-gray-300 tw:cursor-pointer"
_="on click showOnlyAccountDataset('{{ account.account.name }}')"> _="on click showOnlyAccountDataset('{{ account.account.name }}')">
<span class="hierarchy-line-icon"></span>{{ account.account.name }}</div> <span class="hierarchy-line-icon"></span>{{ account.account.name }}</div>
<div class="dotted-line flex-grow-1"></div> <div class="dotted-line flex-grow-1"></div>
@@ -143,7 +168,7 @@
{% for account in data.list %} {% for account in data.list %}
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100"> <div class="d-flex align-items-baseline w-100">
<div class="account-name text-start font-monospace tw:text-gray-300" <div class="account-name text-start font-monospace tw:text-gray-300 tw:cursor-pointer"
_="on click showOnlyAccountDataset('{{ account.account.name }}')"> _="on click showOnlyAccountDataset('{{ account.account.name }}')">
{{ account.account.name }} {{ account.account.name }}
</div> </div>
@@ -310,6 +335,54 @@
} }
</script> </script>
<script id="monthlyDifferenceChartScript">
var monthlyDifferenceChart;
function initializeMonthlyDifferenceChart() {
if (monthlyDifferenceChart) {
monthlyDifferenceChart.destroy();
}
var chartData = JSON.parse('{{ chart_data_monthly_difference_json|safe }}');
var ctx = document.getElementById('monthlyDifferenceChart').getContext('2d');
monthlyDifferenceChart = new Chart(ctx, {
type: 'bar',
data: chartData,
options: {
maintainAspectRatio: false,
responsive: true,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
title: {
display: true,
text: '{% translate "Difference" %}'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
ticks: {
display: false,
format: {maximumFractionDigits: 40, minimumFractionDigits: 0}
},
}
}
}
});
}
</script>
<script type="text/hyperscript"> <script type="text/hyperscript">
def showOnlyAccountDataset(datasetName) def showOnlyAccountDataset(datasetName)
for dataset in accountChart.data.datasets for dataset in accountChart.data.datasets
@@ -325,6 +398,12 @@
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch) call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch)
end end
call currencyChart.update() call currencyChart.update()
for dataset in monthlyDifferenceChart.data.datasets
set isMatch to dataset.label is datasetName
call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), isMatch)
end
call monthlyDifferenceChart.update()
end end
def showAllDatasetsAccount() def showAllDatasetsAccount()
@@ -339,8 +418,13 @@
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), true) call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), true)
end end
call currencyChart.update() call currencyChart.update()
for dataset in monthlyDifferenceChart.data.datasets
call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), true)
end
call monthlyDifferenceChart.update()
end end
</script> </script>
</div> </div>
<c-ui.transactions_fab></c-ui.transactions_fab> <c-ui.transactions_fab></c-ui.transactions_fab>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -9,7 +9,7 @@
{% block content %} {% block content %}
<div> <div>
<div class="container"> <div class="container">
<div class="row vh-100 d-flex justify-content-center align-items-center"> <div class="row tw:h-dvh d-flex justify-content-center align-items-center">
<div class="col-md-6 col-xl-4 col-12"> <div class="col-md-6 col-xl-4 col-12">
{% settings "DEMO" as demo_mode %} {% settings "DEMO" as demo_mode %}
{% if demo_mode %} {% if demo_mode %}