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

I want to add flag images such as below to my bar chart:
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()

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()
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()

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:

Related

matplotlib Gridspec subplots unexpected different size

I am trying to create a grid of images using matplotlib.
The first row and column define the input to a function and the rest of the grid is the output.
Here's someone else's reference of how I would like it to look: reference.
Especially note that lines seperating the first row and column from everything else.
I was trying for the last couple of hours to make it work. The best I've come so far is using Gridspec to divide the image into four groups and construct the image using PIL.
However, for a reason I cannot understand the shapes of the different subplots don't match.
Attaching a minimal code and it's output.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import PIL
f = plt.figure(figsize=(20, 20))
resolution = 256
num_images = 6
h = w = num_images
main_grid = gridspec.GridSpec(h, w, hspace=0, wspace=0)
col = f.add_subplot(main_grid[0, 1:])
row = f.add_subplot(main_grid[1:, 0])
mid = f.add_subplot(main_grid[1:, 1:])
corner = f.add_subplot(main_grid[0, 0])
corner_canvas = PIL.Image.new('RGB', (resolution, resolution), 'gray')
mid_canvas = PIL.Image.new('RGB', (resolution * w, resolution * h), 'yellow')
col_canvas = PIL.Image.new('RGB', (resolution * w, resolution), 'blue')
row_canvas = PIL.Image.new('RGB', (resolution, resolution * h), 'red')
corner.imshow(corner_canvas)
col.imshow(col_canvas)
row.imshow(row_canvas)
mid.imshow(mid_canvas)
plt.savefig('fig.png')
As you can see here, the shapes don't match which make the grid not aligned.
Any solution producing an image in the style of the reference would be great !
I would use a combination of GridSpec and GridSpecFromSubplotSpec for this kind of layout:
Nx = 2
Ny = 3
sp = 0.5
fig = plt.figure()
gs0 = matplotlib.gridspec.GridSpec(2,2, width_ratios=[1,Nx+1], height_ratios=[1,Ny+1], wspace=sp, hspace=sp, figure=fig)
gs00 = matplotlib.gridspec.GridSpecFromSubplotSpec(1,Nx,subplot_spec=gs0[0,1:], wspace=0, hspace=0)
gs01 = matplotlib.gridspec.GridSpecFromSubplotSpec(Ny,1,subplot_spec=gs0[1:,0], wspace=0, hspace=0)
gs11 = matplotlib.gridspec.GridSpecFromSubplotSpec(Ny,Nx, subplot_spec=gs0[1:,1:], wspace=0, hspace=0)
top_axes = [fig.add_subplot(gs00[i]) for i in range(Nx)]
left_axes = [fig.add_subplot(gs01[i]) for i in range(Ny)]
center_axes = [fig.add_subplot(gs11[j,i]) for j in range(Ny) for i in range(Nx)]

How to use matplotlib to add image to the top level of surface of all the existing layers

I am trying to add a logo to the current plot, which already has existing plot elements. I defined a background in the plot_pic() function. Then plot it, and I want to add a logo to the top surface. I've tried to put the zorder = 10, but it doesn't work. The codes in Jupyter Notebook are:
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from matplotlib.offsetbox import OffsetImage
%matplotlib inline
from matplotlib.patches import Circle, Rectangle, Arc, Ellipse
def plot_pic(ax=None, color='black', lw=2, scale = 15):
# get the current ax if ax is None
if ax is None:
ax = plt.gca()
big_box = Rectangle((-34 * scale, 0), width = 68 * scale, height = 105 / 2 * scale, linewidth=lw, color=color, fill=False)
middle_box = Rectangle((-(7.32 * scale / 2+ 5.5 * scale +11 * scale),0), width = (5.5 * scale * 2 + 11 * scale * 2 + 7.32 * scale), height = 16.5 * scale, linewidth = lw, color = color, fc = "white")
small_box = Rectangle((-(7.32 * scale/ 2 + 5.5 * scale), 0), width = 7.32 * scale + 5.5 * scale * 2, height = 5.5 * scale, linewidth = lw, color = color, fill = False)
arc = Circle((0, 11 * scale), radius = 9.15 * scale, color = color, lw = lw, fill = False, zorder = 0)
# List of elements to be plotted
pic_elements = [big_box, middle_box, small_box, arc]
# Add the elements onto the axes
for element in pic_elements:
ax.add_patch(element)
return ax
fig = plt.figure()
fig = plt.figure(figsize=(10, 10))
ax = plt.subplot()
logo=mpimg.imread('rbl_logo.png')
# You have to add your own logo, this is in my own folder
addLogo = OffsetImage(logo, zoom=0.6, zorder = 10)
addLogo.set_offset((200,-10)) # pass the position in a tuple
ax.add_artist(addLogo)
plt.xlim(-600,600)
plt.ylim(-100,1000)
plot_pic()
The result is that, the plot_pic() layer covers part of the logo that I wanted to show, and I just want to place the logo on the very top surface that covers all the elements below.
Is there anyway to do so? Thank you very much.
The problem is that setting the zorder with the keyword arguments sets the zorder of the image inside the OffsetBox, which will have no effect. In order to set the zorder of the box itself, you need to set this externally:
addLogo = OffsetImage(logo, zoom=0.6)
addLogo.set_zorder(10)

