Skip to content

[Bug]: linscale parameter to SymmetricalLogScale behaves incorrectly #26280

Open

Description

@simon-dima

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

image

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

  1. schtandard commented on Aug 30, 2026

    @schtandard

    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() or expected_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, that expected_transform_b is 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).

  2. added a commit that references this issue on Sep 12, 2026
    15ca9ae
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions