Skip to content

Secondary Axes

Secondary axes display an alternative scale or transformation of the same data, useful for showing different units or transformations.

Mental Model

Secondary axes add a second ruler on the opposite side that shows the same data in different units (e.g., Celsius on the left, Fahrenheit on the right). Unlike twin axes, they do not plot new data -- they just relabel the existing scale through a forward and inverse transform function. Use them when readers need to read values in two unit systems.

secondary_xaxis / secondary_yaxis

Unlike twinx()/twiny() which create independent axes for different data, secondary axes transform the same data to a different scale.

Basic Usage

```python import matplotlib.pyplot as plt import numpy as np

fig, ax = plt.subplots()

x = np.linspace(0, 100, 100) y = np.sin(x * np.pi / 50)

ax.plot(x, y) ax.set_xlabel('Distance (km)')

Secondary axis showing same data in miles

def km_to_miles(x): return x * 0.621371

def miles_to_km(x): return x / 0.621371

secax = ax.secondary_xaxis('top', functions=(km_to_miles, miles_to_km)) secax.set_xlabel('Distance (miles)')

plt.show() ```

Temperature Conversion

```python fig, ax = plt.subplots()

temp_c = np.linspace(-40, 100, 100) y = np.exp(-temp_c / 50)

ax.plot(temp_c, y) ax.set_xlabel('Temperature (°C)') ax.set_ylabel('Value')

Secondary axis in Fahrenheit

def c_to_f(x): return x * 9/5 + 32

def f_to_c(x): return (x - 32) * 5/9

secax = ax.secondary_xaxis('top', functions=(c_to_f, f_to_c)) secax.set_xlabel('Temperature (°F)')

plt.show() ```

Secondary Y-Axis

```python fig, ax = plt.subplots()

x = np.linspace(0, 10, 100) y = x ** 2

ax.plot(x, y) ax.set_ylabel('Energy (Joules)')

Secondary axis showing same energy in calories

def j_to_cal(x): return x / 4.184

def cal_to_j(x): return x * 4.184

secax = ax.secondary_yaxis('right', functions=(j_to_cal, cal_to_j)) secax.set_ylabel('Energy (calories)')

plt.show() ```

Logarithmic Secondary Axis

```python fig, ax = plt.subplots()

x = np.linspace(1, 100, 100) ax.plot(x, x ** 2) ax.set_xlabel('Frequency (Hz)') ax.set_yscale('log')

Secondary axis in decades

def hz_to_decades(x): return np.log10(x)

def decades_to_hz(x): return 10 ** x

secax = ax.secondary_xaxis('top', functions=(hz_to_decades, decades_to_hz)) secax.set_xlabel('Frequency (decades)')

plt.show() ```

Comparison: Secondary vs Twin Axes

Feature Secondary Axes Twin Axes
Data Same data, different units Different data
Scaling Mathematical transformation Independent
Method secondary_xaxis() twinx()
Alignment Automatically aligned Manual alignment
Use case Unit conversion Overlay different variables

When to Use Each

Secondary Axes:

  • Same measurement in different units (km ↔ miles)
  • Same scale with transformation (linear ↔ log)
  • Wavelength and frequency

Twin Axes:

  • Price and volume on same chart
  • Temperature and humidity
  • Different physical quantities

Practical Examples

1. Wavelength and Frequency

```python fig, ax = plt.subplots()

wavelength = np.linspace(400, 700, 100) # nm intensity = np.exp(-((wavelength - 550) ** 2) / 5000)

ax.plot(wavelength, intensity) ax.set_xlabel('Wavelength (nm)') ax.set_ylabel('Intensity')

Frequency = c / wavelength

c = 3e8 # m/s

def wavelength_to_freq(wl): return c / (wl * 1e-9) / 1e12 # THz

def freq_to_wavelength(f): return c / (f * 1e12) / 1e-9 # nm

secax = ax.secondary_xaxis('top', functions=(wavelength_to_freq, freq_to_wavelength)) secax.set_xlabel('Frequency (THz)')

plt.show() ```

2. Pressure Units

```python fig, ax = plt.subplots()

altitude = np.linspace(0, 50000, 100) # meters pressure = 101325 * np.exp(-altitude / 8500) # Pa

ax.plot(altitude / 1000, pressure / 1000) ax.set_xlabel('Altitude (km)') ax.set_ylabel('Pressure (kPa)')

Secondary in atmospheres

def kpa_to_atm(x): return x / 101.325

def atm_to_kpa(x): return x * 101.325

secax = ax.secondary_yaxis('right', functions=(kpa_to_atm, atm_to_kpa)) secax.set_ylabel('Pressure (atm)')

plt.show() ```

3. Date and Day Number

```python import matplotlib.pyplot as plt import matplotlib.dates as mdates import numpy as np from datetime import datetime, timedelta

fig, ax = plt.subplots()

Data for one year

start = datetime(2024, 1, 1) days = np.arange(0, 365) dates = [start + timedelta(days=int(d)) for d in days] values = np.sin(days * 2 * np.pi / 365)

ax.plot(days, values) ax.set_xlabel('Day of Year') ax.set_ylabel('Value')

plt.show() ```

Key Parameters

Parameter Description
location 'top', 'bottom', 'left', or 'right'
functions Tuple of (forward, inverse) functions
transform Alternative to functions

Common Pitfalls

1. Inverse Function Required

