106

How can multiple scales can be implemented in Matplotlib? I am not talking about the primary and secondary axis plotted against the same x-axis, but something like many trends which have different scales plotted in same y-axis and that can be identified by their colors.

For example, if I have trend1 ([0,1,2,3,4]) and trend2 ([5000,6000,7000,8000,9000]) to be plotted against time and want the two trends to be of different colors and in Y-axis, different scales, how can I accomplish this with Matplotlib?

When I looked into Matplotlib, they say that they don't have this for now though it is definitely on their wishlist, Is there a way around to make this happen?

Are there any other plotting tools for python that can make this happen?

naught101
  • 18,687
  • 19
  • 90
  • 138
Jack_of_All_Trades
  • 10,942
  • 18
  • 58
  • 88
  • A more recent example has been provided by Matthew Kudija [here](https://github.com/mkudija/blog/tree/master/content/downloads/code/matplotlib-twin-axes). – Mike O'Connor Nov 17 '19 at 11:18

3 Answers3

127

Since Steve Tjoa's answer always pops up first and mostly lonely when I search for multiple y-axes at Google, I decided to add a slightly modified version of his answer. This is the approach from this matplotlib example.

Reasons:

  • His modules sometimes fail for me in unknown circumstances and cryptic intern errors.
  • I don't like to load exotic modules I don't know (mpl_toolkits.axisartist, mpl_toolkits.axes_grid1).
  • The code below contains more explicit commands of problems people often stumble over (like single legend for multiple axes, using viridis, ...) rather than implicit behavior.

Plot

import matplotlib.pyplot as plt 

# Create figure and subplot manually
# fig = plt.figure()
# host = fig.add_subplot(111)

# More versatile wrapper
fig, host = plt.subplots(figsize=(8,5), layout='constrained') # (width, height) in inches
# (see https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.subplots.html and
# .. https://matplotlib.org/stable/tutorials/intermediate/constrainedlayout_guide.html)
    
ax2 = host.twinx()
ax3 = host.twinx()
    
host.set_xlim(0, 2)
host.set_ylim(0, 2)
ax2.set_ylim(0, 4)
ax3.set_ylim(1, 65)
    
host.set_xlabel("Distance")
host.set_ylabel("Density")
ax2.set_ylabel("Temperature")
ax3.set_ylabel("Velocity")

color1, color2, color3 = plt.cm.viridis([0, .5, .9])

p1 = host.plot([0, 1, 2], [0, 1, 2],    color=color1, label="Density")
p2 = ax2.plot( [0, 1, 2], [0, 3, 2],    color=color2, label="Temperature")
p3 = ax3.plot( [0, 1, 2], [50, 30, 15], color=color3, label="Velocity")

host.legend(handles=p1+p2+p3, loc='best')

# right, left, top, bottom
ax3.spines['right'].set_position(('outward', 60))

# no x-ticks                 
host.xaxis.set_ticks([])

# Alternatively (more verbose):
# host.tick_params(
#     axis='x',          # changes apply to the x-axis
#     which='both',      # both major and minor ticks are affected
#     bottom=False,      # ticks along the bottom edge are off)
#     labelbottom=False) # labels along the bottom edge are off
# sometimes handy:  direction='in'    

# Move "Velocity"-axis to the left
# ax3.spines['left'].set_position(('outward', 60))
# ax3.spines['left'].set_visible(True)
# ax3.spines['right'].set_visible(False)
# ax3.yaxis.set_label_position('left')
# ax3.yaxis.set_ticks_position('left')

host.yaxis.label.set_color(p1[0].get_color())
ax2.yaxis.label.set_color(p2[0].get_color())
ax3.yaxis.label.set_color(p3[0].get_color())

# For professional typesetting, e.g. LaTeX, use .pgf or .pdf
# For raster graphics use the dpi argument. E.g. '[...].png", dpi=300)'
plt.savefig("pyplot_multiple_y-axis.pdf", bbox_inches='tight')
# bbox_inches='tight': Try to strip excess whitespace
# https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html
Suuuehgi
  • 4,547
  • 3
  • 27
  • 32
  • 12
    +1 for a version that enables use of the standard matplotlib module. I'd also steer current users towards using the modern, more pythonic `subplots()` method as highlighted [here](https://matplotlib.org/users/recipes.html) and as jarondl urges as well [here](https://stackoverflow.com/questions/3584805/in-matplotlib-what-does-the-argument-mean-in-fig-add-subplot111#comment18305007_3584933). Fortunately, it works with this answer. You just need to replace the two lines after the import with `fig, host = plt.subplots(nrows=1, ncols=1)`. – Wayne Nov 29 '17 at 17:33
  • 6
    I also note that this answer still allows application of [Rutger Kassies solution](https://stackoverflow.com/a/20147135/8508004) to move the secondary axis (a.k.a. parasite axis) to the left side. In this code, to do that you'd replace `par2.spines['right'].set_position(('outward', 60))` with the following **four** lines: `par2.spines['left'].set_position(('outward', 60))` `par2.spines["left"].set_visible(True)` `par2.yaxis.set_label_position('left')` `par2.yaxis.set_ticks_position('left')` – Wayne Nov 29 '17 at 17:35
  • 1
    This is according to the example [shown here on the matplotlib page](https://matplotlib.org/gallery/ticks_and_spines/multiple_yaxis_with_spines.html), which is indeed much easier to use than the `host_subplots`. – ImportanceOfBeingErnest Mar 28 '18 at 11:42
  • 2
    @Wayne Thank you for the hints! I incorporated them above. – Suuuehgi Jan 21 '21 at 11:26
  • 1
    The two lines doing most of the magic are, first: `par2 = host.twinx()`, second: `par2.spines['right'].set_position(('outward', 60))` – Stefan Jan 27 '21 at 11:03
125

If I understand the question, you may interested in this example in the Matplotlib gallery.

enter image description here

Yann's comment above provides a similar example.


Edit - Link above fixed. Corresponding code copied from the Matplotlib gallery:

from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as AA
import matplotlib.pyplot as plt

host = host_subplot(111, axes_class=AA.Axes)
plt.subplots_adjust(right=0.75)

par1 = host.twinx()
par2 = host.twinx()

offset = 60
new_fixed_axis = par2.get_grid_helper().new_fixed_axis
par2.axis["right"] = new_fixed_axis(loc="right", axes=par2,
                                        offset=(offset, 0))

par2.axis["right"].toggle(all=True)

host.set_xlim(0, 2)
host.set_ylim(0, 2)

host.set_xlabel("Distance")
host.set_ylabel("Density")
par1.set_ylabel("Temperature")
par2.set_ylabel("Velocity")

p1, = host.plot([0, 1, 2], [0, 1, 2], label="Density")
p2, = par1.plot([0, 1, 2], [0, 3, 2], label="Temperature")
p3, = par2.plot([0, 1, 2], [50, 30, 15], label="Velocity")

par1.set_ylim(0, 4)
par2.set_ylim(1, 65)

host.legend()

host.axis["left"].label.set_color(p1.get_color())
par1.axis["right"].label.set_color(p2.get_color())
par2.axis["right"].label.set_color(p3.get_color())

plt.draw()
plt.show()

#plt.savefig("Test")
Steve Tjoa
  • 59,122
  • 18
  • 90
  • 101
  • 8
    -1 because answers hidden behind links are less helpful and tend to rot. – dlras2 Oct 23 '12 at 14:28
  • Nice one, but this host plot tested on several plots seems utterly slow compared to the implementation of Yann. Furthermore, it seems to me that set_title in this case is buggy, so that if I plot many charts, the titles are all overlapped to each other. The only advantage of this implementation seems to be that it better supports the legend command. – Antonio Oct 04 '13 at 08:01
  • 1
    @SteveTjoa, is there any way to avoid the empty room by side the produced figure? – Py-ser Jun 05 '14 at 03:36
  • 1
    I could not find get_grid_helper documented anywhere. What exactly does it do? – tommy.carstensen Feb 14 '15 at 22:32
  • There is a bug: when the y axes have different orders of magnitude, the tens multiplier of each axis overlaps each other, i.e. the location of the tens multiplier is not affected by the offset. – Sparkler Jan 17 '16 at 00:28
  • 1
    Why the `if 1:` – Jonathan Wheeler Dec 08 '16 at 17:31
  • 1
    The "Temperature" label on the right axis does not show up? Running MPL version 2.2.2. – user1834164 Jul 24 '18 at 09:21
  • 3
    `par1.axis["right"].toggle(all=True)` is missing! – henry Oct 30 '20 at 15:16
79

if you want to do very quick plots with secondary Y-Axis then there is much easier way using Pandas wrapper function and just 2 lines of code. Just plot your first column then plot the second but with parameter secondary_y=True, like this:

df.A.plot(label="Points", legend=True)
df.B.plot(secondary_y=True, label="Comments", legend=True)

This would look something like below:

enter image description here

You can do few more things as well. Take a look at Pandas plotting doc.

Shital Shah
  • 63,284
  • 17
  • 238
  • 185