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

2

7

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

Agro answered 23/5, 2020 at 11:8 Comment(1)
Looks like the answer is very similar to Add image annotations to bar plots axis tick labelsCinderellacindi
M
6

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>

Mehala answered 23/5, 2020 at 14:21 Comment(8)
I am getting the error "Image size of 191841060x533 pixels is too large. It must be less than 2^16 in each direction." I am using 44x44 images to plot.Agro
I am reading the files from the internet 'countryflags.io/DO/flat/64.png'.Agro
Thanks. The image size issue is fixed. I was not setting the limits for the axes. But now I see that the images are appearing slightly at the edge of the bars. What could be causing this?Agro
You could experiment with the x-values in extent=[ .... If your x-axis is like 300000, you could choose something like extent=[value - 60000, value - 10000, .... An exact number is hard to set, because it would change as you add more bars, or the bars get longer or the figure changes.Mehala
Check existing Q&A. E.g. this would suggest to better use OffsetImage and thereby circumvent the data coordinates issue.Presbyter
Using OffsetImage with annotationbbox, I am able to solve the dynamic coordinates issue, but now the flags go past the axis whenever the value reaches close to zero. How do I solve this? zorder?Agro
the problem is that I have the value rendered in front of the bar.Agro
I want to make it such that the image gets behind the axis. As in it just appears as a part of the bar.Agro
G
2

To complete @johanC answer, it's possible to use flags from iso-flags-png under GNU/linux and the iso3166 python package:

import matplotlib.pyplot as plt
from iso3166 import countries
import matplotlib.image as mpimg


def pos_image(x, y, pays, haut):
    pays = countries.get(pays).alpha2.lower()
    fichier = "/usr/share/iso-flags-png-320x240"
    fichier += f"/{pays}.png"
    im = mpimg.imread(fichier)
    ratio = 4 / 3
    w = ratio * haut
    ax.imshow(im,
              extent=(x - w, x, y, y + haut),
              zorder=2)


plt.style.use('seaborn')
fig, ax = plt.subplots()

liste_pays = [('France', 10), ('USA', 9), ('Spain', 5), ('Italy', 5)]

X = [p[1] for p in liste_pays]
Y = [p[0] for p in liste_pays]

haut = .8
r = ax.barh(y=Y, width=X, height=haut, zorder=1)
y_bar = [rectangle.get_y() for rectangle in r]
# Fix axes' limits; see https://mcmap.net/q/1621805/-adding-a-flag-to-the-end-of-a-bar-chart-in-python
x_lim, y_lim = ax.get_xlim(), ax.get_ylim()
for pays, y in zip(liste_pays, y_bar):
    pos_image(pays[1], y, pays[0], haut)
ax.set_xlim(x_lim)
ax.set_ylim(y_lim)


plt.show()

which gives: enter image description here

Gentlemanfarmer answered 23/5, 2020 at 14:56 Comment(2)
With a current version of Matplotlib (tested with 3.9.1), this doesn't work (for me): Calling pos_image() and, with it, plt.imshow() seems to adjust the x and y limits of the axes to the region of the flag, so that the complete plot is not visible, any more. Solution/workaround: Use get_xlim()/get_ylim() before calling pos_image(), and set_xlim()/set_ylim() afterwards. See this follow-up question and answer: stackoverflow.com/questions/78797590Typographer
@Typographer Feel free to edit the answer to correct the code. ThxGentlemanfarmer

© 2022 - 2024 — McMap. All rights reserved.