Simplot

Introduction

Simplot is designed to make technical style graphs with Common Lisp using a simple user interface. It is based on the GD Graphics Library1 and uses the CL-GD2 package.

Simplot has been written with the intention to be as portable as possible, but has only been developed and tested to date on Linux with SBCL.

This is an early release – designed to illicit feedback, suggestions on the interface and help with portability issues.

Requirements

Assuming that the dependency requirements have been met, i.e., that CL-GD is already installed properly and tested, and that the Simplot package definition file is in the asdf central-registry path. The best method to compile and load SIMPLOT is to use ASDF:

  asdf:oos 'asdf:load-op :simplot

Useage

The following package environment has been used for the examples which follow:

  (defpackage #:simplot-user
    (:use #:cl
          #:cl-gd
          #:simplot))
  (in-package :simplot-user)

Data Structure

Simplot only makes 2-dimensional graphs currently. The data structure used manage data to the PLOT2 graphing function is the 2D-DATA structure, which consists of a two simple lists of numeric data. These should be the same length, but if they are not, the graph will merely assume they are correlated index-wise from the beginning and stop plotting at their common length.

  (defstruct 2d-data
    "Container for two lists of numeric data"
    x
    y)

Reading the sample data

The tarball contains a file called "user-helpers.lisp" which contains some functions which know how to read and manipulate the data used for this demonstration. After loading the file the data can be read and a data structure created and plotted:

  (load "user-helpers")

  (defparameter *data1* (read-ascii-file "test/temp-s.data"))
  (defparameter *time-temp* (make-2d-data
                             :x (aref *data1* 0)
                             :y (aref *data1* 1)))

*DATA1* is a 2-dimensional simple array of UTC times and temperatures in Fahrenheit.

Note: all of the code used for these examples can be downloaded including the weather station3 data and the helper functions used to manipulate it.

Basic graphing

SIMPLOT has been designed to get out of your way and 'just work' as much as possible. For example, if you just want to get a quick look at what the data above looks like, you can call PLOT2 as follows:

  (plot2 *time-temp*)

If you look in the current directory, you should see a newly created image file "_plot.png" which looks like Figure 1

Figure 1. Simple graphing example.
Figure 1. Simple graphing example.

The only trouble with doing it this way is you have to go off and manually launch a viewer each time, which can get to be tiresome.

Viewer applications

Now, I don't know yet what portablity challenges I'll run into on non-Linux platforms, but I believe there is always a way to call an external viewer application from any Lisp. Assuming this is true, I have enabled this capability in to PLOT2 using SBCL's sb-ext:run-program extension.

So, glossing over the compatibility issue for the moment, the idea is – if you have an image viewer that launches from the command line with the image path as an argument, PLOT2 can use it.

  1. You can tell it the name of your viewer as a keyword to launch it:
      (plot2 *time-temps* :viewer "/usr/bin/eog")
  2. There is also a function which will setup a default viewer application, so that all you need to do to enable vieweing is pass the viewer keyword to a non-NIL value to enable it:
      (set-viewer-app "/usr/bin/feh")
      (set-viewer-state T)
  3. You can also turn on viewing so that all calls to PLOT2 will automatically launch the viewer application4 without any further prompting whatever:
      (set-viewer-app "/usr/bin/feh")
      (set-viewer-state T)
      (plot2 *time-temps*)

Output

By default, PLOT2 produces the file "_plot.png" in the current Lisp directory. This can be set to any valid pathname by keyword parameter:

  (plot2 *time-temp* :ofile "user/fig1.png")

Note that output image files are always PNG files currently. The GD Library supports other image formats and this capability could be easily exploited in the future.

Adding Axes

Axes are located using the keywords (:left :bottom :right :top :origin-v :origin-h). To turn on an axis, its locater is added to the PLOT2 argument list after it's data:

  (plot2 *time-temp* :left :bottom)

By default, the viewing region is selected to bound the data. Axes divisions are chosen to give something reasonable given the span of the data. The graph obtained immediately above shows a reasonable vertical axis, but UTC seconds are unreasonable numbers and not very useful. later we will plot dates directly, but for now, we will get by merely by scaling the time into relative hours:

  (defparameter *hour-temp* (make-2d-data
                             :x (utc-to-hours (aref *data1* 0))
                             :y (aref *data1* 1)))

  (plot2 *hour-temp* :left :bottom)

Figure 2 shows the result below.

Figure 2. Simple graph with two
			autoranged axes which bound the data.
Figure 2. Simple graph with two autoranged axes which bound the data.

Multiple view data on one graph

Plotting multiple data sets on a single graph is simple: just add more data arguments. Building on the previous example, if we read an additional data set *DATA2*, convert it into relative hours as before, and add its 2D-DATA structure *TIME-DEWP* to the PLOT2 arguments:

  (defparameter *data2* (read-ascii-file "test/humi.data"))
  (defparameter *time-dewp* (make-2d-data
                            :x (utc-to-hours (aref *data2* 0))
                            :y (calc-dewpoints (aref *data2* 1)
                                                           (aref *data2* 2))))
  (plot2 *hour-temp* :left :bottom *time-dewp* :right :top)
The graph of Figure 3 will result.
Figure 3. Overlay of data
                                         sets — multiple dataset
                                         graphs.
Figure 3. Overlay of data sets — multiple dataset graphs.

Unfortunately, they came out the same (default) color5 . The best solution to this is differentiate between these two datasets with color.

Color

Colors are specified by unsigned RGB triplets (mod 256). Many common colors have DEFVAR values available already, such as:

      +wht+, +blk+, +red+, +grn+, +blu+, +mag+, +cyn+, +ylw+, +orn+
    

Colors typically modify the nearest object they follow in the PLOT2D argument list. The following examples illustrates two ways of specifying the color directly (see the result in Figure 4 ).

  (plot2 *hour-temp* '(0 80 40) :left :bottom *time-dewp* +blu+ :right :top)
Figure 4. Using color to differentiate
                             multiple datasets
Figure 4. Using color to differentiate multiple datasets

One still cannot tell which axis goes with which dataset. We will give each axis a label and a color associated with it's dataset to make the correspondence clear:

  (let ((grn '(0 80 40)))
    (plot2 *hour-temp* grn
           :left "Temperature (degrees F)" grn :bottom "Relative Hours"
           *time-dewp* +blu+ :right "Dew Point (degrees F)" +blu+
           :top "Relative Hours"))
  

Figure 5 shows a much clearer graph. The main objection here might be that the time scales ought to line up, and perhaps the temperature scales as well. This can be accomplished by using the same view for both datasets.

Figure 5. Example using colored labels
                             and multiple datasets
Figure 5. Example using colored labels and multiple datasets

Common Views

It is common to want to look at multiple data sets on common axes. To accomplish this in Simplot you just instantiate a view structure and pass it as an argument after each data object being graphed. Here is an example:

  (let ((grn '(0 80 40))
      (the-view (mk-view :view-bounds '(0 0 500 60))))
  (plot2 *hour-temp* the-view grn
         :left "Temperature (degrees F)" grn :bottom "Relative Hours"
         *time-dewp* the-view +blu+ :right "Dew Point (degrees F)" +blu+))
Figure 6 shows the result.
Figure 6. Sharing a view so axes are
                         coincident for multiple datasets.
Figure 6. Sharing a view so axes are coincident for multiple datasets.

It is worth commenting on the order here: the data colors needs to come after their views, since they are attributes of the view and a new view would override the color with its own.

Titles

Adding a title is can be accomplished with a string preceding any data, or with a keyword anywhere in the argument list:

  (let ((grn '(0 80 40))
        (the-view (mk-view :view-bounds '(0 0 500 60))))
    (plot2 "December Weather Metrics"
           *hour-temp* the-view grn
           :left "Temperature (degrees F)" grn :bottom "Relative Hours"
           *time-dewp* the-view +blu+ :right "Dew Point (degrees F)" +blu+))
Figure 7 shows the result.
Figure 7. Titles are an attribute of the
                         plot.
Figure 7. Titles are an attribute of the plot.

Fonts

The GD Graphics Library can handle both bitmapped and truetype fonts. The bitmapped fonts will work in all environments. The truetype fonts should work in all but the most limited or brain-damaged environments. The assumption here is that anyone trying to do technical graphing probably isn't working in such limited environment and has truetype fonts installed6 .

Font environment settings

In order to work properly, both the GD Graphics Library and SIMPLOT need to know where to find your truetype fonts. In Unix systems this is managed by setting the GDFONTPATH environment variable7 . Here is what it looks like on one system:

  GDFONTPATH=/usr/share/fonts/truetype/msttcorefonts:\
             /usr/share/fonts/truetype/freefont:\
             /usr/share/fonts/truetype/latex-xft-fonts:\
             /usr/share/fonts/truetype/ttf-bitstream-vera:\
             /usr/share/fonts/truetype/ttf-dejavu:\
             /usr/share/fonts/truetype/ttf-liberation:\
             /var/lib/defoma/gs.d/dirs/fonts
Note that each separate path is separated by a colon ":".

When Simplot is compiled it will read this environment variable (by calling an SBCL system function which, hopefully, has counterparts in other Lisps). It walks each of these directories searching for ttf files, and adds the ones it finds to an internal hash it uses to keep track of them. It will work with "families" of fonts — lists of font names like CSS — which it will use if available, falling down through the list if not.

Changing fonts

You can examine your current font configuration with the function:

  (dump-font-config)

You can see the output here . It lists all the installed fonts alphabetically, the value of GDFONTPATH, the font paths it searched (these may not be the same, as you can add your own). It also shows the current values of the four default fonts used by Simplot: the default title, label, tic and legend fonts.

To change these defaults, instantiate a UFONT structure and set the corresponding default:

  (let ((title "Sample plot with font substitutions")
        (left   "degrees Fahrenheit")
        (bottom "Relative Hours")
        (title  '("December Temperature" "North Bend, WA"))
        (blu '(75 138 224))
        (purisa (make-ufont :name "Purisa" :size 14 :color '((0 45 255)))))
    (set-default-title-font purisa)
    (plot2 purisa *hour-temps* blu :left left purisa :bottom bottom purisa
           :right :title title)
    (set-default-label-font purisa)
    (plot2 purisa *hour-temps* blu :left left :bottom bottom :right
          :title title))

Both these PLOT2 calls produce the same graph (see Figure 8 ), but only the first one makes the font substitution temporarily8 . The SET-DEFAULT-LABEL-FONT call preceding the second PLOT2 call changes the default axis label font making it permanent.

Figure 8. Customized font example (Purisa).
Figure 8. Customized font example (Purisa).

Notice that the RGB value in the call to MAKE-UFONT is wrapped in a list? That's because it is also used9 to hold the color pallete index allocated by the GD library for the image (you can see this by examining the structures in the output here . The legend isn't being used so its color wasn't allocated.

Graph dimensions

The image size and margins can be modified by keywords. The image size is a list of two pixel dimensions: (width height); the margins are specified as the list of four percentages of the total span from edge to graph (left bottom right top).

  (let* ((title '("December Temperatures Metrics"))
         (size  '(450 250))
         (margs '(14 13 3 13))
         (green '(10 65 15))
         (purpl '(110 5 90))
         (tfont  (make-ufont :name "Waree" :size 12 :color (list green)))
         (afont  (make-ufont :name "Waree" :size 8 :color (list green))))
    (plot2 :margins margs :image-size size :title title tfont
           *hour-temps* purpl 
           :left "degrees Fahrenheit" afont :bottom "Elapsed Hours" afont)
which is shown in Figure 9 .

Figure 9. Resized
          figure from default has margins shrunk for optimal use of
          space<sup><small>10</small></sup>.
Figure 9. Resized figure from default has margins shrunk for optimal use of space10.


To Do List

  1. Document UTC date axis handling
  2. Grid lines
  3. Legends
  4. Arbitrary positions for titles and axis labels
  5. Hide rgb.idx color list input to UFONT constructors
  6. Enable other output image file types
  7. Rebuild bitmapped font compatibility
  8. Add color palette for multiple data set defaults

Known Bugs

  1. Labels which are located entirely outside the image will cause the GD Library to generate a floating-point exception which leaves Lisp in a state it can't recover from without reloading the library.
  2. Multi-line titles don't auto-center vertically
  3. I can't figure out yet how to make my notes in the right column "float" to the bottom of the column in each section of this document. Is this a basic limitation of HTML? Surely there must be some way — short of adding a calculated crap-load of <br>'s...




Back to Home
Last Modified: 2010-2-8 07:29:35
Contact