6

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

Hassan Zaheer
  • 1,361
  • 2
  • 20
  • 34
  • Looks like the answer is very similar to [Add image annotations to bar plots axis tick labels](https://stackoverflow.com/q/44246650/7758804) – Trenton McKinney Aug 28 '21 at 22:11

2 Answers2

5

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>

JohanC
  • 71,591
  • 8
  • 33
  • 66
  • 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. – Hassan Zaheer May 23 '20 at 15:06
  • I am reading the files from the internet 'https://www.countryflags.io/DO/flat/64.png'. – Hassan Zaheer May 23 '20 at 15:46
  • 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? – Hassan Zaheer May 23 '20 at 16:19
  • 1
    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. – JohanC May 23 '20 at 16:22
  • 2
    Check existing Q&A. E.g. [this](https://stackoverflow.com/a/44264051/4124317) would suggest to better use `OffsetImage` and thereby circumvent the data coordinates issue. – ImportanceOfBeingErnest May 24 '20 at 01:17
  • 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? – Hassan Zaheer May 25 '20 at 20:36
  • the problem is that I have the value rendered in front of the bar. – Hassan Zaheer May 25 '20 at 21:36
  • I want to make it such that the image gets behind the axis. As in it just appears as a part of the bar. – Hassan Zaheer May 25 '20 at 21:43
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]
for pays, y in zip(liste_pays, y_bar):
    pos_image(pays[1], y, pays[0], haut)


plt.show()

which gives: enter image description here

david
  • 1,302
  • 1
  • 10
  • 21