Recently I have moaned about not really knowing what I was doing with the grid package (see here and here). I’m happy to say, not only did I take the time to better understand the grid package, I also wrote my own package around it - the kandinsky package! To generate random Wassily Kandinsky paintings or even make any dataset into one. You’re probably wondering what on earth am I talking about. If you’re not interested in my ramblings just go to the package itself.

A Kandinsky Painting

So this is Kandinsky’s Composition VIII:

Kandinsky was a Russian abstract painter, known for using colorful geometrical shapes in his paintings. Geometrical shapes are relatively easy to make in R, with the grid package, on our way to generating completely random paintings. We just need to specify which shapes exactly are we after. I can describe a few when looking into Kandinsky’s paintings:

  • Rectangles
  • Circles
  • Triangles
  • Archs
  • Waves
  • Criss Cross or “Grills”

If we can draw each of these in grid, using various angles and colors, we already have something worth looking at.

Rectangles

To get a blank canvas in grid we use the grid.newpage() function:

library(grid)
grid.newpage()

This canvas has coordinate axes x and y ranging by default from 0 to 1, starting from the bottom-left corner (x = 0, y = 0), to the top-right corner (x = 1, y = 1).

And a rectangle is as simple as calling grid.rect with the proper parameters: x for the rectangle’s center x-location, y for the rectangle’s center y-location, width for the 0 to 1 width of the rectangle and height for the rectangle’s height1.

grid.rect(x = 0.4, y = 0.6, width = 0.5, height = 0.4,
          gp = gpar(col = "red", lwd = 2))

Notice I also specified some well-known base R graphical parameters via the gp parameter and gpar function, i.e. making the rectangle’s outline red, with a line width of 2.

Now how about a tilted rectangle?

Unfortunately there is no magical angle parameter in grid.rect, that’s where the concept of a viewport comes in. A viewport allows you to define a “sub-canvas” within the current canvas, and draw whatever you like in that sub-canvas. A viewport does have an angle parameter, so drawing a regular rectangle with grid.rect inside a viewport with angle = 45 results in a 45 degrees tilted rectangle.

You can do it by “pushing” the new viewport with pushViewport, drawing the rectangle and returning to the current canvas with upViewport:

vp1 <- viewport(x = 0.5, y = 0.6, width = 0.8, height = 0.7, angle = 45)
pushViewport(vp1)
grid.rect(x = 0.4, y = 0.6, width = 0.5, height = 0.4,
          gp = gpar(col = "red", lwd = 2, fill = "yellow"))
upViewport()

In this case it is probably more convenient to enter the viewport vp1 as a parameter into grid.rect:

vp1 <- viewport(x = 0.5, y = 0.6, width = 0.8, height = 0.7, angle = 45)
grid.rect(x = 0.4, y = 0.6, width = 0.5, height = 0.4,
          gp = gpar(col = "red", lwd = 2, fill = "yellow"), vp = vp1)

Circles

Circles are simple with grid.circle:

grid.circle(x = 0.8, y = 0.5, r = 0.3,
                gp = gpar(lwd = 10, col = "blue", fill = "lightblue"))

Triangles

Triangles are simple with grid.polygon:

grid.polygon(x = c(0.1, 0.7, 0.8), y = c(0.2, 0.5, 0.8),
                 gp = gpar(col = NA, fill = "pink"))

Archs

Archs are simple with grid.curve:

grid.curve(x1 = 0.1, y1 = 0.7, x2 = 0.6, y2 = 0.2,
               curvature = 0.15, square = FALSE, ncp = 30,
               gp = gpar(lwd = 10, col = "navyblue"))

Waves

Ah. There’s no grid function that can take a formula such as sin(x)/x which represents a nice wave and plot it. At least I couldn’t find one. I took inspiration from this great article, to define the gCurve function which wraps grid.lines:

gCurve <- function (expr, from, to, n = 101,
                    gp = gpar(),
                    default.units = "npc", vp = NULL,
                    name = NULL, draw = TRUE, xname = "x", ...) 
{
  sexpr <- substitute(expr)
  if (is.name(sexpr)) {
    expr <- call(as.character(sexpr), as.name(xname))
  }
  else {
    if (!((is.call(sexpr) || is.expression(sexpr)) && xname %in% 
          all.vars(sexpr))) 
      stop(gettextf("'expr' must be a function, or a call or an expression containing '%s'", 
                    xname), domain = NA)
    expr <- sexpr
  }
  x <- seq.int(from, to, length.out = n)
  ll <- list(x = x)
  y <- eval(expr, envir = ll, enclos = parent.frame())
  if (length(y) != length(x)) 
    stop("'expr' did not evaluate to an object of length 'n'")
  x <- (x - min(x, na.rm = T))/(max(x, na.rm = T) - min(x, na.rm = T))
  y <- (y - min(y, na.rm = T))/(max(y, na.rm = T) - min(y, na.rm = T))
  grid.lines(x = x, y = y, default.units = default.units, gp = gp, vp = vp, ...)
  invisible(list(x = x, y = y))
}

The usage:

gCurve(sin(x)/x, from = 0.1, to = 20, gp = gpar(lty = 2))

And of course you can use a viewport to tilt the wave, stretch it etc.

Criss Cross

Again, no ready-made function, this is DIY:

gCrissCross <- function(n = 5, gp = gpar(), vp = NULL, ...) {
  x0 <- runif(1, 0, 0.1)
  ccGap <- runif(1, 0.3, 0.7)
  y0 <- runif(1, 0, 0.5)
  for (i in 1:n) {
    x0 <- x0 + runif(1, 0.1, 0.2)
    y0 <- y0 + runif(1, -0.05, 0.05)
    y1 <- y0 + ccGap + runif(1, -0.05, 0.05)
    grid.segments(x0, y0, x0 + ccGap, y1, vp = vp, gp = gp, ...)
    grid.segments(x0, y1, x0 + ccGap, y0, vp = vp, gp = gp, ...)
  }
}

The usage:

gCrissCross()

Random Kandinsky

Putting some random rectangles, circles, triangles, archs, waves and grills - we get a really nice “Random Kandinsky”:

randomKandinsky <- function(n = 10) {
  grid.newpage()
  
  grid.rect(gp=gpar(fill=rgb(runif(1),
                             runif(1),
                             runif(1),
                             runif(1))))
  
  for (i in 1:n) {
    grid.rect(x = runif(1), y = runif(1), width = runif(1), height = runif(1),
              gp = gpar(col = NA,
                        fill=rgb(runif(1),
                                 runif(1),
                                 runif(1),
                                 runif(1))))
    grid.circle(x = runif(1), y = runif(1), r = runif(1),
                gp = gpar(
                  lwd = runif(1, 0, 100),
                  col = rgb(runif(1),
                            runif(1),
                            runif(1),
                            runif(1)),
                  fill=rgb(runif(1),
                           runif(1),
                           runif(1),
                           runif(1))))
    grid.polygon(x = runif(3), y = runif(3),
                 gp = gpar(col = NA,
                           fill=rgb(runif(1),
                                    runif(1),
                                    runif(1),
                                    runif(1))))
    
    grid.curve(runif(1), runif(1), runif(1), runif(1),
               curvature = runif(1, -1, 1), square = FALSE, ncp = sample(100, 1),
               gp = gpar(lwd = runif(1, 0, 10),
                         col = rgb(runif(1),
                                   runif(1),
                                   runif(1),
                                   1)))
    
    vp1 <- viewport(x = runif(1), y = runif(1), width = runif(1), height = runif(1), angle = runif(1) * 360)
    grid.rect(x = runif(1), y = runif(1), width = runif(1), height = runif(1),
              vp = vp1,
              gp = gpar(col = NA,
                        fill=rgb(runif(1),
                                 runif(1),
                                 runif(1),
                                 runif(1))))
    
    vp2 <- viewport(x = runif(1), y = runif(1), width = runif(1), height = runif(1), angle = runif(1) * 360)
    gCurve(sin(x)/(x), sample(5, 1), sample(10:50, 1), vp = vp2,
           gp = gpar(lwd = runif(1, 0, 10),
                     col = rgb(runif(1),
                               runif(1),
                               runif(1),
                               1)))
  }
  vp3 <- viewport(x = runif(1), y = runif(1), width = runif(1), height = runif(1), clip = "off")
  gCrissCross(vp = vp3,
              gp = gpar(lwd = runif(1, 0, 10),
                        col = rgb(runif(1),
                                  runif(1),
                                  runif(1),
                                  1)))
  
}

