Using The Plot3D Extension Widget

The Plot3D widget is a special extension for Tcl/Tk that drawings 3-dimensional graphs. The Plot3D widget is unusual in that it does not do any rendering or drawing itself. Instead of doing its own drawing, the Plot3D widget generates Tcl commands that cause the 3-dimensional image to be drawn on an ordinary canvas widget. This helps keep the Plot3D widget code small and compact and also allows the generated graph to take advantage of existing canvas widget features such as postscript printing.

This article is a tutorial guide to using the Plot3D widget.

Table Of Contents

List Of Figures

1 Introduction

The Plot3D widget draws one or more lines of data in a 3-dimensional space and then renders the results on a canvas widget. Figure 1.1 displayed is a typical Plot3D graph with addition annotation to show its coordinate axes and axis planes.

As shown in figure 1.1, the Plot3D widget uses a right-handed coordinate system. The Y axis is always pointing up. Each line of data is drawn in its own plane which is parallel to the XY-axis plane.


**Image**

Figure 1.1: A Typical Plot3D Widget With Extra Annotation Showing The Coordinate Axes and Axis Planes


Lines of data are numbered beginning with 0. Line 0 has the largest Z coordinate and higher numbered lines are drawn closer to the XY-Plane. This is illustrated in figures 1.2 and 1.3 which show the same graph as figure 1.1 but with labels on both graph lines.


**Image**

Figure 1.2: The Previous Figure With Additional Annotation To Show The Graph Line Numbers



**Image**

Figure 1.3: The Previous Graph Viewed From A Different Angle


A simple Plot3D graph can be created using the following 4 lines of Tcl code:

   plot3d gr -canvas .c
   gr data 0   100 120 140 140 130 150 140 140 \
               120 100  90 130 150 180 170 130
   gr data 1    50  40  10 100 120 140 200 130 \
               150  90 140 140 120 100  90 100
   pack [canvas .c]
The first command creates a new instance of the Plot3D widget named ``gr''. Like other Tcl/Tk widgets, the name of the new Plot3D widget becomes a new Tcl/Tk command that is used to control and interrogate the widget. A single option to the first command declares that the Plot3D widget should draw its graph in a canvas widget named ``.c''.

The second and third lines of the above example create two lines of data in the Plot3D widget. Each of these commands consists of the name of the widget, the ``data'' subcommand word, the line number (0 and 1 in the example) and one or more data points. The example shows two lines of data each with 16 points (exactly the same data used to generate figure 1.1, by the way) but these are not hard limits. You can create up to 50 lines of data with up to 50 data points each. You can create even more if you change the MAX_PLANES and MAX_POINTS macros in the Plot3D source code and recompile.

It is not necessary to give each data line the same number of data points. The Plot3D widget will automatically scale its X-axis to accomodate the longest data line. Also, there is no limit to the Y values for any data point other than the fact that they must be non-negative. Each Y value can be any non-negative floating point number. The Y axis of the graph will automatically scale to accomodate the largest Y value.

2 Common Configuration Options

If you put the four Tcl/Tk commands shown in the example above into a file, then run a Plot3D enabled ``wish'' on that file, you'll get a graph that looks like figure 2.1. As you can see, this graph is correct, but visually uninspiring. This section will describe options to the Plot3D widget that will make the graph more eye-catching and easier to read.


**Image**

Figure 2.1: An Unadorned Graph


2.1 Changing The Color Of Graph Lines

The first thing we can do is give some color and width to the two graph lines. This is accomplished by using a variation on the config widget command where the first argument is the number of the graph line we want to configure. Like this:

   gr config 0 -options {-fill blue -width 4}
   gr config 1 -options {-fill yellow -width 4}
The value of the ``-options'' option is a string that is appended to the canvas commands used to generate the graph lines. In the first command above, we are instructing the Plot3D widget to append the text ``-fill blue -width 4'' to the canvas widget command that actually draws the 0 graph line. This causes the line to be colored blue and to be 4 pixels wide. The second command above causes graph line 1 to be colored yellow. The effect is a more easily understood graph, as is seen in figure 2.2


**Image**

Figure 2.2: The Graph After Setting Line Colors And Widths


