Introduction
Creating a plot with two y-axes can be a useful way to visualize data that share the same x-axis but have different scales or units. Matplotlib, a powerful plotting library in Python, provides straightforward methods to achieve this. Here’s a step-by-step guide.
Table of contents
Add a Secondary Y-Axis using twinx method
Import Required Libraries
Start by importing Matplotlib and other necessary libraries.
1 2 | import matplotlib.pyplot as plt import numpy as np |
Generate Sample Data
For demonstration, create two datasets that you want to visualize on the same plot.
1 2 3 | x = np.linspace(0, 10, 100) # Common x-axis y1 = np.sin(x) # First dataset y2 = np.exp(x / 3) # Second dataset |
Create the Primary Plot
Use the subplots method to create a figure and axis for the primary y-axis.
1 2 3 4 5 6 7 | fig, ax1 = plt.subplots() # Plot the first dataset ax1.plot(x, y1, 'b-', label='Sine Wave') ax1.set_xlabel('X-axis') ax1.set_ylabel('Sine', color='b') ax1.tick_params(axis='y', labelcolor='b') |
Add a Secondary Y-Axis
Use the twinx method to add a second y-axis that shares the same x-axis.
1 2 3 4 5 6 | ax2 = ax1.twinx() # Create a second axes that shares the same x-axis # Plot the second dataset ax2.plot(x, y2, 'r-', label='Exponential Growth') ax2.set_ylabel('Exponential', color='r') ax2.tick_params(axis='y', labelcolor='r') |
Add a Title and Legend
Customize the plot with titles and legends.
1 2 3 4 5 | fig.suptitle('Plot with Two Y-Axes') # Optional: Add legends ax1.legend(loc='upper left') ax2.legend(loc='upper right') |
Display the Plot
Finally, show the plot using the show method.
1 | plt.show() |
Full Example Code
Here is the complete script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import matplotlib.pyplot as plt import numpy as np # Generate data x = np.linspace(0, 10, 100) y1 = np.sin(x) y2 = np.exp(x / 3) # Create primary y-axis plot fig, ax1 = plt.subplots() ax1.plot(x, y1, 'b-', label='Sine Wave') ax1.set_xlabel('X-axis') ax1.set_ylabel('Sine', color='b') ax1.tick_params(axis='y', labelcolor='b') # Create secondary y-axis ax2 = ax1.twinx() ax2.plot(x, y2, 'r-', label='Exponential Growth') ax2.set_ylabel('Exponential', color='r') ax2.tick_params(axis='y', labelcolor='r') # Add title and legends fig.suptitle('Plot with Two Y-Axes') ax1.legend(loc='upper left') ax2.legend(loc='upper right') # Display the plot plt.show() |
Output
The resulting plot will have:
- A shared x-axis.
- The sine wave (y1) on the left y-axis with blue labels and a blue line.
- The exponential growth (y2) on the right y-axis with red labels and a red line.

