Skip to content

Add asinh axis scaling (*smooth* symmetric logscale) #21178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Feb 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7ec127a
Grafted prototype arcsinh axis-scaling from stand-alone script
rwpenney Sep 26, 2021
15ff4ec
Tidied various baseclass references
rwpenney Sep 26, 2021
9680432
Added more documentation and apx_tick_count parameter
rwpenney Sep 26, 2021
3f69788
Added demo script for asinh axis scaling
rwpenney Sep 26, 2021
dd4f4d8
Tidied various flake8 transgressions
rwpenney Sep 26, 2021
1ec0b22
Improved documentation of asinh transformation and parameter naming
rwpenney Sep 30, 2021
2b0d588
Moved AsinhLocator into ticker.py and extracted AsinhScale nested tra…
rwpenney Sep 30, 2021
2002cb3
Added set_params() method to AsinhLocator
rwpenney Sep 30, 2021
070d984
Patched asinh-transforms test numpy namespace
rwpenney Sep 30, 2021
8495f7b
Fixed and refactored various AsinhScale tests
rwpenney Oct 1, 2021
279c38b
Added tests for Asinh tick locations and improved handling of zero-st…
rwpenney Oct 1, 2021
76d9b42
Improved overview documentation
rwpenney Oct 1, 2021
8143153
Improved testing of locator edge-cases, and patched constrained_layou…
rwpenney Oct 1, 2021
800ef38
Minor corrections to documentation
rwpenney Oct 3, 2021
0ad43a5
Improved handling of data ranges almost symmetrical about zero
rwpenney Oct 3, 2021
ba597de
Added AutoLocator for minor ticks and further tweaks of zero-crossing…
rwpenney Oct 3, 2021
80f2600
Patched flake8 slip-ups
rwpenney Oct 5, 2021
65eff09
Reworked AsinhLocator to allow rounding on arbitrary number base
rwpenney Oct 16, 2021
8168e56
Improved minor-tick location for common number bases and widened test…
rwpenney Oct 16, 2021
0249861
Added cross-references between symlog and asinh demo pages
rwpenney Oct 16, 2021
9fd8b0c
Added AsinhNorm for colorscale support
rwpenney Oct 16, 2021
8d7c2ad
Fixed SymlogNorm demo to actually use positive & negative values
rwpenney Oct 16, 2021
ae57d8d
Further refinements to documentation and test coverage
rwpenney Oct 16, 2021
b01d03c
Reworked SymLogNorm demonstration, and added comparision with AsinhNorm
rwpenney Oct 17, 2021
e0dcff7
Tweaked flake8 issues
rwpenney Oct 17, 2021
3d92406
Apply suggestions from code review by greglucas
rwpenney Oct 26, 2021
7bec3f4
Patched tick-generation on pan/zoom & misc. tidying
rwpenney Oct 26, 2021
97693e8
Patched overzealous zero edge-case in ticker
rwpenney Oct 26, 2021
a1af12c
Apply suggestions from code review
rwpenney Oct 31, 2021
4b23326
Merge branch 'main' into feature/asinh-scale
rwpenney Nov 20, 2021
555bac1
Patched unit-test for default base in AsinhLocator
rwpenney Nov 20, 2021
545faf2
Merge branch 'main' into feature/asinh-scale
rwpenney Feb 2, 2022
a2b210d
Added documentation comments forewarning of possible API changes
rwpenney Feb 2, 2022
8a258cf
Apply suggestions from code review
rwpenney Feb 3, 2022
5a03297
Update examples/scales/asinh_demo.py
rwpenney Feb 3, 2022
de30538
Minor repair of duplicate "experimental" annotation
rwpenney Feb 3, 2022
3e487a6
Merge branch 'main' into feature/asinh-scale
rwpenney Feb 4, 2022
c64d0c4
Tweaked indendation etc. following review by @QuLogic
rwpenney Feb 11, 2022
e43cbfd
Patched various 79-character section breaks in documentation
rwpenney Feb 12, 2022
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
1 change: 1 addition & 0 deletions doc/api/colors_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Classes
:toctree: _as_gen/
:template: autosummary.rst

AsinhNorm
BoundaryNorm
Colormap
CenteredNorm
Expand Down
32 changes: 32 additions & 0 deletions doc/users/next_whats_new/asinh_scale.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
New axis scale ``asinh`` (experimental)
---------------------------------------

The new ``asinh`` axis scale offers an alternative to ``symlog`` that
smoothly transitions between the quasi-linear and asymptotically logarithmic
regions of the scale. This is based on an arcsinh transformation that
allows plotting both positive and negative values that span many orders
of magnitude.

.. plot::

import matplotlib.pyplot as plt
import numpy as np

