Browse Source

Basic charting and parsing hooked up

It's primitive now, but a good start.
master
Macoy Madson 11 months ago
parent
commit
4a3b96598e
11 changed files with 21073 additions and 6 deletions
  1. +22
    -0
      .clang-format
  2. +8
    -5
      LazyBudgetServer.py
  3. +47
    -0
      webInterface/Chart.css
  4. +16151
    -0
      webInterface/Chart.js
  5. +44
    -0
      webInterface/Files.js
  6. +4602
    -0
      webInterface/Moment.js
  7. +83
    -0
      webInterface/TransactionData.js
  8. +25
    -0
      webInterface/Upload.html
  9. +37
    -0
      webInterface/Visualization.js
  10. +52
    -0
      webInterface/index.html
  11. +2
    -1
      webInterfaceNoAuth/index.css

+ 22
- 0
.clang-format View File

@@ -0,0 +1,22 @@
# http://releases.llvm.org/6.0.0/tools/clang/docs/ClangFormatStyleOptions.html
BasedOnStyle: Google
AccessModifierOffset: -4
AllowShortBlocksOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
BreakBeforeBraces: Allman
BraceWrapping:
AfterNamespace: false
BreakBeforeTernaryOperators: false
ColumnLimit: 100
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
IndentWidth: 4
Standard: Cpp11
TabWidth: 4
UseTab: ForIndentation
DerivePointerAlignment: false
PointerAlignment: Left
NamespaceIndentation: None
IndentCaseLabels: true

+ 8
- 5
LazyBudgetServer.py View File

@@ -8,6 +8,7 @@ import tornado.gen

import os
import random
import webbrowser

# Require a username and password in order to use the web interface. See ReadMe.org for details.
#enable_authentication = False
@@ -144,9 +145,7 @@ class AuthedStaticHandler(tornado.web.StaticFileHandler):
class HomeHandler(AuthHandler):
@tornado.web.authenticated
def get(self):
# Replace with your desired behavior, e.g.
# self.render('webInterface/index.html')
self.write('You are logged in!')
self.render('webInterface/index.html')

class ExampleWebSocket(tornado.websocket.WebSocketHandler):
connections = set()
@@ -190,7 +189,7 @@ def make_app():
# (r'/ExampleWebSocket', ExampleWebSocket),

# Static files
# (r'/webInterface/(.*)', AuthedStaticHandler, {'path' : 'webInterface'}),
(r'/webInterface/(.*)', AuthedStaticHandler, {'path' : 'webInterface'}),

# Files served regardless of whether the user is authenticated. Only login page resources
# should be in this folder, because anyone can see them
@@ -222,6 +221,10 @@ if __name__ == '__main__':
print('\n\tWARNING: Do NOT run this server on the internet (e.g. port-forwarded)'
' nor when\n\t connected to an insecure LAN! It is not protected against malicious use.\n')
app.listen(port)

browseUrl ="{}://localhost:{}".format('https' if useSSL else 'http', port)
print("Attempting to launch user's default browser to {}".format(browseUrl))
webbrowser.open(browseUrl)

ioLoop = tornado.ioloop.IOLoop.current()
ioLoop.start()

+ 47
- 0
webInterface/Chart.css View File

@@ -0,0 +1,47 @@
/*
* DOM element rendering detection
* https://davidwalsh.name/detect-node-insertion
*/
@keyframes chartjs-render-animation {
from { opacity: 0.99; }
to { opacity: 1; }
}

.chartjs-render-monitor {
animation: chartjs-render-animation 0.001s;
}

/*
* DOM element resizing detection
* https://github.com/marcj/css-element-queries
*/
.chartjs-size-monitor,
.chartjs-size-monitor-expand,
.chartjs-size-monitor-shrink {
position: absolute;
direction: ltr;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
pointer-events: none;
visibility: hidden;
z-index: -1;
}

.chartjs-size-monitor-expand > div {
position: absolute;
width: 1000000px;
height: 1000000px;
left: 0;
top: 0;
}

.chartjs-size-monitor-shrink > div {
position: absolute;
width: 200%;
height: 200%;
left: 0;
top: 0;
}

