aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md15
-rwxr-xr-xplotasn.py118
3 files changed, 134 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..afed073
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.csv
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f9a8690
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# plotasn.py
+
+This is a simple python application to plot the balance of your [ASN][] bank
+accounts. It combines lines of the individual accounts and their total sum in a
+single graph.
+
+Download CSV exports from the online environment and run:
+
+```bash
+./plotasn.py account-1.csv [account-2.csv ...]
+```
+
+This is tool is hereby placed in the public domain.
+
+[ASN]: https://www.asnbank.nl/
diff --git a/plotasn.py b/plotasn.py
new file mode 100755
index 0000000..0296ba5
--- /dev/null
+++ b/plotasn.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+
+from argparse import ArgumentParser
+import csv
+import datetime as dt
+import matplotlib.pyplot as plt
+import matplotlib.dates as mdates
+import numpy as np
+
+class Record:
+ def __init__(self, date, balance):
+ if type(date) == str:
+ self.datetime = dt.datetime.strptime(date, '%d-%m-%Y')
+ else:
+ self.datetime = date
+ self.balance = balance
+
+ def __repr__(self):
+ return str(self.datetime) + ': ' + str(self.balance)
+
+def read_csv(f):
+ """Read a CSV file into a list of records."""
+ records = []
+ with open(f) as csvfile:
+ rdr = csv.reader(csvfile)
+ for row in rdr:
+ before = float(row[8])
+ after = before + float(row[10])
+ records.append(Record(row[11], before))
+ records.append(Record(row[11], after))
+ return records
+
+def remove_day_internal_records(records):
+ """Keeps only the first and last record for each day."""
+ i = 0
+ while i < len(records):
+ if i + 2 >= len(records):
+ break
+ elif records[i].datetime == records[i+2].datetime:
+ del records[i+1]
+ else:
+ records[i+1].datetime = records[i+1].datetime.replace(hour=23, minute=59, second=59)
+ i += 2
+ if i + 1 < len(records):
+ records[i+1].datetime = records[i+1].datetime.replace(hour=23, minute=59, second=59)
+
+def add_limits(record_lists):
+ """Add records for the start and end of all record lists.
+
+ This fixes files that do not cover the same interval.
+ """
+ mindate = record_lists[0][0].datetime
+ maxdate = record_lists[0][-1].datetime
+
+ for rs in record_lists:
+ mindate = min(mindate, rs[0].datetime)
+ maxdate = max(maxdate, rs[-1].datetime)
+
+ for rs in record_lists:
+ # We add two because this still goes through remove_day_internal_records
+ rs.insert(0, Record(mindate, rs[0].balance))
+ rs.insert(0, Record(mindate, rs[0].balance))
+ rs.append(Record(maxdate, rs[-1].balance))
+ rs.append(Record(maxdate, rs[-1].balance))
+
+def sum_records(axes_lists):
+ """Return a new axes pair for the sum of all given axes pairs."""
+ for i in range(len(axes_lists)):
+ axes_lists[i][0] = [x.timestamp() for x in axes_lists[i][0]]
+ xs = np.unique(np.concatenate([xy[0] for xy in axes_lists]))
+ yss = [np.interp(xs, xy[0], xy[1], left=0, right=0) for xy in axes_lists]
+ ys = sum(yss)
+ return [dt.datetime.fromtimestamp(x) for x in xs], ys
+
+def records_to_axes(records):
+ """Convert a list of records to an axes pair."""
+ xs = []
+ ys = []
+ for r in records:
+ xs.append(r.datetime)
+ ys.append(r.balance)
+ return xs, ys
+
+def plot_files(files):
+ record_lists = [read_csv(f) for f in files]
+ add_limits(record_lists)
+
+ axes_lists = []
+ for rs in record_lists:
+ remove_day_internal_records(rs)
+ xs, ys = records_to_axes(rs)
+ axes_lists.append([xs,ys])
+
+ i = 0
+ for xs, ys in axes_lists:
+ plt.plot(xs, ys, label=files[i])
+ i += 1
+
+ if len(files) > 1:
+ xs, ys = sum_records(axes_lists)
+ plt.plot(xs, ys, label='Sum')
+
+ plt.legend()
+ plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
+ plt.gca().set_ylim((0, None))
+ plt.gcf().autofmt_xdate()
+ plt.show()
+
+def main():
+ p = ArgumentParser(description='Plot balances of ASN bank accounts')
+ p.add_argument('files', metavar='FILE', type=str, nargs='+',
+ help='a CSV export of an ASN bank account')
+ args = p.parse_args()
+
+ plot_files(args.files)
+
+if __name__ == '__main__':
+ main()