Project: Plotting a function

Standard Scheme does not provide any facilities for graphics programming, but for particular implementations of Scheme it is often possible to obtain libraries that create windows, draw lines and curves onto them, add text in various fonts and colors, provide text-entry forms and labelled buttons and sliders and other ``widgets,'' and generally integrate Scheme programs with the multimedia resources of the machines on which they run.

As a small example of graphics programming in Scheme, we'll use Elk, an implementation of Scheme created by Oliver Laumann, to develop a plotting program. The program will open up a new window on your workstation's screen and draw the graph of a function into it.

The file /u2/stone/courses/scheme/html/plot-project.scm contains a working, but primitive, version of this plotting program. Your project will be to refine it, adding features of various sorts to the graphical display.

  1. Begin by opening a dtterm window and using the shell's cp command to make a copy of this program in your working directory.

To run the program, type the command elk -i -l plot-project.scm at the prompt in the dtterm window (naming the file in which you stored your copy of the program). A square window appears, presenting a white background against on which the plot will subsequently be drawn. Pressing any key on the keyboard has some effect on the window: The Q key closes the window and exits from the program, and any other key causes the plot to be displayed. (Initially, the plot shows the function x3 - 8x - 4 in the range from -4 to +5.)

  1. Run the program as described and exit from it as described (with the Q command).

  2. Start XEmacs and look at the last few lines of the source code for the program. Note that the function that is being graphed, f(x) = x3 - 8x - 4, appears in the Scheme code as a lambda-expression that names a procedure that computes that function. Replace this lambda-expression with your favorite function from real numbers to real numbers and re-run the program. If you don't have a favorite, try one of the following:

    Initially, too, the graph of the function shows only the values for arguments in the range from -4 to +5. This too is determined by arguments to the plotter procedure; if you like, you may experiment with changing those arguments.

On MathLAN workstations, windows are normally created and displayed with the assistance of a ``window manager'' program, dtwm, by invoking procedures in a library known as ``X Windows.'' Although this library is intended primarily for programmers using the C and C++ languages, Laumann provided us with an interface that allows us to invoke X Windows procedures from inside Elk, using Scheme syntax, and thus to integrate X Windows programming with Scheme programming. A full list of the procedures that make up Elk's X Windows library can be found in the Elk/Xlib reference manual. Here, however, we shall cover only the twenty or so that are used in the plotting program.

One of X Windows data structures is the display, which contains information about the output device on which any window we create will appear. Open-display is a procedure of zero arguments that allocates and returns an appropriately initialized display structure describing the workstation's monitor; it returns #f if the monitor is not suitable for displaying windows (which might happen if, for instance, one tried to run an X Windows program from a VAX terminal). Display? is a type predicate that tests whether its argument is a display structure.

One of the components of a display is its color map, a vector containing all of the various colors that appear simultaneously in the display. Our workstations are capable of displaying 16777216 different colors, but no more than 256 of them can be on screen at the same time, and the elements of the color map are the ones that have been selected by the various programs that are running as ones that they wish to use. The display-colormap procedure is a selector that extracts the color map from a given display.

In order to add a new color to the color map, making it eligible for use on screen, one invokes a procedure of two arguments named alloc-color. The first argument to alloc-color is the color map to which the new color is to be added; the second is a structure indicating the intensities of the red, green, and blue components of the new color, as real numbers in the range from 0.0 to 1.0. The make-color procedure is the constructor for color structures; it takes three arguments -- the red, green, and blue intensities -- and returns the color characterized by those intensities. Here's a typical use of these procedures:

(alloc-color current-color-map (make-color 0.0 1.0 0.0))

This expression builds a bright green color and adds it to the current color map. Alloc-color returns the index of the position within the color map at which the color was installed. Often this index is given a mnemonic name that suggests the color:

(define sky-blue
  (alloc-color current-color-map (make-color 0.4 0.6 1.0)))

Another element of the display is its root window, which is the window that typically occupies the entire display and serves as a backdrop against which other windows are placed. The display-root-window procedure selects this component from the display structure.

