matplotlib: place axes relative to other axes, with automatic updating - python

Matplotlib has well documented methods of how to place multiple sets of axes in a figure window, but I cannot figure out how to define the position of one set of axes relative to the position of another set of axes. For example,
import matplotlib.pyplot as plt
import numpy as np
#Define data
x1 = np.arange(0,10,0.01)
y1 = np.sqrt(x1)
x2 = x1
y2 = 1.0/2.0 * x2**2.0
#Generate vertically stacked plots
fig = plt.figure()
ax1 = fig.add_subplot(211)
ax1.plot(x1,y1)
ax2 = fig.add_subplot(212)
ax2.plot(x2,y2)
fig.savefig('nice_stacked_plots.png')
gives the following plot:
This is all well and good, but when I change the size of the bottom axes
#Change the size of the bottom plot
bbox2 = ax2.get_position()
ax2.set_position([bbox2.x0, bbox2.y0, bbox2.width, bbox2.height * 1.25])
ax2.set_ylim(0,60)
fig.savefig('overlapping_stacked_plots.png')
the bottom axes overlap with the top axes
I realize I could subsequently update the position of the top axes to remove the overlap, but I would like to just specify the top axes position relative to the bottom axes at the outset, and have things automatically update.
For example, in the annotate tutorial it is possible to place an annotation and then place a 2nd annotation at a specified offset from the 1st annotation using the OffsetFrom class. If the 1st annotation moves, then the 2nd annotation moves with it. I would like to do something similar with axes.

I am afraid I have no general answer to offer, but do you know about add_axes?
It lets you define the location of your subplots precisely - it is then easy to make one dependent of the other.
Here is an example - as I said, pretty specific for your task, but perhaps it may inspire you?
# General aspect of the Fig (margins)
left = 0.1
right = 0.05
width= 1.-left-right
bottom = 0.1
top = 0.05
hspace = 0.10 #space between the subplots
def placeSubplots(fig, ax2height = (1.-top-bottom-hspace)/2.):
ax1height = 1-top-bottom-hspace-ax2height
ax1 = fig.add_axes([left, bottom+ax2height+hspace, width, ax1height])
ax1.plot(x1, y1)
ax2 = fig.add_axes([left, bottom, width, ax2height])
ax2.plot(x2, y2)
return fig
fig1 = placeSubplots(plt.figure())
fig2 = placeSubplots(plt.figure(), ax2height=0.6)
fig1.savefig('fig1_equal_heigth.png')
fig2.savefig('fig2_ax2_taller.png')
fig1:
fig2:
Above, the second axis height is specified in an absolute way, but you could define the height of your subplots as a ratio between them too:
def placeSubplotsRatio(fig, ax1ax2ratio = 1.):
subplotSpace = 1.-top-bottom-hspace
ax1height = subplotSpace/(1.+1./ax1ax2ratio)
ax2height = subplotSpace/(1.+ax1ax2ratio)
ax1 = fig.add_axes([left, bottom+ax2height+hspace, width, ax1height])
ax1.plot(x1, y1)
ax2 = fig.add_axes([left, bottom, width, ax2height])
ax2.plot(x2, y2)
return fig
fig3 = placeSubplotsRatio(plt.figure()) # idem as fig1
fig4 = placeSubplotsRatio(plt.figure(), ax1ax2ratio=3.) #ax1 is 3 times taller
fig5 = placeSubplotsRatio(plt.figure(), ax1ax2ratio=0.25) #ax2 is 4 times taller
fig4.savefig('fig4_ax1ax2ratio3.png')
fig5.savefig('fig5_ax1ax2ratio025.png')
fig4:
fig5:

Related

Force inset figure to share axis style of main figure- Matplotlib

