pythonmatplotlibplotfigureaxes

How can I add images to bars in axes (matplotlib)


I want to add flag images such as below to my bar chart:

enter image description here

I have tried AnnotationBbox but that shows with a square outline. Can anyone tell how to achieve this exactly as above image?

Edit:

Below is my code

ax.barh(y = y, width = values, color = r, height = 0.8)

height = 0.8
for i, (value, url) in enumerate(zip(values, image_urls)):
    response = requests.get(url)
    img = Image.open(BytesIO(response.content))

    width, height = img.size
    left = 10
    top = 10
    right = width-10
    bottom = height-10
    im1 = img.crop((left, top, right, bottom)) 
    print(im1.size)
    im1

    ax.imshow(im1, extent = [value - 6, value, i - height / 2, i + height / 2], aspect = 'auto', zorder = 2)

Edit 2:

height = 0.8
for j, (value, url) in enumerate(zip(ww, image_urls)):
    response = requests.get(url)
    img = Image.open(BytesIO(response.content))
    ax.imshow(img, extent = [value - 6, value - 2, j - height / 2, j + height / 2], aspect = 'auto', zorder = 2)

ax.set_xlim(0, max(ww)*1.05)
ax.set_ylim(-0.5, len(yy) - 0.5)
plt.tight_layout()

enter image description here


Solution

  • You need the images in a .png format with a transparent background. (Software such as Gimp or ImageMagick could help in case the images don't already have the desired background.)

    With such an image, plt.imshow() can place it in the plot. The location is given via extent=[x0, x1, y0, y1]. To prevent imshow to force an equal aspect ratio, add aspect='auto'. zorder=2 helps to get the image on top of the bars. Afterwards, the plt.xlim and plt.ylim need to be set explicitly (also because imshow messes with them.)

    The example code below used 'ada.png' as that comes standard with matplotlib, so the code can be tested standalone. Now it is loading flags from countryflags.io, following this post.

    Note that the image gets placed into a box in data coordinates (6 wide and 0.9 high in this case). This box will get stretched, for example when the plot gets resized. You might want to change the 6 to another value, depending on the x-scale and on the figure size.

    import numpy as np
    import matplotlib.pyplot as plt
    # import matplotlib.cbook as cbook
    import requests
    from io import BytesIO
    
    labels = ['CW', 'CV', 'GW', 'SX', 'DO']
    colors = ['crimson', 'dodgerblue', 'teal', 'limegreen', 'gold']
    values = 30 + np.random.randint(5, 20, len(labels)).cumsum()
    
    height = 0.9
    plt.barh(y=labels, width=values, height=height, color=colors, align='center')
    
    for i, (label, value) in enumerate(zip(labels, values)):
        # load the image corresponding to label into img
        # with cbook.get_sample_data('ada.png') as image_file:
        #    img = plt.imread(image_file)
        response = requests.get(f'https://www.countryflags.io/{label}/flat/64.png')
        img = plt.imread(BytesIO(response.content))
        plt.imshow(img, extent=[value - 8, value - 2, i - height / 2, i + height / 2], aspect='auto', zorder=2)
    plt.xlim(0, max(values) * 1.05)
    plt.ylim(-0.5, len(labels) - 0.5)
    plt.tight_layout()
    plt.show()
    

    example plot

    PS: As explained by Ernest in the comments and in this post, using OffsetImage the aspect ratio of the image stays intact. (Also, the xlim and ylim stay intact.) The image will not shrink when there are more bars, so you might need to experiment with the factor in OffsetImage(img, zoom=0.65) and the x-offset in AnnotationBbox(..., xybox=(-25, 0)).

    An extra option could place the flags outside the bar for bars that are too short. Or at the left of the y-axis.

    The code adapted for horizontal bars could look like:

    import numpy as np
    import requests
    from io import BytesIO
    import matplotlib.pyplot as plt
    from matplotlib.offsetbox import OffsetImage, AnnotationBbox
    
    def offset_image(x, y, label, bar_is_too_short, ax):
        response = requests.get(f'https://www.countryflags.io/{label}/flat/64.png')
        img = plt.imread(BytesIO(response.content))
        im = OffsetImage(img, zoom=0.65)
        im.image.axes = ax
        x_offset = -25
        if bar_is_too_short:
            x = 0
        ab = AnnotationBbox(im, (x, y), xybox=(x_offset, 0), frameon=False,
                            xycoords='data', boxcoords="offset points", pad=0)
        ax.add_artist(ab)
    
    labels = ['CW', 'CV', 'GW', 'SX', 'DO']
    colors = ['crimson', 'dodgerblue', 'teal', 'limegreen', 'gold']
    values = 2 ** np.random.randint(2, 10, len(labels))
    
    height = 0.9
    plt.barh(y=labels, width=values, height=height, color=colors, align='center', alpha=0.8)
    
    max_value = values.max()
    for i, (label, value) in enumerate(zip(labels, values)):
        offset_image(value, i, label, bar_is_too_short=value < max_value / 10, ax=plt.gca())
    plt.subplots_adjust(left=0.15)
    plt.show()
    

    example using <code>OffsetImage</code>