fig, (ax0, ax1) = plt.subplots(1, 2, sharex=True)
x = np.linspace(-3, 6, 100)

ax0.plot(x, x)
ax0.set_yscale('symlog')
ax0.grid()
ax0.set_title('symlog')

ax1.plot(x, x)
ax1.set_yscale('asinh')
ax1.grid()
ax1.set_title(r'$sinh^{-1}$')

for p in (-2, 2):
for ax in (ax0, ax1):
c = plt.Circle((p, p), radius=0.5, fill=False,
color='red', alpha=0.8, lw=3)
ax.add_patch(c)
Original file line number Diff line number Diff line change
@@ -1,42 +1,84 @@
"""
==================================
Colormap Normalizations Symlognorm
Colormap Normalizations SymLogNorm
==================================

Demonstration of using norm to map colormaps onto data in non-linear ways.

.. redirect-from:: /gallery/userdemo/colormap_normalization_symlognorm
"""

###############################################################################
# Synthetic dataset consisting of two humps, one negative and one positive,
# the positive with 8-times the amplitude.
# Linearly, the negative hump is almost invisible,
# and it is very difficult to see any detail of its profile.
# With the logarithmic scaling applied to both positive and negative values,
# it is much easier to see the shape of each hump.
#
# See `~.colors.SymLogNorm`.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as colors

"""
SymLogNorm: two humps, one negative and one positive, The positive
with 5-times the amplitude. Linearly, you cannot see detail in the
negative hump. Here we logarithmically scale the positive and
negative data separately.

Note that colorbar labels do not come out looking very good.
"""
def rbf(x, y):
return 1.0 / (1 + 5 * ((x ** 2) + (y ** 2)))

N = 100
N = 200
gain = 8
X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
Z = (Z1 - Z2) * 2
Z1 = rbf(X + 0.5, Y + 0.5)
Z2 = rbf(X - 0.5, Y - 0.5)
Z = gain * Z1 - Z2

shadeopts = {'cmap': 'PRGn', 'shading': 'gouraud'}
colormap = 'PRGn'
lnrwidth = 0.5

fig, ax = plt.subplots(2, 1)
fig, ax = plt.subplots(2, 1, sharex=True, sharey=True)

pcm = ax[0].pcolormesh(X, Y, Z,
norm=colors.SymLogNorm(linthresh=0.03, linscale=0.03,
vmin=-1.0, vmax=1.0, base=10),
cmap='RdBu_r', shading='nearest')
norm=colors.SymLogNorm(linthresh=lnrwidth, linscale=1,
vmin=-gain, vmax=gain, base=10),
**shadeopts)
fig.colorbar(pcm, ax=ax[0], extend='both')
ax[0].text(-2.5, 1.5, 'symlog')

pcm = ax[1].pcolormesh(X, Y, Z, cmap='RdBu_r', vmin=-np.max(Z),
shading='nearest')
pcm = ax[1].pcolormesh(X, Y, Z, vmin=-gain, vmax=gain,
**shadeopts)
fig.colorbar(pcm, ax=ax[1], extend='both')
ax[1].text(-2.5, 1.5, 'linear')


###############################################################################
# In order to find the best visualization for any particular dataset,
# it may be necessary to experiment with multiple different color scales.
# As well as the `~.colors.SymLogNorm` scaling, there is also
# the option of using `~.colors.AsinhNorm` (experimental), which has a smoother
# transition between the linear and logarithmic regions of the transformation
# applied to the data values, "Z".
# In the plots below, it may be possible to see contour-like artifacts
# around each hump despite there being no sharp features
# in the dataset itself. The ``asinh`` scaling shows a smoother shading
# of each hump.

fig, ax = plt.subplots(2, 1, sharex=True, sharey=True)

pcm = ax[0].pcolormesh(X, Y, Z,
norm=colors.SymLogNorm(linthresh=lnrwidth, linscale=1,
vmin=-gain, vmax=gain, base=10),
**shadeopts)
fig.colorbar(pcm, ax=ax[0], extend='both')
ax[0].text(-2.5, 1.5, 'symlog')

pcm = ax[1].pcolormesh(X, Y, Z,
norm=colors.AsinhNorm(linear_width=lnrwidth,
vmin=-gain, vmax=gain),
**shadeopts)
fig.colorbar(pcm, ax=ax[1], extend='both')
ax[1].text(-2.5, 1.5, 'asinh')


plt.show()
109 changes: 109 additions & 0 deletions examples/scales/asinh_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
============
Asinh Demo
============

Illustration of the `asinh <.scale.AsinhScale>` axis scaling,
which uses the transformation

.. math::

a \\rightarrow a_0 \\sinh^{-1} (a / a_0)

For coordinate values close to zero (i.e. much smaller than
the "linear width" :math:`a_0`), this leaves values essentially unchanged:

.. math::

a \\rightarrow a + {\\cal O}(a^3)

but for larger values (i.e. :math:`|a| \\gg a_0`, this is asymptotically

.. math::

a \\rightarrow a_0 \\, {\\rm sgn}(a) \\ln |a| + {\\cal O}(1)

As with the `symlog <.scale.SymmetricalLogScale>` scaling,
this allows one to plot quantities
that cover a very wide dynamic range that includes both positive
and negative values. However, ``symlog`` involves a transformation
that has discontinuities in its gradient because it is built
from *separate* linear and logarithmic transformations.
The ``asinh`` scaling uses a transformation that is smooth
for all (finite) values, which is both mathematically cleaner
and reduces visual artifacts associated with an abrupt
transition between linear and logarithmic regions of the plot.

.. note::
`.scale.AsinhScale` is experimental, and the API may change.

See `~.scale.AsinhScale`, `~.scale.SymmetricalLogScale`.
"""

import numpy as np
import matplotlib.pyplot as plt

# Prepare sample values for variations on y=x graph:
x = np.linspace(-3, 6, 500)

###############################################################################
# Compare "symlog" and "asinh" behaviour on sample y=x graph,
# where there is a discontinuous gradient in "symlog" near y=2:
fig1 = plt.figure()
ax0, ax1 = fig1.subplots(1, 2, sharex=True)

ax0.plot(x, x)
ax0.set_yscale('symlog')
ax0.grid()
ax0.set_title('symlog')

ax1.plot(x, x)
ax1.set_yscale('asinh')
ax1.grid()
ax1.set_title('asinh')


###############################################################################
# Compare "asinh" graphs with different scale parameter "linear_width":
fig2 = plt.figure(constrained_layout=True)
axs = fig2.subplots(1, 3, sharex=True)
for ax, (a0, base) in zip(axs, ((0.2, 2), (1.0, 0), (5.0, 10))):
ax.set_title('linear_width={:.3g}'.format(a0))
ax.plot(x, x, label='y=x')
ax.plot(x, 10*x, label='y=10x')
ax.plot(x, 100*x, label='y=100x')
ax.set_yscale('asinh', linear_width=a0, base=base)
ax.grid()
ax.legend(loc='best', fontsize='small')


###############################################################################
# Compare "symlog" and "asinh" scalings
# on 2D Cauchy-distributed random numbers,
# where one may be able to see more subtle artifacts near y=2
# due to the gradient-discontinuity in "symlog":
fig3 = plt.figure()
ax = fig3.subplots(1, 1)
r = 3 * np.tan(np.random.uniform(-np.pi / 2.02, np.pi / 2.02,
size=(5000,)))
th = np.random.uniform(0, 2*np.pi, size=r.shape)

ax.scatter(r * np.cos(th), r * np.sin(th), s=4, alpha=0.5)
ax.set_xscale('asinh')
ax.set_yscale('symlog')
ax.set_xlabel('asinh')
ax.set_ylabel('symlog')
ax.set_title('2D Cauchy random deviates')
ax.set_xlim(-50, 50)
ax.set_ylim(-50, 50)
ax.grid()

plt.show()

###############################################################################
#
# .. admonition:: References
#
# - `matplotlib.scale.AsinhScale`
# - `matplotlib.ticker.AsinhLocator`
# - `matplotlib.scale.SymmetricalLogScale`
14 changes: 14 additions & 0 deletions examples/scales/symlog_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,17 @@

fig.tight_layout()
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 disconinuities.

###############################################################################
#
# .. admonition:: References
#
# - `matplotlib.scale.SymmetricalLogScale`
# - `matplotlib.ticker.SymmetricalLogLocator`
# - `matplotlib.scale.AsinhScale`
32 changes: 32 additions & 0 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,38 @@ def linthresh(self, value):
self._scale.linthresh = value


@make_norm_from_scale(
scale.AsinhScale,
init=lambda linear_width=1, vmin=None, vmax=None, clip=False: None)
class AsinhNorm(Normalize):
"""
The inverse hyperbolic sine scale is approximately linear near
the origin, but becomes logarithmic for larger positive
or negative values. Unlike the `SymLogNorm`, the transition between
these linear and logarithmic regions is smooth, which may reduce
the risk of visual artifacts.

.. note::

This API is provisional and may be revised in the future
based on early user feedback.

Parameters
----------
linear_width : float, default: 1
The effective width of the linear region, beyond which
the transformation becomes asymptotically logarithmic
"""

@property
def linear_width(self):
return self._scale.linear_width

@linear_width.setter
def linear_width(self, value):
self._scale.linear_width = value


class PowerNorm(Normalize):
"""
Linearly map a given value to the 0-1 range and then apply
Expand Down
Loading