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