Manipulating colors with {prismatic}

I’m happy to announce my newest package prismatic which facilitates simple manipulations of colors. I had been working on this package online and offline for some time, but the promise of easy manipulation of mapped data in ggplot2 forced me to get some work done to get this package out before ggplot2 version 3.3.0. (as of time of writing.)

This post will go over some of the finer details with lots of pretty pictures!

Loading Packages

The prismatic package is fairly low dependency with only 1 import being farver for lightning fast conversion between color spaces. I have also loaded the colorspace package, from which some of the following functions have been inspired. I will use colorspace to enable plotting of multiple color palettes side by side, but I will not showcase the code each time. Go to the end of the post for example code for comparison plots.

library(prismatic)
library(colorspace) # for plotting functions
library(magrittr) # for the glorious pipe

Let me see the colors!!

If you have seen my work, you will properly know that I like colors alot! But being also to quickly inspect some colors have always been a little too much work. Now all you have to do it pass your colors to color() (or colour() for our friends across the pond) to get a object which has a nice plot() method

rainbow(10) %>% color() %>% plot()

hcl.colors(25) %>% color() %>% plot()

scico::scico(256, palette = "buda") %>% color() %>% plot()

Which I would like to think is one of the main features of the package. If you happens to have crayon available you will see a approximation of the colors with a filled in background (this limited to 256 colors so you milage might very, when in doubt use plot())

This is the extent of what the color object can do.

Manipulations

The second star of the package is the collection of functions to manipulate the colors. All these functions have a couple of things in common.

  • They all start with clr_ for easy auto completion in your favorite IDE.
  • They all take a vector of colors as the first argument and results a colors object of the same length.

these two facts make the function super pipe friendly.

Saturation

The two functions clr_saturate() and clr_desaturate() both modifies the saturation of a color. It takes a single additional argument to specifying the degree of which the (de)saturation should occur. These values should be between 0(nothing happens) and 1(full on power!).

notice how you don’t have to call color() on the output of clr_desaturate() as it already returns a colors object.

hcl.colors(10, "plasma") %>%
  clr_desaturate(0.8) %>%
  plot()

Example done with Mango palette from LaCroixColoR package.

Seeing life in black and white

Turns out there is a lot of different ways to turn colors into grayscale. Prismatic has implemented a handful of these. Notice how the viridis palette is still working once you have it transformed to black and white.

hcl.colors(10) %>%
  clr_greyscale() %>%
  plot()

Be advised that not all of these methods are meant to be perceptually uniform.

Negate

Negation of a color is pretty simple. it will just pick the opposite color in RGB space.

terrain.colors(10) %>%
  clr_negate() %>%
  plot()

Mixing

Mixing is just adding colors together. Thus my mixing a color with red would make a color more red.

rainbow(10) %>%
  clr_mix("red") %>%
  plot()

Rotation

the clr_rotate() function will take a color and rotate its hue, which is a way walk around the rainbow.

terrain.colors(10) %>%
  clr_rotate(90) %>%
  plot()

Color blindness

also includes 3 functions (clr_protan(), clr_deutan() and clr_tritan()) to simulate colorblindness. These functions has a severity argument to control the strength of the deficiency.

hcl.colors(10) %>%
  clr_deutan() %>%
  plot()

Light and darkness

Lastly we have functions to simulate lightness and darkness. This is surprisingly hard to do and no one way works great all the time. Please refer to the excellent colorspace paper for more information. These functions (clr_lighten() and clr_darken()) also include a space argument to determine the space in which to perform the transformation. Please try each of these to find the optimal method for your use case.

rainbow(10) %>%
  clr_darken() %>%
  plot()

Comparison Code

swatchplot(
  list(
    saturate = rbind("0" = clr_rotate(terrain.colors(10),  0),
                     "60" = clr_rotate(terrain.colors(10),  60),
                     "120" = clr_rotate(terrain.colors(10),  120),
                     "180" = clr_rotate(terrain.colors(10),  180),
                     "240" = clr_rotate(terrain.colors(10),  240),
                     "300" = clr_rotate(terrain.colors(10),  300)),
    desaturate = rbind("0" = clr_rotate(hcl.colors(10),  0),
                       "60" = clr_rotate(hcl.colors(10),  60),
                       "120" = clr_rotate(hcl.colors(10),  120),
                       "180" = clr_rotate(hcl.colors(10),  180),
                       "240" = clr_rotate(hcl.colors(10),  240),
                       "300" = clr_rotate(hcl.colors(10),  300))
  ),
  nrow = 7, line = 2.5
)