```python

Both forward AND inverse functions needed

def forward(x): return x * 2

def inverse(x): return x / 2

CORRECT

secax = ax.secondary_xaxis('top', functions=(forward, inverse))

WRONG: Missing inverse

secax = ax.secondary_xaxis('top', functions=(forward,))

```

2. Non-Monotonic Transforms

Secondary axes work best with monotonic transformations. Non-monotonic transforms may produce unexpected results.

3. Log Scale Interactions

When using log scales, ensure transformations handle the log space correctly.


Exercises

Exercise 1. Plot temperature data in Celsius for 12 months (e.g., [5, 7, 12, 18, 23, 28, 31, 30, 25, 18, 11, 6]) and add a secondary y-axis that shows the values in Fahrenheit using the conversion \(F = C \times 9/5 + 32\). Use secondary_yaxis with forward and inverse functions.

Solution to Exercise 1
import matplotlib.pyplot as plt
import numpy as np

months = np.arange(1, 13)
temps_c = [5, 7, 12, 18, 23, 28, 31, 30, 25, 18, 11, 6]

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(months, temps_c, 'o-', color='tab:blue')
ax.set_xlabel('Month')
ax.set_ylabel('Temperature (°C)', color='tab:blue')
ax.set_xticks(months)

secax = ax.secondary_yaxis('right',
                            functions=(lambda c: c * 9/5 + 32,
                                       lambda f: (f - 32) * 5/9))
secax.set_ylabel('Temperature (°F)', color='tab:red')
ax.set_title('Monthly Temperature with Celsius and Fahrenheit')
plt.show()

Exercise 2. Plot the function \(y = e^x\) for x in \([0, 5]\) with a primary x-axis in linear scale. Add a secondary x-axis on top that shows the natural log of the x values. Use secondary_xaxis with functions=(np.log, np.exp).

Solution to Exercise 2
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0.1, 5, 200)
y = np.exp(x)

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(x, y, color='navy')
ax.set_xlabel('x (linear)')
ax.set_ylabel(r'$e^x$')

secax = ax.secondary_xaxis('top', functions=(np.log, np.exp))
secax.set_xlabel('ln(x)')
ax.set_title(r'$y = e^x$ with Secondary Log Axis')
plt.show()

Exercise 3. Create a plot showing distance in kilometers over time in hours. Add a secondary y-axis that converts kilometers to miles (1 km = 0.621371 miles) and a secondary x-axis that converts hours to minutes. Use sample data time = [0, 1, 2, 3, 4, 5] and distance = [0, 10, 25, 45, 60, 80].

Solution to Exercise 3
import matplotlib.pyplot as plt

time_h = [0, 1, 2, 3, 4, 5]
distance_km = [0, 10, 25, 45, 60, 80]

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(time_h, distance_km, 'o-', color='steelblue', linewidth=2)
ax.set_xlabel('Time (hours)')
ax.set_ylabel('Distance (km)')

secax_y = ax.secondary_yaxis('right',
                              functions=(lambda km: km * 0.621371,
                                         lambda mi: mi / 0.621371))
secax_y.set_ylabel('Distance (miles)')

secax_x = ax.secondary_xaxis('top',
                              functions=(lambda h: h * 60,
                                         lambda m: m / 60))
secax_x.set_xlabel('Time (minutes)')

ax.set_title('Distance over Time with Unit Conversions')
plt.show()

Exercise 4. Create a plot of temperature in Celsius over 24 hours. Add a secondary y-axis showing Fahrenheit using the conversion F = C * 9/5 + 32. Verify that the two axes stay synchronized by checking that 0°C aligns with 32°F.

Solution to Exercise 4
import matplotlib.pyplot as plt
import numpy as np

hours = np.arange(24)
temp_c = 10 + 8 * np.sin((hours - 6) * np.pi / 12)

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(hours, temp_c, 'b-o', markersize=4)
ax.set_xlabel('Hour of Day')
ax.set_ylabel('Temperature (°C)')

secax = ax.secondary_yaxis('right',
                            functions=(lambda c: c * 9/5 + 32,
                                      lambda f: (f - 32) * 5/9))
secax.set_ylabel('Temperature (°F)')
ax.set_title('Daily Temperature with Dual Units')
ax.axhline(0, color='gray', linestyle=':', alpha=0.5, label='0°C = 32°F')
ax.legend()
plt.show()

Exercise 5. Explain the difference between secondary_yaxis and twinx. When should you use each? Give a concrete example where using twinx for a unit conversion would be incorrect.

Solution to Exercise 5

secondary_yaxis shows the same data in different units (e.g., km and miles, °C and °F). The two axes are mathematically linked by a conversion function — every point on one axis maps to exactly one point on the other.

twinx shows different data that share the same x-axis but have unrelated y-scales (e.g., temperature and precipitation).

Incorrect use of twinx for unit conversion:

# WRONG — manually plotting the same data twice
fig, ax1 = plt.subplots()
ax1.plot(time, temp_c, 'b-')
ax1.set_ylabel('°C')
ax2 = ax1.twinx()
ax2.plot(time, temp_c * 9/5 + 32, 'r-')  # Same data, duplicated!
ax2.set_ylabel('°F')

This draws two independent lines that can drift apart if axis limits are changed manually. secondary_yaxis guarantees the conversion is always correct because it is computed from the primary axis — no data duplication, no drift.