A Bike Gear Ratio Calculator

[Sun Home | Tcl Plugin | Demos]

This application computes the effect of different gear combinations. One way to visualize this is to think of replacing your modern bicycle with an old fashioned "ordinary" bike that has a large diameter front wheel and the pedals connected right to the front axel. In those bikes, the mechanical advantage comes from the wheel diameter. The larger the wheel the more power, but the harder it is to pedal.

The graph displays the effective wheel diameter for each gear combination. The horizontal lines correspond to the different gears up front (at the pedals). The points on each line correspond to each gear in the gear cluster at the back wheel. The values are the effective wheel diameter.

To use the application, just enter numbers and press the Recompute button. Just pressing Return in a text entry causes the graph to update, too. If you move the mouse over the graph, the specific values for the point under the mouse cursor are displayed.

The graph tells you the shift sequence that lets you use your gears effectively. As you ride along, if a hill becomes steeper, you want to know the derailleur positions for the next lower gear. Similarly, if the hill starts to level out, you want to know the positions for the next higher gear. In the initial chart displayed when you load (or reload) this page, the combination of the middle front sprocket and the smallest rear gear give you an effective wheel diameter of 83 inches (move the mouse cursor over the rightmost point on the middle line to check this out). The next lower gear is 79 inches, which you get by using the large front sprocket and the third smallest rear gear. The next lower gear doesn't use the middle sprocket!

The gear ratio graph also makes it easy to tell how well spaced out your gears are. Notice in the initial chart that several derailleur positions have the same or nearly the same gearing. This 18 speed bike really has about 13 gears. You can buy real gear clusters and front sprockets to space out the gears more evenly. The gear chart application makes it easy for you to experiment with different gear combinations.

The Code

# bike.tcl
# This program computes the effective wheel diameter of a bike given its
# gear setup.  For example, if the effective diameter is 100 inches, it is
# as if you have an old-fashioned bike with a 100 inch diameter front wheel
# and the pedals are connected right to its axle.  The bigger the diameter
# the harder it is to pedal.

option add *Entry.width 2
option add *background gray75
option add *highlightBackground gray75
. config -borderwidth 10 -bg gray75

