ToCDocOverviewCGDocRelNotesFAQIndexPermutedIndex
Allegro CL version 8.1
This page is new in 8.1.

A Chart Widget Tutorial

This document contains the following sections:

1.0 Introduction to the Chart Widget
2.0 An initial complete chart-widget example
3.0 The chart-widget tutorial
   3.1 Adding data to a chart-widget
   3.2 Chart axes
   3.3 Modifying existing chart items
   3.4 Additional types of chart values
   3.5 Animated charts
   3.6 Multiple chart objects
   3.7 Alternate chart views
   3.8 Meaningful ID's for chart objects
   3.9 Label-printing functions
   3.10 Meaningful labels for chart items
   3.11 Meaningful ID's for chart items
   3.12 Accessing existing data rather than pushing chart values
   3.13 Graphing Mathematical Functions
   3.14 Animating mathematical functions
   3.15 Major and minor tic marks, tic labels, and grid lines
   3.16 Filling the entire widget
   3.17 Saving a chart image
   3.18 Some final standalone examples
   3.19 Using Two Value Axes


1.0 Introduction to the Chart Widget

The Chart Widget is a new feature in Allegro CL 8.1. This document provides a tutorial on its use.

There is a chart-widget example in the IDE's Navigator Dialog. You may want to try it first to get a feel for the variety of things that the chart-widget can do. Then return to this tutorial to learn how to write chart-widget code from scratch. Once you have displayed the Navigator Dialog (using the Help menu), display the Examples tab and search for chart-widget in the list of examples.



2.0 An initial complete chart-widget example

Before beginning the actual tutorial, here is a complete example of typical code for creating a chart-widget and giving it some values. This is to give you an overview of the kind of code that is involved.

Evaluate the following code and check out the chart that it creates. Here's how you can evaluate the pieces of code throughout this tutorial. If you are viewing this in the IDE's Allegro Help window, then you can select each form by clicking down just to the left of the opening parenthesis and then dragging just past the final closing parenthesis and releasing. You can then use the Tools | Incremental Evaluation command (control-E in Host mode) to evaluate the form in place. Otherwise you could first copy each form to the editor and evaluate it there. To evaluate the whole sequence of forms most quickly, open the file [Allegro directory]/examples/cg/chart-tutorial.cl. In that file you can simply press the numeric keypad's Enter key multiple times to evaluate successive forms and skip over the comment blocks. (Alternately use the main Enter key while holding down the Alt key.)

(let* ((width 400)
       (height 400)
       (chart-widget
        (make-instance 'chart-widget
          :chart-view :bar
          :title "Bowling Scores"
          :chart-objects #((:id :doris)
                           (:id :tal)
                           (:id :hubert))
          :item-axis (make-instance 'item-axis
                       :axis-label "Month"
                       :minor-tics-per-major-tic 3
                       :draw-minor-labels nil
                       :on-print-major-label
                       (lambda (month-symbol)
                         (format nil "~:(~a~)" month-symbol)))
          :value-axis (make-instance 'value-axis
                        :axis-label "Average Scores"
                        :margin-inside-axis-label 8
                        :range-bottom 0
                        :range-top 900
                        :major-tic-increment 300)
          :bar-chart-view (make-instance 'bar-chart-view
                            :values-are-stacked t
                            :bar-width 16
                            :bar-spacing 8)
          :chart-legend (make-instance 'chart-legend
                          :on-print-chart-object
                          (lambda (value)
                            (format nil "~:(~a~)" value)))
          :right-attachment :right
          :bottom-attachment :bottom
          :left 0 :top 0
          :width width :height height))
       (dialog (make-window :chart-tutorial
                 :class 'dialog
                 :title "Sample Chart"
                 :scrollbars nil
                 :interior (make-box-relative 0 40 width height)
                 :dialog-items (list chart-widget))))
  (dotimes (item-index 12)
    (dotimes (object-index 3)
      (set-chart-value
       chart-widget
       :item-id (aref #(january february march april may june july
                            august september october november december)
                      item-index)
       :object-id (aref #(:doris :tal :hubert)
                        object-index)
       :value (+ 100 (random 150)))))
  dialog)

This window should be displayed (but the values are chosen randomly so the heights of the bars will be different):

That example specifies the values of various chart properties as initargs to make-instance, which is the typical way to do it. In the tutorial we will usually specify property values by calling accessor functions instead, so you can see them change one at a time.

