Use an xy-plot to edit data series

Arjen Markus (23 november 2007) Plotchart is very useful for displaying data in plots or charts, but the usual use is rather static: the data come from "somewhere" and are then shown.

The program below shows that it is possible to use it in an interactive way too. The idea is that rather than typing in a lot of numbers in a table, it is easier to click in a window and to see the resulting data series in an xy-plot immediately.

Notes:

  • It does use some inside information on the workings of Plotchart, and I hope to come up with a simpler way to achieve the effects shown here shortly.
  • You need a recent version of Plotchart, as it requires support for "missing values"
 # editgraph.tcl --
 #     Widget to edit data series graphically
 #
 #     Note:
 #     Not as elegant as could be with bindings via Plotchart itself
 #
 package require Plotchart
 
 namespace eval ::Editgraph {

     variable graph
     variable endedit
 }

 # editSeries --
 #     Procedure to edit a series of data graphically
 #
 # Arguments:
 #     xrange          Range for the x values
 #     yrange          Range for the y values
 #     number          Number of data points
 #
 # Result:
 #     List of x,y values representing the data series
 #
 # Note:
 #     The widget allows setting new y values for given x values only
 #
 proc ::Editgraph::editSeries {xrange yrange number} {

     variable graph
     variable endedit

     toplevel .graph
     wm title .graph "Editing series"
     canvas   .graph.c -width 400 -height 300
     button   .graph.ok     -text OK     -width 10 -command {::Editgraph::SaveSeries}
     button   .graph.cancel -text Cancel -width 10 -command {::Editgraph::CancelSeries}
     grid     .graph.c -
     grid     .graph.ok .graph.cancel

     #
     # Initialise the series
     #
     foreach {xmin xmax} $xrange {break}
     foreach {ymin ymax} $yrange {break}

     if { $ymin <= 0.0 && $ymax >= 0.0 } {
         set y 0.0
     } else {
         set y $ymin
     }

     set delx [expr {($xmax-$xmin)/double($number-1)}]

     set graph(xy) {}
     set x         $xmin
     while { $x < $xmax+0.5*$delx } {
         lappend graph(xy) $x $y
         set x [expr {$x + $delx}]
         if { abs($x) < 0.5 *$delx } {
             set x 0.0
         }
     }
     set graph(xmin)   $xmin
     set graph(xmax)   $xmax
     set graph(delx)   $delx
     set graph(number) $number

     set graph(plot) [::Plotchart::createXYPlot .graph.c \
                         [::Plotchart::determineScale $xmin $xmax] \
                         [::Plotchart::determineScale $ymin $ymax]]

     $graph(plot) dataconfig data -colour blue
     set graph(widget) .graph.c

     foreach {x y} $graph(xy) {
         $graph(plot) plot data $x $y
     }

     #
     # Set the bindings
     #
     # bind .graph.c <Button-1> {::Editgraph::EditPoint %W %x %y}
     bind .graph.c <ButtonPress-1> {
         ::Editgraph::EditPoint %W %x %y
         bind %W <Motion> {::Editgraph::EditPoint %W %%x %%y}
     }
     bind .graph.c <ButtonRelease-1> {
         bind %W <Motion> {}
     }


     vwait ::Editgraph::endedit
     puts "$endedit"

     if { $endedit == 1 } {
         return $graph(xy)
     } else {
         return {}
     }
 }

 # SaveSeries, CancelSeries --
 #     Callback procedure for the widget
 #
 # Arguments:
 #     None
 #
 # Result:
 #     None
 #
 # Side effects:
 #     The widget is closed
 #
 proc ::Editgraph::SaveSeries {} {
     set ::Editgraph::endedit 1
     destroy .graph
 }

 proc ::Editgraph::CancelSeries {} {
     set ::Editgraph::endedit 0
     destroy .graph
 }

 # EditPoint --
 #     Callback procedure for setting the new data point
 #
 # Arguments:
 #     w             Widget
 #     x             X-coordinate of selected point (pixel)
 #     y             Y-coordinate of selected point (pixel)
 #
 # Result:
 #     None
 #
 # Side effects:
 #     The data points are updated and the graph redrawn
 #

 proc ::Editgraph::EditPoint {w x y} {
     variable graph

     foreach {xc yc} [::Plotchart::pixelToCoords $graph(widget) $x $y] {break}

     set xp [expr {int(0.5+($xc-$graph(xmin))/$graph(delx))}]

     if { $xp < 0 } { set xp 0 }
     if { $xp > $graph(number) } { set xp $graph(number) }

     lset graph(xy) [expr {2*$xp+1}] $yc

     #
     # Reset the plotted data
     #
     $graph(widget) delete data
     $graph(plot) plot data "" ""

     #
     # Redraw the data
     #
     foreach {x y} $graph(xy) {
         $graph(plot) plot data $x $y
     }

 }

 # main --
 #     Make it all work

 catch {
     console show
 }
 set xyseries [::Editgraph::editSeries {0 100} {-50 50} 10]

 puts "Series:"
 foreach {x y} $xyseries {
     puts [format "%10.4f %10.4f" $x $y]
 }

aaaaaaaaa - 2009-06-17 12:33:32

I am getting this error. Does the Plotchart have undocumented dependencies?

invalid command name "console"

    while executing

"console show"

    (procedure "DrawXaxis" line 25)
    invoked from within

"DrawXaxis $w $xmin $xmax $xdelt"

    (procedure "::Plotchart::createXYPlot" line 31)
    invoked from within

"::Plotchart::createXYPlot .graph.c ::Plotchart::determineScale $xmin $xmax ::Plotchart::determineScale $ymin $ymax"

    (procedure "::Editgraph::editSeries" line 42)
    invoked from within

"::Editgraph::editSeries {0 100} {-50 50} 10"

    invoked from within

"set xyseries ::Editgraph::editSeries {0 100} {-50 50} 10"

    ("uplevel" body line 1)
    invoked from within

"uplevel #0 {set xyseries ::Editgraph::editSeries {0 100} {-50 50} 10}"

MG The console show command is only available in wish on Windows, where the process isn't attached to a system console, and shows a pure-Tcl console with stdin/stdout redirected to it. Plotchart likely should't be calling it (or should at least catch it). You can most likely just define

  proc console {args} {return}

or something like that so that the console show command does nothing, which should make your code run OK.


aaaaaaaaa - 2009-06-17 13:39:40

That makes sense. I was using tkcon.

I see the catch statement in the sample code above. However, it seems that "console" is being used internally by Plotchart too. Hope it will get fixed. Would a "package req Tk" eliminate the need for console all together?

MG I've never used Plotchart, so I'm not entirely sure what it does. But a quick glance at the docs suggests it's a Tk/graphical package, so I honestly don't know why it would need to use the console command at all. I can only assume it's either a mistake (left from debugging on Windows or something), or that it prints something to stdout at some point, and the console is intended to make sure that's visible on non-linux systems, and should be in a catch. But if you don't want to go sourcediving into the code for Plotchart, a no-op console command is likely the easiest way to avoid the error. (Would recommend reporting the error, too - I think tklib lives in tcllib's sf.net site?)

AM (18 june 2009) Hm, yes, that was a left-over - it should have gone from the source code in CVS, but not yet in the released version.