randomKandinsky(10)

And another one:

randomKandinsky(50)

And another one:

randomKandinsky(100)

Paint with Data

A random Kandinsky is nice. But what about turning a dataset, any dataset, into a Kandinsky painting?

If you look closely at the randomKandinsky function, you’ll see it narrows down to inputting a bunch of \(U(0, 1)\) random numbers into all of the functions we’ve seen above. What if we could turn a dataset into these 0 to 1 numbers? We’d have the input we desire to draw a Kandinsky!

One way to do this normalization is by a simple formula: given any numeric \(x\) with \(max(x) > min(x)\), the result of the transformation \(\frac{x - min(x)}{max(x) - min(x)}\) will be bounded beween 0 and 1. So we could do this to each and every numeric column of our dataset. Keeping in mind we could also have NA values, character and factor columns, we get to this function:

zeroOneNormalize <- function(x) {
  if (!is.numeric(x)) {
    if (is.factor(x)) {
      x <- as.numeric(unclass(x))
    } else {
      x <- as.numeric(unclass(as.factor(x)))
    }
  }
  if (length(unique(x)) == 1) {
    return(rep(0.5, length(x)))
  } else {
    x <- (x - min(x, na.rm = TRUE)) /
      (max(x, na.rm = TRUE) - min(x, na.rm = TRUE))
    x[is.na(x)] <- 0.5
    return(x)
  }
}

I won’t bore you with the final code for all the utility sub-functions, you can see them at the source. I’ll walk you through the final kandinsky function:

kandinsky <- function(df = NULL, rv = runif(1000)) {
  
  library(grid)
  library(purrr)
  
  if (!is.null(df)) {
    rv <- normalizeAndVectorize(df)
  }
  
  grid.newpage()
  
  i <<- 0
  
  grid.rect(gp = gpar(fill = rgb(nex(rv), nex(rv), nex(rv), nex(rv))))
  
  nRectangles <- floor(nex(rv) * 10) + 3
  nCircles <- floor(nex(rv) * 10) + 3
  nTriangles <- floor(nex(rv) * 10) + 3
  nArchs <- floor(nex(rv) * 10) + 3
  nTiltedRectangles <- floor(nex(rv) * 10) + 3
  nWaves <- floor(nex(rv) * 10) + 3
  nCrissCross <- floor(nex(rv) * 3) + 1
  
  walk(1:nRectangles, drawRectangle, rv)
  walk(1:nCircles, drawCircle, rv)
  walk(1:nTriangles, drawTriangle, rv)
  walk(1:nArchs, drawArch, rv)
  walk(1:nTiltedRectangles, drawTiltedRectangle, rv)
  walk(1:nWaves, drawWave, rv)
  walk(1:nCrissCross, drawCrissCross, rv)
}

We first normalize and vectorize the dataset into the rv numeric vector in the 0 to 1 range. We then set a random background color to our canvas with grid.rect. We then set the number of rectangles, circles, etc. Notice we’re always using the numbers in rv, meaning that we always use the dataset for any random decision. So in a sense, nothing is random, and we’ll always get the same painting for a given dataset. Finally we use walk from the purrr package to draw all shapes.

So this is the mtcars dataset as a Kandinsky painting:

library(kandinsky)

kandinsky(mtcars)

The iris dataset (note this dataset has factor columns):

kandinsky(iris)

The airquality dataset:

kandinsky(airquality)

The USArrests dataset (note this dataset has NA values):

kandinsky(USArrests)

The fdeaths dataset (note this dataset is a ts Time Series object):

kandinsky(fdeaths)
## Warning in bind_rows_(x, .id): Vectorizing 'ts' elements may not preserve
## their attributes

And finally if you want a random Kandinsky painting, you can just leave the df parameter blank and/or supply your own 0 to 1 vector for the rv parameter:

kandinsky()

Finish Your Painting

In my old highschool there were Kandinsky replicas hanging everywhere in the hallways. I hated highschool. I loved those Kandinskys though. To call the above paintings “Kandinskys” is a bit presumptuous, I know. But wasn’t that fun? Go ahead, install the package, paint your data, tell me if and when it breaks.


  1. If you’re thinking these parameters are pretty self-explanatory you’re not wrong…