Matplotlib bar charts: Aligning two different y axes to zero - matplotlib

I have two sets of data in a barchart which have very different axes: one is very negative (-7500) and one is slightly positive (+5).
How can I have the two y axes aligned at 0, yet still be a good size? Using set_ylim means you can't see the second data set.
Current code I'm using:
A165H = [-4915, -7037]
B167H = [-6927, -4105]
B186H = [-5597, 0]
CH =[0, 0]
ConH = [0, 0]
# Lists of dS values
A165S = [6.28,-4.91]
B167S = [-3.25, 6.7]
B186S = [3.93, 0]
CS = [0, 0]
ConS = [0, 0]
N1H = [A165H[0], B167H[0], B186H[0], CH[0], ConH[0]]
N1S = [A165S[0], B167S[0], B186S[0], CS[0], ConS[0]]
print(N1H)
print(N1S)
N2H = [A165H[1], B167H[1], B186H[1], CH[1], ConH[1]]
N2S = [A165S[1], B167S[1], B186S[1], CS[1], ConS[1]]
width = 0.35
fig, ax1 = plt.subplots()
ind = np.arange(len(N1H))
rects1 = ax1.bar(ind, N1H, width, color = 'b')
ax1.set_xticks(ind+width)
ax1.set_xticklabels(('A165', 'B167', 'B186', 'C', 'Con'))
ax1.set_ylabel('dH', color='b')
for tl in ax1.get_yticklabels():
tl.set_color('b')
ax2 = ax1.twinx()
rects2 = ax2.bar(ind + width, N1S, width, color = 'r')
ax2.set_ylabel('dS', color='r')
for tl in ax2.get_yticklabels():
tl.set_color('r')
plt.show()
Here is my standard image
EDIT:
using the align_yaxis() from this question only shows me the negative values of the second data set:

If I had carried on reading the the other post I would have found the adjust_yaxis which solved my problem
The code given on that answer:
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))
adjust_yaxis(ax2,(y1-y2)/2,v2)
adjust_yaxis(ax1,(y2-y1)/2,v1)
def adjust_yaxis(ax,ydif,v):
"""shift axis ax by ydiff, maintaining point v at the same location"""
inv = ax.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
miny, maxy = ax.get_ylim()
miny, maxy = miny - v, maxy - v
if -miny>maxy or (-miny==maxy and dy > 0):
nminy = miny
nmaxy = miny*(maxy+dy)/(miny+dy)
else:
nmaxy = maxy
nminy = maxy*(miny+dy)/(maxy+dy)
ax.set_ylim(nminy+v, nmaxy+v)

Related

Is there a way to propelly align the two yAxis

The problem is the y_h = 1 is not aligned properly with the y = 0.3 and so on, how do i fix this? Below i left a picture of the problem im facing and the code.
yaxis's not aligned
x = 2
y = np.array([0.3, 0.11, 0.43])
y_h = np.array([1, 2, 3])
fig, host = plt.subplots(figsize=(8, 5))
fig.subplots_adjust(right=0.75)
colormap = plt.get_cmap("gist_rainbow")
colors = [colormap(i) for i in np.linspace(0, 1, y.size)]
line = host.twinx()
#plot the lines the give them colors and labels
for i in range(y.size):
p = line.plot([x - x, x], [y_h[i], y[i]], color=colors[i], label="p"+str(i))
#append the line labels to a list
'''for i in range(y.size):
pis.append('p'+str(i))'''
host.set_xlim(0, x)
host.set_ylim(0, y_h.size)
line.set_ylim(0, max(y))
host.set_xlabel("Rev_Count")
host.set_ylabel("Value")
line.set_ylabel('Value_Header')
plt.show()

How can I have a colormap legend for different circle labels

