Repository navigation
Description
Bug summary
The linscale parameter to matplotlib.scale.SymmetricalLogScale does not behave as described in its documentation.
Code for reproduction
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
base = 10
linthresh = 2
linscale = 5
symlog = mpl.scale.SymmetricalLogTransform(
base=base,
linthresh=linthresh,
linscale=linscale,
).transform_non_affine
def expected_transform_a(a):
abs_a = np.abs(a)
with np.errstate(divide="ignore", invalid="ignore"):
out = np.sign(a) * (linscale + np.log(abs_a / linthresh) / np.log(base))
inside = abs_a <= linthresh
out[inside] = a[inside] / linthresh * linscale
return out
def expected_transform_b(a):
abs_a = np.abs(a)
with np.errstate(divide="ignore", invalid="ignore"):
out = (
np.sign(a)
* linthresh
* (linscale + np.log(abs_a / linthresh) / np.log(base))
)
inside = abs_a <= linthresh
out[inside] = a[inside] * linscale
return out
xvals = np.linspace(0, 30, 150)
fig, (ax0, ax1, ax2) = plt.subplots(3, 1)
fig.tight_layout()
for ax, transform in [
(ax0, symlog),
(ax1, expected_transform_a),
(ax2, expected_transform_b),
]:
ax.set_title(transform.__name__)
ax.plot(xvals, transform(xvals))
checkpoints = np.array([linthresh * base**i for i in range(2)])
checky = transform(checkpoints)
ax.scatter(checkpoints, checky)
for x, y in zip(checkpoints, checky):
ax.annotate(
f"({x}, {y:.2f})",
(x, y),
xytext=(10, -10),
textcoords="offset points",
)
plt.savefig("symlog_example.png")
plt.show()Actual outcome
Expected outcome
The value of symlog(linthresh) should be equal to linscale*(symlog(base*linthresh) - symlog(linthresh)) , as is the case in the lower two subplots.
Additional information
The documentation states
linscale: float, optional
This allows the linear range
(-linthresh, linthresh)to be stretched relative to the logarithmic range. Its value is the number of decades to use for each half of the linear range. For example, when linscale == 1.0 (the default), the space used for the positive and negative halves of the linear range will be equal to one decade in the logarithmic range.
The behavior is incorrect for any value of base, linthresh and linscale, including the default values base=10, linthresh=2, linscale=1.
The bug is in the definition of the transform_non_affine method of matplotlib.scale.SymmetricalLogTransform: https://github.com/matplotlib/matplotlib/blob/v3.7.2/lib/matplotlib/scale.py#L363
Replacing that definition with either expected_transform_a or expected_transform_b fixes the bug.
expected_transform_a is preferable, as it makes the parameter base have the mathematically correct meaning: whenever the argument x is multiplied by a factor of base, expected_transform_a(x) increases by 1.
The variant expected_transform_b is closer to the current, in my opinion unintuitive, behavior of SymmetricalLogTransform, in that expected_transform_b(x) increases by linthresh when x is multiplied by base.
Operating system
Arch
Matplotlib Version
3.7.2
Matplotlib Backend
TkAgg
Python version
3.11.3
Jupyter version
No response
Installation
pip

Activity
schtandard commentedon Aug 30, 2026
I stumbled upon this bug while finishing up #27310 and I'd like to push a PR to fix it. Before I commit:
Do we want
expected_transform_a()orexpected_transform_b()? My assumption was that the current behavior is scaled the way it is to avoid division (as multiplication is more efficient). I agree with OP, though, thatexpected_transform_bis more intuitive (and this would match the new_SymmetricalLogMixin._val2axpos()).For clarity: This would be a breaking change in the sense that it changes the visual output (as the current output does not match the documented behavior). For base 10 plots, the difference is small, though (the linear range will be 10 % smaller than before), so I doubt anyone will mind (except for automatic tests that will of course fail with changed output, though that is already the case with #27310, so there's no harm in adding this change here).
Make linscale behave as documented