change to col-auto everywhere, with some more forcing of width maximums to make the page more consistent, redid defaults to incorporate updated per fortnight lease cost, and updated inflation figure/notes/references. Updated default Living expenses to 84000 to match latest view of data, fixed up compare_to to now work, shows a graph of data to compare with, allows drop-down to be compare_to nothing or a saved set, only 1 hand saved for now. Annotations on graph for large changes in savings now work and are legible - had to allow overlap and do some overly complex left/right up/down offsetting to make them all sensible

This commit is contained in:
2025-02-13 17:12:02 +11:00
parent 3cf1f1d4de
commit 14de3f1790
5 changed files with 148 additions and 128 deletions

26
calc.py
View File

@@ -1,9 +1,16 @@
# calc.py
from datetime import datetime, timedelta
def add_annotation(finance, dt, amt, text):
def add_annotation(finance, dt, total, delta, text):
# dont add an annotation for small changes (jic)
if abs(delta) < 5000:
return
tm = dt.timestamp() * 1000
finance['annotations'].append( { 'label': text, 'x': tm, 'y': amt } )
if delta > 0:
text += f": ${int(abs(delta))}"
else:
text += f": -${int(abs(delta))}"
finance['annotations'].append( { 'label': text, 'x': tm, 'y': total } )
return
def calculate_savings_depletion(finance):
@@ -98,6 +105,7 @@ def calculate_savings_depletion(finance):
if D_Num_fortnights_pay == 0 and D_leave_after_tax > 0:
print(f"D has resigned {current_date}: get paid out my 9 weeks leave and lose 45% to tax - ${D_leave_after_tax}" )
current_savings += D_leave_after_tax
add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
D_leave_after_tax = 0
if fortnight_income:
@@ -125,28 +133,28 @@ def calculate_savings_depletion(finance):
if current_date.date() == school_fees_date.date():
current_savings -= School_Fees
add_annotation(finance, current_date, current_savings, f"Pay School Fees: ${School_Fees}")
add_annotation(finance, current_date, current_savings, -School_Fees, "School Fees")
if current_date.date() == car_balloon_date.date():
current_savings -= Car_balloon
add_annotation(finance, current_date, current_savings, f"car balloon paid: ${Car_balloon}" )
add_annotation(finance, current_date, current_savings, -Car_balloon, "car balloon")
# Anniversary of Car balloon so pay insurance/rego
if current_date.year >= car_balloon_date.year and current_date.month == car_balloon_date.month and current_date.day == car_balloon_date.day:
current_savings -= post_lease_car_costs
add_annotation(finance, current_date, current_savings, f"IONIQ 6 ins/rego: ${post_lease_car_costs}" )
add_annotation(finance, current_date, current_savings, -post_lease_car_costs, "IONIQ 6 ins/rego" )
if current_date.date() == overseas_trip_date.date():
current_savings -= Overseas_trip
add_annotation(finance, current_date, current_savings, f"Overseas trip: ${Overseas_trip}")
add_annotation(finance, current_date, current_savings, -Overseas_trip, "O/S trip")
if current_date.date() == mich_present_date.date():
current_savings -= Mich_present
add_annotation(finance, current_date, current_savings, f"Michelle's present: ${Mich_present}")
add_annotation(finance, current_date, current_savings, -Mich_present, "Mich's present" )
if current_date.date() == mark_reno_date.date():
current_savings -= Mark_reno
add_annotation(finance, current_date, current_savings, f"Mark/reno costs: ${Mark_reno}")
add_annotation(finance, current_date, current_savings, -Mark_reno, "Mark/reno" )
if current_savings < 0:
depletion_date = current_date
@@ -177,7 +185,7 @@ def calculate_savings_depletion(finance):
Sell_shares -= 1
current_savings += actual_sell
add_annotation(finance, current_date, current_savings, f"Selling shares: ${int(actual_sell)}" )
add_annotation(finance, current_date, current_savings, actual_sell, "Sell shares" )
current_date += timedelta(days=1)

44
db.py
View File