The ``-options'' option can be applied only to a specific line. If you want to specify additional arguments to be appended to all graph lines, you should use the ``-lineoptions'' option. For example, the three commands that follow have exactly the same effect as the two command above:

   gr config 0 -options {-fill blue}
   gr config 1 -options {-fill yellow}
   gr config -lineoptions {-width 4}
Notice that because the ``-lineoptions'' option applies to the graph as a whole and not to a specific graph line, the graph line number after the ``config'' keyword is omitted.

In general, you should use the ``-options'' option for graph line attributes that apply only to a single graph line (such as color), and use ``-lineoptions'' for all attributes that apply to all lines (such as width). A common argument to ``-lineoptions'' is the following:

   gr config -lineoptions {-width 4 -capstyle round -smooth 1}
The ``-smooth'' argument is particularly noteworthy. Figure 2.3 shows the result of using the ``-smooth'' option on our sample graph.


**Image**

Figure 2.3: The Example Graph With Smooth Lines


2.2 Changing Attributes Of The Axis Planes

The Plot3D widget also allows extra attributes to be specified for the axis planes. Just as the ``-lineoptions'' option will cause extra text to be appended to the canvas widget commands that create graph lines, the ``-xyplane'' option will cause extra text to be appended to the canvas widget command that draws the XY Axis Plane. By default, the value of the ``-xyplane'' option is ``-fill honeydew3 -outline black'' which works well for most people. But if you want to change it you can.

There are similar options ``-xzplane'' and ``-yzplane'' for controlling the appearance of the XZ and YZ axis planes, respectively. Both have reasonable defaults, but you are free to change them if you want something different.

2.3 Scaling The Graph

The Plot3D widget supports two configurations that control the amount of space the graph takes up on the canvas: ``-bbox'' and ``-magnification''. There is a third option ``-size'' that controls the relative sizes of the three coordinate axes of the graph.

The argument to the ``-bbox'' option is a list of four integers that specify the X and Y canvas coordinates for the upper left-hand corner of the graph, and the width and height of the graph in pixels. The default setup is equivalent to the following:

   gr config -bbox {0 0 300 200}
In other words, the graph is drawn on the canvas so that the upper left-hand corner of the graph is at canvas coorindates 0,0 and the width and height of the graph is 300 by 200. The graph will always be drawn so that it is centered in this region, and if no ``-magnification'' option is specified, the size of the graph will be scaled to fit inside this region.

The size of the graph as it is drawn on the canvas can also be adjusted using the ``-magnification'' option. The argument to ``-magnification'' is a single integer that is the amount by which the graph should be scaled before it is displayed. Useful values for the magnification normally are in the range of 500 to 2000, though you will doubtless want experiment to find a value suitable for each particular use. If no ``-magnification'' option is used, or if the value is set to the empty string, then graph will scale to fit the bounding box specified by the ``-bbox'' option.

The ``-size'' option does not change how big the graph is drawn, as its name might imply, but rather the relative sizes of the X, Y and Z axes. The argument to ``-size'' is a list of 3 real numbers that specify the relative size of the X, Y and Z axis, respectively. The default value is the equivalent of:

   gr config -size {2 1 1}
Hence, the X axis is twice as long as the Y and Z axes, which are both the same size. Sometimes a graph looks better when these ratios are changed, however. Consider, for example, figure 2.4 which shows the sample graph with the size set to ``{2 1 0.5}''.


**Image**

Figure 2.4: Changing The Relative Sizes Of The Coordinate Axes


2.4 Labeling The Axes

The tic-marks on the X and Y axes of the graph will be automatically labeled if you just specify a font to use for the labelling with the ``-font'' option. Like this:

   gr config -font fixed
All labels will be drawn in the same font. Note that the Plot3D makes no attempt to scale or rotate this font to suit the graph -- whatever font you specify is exactly the font that appears on the screen. Also note that the automatic magnification calculations that occur when ``-magnification'' is empty do not take into account the size of the label text. So if you don't leave extra space around the outside of the graph, the labels might fall off the edge of the window.

Labels for the tic-marks along the Z axis are specified individually for each data line using a line specific configuration. Like this:

   gr config 0 -label {Line 0}
   gr config 1 -label {Line 1}
