Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8217fe0
Do not discard sign of tick value
schtandard Mar 15, 2026
27f3482
Improve some log ticker details
schtandard Mar 16, 2026
6a28eff
Improve symlog ticker
schtandard Mar 16, 2026
907a5eb
Update symlog tests
schtandard Mar 16, 2026
2df2b64
Rename new rcParam
schtandard Mar 16, 2026
97063d5
Streamline subtick determination
schtandard Mar 16, 2026
e9be32d
Fix missing stub parameter
schtandard Mar 16, 2026
42772e5
Remove legacy behavior
schtandard Jun 24, 2026
12a2b26
Add new parameters backward-compatibly
schtandard Jun 24, 2026
aa4e895
Note API changes
schtandard Jun 24, 2026
d959af3
Fix rst syntax
schtandard Jun 25, 2026
b39a71c
Rename _symlog to _is_symlog
schtandard Jun 25, 2026
67c5ef7
Instantiate _symlogutil in init()
schtandard Jun 25, 2026
5bb8291
Relay changed base value to _symlogutil
schtandard Jun 25, 2026
1b7b6ee
Add versioning directives
schtandard Jun 25, 2026
1a8bd23
Create _symlogutil before using set_base()
schtandard Jun 25, 2026
605765b
Delay _symlogutil instantiation
schtandard Jun 25, 2026
6fdc786
Use minor ticks in symlog test
schtandard Jun 25, 2026
f51ea20
Update test output
schtandard Jun 25, 2026
f4f2108
Simplify _SymmetricalLogUtil.init()
schtandard Jun 26, 2026
ccaf6ed
Make _symlogutil instantiation more explicit
schtandard Jun 26, 2026
ebb6a30
Simplify calculation syntax
schtandard Jun 26, 2026
7cb1be8
Remove docstring remnants
schtandard Jun 26, 2026
b2b2dd9
Explain normalized position and decade number
schtandard Jun 26, 2026
ce3151f
Streamline firstdec usage
schtandard Jun 26, 2026
18896bd
Rename variables consistently
schtandard Jun 27, 2026
a8957ee
Update stub patterns
schtandard Jun 27, 2026
8353830
Remove spurious stub default values
schtandard Jun 27, 2026
2a13651
Choose clearer method names
schtandard Jun 30, 2026
35621af
Motivate threshold for acceptable tick positions
schtandard Jun 30, 2026
420e057
Describe function of helper coordinates first
schtandard Jun 30, 2026
03fcaa2
Include ASCII drawing relating coordinate systems
schtandard Jul 1, 2026
5615500
Instantiate symlog formatter with all parameters
schtandard Jul 1, 2026
89985c3
Convert _SymmetricalLogUtil to a mixin
schtandard Jul 1, 2026
1b38253
Make mixin methods private
schtandard Jul 1, 2026
9962282
Document where mixin is used
schtandard Jul 2, 2026
b4547c8
Avoid spurious check
schtandard Jul 2, 2026
9ad864b
Use individual parameters rather than transform
schtandard Jul 2, 2026
c2f1de2
Simplify _is_symlog test
schtandard Jul 3, 2026
69a28b8
Fix changed attribute name
schtandard Jul 3, 2026
f1572f8
Register missing _SymmetricalLogMixin warning
schtandard Jul 3, 2026
ec32280
Add tests for the mixin methods
schtandard Jul 5, 2026
0e89165
Fix rst markup and typos
schtandard Jul 10, 2026
97488ea
Improve docstring phrasing
schtandard Jul 10, 2026
d243c3f
Document all possible values of subs
schtandard Jul 10, 2026
6459a7e
Use mpl20 style in modified test
schtandard Jul 10, 2026
ac60281
Optimize modified png images using oxipng
schtandard Jul 10, 2026
b7217a3
Clarify release note
schtandard Jul 10, 2026
81749a8
Add example plot
schtandard Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/api/next_api_changes/behavior/27310-BW.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Improved ``symlog`` tick placement
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The tick placement for ``symlog`` axes has been improved. It now also supports
minor ticks and `.SymmetricalLogScale` uses ``subs='auto'`` (minor ticks if
appropriate) by default rather than ``subs=None`` (no minor ticks).
4 changes: 4 additions & 0 deletions doc/missing-references.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
"lib/matplotlib/offsetbox.py:docstring of matplotlib.offsetbox.AnnotationBbox:1",
"lib/matplotlib/text.py:docstring of matplotlib.text.Annotation:1"
],
"matplotlib.ticker._SymmetricalLogMixin": [
"lib/matplotlib/ticker.py:docstring of matplotlib.ticker.LogFormatter:1",
"lib/matplotlib/ticker.py:docstring of matplotlib.ticker.SymmetricalLogLocator:1"
],
"matplotlib.transforms._BlendedMixin": [
"lib/matplotlib/transforms.py:docstring of matplotlib.transforms.BlendedAffine2D:1",
"lib/matplotlib/transforms.py:docstring of matplotlib.transforms.BlendedGenericTransform:1"
Expand Down
21 changes: 21 additions & 0 deletions doc/release/next_whats_new/improved_symlog_ticks.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Improved tick placement for ``symlog`` axes
-------------------------------------------
Comment on lines +1 to +2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a sample plot for the what's new note?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I added one. Have a look if you think it does a good job.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the text says ticks will be placed identically to log axes, can you rearrange it a bit to show that? Currently, the limits are so different that that isn't so clear.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I understand you correctly. Do you mean adding log plots alongside the symlog plots showing the logarithmic part of the respective axis range (i.e. everything above 2)? Or do you mean add a symlog axis containing only the logarithmic range and put a log axis with the same range next to it? Or something else?


The placement of ticks for ``symlog`` axes has been improved. Ticks are now
placed identically to ``log`` axes in the logarithmic part with a reasonable
extension of this behavior to the linear part of the axis. Axes with too few
ticks or spurious ticks are avoided by the new implementation.

.. plot::
:include-source: true

fig, axs = plt.subplots(1, 2)
x = np.arange(201)

for ax, (ymin, ymax) in zip(axs, [(-30, 200), (0.6, 6)]):
y = np.linspace(ymin, ymax, x.size)
ax.set_yscale('symlog')
ax.grid(which='major')
ax.plot(x, y, '.')

fig.tight_layout()
33 changes: 25 additions & 8 deletions lib/matplotlib/scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,10 +594,17 @@ class SymmetricalLogScale(ScaleBase):
Defines the range ``(-x, x)``, within which the plot is linear.
This avoids having the plot go to infinity around zero.

subs : sequence of int
subs : None, 'auto', 'all' or sequence of int, default: 'auto'
Where to place the subticks between each major tick.
For example, in a log10 scale: ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place
8 logarithmically spaced minor ticks between each major tick.
8 logarithmically spaced minor ticks between each major tick. Both ``'auto'``
and ``'all'`` use an algorithm based on the axis view limits to determine
whether and how to put ticks between integer powers of the base. With
``'auto'``, minor ticks are placed only between integer powers; with ``'all'``,
the integer powers are included. With ``None``, minor ticks are suppressed.

.. versionchanged:: 3.12
The default value is now ``'auto'``.
Comment thread
schtandard marked this conversation as resolved.

linscale : float, optional
This allows the linear range ``(-linthresh, linthresh)`` to be
Expand All @@ -610,7 +617,7 @@ class SymmetricalLogScale(ScaleBase):
name = 'symlog'

@_make_axis_parameter_optional
def __init__(self, axis=None, *, base=10, linthresh=2, subs=None, linscale=1):
def __init__(self, axis=None, *, base=10, linthresh=2, subs='auto', linscale=1):
self._transform = SymmetricalLogTransform(base, linthresh, linscale)
self.subs = subs

Expand All @@ -620,11 +627,21 @@ def __init__(self, axis=None, *, base=10, linthresh=2, subs=None, linscale=1):

def set_default_locators_and_formatters(self, axis):
# docstring inherited
axis.set_major_locator(SymmetricalLogLocator(self.get_transform()))
axis.set_major_formatter(LogFormatterSciNotation(self.base))
axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(),
self.subs))
axis.set_minor_formatter(NullFormatter())
axis.set_major_locator(SymmetricalLogLocator(base=self.base,
linthresh=self.linthresh,
linscale=self.linscale))
axis.set_major_formatter(LogFormatterSciNotation(base=self.base,
linthresh=self.linthresh,
linscale=self.linscale))
axis.set_minor_locator(SymmetricalLogLocator(base=self.base,
linthresh=self.linthresh,
linscale=self.linscale,
subs=self.subs))
axis.set_minor_formatter(
LogFormatterSciNotation(base=self.base,
linthresh=self.linthresh,
linscale=self.linscale,
labelOnlyBase=(self.subs != 'auto')))

