In R, the

persp() is a built-in function to

create surface plots. The basic usage is straightforward: create a matrix of values

# plot a 10x10 matrix of random values in the range -100..100:
persp( matrix(runif(100, min=-100, max=100), nrow=10, ncol=10) )
All well and good, until it's time to prepare the plots for presentation -- and suddenly it becomes apparent that plots created with

persp() do not work well with

axis(),

text(),

mtext(),

par(), and other standard graphics device functions.

The

trans3d documentation refers the reader to the

persp documentation for examples; those examples are too convoluted to serve any useful educational purpose. A quick note to documentation writers: always include an example showing the simplest possible use of your function on trivial data sets (usually the array of integers from 1 to 10, or sin(x) if a function is required). Do not use only edge cases and exciting demos as examples.

The discussion that follows will demonstrate how to construct a perspective plot with custom labels using

persp() and

trans3d(). The data to be plotted is a 10x10 matrix of values in the range

-100:100.

The first thing to understand is that

persp() does not just draw a plot; it also returns a

*perspective matrix* (or

pmat) which can be used to translate 3-dimensional coordinates to the 2-dimensional coordinate system used in the image of the plot.

The function that performs this translation is

trans3d(). Its arguments are the

**x**,

**y**, and

**z** coordinates to be translated, followed by the

pmat. The return value is a list with two elements:

x and

y, the two-dimensional coordinates in the image.

If one of the

**x**,

**y**, and

**z** arguments is a vector, then the vector is considered to be a line at the other two coordinates. Thus, a line along the X axis from (0, 10, 10) to (10, 10, 10) would be translated using

trans3d(0:10, 10, 10, pmat); a line along the Y axis from (0, 3, 10) to (0, 7, 10) would be translated using

trans3d(0, 3:7, 10, pmat).

Enough background; time for an example.

**Basic Perspective Plot**
First, some definitions of the data ranges to keep things clear:

x.axis <- 1:10
min.x <- 1
max.x <- 10
y.axis <- 1:10
min.y <- 1
max.y <- 10
z.axis <- seq(-100, 100, by=25)
min.z <- -100
max.z <- 100
Pay particular attention to

z.axis: in addition to specifying the

*range* of each axis, the

*.axis variables also specify the

*tick marks* of each axis.

Next, a draw the initial perspective plot, saving the

pmat:

pmat <- persp( x=x.axis, y=y.axis,
matrix(runif(100, min=-100, max=100), nrow=10, ncol=10),
xlab='', ylab='', zlab='',
ticktype='detailed', box=FALSE, axes=FALSE,
mar=c(10, 1, 0, 2), expand=0.25,
col='green', shade=0.25, theta=40, phi=30 )
Note the

theta (rotation along the vertical axis) and

phi (rotation along the horizontal axis) parameters. It is useful to play with these a bit, as different data sets will require different viewing angles. The

r ("eyepoint distance") and

d ("perspective strength") parameters provide further control of the view. Note also that

box and

axes parameters are

FALSE: we will be drawing our own axes.

**Drawing the Axes**
In this plot, the X axis will be drawn at

min.y and

min.z (left side of Y, bottom of Z), Y at

max.x and

min.z (right side of X, bottom of Z), and Z at

min.x and

min.y (left side of X, left side of Y).

These parameters are passed to

trans3d() to calculate the coordinates of a line at each axis, as described previously. The translated coordinates can be passed directly to

lines().

lines(trans3d(x.axis, min.y, min.z, pmat) , col="black")
lines(trans3d(max.x, y.axis, min.z, pmat) , col="black")
lines(trans3d(min.x, min.y, z.axis, pmat) , col="black")
**Drawing Tick Marks**
Adding tick marks requires calculating the position of a

*second* line, parallel to the axis, and using

segments() to draw ticks that span the distance between the axis and the second line. The basic procedure is as follows:

tick.start <- trans3d(x.axis, min.y, min.z, pmat)
tick.end <- trans3d(x.axis, (min.y - 0.20), min.z, pmat)
segments(tick.start$x, tick.start$y, tick.end$x, tick.end$y)
Note the

(min.y - 0.20) in the calculation of tick.end. This places the second line, parallel to the X axis, at the position -0.20 on the Y axis (i.e., into negative/unplotted space).

The tick marks on the Y and Z axes can be handled similarly:

tick.start <- trans3d(max.x, y.axis, min.z, pmat)

tick.end <- trans3d(max.x + 0.20, y.axis, min.z, pmat)

segments(tick.start$x, tick.start$y, tick.end$x, tick.end$y)

tick.start <- trans3d(min.x, min.y, z.axis, pmat)

tick.end <- trans3d(min.x, (min.y - 0.20), z.axis, pmat)

segments(tick.start$x, tick.start$y, tick.end$x, tick.end$y)

**Adding Tick Mark Labels**

The final step is to label the ticks on each axis. Once again, the procedure is to calculate the position of a line, parallel to the axis, at the position where the labels are to be displayed:

labels <- c('first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth')

label.pos <- trans3d(x.axis, (min.y - 0.25), min.z, pmat)

text(label.pos$x, label.pos$y, labels=labels, adj=c(0, NA), srt=270, cex=0.5)

The adj=c(0, NA) expression is used to left-justify the labels, the srt=270 expression is used to rotate the labels 270°, and the cex=0.5 expression is used to scale the label text to 75% of its original size.

The labels on the Y and Z axes are produced similarly:

labels <- c('alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', 'iota', 'kappa')
label.pos <- trans3d((max.x + 0.25), y.axis, min.z, pmat)
text(label.pos$x, label.pos$y, labels=labels, adj=c(0, NA), cex=0.5)

labels <- as.character(z.axis)
label.pos <- trans3d(min.x, (min.y - 0.5), z.axis, pmat)
text(label.pos$x, label.pos$y, labels=labels, adj=c(1, NA), cex=0.5)
Note that the Y and Z axis tick labels do not need to be rotated.

**The Final Product**