Key Notes
- Use different colors for each axis to improve readability.
- Ensure that the datasets have a logical relationship or justification for being visualized together.
- If additional customization is needed, Matplotlib’s extensive documentation can be a helpful resource.
Another example
Code to create a dual y-axis time series plot visualizing atmospheric CO₂ concentrations and global temperature anomalies. The plot combines a line plot for CO₂ and a bar plot for temperature anomalies.
Import Libraries
1 2 3 | import pandas as pd import matplotlib.pyplot as plt import numpy as np |
pandas: Used for loading and processing time series data.matplotlib.pyplot: Used for plotting.numpy: Provides numerical utilities.
Load and Prepare CO₂ Data
1 2 3 4 5 6 7 8 9 10 | # Load CO₂ data co2_data = pd.read_csv( "./inputs/co2_mm_mlo.csv", # Replace with your file path skiprows=40, # Skip header rows specific to the dataset na_values=["-99.99"] # Handle missing values ) co2_data = co2_data.dropna() # Remove rows with missing data # Create a datetime column from year and month co2_data['date'] = pd.to_datetime(co2_data[['year', 'month']].assign(day=1)) |
- Loads monthly CO₂ data from Mauna Loa Observatory.
- Cleans the dataset and combines year and month into a
datetimeobject for plotting.
Load and Prepare Temperature Anomaly Data
1 2 3 4 5 6 7 8 9 10 11 12 | # Load temperature anomaly data temp_data = pd.read_csv( "./inputs/GLB.Ts+dSST.csv", # Replace with your file path skiprows=1 # Skip header row ) # Rename and select relevant columns temp_data = temp_data.rename(columns={"Year": "year", "J-D": "anomaly"}) temp_data = temp_data[['year', 'anomaly']] # Add a datetime column (mid-year for annual data) temp_data['date'] = pd.to_datetime(temp_data['year'].astype(str) + '-07-01') |
- Loads global temperature anomaly data and cleans it by renaming and selecting necessary columns.
- Converts the
yearcolumn into adatetimeobject for alignment with CO₂ data.
Align and Filter Data
1 2 3 4 5 6 7 8 9 | # Filter data to shared date range start_date = max(co2_data['date'].min(), temp_data['date'].min()) end_date = min(co2_data['date'].max(), temp_data['date'].max()) co2_filtered = co2_data[(co2_data['date'] >= start_date) & (co2_data['date'] <= end_date)] temp_filtered = temp_data[(temp_data['date'] >= start_date) & (temp_data['date'] <= end_date)] # Ensure 'anomaly' column is numeric and drop invalid rows temp_filtered['anomaly'] = pd.to_numeric(temp_filtered['anomaly'], errors='coerce') temp_filtered = temp_filtered.dropna(subset=['anomaly']) |
- Aligns CO₂ and temperature data by filtering to the overlapping date range.
- Converts the
anomalycolumn to numeric and removes invalid rows.
Separate Positive and Negative Anomalies
1 2 3 | # Separate data into positive and negative anomalies positive_anomalies = temp_filtered[temp_filtered['anomaly'] > 0] negative_anomalies = temp_filtered[temp_filtered['anomaly'] <= 0] |
- Splits the temperature anomaly data into two subsets for positive (red) and negative (blue) bars.
Create the Plot
1 2 3 4 5 6 7 8 | fig, ax1 = plt.subplots(figsize=(10, 6)) # Plot CO₂ data ax1.plot(co2_filtered['year'], co2_filtered['average'], 'g-', label='CO₂ (ppm)') ax1.set_xlabel('Year') ax1.set_ylabel('CO₂ Concentration (ppm)', color='g') ax1.tick_params(axis='y', colors='g') ax1.legend(loc='upper left') |
- Creates the primary y-axis for CO₂ data with a green line plot.
Add Secondary Y-Axis for Temperature Anomalies
1 2 3 4 5 6 7 8 9 10 11 | # Add a second axis for temperature anomalies ax2 = ax1.twinx() # Plot bars with different colors for anomalies ax2.bar(positive_anomalies['year'], positive_anomalies['anomaly'], color='r', alpha=0.7, label='Positive Anomaly') ax2.bar(negative_anomalies['year'], negative_anomalies['anomaly'], color='b', alpha=0.7, label='Negative Anomaly') # Set secondary y-axis label and formatting ax2.set_ylabel('Temperature Anomaly (°C)', color='k') ax2.tick_params(axis='y', colors='k') ax2.legend(loc='upper right') |
- Adds a secondary y-axis for temperature anomalies.
- Uses a bar plot with red bars for positive anomalies and blue bars for negative anomalies.
Add Title and Display the Plot
1 2 3 4 5 | # Add title and grid plt.title('Atmospheric CO₂ and Global Temperature Anomalies') plt.grid() plt.tight_layout() plt.show() |
- Adds a title, grid, and adjusts layout to prevent overlap.
Complete Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | import pandas as pd import matplotlib.pyplot as plt import numpy as np # Load CO₂ data co2_data = pd.read_csv( "./inputs/co2_mm_mlo.csv", # Replace with your file path skiprows=40, na_values=["-99.99"]) co2_data = co2_data.dropna() co2_data['date'] = pd.to_datetime(co2_data[['year', 'month']].assign(day=1)) # Load temperature anomaly data temp_data = pd.read_csv( "./inputs/GLB.Ts+dSST.csv", # Replace with your file path skiprows=1 ) temp_data = temp_data.rename(columns={"Year": "year", "J-D": "anomaly"}) temp_data = temp_data[['year', 'anomaly']] temp_data['date'] = pd.to_datetime(temp_data['year'].astype(str) + '-07-01') # Mid-year for annual data start_date = max(co2_data['date'].min(), temp_data['date'].min()) end_date = min(co2_data['date'].max(), temp_data['date'].max()) co2_filtered = co2_data[(co2_data['date'] >= start_date) & (co2_data['date'] <= end_date)] temp_filtered = temp_data[(temp_data['date'] >= start_date) & (temp_data['date'] <= end_date)] # Ensure 'anomaly' column is numeric temp_filtered['anomaly'] = pd.to_numeric(temp_filtered['anomaly'], errors='coerce') # Drop rows with NaN values in 'anomaly' after conversion temp_filtered = temp_filtered.dropna(subset=['anomaly']) # Separate data into positive and negative anomalies positive_anomalies = temp_filtered[temp_filtered['anomaly'] > 0] negative_anomalies = temp_filtered[temp_filtered['anomaly'] <= 0] # Plotting fig, ax1 = plt.subplots(figsize=(10, 6)) # Plot CO₂ data ax1.plot(co2_filtered['year'], co2_filtered['average'], 'g-', label='CO₂ (ppm)') ax1.set_xlabel('Year') ax1.set_ylabel('CO₂ Concentration (ppm)', color='g') ax1.tick_params(axis='y', colors='g') ax1.legend(loc='upper left') # Add a second axis for temperature anomalies ax2 = ax1.twinx() # Plot bars with different colors ax2.bar(positive_anomalies['year'], positive_anomalies['anomaly'], color='r', alpha=0.7, label='Positive Anomaly') ax2.bar(negative_anomalies['year'], negative_anomalies['anomaly'], color='b', alpha=0.7, label='Negative Anomaly') # Set secondary y-axis label and formatting ax2.set_ylabel('Temperature Anomaly (°C)', color='k') ax2.tick_params(axis='y', colors='k') ax2.legend(loc='upper right') # Add title and grid plt.title('Atmospheric CO₂ and Global Temperature Anomalies') plt.grid() plt.tight_layout() plt.show() |
Output
- Left Y-Axis: CO₂ concentration (ppm) as a green line plot.
- Right Y-Axis: Temperature anomalies (°C) as red and blue bars.
- The plot provides a clear visualization of the relationship between CO₂ levels and global temperature anomalies over time.

Key Notes
- Replace file paths (
"./inputs/co2_mm_mlo.csv"and"./inputs/GLB.Ts+dSST.csv") with actual file locations on your system. - Ensure data formats match the code expectations, or adjust column names and parsing logic as needed.
References
| Links | Site |
|---|---|
| twinx | matplotlib.org |
| Trends in CO2, CH4, N2O, SF6 | gml.noaa.gov |
| If carbon dioxide hits a new high every year, why isn’t every year hotter than the last? | climate.gov |
| GISS Surface Temperature Analysis (GISTEMP v4) | data.giss.nasa.gov |