I would like to create a figure that has an inset figure. However, the inset does not share the style properties of the main figure. How could I force the inset to share the style properties of the main figure? The code I am using is the following:
def initializeFigure(xlabel, ylabel, scale= 'loglog',width='1col', height=None):
import matplotlib as mpl
from matplotlib import pyplot as plt
# make sure defaults are used
plt.style.use(['science', 'scatter'])
plt.rcParams['text.usetex'] = True
import matplotlib
# Prepare figure width and height
cm_to_inch = 0.393701 # [inch/cm]
# Get figure width in inch
if width == '1col':
width = 8.8 # width [cm]
elif width == '2col':
width = 18.0 # width [cm]
figWidth = width * cm_to_inch # width [inch]
# Get figure height in inch
if height is None:
fig_aspect_ratio = 7.5/10.
figHeight = figWidth * fig_aspect_ratio # height [inch]
else:
figHeight = height * cm_to_inch # height [inch]
# Create figure with right resolution for publication
fig = plt.figure(figsize=(figWidth, figHeight), dpi=300)
# Add axis object and select as current axis for pyplot
ax = fig.add_subplot(111)
plt.sca(ax)
ax.tick_params(axis='both', which='minor',left=0,right=0,bottom=0, top=0, direction='out', labelsize='medium', pad=2)
ax.tick_params(axis='both', which='major',left=1,right=0,bottom=1, top=0, direction='out', labelsize='small', pad=2)
if scale=='loglog':
# ax.loglog(x,y, label =label)
ax.set_yscale('log')
ax.set_xscale('log')
elif scale=='semilogy':
ax.set_yscale('log')
elif scale=='semilogx':
ax.set_xscale('log')
else:
pass
ax.set_ylabel(xlabel)
ax.set_xlabel(ylabel)
return fig, ax
ylabel =r'$p(\delta \ell)$'
xlabel = r'$\delta \ell~[d_{i}]$'
fig, ax= initializeFigure(ylabel, xlabel,'2col')
plt.loglog(np.logspace(np.log10(1), np.log10(100), 1000), 1/np.logspace(np.log10(1), np.log10(100), 1000))
axins2 = ax.inset_axes([0.02, 0.02, 0.42, 0.42])
axins2.yaxis.set_label_position("right")
axins2.xaxis.set_label_position("top")
axins2.yaxis.tick_right()
x, y = np.random.rand(100),np.random.rand(100)
axins2.plot(x, y )
My question is the following:
How could I force the inset to share the style properties of the main figure?
One option is to dive into the implementation details of the Axes class and define a function to return the values properties that you define for the original axes. Then you can use this data to set the same properties on the inset Axes.
An alternative, more feasible approach is to accept two additional parameters in your function (major_tick_params=None, minor_tick_params=None) and define inside the function:
if major_tick_params is None:
major_params = {'axis':'both',
'which':'major',
'left':1,
'right':0,
'bottom':1,
'top':0,
'direction':'out',
'labelsize':'small',
'pad':2}
if minor_tick_params is None:
minor_params = {'axis':'both',
'which':'minor',
'left':0,
'right':0,
'bottom':0,
'top':0,
'direction':'out',
'labelsize':'medium',
'pad':2}
ax.tick_params(**major_tick_params)
ax.tick_params(**minor_tick_params)
Then you can define the major_tick_params and minor_tick_params before you call the function and reuse it on the inset axes.
ylabel =r'$p(\delta \ell)$'
xlabel = r'$\delta \ell~[d_{i}]$'
minor_tick_params = {'axis':'both',
'which':'minor',
'left':0,
'right':0,
'bottom':0,
'top':0,
'direction':'out',
'labelsize':'medium',
'pad':2}
major_tick_params = {'axis':'both',
'which':'major',
'left':1,
'right':0,
'bottom':1,
'top':0,
'direction':'out',
'labelsize':'small',
'pad':2}
fig, ax= initializeFigure(ylabel, xlabel,'2col', major_tick_params=major_tick_params, minor_tick_params=minor_tick_params)
xvals = np.logspace(np.log10(1), np.log10(100), 1000)
ax.plot(xvals, 1/xvals)
ax.set_xscale('log')
ax.set_yscale('log')
axins2 = ax.inset_axes([0.02, 0.02, 0.42, 0.42])
axins2.yaxis.set_label_position("right")
axins2.xaxis.set_label_position("top")
axins2.yaxis.tick_right()
x, y = np.random.rand(100),np.random.rand(100)
axins2.plot(x, y)
axins2.tick_params(**minor_tick_params)
axins2.tick_params(**major_tick_params)

