Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
...
  • 6 commits
  • 4 files changed
  • 0 commit comments
  • 1 contributor
Commits on Aug 08, 2012
@borgar Settinga an upper bound for ticks and better ranges for empty charts. 8949212
@borgar Speling.
Fixes #7
4f63236
@borgar Adding option to display future money in current value. 445d2a5
@borgar Label axis with units. d3974c1
@borgar Fixing bug with lack of serialization for economic parameters. 5b12a6b
Commits on Aug 12, 2012
@borgar Loans replaced with loan collections.
This means that user can build any combination of loans into a combined loan-collection. This is useful to test the curves of real world situations where people often have multiple mortgages on a single property. The current Icelandic landscape also includes mixed loans of indexed/unindexed parts and this would be a requirement to calculating them.

This now adds the need to show collection breakdowns as small multiples.

As it turns out I am starting to regret some design decisions, it would have been much less complicated to classify the loan collection and loans into a model. The current structure made more sense when the form was static. I've added a bunch of helper methods to reduce the work of dealing with it, but I may need to refactor the whole thing if any more complexity is added.
b449560
Showing with 319 additions and 115 deletions.
  1. +26 −17 css/styles.less
  2. +92 −64 index.html
  3. +194 −33 js/app.js
  4. +7 −1 js/jquery.to_query.js
