How to Add a Secondary Y-Axis in Matplotlib (Two Y-Axes)

To add a secondary y-axis in Matplotlib, call ax2 = ax.twinx(): it creates a second axes that shares the x-axis and puts its y-axis on the right, so two series with different scales can share one plot. If the right axis should show the same data in another unit (°C and °F, km and miles), use ax.secondary_yaxis() instead. This guide covers two y-axes, a combined legend, bar-and-line charts, three y-axes, pandas and seaborn, and secondary axes in subplots.

Tested with Python 3.12.5, Matplotlib 3.11.2, pandas 3.0.6 and seaborn 0.13.2; the plots are real Matplotlib windows. Reference: Axes.twinx and Secondary axis example.

Two y-axes with twinx()

import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
revenue = [120, 132, 150, 161, 175, 190, 210, 205, 188, 170, 160, 230]      # thousand USD
customers = [4.2, 4.0, 3.7, 3.5, 3.6, 3.4, 3.9, 4.3, 4.6, 4.8, 5.1, 5.6]    # thousand people

fig, ax1 = plt.subplots(figsize=(8, 4.5))
ax1.plot(months, revenue, color="tab:blue", marker="o", label="Revenue")
ax1.set_ylabel("Revenue (thousand USD)", color="tab:blue")
ax1.tick_params(axis="y", labelcolor="tab:blue")

ax2 = ax1.twinx()                                   # second y-axis on the right, same x-axis
ax2.plot(months, customers, color="tab:red", marker="s", label="Customers")
ax2.set_ylabel("Customers (thousands)", color="tab:red")
ax2.tick_params(axis="y", labelcolor="tab:red")

lines = ax1.get_lines() + ax2.get_lines()           # one legend for both axes
ax1.legend(lines, [l.get_label() for l in lines], loc="upper left")
plt.tight_layout()
plt.show()
Matplotlib chart with monthly revenue on a blue left y-axis and customers on a red right y-axis created with twinx and a combined legend
Two scales in one chart; colouring each axis like its line tells readers which is which.

twinx() returns a completely new axes object placed on top of the first one. Everything you do to the right axis goes through ax2:

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

fig, ax1 = plt.subplots()
ax1.plot([1, 2, 3], [100, 200, 300])
ax2 = ax1.twinx()
ax2.plot([1, 2, 3], [0.1, 0.5, 0.2])

print("axes in the figure:", len(fig.axes))
print("ax1 y limits:", [round(float(v), 2) for v in ax1.get_ylim()])
print("ax2 y limits:", [round(float(v), 2) for v in ax2.get_ylim()])
print("shared x limits:", ax1.get_xlim() == ax2.get_xlim())
print("ax2 y-axis side:", ax2.yaxis.get_label_position())

Output:

axes in the figure: 2
ax1 y limits: [90.0, 310.0]
ax2 y limits: [0.08, 0.52]
shared x limits: True
ax2 y-axis side: right
Command Prompt output showing that twinx adds a second axes to the figure with its own y limits, shared x limits and the y-axis on the right
Two axes, independent y limits, shared x limits.

Because each axes has its own legend, collect the lines from both (as above) or use fig.legend() to get one combined legend.

secondary_yaxis(): the same data in another unit

twinx() is for a second data series. When the right axis is only a different unit for the same values, secondary_yaxis() with a pair of conversion functions keeps both axes in sync automatically, even when you zoom:

import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
temp_c = [-2, 1, 6, 12, 18, 23]

fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(months, temp_c, marker="o")
ax.set_ylabel("°C")

# a secondary axis that shows the SAME data in another unit
secax = ax.secondary_yaxis("right", functions=(lambda c: c * 9 / 5 + 32, lambda f: (f - 32) * 5 / 9))
secax.set_ylabel("°F")
ax.set_title("secondary_yaxis: Celsius and Fahrenheit")
plt.tight_layout()
plt.show()
Matplotlib line chart of monthly temperature with Celsius on the left axis and a Fahrenheit secondary_yaxis on the right
secondary_yaxis("right", functions=(to_f, to_c)).

Bar chart and line chart with two y-axes

import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
rainfall = [78, 60, 70, 65, 80, 95, 110, 105, 90, 85, 88, 82]        # mm
temperature = [1, 2, 7, 13, 18, 23, 26, 25, 21, 14, 8, 3]             # °C

fig, ax1 = plt.subplots(figsize=(8, 4.5))
ax1.bar(months, rainfall, color="lightsteelblue", label="Rainfall (mm)")
ax1.set_ylabel("Rainfall (mm)")

ax2 = ax1.twinx()
ax2.plot(months, temperature, color="tab:orange", marker="o", linewidth=2, label="Temperature (°C)")
ax2.set_ylabel("Temperature (°C)")
fig.legend(loc="upper left", bbox_to_anchor=(0.1, 0.9))
plt.tight_layout()
plt.show()
Matplotlib combo chart with monthly rainfall bars on the left y-axis and a temperature line on the right y-axis
Bars on the left axis, a line on the right axis.

Three or more y-axes

Create another twinx() and move its spine outward so the axes do not overlap. Leave space on the right with subplots_adjust(right=...):

import matplotlib.pyplot as plt
import numpy as np

hours = np.arange(0, 24)
temp = 15 + 8 * np.sin((hours - 8) / 24 * 2 * np.pi)
humidity = 70 - 20 * np.sin((hours - 8) / 24 * 2 * np.pi)
wind = 5 + 3 * np.cos(hours / 24 * 2 * np.pi)

