Matplotlib: align origin of right axis with specific left axis value - python

When plotting several y axis in Matplotlib, is there a way to specify how to align the origin (and/or some ytick labels) of the right axis with a specific value of the left axis?
Here is my problem: I would like to plot two set of data as well as their difference (basically, I am trying to reproduce this kind of graph).
I can reproduce it, but I have to manually adjust the ylim of the right axis so that the origin is aligned with the value I want from the left axis.
I putted below an example of a simplified version of the code I use. As you can see, I have to manually adjust scale of the right axis to align the origin of the right axis as well as the square.
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
grp1 = np.array([1.202, 1.477, 1.223, 1.284, 1.701, 1.724, 1.099,
1.242, 1.099, 1.217, 1.291, 1.305, 1.333, 1.246])
grp2 = np.array([1.802, 2.399, 2.559, 2.286, 2.460, 2.511, 2.296,
1.975])
fig = plt.figure(figsize=(6, 6))
ax = fig.add_axes([0.17, 0.13, 0.6, 0.7])
# remove top and right spines and turn ticks off if no spine
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')
ax.xaxis.set_ticks_position('none')
ax.yaxis.set_ticks_position('left')
# postition of tick out
ax.tick_params(axis='both', direction='out', width=3, length=7,
labelsize=24, pad=8)
ax.spines['left'].set_linewidth(3)
# plot groups vs random numbers to create dot plot
ax.plot(np.random.normal(1, 0.05, grp2.size), grp2, 'ok', markersize=10)
ax.plot(np.random.normal(2, 0.05, grp1.size), grp1, 'ok', markersize=10)
ax.errorbar(1, np.mean(grp2), fmt='_r', markersize=50,
markeredgewidth=3)
ax.errorbar(2, np.mean(grp1), fmt='_r', markersize=50,
markeredgewidth=3)
ax.set_xlim((0.5, 3.5))
ax.set_ylim((0, 2.7))
# create right axis
ax2 = fig.add_axes(ax.get_position(), sharex=ax, frameon=False)
ax2.spines['left'].set_color('none')
ax2.spines['top'].set_color('none')
ax2.spines['bottom'].set_color('none')
ax2.xaxis.set_ticks_position('none')
ax2.yaxis.set_ticks_position('right')
# postition of tick out
ax2.tick_params(axis='both', direction='out', width=3, length=7,
labelsize=24, pad=8)
ax2.spines['right'].set_linewidth(3)
ax2.set_xticks([1, 2, 3])
ax2.set_xticklabels(('gr2', 'gr1', 'D'))
ax2.hlines(0, 0.5, 3.5, linestyle='dotted')
#ax2.hlines((np.mean(adult)-np.mean(nrvm)), 0, 3.5, linestyle='dotted')
ax2.plot(3, (np.mean(grp1)-np.mean(grp2)), 'sk', markersize=12)
# manual adjustment so the origin is aligned width left group2
ax2.set_ylim((-2.3, 0.42))
ax2.set_xlim((0.5, 3.5))
plt.show()

