Matplotlib - mark_inset with different edges for axes - python

I want to plot a time series of a damped random walk in one subplot and then zoom into it in a second subplot. I know mark_inset from matplotlib, which works fine. The code I have so far is:
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
from astroML.time_series import generate_damped_RW
fig = plt.figure()
ax = fig.add_subplot(111)
ax0 = fig.add_subplot(211)
ax1 = fig.add_subplot(212)
ax.set_ylabel('Brightness[mag]')
ax.yaxis.labelpad=30
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')
ax.spines['left'].set_color('none')
ax.spines['right'].set_color('none')
ax.tick_params(labelcolor='w', top='off', bottom='off', left='off',
right='off')
t = np.linspace(0, 5000, 100000)
data = generate_damped_RW(t, tau=100, xmean=20, z=0, SFinf=0.3,
random_state=1)
ax0.scatter(t, data, s=0.5)
ax0.text(1, 1, r'$E(m) = %.2f, \sigma(m) = %.2f$'%(np.mean(data),
np.std(data)),
verticalalignment='top', horizontalalignment='right',
transform=ax0.transAxes, fontsize=23)
mask = (t > 370) & (t < 470)
ax1.set_xlabel('Time[years]')
ax1.scatter(t[mask], data[mask], s=0.5)
mark_inset(ax0, ax1, loc1=2, loc=1, fc='none')
which creates a plot like this:
Which is almost what I want, except that the lines connecting the 2 subplots start at the upper edges of the box in the first subplot. Is it possible to have those start at the lower two edges while they still end up at the upper two in the second subplot? What would I have to do to achieve this?

The mark_inset has two arguments loc1 and loc2 to set the locations of the two connectors. Those locations are then the same for the box and and the inset axes.
We may however add two new arguments to the mark_inset function to set different locations for the start and end of the connector.
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import TransformedBbox, BboxPatch, BboxConnector
import numpy as np
fig, (ax, axins) = plt.subplots(nrows=2)
x = np.linspace(0,6*np.pi)
y = np.sin(x)
ax.plot(x,y)
axins.plot(x,y)
axins.set_xlim((2*np.pi, 2.5*np.pi))
axins.set_ylim((0, 1))
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
# loc1, loc2 : {1, 2, 3, 4}
def mark_inset(parent_axes, inset_axes, loc1a=1, loc1b=1, loc2a=2, loc2b=2, **kwargs):
rect = TransformedBbox(inset_axes.viewLim, parent_axes.transData)
pp = BboxPatch(rect, fill=False, **kwargs)
parent_axes.add_patch(pp)
p1 = BboxConnector(inset_axes.bbox, rect, loc1=loc1a, loc2=loc1b, **kwargs)
inset_axes.add_patch(p1)
p1.set_clip_on(False)
p2 = BboxConnector(inset_axes.bbox, rect, loc1=loc2a, loc2=loc2b, **kwargs)
inset_axes.add_patch(p2)
p2.set_clip_on(False)
return pp, p1, p2
mark_inset(ax, axins, loc1a=1, loc1b=4, loc2a=2, loc2b=3, fc="none", ec="crimson")
plt.draw()
plt.show()