+ 16151
- 0
webInterface/Chart.js
File diff suppressed because it is too large
View File


+ 44
- 0
webInterface/Files.js View File

@@ -0,0 +1,44 @@
// Thanks https://www.html5rocks.com/en/tutorials/file/dndfiles/
function handleFileSelect(evt)
{
var files = evt.target.files; // FileList object

// files is a FileList of File objects. List some properties.
var output = [];
for (var i = 0, file; file = files[i]; i++)
{
/* output.push(
'<li><strong>', escape(file.name), '</strong> (', file.type || 'n/a', ') - ', file.size,
' bytes, last modified: ',
file.lastModifiedDate ? file.lastModifiedDate.toLocaleDateString() : 'n/a',
!file.type.match('text.csv') ? ' (unsupported format)' : ' (CSV supported)', '</li>');
*/
var reader = new FileReader();
reader.onload = function() {
var transactionTable = document.getElementById('transactionTable');
transactionTable.innerHTML = '<thead></thead><tbody>';

var text = reader.result;
var lines = text.split('\n');
// Note: Skip row 0, which has header labels
for (var lineIndex = 1; lineIndex < lines.length; ++lineIndex)
{
line = lines[lineIndex];
var logEntry = makeTransactionLogEntryFromCSV(line);
/* console.log(logEntry); */
if (logEntry)
{
gTransactionData.push(logEntry);
}
}

processTransactionData();
updateVisualizations();
transactionTable.innerHTML += '</tbody>';
};
reader.readAsText(file);
}
}

document.getElementById('filesUpload').addEventListener('change', handleFileSelect, false);

+ 4602
- 0
webInterface/Moment.js
File diff suppressed because it is too large
View File


+ 83
- 0
webInterface/TransactionData.js View File

@@ -0,0 +1,83 @@
var gTransactionData = [];
var gBalanceOverTime = [];
var gTransactionsByCategory = {};

// Recalculate charts now that transaction data changed
function processTransactionData()
{
gBalanceOverTime = [];
for (var i = 0; i < gTransactionData.length; ++i)
{
gBalanceOverTime.push({x: gTransactionData[i].date, y: gTransactionData[i].balance});

// Separate by category
if (gTransactionData[i].userCategories) {
for (var categoryIndex = 0; categoryIndex < gTransactionData[i].userCategories.length; ++categoryIndex) {
categoryName = gTransactionData[i].userCategories[categoryIndex];
if (categoryName in gTransactionsByCategory) {
gTransactionsByCategory[categoryName].push(gTransactionData[i]);
}
else {
gTransactionsByCategory[categoryName] = [gTransactionData[i]];
}
}
}
}
}

let dateRegex = /(.*)\/(.*)\/(.*)/

// Assume format account,date,amount,balance,category,description,memo,notes\r
function makeTransactionLogEntryFromCSV(csvLine)
{
splitLine = csvLine.trim().split(',');
if (splitLine.length != 8)
{
console.log('Error: CSV line has unexpected formatting: \n' + csvLine);
return null;
}

dateMatch = splitLine[1].match(dateRegex);

var newEntry = {
account: splitLine[0],
// E.g. 12/17/2019
// Month is zero indexed in JS
date: new Date(dateMatch[3], dateMatch[1] - 1, dateMatch[2], 1, 1, 1, 1),
amount: splitLine[2],
balance: parseFloat(splitLine[3].replace("$", "")),
bankCategory: splitLine[4],
description: splitLine[5],
memo: splitLine[6],
notes: splitLine[7],
userCategories: []
};

categorizeEntry(newEntry);

return newEntry;
}

function addUserCategory(entry, newCategory) {
if (entry.userCategories.indexOf(newCategory) === -1) {
entry.userCategories.push(newCategory);
}
}


function categorizeEntry(entry)
{
// These are provided by my bank. Not sure I'm happy they're analyzing my transactions... :(
if (entry.bankCategory == 'DiningOut') {
addUserCategory(entry, 'Food');
}
else if (entry.bankCategory == 'Travel') {
addUserCategory(entry, 'Travel');
}
else if (entry.bankCategory == 'Entertainment') {
addUserCategory(entry, 'Entertainment');
}
else if (entry.bankCategory == 'Automobile') {
addUserCategory(entry, 'Gas');
}
}

