An application with perl/Tk: tkgnuplot

By: Slaven Rezic

After Steve O. Lidie's introduction to perl/Tk in the previous issue of PerlMonth, what about a simple and small application, maybe a GUI wrapper for gnuplot? So here we go. But first a note for Windows users: this will only work for Unix, since this application uses pipes, which are not implemented in the Windows version of perl. Anyway, gnuplot for Windows comes already with a GUI, so this application wouldn't be of use anyway.

OK, let's begin. The first version of tkgnuplot is very simple. It only offers an entry field to input the function to plot and two buttons for starting the plot and quitting the program. So here it is:

    #!/usr/bin/perl
    
    use Tk;
    use Tk::LabEntry;
    use FileHandle;
    use strict;
    
    my $gnuplot_path = "/usr/local/bin/gnuplot";
Note that you have to change the $gnuplot_path variable to reflect the path to the gnuplot executable.

    my $top = new MainWindow;
    
    my $function = "sin(x)";
    
    my $funcentry = $top->LabEntry
      (-label        => 'Function:',
       -textvariable => \$function,
       -labelPack    => [-side => 'left'])->pack;
The LabEntry widget is a composite widget holding an entry and an associated label. This widget is meant as a convenience, otherwise, one would create a frame and put the label and entry widget manually. With LabEntry you have only to specify the label text with the -label option and the position of the label relative to the entry with the -labelPack option. The argument of this option is a reference to an array with the same options one would supply to pack. Try to find out by replacing 'left' with 'right', 'top' or 'bottom'.

    my $butframe   = $top->Frame->pack(-anchor => 'w');
    my $plotbutton = $butframe->Button(-text => 'Plot',
    				   -command => \&plot,
    				  )->pack(-side => 'left');
    $butframe->Button(-text => 'Quit',
    		  -command => \&quit,
    		 )->pack(-side => 'left');
    $top->protocol('WM_DELETE_WINDOW', \&quit);
The next maybe unfamiliar line is the one with the $top->protocol method. With this method it is possible to supply a callback, which gets called if the user closes the window. It is not crucial to have this method called, as Tk always defines a standard callback for closing windows, but in this case, it is necessary to quit gnuplot gracefully. More information for the $top->protocol method is in the Tk::Wm manpage.

    my $gnuplot = new FileHandle("| $gnuplot_path");
    $gnuplot->autoflush(1);
Next, tkgnuplot starts gnuplot and records the associated file handle in the variable $gnuplot. The file handle is set to autoflush, so we assure that all commands we send to gnuplot will executed immediately.

    $top->bind("<Return>", sub { $plotbutton->invoke });
    MainLoop;
As a convenience, the RETURN key will start the plot as if the user has clicked on the Plot button. This is done in the $top->bind line.

    sub plot {
        $gnuplot->print("plot $function\n") if $function ne '';
    }
The plotting is done in the plot subroutine. This subroutine just sends "plot $function" to the gnuplot process, which will create a X11 window and output the plot.

    sub quit {
        $gnuplot->close;
        $top->destroy;
    }
Finally, the quit subroutine closes the file handle to the gnuplot process and destroys the main window.

Easy, isn't it?

Here's what Version One Looks like Snap Shot

The second version implements some additional features like

    #!/usr/bin/perl
    
    use Tk;
    use Tk::LabEntry;
    use FileHandle;
    use strict;
    
    my $gnuplot_path = "/usr/local/bin/gnuplot";
    my $tempfile     = "/tmp/gnuplot-%d.ps";
    my $psprintprg   = "lpr -Pps %s";
Here are two new configuration variables. $tempfile is the path to the generated postscript file and should live in a temporary directory. $psprintprg is a command to print the plot to a postscript printer. The %s is a placeholder for the printed file. It is possible to use a postscript viewer like ghostscript or gv instead of lpr.
    
    my(@function) = ('sin(x)');
    my($x_from, $x_to, $y_from, $y_to);

    my $top = new MainWindow;
    
    my $gnuplot;
    
    my($command, @cmd_history, $cmd_index);
    my $funcframebox = $top->Frame->pack(-anchor => 'w', -fill => 'both',
    				     -expand => 1);
    my @funcframe;
    my $funcframeno = -1;
    my $funcentry = funcadd();
    $funcframe[0]->Button(-text => '+',
    		      -command => \&funcadd
    		     )->pack(-side => 'left');
    $funcframe[0]->Button(-text => '-',
    		      -command => \&funcdel
    		     )->pack(-side => 'left');
There are two new buttons labeled with plus and minus. These buttons call the subroutines funcadd and funcdel, which create or delete a new entry for the input of a function definition.
    
    my $xframe = $top->Frame->pack(-anchor => 'w', -fill => 'x', -expand => 1);
    $xframe->LabEntry(-label => 'X from',
    		  -textvariable => \$x_from,
    		  -width => 6,
    		  -labelPack => [-side => 'left'])->pack(-side => 'left');
    $xframe->LabEntry(-label => 'to',
    		  -textvariable => \$x_to,
    		  -width => 6,
    		  -labelPack => [-side => 'left'])->pack(-side => 'left');
    
    my $yframe = $top->Frame->pack(-anchor => 'w', -fill => 'x', -expand => 1);
    $yframe->LabEntry(-label => 'Y from',
    		  -textvariable => \$y_from,
    		  -width => 6,
    		  -labelPack => [-side => 'left'])->pack(-side => 'left');
    $yframe->LabEntry(-label => 'to',
    		  -textvariable => \$y_to,
    		  -width => 6,
    		  -labelPack => [-side => 'left'])->pack(-side => 'left');