if [info exists embed_args] {
    # Don't resize within plugin
    grid propagate . false
    set maxwidth $embed_args(width)
} else {
    set maxwidth 400
message .msg -text "Bicycle Gear Ratios" -aspect 2000

# Every text entry widget can be associated with a Tcl variable.  In these cases
# front(n) and back(n) are the number of gears at the front and back of the bike.
entry .efront -textvariable front(n)
entry .eback -textvariable back(n)

label .lfront -text "# front gears"
label .lback -text "# back gears"

entry .size -textvariable size
label .lsize -text "Wheel diameter"
set size 27

# Hitting Return in a text entry recomputes the graph.
foreach e {.size .efront .eback} {
    bind $e <Return> UpdateRatios
button .doit -text Recompute -command UpdateRatios

# The grid geometry manager is used for the main arrangement of widgets
grid .msg  -columnspan 3        -sticky news
grid .lfront .efront		-sticky w
grid .lback  .eback		-sticky w
grid .lsize  .size .doit	-sticky w
grid .doit 			-sticky e

# When the number of gears is changed, the set of entries for each gear is re-created.
trace variable front(n) w UpdateFront
trace variable back(n) w UpdateBack

proc UpdateFront {args} {
    UpdateBoxes .front 1 front ff0000 110000
proc UpdateBack {args} {
    UpdateBoxes .back 2 back 0000ff 000011
proc UpdateBoxes {frame row varName color dColor} {
    # upvar creates a local name alias for a global variable (absolute scope #0)
    # In this case, 'var' is an alias for 'front' or 'back'
    upvar #0 $varName var
    catch {destroy $frame}
    if ![NumberOk var(n) 1 10] {
    # Make a horizontal stack of a label and several entries, using the packer
    frame $frame
    label $frame.teeth -text "# teeth"
    pack $frame.teeth -side left
    for {set i 0} {$i < $var(n)} {incr i} {
	entry $frame.$i -textvariable $varName\($i) -foreground #$color
	# Fade the text color from bright to black
	set color [format %06x [expr 0x$color - 0x$dColor]]
	pack $frame.$i -side left
	bind $frame.$i <Return> UpdateRatios
    # Position the horizontal stack in the main grid of widgets
    grid $frame -column 2 -row $row -sticky w
# Validate a number.  Returns 0 if there isn't a number in the variable.
proc NumberOk {varName min max} {
    upvar $varName var
    if {([string length [string trim $var]] == 0)} {
	return 0
    if {[catch {expr $var}]} {
	set var ""
	return 0
    if {$var < $min} {
	set var $min
    if {$var > $max} {
	set var $max
    return 1

# Set default values for the front and back gear configuration.
array set front {
    0 27
    1 40 
    2 50

set front(n) [expr [array size front] -1]
array set back {
    0 27
    1 21
    2 19
    3 17
    4 15
    5 13
set back(n) [expr [array size back] -1]

# UpdateRatios displays a graph of the effective wheel diameter.
proc UpdateRatios {} {
    global front back
    # Find the minimum and maximum gear sizes in front and back.
    foreach name {front back} {
	# Here upvar creates an alias for a local variable (scope 0 frames up)
	upvar 0 $name array
	catch {unset array(max)}
	catch {unset array(min)}
	for {set n 0} {$n < $array(n)} {incr n} {
	    if {([string length [string trim $array($n)]] == 0) ||
		[catch {expr $array($n)}]} {
	    if {![info exists array(min)] || ($array($n) < $array(min)) } {
		set array(min) $array($n)
	    if {![info exists array(max)] || ($array($n) > $array(max)) } {
		set array(max) $array($n)
	if {![info exists array(max)] || ![info exists array(min)]} {
    # Find the bounds of the graph, rounded to 5
    set min [expr [Gearage $front(min) $back(max)]/5 * 5]
    set max [expr ([Gearage $front(max) $back(min)]+5)/5 * 5]
    set span [expr $max - $min]

    if [winfo exists .c] {
	.c delete all
    } else {
	canvas .c
    global maxwidth
    set y 10
    set xoff 20
    set xscale [expr double($maxwidth-$xoff-35)/$span]
    set fcolor ff0000
    set df     110000
    for {set f 0} {$f < $front(n)} {incr f} {
	if ![NumberOk front($f) 3 1000] {
	# Create a horizontal line for each front gear, same color as text entry.
	.c create text 0 $y -text $front($f) -fill #$fcolor
	.c create line $xoff $y [expr $xscale*$span + $xoff] $y -fill #$fcolor
	set fcolor [format %06x [expr 0x$fcolor - 0x$df]]

	set bcolor 0000ff
	for {set b 0} {$b < $back(n)} {incr b} {
	    if ![NumberOk back($b) 3 1000] {
	    if [catch {Gearage $front($f) $back($b)} g] {
	    # Plot the points in the same color as rear wheel text.
	    set x [expr $xscale *($g-$min) +$xoff]
	    set id [.c create oval [expr $x-3] [expr $y-3] [expr $x+3] [expr $y+3] \
		-fill #$bcolor -tags "gearage=$g f=$f b=$b"]
	    set bcolor [format %06x [expr 0x$bcolor - 0x11]]
	incr y 20
    bind .c <ButtonPress-1> [list FeedbackOn %x %y]
    bind .c <Motion> [list FeedbackOn %x %y]
    bind .c <ButtonRelease-1> [list FeedbackOff]
    .c create text $xoff $y -text $min -anchor w
    .c create text [expr $xscale*$span+$xoff] $y -text $max -anchor e
    .c create text [expr ($xscale*$span+$xoff)/2] $y \
	-text "Effective Wheel Diameter" -anchor c
    # Size the canvas to fit the plot
    .c move all 10 10
    foreach {x1 y1 x2 y2} [.c bbox all] {}
    .c config -width [expr $x2 + 0] -height [expr $y2 +0]
    grid .c -row 4 -column 0 -columnspan 3
proc Gearage {f b} {
    global size
    expr round($f.0 / $b.0 * $size)
# As the mouse moves over the plot, display the exact value for each point it crosses.
proc FeedbackOn {x y} {
    global feedback
    foreach id [.c find overlapping [expr $x-2] [expr $y-2] [expr $x+2] [expr $y+2]] {
	if [regexp {gearage=([0-9]+) f=([0-9]+) b=([0-9]+)} [.c itemconfigure $id -tags] junk g f b] {
	    foreach {x1 y1 x2 y2} [.c coords $id] {}
	    set feedback [.c create text $x1 $y1 -text $g -fill blue -anchor s]
	    .front.$f config -bg white
	    .back.$b config -bg white
	    lappend feedback $f $b
proc FeedbackOff {} {
    global feedback
    catch {.c delete [lindex $feedback 0]}
    catch {.front.[lindex $feedback 1] config -bg grey75}
    catch {.back.[lindex $feedback 2] config -bg grey75}
update	;# So auto-sizing works ok