replicating R/ggplot2 colours in python

2.9k views Asked by At

This link has R code to replicate ggplot's colours: Plotting family of functions with qplot without duplicating data

I have had a go at replicating the code in python - but the results are not right ...

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import math
import colorsys

# function to return a list of hex colour strings
def colorMaker(n=12, start=15.0/360.0, saturation=1.0, valight=0.65) :
    listOfColours = []
    for i in range(n) :
        hue = math.modf(float(i)/float(n) + start)[0]
        #(r,g,b) = colorsys.hsv_to_rgb(hue, saturation, valight)
        (r,g,b) = colorsys.hls_to_rgb(hue, valight, saturation)
        listOfColours.append( '#%02x%02x%02x' % (int(r*255), int(g*255), int(b*255)) )
    return listOfColours

# made up data
x = np.array(range(20))
d = {}
d['y1'] = pd.Series(x, index=x)
d['y2'] = pd.Series(1.5*x + 1, index=x)
d['y3'] = pd.Series(2*x + 2, index=x)
df = pd.DataFrame(d)

# plot example
plt.figure(num=1, figsize=(10,5), dpi=100) # set default image size
colours = colorMaker(n=3)
df.plot(linewidth=2.0, color=colours)
fig = plt.gcf()
fig.savefig('test.png')

The result ...

enter image description here

5

There are 5 answers

0
Mark Graph On BEST ANSWER

Looking closely at the gg code - it looks like I had two problems. The first is the denominator should have been n+1, rather than n. The second problem is that I needed a hue-chroma-luma conversion tool; rather than the hue-luma-saturation approach I had been taking. Unfortunately, I was not aware of a hcl_to_rgb tool in python; so one was written.

My solution (below) addresses these two problems. To my eye, I think it replicates ggplot2's colours.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import math
import collections

def hcl_to_rgb(hue=0, chroma=0, luma=0) :
    # Notes:
    #   coded from http://en.wikipedia.org/wiki/HSL_and_HSV#From_luma.2Fchroma.2Fhue
    #   with insights from gem.c in MagickCore 6.7.8
    #   http://www.imagemagick.org/api/MagickCore/gem_8c_source.html
    # Assume:
    #   h, c, l all in range 0 .. 1 (cylindrical coordinates)
    # Returns a tuple:
    #   r, g, b all in the range 0 .. 1 (cubic cartesian coordinates)

    # sanity checks
    hue = math.modf(float(hue))[0]
    if hue < 0 or hue >= 1 :
        raise ValueError('hue is a value greater than or equal to 0 and less than 1')
    chroma = float(chroma)
    if chroma < 0 or chroma > 1 :
        raise ValueError('chroma is a value between 0 and 1')
    luma = float(luma)
    if luma < 0 or luma > 1 :
        raise ValueError('luma is a value between 0 and 1')

    # do the conversion
    _h = hue * 6.0
    x = chroma * ( 1 - abs((_h % 2) - 1) )

    c = chroma
    if   0 <= _h and _h < 1 :
        r, g, b = (c, x, 0.0)
    elif 1 <= _h and _h < 2 :
        r, g, b = (x, c, 0.0)
    elif 2 <= _h and _h < 3 :
        r, g, b = (0.0, c, x)
    elif 3 <= _h and _h < 4 :
        r, g, b = (0.0, x, c)
    elif 4 <= _h and _h < 5 :
        r, g, b = (x, 0.0, c)
    elif 5 <= _h and _h <= 6 :
        r, g, b = (c, 0.0, x)
    else :
        r, g, b = (0.0, 0.0, 0.0)

    m = luma - (0.298839*r + 0.586811*g + 0.114350*b)
    z = 1.0
    if m < 0.0 :
        z = luma/(luma-m)
        m = 0.0
    elif m + c > 1.0 :
        z = (1.0-luma)/(m+c-luma)
        m = 1.0 - z * c
    (r, g, b) = (z*r+m, z*g+m, z*b+m)

    # clipping ...
    (r, g, b) = (min(r, 1.0), min(g, 1.0), min(b, 1.0))
    (r, g, b) = (max(r, 0.0), max(g, 0.0), max(b, 0.0))
    return (r, g, b)

