how to make a graph fill all the window - python

I am ploting a graph in an application created with QtDesigner, the problem is that, when the grapth is showed, a big "grey edge" appears between the graph space and the mplwidget space. That makes the plot smaller, so how could I delete this big "grey border" that appears when I show my graph in the main Window??
I would like my graph to fill all the available space for the widget.

The short answer is: "Use fig.tight_layout()."
Let me give a bit more explanation about what's going on, though.
You're seeing the interaction between the figure and the axes.
A Figure contains one or more Axes (plots/subplots/etc). Everything is drawn on the figure's Canvas (basically, the backend-specific pixel buffer or vector page).
When you make an axes, it does not fill up all of the figure.
The default for a single axes is for the lower left corner of the axes to be a 12.5% of the width of the figure and 10% of the height and for the axes to take up 90% of the width and height of the figure. (The asymmetry is to leave room for the tick labels on the left.)
The position you set is the extent of the white box in the figure below. It doesn't include the tick labels, title, axes labels, etc (which is why the axes doesn't fill up the entire figure).
The default will look like this:
Side note: To keep the code short, I'm going to use the pyplot interface to automatically generate a Figure, Axes, and Canvas, while you're probably explicitly creating each one to work with your gui framework. The result is the same, though.
The percentage of the figure that each axes instance takes up is set at the time that it's created. You can either explicitly specify it:
fig = plt.figure()
ax = fig.add_axes([left, bottom, width, height])
Or use a grid of subplots, which can be easier to adjust through fig.subplots_adjust:
fig, axes = plt.subplots(nrows=2, ncols=2)
# Expand the grid of subplots out (Notice that
fig.subplots_adjust(left=0.05, bottom=0.05, right=0.98, top=0.98)
What tight_layout does is to calculate the extent of the tick labels, title, axis labels, etc and determine parameters for fig.subplots_adjust such that everything will be just barely inside the figure. (Remember that subplots_adjust and the axes position specification control the extent of the "white box" -- the actual axes itself -- and doesn't include the tick labels, etc.)
So, if we do just what we did before:
fig, ax = plt.subplots()
And then call:
fig.tight_layout()
We'll get something that has less of a "border", as the axes will take up a larger percentage of the figure:
If you want to control the color of the "border" use fig.set_facecolor(color) (or fig.patch.set_facecolor).

#newPyUser, if you have an embedded graph as self.ui.mplwidget do
self.ui.mplwidget.canvas.fig.tight_layout()
assuming your widgets are defined like here:
from PyQt4 import QtGui
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
class MplCanvas(FigureCanvas):
def __init__(self):
self.fig = Figure()
self.ax = self.fig.add_subplot(111)
FigureCanvas.__init__(self, self.fig)
FigureCanvas.setSizePolicy(self,
QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
class MplWidget(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.canvas = MplCanvas()
self.vbl = QtGui.QVBoxLayout()
self.vbl.addWidget(self.canvas)
self.setLayout(self.vbl)

With an embedded graph as self.ui.mplwidget, a very simple self.ui.mplwidget.figure.tight_layout() will do the job, without defining a canvas before.
If you have other features on the graph such as axis labels or title, just be sure to put the self.ui.mplwidget.figure.tight_layout() after those.

Related

How to add border or frame around individual subplots

I want to create an image like this, but I'm unable to put the individual plots inside a frame.
Figures and axes have a patch attribute, which is the rectangle that makes up the background. Setting a figure frame is hence pretty straightforward:
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 1)
# add a bit more breathing room around the axes for the frames
fig.subplots_adjust(top=0.85, bottom=0.15, left=0.2, hspace=0.8)
fig.patch.set_linewidth(10)
fig.patch.set_edgecolor('cornflowerblue')
# When saving the figure, the figure patch parameters are overwritten (WTF?).
# Hence we need to specify them again in the save command.
fig.savefig('test.png', edgecolor=fig.get_edgecolor())
Now the axes are a much tougher nut to crack. We could use the same approach as for the figure (which #jody-klymak I think is suggesting), however, the patch only corresponds to the area that is inside the axis limits, i.e. it does not include the tick labels, axis labels, nor the title.
However, axes have a get_tightbbox method, which is what we are after. However, using that also has some gotchas, as explained in the code comments.
# We want to use axis.get_tightbbox to determine the axis dimensions including all
# decorators, i.e. tick labels, axis labels, etc.
# However, get_tightbox requires the figure renderer, which is not initialized
# until the figure is drawn.
plt.ion()
fig.canvas.draw()
for ii, ax in enumerate(axes):
ax.set_title(f'Title {ii+1}')
ax.set_ylabel(f'Y-Label {ii+1}')
ax.set_xlabel(f'X-Label {ii+1}')
bbox = ax.get_tightbbox(fig.canvas.get_renderer())
x0, y0, width, height = bbox.transformed(fig.transFigure.inverted()).bounds
# slightly increase the very tight bounds:
xpad = 0.05 * width
ypad = 0.05 * height
fig.add_artist(plt.Rectangle((x0-xpad, y0-ypad), width+2*xpad, height+2*ypad, edgecolor='red', linewidth=3, fill=False))
fig.savefig('test2.png', edgecolor=fig.get_edgecolor())
plt.show()
I found something very similar and somehow configured it out what its doing .
autoAxis1 = ax8i[1].axis() #ax8i[1] is the axis where we want the border
import matplotlib.patches as ptch
rec = ptch.Rectangle((autoAxis1[0]-12,autoAxis1[2]-30),(autoAxis1[1]-
autoAxis1[0])+18,(autoAxis1[3]-
autoAxis1[2])+35,fill=False,lw=2,edgecolor='cyan')
rec = ax8i[1].add_patch(rec)
rec.set_clip_on(False)
The code is a bit complex but once we get to know what part of the bracket inside the Rectangle() is doing what its quite easy to get the code .

matplotlib colorbar update/remove destroys axes layout

I have matplotlib embedded in a tkinter gui. With the seaborn heatmap function, I create a heatmap and a colorbar which works as I want it to when creating the first plot. However, if I plot again, this will not overwrite the colorbar but add another colorbar to my figure. I end up with to many colorbars this way.
The figure I create for the plot contains two axes:
[<matplotlib.axes._subplots.AxesSubplot object at 0x000001F36C8C3390>, <matplotlib.axes._subplots.AxesSubplot object at 0x000001F36D6ABF98>]
the first one is the plot itself and the second the colorbar and looks like this:
plot 1
If I plot again, the result is:
plot 2
Simply deleting the colorbar with
self.fig.axes[1].remove()
before creating the next plot doesn't do the trick because it will just remove the colorbar but the layout of the plot keeps shrinking:
plot 3
plot 4
Note that the figure size stays the same but the size of the plot keeps getting smaller when I plot again and the colorbar moves futher to the left while the entire right part of the plot stays white.
As I create a tkinter gui, the ploting window is initialized when the program is first run.
self.fig = plt.figure.Figure( facecolor = "white", figsize = (7,4))
self.ax = self.fig.subplots()
self.x_data = x_data
self.y_data = y_data
when somebody presses a plot button the plot is created
def plot_on_plotframe(self):
self.ax.cla()
#executes required matplotlib layout
self.plotlayout[self.plot_type]()
print('plottype: {}'.format(self.plot_type))
#print('Plot xdata: {}'.format(self.x_data))
self.canvas.draw()
I need to make different types of plot and the proper type is selected by plotlayout:
self.ax = sns.heatmap(self.x_data, vmin=self.settings[2][0], vmax=self.settings[2][1], cmap='viridis', fmt=self.settings[0], annot=self.settings[1], linewidths=0.5, annot_kws={'size': 8}, ax = self.ax)
self.ax.set_xticklabels(self.ax.get_xticklabels(), rotation=0)
self.fig.tight_layout()
It would be fantastic if somebody could tell me why matplotlib messes with the layout even after I delete the old colorbar in the first place. I think this has something to do with the gridSpec but I don't get how I tell matplotlib to reset the layout properly. Also any other suggestions on how to resovle this?
Thanks in advance

Cannot set spine line style with matplotlib

I try to set the line style of matplotlib plot spines, but for some reason, it does not work. I can set them invisible or make them thinner, but I cannot change the line style.
My aim is to present one plot cut up into two to show outliers at the top. I would like to set the respective bottom/top spines to dotted so they clearly show that there is a break.
import numpy as np
import matplotlib.pyplot as plt
# Break ratio of the bottom/top plots respectively
ybreaks = [.25, .9]
figure, (ax1, ax2) = plt.subplots(
nrows=2, ncols=1,
sharex=True, figsize=(22, 10),
gridspec_kw = {'height_ratios':[1 - ybreaks[1], ybreaks[0]]}
)
d = np.random.random(100)
ax1.plot(d)
ax2.plot(d)
# Set the y axis limits
ori_ylim = ax1.get_ylim()
ax1.set_ylim(ori_ylim[1] * ybreaks[1], ori_ylim[1])
ax2.set_ylim(ori_ylim[0], ori_ylim[1] * ybreaks[0])
# Spine formatting
# ax1.spines['bottom'].set_visible(False) # This works
ax1.spines['bottom'].set_linewidth(.25) # This works
ax1.spines['bottom'].set_linestyle('dashed') # This does not work
ax2.spines['top'].set_linestyle('-') # Does not work
ax2.spines['top'].set_linewidth(.25) # Works
plt.subplots_adjust(hspace=0.05)
I would expect the above code to draw the top plot's bottom spine and the bottom plot's top spine dashed.
What do I miss?
First one should mention that if you do not change the linewidth, the dashed style shows fine.
ax1.spines['bottom'].set_linestyle("dashed")
However the spacing may be a bit too tight. This is due to the capstyle being set to "projecting" for spines by default.
One can hence set the capstyle to "butt" instead (which is also the default for normal lines in plots),
ax1.spines['bottom'].set_linestyle('dashed')
ax1.spines['bottom'].set_capstyle("butt")
Or, one can separate the dashes further. E.g.
ax1.spines['bottom'].set_linestyle((0,(4,4)))
Now, if you also set the linewidth so something smaller, then you would need proportionally more spacing. E.g.
ax1.spines['bottom'].set_linewidth(.2)
ax1.spines['bottom'].set_linestyle((0,(16,16)))
Note that the line does not actually become thinner on screen due to the antialiasing in use. It just washes out, such that it becomes lighter in color. So in total it may make sense to keep the lineswidth at some 0.72 points (0.72 points = 1 pixel at 100 dpi) and change the color to light gray instead.

imshow() subplots generate unwanted white spaces

When plotting two (or more) subplots, there is a large areas of white spaces within the plots (on all four sides) as seen here:
Following is the code which I used to plot it.
from pylab import *
from matplotlib import rc, rcParams
import matplotlib.pyplot as plt
for kk in range(57,58):
fn_i=str(kk)
image_file_1='RedshiftOutput00'+fn_i+'_Slice_z_RadioPowerDSA.png'
image_file_2='RedshiftOutput00'+fn_i+'_Slice_z_RadioPowerTRA.png'
image_file_3='RedshiftOutput00'+fn_i+'_Slice_z_RadioPowerDSA+TRA.png'
image_1 = plt.imread(image_file_1)
image_2 = plt.imread(image_file_2)
image_3 = plt.imread(image_file_3)
ax1 = subplot(131)
plt.imshow(image_1)
plt.axis('off') # clear x- and y-axes
ax2 = subplot(132)
plt.imshow(image_2)
plt.axis('off') # clear x- and y-axes
ax3 = subplot(133)
plt.imshow(image_3)
plt.axis('off') # clear x- and y-axes
plt.savefig('RedshiftOutput00'+fn_i+'_all.png')
I am also uploading the 3 images used in this code to making the code a Minimal Working Example
1) https://drive.google.com/file/d/0B6l5iRWTUbHWSTF2R3E1THBGeVk/view?usp=sharing
2) https://drive.google.com/file/d/0B6l5iRWTUbHWaFI4dHAzcWpiOEU/view?usp=sharing
3) https://drive.google.com/file/d/0B6l5iRWTUbHWaG8xclFlcGJNaUk/view?usp=sharing
How we can remove this white space ? I tried by fixing the whole plot size, still white space is comming.
Mel's comment above (use plt.tight_layout()) works in many situations, but sometimes you need a little more control. To manipulate axes more finely (useful, e.g., when you have lots of colorbars or twin-ned axes), you can use plt.subplots_adjust() or a GridSpec object.
GridSpec objects allow you to specify the horizontal and vertical extents of individual axes, as well as their proportional width and height & spacing. subplots_adjust() moves your axes around after you've already plotted stuff on them. I prefer using the first option, but both are documented well.
It also may help to fool around with the size of your figure. If you have lots of whitespace width-wise, make the width of the figure smaller.
Here's some example code that I used to set up a recent plot:
gs = gridspec.GridSpec(
nrows=1, ncols=3, left=0.1, bottom=0.25, right=0.95, top=0.95,
wspace=0.05, hspace=0., width_ratios=[1, 1, 1])
NII_ax = plt.subplot(gs[0])
SII_ax = plt.subplot(gs[1])
OI_ax = plt.subplot(gs[2])
And the result:
Then, if you need a colorbar, adjust the right argument in GridSpec to something like 0.85, and use fig.add_axes() with a list [left_lim, bottom, width, height] and use that as the axis argument for a fig.colorbar()