You can make a little function that calculates the alignment of ax2:
def align_yaxis(ax1, v1, ax2, v2):
"""adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
_, y1 = ax1.transData.transform((0, v1))
_, y2 = ax2.transData.transform((0, v2))
inv = ax2.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, y1-y2))
miny, maxy = ax2.get_ylim()
ax2.set_ylim(miny+dy, maxy+dy)
by using align_yaxis(), you can align the axes quickly:
#...... your code
# adjustment so the origin is aligned width left group2
ax2.set_ylim((0, 2.7))
align_yaxis(ax, np.mean(grp2), ax2, 0)
plt.show()

The above answer is Okay, but sometimes cuts out data, it is more thoroughly answered in the second answer here,
Matplotlib axis with two scales shared origin
or with a quick hack
def align_yaxis(ax1, v1, ax2, v2, y2min, y2max):
"""adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1."""
"""where y2max is the maximum value in your secondary plot. I haven't
had a problem with minimum values being cut, so haven't set this. This
approach doesn't necessarily make for axis limits at nice near units,
but does optimist plot space"""
_, y1 = ax1.transData.transform((0, v1))
_, y2 = ax2.transData.transform((0, v2))
inv = ax2.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, y1-y2))
miny, maxy = ax2.get_ylim()
scale = 1
while scale*(maxy+dy) < y2max:
scale += 0.05
ax2.set_ylim(scale*(miny+dy), scale*(maxy+dy))

Related

Plotting a plot with an additional y axis on the right and an additional x axis on the top, linked to the bottom one

I am trying to make a figure that has two plots, that share the same x axis on the bottom, one is linked to the left y axis, the other to the right y axis, and also have the top x-axis, which is a function of the bottom x-axis (current divided by area). Basically what I would like to have in the end is something like the attached figure on the left.
So far I can only make the plots with the left and right y axis, but I cannot find the right way to also include the top x-axis. I have run out of ideas, and I would like to request you help and suggestions on how to deal with this.
This is what I have tried so far:
# Open and Plot Data
fname = folder + r'/' +f
#print(fname)
vect = np.loadtxt(fname, delimiter=' ')
current = vect[:,0]
voltage = vect[:,1]
power = vect[:,2]
current_density=current/area1/1000 #in kA/cm^2
fig,ax1=plt.subplots()
ax1.plot(current,voltage)
#l = ax2.plot(current_density,voltage)
#l.set_visible(False)
#ax1.grid(True) #add a grid to the LIV
#ax2 = ax1.twinx()
ax2= ax1.twinx()
#ax2=ax1.twiny()
ax2.plot(current, power)
The right axis would be a twin axes, using the same x axis, but a different y axis as the original one.
The top axis would be a secondary axis, being linked to the original x axis by a functional dependence.
In total:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax_right = ax.twinx()
area = 0.226
density = lambda current: current / area / 1000
current = lambda density: density * area * 1000
ax_top = ax.secondary_xaxis("top", functions=(density, current))
ax.plot([0, 250, 565], [0,8,12], label="Voltage")
ax_right.plot([0, 300, 565], [0, 0.3, 40], label="Power", color="C3")
ax.set_xlabel("Current [mA]")
ax.set_ylabel("Voltage [V]")
ax_right.set_ylabel("Power [mW]", color="C3")
ax_top.set_xlabel("Density [kA/cm${}^2$]", color="C1")
ax_right.tick_params(axis="y", color="C3", labelcolor="C3")
ax_right.spines["right"].set_color("C3")
ax_top.set_color("C1")
ax.spines["top"].set_color("C1")
ax_right.spines["top"].set_color("C1")
plt.show()
IIUC, you can do something like this:
fig, axes = plt.subplots(1,2)
# set up clones
axes_cp = []
for i in range(2):
ax = fig.add_subplot(1,2,i+1)
ax.set_alpha(0)
ax.set_facecolor('none')
ax.xaxis.tick_top()
ax.yaxis.tick_right()
axes_cp.append(ax)
# plot
axes[0].plot([0,1], [0,1])
axes_cp[0].plot([0,100], [100,0], color='r')
x = np.linspace(0,1,100)
axes[1].plot(x, x**2)
axes_cp[1].plot(x, np.sqrt(x), color='r')
fig.tight_layout()
Output:

Plotting Curves aligned to Dynamic Time Warping Matrix

I have problems to plot two arrays with the right scaling. I use the dtw package to compare the two arrays, x and y (https://pypi.python.org/pypi/dtw/1.0). The function dtw returns a matrix and a path.
With the following code, I can plot the matrix and the path:
import matplotlib.pyplot as plt
dist, cost, acc, path = dtw(x, y, dist=lambda x, y: norm(x - y, ord=1))
plt.imshow(acc.T, origin='lower', cmap=cm.gray, interpolation='nearest')
plt.colorbar()
plt.plot(path[0], path[1], 'w')
plt.ylim((-0.5, acc.shape[1]-0.5))
plt.xlim((-0.5, acc.shape[0]-0.5))
Resulting figure:
However, I would like to plot the two curves aligned to it, like shown in (http://www.psb.ugent.be/cbd/papers/gentxwarper/DTWalgorithm.htm). One curve is above the matrix, the other one is on the left side, so that you can compare which parts are equal.
Like suggested by kwinkunks (see comment) I used this example as template. Please note that I used "plt.pcolor()" instead of "plt.image()" to plot the matrix. This is my code and the resulting figure:
'''
Plotting
'''
nullfmt = NullFormatter()
# definitions for the axes
left, width = 0.12, 0.60
bottom, height = 0.08, 0.60
bottom_h = 0.16 + width
left_h = left + 0.27
rect_plot = [left_h, bottom, width, height]
rect_x = [left_h, bottom_h, width, 0.2]
rect_y = [left, bottom, 0.2, height]
# start with a rectangular Figure
plt.figure(2, figsize=(8, 8))
axplot = plt.axes(rect_plot)
axx = plt.axes(rect_x)
axy = plt.axes(rect_y)
# Plot the matrix
axplot.pcolor(acc.T,cmap=cm.gray)
axplot.plot(path[0], path[1], 'w')
axplot.set_xlim((0, len(x)))
axplot.set_ylim((0, len(linear)))
axplot.tick_params(axis='both', which='major', labelsize=18)
# Plot time serie horizontal
axx.plot(x,'.', color='k')
axx.tick_params(axis='both', which='major', labelsize=18)
xloc = plt.MaxNLocator(4)
x2Formatter = FormatStrFormatter('%d')
axx.yaxis.set_major_locator(xloc)
axx.yaxis.set_major_formatter(x2Formatter)
# Plot time serie vertical
axy.plot(y,linear,'.',color='k')
axy.invert_xaxis()
yloc = plt.MaxNLocator(4)
xFormatter = FormatStrFormatter('%d')
axy.xaxis.set_major_locator(yloc)
axy.xaxis.set_major_formatter(xFormatter)
axy.tick_params(axis='both', which='major', labelsize=18)
#Limits
axx.set_xlim(axplot.get_xlim())
axy.set_ylim(axplot.get_ylim())
plt.show()

Why is there extra space at the bottom of this plot?

I just created a horizontal stacked bar chart using matplotlib, and I can't figure out why there is extra space between the x axis and the first bar (code and picture below). Any suggestions or questions? Thanks!
Code:
fig = figure(facecolor="white")
ax1 = fig.add_subplot(111, axisbg="white")
heights = .43
data = np.array([source['loan1'],source['loan2'],source['loan3']])
dat2 = np.array(source2)
ind=np.arange(N)
left = np.vstack((np.zeros((data.shape[1],), dtype=data.dtype), np.cumsum(data, axis=0) [:-1]))
colors = ( '#27A545', '#7D3CBD', '#C72121')
for dat, col, lefts, pname2 in zip(data, colors, left, pname):
ax1.barh(ind+(heights/2), dat, color=col, left=lefts, height = heights, align='center', alpha = .5)
p4 = ax1.barh(ind-(heights/2), dat2, height=heights, color = "#C6C6C6", align='center', alpha = .7)
ax1.spines['right'].set_visible(False)
ax1.yaxis.set_ticks_position('left')
ax1.spines['top'].set_visible(False)
ax1.xaxis.set_ticks_position('bottom')
yticks([z for z in range(N)], namelist)
#mostly for the legend
params = {'legend.fontsize': 8}
rcParams.update(params)
box = ax1.get_position()
ax1.set_position([box.x0, box.y0 + box.height * 0.1, box.width, box.height * 0.9])
l = ax1.legend(loc = 'upper center', bbox_to_anchor=(0.5,-0.05), fancybox=True, shadow = True, ncol = 4)
show()
This is because matplotlib tries to intelligently choose minimum and maximum limits for the plot (i.e. "round-ish" numbers) by default.
This makes a lot of sense for some plots, but not for others.
To disable it, just do ax.axis('tight') to snap the data limits to the strict extents of the data.
If you want a bit of padding despite the "tight" bounds on the axes limits, use ax.margins.
In your case, you'd probably want something like:
# 5% padding on the y-axis and none on the x-axis
ax.margins(0, 0.05)
# Snap to data limits (with padding specified above)
ax.axis('tight')
Also, if you want to set the extents manually, you can just do
ax.axis([xmin, xmax, ymin, ymax])`
or use set_xlim, set_ylim, or even
ax.set(xlim=[xmin, xmax], ylim=[ymin, ymax], title='blah', xlabel='etc')

Graphing tan in matplotlib

I have the following code:
from mpl_toolkits.axes_grid.axislines import SubplotZero
from matplotlib.transforms import BlendedGenericTransform
import matplotlib.pyplot as plt
import numpy
if 1:
fig = plt.figure(1)
ax = SubplotZero(fig, 111)
fig.add_subplot(ax)
ax.axhline(linewidth=1.7, color="black")
ax.axvline(linewidth=1.7, color="black")
plt.xticks([1])
plt.yticks([])
ax.text(0, 1.05, 'y', transform=BlendedGenericTransform(ax.transData, ax.transAxes), ha='center')
ax.text(1.05, 0, 'x', transform=BlendedGenericTransform(ax.transAxes, ax.transData), va='center')
for direction in ["xzero", "yzero"]:
ax.axis[direction].set_axisline_style("-|>")
ax.axis[direction].set_visible(True)
for direction in ["left", "right", "bottom", "top"]:
ax.axis[direction].set_visible(False)
x = numpy.linspace(-1, 1, 10000)
ax.plot(x, numpy.tan(2*(x - numpy.pi/2)), linewidth=1.2, color="black")
plt.ylim(-5, 5)
plt.savefig('graph.png')
which produces this graph:
As you can see, not only is the tan graph sketched, but a portion of line is added to join the asymptotic regions of the tan graph, where an asymptote would normally be.
Is there some built in way to skip that section? Or will I graph separate disjoint domains of tan that are bounded by asymptotes (if you get what I mean)?
Something you could try: set a finite threshold and modify your function to provide non-finite values after those points. Practical code modification:
yy = numpy.tan(2*(x - numpy.pi/2))
threshold = 10000
yy[yy>threshold] = numpy.inf
yy[yy<-threshold] = numpy.inf
ax.plot(x, yy, linewidth=1.2, color="black")
Results in:
This code creates a figure and one subplot for tangent function. NaN are inserted when cos(x) is tending to 0 (NaN means "Not a Number" and NaNs are not plotted or connected).
matplot-fmt-pi created by k-donn(https://pypi.org/project/matplot-fmt-pi/) used to change the formatter to make x labels and ticks correspond to multiples of π/8 in fractional format.
plot formatting (grid, legend, limits, axis) is performed as commented.
import matplotlib.pyplot as plt
import numpy as np
from matplot_fmt_pi import MultiplePi
fig, ax = plt.subplots() # creates a figure and one subplot
x = np.linspace(-2 * np.pi, 2 * np.pi, 1000)
y = np.tan(x)
y[np.abs(np.cos(x)) <= np.abs(np.sin(x[1]-x[0]))] = np.nan
# This operation inserts a NaN where cos(x) is reaching 0
# NaN means "Not a Number" and NaNs are not plotted or connected
ax.plot(x, y, lw=2, color="blue", label='Tangent')
# Set up grid, legend, and limits
ax.grid(True)
ax.axhline(0, color='black', lw=.75)
ax.axvline(0, color='black', lw=.75)
ax.set_title("Trigonometric Functions")
ax.legend(frameon=False) # remove frame legend frame
# axis formatting
ax.set_xlim(-2 * np.pi, 2 * np.pi)
pi_manager = MultiplePi(8) # number= ticks between 0 - pi
ax.xaxis.set_major_locator(pi_manager.locator())
ax.xaxis.set_major_formatter(pi_manager.formatter())
plt.ylim(top=10) # y axis limit values
plt.ylim(bottom=-10)
y_ticks = np.arange(-10, 10, 1)
plt.yticks(y_ticks)
fig
[![enter image description here][1]][1]plt.show()

Set radial axis on Matplotlib polar plots

I'm plotting an azimuth-elevation curve on a polar plot where the elevation is the radial component. By default, Matplotlib plots the radial value from 0 in the center to 90 on the perimeter. I want to reverse that so 90 degrees is at the center. I tried setting the limits with a call to ax.set_ylim(90,0) but this results in a LinAlgError exception being thrown. ax is the axes object obtained from a call to add_axes.
Can this be done and, if so, what must I do?
Edit: Here is what I'm using now. The basic plotting code was taken from one of the Matplotlib examples
# radar green, solid grid lines
rc('grid', color='#316931', linewidth=1, linestyle='-')
rc('xtick', labelsize=10)
rc('ytick', labelsize=10)
# force square figure and square axes looks better for polar, IMO
width, height = matplotlib.rcParams['figure.figsize']
size = min(width, height)
# make a square figure
fig = figure(figsize=(size, size))
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], projection='polar', axisbg='#d5de9c')
# Adjust radius so it goes 90 at the center to 0 at the perimeter (doesn't work)
#ax.set_ylim(90, 0)
# Rotate plot so 0 degrees is due north, 180 is due south
ax.set_theta_zero_location("N")
obs.date = datetime.datetime.utcnow()
az,el = azel_calc(obs, ephem.Sun())
ax.plot(az, el, color='#ee8d18', lw=3)
obs.date = datetime.datetime.utcnow()
az,el = azel_calc(obs, ephem.Moon())
ax.plot(az, el, color='#bf7033', lw=3)
ax.set_rmax(90.)
grid(True)
ax.set_title("Solar Az-El Plot", fontsize=10)
show()
The plot that results from this is
I managed to put he radial axis inverted. I had to remap the radius, in order to match the new axis:
fig = figure()
ax = fig.add_subplot(1, 1, 1, polar=True)
def mapr(r):
"""Remap the radial axis."""
return 90 - r
r = np.arange(0, 90, 0.01)
theta = 2 * np.pi * r / 90
ax.plot(theta, mapr(r))
ax.set_yticks(range(0, 90, 10)) # Define the yticks
ax.set_yticklabels(map(str, range(90, 0, -10))) # Change the labels
Note that is just a hack, the axis is still with the 0 in the center and 90 in the perimeter. You will have to use the mapping function for all the variables that you are plotting.

Categories