Repository: matplotlib/matplotlib
Pull request: DOC: Explain parameters linthresh and linscale of symlog scale
Closes #29335 via addressing https://github.com/matplotlib/matplotlib/issues/29335#issuecomment-2548980673.


Problem statement:
[ENH]: Indicating change of scale for symlog
### Problem

I appreciate that matplotlib provides various scales, including symlog. 
However, I see the problem that it is not quite clear in the axis labels what happens.

### Proposed solution

I propose to (at least optionally) insert automatically a break (like // ) on the axis at the transitions between lin and log scales.

Hints from issue discussion:
Fundamentally, I believe this is a good idea. It's a bit difficult to implement though. Conceptually, we only have the locator, which determines tick positions. We don't have any infrastructure/API to handle the additional markers. Any automatism would need architectural changes.

A simple workaround would be a dedicated `mark_symlog_linear_range()` method that the user must call explicitly. It would add appropriate markers statically, but wouldn't update with a change of scale. - Possibly an example with such a function would be a good first step.


Dear Tim,
many thanks for the prompt reply.
Having such a method would indeed be great!
Kind regards,
Steffen


From: Tim Hoffmann ***@***.***>
Sent: Dienstag, 17. Dezember 2024 12:09
To: matplotlib/matplotlib ***@***.***>
Cc: Beirle, Steffen ***@***.***>; Author ***@***.***>
Subject: Re: [matplotlib/matplotlib] [ENH]: Indicating change of scale for symlog (Issue #29335)


Fundamentally, I believe this is a good idea. It's a bit difficult to implement though. Conceptually, we only have the locator, which determines tick positions. We don't have any infrastructure/API to handle the additional markers. Any automatism would need architectural changes.

A simple workaround would be a dedicated mark_symlog_linear_range() method that the user must call explicitly. It would add appropriate markers statically, but wouldn't update with a change of scale.

—
Reply to this email directly, view it on GitHub<https://github.com/matplotlib/matplotlib/issues/29335#issuecomment-2548169715>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AKY34PZAEUVYO3QM6IQIBX32GAA53AVCNFSM6AAAAABTYCONOOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDKNBYGE3DSNZRGU>.
You are receiving this because you authored the thread.Message ID: ***@***.***>

One quick solution would be something like this:


```python
def mark_symlog_linear_range(axis):
    trafo =  axis.get_transform()
    if not hasattr(trafo, "linthresh"):
        raise ValueError("mark_symlog_linear_range() can only be applied to Axis with symmetrical log scale")

    if axis.axis_name == 'x':
        axis.axes.axvspan(-trafo.linthresh, trafo.linthresh, -0.02, 0.0, clip_on=False, color='lightgray')
    elif axis.axis_name == 'y':
        axis.axes.axhspan(-trafo.linthresh, trafo.linthresh, -0.02, 0.0, clip_on=False, color='lightgray')
    else:
        raise ValueError("mark_symlog_linear_range() only works on x-axis and y-axis")
```

Which then could be used as

```python
import matplotlib.pyplot as plt
import numpy as np

dt = 0.01
x = np.arange(-50.0, 50.0, dt)
y = np.arange(0, 100.0, dt)

fig, ax = plt.subplots()

ax.plot(x, y)
ax.set_xscale('symlog', linthresh=10)
ax.grid()
ax.set_xlim(-20, 20)
mark_symlog_linear_range(ax.xaxis)
```

![image](https://github.com/user-attachments/assets/305c5478-8dca-4864-b405-f5298f6707ac)

Usually people want the linear region to be imperceptible.  If folks want the split in scales to be obvious there are lots of ways to do that which will depend on the reason for making it clear.  Perhaps @timhoffm example above could be added to the examples page, but I don't think we could or should provide a universal method for indicating this.  

Symlog should really be discouraged compared to asinh. 
The linear region is a necessary deviation from the log scaling to be able to map data in a range (-x0, x0) onto a finite region. I agree with @jklymak that the linear range should typically a tiny fraction of the view range. It should be chosen to be slightly smaller than the absolute value of all “interesting data”.
We should better explain why it is necessary and how to use it, rather than adding tooling to visualize it. This explanation should likely go into https://matplotlib.org/devdocs/gallery/scales/symlog_demo.html and be cross-linked from https://matplotlib.org/devdocs/api/scale_api.html#matplotlib.scale.SymmetricalLogScale.
Thanks for your efforts and replies.
I understand your concerns.
Thanks as well for the recommendation of asinh.


From: Tim Hoffmann ***@***.***>
Sent: Dienstag, 17. Dezember 2024 17:39
To: matplotlib/matplotlib ***@***.***>
Cc: Beirle, Steffen ***@***.***>; Author ***@***.***>
Subject: Re: [matplotlib/matplotlib] [ENH]: Indicating change of scale for symlog (Issue #29335)


The linear region is a necessary deviation from the log scaling to be able to map data in a range (-x0, x0) onto a finite region. I agree with @jklymak<https://github.com/jklymak> that the linear range should typically a tiny fraction of the view range. It should be chosen to be slightly smaller than the absolute value of all “interesting data”.
We should better explain why it is necessary and how to use it, rather than adding tooling to visualize it. This explanation should likely go into https://matplotlib.org/devdocs/gallery/scales/symlog_demo.html and be cross-linked from https://matplotlib.org/devdocs/api/scale_api.html#matplotlib.scale.SymmetricalLogScale.

—
Reply to this email directly, view it on GitHub<https://github.com/matplotlib/matplotlib/issues/29335#issuecomment-2548980673>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AKY34P4J4GFTYYYQTCCFRID2GBHRBAVCNFSM6AAAAABTYCONOOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDKNBYHE4DANRXGM>.
You are receiving this because you authored the thread.Message ID: ***@***.***>

Commit message: DOC: Explain parameters linthresh and linscale of symlog scale

Closes #29335 via addressing https://github.com/matplotlib/matplotlib/issues/29335#issuecomment-2548980673.

Patch to review:
diff --git a/galleries/examples/scales/symlog_demo.py b/galleries/examples/scales/symlog_demo.py
index e50be0d0c8a8..ff24860b4210 100644
--- a/galleries/examples/scales/symlog_demo.py
+++ b/galleries/examples/scales/symlog_demo.py
@@ -1,7 +1,12 @@
 """
-===========
-Symlog Demo
-===========
+============
+Symlog scale
+============
+
+The symmetric logarithmic scale is an extension of the logarithmic scale that
+also covers negative values. As with the logarithmic scale, it is particularly
+useful for numerical data that spans a broad range of values, especially when there
+are significant differences between the magnitudes of the numbers involved.
 
 Example use of symlog (symmetric log) axis scaling.
 """
@@ -34,12 +39,85 @@
 plt.show()
 
 # %%
-# It should be noted that the coordinate transform used by ``symlog``
-# has a discontinuous gradient at the transition between its linear
-# and logarithmic regions. The ``asinh`` axis scale is an alternative
-# technique that may avoid visual artifacts caused by these discontinuities.
+# Linear threshold
+# ----------------
+# Since each decade on a logarithmic scale covers the same amount of visual space
+# and there are infinitely many decades between a given number and zero, the symlog
+# scale must deviate from logarithmic mapping in a small range (-x0, x0), so that
+# that range is mapped to a finite visual space.
+#
+# The symlog scale achieves this by defining a parameter *linthresh* and switching
+# to a linear mapping in the region *(-linthresh, linthresh)*.
+
+
+def format_axes(ax, title=None):
+    """A helper function to better visualize properties of the symlog scale."""
+    ax.xaxis.get_minor_locator().set_params(subs=[2, 3, 4, 5, 6, 7, 8, 9])
+    ax.grid()
+    ax.xaxis.grid(which='minor')  # minor grid on too
+    linthresh = ax.xaxis.get_transform().linthresh
+    linscale = ax.xaxis.get_transform().linscale
+    ax.axvspan(-linthresh, linthresh, color='0.9')
+    if title:
+        ax.set_title(title.format(linthresh=linthresh, linscale=linscale))
+
+
+x = np.linspace(-60, 60, 201)
+y = np.linspace(0, 100.0, 201)
+
+fig, (ax1, ax2) = plt.subplots(nrows=2, layout="constrained")
+
+ax1.plot(x, y)
+ax1.set_xscale('symlog', linthresh=1)
+format_axes(ax1, title='Linear region: linthresh={linthresh}')
+
+ax2.plot(x, y)
+ax2.set_xscale('symlog', linthresh=5)
+format_axes(ax2, title='Linear region: linthresh={linthresh}')
 
 # %%
+# Generally, *linthresh* should be chosen so that no or only a few
+# data points are in the linear region. As a rule of thumb,
+# :math:`linthresh \approx \mathrm{min} |x|`.
+#
+#
+# Linear scale
+# ------------
+# Additionally, the *linscale* parameter determines how much visual space should be
+# used for the linear range. More precisely, it defines the ratio of visual space
+# of the region (0, linthresh) relative to one decade.
+
+fig, (ax1, ax2) = plt.subplots(nrows=2, layout="constrained")
+
+ax1.plot(x, y)
+ax1.set_xscale('symlog', linthresh=1)
+format_axes(ax1, title='Linear region: linthresh={linthresh}, linscale={linscale}')
+
+ax2.plot(x, y)
+ax2.set_xscale('symlog', linthresh=1, linscale=0.1)
+format_axes(ax2, title='Linear region: linthresh={linthresh}, linscale={linscale}')
+
+# %%
+# The suitable value for linscale depends on the dynamic range of data. As most data
+# will be outside the linear region, you typically the linear region only to cover
+# a small fraction of the visual area.
+#
+# Limitations and alternatives
+# ----------------------------
+# The coordinate transform used by ``symlog`` has a discontinuous gradient at the
+# transition between its linear and logarithmic regions. Depending on data and
+# scaling, this will be more or less obvious in the plot.
+
+fig, ax = plt.subplots()
+ax.plot(x, y)
+ax.set_xscale('symlog', linscale=0.05)
+format_axes(ax, title="Discontinuous gradient at linear/log transition")
+
+# %%
+# The ``asinh`` axis scale is an alternative transformation that supports a wide
+# dynamic range with a smooth gradient and thus may avoid such visual artifacts.
+# See :doc:`/gallery/scales/asinh_demo`.
+#
 #
 # .. admonition:: References
 #


Code review comments:
diff_hunk: @@ -34,12 +39,85 @@
 plt.show()
 
 # %%
-# It should be noted that the coordinate transform used by ``symlog``
-# has a discontinuous gradient at the transition between its linear
-# and logarithmic regions. The ``asinh`` axis scale is an alternative
-# technique that may avoid visual artifacts caused by these discontinuities.
+# Linear threshold
+# ----------------
+# Since each decade on a logarithmic scale covers the same amount of visual space
+# and there are infinitely many decades between a given number and zero, the symlog
+# scale must deviate from logarithmic mapping in a small range (-x0, x0), so that
+# that range is mapped to a finite visual space.
+#
+# The symlog scale achieves this by defining a parameter *linthresh* and switching
+# to a linear mapping in the region *(-linthresh, linthresh)*.
path: galleries/examples/scales/symlog_demo.py
text: @user1:
x0 isn't defined before this, so perhaps just go right to the *linthresh* name that we use internally instead of adding the next sentence which says basically the same thing.
```suggestion
# scale must deviate from logarithmic mapping in a small range *(-linthresh, linthresh)*, so that
# the range is mapped to a finite visual space.
```