If no label is specified for a particular data line, no label is drawn for the corresponding tic-mark on the Z axis. No data line labels are drawn anywhere unless there is a font specified by the ``-font'' option.

The combined effect of specifying a font and putting labels on the data lines is a graph that looks something like figure 2.5.


**Image**

Figure 2.5: Labels On The Graph


3 Making The Graph Rotate

One of the more interesting features of the Plot3D widget is its ability to view a graph from many different angles. The ability to change the viewing angle dynamically heightens the 3-D effect and make the graph easier to understand. It also increases the visual appeal of an application using the Plot3D widget.

3.1 Manually Positioning The Observer

The viewing angle of the graph is controlled by the position of an imaginary observer looking directly at the center of the graph. The position of the observer can be set or queried at any time using the ``-observer'' configuration option. Like this:

   gr config -observer {0.8 0.23 10}
The position of the observer is expressed in polar coordinates relative to the center of the graph. The first number in the observer's coordinates is the azimuth which is the amount of rotation around the Y axis, expressed in radians. The second number is the elevation, which is the angle between a line connecting the observer to the graph center and the XZ axis plane. The elevation is also expressed in radians. The third and final coordinate number is the distance from the observer to the center of the graph.

3.2 Connecting The Graph To Scrollbars

A Plot3D widget is connected to scrollbars just like any other scrollable Tk widget such as a canvas, or text widget or listbox. The ``-xscrollcommand'' and ``-yscrollcommand'' configuration options on the Plot3D widget are configured to invoke the ``set'' widget command of the appropriate scrollbars, and the ``-command'' configuration options of the scrollbars are set to invoke the ``xview'' or ``yview'' commands of the Plot3D widget. The following code illustrates:

   canvas .c
   scrollbar .xs -orient horizontal -command {gr xview}
   scrollbar .ys -orient vertical -command {gr yview}
   plot3d gr -canvas .c -xscrollcommand {.xs set} -yscrollcommand {.ys set} \
   grid .c -column 0 -row 0
   grid .ys -column 1 -row 0 -sticky ns
   grid .xs -column 0 -row 1 -sticky ew
The only difference between the scrollbars connected to a Plot3D widget and the scrollbars connected with any other Tk widget is that in the Plot3D widget, the scrollbars are associated with rotating the graph, not moving up, down, left or right. The X scrollbar, instead of moving the whole image right and left, changes the azimuth of the observer and makes the graph appear to rotate around the Y axis. The Y scrollbar, instead of moving the images up and down, changes the elevation of the observer and makes the graph appear to rotate around the X axis.

3.3 Rotating The Graph By Dragging

The position of the observer can be varied by clicking and dragging on the surface of the canvas in addition to moving the scrollbars. The following bindings induce this behavior:

   bind .c <ButtonPress-1> {gr drag from %x %y}
   bind .c <B1-Motion> {gr drag to %x %y}
When button 1 on the mouse is pressed, the ``drag from'' command of then Plot3D widget is called to record the position of the original mouse press. For every subsequent mouse movement (while button 1 is held down) the ``drag to'' command of the Plot3D widget with the new mouse coordinates. The ``drag to'' command compares the current mouse coordinates with the prior mouse coordinates and adjusts the position of the observer accordingly.

It works to connect a Plot3D widget to scrollbars and to allow drag rotations at the same time. If you do both, then the scrollbars will automatically move when you rotate the graph by dragging.

3.4 Making The Graph Rotate By Itself

To heighten the 3-D effect, you can cause the graph in a Plot3D widget to continuously change the position of the observer relative to the center of the graph. This gives the graph the appearance of constantly rotating in three dimensions. We call the effect ``motoring''.

There are two configuration options that control the automatic rotation motor. The ``-motorframedelay'' option specifies the amount of time between frames in the rotation animation, in milliseconds. Thus if you set ``-motorframedelay'' to 100, the graph will redraw itself with at different viewing angle about 10 times per second. You can turn the motor off by setting ``-motorframedelay'' to 0.

To obtain a pleasing animation effect for our example graph above, we can add the following line of code:

  gr config -motorframedelay 150
