I want to create a plot with two custom images below the plot, but I haven't figured out how to do it.
I'm using python 3.7 and matplotlib
this is what I have so far
df = pd.DataFrame({
'some data':[2,0,0,3,2,1,4],
'other data':[5,1,0,5,2,2,3]
})
logo = plt.imread('A.jpg')
title = 'images'
ax = df.plot(kind='bar',x='some data',y='other data')
ax.set_title(title, fontsize=20)
ax.figure.figimage(logo, 40, 40, alpha=.15, zorder=1)
This is the result
and this is what I'm trying to get.
This is the image in the plot
In order to get the images how you have them displayed in your expected output (one aligned left, one aligned right, both below plot) you can do something like this
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
'some data':[2,0,0,3,2,1,4],
'other data':[5,1,0,5,2,2,3]
})
logo = plt.imread('A.jpg')
title = 'images'
fig, ax = plt.subplots(1)
df.plot(kind='bar',x='some data',y='other data', ax=ax)
ax.set_title(title, fontsize=20)
h = logo.shape[1]/fig.bbox.ymax
fig.subplots_adjust(0.05, h, 0.97, 0.93)
ax.figure.figimage(logo, 0, 0, alpha=.15, zorder=1)
ax.figure.figimage(logo, fig.bbox.xmax - logo.shape[0], 0, alpha=.15, zorder=1)
plt.show()
This will resize the figure to accommodate the height of the logos: logo.shape[0] is the width of the logo in pixels logo.shape[1] is the height of the logo in pixels, fig.bbox.ymax is the height of the figure in pixels. h is the fractional height of the logo which can then be used in fig.subplots_adjust to adjust the height accordingly.
It then positions one logo with the bottom left corner at offset (0, 0) and another with the bottom left corner at (fig.bbox.xmax - logo.shape[0], 0) where fig.bbox.xmax is the width of the figure in pixels.1
This will give you something that looks like this:
you can export the plot as a svg file. Open this with inkscape and add the picture.
Related
I want to make a plot with a grid of thumbnails on the left and a line plot on the right. Here is a minimal example
import numpy as np
from matplotlib import pyplot as plt
### This can change at runtime
n_grid = 4
### Grid of thumbnails
fig = plt.figure(figsize=(20,10.2))
for i in range(n_grid):
for j in range(n_grid):
ax = plt.subplot2grid(shape=(n_grid, 2*n_grid), loc=(i,j))
plt.imshow(np.random.random((16,16)))
ax.set_axis_off()
### Line plot
ax = plt.subplot2grid(shape=(n_grid, 2*n_grid), loc=(0,n_grid), rowspan=n_grid-1, colspan=n_grid)
plt.plot(np.cumsum(np.random.random(100)), label='Random Sum')
plt.xlim([0, 100])
plt.ylim(0,50)
plt.xlabel('Number', fontsize=12)
plt.ylabel('Sum', fontsize=12)
plt.figtext(0.5, 0.01, f'Unique identifier', ha='center', va='baseline')
#plt.tight_layout()
plt.subplots_adjust(left=0.01, bottom=0.03, right=0.99, top=0.99, wspace = 0.06, hspace=0.06)
plt.savefig('plot_1.png', dpi=96)
The problem is that the yticklabels and ylabel stick over the center into the area of the thumbnails. The lineplot on the right is too wide.
One common solution found on the internet is using automatic resizing with tight_layout(), so I change the last three lines to
plt.tight_layout()
#plt.subplots_adjust(left=0.01, bottom=0.03, right=0.99, top=0.99, wspace = 0.06, hspace=0.06)
plt.savefig('plot_2.png', dpi=96)
This does not rescale the lineplot, but instead makes the wspace and hspace attributes so big I get way too much whitespace between the thumbnails.
I am looking for a solution to either
Set wspace and hspace of only the right subplot, not all of them together, or
resize the lineplot to fit into the designated area, without the labels sticking out
It would seem that this is an easy problem, but despite searching for about 2 hours and digging around in the object properties with iPython I found nothing suitable. All solutions seem to change the size and padding of the subplots, not fitting a plot into the area defined with subplot2grid. The only other solution I can think of is a hack that calculates a modified aspect from the value ranges to make the lineplot always a given percentage thinner.
You can play around with subfigures. For example, if you do:
import numpy as np
from matplotlib import pyplot as plt
### This can change at runtime
n_grid = 4
### Grid of thumbnails
fig = plt.figure(figsize=(20,10.2))
# add 2 subfigures
subfigs = fig.subfigures(1, 2, wspace=0)
# add thumbnail grid into left subfig
gsLeft = subfigs[0].add_gridspec(n_grid, n_grid)
axLeft = []
for i in range(n_grid):
for j in range(n_grid):
axLeft.append(subfigs[0].add_subplot(gsLeft[i, j]))
axLeft[-1].imshow(np.random.random((16,16)))
axLeft[-1].set_axis_off()
### Line plot
gsRight = subfigs[1].add_gridspec(3, 1)
axRight = subfigs[1].add_subplot(gsRight[:2, 0])
axRight.plot(np.cumsum(np.random.random(100)), label='Random Sum')
axRight.set_xlim([0, 100])
axRight.set_ylim(0,50)
axRight.set_xlabel('Number', fontsize=12)
axRight.set_ylabel('Sum', fontsize=12)
# adjust subfigures here (play around with these to get the desired effect)
subfigs[0].subplots_adjust(wspace=0.03, hspace=0.03, bottom=0.05, top=0.95, left=0.05, right=0.95)
subfigs[1].subplots_adjust(left=0.01)
# add title (here I've had to add it to the left figure, so it's not centred,
# in my test adding it to the figure itself meant it was not visible, although
# the example in the Matplotlib docs suggests it should work!)
# fig.suptitle(f'Unique identifier', x=0.5, y=0.025, ha='center', va='baseline')
subfigs[0].suptitle(f'Unique identifier', x=0.5, y=0.025, ha='center', va='baseline')
fig.savefig("plot_1.png", dpi=150)
This gives:
but you can play around with the values to adjust it as you like.
I want to generate publication quality plots with matplotlib. For consistency reasons I want all diagrams (axes) to look the same, particularly in size. I look through many tutorials and came up with the following idea:
1. create a plot and determine the height of the axes in inches
2. add the legend and determine the height of the legend in inches
3. enlarge the figure height by the legend height in inches
4. shrink the new axes height to the original axes height to keep the legend in the figure area
Unfortunately, this is not working as intended.
Any suggestions on the Code?
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms
# GENERATE DATA
x = np.arange(-2.*np.pi,4.*np.pi,0.01)
sin_arr = [np.sin(x-dx) for dx in np.arange(0.,2.*np.pi,0.5)]
# SET FIGURE SIZE AND RCPARAMETERS
plt.rcParams.update( {'figure.figsize' : [5.90551197, 3.64980712]} )
params = {
'text.usetex' : True,
'backend' :'ps',
## FONTS
"font.family" : "serif",
"font.serif" : [], # blank entries should cause plots to inherit fonts from the document
"font.monospace" : [],
## FONT SIZES
'axes.labelsize' : 12,
'font.size' : 12,
'legend.fontsize': 12,
'xtick.labelsize': 12,
'ytick.labelsize': 12,
## LINEWIDTH
'axes.linewidth' : 0.5,
'patch.linewidth': 0.5, # legend frame
'lines.linewidth': 1.5,
## LEGEND
'legend.edgecolor':'black',
'legend.frameon' :True,
'legend.fancybox' :False,
}
plt.rcParams.update(params)
# GENERATE PLOT
fig = plt.figure()
ax = fig.add_subplot(111)
for i,sin in enumerate(sin_arr):
ax.plot(x,sin,label=r'$\sin(x)$ '+str(i))
ax.set_xlabel(r'$\varepsilon$')
ax.set_ylabel(r'$\sigma$ in [MPa]', labelpad=15)
ax.set_xlim([0.,2.*np.pi])
# SHRINK PADDING
plt.tight_layout(pad=0)
# ADD LEGEND ON TOP OF AXES WITHOUT CHANGING AXES SIZE
legend = ax.legend( bbox_to_anchor=(0., 1.02, 1., .102),
loc='lower left',
ncol=3,
borderaxespad=0.,mode="expand" )
# GET LEGEND / AXES HEIGHT IN INCHES
ax_box = ax.get_window_extent() # AXES BBOX IN DISPLAY UNITS
leg_box = legend.get_bbox_to_anchor() # LEGEND BBOX IN DISPLAY UNITS
ax_box_inch = ax_box.transformed( fig.dpi_scale_trans.inverted() ) # TRANSFORM TO INCHES
leg_box_inch = leg_box.transformed( fig.dpi_scale_trans.inverted() ) # TRANSFORM TO INCHES
# ORIGINAL_AX_HEIGHT
ax_height_inch_orig = ax_box_inch.height
# CHANGE FINGURE TO FIT LEGEND
fig.set_size_inches(fig.get_figwidth(), fig.get_figheight() + leg_box_inch.height)
# GET NEW HEIGHT OF AXES
ax_box_new_inch = ax.get_window_extent().transformed( fig.dpi_scale_trans.inverted() )
ax_height_inch_new = ax_box_new_inch.height
factor = ax_height_inch_orig/ax_height_inch_new
# GET AXES BBOX IN FIGURE COORDINATES
ax_box = ax.get_window_extent().transformed( fig.transFigure.inverted() )
# CHANGE AXES TO ORIGINAL HEIHGT BUT WITH LEGEND FULLY VISIBLE
ax.set_position([ax_box.x0, ax_box.y0,ax_box.width, ax_box.height*factor])
plt.savefig('test.pdf',format='pdf',dpi=90)
It looks like you can achieve the desired outcome much easier by using savefig's bbox_inches argument.
plt.savefig("output.pdf", bbox_inches="tight", pad_inches=0)
This works if you don't need the figure for anything but saving it.
import matplotlib.pyplot as plt
import numpy as np
# GENERATE DATA
x = np.arange(-2.*np.pi,4.*np.pi,0.01)
sin_arr = [np.sin(x-dx) for dx in np.arange(0.,2.*np.pi,0.5)]
# SET FIGURE SIZE AND RCPARAMETERS
plt.rcParams.update( {'figure.figsize' : [5.90551197, 3.64980712]} )
fig, ax = plt.subplots()
for i,sin in enumerate(sin_arr):
ax.plot(x,sin,label=r'$\sin(x)$ '+str(i))
ax.set_xlabel(r'$\varepsilon$')
ax.set_ylabel(r'$\sigma$ in [MPa]', labelpad=15)
ax.set_xlim([0.,2.*np.pi])
# SHRINK PADDING
fig.tight_layout(pad=0)
# ADD LEGEND ON TOP OF AXES WITHOUT CHANGING AXES SIZE
legend = ax.legend( bbox_to_anchor=(0., 1.02, 1., .102),
loc='lower left',
ncol=3,
borderaxespad=0.,mode="expand" )
plt.savefig("output.pdf", bbox_inches="tight", pad_inches=0)
Note that if you use plt.tight_layout the resulting axes size may still be different if you use different x- or y-labels (e.g. if they sometimes contain capital letters or letters which go below the baseline, like "p" or "g"). In such case it would be better to manually decide for some parameters, and replace tight_layout with
fig.subplots_adjust(left=0.151, bottom=0.130, right=0.994, top=0.990)
or whatever other parameters work for you, given the fontsizes in use.
The problem of constant axes size is hence rather easy to solve. What would be more complicated is the inverse. Having a constant figure size, but shrinking the axes such that the figure still accommodates the legend. This would be shown in this question Creating figure with exact size and no padding (and legend outside the axes)
I have specs for a mockup, for example, Photoshop/Illustrator/Sketch file with each elements specifications like size, position, and coordinates in pixels.
An image looks like:
I can draw similar image just using standard matplotlib technic without any problems.
The question is, how to render an image exactly with its specs? All sizes, font sizes, alignments, should be the same as in the specs (as it's drawn in Illustrator).
I've researched over matplotlib docs, but even transformers tutorial doesn't help.
Update with an exact example.
I have a mock at Zeplin which shows coordinated of each plot (Image is also a plot here). So I know, that the image has margins 25x25px from a border and its size is 80x80 pixels.
this is mock image (again not allowed to embed the image).
How would you do that?
The code I use for drawing
fig, ax = plt.subplots(1, 2, figsize=(20, 10), sharey=True)
recs = ax[0].barh(y_pos, widths, align='edge');
img = mpimg.imread('/Users/iwitaly/Downloads/2017-11-12 17.40.46.jpg')
ax[0].spines['left'].set_visible(False);
ax[0].spines['right'].set_visible(False);
ax[0].spines['bottom'].set_visible(False);
ax[0].spines['top'].set_visible(False);
ax[0].get_xaxis().set_visible(True);
ax[0].get_yaxis().set_visible(True);
obj = ax[0].text(x=0, y=3.9, s=r'Name: Vitaly Davydov',
fontsize=30, fontname="Courier New", weight='bold');
ax[0].axhline(y=3.85, xmin=0, xmax=0.75, color='black');
# I'd like to place axicon exactly with 25x25 marging from top left corner
axicon = fig.add_axes([0.08, 1, 0.2, 0.2], transform=None)
axicon.axis('off');
axicon.imshow(img, interpolation='none');
for i, r in enumerate(recs):
r.set_color(index_to_color[i]);
r.set_height(col_wight);
ax[0].text(x=0, y=text_positions[i], s=index_to_text[i], fontsize=30, fontname="Courier New");
ax[1].spines['left'].set_visible(False);
ax[1].spines['right'].set_visible(False);
ax[1].spines['bottom'].set_visible(False);
ax[1].spines['top'].set_visible(False);
ax[1].get_xaxis().set_visible(False);
ax[1].get_yaxis().set_visible(False);
ax[1].text(x=0, y=3.9, s='Increment:', fontsize=30,
fontname="Courier New", weight='bold');
ax[1].axhline(y=3.85, xmin=0, xmax=0.4, color='black');
for i, r in enumerate(recs):
text_x, text_y = r.xy
increment_pos_y = text_y + col_wight / 2
if increment_values[i] > 0:
increment_text = '+{}'.format(increment_values[i])
elif increment_values[i] < 0:
increment_text = '-{}'.format(increment_values[i])
else:
increment_text = '{}'.format(increment_values[i])
ax[1].text(x=0, y=increment_pos_y, s=increment_text,
fontsize=30, color=index_to_color[i],
fontname="Courier New", weight='bold');
In this example I'd like to place axicon axes that is an image with 25x25 margin and 80x80 size (all in pixels).
To place an axes to the figure, you can use fig.add_axes([left, bottom, width, height]), where left, bottom, width, height are fractions of the figure size.
To convert from pixels to fraction of figure size, you need to divide the pixels by the figure dpi and the figure size. E.g. for the left edge
left = 25/fig.dpi/fig.get_size_inches()[0]
Complete example:
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(1, 2, figsize=(10, 7))
y_pos, widths = np.linspace(0,3,4), np.random.rand(4)
recs = ax[0].barh(y_pos, widths, align='edge');
#img = plt.imread('/Users/iwitaly/Downloads/2017-11-12 17.40.46.jpg')
img = np.random.rand(80,80)
# I'd like to place axicon exactly with 25x25 marging from top left corner
x,y= 25,25 #pixels
dx,dy = 80,80
w,h = fig.get_size_inches()
axicon = fig.add_axes([x/float(fig.dpi)/w, 1.-(y+dy)/float(fig.dpi)/h,
dx/float(fig.dpi)/w, dy/float(fig.dpi)/h])
axicon.axis('off');
axicon.imshow(img, interpolation='none');
plt.show()
I am using python 3.5.2
I would like to make a pie chart with an png image imbedded. I have pictures of certain bulk products that I would like to insert into the slices. For example strawberries in one slice and raspberries in another. Much like the picture http://www.python-course.eu/images/pie_chart_with_raspberries.png shows.
I can produce images and even plot images instead of points as demonstrated here Matplotlib: How to plot images instead of points?
However, I could not find any approach towards what I am proposing. I suppose it could be manually done in paint, but I was trying to avoid that.
That is sure possible. We can start with a normal pie chart. Then we would need to get the images into the plot. This is done using plt.imread and by using a matplotlib.offsetbox.OffsetImage. We would need to find good coordinates and zoom levels to place the image, such that it overlapps completely with respective pie wedge. Then the Path of the pie's wedge is used as a clip path of the image, such that only the part inside the wedge is left over. Setting the zorder of the unfilled wedge to a high number ensures the borders to be placed on top of the image. This way it looks like the wedges are filled with the image.
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
total = [5,7,4]
labels = ["Raspberries", "Blueberries", "Blackberries"]
plt.title('Berries')
plt.gca().axis("equal")
wedges, texts = plt.pie(total, startangle=90, labels=labels,
wedgeprops = { 'linewidth': 2, "edgecolor" :"k","fill":False, })
def img_to_pie( fn, wedge, xy, zoom=1, ax = None):
if ax==None: ax=plt.gca()
im = plt.imread(fn, format='png')
path = wedge.get_path()
patch = PathPatch(path, facecolor='none')
ax.add_patch(patch)
imagebox = OffsetImage(im, zoom=zoom, clip_path=patch, zorder=-10)
ab = AnnotationBbox(imagebox, xy, xycoords='data', pad=0, frameon=False)
ax.add_artist(ab)
positions = [(-1,0.3),(0,-0.5),(0.5,0.5)]
zooms = [0.4,0.4,0.4]
for i in range(3):
fn = "data/{}.png".format(labels[i].lower())
img_to_pie(fn, wedges[i], xy=positions[i], zoom=zooms[i] )
wedges[i].set_zorder(10)
plt.show()
I would like to plot a number of curves over an image
Using this code I am reasonably close:
G=plt.matplotlib.gridspec.GridSpec(64,1)
fig = plt.figure()
plt.imshow(img.data[:,:],cmap='gray')
plt.axis('off')
plt.axis([0,128,0,64])
for i in arange(64):
fig.add_subplot(G[i,0])
plt.axis('off')
# note that vtc.data.shape = (64, 128*400=51200)
# so every trace for each image pixel is 400 points long
plt.plot(vtc.data[i,:])
plt.axis([0, 51200, 0, 5])
The result that I am getting looks like this:
The problem is that while I seem to be able to get rid of all the padding in the horizontal (x) direction, there is different amount of padding in the image and the stacked plots in the vertical direction.
I tried using
ax = plt.gca()
ax.autoscale_view('tight')
but that didn't reduce the margin either.
How can I get a grid of m-by-n line plots to line up precisely with a blown up (by factor f) version of an image with dimensions (fm)-by-(fn)?
UPDATE and Solution:
The answer by #RutgerKassies works quite well. I achieved it using his code like so:
fig, axs = plt.subplots(1,1,figsize=(8,4))
axs.imshow(img.data[:,:],cmap='gray', interpolation='none')
nplots = 64
fig.canvas.draw()
box = axs._position.bounds
height = box[3] / nplots
for i in arange(nplots):
tmpax = fig.add_axes([box[0], box[1] + i * height, box[2], height])
tmpax.set_axis_off()
# make sure to get image orientation right and
tmpax.plot(vtc.data[nplots-i-1,:],alpha=.3)
tmpax.set_ylim(0,5)
tmpax.set_xlim(0, 51200)
I think the easiest way is to use the boundaries from your 'imshow axes' to manually calculate the boundaries of all your 'lineplot axes':
import matplotlib.pyplot as plt
import numpy as np
fig, axs = plt.subplots(1,1,figsize=(15,10))
axs.imshow(np.random.rand(50,100) ,cmap='gray', interpolation='none', alpha=0.3)
nplots = 50
fig.canvas.draw()
box = axs._position.bounds
height = box[3] / nplots
for i in arange(nplots):
tmpax = fig.add_axes([box[0], box[1] + i * height, box[2], height])
tmpax.set_axis_off()
tmpax.plot(np.sin(np.linspace(0,np.random.randint(20,1000),1000))*0.4)
tmpax.set_ylim(-1,1)
The above code seems nice, but i do have some issues with the autoscale chopping off part of the plot. Try removing the last line to see the effect, im not sure why thats happening.