Unfortunately, mark_inset always has to connect the same corners (i.e. bottom right always has to connect to bottom right, etc.).
We can make our own function that mimics the mark_inset function though, to connect the two bottom corners with the two top corners in the inset (custom_mark_inset in the code below).
This makes use of a Rectangle patch to draw the box on the primary axes, and the ConnectionPatch instances to draw the connecting lines between axes.
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
#from astroML.time_series import generate_damped_RW
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(111)
ax0 = fig.add_subplot(211)
ax1 = fig.add_subplot(212)
ax.set_ylabel('Brightness[mag]')
ax.yaxis.labelpad=30
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')
ax.spines['left'].set_color('none')
ax.spines['right'].set_color('none')
ax.tick_params(labelcolor='w', top='off', bottom='off', left='off',
right='off')
t = np.linspace(0, 5000, 10000)
#data = generate_damped_RW(t, tau=100, xmean=20, z=0, SFinf=0.3,
# random_state=1)
## Fake some data
data = np.sin(t/800.) + 20.
ax0.scatter(t, data, s=0.5)
ax0.text(1, 1, r'$E(m) = %.2f, \sigma(m) = %.2f$'%(np.mean(data),
np.std(data)),
verticalalignment='top', horizontalalignment='right',
transform=ax0.transAxes, fontsize=23)
mask = (t > 370) & (t < 470)
ax1.set_xlabel('Time[years]')
ax1.scatter(t[mask], data[mask], s=0.5)
def custom_mark_inset(axA, axB, fc='None', ec='k'):
xx = axB.get_xlim()
yy = axB.get_ylim()
xy = (xx[0], yy[0])
width = xx[1] - xx[0]
height = yy[1] - yy[0]
pp = axA.add_patch(patches.Rectangle(xy, width, height, fc=fc, ec=ec))
p1 = axA.add_patch(patches.ConnectionPatch(
xyA=(xx[0], yy[0]), xyB=(xx[0], yy[1]),
coordsA='data', coordsB='data',
axesA=axA, axesB=axB))
p2 = axA.add_patch(patches.ConnectionPatch(
xyA=(xx[1], yy[0]), xyB=(xx[1], yy[1]),
coordsA='data', coordsB='data',
axesA=axA, axesB=axB))
return pp, p1, p2
pp, p1, p2 = custom_mark_inset(ax0, ax1)
plt.show()

Related

matplotlib: colorbar make subplots unequal size

I make two subplots with a common shared colorbar. So naturally I want to plot the colorbar only once.
However, when I do so, then my subplots become unequal in size.
How to place the colorbar outside the subplots on the right?
Minimal working example below
import numpy as np
from matplotlib import colors
import matplotlib.pyplot as plt
res = 100
x = np.linspace(0, 2*np.pi, res)
y = np.sin(x)
z = np.cos(x)
y2 = -np.sin(x)+0.4
z2 = 0.5*np.cos(2*x)
fig_width = 200/25.4
fig_height = 100/25.4
fig = plt.figure(figsize=(fig_width, fig_height))
gs = fig.add_gridspec(1, 2, wspace=0)
(ax, ax2) = gs.subplots(sharey='row')
images = []
images.append(ax.scatter(x, y, c=z))
images.append(ax2.scatter(x, y2, c=z2))
vmin = min(image.get_array().min() for image in images)
vmax = max(image.get_array().max() for image in images)
norm = colors.Normalize(vmin=vmin, vmax=vmax)
for im in images:
im.set_norm(norm)
cbar = fig.colorbar(images[0], ax=ax2)
cbar.set_label("mylabel", loc='top')
fig.tight_layout()
plt.show()
Try 1) pass the two axes as ax, and 2) move tight_layout before colorbar:
# other stuff
fig.tight_layout()
cbar = plt.colorbar(images[0], ax=(ax,ax2))
# other - other stuff
Output:

matplotlib: ordering of zoomed axes objects

