|
| 1 | +<!DOCTYPE html> |
| 2 | +<html> |
| 3 | +<head> |
| 4 | +<title>JavaScript Loan Calculator</title> |
| 5 | +<style> /* This is a CSS style sheet: it adds style to the program output */ |
| 6 | +.output { font-weight: bold; } /* Calculated values in bold */ |
| 7 | +#payment { text-decoration: underline; } /* For element with id="payment" */ |
| 8 | +#graph { border: solid black 1px; } /* Chart has a simple border */ |
| 9 | +th, td { vertical-align: top; } /* Don't center table cells */ |
| 10 | +</style> |
| 11 | +</head> |
| 12 | +<body> |
| 13 | +<!-- |
| 14 | + This is an HTML table with <input> elements that allow the user to enter data |
| 15 | + and <span> elements in which the program can display its results. |
| 16 | + These elements have ids like "interest" and "years". These ids are used |
| 17 | + in the JavaScript code that follows the table. Note that some of the input |
| 18 | + elements define "onchange" or "onclick" event handlers. These specify strings |
| 19 | + of JavaScript code to be executed when the user enters data or clicks. |
| 20 | +--> |
| 21 | +<table> |
| 22 | + <tr><th>Enter Loan Data:</th> |
| 23 | + <td></td> |
| 24 | + <th>Loan Balance, Cumulative Equity, and Interest Payments</th></tr> |
| 25 | + <tr><td>Amount of the loan ($):</td> |
| 26 | + <td><input id="amount" onchange="calculate();"></td> |
| 27 | + <td rowspan=8> |
| 28 | + <canvas id="graph" width="400" height="250"></canvas></td></tr> |
| 29 | + <tr><td>Annual interest (%):</td> |
| 30 | + <td><input id="apr" onchange="calculate();"></td></tr> |
| 31 | + <tr><td>Repayment period (years):</td> |
| 32 | + <td><input id="years" onchange="calculate();"></td> |
| 33 | + <tr><td>Zipcode (to find lenders):</td> |
| 34 | + <td><input id="zipcode" onchange="calculate();"></td> |
| 35 | + <tr><th>Approximate Payments:</th> |
| 36 | + <td><button onclick="calculate();">Calculate</button></td></tr> |
| 37 | + <tr><td>Monthly payment:</td> |
| 38 | + <td>$<span class="output" id="payment"></span></td></tr> |
| 39 | + <tr><td>Total payment:</td> |
| 40 | + <td>$<span class="output" id="total"></span></td></tr> |
| 41 | + <tr><td>Total interest:</td> |
| 42 | + <td>$<span class="output" id="totalinterest"></span></td></tr> |
| 43 | + <tr><th>Sponsors:</th><td colspan=2> |
| 44 | + Apply for your loan with one of these fine lenders: |
| 45 | + <div id="lenders"></div></td></tr> |
| 46 | +</table> |
| 47 | + |
| 48 | +<!-- The rest of this example is JavaScript code in the <script> tag below --> |
| 49 | +<!-- Normally, this script would go in the document <head> above but it --> |
| 50 | +<!-- is easier to understand here, after you've seen its HTML context. --> |
| 51 | +<script> |
| 52 | +"use strict"; // Use ECMAScript 5 strict mode in browsers that support it |
| 53 | + |
| 54 | +/* |
| 55 | + * This script defines the calculate() function called by the event handlers |
| 56 | + * in HTML above. The function reads values from <input> elements, calculates |
| 57 | + * loan payment information, displays the results in <span> elements. It also |
| 58 | + * saves the user's data, displays links to lenders, and draws a chart. |
| 59 | + */ |
| 60 | +function calculate() { |
| 61 | + // Look up the input and output elements in the document |
| 62 | + var amount = document.getElementById("amount"); |
| 63 | + var apr = document.getElementById("apr"); |
| 64 | + var years = document.getElementById("years"); |
| 65 | + var zipcode = document.getElementById("zipcode"); |
| 66 | + var payment = document.getElementById("payment"); |
| 67 | + var total = document.getElementById("total"); |
| 68 | + var totalinterest = document.getElementById("totalinterest"); |
| 69 | + |
| 70 | + // Get the user's input from the input elements. Assume it is all valid. |
| 71 | + // Convert interest from a percentage to a decimal, and convert from |
| 72 | + // an annual rate to a monthly rate. Convert payment period in years |
| 73 | + // to the number of monthly payments. |
| 74 | + var principal = parseFloat(amount.value); |
| 75 | + var interest = parseFloat(apr.value) / 100 / 12; |
| 76 | + var payments = parseFloat(years.value) * 12; |
| 77 | + |
| 78 | + // Now compute the monthly payment figure. |
| 79 | + var x = Math.pow(1 + interest, payments); // Math.pow() computes powers |
| 80 | + var monthly = (principal*x*interest)/(x-1); |
| 81 | + |
| 82 | + // If the result is a finite number, the user's input was good and |
| 83 | + // we have meaningful results to display |
| 84 | + if (isFinite(monthly)) { |
| 85 | + // Fill in the output fields, rounding to 2 decimal places |
| 86 | + payment.innerHTML = monthly.toFixed(2); |
| 87 | + total.innerHTML = (monthly * payments).toFixed(2); |
| 88 | + totalinterest.innerHTML = ((monthly*payments)-principal).toFixed(2); |
| 89 | + |
| 90 | + // Save the user's input so we can restore it the next time they visit |
| 91 | + save(amount.value, apr.value, years.value, zipcode.value); |
| 92 | + |
| 93 | + // Advertise: find and display local lenders, but ignore network errors |
| 94 | + try { // Catch any errors that occur within these curly braces |
| 95 | + getLenders(amount.value, apr.value, years.value, zipcode.value); |
| 96 | + } |
| 97 | + catch(e) { /* And ignore those errors */ } |
| 98 | + |
| 99 | + // Finally, chart loan balance, and interest and equity payments |
| 100 | + chart(principal, interest, monthly, payments); |
| 101 | + } |
| 102 | + else { |
| 103 | + // Result was Not-a-Number or infinite, which means the input was |
| 104 | + // incomplete or invalid. Clear any previously displayed output. |
| 105 | + payment.innerHTML = ""; // Erase the content of these elements |
| 106 | + total.innerHTML = "" |
| 107 | + totalinterest.innerHTML = ""; |
| 108 | + chart(); // With no arguments, clears the chart |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +// Save the user's input as properties of the localStorage object. Those |
| 113 | +// properties will still be there when the user visits in the future |
| 114 | +// This storage feature will not work in some browsers (Firefox, e.g.) if you |
| 115 | +// run the example from a local file:// URL. It does work over HTTP, however. |
| 116 | +function save(amount, apr, years, zipcode) { |
| 117 | + if (window.localStorage) { // Only do this if the browser supports it |
| 118 | + localStorage.loan_amount = amount; |
| 119 | + localStorage.loan_apr = apr; |
| 120 | + localStorage.loan_years = years; |
| 121 | + localStorage.loan_zipcode = zipcode; |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +// Automatically attempt to restore input fields when the document first loads. |
| 126 | +window.onload = function() { |
| 127 | + // If the browser supports localStorage and we have some stored data |
| 128 | + if (window.localStorage && localStorage.loan_amount) { |
| 129 | + document.getElementById("amount").value = localStorage.loan_amount; |
| 130 | + document.getElementById("apr").value = localStorage.loan_apr; |
| 131 | + document.getElementById("years").value = localStorage.loan_years; |
| 132 | + document.getElementById("zipcode").value = localStorage.loan_zipcode; |
| 133 | + } |
| 134 | +}; |
| 135 | + |
| 136 | +// Pass the user's input to a server-side script which can (in theory) return |
| 137 | +// a list of links to local lenders interested in making loans. This example |
| 138 | +// does not actually include a working implementation of such a lender-finding |
| 139 | +// service. But if the service existed, this function would work with it. |
| 140 | +function getLenders(amount, apr, years, zipcode) { |
| 141 | + // If the browser does not support the XMLHttpRequest object, do nothing |
| 142 | + if (!window.XMLHttpRequest) return; |
| 143 | + |
| 144 | + // Find the element to display the list of lenders in |
| 145 | + var ad = document.getElementById("lenders"); |
| 146 | + if (!ad) return; // Quit if no spot for output |
| 147 | + |
| 148 | + // Encode the user's input as query parameters in a URL |
| 149 | + var url = "getLenders.php" + // Service url plus |
| 150 | + "?amt=" + encodeURIComponent(amount) + // user data in query string |
| 151 | + "&apr=" + encodeURIComponent(apr) + |
| 152 | + "&yrs=" + encodeURIComponent(years) + |
| 153 | + "&zip=" + encodeURIComponent(zipcode); |
| 154 | + |
| 155 | + // Fetch the contents of that URL using the XMLHttpRequest object |
| 156 | + var req = new XMLHttpRequest(); // Begin a new request |
| 157 | + req.open("GET", url); // An HTTP GET request for the url |
| 158 | + req.send(null); // Send the request with no body |
| 159 | + |
| 160 | + // Before returning, register an event handler function that will be called |
| 161 | + // at some later time when the HTTP server's response arrives. This kind of |
| 162 | + // asynchronous programming is very common in client-side JavaScript. |
| 163 | + req.onreadystatechange = function() { |
| 164 | + if (req.readyState == 4 && req.status == 200) { |
| 165 | + // If we get here, we got a complete valid HTTP response |
| 166 | + var response = req.responseText; // HTTP response as a string |
| 167 | + var lenders = JSON.parse(response); // Parse it to a JS array |
| 168 | + |
| 169 | + // Convert the array of lender objects to a string of HTML |
| 170 | + var list = ""; |
| 171 | + for(var i = 0; i < lenders.length; i++) { |
| 172 | + list += "<li><a href='" + lenders[i].url + "'>" + |
| 173 | + lenders[i].name + "</a>"; |
| 174 | + } |
| 175 | + |
| 176 | + // Display the HTML in the element from above. |
| 177 | + ad.innerHTML = "<ul>" + list + "</ul>"; |
| 178 | + } |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +// Chart monthly loan balance, interest and equity in an HTML <canvas> element. |
| 183 | +// If called with no arguments then just erase any previously drawn chart. |
| 184 | +function chart(principal, interest, monthly, payments) { |
| 185 | + var graph = document.getElementById("graph"); // Get the <canvas> tag |
| 186 | + graph.width = graph.width; // Magic to clear and reset the canvas element |
| 187 | + |
| 188 | + // If we're called with no arguments, or if this browser does not support |
| 189 | + // graphics in a <canvas> element, then just return now. |
| 190 | + if (arguments.length == 0 || !graph.getContext) return; |
| 191 | + |
| 192 | + // Get the "context" object for the <canvas> that defines the drawing API |
| 193 | + var g = graph.getContext("2d"); // All drawing is done with this object |
| 194 | + var width = graph.width, height = graph.height; // Get canvas size |
| 195 | + |
| 196 | + // These functions convert payment numbers and dollar amounts to pixels |
| 197 | + function paymentToX(n) { return n * width/payments; } |
| 198 | + function amountToY(a) { return height-(a * height/(monthly*payments*1.05));} |
| 199 | + |
| 200 | + // Payments are a straight line from (0,0) to (payments, monthly*payments) |
| 201 | + g.moveTo(paymentToX(0), amountToY(0)); // Start at lower left |
| 202 | + g.lineTo(paymentToX(payments), // Draw to upper right |
| 203 | + amountToY(monthly*payments)); |
| 204 | + g.lineTo(paymentToX(payments), amountToY(0)); // Down to lower right |
| 205 | + g.closePath(); // And back to start |
| 206 | + g.fillStyle = "#f88"; // Light red |
| 207 | + g.fill(); // Fill the triangle |
| 208 | + g.font = "bold 12px sans-serif"; // Define a font |
| 209 | + g.fillText("Total Interest Payments", 20,20); // Draw text in legend |
| 210 | + |
| 211 | + // Cumulative equity is non-linear and trickier to chart |
| 212 | + var equity = 0; |
| 213 | + g.beginPath(); // Begin a new shape |
| 214 | + g.moveTo(paymentToX(0), amountToY(0)); // starting at lower-left |
| 215 | + for(var p = 1; p <= payments; p++) { |
| 216 | + // For each payment, figure out how much is interest |
| 217 | + var thisMonthsInterest = (principal-equity)*interest; |
| 218 | + equity += (monthly - thisMonthsInterest); // The rest goes to equity |
| 219 | + g.lineTo(paymentToX(p),amountToY(equity)); // Line to this point |
| 220 | + } |
| 221 | + g.lineTo(paymentToX(payments), amountToY(0)); // Line back to X axis |
| 222 | + g.closePath(); // And back to start point |
| 223 | + g.fillStyle = "green"; // Now use green paint |
| 224 | + g.fill(); // And fill area under curve |
| 225 | + g.fillText("Total Equity", 20,35); // Label it in green |
| 226 | + |
| 227 | + // Loop again, as above, but chart loan balance as a thick black line |
| 228 | + var bal = principal; |
| 229 | + g.beginPath(); |
| 230 | + g.moveTo(paymentToX(0),amountToY(bal)); |
| 231 | + for(var p = 1; p <= payments; p++) { |
| 232 | + var thisMonthsInterest = bal*interest; |
| 233 | + bal -= (monthly - thisMonthsInterest); // The rest goes to equity |
| 234 | + g.lineTo(paymentToX(p),amountToY(bal)); // Draw line to this point |
| 235 | + } |
| 236 | + g.lineWidth = 3; // Use a thick line |
| 237 | + g.stroke(); // Draw the balance curve |
| 238 | + g.fillStyle = "black"; // Switch to black text |
| 239 | + g.fillText("Loan Balance", 20,50); // Legend entry |
| 240 | + |
| 241 | + // Now make yearly tick marks and year numbers on X axis |
| 242 | + g.textAlign="center"; // Center text over ticks |
| 243 | + var y = amountToY(0); // Y coordinate of X axis |
| 244 | + for(var year=1; year*12 <= payments; year++) { // For each year |
| 245 | + var x = paymentToX(year*12); // Compute tick position |
| 246 | + g.fillRect(x-0.5,y-3,1,3); // Draw the tick |
| 247 | + if (year == 1) g.fillText("Year", x, y-5); // Label the axis |
| 248 | + if (year % 5 == 0 && year*12 !== payments) // Number every 5 years |
| 249 | + g.fillText(String(year), x, y-5); |
| 250 | + } |
| 251 | + |
| 252 | + // Mark payment amounts along the right edge |
| 253 | + g.textAlign = "right"; // Right-justify text |
| 254 | + g.textBaseline = "middle"; // Center it vertically |
| 255 | + var ticks = [monthly*payments, principal]; // The two points we'll mark |
| 256 | + var rightEdge = paymentToX(payments); // X coordinate of Y axis |
| 257 | + for(var i = 0; i < ticks.length; i++) { // For each of the 2 points |
| 258 | + var y = amountToY(ticks[i]); // Compute Y position of tick |
| 259 | + g.fillRect(rightEdge-3, y-0.5, 3,1); // Draw the tick mark |
| 260 | + g.fillText(String(ticks[i].toFixed(0)), // And label it. |
| 261 | + rightEdge-5, y); |
| 262 | + } |
| 263 | +} |
| 264 | +</script> |
| 265 | +</body> |
| 266 | +</html> |
0 commit comments