When I build dashboards for financial analysis, I often need to compare two different metrics that don’t share the same scale.
For instance, you might want to visualize the S&P 500 index price alongside the daily trading volume.
If you plot them on the same Y-axis, the volume (in millions) will completely flatten the index price (in thousands).
In this tutorial, I will show you exactly how I handle this using a secondary Y-axis within Matplotlib subplots.
The Need for a Dual Y-Axis
I’ve found that using twinx() is the most efficient way to overlay two different scales on a single plot.
This allows both datasets to share the same X-axis (like time) while maintaining their own independent Y-ranges.
Below, I’ll walk you through the primary methods I use to achieve this, from basic dual-axis plots to complex subplot grids.
Method 1: Use the twinx() Function on a Single Subplot
This is my go-to method for a quick comparison. We create a standard axis and then “twin” it to create a second one.
In this example, let’s look at the average temperature in New York City versus the monthly precipitation.
import matplotlib.pyplot as plt
import numpy as np
# Data: NYC Monthly Weather (Approximate)
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
temp_f = [32, 35, 43, 53, 63, 73, 79, 78, 70, 59, 48, 37]
precip_in = [3.6, 3.2, 4.4, 4.5, 4.5, 4.8, 4.6, 4.4, 4.3, 4.4, 4.0, 4.4]
# Create the figure and the first axis
fig, ax1 = plt.subplots(figsize=(10, 6))
# Plotting the Temperature on the left Y-axis
color_temp = 'tab:red'
ax1.set_xlabel('Month')
ax1.set_ylabel('Average Temp (°F)', color=color_temp, fontsize=12)
ax1.plot(months, temp_f, color=color_temp, marker='o', linewidth=2, label='Temp')
ax1.tick_params(axis='y', labelcolor=color_temp)
ax1.grid(True, alpha=0.3)
# Instantiate a second axes that shares the same x-axis
ax2 = ax1.twinx()
# Plotting Precipitation on the right Y-axis
color_precip = 'tab:blue'
ax2.set_ylabel('Precipitation (inches)', color=color_precip, fontsize=12)
ax2.bar(months, precip_in, color=color_precip, alpha=0.3, label='Precip')
ax2.tick_params(axis='y', labelcolor=color_precip)
# Adding a title and layout adjustments
plt.title('New York City: Temperature vs. Precipitation Analysis', fontsize=14)
fig.tight_layout()
plt.show()I executed the above example code and added the screenshot below.

I use ax1.twinx() to create ax2. This ensures that even though the temperature is in the 70s and the rain is around 4 inches, both are clearly visible.
I always recommend coloring the Y-axis labels to match the plot lines; it makes the chart much more intuitive for the reader.
Method 2: Secondary Y-Axis within a Multi-Subplot Grid
Sometimes one plot isn’t enough. I often have to create a grid of subplots where only one specific pane requires a secondary axis.
Let’s imagine we are analyzing California’s Green Energy production. We want one plot for Solar/Wind and another comparing Total Capacity to Cost.
import matplotlib.pyplot as plt
# Data: California Energy Trends
years = [2018, 2019, 2020, 2021, 2022]
solar_mwh = [27000, 28500, 31000, 35000, 39000]
wind_mwh = [14000, 13800, 13500, 15000, 16500]
avg_cost_index = [100, 95, 88, 82, 75] # Dropping cost of tech
# Create a figure with two subplots (1 row, 2 columns)
fig, (plt1, plt2) = plt.subplots(1, 2, figsize=(15, 6))
# Subplot 1: Stacked Solar and Wind
plt1.stackplot(years, solar_mwh, wind_mwh, labels=['Solar', 'Wind'], colors=['orange', 'skyblue'])
plt1.set_title('Renewable Energy Generation (CA)')
plt1.set_ylabel('MWh')
plt1.legend(loc='upper left')
# Subplot 2: Comparing Solar to Cost Index (Using twinx here)
plt2.plot(years, solar_mwh, color='orange', marker='s', label='Solar Production')
plt2.set_ylabel('Solar MWh', color='orange')
plt2.tick_params(axis='y', labelcolor='orange')
# Create the twin axis for the second subplot only
plt2_twin = plt2.twinx()
plt2_twin.plot(years, avg_cost_index, color='green', linestyle='--', marker='d', label='Cost Index')
plt2_twin.set_ylabel('Cost Index (Normalized)', color='green')
plt2_twin.tick_params(axis='y', labelcolor='green')
plt2.set_title('Solar Output vs. Infrastructure Cost')
fig.tight_layout(pad=3.0)
plt.show()I executed the above example code and added the screenshot below.

One thing I struggled with early on was the legend. Because plt2 and plt2_twin are technically different axes, calling .legend() on one won’t show the lines from the other.
To fix this, I manually combine the “handles” and “labels” from both axes into a single legend box.
Method 3: Use the secondary_yaxis Function (Modern Matplotlib)
In newer versions of Matplotlib, there is a secondary_yaxis function. I use this specifically when the second axis is a direct mathematical conversion of the first.
For example, if your primary Y-axis is in Meters, but your US-based audience needs to see Feet.
import matplotlib.pyplot as plt
import numpy as np
# Sample Data: Elevation of a hiking trail in the Rocky Mountains
miles = np.linspace(0, 10, 100)
elevation_meters = 2000 + 500 * np.sin(miles / 2) + 10 * miles
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(miles, elevation_meters, color='brown')
ax.set_xlabel('Distance (Miles)')
ax.set_ylabel('Elevation (Meters)')
ax.set_title('Trail Elevation: Rocky Mountain National Park')
# Define the conversion functions
def m_to_ft(x):
return x * 3.28084
def ft_to_m(x):
return x / 3.28084
# Add the secondary Y-axis on the right
secax = ax.secondary_yaxis('right', functions=(m_to_ft, ft_to_m))
secax.set_ylabel('Elevation (Feet)')
plt.show()I executed the above example code and added the screenshot below.

I use this method whenever the two scales are dependent on each other. It’s much cleaner than manually calculating tick marks for a twinx() axis.
If the data is unrelated (like temperature vs. price), stick to twinx().
Common Issues to Avoid
I’ve spent many hours debugging overlapping labels. Always use plt.tight_layout() or fig.subplots_adjust().
When you add a secondary axis, the right-side labels can often get cut off the edge of the saved image.
Another tip: disable the grid on the secondary axis. If both axes have grids, the chart becomes a messy web of lines that are impossible to read.
I usually keep the grid on for the primary axis (ax1) and set ax2.grid(False).
In this tutorial, we looked at several ways to implement a secondary Y-axis in your Matplotlib subplots.
Whether you are overlaying different units or comparing completely different datasets like energy and cost, these methods provide the flexibility you need.
I hope you found this guide helpful. If you’re working on complex data visualizations, try incorporating these dual-axis techniques to make your charts more insightful.
You may read:
- Set Axis Limits for All Subplots in Matplotlib
- Set Axis Range in Matplotlib imshow
- Set the Secondary Axis Range in Matplotlib
- How to Set Axis Lower Limit in Matplotlib

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.