diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 15 | ||||
-rwxr-xr-x | plotasn.py | 118 |
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() |