Notice in the code above how certain chart "sub-objects" such as the item-axis are created first, and then the sub-objects are passed as initargs to the chart-widget itself. (If you don't specify a particular sub-oject explicitly, a default one will be created automatically.)

After creating the chart-widget, set-chart-value is called a number of times to add data values to the chart.

After looking over that code a bit to get a general feel for it, you can close the chart dialog and proceed with the tutorial, where we will take things one step at a time.

In the IDE, a chart-widget can alternately be created interactively by using the chart-widget button on the Components Toolbar to place a chart-widget onto a form window in a project. The button shows two vertical bar-chart bars, one red and one blue. You can then modify the properties in the inspector as with other widgets.



3.0 The chart-widget tutorial

First we'll make an empty chart-widget on a dialog, looking like this:

We'll reuse this single chart-widget throughout the tutorial, and so we'll bind the global variable *chart-widget* to the tutorial widget to make it easy to modify it with top-level forms. And since we'll be changing the state of this single widget through the tutorial, it's important to evaluate the tutorial code sequentially, since some steps undo a state that was created in the previous step. And don't close the "Chart Tutorial" window until the end.

(let* ((width 500)
       (height 500)
       (chart-widget (make-instance 'chart-widget
                       :chart-view :bar
                       :right-attachment :right
                       :bottom-attachment :bottom
                       :left 0 :top 0
                       :width width :height height))
       (dialog (make-window :chart-tutorial
                 :class 'dialog
                 :title "Chart Tutorial"
                 :scrollbars nil
                 :interior (make-box-relative 0 40 width height)
                 :dialog-items (list chart-widget))))
  (setq *chart-widget* chart-widget)
  dialog)

You may want to move and resize the tutorial dialog that you just created so that it's beside this window where you're reading the tutorial. And you might want to watch the chart widget as you evaluate each expression so that you notice each change to the widget.

Remember not to close this "Chart Tutorial" dialog until you're done with the tutorial.

While the chart-widget has many properties for customizing its appearance, you typically don't need to specify many of them because they have reasonable default values. Above we initially specified only :chart-view :bar to display the data as a bar chart. (The default value is :line, which displays the same data as a line graph instead.) As usual with widget properties, most any chart-widget property can be set up at creation time with an initarg, as in :chart-view :bar, or changed any time later by calling the associated property-setting function, as in (setf (chart-view *chart-widget*) :bar).


3.1 Adding data to a chart-widget

Without specifying any further properties, we can immediately begin adding values to the chart and see what happens. We do this by calling the main chart-widget function, which is set-chart-value. Let's add an initial value of 12.

(set-chart-value *chart-widget* :value 12)

Notice that the range of values on the vertical axis at the left automatically became 0 to 15, with additional tic marks for 5 and 10. The chart-widget by default will automatically choose "round" (decimal) values for the value range so that it contains all of the values that you've added so far. (To override this computed range, you could provide explicit values for the range-bottom and range-top properties.)

Adding additional values will continue to use the same range as long as the new values fit within it.

(progn
  (set-chart-value *chart-widget* :value 7)
  (set-chart-value *chart-widget* :value 14))

But adding a value outside the range will cause the range to be expanded as needed.

(set-chart-value *chart-widget* :value 37)

And of course negative values are no problem.

(set-chart-value *chart-widget* :value -4)

3.2 Chart axes

The vertical axis at the left is called the value axis. It always displays a continuous range of values between a minimum value and a maximum. The horizontal axis at the bottom, on the other hand, is called the item axis because it always displays a sequence of discrete items, which have values.

The value axis does not have to be the vertical one; the axes can be swapped by setting the chart-orientation property. So it's best to always speak of the value axis and the item axis rather the vertical and horizontal axes. And either axis can increase in the opposite direction via the invert-axis property.

Let's give these axes some labels to identify them. While we're at it, let's add titles to the whole chart as well. Normally you'd pass the initargs for these properties when creating the widget, but we're doing things one at a time here.

(progn
  (setf (axis-label (value-axis *chart-widget*)) "Value")
  (setf (axis-label (item-axis *chart-widget*)) "Item")
  (setf (title *chart-widget*) "Chart Tutorial")
  (setf (subtitle *chart-widget*)
    "Building a chart one step at a time."))

Notice how the body of the chart became smaller to make room for the new labels. The layout is another thing that the widget handles for you automatically, though there are properties for overriding things like the spacing between parts of the chart and the font sizes and so on.

Don't miss the special thing that we needed to do just now to modify a property of the value axis:

(setf (axis-label (value-axis *chart-widget*)) "Value")

We first needed to call value-axis to find the value axis sub-object of the chart widget, and then call (setf axis-label) on the value axis. There are two reasons for this design:

  1. It avoids redundant properties between the two kinds of axes, which share almost all of the same properties.
  2. There are so many chart properties that dealing with all of them on a single object would be somewhat unwieldy.

The complete set of sub-objects is item-axis, value-axis, bar-chart-view, line-graph-view, and chart-legend. If you're editing a chart-widget in the inspector, you will need to click on those sub-object names in the left column to get to their properties. That's not needed with most widgets.


3.3 Modifying existing chart items

So far we've used set-chart-value only to add new bars to our bar graph, but it's more versatile. You can also change the value of existing chart items at any time, for example. The bar labeled "3" looks a little too tall, so let's make it shorter by passing its index along with a new value.

(set-chart-value *chart-widget* :item-index 3 :value 10)

You might notice that the value axis range could be made smaller now, but that wasn't done automatically. The value range is adjusted automatically only when it needs to be larger for a few different reasons:

  1. The smaller case is less of a problem since no values get clipped off.
  2. Typically values don't all shrink by enough to make a smaller range important.
  3. (This one especially) it would take much longer to detect the smaller case because all values would need to be checked rather than just the new one, and so it might noticeably slow down calls to set-chart-value.

But you can tell the chart to recompute the range at any time by calling update-chart-widget.

(update-chart-widget *chart-widget*)

3.4 Additional types of chart values

The value argument specifies the principal value for a chart-item, but there are additional types of values as well. When showing the bar-chart view, the widget can also display a from-value argument by making the bar begin at a different place than the usual "base value". Let's make the fourth bar begin at a non-zero value.

(set-chart-value *chart-widget* :item-index 3 :from-value 3)

Other auxilliary value types include high-value, low-value, and icon-size.

(set-chart-value *chart-widget* :item-index 1
                 :high-value 8 :low-value 4)

These values are drawn as T's by default, a style which is often used to indicate error ranges. But high and low values have a couple of other drawing styles as well. (The default is called :tee.)

(setf (high-low-value-style (bar-chart-view *chart-widget*))
  :bar)

(setf (high-low-value-style (bar-chart-view *chart-widget*))
  :diamond)

Any values that you've added can be turned off by setting them to nil instead of a number. So let's turn those high and low values back off (one at a time this time, to show that they are independent).

(set-chart-value *chart-widget* :item-index 1 :low-value nil)

(set-chart-value *chart-widget* :item-index 1 :high-value nil)

3.5 Animated charts

A set of bars could be used as a set of meters by updating their values repeatedly. Here's a quick example of modifying bars randomly. This example passes the update-now argument to redisplay the chart after every change for an animation effect, rather than letting them all get queued for a single redisplay at the end, as would normally happen. It also calls chart-value to read an existing value in order to modify it incrementatlly.

(dotimes (j 150)
  (dotimes (item-index 5)
    (set-chart-value *chart-widget*
                     :item-index item-index
                     :value (+ (chart-value *chart-widget*
                                            :item-index item-index)
                               (* (random 4)
                                  (sin (/ (* 2 pi (+ j (* 10 item-index)))
                                          50))))
                     :update-now t)))

Feel free to evaluate that one again if you didn't catch it well.

The item-index argument to set-chart-value can be any non-negative integer to either add a new item or modify an existing one. It can also be the keyword :new to add a new item just after all existing items; that's the default behavior that we saw early on.

item-index can also be the keyword :last, to modify the chart item that is currently the "last" one, meaning the one with the highest index. This is particularly useful when adding a series of values for multiple chart "objects", which we'll learn about next.


3.6 Multiple chart objects

Our chart has several bars so far, but these bars all refer to a single "chart object". A chart object typically represents a real-world thing of some sort, such as a bowler. A chart object will have a sequence of values that are displayed in multiple "chart items" along the item axis, such as an average bowling score for each month. The chart-widget can simultaneously display multiple objects by drawing multiple lines across a line graph, or stacked bars or side-by-side bars in a bar graph.

Use the object-index argument to set-chart-value to specify the object for the new value. The default value is 0, so you don't need to pass an object-index at all when there is only a single object in the chart.

Let's fill in some values for multiple objects to see what that looks like.

First we'll clear off those old values by setting the chart-items property to nil. (The chart-items property holds a tree of values that the widget collected from all of the calls to set-chart-value. You could cons up this tree yourself and place it into this property instead of calling set-chart-value, but that's probably less convenient.)