Smoothing and breaking x-axis with datetime index in matplotlib python [duplicate]

I'm trying to create a plot using pyplot that has a discontinuous x-axis. The usual way this is drawn is that the axis will have something like this:
(values)----//----(later values)
where the // indicates that you're skipping everything between (values) and (later values).
I haven't been able to find any examples of this, so I'm wondering if it's even possible. I know you can join data over a discontinuity for, eg, financial data, but I'd like to make the jump in the axis more explicit. At the moment I'm just using subplots but I'd really like to have everything end up on the same graph in the end.
Paul's answer is a perfectly fine method of doing this.
However, if you don't want to make a custom transform, you can just use two subplots to create the same effect.
Rather than put together an example from scratch, there's an excellent example of this written by Paul Ivanov in the matplotlib examples (It's only in the current git tip, as it was only committed a few months ago. It's not on the webpage yet.).
This is just a simple modification of this example to have a discontinuous x-axis instead of the y-axis. (Which is why I'm making this post a CW)
Basically, you just do something like this:
import matplotlib.pylab as plt
import numpy as np
# If you're not familiar with np.r_, don't worry too much about this. It's just
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)
fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)
# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')
# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only
# 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(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()
# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)
plt.show()
To add the broken axis lines // effect, we can do this (again, modified from Paul Ivanov's example):
import matplotlib.pylab as plt
import numpy as np
# If you're not familiar with np.r_, don't worry too much about this. It's just
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)
fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)
# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')
# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only
# 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(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()
# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)
# 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) # top-left diagonal
ax.plot((1-d,1+d),(1-d,1+d), **kwargs) # bottom-left diagonal
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,d),(-d,+d), **kwargs) # top-right diagonal
ax2.plot((-d,d),(1-d,1+d), **kwargs) # bottom-right diagonal
# 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()
I see many suggestions for this feature but no indication that it's been implemented. Here is a workable solution for the time-being. It applies a step-function transform to the x-axis. It's a lot of code, but it's fairly simple since most of it is boilerplate custom scale stuff. I have not added any graphics to indicate the location of the break, since that is a matter of style. Good luck finishing the job.
from matplotlib import pyplot as plt
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
import numpy as np
def CustomScaleFactory(l, u):
class CustomScale(mscale.ScaleBase):
name = 'custom'
def __init__(self, axis, **kwargs):
mscale.ScaleBase.__init__(self)
self.thresh = None #thresh
def get_transform(self):
return self.CustomTransform(self.thresh)
def set_default_locators_and_formatters(self, axis):
pass
class CustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
lower = l
upper = u
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform(self, a):
aa = a.copy()
aa[a>self.lower] = a[a>self.lower]-(self.upper-self.lower)
aa[(a>self.lower)&(a<self.upper)] = self.lower
return aa
def inverted(self):
return CustomScale.InvertedCustomTransform(self.thresh)
class InvertedCustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
lower = l
upper = u
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform(self, a):
aa = a.copy()
aa[a>self.lower] = a[a>self.lower]+(self.upper-self.lower)
return aa
def inverted(self):
return CustomScale.CustomTransform(self.thresh)
return CustomScale
mscale.register_scale(CustomScaleFactory(1.12, 8.88))
x = np.concatenate((np.linspace(0,1,10), np.linspace(9,10,10)))
xticks = np.concatenate((np.linspace(0,1,6), np.linspace(9,10,6)))
y = np.sin(x)
plt.plot(x, y, '.')
ax = plt.gca()
ax.set_xscale('custom')
ax.set_xticks(xticks)
plt.show()
Check the brokenaxes package:
import matplotlib.pyplot as plt
from brokenaxes import brokenaxes
import numpy as np
fig = plt.figure(figsize=(5,2))
bax = brokenaxes(
xlims=((0, .1), (.4, .7)),
ylims=((-1, .7), (.79, 1)),
hspace=.05
)
x = np.linspace(0, 1, 100)
bax.plot(x, np.sin(10 * x), label='sin')
bax.plot(x, np.cos(10 * x), label='cos')
bax.legend(loc=3)
bax.set_xlabel('time')
bax.set_ylabel('value')
A very simple hack is to
scatter plot rectangles over the axes' spines and
draw the "//" as text at that position.
Worked like a charm for me:
# FAKE BROKEN AXES
# plot a white rectangle on the x-axis-spine to "break" it
xpos = 10 # x position of the "break"
ypos = plt.gca().get_ylim()[0] # y position of the "break"
plt.scatter(xpos, ypos, color='white', marker='s', s=80, clip_on=False, zorder=100)
# draw "//" on the same place as text
plt.text(xpos, ymin-0.125, r'//', fontsize=label_size, zorder=101, horizontalalignment='center', verticalalignment='center')
Example Plot:
For those interested, I've expanded upon #Paul's answer and added it to the matplotlib wrapper proplot. It can do axis "jumps", "speedups", and "slowdowns".
There is no way currently to add "crosses" that indicate the discrete jump like in Joe's answer, but I plan to add this in the future. I also plan to add a default "tick locator" that sets sensible default tick locations depending on the CutoffScale arguments.
Adressing Frederick Nord's question how to enable parallel orientation of the diagonal "breaking" lines when using a gridspec with ratios unequal 1:1, the following changes based on the proposals of Paul Ivanov and Joe Kingtons may be helpful. Width ratio can be varied using variables n and m.
import matplotlib.pylab as plt
import numpy as np
import matplotlib.gridspec as gridspec
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)
n = 5; m = 1;
gs = gridspec.GridSpec(1,2, width_ratios = [n,m])
plt.figure(figsize=(10,8))
ax = plt.subplot(gs[0,0])
ax2 = plt.subplot(gs[0,1], sharey = ax)
plt.setp(ax2.get_yticklabels(), visible=False)
plt.subplots_adjust(wspace = 0.1)
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')
ax.set_xlim(0,1)
ax2.set_xlim(10,8)
# 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(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()
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)
on = (n+m)/n; om = (n+m)/m;
ax.plot((1-d*on,1+d*on),(-d,d), **kwargs) # bottom-left diagonal
ax.plot((1-d*on,1+d*on),(1-d,1+d), **kwargs) # top-left diagonal
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d*om,d*om),(-d,d), **kwargs) # bottom-right diagonal
ax2.plot((-d*om,d*om),(1-d,1+d), **kwargs) # top-right diagonal
plt.show()
This is a hacky but pretty solution for x-axis breaks.
The solution is based on https://matplotlib.org/stable/gallery/subplots_axes_and_figures/broken_axis.html, which gets rid of the problem with positioning the break above the spine, solved by How can I plot points so they appear over top of the spines with matplotlib?
from matplotlib.patches import Rectangle
import matplotlib.pyplot as plt
def axis_break(axis, xpos=[0.1, 0.125], slant=1.5):
d = slant # proportion of vertical to horizontal extent of the slanted line
anchor = (xpos[0], -1)
w = xpos[1] - xpos[0]
h = 1
kwargs = dict(marker=[(-1, -d), (1, d)], markersize=12, zorder=3,
linestyle="none", color='k', mec='k', mew=1, clip_on=False)
axis.add_patch(Rectangle(
anchor, w, h, fill=True, color="white",
transform=axis.transAxes, clip_on=False, zorder=3)
)
axis.plot(xpos, [0, 0], transform=axis.transAxes, **kwargs)
fig, ax = plt.subplots(1,1)
plt.plot(np.arange(10))
axis_break(ax, xpos=[0.1, 0.12], slant=1.5)
axis_break(ax, xpos=[0.3, 0.31], slant=-10)
if you want to replace an axis label, this would do the trick:
from matplotlib import ticker
def replace_pos_with_label(fig, pos, label, axis):
fig.canvas.draw() # this is needed to set up the x-ticks
labs = axis.get_xticklabels()
labels = []
locs = []
for text in labs:
x = text._x
lab = text._text
if x == pos:
lab = label
labels.append(lab)
locs.append(x)
axis.xaxis.set_major_locator(ticker.FixedLocator(locs))
axis.set_xticklabels(labels)
fig, ax = plt.subplots(1,1)
plt.plot(np.arange(10))
replace_pos_with_label(fig, 0, "-10", axis=ax)
replace_pos_with_label(fig, 6, "$10^{4}$", axis=ax)
axis_break(ax, xpos=[0.1, 0.12], slant=2)