@@ -34,7 +34,8 @@ def init_db():
CBA_price REAL,
Overseas_trip_date STRING,
Mark_reno_date STRING,
Sell_shares INTEGER
Sell_shares INTEGER,
compare_to INTEGER
)''')
cur.execute('''CREATE TABLE IF NOT EXISTS comparison_set (
@@ -82,14 +83,15 @@ def init_db():
# Interest rate
# D_leave_owed_in_days
# maybe quarterly update Inflation? (this is harder to appreciate, seems much lower officialy than Savings, but which inflation:
# I've decided to consider food only or overall, whichever is worse - but only found a rabobank reference - THIS IS A MATERIAL ISSUE WITH PROJECTING OUT)
# I've decided to consider food only or overall, whichever is worse - using this: https://tradingeconomics.com/australia/food-inflation
# FOR NOW inf=3 THIS IS A MATERIAL ISSUE WITH PROJECTING OUT)
###
cur.execute('''INSERT INTO finance (D_Salary, D_Num_fortnights_pay, School_Fees, Car_loan_via_pay, Car_loan, Car_balloon, Living_Expenses, Savings, Interest_Rate,
Inflation, Mich_present, Overseas_trip, Mark_reno, D_leave_owed_in_days, D_TLS_shares, M_TLS_shares, D_CBA_shares, TLS_price, CBA_price, Overseas_trip_date, Mark_reno_date, Sell_shares)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(4762.29, 6, 22000, 620, 1001.12, 45824.68, 83000, 416670.67, 5.0, 3.9, 10000, 32000, 10000, 93.64, 1000, 750, 1095, 3.94, 158.57, '2025-06-01', '2025-09-01', 6))
# NOTE: 1001.12 car-pay -- is 1017.99 (actual rate) - 16.87 (car park)
# NOTE: o/s trip. ~ $4kpp flights x3, then ~$3k / week in barcelona accom, $100pp/pd for food ($2k), + spending money
Inflation, Mich_present, Overseas_trip, Mark_reno, D_leave_owed_in_days, D_TLS_shares, M_TLS_shares, D_CBA_shares, TLS_price, CBA_price, Overseas_trip_date, Mark_reno_date, Sell_shares, compare_to)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(4762.29, 6, 22000, 620, 1017.99, 45824.68, 84000, 416670.67, 5.0, 3.0, 10000, 32000, 10000, 93.64, 1000, 750, 1095, 3.92, 167.03, '2025-06-01', '2025-09-01', 6, 0))
# NOTE: 1017.99 car-lease (NEED TO VERIFY)
# NOTE: o/s trip. ~ $4kpp flights x3, then ~$3k / week in barcelona accom, $100pp/pd for food ($2k), + spending money (could be less)
conn.commit()
conn.close()
@@ -112,8 +114,8 @@ def get_budget_data(finance_data):
return BUDGET
def update_finance(data):
conn = connect_db()
print(data)
conn = connect_db(False)
cur = conn.cursor()
cur.execute('''UPDATE finance SET
D_Salary = ?,
@@ -137,14 +139,15 @@ def update_finance(data):
CBA_price = ?,
Overseas_trip_date = ?,
Mark_reno_date = ?,
Sell_shares = ?
Sell_shares = ?,
compare_to = ?
WHERE id = 1''', data)
conn.commit()
conn.close()
def insert_cset( data ):
conn = connect_db()
conn = connect_db(False)
cur = conn.cursor()
cur.execute('''INSERT INTO comparison_set (
name, D_Salary, D_Num_fortnights_pay, School_Fees, Car_loan_via_pay,
@@ -181,12 +184,12 @@ def get_comp_set_data(cset_id):
conn = connect_db(True)
cur = conn.cursor()
# HARDCODED FOR NOW
cset_id = 1
# get saved finance data for this comparison set
cur.execute( f"select * from comparison_set where id = {cset_id}" )
COMP['vars']= dict(cur.fetchone())
res=cur.fetchone()
if not res:
return None
COMP['vars']= dict(res)
conn.close()
# open new connection so we get rows back as basic array
@@ -201,3 +204,14 @@ def get_comp_set_data(cset_id):
COMP['date'], COMP['amount'] = COMP['savings_data'][-1]
return COMP
def get_comp_set_options(finance):
finance['COMP_SETS']=[ ( 0, 'Nothing' ) ]
# get comp data from DB (as object so dict conversion works below)
conn = connect_db(False)
cur = conn.cursor()
# get saved finance data for this comparison set
cur.execute( f"select id, name from comparison_set order by id" )
finance['COMP_SETS'].extend( cur.fetchall() )
return

View File

@@ -22,7 +22,7 @@ class FP_VAR(FP):
dot-notation of fields
"""
def __init__(self, label, varname, display='', cl='col-2', datevarname=''):
def __init__(self, label, varname, display='', cl='col-auto', datevarname=''):
### Var Attributes -- note, simple class, no methods ###
self.label=label
self.varname=varname

22
main.py
View File

@@ -1,7 +1,7 @@
# main.py
from flask import Flask, render_template, request, redirect, url_for, Response
from calc import calculate_savings_depletion
from db import init_db, get_finance_data, update_finance, get_budget_data, get_comp_set_data
from db import init_db, get_finance_data, update_finance, get_budget_data, get_comp_set_data, get_comp_set_options
from collections import defaultdict
from datetime import datetime
import csv
@@ -16,18 +16,15 @@ init_db()
@app.route('/')
def index():
finance_data = get_finance_data()
get_comp_set_options(finance_data)
depletion_date, savings_per_fortnight, final_savings = calculate_savings_depletion(finance_data)
BUDGET=get_budget_data(finance_data)
if depletion_date:
depletion_date=depletion_date.date(); # just show date
#HARDCODE HACK:
finance_data['comp_set']=1
# we are comparing...
if finance_data['comp_set'] >= 1:
COMP=get_comp_set_data(finance_data['comp_set'])
# if we are comparing...(compare_to will be 0 / None to start with, and then COMP will be None
COMP=get_comp_set_data(finance_data['compare_to'])
DISP=[]
# Row 1
@@ -36,7 +33,7 @@ def index():
r.append( FP_VAR( 'Savings', 'Savings' ) )
r.append( FP_VAR( 'Car Loan via Pay', 'Car_loan_via_pay', 'readonly' ) )
r.append( FP_VAR( 'Living Expenses', 'Living_Expenses' ) )
r.append( FP_VAR( 'Overseas Trip', 'Overseas_trip', 'date', 'col-4', 'Overseas_trip_date' ) )
r.append( FP_VAR( 'Overseas Trip', 'Overseas_trip', 'date', 'col-auto', 'Overseas_trip_date' ) )
DISP.append(r)
# Row 2
@@ -45,12 +42,12 @@ def index():
r.append( FP_VAR( 'Interest Rate', 'Interest_Rate' ) )
r.append( FP_VAR( 'Car Loan', 'Car_loan', 'readonly' ) )
r.append( FP_VAR( 'Inflation', 'Inflation' ) )
r.append( FP_VAR( 'Reno Costs', 'Mark_reno', 'date', 'col-4', 'Mark_reno_date' ) )
r.append( FP_VAR( 'Reno Costs', 'Mark_reno', 'date', 'col-auto', 'Mark_reno_date' ) )
DISP.append(r)
# Row 2
r=[]
r.append( FP_VAR( 'D leave owed (in days)', 'D_leave_owed_in_days' ) )
r.append( FP_VAR( 'D # days leave', 'D_leave_owed_in_days' ) )
r.append( FP_VAR( 'M TLS amount', 'M_TLS_shares' ) )
r.append( FP_VAR( 'Car Balloon', 'Car_balloon', 'readonly' ) )
DISP.append(r)
@@ -60,7 +57,7 @@ def index():
r.append( FP_VAR( 'D CBA amount', 'D_CBA_shares' ) )
r.append( FP_VAR( 'D TLS amount', 'D_TLS_shares' ) )
r.append( FP_VAR( 'Mich Present', 'Mich_present', 'readonly' ) )
r.append( FP_VAR( 'Sell Shares for:', 'Sell_shares', 'select', 'offset-2 col-2' ) )
r.append( FP_VAR( 'Sell Shares for:', 'Sell_shares', 'select', 'col-auto' ) )
DISP.append(r)
# Row 4
@@ -100,6 +97,7 @@ def update():
request.form['Overseas_trip_date'],
request.form['Mark_reno_date'],
request.form['Sell_shares'],
request.form['compare_to']
)
update_finance(finance_data)
@@ -110,7 +108,7 @@ def update():
def download_csv():
finance_data = get_finance_data()
depletion_date, savings_per_fortnight, final_savings, TLS, CBA = calculate_savings_depletion(finance_data)
depletion_date, savings_per_fortnight, final_savings = calculate_savings_depletion(finance_data)
BUDGET=get_budget_data(finance_data)
# Group data by year

View File

@@ -8,38 +8,34 @@
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/annotations.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
<style>
.col-form-label {
width:170px;
}
.col-form-label { width:140px; }
</style>
</head>
<body>
<div class="container-fluid">
<div class="containerfluid">
<h3 align="center">Finance Tracker</h3>
<form id="vals_form" class="mt-3" action="/update" method="POST">
<form id="vals_form" class="ms-3 mt-3" action="/update" method="POST">
{% for r in DISP %}
<div class="row align-items-center">
{% for el in r %}
<div class="{{el.cl}}">
<div class="input-group">
{% if el.display=="select" %}
<label for="{{el.varname}}" class="input-group-text">{{el.label}}</label>
<select class="form-select border border-primary text-primary" id="{{el.varname}}" name="{{el.varname}}" onchange="this.form.submit()">
<label for="{{el.varname}}" class="col-form-label me-2 text-end float-end">{{el.label}}</label>
<select class="form-select border border-primary text-primary" id="{{el.varname}}" name="{{el.varname}}" style="width: 120px;" onchange="this.form.submit()">
<option value="0">Never</option>
<option value="1">1 years</option>
<option value="2">2 years</option>
<option value="3">3 years</option>
<option value="4">4 years</option>
<option value="5">5 years</option>
<option value="6">6 years</option>
{% for el in range( 1,7 ) %}
<option value="{{el}}">{{el}} years</option>
{% endfor %}
</select>
{% elif el.display=="date" %}
<label for="{{el.varname}}" class="col-form-label me-2 text-end float-end">{{el.label}}</label>
<input type="number" step="any" class="form-control text-end float-end border border-primary" onchange="this.form.submit()"
<input type="number" step="any" class="form-control text-end float-end border border-primary" onchange="this.form.submit()" style="max-width: 120px;"
id="{{el.varname}}" name="{{el.varname}}" value="{{ finance[el.varname] }}" {{el.display}}>
<input type="date" class="form-control text-end float-end border border-primary" id="{{el.datevarname}}"
<input type="date" class="form-control text-end float-end border border-primary" id="{{el.datevarname}}" style="max-width: 150px;"
name="{{el.datevarname}}" value="{{ finance[el.datevarname] }}" onchange="this.form.submit()">
{% else %}
<label for="{{el.varname}}" class="col-form-label me-2 text-end float-end">{{el.label}}</label>
@@ -50,7 +46,7 @@
{% set bg="" %}
{% set bd="border-1 border-primary" %}
{% endif %}
<input type="number" step="any" class="{{bg}} form-control text-end float-end {{bd}}" onchange="this.form.submit()"
<input type="number" step="any" class="{{bg}} form-control text-end float-end {{bd}}" onchange="this.form.submit()" style="max-width: 120px;"
id="{{el.varname}}" name="{{el.varname}}" value="{{ finance[el.varname] }}" {{el.display}}>
{% endif %}
</div>
@@ -58,24 +54,23 @@
{% endfor %}
</div>
{% endfor %}
</form>
<h5 align="center" class="mt-4">Fortnighthly Savings data:
{% if COMP %}
<font color="blue">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Note: value in blue below is value we should have been at when comparing to saved values</font>
{# get comparison date so we can use it below in loop to know when to print it out #}
{% set comp_yr=COMP['date'][:4] %}
{% set comp_mon=COMP['date'][5:7] %}
{% set comp_day=COMP['date'][8:10 ] %}
{% set comp_done=namespace( val=0 ) %}
{% else %}
{# we dont need to do a comparison, so consider it done before we begin #}
{% set comp_done=namespace( val=1 ) %}
{% endif %}
</h5>
<div class="row">
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center" style="background:lemonchiffon">2025</div>
{# get comparison date so we can use it below in loop to know when to print it out #}
{% set comp_yr=COMP['date'][:4] %}
{% set comp_mon=COMP['date'][5:7] %}
{% set comp_day=COMP['date'][8:10 ] %}
{% set comp_done=namespace( val=0 ) %}
{% set first_yr=2025 %}
{% for date, dollars in savings %}
{% set yr=date[:4] %}
@@ -103,6 +98,9 @@
{% else %}
<div class="alert alert-success">Super kicks in!!!</div>
{% endif %}
{% if COMP %}
<div class="alert alert-info">Note: value in blue<br>above is value we<br>should have been<br>at when comparing<br> to saved values</div>
{% endif %}
</div>
<div class="col-auto">
<div class="alert alert-warning">
@@ -115,18 +113,7 @@
</div>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary" onClick="$('#vals_form').submit()">Update</button>
</div>
<div class="col-auto">
<div class="input-group">
<button type="submit" class="disabled btn btn-primary" onClick="$('#vals_form').submit() disabled">Compare with:</button>
<select class="form-select border border-primary text-primary" id="comp_set" name="comp_set" onchange="">
<option value="0">None</option>
<option value="1">something</option>
</select>
</div>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" onClick="alert('not yet'); return false">Save</button>
<button type="button" class="btn btn-primary" onClick="
$.ajax( { type: 'GET', url: '/download_csv', xhrFields: { responseType: 'blob' },
success: function(res){
@@ -142,24 +129,42 @@
console.log('done') } })
"> Export to CSV </button>
</div>
<div class="col-auto">
<div class="input-group">
<button type="submit" class="disabled btn btn-primary" onClick="$('#vals_form').submit() disabled">Compare to:</button>
<select class="form-select border border-primary text-primary" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
{% for el in finance['COMP_SETS'] %}
<option value="{{el[0]}}">{{el[1]}}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</form>
<div class="row mt-4" id="container" style="width:100%; height:400px;"></div>
<script type="text/javascript">
window.onload = function() {
document.getElementById("Sell_shares").value = {{finance['Sell_shares']}};
};
$('#Sell_shares').val( {{finance['Sell_shares']}} )
$('#compare_to').val( {{finance['compare_to']}} )
};
document.addEventListener('DOMContentLoaded', function () {
// Parse the savings_data from Flask
const savingsData = JSON.parse('{{ COMP['savings_data'] | tojson }}');
const savingsData = JSON.parse('{{ savings | tojson }}');
const chartData = savingsData.map(entry => [new Date(entry[0]).getTime(), parseFloat(entry[1])]);
{% if COMP %}
const compSavingsData = JSON.parse('{{ COMP['savings_data'] | tojson }}');
const compChartData = compSavingsData.map(entry => [new Date(entry[0]).getTime(), parseFloat(entry[1])]);
{% endif %}
// Get the legend name and vars for the tooltip
const legendName = '{{ COMP["vars"]["name"] }}';
const vars = JSON.parse('{{ COMP["vars"] | tojson }}');
const legendName = 'Savings';
const vars = JSON.parse('{{ finance | tojson }}');
// Tooltip content from vars
const tooltipContent = Object.entries(vars).map(([key, value]) => `${key}: ${value}`).join('<br>');
console.log(tooltipContent)
// Calculate plot bands for each year with alternating background colors
const plotBands = [];
@@ -188,63 +193,58 @@
color: year % 2 === 0 ? 'white' : 'lemonchiffon'
});
// Add annotations for changes greater than 5000
const annotations = [];
{% for a in finance['annotations'] %}
console.log( "{{a['x']}}" )
console.log( "{{a['y']}}" )
console.log( "{{a['label']}}" )
annotations.push({
labels: [{
point: {
x: {{a['x']}},
y: {{a['y']}},
xAxis: 0,
yAxis: 0
},
text: '{{a['label']}}'
}]
});
{% endfor %}
var offset=13
var al='left'
var x=-130
var done=0
{% if not COMP %}
// Add annotations for changes greater than 5000
{% for a in finance['annotations'] %}
annotations.push({
labels: [{
point: {
x: {{a['x']}},
y: {{a['y']}},
crop: true,
xAxis: 0,
yAxis: 0
},
x: x,
y: offset,
align: al,
text: '{{a['label']}}'
}], labelOptions: { allowOverlap: true }
});
if( offset == 150 ) { offset=100 } else { offset=150 }
if( done == 2 ) {
if( al=='right' ) { console.log('change to left'); al='left'; x=-130 } else { console.log('change to right'); al='right'; x=130 }
done=0
}
done++
{% endfor %}
document.keep = annotations
{% endif %}
// Highcharts configuration
Highcharts.chart('container', {
chart: {
type: 'line'
},
title: {
text: 'Savings Over Time'
},
chart: { type: 'line' },
title: { text: 'Savings Over Time' },
xAxis: {
type: 'datetime',
title: {
text: 'Date'
},
title: { text: 'Date' },
plotBands: plotBands // Alternating background for years
},
yAxis: {
title: {
text: 'Amount ($)'
}
},
legend: {
labelFormatter: function () {
return `<span title="${tooltipContent}">${legendName}</span>`;
}
},
tooltip: {
pointFormat: '{point.x:%Y-%m-%d}: <b>{point.y:.2f}</b>'
},
yAxis: { title: { text: 'Amount ($)' } },
legend: { labelFormatter: function () { return `<span title="${tooltipContent}">${legendName}</span>`; } },
tooltip: { pointFormat: '{point.x:%Y-%m-%d}: <b>{point.y:.2f}</b>' },
annotations: annotations, // Add annotations
series: [{
name: legendName,
data: chartData,
marker: {
radius: 2, // Smaller points (default is 4)
lineWidth: 0, // Optional: thinner border
symbol: 'circle' // Optional: shape of the points (default is 'circle')
}
}]
series: [
{ name: legendName, data: chartData, marker: { radius: 2 } }
{% if COMP %}
,{ name: "TEST", data: compChartData, marker: { radius: 2 } }
{% endif %}
]
});
});
</script>