dmoench
dmoench

Reputation: 744

Matplotlib Rectangle With Color Gradient Fill

I want to draw a rectangle, with a gradient color fill from left to right, at an arbitrary position with arbitrary dimensions in my axes instance (ax1) coordinate system.

enter image description here

My first thought was to create a path patch and somehow set its fill as a color gradient. But according to THIS POST there isn't a way to do that.

Next I tried using a colorbar. I created a second axes instance ax2 using fig.add_axes([left, bottom, width, height]) and added a color bar to that.

ax2 = fig.add_axes([0, 0, width, height/8])
colors = [grad_start_color, grad_end_color]
index  = [0.0, 1.0]
cm = LinearSegmentedColormap.from_list('my_colormap', zip(index, colors))
colorbar.ColorbarBase(ax2, cmap=cm, orientation='horizontal')

But the positional parameters passed to fig.add_axes() are in the coordinate system of fig, and don't match up with the coordinate system of ax1.

How can I do this?

Upvotes: 9

Views: 10171

Answers (3)

Takumi Matz
Takumi Matz

Reputation: 41

How about something like this?

import matplotlib as mpl
import matplotlib.pyplot as plt

def cname2hex(cname):
    colors = dict(mpl.colors.BASE_COLORS, **mpl.colors.CSS4_COLORS) # dictionary. key: names, values: hex codes
    try:
       hex = colors[cname]
       return hex
    except KeyError:
       print(cname, ' is not registered as default colors by matplotlib!')
            return None

def hex2rgb(hex, normalize=False):
    h = hex.strip('#')
    rgb = np.asarray(list(int(h[i:i + 2], 16) for i in (0, 2, 4)))
    return rgb

def draw_rectangle_gradient(ax, x1, y1, width, height, color1='white', color2='blue', alpha1=0.0, alpha2=0.5, n=100):
    # convert color names to rgb if rgb is not given as arguments
    if not color1.startswith('#'):
        color1 = cname2hex(color1)
    if not color2.startswith('#'):
        color2 = cname2hex(color2)
    color1 = hex2rgb(color1) / 255.  # np array
    color2 = hex2rgb(color2) / 255.  # np array


    # Create an array of the linear gradient between the two colors
    gradient_colors = []
    for segment in np.linspace(0, width, n):
        interp_color = [(1 - segment / width) * color1[j] + (segment / width) * color2[j] for j in range(3)]
        interp_alpha = (1 - segment / width) * alpha1 + (segment / width) * alpha2
        gradient_colors.append((*interp_color, interp_alpha))
    for i, color in enumerate(gradient_colors):
        ax.add_patch(plt.Rectangle((x1 + width/n * i, y1), width/n, height, color=color, linewidth=0, zorder=0))
    return ax

# SAMPLE
fig, ax = plt.subplots(figsize=(4, 2))
ax.set_xlim(0, 100)
ax.set_ylim(0, 40)
draw_rectangle_gradient(ax, 0, 0, 30, 10, color1='blue', color2='orange', alpha1=1, alpha2=1)
draw_rectangle_gradient(ax, 40, 15, 40, 10, color1='white', color2='pink', alpha1=1, alpha2=1)
draw_rectangle_gradient(ax, 15, 30, 80, 5, color1='gold', color2='red', alpha1=0.0, alpha2=1)

enter image description here

Upvotes: 2

I have been asking myself a similar question and spent some time looking for the answer to find in the end that this can quite easily be done by imshow:

from matplotlib import pyplot

pyplot.imshow([[0.,1.], [0.,1.]], 
  cmap = pyplot.cm.Greens, 
  interpolation = 'bicubic'
)

enter image description here

It is possible to specify a colormap, what interpolation to use and much more. One additional thing, I find very interesting, is the possibility to specify which part of the colormap to use. This is done by means of vmin and vmax:

pyplot.imshow([[64, 192], [64, 192]], 
  cmap = pyplot.cm.Greens, 
  interpolation = 'bicubic', 
  vmin = 0, vmax = 255
)

enter image description here

Inspired by this example


Additional Note:

I chose X = [[0.,1.], [0.,1.]] to make the gradient change from left to right. By setting the array to something like X = [[0.,0.], [1.,1.]], you get a gradient from top to bottom. In general, it is possible to specify the colour for each corner where in X = [[i00, i01],[i10, i11]], i00, i01, i10 and i11 specify colours for the upper-left, upper-right, lower-left and lower-right corners respectively. Increasing the size of X obviously allows to set colours for more specific points.

Upvotes: 9

Ed Smith
Ed Smith

Reputation: 13206

did you ever solve this? I wanted the same thing and found the answer using the coordinate mapping from here,

 #Map axis to coordinate system
def maptodatacoords(ax, dat_coord):
    tr1 = ax.transData.transform(dat_coord)
    #create an inverse transversion from display to figure coordinates:
    fig = ax.get_figure()
    inv = fig.transFigure.inverted()
    tr2 = inv.transform(tr1)
    #left, bottom, width, height are obtained like this:
    datco = [tr2[0,0], tr2[0,1], tr2[1,0]-tr2[0,0],tr2[1,1]-tr2[0,1]]

    return datco

#Plot a new axis with a colorbar inside
def crect(ax,x,y,w,h,c,**kwargs):

    xa, ya, wa, ha = maptodatacoords(ax, [(x,y),(x+w,y+h)])
    fig = ax.get_figure()
    axnew = fig.add_axes([xa, ya, wa, ha])
    cp = mpl.colorbar.ColorbarBase(axnew, cmap=plt.get_cmap("Reds"),
                                   orientation='vertical',                                
                                   ticks=[],
                                   **kwargs)
    cp.outline.set_linewidth(0.)
    plt.sca(ax)

Hopefully this helps anyone in the future who needs similar functionality. I ended up using a grid of patch objects instead.

Upvotes: 2

Related Questions