Search code examples
pythonmatplotlibcolorscolor-space

Smooth, interpolated tertiary or quaternary colour scales (e.g. r, g, b triangle) for matplotlib?


Say I want to create a colour scheme for points in a matplotlib plot, and each point has a proportion of properties A, B and C (each of which can take values from 0 to 1). A simple way of doing this would be with an rgb triangle, so that the point's red value is given by A, its green value is given by B, its blue value given by C.

However, it seems like maybe an alternative triangle like this might give more 'dynamic range', i.e. make it easier to see where in the triangle each point is. However, I don't know how to code that specific triangle (i.e. given A, B and C, return a (r,g,b) tuple). It also has the additional problem of having white in the centre, which wouldn't appear well against a white background.

My question: What would be a good triangle colour scale for a problem like this? And what is this kind of thing called, because I'm finding it difficult to search for?

Additionally, if we wanted to extend this to four properties, A, B, C and D, and have a "colour square," what would be appropriate colours for the four corners, and how would you write a function to go from A, B, C and D and return a (r,g,b) tuple?

It seems like ideally a colour scheme like this (a) has unique colours everywhere, (b) doesn't have white in it, and (c) ideally has high variation to make it easier to see where in the colour scale you are by looking at it. Even better would be something colour blind safe, but in this circumstance that might be too much to ask for.

Advice very much appreciated. My end goal is to make something like this but with one of these colour schemes. Thanks.

Edit: Adding the important constraint A+B+C=1 to make this technically possible. For the sake of example, let's say I want to come up with a colour scheme so that a unique colour maps to a specific composition in a ternary phase diagram such as this. The 'obvious' case is to use a RGB triangle, but is there a better solution? And how would I code that in Python?


Solution

  • Okay, I've given this a go. It's not perfect and is a bit of a hack, so improvements/suggested are very much welcomed. I'd be happy to accept a better answer (in particular improvements to abc_to_rgb either to remove errors, improve dynamic range, or just a better colour scheme for colour blindness). I was thinking about maybe using the YIQ colour space with Y=0.5 for an equivalent quaternary colour scheme (square legend). Also, this is the first time I've used barycentric co-ordinates, so my plotting of the legend is probably not ideal (a better legend might not 'bleed' out of the edges, for example, or need so many points).

    import matplotlib.pyplot as plt
    import numpy as np
    import math
    
    def abc_to_rgb(A=0.0,B=0.0,C=0.0):
        ''' Map values A, B, C (all in domain [0,1]) to
        suitable red, green, blue values.'''
        return (min(B+C,1.0),min(A+C,1.0),min(A+B,1.0))
    
    def plot_legend():
        ''' Plots a legend for the colour scheme
        given by abc_to_rgb. Includes some code adapted
        from http://stackoverflow.com/a/6076050/637562'''
    
        # Basis vectors for triangle
        basis = np.array([[0.0, 1.0], [-1.5/np.sqrt(3), -0.5],[1.5/np.sqrt(3), -0.5]])
    
        fig = plt.figure()
        ax = fig.add_subplot(111,aspect='equal')
    
        # Plot points
        a, b, c = np.mgrid[0.0:1.0:50j, 0.0:1.0:50j, 0.0:1.0:50j]
        a, b, c = a.flatten(), b.flatten(), c.flatten()
    
        abc = np.dstack((a,b,c))[0]
        #abc = filter(lambda x: x[0]+x[1]+x[2]==1, abc) # remove points outside triangle
        abc = map(lambda x: x/sum(x), abc) # or just make sure points lie inside triangle ...
    
        data = np.dot(abc, basis)
        colours = [abc_to_rgb(A=point[0],B=point[1],C=point[2]) for point in abc]
    
        ax.scatter(data[:,0], data[:,1],marker=',',edgecolors='none',facecolors=colours)
    
        # Plot triangle
        ax.plot([basis[_,0] for _ in range(3) + [0,]],[basis[_,1] for _ in range(3) + [0,]],**{'color':'black','linewidth':3})
    
        # Plot labels at vertices
        offset = 0.25
        fontsize = 32
        ax.text(basis[0,0]*(1+offset), basis[0,1]*(1+offset), '$A$', horizontalalignment='center',
                verticalalignment='center', fontsize=fontsize)
        ax.text(basis[1,0]*(1+offset), basis[1,1]*(1+offset), '$B$', horizontalalignment='center',
                verticalalignment='center', fontsize=fontsize)
        ax.text(basis[2,0]*(1+offset), basis[2,1]*(1+offset), '$C$', horizontalalignment='center',
                verticalalignment='center', fontsize=fontsize)    
    
        ax.set_frame_on(False)
        ax.set_xticks(())
        ax.set_yticks(())
    
        plt.show()
    

    This gives our legend as follows:

    Example ternary colour legend.

    And then abc_to_rgb can be used to get a suitable colour for a given point in a scatter or line graph...