Skip to content
Alex Harsányi edited this page Jan 20, 2022 · 1 revision

There are many examples of renderers in the plot code base, but since the various parts are in separate files, it is difficult to see the "big picture" just by reading the code.

This is a companion document, which can be used as a "quick reference" while reading the plot source code, to assist in understanding what the plot package expects from a renderer. Only 2D plot renderers are considered here, although most things also apply to 3D renderers.

The intended audience is developers who want to write new renderers, that is new "things" that are to be plotted. It is not a "self contained" document, some familiarity with the plot source code is required.


Plot renderers, such as lines, points, function, and others, will process the input data and produce a renderer2d structure instance which is used by the rest of the plot package for layout and rendering. The renderer2d structure sits at the border between the "high level" plotting functions and the "low level" layout and rendering functions of the plot package.

The renderer2d struct instance is used by the plot package to:

  • determine the plot bounds (X and Y axis ranges for the plot)
  • determine what ticks (notches on the X and Y axes) to display and what labels to put next to them
  • determine how to construct a legend entry for this renderer
  • actually render the element of the plot.

The renderer2d structure is defined as follows:

;; defined in plot/plot-lib/plot/private/plot2d/renderer.rkt
(struct renderer2d plot-element 
  ([label :       (U #f (-> Rect (Treeof legend-entry)))]
   [render-proc : (U #f 2D-Render-Proc)])
  #:transparent)

;; defined in plot/plot-lib/plot/private/common/plot-element.rkt
(struct plot-element 
  ([bounds-rect : (U #f Rect)]
   [bounds-fun :  (U #f Bounds-Fun)]
   [ticks-fun :   (U #f Ticks-Fun)])
  #:transparent)

Except for bounds-rect all structure slots are functions and all the slots, including bounds-rect, are optional, i.e. they can be set to #f.

There is also convenience value, empty-renderer2d defined for functions which need to return a renderer, but want to return "nothing":

;; defined in plot/plot-lib/plot/private/plot2d/renderer.rkt
(define empty-renderer2d (renderer2d #f #f #f #f #f))

Rectangles and Intervals (Rect objects)

Most of the functions in a renderer2d work with rectangles, which are used to define the bounds of the plot. The Rect type is defined as a vector of intervals, which are ivl structures. For 2D plot, the Rect instance contains 2 elements, for the intervals on the X and Y axis, while for 3D plots it contains 3 elements, for the intervals on the X, Y and Z axes.

;; plot/plot-lib/plot/private/common/math.rkt
(deftype Rect (Vectorof ivl)) ;; can be 2 or 3 element vector
(struct ivl ([min : (U Real #f)] [max : (U Real #f)]) #:transparent)

A Rect object can have members that are #f, which means that it allows for partially specified rectangles. For example a "bounds" value of (vector (ivl 0 10) (ivl #f #f)) represents plot bounds where the X axis bounds are known, they are between 0 and 10, while the Y axis bounds are not.

Plot Bounds (bounds-rect and bounds-fun structure slots)

(deftype Bounds-Fun (-> Rect Rect))

The bounds-rect rectangle and bounds-fun function are used by plot to determine the plot bounds (X and Y axis range) for the entire plot. One or both of these slots can be omitted, however, each renderer should do its best to supply at least some partial values, since the plot package aggregates this information from all renderers to determine the actual plot bounds.

bounds-rect can be used to supply a, possibly partial, set of bounds that can be computed by the renderer itself based on the data it has to render. For example, the points renderer supplies a bounds-rect based on the minimum and maximum point coordinates supplied to the renderer.

The bounds-fun function can be used to refine a set of bounds received as an argument. As an example, the function renderer uses a bounds-fun which accepts a set of bounds as a parameter, looks at the X interval of these bounds and returns a new set of bounds with the Y interval changed to the min/max values of the function over the input X interval.

To be able to plot, the full bounds of the plot must be constructed, that is, a Rect structure without any #f fields. The algorithm for determining the plot bounds is as follows:

  • The initial plot bounds (B0) are constructed from the #:x-min, #:x-max, #:y-min, and #:y-max parameters to the plot call. Most of the time, these parameters are not specified by the user, so the bounds starts as (vector (ivl #f #f) (ivl #f #f)). These initial plot bounds are passed through each renderer.
  • The bounds B0 are passed to the bounds-fun, if present, and the result is a new set of bounds (B1)
  • This new set of bounds, B1, are joined using rect-join with the bounds-rect to obtain a new set of bounds (B2)
  • These bounds (B2) are passed into the next renderer
  • The process is repeated a few times, until either the bounds are completely specified or the call fails that insufficient information was supplied by the user.

Here is an example, consider the following plot:

(plot (function sin -5 5))

Here the function renderer supplies a bounds-rect of (vector (ivl -5 5) (vector #f #f)) (the Y range is not specified), and also supplies a bounds-fun function which calculates an Y interval from an input set of bounds. The plot bounds are determined as follows:

  • Since the user did not specify #:x-min, #:x-max, #:y-min and #:y-max parameters to plot, the initial bounds are (vector (ivl #f #f) (ivl #f #f))
  • The bounds-fun for the renderer is called with these bounds, and produces (vector (ivl #f #f) (ivl #f #f)), since the Y bounds for the sin function cannot be determined without the X bounds.
  • These bounds are than joined with the bounds-rect value from the renderer, producing a new set of bounds, (vector (ivl -5 5) (vector #f #f))
  • Since the plot bounds are not yet complete, the loop is repeated
  • The bounds-fun for the renderer is called with (vector (ivl -5 5) (vector #f #f)), and it can now determine that the Y bounds are -1 to 1 so it returns a new set of bounds (vector (ivl -5 5) (vector -1 1))
  • These bounds are joined with the bounds-rect value producing again (vector (ivl -5 5) (vector -1 1))
  • Since the bounds are now known, the plot can be rendered.

Axis Ticks (ticks-fun structure slot)

"Ticks" represent the markers on the X and Y axes of the plot. The plot library will draw a small notch at the location of each tick, and if the tick is a "major" tick, it will also display a label next to this notch. These markers are represented by tick structures and the plot package will use the ticks-fun function in all renderers to obtain these ticks, remove any duplicates, and display them.

(deftype Ticks-Fun (-> Rect (Listof (Listof tick))))

This function allows a renderer to place ticks on any of the X,Y near and far axes. The function receives the plot bounds rect as an argument and it can use it to determine which ticks and how many of them to construct.

The function must return a list of 4 elements, each element is a list of ticks, which can be empty, the 4 elements are:

  • a list of X "near" ticks -- these will be on the bottom axis
  • a list of X "far" ticks -- these will be placed on the top axis
  • a list of Y "near" ticks -- these will be placed on the left axis
  • a list of Y "far" ticks -- these will be placed on the far axis

Most of the time, you can just use default-ticks-fun for this slot, however, this function can also be used to add special marker ticks to axes. For an interesting example, see discrete-histogram-ticks-fun, which adds labels corresponding to the discrete histogram bars to the axes, effectively labeling each histogram bar.

NOTE A renderers tick function is the only way for the plot to place ticks on plot axes. Except for special non-renderers, you should use default-ticks-fun, instead of #f if your renderer does not need a "special" ticks function. If no renderer in a plot has a ticks function, the plot will have no ticks on its axis.

Legend Entries (label structure slot)

The plot legend is constructed from legend-entry structures, which is defined as follows:

;; defined in plot/plot-lib/plot/private/common/types.rkt
(struct legend-entry 
  ([label : (U String pict)]
   [draw : Legend-Draw-Proc])
  #:transparent)

(deftype Legend-Draw-Proc (-> (Instance Plot-Device%) Real Real Void))

The structure has a label and a draw function -- the draw function should draw the sample line, or any other indicator, with the same colors, width and style as the actual plot element. The plot package will render the labels and these samples separately, so plot legend entries can be correctly aligned. The drawing procedure should draw its sample in an area between 0,0 and width/height parameters passed to it (the plot device is already set up with translated origin and clipping). That is, the draw function should look something like this:

(define (sample-draw-proc plot-device width height)
  ;; Use the plot-device to draw a sample of the plot line
  ;; between 0,0 and width,height
  )

The label structure slot in the renderer2d struct is a function, which receives the plot bounds as a Rect and should return a tree (nested lists) of legend-entry structures. Most renderers ignore the plot bounds, so the label procedure looks something like:

(define (sample-label-proc _bounds)
  ;; ignore bounds, and construct and return `legend-entry` structures
  )

The plot package contains many legend-entry constructors, which can be reused by new renderers.

The bounds parameters to the label function are used by renderers whose colors, and therefore legend entry, depend on the plot bounds being rendered, for an example of this use, see the contours renderer.

Drawing the plot element (render-proc structure slot)

;; defined in plot/plot-lib/plot/private/plot2d/renderer.rkt
(deftype 2D-Render-Proc (-> (Instance 2D-Plot-Area%) Void))

The render-proc is the actual procedure which draws the plot element onto a 2D-Plot-Area% using basic draw operations provided by this draw area. It receives a single argument, the plot area. This function is usually a closure over the other information it needs to draw the plot, and it should simply draw without other considerations for sizes and bounds, since clipping is already correctly set up based on other renderer arguments.