(setf (chart-items *chart-widget*) nil)

Now we'll fill in a bunch of monthly bowling scores. This example uses the special :last and :new values of the item-index argument to add values for several chart objects to one chart-item before making a new chart item.

(progn
  (setf (axis-label (item-axis *chart-widget*)) "Month")
  (setf (axis-label (value-axis *chart-widget*)) "Average Score")
  (dotimes (item-index 24)
    (dotimes (object-index 5)
      (set-chart-value
       *chart-widget*
       :item-index (if (zerop object-index) :new :last)
       :object-index object-index
       :value
       (max 100
            (min 300
                 (+ (if (plusp item-index)
                        (chart-value
                         *chart-widget*
                         :item-index (1- item-index)
                         :object-index object-index)
                      (+ 100 (random 150)))
                    (random 21)
                    -10)))))))

That value argument did some fancy footwork to ensure that nobody's average changes by more than 10 points in one month, for a more realistic chart.


3.7 Alternate chart views

The incremental nature of each bowler's monthly score change may be more apparent if the bars are stacked vertically instead of resting side-by-side. That also saves space.

(setf (values-are-stacked (bar-chart-view *chart-widget*))
  t)

Or even more apparent if we were looking at a line graph instead of a bar chart.

(setf (chart-view *chart-widget*) :line)