I am drawing different circles with Matplotlib. Each circle has a label, and each label has a colour. What can I do to have a colourmap legend for these different labels?
I have tried a lot of solutions online, including the most naive one by just adding plt.colorbar(), which I will get the error
RuntimeError('No mappable was found to use for colorbar')
Here is my complete code. It's a little bit long. Please note that the key part just starts from if labels is None:. I just include everything for completeness.
def plot_gaussian_circles(loc_list, scale_list, save_path=None, sigma_coe=3, num_to_plot=300, labels=None):
mu_x_max = -float('inf')
mu_y_max = -float('inf')
mu_x_min = float('inf')
mu_y_min = float('inf')
color_idx = 0
rvs = []
lim_loc_list = loc_list[:num_to_plot]
lim_scale_list = scale_list[:num_to_plot]
for a_mu_, a_sigma_ in zip(lim_loc_list, lim_scale_list):
a_mu = a_mu_.squeeze()
a_sigma_ = a_sigma_.squeeze()
if not type(a_sigma_) is np.ndarray:
a_sigma_ = a_sigma_.numpy()
radius = sigma_coe * np.max(a_sigma_)
a_mu_x = a_mu[0]
a_mu_y = a_mu[1]
if (a_mu_x + radius) >= mu_x_max:
mu_x_max = a_mu_x + radius
if (a_mu_x - radius) <= mu_x_min:
mu_x_min = a_mu_x - radius
if (a_mu_y + radius) >= mu_y_max:
mu_y_max = a_mu_y + radius
if (a_mu_y - radius) <= mu_y_min:
mu_y_min = a_mu_y - radius
if labels is None:
rv = plt.Circle(a_mu, radius, fill=False, clip_on=False)
else:
colors = cm.rainbow(np.linspace(0, 1, len(set(labels))))
rv = plt.Circle(a_mu, radius, color=colors[labels[color_idx]], fill=False, clip_on=False)
rvs.append(rv)
color_idx = (color_idx + 1)
fig, ax = plt.subplots()
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
axes = plt.gca()
axes.set_xlim([mu_x_min - 1, mu_x_max + 1])
axes.set_ylim([mu_y_min - 1, mu_y_max + 1])
for rv in rvs:
ax.add_artist(rv)
if not(labels is None):
# plt.legend(colors, list(range(len(set(labels)))))
plt.colorbar()
if save_path is None:
plt.plot()
plt.show()
# plt.savefig('plotcircles_test.png')
else:
plt.savefig(save_path, dpi=200)
The image here is currently what I am getting, while I wish to have a legend of the colormap.
I found doing this will have the colour for the circles. Thanks ImportanceOfBeingErnest's comment for suggesting PatchCollection.
p = PatchCollection(rvs, cmap=cm.jet, alpha=0.4)
p.set_array(labels)
ax.add_collection(p)
fig.colorbar(p, ax=ax)

Rotating labels on second axis

