-
-
Notifications
You must be signed in to change notification settings - Fork 30
Renderer2D Notes
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))
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.
(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 theplot
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 thebounds-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 toplot
, 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 thesin
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.
"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.
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.
;; 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.