2025, Nov 23 07:00

Fixing Matplotlib Log Y-Axis: Prevent set_yticks from Resetting Y-Limits by Applying Limits Last

Learn why Matplotlib set_yticks on a log y-axis resets y-limits, and how to fix it: set scale, set ticks, then set limits, or cache and restore ylim now.

When you work with Matplotlib on a logarithmic y-axis, a seemingly harmless call to set specific tick locations can unexpectedly alter your y-limits. This often shows up when you try to reuse ticks between axes or standardize tick placement across plots. The core of the issue is the order in which axis properties are applied: set_yticks may recompute limits, while set_ylim does not recompute ticks.

Minimal example that exposes the issue

The following snippet draws a simple bar chart on a log-scaled y-axis. Even if you don’t actually change the ticks, merely touching them can trigger a change in both yticks and ylim.

import pandas as pd
import matplotlib.pyplot as plt

plt.close('all')
chart_axis = pd.Series([600, 1e3, 1e4], index=['A', 'B', 'C']).plot.bar()
plt.yscale('log')

# This is enough to make ylim and major yticks change
if False:
    chart_axis.set_yticks(chart_axis.get_yticks(minor=False))

This behavior was observed with Spyder 6.0.5 (conda), Python 3.9.18 64-bit, Qt 5.15.2, PyQt5 5.15.10 on Windows 10, but the root cause is not environment-specific. It’s how Matplotlib applies axis settings in this situation.

What’s really happening

The order in which you set axis properties matters. Calling set_ylim does not reset tick locations. In contrast, calling set_yticks resets the y-limits. On a log axis this is especially visible because the tick locator and scaler cooperate to pick "nice" powers of ten, and when ticks are applied, the axis may recompute its limits accordingly.

That is why a pattern like "get ticks, set ticks" can produce a limit jump, even if you feed back the very same values. On the other hand, setting limits after ticks preserves the limits you want.

Reliable fix: apply limits last

If you need explicit ticks and explicit limits on a log y-axis, set the scale, then set the ticks, and finally set the limits. The sequence below shows the contrast between two orders of operations.

import matplotlib.pyplot as plt

fig, (left_ax, right_ax) = plt.subplots(1, 2)

# First subplot: ticks, then limits (limits are preserved)
left_ax.plot()
left_ax.set_yscale('log')
left_ax.set_xticks((-1, 0, 1))
left_ax.set_yticks([1.0e01, 1.0e02, 1.0e03, 1.0e04, 1.0e05, 1.0e06])
left_ax.set_ylim((100, 12000))
print(left_ax.get_yticks())  # [1.e+01 1.e+02 1.e+03 1.e+04 1.e+05 1.e+06]

# Second subplot: limits, then ticks (limits are reset by set_yticks)
right_ax.plot()
right_ax.set_yscale('log')
right_ax.set_xticks((-1, 0, 1))
right_ax.set_ylim((100, 12000))
right_ax.set_yticks([1.0e01, 1.0e02, 1.0e03, 1.0e04, 1.0e05, 1.0e06])
print(right_ax.get_ylim())  # (np.float64(10.0), np.float64(1000000.0))

plt.show()

If you already have code that must call set_yticks and you want to preserve existing limits, a practical technique is to cache the current limits via ylim = plt.ylim() before set_yticks and then restore them with plt.ylim(ylim). This works because the limit change stems from set_yticks itself; restoring the limits afterward enforces the intended range.

About sharey and why it doesn’t solve this case

Sometimes the suggestion arises to use plt.subplots(..., sharey=True). That option is only appropriate when multiple subplots in the same figure truly share the same y-scale. It doesn’t address the fundamental behavior of set_yticks recomputing limits, and it becomes counterproductive if the axes are conceptually different. Consider a side-by-side boxplot experiment where one axis is log-scaled data and the other is the log10-transformed values, which uses different underlying distributions for whisker computation.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import weibull_min

# Create 3 bins of linear data, 1000 points each
bin_labels = ['Alpha', 'Brave', 'Charlie']
scale_factors = [1, 10, 100]
sample_size = 1000
shape_param = 0.5

frame = pd.concat([
    pd.DataFrame({
        'Bin': [bin_labels[i]] * sample_size,
        'Linear': weibull_min(c=shape_param, scale=scale_factors[i]).rvs(size=sample_size)
    })
    for i in range(3)
])

# Linear box plot, then apply logarithmic y-scaling
plt.close('all')
fig, (axis_a, axis_b) = plt.subplots(1, 2, layout='constrained', sharey=True)
frame.boxplot('Linear', by='Bin', ax=axis_a)
plt.yscale('log')

# Box plot of the log10-transformed data
frame['Log10'] = np.log10(frame.Linear)
frame.boxplot('Log10', by='Bin', ax=axis_b)

Here the second boxplot’s boxes and whiskers are computed on the transformed data, which doesn’t map correctly onto the untransformed scale. In scenarios like this you don’t want sharey=True at all. In a more realistic workflow you might plot linear data, log-scale it only to harvest yticks, then clear and replot using log-transformed data to get proper whiskers. That sequence doesn’t involve multiple shared subplots, so sharey is not applicable.

Why this nuance matters

Programmatically standardizing axes is common in data pipelines and dashboards. Copying yticks from one axes object to another, or locking down tick locations for readability, should not silently rescale your plot. Understanding that set_yticks triggers a limit recomputation on a log axis helps you avoid hard-to-spot inconsistencies, especially when composing multi-axes layouts or post-processing plots created by higher-level wrappers.

Takeaways

On log-scaled y-axes, the order of operations is crucial. If you need explicit ticks and explicit limits, set the scale first, then the ticks, and finally the limits. If you must call set_yticks after limits are in place, cache and restore ylim around that call. Avoid using sharey unless both subplots truly represent the same scale and semantics, as it won’t fix the limit-reset behavior and can make mismatched axes misleading.