You have probably noticed that a legend box appeared at the bottom of the chart for the first time when we used multiple chart objects. The reason is that the default value of the draw-legend property is :when-multiple-objects, since you typically don't need a legend for a single object. The chart-legend sub-object has various properties to control the layout of the legend.


3.8 Meaningful ID's for chart objects

Now this chart isn't particularly meaningful when each chart item and each chart object is labeled with an integer. That's the default behavior, but you can specify any labels that you like for each index.

First let's associate a real-world object with each chart object index. Watch the legend as you evaluate this form.

(setf (chart-objects *chart-widget*)
  (vector (list :id :doris) (list :id :cloyd)
          (list :id :hubert) (list :id :tal)
          (list :id :myrtle)))

The chart-objects property value can be either a vector or a list. Each object's plist within that sequence must be a list, though. The ID for each object is part of a plist because an object can have additional attributes, such as which of two value axes it belongs with (that's covered at the very end of the tutorial).


3.9 Label-printing functions

Adding chart object IDs causes the legend to print the IDs rather than their indices. By default the object IDs are printed with princ-to-string. But since people's names are normally capitalized, we should give the legend a printer function that converts the real-world object into a pretty string to display for it in the legend. Watch the legend again as you evaluate this form.

(setf (on-print-chart-object (chart-legend *chart-widget*))
  (lambda (value)(format nil "~:(~a~)" value)))

3.10 Meaningful labels for chart items

We can similarly add meaningful month labels to the chart item tic marks, to replace the default indices. There are two ways to do this. One way is to supply a printer function that converts directly from an item's index to the string to display. If we know that the values we added began at January 2005, then our function can calculate the month and year of each index, and format a string from those two values.

Watch the item axis as you evaluate this expression.

(setf (on-print-major-label (item-axis *chart-widget*))
  (lambda (value)
    (format nil "~:(~a~) ~a"
      (aref #(:jan :feb :mar :apr :may :jun :jul
                   :aug :sep :oct :nov :dec)
            (mod value 12))
      (+ 2005 (floor value 12)))))

By default, axis labels will wrap at spaces to save space. You can turn that off by setting the major-label-wrapping and/or minor-label-wrapping properties of the axis object.


3.11 Meaningful ID's for chart items

That was a little bit tricky writing an on-print-major-label function to convert item indices to item label strings, because the item indices aren't very meaningful. Another approach is to give each chart item a meaningful ID when the item is created. Then the meaningful ID will be passed to the printer function, which it can interpret more easily. (The default ID is the index.)

This second approach would even be necessary if the items are not regularly-spaced values with a simple mapping from their indices to their meanings. Let's say that our bowlers aren't in a league and instead bowl occasionally whenever they get the urge. Let's furthermore pretend that they've already got a record of recent scores stored away in a list like the following.

(defparameter *scores*
  '(((2005 dec 12) :doris 164 :tal 152)
    ((2006 feb 3) :doris 168 :tal 145 :hubert 97)
    ((2006 feb 18) :doris 160 :tal 173 :hubert 110 :myrtle 124)
    ((2006 jun 17) :doris 172 :tal 160 :myrtle 142)
    ((2006 aug 31) :tal 170 :myrtle 135)
    ))

We could use each date list like (2005 dec 12) as the ID of each chart item as we add it to the chart. Then all that the printer function needs to do is convert the date list to a pretty string. Let's undo the current chart values and printer function, and add values from our master *scores* list.

This example also passes an object ID instead of an object index. We already set up the object ID's earlier when we set the chart-objects property so that the object ID's would appear in the legend.