The internal representation of one specific window, such as the one in which the plotting program draws its graphics, is also a structure. The create-window procedure allocates and returns such a structure; it takes a variable number of parameters -- always, however, an even number -- which are alternately symbols naming the components that need to be initialized and values to store at those positions in the structure. Some typical components of a window structure are parent (another window within which the new one is to be placed), width (the number of pixels from the left edge of the window to the right edge), height (the number of pixels from the top edge to the bottom), and background-pixel (the color map's index for the color to be used as the background within the new window).

In order to perform any drawing operations within a window, we'll also need a structure called a graphics context, which keeps track of various quantities that control the drawing style -- the foreground color in which new graphic features are drawn, the width of lines, the font in which text is to be displayed, and so on. The root window of a display already possesses such a graphics context, and one common way to get started in a new window is to make a fresh copy of the root window's graphics context and associate it with the new window. (This makes it possible to change some of the elements of the graphics context without affecting the appearance of any other window.) A procedure named copy-gcontext constructs and returns a copy of a given graphics context structure.

Elk provides mutator procedures for several elements of a graphics context, and the plotting program uses two of these:

The plotting program invokes both of these procedures before each drawing operation, to determine the line width and color used for that operation.

  1. Find the place in the program at which colors are added to the color map with alloc-color. Add an additional color of your choice, giving it a name as shown above. Then find the place in the program at which the color of the function's graph (red) is actually determined by a call to set-gcontext-foreground!; put in the name of your color instead. Save the program from XEmacs and re-run it in Elk Scheme; confirm that the program now uses your color for the graph.

From time to time, the plotting program will pause to process and respond to input from the user, in the form of keystrokes. The pause is produced by a call to a procedure called handle-events, which examines all the ``input events'' that have occurred since the last such pause and invokes an appropriate ``event handler'' procedure for each one. The event handler is expected to work out exactly what happened and where (which key was pressed, for instance) and to act on it. Initially, the responses of the plotting program are rather elementary: If the user types the letter Q, the plotter closes the window and exits, and if the user presses any other key, the plotter draws (or redraws) the plot of the current function. The event handler must be in place before X Windows will actually agree to draw the window onto the screen.

Finally, the dtwm window manager keeps structures containing information about the windows it helps to put up, and there are procedures for mutating various components of those structures, with such names as set-wm-hints!, set-wm-normal-hints!, set-wm-class!, set-window-event-mask!, set-wm-name!, and set-wm-icon-name!. Perhaps the most interesting of these is set-window-event-mask!, which determines which of the various kinds of ``input events'' -- moving the mouse, pressing and releasing mouse buttons, pressing and releasing keyboard keys, and so on -- the window will pay attention to.

Once the structure for a window has been created, and its event handler has been made ready, and the window manager has been made aware of the existence and nature of the new window, one can ask for the window to be drawn onto the display by invoking the map-window procedure, which takes the window structure as its only argument.

X Windows includes several procedures for drawing shapes into a window. Here are some of the ones that you're most likely to use, with sample invocations:

  1. One page of the Elk/Xlib manual contains a list of the Xlib graphics functions; look it over to see whether there might be other useful library procedures. (Unfortunately, the abundant cross-references of the form ``See XDrawLine'' are intended for people who already know the X Window System well -- they are references to procedures in the original X Window System library -- so it's sometimes a little difficult to determine what the various procedures do.)

The credit line (``Drawn by PLOTTER'') appearing in the lower left-hand corner of the window is drawn onto the window by the text-drawing procedure draw-image-text. This procedure takes six arguments -- the window, the graphics context, the x- and y-coordinates of the upper-left-hand corner of the rectangle within which the text is to be written, the text itself (as a vector of character codes), and finally a symbol that indicates the amount of storage occupied by each character in the character set (for instance, the symbol 1-byte would be used for ASCII characters, while for Unicode characters you'd use the symbol 2-byte).

Elk Scheme also provides a procedure named translate-text, which takes a string as argument and returns a vector containing the character codes corresponding to the characters in that string -- basically, it applies char->integer to each character in the string and accumulates the results in a vector. So a typical call to draw-image-text would look like this:

(draw-image-text my-window current-gcontext 48 64
                 (translate-text "Hi, Mom!") '1-byte)

This causes the string "Hi, Mom!" (without the enclosing quotation marks) to be drawn into my-window, with the upper-left-hand pixel of the H placed forty-eight pixels from the left edge of the window and sixty-four pixels down from the top edge.

  1. Have plotter add another line of text in the upper left-hand corner, giving the date on which the plot was made. Initially, you may want to type the date string in by hand. However, Elk also provides facilities for building a date-stamp automatically; if you add the Unix ``feature'' by placing (require 'unix) at the beginning of an Elk Scheme program, the following procedure returns the current date and time as a string whenever it is invoked:

    (define datestamp
      (lambda ()
        (let* ((almost (unix-time->string (unix-decode-localtime (unix-time))))
               (len (string-length almost)))
          (substring almost 0 (- len 1)))))
    

When the program has finished its work, an appropriate sequence of X Windows procedures must be invoked to free the various resources: The window allocated by create-window must be deallocated with a call to the destroy-window procedure, the color map returned by display-colormap must be released with a call to free-colormap, the graphics context constructed by copy-gcontext must be freed by calling free-gcontext, and finally the display obtained from open-display must be released with a call to close-display.

  1. Read through the source code for the plotting program. (After this long introduction, I'm hoping that you'll be able to figure out most of the details.)

  2. It would be helpful to have some labelled tick marks along the axes, marking the coordinates at those points. Add them. The tick marks can be drawn in with draw-line, once you figure out exactly where they should go; the labels can be drawn in with draw-image-text, once you figure out exactly what they should say! (This is a difficult exercise.)


This document is available on the World Wide Web as

http://www.math.grin.edu/~stone/courses/scheme/plot-project.html

created November 19, 1997
last revised December 9, 1997

John David Stone (stone@math.grin.edu)