def ggColorSlice(n=12, hue=(0.004,1.00399), chroma=0.8, luma=0.6, skipHue=True) :
    # Assume:
    #   n: integer >= 1
    #   hue[from, to]: all floats - red = 0; green = 0.33333 (or -0.66667) ; blue = 0.66667 (or -0.33333)
    #   chroma[from, to]: floats all in range 0 .. 1
    #   luma[from, to]: floats all in range 0 .. 1
    # Returns a list of #rgb colour strings:

    # convert stand alone values to ranges
    if not isinstance(hue, collections.Iterable):
        hue = (hue, hue)
    if not isinstance(chroma, collections.Iterable):
        chroma = (chroma, chroma)
    if not isinstance(luma, collections.Iterable):
        luma = (luma, luma)

    # convert ints to floats
    hue = [float(hue[y]) for y in (0, 1)]
    chroma = [float(chroma[y]) for y in (0, 1)]
    luma = [float(luma[y]) for y in (0, 1)]

    # some sanity checks
    n = int(n)
    if n < 1 or n > 360 :
        raise ValueError('n is a value between 1 and 360')
    if any([chroma[y] < 0.0 or chroma[y] > 1.0 for y in (0, 1)]) :
        raise ValueError('chroma is a value between 0 and 1')
    if any([luma[y] < 0.0 or luma[y] > 1.0 for y in (0, 1)]) :
        raise ValueError('luma is a value between 0 and 1')

    # generate a list of hex colour strings
    x = n + 1 if n % 2 else n
    if n > 1 :
        lDiff = (luma[1] - luma[0]) / float(n - 1.0)
        cDiff = (chroma[1] - chroma[0]) / float(n - 1.0)
        if skipHue :
            hDiff = (hue[1] - hue[0]) / float(x)
        else :
            hDiff = (hue[1] - hue[0]) / float(x - 1.0)
    else:
        hDiff = 0.0
        lDiff = 0.0
        cDiff = 0.0

    listOfColours = []
    for i in range(n) :
        c = chroma[0] + i * cDiff
        l = luma[0] + i * lDiff
        h = math.modf(hue[0] + i * hDiff)[0]
        h = h + 1 if h < 0.0 else h
        (h, c, l) = (min(h, 0.99999999999), min(c, 1.0), min(l, 1.0))
        (h, c, l) = (max(h, 0.0), max(c, 0.0), max(l, 0.0))
        (r, g, b) = hcl_to_rgb(h, c, l)
        listOfColours.append( '#%02x%02x%02x' % (int(r*255), int(g*255), int(b*255)) )
    return listOfColours

for i in range(1, 20) :
    # made up data
    x = np.array(range(20))
    d = {}
    for j in range(1, i+1) :
        y = x * (1.0 + j/10.0) + j/10.0
        d['y'+'%03.0d' % j] = pd.Series(data=y , index=x)
    df = pd.DataFrame(d)

    # plot example
    plt.figure(num=1, figsize=(10,5), dpi=100) # set default image size
    colours = ggColorSlice(n=i)
    if len(colours) == 1:
        colours = colours[0] # kludge
    df.plot(linewidth=4.0, color=colours)
    fig = plt.gcf()
    f = 'test-'+str(i)+'.png'
    print f
    plt.title(f)
    fig.savefig(f)
    plt.close()

enter image description here

enter image description here

enter image description here

4
Jakob On

You should check mpltools a really neat matplotlib extension. There is a ggplot style see here. E.g. (from link): enter image description here

0
Paul H On

There has been a fair amount of talk lately about unifying efforts to build libraries on top of matplotlib that create aesthetically pleasing plots in a much more direct fashion. Currently, there are several options out there. Unfortunately, the ecosystem is heavily fragmented.

In addition to mpltools mentioned by Jakob, I'd like to point out these three:

If you're familiar with R's ggplot2 already, you should feel pretty comfortable with python-ggplot and I heartily encourage you to contribute to it. Personally, I don't think I'll ever get my head around that style of plotting API (that's a critique of myself, not ggplot). Finally, my cursory looks at Seaborn and prettyplotlib lead me to believe that they're more akin to mpltools in that they provide convenience functions that build off of matplotlib.

From the sounds of things, the pandas community at least is putting more effort into growing seaborn and ggplot. I'm personally excited by this. It's worth mentioning that all of the efforts are to build on top of -- not replace -- matplotlib. I think most folks (self included) are very grateful for the robust and general framework the MPL devs have created over the past decade.

0
mwaskom On

You'll want to sample evenly along the hue dimension of the HUSL colorspace, for which there is a nice lightweight Python package. This is what's used in seaborn.

0
Michael Hall On

There is a builtin ggplot style in matplotlib.

Here is a sample of what colours it uses.

import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.patches as mpatch
ggplot_cm = plt.rcParams["axes.prop_cycle"].by_key()["color"]

fig, ax = plt.subplots()

n_rows = len(ggplot_cm)

for j, color_name in enumerate(ggplot_cm):
    # Pick text colour based on perceived luminance.
    rgba = mcolors.to_rgba_array([color_name])
    luma = 0.299 * rgba[:, 0] + 0.587 * rgba[:, 1] + 0.114 * rgba[:, 2]
    text_color = 'k' if luma[0] > 0.5 else 'w'

    col_shift = (j // n_rows)
    y_pos = j
    text_args = dict(fontsize=10, weight='bold')
    ax.add_patch(mpatch.Rectangle((0 + col_shift, y_pos), 1, 1, color=color_name))
    ax.text(0.5 + col_shift, y_pos + .7, color_name,
            color=text_color, ha='center', **text_args)

ax.set_ylim(n_rows, -1)
ax.axis('off')

enter image description here