Here is another set of LabEntries for the x- and y-range of the plot.
    
    my $directframe = $top->Frame->pack(-anchor => 'w');
    $directframe->Label(-text => "Command:")->pack(-side => 'left');
    my $directentry = $directframe->Entry(-textvariable => \$command,
    				      -width => 30);
    $directentry->pack(-side => 'left');
    $directentry->bind('<Return>',
    		   sub {
    		       push(@cmd_history, $command);
    		       $gnuplot->print("$command\n");
    		       undef $command;
    		       $cmd_index = $#cmd_history+1;
    		   });
    $directentry->bind('<Up>',
    		   sub {
    		       if ($cmd_index > 0) {
    			   $cmd_index--;
    			   $command = $cmd_history[$cmd_index];
    		       } else {
    			   $top->bell;
    		       }
    		   });
    $directentry->bind('<Down>',
    		   sub {
    		       if ($cmd_index < $#cmd_history) {
    			   $cmd_index++;
    			   $command = $cmd_history[$cmd_index];
    		       } elsif ($cmd_index == $#cmd_history + 1) {
    			   $top->bell;
    		       } else {
    			   undef $command;
    			   $cmd_index = $#cmd_history+1;
    		       }
    		   });
$directentry is the entry widget for typing raw gnuplot commands. So you can type "plot sin(x)" directly in this entry and press RETURN to execute the command. The RETURN key also puts the command in the command history. You can browse with the UP and DOWN cursor keys through the history. At CPAN, there is also a history entry widget called Tk::HistEntry.
    
    my $butframe   = $top->Frame->pack(-anchor => 'w');
    my $plotbutton = $butframe->Button(-text => 'Plot',
    				   -command => \&plot,
    				  )->pack(-side => 'left');
    $butframe->Button(-text => 'Print',
    		  -command => \&psprint,
    		 )->pack(-side => 'left');
    $butframe->Button(-text => 'Quit',
    		  -command => \&quit,
    		 )->pack(-side => 'left');
    $top->protocol('WM_DELETE_WINDOW', \&quit);
    
    $gnuplot = new FileHandle ("| $gnuplot_path");
    $gnuplot->autoflush(1);
    
    $top->bind("<Return>",
    	   sub { $plotbutton->invoke if $top->focusCurrent ne $directentry });
Note the small change to the RETURN key binding. As the command entry also uses the RETURN key, we have to protect the binding of the global RETURN key from executing while the focus is in the command entry widget. $top->focusCurrent returns the reference of the widget which posseses the focus, so we can easily determine which of the two callbacks to call.
    MainLoop;
    
    sub autorange {
        my $margin = shift;
        $margin eq '' ? '*' : $margin;
    }

    sub plot {
        $gnuplot->print('set xrange [' .
    		        autorange($x_from) . ":" . autorange($x_to) . 
    		        "]\nset yrange [" .
       		        autorange($y_from) . ':' . autorange($y_to) .
        		"]\n");
        my @functions;
        foreach (@function) {
    	    push(@functions, $_) if $_ ne '';
        }
        $gnuplot->print("plot " . join(", ", @functions), "\n")
          if @functions;
    }
The plot subroutine now sets the x- and y-ranges according to the values in the range LabEntries. If a range entry is empty, then the autorange subroutine will return *, which means auto-determine range. All non-empty functions are joined into one plot string for gnuplot to execute.
    
    sub psprint {
        my $tempfile = sprintf $tempfile, $$;
        $top->Busy;
        plot();
        $gnuplot->print(<<EOF);
    set term postscript
    set output "$tempfile"
    replot
    set output
    set term x11
    EOF
        system(sprintf($psprintprg, $tempfile));
        $top->Unbusy;
    }
Here is the print function. All this subroutine does is:
    sub funcadd {
        $funcframeno++;
        $funcframe[$funcframeno] = $funcframebox->Frame->pack(-anchor => 'w', 
    							      -fill => 'x',
    							      -expand => 1);
        my $funcentry 
          = $funcframe[$funcframeno]->LabEntry
    	(-label => 'Function:',
    	 -textvariable => \$function[$funcframeno],
    	 -labelPack => [-side => 'left']);
        $funcentry->pack(-side => 'left');
        $funcentry->focus;
        $funcentry;
    }
    
    sub funcdel {
        if ($funcframeno > 0) {
    	    $funcframe[$funcframeno]->destroy;
    	    pop @function;
    	    $funcframeno--;
        }
    }
Adding new widgets to a running application is easy - simply pack a new widget and you're done. There is a container frame $funcframebox to get the widget in the right place.

The destroy method is called to get rid of the new widgets.

    sub quit {
        $gnuplot->close;
        $top->destroy;
    }
Here's what Version Two Looks like Snap Shot

There are many ways to connect perl with gnuplot. Another is the use of the lowlevel graphics driver Term::Gnuplot. The Chart::GnuPlot module is a complete wrapper to the gnuplot commands. There is also perl/Tk driver for gnuplot to create plots which can be included directly in perl/Tk. It's also possible to use the xlib terminal driver of gnuplot and translate the issued commands to create lines in the Canvas widget.