The following code fragment shows how to construct a push button that will toggle the motor on and off:
  button .b -command MotorOnOff
  pack .b
  proc MotorOnOff {} {
    if {[gr cget -motorframedelay]==0} {
       .b config -text {Stop the Motor}
       gr config -motorframedelay 150
    } else {
       .b config -text {Start the Motor}
       gr config -motorframedelay 0
    }
  }

The other configuration option associated with the motor is called ``-motorcontrol''. This option allows you to specify the minimum and maximum azimuth and elevation that the motor will traverse and the rate at which the motor moves in each direction. You can change any or all of these values to give different rotation angles and speed. But the default values usually give a satisfactory appearance, and so these parameters rarely need to be changed.

The argument to the ``-motorcontrol'' option is a list of 6 floating point numbers in this order: minimum azimuth, maximum azimuth, rate of change in azimuth, minimum elevation, maximum elevation and rate of change of elevation. The defaults values are -0.15, 1.5, 0.1, 0.0, 0.8 and 0.071, respectively.

3.5 Speed And Efficiency Considerations

Every time the position of the observer to a Plot3D graph widget changes, the entire graph must be completely redrawn. This can involve issuing hundreds of canvas widget commands, and for complex graphs or slow computers, can take a substantial fraction of a second. But in order to achieve a responsive display, the graph needs to be updated in less than a tenth of a second.

In order to help the graph redisplay itself quickly enough, the amount of detail shown by the graph can be reduced while it is being rotated. Reducing the amount of detail displayed reduces the number of canvas widget commands that must be issued, and thus makes the graph seem more responsive.

The amount of detail shown by the Plot3D widget is controlled by the ``-detail'' configuration option. The normal setting for this option is ``full'' which means that all graph details are displayed. The other two possible values for the -detail option are ``partial'' and ``outline''. When -detail is set ot ``partial'', the graph lines and background planes are shown as normal, but the tic marks and text markings around the edge of the graph are omitted. In ``outline'' mode, most of the graph drawing is omitted, and only a box outline of the graph is displayed. Figure 3.1 shows the same graph at each of the three levels of detail selectable using the -detail option.


**Image**

Figure 3.1: The Same Graph At Three Levels Of Detail: ``full'', ``partial'' and ``outline''


The code fragment below shows a typical application of the -detail configuration option in reducing the amount of redisplaying necessary while a graph is being rotated by dragging.

   bind .c <ButtonPress-1> {gr drag from %x %y}
   bind .c <B1-Motion> {gr config -detail partial; gr drag to %x %y}
   bind .c <ButtonRelease-1> {gr config -detail full}

With these bindings, the amount of detail drawn on the graph is reduced as soon as the you begin to drag the graph, and remains reduced as long as the mouse button is held down. But when the mouse button is released, indicating that the drag has completed, the -detail option is restored to ``full'' so that all graph details are once again displayed.

In this example, the level of detail is reduced to ``partial'' which is adequate for a responsive display on most systems. On a slow computer, or with an unusually complex graph, you may get better results by reducing the level of detail to ``outline''.

Bindings similar to the above can reduce the level of detail when the graph is rotated using scrollbars. We have:

  bind .xs <B1-Motion> {gr config -detail partial}
  bind .xs <ButtonRelease-1> {gr config -detail full}
  bind .ys <B1-Motion> {gr config -detail partial}
  bind .ys <ButtonRelease-1> {gr config -detail full}
Notice that in every case, we don't change the -detail for the graph until after the graph starts to rotate. We could have put the ``-detail partial'' bindings on the <ButtonPress-1> event in order to get the graph to go into partial display mode as soon as the mouse button was clicked over the graph or scrollbar, but we find that the effect is more appealing if the graph waits until a drag actually begins before reducing the display detail.

4 Viewing Slices Of The Graph

Sometimes it is helpful to be able to see a cross-section or slice of a graph in order to better compare the Y values of the various lines in the graph at a fixed X. An example of this is shown in figure 4.1. This figure shows three images of the same graph sliced at different points along the X axis. To the right of each graph is a two-dimensional image of the slice.


**Image**

Figure 4.1: Three Slices Of The Same Graph


The rectangular red box that defines the plane of the slices in figure 4.1 is called the ``slice cursor''. You can position the slice cursor at any point along the X axis using the cursor subcommand to the Plot3D widget. Like this:

  gr cursor 4.17934