Matplotlib legend vertical rotation

Does someone perhaps know if it is possible to rotate a legend on a plot in matplotlib? I made a simple plot with the below code, and edited the graph in paint to show what I want.
plt.plot([4,5,6], label = 'test')
ax = plt.gca()
ax.legend()
plt.show()
I went to a similar problem and solved it by writing the function legendAsLatex that generates a latex code to be used as the label of the y-axis. The function gathers the color, the marker, the line style, and the label provided to the plot function. It requires enabling the latex and loading the required packages. Here is the code to generate your plot with extra curves that use both vertical axis.
from matplotlib import pyplot as plt
import matplotlib.colors as cor
plt.rc('text', usetex=True)
plt.rc('text.latex', preamble=r'\usepackage{amsmath} \usepackage{wasysym}'+
r'\usepackage[dvipsnames]{xcolor} \usepackage{MnSymbol} \usepackage{txfonts}')
def legendAsLatex(axes, rotation=90) :
'''Generate a latex code to be used instead of the legend.
Uses the label, color, marker and linestyle provided to the pyplot.plot.
The marker and the linestyle must be defined using the one or two character
abreviations shown in the help of pyplot.plot.
Rotation of the markers must be multiple of 90.
'''
latexLine = {'-':'\\textbf{\Large ---}',
'-.':'\\textbf{\Large --\:\!$\\boldsymbol{\cdot}$\:\!--}',
'--':'\\textbf{\Large --\,--}',':':'\\textbf{\Large -\:\!-}'}
latexSymbol = {'o':'medbullet', 'd':'diamond', 's':'filledmedsquare',
'D':'Diamondblack', '*':'bigstar', '+':'boldsymbol{\plus}',
'x':'boldsymbol{\\times}', 'p':'pentagon', 'h':'hexagon',
',':'boldsymbol{\cdot}', '_':'boldsymbol{\minus}','<':'LHD',
'>':'RHD','v':'blacktriangledown', '^':'blacktriangle'}
rot90=['^','<','v','>']
di = [0,-1,2,1][rotation%360//90]
latexSymbol.update({rot90[i]:latexSymbol[rot90[(i+di)%4]] for i in range(4)})
return ', '.join(['\\textcolor[rgb]{'\
+ ','.join([str(x) for x in cor.to_rgb(handle.get_color())]) +'}{'
+ '$\\'+latexSymbol.get(handle.get_marker(),';')+'$'
+ latexLine.get(handle.get_linestyle(),'') + '} ' + label
for handle,label in zip(*axes.get_legend_handles_labels())])
ax = plt.axes()
ax.plot(range(0,10), 'b-', label = 'Blue line')
ax.plot(range(10,0,-1), 'sm', label = 'Magenta squares')
ax.set_ylabel(legendAsLatex(ax))
ax2 = plt.twinx()
ax2.plot([x**0.5 for x in range(0,10)], 'ro', label = 'Red circles')
ax2.plot([x**0.5 for x in range(10,0,-1)],'g--', label = 'Green dashed line')
ax2.set_ylabel(legendAsLatex(ax2))
plt.savefig('legend.eps')
plt.close()
Figure generated by the code:
I spent a few hours chipping away at this yesterday, and made a bit of progress so I'll share that below along with some suggestions moving forward.
First, it seems that we can certainly rotate and translate the bounding box (bbox) or frame around the legend. In the first example below you can see that a transform can be applied, albeit requiring some oddly large translation numbers after applying the 90 degree rotation. But, there are actually problems saving the translated legend frame to an image file so I had to take a screenshot from the IPython notebook. I've added some comments as well.
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import matplotlib.transforms
fig = plt.figure()
ax = fig.add_subplot('121') #make room for second subplot, where we are actually placing the legend
ax2 = fig.add_subplot('122') #blank subplot to make space for legend
ax2.axis('off')
ax.plot([4,5,6], label = 'test')
transform = matplotlib.transforms.Affine2D(matrix=np.eye(3)) #start with the identity transform, which does nothing
transform.rotate_deg(90) #add the desired 90 degree rotation
transform.translate(410,11) #for some reason we need to play with some pretty extreme translation values to position the rotated legend
legend = ax.legend(bbox_to_anchor=[1.5,1.0])
legend.set_title('test title')
legend.get_frame().set_transform(transform) #This actually works! But, only for the frame of the legend (see below)
frame = legend.get_frame()
fig.subplots_adjust(wspace = 0.4, right = 0.9)
fig.savefig('rotate_legend_1.png',bbox_extra_artists=(legend,frame),bbox_inches='tight', dpi = 300) #even with the extra bbox parameters the legend frame is still getting clipped
Next, I thought it would be smart to explore the get_methods() of other legend components. You can sort of dig through these things with dir(legend) and legend.__dict__ and so on. In particular, I noticed that you can do this: legend.get_title().set_transform(transform), which would seem to imply that we could translate the legend text (and not just the frame as above). Let's see what happens when I tried that:
fig2 = plt.figure()
ax = fig2.add_subplot('121')
ax2 = fig2.add_subplot('122')
ax2.axis('off')
ax.plot([4,5,6], label = 'test')
transform = matplotlib.transforms.Affine2D(matrix=np.eye(3))
transform.rotate_deg(90)
transform.translate(410,11)
legend = ax.legend(bbox_to_anchor=[1.5,1.0])
legend.set_title('test title')
legend.get_frame().set_transform(transform)
legend.get_title().set_transform(transform) #one would expect this to apply the same transformation to the title text in the legend, rotating it 90 degrees and translating it
frame = legend.get_frame()
fig2.subplots_adjust(wspace = 0.4, right = 0.9)
fig2.savefig('rotate_legend_1.png',bbox_extra_artists=(legend,frame),bbox_inches='tight', dpi = 300)
The legend title seems to have disappeared in the screenshot from the IPython notebook. But, if we look at the saved file the legend title is now in the bottom left corner and seems to have ignored the rotation component of the transformation (why?):
I had similar technical difficulties with this type of approach:
bbox = matplotlib.transforms.Bbox([[0.,1],[1,1]])
trans_bbox = matplotlib.transforms.TransformedBbox(bbox, transform)
legend.set_bbox_to_anchor(trans_bbox)
Other notes and suggestions:
It might be a sensible idea to dig into the differences in behaviour between the legend title and frame objects--why do they both accept transforms, but only the frame accepts a rotation? Perhaps it would be possible to subclass the legend object in the source code and make some adjustments.
We also need to find a solution for the rotated / translated legend frame not being saved to output, even after following various related suggestion on SO (i.e., Matplotlib savefig with a legend outside the plot).

Categories