I am building a figure with a primary axis that is a scatter plot and a zoomed axis which focuses on a particular region of the primary axis, both of which have gridlines. When I place the zoomed axis as an inset, it "covers" up some of the primary axis data. I want to be able to show the primary axis data (zorder=100) through the zoomed axis, so I set the zoomed axis to be transparent (alpha=0). Finally, I want the primary axis gridlines to "cut-off" when they meet the zoomed axis (zorder=10) but I want to show the zoomed axis gridlines (zorder=50). Is this possible? Below is my attempt:
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
import numpy as np
fig = plt.figure(figsize=(10,7.5))
gs = matplotlib.gridspec.GridSpec(1, 2, width_ratios=[20,1], height_ratios=[1])
ax = plt.subplot(111)
## data
xx = np.linspace(1,100,num=100) + 20 * np.random.normal(0,1,100)
yy = np.linspace(1,100,num=100) + 10 * np.random.normal(0,1,100)
## scatter
sc = ax.scatter(xx, yy, s=250, alpha=0.35, zorder=100)
ax.plot(np.linspace(-100,200,301), np.linspace(-100,200,301),)
ax.set_xlim((0, 100))
ax.set_ylim((0, 100))
ax.grid(linestyle="--", zorder=10)
## zoom
axins = zoomed_inset_axes(ax, 2, loc="upper left")
scins = axins.scatter(xx, yy, s=100, alpha=0.35, zorder=50, marker=".", c="red")
axins.plot(np.linspace(-100,200,301), np.linspace(-100,200,301), c="red")
axins.set_xlim((70, 90))
axins.set_ylim((70, 90))
mark_inset(ax, axins, loc1=1, loc2=4, fc="none", ec="0.5")
axins.grid(linestyle="--", zorder=50)
plt.show()
In particular, one of the blue data points near x=80 gets cut off. I can set axins.patch.set_alpha(0.0), but then it doesn't remove the primary grid lines.
One option is indeed to put a white patch (white rectangle) in ax at the position where axins lives and set that patches' zorder to higher than the one from the gridlines, but lower than the one from the scatter.
# Set axins' background patch invisible
axins.patch.set_visible(False)
# Create a new patch at the position of the axins axes.
rect = matplotlib.patches.Rectangle((0,0), 1,1,
fill=True, facecolor="white", edgecolor="red",zorder=25,
transform=axins.transAxes)
ax.add_patch(rect)
Thanks to #ImportanceOfBeingErnest for the suggestion. It works to add a rectangle with an intermediate zorder to ax per the following (where I've left the red outline of the rectangle):
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
import matplotlib.patches as patches
import numpy as np
## data
xx = np.linspace(1,100,num=100) + 20 * np.random.normal(0,1,100)
yy = np.linspace(1,100,num=100) + 10 * np.random.normal(0,1,100)
## fig
fig = plt.figure(figsize=(10,7.5))
gs = matplotlib.gridspec.GridSpec(1, 2, width_ratios=[20,1], height_ratios=[1])
ax = plt.subplot(111)
## scatter
sc = ax.scatter(xx, yy, s=250, alpha=0.35, zorder=100)
ax.plot(np.linspace(-100,200,301), np.linspace(-100,200,301))
ax.set_xlim((0, 100))
ax.set_ylim((0, 100))
ax.grid(linestyle="--", zorder=10)
ax.patches.extend([patches.Rectangle((0.2, 0.6), 0.4, 0.4,
fill=True, facecolor="white", edgecolor="red",zorder=25,
transform=ax.transAxes, figure=ax)])
## zoom
axins = zoomed_inset_axes(ax, 2,
bbox_to_anchor=(0.6, 1.0, 0.0, 0.0),
bbox_transform=ax.transAxes)
scins = axins.scatter(xx, yy, s=100, alpha=0.35, zorder=50, marker=".", c="red")
axins.plot(np.linspace(-100,200,301), np.linspace(-100,200,301), c="red")
axins.set_xlim((70, 90))
axins.set_ylim((70, 90))
axins.patch.set_alpha(0.0)
mark_inset(ax, axins, loc1=1, loc2=4, fc="none", ec="0.5")
axins.grid(linestyle="--", zorder=50)
plt.show()

Matplotlib secondary / dual axis - marking with circle and arrow - for black and white (bw) publishing

