Mathematical notebook

Arjen Markus (18 december 2003) I am mildly proud to present a first (underdocumented) version of a mathematical notebook.


LV Is this in any way related to your tclmath.kit mathematical workbench work?

AM Yes, in a manner of speaking: it is another way of working with mathematical subjects in Tcl. This time, a bit more educational/presentational. I guess I am looking for the right form.


After at least 10000 seconds of hard labour, meticulous programming and self-defying testing, it works! I had to use the most cunning techniques I could muster:

  • pre-coding design phase (meaning I wrote some scribbles on a piece of paper on the way home)
  • rapid application development (meaning I was anxious to see it grow, so I ran it as soon as something could show up)
  • code reuse by multiple copy-paste-edit cycles (I hardly ever start from scratch, but simply copy an existing file into a new directory)
  • visual testing (meaning I tried to see that it did what I wanted it to do)

This was all needed to meet my deadline: the end of the evening.

But I did it!

Below you will find the first fruits of this herculean achievement. Enjoy!

Okay, what is it? It is a viewer for files that I call "mathematical notes":

  • HTML-like formatted text to show an explanation
  • Tcl code to define entry widgets for editing parameters
  • Tcl code to embed a canvas widget with a picture

The example will make it a bit clearer.

Note: this is not only meant for the mathematically inclined, you can set up any kind of "exercise book" you like.

This being the first version, its user-interface is limited: You will have to use a command-line like:

   wish mathbook.tcl example.nb

Example of an input file - store this as a file "example.nb"

For other examples: Mathematical notebook - examples

AM I updated the code below dd 22 december 2003.

AM Here is a new version - Mathematical notebook revisited


 # Example of a mathematical note to show the possibilities
 #
 # Note:
 # The number of HTML-like tags is kept to a bare minimum:
 # - <h1> big letters (the whole line; </h1> ignored)
 # - <p> for a new paragraph
 # - <br> to break the line
 # - <pre> and </pre> to indicate preformatted text
 # - leading blanks are significant
 #
 # These tags are only recognised at the start of the line!
 #
 # Initialisation: define a simple function
 #
 @init {
    proc pol3 {x} {
       variable a
       expr {1.0+$a*$x+$x*$x*$x}
    }

    set a     0.0
    set xmin -5.0
    set xmax  5.0
 }
 <h1>Third-degree polynomial functions</h1>
 <p>
 This note lets you inspect the following function:
 <pre>

    f(x) = 1.0 + a*x + x^3

 </pre>

 You can fill in the parameter a:
 @entry a 10
 <p>
 You can also change the horizontal limits of the graph:
 <p>
 Minimum x:
 @entry xmin 10
 <br>
 Maximum x:
 @entry xmax 10
 <p>
 By changing the value of "a" and pressing the Refresh button (or the
 Enter key), you update the graph:
 <p>
 @canvas 300 200 {
    set nosteps 50
    set data [func pol3 $xmin $xmax $nosteps]
    scale $data
    axes
    polyline $data black
    polyline [list $xmin $xmin $xmax $xmax] red
    text     0.0 0.0 "Origin"
 }

 Note: the red line is the line "y = x"

End of example