How to plot cross-sections of imshow?

I am doing numerical simulations in python 3.6 and trying to inspect cross-sections of 2d imshow. I made the horizontal inspection and would like to have vertical, but got into some difficulties. The blue inspection lines correspond to 'bottom' (horizontal) and 'left' (vertical) subplots. Example code (I haven't been allowed to attach a matplotlib image):
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.pyplot as plt
import numpy as np
Array = np.random.rand(100, 100)
grid_points = 100
fig_mpl, ax = plt.subplots(figsize = (10, 10), facecolor = 'white')
line = ax.imshow(Array, cmap = 'hot')
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size = "5%", pad = 0.05)
caxb = divider.append_axes("bottom", size = "10%", pad = 0.05)
caxl = divider.append_axes("left", size = "10%", pad = 0.05)
bar = fig_mpl.colorbar(line, cax = cax, orientation = 'vertical')
ax.axhline(grid_points/2)
ax.axvline(grid_points/2)
X = np.linspace(0, grid_points - 1, grid_points)
projb, = caxb.plot(X, Array[int(grid_points/2)], color = 'red')
projl, = caxl.plot(X, Array[:, int(grid_points/2)], color = 'red')
caxb.set_ylim(-0.1*np.max(Array), 1.1*np.max(Array))
caxb.set_xlim(0, grid_points - 1)
caxl.set_xlim(-0.1*np.max(Array), 1.1*np.max(Array))
caxl.set_ylim(0, grid_points - 1)
ax.set_xticks([])
ax.set_yticks([])
caxb.set_xticks([])
caxl.set_yticks([])
caxb.set_yticks([np.min(Array), np.max(Array)])
caxl.set_xticks([np.min(Array), np.max(Array)])
caxb.yaxis.tick_right()
for tick in caxl.get_xticklabels():
tick.set_rotation(-90)
caxb.grid(color = 'black', marker = 8)
caxl.grid(color = 'black', marker = 8)
fig_mpl.subplots_adjust(wspace = 0)
fig_mpl.tight_layout()
I want projl to plot the cross-section of Array in vertical caxl.
Is there any proper way to do the thing?
Instead of
projl, = caxl.plot(X, Array[:, int(grid_points/2)], color = 'red')
you need
projl, = caxl.plot(Array[:, int(grid_points/2)], X, color = 'red')
because the amplitude should be shown along the horizontal (x-) axis and the grid index (X) along the vertical (y-) axis.

Setting the size of a matplotlib ColorbarBase object

I have an patch collection that I'd like to display a color map for. Because of some manipulations I do on top of the colormap, it's not possible for me to define it using a matplotlib.colorbar instance. At least not as far as I can tell; doing so strips some manipulations I do with my colors that blank out patches lacking data:
cmap = matplotlib.cm.YlOrRd
colors = [cmap(n) if pd.notnull(n) else [1,1,1,1]
for n in plt.Normalize(0, 1)([nullity for _, nullity in squares])]
# Now we draw.
for i, ((min_x, max_x, min_y, max_y), _) in enumerate(squares):
square = shapely.geometry.Polygon([[min_x, min_y], [max_x, min_y],
[max_x, max_y], [min_x, max_y]])
ax0.add_patch(descartes.PolygonPatch(square, fc=colors[i],
ec='white', alpha=1, zorder=4))
So I define a matplotlib.colorbar.ColorbarBase instance instead, which works:
matplotlib.colorbar.ColorbarBase(ax1, cmap=cmap, orientation='vertical',
norm=matplotlib.colors.Normalize(vmin=0, vmax=1))
Which results in e.g.:
The problem I have is that I want to reduce the size of this colorbar (specifically, the shrink it down to a specific vertical size, say, 500 pixels), but I don't see any obvious way of doing this. If I had a colorbar instance, I could adjust this easily using its axis property arguments, but ColorbarBase lacks these.
For further reference:
The example my implementation is based on.
The source code in question (warning: lengthy).
The size and shape is defined with the axis. This is a snippet from code I have where I group 2 plots together and add a colorbar at the top independently. I played with the values in that add_axes instance until I got a size that worked for me:
cax = fig.add_axes([0.125, 0.925, 0.775, 0.0725]) #has to be as a list - starts with x, y coordinates for start and then width and height in % of figure width
norm = mpl.colors.Normalize(vmin = low_val, vmax = high_val)
mpl.colorbar.ColorbarBase(cax, cmap = self.cmap, norm = norm, orientation = 'horizontal')
The question may be a bit old, but I found another solution that can be of help for anyone who is not willing to manually create a colorbar axes for the ColorbarBase class.
The solution below uses the matplotlib.colorbar.make_axes class to create a dependent sub_axes from the given axes. That sub_axes can then be supplied for the ColorbarBase class for the colorbar creation.
The code is derived from the matplotlib code example describe in here
Here is a snippet code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.colorbar as mcbar
from matplotlib import ticker
import matplotlib.colors as mcolors
# Make some illustrative fake data:
x = np.arange(0, np.pi, 0.1)
y = np.arange(0, 2 * np.pi, 0.1)
X, Y = np.meshgrid(x, y)
Z = np.cos(X) * np.sin(Y) * 10
colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] # R -> G -> B
n_bins = [3, 6, 10, 100] # Discretizes the interpolation into bins
cmap_name = 'my_list'
fig, axs = plt.subplots(2, 2, figsize=(9, 7))
fig.subplots_adjust(left=0.02, bottom=0.06, right=0.95, top=0.94, wspace=0.05)
for n_bin, ax in zip(n_bins, axs.ravel()):
# Create the colormap
cm = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bin)
# Fewer bins will result in "coarser" colomap interpolation
im = ax.imshow(Z, interpolation='nearest', origin='lower', cmap=cm)
ax.set_title("N bins: %s" % n_bin)
cax, cbar_kwds = mcbar.make_axes(ax, location = 'right',
fraction=0.15, shrink=0.5, aspect=20)
cbar = mcbar.ColorbarBase(cax, cmap=cm,
norm=mcolors.Normalize(clip=False),
alpha=None,
values=None,
boundaries=None,
orientation='vertical', ticklocation='auto', extend='both',
ticks=n_bins,
format=ticker.FormatStrFormatter('%.2f'),
drawedges=False,
filled=True,
extendfrac=None,
extendrect=False, label='my label')
if n_bin <= 10:
cbar.locator = ticker.MaxNLocator(n_bin)
cbar.update_ticks()
else:
cbar.locator = ticker.MaxNLocator(5)
cbar.update_ticks()
fig.show()