View
43 css/styles.less
@@ -21,6 +21,7 @@ select {
input {
padding : 3px 4px;
box-shadow: 1px 1px 5px #ccc inset;
+ border-radius : 3px;
}
h1 {
margin : 0;
@@ -76,25 +77,30 @@ fieldset {
clear: both;
}
-#loan1, #loan2 {
- float : left;
- clear: none;
- width : 49.5%;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- select {
- width : 65%;
- }
- .formcontrol label {
- display: inline-block;
- width : 30%;
- text-align : right;
- }
+
+table {
+ width : 100%;
+}
+th, td {
+ text-align : left;
+ padding : 4px 0;
+ border-bottom: 1px solid #ccc;
+}
+td.actions {
+ text-align : right;
}
-#loan2 {
- float : right;
+
+.buttons {
+ padding : 6px 0;
+ text-align : right;
+ margin-bottom: 2px;
+}
+
+tr:only-child button[data-action] {
+ visibility: hidden;
}
+
fieldset.inactive {
color : #999;
input, option, select {
@@ -102,7 +108,10 @@ fieldset.inactive {
}
}
-
+#economy .formcontrol {
+ display : inline-block;
+ margin-right: 1em;
+}
legend {
background : #fff;
View
156 index.html
@@ -21,90 +21,118 @@
<div id="col1">
- <fieldset class="" id="loan1">
- <legend><label><input type="checkbox" id="loan1_on" checked> Lán 1:</label></legend>
- <div class="formcontrol">
- <label for="loan1_principal">Lánsfjárhæð:</label>
- <input id="loan1_principal" name="loan1_principal" size="10"
- value="20000000" min="1000000" step="1000000" type="number" />
- <abbr class="unit" title="krónur">kr.</abbr>
- </div>
- <div class="formcontrol">
- <label for="loan1_period">Tímabil:</label>
- <input id="loan1_period" name="loan1_period" size="2"
- value="40" max="60" min="5" step="1" type="number" />
- <span class="unit">ár</span>
+ <fieldset class="" id="l1wrap">
+ <legend><label><input type="checkbox" id="l1" checked> Lánasafn 1:</label></legend>
+ <input id="l1n" name="l1n" type="hidden" value="1">
+
+ <table cellspacing="0">
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>Lánsfjárhæð</th>
+ <th>Tímabil</th>
+ <th>Tegund láns</th>
+ <th>Vextir</th>
+ <th>&nbsp;</th>
+ </tr>
+ </thead>
+ <tbody id="l1body">
+
+ <tr id="l1(1)row">
+ <td class="formcontrol formselect formcheck">
+ <label>
+ <input id="l1(1)" name="l1(1)" value="on" type="checkbox" checked />
+ </label>
+ </td>
+ <td class="formcontrol">
+ <input id="l1(1)principal" name="l1(1)principal" size="10"
+ value="20000000" min="1000000" step="1000000" type="number" />
+ <abbr class="unit" title="krónur">kr.</abbr>
+ </td>
+ <td class="formcontrol">
+ <input id="l1(1)period" name="l1(1)period" size="2"
+ value="40" max="60" min="5" step="1" type="number" />
+ <span class="unit">ár</span>
+ </td>
+ <td class="formcontrol formselect">
+ <select id="l1(1)type" name="l1(1)type">
+ <option value="idx_eq">Verðtryggt - Jafnar afborganir</option>
+ <option value="idx_pmt">Verðtryggt - Jafngreiðslur</option>
+ <option value="eq">Óverðtryggt - Jafnar afborganir</option>
+ <option value="pmt">Óverðtryggt - Jafngreiðslur</option>
+ </select>
+ </td>
+ <td class="formcontrol">
+ <input id="l1(1)interest" name="l1(1)interest" size="2"
+ value="5" max="25" min="1" step="1" type="number" />
+ <abbr class="unit" title="prósent">%</abbr>
+ </td>
+ <td class="actions">
+ <button data-action="delete-loan">✖</button>
+ </td>
+ </tr>
+
+ </tbody>
+ </table>
+
+ <div class="buttons">
+ <button data-action="add-loan">Bæta við láni</button>
</div>
- <fieldset class="">
- <div class="formcontrol formselect">
- <label for="loan1_type">Tegund láns:</label>
- <select id="loan1_type" name="loan1_type">
- <option value="idx_eq">Verðtryggt - Jafnar afborganir</option>
- <option value="idx_pmt">Verðtryggt - Jafngreiðslur</option>
- <option value="eq">Óverðtryggt - Jafnar afborganir</option>
- <option value="pmt">Óverðtryggt - Jafngreiðslur</option>
- </select>
- </div>
- <div class="formcontrol">
- <label for="loan1_interest">Vextir:</label>
- <input id="loan1_interest" name="loan1_interest" size="2"
- value="5" max="25" min="1" step="1" type="number" />
- <abbr class="unit" title="prósent">%</abbr>
- </div>
- </fieldset>
+
</fieldset>
- <fieldset class="" id="loan2">
- <legend><label><input type="checkbox" id="loan2_on"> Lán 2:</label></legend>
- <div class="formcontrol">
- <label for="loan2_principal">Lánsfjárhæð:</label>
- <input id="loan2_principal" name="loan2_principal" size="10"
- value="20000000" min="1000000" step="1000000" type="number" />
- <abbr class="unit" title="krónur">kr.</abbr>
- </div>
- <div class="formcontrol">
- <label for="loan2_period">Tímabil:</label>
- <input id="loan2_period" name="loan2_period" size="2"
- value="40" max="60" min="5" step="1" type="number" />
- <span class="unit">ár</span>
+
+ <fieldset class="" id="l2wrap">
+ <legend><label><input type="checkbox" id="l2"> Lánasafn 2:</label></legend>
+ <input id="l2n" name="l2n" type="hidden" value="1">
+
+ <table cellspacing="0">
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>Lánsfjárhæð</th>
+ <th>Tímabil</th>
+ <th>Tegund láns</th>
+ <th>Vextir</th>
+ <th>&nbsp;</th>
+ </tr>
+ </thead>
+ <tbody id="l2body"></tbody>
+ </table>
+
+ <div class="buttons">
+ <button data-action="add-loan">Bæta við láni</button>
</div>
- <fieldset class="">
- <div class="formcontrol formselect">
- <label for="loan2_type">Tegund láns:</label>
- <select id="loan2_type" name="loan2_type">
- <option value="idx_eq">Verðtryggt - Jafnar afborganir</option>
- <option selected value="idx_pmt">Verðtryggt - Jafngreiðslur</option>
- <option value="eq">Óverðtryggt - Jafnar afborganir</option>
- <option value="pmt">Óverðtryggt - Jafngreiðslur</option>
- </select>
- </div>
- <div class="formcontrol if_index">
- <label for="loan2_interest">Vextir:</label>
- <input id="loan2_interest" name="loan2_interest" size="2"
- value="5" max="25" min="1" step="1" type="number" />
- <abbr class="unit" title="prósent">%</abbr>
- </div>
- </fieldset>
+
</fieldset>
- <fieldset class="">
- <legend>Verðbólga:</legend>
+
+
+ <fieldset class="" id="economy">
+ <legend>Verðþróun:</legend>
<div class="formcontrol">
<label for="inflation">Verðbólga:</label>
<input id="inflation" name="inflation" size="2"
value="4" max="30" min="1" step="0.5" type="number" />
<abbr class="unit" title="prósent">%</abbr>
</div>
+ <div class="formcontrol formselect formcheck" id="">
+ <label>
+ <input id="value" name="value" value="current" type="checkbox" />
+ Sýna fjárhæðir núvirtar
+ </label>
+ </div>
+
</fieldset>
- <fieldset class="">
+ <fieldset class="" id="calcview">
<legend>Reikna:</legend>
<div class="formcontrol formselect">
<!--<label for="display_driver">Sýna:</label>-->
<select id="display_driver" name="display_driver">
<option value="loan_std_capital">Þróun höfuðstóls yfir lánstímabil</option>
<option value="loan_std_payments">Afborganir á ári yfir lánstímabil</option>
- <option value="loan_vs_property1">Höfuðstóll sem hlutfall af markaðvirði eignar</option>
+ <option value="loan_vs_property1">Höfuðstóll sem hlutfall af markaðsvirði eignar</option>
<option value="loan_vs_property2">Verðmæti eignar umfram áhvílandi höfuðstól</option>
<option value="loan_vs_income1">Afborganir sem hlutfall af ráðstöfunartekjum</option>
<option value="loan_vs_income2">Ráðstöfunartekjur umfram afborgun</option>
View
227 js/app.js
@@ -4,6 +4,37 @@ numfmt.locale['is'] = {
, decimal_separator : ','
};
+jQuery.fn.collectionId = function ( nid ) {
+ if ( nid ) {
+ return this.each(function () {
+ this.id = this.id.replace( /^l\d+/, nid );
+ if ( this.name ) {
+ this.name = this.name.replace( /^l\d+/, nid );
+ }
+ });
+ }
+ var id = this[0] ? this[0].id : '';
+ return id.replace( /^(l\d+).*/, '$1' );
+};
+
+jQuery.fn.loanId = function ( nid ) {
+ if ( nid ) {
+ return this.each(function () {
+ this.id = this.id.replace( /^l\d+\(\d+\)/, nid );
+ if ( this.name ) {
+ this.name = this.name.replace( /^l\d+\(\d+\)/, nid );
+ }
+ });
+ // this.id = id.replace( /^l\d+\(\d+\)/, nid );
+ return this;
+ }
+ var id = this[0] ? this[0].id : '';
+ var i = Number( id.replace( /^.+\((\d+)\).*$/, '$1' ) );
+ if ( !isFinite( i ) ) { i = 0; };
+ return i;
+};
+
+
// run app
jQuery(function ($) {
@@ -16,6 +47,12 @@ jQuery(function ($) {
var current_data;
var current_driver;
+ // get loan row template
+ var tmplElm = $( 'table tr[id$=\\)row]' );
+ var loan_tmpl = $( '<div></div>' ).append( tmplElm[0] ).html().split( /l\d\(\d\)/ );
+ tmplElm.remove(); delete tmplElm;
+
+
// these are called by the amortizer to calculate each payment
// the init/calculate functions are called with this === loan, first parameter === payment row
var loan_drivers = {
@@ -66,16 +103,18 @@ jQuery(function ($) {
title: "Þróun höfuðstóls yfir lánstímabil",
plot: 'line',
calculate: function ( d ) {
- return { x: d.index + 1, y: d.period_captial_end };
+ var mul = this.in_current_value ? d.currency_worth : 1;
+ return { x: d.index + 1, y: d.period_captial_end * mul };
}
},
loan_std_payments: {
title: "Afborganir á ári yfir lánstímabil",
plot: 'bar',
calculate: function ( d ) {
+ var mul = this.in_current_value ? d.currency_worth : 1;
return { x: d.index + 1
- , y: d.amount_payed
- , stack: [ d.capital_payment, d.interest ]
+ , y: d.amount_payed * mul
+ , stack: [ d.capital_payment * mul, d.interest * mul ]
};
}
},
@@ -103,9 +142,10 @@ jQuery(function ($) {
this.property_value = $( '#property_value' ).val() * 1;
},
calculate: function ( d ) {
+ var mul = this.in_current_value ? d.currency_worth : 1;
var upvalue = this.property_value * ( 1 + this.property_growth * d.index );
var y = (upvalue - d.period_captial_end);
- return { x: d.index + 1, y: y };
+ return { x: d.index + 1, y: y * mul };
}
},
loan_vs_income1: {
@@ -132,26 +172,69 @@ jQuery(function ($) {
this.income_growth = Number( $( '#income_growth' ).val() ) / 100;
},
calculate: function ( d ) {
+ var mul = this.in_current_value ? d.currency_worth : 1;
var upvalue = this.income_post_taxes * ( 1 + this.income_growth * d.index );
var y = upvalue - d.amount_payed; // TODO: allow negatives?
- return { x: d.index + 1, y: y };
+ return { x: d.index + 1, y: y * mul };
}
}
};
+ // helper functions to get/put data into form
+ function update_loans ( id ) {
+ var loans = 0;
+ $( '#' + id + 'wrap' )
+ .find( 'tr[id^='+id+']' )
+ .each(function () {
+ var pre = id + '(' + (loans++) + ')';
+ $( this ).loanId( pre )
+ .find( '*[id^='+id+'\\(]' ).loanId( pre );
+ })
+ ;
+ $( '#' + id + 'n' ).val( loans );
+ }
+
+ function add_loan ( c ) {
+ $( '#' + c + 'body' ).append( loan_tmpl.join( c + '(0)' ) );
+ }
+
+ function loans_count ( c, n ) {
+ var elm = $( '#' + c + 'n' );
+ if ( n != null ) {
+ elm.val( n );
+ }
+ else {
+ n = Number( elm.val() );
+ if ( !isFinite( n ) || !n ) { n = 0; };
+ }
+ return n;
+ }
+
// read the properties of a loan for the UI and return a customized "driver"
- function get_loan_properties ( loan_elm_id ) {
- var type = $( loan_elm_id + '_type' ).val();
+ function get_loan_properties ( id, collection ) {
+ var type = $( '#' + id + 'type' ).val();
var driver = $.extend( {}, loan_drivers[ type ] );
- driver.principal = Number( $( loan_elm_id + '_principal' ).val() || 0 );
- driver.interest = Number( $( loan_elm_id + '_interest' ).val() || 0 ) / 100;
- driver.period = Number( $( loan_elm_id + '_period' ).val() || 1 );
- driver.inflation = Number( $( '#inflation' ).val() || 0 ) / 100;
- driver.active = $( loan_elm_id + "_on" )[ 0 ].checked;
+ driver.principal = Number( $( '#' + id + 'principal' ).val() || 0 );
+ driver.interest = Number( $( '#' + id + 'interest' ).val() || 0 ) / 100;
+ driver.period = Number( $( '#' + id + 'period' ).val() || 1 );
+ driver.active = collection.active && $( '#' + id )[ 0 ].checked;
+ driver.inflation = collection.inflation;
+ driver.in_current_value = collection.in_current_value;
return driver;
}
+ function get_collection_properties ( id ) {
+ var collection = {};
+ collection.active = $( '#' + id )[ 0 ].checked;
+ collection.inflation = Number( $( '#inflation' ).val() || 0 ) / 100;
+ collection.in_current_value = $( '#value' )[ 0 ].checked;
+ collection.loans = pv.range( loans_count( id ) ).map(function (i) {
+ return get_loan_properties( id + '\\(' + i + '\\)', collection );
+ });
+ return collection;
+ }
+
// amortization
function amortize ( loan ) {
@@ -187,6 +270,47 @@ jQuery(function ($) {
return loan;
}
+ function sum_loans ( collection ) {
+
+ // make a copy of the first active one
+ var period = 0;
+ var principal = 0;
+ var loans = collection.loans.filter(function(d){
+ if ( d.active ) {
+ if ( d.period > period ) {
+ period = d.period;
+ }
+ principal += d.principal;
+ }
+ return d.active;
+ });
+ var payments = [];
+ for (var p=0; p<period; p++) {
+
+ var payment = $.extend( {}, loans[0].payments[p] );
+ for (var li=1,ll=loans.length; li<ll; li++) {
+ var px = loans[li].payments[p];
+ if ( px ) {
+ payment.amount_payed += px.amount_payed;
+ payment.capital_payment += px.capital_payment;
+ payment.interest += px.interest;
+ payment.payment_upcalc += px.payment_upcalc;
+ payment.period_captial_end += px.period_captial_end;
+ payment.period_captial_start += px.period_captial_start;
+ }
+ }
+ payments.push( payment );
+ }
+ var r = {
+ active: !!payments.length,
+ in_current_value: collection.loans[0].in_current_value,
+ payments: payments,
+ period: period,
+ principal: principal
+ };
+ return r;
+ }
+
function plot ( loans, display_driver ) {
@@ -203,7 +327,7 @@ jQuery(function ($) {
display_driver.init.call( loan );
}
if ( !loan.active ) { return []; }
- var series = loan.payments
+ var series = loan.payments
.map( display_driver.calculate, loan )
.filter(function ( d ) {
if ( isFinite(d.x) && isFinite(d.y) ) {
@@ -223,6 +347,7 @@ jQuery(function ($) {
})
;
+
// prevent floating point artifacts from triggering axis
if ( Math.abs( y_min ) > 0 && Math.abs( y_min ) < 1e-8 ) {
y_min = 0;
@@ -232,7 +357,7 @@ jQuery(function ($) {
var fmt = numfmt( fmt_ptn, 'is' );
// make sure ranges are finite
- if ( !isFinite( y_max ) ) { y_max = 1000000; }
+ if ( !isFinite( y_max ) ) { y_max = display_driver.percent ? 100 : 10; }
if ( !isFinite( y_min ) ) { y_min = 0; }
if ( !isFinite( x_max ) ) { x_max = 10; }
if ( !isFinite( x_min ) ) { x_min = 0; }
@@ -243,7 +368,7 @@ jQuery(function ($) {
, margin_right = 10
, margin_bottom = 20
, margin_left = 65
- , w = fit_space - (margin_left + margin_right) // 480
+ , w = fit_space - ( margin_left + margin_right ) // 480
, aspect = 1 / 1.618
, h = w * aspect
, x_scale = pv.Scale.linear( x_min, x_max )
@@ -257,12 +382,11 @@ jQuery(function ($) {
var y_axis_bottom = 0;
var y = y_scale.range( 0, h );
var n_y_ticks = ~~( h / 25 );
- var y_ticks = y.ticks( Math.max( 2, n_y_ticks ) );
+ var y_ticks = y.ticks( Math.min( Math.max( 2, n_y_ticks ), 10 ) );
// frame
var vis = new pv.Panel()
.canvas( document.getElementById('plot_area') )
- //.fillStyle('#ddd')
.width( w )
.height( w * aspect )
.bottom( margin_bottom )
@@ -271,8 +395,13 @@ jQuery(function ($) {
.top( margin_top )
;
+ var cv = '';
+ if ( loans[0].in_current_value ) {
+ // núvirtar kr. // fært til verðlags dagsins í dag // ??
+ cv = " (núvirt verðlag)";
+ }
vis.add(pv.Label)
- .text( display_driver.title )
+ .text( display_driver.title + cv )
.top( 0 )
.textMargin( 10 )
.textAlign( 'center' )
@@ -293,7 +422,7 @@ jQuery(function ($) {
.anchor("left")
.add(pv.Label)
.textStyle( y_axis_color )
- .text( fmt ) // y.tickFormat
+ .text(function (d) { return !d ? '0 kr.' : fmt( d ) })
;
// X-axis
@@ -317,7 +446,9 @@ jQuery(function ($) {
.add(pv.Label)
.textStyle( x_axis_color )
.textMargin( 5 )
- .text(function(d){ return d.toFixed(); })
+ .text(function(d){
+ return d.toFixed() + ( this.index === 0 ? '. ár' : '' );
+ })
;
if ( display_driver.plot === "line" ) {
@@ -413,31 +544,35 @@ jQuery(function ($) {
}
// Plot
- var loan1 = amortize( get_loan_properties( "#loan1" ) );
- $( '#loan1' )
- .css( 'border-color', loan1.active ? colors[0] : '' )
- .toggleClass( 'inactive', !loan1.active )
+ var collection1 = get_collection_properties( 'l1' );
+ collection1.loans.forEach( amortize );
+ $( '#l1wrap' )
+ .css( 'border-color', collection1.active ? colors[0] : '' )
+ .toggleClass( 'inactive', !collection1.active )
;
- var loan2 = amortize( get_loan_properties( "#loan2" ) );
- $( '#loan2' )
- .css( 'border-color', loan2.active ? colors[1] : '' )
- .toggleClass( 'inactive', !loan2.active )
+ var collection2 = get_collection_properties( 'l2' );
+ collection2.loans.forEach( amortize );
+ $( '#l2wrap' )
+ .css( 'border-color', collection2.active ? colors[1] : '' )
+ .toggleClass( 'inactive', !collection2.active )
;
- current_data = [ loan1, loan2 ];
+ // TODO: fork in the road - small multiples for collections, or one with both?
+ current_data = [ sum_loans( collection1 ), collection2.loans[0] ];
plot( current_data, current_driver );
// serialize to hash
// NOTE: don't do this naiively because ppl may put their income in, change to something else
// and then share the URL - this would then be leaking their income.
- var cq = [ '#col1>fieldset:not([id])' ];
- cq.push( loan1.active ? '#loan1' : '#loan1 legend' );
- cq.push( loan2.active ? '#loan2' : '#loan2 legend' );
+ var cq = [ '#calcview', '#economy' ];
+ cq.push( collection1.active ? '#l1wrap' : '#l1wrap legend' );
+ cq.push( collection2.active ? '#l2wrap' : '#l2wrap legend' );
if ( current_driver.extra_controls ) {
cq.push( '#' + current_driver.extra_controls );
}
- document.location.hash = '!' + $( cq.join(', ') ).to_query();
+ var q = $( cq.join(', ') ).to_query();
+ document.location.hash = q ? '!' + q : '';
}
// hook updates to pretty much all UI events that do anything
@@ -447,6 +582,21 @@ jQuery(function ($) {
.bind( 'submit', update_app )
.bind( 'blur', update_app )
.bind( 'focusout', update_app )
+ .on( 'click', 'button[data-action=add-loan]', function (e) {
+ var c_id = $( e.target ).closest( 'fieldset' ).collectionId();
+ add_loan( c_id );
+ update_loans( c_id );
+ update_app();
+ })
+ .on( 'click', 'button[data-action=delete-loan]', function (e) {
+ var row = $( e.target ).closest( 'tr' );
+ var c_id = row.collectionId();
+ if ( loans_count( c_id ) > 1 ) {
+ row.remove();
+ update_loans( c_id );
+ update_app();
+ }
+ })
;
// window resize handler
@@ -468,6 +618,17 @@ jQuery(function ($) {
var hash = String( document.location.hash ).replace( /^#?!?/, '' );
$( '#inputform' ).apply_query( hash );
+ [ 'l1', 'l2' ].forEach(function (id) {
+ for (var l=0,n=loans_count( id ); l<n; l++) {
+ add_loan( id );
+ }
+ update_loans( id );
+ });
+
+ $( '#inputform' ).apply_query( hash );
+
+
+
// trigger initial rendering
update_app();
View
8 js/jquery.to_query.js
@@ -23,6 +23,9 @@
if ( ctrl.type === 'checkbox' ) {
val = ctrl.defaultChecked ? ctrl.value : '';
}
+ else if ( ctrl.type === 'hidden' ) {
+ val = ''; // hidden values don't track defaults
+ }
// TODO: radio
else if ( ctrl.type === 'select-one' ) {
var d = -1;
@@ -100,7 +103,10 @@
if ( this.type === 'checkbox' ) {
this.checked = this.defaultChecked;
}
- else {
+ else if ( this.type === 'hidden' ) {
+ //this.value = getDefaultValue( this );
+ }
+ else {
this.value = getDefaultValue( this );
}
}

No commit comments for this range

Something went wrong with that request. Please try again.