You can also interrogate the current position of the slice cursor by using the cursor subcommand without an argument.
  set x [gr cursor]
Setting the position of the cursor to any value less than 0 or greater than the number of points on the longest line causes the cursor to disappear. The cursor is not visible by default, so if you want to use it, you must initialize it, probably to a value like 0.0.

The default appearance of the slice cursor is a black line 1 pixel wide. Usually you will want something different that the default. Fortunately, it is easy to change the appearance of the slice cursor using the ``-cursoroptions'' configuration option. The -cursoroptions option works much like the ``-lineoptions'' option. Its argument is one or more options that are passed to the canvas widget commands that actually draw the cursor. In figure 4.1, the cursor is configured as follows:

  gr config -cursoroptions {-fill red -width 2}

Once a cursor is visible, the user can change the position of the cursor by dragging it with the mouse. No special action is required to enable this feature -- it occurs automatically as part of the drag from and drag to subcommands. If the user presses the mouse button directly on top of the slice cursor and moves the mouse, the slice cursor will move but the graph will stay stationary. If the user presses the mouse button anywhere on the graph other than directly over the slice cursor, then moves the mouse, the slice cursor will stay in the same spot and the graph will rotate. This sounds complex when expressed in words, but it is very intuitive when viewed graphically.

In addition to configuring and positioning the slice cursor, we must have some means of extracting information about a particular slice from the Plot3D widget, so that the slice can be displayed. This is accomplished using the slice subcommands.

The slice subcommand returns a list which contains the value of every line in the graph for a given X coordinate. The X coordinate need not be an integer -- adjacent integral values of the line are interpolated to find the values at intermediate points.

As an example, consider figure 4.1, and especially the middle graph in that figure. The 2-D plot to the right of the 3-D plot is a slice of the 3-D plot taken at an X coordinate of 6.62276. The values for the 2-D plot were extracted from the 3-D plot using the command:

  gr slice 6.62276
The result of these command is the list:
  {140 130 180 98.862 192.455}
Because none of the lines actually define a value where X is 6.62276, the values returned above are interpolations of the values at X equals 6 and X equals 7. Thus the fourth number, 98.862, is 62% of the value when X is 7 (80.0 * 0.62276 = 49.0412) plus 38% of the value when X is 6 (130.0 * 0.37734 = 49.8208).

The 2-D graph of the slice must be handled separately from the Plot3D widget. (One of the 2-D barchart widgets in the BLT extension might be good for this, or you can grow your own from a canvas.) But the Plot3D widget does provide a means to notify the 2-D graph to change its display whenever the slice cursor moves. The ``-cursormovecallback'' configuration option on the Plot3D widget defines a Tcl/Tk command that is executed whenever the slice cursor is moved. In the example, the argument to -cursormovecallback is a Tcl procedure named ``PlotSlice''. The PlotSlice procedure finds the value of the current slice and then updates the 2-D plot accordingly. The following code illustrates the idea:

  proc PlotSlice {} {
    set data [gr slice [gr cursor]]
    # Update the 2-D plot with the information in $data
  }
  gr config -cursormovecallback PlotSlice
We have omitted the details of how PlotSlice updates the 2-D plot since that is very implementation dependent. But you should be able to get the general idea from this simple example.

5 Adding You Own Annotations

Look back at figures 1.2 and 1.3 above. Both of these images are screen shots of an ordinary Plot3D widget. The Plot3D widget drew everything in these figures, including the extra annotation of the axis and labels and arrows pointing to the two graph lines. You too can add additional annotation like this to your graphs, and this section will tell you how.

There are two features of the Plot3D widget that allow you to add your own annotations: the ``-redrawcallback'' configuration option and the translate subcommand. The -redrawcallback configuration option allows you to specify a Tcl procedure that is invoked every time the graph is redrawn (because the position of the observer changes). Thus, you use the -redrawcallback to specify a routine that actually draws your annotations on the canvas. The translate subcommand converts 3-D coordinate in the space of the graph into the 2-D coordinates space of the canvas widget on which the graph is drawn. This allows your annotation drawing routine, the one called by the -redrawcallback, to figure out where on the canvas widget it needs to draw.