How can I create a figure with optimal resolution for printing?

I'm rather fond of The Logistic Map' Period Doubling Bifurcation and would like to print it on a canvas.
I can create the plot in python, but need some help preparing figure properties so that it has suitable resolution to be printed. My code right ow produces some jagged lines.
Here is my code:
import numpy as np
import matplotlib.pyplot as plt
# overall image properties
width, height, dpi = 2560, 1440, 96
picture_background = 'white'
aspect_ratio = width / height
plt.close('all')
R = np.linspace(3.5,4,5001)
fig = plt.figure(figsize=(width / dpi, height / dpi), frameon=False)
ylim = -0.1,1.1
ax = plt.Axes(fig, [0, 0, 1, 1], xlim = (3.4,4))
ax.set_axis_off()
fig.add_axes(ax)
for r in R:
x = np.zeros(5001)
x[0] = 0.1
for i in range(1,len(x)):
x[i] = r*x[i-1]*(1-x[i-1])
ax.plot(r*np.ones(2500),x[-2500:],marker = '.', markersize= 0.01,color = 'grey', linestyle = 'none')
plt.show()
plt.savefig('figure.eps', dpi=dpi, bbox_inches=0, pad_inches=0, facecolor=picture_background)
Here is what the code produces:
As you can see, some of the lines to the far left of the plot are rather jagged.
How can I create this figure so that the resolution is suitable to be printed on a variety of frame dimensions?
I think the source of the jaggies is underlying pixel size + that you are drawing this using very small 'point' markers. The pixels that the line are going through are getting fully saturated so you get the 'jaggy'.
A somewhat better way to plot this data is to do the binning ahead of time and then have mpl plot a heat map:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
plt.ion()
width, height, dpi = 2560 / 2, 1440 / 2, 96 # cut to so SO will let me upload result
aspect_ratio = width / height
fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), frameon=False,
tight_layout=True)
ylim = -0.1, 1.1
ax.axis('off')
# make heatmap at double resolution
accumulator = np.zeros((height, width), dtype=np.uint64)
burn_in_count = 25000
N = 25000
R = np.linspace(3.5, 4, width)
x = 0.1 * np.ones_like(R)
row_indx = np.arange(len(R), dtype='uint')
# do all of the r values in parallel
for j in range(burn_in_count):
x = R * x * (1 - x)
for j in range(N):
x = R * x * (1 - x)
col_indx = (height * x).astype('int')
accumulator[col_indx, row_indx] += 1
im = ax.imshow(accumulator, cmap='gray_r',
norm=mcolors.LogNorm(), interpolation='none')
Note that this is log-scaled, if you just want to see what pixels are hit
use
im = ax.imshow(accumulator>0, cmap='gray_r', interpolation='nearest')
but these still have issues of the jaggies and (possibly worse) sometimes the narrow lines get aliased out.
This is the sort of problem that datashader or rasterized scatter is intended to solve by re-binning the data at draw time in an intelligent way (see this PR for a prototype datashader/mpl integartion). Both of those are still prototype/proof-of-concept, but usable.
http://matplotlib.org/examples/event_handling/viewlims.html which re-compute the Mandelbrot set on zoom might also be of interest to you.

Categories

Resources