Usually two y-axes are kept apart with different colors, as shown in the example below.
For publications it's often necessary to keep it distinguishable, even when it's printed in black and white.
This is usually done by plotting circles around a line, which have an arrow in the direction of the corresponding axis attached.
How can this be achieved with matplotlib? Or is there a better way to accomplish black and white readability without those circles?
Code from matplotlib.org:
import numpy as np
import matplotlib.pyplot as plt
# Create some mock data
t = np.arange(0.01, 10.0, 0.01)
data1 = np.exp(t)
data2 = np.sin(2 * np.pi * t)
fig, ax1 = plt.subplots()
color = 'tab:red'
ax1.set_xlabel('time (s)')
ax1.set_ylabel('exp', color=color)
ax1.plot(t, data1, color=color)
ax1.tick_params(axis='y', labelcolor=color)
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:blue'
ax2.set_ylabel('sin', color=color) # we already handled the x-label with ax1
ax2.plot(t, data2, color=color)
ax2.tick_params(axis='y', labelcolor=color)
fig.tight_layout() # otherwise the right y-label is slightly clipped
plt.show()
This approach is based on this answer. It uses arc, which can be configured as follows:
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
# Generate example graph
fig = plt.figure(figsize=(5, 5))
ax = fig.add_subplot(1, 1, 1)
ax.plot([1,2,3,4,5,6], [2,4,6,8,10,12])
# Configure arc
center_x = 2 # x coordinate
center_y = 3.8 # y coordinate
radius_1 = 0.25 # radius 1
radius_2 = 1 # radius 2 >> for cicle: radius_2 = 2 x radius_1
angle = 180 # orientation
theta_1 = 70 # arc starts at this angle
theta_2 = 290 # arc finishes at this angle
arc = Arc([center_x, center_y],
radius_1,
radius_2,
angle = angle,
theta1 = theta_1,
theta2=theta_2,
capstyle = 'round',
linestyle='-',
lw=1,
color = 'black')
# Add arc
ax.add_patch(arc)
# Add arrow
x1 = 1.9 # x coordinate
y1 = 4 # y coordinate
length_x = -0.5 # length on the x axis (negative so the arrow points to the left)
length_y = 0 # length on the y axis
ax.arrow(x1,
y1,
length_x,
length_y,
head_width=0.1,
head_length=0.05,
fc='k',
ec='k',
linewidth = 0.6)
The result is shown below:
You can use matplotlib's axes annotate to draw arrows to the y-axes. You will need to find the points in the plot where the arrows should start. However, this does not plot circles around lines. If you really want to plot a circle, you could use plt.scatter or plt.Circle to plot an appropriate circle covering the relevant area.
import numpy as np
import matplotlib.pyplot as plt
# Create some mock data
t = np.arange(0.01, 10.0, 0.01)
data1 = np.exp(t)
data2 = np.sin(2 * np.pi * t)
fig, ax1 = plt.subplots()
color = 'tab:red'
ax1.set_xlabel('time (s)')
ax1.set_ylabel('exp', color=color)
ax1.plot(t, data1, color=color)
ax1.tick_params(axis='y', labelcolor=color)
ax1.annotate('', xy=(7, 1096), xytext=(-0.5, 1096), # start the arrow from x=7 and draw towards primary y-axis
arrowprops=dict(arrowstyle="<-", color=color))
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:blue'
ax2.set_ylabel('sin', color=color) # we already handled the x-label with ax1
ax2.plot(t, data2, color=color)
ax2.tick_params(axis='y', labelcolor=color)
# plt.arrow()
ax2.annotate('', xy=(6,0), xytext=(10.4, 0), # start the arrow from x=6 and draw towards secondary y-axis
arrowprops=dict(arrowstyle="<-", color=color))
fig.tight_layout() # otherwise the right y-label is slightly clipped
plt.show()
Following is the sample output figure.
EDIT: Following is the snippet with the circles you've requested. I have used plt.scatter.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
# Create some mock data
t = np.arange(0.01, 10.0, 0.01)
data1 = np.exp(t)
data2 = np.sin(2 * np.pi * t)
fig, ax1 = plt.subplots()
color = 'tab:red'
ax1.set_xlabel('time (s)')
ax1.set_ylabel('exp', color=color)
ax1.plot(t, data1, color=color)
ax1.tick_params(axis='y', labelcolor=color)
ax1.annotate('', xy=(7, 1096), xytext=(-0.5, 1096), # start the arrow from x=7 and draw towards primary y-axis
arrowprops=dict(arrowstyle="<-", color=color))
# circle1 = Circle((5, 3000), color='r')
# ax1.add_artist(circle1)
plt.scatter(7, 1096, s=100, facecolors='none', edgecolors='r')
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:blue'
ax2.set_ylabel('sin', color=color) # we already handled the x-label with ax1
ax2.plot(t, data2, color=color)
ax2.tick_params(axis='y', labelcolor=color)
# plt.arrow()
ax2.annotate('', xy=(6.7,0), xytext=(10.5, 0), # start the arrow from x=6.7 and draw towards secondary y-axis
arrowprops=dict(arrowstyle="<-", color=color))
plt.scatter(6,0, s=2000, facecolors='none', edgecolors=color)
fig.tight_layout() # otherwise the right y-label is slightly clipped
plt.savefig('fig')
plt.show()
Here is the sample output.

Matplotlib's inset_locator with inverted axes