def get_transform(self):
"""Return the `.SymmetricalLogTransform` associated with this scale."""
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/scale.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class SymmetricalLogScale(ScaleBase):
*,
base: float = ...,
linthresh: float = ...,
subs: Iterable[int] | None = ...,
subs: Iterable[int] | Literal["auto", "all"] | None = ...,
linscale: float = ...
) -> None: ...
@property
Expand Down
Binary file modified lib/matplotlib/tests/baseline_images/test_axes/symlog.pdf
Binary file not shown.
Binary file modified lib/matplotlib/tests/baseline_images/test_axes/symlog2.pdf
Binary file not shown.
21 changes: 12 additions & 9 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,22 +1377,25 @@ def test_fill_between_interpolate_nan():
interpolate=True, alpha=0.5)


# test_symlog and test_symlog2 used to have baseline images in all three
# formats, but the png and svg baselines got invalidated by the removal of
# minor tick overstriking.
@image_comparison(['symlog.pdf'], style='mpl20')
def test_symlog():
fig, axs = plt.subplots(2, figsize=[6.4, 9.6])

x = np.array([0, 1, 2, 4, 6, 9, 12, 24])
y = np.array([1000000, 500000, 100000, 100, 5, 0, 0, 0])
axs[0].plot(x, y)
axs[0].set_yscale('symlog')
axs[0].set_xscale('linear')
axs[0].set_ylim(-1, 10000000)

fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_yscale('symlog')
ax.set_xscale('linear')
ax.set_ylim(-1, 10000000)
x = np.linspace(-25, 25, 101)
y = np.linspace(0, 2500, 101)
axs[1].plot(x, y)
axs[1].set_xscale('symlog')
axs[1].set_yscale('symlog', linthresh=1, linscale=2)


@image_comparison(['symlog2.pdf'], remove_text=True, style='_classic_test')
@image_comparison(['symlog2.pdf'], remove_text=True, style='mpl20')
def test_symlog2():
# Numbers from -50 to 50, with 0.1 as step
x = np.arange(-50, 50, 0.001)
Expand Down
58 changes: 47 additions & 11 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,45 +624,81 @@ def test_tick_values_not_exceeding_vmax(self):
class TestSymmetricalLogLocator:
def test_set_params(self):
"""
Create symmetrical log locator with default subs =[1.0] numticks = 15,
Create symmetrical log locator with default subs=[1.0] numticks='auto',
and change it to something else.
See if change was successful.
Should not exception.
"""
sym = mticker.SymmetricalLogLocator(base=10, linthresh=1)
sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, linscale=1)
sym.set_params(subs=[2.0], numticks=8)
assert sym._subs == [2.0]
assert sym.numticks == 8

def test_mixin(self):
sym = mticker.SymmetricalLogLocator(base=10, linthresh=20, linscale=3)

assert_almost_equal(sym._val2axpos(20), 3)
assert_almost_equal(sym._val2axpos(-200), -4)
assert_almost_equal(sym._val2axpos(20000000), 9)
assert_almost_equal(sym._val2axpos(0), 0)
assert_almost_equal(sym._val2axpos(1), 0.15)
assert_almost_equal(sym._val2axpos(-4), -0.6)

assert_almost_equal(sym._axpos2val(5), 2000)
assert_almost_equal(sym._axpos2val(-3.5), -63.2455532033676)
assert_almost_equal(sym._axpos2val(0.5), 3.3333333333333)

assert_almost_equal(sym._firsttickval(), 10)

assert_almost_equal(sym._val2decnum(0), 0)
assert_almost_equal(sym._val2decnum(-100), -2)
assert_almost_equal(sym._val2decnum(316.227766016838), 2.5)
assert_almost_equal(sym._val2decnum(5), 0.5)
assert_almost_equal(sym._val2decnum(20), 1.30102999566398)

assert_almost_equal(sym._decnum2val(-1), -10)
assert_almost_equal(sym._decnum2val(2), 100)
assert_almost_equal(sym._decnum2val(0.1), 1)
assert_almost_equal(sym._decnum2val(0), 0)
assert_almost_equal(sym._decnum2val(1.5), 31.6227766016838)

sym = mticker.SymmetricalLogLocator(base=10, linthresh=20, linscale=15)
assert_almost_equal(sym._firsttickval(), 1)

sym = mticker.SymmetricalLogLocator(base=10, linthresh=20, linscale=0.8)
assert_almost_equal(sym._firsttickval(), 100)

@pytest.mark.parametrize(
'vmin, vmax, expected',
[
(0, 1, [0, 1]),
(-1, 1, [-1, 0, 1]),
(0, 1, [-1, 0, 1, 10]),
(-1, 1, [-10, -1, 0, 1, 10]),
],
)
def test_values(self, vmin, vmax, expected):
# https://github.com/matplotlib/matplotlib/issues/25945
sym = mticker.SymmetricalLogLocator(base=10, linthresh=1)
sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, linscale=1)
ticks = sym.tick_values(vmin=vmin, vmax=vmax)
assert_array_equal(ticks, expected)

def test_subs(self):
sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, subs=[2.0, 4.0])
sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, linscale=1,
subs=[2.0, 4.0])
sym.create_dummy_axis()
sym.axis.set_view_interval(-10, 10)
assert_array_equal(sym(), [-20, -40, -2, -4, 0, 2, 4, 20, 40])
assert_array_equal(sym(), [-400, -200, -40, -20, -4, -2, -0.4, -0.2, -0.1,
0.1, 0.2, 0.4, 2, 4, 20, 40, 200, 400])

def test_extending(self):
sym = mticker.SymmetricalLogLocator(base=10, linthresh=1)
sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, linscale=1)
sym.create_dummy_axis()
sym.axis.set_view_interval(8, 9)
assert (sym() == [1.0]).all()
assert_array_equal(sym(), [1, 10])
sym.axis.set_view_interval(8, 12)
assert (sym() == [1.0, 10.0]).all()
assert_array_equal(sym(), [1, 10, 100])
assert sym.view_limits(10, 10) == (1, 100)
assert sym.view_limits(-10, -10) == (-100, -1)
assert sym.view_limits(0, 0) == (-0.001, 0.001)
assert sym.view_limits(0, 0) == (-1, 1)


class TestAsinhLocator:
Expand Down
Loading
Loading