These ideas are best illustrated by an example. The code that puts the ``Graph Line 0'' annotation on figures 1.2 and 1.3 looks something like the following:

  proc DrawLabel {} {
    eval .c create line \
      [gr translate 0.625 1.0 1.2] \
      [gr translate 0.625 0.68 0.75] \
      -arrow last -tags gr
    eval .c create text \
      [gr translate 0.625 1.0 1.25] \
      -text {{Graph Line 0}} -anchor e -tags gr
  }
  gr config -redrawcallback DrawLabel

The procedure DrawLabel does the actual work of drawing the annotation. This procedure contains two commands. The first adds an arrow to the canvas between the points (0.625,1.0,1.2) and (0.625,0.68,0.75). This is the arrow that points from the label to the graph line. The second statement in the DrawLabel procedure adds the text of the label to the canvas.

Notice in both statements how the gr translate subcommand is used to convert 3-D graph coordinates into 2-D canvas coordinates. This translation takes into account the current magnification and rotation of the graph, so your annotation rotates with your graph image automatically.

The return value from gr translate is a list of two floating point numbers. However, the canvas create command requires coordinates that are separate arguments, not elements of a list. For that reason, we have to preface each canvas create command with the eval command that will expand the list of coordinates returned by gr translate into separate arguments. The fact the eval is used means that the argument to the ``-text'' option on the second statement must be enclosed in two sets of curly braces.

One final detail that is important to note is the ``-tags gr'' argument which is added to every canvas item created as part of the annotation. Whenever the graph is redrawn, the Plot3D widget must first remove the prior version of the graph. It does this by executing the command

  .c delete gr
where ``gr'' is the name of the Plot3D widget instance. Everything that the Plot3D widget draws on the canvas is tagged with its own name, so that deleting everything with that tag will delete everything previously drawn. Any annotations you add to the graph should also be tagged with the name of the Plot3D widget instance so that they too will be deleted whenever the graph gets redrawn. If you don't, your old annotations won't be deleted when new ones are drawn, and your display will quickly become cluttered with out-of-date annotations.

6 Compiling And Installing The Plot3D Widget

All source code to the Plot3D widget is contained in a single file named plot3d.c. Compile this file as any other Tcl/Tk source file.

  cc -O -c plot3d.o
The entry point for initializing the Plot3D widget is a function named Plot3d_Init(). This function takes a single argument which is a pointer to the Tcl/Tk interpreter and returns TCL_OK. To permanently add the Plot3D widget to a custom version of the wish interpreter, modify the tkAppInit.c source file in the Tk source tree to include the following code:
  if (Plot3D_Init(interp) == TCL_ERROR) {
     return TCL_ERROR;
  }
Comments in the tkAppInit.c will show you exactly where this code needs to be inserted. After making this modification to tkAppInit.c, add the plot3d.o object file to the link list in your Makefile and then type ``make'' to rebuild your customized wish.

You can also compile the Plot3D widget as a loadable module so that it can be used with an existing wish interpreter or from the Tcl/Tk plugin for Netscape. Under Linux, the command sequence to compile Plot3D into a loadable modules is the following:

  gcc -fPIC -O -c plot3d.c
  gcc -shared -o plot3d.so plot3d.o
The output file plot3d.so is the loadable module. To use it in a Tcl/Tk script, enter the following command:
  load ./plot3d.so Plot3d
Notice that you must put the plot3d.so file in one of the directories in which Tcl normally looks for loadable modules, or else you have to specify a complete pathname for the module.

7 Summary

The Plot3D widget is useful on its own as a means for generating interactive 3-D plots of numerical data. But more than that, the design of the Plot3D widget illustrates a promising new technique for developing highly graphical super-widgets on top of the very versatile canvas widget that is a stardard part of Tcl/Tk. By leveraging the existing functionality of the canvas widget, the Plot3D widget was constructed with only a modest amount of additional code (less than 2000 lines) and in only a few weeks of programmer time. We hope that others will join us in generating new graphical super-widgets built on top of the Tk canvas.

8 Copyright Information

The copyright for the source code to the Plot3D widget, and this documentation, is the exclusive property of

Conservation Through Innovation, Ltd
1040 Whipple Street, Suite 225
Prescott, AZ 86301
520.776.7107
http://www.cti-ltd.com