I am adding a second x-axis to my plot like this:
ax2 = ax.twiny()
offset = 0, -25
new_axisline = ax2.get_grid_helper().new_fixed_axis
ax2.axis["bottom"] = new_axisline(loc="bottom", axes=ax2, offset=offset)
ax2.axis["top"].set_visible(False)
ax2.set_xticks(xticks)
ax2.xaxis.set_major_formatter(ticker.NullFormatter())
ax2.xaxis.set_minor_locator(ticker.FixedLocator(xticks))
ax2.xaxis.set_minor_formatter(ticker.FixedFormatter(xticks_labels))
the problem is I don't know how I can rotate the labels from there.
Also: If I add ticks to my first axis:
plt.xticks(xticks1, xticks1_labels, rotation='vertical')
the rotation argument gets ignored and I don't understand why either.
I have tried
ax2.set_xticklabels(ax2.xaxis.get_minorticklabels(), rotation=45)
but it also has no effect.
Any help would be appreciated.
You can take a look at the complete plotting logic below:
def event_plot(event_list, labels=None, figsize=(16, 9), padding=0.85, grid=False, title=None, colors=None):
fig = plt.figure(figsize=figsize)
ax = SubplotHost(fig, 111)
# ax = fig.add_subplot(111)
fig.add_subplot(ax)
ax.grid(grid)
if title is not None:
ax.set_title(title)
max_end = 0
for i, events in enumerate(event_list):
for event in events:
start = event[0]
end = event[1]
max_end = max(max_end, end)
y = (i, i + padding)
c = 'red' if colors is None else colors[i]
plt.fill_between([start, end], y[0], y2=y[1], color=c, alpha=0.35, linewidth=0.0)
plt.legend(['Recording data available for channel'], loc='upper center')
if labels is not None:
labels_ids = np.asarray(range(len(labels))) + 1
labels_y = labels_ids - 0.5 - (1 - padding) / 2.
plt.yticks(labels_y, labels)
for y in labels_y:
plt.axhline(y, alpha=0.125, color='k', linestyle='--')
return ax, fig
def plot_case_windows(all_records, case_windows, filename_title, filename=None):
channel_event_list = list()
labels = list()
for group_name, group in all_records.channel_groups.items():
[(labels.append(x[0]), channel_event_list.append(x[1])) for x in group.items()]
recording_time = all_records.end - all_records.start
title = 'File: {:s}, recording time: {:d} sec'.format(os.path.basename(filename_title), int(recording_time))
ax1, fig = event_plot(channel_event_list, title=title, labels=labels)
xticksmax = 0
xticksmin = float('Inf')
xticks1 = list()
xticks1_labels = list()
xticks2 = list()
xticks2_labels = list()
for case_win in case_windows:
xticks1.append(int(case_win.start + (case_win.end - case_win.start)/2.))
xticks2.append(case_win.start)
xticksmax = max(xticksmax, case_win.end)
xticksmin = min(xticksmin, case_win.start)
xticks1_labels.append(case_win.name)
xticks2_labels.append(str(case_win.start) + ' s')
plt.axvline(x=case_win.start, color='k', linestyle='--')
plt.axvline(x=case_win.end, color='k', linestyle='--')
xticks2 = (np.asarray(xticks2) - xticksmin) / (xticksmax - xticksmin)
plt.xlim([xticksmin, xticksmax])
ax1.set_xticks(xticks1)
ax1.xaxis.set_major_formatter(ticker.NullFormatter())
ax1.xaxis.set_minor_locator(ticker.FixedLocator(xticks1))
ax1.xaxis.set_minor_formatter(ticker.FixedFormatter(xticks1_labels))
ax2 = ax1.twiny()
offset = 0, -20
new_axisline = ax2.get_grid_helper().new_fixed_axis
ax2.axis["bottom"] = new_axisline(loc="bottom", axes=ax2, offset=offset)
ax2.axis["top"].set_visible(False)
ax2.set_xticks(xticks2)
ax2.xaxis.set_major_formatter(ticker.NullFormatter())
ax2.xaxis.set_minor_locator(ticker.FixedLocator(xticks2))
ax2.xaxis.set_minor_formatter(ticker.FixedFormatter(xticks2_labels))
plt.setp(ax2.xaxis.get_minorticklabels(), rotation=45)
# ax2.set_xticklabels(ax1.xaxis.get_minorticklabels(), rotation=45)
#plt.show()
plt.tight_layout()
if filename:
plt.savefig(filename)

How to draw polygons around 3D coordinates while also wrapping round the box?