Setting plot border frame for two subplot containing MatPlotLib.Basemap contents

As an illustration, I present a figure here to depict my question.
fig = plt.figure(figsize = (10,4))
ax1 = plt.subplot(121)
map =Basemap(llcrnrlon=x_map1,llcrnrlat=y_map1,urcrnrlon=x_map2,urcrnrlat=y_map2)
map.readshapefile('shapefile','shapefile',zorder =2,linewidth = 0)
for info, shape in zip(map.shapefile, map.shapefile):
x, y = zip(*shape)
map.plot(x, y, marker=None,color='k',linewidth = 0.5)
plt.title("a")
ax2 = plt.subplot(122)
y_pos = [0.5,1,]
performance = [484.0,1080.0]
bars = plt.bar(y_pos, performance, align='center')
plt.title("b")
Due to the mapping setting is not consistent with the subplot(b). Thus, subplot(a) and subplot(b) has distinct board frame. In my opinion, the un-aligned borders are not pleasant for reader.
Is there any way to adjust the boarder size of subplot(b) in order to harmony as a whole figure.
This is my target:
Notice that, subplot(a) need to contain matplotlib.basemap elements.
Currently, your subplot on the left has an 'equal' aspect ratio, while for the other one it is automatic. Therefore, you have to manually set the aspect ratio of the subplot on the right:
def get_aspect(ax):
xlim = ax.get_xlim()
ylim = ax.get_ylim()
aspect_ratio = abs((ylim[0]-ylim[1]) / (xlim[0]-xlim[1]))
return aspect_ratio
ax2.set_aspect(get_aspect(ax1) / get_aspect(ax2))