(progn
  (setf (chart-items *chart-widget*) nil)
  (setf (on-print-major-label (item-axis *chart-widget*)) nil)
  (let* (date person score)
    (dolist (event *scores*)
      (setq date (pop event))
      (loop (unless event (return))
            (setq person (pop event))
            (setq score (pop event))
            (set-chart-value *chart-widget*
                             :item-id date
                             :object-id person
                             :value score)))))

But those date ID's don't look very nice on the tic marks, so let's add a printer function back on, though this time it can be simpler by acting directly on a meaningful ID rather than on an index.

(setf (on-print-major-label (item-axis *chart-widget*))
  (lambda (value)
    (format nil "~:(~a~) ~a ~a"
      (second value)(third value)(first value))))

When the item-id argument is passed and no item-index argument is, then set-chart-value will try to find an existing chart-item that has that ID. If it finds one, then it modifies that existing chart-item, and otherwise it creates a new chart-item at the end. That's why we only needed to specify item-id and object-id to both create each new item and also to give an existing item additional values for the other chart-objects.

A subtle point: it's OK to use a list as an item ID because the default chart-item-id-equality-test is the function equal, which is good for comparing lists. For some types of ID's you may need to change that property (which in the inspector is on the Events tab).


3.12 Accessing existing data rather than pushing chart values

If you already have all of your values stored in a readily-accessible place such as an array, then you may want to use an alternative to set-chart-value, which is to supply a chart-value-returner function. This function will get called whenever a chart item is actually drawn (usually due to it being uncovered and/or scrolled into view). Your function should then look up the data value wherever it's already stored and return it. This avoids having the chart-widget code internally cons up a tree of values that you pass individually to set-chart-value.

Let's test this out by supplying a chart-value-returner function that looks up the requested value in our *scores* list. The arguments to our function will be the chart-widget itself, the value type (such as :value for the main value, as when calling set-chart-value), the item-index, the object-index, and the object-id. Our *scores* list records value by the object-id, so we'll look them up using that rather than the object-index. Using a chart-value-returner means that we no longer have chart-items with item-id's, and therefore our printer function will once again receive item indices, so we have to replace our tic label printer function again with one that takes indices.

First we'll clear the accumulated data in the chart-items property, or else it would override the chart-value-returner function.

(setf (chart-items *chart-widget*) nil)

And now we'll provide a chart-value-returner that will supply the same values lazily when needed, without ever accumulating data in the chart-items property. Since there is no longer an accumulated list of items from which a value range would be automatically computed, we also must set the chart-items-max-index property to tell the widget how many items it should draw.

When the value-type argument is :id, we will return the date list that we use as an item ID, and our on-print-major-label function will create a tic label string from that ID. When the value-type argument is :value, we will return the data value for the specified object and item. We can just ignore other value-type arguments since we don't care to display those values.

(progn
  (setf (chart-items-max-index *chart-widget*) 4)
  (setf (chart-value-returner *chart-widget*)
    (lambda (chart-widget value-type item-index
                          object-index object-id)
      (declare (ignore chart-widget object-index))
      (let* ((score-item (nth item-index *scores*)))
        (case value-type
          (:id (first score-item))
          (:value (getf (rest score-item) object-id)))))))

Of course, using an array would be more efficient than a list, but this is just a demo.


3.13 Graphing Mathematical Functions

Even if you have no data on hand, you could still use a chart-value-returner function to graph a mathematical function, for example. The item indices will always be integers beginning at zero, though, so you need to translate those indices into the actual X values of interest. Let's try a sine wave.