I have points plotted in 3D space, with four polygons that slice through the data cube at angles to the y and x axes.
I would like to, for each black data point in the box, work out the coordinates of the vertices of a square centred on the data point. Using these coordinates I can draw a polgyon, in the same way that I am doing now. The dimensions of this square would need to be the same as the width value defined on line 7. The square drawn must be inclined so it lies exactly on the existing plotted plane.
Does anybody know the best way to approach this? The other difficult thing would be that, if the square leaves the box, it should wrap round the other side of the box. The box can be stacked horizontallyand vertically with identical boxes, tiling infinitely.
My code can be found belw (sorry it's messy):
import matplotlib.pyplot as pyplot
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
angle = np.arctan2(1,4)
xangle = np.arctan2(1,4)
width = 100/np.cos(angle)
print 'angle:', angle*(180/np.pi)
print 'xangle:', xangle*(180/np.pi)
print 'width:', width
x1 = [0, 0, 0, 0, 50, 50, 50, 50]
y1 = [50,50,50,50,50,50,50,50]
z1 = [12.5,37.5,62.5,87.5,25,50,75,0]
x2 = [0,0,0,0,0,0,0,0,50,50,50,50,50,50,50,50]
y2 = [0,50,0,50,0,50,0,50,0,50,0,50,0,50,0,50]
z2 = [0,12.5,25,37.5,50,62.5,75,87.5,12.5,25,37.5,50,62.5,75,87.5,0]
fig = pyplot.figure()
ax = fig.add_subplot(111, projection = '3d')
ax.set_xlim(0,100)
ax.set_ylim(0,100)
ax.set_zlim(0,100)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
#ax.view_init(elev=90, azim=90)
#ax.scatter(x1, y1, z1, zdir='z', s=20, c='g')
ax.scatter(x2, y2, z2, zdir='z', s=20, c='r') #THESE ARE RICHARD'S COORDINATES notice how they do not lie on the plane
xa = [0,100,100,0]
ya = [0,0,100,100]
za = [0,-6.25,18.75,25]
verts = [zip(xa,ya,za)]
ax.add_collection3d(Poly3DCollection(verts))
xb = [0,100,100,0]
yb = [0,0,100,100]
zb = [25,-6.25+25,18.75+25,50]
verts = [zip(xb,yb,zb)]
ax.add_collection3d(Poly3DCollection(verts))
xc = [0,100,100,0]
yc = [0,0,100,100]
zc = [50,-6.25+25*2,18.75+25*2,75]
verts = [zip(xc,yc,zc)]
ax.add_collection3d(Poly3DCollection(verts))
xd = [0,100,100,0]
yd = [0,0,100,100]
zd = [75,-6.25+25*3,18.75+25*3,100]
verts = [zip(xd,yd,zd)]
ax.add_collection3d(Poly3DCollection(verts))
#pyplot.show()
x = [0]
y = [0]
z = [0]
for i in range(1,100):
new_x = x[(len(x)-1)] + 50
new_y = y[(len(y)-1)] + 12.5
new_z = z[(len(z)-1)]
if new_x >= 100:
new_x = new_x - 100
new_z = new_z + 6.25
if new_y >= 100:
new_y = new_y - 100
if new_z >= 100:
new_z = new_z - 100
if new_x == 0 and new_y == 0 and new_z == 0:
print 'done!', i
x.append(new_x)
y.append(new_y)
z.append(new_z)
ax.scatter(x, y, z, zdir='z', s=20, c='k', zorder=1)
pyplot.show()
Done! (Without wrapping around the box though..)
import matplotlib.pyplot as pyplot
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import random
angle = np.arctan2(1,4)
xangle = np.arctan2(1,4)
width = 100/np.cos(angle)
print 'angle:', angle*(180/np.pi)
print 'xangle:', xangle*(180/np.pi)
print 'width:', width
def totuple(a):
try:
return tuple(totuple(i) for i in a)
except TypeError:
return a
def plotSquare(ax,cx,cy,cz, yaw, pitch, diameter):
colour1 = random.randint(0,255)/256.0
colour2 = random.randint(0,255)/256.0
colour3 = random.randint(0,255)/256.0
centre = np.array([cx,cy,cz])
radius = diameter/2
#0,0,0 to 100,25,125 diff
#100,25,125 /2
#50,12.5,62.5 /50
#1.0,0.25,1.25
d1offset = np.array([np.cos(pitch)*radius,(np.cos(yaw)*radius)+(np.sin(pitch)*radius),np.sin(yaw)*radius])
c1 = centre - d1offset
c2 = centre + d1offset
#100,0,25 to 0,25,100 diff
#-100,25,75 /2
#-50,12.5,37.5 /50
#-1.0,0.25,0.75
d2offset = np.array([-np.cos(yaw)*radius,(np.cos(pitch)*radius)-(np.sin(yaw)*radius),np.sin(pitch)*radius])
c3 = centre - d2offset
c4 = centre + d2offset
verts = [[totuple(c1),totuple(c3),totuple(c2),totuple(c4)]]
ax.add_collection3d(Poly3DCollection(verts, facecolors=[colour1,colour2,colour3]))
x1 = [0, 0, 0, 0, 50, 50, 50, 50]
y1 = [50,50,50,50,50,50,50,50]
z1 = [12.5,37.5,62.5,87.5,25,50,75,0]
x2 = [0,0,0,0,0,0,0,0,50,50,50,50,50,50,50,50]
y2 = [0,50,0,50,0,50,0,50,0,50,0,50,0,50,0,50]
z2 = [0,12.5,25,37.5,50,62.5,75,87.5,12.5,25,37.5,50,62.5,75,87.5,0]
fig = pyplot.figure()
ax = fig.add_subplot(111, projection = '3d')
ax.set_xlim(0,100)
ax.set_ylim(0,100)
ax.set_zlim(0,100)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
#ax.view_init(elev=90, azim=90)
#ax.scatter(x1, y1, z1, zdir='z', s=20, c='g')
ax.scatter(x2, y2, z2, zdir='z', s=20, c='r') #EXISTING COORDINATES notice how they do not lie on the plane
xa = [0,100,100,0]
ya = [0,0,100,100]
za = [0,-6.25,18.75,25]
verts = [zip(xa,ya,za)]
ax.add_collection3d(Poly3DCollection(verts))
xb = [0,100,100,0]
yb = [0,0,100,100]
zb = [25,-6.25+25,18.75+25,50]
verts = [zip(xb,yb,zb)]
ax.add_collection3d(Poly3DCollection(verts))
xc = [0,100,100,0]
yc = [0,0,100,100]
zc = [50,-6.25+25*2,18.75+25*2,75]
verts = [zip(xc,yc,zc)]
ax.add_collection3d(Poly3DCollection(verts))
xd = [0,100,100,0]
yd = [0,0,100,100]
zd = [75,-6.25+25*3,18.75+25*3,100]
verts = [zip(xd,yd,zd)]
ax.add_collection3d(Poly3DCollection(verts))
x = [0]
y = [0]
z = [0]
for i in range(1,100):
new_x = x[(len(x)-1)] + 50
new_y = y[(len(y)-1)] + 12.5
new_z = z[(len(z)-1)]
if new_x >= 100:
new_x = new_x - 100
new_z = new_z + 6.25
#new_y = new_y + (50-12.5)
if new_y >= 100:
new_y = new_y - 100
if new_z >= 100:
new_z = new_z - 100
if new_x == 0 and new_y == 0 and new_z == 0:
print 'done!', i
x.append(new_x)
y.append(new_y)
z.append(new_z)
ax.scatter(x, y, z, zdir='z', s=20, c='k', zorder=1)
storedcoordinates = zip(x,y,z)
for x,y,z in storedcoordinates:
plotSquare(ax,x,y,z,xangle,angle, width/2)
pyplot.show()

Matplotlib axis with two scales shared origin

I need two overlay two datasets with different Y-axis scales in Matplotlib. The data contains both positive and negative values. I want the two axes to share one origin, but Matplotlib does not align the two scales by default.
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax1.bar(range(6), (2, -2, 1, 0, 0, 0))
ax2.plot(range(6), (0, 2, 8, -2, 0, 0))
plt.show()
I suppose it is possible to perform some computation with .get_ylim() and .set_ylim() two align the two scales. Is there an easier solution?
use the align_yaxis() function:
import numpy as np
import matplotlib.pyplot as plt
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)
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax1.bar(range(6), (2, -2, 1, 0, 0, 0))
ax2.plot(range(6), (0, 2, 8, -2, 0, 0))
align_yaxis(ax1, 0, ax2, 0)
plt.show()
In order to ensure that the y-bounds are maintained (so no data points are shifted off the plot), and to balance adjustment of both y-axes, I made some additions to #HYRY's answer:
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))
adjust_yaxis(ax2,(y1-y2)/2,v2)
adjust_yaxis(ax1,(y2-y1)/2,v1)
def adjust_yaxis(ax,ydif,v):
"""shift axis ax by ydiff, maintaining point v at the same location"""
inv = ax.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
miny, maxy = ax.get_ylim()
miny, maxy = miny - v, maxy - v
if -miny>maxy or (-miny==maxy and dy > 0):
nminy = miny
nmaxy = miny*(maxy+dy)/(miny+dy)
else:
nmaxy = maxy
nminy = maxy*(miny+dy)/(maxy+dy)
ax.set_ylim(nminy+v, nmaxy+v)
#drevicko's answer fails for me when plotting the following two sequences of points:
l1 = [0.03, -0.6, 1, 0.05]
l2 = [0.8, 0.9, 1, 1.1]
fig, ax1 = plt.subplots()
ax1.plot(l1)
ax2 = ax1.twinx()
ax2.plot(l2, color='r')
align_yaxis(ax1, 0, ax2, 0)
... so here's my version:
def align_yaxis(ax1, ax2):
"""Align zeros of the two axes, zooming them out by same ratio"""
axes = (ax1, ax2)
extrema = [ax.get_ylim() for ax in axes]
tops = [extr[1] / (extr[1] - extr[0]) for extr in extrema]
# Ensure that plots (intervals) are ordered bottom to top:
if tops[0] > tops[1]:
axes, extrema, tops = [list(reversed(l)) for l in (axes, extrema, tops)]
# How much would the plot overflow if we kept current zoom levels?
tot_span = tops[1] + 1 - tops[0]
b_new_t = extrema[0][0] + tot_span * (extrema[0][1] - extrema[0][0])
t_new_b = extrema[1][1] - tot_span * (extrema[1][1] - extrema[1][0])
axes[0].set_ylim(extrema[0][0], b_new_t)
axes[1].set_ylim(t_new_b, extrema[1][1])
There are in principle infinite different possibilities to align the zeros (or other values, which the other provided solutions accept): wherever you place zero on the y axis, you can zoom each of the two series so that it fits. We just pick the position such that, after the transformation, the two cover a vertical interval of same height.
Or in other terms, we minimize them of a same factor compared to the non-aligned plot.
(This does not mean that 0 is at half of the plot: this will happen e.g. if one plot is all negative and the other all positive.)
Numpy version:
def align_yaxis_np(ax1, ax2):
"""Align zeros of the two axes, zooming them out by same ratio"""
axes = np.array([ax1, ax2])
extrema = np.array([ax.get_ylim() for ax in axes])
tops = extrema[:,1] / (extrema[:,1] - extrema[:,0])
# Ensure that plots (intervals) are ordered bottom to top:
if tops[0] > tops[1]:
axes, extrema, tops = [a[::-1] for a in (axes, extrema, tops)]
# How much would the plot overflow if we kept current zoom levels?
tot_span = tops[1] + 1 - tops[0]
extrema[0,1] = extrema[0,0] + tot_span * (extrema[0,1] - extrema[0,0])
extrema[1,0] = extrema[1,1] + tot_span * (extrema[1,0] - extrema[1,1])
[axes[i].set_ylim(*extrema[i]) for i in range(2)]
The other answers here seem overly complicated and don't necessarily work for all the scenarios (e.g. ax1 is all negative and ax2 is all positive). There are 2 easy methods that always work:
Always put 0 in the middle of the graph for both y axes
A bit fancy and somewhat preserves the positive-to-negative ratio, see below
def align_yaxis(ax1, ax2):
y_lims = numpy.array([ax.get_ylim() for ax in [ax1, ax2]])
# force 0 to appear on both axes, comment if don't need
y_lims[:, 0] = y_lims[:, 0].clip(None, 0)
y_lims[:, 1] = y_lims[:, 1].clip(0, None)
# normalize both axes
y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
y_lims_normalized = y_lims / y_mags
# find combined range
y_new_lims_normalized = numpy.array([numpy.min(y_lims_normalized), numpy.max(y_lims_normalized)])
# denormalize combined range to get new axes
new_lim1, new_lim2 = y_new_lims_normalized * y_mags
ax1.set_ylim(new_lim1)
ax2.set_ylim(new_lim2)
I've cooked up a solution starting from the above that will align any number of axes:
def align_yaxis_np(axes):
"""Align zeros of the two axes, zooming them out by same ratio"""
axes = np.array(axes)
extrema = np.array([ax.get_ylim() for ax in axes])
# reset for divide by zero issues
for i in range(len(extrema)):
if np.isclose(extrema[i, 0], 0.0):
extrema[i, 0] = -1
if np.isclose(extrema[i, 1], 0.0):
extrema[i, 1] = 1
# upper and lower limits
lowers = extrema[:, 0]
uppers = extrema[:, 1]
# if all pos or all neg, don't scale
all_positive = False
all_negative = False
if lowers.min() > 0.0:
all_positive = True
if uppers.max() < 0.0:
all_negative = True
if all_negative or all_positive:
# don't scale
return
# pick "most centered" axis
res = abs(uppers+lowers)
min_index = np.argmin(res)
# scale positive or negative part
multiplier1 = abs(uppers[min_index]/lowers[min_index])
multiplier2 = abs(lowers[min_index]/uppers[min_index])
for i in range(len(extrema)):
# scale positive or negative part based on which induces valid
if i != min_index:
lower_change = extrema[i, 1] * -1*multiplier2
upper_change = extrema[i, 0] * -1*multiplier1
if upper_change < extrema[i, 1]:
extrema[i, 0] = lower_change
else:
extrema[i, 1] = upper_change
# bump by 10% for a margin
extrema[i, 0] *= 1.1
extrema[i, 1] *= 1.1
# set axes limits
[axes[i].set_ylim(*extrema[i]) for i in range(len(extrema))]
example on 4 random series (you can see the discrete ranges on the 4 separate sets of y axis labels):
#Tim's solution adapted to work for more than two axes:
import numpy as np
def align_yaxis(axes):
y_lims = np.array([ax.get_ylim() for ax in axes])
# force 0 to appear on all axes, comment if don't need
y_lims[:, 0] = y_lims[:, 0].clip(None, 0)
y_lims[:, 1] = y_lims[:, 1].clip(0, None)
# normalize all axes
y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
y_lims_normalized = y_lims / y_mags
# find combined range
y_new_lims_normalized = np.array([np.min(y_lims_normalized), np.max(y_lims_normalized)])
# denormalize combined range to get new axes
new_lims = y_new_lims_normalized * y_mags
for i, ax in enumerate(axes):
ax.set_ylim(new_lims[i])
I needed to align two subplots but not at their zeros. And other solutions didn't quite work for me.
The main code of my program looks like this. The subplots are not aligned. Further I only change align_yaxis function and keep all other code the same.
import matplotlib.pyplot as plt
def align_yaxis(ax1, v1, ax2, v2):
return 0
x = range(10)
y1 = [3.2, 1.3, -0.3, 0.4, 2.3, -0.9, 0.2, 0.1, 1.3, -3.4]
y2, s = [], 100
for i in y1:
s *= 1 + i/100
y2.append(s)
fig = plt.figure()
ax1 = fig.add_subplot()
ax2 = ax1.twinx()
ax1.axhline(y=0, color='k', linestyle='-', linewidth=0.5)
ax1.bar(x, y1, color='tab:blue')
ax2.plot(x, y2, color='tab:red')
fig.tight_layout()
align_yaxis(ax1, 0, ax2, 100)
plt.show()
Picture of not aligned subplots
Using #HYRY's solution I get aligned subplots, but the second subplot is out of the figure. You can't see it.
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)
Picture without second subplot
Using #drevicko's solution I also get aligned plot. But now the first subplot is out of the picture and first Y axis is quite weird.
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))
adjust_yaxis(ax2,(y1-y2)/2,v2)
adjust_yaxis(ax1,(y2-y1)/2,v1)
def adjust_yaxis(ax,ydif,v):
"""shift axis ax by ydiff, maintaining point v at the same location"""
inv = ax.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
miny, maxy = ax.get_ylim()
miny, maxy = miny - v, maxy - v
if -miny>maxy or (-miny==maxy and dy > 0):
nminy = miny
nmaxy = miny*(maxy+dy)/(miny+dy)
else:
nmaxy = maxy
nminy = maxy*(miny+dy)/(maxy+dy)
ax.set_ylim(nminy+v, nmaxy+v)
Picture without firstsubplot
So I've tuned #drevicko's solution a little and got what I wanted.
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))
adjust_yaxis(ax1,(y2 - y1)/2,v1)
adjust_yaxis(ax2,(y1 - y2)/2,v2)
def adjust_yaxis(ax,ydif,v):
"""shift axis ax by ydiff, maintaining point v at the same location"""
inv = ax.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
miny, maxy = ax.get_ylim()
nminy = miny - v + dy - abs(dy)
nmaxy = maxy - v + dy + abs(dy)
ax.set_ylim(nminy+v, nmaxy+v)
Subplots as I've expected them to look
This might not be what you are looking for but this helped me get whole numbers to line up on two different vertical axis:
ax1.set_ylim(0,4000)
ax2.set_ylim(0,120)
ax2.set_yticks(np.linspace(ax2.get_yticks()[0], ax2.get_yticks()[-1], len(ax1.get_yticks())))