Objectives

  • Extend your knowledge of gradient coloring to iterative segments
  • Manipulate data via transposition
  • Add optional arguments to a function
  • Control the user with a conditional return case

Data

You’re going to use the same data set that you used for the first peer code review session last week:

load("/shared/groups/jrole001/pals0047/data/EMA-EMG_data2.Rda")

It last week’s tutorial, we created an EMA plotting function that represented time as a color gradient:

t1 <- round(data$segments$start[1]*data$SR$EMA)
t2 <- round(data$segments$end[4]*data$SR$EMA)

gradientEMA <- function(x,y,from=c(1,0,0),to=c(0,0,1),pch=1) {
  
  N <- length(x)
  
  mycols <- cbind(seq(from[1],to[1],length.out=N), 
                  seq(from[2],to[2],length.out=N),
                  seq(from[3],to[3],length.out=N))
  
  plot(x,y,col=rgb(mycols),pch=pch)
}

gradientEMA(data$EMA$TT_x[t1:t2], data$EMA$TT_z[t1:t2], from=c(1,0,1), to=c(0,1,0), pch=19)

N <- length(data$EMA$TT_x[t1:t2])

mycols <- cbind(seq(1,0,length.out=N), 
                seq(0,1,length.out=N),
                seq(1,0,length.out=N))

Although this was a really nice way to represent 3 dimensions on a single 2-dimensional plot, the trajectory of the tongue tip movement is a bit difficult to see here because the dots aren’t connected to each other. What if we want to use a single line of gradient colors instead of separate dots?

We can plot a connected line using the base R plot function:

plot(data$EMA$TT_x[t1:t2], data$EMA$TT_z[t1:t2], type='l')

However, if we try to add our color gradient vector, the first color is used for all segments of the line:

plot(data$EMA$TT_x[t1:t2], data$EMA$TT_z[t1:t2], type='l', col=rgb(mycols))

R does not have a built-in function to be able to do this… so you are going to create one!

Exercises

Excercise #1: gradient line segments

To make your function, you will be using the segments() function, which plots a line segment from one set of coordinates to another; run ? segments to read the description. The key arguments you will need to understand are x0 and y0, which are the starting coordinates, and x1 and y1, which are the ending coordinates. Also, just like the functions points() and lines(), the function segments() can only plot onto an existing figure.

The basic order of operations should be similar to the following:

  1. Create a blank plot, which the segments will be added onto. (hint: use the argument type='n' to set the axes limits to the data, but without plotting anything)

  2. Create a matrix of color gradients (hint: you did this last week!)

  3. Loop through the data points (hint: the iteration range here is a bit tricky!)

  4. At each iteration of the loop, use segments() to plot from one data point to the next, using the corresponding color from the matrix you created

Before starting

Before you begin, there is one issue you need to be aware of, which appears in this portion of the help documentation for the function rgb():

“The colors may be specified by passing a matrix or data frame as argument red, and leaving blue and green missing. In this case the first three columns of red are taken to be the red, green and blue values.”

What this means is that, although you can use an entire matrix of RGB values (like you did last week), if you try to use a single vector of RGB values from the matrix you will get the following error:

rgb(mycols[1,])
## Error in rgb(mycols[1, ]): argument "green" is missing, with no default

Why? Because this vector is not a matrix or a data frame:

class(mycols[1,])
## [1] "numeric"

You can convert the vector to a 3-element matrix, but then you will get a new error:

rgb(as.matrix(mycols[1,]))
## Error in rgb(as.matrix(mycols[1, ])): at least 3 columns needed

So what’s the problem now? The issue is that the as.matrix() function creates a 3x1 matrix from the 3-element vector, but the rgb() function requires a 1x3 matrix.

as.matrix(mycols[1,])
##      [,1]
## [1,]    1
## [2,]    0
## [3,]    1

The solution is to transpose the matrix using the t() function:

t(as.matrix(mycols[1,]))
##      [,1] [,2] [,3]
## [1,]    1    0    1

Transposing the data into a 1x3 matrix will now allow you to generate a hex color code with the rgb() function:

rgb(t(as.matrix(mycols[1,])))
## [1] "#FF00FF"

In the end, your function should be able to produce the following plot:

plot_linecols <- function(xx,yy,from=c(1,0,1), to=c(0,1,0)) {
  plot(xx,yy,type="n")
  
  N <- length(xx)
  
  mycols <- cbind(seq(from[1],to[1],length.out=N), 
                  seq(from[2],to[2],length.out=N),
                  seq(from[3],to[3],length.out=N))
  
  for (obs in 1:(N-1)) {
    segments(xx[obs],yy[obs],xx[obs+1],yy[obs+1],
             col=rgb(t(as.matrix(mycols[obs,]))))
  }
}
t1 <- round(data$segments$start[1]*data$SR$EMA)
t2 <- round(data$segments$end[4]*data$SR$EMA)


plot_linecols(data$EMA$TT_x[t1:t2],
              data$EMA$TT_z[t1:t2],
              from=c(1,0,1), 
              to=c(0,1,0)
)

Exercise #2: adding optional arguments

The advantage of the line plot you just made is that you can now easily follow the trajectory of the tongue tip movement. However, an advantage of the previous dot plot was that you could easily see how quickly the tongue tip was moving at any given point in time based on how far apart one dot was from the next. Ideally, we want a plot that will allow us to do both.

For this second exercise, your goal is to add two optional arguments and a return condition:

  1. Add an argument for the lwd plotting parameter, with a default value of 1. If the function is run with any other value given for this argument, the value should be passed to the segments() function.

  2. Add an argument called dots that has a default NULL value. If a number in the range 0-25 is given, the value should be passed as the pch argument (see examples here) to a points() function to change the plotting shape. This points() function should only executed if a value is provided for the dots argument.

  3. Break the function and don’t plot anything if the value for dots falls outside the range 0-25

plot_linecols <- function(xx, yy, from=c(1,0,1), to=c(0,1,0), lwd=1, dots=NULL) {
  
  if(!(is.null(dots))){
    if (dots < 0 | dots > 25){
      return("The value for 'dots' must be in the range 0-25!")
    } 
  }
  
  plot(xx,yy,type="n")
  
  N <- length(xx)
  
  mycols <- cbind(seq(from[1],to[1],length.out=N), 
                  seq(from[2],to[2],length.out=N),
                  seq(from[3],to[3],length.out=N))
  
  if(!(is.null(dots))){
    points(xx,yy,col=rgb(mycols),pch=dots)
  }
  
  for (obs in 1:(N-1)) {
    segments(xx[obs],yy[obs],xx[obs+1],yy[obs+1],col=rgb(t(as.matrix(mycols[obs,]))),lwd=lwd)
  }
}

In the end, your new function should produce the following plot if it is run without specifying values for the arguments lwd and dots (hint: this should be the same plot from Exercise #1):

plot_linecols(data$EMA$TT_x[t1:t2],
              data$EMA$TT_z[t1:t2],
              from=c(1,0,1), 
              to=c(0,1,0))

But it should also produce the following plot if it is run with, for example, a value of 3 for lwd and 19 for dots:

plot_linecols(data$EMA$TT_x[t1:t2],
              data$EMA$TT_z[t1:t2],
              from=c(1,0,1), 
              to=c(0,1,0),
              lwd=3,
              dots=19)

And if the function is run with a dots value that is outside of the range 0-25, nothing should be plotted and the following message should be printed instead:

plot_linecols(data$EMA$TT_x[t1:t2],
              data$EMA$TT_z[t1:t2],
              from=c(1,0,1), 
              to=c(0,1,0),
              lwd=3,
              dots=30)
## [1] "The value for 'dots' must be in the range 0-25!"