Expanding axes to fill figure, same scale on x and y

I know 2 things but separately.
figure.tight_layout
will expand my current axes
axes.aspect('equal')
will keep same scale on x and y.
But when I use them both I get square axes view and I want it to be expanded.
By keeping same scale I mean there is same distance from 0 to 1 on x and y axis.
Is there any way to make it happen? Keep same scale and expand to full figure(not only a square)
The answer should work with autoscale
There might be less clumsy way, but at least you can do it manually. A very simple example:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([0,1],[1,0])
ax.set_aspect(1)
ax.set_xlim(0, 1.5)
creates
which honours the aspect ratio.
If you want to have the automatic scaling offered by the tight_layout, then you'll have to do some maths of your own:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([0,1],[1,0])
fig.tight_layout()
# capture the axis positioning in pixels
bb = fig.transFigure.transform(ax.get_position())
x0, y0 = bb[0]
x1, y1 = bb[1]
width = x1 - x0
height = y1 - y0
# set the aspect ratio
ax.set_aspect(1)
# calculate the aspect ratio of the plot
plot_aspect = width / height
# get the axis limits in data coordinates
ax0, ax1 = ax.get_xlim()
ay0, ay1 = ax.get_ylim()
awidth = ax1 - ax0
aheight = ay1 - ay0
# calculate the plot aspect
data_aspect = awidth / aheight
# check which one needs to be corrected
if data_aspect < plot_aspect:
ax.set_xlim(ax0, ax0 + plot_aspect * aheight)
else:
ax.set_ylim(ay0, ay0 + awidth / plot_aspect)
Of course, you may set the xlim and ylim any way you want, you might, for example, want to add an equal amount of space to either end of the scale.
The solution that worked in my case was to call
axis.aspect("equal")
axis.set_adjustable("datalim")
stolen from this example in the documentation.

Is there a way to make a discontinuous axis in Matplotlib?