(let* ((num-steps 16)
       (item-axis (item-axis *chart-widget*))
       (value-axis (value-axis *chart-widget*)))
  (setf (on-print-major-label item-axis) 'princ-to-string)
  (setf (axis-label item-axis) "Degrees")
  (setf (axis-label value-axis) "Sin (stacked)")
  (setf (values-are-stacked (line-graph-view *chart-widget*)) t)
  (setf (chart-items-max-index *chart-widget*) num-steps)
  (setf (chart-value-returner *chart-widget*)
    (lambda (chart-widget value-type item-index
                          object-index object-id)
      (declare (ignore chart-widget object-index object-id))
      (let* ((degrees (* (/ item-index num-steps) 360)))
        (case value-type
          (:id (float degrees))
          (:value (sin (/ (* degrees 2 pi) 360))))))))

3.14 Animating mathematical functions

If the chart-value-returner property remains the same function, but you happen to know that something has changed that will cause the function to return different values, then you can call update-chart-widget to display the new values. That function will uncache data that was computed for the old values, and invalidate the widget so that the new values will be drawn. To cause the new values to be drawn immediately, pass the update-now argument.

Here's a variation of the sine wave example that animates the wave in this way. Here the chart-value-returner function closes over a frame variable that is later incremented to cause the chart-value-returner to draw different values each time the chart is redisplayed.

(let* ((num-frames 60)
       (frame 0)
       (num-steps 16)
       (value-axis (value-axis *chart-widget*)))
  (setf (range-bottom value-axis) -5)
  (setf (range-top value-axis) 5)
  (setf (chart-items-max-index *chart-widget*) num-steps)
  (setf (chart-value-returner *chart-widget*)
    (lambda (chart-widget value-type item-index
                          object-index object-id)
      (declare (ignore chart-widget object-index object-id))
      (let* ((degrees (* (/ item-index num-steps) 360)))
        (case value-type
          (:id (float degrees))
          (:value (* (sin (+ (/ (+ (* degrees 2 pi)
                                   (/ (* 360 frame pi) num-frames))
                                360)))
                     (1- (/ frame (floor num-frames 2)))))))))
  (loop (when (> frame num-frames)(return))
        (update-chart-widget *chart-widget* :update-now t)
        (incf frame)))

Here's a more interesting variation that we won't attempt to explain.

(let* ((num-frames 60)
       (frame 0)
       (num-steps 16)
       (value-axis (value-axis *chart-widget*)))
  (setf (range-bottom value-axis) -5)
  (setf (range-top value-axis) 5)
  (setf (chart-items-max-index *chart-widget*) num-steps)
  (setf (chart-value-returner *chart-widget*)
    (lambda (chart-widget value-type item-index
                          object-index object-id)
      (declare (ignore chart-widget object-index object-id))
      (let* ((degrees (* (/ item-index num-steps) 360)))
        (case value-type
          (:id (float degrees))
          (:value (* (sin (/ (* degrees frame .18 pi) 360))
                     (1- (/ frame (floor num-frames 2)))))))))
  (loop (when (> frame num-frames)(return))
        (update-chart-widget *chart-widget* :update-now t)
        (incf frame)))

Well, that was all jolly good fun. But now to back up and cover some other chart-widget properties. First we need to restore the more boring sine wave.

(let* ((num-steps 16)
       (item-axis (item-axis *chart-widget*))
       (value-axis (value-axis *chart-widget*)))
  (setf (on-print-major-label item-axis) 'princ-to-string)
  (setf (axis-label item-axis) "Degrees")
  (setf (axis-label value-axis) "Sin (stacked)")
  (setf (values-are-stacked (line-graph-view *chart-widget*)) t)
  (setf (chart-items-max-index *chart-widget*) num-steps)
  (setf (chart-value-returner *chart-widget*)
    (lambda (chart-widget value-type item-index
                          object-index object-id)
      (declare (ignore chart-widget object-index object-id))
      (let* ((degrees (* (/ item-index num-steps) 360)))
        (case value-type
          (:id (float degrees))
          (:value (sin (/ (* degrees 2 pi) 360))))))))

3.15 Major and minor tic marks, tic labels, and grid lines

The chart-widget and its sub-objects have many additional properties that determine the final look of the chart. Most of these should be fairly obvious, or easy enough to figure out by playing with them in the inspector. One final matter that may not be so obvious, though, is major and minor tic marks. Both the value axis and the item axis can have major tic marks, tic labels, and grid lines and/or minor ones between successive major ones. The value axis tends to pick out "round" values for these properties for you, but you are likely to want to set these yourself for the item axis where the widget can't know what values would fit your arbitrary chart items.

In particular, you may want to set the minor-tics-per-major-tic property of the item-axis. This is always 1 by default, and so all of the tics along the item axis are major ones. If you happen to know that each of your items represent a quarter of a year, then you may want to set minor-tics-per-major-tic to 4 so that there will be a major tic for each year, with three minor tics in between for each quarter.

Let's try that with our sine wave, where a minor-tics-per-major-tic of 4 will place a major tic at each 90-degree point. And by turning off minor labels, we will cause the values to be closer together since they don't have to leave room for all those minor labels.

(let* ((item-axis (item-axis *chart-widget*)))
  (setf (minor-tics-per-major-tic item-axis) 4)
  (setf (draw-major-grid-lines item-axis) t)
  (setf (draw-minor-grid-lines item-axis) t)
  (setf (draw-minor-tics item-axis) nil)
  (setf (draw-minor-labels item-axis) nil))

Another way to save tic label space is to draw only every N'th label. So we could decide to draw minor labels again, but only every second one. Notice that a minor label will never be drawn on the same item where a major label is drawn (or even where a major tic is drawn).

(let* ((item-axis (item-axis *chart-widget*)))
  (setf (draw-minor-labels item-axis) t)
  (setf (minor-label-frequency item-axis) 2))

3.16 Filling the entire widget

One more interesting property to mention: If your data doesn't fill the entire width of your chart, you may want to make it stretch to fit by turning on the fit-chart-items property.

(setf (fit-chart-items *chart-widget*) t)

3.17 Saving a chart image

Here's how you can save the current image of the chart-widget as a BMP file. This works with any Common Graphics window other than an os-widget-window, though if the double-buffered property is not turned on then the window must be exposed at the time. (A chart-widget always has double-buffered turned on.)

This example will save a chart as a BMP file and then show it in whatever program is registered on your machine to display BMP files.

(let* ((pathname (merge-pathnames "chart.bmp"
                                  (sys:temporary-directory))))
  
  ;; Create a pixmap from the chart's window, and then write it to a
  ;; BMP file.
  (save-pixmap (get-pixmap (window *chart-widget*))
               pathname)
  
  ;; Show the saved pixmap in a third party program.
  (run-shell-command
   (windows-command-for-document-type "bmp" :path pathname)))

3.18 Some final standalone examples

We'll close the tutorial with a couple of standalone examples that don't follow from the sequence above. So feel free to close the Chart Tutorial window at any time now.

First an example similar to the one at the top. This will illustrate some of the finer points of major and minor tics and labels. Specifically, we'll show monthly data with three months per minor tic and four quarters per major tic, and offset the major tic labels so that they are drawn in the middle of the years that they represent rather than at the beginning of the years.

We recommend that you study the various properties in this code to see how they work together.

(let* ((width 800)
       (height 400)
       (object-ids #(:doris :hubert :myrtle :cloyd
                           :tal :gladys))
       (chart-widget
        (make-instance 'chart-widget
          :chart-view :line
          :title "Bowling Scores"
          :chart-objects (map 'vector (lambda (id)
                                        (list :id id))
                           object-ids)
          :item-axis (make-instance 'item-axis
                       :axis-label "Date"
                       
                       ;; This one is for data values
                       ;; per minor tic, which in this
                       ;; case is months per quarter.
                       :minor-tic-increment 3
                       
                       ;; A minor tic for each quarter of the year.
                       :minor-tics-per-major-tic 4
                       
                       ;; Draw each major label AFTER its tic
                       ;; mark by half of a major increment.
                       ;; This draws the year in the middle
                       ;; of the set of values for the year.
                       :draw-major-labels :after
                       
                       :draw-minor-labels t
                       :draw-major-grid-lines t
                       :draw-minor-grid-lines t
                       
                       ;; You wouldn't normally spell out each year
                       ;; in English words, but we do so here to
                       ;; illustrate how a major tic label can
                       ;; span across multiple minor tic labels.
                       :major-label-wrapping nil
                       :on-print-major-label
                       (lambda (month-and-year)
                         (format nil "~:(~r~)"
                           (second month-and-year)))

                       :on-print-minor-label
                       (lambda (month-and-year)
                         (format nil "~:(~a~)"
                           (first month-and-year)))
                       )
          :value-axis (make-instance 'value-axis
                        :axis-label "Average Scores"
                        :margin-inside-axis-label 8
                        :major-tic-increment 300)
          :line-graph-view (make-instance 'line-graph-view
                             :values-are-stacked nil
                             :draw-icons nil)
          :chart-legend (make-instance 'chart-legend
                          :on-print-chart-object
                          (lambda (value)
                            (format nil "~:(~a~)" value)))
          :right-attachment :right
          :bottom-attachment :bottom
          :left 0 :top 0
          :width width :height height))
       (dialog (make-window :chart-tutorial
                 :class 'dialog
                 :title "Sample Chart"
                 :scrollbars nil
                 :interior (make-box-relative 0 40 width height)
                 :dialog-items (list chart-widget))))
  (dotimes (item-index 36)
    (dotimes (object-index 6)
      (set-chart-value
       chart-widget
       :item-id (list (aref #(january february march april may
                                      june july august september
                                      october november december)
                            (mod item-index 12))
                      (+ 2004 (floor item-index 12)))
       :object-id (aref object-ids object-index)
       :value (max 100
                   (min 300
                        (+ (if (plusp item-index)
                               (chart-value
                                chart-widget
                                :item-index (1- item-index)
                                :object-index object-index)
                             (+ 100 (random 150)))
                           (random 21)
                           -10))))))
  dialog)

