This question already has answers here:
Is there a way to make a discontinuous axis in Matplotlib?
(7 answers)
Closed 5 years ago.
Best way to describe what I want to achieve is using my own image:
Now I have a lot of dead space in the spectra plot, especially between 5200 and 6300. My question is quite simple, how would I add in a nice little // break that looks something similar to this (image lifted from the net):
I'm using this setup for my plots:
nullfmt = pyplot.NullFormatter()
fig = pyplot.figure(figsize=(16,6))
gridspec_layout1= gridspec.GridSpec(2,1)
gridspec_layout1.update(left=0.05, right=0.97, hspace=0, wspace=0.018)
pyplot_top = fig.add_subplot(gridspec_layout1[0])
pyplot_bottom = fig.add_subplot(gridspec_layout1[1])
pyplot_top.xaxis.set_major_formatter(nullfmt)
I'm quite certain it is achievable with gridpsec but an advanced tutorial cover exactly how this is achieved would be greatly appreciated.
Apologies also if this question has been dealt with previously on stackoverflow but I have looked extensively for the correct procedure for gridSpec but found nothing as yet.
I have managed to go as far as this, pretty much there:
However, my break lines are not as steep as I would like them...how do I change them? (I have made use of the example answer below)
You could adapt the matplotlib example for a break in the x-axis directly:
"""
Broken axis example, where the x-axis will have a portion cut out.
"""
import matplotlib.pylab as plt
import numpy as np
x = np.linspace(0,10,100)
x[75:] = np.linspace(40,42.5,25)
y = np.sin(x)
f,(ax,ax2) = plt.subplots(1,2,sharey=True, facecolor='w')
# plot the same data on both axes
ax.plot(x, y)
ax2.plot(x, y)
ax.set_xlim(0,7.5)
ax2.set_xlim(40,42.5)
# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labelright='off')
ax2.yaxis.tick_right()
# This looks pretty good, and was fairly painless, but you can get that
# cut-out diagonal lines look with just a bit more work. The important
# thing to know here is that in axes coordinates, which are always
# between 0-1, spine endpoints are at these locations (0,0), (0,1),
# (1,0), and (1,1). Thus, we just need to put the diagonals in the
# appropriate corners of each of our axes, and so long as we use the
# right transform and disable clipping.
d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((1-d,1+d), (-d,+d), **kwargs)
ax.plot((1-d,1+d),(1-d,1+d), **kwargs)
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,+d), (1-d,1+d), **kwargs)
ax2.plot((-d,+d), (-d,+d), **kwargs)
# What's cool about this is that now if we vary the distance between
# ax and ax2 via f.subplots_adjust(hspace=...) or plt.subplot_tool(),
# the diagonal lines will move accordingly, and stay right at the tips
# of the spines they are 'breaking'
plt.show()
For your purposes, just plot your data twice (once on each axis, ax and ax2 and set your xlims appropriately. The "break lines" should move to match the new break because they are plotted in relative axis coordinates rather than data coordinates.
The break lines are just unclipped plot lines drawn between a pair of points. E.g. ax.plot((1-d,1+d), (-d,+d), **kwargs) plots the break line between point (1-d,-d) and (1+d,+d) on the first axis: this is the bottom righthand one. If you want to change the graident, change these values appropriately. For example, to make this one steeper, try ax.plot((1-d/2,1+d/2), (-d,+d), **kwargs)
The solution provided by xnx is a good start, but there is a remaining issue that the scales of the x-axes are different between the plots. This is not a problem if the range in the left plot and the range in the right plot are the same, but if they are unequal, subplot will still give the two plots equal width, so the x-axis scale will be different between the two plots (as is the case with xnx's example). I made a package, brokenaxes to deal with this.
Related
In a previous answer it was recommended to me to use add_subplot instead of add_axes to show axes correctly, but searching the documentation I couldn't understand when and why I should use either one of these functions.
Can anyone explain the differences?
Common grounds
Both, add_axes and add_subplot add an axes to a figure. They both return a (subclass of a) matplotlib.axes.Axes object.
However, the mechanism which is used to add the axes differs substantially.
add_axes
The calling signature of add_axes is add_axes(rect), where rect is a list [x0, y0, width, height] denoting the lower left point of the new axes in figure coodinates (x0,y0) and its width and height. So the axes is positionned in absolute coordinates on the canvas. E.g.
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
places a figure in the canvas that is exactly as large as the canvas itself.
add_subplot
The calling signature of add_subplot does not directly provide the option to place the axes at a predefined position. It rather allows to specify where the axes should be situated according to a subplot grid. The usual and easiest way to specify this position is the 3 integer notation,
fig = plt.figure()
ax = fig.add_subplot(231)
In this example a new axes is created at the first position (1) on a grid of 2 rows and 3 columns. To produce only a single axes, add_subplot(111) would be used (First plot on a 1 by 1 subplot grid). (In newer matplotlib versions, add_subplot() without any arguments is possible as well.)
The advantage of this method is that matplotlib takes care of the exact positioning. By default add_subplot(111) would produce an axes positioned at [0.125,0.11,0.775,0.77] or similar, which already leaves enough space around the axes for the title and the (tick)labels. However, this position may also change depending on other elements in the plot, titles set, etc.
It can also be adjusted using pyplot.subplots_adjust(...) or pyplot.tight_layout().
In most cases, add_subplot would be the prefered method to create axes for plots on a canvas. Only in cases where exact positioning matters, add_axes might be useful.
Example
import matplotlib.pyplot as plt
plt.rcParams["figure.figsize"] = (5,3)
fig = plt.figure()
fig.add_subplot(241)
fig.add_subplot(242)
ax = fig.add_subplot(223)
ax.set_title("subplots")
fig.add_axes([0.77,.3,.2,.6])
ax2 =fig.add_axes([0.67,.5,.2,.3])
fig.add_axes([0.6,.1,.35,.3])
ax2.set_title("random axes")
plt.tight_layout()
plt.show()
Alternative
The easiest way to obtain one or more subplots together with their handles is plt.subplots(). For one axes, use
fig, ax = plt.subplots()
or, if more subplots are needed,
fig, axes = plt.subplots(nrows=3, ncols=4)
The initial question
In the initial question an axes was placed using fig.add_axes([0,0,1,1]), such that it sits tight to the figure boundaries. The disadvantage of this is of course that ticks, ticklabels, axes labels and titles are cut off. Therefore I suggested in one of the comments to the answer to use fig.add_subplot as this will automatically allow for enough space for those elements, and, if this is not enough, can be adjusted using pyplot.subplots_adjust(...) or pyplot.tight_layout().
The answer by #ImportanceOfBeingErnest is great.
Yet in that context usually one want to generate an axes for a plot and add_axes() has too much overhead.
So one trick is, as in the answer of #ImportanceOfBeingErnest, is to use add_subplot(111).
Yet more elegant alternative and simple would be:
hAx = plt.figure(figsize = (10, 10)).gca()
If you want 3D projection you can pass any axes property. For instance the projection:
hAx = plt.figure(figsize = (16, 10)).gca(projection = '3d')
I want to draw a line or some kind of separator between subplots.
I am well aware of this question, but I couldn't get the solution given there to work, as I explain below.
I (sort of) understand these transformations, but the fundamental question I have, which would clarify everything else, is: is there a way to identify the "real" bottom left of each subplot as the (0,0) point of some transformation? Or, once you start adding titles, labels, etc, can these items mes up the coordinate system, eg an axis label may end up with y coordinates < 0?
Plotting a line using the plot() method of each axis, and using transform=axis.transAxes, so that (0,0) is the bottom left and (1,1) is the top right does draw a line, but:
drawing it from (0,0) to (1,0) draws it at the bottom of the axis; I need it farther below, below the x_axis label etc
determining the exact coordinates where to plot it, exactly how much below, is cumbersome, as that depends on what elements are present in the figure: titles,
labels etc.
The line end ups beneath the other elements of the
figure, and the output is very messy
The second solution given at that link uses blended_transform_factory , but, to be honest, I couldn't get the syntax to work.
A toy example is:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
sns.set(style='darkgrid')
n = int(100)
x = np.arange(0,n)
fig, ax = plt.subplots(3,2)
for i,a in enumerate(ax.flatten() ):
y = np.random.rand(n)
sns.lineplot(x, y, ax=a)
a.set_title('Chart # ' + str(i+1))
a.set_xlabel('my x')
a.set_ylabel('my y')
# this is the first solution; messy output, line behind other elements of the figure
a.plot([-1, 1.5], [-0.2, -0.2], color='black', lw=0.5, transform=a.transAxes, clip_on=False)
The second solution would mean replacing the last line with those below (always within the for loop):
trans = matplotlib.transforms.blended_transform_factory(fig.transFigure, a.transAxes)
line = matplotlib.lines.Line2D([0, 1], [0,0], color='w', transform=trans)
fig.lines.append(line)
I must have done this incorrectly, because, while the first solution at least draws something, this draws nothing at all - but no errors are given.
I am new to matplotlib and trying to create and save plots from pandas dataframes via a loop. Each plot should have an identical x-axis, but different y-axis lengths and labels. I have no problem creating and saving the plots with different y-axis lengths and labels, but when I create the plots, matplotlib rescales the x-axis depending on how much space is needed for the y-axis labels on the left side of the figure.
These figures are for a technical report. I plan to place one on each page of the report and I would like to have all of the x-axes take up the same amount of space on the page.
Here is an MSPaint version of what I'm getting and what I'd like to get.
Hopefully this is enough code to help. I'm sure there are lots of non-optimal parts of this.
import pandas as pd
import matplotlib.pyplot as plt
import pylab as pl
from matplotlib import collections as mc
from matplotlib.lines import Line2D
import seaborn as sns
# elements for x-axis
start = -1600
end = 2001
interval = 200 # x-axis tick interval
xticks = [x for x in range(start, end, interval)] # create x ticks
# items needed for legend construction
lw_bins = [0,10,25,50,75,90,100] # bins for line width
lw_labels = [3,6,9,12,15,18] # line widths
def make_proxy(zvalue, scalar_mappable, **kwargs):
color = 'black'
return Line2D([0, 1], [0, 1], color=color, solid_capstyle='butt', **kwargs)
# generic image ID
img_path = r'C:\\Users\\user\\chart'
img_ID = 0
for line_subset in data:
# create line collection for this run through loop
lc = mc.LineCollection(line_subset)
# create plot and set properties
sns.set(style="ticks")
sns.set_context("notebook")
fig, ax = pl.subplots(figsize=(16, len(line_subset)*0.5)) # I want the height of the figure to change based on number of labels on y-axis
# Figure width should stay the same
ax.add_collection(lc)
ax.set_xlim(left=start, right=end)
ax.set_xticks(xticks)
ax.set_ylim(0, len(line_subset)+1)
ax.margins(0.05)
sns.despine(left=True)
ax.xaxis.set_ticks_position('bottom')
ax.set_yticks(line_subset['order'])
ax.set_yticklabels(line_subset['ylabel'])
ax.tick_params(axis='y', length=0)
# legend
proxies = [make_proxy(item, lc, linewidth=item) for item in lw_labels]
ax.legend(proxies, ['0-10%', '10-25%', '25-50%', '50-75%', '75-90%', '90-100%'], bbox_to_anchor=(1.05, 1.0),
loc=2, ncol=2, labelspacing=1.25, handlelength=4.0, handletextpad=0.5, markerfirst=False,
columnspacing=1.0)
# title
ax.text(0, len(line_subset)+2, s=str(img_ID), fontsize=20)
# save as .png images
plt.savefig(r'C:\\Users\\user\\Desktop\\chart' + str(img_ID) + '.png', dpi=300, bbox_inches='tight')
Unless you use an axes of specifically defined aspect ratio (like in an imshow plot or by calling .set_aspect("equal")), the space taken by the axes should only depend on the figure size along that direction and the spacings set to the figure.
You are therefore pretty much asking for the default behaviour and the only thing that prevents you from obtaining that is that you use bbox_inches='tight' in the savefig command.
bbox_inches='tight' will change the figure size! So don't use it and the axes will remain constant in size. `
Your figure size, defined like figsize=(16, len(line_subset)*0.5) seems to make sense according to what I understand from the question. So what remains is to make sure the axes inside the figure are the size you want them to be. You can do that by manually placing it using fig.add_axes
fig.add_axes([left, bottom, width, height])
where left, bottom, width, height are in figure coordinates ranging from 0 to 1. Or, you can adjust the spacings outside the subplot using subplots_adjust
plt.subplots_adjust(left, bottom, right, top)
To get matching x axis for the subplots (same x axis length for each subplot) , you need to share the x axis between subplots.
See the example here https://matplotlib.org/examples/pylab_examples/shared_axis_demo.html
I have following graph:
However, I want that graphs 221 and 223 share the same x axis. I have the following code:
self.fig_part_1 = plt.figure()
self.plots_part_1 = [
plt.subplot(221),
plt.subplot(223),
plt.subplot(122),
]
How can I achieve that? In the end I do not want the numbers of axis x in plot 221 to be shown.
(This is mostly a comment to #H. Rev. but I post it as an "answer" to get nicer code formatting)
I think it is way better to just add the subplots manually, since as you implemented it now it will give two axes that you just throw away. They might even give problems with overlapping axis-ticks and a lot of confusion in general. I believe it is better to create the figure first, and then add axes one by one. This way also solves the problem by having to "update" the current figure with plt.figure(self.f.number) since you have direct access to e.g. fig_N
import matplotlib.pyplot as plt
fig1 = plt.figure()
# fig2 = plt.figure() # more figures are easily accessible
# fig3 = plt.figure() # more figures are easily accessible
ax11 = fig1.add_subplot(221) # add subplot into first position in a 2x2 grid (upper left)
ax12 = fig1.add_subplot(223, sharex=ax11) # add to third position in 2x2 grid (lower left) and sharex with ax11
ax13 = fig1.add_subplot(122) # add subplot to cover both upper and lower right, in a 2x2 grid. This is the same as the rightmost panel in a 1x2 grid.
# ax21 = fig2.add_subplot(211) # add axes to the extra figures
# ax21 = fig2.add_subplot(212) # add axes to the extra figures
# ax31 = fig3.add_subplot(111) # add axes to the extra figures
plt.show()
Just use plt.subplots (different from plt.subplot) to define all your axes, with the option sharex=True:
f, axes = plt.subplots(2,2, sharex=True)
plt.subplot(122)
plt.show()
Note that the second call with larger subplot array overlay the preceding one.
Example (could not display image due to reputation...)
I'm writing a pythonic script for a coastal engineering application which should output, amongst other things, a figure with two subplots.
The problem is that I would like to shade a section of both subplots using plt.axvspan() but for some reason it only shades one of them.
Please find below an excerpt of the section of the code where I set up the plots as well as the figure that it's currently outputting (link after code).
Thanks for your help, and sorry if this is a rookie question (but it just happens that I am indeed a rookie in Python... and programming in general) but I couldn't find an answer for this anywhere else.
Feel free to add any comments to the code.
# PLOTTING
# now we generate a figure with the bathymetry vs required m50 and another figure with bathy vs Hs
#1. Generate plots
fig = plt.figure() # Generate Figure
ax = fig.add_subplot(211) # add the first plot to the figure.
depth = ax.plot(results[:,0],results[:,1]*-1,label="Depth [mDMD]") #plot the first set of data onto the first set of axis.
ax2 = ax.twinx() # generate a secondary vertical axis with the same horizontal axis as the first
m50 = ax2.plot(results[:,0],results[:,6],"r",label="M50 [kg]") # plot the second set of data onto the second vertical axis
ax3 = fig.add_subplot(212) # generate the second subplot
hs = ax3.plot(results[:,0],results[:,2],"g",label="Hs(m)")
#Now we want to find where breaking starts to occur so we shade it on the plot.
xBreakingDistance = results[numpy.argmax(breakingIndex),0]
# and now we plot a box from the origin to the depth of breaking.
plt.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) # this box is called a span in matplotlib (also works for axhspan)
# and then we write BREAKING ZONE in the box we just created
yLimits = ax.get_ylim() # first we get the range of y being plotted
yMiddle = (float(yLimits[1])-float(yLimits[0])) / 2 + yLimits[0] # then we calculate the middle value in y (to center the text)
xMiddle = xBreakingDistance / 2 # and then the middle value in x (to center the text)
#now we write BREAKING ZONE in the center of the box.
ax.text(xMiddle,yMiddle,"BREAKING ZONE",fontweight="bold",rotation=90,verticalalignment="center",horizontalalignment="center")
#FIGURE FORMATTING
ax.set_xlabel("Distance [m]") # define x label
ax.set_ylabel("Depth [mDMD]") # define y label on the first vertical axis (ax)
ax2.set_ylabel("M50 [kg]") # define y label on the second vertical axis (ax2)
ax.grid() # show grid
ax3.set_xlabel("Distance[m]") #define x label
ax3.set_ylabel("Hs[m]") # define y label
ax3.grid()
plt.tight_layout() # minimize subplot labels overlapping
# generating a label on a plot with 2 vertical axis is not very intuitive. Normally we would just write ax.label(loc=0)
combined_plots = depth+m50 #first we need to combine the plots in a vector
combined_labels = [i.get_label() for i in combined_plots] # and then we combine the labels
ax.legend(combined_plots,combined_labels,loc=0) # and finally we plot the combined_labels of the combined_plots
plt.savefig("Required M50(kg) along the trench.png",dpi=1000)
plt.close(fig)
Output Figure:
By just calling plt.axvspan, you are telling matplotlib to create the axvspan on the currently active axes (i.e. in this case, the last one you created, ax3)
You need to plot the axvspan on both of the axes you would like for it to appear on. In this case, ax and ax3.
So, you could do:
ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
ax3.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
or in one line:
[this_ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) for this_ax in [ax,ax3]]
It's difficult to analyze your code and not being able to reproduce it. I advise you to build a minimal example. In any case notice that you are calling "plt.axvspan(" which is general call to the library.
You need to specifically state that you want this in both "ax" and "ax2" (i think).
Also if you need more control consider using Patches (I don't know axvspan):
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig1 = plt.figure()
ax1 = fig1.add_subplot(111, aspect='equal')
ax1.add_patch(
patches.Rectangle(
(0.1, 0.1), # (x,y)
0.5, # width
0.5, # height
)
)
fig1.savefig('rect1.png', dpi=90, bbox_inches='tight')
See that call to "ax1" in the example? Just make something similar to yours. Or just add axvspan to each of your plots.