12

I have a plot with two y-axis on a shared x-axis, and I want to make them align at y=0 so that I can draw a horizontal line to highlight the zero tick. currently the two axis are not aligned and I have to draw two line which is terrible. How can I achieve this?

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
dingx
  • 1,621
  • 3
  • 20
  • 38

4 Answers4

9

Assuming that you created the plots with a shared axis, you just have to modify the range of y to be centered at zero or have a similar offset multiplier in both plots (i.e set ax.set_ylim(-6,6) for both plots). The following code is an example.

from matplotlib import pyplot as plt
import numpy as np

#Create some Fake Data
x =np.arange(-10,10)
y = x+np.random.rand(20)
y2 = 0.5*x-3.*np.random.rand(20)


#Figure
fig = plt.figure(figsize=(12,6))

#First subplot with zero line not even
ax1 = plt.subplot(121)
ax2 = ax1.twinx()

ax1.plot(x,y,c='r')
ax2.plot(x,y2,c='b')

ax1.axhline(0)

#Second Subplot with zero line the same on both axes
ax3 = plt.subplot(122)
ax4 = ax3.twinx()

ax3.plot(x,y,c='r')
ax4.plot(x,y2,c='b')

ax3.axhline(0)

#If you set your limits on both sides to have the same interval you will get the same zero line
ax3.set_ylim(-10,10)
ax4.set_ylim(-6,6)

plt.show()

enter image description here

BenT
  • 3,172
  • 3
  • 18
  • 38
  • So we need to define the axis ranges manually to ensure they are proportional. No possibility to keep the scales automatic but with the zeroes linked? – mins Oct 28 '20 at 20:25
  • There are definitely ways to automate this, but when you create the subplot axis in matplotlib the bounds are set based on the range of values. So the automation would be done on the data and finding the best edges to keep the zero lines the same. – BenT Oct 29 '20 at 17:44
7

I had the same issue, and what I did was to change the extents of the y-axis, depending on the ratio of the min to max limits. If you set the ratio of the y-axes to be the same, the zero point should be the same.

fig, ax1 = plt.subplots()

ax1.plot(...)     # Plot first data set
ax2 = ax1.twinx()
ax2.plot(...)     # Plot second data set

ax1_ylims = ax1.axes.get_ylim()           # Find y-axis limits set by the plotter
ax1_yratio = ax1_ylims[0] / ax1_ylims[1]  # Calculate ratio of lowest limit to highest limit

ax2_ylims = ax2.axes.get_ylim()           # Find y-axis limits set by the plotter
ax2_yratio = ax2_ylims[0] / ax2_ylims[1]  # Calculate ratio of lowest limit to highest limit


# If the plot limits ratio of plot 1 is smaller than plot 2, the first data set has
# a wider range range than the second data set. Calculate a new low limit for the
# second data set to obtain a similar ratio to the first data set.
# Else, do it the other way around

if ax1_yratio < ax2_yratio: 
    ax2.set_ylim(bottom = ax2_ylims[1]*ax1_yratio)
else:
    ax1.set_ylim(bottom = ax1_ylims[1]*ax2_yratio)

plt.tight_layout()
plt.show()

This is my first answer, so I hope it is sufficient and okay.

KobusNell
  • 71
  • 1
  • 3
  • Doesn't that work only for positive y axis values? – KobeJohn Jan 30 '23 at 06:43
  • 1
    I believe some logic is required, yes. I will need to test and review. When I get a chance I will try to update. I THINK the above should still give the same zero point, but I think there can be situations where the data is not shown properly, yes. – KobusNell May 15 '23 at 14:02
  • Thanks. I could not find this answer with traditional search, but phind pointed me to it as the source for its answer. Your code is now part of the vast body of human knowledge :) – IanS May 23 '23 at 14:02
5

I found this excellent library which aligns the axes and keeps automatic scaling.

Install

pip install mpl-axes-aligner

Usage

import numpy as np
import matplotlib.pyplot as plt
import mpl_axes_aligner

x = np.arange(0.0, 30, 0.1)
y1 = 0.1 * x * np.sin(x)
y2 = 0.001*x**3 - 0.03*x**2 + 0.12*x

fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()

ax1.plot(x, y1, color='blue', label='Plot 1')
ax2.plot(x, y2, color='red', label='Plot 2')

# Align y = 0 of ax1 and ax2 with the center of figure.
mpl_axes_aligner.align.yaxes(ax1, 0, ax2, 0, 0.5)

plt.show()

Output

Output graph

Credits

This package was developed by ryotuk and the usage example above is from his package's documentation.

abrac
  • 521
  • 5
  • 12
1

Here's another way, which I think is more versatile.

import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt

def align_zeros(axes):

    ylims_current = {}   #  Current ylims
    ylims_mod     = {}   #  Modified ylims
    deltas        = {}   #  ymax - ymin for ylims_current
    ratios        = {}   #  ratio of the zero point within deltas

    for ax in axes:
        ylims_current[ax] = list(ax.get_ylim())
                        # Need to convert a tuple to a list to manipulate elements.
        deltas[ax]        = ylims_current[ax][1] - ylims_current[ax][0]
        ratios[ax]        = -ylims_current[ax][0]/deltas[ax]
    
    for ax in axes:      # Loop through all axes to ensure each ax fits in others.
        ylims_mod[ax]     = [np.nan,np.nan]   # Construct a blank list
        ylims_mod[ax][1]  = max(deltas[ax] * (1-np.array(list(ratios.values()))))
                        # Choose the max value among (delta for ax)*(1-ratios),
                        # and apply it to ymax for ax
        ylims_mod[ax][0]  = min(-deltas[ax] * np.array(list(ratios.values())))
                        # Do the same for ymin
        ax.set_ylim(tuple(ylims_mod[ax]))

x = np.array(range(1,11))
y1 = 5*x-10
y2 = -10*x+5

fig = plt.figure()
ax1 = fig.add_subplot(111)

ax1.plot(x,y1,'r',label='y1')
ax1.set_ylabel('y1')

ax2 = ax1.twinx()
ax2.plot(x,y2,'g',label='y2')
ax2.set_ylabel('y2')

align_zeros([ax1,ax2])

ax1.legend(loc='upper left')
ax2.legend(loc='upper right')

plt.show()

enter image description here

Hyde Fukui
  • 11
  • 1