I want to make a standard inset into my plot. But I don't get how to use the inset_locator.
Here is my code:
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset
x = np.linspace(0, 2)
plt.plot(x, np.sin(x))
ax = plt.gca()
ax.invert_yaxis()
axins = inset_axes(ax, width='40%', height='30%', loc='lower left')
x_in = np.linspace(1.25, 1.75)
axins.plot(x_in, np.sin(x_in))
axins.invert_yaxis()
mark_inset(ax, axins, loc1=2, loc2=4)
plt.show()
And the result is:
Apparently it the edges connect the wrong corners. How do I get them right, when my axis goes from maximum to minimum?
Unfortunately the mark_inset cannot cope with inverted axes. So you need to set the locations of the connectors manually.
patch, pp1,pp2 = mark_inset(ax, axins, loc1=1,loc2=1)
pp1.loc1 = 1
pp1.loc2 = 4
pp2.loc1 = 4
pp2.loc2 = 1

Force square subplots when plotting a colorbar

I'm trying to generate two subplots side by side, sharing the y axis, with a single colorbar for both.
This is a MWE of my code:
import matplotlib.pyplot as plt
import numpy as np
def rand_data(l, h):
return np.random.uniform(low=l, high=h, size=(100,))
# Generate data.
x1, x2, y, z = rand_data(0., 1.), rand_data(100., 175.), \
rand_data(150., 200.), rand_data(15., 33.)
fig = plt.figure()
cm = plt.cm.get_cmap('RdYlBu')
ax0 = plt.subplot(121)
plt.scatter(x1, y, c=z, cmap=cm)
ax1 = plt.subplot(122)
# make these y tick labels invisible
plt.setp(ax1.get_yticklabels(), visible=False)
plt.scatter(x2, y, c=z, cmap=cm)
cbar = plt.colorbar()
plt.show()
what this returns is a left subplot slightly larger horizontally than the right one since this last includes the colorbar, see below:
I've tried using ax.set_aspect('equal') but since the x axis are not in the same range the result looks awful.
I need both these plots to be displayed squared. How can I do this?
To expend my comment that one can make 3 plots, plot the colorbar() in the 3rd one, the data plots in the 1st and 2nd. This way, if necessary, we are free to do anything we want to the 1st and 2nd plots:
def rand_data(l, h):
return np.random.uniform(low=l, high=h, size=(100,))
# Generate data.
x1, x2, y, z = rand_data(0., 1.), rand_data(100., 175.), \
rand_data(150., 200.), rand_data(15., 33.)
fig = plt.figure(figsize=(12,6))
gs=gridspec.GridSpec(1,3, width_ratios=[4,4,0.2])
ax1 = plt.subplot(gs[0])
ax2 = plt.subplot(gs[1])
ax3 = plt.subplot(gs[2])
cm = plt.cm.get_cmap('RdYlBu')
ax1.scatter(x1, y, c=z, cmap=cm)
SC=ax2.scatter(x2, y, c=z, cmap=cm)
plt.setp(ax2.get_yticklabels(), visible=False)
plt.colorbar(SC, cax=ax3)
plt.tight_layout()
plt.savefig('temp.png')
Updated - here is another option without using GridSpec.
import numpy as np
import matplotlib.pyplot as plt
N = 50
x_vals = np.random.rand(N)
y_vals = np.random.rand(N)
z1_vals = np.random.rand(N)
z2_vals = np.random.rand(N)
minimum_z = min(np.min(z1_vals), np.min(z2_vals))
maximum_z = max(np.max(z1_vals), np.max(z2_vals))
fig, axis_array = plt.subplots(1,2, figsize = (20, 10), subplot_kw = {'aspect':1})
ax0 = axis_array[0].scatter(x_vals, y_vals, c = z1_vals, s = 100, cmap = 'rainbow', vmin = minimum_z, vmax = maximum_z)
ax1 = axis_array[1].scatter(x_vals, y_vals, c = z2_vals, s = 100, cmap = 'rainbow', vmin = minimum_z, vmax = maximum_z)
cax = fig.add_axes([0.95, 0.05, 0.02, 0.95]) #this locates the axis that is used for your colorbar. It is scaled 0 - 1.
fig.colorbar(ax0, cax, orientation = 'vertical') #'ax0' tells it which plot to base the colors on
plt.show()

Categories