|   | Hosted by  | 
|  | 
| Mortgage Payments: Interest and Principal
This Web page shows a simple example that uses the
Tcl web browser plugin. Enter information about a home
loan in the three entries, then type Return in any of the entries or
click on the  
 Source:The Tcl script for this application is about 270 lines long, including comments: # mortgage.tcl --
#
# This file contains a script that displays a simple spreadsheet for
# calculating interest payments on a mortgage or other loan.  It is
# intended to be used in conjunction with the Tcl/Tk plugin for
# Netscape.
#
# SCCS: @(#) mortgage.tcl 1.2 96/06/05 17:43:53
eval destroy [winfo child .]
# The following variables provide configuration information that
# controls the display.
set axisFont {Helvetica 12 bold}
set titleFont {Helvetica 18 bold}
set barColor #7ab8cd
set accentColor #90daf3
set msgColor red
set graph(width) [winfo pixels . 5i]
set graph(height) [winfo pixels . 3i]
set graph(xOrigin) [winfo pixels . 0.8i]
set graph(yOrigin) [winfo pixels . 3.5i]
# monthly --
#
# Given a loan principal, a term in years, and an annual percentage
# rate, returns the monthly payment.
#
# Arguments:
# principal -		The amount borrowed.
# term -		The time over which the loan must be repaid, in used.
# rate -		The interest rate, in percent.
proc monthly {principal term rate} {
    set months [expr round($term*12)]
    set interest [expr $rate/1200.0]
    set principal [expr (double($principal))]
    if {$interest <= 0} {
	 set payment [expr $principal/$months]
    } else {
	set annuity [expr (1 - pow(1+$interest, -$months))/$interest]
	set payment [expr $principal/$annuity]
    }
    return $payment
}
# validate --
# This procedure is invoked to make sure that an entry contains a valid
# number.  If so, it returns 1.  If not, it sets the global variable
# "msg" with a diagnostic message, moves the input focus to the
# offending entry, and returns 0.
#
# Arguments:
# entry -		Name of the entry widget.
# info -		Description of the value stored in the entry, such
#			as "principal";  used in diagnostic messages.
proc validate {entry info} {
    global msg
    set value [$entry get]
    if {$value == ""} {
	set msg "Please enter a $info"
	focus $entry
	return 0
    }
    if {$value <= 0} {
	set msg "Please enter a positive $info"
	focus $entry
	return 0
    }
    if [catch {expr {2 * $value}}] {
	set msg "The $info isn't a proper number; please re-enter"
	$entry selection range 0 end
	focus $entry
	return 0
    }
    if {($info == "loan period") && ($value > 60)} {
	set msg "The $info is too long - use a $info of less than 60 years"
	$entry selection range 0 end
	focus $entry
	return 0
    }
    if {($info == "rate of interest") && ($value > 20)} {
	set msg "You can't afford an interest rate of $value%!"
	$entry selection range 0 end
	focus $entry
	return 0
    }
    return 1
}
# calculate --
#
# This procedure is invoked whatever Return is typed in any of the entry
# widgets.  It validates the contents of the entries, and recalculates
# the payment information.
#
# Arguments:
# None.
proc calculate {} {
    global principal term rate msg graph
    if {![validate .f1.e "principal amount"]
	    || ![validate .f2.e "loan period"]
	    || ![validate .f3.e "rate of interest"]} {
	return
    }
    set msg "Calculating "
    update idletasks
    set payment [monthly $principal $term $rate]
    .pay configure -text "Monthly payment is [format {$%.2f} $payment]."
    set msg ""
    plot .c $graph(xOrigin) $graph(yOrigin) $graph(width) $graph(height) $principal $term $rate $payment
}
# yaxis --
#
# This procedure picks an an appropriate scale factor for the y-axis of
# a plot, and draws the y axis, tick marks, and labels in a canvas.  It
# returns the vertical scale factor to use for the plot (what to
# multiply a y-value by to get a canvas coordinate).
#
# Arguments:
# c -			Canvas in which to draw.
# xOrigin, yOrigin -	Location of the origin for the plot.
# height -		Height of the y-axis, in pixels.
# yMax -		Maximum y-coordinate that will be displayed.
# format -		Format string to use when displaying tick labels
#			(such as %.2f).
proc yaxis {c xOrigin yOrigin height yMax format} {
    global axisFont
    set log [expr log10($yMax)]
    set unit [expr pow(10, floor($log))]
    set factor [expr $yMax/$unit]
    if {$factor <= 2.0} {
	set unit [expr $unit/5.0]
    } elseif {$factor < 5.0} {
	set unit [expr $unit/2.0]
    }
    set nUnits [expr floor(($yMax + $unit - 1)/$unit)]
    set scale [expr $height/($unit*$nUnits)]
    $c create line $xOrigin $yOrigin $xOrigin [expr $yOrigin-$height] \
	    -width 1 -fill black
    set x2 [expr $xOrigin + 5]
    for {set i 0} {$i <= $nUnits} {incr i} {
	set y [expr $i*$unit]
	set yCanv [expr $yOrigin - $y * $scale]
	$c create line $xOrigin $yCanv $x2 $yCanv -width 1 -fill black
	$c create text [expr $xOrigin-2] $yCanv -text [format $format $y] \
		-anchor e -font $axisFont -fill black
    }
    return $scale
}
# xaxis --
#
# This procedure is similar to yaxis above except that it handles the
# x-axis instead of a y-axis.
#
# Arguments:
# c -			Canvas in which to draw.
# xOrigin, yOrigin -	Location of the origin for the plot.
# width -		Width of the y-axis, in pixels.
# xMax -		Maximum x-coordinate that will be displayed.
# format -		Format string to use when displaying tick labels
#			(such as %.2f).
proc xaxis {c xOrigin yOrigin width xMax format} {
    global axisFont
    set log [expr log10($xMax)]
    set unit [expr pow(10, floor($log))]
    set factor [expr $xMax/$unit]
    if {$factor <= 2.0} {
	set unit [expr $unit/5.0]
    } elseif {$factor < 5.0} {
	set unit [expr $unit/2.0]
    }
    set nUnits [expr floor(($xMax + $unit - 1)/$unit)]
    set scale [expr $width/($unit*$nUnits)]
    $c create line $xOrigin $yOrigin [expr $xOrigin+$width] $yOrigin \
	    -width 1 -fill black
    set y2 [expr $yOrigin - 5]
    for {set i 0} {$i <= $nUnits} {incr i} {
	set x [expr $i*$unit]
	set xCanv [expr $xOrigin + $x * $scale]
	$c create line $xCanv $yOrigin $xCanv $y2 -width 1 -fill black
	$c create text $xCanv [expr $yOrigin+2] -text [format $format $x] \
		-anchor n -font $axisFont -fill black
    }
    return $scale
}
# plot --
#
# Given information about a loan and a canvas to draw in, create a bar
# chart in the canvas of principal paid, as a function of time, with
# active bars that provide additional information when the mouse passes
# over them.
#
# Arguments:
# c -			Canvas in which to draw.
# xOrigin, yOrigin -	Location of the origin for the plot.
# width, height -	Dimensions of the plot, in pixels.
# principal -		Initial loan amount.
# term -		Duration of loan, in years.
# rate -		Interest rate for the loan, in percent.
# payment -		Monthly payment.
proc plot {c xOrigin yOrigin width height principal term rate payment} {
    global barColor accentColor titleFont axisFont
    $c delete all
    set xScale [xaxis $c $xOrigin $yOrigin $width $term %.0f]
    set yScale [yaxis $c $xOrigin $yOrigin $height $principal %.0f]
    set x [expr $xOrigin + $width/2]
    $c create text $x [expr $yOrigin - $height - 5] \
	    -text {Principal Paid ($)} -font $titleFont -anchor s
    $c create text $x [expr $yOrigin + [winfo pixels $c 16p]] \
	    -text "Year Of Loan" -font $axisFont -anchor n
    set orig $principal
    set rate [expr $rate/1200.0]
    for {set year 1} {$year <= $term} {incr year} {
	for {set month 0} {$month < 12} {incr month} {
	    set principal [expr $principal + ($principal*$rate) - $payment]
	}
	if {$year == 1} {
	    set plural ""
	} else {
	    set plural "s"
	}
	set princPaid [expr $orig - $principal]
	set msg [format {After %d year%s you will have paid $%.0f\
		of principal and $%.0f of interest.} $year $plural $princPaid \
		[expr 12*$year*$payment - $princPaid]]
	set x2 [expr $xOrigin + $xScale*$year]
	set x1 [expr $x2 - $xScale]
	set y1 [expr $yOrigin - $yScale*$princPaid]
	if {$y1 > ($yOrigin-1)} {
	    set y1 [expr $yOrigin - 1]
	}
	set id [$c create rectangle $x1 $y1 $x2 $yOrigin -fill $barColor \
		-outline black -width 1 -tags bar]
	$c bind $id <Enter> [list set msg $msg]
    }
    $c bind bar <Enter> "$c itemconfigure current -fill $accentColor"
    $c bind bar <Leave> "$c itemconfigure current -fill $barColor"
    $c lower bar
}
# resize --
#
# This procedure is invoked when the canvas window used for plotting
# changes size.  It recalculates the geometry of the plot to take
# advantage of all the space available in the canvas.
#
# Arguments:
# c -			Name of the canvas widget that changed size.
proc resize c {
    global graph
    set graph(width) [expr [winfo width $c] - [winfo pixels $c 1i]]
    set graph(height) [expr [winfo height $c] - [winfo pixels $c 1i]]
    set graph(xOrigin) [winfo pixels $c .8i]
    set graph(yOrigin) [expr $graph(height) + [winfo pixels $c .5i]]
}
frame .f1
frame .f2
frame .f3
pack .f1 .f2 .f3 -side top -anchor w -fill x
label .f1.l -text "Enter principal amount ($):" -width 25 -anchor w
entry .f1.e -textvariable principal
bind .f1.e <Return> calculate
pack .f1.l .f1.e -side left
label .f2.l -text "Enter loan period (years):" -width 25 -anchor w
entry .f2.e -textvariable term
bind .f2.e <Return> calculate
pack .f2.l .f2.e -side left
button .f2.calculate -text Calculate -command calculate
pack .f2.calculate -side right -expand 1
label .f3.l -text "Enter interest rate (%):" -width 25 -anchor w
entry .f3.e -textvariable rate
bind .f3.e <Return> calculate
pack .f3.l .f3.e -side left
label .pay -anchor w
pack .pay -side top -fill x
canvas .c -width 10 -height 10 -relief sunken -bd 2
pack .c -side top -anchor w -fill both -expand yes
bind .c <Configure> {resize .c}
label .msg -textvariable msg -anchor w -fg $msgColor
pack .msg -side top -fill x
 |