3.19 Using Two Value Axes

And, finally, an example to show the option for two value axes. Many charts use two value axes with different ranges (and often different units) to plot two lines close together to see how well they correlate. This can be done with the chart-widget by supplying a chart-axis object for the value-axis-2 property, along with the usual one for the value-axis property. The two axes work exactly the same way.

The key value here is the chart-objects property, which specifies that the :temperature value goes with the value-axis number 2. (Any other value means value axis number 1.)

          :chart-objects #((:id :doris)
                           (:id :tal)
                           (:id :temperature :value-axis 2))

So let's see how room temperature affects bowling performance. This code demonstrates a few different ways to tell the viewer which graph lines go with each axis. These include color matching as well as textual notes in the axis labels and legend.

(let* ((width 400)
       (height 400)
       (data #(
               ((2005 sep 12) :doris 164 :tal 156 :temperature 72)
               ((2006 jan 31) :doris 155 :tal 162 :temperature 68)
               ((2006 feb 18) :doris 150 :tal 173 :temperature 66)
               ((2006 aug 18) :doris 172 :tal 148 :temperature 79)
               ((2006 sep 12) :doris 168 :tal 152 :temperature 77)
               ))
       (value-axis-2 (make-instance 'value-axis
                       :axis-label "Temperature (red dashes)"
                       :draw-minor-grid-lines nil
                       :draw-minor-labels nil
                       :axis-width 2
                       :axis-color red
                       :major-grid-line-color
                       (make-rgb :red 255 :green 196 :blue 196)
                       :major-label-color dark-red))
       (chart-widget
        (make-instance 'chart-widget
          :title "Bowling Scores and Room Temperature"
          :footnote (format nil "Doris bowls better when it's warmer, ~
                        while Tal bowls betters when it's cooler.")
          :chart-view :line
          :chart-objects #((:id :doris)
                           (:id :tal)
                           (:id :temperature :value-axis 2))
          :item-axis (make-instance 'item-axis
                       :on-print-major-label
                       (lambda (value)
                         (format nil "~:(~a~) ~a ~a"
                           (second value)(third value)
                           (first value))))
          :value-axis (make-instance 'value-axis
                        :axis-label "Scores"
                        :axis-width 2
                        :axis-color blue
                        :major-label-color dark-blue
                        :minor-label-color dark-blue)
          :value-axis-2 value-axis-2
          :line-graph-view (make-instance 'line-graph-view
                             :line-dashings
                             '(:solid :solid :dash)
                             :line-colors
                             '(blue dark-green red))
          :fit-chart-items t
          :chart-items-max-index (1- (length data))
          :chart-value-returner
          (lambda (chart-widget value-type item-index
                                object-index object-id)
            (declare (ignore chart-widget object-index))
            (let* ((item-entry (elt data item-index)))
              (case value-type
                (:id (first item-entry))
                (:value (getf (rest item-entry) object-id)))))
          
          ;; The legend's print function indicates which
          ;; value goes with each axis.
          :chart-legend
          (make-instance 'chart-legend
            :on-print-chart-object
            (lambda (value)
              (format nil "~:(~a~) (~a axis)"
                value (case value
                        (:temperature "right")
                        (t "left")))))
          
          :right-attachment :right
          :bottom-attachment :bottom
          :left 0 :top 0
          :width width :height height))
       (dialog (make-window :bowling-and-temperature
                 :class 'dialog
                 :title "Bowling Scores and Temperature"
                 :scrollbars nil
                 :interior (make-box-relative 0 80 width height)
                 :dialog-items (list chart-widget))))
  
  ;; A final animation:
  ;; Reverse the temperature axis several times to show
  ;; correlations in each orientation.
  (dotimes (j 4)
    (sleep 2)
    (setf (invert-axis value-axis-2) t)
    (sleep 2)
    (setf (invert-axis value-axis-2) nil))
  
  dialog)

As the chart clearly shows, Doris bowls better when it's hotter and Tal bowls better when it's colder. (You might show this chart to Doris and Tal and watch them fight over the thermostat.)

That's the end of the tutorial. You may want to inspect some charts and play with the properties that weren't covered here. Happy charting!


Copyright (c) 1998-2009, Franz Inc. Oakland, CA., USA. All rights reserved.
Documentation for Allegro CL version 8.1. This page is new in the 8.1 release.
Created 2007.4.30.

ToCDocOverviewCGDocRelNotesFAQIndexPermutedIndex
Allegro CL version 8.1
This page is new in 8.1.