I'm trying to create a plot using pyplot that has a discontinuous x-axis. The usual way this is drawn is that the axis will have something like this:
(values)----//----(later values)
where the // indicates that you're skipping everything between (values) and (later values).
I haven't been able to find any examples of this, so I'm wondering if it's even possible. I know you can join data over a discontinuity for, eg, financial data, but I'd like to make the jump in the axis more explicit. At the moment I'm just using subplots but I'd really like to have everything end up on the same graph in the end.
Paul's answer is a perfectly fine method of doing this.
However, if you don't want to make a custom transform, you can just use two subplots to create the same effect.
Rather than put together an example from scratch, there's an excellent example of this written by Paul Ivanov in the matplotlib examples (It's only in the current git tip, as it was only committed a few months ago. It's not on the webpage yet.).
This is just a simple modification of this example to have a discontinuous x-axis instead of the y-axis. (Which is why I'm making this post a CW)
Basically, you just do something like this:
import matplotlib.pylab as plt
import numpy as np
# If you're not familiar with np.r_, don't worry too much about this. It's just
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)
fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)
# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')
# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only
# 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(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()
# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)
plt.show()
To add the broken axis lines // effect, we can do this (again, modified from Paul Ivanov's example):
import matplotlib.pylab as plt
import numpy as np
# If you're not familiar with np.r_, don't worry too much about this. It's just
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)
fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)
# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')
# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only
# 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(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()
# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)
# 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) # top-left diagonal
ax.plot((1-d,1+d),(1-d,1+d), **kwargs) # bottom-left diagonal
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,d),(-d,+d), **kwargs) # top-right diagonal
ax2.plot((-d,d),(1-d,1+d), **kwargs) # bottom-right diagonal
# 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()
I see many suggestions for this feature but no indication that it's been implemented. Here is a workable solution for the time-being. It applies a step-function transform to the x-axis. It's a lot of code, but it's fairly simple since most of it is boilerplate custom scale stuff. I have not added any graphics to indicate the location of the break, since that is a matter of style. Good luck finishing the job.
from matplotlib import pyplot as plt
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
import numpy as np
def CustomScaleFactory(l, u):
class CustomScale(mscale.ScaleBase):
name = 'custom'
def __init__(self, axis, **kwargs):
mscale.ScaleBase.__init__(self)
self.thresh = None #thresh
def get_transform(self):
return self.CustomTransform(self.thresh)
def set_default_locators_and_formatters(self, axis):
pass
class CustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
lower = l
upper = u
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform(self, a):
aa = a.copy()
aa[a>self.lower] = a[a>self.lower]-(self.upper-self.lower)
aa[(a>self.lower)&(a<self.upper)] = self.lower
return aa
def inverted(self):
return CustomScale.InvertedCustomTransform(self.thresh)
class InvertedCustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
lower = l
upper = u
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform(self, a):
aa = a.copy()
aa[a>self.lower] = a[a>self.lower]+(self.upper-self.lower)
return aa
def inverted(self):
return CustomScale.CustomTransform(self.thresh)
return CustomScale
mscale.register_scale(CustomScaleFactory(1.12, 8.88))
x = np.concatenate((np.linspace(0,1,10), np.linspace(9,10,10)))
xticks = np.concatenate((np.linspace(0,1,6), np.linspace(9,10,6)))
y = np.sin(x)
plt.plot(x, y, '.')
ax = plt.gca()
ax.set_xscale('custom')
ax.set_xticks(xticks)
plt.show()
Check the brokenaxes package:
import matplotlib.pyplot as plt
from brokenaxes import brokenaxes
import numpy as np
fig = plt.figure(figsize=(5,2))
bax = brokenaxes(
xlims=((0, .1), (.4, .7)),
ylims=((-1, .7), (.79, 1)),
hspace=.05
)
x = np.linspace(0, 1, 100)
bax.plot(x, np.sin(10 * x), label='sin')
bax.plot(x, np.cos(10 * x), label='cos')
bax.legend(loc=3)
bax.set_xlabel('time')
bax.set_ylabel('value')
A very simple hack is to
scatter plot rectangles over the axes' spines and
draw the "//" as text at that position.
Worked like a charm for me:
# FAKE BROKEN AXES
# plot a white rectangle on the x-axis-spine to "break" it
xpos = 10 # x position of the "break"
ypos = plt.gca().get_ylim()[0] # y position of the "break"
plt.scatter(xpos, ypos, color='white', marker='s', s=80, clip_on=False, zorder=100)
# draw "//" on the same place as text
plt.text(xpos, ymin-0.125, r'//', fontsize=label_size, zorder=101, horizontalalignment='center', verticalalignment='center')
Example Plot:
For those interested, I've expanded upon #Paul's answer and added it to the matplotlib wrapper proplot. It can do axis "jumps", "speedups", and "slowdowns".
There is no way currently to add "crosses" that indicate the discrete jump like in Joe's answer, but I plan to add this in the future. I also plan to add a default "tick locator" that sets sensible default tick locations depending on the CutoffScale arguments.
Adressing Frederick Nord's question how to enable parallel orientation of the diagonal "breaking" lines when using a gridspec with ratios unequal 1:1, the following changes based on the proposals of Paul Ivanov and Joe Kingtons may be helpful. Width ratio can be varied using variables n and m.
import matplotlib.pylab as plt
import numpy as np
import matplotlib.gridspec as gridspec
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)
n = 5; m = 1;
gs = gridspec.GridSpec(1,2, width_ratios = [n,m])
plt.figure(figsize=(10,8))
ax = plt.subplot(gs[0,0])
ax2 = plt.subplot(gs[0,1], sharey = ax)
plt.setp(ax2.get_yticklabels(), visible=False)
plt.subplots_adjust(wspace = 0.1)
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')
ax.set_xlim(0,1)
ax2.set_xlim(10,8)
# 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(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()
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)
on = (n+m)/n; om = (n+m)/m;
ax.plot((1-d*on,1+d*on),(-d,d), **kwargs) # bottom-left diagonal
ax.plot((1-d*on,1+d*on),(1-d,1+d), **kwargs) # top-left diagonal
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d*om,d*om),(-d,d), **kwargs) # bottom-right diagonal
ax2.plot((-d*om,d*om),(1-d,1+d), **kwargs) # top-right diagonal
plt.show()
This is a hacky but pretty solution for x-axis breaks.
The solution is based on https://matplotlib.org/stable/gallery/subplots_axes_and_figures/broken_axis.html, which gets rid of the problem with positioning the break above the spine, solved by How can I plot points so they appear over top of the spines with matplotlib?
from matplotlib.patches import Rectangle
import matplotlib.pyplot as plt
def axis_break(axis, xpos=[0.1, 0.125], slant=1.5):
d = slant # proportion of vertical to horizontal extent of the slanted line
anchor = (xpos[0], -1)
w = xpos[1] - xpos[0]
h = 1
kwargs = dict(marker=[(-1, -d), (1, d)], markersize=12, zorder=3,
linestyle="none", color='k', mec='k', mew=1, clip_on=False)
axis.add_patch(Rectangle(
anchor, w, h, fill=True, color="white",
transform=axis.transAxes, clip_on=False, zorder=3)
)
axis.plot(xpos, [0, 0], transform=axis.transAxes, **kwargs)
fig, ax = plt.subplots(1,1)
plt.plot(np.arange(10))
axis_break(ax, xpos=[0.1, 0.12], slant=1.5)
axis_break(ax, xpos=[0.3, 0.31], slant=-10)
if you want to replace an axis label, this would do the trick:
from matplotlib import ticker
def replace_pos_with_label(fig, pos, label, axis):
fig.canvas.draw() # this is needed to set up the x-ticks
labs = axis.get_xticklabels()
labels = []
locs = []
for text in labs:
x = text._x
lab = text._text
if x == pos:
lab = label
labels.append(lab)
locs.append(x)
axis.xaxis.set_major_locator(ticker.FixedLocator(locs))
axis.set_xticklabels(labels)
fig, ax = plt.subplots(1,1)
plt.plot(np.arange(10))
replace_pos_with_label(fig, 0, "-10", axis=ax)
replace_pos_with_label(fig, 6, "$10^{4}$", axis=ax)
axis_break(ax, xpos=[0.1, 0.12], slant=2)

Categories