fig, ax1 = plt.subplots(figsize=(9, 4.5))
fig.subplots_adjust(right=0.78)

ax2 = ax1.twinx()
ax3 = ax1.twinx()
ax3.spines["right"].set_position(("axes", 1.15))     # move the third axis further right

p1, = ax1.plot(hours, temp, color="tab:red", label="Temperature (°C)")
p2, = ax2.plot(hours, humidity, color="tab:blue", label="Humidity (%)")
p3, = ax3.plot(hours, wind, color="tab:green", label="Wind (m/s)")
for ax, p in [(ax1, p1), (ax2, p2), (ax3, p3)]:
    ax.set_ylabel(p.get_label(), color=p.get_color())
    ax.tick_params(axis="y", colors=p.get_color())
ax1.set_xlabel("Hour of day")
plt.show()
Matplotlib plot with three y-axes for temperature, humidity and wind, the third axis offset to the right
A third y-axis offset with spines["right"].set_position(("axes", 1.15)).

Secondary y-axis with pandas

DataFrame.plot(secondary_y=...) puts the listed columns on a right-hand axis, available afterwards as ax.right_ax:

import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({"visits": [1200, 1350, 1600, 1580, 1900, 2250],
                   "conversion_rate": [2.1, 2.4, 2.2, 2.9, 3.1, 3.4]},
                  index=["Jan", "Feb", "Mar", "Apr", "May", "Jun"])

ax = df.plot(secondary_y="conversion_rate", marker="o", figsize=(7, 4))   # right axis for this column
ax.set_ylabel("Visits")
ax.right_ax.set_ylabel("Conversion rate (%)")
plt.tight_layout()
plt.show()
pandas DataFrame line plot with visits on the left y-axis and conversion rate on a secondary y-axis created with secondary_y
df.plot(secondary_y="conversion_rate").

Secondary y-axis in subplots

Call twinx() on each subplot that needs a second axis:

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(1, 13)
stores = {"Store A": (x * 10 + 50, 34 + 4 * np.sin(x / 2)), "Store B": (x * 7 + 80, 45 - x)}

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
for ax, (name, (sales, staff)) in zip(axes, stores.items()):
    ax.plot(x, sales, color="tab:blue")
    ax.set_ylabel("Sales", color="tab:blue")
    right = ax.twinx()                           # each subplot gets its own secondary axis
    right.plot(x, staff, color="tab:green", linestyle="--")
    right.set_ylabel("Staff", color="tab:green")
    ax.set_title(name)
    ax.set_xlabel("Month")
plt.tight_layout()
plt.show()
Two Matplotlib subplots, each with sales on the left y-axis and staff on its own secondary y-axis
Each subplot has its own right-hand axis.

Secondary y-axis with seaborn

Seaborn functions accept ax=, so draw one plot on ax1 and the other on ax1.twinx(). With a bar plot on the x-axis, seaborn uses positions 0, 1, 2 …, so the line uses range(len(df)) as x:

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

df = pd.DataFrame({"day": range(1, 11),
                   "orders": [52, 58, 61, 55, 70, 74, 69, 80, 85, 83],
                   "avg_basket": [31.5, 30.2, 32.8, 33.1, 29.9, 31.0, 34.2, 35.0, 33.7, 36.1]})

fig, ax1 = plt.subplots(figsize=(7, 4))
sns.barplot(data=df, x="day", y="orders", color="lightgray", ax=ax1)
ax2 = ax1.twinx()                                            # seaborn draws on any Matplotlib axes
sns.lineplot(x=range(len(df)), y=df["avg_basket"], color="tab:purple", marker="o", ax=ax2)
ax2.set_ylabel("Average basket (USD)")
plt.tight_layout()
plt.show()
seaborn bar plot of daily orders with a purple line of average basket value on a secondary y-axis
seaborn barplot and lineplot on two y-axes.

Tips for charts with two y-axes

  • Colour each axis label and tick labels like its data so readers do not mix them up.
  • Two axes can suggest a relationship that is not there, because the scales are arbitrary; consider two stacked subplots with sharex=True as an alternative.
  • Set sensible limits for both axes (ax1.set_ylim(), ax2.set_ylim()), for example starting both at zero.
  • For a second x-axis on top, use twiny() or secondary_xaxis("top").

More Matplotlib axis tutorials:

Frequently asked questions

How do I add a secondary y-axis in Matplotlib?

Call ax2 = ax.twinx() and plot the second series on ax2. Its y-axis appears on the right and it shares the x-axis with ax.

What is the difference between twinx and secondary_yaxis?

twinx() creates a new axes for a different data series. secondary_yaxis() shows the same data in another unit using conversion functions.

How do I make one legend for both y-axes?

Combine the lines: lines = ax1.get_lines() + ax2.get_lines() and call ax1.legend(lines, [l.get_label() for l in lines]), or use fig.legend().

How do I add a third y-axis?

Create another ax3 = ax1.twinx() and move its spine: ax3.spines["right"].set_position(("axes", 1.15)).

How do I plot a secondary y-axis with pandas?

df.plot(secondary_y="column") puts that column on a right-hand axis; label it through ax.right_ax.

How do I set limits on the secondary y-axis?

Call ax2.set_ylim(bottom, top) on the twin axes; each y-axis has its own limits.