Here is the code:


 # mathbook.tcl --
 #    Script to show notes on mathematical subjects
 #
 # TODO:
 #    - Implement a number of useful drawing commands
 #    - Implement a formula renderer (a basic one _is_ available)
 #    - Implement more convenient bindings
 #    - Describe the application
 #
 #    Missing commands:
 #    @refresh - define your own refresh method
 #    @label   - allow a label (useful for variable text)
 #    @button  - allow a pushbutton
 #
 package require Tk

 # Slightly updated version of gtklook.tcl included 
 if { [tk windowingsystem] == "x11" } {
   . configure -background #dcdad5
   option add *background #dcdad5
   option add *foreground black
   option add *borderWidth 1 widgetDefault
   option add *activeBorderWidth 1 widgetDefault
   option add *selectBorderWidth 1 widgetDefault
   option add *font -adobe-helvetica-medium-r-normal-*-12-*-*-*-*-*-*

   option add *padX 2
   option add *padY 4

   option add *Listbox.background white
   option add *Listbox.selectBorderWidth 0
   option add *Listbox.selectForeground white
   option add *Listbox.selectBackground #4a6984

   option add *Entry.foreground black
   option add *Entry.background white
   option add *Entry.selectBorderWidth 0
   option add *Entry.selectForeground white
   option add *Entry.selectBackground #4a6984

   option add *Text.background white
   option add *Text.selectBorderWidth 0
   option add *Text.selectForeground white
   option add *Text.selectBackground #4a6984

   option add *Menu.activeBackground #4a6984
   option add *Menu.activeForeground white
   option add *Menu.activeBorderWidth 0
   option add *Menu.highlightThickness 0
   option add *Menu.borderWidth 2

   option add *MenuButton.activeBackground #4a6984
   option add *MenuButton.activeForeground white
   option add *MenuButton.activeBorderWidth 0
   option add *MenuButton.highlightThickness 0
   option add *MenuButton.borderWidth 0

   option add *highlightThickness 0
   option add *troughColor #bdb6ad
 }


 # MathData --
 #    Namespace for the user-defined commands and data
 #
 namespace eval ::MathData:: {
    variable CNV          ""
    variable TXT          ""
 }

 # scale --
 #    Set up the scaling for the given canvas
 # Arguments:
 #    data            List of data (x, y, x, y ...)
 # Result:
 #    None
 # Side effects:
 #    Scaling parameters set
 # Note:
 #    TODO: Should make sure there is some scaling involved
 #          if only using pixels
 #
 proc ::MathData::scale { data } {
    variable CNV
    variable SCALE

    set width  [$CNV cget -width]
    set height [$CNV cget -height]
    set xmin  1.0e30
    set xmax -1.0e30
    set ymin  1.0e30
    set ymax -1.0e30

    foreach {x y} $data {
       if { $x < $xmin } { set xmin $x }
       if { $x > $xmax } { set xmax $x }
       if { $y < $ymin } { set ymin $y }
       if { $y > $ymax } { set ymax $y }
    }

    if { $xmin == $xmax } { set xmax [expr {$xmax+1.0}] }
    if { $ymin == $ymax } { set ymax [expr {$ymax+1.0}] }

    set SCALE(xscale) [expr {$width/double($xmax-$xmin)}]
    set SCALE(yscale) [expr {$height/double($ymax-$ymin)}]
    set SCALE(xmin)   $xmin
    set SCALE(xmax)   $xmax
    set SCALE(ymin)   $ymin
    set SCALE(ymax)   $ymax
 }

 # polyline --
 #    Draw a line consisting of multiple points
 # Arguments:
 #    data            List of data (x, y, x, y ...)
 #    colour          Colour to use (default: black)
 # Result:
 #    None
 # Side effects:
 #    Line drawn according to current scales
 #
 proc ::MathData::polyline { data {colour black} } {
    variable CNV
    variable SCALE

    set xscale $SCALE(xscale)
    set yscale $SCALE(yscale)
    set xmin   $SCALE(xmin)
    set xmax   $SCALE(xmax)
    set ymin   $SCALE(ymin)
    set ymax   $SCALE(ymax)

    set pixels {}
    foreach {x y} $data {
       set px [expr {$xscale*($x-$xmin)}]
       set py [expr {$yscale*($ymax-$y)}]
       lappend pixels $px $py
    }

    $CNV create line $pixels -fill $colour
 }

 # text --
 #    Draw a text string at a given position
 # Arguments:
 #    x               X coordinate
 #    y               Y coordinate
 #    string          String to show
 # Result:
 #    None
 # Side effects:
 #    String drawn
 #
 proc ::MathData::text { x y string } {
    variable CNV
    variable SCALE

    set xscale $SCALE(xscale)
    set yscale $SCALE(yscale)
    set xmin   $SCALE(xmin)
    set xmax   $SCALE(xmax)
    set ymin   $SCALE(ymin)
    set ymax   $SCALE(ymax)

    set px [expr {$xscale*($x-$xmin)}]
    set py [expr {$yscale*($ymax-$y)}]

    $CNV create text $px $py -text $string -anchor nw
 }

 # axes --
 #    Draw two lines representing the axes
 # Arguments:
 #    None
 # Result:
 #    None
 # Side effects:
 #    Two lines drawn (no labels yet)
 #
 proc ::MathData::axes { } {
    variable CNV
    variable SCALE

    set width  [$CNV cget -width]
    set height [$CNV cget -height]
    set xscale $SCALE(xscale)
    set yscale $SCALE(yscale)
    set xmin   $SCALE(xmin)
    set xmax   $SCALE(xmax)
    set ymin   $SCALE(ymin)
    set ymax   $SCALE(ymax)

    set px0 [expr {$xscale*(0.0-$xmin)}]
    set py0 [expr {$yscale*($ymax-0.0)}]

    $CNV create line $px0 0    $px0   $height -fill black
    $CNV create line 0    $py0 $width $py0    -fill black
 }

 # func --
 #    Repeatedly run a function and return xy-pairs
 # Arguments:
 #    funcname       Name of the function (procedure)
 #    xmin           Minimum x-value
 #    xmax           Maximum x-value
 #    nosteps        Number of steps (inbetween; default: 50)
 # Result:
 #    List of x, y values
 #
 proc ::MathData::func { funcname xmin xmax { nosteps 50 } } {

    set coords {}
    set xstep  [expr {($xmax-$xmin)/double($nosteps)}]

    for { set i 0 } { $i <= $nosteps } { incr i } {
       set x [expr {$xmin+$i*$xstep}]
       set y [$funcname $x]
       lappend coords $x $y
    }

    return $coords
 }

 # MathBook --
 #    Namespace for the mathbook commands and data
 #
 namespace eval ::MathBook:: {
    variable count        0
    variable CNV
    variable TXT
 }

 # @init --
 #    Execute code once (when reading the notebook file)
 # Arguments:
 #    code            Code to run
 # Result:
 #    Nothing
 #
 proc ::MathBook::@init { code } {

    namespace eval ::MathData $code

 }

 # @canvas --
 #    Create a canvas of given size
 # Arguments:
 #    width            Width in pixels
 #    height           Height in pixels
 #    code             Code to execute
 # Result:
 #    Nothing
 # Side effect:
 #    Canvas created
 #
 proc ::MathBook::@canvas { width height code } {
    variable CNV
    variable CNVCODE
    variable TXT
    variable count

    incr count

    set CNV             $TXT.cnv$count
    set ::MathData::CNV $CNV

    set CNVCODE($CNV)   $code

    canvas $CNV -width $width -height $height -bg white
    $TXT insert end "\n"
    $TXT window create end -window $CNV
    $TXT insert end "\n"

    namespace eval ::MathData $code

 }

 # @entry --
 #    Create an entry widget of given width
 # Arguments:
 #    name             Name of the associated variable
 #    width            Width of the widget (in characters)
 # Result:
 #    Nothing
 # Side effect:
 #    Entry created
 #
 proc ::MathBook::@entry { name width } {
    variable TXT
    variable count

    incr count

    set entry $TXT.entry$count

    entry $entry -textvariable ::MathData::$name -width $width
    $TXT window create end -window $entry

    bind $entry <Return> ::MathBook::Refresh
 }

 # @label --
 #    Create a label widget of given width
 # Arguments:
 #    name             Name of the associated variable
 #    width            Width of the widget (in characters)
 # Result:
 #    Nothing
 # Side effect:
 #    Entry created
 #
 proc ::MathBook::@label { name width } {
    variable TXT
    variable count
 
    incr count

    set label $TXT.label$count

    label $label -textvariable ::MathData::$name -width $width \
                 -background white -anchor nw -font "Courier 10"
    $TXT window create end -window $label
 }

 # @refresh --
 #    Define a refresh method - called before the canvas methods
 # Arguments:
 #    code        Code to be run on refresh
 # Result:
 #    None
 # Side effect:
 #    Defines the REFRESH variable
 #
 proc ::MathBook::@refresh { code } {
    variable REFRESH

    set REFRESH $code
 } 

 # Refresh --
 #    Refresh the canvases and labels etc.
 # Arguments:
 #    None
 # Result:
 #    None
 # Side effect:
 #    Canvases refreshed and whatever occurs in the @refresh method
 #
 proc ::MathBook::Refresh { } {
    variable CNV
    variable CNVCODE
    variable REFRESH
    variable TXT
    variable count

    if { [info exists REFRESH] } {
       namespace eval ::MathData $REFRESH
    }
    foreach {name code} [array get CNVCODE] {
       set ::MathData::CNV $name
       $name delete all
       namespace eval ::MathData $code
    }
 }

 # initMainWindow --
 #    Create the main window
 # Arguments:
 #    None
 # Result:
 #    None
 # Side effect:
 #    Main window created
 #
 proc ::MathBook::initMainWindow { } {
    variable TXT
    variable count

    set count 0

    set tf  .textframe
    set tw  $tf.text
    set TXT $tw

    frame $tf
    scrollbar $tf.scrollx -orient horiz -command "$tw xview"
    scrollbar $tf.scrolly               -command "$tw yview"
    text      $tw         -yscrollcommand "$tf.scrolly set" \
                          -xscrollcommand "$tf.scrollx set" \
                          -fg black -bg white -font "courier 10" \
                          -wrap word

    grid      $tw         $tf.scrolly
    grid      $tf.scrollx x
    grid      $tw         -sticky news
    grid      $tf.scrolly -sticky ns
    grid      $tf.scrollx -sticky ew

    grid columnconfigure $tf 0 -weight 1
    grid rowconfigure    $tf 0 -weight 1

    button .refresh -text Refresh -command ::MathBook::Refresh -width 10
    button .exit    -text Exit    -command exit -width 10
    grid $tf      -       -sticky news
    grid .refresh .exit   -sticky nw

    grid columnconfigure . 0 -weight 1
    grid columnconfigure . 1 -weight 1
    grid rowconfigure    . 0 -weight 1

    $tw tag configure bigbold -font "helvetica 12 bold"
    $tw tag configure normal  -font "courier 10"
    $tw tag configure preform -font "courier 10" -background "lightgrey"
 }

 # fillTextWindow --
 #    Fill the text window
 # Arguments:
 #    filename       Name of the notebook file to use
 # Result:
 #    None
 # Side effect:
 #    Text window filled
 #
 proc ::MathBook::fillTextWindow { filename } {
    variable TXT

    set infile [open $filename "r"]

    set just ""
    set tag normal

    while { [gets $infile line] >= 0 } {
       set trimmed [string trim $line]

       #
       # Analyse the contents ...
       #
       if { [string first "#" $trimmed] == 0 } {
          continue
       }
       # Ignore empty lines, unless in preformatted text
       if { $trimmed == "" } {
          if { $just != "" } {
             $TXT insert end "\n" $tag
          }
          continue
       }

       if { [string first "@" $trimmed] == 0 } {
          RunWholeCommand $infile $line
          continue
       }

       if { [string first "<h1>" $trimmed] == 0 } {
          set tag bigbold
          set trimmed [string map {<h1> "" </h1> ""} $trimmed]
       }
       if { [string first "<pre>" $trimmed] == 0 } {
          $TXT insert end "\n"
          set tag  "preform"
          set just "\n"
          continue
       }
       if { [string first "</pre>" $trimmed] == 0 } {
          $TXT insert end "\n"
          set tag  "normal"
          set just ""
          continue
       }
       if { [string first "<p>" $trimmed] == 0 } {
          $TXT insert end "\n\n"
          continue
       }
       if { [string first "<br>" $trimmed] == 0 } {
          $TXT insert end "\n"
          continue
       }
       if { $just == "" } {
          $TXT insert end "$trimmed " $tag
       } else {
          $TXT insert end "$line\n" $tag
       }
       if { $tag == "bigbold" } {
          set tag "normal"
       }
    }

    close $infile
    $TXT configure -state disabled
 }

 # RunWholeCommand --
 #    Run an embedded command
 # Arguments:
 #    infile         Handle to the file
 #    line           First line of the command
 # Result:
 #    None
 # Side effect:
 #    Whatever the command does
 #
 proc ::MathBook::RunWholeCommand { infile line } {
    variable TXT

    while { ! [info complete $line] } {
       if { [gets $infile nextline] >= 0 } {
          append line "\n$nextline"
       } else {
          break
       }
    }

    eval $line
 }

 # main --
 #    Get the whole thing going
 #
 ::MathBook::initMainWindow
 ::MathBook::fillTextWindow [lindex $argv 0]