+ 25
- 0
webInterface/Upload.html View File

@@ -0,0 +1,25 @@
<html>
<head>
<title>Upload Transactions</title>

<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- For mobile: set scale to native -->
<meta name="viewport" content="width=device-width, initial-scale=1">

<link rel="stylesheet" type="text/css" href="/webInterfaceNoAuth/index.css">
</head>
<body>
<h1>Upload Transactions</h1>
<a href="/">Back to Homepage</a><br /><br />
<p>Lazy Budget cannot access your bank account automatically.
If you want this functionality, I recommend using <a href="https://www.mint.com/">Mint</a>, though you may not have your privacy respected with that service.</p>
<h2>Supported Formats</h2>
<p>Lazy Budget supports the following formats:</p>
<ul>
<li>CSV</li>
</ul>
</body>
</html>

+ 37
- 0
webInterface/Visualization.js View File

@@ -0,0 +1,37 @@
// This script uses Chart.js
// See https://www.chartjs.org/docs/latest/

var ctx = document.getElementById('myChart').getContext('2d');
// Bar graph
/* var myChart = new Chart(ctx, {
* type: 'bar',
* data: {
* labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
* datasets: [{
* label: '# of Votes',
* data: [12, 19, 3, 5, 2, 3],
* backgroundColor: [
* 'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)',
* 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)'
* ],
* borderColor: [
* 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)',
* 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'
* ],
* borderWidth: 1
* }]
* },
* options: {scales: {yAxes: [{ticks: {beginAtZero: true}}]}}
* }); */

var gBalanceOverTimeChart = new Chart(ctx, {
type: 'line',
data: {datasets: [{label: 'Balance over time', data: gBalanceOverTime, backgroundColor: 'rgba(200, 30, 30, 0.2)'}]},
options: {scales: {xAxes: [{type: 'time'}]}}
});

function updateVisualizations()
{
gBalanceOverTimeChart.data.datasets[0].data = gBalanceOverTime;
gBalanceOverTimeChart.update();
}

+ 52
- 0
webInterface/index.html View File

@@ -0,0 +1,52 @@
<html>
<head>
<title>Lazy Budget</title>

<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- For mobile: set scale to native -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/webInterfaceNoAuth/index.css">
</head>
<body>
<h1>Lazy Budget</h1>

<script type="text/javascript" src="/webInterface/TransactionData.js"></script>
<!--
Visualization
-->
<!-- Note that this size will be changed automatically by Chart.js -->
<canvas id="myChart" width="400" height="400"></canvas><br />
<!-- Moment: Required by Chart for time support; https://momentjs.com/ -->
<script type="text/javascript" src="/webInterface/Moment.js"></script>
<!-- From https://github.com/chartjs/Chart.js -->
<script type="text/javascript" src="/webInterface/Chart.js"></script>
<!-- Code using Chart.js for Lazy Budget -->
<script type="text/javascript" src="/webInterface/Visualization.js"></script>

<!-- File upload -->
<input type="file" id="filesUpload" name="files[]" multiple />
<output id="output"></output>
<script type="text/javascript" src="/webInterface/Files.js"></script>
<!-- Raw data -->
<table id="transactionTable">
</table>
<nav>
<a href="/webInterface/Upload.html">Upload Transactions</a><br />
<!-- <a href="/settings">Settings</a><br /><br /> -->

<!-- Less frequent options example -->
<!-- <a class="small" href="/unsupportedSubmissions">Content which failed to download</a><br />
<br />
<a class="small" href="/logout">Sign Out</a><br /> -->
</nav>
<footer>
<a href="https://github.com/makuto/Lazy-Budget">See the code</a>
<a href="mailto:macoy@macoy.me">Give feedback</a>
</footer>
</body>
</html>

+ 2
- 1
webInterfaceNoAuth/index.css View File

@@ -94,7 +94,8 @@ tr,
td,
input,
textarea,
button {
button,
output {
color: #95aec7;
}



Loading…
Cancel
Save