Another Graphing Widget

GWM This is an example of creating a derived widget - in this case for drawing a graph. The main differences are:

 - lines to be drawn are defined in 'user coordinates' not pixels
 - other added items (polygons, text, ovals..) can be positioned from user coordinates
 - the coordinates use normal graph directions (canvas pixels are defined as 0 at top, large at bottom of screen).
 - the pen colour is an option which persists from object to object.

The code is a simplification of Overloading widgets and shows how to add an option to a derived widget. I do not like my use of globals here to store the widget's options. Any improvements/suggestions sought.

I have simplified the options to using an array rather than separate globals. The exercising example is noww run from a button in the Wish84 window.

15/05/.05 - corrected initialisation of graph ranges (xmin, xmax...).

 proc grapharea {w args} { ;# a graph plotter canvas derived thing.
        set dx 400
        set dy 400
        global $w.props ;# list of specialised options
        array set $w.props { -pencolour black -xmin -1 -xmax 1 -ymin -1 -ymax 1}
        array set options {}
        # split off the custom options:
        set generalopts [list]
        foreach {opt val} $args {
                if {[array names $w.props $opt]!=""} {set options($opt) $val
                } else { lappend generalopts $opt $val }
        eval canvas $w $generalopts ;# generalopts are the options inherited from canvas.

        interp hide {} $w
        # Install the alias:
        interp alias {} $w {} graphareaCmd $w
        foreach opt [array names options] {
                $w configure $opt $options($opt)
          return $w ;# the original canvas
  proc graphareaCmd {self cmd args} {
        switch -- $cmd {
                configure {eval graphareaConfigure $self $cmd $args}
                cget {eval graphareaCget $self $args}
                showgrid { ;# draw a grid of lines on the graph
                        set x0 [$self cget -xmin];                set x1 [$self cget -xmax]
                        set y0 [$self cget -ymin];                set y1 [$self cget -ymax]
                        set dx [lindex $args 0] ;#number of grids in X direction
                        set x [expr int($x0/$dx)*$dx] ;# choose lines s.t. zero is a line.
                        while {$x<$x1} {
                                $self create line $x $y0 $x $y1
                                set x [expr $x+$dx]
                        set dy [lindex $args 1] ;# No grids in Y direction
                        set y [expr int($y0/$dy)*$dy]
                        while {$y<$y1} {
                                $self create line $x0 $y $x1 $y
                                set y [expr $y+$dy]
                  create  { ;# replaces the create default of a canvas which works in pixels
                        #- adds text, rectangle, oval, polygon... (eg) positioned at scaled position
                        set args [eval concat $args] ;# this removes a level of curlies if necessary from the list.
                        set penc [$self cget -pencolour];# get "local" pen colour name
                        # scale factor for draw space to pixels
                        set x0 [$self cget -xmin];                set x1 [$self cget -xmax]
                        set y0 [$self cget -ymin];                set y1 [$self cget -ymax]
                        set wid [$self cget -width]
                        set ht [$self cget -height]
                        set idx 1 ;# where to start in args for coordinates
                        while {[string is double [lindex $args 1]] && [llength $args]>1} {
                                if {$idx%2==1} {
                                        lappend xylist [expr {int($wid*double([lindex $args 1]-$x0)/($x1-$x0))}]
                                } else { ;# a y coordinate
                                        lappend xylist [expr {int($ht*double([lindex $args 1]-$y1)/($y0-$y1))}]
                                set args [lreplace $args 1 1]
                                incr idx
                        switch [lindex $args 0] {
                                {line}  -
                                {text} {
                                        lappend command [lindex $args 0] $xylist -fill $penc
                                        if {[llength $args]>1} { set command [concat $command [lrange $args 1 end]] }
                                        eval interp invokehidden {{}} $self create $command
                                {default} {
                                        lappend command [lindex $args 0] $xylist -outline $penc
                                        if {[llength $args]>1} { set command [concat $command [lrange $args 1 end]] }
                                        eval interp invokehidden {{}} $self create $command
                  default  { ;# pass to default of a canvas which works in pixels
                        #puts "Action $cmd $args"
                        eval interp invokehidden {{}} $self $cmd $args
  proc graphareaConfigure {self cmd args} {
        # 3 scenarios:
        # $args is empty       -> return all options with their values
        # $args is one element -> return current values
        # $args is 2+ elements -> configure the options
        global $self.props
        switch [llength $args] {
                0 { ;# return argument values
                        lappend result [array names $self.props]

                        # default options:
                        lappend result [interp invokehidden {} $self configure ]
                        #        lappend result [uplevel 1  $self cconfigure]
                        return $result
                1 {
                        if {[array names $self.props $args ] != ""} {
                                 lappend opts [$self cget $args ]
                        } else {
                                set opts [uplevel 1 interp invokehidden {} $self configure $args]
                        return $opts
                default { ;# >1 arg - an option and its value
                                # go through each option:
                                foreach {option value} $args {
                                        #puts "setting $option to $value [array names $self.props]"
                                        if {[array names $self.props $option ]!=""} {
                                                set $self.props($option) $value
                                        } else {
                                                puts " default $option, $value for $self";$self configure $option $value
                        return {}
  proc graphareaCget {self args} {
        # cget defaults done by the canvas cget command
        #puts "In graphareaCget option $self $args"
        upvar #0 $self.props props
        if {[array names props $args]!=""} { return $props($args) }
        return [uplevel 1 [list interp invokehidden {} $self cget $args]]

  proc testplotc {} { ;#Now exercise this 'new' widget 
        catch {destroy .fib}
        # create a grapharea widget.
         set dr2 [grapharea .fib -width 500 -height 400 -pencolour red -xmin -1 -xmax 20  -ymin -1.02 -ymax 1.02]
         # points are drawn with the canvas area scaled to these values:
  #        $dr2 configure -xmin -2 -xmax 20  -ymin -1.02 -ymax 1.02
        pack $dr2 -expand true -fill both
            $dr2 showgrid 5 1

        $dr2 configure -pencolour blue ;# this value persist for all lines from here
        set xold 0
        set yold [expr sin(0)]
        for {set x 0} {$x<20} {set x [expr $x+.25]} {
                set y [expr sin($x)*exp(-0.1*$x)]
                $dr2 create line $xold $yold $x $y
                set xold $x
                set yold $y
        $dr2 configure -pencolour orange
        set xold 0
        set yold [expr cos(0)]
        for {set x 0} {$x<20} {set x [expr $x+.25]} {
                set y [expr cos($x)*exp(-0.1*$x)]
                $dr2 create line $xold $yold $x $y
                set xold $x
                set yold $y
        $dr2 configure -pencolour green
        # a simple rectangle drawn as a line with 5 coordinates (10 values, last repeats first)
        $dr2 create line  1 .8 19 .8 19 -.8 1 -.8 1 .8
        $dr2 configure -pencolour yellow
        #$dr2  create polygon 0  -1 1.0 -.9 2 -.7 3 -.2 4 0.5 2 .8 -smooth true
        $dr2  create rectangle 0  -1 10.0 1.0
        $dr2  create oval 0  -1 10.0 1.0
        # you can also add 'pure' canvas options to the grapharea using pixel coordinates:
        interp invokehidden {} $dr2  create text 120 30  -fill black -text "Graph Area"
        $dr2 configure -pencolour purple
      # or you can use the graph space to define the text position
        $dr2 create text 10.0 0.9 -text {{Text Placed using Graph Space}}
        $dr2 create line 4.0 0.86 14.0 .86
        # creating a long line of coordinates
        set xys ""
        for {set x 0} {$x<20} {set x [expr $x+.25]} {
                set y [expr cos(2*$x)*sin(0.3*$x)]
                lappend xys  $x $y
        puts "Length [llength $xys] [$dr2 cget -pencolour] [$dr2 cget -xmin] [$dr2 cget -xmax]"
        puts "[$dr2 configure]"
        puts "[$dr2 configure -pencolour]"
        $dr2 create line  $xys

 puts "Call testplotc to test the plot canvas"
 set entrypts {}
 lappend entrypts {testplotc "Call testplotc to test the plot canvas"}
 catch {destroy .testplotcanvas}
 set fex [frame .testplotcanvas]
 foreach ep $entrypts { ;# create a button to launch an example procedure
        set choice [lindex $ep 0]
        button $fex.$choice -text "[lindex $ep 1]" -command [lindex $ep 0]
        pack $fex.$choice -side left
 pack $fex -side top