This is the best I can do using passing arguments:
foo.upper <- function(x,y,ind.upper,col.upper,ind.lower,col.lower,...){ points(x[ind.upper],y[ind.upper],col = col.upper,...) } foo.lower <- function(x,y,ind.lower,col.lower,ind.upper,col.upper,...){ points(x[ind.lower],y[ind.lower],col = col.lower,...) } pairs(dat[,-5], lower.panel = foo.lower, upper.panel = foo.upper, ind.upper = dat$type == 'brain', ind.lower = dat$type == 'heart', col.upper = 'blue', col.lower = 'red')
Note that each panel requires all arguments. ...
is a cruel lover. If you include only specific panel arguments in each function, it works, but you get many, many warnings from R trying to pass these arguments to regular build functions and obviously they won't.
This was my first attempt, but it seems ugly:
dat <- as.data.frame(do.call(rbind,data)) dat$type <- rep(c('brain','heart'),each = 100) foo.upper <- function(x,y,...){ points(x[dat$type == 'brain'],y[dat$type == 'brain'],col = 'red',...) } foo.lower <- function(x,y,...){ points(x[dat$type == 'heart'],y[dat$type == 'heart'],col = 'blue',...) } pairs(dat[,-5],lower.panel = foo.lower,upper.panel = foo.upper)

I am abusing R, considering the somewhat ugly path here in this second version. (Of course, you could do it more cleanly in a grid , but you probably knew that.)
The only other option I can think of is to create my own matrix of scattering matrices using layout
, but this is probably quite a bit of work.
Lattice Editing
Here, at least, is the beginning of a lattice solution. It should handle different ranges of x, y axes better, but I have not tested this.
dat <- do.call(rbind,data) dat <- as.data.frame(dat) dat$grp <- rep(letters[1:2],each = 100) plower <- function(x,y,grp,...){ panel.xyplot(x[grp == 'a'],y[grp == 'a'],col = 'red',...) } pupper <- function(x,y,grp,...){ panel.xyplot(x[grp == 'b'],y[grp == 'b'],...) } splom(~dat[,1:4], data = dat, lower.panel = plower, upper.panel = pupper, grp = dat$grp)