├── _config.yml ├── screenshot.png ├── input.sample.json ├── README.md ├── calculator.html └── calculator.js /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srs81/Savings-Calculator/master/screenshot.png -------------------------------------------------------------------------------- /input.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "startDate": "2017-3", 3 | "endDate": "2019-7", 4 | "startingSavings": { 5 | "Cash": 20000, 6 | "401K": 10000, 7 | "House": 40000 8 | }, 9 | "monthlyNumbers": { 10 | "Cash": [ 11 | [5000, "Salary", {}], 12 | [-1000, "Fed taxes", {}], 13 | [-500, "Social security & Medicare", {}], 14 | [-500, "401K from salary", {}], 15 | [-1000, "House mortgage", {"end": "2018-9"}], 16 | [5001, "Bonus", {"start": "2017-6", "recurs": 6}] 17 | ], 18 | "401K": [ 19 | [1000, "Savings", {}] 20 | ], 21 | "House": [ 22 | [800, "Ownership", {"end": "2018-10"}] 23 | ] 24 | }, 25 | "yearlyNumbers": { 26 | "Cash": { 27 | "2": [5000, "Annual bonus"], 28 | "7": [-1000, "House taxes"] 29 | } 30 | }, 31 | "annualIncreases": { 32 | "401K": 5 33 | }, 34 | "specialNumbers": { 35 | "Cash": { 36 | "2017-9": [-10000, "Big expense"], 37 | "2018-4": [2000, "Friend paid me back"], 38 | "2018-7": [-900, "House repairs"] 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Savings Calculator 2 | 3 | A free, open-source, browser-based savings calculator written in AngularJS 1.x. Written in HTML, CSS, Javascript (AngularJS). No backend technologies involved. 4 | 5 | You can use it for calculating savings, net worth, profit / loss, income / expenses, etc. It can be used for personal and business finances. 6 | 7 | This is meant to be run in your browser as a as a fully HTML / JS application, either online here on Github Pages, or locally on your computer. This has no dependencies on any backend service, and rendering is done fully in your browser. 8 | 9 | ## Screenshot 10 | 11 | ![Savings Calculator](screenshot.png "Screenshot of the Savings Calculator") 12 | 13 | ## [Demo](https://srs81.github.io/Savings-Calculator/calculator.html) 14 | 15 | You can run a [demo right here on Github pages](https://srs81.github.io/Savings-Calculator/calculator.html) using the link above. 16 | 17 | Note that when you update the JSON input, the values are rendered entirely in the browser. There is no backend call. 18 | 19 | ## Technologies used 20 | 21 | * [AngularJS 1.x](https://angularjs.org/) 22 | * [Chart.JS](http://www.chartjs.org/) and [Angular-Chart](http://jtblin.github.io/angular-chart.js/) for charts 23 | * Cookies for local storage: only used if you specifically choose to save the parameters. 24 | * JSON for parameter input 25 | 26 | The parameters are input using the JSON format documented below. A sample is pre-loaded when you load the page, and you can use that to customize and play with the JSON. 27 | 28 | The "Save" and "Load" buttons basically save the JSON parameters in your local browser. Again, no backend technologies are used for this. 29 | 30 | ## JSON parameters 31 | 32 | Currently, you need to input parameters into the application as pure JSON. There may be a GUI in the future to generate this JSON, but for now, you will have to input the JSON yourself by adjusting parameters in the sample JSON, and then saving locally on your computer if you need to reload it as some point. 33 | 34 | The input JSON is one dictionary with these elements: 35 | 36 | * **startDate**: Start month for calculations, expressed in yyyy-mm (year-month) format. 37 | * **endDate**: End month for calculations, expressed in yyyy-mm (year-month) format. 38 | * **startingSavings**: A dictionary with the starting amount of each category. You can have multiple categories here. 39 | * **monthlyNumbers**: A dictionary with the income / expenses for each category here. Each entry consists of three elements 40 | * Number (can be positive for income, negative for expenses) 41 | * Description 42 | * Dictionary of meta-data. This meta-data lets you specify when an income / expense starts, ends or recurs. 43 | * **yearlyNumbers**: A dictionary of yearly updates (as opposed to monthly above). For each category, you can specify the month of the year in which you have a corresponding income or expense. 44 | * **annualIncreases**: A dictionary of percentage increases for each category. 45 | * **specialNumbers**: A dictionary of special-case income or expenses in a particular month. You can use this for one-time revenue or expenses. 46 | 47 | ## JSON example 48 | 49 | Here is a very simple example for the format you can enter. A sample JSON that is somewhat more complex is also included when you load the application in your browser. 50 | 51 | { 52 | "startDate": "2017-3", 53 | "endDate": "2019-7", 54 | "startingSavings": { "Cash": 20000 }, 55 | "monthlyNumbers": { 56 | "Cash": [ 57 | [5000, "Salary", {}], 58 | [-1000, "Fed taxes", {}] 59 | ] 60 | }, 61 | "yearlyNumbers": { 62 | "Cash": { "2": [5000, "Annual bonus in February"] } 63 | }, 64 | "annualIncreases": { 65 | "Cash": 1 66 | }, 67 | "specialNumbers": { 68 | "Cash": { 69 | "2017-9": [-10000, "Big expense"], 70 | "2018-4": [2000, "Friend paid me back"] 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /calculator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Savings Calculator 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 29 | 30 | 31 | 32 |
33 |

34 | Savings Calculator 35 | 36 | 37 | 38 | 39 | {{calculatorCtl.savedMessage}} 40 |

41 | 42 | 43 |
44 | 45 | 47 | 48 | 49 |
50 |
51 |
{{year}}
52 |
53 |
54 |
57 | 58 |
59 | {{month}} 60 | 64 |
65 |
66 | {{type}} {{number | currency}} 67 |

68 |
69 | TOTAL {{calculatorCtl.outputTotals[year][month] | currency}} 70 |
71 |
72 |
73 |
74 | 75 |
76 |
77 |
{{ calculatorCtl.details[calculatorCtl.showDetails['year']][calculatorCtl.showDetails['month']] | json}}
78 |
79 |
80 | 81 |
82 | 83 | 84 | -------------------------------------------------------------------------------- /calculator.js: -------------------------------------------------------------------------------- 1 | angular.module('todoApp', ['ngCookies', 'chart.js']) 2 | .controller('CalculatorController', function($http, $cookies, $timeout) { 3 | var cCtl = this; 4 | cCtl.graph = {}; 5 | cCtl.showDetails = {}; 6 | 7 | // Options for Chart.JS graph 8 | cCtl.chartOptions = { 9 | scales: { 10 | yAxes: [ 11 | { 12 | stacked: true, 13 | ticks: { 14 | callback: function(value, index, values) { 15 | return value.toLocaleString("en-US",{style:"currency", currency:"USD"}); 16 | } 17 | } 18 | } 19 | ], 20 | xAxes: [{ stacked: true }] 21 | }, 22 | legend: { 23 | display: true 24 | } 25 | }; 26 | 27 | // If this cookie is set, user has saved input before 28 | if ($cookies.get("input")) { 29 | cCtl.savedInput = true; 30 | } 31 | 32 | // If no cookie value, get data from input.sample.json 33 | if (!cCtl.input) { 34 | $http.get('input.sample.json') 35 | .then(function(response) { 36 | cCtl.input = response.data; 37 | updateEverything(); 38 | }) 39 | .catch(function(response) { 40 | // If we can't read from input.json 41 | // (run locally from file://), use JS defaults 42 | cCtl.input = loadDefault(); 43 | updateEverything(); 44 | }); 45 | } 46 | 47 | // Return a default object for input 48 | function loadDefault() { 49 | return { 50 | "startDate": "2017-3", 51 | "endDate": "2019-7", 52 | "startingSavings": { 53 | "Cash": 20000, 54 | "401K": 10000, 55 | "House": 40000 56 | }, 57 | "monthlyNumbers": { 58 | "Cash": [ 59 | [5000, "Salary", {}], 60 | [-1000, "Fed taxes", {}], 61 | [-500, "Social security & Medicare", {}], 62 | [-500, "401K from salary", {}], 63 | [-1000, "House mortgage", {"end": "2018-9"}], 64 | [5001, "Bonus", {"start": "2017-6", "recurs": 6}] 65 | ], 66 | "401K": [ 67 | [1000, "Savings", {}] 68 | ], 69 | "House": [ 70 | [800, "Ownership", {"end": "2018-10"}] 71 | ] 72 | }, 73 | "yearlyNumbers": { 74 | "Cash": { 75 | "2": [5000, "Annual bonus"], 76 | "7": [-1000, "House taxes"] 77 | } 78 | }, 79 | "annualIncreases": { 80 | "401K": 5 81 | }, 82 | "specialNumbers": { 83 | "Cash": { 84 | "2017-9": [-10000, "Big expense"], 85 | "2018-4": [2000, "Friend paid me back"], 86 | "2018-7": [-900, "House repairs"] 87 | } 88 | } 89 | }; 90 | } 91 | 92 | // Is this start of the year? 93 | cCtl.startOfYear = function(date) { 94 | return fromDate(date).month == 1; 95 | } 96 | 97 | // Convert from string to date object 98 | function fromDate(str) { 99 | var arr = str.split("-"); 100 | return { "year": parseInt(arr[0]), "month": parseInt(arr[1]) }; 101 | } 102 | 103 | // Convert to string from date object 104 | function toDate(yyyy, mm) { 105 | return "" + yyyy + "-" + mm; 106 | } 107 | 108 | // Check difference between two string objects 109 | function dateDiff(str1, str2) { 110 | var d1 = fromDate(str1); 111 | var d2 = fromDate(str2); 112 | return 12 * (d1.year - d2.year) + d1.month - d2.month; 113 | } 114 | 115 | // Save input JSON to a cookie 116 | cCtl.saveInput = function() { 117 | cCtl.savedMessage = "Saving..."; 118 | deleteSavedMessage(); 119 | $cookies.put("input", cCtl.inputJson); 120 | cCtl.savedInput = true; 121 | } 122 | 123 | // Load input JSON from a cookie 124 | cCtl.loadInput = function() { 125 | cCtl.savedMessage = "Loading..."; 126 | deleteSavedMessage(); 127 | var cookieInput = $cookies.get("input"); 128 | if (cookieInput) { 129 | cCtl.input = JSON.parse(cookieInput); 130 | updateEverything(); 131 | } 132 | } 133 | 134 | // Delete the stored cookie 135 | cCtl.deleteInput = function() { 136 | $cookies.remove("input"); 137 | cCtl.savedInput = false; 138 | } 139 | 140 | // Show the user that we are deleting the input locally 141 | function deleteSavedMessage() { 142 | $timeout(function() { delete cCtl.savedMessage; }, 100); 143 | } 144 | 145 | // Update input parameters by loading from JSON string 146 | cCtl.updateInput = function() { 147 | cCtl.input = angular.fromJson(cCtl.inputJson); 148 | updateEverything(); 149 | } 150 | 151 | // Main internal function that updates all data structures 152 | // from updated JSON input. 153 | function updateEverything() { 154 | cCtl.inputJson = angular.toJson(cCtl.input); 155 | 156 | // Calculate internal start and end date objects 157 | var startDate = fromDate(cCtl.input.startDate); 158 | var startYear = startDate.year; 159 | var startMonth = startDate.month; 160 | 161 | var endDate = fromDate(cCtl.input.endDate); 162 | var endYear = endDate.year; 163 | var endMonth = endDate.month; 164 | 165 | // Initialize variables and objects 166 | var savings = {}; 167 | cCtl.output = {}; 168 | cCtl.outputTotals = {}; 169 | cCtl.details = {}; 170 | 171 | cCtl.graph.data = []; 172 | cCtl.graph.labels = []; 173 | cCtl.graph.series = []; 174 | 175 | // Use startingSavings numbers for local objects 176 | for (var sType in cCtl.input.startingSavings) { 177 | savings[sType] = cCtl.input.startingSavings[sType]; 178 | cCtl.graph.series.push(sType); 179 | cCtl.graph.data.push([]); 180 | } 181 | 182 | // Make a copy to keep track of recurring numbers 183 | var copyForRecurs = angular.copy(cCtl.input.monthlyNumbers); 184 | 185 | // Iterate over each year from start to end 186 | for (var year = startYear; year <= endYear; year++) { 187 | // If we are over the final year, quit 188 | if (year > endYear) break; 189 | 190 | // Initialize objects for this year 191 | cCtl.output[year] = {}; 192 | cCtl.outputTotals[year] = {}; 193 | cCtl.details[year] = {}; 194 | 195 | // Iterate over every month for this year 196 | for (var month = 1; month <= 12; month++) { 197 | // If we are before the first month, continue 198 | if (year == startYear && month < startMonth) continue; 199 | // If we are after the last month, quit 200 | if (year == endYear && month == endMonth) break; 201 | 202 | // Initalize parameters for this month 203 | var thisDate = toDate(year, month); 204 | cCtl.graph.labels.push(thisDate); 205 | 206 | cCtl.output[year][month] = {}; 207 | cCtl.details[year][month] = {}; 208 | cCtl.outputTotals[year][month] = 0.0; 209 | 210 | var i = 0; 211 | // For each category in savings 212 | for (var sType in cCtl.input.startingSavings) { 213 | cCtl.details[year][month][sType] = {}; 214 | 215 | var savingsThisMonth = 0.0; 216 | var mNumbers = cCtl.input.monthlyNumbers[sType]; 217 | // For each monthly income or expense related to this category 218 | for (var count in mNumbers) { 219 | var number = mNumbers[count][0]; 220 | var comment = mNumbers[count][1]; 221 | var metadata = mNumbers[count][2]; 222 | 223 | // Is there a start date meta-data for this income-expense? 224 | var mStart = metadata['start']; 225 | if (mStart && dateDiff(thisDate, mStart) < 0) continue; 226 | 227 | // Is there an end date meta-data for this income-expense? 228 | var mEnd = metadata['end']; 229 | if (mEnd && dateDiff(thisDate, mEnd) > 0) continue; 230 | 231 | // Does this income-expense recur every few months? 232 | var mRecurs = metadata['recurs']; 233 | if (mRecurs) { 234 | // Get handle to clone object to keep track of recurring 235 | var rObj = copyForRecurs[sType][count][2]; 236 | rObj.recurs--; 237 | if (rObj.recurs == 0) { 238 | rObj.recurs = mRecurs; 239 | } 240 | if (rObj.recurs != mRecurs - 1) { 241 | continue; 242 | } 243 | } 244 | 245 | // Add up the savings, as well as detailed breakdown 246 | savingsThisMonth += number; 247 | cCtl.details[year][month][sType][comment] = number; 248 | } 249 | savings[sType] += savingsThisMonth; 250 | 251 | // Is there an annual percentage increase for this number 252 | var aIncreases = cCtl.input.annualIncreases[sType]; 253 | if (aIncreases) { 254 | var oldNumber = savings[sType]; 255 | // Approximate the annual percentage increase per month 256 | savings[sType] *= 1 + (aIncreases / (100*12*1.4)); 257 | cCtl.details[year][month][sType]["Monthly increase"] = savings[sType] - oldNumber; 258 | } 259 | 260 | // Are there yearly income-expense numbers for this category? 261 | var yNumbers = cCtl.input.yearlyNumbers[sType]; 262 | for (var m in yNumbers) { 263 | // Do those numbers apply for this month? 264 | if (month == m) { 265 | var number = yNumbers[m][0]; 266 | var comment = yNumbers[m][1]; 267 | savings[sType] += number; 268 | cCtl.details[year][month][sType][comment] = number; 269 | } 270 | } 271 | 272 | // Are there special one-off income-expense numbers for this category? 273 | var sNumbers = cCtl.input.specialNumbers[sType]; 274 | for (var ym in sNumbers) { 275 | // Do they apply to this year-month? 276 | if (toDate(year, month) == ym) { 277 | var number = sNumbers[ym][0]; 278 | var comment = sNumbers[ym][0]; 279 | savings[sType] += number; 280 | cCtl.details[year][month][sType][comment] = number; 281 | } 282 | } 283 | 284 | // Finalize the calculated numbers 285 | cCtl.output[year][month][sType] = savings[sType]; 286 | cCtl.graph.data[i++].push(savings[sType]); 287 | cCtl.outputTotals[year][month] += savings[sType]; 288 | } 289 | } 290 | } 291 | } 292 | }); 293 | --------------------------------------------------------------------------------